@scoperat/tracker 0.1.1 → 0.3.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,2780 @@
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 USER_ID_KEY = "__scoperat_user_id";
1208
+ var SESSION_START_EVENT = "$session_start";
1209
+ var SESSION_END_EVENT = "$session_end";
1210
+ var TICKET_SESSION_KEY = "__scoperat_ticket_session";
1211
+ function generateAnonId() {
1212
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
1213
+ return crypto.randomUUID();
1214
+ }
1215
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
1216
+ const r = Math.random() * 16 | 0;
1217
+ const v = c === "x" ? r : r & 3 | 8;
1218
+ return v.toString(16);
1219
+ });
1220
+ }
1221
+ function getOrCreateAnonId() {
1222
+ if (typeof window === "undefined" || typeof localStorage === "undefined") {
1223
+ return void 0;
1224
+ }
1225
+ try {
1226
+ let id = localStorage.getItem(ANON_ID_KEY);
1227
+ if (!id) {
1228
+ id = generateAnonId();
1229
+ localStorage.setItem(ANON_ID_KEY, id);
1230
+ }
1231
+ return id;
1232
+ } catch {
1233
+ return generateAnonId();
1234
+ }
1235
+ }
1236
+ var TrackerImpl = class {
1237
+ writeKey = null;
1238
+ endpoint = "";
1239
+ flushAt = DEFAULT_FLUSH_AT;
1240
+ flushInterval = DEFAULT_FLUSH_INTERVAL;
1241
+ timeout = DEFAULT_TIMEOUT;
1242
+ onError;
1243
+ buffer = [];
1244
+ timer = null;
1245
+ visibilityHandler = null;
1246
+ userId;
1247
+ orgId;
1248
+ traits = {};
1249
+ userHash;
1250
+ anonymousId;
1251
+ sessionId;
1252
+ sessionTimeout = DEFAULT_SESSION_TIMEOUT;
1253
+ ticketSessionFetch = null;
1254
+ _flagClient = null;
1255
+ init(writeKey, options = {}) {
1256
+ this.writeKey = writeKey;
1257
+ this.endpoint = (options.endpoint ?? globalThis.location?.origin ?? "").replace(/\/+$/, "");
1258
+ this.flushAt = options.flushAt ?? DEFAULT_FLUSH_AT;
1259
+ this.flushInterval = options.flushInterval ?? DEFAULT_FLUSH_INTERVAL;
1260
+ this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
1261
+ this.onError = options.onError;
1262
+ this.sessionTimeout = options.sessionTimeout ?? DEFAULT_SESSION_TIMEOUT;
1263
+ this.anonymousId = getOrCreateAnonId();
1264
+ if (this.timer) clearInterval(this.timer);
1265
+ this.timer = setInterval(() => {
1266
+ if (this.buffer.length > 0) {
1267
+ this.flush().catch(() => {
1268
+ });
1269
+ }
1270
+ }, this.flushInterval);
1271
+ if (typeof window !== "undefined") {
1272
+ if (this.visibilityHandler) {
1273
+ window.removeEventListener("visibilitychange", this.visibilityHandler);
1274
+ }
1275
+ this.visibilityHandler = () => {
1276
+ if (document.visibilityState === "hidden" && this.buffer.length > 0) {
1277
+ this.flushSync();
1278
+ }
1279
+ };
1280
+ window.addEventListener("visibilitychange", this.visibilityHandler);
1281
+ }
1282
+ if (options.captureErrors) {
1283
+ installGlobalHandlers(this, getBreadcrumbs);
1284
+ startBreadcrumbCollection();
1285
+ }
1286
+ this._flagClient?.destroy();
1287
+ this._flagClient = new FlagClient({
1288
+ endpoint: this.endpoint,
1289
+ writeKey,
1290
+ timeout: this.timeout,
1291
+ environment: options.environment,
1292
+ pollInterval: options.flagPollInterval,
1293
+ getIdentity: () => ({
1294
+ userId: this.userId,
1295
+ anonymousId: this.anonymousId,
1296
+ orgId: this.orgId
1297
+ }),
1298
+ onEvaluation: (key, flag) => this.track("$flag_evaluation", {
1299
+ flag_key: key,
1300
+ flag_value: flag.value,
1301
+ flag_type: flag.type,
1302
+ flag_source: flag.source
1303
+ })
1304
+ });
1305
+ }
1306
+ /** The underlying flag evaluation client. Use for direct SDK integration. */
1307
+ get flags() {
1308
+ if (!this._flagClient) {
1309
+ throw new Error("Tracker not initialized. Call init() first.");
1310
+ }
1311
+ return this._flagClient;
1312
+ }
1313
+ identify(userId, traits, options) {
1314
+ const previousUserId = this.userId ?? this.readStoredUserId();
1315
+ this.userId = userId;
1316
+ if (traits) {
1317
+ this.traits = { ...traits };
1318
+ if (traits.orgId) this.orgId = traits.orgId;
1319
+ }
1320
+ this.userHash = options?.userHash;
1321
+ for (const evt of this.buffer) {
1322
+ if (!evt.user_id) evt.user_id = userId;
1323
+ if (!evt.org_id && this.orgId) evt.org_id = this.orgId;
1324
+ }
1325
+ if (userId !== previousUserId) {
1326
+ this.writeStoredUserId(userId);
1327
+ this.startSession("login");
1328
+ }
1329
+ this._flagClient?.refresh();
1330
+ }
1331
+ reset() {
1332
+ this.endSession("logout", true);
1333
+ this.userId = void 0;
1334
+ this.orgId = void 0;
1335
+ this.traits = {};
1336
+ this.userHash = void 0;
1337
+ this.anonymousId = getOrCreateAnonId();
1338
+ this.clearStoredUserId();
1339
+ this.clearTicketSession();
1340
+ }
1341
+ /**
1342
+ * Begin a new analytics session, ending the current one first if it
1343
+ * exists. Emits `$session_end` (for the old session) and
1344
+ * `$session_start` (for the new). Called automatically on login via
1345
+ * {@link identify}; also exposed for explicit control.
1346
+ */
1347
+ startSession(reason = "login") {
1348
+ if (typeof window === "undefined") return this.sessionId;
1349
+ const now = Date.now();
1350
+ const existingId = this.readStoredSession();
1351
+ if (existingId) {
1352
+ this.emitSessionEvent(SESSION_END_EVENT, existingId, reason);
1353
+ }
1354
+ const newId = generateAnonId();
1355
+ this.persistSession(newId, now);
1356
+ this.emitSessionEvent(SESSION_START_EVENT, newId, reason);
1357
+ return newId;
1358
+ }
1359
+ /**
1360
+ * End the current analytics session (if any), emitting `$session_end`,
1361
+ * and clear the stored session. The next tracked event will lazily
1362
+ * start a fresh session. Called automatically on logout via
1363
+ * {@link reset}; also exposed for explicit control.
1364
+ *
1365
+ * @param immediate - flush buffered events synchronously (via
1366
+ * `sendBeacon`) so the end event isn't lost if the page unloads.
1367
+ */
1368
+ endSession(reason = "logout", immediate = false) {
1369
+ if (typeof window === "undefined") return;
1370
+ const existingId = this.readStoredSession();
1371
+ if (existingId) {
1372
+ this.emitSessionEvent(SESSION_END_EVENT, existingId, reason);
1373
+ }
1374
+ this.sessionId = void 0;
1375
+ this.clearSession();
1376
+ if (immediate) {
1377
+ if (typeof navigator === "undefined" || !navigator.sendBeacon) {
1378
+ this.flush().catch(() => {
1379
+ });
1380
+ } else {
1381
+ this.flushSync();
1382
+ }
1383
+ }
1384
+ }
1385
+ readStoredUserId() {
1386
+ if (typeof localStorage === "undefined") return void 0;
1387
+ try {
1388
+ return localStorage.getItem(USER_ID_KEY) ?? void 0;
1389
+ } catch {
1390
+ return void 0;
1391
+ }
1392
+ }
1393
+ writeStoredUserId(userId) {
1394
+ if (typeof localStorage === "undefined") return;
1395
+ try {
1396
+ localStorage.setItem(USER_ID_KEY, userId);
1397
+ } catch {
1398
+ }
1399
+ }
1400
+ clearStoredUserId() {
1401
+ if (typeof localStorage === "undefined") return;
1402
+ try {
1403
+ localStorage.removeItem(USER_ID_KEY);
1404
+ } catch {
1405
+ }
1406
+ }
1407
+ clearTicketSession() {
1408
+ this.ticketSessionFetch = null;
1409
+ if (typeof localStorage === "undefined") return;
1410
+ try {
1411
+ localStorage.removeItem(TICKET_SESSION_KEY);
1412
+ } catch {
1413
+ }
1414
+ }
1415
+ readStoredTicketSession() {
1416
+ if (typeof localStorage === "undefined") return null;
1417
+ try {
1418
+ const raw = localStorage.getItem(TICKET_SESSION_KEY);
1419
+ if (!raw) return null;
1420
+ const parsed = JSON.parse(raw);
1421
+ if (typeof parsed.anonymousId === "string" && typeof parsed.token === "string" && typeof parsed.expiresAt === "number" && parsed.expiresAt > Date.now() + 6e4) {
1422
+ return parsed;
1423
+ }
1424
+ localStorage.removeItem(TICKET_SESSION_KEY);
1425
+ } catch {
1426
+ }
1427
+ return null;
1428
+ }
1429
+ /**
1430
+ * @internal Used by the widget API helpers. Returns a cached or
1431
+ * newly-minted anonymous session token, or `null` if the server
1432
+ * has anonymous sessions disabled. Coalesces concurrent callers
1433
+ * onto a single in-flight network request so mount-time parallel
1434
+ * API calls don't each trigger a `POST /ticket/session`.
1435
+ */
1436
+ _ensureTicketSession() {
1437
+ const cached = this.readStoredTicketSession();
1438
+ if (cached) return Promise.resolve(cached);
1439
+ if (this.ticketSessionFetch) return this.ticketSessionFetch;
1440
+ if (!this.writeKey) return Promise.resolve(null);
1441
+ this.ticketSessionFetch = (async () => {
1442
+ try {
1443
+ const res = await fetch(`${this.endpoint}/api/ingest/ticket/session`, {
1444
+ method: "POST",
1445
+ headers: {
1446
+ "Content-Type": "application/json",
1447
+ Authorization: `Bearer ${this.writeKey}`
1448
+ },
1449
+ body: "{}",
1450
+ signal: AbortSignal.timeout(this.timeout)
1451
+ });
1452
+ if (!res.ok) return null;
1453
+ const json = await res.json();
1454
+ if (typeof json?.anonymousId === "string" && typeof json?.token === "string" && typeof json?.expiresAt === "number") {
1455
+ try {
1456
+ localStorage?.setItem(TICKET_SESSION_KEY, JSON.stringify(json));
1457
+ } catch {
1458
+ }
1459
+ this.anonymousId = json.anonymousId;
1460
+ return json;
1461
+ }
1462
+ return null;
1463
+ } catch {
1464
+ return null;
1465
+ } finally {
1466
+ this.ticketSessionFetch = null;
1467
+ }
1468
+ })();
1469
+ return this.ticketSessionFetch;
1470
+ }
1471
+ /**
1472
+ * Return the id of the current session, rotating it when the previous
1473
+ * session has expired through inactivity. On rotation it emits a
1474
+ * `$session_end` for the timed-out session (backdated to the last
1475
+ * recorded activity) and a `$session_start` for the new one. Called on
1476
+ * every tracked event so sessions advance with real user activity.
1477
+ */
1478
+ ensureSession(now) {
1479
+ if (typeof window === "undefined") return this.sessionId;
1480
+ try {
1481
+ const lastActivity = this.readLastActivity();
1482
+ const existingId = this.readStoredSession();
1483
+ const expired = lastActivity === void 0 || now - lastActivity > this.sessionTimeout;
1484
+ if (existingId && !expired) {
1485
+ this.sessionId = existingId;
1486
+ try {
1487
+ localStorage.setItem(LAST_ACTIVITY_KEY, String(now));
1488
+ } catch {
1489
+ }
1490
+ return existingId;
1491
+ }
1492
+ if (existingId) {
1493
+ this.emitSessionEvent(
1494
+ SESSION_END_EVENT,
1495
+ existingId,
1496
+ "timeout",
1497
+ lastActivity !== void 0 ? new Date(lastActivity).toISOString() : void 0
1498
+ );
1499
+ }
1500
+ const newId = generateAnonId();
1501
+ this.persistSession(newId, now);
1502
+ this.emitSessionEvent(
1503
+ SESSION_START_EVENT,
1504
+ newId,
1505
+ existingId ? "timeout" : "initial"
1506
+ );
1507
+ return newId;
1508
+ } catch {
1509
+ return this.sessionId ?? generateAnonId();
1510
+ }
1511
+ }
1512
+ readStoredSession() {
1513
+ if (this.sessionId) return this.sessionId;
1514
+ if (typeof sessionStorage === "undefined") return void 0;
1515
+ try {
1516
+ return sessionStorage.getItem(SESSION_ID_KEY) ?? void 0;
1517
+ } catch {
1518
+ return void 0;
1519
+ }
1520
+ }
1521
+ readLastActivity() {
1522
+ if (typeof localStorage === "undefined") return void 0;
1523
+ try {
1524
+ const raw = localStorage.getItem(LAST_ACTIVITY_KEY);
1525
+ return raw ? Number(raw) : void 0;
1526
+ } catch {
1527
+ return void 0;
1528
+ }
1529
+ }
1530
+ persistSession(id, now) {
1531
+ this.sessionId = id;
1532
+ if (typeof window === "undefined") return;
1533
+ try {
1534
+ sessionStorage.setItem(SESSION_ID_KEY, id);
1535
+ localStorage.setItem(LAST_ACTIVITY_KEY, String(now));
1536
+ } catch {
1537
+ }
1538
+ }
1539
+ /**
1540
+ * Enqueue a session lifecycle event already stamped with its session id
1541
+ * (so it never re-enters {@link ensureSession} and risks recursion).
1542
+ */
1543
+ emitSessionEvent(event, sessionId, reason, occurredAt) {
1544
+ if (!this.writeKey) return;
1545
+ const traitsContext = Object.keys(this.traits).length > 0 ? { traits: this.traits } : {};
1546
+ this.buffer.push({
1547
+ event,
1548
+ properties: { reason },
1549
+ occurred_at: occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
1550
+ source: "browser",
1551
+ org_id: this.orgId,
1552
+ user_id: this.userId,
1553
+ anonymous_id: this.anonymousId,
1554
+ session_id: sessionId,
1555
+ context: {
1556
+ ...getBrowserContext(),
1557
+ ...traitsContext
1558
+ }
1559
+ });
1560
+ }
1561
+ clearSession() {
1562
+ if (typeof window === "undefined") return;
1563
+ try {
1564
+ sessionStorage.removeItem(SESSION_ID_KEY);
1565
+ localStorage.removeItem(LAST_ACTIVITY_KEY);
1566
+ } catch {
1567
+ }
1568
+ }
1569
+ getSessionId() {
1570
+ return this.ensureSession(Date.now());
1571
+ }
1572
+ /** @internal Used by the widget to read tracker config. */
1573
+ _getEndpoint() {
1574
+ return this.endpoint;
1575
+ }
1576
+ /** @internal Used by the widget to read the write key. */
1577
+ _getWriteKey() {
1578
+ return this.writeKey;
1579
+ }
1580
+ /** @internal Used by the widget to read the current user identity. */
1581
+ _getIdentity() {
1582
+ return {
1583
+ userId: this.userId,
1584
+ anonymousId: this.anonymousId,
1585
+ email: this.traits.email,
1586
+ name: this.traits.name,
1587
+ userHash: this.userHash
1588
+ };
1589
+ }
1590
+ captureException(error, context) {
1591
+ if (!this.writeKey) return;
1592
+ const breadcrumbs = getBreadcrumbs();
1593
+ this.track("$exception", {
1594
+ type: error.name || "Error",
1595
+ message: error.message,
1596
+ stack: error.stack,
1597
+ handled: true,
1598
+ mechanism: context?.mechanism ?? "manual",
1599
+ source: "browser",
1600
+ ...context,
1601
+ context: { breadcrumbs }
1602
+ });
1603
+ }
1604
+ addBreadcrumb(category, message, data) {
1605
+ addBreadcrumb({
1606
+ type: "custom",
1607
+ category,
1608
+ message,
1609
+ timestamp: Date.now(),
1610
+ data
1611
+ });
1612
+ }
1613
+ track(eventOrName, properties) {
1614
+ if (!this.writeKey) return;
1615
+ const evt = typeof eventOrName === "string" ? { event: eventOrName, properties } : eventOrName;
1616
+ const traitsContext = Object.keys(this.traits).length > 0 ? { traits: this.traits } : {};
1617
+ const enriched = {
1618
+ ...evt,
1619
+ occurred_at: evt.occurred_at ?? (/* @__PURE__ */ new Date()).toISOString(),
1620
+ source: evt.source ?? "browser",
1621
+ org_id: evt.org_id ?? this.orgId,
1622
+ user_id: evt.user_id ?? this.userId,
1623
+ anonymous_id: evt.anonymous_id ?? this.anonymousId,
1624
+ session_id: evt.session_id ?? this.ensureSession(Date.now()),
1625
+ context: {
1626
+ ...getBrowserContext(),
1627
+ ...traitsContext,
1628
+ ...evt.context
1629
+ }
1630
+ };
1631
+ this.buffer.push(enriched);
1632
+ if (this.buffer.length >= this.flushAt) {
1633
+ this.flush().catch(() => {
1634
+ });
1635
+ }
1636
+ }
1637
+ async flush() {
1638
+ if (!this.writeKey || this.buffer.length === 0) {
1639
+ return { success: true, ingested: 0 };
1640
+ }
1641
+ const batch = this.buffer.splice(0, MAX_BATCH_SIZE);
1642
+ try {
1643
+ const res = await fetch(`${this.endpoint}/api/ingest/track`, {
1644
+ method: "POST",
1645
+ headers: {
1646
+ "Content-Type": "application/json",
1647
+ Authorization: `Bearer ${this.writeKey}`
1648
+ },
1649
+ body: JSON.stringify({ events: batch }),
1650
+ signal: AbortSignal.timeout(this.timeout)
1651
+ });
1652
+ if (!res.ok) {
1653
+ const body = await res.text().catch(() => "");
1654
+ throw new Error(`API returned ${res.status}: ${body}`);
1655
+ }
1656
+ const json = await res.json();
1657
+ return { success: true, ingested: json.ingested };
1658
+ } catch (error) {
1659
+ this.buffer.unshift(...batch);
1660
+ const err = error instanceof Error ? error : new Error(String(error));
1661
+ this.onError?.(err, batch);
1662
+ return { success: false, ingested: 0 };
1663
+ }
1664
+ }
1665
+ flushSync() {
1666
+ if (!this.writeKey) return;
1667
+ if (typeof navigator === "undefined" || !navigator.sendBeacon) return;
1668
+ if (this.buffer.length === 0) return;
1669
+ const batch = this.buffer.splice(0, MAX_BATCH_SIZE);
1670
+ const payload = JSON.stringify({ events: batch });
1671
+ const url = `${this.endpoint}/api/ingest/track?key=${encodeURIComponent(this.writeKey)}`;
1672
+ try {
1673
+ const blob = new Blob([payload], { type: "application/json" });
1674
+ navigator.sendBeacon(url, blob);
1675
+ } catch {
1676
+ }
1677
+ }
1678
+ getFlag(key, defaultValue) {
1679
+ return this._flagClient?.getFlag(key, defaultValue) ?? defaultValue;
1680
+ }
1681
+ onFlagsReady(callback) {
1682
+ this._flagClient?.onFlagsReady(callback);
1683
+ }
1684
+ onFlagChange(listener) {
1685
+ return this._flagClient?.onFlagChange(listener) ?? (() => {
1686
+ });
1687
+ }
1688
+ async refreshFlags() {
1689
+ await this._flagClient?.refresh();
1690
+ }
1691
+ async shutdown() {
1692
+ if (this.timer) {
1693
+ clearInterval(this.timer);
1694
+ this.timer = null;
1695
+ }
1696
+ this._flagClient?.destroy();
1697
+ this._flagClient = null;
1698
+ if (typeof window !== "undefined" && this.visibilityHandler) {
1699
+ window.removeEventListener("visibilitychange", this.visibilityHandler);
1700
+ this.visibilityHandler = null;
1701
+ }
1702
+ while (this.buffer.length > 0) {
1703
+ await this.flush();
1704
+ }
1705
+ }
1706
+ };
1707
+ var scoperat = new TrackerImpl();
1708
+ var client_default = scoperat;
1709
+
1710
+ // src/widget/api.ts
1711
+ async function identityHeaders() {
1712
+ const identity = client_default._getIdentity();
1713
+ if (identity.userId && identity.userHash) {
1714
+ return {
1715
+ "X-Scoperat-User-Hash": identity.userHash,
1716
+ "X-Scoperat-User-Id": identity.userId
1717
+ };
1718
+ }
1719
+ const session = await client_default._ensureTicketSession();
1720
+ if (session) {
1721
+ return { "X-Scoperat-Session": session.token };
1722
+ }
1723
+ return {};
1724
+ }
1725
+ async function identityQueryParams() {
1726
+ const params = new URLSearchParams();
1727
+ const identity = client_default._getIdentity();
1728
+ if (identity.userId && identity.userHash) {
1729
+ params.set("user_id", identity.userId);
1730
+ params.set("user_hash", identity.userHash);
1731
+ return params;
1732
+ }
1733
+ const session = await client_default._ensureTicketSession();
1734
+ if (session) {
1735
+ params.set("session", session.token);
1736
+ }
1737
+ return params;
1738
+ }
1739
+ async function jsonHeaders() {
1740
+ const key = client_default._getWriteKey();
1741
+ return {
1742
+ "Content-Type": "application/json",
1743
+ ...key ? { Authorization: `Bearer ${key}` } : {},
1744
+ ...await identityHeaders()
1745
+ };
1746
+ }
1747
+ async function authHeaders() {
1748
+ const key = client_default._getWriteKey();
1749
+ return {
1750
+ ...key ? { Authorization: `Bearer ${key}` } : {},
1751
+ ...await identityHeaders()
1752
+ };
1753
+ }
1754
+ function baseUrl() {
1755
+ return client_default._getEndpoint();
1756
+ }
1757
+ async function uploadFile(file) {
1758
+ const form = new FormData();
1759
+ form.append("file", file);
1760
+ const res = await fetch(`${baseUrl()}/api/ingest/ticket/upload`, {
1761
+ method: "POST",
1762
+ headers: await authHeaders(),
1763
+ body: form
1764
+ });
1765
+ if (!res.ok) {
1766
+ const text = await res.text().catch(() => "");
1767
+ throw new Error(`Upload failed: ${res.status} ${text}`);
1768
+ }
1769
+ return res.json();
1770
+ }
1771
+ async function createTicket(data) {
1772
+ const identity = client_default._getIdentity();
1773
+ const context = getBrowserContext();
1774
+ const res = await fetch(`${baseUrl()}/api/ingest/ticket`, {
1775
+ method: "POST",
1776
+ headers: await jsonHeaders(),
1777
+ body: JSON.stringify({
1778
+ ...data,
1779
+ attachments: data.attachments?.map((a) => ({
1780
+ storageKey: a.storageKey,
1781
+ fileName: a.fileName,
1782
+ mimeType: a.mimeType,
1783
+ fileSize: a.size
1784
+ })),
1785
+ email: data.email ?? identity.email,
1786
+ name: data.name ?? identity.name,
1787
+ metadata: {
1788
+ ...context,
1789
+ ...data.metadata
1790
+ }
1791
+ })
1792
+ });
1793
+ if (!res.ok) {
1794
+ const body = await res.text().catch(() => "");
1795
+ throw new Error(`Failed to create ticket: ${res.status} ${body}`);
1796
+ }
1797
+ return res.json();
1798
+ }
1799
+ async function sendMessage(ticketId, body, attachments) {
1800
+ const res = await fetch(`${baseUrl()}/api/ingest/ticket/message`, {
1801
+ method: "POST",
1802
+ headers: await jsonHeaders(),
1803
+ body: JSON.stringify({
1804
+ ticketId,
1805
+ body,
1806
+ attachments: attachments?.map((a) => ({
1807
+ storageKey: a.storageKey,
1808
+ fileName: a.fileName,
1809
+ mimeType: a.mimeType,
1810
+ fileSize: a.size
1811
+ }))
1812
+ })
1813
+ });
1814
+ if (!res.ok) {
1815
+ const text = await res.text().catch(() => "");
1816
+ throw new Error(`Failed to send message: ${res.status} ${text}`);
1817
+ }
1818
+ return res.json();
1819
+ }
1820
+ async function fetchTickets() {
1821
+ const params = await identityQueryParams();
1822
+ const key = client_default._getWriteKey();
1823
+ if (key) params.set("key", key);
1824
+ const res = await fetch(
1825
+ `${baseUrl()}/api/ingest/tickets?${params.toString()}`,
1826
+ { headers: await authHeaders() }
1827
+ );
1828
+ if (!res.ok) return [];
1829
+ const data = await res.json();
1830
+ return data.tickets;
1831
+ }
1832
+ async function fetchConversation(ticketId) {
1833
+ const params = await identityQueryParams();
1834
+ const res = await fetch(
1835
+ `${baseUrl()}/api/ingest/ticket/${ticketId}?${params.toString()}`,
1836
+ { headers: await authHeaders() }
1837
+ );
1838
+ if (!res.ok) {
1839
+ throw new Error(`Failed to fetch conversation: ${res.status}`);
1840
+ }
1841
+ return res.json();
1842
+ }
1843
+ async function fetchPublicKbPage(workspaceSlug, pageSlug) {
1844
+ try {
1845
+ const res = await fetch(`${baseUrl()}/api/kb/${workspaceSlug}/${pageSlug}`);
1846
+ if (!res.ok) return null;
1847
+ return res.json();
1848
+ } catch {
1849
+ return null;
1850
+ }
1851
+ }
1852
+
1853
+ // src/widget/state.ts
1854
+ var listeners = /* @__PURE__ */ new Set();
1855
+ var state = {
1856
+ isOpen: false,
1857
+ view: "conversations",
1858
+ tickets: [],
1859
+ activeTicketId: null,
1860
+ activeConversation: null,
1861
+ loading: false,
1862
+ error: null
1863
+ };
1864
+ function notify() {
1865
+ for (const fn of listeners) fn();
1866
+ }
1867
+ function getState() {
1868
+ return state;
1869
+ }
1870
+ function subscribe(fn) {
1871
+ listeners.add(fn);
1872
+ return () => listeners.delete(fn);
1873
+ }
1874
+ function setState(partial) {
1875
+ state = { ...state, ...partial };
1876
+ notify();
1877
+ }
1878
+ function toggleOpen() {
1879
+ setState({ isOpen: !state.isOpen });
1880
+ }
1881
+ function setView(view) {
1882
+ setState({ view });
1883
+ }
1884
+ function openThread(ticketId) {
1885
+ setState({ view: "thread", activeTicketId: ticketId });
1886
+ }
1887
+ function goBack() {
1888
+ setState({
1889
+ view: "conversations",
1890
+ activeTicketId: null,
1891
+ activeConversation: null
1892
+ });
1893
+ }
1894
+
1895
+ // src/widget/ConversationList.tsx
1896
+ import { jsx, jsxs } from "preact/jsx-runtime";
1897
+ function timeAgo(dateStr) {
1898
+ const seconds = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1e3);
1899
+ if (seconds < 60) return "just now";
1900
+ const minutes = Math.floor(seconds / 60);
1901
+ if (minutes < 60) return `${minutes}m ago`;
1902
+ const hours = Math.floor(minutes / 60);
1903
+ if (hours < 24) return `${hours}h ago`;
1904
+ const days = Math.floor(hours / 24);
1905
+ return `${days}d ago`;
1906
+ }
1907
+ function TicketItem({ ticket }) {
1908
+ return /* @__PURE__ */ jsxs("li", { class: "sr-ticket-item", onClick: () => openThread(ticket.id), children: [
1909
+ /* @__PURE__ */ jsx("h4", { children: ticket.subject }),
1910
+ /* @__PURE__ */ jsx("div", { class: "sr-meta", children: /* @__PURE__ */ jsx("span", { children: timeAgo(ticket.lastMessageAt ?? ticket.createdAt) }) })
1911
+ ] });
1912
+ }
1913
+ function ConversationList() {
1914
+ const state2 = getState();
1915
+ useEffect(() => {
1916
+ setState({ loading: true });
1917
+ fetchTickets().then((tickets) => setState({ tickets, loading: false })).catch(() => setState({ loading: false }));
1918
+ }, []);
1919
+ if (state2.loading) {
1920
+ return /* @__PURE__ */ jsx("div", { class: "sr-loading", children: "Loading conversations..." });
1921
+ }
1922
+ return /* @__PURE__ */ jsx("div", { children: state2.tickets.length === 0 ? /* @__PURE__ */ jsxs("div", { class: "sr-empty", children: [
1923
+ /* @__PURE__ */ jsx("p", { children: "No conversations yet" }),
1924
+ /* @__PURE__ */ jsx(
1925
+ "button",
1926
+ {
1927
+ class: "sr-btn sr-btn-secondary",
1928
+ onClick: () => setView("new-ticket"),
1929
+ children: "Start one"
1930
+ }
1931
+ )
1932
+ ] }) : /* @__PURE__ */ jsx("ul", { class: "sr-ticket-list", children: state2.tickets.map((t) => /* @__PURE__ */ jsx(TicketItem, { ticket: t }, t.id)) }) });
1933
+ }
1934
+
1935
+ // src/widget/ConversationThread.tsx
1936
+ import DOMPurify from "dompurify";
1937
+ import { Marked } from "marked";
1938
+ import {
1939
+ useCallback,
1940
+ useEffect as useEffect2,
1941
+ useMemo,
1942
+ useRef,
1943
+ useState
1944
+ } from "preact/hooks";
1945
+ import { jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
1946
+ var marked = new Marked({ async: false, gfm: true, breaks: true });
1947
+ if (typeof window !== "undefined") {
1948
+ DOMPurify.addHook("afterSanitizeAttributes", (node) => {
1949
+ if (node.tagName === "A") {
1950
+ node.setAttribute("target", "_blank");
1951
+ node.setAttribute("rel", "noopener noreferrer");
1952
+ }
1953
+ });
1954
+ }
1955
+ function renderMarkdown(text) {
1956
+ const rawHtml = marked.parse(text);
1957
+ return DOMPurify.sanitize(rawHtml, { USE_PROFILES: { html: true } });
1958
+ }
1959
+ function formatTime(dateStr) {
1960
+ const d = new Date(dateStr);
1961
+ const now = /* @__PURE__ */ new Date();
1962
+ const sameDay = d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate();
1963
+ if (sameDay) {
1964
+ return d.toLocaleTimeString(void 0, {
1965
+ hour: "numeric",
1966
+ minute: "2-digit"
1967
+ });
1968
+ }
1969
+ return d.toLocaleDateString(void 0, {
1970
+ month: "short",
1971
+ day: "numeric",
1972
+ hour: "numeric",
1973
+ minute: "2-digit"
1974
+ });
1975
+ }
1976
+ function Avatar({
1977
+ name,
1978
+ imageUrl
1979
+ }) {
1980
+ if (imageUrl) {
1981
+ return /* @__PURE__ */ jsx2("div", { class: "sr-avatar", children: /* @__PURE__ */ jsx2("img", { src: imageUrl, alt: name }) });
1982
+ }
1983
+ return /* @__PURE__ */ jsx2("div", { class: "sr-avatar", children: name.charAt(0).toUpperCase() });
1984
+ }
1985
+ function formatFileSize(bytes) {
1986
+ if (bytes < 1024) return `${bytes} B`;
1987
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1988
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1989
+ }
1990
+ function FilePreview({ attachment }) {
1991
+ const isImage = attachment.mimeType.startsWith("image/");
1992
+ return /* @__PURE__ */ jsxs2("div", { class: "sr-file-preview", children: [
1993
+ isImage && attachment.url && /* @__PURE__ */ jsx2(
1994
+ "a",
1995
+ {
1996
+ href: attachment.url,
1997
+ target: "_blank",
1998
+ rel: "noopener noreferrer",
1999
+ class: "sr-file-preview-image",
2000
+ children: /* @__PURE__ */ jsx2("img", { src: attachment.url, alt: attachment.fileName })
2001
+ }
2002
+ ),
2003
+ /* @__PURE__ */ jsxs2("div", { class: "sr-file-preview-row", children: [
2004
+ /* @__PURE__ */ jsxs2(
2005
+ "svg",
2006
+ {
2007
+ class: "sr-file-preview-icon",
2008
+ viewBox: "0 0 24 24",
2009
+ xmlns: "http://www.w3.org/2000/svg",
2010
+ children: [
2011
+ /* @__PURE__ */ jsx2(
2012
+ "path",
2013
+ {
2014
+ d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z",
2015
+ fill: "none",
2016
+ stroke: "currentColor",
2017
+ "stroke-width": "2",
2018
+ "stroke-linecap": "round",
2019
+ "stroke-linejoin": "round"
2020
+ }
2021
+ ),
2022
+ /* @__PURE__ */ jsx2(
2023
+ "polyline",
2024
+ {
2025
+ points: "14 2 14 8 20 8",
2026
+ fill: "none",
2027
+ stroke: "currentColor",
2028
+ "stroke-width": "2",
2029
+ "stroke-linecap": "round",
2030
+ "stroke-linejoin": "round"
2031
+ }
2032
+ )
2033
+ ]
2034
+ }
2035
+ ),
2036
+ /* @__PURE__ */ jsxs2("div", { class: "sr-file-preview-info", children: [
2037
+ attachment.url ? /* @__PURE__ */ jsx2(
2038
+ "a",
2039
+ {
2040
+ href: attachment.url,
2041
+ target: "_blank",
2042
+ rel: "noopener noreferrer",
2043
+ class: "sr-file-preview-name",
2044
+ children: attachment.fileName
2045
+ }
2046
+ ) : /* @__PURE__ */ jsx2("span", { class: "sr-file-preview-name", children: attachment.fileName }),
2047
+ /* @__PURE__ */ jsx2("span", { class: "sr-file-preview-size", children: formatFileSize(attachment.fileSize) })
2048
+ ] }),
2049
+ attachment.url && /* @__PURE__ */ jsx2(
2050
+ "a",
2051
+ {
2052
+ href: attachment.url,
2053
+ download: attachment.fileName,
2054
+ target: "_blank",
2055
+ rel: "noopener noreferrer",
2056
+ class: "sr-file-preview-download",
2057
+ "aria-label": "Download",
2058
+ children: /* @__PURE__ */ jsx2("svg", { viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsx2(
2059
+ "path",
2060
+ {
2061
+ d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3",
2062
+ fill: "none",
2063
+ stroke: "currentColor",
2064
+ "stroke-width": "2",
2065
+ "stroke-linecap": "round",
2066
+ "stroke-linejoin": "round"
2067
+ }
2068
+ ) })
2069
+ }
2070
+ )
2071
+ ] })
2072
+ ] });
2073
+ }
2074
+ function MessageAttachments({ messageId }) {
2075
+ const state2 = getState();
2076
+ const allAttachments = state2.activeConversation?.attachments ?? [];
2077
+ const msgAttachments = allAttachments.filter(
2078
+ (a) => a.messageId === messageId
2079
+ );
2080
+ if (msgAttachments.length === 0) return null;
2081
+ return /* @__PURE__ */ jsx2("div", { class: "sr-message-attachments", children: msgAttachments.map((att) => /* @__PURE__ */ jsx2(FilePreview, { attachment: att }, att.id)) });
2082
+ }
2083
+ function LinkPreviewCard({ preview }) {
2084
+ let hostname = "";
2085
+ try {
2086
+ hostname = new URL(preview.url).hostname.replace(/^www\./, "");
2087
+ } catch {
2088
+ }
2089
+ return /* @__PURE__ */ jsxs2(
2090
+ "a",
2091
+ {
2092
+ class: "sr-link-preview",
2093
+ href: preview.url,
2094
+ target: "_blank",
2095
+ rel: "noopener noreferrer",
2096
+ children: [
2097
+ preview.image && /* @__PURE__ */ jsx2("div", { class: "sr-link-preview-img", children: /* @__PURE__ */ jsx2(
2098
+ "img",
2099
+ {
2100
+ src: preview.image,
2101
+ alt: "",
2102
+ loading: "lazy",
2103
+ onError: (e) => {
2104
+ const parent = e.currentTarget.parentElement;
2105
+ if (parent) parent.style.display = "none";
2106
+ }
2107
+ }
2108
+ ) }),
2109
+ /* @__PURE__ */ jsxs2("div", { class: "sr-link-preview-body", children: [
2110
+ /* @__PURE__ */ jsxs2("div", { class: "sr-link-preview-site", children: [
2111
+ preview.favicon ? /* @__PURE__ */ jsx2(
2112
+ "img",
2113
+ {
2114
+ class: "sr-link-preview-favicon",
2115
+ src: preview.favicon,
2116
+ alt: "",
2117
+ onError: (e) => {
2118
+ e.currentTarget.style.display = "none";
2119
+ }
2120
+ }
2121
+ ) : null,
2122
+ /* @__PURE__ */ jsx2("span", { children: preview.siteName ?? hostname })
2123
+ ] }),
2124
+ preview.title && /* @__PURE__ */ jsx2("p", { class: "sr-link-preview-title", children: preview.title }),
2125
+ preview.description && /* @__PURE__ */ jsx2("p", { class: "sr-link-preview-desc", children: preview.description })
2126
+ ] })
2127
+ ]
2128
+ }
2129
+ );
2130
+ }
2131
+ function Message({ msg }) {
2132
+ const isUser = msg.senderType === "user";
2133
+ const senderName = msg.senderType === "agent" ? msg.agentName ?? msg.senderName ?? "Support" : null;
2134
+ const displayName = isUser ? "You" : senderName ?? "Support";
2135
+ const html = useMemo(() => renderMarkdown(msg.body), [msg.body]);
2136
+ const isAttachmentOnly = msg.body === "(attachment)";
2137
+ const messageArticles = useMemo(() => {
2138
+ const raw = msg.metadata?.articles;
2139
+ if (!Array.isArray(raw) || raw.length === 0) return null;
2140
+ return raw;
2141
+ }, [msg.metadata]);
2142
+ const linkPreview = useMemo(() => {
2143
+ const raw = msg.metadata?.linkPreview;
2144
+ if (!raw || typeof raw !== "object") return null;
2145
+ const candidate = raw;
2146
+ if (typeof candidate.url !== "string") return null;
2147
+ return candidate;
2148
+ }, [msg.metadata]);
2149
+ return /* @__PURE__ */ jsxs2(
2150
+ "div",
2151
+ {
2152
+ class: `sr-message-row ${isUser ? "sr-message-row-user" : "sr-message-row-agent"}`,
2153
+ children: [
2154
+ !isAttachmentOnly && /* @__PURE__ */ jsx2(
2155
+ "div",
2156
+ {
2157
+ class: "sr-message sr-prose",
2158
+ dangerouslySetInnerHTML: { __html: html }
2159
+ }
2160
+ ),
2161
+ /* @__PURE__ */ jsx2(MessageAttachments, { messageId: msg.id }),
2162
+ messageArticles && /* @__PURE__ */ jsx2(SuggestedArticles, { articles: messageArticles }),
2163
+ linkPreview && /* @__PURE__ */ jsx2(LinkPreviewCard, { preview: linkPreview }),
2164
+ /* @__PURE__ */ jsxs2("div", { class: "sr-message-meta", children: [
2165
+ !isUser && /* @__PURE__ */ jsx2(Avatar, { name: displayName, imageUrl: msg.agentAvatarUrl }),
2166
+ /* @__PURE__ */ jsx2("span", { class: "sr-message-sender", children: displayName }),
2167
+ /* @__PURE__ */ jsx2("span", { children: "\xB7" }),
2168
+ /* @__PURE__ */ jsx2("span", { children: formatTime(msg.createdAt) })
2169
+ ] })
2170
+ ]
2171
+ }
2172
+ );
2173
+ }
2174
+ function MessageComposer({ ticketId }) {
2175
+ const [body, setBody] = useState("");
2176
+ const [sending, setSending] = useState(false);
2177
+ const [attachments, setAttachments] = useState([]);
2178
+ const [uploading, setUploading] = useState(false);
2179
+ const fileInputRef = useRef(null);
2180
+ const handleFileSelect = useCallback(
2181
+ async (e) => {
2182
+ const input = e.target;
2183
+ const files = input.files;
2184
+ if (!files) return;
2185
+ for (let i = 0; i < files.length && attachments.length + i < 5; i++) {
2186
+ setUploading(true);
2187
+ try {
2188
+ const result = await uploadFile(files[i]);
2189
+ setAttachments((prev) => [...prev, result]);
2190
+ } catch {
2191
+ } finally {
2192
+ setUploading(false);
2193
+ }
2194
+ }
2195
+ input.value = "";
2196
+ },
2197
+ [attachments.length]
2198
+ );
2199
+ const handleSend = useCallback(async () => {
2200
+ if (!body.trim() && attachments.length === 0 || sending) return;
2201
+ setSending(true);
2202
+ try {
2203
+ await sendMessage(
2204
+ ticketId,
2205
+ body.trim() || "(attachment)",
2206
+ attachments.length ? attachments : void 0
2207
+ );
2208
+ setBody("");
2209
+ setAttachments([]);
2210
+ const conv = await fetchConversation(ticketId);
2211
+ setState({ activeConversation: conv });
2212
+ } catch {
2213
+ } finally {
2214
+ setSending(false);
2215
+ }
2216
+ }, [body, sending, ticketId, attachments]);
2217
+ const handleKeyDown = useCallback(
2218
+ (e) => {
2219
+ if (e.key === "Enter" && !e.shiftKey) {
2220
+ e.preventDefault();
2221
+ handleSend();
2222
+ }
2223
+ },
2224
+ [handleSend]
2225
+ );
2226
+ const canSend = (!!body.trim() || attachments.length > 0) && !sending;
2227
+ return /* @__PURE__ */ jsxs2("div", { class: "sr-composer", children: [
2228
+ (attachments.length > 0 || uploading) && /* @__PURE__ */ jsxs2("div", { class: "sr-attachment-pills", children: [
2229
+ attachments.map((att) => /* @__PURE__ */ jsxs2("div", { class: "sr-attachment-pill", children: [
2230
+ /* @__PURE__ */ jsx2(
2231
+ "svg",
2232
+ {
2233
+ class: "sr-attachment-pill-icon",
2234
+ viewBox: "0 0 24 24",
2235
+ xmlns: "http://www.w3.org/2000/svg",
2236
+ children: /* @__PURE__ */ jsx2(
2237
+ "path",
2238
+ {
2239
+ 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",
2240
+ fill: "none",
2241
+ stroke: "currentColor",
2242
+ "stroke-width": "2",
2243
+ "stroke-linecap": "round",
2244
+ "stroke-linejoin": "round"
2245
+ }
2246
+ )
2247
+ }
2248
+ ),
2249
+ /* @__PURE__ */ jsx2("span", { class: "sr-attachment-pill-name", children: att.fileName }),
2250
+ /* @__PURE__ */ jsx2(
2251
+ "button",
2252
+ {
2253
+ type: "button",
2254
+ class: "sr-attachment-pill-remove",
2255
+ onClick: () => setAttachments(
2256
+ (prev) => prev.filter((a) => a.storageKey !== att.storageKey)
2257
+ ),
2258
+ "aria-label": "Remove",
2259
+ children: /* @__PURE__ */ jsx2("svg", { viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsx2(
2260
+ "path",
2261
+ {
2262
+ d: "M18 6L6 18M6 6l12 12",
2263
+ fill: "none",
2264
+ stroke: "currentColor",
2265
+ "stroke-width": "2",
2266
+ "stroke-linecap": "round"
2267
+ }
2268
+ ) })
2269
+ }
2270
+ )
2271
+ ] }, att.storageKey)),
2272
+ uploading && /* @__PURE__ */ jsx2("div", { class: "sr-attachment-pill", children: /* @__PURE__ */ jsx2("span", { class: "sr-attachment-pill-name", children: "Uploading..." }) })
2273
+ ] }),
2274
+ /* @__PURE__ */ jsxs2("div", { class: "sr-composer-wrap", children: [
2275
+ /* @__PURE__ */ jsx2(
2276
+ "textarea",
2277
+ {
2278
+ value: body,
2279
+ onInput: (e) => setBody(e.target.value),
2280
+ onKeyDown: handleKeyDown,
2281
+ placeholder: "Ask about this issue...",
2282
+ rows: 3
2283
+ }
2284
+ ),
2285
+ /* @__PURE__ */ jsx2(
2286
+ "button",
2287
+ {
2288
+ type: "button",
2289
+ class: "sr-attach-btn sr-attach-btn-left",
2290
+ onClick: () => fileInputRef.current?.click(),
2291
+ "aria-label": "Attach file",
2292
+ disabled: attachments.length >= 5,
2293
+ children: /* @__PURE__ */ jsx2("svg", { viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsx2(
2294
+ "path",
2295
+ {
2296
+ 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",
2297
+ fill: "none",
2298
+ stroke: "currentColor",
2299
+ "stroke-width": "2",
2300
+ "stroke-linecap": "round",
2301
+ "stroke-linejoin": "round"
2302
+ }
2303
+ ) })
2304
+ }
2305
+ ),
2306
+ /* @__PURE__ */ jsx2(
2307
+ "button",
2308
+ {
2309
+ class: "sr-send-btn sr-send-btn-abs",
2310
+ onClick: handleSend,
2311
+ disabled: !canSend,
2312
+ "aria-label": "Send",
2313
+ 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" }) })
2314
+ }
2315
+ )
2316
+ ] }),
2317
+ /* @__PURE__ */ jsx2(
2318
+ "input",
2319
+ {
2320
+ ref: fileInputRef,
2321
+ type: "file",
2322
+ multiple: true,
2323
+ style: { display: "none" },
2324
+ onChange: handleFileSelect
2325
+ }
2326
+ )
2327
+ ] });
2328
+ }
2329
+ function ArticleCard({ article }) {
2330
+ const [rawContent, setRawContent] = useState(null);
2331
+ const [expanded, setExpanded] = useState(false);
2332
+ const [loading, setLoading] = useState(false);
2333
+ const html = useMemo(() => {
2334
+ if (!rawContent) return "";
2335
+ return renderMarkdown(rawContent);
2336
+ }, [rawContent]);
2337
+ const handleClick = useCallback(async () => {
2338
+ if (expanded) {
2339
+ setExpanded(false);
2340
+ return;
2341
+ }
2342
+ if (rawContent) {
2343
+ setExpanded(true);
2344
+ return;
2345
+ }
2346
+ setLoading(true);
2347
+ const data = await fetchPublicKbPage(article.workspaceSlug, article.slug);
2348
+ if (data?.page.contentUrl) {
2349
+ try {
2350
+ const res = await fetch(data.page.contentUrl);
2351
+ if (res.ok) {
2352
+ const text = await res.text();
2353
+ setRawContent(text);
2354
+ }
2355
+ } catch {
2356
+ }
2357
+ }
2358
+ setExpanded(true);
2359
+ setLoading(false);
2360
+ }, [article, rawContent, expanded]);
2361
+ return /* @__PURE__ */ jsxs2("div", { class: "sr-article-card", children: [
2362
+ /* @__PURE__ */ jsxs2("button", { type: "button", class: "sr-article-btn", onClick: handleClick, children: [
2363
+ /* @__PURE__ */ jsx2(
2364
+ "svg",
2365
+ {
2366
+ class: "sr-article-icon",
2367
+ viewBox: "0 0 24 24",
2368
+ fill: "none",
2369
+ stroke: "currentColor",
2370
+ "stroke-width": "2",
2371
+ "stroke-linecap": "round",
2372
+ "stroke-linejoin": "round",
2373
+ 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" })
2374
+ }
2375
+ ),
2376
+ /* @__PURE__ */ jsx2("span", { class: "sr-article-title", children: article.title }),
2377
+ /* @__PURE__ */ jsx2(
2378
+ "svg",
2379
+ {
2380
+ class: `sr-article-chevron ${expanded ? "sr-expanded" : ""}`,
2381
+ viewBox: "0 0 24 24",
2382
+ fill: "none",
2383
+ stroke: "currentColor",
2384
+ "stroke-width": "2",
2385
+ "stroke-linecap": "round",
2386
+ "stroke-linejoin": "round",
2387
+ children: /* @__PURE__ */ jsx2("polyline", { points: "6 9 12 15 18 9" })
2388
+ }
2389
+ )
2390
+ ] }),
2391
+ loading && /* @__PURE__ */ jsx2("div", { class: "sr-article-loading", children: "Loading..." }),
2392
+ expanded && html && /* @__PURE__ */ jsx2(
2393
+ "div",
2394
+ {
2395
+ class: "sr-article-content sr-prose",
2396
+ dangerouslySetInnerHTML: { __html: html }
2397
+ }
2398
+ )
2399
+ ] });
2400
+ }
2401
+ function SuggestedArticles({ articles }) {
2402
+ if (!articles || articles.length === 0) return null;
2403
+ return /* @__PURE__ */ jsxs2("div", { class: "sr-suggested-articles", children: [
2404
+ /* @__PURE__ */ jsxs2("div", { class: "sr-suggested-label", children: [
2405
+ /* @__PURE__ */ jsxs2(
2406
+ "svg",
2407
+ {
2408
+ viewBox: "0 0 24 24",
2409
+ fill: "none",
2410
+ stroke: "currentColor",
2411
+ "stroke-width": "2",
2412
+ "stroke-linecap": "round",
2413
+ "stroke-linejoin": "round",
2414
+ children: [
2415
+ /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "10" }),
2416
+ /* @__PURE__ */ jsx2("path", { d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" }),
2417
+ /* @__PURE__ */ jsx2("line", { x1: "12", y1: "17", x2: "12.01", y2: "17" })
2418
+ ]
2419
+ }
2420
+ ),
2421
+ "These articles may help"
2422
+ ] }),
2423
+ articles.map((article) => /* @__PURE__ */ jsx2(ArticleCard, { article }, article.pageId))
2424
+ ] });
2425
+ }
2426
+ function ConversationThread() {
2427
+ const state2 = getState();
2428
+ const ticketId = state2.activeTicketId;
2429
+ const conversation = state2.activeConversation;
2430
+ const messagesEndRef = useRef(null);
2431
+ useEffect2(() => {
2432
+ if (!ticketId) return;
2433
+ setState({ loading: true });
2434
+ fetchConversation(ticketId).then((conv) => setState({ activeConversation: conv, loading: false })).catch(() => setState({ loading: false }));
2435
+ }, [ticketId]);
2436
+ useEffect2(() => {
2437
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
2438
+ }, [conversation?.messages.length]);
2439
+ if (!ticketId) return null;
2440
+ if (state2.loading || !conversation) {
2441
+ return /* @__PURE__ */ jsx2("div", { class: "sr-loading", children: "Loading..." });
2442
+ }
2443
+ return /* @__PURE__ */ jsxs2("div", { class: "sr-thread", children: [
2444
+ /* @__PURE__ */ jsxs2("div", { class: "sr-thread-messages", children: [
2445
+ /* @__PURE__ */ jsxs2("div", { class: "sr-message-row sr-message-row-user", children: [
2446
+ /* @__PURE__ */ jsx2(
2447
+ "div",
2448
+ {
2449
+ class: "sr-message sr-prose",
2450
+ dangerouslySetInnerHTML: {
2451
+ __html: renderMarkdown(conversation.ticket.description)
2452
+ }
2453
+ }
2454
+ ),
2455
+ /* @__PURE__ */ jsx2(MessageAttachments, { messageId: null }),
2456
+ /* @__PURE__ */ jsxs2("div", { class: "sr-message-meta", children: [
2457
+ /* @__PURE__ */ jsx2("span", { class: "sr-message-sender", children: "You" }),
2458
+ /* @__PURE__ */ jsx2("span", { children: "\xB7" }),
2459
+ /* @__PURE__ */ jsx2("span", { children: formatTime(conversation.ticket.createdAt) })
2460
+ ] })
2461
+ ] }),
2462
+ conversation.ticket.suggestedArticles && conversation.ticket.suggestedArticles.length > 0 && /* @__PURE__ */ jsx2(
2463
+ SuggestedArticles,
2464
+ {
2465
+ articles: conversation.ticket.suggestedArticles
2466
+ }
2467
+ ),
2468
+ conversation.messages.map((msg) => /* @__PURE__ */ jsx2(Message, { msg }, msg.id)),
2469
+ /* @__PURE__ */ jsx2("div", { ref: messagesEndRef })
2470
+ ] }),
2471
+ /* @__PURE__ */ jsx2(MessageComposer, { ticketId })
2472
+ ] });
2473
+ }
2474
+
2475
+ // src/widget/Launcher.tsx
2476
+ import { jsx as jsx3, jsxs as jsxs3 } from "preact/jsx-runtime";
2477
+ function Launcher({
2478
+ position,
2479
+ isOpen
2480
+ }) {
2481
+ const posClass = position === "bottom-left" ? "left" : "right";
2482
+ return /* @__PURE__ */ jsx3(
2483
+ "button",
2484
+ {
2485
+ class: `sr-launcher ${posClass}`,
2486
+ onClick: toggleOpen,
2487
+ "aria-label": isOpen ? "Close support" : "Open support",
2488
+ 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: [
2489
+ /* @__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" }),
2490
+ /* @__PURE__ */ jsx3("path", { d: "M7 9h2v2H7zm4 0h2v2h-2zm4 0h2v2h-2z" })
2491
+ ] })
2492
+ }
2493
+ );
2494
+ }
2495
+
2496
+ // src/widget/TicketForm.tsx
2497
+ import { useCallback as useCallback2, useRef as useRef2, useState as useState2 } from "preact/hooks";
2498
+ import { jsx as jsx4, jsxs as jsxs4 } from "preact/jsx-runtime";
2499
+ function TicketForm() {
2500
+ const identity = client_default._getIdentity();
2501
+ const hasIdentity = !!identity.email;
2502
+ const [subject, setSubject] = useState2("");
2503
+ const [description, setDescription] = useState2("");
2504
+ const [email, setEmail] = useState2(identity.email ?? "");
2505
+ const [submitting, setSubmitting] = useState2(false);
2506
+ const [error, setError] = useState2(null);
2507
+ const [success, setSuccess] = useState2(false);
2508
+ const [ticketId, setTicketId] = useState2(null);
2509
+ const [attachments, setAttachments] = useState2([]);
2510
+ const [uploading, setUploading] = useState2(false);
2511
+ const fileInputRef = useRef2(null);
2512
+ const handleFileSelect = useCallback2(
2513
+ async (e) => {
2514
+ const input = e.target;
2515
+ const files = input.files;
2516
+ if (!files) return;
2517
+ for (let i = 0; i < files.length && attachments.length + i < 5; i++) {
2518
+ setUploading(true);
2519
+ try {
2520
+ const result = await uploadFile(files[i]);
2521
+ setAttachments((prev) => [...prev, result]);
2522
+ } catch {
2523
+ setError("File upload failed");
2524
+ } finally {
2525
+ setUploading(false);
2526
+ }
2527
+ }
2528
+ input.value = "";
2529
+ },
2530
+ [attachments.length]
2531
+ );
2532
+ const removeAttachment = useCallback2((storageKey) => {
2533
+ setAttachments((prev) => prev.filter((a) => a.storageKey !== storageKey));
2534
+ }, []);
2535
+ const handleSubmit = useCallback2(
2536
+ async (e) => {
2537
+ e.preventDefault();
2538
+ if (!subject.trim() || !description.trim()) return;
2539
+ setSubmitting(true);
2540
+ setError(null);
2541
+ try {
2542
+ const result = await createTicket({
2543
+ subject: subject.trim(),
2544
+ description: description.trim(),
2545
+ email: email.trim() || void 0,
2546
+ attachments: attachments.length ? attachments : void 0
2547
+ });
2548
+ setSuccess(true);
2549
+ setTicketId(result.id);
2550
+ } catch (err) {
2551
+ setError(err instanceof Error ? err.message : "Something went wrong");
2552
+ } finally {
2553
+ setSubmitting(false);
2554
+ }
2555
+ },
2556
+ [subject, description, email, attachments]
2557
+ );
2558
+ if (success && ticketId) {
2559
+ return /* @__PURE__ */ jsxs4("div", { class: "sr-success", children: [
2560
+ /* @__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" }) }) }),
2561
+ /* @__PURE__ */ jsx4("h4", { children: "Message sent" }),
2562
+ /* @__PURE__ */ jsx4("p", { children: "We'll get back to you soon." }),
2563
+ /* @__PURE__ */ jsx4("div", { style: { marginTop: "16px" }, children: /* @__PURE__ */ jsx4(
2564
+ "button",
2565
+ {
2566
+ class: "sr-btn sr-btn-primary",
2567
+ onClick: () => openThread(ticketId),
2568
+ children: "View conversation"
2569
+ }
2570
+ ) })
2571
+ ] });
2572
+ }
2573
+ const canSubmit = !!subject.trim() && !!description.trim() && !submitting;
2574
+ return /* @__PURE__ */ jsxs4("form", { class: "sr-form", onSubmit: handleSubmit, children: [
2575
+ /* @__PURE__ */ jsxs4("div", { class: "sr-field", children: [
2576
+ /* @__PURE__ */ jsx4("label", { children: "Subject" }),
2577
+ /* @__PURE__ */ jsx4(
2578
+ "input",
2579
+ {
2580
+ type: "text",
2581
+ value: subject,
2582
+ onInput: (e) => setSubject(e.target.value),
2583
+ placeholder: "Brief summary",
2584
+ maxLength: 500,
2585
+ required: true
2586
+ }
2587
+ )
2588
+ ] }),
2589
+ /* @__PURE__ */ jsxs4("div", { class: "sr-field sr-field-grow", children: [
2590
+ /* @__PURE__ */ jsx4("label", { children: "Description" }),
2591
+ /* @__PURE__ */ jsxs4("div", { class: "sr-textarea-wrap", children: [
2592
+ /* @__PURE__ */ jsx4(
2593
+ "textarea",
2594
+ {
2595
+ value: description,
2596
+ onInput: (e) => setDescription(e.target.value),
2597
+ placeholder: "Describe the issue or request...",
2598
+ maxLength: 1e4,
2599
+ required: true
2600
+ }
2601
+ ),
2602
+ /* @__PURE__ */ jsx4(
2603
+ "button",
2604
+ {
2605
+ type: "button",
2606
+ class: "sr-attach-btn sr-attach-btn-left",
2607
+ onClick: () => fileInputRef.current?.click(),
2608
+ "aria-label": "Attach file",
2609
+ disabled: attachments.length >= 5,
2610
+ children: /* @__PURE__ */ jsx4("svg", { viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsx4(
2611
+ "path",
2612
+ {
2613
+ 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",
2614
+ fill: "none",
2615
+ stroke: "currentColor",
2616
+ "stroke-width": "2",
2617
+ "stroke-linecap": "round",
2618
+ "stroke-linejoin": "round"
2619
+ }
2620
+ ) })
2621
+ }
2622
+ ),
2623
+ /* @__PURE__ */ jsx4(
2624
+ "button",
2625
+ {
2626
+ type: "submit",
2627
+ class: "sr-send-btn sr-send-btn-abs",
2628
+ disabled: !canSubmit,
2629
+ "aria-label": "Send",
2630
+ 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" }) })
2631
+ }
2632
+ )
2633
+ ] }),
2634
+ /* @__PURE__ */ jsx4(
2635
+ "input",
2636
+ {
2637
+ ref: fileInputRef,
2638
+ type: "file",
2639
+ multiple: true,
2640
+ style: { display: "none" },
2641
+ onChange: handleFileSelect
2642
+ }
2643
+ )
2644
+ ] }),
2645
+ (attachments.length > 0 || uploading) && /* @__PURE__ */ jsxs4("div", { class: "sr-attachment-pills", children: [
2646
+ attachments.map((att) => /* @__PURE__ */ jsxs4("div", { class: "sr-attachment-pill", children: [
2647
+ /* @__PURE__ */ jsx4(
2648
+ "svg",
2649
+ {
2650
+ class: "sr-attachment-pill-icon",
2651
+ viewBox: "0 0 24 24",
2652
+ xmlns: "http://www.w3.org/2000/svg",
2653
+ children: /* @__PURE__ */ jsx4(
2654
+ "path",
2655
+ {
2656
+ 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",
2657
+ fill: "none",
2658
+ stroke: "currentColor",
2659
+ "stroke-width": "2",
2660
+ "stroke-linecap": "round",
2661
+ "stroke-linejoin": "round"
2662
+ }
2663
+ )
2664
+ }
2665
+ ),
2666
+ /* @__PURE__ */ jsx4("span", { class: "sr-attachment-pill-name", children: att.fileName }),
2667
+ /* @__PURE__ */ jsx4(
2668
+ "button",
2669
+ {
2670
+ type: "button",
2671
+ class: "sr-attachment-pill-remove",
2672
+ onClick: () => removeAttachment(att.storageKey),
2673
+ "aria-label": "Remove",
2674
+ children: /* @__PURE__ */ jsx4("svg", { viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsx4(
2675
+ "path",
2676
+ {
2677
+ d: "M18 6L6 18M6 6l12 12",
2678
+ fill: "none",
2679
+ stroke: "currentColor",
2680
+ "stroke-width": "2",
2681
+ "stroke-linecap": "round"
2682
+ }
2683
+ ) })
2684
+ }
2685
+ )
2686
+ ] }, att.storageKey)),
2687
+ uploading && /* @__PURE__ */ jsx4("div", { class: "sr-attachment-pill", children: /* @__PURE__ */ jsx4("span", { class: "sr-attachment-pill-name", children: "Uploading..." }) })
2688
+ ] }),
2689
+ !hasIdentity && /* @__PURE__ */ jsxs4("div", { class: "sr-field", children: [
2690
+ /* @__PURE__ */ jsx4("label", { children: "Email" }),
2691
+ /* @__PURE__ */ jsx4(
2692
+ "input",
2693
+ {
2694
+ type: "email",
2695
+ value: email,
2696
+ onInput: (e) => setEmail(e.target.value),
2697
+ placeholder: "your@email.com"
2698
+ }
2699
+ )
2700
+ ] }),
2701
+ error && /* @__PURE__ */ jsx4("div", { class: "sr-error", children: error })
2702
+ ] });
2703
+ }
2704
+
2705
+ // src/widget/Widget.tsx
2706
+ import { Fragment, jsx as jsx5, jsxs as jsxs5 } from "preact/jsx-runtime";
2707
+ function BackButton() {
2708
+ 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" }) }) });
2709
+ }
2710
+ function NewTicketButton() {
2711
+ 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" }) }) });
2712
+ }
2713
+ function Widget({ options }) {
2714
+ const [, setTick] = useState3(0);
2715
+ const position = options.position ?? "bottom-right";
2716
+ const title = options.labels?.title ?? "How can we help?";
2717
+ const posClass = position === "bottom-left" ? "left" : "right";
2718
+ useEffect3(() => {
2719
+ return subscribe(() => setTick((t) => t + 1));
2720
+ }, []);
2721
+ const state2 = getState();
2722
+ const threadTitle = state2.activeConversation?.ticket.subject ?? state2.tickets.find((t) => t.id === state2.activeTicketId)?.subject ?? "Conversation";
2723
+ const viewTitles = {
2724
+ conversations: title,
2725
+ "new-ticket": options.labels?.newTicket ?? "New conversation",
2726
+ thread: threadTitle
2727
+ };
2728
+ return /* @__PURE__ */ jsxs5(Fragment, { children: [
2729
+ /* @__PURE__ */ jsx5(Launcher, { position, isOpen: state2.isOpen }),
2730
+ state2.isOpen && /* @__PURE__ */ jsxs5("div", { class: `sr-panel ${posClass}`, children: [
2731
+ /* @__PURE__ */ jsxs5("div", { class: "sr-header", children: [
2732
+ /* @__PURE__ */ jsx5("div", { class: "sr-header-actions", children: (state2.view === "new-ticket" || state2.view === "thread") && /* @__PURE__ */ jsx5(BackButton, {}) }),
2733
+ /* @__PURE__ */ jsx5(
2734
+ "h3",
2735
+ {
2736
+ class: state2.view === "thread" ? "sr-header-title-truncate" : "",
2737
+ children: viewTitles[state2.view]
2738
+ }
2739
+ ),
2740
+ /* @__PURE__ */ jsx5("div", { class: "sr-header-actions", children: state2.view === "conversations" && /* @__PURE__ */ jsx5(NewTicketButton, {}) })
2741
+ ] }),
2742
+ /* @__PURE__ */ jsxs5("div", { class: "sr-body", children: [
2743
+ state2.view === "conversations" && /* @__PURE__ */ jsx5(ConversationList, {}),
2744
+ state2.view === "new-ticket" && /* @__PURE__ */ jsx5(TicketForm, {}),
2745
+ state2.view === "thread" && /* @__PURE__ */ jsx5(ConversationThread, {})
2746
+ ] })
2747
+ ] })
2748
+ ] });
2749
+ }
2750
+
2751
+ // src/widget/index.ts
2752
+ var initialized = false;
2753
+ var hostEl = null;
2754
+ function initWidget(options = {}) {
2755
+ if (initialized) return;
2756
+ if (typeof document === "undefined") return;
2757
+ initialized = true;
2758
+ hostEl = document.createElement("div");
2759
+ hostEl.id = "scoperat-widget";
2760
+ const shadow = hostEl.attachShadow({ mode: "open" });
2761
+ const styleEl = document.createElement("style");
2762
+ styleEl.textContent = getStyles(options.theme, options.darkMode);
2763
+ shadow.appendChild(styleEl);
2764
+ const mountPoint = document.createElement("div");
2765
+ shadow.appendChild(mountPoint);
2766
+ document.body.appendChild(hostEl);
2767
+ render(h(Widget, { options }), mountPoint);
2768
+ }
2769
+ function destroyWidget() {
2770
+ if (!initialized || !hostEl) return;
2771
+ render(null, hostEl.shadowRoot.lastElementChild);
2772
+ hostEl.remove();
2773
+ hostEl = null;
2774
+ initialized = false;
2775
+ }
2776
+ export {
2777
+ destroyWidget,
2778
+ initWidget
2779
+ };
2780
+ //# sourceMappingURL=index.js.map