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.
- package/README.md +55 -1
- package/fesm2022/osi-cards-lib.mjs +3022 -735
- package/index.d.ts +1187 -64
- package/package.json +1 -1
- package/styles/layout/_tilt.scss +12 -14
|
@@ -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,
|
|
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
|
|
7
|
-
import {
|
|
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
|
|
509
|
-
const BASE_GLOW_BLUR =
|
|
510
|
-
const MAX_GLOW_BLUR_OFFSET =
|
|
511
|
-
const BASE_GLOW_OPACITY = 0.
|
|
512
|
-
const MAX_GLOW_OPACITY_OFFSET = 0.
|
|
513
|
-
const MAX_REFLECTION_OPACITY = 0.
|
|
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
|
-
|
|
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 =
|
|
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
|
-
//
|
|
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
|
-
|
|
566
|
-
|
|
567
|
-
const
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
const
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
const
|
|
576
|
-
const
|
|
577
|
-
|
|
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
|
-
|
|
580
|
-
|
|
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
|
-
//
|
|
586
|
-
|
|
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
|
-
//
|
|
769
|
+
// Use RAF for smooth 60fps animation
|
|
643
770
|
const startTime = performance.now();
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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
|
-
//
|
|
655
|
-
const
|
|
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
|
-
|
|
658
|
-
|
|
659
|
-
glowBlur:
|
|
660
|
-
glowOpacity:
|
|
661
|
-
reflectionOpacity:
|
|
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);
|
|
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
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
890
|
-
* Preserves references to unchanged sections for optimal performance
|
|
1118
|
+
* Get fields from section (standardized access pattern)
|
|
891
1119
|
*/
|
|
892
|
-
|
|
893
|
-
|
|
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
|
-
*
|
|
1124
|
+
* Get items from section (standardized access pattern)
|
|
1125
|
+
* Falls back to fields if items are not available
|
|
960
1126
|
*/
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
return
|
|
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
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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 (
|
|
979
|
-
|
|
1153
|
+
if (this.itemAnimationUpdateRafId !== null) {
|
|
1154
|
+
cancelAnimationFrame(this.itemAnimationUpdateRafId);
|
|
1155
|
+
this.itemAnimationUpdateRafId = null;
|
|
980
1156
|
}
|
|
981
|
-
//
|
|
982
|
-
|
|
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
|
-
*
|
|
1168
|
+
* Get animation class for a field based on its appearance state
|
|
987
1169
|
*/
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1188
|
+
getItemAnimationClass(itemId, index) {
|
|
1189
|
+
const state = this.itemAnimationStates.get(itemId);
|
|
1190
|
+
if (state === 'entering') {
|
|
1191
|
+
return 'item-streaming';
|
|
1045
1192
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
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
|
-
*
|
|
1075
|
-
* Uses content hashing instead of JSON.stringify for better performance
|
|
1204
|
+
* Get stagger delay index for field animation
|
|
1076
1205
|
*/
|
|
1077
|
-
|
|
1078
|
-
|
|
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
|
-
*
|
|
1210
|
+
* Get stagger delay index for item animation
|
|
1109
1211
|
*/
|
|
1110
|
-
|
|
1111
|
-
return
|
|
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
|
-
*
|
|
1216
|
+
* Mark field as entering and schedule entered state
|
|
1217
|
+
* Optimized: Batches change detection for better performance
|
|
1119
1218
|
*/
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
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
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
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
|
-
*
|
|
1255
|
+
* Mark item as entering and schedule entered state
|
|
1256
|
+
* Optimized: Batches change detection for better performance
|
|
1137
1257
|
*/
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
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
|
-
*
|
|
1278
|
+
* Batch change detection for item animation state updates
|
|
1157
1279
|
*/
|
|
1158
|
-
|
|
1159
|
-
if (
|
|
1160
|
-
return
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
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
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
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
|
-
*
|
|
1301
|
+
* Reset item animation states
|
|
1305
1302
|
*/
|
|
1306
|
-
|
|
1307
|
-
|
|
1303
|
+
resetItemAnimations() {
|
|
1304
|
+
this.itemAnimationStates.clear();
|
|
1305
|
+
this.itemAnimationTimes.clear();
|
|
1308
1306
|
}
|
|
1309
1307
|
/**
|
|
1310
|
-
* Get
|
|
1311
|
-
* Falls back to fields if items are not available
|
|
1308
|
+
* Get field ID for tracking
|
|
1312
1309
|
*/
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
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
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
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
|
-
*
|
|
2682
|
+
* Set theme to a built-in preset
|
|
2683
|
+
*
|
|
2684
|
+
* @param preset - Built-in theme preset name
|
|
1355
2685
|
*/
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
2695
|
+
* Apply a custom theme configuration
|
|
2696
|
+
*
|
|
2697
|
+
* @param config - Custom theme configuration
|
|
1373
2698
|
*/
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
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
|
-
//
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
2726
|
+
* Reset CSS variables (useful when switching themes)
|
|
1391
2727
|
*/
|
|
1392
|
-
|
|
1393
|
-
|
|
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
|
|
2739
|
+
* Get a custom theme configuration
|
|
2740
|
+
*
|
|
2741
|
+
* @param name - Theme name
|
|
2742
|
+
* @returns Theme configuration or null if not found
|
|
1397
2743
|
*/
|
|
1398
|
-
|
|
1399
|
-
return
|
|
2744
|
+
getCustomTheme(name) {
|
|
2745
|
+
return this.customThemes.get(name) ?? null;
|
|
1400
2746
|
}
|
|
1401
2747
|
/**
|
|
1402
|
-
*
|
|
1403
|
-
*
|
|
2748
|
+
* Register a custom theme (without applying it)
|
|
2749
|
+
*
|
|
2750
|
+
* @param config - Custom theme configuration
|
|
1404
2751
|
*/
|
|
1405
|
-
|
|
1406
|
-
this.
|
|
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
|
-
*
|
|
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
|
-
|
|
1428
|
-
|
|
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
|
-
*
|
|
1442
|
-
*
|
|
2765
|
+
* Get all registered custom themes
|
|
2766
|
+
*
|
|
2767
|
+
* @returns Array of custom theme configurations
|
|
1443
2768
|
*/
|
|
1444
|
-
|
|
1445
|
-
this.
|
|
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
|
-
*
|
|
2773
|
+
* Validate a theme configuration
|
|
2774
|
+
*
|
|
2775
|
+
* @param config - Theme configuration to validate
|
|
2776
|
+
* @returns Validation result with errors if any
|
|
1465
2777
|
*/
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
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
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
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
|
-
*
|
|
3331
|
+
* Create a company card
|
|
3332
|
+
*
|
|
3333
|
+
* @param options - Company card options
|
|
3334
|
+
* @returns Company card configuration
|
|
1488
3335
|
*/
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
this.itemAnimationTimes.clear();
|
|
3336
|
+
static createCompany(options) {
|
|
3337
|
+
return createCompanyCard(options);
|
|
1492
3338
|
}
|
|
1493
3339
|
/**
|
|
1494
|
-
*
|
|
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
|
-
|
|
1497
|
-
return
|
|
3345
|
+
static createEnhancedCompany(options) {
|
|
3346
|
+
return createEnhancedCompanyCard(options);
|
|
1498
3347
|
}
|
|
1499
3348
|
/**
|
|
1500
|
-
*
|
|
3349
|
+
* Create a contact card
|
|
3350
|
+
*
|
|
3351
|
+
* @param options - Contact card options
|
|
3352
|
+
* @returns Contact card configuration
|
|
1501
3353
|
*/
|
|
1502
|
-
|
|
1503
|
-
return
|
|
3354
|
+
static createContact(options) {
|
|
3355
|
+
return createContactCard(options);
|
|
1504
3356
|
}
|
|
1505
3357
|
/**
|
|
1506
|
-
*
|
|
1507
|
-
*
|
|
3358
|
+
* Create an analytics dashboard card
|
|
3359
|
+
*
|
|
3360
|
+
* @param options - Analytics dashboard options
|
|
3361
|
+
* @returns Analytics dashboard configuration
|
|
1508
3362
|
*/
|
|
1509
|
-
|
|
1510
|
-
return
|
|
3363
|
+
static createAnalytics(options) {
|
|
3364
|
+
return createAnalyticsDashboard(options);
|
|
1511
3365
|
}
|
|
1512
3366
|
/**
|
|
1513
|
-
*
|
|
1514
|
-
*
|
|
3367
|
+
* Create a custom card from a template
|
|
3368
|
+
*
|
|
3369
|
+
* @param template - Template function that returns AICardConfig
|
|
3370
|
+
* @returns Card configuration
|
|
1515
3371
|
*/
|
|
1516
|
-
|
|
1517
|
-
return
|
|
3372
|
+
static createCustom(template, config) {
|
|
3373
|
+
return template(config);
|
|
1518
3374
|
}
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
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
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
3425
|
+
ngOnDestroy() {
|
|
3426
|
+
this.destroy$.next();
|
|
3427
|
+
this.destroy$.complete();
|
|
3428
|
+
if (this.componentRef) {
|
|
3429
|
+
this.componentRef.destroy();
|
|
3430
|
+
}
|
|
1558
3431
|
}
|
|
1559
|
-
|
|
1560
|
-
|
|
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
|
-
|
|
1571
|
-
if (
|
|
1572
|
-
|
|
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
|
-
//
|
|
1575
|
-
if (
|
|
1576
|
-
|
|
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
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
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:
|
|
1593
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type:
|
|
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:
|
|
3478
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PluginSectionWrapperComponent, decorators: [{
|
|
1596
3479
|
type: Component,
|
|
1597
3480
|
args: [{
|
|
1598
|
-
|
|
1599
|
-
|
|
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
|
-
}],
|
|
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.
|
|
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
|