osi-cards-lib 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +763 -0
- package/esm2022/lib/components/ai-card-renderer/ai-card-renderer.component.mjs +911 -0
- package/esm2022/lib/components/card-preview/card-preview.component.mjs +74 -0
- package/esm2022/lib/components/card-skeleton/card-skeleton.component.mjs +24 -0
- package/esm2022/lib/components/masonry-grid/masonry-grid.component.mjs +330 -0
- package/esm2022/lib/components/section-renderer/section-renderer.component.mjs +166 -0
- package/esm2022/lib/components/sections/analytics-section/analytics-section.component.mjs +70 -0
- package/esm2022/lib/components/sections/base-section.component.mjs +335 -0
- package/esm2022/lib/components/sections/brand-colors-section/brand-colors-section.component.mjs +89 -0
- package/esm2022/lib/components/sections/chart-section/chart-section.component.mjs +92 -0
- package/esm2022/lib/components/sections/contact-card-section/contact-card-section.component.mjs +70 -0
- package/esm2022/lib/components/sections/event-section/event-section.component.mjs +32 -0
- package/esm2022/lib/components/sections/fallback-section/fallback-section.component.mjs +16 -0
- package/esm2022/lib/components/sections/financials-section/financials-section.component.mjs +53 -0
- package/esm2022/lib/components/sections/info-section.component.mjs +68 -0
- package/esm2022/lib/components/sections/list-section/list-section.component.mjs +36 -0
- package/esm2022/lib/components/sections/map-section/map-section.component.mjs +52 -0
- package/esm2022/lib/components/sections/network-card-section/network-card-section.component.mjs +41 -0
- package/esm2022/lib/components/sections/news-section/news-section.component.mjs +44 -0
- package/esm2022/lib/components/sections/overview-section/overview-section.component.mjs +47 -0
- package/esm2022/lib/components/sections/product-section/product-section.component.mjs +129 -0
- package/esm2022/lib/components/sections/quotation-section/quotation-section.component.mjs +39 -0
- package/esm2022/lib/components/sections/social-media-section/social-media-section.component.mjs +45 -0
- package/esm2022/lib/components/sections/solutions-section/solutions-section.component.mjs +29 -0
- package/esm2022/lib/components/sections/text-reference-section/text-reference-section.component.mjs +42 -0
- package/esm2022/lib/icons/index.mjs +2 -0
- package/esm2022/lib/icons/lucide-icons.module.mjs +91 -0
- package/esm2022/lib/models/card.model.mjs +111 -0
- package/esm2022/lib/models/index.mjs +2 -0
- package/esm2022/lib/services/icon.service.mjs +148 -0
- package/esm2022/lib/services/index.mjs +5 -0
- package/esm2022/lib/services/magnetic-tilt.service.mjs +224 -0
- package/esm2022/lib/services/section-normalization.service.mjs +243 -0
- package/esm2022/lib/services/section-utils.service.mjs +122 -0
- package/esm2022/lib/utils/card-diff.util.mjs +327 -0
- package/esm2022/lib/utils/index.mjs +3 -0
- package/esm2022/lib/utils/responsive.util.mjs +14 -0
- package/esm2022/osi-cards-lib.mjs +5 -0
- package/esm2022/public-api.mjs +57 -0
- package/fesm2022/osi-cards-lib.mjs +3960 -0
- package/index.d.ts +5 -0
- package/lib/components/ai-card-renderer/ai-card-renderer.component.d.ts +163 -0
- package/lib/components/card-preview/card-preview.component.d.ts +52 -0
- package/lib/components/card-skeleton/card-skeleton.component.d.ts +8 -0
- package/lib/components/masonry-grid/masonry-grid.component.d.ts +72 -0
- package/lib/components/section-renderer/section-renderer.component.d.ts +25 -0
- package/lib/components/sections/analytics-section/analytics-section.component.d.ts +32 -0
- package/lib/components/sections/base-section.component.d.ts +138 -0
- package/lib/components/sections/brand-colors-section/brand-colors-section.component.d.ts +28 -0
- package/lib/components/sections/chart-section/chart-section.component.d.ts +30 -0
- package/lib/components/sections/contact-card-section/contact-card-section.component.d.ts +35 -0
- package/lib/components/sections/event-section/event-section.component.d.ts +17 -0
- package/lib/components/sections/fallback-section/fallback-section.component.d.ts +7 -0
- package/lib/components/sections/financials-section/financials-section.component.d.ts +27 -0
- package/lib/components/sections/info-section.component.d.ts +33 -0
- package/lib/components/sections/list-section/list-section.component.d.ts +21 -0
- package/lib/components/sections/map-section/map-section.component.d.ts +22 -0
- package/lib/components/sections/network-card-section/network-card-section.component.d.ts +18 -0
- package/lib/components/sections/news-section/news-section.component.d.ts +16 -0
- package/lib/components/sections/overview-section/overview-section.component.d.ts +19 -0
- package/lib/components/sections/product-section/product-section.component.d.ts +57 -0
- package/lib/components/sections/quotation-section/quotation-section.component.d.ts +23 -0
- package/lib/components/sections/social-media-section/social-media-section.component.d.ts +11 -0
- package/lib/components/sections/solutions-section/solutions-section.component.d.ts +19 -0
- package/lib/components/sections/text-reference-section/text-reference-section.component.d.ts +25 -0
- package/lib/icons/index.d.ts +1 -0
- package/lib/icons/lucide-icons.module.d.ts +7 -0
- package/lib/models/card.model.d.ts +289 -0
- package/lib/models/index.d.ts +1 -0
- package/lib/services/icon.service.d.ts +9 -0
- package/lib/services/index.d.ts +4 -0
- package/lib/services/magnetic-tilt.service.d.ts +34 -0
- package/lib/services/section-normalization.service.d.ts +38 -0
- package/lib/services/section-utils.service.d.ts +46 -0
- package/lib/utils/card-diff.util.d.ts +52 -0
- package/lib/utils/index.d.ts +2 -0
- package/lib/utils/responsive.util.d.ts +2 -0
- package/package.json +63 -0
- package/public-api.d.ts +50 -0
- package/styles/_styles.scss +95 -0
- package/styles/components/cards/_ai-card.scss +743 -0
- package/styles/components/sections/_analytics.scss +280 -0
- package/styles/components/sections/_brand-colors.scss +280 -0
- package/styles/components/sections/_chart.scss +494 -0
- package/styles/components/sections/_contact.scss +250 -0
- package/styles/components/sections/_design-system.scss +540 -0
- package/styles/components/sections/_event.scss +246 -0
- package/styles/components/sections/_fallback.scss +172 -0
- package/styles/components/sections/_financials.scss +258 -0
- package/styles/components/sections/_global-enforcement.scss +648 -0
- package/styles/components/sections/_info.scss +224 -0
- package/styles/components/sections/_list.scss +216 -0
- package/styles/components/sections/_map.scss +186 -0
- package/styles/components/sections/_network.scss +115 -0
- package/styles/components/sections/_news.scss +81 -0
- package/styles/components/sections/_overview.scss +159 -0
- package/styles/components/sections/_product.scss +906 -0
- package/styles/components/sections/_quotation.scss +151 -0
- package/styles/components/sections/_section-shell.scss +385 -0
- package/styles/components/sections/_section-types.scss +290 -0
- package/styles/components/sections/_sections-base.scss +332 -0
- package/styles/components/sections/_social-media.scss +88 -0
- package/styles/components/sections/_solutions.scss +205 -0
- package/styles/components/sections/_text-reference.scss +158 -0
- package/styles/components/sections/_unified-cards.scss +124 -0
- package/styles/core/_animations.scss +766 -0
- package/styles/core/_global.scss +66 -0
- package/styles/core/_mixins.scss +140 -0
- package/styles/core/_surface-layers.scss +76 -0
- package/styles/core/_utilities.scss +193 -0
- package/styles/core/_variables.scss +462 -0
- package/styles/core/variables/_colors.scss +212 -0
- package/styles/layout/_masonry.scss +60 -0
- package/styles/layout/_tilt.scss +214 -0
|
@@ -0,0 +1,3960 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { Injectable, inject, NgZone, NgModule, EventEmitter, ChangeDetectorRef, Output, Input, ChangeDetectionStrategy, Component, ViewChildren, ViewChild, ElementRef } from '@angular/core';
|
|
3
|
+
import { BehaviorSubject, Subject, fromEvent, takeUntil, interval } from 'rxjs';
|
|
4
|
+
import * as i2 from 'lucide-angular';
|
|
5
|
+
import { Zap, XCircle, Wrench, Type, Video, User, UserCheck, Users, Twitter, Trophy, TrendingUp, TrendingDown, Timer, Target, Star, Tag, Sparkles, Shield, Settings, Save, ShoppingCart, Share2, RefreshCw, Quote, PieChart, Phone, Package, Minus, Minimize2, MessageCircle, Maximize2, MapPin, Mail, List, Lightbulb, Linkedin, Instagram, Info, HelpCircle, Hash, Handshake, Grid, GitBranch, Globe, Folder, FileText, Download, DollarSign, Facebook, ExternalLink, Calculator, Code2, Clock, Circle, ChevronRight, Check, CheckCircle2, CalendarX, CalendarPlus, CalendarCheck, Calendar, Building, Briefcase, BookOpen, BarChart3, Box, Award, ArrowUp, ArrowDown, ArrowRight, AlertCircle, Activity, LucideAngularModule } from 'lucide-angular';
|
|
6
|
+
import * as i1 from '@angular/common';
|
|
7
|
+
import { CommonModule, ViewportScroller } from '@angular/common';
|
|
8
|
+
import { trigger, transition, style, animate } from '@angular/animations';
|
|
9
|
+
|
|
10
|
+
class CardTypeGuards {
|
|
11
|
+
static isAICardConfig(obj) {
|
|
12
|
+
if (!obj || typeof obj !== 'object')
|
|
13
|
+
return false;
|
|
14
|
+
const card = obj;
|
|
15
|
+
return (typeof card['cardTitle'] === 'string' &&
|
|
16
|
+
Array.isArray(card['sections']) &&
|
|
17
|
+
card['cardTitle'].length > 0);
|
|
18
|
+
}
|
|
19
|
+
static isCardSection(obj) {
|
|
20
|
+
if (!obj || typeof obj !== 'object')
|
|
21
|
+
return false;
|
|
22
|
+
const section = obj;
|
|
23
|
+
return typeof section['title'] === 'string' && typeof section['type'] === 'string';
|
|
24
|
+
}
|
|
25
|
+
static isCardField(obj) {
|
|
26
|
+
if (!obj || typeof obj !== 'object')
|
|
27
|
+
return false;
|
|
28
|
+
// CardField can have any properties, just needs to be an object
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Type guard to check if an action is a valid mail action
|
|
33
|
+
* Validates that required fields (contact, subject, body) are present
|
|
34
|
+
*/
|
|
35
|
+
static isMailAction(obj) {
|
|
36
|
+
if (!obj || typeof obj !== 'object')
|
|
37
|
+
return false;
|
|
38
|
+
const action = obj;
|
|
39
|
+
// Must have type 'mail'
|
|
40
|
+
if (action['type'] !== 'mail')
|
|
41
|
+
return false;
|
|
42
|
+
// Must have email property
|
|
43
|
+
if (!action['email'] || typeof action['email'] !== 'object')
|
|
44
|
+
return false;
|
|
45
|
+
const email = action['email'];
|
|
46
|
+
// Must have contact with name, email, and role
|
|
47
|
+
if (!email['contact'] || typeof email['contact'] !== 'object')
|
|
48
|
+
return false;
|
|
49
|
+
const contact = email['contact'];
|
|
50
|
+
if (typeof contact['name'] !== 'string' ||
|
|
51
|
+
typeof contact['email'] !== 'string' ||
|
|
52
|
+
typeof contact['role'] !== 'string') {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
// Must have subject
|
|
56
|
+
if (typeof email['subject'] !== 'string')
|
|
57
|
+
return false;
|
|
58
|
+
// Must have body
|
|
59
|
+
if (typeof email['body'] !== 'string')
|
|
60
|
+
return false;
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
class CardUtils {
|
|
65
|
+
static safeString(value, maxLength = 1000) {
|
|
66
|
+
if (typeof value === 'string') {
|
|
67
|
+
return value.substring(0, maxLength);
|
|
68
|
+
}
|
|
69
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
70
|
+
return String(value);
|
|
71
|
+
}
|
|
72
|
+
return '';
|
|
73
|
+
}
|
|
74
|
+
static safeNumber(value, defaultValue = 0) {
|
|
75
|
+
if (typeof value === 'number') {
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
if (typeof value === 'string') {
|
|
79
|
+
const parsed = parseFloat(value);
|
|
80
|
+
return isNaN(parsed) ? defaultValue : parsed;
|
|
81
|
+
}
|
|
82
|
+
return defaultValue;
|
|
83
|
+
}
|
|
84
|
+
static generateId(prefix = 'item') {
|
|
85
|
+
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
86
|
+
}
|
|
87
|
+
static ensureSectionIds(sections) {
|
|
88
|
+
return sections.map((section, sectionIndex) => ({
|
|
89
|
+
...section,
|
|
90
|
+
id: section.id || this.generateId(`section_${sectionIndex}`),
|
|
91
|
+
fields: section.fields ? this.ensureFieldIds(section.fields, sectionIndex) : undefined,
|
|
92
|
+
items: section.items ? this.ensureItemIds(section.items, sectionIndex) : undefined
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
static ensureFieldIds(fields, sectionIndex) {
|
|
96
|
+
return fields.map((field, fieldIndex) => ({
|
|
97
|
+
...field,
|
|
98
|
+
id: field.id || this.generateId(`field_${sectionIndex}_${fieldIndex}`)
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
static ensureItemIds(items, sectionIndex) {
|
|
102
|
+
return items.map((item, itemIndex) => ({
|
|
103
|
+
...item,
|
|
104
|
+
id: item.id || this.generateId(`item_${sectionIndex}_${itemIndex}`)
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
static sanitizeCardConfig(config) {
|
|
108
|
+
if (!CardTypeGuards.isAICardConfig(config)) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
...config,
|
|
113
|
+
cardTitle: this.safeString(config.cardTitle, 200),
|
|
114
|
+
cardSubtitle: config.cardSubtitle ? this.safeString(config.cardSubtitle, 500) : undefined,
|
|
115
|
+
sections: this.ensureSectionIds(config.sections.filter(CardTypeGuards.isCardSection)),
|
|
116
|
+
actions: config.actions?.map((action) => ({ ...action, id: action.id ?? this.generateId('action') }))
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
class IconService {
|
|
122
|
+
constructor() {
|
|
123
|
+
this.iconMap = {
|
|
124
|
+
// Business & Finance
|
|
125
|
+
'revenue': 'dollar-sign',
|
|
126
|
+
'profit': 'trending-up',
|
|
127
|
+
'expenses': 'trending-down',
|
|
128
|
+
'budget': 'pie-chart',
|
|
129
|
+
'sales': 'shopping-cart',
|
|
130
|
+
'cost': 'calculator',
|
|
131
|
+
'price': 'tag',
|
|
132
|
+
'value': 'star',
|
|
133
|
+
'growth': 'arrow-up',
|
|
134
|
+
'decline': 'arrow-down',
|
|
135
|
+
// Contact & Communication
|
|
136
|
+
'email': 'mail',
|
|
137
|
+
'phone': 'phone',
|
|
138
|
+
'address': 'map-pin',
|
|
139
|
+
'website': 'globe',
|
|
140
|
+
'linkedin': 'linkedin',
|
|
141
|
+
'twitter': 'twitter',
|
|
142
|
+
'facebook': 'facebook',
|
|
143
|
+
'instagram': 'instagram',
|
|
144
|
+
'contact': 'user',
|
|
145
|
+
'message': 'message-circle',
|
|
146
|
+
// Time & Dates
|
|
147
|
+
'date': 'calendar',
|
|
148
|
+
'time': 'clock',
|
|
149
|
+
'deadline': 'calendar-x',
|
|
150
|
+
'schedule': 'calendar-check',
|
|
151
|
+
'created': 'calendar-plus',
|
|
152
|
+
'updated': 'refresh-cw',
|
|
153
|
+
'duration': 'timer',
|
|
154
|
+
// Status & Progress
|
|
155
|
+
'status': 'info',
|
|
156
|
+
'progress': 'activity',
|
|
157
|
+
'completed': 'check-circle',
|
|
158
|
+
'pending': 'clock',
|
|
159
|
+
'failed': 'x-circle',
|
|
160
|
+
'warning': 'alert-triangle',
|
|
161
|
+
'success': 'check',
|
|
162
|
+
'error': 'alert-circle',
|
|
163
|
+
// Business Operations
|
|
164
|
+
'company': 'building',
|
|
165
|
+
'department': 'users',
|
|
166
|
+
'team': 'users',
|
|
167
|
+
'manager': 'user-check',
|
|
168
|
+
'employee': 'user',
|
|
169
|
+
'position': 'briefcase',
|
|
170
|
+
'role': 'shield',
|
|
171
|
+
'title': 'tag',
|
|
172
|
+
// Products & Services
|
|
173
|
+
'product': 'package',
|
|
174
|
+
'service': 'wrench',
|
|
175
|
+
'category': 'folder',
|
|
176
|
+
'type': 'type',
|
|
177
|
+
'brand': 'award',
|
|
178
|
+
'model': 'box',
|
|
179
|
+
'version': 'git-branch',
|
|
180
|
+
// Metrics & Analytics
|
|
181
|
+
'metric': 'bar-chart',
|
|
182
|
+
'analytics': 'trending-up',
|
|
183
|
+
'performance': 'zap',
|
|
184
|
+
'efficiency': 'target',
|
|
185
|
+
'quality': 'award',
|
|
186
|
+
'rating': 'star',
|
|
187
|
+
'score': 'hash',
|
|
188
|
+
'rank': 'trending-up',
|
|
189
|
+
// Default fallbacks
|
|
190
|
+
'default': 'circle',
|
|
191
|
+
'unknown': 'help-circle'
|
|
192
|
+
};
|
|
193
|
+
this.classMap = {
|
|
194
|
+
// Business & Finance
|
|
195
|
+
'revenue': 'text-green-500',
|
|
196
|
+
'profit': 'text-green-600',
|
|
197
|
+
'expenses': 'text-red-500',
|
|
198
|
+
'budget': 'text-blue-500',
|
|
199
|
+
'sales': 'text-purple-500',
|
|
200
|
+
'cost': 'text-orange-500',
|
|
201
|
+
'price': 'text-yellow-600',
|
|
202
|
+
'value': 'text-amber-500',
|
|
203
|
+
'growth': 'text-green-500',
|
|
204
|
+
'decline': 'text-red-500',
|
|
205
|
+
// Contact & Communication
|
|
206
|
+
'email': 'text-blue-500',
|
|
207
|
+
'phone': 'text-green-500',
|
|
208
|
+
'address': 'text-red-500',
|
|
209
|
+
'website': 'text-blue-600',
|
|
210
|
+
'linkedin': 'text-blue-700',
|
|
211
|
+
'twitter': 'text-sky-500',
|
|
212
|
+
'facebook': 'text-blue-600',
|
|
213
|
+
'instagram': 'text-pink-500',
|
|
214
|
+
'contact': 'text-gray-600',
|
|
215
|
+
'message': 'text-blue-500',
|
|
216
|
+
// Status & Progress
|
|
217
|
+
'status': 'text-blue-500',
|
|
218
|
+
'progress': 'text-orange-500',
|
|
219
|
+
'completed': 'text-green-500',
|
|
220
|
+
'pending': 'text-yellow-500',
|
|
221
|
+
'failed': 'text-red-500',
|
|
222
|
+
'warning': 'text-amber-500',
|
|
223
|
+
'success': 'text-green-500',
|
|
224
|
+
'error': 'text-red-500',
|
|
225
|
+
// Default
|
|
226
|
+
'default': 'text-gray-500'
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
getFieldIcon(fieldName) {
|
|
230
|
+
const normalizedName = fieldName.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
231
|
+
// Try exact match first
|
|
232
|
+
if (this.iconMap[normalizedName]) {
|
|
233
|
+
return this.iconMap[normalizedName];
|
|
234
|
+
}
|
|
235
|
+
// Try partial matches
|
|
236
|
+
for (const [key, icon] of Object.entries(this.iconMap)) {
|
|
237
|
+
if (normalizedName.includes(key) || key.includes(normalizedName)) {
|
|
238
|
+
return icon;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return this.iconMap['default'];
|
|
242
|
+
}
|
|
243
|
+
getFieldIconClass(fieldName) {
|
|
244
|
+
const normalizedName = fieldName.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
245
|
+
// Try exact match first
|
|
246
|
+
if (this.classMap[normalizedName]) {
|
|
247
|
+
return this.classMap[normalizedName];
|
|
248
|
+
}
|
|
249
|
+
// Try partial matches
|
|
250
|
+
for (const [key, className] of Object.entries(this.classMap)) {
|
|
251
|
+
if (normalizedName.includes(key) || key.includes(normalizedName)) {
|
|
252
|
+
return className;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return this.classMap['default'];
|
|
256
|
+
}
|
|
257
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: IconService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
258
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: IconService, providedIn: 'root' }); }
|
|
259
|
+
}
|
|
260
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: IconService, decorators: [{
|
|
261
|
+
type: Injectable,
|
|
262
|
+
args: [{
|
|
263
|
+
providedIn: 'root'
|
|
264
|
+
}]
|
|
265
|
+
}] });
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Column span thresholds for each section type
|
|
269
|
+
* These define when a section should span 2 or 3 columns based on content density
|
|
270
|
+
* Lower thresholds = sections span 2 columns more easily (with less content)
|
|
271
|
+
*
|
|
272
|
+
* Threshold calculation: fieldCount + itemCount + descriptionDensity >= threshold
|
|
273
|
+
* - two: minimum score to span 2 columns
|
|
274
|
+
* - three: minimum score to span 3 columns (optional)
|
|
275
|
+
*/
|
|
276
|
+
const SECTION_COL_SPAN_THRESHOLDS = {
|
|
277
|
+
// Overview sections typically have 6-10 key-value pairs, should span 2 columns easily
|
|
278
|
+
overview: { two: 5, three: 10 },
|
|
279
|
+
// Charts and maps need space, should span 2 columns with minimal content
|
|
280
|
+
chart: { two: 2 },
|
|
281
|
+
map: { two: 2 },
|
|
282
|
+
locations: { two: 2 },
|
|
283
|
+
// Contact cards typically have 3-4 contacts, should span 2 columns easily
|
|
284
|
+
'contact-card': { two: 3 },
|
|
285
|
+
'network-card': { two: 3 },
|
|
286
|
+
// Analytics/Stats typically have 3-4 metrics, should span 2 columns
|
|
287
|
+
analytics: { two: 3 },
|
|
288
|
+
stats: { two: 3 },
|
|
289
|
+
// Financials typically have 3-5 fields, should span 2 columns
|
|
290
|
+
financials: { two: 3 },
|
|
291
|
+
// Info sections with key-value pairs, should span 2 columns with 4+ fields
|
|
292
|
+
info: { two: 4, three: 8 },
|
|
293
|
+
// Solutions typically have 3-4 items, should span 2 columns
|
|
294
|
+
solutions: { two: 3 },
|
|
295
|
+
product: { two: 3 },
|
|
296
|
+
// Lists typically have 4-6 items, should span 2 columns
|
|
297
|
+
list: { two: 4 },
|
|
298
|
+
// Events/Timelines typically have 3-5 phases, should span 2 columns
|
|
299
|
+
event: { two: 3 },
|
|
300
|
+
// Text-heavy sections should span 2 columns for readability
|
|
301
|
+
quotation: { two: 3 },
|
|
302
|
+
'text-reference': { two: 3 },
|
|
303
|
+
// Projects always span 1 column (special case handled in masonry grid)
|
|
304
|
+
project: { two: 999 } // Effectively always 1 column
|
|
305
|
+
};
|
|
306
|
+
const DEFAULT_COL_SPAN_THRESHOLD$1 = { two: 6 };
|
|
307
|
+
class SectionNormalizationService {
|
|
308
|
+
constructor() {
|
|
309
|
+
/**
|
|
310
|
+
* Supported section types
|
|
311
|
+
*/
|
|
312
|
+
this.supportedTypes = [
|
|
313
|
+
'info',
|
|
314
|
+
'analytics',
|
|
315
|
+
'contact-card',
|
|
316
|
+
'network-card',
|
|
317
|
+
'map',
|
|
318
|
+
'financials',
|
|
319
|
+
'locations',
|
|
320
|
+
'event',
|
|
321
|
+
'project',
|
|
322
|
+
'list',
|
|
323
|
+
'chart',
|
|
324
|
+
'product',
|
|
325
|
+
'solutions',
|
|
326
|
+
'overview',
|
|
327
|
+
'stats',
|
|
328
|
+
'quotation',
|
|
329
|
+
'text-reference'
|
|
330
|
+
];
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Normalize a section by resolving its type and ensuring required properties
|
|
334
|
+
*/
|
|
335
|
+
normalizeSection(section) {
|
|
336
|
+
const rawType = (section.type ?? '').toLowerCase();
|
|
337
|
+
const title = (section.title ?? '').toLowerCase();
|
|
338
|
+
const resolvedType = this.resolveSectionType(rawType, title);
|
|
339
|
+
const normalized = {
|
|
340
|
+
...section,
|
|
341
|
+
type: resolvedType
|
|
342
|
+
};
|
|
343
|
+
// Handle analytics sections with metrics array
|
|
344
|
+
if (resolvedType === 'analytics' && (!normalized.fields || !normalized.fields.length)) {
|
|
345
|
+
const metrics = section['metrics'];
|
|
346
|
+
if (Array.isArray(metrics)) {
|
|
347
|
+
normalized.fields = metrics;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// Use subtitle as description if description is missing
|
|
351
|
+
if (!normalized.description && section.subtitle) {
|
|
352
|
+
normalized.description = section.subtitle;
|
|
353
|
+
}
|
|
354
|
+
// Add column span thresholds to section meta if not already present
|
|
355
|
+
// This allows each section to have its own column logic
|
|
356
|
+
const existingMeta = normalized.meta;
|
|
357
|
+
const colSpanThresholds = this.getColSpanThresholdsForType(resolvedType);
|
|
358
|
+
normalized.meta = {
|
|
359
|
+
...existingMeta,
|
|
360
|
+
// Only add if not already defined (allows sections to override)
|
|
361
|
+
colSpanThresholds: existingMeta?.['colSpanThresholds'] ?? colSpanThresholds
|
|
362
|
+
};
|
|
363
|
+
return normalized;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Get column span thresholds for a section type
|
|
367
|
+
* This is the default logic for each section type
|
|
368
|
+
*/
|
|
369
|
+
getColSpanThresholdsForType(type) {
|
|
370
|
+
return SECTION_COL_SPAN_THRESHOLDS[type] ?? DEFAULT_COL_SPAN_THRESHOLD$1;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Resolve section type from raw type and title
|
|
374
|
+
*/
|
|
375
|
+
resolveSectionType(rawType, title) {
|
|
376
|
+
// Title-based overrides take precedence
|
|
377
|
+
if (!rawType && title.includes('overview')) {
|
|
378
|
+
return 'overview';
|
|
379
|
+
}
|
|
380
|
+
// Type-based resolution
|
|
381
|
+
switch (rawType) {
|
|
382
|
+
case 'timeline':
|
|
383
|
+
return 'event';
|
|
384
|
+
case 'metrics':
|
|
385
|
+
case 'stats':
|
|
386
|
+
return 'analytics';
|
|
387
|
+
case 'table':
|
|
388
|
+
return 'list';
|
|
389
|
+
case 'locations':
|
|
390
|
+
return 'map';
|
|
391
|
+
case 'project':
|
|
392
|
+
return 'info';
|
|
393
|
+
case 'contact':
|
|
394
|
+
return 'contact-card';
|
|
395
|
+
case 'network':
|
|
396
|
+
return 'network-card';
|
|
397
|
+
case 'quotation':
|
|
398
|
+
case 'quote':
|
|
399
|
+
return 'quotation';
|
|
400
|
+
case 'text-reference':
|
|
401
|
+
case 'reference':
|
|
402
|
+
case 'text-ref':
|
|
403
|
+
return 'text-reference';
|
|
404
|
+
case '':
|
|
405
|
+
return title.includes('overview') ? 'overview' : 'info';
|
|
406
|
+
default:
|
|
407
|
+
return this.supportedTypes.includes(rawType)
|
|
408
|
+
? rawType
|
|
409
|
+
: 'info';
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Get section priority for sorting
|
|
414
|
+
* Lower numbers appear first
|
|
415
|
+
*/
|
|
416
|
+
getSectionPriority(section) {
|
|
417
|
+
const type = section.type?.toLowerCase() ?? '';
|
|
418
|
+
const title = section.title?.toLowerCase() ?? '';
|
|
419
|
+
// Priority order
|
|
420
|
+
if (type === 'contact-card' || type === 'contact')
|
|
421
|
+
return 1;
|
|
422
|
+
if (type === 'overview' || title.includes('overview'))
|
|
423
|
+
return 2;
|
|
424
|
+
if (type === 'analytics')
|
|
425
|
+
return 3;
|
|
426
|
+
if (type === 'product')
|
|
427
|
+
return 4;
|
|
428
|
+
if (type === 'solutions')
|
|
429
|
+
return 5;
|
|
430
|
+
if (type === 'map')
|
|
431
|
+
return 6;
|
|
432
|
+
if (type === 'financials')
|
|
433
|
+
return 7;
|
|
434
|
+
if (type === 'chart')
|
|
435
|
+
return 8;
|
|
436
|
+
if (type === 'list')
|
|
437
|
+
return 9;
|
|
438
|
+
if (type === 'event')
|
|
439
|
+
return 10;
|
|
440
|
+
if (type === 'info')
|
|
441
|
+
return 11;
|
|
442
|
+
return 12;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Sort sections by priority
|
|
446
|
+
*/
|
|
447
|
+
sortSections(sections) {
|
|
448
|
+
return [...sections].sort((a, b) => {
|
|
449
|
+
const streamingOrderComparison = this.compareStreamingOrder(a, b);
|
|
450
|
+
if (streamingOrderComparison !== 0) {
|
|
451
|
+
return streamingOrderComparison;
|
|
452
|
+
}
|
|
453
|
+
const priorityA = this.getSectionPriority(a);
|
|
454
|
+
const priorityB = this.getSectionPriority(b);
|
|
455
|
+
return priorityA - priorityB;
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
compareStreamingOrder(a, b) {
|
|
459
|
+
const orderA = this.getStreamingOrder(a);
|
|
460
|
+
const orderB = this.getStreamingOrder(b);
|
|
461
|
+
const hasOrderA = orderA !== null;
|
|
462
|
+
const hasOrderB = orderB !== null;
|
|
463
|
+
if (!hasOrderA && !hasOrderB) {
|
|
464
|
+
return 0;
|
|
465
|
+
}
|
|
466
|
+
if (hasOrderA && !hasOrderB) {
|
|
467
|
+
return -1;
|
|
468
|
+
}
|
|
469
|
+
if (!hasOrderA && hasOrderB) {
|
|
470
|
+
return 1;
|
|
471
|
+
}
|
|
472
|
+
if (orderA < orderB) {
|
|
473
|
+
return -1;
|
|
474
|
+
}
|
|
475
|
+
if (orderA > orderB) {
|
|
476
|
+
return 1;
|
|
477
|
+
}
|
|
478
|
+
return 0;
|
|
479
|
+
}
|
|
480
|
+
getStreamingOrder(section) {
|
|
481
|
+
const metadata = section.meta;
|
|
482
|
+
if (!metadata) {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
const rawOrder = metadata['streamingOrder'];
|
|
486
|
+
if (typeof rawOrder === 'number' && Number.isFinite(rawOrder)) {
|
|
487
|
+
return rawOrder;
|
|
488
|
+
}
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Normalize and sort sections
|
|
493
|
+
*/
|
|
494
|
+
normalizeAndSortSections(sections) {
|
|
495
|
+
const normalized = sections.map(section => this.normalizeSection(section));
|
|
496
|
+
return this.sortSections(normalized);
|
|
497
|
+
}
|
|
498
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SectionNormalizationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
499
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SectionNormalizationService, providedIn: 'root' }); }
|
|
500
|
+
}
|
|
501
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SectionNormalizationService, decorators: [{
|
|
502
|
+
type: Injectable,
|
|
503
|
+
args: [{
|
|
504
|
+
providedIn: 'root'
|
|
505
|
+
}]
|
|
506
|
+
}] });
|
|
507
|
+
|
|
508
|
+
const MAX_LIFT_PX = 1.0; // Doubled from 0.5 for stronger tilt effect
|
|
509
|
+
const BASE_GLOW_BLUR = 8; // Reduced from 12 - tighter glow
|
|
510
|
+
const MAX_GLOW_BLUR_OFFSET = 4; // Reduced from 6 - less spread
|
|
511
|
+
const BASE_GLOW_OPACITY = 0.225; // Intensified by 50% (0.15 * 1.5)
|
|
512
|
+
const MAX_GLOW_OPACITY_OFFSET = 0.18; // Intensified by 50% (0.12 * 1.5)
|
|
513
|
+
const MAX_REFLECTION_OPACITY = 0.22;
|
|
514
|
+
class MagneticTiltService {
|
|
515
|
+
constructor() {
|
|
516
|
+
this.tiltCalculationsSubject = new BehaviorSubject({
|
|
517
|
+
rotateX: 0,
|
|
518
|
+
rotateY: 0,
|
|
519
|
+
glowBlur: BASE_GLOW_BLUR,
|
|
520
|
+
glowOpacity: BASE_GLOW_OPACITY,
|
|
521
|
+
reflectionOpacity: 0
|
|
522
|
+
});
|
|
523
|
+
this.tiltCalculations$ = this.tiltCalculationsSubject.asObservable();
|
|
524
|
+
// Performance: cache element dimensions to avoid repeated getBoundingClientRect calls
|
|
525
|
+
this.elementCache = new Map();
|
|
526
|
+
this.rafId = null;
|
|
527
|
+
this.pendingUpdate = null;
|
|
528
|
+
this.lastCalculations = null;
|
|
529
|
+
this.CACHE_DURATION = 100; // Recalculate rect every 100ms max
|
|
530
|
+
this.ngZone = inject(NgZone);
|
|
531
|
+
this.resetTimeoutId = null;
|
|
532
|
+
this.RESET_TRANSITION_DURATION_MS = 500; // Smooth exit transition duration
|
|
533
|
+
}
|
|
534
|
+
calculateTilt(mousePosition, element) {
|
|
535
|
+
if (!element) {
|
|
536
|
+
this.resetTilt();
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
// Cancel any ongoing reset animation when mouse re-enters
|
|
540
|
+
if (this.resetTimeoutId !== null) {
|
|
541
|
+
clearTimeout(this.resetTimeoutId);
|
|
542
|
+
this.resetTimeoutId = null;
|
|
543
|
+
}
|
|
544
|
+
// Store pending update for RAF batching
|
|
545
|
+
this.pendingUpdate = { mousePosition, element };
|
|
546
|
+
// Schedule update via RAF for smooth 60fps
|
|
547
|
+
if (this.rafId === null) {
|
|
548
|
+
this.rafId = requestAnimationFrame(() => {
|
|
549
|
+
this.processTiltUpdate();
|
|
550
|
+
this.rafId = null;
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
processTiltUpdate() {
|
|
555
|
+
if (!this.pendingUpdate)
|
|
556
|
+
return;
|
|
557
|
+
const { mousePosition, element } = this.pendingUpdate;
|
|
558
|
+
this.pendingUpdate = null;
|
|
559
|
+
if (!element) {
|
|
560
|
+
this.resetTilt();
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
// Get or update cached element dimensions
|
|
564
|
+
const cache = this.getElementCache(element);
|
|
565
|
+
const fx = (mousePosition.x - cache.rect.left) / cache.rect.width;
|
|
566
|
+
const fy = (mousePosition.y - cache.rect.top) / cache.rect.height;
|
|
567
|
+
const clampedFx = Math.max(0, Math.min(1, fx));
|
|
568
|
+
const clampedFy = Math.max(0, Math.min(1, fy));
|
|
569
|
+
// Optimized calculations
|
|
570
|
+
const sinX = Math.sin(clampedFx * 2 * Math.PI);
|
|
571
|
+
const sinY = Math.sin(clampedFy * 2 * Math.PI);
|
|
572
|
+
const rotateY = sinX * cache.maxAngleY;
|
|
573
|
+
const rotateX = -sinY * cache.maxAngleX;
|
|
574
|
+
const intensity = Math.max(Math.abs(sinX), Math.abs(sinY));
|
|
575
|
+
const glowBlur = BASE_GLOW_BLUR + intensity * MAX_GLOW_BLUR_OFFSET;
|
|
576
|
+
const glowOpacity = BASE_GLOW_OPACITY + intensity * MAX_GLOW_OPACITY_OFFSET;
|
|
577
|
+
const reflectionOpacity = intensity * MAX_REFLECTION_OPACITY;
|
|
578
|
+
const newCalculations = {
|
|
579
|
+
rotateX,
|
|
580
|
+
rotateY,
|
|
581
|
+
glowBlur,
|
|
582
|
+
glowOpacity,
|
|
583
|
+
reflectionOpacity
|
|
584
|
+
};
|
|
585
|
+
// Only emit if values actually changed (prevent unnecessary updates)
|
|
586
|
+
if (!this.lastCalculations || this.hasCalculationsChanged(this.lastCalculations, newCalculations)) {
|
|
587
|
+
this.lastCalculations = newCalculations;
|
|
588
|
+
// Run outside Angular zone for better performance
|
|
589
|
+
this.ngZone.runOutsideAngular(() => {
|
|
590
|
+
this.tiltCalculationsSubject.next(newCalculations);
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
getElementCache(element) {
|
|
595
|
+
const now = performance.now();
|
|
596
|
+
const cached = this.elementCache.get(element);
|
|
597
|
+
// Use cache if recent (within CACHE_DURATION ms)
|
|
598
|
+
if (cached && (now - cached.lastUpdate) < this.CACHE_DURATION) {
|
|
599
|
+
return cached;
|
|
600
|
+
}
|
|
601
|
+
// Recalculate and cache
|
|
602
|
+
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
|
+
const cache = {
|
|
608
|
+
element,
|
|
609
|
+
rect,
|
|
610
|
+
halfW,
|
|
611
|
+
halfH,
|
|
612
|
+
maxAngleY,
|
|
613
|
+
maxAngleX,
|
|
614
|
+
lastUpdate: now
|
|
615
|
+
};
|
|
616
|
+
this.elementCache.set(element, cache);
|
|
617
|
+
return cache;
|
|
618
|
+
}
|
|
619
|
+
hasCalculationsChanged(old, newCalc) {
|
|
620
|
+
// Reduced threshold for smoother glow transitions - allow more frequent updates
|
|
621
|
+
const threshold = 0.005; // Reduced from 0.01 for smoother glow
|
|
622
|
+
return (Math.abs(old.rotateX - newCalc.rotateX) > threshold ||
|
|
623
|
+
Math.abs(old.rotateY - newCalc.rotateY) > threshold ||
|
|
624
|
+
Math.abs(old.glowBlur - newCalc.glowBlur) > threshold ||
|
|
625
|
+
Math.abs(old.glowOpacity - newCalc.glowOpacity) > threshold ||
|
|
626
|
+
Math.abs(old.reflectionOpacity - newCalc.reflectionOpacity) > threshold);
|
|
627
|
+
}
|
|
628
|
+
resetTilt(smooth = true) {
|
|
629
|
+
// Cancel any pending tilt calculations
|
|
630
|
+
if (this.rafId !== null) {
|
|
631
|
+
cancelAnimationFrame(this.rafId);
|
|
632
|
+
this.rafId = null;
|
|
633
|
+
}
|
|
634
|
+
this.pendingUpdate = null;
|
|
635
|
+
// Clear any existing reset timeout
|
|
636
|
+
if (this.resetTimeoutId !== null) {
|
|
637
|
+
clearTimeout(this.resetTimeoutId);
|
|
638
|
+
this.resetTimeoutId = null;
|
|
639
|
+
}
|
|
640
|
+
if (smooth) {
|
|
641
|
+
// Smooth reset: gradually transition to zero over the transition duration
|
|
642
|
+
// This allows the CSS transition to complete smoothly even if mouse leaves quickly
|
|
643
|
+
const startTime = performance.now();
|
|
644
|
+
const startCalculations = this.lastCalculations || {
|
|
645
|
+
rotateX: 0,
|
|
646
|
+
rotateY: 0,
|
|
647
|
+
glowBlur: BASE_GLOW_BLUR,
|
|
648
|
+
glowOpacity: BASE_GLOW_OPACITY,
|
|
649
|
+
reflectionOpacity: 0
|
|
650
|
+
};
|
|
651
|
+
const animateReset = () => {
|
|
652
|
+
const elapsed = performance.now() - startTime;
|
|
653
|
+
const progress = Math.min(elapsed / this.RESET_TRANSITION_DURATION_MS, 1);
|
|
654
|
+
// Ease-out function for smooth deceleration
|
|
655
|
+
const easeOut = 1 - Math.pow(1 - progress, 3);
|
|
656
|
+
const currentCalculations = {
|
|
657
|
+
rotateX: startCalculations.rotateX * (1 - easeOut),
|
|
658
|
+
rotateY: startCalculations.rotateY * (1 - easeOut),
|
|
659
|
+
glowBlur: BASE_GLOW_BLUR + (startCalculations.glowBlur - BASE_GLOW_BLUR) * (1 - easeOut),
|
|
660
|
+
glowOpacity: BASE_GLOW_OPACITY + (startCalculations.glowOpacity - BASE_GLOW_OPACITY) * (1 - easeOut),
|
|
661
|
+
reflectionOpacity: startCalculations.reflectionOpacity * (1 - easeOut)
|
|
662
|
+
};
|
|
663
|
+
this.lastCalculations = currentCalculations;
|
|
664
|
+
this.ngZone.runOutsideAngular(() => {
|
|
665
|
+
this.tiltCalculationsSubject.next(currentCalculations);
|
|
666
|
+
});
|
|
667
|
+
if (progress < 1) {
|
|
668
|
+
// Continue animation
|
|
669
|
+
this.resetTimeoutId = window.setTimeout(animateReset, 16); // ~60fps
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
// Animation complete - set final values
|
|
673
|
+
this.lastCalculations = null;
|
|
674
|
+
this.tiltCalculationsSubject.next({
|
|
675
|
+
rotateX: 0,
|
|
676
|
+
rotateY: 0,
|
|
677
|
+
glowBlur: BASE_GLOW_BLUR,
|
|
678
|
+
glowOpacity: BASE_GLOW_OPACITY,
|
|
679
|
+
reflectionOpacity: 0
|
|
680
|
+
});
|
|
681
|
+
this.resetTimeoutId = null;
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
// Start smooth reset animation
|
|
685
|
+
animateReset();
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
// Immediate reset (for cleanup)
|
|
689
|
+
this.lastCalculations = null;
|
|
690
|
+
this.tiltCalculationsSubject.next({
|
|
691
|
+
rotateX: 0,
|
|
692
|
+
rotateY: 0,
|
|
693
|
+
glowBlur: BASE_GLOW_BLUR,
|
|
694
|
+
glowOpacity: BASE_GLOW_OPACITY,
|
|
695
|
+
reflectionOpacity: 0
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
// Cleanup cached elements when component is destroyed
|
|
700
|
+
clearCache(element) {
|
|
701
|
+
if (element) {
|
|
702
|
+
this.elementCache.delete(element);
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
this.elementCache.clear();
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
// Cleanup method for service destruction
|
|
709
|
+
ngOnDestroy() {
|
|
710
|
+
if (this.resetTimeoutId !== null) {
|
|
711
|
+
clearTimeout(this.resetTimeoutId);
|
|
712
|
+
this.resetTimeoutId = null;
|
|
713
|
+
}
|
|
714
|
+
if (this.rafId !== null) {
|
|
715
|
+
cancelAnimationFrame(this.rafId);
|
|
716
|
+
this.rafId = null;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MagneticTiltService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
720
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MagneticTiltService, providedIn: 'root' }); }
|
|
721
|
+
}
|
|
722
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MagneticTiltService, decorators: [{
|
|
723
|
+
type: Injectable,
|
|
724
|
+
args: [{
|
|
725
|
+
providedIn: 'root'
|
|
726
|
+
}]
|
|
727
|
+
}] });
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Utility service for section components
|
|
731
|
+
* Provides consistent status, trend, and icon handling across all sections
|
|
732
|
+
*/
|
|
733
|
+
class SectionUtilsService {
|
|
734
|
+
/**
|
|
735
|
+
* Get CSS classes for status badges/tags
|
|
736
|
+
* Returns consistent classes across all section types
|
|
737
|
+
*/
|
|
738
|
+
getStatusClasses(status) {
|
|
739
|
+
const normalizedStatus = (status ?? '').toLowerCase().trim();
|
|
740
|
+
switch (normalizedStatus) {
|
|
741
|
+
case 'completed':
|
|
742
|
+
case 'success':
|
|
743
|
+
return 'status--completed';
|
|
744
|
+
case 'active':
|
|
745
|
+
case 'in-progress':
|
|
746
|
+
return 'status--active';
|
|
747
|
+
case 'pending':
|
|
748
|
+
case 'warning':
|
|
749
|
+
return 'status--pending';
|
|
750
|
+
case 'cancelled':
|
|
751
|
+
case 'blocked':
|
|
752
|
+
case 'delayed':
|
|
753
|
+
case 'inactive':
|
|
754
|
+
case 'error':
|
|
755
|
+
return 'status--blocked';
|
|
756
|
+
default:
|
|
757
|
+
return 'status--default';
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Get CSS classes for priority badges/tags
|
|
762
|
+
*/
|
|
763
|
+
getPriorityClasses(priority) {
|
|
764
|
+
const normalizedPriority = (priority ?? '').toLowerCase().trim();
|
|
765
|
+
switch (normalizedPriority) {
|
|
766
|
+
case 'high':
|
|
767
|
+
return 'priority--high';
|
|
768
|
+
case 'medium':
|
|
769
|
+
return 'priority--medium';
|
|
770
|
+
case 'low':
|
|
771
|
+
return 'priority--low';
|
|
772
|
+
default:
|
|
773
|
+
return 'priority--default';
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Get icon name for trend indicators
|
|
778
|
+
*/
|
|
779
|
+
getTrendIcon(trend) {
|
|
780
|
+
const normalizedTrend = (trend ?? '').toLowerCase().trim();
|
|
781
|
+
switch (normalizedTrend) {
|
|
782
|
+
case 'up':
|
|
783
|
+
return 'trending-up';
|
|
784
|
+
case 'down':
|
|
785
|
+
return 'trending-down';
|
|
786
|
+
case 'stable':
|
|
787
|
+
case 'neutral':
|
|
788
|
+
return 'minus';
|
|
789
|
+
default:
|
|
790
|
+
return 'bar-chart-3';
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Get CSS classes for trend indicators
|
|
795
|
+
*/
|
|
796
|
+
getTrendClass(trend) {
|
|
797
|
+
// Handle numeric values (change percentages)
|
|
798
|
+
if (typeof trend === 'number') {
|
|
799
|
+
if (trend > 0)
|
|
800
|
+
return 'trend--up';
|
|
801
|
+
if (trend < 0)
|
|
802
|
+
return 'trend--down';
|
|
803
|
+
return 'trend--stable';
|
|
804
|
+
}
|
|
805
|
+
const normalizedTrend = (trend ?? '').toLowerCase().trim();
|
|
806
|
+
switch (normalizedTrend) {
|
|
807
|
+
case 'up':
|
|
808
|
+
return 'trend--up';
|
|
809
|
+
case 'down':
|
|
810
|
+
return 'trend--down';
|
|
811
|
+
case 'stable':
|
|
812
|
+
return 'trend--stable';
|
|
813
|
+
default:
|
|
814
|
+
return 'trend--neutral';
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Calculate trend from change value
|
|
819
|
+
*/
|
|
820
|
+
calculateTrend(change) {
|
|
821
|
+
if (change === undefined || change === null) {
|
|
822
|
+
return 'neutral';
|
|
823
|
+
}
|
|
824
|
+
if (change > 0)
|
|
825
|
+
return 'up';
|
|
826
|
+
if (change < 0)
|
|
827
|
+
return 'down';
|
|
828
|
+
return 'stable';
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Format change value with sign
|
|
832
|
+
*/
|
|
833
|
+
formatChange(change) {
|
|
834
|
+
if (change === undefined || change === null) {
|
|
835
|
+
return '';
|
|
836
|
+
}
|
|
837
|
+
return `${change > 0 ? '+' : ''}${change}%`;
|
|
838
|
+
}
|
|
839
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SectionUtilsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
840
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SectionUtilsService, providedIn: 'root' }); }
|
|
841
|
+
}
|
|
842
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SectionUtilsService, decorators: [{
|
|
843
|
+
type: Injectable,
|
|
844
|
+
args: [{
|
|
845
|
+
providedIn: 'root'
|
|
846
|
+
}]
|
|
847
|
+
}] });
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Simple hash function for content hashing (replaces JSON.stringify)
|
|
851
|
+
* Uses MurmurHash-inspired algorithm for fast hashing
|
|
852
|
+
*/
|
|
853
|
+
function hashString(str) {
|
|
854
|
+
let hash = 0;
|
|
855
|
+
if (str.length === 0)
|
|
856
|
+
return hash;
|
|
857
|
+
for (let i = 0; i < str.length; i++) {
|
|
858
|
+
const char = str.charCodeAt(i);
|
|
859
|
+
hash = ((hash << 5) - hash) + char;
|
|
860
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
861
|
+
}
|
|
862
|
+
return hash;
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Generate content hash for a field (faster than JSON.stringify)
|
|
866
|
+
*/
|
|
867
|
+
function hashField(field) {
|
|
868
|
+
const key = `${field.id || ''}|${field.label || ''}|${field.value || ''}|${field.type || ''}|${field.title || ''}`;
|
|
869
|
+
return String(hashString(key));
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Generate content hash for an item (faster than JSON.stringify)
|
|
873
|
+
*/
|
|
874
|
+
function hashItem(item) {
|
|
875
|
+
const key = `${item.id || ''}|${item.title || ''}|${item.value || ''}`;
|
|
876
|
+
return String(hashString(key));
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* WeakMap cache for field hashes to avoid recomputation
|
|
880
|
+
*/
|
|
881
|
+
const fieldHashCache = new WeakMap();
|
|
882
|
+
const itemHashCache = new WeakMap();
|
|
883
|
+
/**
|
|
884
|
+
* Deep comparison utility for card objects
|
|
885
|
+
* Uses content hashing instead of JSON.stringify for better performance
|
|
886
|
+
*/
|
|
887
|
+
class CardDiffUtil {
|
|
888
|
+
/**
|
|
889
|
+
* Creates an updated card with only changed sections/fields updated
|
|
890
|
+
* Preserves references to unchanged sections for optimal performance
|
|
891
|
+
*/
|
|
892
|
+
static mergeCardUpdates(oldCard, newCard) {
|
|
893
|
+
// If cards are identical, return old card (preserve reference)
|
|
894
|
+
if (this.areCardsEqual(oldCard, newCard)) {
|
|
895
|
+
return { card: oldCard, changeType: 'content' };
|
|
896
|
+
}
|
|
897
|
+
// Check if only top-level properties changed (title, subtitle, etc.)
|
|
898
|
+
// Check if sections array changed
|
|
899
|
+
const sectionsChanged = !this.areSectionsEqual(oldCard.sections, newCard.sections);
|
|
900
|
+
// If only top-level changed, update only those
|
|
901
|
+
if (!sectionsChanged) {
|
|
902
|
+
return {
|
|
903
|
+
card: {
|
|
904
|
+
...oldCard,
|
|
905
|
+
cardTitle: newCard.cardTitle,
|
|
906
|
+
cardSubtitle: newCard.cardSubtitle,
|
|
907
|
+
cardType: newCard.cardType,
|
|
908
|
+
description: newCard.description,
|
|
909
|
+
columns: newCard.columns,
|
|
910
|
+
actions: newCard.actions,
|
|
911
|
+
// Keep same sections reference
|
|
912
|
+
sections: oldCard.sections
|
|
913
|
+
},
|
|
914
|
+
changeType: 'content'
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
// Merge sections incrementally
|
|
918
|
+
const mergedSections = this.mergeSections(oldCard.sections, newCard.sections);
|
|
919
|
+
const changeType = sectionsChanged && !this.didStructureChange(oldCard.sections, newCard.sections)
|
|
920
|
+
? 'content'
|
|
921
|
+
: 'structural';
|
|
922
|
+
return {
|
|
923
|
+
card: {
|
|
924
|
+
...oldCard,
|
|
925
|
+
cardTitle: newCard.cardTitle,
|
|
926
|
+
cardSubtitle: newCard.cardSubtitle,
|
|
927
|
+
cardType: newCard.cardType,
|
|
928
|
+
description: newCard.description,
|
|
929
|
+
columns: newCard.columns,
|
|
930
|
+
actions: newCard.actions,
|
|
931
|
+
sections: mergedSections
|
|
932
|
+
},
|
|
933
|
+
changeType
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
static didStructureChange(oldSections, newSections) {
|
|
937
|
+
if (oldSections.length !== newSections.length) {
|
|
938
|
+
return true;
|
|
939
|
+
}
|
|
940
|
+
return oldSections.some((oldSection, index) => {
|
|
941
|
+
const newSection = newSections[index];
|
|
942
|
+
if (!newSection) {
|
|
943
|
+
return true;
|
|
944
|
+
}
|
|
945
|
+
if ((oldSection.id || index) !== (newSection.id || index)) {
|
|
946
|
+
return true;
|
|
947
|
+
}
|
|
948
|
+
if (oldSection.type !== newSection.type) {
|
|
949
|
+
return true;
|
|
950
|
+
}
|
|
951
|
+
const oldFieldsLength = oldSection.fields?.length ?? 0;
|
|
952
|
+
const newFieldsLength = newSection.fields?.length ?? 0;
|
|
953
|
+
const oldItemsLength = oldSection.items?.length ?? 0;
|
|
954
|
+
const newItemsLength = newSection.items?.length ?? 0;
|
|
955
|
+
return oldFieldsLength !== newFieldsLength || newItemsLength !== oldItemsLength;
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Merges sections array, preserving references to unchanged sections
|
|
960
|
+
*/
|
|
961
|
+
static mergeSections(oldSections, newSections) {
|
|
962
|
+
// If sections array length changed, we need to rebuild
|
|
963
|
+
if (oldSections.length !== newSections.length) {
|
|
964
|
+
return newSections.map((section, index) => {
|
|
965
|
+
const oldSection = oldSections[index];
|
|
966
|
+
if (oldSection && this.areSectionsEqual([oldSection], [section])) {
|
|
967
|
+
return oldSection; // Preserve reference
|
|
968
|
+
}
|
|
969
|
+
return section;
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
// Merge each section
|
|
973
|
+
return newSections.map((newSection, index) => {
|
|
974
|
+
const oldSection = oldSections[index];
|
|
975
|
+
if (!oldSection) {
|
|
976
|
+
return newSection;
|
|
977
|
+
}
|
|
978
|
+
if ((oldSection.id || index) !== (newSection.id || index)) {
|
|
979
|
+
return newSection;
|
|
980
|
+
}
|
|
981
|
+
// Merge section fields/items
|
|
982
|
+
return this.mergeSection(oldSection, newSection);
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Merges a single section, preserving references to unchanged fields/items
|
|
987
|
+
*/
|
|
988
|
+
static mergeSection(oldSection, newSection) {
|
|
989
|
+
// Check if only top-level section properties changed
|
|
990
|
+
// Check if fields changed
|
|
991
|
+
const fieldsChanged = !this.areFieldsEqual(oldSection.fields, newSection.fields);
|
|
992
|
+
const itemsChanged = !this.areItemsEqual(oldSection.items, newSection.items);
|
|
993
|
+
// If only top-level changed, preserve fields/items references
|
|
994
|
+
if (!fieldsChanged && !itemsChanged) {
|
|
995
|
+
return {
|
|
996
|
+
...oldSection,
|
|
997
|
+
title: newSection.title,
|
|
998
|
+
type: newSection.type,
|
|
999
|
+
description: newSection.description,
|
|
1000
|
+
subtitle: newSection.subtitle,
|
|
1001
|
+
columns: newSection.columns,
|
|
1002
|
+
colSpan: newSection.colSpan,
|
|
1003
|
+
collapsed: newSection.collapsed,
|
|
1004
|
+
emoji: newSection.emoji,
|
|
1005
|
+
chartType: newSection.chartType,
|
|
1006
|
+
chartData: newSection.chartData,
|
|
1007
|
+
meta: newSection.meta,
|
|
1008
|
+
// Preserve fields/items references
|
|
1009
|
+
fields: oldSection.fields,
|
|
1010
|
+
items: oldSection.items
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
// Merge fields if they exist
|
|
1014
|
+
const mergedFields = oldSection.fields && newSection.fields
|
|
1015
|
+
? this.mergeFields(oldSection.fields, newSection.fields)
|
|
1016
|
+
: newSection.fields;
|
|
1017
|
+
// Merge items if they exist
|
|
1018
|
+
const mergedItems = oldSection.items && newSection.items
|
|
1019
|
+
? this.mergeItems(oldSection.items, newSection.items)
|
|
1020
|
+
: newSection.items;
|
|
1021
|
+
return {
|
|
1022
|
+
...oldSection,
|
|
1023
|
+
title: newSection.title,
|
|
1024
|
+
type: newSection.type,
|
|
1025
|
+
description: newSection.description,
|
|
1026
|
+
subtitle: newSection.subtitle,
|
|
1027
|
+
columns: newSection.columns,
|
|
1028
|
+
colSpan: newSection.colSpan,
|
|
1029
|
+
collapsed: newSection.collapsed,
|
|
1030
|
+
emoji: newSection.emoji,
|
|
1031
|
+
chartType: newSection.chartType,
|
|
1032
|
+
chartData: newSection.chartData,
|
|
1033
|
+
meta: newSection.meta,
|
|
1034
|
+
fields: mergedFields,
|
|
1035
|
+
items: mergedItems
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Merges fields array, preserving references to unchanged fields
|
|
1040
|
+
* Uses content hashing instead of JSON.stringify for better performance
|
|
1041
|
+
*/
|
|
1042
|
+
static mergeFields(oldFields, newFields) {
|
|
1043
|
+
if (oldFields.length !== newFields.length) {
|
|
1044
|
+
return newFields;
|
|
1045
|
+
}
|
|
1046
|
+
return newFields.map((newField, index) => {
|
|
1047
|
+
const oldField = oldFields[index];
|
|
1048
|
+
if (!oldField) {
|
|
1049
|
+
return newField;
|
|
1050
|
+
}
|
|
1051
|
+
// Fast comparison: check key properties first
|
|
1052
|
+
if (oldField.id === newField.id &&
|
|
1053
|
+
oldField.label === newField.label &&
|
|
1054
|
+
oldField.value === newField.value &&
|
|
1055
|
+
oldField.title === newField.title) {
|
|
1056
|
+
// Use content hashing instead of JSON.stringify
|
|
1057
|
+
const oldHash = fieldHashCache.get(oldField) || hashField(oldField);
|
|
1058
|
+
const newHash = hashField(newField);
|
|
1059
|
+
// Cache hashes for future comparisons
|
|
1060
|
+
if (!fieldHashCache.has(oldField)) {
|
|
1061
|
+
fieldHashCache.set(oldField, oldHash);
|
|
1062
|
+
}
|
|
1063
|
+
if (!fieldHashCache.has(newField)) {
|
|
1064
|
+
fieldHashCache.set(newField, newHash);
|
|
1065
|
+
}
|
|
1066
|
+
if (oldHash === newHash) {
|
|
1067
|
+
return oldField; // Preserve reference
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
return newField;
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Merges items array, preserving references to unchanged items
|
|
1075
|
+
* Uses content hashing instead of JSON.stringify for better performance
|
|
1076
|
+
*/
|
|
1077
|
+
static mergeItems(oldItems, newItems) {
|
|
1078
|
+
if (oldItems.length !== newItems.length) {
|
|
1079
|
+
return newItems;
|
|
1080
|
+
}
|
|
1081
|
+
return newItems.map((newItem, index) => {
|
|
1082
|
+
const oldItem = oldItems[index];
|
|
1083
|
+
if (!oldItem) {
|
|
1084
|
+
return newItem;
|
|
1085
|
+
}
|
|
1086
|
+
// Fast comparison
|
|
1087
|
+
if (oldItem.id === newItem.id &&
|
|
1088
|
+
oldItem.title === newItem.title &&
|
|
1089
|
+
oldItem.value === newItem.value) {
|
|
1090
|
+
// Use content hashing instead of JSON.stringify
|
|
1091
|
+
const oldHash = itemHashCache.get(oldItem) || hashItem(oldItem);
|
|
1092
|
+
const newHash = hashItem(newItem);
|
|
1093
|
+
// Cache hashes for future comparisons
|
|
1094
|
+
if (!itemHashCache.has(oldItem)) {
|
|
1095
|
+
itemHashCache.set(oldItem, oldHash);
|
|
1096
|
+
}
|
|
1097
|
+
if (!itemHashCache.has(newItem)) {
|
|
1098
|
+
itemHashCache.set(newItem, newHash);
|
|
1099
|
+
}
|
|
1100
|
+
if (oldHash === newHash) {
|
|
1101
|
+
return oldItem; // Preserve reference
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
return newItem;
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Fast equality check for cards
|
|
1109
|
+
*/
|
|
1110
|
+
static areCardsEqual(card1, card2) {
|
|
1111
|
+
return card1.id === card2.id &&
|
|
1112
|
+
card1.cardTitle === card2.cardTitle &&
|
|
1113
|
+
card1.cardSubtitle === card2.cardSubtitle &&
|
|
1114
|
+
card1.cardType === card2.cardType &&
|
|
1115
|
+
this.areSectionsEqual(card1.sections, card2.sections);
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Fast equality check for sections arrays
|
|
1119
|
+
*/
|
|
1120
|
+
static areSectionsEqual(sections1, sections2) {
|
|
1121
|
+
if (sections1.length !== sections2.length) {
|
|
1122
|
+
return false;
|
|
1123
|
+
}
|
|
1124
|
+
return sections1.every((section1, index) => {
|
|
1125
|
+
const section2 = sections2[index];
|
|
1126
|
+
if (!section2)
|
|
1127
|
+
return false;
|
|
1128
|
+
return section1.id === section2.id &&
|
|
1129
|
+
section1.title === section2.title &&
|
|
1130
|
+
section1.type === section2.type &&
|
|
1131
|
+
this.areFieldsEqual(section1.fields, section2.fields) &&
|
|
1132
|
+
this.areItemsEqual(section1.items, section2.items);
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* Fast equality check for fields arrays
|
|
1137
|
+
*/
|
|
1138
|
+
static areFieldsEqual(fields1, fields2) {
|
|
1139
|
+
if (!fields1 && !fields2)
|
|
1140
|
+
return true;
|
|
1141
|
+
if (!fields1 || !fields2)
|
|
1142
|
+
return false;
|
|
1143
|
+
if (fields1.length !== fields2.length)
|
|
1144
|
+
return false;
|
|
1145
|
+
return fields1.every((field1, index) => {
|
|
1146
|
+
const field2 = fields2[index];
|
|
1147
|
+
if (!field2)
|
|
1148
|
+
return false;
|
|
1149
|
+
return field1.id === field2.id &&
|
|
1150
|
+
field1.label === field2.label &&
|
|
1151
|
+
field1.value === field2.value &&
|
|
1152
|
+
field1.title === field2.title;
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Fast equality check for items arrays
|
|
1157
|
+
*/
|
|
1158
|
+
static areItemsEqual(items1, items2) {
|
|
1159
|
+
if (!items1 && !items2)
|
|
1160
|
+
return true;
|
|
1161
|
+
if (!items1 || !items2)
|
|
1162
|
+
return false;
|
|
1163
|
+
if (items1.length !== items2.length)
|
|
1164
|
+
return false;
|
|
1165
|
+
return items1.every((item1, index) => {
|
|
1166
|
+
const item2 = items2[index];
|
|
1167
|
+
if (!item2)
|
|
1168
|
+
return false;
|
|
1169
|
+
return item1.id === item2.id &&
|
|
1170
|
+
item1.title === item2.title &&
|
|
1171
|
+
item1.value === item2.value;
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function getBreakpointFromWidth(width) {
|
|
1177
|
+
if (width < 640)
|
|
1178
|
+
return 'xs';
|
|
1179
|
+
if (width < 768)
|
|
1180
|
+
return 'sm';
|
|
1181
|
+
if (width < 1024)
|
|
1182
|
+
return 'md';
|
|
1183
|
+
if (width < 1280)
|
|
1184
|
+
return 'lg';
|
|
1185
|
+
if (width < 1536)
|
|
1186
|
+
return 'xl';
|
|
1187
|
+
return '2xl';
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
const ICONS = {
|
|
1191
|
+
Activity,
|
|
1192
|
+
AlertCircle,
|
|
1193
|
+
ArrowRight,
|
|
1194
|
+
ArrowDown,
|
|
1195
|
+
ArrowUp,
|
|
1196
|
+
Award,
|
|
1197
|
+
Box,
|
|
1198
|
+
BarChart3,
|
|
1199
|
+
BookOpen,
|
|
1200
|
+
Briefcase,
|
|
1201
|
+
Building,
|
|
1202
|
+
Calendar,
|
|
1203
|
+
CalendarCheck,
|
|
1204
|
+
CalendarPlus,
|
|
1205
|
+
CalendarX,
|
|
1206
|
+
CheckCircle2,
|
|
1207
|
+
Check,
|
|
1208
|
+
ChevronRight,
|
|
1209
|
+
Circle,
|
|
1210
|
+
Clock,
|
|
1211
|
+
Code2,
|
|
1212
|
+
Calculator,
|
|
1213
|
+
ExternalLink,
|
|
1214
|
+
Facebook,
|
|
1215
|
+
DollarSign,
|
|
1216
|
+
Download,
|
|
1217
|
+
FileText,
|
|
1218
|
+
Folder,
|
|
1219
|
+
Globe,
|
|
1220
|
+
GitBranch,
|
|
1221
|
+
Grid,
|
|
1222
|
+
Handshake,
|
|
1223
|
+
Hash,
|
|
1224
|
+
HelpCircle,
|
|
1225
|
+
Info,
|
|
1226
|
+
Instagram,
|
|
1227
|
+
Linkedin,
|
|
1228
|
+
Lightbulb,
|
|
1229
|
+
List,
|
|
1230
|
+
Mail,
|
|
1231
|
+
MapPin,
|
|
1232
|
+
Maximize2,
|
|
1233
|
+
MessageCircle,
|
|
1234
|
+
Minimize2,
|
|
1235
|
+
Minus,
|
|
1236
|
+
Package,
|
|
1237
|
+
Phone,
|
|
1238
|
+
PieChart,
|
|
1239
|
+
Quote,
|
|
1240
|
+
RefreshCw,
|
|
1241
|
+
Share2,
|
|
1242
|
+
ShoppingCart,
|
|
1243
|
+
Save,
|
|
1244
|
+
Settings,
|
|
1245
|
+
Shield,
|
|
1246
|
+
Sparkles,
|
|
1247
|
+
Tag,
|
|
1248
|
+
Star,
|
|
1249
|
+
Target,
|
|
1250
|
+
Timer,
|
|
1251
|
+
TrendingDown,
|
|
1252
|
+
TrendingUp,
|
|
1253
|
+
Trophy,
|
|
1254
|
+
Twitter,
|
|
1255
|
+
Users,
|
|
1256
|
+
UserCheck,
|
|
1257
|
+
User,
|
|
1258
|
+
Video,
|
|
1259
|
+
Type,
|
|
1260
|
+
Wrench,
|
|
1261
|
+
XCircle,
|
|
1262
|
+
Zap
|
|
1263
|
+
};
|
|
1264
|
+
class LucideIconsModule {
|
|
1265
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: LucideIconsModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
1266
|
+
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "17.3.12", ngImport: i0, type: LucideIconsModule, imports: [i2.LucideAngularModule], exports: [LucideAngularModule] }); }
|
|
1267
|
+
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: LucideIconsModule, imports: [LucideAngularModule.pick(ICONS), LucideAngularModule] }); }
|
|
1268
|
+
}
|
|
1269
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", 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;
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Get fields from section (standardized access pattern)
|
|
1305
|
+
*/
|
|
1306
|
+
getFields() {
|
|
1307
|
+
return this.section.fields ?? [];
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Get items from section (standardized access pattern)
|
|
1311
|
+
* Falls back to fields if items are not available
|
|
1312
|
+
*/
|
|
1313
|
+
getItems() {
|
|
1314
|
+
if (Array.isArray(this.section.items) && this.section.items.length > 0) {
|
|
1315
|
+
return this.section.items;
|
|
1316
|
+
}
|
|
1317
|
+
// Fallback to fields if items are not available
|
|
1318
|
+
if (Array.isArray(this.section.fields) && this.section.fields.length > 0) {
|
|
1319
|
+
return this.section.fields.map((field) => {
|
|
1320
|
+
const cardField = field;
|
|
1321
|
+
return {
|
|
1322
|
+
...cardField,
|
|
1323
|
+
title: cardField.title ?? cardField.label ?? cardField.id,
|
|
1324
|
+
description: cardField.description ?? (typeof cardField.meta?.['description'] === 'string'
|
|
1325
|
+
? cardField.meta['description']
|
|
1326
|
+
: undefined)
|
|
1327
|
+
};
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
return [];
|
|
1331
|
+
}
|
|
1332
|
+
ngOnChanges(changes) {
|
|
1333
|
+
if (changes['section']) {
|
|
1334
|
+
// Cancel pending RAFs
|
|
1335
|
+
if (this.fieldAnimationUpdateRafId !== null) {
|
|
1336
|
+
cancelAnimationFrame(this.fieldAnimationUpdateRafId);
|
|
1337
|
+
this.fieldAnimationUpdateRafId = null;
|
|
1338
|
+
}
|
|
1339
|
+
if (this.itemAnimationUpdateRafId !== null) {
|
|
1340
|
+
cancelAnimationFrame(this.itemAnimationUpdateRafId);
|
|
1341
|
+
this.itemAnimationUpdateRafId = null;
|
|
1342
|
+
}
|
|
1343
|
+
// Reset animation states when section changes
|
|
1344
|
+
this.resetFieldAnimations();
|
|
1345
|
+
this.resetItemAnimations();
|
|
1346
|
+
this.fieldsAnimated = false;
|
|
1347
|
+
this.itemsAnimated = false;
|
|
1348
|
+
// Clear pending updates
|
|
1349
|
+
this.pendingFieldAnimationUpdates.clear();
|
|
1350
|
+
this.pendingItemAnimationUpdates.clear();
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Get animation class for a field based on its appearance state
|
|
1355
|
+
*/
|
|
1356
|
+
getFieldAnimationClass(fieldId, index) {
|
|
1357
|
+
const state = this.fieldAnimationStates.get(fieldId);
|
|
1358
|
+
if (state === 'entering') {
|
|
1359
|
+
return 'field-streaming';
|
|
1360
|
+
}
|
|
1361
|
+
if (state === 'entered') {
|
|
1362
|
+
return 'field-entered';
|
|
1363
|
+
}
|
|
1364
|
+
// New field - mark as entering
|
|
1365
|
+
if (state === undefined || state === 'none') {
|
|
1366
|
+
this.markFieldEntering(fieldId, index);
|
|
1367
|
+
return 'field-streaming';
|
|
1368
|
+
}
|
|
1369
|
+
return '';
|
|
1370
|
+
}
|
|
1371
|
+
/**
|
|
1372
|
+
* Get animation class for an item based on its appearance state
|
|
1373
|
+
*/
|
|
1374
|
+
getItemAnimationClass(itemId, index) {
|
|
1375
|
+
const state = this.itemAnimationStates.get(itemId);
|
|
1376
|
+
if (state === 'entering') {
|
|
1377
|
+
return 'item-streaming';
|
|
1378
|
+
}
|
|
1379
|
+
if (state === 'entered') {
|
|
1380
|
+
return 'item-entered';
|
|
1381
|
+
}
|
|
1382
|
+
// New item - mark as entering
|
|
1383
|
+
if (state === undefined || state === 'none') {
|
|
1384
|
+
this.markItemEntering(itemId, index);
|
|
1385
|
+
return 'item-streaming';
|
|
1386
|
+
}
|
|
1387
|
+
return '';
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Get stagger delay index for field animation
|
|
1391
|
+
*/
|
|
1392
|
+
getFieldStaggerIndex(index) {
|
|
1393
|
+
return Math.min(index, 15);
|
|
1394
|
+
}
|
|
1395
|
+
/**
|
|
1396
|
+
* Get stagger delay index for item animation
|
|
1397
|
+
*/
|
|
1398
|
+
getItemStaggerIndex(index) {
|
|
1399
|
+
return Math.min(index, 15);
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Mark field as entering and schedule entered state
|
|
1403
|
+
* Optimized: Batches change detection for better performance
|
|
1404
|
+
*/
|
|
1405
|
+
markFieldEntering(fieldId, index) {
|
|
1406
|
+
this.fieldAnimationStates.set(fieldId, 'entering');
|
|
1407
|
+
const appearanceTime = Date.now();
|
|
1408
|
+
this.fieldAnimationTimes.set(fieldId, appearanceTime);
|
|
1409
|
+
// Calculate total delay (stagger + animation duration)
|
|
1410
|
+
const staggerDelay = index * this.FIELD_STAGGER_DELAY_MS;
|
|
1411
|
+
const totalDelay = staggerDelay + this.FIELD_ANIMATION_DURATION_MS;
|
|
1412
|
+
// Mark as entered after animation completes
|
|
1413
|
+
// Batch change detection for multiple fields
|
|
1414
|
+
setTimeout(() => {
|
|
1415
|
+
// Only update if this is still the latest appearance
|
|
1416
|
+
if (this.fieldAnimationTimes.get(fieldId) === appearanceTime) {
|
|
1417
|
+
this.fieldAnimationStates.set(fieldId, 'entered');
|
|
1418
|
+
// Batch change detection - add to pending updates
|
|
1419
|
+
this.pendingFieldAnimationUpdates.add(fieldId);
|
|
1420
|
+
this.scheduleBatchedFieldChangeDetection();
|
|
1421
|
+
}
|
|
1422
|
+
}, totalDelay);
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Batch change detection for field animation state updates
|
|
1426
|
+
*/
|
|
1427
|
+
scheduleBatchedFieldChangeDetection() {
|
|
1428
|
+
if (this.fieldAnimationUpdateRafId !== null) {
|
|
1429
|
+
return; // Already scheduled
|
|
1430
|
+
}
|
|
1431
|
+
this.fieldAnimationUpdateRafId = requestAnimationFrame(() => {
|
|
1432
|
+
if (this.pendingFieldAnimationUpdates.size > 0) {
|
|
1433
|
+
// Single change detection for all pending updates
|
|
1434
|
+
this.cdr.markForCheck();
|
|
1435
|
+
this.pendingFieldAnimationUpdates.clear();
|
|
1436
|
+
}
|
|
1437
|
+
this.fieldAnimationUpdateRafId = null;
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
/**
|
|
1441
|
+
* Mark item as entering and schedule entered state
|
|
1442
|
+
* Optimized: Batches change detection for better performance
|
|
1443
|
+
*/
|
|
1444
|
+
markItemEntering(itemId, index) {
|
|
1445
|
+
this.itemAnimationStates.set(itemId, 'entering');
|
|
1446
|
+
const appearanceTime = Date.now();
|
|
1447
|
+
this.itemAnimationTimes.set(itemId, appearanceTime);
|
|
1448
|
+
// Calculate total delay (stagger + animation duration)
|
|
1449
|
+
const staggerDelay = index * this.ITEM_STAGGER_DELAY_MS;
|
|
1450
|
+
const totalDelay = staggerDelay + this.ITEM_ANIMATION_DURATION_MS;
|
|
1451
|
+
// Mark as entered after animation completes
|
|
1452
|
+
// Batch change detection for multiple items
|
|
1453
|
+
setTimeout(() => {
|
|
1454
|
+
// Only update if this is still the latest appearance
|
|
1455
|
+
if (this.itemAnimationTimes.get(itemId) === appearanceTime) {
|
|
1456
|
+
this.itemAnimationStates.set(itemId, 'entered');
|
|
1457
|
+
// Batch change detection - add to pending updates
|
|
1458
|
+
this.pendingItemAnimationUpdates.add(itemId);
|
|
1459
|
+
this.scheduleBatchedItemChangeDetection();
|
|
1460
|
+
}
|
|
1461
|
+
}, totalDelay);
|
|
1462
|
+
}
|
|
1463
|
+
/**
|
|
1464
|
+
* Batch change detection for item animation state updates
|
|
1465
|
+
*/
|
|
1466
|
+
scheduleBatchedItemChangeDetection() {
|
|
1467
|
+
if (this.itemAnimationUpdateRafId !== null) {
|
|
1468
|
+
return; // Already scheduled
|
|
1469
|
+
}
|
|
1470
|
+
this.itemAnimationUpdateRafId = requestAnimationFrame(() => {
|
|
1471
|
+
if (this.pendingItemAnimationUpdates.size > 0) {
|
|
1472
|
+
// Single change detection for all pending updates
|
|
1473
|
+
this.cdr.markForCheck();
|
|
1474
|
+
this.pendingItemAnimationUpdates.clear();
|
|
1475
|
+
}
|
|
1476
|
+
this.itemAnimationUpdateRafId = null;
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Reset field animation states
|
|
1481
|
+
*/
|
|
1482
|
+
resetFieldAnimations() {
|
|
1483
|
+
this.fieldAnimationStates.clear();
|
|
1484
|
+
this.fieldAnimationTimes.clear();
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Reset item animation states
|
|
1488
|
+
*/
|
|
1489
|
+
resetItemAnimations() {
|
|
1490
|
+
this.itemAnimationStates.clear();
|
|
1491
|
+
this.itemAnimationTimes.clear();
|
|
1492
|
+
}
|
|
1493
|
+
/**
|
|
1494
|
+
* Get field ID for tracking
|
|
1495
|
+
*/
|
|
1496
|
+
getFieldId(field, index) {
|
|
1497
|
+
return field.id || `field-${index}-${field.label || ''}`;
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Get item ID for tracking
|
|
1501
|
+
*/
|
|
1502
|
+
getItemId(item, index) {
|
|
1503
|
+
return item.id || `item-${index}-${item.title || ''}`;
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Check if section has fields
|
|
1507
|
+
* Public getter for template access
|
|
1508
|
+
*/
|
|
1509
|
+
get hasFields() {
|
|
1510
|
+
return this.getFields().length > 0;
|
|
1511
|
+
}
|
|
1512
|
+
/**
|
|
1513
|
+
* Check if section has items
|
|
1514
|
+
* Public getter for template access
|
|
1515
|
+
*/
|
|
1516
|
+
get hasItems() {
|
|
1517
|
+
return this.getItems().length > 0;
|
|
1518
|
+
}
|
|
1519
|
+
/**
|
|
1520
|
+
* Emit field interaction event (standardized pattern)
|
|
1521
|
+
*/
|
|
1522
|
+
emitFieldInteraction(field, metadata) {
|
|
1523
|
+
this.fieldInteraction.emit({
|
|
1524
|
+
field,
|
|
1525
|
+
metadata: {
|
|
1526
|
+
sectionId: this.section.id,
|
|
1527
|
+
sectionTitle: this.section.title,
|
|
1528
|
+
...metadata
|
|
1529
|
+
}
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
/**
|
|
1533
|
+
* Emit item interaction event (standardized pattern)
|
|
1534
|
+
*/
|
|
1535
|
+
emitItemInteraction(item, metadata) {
|
|
1536
|
+
this.itemInteraction.emit({
|
|
1537
|
+
item,
|
|
1538
|
+
metadata: {
|
|
1539
|
+
sectionId: this.section.id,
|
|
1540
|
+
sectionTitle: this.section.title,
|
|
1541
|
+
...metadata
|
|
1542
|
+
}
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
/**
|
|
1546
|
+
* Phase 5: Perfect trackBy function for fields - uses stable field ID
|
|
1547
|
+
* Can be overridden by child classes for custom tracking
|
|
1548
|
+
*/
|
|
1549
|
+
trackField(index, field) {
|
|
1550
|
+
return field.id || `field-${index}-${field.label || ''}`;
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* Phase 5: Perfect trackBy function for items - uses stable item ID
|
|
1554
|
+
* Can be overridden by child classes for custom tracking
|
|
1555
|
+
*/
|
|
1556
|
+
trackItem(index, item) {
|
|
1557
|
+
return item.id || `item-${index}-${item.title || ''}`;
|
|
1558
|
+
}
|
|
1559
|
+
// Display methods removed - each component now implements its own to avoid TypeScript override conflicts
|
|
1560
|
+
// The logic is consistent: filter out "Streaming…" placeholder text
|
|
1561
|
+
/**
|
|
1562
|
+
* Safe value accessor - extracts value from field with fallback options
|
|
1563
|
+
* Handles field.value, field.text, field.quote based on field type
|
|
1564
|
+
*/
|
|
1565
|
+
getFieldValue(field) {
|
|
1566
|
+
// Try value first (most common)
|
|
1567
|
+
if (field.value !== undefined && field.value !== null) {
|
|
1568
|
+
return field.value;
|
|
1569
|
+
}
|
|
1570
|
+
// Try text (for text-reference fields)
|
|
1571
|
+
if ('text' in field && field.text !== undefined && field.text !== null) {
|
|
1572
|
+
return field.text;
|
|
1573
|
+
}
|
|
1574
|
+
// Try quote (for quotation fields)
|
|
1575
|
+
if ('quote' in field && field.quote !== undefined && field.quote !== null) {
|
|
1576
|
+
return field.quote;
|
|
1577
|
+
}
|
|
1578
|
+
return undefined;
|
|
1579
|
+
}
|
|
1580
|
+
/**
|
|
1581
|
+
* Safe metadata accessor - extracts metadata value safely
|
|
1582
|
+
*/
|
|
1583
|
+
getMetaValue(field, key) {
|
|
1584
|
+
return field.meta?.[key];
|
|
1585
|
+
}
|
|
1586
|
+
/**
|
|
1587
|
+
* Check if a value represents streaming placeholder
|
|
1588
|
+
*/
|
|
1589
|
+
isStreamingPlaceholder(value) {
|
|
1590
|
+
return value === 'Streaming…' || value === 'Streaming...';
|
|
1591
|
+
}
|
|
1592
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: BaseSectionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1593
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: BaseSectionComponent, selector: "ng-component", inputs: { section: "section" }, outputs: { fieldInteraction: "fieldInteraction", itemInteraction: "itemInteraction" }, usesOnChanges: true, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1594
|
+
}
|
|
1595
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: BaseSectionComponent, decorators: [{
|
|
1596
|
+
type: Component,
|
|
1597
|
+
args: [{
|
|
1598
|
+
template: '',
|
|
1599
|
+
changeDetection: ChangeDetectionStrategy.OnPush
|
|
1600
|
+
}]
|
|
1601
|
+
}], propDecorators: { section: [{
|
|
1602
|
+
type: Input,
|
|
1603
|
+
args: [{ required: true }]
|
|
1604
|
+
}], fieldInteraction: [{
|
|
1605
|
+
type: Output
|
|
1606
|
+
}], itemInteraction: [{
|
|
1607
|
+
type: Output
|
|
1608
|
+
}] } });
|
|
1609
|
+
|
|
1610
|
+
class InfoSectionComponent extends BaseSectionComponent {
|
|
1611
|
+
constructor() {
|
|
1612
|
+
super(...arguments);
|
|
1613
|
+
this.utils = inject(SectionUtilsService);
|
|
1614
|
+
// Custom output for backward compatibility with InfoSectionFieldInteraction format
|
|
1615
|
+
this.infoFieldInteraction = new EventEmitter();
|
|
1616
|
+
}
|
|
1617
|
+
get fields() {
|
|
1618
|
+
return super.getFields();
|
|
1619
|
+
}
|
|
1620
|
+
get hasFields() {
|
|
1621
|
+
return super.hasFields;
|
|
1622
|
+
}
|
|
1623
|
+
onFieldClick(field) {
|
|
1624
|
+
// Emit to base class for standard handling
|
|
1625
|
+
this.emitFieldInteraction(field, { sectionTitle: this.section.title });
|
|
1626
|
+
// Also emit in InfoSectionFieldInteraction format for backward compatibility
|
|
1627
|
+
this.infoFieldInteraction.emit({
|
|
1628
|
+
field,
|
|
1629
|
+
sectionTitle: this.section.title
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
getTrendIcon(field) {
|
|
1633
|
+
const icon = this.utils.getTrendIcon(field.trend ?? this.utils.calculateTrend(field.change));
|
|
1634
|
+
// Return null for neutral/default to hide icon
|
|
1635
|
+
return icon === 'bar-chart-3' ? null : icon;
|
|
1636
|
+
}
|
|
1637
|
+
getTrendClass(field) {
|
|
1638
|
+
return this.utils.getTrendClass(field.trend ?? field.change);
|
|
1639
|
+
}
|
|
1640
|
+
getTrendIconClass(field) {
|
|
1641
|
+
return this.getTrendClass(field);
|
|
1642
|
+
}
|
|
1643
|
+
formatChange(change) {
|
|
1644
|
+
return this.utils.formatChange(change);
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* Get display value, hiding "Streaming…" placeholder text
|
|
1648
|
+
* Inline implementation to avoid TypeScript override conflicts
|
|
1649
|
+
*/
|
|
1650
|
+
getDisplayValue(field) {
|
|
1651
|
+
const value = field.value;
|
|
1652
|
+
if (value === 'Streaming…' || value === 'Streaming...') {
|
|
1653
|
+
return '';
|
|
1654
|
+
}
|
|
1655
|
+
return value != null ? String(value) : '';
|
|
1656
|
+
}
|
|
1657
|
+
trackField(index, field) {
|
|
1658
|
+
return field.id ?? `${field.label}-${index}`;
|
|
1659
|
+
}
|
|
1660
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: InfoSectionComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
1661
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: InfoSectionComponent, isStandalone: true, selector: "app-info-section", outputs: { infoFieldInteraction: "infoFieldInteraction" }, usesInheritance: true, ngImport: i0, template: "<div class=\"ai-section ai-section--info\">\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 infoEmpty\">\n <div class=\"info-matrix\">\n <button\n *ngFor=\"let field of fields; trackBy: trackField; let idx = index\"\n type=\"button\"\n class=\"info-row\"\n [class.field-streaming]=\"getFieldAnimationClass(getFieldId(field, idx), idx) === 'field-streaming'\"\n [class.field-entered]=\"getFieldAnimationClass(getFieldId(field, idx), idx) === 'field-entered'\"\n [class.field-stagger-0]=\"getFieldStaggerIndex(idx) === 0\"\n [class.field-stagger-1]=\"getFieldStaggerIndex(idx) === 1\"\n [class.field-stagger-2]=\"getFieldStaggerIndex(idx) === 2\"\n [class.field-stagger-3]=\"getFieldStaggerIndex(idx) === 3\"\n [class.field-stagger-4]=\"getFieldStaggerIndex(idx) === 4\"\n [class.field-stagger-5]=\"getFieldStaggerIndex(idx) === 5\"\n [class.field-stagger-6]=\"getFieldStaggerIndex(idx) === 6\"\n [class.field-stagger-7]=\"getFieldStaggerIndex(idx) === 7\"\n [class.field-stagger-8]=\"getFieldStaggerIndex(idx) === 8\"\n [class.field-stagger-9]=\"getFieldStaggerIndex(idx) === 9\"\n [class.field-stagger-10]=\"getFieldStaggerIndex(idx) === 10\"\n [class.field-stagger-11]=\"getFieldStaggerIndex(idx) === 11\"\n [class.field-stagger-12]=\"getFieldStaggerIndex(idx) === 12\"\n [class.field-stagger-13]=\"getFieldStaggerIndex(idx) === 13\"\n [class.field-stagger-14]=\"getFieldStaggerIndex(idx) === 14\"\n [class.field-stagger-15]=\"getFieldStaggerIndex(idx) === 15\"\n (click)=\"onFieldClick(field)\"\n (keydown.enter)=\"onFieldClick(field)\"\n (keydown.space)=\"$event.preventDefault(); onFieldClick(field)\"\n >\n <div class=\"info-row__primary\">\n <span class=\"info-row__label\">{{ field.label }}</span>\n <span class=\"info-row__value\">{{ getDisplayValue(field) }}</span>\n </div>\n\n <div class=\"info-row__meta\" *ngIf=\"field.description || field.change !== undefined\">\n <p class=\"info-row__description\" *ngIf=\"field.description\">\n {{ field.description }}\n </p>\n <span class=\"info-row__change\" *ngIf=\"field.change !== undefined\" [ngClass]=\"getTrendClass(field)\">\n <lucide-icon\n *ngIf=\"getTrendIcon(field) as trendIcon\"\n [name]=\"trendIcon\"\n [size]=\"12\"\n aria-hidden=\"true\"\n ></lucide-icon>\n {{ formatChange(field.change) }}\n </span>\n </div>\n </button>\n </div>\n </ng-container>\n\n <ng-template #infoEmpty>\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 information available</p>\n </div>\n </ng-template>\n </div>\n</div>\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 }); }
|
|
1662
|
+
}
|
|
1663
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: InfoSectionComponent, decorators: [{
|
|
1664
|
+
type: Component,
|
|
1665
|
+
args: [{ selector: 'app-info-section', standalone: true, imports: [CommonModule, LucideIconsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ai-section ai-section--info\">\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 infoEmpty\">\n <div class=\"info-matrix\">\n <button\n *ngFor=\"let field of fields; trackBy: trackField; let idx = index\"\n type=\"button\"\n class=\"info-row\"\n [class.field-streaming]=\"getFieldAnimationClass(getFieldId(field, idx), idx) === 'field-streaming'\"\n [class.field-entered]=\"getFieldAnimationClass(getFieldId(field, idx), idx) === 'field-entered'\"\n [class.field-stagger-0]=\"getFieldStaggerIndex(idx) === 0\"\n [class.field-stagger-1]=\"getFieldStaggerIndex(idx) === 1\"\n [class.field-stagger-2]=\"getFieldStaggerIndex(idx) === 2\"\n [class.field-stagger-3]=\"getFieldStaggerIndex(idx) === 3\"\n [class.field-stagger-4]=\"getFieldStaggerIndex(idx) === 4\"\n [class.field-stagger-5]=\"getFieldStaggerIndex(idx) === 5\"\n [class.field-stagger-6]=\"getFieldStaggerIndex(idx) === 6\"\n [class.field-stagger-7]=\"getFieldStaggerIndex(idx) === 7\"\n [class.field-stagger-8]=\"getFieldStaggerIndex(idx) === 8\"\n [class.field-stagger-9]=\"getFieldStaggerIndex(idx) === 9\"\n [class.field-stagger-10]=\"getFieldStaggerIndex(idx) === 10\"\n [class.field-stagger-11]=\"getFieldStaggerIndex(idx) === 11\"\n [class.field-stagger-12]=\"getFieldStaggerIndex(idx) === 12\"\n [class.field-stagger-13]=\"getFieldStaggerIndex(idx) === 13\"\n [class.field-stagger-14]=\"getFieldStaggerIndex(idx) === 14\"\n [class.field-stagger-15]=\"getFieldStaggerIndex(idx) === 15\"\n (click)=\"onFieldClick(field)\"\n (keydown.enter)=\"onFieldClick(field)\"\n (keydown.space)=\"$event.preventDefault(); onFieldClick(field)\"\n >\n <div class=\"info-row__primary\">\n <span class=\"info-row__label\">{{ field.label }}</span>\n <span class=\"info-row__value\">{{ getDisplayValue(field) }}</span>\n </div>\n\n <div class=\"info-row__meta\" *ngIf=\"field.description || field.change !== undefined\">\n <p class=\"info-row__description\" *ngIf=\"field.description\">\n {{ field.description }}\n </p>\n <span class=\"info-row__change\" *ngIf=\"field.change !== undefined\" [ngClass]=\"getTrendClass(field)\">\n <lucide-icon\n *ngIf=\"getTrendIcon(field) as trendIcon\"\n [name]=\"trendIcon\"\n [size]=\"12\"\n aria-hidden=\"true\"\n ></lucide-icon>\n {{ formatChange(field.change) }}\n </span>\n </div>\n </button>\n </div>\n </ng-container>\n\n <ng-template #infoEmpty>\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 information available</p>\n </div>\n </ng-template>\n </div>\n</div>\n" }]
|
|
1666
|
+
}], propDecorators: { infoFieldInteraction: [{
|
|
1667
|
+
type: Output
|
|
1668
|
+
}] } });
|
|
1669
|
+
|
|
1670
|
+
class AnalyticsSectionComponent extends BaseSectionComponent {
|
|
1671
|
+
constructor() {
|
|
1672
|
+
super(...arguments);
|
|
1673
|
+
this.Math = Math;
|
|
1674
|
+
this.utils = inject(SectionUtilsService);
|
|
1675
|
+
}
|
|
1676
|
+
get fields() {
|
|
1677
|
+
return this.getFields();
|
|
1678
|
+
}
|
|
1679
|
+
onFieldClick(field) {
|
|
1680
|
+
this.emitFieldInteraction(field);
|
|
1681
|
+
}
|
|
1682
|
+
getTrendIcon(field) {
|
|
1683
|
+
return this.utils.getTrendIcon(field.trend ?? this.utils.calculateTrend(field.change));
|
|
1684
|
+
}
|
|
1685
|
+
getTrendClass(field) {
|
|
1686
|
+
return this.utils.getTrendClass(field.trend ?? field.change);
|
|
1687
|
+
}
|
|
1688
|
+
formatChange(change) {
|
|
1689
|
+
return this.utils.formatChange(change);
|
|
1690
|
+
}
|
|
1691
|
+
/**
|
|
1692
|
+
* Get display value, hiding "Streaming…" placeholder text
|
|
1693
|
+
* Inline implementation to avoid TypeScript override conflicts
|
|
1694
|
+
*/
|
|
1695
|
+
getDisplayValue(field) {
|
|
1696
|
+
const value = field.value;
|
|
1697
|
+
if (value === 'Streaming…' || value === 'Streaming...') {
|
|
1698
|
+
return '';
|
|
1699
|
+
}
|
|
1700
|
+
return value != null ? String(value) : '';
|
|
1701
|
+
}
|
|
1702
|
+
/**
|
|
1703
|
+
* Check if percentage should be shown in meta
|
|
1704
|
+
* Only show if percentage exists and is not already included in the value
|
|
1705
|
+
*/
|
|
1706
|
+
shouldShowPercentage(field) {
|
|
1707
|
+
if (field.percentage === undefined) {
|
|
1708
|
+
return false;
|
|
1709
|
+
}
|
|
1710
|
+
const value = this.getDisplayValue(field);
|
|
1711
|
+
if (!value) {
|
|
1712
|
+
return false;
|
|
1713
|
+
}
|
|
1714
|
+
// Check if the value already contains the percentage
|
|
1715
|
+
// Remove % signs and compare numbers
|
|
1716
|
+
const valueWithoutPercent = value.replace(/%/g, '').trim();
|
|
1717
|
+
const percentageStr = String(field.percentage);
|
|
1718
|
+
// If value already contains the percentage number, don't show it again
|
|
1719
|
+
return !valueWithoutPercent.includes(percentageStr);
|
|
1720
|
+
}
|
|
1721
|
+
trackField(index, field) {
|
|
1722
|
+
return field.id ?? `${field.label}-${index}`;
|
|
1723
|
+
}
|
|
1724
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AnalyticsSectionComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
1725
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: AnalyticsSectionComponent, isStandalone: true, selector: "app-analytics-section", usesInheritance: true, ngImport: i0, template: "<div class=\"ai-section ai-section--analytics\">\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=\"fields.length; else analyticsEmpty\">\n <div class=\"section-grid section-grid--metrics\">\n <article\n *ngFor=\"let field of fields; trackBy: trackField\"\n class=\"section-card section-card--metric\"\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=\"section-card__label\">\n <span>{{ field.label }}</span>\n </div>\n\n <div class=\"section-card__value\">\n <span>{{ getDisplayValue(field) }}</span>\n <lucide-icon\n *ngIf=\"getTrendIcon(field)\"\n [name]=\"getTrendIcon(field)\"\n [size]=\"18\"\n [ngClass]=\"getTrendClass(field)\"\n ></lucide-icon>\n </div>\n\n <div *ngIf=\"field.percentage !== undefined\" class=\"section-card__progress\">\n <div\n class=\"section-card__progress-bar\"\n [style.width.%]=\"Math.min(field.percentage, 100)\"\n ></div>\n </div>\n\n <div *ngIf=\"shouldShowPercentage(field)\" class=\"section-card__meta\">\n <span>{{ field.percentage }}%</span>\n </div>\n\n <div *ngIf=\"field.change !== undefined\" class=\"section-card__meta\">\n <span>Change:</span>\n <span class=\"section-card__meta-change\" [ngClass]=\"getTrendClass(field)\">\n {{ formatChange(field.change) }}\n </span>\n <lucide-icon [name]=\"getTrendIcon(field)\" [size]=\"14\" [ngClass]=\"getTrendClass(field)\"></lucide-icon>\n </div>\n </article>\n </div>\n </ng-container>\n\n <ng-template #analyticsEmpty>\n <div class=\"section-empty\">\n <lucide-icon name=\"bar-chart-3\" [size]=\"32\" class=\"mb-4 opacity-50\"></lucide-icon>\n <p class=\"text-sm\">No analytics data configured</p>\n </div>\n </ng-template>\n </div>\n</div>\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 }); }
|
|
1726
|
+
}
|
|
1727
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AnalyticsSectionComponent, decorators: [{
|
|
1728
|
+
type: Component,
|
|
1729
|
+
args: [{ selector: 'app-analytics-section', standalone: true, imports: [CommonModule, LucideIconsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ai-section ai-section--analytics\">\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=\"fields.length; else analyticsEmpty\">\n <div class=\"section-grid section-grid--metrics\">\n <article\n *ngFor=\"let field of fields; trackBy: trackField\"\n class=\"section-card section-card--metric\"\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=\"section-card__label\">\n <span>{{ field.label }}</span>\n </div>\n\n <div class=\"section-card__value\">\n <span>{{ getDisplayValue(field) }}</span>\n <lucide-icon\n *ngIf=\"getTrendIcon(field)\"\n [name]=\"getTrendIcon(field)\"\n [size]=\"18\"\n [ngClass]=\"getTrendClass(field)\"\n ></lucide-icon>\n </div>\n\n <div *ngIf=\"field.percentage !== undefined\" class=\"section-card__progress\">\n <div\n class=\"section-card__progress-bar\"\n [style.width.%]=\"Math.min(field.percentage, 100)\"\n ></div>\n </div>\n\n <div *ngIf=\"shouldShowPercentage(field)\" class=\"section-card__meta\">\n <span>{{ field.percentage }}%</span>\n </div>\n\n <div *ngIf=\"field.change !== undefined\" class=\"section-card__meta\">\n <span>Change:</span>\n <span class=\"section-card__meta-change\" [ngClass]=\"getTrendClass(field)\">\n {{ formatChange(field.change) }}\n </span>\n <lucide-icon [name]=\"getTrendIcon(field)\" [size]=\"14\" [ngClass]=\"getTrendClass(field)\"></lucide-icon>\n </div>\n </article>\n </div>\n </ng-container>\n\n <ng-template #analyticsEmpty>\n <div class=\"section-empty\">\n <lucide-icon name=\"bar-chart-3\" [size]=\"32\" class=\"mb-4 opacity-50\"></lucide-icon>\n <p class=\"text-sm\">No analytics data configured</p>\n </div>\n </ng-template>\n </div>\n</div>\n" }]
|
|
1730
|
+
}] });
|
|
1731
|
+
|
|
1732
|
+
class FinancialsSectionComponent extends BaseSectionComponent {
|
|
1733
|
+
constructor() {
|
|
1734
|
+
super(...arguments);
|
|
1735
|
+
this.utils = inject(SectionUtilsService);
|
|
1736
|
+
}
|
|
1737
|
+
get fields() {
|
|
1738
|
+
return super.getFields();
|
|
1739
|
+
}
|
|
1740
|
+
get hasFields() {
|
|
1741
|
+
return super.hasFields;
|
|
1742
|
+
}
|
|
1743
|
+
onFieldClick(field) {
|
|
1744
|
+
this.emitFieldInteraction(field);
|
|
1745
|
+
}
|
|
1746
|
+
getTrendIcon(field) {
|
|
1747
|
+
return this.utils.getTrendIcon(field.trend ?? this.utils.calculateTrend(field.change));
|
|
1748
|
+
}
|
|
1749
|
+
getChangeClass(field) {
|
|
1750
|
+
return this.utils.getTrendClass(field.trend ?? field.change);
|
|
1751
|
+
}
|
|
1752
|
+
formatChange(change) {
|
|
1753
|
+
return this.utils.formatChange(change);
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1756
|
+
* Get display value, hiding "Streaming…" placeholder text
|
|
1757
|
+
* Inline implementation to avoid TypeScript override conflicts
|
|
1758
|
+
*/
|
|
1759
|
+
getDisplayValue(field) {
|
|
1760
|
+
const value = field.value;
|
|
1761
|
+
if (value === 'Streaming…' || value === 'Streaming...') {
|
|
1762
|
+
return '';
|
|
1763
|
+
}
|
|
1764
|
+
return value != null ? String(value) : '';
|
|
1765
|
+
}
|
|
1766
|
+
trackField(index, field) {
|
|
1767
|
+
return field.id ?? `${field.label}-${index}`;
|
|
1768
|
+
}
|
|
1769
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FinancialsSectionComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
1770
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: FinancialsSectionComponent, isStandalone: true, selector: "app-financials-section", usesInheritance: true, ngImport: i0, template: "<div class=\"ai-section ai-section--financials\">\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 <span *ngIf=\"fields.length\" class=\"ai-section__badge\">\n {{ fields.length }} {{ fields.length === 1 ? 'metric' : 'metrics' }}\n </span>\n </div>\n\n <div class=\"ai-section__body\">\n <ng-container *ngIf=\"fields.length; else financialsEmpty\">\n <div class=\"financials-grid\">\n <article\n *ngFor=\"let field of fields; trackBy: trackField\"\n class=\"financial-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=\"financial-card__content\">\n <span class=\"financial-card__label\">\n {{ field.label }}\n </span>\n <div class=\"financial-card__value-wrapper\">\n <span class=\"financial-card__value\" [ngClass]=\"{'financial-card__value--currency': field.format === 'currency'}\">\n <lucide-icon *ngIf=\"field.format === 'currency'\" name=\"dollar-sign\" size=\"16\" class=\"financial-card__currency-icon\"></lucide-icon>\n {{ getDisplayValue(field) }}\n </span>\n <lucide-icon\n *ngIf=\"getTrendIcon(field) as trendIcon\"\n [name]=\"trendIcon\"\n size=\"16\"\n class=\"financial-card__trend-icon\"\n [ngClass]=\"getChangeClass(field)\"\n ></lucide-icon>\n </div>\n </div>\n <div *ngIf=\"field.change !== undefined && field.change !== null\" class=\"financial-card__change\" [ngClass]=\"getChangeClass(field)\">\n {{ field.change > 0 ? '+' : '' }}{{ field.change }}%\n </div>\n </article>\n </div>\n </ng-container>\n\n <ng-template #financialsEmpty>\n <div class=\"section-empty\">\n <lucide-icon name=\"dollar-sign\" size=\"32\" class=\"section-empty__icon\"></lucide-icon>\n <p class=\"section-empty__text\">No financial metrics available</p>\n </div>\n </ng-template>\n </div>\n</div>\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 }); }
|
|
1771
|
+
}
|
|
1772
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FinancialsSectionComponent, decorators: [{
|
|
1773
|
+
type: Component,
|
|
1774
|
+
args: [{ selector: 'app-financials-section', standalone: true, imports: [CommonModule, LucideIconsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ai-section ai-section--financials\">\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 <span *ngIf=\"fields.length\" class=\"ai-section__badge\">\n {{ fields.length }} {{ fields.length === 1 ? 'metric' : 'metrics' }}\n </span>\n </div>\n\n <div class=\"ai-section__body\">\n <ng-container *ngIf=\"fields.length; else financialsEmpty\">\n <div class=\"financials-grid\">\n <article\n *ngFor=\"let field of fields; trackBy: trackField\"\n class=\"financial-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=\"financial-card__content\">\n <span class=\"financial-card__label\">\n {{ field.label }}\n </span>\n <div class=\"financial-card__value-wrapper\">\n <span class=\"financial-card__value\" [ngClass]=\"{'financial-card__value--currency': field.format === 'currency'}\">\n <lucide-icon *ngIf=\"field.format === 'currency'\" name=\"dollar-sign\" size=\"16\" class=\"financial-card__currency-icon\"></lucide-icon>\n {{ getDisplayValue(field) }}\n </span>\n <lucide-icon\n *ngIf=\"getTrendIcon(field) as trendIcon\"\n [name]=\"trendIcon\"\n size=\"16\"\n class=\"financial-card__trend-icon\"\n [ngClass]=\"getChangeClass(field)\"\n ></lucide-icon>\n </div>\n </div>\n <div *ngIf=\"field.change !== undefined && field.change !== null\" class=\"financial-card__change\" [ngClass]=\"getChangeClass(field)\">\n {{ field.change > 0 ? '+' : '' }}{{ field.change }}%\n </div>\n </article>\n </div>\n </ng-container>\n\n <ng-template #financialsEmpty>\n <div class=\"section-empty\">\n <lucide-icon name=\"dollar-sign\" size=\"32\" class=\"section-empty__icon\"></lucide-icon>\n <p class=\"section-empty__text\">No financial metrics available</p>\n </div>\n </ng-template>\n </div>\n</div>\n" }]
|
|
1775
|
+
}] });
|
|
1776
|
+
|
|
1777
|
+
class ListSectionComponent extends BaseSectionComponent {
|
|
1778
|
+
get items() {
|
|
1779
|
+
return super.getItems();
|
|
1780
|
+
}
|
|
1781
|
+
onItemClick(item) {
|
|
1782
|
+
this.emitItemInteraction(item);
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* Get display description, hiding "Streaming…" placeholder text
|
|
1786
|
+
* Inline implementation to avoid TypeScript override conflicts
|
|
1787
|
+
*/
|
|
1788
|
+
getDisplayDescription(item) {
|
|
1789
|
+
const description = item.description;
|
|
1790
|
+
if (description === 'Streaming…' || description === 'Streaming...') {
|
|
1791
|
+
return '';
|
|
1792
|
+
}
|
|
1793
|
+
return description ?? '';
|
|
1794
|
+
}
|
|
1795
|
+
trackItem(index, item) {
|
|
1796
|
+
return item.id ?? `${item.title ?? item.label}-${index}`;
|
|
1797
|
+
}
|
|
1798
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ListSectionComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
1799
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: ListSectionComponent, isStandalone: true, selector: "app-list-section", usesInheritance: true, ngImport: i0, template: "<div class=\"ai-section ai-section--list\">\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 <span *ngIf=\"items.length\" class=\"ai-section__badge\">\n {{ items.length }} {{ items.length === 1 ? 'item' : 'items' }}\n </span>\n </div>\n\n <div class=\"ai-section__body\">\n <ng-container *ngIf=\"items.length; else listEmpty\">\n <div class=\"list-grid\">\n <article\n *ngFor=\"let item of items; trackBy: trackItem; let idx = index\"\n class=\"list-card\"\n role=\"button\"\n tabindex=\"0\"\n [class.item-streaming]=\"getItemAnimationClass(getItemId(item, idx), idx) === 'item-streaming'\"\n [class.item-entered]=\"getItemAnimationClass(getItemId(item, idx), idx) === 'item-entered'\"\n [class.item-stagger-0]=\"getItemStaggerIndex(idx) === 0\"\n [class.item-stagger-1]=\"getItemStaggerIndex(idx) === 1\"\n [class.item-stagger-2]=\"getItemStaggerIndex(idx) === 2\"\n [class.item-stagger-3]=\"getItemStaggerIndex(idx) === 3\"\n [class.item-stagger-4]=\"getItemStaggerIndex(idx) === 4\"\n [class.item-stagger-5]=\"getItemStaggerIndex(idx) === 5\"\n [class.item-stagger-6]=\"getItemStaggerIndex(idx) === 6\"\n [class.item-stagger-7]=\"getItemStaggerIndex(idx) === 7\"\n [class.item-stagger-8]=\"getItemStaggerIndex(idx) === 8\"\n [class.item-stagger-9]=\"getItemStaggerIndex(idx) === 9\"\n [class.item-stagger-10]=\"getItemStaggerIndex(idx) === 10\"\n [class.item-stagger-11]=\"getItemStaggerIndex(idx) === 11\"\n [class.item-stagger-12]=\"getItemStaggerIndex(idx) === 12\"\n [class.item-stagger-13]=\"getItemStaggerIndex(idx) === 13\"\n [class.item-stagger-14]=\"getItemStaggerIndex(idx) === 14\"\n [class.item-stagger-15]=\"getItemStaggerIndex(idx) === 15\"\n (click)=\"onItemClick(item)\"\n (keydown.enter)=\"onItemClick(item)\"\n (keydown.space)=\"$event.preventDefault(); onItemClick(item)\"\n >\n <!-- Header: Title -->\n <header class=\"list-card__header\">\n <div class=\"list-card__header-content\">\n <h3 class=\"list-card__title\">\n {{ item.title || item.label }}\n </h3>\n <div class=\"list-card__badges\" *ngIf=\"item.priority || item.status\">\n <span *ngIf=\"item.priority\" class=\"list-card__badge list-card__badge--priority\" [attr.data-priority]=\"item.priority ? (item.priority + '') : null\">\n {{ item.priority }}\n </span>\n <span *ngIf=\"item.status\" class=\"list-card__badge list-card__badge--status\" [attr.data-status]=\"item.status ? (item.status + '') : null\">\n {{ item.status }}\n </span>\n </div>\n </div>\n <div *ngIf=\"item.value\" class=\"list-card__value\">\n {{ item.value }}\n </div>\n </header>\n\n <!-- Description -->\n <p *ngIf=\"getDisplayDescription(item)\" class=\"list-card__description\">\n {{ getDisplayDescription(item) }}\n </p>\n\n <!-- Meta Information -->\n <footer *ngIf=\"item.assignee || item.date\" class=\"list-card__footer\">\n <div *ngIf=\"item.assignee\" class=\"list-card__meta\">\n <lucide-icon name=\"user\" size=\"14\" class=\"list-card__meta-icon\"></lucide-icon>\n <span class=\"list-card__meta-text\">Assigned to <strong>{{ item.assignee }}</strong></span>\n </div>\n <div *ngIf=\"item.date\" class=\"list-card__meta\">\n <lucide-icon name=\"calendar\" size=\"14\" class=\"list-card__meta-icon\"></lucide-icon>\n <span class=\"list-card__meta-text\">Due {{ item.date }}</span>\n </div>\n </footer>\n </article>\n </div>\n </ng-container>\n\n <ng-template #listEmpty>\n <div class=\"section-empty\">\n <lucide-icon name=\"list\" size=\"32\" class=\"section-empty__icon\"></lucide-icon>\n <p class=\"section-empty__text\">No items available</p>\n </div>\n </ng-template>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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 }); }
|
|
1800
|
+
}
|
|
1801
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ListSectionComponent, decorators: [{
|
|
1802
|
+
type: Component,
|
|
1803
|
+
args: [{ selector: 'app-list-section', standalone: true, imports: [CommonModule, LucideIconsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ai-section ai-section--list\">\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 <span *ngIf=\"items.length\" class=\"ai-section__badge\">\n {{ items.length }} {{ items.length === 1 ? 'item' : 'items' }}\n </span>\n </div>\n\n <div class=\"ai-section__body\">\n <ng-container *ngIf=\"items.length; else listEmpty\">\n <div class=\"list-grid\">\n <article\n *ngFor=\"let item of items; trackBy: trackItem; let idx = index\"\n class=\"list-card\"\n role=\"button\"\n tabindex=\"0\"\n [class.item-streaming]=\"getItemAnimationClass(getItemId(item, idx), idx) === 'item-streaming'\"\n [class.item-entered]=\"getItemAnimationClass(getItemId(item, idx), idx) === 'item-entered'\"\n [class.item-stagger-0]=\"getItemStaggerIndex(idx) === 0\"\n [class.item-stagger-1]=\"getItemStaggerIndex(idx) === 1\"\n [class.item-stagger-2]=\"getItemStaggerIndex(idx) === 2\"\n [class.item-stagger-3]=\"getItemStaggerIndex(idx) === 3\"\n [class.item-stagger-4]=\"getItemStaggerIndex(idx) === 4\"\n [class.item-stagger-5]=\"getItemStaggerIndex(idx) === 5\"\n [class.item-stagger-6]=\"getItemStaggerIndex(idx) === 6\"\n [class.item-stagger-7]=\"getItemStaggerIndex(idx) === 7\"\n [class.item-stagger-8]=\"getItemStaggerIndex(idx) === 8\"\n [class.item-stagger-9]=\"getItemStaggerIndex(idx) === 9\"\n [class.item-stagger-10]=\"getItemStaggerIndex(idx) === 10\"\n [class.item-stagger-11]=\"getItemStaggerIndex(idx) === 11\"\n [class.item-stagger-12]=\"getItemStaggerIndex(idx) === 12\"\n [class.item-stagger-13]=\"getItemStaggerIndex(idx) === 13\"\n [class.item-stagger-14]=\"getItemStaggerIndex(idx) === 14\"\n [class.item-stagger-15]=\"getItemStaggerIndex(idx) === 15\"\n (click)=\"onItemClick(item)\"\n (keydown.enter)=\"onItemClick(item)\"\n (keydown.space)=\"$event.preventDefault(); onItemClick(item)\"\n >\n <!-- Header: Title -->\n <header class=\"list-card__header\">\n <div class=\"list-card__header-content\">\n <h3 class=\"list-card__title\">\n {{ item.title || item.label }}\n </h3>\n <div class=\"list-card__badges\" *ngIf=\"item.priority || item.status\">\n <span *ngIf=\"item.priority\" class=\"list-card__badge list-card__badge--priority\" [attr.data-priority]=\"item.priority ? (item.priority + '') : null\">\n {{ item.priority }}\n </span>\n <span *ngIf=\"item.status\" class=\"list-card__badge list-card__badge--status\" [attr.data-status]=\"item.status ? (item.status + '') : null\">\n {{ item.status }}\n </span>\n </div>\n </div>\n <div *ngIf=\"item.value\" class=\"list-card__value\">\n {{ item.value }}\n </div>\n </header>\n\n <!-- Description -->\n <p *ngIf=\"getDisplayDescription(item)\" class=\"list-card__description\">\n {{ getDisplayDescription(item) }}\n </p>\n\n <!-- Meta Information -->\n <footer *ngIf=\"item.assignee || item.date\" class=\"list-card__footer\">\n <div *ngIf=\"item.assignee\" class=\"list-card__meta\">\n <lucide-icon name=\"user\" size=\"14\" class=\"list-card__meta-icon\"></lucide-icon>\n <span class=\"list-card__meta-text\">Assigned to <strong>{{ item.assignee }}</strong></span>\n </div>\n <div *ngIf=\"item.date\" class=\"list-card__meta\">\n <lucide-icon name=\"calendar\" size=\"14\" class=\"list-card__meta-icon\"></lucide-icon>\n <span class=\"list-card__meta-text\">Due {{ item.date }}</span>\n </div>\n </footer>\n </article>\n </div>\n </ng-container>\n\n <ng-template #listEmpty>\n <div class=\"section-empty\">\n <lucide-icon name=\"list\" size=\"32\" class=\"section-empty__icon\"></lucide-icon>\n <p class=\"section-empty__text\">No items available</p>\n </div>\n </ng-template>\n </div>\n</div>\n" }]
|
|
1804
|
+
}] });
|
|
1805
|
+
|
|
1806
|
+
class EventSectionComponent extends BaseSectionComponent {
|
|
1807
|
+
get events() {
|
|
1808
|
+
const timeline = this.section['timelineEvents'];
|
|
1809
|
+
if (Array.isArray(timeline)) {
|
|
1810
|
+
return timeline;
|
|
1811
|
+
}
|
|
1812
|
+
return super.getItems();
|
|
1813
|
+
}
|
|
1814
|
+
get hasItems() {
|
|
1815
|
+
return this.events.length > 0;
|
|
1816
|
+
}
|
|
1817
|
+
onEventClick(event) {
|
|
1818
|
+
this.emitItemInteraction(event);
|
|
1819
|
+
}
|
|
1820
|
+
trackItem(index, event) {
|
|
1821
|
+
return event.id ?? `${event.title}-${index}`;
|
|
1822
|
+
}
|
|
1823
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: EventSectionComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
1824
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: EventSectionComponent, isStandalone: true, selector: "app-event-section", usesInheritance: true, ngImport: i0, template: "<div class=\"ai-section ai-section--event\">\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 <span *ngIf=\"events.length\" class=\"ai-section__badge\">\n {{ events.length }} {{ events.length === 1 ? 'event' : 'events' }}\n </span>\n </div>\n\n <div class=\"ai-section__body\">\n <ng-container *ngIf=\"events.length; else eventEmpty\">\n <div class=\"event-timeline\">\n <div class=\"event-timeline__line\"></div>\n\n <article\n *ngFor=\"let event of events; trackBy: trackItem\"\n class=\"event-card\"\n role=\"button\"\n tabindex=\"0\"\n (click)=\"onEventClick(event)\"\n (keydown.enter)=\"onEventClick(event)\"\n (keydown.space)=\"$event.preventDefault(); onEventClick(event)\"\n >\n <div class=\"event-card__marker\">\n <lucide-icon name=\"calendar\" size=\"14\" class=\"event-card__marker-icon\"></lucide-icon>\n <div class=\"event-card__marker-line\"></div>\n </div>\n\n <div class=\"event-card__content\">\n <header class=\"event-card__header\">\n <h3 class=\"event-card__title\">\n {{ event.title }}\n </h3>\n <span *ngIf=\"event.status\" class=\"event-card__badge\" [attr.data-status]=\"event.status ? (event.status + '') : null\">\n {{ event.status | uppercase }}\n </span>\n </header>\n\n <div *ngIf=\"event.date || event.time\" class=\"event-card__datetime\">\n <div *ngIf=\"event.date\" class=\"event-card__date\">\n <lucide-icon name=\"calendar\" size=\"14\" class=\"event-card__datetime-icon\"></lucide-icon>\n <span>{{ event.date }}</span>\n </div>\n <div *ngIf=\"event.time\" class=\"event-card__time\">\n <lucide-icon name=\"clock\" size=\"14\" class=\"event-card__datetime-icon\"></lucide-icon>\n <span>{{ event.time }}</span>\n </div>\n </div>\n\n <p *ngIf=\"event.description\" class=\"event-card__description\">\n {{ event.description }}\n </p>\n </div>\n </article>\n </div>\n </ng-container>\n\n <ng-template #eventEmpty>\n <div class=\"section-empty\">\n <lucide-icon name=\"calendar\" size=\"32\" class=\"section-empty__icon\"></lucide-icon>\n <p class=\"section-empty__text\">No timeline data available</p>\n </div>\n </ng-template>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1.UpperCasePipe, name: "uppercase" }, { 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 }); }
|
|
1825
|
+
}
|
|
1826
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: EventSectionComponent, decorators: [{
|
|
1827
|
+
type: Component,
|
|
1828
|
+
args: [{ selector: 'app-event-section', standalone: true, imports: [CommonModule, LucideIconsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ai-section ai-section--event\">\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 <span *ngIf=\"events.length\" class=\"ai-section__badge\">\n {{ events.length }} {{ events.length === 1 ? 'event' : 'events' }}\n </span>\n </div>\n\n <div class=\"ai-section__body\">\n <ng-container *ngIf=\"events.length; else eventEmpty\">\n <div class=\"event-timeline\">\n <div class=\"event-timeline__line\"></div>\n\n <article\n *ngFor=\"let event of events; trackBy: trackItem\"\n class=\"event-card\"\n role=\"button\"\n tabindex=\"0\"\n (click)=\"onEventClick(event)\"\n (keydown.enter)=\"onEventClick(event)\"\n (keydown.space)=\"$event.preventDefault(); onEventClick(event)\"\n >\n <div class=\"event-card__marker\">\n <lucide-icon name=\"calendar\" size=\"14\" class=\"event-card__marker-icon\"></lucide-icon>\n <div class=\"event-card__marker-line\"></div>\n </div>\n\n <div class=\"event-card__content\">\n <header class=\"event-card__header\">\n <h3 class=\"event-card__title\">\n {{ event.title }}\n </h3>\n <span *ngIf=\"event.status\" class=\"event-card__badge\" [attr.data-status]=\"event.status ? (event.status + '') : null\">\n {{ event.status | uppercase }}\n </span>\n </header>\n\n <div *ngIf=\"event.date || event.time\" class=\"event-card__datetime\">\n <div *ngIf=\"event.date\" class=\"event-card__date\">\n <lucide-icon name=\"calendar\" size=\"14\" class=\"event-card__datetime-icon\"></lucide-icon>\n <span>{{ event.date }}</span>\n </div>\n <div *ngIf=\"event.time\" class=\"event-card__time\">\n <lucide-icon name=\"clock\" size=\"14\" class=\"event-card__datetime-icon\"></lucide-icon>\n <span>{{ event.time }}</span>\n </div>\n </div>\n\n <p *ngIf=\"event.description\" class=\"event-card__description\">\n {{ event.description }}\n </p>\n </div>\n </article>\n </div>\n </ng-container>\n\n <ng-template #eventEmpty>\n <div class=\"section-empty\">\n <lucide-icon name=\"calendar\" size=\"32\" class=\"section-empty__icon\"></lucide-icon>\n <p class=\"section-empty__text\">No timeline data available</p>\n </div>\n </ng-template>\n </div>\n</div>\n" }]
|
|
1829
|
+
}] });
|
|
1830
|
+
|
|
1831
|
+
class ProductSectionComponent extends BaseSectionComponent {
|
|
1832
|
+
constructor() {
|
|
1833
|
+
super(...arguments);
|
|
1834
|
+
this.categoryOrder = ['references', 'pricing', 'features', 'advantages', 'process', 'contacts'];
|
|
1835
|
+
this.categoryConfig = {
|
|
1836
|
+
references: { title: 'Client References', icon: 'award' },
|
|
1837
|
+
pricing: { title: 'Pricing & Packages', icon: 'dollar-sign' },
|
|
1838
|
+
features: { title: 'Key Features', icon: 'zap' },
|
|
1839
|
+
advantages: { title: 'Competitive Advantages', icon: 'trending-up' },
|
|
1840
|
+
process: { title: 'Sales Process', icon: 'target' },
|
|
1841
|
+
contacts: { title: 'Internal Contacts', icon: 'users' },
|
|
1842
|
+
default: { title: 'Product Information', icon: 'box' }
|
|
1843
|
+
};
|
|
1844
|
+
this.referenceStars = [1, 2, 3, 4, 5];
|
|
1845
|
+
this.trackGroup = (_index, group) => group.key;
|
|
1846
|
+
this.trackField = (_index, field) => field.id ?? field.label ?? `product-field-${_index}`;
|
|
1847
|
+
}
|
|
1848
|
+
get fields() {
|
|
1849
|
+
return super.getFields();
|
|
1850
|
+
}
|
|
1851
|
+
get hasFields() {
|
|
1852
|
+
return super.hasFields;
|
|
1853
|
+
}
|
|
1854
|
+
get categoryGroups() {
|
|
1855
|
+
const groups = new Map();
|
|
1856
|
+
this.fields.forEach((field) => {
|
|
1857
|
+
const key = (field.category ?? 'default').toLowerCase();
|
|
1858
|
+
const bucket = groups.get(key) ?? [];
|
|
1859
|
+
bucket.push(field);
|
|
1860
|
+
groups.set(key, bucket);
|
|
1861
|
+
});
|
|
1862
|
+
const orderedKeys = Array.from(groups.keys()).sort((a, b) => {
|
|
1863
|
+
const orderA = this.categoryOrder.indexOf(a);
|
|
1864
|
+
const orderB = this.categoryOrder.indexOf(b);
|
|
1865
|
+
if (orderA === -1 && orderB === -1) {
|
|
1866
|
+
return a.localeCompare(b);
|
|
1867
|
+
}
|
|
1868
|
+
if (orderA === -1) {
|
|
1869
|
+
return 1;
|
|
1870
|
+
}
|
|
1871
|
+
if (orderB === -1) {
|
|
1872
|
+
return -1;
|
|
1873
|
+
}
|
|
1874
|
+
return orderA - orderB;
|
|
1875
|
+
});
|
|
1876
|
+
return orderedKeys.map((key) => {
|
|
1877
|
+
const config = this.categoryConfig[key] ?? this.categoryConfig['default'];
|
|
1878
|
+
return {
|
|
1879
|
+
key,
|
|
1880
|
+
title: config.title,
|
|
1881
|
+
icon: config.icon,
|
|
1882
|
+
fields: groups.get(key) ?? []
|
|
1883
|
+
};
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
get referenceGroup() {
|
|
1887
|
+
return this.categoryGroups.find((group) => group.key === 'references') ?? null;
|
|
1888
|
+
}
|
|
1889
|
+
get gridGroups() {
|
|
1890
|
+
return this.categoryGroups.filter((group) => group.key !== 'references');
|
|
1891
|
+
}
|
|
1892
|
+
get summaryStats() {
|
|
1893
|
+
const totalReferences = this.fields.filter((field) => field.category === 'references').length;
|
|
1894
|
+
const totalContacts = this.fields.filter((field) => field.category === 'contacts').length;
|
|
1895
|
+
const totalAdvantages = this.fields.filter((field) => field.category === 'advantages').length;
|
|
1896
|
+
const totalFeatures = this.fields.filter((field) => field.category === 'features').length;
|
|
1897
|
+
return [
|
|
1898
|
+
{ label: 'Features', value: totalFeatures },
|
|
1899
|
+
{ label: 'References', value: totalReferences },
|
|
1900
|
+
{ label: 'Contacts', value: totalContacts },
|
|
1901
|
+
{ label: 'Advantages', value: totalAdvantages }
|
|
1902
|
+
];
|
|
1903
|
+
}
|
|
1904
|
+
isContactField(field) {
|
|
1905
|
+
return !!field.contact;
|
|
1906
|
+
}
|
|
1907
|
+
isReferenceField(field) {
|
|
1908
|
+
return !!field.reference;
|
|
1909
|
+
}
|
|
1910
|
+
onFieldClick(field) {
|
|
1911
|
+
this.emitFieldInteraction(field, { category: field.category });
|
|
1912
|
+
}
|
|
1913
|
+
getGroupBadgeLabel(group) {
|
|
1914
|
+
return `${group.fields.length} ${group.fields.length === 1 ? 'item' : 'items'}`;
|
|
1915
|
+
}
|
|
1916
|
+
getCategoryIconTone(group) {
|
|
1917
|
+
switch (group.key) {
|
|
1918
|
+
case 'pricing':
|
|
1919
|
+
return 'product-card__icon--pricing';
|
|
1920
|
+
case 'features':
|
|
1921
|
+
return 'product-card__icon--features';
|
|
1922
|
+
case 'process':
|
|
1923
|
+
return 'product-card__icon--process';
|
|
1924
|
+
case 'references':
|
|
1925
|
+
return 'product-card__icon--references';
|
|
1926
|
+
case 'contacts':
|
|
1927
|
+
return 'product-card__icon--contacts';
|
|
1928
|
+
case 'advantages':
|
|
1929
|
+
return 'product-card__icon--advantages';
|
|
1930
|
+
default:
|
|
1931
|
+
return 'product-card__icon--default';
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
/**
|
|
1935
|
+
* Get display value, hiding "Streaming…" placeholder text
|
|
1936
|
+
* Inline implementation to avoid TypeScript override conflicts
|
|
1937
|
+
*/
|
|
1938
|
+
getDisplayValue(field) {
|
|
1939
|
+
const value = field.value;
|
|
1940
|
+
if (value === 'Streaming…' || value === 'Streaming...') {
|
|
1941
|
+
return '';
|
|
1942
|
+
}
|
|
1943
|
+
return value != null ? String(value) : '';
|
|
1944
|
+
}
|
|
1945
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ProductSectionComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
1946
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: ProductSectionComponent, isStandalone: true, selector: "app-product-section", usesInheritance: true, ngImport: i0, template: "<div class=\"ai-section ai-section--product\">\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 productEmpty\">\n <div class=\"product-layout\">\n <ng-container *ngIf=\"referenceGroup as references\">\n <section class=\"product-card product-card--references\" [style.animation]=\"'fadeInUp 0.6s ease-out forwards'\">\n <div class=\"product-card__header\">\n <div class=\"product-card__icon\" [ngClass]=\"getCategoryIconTone(references)\">\n <lucide-icon [name]=\"references.icon\" size=\"18\"></lucide-icon>\n </div>\n <div class=\"product-card__headline\">\n <p class=\"product-card__title\">{{ references.title }}</p>\n <p class=\"product-card__subtitle\">Verified client testimonials and success stories</p>\n </div>\n <span class=\"product-card__badge\">\n {{ getGroupBadgeLabel(references) }}\n </span>\n </div>\n\n <div class=\"product-reference-list\">\n <article\n *ngFor=\"let field of references.fields; trackBy: trackField\"\n class=\"product-reference\"\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=\"product-reference__avatar\">\n <lucide-icon name=\"award\" size=\"18\"></lucide-icon>\n </div>\n <div class=\"product-reference__content\">\n <p class=\"product-reference__company\">\n {{ field.reference?.company || field.title || field.label }}\n </p>\n <p *ngIf=\"field.reference?.testimonial\" class=\"product-reference__testimonial\">\n \"{{ field.reference?.testimonial }}\"\n </p>\n <div class=\"product-reference__meta\">\n <div class=\"product-reference__stars\">\n <lucide-icon\n *ngFor=\"let star of referenceStars\"\n name=\"star\"\n size=\"14\"\n ></lucide-icon>\n </div>\n <span class=\"product-reference__badge\">Success Story</span>\n </div>\n </div>\n <span class=\"product-entry__chevron\">\n <lucide-icon name=\"chevron-right\" size=\"16\"></lucide-icon>\n </span>\n </article>\n </div>\n </section>\n </ng-container>\n\n <div class=\"product-grid\" [ngClass]=\"{ 'product-grid--single': gridGroups.length <= 1 }\">\n <section\n *ngFor=\"let group of gridGroups; trackBy: trackGroup\"\n class=\"product-card\"\n >\n <div class=\"product-card__header\">\n <div class=\"product-card__icon\" [ngClass]=\"getCategoryIconTone(group)\">\n <lucide-icon [name]=\"group.icon\" size=\"18\"></lucide-icon>\n </div>\n <div class=\"product-card__headline\">\n <p class=\"product-card__title\" *ngIf=\"group.title !== section.title\">{{ group.title }}</p>\n <p class=\"product-card__subtitle\" *ngIf=\"group.key === 'pricing'\">\n Tailored packages and commercial structures\n </p>\n <p class=\"product-card__subtitle\" *ngIf=\"group.key === 'features'\">\n Capabilities that differentiate our solution\n </p>\n <p class=\"product-card__subtitle\" *ngIf=\"group.key === 'advantages'\">\n Strategic benefits for your organization\n </p>\n <p class=\"product-card__subtitle\" *ngIf=\"group.key === 'process'\">\n Structured delivery and engagement steps\n </p>\n <p class=\"product-card__subtitle\" *ngIf=\"group.key === 'contacts'\">\n Key people to accelerate collaboration\n </p>\n <p class=\"product-card__subtitle\" *ngIf=\"group.key === 'default' && group.title === section.title\">\n Additional product details and specifications\n </p>\n </div>\n <span class=\"product-card__badge\">\n {{ getGroupBadgeLabel(group) }}\n </span>\n </div>\n\n <div class=\"product-card__content\">\n <article\n *ngFor=\"let field of group.fields; trackBy: trackField\"\n class=\"product-entry\"\n role=\"button\"\n tabindex=\"0\"\n (click)=\"onFieldClick(field)\"\n (keydown.enter)=\"onFieldClick(field)\"\n (keydown.space)=\"$event.preventDefault(); onFieldClick(field)\"\n >\n <ng-container *ngIf=\"isContactField(field); else productEntryDefault\">\n <div class=\"product-contact\">\n <div class=\"product-contact__avatar\">\n <lucide-icon name=\"users\" size=\"18\"></lucide-icon>\n </div>\n <div class=\"product-contact__details\">\n <p class=\"product-contact__name\">{{ field.contact?.name }}</p>\n <p *ngIf=\"field.contact?.role\" class=\"product-contact__role\">{{ field.contact?.role }}</p>\n <div class=\"product-contact__meta\">\n <a\n *ngIf=\"field.contact?.email\"\n class=\"product-contact__link\"\n href=\"mailto:{{ field.contact?.email }}\"\n (click)=\"$event.stopPropagation()\"\n >\n <lucide-icon name=\"mail\" size=\"12\"></lucide-icon>\n <span>{{ field.contact?.email }}</span>\n </a>\n <a\n *ngIf=\"field.contact?.phone\"\n class=\"product-contact__link\"\n href=\"tel:{{ field.contact?.phone }}\"\n (click)=\"$event.stopPropagation()\"\n >\n <lucide-icon name=\"phone\" size=\"12\"></lucide-icon>\n <span>{{ field.contact?.phone }}</span>\n </a>\n </div>\n </div>\n </div>\n </ng-container>\n\n <ng-template #productEntryDefault>\n <div class=\"product-entry__body\">\n <div class=\"product-entry__heading\">\n <p class=\"product-entry__title\">\n {{ field.title || field.label }}\n </p>\n <span *ngIf=\"getDisplayValue(field)\" class=\"product-entry__value\">\n {{ getDisplayValue(field) }}\n </span>\n </div>\n\n <p *ngIf=\"field.description\" class=\"product-entry__description\">\n {{ field.description }}\n </p>\n\n <div *ngIf=\"field.benefits?.length\" class=\"product-entry__benefits\">\n <div\n *ngFor=\"let benefit of (field.benefits || []) | slice:0:3\"\n class=\"product-entry__benefit\"\n >\n <lucide-icon name=\"check-circle-2\" size=\"14\"></lucide-icon>\n <span>{{ benefit }}</span>\n </div>\n </div>\n\n <div class=\"product-entry__meta\">\n <span *ngIf=\"field.deliveryTime\" class=\"product-entry__meta-item\">\n <lucide-icon name=\"clock\" size=\"12\"></lucide-icon>\n {{ field.deliveryTime }}\n </span>\n <span *ngIf=\"field.teamSize\" class=\"product-entry__meta-item\">\n <lucide-icon name=\"users\" size=\"12\"></lucide-icon>\n {{ field.teamSize }}\n </span>\n </div>\n </div>\n </ng-template>\n\n <span class=\"product-entry__chevron\">\n <lucide-icon name=\"chevron-right\" size=\"16\"></lucide-icon>\n </span>\n </article>\n </div>\n </section>\n </div>\n\n <div class=\"product-summary\" *ngIf=\"summaryStats.length\">\n <div class=\"product-summary__item\" *ngFor=\"let stat of summaryStats\">\n <span class=\"product-summary__value\">{{ stat.value }}</span>\n <span class=\"product-summary__label\">{{ stat.label }}</span>\n </div>\n </div>\n </div>\n </ng-container>\n\n <ng-template #productEmpty>\n <div class=\"product-empty\">\n <lucide-icon name=\"box\" size=\"32\" class=\"mb-3 opacity-60\"></lucide-icon>\n <p class=\"text-sm\">No product insights configured.</p>\n </div>\n </ng-template>\n </div>\n</div>\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: "pipe", type: i1.SlicePipe, name: "slice" }, { 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 }); }
|
|
1947
|
+
}
|
|
1948
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ProductSectionComponent, decorators: [{
|
|
1949
|
+
type: Component,
|
|
1950
|
+
args: [{ selector: 'app-product-section', standalone: true, imports: [CommonModule, LucideIconsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ai-section ai-section--product\">\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 productEmpty\">\n <div class=\"product-layout\">\n <ng-container *ngIf=\"referenceGroup as references\">\n <section class=\"product-card product-card--references\" [style.animation]=\"'fadeInUp 0.6s ease-out forwards'\">\n <div class=\"product-card__header\">\n <div class=\"product-card__icon\" [ngClass]=\"getCategoryIconTone(references)\">\n <lucide-icon [name]=\"references.icon\" size=\"18\"></lucide-icon>\n </div>\n <div class=\"product-card__headline\">\n <p class=\"product-card__title\">{{ references.title }}</p>\n <p class=\"product-card__subtitle\">Verified client testimonials and success stories</p>\n </div>\n <span class=\"product-card__badge\">\n {{ getGroupBadgeLabel(references) }}\n </span>\n </div>\n\n <div class=\"product-reference-list\">\n <article\n *ngFor=\"let field of references.fields; trackBy: trackField\"\n class=\"product-reference\"\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=\"product-reference__avatar\">\n <lucide-icon name=\"award\" size=\"18\"></lucide-icon>\n </div>\n <div class=\"product-reference__content\">\n <p class=\"product-reference__company\">\n {{ field.reference?.company || field.title || field.label }}\n </p>\n <p *ngIf=\"field.reference?.testimonial\" class=\"product-reference__testimonial\">\n \"{{ field.reference?.testimonial }}\"\n </p>\n <div class=\"product-reference__meta\">\n <div class=\"product-reference__stars\">\n <lucide-icon\n *ngFor=\"let star of referenceStars\"\n name=\"star\"\n size=\"14\"\n ></lucide-icon>\n </div>\n <span class=\"product-reference__badge\">Success Story</span>\n </div>\n </div>\n <span class=\"product-entry__chevron\">\n <lucide-icon name=\"chevron-right\" size=\"16\"></lucide-icon>\n </span>\n </article>\n </div>\n </section>\n </ng-container>\n\n <div class=\"product-grid\" [ngClass]=\"{ 'product-grid--single': gridGroups.length <= 1 }\">\n <section\n *ngFor=\"let group of gridGroups; trackBy: trackGroup\"\n class=\"product-card\"\n >\n <div class=\"product-card__header\">\n <div class=\"product-card__icon\" [ngClass]=\"getCategoryIconTone(group)\">\n <lucide-icon [name]=\"group.icon\" size=\"18\"></lucide-icon>\n </div>\n <div class=\"product-card__headline\">\n <p class=\"product-card__title\" *ngIf=\"group.title !== section.title\">{{ group.title }}</p>\n <p class=\"product-card__subtitle\" *ngIf=\"group.key === 'pricing'\">\n Tailored packages and commercial structures\n </p>\n <p class=\"product-card__subtitle\" *ngIf=\"group.key === 'features'\">\n Capabilities that differentiate our solution\n </p>\n <p class=\"product-card__subtitle\" *ngIf=\"group.key === 'advantages'\">\n Strategic benefits for your organization\n </p>\n <p class=\"product-card__subtitle\" *ngIf=\"group.key === 'process'\">\n Structured delivery and engagement steps\n </p>\n <p class=\"product-card__subtitle\" *ngIf=\"group.key === 'contacts'\">\n Key people to accelerate collaboration\n </p>\n <p class=\"product-card__subtitle\" *ngIf=\"group.key === 'default' && group.title === section.title\">\n Additional product details and specifications\n </p>\n </div>\n <span class=\"product-card__badge\">\n {{ getGroupBadgeLabel(group) }}\n </span>\n </div>\n\n <div class=\"product-card__content\">\n <article\n *ngFor=\"let field of group.fields; trackBy: trackField\"\n class=\"product-entry\"\n role=\"button\"\n tabindex=\"0\"\n (click)=\"onFieldClick(field)\"\n (keydown.enter)=\"onFieldClick(field)\"\n (keydown.space)=\"$event.preventDefault(); onFieldClick(field)\"\n >\n <ng-container *ngIf=\"isContactField(field); else productEntryDefault\">\n <div class=\"product-contact\">\n <div class=\"product-contact__avatar\">\n <lucide-icon name=\"users\" size=\"18\"></lucide-icon>\n </div>\n <div class=\"product-contact__details\">\n <p class=\"product-contact__name\">{{ field.contact?.name }}</p>\n <p *ngIf=\"field.contact?.role\" class=\"product-contact__role\">{{ field.contact?.role }}</p>\n <div class=\"product-contact__meta\">\n <a\n *ngIf=\"field.contact?.email\"\n class=\"product-contact__link\"\n href=\"mailto:{{ field.contact?.email }}\"\n (click)=\"$event.stopPropagation()\"\n >\n <lucide-icon name=\"mail\" size=\"12\"></lucide-icon>\n <span>{{ field.contact?.email }}</span>\n </a>\n <a\n *ngIf=\"field.contact?.phone\"\n class=\"product-contact__link\"\n href=\"tel:{{ field.contact?.phone }}\"\n (click)=\"$event.stopPropagation()\"\n >\n <lucide-icon name=\"phone\" size=\"12\"></lucide-icon>\n <span>{{ field.contact?.phone }}</span>\n </a>\n </div>\n </div>\n </div>\n </ng-container>\n\n <ng-template #productEntryDefault>\n <div class=\"product-entry__body\">\n <div class=\"product-entry__heading\">\n <p class=\"product-entry__title\">\n {{ field.title || field.label }}\n </p>\n <span *ngIf=\"getDisplayValue(field)\" class=\"product-entry__value\">\n {{ getDisplayValue(field) }}\n </span>\n </div>\n\n <p *ngIf=\"field.description\" class=\"product-entry__description\">\n {{ field.description }}\n </p>\n\n <div *ngIf=\"field.benefits?.length\" class=\"product-entry__benefits\">\n <div\n *ngFor=\"let benefit of (field.benefits || []) | slice:0:3\"\n class=\"product-entry__benefit\"\n >\n <lucide-icon name=\"check-circle-2\" size=\"14\"></lucide-icon>\n <span>{{ benefit }}</span>\n </div>\n </div>\n\n <div class=\"product-entry__meta\">\n <span *ngIf=\"field.deliveryTime\" class=\"product-entry__meta-item\">\n <lucide-icon name=\"clock\" size=\"12\"></lucide-icon>\n {{ field.deliveryTime }}\n </span>\n <span *ngIf=\"field.teamSize\" class=\"product-entry__meta-item\">\n <lucide-icon name=\"users\" size=\"12\"></lucide-icon>\n {{ field.teamSize }}\n </span>\n </div>\n </div>\n </ng-template>\n\n <span class=\"product-entry__chevron\">\n <lucide-icon name=\"chevron-right\" size=\"16\"></lucide-icon>\n </span>\n </article>\n </div>\n </section>\n </div>\n\n <div class=\"product-summary\" *ngIf=\"summaryStats.length\">\n <div class=\"product-summary__item\" *ngFor=\"let stat of summaryStats\">\n <span class=\"product-summary__value\">{{ stat.value }}</span>\n <span class=\"product-summary__label\">{{ stat.label }}</span>\n </div>\n </div>\n </div>\n </ng-container>\n\n <ng-template #productEmpty>\n <div class=\"product-empty\">\n <lucide-icon name=\"box\" size=\"32\" class=\"mb-3 opacity-60\"></lucide-icon>\n <p class=\"text-sm\">No product insights configured.</p>\n </div>\n </ng-template>\n </div>\n</div>\n" }]
|
|
1951
|
+
}] });
|
|
1952
|
+
|
|
1953
|
+
class SolutionsSectionComponent extends BaseSectionComponent {
|
|
1954
|
+
get fields() {
|
|
1955
|
+
return super.getFields();
|
|
1956
|
+
}
|
|
1957
|
+
get hasFields() {
|
|
1958
|
+
return super.hasFields;
|
|
1959
|
+
}
|
|
1960
|
+
trackField(index, field) {
|
|
1961
|
+
return field.id || field.title || field.label || index.toString();
|
|
1962
|
+
}
|
|
1963
|
+
onSolutionClick(field) {
|
|
1964
|
+
// Solutions are treated as items in the template
|
|
1965
|
+
this.emitItemInteraction(field, { category: field.category });
|
|
1966
|
+
}
|
|
1967
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SolutionsSectionComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
1968
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: SolutionsSectionComponent, isStandalone: true, selector: "app-solutions-section", usesInheritance: true, ngImport: i0, template: "<div class=\"ai-section ai-section--solutions\">\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 <span *ngIf=\"hasFields\" class=\"ai-section__badge\">\n {{ fields.length }} {{ fields.length === 1 ? 'solution' : 'solutions' }}\n </span>\n </div>\n\n <div class=\"ai-section__body\">\n <ng-container *ngIf=\"hasFields; else solutionsEmpty\">\n <div class=\"solutions-grid\">\n <article\n *ngFor=\"let field of fields; trackBy: trackField\"\n class=\"solution-card\"\n role=\"button\"\n tabindex=\"0\"\n (click)=\"onSolutionClick(field)\"\n (keydown.enter)=\"onSolutionClick(field)\"\n (keydown.space)=\"$event.preventDefault(); onSolutionClick(field)\"\n >\n <!-- Header: Title and Category -->\n <header class=\"solution-card__header\">\n <h3 class=\"solution-card__title\">\n {{ field.title || field.label }}\n </h3>\n <div class=\"solution-card__badges\" *ngIf=\"field.category\">\n <span *ngIf=\"field.category\" class=\"solution-card__badge solution-card__badge--category\">\n {{ field.category }}\n </span>\n </div>\n </header>\n\n <!-- Description -->\n <p *ngIf=\"field.description\" class=\"solution-card__description\">\n {{ field.description }}\n </p>\n\n <!-- Benefits/Features -->\n <ul *ngIf=\"field.benefits?.length\" class=\"solution-card__benefits\">\n <li *ngFor=\"let benefit of field.benefits | slice:0:4\" class=\"solution-card__benefit\">\n <lucide-icon name=\"check\" size=\"14\" class=\"solution-card__benefit-icon\"></lucide-icon>\n <span class=\"solution-card__benefit-text\">{{ benefit }}</span>\n </li>\n </ul>\n\n <!-- Meta Information -->\n <footer *ngIf=\"field.deliveryTime || field.teamSize || field.outcomes?.length\" class=\"solution-card__footer\">\n <div *ngIf=\"field.deliveryTime\" class=\"solution-card__meta\">\n <lucide-icon name=\"clock\" size=\"14\" class=\"solution-card__meta-icon\"></lucide-icon>\n <span class=\"solution-card__meta-text\">{{ field.deliveryTime }}</span>\n </div>\n <div *ngIf=\"field.teamSize\" class=\"solution-card__meta\">\n <lucide-icon name=\"users\" size=\"14\" class=\"solution-card__meta-icon\"></lucide-icon>\n <span class=\"solution-card__meta-text\">{{ field.teamSize }}</span>\n </div>\n <ng-container *ngIf=\"field.outcomes as outcomes\">\n <div *ngIf=\"outcomes[0]\" class=\"solution-card__meta\">\n <lucide-icon name=\"trending-up\" size=\"14\" class=\"solution-card__meta-icon\"></lucide-icon>\n <span class=\"solution-card__meta-text\">{{ outcomes[0] }}</span>\n </div>\n <div *ngIf=\"outcomes[1]\" class=\"solution-card__meta\">\n <lucide-icon name=\"sparkles\" size=\"14\" class=\"solution-card__meta-icon\"></lucide-icon>\n <span class=\"solution-card__meta-text\">{{ outcomes[1] }}</span>\n </div>\n </ng-container>\n </footer>\n </article>\n </div>\n </ng-container>\n\n <ng-template #solutionsEmpty>\n <div class=\"section-empty\">\n <lucide-icon name=\"sparkles\" size=\"32\" class=\"section-empty__icon\"></lucide-icon>\n <p class=\"section-empty__text\">No solutions available</p>\n </div>\n </ng-template>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1.SlicePipe, name: "slice" }, { 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 }); }
|
|
1969
|
+
}
|
|
1970
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SolutionsSectionComponent, decorators: [{
|
|
1971
|
+
type: Component,
|
|
1972
|
+
args: [{ selector: 'app-solutions-section', standalone: true, imports: [CommonModule, LucideIconsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ai-section ai-section--solutions\">\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 <span *ngIf=\"hasFields\" class=\"ai-section__badge\">\n {{ fields.length }} {{ fields.length === 1 ? 'solution' : 'solutions' }}\n </span>\n </div>\n\n <div class=\"ai-section__body\">\n <ng-container *ngIf=\"hasFields; else solutionsEmpty\">\n <div class=\"solutions-grid\">\n <article\n *ngFor=\"let field of fields; trackBy: trackField\"\n class=\"solution-card\"\n role=\"button\"\n tabindex=\"0\"\n (click)=\"onSolutionClick(field)\"\n (keydown.enter)=\"onSolutionClick(field)\"\n (keydown.space)=\"$event.preventDefault(); onSolutionClick(field)\"\n >\n <!-- Header: Title and Category -->\n <header class=\"solution-card__header\">\n <h3 class=\"solution-card__title\">\n {{ field.title || field.label }}\n </h3>\n <div class=\"solution-card__badges\" *ngIf=\"field.category\">\n <span *ngIf=\"field.category\" class=\"solution-card__badge solution-card__badge--category\">\n {{ field.category }}\n </span>\n </div>\n </header>\n\n <!-- Description -->\n <p *ngIf=\"field.description\" class=\"solution-card__description\">\n {{ field.description }}\n </p>\n\n <!-- Benefits/Features -->\n <ul *ngIf=\"field.benefits?.length\" class=\"solution-card__benefits\">\n <li *ngFor=\"let benefit of field.benefits | slice:0:4\" class=\"solution-card__benefit\">\n <lucide-icon name=\"check\" size=\"14\" class=\"solution-card__benefit-icon\"></lucide-icon>\n <span class=\"solution-card__benefit-text\">{{ benefit }}</span>\n </li>\n </ul>\n\n <!-- Meta Information -->\n <footer *ngIf=\"field.deliveryTime || field.teamSize || field.outcomes?.length\" class=\"solution-card__footer\">\n <div *ngIf=\"field.deliveryTime\" class=\"solution-card__meta\">\n <lucide-icon name=\"clock\" size=\"14\" class=\"solution-card__meta-icon\"></lucide-icon>\n <span class=\"solution-card__meta-text\">{{ field.deliveryTime }}</span>\n </div>\n <div *ngIf=\"field.teamSize\" class=\"solution-card__meta\">\n <lucide-icon name=\"users\" size=\"14\" class=\"solution-card__meta-icon\"></lucide-icon>\n <span class=\"solution-card__meta-text\">{{ field.teamSize }}</span>\n </div>\n <ng-container *ngIf=\"field.outcomes as outcomes\">\n <div *ngIf=\"outcomes[0]\" class=\"solution-card__meta\">\n <lucide-icon name=\"trending-up\" size=\"14\" class=\"solution-card__meta-icon\"></lucide-icon>\n <span class=\"solution-card__meta-text\">{{ outcomes[0] }}</span>\n </div>\n <div *ngIf=\"outcomes[1]\" class=\"solution-card__meta\">\n <lucide-icon name=\"sparkles\" size=\"14\" class=\"solution-card__meta-icon\"></lucide-icon>\n <span class=\"solution-card__meta-text\">{{ outcomes[1] }}</span>\n </div>\n </ng-container>\n </footer>\n </article>\n </div>\n </ng-container>\n\n <ng-template #solutionsEmpty>\n <div class=\"section-empty\">\n <lucide-icon name=\"sparkles\" size=\"32\" class=\"section-empty__icon\"></lucide-icon>\n <p class=\"section-empty__text\">No solutions available</p>\n </div>\n </ng-template>\n </div>\n</div>\n" }]
|
|
1973
|
+
}] });
|
|
1974
|
+
|
|
1975
|
+
class ContactCardSectionComponent extends BaseSectionComponent {
|
|
1976
|
+
constructor() {
|
|
1977
|
+
super(...arguments);
|
|
1978
|
+
this.trackContact = (_index, contact) => contact.id ?? this.getContactEmail(contact) ?? this.getContactName(contact) ?? `contact-${_index}`;
|
|
1979
|
+
}
|
|
1980
|
+
get contacts() {
|
|
1981
|
+
return super.getFields();
|
|
1982
|
+
}
|
|
1983
|
+
get hasContacts() {
|
|
1984
|
+
return super.hasFields;
|
|
1985
|
+
}
|
|
1986
|
+
onContactClick(field) {
|
|
1987
|
+
this.emitFieldInteraction(field);
|
|
1988
|
+
}
|
|
1989
|
+
getContactName(contact) {
|
|
1990
|
+
return (contact.contact?.name ??
|
|
1991
|
+
contact.name ??
|
|
1992
|
+
contact.title ??
|
|
1993
|
+
contact.label ??
|
|
1994
|
+
contact.contact?.email ??
|
|
1995
|
+
contact.email ??
|
|
1996
|
+
'Unnamed contact');
|
|
1997
|
+
}
|
|
1998
|
+
getContactRole(contact) {
|
|
1999
|
+
// Prioritize title field as it often contains the role description
|
|
2000
|
+
if (contact.title) {
|
|
2001
|
+
const role = contact.contact?.role ?? contact.role;
|
|
2002
|
+
// Combine title and role if both exist
|
|
2003
|
+
return role ? `${contact.title} ${role}` : contact.title;
|
|
2004
|
+
}
|
|
2005
|
+
return contact.contact?.role ?? contact.role ?? undefined;
|
|
2006
|
+
}
|
|
2007
|
+
getContactTitle(contact) {
|
|
2008
|
+
return contact.title ?? contact.contact?.role ?? contact.role ?? undefined;
|
|
2009
|
+
}
|
|
2010
|
+
getContactEmail(contact) {
|
|
2011
|
+
return contact.contact?.email ?? contact.email ?? undefined;
|
|
2012
|
+
}
|
|
2013
|
+
getContactPhone(contact) {
|
|
2014
|
+
return contact.contact?.phone ?? contact.phone ?? undefined;
|
|
2015
|
+
}
|
|
2016
|
+
getContactAvatar(contact) {
|
|
2017
|
+
return contact.contact?.avatar ?? contact.avatar ?? undefined;
|
|
2018
|
+
}
|
|
2019
|
+
getInitials(name) {
|
|
2020
|
+
if (!name) {
|
|
2021
|
+
return 'NA';
|
|
2022
|
+
}
|
|
2023
|
+
return name
|
|
2024
|
+
.split(' ')
|
|
2025
|
+
.filter(Boolean)
|
|
2026
|
+
.slice(0, 2)
|
|
2027
|
+
.map((part) => part.charAt(0).toUpperCase())
|
|
2028
|
+
.join('');
|
|
2029
|
+
}
|
|
2030
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ContactCardSectionComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
2031
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: ContactCardSectionComponent, isStandalone: true, selector: "app-contact-card-section", usesInheritance: true, ngImport: i0, template: "<div class=\"ai-section ai-section--contact\">\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 <span *ngIf=\"hasContacts\" class=\"ai-section__badge\">\n {{ contacts.length }} {{ contacts.length === 1 ? 'contact' : 'contacts' }}\n </span>\n </div>\n\n <div class=\"ai-section__body\">\n <ng-container *ngIf=\"hasContacts; else contactEmpty\">\n <div class=\"contact-grid\">\n <article\n *ngFor=\"let contact of contacts; trackBy: trackContact\"\n class=\"contact-card\"\n role=\"button\"\n tabindex=\"0\"\n (click)=\"onContactClick(contact)\"\n (keydown.enter)=\"onContactClick(contact)\"\n (keydown.space)=\"$event.preventDefault(); onContactClick(contact)\"\n >\n <!-- Header: Avatar and Name/Role -->\n <header class=\"contact-card__header\">\n <div class=\"contact-card__avatar\">\n <ng-container *ngIf=\"getContactAvatar(contact) as avatar; else contactInitials\">\n <img [src]=\"avatar\" [alt]=\"getContactName(contact)\" class=\"contact-card__avatar-img\" />\n </ng-container>\n <ng-template #contactInitials>\n <span class=\"contact-card__initials\">\n {{ getInitials(getContactName(contact)) }}\n </span>\n </ng-template>\n </div>\n <div class=\"contact-card__header-content\">\n <h3 class=\"contact-card__name\">{{ getContactName(contact) }}</h3>\n <p *ngIf=\"getContactRole(contact) as role\" class=\"contact-card__role\">{{ role }}</p>\n </div>\n </header>\n\n <!-- Contact Information -->\n <div class=\"contact-card__meta\">\n <a\n *ngIf=\"getContactEmail(contact) as email\"\n class=\"contact-card__meta-item contact-card__meta-item--link\"\n href=\"mailto:{{ email }}\"\n (click)=\"$event.stopPropagation()\"\n >\n <lucide-icon name=\"mail\" size=\"14\" class=\"contact-card__meta-icon\"></lucide-icon>\n <span class=\"contact-card__meta-text\">{{ email }}</span>\n </a>\n\n <a\n *ngIf=\"getContactPhone(contact) as phone\"\n class=\"contact-card__meta-item contact-card__meta-item--link\"\n href=\"tel:{{ phone }}\"\n (click)=\"$event.stopPropagation()\"\n >\n <lucide-icon name=\"phone\" size=\"14\" class=\"contact-card__meta-icon\"></lucide-icon>\n <span class=\"contact-card__meta-text\">{{ phone }}</span>\n </a>\n\n <div *ngIf=\"contact.department\" class=\"contact-card__meta-item\">\n <lucide-icon name=\"building\" size=\"14\" class=\"contact-card__meta-icon\"></lucide-icon>\n <span class=\"contact-card__meta-text\">{{ contact.department }}</span>\n </div>\n\n <div *ngIf=\"contact.location\" class=\"contact-card__meta-item\">\n <lucide-icon name=\"map-pin\" size=\"14\" class=\"contact-card__meta-icon\"></lucide-icon>\n <span class=\"contact-card__meta-text\">{{ contact.location }}</span>\n </div>\n </div>\n </article>\n </div>\n </ng-container>\n\n <ng-template #contactEmpty>\n <div class=\"section-empty\">\n <lucide-icon name=\"user\" size=\"32\" class=\"section-empty__icon\"></lucide-icon>\n <p class=\"section-empty__text\">No contacts available</p>\n </div>\n </ng-template>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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 }); }
|
|
2032
|
+
}
|
|
2033
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ContactCardSectionComponent, decorators: [{
|
|
2034
|
+
type: Component,
|
|
2035
|
+
args: [{ selector: 'app-contact-card-section', standalone: true, imports: [CommonModule, LucideIconsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ai-section ai-section--contact\">\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 <span *ngIf=\"hasContacts\" class=\"ai-section__badge\">\n {{ contacts.length }} {{ contacts.length === 1 ? 'contact' : 'contacts' }}\n </span>\n </div>\n\n <div class=\"ai-section__body\">\n <ng-container *ngIf=\"hasContacts; else contactEmpty\">\n <div class=\"contact-grid\">\n <article\n *ngFor=\"let contact of contacts; trackBy: trackContact\"\n class=\"contact-card\"\n role=\"button\"\n tabindex=\"0\"\n (click)=\"onContactClick(contact)\"\n (keydown.enter)=\"onContactClick(contact)\"\n (keydown.space)=\"$event.preventDefault(); onContactClick(contact)\"\n >\n <!-- Header: Avatar and Name/Role -->\n <header class=\"contact-card__header\">\n <div class=\"contact-card__avatar\">\n <ng-container *ngIf=\"getContactAvatar(contact) as avatar; else contactInitials\">\n <img [src]=\"avatar\" [alt]=\"getContactName(contact)\" class=\"contact-card__avatar-img\" />\n </ng-container>\n <ng-template #contactInitials>\n <span class=\"contact-card__initials\">\n {{ getInitials(getContactName(contact)) }}\n </span>\n </ng-template>\n </div>\n <div class=\"contact-card__header-content\">\n <h3 class=\"contact-card__name\">{{ getContactName(contact) }}</h3>\n <p *ngIf=\"getContactRole(contact) as role\" class=\"contact-card__role\">{{ role }}</p>\n </div>\n </header>\n\n <!-- Contact Information -->\n <div class=\"contact-card__meta\">\n <a\n *ngIf=\"getContactEmail(contact) as email\"\n class=\"contact-card__meta-item contact-card__meta-item--link\"\n href=\"mailto:{{ email }}\"\n (click)=\"$event.stopPropagation()\"\n >\n <lucide-icon name=\"mail\" size=\"14\" class=\"contact-card__meta-icon\"></lucide-icon>\n <span class=\"contact-card__meta-text\">{{ email }}</span>\n </a>\n\n <a\n *ngIf=\"getContactPhone(contact) as phone\"\n class=\"contact-card__meta-item contact-card__meta-item--link\"\n href=\"tel:{{ phone }}\"\n (click)=\"$event.stopPropagation()\"\n >\n <lucide-icon name=\"phone\" size=\"14\" class=\"contact-card__meta-icon\"></lucide-icon>\n <span class=\"contact-card__meta-text\">{{ phone }}</span>\n </a>\n\n <div *ngIf=\"contact.department\" class=\"contact-card__meta-item\">\n <lucide-icon name=\"building\" size=\"14\" class=\"contact-card__meta-icon\"></lucide-icon>\n <span class=\"contact-card__meta-text\">{{ contact.department }}</span>\n </div>\n\n <div *ngIf=\"contact.location\" class=\"contact-card__meta-item\">\n <lucide-icon name=\"map-pin\" size=\"14\" class=\"contact-card__meta-icon\"></lucide-icon>\n <span class=\"contact-card__meta-text\">{{ contact.location }}</span>\n </div>\n </div>\n </article>\n </div>\n </ng-container>\n\n <ng-template #contactEmpty>\n <div class=\"section-empty\">\n <lucide-icon name=\"user\" size=\"32\" class=\"section-empty__icon\"></lucide-icon>\n <p class=\"section-empty__text\">No contacts available</p>\n </div>\n </ng-template>\n </div>\n</div>\n" }]
|
|
2036
|
+
}] });
|
|
2037
|
+
|
|
2038
|
+
class NetworkCardSectionComponent extends BaseSectionComponent {
|
|
2039
|
+
constructor() {
|
|
2040
|
+
super(...arguments);
|
|
2041
|
+
this.trackField = (_index, field) => field.id ?? field.label ?? `network-${_index}`;
|
|
2042
|
+
}
|
|
2043
|
+
get fields() {
|
|
2044
|
+
return super.getFields();
|
|
2045
|
+
}
|
|
2046
|
+
get hasFields() {
|
|
2047
|
+
return super.hasFields;
|
|
2048
|
+
}
|
|
2049
|
+
onItemClick(field) {
|
|
2050
|
+
// Emit as field interaction (network fields are treated as fields)
|
|
2051
|
+
this.emitFieldInteraction(field);
|
|
2052
|
+
}
|
|
2053
|
+
/**
|
|
2054
|
+
* Get display value, hiding "Streaming…" placeholder text
|
|
2055
|
+
* Inline implementation to avoid TypeScript override conflicts
|
|
2056
|
+
*/
|
|
2057
|
+
getDisplayValue(field) {
|
|
2058
|
+
const value = field.value;
|
|
2059
|
+
if (value === 'Streaming…' || value === 'Streaming...') {
|
|
2060
|
+
return '';
|
|
2061
|
+
}
|
|
2062
|
+
return value != null ? String(value) : '';
|
|
2063
|
+
}
|
|
2064
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NetworkCardSectionComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
2065
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: NetworkCardSectionComponent, isStandalone: true, selector: "app-network-card-section", usesInheritance: true, ngImport: i0, template: "<div class=\"ai-section ai-section--network-card\">\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 <span *ngIf=\"fields.length\" class=\"ai-section__badge\">\n {{ fields.length }} {{ fields.length === 1 ? 'metric' : 'metrics' }}\n </span>\n </div>\n\n <div class=\"ai-section__body\">\n <ng-container *ngIf=\"fields.length; else networkEmpty\">\n <div class=\"network-grid\">\n <article\n *ngFor=\"let field of fields; trackBy: trackField\"\n class=\"network-card\"\n role=\"button\"\n tabindex=\"0\"\n (click)=\"onItemClick(field)\"\n (keydown.enter)=\"onItemClick(field)\"\n (keydown.space)=\"$event.preventDefault(); onItemClick(field)\"\n >\n <div class=\"network-card__content\">\n <span class=\"network-card__label\">\n {{ field.label || field.title }}\n </span>\n <span *ngIf=\"getDisplayValue(field)\" class=\"network-card__value\">\n {{ getDisplayValue(field) }}\n </span>\n </div>\n </article>\n </div>\n </ng-container>\n\n <ng-template #networkEmpty>\n <div class=\"section-empty\">\n <lucide-icon name=\"network\" size=\"32\" class=\"section-empty__icon\"></lucide-icon>\n <p class=\"section-empty__text\">No network data available</p>\n </div>\n </ng-template>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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 }); }
|
|
2066
|
+
}
|
|
2067
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NetworkCardSectionComponent, decorators: [{
|
|
2068
|
+
type: Component,
|
|
2069
|
+
args: [{ selector: 'app-network-card-section', standalone: true, imports: [CommonModule, LucideIconsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ai-section ai-section--network-card\">\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 <span *ngIf=\"fields.length\" class=\"ai-section__badge\">\n {{ fields.length }} {{ fields.length === 1 ? 'metric' : 'metrics' }}\n </span>\n </div>\n\n <div class=\"ai-section__body\">\n <ng-container *ngIf=\"fields.length; else networkEmpty\">\n <div class=\"network-grid\">\n <article\n *ngFor=\"let field of fields; trackBy: trackField\"\n class=\"network-card\"\n role=\"button\"\n tabindex=\"0\"\n (click)=\"onItemClick(field)\"\n (keydown.enter)=\"onItemClick(field)\"\n (keydown.space)=\"$event.preventDefault(); onItemClick(field)\"\n >\n <div class=\"network-card__content\">\n <span class=\"network-card__label\">\n {{ field.label || field.title }}\n </span>\n <span *ngIf=\"getDisplayValue(field)\" class=\"network-card__value\">\n {{ getDisplayValue(field) }}\n </span>\n </div>\n </article>\n </div>\n </ng-container>\n\n <ng-template #networkEmpty>\n <div class=\"section-empty\">\n <lucide-icon name=\"network\" size=\"32\" class=\"section-empty__icon\"></lucide-icon>\n <p class=\"section-empty__text\">No network data available</p>\n </div>\n </ng-template>\n </div>\n</div>\n" }]
|
|
2070
|
+
}] });
|
|
2071
|
+
|
|
2072
|
+
class MapSectionComponent extends BaseSectionComponent {
|
|
2073
|
+
get locations() {
|
|
2074
|
+
const fromFields = super.getFields();
|
|
2075
|
+
const mappedFields = fromFields
|
|
2076
|
+
.map((field) => ({
|
|
2077
|
+
...field,
|
|
2078
|
+
name: field.name || field.title || field.label || field.id || 'Unknown Location'
|
|
2079
|
+
}))
|
|
2080
|
+
.filter((field) => !!field.name && typeof field.name === 'string');
|
|
2081
|
+
const items = super.getItems();
|
|
2082
|
+
if (items.length) {
|
|
2083
|
+
const mappedItems = items
|
|
2084
|
+
.map((item) => ({
|
|
2085
|
+
...item,
|
|
2086
|
+
name: item.name || item.title || item.id || 'Unknown Location'
|
|
2087
|
+
}))
|
|
2088
|
+
.filter((item) => !!item.name && typeof item.name === 'string');
|
|
2089
|
+
return mappedFields.concat(mappedItems);
|
|
2090
|
+
}
|
|
2091
|
+
return mappedFields;
|
|
2092
|
+
}
|
|
2093
|
+
get hasItems() {
|
|
2094
|
+
return this.locations.length > 0;
|
|
2095
|
+
}
|
|
2096
|
+
onLocationClick(location) {
|
|
2097
|
+
this.emitItemInteraction(location);
|
|
2098
|
+
}
|
|
2099
|
+
formatCoordinates(location) {
|
|
2100
|
+
if (!location.coordinates) {
|
|
2101
|
+
return null;
|
|
2102
|
+
}
|
|
2103
|
+
const { lat, lng } = location.coordinates;
|
|
2104
|
+
return `${lat.toFixed(2)}, ${lng.toFixed(2)}`;
|
|
2105
|
+
}
|
|
2106
|
+
trackItem(index, location) {
|
|
2107
|
+
return location.id ?? `${location.name}-${index}`;
|
|
2108
|
+
}
|
|
2109
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MapSectionComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
2110
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: MapSectionComponent, isStandalone: true, selector: "app-map-section", usesInheritance: true, ngImport: i0, template: "<div class=\"ai-section ai-section--map\">\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 <span *ngIf=\"hasItems\" class=\"ai-section__badge\">\n {{ locations.length }} {{ locations.length === 1 ? 'location' : 'locations' }}\n </span>\n </div>\n\n <div class=\"ai-section__body\">\n <ng-container *ngIf=\"hasItems; else mapEmpty\">\n <div class=\"map-grid\">\n <article\n *ngFor=\"let location of locations; trackBy: trackItem\"\n class=\"map-card\"\n role=\"button\"\n tabindex=\"0\"\n (click)=\"onLocationClick(location)\"\n (keydown.enter)=\"onLocationClick(location)\"\n (keydown.space)=\"$event.preventDefault(); onLocationClick(location)\"\n >\n <!-- Header: Title and Badge -->\n <header class=\"map-card__header\">\n <div class=\"map-card__header-content\">\n <h3 class=\"map-card__title\">\n {{ location.name }}\n </h3>\n <div class=\"map-card__badges\" *ngIf=\"location.type\">\n <span class=\"map-card__badge\">\n {{ location.type }}\n </span>\n </div>\n </div>\n </header>\n\n <!-- Address -->\n <div *ngIf=\"location.address\" class=\"map-card__address-wrapper\">\n <lucide-icon name=\"map-pin\" size=\"14\" class=\"map-card__address-icon\"></lucide-icon>\n <p class=\"map-card__address\">\n {{ location.address }}\n </p>\n </div>\n\n <!-- Meta Information -->\n <footer *ngIf=\"formatCoordinates(location) || location.value\" class=\"map-card__footer\">\n <div *ngIf=\"formatCoordinates(location) as coords\" class=\"map-card__meta\">\n <lucide-icon name=\"map-pin\" size=\"14\" class=\"map-card__meta-icon\"></lucide-icon>\n <span class=\"map-card__meta-text\">{{ coords }}</span>\n </div>\n <div *ngIf=\"location.value\" class=\"map-card__meta\">\n <lucide-icon name=\"activity\" size=\"14\" class=\"map-card__meta-icon\"></lucide-icon>\n <span class=\"map-card__meta-text\">{{ location.value }}</span>\n </div>\n </footer>\n </article>\n </div>\n </ng-container>\n\n <ng-template #mapEmpty>\n <div class=\"section-empty\">\n <lucide-icon name=\"map-pin\" size=\"32\" class=\"section-empty__icon\"></lucide-icon>\n <p class=\"section-empty__text\">No location data provided</p>\n </div>\n </ng-template>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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 }); }
|
|
2111
|
+
}
|
|
2112
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MapSectionComponent, decorators: [{
|
|
2113
|
+
type: Component,
|
|
2114
|
+
args: [{ selector: 'app-map-section', standalone: true, imports: [CommonModule, LucideIconsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ai-section ai-section--map\">\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 <span *ngIf=\"hasItems\" class=\"ai-section__badge\">\n {{ locations.length }} {{ locations.length === 1 ? 'location' : 'locations' }}\n </span>\n </div>\n\n <div class=\"ai-section__body\">\n <ng-container *ngIf=\"hasItems; else mapEmpty\">\n <div class=\"map-grid\">\n <article\n *ngFor=\"let location of locations; trackBy: trackItem\"\n class=\"map-card\"\n role=\"button\"\n tabindex=\"0\"\n (click)=\"onLocationClick(location)\"\n (keydown.enter)=\"onLocationClick(location)\"\n (keydown.space)=\"$event.preventDefault(); onLocationClick(location)\"\n >\n <!-- Header: Title and Badge -->\n <header class=\"map-card__header\">\n <div class=\"map-card__header-content\">\n <h3 class=\"map-card__title\">\n {{ location.name }}\n </h3>\n <div class=\"map-card__badges\" *ngIf=\"location.type\">\n <span class=\"map-card__badge\">\n {{ location.type }}\n </span>\n </div>\n </div>\n </header>\n\n <!-- Address -->\n <div *ngIf=\"location.address\" class=\"map-card__address-wrapper\">\n <lucide-icon name=\"map-pin\" size=\"14\" class=\"map-card__address-icon\"></lucide-icon>\n <p class=\"map-card__address\">\n {{ location.address }}\n </p>\n </div>\n\n <!-- Meta Information -->\n <footer *ngIf=\"formatCoordinates(location) || location.value\" class=\"map-card__footer\">\n <div *ngIf=\"formatCoordinates(location) as coords\" class=\"map-card__meta\">\n <lucide-icon name=\"map-pin\" size=\"14\" class=\"map-card__meta-icon\"></lucide-icon>\n <span class=\"map-card__meta-text\">{{ coords }}</span>\n </div>\n <div *ngIf=\"location.value\" class=\"map-card__meta\">\n <lucide-icon name=\"activity\" size=\"14\" class=\"map-card__meta-icon\"></lucide-icon>\n <span class=\"map-card__meta-text\">{{ location.value }}</span>\n </div>\n </footer>\n </article>\n </div>\n </ng-container>\n\n <ng-template #mapEmpty>\n <div class=\"section-empty\">\n <lucide-icon name=\"map-pin\" size=\"32\" class=\"section-empty__icon\"></lucide-icon>\n <p class=\"section-empty__text\">No location data provided</p>\n </div>\n </ng-template>\n </div>\n</div>\n" }]
|
|
2115
|
+
}] });
|
|
2116
|
+
|
|
2117
|
+
class ChartSectionComponent extends BaseSectionComponent {
|
|
2118
|
+
constructor() {
|
|
2119
|
+
super(...arguments);
|
|
2120
|
+
this.palette = ['#FF7900', '#FF9A3C', '#CC5F00', '#FFB873', '#FFD8B0'];
|
|
2121
|
+
}
|
|
2122
|
+
get chartType() {
|
|
2123
|
+
return (this.section.chartType ?? 'bar').toLowerCase();
|
|
2124
|
+
}
|
|
2125
|
+
get fields() {
|
|
2126
|
+
const data = super.getFields();
|
|
2127
|
+
return data
|
|
2128
|
+
.filter((field) => typeof field.value === 'number')
|
|
2129
|
+
.map((field) => ({ ...field, value: Number(field.value) }));
|
|
2130
|
+
}
|
|
2131
|
+
get hasFields() {
|
|
2132
|
+
return this.fields.length > 0;
|
|
2133
|
+
}
|
|
2134
|
+
get hasData() {
|
|
2135
|
+
return this.fields.length > 0;
|
|
2136
|
+
}
|
|
2137
|
+
get maxValue() {
|
|
2138
|
+
if (!this.hasData) {
|
|
2139
|
+
return 0;
|
|
2140
|
+
}
|
|
2141
|
+
return Math.max(...this.fields.map((field) => field.value));
|
|
2142
|
+
}
|
|
2143
|
+
get totalValue() {
|
|
2144
|
+
if (!this.hasData) {
|
|
2145
|
+
return 0;
|
|
2146
|
+
}
|
|
2147
|
+
return this.fields.reduce((total, field) => total + field.value, 0);
|
|
2148
|
+
}
|
|
2149
|
+
onFieldClick(field) {
|
|
2150
|
+
this.emitFieldInteraction(field);
|
|
2151
|
+
}
|
|
2152
|
+
getBarWidth(field) {
|
|
2153
|
+
if (!this.maxValue) {
|
|
2154
|
+
return '0%';
|
|
2155
|
+
}
|
|
2156
|
+
const percentage = Math.min((field.value / this.maxValue) * 100, 100);
|
|
2157
|
+
return `${percentage}%`;
|
|
2158
|
+
}
|
|
2159
|
+
getPercentage(field) {
|
|
2160
|
+
if (!this.totalValue) {
|
|
2161
|
+
return '0%';
|
|
2162
|
+
}
|
|
2163
|
+
return `${((field.value / this.totalValue) * 100).toFixed(1)}%`;
|
|
2164
|
+
}
|
|
2165
|
+
getChartIcon() {
|
|
2166
|
+
switch (this.chartType) {
|
|
2167
|
+
case 'pie':
|
|
2168
|
+
case 'doughnut':
|
|
2169
|
+
return 'pie-chart';
|
|
2170
|
+
case 'line':
|
|
2171
|
+
return 'trending-up';
|
|
2172
|
+
default:
|
|
2173
|
+
return 'bar-chart-3';
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
getColor(field, index) {
|
|
2177
|
+
return field.color ?? this.palette[index % this.palette.length];
|
|
2178
|
+
}
|
|
2179
|
+
/**
|
|
2180
|
+
* Get display value, hiding "Streaming…" placeholder text
|
|
2181
|
+
* Inline implementation to avoid TypeScript override conflicts
|
|
2182
|
+
*/
|
|
2183
|
+
getDisplayValue(field) {
|
|
2184
|
+
const value = field.value;
|
|
2185
|
+
// Chart values are numbers, but check string representation just in case
|
|
2186
|
+
if (typeof value === 'string' && (value === 'Streaming…' || value === 'Streaming...')) {
|
|
2187
|
+
return '';
|
|
2188
|
+
}
|
|
2189
|
+
return String(value ?? '');
|
|
2190
|
+
}
|
|
2191
|
+
trackField(index, field) {
|
|
2192
|
+
return field.id ?? `${field.label ?? field.title}-${index}`;
|
|
2193
|
+
}
|
|
2194
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ChartSectionComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
2195
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: ChartSectionComponent, isStandalone: true, selector: "app-chart-section", usesInheritance: true, ngImport: i0, template: "<div class=\"ai-section ai-section--chart\">\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 <span *ngIf=\"fields.length\" class=\"ai-section__badge\">\n {{ fields.length }} {{ fields.length === 1 ? 'item' : 'items' }}\n </span>\n </div>\n\n <div class=\"ai-section__body\">\n <ng-container *ngIf=\"hasData; else chartEmpty\">\n <ng-container [ngSwitch]=\"chartType\">\n <div *ngSwitchCase=\"'pie'\" class=\"chart-section__split\">\n <div class=\"chart-section__list\">\n <article\n *ngFor=\"let field of fields; trackBy: trackField; let idx = index\"\n class=\"chart-card chart-card--pie\"\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=\"h-3 w-3 rounded-full flex-shrink-0\" [ngStyle]=\"{ backgroundColor: getColor(field, idx) }\"></div>\n <div class=\"flex-1 min-w-0\">\n <p class=\"text-sm font-semibold text-foreground\">{{ field.label || field.title }}</p>\n <p class=\"text-xs text-muted-foreground\">{{ getPercentage(field) }} · {{ getDisplayValue(field) }}</p>\n </div>\n </article>\n </div>\n <div class=\"chart-card\">\n <p>Total: <span class=\"font-semibold text-foreground\">{{ totalValue }}</span></p>\n <p class=\"mt-2\">Tap any segment to focus on the underlying metric.</p>\n </div>\n </div>\n\n <div *ngSwitchDefault class=\"chart-section__list\">\n <article\n *ngFor=\"let field of fields; trackBy: trackField; let idx = index\"\n class=\"chart-card chart-card--with-progress\"\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=\"chart-card__content-wrapper\">\n <div class=\"flex items-center justify-between gap-4 w-full\">\n <p class=\"text-sm font-semibold text-foreground\">\n {{ field.label || field.title }}\n </p>\n <span class=\"text-sm text-muted-foreground\">{{ field.value }}</span>\n </div>\n </div>\n <div class=\"chart-card__progress-wrapper\">\n <div class=\"h-2 w-full rounded-full bg-muted/30\">\n <div\n class=\"h-full rounded-full transition-all duration-500\"\n [ngStyle]=\"{ width: getBarWidth(field), backgroundColor: getColor(field, idx) }\"\n ></div>\n </div>\n </div>\n </article>\n </div>\n </ng-container>\n </ng-container>\n\n <ng-template #chartEmpty>\n <div class=\"section-empty\">\n <lucide-icon name=\"activity\" size=\"32\" class=\"mb-3 opacity-60\"></lucide-icon>\n <p class=\"text-sm\">No chart data available.</p>\n </div>\n </ng-template>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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: "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: "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 }); }
|
|
2196
|
+
}
|
|
2197
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ChartSectionComponent, decorators: [{
|
|
2198
|
+
type: Component,
|
|
2199
|
+
args: [{ selector: 'app-chart-section', standalone: true, imports: [CommonModule, LucideIconsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ai-section ai-section--chart\">\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 <span *ngIf=\"fields.length\" class=\"ai-section__badge\">\n {{ fields.length }} {{ fields.length === 1 ? 'item' : 'items' }}\n </span>\n </div>\n\n <div class=\"ai-section__body\">\n <ng-container *ngIf=\"hasData; else chartEmpty\">\n <ng-container [ngSwitch]=\"chartType\">\n <div *ngSwitchCase=\"'pie'\" class=\"chart-section__split\">\n <div class=\"chart-section__list\">\n <article\n *ngFor=\"let field of fields; trackBy: trackField; let idx = index\"\n class=\"chart-card chart-card--pie\"\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=\"h-3 w-3 rounded-full flex-shrink-0\" [ngStyle]=\"{ backgroundColor: getColor(field, idx) }\"></div>\n <div class=\"flex-1 min-w-0\">\n <p class=\"text-sm font-semibold text-foreground\">{{ field.label || field.title }}</p>\n <p class=\"text-xs text-muted-foreground\">{{ getPercentage(field) }} · {{ getDisplayValue(field) }}</p>\n </div>\n </article>\n </div>\n <div class=\"chart-card\">\n <p>Total: <span class=\"font-semibold text-foreground\">{{ totalValue }}</span></p>\n <p class=\"mt-2\">Tap any segment to focus on the underlying metric.</p>\n </div>\n </div>\n\n <div *ngSwitchDefault class=\"chart-section__list\">\n <article\n *ngFor=\"let field of fields; trackBy: trackField; let idx = index\"\n class=\"chart-card chart-card--with-progress\"\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=\"chart-card__content-wrapper\">\n <div class=\"flex items-center justify-between gap-4 w-full\">\n <p class=\"text-sm font-semibold text-foreground\">\n {{ field.label || field.title }}\n </p>\n <span class=\"text-sm text-muted-foreground\">{{ field.value }}</span>\n </div>\n </div>\n <div class=\"chart-card__progress-wrapper\">\n <div class=\"h-2 w-full rounded-full bg-muted/30\">\n <div\n class=\"h-full rounded-full transition-all duration-500\"\n [ngStyle]=\"{ width: getBarWidth(field), backgroundColor: getColor(field, idx) }\"\n ></div>\n </div>\n </div>\n </article>\n </div>\n </ng-container>\n </ng-container>\n\n <ng-template #chartEmpty>\n <div class=\"section-empty\">\n <lucide-icon name=\"activity\" size=\"32\" class=\"mb-3 opacity-60\"></lucide-icon>\n <p class=\"text-sm\">No chart data available.</p>\n </div>\n </ng-template>\n </div>\n</div>\n" }]
|
|
2200
|
+
}] });
|
|
2201
|
+
|
|
2202
|
+
class OverviewSectionComponent extends BaseSectionComponent {
|
|
2203
|
+
constructor() {
|
|
2204
|
+
super(...arguments);
|
|
2205
|
+
this.utils = inject(SectionUtilsService);
|
|
2206
|
+
}
|
|
2207
|
+
get fields() {
|
|
2208
|
+
return super.getFields();
|
|
2209
|
+
}
|
|
2210
|
+
get hasFields() {
|
|
2211
|
+
return super.hasFields;
|
|
2212
|
+
}
|
|
2213
|
+
onFieldClick(field) {
|
|
2214
|
+
this.emitFieldInteraction(field);
|
|
2215
|
+
}
|
|
2216
|
+
trackField(index, field) {
|
|
2217
|
+
return field.id ?? `${field.label}-${index}`;
|
|
2218
|
+
}
|
|
2219
|
+
getStatusClasses(status) {
|
|
2220
|
+
return this.utils.getStatusClasses(status);
|
|
2221
|
+
}
|
|
2222
|
+
/**
|
|
2223
|
+
* Get display value, hiding "Streaming…" placeholder text
|
|
2224
|
+
* Inline implementation to avoid TypeScript override conflicts
|
|
2225
|
+
*/
|
|
2226
|
+
getDisplayValue(field) {
|
|
2227
|
+
const value = field.value;
|
|
2228
|
+
if (value === 'Streaming…' || value === 'Streaming...') {
|
|
2229
|
+
return '';
|
|
2230
|
+
}
|
|
2231
|
+
return value != null ? String(value) : '';
|
|
2232
|
+
}
|
|
2233
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OverviewSectionComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
2234
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: OverviewSectionComponent, isStandalone: true, selector: "app-overview-section", usesInheritance: true, ngImport: i0, 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", dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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 }); }
|
|
2235
|
+
}
|
|
2236
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: OverviewSectionComponent, decorators: [{
|
|
2237
|
+
type: Component,
|
|
2238
|
+
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
|
+
}] });
|
|
2240
|
+
|
|
2241
|
+
class FallbackSectionComponent extends BaseSectionComponent {
|
|
2242
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FallbackSectionComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
2243
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", 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: "17.3.12", 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
|
+
class QuotationSectionComponent extends BaseSectionComponent {
|
|
2251
|
+
get fields() {
|
|
2252
|
+
return super.getFields();
|
|
2253
|
+
}
|
|
2254
|
+
get hasFields() {
|
|
2255
|
+
return super.hasFields;
|
|
2256
|
+
}
|
|
2257
|
+
trackField(index, field) {
|
|
2258
|
+
return field.id || `${field.quote?.substring(0, 20)}-${index}` || String(index);
|
|
2259
|
+
}
|
|
2260
|
+
onQuotationClick(field) {
|
|
2261
|
+
this.emitFieldInteraction(field, { sectionTitle: this.section.title });
|
|
2262
|
+
}
|
|
2263
|
+
/**
|
|
2264
|
+
* Get display quote, hiding "Streaming…" placeholder text
|
|
2265
|
+
* Inline implementation to avoid TypeScript override conflicts
|
|
2266
|
+
*/
|
|
2267
|
+
getDisplayQuote(field) {
|
|
2268
|
+
const quote = field.quote || field.value;
|
|
2269
|
+
if (quote === 'Streaming…' || quote === 'Streaming...') {
|
|
2270
|
+
return '';
|
|
2271
|
+
}
|
|
2272
|
+
return quote != null ? String(quote) : '';
|
|
2273
|
+
}
|
|
2274
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: QuotationSectionComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
2275
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: QuotationSectionComponent, isStandalone: true, selector: "app-quotation-section", usesInheritance: true, ngImport: i0, template: "<div class=\"ai-section ai-section--quotation\">\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 quotationEmpty\">\n <div class=\"section-grid section-grid--quotation\">\n <article\n *ngFor=\"let field of fields; trackBy: trackField; let idx = index\"\n class=\"section-card section-card--quotation\"\n role=\"button\"\n tabindex=\"0\"\n [class.field-streaming]=\"getFieldAnimationClass(getFieldId(field, idx), idx) === 'field-streaming'\"\n [class.field-entered]=\"getFieldAnimationClass(getFieldId(field, idx), idx) === 'field-entered'\"\n [class.field-stagger-0]=\"getFieldStaggerIndex(idx) === 0\"\n [class.field-stagger-1]=\"getFieldStaggerIndex(idx) === 1\"\n [class.field-stagger-2]=\"getFieldStaggerIndex(idx) === 2\"\n [class.field-stagger-3]=\"getFieldStaggerIndex(idx) === 3\"\n [class.field-stagger-4]=\"getFieldStaggerIndex(idx) === 4\"\n [class.field-stagger-5]=\"getFieldStaggerIndex(idx) === 5\"\n [class.field-stagger-6]=\"getFieldStaggerIndex(idx) === 6\"\n [class.field-stagger-7]=\"getFieldStaggerIndex(idx) === 7\"\n [class.field-stagger-8]=\"getFieldStaggerIndex(idx) === 8\"\n [class.field-stagger-9]=\"getFieldStaggerIndex(idx) === 9\"\n [class.field-stagger-10]=\"getFieldStaggerIndex(idx) === 10\"\n [class.field-stagger-11]=\"getFieldStaggerIndex(idx) === 11\"\n [class.field-stagger-12]=\"getFieldStaggerIndex(idx) === 12\"\n [class.field-stagger-13]=\"getFieldStaggerIndex(idx) === 13\"\n [class.field-stagger-14]=\"getFieldStaggerIndex(idx) === 14\"\n [class.field-stagger-15]=\"getFieldStaggerIndex(idx) === 15\"\n [attr.aria-label]=\"'Open quotation from ' + (field.author || 'unknown source')\"\n (click)=\"onQuotationClick(field)\"\n (keydown.enter)=\"onQuotationClick(field)\"\n (keydown.space)=\"$event.preventDefault(); onQuotationClick(field)\"\n >\n <div class=\"quotation-card__quote\">\n <lucide-icon name=\"quote\" [size]=\"24\" class=\"quotation-card__icon\" aria-hidden=\"true\"></lucide-icon>\n <blockquote class=\"quotation-card__text\">\n {{ getDisplayQuote(field) }}\n </blockquote>\n </div>\n\n <footer class=\"quotation-card__footer\" *ngIf=\"field.author || field.source || field.date\">\n <div class=\"quotation-card__author\" *ngIf=\"field.author\">\n <span class=\"quotation-card__author-name\">{{ field.author }}</span>\n <span class=\"quotation-card__author-role\" *ngIf=\"field.source\">{{ field.source }}</span>\n </div>\n <time class=\"quotation-card__date\" *ngIf=\"field.date\">{{ field.date }}</time>\n </footer>\n </article>\n </div>\n </ng-container>\n\n <ng-template #quotationEmpty>\n <div class=\"section-empty\">\n <lucide-icon name=\"message-square\" [size]=\"32\" class=\"mb-4 opacity-50\" aria-hidden=\"true\"></lucide-icon>\n <p class=\"text-sm\">No quotations available</p>\n </div>\n </ng-template>\n </div>\n</div>\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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 }); }
|
|
2276
|
+
}
|
|
2277
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: QuotationSectionComponent, decorators: [{
|
|
2278
|
+
type: Component,
|
|
2279
|
+
args: [{ selector: 'app-quotation-section', standalone: true, imports: [CommonModule, LucideIconsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ai-section ai-section--quotation\">\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 quotationEmpty\">\n <div class=\"section-grid section-grid--quotation\">\n <article\n *ngFor=\"let field of fields; trackBy: trackField; let idx = index\"\n class=\"section-card section-card--quotation\"\n role=\"button\"\n tabindex=\"0\"\n [class.field-streaming]=\"getFieldAnimationClass(getFieldId(field, idx), idx) === 'field-streaming'\"\n [class.field-entered]=\"getFieldAnimationClass(getFieldId(field, idx), idx) === 'field-entered'\"\n [class.field-stagger-0]=\"getFieldStaggerIndex(idx) === 0\"\n [class.field-stagger-1]=\"getFieldStaggerIndex(idx) === 1\"\n [class.field-stagger-2]=\"getFieldStaggerIndex(idx) === 2\"\n [class.field-stagger-3]=\"getFieldStaggerIndex(idx) === 3\"\n [class.field-stagger-4]=\"getFieldStaggerIndex(idx) === 4\"\n [class.field-stagger-5]=\"getFieldStaggerIndex(idx) === 5\"\n [class.field-stagger-6]=\"getFieldStaggerIndex(idx) === 6\"\n [class.field-stagger-7]=\"getFieldStaggerIndex(idx) === 7\"\n [class.field-stagger-8]=\"getFieldStaggerIndex(idx) === 8\"\n [class.field-stagger-9]=\"getFieldStaggerIndex(idx) === 9\"\n [class.field-stagger-10]=\"getFieldStaggerIndex(idx) === 10\"\n [class.field-stagger-11]=\"getFieldStaggerIndex(idx) === 11\"\n [class.field-stagger-12]=\"getFieldStaggerIndex(idx) === 12\"\n [class.field-stagger-13]=\"getFieldStaggerIndex(idx) === 13\"\n [class.field-stagger-14]=\"getFieldStaggerIndex(idx) === 14\"\n [class.field-stagger-15]=\"getFieldStaggerIndex(idx) === 15\"\n [attr.aria-label]=\"'Open quotation from ' + (field.author || 'unknown source')\"\n (click)=\"onQuotationClick(field)\"\n (keydown.enter)=\"onQuotationClick(field)\"\n (keydown.space)=\"$event.preventDefault(); onQuotationClick(field)\"\n >\n <div class=\"quotation-card__quote\">\n <lucide-icon name=\"quote\" [size]=\"24\" class=\"quotation-card__icon\" aria-hidden=\"true\"></lucide-icon>\n <blockquote class=\"quotation-card__text\">\n {{ getDisplayQuote(field) }}\n </blockquote>\n </div>\n\n <footer class=\"quotation-card__footer\" *ngIf=\"field.author || field.source || field.date\">\n <div class=\"quotation-card__author\" *ngIf=\"field.author\">\n <span class=\"quotation-card__author-name\">{{ field.author }}</span>\n <span class=\"quotation-card__author-role\" *ngIf=\"field.source\">{{ field.source }}</span>\n </div>\n <time class=\"quotation-card__date\" *ngIf=\"field.date\">{{ field.date }}</time>\n </footer>\n </article>\n </div>\n </ng-container>\n\n <ng-template #quotationEmpty>\n <div class=\"section-empty\">\n <lucide-icon name=\"message-square\" [size]=\"32\" class=\"mb-4 opacity-50\" aria-hidden=\"true\"></lucide-icon>\n <p class=\"text-sm\">No quotations available</p>\n </div>\n </ng-template>\n </div>\n</div>\n\n" }]
|
|
2280
|
+
}] });
|
|
2281
|
+
|
|
2282
|
+
class TextReferenceSectionComponent extends BaseSectionComponent {
|
|
2283
|
+
get fields() {
|
|
2284
|
+
return super.getFields();
|
|
2285
|
+
}
|
|
2286
|
+
get hasFields() {
|
|
2287
|
+
return super.hasFields;
|
|
2288
|
+
}
|
|
2289
|
+
onReferenceClick(field) {
|
|
2290
|
+
this.emitFieldInteraction(field, { sectionTitle: this.section.title });
|
|
2291
|
+
}
|
|
2292
|
+
openReference(field, event) {
|
|
2293
|
+
if (field.url) {
|
|
2294
|
+
event.stopPropagation();
|
|
2295
|
+
window.open(field.url, '_blank', 'noopener,noreferrer');
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
/**
|
|
2299
|
+
* Get display text, hiding "Streaming…" placeholder text
|
|
2300
|
+
* Inline implementation to avoid TypeScript override conflicts
|
|
2301
|
+
*/
|
|
2302
|
+
getDisplayText(field) {
|
|
2303
|
+
const text = field.text || field.value;
|
|
2304
|
+
if (text === 'Streaming…' || text === 'Streaming...') {
|
|
2305
|
+
return '';
|
|
2306
|
+
}
|
|
2307
|
+
return text != null ? String(text) : '';
|
|
2308
|
+
}
|
|
2309
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TextReferenceSectionComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
2310
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: TextReferenceSectionComponent, isStandalone: true, selector: "app-text-reference-section", usesInheritance: true, ngImport: i0, template: "<div class=\"ai-section ai-section--text-reference\">\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 referenceEmpty\">\n <div class=\"section-grid section-grid--reference\">\n <article\n *ngFor=\"let field of fields\"\n class=\"section-card section-card--reference\"\n role=\"button\"\n tabindex=\"0\"\n [attr.aria-label]=\"'Open reference ' + (field.title || field.category || 'details')\"\n (click)=\"onReferenceClick(field)\"\n (keydown.enter)=\"onReferenceClick(field)\"\n (keydown.space)=\"$event.preventDefault(); onReferenceClick(field)\"\n >\n <div class=\"reference-card__content\">\n <p class=\"reference-card__text\">\n {{ getDisplayText(field) }}\n </p>\n </div>\n\n <footer class=\"reference-card__footer\" *ngIf=\"field.referenceText || field.source || field.date || field.category\">\n <div class=\"reference-card__meta\">\n <span class=\"reference-card__category\" *ngIf=\"field.category\">{{ field.category }}</span>\n <span class=\"reference-card__source\" *ngIf=\"field.source\">{{ field.source }}</span>\n <span class=\"reference-card__reference\" *ngIf=\"field.referenceText\">{{ field.referenceText }}</span>\n <time class=\"reference-card__date\" *ngIf=\"field.date\">{{ field.date }}</time>\n </div>\n <a\n *ngIf=\"field.url\"\n href=\"{{ field.url }}\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"reference-card__link\"\n (click)=\"openReference(field, $event)\"\n >\n <lucide-icon name=\"external-link\" [size]=\"14\" aria-hidden=\"true\"></lucide-icon>\n <span>View source</span>\n </a>\n </footer>\n </article>\n </div>\n </ng-container>\n\n <ng-template #referenceEmpty>\n <div class=\"section-empty\">\n <lucide-icon name=\"file-text\" [size]=\"32\" class=\"mb-4 opacity-50\" aria-hidden=\"true\"></lucide-icon>\n <p class=\"text-sm\">No references available</p>\n </div>\n </ng-template>\n </div>\n</div>\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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 }); }
|
|
2311
|
+
}
|
|
2312
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TextReferenceSectionComponent, decorators: [{
|
|
2313
|
+
type: Component,
|
|
2314
|
+
args: [{ selector: 'app-text-reference-section', standalone: true, imports: [CommonModule, LucideIconsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ai-section ai-section--text-reference\">\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 referenceEmpty\">\n <div class=\"section-grid section-grid--reference\">\n <article\n *ngFor=\"let field of fields\"\n class=\"section-card section-card--reference\"\n role=\"button\"\n tabindex=\"0\"\n [attr.aria-label]=\"'Open reference ' + (field.title || field.category || 'details')\"\n (click)=\"onReferenceClick(field)\"\n (keydown.enter)=\"onReferenceClick(field)\"\n (keydown.space)=\"$event.preventDefault(); onReferenceClick(field)\"\n >\n <div class=\"reference-card__content\">\n <p class=\"reference-card__text\">\n {{ getDisplayText(field) }}\n </p>\n </div>\n\n <footer class=\"reference-card__footer\" *ngIf=\"field.referenceText || field.source || field.date || field.category\">\n <div class=\"reference-card__meta\">\n <span class=\"reference-card__category\" *ngIf=\"field.category\">{{ field.category }}</span>\n <span class=\"reference-card__source\" *ngIf=\"field.source\">{{ field.source }}</span>\n <span class=\"reference-card__reference\" *ngIf=\"field.referenceText\">{{ field.referenceText }}</span>\n <time class=\"reference-card__date\" *ngIf=\"field.date\">{{ field.date }}</time>\n </div>\n <a\n *ngIf=\"field.url\"\n href=\"{{ field.url }}\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"reference-card__link\"\n (click)=\"openReference(field, $event)\"\n >\n <lucide-icon name=\"external-link\" [size]=\"14\" aria-hidden=\"true\"></lucide-icon>\n <span>View source</span>\n </a>\n </footer>\n </article>\n </div>\n </ng-container>\n\n <ng-template #referenceEmpty>\n <div class=\"section-empty\">\n <lucide-icon name=\"file-text\" [size]=\"32\" class=\"mb-4 opacity-50\" aria-hidden=\"true\"></lucide-icon>\n <p class=\"text-sm\">No references available</p>\n </div>\n </ng-template>\n </div>\n</div>\n\n" }]
|
|
2315
|
+
}] });
|
|
2316
|
+
|
|
2317
|
+
class BrandColorsSectionComponent extends BaseSectionComponent {
|
|
2318
|
+
constructor() {
|
|
2319
|
+
super(...arguments);
|
|
2320
|
+
this.brandColors = [];
|
|
2321
|
+
this.copiedColorId = null;
|
|
2322
|
+
}
|
|
2323
|
+
ngOnChanges(changes) {
|
|
2324
|
+
super.ngOnChanges(changes);
|
|
2325
|
+
if (changes['section']) {
|
|
2326
|
+
this.extractBrandColors();
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
extractBrandColors() {
|
|
2330
|
+
const fields = this.getFields();
|
|
2331
|
+
const colors = [];
|
|
2332
|
+
fields.forEach((field, index) => {
|
|
2333
|
+
if (field.value && typeof field.value === 'string') {
|
|
2334
|
+
// Check if it's a hex color
|
|
2335
|
+
if (this.isHexColor(field.value)) {
|
|
2336
|
+
colors.push({
|
|
2337
|
+
id: field.id || `color-${index}`,
|
|
2338
|
+
label: field.label || field.title || `Color ${index + 1}`,
|
|
2339
|
+
hex: field.value.toUpperCase(),
|
|
2340
|
+
rgb: this.hexToRgb(field.value),
|
|
2341
|
+
copied: false
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
});
|
|
2346
|
+
this.brandColors = colors;
|
|
2347
|
+
}
|
|
2348
|
+
isHexColor(value) {
|
|
2349
|
+
return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(value);
|
|
2350
|
+
}
|
|
2351
|
+
hexToRgb(hex) {
|
|
2352
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
2353
|
+
if (result) {
|
|
2354
|
+
const r = parseInt(result[1], 16);
|
|
2355
|
+
const g = parseInt(result[2], 16);
|
|
2356
|
+
const b = parseInt(result[3], 16);
|
|
2357
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
2358
|
+
}
|
|
2359
|
+
return '';
|
|
2360
|
+
}
|
|
2361
|
+
async copyToClipboard(color) {
|
|
2362
|
+
try {
|
|
2363
|
+
await navigator.clipboard.writeText(color.hex);
|
|
2364
|
+
this.copiedColorId = color.id;
|
|
2365
|
+
// Reset after 2 seconds
|
|
2366
|
+
setTimeout(() => {
|
|
2367
|
+
this.copiedColorId = null;
|
|
2368
|
+
this.cdr.markForCheck();
|
|
2369
|
+
}, 2000);
|
|
2370
|
+
this.cdr.markForCheck();
|
|
2371
|
+
}
|
|
2372
|
+
catch (err) {
|
|
2373
|
+
console.error('Failed to copy to clipboard:', err);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
get hasColors() {
|
|
2377
|
+
return this.brandColors.length > 0;
|
|
2378
|
+
}
|
|
2379
|
+
get fields() {
|
|
2380
|
+
return this.getFields();
|
|
2381
|
+
}
|
|
2382
|
+
get hasFields() {
|
|
2383
|
+
return this.hasColors;
|
|
2384
|
+
}
|
|
2385
|
+
onColorClick(color) {
|
|
2386
|
+
this.copyToClipboard(color);
|
|
2387
|
+
}
|
|
2388
|
+
trackColor(index, color) {
|
|
2389
|
+
return color.id;
|
|
2390
|
+
}
|
|
2391
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: BrandColorsSectionComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
2392
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: BrandColorsSectionComponent, isStandalone: true, selector: "app-brand-colors-section", usesInheritance: true, usesOnChanges: true, ngImport: i0, template: "<div class=\"ai-section ai-section--brand-colors\">\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=\"hasColors; else brandColorsEmpty\">\n <div class=\"brand-colors-grid\">\n <div\n *ngFor=\"let color of brandColors; trackBy: trackColor\"\n class=\"brand-color-tile\"\n role=\"button\"\n tabindex=\"0\"\n (click)=\"onColorClick(color)\"\n (keydown.enter)=\"onColorClick(color)\"\n (keydown.space)=\"$event.preventDefault(); onColorClick(color)\"\n [attr.aria-label]=\"'Copy ' + color.label + ' color code ' + color.hex\"\n >\n <div \n class=\"brand-color-tile__swatch\"\n [style.background-color]=\"color.hex\"\n [class.brand-color-tile__swatch--copied]=\"copiedColorId === color.id\"\n >\n <div *ngIf=\"copiedColorId === color.id\" class=\"brand-color-tile__check\">\n <lucide-icon name=\"check\" [size]=\"24\" class=\"check-icon\"></lucide-icon>\n </div>\n </div>\n \n <div class=\"brand-color-tile__info\">\n <span class=\"brand-color-tile__label\">{{ color.label }}</span>\n <div class=\"brand-color-tile__codes\">\n <code class=\"brand-color-tile__code brand-color-tile__code--hex\">{{ color.hex }}</code>\n <code *ngIf=\"color.rgb\" class=\"brand-color-tile__code brand-color-tile__code--rgb\">{{ color.rgb }}</code>\n </div>\n <div *ngIf=\"copiedColorId === color.id\" class=\"brand-color-tile__copied-text\">\n <lucide-icon name=\"copy\" [size]=\"14\"></lucide-icon>\n Copied!\n </div>\n </div>\n </div>\n </div>\n </ng-container>\n\n <ng-template #brandColorsEmpty>\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 brand colors available</p>\n </div>\n </ng-template>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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 }); }
|
|
2393
|
+
}
|
|
2394
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: BrandColorsSectionComponent, decorators: [{
|
|
2395
|
+
type: Component,
|
|
2396
|
+
args: [{ selector: 'app-brand-colors-section', standalone: true, imports: [CommonModule, LucideIconsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ai-section ai-section--brand-colors\">\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=\"hasColors; else brandColorsEmpty\">\n <div class=\"brand-colors-grid\">\n <div\n *ngFor=\"let color of brandColors; trackBy: trackColor\"\n class=\"brand-color-tile\"\n role=\"button\"\n tabindex=\"0\"\n (click)=\"onColorClick(color)\"\n (keydown.enter)=\"onColorClick(color)\"\n (keydown.space)=\"$event.preventDefault(); onColorClick(color)\"\n [attr.aria-label]=\"'Copy ' + color.label + ' color code ' + color.hex\"\n >\n <div \n class=\"brand-color-tile__swatch\"\n [style.background-color]=\"color.hex\"\n [class.brand-color-tile__swatch--copied]=\"copiedColorId === color.id\"\n >\n <div *ngIf=\"copiedColorId === color.id\" class=\"brand-color-tile__check\">\n <lucide-icon name=\"check\" [size]=\"24\" class=\"check-icon\"></lucide-icon>\n </div>\n </div>\n \n <div class=\"brand-color-tile__info\">\n <span class=\"brand-color-tile__label\">{{ color.label }}</span>\n <div class=\"brand-color-tile__codes\">\n <code class=\"brand-color-tile__code brand-color-tile__code--hex\">{{ color.hex }}</code>\n <code *ngIf=\"color.rgb\" class=\"brand-color-tile__code brand-color-tile__code--rgb\">{{ color.rgb }}</code>\n </div>\n <div *ngIf=\"copiedColorId === color.id\" class=\"brand-color-tile__copied-text\">\n <lucide-icon name=\"copy\" [size]=\"14\"></lucide-icon>\n Copied!\n </div>\n </div>\n </div>\n </div>\n </ng-container>\n\n <ng-template #brandColorsEmpty>\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 brand colors available</p>\n </div>\n </ng-template>\n </div>\n</div>\n" }]
|
|
2397
|
+
}] });
|
|
2398
|
+
|
|
2399
|
+
class SectionRendererComponent {
|
|
2400
|
+
constructor() {
|
|
2401
|
+
this.sectionEvent = new EventEmitter();
|
|
2402
|
+
}
|
|
2403
|
+
// Removed @HostBinding - will be set in template instead to avoid setAttribute errors
|
|
2404
|
+
get sectionTypeAttribute() {
|
|
2405
|
+
if (!this.section) {
|
|
2406
|
+
return 'unknown';
|
|
2407
|
+
}
|
|
2408
|
+
try {
|
|
2409
|
+
const typeLabel = (this.section.type ?? '').trim();
|
|
2410
|
+
const resolved = this.resolvedType;
|
|
2411
|
+
return (typeLabel || resolved || 'unknown').toLowerCase();
|
|
2412
|
+
}
|
|
2413
|
+
catch {
|
|
2414
|
+
return 'unknown';
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
get sectionIdAttribute() {
|
|
2418
|
+
if (!this.section?.id) {
|
|
2419
|
+
return null;
|
|
2420
|
+
}
|
|
2421
|
+
try {
|
|
2422
|
+
return String(this.section.id);
|
|
2423
|
+
}
|
|
2424
|
+
catch {
|
|
2425
|
+
return null;
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
get resolvedType() {
|
|
2429
|
+
if (!this.section) {
|
|
2430
|
+
return 'unknown';
|
|
2431
|
+
}
|
|
2432
|
+
try {
|
|
2433
|
+
const type = (this.section.type ?? '').toLowerCase();
|
|
2434
|
+
const title = (this.section.title ?? '').toLowerCase();
|
|
2435
|
+
if (type === 'info' && title.includes('overview')) {
|
|
2436
|
+
return 'overview';
|
|
2437
|
+
}
|
|
2438
|
+
if (type === 'timeline') {
|
|
2439
|
+
return 'event';
|
|
2440
|
+
}
|
|
2441
|
+
if (type === 'metrics' || type === 'stats') {
|
|
2442
|
+
return 'analytics';
|
|
2443
|
+
}
|
|
2444
|
+
if (type === 'table') {
|
|
2445
|
+
return 'list';
|
|
2446
|
+
}
|
|
2447
|
+
if (type === 'project') {
|
|
2448
|
+
return 'info';
|
|
2449
|
+
}
|
|
2450
|
+
if (type === 'locations') {
|
|
2451
|
+
return 'map';
|
|
2452
|
+
}
|
|
2453
|
+
if (type === 'quotation' || type === 'quote') {
|
|
2454
|
+
return 'quotation';
|
|
2455
|
+
}
|
|
2456
|
+
if (type === 'text-reference' || type === 'reference' || type === 'text-ref') {
|
|
2457
|
+
return 'text-reference';
|
|
2458
|
+
}
|
|
2459
|
+
if (type === 'brand-colors' || type === 'brands' || type === 'colors') {
|
|
2460
|
+
return 'brand-colors';
|
|
2461
|
+
}
|
|
2462
|
+
if (!type) {
|
|
2463
|
+
if (title.includes('overview')) {
|
|
2464
|
+
return 'overview';
|
|
2465
|
+
}
|
|
2466
|
+
return 'fallback';
|
|
2467
|
+
}
|
|
2468
|
+
return type;
|
|
2469
|
+
}
|
|
2470
|
+
catch {
|
|
2471
|
+
return 'unknown';
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
onInfoFieldInteraction(event) {
|
|
2475
|
+
this.sectionEvent.emit({
|
|
2476
|
+
type: 'field',
|
|
2477
|
+
section: this.section,
|
|
2478
|
+
field: event.field,
|
|
2479
|
+
metadata: { sectionTitle: event.sectionTitle }
|
|
2480
|
+
});
|
|
2481
|
+
}
|
|
2482
|
+
emitFieldInteraction(field, metadata) {
|
|
2483
|
+
this.sectionEvent.emit({
|
|
2484
|
+
type: 'field',
|
|
2485
|
+
section: this.section,
|
|
2486
|
+
field,
|
|
2487
|
+
metadata: {
|
|
2488
|
+
sectionId: this.section.id,
|
|
2489
|
+
sectionTitle: this.section.title,
|
|
2490
|
+
...metadata
|
|
2491
|
+
}
|
|
2492
|
+
});
|
|
2493
|
+
}
|
|
2494
|
+
emitItemInteraction(item, metadata) {
|
|
2495
|
+
this.sectionEvent.emit({
|
|
2496
|
+
type: 'item',
|
|
2497
|
+
section: this.section,
|
|
2498
|
+
item,
|
|
2499
|
+
metadata: {
|
|
2500
|
+
sectionId: this.section.id,
|
|
2501
|
+
sectionTitle: this.section.title,
|
|
2502
|
+
...metadata
|
|
2503
|
+
}
|
|
2504
|
+
});
|
|
2505
|
+
}
|
|
2506
|
+
emitActionInteraction(action, metadata) {
|
|
2507
|
+
this.sectionEvent.emit({
|
|
2508
|
+
type: 'action',
|
|
2509
|
+
section: this.section,
|
|
2510
|
+
action,
|
|
2511
|
+
metadata
|
|
2512
|
+
});
|
|
2513
|
+
}
|
|
2514
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SectionRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2515
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", 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 }); }
|
|
2516
|
+
}
|
|
2517
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SectionRendererComponent, decorators: [{
|
|
2518
|
+
type: Component,
|
|
2519
|
+
args: [{ selector: 'app-section-renderer', standalone: true, imports: [
|
|
2520
|
+
CommonModule,
|
|
2521
|
+
InfoSectionComponent,
|
|
2522
|
+
AnalyticsSectionComponent,
|
|
2523
|
+
FinancialsSectionComponent,
|
|
2524
|
+
ListSectionComponent,
|
|
2525
|
+
EventSectionComponent,
|
|
2526
|
+
ProductSectionComponent,
|
|
2527
|
+
SolutionsSectionComponent,
|
|
2528
|
+
ContactCardSectionComponent,
|
|
2529
|
+
NetworkCardSectionComponent,
|
|
2530
|
+
MapSectionComponent,
|
|
2531
|
+
ChartSectionComponent,
|
|
2532
|
+
OverviewSectionComponent,
|
|
2533
|
+
FallbackSectionComponent,
|
|
2534
|
+
QuotationSectionComponent,
|
|
2535
|
+
TextReferenceSectionComponent,
|
|
2536
|
+
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"] }]
|
|
2538
|
+
}], propDecorators: { section: [{
|
|
2539
|
+
type: Input,
|
|
2540
|
+
args: [{ required: true }]
|
|
2541
|
+
}], sectionEvent: [{
|
|
2542
|
+
type: Output
|
|
2543
|
+
}] } });
|
|
2544
|
+
|
|
2545
|
+
const DEFAULT_COL_SPAN_THRESHOLD = { two: 6 };
|
|
2546
|
+
class MasonryGridComponent {
|
|
2547
|
+
constructor() {
|
|
2548
|
+
this.sections = [];
|
|
2549
|
+
this.gap = 12; // Harmonize with section grid tokens for consistent gutters
|
|
2550
|
+
this.minColumnWidth = 260; // Keep cards readable when columns increase
|
|
2551
|
+
this.maxColumns = 4; // Allow wider canvases to display four columns for better uniformity
|
|
2552
|
+
this.sectionEvent = new EventEmitter();
|
|
2553
|
+
this.layoutChange = new EventEmitter();
|
|
2554
|
+
this.cdr = inject(ChangeDetectorRef);
|
|
2555
|
+
this.positionedSections = [];
|
|
2556
|
+
this.containerHeight = 0;
|
|
2557
|
+
this.isLayoutReady = false; // Prevent FOUC (Flash of Unstyled Content)
|
|
2558
|
+
this.reflowCount = 0;
|
|
2559
|
+
this.MAX_REFLOWS = 3; // Reduced for faster initial layout
|
|
2560
|
+
this.RESIZE_THROTTLE_MS = 16; // ~1 frame at 60fps for minimal throttling
|
|
2561
|
+
this.trackItem = (_, item) => item.key;
|
|
2562
|
+
}
|
|
2563
|
+
ngOnChanges(changes) {
|
|
2564
|
+
if (changes['sections']) {
|
|
2565
|
+
this.computeInitialLayout();
|
|
2566
|
+
// Schedule immediate layout update for section changes
|
|
2567
|
+
this.scheduleLayoutUpdate();
|
|
2568
|
+
this.cdr.markForCheck();
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
ngAfterViewInit() {
|
|
2572
|
+
this.computeInitialLayout();
|
|
2573
|
+
this.observeContainer();
|
|
2574
|
+
this.observeItems();
|
|
2575
|
+
// Immediate reflow using RAF chain for fastest layout
|
|
2576
|
+
requestAnimationFrame(() => {
|
|
2577
|
+
this.reflowWithActualHeights();
|
|
2578
|
+
requestAnimationFrame(() => {
|
|
2579
|
+
this.reflowWithActualHeights();
|
|
2580
|
+
// Mark layout as ready after second reflow
|
|
2581
|
+
this.isLayoutReady = true;
|
|
2582
|
+
this.cdr.markForCheck();
|
|
2583
|
+
});
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2586
|
+
ngOnDestroy() {
|
|
2587
|
+
this.resizeObserver?.disconnect();
|
|
2588
|
+
this.itemObserver?.disconnect();
|
|
2589
|
+
if (this.pendingAnimationFrame) {
|
|
2590
|
+
cancelAnimationFrame(this.pendingAnimationFrame);
|
|
2591
|
+
}
|
|
2592
|
+
if (this.rafId) {
|
|
2593
|
+
cancelAnimationFrame(this.rafId);
|
|
2594
|
+
}
|
|
2595
|
+
if (this.resizeThrottleTimeout) {
|
|
2596
|
+
clearTimeout(this.resizeThrottleTimeout);
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
onSectionEvent(event) {
|
|
2600
|
+
this.sectionEvent.emit(event);
|
|
2601
|
+
}
|
|
2602
|
+
/**
|
|
2603
|
+
* Gets a unique section ID for scrolling
|
|
2604
|
+
*/
|
|
2605
|
+
getSectionId(section) {
|
|
2606
|
+
return `section-${this.sanitizeSectionId(section.title || section.id || 'unknown')}`;
|
|
2607
|
+
}
|
|
2608
|
+
/**
|
|
2609
|
+
* Sanitizes section title for use as HTML ID
|
|
2610
|
+
*/
|
|
2611
|
+
sanitizeSectionId(title) {
|
|
2612
|
+
return title.toLowerCase()
|
|
2613
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
2614
|
+
.replace(/^-|-$/g, '');
|
|
2615
|
+
}
|
|
2616
|
+
observeContainer() {
|
|
2617
|
+
if (typeof ResizeObserver === 'undefined' || !this.containerRef) {
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
this.resizeObserver = new ResizeObserver(() => this.throttledScheduleLayoutUpdate());
|
|
2621
|
+
this.resizeObserver.observe(this.containerRef.nativeElement);
|
|
2622
|
+
}
|
|
2623
|
+
observeItems() {
|
|
2624
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
2625
|
+
return;
|
|
2626
|
+
}
|
|
2627
|
+
this.itemObserver = new ResizeObserver(() => this.throttledScheduleLayoutUpdate());
|
|
2628
|
+
this.itemRefs.changes.subscribe((items) => {
|
|
2629
|
+
this.itemObserver?.disconnect();
|
|
2630
|
+
items.forEach((item) => this.itemObserver?.observe(item.nativeElement));
|
|
2631
|
+
this.scheduleLayoutUpdate();
|
|
2632
|
+
});
|
|
2633
|
+
this.itemRefs.forEach((item) => this.itemObserver?.observe(item.nativeElement));
|
|
2634
|
+
}
|
|
2635
|
+
throttledScheduleLayoutUpdate() {
|
|
2636
|
+
if (this.resizeThrottleTimeout) {
|
|
2637
|
+
return;
|
|
2638
|
+
}
|
|
2639
|
+
this.resizeThrottleTimeout = window.setTimeout(() => {
|
|
2640
|
+
this.resizeThrottleTimeout = undefined;
|
|
2641
|
+
this.scheduleLayoutUpdate();
|
|
2642
|
+
}, this.RESIZE_THROTTLE_MS);
|
|
2643
|
+
}
|
|
2644
|
+
scheduleLayoutUpdate() {
|
|
2645
|
+
// Cancel any pending RAF
|
|
2646
|
+
if (this.rafId) {
|
|
2647
|
+
cancelAnimationFrame(this.rafId);
|
|
2648
|
+
}
|
|
2649
|
+
if (this.pendingAnimationFrame) {
|
|
2650
|
+
cancelAnimationFrame(this.pendingAnimationFrame);
|
|
2651
|
+
}
|
|
2652
|
+
// Reset reflow counter for new layout calculation
|
|
2653
|
+
this.reflowCount = 0;
|
|
2654
|
+
// Use immediate RAF for fastest response
|
|
2655
|
+
this.rafId = requestAnimationFrame(() => {
|
|
2656
|
+
this.rafId = undefined;
|
|
2657
|
+
this.pendingAnimationFrame = requestAnimationFrame(() => {
|
|
2658
|
+
this.pendingAnimationFrame = undefined;
|
|
2659
|
+
this.reflowWithActualHeights();
|
|
2660
|
+
});
|
|
2661
|
+
});
|
|
2662
|
+
}
|
|
2663
|
+
computeInitialLayout() {
|
|
2664
|
+
const resolvedSections = this.sections ?? [];
|
|
2665
|
+
this.reflowCount = 0;
|
|
2666
|
+
this.containerHeight = 0;
|
|
2667
|
+
this.isLayoutReady = false; // Reset layout ready state
|
|
2668
|
+
// Stack sections vertically initially to prevent overlap
|
|
2669
|
+
let cumulativeTop = 0;
|
|
2670
|
+
this.positionedSections = resolvedSections.map((section, index) => {
|
|
2671
|
+
const item = {
|
|
2672
|
+
section,
|
|
2673
|
+
key: section.id ?? `${section.title}-${index}`,
|
|
2674
|
+
colSpan: this.getSectionColSpan(section),
|
|
2675
|
+
left: '0px',
|
|
2676
|
+
top: cumulativeTop,
|
|
2677
|
+
width: '100%'
|
|
2678
|
+
};
|
|
2679
|
+
// Add estimated spacing (will be recalculated with actual heights)
|
|
2680
|
+
cumulativeTop += 300 + this.gap;
|
|
2681
|
+
return item;
|
|
2682
|
+
});
|
|
2683
|
+
this.containerHeight = cumulativeTop;
|
|
2684
|
+
this.cdr.markForCheck();
|
|
2685
|
+
}
|
|
2686
|
+
reflowWithActualHeights() {
|
|
2687
|
+
if (!this.containerRef?.nativeElement || this.reflowCount >= this.MAX_REFLOWS) {
|
|
2688
|
+
return;
|
|
2689
|
+
}
|
|
2690
|
+
this.reflowCount++;
|
|
2691
|
+
const containerElement = this.containerRef.nativeElement;
|
|
2692
|
+
if (!containerElement || typeof containerElement.clientWidth === 'undefined') {
|
|
2693
|
+
return;
|
|
2694
|
+
}
|
|
2695
|
+
const containerWidth = containerElement.clientWidth;
|
|
2696
|
+
if (!containerWidth) {
|
|
2697
|
+
return;
|
|
2698
|
+
}
|
|
2699
|
+
// Smart responsive column calculation that adapts continuously
|
|
2700
|
+
const columns = Math.min(this.maxColumns, Math.max(1, Math.floor((containerWidth + this.gap) / (this.minColumnWidth + this.gap))));
|
|
2701
|
+
// Expose column count as CSS custom property for section grids to consume
|
|
2702
|
+
if (containerElement.style && typeof containerElement.style.setProperty === 'function') {
|
|
2703
|
+
containerElement.style.setProperty('--masonry-columns', columns.toString());
|
|
2704
|
+
}
|
|
2705
|
+
this.emitLayoutInfo(columns, containerWidth);
|
|
2706
|
+
const colHeights = Array(columns).fill(0);
|
|
2707
|
+
let hasZeroHeights = false;
|
|
2708
|
+
const itemRefArray = this.itemRefs?.toArray() ?? [];
|
|
2709
|
+
// Pre-calculate gap and column width expressions once
|
|
2710
|
+
const gapTotal = this.gap * (columns - 1);
|
|
2711
|
+
const columnWidthExpr = `calc((100% - ${gapTotal}px) / ${columns})`;
|
|
2712
|
+
const updated = this.positionedSections.map((item, index) => {
|
|
2713
|
+
const colSpan = Math.min(item.colSpan, columns);
|
|
2714
|
+
let bestColumn = 0;
|
|
2715
|
+
let minHeight = Number.MAX_VALUE;
|
|
2716
|
+
// Optimized: Find best column more efficiently
|
|
2717
|
+
for (let col = 0; col <= columns - colSpan; col += 1) {
|
|
2718
|
+
let maxColHeight = 0;
|
|
2719
|
+
for (let c = col; c < col + colSpan; c++) {
|
|
2720
|
+
if (colHeights[c] > maxColHeight) {
|
|
2721
|
+
maxColHeight = colHeights[c];
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
if (maxColHeight < minHeight) {
|
|
2725
|
+
minHeight = maxColHeight;
|
|
2726
|
+
bestColumn = col;
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
// Calculate width and left expressions
|
|
2730
|
+
const widthExpr = colSpan === 1
|
|
2731
|
+
? columnWidthExpr
|
|
2732
|
+
: `calc(${columnWidthExpr} * ${colSpan} + ${this.gap * (colSpan - 1)}px)`;
|
|
2733
|
+
const leftExpr = `calc((${columnWidthExpr} + ${this.gap}px) * ${bestColumn})`;
|
|
2734
|
+
// Get actual rendered height from DOM element
|
|
2735
|
+
const itemElement = itemRefArray[index]?.nativeElement;
|
|
2736
|
+
let height = itemElement?.offsetHeight ?? 0;
|
|
2737
|
+
// If height is 0, try to get the first child's height (the section renderer content)
|
|
2738
|
+
if (height === 0 && itemElement?.firstElementChild) {
|
|
2739
|
+
height = itemElement.firstElementChild.offsetHeight ?? 0;
|
|
2740
|
+
}
|
|
2741
|
+
// Still 0? Use a reasonable minimum
|
|
2742
|
+
if (height === 0) {
|
|
2743
|
+
height = 200;
|
|
2744
|
+
hasZeroHeights = true;
|
|
2745
|
+
}
|
|
2746
|
+
// Update column heights
|
|
2747
|
+
for (let col = bestColumn; col < bestColumn + colSpan; col += 1) {
|
|
2748
|
+
colHeights[col] = minHeight + height + this.gap;
|
|
2749
|
+
}
|
|
2750
|
+
return {
|
|
2751
|
+
...item,
|
|
2752
|
+
colSpan,
|
|
2753
|
+
left: leftExpr,
|
|
2754
|
+
top: minHeight,
|
|
2755
|
+
width: widthExpr
|
|
2756
|
+
};
|
|
2757
|
+
});
|
|
2758
|
+
this.positionedSections = updated;
|
|
2759
|
+
this.containerHeight = Math.max(...colHeights, 0);
|
|
2760
|
+
// Mark layout as ready on first successful reflow without zero heights
|
|
2761
|
+
if (!hasZeroHeights) {
|
|
2762
|
+
this.isLayoutReady = true;
|
|
2763
|
+
}
|
|
2764
|
+
// Force change detection - transitions will be handled by CSS
|
|
2765
|
+
this.cdr.markForCheck();
|
|
2766
|
+
// If we detected zero heights and haven't hit max reflows, try again immediately
|
|
2767
|
+
if (hasZeroHeights && this.reflowCount < this.MAX_REFLOWS) {
|
|
2768
|
+
requestAnimationFrame(() => {
|
|
2769
|
+
requestAnimationFrame(() => {
|
|
2770
|
+
this.reflowWithActualHeights();
|
|
2771
|
+
});
|
|
2772
|
+
});
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
emitLayoutInfo(columns, containerWidth) {
|
|
2776
|
+
const layoutInfo = {
|
|
2777
|
+
columns,
|
|
2778
|
+
containerWidth,
|
|
2779
|
+
breakpoint: getBreakpointFromWidth(containerWidth)
|
|
2780
|
+
};
|
|
2781
|
+
if (this.isSameLayoutInfo(layoutInfo, this.lastLayoutInfo)) {
|
|
2782
|
+
return;
|
|
2783
|
+
}
|
|
2784
|
+
this.lastLayoutInfo = layoutInfo;
|
|
2785
|
+
this.layoutChange.emit(layoutInfo);
|
|
2786
|
+
}
|
|
2787
|
+
isSameLayoutInfo(a, b) {
|
|
2788
|
+
if (!b) {
|
|
2789
|
+
return false;
|
|
2790
|
+
}
|
|
2791
|
+
return (a.breakpoint === b.breakpoint &&
|
|
2792
|
+
a.columns === b.columns &&
|
|
2793
|
+
Math.abs(a.containerWidth - b.containerWidth) < 4);
|
|
2794
|
+
}
|
|
2795
|
+
getSectionColSpan(section) {
|
|
2796
|
+
// Explicit colSpan always takes precedence
|
|
2797
|
+
if (section.colSpan) {
|
|
2798
|
+
return Math.min(section.colSpan, this.maxColumns);
|
|
2799
|
+
}
|
|
2800
|
+
const type = (section.type ?? '').toLowerCase();
|
|
2801
|
+
const title = (section.title ?? '').toLowerCase();
|
|
2802
|
+
if (type === 'project') {
|
|
2803
|
+
return 1;
|
|
2804
|
+
}
|
|
2805
|
+
const fieldCount = section.fields?.length ?? 0;
|
|
2806
|
+
const itemCount = section.items?.length ?? 0;
|
|
2807
|
+
const descriptionDensity = this.getDescriptionDensity(section.description);
|
|
2808
|
+
const baseScore = fieldCount + itemCount + descriptionDensity;
|
|
2809
|
+
// Get thresholds from section's meta (set during normalization)
|
|
2810
|
+
// This allows each section to have its own column logic
|
|
2811
|
+
const thresholds = this.getColSpanThresholds(section);
|
|
2812
|
+
if (thresholds.three && baseScore >= thresholds.three) {
|
|
2813
|
+
return Math.min(3, this.maxColumns);
|
|
2814
|
+
}
|
|
2815
|
+
if (baseScore >= thresholds.two) {
|
|
2816
|
+
return Math.min(2, this.maxColumns);
|
|
2817
|
+
}
|
|
2818
|
+
return 1;
|
|
2819
|
+
}
|
|
2820
|
+
/**
|
|
2821
|
+
* Get column span thresholds for a section
|
|
2822
|
+
* First checks section's meta (set during normalization), then falls back to default
|
|
2823
|
+
*/
|
|
2824
|
+
getColSpanThresholds(section) {
|
|
2825
|
+
const meta = section.meta;
|
|
2826
|
+
const thresholds = meta?.['colSpanThresholds'];
|
|
2827
|
+
if (thresholds && typeof thresholds === 'object' && 'two' in thresholds) {
|
|
2828
|
+
return thresholds;
|
|
2829
|
+
}
|
|
2830
|
+
// Fallback to default if not found in meta
|
|
2831
|
+
return DEFAULT_COL_SPAN_THRESHOLD;
|
|
2832
|
+
}
|
|
2833
|
+
getDescriptionDensity(description) {
|
|
2834
|
+
if (!description) {
|
|
2835
|
+
return 0;
|
|
2836
|
+
}
|
|
2837
|
+
const trimmedLength = description.trim().length;
|
|
2838
|
+
if (trimmedLength < 120) {
|
|
2839
|
+
return 0;
|
|
2840
|
+
}
|
|
2841
|
+
return Math.ceil(trimmedLength / 120);
|
|
2842
|
+
}
|
|
2843
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MasonryGridComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2844
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: MasonryGridComponent, isStandalone: true, selector: "app-masonry-grid", inputs: { sections: "sections", gap: "gap", minColumnWidth: "minColumnWidth", maxColumns: "maxColumns" }, outputs: { sectionEvent: "sectionEvent", layoutChange: "layoutChange" }, viewQueries: [{ propertyName: "containerRef", first: true, predicate: ["container"], descendants: true, static: true }, { propertyName: "itemRefs", predicate: ["itemRef"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div\n #container\n class=\"relative w-full masonry-container\"\n [class.masonry-container--loading]=\"!isLayoutReady\"\n [style.height.px]=\"containerHeight\"\n [style.gap.px]=\"gap\"\n>\n <div\n *ngFor=\"let item of positionedSections; trackBy: trackItem; let idx = index\"\n #itemRef\n class=\"absolute masonry-item\"\n [class.masonry-item--ready]=\"isLayoutReady\"\n [attr.id]=\"getSectionId(item.section)\"\n [ngStyle]=\"{\n position: 'absolute',\n left: item.left,\n top: item.top + 'px',\n width: item.width,\n zIndex: idx\n }\"\n >\n <app-section-renderer\n [section]=\"item.section\"\n (sectionEvent)=\"onSectionEvent($event)\"\n ></app-section-renderer>\n </div>\n</div>\n", styles: [":host{display:block;width:100%}.masonry-container{transition:opacity .25s ease,height .5s cubic-bezier(.25,.46,.45,.94);opacity:1;min-height:400px;contain:layout style paint}.masonry-container--loading{opacity:.6}.masonry-item{will-change:top,left,width;transition:opacity .4s ease;opacity:.3;backface-visibility:hidden;transform:translateZ(0);pointer-events:auto}.masonry-item--ready{opacity:1;transition:top .5s cubic-bezier(.25,.46,.45,.94),left .5s cubic-bezier(.25,.46,.45,.94),width .5s cubic-bezier(.25,.46,.45,.94),opacity .5s ease .1s,transform .3s ease}.masonry-item>*{display:block;width:100%;height:auto}@media (max-width: 768px){.masonry-container{min-height:300px}.masonry-item{transition:opacity .3s ease}.masonry-item--ready{transition:top .4s cubic-bezier(.25,.46,.45,.94),left .4s cubic-bezier(.25,.46,.45,.94),width .4s cubic-bezier(.25,.46,.45,.94),opacity .4s ease .05s}}@media (min-width: 1400px){.masonry-container{min-height:500px}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: SectionRendererComponent, selector: "app-section-renderer", inputs: ["section"], outputs: ["sectionEvent"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2845
|
+
}
|
|
2846
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MasonryGridComponent, decorators: [{
|
|
2847
|
+
type: Component,
|
|
2848
|
+
args: [{ selector: 'app-masonry-grid', standalone: true, imports: [CommonModule, SectionRendererComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n #container\n class=\"relative w-full masonry-container\"\n [class.masonry-container--loading]=\"!isLayoutReady\"\n [style.height.px]=\"containerHeight\"\n [style.gap.px]=\"gap\"\n>\n <div\n *ngFor=\"let item of positionedSections; trackBy: trackItem; let idx = index\"\n #itemRef\n class=\"absolute masonry-item\"\n [class.masonry-item--ready]=\"isLayoutReady\"\n [attr.id]=\"getSectionId(item.section)\"\n [ngStyle]=\"{\n position: 'absolute',\n left: item.left,\n top: item.top + 'px',\n width: item.width,\n zIndex: idx\n }\"\n >\n <app-section-renderer\n [section]=\"item.section\"\n (sectionEvent)=\"onSectionEvent($event)\"\n ></app-section-renderer>\n </div>\n</div>\n", styles: [":host{display:block;width:100%}.masonry-container{transition:opacity .25s ease,height .5s cubic-bezier(.25,.46,.45,.94);opacity:1;min-height:400px;contain:layout style paint}.masonry-container--loading{opacity:.6}.masonry-item{will-change:top,left,width;transition:opacity .4s ease;opacity:.3;backface-visibility:hidden;transform:translateZ(0);pointer-events:auto}.masonry-item--ready{opacity:1;transition:top .5s cubic-bezier(.25,.46,.45,.94),left .5s cubic-bezier(.25,.46,.45,.94),width .5s cubic-bezier(.25,.46,.45,.94),opacity .5s ease .1s,transform .3s ease}.masonry-item>*{display:block;width:100%;height:auto}@media (max-width: 768px){.masonry-container{min-height:300px}.masonry-item{transition:opacity .3s ease}.masonry-item--ready{transition:top .4s cubic-bezier(.25,.46,.45,.94),left .4s cubic-bezier(.25,.46,.45,.94),width .4s cubic-bezier(.25,.46,.45,.94),opacity .4s ease .05s}}@media (min-width: 1400px){.masonry-container{min-height:500px}}\n"] }]
|
|
2849
|
+
}], propDecorators: { sections: [{
|
|
2850
|
+
type: Input
|
|
2851
|
+
}], gap: [{
|
|
2852
|
+
type: Input
|
|
2853
|
+
}], minColumnWidth: [{
|
|
2854
|
+
type: Input
|
|
2855
|
+
}], maxColumns: [{
|
|
2856
|
+
type: Input
|
|
2857
|
+
}], sectionEvent: [{
|
|
2858
|
+
type: Output
|
|
2859
|
+
}], layoutChange: [{
|
|
2860
|
+
type: Output
|
|
2861
|
+
}], containerRef: [{
|
|
2862
|
+
type: ViewChild,
|
|
2863
|
+
args: ['container', { static: true }]
|
|
2864
|
+
}], itemRefs: [{
|
|
2865
|
+
type: ViewChildren,
|
|
2866
|
+
args: ['itemRef']
|
|
2867
|
+
}] } });
|
|
2868
|
+
|
|
2869
|
+
class AICardRendererComponent {
|
|
2870
|
+
constructor() {
|
|
2871
|
+
this.el = inject(ElementRef);
|
|
2872
|
+
this.cdr = inject(ChangeDetectorRef);
|
|
2873
|
+
// Expose Math for template
|
|
2874
|
+
this.Math = Math;
|
|
2875
|
+
this.previousSectionsHash = '';
|
|
2876
|
+
this.normalizedSectionCache = new WeakMap();
|
|
2877
|
+
this.sectionHashCache = new WeakMap();
|
|
2878
|
+
this.sectionOrderKeys = [];
|
|
2879
|
+
this._changeType = 'structural';
|
|
2880
|
+
// Empty state animations
|
|
2881
|
+
this.particles = [];
|
|
2882
|
+
this.gradientTransform = 'translate(-50%, -50%)';
|
|
2883
|
+
this.contentTransform = 'translate(0, 0)';
|
|
2884
|
+
this.currentMessageIndex = 0;
|
|
2885
|
+
this.currentMessage = '';
|
|
2886
|
+
this.mouseX = 0;
|
|
2887
|
+
this.mouseY = 0;
|
|
2888
|
+
this.isMouseOverEmptyState = false;
|
|
2889
|
+
this.scrollY = 0;
|
|
2890
|
+
this.funnyMessages = [
|
|
2891
|
+
'Deepening into archives...',
|
|
2892
|
+
'Asking all 40,000 employees...',
|
|
2893
|
+
'Re-reading manifesto...',
|
|
2894
|
+
'Consulting the oracle...',
|
|
2895
|
+
'Checking under the couch...',
|
|
2896
|
+
'Asking ChatGPT for help...',
|
|
2897
|
+
'Brewing coffee first...',
|
|
2898
|
+
'Counting to infinity...',
|
|
2899
|
+
'Summoning the data spirits...',
|
|
2900
|
+
'Teaching AI to read minds...',
|
|
2901
|
+
'Searching parallel universes...',
|
|
2902
|
+
'Waiting for inspiration...',
|
|
2903
|
+
'Polishing crystal ball...',
|
|
2904
|
+
'Decoding ancient scrolls...',
|
|
2905
|
+
'Training neural networks...',
|
|
2906
|
+
'Consulting the stars...',
|
|
2907
|
+
'Asking Siri nicely...',
|
|
2908
|
+
'Reading tea leaves...',
|
|
2909
|
+
'Channeling inner wisdom...',
|
|
2910
|
+
'Waiting for the right moment...'
|
|
2911
|
+
];
|
|
2912
|
+
this._updateSource = 'stream';
|
|
2913
|
+
this.isFullscreen = false;
|
|
2914
|
+
this.tiltEnabled = true;
|
|
2915
|
+
this.streamingStage = undefined;
|
|
2916
|
+
this.fieldInteraction = new EventEmitter();
|
|
2917
|
+
this.cardInteraction = new EventEmitter();
|
|
2918
|
+
this.fullscreenToggle = new EventEmitter();
|
|
2919
|
+
this.agentAction = new EventEmitter();
|
|
2920
|
+
this.questionAction = new EventEmitter();
|
|
2921
|
+
this.processedSections = [];
|
|
2922
|
+
this.isHovered = false;
|
|
2923
|
+
this.mousePosition = { x: 0, y: 0 };
|
|
2924
|
+
// CSS variables for the tilt effect
|
|
2925
|
+
this.tiltStyle = {};
|
|
2926
|
+
// Performance: RAF batching for mouse moves
|
|
2927
|
+
this.mouseMoveRafId = null;
|
|
2928
|
+
this.pendingMouseMove = null;
|
|
2929
|
+
this.destroyed$ = new Subject();
|
|
2930
|
+
this.magneticTiltService = inject(MagneticTiltService);
|
|
2931
|
+
this.iconService = inject(IconService);
|
|
2932
|
+
this.sectionNormalizationService = inject(SectionNormalizationService);
|
|
2933
|
+
this.viewportScroller = inject(ViewportScroller);
|
|
2934
|
+
// Fallback card configuration for testing
|
|
2935
|
+
this.fallbackCard = {
|
|
2936
|
+
id: 'fallback-test',
|
|
2937
|
+
cardTitle: 'Test Company',
|
|
2938
|
+
cardSubtitle: 'Fallback Card for Testing',
|
|
2939
|
+
sections: [
|
|
2940
|
+
{
|
|
2941
|
+
id: 'test-info',
|
|
2942
|
+
title: 'Company Information',
|
|
2943
|
+
type: 'info',
|
|
2944
|
+
fields: [
|
|
2945
|
+
{
|
|
2946
|
+
id: 'industry',
|
|
2947
|
+
label: 'Industry',
|
|
2948
|
+
value: 'Technology',
|
|
2949
|
+
type: 'text'
|
|
2950
|
+
},
|
|
2951
|
+
{
|
|
2952
|
+
id: 'employees',
|
|
2953
|
+
label: 'Employees',
|
|
2954
|
+
value: '250',
|
|
2955
|
+
type: 'text'
|
|
2956
|
+
}
|
|
2957
|
+
]
|
|
2958
|
+
}
|
|
2959
|
+
],
|
|
2960
|
+
actions: [
|
|
2961
|
+
{
|
|
2962
|
+
id: 'view-details',
|
|
2963
|
+
label: 'View Details',
|
|
2964
|
+
variant: 'primary'
|
|
2965
|
+
}
|
|
2966
|
+
]
|
|
2967
|
+
};
|
|
2968
|
+
this.trackSection = (_index, section) => section.id ?? `${section.title}-${_index}`;
|
|
2969
|
+
this.trackField = (_index, field) => field.id ?? `${field.label}-${_index}`;
|
|
2970
|
+
this.trackItem = (_index, item) => item.id ?? `${item.title}-${_index}`;
|
|
2971
|
+
this.trackAction = (_index, action) => action.id ?? `${action.label}-${_index}`;
|
|
2972
|
+
}
|
|
2973
|
+
set cardConfig(value) {
|
|
2974
|
+
this._cardConfig = value ?? undefined;
|
|
2975
|
+
if (!this._cardConfig?.sections?.length) {
|
|
2976
|
+
this.resetProcessedSections();
|
|
2977
|
+
this.cdr.markForCheck();
|
|
2978
|
+
return;
|
|
2979
|
+
}
|
|
2980
|
+
const sectionsHash = this.hashSections(this._cardConfig.sections);
|
|
2981
|
+
const shouldForceStructural = sectionsHash !== this.previousSectionsHash || this._updateSource === 'liveEdit';
|
|
2982
|
+
this.refreshProcessedSections(shouldForceStructural);
|
|
2983
|
+
this.cdr.markForCheck();
|
|
2984
|
+
}
|
|
2985
|
+
get cardConfig() {
|
|
2986
|
+
return this._cardConfig;
|
|
2987
|
+
}
|
|
2988
|
+
/**
|
|
2989
|
+
* Input to track update source - used to bypass hash caching for live edits
|
|
2990
|
+
* When 'liveEdit', always forces reprocessing even if sections haven't changed structurally
|
|
2991
|
+
*/
|
|
2992
|
+
set updateSource(value) {
|
|
2993
|
+
if (value === 'liveEdit' && this._updateSource !== value) {
|
|
2994
|
+
// Live edit detected - force section reprocessing by invalidating hash
|
|
2995
|
+
this.previousSectionsHash = '';
|
|
2996
|
+
if (this._cardConfig?.sections?.length) {
|
|
2997
|
+
this.refreshProcessedSections(true);
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
this._updateSource = value;
|
|
3001
|
+
}
|
|
3002
|
+
get updateSource() {
|
|
3003
|
+
return this._updateSource;
|
|
3004
|
+
}
|
|
3005
|
+
/**
|
|
3006
|
+
* Fast hash function for sections (replaces JSON.stringify)
|
|
3007
|
+
* Uses WeakMap cache to avoid recomputation
|
|
3008
|
+
*/
|
|
3009
|
+
hashSections(sections) {
|
|
3010
|
+
// Check cache first
|
|
3011
|
+
if (this.sectionHashCache.has(sections)) {
|
|
3012
|
+
return this.sectionHashCache.get(sections);
|
|
3013
|
+
}
|
|
3014
|
+
// Create a lightweight hash based on section metadata
|
|
3015
|
+
const hash = sections
|
|
3016
|
+
.map(section => {
|
|
3017
|
+
const fieldCount = section.fields?.length ?? 0;
|
|
3018
|
+
const itemCount = section.items?.length ?? 0;
|
|
3019
|
+
return `${section.id || ''}|${section.title || ''}|${section.type || ''}|f${fieldCount}|i${itemCount}`;
|
|
3020
|
+
})
|
|
3021
|
+
.join('||');
|
|
3022
|
+
// Simple hash of the string
|
|
3023
|
+
let result = 0;
|
|
3024
|
+
for (let i = 0; i < hash.length; i++) {
|
|
3025
|
+
const char = hash.charCodeAt(i);
|
|
3026
|
+
result = ((result << 5) - result) + char;
|
|
3027
|
+
result = result & result; // Convert to 32-bit integer
|
|
3028
|
+
}
|
|
3029
|
+
const hashString = String(result);
|
|
3030
|
+
// Cache the result
|
|
3031
|
+
this.sectionHashCache.set(sections, hashString);
|
|
3032
|
+
return hashString;
|
|
3033
|
+
}
|
|
3034
|
+
set changeType(value) {
|
|
3035
|
+
if (this._changeType === value) {
|
|
3036
|
+
return;
|
|
3037
|
+
}
|
|
3038
|
+
this._changeType = value;
|
|
3039
|
+
if (this._cardConfig?.sections?.length) {
|
|
3040
|
+
this.refreshProcessedSections(value === 'structural');
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
get changeType() {
|
|
3044
|
+
return this._changeType;
|
|
3045
|
+
}
|
|
3046
|
+
ngOnInit() {
|
|
3047
|
+
// Initialize particles
|
|
3048
|
+
this.initializeParticles();
|
|
3049
|
+
// Start message rotation
|
|
3050
|
+
this.startMessageRotation();
|
|
3051
|
+
// Track scroll for parallax effect
|
|
3052
|
+
this.setupScrollTracking();
|
|
3053
|
+
// Use fallback if no card config provided
|
|
3054
|
+
if (!this.cardConfig) {
|
|
3055
|
+
this.cardConfig = this.fallbackCard;
|
|
3056
|
+
}
|
|
3057
|
+
if (!this.processedSections.length) {
|
|
3058
|
+
this.refreshProcessedSections(true);
|
|
3059
|
+
}
|
|
3060
|
+
// Handle Escape key for fullscreen exit
|
|
3061
|
+
fromEvent(document, 'keydown')
|
|
3062
|
+
.pipe(takeUntil(this.destroyed$))
|
|
3063
|
+
.subscribe((event) => {
|
|
3064
|
+
if (event.key === 'Escape' && this.isFullscreen) {
|
|
3065
|
+
this.toggleFullscreen();
|
|
3066
|
+
}
|
|
3067
|
+
});
|
|
3068
|
+
// Subscribe to tilt calculations with RAF batching for performance
|
|
3069
|
+
let tiltRafId = null;
|
|
3070
|
+
let pendingCalculations = null;
|
|
3071
|
+
this.magneticTiltService.tiltCalculations$
|
|
3072
|
+
.pipe(takeUntil(this.destroyed$))
|
|
3073
|
+
.subscribe((calculations) => {
|
|
3074
|
+
pendingCalculations = calculations;
|
|
3075
|
+
// Batch updates via RAF to avoid excessive change detection
|
|
3076
|
+
// Always update to ensure smooth transitions
|
|
3077
|
+
if (tiltRafId === null) {
|
|
3078
|
+
tiltRafId = requestAnimationFrame(() => {
|
|
3079
|
+
if (pendingCalculations) {
|
|
3080
|
+
// Always update glow values for smooth transitions
|
|
3081
|
+
this.tiltStyle = {
|
|
3082
|
+
'--tilt-x': `${pendingCalculations.rotateX}deg`,
|
|
3083
|
+
'--tilt-y': `${pendingCalculations.rotateY}deg`,
|
|
3084
|
+
'--glow-blur': `${pendingCalculations.glowBlur}px`,
|
|
3085
|
+
'--glow-color': `rgba(255,121,0,${pendingCalculations.glowOpacity})`,
|
|
3086
|
+
'--reflection-opacity': pendingCalculations.reflectionOpacity
|
|
3087
|
+
};
|
|
3088
|
+
this.cdr.markForCheck();
|
|
3089
|
+
}
|
|
3090
|
+
pendingCalculations = null;
|
|
3091
|
+
tiltRafId = null;
|
|
3092
|
+
});
|
|
3093
|
+
}
|
|
3094
|
+
});
|
|
3095
|
+
}
|
|
3096
|
+
ngAfterViewInit() {
|
|
3097
|
+
// Fragment handling removed for standalone library
|
|
3098
|
+
// Consumers can implement their own fragment handling if needed
|
|
3099
|
+
}
|
|
3100
|
+
/**
|
|
3101
|
+
* Scrolls to a section by ID or sanitized title
|
|
3102
|
+
*/
|
|
3103
|
+
scrollToSection(sectionId) {
|
|
3104
|
+
if (typeof document === 'undefined')
|
|
3105
|
+
return;
|
|
3106
|
+
// Try direct ID match first
|
|
3107
|
+
let targetElement = document.getElementById(sectionId);
|
|
3108
|
+
// If not found, try to find by sanitized section title
|
|
3109
|
+
if (!targetElement) {
|
|
3110
|
+
const section = this.processedSections.find(s => this.sanitizeSectionId(s.title) === sectionId);
|
|
3111
|
+
if (section) {
|
|
3112
|
+
targetElement = document.getElementById(this.getSectionId(section));
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
if (targetElement) {
|
|
3116
|
+
// Scroll with smooth behavior and offset for header
|
|
3117
|
+
const yOffset = -20; // Offset from top
|
|
3118
|
+
const y = targetElement.getBoundingClientRect().top + window.pageYOffset + yOffset;
|
|
3119
|
+
window.scrollTo({
|
|
3120
|
+
top: y,
|
|
3121
|
+
behavior: 'smooth'
|
|
3122
|
+
});
|
|
3123
|
+
// Add visual highlight effect
|
|
3124
|
+
targetElement.classList.add('section-highlight');
|
|
3125
|
+
setTimeout(() => {
|
|
3126
|
+
targetElement?.classList.remove('section-highlight');
|
|
3127
|
+
}, 2000);
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
/**
|
|
3131
|
+
* Gets a unique section ID for scrolling
|
|
3132
|
+
*/
|
|
3133
|
+
getSectionId(section) {
|
|
3134
|
+
return `section-${this.sanitizeSectionId(section.title || section.id || 'unknown')}`;
|
|
3135
|
+
}
|
|
3136
|
+
/**
|
|
3137
|
+
* Sanitizes section title for use as HTML ID
|
|
3138
|
+
*/
|
|
3139
|
+
sanitizeSectionId(title) {
|
|
3140
|
+
return title.toLowerCase()
|
|
3141
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
3142
|
+
.replace(/^-|-$/g, '');
|
|
3143
|
+
}
|
|
3144
|
+
initializeParticles() {
|
|
3145
|
+
// Create 20 smaller particles for smoother mouse following trail
|
|
3146
|
+
this.particles = Array.from({ length: 20 }, () => ({
|
|
3147
|
+
transform: 'translate(0, 0) scale(1)',
|
|
3148
|
+
opacity: 0.5
|
|
3149
|
+
}));
|
|
3150
|
+
}
|
|
3151
|
+
startMessageRotation() {
|
|
3152
|
+
this.currentMessage = this.funnyMessages[0];
|
|
3153
|
+
this.currentMessageIndex = 0;
|
|
3154
|
+
interval(2500) // Change message every 2.5 seconds
|
|
3155
|
+
.pipe(takeUntil(this.destroyed$))
|
|
3156
|
+
.subscribe(() => {
|
|
3157
|
+
this.currentMessageIndex = (this.currentMessageIndex + 1) % this.funnyMessages.length;
|
|
3158
|
+
this.currentMessage = this.funnyMessages[this.currentMessageIndex];
|
|
3159
|
+
this.cdr.markForCheck();
|
|
3160
|
+
});
|
|
3161
|
+
}
|
|
3162
|
+
setupScrollTracking() {
|
|
3163
|
+
fromEvent(window, 'scroll')
|
|
3164
|
+
.pipe(takeUntil(this.destroyed$))
|
|
3165
|
+
.subscribe(() => {
|
|
3166
|
+
this.scrollY = window.scrollY;
|
|
3167
|
+
this.updateContentTransform();
|
|
3168
|
+
});
|
|
3169
|
+
}
|
|
3170
|
+
onEmptyStateMouseMove(event) {
|
|
3171
|
+
if (!this.emptyStateContainer)
|
|
3172
|
+
return;
|
|
3173
|
+
const rect = this.emptyStateContainer.nativeElement.getBoundingClientRect();
|
|
3174
|
+
this.mouseX = event.clientX - rect.left;
|
|
3175
|
+
this.mouseY = event.clientY - rect.top;
|
|
3176
|
+
this.isMouseOverEmptyState = true;
|
|
3177
|
+
this.updateParticlePositions();
|
|
3178
|
+
this.updateGradientTransform();
|
|
3179
|
+
this.updateContentTransform();
|
|
3180
|
+
}
|
|
3181
|
+
onEmptyStateMouseLeave() {
|
|
3182
|
+
this.isMouseOverEmptyState = false;
|
|
3183
|
+
// Reset particles to center with smooth animation
|
|
3184
|
+
this.resetParticles();
|
|
3185
|
+
}
|
|
3186
|
+
updateParticlePositions() {
|
|
3187
|
+
if (!this.emptyStateContainer)
|
|
3188
|
+
return;
|
|
3189
|
+
const rect = this.emptyStateContainer.nativeElement.getBoundingClientRect();
|
|
3190
|
+
const centerX = rect.width / 2;
|
|
3191
|
+
const centerY = rect.height / 2;
|
|
3192
|
+
const deltaX = this.mouseX - centerX;
|
|
3193
|
+
const deltaY = this.mouseY - centerY;
|
|
3194
|
+
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
3195
|
+
const maxDistance = Math.sqrt(rect.width * rect.width + rect.height * rect.height) / 2;
|
|
3196
|
+
const normalizedDistance = Math.min(distance / maxDistance, 1);
|
|
3197
|
+
this.particles = this.particles.map((particle, index) => {
|
|
3198
|
+
// Create a trailing effect with exponential easing
|
|
3199
|
+
const delay = index * 0.08;
|
|
3200
|
+
const followStrength = 0.4 - (delay * 0.3); // Particles further back follow less
|
|
3201
|
+
const spiralRadius = 15 + (index % 4) * 8;
|
|
3202
|
+
const angle = (index * 137.5) % 360; // Golden angle for spiral distribution
|
|
3203
|
+
const angleRad = angle * Math.PI / 180;
|
|
3204
|
+
// Calculate spiral offset
|
|
3205
|
+
const spiralX = Math.cos(angleRad) * spiralRadius;
|
|
3206
|
+
const spiralY = Math.sin(angleRad) * spiralRadius;
|
|
3207
|
+
// Smooth following with easing
|
|
3208
|
+
const targetX = deltaX * followStrength + spiralX;
|
|
3209
|
+
const targetY = deltaY * followStrength + spiralY;
|
|
3210
|
+
// Opacity based on distance and position
|
|
3211
|
+
const baseOpacity = 0.5;
|
|
3212
|
+
const distanceOpacity = normalizedDistance * 0.3;
|
|
3213
|
+
const positionOpacity = (1 - Math.abs(index - this.particles.length / 2) / this.particles.length) * 0.2;
|
|
3214
|
+
const finalOpacity = Math.min(1, baseOpacity + distanceOpacity + positionOpacity);
|
|
3215
|
+
return {
|
|
3216
|
+
transform: `translate(${targetX}px, ${targetY}px) scale(${0.8 + normalizedDistance * 0.4})`,
|
|
3217
|
+
opacity: finalOpacity
|
|
3218
|
+
};
|
|
3219
|
+
});
|
|
3220
|
+
this.cdr.markForCheck();
|
|
3221
|
+
}
|
|
3222
|
+
resetParticles() {
|
|
3223
|
+
this.particles = this.particles.map(particle => ({
|
|
3224
|
+
transform: 'translate(0, 0) scale(1)',
|
|
3225
|
+
opacity: 0.5
|
|
3226
|
+
}));
|
|
3227
|
+
this.cdr.markForCheck();
|
|
3228
|
+
}
|
|
3229
|
+
updateGradientTransform() {
|
|
3230
|
+
if (!this.emptyStateContainer)
|
|
3231
|
+
return;
|
|
3232
|
+
const rect = this.emptyStateContainer.nativeElement.getBoundingClientRect();
|
|
3233
|
+
const centerX = rect.width / 2;
|
|
3234
|
+
const centerY = rect.height / 2;
|
|
3235
|
+
const deltaX = (this.mouseX - centerX) * 0.1;
|
|
3236
|
+
const deltaY = (this.mouseY - centerY) * 0.1;
|
|
3237
|
+
this.gradientTransform = `translate(calc(-50% + ${deltaX}px), calc(-50% + ${deltaY}px))`;
|
|
3238
|
+
this.cdr.markForCheck();
|
|
3239
|
+
}
|
|
3240
|
+
updateContentTransform() {
|
|
3241
|
+
if (!this.emptyStateContainer)
|
|
3242
|
+
return;
|
|
3243
|
+
const rect = this.emptyStateContainer.nativeElement.getBoundingClientRect();
|
|
3244
|
+
const centerX = rect.width / 2;
|
|
3245
|
+
const centerY = rect.height / 2;
|
|
3246
|
+
// Parallax effect based on mouse position and scroll
|
|
3247
|
+
const mouseParallaxX = this.isMouseOverEmptyState ? (this.mouseX - centerX) * 0.02 : 0;
|
|
3248
|
+
const mouseParallaxY = this.isMouseOverEmptyState ? (this.mouseY - centerY) * 0.02 : 0;
|
|
3249
|
+
const scrollParallaxY = this.scrollY * 0.05;
|
|
3250
|
+
this.contentTransform = `translate(${mouseParallaxX}px, calc(${mouseParallaxY}px + ${scrollParallaxY}px))`;
|
|
3251
|
+
this.cdr.markForCheck();
|
|
3252
|
+
}
|
|
3253
|
+
ngOnDestroy() {
|
|
3254
|
+
// Cancel any pending RAFs
|
|
3255
|
+
if (this.mouseMoveRafId !== null) {
|
|
3256
|
+
cancelAnimationFrame(this.mouseMoveRafId);
|
|
3257
|
+
}
|
|
3258
|
+
// Clear tilt service cache for this element
|
|
3259
|
+
if (this.tiltContainerRef?.nativeElement) {
|
|
3260
|
+
this.magneticTiltService.clearCache(this.tiltContainerRef.nativeElement);
|
|
3261
|
+
}
|
|
3262
|
+
this.destroyed$.next();
|
|
3263
|
+
this.destroyed$.complete();
|
|
3264
|
+
}
|
|
3265
|
+
onMouseEnter(event) {
|
|
3266
|
+
this.isHovered = true;
|
|
3267
|
+
this.mousePosition = { x: event.clientX, y: event.clientY };
|
|
3268
|
+
if (this.tiltContainerRef?.nativeElement) {
|
|
3269
|
+
// Smooth enter: calculate tilt immediately for responsive feel
|
|
3270
|
+
this.magneticTiltService.calculateTilt(this.mousePosition, this.tiltContainerRef.nativeElement);
|
|
3271
|
+
}
|
|
3272
|
+
// No need for markForCheck - tilt updates are handled via RAF in subscription
|
|
3273
|
+
}
|
|
3274
|
+
onMouseLeave() {
|
|
3275
|
+
this.isHovered = false;
|
|
3276
|
+
// Cancel pending RAF for mouse moves
|
|
3277
|
+
if (this.mouseMoveRafId !== null) {
|
|
3278
|
+
cancelAnimationFrame(this.mouseMoveRafId);
|
|
3279
|
+
this.mouseMoveRafId = null;
|
|
3280
|
+
}
|
|
3281
|
+
this.pendingMouseMove = null;
|
|
3282
|
+
// Smooth exit: reset with smooth transition that completes even if cursor leaves quickly
|
|
3283
|
+
this.magneticTiltService.resetTilt(true);
|
|
3284
|
+
// No need for markForCheck - tilt reset is handled via smooth animation
|
|
3285
|
+
}
|
|
3286
|
+
onMouseMove(event) {
|
|
3287
|
+
if (!this.isHovered || !this.tiltContainerRef?.nativeElement) {
|
|
3288
|
+
return;
|
|
3289
|
+
}
|
|
3290
|
+
// Store latest mouse position
|
|
3291
|
+
this.pendingMouseMove = event;
|
|
3292
|
+
// Throttle with RAF for 60fps smooth updates
|
|
3293
|
+
if (this.mouseMoveRafId === null) {
|
|
3294
|
+
this.mouseMoveRafId = requestAnimationFrame(() => {
|
|
3295
|
+
if (this.pendingMouseMove && this.tiltContainerRef?.nativeElement) {
|
|
3296
|
+
this.mousePosition = {
|
|
3297
|
+
x: this.pendingMouseMove.clientX,
|
|
3298
|
+
y: this.pendingMouseMove.clientY
|
|
3299
|
+
};
|
|
3300
|
+
this.magneticTiltService.calculateTilt(this.mousePosition, this.tiltContainerRef.nativeElement);
|
|
3301
|
+
}
|
|
3302
|
+
this.pendingMouseMove = null;
|
|
3303
|
+
this.mouseMoveRafId = null;
|
|
3304
|
+
});
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
onFieldClick(field, section) {
|
|
3308
|
+
this.fieldInteraction.emit({
|
|
3309
|
+
field,
|
|
3310
|
+
action: 'click',
|
|
3311
|
+
sectionTitle: section?.title
|
|
3312
|
+
});
|
|
3313
|
+
}
|
|
3314
|
+
/**
|
|
3315
|
+
* Type guard to check if action has email property
|
|
3316
|
+
*/
|
|
3317
|
+
hasEmailProperty(action) {
|
|
3318
|
+
return 'email' in action && action.email !== undefined;
|
|
3319
|
+
}
|
|
3320
|
+
onActionClick(actionObj) {
|
|
3321
|
+
if (!this.cardConfig) {
|
|
3322
|
+
return;
|
|
3323
|
+
}
|
|
3324
|
+
// Handle button types based on 'type' field from JSON
|
|
3325
|
+
// Check if type is a button behavior type (not legacy styling value)
|
|
3326
|
+
if (actionObj.type && ['mail', 'website', 'agent', 'question'].includes(actionObj.type)) {
|
|
3327
|
+
switch (actionObj.type) {
|
|
3328
|
+
case 'mail':
|
|
3329
|
+
if (this.hasEmailProperty(actionObj)) {
|
|
3330
|
+
this.handleEmailAction(actionObj);
|
|
3331
|
+
}
|
|
3332
|
+
else {
|
|
3333
|
+
console.error('Mail action requires email configuration');
|
|
3334
|
+
}
|
|
3335
|
+
return;
|
|
3336
|
+
case 'website':
|
|
3337
|
+
// Use url property if available, otherwise fall back to action property
|
|
3338
|
+
const url = actionObj.url || actionObj.action;
|
|
3339
|
+
if (url && url !== '#' && (url.startsWith('http://') || url.startsWith('https://'))) {
|
|
3340
|
+
window.open(url, '_blank', 'noopener,noreferrer');
|
|
3341
|
+
}
|
|
3342
|
+
else {
|
|
3343
|
+
console.warn('No valid URL provided for website button type');
|
|
3344
|
+
}
|
|
3345
|
+
return;
|
|
3346
|
+
case 'agent':
|
|
3347
|
+
this.agentAction.emit({
|
|
3348
|
+
action: actionObj,
|
|
3349
|
+
card: this.cardConfig,
|
|
3350
|
+
agentId: actionObj.agentId,
|
|
3351
|
+
context: actionObj.agentContext || actionObj.meta
|
|
3352
|
+
});
|
|
3353
|
+
return;
|
|
3354
|
+
case 'question':
|
|
3355
|
+
this.questionAction.emit({
|
|
3356
|
+
action: actionObj,
|
|
3357
|
+
card: this.cardConfig,
|
|
3358
|
+
question: actionObj.question || actionObj.label
|
|
3359
|
+
});
|
|
3360
|
+
return;
|
|
3361
|
+
default:
|
|
3362
|
+
// Should not reach here, but fall through to legacy handling
|
|
3363
|
+
break;
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
// Legacy handling for backwards compatibility
|
|
3367
|
+
// If type is 'primary' or 'secondary' (legacy styling), treat as regular action
|
|
3368
|
+
// Handle email actions (email property present)
|
|
3369
|
+
if (this.hasEmailProperty(actionObj)) {
|
|
3370
|
+
this.handleEmailAction(actionObj);
|
|
3371
|
+
return;
|
|
3372
|
+
}
|
|
3373
|
+
// Handle URL actions (action property contains a URL)
|
|
3374
|
+
if (actionObj.action && actionObj.action !== '#' && actionObj.action.startsWith('http')) {
|
|
3375
|
+
window.open(actionObj.action, '_blank', 'noopener,noreferrer');
|
|
3376
|
+
return;
|
|
3377
|
+
}
|
|
3378
|
+
// Handle regular actions (emit event for custom handling)
|
|
3379
|
+
const action = actionObj.action || actionObj.label;
|
|
3380
|
+
this.cardInteraction.emit({
|
|
3381
|
+
action: action,
|
|
3382
|
+
card: this.cardConfig
|
|
3383
|
+
});
|
|
3384
|
+
}
|
|
3385
|
+
handleEmailAction(action) {
|
|
3386
|
+
// Validate that email configuration exists
|
|
3387
|
+
if (!action.email) {
|
|
3388
|
+
console.error('Email action requires email configuration');
|
|
3389
|
+
return;
|
|
3390
|
+
}
|
|
3391
|
+
const email = action.email;
|
|
3392
|
+
// Validate required fields for mail type
|
|
3393
|
+
if (action.type === 'mail') {
|
|
3394
|
+
if (!email.contact) {
|
|
3395
|
+
console.error('Mail action requires email.contact with name, email, and role');
|
|
3396
|
+
return;
|
|
3397
|
+
}
|
|
3398
|
+
if (!email.contact.name || !email.contact.email || !email.contact.role) {
|
|
3399
|
+
console.error('Mail action requires email.contact.name, email.contact.email, and email.contact.role');
|
|
3400
|
+
return;
|
|
3401
|
+
}
|
|
3402
|
+
if (!email.subject) {
|
|
3403
|
+
console.error('Mail action requires email.subject');
|
|
3404
|
+
return;
|
|
3405
|
+
}
|
|
3406
|
+
if (!email.body) {
|
|
3407
|
+
console.error('Mail action requires email.body');
|
|
3408
|
+
return;
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
// Determine recipient email address - prioritize to, then contact.email
|
|
3412
|
+
let recipientEmail = '';
|
|
3413
|
+
if (email.to) {
|
|
3414
|
+
recipientEmail = Array.isArray(email.to) ? email.to.join(',') : email.to;
|
|
3415
|
+
}
|
|
3416
|
+
else if (email.contact?.email) {
|
|
3417
|
+
recipientEmail = email.contact.email;
|
|
3418
|
+
}
|
|
3419
|
+
if (!recipientEmail) {
|
|
3420
|
+
console.warn('No email address provided for email action');
|
|
3421
|
+
return;
|
|
3422
|
+
}
|
|
3423
|
+
// Build mailto URL parameters manually for better control over encoding
|
|
3424
|
+
const params = [];
|
|
3425
|
+
// Add CC if provided
|
|
3426
|
+
if (email.cc) {
|
|
3427
|
+
const cc = Array.isArray(email.cc) ? email.cc.join(',') : email.cc;
|
|
3428
|
+
params.push(`cc=${encodeURIComponent(cc)}`);
|
|
3429
|
+
}
|
|
3430
|
+
// Add BCC if provided
|
|
3431
|
+
if (email.bcc) {
|
|
3432
|
+
const bcc = Array.isArray(email.bcc) ? email.bcc.join(',') : email.bcc;
|
|
3433
|
+
params.push(`bcc=${encodeURIComponent(bcc)}`);
|
|
3434
|
+
}
|
|
3435
|
+
// Add subject - required for mail type, optional for legacy
|
|
3436
|
+
if (email.subject) {
|
|
3437
|
+
params.push(`subject=${encodeURIComponent(email.subject)}`);
|
|
3438
|
+
}
|
|
3439
|
+
else if (action.type === 'mail') {
|
|
3440
|
+
console.warn('Email subject is missing for mail action');
|
|
3441
|
+
}
|
|
3442
|
+
// Process body - replace placeholders with contact information if available
|
|
3443
|
+
let processedBody = email.body || '';
|
|
3444
|
+
if (email.contact) {
|
|
3445
|
+
// Replace {name} placeholder if contact name is available
|
|
3446
|
+
if (email.contact.name) {
|
|
3447
|
+
processedBody = processedBody.replace(/\{name\}/g, email.contact.name);
|
|
3448
|
+
// Also replace common placeholders like {contact} or {recipient}
|
|
3449
|
+
processedBody = processedBody.replace(/\{contact\}/g, email.contact.name);
|
|
3450
|
+
processedBody = processedBody.replace(/\{recipient\}/g, email.contact.name);
|
|
3451
|
+
}
|
|
3452
|
+
// Replace {role} placeholder if contact role is available
|
|
3453
|
+
if (email.contact.role) {
|
|
3454
|
+
processedBody = processedBody.replace(/\{role\}/g, email.contact.role);
|
|
3455
|
+
}
|
|
3456
|
+
// Replace {email} placeholder
|
|
3457
|
+
if (email.contact.email) {
|
|
3458
|
+
processedBody = processedBody.replace(/\{email\}/g, email.contact.email);
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
// Add body - required for mail type, optional for legacy
|
|
3462
|
+
if (processedBody) {
|
|
3463
|
+
// Replace newlines with %0D%0A (CRLF) for proper email formatting
|
|
3464
|
+
// Then encode the rest of the content
|
|
3465
|
+
const bodyWithLineBreaks = processedBody.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
3466
|
+
const encodedBody = encodeURIComponent(bodyWithLineBreaks).replace(/%0A/g, '%0D%0A');
|
|
3467
|
+
params.push(`body=${encodedBody}`);
|
|
3468
|
+
}
|
|
3469
|
+
else if (action.type === 'mail') {
|
|
3470
|
+
console.warn('Email body is missing for mail action');
|
|
3471
|
+
}
|
|
3472
|
+
// Construct mailto link
|
|
3473
|
+
const queryString = params.length > 0 ? '?' + params.join('&') : '';
|
|
3474
|
+
const mailtoLink = `mailto:${recipientEmail}${queryString}`;
|
|
3475
|
+
// Open email client using a temporary anchor element (most reliable method)
|
|
3476
|
+
// This ensures the email client opens without navigating away from the page
|
|
3477
|
+
const anchor = document.createElement('a');
|
|
3478
|
+
anchor.href = mailtoLink;
|
|
3479
|
+
anchor.style.display = 'none';
|
|
3480
|
+
document.body.appendChild(anchor);
|
|
3481
|
+
anchor.click();
|
|
3482
|
+
document.body.removeChild(anchor);
|
|
3483
|
+
}
|
|
3484
|
+
toggleFullscreen() {
|
|
3485
|
+
this.fullscreenToggle.emit(!this.isFullscreen);
|
|
3486
|
+
// Immediate reset when toggling fullscreen (no smooth transition needed)
|
|
3487
|
+
this.magneticTiltService.resetTilt(false);
|
|
3488
|
+
}
|
|
3489
|
+
get isStreamingActive() {
|
|
3490
|
+
return this.streamingStage === 'streaming';
|
|
3491
|
+
}
|
|
3492
|
+
get hasLoadingOverlay() {
|
|
3493
|
+
const isStreamingOrThinking = this.streamingStage === 'streaming' || this.streamingStage === 'thinking';
|
|
3494
|
+
const hasNoProcessedSections = !this.processedSections || this.processedSections.length === 0;
|
|
3495
|
+
const hasNoConfigSections = !this.cardConfig?.sections || this.cardConfig.sections.length === 0;
|
|
3496
|
+
return isStreamingOrThinking && hasNoProcessedSections && hasNoConfigSections;
|
|
3497
|
+
}
|
|
3498
|
+
onSectionEvent(event) {
|
|
3499
|
+
switch (event.type) {
|
|
3500
|
+
case 'field':
|
|
3501
|
+
if (event.field) {
|
|
3502
|
+
this.fieldInteraction.emit({
|
|
3503
|
+
field: event.field,
|
|
3504
|
+
action: 'click',
|
|
3505
|
+
sectionTitle: event.metadata?.['sectionTitle'] ?? event.section.title,
|
|
3506
|
+
metadata: event.metadata
|
|
3507
|
+
});
|
|
3508
|
+
}
|
|
3509
|
+
break;
|
|
3510
|
+
case 'item':
|
|
3511
|
+
if (event.item) {
|
|
3512
|
+
this.fieldInteraction.emit({
|
|
3513
|
+
item: event.item,
|
|
3514
|
+
action: 'click',
|
|
3515
|
+
sectionTitle: event.metadata?.['sectionTitle'] ?? event.section.title,
|
|
3516
|
+
metadata: event.metadata
|
|
3517
|
+
});
|
|
3518
|
+
}
|
|
3519
|
+
break;
|
|
3520
|
+
case 'action':
|
|
3521
|
+
if (event.action && this.cardConfig) {
|
|
3522
|
+
const identifier = event.action.action ?? event.action.id ?? event.action.label ?? 'section-action';
|
|
3523
|
+
this.cardInteraction.emit({
|
|
3524
|
+
action: identifier,
|
|
3525
|
+
card: this.cardConfig
|
|
3526
|
+
});
|
|
3527
|
+
}
|
|
3528
|
+
break;
|
|
3529
|
+
default:
|
|
3530
|
+
break;
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
onLayoutChange(layout) {
|
|
3534
|
+
// Layout change handler - kept for potential future use
|
|
3535
|
+
}
|
|
3536
|
+
getActionIconName(action) {
|
|
3537
|
+
// If icon is explicitly provided, use it
|
|
3538
|
+
if (action.icon) {
|
|
3539
|
+
return this.iconService.getFieldIcon(action.icon);
|
|
3540
|
+
}
|
|
3541
|
+
// If type is specified and it's a button behavior type (not legacy styling), use default icons
|
|
3542
|
+
if (action.type && ['mail', 'website', 'agent', 'question'].includes(action.type)) {
|
|
3543
|
+
switch (action.type) {
|
|
3544
|
+
case 'mail':
|
|
3545
|
+
return 'mail';
|
|
3546
|
+
case 'website':
|
|
3547
|
+
return 'external-link';
|
|
3548
|
+
case 'agent':
|
|
3549
|
+
return 'user';
|
|
3550
|
+
case 'question':
|
|
3551
|
+
return 'message-circle';
|
|
3552
|
+
default:
|
|
3553
|
+
break;
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
// Fallback to deriving icon from label
|
|
3557
|
+
return this.iconService.getFieldIcon(action.label);
|
|
3558
|
+
}
|
|
3559
|
+
/**
|
|
3560
|
+
* Get the default icon name for a button type (returns lucide icon name)
|
|
3561
|
+
* Uses 'type' field from JSON for button behavior
|
|
3562
|
+
*/
|
|
3563
|
+
getDefaultIconForButtonType(buttonType) {
|
|
3564
|
+
if (!buttonType || !['mail', 'website', 'agent', 'question'].includes(buttonType)) {
|
|
3565
|
+
return null;
|
|
3566
|
+
}
|
|
3567
|
+
switch (buttonType) {
|
|
3568
|
+
case 'mail':
|
|
3569
|
+
return 'mail';
|
|
3570
|
+
case 'website':
|
|
3571
|
+
return 'external-link';
|
|
3572
|
+
case 'agent':
|
|
3573
|
+
return 'user';
|
|
3574
|
+
case 'question':
|
|
3575
|
+
return 'message-circle';
|
|
3576
|
+
default:
|
|
3577
|
+
return null;
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
/**
|
|
3581
|
+
* Get the icon name to display for an action button
|
|
3582
|
+
* Returns the icon name (lucide icon name) or null if no icon should be shown
|
|
3583
|
+
*/
|
|
3584
|
+
getActionIconNameForDisplay(action) {
|
|
3585
|
+
// If explicit icon is provided and it's a URL, return null (will be handled as image)
|
|
3586
|
+
if (action.icon && action.icon.startsWith('http')) {
|
|
3587
|
+
return null;
|
|
3588
|
+
}
|
|
3589
|
+
// If explicit icon is provided and it's a lucide icon name, use it
|
|
3590
|
+
if (action.icon && !action.icon.startsWith('http')) {
|
|
3591
|
+
// Check if it's a lucide icon name (simple string like 'mail', 'user', etc.)
|
|
3592
|
+
if (/^[a-z-]+$/i.test(action.icon)) {
|
|
3593
|
+
return this.getActionIconName(action);
|
|
3594
|
+
}
|
|
3595
|
+
// Otherwise it's a text icon, return null (will be handled as text)
|
|
3596
|
+
return null;
|
|
3597
|
+
}
|
|
3598
|
+
// If no explicit icon, check if type is a button behavior type with default icon
|
|
3599
|
+
if (action.type && ['mail', 'website', 'agent', 'question'].includes(action.type)) {
|
|
3600
|
+
return this.getDefaultIconForButtonType(action.type);
|
|
3601
|
+
}
|
|
3602
|
+
// Fallback: try to derive icon from label
|
|
3603
|
+
const derivedIcon = this.getActionIconName(action);
|
|
3604
|
+
// Only use if it's a valid lucide icon name (simple string)
|
|
3605
|
+
if (derivedIcon && /^[a-z-]+$/i.test(derivedIcon)) {
|
|
3606
|
+
return derivedIcon;
|
|
3607
|
+
}
|
|
3608
|
+
return null;
|
|
3609
|
+
}
|
|
3610
|
+
/**
|
|
3611
|
+
* Check if action has a text icon (non-lucide, non-URL)
|
|
3612
|
+
*/
|
|
3613
|
+
hasTextIcon(action) {
|
|
3614
|
+
return !!(action.icon && !action.icon.startsWith('http') && !/^[a-z-]+$/i.test(action.icon));
|
|
3615
|
+
}
|
|
3616
|
+
/**
|
|
3617
|
+
* Check if action has an image icon (URL)
|
|
3618
|
+
*/
|
|
3619
|
+
hasImageIcon(action) {
|
|
3620
|
+
return !!(action.icon && action.icon.startsWith('http'));
|
|
3621
|
+
}
|
|
3622
|
+
getActionButtonClasses(action) {
|
|
3623
|
+
const primaryClasses = 'bg-[var(--color-brand)] text-white font-semibold border-0 hover:bg-[var(--color-brand)]/90 hover:shadow-lg hover:shadow-[var(--color-brand)]/40 active:scale-95';
|
|
3624
|
+
const outlineClasses = 'text-[var(--color-brand)] border border-[var(--color-brand)] bg-transparent font-semibold hover:bg-[var(--color-brand)]/10 active:scale-95';
|
|
3625
|
+
const ghostClasses = 'text-[var(--color-brand)] bg-transparent border-0 font-semibold hover:bg-[var(--color-brand)]/10 active:scale-95';
|
|
3626
|
+
// Use variant field if present, otherwise check legacy type field for styling
|
|
3627
|
+
const styleVariant = action.variant || (action.type === 'primary' || action.type === 'secondary' ? action.type : 'primary');
|
|
3628
|
+
switch (styleVariant) {
|
|
3629
|
+
case 'secondary':
|
|
3630
|
+
case 'outline':
|
|
3631
|
+
return outlineClasses;
|
|
3632
|
+
case 'ghost':
|
|
3633
|
+
return ghostClasses;
|
|
3634
|
+
default:
|
|
3635
|
+
return primaryClasses;
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
refreshProcessedSections(forceStructural = false) {
|
|
3639
|
+
if (!this._cardConfig?.sections?.length) {
|
|
3640
|
+
this.resetProcessedSections();
|
|
3641
|
+
return;
|
|
3642
|
+
}
|
|
3643
|
+
const sections = this._cardConfig.sections;
|
|
3644
|
+
const nextHash = this.hashSections(sections);
|
|
3645
|
+
const structureChanged = nextHash !== this.previousSectionsHash;
|
|
3646
|
+
const requiresStructuralRebuild = forceStructural ||
|
|
3647
|
+
structureChanged ||
|
|
3648
|
+
this._changeType === 'structural' ||
|
|
3649
|
+
!this.processedSections.length;
|
|
3650
|
+
// Removed excessive logging for performance
|
|
3651
|
+
if (requiresStructuralRebuild) {
|
|
3652
|
+
this.normalizedSectionCache = new WeakMap();
|
|
3653
|
+
}
|
|
3654
|
+
const normalizedSections = sections.map(section => this.getNormalizedSection(section, requiresStructuralRebuild));
|
|
3655
|
+
const orderedSections = requiresStructuralRebuild
|
|
3656
|
+
? this.sectionNormalizationService.sortSections(normalizedSections)
|
|
3657
|
+
: this.mergeWithPreviousOrder(normalizedSections);
|
|
3658
|
+
this.processedSections = orderedSections;
|
|
3659
|
+
this.sectionOrderKeys = orderedSections.map(section => this.getSectionKey(section));
|
|
3660
|
+
this.previousSectionsHash = nextHash;
|
|
3661
|
+
// Removed excessive logging for performance
|
|
3662
|
+
this.cdr.markForCheck();
|
|
3663
|
+
}
|
|
3664
|
+
getNormalizedSection(section, forceRebuild) {
|
|
3665
|
+
if (!forceRebuild) {
|
|
3666
|
+
const cached = this.normalizedSectionCache.get(section);
|
|
3667
|
+
if (cached) {
|
|
3668
|
+
return cached;
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
const normalized = this.sectionNormalizationService.normalizeSection(section);
|
|
3672
|
+
this.normalizedSectionCache.set(section, normalized);
|
|
3673
|
+
return normalized;
|
|
3674
|
+
}
|
|
3675
|
+
mergeWithPreviousOrder(normalizedSections) {
|
|
3676
|
+
if (!this.sectionOrderKeys.length) {
|
|
3677
|
+
return normalizedSections;
|
|
3678
|
+
}
|
|
3679
|
+
const nextByKey = new Map();
|
|
3680
|
+
normalizedSections.forEach(section => {
|
|
3681
|
+
nextByKey.set(this.getSectionKey(section), section);
|
|
3682
|
+
});
|
|
3683
|
+
const ordered = [];
|
|
3684
|
+
this.sectionOrderKeys.forEach(key => {
|
|
3685
|
+
const match = nextByKey.get(key);
|
|
3686
|
+
if (match) {
|
|
3687
|
+
ordered.push(match);
|
|
3688
|
+
nextByKey.delete(key);
|
|
3689
|
+
}
|
|
3690
|
+
});
|
|
3691
|
+
nextByKey.forEach(section => ordered.push(section));
|
|
3692
|
+
return ordered;
|
|
3693
|
+
}
|
|
3694
|
+
getSectionKey(section) {
|
|
3695
|
+
if (section.id) {
|
|
3696
|
+
return section.id;
|
|
3697
|
+
}
|
|
3698
|
+
const titleKey = section.title?.toLowerCase().replace(/[^a-z0-9]+/g, '-') ?? 'section';
|
|
3699
|
+
const typeKey = section.type ?? 'info';
|
|
3700
|
+
return `${titleKey}-${typeKey}`;
|
|
3701
|
+
}
|
|
3702
|
+
resetProcessedSections() {
|
|
3703
|
+
this.processedSections = [];
|
|
3704
|
+
this.sectionOrderKeys = [];
|
|
3705
|
+
this.previousSectionsHash = '';
|
|
3706
|
+
this.normalizedSectionCache = new WeakMap();
|
|
3707
|
+
this.cdr.markForCheck();
|
|
3708
|
+
}
|
|
3709
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AICardRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3710
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", 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
|
+
trigger('messageAnimation', [
|
|
3712
|
+
transition('* => *', [
|
|
3713
|
+
style({ opacity: 0, transform: 'translateY(10px)' }),
|
|
3714
|
+
animate('0.4s ease-out', style({ opacity: 1, transform: 'translateY(0)' }))
|
|
3715
|
+
])
|
|
3716
|
+
])
|
|
3717
|
+
], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
3718
|
+
}
|
|
3719
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AICardRendererComponent, decorators: [{
|
|
3720
|
+
type: Component,
|
|
3721
|
+
args: [{ selector: 'app-ai-card-renderer', standalone: true, imports: [CommonModule, LucideIconsModule, MasonryGridComponent], changeDetection: ChangeDetectionStrategy.OnPush, animations: [
|
|
3722
|
+
trigger('messageAnimation', [
|
|
3723
|
+
transition('* => *', [
|
|
3724
|
+
style({ opacity: 0, transform: 'translateY(10px)' }),
|
|
3725
|
+
animate('0.4s ease-out', style({ opacity: 1, transform: 'translateY(0)' }))
|
|
3726
|
+
])
|
|
3727
|
+
])
|
|
3728
|
+
], 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"] }]
|
|
3729
|
+
}], propDecorators: { cardConfig: [{
|
|
3730
|
+
type: Input
|
|
3731
|
+
}], updateSource: [{
|
|
3732
|
+
type: Input
|
|
3733
|
+
}], isFullscreen: [{
|
|
3734
|
+
type: Input
|
|
3735
|
+
}], tiltEnabled: [{
|
|
3736
|
+
type: Input
|
|
3737
|
+
}], streamingStage: [{
|
|
3738
|
+
type: Input
|
|
3739
|
+
}], streamingProgress: [{
|
|
3740
|
+
type: Input
|
|
3741
|
+
}], streamingProgressLabel: [{
|
|
3742
|
+
type: Input
|
|
3743
|
+
}], changeType: [{
|
|
3744
|
+
type: Input
|
|
3745
|
+
}], fieldInteraction: [{
|
|
3746
|
+
type: Output
|
|
3747
|
+
}], cardInteraction: [{
|
|
3748
|
+
type: Output
|
|
3749
|
+
}], fullscreenToggle: [{
|
|
3750
|
+
type: Output
|
|
3751
|
+
}], agentAction: [{
|
|
3752
|
+
type: Output
|
|
3753
|
+
}], questionAction: [{
|
|
3754
|
+
type: Output
|
|
3755
|
+
}], cardContainer: [{
|
|
3756
|
+
type: ViewChild,
|
|
3757
|
+
args: ['cardContainer']
|
|
3758
|
+
}], tiltContainerRef: [{
|
|
3759
|
+
type: ViewChild,
|
|
3760
|
+
args: ['tiltContainer']
|
|
3761
|
+
}], masonryGrid: [{
|
|
3762
|
+
type: ViewChild,
|
|
3763
|
+
args: [MasonryGridComponent]
|
|
3764
|
+
}], emptyStateContainer: [{
|
|
3765
|
+
type: ViewChild,
|
|
3766
|
+
args: ['emptyStateContainer']
|
|
3767
|
+
}] } });
|
|
3768
|
+
|
|
3769
|
+
class CardSkeletonComponent {
|
|
3770
|
+
constructor() {
|
|
3771
|
+
this.cardTitle = '';
|
|
3772
|
+
this.sectionCount = 0;
|
|
3773
|
+
this.isFullscreen = false;
|
|
3774
|
+
}
|
|
3775
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CardSkeletonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3776
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: CardSkeletonComponent, isStandalone: true, selector: "app-card-skeleton", inputs: { cardTitle: "cardTitle", sectionCount: "sectionCount", isFullscreen: "isFullscreen" }, ngImport: i0, template: "<div class=\"card-skeleton\" [class.card-skeleton--fullscreen]=\"isFullscreen\">\n <div class=\"card-skeleton-header\">\n <div class=\"skeleton-title\" *ngIf=\"cardTitle; else noTitle\">\n {{ cardTitle }}\n </div>\n <ng-template #noTitle>\n <div class=\"skeleton-title-placeholder\"></div>\n </ng-template>\n </div>\n \n <div class=\"card-skeleton-sections\" *ngIf=\"sectionCount > 0\">\n <div \n *ngFor=\"let section of [].constructor(sectionCount); let i = index\"\n class=\"skeleton-section\"\n [style.animation-delay.ms]=\"i * 100\">\n <div class=\"skeleton-section-header\">\n <div class=\"skeleton-line skeleton-line--title\"></div>\n </div>\n <div class=\"skeleton-section-content\">\n <div class=\"skeleton-line\" *ngFor=\"let line of [].constructor(2)\"></div>\n </div>\n </div>\n </div>\n \n <!-- Show placeholder when no sections yet -->\n <div class=\"card-skeleton-empty\" *ngIf=\"sectionCount === 0\">\n <div class=\"skeleton-line skeleton-line--empty\"></div>\n </div>\n</div>\n", styles: [".card-skeleton{width:100%;min-height:400px;background:var(--card-background, var(--background));border:1px solid var(--border);border-radius:var(--card-border-radius, 1rem);padding:var(--card-main-padding, 1.5rem);display:flex;flex-direction:column;gap:1.5rem;position:relative;overflow:hidden}.card-skeleton--fullscreen{min-height:100%;border-radius:0}.card-skeleton-header{padding-bottom:1rem;border-bottom:1px solid var(--border)}.skeleton-title{font-size:1.5rem;font-weight:600;color:var(--foreground);min-height:2rem;display:flex;align-items:center}.skeleton-title-placeholder{width:60%;height:2rem;background:linear-gradient(90deg,color-mix(in srgb,var(--muted, rgba(255, 255, 255, .1)) 100%,transparent),color-mix(in srgb,var(--muted-foreground, rgba(255, 255, 255, .2)) 100%,transparent) 20%,color-mix(in srgb,#FF7900 15%,transparent) 50%,color-mix(in srgb,var(--muted, rgba(255, 255, 255, .1)) 100%,transparent));background-size:200% 100%;border-radius:.5rem;animation:shimmerWave 1.5s ease-in-out infinite;position:relative;overflow:hidden;box-shadow:0 2px 4px color-mix(in srgb,var(--foreground) 5%,transparent)}.skeleton-title-placeholder:after{content:\"\";position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent 0%,color-mix(in srgb,#FF7900 20%,transparent) 50%,transparent 100%);animation:shimmerWave 1.5s ease-in-out infinite}.card-skeleton-sections{display:flex;flex-direction:column;gap:1.5rem;flex:1}.skeleton-section{opacity:0;animation:skeleton-fade-in .4s ease-out forwards;padding:1.25rem;border:1px solid color-mix(in srgb,var(--border) 60%,transparent);border-radius:.75rem;background:linear-gradient(135deg,var(--card-background, var(--background)) 0%,color-mix(in srgb,var(--card-background, var(--background)) 98%,#FF7900 2%) 100%);box-shadow:0 2px 4px color-mix(in srgb,var(--foreground) 5%,transparent);position:relative;overflow:hidden}.skeleton-section:before{content:\"\";position:absolute;left:0;top:0;bottom:0;width:2px;background:linear-gradient(180deg,#ff7900,color-mix(in srgb,#FF7900 50%,transparent));opacity:.3}.skeleton-section:nth-child(1){animation-delay:.1s}.skeleton-section:nth-child(2){animation-delay:.2s}.skeleton-section:nth-child(3){animation-delay:.3s}.skeleton-section:nth-child(4){animation-delay:.4s}.skeleton-section:nth-child(5){animation-delay:.5s}.skeleton-section:nth-child(6){animation-delay:.6s}.skeleton-section:nth-child(7){animation-delay:.7s}.skeleton-section:nth-child(8){animation-delay:.8s}.skeleton-section:nth-child(9){animation-delay:.9s}.skeleton-section:nth-child(10){animation-delay:1s}.skeleton-section-header{margin-bottom:.75rem}.skeleton-section-content{display:flex;flex-direction:column;gap:.5rem}.skeleton-line{height:1rem;background:linear-gradient(90deg,color-mix(in srgb,var(--muted, rgba(255, 255, 255, .1)) 100%,transparent),color-mix(in srgb,var(--muted-foreground, rgba(255, 255, 255, .2)) 100%,transparent) 20%,color-mix(in srgb,#FF7900 10%,transparent) 50%,color-mix(in srgb,var(--muted, rgba(255, 255, 255, .1)) 100%,transparent));background-size:200% 100%;border-radius:.375rem;animation:shimmerWave 1.5s ease-in-out infinite;position:relative;overflow:hidden}.skeleton-line:after{content:\"\";position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent 0%,color-mix(in srgb,#FF7900 15%,transparent) 50%,transparent 100%);animation:shimmerWave 1.5s ease-in-out infinite}.skeleton-line--title{width:40%;height:1.25rem}.skeleton-line:not(.skeleton-line--title){width:100%}.skeleton-line:not(.skeleton-line--title):nth-child(2){width:80%}.card-skeleton-empty{padding:2rem;display:flex;align-items:center;justify-content:center;min-height:200px}.skeleton-line--empty{width:60%;height:1.5rem}@keyframes shimmerWave{0%{background-position:-200% 0}to{background-position:200% 0}}@keyframes skeleton-fade-in{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
3777
|
+
}
|
|
3778
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CardSkeletonComponent, decorators: [{
|
|
3779
|
+
type: Component,
|
|
3780
|
+
args: [{ selector: 'app-card-skeleton', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"card-skeleton\" [class.card-skeleton--fullscreen]=\"isFullscreen\">\n <div class=\"card-skeleton-header\">\n <div class=\"skeleton-title\" *ngIf=\"cardTitle; else noTitle\">\n {{ cardTitle }}\n </div>\n <ng-template #noTitle>\n <div class=\"skeleton-title-placeholder\"></div>\n </ng-template>\n </div>\n \n <div class=\"card-skeleton-sections\" *ngIf=\"sectionCount > 0\">\n <div \n *ngFor=\"let section of [].constructor(sectionCount); let i = index\"\n class=\"skeleton-section\"\n [style.animation-delay.ms]=\"i * 100\">\n <div class=\"skeleton-section-header\">\n <div class=\"skeleton-line skeleton-line--title\"></div>\n </div>\n <div class=\"skeleton-section-content\">\n <div class=\"skeleton-line\" *ngFor=\"let line of [].constructor(2)\"></div>\n </div>\n </div>\n </div>\n \n <!-- Show placeholder when no sections yet -->\n <div class=\"card-skeleton-empty\" *ngIf=\"sectionCount === 0\">\n <div class=\"skeleton-line skeleton-line--empty\"></div>\n </div>\n</div>\n", styles: [".card-skeleton{width:100%;min-height:400px;background:var(--card-background, var(--background));border:1px solid var(--border);border-radius:var(--card-border-radius, 1rem);padding:var(--card-main-padding, 1.5rem);display:flex;flex-direction:column;gap:1.5rem;position:relative;overflow:hidden}.card-skeleton--fullscreen{min-height:100%;border-radius:0}.card-skeleton-header{padding-bottom:1rem;border-bottom:1px solid var(--border)}.skeleton-title{font-size:1.5rem;font-weight:600;color:var(--foreground);min-height:2rem;display:flex;align-items:center}.skeleton-title-placeholder{width:60%;height:2rem;background:linear-gradient(90deg,color-mix(in srgb,var(--muted, rgba(255, 255, 255, .1)) 100%,transparent),color-mix(in srgb,var(--muted-foreground, rgba(255, 255, 255, .2)) 100%,transparent) 20%,color-mix(in srgb,#FF7900 15%,transparent) 50%,color-mix(in srgb,var(--muted, rgba(255, 255, 255, .1)) 100%,transparent));background-size:200% 100%;border-radius:.5rem;animation:shimmerWave 1.5s ease-in-out infinite;position:relative;overflow:hidden;box-shadow:0 2px 4px color-mix(in srgb,var(--foreground) 5%,transparent)}.skeleton-title-placeholder:after{content:\"\";position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent 0%,color-mix(in srgb,#FF7900 20%,transparent) 50%,transparent 100%);animation:shimmerWave 1.5s ease-in-out infinite}.card-skeleton-sections{display:flex;flex-direction:column;gap:1.5rem;flex:1}.skeleton-section{opacity:0;animation:skeleton-fade-in .4s ease-out forwards;padding:1.25rem;border:1px solid color-mix(in srgb,var(--border) 60%,transparent);border-radius:.75rem;background:linear-gradient(135deg,var(--card-background, var(--background)) 0%,color-mix(in srgb,var(--card-background, var(--background)) 98%,#FF7900 2%) 100%);box-shadow:0 2px 4px color-mix(in srgb,var(--foreground) 5%,transparent);position:relative;overflow:hidden}.skeleton-section:before{content:\"\";position:absolute;left:0;top:0;bottom:0;width:2px;background:linear-gradient(180deg,#ff7900,color-mix(in srgb,#FF7900 50%,transparent));opacity:.3}.skeleton-section:nth-child(1){animation-delay:.1s}.skeleton-section:nth-child(2){animation-delay:.2s}.skeleton-section:nth-child(3){animation-delay:.3s}.skeleton-section:nth-child(4){animation-delay:.4s}.skeleton-section:nth-child(5){animation-delay:.5s}.skeleton-section:nth-child(6){animation-delay:.6s}.skeleton-section:nth-child(7){animation-delay:.7s}.skeleton-section:nth-child(8){animation-delay:.8s}.skeleton-section:nth-child(9){animation-delay:.9s}.skeleton-section:nth-child(10){animation-delay:1s}.skeleton-section-header{margin-bottom:.75rem}.skeleton-section-content{display:flex;flex-direction:column;gap:.5rem}.skeleton-line{height:1rem;background:linear-gradient(90deg,color-mix(in srgb,var(--muted, rgba(255, 255, 255, .1)) 100%,transparent),color-mix(in srgb,var(--muted-foreground, rgba(255, 255, 255, .2)) 100%,transparent) 20%,color-mix(in srgb,#FF7900 10%,transparent) 50%,color-mix(in srgb,var(--muted, rgba(255, 255, 255, .1)) 100%,transparent));background-size:200% 100%;border-radius:.375rem;animation:shimmerWave 1.5s ease-in-out infinite;position:relative;overflow:hidden}.skeleton-line:after{content:\"\";position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent 0%,color-mix(in srgb,#FF7900 15%,transparent) 50%,transparent 100%);animation:shimmerWave 1.5s ease-in-out infinite}.skeleton-line--title{width:40%;height:1.25rem}.skeleton-line:not(.skeleton-line--title){width:100%}.skeleton-line:not(.skeleton-line--title):nth-child(2){width:80%}.card-skeleton-empty{padding:2rem;display:flex;align-items:center;justify-content:center;min-height:200px}.skeleton-line--empty{width:60%;height:1.5rem}@keyframes shimmerWave{0%{background-position:-200% 0}to{background-position:200% 0}}@keyframes skeleton-fade-in{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}\n"] }]
|
|
3781
|
+
}], propDecorators: { cardTitle: [{
|
|
3782
|
+
type: Input
|
|
3783
|
+
}], sectionCount: [{
|
|
3784
|
+
type: Input
|
|
3785
|
+
}], isFullscreen: [{
|
|
3786
|
+
type: Input
|
|
3787
|
+
}] } });
|
|
3788
|
+
|
|
3789
|
+
class CardPreviewComponent {
|
|
3790
|
+
constructor(cdr) {
|
|
3791
|
+
this.cdr = cdr;
|
|
3792
|
+
this.generatedCard = null;
|
|
3793
|
+
this.isGenerating = false;
|
|
3794
|
+
this.isInitialized = false;
|
|
3795
|
+
this.isFullscreen = false;
|
|
3796
|
+
this.cardInteraction = new EventEmitter();
|
|
3797
|
+
this.fieldInteraction = new EventEmitter();
|
|
3798
|
+
this.fullscreenToggle = new EventEmitter();
|
|
3799
|
+
this.agentAction = new EventEmitter();
|
|
3800
|
+
this.questionAction = new EventEmitter();
|
|
3801
|
+
}
|
|
3802
|
+
ngOnInit() {
|
|
3803
|
+
// Component initialized
|
|
3804
|
+
}
|
|
3805
|
+
ngOnChanges(changes) {
|
|
3806
|
+
if (changes['generatedCard'] || changes['isGenerating'] || changes['isInitialized']) {
|
|
3807
|
+
this.cdr.markForCheck();
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
ngOnDestroy() {
|
|
3811
|
+
// Cleanup if needed
|
|
3812
|
+
}
|
|
3813
|
+
get showSkeleton() {
|
|
3814
|
+
return this.isGenerating && !this.generatedCard;
|
|
3815
|
+
}
|
|
3816
|
+
onCardInteraction(event) {
|
|
3817
|
+
this.cardInteraction.emit(event);
|
|
3818
|
+
}
|
|
3819
|
+
onFieldInteraction(event) {
|
|
3820
|
+
this.fieldInteraction.emit(event);
|
|
3821
|
+
}
|
|
3822
|
+
onFullscreenToggle(isFullscreen) {
|
|
3823
|
+
this.fullscreenToggle.emit(isFullscreen);
|
|
3824
|
+
}
|
|
3825
|
+
onAgentAction(event) {
|
|
3826
|
+
this.agentAction.emit(event);
|
|
3827
|
+
}
|
|
3828
|
+
onQuestionAction(event) {
|
|
3829
|
+
this.questionAction.emit(event);
|
|
3830
|
+
}
|
|
3831
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CardPreviewComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3832
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: CardPreviewComponent, isStandalone: true, selector: "app-card-preview", inputs: { generatedCard: "generatedCard", isGenerating: "isGenerating", isInitialized: "isInitialized", isFullscreen: "isFullscreen" }, outputs: { cardInteraction: "cardInteraction", fieldInteraction: "fieldInteraction", fullscreenToggle: "fullscreenToggle", agentAction: "agentAction", questionAction: "questionAction" }, usesOnChanges: true, ngImport: i0, template: " <ng-container [attr.aria-busy]=\"isGenerating\">\n <!-- Initial loading state -->\n <ng-container *ngIf=\"!isInitialized; else initializedState\">\n <div class=\"preview-loading\" role=\"status\">\n <div class=\"preview-spinner\"></div>\n <div>\n <p class=\"preview-loading-title\">Initializing System\u2026</p>\n <p class=\"preview-loading-subtitle\">Loading templates and services</p>\n </div>\n </div>\n </ng-container>\n\n <ng-template #initializedState>\n <!-- Show skeleton frame only when generating -->\n <ng-container *ngIf=\"showSkeleton; else cardContent\">\n <app-card-skeleton\n [cardTitle]=\"generatedCard?.cardTitle || ''\"\n [sectionCount]=\"generatedCard?.sections?.length || 0\"\n [isFullscreen]=\"isFullscreen\">\n </app-card-skeleton>\n </ng-container>\n\n <ng-template #cardContent>\n <!-- Show card if we have generatedCard -->\n <ng-container *ngIf=\"generatedCard; else previewEmpty\">\n <div class=\"card-preview-container\">\n <app-ai-card-renderer\n [cardConfig]=\"generatedCard\"\n [updateSource]=\"'liveEdit'\"\n [isFullscreen]=\"isFullscreen\"\n (cardInteraction)=\"onCardInteraction($event)\"\n (fieldInteraction)=\"onFieldInteraction($event)\"\n (fullscreenToggle)=\"onFullscreenToggle($event)\"\n (agentAction)=\"onAgentAction($event)\"\n (questionAction)=\"onQuestionAction($event)\">\n </app-ai-card-renderer>\n </div>\n </ng-container>\n </ng-template>\n\n <ng-template #previewEmpty>\n <div class=\"preview-empty\">\n <div class=\"preview-empty-icon\" aria-hidden=\"true\">\n <div class=\"code-icon w-8 h-8\"></div>\n </div>\n <div class=\"preview-empty-copy\">\n <p class=\"preview-empty-title\">No Card Preview</p>\n <p class=\"preview-empty-subtitle\">\n Enter a valid JSON configuration or load a template to see your card design render in real time.\n </p>\n </div>\n <div class=\"preview-empty-hints\">\n <div class=\"preview-empty-hint\">\n <div class=\"hint-dot bg-green-500\"></div>\n <span>Valid JSON auto-generates layouts</span>\n </div>\n <div class=\"preview-empty-hint\">\n <div class=\"hint-dot bg-blue-500\"></div>\n <span>Magnetic tilt responds to cursor position</span>\n </div>\n <div class=\"preview-empty-hint\">\n <div class=\"hint-dot bg-purple-500\"></div>\n <span>Export high-fidelity PNG snapshots</span>\n </div>\n </div>\n </div>\n </ng-template>\n </ng-template>\n </ng-container>\n", styles: [":host{display:block;width:100%;height:100%}.preview-shell{position:relative;padding:clamp(1.5rem,3vw,2.25rem);border-radius:1.25rem;background:#0c0c0ce6;border:1px solid rgba(255,121,0,.25);box-shadow:0 4px 14px #0000002e,0 0 12px #ff790014;-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);overflow:hidden}.preview-shell:before,.preview-shell:after{content:\"\";position:absolute;inset:0;opacity:0;pointer-events:none}.preview-header{position:relative;z-index:1;display:flex;align-items:center;gap:.75rem;margin-bottom:1.75rem}.preview-heading{display:flex;flex-direction:column;gap:.25rem}.preview-title{font-size:1.125rem;font-weight:700;color:var(--foreground)}.preview-subtitle{font-size:.85rem;color:#ffffffa6;letter-spacing:.05em;text-transform:uppercase}app-ai-card-renderer{display:block;width:100%;height:100%}.card-preview-container{display:block;width:100%;height:100%;transition:opacity .4s cubic-bezier(.4,0,.2,1),transform .4s cubic-bezier(.4,0,.2,1);will-change:opacity,transform;animation:fadeInScale .5s cubic-bezier(.4,0,.2,1)}@keyframes fadeInScale{0%{opacity:0;transform:scale(.98)}to{opacity:1;transform:scale(1)}}@keyframes fadeInUp{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.preview-loading{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2rem;text-align:center;padding:5rem 1.5rem;position:relative;min-height:400px;&:before{content:\"\";position:absolute;inset:0;background:radial-gradient(circle at center,color-mix(in srgb,#FF7900 8%,transparent) 0%,transparent 70%);animation:pulseGlow 2s ease-in-out infinite;pointer-events:none}}@keyframes pulseGlow{0%,to{opacity:.5;transform:scale(1)}50%{opacity:.8;transform:scale(1.1)}}.preview-spinner{width:3.5rem;height:3.5rem;border-radius:999px;border:4px solid color-mix(in srgb,#FF7900 20%,transparent);border-top-color:#ff7900;border-right-color:#ff7900;animation:spin .8s linear infinite;position:relative;z-index:1;box-shadow:0 0 20px color-mix(in srgb,#FF7900 30%,transparent);&:after{content:\"\";position:absolute;inset:-4px;border-radius:999px;border:4px solid transparent;border-top-color:color-mix(in srgb,#FF7900 40%,transparent);animation:spin 1.2s linear infinite reverse}}.preview-loading-title{font-size:1.25rem;font-weight:700;color:var(--foreground);position:relative;z-index:1;letter-spacing:-.01em;animation:fadeInUp .6s ease-out}.preview-loading-subtitle{font-size:.9375rem;color:var(--muted-foreground);position:relative;z-index:1;animation:fadeInUp .6s ease-out .2s both}.preview-empty{display:flex;flex-direction:column;align-items:center;text-align:center;gap:2rem;padding:5rem 1.5rem;position:relative;min-height:400px;&:before{content:\"\";position:absolute;inset:0;background:radial-gradient(circle at center,color-mix(in srgb,#FF7900 5%,transparent) 0%,transparent 70%);animation:pulseGlow 3s ease-in-out infinite;pointer-events:none}}.preview-empty-icon{width:5rem;height:5rem;border-radius:999px;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,color-mix(in srgb,#FF7900 20%,transparent),color-mix(in srgb,#FF7900 12%,transparent));border:2px solid color-mix(in srgb,#FF7900 40%,transparent);box-shadow:0 8px 24px color-mix(in srgb,#FF7900 20%,transparent),inset 0 0 0 1px color-mix(in srgb,rgba(255,255,255,.1),transparent);position:relative;z-index:1;animation:float 3s ease-in-out infinite;&:after{content:\"\";position:absolute;inset:-4px;border-radius:999px;border:2px solid color-mix(in srgb,#FF7900 20%,transparent);animation:pulseRing 2s ease-in-out infinite}}@keyframes float{0%,to{transform:translateY(0)}50%{transform:translateY(-10px)}}@keyframes pulseRing{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(1.3)}}.preview-empty-title{font-size:1.375rem;font-weight:700;color:var(--foreground);letter-spacing:-.01em;position:relative;z-index:1;animation:fadeInUp .6s ease-out}.preview-empty-subtitle{font-size:1rem;color:var(--muted-foreground);max-width:28rem;line-height:1.6;position:relative;z-index:1;animation:fadeInUp .6s ease-out .2s both}.preview-empty-hints{display:flex;flex-direction:column;gap:1rem;font-size:.875rem;color:var(--muted-foreground);position:relative;z-index:1;animation:fadeInUp .6s ease-out .4s both}.preview-empty-hint{display:flex;align-items:center;gap:.75rem;justify-content:center;padding:.5rem 1rem;background:color-mix(in srgb,var(--card-background) 80%,transparent);border:1px solid color-mix(in srgb,var(--border) 50%,transparent);border-radius:.5rem;transition:all .3s ease;&:hover{background:color-mix(in srgb,var(--card-background) 90%,transparent);border-color:color-mix(in srgb,#FF7900 30%,transparent);transform:translate(4px)}}.hint-dot{width:8px;height:8px;border-radius:999px;box-shadow:0 0 12px currentColor;flex-shrink:0;animation:pulse 2s ease-in-out infinite}.badge{background-color:#ff790033;color:var(--primary);border:1px solid rgba(255,121,0,.35);padding:.3rem .6rem;border-radius:999px;font-size:.7rem;font-weight:600;text-transform:uppercase;letter-spacing:.08em;margin-left:auto}.sparkles-icon,.code-icon{display:inline-flex;align-items:center;justify-content:center}.sparkles-icon:before{content:\"\\2728\";font-size:1.2rem;color:var(--primary)}.code-icon:before{content:\"\\1f4bb\";font-size:1.2rem;color:var(--primary)}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@media (min-width: 768px){.preview-empty{padding:5rem 2rem}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: AICardRendererComponent, selector: "app-ai-card-renderer", inputs: ["cardConfig", "updateSource", "isFullscreen", "tiltEnabled", "streamingStage", "streamingProgress", "streamingProgressLabel", "changeType"], outputs: ["fieldInteraction", "cardInteraction", "fullscreenToggle", "agentAction", "questionAction"] }, { kind: "component", type: CardSkeletonComponent, selector: "app-card-skeleton", inputs: ["cardTitle", "sectionCount", "isFullscreen"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
3833
|
+
}
|
|
3834
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CardPreviewComponent, decorators: [{
|
|
3835
|
+
type: Component,
|
|
3836
|
+
args: [{ selector: 'app-card-preview', standalone: true, imports: [CommonModule, AICardRendererComponent, CardSkeletonComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: " <ng-container [attr.aria-busy]=\"isGenerating\">\n <!-- Initial loading state -->\n <ng-container *ngIf=\"!isInitialized; else initializedState\">\n <div class=\"preview-loading\" role=\"status\">\n <div class=\"preview-spinner\"></div>\n <div>\n <p class=\"preview-loading-title\">Initializing System\u2026</p>\n <p class=\"preview-loading-subtitle\">Loading templates and services</p>\n </div>\n </div>\n </ng-container>\n\n <ng-template #initializedState>\n <!-- Show skeleton frame only when generating -->\n <ng-container *ngIf=\"showSkeleton; else cardContent\">\n <app-card-skeleton\n [cardTitle]=\"generatedCard?.cardTitle || ''\"\n [sectionCount]=\"generatedCard?.sections?.length || 0\"\n [isFullscreen]=\"isFullscreen\">\n </app-card-skeleton>\n </ng-container>\n\n <ng-template #cardContent>\n <!-- Show card if we have generatedCard -->\n <ng-container *ngIf=\"generatedCard; else previewEmpty\">\n <div class=\"card-preview-container\">\n <app-ai-card-renderer\n [cardConfig]=\"generatedCard\"\n [updateSource]=\"'liveEdit'\"\n [isFullscreen]=\"isFullscreen\"\n (cardInteraction)=\"onCardInteraction($event)\"\n (fieldInteraction)=\"onFieldInteraction($event)\"\n (fullscreenToggle)=\"onFullscreenToggle($event)\"\n (agentAction)=\"onAgentAction($event)\"\n (questionAction)=\"onQuestionAction($event)\">\n </app-ai-card-renderer>\n </div>\n </ng-container>\n </ng-template>\n\n <ng-template #previewEmpty>\n <div class=\"preview-empty\">\n <div class=\"preview-empty-icon\" aria-hidden=\"true\">\n <div class=\"code-icon w-8 h-8\"></div>\n </div>\n <div class=\"preview-empty-copy\">\n <p class=\"preview-empty-title\">No Card Preview</p>\n <p class=\"preview-empty-subtitle\">\n Enter a valid JSON configuration or load a template to see your card design render in real time.\n </p>\n </div>\n <div class=\"preview-empty-hints\">\n <div class=\"preview-empty-hint\">\n <div class=\"hint-dot bg-green-500\"></div>\n <span>Valid JSON auto-generates layouts</span>\n </div>\n <div class=\"preview-empty-hint\">\n <div class=\"hint-dot bg-blue-500\"></div>\n <span>Magnetic tilt responds to cursor position</span>\n </div>\n <div class=\"preview-empty-hint\">\n <div class=\"hint-dot bg-purple-500\"></div>\n <span>Export high-fidelity PNG snapshots</span>\n </div>\n </div>\n </div>\n </ng-template>\n </ng-template>\n </ng-container>\n", styles: [":host{display:block;width:100%;height:100%}.preview-shell{position:relative;padding:clamp(1.5rem,3vw,2.25rem);border-radius:1.25rem;background:#0c0c0ce6;border:1px solid rgba(255,121,0,.25);box-shadow:0 4px 14px #0000002e,0 0 12px #ff790014;-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);overflow:hidden}.preview-shell:before,.preview-shell:after{content:\"\";position:absolute;inset:0;opacity:0;pointer-events:none}.preview-header{position:relative;z-index:1;display:flex;align-items:center;gap:.75rem;margin-bottom:1.75rem}.preview-heading{display:flex;flex-direction:column;gap:.25rem}.preview-title{font-size:1.125rem;font-weight:700;color:var(--foreground)}.preview-subtitle{font-size:.85rem;color:#ffffffa6;letter-spacing:.05em;text-transform:uppercase}app-ai-card-renderer{display:block;width:100%;height:100%}.card-preview-container{display:block;width:100%;height:100%;transition:opacity .4s cubic-bezier(.4,0,.2,1),transform .4s cubic-bezier(.4,0,.2,1);will-change:opacity,transform;animation:fadeInScale .5s cubic-bezier(.4,0,.2,1)}@keyframes fadeInScale{0%{opacity:0;transform:scale(.98)}to{opacity:1;transform:scale(1)}}@keyframes fadeInUp{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.preview-loading{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2rem;text-align:center;padding:5rem 1.5rem;position:relative;min-height:400px;&:before{content:\"\";position:absolute;inset:0;background:radial-gradient(circle at center,color-mix(in srgb,#FF7900 8%,transparent) 0%,transparent 70%);animation:pulseGlow 2s ease-in-out infinite;pointer-events:none}}@keyframes pulseGlow{0%,to{opacity:.5;transform:scale(1)}50%{opacity:.8;transform:scale(1.1)}}.preview-spinner{width:3.5rem;height:3.5rem;border-radius:999px;border:4px solid color-mix(in srgb,#FF7900 20%,transparent);border-top-color:#ff7900;border-right-color:#ff7900;animation:spin .8s linear infinite;position:relative;z-index:1;box-shadow:0 0 20px color-mix(in srgb,#FF7900 30%,transparent);&:after{content:\"\";position:absolute;inset:-4px;border-radius:999px;border:4px solid transparent;border-top-color:color-mix(in srgb,#FF7900 40%,transparent);animation:spin 1.2s linear infinite reverse}}.preview-loading-title{font-size:1.25rem;font-weight:700;color:var(--foreground);position:relative;z-index:1;letter-spacing:-.01em;animation:fadeInUp .6s ease-out}.preview-loading-subtitle{font-size:.9375rem;color:var(--muted-foreground);position:relative;z-index:1;animation:fadeInUp .6s ease-out .2s both}.preview-empty{display:flex;flex-direction:column;align-items:center;text-align:center;gap:2rem;padding:5rem 1.5rem;position:relative;min-height:400px;&:before{content:\"\";position:absolute;inset:0;background:radial-gradient(circle at center,color-mix(in srgb,#FF7900 5%,transparent) 0%,transparent 70%);animation:pulseGlow 3s ease-in-out infinite;pointer-events:none}}.preview-empty-icon{width:5rem;height:5rem;border-radius:999px;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,color-mix(in srgb,#FF7900 20%,transparent),color-mix(in srgb,#FF7900 12%,transparent));border:2px solid color-mix(in srgb,#FF7900 40%,transparent);box-shadow:0 8px 24px color-mix(in srgb,#FF7900 20%,transparent),inset 0 0 0 1px color-mix(in srgb,rgba(255,255,255,.1),transparent);position:relative;z-index:1;animation:float 3s ease-in-out infinite;&:after{content:\"\";position:absolute;inset:-4px;border-radius:999px;border:2px solid color-mix(in srgb,#FF7900 20%,transparent);animation:pulseRing 2s ease-in-out infinite}}@keyframes float{0%,to{transform:translateY(0)}50%{transform:translateY(-10px)}}@keyframes pulseRing{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(1.3)}}.preview-empty-title{font-size:1.375rem;font-weight:700;color:var(--foreground);letter-spacing:-.01em;position:relative;z-index:1;animation:fadeInUp .6s ease-out}.preview-empty-subtitle{font-size:1rem;color:var(--muted-foreground);max-width:28rem;line-height:1.6;position:relative;z-index:1;animation:fadeInUp .6s ease-out .2s both}.preview-empty-hints{display:flex;flex-direction:column;gap:1rem;font-size:.875rem;color:var(--muted-foreground);position:relative;z-index:1;animation:fadeInUp .6s ease-out .4s both}.preview-empty-hint{display:flex;align-items:center;gap:.75rem;justify-content:center;padding:.5rem 1rem;background:color-mix(in srgb,var(--card-background) 80%,transparent);border:1px solid color-mix(in srgb,var(--border) 50%,transparent);border-radius:.5rem;transition:all .3s ease;&:hover{background:color-mix(in srgb,var(--card-background) 90%,transparent);border-color:color-mix(in srgb,#FF7900 30%,transparent);transform:translate(4px)}}.hint-dot{width:8px;height:8px;border-radius:999px;box-shadow:0 0 12px currentColor;flex-shrink:0;animation:pulse 2s ease-in-out infinite}.badge{background-color:#ff790033;color:var(--primary);border:1px solid rgba(255,121,0,.35);padding:.3rem .6rem;border-radius:999px;font-size:.7rem;font-weight:600;text-transform:uppercase;letter-spacing:.08em;margin-left:auto}.sparkles-icon,.code-icon{display:inline-flex;align-items:center;justify-content:center}.sparkles-icon:before{content:\"\\2728\";font-size:1.2rem;color:var(--primary)}.code-icon:before{content:\"\\1f4bb\";font-size:1.2rem;color:var(--primary)}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@media (min-width: 768px){.preview-empty{padding:5rem 2rem}}\n"] }]
|
|
3837
|
+
}], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { generatedCard: [{
|
|
3838
|
+
type: Input
|
|
3839
|
+
}], isGenerating: [{
|
|
3840
|
+
type: Input
|
|
3841
|
+
}], isInitialized: [{
|
|
3842
|
+
type: Input
|
|
3843
|
+
}], isFullscreen: [{
|
|
3844
|
+
type: Input
|
|
3845
|
+
}], cardInteraction: [{
|
|
3846
|
+
type: Output
|
|
3847
|
+
}], fieldInteraction: [{
|
|
3848
|
+
type: Output
|
|
3849
|
+
}], fullscreenToggle: [{
|
|
3850
|
+
type: Output
|
|
3851
|
+
}], agentAction: [{
|
|
3852
|
+
type: Output
|
|
3853
|
+
}], questionAction: [{
|
|
3854
|
+
type: Output
|
|
3855
|
+
}] } });
|
|
3856
|
+
|
|
3857
|
+
class NewsSectionComponent extends BaseSectionComponent {
|
|
3858
|
+
get newsItems() {
|
|
3859
|
+
return this.getItems();
|
|
3860
|
+
}
|
|
3861
|
+
formatSource(item) {
|
|
3862
|
+
const meta = item.meta ?? {};
|
|
3863
|
+
return (typeof meta['source'] === 'string' && meta['source'])
|
|
3864
|
+
? meta['source']
|
|
3865
|
+
: (typeof meta['publisher'] === 'string' ? meta['publisher'] : 'News');
|
|
3866
|
+
}
|
|
3867
|
+
formatTimestamp(item) {
|
|
3868
|
+
const meta = item.meta ?? {};
|
|
3869
|
+
const timestamp = meta['publishedAt'] ?? meta['time'] ?? meta['date'];
|
|
3870
|
+
return typeof timestamp === 'string' ? timestamp : '';
|
|
3871
|
+
}
|
|
3872
|
+
/**
|
|
3873
|
+
* Get display description, hiding "Streaming…" placeholder text
|
|
3874
|
+
* Inline implementation to avoid TypeScript override conflicts
|
|
3875
|
+
*/
|
|
3876
|
+
getDisplayDescription(item) {
|
|
3877
|
+
const description = item.description;
|
|
3878
|
+
if (description === 'Streaming…' || description === 'Streaming...') {
|
|
3879
|
+
return '';
|
|
3880
|
+
}
|
|
3881
|
+
return description ?? '';
|
|
3882
|
+
}
|
|
3883
|
+
trackItem(index, item) {
|
|
3884
|
+
return item.id ?? `news-item-${index}-${this.formatSource(item)}`;
|
|
3885
|
+
}
|
|
3886
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NewsSectionComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
3887
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: NewsSectionComponent, isStandalone: true, selector: "app-news-section", usesInheritance: true, ngImport: i0, template: "<div class=\"ai-section ai-section--news section-block section-block--news\">\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 <span class=\"ai-section__badge\" *ngIf=\"section.meta?.['category']\">\n {{ section.meta?.['category'] }}\n </span>\n </div>\n\n <div class=\"ai-section__body\">\n <div *ngIf=\"newsItems?.length; else newsEmpty\">\n <div class=\"news-feed\">\n <button\n *ngFor=\"let item of newsItems; trackBy: trackItem; let idx = index\"\n type=\"button\"\n class=\"news-item\"\n (click)=\"emitItemInteraction(item, { streamIndex: idx })\"\n [class.item-streaming]=\"getItemAnimationClass(getItemId(item, idx), idx) === 'item-streaming'\"\n [class.item-entered]=\"getItemAnimationClass(getItemId(item, idx), idx) === 'item-entered'\"\n >\n <div class=\"news-item__header\">\n <div class=\"news-item__tag\">\n <lucide-icon name=\"newspaper\" size=\"16\" aria-hidden=\"true\"></lucide-icon>\n <span>{{ formatSource(item) }}</span>\n </div>\n <span class=\"news-item__time\" *ngIf=\"formatTimestamp(item)\">{{ formatTimestamp(item) }}</span>\n </div>\n <p class=\"news-item__title\">{{ item.title }}</p>\n <p class=\"news-item__summary\" *ngIf=\"getDisplayDescription(item)\">{{ getDisplayDescription(item) }}</p>\n <div class=\"news-item__meta\" *ngIf=\"item.value\">\n <span class=\"news-item__value\">{{ item.value }}</span>\n <span class=\"news-item__indicator\">Live</span>\n </div>\n </button>\n </div>\n </div>\n\n <ng-template #newsEmpty>\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 headlines available</p>\n </div>\n </ng-template>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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 }); }
|
|
3888
|
+
}
|
|
3889
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NewsSectionComponent, decorators: [{
|
|
3890
|
+
type: Component,
|
|
3891
|
+
args: [{ selector: 'app-news-section', standalone: true, imports: [CommonModule, LucideIconsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ai-section ai-section--news section-block section-block--news\">\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 <span class=\"ai-section__badge\" *ngIf=\"section.meta?.['category']\">\n {{ section.meta?.['category'] }}\n </span>\n </div>\n\n <div class=\"ai-section__body\">\n <div *ngIf=\"newsItems?.length; else newsEmpty\">\n <div class=\"news-feed\">\n <button\n *ngFor=\"let item of newsItems; trackBy: trackItem; let idx = index\"\n type=\"button\"\n class=\"news-item\"\n (click)=\"emitItemInteraction(item, { streamIndex: idx })\"\n [class.item-streaming]=\"getItemAnimationClass(getItemId(item, idx), idx) === 'item-streaming'\"\n [class.item-entered]=\"getItemAnimationClass(getItemId(item, idx), idx) === 'item-entered'\"\n >\n <div class=\"news-item__header\">\n <div class=\"news-item__tag\">\n <lucide-icon name=\"newspaper\" size=\"16\" aria-hidden=\"true\"></lucide-icon>\n <span>{{ formatSource(item) }}</span>\n </div>\n <span class=\"news-item__time\" *ngIf=\"formatTimestamp(item)\">{{ formatTimestamp(item) }}</span>\n </div>\n <p class=\"news-item__title\">{{ item.title }}</p>\n <p class=\"news-item__summary\" *ngIf=\"getDisplayDescription(item)\">{{ getDisplayDescription(item) }}</p>\n <div class=\"news-item__meta\" *ngIf=\"item.value\">\n <span class=\"news-item__value\">{{ item.value }}</span>\n <span class=\"news-item__indicator\">Live</span>\n </div>\n </button>\n </div>\n </div>\n\n <ng-template #newsEmpty>\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 headlines available</p>\n </div>\n </ng-template>\n </div>\n</div>\n" }]
|
|
3892
|
+
}] });
|
|
3893
|
+
|
|
3894
|
+
class SocialMediaSectionComponent extends BaseSectionComponent {
|
|
3895
|
+
get posts() {
|
|
3896
|
+
return this.getItems();
|
|
3897
|
+
}
|
|
3898
|
+
formatPlatform(item) {
|
|
3899
|
+
const meta = item.meta ?? {};
|
|
3900
|
+
return typeof meta['platform'] === 'string'
|
|
3901
|
+
? meta['platform']
|
|
3902
|
+
: typeof meta['network'] === 'string'
|
|
3903
|
+
? meta['network']
|
|
3904
|
+
: 'Social';
|
|
3905
|
+
}
|
|
3906
|
+
formatMetric(item) {
|
|
3907
|
+
const meta = item.meta ?? {};
|
|
3908
|
+
const likes = meta['likes'];
|
|
3909
|
+
const comments = meta['comments'];
|
|
3910
|
+
if (typeof likes === 'number' && typeof comments === 'number') {
|
|
3911
|
+
return `${likes} likes · ${comments} comments`;
|
|
3912
|
+
}
|
|
3913
|
+
if (typeof likes === 'number') {
|
|
3914
|
+
return `${likes} likes`;
|
|
3915
|
+
}
|
|
3916
|
+
if (typeof comments === 'number') {
|
|
3917
|
+
return `${comments} comments`;
|
|
3918
|
+
}
|
|
3919
|
+
return '';
|
|
3920
|
+
}
|
|
3921
|
+
trackPost(index, post) {
|
|
3922
|
+
return post.id ?? `social-post-${index}`;
|
|
3923
|
+
}
|
|
3924
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SocialMediaSectionComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
3925
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: SocialMediaSectionComponent, isStandalone: true, selector: "app-social-media-section", usesInheritance: true, ngImport: i0, template: "<div class=\"ai-section ai-section--social-media section-block section-block--social-media\">\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 <span class=\"ai-section__badge\" *ngIf=\"section.meta?.['platform']\">\n {{ section.meta?.['platform'] }}\n </span>\n </div>\n\n <div class=\"ai-section__body\">\n <div *ngIf=\"posts?.length; else socialEmpty\">\n <div class=\"social-feed\">\n <button\n *ngFor=\"let post of posts; trackBy: trackPost; let idx = index\"\n type=\"button\"\n class=\"social-post\"\n (click)=\"emitItemInteraction(post, { streamIndex: idx })\"\n [class.item-streaming]=\"getItemAnimationClass(getItemId(post, idx), idx) === 'item-streaming'\"\n [class.item-entered]=\"getItemAnimationClass(getItemId(post, idx), idx) === 'item-entered'\"\n >\n <div class=\"social-post__header\">\n <div class=\"social-post__platform\">\n <lucide-icon name=\"message-circle\" size=\"16\" aria-hidden=\"true\"></lucide-icon>\n <span>{{ formatPlatform(post) }}</span>\n </div>\n <span class=\"social-post__engagement\" *ngIf=\"formatMetric(post)\">\n {{ formatMetric(post) }}\n </span>\n </div>\n <p class=\"social-post__content\" *ngIf=\"post.description\">{{ post.description }}</p>\n <div class=\"social-post__meta\">\n <span *ngIf=\"post.value\" class=\"social-post__meta-value\">{{ post.value }}</span>\n <span class=\"social-post__meta-hint\">{{ post.meta?.['tag'] ?? post.meta?.['topic'] ?? 'Updates' }}</span>\n </div>\n </button>\n </div>\n </div>\n\n <ng-template #socialEmpty>\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 social posts available</p>\n </div>\n </ng-template>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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 }); }
|
|
3926
|
+
}
|
|
3927
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SocialMediaSectionComponent, decorators: [{
|
|
3928
|
+
type: Component,
|
|
3929
|
+
args: [{ selector: 'app-social-media-section', standalone: true, imports: [CommonModule, LucideIconsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ai-section ai-section--social-media section-block section-block--social-media\">\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 <span class=\"ai-section__badge\" *ngIf=\"section.meta?.['platform']\">\n {{ section.meta?.['platform'] }}\n </span>\n </div>\n\n <div class=\"ai-section__body\">\n <div *ngIf=\"posts?.length; else socialEmpty\">\n <div class=\"social-feed\">\n <button\n *ngFor=\"let post of posts; trackBy: trackPost; let idx = index\"\n type=\"button\"\n class=\"social-post\"\n (click)=\"emitItemInteraction(post, { streamIndex: idx })\"\n [class.item-streaming]=\"getItemAnimationClass(getItemId(post, idx), idx) === 'item-streaming'\"\n [class.item-entered]=\"getItemAnimationClass(getItemId(post, idx), idx) === 'item-entered'\"\n >\n <div class=\"social-post__header\">\n <div class=\"social-post__platform\">\n <lucide-icon name=\"message-circle\" size=\"16\" aria-hidden=\"true\"></lucide-icon>\n <span>{{ formatPlatform(post) }}</span>\n </div>\n <span class=\"social-post__engagement\" *ngIf=\"formatMetric(post)\">\n {{ formatMetric(post) }}\n </span>\n </div>\n <p class=\"social-post__content\" *ngIf=\"post.description\">{{ post.description }}</p>\n <div class=\"social-post__meta\">\n <span *ngIf=\"post.value\" class=\"social-post__meta-value\">{{ post.value }}</span>\n <span class=\"social-post__meta-hint\">{{ post.meta?.['tag'] ?? post.meta?.['topic'] ?? 'Updates' }}</span>\n </div>\n </button>\n </div>\n </div>\n\n <ng-template #socialEmpty>\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 social posts available</p>\n </div>\n </ng-template>\n </div>\n</div>\n" }]
|
|
3930
|
+
}] });
|
|
3931
|
+
|
|
3932
|
+
/**
|
|
3933
|
+
* Public API Surface of OSI Cards Library
|
|
3934
|
+
*
|
|
3935
|
+
* This file exports all public APIs that can be imported by other Angular projects.
|
|
3936
|
+
*
|
|
3937
|
+
* @example
|
|
3938
|
+
* ```typescript
|
|
3939
|
+
* import { AICardRendererComponent, CardDataService } from '@osi/cards-lib';
|
|
3940
|
+
* ```
|
|
3941
|
+
*/
|
|
3942
|
+
// Models
|
|
3943
|
+
/**
|
|
3944
|
+
* Note: For full functionality including:
|
|
3945
|
+
* - CardDataService and other core services
|
|
3946
|
+
* - NgRx store (actions, selectors, reducers, effects)
|
|
3947
|
+
* - Additional utilities and providers
|
|
3948
|
+
*
|
|
3949
|
+
* You may need to import from the main application source or
|
|
3950
|
+
* extend the library exports. See integration documentation for details.
|
|
3951
|
+
*
|
|
3952
|
+
* Styles entry point: '@osi/cards-lib/styles/_styles.scss'
|
|
3953
|
+
*/
|
|
3954
|
+
|
|
3955
|
+
/**
|
|
3956
|
+
* Generated bundle index. Do not edit.
|
|
3957
|
+
*/
|
|
3958
|
+
|
|
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 };
|
|
3960
|
+
//# sourceMappingURL=osi-cards-lib.mjs.map
|