hypercrm 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2958 @@
1
+ (function(global) {
2
+ 'use strict';
3
+
4
+ const getFromStorage = (key, fallback) => {
5
+ try {
6
+ const value = localStorage.getItem(key);
7
+ return value == null ? fallback : value;
8
+ } catch (_) {
9
+ return fallback;
10
+ }
11
+ };
12
+
13
+ const WIDGET_API_URL = global.HYPERCRM_API_URL || getFromStorage('hypercrm_api_url', 'http://localhost:3000/api');
14
+ const WIDGET_API_KEY = global.HYPERCRM_API_KEY || getFromStorage('hypercrm_api_key', 'hypercrm_demo_key');
15
+
16
+ const DEFAULT_THEME = {
17
+ primary: '#137fec',
18
+ primaryContrast: '#ffffff',
19
+ canvasLight: '#f5f7fb',
20
+ canvasDark: '#101922',
21
+ surfaceLight: '#ffffff',
22
+ surfaceDark: '#1b2b3a',
23
+ textPrimaryLight: '#111827',
24
+ textSecondaryLight: '#6b7280',
25
+ textPrimaryDark: '#f8fafc',
26
+ textSecondaryDark: 'rgba(226, 232, 240, 0.7)',
27
+ borderLight: 'rgba(15, 23, 42, 0.08)',
28
+ borderDark: 'rgba(255, 255, 255, 0.12)',
29
+ shadowPanel: '0 24px 60px rgba(15, 23, 42, 0.2)',
30
+ shadowPanelDark: '0 24px 60px rgba(0, 0, 0, 0.45)',
31
+ statusOnline: '#10b981',
32
+ statusOnlineBg: 'rgba(16, 185, 129, 0.16)',
33
+ statusOffline: '#94a3b8',
34
+ statusOfflineBg: 'rgba(148, 163, 184, 0.22)'
35
+ };
36
+
37
+ const camelToKebab = (value) => value.replace(/[A-Z]/g, (match) => '-' + match.toLowerCase());
38
+
39
+ const hexToRgba = (hex, alpha) => {
40
+ if (!hex) return '';
41
+ let normalized = hex.replace('#', '').trim();
42
+ if (normalized.length === 3) {
43
+ normalized = normalized.split('').map((c) => c + c).join('');
44
+ }
45
+ if (normalized.length !== 6) return hex;
46
+ const bigint = parseInt(normalized, 16);
47
+ const r = (bigint >> 16) & 255;
48
+ const g = (bigint >> 8) & 255;
49
+ const b = bigint & 255;
50
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
51
+ };
52
+
53
+ const styles = `
54
+ .hypercrm-widget-container {
55
+ position: fixed;
56
+ bottom: 24px;
57
+ right: 24px;
58
+ z-index: 2147483000;
59
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
60
+ color: var(--hypercrm-text-primary, #111827);
61
+ --hypercrm-primary: #137fec;
62
+ --hypercrm-primary-contrast: #ffffff;
63
+ --hypercrm-primary-soft: rgba(19, 127, 236, 0.14);
64
+ --hypercrm-primary-soft-strong: rgba(19, 127, 236, 0.24);
65
+ --hypercrm-canvas: #f5f7fb;
66
+ --hypercrm-surface: #ffffff;
67
+ --hypercrm-surface-muted: #f9fbff;
68
+ --hypercrm-border: rgba(15, 23, 42, 0.08);
69
+ --hypercrm-border-subtle: rgba(15, 23, 42, 0.06);
70
+ --hypercrm-text-primary: #111827;
71
+ --hypercrm-text-secondary: #6b7280;
72
+ --hypercrm-text-tertiary: rgba(17, 24, 39, 0.5);
73
+ --hypercrm-status-online: #10b981;
74
+ --hypercrm-status-online-bg: rgba(16, 185, 129, 0.16);
75
+ --hypercrm-status-offline: #94a3b8;
76
+ --hypercrm-status-offline-bg: rgba(148, 163, 184, 0.24);
77
+ --hypercrm-shadow-panel: 0 24px 60px rgba(15, 23, 42, 0.2);
78
+ --hypercrm-radius-lg: 20px;
79
+ --hypercrm-radius-md: 16px;
80
+ --hypercrm-radius-sm: 12px;
81
+ color-scheme: light;
82
+ -webkit-font-smoothing: antialiased;
83
+ }
84
+
85
+ .hypercrm-widget-container.hypercrm-position-left {
86
+ right: auto;
87
+ left: 24px;
88
+ }
89
+
90
+ .hypercrm-widget-container.hypercrm-position-left .hypercrm-widget-panel {
91
+ right: auto;
92
+ left: 0;
93
+ }
94
+
95
+ .hypercrm-widget-container.hypercrm-dark {
96
+ color: var(--hypercrm-text-primary-dark, #f8fafc);
97
+ color-scheme: dark;
98
+ }
99
+
100
+ .hypercrm-widget-container *,
101
+ .hypercrm-widget-container *::before,
102
+ .hypercrm-widget-container *::after {
103
+ box-sizing: border-box;
104
+ }
105
+
106
+ .hypercrm-toggle-btn {
107
+ width: 50px;
108
+ height: 50px;
109
+ border-radius: 50%;
110
+ border: none;
111
+ display: flex;
112
+ align-items: center;
113
+ justify-content: center;
114
+ cursor: pointer;
115
+ background: var(--hypercrm-primary, #137fec);
116
+ color: var(--hypercrm-primary-contrast, #ffffff);
117
+ box-shadow: 0 18px 44px rgba(19, 127, 236, 0.35);
118
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
119
+ overflow: hidden;
120
+ }
121
+
122
+ #hypercrm-toggle-inner {
123
+ width: 100%;
124
+ height: 100%;
125
+ display: flex;
126
+ align-items: center;
127
+ justify-content: center;
128
+ }
129
+
130
+ #hypercrm-toggle-inner img.hypercrm-toggle-avatar {
131
+ width: 100%;
132
+ height: 100%;
133
+ object-fit: cover;
134
+ border-radius: 50%;
135
+ display: block;
136
+ }
137
+
138
+ .hypercrm-icon-img {
139
+ width: 18px;
140
+ height: 18px;
141
+ display: block;
142
+ }
143
+
144
+ .hypercrm-icon-img.small {
145
+ width: 14px;
146
+ height: 14px;
147
+ }
148
+
149
+ .hypercrm-icon-img.tiny {
150
+ width: 12px;
151
+ height: 12px;
152
+ }
153
+
154
+ .hypercrm-icon-img.large {
155
+ width: 24px;
156
+ height: 24px;
157
+ }
158
+
159
+ .hypercrm-icon-img.xl {
160
+ width: 28px;
161
+ height: 28px;
162
+ }
163
+
164
+ .hypercrm-toggle-btn:hover {
165
+ transform: translateY(-4px);
166
+ box-shadow: 0 22px 60px rgba(19, 127, 236, 0.42);
167
+ }
168
+
169
+ .hypercrm-widget-panel {
170
+ position: absolute;
171
+ bottom: 84px;
172
+ right: 0;
173
+ width: min(504px, calc(100vw - 32px));
174
+ max-height: min(80vh, 792px);
175
+ height: min(80vh, 792px);
176
+ background: var(--hypercrm-surface, #ffffff);
177
+ border-radius: 24px;
178
+ border: 1px solid var(--hypercrm-border, rgba(15, 23, 42, 0.08));
179
+ box-shadow: var(--hypercrm-shadow-panel, 0 24px 60px rgba(15, 23, 42, 0.2));
180
+ overflow: hidden;
181
+ display: none;
182
+ flex-direction: column;
183
+ }
184
+
185
+ .hypercrm-widget-panel.open {
186
+ display: flex;
187
+ }
188
+
189
+ .hypercrm-widget-container.hypercrm-dark .hypercrm-widget-panel {
190
+ background: var(--hypercrm-surface-dark, #1b2b3a);
191
+ border-color: var(--hypercrm-border-dark, rgba(255, 255, 255, 0.12));
192
+ box-shadow: var(--hypercrm-shadow-panel-dark, 0 24px 60px rgba(0, 0, 0, 0.45));
193
+ }
194
+
195
+ .hypercrm-panel-inner {
196
+ display: flex;
197
+ flex-direction: column;
198
+ height: 100%;
199
+ min-height: 0;
200
+ }
201
+
202
+ .hypercrm-header {
203
+ display: flex;
204
+ align-items: center;
205
+ justify-content: space-between;
206
+ padding: 8px 14px;
207
+ border-bottom: 1px solid var(--hypercrm-border-subtle, rgba(15, 23, 42, 0.06));
208
+ gap: 10px;
209
+ min-height: 48px;
210
+ flex-shrink: 0;
211
+ }
212
+
213
+ .hypercrm-widget-container.hypercrm-dark .hypercrm-header {
214
+ border-bottom-color: var(--hypercrm-border-dark, rgba(255, 255, 255, 0.12));
215
+ }
216
+
217
+ .hypercrm-header-main {
218
+ display: flex;
219
+ align-items: center;
220
+ gap: 8px;
221
+ flex: 1;
222
+ min-width: 0;
223
+ }
224
+
225
+ .hypercrm-header-wallet {
226
+ display: flex;
227
+ align-items: center;
228
+ gap: 8px;
229
+ flex-shrink: 0;
230
+ }
231
+
232
+ .hypercrm-wallet-chip,
233
+ .hypercrm-wallet-connect {
234
+ display: inline-flex;
235
+ align-items: center;
236
+ justify-content: center;
237
+ gap: 6px;
238
+ border-radius: 999px;
239
+ font-size: 12px;
240
+ font-weight: 600;
241
+ padding: 4px 11px;
242
+ border: 1px solid transparent;
243
+ cursor: pointer;
244
+ transition: background 0.2s ease, color 0.2s ease, border 0.2s ease, transform 0.2s ease;
245
+ }
246
+
247
+ .hypercrm-wallet-chip {
248
+ background: var(--hypercrm-primary-soft, rgba(19, 127, 236, 0.14));
249
+ color: var(--hypercrm-primary, #137fec);
250
+ }
251
+
252
+ .hypercrm-wallet-chip:hover {
253
+ transform: translateY(-1px);
254
+ background: var(--hypercrm-primary-soft-strong, rgba(19, 127, 236, 0.22));
255
+ }
256
+
257
+ .hypercrm-wallet-connect {
258
+ background: transparent;
259
+ border-color: var(--hypercrm-border, rgba(15, 23, 42, 0.08));
260
+ color: var(--hypercrm-text-primary, #111827);
261
+ }
262
+
263
+ .hypercrm-wallet-connect:hover {
264
+ border-color: var(--hypercrm-primary, #137fec);
265
+ color: var(--hypercrm-primary, #137fec);
266
+ }
267
+
268
+ .hypercrm-widget-container.hypercrm-dark .hypercrm-wallet-chip {
269
+ background: rgba(19, 127, 236, 0.28);
270
+ color: var(--hypercrm-primary-contrast, #ffffff);
271
+ }
272
+
273
+ .hypercrm-widget-container.hypercrm-dark .hypercrm-wallet-connect {
274
+ border-color: rgba(255, 255, 255, 0.16);
275
+ color: var(--hypercrm-text-primary-dark, #f8fafc);
276
+ }
277
+
278
+ .hypercrm-header-actions {
279
+ display: flex;
280
+ align-items: center;
281
+ gap: 6px;
282
+ }
283
+
284
+ .hypercrm-icon-btn {
285
+ width: 32px;
286
+ height: 32px;
287
+ border-radius: 12px;
288
+ border: 1px solid transparent;
289
+ background: transparent;
290
+ color: var(--hypercrm-text-secondary, #6b7280);
291
+ display: inline-flex;
292
+ align-items: center;
293
+ justify-content: center;
294
+ cursor: pointer;
295
+ transition: background 0.2s ease, color 0.2s ease, border 0.2s ease;
296
+ }
297
+
298
+ .hypercrm-icon-btn:hover {
299
+ background: var(--hypercrm-primary-soft, rgba(19, 127, 236, 0.14));
300
+ border-color: var(--hypercrm-primary, #137fec);
301
+ color: var(--hypercrm-primary, #137fec);
302
+ }
303
+
304
+ .hypercrm-icon-btn.disabled {
305
+ opacity: 0.4;
306
+ pointer-events: none;
307
+ }
308
+
309
+ .hypercrm-close-btn {
310
+ width: 32px;
311
+ height: 32px;
312
+ border-radius: 12px;
313
+ border: 1px solid transparent;
314
+ background: transparent;
315
+ color: inherit;
316
+ display: inline-flex;
317
+ align-items: center;
318
+ justify-content: center;
319
+ font-size: 20px;
320
+ cursor: pointer;
321
+ transition: background 0.2s ease, border 0.2s ease;
322
+ }
323
+
324
+ .hypercrm-close-btn:hover {
325
+ background: rgba(148, 163, 184, 0.18);
326
+ border-color: rgba(148, 163, 184, 0.38);
327
+ }
328
+
329
+ .hypercrm-tabs {
330
+ display: flex;
331
+ align-items: center;
332
+ justify-content: space-between;
333
+ gap: 16px;
334
+ padding: 0 14px;
335
+ border-bottom: 1px solid var(--hypercrm-border-subtle, rgba(15, 23, 42, 0.06));
336
+ flex-shrink: 0;
337
+ }
338
+
339
+ .hypercrm-widget-container.hypercrm-dark .hypercrm-tabs {
340
+ border-bottom-color: var(--hypercrm-border-dark, rgba(255, 255, 255, 0.12));
341
+ }
342
+
343
+ .hypercrm-tabs-left {
344
+ display: flex;
345
+ align-items: center;
346
+ gap: 16px;
347
+ }
348
+
349
+ .hypercrm-tab {
350
+ position: relative;
351
+ padding: 12px 0;
352
+ background: none;
353
+ border: none;
354
+ font-size: 14px;
355
+ font-weight: 600;
356
+ color: var(--hypercrm-text-secondary, #6b7280);
357
+ cursor: pointer;
358
+ transition: color 0.2s ease;
359
+ }
360
+
361
+ .hypercrm-tab::after {
362
+ content: '';
363
+ position: absolute;
364
+ left: 0;
365
+ bottom: -1px;
366
+ width: 100%;
367
+ height: 2px;
368
+ border-radius: 999px;
369
+ background: var(--hypercrm-primary, #137fec);
370
+ transform: scaleX(0);
371
+ transform-origin: center;
372
+ transition: transform 0.25s ease;
373
+ }
374
+
375
+ .hypercrm-tab:hover {
376
+ color: var(--hypercrm-text-primary, #111827);
377
+ }
378
+
379
+ .hypercrm-tab.active {
380
+ color: var(--hypercrm-primary, #137fec);
381
+ }
382
+
383
+ .hypercrm-tab.active::after {
384
+ transform: scaleX(1);
385
+ }
386
+
387
+ .hypercrm-main {
388
+ flex: 1;
389
+ display: flex;
390
+ flex-direction: column;
391
+ background: var(--hypercrm-canvas, #f5f7fb);
392
+ padding: 16px;
393
+ gap: 16px;
394
+ overflow: hidden;
395
+ min-height: 0;
396
+ }
397
+
398
+ .hypercrm-widget-container.hypercrm-dark .hypercrm-main {
399
+ background: var(--hypercrm-canvas-dark, #101922);
400
+ }
401
+
402
+ .hypercrm-toast {
403
+ display: none;
404
+ padding: 10px 14px;
405
+ border-radius: 12px;
406
+ font-size: 12px;
407
+ font-weight: 500;
408
+ background: rgba(17, 24, 39, 0.08);
409
+ color: var(--hypercrm-text-primary, #111827);
410
+ }
411
+
412
+ .hypercrm-toast.show {
413
+ display: block;
414
+ }
415
+
416
+ .hypercrm-toast.success {
417
+ background: rgba(16, 185, 129, 0.18);
418
+ color: #047857;
419
+ }
420
+
421
+ .hypercrm-toast.error {
422
+ background: rgba(239, 68, 68, 0.16);
423
+ color: #b91c1c;
424
+ }
425
+
426
+ .hypercrm-toast.info {
427
+ background: rgba(59, 130, 246, 0.16);
428
+ color: #1d4ed8;
429
+ }
430
+
431
+ .hypercrm-pane {
432
+ display: none;
433
+ flex-direction: column;
434
+ gap: 16px;
435
+ min-height: 0;
436
+ flex: 1;
437
+ }
438
+
439
+ .hypercrm-pane.active {
440
+ display: flex;
441
+ }
442
+
443
+ #hypercrm-ticket-pane,
444
+ #hypercrm-events-pane {
445
+ flex: 1;
446
+ min-height: 0;
447
+ flex-direction: column;
448
+ }
449
+
450
+ #hypercrm-ticket-pane {
451
+ overflow: hidden;
452
+ }
453
+
454
+ #hypercrm-events-pane {
455
+ overflow: hidden;
456
+ }
457
+
458
+ .hypercrm-tabs-close {
459
+ border: none;
460
+ background: none;
461
+ color: var(--hypercrm-text-secondary, #6b7280);
462
+ font-size: 12px;
463
+ font-weight: 600;
464
+ cursor: pointer;
465
+ padding: 0;
466
+ display: none;
467
+ }
468
+
469
+ .hypercrm-tabs-close:hover {
470
+ color: var(--hypercrm-primary, #137fec);
471
+ }
472
+
473
+ .hypercrm-ticket-surface {
474
+ flex: 1;
475
+ display: flex;
476
+ flex-direction: column;
477
+ min-height: 0;
478
+ }
479
+
480
+ .hypercrm-ticket-card {
481
+ flex: 1;
482
+ display: flex;
483
+ flex-direction: column;
484
+ border-radius: var(--hypercrm-radius-lg, 20px);
485
+ background: var(--hypercrm-surface, #ffffff);
486
+ border: 1px solid var(--hypercrm-border, rgba(15, 23, 42, 0.08));
487
+ box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08);
488
+ padding: 16px;
489
+ gap: 14px;
490
+ min-height: 0;
491
+ overflow: hidden;
492
+ }
493
+
494
+ .hypercrm-widget-container.hypercrm-dark .hypercrm-ticket-card {
495
+ background: rgba(27, 43, 58, 0.92);
496
+ border-color: var(--hypercrm-border-dark, rgba(255, 255, 255, 0.12));
497
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04);
498
+ }
499
+
500
+ .hypercrm-empty-state {
501
+ flex: 1;
502
+ display: flex;
503
+ flex-direction: column;
504
+ align-items: center;
505
+ justify-content: center;
506
+ text-align: center;
507
+ gap: 14px;
508
+ padding: 24px;
509
+ }
510
+
511
+ .hypercrm-empty-icon {
512
+ width: 64px;
513
+ height: 64px;
514
+ border-radius: 18px;
515
+ background: var(--hypercrm-primary-soft, rgba(19, 127, 236, 0.14));
516
+ color: var(--hypercrm-primary, #137fec);
517
+ display: flex;
518
+ align-items: center;
519
+ justify-content: center;
520
+ font-size: 28px;
521
+ }
522
+
523
+ .hypercrm-empty-icon img {
524
+ width: 28px;
525
+ height: 28px;
526
+ }
527
+
528
+ .hypercrm-empty-title {
529
+ margin: 0;
530
+ font-size: 18px;
531
+ font-weight: 700;
532
+ color: var(--hypercrm-text-primary, #111827);
533
+ }
534
+
535
+ .hypercrm-empty-desc {
536
+ margin: 0;
537
+ font-size: 14px;
538
+ color: var(--hypercrm-text-secondary, #6b7280);
539
+ max-width: 260px;
540
+ }
541
+
542
+ .hypercrm-empty-warning {
543
+ margin: 0;
544
+ font-size: 12px;
545
+ line-height: 1.4;
546
+ color: var(--hypercrm-warning, #b45309);
547
+ max-width: 260px;
548
+ }
549
+
550
+ .hypercrm-primary-btn,
551
+ .hypercrm-secondary-btn {
552
+ border-radius: 999px;
553
+ padding: 10px 20px;
554
+ font-size: 14px;
555
+ font-weight: 600;
556
+ border: none;
557
+ cursor: pointer;
558
+ transition: transform 0.2s ease, box-shadow 0.2s ease, color 0.2s ease, background 0.2s ease;
559
+ }
560
+
561
+ .hypercrm-primary-btn {
562
+ background: var(--hypercrm-primary, #137fec);
563
+ color: var(--hypercrm-primary-contrast, #ffffff);
564
+ box-shadow: 0 14px 30px rgba(19, 127, 236, 0.24);
565
+ }
566
+
567
+ .hypercrm-primary-btn:hover {
568
+ transform: translateY(-1px);
569
+ box-shadow: 0 18px 36px rgba(19, 127, 236, 0.28);
570
+ }
571
+
572
+ .hypercrm-secondary-btn {
573
+ background: transparent;
574
+ border: 1px solid var(--hypercrm-border, rgba(15, 23, 42, 0.08));
575
+ color: var(--hypercrm-text-primary, #111827);
576
+ }
577
+
578
+ .hypercrm-secondary-btn.hypercrm-with-icon {
579
+ display: inline-flex;
580
+ align-items: center;
581
+ gap: 8px;
582
+ }
583
+
584
+ .hypercrm-secondary-btn:hover {
585
+ border-color: var(--hypercrm-primary, #137fec);
586
+ color: var(--hypercrm-primary, #137fec);
587
+ background: var(--hypercrm-primary-soft, rgba(19, 127, 236, 0.14));
588
+ }
589
+
590
+ .hypercrm-widget-container.hypercrm-dark .hypercrm-secondary-btn {
591
+ border-color: rgba(255, 255, 255, 0.16);
592
+ color: var(--hypercrm-text-primary-dark, #f8fafc);
593
+ }
594
+
595
+ .hypercrm-widget-container.hypercrm-dark .hypercrm-secondary-btn:hover {
596
+ background: rgba(255, 255, 255, 0.08);
597
+ border-color: rgba(255, 255, 255, 0.26);
598
+ }
599
+
600
+ .hypercrm-resume-card {
601
+ display: flex;
602
+ flex-direction: column;
603
+ gap: 14px;
604
+ padding: 18px;
605
+ border-radius: 16px;
606
+ background: rgba(19, 127, 236, 0.08);
607
+ border: 1px solid rgba(19, 127, 236, 0.14);
608
+ }
609
+
610
+ .hypercrm-resume-meta {
611
+ display: flex;
612
+ align-items: center;
613
+ justify-content: space-between;
614
+ font-size: 12px;
615
+ color: var(--hypercrm-text-secondary, #6b7280);
616
+ }
617
+
618
+ .hypercrm-resume-title {
619
+ margin: 0;
620
+ font-size: 16px;
621
+ font-weight: 600;
622
+ color: var(--hypercrm-text-primary, #111827);
623
+ }
624
+
625
+ .hypercrm-resume-desc {
626
+ margin: 0;
627
+ font-size: 13px;
628
+ color: var(--hypercrm-text-secondary, #6b7280);
629
+ }
630
+
631
+ .hypercrm-resume-actions {
632
+ display: flex;
633
+ gap: 12px;
634
+ flex-wrap: wrap;
635
+ }
636
+
637
+ .hypercrm-conversation-header {
638
+ display: flex;
639
+ justify-content: space-between;
640
+ align-items: flex-start;
641
+ gap: 12px;
642
+ }
643
+
644
+ .hypercrm-ticket-heading {
645
+ display: flex;
646
+ flex-direction: column;
647
+ gap: 4px;
648
+ min-width: 0;
649
+ }
650
+
651
+ .hypercrm-ticket-number {
652
+ font-size: 12px;
653
+ font-weight: 600;
654
+ color: var(--hypercrm-primary, #137fec);
655
+ }
656
+
657
+ .hypercrm-ticket-title {
658
+ font-size: 16px;
659
+ font-weight: 700;
660
+ color: var(--hypercrm-text-primary, #111827);
661
+ margin: 0;
662
+ }
663
+
664
+ .hypercrm-ticket-status {
665
+ display: inline-flex;
666
+ align-items: center;
667
+ gap: 6px;
668
+ padding: 4px 12px;
669
+ border-radius: 999px;
670
+ font-size: 11px;
671
+ font-weight: 600;
672
+ text-transform: uppercase;
673
+ letter-spacing: 0.06em;
674
+ background: rgba(19, 127, 236, 0.14);
675
+ color: var(--hypercrm-primary, #137fec);
676
+ }
677
+
678
+ .hypercrm-ticket-status.pending {
679
+ background: rgba(234, 179, 8, 0.18);
680
+ color: #b45309;
681
+ }
682
+
683
+ .hypercrm-ticket-status.resolved,
684
+ .hypercrm-ticket-status.closed {
685
+ background: rgba(148, 163, 184, 0.2);
686
+ color: #475569;
687
+ }
688
+
689
+ .hypercrm-message-list {
690
+ flex: 1;
691
+ overflow-y: auto;
692
+ display: flex;
693
+ flex-direction: column;
694
+ gap: 12px;
695
+ padding-right: 6px;
696
+ min-height: 0;
697
+ }
698
+
699
+ .hypercrm-message-list::-webkit-scrollbar {
700
+ width: 6px;
701
+ }
702
+
703
+ .hypercrm-message-list::-webkit-scrollbar-thumb {
704
+ background: rgba(148, 163, 184, 0.35);
705
+ border-radius: 999px;
706
+ }
707
+
708
+ .hypercrm-message-group {
709
+ display: flex;
710
+ gap: 10px;
711
+ align-items: flex-start;
712
+ }
713
+
714
+ .hypercrm-message-group.me {
715
+ flex-direction: row-reverse;
716
+ }
717
+
718
+ .hypercrm-avatar {
719
+ width: 32px;
720
+ height: 32px;
721
+ border-radius: 50%;
722
+ background: rgba(148, 163, 184, 0.2);
723
+ color: var(--hypercrm-text-primary, #111827);
724
+ display: inline-flex;
725
+ align-items: center;
726
+ justify-content: center;
727
+ font-size: 13px;
728
+ font-weight: 600;
729
+ flex-shrink: 0;
730
+ }
731
+
732
+ .hypercrm-widget-container.hypercrm-dark .hypercrm-avatar {
733
+ background: rgba(255, 255, 255, 0.14);
734
+ color: var(--hypercrm-text-primary-dark, #f8fafc);
735
+ }
736
+
737
+ .hypercrm-avatar.me {
738
+ background: var(--hypercrm-primary, #137fec);
739
+ color: var(--hypercrm-primary-contrast, #ffffff);
740
+ }
741
+
742
+ .hypercrm-avatar img {
743
+ width: 100%;
744
+ height: 100%;
745
+ object-fit: cover;
746
+ border-radius: 50%;
747
+ display: block;
748
+ }
749
+
750
+ .hypercrm-bubble {
751
+ max-width: 78%;
752
+ border-radius: 18px;
753
+ padding: 12px 16px;
754
+ font-size: 14px;
755
+ line-height: 1.5;
756
+ background: rgba(15, 23, 42, 0.08);
757
+ color: var(--hypercrm-text-primary, #111827);
758
+ position: relative;
759
+ }
760
+
761
+ .hypercrm-bubble.agent {
762
+ border-top-left-radius: 6px;
763
+ }
764
+
765
+ .hypercrm-bubble.me {
766
+ background: var(--hypercrm-primary, #137fec);
767
+ color: var(--hypercrm-primary-contrast, #ffffff);
768
+ border-top-right-radius: 6px;
769
+ }
770
+
771
+ .hypercrm-widget-container.hypercrm-dark .hypercrm-bubble.agent {
772
+ background: rgba(255, 255, 255, 0.08);
773
+ color: var(--hypercrm-text-primary-dark, #f8fafc);
774
+ }
775
+
776
+ .hypercrm-bubble.system {
777
+ background: rgba(148, 163, 184, 0.24);
778
+ color: var(--hypercrm-text-primary, #111827);
779
+ text-align: center;
780
+ width: auto;
781
+ margin: 0 auto;
782
+ }
783
+
784
+ .hypercrm-bubble-content {
785
+ white-space: pre-wrap;
786
+ word-break: break-word;
787
+ }
788
+
789
+ .hypercrm-bubble-meta {
790
+ margin-top: 6px;
791
+ font-size: 11px;
792
+ color: var(--hypercrm-text-secondary, #6b7280);
793
+ }
794
+
795
+ .hypercrm-message-group.me .hypercrm-bubble-meta {
796
+ color: rgba(255, 255, 255, 0.8);
797
+ text-align: right;
798
+ }
799
+
800
+ .hypercrm-bubble-status {
801
+ margin-top: 6px;
802
+ font-size: 11px;
803
+ color: var(--hypercrm-text-secondary, #6b7280);
804
+ display: flex;
805
+ align-items: center;
806
+ gap: 6px;
807
+ }
808
+
809
+ .hypercrm-message-group.me .hypercrm-bubble-status {
810
+ color: rgba(255, 255, 255, 0.8);
811
+ }
812
+
813
+ .hypercrm-bubble-spinner {
814
+ width: 12px;
815
+ height: 12px;
816
+ border: 2px solid currentColor;
817
+ border-right-color: transparent;
818
+ border-radius: 50%;
819
+ animation: hypercrm-spin 0.8s linear infinite;
820
+ }
821
+
822
+ @keyframes hypercrm-spin {
823
+ from { transform: rotate(0deg); }
824
+ to { transform: rotate(360deg); }
825
+ }
826
+
827
+ .hypercrm-bubble-error {
828
+ color: #ef4444;
829
+ }
830
+
831
+ .hypercrm-bubble-error-actions {
832
+ display: flex;
833
+ align-items: center;
834
+ gap: 6px;
835
+ margin-left: auto;
836
+ }
837
+
838
+ .hypercrm-bubble-error-btn {
839
+ border: none;
840
+ background: none;
841
+ color: #ef4444;
842
+ cursor: pointer;
843
+ font-size: 14px;
844
+ padding: 0;
845
+ display: inline-flex;
846
+ align-items: center;
847
+ justify-content: center;
848
+ }
849
+
850
+ .hypercrm-bubble-error-btn img {
851
+ width: 14px;
852
+ height: 14px;
853
+ display: block;
854
+ }
855
+
856
+ .hypercrm-bubble-error-btn:hover {
857
+ color: #dc2626;
858
+ }
859
+
860
+ .hypercrm-conversation-shell {
861
+ flex: 1;
862
+ display: flex;
863
+ flex-direction: column;
864
+ gap: 12px;
865
+ min-height: 0;
866
+ }
867
+
868
+ .hypercrm-conversation-top {
869
+ display: flex;
870
+ align-items: center;
871
+ justify-content: space-between;
872
+ gap: 12px;
873
+ font-size: 12px;
874
+ color: var(--hypercrm-text-secondary, #6b7280);
875
+ }
876
+
877
+ .hypercrm-ticket-title-block {
878
+ display: flex;
879
+ flex-direction: column;
880
+ gap: 4px;
881
+ min-width: 0;
882
+ }
883
+
884
+ .hypercrm-ticket-title-text {
885
+ font-size: 15px;
886
+ font-weight: 600;
887
+ color: var(--hypercrm-text-primary, #111827);
888
+ margin: 0;
889
+ white-space: nowrap;
890
+ overflow: hidden;
891
+ text-overflow: ellipsis;
892
+ }
893
+
894
+ .hypercrm-widget-container.hypercrm-dark .hypercrm-ticket-title-text {
895
+ color: var(--hypercrm-text-primary-dark, #f8fafc);
896
+ }
897
+
898
+ .hypercrm-ticket-meta-line {
899
+ display: flex;
900
+ align-items: center;
901
+ gap: 10px;
902
+ flex-wrap: wrap;
903
+ font-size: 12px;
904
+ color: var(--hypercrm-text-secondary, #6b7280);
905
+ }
906
+
907
+ .hypercrm-widget-container.hypercrm-dark .hypercrm-ticket-meta-line {
908
+ color: var(--hypercrm-text-secondary-dark, rgba(226, 232, 240, 0.7));
909
+ }
910
+
911
+ .hypercrm-inline-btn {
912
+ border: none;
913
+ background: none;
914
+ color: var(--hypercrm-primary, #137fec);
915
+ font-size: 12px;
916
+ font-weight: 600;
917
+ cursor: pointer;
918
+ padding: 0;
919
+ }
920
+
921
+ .hypercrm-inline-btn:hover {
922
+ text-decoration: underline;
923
+ }
924
+
925
+ .hypercrm-closed-banner {
926
+ margin-top: 4px;
927
+ padding: 14px;
928
+ border-radius: 16px;
929
+ background: rgba(148, 163, 184, 0.12);
930
+ display: flex;
931
+ flex-direction: column;
932
+ gap: 8px;
933
+ text-align: left;
934
+ }
935
+
936
+ .hypercrm-widget-container.hypercrm-dark .hypercrm-closed-banner {
937
+ background: rgba(148, 163, 184, 0.18);
938
+ }
939
+
940
+ .hypercrm-closed-banner h4 {
941
+ margin: 0;
942
+ font-size: 14px;
943
+ font-weight: 600;
944
+ color: var(--hypercrm-text-primary, #111827);
945
+ }
946
+
947
+ .hypercrm-widget-container.hypercrm-dark .hypercrm-closed-banner h4 {
948
+ color: var(--hypercrm-text-primary-dark, #f8fafc);
949
+ }
950
+
951
+ .hypercrm-closed-banner p {
952
+ margin: 0;
953
+ font-size: 13px;
954
+ color: var(--hypercrm-text-secondary, #6b7280);
955
+ }
956
+
957
+ .hypercrm-widget-container.hypercrm-dark .hypercrm-closed-banner p {
958
+ color: var(--hypercrm-text-secondary-dark, rgba(226, 232, 240, 0.7));
959
+ }
960
+
961
+ .hypercrm-loading-state {
962
+ flex: 1;
963
+ display: flex;
964
+ align-items: center;
965
+ justify-content: center;
966
+ text-align: center;
967
+ font-size: 13px;
968
+ color: var(--hypercrm-text-secondary, #6b7280);
969
+ }
970
+
971
+ .hypercrm-attachment-chip {
972
+ margin-top: 10px;
973
+ display: inline-flex;
974
+ align-items: center;
975
+ gap: 8px;
976
+ padding: 8px 12px;
977
+ border-radius: 12px;
978
+ background: rgba(19, 127, 236, 0.12);
979
+ color: var(--hypercrm-primary, #137fec);
980
+ font-size: 12px;
981
+ }
982
+
983
+ .hypercrm-attachment-chip img[data-hypercrm-img],
984
+ .hypercrm-attachment-thumb {
985
+ max-width: 160px;
986
+ border-radius: 12px;
987
+ cursor: pointer;
988
+ border: 1px solid rgba(148, 163, 184, 0.28);
989
+ display: block;
990
+ }
991
+
992
+ .hypercrm-attachment-chip .hypercrm-icon-img {
993
+ width: 14px;
994
+ height: 14px;
995
+ }
996
+
997
+ .hypercrm-composer {
998
+ padding: 12px 16px 16px;
999
+ border-top: 1px solid var(--hypercrm-border-subtle, rgba(15, 23, 42, 0.06));
1000
+ background: var(--hypercrm-surface, #ffffff);
1001
+ display: flex;
1002
+ flex-direction: column;
1003
+ gap: 8px;
1004
+ flex-shrink: 0;
1005
+ }
1006
+
1007
+ .hypercrm-widget-container.hypercrm-dark .hypercrm-composer {
1008
+ background: rgba(18, 30, 42, 0.92);
1009
+ border-top-color: var(--hypercrm-border-dark, rgba(255, 255, 255, 0.12));
1010
+ }
1011
+
1012
+ .hypercrm-composer.disabled .hypercrm-compose-inner {
1013
+ opacity: 0.6;
1014
+ }
1015
+
1016
+ .hypercrm-compose-inner {
1017
+ display: flex;
1018
+ align-items: center;
1019
+ gap: 12px;
1020
+ border: 1px solid var(--hypercrm-border, rgba(15, 23, 42, 0.08));
1021
+ border-radius: 16px;
1022
+ background: var(--hypercrm-surface-muted, #f9fbff);
1023
+ padding: 10px 12px;
1024
+ transition: border 0.2s ease, background 0.2s ease;
1025
+ }
1026
+
1027
+ .hypercrm-widget-container.hypercrm-dark .hypercrm-compose-inner {
1028
+ background: rgba(27, 43, 58, 0.8);
1029
+ border-color: rgba(255, 255, 255, 0.12);
1030
+ }
1031
+
1032
+ .hypercrm-compose-inner.focused {
1033
+ border-color: var(--hypercrm-primary, #137fec);
1034
+ box-shadow: 0 0 0 3px rgba(19, 127, 236, 0.12);
1035
+ }
1036
+
1037
+ .hypercrm-composer-textarea {
1038
+ flex: 1;
1039
+ min-height: 52px;
1040
+ max-height: 140px;
1041
+ resize: none;
1042
+ border: none;
1043
+ background: transparent;
1044
+ color: inherit;
1045
+ font-family: inherit;
1046
+ font-size: 14px;
1047
+ line-height: 1.5;
1048
+ }
1049
+
1050
+ .hypercrm-composer-textarea:focus {
1051
+ outline: none;
1052
+ }
1053
+
1054
+ .hypercrm-composer-toolbar {
1055
+ display: flex;
1056
+ align-items: center;
1057
+ gap: 6px;
1058
+ }
1059
+
1060
+ .hypercrm-circle-btn {
1061
+ width: 32px;
1062
+ height: 32px;
1063
+ border-radius: 12px;
1064
+ border: 1px solid transparent;
1065
+ background: transparent;
1066
+ color: var(--hypercrm-text-secondary, #6b7280);
1067
+ display: inline-flex;
1068
+ align-items: center;
1069
+ justify-content: center;
1070
+ cursor: pointer;
1071
+ transition: background 0.2s ease, color 0.2s ease, border 0.2s ease;
1072
+ }
1073
+
1074
+ .hypercrm-circle-btn:hover {
1075
+ background: var(--hypercrm-primary-soft, rgba(19, 127, 236, 0.14));
1076
+ border-color: var(--hypercrm-primary, #137fec);
1077
+ color: var(--hypercrm-primary, #137fec);
1078
+ }
1079
+
1080
+ .hypercrm-primary-pill {
1081
+ border-radius: 50%;
1082
+ width: 36px;
1083
+ height: 36px;
1084
+ border: none;
1085
+ cursor: pointer;
1086
+ background: var(--hypercrm-primary, #137fec);
1087
+ color: var(--hypercrm-primary-contrast, #ffffff);
1088
+ display: inline-flex;
1089
+ align-items: center;
1090
+ justify-content: center;
1091
+ transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease;
1092
+ }
1093
+
1094
+ .hypercrm-primary-pill:hover:not(:disabled) {
1095
+ transform: translateY(-1px);
1096
+ box-shadow: 0 16px 32px rgba(19, 127, 236, 0.24);
1097
+ }
1098
+
1099
+ .hypercrm-primary-pill:disabled {
1100
+ opacity: 0.6;
1101
+ cursor: not-allowed;
1102
+ box-shadow: none;
1103
+ }
1104
+
1105
+ .hypercrm-attachment-chips {
1106
+ display: flex;
1107
+ flex-wrap: wrap;
1108
+ gap: 8px;
1109
+ }
1110
+
1111
+ .hypercrm-chip {
1112
+ display: inline-flex;
1113
+ align-items: center;
1114
+ gap: 8px;
1115
+ padding: 6px 12px;
1116
+ border-radius: 12px;
1117
+ background: rgba(19, 127, 236, 0.08);
1118
+ color: var(--hypercrm-primary, #137fec);
1119
+ font-size: 12px;
1120
+ }
1121
+
1122
+ .hypercrm-chip img {
1123
+ width: 14px;
1124
+ height: 14px;
1125
+ display: block;
1126
+ }
1127
+
1128
+ .hypercrm-chip button {
1129
+ border: none;
1130
+ background: transparent;
1131
+ color: inherit;
1132
+ cursor: pointer;
1133
+ font-size: 16px;
1134
+ line-height: 1;
1135
+ opacity: 0.7;
1136
+ }
1137
+
1138
+ .hypercrm-chip button img {
1139
+ width: 12px;
1140
+ height: 12px;
1141
+ }
1142
+
1143
+ .hypercrm-chip button:hover {
1144
+ opacity: 1;
1145
+ }
1146
+
1147
+ .hypercrm-composer-note {
1148
+ font-size: 12px;
1149
+ color: var(--hypercrm-text-secondary, #6b7280);
1150
+ }
1151
+
1152
+ .hypercrm-powered {
1153
+ text-align: center;
1154
+ font-size: 11px;
1155
+ color: var(--hypercrm-text-tertiary, rgba(17, 24, 39, 0.5));
1156
+ text-transform: none;
1157
+ letter-spacing: 0;
1158
+ }
1159
+
1160
+ .hypercrm-events-grid {
1161
+ display: flex;
1162
+ flex-direction: column;
1163
+ gap: 14px;
1164
+ flex: 1;
1165
+ overflow-y: auto;
1166
+ min-height: 0;
1167
+ }
1168
+
1169
+ .hypercrm-event-card {
1170
+ border-radius: 18px;
1171
+ border: 1px solid var(--hypercrm-border, rgba(15, 23, 42, 0.08));
1172
+ overflow: hidden;
1173
+ background: var(--hypercrm-surface, #ffffff);
1174
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06);
1175
+ cursor: pointer;
1176
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
1177
+ }
1178
+
1179
+ .hypercrm-event-card:hover {
1180
+ transform: translateY(-2px);
1181
+ box-shadow: 0 18px 36px rgba(15, 23, 42, 0.16);
1182
+ }
1183
+
1184
+ .hypercrm-widget-container.hypercrm-dark .hypercrm-event-card {
1185
+ background: rgba(27, 43, 58, 0.92);
1186
+ border-color: var(--hypercrm-border-dark, rgba(255, 255, 255, 0.12));
1187
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04);
1188
+ }
1189
+
1190
+ .hypercrm-event-banner {
1191
+ position: relative;
1192
+ padding-top: 56%;
1193
+ overflow: hidden;
1194
+ }
1195
+
1196
+ .hypercrm-event-banner img {
1197
+ position: absolute;
1198
+ inset: 0;
1199
+ width: 100%;
1200
+ height: 100%;
1201
+ object-fit: cover;
1202
+ }
1203
+
1204
+ .hypercrm-event-badge {
1205
+ position: absolute;
1206
+ top: 14px;
1207
+ left: 14px;
1208
+ padding: 6px 12px;
1209
+ border-radius: 999px;
1210
+ font-size: 11px;
1211
+ font-weight: 600;
1212
+ color: #ffffff;
1213
+ text-transform: uppercase;
1214
+ letter-spacing: 0.04em;
1215
+ }
1216
+
1217
+ .hypercrm-event-badge.ongoing { background: #10b981; }
1218
+ .hypercrm-event-badge.upcoming { background: #2563eb; }
1219
+ .hypercrm-event-badge.ended { background: rgba(15, 23, 42, 0.65); }
1220
+
1221
+ .hypercrm-event-content {
1222
+ padding: 16px;
1223
+ display: flex;
1224
+ flex-direction: column;
1225
+ gap: 8px;
1226
+ }
1227
+
1228
+ .hypercrm-event-title {
1229
+ margin: 0;
1230
+ font-size: 16px;
1231
+ font-weight: 600;
1232
+ color: var(--hypercrm-text-primary, #111827);
1233
+ }
1234
+
1235
+ .hypercrm-event-meta {
1236
+ font-size: 12px;
1237
+ color: var(--hypercrm-text-secondary, #6b7280);
1238
+ }
1239
+
1240
+ .hypercrm-event-text {
1241
+ font-size: 13px;
1242
+ color: var(--hypercrm-text-primary, #111827);
1243
+ margin: 0;
1244
+ }
1245
+
1246
+ .hypercrm-event-actions {
1247
+ margin-top: 10px;
1248
+ display: inline-flex;
1249
+ }
1250
+
1251
+ .hypercrm-event-detail {
1252
+ display: flex;
1253
+ flex-direction: column;
1254
+ gap: 16px;
1255
+ flex: 1;
1256
+ overflow-y: auto;
1257
+ min-height: 0;
1258
+ }
1259
+
1260
+ .hypercrm-lightbox {
1261
+ position: fixed;
1262
+ inset: 0;
1263
+ background: rgba(15, 23, 42, 0.78);
1264
+ display: none;
1265
+ align-items: center;
1266
+ justify-content: center;
1267
+ z-index: 2147483600;
1268
+ backdrop-filter: blur(6px);
1269
+ }
1270
+
1271
+ .hypercrm-lightbox.open {
1272
+ display: flex;
1273
+ }
1274
+
1275
+ .hypercrm-lightbox img {
1276
+ max-width: 90vw;
1277
+ max-height: 90vh;
1278
+ border-radius: 18px;
1279
+ box-shadow: 0 24px 80px rgba(15, 23, 42, 0.38);
1280
+ }
1281
+
1282
+ @media (max-width: 640px) {
1283
+ .hypercrm-widget-container {
1284
+ bottom: 16px;
1285
+ right: 16px;
1286
+ }
1287
+
1288
+ .hypercrm-widget-container.hypercrm-position-left {
1289
+ left: 16px;
1290
+ right: auto;
1291
+ }
1292
+
1293
+ .hypercrm-toggle-btn {
1294
+ width: 56px;
1295
+ height: 56px;
1296
+ }
1297
+
1298
+ .hypercrm-widget-panel {
1299
+ width: min(456px, calc(100vw - 20px));
1300
+ bottom: 76px;
1301
+ }
1302
+
1303
+ .hypercrm-widget-container.hypercrm-position-left .hypercrm-widget-panel {
1304
+ left: 0;
1305
+ right: auto;
1306
+ }
1307
+
1308
+ .hypercrm-main {
1309
+ padding: 14px;
1310
+ }
1311
+
1312
+ .hypercrm-composer {
1313
+ padding: 14px;
1314
+ }
1315
+ }
1316
+ `;
1317
+
1318
+ class HyperCRMWidget {
1319
+ constructor(options) {
1320
+ this.options = options || {};
1321
+ this.theme = { ...DEFAULT_THEME, ...(this.options.theme || {}) };
1322
+ this.colorModePreference = this.options.colorMode || 'light';
1323
+ this.position = this.normalizePosition(this.options.position);
1324
+ this.mediaColorQuery = null;
1325
+ this.systemColorListener = null;
1326
+ this.systemColorListenerAttached = false;
1327
+ this.isOpen = false;
1328
+
1329
+ this.apiUrl = this.options.apiUrl || WIDGET_API_URL;
1330
+ this.apiKey = this.options.apiKey || WIDGET_API_KEY;
1331
+ this.apiOrigin = this.resolveApiOrigin(this.apiUrl);
1332
+
1333
+ this.iconUrls = {
1334
+ chat: this.absUrl('/widget/assets/icon-chat.svg'),
1335
+ close: this.absUrl('/widget/assets/icon-close.svg'),
1336
+ attach: this.absUrl('/widget/assets/icon-attach.svg'),
1337
+ send: this.absUrl('/widget/assets/icon-send.svg'),
1338
+ file: this.absUrl('/widget/assets/icon-file.svg'),
1339
+ retry: this.absUrl('/widget/assets/icon-retry.svg'),
1340
+ lock: this.absUrl('/widget/assets/icon-lock.svg'),
1341
+ announcement: this.absUrl('/widget/assets/icon-announcement.svg'),
1342
+ back: this.absUrl('/widget/assets/icon-back.svg'),
1343
+ telegram: this.absUrl('/widget/assets/telegram-logo.svg'),
1344
+ discord: this.absUrl('/widget/assets/discord-logo.png'),
1345
+ };
1346
+
1347
+ this.walletAddress = null;
1348
+ this.tickets = [];
1349
+ this.currentTicket = null;
1350
+ this.showTicketOverview = false;
1351
+ this.events = [];
1352
+ this.orgInfo = null;
1353
+ this.orgIconUrl = null;
1354
+
1355
+ this.socket = null;
1356
+ this.socketConnected = false;
1357
+ this.socketRetryTimer = null;
1358
+ this.refreshInterval = null;
1359
+
1360
+ this.state = {
1361
+ composerMode: 'disabled',
1362
+ composerLoading: false,
1363
+ };
1364
+
1365
+ this.composerFiles = [];
1366
+ this.container = null;
1367
+ this.elements = {};
1368
+ this.pendingTicketId = null;
1369
+ this.optimisticMessages = new Map();
1370
+
1371
+ this.handlePasteBound = (event) => this.handlePaste(event);
1372
+ this.handleSystemColorScheme = (event) => {
1373
+ if (this.colorModePreference === 'auto') {
1374
+ this.applyColorMode(event.matches ? 'dark' : 'light');
1375
+ }
1376
+ };
1377
+
1378
+ this.init();
1379
+ }
1380
+
1381
+ resolveApiOrigin(url) {
1382
+ try {
1383
+ const parsed = new URL(url);
1384
+ return `${parsed.protocol}//${parsed.host}`;
1385
+ } catch (_) {
1386
+ return global.location ? global.location.origin : '';
1387
+ }
1388
+ }
1389
+
1390
+ normalizePosition(value) {
1391
+ return value === 'left' ? 'left' : 'right';
1392
+ }
1393
+
1394
+ applyPosition() {
1395
+ if (!this.container) return;
1396
+ const position = this.normalizePosition(this.position || this.options.position);
1397
+ this.position = position;
1398
+ this.options.position = position;
1399
+ this.container.classList.toggle('hypercrm-position-left', position === 'left');
1400
+ }
1401
+
1402
+ setPosition(position) {
1403
+ this.position = this.normalizePosition(position);
1404
+ this.applyPosition();
1405
+ }
1406
+
1407
+ absUrl(url) {
1408
+ if (!url) return url;
1409
+ if (/^https?:/i.test(url)) return url;
1410
+ if (url.startsWith('/')) return `${this.apiOrigin}${url}`;
1411
+ return `${this.apiOrigin}/${url}`;
1412
+ }
1413
+
1414
+ async init() {
1415
+ this.injectStyles();
1416
+ this.createWidget();
1417
+ this.cacheElements();
1418
+ this.applyTheme();
1419
+ this.ensureSystemColorListener();
1420
+ this.applyColorMode();
1421
+ this.attachEventListeners();
1422
+ await this.loadOrgInfo();
1423
+ await this.checkWalletConnection();
1424
+ this.renderTicketState();
1425
+ if (this.options.autoOpen) {
1426
+ this.openWidget();
1427
+ }
1428
+ }
1429
+
1430
+ injectStyles() {
1431
+ const styleSheet = document.createElement('style');
1432
+ styleSheet.textContent = styles;
1433
+ document.head.appendChild(styleSheet);
1434
+ }
1435
+
1436
+ applyTheme(themeOverride) {
1437
+ if (themeOverride && typeof themeOverride === 'object') {
1438
+ this.options.theme = { ...(this.options.theme || {}), ...themeOverride };
1439
+ }
1440
+ this.theme = { ...DEFAULT_THEME, ...(this.options.theme || {}) };
1441
+ if (!this.container) return;
1442
+ const target = this.container;
1443
+ Object.entries(this.theme).forEach(([key, value]) => {
1444
+ const cssName = `--hypercrm-${camelToKebab(key)}`;
1445
+ target.style.setProperty(cssName, value);
1446
+ });
1447
+ const primary = this.theme.primary || DEFAULT_THEME.primary;
1448
+ target.style.setProperty('--hypercrm-primary', primary);
1449
+ target.style.setProperty('--hypercrm-primary-contrast', this.theme.primaryContrast || DEFAULT_THEME.primaryContrast);
1450
+ target.style.setProperty('--hypercrm-primary-soft', hexToRgba(primary, 0.14));
1451
+ target.style.setProperty('--hypercrm-primary-soft-strong', hexToRgba(primary, 0.24));
1452
+ target.style.setProperty('--hypercrm-canvas', this.theme.canvasLight || DEFAULT_THEME.canvasLight);
1453
+ target.style.setProperty('--hypercrm-canvas-dark', this.theme.canvasDark || DEFAULT_THEME.canvasDark);
1454
+ target.style.setProperty('--hypercrm-surface', this.theme.surfaceLight || DEFAULT_THEME.surfaceLight);
1455
+ target.style.setProperty('--hypercrm-surface-dark', this.theme.surfaceDark || DEFAULT_THEME.surfaceDark);
1456
+ target.style.setProperty('--hypercrm-text-primary', this.theme.textPrimaryLight || DEFAULT_THEME.textPrimaryLight);
1457
+ target.style.setProperty('--hypercrm-text-secondary', this.theme.textSecondaryLight || DEFAULT_THEME.textSecondaryLight);
1458
+ target.style.setProperty('--hypercrm-text-primary-dark', this.theme.textPrimaryDark || DEFAULT_THEME.textPrimaryDark);
1459
+ target.style.setProperty('--hypercrm-text-secondary-dark', this.theme.textSecondaryDark || DEFAULT_THEME.textSecondaryDark);
1460
+ target.style.setProperty('--hypercrm-border', this.theme.borderLight || DEFAULT_THEME.borderLight);
1461
+ target.style.setProperty('--hypercrm-border-dark', this.theme.borderDark || DEFAULT_THEME.borderDark);
1462
+ target.style.setProperty('--hypercrm-status-online', this.theme.statusOnline || DEFAULT_THEME.statusOnline);
1463
+ target.style.setProperty('--hypercrm-status-online-bg', this.theme.statusOnlineBg || DEFAULT_THEME.statusOnlineBg);
1464
+ target.style.setProperty('--hypercrm-status-offline', this.theme.statusOffline || DEFAULT_THEME.statusOffline);
1465
+ target.style.setProperty('--hypercrm-status-offline-bg', this.theme.statusOfflineBg || DEFAULT_THEME.statusOfflineBg);
1466
+ target.style.setProperty('--hypercrm-shadow-panel', this.theme.shadowPanel || DEFAULT_THEME.shadowPanel);
1467
+ target.style.setProperty('--hypercrm-shadow-panel-dark', this.theme.shadowPanelDark || DEFAULT_THEME.shadowPanelDark);
1468
+ }
1469
+
1470
+ ensureSystemColorListener() {
1471
+ if (typeof window === 'undefined') return;
1472
+ if (this.colorModePreference !== 'auto') {
1473
+ this.removeSystemColorListener();
1474
+ return;
1475
+ }
1476
+ if (!window.matchMedia) return;
1477
+ if (!this.mediaColorQuery) {
1478
+ this.mediaColorQuery = window.matchMedia('(prefers-color-scheme: dark)');
1479
+ }
1480
+ if (!this.systemColorListenerAttached && this.mediaColorQuery) {
1481
+ const mq = this.mediaColorQuery;
1482
+ if (mq.addEventListener) {
1483
+ mq.addEventListener('change', this.handleSystemColorScheme);
1484
+ } else if (mq.addListener) {
1485
+ mq.addListener(this.handleSystemColorScheme);
1486
+ }
1487
+ this.systemColorListenerAttached = true;
1488
+ }
1489
+ }
1490
+
1491
+ removeSystemColorListener() {
1492
+ if (!this.mediaColorQuery || !this.systemColorListenerAttached) return;
1493
+ const mq = this.mediaColorQuery;
1494
+ if (mq.removeEventListener) {
1495
+ mq.removeEventListener('change', this.handleSystemColorScheme);
1496
+ } else if (mq.removeListener) {
1497
+ mq.removeListener(this.handleSystemColorScheme);
1498
+ }
1499
+ this.systemColorListenerAttached = false;
1500
+ }
1501
+
1502
+ getSystemColorMode() {
1503
+ if (typeof window === 'undefined' || !window.matchMedia) return 'light';
1504
+ if (!this.mediaColorQuery) {
1505
+ this.mediaColorQuery = window.matchMedia('(prefers-color-scheme: dark)');
1506
+ }
1507
+ return this.mediaColorQuery && this.mediaColorQuery.matches ? 'dark' : 'light';
1508
+ }
1509
+
1510
+ applyColorMode(modeOverride) {
1511
+ if (modeOverride) {
1512
+ this.colorModePreference = modeOverride;
1513
+ }
1514
+ const mode = this.colorModePreference === 'auto' ? this.getSystemColorMode() : this.colorModePreference;
1515
+ if (!this.container) return mode;
1516
+ if (mode === 'dark') {
1517
+ this.container.classList.add('hypercrm-dark');
1518
+ this.container.style.setProperty('color-scheme', 'dark');
1519
+ } else {
1520
+ this.container.classList.remove('hypercrm-dark');
1521
+ this.container.style.setProperty('color-scheme', 'light');
1522
+ }
1523
+ return mode;
1524
+ }
1525
+
1526
+ setTheme(theme) {
1527
+ this.applyTheme(theme || {});
1528
+ this.applyColorMode();
1529
+ }
1530
+
1531
+ setColorMode(mode) {
1532
+ if (!mode) return;
1533
+ this.colorModePreference = mode;
1534
+ if (mode === 'auto') {
1535
+ this.ensureSystemColorListener();
1536
+ } else {
1537
+ this.removeSystemColorListener();
1538
+ }
1539
+ this.applyColorMode();
1540
+ }
1541
+
1542
+ createWidget() {
1543
+ const iconUrls = this.iconUrls || {};
1544
+ const discordIconUrl = iconUrls.discord || this.absUrl('/widget/assets/discord-logo.png');
1545
+ const telegramIconUrl = iconUrls.telegram || this.absUrl('/widget/assets/telegram-logo.svg');
1546
+ const chatIcon = this.renderIcon(iconUrls.chat, '', 'hypercrm-icon-img large');
1547
+ const closeIcon = this.renderIcon(iconUrls.close, '', 'hypercrm-icon-img');
1548
+ const attachIcon = this.renderIcon(iconUrls.attach, '', 'hypercrm-icon-img');
1549
+ const sendIcon = this.renderIcon(iconUrls.send, '', 'hypercrm-icon-img');
1550
+ const telegramIcon = this.renderIcon(telegramIconUrl, 'Telegram', 'hypercrm-icon-img');
1551
+ const discordIcon = this.renderIcon(discordIconUrl, 'Discord', 'hypercrm-icon-img');
1552
+ const container = document.createElement('div');
1553
+ container.className = 'hypercrm-widget-container';
1554
+ container.innerHTML = `
1555
+ <button class="hypercrm-toggle-btn" id="hypercrm-toggle" aria-label="Open support widget"><span id="hypercrm-toggle-inner">${chatIcon}</span></button>
1556
+ <div class="hypercrm-widget-panel" id="hypercrm-panel">
1557
+ <div class="hypercrm-panel-inner">
1558
+ <header class="hypercrm-header">
1559
+ <div class="hypercrm-header-main">
1560
+ <div id="hypercrm-wallet-slot" class="hypercrm-header-wallet"></div>
1561
+ </div>
1562
+ <div class="hypercrm-header-actions">
1563
+ <a id="hypercrm-tg-link" class="hypercrm-icon-btn disabled" title="Telegram" target="_blank" rel="noopener">
1564
+ ${telegramIcon}
1565
+ </a>
1566
+ <a id="hypercrm-dc-link" class="hypercrm-icon-btn disabled" title="Discord" target="_blank" rel="noopener">
1567
+ ${discordIcon}
1568
+ </a>
1569
+ <button class="hypercrm-close-btn" id="hypercrm-close" aria-label="Close widget">${closeIcon}</button>
1570
+ </div>
1571
+ </header>
1572
+ <nav class="hypercrm-tabs">
1573
+ <div class="hypercrm-tabs-left">
1574
+ <button class="hypercrm-tab active" data-tab="ticket">Ticket</button>
1575
+ <button class="hypercrm-tab" data-tab="events">Announcements</button>
1576
+ </div>
1577
+ <button type="button" class="hypercrm-tabs-close" id="hypercrm-close-inline">Close ticket</button>
1578
+ </nav>
1579
+ <div class="hypercrm-main">
1580
+ <div id="hypercrm-toast" class="hypercrm-toast" role="status"></div>
1581
+ <section id="hypercrm-ticket-pane" class="hypercrm-pane active">
1582
+ <div class="hypercrm-ticket-surface">
1583
+ <div id="hypercrm-ticket-content" class="hypercrm-ticket-card"></div>
1584
+ </div>
1585
+ </section>
1586
+ <section id="hypercrm-events-pane" class="hypercrm-pane">
1587
+ <div id="hypercrm-events-list" class="hypercrm-events-grid"></div>
1588
+ <div id="hypercrm-event-detail" class="hypercrm-event-detail" style="display:none;"></div>
1589
+ </section>
1590
+ </div>
1591
+ <div class="hypercrm-composer" id="hypercrm-composer">
1592
+ <div class="hypercrm-compose-inner" id="hypercrm-composer-dropzone">
1593
+ <textarea id="hypercrm-composer-input" class="hypercrm-composer-textarea" placeholder="Type your message..." rows="1"></textarea>
1594
+ <div class="hypercrm-composer-toolbar">
1595
+ <input type="file" id="hypercrm-composer-file-input" class="hypercrm-hidden" multiple accept="image/*,.pdf,.doc,.docx" style="display:none;" />
1596
+ <button type="button" id="hypercrm-attach-btn" class="hypercrm-circle-btn" aria-label="Attach files">${attachIcon}</button>
1597
+ <button type="button" id="hypercrm-composer-send" class="hypercrm-primary-pill" aria-label="Send message">${sendIcon}</button>
1598
+ </div>
1599
+ </div>
1600
+ <div id="hypercrm-attachment-chips" class="hypercrm-attachment-chips"></div>
1601
+ <div id="hypercrm-composer-note" class="hypercrm-composer-note"></div>
1602
+ <div class="hypercrm-powered">Powered by HyperCRM</div>
1603
+ </div>
1604
+ </div>
1605
+ </div>
1606
+ `;
1607
+
1608
+ document.body.appendChild(container);
1609
+ this.container = container;
1610
+ this.applyPosition();
1611
+
1612
+ // Lightbox overlay
1613
+ const lightbox = document.createElement('div');
1614
+ lightbox.id = 'hypercrm-lightbox';
1615
+ lightbox.className = 'hypercrm-lightbox';
1616
+ lightbox.innerHTML = '<img id="hypercrm-lightbox-img" alt="Preview" />';
1617
+ lightbox.addEventListener('click', () => this.closeLightbox());
1618
+ document.body.appendChild(lightbox);
1619
+ }
1620
+
1621
+ cacheElements() {
1622
+ const qs = (selector) => this.container ? this.container.querySelector(selector) : null;
1623
+ this.elements = {
1624
+ toggle: qs('#hypercrm-toggle'),
1625
+ toggleInner: qs('#hypercrm-toggle-inner'),
1626
+ panel: qs('#hypercrm-panel'),
1627
+ tabs: this.container ? Array.from(this.container.querySelectorAll('.hypercrm-tab')) : [],
1628
+ ticketPane: qs('#hypercrm-ticket-pane'),
1629
+ ticketContent: qs('#hypercrm-ticket-content'),
1630
+ eventsPane: qs('#hypercrm-events-pane'),
1631
+ eventsList: qs('#hypercrm-events-list'),
1632
+ eventDetail: qs('#hypercrm-event-detail'),
1633
+ toast: qs('#hypercrm-toast'),
1634
+ walletSlot: qs('#hypercrm-wallet-slot'),
1635
+ telegramBtn: qs('#hypercrm-tg-link'),
1636
+ discordBtn: qs('#hypercrm-dc-link'),
1637
+ closeBtn: qs('#hypercrm-close'),
1638
+ closeInline: qs('#hypercrm-close-inline'),
1639
+ composer: qs('#hypercrm-composer'),
1640
+ composerInner: qs('#hypercrm-composer-dropzone'),
1641
+ composerInput: qs('#hypercrm-composer-input'),
1642
+ composerSend: qs('#hypercrm-composer-send'),
1643
+ composerAttach: qs('#hypercrm-attach-btn'),
1644
+ composerFileInput: qs('#hypercrm-composer-file-input'),
1645
+ composerChips: qs('#hypercrm-attachment-chips'),
1646
+ composerNote: qs('#hypercrm-composer-note'),
1647
+ };
1648
+ }
1649
+
1650
+ attachEventListeners() {
1651
+ const { toggle, panel, closeBtn, closeInline, tabs, composerInput, composerInner, composerSend, composerAttach, composerFileInput } = this.elements;
1652
+
1653
+ if (toggle) toggle.addEventListener('click', () => this.toggleWidget());
1654
+ if (closeBtn) closeBtn.addEventListener('click', () => this.closeWidget());
1655
+ if (panel) panel.addEventListener('keydown', (event) => {
1656
+ if (event.key === 'Escape') this.closeWidget();
1657
+ });
1658
+
1659
+ tabs.forEach((tab) => {
1660
+ tab.addEventListener('click', () => {
1661
+ const target = tab.getAttribute('data-tab');
1662
+ if (!target) return;
1663
+ this.showTab(target);
1664
+ });
1665
+ });
1666
+
1667
+ if (closeInline) {
1668
+ closeInline.addEventListener('click', (event) => {
1669
+ event.preventDefault();
1670
+ this.closeTicketFromWidget();
1671
+ });
1672
+ }
1673
+
1674
+ if (composerInput) {
1675
+ composerInput.addEventListener('focus', () => composerInner && composerInner.classList.add('focused'));
1676
+ composerInput.addEventListener('blur', () => composerInner && composerInner.classList.remove('focused'));
1677
+ composerInput.addEventListener('keydown', (event) => {
1678
+ if (event.key === 'Enter' && !event.shiftKey) {
1679
+ event.preventDefault();
1680
+ this.handleComposerSend();
1681
+ }
1682
+ });
1683
+ }
1684
+
1685
+ if (composerSend) composerSend.addEventListener('click', () => this.handleComposerSend());
1686
+ if (composerAttach) composerAttach.addEventListener('click', () => composerFileInput && composerFileInput.click());
1687
+ if (composerFileInput) composerFileInput.addEventListener('change', (event) => this.handleFileSelect(event));
1688
+
1689
+ if (composerInner) {
1690
+ ['dragenter', 'dragover'].forEach((type) => {
1691
+ composerInner.addEventListener(type, (event) => {
1692
+ event.preventDefault();
1693
+ composerInner.classList.add('focused');
1694
+ });
1695
+ });
1696
+ ['dragleave', 'dragend'].forEach((type) => {
1697
+ composerInner.addEventListener(type, () => composerInner.classList.remove('focused'));
1698
+ });
1699
+ composerInner.addEventListener('drop', (event) => {
1700
+ event.preventDefault();
1701
+ composerInner.classList.remove('focused');
1702
+ const files = Array.from(event.dataTransfer?.files || []);
1703
+ if (files.length) this.addComposerFiles(files);
1704
+ });
1705
+ }
1706
+
1707
+ document.addEventListener('paste', this.handlePasteBound);
1708
+
1709
+ if (typeof window !== 'undefined' && window.ethereum) {
1710
+ try {
1711
+ window.ethereum.on('accountsChanged', (accounts) => {
1712
+ if (accounts.length > 0) {
1713
+ this.walletAddress = accounts[0];
1714
+ this.updateWalletUI();
1715
+ this.loadMyTickets();
1716
+ } else {
1717
+ this.disconnectWallet();
1718
+ }
1719
+ });
1720
+ } catch (_) {}
1721
+ }
1722
+ }
1723
+
1724
+ setComposerMode(mode, options = {}) {
1725
+ this.state.composerMode = mode;
1726
+ const { composer, composerInput, composerSend, composerNote } = this.elements;
1727
+ if (!composer || !composerInput || !composerSend) return;
1728
+
1729
+ const defaults = {
1730
+ placeholder: 'Type your message...',
1731
+ note: '',
1732
+ sendLabel: this.getSendIcon(),
1733
+ disabled: false,
1734
+ };
1735
+ const config = { ...defaults, ...options };
1736
+
1737
+ composer.classList.toggle('disabled', Boolean(config.disabled));
1738
+ composerInput.disabled = Boolean(config.disabled);
1739
+ composerSend.disabled = Boolean(config.disabled);
1740
+ composerInput.placeholder = config.placeholder;
1741
+ composerSend.innerHTML = config.sendLabel;
1742
+ if (composerNote) {
1743
+ composerNote.textContent = config.note || '';
1744
+ composerNote.style.display = config.note ? 'block' : 'none';
1745
+ }
1746
+
1747
+ if (mode === 'new') {
1748
+ composerInput.value = '';
1749
+ }
1750
+
1751
+ if (mode === 'disabled' || mode === 'closed') {
1752
+ this.clearComposer();
1753
+ }
1754
+ }
1755
+
1756
+ setComposerLoading(isLoading) {
1757
+ this.state.composerLoading = isLoading;
1758
+ const { composerSend } = this.elements;
1759
+ if (composerSend) {
1760
+ composerSend.disabled = isLoading || this.state.composerMode === 'disabled' || this.state.composerMode === 'closed';
1761
+ if (isLoading) {
1762
+ composerSend.textContent = '…';
1763
+ } else {
1764
+ composerSend.innerHTML = this.getSendIcon();
1765
+ }
1766
+ }
1767
+ }
1768
+
1769
+ clearComposer() {
1770
+ const { composerInput, composerFileInput } = this.elements;
1771
+ if (composerInput) composerInput.value = '';
1772
+ if (composerFileInput) composerFileInput.value = '';
1773
+ this.composerFiles = [];
1774
+ this.renderComposerAttachments();
1775
+ }
1776
+
1777
+ addComposerFiles(files) {
1778
+ if (!Array.isArray(files)) files = Array.from(files || []);
1779
+ if (files.length === 0) return;
1780
+ const maxFiles = 5;
1781
+ const maxSize = 5 * 1024 * 1024; // 5MB
1782
+ const accepted = [];
1783
+ for (const file of files) {
1784
+ if (this.composerFiles.length + accepted.length >= maxFiles) {
1785
+ this.showMessage(`Up to ${maxFiles} files allowed`, 'error');
1786
+ break;
1787
+ }
1788
+ if (file.size > maxSize) {
1789
+ this.showMessage(`File ${file.name} exceeds 5MB.`, 'error');
1790
+ continue;
1791
+ }
1792
+ accepted.push(file);
1793
+ }
1794
+ if (accepted.length) {
1795
+ this.composerFiles = [...this.composerFiles, ...accepted];
1796
+ this.renderComposerAttachments();
1797
+ }
1798
+ }
1799
+
1800
+ removeComposerFile(index) {
1801
+ if (index < 0 || index >= this.composerFiles.length) return;
1802
+ this.composerFiles.splice(index, 1);
1803
+ this.renderComposerAttachments();
1804
+ }
1805
+
1806
+ renderComposerAttachments() {
1807
+ const { composerChips } = this.elements;
1808
+ if (!composerChips) return;
1809
+ if (!this.composerFiles.length) {
1810
+ composerChips.innerHTML = '';
1811
+ return;
1812
+ }
1813
+ composerChips.innerHTML = this.composerFiles.map((file, index) => `
1814
+ <div class="hypercrm-chip">
1815
+ ${this.renderIcon(this.iconUrls?.file, '', 'hypercrm-icon-img small')}
1816
+ <span>${this.escapeHtml(file.name)}</span>
1817
+ <button type="button" data-index="${index}" aria-label="Remove">${this.renderIcon(this.iconUrls?.close, '', 'hypercrm-icon-img tiny')}</button>
1818
+ </div>
1819
+ `).join('');
1820
+ composerChips.querySelectorAll('button[data-index]').forEach((btn) => {
1821
+ btn.addEventListener('click', (event) => {
1822
+ event.preventDefault();
1823
+ const idx = parseInt(btn.getAttribute('data-index'), 10);
1824
+ this.removeComposerFile(idx);
1825
+ });
1826
+ });
1827
+ }
1828
+
1829
+ handleFileSelect(event) {
1830
+ const files = Array.from(event.target?.files || []);
1831
+ if (files.length) this.addComposerFiles(files);
1832
+ }
1833
+
1834
+ handlePaste(event) {
1835
+ if (!event || !event.clipboardData) return;
1836
+ const items = event.clipboardData.items || [];
1837
+ const files = [];
1838
+ for (const item of items) {
1839
+ if (item.kind === 'file') {
1840
+ const file = item.getAsFile();
1841
+ if (file) files.push(file);
1842
+ }
1843
+ }
1844
+ if (files.length) {
1845
+ event.preventDefault();
1846
+ this.addComposerFiles(files);
1847
+ }
1848
+ }
1849
+ async handleComposerSend() {
1850
+ const mode = this.state.composerMode;
1851
+ if (mode === 'disabled' || mode === 'closed' || !this.elements.composerInput) return;
1852
+ const message = this.elements.composerInput.value.trim();
1853
+ const attachments = [...this.composerFiles];
1854
+ if (!message && attachments.length === 0) return;
1855
+
1856
+ const optimisticId = `tmp-${Date.now()}`;
1857
+ if (mode === 'new') {
1858
+ if (!this.walletAddress) {
1859
+ this.showMessage('Connect your wallet to create a ticket.', 'error');
1860
+ return;
1861
+ }
1862
+ const payload = { message, attachments };
1863
+ this.prepareConversationForNewTicket();
1864
+ this.addOptimisticBubble({ id: optimisticId, mode: 'new', payload });
1865
+ this.setComposerMode('disabled', { note: 'Creating ticket…', disabled: true, sendLabel: this.getSendIcon() });
1866
+ this.clearComposer();
1867
+ this.sendNewTicket(optimisticId, payload);
1868
+ } else if (mode === 'reply') {
1869
+ if (!this.currentTicket || !this.currentTicket.id) {
1870
+ this.showMessage('No active ticket selected.', 'error');
1871
+ return;
1872
+ }
1873
+ const payload = { message, attachments, ticketId: this.currentTicket.id };
1874
+ this.addOptimisticBubble({ id: optimisticId, mode: 'reply', payload });
1875
+ this.clearComposer();
1876
+ this.sendReply(optimisticId, payload);
1877
+ }
1878
+ }
1879
+
1880
+ prepareConversationForNewTicket() {
1881
+ if (this.currentTicket && this.currentTicket.id) return;
1882
+ this.currentTicket = { id: null, status: 'open', Messages: [] };
1883
+ this.showTicketOverview = false;
1884
+ this.renderTicketDetail(this.currentTicket);
1885
+ }
1886
+
1887
+ async sendNewTicket(optimisticId, payload) {
1888
+ try {
1889
+ const data = await this.createTicketRequest(payload);
1890
+ this.resolveOptimisticBubble(optimisticId);
1891
+ await this.loadMyTickets();
1892
+ if (data?.ticket?.id) {
1893
+ await this.showTicketDetail(data.ticket.id);
1894
+ this.showMessage(`Ticket created! (#${data.ticket.ticketNumber || ''})`, 'success');
1895
+ } else {
1896
+ this.renderTicketState();
1897
+ }
1898
+ } catch (error) {
1899
+ console.error('Failed to create ticket:', error);
1900
+ this.markOptimisticBubbleFailed(optimisticId, () => this.sendNewTicket(optimisticId, payload), error.message || 'Failed to create ticket.');
1901
+ this.showMessage(error.message || 'Failed to create ticket.', 'error');
1902
+ }
1903
+ }
1904
+
1905
+ async sendReply(optimisticId, payload) {
1906
+ try {
1907
+ await this.submitReplyRequest(payload);
1908
+ this.resolveOptimisticBubble(optimisticId);
1909
+ await this.refreshTicketDetail();
1910
+ } catch (error) {
1911
+ console.error('Failed to send reply:', error);
1912
+ this.markOptimisticBubbleFailed(optimisticId, () => this.sendReply(optimisticId, payload), error.message || 'Failed to send reply.');
1913
+ this.showMessage(error.message || 'Failed to send reply.', 'error');
1914
+ }
1915
+ }
1916
+
1917
+ async createTicketRequest({ message, attachments }) {
1918
+ const formData = new FormData();
1919
+ formData.append('message', message);
1920
+ formData.append('source', 'widget');
1921
+ const subject = message ? message.slice(0, 60) : 'New ticket via widget';
1922
+ formData.append('subject', subject);
1923
+ formData.append('customerWalletAddress', this.walletAddress);
1924
+ formData.append('customerName', `User ${this.shortenAddress(this.walletAddress)}`);
1925
+ attachments.forEach((file) => formData.append('attachments', file));
1926
+
1927
+ const response = await fetch(`${this.apiUrl}/tickets`, {
1928
+ method: 'POST',
1929
+ headers: { 'X-API-Key': this.apiKey },
1930
+ body: formData,
1931
+ });
1932
+ const data = await response.json().catch(() => ({}));
1933
+ if (!response.ok) {
1934
+ const error = new Error(data?.error || 'Failed to create ticket.');
1935
+ throw error;
1936
+ }
1937
+ return data;
1938
+ }
1939
+
1940
+ async submitReplyRequest({ message, attachments, ticketId }) {
1941
+ let response;
1942
+ if (attachments && attachments.length > 0) {
1943
+ const form = new FormData();
1944
+ if (message) form.append('content', message);
1945
+ attachments.forEach((file) => form.append('attachments', file));
1946
+ response = await fetch(`${this.apiUrl}/tickets/public/${ticketId}/messages`, {
1947
+ method: 'POST',
1948
+ headers: { 'X-API-Key': this.apiKey },
1949
+ body: form,
1950
+ });
1951
+ } else {
1952
+ response = await fetch(`${this.apiUrl}/tickets/public/${ticketId}/messages`, {
1953
+ method: 'POST',
1954
+ headers: {
1955
+ 'Content-Type': 'application/json',
1956
+ 'X-API-Key': this.apiKey,
1957
+ },
1958
+ body: JSON.stringify({ content: message }),
1959
+ });
1960
+ }
1961
+ const data = await response.json().catch(() => ({}));
1962
+ if (!response.ok) {
1963
+ const error = new Error(data?.error || 'Failed to send reply.');
1964
+ throw error;
1965
+ }
1966
+ return data;
1967
+ }
1968
+
1969
+ addOptimisticBubble({ id, mode, payload }) {
1970
+ const list = document.getElementById('hypercrm-message-list');
1971
+ if (!list) return;
1972
+ const group = document.createElement('div');
1973
+ group.className = 'hypercrm-message-group me hypercrm-optimistic';
1974
+ group.dataset.optimisticId = id;
1975
+
1976
+ const avatarHtml = '<div class="hypercrm-avatar me">Me</div>';
1977
+ const bubble = document.createElement('div');
1978
+ bubble.className = 'hypercrm-bubble me';
1979
+
1980
+ const contentEl = document.createElement('div');
1981
+ contentEl.className = 'hypercrm-bubble-content';
1982
+ contentEl.textContent = payload.message || (payload.attachments && payload.attachments.length ? '[Attachment]' : '');
1983
+ bubble.appendChild(contentEl);
1984
+
1985
+ const previewUrls = [];
1986
+ if (payload.attachments && payload.attachments.length) {
1987
+ payload.attachments.forEach((file) => {
1988
+ const item = document.createElement('div');
1989
+ item.className = 'hypercrm-attachment-chip';
1990
+ if (file.type && file.type.startsWith('image/')) {
1991
+ const url = URL.createObjectURL(file);
1992
+ previewUrls.push(url);
1993
+ const img = document.createElement('img');
1994
+ img.src = url;
1995
+ img.alt = file.name;
1996
+ item.appendChild(img);
1997
+ } else {
1998
+ item.textContent = file.name || 'Attachment';
1999
+ }
2000
+ bubble.appendChild(item);
2001
+ });
2002
+ }
2003
+
2004
+ const status = document.createElement('div');
2005
+ status.className = 'hypercrm-bubble-status';
2006
+ status.innerHTML = `<span class="hypercrm-bubble-spinner"></span>Sending…`;
2007
+ bubble.appendChild(status);
2008
+
2009
+ group.innerHTML = `${avatarHtml}`;
2010
+ group.appendChild(bubble);
2011
+ list.appendChild(group);
2012
+ list.scrollTop = list.scrollHeight;
2013
+
2014
+ this.optimisticMessages.set(id, {
2015
+ mode,
2016
+ payload,
2017
+ group,
2018
+ status,
2019
+ previewUrls,
2020
+ });
2021
+ }
2022
+
2023
+ resolveOptimisticBubble(id) {
2024
+ const entry = this.optimisticMessages.get(id);
2025
+ if (!entry) return;
2026
+ try { entry.previewUrls?.forEach((url) => URL.revokeObjectURL(url)); } catch (_) {}
2027
+ entry.group?.remove();
2028
+ this.optimisticMessages.delete(id);
2029
+ }
2030
+
2031
+ markOptimisticBubbleFailed(id, retryFn, message) {
2032
+ const entry = this.optimisticMessages.get(id);
2033
+ if (!entry || !entry.status) return;
2034
+ entry.status.innerHTML = '';
2035
+ entry.status.classList.add('hypercrm-bubble-error');
2036
+ const text = document.createElement('span');
2037
+ text.className = 'hypercrm-bubble-error';
2038
+ text.textContent = message || 'Failed to send.';
2039
+ entry.status.appendChild(text);
2040
+
2041
+ if (entry.mode === 'new') {
2042
+ this.setComposerMode('new', { placeholder: 'Type your message…', sendLabel: this.getSendIcon() });
2043
+ if (this.elements.composer) this.elements.composer.style.display = 'flex';
2044
+ if (this.elements.composerInput) {
2045
+ this.elements.composerInput.value = entry.payload?.message || '';
2046
+ }
2047
+ this.composerFiles = [];
2048
+ this.updateFileList();
2049
+ if (entry.payload?.attachments?.length) {
2050
+ this.addComposerFiles(entry.payload.attachments);
2051
+ }
2052
+ } else if (entry.mode === 'reply') {
2053
+ if (this.elements.composer) this.elements.composer.style.display = 'flex';
2054
+ this.setComposerMode('reply', { placeholder: 'Type your message…', sendLabel: this.getSendIcon() });
2055
+ if (this.elements.composerInput) {
2056
+ this.elements.composerInput.value = entry.payload?.message || '';
2057
+ }
2058
+ this.composerFiles = [];
2059
+ this.updateFileList();
2060
+ if (entry.payload?.attachments?.length) {
2061
+ this.addComposerFiles(entry.payload.attachments);
2062
+ }
2063
+ }
2064
+
2065
+ const actions = document.createElement('div');
2066
+ actions.className = 'hypercrm-bubble-error-actions';
2067
+ const retryBtn = document.createElement('button');
2068
+ retryBtn.className = 'hypercrm-bubble-error-btn';
2069
+ retryBtn.title = 'Retry';
2070
+ retryBtn.innerHTML = this.renderIcon(this.iconUrls?.retry, '', 'hypercrm-icon-img small');
2071
+ const cancelBtn = document.createElement('button');
2072
+ cancelBtn.className = 'hypercrm-bubble-error-btn';
2073
+ cancelBtn.title = 'Cancel';
2074
+ cancelBtn.innerHTML = this.renderIcon(this.iconUrls?.close, '', 'hypercrm-icon-img small');
2075
+ actions.appendChild(retryBtn);
2076
+ actions.appendChild(cancelBtn);
2077
+ entry.status.appendChild(actions);
2078
+
2079
+ retryBtn.addEventListener('click', () => {
2080
+ entry.status.classList.remove('hypercrm-bubble-error');
2081
+ entry.status.innerHTML = `<span class="hypercrm-bubble-spinner"></span>Sending…`;
2082
+ retryFn();
2083
+ });
2084
+
2085
+ cancelBtn.addEventListener('click', () => {
2086
+ this.resolveOptimisticBubble(id);
2087
+ if (entry.mode === 'new') {
2088
+ this.currentTicket = null;
2089
+ this.renderTicketState();
2090
+ }
2091
+ });
2092
+ }
2093
+
2094
+ showMessage(message, type = 'info') {
2095
+ const { toast } = this.elements;
2096
+ if (!toast) return;
2097
+ toast.textContent = message;
2098
+ toast.classList.remove('success', 'error', 'info');
2099
+ toast.classList.add(type, 'show');
2100
+ clearTimeout(this.toastTimer);
2101
+ this.toastTimer = setTimeout(() => {
2102
+ toast.classList.remove('show');
2103
+ }, 4000);
2104
+ }
2105
+
2106
+ showTab(tab) {
2107
+ const { tabs, ticketPane, eventsPane, composer } = this.elements;
2108
+ this.currentTab = tab;
2109
+ tabs.forEach((button) => {
2110
+ const target = button.getAttribute('data-tab');
2111
+ button.classList.toggle('active', target === tab);
2112
+ });
2113
+ if (ticketPane) ticketPane.classList.toggle('active', tab === 'ticket');
2114
+ if (eventsPane) eventsPane.classList.toggle('active', tab === 'events');
2115
+ if (tab === 'ticket') {
2116
+ if (composer) composer.style.display = 'flex';
2117
+ this.renderTicketState();
2118
+ } else if (tab === 'events') {
2119
+ if (composer) composer.style.display = 'none';
2120
+ this.loadEvents();
2121
+ this.updateCloseInline(false);
2122
+ }
2123
+ }
2124
+
2125
+ renderTicketState() {
2126
+ const { ticketContent, composer } = this.elements;
2127
+ if (!ticketContent) return;
2128
+ this.updateCloseInline(false);
2129
+ if (!this.walletAddress) {
2130
+ this.pendingTicketId = null;
2131
+ this.clearComposer();
2132
+ ticketContent.innerHTML = `
2133
+ <div class="hypercrm-empty-state">
2134
+ <div class="hypercrm-empty-icon">${this.renderIcon(this.iconUrls?.lock, '', 'hypercrm-icon-img xl')}</div>
2135
+ <h3 class="hypercrm-empty-title">Connect your wallet</h3>
2136
+ <p class="hypercrm-empty-desc">Connect your wallet to resume past conversations or start a new one.</p>
2137
+ <button type="button" class="hypercrm-primary-btn" id="hypercrm-connect-inline">Connect wallet</button>
2138
+ </div>`;
2139
+ this.setComposerMode('disabled', { note: 'Connect your wallet to start a conversation.', disabled: true, sendLabel: this.getSendIcon() });
2140
+ if (composer) composer.style.display = 'flex';
2141
+ const btn = document.getElementById('hypercrm-connect-inline');
2142
+ if (btn) btn.addEventListener('click', () => this.connectWallet());
2143
+ return;
2144
+ }
2145
+
2146
+ const openTicket = (this.tickets || []).find((ticket) => (ticket.status || '').toLowerCase() !== 'closed');
2147
+
2148
+ if (openTicket) {
2149
+ if (this.currentTicket && this.currentTicket.id && String(this.currentTicket.id) === String(openTicket.id) && Array.isArray(this.currentTicket.Messages)) {
2150
+ this.renderTicketDetail(this.currentTicket);
2151
+ return;
2152
+ }
2153
+ if (this.pendingTicketId && String(this.pendingTicketId) === String(openTicket.id)) {
2154
+ return;
2155
+ }
2156
+ this.pendingTicketId = openTicket.id;
2157
+ this.renderTicketLoading('Loading your conversation…');
2158
+ this.setComposerMode('disabled', { note: 'Loading ticket…', disabled: true, sendLabel: this.getSendIcon() });
2159
+ this.showTicketDetail(openTicket.id);
2160
+ return;
2161
+ }
2162
+
2163
+ this.currentTicket = null;
2164
+ this.pendingTicketId = null;
2165
+ this.renderEmptyTicket();
2166
+ if (composer) composer.style.display = 'flex';
2167
+ }
2168
+
2169
+ renderEmptyTicket() {
2170
+ const { ticketContent, composer } = this.elements;
2171
+ if (!ticketContent) return;
2172
+ ticketContent.innerHTML = `
2173
+ <div class="hypercrm-empty-state">
2174
+ <div class="hypercrm-empty-icon" aria-hidden="true">
2175
+ <svg viewBox="0 0 24 24" width="30" height="30" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
2176
+ <path d="M21 14a4 4 0 0 1-4 4H8l-4 3V8a4 4 0 0 1 4-4h9a4 4 0 0 1 4 4z"/>
2177
+ <path d="M8.5 9.5h7"/>
2178
+ <path d="M8.5 12.5h5"/>
2179
+ </svg>
2180
+ </div>
2181
+ <h3 class="hypercrm-empty-title">Start a new conversation</h3>
2182
+ <p class="hypercrm-empty-desc">Click the button below to start a conversation with our support team.</p>
2183
+ <p class="hypercrm-empty-warning">Never share private keys, passwords, or seed phrases.</p>
2184
+ <button type="button" class="hypercrm-primary-btn" id="hypercrm-start-ticket">Create new ticket</button>
2185
+ </div>`;
2186
+ if (this.state.composerMode !== 'new') {
2187
+ this.setComposerMode('new', { placeholder: 'Type your message…', sendLabel: this.getSendIcon() });
2188
+ }
2189
+ if (composer) composer.style.display = 'flex';
2190
+ const btn = document.getElementById('hypercrm-start-ticket');
2191
+ if (btn) btn.addEventListener('click', () => {
2192
+ const { composerInput } = this.elements;
2193
+ if (composerInput) composerInput.focus();
2194
+ });
2195
+ }
2196
+
2197
+ renderTicketLoading(message) {
2198
+ const { ticketContent, composer } = this.elements;
2199
+ if (!ticketContent) return;
2200
+ ticketContent.innerHTML = `<div class="hypercrm-loading-state">${this.escapeHtml(message || 'Loading…')}</div>`;
2201
+ this.updateCloseInline(false);
2202
+ if (composer) composer.style.display = 'none';
2203
+ }
2204
+
2205
+ updateCloseInline(visible) {
2206
+ const { closeInline } = this.elements;
2207
+ if (!closeInline) return;
2208
+ if (visible) {
2209
+ closeInline.style.display = 'inline-flex';
2210
+ closeInline.disabled = false;
2211
+ } else {
2212
+ closeInline.style.display = 'none';
2213
+ closeInline.disabled = true;
2214
+ }
2215
+ }
2216
+ renderTicketDetail(ticket) {
2217
+ const { ticketContent, composer } = this.elements;
2218
+ if (!ticket || !ticketContent) return;
2219
+ this.currentTicket = ticket;
2220
+ this.pendingTicketId = null;
2221
+ this.showTicketOverview = false;
2222
+
2223
+ const statusRaw = (ticket.status || 'open').toLowerCase();
2224
+ const isClosed = statusRaw === 'closed';
2225
+ const messages = Array.isArray(ticket.Messages) ? ticket.Messages : [];
2226
+ const messagesHtml = messages.map((message) => this.renderMessageBubble(message)).join('');
2227
+ const systemNotes = messages
2228
+ .filter((message) => (message.senderType || '').toLowerCase() === 'system')
2229
+ .map((message) => String(message.content || '').toLowerCase());
2230
+ const closedByAdmin = systemNotes.some((text) => text.includes('closed by admin'));
2231
+ const closedTitle = ticket.__closedReason === 'missing'
2232
+ ? 'This ticket is no longer available'
2233
+ : (ticket.__closedReason === 'admin' || closedByAdmin)
2234
+ ? 'This ticket was closed by an admin'
2235
+ : 'This ticket is closed';
2236
+ const closedDesc = 'Start a new ticket if you need more help.';
2237
+ const closedBannerHtml = isClosed
2238
+ ? `<div class="hypercrm-closed-banner"><h4>${closedTitle}</h4><p>${closedDesc}</p><button type="button" class="hypercrm-primary-btn" id="hypercrm-new-ticket">Create new ticket</button></div>`
2239
+ : '';
2240
+
2241
+ this.updateCloseInline(Boolean(ticket.id && !isClosed));
2242
+
2243
+ ticketContent.innerHTML = `
2244
+ <div class="hypercrm-conversation-shell">
2245
+ <div class="hypercrm-message-list" id="hypercrm-message-list">${messagesHtml}</div>
2246
+ ${closedBannerHtml}
2247
+ </div>
2248
+ `;
2249
+
2250
+ if (isClosed) {
2251
+ this.setComposerMode('closed', {
2252
+ note: 'This ticket is closed. Start a new ticket to continue.',
2253
+ placeholder: 'Ticket closed',
2254
+ sendLabel: this.getSendIcon(),
2255
+ disabled: true,
2256
+ });
2257
+ if (composer) composer.style.display = 'flex';
2258
+ } else {
2259
+ if (composer) composer.style.display = 'flex';
2260
+ this.setComposerMode('reply', { placeholder: 'Type your message…', sendLabel: this.getSendIcon() });
2261
+ }
2262
+
2263
+ const newTicketBtn = document.getElementById('hypercrm-new-ticket');
2264
+ if (newTicketBtn) {
2265
+ newTicketBtn.addEventListener('click', () => {
2266
+ const closedId = this.currentTicket?.id;
2267
+ this.currentTicket = null;
2268
+ this.pendingTicketId = null;
2269
+ this.clearComposer();
2270
+ if (closedId && Array.isArray(this.tickets)) {
2271
+ this.tickets = this.tickets.filter((ticket) => String(ticket.id) !== String(closedId));
2272
+ }
2273
+ this.renderTicketState();
2274
+ const { composerInput } = this.elements;
2275
+ if (composerInput) composerInput.focus();
2276
+ });
2277
+ }
2278
+
2279
+ const messageList = document.getElementById('hypercrm-message-list');
2280
+ if (messageList) {
2281
+ messageList.querySelectorAll('[data-hypercrm-img]').forEach((img) => {
2282
+ img.addEventListener('click', () => {
2283
+ const url = img.getAttribute('data-hypercrm-img');
2284
+ if (url) this.openLightbox(url);
2285
+ });
2286
+ });
2287
+ messageList.scrollTop = messageList.scrollHeight;
2288
+ }
2289
+
2290
+ this.startAutoRefresh();
2291
+ }
2292
+
2293
+ markTicketClosed(reason = 'closed') {
2294
+ if (!this.currentTicket) return;
2295
+ const closedId = this.currentTicket.id;
2296
+ this.currentTicket = {
2297
+ ...this.currentTicket,
2298
+ status: 'closed',
2299
+ __closedReason: reason,
2300
+ };
2301
+ this.pendingTicketId = null;
2302
+ if (!Array.isArray(this.currentTicket.Messages)) {
2303
+ this.currentTicket.Messages = [];
2304
+ }
2305
+ if (closedId && Array.isArray(this.tickets)) {
2306
+ this.tickets = this.tickets.filter((ticket) => String(ticket.id) !== String(closedId));
2307
+ }
2308
+ this.renderTicketDetail(this.currentTicket);
2309
+ }
2310
+
2311
+ renderMessageBubble(message) {
2312
+ const senderType = (message.senderType || 'agent').toLowerCase();
2313
+ const bubbleClass = senderType === 'customer' ? 'hypercrm-bubble me' : senderType === 'system' ? 'hypercrm-bubble system' : 'hypercrm-bubble agent';
2314
+ const groupClass = senderType === 'customer' ? 'hypercrm-message-group me' : 'hypercrm-message-group';
2315
+ const timestamp = message.createdAt ? new Date(message.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '';
2316
+ const attachments = (message.Attachments || []).map((attachment) => this.renderMessageAttachment(attachment)).join('');
2317
+ const body = this.escapeHtml(message.content || '');
2318
+ if (senderType === 'system') {
2319
+ return `
2320
+ <div class="hypercrm-message-group">
2321
+ <div class="hypercrm-bubble system">
2322
+ <div class="hypercrm-bubble-content">${body}</div>
2323
+ <div class="hypercrm-bubble-meta">${timestamp}</div>
2324
+ </div>
2325
+ </div>`;
2326
+ }
2327
+ const avatarHtml = senderType === 'customer' ? '<div class="hypercrm-avatar me">Me</div>' : this.renderAgentAvatar();
2328
+ return `
2329
+ <div class="${groupClass}">
2330
+ ${avatarHtml}
2331
+ <div class="${bubbleClass}">
2332
+ <div class="hypercrm-bubble-content">${body}</div>
2333
+ ${attachments}
2334
+ ${timestamp ? `<div class="hypercrm-bubble-meta">${timestamp}</div>` : ''}
2335
+ </div>
2336
+ </div>`;
2337
+ }
2338
+
2339
+ renderMessageAttachment(attachment) {
2340
+ if (!attachment || !attachment.url) return '';
2341
+ const url = this.absUrl(attachment.url);
2342
+ const fileName = this.escapeHtml(attachment.fileName || 'Attachment');
2343
+ const isImage = (attachment.fileType && attachment.fileType.startsWith('image/')) || /\.(png|jpe?g|gif|webp|svg)$/i.test(attachment.fileName || '');
2344
+ if (isImage) {
2345
+ return `<div class="hypercrm-attachment-chip"><img src="${url}" alt="${fileName}" data-hypercrm-img="${url}" /></div>`;
2346
+ }
2347
+ return `<div class="hypercrm-attachment-chip">${this.renderIcon(this.iconUrls?.file, '', 'hypercrm-icon-img small')}<a href="${url}" target="_blank" rel="noopener">${fileName}</a></div>`;
2348
+ }
2349
+
2350
+ getStatusText(status) {
2351
+ const map = { open: 'Open', pending: 'Pending', resolved: 'Resolved', closed: 'Closed' };
2352
+ return map[(status || '').toLowerCase()] || status || 'Open';
2353
+ }
2354
+ async loadOrgInfo() {
2355
+ try {
2356
+ const response = await fetch(`${this.apiUrl}/organizations/public/info`, {
2357
+ headers: { 'X-API-Key': this.apiKey },
2358
+ });
2359
+ if (!response.ok) return;
2360
+ this.orgInfo = await response.json();
2361
+ if (!this.options.position && this.orgInfo?.widget?.position) {
2362
+ this.setPosition(this.orgInfo.widget.position);
2363
+ }
2364
+ const iconUrl = this.orgInfo?.widget?.iconUrl;
2365
+ this.orgIconUrl = iconUrl ? this.absUrl(iconUrl) : null;
2366
+ this.updateHeaderIcons();
2367
+ this.updateToggleIcon();
2368
+ } catch (error) {
2369
+ console.warn('Failed to load organization info', error);
2370
+ }
2371
+ }
2372
+ updateHeaderIcons() {
2373
+ const { telegramBtn, discordBtn } = this.elements;
2374
+ const telegramUsername = this.orgInfo?.telegram?.userBotUsername;
2375
+ const discordInvite = this.orgInfo?.discord?.inviteUrl;
2376
+ if (telegramBtn) {
2377
+ if (telegramUsername) {
2378
+ const username = String(telegramUsername).replace(/^@/, '');
2379
+ telegramBtn.classList.remove('disabled');
2380
+ telegramBtn.href = `https://t.me/${username}`;
2381
+ telegramBtn.title = `Telegram @${username}`;
2382
+ } else {
2383
+ telegramBtn.classList.add('disabled');
2384
+ telegramBtn.removeAttribute('href');
2385
+ }
2386
+ }
2387
+ if (discordBtn) {
2388
+ if (discordInvite) {
2389
+ discordBtn.classList.remove('disabled');
2390
+ discordBtn.href = discordInvite;
2391
+ discordBtn.title = 'Discord channel';
2392
+ } else {
2393
+ discordBtn.classList.add('disabled');
2394
+ discordBtn.removeAttribute('href');
2395
+ }
2396
+ }
2397
+ }
2398
+
2399
+ updateToggleIcon() {
2400
+ const iconUrl = this.orgIconUrl;
2401
+ const { toggleInner } = this.elements;
2402
+ if (!toggleInner) return;
2403
+ if (iconUrl) {
2404
+ toggleInner.innerHTML = `<img src="${iconUrl}" alt="Chat" class="hypercrm-toggle-avatar" />`;
2405
+ } else {
2406
+ toggleInner.innerHTML = this.renderIcon(this.iconUrls?.chat, '', 'hypercrm-icon-img large');
2407
+ }
2408
+ }
2409
+
2410
+ async checkWalletConnection() {
2411
+ if (typeof window.ethereum !== 'undefined') {
2412
+ try {
2413
+ const accounts = await window.ethereum.request({ method: 'eth_accounts' });
2414
+ if (accounts.length > 0) {
2415
+ this.walletAddress = accounts[0];
2416
+ this.updateWalletUI();
2417
+ await this.loadMyTickets();
2418
+ this.showTicketOverview = false;
2419
+ }
2420
+ } catch (error) {
2421
+ console.error('Failed to check wallet:', error);
2422
+ if (error?.code === 4900) {
2423
+ this.handleWalletDisconnected();
2424
+ }
2425
+ }
2426
+ }
2427
+ this.updateWalletUI();
2428
+ }
2429
+
2430
+ async connectWallet() {
2431
+ if (typeof window.ethereum === 'undefined') {
2432
+ this.showMessage('MetaMask is not installed.', 'error');
2433
+ return;
2434
+ }
2435
+ try {
2436
+ const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
2437
+ this.walletAddress = accounts[0];
2438
+ this.updateWalletUI();
2439
+ await this.loadMyTickets();
2440
+ this.showTicketOverview = false;
2441
+ this.showMessage('Wallet connected', 'success');
2442
+ } catch (error) {
2443
+ console.error('Failed to connect wallet:', error);
2444
+ this.showMessage('Failed to connect wallet.', 'error');
2445
+ }
2446
+ }
2447
+
2448
+ updateWalletUI() {
2449
+ const { walletSlot } = this.elements;
2450
+ if (!walletSlot) return;
2451
+ walletSlot.innerHTML = '';
2452
+ if (this.walletAddress) {
2453
+ const button = document.createElement('button');
2454
+ button.type = 'button';
2455
+ button.className = 'hypercrm-wallet-chip';
2456
+ button.title = this.walletAddress;
2457
+ button.textContent = this.shortenAddress(this.walletAddress);
2458
+ button.addEventListener('click', () => this.disconnectWallet());
2459
+ walletSlot.appendChild(button);
2460
+ } else {
2461
+ const button = document.createElement('button');
2462
+ button.type = 'button';
2463
+ button.className = 'hypercrm-wallet-connect';
2464
+ button.textContent = 'Connect wallet';
2465
+ button.addEventListener('click', () => this.connectWallet());
2466
+ walletSlot.appendChild(button);
2467
+ }
2468
+ }
2469
+
2470
+ disconnectWallet() {
2471
+ this.walletAddress = null;
2472
+ this.tickets = [];
2473
+ this.currentTicket = null;
2474
+ this.showTicketOverview = false;
2475
+ this.pendingTicketId = null;
2476
+ this.clearComposer();
2477
+ this.updateWalletUI();
2478
+ this.renderTicketState();
2479
+ }
2480
+
2481
+ handleWalletDisconnected() {
2482
+ this.disconnectWallet();
2483
+ }
2484
+ async loadMyTickets() {
2485
+ if (!this.walletAddress) return;
2486
+ try {
2487
+ const response = await fetch(`${this.apiUrl}/tickets/my?wallet=${encodeURIComponent(this.walletAddress)}`, {
2488
+ headers: { 'X-API-Key': this.apiKey },
2489
+ });
2490
+ if (!response.ok) return;
2491
+ const data = await response.json();
2492
+ this.tickets = Array.isArray(data.tickets) ? data.tickets : [];
2493
+ const openTicket = this.tickets.find((ticket) => (ticket.status || '').toLowerCase() !== 'closed');
2494
+ if (this.currentTicket && openTicket && String(this.currentTicket.id) === String(openTicket.id)) {
2495
+ this.currentTicket.status = openTicket.status;
2496
+ this.currentTicket.ticketNumber = openTicket.ticketNumber;
2497
+ this.currentTicket.subject = openTicket.subject;
2498
+ }
2499
+ if (!openTicket && this.currentTicket && (this.currentTicket.status || '').toLowerCase() !== 'closed') {
2500
+ this.currentTicket.status = 'closed';
2501
+ }
2502
+ this.renderTicketState();
2503
+ } catch (error) {
2504
+ console.error('Failed to load tickets:', error);
2505
+ }
2506
+ }
2507
+
2508
+ async showTicketDetail(ticketId) {
2509
+ try {
2510
+ const expectedId = String(ticketId);
2511
+ this.pendingTicketId = ticketId;
2512
+ const response = await fetch(`${this.apiUrl}/tickets/public/${ticketId}`, {
2513
+ headers: { 'X-API-Key': this.apiKey },
2514
+ });
2515
+ if (!response.ok) {
2516
+ if (response.status === 404) {
2517
+ const stillActive = (this.pendingTicketId && String(this.pendingTicketId) === expectedId)
2518
+ || (this.currentTicket && String(this.currentTicket.id) === expectedId);
2519
+ if (stillActive) this.markTicketClosed('missing');
2520
+ }
2521
+ if (this.pendingTicketId && String(this.pendingTicketId) === expectedId) {
2522
+ this.pendingTicketId = null;
2523
+ }
2524
+ return;
2525
+ }
2526
+ const ticket = await response.json();
2527
+ const stillActive = (this.pendingTicketId && String(this.pendingTicketId) === expectedId)
2528
+ || (this.currentTicket && String(this.currentTicket.id) === expectedId);
2529
+ if (!stillActive) return;
2530
+ this.pendingTicketId = null;
2531
+ this.renderTicketDetail(ticket);
2532
+ } catch (error) {
2533
+ this.pendingTicketId = null;
2534
+ console.error('Failed to load ticket detail:', error);
2535
+ }
2536
+ }
2537
+
2538
+ async closeTicketFromWidget() {
2539
+ if (!this.currentTicket) return;
2540
+ try {
2541
+ const closeToken = this.currentTicket?.metadata?.closeToken || null;
2542
+ const headers = { 'X-API-Key': this.apiKey };
2543
+ let body = null;
2544
+ if (closeToken) {
2545
+ headers['X-Close-Token'] = closeToken;
2546
+ body = JSON.stringify({ token: closeToken });
2547
+ headers['Content-Type'] = 'application/json';
2548
+ }
2549
+ const response = await fetch(`${this.apiUrl}/tickets/public/${this.currentTicket.id}/close`, {
2550
+ method: 'POST',
2551
+ headers,
2552
+ body,
2553
+ });
2554
+ if (response.ok) {
2555
+ const closedId = this.currentTicket?.id;
2556
+ this.currentTicket = null;
2557
+ this.pendingTicketId = null;
2558
+ this.clearComposer();
2559
+ try {
2560
+ if (closedId) {
2561
+ this.tickets = (this.tickets || []).filter((t) => String(t.id) !== String(closedId));
2562
+ }
2563
+ } catch (_) {}
2564
+ this.updateCloseInline(false);
2565
+ this.renderEmptyTicket();
2566
+ await this.loadMyTickets();
2567
+ this.renderTicketState();
2568
+ }
2569
+ } catch (error) {
2570
+ console.error('Failed to close ticket:', error);
2571
+ }
2572
+ }
2573
+
2574
+ async loadEvents() {
2575
+ try {
2576
+ const response = await fetch(`${this.apiUrl}/events/public`, {
2577
+ headers: { 'X-API-Key': this.apiKey },
2578
+ });
2579
+ if (!response.ok) return;
2580
+ const events = await response.json();
2581
+ this.events = Array.isArray(events) ? events : [];
2582
+ this.renderEvents();
2583
+ } catch (error) {
2584
+ console.error('Failed to load announcements:', error);
2585
+ }
2586
+ }
2587
+
2588
+ renderEvents() {
2589
+ const { eventsList, eventDetail } = this.elements;
2590
+ if (!eventsList) return;
2591
+ if (eventDetail) eventDetail.style.display = 'none';
2592
+ eventsList.style.display = 'block';
2593
+ if (!this.events.length) {
2594
+ eventsList.innerHTML = `<div class="hypercrm-empty-state"><div class="hypercrm-empty-icon">${this.renderIcon(this.iconUrls?.announcement, '', 'hypercrm-icon-img xl')}</div><h3 class="hypercrm-empty-title">No announcements yet</h3><p class="hypercrm-empty-desc">Announcements you publish will show up here for your users.</p></div>`;
2595
+ return;
2596
+ }
2597
+ const sorted = [...this.events].sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
2598
+ eventsList.innerHTML = sorted.map((event) => {
2599
+ const banner = event.bannerUrl ? `<img src="${this.absUrl(event.bannerUrl)}" alt="${this.escapeHtml(event.title || 'Announcement banner')}" />` : '';
2600
+ const createdAt = event.createdAt ? new Date(event.createdAt).toLocaleString() : '';
2601
+ const rawText = event.content || '';
2602
+ const summary = rawText.length > 140 ? `${rawText.slice(0, 140)}…` : rawText;
2603
+ return `
2604
+ <article class="hypercrm-event-card" data-id="${event.id}">
2605
+ <div class="hypercrm-event-banner">
2606
+ ${banner || '<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(15,23,42,0.08);color:#64748b;font-size:13px;">No banner</div>'}
2607
+ </div>
2608
+ <div class="hypercrm-event-content">
2609
+ <h3 class="hypercrm-event-title">${this.escapeHtml(event.title || 'Announcement')}</h3>
2610
+ <span class="hypercrm-event-meta">${this.escapeHtml(createdAt)}</span>
2611
+ ${summary ? `<p class="hypercrm-event-text">${this.escapeHtml(summary)}</p>` : ''}
2612
+ </div>
2613
+ </article>`;
2614
+ }).join('');
2615
+ eventsList.querySelectorAll('.hypercrm-event-card').forEach((card) => {
2616
+ card.addEventListener('click', () => {
2617
+ const id = card.getAttribute('data-id');
2618
+ const event = this.events.find((item) => String(item.id) === String(id));
2619
+ if (event) this.renderEventDetail(event);
2620
+ });
2621
+ });
2622
+ }
2623
+
2624
+ renderEventDetail(event) {
2625
+ const { eventsList, eventDetail } = this.elements;
2626
+ if (!eventDetail || !eventsList) return;
2627
+ eventsList.style.display = 'none';
2628
+ eventDetail.style.display = 'block';
2629
+ const banner = event.bannerUrl ? `<img src="${this.absUrl(event.bannerUrl)}" alt="${this.escapeHtml(event.title || 'Announcement banner')}" />` : '';
2630
+ const createdAt = event.createdAt ? new Date(event.createdAt).toLocaleString() : '';
2631
+ eventDetail.innerHTML = `
2632
+ <button type="button" class="hypercrm-secondary-btn hypercrm-with-icon" id="hypercrm-back-events">${this.renderIcon(this.iconUrls?.back, '', 'hypercrm-icon-img small')}Back to announcements</button>
2633
+ <article class="hypercrm-event-card" style="cursor:auto;">
2634
+ <div class="hypercrm-event-banner">
2635
+ ${banner || '<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(15,23,42,0.08);color:#64748b;font-size:13px;">No banner</div>'}
2636
+ </div>
2637
+ <div class="hypercrm-event-content">
2638
+ <h3 class="hypercrm-event-title">${this.escapeHtml(event.title || 'Announcement')}</h3>
2639
+ <span class="hypercrm-event-meta">${this.escapeHtml(createdAt)}</span>
2640
+ <p class="hypercrm-event-text">${this.escapeHtml(event.content || '')}</p>
2641
+ ${event.ctaUrl ? `<div class="hypercrm-event-actions"><a class="hypercrm-primary-btn" href="${event.ctaUrl}" target="_blank" rel="noopener">${this.escapeHtml(event.ctaLabel || 'Open link')}</a></div>` : ''}
2642
+ </div>
2643
+ </article>`;
2644
+ const backBtn = document.getElementById('hypercrm-back-events');
2645
+ if (backBtn) backBtn.addEventListener('click', () => this.renderEvents());
2646
+ }
2647
+ startAutoRefresh() {
2648
+ this.stopAutoRefresh();
2649
+ if (this.isOpen && this.currentTicket && !this.socketConnected) {
2650
+ this.refreshInterval = setInterval(() => {
2651
+ if (this.isOpen && this.currentTicket) this.refreshTicketDetail();
2652
+ }, 3000);
2653
+ }
2654
+ }
2655
+
2656
+ stopAutoRefresh() {
2657
+ if (this.refreshInterval) {
2658
+ clearInterval(this.refreshInterval);
2659
+ this.refreshInterval = null;
2660
+ }
2661
+ }
2662
+
2663
+ async refreshTicketDetail() {
2664
+ if (!this.currentTicket) return;
2665
+ try {
2666
+ const activeId = String(this.currentTicket.id);
2667
+ const response = await fetch(`${this.apiUrl}/tickets/public/${this.currentTicket.id}`, {
2668
+ headers: { 'X-API-Key': this.apiKey },
2669
+ });
2670
+ if (!response.ok) {
2671
+ if (response.status === 404) {
2672
+ if (this.currentTicket && String(this.currentTicket.id) === activeId) {
2673
+ this.markTicketClosed('missing');
2674
+ }
2675
+ }
2676
+ return;
2677
+ }
2678
+ const updated = await response.json();
2679
+ if (!this.currentTicket || String(this.currentTicket.id) !== activeId) return;
2680
+ const previousMessages = this.currentTicket?.Messages?.length || 0;
2681
+ const previousStatus = this.currentTicket?.status;
2682
+ const newMessages = updated?.Messages?.length || 0;
2683
+ this.currentTicket = updated;
2684
+ if (Array.isArray(this.tickets)) {
2685
+ const idx = this.tickets.findIndex((ticket) => String(ticket.id) === String(updated.id));
2686
+ if (idx !== -1) {
2687
+ this.tickets[idx] = {
2688
+ ...this.tickets[idx],
2689
+ status: updated.status,
2690
+ subject: updated.subject,
2691
+ ticketNumber: updated.ticketNumber,
2692
+ };
2693
+ }
2694
+ }
2695
+ if (newMessages !== previousMessages || updated.status !== previousStatus) {
2696
+ this.renderTicketDetail(updated);
2697
+ }
2698
+ } catch (error) {
2699
+ console.error('Failed to refresh ticket:', error);
2700
+ }
2701
+ }
2702
+
2703
+ async loadSocketIoScript() {
2704
+ if (global.io && typeof global.io === 'function') return;
2705
+ await new Promise((resolve, reject) => {
2706
+ const script = document.createElement('script');
2707
+ script.src = `${this.apiOrigin}/socket.io/socket.io.js`;
2708
+ script.async = true;
2709
+ script.onload = () => resolve();
2710
+ script.onerror = () => reject(new Error('Failed to load socket.io client'));
2711
+ document.head.appendChild(script);
2712
+ });
2713
+ }
2714
+
2715
+ async connectRealtime() {
2716
+ try {
2717
+ if (!this.orgInfo) await this.loadOrgInfo();
2718
+ if (!this.orgInfo?.id) return;
2719
+ await this.loadSocketIoScript();
2720
+ if (this.socket) {
2721
+ try { this.socket.disconnect(); } catch (_) {}
2722
+ this.socket = null;
2723
+ }
2724
+ if (!global.io) return;
2725
+ this.socket = global.io(this.apiOrigin, { transports: ['websocket'], forceNew: true });
2726
+ this.socket.on('connect', () => {
2727
+ this.socketConnected = true;
2728
+ try { this.socket.emit('join-organization', this.orgInfo.id); } catch (_) {}
2729
+ this.stopAutoRefresh();
2730
+ });
2731
+ this.socket.on('disconnect', () => {
2732
+ this.socketConnected = false;
2733
+ if (this.isOpen && this.currentTicket) this.startAutoRefresh();
2734
+ });
2735
+ this.socket.on('new-message', (payload) => {
2736
+ try {
2737
+ if (this.currentTicket && String(payload.ticketId) === String(this.currentTicket.id)) {
2738
+ this.refreshTicketDetail();
2739
+ }
2740
+ } catch (_) {}
2741
+ });
2742
+ this.socket.on('ticket-status-changed', (payload) => {
2743
+ try {
2744
+ if (!this.currentTicket || !payload) return;
2745
+ if (String(payload.ticketId) !== String(this.currentTicket.id)) return;
2746
+ const newStatus = String(payload.newStatus || '').toLowerCase();
2747
+ if (payload.deleted || newStatus === 'deleted') {
2748
+ this.markTicketClosed('admin');
2749
+ return;
2750
+ }
2751
+ this.refreshTicketDetail();
2752
+ } catch (_) {}
2753
+ });
2754
+ } catch (error) {
2755
+ console.warn('Realtime connection failed; falling back to polling.', error);
2756
+ if (!this.refreshInterval && this.isOpen && this.currentTicket) this.startAutoRefresh();
2757
+ }
2758
+ }
2759
+
2760
+ disconnectRealtime() {
2761
+ try {
2762
+ if (this.socket) this.socket.disconnect();
2763
+ } catch (_) {}
2764
+ this.socket = null;
2765
+ this.socketConnected = false;
2766
+ if (this.socketRetryTimer) {
2767
+ clearTimeout(this.socketRetryTimer);
2768
+ this.socketRetryTimer = null;
2769
+ }
2770
+ }
2771
+
2772
+ openWidget() {
2773
+ if (this.isOpen) return;
2774
+ const { panel } = this.elements;
2775
+ this.isOpen = true;
2776
+ if (panel) panel.classList.add('open');
2777
+ this.ensureSystemColorListener();
2778
+ this.applyColorMode();
2779
+ this.renderTicketState();
2780
+ if (this.walletAddress) this.loadMyTickets();
2781
+ this.loadOrgInfo();
2782
+ this.connectRealtime();
2783
+ this.startAutoRefresh();
2784
+ }
2785
+
2786
+ closeWidget() {
2787
+ if (!this.isOpen) return;
2788
+ const { panel } = this.elements;
2789
+ this.isOpen = false;
2790
+ if (panel) panel.classList.remove('open');
2791
+ this.stopAutoRefresh();
2792
+ this.disconnectRealtime();
2793
+ }
2794
+
2795
+ toggleWidget() {
2796
+ if (this.isOpen) this.closeWidget(); else this.openWidget();
2797
+ }
2798
+
2799
+ openLightbox(url) {
2800
+ const lb = document.getElementById('hypercrm-lightbox');
2801
+ const img = document.getElementById('hypercrm-lightbox-img');
2802
+ if (!lb || !img) return;
2803
+ img.src = url;
2804
+ lb.classList.add('open');
2805
+ this.bindLightboxEscape();
2806
+ }
2807
+
2808
+ closeLightbox() {
2809
+ const lb = document.getElementById('hypercrm-lightbox');
2810
+ if (!lb) return;
2811
+ lb.classList.remove('open');
2812
+ this.unbindLightboxEscape();
2813
+ }
2814
+
2815
+ bindLightboxEscape() {
2816
+ if (!this._handleEsc) {
2817
+ this._handleEsc = (event) => {
2818
+ if (event.key === 'Escape') this.closeLightbox();
2819
+ };
2820
+ }
2821
+ document.addEventListener('keydown', this._handleEsc);
2822
+ }
2823
+
2824
+ unbindLightboxEscape() {
2825
+ if (this._handleEsc) {
2826
+ document.removeEventListener('keydown', this._handleEsc);
2827
+ }
2828
+ }
2829
+ escapeHtml(value) {
2830
+ if (value == null) return '';
2831
+ return String(value)
2832
+ .replace(/&/g, '&amp;')
2833
+ .replace(/</g, '&lt;')
2834
+ .replace(/>/g, '&gt;')
2835
+ .replace(/"/g, '&quot;')
2836
+ .replace(/'/g, '&#39;');
2837
+ }
2838
+
2839
+ renderIcon(src, alt, className) {
2840
+ if (!src) return '';
2841
+ const safeAlt = alt ? this.escapeHtml(alt) : '';
2842
+ const classAttr = className ? ` class="${className}"` : '';
2843
+ if (safeAlt) {
2844
+ return `<img src="${src}" alt="${safeAlt}"${classAttr} />`;
2845
+ }
2846
+ return `<img src="${src}" alt="" aria-hidden="true"${classAttr} />`;
2847
+ }
2848
+
2849
+ getSendIcon() {
2850
+ return this.renderIcon(this.iconUrls?.send, '', 'hypercrm-icon-img');
2851
+ }
2852
+
2853
+ shortenAddress(address) {
2854
+ if (!address || address.length < 10) return address || '';
2855
+ return `${address.slice(0, 6)}…${address.slice(-4)}`;
2856
+ }
2857
+
2858
+ getSenderDisplayName(message) {
2859
+ if (!message) return 'Support';
2860
+ if (message.senderType === 'customer') return 'You';
2861
+ if (message.senderType === 'system') return 'System';
2862
+ return this.orgInfo?.name || 'Support';
2863
+ }
2864
+
2865
+ getAvatarLabel(senderType) {
2866
+ if (senderType === 'customer') {
2867
+ if (this.walletAddress) return (this.walletAddress.slice(2, 4) || 'YOU').toUpperCase();
2868
+ return 'YOU';
2869
+ }
2870
+ if (senderType === 'system') return 'SYS';
2871
+ const name = (this.orgInfo?.name || 'HC').replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
2872
+ return name.slice(0, 2) || 'HC';
2873
+ }
2874
+
2875
+ renderAgentAvatar() {
2876
+ if (this.orgIconUrl) {
2877
+ return `<div class="hypercrm-avatar"><img src="${this.orgIconUrl}" alt="Agent" /></div>`;
2878
+ }
2879
+ const initials = this.getAvatarLabel('agent');
2880
+ return `<div class="hypercrm-avatar">${initials}</div>`;
2881
+ }
2882
+
2883
+ destroy() {
2884
+ try { this.disconnectRealtime(); } catch (_) {}
2885
+ this.stopAutoRefresh();
2886
+ this.removeSystemColorListener();
2887
+ document.removeEventListener('paste', this.handlePasteBound);
2888
+ try {
2889
+ this.optimisticMessages?.forEach((entry) => {
2890
+ entry?.previewUrls?.forEach((url) => URL.revokeObjectURL(url));
2891
+ });
2892
+ } catch (_) {}
2893
+ this.optimisticMessages = new Map();
2894
+ if (this.container && this.container.parentNode) {
2895
+ this.container.parentNode.removeChild(this.container);
2896
+ }
2897
+ const lb = document.getElementById('hypercrm-lightbox');
2898
+ if (lb && lb.parentNode === document.body) {
2899
+ document.body.removeChild(lb);
2900
+ }
2901
+ }
2902
+ }
2903
+
2904
+ let activeWidget = null;
2905
+ const defaultOptions = global.HyperCRM_WIDGET_OPTIONS || {};
2906
+ const shouldAutoInit = defaultOptions.autoInit !== false;
2907
+
2908
+ const api = {
2909
+ init(options) {
2910
+ const configuration = options || defaultOptions;
2911
+ if (activeWidget) {
2912
+ try { activeWidget.destroy(); } catch (_) {}
2913
+ }
2914
+ activeWidget = new HyperCRMWidget(configuration);
2915
+ return activeWidget;
2916
+ },
2917
+ destroy() {
2918
+ if (!activeWidget) return;
2919
+ try { activeWidget.destroy(); } catch (_) {}
2920
+ activeWidget = null;
2921
+ },
2922
+ setTheme(theme) {
2923
+ if (activeWidget) activeWidget.setTheme(theme);
2924
+ },
2925
+ setColorMode(mode) {
2926
+ if (activeWidget) activeWidget.setColorMode(mode);
2927
+ },
2928
+ setPosition(position) {
2929
+ if (activeWidget) activeWidget.setPosition(position);
2930
+ },
2931
+ open() {
2932
+ if (activeWidget) activeWidget.openWidget();
2933
+ },
2934
+ close() {
2935
+ if (activeWidget) activeWidget.closeWidget();
2936
+ },
2937
+ toggle() {
2938
+ if (activeWidget) activeWidget.toggleWidget();
2939
+ },
2940
+ openLightbox(url) {
2941
+ if (activeWidget) activeWidget.openLightbox(url);
2942
+ },
2943
+ closeLightbox() {
2944
+ if (activeWidget) activeWidget.closeLightbox();
2945
+ },
2946
+ };
2947
+
2948
+ global.HyperCRMWidget = api;
2949
+
2950
+ const boot = () => { api.init(defaultOptions); };
2951
+ if (shouldAutoInit) {
2952
+ if (document.readyState === 'loading') {
2953
+ document.addEventListener('DOMContentLoaded', boot);
2954
+ } else {
2955
+ boot();
2956
+ }
2957
+ }
2958
+ })(typeof window !== 'undefined' ? window : this);