@zohaibarsalan/screenshotter 0.1.6

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,1972 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
4
+ import html2canvas from "html2canvas-pro";
5
+ import { toCanvas } from "html-to-image";
6
+ import { buildCaptureFileParts, clampQualityToScale, } from "@screenshotter/protocol";
7
+ const UI_MARKER_ATTR = "data-screenshotter-ui";
8
+ const CAPTURE_COLOR_PROPERTIES = [
9
+ "color",
10
+ "background-color",
11
+ "background-image",
12
+ "border-top-color",
13
+ "border-right-color",
14
+ "border-bottom-color",
15
+ "border-left-color",
16
+ "outline-color",
17
+ "text-decoration-color",
18
+ "text-shadow",
19
+ "box-shadow",
20
+ "caret-color",
21
+ "fill",
22
+ "stroke",
23
+ "-webkit-text-fill-color",
24
+ "-webkit-text-stroke-color",
25
+ ];
26
+ const OKLCH_LIKE_TOKEN_PATTERN = /\bokl(?:ab|ch)\([^)]*\)/gi;
27
+ const CSS_NUMBER_PATTERN = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i;
28
+ const OKL_TOKEN_CACHE = new Map();
29
+ let oklColorResolverElement = null;
30
+ const THEME_STORAGE_KEYS = [
31
+ "vite-ui-theme",
32
+ "theme",
33
+ "next-theme",
34
+ "next-themes-theme",
35
+ ];
36
+ const CAPTURE_MODE_OPTIONS = [
37
+ "element",
38
+ "viewport",
39
+ "fullpage",
40
+ ];
41
+ const FORMAT_OPTIONS = ["png", "jpeg"];
42
+ const THEME_OPTIONS = ["current", "both"];
43
+ const LIVE_VIEWPORT_PRESET_KEY = "current-window";
44
+ const VIEWPORT_PRESET_GROUPS = [
45
+ "phone",
46
+ "tablet",
47
+ "laptop",
48
+ "desktop",
49
+ ];
50
+ const VIEWPORT_PRESET_GROUP_LABELS = {
51
+ phone: "Phones",
52
+ tablet: "Tablets",
53
+ laptop: "Laptops",
54
+ desktop: "Desktop sizes",
55
+ };
56
+ const VIEWPORT_PRESETS = [
57
+ {
58
+ key: "iphone-se",
59
+ label: "iPhone SE",
60
+ width: 375,
61
+ height: 667,
62
+ dpr: 2,
63
+ group: "phone",
64
+ },
65
+ {
66
+ key: "iphone-15",
67
+ label: "iPhone 15",
68
+ width: 393,
69
+ height: 852,
70
+ dpr: 3,
71
+ group: "phone",
72
+ },
73
+ {
74
+ key: "iphone-15-plus",
75
+ label: "iPhone 15 Plus",
76
+ width: 430,
77
+ height: 932,
78
+ dpr: 3,
79
+ group: "phone",
80
+ },
81
+ {
82
+ key: "pixel-8",
83
+ label: "Pixel 8",
84
+ width: 412,
85
+ height: 915,
86
+ dpr: 2.6,
87
+ group: "phone",
88
+ },
89
+ {
90
+ key: "ipad-mini",
91
+ label: "iPad mini",
92
+ width: 768,
93
+ height: 1024,
94
+ dpr: 2,
95
+ group: "tablet",
96
+ },
97
+ {
98
+ key: "ipad-pro-11",
99
+ label: "iPad Pro 11",
100
+ width: 834,
101
+ height: 1194,
102
+ dpr: 2,
103
+ group: "tablet",
104
+ },
105
+ {
106
+ key: "macbook-air-13",
107
+ label: "MacBook Air 13",
108
+ width: 1440,
109
+ height: 900,
110
+ dpr: 2,
111
+ group: "laptop",
112
+ },
113
+ {
114
+ key: "macbook-pro-14",
115
+ label: "MacBook Pro 14",
116
+ width: 1512,
117
+ height: 982,
118
+ dpr: 2,
119
+ group: "laptop",
120
+ },
121
+ {
122
+ key: "macbook-pro-16",
123
+ label: "MacBook Pro 16",
124
+ width: 1728,
125
+ height: 1117,
126
+ dpr: 2,
127
+ group: "laptop",
128
+ },
129
+ {
130
+ key: "hd-1366",
131
+ label: "HD 1366",
132
+ width: 1366,
133
+ height: 768,
134
+ dpr: 1,
135
+ group: "desktop",
136
+ },
137
+ {
138
+ key: "full-hd",
139
+ label: "Full HD 1080p",
140
+ width: 1920,
141
+ height: 1080,
142
+ dpr: 1,
143
+ group: "desktop",
144
+ },
145
+ {
146
+ key: "qhd-1440p",
147
+ label: "QHD 1440p",
148
+ width: 2560,
149
+ height: 1440,
150
+ dpr: 1,
151
+ group: "desktop",
152
+ },
153
+ ];
154
+ const VIEWPORT_PRESETS_BY_KEY = new Map(VIEWPORT_PRESETS.map((preset) => [preset.key, preset]));
155
+ const VIEWPORT_PRESETS_BY_GROUP = {
156
+ phone: VIEWPORT_PRESETS.filter((preset) => preset.group === "phone"),
157
+ tablet: VIEWPORT_PRESETS.filter((preset) => preset.group === "tablet"),
158
+ laptop: VIEWPORT_PRESETS.filter((preset) => preset.group === "laptop"),
159
+ desktop: VIEWPORT_PRESETS.filter((preset) => preset.group === "desktop"),
160
+ };
161
+ const STATUS_HIDE_DELAY_MS = 2600;
162
+ const WIDGET_PANEL_CSS = `
163
+ .ssw-root,
164
+ .ssw-root * {
165
+ box-sizing: border-box;
166
+ }
167
+ .ssw-root {
168
+ --ui-bg: 0 0% 99%;
169
+ --ui-panel: 0 0% 100%;
170
+ --ui-panel-2: 0 0% 97%;
171
+ --ui-border: 0 0% 86%;
172
+ --ui-border-strong: 0 0% 80%;
173
+ --ui-fg: 0 0% 10%;
174
+ --ui-muted: 0 0% 38%;
175
+ --ui-accent: 196 100% 42%;
176
+ --ui-accent-hover: 196 100% 38%;
177
+ --ui-accent-pressed: 196 100% 35%;
178
+ --ui-accent-fg: 0 0% 100%;
179
+ --ui-ring: 196 100% 42%;
180
+ --ui-radius: 12px;
181
+ --ui-shadow: 0 10px 30px rgba(0, 0, 0, 0.12);
182
+ --ssw-font-ui: "IBM Plex Sans", "Avenir Next", "Segoe UI", sans-serif;
183
+ --ssw-font-mono: "IBM Plex Mono", "JetBrains Mono", monospace;
184
+ color: hsl(var(--ui-fg));
185
+ font-family: var(--ssw-font-ui);
186
+ }
187
+ .ssw-root[data-ui-theme="dark"] {
188
+ --ui-bg: 0 0% 3.5%;
189
+ --ui-panel: 0 0% 6.5%;
190
+ --ui-panel-2: 0 0% 9%;
191
+ --ui-border: 0 0% 14%;
192
+ --ui-border-strong: 0 0% 18%;
193
+ --ui-fg: 0 0% 96%;
194
+ --ui-muted: 0 0% 72%;
195
+ --ui-accent: 196 100% 50%;
196
+ --ui-accent-hover: 196 100% 54%;
197
+ --ui-accent-pressed: 196 100% 46%;
198
+ --ui-accent-fg: 0 0% 6%;
199
+ --ui-ring: 196 100% 55%;
200
+ --ui-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
201
+ }
202
+
203
+ .ui-focus {
204
+ transition: border-color 150ms ease-out, box-shadow 150ms ease-out;
205
+ }
206
+ .ui-focus:focus-visible {
207
+ outline: none;
208
+ box-shadow: 0 0 0 2px hsl(var(--ui-ring)), 0 0 0 4px hsl(var(--ui-panel));
209
+ }
210
+
211
+ .ui-btn {
212
+ display: inline-flex;
213
+ align-items: center;
214
+ justify-content: center;
215
+ gap: 8px;
216
+ white-space: nowrap;
217
+ border-radius: var(--ui-radius);
218
+ padding: 0 16px;
219
+ font-family: var(--ssw-font-ui);
220
+ font-size: 13px;
221
+ font-weight: 600;
222
+ line-height: 1;
223
+ cursor: pointer;
224
+ transition: transform 150ms ease-out, background-color 150ms ease-out,
225
+ border-color 150ms ease-out, color 150ms ease-out, box-shadow 150ms ease-out,
226
+ filter 150ms ease-out;
227
+ }
228
+ .ui-btn:active {
229
+ transform: translateY(1px);
230
+ }
231
+ .ui-btn:disabled {
232
+ opacity: 0.42;
233
+ cursor: not-allowed;
234
+ }
235
+
236
+ .ui-btn-primary {
237
+ border: 1px solid hsl(var(--ui-border-strong));
238
+ background: hsl(var(--ui-accent));
239
+ color: hsl(var(--ui-accent-fg));
240
+ box-shadow: 0 6px 14px rgba(2, 6, 23, 0.2);
241
+ }
242
+ .ui-btn-primary:hover:not(:disabled) {
243
+ background: hsl(var(--ui-accent-hover));
244
+ }
245
+ .ui-btn-primary:active:not(:disabled) {
246
+ background: hsl(var(--ui-accent-pressed));
247
+ }
248
+
249
+ .ui-btn-outline {
250
+ border: 1px solid hsl(var(--ui-border));
251
+ background: hsl(var(--ui-panel-2) / 0.35);
252
+ color: hsl(var(--ui-fg));
253
+ }
254
+ .ui-btn-outline:hover:not(:disabled) {
255
+ background: hsl(var(--ui-panel-2) / 0.5);
256
+ border-color: hsl(var(--ui-border) / 0.82);
257
+ }
258
+
259
+ .ui-btn-ghost {
260
+ border: 1px solid transparent;
261
+ background: transparent;
262
+ color: hsl(var(--ui-fg));
263
+ }
264
+ .ui-btn-ghost:hover:not(:disabled) {
265
+ background: hsl(var(--ui-panel-2) / 0.4);
266
+ }
267
+
268
+ .ui-btn-lg {
269
+ height: 40px;
270
+ padding: 0 18px;
271
+ font-size: 14px;
272
+ }
273
+
274
+ .ui-panel {
275
+ border: 1px solid hsl(var(--ui-border));
276
+ background: linear-gradient(
277
+ 180deg,
278
+ hsl(var(--ui-panel) / 0.95) 0%,
279
+ hsl(var(--ui-bg) / 0.93) 100%
280
+ );
281
+ box-shadow: var(--ui-shadow);
282
+ border-radius: 18px;
283
+ backdrop-filter: blur(8px);
284
+ }
285
+
286
+ .ui-divider {
287
+ border-top: 1px solid hsl(var(--ui-border) / 0.7);
288
+ }
289
+
290
+ .ui-seg-row {
291
+ display: grid;
292
+ grid-template-columns: repeat(3, minmax(0, 1fr));
293
+ gap: 6px;
294
+ }
295
+ .ssw-output-row {
296
+ grid-template-columns: repeat(2, minmax(0, 1fr));
297
+ }
298
+
299
+ .ui-toggle-group {
300
+ border: 1px solid hsl(var(--ui-border));
301
+ border-radius: 16px;
302
+ padding: 4px;
303
+ background: hsl(var(--ui-panel-2) / 0.2);
304
+ }
305
+ .ui-seg-item {
306
+ height: 44px;
307
+ border-radius: 12px;
308
+ border: 1px solid hsl(var(--ui-border) / 0.72);
309
+ background: hsl(var(--ui-panel-2) / 0.22);
310
+ color: hsl(var(--ui-muted));
311
+ }
312
+ .ui-seg-item[data-active="true"] {
313
+ border-color: hsl(var(--ui-accent));
314
+ background: hsl(var(--ui-panel-2) / 0.62);
315
+ color: hsl(var(--ui-fg));
316
+ box-shadow:
317
+ 0 0 0 1px hsl(var(--ui-accent) / 0.58),
318
+ inset 0 0 0 1px hsl(var(--ui-accent) / 0.24),
319
+ inset 0 1px 0 hsl(0 0% 100% / 0.05);
320
+ }
321
+ .ui-seg-item[data-active="true"]:hover:not(:disabled) {
322
+ background: hsl(var(--ui-panel-2) / 0.7);
323
+ }
324
+ .ui-seg-item:disabled {
325
+ color: hsl(var(--ui-muted) / 0.5);
326
+ border-color: hsl(var(--ui-border) / 0.3);
327
+ background: hsl(var(--ui-panel-2) / 0.1);
328
+ cursor: not-allowed;
329
+ }
330
+
331
+ .ui-range {
332
+ display: flex;
333
+ align-items: center;
334
+ width: 100%;
335
+ height: 36px;
336
+ }
337
+ .ui-range input[type="range"] {
338
+ --pct: 0%;
339
+ -webkit-appearance: none;
340
+ appearance: none;
341
+ width: 100%;
342
+ height: 4px;
343
+ border-radius: 999px;
344
+ background: linear-gradient(
345
+ to right,
346
+ hsl(var(--ui-accent)) 0%,
347
+ hsl(var(--ui-accent)) var(--pct),
348
+ hsl(var(--ui-border)) var(--pct),
349
+ hsl(var(--ui-border)) 100%
350
+ );
351
+ outline: none;
352
+ transition: background-color 150ms ease-out;
353
+ }
354
+ .ui-range input[type="range"]::-webkit-slider-runnable-track {
355
+ height: 4px;
356
+ border-radius: 999px;
357
+ background: linear-gradient(
358
+ to right,
359
+ hsl(var(--ui-accent)) 0%,
360
+ hsl(var(--ui-accent)) var(--pct),
361
+ hsl(var(--ui-border)) var(--pct),
362
+ hsl(var(--ui-border)) 100%
363
+ );
364
+ }
365
+ .ui-range input[type="range"]::-webkit-slider-thumb {
366
+ -webkit-appearance: none;
367
+ appearance: none;
368
+ width: 18px;
369
+ height: 18px;
370
+ margin-top: -7px;
371
+ border-radius: 999px;
372
+ background: hsl(var(--ui-accent));
373
+ border: 2px solid hsl(var(--ui-panel));
374
+ box-shadow: 0 0 0 4px hsl(var(--ui-ring) / 0);
375
+ transition: transform 150ms ease-out, box-shadow 150ms ease-out, filter 150ms ease-out;
376
+ }
377
+ .ui-range input[type="range"]:hover::-webkit-slider-thumb {
378
+ filter: brightness(1.08);
379
+ }
380
+ .ui-range input[type="range"]:active::-webkit-slider-thumb {
381
+ transform: scale(0.98);
382
+ }
383
+ .ui-range input[type="range"]:focus-visible::-webkit-slider-thumb {
384
+ box-shadow: 0 0 0 4px hsl(var(--ui-ring) / 0.35);
385
+ }
386
+ .ui-range input[type="range"]::-moz-range-thumb {
387
+ width: 18px;
388
+ height: 18px;
389
+ border-radius: 999px;
390
+ background: hsl(var(--ui-accent));
391
+ border: 2px solid hsl(var(--ui-panel));
392
+ box-shadow: 0 0 0 4px hsl(var(--ui-ring) / 0);
393
+ transition: transform 150ms ease-out, box-shadow 150ms ease-out, filter 150ms ease-out;
394
+ }
395
+ .ui-range input[type="range"]:focus-visible::-moz-range-thumb {
396
+ box-shadow: 0 0 0 4px hsl(var(--ui-ring) / 0.35);
397
+ }
398
+ .ui-range input[type="range"]::-moz-range-progress {
399
+ height: 4px;
400
+ border-radius: 999px;
401
+ background: hsl(var(--ui-accent));
402
+ }
403
+ .ui-range input[type="range"]::-moz-range-track {
404
+ height: 4px;
405
+ border-radius: 999px;
406
+ background: hsl(var(--ui-border));
407
+ }
408
+
409
+ .ssw-launcher {
410
+ width: 68px;
411
+ height: 32px;
412
+ border-radius: 999px;
413
+ font-size: 11px;
414
+ cursor: pointer;
415
+ }
416
+ .ssw-panel {
417
+ position: absolute;
418
+ right: 0;
419
+ bottom: 46px;
420
+ width: 328px;
421
+ max-width: calc(100vw - 24px);
422
+ transform: translateY(8px);
423
+ opacity: 0;
424
+ pointer-events: none;
425
+ transition: transform 140ms ease-out, opacity 140ms ease-out;
426
+ }
427
+ .ssw-panel[data-open="true"] {
428
+ transform: translateY(0);
429
+ opacity: 1;
430
+ pointer-events: auto;
431
+ }
432
+
433
+ .ssw-header {
434
+ display: flex;
435
+ align-items: center;
436
+ justify-content: space-between;
437
+ padding: 11px 12px 10px;
438
+ border-bottom: 1px solid hsl(var(--ui-border));
439
+ }
440
+ .ssw-title {
441
+ margin: 0;
442
+ font-size: 13px;
443
+ font-weight: 650;
444
+ }
445
+ .ssw-subtitle {
446
+ margin: 2px 0 0;
447
+ font-size: 10px;
448
+ line-height: 1.3;
449
+ color: hsl(var(--ui-muted));
450
+ }
451
+ .ssw-header-actions {
452
+ display: flex;
453
+ align-items: center;
454
+ gap: 6px;
455
+ }
456
+ .ssw-hotkey {
457
+ padding: 2px 7px;
458
+ border: 1px solid hsl(var(--ui-border));
459
+ border-radius: 999px;
460
+ font-family: var(--ssw-font-mono);
461
+ font-size: 10px;
462
+ color: hsl(var(--ui-muted));
463
+ }
464
+ .ssw-icon-btn {
465
+ width: 34px;
466
+ height: 34px;
467
+ padding: 0;
468
+ border-radius: 10px;
469
+ font-size: 18px;
470
+ line-height: 1;
471
+ cursor: pointer;
472
+ }
473
+
474
+ .ssw-body {
475
+ padding: 12px;
476
+ }
477
+ .ssw-group {
478
+ margin-bottom: 12px;
479
+ }
480
+ .ssw-group-title {
481
+ margin: 0 0 6px;
482
+ font-size: 10px;
483
+ font-weight: 600;
484
+ color: hsl(var(--ui-muted));
485
+ text-transform: uppercase;
486
+ letter-spacing: 0.08em;
487
+ }
488
+
489
+ .ssw-advanced-card {
490
+ margin-bottom: 10px;
491
+ border: 1px solid hsl(var(--ui-border));
492
+ border-radius: 14px;
493
+ background: hsl(var(--ui-panel-2) / 0.2);
494
+ overflow: hidden;
495
+ }
496
+ .ssw-advanced-toggle {
497
+ width: 100%;
498
+ height: 36px;
499
+ justify-content: space-between;
500
+ padding: 0 12px;
501
+ }
502
+ .ssw-advanced-card[data-open="true"] .ssw-advanced-toggle {
503
+ border-bottom: 1px solid hsl(var(--ui-border) / 0.7);
504
+ }
505
+ .ssw-advanced-toggle:hover {
506
+ background: hsl(var(--ui-panel-2) / 0.22);
507
+ }
508
+ .ssw-chevron {
509
+ display: inline-block;
510
+ transition: transform 150ms ease-out;
511
+ }
512
+ .ssw-chevron.is-open {
513
+ transform: rotate(180deg);
514
+ }
515
+ .ssw-advanced-body {
516
+ padding: 9px;
517
+ }
518
+
519
+ .ssw-setting {
520
+ display: grid;
521
+ grid-template-columns: minmax(0, 1fr) minmax(118px, 132px);
522
+ gap: 10px;
523
+ align-items: center;
524
+ padding: 7px 0;
525
+ }
526
+ .ssw-setting + .ssw-setting {
527
+ border-top: 1px solid hsl(var(--ui-border) / 0.7);
528
+ }
529
+ .ssw-setting-label {
530
+ margin: 0;
531
+ font-size: 12px;
532
+ font-weight: 550;
533
+ line-height: 1.2;
534
+ }
535
+ .ssw-setting-help {
536
+ margin: 2px 0 0;
537
+ font-size: 10px;
538
+ line-height: 1.25;
539
+ color: hsl(var(--ui-muted));
540
+ }
541
+ .ssw-setting-value {
542
+ display: block;
543
+ margin-bottom: 4px;
544
+ text-align: right;
545
+ font-family: var(--ssw-font-mono);
546
+ font-size: 10px;
547
+ }
548
+ .ssw-setting-value-left {
549
+ text-align: left;
550
+ }
551
+ .ssw-preset-select {
552
+ width: 100%;
553
+ height: 34px;
554
+ border-radius: 10px;
555
+ border: 1px solid hsl(var(--ui-border));
556
+ background: hsl(var(--ui-panel-2) / 0.35);
557
+ color: hsl(var(--ui-fg));
558
+ font-family: var(--ssw-font-ui);
559
+ font-size: 12px;
560
+ padding: 0 9px;
561
+ }
562
+ .ssw-preset-select:disabled {
563
+ opacity: 0.6;
564
+ }
565
+ .ssw-mini-segment {
566
+ display: grid;
567
+ grid-template-columns: repeat(2, minmax(0, 1fr));
568
+ gap: 6px;
569
+ }
570
+ .ssw-mini-segment .ui-seg-item {
571
+ height: 34px;
572
+ }
573
+
574
+ .ssw-footer {
575
+ display: flex;
576
+ align-items: center;
577
+ justify-content: space-between;
578
+ gap: 8px;
579
+ margin-top: 10px;
580
+ }
581
+ .ssw-status-chip {
582
+ display: inline-flex;
583
+ align-items: center;
584
+ border: 1px solid hsl(var(--ui-border));
585
+ border-radius: 999px;
586
+ padding: 5px 9px;
587
+ font-family: var(--ssw-font-mono);
588
+ font-size: 10px;
589
+ color: hsl(var(--ui-muted));
590
+ background: hsl(var(--ui-panel-2) / 0.22);
591
+ }
592
+ .ssw-status-chip[data-kind="idle"] {
593
+ border-color: hsl(var(--ui-border) / 0.65);
594
+ color: hsl(var(--ui-muted));
595
+ }
596
+ .ssw-status-chip[data-kind="success"] {
597
+ border-color: hsl(142 70% 35% / 0.65);
598
+ background: hsl(142 70% 20% / 0.25);
599
+ color: hsl(142 70% 75%);
600
+ }
601
+ .ssw-root[data-ui-theme="light"] .ssw-status-chip[data-kind="success"] {
602
+ border-color: hsl(145 46% 45%);
603
+ background: hsl(145 68% 90%);
604
+ color: hsl(145 76% 18%);
605
+ }
606
+ .ssw-status-chip[data-kind="error"] {
607
+ border-color: hsl(350 82% 45% / 0.55);
608
+ background: hsl(350 82% 20% / 0.24);
609
+ color: hsl(350 95% 83%);
610
+ }
611
+ .ssw-status-chip[data-kind="info"] {
612
+ border-color: hsl(206 95% 58% / 0.58);
613
+ background: hsl(206 95% 32% / 0.22);
614
+ color: hsl(206 95% 84%);
615
+ }
616
+ .ssw-action-btn {
617
+ width: calc((100% - 8px) / 2);
618
+ min-width: 0;
619
+ height: 44px;
620
+ font-size: 13px;
621
+ }
622
+ .ssw-toast {
623
+ margin: 6px 2px 0;
624
+ font-family: var(--ssw-font-mono);
625
+ font-size: 11px;
626
+ line-height: 16px;
627
+ color: hsl(var(--ui-muted));
628
+ opacity: 1;
629
+ transform: translateY(0);
630
+ pointer-events: none;
631
+ }
632
+ .ssw-toast[data-kind="success"] {
633
+ color: hsl(142 70% 76%);
634
+ }
635
+ .ssw-root[data-ui-theme="light"] .ssw-toast[data-kind="success"] {
636
+ color: hsl(145 72% 22%);
637
+ }
638
+ .ssw-toast[data-kind="error"] {
639
+ color: hsl(350 95% 83%);
640
+ }
641
+ .ssw-toast[data-kind="info"] {
642
+ color: hsl(206 95% 84%);
643
+ }
644
+
645
+ @media (max-width: 520px) {
646
+ .ssw-panel {
647
+ width: min(328px, calc(100vw - 18px));
648
+ }
649
+ .ssw-subtitle {
650
+ display: none;
651
+ }
652
+ }
653
+ @media (max-width: 760px) {
654
+ .ssw-hotkey {
655
+ display: none;
656
+ }
657
+ }
658
+ `;
659
+ function waitForPaint() {
660
+ return new Promise((resolve) => {
661
+ requestAnimationFrame(() => {
662
+ requestAnimationFrame(() => resolve());
663
+ });
664
+ });
665
+ }
666
+ function waitForMs(ms) {
667
+ return new Promise((resolve) => setTimeout(resolve, ms));
668
+ }
669
+ async function waitForVisualAssetsReady(sourceDocument, timeoutMs = 2500) {
670
+ const fontsPromise = "fonts" in sourceDocument
671
+ ? sourceDocument.fonts?.ready
672
+ : undefined;
673
+ const imageDecodes = Array.from(sourceDocument.images)
674
+ .filter((image) => !image.complete)
675
+ .map((image) => image.decode().catch(() => undefined));
676
+ await Promise.race([
677
+ Promise.all([
678
+ fontsPromise?.catch(() => undefined),
679
+ Promise.all(imageDecodes),
680
+ ]),
681
+ waitForMs(timeoutMs),
682
+ ]);
683
+ }
684
+ function isEditableTarget(target) {
685
+ if (!(target instanceof HTMLElement))
686
+ return false;
687
+ const tag = target.tagName.toLowerCase();
688
+ if (tag === "input" || tag === "textarea" || tag === "select")
689
+ return true;
690
+ return target.isContentEditable;
691
+ }
692
+ function inferCurrentTheme() {
693
+ const root = document.documentElement;
694
+ if (root.classList.contains("dark") ||
695
+ root.dataset.theme === "dark" ||
696
+ root.getAttribute("data-mode") === "dark") {
697
+ return "dark";
698
+ }
699
+ if (typeof window.matchMedia === "function" &&
700
+ window.matchMedia("(prefers-color-scheme: dark)").matches) {
701
+ return "dark";
702
+ }
703
+ return "light";
704
+ }
705
+ function parseStoredThemeValue(raw) {
706
+ if (!raw)
707
+ return null;
708
+ const normalized = raw.trim().toLowerCase().replace(/^['"]|['"]$/g, "");
709
+ if (!normalized || normalized === "system")
710
+ return null;
711
+ if (normalized === "dark" || normalized.endsWith(":dark") || normalized.endsWith("-dark")) {
712
+ return "dark";
713
+ }
714
+ if (normalized === "light" || normalized.endsWith(":light") || normalized.endsWith("-light")) {
715
+ return "light";
716
+ }
717
+ if (normalized.includes("dark"))
718
+ return "dark";
719
+ if (normalized.includes("light"))
720
+ return "light";
721
+ return null;
722
+ }
723
+ function getStorageTheme() {
724
+ if (typeof window === "undefined")
725
+ return null;
726
+ for (const key of THEME_STORAGE_KEYS) {
727
+ let raw = null;
728
+ try {
729
+ raw = window.localStorage.getItem(key);
730
+ }
731
+ catch {
732
+ raw = null;
733
+ }
734
+ const parsed = parseStoredThemeValue(raw);
735
+ if (parsed)
736
+ return parsed;
737
+ }
738
+ return null;
739
+ }
740
+ function resolveWidgetTheme() {
741
+ return getStorageTheme() ?? inferCurrentTheme();
742
+ }
743
+ function oppositeTheme(theme) {
744
+ return theme === "light" ? "dark" : "light";
745
+ }
746
+ function modeLabel(mode) {
747
+ if (mode === "element")
748
+ return "Element";
749
+ if (mode === "viewport")
750
+ return "Viewport";
751
+ return "Full page";
752
+ }
753
+ function actionLabel(mode, isPickingElement, isSaving) {
754
+ if (mode === "element") {
755
+ return isPickingElement ? "Picking..." : "Pick element";
756
+ }
757
+ if (mode === "viewport") {
758
+ return isSaving ? "Capturing..." : "Capture viewport";
759
+ }
760
+ return isSaving ? "Capturing..." : "Capture full page";
761
+ }
762
+ function getViewportPreset(key) {
763
+ return VIEWPORT_PRESETS_BY_KEY.get(key) ?? null;
764
+ }
765
+ function readLiveViewport() {
766
+ if (typeof window === "undefined") {
767
+ return {
768
+ width: 1,
769
+ height: 1,
770
+ dpr: 1,
771
+ };
772
+ }
773
+ return {
774
+ width: Math.max(1, window.innerWidth),
775
+ height: Math.max(1, window.innerHeight),
776
+ dpr: Math.max(0.1, window.devicePixelRatio || 1),
777
+ };
778
+ }
779
+ function resolveCaptureViewport(mode, presetKey, liveViewport) {
780
+ if (mode === "element" || presetKey === LIVE_VIEWPORT_PRESET_KEY) {
781
+ return liveViewport;
782
+ }
783
+ const preset = getViewportPreset(presetKey);
784
+ if (!preset)
785
+ return liveViewport;
786
+ return {
787
+ width: preset.width,
788
+ height: preset.height,
789
+ dpr: preset.dpr,
790
+ };
791
+ }
792
+ function formatDpr(dpr) {
793
+ const rounded = Math.round(dpr * 10) / 10;
794
+ if (Number.isInteger(rounded))
795
+ return String(rounded);
796
+ return rounded.toFixed(1).replace(/\.0$/, "");
797
+ }
798
+ function formatViewportMetrics(viewport) {
799
+ return `${viewport.width}x${viewport.height} @${formatDpr(viewport.dpr)}x`;
800
+ }
801
+ function statusChipLabel(kind) {
802
+ if (kind === "saving")
803
+ return "Capturing...";
804
+ if (kind === "success")
805
+ return "Saved";
806
+ if (kind === "error")
807
+ return "Error";
808
+ if (kind === "info")
809
+ return "Picking...";
810
+ return "Ready";
811
+ }
812
+ function toSelectorName(element) {
813
+ const preferred = element.getAttribute("aria-label") ||
814
+ element.getAttribute("data-testid") ||
815
+ element.getAttribute("data-test") ||
816
+ element.id;
817
+ if (preferred)
818
+ return preferred;
819
+ const className = typeof element.className === "string" ? element.className.trim().split(/\s+/)[0] : "";
820
+ if (className)
821
+ return className;
822
+ return element.tagName.toLowerCase();
823
+ }
824
+ function toSelector(element) {
825
+ if (element.id)
826
+ return `#${element.id}`;
827
+ const testId = element.getAttribute("data-testid") || element.getAttribute("data-test");
828
+ if (testId)
829
+ return `[data-testid="${testId}"]`;
830
+ const classList = Array.from(element.classList).slice(0, 2);
831
+ if (classList.length) {
832
+ return `${element.tagName.toLowerCase()}.${classList.join(".")}`;
833
+ }
834
+ return element.tagName.toLowerCase();
835
+ }
836
+ function decodeBase64ToBytes(raw) {
837
+ const normalized = raw.includes(",") ? raw.split(",").pop() || "" : raw;
838
+ if (!normalized)
839
+ return new Uint8Array();
840
+ const decoded = atob(normalized);
841
+ const bytes = new Uint8Array(decoded.length);
842
+ for (let i = 0; i < decoded.length; i += 1) {
843
+ bytes[i] = decoded.charCodeAt(i);
844
+ }
845
+ return bytes;
846
+ }
847
+ function downloadCapture(payload) {
848
+ const bytes = decodeBase64ToBytes(payload.imageBase64);
849
+ if (!bytes.length) {
850
+ throw new Error("imageBase64 could not be decoded.");
851
+ }
852
+ const fileParts = buildCaptureFileParts(payload);
853
+ const mimeType = payload.format === "jpeg" ? "image/jpeg" : "image/png";
854
+ const blobBytes = new Uint8Array(bytes.byteLength);
855
+ blobBytes.set(bytes);
856
+ const blob = new Blob([blobBytes], { type: mimeType });
857
+ const objectUrl = URL.createObjectURL(blob);
858
+ const anchor = document.createElement("a");
859
+ anchor.href = objectUrl;
860
+ anchor.download = fileParts.fileName;
861
+ anchor.style.display = "none";
862
+ document.body.appendChild(anchor);
863
+ anchor.click();
864
+ anchor.remove();
865
+ URL.revokeObjectURL(objectUrl);
866
+ return {
867
+ ok: true,
868
+ relativePath: fileParts.relativePath,
869
+ absolutePath: fileParts.fileName,
870
+ bytes: bytes.byteLength,
871
+ };
872
+ }
873
+ function numberFromPx(raw) {
874
+ const parsed = Number.parseFloat(raw);
875
+ return Number.isFinite(parsed) ? parsed : 0;
876
+ }
877
+ function isTransparentColor(raw) {
878
+ const value = raw.trim().toLowerCase().replace(/\s+/g, "");
879
+ if (!value || value === "transparent")
880
+ return true;
881
+ if (value === "rgba(0,0,0,0)")
882
+ return true;
883
+ const rgba = value.match(/^rgba\(([^,]+),([^,]+),([^,]+),([^)]+)\)$/);
884
+ if (!rgba)
885
+ return false;
886
+ const alpha = Number.parseFloat(rgba[4] || "1");
887
+ return Number.isFinite(alpha) && alpha <= 0;
888
+ }
889
+ function isPointInsideRect(x, y, rect) {
890
+ return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
891
+ }
892
+ function resolveElementCaptureTarget(rawTarget) {
893
+ const viewportArea = Math.max(1, window.innerWidth * window.innerHeight);
894
+ const maxDepth = 8;
895
+ let fallback = rawTarget;
896
+ let fallbackArea = Number.POSITIVE_INFINITY;
897
+ for (let current = rawTarget, depth = 0; current && current !== document.body && current !== document.documentElement && depth <= maxDepth; current = current.parentElement, depth += 1) {
898
+ if (isWidgetElement(current))
899
+ continue;
900
+ const rect = current.getBoundingClientRect();
901
+ if (rect.width < 8 || rect.height < 8)
902
+ continue;
903
+ const area = rect.width * rect.height;
904
+ const areaRatio = area / viewportArea;
905
+ if (areaRatio >= 0.55)
906
+ continue;
907
+ const computed = window.getComputedStyle(current);
908
+ const borderWidth = numberFromPx(computed.borderTopWidth) +
909
+ numberFromPx(computed.borderRightWidth) +
910
+ numberFromPx(computed.borderBottomWidth) +
911
+ numberFromPx(computed.borderLeftWidth);
912
+ const hasBackground = !isTransparentColor(computed.backgroundColor);
913
+ const hasBorder = borderWidth > 0;
914
+ const hasShadow = Boolean(computed.boxShadow && computed.boxShadow !== "none");
915
+ const hasRadius = numberFromPx(computed.borderTopLeftRadius) > 0 ||
916
+ numberFromPx(computed.borderTopRightRadius) > 0 ||
917
+ numberFromPx(computed.borderBottomLeftRadius) > 0 ||
918
+ numberFromPx(computed.borderBottomRightRadius) > 0;
919
+ const isInline = computed.display.startsWith("inline");
920
+ const isLayoutContainer = (computed.display.includes("flex") || computed.display.includes("grid")) &&
921
+ current.children.length > 1 &&
922
+ !hasBackground &&
923
+ !hasBorder &&
924
+ !hasShadow;
925
+ if (!isInline && area < fallbackArea) {
926
+ fallback = current;
927
+ fallbackArea = area;
928
+ }
929
+ const hasVisualSurface = hasBackground || hasBorder || hasShadow || hasRadius;
930
+ if (hasVisualSurface && areaRatio <= 0.18) {
931
+ return current;
932
+ }
933
+ // Allow compact semantic blocks, but avoid broad row/page wrappers.
934
+ if (!isLayoutContainer && !isInline && depth <= 2 && areaRatio <= 0.08) {
935
+ return current;
936
+ }
937
+ }
938
+ return fallback;
939
+ }
940
+ function toFriendlyError(error) {
941
+ if (error instanceof Error && error.message)
942
+ return error.message;
943
+ return String(error);
944
+ }
945
+ function isWidgetElement(element) {
946
+ if (!element)
947
+ return false;
948
+ return element.closest(`[${UI_MARKER_ATTR}="true"]`) !== null;
949
+ }
950
+ function getDomCaptureTarget(mode, element, sourceDocument) {
951
+ if (mode === "element") {
952
+ if (!element) {
953
+ throw new Error("No element selected for element capture.");
954
+ }
955
+ return element;
956
+ }
957
+ return sourceDocument.documentElement;
958
+ }
959
+ async function createPresetCaptureContext(viewport) {
960
+ const host = document.body || document.documentElement;
961
+ if (!host) {
962
+ throw new Error("Document is not ready for preset capture.");
963
+ }
964
+ const frame = document.createElement("iframe");
965
+ frame.setAttribute(UI_MARKER_ATTR, "true");
966
+ frame.setAttribute("aria-hidden", "true");
967
+ frame.tabIndex = -1;
968
+ frame.style.position = "fixed";
969
+ frame.style.left = "-100000px";
970
+ frame.style.top = "0";
971
+ frame.style.width = `${Math.max(1, Math.round(viewport.width))}px`;
972
+ frame.style.height = `${Math.max(1, Math.round(viewport.height))}px`;
973
+ frame.style.border = "0";
974
+ frame.style.opacity = "0";
975
+ frame.style.pointerEvents = "none";
976
+ frame.style.zIndex = "-1";
977
+ host.appendChild(frame);
978
+ const cleanup = () => {
979
+ frame.remove();
980
+ };
981
+ try {
982
+ const frameDocument = frame.contentDocument;
983
+ if (!frameDocument) {
984
+ throw new Error("Could not initialize preset capture frame.");
985
+ }
986
+ frameDocument.open();
987
+ frameDocument.write("<!doctype html><html><head></head><body></body></html>");
988
+ frameDocument.close();
989
+ const clonedRoot = document.documentElement.cloneNode(true);
990
+ const widgetNodes = clonedRoot.querySelectorAll(`[${UI_MARKER_ATTR}="true"]`);
991
+ for (const node of widgetNodes) {
992
+ node.remove();
993
+ }
994
+ const scriptNodes = clonedRoot.querySelectorAll("script");
995
+ for (const node of scriptNodes) {
996
+ node.remove();
997
+ }
998
+ const importedRoot = frameDocument.importNode(clonedRoot, true);
999
+ if (frameDocument.documentElement) {
1000
+ frameDocument.replaceChild(importedRoot, frameDocument.documentElement);
1001
+ }
1002
+ else {
1003
+ frameDocument.appendChild(importedRoot);
1004
+ }
1005
+ const frameHead = frameDocument.head;
1006
+ if (frameHead && !frameHead.querySelector("base")) {
1007
+ const base = frameDocument.createElement("base");
1008
+ base.href = window.location.href;
1009
+ frameHead.prepend(base);
1010
+ }
1011
+ await waitForMs(60);
1012
+ await waitForVisualAssetsReady(frameDocument, 3000);
1013
+ const frameRoot = frameDocument.documentElement;
1014
+ const maxScrollX = Math.max(0, frameRoot.scrollWidth - viewport.width);
1015
+ const maxScrollY = Math.max(0, frameRoot.scrollHeight - viewport.height);
1016
+ const scrollX = Math.min(Math.max(0, window.scrollX), maxScrollX);
1017
+ const scrollY = Math.min(Math.max(0, window.scrollY), maxScrollY);
1018
+ return {
1019
+ targetDocument: frameDocument,
1020
+ targetRoot: frameRoot,
1021
+ scrollX,
1022
+ scrollY,
1023
+ cleanup,
1024
+ };
1025
+ }
1026
+ catch (error) {
1027
+ cleanup();
1028
+ throw error;
1029
+ }
1030
+ }
1031
+ function assertBrowser() {
1032
+ if (typeof window === "undefined" || typeof document === "undefined") {
1033
+ throw new Error("ScreenshotterWidget can only run in a browser.");
1034
+ }
1035
+ }
1036
+ function hasUnsupportedColorFunction(value) {
1037
+ const normalized = value.toLowerCase();
1038
+ return normalized.includes("oklch") || normalized.includes("oklab");
1039
+ }
1040
+ function isUnsupportedColorFunctionError(error) {
1041
+ const message = toFriendlyError(error).toLowerCase();
1042
+ return message.includes("unsupported color function") && hasUnsupportedColorFunction(message);
1043
+ }
1044
+ function clamp(value, min, max) {
1045
+ return Math.min(max, Math.max(min, value));
1046
+ }
1047
+ function normalizeElementPadding(value) {
1048
+ return clamp(Math.round(value), 0, 96);
1049
+ }
1050
+ function isAbortError(error) {
1051
+ if (error instanceof DOMException && error.name === "AbortError")
1052
+ return true;
1053
+ if (error instanceof Error && error.name === "AbortError")
1054
+ return true;
1055
+ const message = toFriendlyError(error).toLowerCase();
1056
+ return message.includes("abort");
1057
+ }
1058
+ function parseCssNumber(raw) {
1059
+ const token = raw.trim();
1060
+ if (!CSS_NUMBER_PATTERN.test(token))
1061
+ return null;
1062
+ const parsed = Number(token);
1063
+ if (!Number.isFinite(parsed))
1064
+ return null;
1065
+ return parsed;
1066
+ }
1067
+ function parseAlpha(raw) {
1068
+ if (!raw)
1069
+ return 1;
1070
+ const token = raw.trim().toLowerCase();
1071
+ if (!token)
1072
+ return 1;
1073
+ if (token.endsWith("%")) {
1074
+ const numeric = parseCssNumber(token.slice(0, -1));
1075
+ if (numeric === null)
1076
+ return null;
1077
+ return clamp(numeric / 100, 0, 1);
1078
+ }
1079
+ const numeric = parseCssNumber(token);
1080
+ if (numeric === null)
1081
+ return null;
1082
+ return clamp(numeric, 0, 1);
1083
+ }
1084
+ function parseHueDegrees(raw) {
1085
+ const token = raw.trim().toLowerCase();
1086
+ if (!token || token === "none")
1087
+ return 0;
1088
+ const parseUnitValue = (suffix, multiplier) => {
1089
+ if (!token.endsWith(suffix))
1090
+ return null;
1091
+ const numeric = parseCssNumber(token.slice(0, -suffix.length));
1092
+ if (numeric === null)
1093
+ return null;
1094
+ return numeric * multiplier;
1095
+ };
1096
+ const unitValue = parseUnitValue("deg", 1) ??
1097
+ parseUnitValue("grad", 0.9) ??
1098
+ parseUnitValue("rad", 180 / Math.PI) ??
1099
+ parseUnitValue("turn", 360);
1100
+ if (unitValue !== null)
1101
+ return unitValue;
1102
+ return parseCssNumber(token);
1103
+ }
1104
+ function parseLightness(raw) {
1105
+ const token = raw.trim().toLowerCase();
1106
+ if (!token)
1107
+ return null;
1108
+ if (token.endsWith("%")) {
1109
+ const numeric = parseCssNumber(token.slice(0, -1));
1110
+ if (numeric === null)
1111
+ return null;
1112
+ return clamp(numeric / 100, 0, 1);
1113
+ }
1114
+ const numeric = parseCssNumber(token);
1115
+ if (numeric === null)
1116
+ return null;
1117
+ return clamp(numeric, 0, 1);
1118
+ }
1119
+ function parseChroma(raw) {
1120
+ const token = raw.trim().toLowerCase();
1121
+ if (!token)
1122
+ return null;
1123
+ if (token.endsWith("%")) {
1124
+ const numeric = parseCssNumber(token.slice(0, -1));
1125
+ if (numeric === null)
1126
+ return null;
1127
+ return Math.max(0, (numeric / 100) * 0.4);
1128
+ }
1129
+ const numeric = parseCssNumber(token);
1130
+ if (numeric === null)
1131
+ return null;
1132
+ return Math.max(0, numeric);
1133
+ }
1134
+ function parseOklabAxis(raw) {
1135
+ const token = raw.trim().toLowerCase();
1136
+ if (!token)
1137
+ return null;
1138
+ if (token.endsWith("%")) {
1139
+ const numeric = parseCssNumber(token.slice(0, -1));
1140
+ if (numeric === null)
1141
+ return null;
1142
+ return (numeric / 100) * 0.4;
1143
+ }
1144
+ return parseCssNumber(token);
1145
+ }
1146
+ function splitFunctionBodyAndAlpha(body) {
1147
+ let depth = 0;
1148
+ for (let i = 0; i < body.length; i += 1) {
1149
+ const ch = body[i];
1150
+ if (ch === "(")
1151
+ depth += 1;
1152
+ else if (ch === ")")
1153
+ depth = Math.max(0, depth - 1);
1154
+ else if (ch === "/" && depth === 0) {
1155
+ return {
1156
+ channelsPart: body.slice(0, i).trim(),
1157
+ alphaPart: body.slice(i + 1).trim(),
1158
+ };
1159
+ }
1160
+ }
1161
+ return {
1162
+ channelsPart: body.trim(),
1163
+ alphaPart: null,
1164
+ };
1165
+ }
1166
+ function gammaEncodeSrgb(linear) {
1167
+ const abs = Math.abs(linear);
1168
+ const encoded = abs > 0.0031308
1169
+ ? Math.sign(linear) * (1.055 * Math.pow(abs, 1 / 2.4) - 0.055)
1170
+ : 12.92 * linear;
1171
+ return clamp(encoded, 0, 1);
1172
+ }
1173
+ function oklabToSrgb(lightness, a, b) {
1174
+ const l = lightness + 0.3963377774 * a + 0.2158037573 * b;
1175
+ const m = lightness - 0.1055613458 * a - 0.0638541728 * b;
1176
+ const s = lightness - 0.0894841775 * a - 1.291485548 * b;
1177
+ const l3 = l * l * l;
1178
+ const m3 = m * m * m;
1179
+ const s3 = s * s * s;
1180
+ const redLinear = 4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3;
1181
+ const greenLinear = -1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3;
1182
+ const blueLinear = -0.0041960863 * l3 - 0.7034186147 * m3 + 1.707614701 * s3;
1183
+ return [
1184
+ gammaEncodeSrgb(redLinear),
1185
+ gammaEncodeSrgb(greenLinear),
1186
+ gammaEncodeSrgb(blueLinear),
1187
+ ];
1188
+ }
1189
+ function toRgbCssString(red, green, blue, alpha) {
1190
+ const r255 = Math.round(clamp(red, 0, 1) * 255);
1191
+ const g255 = Math.round(clamp(green, 0, 1) * 255);
1192
+ const b255 = Math.round(clamp(blue, 0, 1) * 255);
1193
+ if (alpha >= 1) {
1194
+ return `rgb(${r255}, ${g255}, ${b255})`;
1195
+ }
1196
+ return `rgba(${r255}, ${g255}, ${b255}, ${Number(alpha.toFixed(4))})`;
1197
+ }
1198
+ function parseOklToken(token) {
1199
+ const match = token.trim().match(/^okl(ch|ab)\((.*)\)$/i);
1200
+ if (!match)
1201
+ return null;
1202
+ const type = match[1]?.toLowerCase();
1203
+ const inner = match[2] ?? "";
1204
+ if (!type || !inner.trim())
1205
+ return null;
1206
+ const { channelsPart, alphaPart } = splitFunctionBodyAndAlpha(inner);
1207
+ const alpha = parseAlpha(alphaPart);
1208
+ if (alpha === null)
1209
+ return null;
1210
+ const tokens = channelsPart.split(/\s+/).filter(Boolean);
1211
+ if (tokens.length !== 3)
1212
+ return null;
1213
+ const lightness = parseLightness(tokens[0]);
1214
+ if (lightness === null)
1215
+ return null;
1216
+ if (type === "ab") {
1217
+ const axisA = parseOklabAxis(tokens[1]);
1218
+ const axisB = parseOklabAxis(tokens[2]);
1219
+ if (axisA === null || axisB === null)
1220
+ return null;
1221
+ return {
1222
+ lightness,
1223
+ a: axisA,
1224
+ b: axisB,
1225
+ alpha,
1226
+ };
1227
+ }
1228
+ const chroma = parseChroma(tokens[1]);
1229
+ const hue = parseHueDegrees(tokens[2]);
1230
+ if (chroma === null || hue === null)
1231
+ return null;
1232
+ const hueRadians = (hue * Math.PI) / 180;
1233
+ return {
1234
+ lightness,
1235
+ a: chroma * Math.cos(hueRadians),
1236
+ b: chroma * Math.sin(hueRadians),
1237
+ alpha,
1238
+ };
1239
+ }
1240
+ function getOklColorResolverElement() {
1241
+ if (typeof document === "undefined")
1242
+ return null;
1243
+ if (oklColorResolverElement?.isConnected) {
1244
+ return oklColorResolverElement;
1245
+ }
1246
+ const host = document.body || document.documentElement;
1247
+ if (!host)
1248
+ return null;
1249
+ const resolver = document.createElement("span");
1250
+ resolver.setAttribute(UI_MARKER_ATTR, "true");
1251
+ resolver.setAttribute("aria-hidden", "true");
1252
+ resolver.style.position = "fixed";
1253
+ resolver.style.left = "-99999px";
1254
+ resolver.style.top = "-99999px";
1255
+ resolver.style.width = "0";
1256
+ resolver.style.height = "0";
1257
+ resolver.style.pointerEvents = "none";
1258
+ resolver.style.opacity = "0";
1259
+ host.appendChild(resolver);
1260
+ oklColorResolverElement = resolver;
1261
+ return resolver;
1262
+ }
1263
+ function resolveOklTokenWithBrowser(token) {
1264
+ const cached = OKL_TOKEN_CACHE.get(token);
1265
+ if (cached)
1266
+ return cached;
1267
+ const resolver = getOklColorResolverElement();
1268
+ if (!resolver)
1269
+ return null;
1270
+ resolver.style.color = "";
1271
+ resolver.style.color = token;
1272
+ if (!resolver.style.color) {
1273
+ return null;
1274
+ }
1275
+ const resolved = window.getComputedStyle(resolver).color;
1276
+ if (!resolved || hasUnsupportedColorFunction(resolved)) {
1277
+ return null;
1278
+ }
1279
+ OKL_TOKEN_CACHE.set(token, resolved);
1280
+ return resolved;
1281
+ }
1282
+ function normalizeOklColorToken(token) {
1283
+ const browserResolved = resolveOklTokenWithBrowser(token);
1284
+ if (browserResolved)
1285
+ return browserResolved;
1286
+ const parsed = parseOklToken(token);
1287
+ if (!parsed)
1288
+ return token;
1289
+ const [red, green, blue] = oklabToSrgb(parsed.lightness, parsed.a, parsed.b);
1290
+ const fallback = toRgbCssString(red, green, blue, parsed.alpha);
1291
+ OKL_TOKEN_CACHE.set(token, fallback);
1292
+ return fallback;
1293
+ }
1294
+ function normalizeOklColorsInValue(value) {
1295
+ return value.replace(OKLCH_LIKE_TOKEN_PATTERN, (token) => normalizeOklColorToken(token));
1296
+ }
1297
+ function applyCaptureSafeComputedStyles(clonedDocument) {
1298
+ if (!document.body || !clonedDocument.body)
1299
+ return;
1300
+ const sourceNodes = [
1301
+ document.documentElement,
1302
+ document.body,
1303
+ ...Array.from(document.body.querySelectorAll("*")),
1304
+ ];
1305
+ const clonedNodes = [
1306
+ clonedDocument.documentElement,
1307
+ clonedDocument.body,
1308
+ ...Array.from(clonedDocument.body.querySelectorAll("*")),
1309
+ ];
1310
+ const total = Math.min(sourceNodes.length, clonedNodes.length);
1311
+ for (let i = 0; i < total; i += 1) {
1312
+ const source = sourceNodes[i];
1313
+ const target = clonedNodes[i];
1314
+ const computed = window.getComputedStyle(source);
1315
+ for (const property of CAPTURE_COLOR_PROPERTIES) {
1316
+ const value = computed.getPropertyValue(property);
1317
+ if (!value)
1318
+ continue;
1319
+ const nextValue = hasUnsupportedColorFunction(value)
1320
+ ? normalizeOklColorsInValue(value)
1321
+ : value;
1322
+ target.style.setProperty(property, nextValue);
1323
+ }
1324
+ for (let index = 0; index < computed.length; index += 1) {
1325
+ const property = computed.item(index);
1326
+ if (!property)
1327
+ continue;
1328
+ if (property.startsWith("--"))
1329
+ continue;
1330
+ const value = computed.getPropertyValue(property);
1331
+ if (!value || !hasUnsupportedColorFunction(value))
1332
+ continue;
1333
+ const nextValue = normalizeOklColorsInValue(value);
1334
+ if (nextValue === value)
1335
+ continue;
1336
+ const priority = computed.getPropertyPriority(property);
1337
+ target.style.setProperty(property, nextValue, priority);
1338
+ }
1339
+ }
1340
+ }
1341
+ function getViewportCrop(scale, sourceWidth, sourceHeight, viewport, scrollX, scrollY) {
1342
+ const requestedWidth = Math.max(1, Math.round(viewport.width * scale));
1343
+ const requestedHeight = Math.max(1, Math.round(viewport.height * scale));
1344
+ const sx = Math.max(0, Math.round(scrollX * scale));
1345
+ const sy = Math.max(0, Math.round(scrollY * scale));
1346
+ const sw = Math.max(1, Math.min(requestedWidth, sourceWidth - sx));
1347
+ const sh = Math.max(1, Math.min(requestedHeight, sourceHeight - sy));
1348
+ return {
1349
+ sx,
1350
+ sy,
1351
+ sw,
1352
+ sh,
1353
+ };
1354
+ }
1355
+ function getFullPageRenderSize(viewport, sourceDocument) {
1356
+ const doc = sourceDocument.documentElement;
1357
+ return {
1358
+ width: Math.max(1, viewport.width),
1359
+ height: Math.max(Math.max(doc.scrollHeight, doc.clientHeight), viewport.height),
1360
+ };
1361
+ }
1362
+ function cropCanvas(source, crop) {
1363
+ const canvas = document.createElement("canvas");
1364
+ canvas.width = crop.sw;
1365
+ canvas.height = crop.sh;
1366
+ const context = canvas.getContext("2d");
1367
+ if (!context) {
1368
+ throw new Error("Could not create 2D context for viewport crop.");
1369
+ }
1370
+ context.drawImage(source, crop.sx, crop.sy, crop.sw, crop.sh, 0, 0, crop.sw, crop.sh);
1371
+ return canvas;
1372
+ }
1373
+ function cropElementFromViewportCanvas(viewportCanvas, rect, scale, paddingPx = 0) {
1374
+ const safePadding = Math.max(0, paddingPx);
1375
+ const sx = Math.max(0, Math.floor((rect.left - safePadding) * scale));
1376
+ const sy = Math.max(0, Math.floor((rect.top - safePadding) * scale));
1377
+ const requestedWidth = Math.max(1, Math.ceil((rect.width + safePadding * 2) * scale));
1378
+ const requestedHeight = Math.max(1, Math.ceil((rect.height + safePadding * 2) * scale));
1379
+ const sw = Math.max(1, Math.min(requestedWidth, viewportCanvas.width - sx));
1380
+ const sh = Math.max(1, Math.min(requestedHeight, viewportCanvas.height - sy));
1381
+ return cropCanvas(viewportCanvas, { sx, sy, sw, sh });
1382
+ }
1383
+ async function renderWithHtml2Canvas(mode, target, scale, ignoreElements, viewport, sourceDocument, scroll) {
1384
+ const commonOptions = {
1385
+ backgroundColor: null,
1386
+ logging: false,
1387
+ useCORS: true,
1388
+ scale,
1389
+ ignoreElements,
1390
+ };
1391
+ let options = commonOptions;
1392
+ if (mode === "viewport") {
1393
+ options = {
1394
+ ...commonOptions,
1395
+ width: viewport.width,
1396
+ height: viewport.height,
1397
+ x: scroll.x,
1398
+ y: scroll.y,
1399
+ scrollX: scroll.x,
1400
+ scrollY: scroll.y,
1401
+ windowWidth: viewport.width,
1402
+ windowHeight: viewport.height,
1403
+ };
1404
+ }
1405
+ if (mode === "fullpage") {
1406
+ const fullPage = getFullPageRenderSize(viewport, sourceDocument);
1407
+ options = {
1408
+ ...commonOptions,
1409
+ width: fullPage.width,
1410
+ height: fullPage.height,
1411
+ x: 0,
1412
+ y: 0,
1413
+ scrollX: 0,
1414
+ scrollY: 0,
1415
+ windowWidth: viewport.width,
1416
+ windowHeight: viewport.height,
1417
+ };
1418
+ }
1419
+ const standardOptions = {
1420
+ ...options,
1421
+ foreignObjectRendering: false,
1422
+ };
1423
+ const foreignObjectOptions = {
1424
+ ...options,
1425
+ foreignObjectRendering: true,
1426
+ };
1427
+ const primaryOptions = mode === "element" ? standardOptions : foreignObjectOptions;
1428
+ const fallbackOptions = mode === "element" ? foreignObjectOptions : standardOptions;
1429
+ try {
1430
+ return await html2canvas(target, primaryOptions);
1431
+ }
1432
+ catch (primaryError) {
1433
+ try {
1434
+ return await html2canvas(target, fallbackOptions);
1435
+ }
1436
+ catch (fallbackError) {
1437
+ if (isUnsupportedColorFunctionError(primaryError) ||
1438
+ isUnsupportedColorFunctionError(fallbackError)) {
1439
+ throw new Error("Capture failed because unsupported color functions could not be normalized.");
1440
+ }
1441
+ throw fallbackError;
1442
+ }
1443
+ }
1444
+ }
1445
+ async function renderWithHtmlToImageFallback(mode, target, scale, viewport, sourceDocument, scroll) {
1446
+ const filter = (node) => {
1447
+ return !isWidgetElement(node);
1448
+ };
1449
+ if (mode === "element") {
1450
+ return toCanvas(target, {
1451
+ cacheBust: true,
1452
+ pixelRatio: scale,
1453
+ filter,
1454
+ backgroundColor: "transparent",
1455
+ });
1456
+ }
1457
+ const doc = sourceDocument.documentElement;
1458
+ const fullPage = getFullPageRenderSize(viewport, sourceDocument);
1459
+ const fullWidth = mode === "fullpage"
1460
+ ? fullPage.width
1461
+ : Math.max(Math.max(doc.scrollWidth, doc.clientWidth), viewport.width);
1462
+ const fullHeight = mode === "fullpage"
1463
+ ? fullPage.height
1464
+ : Math.max(Math.max(doc.scrollHeight, doc.clientHeight), viewport.height);
1465
+ const fullCanvas = await toCanvas(doc, {
1466
+ cacheBust: true,
1467
+ pixelRatio: scale,
1468
+ filter,
1469
+ backgroundColor: "transparent",
1470
+ width: fullWidth,
1471
+ height: fullHeight,
1472
+ canvasWidth: Math.max(1, Math.round(fullWidth * scale)),
1473
+ canvasHeight: Math.max(1, Math.round(fullHeight * scale)),
1474
+ style: {
1475
+ width: `${fullWidth}px`,
1476
+ height: `${fullHeight}px`,
1477
+ },
1478
+ });
1479
+ if (mode === "fullpage") {
1480
+ return fullCanvas;
1481
+ }
1482
+ const crop = getViewportCrop(scale, fullCanvas.width, fullCanvas.height, viewport, scroll.x, scroll.y);
1483
+ return cropCanvas(fullCanvas, crop);
1484
+ }
1485
+ export function ScreenshotterWidget({ enabled, project = "app", elementPaddingPx = 8, captureSettleMs = 700, defaultMode = "element", themeSelectionDefault = "current", themeAdapter, onSaved, onError, }) {
1486
+ const defaultEnabled = typeof process !== "undefined" ? process.env.NODE_ENV === "development" : false;
1487
+ const isEnabled = enabled ?? defaultEnabled;
1488
+ const [isPanelOpen, setIsPanelOpen] = useState(false);
1489
+ const [isAdvancedOpen, setIsAdvancedOpen] = useState(false);
1490
+ const [mode, setMode] = useState(defaultMode);
1491
+ const [presetKey, setPresetKey] = useState(LIVE_VIEWPORT_PRESET_KEY);
1492
+ const [format, setFormat] = useState("png");
1493
+ const [quality, setQuality] = useState(90);
1494
+ const [elementPadding, setElementPadding] = useState(() => normalizeElementPadding(elementPaddingPx));
1495
+ const [themeSelection, setThemeSelection] = useState(themeSelectionDefault);
1496
+ const [isPickingElement, setIsPickingElement] = useState(false);
1497
+ const [isSaving, setIsSaving] = useState(false);
1498
+ const [status, setStatus] = useState({
1499
+ kind: "idle",
1500
+ message: "Ready",
1501
+ });
1502
+ const [isStatusToastVisible, setIsStatusToastVisible] = useState(false);
1503
+ const [hideUiForCapture, setHideUiForCapture] = useState(false);
1504
+ const [uiTheme, setUiTheme] = useState(() => typeof window !== "undefined" ? resolveWidgetTheme() : "dark");
1505
+ const [liveViewport, setLiveViewport] = useState(() => readLiveViewport());
1506
+ const rootRef = useRef(null);
1507
+ const isMountedRef = useRef(true);
1508
+ const isCaptureInFlightRef = useRef(false);
1509
+ const canCaptureBothThemes = Boolean(themeAdapter);
1510
+ const scale = useMemo(() => clampQualityToScale(quality), [quality]);
1511
+ const safeElementPaddingPx = useMemo(() => normalizeElementPadding(elementPadding), [elementPadding]);
1512
+ const captureViewportPreview = useMemo(() => resolveCaptureViewport(mode, presetKey, liveViewport), [liveViewport, mode, presetKey]);
1513
+ const capturePreset = useMemo(() => getViewportPreset(presetKey), [presetKey]);
1514
+ const presetDescription = useMemo(() => {
1515
+ if (mode === "element")
1516
+ return formatViewportMetrics(liveViewport);
1517
+ if (presetKey === LIVE_VIEWPORT_PRESET_KEY || !capturePreset) {
1518
+ return `Current window ${formatViewportMetrics(captureViewportPreview)}`;
1519
+ }
1520
+ return `${capturePreset.label} ${formatViewportMetrics(captureViewportPreview)}`;
1521
+ }, [capturePreset, captureViewportPreview, liveViewport, mode, presetKey]);
1522
+ const runIfMounted = useCallback((work) => {
1523
+ if (!isMountedRef.current)
1524
+ return;
1525
+ work();
1526
+ }, []);
1527
+ useEffect(() => {
1528
+ setElementPadding(normalizeElementPadding(elementPaddingPx));
1529
+ }, [elementPaddingPx]);
1530
+ useEffect(() => {
1531
+ isMountedRef.current = true;
1532
+ return () => {
1533
+ isMountedRef.current = false;
1534
+ isCaptureInFlightRef.current = false;
1535
+ };
1536
+ }, []);
1537
+ useEffect(() => {
1538
+ if (!isEnabled || typeof window === "undefined")
1539
+ return undefined;
1540
+ const syncViewport = () => {
1541
+ setLiveViewport(readLiveViewport());
1542
+ };
1543
+ syncViewport();
1544
+ window.addEventListener("resize", syncViewport);
1545
+ return () => {
1546
+ window.removeEventListener("resize", syncViewport);
1547
+ };
1548
+ }, [isEnabled]);
1549
+ useEffect(() => {
1550
+ if (!isEnabled || typeof window === "undefined")
1551
+ return undefined;
1552
+ const syncTheme = () => {
1553
+ setUiTheme(resolveWidgetTheme());
1554
+ };
1555
+ syncTheme();
1556
+ window.addEventListener("storage", syncTheme);
1557
+ const root = document.documentElement;
1558
+ const observer = new MutationObserver(() => {
1559
+ syncTheme();
1560
+ });
1561
+ observer.observe(root, {
1562
+ attributes: true,
1563
+ attributeFilter: ["class", "data-theme", "data-mode"],
1564
+ });
1565
+ const media = window.matchMedia("(prefers-color-scheme: dark)");
1566
+ const legacyMedia = media;
1567
+ const onMediaChange = () => {
1568
+ if (!getStorageTheme()) {
1569
+ syncTheme();
1570
+ }
1571
+ };
1572
+ if (typeof media.addEventListener === "function") {
1573
+ media.addEventListener("change", onMediaChange);
1574
+ }
1575
+ else {
1576
+ legacyMedia.addListener?.(onMediaChange);
1577
+ }
1578
+ return () => {
1579
+ window.removeEventListener("storage", syncTheme);
1580
+ observer.disconnect();
1581
+ if (typeof media.removeEventListener === "function") {
1582
+ media.removeEventListener("change", onMediaChange);
1583
+ }
1584
+ else {
1585
+ legacyMedia.removeListener?.(onMediaChange);
1586
+ }
1587
+ };
1588
+ }, [isEnabled]);
1589
+ useEffect(() => {
1590
+ if (!canCaptureBothThemes && themeSelection === "both") {
1591
+ setThemeSelection("current");
1592
+ }
1593
+ }, [canCaptureBothThemes, themeSelection]);
1594
+ useEffect(() => {
1595
+ if (status.kind === "idle") {
1596
+ setIsStatusToastVisible(false);
1597
+ return undefined;
1598
+ }
1599
+ setIsStatusToastVisible(true);
1600
+ if (status.kind === "success" || status.kind === "info") {
1601
+ const timeout = window.setTimeout(() => {
1602
+ setIsStatusToastVisible(false);
1603
+ }, STATUS_HIDE_DELAY_MS);
1604
+ return () => window.clearTimeout(timeout);
1605
+ }
1606
+ return undefined;
1607
+ }, [status.kind, status.message]);
1608
+ const getCurrentTheme = useCallback(() => {
1609
+ try {
1610
+ if (themeAdapter)
1611
+ return themeAdapter.getCurrentTheme();
1612
+ }
1613
+ catch {
1614
+ // fall back to DOM inference
1615
+ }
1616
+ return inferCurrentTheme();
1617
+ }, [themeAdapter]);
1618
+ const postCapture = useCallback(async (payload) => {
1619
+ return downloadCapture(payload);
1620
+ }, []);
1621
+ const runSingleCapture = useCallback(async (theme, selectedElement) => {
1622
+ assertBrowser();
1623
+ runIfMounted(() => setHideUiForCapture(true));
1624
+ await waitForPaint();
1625
+ await waitForVisualAssetsReady(document);
1626
+ const live = readLiveViewport();
1627
+ const captureViewport = resolveCaptureViewport(mode, presetKey, live);
1628
+ let captureDocument = document;
1629
+ let captureTarget = getDomCaptureTarget(mode, selectedElement, captureDocument);
1630
+ let captureScroll = { x: window.scrollX, y: window.scrollY };
1631
+ let cleanupCaptureContext = () => undefined;
1632
+ try {
1633
+ if (mode !== "element" && presetKey !== LIVE_VIEWPORT_PRESET_KEY) {
1634
+ const presetContext = await createPresetCaptureContext(captureViewport);
1635
+ captureDocument = presetContext.targetDocument;
1636
+ captureTarget = presetContext.targetRoot;
1637
+ captureScroll = {
1638
+ x: presetContext.scrollX,
1639
+ y: presetContext.scrollY,
1640
+ };
1641
+ cleanupCaptureContext = presetContext.cleanup;
1642
+ }
1643
+ const ignoreElements = (element) => isWidgetElement(element);
1644
+ let canvas;
1645
+ if (mode === "element" && selectedElement) {
1646
+ const rect = selectedElement.getBoundingClientRect();
1647
+ try {
1648
+ const viewportCanvas = await renderWithHtml2Canvas("viewport", captureDocument.documentElement, scale, ignoreElements, live, captureDocument, { x: window.scrollX, y: window.scrollY });
1649
+ canvas = cropElementFromViewportCanvas(viewportCanvas, rect, scale, safeElementPaddingPx);
1650
+ }
1651
+ catch {
1652
+ try {
1653
+ const viewportCanvas = await renderWithHtmlToImageFallback("viewport", captureDocument.documentElement, scale, live, captureDocument, { x: window.scrollX, y: window.scrollY });
1654
+ canvas = cropElementFromViewportCanvas(viewportCanvas, rect, scale, safeElementPaddingPx);
1655
+ }
1656
+ catch {
1657
+ canvas = await renderWithHtml2Canvas(mode, captureTarget, scale, ignoreElements, captureViewport, captureDocument, captureScroll);
1658
+ }
1659
+ }
1660
+ }
1661
+ else {
1662
+ try {
1663
+ canvas = await renderWithHtml2Canvas(mode, captureTarget, scale, ignoreElements, captureViewport, captureDocument, captureScroll);
1664
+ }
1665
+ catch {
1666
+ canvas = await renderWithHtmlToImageFallback(mode, captureTarget, scale, captureViewport, captureDocument, captureScroll);
1667
+ }
1668
+ }
1669
+ const mimeType = format === "jpeg" ? "image/jpeg" : "image/png";
1670
+ const encoded = canvas.toDataURL(mimeType, format === "jpeg" ? quality / 100 : undefined);
1671
+ if (typeof encoded !== "string" || !encoded) {
1672
+ throw new Error("Canvas encoder returned an empty image.");
1673
+ }
1674
+ const imageBase64 = encoded.includes(",") ? encoded.split(",")[1] : encoded;
1675
+ if (!imageBase64) {
1676
+ throw new Error("Failed to encode image.");
1677
+ }
1678
+ const now = new Date().toISOString();
1679
+ const elementTarget = mode === "element" ? selectedElement : null;
1680
+ const payload = {
1681
+ project,
1682
+ route: window.location.pathname || "/",
1683
+ mode,
1684
+ format,
1685
+ quality,
1686
+ scale,
1687
+ theme,
1688
+ selector: elementTarget ? toSelector(elementTarget) : undefined,
1689
+ selectorName: elementTarget ? toSelectorName(elementTarget) : undefined,
1690
+ viewport: captureViewport,
1691
+ capturedAt: now,
1692
+ imageBase64,
1693
+ };
1694
+ return await postCapture(payload);
1695
+ }
1696
+ finally {
1697
+ cleanupCaptureContext();
1698
+ runIfMounted(() => setHideUiForCapture(false));
1699
+ }
1700
+ }, [
1701
+ format,
1702
+ mode,
1703
+ postCapture,
1704
+ presetKey,
1705
+ project,
1706
+ quality,
1707
+ runIfMounted,
1708
+ safeElementPaddingPx,
1709
+ scale,
1710
+ ]);
1711
+ const executeCapture = useCallback(async (selectedElement) => {
1712
+ if (isCaptureInFlightRef.current)
1713
+ return;
1714
+ if (mode === "element" && !selectedElement) {
1715
+ runIfMounted(() => setStatus({
1716
+ kind: "error",
1717
+ message: "Pick an element first.",
1718
+ }));
1719
+ return;
1720
+ }
1721
+ if (themeSelection === "both" && !themeAdapter) {
1722
+ runIfMounted(() => setStatus({
1723
+ kind: "error",
1724
+ message: "Theme adapter required for both-theme capture.",
1725
+ }));
1726
+ return;
1727
+ }
1728
+ isCaptureInFlightRef.current = true;
1729
+ runIfMounted(() => {
1730
+ setIsSaving(true);
1731
+ setStatus({
1732
+ kind: "saving",
1733
+ message: "Capturing...",
1734
+ });
1735
+ });
1736
+ let originalTheme = "light";
1737
+ const results = [];
1738
+ try {
1739
+ originalTheme = getCurrentTheme();
1740
+ const captureThemes = themeSelection === "both"
1741
+ ? [originalTheme, oppositeTheme(originalTheme)]
1742
+ : [originalTheme];
1743
+ for (const theme of captureThemes) {
1744
+ if (themeAdapter) {
1745
+ await themeAdapter.setTheme(theme);
1746
+ await waitForMs(120);
1747
+ }
1748
+ if (captureSettleMs > 0) {
1749
+ await waitForMs(captureSettleMs);
1750
+ }
1751
+ const result = await runSingleCapture(theme, selectedElement);
1752
+ results.push(result);
1753
+ onSaved?.(result);
1754
+ }
1755
+ const last = results[results.length - 1];
1756
+ runIfMounted(() => setStatus({
1757
+ kind: "success",
1758
+ message: results.length === 1
1759
+ ? `Saved ${last.relativePath}`
1760
+ : `Saved ${results.length} files. Last: ${last.relativePath}`,
1761
+ }));
1762
+ }
1763
+ catch (error) {
1764
+ if (isAbortError(error))
1765
+ return;
1766
+ const message = toFriendlyError(error);
1767
+ runIfMounted(() => setStatus({
1768
+ kind: "error",
1769
+ message,
1770
+ }));
1771
+ onError?.(message);
1772
+ }
1773
+ finally {
1774
+ if (themeAdapter) {
1775
+ try {
1776
+ await themeAdapter.setTheme(originalTheme);
1777
+ }
1778
+ catch {
1779
+ // ignore restoration failures in UI flow
1780
+ }
1781
+ }
1782
+ isCaptureInFlightRef.current = false;
1783
+ runIfMounted(() => setIsSaving(false));
1784
+ }
1785
+ }, [
1786
+ captureSettleMs,
1787
+ getCurrentTheme,
1788
+ mode,
1789
+ onError,
1790
+ onSaved,
1791
+ runIfMounted,
1792
+ runSingleCapture,
1793
+ themeAdapter,
1794
+ themeSelection,
1795
+ ]);
1796
+ useEffect(() => {
1797
+ if (!isEnabled)
1798
+ return;
1799
+ const onKeyDown = (event) => {
1800
+ if (!(event.metaKey || event.ctrlKey) || !event.shiftKey)
1801
+ return;
1802
+ if (event.key.toLowerCase() !== "k")
1803
+ return;
1804
+ if (isEditableTarget(event.target))
1805
+ return;
1806
+ event.preventDefault();
1807
+ setIsPanelOpen((open) => !open);
1808
+ };
1809
+ window.addEventListener("keydown", onKeyDown);
1810
+ return () => {
1811
+ window.removeEventListener("keydown", onKeyDown);
1812
+ };
1813
+ }, [isEnabled]);
1814
+ useEffect(() => {
1815
+ if (!isPickingElement)
1816
+ return;
1817
+ assertBrowser();
1818
+ const overlay = document.createElement("div");
1819
+ overlay.setAttribute(UI_MARKER_ATTR, "true");
1820
+ overlay.setAttribute("data-testid", "screenshotter-picker-overlay");
1821
+ overlay.style.position = "fixed";
1822
+ overlay.style.left = "0";
1823
+ overlay.style.top = "0";
1824
+ overlay.style.width = "0";
1825
+ overlay.style.height = "0";
1826
+ overlay.style.border = "2px solid #00d2ff";
1827
+ overlay.style.background = "rgba(0, 210, 255, 0.1)";
1828
+ overlay.style.borderRadius = "8px";
1829
+ overlay.style.pointerEvents = "none";
1830
+ overlay.style.zIndex = "70";
1831
+ overlay.style.display = "none";
1832
+ const badge = document.createElement("div");
1833
+ badge.setAttribute(UI_MARKER_ATTR, "true");
1834
+ badge.style.position = "fixed";
1835
+ badge.style.left = "0";
1836
+ badge.style.top = "0";
1837
+ badge.style.padding = "5px 8px";
1838
+ badge.style.background = "rgba(3, 7, 18, 0.94)";
1839
+ badge.style.color = "#e5e7eb";
1840
+ badge.style.fontSize = "11px";
1841
+ badge.style.fontFamily = "'IBM Plex Mono', 'JetBrains Mono', monospace";
1842
+ badge.style.pointerEvents = "none";
1843
+ badge.style.border = "1px solid rgba(0, 210, 255, 0.45)";
1844
+ badge.style.borderRadius = "6px";
1845
+ badge.style.zIndex = "71";
1846
+ badge.style.display = "none";
1847
+ document.body.appendChild(overlay);
1848
+ document.body.appendChild(badge);
1849
+ let highlightedTarget = null;
1850
+ const setTarget = (target) => {
1851
+ if (!target || isWidgetElement(target)) {
1852
+ highlightedTarget = null;
1853
+ overlay.style.display = "none";
1854
+ badge.style.display = "none";
1855
+ return;
1856
+ }
1857
+ const rect = target.getBoundingClientRect();
1858
+ if (!rect.width || !rect.height) {
1859
+ highlightedTarget = null;
1860
+ overlay.style.display = "none";
1861
+ badge.style.display = "none";
1862
+ return;
1863
+ }
1864
+ highlightedTarget = target;
1865
+ overlay.style.display = "block";
1866
+ const paddedLeft = rect.left - safeElementPaddingPx;
1867
+ const paddedTop = rect.top - safeElementPaddingPx;
1868
+ const paddedWidth = rect.width + safeElementPaddingPx * 2;
1869
+ const paddedHeight = rect.height + safeElementPaddingPx * 2;
1870
+ overlay.style.left = `${paddedLeft}px`;
1871
+ overlay.style.top = `${paddedTop}px`;
1872
+ overlay.style.width = `${paddedWidth}px`;
1873
+ overlay.style.height = `${paddedHeight}px`;
1874
+ badge.style.display = "block";
1875
+ badge.style.left = `${Math.max(8, rect.left)}px`;
1876
+ badge.style.top = `${Math.max(8, rect.top - 30)}px`;
1877
+ badge.textContent = toSelectorName(target);
1878
+ };
1879
+ const onMouseMove = (event) => {
1880
+ const pointerTarget = typeof document.elementFromPoint === "function"
1881
+ ? document.elementFromPoint(event.clientX, event.clientY)
1882
+ : null;
1883
+ const rawTarget = pointerTarget instanceof HTMLElement
1884
+ ? pointerTarget
1885
+ : event.target instanceof HTMLElement
1886
+ ? event.target
1887
+ : null;
1888
+ if (!rawTarget) {
1889
+ setTarget(null);
1890
+ return;
1891
+ }
1892
+ const target = resolveElementCaptureTarget(rawTarget);
1893
+ setTarget(target);
1894
+ };
1895
+ const onClick = (event) => {
1896
+ const rawTarget = event.target instanceof HTMLElement ? event.target : null;
1897
+ let target = null;
1898
+ if (highlightedTarget) {
1899
+ const rect = highlightedTarget.getBoundingClientRect();
1900
+ if (isPointInsideRect(event.clientX, event.clientY, rect)) {
1901
+ target = highlightedTarget;
1902
+ }
1903
+ }
1904
+ if (!target && rawTarget) {
1905
+ target = resolveElementCaptureTarget(rawTarget);
1906
+ }
1907
+ if (!target || isWidgetElement(target))
1908
+ return;
1909
+ event.preventDefault();
1910
+ event.stopPropagation();
1911
+ setTarget(null);
1912
+ setIsPickingElement(false);
1913
+ void executeCapture(target);
1914
+ };
1915
+ const onKeyDown = (event) => {
1916
+ if (event.key !== "Escape")
1917
+ return;
1918
+ setIsPickingElement(false);
1919
+ setStatus({
1920
+ kind: "info",
1921
+ message: "Element picking canceled.",
1922
+ });
1923
+ };
1924
+ setStatus({
1925
+ kind: "info",
1926
+ message: "Click an element to capture.",
1927
+ });
1928
+ document.addEventListener("mousemove", onMouseMove, true);
1929
+ document.addEventListener("click", onClick, true);
1930
+ document.addEventListener("keydown", onKeyDown, true);
1931
+ return () => {
1932
+ document.removeEventListener("mousemove", onMouseMove, true);
1933
+ document.removeEventListener("click", onClick, true);
1934
+ document.removeEventListener("keydown", onKeyDown, true);
1935
+ overlay.remove();
1936
+ badge.remove();
1937
+ };
1938
+ }, [executeCapture, isPickingElement, safeElementPaddingPx]);
1939
+ const currentActionLabel = actionLabel(mode, isPickingElement, isSaving);
1940
+ const actionDisabled = isSaving ||
1941
+ (mode === "element" ? isPickingElement : false) ||
1942
+ (themeSelection === "both" && !canCaptureBothThemes);
1943
+ const currentStatusChipLabel = statusChipLabel(status.kind);
1944
+ const qualityPct = useMemo(() => ((quality - 1) / 99) * 100, [quality]);
1945
+ const paddingPct = useMemo(() => (safeElementPaddingPx / 32) * 100, [safeElementPaddingPx]);
1946
+ if (!isEnabled)
1947
+ return null;
1948
+ return (_jsxs("div", { ref: rootRef, "data-screenshotter-ui": "true", "data-ui-theme": uiTheme, className: "ssw-root", style: {
1949
+ position: "fixed",
1950
+ right: "calc(16px + env(safe-area-inset-right, 0px))",
1951
+ bottom: "calc(16px + env(safe-area-inset-bottom, 0px))",
1952
+ zIndex: 60,
1953
+ opacity: hideUiForCapture ? 0 : 1,
1954
+ pointerEvents: hideUiForCapture ? "none" : "auto",
1955
+ transition: "opacity 120ms ease-out",
1956
+ }, children: [_jsx("style", { "data-screenshotter-ui": "true", children: WIDGET_PANEL_CSS }), _jsx("button", { type: "button", "data-testid": "screenshotter-launcher", "aria-label": "Toggle screenshot panel", className: "ui-btn ui-btn-outline ui-focus ssw-launcher", onClick: () => setIsPanelOpen((value) => !value), children: "Shot" }), _jsxs("section", { "data-testid": "screenshotter-panel", "aria-hidden": !isPanelOpen, className: "ui-panel ssw-panel", "data-open": isPanelOpen ? "true" : "false", children: [_jsxs("div", { className: "ssw-header", children: [_jsxs("div", { children: [_jsx("h3", { className: "ssw-title", children: "Screenshotter" }), _jsx("p", { className: "ssw-subtitle", children: "Only what matters" })] }), _jsxs("div", { className: "ssw-header-actions", children: [_jsx("span", { className: "ssw-hotkey", children: "Cmd/Ctrl + Shift + K" }), _jsx("button", { type: "button", className: "ui-btn ui-btn-ghost ui-focus ssw-icon-btn", "aria-label": "Close screenshot panel", onClick: () => setIsPanelOpen(false), children: "\u00D7" })] })] }), _jsxs("div", { className: "ssw-body", children: [_jsxs("div", { className: "ssw-group", children: [_jsx("p", { className: "ssw-group-title", children: "Capture" }), _jsx("div", { className: "ui-toggle-group", children: _jsx("div", { className: "ui-seg-row", children: CAPTURE_MODE_OPTIONS.map((value) => (_jsx("button", { type: "button", "data-testid": `mode-${value}`, "aria-label": `Switch to ${modeLabel(value)} mode`, "aria-pressed": mode === value, "data-active": mode === value ? "true" : "false", className: "ui-btn ui-btn-outline ui-focus ui-seg-item", onClick: () => setMode(value), children: modeLabel(value) }, value))) }) })] }), _jsxs("div", { className: "ssw-group", children: [_jsx("p", { className: "ssw-group-title", children: "Output" }), _jsx("div", { className: "ui-toggle-group", children: _jsx("div", { className: "ui-seg-row ssw-output-row", children: FORMAT_OPTIONS.map((value) => (_jsx("button", { type: "button", "aria-label": `Use ${value.toUpperCase()} format`, "aria-pressed": format === value, "data-active": format === value ? "true" : "false", className: "ui-btn ui-btn-outline ui-focus ui-seg-item", onClick: () => setFormat(value), children: value.toUpperCase() }, value))) }) })] }), _jsxs("div", { className: "ssw-advanced-card", "data-open": isAdvancedOpen ? "true" : "false", children: [_jsxs("button", { type: "button", "aria-expanded": isAdvancedOpen, className: "ui-btn ui-btn-ghost ui-focus ssw-advanced-toggle", onClick: () => setIsAdvancedOpen((open) => !open), children: [_jsx("span", { children: "Advanced" }), _jsx("span", { className: `ssw-chevron${isAdvancedOpen ? " is-open" : ""}`, children: "\u25BE" })] }), isAdvancedOpen ? (_jsxs("div", { className: "ssw-advanced-body", children: [format === "jpeg" ? (_jsxs("div", { className: "ssw-setting", children: [_jsxs("div", { children: [_jsx("p", { className: "ssw-setting-label", children: "JPEG quality" }), _jsx("p", { className: "ssw-setting-help", children: "JPEG only" })] }), _jsxs("div", { children: [_jsxs("span", { className: "ssw-setting-value", children: [quality, "%"] }), _jsx("div", { className: "ui-range", children: _jsx("input", { "aria-label": "JPEG quality", type: "range", min: 1, max: 100, value: quality, style: { "--pct": `${qualityPct}%` }, onChange: (event) => setQuality(Number(event.currentTarget.value)) }) })] })] })) : null, mode === "element" ? (_jsxs("div", { className: "ssw-setting", children: [_jsxs("div", { children: [_jsx("p", { className: "ssw-setting-label", children: "Padding" }), _jsx("p", { className: "ssw-setting-help", children: "Element capture" })] }), _jsxs("div", { children: [_jsxs("span", { className: "ssw-setting-value", children: [safeElementPaddingPx, "px"] }), _jsx("div", { className: "ui-range", children: _jsx("input", { "aria-label": "Element padding", "data-testid": "element-padding", type: "range", min: 0, max: 32, step: 1, value: safeElementPaddingPx, style: { "--pct": `${paddingPct}%` }, onChange: (event) => setElementPadding(Number(event.currentTarget.value)) }) })] })] })) : null, mode !== "element" ? (_jsxs("div", { className: "ssw-setting", children: [_jsxs("div", { children: [_jsx("p", { className: "ssw-setting-label", children: "Preset" }), _jsx("p", { className: "ssw-setting-help", children: "Viewport and full page only" })] }), _jsxs("div", { children: [_jsxs("select", { "aria-label": "Capture preset", "data-testid": "capture-preset-select", className: "ui-focus ssw-preset-select", value: presetKey, onChange: (event) => setPresetKey(event.currentTarget.value), children: [_jsx("option", { value: LIVE_VIEWPORT_PRESET_KEY, children: "Current window" }), VIEWPORT_PRESET_GROUPS.map((group) => (_jsx("optgroup", { label: VIEWPORT_PRESET_GROUP_LABELS[group], children: VIEWPORT_PRESETS_BY_GROUP[group].map((preset) => (_jsx("option", { value: preset.key, children: preset.label }, preset.key))) }, group)))] }), _jsx("span", { className: "ssw-setting-value ssw-setting-value-left", children: presetDescription })] })] })) : null, _jsxs("div", { className: "ssw-setting", children: [_jsxs("div", { children: [_jsx("p", { className: "ssw-setting-label", children: "Theme" }), _jsx("p", { className: "ssw-setting-help", children: canCaptureBothThemes
1957
+ ? "Current or both themes"
1958
+ : "Dual-theme capture unavailable" })] }), _jsx("div", { className: "ui-toggle-group", children: _jsx("div", { className: "ssw-mini-segment", children: THEME_OPTIONS.map((value) => {
1959
+ const disabled = value === "both" && !canCaptureBothThemes;
1960
+ const active = themeSelection === value;
1961
+ return (_jsx("button", { type: "button", "aria-label": `Set theme capture to ${value}`, "aria-pressed": active, "data-active": active ? "true" : "false", className: "ui-btn ui-btn-outline ui-focus ui-seg-item", disabled: disabled, onClick: () => setThemeSelection(value), children: value }, value));
1962
+ }) }) })] })] })) : null] }), _jsxs("div", { className: "ssw-footer", children: [_jsx("span", { className: "ssw-status-chip", "data-kind": status.kind, children: currentStatusChipLabel }), _jsx("button", { type: "button", "data-testid": "action-button", "aria-label": mode === "element" ? "Pick element to capture" : "Capture screenshot", className: "ui-btn ui-btn-primary ui-focus ssw-action-btn", disabled: actionDisabled, onClick: () => {
1963
+ if (mode === "element") {
1964
+ setMode("element");
1965
+ setIsPickingElement(true);
1966
+ return;
1967
+ }
1968
+ void executeCapture(null);
1969
+ }, children: currentActionLabel })] }), isStatusToastVisible ? (_jsx("p", { className: "ssw-toast", "data-kind": status.kind, children: status.message })) : null] })] })] }));
1970
+ }
1971
+ export default ScreenshotterWidget;
1972
+ //# sourceMappingURL=ScreenshotterWidget.js.map