osi-cards-lib 1.2.0 → 1.2.1

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,11 +1,12 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, inject, NgZone, NgModule, EventEmitter, ChangeDetectorRef, Output, Input, ChangeDetectionStrategy, Component, ViewChildren, ViewChild, ElementRef } from '@angular/core';
3
- import { BehaviorSubject, Subject, fromEvent, takeUntil, interval } from 'rxjs';
2
+ import { Injectable, inject, NgZone, NgModule, EventEmitter, ChangeDetectorRef, Output, Input, ChangeDetectionStrategy, Component, PLATFORM_ID, ViewContainerRef, ViewChildren, ViewChild, ElementRef, Injector, isDevMode } from '@angular/core';
3
+ import { BehaviorSubject, Subject, takeUntil, fromEvent, interval } from 'rxjs';
4
+ import * as i1 from '@angular/common';
5
+ import { CommonModule, DOCUMENT, isPlatformBrowser, ViewportScroller } from '@angular/common';
4
6
  import * as i2 from 'lucide-angular';
5
7
  import { Zap, XCircle, Wrench, Type, Video, User, UserCheck, Users, Twitter, Trophy, TrendingUp, TrendingDown, Timer, Target, Star, Tag, Sparkles, Shield, Settings, Save, ShoppingCart, Share2, RefreshCw, Quote, PieChart, Phone, Package, Minus, Minimize2, MessageCircle, Maximize2, MapPin, Mail, List, Lightbulb, Linkedin, Instagram, Info, HelpCircle, Hash, Handshake, Grid, GitBranch, Globe, Folder, FileText, Download, DollarSign, Facebook, ExternalLink, Calculator, Code2, Clock, Circle, ChevronRight, Check, CheckCircle2, CalendarX, CalendarPlus, CalendarCheck, Calendar, Building, Briefcase, BookOpen, BarChart3, Box, Award, ArrowUp, ArrowDown, ArrowRight, AlertCircle, Activity, LucideAngularModule } from 'lucide-angular';
6
- import * as i1 from '@angular/common';
7
- import { CommonModule, ViewportScroller } from '@angular/common';
8
- import { trigger, transition, style, animate } from '@angular/animations';
8
+ import { provideAnimations, provideNoopAnimations } from '@angular/platform-browser/animations';
9
+ import { AnimationBuilder, trigger, transition, style, animate } from '@angular/animations';
9
10
 
10
11
  class CardTypeGuards {
11
12
  static isAICardConfig(obj) {
@@ -238,7 +239,7 @@ class IconService {
238
239
  return icon;
239
240
  }
240
241
  }
241
- return this.iconMap['default'];
242
+ return this.iconMap['default'] || 'circle';
242
243
  }
243
244
  getFieldIconClass(fieldName) {
244
245
  const normalizedName = fieldName.toLowerCase().replace(/[^a-z0-9]/g, '');
@@ -252,7 +253,7 @@ class IconService {
252
253
  return className;
253
254
  }
254
255
  }
255
- return this.classMap['default'];
256
+ return this.classMap['default'] || 'text-gray-500';
256
257
  }
257
258
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: IconService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
258
259
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: IconService, providedIn: 'root' }); }
@@ -505,12 +506,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
505
506
  }]
506
507
  }] });
507
508
 
