@scoperat/tracker 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2633 @@
1
+ // src/widget/index.ts
2
+ import { h, render } from "preact";
3
+
4
+ // src/widget/styles.ts
5
+ function getStyles(theme = {}, darkMode = false) {
6
+ const primary = theme.primaryColor ?? "#6366f1";
7
+ const text = theme.textColor ?? (darkMode ? "#d4d4d4" : "#1f2937");
8
+ const bg = theme.backgroundColor ?? (darkMode ? "#121212" : "#ffffff");
9
+ const font = theme.fontFamily ?? '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
10
+ const surfaceBg = darkMode ? "#272727" : "#ffffff";
11
+ const subtleBorder = darkMode ? "rgba(255,255,255,0.05)" : "#f0f0f0";
12
+ const border = darkMode ? "rgba(255,255,255,0.15)" : "#d1d5db";
13
+ const borderLight = darkMode ? "rgba(255,255,255,0.05)" : "#e5e7eb";
14
+ const hoverBg = darkMode ? "#3a3a3a" : "#f9fafb";
15
+ const secondaryBg = darkMode ? "#3a3a3a" : "#f3f4f6";
16
+ const secondaryHoverBg = darkMode ? "#525252" : "#e5e7eb";
17
+ const mutedText = darkMode ? "#a3a3a3" : "#6b7280";
18
+ const subtleText = darkMode ? "#a3a3a3" : "#4b5563";
19
+ const labelText = darkMode ? "#d4d4d4" : "#374151";
20
+ const inputBg = darkMode ? "#272727" : "white";
21
+ const panelShadow = darkMode ? "0 8px 30px rgba(0,0,0,0.5)" : "0 8px 30px rgba(0,0,0,0.12)";
22
+ const panelBorder = darkMode ? "1px solid rgba(255,255,255,0.05)" : "1px solid rgba(0,0,0,0.08)";
23
+ const inlineCodeBg = darkMode ? "#3a3a3a" : "#f3f4f6";
24
+ const preCodeBg = darkMode ? "#1a1a1a" : "#1f2937";
25
+ const preCodeText = darkMode ? "#d4d4d4" : "#e5e7eb";
26
+ const errorBg = darkMode ? "#3b1111" : "#fef2f2";
27
+ const errorText = darkMode ? "#fca5a5" : "#dc2626";
28
+ const successBg = darkMode ? "#052e16" : "#f0fdf4";
29
+ const successText = darkMode ? "#4ade80" : "#16a34a";
30
+ const blockquoteColor = darkMode ? "#a3a3a3" : "#6b7280";
31
+ const tableBg = darkMode ? "#272727" : "#f9fafb";
32
+ return `
33
+ :host {
34
+ all: initial;
35
+ font-family: ${font};
36
+ color: ${text};
37
+ font-size: 14px;
38
+ line-height: 1.5;
39
+ }
40
+
41
+ * {
42
+ box-sizing: border-box;
43
+ margin: 0;
44
+ padding: 0;
45
+ }
46
+
47
+ .sr-launcher {
48
+ position: fixed;
49
+ bottom: 20px;
50
+ z-index: 2147483647;
51
+ width: 44px;
52
+ height: 44px;
53
+ border-radius: 50%;
54
+ background: ${primary};
55
+ color: white;
56
+ border: none;
57
+ cursor: pointer;
58
+ display: flex;
59
+ align-items: center;
60
+ justify-content: center;
61
+ box-shadow: 0 8px 30px rgba(0,0,0,0.35), 0 4px 12px rgba(0,0,0,0.25), 0 0 0 1px rgba(0,0,0,0.05);
62
+ transition: transform 0.2s, box-shadow 0.2s;
63
+ }
64
+ .sr-launcher:hover {
65
+ transform: scale(1.05);
66
+ box-shadow: 0 12px 40px rgba(0,0,0,0.45), 0 6px 16px rgba(0,0,0,0.3), 0 0 0 1px rgba(0,0,0,0.05);
67
+ }
68
+ .sr-launcher.right { right: 20px; }
69
+ .sr-launcher.left { left: 20px; }
70
+
71
+ .sr-launcher svg {
72
+ width: 20px;
73
+ height: 20px;
74
+ fill: currentColor;
75
+ }
76
+
77
+ .sr-panel {
78
+ position: fixed;
79
+ bottom: 88px;
80
+ z-index: 2147483647;
81
+ width: 380px;
82
+ height: 520px;
83
+ max-height: calc(100vh - 120px);
84
+ background: ${bg};
85
+ border-radius: 16px;
86
+ box-shadow: ${panelShadow};
87
+ border: ${panelBorder};
88
+ display: flex;
89
+ flex-direction: column;
90
+ overflow: hidden;
91
+ animation: sr-slide-up 0.2s ease-out;
92
+ }
93
+ .sr-panel.right { right: 20px; }
94
+ .sr-panel.left { left: 20px; }
95
+
96
+ @keyframes sr-slide-up {
97
+ from { opacity: 0; transform: translateY(12px); }
98
+ to { opacity: 1; transform: translateY(0); }
99
+ }
100
+
101
+ .sr-header {
102
+ padding: 16px 20px;
103
+ background: ${primary};
104
+ color: white;
105
+ display: flex;
106
+ align-items: center;
107
+ justify-content: space-between;
108
+ flex-shrink: 0;
109
+ border-radius: 16px 16px 0 0;
110
+ margin: -1px -1px 0 -1px;
111
+ }
112
+ .sr-header h3 {
113
+ font-size: 16px;
114
+ font-weight: 600;
115
+ margin: 0;
116
+ flex: 1;
117
+ min-width: 0;
118
+ }
119
+ .sr-header-title-truncate {
120
+ overflow: hidden;
121
+ text-overflow: ellipsis;
122
+ white-space: nowrap;
123
+ }
124
+ .sr-header-actions {
125
+ display: flex;
126
+ gap: 8px;
127
+ align-items: center;
128
+ }
129
+ .sr-header button {
130
+ background: none;
131
+ border: none;
132
+ color: white;
133
+ cursor: pointer;
134
+ padding: 4px;
135
+ border-radius: 4px;
136
+ display: flex;
137
+ align-items: center;
138
+ opacity: 0.8;
139
+ }
140
+ .sr-header button:hover { opacity: 1; }
141
+ .sr-header button svg { width: 18px; height: 18px; fill: currentColor; }
142
+
143
+ .sr-body {
144
+ flex: 1;
145
+ overflow-y: auto;
146
+ padding: 0;
147
+ min-height: 0;
148
+ display: flex;
149
+ flex-direction: column;
150
+ }
151
+
152
+ .sr-ticket-list {
153
+ list-style: none;
154
+ padding: 0;
155
+ }
156
+ .sr-ticket-item {
157
+ padding: 14px 20px;
158
+ border-bottom: 1px solid ${subtleBorder};
159
+ cursor: pointer;
160
+ transition: background 0.15s;
161
+ }
162
+ .sr-ticket-item:hover { background: ${hoverBg}; }
163
+ .sr-ticket-item h4 {
164
+ font-size: 14px;
165
+ font-weight: 500;
166
+ margin-bottom: 4px;
167
+ color: ${text};
168
+ }
169
+ .sr-ticket-item .sr-meta {
170
+ font-size: 12px;
171
+ color: ${mutedText};
172
+ display: flex;
173
+ gap: 8px;
174
+ align-items: center;
175
+ }
176
+
177
+ .sr-badge {
178
+ font-size: 11px;
179
+ padding: 2px 8px;
180
+ border-radius: 9999px;
181
+ font-weight: 500;
182
+ }
183
+ .sr-badge-bug { background: ${darkMode ? "#3b1111" : "#fef2f2"}; color: ${darkMode ? "#fca5a5" : "#dc2626"}; }
184
+ .sr-badge-feature_request { background: ${darkMode ? "#052e16" : "#f0fdf4"}; color: ${darkMode ? "#4ade80" : "#16a34a"}; }
185
+ .sr-badge-question { background: ${darkMode ? "#1e2a4a" : "#eff6ff"}; color: ${darkMode ? "#93c5fd" : "#2563eb"}; }
186
+ .sr-badge-access_issue { background: ${darkMode ? "#3b2506" : "#fefce8"}; color: ${darkMode ? "#fcd34d" : "#ca8a04"}; }
187
+ .sr-badge-performance { background: ${darkMode ? "#2e0a4a" : "#fdf4ff"}; color: ${darkMode ? "#c084fc" : "#9333ea"}; }
188
+ .sr-badge-billing { background: ${darkMode ? "#042f2e" : "#f0fdfa"}; color: ${darkMode ? "#5eead4" : "#0d9488"}; }
189
+ .sr-badge-documentation { background: ${secondaryBg}; color: ${mutedText}; }
190
+ .sr-badge-other { background: ${secondaryBg}; color: ${mutedText}; }
191
+
192
+ .sr-empty {
193
+ padding: 40px 20px;
194
+ text-align: center;
195
+ color: ${mutedText};
196
+ }
197
+ .sr-empty p { margin-bottom: 16px; font-size: 14px; }
198
+
199
+ .sr-btn {
200
+ display: inline-flex;
201
+ align-items: center;
202
+ justify-content: center;
203
+ padding: 8px 16px;
204
+ font-size: 14px;
205
+ font-weight: 500;
206
+ border: none;
207
+ border-radius: 8px;
208
+ cursor: pointer;
209
+ transition: background 0.15s;
210
+ }
211
+ .sr-btn-primary {
212
+ background: ${primary};
213
+ color: white;
214
+ }
215
+ .sr-btn-primary:hover { filter: brightness(1.1); }
216
+ .sr-btn-primary:disabled {
217
+ opacity: 0.5;
218
+ cursor: not-allowed;
219
+ }
220
+ .sr-btn-secondary {
221
+ background: ${secondaryBg};
222
+ color: ${text};
223
+ }
224
+ .sr-btn-secondary:hover { background: ${secondaryHoverBg}; }
225
+
226
+ .sr-form {
227
+ padding: 16px 20px;
228
+ display: flex;
229
+ flex-direction: column;
230
+ gap: 12px;
231
+ flex: 1;
232
+ min-height: 0;
233
+ }
234
+
235
+ .sr-field-grow {
236
+ flex: 1;
237
+ display: flex;
238
+ flex-direction: column;
239
+ min-height: 0;
240
+ }
241
+
242
+ .sr-field label {
243
+ display: block;
244
+ font-size: 13px;
245
+ font-weight: 500;
246
+ margin-bottom: 4px;
247
+ color: ${labelText};
248
+ }
249
+ .sr-field input,
250
+ .sr-field textarea,
251
+ .sr-field select {
252
+ width: 100%;
253
+ padding: 8px 12px;
254
+ border: none;
255
+ border-radius: 8px;
256
+ font-size: 14px;
257
+ font-family: inherit;
258
+ color: ${text};
259
+ background: ${inputBg};
260
+ outline: none;
261
+ }
262
+ .sr-field input:focus,
263
+ .sr-field textarea:focus,
264
+ .sr-field select:focus {
265
+ box-shadow: none;
266
+ }
267
+ .sr-field textarea {
268
+ resize: none;
269
+ min-height: 160px;
270
+ padding-bottom: 44px;
271
+ }
272
+
273
+ .sr-textarea-wrap {
274
+ position: relative;
275
+ flex: 1;
276
+ display: flex;
277
+ flex-direction: column;
278
+ }
279
+ .sr-textarea-wrap textarea {
280
+ flex: 1;
281
+ }
282
+ .sr-send-btn {
283
+ width: 32px;
284
+ height: 32px;
285
+ border-radius: 50%;
286
+ background: ${darkMode ? "white" : primary};
287
+ color: ${darkMode ? "#121212" : "white"};
288
+ border: none;
289
+ cursor: pointer;
290
+ display: flex;
291
+ align-items: center;
292
+ justify-content: center;
293
+ padding: 0;
294
+ transition: opacity 0.15s;
295
+ }
296
+ .sr-send-btn:disabled {
297
+ opacity: 0.3;
298
+ cursor: not-allowed;
299
+ }
300
+ .sr-send-btn svg {
301
+ width: 16px;
302
+ height: 16px;
303
+ fill: currentColor;
304
+ }
305
+
306
+ .sr-categories {
307
+ display: flex;
308
+ flex-wrap: wrap;
309
+ gap: 6px;
310
+ }
311
+ .sr-category-btn {
312
+ padding: 6px 12px;
313
+ border: 1px solid ${border};
314
+ border-radius: 9999px;
315
+ font-size: 12px;
316
+ font-weight: 500;
317
+ background: ${inputBg};
318
+ cursor: pointer;
319
+ transition: all 0.15s;
320
+ color: ${subtleText};
321
+ }
322
+ .sr-category-btn.active {
323
+ border-color: ${primary};
324
+ background: ${primary}11;
325
+ color: ${primary};
326
+ }
327
+ .sr-category-btn:hover { border-color: ${primary}; }
328
+
329
+ .sr-thread {
330
+ display: flex;
331
+ flex-direction: column;
332
+ min-height: 0;
333
+ height: 100%;
334
+ }
335
+ .sr-thread-messages {
336
+ flex: 1 1 0;
337
+ min-height: 0;
338
+ overflow-y: auto;
339
+ padding: 16px 20px;
340
+ display: flex;
341
+ flex-direction: column;
342
+ gap: 0;
343
+ }
344
+ .sr-message-row {
345
+ display: flex;
346
+ flex-direction: column;
347
+ gap: 6px;
348
+ max-width: 75%;
349
+ }
350
+ .sr-message-row + .sr-message-row {
351
+ margin-top: 20px;
352
+ }
353
+ .sr-message-row-user {
354
+ align-self: flex-end;
355
+ align-items: flex-end;
356
+ }
357
+ .sr-message-row-agent {
358
+ align-self: flex-start;
359
+ align-items: flex-start;
360
+ }
361
+ .sr-message {
362
+ padding: 12px 16px;
363
+ background: none;
364
+ border: 1px solid ${borderLight};
365
+ border-radius: 8px;
366
+ font-size: 14px;
367
+ line-height: 1.5;
368
+ word-break: break-word;
369
+ color: ${text};
370
+ }
371
+ .sr-message-meta {
372
+ display: flex;
373
+ align-items: center;
374
+ gap: 6px;
375
+ font-size: 11px;
376
+ color: ${mutedText};
377
+ }
378
+ .sr-message-sender {
379
+ font-weight: 600;
380
+ color: ${mutedText};
381
+ }
382
+ .sr-avatar {
383
+ width: 18px;
384
+ height: 18px;
385
+ border-radius: 4px;
386
+ background: ${primary};
387
+ color: white;
388
+ font-size: 10px;
389
+ font-weight: 600;
390
+ display: flex;
391
+ align-items: center;
392
+ justify-content: center;
393
+ flex-shrink: 0;
394
+ overflow: hidden;
395
+ }
396
+ .sr-avatar img {
397
+ width: 100%;
398
+ height: 100%;
399
+ object-fit: cover;
400
+ }
401
+
402
+ .sr-original-desc {
403
+ padding: 12px 16px;
404
+ background: none;
405
+ border-radius: 8px;
406
+ border: 1px solid ${borderLight};
407
+ font-size: 14px;
408
+ color: ${text};
409
+ white-space: pre-wrap;
410
+ word-break: break-word;
411
+ max-width: 75%;
412
+ margin-bottom: 0;
413
+ }
414
+ .sr-original-desc .sr-desc-label {
415
+ font-size: 11px;
416
+ font-weight: 600;
417
+ color: ${mutedText};
418
+ text-transform: uppercase;
419
+ margin-bottom: 6px;
420
+ }
421
+
422
+ .sr-composer {
423
+ padding: 12px 20px;
424
+ border-top: 1px solid ${subtleBorder};
425
+ flex-shrink: 0;
426
+ }
427
+ .sr-composer-wrap {
428
+ position: relative;
429
+ }
430
+ .sr-composer textarea {
431
+ width: 100%;
432
+ padding: 10px 8px;
433
+ padding-right: 8px;
434
+ padding-bottom: 40px;
435
+ border: 1px solid ${borderLight};
436
+ border-radius: 10px;
437
+ font-size: 14px;
438
+ font-family: inherit;
439
+ resize: none;
440
+ min-height: 80px;
441
+ max-height: 120px;
442
+ outline: none;
443
+ color: ${text};
444
+ background: ${darkMode ? "#1a1a1a" : inputBg};
445
+ }
446
+ .sr-composer textarea:focus {
447
+ border-color: ${borderLight};
448
+ box-shadow: none;
449
+ }
450
+ .sr-composer .sr-send-btn {
451
+ width: 26px;
452
+ height: 26px;
453
+ }
454
+ .sr-composer .sr-send-btn svg {
455
+ width: 13px;
456
+ height: 13px;
457
+ }
458
+
459
+ .sr-loading {
460
+ padding: 40px 20px;
461
+ text-align: center;
462
+ color: ${mutedText};
463
+ }
464
+
465
+ .sr-error {
466
+ padding: 12px 20px;
467
+ background: ${errorBg};
468
+ color: ${errorText};
469
+ font-size: 13px;
470
+ text-align: center;
471
+ }
472
+
473
+ .sr-success {
474
+ padding: 40px 20px;
475
+ text-align: center;
476
+ }
477
+ .sr-success-icon {
478
+ width: 48px;
479
+ height: 48px;
480
+ margin: 0 auto 12px;
481
+ background: ${successBg};
482
+ border-radius: 50%;
483
+ display: flex;
484
+ align-items: center;
485
+ justify-content: center;
486
+ }
487
+ .sr-success-icon svg { width: 24px; height: 24px; fill: ${successText}; }
488
+ .sr-success h4 {
489
+ font-size: 16px;
490
+ font-weight: 600;
491
+ margin-bottom: 4px;
492
+ color: ${text};
493
+ }
494
+ .sr-success p {
495
+ font-size: 14px;
496
+ color: ${mutedText};
497
+ }
498
+
499
+ .sr-form-actions {
500
+ display: flex;
501
+ gap: 8px;
502
+ padding-top: 4px;
503
+ }
504
+ .sr-form-actions .sr-btn { flex: 1; }
505
+
506
+ .sr-new-btn-wrap {
507
+ padding: 12px 20px;
508
+ border-bottom: 1px solid ${subtleBorder};
509
+ }
510
+ .sr-new-btn-wrap .sr-btn { width: 100%; }
511
+
512
+ .sr-suggested-articles {
513
+ margin-top: 12px;
514
+ display: flex;
515
+ flex-direction: column;
516
+ gap: 6px;
517
+ }
518
+ .sr-suggested-label {
519
+ display: flex;
520
+ align-items: center;
521
+ gap: 6px;
522
+ font-size: 11px;
523
+ font-weight: 600;
524
+ color: ${mutedText};
525
+ text-transform: uppercase;
526
+ letter-spacing: 0.03em;
527
+ margin-bottom: 2px;
528
+ }
529
+ .sr-suggested-label svg {
530
+ width: 14px;
531
+ height: 14px;
532
+ stroke: ${primary};
533
+ fill: none;
534
+ }
535
+ .sr-article-card {
536
+ border: 1px solid ${borderLight};
537
+ border-radius: 8px;
538
+ overflow: hidden;
539
+ background: ${surfaceBg};
540
+ }
541
+ .sr-article-btn {
542
+ display: flex;
543
+ align-items: center;
544
+ gap: 8px;
545
+ width: 100%;
546
+ padding: 10px 12px;
547
+ background: none;
548
+ border: none;
549
+ cursor: pointer;
550
+ text-align: left;
551
+ font-size: 13px;
552
+ font-family: inherit;
553
+ color: ${text};
554
+ transition: background 0.15s;
555
+ }
556
+ .sr-article-btn:hover { background: ${hoverBg}; }
557
+ .sr-article-icon {
558
+ width: 16px;
559
+ height: 16px;
560
+ stroke: ${primary};
561
+ fill: none;
562
+ flex-shrink: 0;
563
+ }
564
+ .sr-article-title {
565
+ flex: 1;
566
+ min-width: 0;
567
+ font-weight: 500;
568
+ overflow: hidden;
569
+ text-overflow: ellipsis;
570
+ white-space: nowrap;
571
+ }
572
+ .sr-article-chevron {
573
+ width: 14px;
574
+ height: 14px;
575
+ flex-shrink: 0;
576
+ stroke: ${mutedText};
577
+ fill: none;
578
+ transition: transform 0.2s;
579
+ }
580
+ .sr-article-chevron.sr-expanded { transform: rotate(180deg); }
581
+ .sr-article-loading {
582
+ padding: 8px 12px;
583
+ font-size: 12px;
584
+ color: ${mutedText};
585
+ }
586
+ .sr-article-content {
587
+ padding: 12px;
588
+ font-size: 13px;
589
+ line-height: 1.6;
590
+ color: ${labelText};
591
+ word-break: break-word;
592
+ border-top: 1px solid ${subtleBorder};
593
+ max-height: 300px;
594
+ overflow-y: auto;
595
+ }
596
+
597
+ .sr-link-preview {
598
+ display: flex;
599
+ margin-top: 8px;
600
+ max-width: 420px;
601
+ border: 1px solid ${borderLight};
602
+ border-radius: 8px;
603
+ overflow: hidden;
604
+ background: ${surfaceBg};
605
+ text-decoration: none;
606
+ color: inherit;
607
+ transition: border-color 0.15s;
608
+ }
609
+ .sr-link-preview:hover { border-color: ${mutedText}; }
610
+ .sr-link-preview-img {
611
+ flex-shrink: 0;
612
+ width: 96px;
613
+ border-right: 1px solid ${subtleBorder};
614
+ }
615
+ .sr-link-preview-img img {
616
+ width: 100%;
617
+ height: 100%;
618
+ object-fit: cover;
619
+ display: block;
620
+ }
621
+ .sr-link-preview-body {
622
+ flex: 1;
623
+ min-width: 0;
624
+ display: flex;
625
+ flex-direction: column;
626
+ gap: 3px;
627
+ padding: 8px 10px;
628
+ }
629
+ .sr-link-preview-site {
630
+ display: flex;
631
+ align-items: center;
632
+ gap: 5px;
633
+ font-size: 10.5px;
634
+ color: ${mutedText};
635
+ }
636
+ .sr-link-preview-site span {
637
+ overflow: hidden;
638
+ text-overflow: ellipsis;
639
+ white-space: nowrap;
640
+ }
641
+ .sr-link-preview-favicon {
642
+ width: 12px;
643
+ height: 12px;
644
+ border-radius: 2px;
645
+ flex-shrink: 0;
646
+ }
647
+ .sr-link-preview-title {
648
+ margin: 0;
649
+ font-size: 12.5px;
650
+ font-weight: 600;
651
+ color: ${text};
652
+ overflow: hidden;
653
+ text-overflow: ellipsis;
654
+ white-space: nowrap;
655
+ line-height: 1.3;
656
+ }
657
+ .sr-link-preview-desc {
658
+ margin: 0;
659
+ font-size: 11.5px;
660
+ color: ${mutedText};
661
+ line-height: 1.4;
662
+ display: -webkit-box;
663
+ -webkit-line-clamp: 2;
664
+ -webkit-box-orient: vertical;
665
+ overflow: hidden;
666
+ }
667
+
668
+ .sr-prose h1, .sr-prose h2, .sr-prose h3,
669
+ .sr-prose h4, .sr-prose h5, .sr-prose h6 {
670
+ margin-top: 1em;
671
+ margin-bottom: 0.5em;
672
+ font-weight: 600;
673
+ line-height: 1.3;
674
+ color: ${text};
675
+ }
676
+ .sr-prose h1 { font-size: 1.35em; }
677
+ .sr-prose h2 { font-size: 1.2em; }
678
+ .sr-prose h3 { font-size: 1.1em; }
679
+ .sr-prose h4, .sr-prose h5, .sr-prose h6 { font-size: 1em; }
680
+ .sr-prose h1:first-child, .sr-prose h2:first-child,
681
+ .sr-prose h3:first-child { margin-top: 0; }
682
+
683
+ .sr-prose p { margin: 0.5em 0; }
684
+ .sr-prose p:first-child { margin-top: 0; }
685
+ .sr-prose p:last-child { margin-bottom: 0; }
686
+
687
+ .sr-prose ul, .sr-prose ol {
688
+ margin: 0.5em 0;
689
+ padding-left: 1.5em;
690
+ }
691
+ .sr-prose li { margin: 0.2em 0; }
692
+ .sr-prose li > p { margin: 0; }
693
+
694
+ .sr-prose a {
695
+ color: ${primary};
696
+ text-decoration: underline;
697
+ text-underline-offset: 2px;
698
+ }
699
+ .sr-prose a:hover { text-decoration-thickness: 2px; }
700
+
701
+ .sr-prose strong { font-weight: 600; }
702
+ .sr-prose em { font-style: italic; }
703
+
704
+ .sr-prose code {
705
+ background: ${inlineCodeBg};
706
+ padding: 0.15em 0.35em;
707
+ border-radius: 4px;
708
+ font-size: 0.9em;
709
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
710
+ }
711
+ .sr-prose pre {
712
+ background: ${preCodeBg};
713
+ color: ${preCodeText};
714
+ padding: 12px;
715
+ border-radius: 6px;
716
+ overflow-x: auto;
717
+ margin: 0.75em 0;
718
+ font-size: 0.85em;
719
+ line-height: 1.5;
720
+ }
721
+ .sr-prose pre code {
722
+ background: none;
723
+ padding: 0;
724
+ border-radius: 0;
725
+ font-size: inherit;
726
+ color: inherit;
727
+ }
728
+
729
+ .sr-prose blockquote {
730
+ border-left: 3px solid ${border};
731
+ padding-left: 12px;
732
+ margin: 0.75em 0;
733
+ color: ${blockquoteColor};
734
+ }
735
+
736
+ .sr-prose hr {
737
+ border: none;
738
+ border-top: 1px solid ${borderLight};
739
+ margin: 1em 0;
740
+ }
741
+
742
+ .sr-prose img {
743
+ max-width: 100%;
744
+ height: auto;
745
+ border-radius: 6px;
746
+ margin: 0.5em 0;
747
+ }
748
+
749
+ .sr-prose table {
750
+ width: 100%;
751
+ border-collapse: collapse;
752
+ margin: 0.75em 0;
753
+ font-size: 0.9em;
754
+ }
755
+ .sr-prose th, .sr-prose td {
756
+ border: 1px solid ${borderLight};
757
+ padding: 6px 10px;
758
+ text-align: left;
759
+ }
760
+ .sr-prose th {
761
+ background: ${tableBg};
762
+ font-weight: 600;
763
+ }
764
+
765
+ .sr-attach-btn {
766
+ width: 28px;
767
+ height: 28px;
768
+ border-radius: 50%;
769
+ background: none;
770
+ color: ${mutedText};
771
+ border: none;
772
+ cursor: pointer;
773
+ display: flex;
774
+ align-items: center;
775
+ justify-content: center;
776
+ padding: 0;
777
+ transition: color 0.15s;
778
+ }
779
+ .sr-attach-btn:hover { color: ${text}; }
780
+ .sr-attach-btn:disabled { opacity: 0.3; cursor: not-allowed; }
781
+ .sr-attach-btn svg { width: 15px; height: 15px; }
782
+ .sr-attach-btn-left {
783
+ position: absolute;
784
+ bottom: 8px;
785
+ left: 8px;
786
+ }
787
+ .sr-send-btn-abs {
788
+ position: absolute;
789
+ bottom: 8px;
790
+ right: 8px;
791
+ }
792
+
793
+ .sr-attachment-pills {
794
+ display: flex;
795
+ flex-wrap: wrap;
796
+ gap: 6px;
797
+ padding: 4px 0 8px;
798
+ }
799
+ .sr-attachment-pill {
800
+ display: inline-flex;
801
+ align-items: center;
802
+ gap: 4px;
803
+ padding: 3px 8px;
804
+ border-radius: 9999px;
805
+ background: ${secondaryBg};
806
+ font-size: 11px;
807
+ color: ${mutedText};
808
+ max-width: 200px;
809
+ }
810
+ .sr-attachment-pill-icon {
811
+ width: 11px;
812
+ height: 11px;
813
+ flex-shrink: 0;
814
+ }
815
+ .sr-attachment-pill-name {
816
+ overflow: hidden;
817
+ text-overflow: ellipsis;
818
+ white-space: nowrap;
819
+ }
820
+ .sr-attachment-pill-remove {
821
+ width: 14px;
822
+ height: 14px;
823
+ border-radius: 50%;
824
+ background: none;
825
+ border: none;
826
+ cursor: pointer;
827
+ display: flex;
828
+ align-items: center;
829
+ justify-content: center;
830
+ padding: 0;
831
+ color: ${mutedText};
832
+ flex-shrink: 0;
833
+ }
834
+ .sr-attachment-pill-remove:hover { color: ${text}; }
835
+ .sr-attachment-pill-remove svg { width: 10px; height: 10px; }
836
+
837
+ .sr-message-attachments {
838
+ display: flex;
839
+ flex-direction: column;
840
+ gap: 6px;
841
+ margin-top: 6px;
842
+ }
843
+
844
+ .sr-file-preview {
845
+ border: 1px solid ${borderLight};
846
+ border-radius: 8px;
847
+ overflow: hidden;
848
+ max-width: 280px;
849
+ }
850
+ .sr-file-preview-image {
851
+ display: block;
852
+ border-bottom: 1px solid ${borderLight};
853
+ }
854
+ .sr-file-preview-image img {
855
+ display: block;
856
+ max-height: 180px;
857
+ width: 100%;
858
+ object-fit: contain;
859
+ background: ${darkMode ? "#1a1a1a" : "#f9fafb"};
860
+ }
861
+ .sr-file-preview-image:hover img { opacity: 0.9; }
862
+
863
+ .sr-file-preview-row {
864
+ display: flex;
865
+ align-items: center;
866
+ gap: 8px;
867
+ padding: 8px 10px;
868
+ }
869
+ .sr-file-preview-icon {
870
+ width: 16px;
871
+ height: 16px;
872
+ flex-shrink: 0;
873
+ color: ${mutedText};
874
+ }
875
+ .sr-file-preview-info {
876
+ min-width: 0;
877
+ flex: 1;
878
+ }
879
+ .sr-file-preview-name {
880
+ display: block;
881
+ font-size: 12px;
882
+ font-weight: 500;
883
+ color: ${primary};
884
+ overflow: hidden;
885
+ text-overflow: ellipsis;
886
+ white-space: nowrap;
887
+ text-decoration: none;
888
+ }
889
+ .sr-file-preview-name:hover { text-decoration: underline; }
890
+ span.sr-file-preview-name { color: ${text}; }
891
+ span.sr-file-preview-name:hover { text-decoration: none; }
892
+ .sr-file-preview-size {
893
+ font-size: 11px;
894
+ color: ${mutedText};
895
+ }
896
+ .sr-file-preview-download {
897
+ flex-shrink: 0;
898
+ padding: 4px;
899
+ border-radius: 4px;
900
+ color: ${mutedText};
901
+ transition: color 0.15s, background 0.15s;
902
+ }
903
+ .sr-file-preview-download:hover {
904
+ color: ${text};
905
+ background: ${hoverBg};
906
+ }
907
+ .sr-file-preview-download svg {
908
+ width: 14px;
909
+ height: 14px;
910
+ }
911
+
912
+ ::-webkit-scrollbar {
913
+ width: 13px;
914
+ height: 13px;
915
+ }
916
+ ::-webkit-scrollbar-track {
917
+ background: transparent;
918
+ }
919
+ ::-webkit-scrollbar-thumb {
920
+ background: ${darkMode ? "#525252" : "oklch(0.8 0 0)"};
921
+ border: 3px solid transparent;
922
+ border-radius: 7px;
923
+ background-clip: padding-box;
924
+ }
925
+ ::-webkit-scrollbar-thumb:hover {
926
+ background: ${darkMode ? "#666666" : "oklch(0.65 0 0)"};
927
+ border: 3px solid transparent;
928
+ background-clip: padding-box;
929
+ }
930
+ `;
931
+ }
932
+
933
+ // src/widget/Widget.tsx
934
+ import { useEffect as useEffect3, useState as useState3 } from "preact/hooks";
935
+
936
+ // src/widget/ConversationList.tsx
937
+ import { useEffect } from "preact/hooks";
938
+
939
+ // src/breadcrumbs.ts
940
+ var MAX_BREADCRUMBS = 20;
941
+ var MAX_TEXT_LENGTH = 120;
942
+ var buffer = [];
943
+ var installed = false;
944
+ function push(crumb) {
945
+ buffer.push(crumb);
946
+ if (buffer.length > MAX_BREADCRUMBS) {
947
+ buffer = buffer.slice(-MAX_BREADCRUMBS);
948
+ }
949
+ }
950
+ function truncate(str, max = MAX_TEXT_LENGTH) {
951
+ return str.length > max ? str.slice(0, max) + "\u2026" : str;
952
+ }
953
+ function getBreadcrumbs() {
954
+ return [...buffer];
955
+ }
956
+ function addBreadcrumb(crumb) {
957
+ push(crumb);
958
+ }
959
+ function startBreadcrumbCollection() {
960
+ if (installed || typeof window === "undefined") return;
961
+ installed = true;
962
+ document.addEventListener(
963
+ "click",
964
+ (e) => {
965
+ const target = e.target;
966
+ if (!target) return;
967
+ const text = target.textContent?.trim() || target.getAttribute("aria-label") || "";
968
+ push({
969
+ type: "user",
970
+ category: "click",
971
+ message: truncate(
972
+ `${target.tagName.toLowerCase()}${text ? ` "${text}"` : ""}`
973
+ ),
974
+ timestamp: Date.now(),
975
+ data: {
976
+ tagName: target.tagName.toLowerCase(),
977
+ className: target.className ? truncate(target.className, 80) : void 0
978
+ }
979
+ });
980
+ },
981
+ { capture: true }
982
+ );
983
+ const origPushState = history.pushState;
984
+ history.pushState = function(...args) {
985
+ const from = location.pathname;
986
+ origPushState.apply(this, args);
987
+ push({
988
+ type: "navigation",
989
+ category: "navigation",
990
+ message: `${from} \u2192 ${location.pathname}`,
991
+ timestamp: Date.now()
992
+ });
993
+ };
994
+ window.addEventListener("popstate", () => {
995
+ push({
996
+ type: "navigation",
997
+ category: "navigation",
998
+ message: `popstate \u2192 ${location.pathname}`,
999
+ timestamp: Date.now()
1000
+ });
1001
+ });
1002
+ const wrapConsole = (level) => {
1003
+ const orig = console[level];
1004
+ console[level] = (...args) => {
1005
+ push({
1006
+ type: "console",
1007
+ category: `console.${level}`,
1008
+ message: truncate(
1009
+ args.map((a) => typeof a === "string" ? a : String(a)).join(" ")
1010
+ ),
1011
+ timestamp: Date.now()
1012
+ });
1013
+ orig.apply(console, args);
1014
+ };
1015
+ };
1016
+ wrapConsole("error");
1017
+ wrapConsole("warn");
1018
+ }
1019
+
1020
+ // src/context.ts
1021
+ function getBrowserContext() {
1022
+ if (typeof window === "undefined") {
1023
+ return {};
1024
+ }
1025
+ return {
1026
+ page: {
1027
+ url: window.location.href,
1028
+ path: window.location.pathname,
1029
+ title: document.title,
1030
+ referrer: document.referrer || void 0
1031
+ },
1032
+ screen: {
1033
+ width: window.screen.width,
1034
+ height: window.screen.height
1035
+ },
1036
+ locale: navigator.language,
1037
+ userAgent: navigator.userAgent,
1038
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
1039
+ };
1040
+ }
1041
+
1042
+ // src/errors.ts
1043
+ var DEDUP_COOLDOWN_MS = 6e4;
1044
+ var dedupMap = /* @__PURE__ */ new Map();
1045
+ function simpleFingerprint(type, stack) {
1046
+ const firstFrame = stack?.split("\n")[1]?.trim() ?? "";
1047
+ return `${type}:${firstFrame}`;
1048
+ }
1049
+ function isDuplicate(fingerprint) {
1050
+ const lastSent = dedupMap.get(fingerprint);
1051
+ if (lastSent && Date.now() - lastSent < DEDUP_COOLDOWN_MS) return true;
1052
+ dedupMap.set(fingerprint, Date.now());
1053
+ return false;
1054
+ }
1055
+ function sendException(tracker, props, getBreadcrumbs2) {
1056
+ const fp = simpleFingerprint(props.type, props.stack);
1057
+ if (isDuplicate(fp)) return;
1058
+ tracker.track("$exception", {
1059
+ ...props,
1060
+ context: { breadcrumbs: getBreadcrumbs2() }
1061
+ });
1062
+ }
1063
+ function installGlobalHandlers(tracker, getBreadcrumbs2) {
1064
+ if (typeof window === "undefined") return;
1065
+ window.onerror = (_message, filename, lineno, colno, error) => {
1066
+ const err = error ?? new Error(String(_message));
1067
+ sendException(
1068
+ tracker,
1069
+ {
1070
+ type: err.name || "Error",
1071
+ message: err.message || String(_message),
1072
+ stack: err.stack,
1073
+ handled: false,
1074
+ mechanism: "onerror",
1075
+ source: "browser",
1076
+ filename,
1077
+ lineno,
1078
+ colno
1079
+ },
1080
+ getBreadcrumbs2
1081
+ );
1082
+ };
1083
+ window.onunhandledrejection = (event) => {
1084
+ const reason = event.reason;
1085
+ const err = reason instanceof Error ? reason : new Error(String(reason));
1086
+ sendException(
1087
+ tracker,
1088
+ {
1089
+ type: err.name || "UnhandledRejection",
1090
+ message: err.message,
1091
+ stack: err.stack,
1092
+ handled: false,
1093
+ mechanism: "onunhandledrejection",
1094
+ source: "browser"
1095
+ },
1096
+ getBreadcrumbs2
1097
+ );
1098
+ };
1099
+ }
1100
+
1101
+ // src/flag-client.ts
1102
+ var DEFAULT_FLAG_POLL_INTERVAL = 6e4;
1103
+ var FlagClient = class {
1104
+ endpoint;
1105
+ writeKey;
1106
+ timeout;
1107
+ environment;
1108
+ getIdentity;
1109
+ onEvaluation;
1110
+ cache = /* @__PURE__ */ new Map();
1111
+ ready = false;
1112
+ readyCallbacks = [];
1113
+ changeListeners = /* @__PURE__ */ new Set();
1114
+ evalTracked = /* @__PURE__ */ new Set();
1115
+ timer = null;
1116
+ constructor(config) {
1117
+ this.endpoint = config.endpoint;
1118
+ this.writeKey = config.writeKey;
1119
+ this.timeout = config.timeout ?? 1e4;
1120
+ this.environment = config.environment;
1121
+ this.getIdentity = config.getIdentity;
1122
+ this.onEvaluation = config.onEvaluation;
1123
+ const interval = config.pollInterval ?? DEFAULT_FLAG_POLL_INTERVAL;
1124
+ this.fetch();
1125
+ this.timer = setInterval(() => this.fetch(), interval);
1126
+ }
1127
+ getFlag(key, defaultValue) {
1128
+ const flag = this.cache.get(key);
1129
+ if (!flag) return defaultValue;
1130
+ if (!this.evalTracked.has(key)) {
1131
+ this.evalTracked.add(key);
1132
+ this.onEvaluation?.(key, flag);
1133
+ }
1134
+ return flag.value;
1135
+ }
1136
+ onFlagsReady(callback) {
1137
+ if (this.ready) {
1138
+ callback();
1139
+ } else {
1140
+ this.readyCallbacks.push(callback);
1141
+ }
1142
+ }
1143
+ onFlagChange(listener) {
1144
+ this.changeListeners.add(listener);
1145
+ return () => this.changeListeners.delete(listener);
1146
+ }
1147
+ async refresh() {
1148
+ await this.fetch();
1149
+ }
1150
+ destroy() {
1151
+ if (this.timer) {
1152
+ clearInterval(this.timer);
1153
+ this.timer = null;
1154
+ }
1155
+ }
1156
+ async fetch() {
1157
+ if (!this.writeKey || !this.endpoint) return;
1158
+ const params = new URLSearchParams();
1159
+ if (this.environment) params.set("environment", this.environment);
1160
+ const identity = this.getIdentity();
1161
+ const identifier = identity.userId ?? identity.anonymousId;
1162
+ if (identifier) params.set("identifier", identifier);
1163
+ if (identity.orgId) params.set("orgId", identity.orgId);
1164
+ const qs = params.toString();
1165
+ const url = `${this.endpoint}/api/flags${qs ? `?${qs}` : ""}`;
1166
+ try {
1167
+ const res = await globalThis.fetch(url, {
1168
+ headers: { Authorization: `Bearer ${this.writeKey}` },
1169
+ signal: AbortSignal.timeout(this.timeout)
1170
+ });
1171
+ if (!res.ok) return;
1172
+ const json = await res.json();
1173
+ this.cache.clear();
1174
+ for (const [key, flag] of Object.entries(json.flags)) {
1175
+ this.cache.set(key, flag);
1176
+ }
1177
+ if (!this.ready) {
1178
+ this.ready = true;
1179
+ for (const cb of this.readyCallbacks) {
1180
+ try {
1181
+ cb();
1182
+ } catch {
1183
+ }
1184
+ }
1185
+ this.readyCallbacks = [];
1186
+ }
1187
+ for (const listener of this.changeListeners) {
1188
+ try {
1189
+ listener();
1190
+ } catch {
1191
+ }
1192
+ }
1193
+ } catch {
1194
+ }
1195
+ }
1196
+ };
1197
+
1198
+ // src/client.ts
1199
+ var DEFAULT_FLUSH_AT = 10;
1200
+ var DEFAULT_FLUSH_INTERVAL = 3e3;
1201
+ var DEFAULT_TIMEOUT = 1e4;
1202
+ var DEFAULT_SESSION_TIMEOUT = 30 * 60 * 1e3;
1203
+ var MAX_BATCH_SIZE = 100;
1204
+ var ANON_ID_KEY = "__scoperat_anon_id";
1205
+ var SESSION_ID_KEY = "__scoperat_session_id";
1206
+ var LAST_ACTIVITY_KEY = "__scoperat_last_activity";
1207
+ var TICKET_SESSION_KEY = "__scoperat_ticket_session";
1208
+ function generateAnonId() {
1209
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
1210
+ return crypto.randomUUID();
1211
+ }
1212
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
1213
+ const r = Math.random() * 16 | 0;
1214
+ const v = c === "x" ? r : r & 3 | 8;
1215
+ return v.toString(16);
1216
+ });
1217
+ }
1218
+ function getOrCreateAnonId() {
1219
+ if (typeof window === "undefined" || typeof localStorage === "undefined") {
1220
+ return void 0;
1221
+ }
1222
+ try {
1223
+ let id = localStorage.getItem(ANON_ID_KEY);
1224
+ if (!id) {
1225
+ id = generateAnonId();
1226
+ localStorage.setItem(ANON_ID_KEY, id);
1227
+ }
1228
+ return id;
1229
+ } catch {
1230
+ return generateAnonId();
1231
+ }
1232
+ }
1233
+ var TrackerImpl = class {
1234
+ writeKey = null;
1235
+ endpoint = "";
1236
+ flushAt = DEFAULT_FLUSH_AT;
1237
+ flushInterval = DEFAULT_FLUSH_INTERVAL;
1238
+ timeout = DEFAULT_TIMEOUT;
1239
+ onError;
1240
+ buffer = [];
1241
+ timer = null;
1242
+ visibilityHandler = null;
1243
+ userId;
1244
+ orgId;
1245
+ traits = {};
1246
+ userHash;
1247
+ anonymousId;
1248
+ sessionTimeout = DEFAULT_SESSION_TIMEOUT;
1249
+ ticketSessionFetch = null;
1250
+ _flagClient = null;
1251
+ init(writeKey, options = {}) {
1252
+ this.writeKey = writeKey;
1253
+ this.endpoint = (options.endpoint ?? globalThis.location?.origin ?? "").replace(/\/+$/, "");
1254
+ this.flushAt = options.flushAt ?? DEFAULT_FLUSH_AT;
1255
+ this.flushInterval = options.flushInterval ?? DEFAULT_FLUSH_INTERVAL;
1256
+ this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
1257
+ this.onError = options.onError;
1258
+ this.sessionTimeout = options.sessionTimeout ?? DEFAULT_SESSION_TIMEOUT;
1259
+ this.anonymousId = getOrCreateAnonId();
1260
+ if (this.timer) clearInterval(this.timer);
1261
+ this.timer = setInterval(() => {
1262
+ if (this.buffer.length > 0) {
1263
+ this.flush().catch(() => {
1264
+ });
1265
+ }
1266
+ }, this.flushInterval);
1267
+ if (typeof window !== "undefined") {
1268
+ if (this.visibilityHandler) {
1269
+ window.removeEventListener("visibilitychange", this.visibilityHandler);
1270
+ }
1271
+ this.visibilityHandler = () => {
1272
+ if (document.visibilityState === "hidden" && this.buffer.length > 0) {
1273
+ this.flushSync();
1274
+ }
1275
+ };
1276
+ window.addEventListener("visibilitychange", this.visibilityHandler);
1277
+ }
1278
+ if (options.captureErrors) {
1279
+ installGlobalHandlers(this, getBreadcrumbs);
1280
+ startBreadcrumbCollection();
1281
+ }
1282
+ this._flagClient?.destroy();
1283
+ this._flagClient = new FlagClient({
1284
+ endpoint: this.endpoint,
1285
+ writeKey,
1286
+ timeout: this.timeout,
1287
+ environment: options.environment,
1288
+ pollInterval: options.flagPollInterval,
1289
+ getIdentity: () => ({
1290
+ userId: this.userId,
1291
+ anonymousId: this.anonymousId,
1292
+ orgId: this.orgId
1293
+ }),
1294
+ onEvaluation: (key, flag) => this.track("$flag_evaluation", {
1295
+ flag_key: key,
1296
+ flag_value: flag.value,
1297
+ flag_type: flag.type,
1298
+ flag_source: flag.source
1299
+ })
1300
+ });
1301
+ }
1302
+ /** The underlying flag evaluation client. Use for direct SDK integration. */
1303
+ get flags() {
1304
+ if (!this._flagClient) {
1305
+ throw new Error("Tracker not initialized. Call init() first.");
1306
+ }
1307
+ return this._flagClient;
1308
+ }
1309
+ identify(userId, traits, options) {
1310
+ this.userId = userId;
1311
+ if (traits) {
1312
+ this.traits = { ...traits };
1313
+ if (traits.orgId) this.orgId = traits.orgId;
1314
+ }
1315
+ this.userHash = options?.userHash;
1316
+ for (const evt of this.buffer) {
1317
+ if (!evt.user_id) evt.user_id = userId;
1318
+ if (!evt.org_id && this.orgId) evt.org_id = this.orgId;
1319
+ }
1320
+ this._flagClient?.refresh();
1321
+ }
1322
+ reset() {
1323
+ this.userId = void 0;
1324
+ this.orgId = void 0;
1325
+ this.traits = {};
1326
+ this.userHash = void 0;
1327
+ this.anonymousId = getOrCreateAnonId();
1328
+ this.clearSession();
1329
+ this.clearTicketSession();
1330
+ }
1331
+ clearTicketSession() {
1332
+ this.ticketSessionFetch = null;
1333
+ if (typeof localStorage === "undefined") return;
1334
+ try {
1335
+ localStorage.removeItem(TICKET_SESSION_KEY);
1336
+ } catch {
1337
+ }
1338
+ }
1339
+ readStoredTicketSession() {
1340
+ if (typeof localStorage === "undefined") return null;
1341
+ try {
1342
+ const raw = localStorage.getItem(TICKET_SESSION_KEY);
1343
+ if (!raw) return null;
1344
+ const parsed = JSON.parse(raw);
1345
+ if (typeof parsed.anonymousId === "string" && typeof parsed.token === "string" && typeof parsed.expiresAt === "number" && parsed.expiresAt > Date.now() + 6e4) {
1346
+ return parsed;
1347
+ }
1348
+ localStorage.removeItem(TICKET_SESSION_KEY);
1349
+ } catch {
1350
+ }
1351
+ return null;
1352
+ }
1353
+ /**
1354
+ * @internal Used by the widget API helpers. Returns a cached or
1355
+ * newly-minted anonymous session token, or `null` if the server
1356
+ * has anonymous sessions disabled. Coalesces concurrent callers
1357
+ * onto a single in-flight network request so mount-time parallel
1358
+ * API calls don't each trigger a `POST /ticket/session`.
1359
+ */
1360
+ _ensureTicketSession() {
1361
+ const cached = this.readStoredTicketSession();
1362
+ if (cached) return Promise.resolve(cached);
1363
+ if (this.ticketSessionFetch) return this.ticketSessionFetch;
1364
+ if (!this.writeKey) return Promise.resolve(null);
1365
+ this.ticketSessionFetch = (async () => {
1366
+ try {
1367
+ const res = await fetch(`${this.endpoint}/api/ingest/ticket/session`, {
1368
+ method: "POST",
1369
+ headers: {
1370
+ "Content-Type": "application/json",
1371
+ Authorization: `Bearer ${this.writeKey}`
1372
+ },
1373
+ body: "{}",
1374
+ signal: AbortSignal.timeout(this.timeout)
1375
+ });
1376
+ if (!res.ok) return null;
1377
+ const json = await res.json();
1378
+ if (typeof json?.anonymousId === "string" && typeof json?.token === "string" && typeof json?.expiresAt === "number") {
1379
+ try {
1380
+ localStorage?.setItem(TICKET_SESSION_KEY, JSON.stringify(json));
1381
+ } catch {
1382
+ }
1383
+ this.anonymousId = json.anonymousId;
1384
+ return json;
1385
+ }
1386
+ return null;
1387
+ } catch {
1388
+ return null;
1389
+ } finally {
1390
+ this.ticketSessionFetch = null;
1391
+ }
1392
+ })();
1393
+ return this.ticketSessionFetch;
1394
+ }
1395
+ getOrCreateSessionId() {
1396
+ if (typeof window === "undefined") return void 0;
1397
+ try {
1398
+ const now = Date.now();
1399
+ const lastActivity = localStorage.getItem(LAST_ACTIVITY_KEY);
1400
+ const existingId = sessionStorage.getItem(SESSION_ID_KEY);
1401
+ const expired = !lastActivity || now - Number(lastActivity) > this.sessionTimeout;
1402
+ if (existingId && !expired) {
1403
+ localStorage.setItem(LAST_ACTIVITY_KEY, String(now));
1404
+ return existingId;
1405
+ }
1406
+ const newId = generateAnonId();
1407
+ sessionStorage.setItem(SESSION_ID_KEY, newId);
1408
+ localStorage.setItem(LAST_ACTIVITY_KEY, String(now));
1409
+ return newId;
1410
+ } catch {
1411
+ return generateAnonId();
1412
+ }
1413
+ }
1414
+ clearSession() {
1415
+ if (typeof window === "undefined") return;
1416
+ try {
1417
+ sessionStorage.removeItem(SESSION_ID_KEY);
1418
+ localStorage.removeItem(LAST_ACTIVITY_KEY);
1419
+ } catch {
1420
+ }
1421
+ }
1422
+ getSessionId() {
1423
+ return this.getOrCreateSessionId();
1424
+ }
1425
+ /** @internal Used by the widget to read tracker config. */
1426
+ _getEndpoint() {
1427
+ return this.endpoint;
1428
+ }
1429
+ /** @internal Used by the widget to read the write key. */
1430
+ _getWriteKey() {
1431
+ return this.writeKey;
1432
+ }
1433
+ /** @internal Used by the widget to read the current user identity. */
1434
+ _getIdentity() {
1435
+ return {
1436
+ userId: this.userId,
1437
+ anonymousId: this.anonymousId,
1438
+ email: this.traits.email,
1439
+ name: this.traits.name,
1440
+ userHash: this.userHash
1441
+ };
1442
+ }
1443
+ captureException(error, context) {
1444
+ if (!this.writeKey) return;
1445
+ const breadcrumbs = getBreadcrumbs();
1446
+ this.track("$exception", {
1447
+ type: error.name || "Error",
1448
+ message: error.message,
1449
+ stack: error.stack,
1450
+ handled: true,
1451
+ mechanism: context?.mechanism ?? "manual",
1452
+ source: "browser",
1453
+ ...context,
1454
+ context: { breadcrumbs }
1455
+ });
1456
+ }
1457
+ addBreadcrumb(category, message, data) {
1458
+ addBreadcrumb({
1459
+ type: "custom",
1460
+ category,
1461
+ message,
1462
+ timestamp: Date.now(),
1463
+ data
1464
+ });
1465
+ }
1466
+ track(eventOrName, properties) {
1467
+ if (!this.writeKey) return;
1468
+ const evt = typeof eventOrName === "string" ? { event: eventOrName, properties } : eventOrName;
1469
+ const traitsContext = Object.keys(this.traits).length > 0 ? { traits: this.traits } : {};
1470
+ const enriched = {
1471
+ ...evt,
1472
+ occurred_at: evt.occurred_at ?? (/* @__PURE__ */ new Date()).toISOString(),
1473
+ source: evt.source ?? "browser",
1474
+ org_id: evt.org_id ?? this.orgId,
1475
+ user_id: evt.user_id ?? this.userId,
1476
+ anonymous_id: evt.anonymous_id ?? this.anonymousId,
1477
+ session_id: evt.session_id ?? this.getOrCreateSessionId(),
1478
+ context: {
1479
+ ...getBrowserContext(),
1480
+ ...traitsContext,
1481
+ ...evt.context
1482
+ }
1483
+ };
1484
+ this.buffer.push(enriched);
1485
+ if (this.buffer.length >= this.flushAt) {
1486
+ this.flush().catch(() => {
1487
+ });
1488
+ }
1489
+ }
1490
+ async flush() {
1491
+ if (!this.writeKey || this.buffer.length === 0) {
1492
+ return { success: true, ingested: 0 };
1493
+ }
1494
+ const batch = this.buffer.splice(0, MAX_BATCH_SIZE);
1495
+ try {
1496
+ const res = await fetch(`${this.endpoint}/api/ingest/track`, {
1497
+ method: "POST",
1498
+ headers: {
1499
+ "Content-Type": "application/json",
1500
+ Authorization: `Bearer ${this.writeKey}`
1501
+ },
1502
+ body: JSON.stringify({ events: batch }),
1503
+ signal: AbortSignal.timeout(this.timeout)
1504
+ });
1505
+ if (!res.ok) {
1506
+ const body = await res.text().catch(() => "");
1507
+ throw new Error(`API returned ${res.status}: ${body}`);
1508
+ }
1509
+ const json = await res.json();
1510
+ return { success: true, ingested: json.ingested };
1511
+ } catch (error) {
1512
+ this.buffer.unshift(...batch);
1513
+ const err = error instanceof Error ? error : new Error(String(error));
1514
+ this.onError?.(err, batch);
1515
+ return { success: false, ingested: 0 };
1516
+ }
1517
+ }
1518
+ flushSync() {
1519
+ if (!this.writeKey) return;
1520
+ if (typeof navigator === "undefined" || !navigator.sendBeacon) return;
1521
+ if (this.buffer.length === 0) return;
1522
+ const batch = this.buffer.splice(0, MAX_BATCH_SIZE);
1523
+ const payload = JSON.stringify({ events: batch });
1524
+ const url = `${this.endpoint}/api/ingest/track?key=${encodeURIComponent(this.writeKey)}`;
1525
+ try {
1526
+ const blob = new Blob([payload], { type: "application/json" });
1527
+ navigator.sendBeacon(url, blob);
1528
+ } catch {
1529
+ }
1530
+ }
1531
+ getFlag(key, defaultValue) {
1532
+ return this._flagClient?.getFlag(key, defaultValue) ?? defaultValue;
1533
+ }
1534
+ onFlagsReady(callback) {
1535
+ this._flagClient?.onFlagsReady(callback);
1536
+ }
1537
+ onFlagChange(listener) {
1538
+ return this._flagClient?.onFlagChange(listener) ?? (() => {
1539
+ });
1540
+ }
1541
+ async refreshFlags() {
1542
+ await this._flagClient?.refresh();
1543
+ }
1544
+ async shutdown() {
1545
+ if (this.timer) {
1546
+ clearInterval(this.timer);
1547
+ this.timer = null;
1548
+ }
1549
+ this._flagClient?.destroy();
1550
+ this._flagClient = null;
1551
+ if (typeof window !== "undefined" && this.visibilityHandler) {
1552
+ window.removeEventListener("visibilitychange", this.visibilityHandler);
1553
+ this.visibilityHandler = null;
1554
+ }
1555
+ while (this.buffer.length > 0) {
1556
+ await this.flush();
1557
+ }
1558
+ }
1559
+ };
1560
+ var scoperat = new TrackerImpl();
1561
+ var client_default = scoperat;
1562
+
1563
+ // src/widget/api.ts
1564
+ async function identityHeaders() {
1565
+ const identity = client_default._getIdentity();
1566
+ if (identity.userId && identity.userHash) {
1567
+ return {
1568
+ "X-Scoperat-User-Hash": identity.userHash,
1569
+ "X-Scoperat-User-Id": identity.userId
1570
+ };
1571
+ }
1572
+ const session = await client_default._ensureTicketSession();
1573
+ if (session) {
1574
+ return { "X-Scoperat-Session": session.token };
1575
+ }
1576
+ return {};
1577
+ }
1578
+ async function identityQueryParams() {
1579
+ const params = new URLSearchParams();
1580
+ const identity = client_default._getIdentity();
1581
+ if (identity.userId && identity.userHash) {
1582
+ params.set("user_id", identity.userId);
1583
+ params.set("user_hash", identity.userHash);
1584
+ return params;
1585
+ }
1586
+ const session = await client_default._ensureTicketSession();
1587
+ if (session) {
1588
+ params.set("session", session.token);
1589
+ }
1590
+ return params;
1591
+ }
1592
+ async function jsonHeaders() {
1593
+ const key = client_default._getWriteKey();
1594
+ return {
1595
+ "Content-Type": "application/json",
1596
+ ...key ? { Authorization: `Bearer ${key}` } : {},
1597
+ ...await identityHeaders()
1598
+ };
1599
+ }
1600
+ async function authHeaders() {
1601
+ const key = client_default._getWriteKey();
1602
+ return {
1603
+ ...key ? { Authorization: `Bearer ${key}` } : {},
1604
+ ...await identityHeaders()
1605
+ };
1606
+ }
1607
+ function baseUrl() {
1608
+ return client_default._getEndpoint();
1609
+ }
1610
+ async function uploadFile(file) {
1611
+ const form = new FormData();
1612
+ form.append("file", file);
1613
+ const res = await fetch(`${baseUrl()}/api/ingest/ticket/upload`, {
1614
+ method: "POST",
1615
+ headers: await authHeaders(),
1616
+ body: form
1617
+ });
1618
+ if (!res.ok) {
1619
+ const text = await res.text().catch(() => "");
1620
+ throw new Error(`Upload failed: ${res.status} ${text}`);
1621
+ }
1622
+ return res.json();
1623
+ }
1624
+ async function createTicket(data) {
1625
+ const identity = client_default._getIdentity();
1626
+ const context = getBrowserContext();
1627
+ const res = await fetch(`${baseUrl()}/api/ingest/ticket`, {
1628
+ method: "POST",
1629
+ headers: await jsonHeaders(),
1630
+ body: JSON.stringify({
1631
+ ...data,
1632
+ attachments: data.attachments?.map((a) => ({
1633
+ storageKey: a.storageKey,
1634
+ fileName: a.fileName,
1635
+ mimeType: a.mimeType,
1636
+ fileSize: a.size
1637
+ })),
1638
+ email: data.email ?? identity.email,
1639
+ name: data.name ?? identity.name,
1640
+ metadata: {
1641
+ ...context,
1642
+ ...data.metadata
1643
+ }
1644
+ })
1645
+ });
1646
+ if (!res.ok) {
1647
+ const body = await res.text().catch(() => "");
1648
+ throw new Error(`Failed to create ticket: ${res.status} ${body}`);
1649
+ }
1650
+ return res.json();
1651
+ }
1652
+ async function sendMessage(ticketId, body, attachments) {
1653
+ const res = await fetch(`${baseUrl()}/api/ingest/ticket/message`, {
1654
+ method: "POST",
1655
+ headers: await jsonHeaders(),
1656
+ body: JSON.stringify({
1657
+ ticketId,
1658
+ body,
1659
+ attachments: attachments?.map((a) => ({
1660
+ storageKey: a.storageKey,
1661
+ fileName: a.fileName,
1662
+ mimeType: a.mimeType,
1663
+ fileSize: a.size
1664
+ }))
1665
+ })
1666
+ });
1667
+ if (!res.ok) {
1668
+ const text = await res.text().catch(() => "");
1669
+ throw new Error(`Failed to send message: ${res.status} ${text}`);
1670
+ }
1671
+ return res.json();
1672
+ }
1673
+ async function fetchTickets() {
1674
+ const params = await identityQueryParams();
1675
+ const key = client_default._getWriteKey();
1676
+ if (key) params.set("key", key);
1677
+ const res = await fetch(
1678
+ `${baseUrl()}/api/ingest/tickets?${params.toString()}`,
1679
+ { headers: await authHeaders() }
1680
+ );
1681
+ if (!res.ok) return [];
1682
+ const data = await res.json();
1683
+ return data.tickets;
1684
+ }
1685
+ async function fetchConversation(ticketId) {
1686
+ const params = await identityQueryParams();
1687
+ const res = await fetch(
1688
+ `${baseUrl()}/api/ingest/ticket/${ticketId}?${params.toString()}`,
1689
+ { headers: await authHeaders() }
1690
+ );
1691
+ if (!res.ok) {
1692
+ throw new Error(`Failed to fetch conversation: ${res.status}`);
1693
+ }
1694
+ return res.json();
1695
+ }
1696
+ async function fetchPublicKbPage(workspaceSlug, pageSlug) {
1697
+ try {
1698
+ const res = await fetch(`${baseUrl()}/api/kb/${workspaceSlug}/${pageSlug}`);
1699
+ if (!res.ok) return null;
1700
+ return res.json();
1701
+ } catch {
1702
+ return null;
1703
+ }
1704
+ }
1705
+
1706
+ // src/widget/state.ts
1707
+ var listeners = /* @__PURE__ */ new Set();
1708
+ var state = {
1709
+ isOpen: false,
1710
+ view: "conversations",
1711
+ tickets: [],
1712
+ activeTicketId: null,
1713
+ activeConversation: null,
1714
+ loading: false,
1715
+ error: null
1716
+ };
1717
+ function notify() {
1718
+ for (const fn of listeners) fn();
1719
+ }
1720
+ function getState() {
1721
+ return state;
1722
+ }
1723
+ function subscribe(fn) {
1724
+ listeners.add(fn);
1725
+ return () => listeners.delete(fn);
1726
+ }
1727
+ function setState(partial) {
1728
+ state = { ...state, ...partial };
1729
+ notify();
1730
+ }
1731
+ function toggleOpen() {
1732
+ setState({ isOpen: !state.isOpen });
1733
+ }
1734
+ function setView(view) {
1735
+ setState({ view });
1736
+ }
1737
+ function openThread(ticketId) {
1738
+ setState({ view: "thread", activeTicketId: ticketId });
1739
+ }
1740
+ function goBack() {
1741
+ setState({
1742
+ view: "conversations",
1743
+ activeTicketId: null,
1744
+ activeConversation: null
1745
+ });
1746
+ }
1747
+
1748
+ // src/widget/ConversationList.tsx
1749
+ import { jsx, jsxs } from "preact/jsx-runtime";
1750
+ function timeAgo(dateStr) {
1751
+ const seconds = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1e3);
1752
+ if (seconds < 60) return "just now";
1753
+ const minutes = Math.floor(seconds / 60);
1754
+ if (minutes < 60) return `${minutes}m ago`;
1755
+ const hours = Math.floor(minutes / 60);
1756
+ if (hours < 24) return `${hours}h ago`;
1757
+ const days = Math.floor(hours / 24);
1758
+ return `${days}d ago`;
1759
+ }
1760
+ function TicketItem({ ticket }) {
1761
+ return /* @__PURE__ */ jsxs("li", { class: "sr-ticket-item", onClick: () => openThread(ticket.id), children: [
1762
+ /* @__PURE__ */ jsx("h4", { children: ticket.subject }),
1763
+ /* @__PURE__ */ jsx("div", { class: "sr-meta", children: /* @__PURE__ */ jsx("span", { children: timeAgo(ticket.lastMessageAt ?? ticket.createdAt) }) })
1764
+ ] });
1765
+ }
1766
+ function ConversationList() {
1767
+ const state2 = getState();
1768
+ useEffect(() => {
1769
+ setState({ loading: true });
1770
+ fetchTickets().then((tickets) => setState({ tickets, loading: false })).catch(() => setState({ loading: false }));
1771
+ }, []);
1772
+ if (state2.loading) {
1773
+ return /* @__PURE__ */ jsx("div", { class: "sr-loading", children: "Loading conversations..." });
1774
+ }
1775
+ return /* @__PURE__ */ jsx("div", { children: state2.tickets.length === 0 ? /* @__PURE__ */ jsxs("div", { class: "sr-empty", children: [
1776
+ /* @__PURE__ */ jsx("p", { children: "No conversations yet" }),
1777
+ /* @__PURE__ */ jsx(
1778
+ "button",
1779
+ {
1780
+ class: "sr-btn sr-btn-secondary",
1781
+ onClick: () => setView("new-ticket"),
1782
+ children: "Start one"
1783
+ }
1784
+ )
1785
+ ] }) : /* @__PURE__ */ jsx("ul", { class: "sr-ticket-list", children: state2.tickets.map((t) => /* @__PURE__ */ jsx(TicketItem, { ticket: t }, t.id)) }) });
1786
+ }
1787
+
1788
+ // src/widget/ConversationThread.tsx
1789
+ import DOMPurify from "dompurify";
1790
+ import { Marked } from "marked";
1791
+ import {
1792
+ useCallback,
1793
+ useEffect as useEffect2,
1794
+ useMemo,
1795
+ useRef,
1796
+ useState
1797
+ } from "preact/hooks";
1798
+ import { jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
1799
+ var marked = new Marked({ async: false, gfm: true, breaks: true });
1800
+ if (typeof window !== "undefined") {
1801
+ DOMPurify.addHook("afterSanitizeAttributes", (node) => {
1802
+ if (node.tagName === "A") {
1803
+ node.setAttribute("target", "_blank");
1804
+ node.setAttribute("rel", "noopener noreferrer");
1805
+ }
1806
+ });
1807
+ }
1808
+ function renderMarkdown(text) {
1809
+ const rawHtml = marked.parse(text);
1810
+ return DOMPurify.sanitize(rawHtml, { USE_PROFILES: { html: true } });
1811
+ }
1812
+ function formatTime(dateStr) {
1813
+ const d = new Date(dateStr);
1814
+ const now = /* @__PURE__ */ new Date();
1815
+ const sameDay = d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate();
1816
+ if (sameDay) {
1817
+ return d.toLocaleTimeString(void 0, {
1818
+ hour: "numeric",
1819
+ minute: "2-digit"
1820
+ });
1821
+ }
1822
+ return d.toLocaleDateString(void 0, {
1823
+ month: "short",
1824
+ day: "numeric",
1825
+ hour: "numeric",
1826
+ minute: "2-digit"
1827
+ });
1828
+ }
1829
+ function Avatar({
1830
+ name,
1831
+ imageUrl
1832
+ }) {
1833
+ if (imageUrl) {
1834
+ return /* @__PURE__ */ jsx2("div", { class: "sr-avatar", children: /* @__PURE__ */ jsx2("img", { src: imageUrl, alt: name }) });
1835
+ }
1836
+ return /* @__PURE__ */ jsx2("div", { class: "sr-avatar", children: name.charAt(0).toUpperCase() });
1837
+ }
1838
+ function formatFileSize(bytes) {
1839
+ if (bytes < 1024) return `${bytes} B`;
1840
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1841
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1842
+ }
1843
+ function FilePreview({ attachment }) {
1844
+ const isImage = attachment.mimeType.startsWith("image/");
1845
+ return /* @__PURE__ */ jsxs2("div", { class: "sr-file-preview", children: [
1846
+ isImage && attachment.url && /* @__PURE__ */ jsx2(
1847
+ "a",
1848
+ {
1849
+ href: attachment.url,
1850
+ target: "_blank",
1851
+ rel: "noopener noreferrer",
1852
+ class: "sr-file-preview-image",
1853
+ children: /* @__PURE__ */ jsx2("img", { src: attachment.url, alt: attachment.fileName })
1854
+ }
1855
+ ),
1856
+ /* @__PURE__ */ jsxs2("div", { class: "sr-file-preview-row", children: [
1857
+ /* @__PURE__ */ jsxs2(
1858
+ "svg",
1859
+ {
1860
+ class: "sr-file-preview-icon",
1861
+ viewBox: "0 0 24 24",
1862
+ xmlns: "http://www.w3.org/2000/svg",
1863
+ children: [
1864
+ /* @__PURE__ */ jsx2(
1865
+ "path",
1866
+ {
1867
+ d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z",
1868
+ fill: "none",
1869
+ stroke: "currentColor",
1870
+ "stroke-width": "2",
1871
+ "stroke-linecap": "round",
1872
+ "stroke-linejoin": "round"
1873
+ }
1874
+ ),
1875
+ /* @__PURE__ */ jsx2(
1876
+ "polyline",
1877
+ {
1878
+ points: "14 2 14 8 20 8",
1879
+ fill: "none",
1880
+ stroke: "currentColor",
1881
+ "stroke-width": "2",
1882
+ "stroke-linecap": "round",
1883
+ "stroke-linejoin": "round"
1884
+ }
1885
+ )
1886
+ ]
1887
+ }
1888
+ ),
1889
+ /* @__PURE__ */ jsxs2("div", { class: "sr-file-preview-info", children: [
1890
+ attachment.url ? /* @__PURE__ */ jsx2(
1891
+ "a",
1892
+ {
1893
+ href: attachment.url,
1894
+ target: "_blank",
1895
+ rel: "noopener noreferrer",
1896
+ class: "sr-file-preview-name",
1897
+ children: attachment.fileName
1898
+ }
1899
+ ) : /* @__PURE__ */ jsx2("span", { class: "sr-file-preview-name", children: attachment.fileName }),
1900
+ /* @__PURE__ */ jsx2("span", { class: "sr-file-preview-size", children: formatFileSize(attachment.fileSize) })
1901
+ ] }),
1902
+ attachment.url && /* @__PURE__ */ jsx2(
1903
+ "a",
1904
+ {
1905
+ href: attachment.url,
1906
+ download: attachment.fileName,
1907
+ target: "_blank",
1908
+ rel: "noopener noreferrer",
1909
+ class: "sr-file-preview-download",
1910
+ "aria-label": "Download",
1911
+ children: /* @__PURE__ */ jsx2("svg", { viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsx2(
1912
+ "path",
1913
+ {
1914
+ d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3",
1915
+ fill: "none",
1916
+ stroke: "currentColor",
1917
+ "stroke-width": "2",
1918
+ "stroke-linecap": "round",
1919
+ "stroke-linejoin": "round"
1920
+ }
1921
+ ) })
1922
+ }
1923
+ )
1924
+ ] })
1925
+ ] });
1926
+ }
1927
+ function MessageAttachments({ messageId }) {
1928
+ const state2 = getState();
1929
+ const allAttachments = state2.activeConversation?.attachments ?? [];
1930
+ const msgAttachments = allAttachments.filter(
1931
+ (a) => a.messageId === messageId
1932
+ );
1933
+ if (msgAttachments.length === 0) return null;
1934
+ return /* @__PURE__ */ jsx2("div", { class: "sr-message-attachments", children: msgAttachments.map((att) => /* @__PURE__ */ jsx2(FilePreview, { attachment: att }, att.id)) });
1935
+ }
1936
+ function LinkPreviewCard({ preview }) {
1937
+ let hostname = "";
1938
+ try {
1939
+ hostname = new URL(preview.url).hostname.replace(/^www\./, "");
1940
+ } catch {
1941
+ }
1942
+ return /* @__PURE__ */ jsxs2(
1943
+ "a",
1944
+ {
1945
+ class: "sr-link-preview",
1946
+ href: preview.url,
1947
+ target: "_blank",
1948
+ rel: "noopener noreferrer",
1949
+ children: [
1950
+ preview.image && /* @__PURE__ */ jsx2("div", { class: "sr-link-preview-img", children: /* @__PURE__ */ jsx2(
1951
+ "img",
1952
+ {
1953
+ src: preview.image,
1954
+ alt: "",
1955
+ loading: "lazy",
1956
+ onError: (e) => {
1957
+ const parent = e.currentTarget.parentElement;
1958
+ if (parent) parent.style.display = "none";
1959
+ }
1960
+ }
1961
+ ) }),
1962
+ /* @__PURE__ */ jsxs2("div", { class: "sr-link-preview-body", children: [
1963
+ /* @__PURE__ */ jsxs2("div", { class: "sr-link-preview-site", children: [
1964
+ preview.favicon ? /* @__PURE__ */ jsx2(
1965
+ "img",
1966
+ {
1967
+ class: "sr-link-preview-favicon",
1968
+ src: preview.favicon,
1969
+ alt: "",
1970
+ onError: (e) => {
1971
+ e.currentTarget.style.display = "none";
1972
+ }
1973
+ }
1974
+ ) : null,
1975
+ /* @__PURE__ */ jsx2("span", { children: preview.siteName ?? hostname })
1976
+ ] }),
1977
+ preview.title && /* @__PURE__ */ jsx2("p", { class: "sr-link-preview-title", children: preview.title }),
1978
+ preview.description && /* @__PURE__ */ jsx2("p", { class: "sr-link-preview-desc", children: preview.description })
1979
+ ] })
1980
+ ]
1981
+ }
1982
+ );
1983
+ }
1984
+ function Message({ msg }) {
1985
+ const isUser = msg.senderType === "user";
1986
+ const senderName = msg.senderType === "agent" ? msg.agentName ?? msg.senderName ?? "Support" : null;
1987
+ const displayName = isUser ? "You" : senderName ?? "Support";
1988
+ const html = useMemo(() => renderMarkdown(msg.body), [msg.body]);
1989
+ const isAttachmentOnly = msg.body === "(attachment)";
1990
+ const messageArticles = useMemo(() => {
1991
+ const raw = msg.metadata?.articles;
1992
+ if (!Array.isArray(raw) || raw.length === 0) return null;
1993
+ return raw;
1994
+ }, [msg.metadata]);
1995
+ const linkPreview = useMemo(() => {
1996
+ const raw = msg.metadata?.linkPreview;
1997
+ if (!raw || typeof raw !== "object") return null;
1998
+ const candidate = raw;
1999
+ if (typeof candidate.url !== "string") return null;
2000
+ return candidate;
2001
+ }, [msg.metadata]);
2002
+ return /* @__PURE__ */ jsxs2(
2003
+ "div",
2004
+ {
2005
+ class: `sr-message-row ${isUser ? "sr-message-row-user" : "sr-message-row-agent"}`,
2006
+ children: [
2007
+ !isAttachmentOnly && /* @__PURE__ */ jsx2(
2008
+ "div",
2009
+ {
2010
+ class: "sr-message sr-prose",
2011
+ dangerouslySetInnerHTML: { __html: html }
2012
+ }
2013
+ ),
2014
+ /* @__PURE__ */ jsx2(MessageAttachments, { messageId: msg.id }),
2015
+ messageArticles && /* @__PURE__ */ jsx2(SuggestedArticles, { articles: messageArticles }),
2016
+ linkPreview && /* @__PURE__ */ jsx2(LinkPreviewCard, { preview: linkPreview }),
2017
+ /* @__PURE__ */ jsxs2("div", { class: "sr-message-meta", children: [
2018
+ !isUser && /* @__PURE__ */ jsx2(Avatar, { name: displayName, imageUrl: msg.agentAvatarUrl }),
2019
+ /* @__PURE__ */ jsx2("span", { class: "sr-message-sender", children: displayName }),
2020
+ /* @__PURE__ */ jsx2("span", { children: "\xB7" }),
2021
+ /* @__PURE__ */ jsx2("span", { children: formatTime(msg.createdAt) })
2022
+ ] })
2023
+ ]
2024
+ }
2025
+ );
2026
+ }
2027
+ function MessageComposer({ ticketId }) {
2028
+ const [body, setBody] = useState("");
2029
+ const [sending, setSending] = useState(false);
2030
+ const [attachments, setAttachments] = useState([]);
2031
+ const [uploading, setUploading] = useState(false);
2032
+ const fileInputRef = useRef(null);
2033
+ const handleFileSelect = useCallback(
2034
+ async (e) => {
2035
+ const input = e.target;
2036
+ const files = input.files;
2037
+ if (!files) return;
2038
+ for (let i = 0; i < files.length && attachments.length + i < 5; i++) {
2039
+ setUploading(true);
2040
+ try {
2041
+ const result = await uploadFile(files[i]);
2042
+ setAttachments((prev) => [...prev, result]);
2043
+ } catch {
2044
+ } finally {
2045
+ setUploading(false);
2046
+ }
2047
+ }
2048
+ input.value = "";
2049
+ },
2050
+ [attachments.length]
2051
+ );
2052
+ const handleSend = useCallback(async () => {
2053
+ if (!body.trim() && attachments.length === 0 || sending) return;
2054
+ setSending(true);
2055
+ try {
2056
+ await sendMessage(
2057
+ ticketId,
2058
+ body.trim() || "(attachment)",
2059
+ attachments.length ? attachments : void 0
2060
+ );
2061
+ setBody("");
2062
+ setAttachments([]);
2063
+ const conv = await fetchConversation(ticketId);
2064
+ setState({ activeConversation: conv });
2065
+ } catch {
2066
+ } finally {
2067
+ setSending(false);
2068
+ }
2069
+ }, [body, sending, ticketId, attachments]);
2070
+ const handleKeyDown = useCallback(
2071
+ (e) => {
2072
+ if (e.key === "Enter" && !e.shiftKey) {
2073
+ e.preventDefault();
2074
+ handleSend();
2075
+ }
2076
+ },
2077
+ [handleSend]
2078
+ );
2079
+ const canSend = (!!body.trim() || attachments.length > 0) && !sending;
2080
+ return /* @__PURE__ */ jsxs2("div", { class: "sr-composer", children: [
2081
+ (attachments.length > 0 || uploading) && /* @__PURE__ */ jsxs2("div", { class: "sr-attachment-pills", children: [
2082
+ attachments.map((att) => /* @__PURE__ */ jsxs2("div", { class: "sr-attachment-pill", children: [
2083
+ /* @__PURE__ */ jsx2(
2084
+ "svg",
2085
+ {
2086
+ class: "sr-attachment-pill-icon",
2087
+ viewBox: "0 0 24 24",
2088
+ xmlns: "http://www.w3.org/2000/svg",
2089
+ children: /* @__PURE__ */ jsx2(
2090
+ "path",
2091
+ {
2092
+ d: "M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48",
2093
+ fill: "none",
2094
+ stroke: "currentColor",
2095
+ "stroke-width": "2",
2096
+ "stroke-linecap": "round",
2097
+ "stroke-linejoin": "round"
2098
+ }
2099
+ )
2100
+ }
2101
+ ),
2102
+ /* @__PURE__ */ jsx2("span", { class: "sr-attachment-pill-name", children: att.fileName }),
2103
+ /* @__PURE__ */ jsx2(
2104
+ "button",
2105
+ {
2106
+ type: "button",
2107
+ class: "sr-attachment-pill-remove",
2108
+ onClick: () => setAttachments(
2109
+ (prev) => prev.filter((a) => a.storageKey !== att.storageKey)
2110
+ ),
2111
+ "aria-label": "Remove",
2112
+ children: /* @__PURE__ */ jsx2("svg", { viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsx2(
2113
+ "path",
2114
+ {
2115
+ d: "M18 6L6 18M6 6l12 12",
2116
+ fill: "none",
2117
+ stroke: "currentColor",
2118
+ "stroke-width": "2",
2119
+ "stroke-linecap": "round"
2120
+ }
2121
+ ) })
2122
+ }
2123
+ )
2124
+ ] }, att.storageKey)),
2125
+ uploading && /* @__PURE__ */ jsx2("div", { class: "sr-attachment-pill", children: /* @__PURE__ */ jsx2("span", { class: "sr-attachment-pill-name", children: "Uploading..." }) })
2126
+ ] }),
2127
+ /* @__PURE__ */ jsxs2("div", { class: "sr-composer-wrap", children: [
2128
+ /* @__PURE__ */ jsx2(
2129
+ "textarea",
2130
+ {
2131
+ value: body,
2132
+ onInput: (e) => setBody(e.target.value),
2133
+ onKeyDown: handleKeyDown,
2134
+ placeholder: "Ask about this issue...",
2135
+ rows: 3
2136
+ }
2137
+ ),
2138
+ /* @__PURE__ */ jsx2(
2139
+ "button",
2140
+ {
2141
+ type: "button",
2142
+ class: "sr-attach-btn sr-attach-btn-left",
2143
+ onClick: () => fileInputRef.current?.click(),
2144
+ "aria-label": "Attach file",
2145
+ disabled: attachments.length >= 5,
2146
+ children: /* @__PURE__ */ jsx2("svg", { viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsx2(
2147
+ "path",
2148
+ {
2149
+ d: "M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48",
2150
+ fill: "none",
2151
+ stroke: "currentColor",
2152
+ "stroke-width": "2",
2153
+ "stroke-linecap": "round",
2154
+ "stroke-linejoin": "round"
2155
+ }
2156
+ ) })
2157
+ }
2158
+ ),
2159
+ /* @__PURE__ */ jsx2(
2160
+ "button",
2161
+ {
2162
+ class: "sr-send-btn sr-send-btn-abs",
2163
+ onClick: handleSend,
2164
+ disabled: !canSend,
2165
+ "aria-label": "Send",
2166
+ children: /* @__PURE__ */ jsx2("svg", { viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsx2("path", { d: "M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z" }) })
2167
+ }
2168
+ )
2169
+ ] }),
2170
+ /* @__PURE__ */ jsx2(
2171
+ "input",
2172
+ {
2173
+ ref: fileInputRef,
2174
+ type: "file",
2175
+ multiple: true,
2176
+ style: { display: "none" },
2177
+ onChange: handleFileSelect
2178
+ }
2179
+ )
2180
+ ] });
2181
+ }
2182
+ function ArticleCard({ article }) {
2183
+ const [rawContent, setRawContent] = useState(null);
2184
+ const [expanded, setExpanded] = useState(false);
2185
+ const [loading, setLoading] = useState(false);
2186
+ const html = useMemo(() => {
2187
+ if (!rawContent) return "";
2188
+ return renderMarkdown(rawContent);
2189
+ }, [rawContent]);
2190
+ const handleClick = useCallback(async () => {
2191
+ if (expanded) {
2192
+ setExpanded(false);
2193
+ return;
2194
+ }
2195
+ if (rawContent) {
2196
+ setExpanded(true);
2197
+ return;
2198
+ }
2199
+ setLoading(true);
2200
+ const data = await fetchPublicKbPage(article.workspaceSlug, article.slug);
2201
+ if (data?.page.contentUrl) {
2202
+ try {
2203
+ const res = await fetch(data.page.contentUrl);
2204
+ if (res.ok) {
2205
+ const text = await res.text();
2206
+ setRawContent(text);
2207
+ }
2208
+ } catch {
2209
+ }
2210
+ }
2211
+ setExpanded(true);
2212
+ setLoading(false);
2213
+ }, [article, rawContent, expanded]);
2214
+ return /* @__PURE__ */ jsxs2("div", { class: "sr-article-card", children: [
2215
+ /* @__PURE__ */ jsxs2("button", { type: "button", class: "sr-article-btn", onClick: handleClick, children: [
2216
+ /* @__PURE__ */ jsx2(
2217
+ "svg",
2218
+ {
2219
+ class: "sr-article-icon",
2220
+ viewBox: "0 0 24 24",
2221
+ fill: "none",
2222
+ stroke: "currentColor",
2223
+ "stroke-width": "2",
2224
+ "stroke-linecap": "round",
2225
+ "stroke-linejoin": "round",
2226
+ children: /* @__PURE__ */ jsx2("path", { d: "M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20" })
2227
+ }
2228
+ ),
2229
+ /* @__PURE__ */ jsx2("span", { class: "sr-article-title", children: article.title }),
2230
+ /* @__PURE__ */ jsx2(
2231
+ "svg",
2232
+ {
2233
+ class: `sr-article-chevron ${expanded ? "sr-expanded" : ""}`,
2234
+ viewBox: "0 0 24 24",
2235
+ fill: "none",
2236
+ stroke: "currentColor",
2237
+ "stroke-width": "2",
2238
+ "stroke-linecap": "round",
2239
+ "stroke-linejoin": "round",
2240
+ children: /* @__PURE__ */ jsx2("polyline", { points: "6 9 12 15 18 9" })
2241
+ }
2242
+ )
2243
+ ] }),
2244
+ loading && /* @__PURE__ */ jsx2("div", { class: "sr-article-loading", children: "Loading..." }),
2245
+ expanded && html && /* @__PURE__ */ jsx2(
2246
+ "div",
2247
+ {
2248
+ class: "sr-article-content sr-prose",
2249
+ dangerouslySetInnerHTML: { __html: html }
2250
+ }
2251
+ )
2252
+ ] });
2253
+ }
2254
+ function SuggestedArticles({ articles }) {
2255
+ if (!articles || articles.length === 0) return null;
2256
+ return /* @__PURE__ */ jsxs2("div", { class: "sr-suggested-articles", children: [
2257
+ /* @__PURE__ */ jsxs2("div", { class: "sr-suggested-label", children: [
2258
+ /* @__PURE__ */ jsxs2(
2259
+ "svg",
2260
+ {
2261
+ viewBox: "0 0 24 24",
2262
+ fill: "none",
2263
+ stroke: "currentColor",
2264
+ "stroke-width": "2",
2265
+ "stroke-linecap": "round",
2266
+ "stroke-linejoin": "round",
2267
+ children: [
2268
+ /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "10" }),
2269
+ /* @__PURE__ */ jsx2("path", { d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" }),
2270
+ /* @__PURE__ */ jsx2("line", { x1: "12", y1: "17", x2: "12.01", y2: "17" })
2271
+ ]
2272
+ }
2273
+ ),
2274
+ "These articles may help"
2275
+ ] }),
2276
+ articles.map((article) => /* @__PURE__ */ jsx2(ArticleCard, { article }, article.pageId))
2277
+ ] });
2278
+ }
2279
+ function ConversationThread() {
2280
+ const state2 = getState();
2281
+ const ticketId = state2.activeTicketId;
2282
+ const conversation = state2.activeConversation;
2283
+ const messagesEndRef = useRef(null);
2284
+ useEffect2(() => {
2285
+ if (!ticketId) return;
2286
+ setState({ loading: true });
2287
+ fetchConversation(ticketId).then((conv) => setState({ activeConversation: conv, loading: false })).catch(() => setState({ loading: false }));
2288
+ }, [ticketId]);
2289
+ useEffect2(() => {
2290
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
2291
+ }, [conversation?.messages.length]);
2292
+ if (!ticketId) return null;
2293
+ if (state2.loading || !conversation) {
2294
+ return /* @__PURE__ */ jsx2("div", { class: "sr-loading", children: "Loading..." });
2295
+ }
2296
+ return /* @__PURE__ */ jsxs2("div", { class: "sr-thread", children: [
2297
+ /* @__PURE__ */ jsxs2("div", { class: "sr-thread-messages", children: [
2298
+ /* @__PURE__ */ jsxs2("div", { class: "sr-message-row sr-message-row-user", children: [
2299
+ /* @__PURE__ */ jsx2(
2300
+ "div",
2301
+ {
2302
+ class: "sr-message sr-prose",
2303
+ dangerouslySetInnerHTML: {
2304
+ __html: renderMarkdown(conversation.ticket.description)
2305
+ }
2306
+ }
2307
+ ),
2308
+ /* @__PURE__ */ jsx2(MessageAttachments, { messageId: null }),
2309
+ /* @__PURE__ */ jsxs2("div", { class: "sr-message-meta", children: [
2310
+ /* @__PURE__ */ jsx2("span", { class: "sr-message-sender", children: "You" }),
2311
+ /* @__PURE__ */ jsx2("span", { children: "\xB7" }),
2312
+ /* @__PURE__ */ jsx2("span", { children: formatTime(conversation.ticket.createdAt) })
2313
+ ] })
2314
+ ] }),
2315
+ conversation.ticket.suggestedArticles && conversation.ticket.suggestedArticles.length > 0 && /* @__PURE__ */ jsx2(
2316
+ SuggestedArticles,
2317
+ {
2318
+ articles: conversation.ticket.suggestedArticles
2319
+ }
2320
+ ),
2321
+ conversation.messages.map((msg) => /* @__PURE__ */ jsx2(Message, { msg }, msg.id)),
2322
+ /* @__PURE__ */ jsx2("div", { ref: messagesEndRef })
2323
+ ] }),
2324
+ /* @__PURE__ */ jsx2(MessageComposer, { ticketId })
2325
+ ] });
2326
+ }
2327
+
2328
+ // src/widget/Launcher.tsx
2329
+ import { jsx as jsx3, jsxs as jsxs3 } from "preact/jsx-runtime";
2330
+ function Launcher({
2331
+ position,
2332
+ isOpen
2333
+ }) {
2334
+ const posClass = position === "bottom-left" ? "left" : "right";
2335
+ return /* @__PURE__ */ jsx3(
2336
+ "button",
2337
+ {
2338
+ class: `sr-launcher ${posClass}`,
2339
+ onClick: toggleOpen,
2340
+ "aria-label": isOpen ? "Close support" : "Open support",
2341
+ children: isOpen ? /* @__PURE__ */ jsx3("svg", { viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsx3("path", { d: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" }) }) : /* @__PURE__ */ jsxs3("svg", { viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: [
2342
+ /* @__PURE__ */ jsx3("path", { d: "M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H5.17L4 17.17V4h16v12z" }),
2343
+ /* @__PURE__ */ jsx3("path", { d: "M7 9h2v2H7zm4 0h2v2h-2zm4 0h2v2h-2z" })
2344
+ ] })
2345
+ }
2346
+ );
2347
+ }
2348
+
2349
+ // src/widget/TicketForm.tsx
2350
+ import { useCallback as useCallback2, useRef as useRef2, useState as useState2 } from "preact/hooks";
2351
+ import { jsx as jsx4, jsxs as jsxs4 } from "preact/jsx-runtime";
2352
+ function TicketForm() {
2353
+ const identity = client_default._getIdentity();
2354
+ const hasIdentity = !!identity.email;
2355
+ const [subject, setSubject] = useState2("");
2356
+ const [description, setDescription] = useState2("");
2357
+ const [email, setEmail] = useState2(identity.email ?? "");
2358
+ const [submitting, setSubmitting] = useState2(false);
2359
+ const [error, setError] = useState2(null);
2360
+ const [success, setSuccess] = useState2(false);
2361
+ const [ticketId, setTicketId] = useState2(null);
2362
+ const [attachments, setAttachments] = useState2([]);
2363
+ const [uploading, setUploading] = useState2(false);
2364
+ const fileInputRef = useRef2(null);
2365
+ const handleFileSelect = useCallback2(
2366
+ async (e) => {
2367
+ const input = e.target;
2368
+ const files = input.files;
2369
+ if (!files) return;
2370
+ for (let i = 0; i < files.length && attachments.length + i < 5; i++) {
2371
+ setUploading(true);
2372
+ try {
2373
+ const result = await uploadFile(files[i]);
2374
+ setAttachments((prev) => [...prev, result]);
2375
+ } catch {
2376
+ setError("File upload failed");
2377
+ } finally {
2378
+ setUploading(false);
2379
+ }
2380
+ }
2381
+ input.value = "";
2382
+ },
2383
+ [attachments.length]
2384
+ );
2385
+ const removeAttachment = useCallback2((storageKey) => {
2386
+ setAttachments((prev) => prev.filter((a) => a.storageKey !== storageKey));
2387
+ }, []);
2388
+ const handleSubmit = useCallback2(
2389
+ async (e) => {
2390
+ e.preventDefault();
2391
+ if (!subject.trim() || !description.trim()) return;
2392
+ setSubmitting(true);
2393
+ setError(null);
2394
+ try {
2395
+ const result = await createTicket({
2396
+ subject: subject.trim(),
2397
+ description: description.trim(),
2398
+ email: email.trim() || void 0,
2399
+ attachments: attachments.length ? attachments : void 0
2400
+ });
2401
+ setSuccess(true);
2402
+ setTicketId(result.id);
2403
+ } catch (err) {
2404
+ setError(err instanceof Error ? err.message : "Something went wrong");
2405
+ } finally {
2406
+ setSubmitting(false);
2407
+ }
2408
+ },
2409
+ [subject, description, email, attachments]
2410
+ );
2411
+ if (success && ticketId) {
2412
+ return /* @__PURE__ */ jsxs4("div", { class: "sr-success", children: [
2413
+ /* @__PURE__ */ jsx4("div", { class: "sr-success-icon", children: /* @__PURE__ */ jsx4("svg", { viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsx4("path", { d: "M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" }) }) }),
2414
+ /* @__PURE__ */ jsx4("h4", { children: "Message sent" }),
2415
+ /* @__PURE__ */ jsx4("p", { children: "We'll get back to you soon." }),
2416
+ /* @__PURE__ */ jsx4("div", { style: { marginTop: "16px" }, children: /* @__PURE__ */ jsx4(
2417
+ "button",
2418
+ {
2419
+ class: "sr-btn sr-btn-primary",
2420
+ onClick: () => openThread(ticketId),
2421
+ children: "View conversation"
2422
+ }
2423
+ ) })
2424
+ ] });
2425
+ }
2426
+ const canSubmit = !!subject.trim() && !!description.trim() && !submitting;
2427
+ return /* @__PURE__ */ jsxs4("form", { class: "sr-form", onSubmit: handleSubmit, children: [
2428
+ /* @__PURE__ */ jsxs4("div", { class: "sr-field", children: [
2429
+ /* @__PURE__ */ jsx4("label", { children: "Subject" }),
2430
+ /* @__PURE__ */ jsx4(
2431
+ "input",
2432
+ {
2433
+ type: "text",
2434
+ value: subject,
2435
+ onInput: (e) => setSubject(e.target.value),
2436
+ placeholder: "Brief summary",
2437
+ maxLength: 500,
2438
+ required: true
2439
+ }
2440
+ )
2441
+ ] }),
2442
+ /* @__PURE__ */ jsxs4("div", { class: "sr-field sr-field-grow", children: [
2443
+ /* @__PURE__ */ jsx4("label", { children: "Description" }),
2444
+ /* @__PURE__ */ jsxs4("div", { class: "sr-textarea-wrap", children: [
2445
+ /* @__PURE__ */ jsx4(
2446
+ "textarea",
2447
+ {
2448
+ value: description,
2449
+ onInput: (e) => setDescription(e.target.value),
2450
+ placeholder: "Describe the issue or request...",
2451
+ maxLength: 1e4,
2452
+ required: true
2453
+ }
2454
+ ),
2455
+ /* @__PURE__ */ jsx4(
2456
+ "button",
2457
+ {
2458
+ type: "button",
2459
+ class: "sr-attach-btn sr-attach-btn-left",
2460
+ onClick: () => fileInputRef.current?.click(),
2461
+ "aria-label": "Attach file",
2462
+ disabled: attachments.length >= 5,
2463
+ children: /* @__PURE__ */ jsx4("svg", { viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsx4(
2464
+ "path",
2465
+ {
2466
+ d: "M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48",
2467
+ fill: "none",
2468
+ stroke: "currentColor",
2469
+ "stroke-width": "2",
2470
+ "stroke-linecap": "round",
2471
+ "stroke-linejoin": "round"
2472
+ }
2473
+ ) })
2474
+ }
2475
+ ),
2476
+ /* @__PURE__ */ jsx4(
2477
+ "button",
2478
+ {
2479
+ type: "submit",
2480
+ class: "sr-send-btn sr-send-btn-abs",
2481
+ disabled: !canSubmit,
2482
+ "aria-label": "Send",
2483
+ children: /* @__PURE__ */ jsx4("svg", { viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsx4("path", { d: "M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z" }) })
2484
+ }
2485
+ )
2486
+ ] }),
2487
+ /* @__PURE__ */ jsx4(
2488
+ "input",
2489
+ {
2490
+ ref: fileInputRef,
2491
+ type: "file",
2492
+ multiple: true,
2493
+ style: { display: "none" },
2494
+ onChange: handleFileSelect
2495
+ }
2496
+ )
2497
+ ] }),
2498
+ (attachments.length > 0 || uploading) && /* @__PURE__ */ jsxs4("div", { class: "sr-attachment-pills", children: [
2499
+ attachments.map((att) => /* @__PURE__ */ jsxs4("div", { class: "sr-attachment-pill", children: [
2500
+ /* @__PURE__ */ jsx4(
2501
+ "svg",
2502
+ {
2503
+ class: "sr-attachment-pill-icon",
2504
+ viewBox: "0 0 24 24",
2505
+ xmlns: "http://www.w3.org/2000/svg",
2506
+ children: /* @__PURE__ */ jsx4(
2507
+ "path",
2508
+ {
2509
+ d: "M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48",
2510
+ fill: "none",
2511
+ stroke: "currentColor",
2512
+ "stroke-width": "2",
2513
+ "stroke-linecap": "round",
2514
+ "stroke-linejoin": "round"
2515
+ }
2516
+ )
2517
+ }
2518
+ ),
2519
+ /* @__PURE__ */ jsx4("span", { class: "sr-attachment-pill-name", children: att.fileName }),
2520
+ /* @__PURE__ */ jsx4(
2521
+ "button",
2522
+ {
2523
+ type: "button",
2524
+ class: "sr-attachment-pill-remove",
2525
+ onClick: () => removeAttachment(att.storageKey),
2526
+ "aria-label": "Remove",
2527
+ children: /* @__PURE__ */ jsx4("svg", { viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsx4(
2528
+ "path",
2529
+ {
2530
+ d: "M18 6L6 18M6 6l12 12",
2531
+ fill: "none",
2532
+ stroke: "currentColor",
2533
+ "stroke-width": "2",
2534
+ "stroke-linecap": "round"
2535
+ }
2536
+ ) })
2537
+ }
2538
+ )
2539
+ ] }, att.storageKey)),
2540
+ uploading && /* @__PURE__ */ jsx4("div", { class: "sr-attachment-pill", children: /* @__PURE__ */ jsx4("span", { class: "sr-attachment-pill-name", children: "Uploading..." }) })
2541
+ ] }),
2542
+ !hasIdentity && /* @__PURE__ */ jsxs4("div", { class: "sr-field", children: [
2543
+ /* @__PURE__ */ jsx4("label", { children: "Email" }),
2544
+ /* @__PURE__ */ jsx4(
2545
+ "input",
2546
+ {
2547
+ type: "email",
2548
+ value: email,
2549
+ onInput: (e) => setEmail(e.target.value),
2550
+ placeholder: "your@email.com"
2551
+ }
2552
+ )
2553
+ ] }),
2554
+ error && /* @__PURE__ */ jsx4("div", { class: "sr-error", children: error })
2555
+ ] });
2556
+ }
2557
+
2558
+ // src/widget/Widget.tsx
2559
+ import { Fragment, jsx as jsx5, jsxs as jsxs5 } from "preact/jsx-runtime";
2560
+ function BackButton() {
2561
+ return /* @__PURE__ */ jsx5("button", { onClick: goBack, "aria-label": "Back", children: /* @__PURE__ */ jsx5("svg", { viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsx5("path", { d: "M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" }) }) });
2562
+ }
2563
+ function NewTicketButton() {
2564
+ return /* @__PURE__ */ jsx5("button", { onClick: () => setView("new-ticket"), "aria-label": "New conversation", children: /* @__PURE__ */ jsx5("svg", { viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsx5("path", { d: "M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" }) }) });
2565
+ }
2566
+ function Widget({ options }) {
2567
+ const [, setTick] = useState3(0);
2568
+ const position = options.position ?? "bottom-right";
2569
+ const title = options.labels?.title ?? "How can we help?";
2570
+ const posClass = position === "bottom-left" ? "left" : "right";
2571
+ useEffect3(() => {
2572
+ return subscribe(() => setTick((t) => t + 1));
2573
+ }, []);
2574
+ const state2 = getState();
2575
+ const threadTitle = state2.activeConversation?.ticket.subject ?? state2.tickets.find((t) => t.id === state2.activeTicketId)?.subject ?? "Conversation";
2576
+ const viewTitles = {
2577
+ conversations: title,
2578
+ "new-ticket": options.labels?.newTicket ?? "New conversation",
2579
+ thread: threadTitle
2580
+ };
2581
+ return /* @__PURE__ */ jsxs5(Fragment, { children: [
2582
+ /* @__PURE__ */ jsx5(Launcher, { position, isOpen: state2.isOpen }),
2583
+ state2.isOpen && /* @__PURE__ */ jsxs5("div", { class: `sr-panel ${posClass}`, children: [
2584
+ /* @__PURE__ */ jsxs5("div", { class: "sr-header", children: [
2585
+ /* @__PURE__ */ jsx5("div", { class: "sr-header-actions", children: (state2.view === "new-ticket" || state2.view === "thread") && /* @__PURE__ */ jsx5(BackButton, {}) }),
2586
+ /* @__PURE__ */ jsx5(
2587
+ "h3",
2588
+ {
2589
+ class: state2.view === "thread" ? "sr-header-title-truncate" : "",
2590
+ children: viewTitles[state2.view]
2591
+ }
2592
+ ),
2593
+ /* @__PURE__ */ jsx5("div", { class: "sr-header-actions", children: state2.view === "conversations" && /* @__PURE__ */ jsx5(NewTicketButton, {}) })
2594
+ ] }),
2595
+ /* @__PURE__ */ jsxs5("div", { class: "sr-body", children: [
2596
+ state2.view === "conversations" && /* @__PURE__ */ jsx5(ConversationList, {}),
2597
+ state2.view === "new-ticket" && /* @__PURE__ */ jsx5(TicketForm, {}),
2598
+ state2.view === "thread" && /* @__PURE__ */ jsx5(ConversationThread, {})
2599
+ ] })
2600
+ ] })
2601
+ ] });
2602
+ }
2603
+
2604
+ // src/widget/index.ts
2605
+ var initialized = false;
2606
+ var hostEl = null;
2607
+ function initWidget(options = {}) {
2608
+ if (initialized) return;
2609
+ if (typeof document === "undefined") return;
2610
+ initialized = true;
2611
+ hostEl = document.createElement("div");
2612
+ hostEl.id = "scoperat-widget";
2613
+ const shadow = hostEl.attachShadow({ mode: "open" });
2614
+ const styleEl = document.createElement("style");
2615
+ styleEl.textContent = getStyles(options.theme, options.darkMode);
2616
+ shadow.appendChild(styleEl);
2617
+ const mountPoint = document.createElement("div");
2618
+ shadow.appendChild(mountPoint);
2619
+ document.body.appendChild(hostEl);
2620
+ render(h(Widget, { options }), mountPoint);
2621
+ }
2622
+ function destroyWidget() {
2623
+ if (!initialized || !hostEl) return;
2624
+ render(null, hostEl.shadowRoot.lastElementChild);
2625
+ hostEl.remove();
2626
+ hostEl = null;
2627
+ initialized = false;
2628
+ }
2629
+ export {
2630
+ destroyWidget,
2631
+ initWidget
2632
+ };
2633
+ //# sourceMappingURL=index.js.map