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.
Files changed (114) hide show
  1. package/README.md +763 -0
  2. package/esm2022/lib/components/ai-card-renderer/ai-card-renderer.component.mjs +911 -0
  3. package/esm2022/lib/components/card-preview/card-preview.component.mjs +74 -0
  4. package/esm2022/lib/components/card-skeleton/card-skeleton.component.mjs +24 -0
  5. package/esm2022/lib/components/masonry-grid/masonry-grid.component.mjs +330 -0
  6. package/esm2022/lib/components/section-renderer/section-renderer.component.mjs +166 -0
  7. package/esm2022/lib/components/sections/analytics-section/analytics-section.component.mjs +70 -0
  8. package/esm2022/lib/components/sections/base-section.component.mjs +335 -0
  9. package/esm2022/lib/components/sections/brand-colors-section/brand-colors-section.component.mjs +89 -0
  10. package/esm2022/lib/components/sections/chart-section/chart-section.component.mjs +92 -0
  11. package/esm2022/lib/components/sections/contact-card-section/contact-card-section.component.mjs +70 -0
  12. package/esm2022/lib/components/sections/event-section/event-section.component.mjs +32 -0
  13. package/esm2022/lib/components/sections/fallback-section/fallback-section.component.mjs +16 -0
  14. package/esm2022/lib/components/sections/financials-section/financials-section.component.mjs +53 -0
  15. package/esm2022/lib/components/sections/info-section.component.mjs +68 -0
  16. package/esm2022/lib/components/sections/list-section/list-section.component.mjs +36 -0
  17. package/esm2022/lib/components/sections/map-section/map-section.component.mjs +52 -0
  18. package/esm2022/lib/components/sections/network-card-section/network-card-section.component.mjs +41 -0
  19. package/esm2022/lib/components/sections/news-section/news-section.component.mjs +44 -0
  20. package/esm2022/lib/components/sections/overview-section/overview-section.component.mjs +47 -0
  21. package/esm2022/lib/components/sections/product-section/product-section.component.mjs +129 -0
  22. package/esm2022/lib/components/sections/quotation-section/quotation-section.component.mjs +39 -0
  23. package/esm2022/lib/components/sections/social-media-section/social-media-section.component.mjs +45 -0
  24. package/esm2022/lib/components/sections/solutions-section/solutions-section.component.mjs +29 -0
  25. package/esm2022/lib/components/sections/text-reference-section/text-reference-section.component.mjs +42 -0
  26. package/esm2022/lib/icons/index.mjs +2 -0
  27. package/esm2022/lib/icons/lucide-icons.module.mjs +91 -0
  28. package/esm2022/lib/models/card.model.mjs +111 -0
  29. package/esm2022/lib/models/index.mjs +2 -0
  30. package/esm2022/lib/services/icon.service.mjs +148 -0
  31. package/esm2022/lib/services/index.mjs +5 -0
  32. package/esm2022/lib/services/magnetic-tilt.service.mjs +224 -0
  33. package/esm2022/lib/services/section-normalization.service.mjs +243 -0
  34. package/esm2022/lib/services/section-utils.service.mjs +122 -0
  35. package/esm2022/lib/utils/card-diff.util.mjs +327 -0
  36. package/esm2022/lib/utils/index.mjs +3 -0
  37. package/esm2022/lib/utils/responsive.util.mjs +14 -0
  38. package/esm2022/osi-cards-lib.mjs +5 -0
  39. package/esm2022/public-api.mjs +57 -0
  40. package/fesm2022/osi-cards-lib.mjs +3960 -0
  41. package/index.d.ts +5 -0
  42. package/lib/components/ai-card-renderer/ai-card-renderer.component.d.ts +163 -0
  43. package/lib/components/card-preview/card-preview.component.d.ts +52 -0
  44. package/lib/components/card-skeleton/card-skeleton.component.d.ts +8 -0
  45. package/lib/components/masonry-grid/masonry-grid.component.d.ts +72 -0
  46. package/lib/components/section-renderer/section-renderer.component.d.ts +25 -0
  47. package/lib/components/sections/analytics-section/analytics-section.component.d.ts +32 -0
  48. package/lib/components/sections/base-section.component.d.ts +138 -0
  49. package/lib/components/sections/brand-colors-section/brand-colors-section.component.d.ts +28 -0
  50. package/lib/components/sections/chart-section/chart-section.component.d.ts +30 -0
  51. package/lib/components/sections/contact-card-section/contact-card-section.component.d.ts +35 -0
  52. package/lib/components/sections/event-section/event-section.component.d.ts +17 -0
  53. package/lib/components/sections/fallback-section/fallback-section.component.d.ts +7 -0
  54. package/lib/components/sections/financials-section/financials-section.component.d.ts +27 -0
  55. package/lib/components/sections/info-section.component.d.ts +33 -0
  56. package/lib/components/sections/list-section/list-section.component.d.ts +21 -0
  57. package/lib/components/sections/map-section/map-section.component.d.ts +22 -0
  58. package/lib/components/sections/network-card-section/network-card-section.component.d.ts +18 -0
  59. package/lib/components/sections/news-section/news-section.component.d.ts +16 -0
  60. package/lib/components/sections/overview-section/overview-section.component.d.ts +19 -0
  61. package/lib/components/sections/product-section/product-section.component.d.ts +57 -0
  62. package/lib/components/sections/quotation-section/quotation-section.component.d.ts +23 -0
  63. package/lib/components/sections/social-media-section/social-media-section.component.d.ts +11 -0
  64. package/lib/components/sections/solutions-section/solutions-section.component.d.ts +19 -0
  65. package/lib/components/sections/text-reference-section/text-reference-section.component.d.ts +25 -0
  66. package/lib/icons/index.d.ts +1 -0
  67. package/lib/icons/lucide-icons.module.d.ts +7 -0
  68. package/lib/models/card.model.d.ts +289 -0
  69. package/lib/models/index.d.ts +1 -0
  70. package/lib/services/icon.service.d.ts +9 -0
  71. package/lib/services/index.d.ts +4 -0
  72. package/lib/services/magnetic-tilt.service.d.ts +34 -0
  73. package/lib/services/section-normalization.service.d.ts +38 -0
  74. package/lib/services/section-utils.service.d.ts +46 -0
  75. package/lib/utils/card-diff.util.d.ts +52 -0
  76. package/lib/utils/index.d.ts +2 -0
  77. package/lib/utils/responsive.util.d.ts +2 -0
  78. package/package.json +63 -0
  79. package/public-api.d.ts +50 -0
  80. package/styles/_styles.scss +95 -0
  81. package/styles/components/cards/_ai-card.scss +743 -0
  82. package/styles/components/sections/_analytics.scss +280 -0
  83. package/styles/components/sections/_brand-colors.scss +280 -0
  84. package/styles/components/sections/_chart.scss +494 -0
  85. package/styles/components/sections/_contact.scss +250 -0
  86. package/styles/components/sections/_design-system.scss +540 -0
  87. package/styles/components/sections/_event.scss +246 -0
  88. package/styles/components/sections/_fallback.scss +172 -0
  89. package/styles/components/sections/_financials.scss +258 -0
  90. package/styles/components/sections/_global-enforcement.scss +648 -0
  91. package/styles/components/sections/_info.scss +224 -0
  92. package/styles/components/sections/_list.scss +216 -0
  93. package/styles/components/sections/_map.scss +186 -0
  94. package/styles/components/sections/_network.scss +115 -0
  95. package/styles/components/sections/_news.scss +81 -0
  96. package/styles/components/sections/_overview.scss +159 -0
  97. package/styles/components/sections/_product.scss +906 -0
  98. package/styles/components/sections/_quotation.scss +151 -0
  99. package/styles/components/sections/_section-shell.scss +385 -0
  100. package/styles/components/sections/_section-types.scss +290 -0
  101. package/styles/components/sections/_sections-base.scss +332 -0
  102. package/styles/components/sections/_social-media.scss +88 -0
  103. package/styles/components/sections/_solutions.scss +205 -0
  104. package/styles/components/sections/_text-reference.scss +158 -0
  105. package/styles/components/sections/_unified-cards.scss +124 -0
  106. package/styles/core/_animations.scss +766 -0
  107. package/styles/core/_global.scss +66 -0
  108. package/styles/core/_mixins.scss +140 -0
  109. package/styles/core/_surface-layers.scss +76 -0
  110. package/styles/core/_utilities.scss +193 -0
  111. package/styles/core/_variables.scss +462 -0
  112. package/styles/core/variables/_colors.scss +212 -0
  113. package/styles/layout/_masonry.scss +60 -0
  114. 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) }} &middot; {{ 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) }} &middot; {{ 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