508
- const MAX_LIFT_PX = 1.0; // Doubled from 0.5 for stronger tilt effect
509
- const BASE_GLOW_BLUR = 8; // Reduced from 12 - tighter glow
510
- const MAX_GLOW_BLUR_OFFSET = 4; // Reduced from 6 - less spread
511
- const BASE_GLOW_OPACITY = 0.225; // Intensified by 50% (0.15 * 1.5)
512
- const MAX_GLOW_OPACITY_OFFSET = 0.18; // Intensified by 50% (0.12 * 1.5)
513
- const MAX_REFLECTION_OPACITY = 0.22;
509
+ const MAX_LIFT_PX = 2.1; // Increased for more visible effect
510
+ const BASE_GLOW_BLUR = 10; // Increased for more visible glow
511
+ const MAX_GLOW_BLUR_OFFSET = 5; // Increased for more spread
512
+ const BASE_GLOW_OPACITY = 0.25; // Increased for more visible effect
513
+ const MAX_GLOW_OPACITY_OFFSET = 0.20; // Increased for more visible changes
514
+ const MAX_REFLECTION_OPACITY = 0.25; // Increased for more visible reflection
515
+ const SMOOTHING_FACTOR = 0.08; // Smooth interpolation that follows mouse cursor (lower = softer, less reactive)
516
+ const MAX_TILT_ANGLE = 15; // Maximum tilt angle in degrees
514
517
  class MagneticTiltService {
515
518
  constructor() {
516
519
  this.tiltCalculationsSubject = new BehaviorSubject({
@@ -526,10 +529,23 @@ class MagneticTiltService {
526
529
  this.rafId = null;
527
530
  this.pendingUpdate = null;
528
531
  this.lastCalculations = null;
529
- this.CACHE_DURATION = 100; // Recalculate rect every 100ms max
532
+ // Current smoothed values for interpolation
533
+ this.currentRotateY = 0;
534
+ this.currentRotateX = 0;
535
+ this.currentGlowBlur = BASE_GLOW_BLUR;
536
+ this.currentGlowOpacity = BASE_GLOW_OPACITY;
537
+ this.currentReflectionOpacity = 0;
538
+ // Latest target values for smoothing animation
539
+ this.targetRotateY = 0;
540
+ this.targetRotateX = 0;
541
+ this.targetGlowBlur = BASE_GLOW_BLUR;
542
+ this.targetGlowOpacity = BASE_GLOW_OPACITY;
543
+ this.targetReflectionOpacity = 0;
544
+ this.smoothingRafId = null;
545
+ this.CACHE_DURATION = 200; // Recalculate rect every 200ms max (reduces lag from getBoundingClientRect)
530
546
  this.ngZone = inject(NgZone);
531
547
  this.resetTimeoutId = null;
532
- this.RESET_TRANSITION_DURATION_MS = 500; // Smooth exit transition duration
548
+ this.RESET_TRANSITION_DURATION_MS = 800; // Even smoother exit transition
533
549
  }
534
550
  calculateTilt(mousePosition, element) {
535
551
  if (!element) {
@@ -541,9 +557,10 @@ class MagneticTiltService {
541
557
  clearTimeout(this.resetTimeoutId);
542
558
  this.resetTimeoutId = null;
543
559
  }
544
- // Store pending update for RAF batching
560
+ // Don't cancel smoothing animation - let it continue for smooth updates
561
+ // Store pending update for RAF batching (always use latest position)
545
562
  this.pendingUpdate = { mousePosition, element };
546
- // Schedule update via RAF for smooth 60fps
563
+ // Schedule update via RAF for smooth 60fps (always process latest)
547
564
  if (this.rafId === null) {
548
565
  this.rafId = requestAnimationFrame(() => {
549
566
  this.processTiltUpdate();
@@ -562,33 +579,142 @@ class MagneticTiltService {
562
579
  }
563
580
  // Get or update cached element dimensions
564
581
  const cache = this.getElementCache(element);
565
- const fx = (mousePosition.x - cache.rect.left) / cache.rect.width;
566
- const fy = (mousePosition.y - cache.rect.top) / cache.rect.height;
567
- const clampedFx = Math.max(0, Math.min(1, fx));
568
- const clampedFy = Math.max(0, Math.min(1, fy));
569
- // Optimized calculations
570
- const sinX = Math.sin(clampedFx * 2 * Math.PI);
571
- const sinY = Math.sin(clampedFy * 2 * Math.PI);
572
- const rotateY = sinX * cache.maxAngleY;
573
- const rotateX = -sinY * cache.maxAngleX;
574
- const intensity = Math.max(Math.abs(sinX), Math.abs(sinY));
575
- const glowBlur = BASE_GLOW_BLUR + intensity * MAX_GLOW_BLUR_OFFSET;
576
- const glowOpacity = BASE_GLOW_OPACITY + intensity * MAX_GLOW_OPACITY_OFFSET;
577
- const reflectionOpacity = intensity * MAX_REFLECTION_OPACITY;
582
+ // Calculate normalized position (0-1) - optimized with cached inverse width/height
583
+ // Use actual card dimensions, not screen dimensions, to handle tall cards
584
+ const invWidth = 1.0 / cache.rect.width;
585
+ const invHeight = 1.0 / cache.rect.height;
586
+ const fx = (mousePosition.x - cache.rect.left) * invWidth;
587
+ const fy = (mousePosition.y - cache.rect.top) * invHeight;
588
+ // Fast clamp: use ternary for better branch prediction
589
+ const clampedFx = fx < 0 ? 0 : fx > 1 ? 1 : fx;
590
+ const clampedFy = fy < 0 ? 0 : fy > 1 ? 1 : fy;
591
+ // Normalize to -1 to 1 range (center is 0) - used for glow effects
592
+ const normalizedX = (clampedFx - 0.5) * 2;
593
+ const normalizedY = (clampedFy - 0.5) * 2;
594
+ // Calculate tilt multiplier based on position within card (0-1 range)
595
+ // Use clamped position directly for more reliable calculation
596
+ const cardPosX = clampedFx; // 0 = left edge, 1 = right edge
597
+ const cardPosY = clampedFy; // 0 = top edge, 1 = bottom edge
598
+ // Wave function: 0 at 0%, max at 25%, 0 at 50%, -max at 75%, 0 at 100%
599
+ // Pattern: 0, 100, 0, 100, 0
600
+ const getTiltMultiplier = (pos) => {
601
+ if (pos <= 0.25) {
602
+ // 0% to 25%: increase from 0 to 1
603
+ return pos * 4; // 0 -> 1
604
+ }
605
+ else if (pos <= 0.5) {
606
+ // 25% to 50%: decrease from 1 to 0
607
+ return 1 - (pos - 0.25) * 4; // 1 -> 0
608
+ }
609
+ else if (pos <= 0.75) {
610
+ // 50% to 75%: decrease from 0 to -1
611
+ return -(pos - 0.5) * 4; // 0 -> -1
612
+ }
613
+ else {
614
+ // 75% to 100%: increase from -1 to 0
615
+ return -1 + (pos - 0.75) * 4; // -1 -> 0
616
+ }
617
+ };
618
+ const tiltMultiplierX = getTiltMultiplier(cardPosX);
619
+ const tiltMultiplierY = getTiltMultiplier(cardPosY);
620
+ // Tilt based on card position: entry: 0°, 25%: 0.5°, 50%: 0°, 75%: -0.5°, exit: 0°
621
+ // Pattern: 0, 0.5, 0, -0.5, 0 degrees (softer, more subtle effect)
622
+ // Only horizontal tilt (left to right), vertical tilt disabled
623
+ const MAX_TILT_DEGREES = 0.5;
624
+ const targetRotateY = tiltMultiplierX * MAX_TILT_DEGREES; // Horizontal tilt (left to right)
625
+ const targetRotateX = 0; // Vertical tilt disabled
626
+ // Calculate intensity based on distance from center for glow effects
627
+ const distance = Math.sqrt(normalizedX * normalizedX + normalizedY * normalizedY);
628
+ const maxDistance = Math.sqrt(2); // Maximum distance from center (corner)
629
+ const normalizedDistance = Math.min(distance / maxDistance, 1.0);
630
+ const intensity = normalizedDistance;
631
+ // Calculate target glow and reflection values
632
+ const targetGlowBlur = BASE_GLOW_BLUR + intensity * MAX_GLOW_BLUR_OFFSET;
633
+ const targetGlowOpacity = BASE_GLOW_OPACITY + intensity * MAX_GLOW_OPACITY_OFFSET;
634
+ const targetReflectionOpacity = intensity * MAX_REFLECTION_OPACITY;
635
+ // Update target values (these will be used by smoothing animation)
636
+ this.targetRotateY = targetRotateY;
637
+ this.targetRotateX = targetRotateX;
638
+ this.targetGlowBlur = targetGlowBlur;
639
+ this.targetGlowOpacity = targetGlowOpacity;
640
+ this.targetReflectionOpacity = targetReflectionOpacity;
641
+ // Apply smoothing interpolation (lerp) for smooth following
642
+ this.currentRotateY = this.lerp(this.currentRotateY, targetRotateY, SMOOTHING_FACTOR);
643
+ this.currentRotateX = this.lerp(this.currentRotateX, targetRotateX, SMOOTHING_FACTOR);
644
+ this.currentGlowBlur = this.lerp(this.currentGlowBlur, targetGlowBlur, SMOOTHING_FACTOR);
645
+ this.currentGlowOpacity = this.lerp(this.currentGlowOpacity, targetGlowOpacity, SMOOTHING_FACTOR);
646
+ this.currentReflectionOpacity = this.lerp(this.currentReflectionOpacity, targetReflectionOpacity, SMOOTHING_FACTOR);
578
647
  const newCalculations = {
579
- rotateX,
580
- rotateY,
581
- glowBlur,
582
- glowOpacity,
583
- reflectionOpacity
648
+ rotateY: this.currentRotateY,
649
+ rotateX: this.currentRotateX,
650
+ glowBlur: this.currentGlowBlur,
651
+ glowOpacity: this.currentGlowOpacity,
652
+ reflectionOpacity: this.currentReflectionOpacity
584
653
  };
585
- // Only emit if values actually changed (prevent unnecessary updates)
586
- if (!this.lastCalculations || this.hasCalculationsChanged(this.lastCalculations, newCalculations)) {
654
+ // Always emit for smooth continuous updates
655
+ this.lastCalculations = newCalculations;
656
+ // Run outside Angular zone for better performance
657
+ this.ngZone.runOutsideAngular(() => {
658
+ this.tiltCalculationsSubject.next(newCalculations);
659
+ });
660
+ // Always continue smoothing animation for smooth updates
661
+ if (this.smoothingRafId === null) {
662
+ this.smoothingRafId = requestAnimationFrame(() => {
663
+ this.continueSmoothing();
664
+ });
665
+ }
666
+ }
667
+ /**
668
+ * Linear interpolation (lerp) for smooth value transitions
669
+ * @param start Current value
670
+ * @param end Target value
671
+ * @param factor Interpolation factor (0-1)
672
+ */
673
+ lerp(start, end, factor) {
674
+ return start + (end - start) * factor;
675
+ }
676
+ /**
677
+ * Continue smoothing animation until values are close to target
678
+ * Uses the latest target values stored in the service
679
+ * Optimized for smooth cursor following
680
+ */
681
+ continueSmoothing() {
682
+ const threshold = 0.01; // Threshold for smooth updates
683
+ // Check if we need to continue smoothing using latest target values
684
+ const rotateYDiff = Math.abs(this.currentRotateY - this.targetRotateY);
685
+ const rotateXDiff = Math.abs(this.currentRotateX - this.targetRotateX);
686
+ const glowBlurDiff = Math.abs(this.currentGlowBlur - this.targetGlowBlur);
687
+ const glowOpacityDiff = Math.abs(this.currentGlowOpacity - this.targetGlowOpacity);
688
+ const reflectionDiff = Math.abs(this.currentReflectionOpacity - this.targetReflectionOpacity);
689
+ // Continue smoothing if there's any significant difference
690
+ if (rotateYDiff > threshold || rotateXDiff > threshold ||
691
+ glowBlurDiff > threshold || glowOpacityDiff > threshold ||
692
+ reflectionDiff > threshold) {
693
+ // Continue smoothing towards latest targets
694
+ this.currentRotateY = this.lerp(this.currentRotateY, this.targetRotateY, SMOOTHING_FACTOR);
695
+ this.currentRotateX = this.lerp(this.currentRotateX, this.targetRotateX, SMOOTHING_FACTOR);
696
+ this.currentGlowBlur = this.lerp(this.currentGlowBlur, this.targetGlowBlur, SMOOTHING_FACTOR);
697
+ this.currentGlowOpacity = this.lerp(this.currentGlowOpacity, this.targetGlowOpacity, SMOOTHING_FACTOR);
698
+ this.currentReflectionOpacity = this.lerp(this.currentReflectionOpacity, this.targetReflectionOpacity, SMOOTHING_FACTOR);
699
+ const newCalculations = {
700
+ rotateY: this.currentRotateY,
701
+ rotateX: this.currentRotateX,
702
+ glowBlur: this.currentGlowBlur,
703
+ glowOpacity: this.currentGlowOpacity,
704
+ reflectionOpacity: this.currentReflectionOpacity
705
+ };
587
706
  this.lastCalculations = newCalculations;
588
- // Run outside Angular zone for better performance
589
707
  this.ngZone.runOutsideAngular(() => {
590
708
  this.tiltCalculationsSubject.next(newCalculations);
591
709
  });
710
+ // Continue animation for smooth updates
711
+ this.smoothingRafId = requestAnimationFrame(() => {
712
+ this.continueSmoothing();
713
+ });
714
+ }
715
+ else {
716
+ // Very close to target, stop animation
717
+ this.smoothingRafId = null;
592
718
  }
593
719
  }
594
720
  getElementCache(element) {
@@ -600,17 +726,13 @@ class MagneticTiltService {
600
726
  }
601
727
  // Recalculate and cache
602
728
  const rect = element.getBoundingClientRect();
603
- const halfW = rect.width / 2;
604
- const halfH = rect.height / 2;
605
- const maxAngleY = Math.asin(MAX_LIFT_PX / halfW) * (180 / Math.PI);
606
- const maxAngleX = Math.asin(MAX_LIFT_PX / halfH) * (180 / Math.PI);
607
729
  const cache = {
608
730
  element,
609
731
  rect,
610
- halfW,
611
- halfH,
612
- maxAngleY,
613
- maxAngleX,
732
+ halfW: rect.width / 2,
733
+ halfH: rect.height / 2,
734
+ maxAngleY: MAX_TILT_ANGLE, // Use constant max angle for consistent behavior
735
+ maxAngleX: MAX_TILT_ANGLE, // Use constant max angle for consistent behavior
614
736
  lastUpdate: now
615
737
  };
616
738
  this.elementCache.set(element, cache);
@@ -632,6 +754,11 @@ class MagneticTiltService {
632
754
  this.rafId = null;
633
755
  }
634
756
  this.pendingUpdate = null;
757
+ // Cancel smoothing animation
758
+ if (this.smoothingRafId !== null) {
759
+ cancelAnimationFrame(this.smoothingRafId);
760
+ this.smoothingRafId = null;
761
+ }
635
762
  // Clear any existing reset timeout
636
763
  if (this.resetTimeoutId !== null) {
637
764
  clearTimeout(this.resetTimeoutId);
@@ -639,41 +766,55 @@ class MagneticTiltService {
639
766
  }
640
767
  if (smooth) {
641
768
  // Smooth reset: gradually transition to zero over the transition duration
642
- // This allows the CSS transition to complete smoothly even if mouse leaves quickly
769
+ // Use RAF for smooth 60fps animation
643
770
  const startTime = performance.now();
644
- const startCalculations = this.lastCalculations || {
645
- rotateX: 0,
646
- rotateY: 0,
647
- glowBlur: BASE_GLOW_BLUR,
648
- glowOpacity: BASE_GLOW_OPACITY,
649
- reflectionOpacity: 0
650
- };
771
+ const startRotateY = this.currentRotateY;
772
+ const startRotateX = this.currentRotateX;
773
+ const startGlowBlur = this.currentGlowBlur;
774
+ const startGlowOpacity = this.currentGlowOpacity;
775
+ const startReflectionOpacity = this.currentReflectionOpacity;
651
776
  const animateReset = () => {
652
777
  const elapsed = performance.now() - startTime;
653
778
  const progress = Math.min(elapsed / this.RESET_TRANSITION_DURATION_MS, 1);
654
- // Ease-out function for smooth deceleration
655
- const easeOut = 1 - Math.pow(1 - progress, 3);
779
+ // Optimized cubic ease-out - avoid Math.pow() for better performance
780
+ const t = 1 - progress;
781
+ const easeOut = 1 - (t * t * t); // t³ instead of Math.pow(t, 3)
782
+ this.currentRotateY = startRotateY * (1 - easeOut);
783
+ this.currentRotateX = startRotateX * (1 - easeOut);
784
+ this.currentGlowBlur = BASE_GLOW_BLUR + (startGlowBlur - BASE_GLOW_BLUR) * (1 - easeOut);
785
+ this.currentGlowOpacity = BASE_GLOW_OPACITY + (startGlowOpacity - BASE_GLOW_OPACITY) * (1 - easeOut);
786
+ this.currentReflectionOpacity = startReflectionOpacity * (1 - easeOut);
656
787
  const currentCalculations = {
657
- rotateX: startCalculations.rotateX * (1 - easeOut),
658
- rotateY: startCalculations.rotateY * (1 - easeOut),
659
- glowBlur: BASE_GLOW_BLUR + (startCalculations.glowBlur - BASE_GLOW_BLUR) * (1 - easeOut),
660
- glowOpacity: BASE_GLOW_OPACITY + (startCalculations.glowOpacity - BASE_GLOW_OPACITY) * (1 - easeOut),
661
- reflectionOpacity: startCalculations.reflectionOpacity * (1 - easeOut)
788
+ rotateY: this.currentRotateY,
789
+ rotateX: this.currentRotateX,
790
+ glowBlur: this.currentGlowBlur,
791
+ glowOpacity: this.currentGlowOpacity,
792
+ reflectionOpacity: this.currentReflectionOpacity
662
793
  };
663
794
  this.lastCalculations = currentCalculations;
664
795
  this.ngZone.runOutsideAngular(() => {
665
796
  this.tiltCalculationsSubject.next(currentCalculations);
666
797
  });
667
798
  if (progress < 1) {
668
- // Continue animation
669
- this.resetTimeoutId = window.setTimeout(animateReset, 16); // ~60fps
799
+ // Continue animation using RAF for smooth 60fps
800
+ this.resetTimeoutId = window.setTimeout(animateReset, 16);
670
801
  }
671
802
  else {
672
803
  // Animation complete - set final values
804
+ this.currentRotateY = 0;
805
+ this.currentRotateX = 0;
806
+ this.currentGlowBlur = BASE_GLOW_BLUR;
807
+ this.currentGlowOpacity = BASE_GLOW_OPACITY;
808
+ this.currentReflectionOpacity = 0;
809
+ this.targetRotateY = 0;
810
+ this.targetRotateX = 0;
811
+ this.targetGlowBlur = BASE_GLOW_BLUR;
812
+ this.targetGlowOpacity = BASE_GLOW_OPACITY;
813
+ this.targetReflectionOpacity = 0;
673
814
  this.lastCalculations = null;
674
815
  this.tiltCalculationsSubject.next({
675
- rotateX: 0,
676
816
  rotateY: 0,
817
+ rotateX: 0,
677
818
  glowBlur: BASE_GLOW_BLUR,
678
819
  glowOpacity: BASE_GLOW_OPACITY,
679
820
  reflectionOpacity: 0
@@ -681,15 +822,25 @@ class MagneticTiltService {
681
822
  this.resetTimeoutId = null;
682
823
  }
683
824
  };
684
- // Start smooth reset animation
825
+ // Start smooth reset animation using RAF
685
826
  animateReset();
686
827
  }
687
828
  else {
688
829
  // Immediate reset (for cleanup)
830
+ this.currentRotateY = 0;
831
+ this.currentRotateX = 0;
832
+ this.currentGlowBlur = BASE_GLOW_BLUR;
833
+ this.currentGlowOpacity = BASE_GLOW_OPACITY;
834
+ this.currentReflectionOpacity = 0;
835
+ this.targetRotateY = 0;
836
+ this.targetRotateX = 0;
837
+ this.targetGlowBlur = BASE_GLOW_BLUR;
838
+ this.targetGlowOpacity = BASE_GLOW_OPACITY;
839
+ this.targetReflectionOpacity = 0;
689
840
  this.lastCalculations = null;
690
841
  this.tiltCalculationsSubject.next({
691
- rotateX: 0,
692
842
  rotateY: 0,
843
+ rotateX: 0,
693
844
  glowBlur: BASE_GLOW_BLUR,
694
845
  glowOpacity: BASE_GLOW_OPACITY,
695
846
  reflectionOpacity: 0
@@ -715,6 +866,10 @@ class MagneticTiltService {
715
866
  cancelAnimationFrame(this.rafId);
716
867
  this.rafId = null;
717
868
  }
869
+ if (this.smoothingRafId !== null) {
870
+ cancelAnimationFrame(this.smoothingRafId);
871
+ this.smoothingRafId = null;
872
+ }
718
873
  }
719
874
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: MagneticTiltService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
720
875
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: MagneticTiltService, providedIn: 'root' }); }
@@ -846,764 +1001,2492 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
846
1001
  }]
847
1002
  }] });
848
1003
 
849
- /**
850
- * Simple hash function for content hashing (replaces JSON.stringify)
851
- * Uses MurmurHash-inspired algorithm for fast hashing
852
- */
853
- function hashString(str) {
854
- let hash = 0;
855
- if (str.length === 0)
856
- return hash;
857
- for (let i = 0; i < str.length; i++) {
858
- const char = str.charCodeAt(i);
859
- hash = ((hash << 5) - hash) + char;
860
- hash = hash & hash; // Convert to 32-bit integer
861
- }
862
- return hash;
863
- }
864
- /**
865
- * Generate content hash for a field (faster than JSON.stringify)
866
- */
867
- function hashField(field) {
868
- const key = `${field.id || ''}|${field.label || ''}|${field.value || ''}|${field.type || ''}|${field.title || ''}`;
869
- return String(hashString(key));
870
- }
871
- /**
872
- * Generate content hash for an item (faster than JSON.stringify)
873
- */
874
- function hashItem(item) {
875
- const key = `${item.id || ''}|${item.title || ''}|${item.value || ''}`;
876
- return String(hashString(key));
1004
+ const ICONS = {
1005
+ Activity,
1006
+ AlertCircle,
1007
+ ArrowRight,
1008
+ ArrowDown,
1009
+ ArrowUp,
1010
+ Award,
1011
+ Box,
1012
+ BarChart3,
1013
+ BookOpen,
1014
+ Briefcase,
1015
+ Building,
1016
+ Calendar,
1017
+ CalendarCheck,
1018
+ CalendarPlus,
1019
+ CalendarX,
1020
+ CheckCircle2,
1021
+ Check,
1022
+ ChevronRight,
1023
+ Circle,
1024
+ Clock,
1025
+ Code2,
1026
+ Calculator,
1027
+ ExternalLink,
1028
+ Facebook,
1029
+ DollarSign,
1030
+ Download,
1031
+ FileText,
1032
+ Folder,
1033
+ Globe,
1034
+ GitBranch,
1035
+ Grid,
1036
+ Handshake,
1037
+ Hash,
1038
+ HelpCircle,
1039
+ Info,
1040
+ Instagram,
1041
+ Linkedin,
1042
+ Lightbulb,
1043
+ List,
1044
+ Mail,
1045
+ MapPin,
1046
+ Maximize2,
1047
+ MessageCircle,
1048
+ Minimize2,
1049
+ Minus,
1050
+ Package,
1051
+ Phone,
1052
+ PieChart,
1053
+ Quote,
1054
+ RefreshCw,
1055
+ Share2,
1056
+ ShoppingCart,
1057
+ Save,
1058
+ Settings,
1059
+ Shield,
1060
+ Sparkles,
1061
+ Tag,
1062
+ Star,
1063
+ Target,
1064
+ Timer,
1065
+ TrendingDown,
1066
+ TrendingUp,
1067
+ Trophy,
1068
+ Twitter,
1069
+ Users,
1070
+ UserCheck,
1071
+ User,
1072
+ Video,
1073
+ Type,
1074
+ Wrench,
1075
+ XCircle,
1076
+ Zap
1077
+ };
1078
+ class LucideIconsModule {
1079
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: LucideIconsModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
1080
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.14", ngImport: i0, type: LucideIconsModule, imports: [i2.LucideAngularModule], exports: [LucideAngularModule] }); }
1081
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: LucideIconsModule, imports: [LucideAngularModule.pick(ICONS), LucideAngularModule] }); }
877
1082
  }
1083
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: LucideIconsModule, decorators: [{
1084
+ type: NgModule,
1085
+ args: [{
1086
+ imports: [LucideAngularModule.pick(ICONS)],
1087
+ exports: [LucideAngularModule]
1088
+ }]
1089
+ }] });
1090
+
878
1091
  /**
879
- * WeakMap cache for field hashes to avoid recomputation
880
- */
881
- const fieldHashCache = new WeakMap();
882
- const itemHashCache = new WeakMap();
883
- /**
884
- * Deep comparison utility for card objects
885
- * Uses content hashing instead of JSON.stringify for better performance
1092
+ * Base component class for all section components
1093
+ * Provides common functionality and ensures consistency
886
1094
  */
887
- class CardDiffUtil {
1095
+ class BaseSectionComponent {
1096
+ constructor() {
1097
+ this.fieldInteraction = new EventEmitter();
1098
+ this.itemInteraction = new EventEmitter();
1099
+ this.cdr = inject(ChangeDetectorRef);
1100
+ // Animation state tracking
1101
+ this.fieldAnimationStates = new Map();
1102
+ this.itemAnimationStates = new Map();
1103
+ this.fieldAnimationTimes = new Map();
1104
+ this.itemAnimationTimes = new Map();
1105
+ this.FIELD_STAGGER_DELAY_MS = 30;
1106
+ this.ITEM_STAGGER_DELAY_MS = 40;
1107
+ this.FIELD_ANIMATION_DURATION_MS = 300;
1108
+ this.ITEM_ANIMATION_DURATION_MS = 350;
1109
+ this.fieldsAnimated = false;
1110
+ this.itemsAnimated = false;
1111
+ // Performance: Batch change detection for animation state updates
1112
+ this.pendingFieldAnimationUpdates = new Set();
1113
+ this.pendingItemAnimationUpdates = new Set();
1114
+ this.fieldAnimationUpdateRafId = null;
1115
+ this.itemAnimationUpdateRafId = null;
1116
+ }
888
1117
  /**
889
- * Creates an updated card with only changed sections/fields updated
890
- * Preserves references to unchanged sections for optimal performance
1118
+ * Get fields from section (standardized access pattern)
891
1119
  */
892
- static mergeCardUpdates(oldCard, newCard) {
893
- // If cards are identical, return old card (preserve reference)
894
- if (this.areCardsEqual(oldCard, newCard)) {
895
- return { card: oldCard, changeType: 'content' };
896
- }
897
- // Check if only top-level properties changed (title, subtitle, etc.)
898
- // Check if sections array changed
899
- const sectionsChanged = !this.areSectionsEqual(oldCard.sections, newCard.sections);
900
- // If only top-level changed, update only those
901
- if (!sectionsChanged) {
902
- return {
903
- card: {
904
- ...oldCard,
905
- cardTitle: newCard.cardTitle,
906
- cardSubtitle: newCard.cardSubtitle,
907
- cardType: newCard.cardType,
908
- description: newCard.description,
909
- columns: newCard.columns,
910
- actions: newCard.actions,
911
- // Keep same sections reference
912
- sections: oldCard.sections
913
- },
914
- changeType: 'content'
915
- };
916
- }
917
- // Merge sections incrementally
918
- const mergedSections = this.mergeSections(oldCard.sections, newCard.sections);
919
- const changeType = sectionsChanged && !this.didStructureChange(oldCard.sections, newCard.sections)
920
- ? 'content'
921
- : 'structural';
922
- return {
923
- card: {
924
- ...oldCard,
925
- cardTitle: newCard.cardTitle,
926
- cardSubtitle: newCard.cardSubtitle,
927
- cardType: newCard.cardType,
928
- description: newCard.description,
929
- columns: newCard.columns,
930
- actions: newCard.actions,
931
- sections: mergedSections
932
- },
933
- changeType
934
- };
935
- }
936
- static didStructureChange(oldSections, newSections) {
937
- if (oldSections.length !== newSections.length) {
938
- return true;
939
- }
940
- return oldSections.some((oldSection, index) => {
941
- const newSection = newSections[index];
942
- if (!newSection) {
943
- return true;
944
- }
945
- if ((oldSection.id || index) !== (newSection.id || index)) {
946
- return true;
947
- }
948
- if (oldSection.type !== newSection.type) {
949
- return true;
950
- }
951
- const oldFieldsLength = oldSection.fields?.length ?? 0;
952
- const newFieldsLength = newSection.fields?.length ?? 0;
953
- const oldItemsLength = oldSection.items?.length ?? 0;
954
- const newItemsLength = newSection.items?.length ?? 0;
955
- return oldFieldsLength !== newFieldsLength || newItemsLength !== oldItemsLength;
956
- });
1120
+ getFields() {
1121
+ return this.section.fields ?? [];
957
1122
  }
958
1123
  /**
959
- * Merges sections array, preserving references to unchanged sections
1124
+ * Get items from section (standardized access pattern)
1125
+ * Falls back to fields if items are not available
960
1126
  */
961
- static mergeSections(oldSections, newSections) {
962
- // If sections array length changed, we need to rebuild
963
- if (oldSections.length !== newSections.length) {
964
- return newSections.map((section, index) => {
965
- const oldSection = oldSections[index];
966
- if (oldSection && this.areSectionsEqual([oldSection], [section])) {
967
- return oldSection; // Preserve reference
968
- }
969
- return section;
1127
+ getItems() {
1128
+ if (Array.isArray(this.section.items) && this.section.items.length > 0) {
1129
+ return this.section.items;
1130
+ }
1131
+ // Fallback to fields if items are not available
1132
+ if (Array.isArray(this.section.fields) && this.section.fields.length > 0) {
1133
+ return this.section.fields.map((field) => {
1134
+ const cardField = field;
1135
+ return {
1136
+ ...cardField,
1137
+ title: cardField.title ?? cardField.label ?? cardField.id,
1138
+ description: cardField.description ?? (typeof cardField.meta?.['description'] === 'string'
1139
+ ? cardField.meta['description']
1140
+ : undefined)
1141
+ };
970
1142
  });
971
1143
  }
972
- // Merge each section
973
- return newSections.map((newSection, index) => {
974
- const oldSection = oldSections[index];
975
- if (!oldSection) {
976
- return newSection;
1144
+ return [];
1145
+ }
1146
+ ngOnChanges(changes) {
1147
+ if (changes['section']) {
1148
+ // Cancel pending RAFs
1149
+ if (this.fieldAnimationUpdateRafId !== null) {
1150
+ cancelAnimationFrame(this.fieldAnimationUpdateRafId);
1151
+ this.fieldAnimationUpdateRafId = null;
977
1152
  }
978
- if ((oldSection.id || index) !== (newSection.id || index)) {
979
- return newSection;
1153
+ if (this.itemAnimationUpdateRafId !== null) {
1154
+ cancelAnimationFrame(this.itemAnimationUpdateRafId);
1155
+ this.itemAnimationUpdateRafId = null;
980
1156
  }
981
- // Merge section fields/items
982
- return this.mergeSection(oldSection, newSection);
983
- });
1157
+ // Reset animation states when section changes
1158
+ this.resetFieldAnimations();
1159
+ this.resetItemAnimations();
1160
+ this.fieldsAnimated = false;
1161
+ this.itemsAnimated = false;
1162
+ // Clear pending updates
1163
+ this.pendingFieldAnimationUpdates.clear();
1164
+ this.pendingItemAnimationUpdates.clear();
1165
+ }
984
1166
  }
985
1167
  /**
986
- * Merges a single section, preserving references to unchanged fields/items
1168
+ * Get animation class for a field based on its appearance state
987
1169
  */
988
- static mergeSection(oldSection, newSection) {
989
- // Check if only top-level section properties changed
990
- // Check if fields changed
991
- const fieldsChanged = !this.areFieldsEqual(oldSection.fields, newSection.fields);
992
- const itemsChanged = !this.areItemsEqual(oldSection.items, newSection.items);
993
- // If only top-level changed, preserve fields/items references
994
- if (!fieldsChanged && !itemsChanged) {
995
- return {
996
- ...oldSection,
997
- title: newSection.title,
998
- type: newSection.type,
999
- description: newSection.description,
1000
- subtitle: newSection.subtitle,
1001
- columns: newSection.columns,
1002
- colSpan: newSection.colSpan,
1003
- collapsed: newSection.collapsed,
1004
- emoji: newSection.emoji,
1005
- chartType: newSection.chartType,
1006
- chartData: newSection.chartData,
1007
- meta: newSection.meta,
1008
- // Preserve fields/items references
1009
- fields: oldSection.fields,
1010
- items: oldSection.items
1011
- };
1170
+ getFieldAnimationClass(fieldId, index) {
1171
+ const state = this.fieldAnimationStates.get(fieldId);
1172
+ if (state === 'entering') {
1173
+ return 'field-streaming';
1012
1174
  }
1013
- // Merge fields if they exist
1014
- const mergedFields = oldSection.fields && newSection.fields
1015
- ? this.mergeFields(oldSection.fields, newSection.fields)
1016
- : newSection.fields;
1017
- // Merge items if they exist
1018
- const mergedItems = oldSection.items && newSection.items
1019
- ? this.mergeItems(oldSection.items, newSection.items)
1020
- : newSection.items;
1021
- return {
1022
- ...oldSection,
1023
- title: newSection.title,
1024
- type: newSection.type,
1025
- description: newSection.description,
1026
- subtitle: newSection.subtitle,
1027
- columns: newSection.columns,
1028
- colSpan: newSection.colSpan,
1029
- collapsed: newSection.collapsed,
1030
- emoji: newSection.emoji,
1031
- chartType: newSection.chartType,
1032
- chartData: newSection.chartData,
1033
- meta: newSection.meta,
1034
- fields: mergedFields,
1035
- items: mergedItems
1036
- };
1175
+ if (state === 'entered') {
1176
+ return 'field-entered';
1177
+ }
1178
+ // New field - mark as entering
1179
+ if (state === undefined || state === 'none') {
1180
+ this.markFieldEntering(fieldId, index);
1181
+ return 'field-streaming';
1182
+ }
1183
+ return '';
1037
1184
  }
1038
1185
  /**
1039
- * Merges fields array, preserving references to unchanged fields
1040
- * Uses content hashing instead of JSON.stringify for better performance
1186
+ * Get animation class for an item based on its appearance state
1041
1187
  */
1042
- static mergeFields(oldFields, newFields) {
1043
- if (oldFields.length !== newFields.length) {
1044
- return newFields;
1188
+ getItemAnimationClass(itemId, index) {
1189
+ const state = this.itemAnimationStates.get(itemId);
1190
+ if (state === 'entering') {
1191
+ return 'item-streaming';
1045
1192
  }
1046
- return newFields.map((newField, index) => {
1047
- const oldField = oldFields[index];
1048
- if (!oldField) {
1049
- return newField;
1050
- }
1051
- // Fast comparison: check key properties first
1052
- if (oldField.id === newField.id &&
1053
- oldField.label === newField.label &&
1054
- oldField.value === newField.value &&
1055
- oldField.title === newField.title) {
1056
- // Use content hashing instead of JSON.stringify
1057
- const oldHash = fieldHashCache.get(oldField) || hashField(oldField);
1058
- const newHash = hashField(newField);
1059
- // Cache hashes for future comparisons
1060
- if (!fieldHashCache.has(oldField)) {
1061
- fieldHashCache.set(oldField, oldHash);
1062
- }
1063
- if (!fieldHashCache.has(newField)) {
1064
- fieldHashCache.set(newField, newHash);
1065
- }
1066
- if (oldHash === newHash) {
1067
- return oldField; // Preserve reference
1068
- }
1069
- }
1070
- return newField;
1071
- });
1193
+ if (state === 'entered') {
1194
+ return 'item-entered';
1195
+ }
1196
+ // New item - mark as entering
1197
+ if (state === undefined || state === 'none') {
1198
+ this.markItemEntering(itemId, index);
1199
+ return 'item-streaming';
1200
+ }
1201
+ return '';
1072
1202
  }
1073
1203
  /**
1074
- * Merges items array, preserving references to unchanged items
1075
- * Uses content hashing instead of JSON.stringify for better performance
1204
+ * Get stagger delay index for field animation
1076
1205
  */
1077
- static mergeItems(oldItems, newItems) {
1078
- if (oldItems.length !== newItems.length) {
1079
- return newItems;
1080
- }
1081
- return newItems.map((newItem, index) => {
1082
- const oldItem = oldItems[index];
1083
- if (!oldItem) {
1084
- return newItem;
1085
- }
1086
- // Fast comparison
1087
- if (oldItem.id === newItem.id &&
1088
- oldItem.title === newItem.title &&
1089
- oldItem.value === newItem.value) {
1090
- // Use content hashing instead of JSON.stringify
1091
- const oldHash = itemHashCache.get(oldItem) || hashItem(oldItem);
1092
- const newHash = hashItem(newItem);
1093
- // Cache hashes for future comparisons
1094
- if (!itemHashCache.has(oldItem)) {
1095
- itemHashCache.set(oldItem, oldHash);
1096
- }
1097
- if (!itemHashCache.has(newItem)) {
1098
- itemHashCache.set(newItem, newHash);
1099
- }
1100
- if (oldHash === newHash) {
1101
- return oldItem; // Preserve reference
1102
- }
1103
- }
1104
- return newItem;
1105
- });
1206
+ getFieldStaggerIndex(index) {
1207
+ return Math.min(index, 15);
1106
1208
  }
1107
1209
  /**
1108
- * Fast equality check for cards
1210
+ * Get stagger delay index for item animation
1109
1211
  */
1110
- static areCardsEqual(card1, card2) {
1111
- return card1.id === card2.id &&
1112
- card1.cardTitle === card2.cardTitle &&
1113
- card1.cardSubtitle === card2.cardSubtitle &&
1114
- card1.cardType === card2.cardType &&
1115
- this.areSectionsEqual(card1.sections, card2.sections);
1212
+ getItemStaggerIndex(index) {
1213
+ return Math.min(index, 15);
1116
1214
  }
1117
1215
  /**
1118
- * Fast equality check for sections arrays
1216
+ * Mark field as entering and schedule entered state
1217
+ * Optimized: Batches change detection for better performance
1119
1218
  */
1120
- static areSectionsEqual(sections1, sections2) {
1121
- if (sections1.length !== sections2.length) {
1122
- return false;
1219
+ markFieldEntering(fieldId, index) {
1220
+ this.fieldAnimationStates.set(fieldId, 'entering');
1221
+ const appearanceTime = Date.now();
1222
+ this.fieldAnimationTimes.set(fieldId, appearanceTime);
1223
+ // Calculate total delay (stagger + animation duration)
1224
+ const staggerDelay = index * this.FIELD_STAGGER_DELAY_MS;
1225
+ const totalDelay = staggerDelay + this.FIELD_ANIMATION_DURATION_MS;
1226
+ // Mark as entered after animation completes
1227
+ // Batch change detection for multiple fields
1228
+ setTimeout(() => {
1229
+ // Only update if this is still the latest appearance
1230
+ if (this.fieldAnimationTimes.get(fieldId) === appearanceTime) {
1231
+ this.fieldAnimationStates.set(fieldId, 'entered');
1232
+ // Batch change detection - add to pending updates
1233
+ this.pendingFieldAnimationUpdates.add(fieldId);
1234
+ this.scheduleBatchedFieldChangeDetection();
1235
+ }
1236
+ }, totalDelay);
1237
+ }
1238
+ /**
1239
+ * Batch change detection for field animation state updates
1240
+ */
1241
+ scheduleBatchedFieldChangeDetection() {
1242
+ if (this.fieldAnimationUpdateRafId !== null) {
1243
+ return; // Already scheduled
1123
1244
  }
1124
- return sections1.every((section1, index) => {
1125
- const section2 = sections2[index];
1126
- if (!section2)
1127
- return false;
1128
- return section1.id === section2.id &&
1129
- section1.title === section2.title &&
1130
- section1.type === section2.type &&
1131
- this.areFieldsEqual(section1.fields, section2.fields) &&
1132
- this.areItemsEqual(section1.items, section2.items);
1245
+ this.fieldAnimationUpdateRafId = requestAnimationFrame(() => {
1246
+ if (this.pendingFieldAnimationUpdates.size > 0) {
1247
+ // Single change detection for all pending updates
1248
+ this.cdr.markForCheck();
1249
+ this.pendingFieldAnimationUpdates.clear();
1250
+ }
1251
+ this.fieldAnimationUpdateRafId = null;
1133
1252
  });
1134
1253
  }
1135
1254
  /**
1136
- * Fast equality check for fields arrays
1255
+ * Mark item as entering and schedule entered state
1256
+ * Optimized: Batches change detection for better performance
1137
1257
  */
1138
- static areFieldsEqual(fields1, fields2) {
1139
- if (!fields1 && !fields2)
1140
- return true;
1141
- if (!fields1 || !fields2)
1142
- return false;
1143
- if (fields1.length !== fields2.length)
1144
- return false;
1145
- return fields1.every((field1, index) => {
1146
- const field2 = fields2[index];
1147
- if (!field2)
1148
- return false;
1149
- return field1.id === field2.id &&
1150
- field1.label === field2.label &&
1151
- field1.value === field2.value &&
1152
- field1.title === field2.title;
1153
- });
1258
+ markItemEntering(itemId, index) {
1259
+ this.itemAnimationStates.set(itemId, 'entering');
1260
+ const appearanceTime = Date.now();
1261
+ this.itemAnimationTimes.set(itemId, appearanceTime);
1262
+ // Calculate total delay (stagger + animation duration)
1263
+ const staggerDelay = index * this.ITEM_STAGGER_DELAY_MS;
1264
+ const totalDelay = staggerDelay + this.ITEM_ANIMATION_DURATION_MS;
1265
+ // Mark as entered after animation completes
1266
+ // Batch change detection for multiple items
1267
+ setTimeout(() => {
1268
+ // Only update if this is still the latest appearance
1269
+ if (this.itemAnimationTimes.get(itemId) === appearanceTime) {
1270
+ this.itemAnimationStates.set(itemId, 'entered');
1271
+ // Batch change detection - add to pending updates
1272
+ this.pendingItemAnimationUpdates.add(itemId);
1273
+ this.scheduleBatchedItemChangeDetection();
1274
+ }
1275
+ }, totalDelay);
1154
1276
  }
1155
1277
  /**
1156
- * Fast equality check for items arrays
1278
+ * Batch change detection for item animation state updates
1157
1279
  */
1158
- static areItemsEqual(items1, items2) {
1159
- if (!items1 && !items2)
1160
- return true;
1161
- if (!items1 || !items2)
1162
- return false;
1163
- if (items1.length !== items2.length)
1164
- return false;
1165
- return items1.every((item1, index) => {
1166
- const item2 = items2[index];
1167
- if (!item2)
1168
- return false;
1169
- return item1.id === item2.id &&
1170
- item1.title === item2.title &&
1171
- item1.value === item2.value;
1280
+ scheduleBatchedItemChangeDetection() {
1281
+ if (this.itemAnimationUpdateRafId !== null) {
1282
+ return; // Already scheduled
1283
+ }
1284
+ this.itemAnimationUpdateRafId = requestAnimationFrame(() => {
1285
+ if (this.pendingItemAnimationUpdates.size > 0) {
1286
+ // Single change detection for all pending updates
1287
+ this.cdr.markForCheck();
1288
+ this.pendingItemAnimationUpdates.clear();
1289
+ }
1290
+ this.itemAnimationUpdateRafId = null;
1172
1291
  });
1173
1292
  }
1174
- }
1175
-
1176
- function getBreakpointFromWidth(width) {
1177
- if (width < 640)
1178
- return 'xs';
1179
- if (width < 768)
1180
- return 'sm';
1181
- if (width < 1024)
1182
- return 'md';
1183
- if (width < 1280)
1184
- return 'lg';
1185
- if (width < 1536)
1186
- return 'xl';
1187
- return '2xl';
1188
- }
1189
-
1190
- const ICONS = {
1191
- Activity,
1192
- AlertCircle,
1193
- ArrowRight,
1194
- ArrowDown,
1195
- ArrowUp,
1196
- Award,
1197
- Box,
1198
- BarChart3,
1199
- BookOpen,
1200
- Briefcase,
1201
- Building,
1202
- Calendar,
1203
- CalendarCheck,
1204
- CalendarPlus,
1205
- CalendarX,
1206
- CheckCircle2,
1207
- Check,
1208
- ChevronRight,
1209
- Circle,
1210
- Clock,
1211
- Code2,
1212
- Calculator,
1213
- ExternalLink,
1214
- Facebook,
1215
- DollarSign,
1216
- Download,
1217
- FileText,
1218
- Folder,
1219
- Globe,
1220
- GitBranch,
1221
- Grid,
1222
- Handshake,
1223
- Hash,
1224
- HelpCircle,
1225
- Info,
1226
- Instagram,
1227
- Linkedin,
1228
- Lightbulb,
1229
- List,
1230
- Mail,
1231
- MapPin,
1232
- Maximize2,
1233
- MessageCircle,
1234
- Minimize2,
1235
- Minus,
1236
- Package,
1237
- Phone,
1238
- PieChart,
1239
- Quote,
1240
- RefreshCw,
1241
- Share2,
1242
- ShoppingCart,
1243
- Save,
1244
- Settings,
1245
- Shield,
1246
- Sparkles,
1247
- Tag,
1248
- Star,
1249
- Target,
1250
- Timer,
1251
- TrendingDown,
1252
- TrendingUp,
1253
- Trophy,
1254
- Twitter,
1255
- Users,
1256
- UserCheck,
1257
- User,
1258
- Video,
1259
- Type,
1260
- Wrench,
1261
- XCircle,
1262
- Zap
1263
- };
1264
- class LucideIconsModule {
1265
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: LucideIconsModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
1266
- static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.14", ngImport: i0, type: LucideIconsModule, imports: [i2.LucideAngularModule], exports: [LucideAngularModule] }); }
1267
- static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: LucideIconsModule, imports: [LucideAngularModule.pick(ICONS), LucideAngularModule] }); }
1268
- }
1269
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: LucideIconsModule, decorators: [{
1270
- type: NgModule,
1271
- args: [{
1272
- imports: [LucideAngularModule.pick(ICONS)],
1273
- exports: [LucideAngularModule]
1274
- }]
1275
- }] });
1276
-
1277
- /**
1278
- * Base component class for all section components
1279
- * Provides common functionality and ensures consistency
1280
- */
1281
- class BaseSectionComponent {
1282
- constructor() {
1283
- this.fieldInteraction = new EventEmitter();
1284
- this.itemInteraction = new EventEmitter();
1285
- this.cdr = inject(ChangeDetectorRef);
1286
- // Animation state tracking
1287
- this.fieldAnimationStates = new Map();
1288
- this.itemAnimationStates = new Map();
1289
- this.fieldAnimationTimes = new Map();
1290
- this.itemAnimationTimes = new Map();
1291
- this.FIELD_STAGGER_DELAY_MS = 30;
1292
- this.ITEM_STAGGER_DELAY_MS = 40;
1293
- this.FIELD_ANIMATION_DURATION_MS = 300;
1294
- this.ITEM_ANIMATION_DURATION_MS = 350;
1295
- this.fieldsAnimated = false;
1296
- this.itemsAnimated = false;
1297
- // Performance: Batch change detection for animation state updates
1298
- this.pendingFieldAnimationUpdates = new Set();
1299
- this.pendingItemAnimationUpdates = new Set();
1300
- this.fieldAnimationUpdateRafId = null;
1301
- this.itemAnimationUpdateRafId = null;
1293
+ /**
1294
+ * Reset field animation states
1295
+ */
1296
+ resetFieldAnimations() {
1297
+ this.fieldAnimationStates.clear();
1298
+ this.fieldAnimationTimes.clear();
1302
1299
  }
1303
1300
  /**
1304
- * Get fields from section (standardized access pattern)
1301
+ * Reset item animation states
1305
1302
  */
1306
- getFields() {
1307
- return this.section.fields ?? [];
1303
+ resetItemAnimations() {
1304
+ this.itemAnimationStates.clear();
1305
+ this.itemAnimationTimes.clear();
1308
1306
  }
1309
1307
  /**
1310
- * Get items from section (standardized access pattern)
1311
- * Falls back to fields if items are not available
1308
+ * Get field ID for tracking
1312
1309
  */
1313
- getItems() {
1314
- if (Array.isArray(this.section.items) && this.section.items.length > 0) {
1315
- return this.section.items;
1316
- }
1317
- // Fallback to fields if items are not available
1318
- if (Array.isArray(this.section.fields) && this.section.fields.length > 0) {
1319
- return this.section.fields.map((field) => {
1320
- const cardField = field;
1321
- return {
1322
- ...cardField,
1323
- title: cardField.title ?? cardField.label ?? cardField.id,
1324
- description: cardField.description ?? (typeof cardField.meta?.['description'] === 'string'
1325
- ? cardField.meta['description']
1326
- : undefined)
1327
- };
1328
- });
1310
+ getFieldId(field, index) {
1311
+ return field.id || `field-${index}-${field.label || ''}`;
1312
+ }
1313
+ /**
1314
+ * Get item ID for tracking
1315
+ */
1316
+ getItemId(item, index) {
1317
+ return item.id || `item-${index}-${item.title || ''}`;
1318
+ }
1319
+ /**
1320
+ * Check if section has fields
1321
+ * Public getter for template access
1322
+ */
1323
+ get hasFields() {
1324
+ return this.getFields().length > 0;
1325
+ }
1326
+ /**
1327
+ * Check if section has items
1328
+ * Public getter for template access
1329
+ */
1330
+ get hasItems() {
1331
+ return this.getItems().length > 0;
1332
+ }
1333
+ /**
1334
+ * Emit field interaction event (standardized pattern)
1335
+ */
1336
+ emitFieldInteraction(field, metadata) {
1337
+ this.fieldInteraction.emit({
1338
+ field,
1339
+ metadata: {
1340
+ sectionId: this.section.id,
1341
+ sectionTitle: this.section.title,
1342
+ ...metadata
1343
+ }
1344
+ });
1345
+ }
1346
+ /**
1347
+ * Emit item interaction event (standardized pattern)
1348
+ */
1349
+ emitItemInteraction(item, metadata) {
1350
+ this.itemInteraction.emit({
1351
+ item,
1352
+ metadata: {
1353
+ sectionId: this.section.id,
1354
+ sectionTitle: this.section.title,
1355
+ ...metadata
1356
+ }
1357
+ });
1358
+ }
1359
+ /**
1360
+ * Phase 5: Perfect trackBy function for fields - uses stable field ID
1361
+ * Can be overridden by child classes for custom tracking
1362
+ */
1363
+ trackField(index, field) {
1364
+ return field.id || `field-${index}-${field.label || ''}`;
1365
+ }
1366
+ /**
1367
+ * Phase 5: Perfect trackBy function for items - uses stable item ID
1368
+ * Can be overridden by child classes for custom tracking
1369
+ */
1370
+ trackItem(index, item) {
1371
+ return item.id || `item-${index}-${item.title || ''}`;
1372
+ }
1373
+ // Display methods removed - each component now implements its own to avoid TypeScript override conflicts
1374
+ // The logic is consistent: filter out "Streaming…" placeholder text
1375
+ /**
1376
+ * Safe value accessor - extracts value from field with fallback options
1377
+ * Handles field.value, field.text, field.quote based on field type
1378
+ */
1379
+ getFieldValue(field) {
1380
+ // Try value first (most common)
1381
+ if (field.value !== undefined && field.value !== null) {
1382
+ return field.value;
1383
+ }
1384
+ // Try text (for text-reference fields)
1385
+ if ('text' in field && field.text !== undefined && field.text !== null) {
1386
+ return field.text;
1387
+ }
1388
+ // Try quote (for quotation fields)
1389
+ if ('quote' in field && field.quote !== undefined && field.quote !== null) {
1390
+ return field.quote;
1391
+ }
1392
+ return undefined;
1393
+ }
1394
+ /**
1395
+ * Safe metadata accessor - extracts metadata value safely
1396
+ */
1397
+ getMetaValue(field, key) {
1398
+ return field.meta?.[key];
1399
+ }
1400
+ /**
1401
+ * Check if a value represents streaming placeholder
1402
+ */
1403
+ isStreamingPlaceholder(value) {
1404
+ return value === 'Streaming…' || value === 'Streaming...';
1405
+ }
1406
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: BaseSectionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1407
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: BaseSectionComponent, isStandalone: true, selector: "ng-component", inputs: { section: "section" }, outputs: { fieldInteraction: "fieldInteraction", itemInteraction: "itemInteraction" }, usesOnChanges: true, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1408
+ }
1409
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: BaseSectionComponent, decorators: [{
1410
+ type: Component,
1411
+ args: [{
1412
+ template: '',
1413
+ changeDetection: ChangeDetectionStrategy.OnPush
1414
+ }]
1415
+ }], propDecorators: { section: [{
1416
+ type: Input,
1417
+ args: [{ required: true }]
1418
+ }], fieldInteraction: [{
1419
+ type: Output
1420
+ }], itemInteraction: [{
1421
+ type: Output
1422
+ }] } });
1423
+
1424
+ class FallbackSectionComponent extends BaseSectionComponent {
1425
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: FallbackSectionComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
1426
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: FallbackSectionComponent, isStandalone: true, selector: "app-fallback-section", usesInheritance: true, ngImport: i0, template: "<div class=\"ai-section\">\n <div class=\"ai-section__header\">\n <div class=\"ai-section__details\">\n <h3 class=\"ai-section__title\">{{ section.title || 'Unsupported Section' }}</h3>\n <p *ngIf=\"section.description\" class=\"ai-section__description\">{{ section.description }}</p>\n </div>\n </div>\n\n <div class=\"ai-section__body\">\n <div class=\"section-empty\">\n <lucide-icon name=\"alert-circle\" size=\"32\" class=\"mb-3 opacity-60\"></lucide-icon>\n <p class=\"text-sm\">This section type is not yet customized. Add data or configure a renderer to display it.</p>\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: LucideIconsModule }, { kind: "component", type: i2.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1427
+ }
1428
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: FallbackSectionComponent, decorators: [{
1429
+ type: Component,
1430
+ args: [{ selector: 'app-fallback-section', standalone: true, imports: [CommonModule, LucideIconsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ai-section\">\n <div class=\"ai-section__header\">\n <div class=\"ai-section__details\">\n <h3 class=\"ai-section__title\">{{ section.title || 'Unsupported Section' }}</h3>\n <p *ngIf=\"section.description\" class=\"ai-section__description\">{{ section.description }}</p>\n </div>\n </div>\n\n <div class=\"ai-section__body\">\n <div class=\"section-empty\">\n <lucide-icon name=\"alert-circle\" size=\"32\" class=\"mb-3 opacity-60\"></lucide-icon>\n <p class=\"text-sm\">This section type is not yet customized. Add data or configure a renderer to display it.</p>\n </div>\n </div>\n</div>\n" }]
1431
+ }] });
1432
+
1433
+ /**
1434
+ * Registry service for managing custom section type plugins
1435
+ *
1436
+ * Allows external developers to register custom section components that extend
1437
+ * the library's built-in section types.
1438
+ *
1439
+ * @example
1440
+ * ```typescript
1441
+ * const registry = inject(SectionPluginRegistry);
1442
+ *
1443
+ * // Register a custom section plugin
1444
+ * registry.register({
1445
+ * type: 'custom-section',
1446
+ * name: 'Custom Section',
1447
+ * description: 'A custom section type',
1448
+ * component: CustomSectionComponent,
1449
+ * config: {
1450
+ * priority: 10,
1451
+ * override: false
1452
+ * }
1453
+ * });
1454
+ *
1455
+ * // Get a component for a section type
1456
+ * const component = registry.getComponent(section);
1457
+ * ```
1458
+ */
1459
+ class SectionPluginRegistry {
1460
+ constructor() {
1461
+ this.plugins = new Map();
1462
+ this.defaultFallback = FallbackSectionComponent;
1463
+ }
1464
+ /**
1465
+ * Register a custom section plugin
1466
+ *
1467
+ * @param plugin - The plugin metadata and component
1468
+ * @throws Error if plugin type already exists and override is false
1469
+ */
1470
+ register(plugin) {
1471
+ const { type, name, description, component, config = {}, metadata = {} } = plugin;
1472
+ // Check if plugin already exists
1473
+ if (this.plugins.has(type) && !config.override) {
1474
+ throw new Error(`Section plugin with type "${type}" is already registered. ` +
1475
+ `Set override: true in config to replace it.`);
1476
+ }
1477
+ // Validate component implements SectionPlugin
1478
+ const componentInstance = new component();
1479
+ if (!componentInstance.getPluginType || !componentInstance.canHandle) {
1480
+ console.warn(`Component ${component.name} does not properly implement SectionPlugin interface. ` +
1481
+ `It should extend BaseSectionComponent and implement SectionPlugin methods.`);
1482
+ }
1483
+ const registeredPlugin = {
1484
+ type,
1485
+ name,
1486
+ description,
1487
+ component: component,
1488
+ config,
1489
+ priority: config.priority ?? 0,
1490
+ version: metadata.version,
1491
+ author: metadata.author
1492
+ };
1493
+ this.plugins.set(type, registeredPlugin);
1494
+ }
1495
+ /**
1496
+ * Unregister a plugin
1497
+ *
1498
+ * @param type - The section type to unregister
1499
+ * @returns True if plugin was removed, false if not found
1500
+ */
1501
+ unregister(type) {
1502
+ return this.plugins.delete(type);
1503
+ }
1504
+ /**
1505
+ * Get the component class for a given section type
1506
+ *
1507
+ * @param sectionType - The section type identifier
1508
+ * @returns The component class or null if not found
1509
+ */
1510
+ getComponent(sectionType) {
1511
+ const plugin = this.plugins.get(sectionType);
1512
+ return plugin?.component ?? null;
1513
+ }
1514
+ /**
1515
+ * Get the component class for a section
1516
+ * Returns null if no plugin is registered (built-in sections will handle it)
1517
+ *
1518
+ * @param section - The card section
1519
+ * @returns The component class or null if no plugin registered
1520
+ */
1521
+ getComponentForSection(section) {
1522
+ const sectionType = section.type?.toLowerCase();
1523
+ if (!sectionType) {
1524
+ return null;
1525
+ }
1526
+ // Check if a plugin is registered for this type
1527
+ const plugin = this.plugins.get(sectionType);
1528
+ if (plugin) {
1529
+ return plugin.component;
1530
+ }
1531
+ // Return null to let built-in renderer handle it
1532
+ return null;
1533
+ }
1534
+ /**
1535
+ * Check if a plugin is registered for a section type
1536
+ *
1537
+ * @param type - The section type identifier
1538
+ * @returns True if a plugin is registered
1539
+ */
1540
+ hasPlugin(type) {
1541
+ return this.plugins.has(type);
1542
+ }
1543
+ /**
1544
+ * Get all registered plugins
1545
+ *
1546
+ * @returns Array of registered plugin metadata
1547
+ */
1548
+ getPlugins() {
1549
+ return Array.from(this.plugins.values())
1550
+ .sort((a, b) => b.priority - a.priority);
1551
+ }
1552
+ /**
1553
+ * Get plugin metadata for a specific type
1554
+ *
1555
+ * @param type - The section type identifier
1556
+ * @returns Plugin metadata or null if not found
1557
+ */
1558
+ getPluginMetadata(type) {
1559
+ return this.plugins.get(type) ?? null;
1560
+ }
1561
+ /**
1562
+ * Clear all registered plugins
1563
+ */
1564
+ clear() {
1565
+ this.plugins.clear();
1566
+ }
1567
+ /**
1568
+ * Register multiple plugins at once
1569
+ *
1570
+ * @param plugins - Array of plugins to register
1571
+ */
1572
+ registerAll(plugins) {
1573
+ plugins.forEach(plugin => {
1574
+ try {
1575
+ this.register(plugin);
1576
+ }
1577
+ catch (error) {
1578
+ console.error(`Failed to register plugin "${plugin.type}":`, error);
1579
+ }
1580
+ });
1581
+ }
1582
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SectionPluginRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1583
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SectionPluginRegistry, providedIn: 'root' }); }
1584
+ }
1585
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SectionPluginRegistry, decorators: [{
1586
+ type: Injectable,
1587
+ args: [{
1588
+ providedIn: 'root'
1589
+ }]
1590
+ }] });
1591
+
1592
+ /**
1593
+ * Event Middleware Service
1594
+ *
1595
+ * Manages event middleware chains for processing card events before they reach handlers.
1596
+ * Supports logging, transformation, filtering, and analytics integration.
1597
+ *
1598
+ * @example
1599
+ * ```typescript
1600
+ * const eventService = inject(EventMiddlewareService);
1601
+ *
1602
+ * // Add logging middleware
1603
+ * eventService.addMiddleware({
1604
+ * handle: (event, next) => {
1605
+ * console.log('Event:', event);
1606
+ * return next(event);
1607
+ * }
1608
+ * });
1609
+ *
1610
+ * // Subscribe to processed events
1611
+ * eventService.processedEvents$.subscribe(event => {
1612
+ * // Handle event
1613
+ * });
1614
+ * ```
1615
+ */
1616
+ class EventMiddlewareService {
1617
+ constructor() {
1618
+ this.middleware = [];
1619
+ this.processedEventsSubject = new Subject();
1620
+ this.rawEventsSubject = new Subject();
1621
+ /** Observable of processed events (after middleware chain) */
1622
+ this.processedEvents$ = this.processedEventsSubject.asObservable();
1623
+ /** Observable of raw events (before middleware) */
1624
+ this.rawEvents$ = this.rawEventsSubject.asObservable();
1625
+ }
1626
+ /**
1627
+ * Add middleware to the chain
1628
+ *
1629
+ * @param middleware - Middleware to add
1630
+ * @returns Function to remove the middleware
1631
+ */
1632
+ addMiddleware(middleware) {
1633
+ this.middleware.push(middleware);
1634
+ this.sortMiddleware();
1635
+ // Return remove function
1636
+ return () => {
1637
+ const index = this.middleware.indexOf(middleware);
1638
+ if (index > -1) {
1639
+ this.middleware.splice(index, 1);
1640
+ }
1641
+ };
1642
+ }
1643
+ /**
1644
+ * Remove middleware from the chain
1645
+ *
1646
+ * @param middleware - Middleware to remove
1647
+ */
1648
+ removeMiddleware(middleware) {
1649
+ const index = this.middleware.indexOf(middleware);
1650
+ if (index > -1) {
1651
+ this.middleware.splice(index, 1);
1652
+ }
1653
+ }
1654
+ /**
1655
+ * Clear all middleware
1656
+ */
1657
+ clearMiddleware() {
1658
+ this.middleware = [];
1659
+ }
1660
+ /**
1661
+ * Process an event through the middleware chain
1662
+ *
1663
+ * @param event - Event to process
1664
+ * @returns Processed event
1665
+ */
1666
+ processEvent(event) {
1667
+ // Emit raw event
1668
+ this.rawEventsSubject.next(event);
1669
+ if (this.middleware.length === 0) {
1670
+ this.processedEventsSubject.next(event);
1671
+ return event;
1672
+ }
1673
+ // Build middleware chain
1674
+ const processed = this.middleware.reduceRight((next, middleware) => {
1675
+ return (e) => middleware.handle(e, next);
1676
+ }, (e) => e // Final handler returns event as-is
1677
+ )(event);
1678
+ // Emit processed event
1679
+ this.processedEventsSubject.next(processed);
1680
+ return processed;
1681
+ }
1682
+ /**
1683
+ * Create a logging middleware
1684
+ *
1685
+ * @param logger - Optional logger function (defaults to console.log)
1686
+ * @returns Logging middleware
1687
+ */
1688
+ createLoggingMiddleware(logger) {
1689
+ const log = logger || ((msg, evt) => console.log(msg, evt));
1690
+ return {
1691
+ priority: 100, // High priority - log first
1692
+ handle: (event, next) => {
1693
+ log(`[Card Event] ${event.type}`, event);
1694
+ return next(event);
1695
+ }
1696
+ };
1697
+ }
1698
+ /**
1699
+ * Create a filtering middleware
1700
+ *
1701
+ * @param filter - Filter function
1702
+ * @returns Filtering middleware
1703
+ */
1704
+ createFilterMiddleware(filter) {
1705
+ return {
1706
+ handle: (event, next) => {
1707
+ if (filter(event)) {
1708
+ return next(event);
1709
+ }
1710
+ // Return event unchanged if filtered out
1711
+ return event;
1712
+ }
1713
+ };
1714
+ }
1715
+ /**
1716
+ * Create a transformation middleware
1717
+ *
1718
+ * @param transformer - Transformation function
1719
+ * @returns Transformation middleware
1720
+ */
1721
+ createTransformMiddleware(transformer) {
1722
+ return {
1723
+ handle: (event, next) => {
1724
+ const transformed = transformer(event);
1725
+ return next(transformed);
1726
+ }
1727
+ };
1728
+ }
1729
+ /**
1730
+ * Create an analytics middleware
1731
+ *
1732
+ * @param trackEvent - Analytics tracking function
1733
+ * @returns Analytics middleware
1734
+ */
1735
+ createAnalyticsMiddleware(trackEvent) {
1736
+ return {
1737
+ priority: 50,
1738
+ handle: (event, next) => {
1739
+ trackEvent(`card_${event.type}`, {
1740
+ sectionId: event.section.id,
1741
+ sectionType: event.section.type,
1742
+ fieldId: event.field?.id,
1743
+ itemId: event.item?.id,
1744
+ actionId: event.action?.id,
1745
+ metadata: event.metadata
1746
+ });
1747
+ return next(event);
1748
+ }
1749
+ };
1750
+ }
1751
+ /**
1752
+ * Sort middleware by priority (higher priority first)
1753
+ */
1754
+ sortMiddleware() {
1755
+ this.middleware.sort((a, b) => {
1756
+ const priorityA = a.priority ?? 0;
1757
+ const priorityB = b.priority ?? 0;
1758
+ return priorityB - priorityA;
1759
+ });
1760
+ }
1761
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: EventMiddlewareService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1762
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: EventMiddlewareService, providedIn: 'root' }); }
1763
+ }
1764
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: EventMiddlewareService, decorators: [{
1765
+ type: Injectable,
1766
+ args: [{
1767
+ providedIn: 'root'
1768
+ }]
1769
+ }] });
1770
+
1771
+ /**
1772
+ * Public interfaces for OSI Cards Library
1773
+ */
1774
+
1775
+ /**
1776
+ * Simple hash function for content hashing (replaces JSON.stringify)
1777
+ * Uses MurmurHash-inspired algorithm for fast hashing
1778
+ */
1779
+ function hashString(str) {
1780
+ let hash = 0;
1781
+ if (str.length === 0)
1782
+ return hash;
1783
+ for (let i = 0; i < str.length; i++) {
1784
+ const char = str.charCodeAt(i);
1785
+ hash = ((hash << 5) - hash) + char;
1786
+ hash = hash & hash; // Convert to 32-bit integer
1787
+ }
1788
+ return hash;
1789
+ }
1790
+ /**
1791
+ * Generate content hash for a field (faster than JSON.stringify)
1792
+ */
1793
+ function hashField(field) {
1794
+ const key = `${field.id || ''}|${field.label || ''}|${field.value || ''}|${field.type || ''}|${field.title || ''}`;
1795
+ return String(hashString(key));
1796
+ }
1797
+ /**
1798
+ * Generate content hash for an item (faster than JSON.stringify)
1799
+ */
1800
+ function hashItem(item) {
1801
+ const key = `${item.id || ''}|${item.title || ''}|${item.value || ''}`;
1802
+ return String(hashString(key));
1803
+ }
1804
+ /**
1805
+ * WeakMap cache for field hashes to avoid recomputation
1806
+ */
1807
+ const fieldHashCache = new WeakMap();
1808
+ const itemHashCache = new WeakMap();
1809
+ /**
1810
+ * Deep comparison utility for card objects
1811
+ * Uses content hashing instead of JSON.stringify for better performance
1812
+ */
1813
+ class CardDiffUtil {
1814
+ /**
1815
+ * Creates an updated card with only changed sections/fields updated
1816
+ * Preserves references to unchanged sections for optimal performance
1817
+ */
1818
+ static mergeCardUpdates(oldCard, newCard) {
1819
+ // If cards are identical, return old card (preserve reference)
1820
+ if (this.areCardsEqual(oldCard, newCard)) {
1821
+ return { card: oldCard, changeType: 'content' };
1822
+ }
1823
+ // Check if only top-level properties changed (title, subtitle, etc.)
1824
+ // Check if sections array changed
1825
+ const sectionsChanged = !this.areSectionsEqual(oldCard.sections, newCard.sections);
1826
+ // If only top-level changed, update only those
1827
+ if (!sectionsChanged) {
1828
+ return {
1829
+ card: {
1830
+ ...oldCard,
1831
+ cardTitle: newCard.cardTitle,
1832
+ cardSubtitle: newCard.cardSubtitle,
1833
+ cardType: newCard.cardType,
1834
+ description: newCard.description,
1835
+ columns: newCard.columns,
1836
+ actions: newCard.actions,
1837
+ // Keep same sections reference
1838
+ sections: oldCard.sections
1839
+ },
1840
+ changeType: 'content'
1841
+ };
1842
+ }
1843
+ // Merge sections incrementally
1844
+ const mergedSections = this.mergeSections(oldCard.sections, newCard.sections);
1845
+ const changeType = sectionsChanged && !this.didStructureChange(oldCard.sections, newCard.sections)
1846
+ ? 'content'
1847
+ : 'structural';
1848
+ return {
1849
+ card: {
1850
+ ...oldCard,
1851
+ cardTitle: newCard.cardTitle,
1852
+ cardSubtitle: newCard.cardSubtitle,
1853
+ cardType: newCard.cardType,
1854
+ description: newCard.description,
1855
+ columns: newCard.columns,
1856
+ actions: newCard.actions,
1857
+ sections: mergedSections
1858
+ },
1859
+ changeType
1860
+ };
1861
+ }
1862
+ static didStructureChange(oldSections, newSections) {
1863
+ if (oldSections.length !== newSections.length) {
1864
+ return true;
1865
+ }
1866
+ return oldSections.some((oldSection, index) => {
1867
+ const newSection = newSections[index];
1868
+ if (!newSection) {
1869
+ return true;
1870
+ }
1871
+ if ((oldSection.id || index) !== (newSection.id || index)) {
1872
+ return true;
1873
+ }
1874
+ if (oldSection.type !== newSection.type) {
1875
+ return true;
1876
+ }
1877
+ const oldFieldsLength = oldSection.fields?.length ?? 0;
1878
+ const newFieldsLength = newSection.fields?.length ?? 0;
1879
+ const oldItemsLength = oldSection.items?.length ?? 0;
1880
+ const newItemsLength = newSection.items?.length ?? 0;
1881
+ return oldFieldsLength !== newFieldsLength || newItemsLength !== oldItemsLength;
1882
+ });
1883
+ }
1884
+ /**
1885
+ * Merges sections array, preserving references to unchanged sections
1886
+ */
1887
+ static mergeSections(oldSections, newSections) {
1888
+ // If sections array length changed, we need to rebuild
1889
+ if (oldSections.length !== newSections.length) {
1890
+ return newSections.map((section, index) => {
1891
+ const oldSection = oldSections[index];
1892
+ if (oldSection && this.areSectionsEqual([oldSection], [section])) {
1893
+ return oldSection; // Preserve reference
1894
+ }
1895
+ return section;
1896
+ });
1897
+ }
1898
+ // Merge each section
1899
+ return newSections.map((newSection, index) => {
1900
+ const oldSection = oldSections[index];
1901
+ if (!oldSection) {
1902
+ return newSection;
1903
+ }
1904
+ if ((oldSection.id || index) !== (newSection.id || index)) {
1905
+ return newSection;
1906
+ }
1907
+ // Merge section fields/items
1908
+ return this.mergeSection(oldSection, newSection);
1909
+ });
1910
+ }
1911
+ /**
1912
+ * Merges a single section, preserving references to unchanged fields/items
1913
+ */
1914
+ static mergeSection(oldSection, newSection) {
1915
+ // Check if only top-level section properties changed
1916
+ // Check if fields changed
1917
+ const fieldsChanged = !this.areFieldsEqual(oldSection.fields, newSection.fields);
1918
+ const itemsChanged = !this.areItemsEqual(oldSection.items, newSection.items);
1919
+ // If only top-level changed, preserve fields/items references
1920
+ if (!fieldsChanged && !itemsChanged) {
1921
+ return {
1922
+ ...oldSection,
1923
+ title: newSection.title,
1924
+ type: newSection.type,
1925
+ description: newSection.description,
1926
+ subtitle: newSection.subtitle,
1927
+ columns: newSection.columns,
1928
+ colSpan: newSection.colSpan,
1929
+ collapsed: newSection.collapsed,
1930
+ emoji: newSection.emoji,
1931
+ chartType: newSection.chartType,
1932
+ chartData: newSection.chartData,
1933
+ meta: newSection.meta,
1934
+ // Preserve fields/items references
1935
+ fields: oldSection.fields,
1936
+ items: oldSection.items
1937
+ };
1938
+ }
1939
+ // Merge fields if they exist
1940
+ const mergedFields = oldSection.fields && newSection.fields
1941
+ ? this.mergeFields(oldSection.fields, newSection.fields)
1942
+ : newSection.fields;
1943
+ // Merge items if they exist
1944
+ const mergedItems = oldSection.items && newSection.items
1945
+ ? this.mergeItems(oldSection.items, newSection.items)
1946
+ : newSection.items;
1947
+ return {
1948
+ ...oldSection,
1949
+ title: newSection.title,
1950
+ type: newSection.type,
1951
+ description: newSection.description,
1952
+ subtitle: newSection.subtitle,
1953
+ columns: newSection.columns,
1954
+ colSpan: newSection.colSpan,
1955
+ collapsed: newSection.collapsed,
1956
+ emoji: newSection.emoji,
1957
+ chartType: newSection.chartType,
1958
+ chartData: newSection.chartData,
1959
+ meta: newSection.meta,
1960
+ fields: mergedFields,
1961
+ items: mergedItems
1962
+ };
1963
+ }
1964
+ /**
1965
+ * Merges fields array, preserving references to unchanged fields
1966
+ * Uses content hashing instead of JSON.stringify for better performance
1967
+ */
1968
+ static mergeFields(oldFields, newFields) {
1969
+ if (oldFields.length !== newFields.length) {
1970
+ return newFields;
1971
+ }
1972
+ return newFields.map((newField, index) => {
1973
+ const oldField = oldFields[index];
1974
+ if (!oldField) {
1975
+ return newField;
1976
+ }
1977
+ // Fast comparison: check key properties first
1978
+ if (oldField.id === newField.id &&
1979
+ oldField.label === newField.label &&
1980
+ oldField.value === newField.value &&
1981
+ oldField.title === newField.title) {
1982
+ // Use content hashing instead of JSON.stringify
1983
+ const oldHash = fieldHashCache.get(oldField) || hashField(oldField);
1984
+ const newHash = hashField(newField);
1985
+ // Cache hashes for future comparisons
1986
+ if (!fieldHashCache.has(oldField)) {
1987
+ fieldHashCache.set(oldField, oldHash);
1988
+ }
1989
+ if (!fieldHashCache.has(newField)) {
1990
+ fieldHashCache.set(newField, newHash);
1991
+ }
1992
+ if (oldHash === newHash) {
1993
+ return oldField; // Preserve reference
1994
+ }
1995
+ }
1996
+ return newField;
1997
+ });
1998
+ }
1999
+ /**
2000
+ * Merges items array, preserving references to unchanged items
2001
+ * Uses content hashing instead of JSON.stringify for better performance
2002
+ */
2003
+ static mergeItems(oldItems, newItems) {
2004
+ if (oldItems.length !== newItems.length) {
2005
+ return newItems;
2006
+ }
2007
+ return newItems.map((newItem, index) => {
2008
+ const oldItem = oldItems[index];
2009
+ if (!oldItem) {
2010
+ return newItem;
2011
+ }
2012
+ // Fast comparison
2013
+ if (oldItem.id === newItem.id &&
2014
+ oldItem.title === newItem.title &&
2015
+ oldItem.value === newItem.value) {
2016
+ // Use content hashing instead of JSON.stringify
2017
+ const oldHash = itemHashCache.get(oldItem) || hashItem(oldItem);
2018
+ const newHash = hashItem(newItem);
2019
+ // Cache hashes for future comparisons
2020
+ if (!itemHashCache.has(oldItem)) {
2021
+ itemHashCache.set(oldItem, oldHash);
2022
+ }
2023
+ if (!itemHashCache.has(newItem)) {
2024
+ itemHashCache.set(newItem, newHash);
2025
+ }
2026
+ if (oldHash === newHash) {
2027
+ return oldItem; // Preserve reference
2028
+ }
2029
+ }
2030
+ return newItem;
2031
+ });
2032
+ }
2033
+ /**
2034
+ * Fast equality check for cards
2035
+ */
2036
+ static areCardsEqual(card1, card2) {
2037
+ return card1.id === card2.id &&
2038
+ card1.cardTitle === card2.cardTitle &&
2039
+ card1.cardSubtitle === card2.cardSubtitle &&
2040
+ card1.cardType === card2.cardType &&
2041
+ this.areSectionsEqual(card1.sections, card2.sections);
2042
+ }
2043
+ /**
2044
+ * Fast equality check for sections arrays
2045
+ */
2046
+ static areSectionsEqual(sections1, sections2) {
2047
+ if (sections1.length !== sections2.length) {
2048
+ return false;
2049
+ }
2050
+ return sections1.every((section1, index) => {
2051
+ const section2 = sections2[index];
2052
+ if (!section2)
2053
+ return false;
2054
+ return section1.id === section2.id &&
2055
+ section1.title === section2.title &&
2056
+ section1.type === section2.type &&
2057
+ this.areFieldsEqual(section1.fields, section2.fields) &&
2058
+ this.areItemsEqual(section1.items, section2.items);
2059
+ });
2060
+ }
2061
+ /**
2062
+ * Fast equality check for fields arrays
2063
+ */
2064
+ static areFieldsEqual(fields1, fields2) {
2065
+ if (!fields1 && !fields2)
2066
+ return true;
2067
+ if (!fields1 || !fields2)
2068
+ return false;
2069
+ if (fields1.length !== fields2.length)
2070
+ return false;
2071
+ return fields1.every((field1, index) => {
2072
+ const field2 = fields2[index];
2073
+ if (!field2)
2074
+ return false;
2075
+ return field1.id === field2.id &&
2076
+ field1.label === field2.label &&
2077
+ field1.value === field2.value &&
2078
+ field1.title === field2.title;
2079
+ });
2080
+ }
2081
+ /**
2082
+ * Fast equality check for items arrays
2083
+ */
2084
+ static areItemsEqual(items1, items2) {
2085
+ if (!items1 && !items2)
2086
+ return true;
2087
+ if (!items1 || !items2)
2088
+ return false;
2089
+ if (items1.length !== items2.length)
2090
+ return false;
2091
+ return items1.every((item1, index) => {
2092
+ const item2 = items2[index];
2093
+ if (!item2)
2094
+ return false;
2095
+ return item1.id === item2.id &&
2096
+ item1.title === item2.title &&
2097
+ item1.value === item2.value;
2098
+ });
2099
+ }
2100
+ }
2101
+
2102
+ function getBreakpointFromWidth(width) {
2103
+ if (width < 640)
2104
+ return 'xs';
2105
+ if (width < 768)
2106
+ return 'sm';
2107
+ if (width < 1024)
2108
+ return 'md';
2109
+ if (width < 1280)
2110
+ return 'lg';
2111
+ if (width < 1536)
2112
+ return 'xl';
2113
+ return '2xl';
2114
+ }
2115
+
2116
+ /**
2117
+ * Card Spawner Utilities
2118
+ *
2119
+ * Helper functions for dynamically instantiating and managing card components,
2120
+ * particularly useful for agentic flows and LLM integrations.
2121
+ */
2122
+ /**
2123
+ * Creates an empty card configuration for initialization
2124
+ */
2125
+ function createEmptyCard(title = 'Loading...') {
2126
+ return {
2127
+ cardTitle: title,
2128
+ cardSubtitle: undefined,
2129
+ sections: [],
2130
+ actions: []
2131
+ };
2132
+ }
2133
+ /**
2134
+ * Creates a skeleton card configuration for loading states
2135
+ */
2136
+ function createSkeletonCard() {
2137
+ return {
2138
+ cardTitle: 'Loading...',
2139
+ cardSubtitle: 'Please wait while we fetch your data',
2140
+ sections: [
2141
+ {
2142
+ id: 'skeleton-section',
2143
+ title: 'Loading',
2144
+ type: 'info',
2145
+ fields: []
2146
+ }
2147
+ ]
2148
+ };
2149
+ }
2150
+ /**
2151
+ * Merges a partial card configuration into an existing card configuration
2152
+ * This is useful for progressive updates during streaming
2153
+ */
2154
+ function mergeCardConfig(existing, update) {
2155
+ const merged = {
2156
+ ...existing,
2157
+ ...update
2158
+ };
2159
+ // Merge sections intelligently
2160
+ if (update.sections) {
2161
+ merged.sections = mergeSections(existing.sections || [], update.sections);
2162
+ }
2163
+ // Merge actions
2164
+ if (update.actions) {
2165
+ merged.actions = mergeActions(existing.actions || [], update.actions);
2166
+ }
2167
+ return merged;
2168
+ }
2169
+ /**
2170
+ * Merges sections arrays, updating existing sections or adding new ones
2171
+ */
2172
+ function mergeSections(existing, updates) {
2173
+ const sectionMap = new Map();
2174
+ // Add existing sections to map
2175
+ existing.forEach(section => {
2176
+ const key = getSectionKey(section);
2177
+ sectionMap.set(key, { ...section });
2178
+ });
2179
+ // Merge or add updated sections
2180
+ updates.forEach(updateSection => {
2181
+ const key = getSectionKey(updateSection);
2182
+ const existingSection = sectionMap.get(key);
2183
+ if (existingSection) {
2184
+ // Merge fields and items into existing section
2185
+ sectionMap.set(key, {
2186
+ ...existingSection,
2187
+ ...updateSection,
2188
+ fields: mergeFields(existingSection.fields || [], updateSection.fields || []),
2189
+ items: mergeItems(existingSection.items || [], updateSection.items || [])
2190
+ });
2191
+ }
2192
+ else {
2193
+ // Add new section
2194
+ sectionMap.set(key, { ...updateSection });
2195
+ }
2196
+ });
2197
+ return Array.from(sectionMap.values());
2198
+ }
2199
+ /**
2200
+ * Merges fields arrays, avoiding duplicates by ID or label
2201
+ */
2202
+ function mergeFields(existing, updates) {
2203
+ const fieldMap = new Map();
2204
+ // Add existing fields
2205
+ existing.forEach(field => {
2206
+ const key = field.id || field.label || String(field);
2207
+ fieldMap.set(key, field);
2208
+ });
2209
+ // Add or update fields
2210
+ updates.forEach(update => {
2211
+ const key = update.id || update.label || String(update);
2212
+ fieldMap.set(key, update);
2213
+ });
2214
+ return Array.from(fieldMap.values());
2215
+ }
2216
+ /**
2217
+ * Merges items arrays, avoiding duplicates by ID or name
2218
+ */
2219
+ function mergeItems(existing, updates) {
2220
+ const itemMap = new Map();
2221
+ // Add existing items
2222
+ existing.forEach(item => {
2223
+ const key = item.id || item.name || String(item);
2224
+ itemMap.set(key, item);
2225
+ });
2226
+ // Add or update items
2227
+ updates.forEach(update => {
2228
+ const key = update.id || update.name || String(update);
2229
+ itemMap.set(key, update);
2230
+ });
2231
+ return Array.from(itemMap.values());
2232
+ }
2233
+ /**
2234
+ * Merges actions arrays, avoiding duplicates by ID or label
2235
+ */
2236
+ function mergeActions(existing, updates) {
2237
+ const actionMap = new Map();
2238
+ // Add existing actions
2239
+ existing.forEach(action => {
2240
+ const key = action.id || action.label || String(action);
2241
+ actionMap.set(key, action);
2242
+ });
2243
+ // Add or update actions
2244
+ updates.forEach(update => {
2245
+ const key = update.id || update.label || String(update);
2246
+ actionMap.set(key, update);
2247
+ });
2248
+ return Array.from(actionMap.values());
2249
+ }
2250
+ /**
2251
+ * Gets a unique key for a section (for merging)
2252
+ */
2253
+ function getSectionKey(section) {
2254
+ return section.id ||
2255
+ `${section.title || 'section'}-${section.type || 'info'}`;
2256
+ }
2257
+ /**
2258
+ * Validates a card configuration for completeness
2259
+ */
2260
+ function validateCardConfig(card) {
2261
+ const errors = [];
2262
+ if (!card.cardTitle) {
2263
+ errors.push('Card title is required');
2264
+ }
2265
+ if (!card.sections || card.sections.length === 0) {
2266
+ errors.push('At least one section is required');
2267
+ }
2268
+ if (card.sections) {
2269
+ card.sections.forEach((section, index) => {
2270
+ if (!section.title) {
2271
+ errors.push(`Section ${index} is missing a title`);
2272
+ }
2273
+ if (!section.type) {
2274
+ errors.push(`Section ${index} is missing a type`);
2275
+ }
2276
+ });
2277
+ }
2278
+ return {
2279
+ valid: errors.length === 0,
2280
+ errors
2281
+ };
2282
+ }
2283
+ /**
2284
+ * Creates a card configuration from a partial update
2285
+ * Useful when receiving streaming updates that may be incomplete
2286
+ */
2287
+ function createCardFromPartial(partial, defaults = {}) {
2288
+ return {
2289
+ cardTitle: partial.cardTitle || defaults.cardTitle || 'Card',
2290
+ cardSubtitle: partial.cardSubtitle || defaults.cardSubtitle,
2291
+ sections: partial.sections || defaults.sections || [],
2292
+ actions: partial.actions || defaults.actions || []
2293
+ };
2294
+ }
2295
+ /**
2296
+ * Checks if a card configuration is complete (all required fields present)
2297
+ */
2298
+ function isCardComplete(card) {
2299
+ return !!(card.cardTitle &&
2300
+ card.sections &&
2301
+ card.sections.length > 0 &&
2302
+ card.sections.every(section => section.title &&
2303
+ section.type &&
2304
+ ((section.fields && section.fields.length > 0) || (section.items && section.items.length > 0))));
2305
+ }
2306
+ /**
2307
+ * Creates a card configuration with error information
2308
+ */
2309
+ function createErrorCard(error, title = 'Error') {
2310
+ const errorMessage = typeof error === 'string' ? error : error.message;
2311
+ return {
2312
+ cardTitle: title,
2313
+ cardSubtitle: 'An error occurred',
2314
+ sections: [
2315
+ {
2316
+ id: 'error-section',
2317
+ title: 'Error Details',
2318
+ type: 'info',
2319
+ fields: [
2320
+ {
2321
+ id: 'error-message',
2322
+ label: 'Message',
2323
+ value: errorMessage,
2324
+ type: 'text'
2325
+ }
2326
+ ]
2327
+ }
2328
+ ]
2329
+ };
2330
+ }
2331
+ /**
2332
+ * Prepares a card configuration for streaming updates
2333
+ * Ensures the card has the necessary structure for progressive updates
2334
+ */
2335
+ function prepareCardForStreaming(card) {
2336
+ return {
2337
+ cardTitle: card.cardTitle || 'Loading...',
2338
+ cardSubtitle: card.cardSubtitle,
2339
+ sections: card.sections || [],
2340
+ actions: card.actions || [],
2341
+ // Ensure sections have IDs for tracking during updates
2342
+ ...card
2343
+ };
2344
+ }
2345
+ /**
2346
+ * Updates a card configuration incrementally
2347
+ * Optimized for streaming scenarios where partial updates arrive
2348
+ */
2349
+ function updateCardIncremental(existing, update) {
2350
+ // Start with existing card
2351
+ const updated = { ...existing };
2352
+ // Update top-level fields if provided
2353
+ if (update.cardTitle !== undefined) {
2354
+ updated.cardTitle = update.cardTitle;
2355
+ }
2356
+ if (update.cardSubtitle !== undefined) {
2357
+ updated.cardSubtitle = update.cardSubtitle;
2358
+ }
2359
+ // Merge sections incrementally
2360
+ if (update.sections) {
2361
+ updated.sections = mergeSections(existing.sections || [], update.sections);
2362
+ }
2363
+ // Merge actions
2364
+ if (update.actions) {
2365
+ updated.actions = [...(existing.actions || []), ...(update.actions || [])];
2366
+ }
2367
+ return updated;
2368
+ }
2369
+ /**
2370
+ * Creates a copy of a card configuration (deep clone)
2371
+ */
2372
+ function cloneCardConfig(card) {
2373
+ return JSON.parse(JSON.stringify(card));
2374
+ }
2375
+
2376
+ /**
2377
+ * Style Validator Utilities
2378
+ *
2379
+ * Helper functions to validate that the OSI Cards design system styles
2380
+ * are properly loaded and configured.
2381
+ */
2382
+ /**
2383
+ * Required CSS variables that must be present for the library to work correctly
2384
+ */
2385
+ const REQUIRED_CSS_VARIABLES = [
2386
+ '--card-padding',
2387
+ '--card-gap',
2388
+ '--card-border-radius',
2389
+ '--color-brand',
2390
+ '--ai-card-background',
2391
+ '--section-item-background'
2392
+ ];
2393
+ /**
2394
+ * Optional but recommended CSS variables
2395
+ */
2396
+ const RECOMMENDED_CSS_VARIABLES = [
2397
+ '--card-text-primary',
2398
+ '--card-text-secondary',
2399
+ '--card-transition',
2400
+ '--duration-normal',
2401
+ '--duration-moderate'
2402
+ ];
2403
+ /**
2404
+ * Validates that required CSS variables are present
2405
+ *
2406
+ * @param element - Optional element to check (defaults to document root)
2407
+ * @returns Validation result with missing variables and warnings
2408
+ *
2409
+ * @example
2410
+ * ```typescript
2411
+ * const validation = validateStyles();
2412
+ * if (!validation.valid) {
2413
+ * console.warn('Missing styles:', validation.missing);
2414
+ * }
2415
+ * ```
2416
+ */
2417
+ function validateStyles(element) {
2418
+ // Check if we're in a browser environment
2419
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
2420
+ return {
2421
+ valid: false,
2422
+ missing: REQUIRED_CSS_VARIABLES,
2423
+ recommended: RECOMMENDED_CSS_VARIABLES,
2424
+ warnings: ['Style validation requires a browser environment']
2425
+ };
2426
+ }
2427
+ const root = element || document.documentElement;
2428
+ const computedStyle = window.getComputedStyle(root);
2429
+ const missing = [];
2430
+ const recommended = [];
2431
+ const warnings = [];
2432
+ // Check required variables
2433
+ REQUIRED_CSS_VARIABLES.forEach(variable => {
2434
+ const value = computedStyle.getPropertyValue(variable).trim();
2435
+ if (!value || value === 'initial' || value === 'inherit') {
2436
+ missing.push(variable);
2437
+ }
2438
+ });
2439
+ // Check recommended variables
2440
+ RECOMMENDED_CSS_VARIABLES.forEach(variable => {
2441
+ const value = computedStyle.getPropertyValue(variable).trim();
2442
+ if (!value || value === 'initial' || value === 'inherit') {
2443
+ recommended.push(variable);
2444
+ }
2445
+ });
2446
+ // Generate warnings
2447
+ if (missing.length > 0) {
2448
+ warnings.push(`OSI Cards Library: ${missing.length} required CSS variable(s) are missing. ` +
2449
+ `The library styles may not be properly imported. ` +
2450
+ `Please ensure you've added: @import 'osi-cards-lib/styles/_styles';`);
2451
+ }
2452
+ if (recommended.length > 0 && missing.length === 0) {
2453
+ warnings.push(`OSI Cards Library: ${recommended.length} recommended CSS variable(s) are missing. ` +
2454
+ `Some advanced features may not work as expected.`);
2455
+ }
2456
+ return {
2457
+ valid: missing.length === 0,
2458
+ missing,
2459
+ recommended,
2460
+ warnings
2461
+ };
2462
+ }
2463
+ /**
2464
+ * Validates styles and logs warnings to console if issues are found
2465
+ *
2466
+ * @param element - Optional element to check
2467
+ * @param logToConsole - Whether to log warnings to console (default: true)
2468
+ * @returns Validation result
2469
+ *
2470
+ * @example
2471
+ * ```typescript
2472
+ * // In component ngOnInit or after styles load
2473
+ * validateAndWarnStyles();
2474
+ * ```
2475
+ */
2476
+ function validateAndWarnStyles(element, logToConsole = true) {
2477
+ const result = validateStyles(element);
2478
+ if (logToConsole && result.warnings.length > 0) {
2479
+ result.warnings.forEach(warning => {
2480
+ console.warn(warning);
2481
+ });
2482
+ if (result.missing.length > 0) {
2483
+ console.info('Missing CSS variables:\n' +
2484
+ result.missing.map(v => ` - ${v}`).join('\n') + '\n\n' +
2485
+ 'To fix this, add to your styles file:\n' +
2486
+ " @import 'osi-cards-lib/styles/_styles';");
2487
+ }
2488
+ }
2489
+ return result;
2490
+ }
2491
+ /**
2492
+ * Checks if a specific CSS variable is defined
2493
+ *
2494
+ * @param variableName - Name of the CSS variable (with or without -- prefix)
2495
+ * @param element - Optional element to check
2496
+ * @returns True if the variable is defined and has a value
2497
+ */
2498
+ function isCSSVariableDefined(variableName, element) {
2499
+ // Check if we're in a browser environment
2500
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
2501
+ return false;
2502
+ }
2503
+ const root = element || document.documentElement;
2504
+ const computedStyle = window.getComputedStyle(root);
2505
+ // Ensure variable name starts with --
2506
+ const varName = variableName.startsWith('--') ? variableName : `--${variableName}`;
2507
+ const value = computedStyle.getPropertyValue(varName).trim();
2508
+ return !!value && value !== 'initial' && value !== 'inherit';
2509
+ }
2510
+ /**
2511
+ * Gets the value of a CSS variable
2512
+ *
2513
+ * @param variableName - Name of the CSS variable (with or without -- prefix)
2514
+ * @param element - Optional element to check
2515
+ * @returns The CSS variable value, or null if not defined
2516
+ */
2517
+ function getCSSVariableValue(variableName, element) {
2518
+ // Check if we're in a browser environment
2519
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
2520
+ return null;
2521
+ }
2522
+ const root = element || document.documentElement;
2523
+ const computedStyle = window.getComputedStyle(root);
2524
+ // Ensure variable name starts with --
2525
+ const varName = variableName.startsWith('--') ? variableName : `--${variableName}`;
2526
+ const value = computedStyle.getPropertyValue(varName).trim();
2527
+ if (!value || value === 'initial' || value === 'inherit') {
2528
+ return null;
2529
+ }
2530
+ return value;
2531
+ }
2532
+ /**
2533
+ * Checks if styles are loaded by looking for a specific marker class or variable
2534
+ * This is a lighter check than full validation
2535
+ *
2536
+ * @returns True if styles appear to be loaded
2537
+ */
2538
+ function areStylesLoaded() {
2539
+ // Check for presence of a key design token
2540
+ return isCSSVariableDefined('--color-brand') &&
2541
+ isCSSVariableDefined('--card-padding');
2542
+ }
2543
+ /**
2544
+ * Waits for styles to be loaded by polling
2545
+ * Useful when styles are loaded asynchronously
2546
+ *
2547
+ * @param timeout - Maximum time to wait in milliseconds (default: 5000)
2548
+ * @param pollInterval - How often to check in milliseconds (default: 100)
2549
+ * @returns Promise that resolves when styles are loaded or timeout is reached
2550
+ */
2551
+ function waitForStyles(timeout = 5000, pollInterval = 100) {
2552
+ // Check if we're in a browser environment
2553
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
2554
+ return Promise.resolve(false);
2555
+ }
2556
+ return new Promise((resolve) => {
2557
+ const startTime = Date.now();
2558
+ const checkStyles = () => {
2559
+ if (areStylesLoaded()) {
2560
+ resolve(true);
2561
+ return;
2562
+ }
2563
+ if (Date.now() - startTime >= timeout) {
2564
+ resolve(false);
2565
+ return;
2566
+ }
2567
+ setTimeout(checkStyles, pollInterval);
2568
+ };
2569
+ checkStyles();
2570
+ });
2571
+ }
2572
+
2573
+ /**
2574
+ * Provide OSI Cards Library with required providers
2575
+ *
2576
+ * This function provides all necessary providers for the OSI Cards library to function
2577
+ * correctly in an Angular application. It includes:
2578
+ * - Animation providers (required for component animations)
2579
+ * - Service providers (services use providedIn: 'root' so they're automatically available)
2580
+ *
2581
+ * @param config - Optional configuration object
2582
+ * @returns Array of providers to be added to your ApplicationConfig
2583
+ *
2584
+ * @example
2585
+ * ```typescript
2586
+ * // In your app.config.ts
2587
+ * import { ApplicationConfig } from '@angular/core';
2588
+ * import { provideOSICards } from 'osi-cards-lib';
2589
+ *
2590
+ * export const appConfig: ApplicationConfig = {
2591
+ * providers: [
2592
+ * provideOSICards(), // Enable animations (default)
2593
+ * // ... other providers
2594
+ * ]
2595
+ * };
2596
+ * ```
2597
+ *
2598
+ * @example
2599
+ * ```typescript
2600
+ * // Disable animations (for testing or performance)
2601
+ * export const appConfig: ApplicationConfig = {
2602
+ * providers: [
2603
+ * provideOSICards({ enableAnimations: false }),
2604
+ * // ... other providers
2605
+ * ]
2606
+ * };
2607
+ * ```
2608
+ *
2609
+ * @remarks
2610
+ * - **REQUIRED**: You must call this function in your app.config.ts providers array
2611
+ * - Animations are required for proper component behavior (entrance animations, transitions)
2612
+ * - Services (MagneticTiltService, IconService, etc.) are automatically provided via providedIn: 'root'
2613
+ * - Styles must be imported separately: @import 'osi-cards-lib/styles/_styles';
2614
+ */
2615
+ function provideOSICards(config = {}) {
2616
+ const { enableAnimations = true } = config;
2617
+ const providers = [
2618
+ // Animation provider is REQUIRED for component animations
2619
+ enableAnimations ? provideAnimations() : provideNoopAnimations()
2620
+ ];
2621
+ // Services are provided via providedIn: 'root' and don't need explicit providers here
2622
+ // This includes:
2623
+ // - MagneticTiltService
2624
+ // - IconService
2625
+ // - SectionNormalizationService
2626
+ // - SectionUtilsService
2627
+ return providers;
2628
+ }
2629
+
2630
+ /**
2631
+ * OSI Cards Library Providers
2632
+ *
2633
+ * Exports provider functions for configuring the OSI Cards library in your Angular application.
2634
+ */
2635
+
2636
+ /**
2637
+ * Theme Service
2638
+ *
2639
+ * Manages theme configuration and runtime theme switching for OSI Cards library.
2640
+ * Supports built-in presets and custom themes via CSS custom properties.
2641
+ *
2642
+ * @example
2643
+ * ```typescript
2644
+ * const themeService = inject(ThemeService);
2645
+ *
2646
+ * // Switch to dark theme
2647
+ * themeService.setTheme('night');
2648
+ *
2649
+ * // Apply custom theme
2650
+ * themeService.applyCustomTheme({
2651
+ * name: 'my-brand',
2652
+ * variables: {
2653
+ * '--color-brand': '#ff0000',
2654
+ * '--card-padding': '20px'
2655
+ * }
2656
+ * });
2657
+ * ```
2658
+ */
2659
+ class ThemeService {
2660
+ constructor() {
2661
+ this.platformId = inject(PLATFORM_ID);
2662
+ this.document = inject(DOCUMENT);
2663
+ this.currentThemeSubject = new BehaviorSubject('day');
2664
+ this.currentTheme$ = this.currentThemeSubject.asObservable();
2665
+ this.customThemes = new Map();
2666
+ this.rootElement = isPlatformBrowser(this.platformId)
2667
+ ? this.document.documentElement
2668
+ : null;
2669
+ // Initialize theme from document if in browser
2670
+ if (this.rootElement) {
2671
+ const initialTheme = this.rootElement.getAttribute('data-theme') || 'day';
2672
+ this.currentThemeSubject.next(initialTheme);
1329
2673
  }
1330
- return [];
1331
2674
  }
1332
- ngOnChanges(changes) {
1333
- if (changes['section']) {
1334
- // Cancel pending RAFs
1335
- if (this.fieldAnimationUpdateRafId !== null) {
1336
- cancelAnimationFrame(this.fieldAnimationUpdateRafId);
1337
- this.fieldAnimationUpdateRafId = null;
1338
- }
1339
- if (this.itemAnimationUpdateRafId !== null) {
1340
- cancelAnimationFrame(this.itemAnimationUpdateRafId);
1341
- this.itemAnimationUpdateRafId = null;
1342
- }
1343
- // Reset animation states when section changes
1344
- this.resetFieldAnimations();
1345
- this.resetItemAnimations();
1346
- this.fieldsAnimated = false;
1347
- this.itemsAnimated = false;
1348
- // Clear pending updates
1349
- this.pendingFieldAnimationUpdates.clear();
1350
- this.pendingItemAnimationUpdates.clear();
1351
- }
2675
+ /**
2676
+ * Get the current active theme
2677
+ */
2678
+ getCurrentTheme() {
2679
+ return this.currentThemeSubject.value;
1352
2680
  }
1353
2681
  /**
1354
- * Get animation class for a field based on its appearance state
2682
+ * Set theme to a built-in preset
2683
+ *
2684
+ * @param preset - Built-in theme preset name
1355
2685
  */
1356
- getFieldAnimationClass(fieldId, index) {
1357
- const state = this.fieldAnimationStates.get(fieldId);
1358
- if (state === 'entering') {
1359
- return 'field-streaming';
1360
- }
1361
- if (state === 'entered') {
1362
- return 'field-entered';
1363
- }
1364
- // New field - mark as entering
1365
- if (state === undefined || state === 'none') {
1366
- this.markFieldEntering(fieldId, index);
1367
- return 'field-streaming';
2686
+ setTheme(preset) {
2687
+ if (!this.rootElement) {
2688
+ return;
1368
2689
  }
1369
- return '';
2690
+ // Set data-theme attribute which triggers CSS variable changes
2691
+ this.rootElement.setAttribute('data-theme', preset);
2692
+ this.currentThemeSubject.next(preset);
1370
2693
  }
1371
2694
  /**
1372
- * Get animation class for an item based on its appearance state
2695
+ * Apply a custom theme configuration
2696
+ *
2697
+ * @param config - Custom theme configuration
1373
2698
  */
1374
- getItemAnimationClass(itemId, index) {
1375
- const state = this.itemAnimationStates.get(itemId);
1376
- if (state === 'entering') {
1377
- return 'item-streaming';
1378
- }
1379
- if (state === 'entered') {
1380
- return 'item-entered';
2699
+ applyCustomTheme(config) {
2700
+ if (!this.rootElement) {
2701
+ return;
1381
2702
  }
1382
- // New item - mark as entering
1383
- if (state === undefined || state === 'none') {
1384
- this.markItemEntering(itemId, index);
1385
- return 'item-streaming';
2703
+ // Store custom theme
2704
+ this.customThemes.set(config.name, config);
2705
+ // Set data-theme to custom
2706
+ this.rootElement.setAttribute('data-theme', 'custom');
2707
+ // Apply CSS variable overrides
2708
+ this.applyCSSVariables(config.variables);
2709
+ this.currentThemeSubject.next(config.name);
2710
+ }
2711
+ /**
2712
+ * Apply CSS variables to the document root
2713
+ *
2714
+ * @param variables - Object mapping CSS variable names to values
2715
+ */
2716
+ applyCSSVariables(variables) {
2717
+ if (!this.rootElement) {
2718
+ return;
1386
2719
  }
1387
- return '';
2720
+ Object.entries(variables).forEach(([key, value]) => {
2721
+ const varName = key.startsWith('--') ? key : `--${key}`;
2722
+ this.rootElement.style.setProperty(varName, value);
2723
+ });
1388
2724
  }
1389
2725
  /**
1390
- * Get stagger delay index for field animation
2726
+ * Reset CSS variables (useful when switching themes)
1391
2727
  */
1392
- getFieldStaggerIndex(index) {
1393
- return Math.min(index, 15);
2728
+ resetCSSVariables() {
2729
+ if (!this.rootElement) {
2730
+ return;
2731
+ }
2732
+ // Remove all custom CSS variables by cloning the root element's style
2733
+ // In practice, we'll let the CSS cascade handle this when switching themes
2734
+ const computedStyle = window.getComputedStyle(this.rootElement);
2735
+ // Remove any custom properties that aren't in the default theme
2736
+ // This is a simplified approach - in production you might want to track which vars were set
1394
2737
  }
1395
2738
  /**
1396
- * Get stagger delay index for item animation
2739
+ * Get a custom theme configuration
2740
+ *
2741
+ * @param name - Theme name
2742
+ * @returns Theme configuration or null if not found
1397
2743
  */
1398
- getItemStaggerIndex(index) {
1399
- return Math.min(index, 15);
2744
+ getCustomTheme(name) {
2745
+ return this.customThemes.get(name) ?? null;
1400
2746
  }
1401
2747
  /**
1402
- * Mark field as entering and schedule entered state
1403
- * Optimized: Batches change detection for better performance
2748
+ * Register a custom theme (without applying it)
2749
+ *
2750
+ * @param config - Custom theme configuration
1404
2751
  */
1405
- markFieldEntering(fieldId, index) {
1406
- this.fieldAnimationStates.set(fieldId, 'entering');
1407
- const appearanceTime = Date.now();
1408
- this.fieldAnimationTimes.set(fieldId, appearanceTime);
1409
- // Calculate total delay (stagger + animation duration)
1410
- const staggerDelay = index * this.FIELD_STAGGER_DELAY_MS;
1411
- const totalDelay = staggerDelay + this.FIELD_ANIMATION_DURATION_MS;
1412
- // Mark as entered after animation completes
1413
- // Batch change detection for multiple fields
1414
- setTimeout(() => {
1415
- // Only update if this is still the latest appearance
1416
- if (this.fieldAnimationTimes.get(fieldId) === appearanceTime) {
1417
- this.fieldAnimationStates.set(fieldId, 'entered');
1418
- // Batch change detection - add to pending updates
1419
- this.pendingFieldAnimationUpdates.add(fieldId);
1420
- this.scheduleBatchedFieldChangeDetection();
1421
- }
1422
- }, totalDelay);
2752
+ registerTheme(config) {
2753
+ this.customThemes.set(config.name, config);
1423
2754
  }
1424
2755
  /**
1425
- * Batch change detection for field animation state updates
2756
+ * Remove a custom theme
2757
+ *
2758
+ * @param name - Theme name to remove
2759
+ * @returns True if theme was removed, false if not found
1426
2760
  */
1427
- scheduleBatchedFieldChangeDetection() {
1428
- if (this.fieldAnimationUpdateRafId !== null) {
1429
- return; // Already scheduled
1430
- }
1431
- this.fieldAnimationUpdateRafId = requestAnimationFrame(() => {
1432
- if (this.pendingFieldAnimationUpdates.size > 0) {
1433
- // Single change detection for all pending updates
1434
- this.cdr.markForCheck();
1435
- this.pendingFieldAnimationUpdates.clear();
1436
- }
1437
- this.fieldAnimationUpdateRafId = null;
1438
- });
2761
+ unregisterTheme(name) {
2762
+ return this.customThemes.delete(name);
1439
2763
  }
1440
2764
  /**
1441
- * Mark item as entering and schedule entered state
1442
- * Optimized: Batches change detection for better performance
2765
+ * Get all registered custom themes
2766
+ *
2767
+ * @returns Array of custom theme configurations
1443
2768
  */
1444
- markItemEntering(itemId, index) {
1445
- this.itemAnimationStates.set(itemId, 'entering');
1446
- const appearanceTime = Date.now();
1447
- this.itemAnimationTimes.set(itemId, appearanceTime);
1448
- // Calculate total delay (stagger + animation duration)
1449
- const staggerDelay = index * this.ITEM_STAGGER_DELAY_MS;
1450
- const totalDelay = staggerDelay + this.ITEM_ANIMATION_DURATION_MS;
1451
- // Mark as entered after animation completes
1452
- // Batch change detection for multiple items
1453
- setTimeout(() => {
1454
- // Only update if this is still the latest appearance
1455
- if (this.itemAnimationTimes.get(itemId) === appearanceTime) {
1456
- this.itemAnimationStates.set(itemId, 'entered');
1457
- // Batch change detection - add to pending updates
1458
- this.pendingItemAnimationUpdates.add(itemId);
1459
- this.scheduleBatchedItemChangeDetection();
1460
- }
1461
- }, totalDelay);
2769
+ getCustomThemes() {
2770
+ return Array.from(this.customThemes.values());
1462
2771
  }
1463
2772
  /**
1464
- * Batch change detection for item animation state updates
2773
+ * Validate a theme configuration
2774
+ *
2775
+ * @param config - Theme configuration to validate
2776
+ * @returns Validation result with errors if any
1465
2777
  */
1466
- scheduleBatchedItemChangeDetection() {
1467
- if (this.itemAnimationUpdateRafId !== null) {
1468
- return; // Already scheduled
1469
- }
1470
- this.itemAnimationUpdateRafId = requestAnimationFrame(() => {
1471
- if (this.pendingItemAnimationUpdates.size > 0) {
1472
- // Single change detection for all pending updates
1473
- this.cdr.markForCheck();
1474
- this.pendingItemAnimationUpdates.clear();
2778
+ validateTheme(config) {
2779
+ const errors = [];
2780
+ if (!config.name || config.name.trim() === '') {
2781
+ errors.push('Theme name is required');
2782
+ }
2783
+ if (!config.variables || Object.keys(config.variables).length === 0) {
2784
+ errors.push('Theme must have at least one CSS variable');
2785
+ }
2786
+ // Validate CSS variable names
2787
+ Object.keys(config.variables).forEach(key => {
2788
+ if (!key.startsWith('--') && !key.match(/^[a-z-]+$/i)) {
2789
+ errors.push(`Invalid CSS variable name: ${key}`);
1475
2790
  }
1476
- this.itemAnimationUpdateRafId = null;
1477
2791
  });
2792
+ return {
2793
+ valid: errors.length === 0,
2794
+ errors
2795
+ };
1478
2796
  }
1479
- /**
1480
- * Reset field animation states
1481
- */
1482
- resetFieldAnimations() {
1483
- this.fieldAnimationStates.clear();
1484
- this.fieldAnimationTimes.clear();
2797
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ThemeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2798
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ThemeService, providedIn: 'root' }); }
2799
+ }
2800
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ThemeService, decorators: [{
2801
+ type: Injectable,
2802
+ args: [{
2803
+ providedIn: 'root'
2804
+ }]
2805
+ }], ctorParameters: () => [] });
2806
+
2807
+ /**
2808
+ * Light theme preset configuration
2809
+ * Based on the day theme from the design system
2810
+ */
2811
+ const lightTheme = {
2812
+ name: 'day',
2813
+ preset: true,
2814
+ variables: {
2815
+ '--background': '#ffffff',
2816
+ '--foreground': '#1c1c1f',
2817
+ '--muted': '#f4f4f6',
2818
+ '--muted-foreground': '#555861',
2819
+ '--card': 'color-mix(in srgb, var(--background) 99%, var(--surface-contrast-color) 1%)',
2820
+ '--card-foreground': '#1c1c1f',
2821
+ '--primary': '#FF7900',
2822
+ '--primary-foreground': '#ffffff',
2823
+ '--secondary': '#f5f5f5',
2824
+ '--secondary-foreground': '#1a1a1a',
2825
+ '--border': 'rgba(200, 200, 200, 0.5)',
2826
+ '--ring': 'rgba(255, 121, 0, 0.4)'
2827
+ }
2828
+ };
2829
+
2830
+ /**
2831
+ * Dark theme preset configuration
2832
+ * Based on the night theme from the design system
2833
+ */
2834
+ const darkTheme = {
2835
+ name: 'night',
2836
+ preset: true,
2837
+ variables: {
2838
+ '--background': '#0a0a0a',
2839
+ '--foreground': '#ffffff',
2840
+ '--muted': '#242424',
2841
+ '--muted-foreground': '#aaaaaa',
2842
+ '--card': 'color-mix(in srgb, var(--background) 99%, var(--surface-contrast-color) 1%)',
2843
+ '--card-foreground': '#ffffff',
2844
+ '--primary': '#FF7900',
2845
+ '--primary-foreground': '#ffffff',
2846
+ '--secondary': '#333333',
2847
+ '--secondary-foreground': '#ffffff',
2848
+ '--border': 'rgba(200, 200, 200, 0.3)',
2849
+ '--ring': 'rgba(255, 121, 0, 0.6)'
2850
+ }
2851
+ };
2852
+
2853
+ /**
2854
+ * High contrast theme preset for accessibility
2855
+ * Provides maximum contrast for better visibility
2856
+ */
2857
+ const highContrastTheme = {
2858
+ name: 'high-contrast',
2859
+ preset: true,
2860
+ variables: {
2861
+ '--background': '#ffffff',
2862
+ '--foreground': '#000000',
2863
+ '--muted': '#f0f0f0',
2864
+ '--muted-foreground': '#000000',
2865
+ '--card': '#ffffff',
2866
+ '--card-foreground': '#000000',
2867
+ '--primary': '#0066cc',
2868
+ '--primary-foreground': '#ffffff',
2869
+ '--secondary': '#e0e0e0',
2870
+ '--secondary-foreground': '#000000',
2871
+ '--border': '#000000',
2872
+ '--ring': '#0066cc',
2873
+ '--card-border': '2px solid #000000',
2874
+ '--section-border': '2px solid #000000',
2875
+ '--section-item-border': '2px solid #000000'
2876
+ }
2877
+ };
2878
+
2879
+ /**
2880
+ * Theme Presets
2881
+ *
2882
+ * Exports all built-in theme presets for easy importing
2883
+ */
2884
+
2885
+ /**
2886
+ * Theme Builder Utility
2887
+ *
2888
+ * Helper functions for building and modifying theme configurations
2889
+ */
2890
+ /**
2891
+ * Create a theme configuration from a base theme with overrides
2892
+ *
2893
+ * @param baseTheme - Base theme configuration
2894
+ * @param overrides - Variable overrides to apply
2895
+ * @returns New theme configuration with overrides applied
2896
+ *
2897
+ * @example
2898
+ * ```typescript
2899
+ * const customTheme = buildThemeFromBase(lightTheme, {
2900
+ * '--color-brand': '#ff0000',
2901
+ * '--card-padding': '24px'
2902
+ * });
2903
+ * ```
2904
+ */
2905
+ function buildThemeFromBase(baseTheme, overrides) {
2906
+ return {
2907
+ ...baseTheme,
2908
+ name: baseTheme.name + '-custom',
2909
+ preset: false,
2910
+ variables: {
2911
+ ...baseTheme.variables,
2912
+ ...overrides
2913
+ }
2914
+ };
2915
+ }
2916
+ /**
2917
+ * Merge multiple theme configurations
2918
+ * Later themes override earlier ones
2919
+ *
2920
+ * @param themes - Array of theme configurations to merge
2921
+ * @returns Merged theme configuration
2922
+ */
2923
+ function mergeThemes(...themes) {
2924
+ if (themes.length === 0) {
2925
+ throw new Error('At least one theme is required');
2926
+ }
2927
+ const firstTheme = themes[0];
2928
+ if (!firstTheme) {
2929
+ throw new Error('At least one theme is required');
2930
+ }
2931
+ const merged = {
2932
+ name: firstTheme.name,
2933
+ preset: false,
2934
+ variables: {}
2935
+ };
2936
+ themes.forEach(theme => {
2937
+ merged.variables = {
2938
+ ...merged.variables,
2939
+ ...theme.variables
2940
+ };
2941
+ });
2942
+ return merged;
2943
+ }
2944
+ /**
2945
+ * Create a theme with only specific CSS variables
2946
+ * Useful for partial theme customization
2947
+ *
2948
+ * @param name - Theme name
2949
+ * @param variables - CSS variables to include
2950
+ * @returns Theme configuration with only specified variables
2951
+ */
2952
+ function createPartialTheme(name, variables) {
2953
+ return {
2954
+ name,
2955
+ preset: false,
2956
+ variables
2957
+ };
2958
+ }
2959
+ /**
2960
+ * Validate CSS variable names
2961
+ *
2962
+ * @param variables - Object with CSS variable names as keys
2963
+ * @returns Array of invalid variable names
2964
+ */
2965
+ function validateCSSVariableNames(variables) {
2966
+ const invalid = [];
2967
+ Object.keys(variables).forEach(key => {
2968
+ // CSS custom properties should start with --
2969
+ if (!key.startsWith('--')) {
2970
+ invalid.push(key);
2971
+ return;
2972
+ }
2973
+ // Validate format: --property-name (kebab-case)
2974
+ const withoutPrefix = key.substring(2);
2975
+ if (!/^[a-z][a-z0-9-]*$/.test(withoutPrefix)) {
2976
+ invalid.push(key);
2977
+ }
2978
+ });
2979
+ return invalid;
2980
+ }
2981
+ /**
2982
+ * Generate a theme from a color palette
2983
+ * Automatically generates related CSS variables from base colors
2984
+ *
2985
+ * @param name - Theme name
2986
+ * @param colors - Color palette object
2987
+ * @returns Theme configuration with generated variables
2988
+ *
2989
+ * @example
2990
+ * ```typescript
2991
+ * const theme = generateThemeFromPalette('my-brand', {
2992
+ * primary: '#ff7900',
2993
+ * background: '#ffffff',
2994
+ * foreground: '#000000'
2995
+ * });
2996
+ * ```
2997
+ */
2998
+ function generateThemeFromPalette(name, colors) {
2999
+ const variables = {};
3000
+ if (colors.primary) {
3001
+ variables['--color-brand'] = colors.primary;
3002
+ variables['--primary'] = colors.primary;
3003
+ variables['--accent'] = colors.primary;
3004
+ }
3005
+ if (colors.background) {
3006
+ variables['--background'] = colors.background;
3007
+ }
3008
+ if (colors.foreground) {
3009
+ variables['--foreground'] = colors.foreground;
3010
+ variables['--card-foreground'] = colors.foreground;
3011
+ }
3012
+ if (colors.muted) {
3013
+ variables['--muted'] = colors.muted;
3014
+ }
3015
+ if (colors.border) {
3016
+ variables['--border'] = colors.border;
3017
+ }
3018
+ if (colors.secondary) {
3019
+ variables['--secondary'] = colors.secondary;
3020
+ }
3021
+ return {
3022
+ name,
3023
+ preset: false,
3024
+ variables
3025
+ };
3026
+ }
3027
+
3028
+ /**
3029
+ * Theme System
3030
+ *
3031
+ * Exports theme service, presets, and utilities
3032
+ */
3033
+
3034
+ /**
3035
+ * Create a basic company card
3036
+ *
3037
+ * @param options - Company card options
3038
+ * @returns AICardConfig for a company card
3039
+ *
3040
+ * @example
3041
+ * ```typescript
3042
+ * const card = createCompanyCard({
3043
+ * name: 'Acme Corp',
3044
+ * industry: 'Technology',
3045
+ * employees: '500+',
3046
+ * websiteUrl: 'https://acme.com'
3047
+ * });
3048
+ * ```
3049
+ */
3050
+ function createCompanyCard(options) {
3051
+ const { name, subtitle, industry, founded, employees, headquarters, revenue, growthRate, marketShare, websiteUrl, customSections = [], customActions = [] } = options;
3052
+ const sections = [
3053
+ {
3054
+ id: 'company-overview',
3055
+ title: 'Company Overview',
3056
+ type: 'info',
3057
+ fields: [
3058
+ ...(industry ? [{ id: 'industry', label: 'Industry', value: industry }] : []),
3059
+ ...(founded ? [{ id: 'founded', label: 'Founded', value: founded }] : []),
3060
+ ...(employees ? [{ id: 'employees', label: 'Employees', value: employees }] : []),
3061
+ ...(headquarters ? [{ id: 'headquarters', label: 'Headquarters', value: headquarters }] : []),
3062
+ ...(revenue ? [{ id: 'revenue', label: 'Annual Revenue', value: revenue }] : [])
3063
+ ].filter(Boolean)
3064
+ },
3065
+ ...(growthRate || marketShare ? [{
3066
+ id: 'key-metrics',
3067
+ title: 'Key Metrics',
3068
+ type: 'analytics',
3069
+ fields: [
3070
+ ...(growthRate ? [{
3071
+ id: 'growth-rate',
3072
+ label: 'Growth Rate',
3073
+ value: `${growthRate}% YoY`,
3074
+ percentage: growthRate,
3075
+ performance: growthRate > 20 ? 'excellent' : growthRate > 10 ? 'good' : 'fair',
3076
+ trend: 'up'
3077
+ }] : []),
3078
+ ...(marketShare ? [{
3079
+ id: 'market-share',
3080
+ label: 'Market Share',
3081
+ value: `${marketShare}%`,
3082
+ percentage: marketShare,
3083
+ performance: marketShare > 15 ? 'excellent' : marketShare > 10 ? 'good' : 'fair',
3084
+ trend: 'up'
3085
+ }] : [])
3086
+ ]
3087
+ }] : []),
3088
+ ...customSections
3089
+ ].filter(Boolean);
3090
+ const actions = [
3091
+ ...(websiteUrl ? [{
3092
+ id: 'view-website',
3093
+ label: 'View Website',
3094
+ type: 'website',
3095
+ variant: 'primary',
3096
+ icon: 'globe',
3097
+ url: websiteUrl
3098
+ }] : []),
3099
+ ...customActions
3100
+ ];
3101
+ return {
3102
+ id: `company-${name.toLowerCase().replace(/\s+/g, '-')}`,
3103
+ cardTitle: name,
3104
+ cardSubtitle: subtitle,
3105
+ cardType: 'company',
3106
+ sections: sections.filter(s => s.fields && s.fields.length > 0),
3107
+ actions: actions.length > 0 ? actions : undefined
3108
+ };
3109
+ }
3110
+ /**
3111
+ * Create an enhanced company card with more sections
3112
+ */
3113
+ function createEnhancedCompanyCard(options) {
3114
+ const baseCard = createCompanyCard(options);
3115
+ const { financials = [], products = [] } = options;
3116
+ // Add financials section if provided
3117
+ if (financials.length > 0) {
3118
+ baseCard.sections.push({
3119
+ id: 'financials',
3120
+ title: 'Financials',
3121
+ type: 'financials',
3122
+ fields: financials.map((f, i) => ({
3123
+ id: `financial-${i}`,
3124
+ label: f.label,
3125
+ value: f.value
3126
+ }))
3127
+ });
3128
+ }
3129
+ // Add products section if provided
3130
+ if (products.length > 0) {
3131
+ baseCard.sections.push({
3132
+ id: 'products',
3133
+ title: 'Products & Services',
3134
+ type: 'list',
3135
+ items: products.map((p, i) => ({
3136
+ id: `product-${i}`,
3137
+ title: p.name,
3138
+ description: p.description
3139
+ }))
3140
+ });
1485
3141
  }
3142
+ return baseCard;
3143
+ }
3144
+
3145
+ /**
3146
+ * Create a basic contact card
3147
+ *
3148
+ * @param options - Contact card options
3149
+ * @returns AICardConfig for a contact card
3150
+ *
3151
+ * @example
3152
+ * ```typescript
3153
+ * const card = createContactCard({
3154
+ * name: 'John Doe',
3155
+ * jobTitle: 'Sales Director',
3156
+ * email: 'john@example.com',
3157
+ * phone: '+1 555 1234'
3158
+ * });
3159
+ * ```
3160
+ */
3161
+ function createContactCard(options) {
3162
+ const { name, jobTitle, company, location, experience, email, phone, linkedIn, metrics = [], customSections = [], customActions = [] } = options;
3163
+ const sections = [
3164
+ {
3165
+ id: 'professional-profile',
3166
+ title: 'Professional Profile',
3167
+ type: 'info',
3168
+ fields: [
3169
+ ...(jobTitle ? [{ id: 'job-title', label: 'Job Title', value: jobTitle }] : []),
3170
+ ...(company ? [{ id: 'company', label: 'Company', value: company }] : []),
3171
+ ...(location ? [{ id: 'location', label: 'Location', value: location }] : []),
3172
+ ...(experience ? [{ id: 'experience', label: 'Experience', value: experience }] : []),
3173
+ ...(email ? [{ id: 'email', label: 'Email', value: email }] : []),
3174
+ ...(phone ? [{ id: 'phone', label: 'Phone', value: phone }] : [])
3175
+ ].filter(Boolean)
3176
+ },
3177
+ ...(metrics.length > 0 ? [{
3178
+ id: 'performance-metrics',
3179
+ title: 'Performance Metrics',
3180
+ type: 'analytics',
3181
+ fields: metrics.map((m, i) => ({
3182
+ id: `metric-${i}`,
3183
+ label: m.label,
3184
+ value: m.value,
3185
+ percentage: m.percentage,
3186
+ trend: m.trend || 'up'
3187
+ }))
3188
+ }] : []),
3189
+ ...customSections
3190
+ ].filter(Boolean);
3191
+ const actions = [
3192
+ ...(email ? [{
3193
+ id: 'send-email',
3194
+ label: 'Send Email',
3195
+ type: 'mail',
3196
+ variant: 'primary',
3197
+ icon: 'mail',
3198
+ email: {
3199
+ to: email,
3200
+ subject: `Hello ${name.split(' ')[0]}`,
3201
+ body: `Dear ${name},\n\n`
3202
+ }
3203
+ }] : []),
3204
+ ...(linkedIn ? [{
3205
+ id: 'linkedin',
3206
+ label: 'LinkedIn',
3207
+ type: 'website',
3208
+ variant: 'secondary',
3209
+ icon: 'linkedin',
3210
+ url: linkedIn
3211
+ }] : []),
3212
+ ...customActions
3213
+ ];
3214
+ return {
3215
+ id: `contact-${name.toLowerCase().replace(/\s+/g, '-')}`,
3216
+ cardTitle: name,
3217
+ cardSubtitle: jobTitle || company,
3218
+ cardType: 'contact',
3219
+ sections: sections.filter(s => s.fields && s.fields.length > 0),
3220
+ actions: actions.length > 0 ? actions : undefined
3221
+ };
3222
+ }
3223
+
3224
+ /**
3225
+ * Create an analytics dashboard card
3226
+ *
3227
+ * @param options - Analytics dashboard options
3228
+ * @returns AICardConfig for an analytics dashboard
3229
+ *
3230
+ * @example
3231
+ * ```typescript
3232
+ * const card = createAnalyticsDashboard({
3233
+ * title: 'Sales Performance',
3234
+ * kpis: [
3235
+ * { label: 'Revenue', value: '$1.2M', percentage: 105, trend: 'up' },
3236
+ * { label: 'Conversion Rate', value: '32%', percentage: 32, trend: 'up' }
3237
+ * ]
3238
+ * });
3239
+ * ```
3240
+ */
3241
+ function createAnalyticsDashboard(options) {
3242
+ const { title, subtitle, dashboardType, dataSource, updateFrequency, timeRange, kpis = [], chartData, dashboardUrl, customSections = [], customActions = [] } = options;
3243
+ const sections = [
3244
+ ...(dashboardType || dataSource || updateFrequency || timeRange ? [{
3245
+ id: 'analytics-overview',
3246
+ title: 'Analytics Overview',
3247
+ type: 'info',
3248
+ fields: [
3249
+ ...(dashboardType ? [{ id: 'dashboard-type', label: 'Dashboard Type', value: dashboardType }] : []),
3250
+ ...(dataSource ? [{ id: 'data-source', label: 'Data Source', value: dataSource }] : []),
3251
+ ...(updateFrequency ? [{ id: 'update-frequency', label: 'Update Frequency', value: updateFrequency }] : []),
3252
+ ...(timeRange ? [{ id: 'time-range', label: 'Time Range', value: timeRange }] : [])
3253
+ ].filter(Boolean)
3254
+ }] : []),
3255
+ ...(kpis.length > 0 ? [{
3256
+ id: 'kpis',
3257
+ title: 'Key Performance Indicators',
3258
+ type: 'analytics',
3259
+ fields: kpis.map((kpi, i) => ({
3260
+ id: `kpi-${i}`,
3261
+ label: kpi.label,
3262
+ value: kpi.value,
3263
+ percentage: kpi.percentage,
3264
+ performance: kpi.performance || (kpi.percentage && kpi.percentage > 100 ? 'excellent' : 'good'),
3265
+ trend: kpi.trend || 'up'
3266
+ }))
3267
+ }] : []),
3268
+ ...(chartData ? [{
3269
+ id: 'chart',
3270
+ title: 'Performance Chart',
3271
+ type: 'chart',
3272
+ chartType: 'bar',
3273
+ chartData: {
3274
+ labels: chartData.labels,
3275
+ datasets: chartData.datasets.map(ds => ({
3276
+ label: ds.label,
3277
+ data: ds.data,
3278
+ backgroundColor: 'rgba(255, 121, 0, 0.6)',
3279
+ borderColor: 'rgba(255, 121, 0, 1)',
3280
+ borderWidth: 2
3281
+ }))
3282
+ }
3283
+ }] : []),
3284
+ ...customSections
3285
+ ].filter(Boolean);
3286
+ const actions = [
3287
+ ...(dashboardUrl ? [{
3288
+ id: 'view-dashboard',
3289
+ label: 'View Full Dashboard',
3290
+ type: 'website',
3291
+ variant: 'primary',
3292
+ icon: 'bar-chart',
3293
+ url: dashboardUrl
3294
+ }] : []),
3295
+ ...customActions
3296
+ ];
3297
+ return {
3298
+ id: `analytics-${title.toLowerCase().replace(/\s+/g, '-')}`,
3299
+ cardTitle: title,
3300
+ cardSubtitle: subtitle,
3301
+ cardType: 'analytics',
3302
+ sections: sections.filter(s => (s.fields && s.fields.length > 0) || s.chartData),
3303
+ actions: actions.length > 0 ? actions : undefined
3304
+ };
3305
+ }
3306
+
3307
+ /**
3308
+ * Preset Factory
3309
+ *
3310
+ * Centralized factory for creating card presets with common configurations.
3311
+ *
3312
+ * @example
3313
+ * ```typescript
3314
+ * import { PresetFactory } from 'osi-cards-lib';
3315
+ *
3316
+ * // Create a company card
3317
+ * const companyCard = PresetFactory.createCompany({
3318
+ * name: 'Acme Corp',
3319
+ * industry: 'Technology'
3320
+ * });
3321
+ *
3322
+ * // Create a contact card
3323
+ * const contactCard = PresetFactory.createContact({
3324
+ * name: 'John Doe',
3325
+ * email: 'john@example.com'
3326
+ * });
3327
+ * ```
3328
+ */
3329
+ class PresetFactory {
1486
3330
  /**
1487
- * Reset item animation states
3331
+ * Create a company card
3332
+ *
3333
+ * @param options - Company card options
3334
+ * @returns Company card configuration
1488
3335
  */
1489
- resetItemAnimations() {
1490
- this.itemAnimationStates.clear();
1491
- this.itemAnimationTimes.clear();
3336
+ static createCompany(options) {
3337
+ return createCompanyCard(options);
1492
3338
  }
1493
3339
  /**
1494
- * Get field ID for tracking
3340
+ * Create an enhanced company card with additional sections
3341
+ *
3342
+ * @param options - Enhanced company card options
3343
+ * @returns Enhanced company card configuration
1495
3344
  */
1496
- getFieldId(field, index) {
1497
- return field.id || `field-${index}-${field.label || ''}`;
3345
+ static createEnhancedCompany(options) {
3346
+ return createEnhancedCompanyCard(options);
1498
3347
  }
1499
3348
  /**
1500
- * Get item ID for tracking
3349
+ * Create a contact card
3350
+ *
3351
+ * @param options - Contact card options
3352
+ * @returns Contact card configuration
1501
3353
  */
1502
- getItemId(item, index) {
1503
- return item.id || `item-${index}-${item.title || ''}`;
3354
+ static createContact(options) {
3355
+ return createContactCard(options);
1504
3356
  }
1505
3357
  /**
1506
- * Check if section has fields
1507
- * Public getter for template access
3358
+ * Create an analytics dashboard card
3359
+ *
3360
+ * @param options - Analytics dashboard options
3361
+ * @returns Analytics dashboard configuration
1508
3362
  */
1509
- get hasFields() {
1510
- return this.getFields().length > 0;
3363
+ static createAnalytics(options) {
3364
+ return createAnalyticsDashboard(options);
1511
3365
  }
1512
3366
  /**
1513
- * Check if section has items
1514
- * Public getter for template access
3367
+ * Create a custom card from a template
3368
+ *
3369
+ * @param template - Template function that returns AICardConfig
3370
+ * @returns Card configuration
1515
3371
  */
1516
- get hasItems() {
1517
- return this.getItems().length > 0;
3372
+ static createCustom(template, config) {
3373
+ return template(config);
1518
3374
  }
1519
- /**
1520
- * Emit field interaction event (standardized pattern)
1521
- */
1522
- emitFieldInteraction(field, metadata) {
1523
- this.fieldInteraction.emit({
1524
- field,
1525
- metadata: {
1526
- sectionId: this.section.id,
1527
- sectionTitle: this.section.title,
1528
- ...metadata
1529
- }
1530
- });
3375
+ }
3376
+ /**
3377
+ * Convenience factory functions (alternative to PresetFactory class)
3378
+ */
3379
+ /**
3380
+ * Create a company card (convenience function)
3381
+ */
3382
+ function createCompanyPreset(options) {
3383
+ return PresetFactory.createCompany(options);
3384
+ }
3385
+ /**
3386
+ * Create a contact card (convenience function)
3387
+ */
3388
+ function createContactPreset(options) {
3389
+ return PresetFactory.createContact(options);
3390
+ }
3391
+ /**
3392
+ * Create an analytics dashboard (convenience function)
3393
+ */
3394
+ function createAnalyticsPreset(options) {
3395
+ return PresetFactory.createAnalytics(options);
3396
+ }
3397
+
3398
+ /**
3399
+ * Card Presets
3400
+ *
3401
+ * Factory functions and presets for common card configurations
3402
+ */
3403
+
3404
+ /**
3405
+ * Wrapper component for dynamically loading and rendering plugin section components
3406
+ */
3407
+ class PluginSectionWrapperComponent {
3408
+ constructor() {
3409
+ this.sectionEvent = new EventEmitter();
3410
+ this.pluginRegistry = inject(SectionPluginRegistry);
3411
+ this.viewContainer = inject(ViewContainerRef);
3412
+ this.cdr = inject(ChangeDetectorRef);
3413
+ this.destroy$ = new Subject();
3414
+ this.componentRef = null;
1531
3415
  }
1532
- /**
1533
- * Emit item interaction event (standardized pattern)
1534
- */
1535
- emitItemInteraction(item, metadata) {
1536
- this.itemInteraction.emit({
1537
- item,
1538
- metadata: {
1539
- sectionId: this.section.id,
1540
- sectionTitle: this.section.title,
1541
- ...metadata
1542
- }
1543
- });
3416
+ ngOnInit() {
3417
+ this.loadPlugin();
1544
3418
  }
1545
- /**
1546
- * Phase 5: Perfect trackBy function for fields - uses stable field ID
1547
- * Can be overridden by child classes for custom tracking
1548
- */
1549
- trackField(index, field) {
1550
- return field.id || `field-${index}-${field.label || ''}`;
3419
+ ngAfterViewInit() {
3420
+ // Ensure plugin is loaded after view init
3421
+ if (!this.componentRef) {
3422
+ this.loadPlugin();
3423
+ }
1551
3424
  }
1552
- /**
1553
- * Phase 5: Perfect trackBy function for items - uses stable item ID
1554
- * Can be overridden by child classes for custom tracking
1555
- */
1556
- trackItem(index, item) {
1557
- return item.id || `item-${index}-${item.title || ''}`;
3425
+ ngOnDestroy() {
3426
+ this.destroy$.next();
3427
+ this.destroy$.complete();
3428
+ if (this.componentRef) {
3429
+ this.componentRef.destroy();
3430
+ }
1558
3431
  }
1559
- // Display methods removed - each component now implements its own to avoid TypeScript override conflicts
1560
- // The logic is consistent: filter out "Streaming…" placeholder text
1561
- /**
1562
- * Safe value accessor - extracts value from field with fallback options
1563
- * Handles field.value, field.text, field.quote based on field type
1564
- */
1565
- getFieldValue(field) {
1566
- // Try value first (most common)
1567
- if (field.value !== undefined && field.value !== null) {
1568
- return field.value;
3432
+ loadPlugin() {
3433
+ if (!this.section) {
3434
+ return;
1569
3435
  }
1570
- // Try text (for text-reference fields)
1571
- if ('text' in field && field.text !== undefined && field.text !== null) {
1572
- return field.text;
3436
+ const componentType = this.pluginRegistry.getComponentForSection(this.section);
3437
+ if (!componentType) {
3438
+ console.warn(`No plugin registered for section type: ${this.section.type}`);
3439
+ return;
1573
3440
  }
1574
- // Try quote (for quotation fields)
1575
- if ('quote' in field && field.quote !== undefined && field.quote !== null) {
1576
- return field.quote;
3441
+ // Clear existing component
3442
+ if (this.componentRef) {
3443
+ this.componentRef.destroy();
3444
+ }
3445
+ // Create plugin component
3446
+ this.componentRef = this.viewContainer.createComponent(componentType);
3447
+ this.componentRef.instance.section = this.section;
3448
+ // Wire up event emitters
3449
+ if (this.componentRef.instance.fieldInteraction) {
3450
+ this.componentRef.instance.fieldInteraction
3451
+ .pipe(takeUntil(this.destroy$))
3452
+ .subscribe(event => {
3453
+ this.sectionEvent.emit({
3454
+ type: 'field',
3455
+ section: this.section,
3456
+ field: event.field,
3457
+ metadata: event.metadata
3458
+ });
3459
+ });
1577
3460
  }
1578
- return undefined;
1579
- }
1580
- /**
1581
- * Safe metadata accessor - extracts metadata value safely
1582
- */
1583
- getMetaValue(field, key) {
1584
- return field.meta?.[key];
1585
- }
1586
- /**
1587
- * Check if a value represents streaming placeholder
1588
- */
1589
- isStreamingPlaceholder(value) {
1590
- return value === 'Streaming…' || value === 'Streaming...';
3461
+ if (this.componentRef.instance.itemInteraction) {
3462
+ this.componentRef.instance.itemInteraction
3463
+ .pipe(takeUntil(this.destroy$))
3464
+ .subscribe(event => {
3465
+ this.sectionEvent.emit({
3466
+ type: 'item',
3467
+ section: this.section,
3468
+ item: event.item,
3469
+ metadata: event.metadata
3470
+ });
3471
+ });
3472
+ }
3473
+ this.cdr.markForCheck();
1591
3474
  }
1592
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: BaseSectionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1593
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: BaseSectionComponent, isStandalone: true, selector: "ng-component", inputs: { section: "section" }, outputs: { fieldInteraction: "fieldInteraction", itemInteraction: "itemInteraction" }, usesOnChanges: true, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
3475
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PluginSectionWrapperComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3476
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: PluginSectionWrapperComponent, isStandalone: true, selector: "app-plugin-section-wrapper", inputs: { section: "section" }, outputs: { sectionEvent: "sectionEvent" }, ngImport: i0, template: `<!-- Plugin component rendered dynamically -->`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }] }); }
1594
3477
  }
1595
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: BaseSectionComponent, decorators: [{
3478
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PluginSectionWrapperComponent, decorators: [{
1596
3479
  type: Component,
1597
3480
  args: [{
1598
- template: '',
1599
- changeDetection: ChangeDetectionStrategy.OnPush
3481
+ selector: 'app-plugin-section-wrapper',
3482
+ standalone: true,
3483
+ imports: [CommonModule],
3484
+ template: `<!-- Plugin component rendered dynamically -->`
1600
3485
  }]
1601
3486
  }], propDecorators: { section: [{
1602
3487
  type: Input,
1603
3488
  args: [{ required: true }]
1604
- }], fieldInteraction: [{
1605
- type: Output
1606
- }], itemInteraction: [{
3489
+ }], sectionEvent: [{
1607
3490
  type: Output
1608
3491
  }] } });
1609
3492
 
@@ -1874,11 +3757,11 @@ class ProductSectionComponent extends BaseSectionComponent {
1874
3757
  return orderA - orderB;
1875
3758
  });
1876
3759
  return orderedKeys.map((key) => {
1877
- const config = this.categoryConfig[key] ?? this.categoryConfig['default'];
3760
+ const config = this.categoryConfig[key] ?? this.categoryConfig['default'] ?? { title: key, icon: 'circle' };
1878
3761
  return {
1879
3762
  key,
1880
- title: config.title,
1881
- icon: config.icon,
3763
+ title: config.title || key,
3764
+ icon: config.icon || 'circle',
1882
3765
  fields: groups.get(key) ?? []
1883
3766
  };
1884
3767
  });
@@ -2174,7 +4057,7 @@ class ChartSectionComponent extends BaseSectionComponent {
2174
4057
  }
2175
4058
  }
2176
4059
  getColor(field, index) {
2177
- return field.color ?? this.palette[index % this.palette.length];
4060
+ return field.color ?? (this.palette[index % this.palette.length] || '#FF7900');
2178
4061
  }
2179
4062
  /**
2180
4063
  * Get display value, hiding "Streaming…" placeholder text
@@ -2238,15 +4121,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
2238
4121
  args: [{ selector: 'app-overview-section', standalone: true, imports: [CommonModule, LucideIconsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ai-section ai-section--overview\">\n <div class=\"ai-section__header\">\n <div class=\"ai-section__details\">\n <h3 class=\"ai-section__title\">{{ section.title }}</h3>\n <p *ngIf=\"section.description\" class=\"ai-section__description\">{{ section.description }}</p>\n </div>\n </div>\n\n <div class=\"ai-section__body\">\n <ng-container *ngIf=\"hasFields; else overviewEmpty\">\n <div class=\"overview-grid\">\n <article\n *ngFor=\"let field of fields; trackBy: trackField\"\n class=\"overview-card\"\n role=\"button\"\n tabindex=\"0\"\n (click)=\"onFieldClick(field)\"\n (keydown.enter)=\"onFieldClick(field)\"\n (keydown.space)=\"$event.preventDefault(); onFieldClick(field)\"\n >\n <div class=\"overview-card__content\">\n <span class=\"overview-card__label\">\n {{ field.label || field.title }}\n </span>\n <span class=\"overview-card__value\">\n {{ getDisplayValue(field) }}\n </span>\n </div>\n </article>\n </div>\n </ng-container>\n\n <ng-template #overviewEmpty>\n <div class=\"section-empty\">\n <lucide-icon name=\"alert-circle\" [size]=\"32\" class=\"mb-4 opacity-50\" aria-hidden=\"true\"></lucide-icon>\n <p class=\"text-sm\">No overview information available</p>\n </div>\n </ng-template>\n </div>\n</div>\n" }]
2239
4122
  }] });
2240
4123
 
2241
- class FallbackSectionComponent extends BaseSectionComponent {
2242
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: FallbackSectionComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
2243
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: FallbackSectionComponent, isStandalone: true, selector: "app-fallback-section", usesInheritance: true, ngImport: i0, template: "<div class=\"ai-section\">\n <div class=\"ai-section__header\">\n <div class=\"ai-section__details\">\n <h3 class=\"ai-section__title\">{{ section.title || 'Unsupported Section' }}</h3>\n <p *ngIf=\"section.description\" class=\"ai-section__description\">{{ section.description }}</p>\n </div>\n </div>\n\n <div class=\"ai-section__body\">\n <div class=\"section-empty\">\n <lucide-icon name=\"alert-circle\" size=\"32\" class=\"mb-3 opacity-60\"></lucide-icon>\n <p class=\"text-sm\">This section type is not yet customized. Add data or configure a renderer to display it.</p>\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: LucideIconsModule }, { kind: "component", type: i2.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2244
- }
2245
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: FallbackSectionComponent, decorators: [{
2246
- type: Component,
2247
- args: [{ selector: 'app-fallback-section', standalone: true, imports: [CommonModule, LucideIconsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ai-section\">\n <div class=\"ai-section__header\">\n <div class=\"ai-section__details\">\n <h3 class=\"ai-section__title\">{{ section.title || 'Unsupported Section' }}</h3>\n <p *ngIf=\"section.description\" class=\"ai-section__description\">{{ section.description }}</p>\n </div>\n </div>\n\n <div class=\"ai-section__body\">\n <div class=\"section-empty\">\n <lucide-icon name=\"alert-circle\" size=\"32\" class=\"mb-3 opacity-60\"></lucide-icon>\n <p class=\"text-sm\">This section type is not yet customized. Add data or configure a renderer to display it.</p>\n </div>\n </div>\n</div>\n" }]
2248
- }] });
2249
-
2250
4124
  class QuotationSectionComponent extends BaseSectionComponent {
2251
4125
  get fields() {
2252
4126
  return super.getFields();
@@ -2350,7 +4224,7 @@ class BrandColorsSectionComponent extends BaseSectionComponent {
2350
4224
  }
2351
4225
  hexToRgb(hex) {
2352
4226
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
2353
- if (result) {
4227
+ if (result && result[1] && result[2] && result[3]) {
2354
4228
  const r = parseInt(result[1], 16);
2355
4229
  const g = parseInt(result[2], 16);
2356
4230
  const b = parseInt(result[3], 16);
@@ -2399,6 +4273,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
2399
4273
  class SectionRendererComponent {
2400
4274
  constructor() {
2401
4275
  this.sectionEvent = new EventEmitter();
4276
+ this.pluginRegistry = inject(SectionPluginRegistry);
2402
4277
  }
2403
4278
  // Removed @HostBinding - will be set in template instead to avoid setAttribute errors
2404
4279
  get sectionTypeAttribute() {
@@ -2425,7 +4300,35 @@ class SectionRendererComponent {
2425
4300
  return null;
2426
4301
  }
2427
4302
  }
4303
+ /**
4304
+ * Get the component type for the current section, checking plugins first
4305
+ */
4306
+ get sectionComponent() {
4307
+ if (!this.section) {
4308
+ return null;
4309
+ }
4310
+ // Check if a plugin is registered for this section type
4311
+ const pluginComponent = this.pluginRegistry.getComponentForSection(this.section);
4312
+ if (pluginComponent) {
4313
+ return pluginComponent;
4314
+ }
4315
+ // Fall back to built-in sections (handled by resolvedType in template)
4316
+ return null;
4317
+ }
4318
+ /**
4319
+ * Check if current section uses a plugin
4320
+ */
4321
+ get usesPlugin() {
4322
+ if (!this.section?.type) {
4323
+ return false;
4324
+ }
4325
+ return this.pluginRegistry.hasPlugin(this.section.type.toLowerCase());
4326
+ }
2428
4327
  get resolvedType() {
4328
+ // If a plugin is registered, don't resolve type (plugin handles it)
4329
+ if (this.usesPlugin) {
4330
+ return this.section.type?.toLowerCase() || 'unknown';
4331
+ }
2429
4332
  if (!this.section) {
2430
4333
  return 'unknown';
2431
4334
  }
@@ -2511,13 +4414,20 @@ class SectionRendererComponent {
2511
4414
  metadata
2512
4415
  });
2513
4416
  }
4417
+ /**
4418
+ * Handle events from plugin components
4419
+ */
4420
+ onPluginSectionEvent(event) {
4421
+ this.sectionEvent.emit(event);
4422
+ }
2514
4423
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SectionRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2515
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: SectionRendererComponent, isStandalone: true, selector: "app-section-renderer", inputs: { section: "section" }, outputs: { sectionEvent: "sectionEvent" }, ngImport: i0, template: "<ng-container [ngSwitch]=\"resolvedType\">\n <app-overview-section\n *ngSwitchCase=\"'overview'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-overview-section>\n\n <app-info-section\n *ngSwitchCase=\"'info'\"\n [section]=\"section\"\n (infoFieldInteraction)=\"onInfoFieldInteraction($event)\"\n ></app-info-section>\n\n <app-analytics-section\n *ngSwitchCase=\"'analytics'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-analytics-section>\n\n <app-financials-section\n *ngSwitchCase=\"'financials'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-financials-section>\n\n <app-list-section\n *ngSwitchCase=\"'list'\"\n [section]=\"section\"\n (itemInteraction)=\"emitItemInteraction($event.item!, $event.metadata)\"\n ></app-list-section>\n\n <app-event-section\n *ngSwitchCase=\"'event'\"\n [section]=\"section\"\n (itemInteraction)=\"emitItemInteraction($event.item!, $event.metadata)\"\n ></app-event-section>\n\n <app-product-section\n *ngSwitchCase=\"'product'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-product-section>\n\n <app-solutions-section\n *ngSwitchCase=\"'solutions'\"\n [section]=\"section\"\n (itemInteraction)=\"emitItemInteraction($event.item!, $event.metadata)\"\n ></app-solutions-section>\n\n <app-contact-card-section\n *ngSwitchCase=\"'contact-card'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-contact-card-section>\n\n <app-network-card-section\n *ngSwitchCase=\"'network-card'\"\n [section]=\"section\"\n (itemInteraction)=\"emitItemInteraction($event.item!, $event.metadata)\"\n ></app-network-card-section>\n\n <app-map-section\n *ngSwitchCase=\"'map'\"\n [section]=\"section\"\n (itemInteraction)=\"emitItemInteraction($event.item!, $event.metadata)\"\n ></app-map-section>\n\n <app-chart-section\n *ngSwitchCase=\"'chart'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-chart-section>\n\n <app-quotation-section\n *ngSwitchCase=\"'quotation'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-quotation-section>\n\n <app-text-reference-section\n *ngSwitchCase=\"'text-reference'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-text-reference-section>\n\n <app-brand-colors-section\n *ngSwitchCase=\"'brand-colors'\"\n [section]=\"section\"\n ></app-brand-colors-section>\n\n <app-fallback-section\n *ngSwitchDefault\n [section]=\"section\"\n ></app-fallback-section>\n</ng-container>\n", styles: [":host{position:relative;display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "component", type: InfoSectionComponent, selector: "app-info-section", outputs: ["infoFieldInteraction"] }, { kind: "component", type: AnalyticsSectionComponent, selector: "app-analytics-section" }, { kind: "component", type: FinancialsSectionComponent, selector: "app-financials-section" }, { kind: "component", type: ListSectionComponent, selector: "app-list-section" }, { kind: "component", type: EventSectionComponent, selector: "app-event-section" }, { kind: "component", type: ProductSectionComponent, selector: "app-product-section" }, { kind: "component", type: SolutionsSectionComponent, selector: "app-solutions-section" }, { kind: "component", type: ContactCardSectionComponent, selector: "app-contact-card-section" }, { kind: "component", type: NetworkCardSectionComponent, selector: "app-network-card-section" }, { kind: "component", type: MapSectionComponent, selector: "app-map-section" }, { kind: "component", type: ChartSectionComponent, selector: "app-chart-section" }, { kind: "component", type: OverviewSectionComponent, selector: "app-overview-section" }, { kind: "component", type: FallbackSectionComponent, selector: "app-fallback-section" }, { kind: "component", type: QuotationSectionComponent, selector: "app-quotation-section" }, { kind: "component", type: TextReferenceSectionComponent, selector: "app-text-reference-section" }, { kind: "component", type: BrandColorsSectionComponent, selector: "app-brand-colors-section" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
4424
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: SectionRendererComponent, isStandalone: true, selector: "app-section-renderer", inputs: { section: "section" }, outputs: { sectionEvent: "sectionEvent" }, ngImport: i0, template: "<!-- Plugin components are rendered dynamically via wrapper component -->\n<app-plugin-section-wrapper\n *ngIf=\"usesPlugin\"\n [section]=\"section\"\n (sectionEvent)=\"onPluginSectionEvent($event)\">\n</app-plugin-section-wrapper>\n\n<!-- Built-in sections rendered via switch statement -->\n<ng-container *ngIf=\"!usesPlugin\" [ngSwitch]=\"resolvedType\">\n <app-overview-section\n *ngSwitchCase=\"'overview'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-overview-section>\n\n <app-info-section\n *ngSwitchCase=\"'info'\"\n [section]=\"section\"\n (infoFieldInteraction)=\"onInfoFieldInteraction($event)\"\n ></app-info-section>\n\n <app-analytics-section\n *ngSwitchCase=\"'analytics'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-analytics-section>\n\n <app-financials-section\n *ngSwitchCase=\"'financials'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-financials-section>\n\n <app-list-section\n *ngSwitchCase=\"'list'\"\n [section]=\"section\"\n (itemInteraction)=\"emitItemInteraction($event.item!, $event.metadata)\"\n ></app-list-section>\n\n <app-event-section\n *ngSwitchCase=\"'event'\"\n [section]=\"section\"\n (itemInteraction)=\"emitItemInteraction($event.item!, $event.metadata)\"\n ></app-event-section>\n\n <app-product-section\n *ngSwitchCase=\"'product'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-product-section>\n\n <app-solutions-section\n *ngSwitchCase=\"'solutions'\"\n [section]=\"section\"\n (itemInteraction)=\"emitItemInteraction($event.item!, $event.metadata)\"\n ></app-solutions-section>\n\n <app-contact-card-section\n *ngSwitchCase=\"'contact-card'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-contact-card-section>\n\n <app-network-card-section\n *ngSwitchCase=\"'network-card'\"\n [section]=\"section\"\n (itemInteraction)=\"emitItemInteraction($event.item!, $event.metadata)\"\n ></app-network-card-section>\n\n <app-map-section\n *ngSwitchCase=\"'map'\"\n [section]=\"section\"\n (itemInteraction)=\"emitItemInteraction($event.item!, $event.metadata)\"\n ></app-map-section>\n\n <app-chart-section\n *ngSwitchCase=\"'chart'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-chart-section>\n\n <app-quotation-section\n *ngSwitchCase=\"'quotation'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-quotation-section>\n\n <app-text-reference-section\n *ngSwitchCase=\"'text-reference'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-text-reference-section>\n\n <app-brand-colors-section\n *ngSwitchCase=\"'brand-colors'\"\n [section]=\"section\"\n ></app-brand-colors-section>\n\n <app-fallback-section\n *ngSwitchDefault\n [section]=\"section\"\n ></app-fallback-section>\n</ng-container>\n", styles: [":host{position:relative;display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "component", type: PluginSectionWrapperComponent, selector: "app-plugin-section-wrapper", inputs: ["section"], outputs: ["sectionEvent"] }, { kind: "component", type: InfoSectionComponent, selector: "app-info-section", outputs: ["infoFieldInteraction"] }, { kind: "component", type: AnalyticsSectionComponent, selector: "app-analytics-section" }, { kind: "component", type: FinancialsSectionComponent, selector: "app-financials-section" }, { kind: "component", type: ListSectionComponent, selector: "app-list-section" }, { kind: "component", type: EventSectionComponent, selector: "app-event-section" }, { kind: "component", type: ProductSectionComponent, selector: "app-product-section" }, { kind: "component", type: SolutionsSectionComponent, selector: "app-solutions-section" }, { kind: "component", type: ContactCardSectionComponent, selector: "app-contact-card-section" }, { kind: "component", type: NetworkCardSectionComponent, selector: "app-network-card-section" }, { kind: "component", type: MapSectionComponent, selector: "app-map-section" }, { kind: "component", type: ChartSectionComponent, selector: "app-chart-section" }, { kind: "component", type: OverviewSectionComponent, selector: "app-overview-section" }, { kind: "component", type: FallbackSectionComponent, selector: "app-fallback-section" }, { kind: "component", type: QuotationSectionComponent, selector: "app-quotation-section" }, { kind: "component", type: TextReferenceSectionComponent, selector: "app-text-reference-section" }, { kind: "component", type: BrandColorsSectionComponent, selector: "app-brand-colors-section" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2516
4425
  }
2517
4426
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SectionRendererComponent, decorators: [{
2518
4427
  type: Component,
2519
4428
  args: [{ selector: 'app-section-renderer', standalone: true, imports: [
2520
4429
  CommonModule,
4430
+ PluginSectionWrapperComponent,
2521
4431
  InfoSectionComponent,
2522
4432
  AnalyticsSectionComponent,
2523
4433
  FinancialsSectionComponent,
@@ -2534,7 +4444,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
2534
4444
  QuotationSectionComponent,
2535
4445
  TextReferenceSectionComponent,
2536
4446
  BrandColorsSectionComponent
2537
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container [ngSwitch]=\"resolvedType\">\n <app-overview-section\n *ngSwitchCase=\"'overview'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-overview-section>\n\n <app-info-section\n *ngSwitchCase=\"'info'\"\n [section]=\"section\"\n (infoFieldInteraction)=\"onInfoFieldInteraction($event)\"\n ></app-info-section>\n\n <app-analytics-section\n *ngSwitchCase=\"'analytics'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-analytics-section>\n\n <app-financials-section\n *ngSwitchCase=\"'financials'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-financials-section>\n\n <app-list-section\n *ngSwitchCase=\"'list'\"\n [section]=\"section\"\n (itemInteraction)=\"emitItemInteraction($event.item!, $event.metadata)\"\n ></app-list-section>\n\n <app-event-section\n *ngSwitchCase=\"'event'\"\n [section]=\"section\"\n (itemInteraction)=\"emitItemInteraction($event.item!, $event.metadata)\"\n ></app-event-section>\n\n <app-product-section\n *ngSwitchCase=\"'product'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-product-section>\n\n <app-solutions-section\n *ngSwitchCase=\"'solutions'\"\n [section]=\"section\"\n (itemInteraction)=\"emitItemInteraction($event.item!, $event.metadata)\"\n ></app-solutions-section>\n\n <app-contact-card-section\n *ngSwitchCase=\"'contact-card'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-contact-card-section>\n\n <app-network-card-section\n *ngSwitchCase=\"'network-card'\"\n [section]=\"section\"\n (itemInteraction)=\"emitItemInteraction($event.item!, $event.metadata)\"\n ></app-network-card-section>\n\n <app-map-section\n *ngSwitchCase=\"'map'\"\n [section]=\"section\"\n (itemInteraction)=\"emitItemInteraction($event.item!, $event.metadata)\"\n ></app-map-section>\n\n <app-chart-section\n *ngSwitchCase=\"'chart'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-chart-section>\n\n <app-quotation-section\n *ngSwitchCase=\"'quotation'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-quotation-section>\n\n <app-text-reference-section\n *ngSwitchCase=\"'text-reference'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-text-reference-section>\n\n <app-brand-colors-section\n *ngSwitchCase=\"'brand-colors'\"\n [section]=\"section\"\n ></app-brand-colors-section>\n\n <app-fallback-section\n *ngSwitchDefault\n [section]=\"section\"\n ></app-fallback-section>\n</ng-container>\n", styles: [":host{position:relative;display:block}\n"] }]
4447
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Plugin components are rendered dynamically via wrapper component -->\n<app-plugin-section-wrapper\n *ngIf=\"usesPlugin\"\n [section]=\"section\"\n (sectionEvent)=\"onPluginSectionEvent($event)\">\n</app-plugin-section-wrapper>\n\n<!-- Built-in sections rendered via switch statement -->\n<ng-container *ngIf=\"!usesPlugin\" [ngSwitch]=\"resolvedType\">\n <app-overview-section\n *ngSwitchCase=\"'overview'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-overview-section>\n\n <app-info-section\n *ngSwitchCase=\"'info'\"\n [section]=\"section\"\n (infoFieldInteraction)=\"onInfoFieldInteraction($event)\"\n ></app-info-section>\n\n <app-analytics-section\n *ngSwitchCase=\"'analytics'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-analytics-section>\n\n <app-financials-section\n *ngSwitchCase=\"'financials'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-financials-section>\n\n <app-list-section\n *ngSwitchCase=\"'list'\"\n [section]=\"section\"\n (itemInteraction)=\"emitItemInteraction($event.item!, $event.metadata)\"\n ></app-list-section>\n\n <app-event-section\n *ngSwitchCase=\"'event'\"\n [section]=\"section\"\n (itemInteraction)=\"emitItemInteraction($event.item!, $event.metadata)\"\n ></app-event-section>\n\n <app-product-section\n *ngSwitchCase=\"'product'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-product-section>\n\n <app-solutions-section\n *ngSwitchCase=\"'solutions'\"\n [section]=\"section\"\n (itemInteraction)=\"emitItemInteraction($event.item!, $event.metadata)\"\n ></app-solutions-section>\n\n <app-contact-card-section\n *ngSwitchCase=\"'contact-card'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-contact-card-section>\n\n <app-network-card-section\n *ngSwitchCase=\"'network-card'\"\n [section]=\"section\"\n (itemInteraction)=\"emitItemInteraction($event.item!, $event.metadata)\"\n ></app-network-card-section>\n\n <app-map-section\n *ngSwitchCase=\"'map'\"\n [section]=\"section\"\n (itemInteraction)=\"emitItemInteraction($event.item!, $event.metadata)\"\n ></app-map-section>\n\n <app-chart-section\n *ngSwitchCase=\"'chart'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-chart-section>\n\n <app-quotation-section\n *ngSwitchCase=\"'quotation'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-quotation-section>\n\n <app-text-reference-section\n *ngSwitchCase=\"'text-reference'\"\n [section]=\"section\"\n (fieldInteraction)=\"emitFieldInteraction($event.field!, $event.metadata)\"\n ></app-text-reference-section>\n\n <app-brand-colors-section\n *ngSwitchCase=\"'brand-colors'\"\n [section]=\"section\"\n ></app-brand-colors-section>\n\n <app-fallback-section\n *ngSwitchDefault\n [section]=\"section\"\n ></app-fallback-section>\n</ng-container>\n", styles: [":host{position:relative;display:block}\n"] }]
2538
4448
  }], propDecorators: { section: [{
2539
4449
  type: Input,
2540
4450
  args: [{ required: true }]
@@ -2870,6 +4780,8 @@ class AICardRendererComponent {
2870
4780
  constructor() {
2871
4781
  this.el = inject(ElementRef);
2872
4782
  this.cdr = inject(ChangeDetectorRef);
4783
+ this.injector = inject(Injector);
4784
+ this.platformId = inject(PLATFORM_ID);
2873
4785
  // Expose Math for template
2874
4786
  this.Math = Math;
2875
4787
  this.previousSectionsHash = '';
@@ -3044,6 +4956,10 @@ class AICardRendererComponent {
3044
4956
  return this._changeType;
3045
4957
  }
3046
4958
  ngOnInit() {
4959
+ // Validate animations provider in development mode
4960
+ if (isDevMode() && isPlatformBrowser(this.platformId)) {
4961
+ this.validateAnimationsProvider();
4962
+ }
3047
4963
  // Initialize particles
3048
4964
  this.initializeParticles();
3049
4965
  // Start message rotation
@@ -3149,13 +5065,17 @@ class AICardRendererComponent {
3149
5065
  }));
3150
5066
  }
3151
5067
  startMessageRotation() {
3152
- this.currentMessage = this.funnyMessages[0];
5068
+ if (this.funnyMessages.length === 0) {
5069
+ this.currentMessage = 'Loading...';
5070
+ return;
5071
+ }
5072
+ this.currentMessage = this.funnyMessages[0] || 'Loading...';
3153
5073
  this.currentMessageIndex = 0;
3154
5074
  interval(2500) // Change message every 2.5 seconds
3155
5075
  .pipe(takeUntil(this.destroyed$))
3156
5076
  .subscribe(() => {
3157
5077
  this.currentMessageIndex = (this.currentMessageIndex + 1) % this.funnyMessages.length;
3158
- this.currentMessage = this.funnyMessages[this.currentMessageIndex];
5078
+ this.currentMessage = this.funnyMessages[this.currentMessageIndex] || 'Loading...';
3159
5079
  this.cdr.markForCheck();
3160
5080
  });
3161
5081
  }
@@ -3706,6 +5626,33 @@ class AICardRendererComponent {
3706
5626
  this.normalizedSectionCache = new WeakMap();
3707
5627
  this.cdr.markForCheck();
3708
5628
  }
5629
+ /**
5630
+ * Validates that animation providers are configured.
5631
+ * Warns in development mode if animations are not available.
5632
+ */
5633
+ validateAnimationsProvider() {
5634
+ try {
5635
+ // Try to inject AnimationBuilder - if animations are not provided, this will throw
5636
+ const animationBuilder = this.injector.get(AnimationBuilder, null, { optional: true });
5637
+ if (!animationBuilder) {
5638
+ console.warn('⚠️ OSI Cards Library: Animation providers may not be configured.\n' +
5639
+ 'The library requires animation providers to function correctly.\n' +
5640
+ 'Please add provideOSICards() to your app.config.ts providers array:\n\n' +
5641
+ ' import { provideOSICards } from \'osi-cards-lib\';\n' +
5642
+ ' export const appConfig: ApplicationConfig = {\n' +
5643
+ ' providers: [provideOSICards(), ...]\n' +
5644
+ ' };\n\n' +
5645
+ 'See https://github.com/Inutilepat83/OSI-Cards for more information.');
5646
+ }
5647
+ }
5648
+ catch (error) {
5649
+ // AnimationBuilder injection failed - likely no animations provider
5650
+ console.warn('⚠️ OSI Cards Library: Animation providers are not configured.\n' +
5651
+ 'The library requires animation providers for proper functionality.\n' +
5652
+ 'Please add provideOSICards() to your app.config.ts providers array.\n' +
5653
+ 'See documentation for setup instructions.');
5654
+ }
5655
+ }
3709
5656
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AICardRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3710
5657
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: AICardRendererComponent, isStandalone: true, selector: "app-ai-card-renderer", inputs: { cardConfig: "cardConfig", updateSource: "updateSource", isFullscreen: "isFullscreen", tiltEnabled: "tiltEnabled", streamingStage: "streamingStage", streamingProgress: "streamingProgress", streamingProgressLabel: "streamingProgressLabel", changeType: "changeType" }, outputs: { fieldInteraction: "fieldInteraction", cardInteraction: "cardInteraction", fullscreenToggle: "fullscreenToggle", agentAction: "agentAction", questionAction: "questionAction" }, viewQueries: [{ propertyName: "cardContainer", first: true, predicate: ["cardContainer"], descendants: true }, { propertyName: "tiltContainerRef", first: true, predicate: ["tiltContainer"], descendants: true }, { propertyName: "masonryGrid", first: true, predicate: MasonryGridComponent, descendants: true }, { propertyName: "emptyStateContainer", first: true, predicate: ["emptyStateContainer"], descendants: true }], ngImport: i0, template: "<div\n *ngIf=\"cardConfig\"\n #cardContainer\n class=\"w-full\"\n [class.max-w-none]=\"isFullscreen\"\n (mouseenter)=\"onMouseEnter($event)\"\n (mouseleave)=\"onMouseLeave()\"\n (mousemove)=\"onMouseMove($event)\"\n>\n <div class=\"tilt-container glow-container w-full\" \n [ngClass]=\"{ 'max-w-none': isFullscreen }\"\n #tiltContainer\n [ngStyle]=\"tiltStyle\">\n <article\n class=\"ai-card-surface\"\n [ngClass]=\"{ 'ai-card-surface--fullscreen': isFullscreen, 'ai-card-surface--empty-state': !processedSections.length }\"\n >\n <!-- Title and button at the top of the card -->\n <div *ngIf=\"processedSections.length\" class=\"flex items-center justify-between mb-4\">\n <h1 class=\"text-2xl font-bold text-foreground\">\n {{ cardConfig.cardTitle }}\n </h1>\n <button\n type=\"button\"\n class=\"ai-card-fullscreen-btn\"\n [attr.aria-label]=\"isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'\"\n (click)=\"toggleFullscreen()\"\n >\n <lucide-icon [name]=\"isFullscreen ? 'minimize-2' : 'maximize-2'\" [size]=\"16\" aria-hidden=\"true\"></lucide-icon>\n </button>\n </div>\n\n <p *ngIf=\"processedSections.length && cardConfig.cardSubtitle\" class=\"ai-card-subtitle\">\n {{ cardConfig.cardSubtitle }}\n </p>\n\n <ng-container *ngIf=\"processedSections.length; else emptyState\">\n <app-masonry-grid\n [sections]=\"processedSections\"\n [gap]=\"12\"\n [minColumnWidth]=\"280\"\n class=\"w-full\"\n (sectionEvent)=\"onSectionEvent($event)\"\n (layoutChange)=\"onLayoutChange($event)\"\n ></app-masonry-grid>\n </ng-container>\n\n <ng-template #emptyState>\n <div \n class=\"card-empty-state\"\n #emptyStateContainer\n (mousemove)=\"onEmptyStateMouseMove($event)\"\n (mouseleave)=\"onEmptyStateMouseLeave()\">\n <div class=\"empty-state-background\">\n <div class=\"empty-state-gradient\" [style.transform]=\"gradientTransform\"></div>\n <div class=\"empty-state-particles\">\n <div \n *ngFor=\"let particle of particles; let i = index\"\n class=\"particle\"\n [class]=\"'particle-' + (i + 1)\"\n [style.transform]=\"particle.transform\"\n [style.opacity]=\"particle.opacity\">\n </div>\n </div>\n </div>\n <div class=\"empty-state-content\" [style.transform]=\"contentTransform\">\n <div class=\"empty-state-text\">\n <h3 class=\"empty-state-title\">Creating OSI Card</h3>\n <div class=\"empty-state-message-container\">\n <p class=\"empty-state-message\" [@messageAnimation]=\"currentMessageIndex\">\n {{ currentMessage }}\n </p>\n </div>\n </div>\n </div>\n </div>\n </ng-template>\n\n <!-- Action Buttons -->\n <div *ngIf=\"processedSections.length\" class=\"mt-auto\" style=\"margin-top: var(--section-card-gap, 12px); padding-bottom: 16px;\">\n <div class=\"flex flex-wrap items-center gap-3\" style=\"margin-left: 4px; margin-right: 4px;\">\n <button\n *ngFor=\"let action of cardConfig.actions; trackBy: trackAction\"\n type=\"button\"\n class=\"px-5 py-2.5 text-sm transition-all duration-200 flex items-center gap-2 cursor-pointer\"\n [ngClass]=\"getActionButtonClasses(action)\"\n [style.border-radius]=\"'var(--section-card-border-radius, 10px)'\"\n (click)=\"onActionClick(action)\"\n (keydown.enter)=\"onActionClick(action)\"\n (keydown.space)=\"$event.preventDefault(); onActionClick(action)\"\n >\n <!-- Lucide icon (for type defaults or explicit lucide icon names) -->\n <lucide-icon \n *ngIf=\"getActionIconNameForDisplay(action) as iconName\"\n [name]=\"iconName\"\n [size]=\"16\"\n aria-hidden=\"true\">\n </lucide-icon>\n <!-- Image icon (for URL-based icons) -->\n <img \n *ngIf=\"hasImageIcon(action)\"\n [src]=\"action.icon\"\n [alt]=\"action.label + ' icon'\"\n style=\"width: 16px; height: 16px;\"\n aria-hidden=\"true\"\n />\n <!-- Text icon (for emoji or text-based icons) -->\n <span *ngIf=\"hasTextIcon(action)\" aria-hidden=\"true\">{{ action.icon }}</span>\n <span>{{ action.label }}</span>\n </button>\n </div>\n </div>\n\n <!-- Signature at bottom of card -->\n <div *ngIf=\"processedSections.length\" class=\"text-xs text-muted-foreground/60 text-center\">\n Powered by Orange Sales Intelligence\n </div>\n </article>\n </div>\n</div>\n", styles: [":host{display:block;width:100%;height:100%}:host ::ng-deep .ai-card-surface{border:.5px solid color-mix(in srgb,var(--color-brand) 49%,transparent)!important;border-width:.5px!important;border-style:solid!important;border-color:color-mix(in srgb,var(--color-brand) 49%,transparent)!important}:host ::ng-deep .ai-card-surface:hover{border:.5px solid var(--color-brand)!important;border-width:.5px!important;border-style:solid!important;border-color:var(--color-brand)!important}:host ::ng-deep .section-highlight{animation:section-pulse 2s ease-out;position:relative}:host ::ng-deep .section-highlight:after{content:\"\";position:absolute;inset:-4px;border:2px solid rgba(255,121,0,.6);border-radius:14px;pointer-events:none;animation:section-border-fade 2s ease-out forwards}@keyframes section-pulse{0%,to{transform:scale(1)}50%{transform:scale(1.01)}}@keyframes section-border-fade{0%{opacity:1;box-shadow:0 0 20px #ff790066}to{opacity:0;box-shadow:0 0 #ff790000}}.ai-card-breakpoint-pill{display:inline-flex;align-items:center;margin-top:1rem;padding:.5rem 1.25rem;border-radius:999px;border:2px solid rgba(255,121,0,.6);background:#ff790026;color:#ff7900;font-size:.75rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;box-shadow:0 4px 20px #ff79004d;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.ai-card-surface.streaming-active :where(.info-row,.section-card,.list-card,.event-timeline__item,.product-card,.solutions-card,.contact-card,.network-card__item,.map-point,.text-reference-entry,.quote-card,.overview-card__item){opacity:0;transform:translateY(12px);animation:field-enter .45s ease forwards;will-change:opacity,transform}.ai-card-surface.streaming-active :where(.info-row,.section-card,.list-card,.event-timeline__item,.product-card,.solutions-card,.contact-card,.network-card__item,.map-point,.text-reference-entry,.quote-card,.overview-card__item):nth-child(1){animation-delay:0s}.ai-card-surface.streaming-active :where(.info-row,.section-card,.list-card,.event-timeline__item,.product-card,.solutions-card,.contact-card,.network-card__item,.map-point,.text-reference-entry,.quote-card,.overview-card__item):nth-child(2){animation-delay:.05s}.ai-card-surface.streaming-active :where(.info-row,.section-card,.list-card,.event-timeline__item,.product-card,.solutions-card,.contact-card,.network-card__item,.map-point,.text-reference-entry,.quote-card,.overview-card__item):nth-child(3){animation-delay:.1s}.ai-card-surface.streaming-active :where(.info-row,.section-card,.list-card,.event-timeline__item,.product-card,.solutions-card,.contact-card,.network-card__item,.map-point,.text-reference-entry,.quote-card,.overview-card__item):nth-child(4){animation-delay:.15s}.ai-card-surface.streaming-active :where(.info-row,.section-card,.list-card,.event-timeline__item,.product-card,.solutions-card,.contact-card,.network-card__item,.map-point,.text-reference-entry,.quote-card,.overview-card__item):nth-child(5){animation-delay:.2s}.ai-card-surface.streaming-active :where(.info-row,.section-card,.list-card,.event-timeline__item,.product-card,.solutions-card,.contact-card,.network-card__item,.map-point,.text-reference-entry,.quote-card,.overview-card__item):nth-child(n+6){animation-delay:.24s}@keyframes field-enter{0%{opacity:0;transform:translateY(12px) scale(.98)}60%{opacity:1;transform:translateY(-2px) scale(1.01)}to{opacity:1;transform:translateY(0) scale(1)}}.loading-particles{position:absolute;inset:0;pointer-events:none}.particle{position:absolute;width:8px;height:8px;background:var(--color-brand);border-radius:50%;opacity:.6;animation:particle-float 3s ease-in-out infinite;box-shadow:0 0 12px var(--color-brand)}.particle:nth-child(1){top:10%;left:20%}.particle:nth-child(2){top:20%;left:80%;animation-delay:.3s}.particle:nth-child(3){top:60%;left:15%;animation-delay:.6s}.particle:nth-child(4){top:80%;left:70%;animation-delay:.9s}.particle:nth-child(5){top:30%;left:50%;animation-delay:1.2s}.particle:nth-child(6){top:70%;left:40%;animation-delay:1.5s}.particle:nth-child(7){top:50%;left:90%;animation-delay:1.8s}.particle:nth-child(8){top:15%;left:60%;animation-delay:2.1s}@keyframes particle-float{0%,to{transform:translateY(0) translate(0) scale(1);opacity:.3}25%{transform:translateY(-30px) translate(20px) scale(1.2);opacity:.7}50%{transform:translateY(-60px) translate(-10px) scale(.8);opacity:1}75%{transform:translateY(-30px) translate(15px) scale(1.1);opacity:.6}}.loading-content{position:relative;z-index:2;display:flex;flex-direction:column;align-items:center;gap:1.5rem}.loading-spinner{width:64px;height:64px;position:relative}.spinner-svg{width:100%;height:100%;animation:spinner-rotate 1.5s linear infinite;transform-origin:center}.spinner-circle{stroke-dasharray:125.6;stroke-dashoffset:31.4;stroke:var(--color-brand);animation:spinner-dash 1.5s ease-in-out infinite}@keyframes spinner-rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes spinner-dash{0%{stroke-dasharray:1,125.6;stroke-dashoffset:0}50%{stroke-dasharray:94.2,125.6;stroke-dashoffset:-31.4}to{stroke-dasharray:94.2,125.6;stroke-dashoffset:-125.6}}.loading-text{text-align:center}.loading-title{font-size:1.25rem;font-weight:600;color:var(--foreground);margin-bottom:.5rem;background:linear-gradient(90deg,var(--foreground) 0%,var(--color-brand) 50%,var(--foreground) 100%);background-size:200% 100%;background-clip:text;-webkit-background-clip:text;-webkit-text-fill-color:transparent;animation:text-shimmer 2s ease-in-out infinite}.loading-subtitle{font-size:.875rem;color:var(--muted-foreground);margin:0}@keyframes text-shimmer{0%,to{background-position:-200% 0}50%{background-position:200% 0}}.loading-wave{position:absolute;bottom:0;left:0;right:0;height:4px;background:linear-gradient(90deg,transparent 0%,var(--color-brand) 25%,var(--color-brand) 75%,transparent 100%);background-size:200% 100%;animation:wave-slide 2s ease-in-out infinite}@keyframes wave-slide{0%{background-position:-200% 0}to{background-position:200% 0}}.ai-card-surface.has-loading-overlay{display:flex;align-items:center;justify-content:center;min-height:500px;position:relative;overflow:hidden;background:radial-gradient(circle at 50% 50%,rgba(255,121,0,.05) 0%,transparent 70%);animation:generating-bg-pulse 3s ease-in-out infinite,card-generating-pulse 2s ease-in-out infinite}@keyframes card-generating-pulse{0%,to{box-shadow:inset 0 1px 3px #ff790014,0 4px 20px #00000026,0 0 #ff790000}50%{box-shadow:inset 0 1px 3px #ff790026,0 4px 20px #00000026,0 0 30px #ff790033}}@keyframes generating-bg-pulse{0%,to{background:radial-gradient(circle at 50% 50%,rgba(255,121,0,.05) 0%,transparent 70%)}50%{background:radial-gradient(circle at 50% 50%,rgba(255,121,0,.12) 0%,transparent 70%)}}.generating-content{position:relative;z-index:2;text-align:center}.generating-shimmer{position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent 0%,rgba(255,121,0,.15) 50%,transparent 100%);animation:shimmer-sweep 2.5s ease-in-out infinite;pointer-events:none;z-index:1}@keyframes shimmer-sweep{0%{left:-100%}to{left:100%}}.generating-text{font-size:1.125rem;font-weight:600;color:var(--foreground);margin:0;background:linear-gradient(90deg,var(--foreground) 0%,var(--color-brand) 50%,var(--foreground) 100%);background-size:200% 100%;background-clip:text;-webkit-background-clip:text;-webkit-text-fill-color:transparent;animation:text-shimmer-flow 2.5s ease-in-out infinite;position:relative;z-index:2;letter-spacing:.02em}@keyframes text-shimmer-flow{0%,to{background-position:-200% 0}50%{background-position:200% 0}}.card-empty-state{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100%;height:100%;padding:4rem 2rem;border-radius:1.5rem;overflow:hidden;border:1px solid color-mix(in srgb,var(--color-brand) 20%,transparent);background:color-mix(in srgb,var(--background) 98%,transparent);transition:all .3s ease;flex:1}:host ::ng-deep .ai-card-surface--empty-state{display:flex!important;flex-direction:column!important;min-height:100%!important;height:100%!important}:host ::ng-deep .ai-card-surface--empty-state .card-empty-state{flex:1 1 auto!important;min-height:0!important;height:100%!important;display:flex!important}.card-empty-state:hover{border-color:color-mix(in srgb,var(--color-brand) 30%,transparent);background:color-mix(in srgb,var(--background) 99%,transparent)}.empty-state-background{position:absolute;inset:0;overflow:hidden;pointer-events:none}.empty-state-gradient{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:150%;height:150%;background:radial-gradient(circle,color-mix(in srgb,var(--color-brand) 6%,transparent) 0%,color-mix(in srgb,var(--color-brand) 3%,transparent) 50%,transparent 80%);animation:gradient-pulse 5s ease-in-out infinite;opacity:.8}@keyframes gradient-pulse{0%,to{opacity:.6;transform:translate(-50%,-50%) scale(1)}50%{opacity:.9;transform:translate(-50%,-50%) scale(1.05)}}.empty-state-particles{position:absolute;inset:0}.particle{position:absolute;width:2.5px;height:2.5px;border-radius:50%;background:color-mix(in srgb,var(--color-brand) 55%,transparent);box-shadow:0 0 4px color-mix(in srgb,var(--color-brand) 45%,transparent);transition:transform .8s cubic-bezier(.23,1,.32,1),opacity .5s ease;will-change:transform,opacity;top:50%;left:50%;margin-left:-1.25px;margin-top:-1.25px;pointer-events:none;filter:blur(.5px)}.particle:nth-child(odd){background:color-mix(in srgb,var(--color-brand) 65%,transparent);box-shadow:0 0 6px color-mix(in srgb,var(--color-brand) 55%,transparent)}.particle:nth-child(2n){background:color-mix(in srgb,var(--color-brand) 45%,transparent);box-shadow:0 0 3px color-mix(in srgb,var(--color-brand) 35%,transparent)}.empty-state-content{position:relative;z-index:1;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:1rem;max-width:420px;width:100%;transition:transform .2s cubic-bezier(.25,.46,.45,.94);will-change:transform}.empty-state-icon-wrapper{position:relative;display:flex;align-items:center;justify-content:center;width:88px;height:88px}.empty-state-icon-ring{position:absolute;inset:-8px;border:1.5px solid color-mix(in srgb,var(--color-brand) 25%,transparent);border-radius:50%;animation:ring-pulse 3s ease-in-out infinite}.empty-state-icon-pulse{position:absolute;inset:-16px;border:1px solid color-mix(in srgb,var(--color-brand) 15%,transparent);border-radius:50%;animation:ring-pulse 3s ease-in-out infinite .75s}@keyframes ring-pulse{0%,to{opacity:.4;transform:scale(1)}50%{opacity:.7;transform:scale(1.08)}}.empty-state-icon{position:relative;z-index:1;color:var(--color-brand);animation:icon-float 4s ease-in-out infinite;filter:drop-shadow(0 2px 8px color-mix(in srgb,var(--color-brand) 25%,transparent));opacity:.95}@keyframes icon-float{0%,to{transform:translateY(0) rotate(0)}25%{transform:translateY(-6px) rotate(-3deg)}50%{transform:translateY(-10px) rotate(0)}75%{transform:translateY(-6px) rotate(3deg)}}.empty-state-text{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.75rem;width:100%}.empty-state-title{font-size:1.75rem;font-weight:700;color:var(--foreground);margin:0;letter-spacing:-.03em;line-height:1.2;text-align:center;animation:fade-in-up .6s ease-out .2s both}.empty-state-message-container{min-height:2.5rem;display:flex;align-items:center;justify-content:center;width:100%}.empty-state-message{font-size:1rem;color:color-mix(in srgb,var(--color-brand) 75%,transparent);margin:0;line-height:1.6;font-weight:500;font-style:italic;text-align:center;animation:fade-in-up .6s ease-out .4s both;letter-spacing:.01em}@keyframes fade-in-up{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.card-empty-state-legacy{min-height:200px;display:flex;align-items:center;justify-content:center;padding:3rem}.empty-state-icon{position:relative;width:80px;height:80px}.empty-state-dot{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:12px;height:12px;background:var(--color-brand);border-radius:50%;opacity:.6;animation:dot-pulse 2s ease-in-out infinite}.empty-state-ring{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:80px;height:80px;border:2px solid var(--color-brand);border-radius:50%;opacity:.2;animation:ring-expand 2s ease-in-out infinite}@keyframes dot-pulse{0%,to{transform:translate(-50%,-50%) scale(1);opacity:.6}50%{transform:translate(-50%,-50%) scale(1.5);opacity:.3}}@keyframes ring-expand{0%{transform:translate(-50%,-50%) scale(.8);opacity:.2}50%{transform:translate(-50%,-50%) scale(1);opacity:.1}to{transform:translate(-50%,-50%) scale(1.2);opacity:0}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: LucideIconsModule }, { kind: "component", type: i2.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }, { kind: "component", type: MasonryGridComponent, selector: "app-masonry-grid", inputs: ["sections", "gap", "minColumnWidth", "maxColumns"], outputs: ["sectionEvent", "layoutChange"] }], animations: [
3711
5658
  trigger('messageAnimation', [
@@ -3854,6 +5801,346 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
3854
5801
  type: Output
3855
5802
  }] } });
3856
5803
 
5804
+ /**
5805
+ * Card Header Component
5806
+ *
5807
+ * Composable component for rendering card header with title, subtitle, and optional actions.
5808
+ * Can be used independently or as part of the full card renderer.
5809
+ *
5810
+ * @example
5811
+ * ```html
5812
+ * <app-card-header
5813
+ * [title]="card.cardTitle"
5814
+ * [subtitle]="card.cardSubtitle"
5815
+ * [showFullscreenButton]="true"
5816
+ * [isFullscreen]="false"
5817
+ * (fullscreenToggle)="onFullscreenToggle($event)">
5818
+ * </app-card-header>
5819
+ * ```
5820
+ */
5821
+ class CardHeaderComponent {
5822
+ constructor() {
5823
+ /** Whether to show fullscreen toggle button */
5824
+ this.showFullscreenButton = false;
5825
+ /** Current fullscreen state */
5826
+ this.isFullscreen = false;
5827
+ /** Emitted when fullscreen button is clicked */
5828
+ this.fullscreenToggle = new EventEmitter();
5829
+ }
5830
+ onFullscreenClick() {
5831
+ this.fullscreenToggle.emit(!this.isFullscreen);
5832
+ }
5833
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CardHeaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5834
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: CardHeaderComponent, isStandalone: true, selector: "app-card-header", inputs: { title: "title", subtitle: "subtitle", showFullscreenButton: "showFullscreenButton", isFullscreen: "isFullscreen" }, outputs: { fullscreenToggle: "fullscreenToggle" }, ngImport: i0, template: `
5835
+ <div class="card-header" *ngIf="title">
5836
+ <div class="flex items-center justify-between mb-4">
5837
+ <h1 class="text-2xl font-bold text-foreground">
5838
+ {{ title }}
5839
+ </h1>
5840
+ <button
5841
+ *ngIf="showFullscreenButton"
5842
+ type="button"
5843
+ class="ai-card-fullscreen-btn"
5844
+ [attr.aria-label]="isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'"
5845
+ (click)="onFullscreenClick()">
5846
+ <lucide-icon
5847
+ [name]="isFullscreen ? 'minimize-2' : 'maximize-2'"
5848
+ [size]="16"
5849
+ aria-hidden="true">
5850
+ </lucide-icon>
5851
+ </button>
5852
+ </div>
5853
+
5854
+ <p *ngIf="subtitle" class="ai-card-subtitle">
5855
+ {{ subtitle }}
5856
+ </p>
5857
+ </div>
5858
+ `, isInline: true, styles: [".card-header{width:100%}.ai-card-fullscreen-btn{padding:8px;border-radius:6px;background:transparent;border:1px solid transparent;cursor:pointer;color:var(--card-text-secondary);transition:all .2s ease;display:flex;align-items:center;justify-content:center}.ai-card-fullscreen-btn:hover{background:var(--hover-bg, rgba(255, 121, 0, .1));border-color:var(--color-brand);color:var(--color-brand)}.ai-card-subtitle{font-size:.875rem;color:var(--card-text-secondary, var(--muted-foreground));margin:0 0 1rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: LucideIconsModule }, { kind: "component", type: i2.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
5859
+ }
5860
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CardHeaderComponent, decorators: [{
5861
+ type: Component,
5862
+ args: [{ selector: 'app-card-header', standalone: true, imports: [CommonModule, LucideIconsModule], template: `
5863
+ <div class="card-header" *ngIf="title">
5864
+ <div class="flex items-center justify-between mb-4">
5865
+ <h1 class="text-2xl font-bold text-foreground">
5866
+ {{ title }}
5867
+ </h1>
5868
+ <button
5869
+ *ngIf="showFullscreenButton"
5870
+ type="button"
5871
+ class="ai-card-fullscreen-btn"
5872
+ [attr.aria-label]="isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'"
5873
+ (click)="onFullscreenClick()">
5874
+ <lucide-icon
5875
+ [name]="isFullscreen ? 'minimize-2' : 'maximize-2'"
5876
+ [size]="16"
5877
+ aria-hidden="true">
5878
+ </lucide-icon>
5879
+ </button>
5880
+ </div>
5881
+
5882
+ <p *ngIf="subtitle" class="ai-card-subtitle">
5883
+ {{ subtitle }}
5884
+ </p>
5885
+ </div>
5886
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".card-header{width:100%}.ai-card-fullscreen-btn{padding:8px;border-radius:6px;background:transparent;border:1px solid transparent;cursor:pointer;color:var(--card-text-secondary);transition:all .2s ease;display:flex;align-items:center;justify-content:center}.ai-card-fullscreen-btn:hover{background:var(--hover-bg, rgba(255, 121, 0, .1));border-color:var(--color-brand);color:var(--color-brand)}.ai-card-subtitle{font-size:.875rem;color:var(--card-text-secondary, var(--muted-foreground));margin:0 0 1rem}\n"] }]
5887
+ }], propDecorators: { title: [{
5888
+ type: Input
5889
+ }], subtitle: [{
5890
+ type: Input
5891
+ }], showFullscreenButton: [{
5892
+ type: Input
5893
+ }], isFullscreen: [{
5894
+ type: Input
5895
+ }], fullscreenToggle: [{
5896
+ type: Output
5897
+ }] } });
5898
+
5899
+ /**
5900
+ * Card Body Component
5901
+ *
5902
+ * Composable component for rendering card body with sections in a masonry grid layout.
5903
+ * Wraps MasonryGridComponent for easier composition.
5904
+ *
5905
+ * @example
5906
+ * ```html
5907
+ * <app-card-body
5908
+ * [sections]="card.sections"
5909
+ * [gap]="12"
5910
+ * [minColumnWidth]="280"
5911
+ * (sectionEvent)="onSectionEvent($event)"
5912
+ * (layoutChange)="onLayoutChange($event)">
5913
+ * </app-card-body>
5914
+ * ```
5915
+ */
5916
+ class CardBodyComponent {
5917
+ constructor() {
5918
+ /** Sections to render */
5919
+ this.sections = [];
5920
+ /** Gap between grid items in pixels */
5921
+ this.gap = 12;
5922
+ /** Minimum column width in pixels */
5923
+ this.minColumnWidth = 280;
5924
+ /** Emitted when a section event occurs */
5925
+ this.sectionEvent = new EventEmitter();
5926
+ /** Emitted when layout changes */
5927
+ this.layoutChange = new EventEmitter();
5928
+ }
5929
+ onSectionEvent(event) {
5930
+ this.sectionEvent.emit(event);
5931
+ }
5932
+ onLayoutChange(info) {
5933
+ this.layoutChange.emit(info);
5934
+ }
5935
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CardBodyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5936
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: CardBodyComponent, isStandalone: true, selector: "app-card-body", inputs: { sections: "sections", gap: "gap", minColumnWidth: "minColumnWidth" }, outputs: { sectionEvent: "sectionEvent", layoutChange: "layoutChange" }, ngImport: i0, template: `
5937
+ <app-masonry-grid
5938
+ *ngIf="sections && sections.length > 0"
5939
+ [sections]="sections"
5940
+ [gap]="gap"
5941
+ [minColumnWidth]="minColumnWidth"
5942
+ class="w-full"
5943
+ (sectionEvent)="onSectionEvent($event)"
5944
+ (layoutChange)="onLayoutChange($event)">
5945
+ </app-masonry-grid>
5946
+
5947
+ <ng-content></ng-content>
5948
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: MasonryGridComponent, selector: "app-masonry-grid", inputs: ["sections", "gap", "minColumnWidth", "maxColumns"], outputs: ["sectionEvent", "layoutChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
5949
+ }
5950
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CardBodyComponent, decorators: [{
5951
+ type: Component,
5952
+ args: [{
5953
+ selector: 'app-card-body',
5954
+ standalone: true,
5955
+ imports: [CommonModule, MasonryGridComponent],
5956
+ template: `
5957
+ <app-masonry-grid
5958
+ *ngIf="sections && sections.length > 0"
5959
+ [sections]="sections"
5960
+ [gap]="gap"
5961
+ [minColumnWidth]="minColumnWidth"
5962
+ class="w-full"
5963
+ (sectionEvent)="onSectionEvent($event)"
5964
+ (layoutChange)="onLayoutChange($event)">
5965
+ </app-masonry-grid>
5966
+
5967
+ <ng-content></ng-content>
5968
+ `,
5969
+ changeDetection: ChangeDetectionStrategy.OnPush
5970
+ }]
5971
+ }], propDecorators: { sections: [{
5972
+ type: Input
5973
+ }], gap: [{
5974
+ type: Input
5975
+ }], minColumnWidth: [{
5976
+ type: Input
5977
+ }], sectionEvent: [{
5978
+ type: Output
5979
+ }], layoutChange: [{
5980
+ type: Output
5981
+ }] } });
5982
+
5983
+ /**
5984
+ * Card Footer Component
5985
+ *
5986
+ * Composable component for rendering card footer with actions and optional signature.
5987
+ * Can be used independently or as part of the full card renderer.
5988
+ *
5989
+ * @example
5990
+ * ```html
5991
+ * <app-card-footer
5992
+ * [actions]="card.actions"
5993
+ * [showSignature]="true"
5994
+ * (actionClick)="onActionClick($event)">
5995
+ * </app-card-footer>
5996
+ * ```
5997
+ */
5998
+ class CardFooterComponent {
5999
+ constructor() {
6000
+ /** Actions to display */
6001
+ this.actions = [];
6002
+ /** Whether to show the signature */
6003
+ this.showSignature = true;
6004
+ /** Signature text to display */
6005
+ this.signatureText = 'Powered by Orange Sales Intelligence';
6006
+ /** Emitted when an action is clicked */
6007
+ this.actionClick = new EventEmitter();
6008
+ this.iconService = inject(IconService);
6009
+ }
6010
+ get hasActions() {
6011
+ return this.actions && this.actions.length > 0;
6012
+ }
6013
+ trackAction(index, action) {
6014
+ return action.id || `${action.type}-${index}`;
6015
+ }
6016
+ getActionButtonClasses(action) {
6017
+ return {
6018
+ 'action-button--primary': action.variant === 'primary',
6019
+ 'action-button--secondary': action.variant === 'secondary' || !action.variant
6020
+ };
6021
+ }
6022
+ getActionIconNameForDisplay(action) {
6023
+ if (!action.icon) {
6024
+ return null;
6025
+ }
6026
+ // If it's a lucide icon name, return it
6027
+ if (typeof action.icon === 'string' && !action.icon.startsWith('http') && !action.icon.match(/[\u{1F300}-\u{1F9FF}]/u)) {
6028
+ return action.icon;
6029
+ }
6030
+ // If it's a URL or emoji, return null (handled by other icons)
6031
+ return null;
6032
+ }
6033
+ hasImageIcon(action) {
6034
+ return !!action.icon && typeof action.icon === 'string' && action.icon.startsWith('http');
6035
+ }
6036
+ hasTextIcon(action) {
6037
+ if (!action.icon || typeof action.icon !== 'string' || action.icon.startsWith('http')) {
6038
+ return false;
6039
+ }
6040
+ const emojiMatch = action.icon.match(/[\u{1F300}-\u{1F9FF}]/u);
6041
+ return !!emojiMatch || action.icon.length <= 2;
6042
+ }
6043
+ onActionClick(action) {
6044
+ this.actionClick.emit(action);
6045
+ }
6046
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CardFooterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
6047
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: CardFooterComponent, isStandalone: true, selector: "app-card-footer", inputs: { actions: "actions", showSignature: "showSignature", signatureText: "signatureText" }, outputs: { actionClick: "actionClick" }, ngImport: i0, template: `
6048
+ <div class="card-footer" *ngIf="hasActions || showSignature">
6049
+ <!-- Action Buttons -->
6050
+ <div *ngIf="hasActions" class="card-actions">
6051
+ <div class="flex flex-wrap items-center gap-3" style="margin-left: 4px; margin-right: 4px;">
6052
+ <button
6053
+ *ngFor="let action of actions; trackBy: trackAction"
6054
+ type="button"
6055
+ class="action-button"
6056
+ [ngClass]="getActionButtonClasses(action)"
6057
+ [style.border-radius]="'var(--section-card-border-radius, 10px)'"
6058
+ (click)="onActionClick(action)"
6059
+ (keydown.enter)="onActionClick(action)"
6060
+ (keydown.space)="$event.preventDefault(); onActionClick(action)">
6061
+ <!-- Lucide icon -->
6062
+ <lucide-icon
6063
+ *ngIf="getActionIconNameForDisplay(action) as iconName"
6064
+ [name]="iconName"
6065
+ [size]="16"
6066
+ aria-hidden="true">
6067
+ </lucide-icon>
6068
+ <!-- Image icon -->
6069
+ <img
6070
+ *ngIf="hasImageIcon(action)"
6071
+ [src]="action.icon"
6072
+ [alt]="action.label + ' icon'"
6073
+ style="width: 16px; height: 16px;"
6074
+ aria-hidden="true"
6075
+ />
6076
+ <!-- Text icon -->
6077
+ <span *ngIf="hasTextIcon(action)" aria-hidden="true">{{ action.icon }}</span>
6078
+ <span>{{ action.label }}</span>
6079
+ </button>
6080
+ </div>
6081
+ </div>
6082
+
6083
+ <!-- Signature -->
6084
+ <div *ngIf="showSignature" class="card-signature">
6085
+ {{ signatureText }}
6086
+ </div>
6087
+ </div>
6088
+ `, isInline: true, styles: [".card-footer{margin-top:auto;padding-top:var(--section-card-gap, 12px);padding-bottom:16px}.card-actions{margin-bottom:8px}.action-button{padding:10px 20px;font-size:.875rem;transition:all .2s;display:flex;align-items:center;gap:8px;cursor:pointer;border:1px solid transparent}.action-button--primary{background:var(--primary, #ff7900);color:var(--primary-foreground, #ffffff)}.action-button--primary:hover{background:color-mix(in srgb,var(--primary) 85%,transparent);border-color:var(--primary)}.action-button--secondary{background:var(--secondary, #f5f5f5);color:var(--secondary-foreground, #1a1a1a)}.action-button--secondary:hover{background:color-mix(in srgb,var(--secondary) 90%,transparent);border-color:var(--secondary)}.card-signature{font-size:.75rem;color:var(--muted-foreground, rgba(128, 128, 128, .6));text-align:center;margin-top:8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: LucideIconsModule }, { kind: "component", type: i2.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
6089
+ }
6090
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CardFooterComponent, decorators: [{
6091
+ type: Component,
6092
+ args: [{ selector: 'app-card-footer', standalone: true, imports: [CommonModule, LucideIconsModule], template: `
6093
+ <div class="card-footer" *ngIf="hasActions || showSignature">
6094
+ <!-- Action Buttons -->
6095
+ <div *ngIf="hasActions" class="card-actions">
6096
+ <div class="flex flex-wrap items-center gap-3" style="margin-left: 4px; margin-right: 4px;">
6097
+ <button
6098
+ *ngFor="let action of actions; trackBy: trackAction"
6099
+ type="button"
6100
+ class="action-button"
6101
+ [ngClass]="getActionButtonClasses(action)"
6102
+ [style.border-radius]="'var(--section-card-border-radius, 10px)'"
6103
+ (click)="onActionClick(action)"
6104
+ (keydown.enter)="onActionClick(action)"
6105
+ (keydown.space)="$event.preventDefault(); onActionClick(action)">
6106
+ <!-- Lucide icon -->
6107
+ <lucide-icon
6108
+ *ngIf="getActionIconNameForDisplay(action) as iconName"
6109
+ [name]="iconName"
6110
+ [size]="16"
6111
+ aria-hidden="true">
6112
+ </lucide-icon>
6113
+ <!-- Image icon -->
6114
+ <img
6115
+ *ngIf="hasImageIcon(action)"
6116
+ [src]="action.icon"
6117
+ [alt]="action.label + ' icon'"
6118
+ style="width: 16px; height: 16px;"
6119
+ aria-hidden="true"
6120
+ />
6121
+ <!-- Text icon -->
6122
+ <span *ngIf="hasTextIcon(action)" aria-hidden="true">{{ action.icon }}</span>
6123
+ <span>{{ action.label }}</span>
6124
+ </button>
6125
+ </div>
6126
+ </div>
6127
+
6128
+ <!-- Signature -->
6129
+ <div *ngIf="showSignature" class="card-signature">
6130
+ {{ signatureText }}
6131
+ </div>
6132
+ </div>
6133
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".card-footer{margin-top:auto;padding-top:var(--section-card-gap, 12px);padding-bottom:16px}.card-actions{margin-bottom:8px}.action-button{padding:10px 20px;font-size:.875rem;transition:all .2s;display:flex;align-items:center;gap:8px;cursor:pointer;border:1px solid transparent}.action-button--primary{background:var(--primary, #ff7900);color:var(--primary-foreground, #ffffff)}.action-button--primary:hover{background:color-mix(in srgb,var(--primary) 85%,transparent);border-color:var(--primary)}.action-button--secondary{background:var(--secondary, #f5f5f5);color:var(--secondary-foreground, #1a1a1a)}.action-button--secondary:hover{background:color-mix(in srgb,var(--secondary) 90%,transparent);border-color:var(--secondary)}.card-signature{font-size:.75rem;color:var(--muted-foreground, rgba(128, 128, 128, .6));text-align:center;margin-top:8px}\n"] }]
6134
+ }], propDecorators: { actions: [{
6135
+ type: Input
6136
+ }], showSignature: [{
6137
+ type: Input
6138
+ }], signatureText: [{
6139
+ type: Input
6140
+ }], actionClick: [{
6141
+ type: Output
6142
+ }] } });
6143
+
3857
6144
  class NewsSectionComponent extends BaseSectionComponent {
3858
6145
  get newsItems() {
3859
6146
  return this.getItems();
@@ -3956,5 +6243,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
3956
6243
  * Generated bundle index. Do not edit.
3957
6244
  */
3958
6245
 
3959
- export { AICardRendererComponent, AnalyticsSectionComponent, BaseSectionComponent, BrandColorsSectionComponent, CardDiffUtil, CardPreviewComponent, CardSkeletonComponent, CardTypeGuards, CardUtils, ChartSectionComponent, ContactCardSectionComponent, EventSectionComponent, FallbackSectionComponent, FinancialsSectionComponent, IconService, InfoSectionComponent, ListSectionComponent, LucideIconsModule, MagneticTiltService, MapSectionComponent, MasonryGridComponent, NetworkCardSectionComponent, NewsSectionComponent, OverviewSectionComponent, ProductSectionComponent, QuotationSectionComponent, SectionNormalizationService, SectionRendererComponent, SectionUtilsService, SocialMediaSectionComponent, SolutionsSectionComponent, TextReferenceSectionComponent, getBreakpointFromWidth };
6246
+ export { AICardRendererComponent, AnalyticsSectionComponent, BaseSectionComponent, BrandColorsSectionComponent, CardBodyComponent, CardDiffUtil, CardFooterComponent, CardHeaderComponent, CardPreviewComponent, CardSkeletonComponent, CardTypeGuards, CardUtils, ChartSectionComponent, ContactCardSectionComponent, EventMiddlewareService, EventSectionComponent, FallbackSectionComponent, FinancialsSectionComponent, IconService, InfoSectionComponent, ListSectionComponent, LucideIconsModule, MagneticTiltService, MapSectionComponent, MasonryGridComponent, NetworkCardSectionComponent, NewsSectionComponent, OverviewSectionComponent, PresetFactory, ProductSectionComponent, QuotationSectionComponent, SectionNormalizationService, SectionPluginRegistry, SectionRendererComponent, SectionUtilsService, SocialMediaSectionComponent, SolutionsSectionComponent, TextReferenceSectionComponent, ThemeService, areStylesLoaded, buildThemeFromBase, cloneCardConfig, createAnalyticsDashboard, createAnalyticsPreset, createCardFromPartial, createCompanyCard, createCompanyPreset, createContactCard, createContactPreset, createEmptyCard, createEnhancedCompanyCard, createErrorCard, createPartialTheme, createSkeletonCard, darkTheme, generateThemeFromPalette, getBreakpointFromWidth, getCSSVariableValue, highContrastTheme, isCSSVariableDefined, isCardComplete, lightTheme, mergeCardConfig, mergeSections, mergeThemes, prepareCardForStreaming, provideOSICards, updateCardIncremental, validateAndWarnStyles, validateCSSVariableNames, validateCardConfig, validateStyles, waitForStyles };
3960
6247
  //# sourceMappingURL=osi-cards-lib.mjs.map