@loupekit/sdk 0.1.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,2479 @@
1
+ "use strict";
2
+ var Loupe = (() => {
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var src_exports = {};
23
+ __export(src_exports, {
24
+ destroy: () => destroy,
25
+ init: () => init
26
+ });
27
+
28
+ // src/styles.ts
29
+ var STYLES = (
30
+ /* css */
31
+ `
32
+ :host { all: initial; }
33
+ * { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; }
34
+
35
+ :root, .loupe {
36
+ --accent: #4a55d6;
37
+ --pin: #ff5842;
38
+ --ink: #16181f;
39
+ --panel: #ffffff;
40
+ --panel-2: #f4f5f9;
41
+ --line: #e2e5ee;
42
+ --muted: #6b7180;
43
+ }
44
+
45
+ .overlay {
46
+ position: fixed; inset: 0; z-index: 2147483000; pointer-events: none;
47
+ }
48
+
49
+ /* inspector highlight */
50
+ .hl {
51
+ position: fixed; pointer-events: none; z-index: 2147483001;
52
+ border: 2px solid var(--accent);
53
+ background: rgba(74, 85, 214, 0.10);
54
+ border-radius: 4px; display: none;
55
+ transition: all 60ms linear;
56
+ }
57
+ .hl .tip {
58
+ position: absolute; top: -24px; left: 0; background: var(--accent); color: #fff;
59
+ font-size: 11px; font-weight: 600; padding: 2px 7px; border-radius: 4px; white-space: nowrap;
60
+ font-family: ui-monospace, Menlo, monospace;
61
+ }
62
+
63
+ /* pins */
64
+ .pin {
65
+ position: fixed; pointer-events: auto; z-index: 2147483002;
66
+ width: 26px; height: 26px; border-radius: 50% 50% 50% 2px;
67
+ background: var(--pin); color: #fff; border: 2px solid #fff;
68
+ font-size: 12px; font-weight: 700; cursor: pointer;
69
+ box-shadow: 0 2px 8px rgba(0,0,0,.35);
70
+ display: grid; place-items: center; transform: translate(-4px, -4px);
71
+ transition: transform 80ms ease;
72
+ }
73
+ .pin:hover { transform: translate(-4px, -4px) scale(1.12); }
74
+ .pin.detached { background: #9aa0af; }
75
+ .pin.done { background: #10935a; }
76
+ .pin.active { outline: 3px solid rgba(74,85,214,.4); }
77
+
78
+ /* toolbar */
79
+ .toolbar {
80
+ position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
81
+ z-index: 2147483003; pointer-events: auto;
82
+ display: flex; align-items: center; gap: 4px;
83
+ background: #1b1e27; color: #fff; padding: 6px; border-radius: 12px;
84
+ box-shadow: 0 8px 30px rgba(0,0,0,.4); border: 1px solid #2b2f3b;
85
+ }
86
+ .toolbar button {
87
+ background: transparent; color: #cfd3de; border: 0; cursor: pointer;
88
+ font-size: 13px; font-weight: 500; padding: 8px 12px; border-radius: 8px;
89
+ display: flex; align-items: center; gap: 6px;
90
+ }
91
+ .toolbar button:hover { background: #2b2f3b; color: #fff; }
92
+ .toolbar button.on { background: var(--accent); color: #fff; }
93
+ .toolbar .sep { width: 1px; height: 20px; background: #333846; margin: 0 2px; }
94
+ .toolbar .brand { font-weight: 700; padding-left: 8px; padding-right: 4px; letter-spacing: -.01em; }
95
+ .toolbar .count {
96
+ background: var(--pin); color: #fff; font-size: 11px; font-weight: 700;
97
+ border-radius: 999px; padding: 1px 7px; margin-left: 2px;
98
+ }
99
+
100
+ /* composer popover */
101
+ .composer {
102
+ position: fixed; z-index: 2147483004; pointer-events: auto; width: 300px;
103
+ background: var(--panel); color: var(--ink); border: 1px solid var(--line);
104
+ border-radius: 12px; box-shadow: 0 12px 40px rgba(0,0,0,.22); padding: 12px; display: none;
105
+ }
106
+ .composer .target {
107
+ font-family: ui-monospace, Menlo, monospace; font-size: 11px; color: var(--accent);
108
+ background: var(--panel-2); border-radius: 6px; padding: 5px 8px; margin-bottom: 8px;
109
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
110
+ }
111
+ .composer textarea {
112
+ width: 100%; min-height: 68px; resize: vertical; border: 1px solid var(--line);
113
+ border-radius: 8px; padding: 8px; font-size: 13px; color: var(--ink); outline: none;
114
+ }
115
+ .composer textarea:focus { border-color: var(--accent); }
116
+ .composer .row { display: flex; align-items: center; justify-content: space-between; margin-top: 8px; gap: 8px; }
117
+ .composer label.chk { font-size: 12px; color: var(--muted); display: flex; align-items: center; gap: 6px; cursor: pointer; }
118
+ .composer .btns { display: flex; gap: 6px; }
119
+ .composer button {
120
+ border: 0; border-radius: 8px; font-size: 13px; font-weight: 600; padding: 7px 12px; cursor: pointer;
121
+ }
122
+ .composer .primary { background: var(--accent); color: #fff; }
123
+ .composer .primary:disabled { opacity: .5; cursor: default; }
124
+ .composer .ghost { background: var(--panel-2); color: var(--ink); }
125
+
126
+ /* panel (comment list) */
127
+ .panel {
128
+ position: fixed; right: 16px; bottom: 80px; top: 16px; width: 340px;
129
+ z-index: 2147483003; pointer-events: auto; display: none; flex-direction: column;
130
+ background: var(--panel); color: var(--ink); border: 1px solid var(--line);
131
+ border-radius: 14px; box-shadow: 0 12px 40px rgba(0,0,0,.22); overflow: hidden;
132
+ }
133
+ .panel.open { display: flex; }
134
+ .panel .phead { padding: 14px 16px; border-bottom: 1px solid var(--line); font-weight: 700; display:flex; justify-content:space-between; align-items:center; }
135
+ .panel .phead .x { cursor:pointer; color: var(--muted); background:none; border:0; font-size:18px; }
136
+ .panel .list { overflow-y: auto; padding: 8px; display: flex; flex-direction: column; gap: 8px; }
137
+ .panel .empty { color: var(--muted); font-size: 13px; padding: 24px 12px; text-align: center; }
138
+ .item {
139
+ border: 1px solid var(--line); border-radius: 10px; padding: 10px; cursor: pointer; background: var(--panel);
140
+ }
141
+ .item:hover { border-color: var(--accent); }
142
+ .item .top { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
143
+ .item .num { background: var(--pin); color:#fff; width:20px; height:20px; border-radius:50%; font-size:11px; font-weight:700; display:grid; place-items:center; }
144
+ .item .num.detached { background:#9aa0af; }
145
+ .item .num.done { background:#10935a; }
146
+ .item .who { font-size: 12px; font-weight: 600; }
147
+ .item .body { font-size: 13px; line-height: 1.4; }
148
+ .item .meta { font-size: 11px; color: var(--muted); margin-top: 6px; font-family: ui-monospace, Menlo, monospace; }
149
+ .item .badge { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; padding: 1px 6px; border-radius: 5px; margin-left: auto; }
150
+ .badge.detached { background: #eceef2; color: #6b7180; }
151
+ .badge.done { background: #d8f0e4; color: #10935a; }
152
+ .item .actions { display: flex; gap: 6px; margin-top: 8px; }
153
+ .item .actions button { font-size: 11px; border: 1px solid var(--line); background: var(--panel-2); border-radius: 6px; padding: 4px 8px; cursor: pointer; color: var(--ink); }
154
+ .item img.shot { width: 100%; border-radius: 6px; margin-top: 8px; border: 1px solid var(--line); }
155
+ `
156
+ );
157
+
158
+ // src/fingerprint.ts
159
+ var TEXT_MAX = 120;
160
+ var ATTR_KEYS = ["role", "aria-label", "name", "type", "alt", "href", "placeholder", "title"];
161
+ var ACCEPT_THRESHOLD = 0.5;
162
+ var W = { tag: 0.12, text: 0.34, attrs: 0.22, testid: 0.22, cssPath: 0.2, position: 0.1 };
163
+ function captureAnchor(el2) {
164
+ const rect = el2.getBoundingClientRect();
165
+ return {
166
+ tag: el2.tagName.toLowerCase(),
167
+ cssPath: cssPath(el2),
168
+ xpath: xPath(el2),
169
+ testid: stableId(el2),
170
+ text: normText(el2.textContent),
171
+ attrs: readAttrs(el2),
172
+ nthOfType: nthOfType(el2),
173
+ rect: {
174
+ x: Math.round(rect.left + window.scrollX),
175
+ y: Math.round(rect.top + window.scrollY),
176
+ w: Math.round(rect.width),
177
+ h: Math.round(rect.height)
178
+ },
179
+ viewport: { w: window.innerWidth, h: window.innerHeight }
180
+ };
181
+ }
182
+ function resolveAnchor(anchor, root = document) {
183
+ if (anchor.testid) {
184
+ const byTestid = safeQueryAll(root, `[data-testid="${cssStr(anchor.testid)}"], [data-test="${cssStr(anchor.testid)}"]`);
185
+ const byId = document.getElementById(anchor.testid);
186
+ const hits = dedupe([...byTestid, ...byId ? [byId] : []]).filter(usable);
187
+ if (hits.length === 1) return { element: hits[0], score: 0.98, via: "testid" };
188
+ }
189
+ if (/^(\[data-testid=|#)/.test(anchor.cssPath)) {
190
+ const hits = safeQueryAll(root, anchor.cssPath).filter(usable);
191
+ if (hits.length === 1 && hits[0].tagName.toLowerCase() === anchor.tag) {
192
+ return { element: hits[0], score: 0.9, via: "cssPath" };
193
+ }
194
+ }
195
+ const candidates = /* @__PURE__ */ new Map();
196
+ const consider = (el2, via) => {
197
+ if (el2 && usable(el2) && !candidates.has(el2)) candidates.set(el2, via);
198
+ };
199
+ consider(safeQuery(root, anchor.cssPath), "cssPath");
200
+ consider(byXPath(anchor.xpath), "xpath");
201
+ for (const el2 of safeQueryAll(root, anchor.tag)) consider(el2, "scan");
202
+ if (anchor.text) {
203
+ for (const el2 of safeQueryAll(root, "*")) {
204
+ if (candidates.size > 4e3) break;
205
+ if (normText(el2.textContent) === anchor.text) consider(el2, "scan");
206
+ }
207
+ }
208
+ let best = null;
209
+ for (const [el2, via] of candidates) {
210
+ const score = similarity(anchor, el2, via);
211
+ if (!best || score > best.score) best = { element: el2, score, via };
212
+ }
213
+ return best && best.score >= ACCEPT_THRESHOLD ? best : null;
214
+ }
215
+ function similarity(a, el2, via) {
216
+ let sum = 0;
217
+ let total = 0;
218
+ const add = (w, v) => {
219
+ sum += w * v;
220
+ total += w;
221
+ };
222
+ add(W.tag, el2.tagName.toLowerCase() === a.tag ? 1 : 0);
223
+ if (a.text) {
224
+ const t = normText(el2.textContent);
225
+ add(W.text, t === a.text ? 1 : t && (t.includes(a.text) || a.text.includes(t)) ? 0.5 : 0);
226
+ }
227
+ const attrKeys = Object.keys(a.attrs);
228
+ if (attrKeys.length) {
229
+ let matched = 0;
230
+ for (const k of attrKeys) if (el2.getAttribute(k) === a.attrs[k]) matched++;
231
+ add(W.attrs, matched / attrKeys.length);
232
+ }
233
+ if (a.testid) add(W.testid, stableId(el2) === a.testid ? 1 : 0);
234
+ add(W.cssPath, via === "cssPath" ? 1 : 0);
235
+ const r = el2.getBoundingClientRect();
236
+ const cx = r.left + window.scrollX + r.width / 2;
237
+ const cy = r.top + window.scrollY + r.height / 2;
238
+ const ax = a.rect.x + a.rect.w / 2;
239
+ const ay = a.rect.y + a.rect.h / 2;
240
+ const diag = Math.hypot(a.viewport.w, a.viewport.h) || 1;
241
+ const dist = Math.hypot(cx - ax, cy - ay);
242
+ add(W.position, Math.max(0, 1 - dist / diag));
243
+ return total ? sum / total : 0;
244
+ }
245
+ function cssPath(el2) {
246
+ const segs = [];
247
+ let node = el2;
248
+ while (node && node.nodeType === 1 && node !== document.documentElement) {
249
+ const id = node.getAttribute("id");
250
+ const testid = node.getAttribute("data-testid") || node.getAttribute("data-test");
251
+ if (id && isStable(id)) {
252
+ segs.unshift(`#${cssEsc(id)}`);
253
+ break;
254
+ }
255
+ if (testid) {
256
+ segs.unshift(`[data-testid="${cssStr(testid)}"]`);
257
+ break;
258
+ }
259
+ segs.unshift(`${node.tagName.toLowerCase()}:nth-of-type(${nthOfType(node)})`);
260
+ node = node.parentElement;
261
+ if (node === document.body) {
262
+ segs.unshift("body");
263
+ break;
264
+ }
265
+ }
266
+ return segs.join(" > ");
267
+ }
268
+ function xPath(el2) {
269
+ const segs = [];
270
+ let node = el2;
271
+ while (node && node.nodeType === 1 && node !== document.documentElement) {
272
+ segs.unshift(`${node.tagName.toLowerCase()}[${nthOfType(node)}]`);
273
+ node = node.parentElement;
274
+ }
275
+ return "/html/" + segs.join("/");
276
+ }
277
+ function nthOfType(el2) {
278
+ let i = 1;
279
+ let sib = el2.previousElementSibling;
280
+ while (sib) {
281
+ if (sib.tagName === el2.tagName) i++;
282
+ sib = sib.previousElementSibling;
283
+ }
284
+ return i;
285
+ }
286
+ function stableId(el2) {
287
+ const testid = el2.getAttribute("data-testid") || el2.getAttribute("data-test");
288
+ if (testid) return testid;
289
+ const id = el2.getAttribute("id");
290
+ return id && isStable(id) ? id : null;
291
+ }
292
+ function isStable(id) {
293
+ if (id.length > 40) return false;
294
+ if (/^(ember|react|radix|headlessui|:r)/i.test(id)) return false;
295
+ if (/[:]/.test(id)) return false;
296
+ return true;
297
+ }
298
+ function readAttrs(el2) {
299
+ const out = {};
300
+ for (const k of ATTR_KEYS) {
301
+ const v = el2.getAttribute(k);
302
+ if (v != null && v !== "") out[k] = v.length > 200 ? v.slice(0, 200) : v;
303
+ }
304
+ return out;
305
+ }
306
+ function normText(t) {
307
+ return (t || "").replace(/\s+/g, " ").trim().slice(0, TEXT_MAX);
308
+ }
309
+ function usable(el2) {
310
+ if (el2.id === "loupe-root" || el2.closest("#loupe-root")) return false;
311
+ if (el2 === document.body || el2 === document.documentElement) return false;
312
+ return true;
313
+ }
314
+ function byXPath(xp) {
315
+ try {
316
+ const r = document.evaluate(xp, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
317
+ return r.singleNodeValue instanceof Element ? r.singleNodeValue : null;
318
+ } catch {
319
+ return null;
320
+ }
321
+ }
322
+ function safeQuery(root, sel) {
323
+ try {
324
+ return sel ? root.querySelector(sel) : null;
325
+ } catch {
326
+ return null;
327
+ }
328
+ }
329
+ function safeQueryAll(root, sel) {
330
+ try {
331
+ return sel ? Array.from(root.querySelectorAll(sel)) : [];
332
+ } catch {
333
+ return [];
334
+ }
335
+ }
336
+ function dedupe(els) {
337
+ return Array.from(new Set(els));
338
+ }
339
+ function cssEsc(s) {
340
+ return window.CSS && CSS.escape ? CSS.escape(s) : s.replace(/["\\]/g, "\\$&");
341
+ }
342
+ function cssStr(s) {
343
+ return s.replace(/["\\]/g, "\\$&");
344
+ }
345
+
346
+ // ../../node_modules/modern-screenshot/dist/index.mjs
347
+ function changeJpegDpi(uint8Array, dpi) {
348
+ uint8Array[13] = 1;
349
+ uint8Array[14] = dpi >> 8;
350
+ uint8Array[15] = dpi & 255;
351
+ uint8Array[16] = dpi >> 8;
352
+ uint8Array[17] = dpi & 255;
353
+ return uint8Array;
354
+ }
355
+ var _P = "p".charCodeAt(0);
356
+ var _H = "H".charCodeAt(0);
357
+ var _Y = "Y".charCodeAt(0);
358
+ var _S = "s".charCodeAt(0);
359
+ var pngDataTable;
360
+ function createPngDataTable() {
361
+ const crcTable = new Int32Array(256);
362
+ for (let n = 0; n < 256; n++) {
363
+ let c = n;
364
+ for (let k = 0; k < 8; k++) {
365
+ c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
366
+ }
367
+ crcTable[n] = c;
368
+ }
369
+ return crcTable;
370
+ }
371
+ function calcCrc(uint8Array) {
372
+ let c = -1;
373
+ if (!pngDataTable)
374
+ pngDataTable = createPngDataTable();
375
+ for (let n = 0; n < uint8Array.length; n++) {
376
+ c = pngDataTable[(c ^ uint8Array[n]) & 255] ^ c >>> 8;
377
+ }
378
+ return c ^ -1;
379
+ }
380
+ function searchStartOfPhys(uint8Array) {
381
+ const length = uint8Array.length - 1;
382
+ for (let i = length; i >= 4; i--) {
383
+ if (uint8Array[i - 4] === 9 && uint8Array[i - 3] === _P && uint8Array[i - 2] === _H && uint8Array[i - 1] === _Y && uint8Array[i] === _S) {
384
+ return i - 3;
385
+ }
386
+ }
387
+ return 0;
388
+ }
389
+ function changePngDpi(uint8Array, dpi, overwritepHYs = false) {
390
+ const physChunk = new Uint8Array(13);
391
+ dpi *= 39.3701;
392
+ physChunk[0] = _P;
393
+ physChunk[1] = _H;
394
+ physChunk[2] = _Y;
395
+ physChunk[3] = _S;
396
+ physChunk[4] = dpi >>> 24;
397
+ physChunk[5] = dpi >>> 16;
398
+ physChunk[6] = dpi >>> 8;
399
+ physChunk[7] = dpi & 255;
400
+ physChunk[8] = physChunk[4];
401
+ physChunk[9] = physChunk[5];
402
+ physChunk[10] = physChunk[6];
403
+ physChunk[11] = physChunk[7];
404
+ physChunk[12] = 1;
405
+ const crc = calcCrc(physChunk);
406
+ const crcChunk = new Uint8Array(4);
407
+ crcChunk[0] = crc >>> 24;
408
+ crcChunk[1] = crc >>> 16;
409
+ crcChunk[2] = crc >>> 8;
410
+ crcChunk[3] = crc & 255;
411
+ if (overwritepHYs) {
412
+ const startingIndex = searchStartOfPhys(uint8Array);
413
+ uint8Array.set(physChunk, startingIndex);
414
+ uint8Array.set(crcChunk, startingIndex + 13);
415
+ return uint8Array;
416
+ } else {
417
+ const chunkLength = new Uint8Array(4);
418
+ chunkLength[0] = 0;
419
+ chunkLength[1] = 0;
420
+ chunkLength[2] = 0;
421
+ chunkLength[3] = 9;
422
+ const finalHeader = new Uint8Array(54);
423
+ finalHeader.set(uint8Array, 0);
424
+ finalHeader.set(chunkLength, 33);
425
+ finalHeader.set(physChunk, 37);
426
+ finalHeader.set(crcChunk, 50);
427
+ return finalHeader;
428
+ }
429
+ }
430
+ var b64PhysSignature1 = "AAlwSFlz";
431
+ var b64PhysSignature2 = "AAAJcEhZ";
432
+ var b64PhysSignature3 = "AAAACXBI";
433
+ function detectPhysChunkFromDataUrl(dataUrl) {
434
+ let b64index = dataUrl.indexOf(b64PhysSignature1);
435
+ if (b64index === -1) {
436
+ b64index = dataUrl.indexOf(b64PhysSignature2);
437
+ }
438
+ if (b64index === -1) {
439
+ b64index = dataUrl.indexOf(b64PhysSignature3);
440
+ }
441
+ return b64index;
442
+ }
443
+ var PREFIX = "[modern-screenshot]";
444
+ var IN_BROWSER = typeof window !== "undefined";
445
+ var SUPPORT_WEB_WORKER = IN_BROWSER && "Worker" in window;
446
+ var SUPPORT_ATOB = IN_BROWSER && "atob" in window;
447
+ var SUPPORT_BTOA = IN_BROWSER && "btoa" in window;
448
+ var USER_AGENT = IN_BROWSER ? window.navigator?.userAgent : "";
449
+ var IN_CHROME = USER_AGENT.includes("Chrome");
450
+ var IN_SAFARI = USER_AGENT.includes("AppleWebKit") && !IN_CHROME;
451
+ var IN_FIREFOX = USER_AGENT.includes("Firefox");
452
+ var isContext = (value) => value && "__CONTEXT__" in value;
453
+ var isCssFontFaceRule = (rule) => rule.constructor.name === "CSSFontFaceRule";
454
+ var isCSSImportRule = (rule) => rule.constructor.name === "CSSImportRule";
455
+ var isLayerBlockRule = (rule) => rule.constructor.name === "CSSLayerBlockRule";
456
+ var isElementNode = (node) => node.nodeType === 1;
457
+ var isSVGElementNode = (node) => typeof node.className === "object";
458
+ var isSVGImageElementNode = (node) => node.tagName === "image";
459
+ var isSVGUseElementNode = (node) => node.tagName === "use";
460
+ var isHTMLElementNode = (node) => isElementNode(node) && typeof node.style !== "undefined" && !isSVGElementNode(node);
461
+ var isCommentNode = (node) => node.nodeType === 8;
462
+ var isTextNode = (node) => node.nodeType === 3;
463
+ var isImageElement = (node) => node.tagName === "IMG";
464
+ var isVideoElement = (node) => node.tagName === "VIDEO";
465
+ var isCanvasElement = (node) => node.tagName === "CANVAS";
466
+ var isTextareaElement = (node) => node.tagName === "TEXTAREA";
467
+ var isInputElement = (node) => node.tagName === "INPUT";
468
+ var isStyleElement = (node) => node.tagName === "STYLE";
469
+ var isScriptElement = (node) => node.tagName === "SCRIPT";
470
+ var isSelectElement = (node) => node.tagName === "SELECT";
471
+ var isSlotElement = (node) => node.tagName === "SLOT";
472
+ var isIFrameElement = (node) => node.tagName === "IFRAME";
473
+ var consoleWarn = (...args) => console.warn(PREFIX, ...args);
474
+ function supportWebp(ownerDocument) {
475
+ const canvas = ownerDocument?.createElement?.("canvas");
476
+ if (canvas) {
477
+ canvas.height = canvas.width = 1;
478
+ }
479
+ return Boolean(canvas) && "toDataURL" in canvas && Boolean(canvas.toDataURL("image/webp").includes("image/webp"));
480
+ }
481
+ var isDataUrl = (url) => url.startsWith("data:");
482
+ function resolveUrl(url, baseUrl) {
483
+ if (url.match(/^[a-z]+:\/\//i))
484
+ return url;
485
+ if (IN_BROWSER && url.match(/^\/\//))
486
+ return window.location.protocol + url;
487
+ if (url.match(/^[a-z]+:/i))
488
+ return url;
489
+ if (!IN_BROWSER)
490
+ return url;
491
+ const doc = getDocument().implementation.createHTMLDocument();
492
+ const base = doc.createElement("base");
493
+ const a = doc.createElement("a");
494
+ doc.head.appendChild(base);
495
+ doc.body.appendChild(a);
496
+ if (baseUrl)
497
+ base.href = baseUrl;
498
+ a.href = url;
499
+ return a.href;
500
+ }
501
+ function getDocument(target) {
502
+ return (target && isElementNode(target) ? target?.ownerDocument : target) ?? window.document;
503
+ }
504
+ var XMLNS = "http://www.w3.org/2000/svg";
505
+ function createSvg(width, height, ownerDocument) {
506
+ const svg = getDocument(ownerDocument).createElementNS(XMLNS, "svg");
507
+ svg.setAttributeNS(null, "width", width.toString());
508
+ svg.setAttributeNS(null, "height", height.toString());
509
+ svg.setAttributeNS(null, "viewBox", `0 0 ${width} ${height}`);
510
+ return svg;
511
+ }
512
+ function svgToDataUrl(svg, removeControlCharacter) {
513
+ let xhtml = new XMLSerializer().serializeToString(svg);
514
+ if (removeControlCharacter) {
515
+ xhtml = xhtml.replace(/[\u0000-\u0008\v\f\u000E-\u001F\uD800-\uDFFF\uFFFE\uFFFF]/gu, "");
516
+ }
517
+ return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(xhtml)}`;
518
+ }
519
+ function readBlob(blob, type) {
520
+ return new Promise((resolve, reject) => {
521
+ const reader = new FileReader();
522
+ reader.onload = () => resolve(reader.result);
523
+ reader.onerror = () => reject(reader.error);
524
+ reader.onabort = () => reject(new Error(`Failed read blob to ${type}`));
525
+ if (type === "dataUrl") {
526
+ reader.readAsDataURL(blob);
527
+ } else if (type === "arrayBuffer") {
528
+ reader.readAsArrayBuffer(blob);
529
+ }
530
+ });
531
+ }
532
+ var blobToDataUrl = (blob) => readBlob(blob, "dataUrl");
533
+ function createImage(url, ownerDocument) {
534
+ const img = getDocument(ownerDocument).createElement("img");
535
+ img.decoding = "sync";
536
+ img.loading = "eager";
537
+ img.src = url;
538
+ return img;
539
+ }
540
+ function loadMedia(media, options) {
541
+ return new Promise((resolve) => {
542
+ const { timeout, ownerDocument, onError: userOnError, onWarn } = options ?? {};
543
+ const node = typeof media === "string" ? createImage(media, getDocument(ownerDocument)) : media;
544
+ let timer = null;
545
+ let removeEventListeners = null;
546
+ function onResolve() {
547
+ resolve(node);
548
+ timer && clearTimeout(timer);
549
+ removeEventListeners?.();
550
+ }
551
+ if (timeout) {
552
+ timer = setTimeout(onResolve, timeout);
553
+ }
554
+ if (isVideoElement(node)) {
555
+ const currentSrc = node.currentSrc || node.src;
556
+ if (!currentSrc) {
557
+ if (node.poster) {
558
+ return loadMedia(node.poster, options).then(resolve);
559
+ }
560
+ return onResolve();
561
+ }
562
+ if (node.readyState >= 2) {
563
+ return onResolve();
564
+ }
565
+ const onLoadeddata = onResolve;
566
+ const onError = (error) => {
567
+ onWarn?.(
568
+ "Failed video load",
569
+ currentSrc,
570
+ error
571
+ );
572
+ userOnError?.(error);
573
+ onResolve();
574
+ };
575
+ removeEventListeners = () => {
576
+ node.removeEventListener("loadeddata", onLoadeddata);
577
+ node.removeEventListener("error", onError);
578
+ };
579
+ node.addEventListener("loadeddata", onLoadeddata, { once: true });
580
+ node.addEventListener("error", onError, { once: true });
581
+ } else {
582
+ const currentSrc = isSVGImageElementNode(node) ? node.href.baseVal : node.currentSrc || node.src;
583
+ if (!currentSrc) {
584
+ return onResolve();
585
+ }
586
+ const onLoad = async () => {
587
+ if (isImageElement(node) && "decode" in node) {
588
+ try {
589
+ await node.decode();
590
+ } catch (error) {
591
+ onWarn?.(
592
+ "Failed to decode image, trying to render anyway",
593
+ node.dataset.originalSrc || currentSrc,
594
+ error
595
+ );
596
+ }
597
+ }
598
+ onResolve();
599
+ };
600
+ const onError = (error) => {
601
+ onWarn?.(
602
+ "Failed image load",
603
+ node.dataset.originalSrc || currentSrc,
604
+ error
605
+ );
606
+ onResolve();
607
+ };
608
+ if (isImageElement(node) && node.complete) {
609
+ return onLoad();
610
+ }
611
+ removeEventListeners = () => {
612
+ node.removeEventListener("load", onLoad);
613
+ node.removeEventListener("error", onError);
614
+ };
615
+ node.addEventListener("load", onLoad, { once: true });
616
+ node.addEventListener("error", onError, { once: true });
617
+ }
618
+ });
619
+ }
620
+ async function waitUntilLoad(node, options) {
621
+ if (isHTMLElementNode(node)) {
622
+ if (isImageElement(node) || isVideoElement(node)) {
623
+ await loadMedia(node, options);
624
+ } else {
625
+ await Promise.all(
626
+ ["img", "video"].flatMap((selectors) => {
627
+ return Array.from(node.querySelectorAll(selectors)).map((el2) => loadMedia(el2, options));
628
+ })
629
+ );
630
+ }
631
+ }
632
+ }
633
+ var uuid = /* @__PURE__ */ (function uuid2() {
634
+ let counter = 0;
635
+ const random = () => `0000${(Math.random() * 36 ** 4 << 0).toString(36)}`.slice(-4);
636
+ return () => {
637
+ counter += 1;
638
+ return `u${random()}${counter}`;
639
+ };
640
+ })();
641
+ function splitFontFamily(fontFamily) {
642
+ return fontFamily?.split(",").map((val) => val.trim().replace(/"|'/g, "").toLowerCase()).filter(Boolean);
643
+ }
644
+ var uid = 0;
645
+ function createLogger(debug) {
646
+ const prefix = `${PREFIX}[#${uid}]`;
647
+ uid++;
648
+ return {
649
+ // eslint-disable-next-line no-console
650
+ time: (label) => debug && console.time(`${prefix} ${label}`),
651
+ // eslint-disable-next-line no-console
652
+ timeEnd: (label) => debug && console.timeEnd(`${prefix} ${label}`),
653
+ warn: (...args) => debug && consoleWarn(...args)
654
+ };
655
+ }
656
+ function getDefaultRequestInit(bypassingCache) {
657
+ return {
658
+ cache: bypassingCache ? "no-cache" : "force-cache"
659
+ };
660
+ }
661
+ async function orCreateContext(node, options) {
662
+ return isContext(node) ? node : createContext(node, { ...options, autoDestruct: true });
663
+ }
664
+ async function createContext(node, options) {
665
+ const { scale = 1, workerUrl, workerNumber = 1 } = options || {};
666
+ const debug = Boolean(options?.debug);
667
+ const features = options?.features ?? true;
668
+ const ownerDocument = node.ownerDocument ?? (IN_BROWSER ? window.document : void 0);
669
+ const ownerWindow = node.ownerDocument?.defaultView ?? (IN_BROWSER ? window : void 0);
670
+ const requests = /* @__PURE__ */ new Map();
671
+ const context = {
672
+ // Options
673
+ width: 0,
674
+ height: 0,
675
+ quality: 1,
676
+ type: "image/png",
677
+ scale,
678
+ backgroundColor: null,
679
+ style: null,
680
+ filter: null,
681
+ maximumCanvasSize: 0,
682
+ timeout: 3e4,
683
+ progress: null,
684
+ debug,
685
+ fetch: {
686
+ requestInit: getDefaultRequestInit(options?.fetch?.bypassingCache),
687
+ placeholderImage: "data:image/png;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",
688
+ bypassingCache: false,
689
+ ...options?.fetch
690
+ },
691
+ fetchFn: null,
692
+ font: {},
693
+ drawImageInterval: 100,
694
+ workerUrl: null,
695
+ workerNumber,
696
+ onCloneEachNode: null,
697
+ onCloneNode: null,
698
+ onEmbedNode: null,
699
+ onCreateForeignObjectSvg: null,
700
+ includeStyleProperties: null,
701
+ autoDestruct: false,
702
+ ...options,
703
+ // InternalContext
704
+ __CONTEXT__: true,
705
+ log: createLogger(debug),
706
+ node,
707
+ ownerDocument,
708
+ ownerWindow,
709
+ dpi: scale === 1 ? null : 96 * scale,
710
+ svgStyleElement: createStyleElement(ownerDocument),
711
+ svgDefsElement: ownerDocument?.createElementNS(XMLNS, "defs"),
712
+ svgStyles: /* @__PURE__ */ new Map(),
713
+ defaultComputedStyles: /* @__PURE__ */ new Map(),
714
+ workers: [
715
+ ...Array.from({
716
+ length: SUPPORT_WEB_WORKER && workerUrl && workerNumber ? workerNumber : 0
717
+ })
718
+ ].map(() => {
719
+ try {
720
+ const worker = new Worker(workerUrl);
721
+ worker.onmessage = async (event) => {
722
+ const { url, result } = event.data;
723
+ if (result) {
724
+ requests.get(url)?.resolve?.(result);
725
+ } else {
726
+ requests.get(url)?.reject?.(new Error(`Error receiving message from worker: ${url}`));
727
+ }
728
+ };
729
+ worker.onmessageerror = (event) => {
730
+ const { url } = event.data;
731
+ requests.get(url)?.reject?.(new Error(`Error receiving message from worker: ${url}`));
732
+ };
733
+ return worker;
734
+ } catch (error) {
735
+ context.log.warn("Failed to new Worker", error);
736
+ return null;
737
+ }
738
+ }).filter(Boolean),
739
+ fontFamilies: /* @__PURE__ */ new Map(),
740
+ fontCssTexts: /* @__PURE__ */ new Map(),
741
+ acceptOfImage: `${[
742
+ supportWebp(ownerDocument) && "image/webp",
743
+ "image/svg+xml",
744
+ "image/*",
745
+ "*/*"
746
+ ].filter(Boolean).join(",")};q=0.8`,
747
+ requests,
748
+ drawImageCount: 0,
749
+ tasks: [],
750
+ features,
751
+ isEnable: (key) => {
752
+ if (key === "restoreScrollPosition") {
753
+ return typeof features === "boolean" ? false : features[key] ?? false;
754
+ }
755
+ if (typeof features === "boolean") {
756
+ return features;
757
+ }
758
+ return features[key] ?? true;
759
+ },
760
+ shadowRoots: []
761
+ };
762
+ context.log.time("wait until load");
763
+ await waitUntilLoad(node, { timeout: context.timeout, onWarn: context.log.warn });
764
+ context.log.timeEnd("wait until load");
765
+ const { width, height } = resolveBoundingBox(node, context);
766
+ context.width = width;
767
+ context.height = height;
768
+ return context;
769
+ }
770
+ function createStyleElement(ownerDocument) {
771
+ if (!ownerDocument)
772
+ return void 0;
773
+ const style = ownerDocument.createElement("style");
774
+ const cssText = style.ownerDocument.createTextNode(`
775
+ .______background-clip--text {
776
+ background-clip: text;
777
+ -webkit-background-clip: text;
778
+ }
779
+ `);
780
+ style.appendChild(cssText);
781
+ return style;
782
+ }
783
+ function resolveBoundingBox(node, context) {
784
+ let { width, height } = context;
785
+ if (isElementNode(node) && (!width || !height)) {
786
+ const box = node.getBoundingClientRect();
787
+ width = width || box.width || Number(node.getAttribute("width")) || 0;
788
+ height = height || box.height || Number(node.getAttribute("height")) || 0;
789
+ }
790
+ return { width, height };
791
+ }
792
+ async function imageToCanvas(image, context) {
793
+ const {
794
+ log,
795
+ timeout,
796
+ drawImageCount,
797
+ drawImageInterval
798
+ } = context;
799
+ log.time("image to canvas");
800
+ const loaded = await loadMedia(image, { timeout, onWarn: context.log.warn });
801
+ const { canvas, context2d } = createCanvas(image.ownerDocument, context);
802
+ const drawImage = () => {
803
+ try {
804
+ context2d?.drawImage(loaded, 0, 0, canvas.width, canvas.height);
805
+ } catch (error) {
806
+ context.log.warn("Failed to drawImage", error);
807
+ }
808
+ };
809
+ drawImage();
810
+ if (context.isEnable("fixSvgXmlDecode")) {
811
+ for (let i = 0; i < drawImageCount; i++) {
812
+ await new Promise((resolve) => {
813
+ setTimeout(() => {
814
+ context2d?.clearRect(0, 0, canvas.width, canvas.height);
815
+ drawImage();
816
+ resolve();
817
+ }, i + drawImageInterval);
818
+ });
819
+ }
820
+ }
821
+ context.drawImageCount = 0;
822
+ log.timeEnd("image to canvas");
823
+ return canvas;
824
+ }
825
+ function createCanvas(ownerDocument, context) {
826
+ const { width, height, scale, backgroundColor, maximumCanvasSize: max } = context;
827
+ const canvas = ownerDocument.createElement("canvas");
828
+ canvas.width = Math.floor(width * scale);
829
+ canvas.height = Math.floor(height * scale);
830
+ canvas.style.width = `${width}px`;
831
+ canvas.style.height = `${height}px`;
832
+ if (max) {
833
+ if (canvas.width > max || canvas.height > max) {
834
+ if (canvas.width > max && canvas.height > max) {
835
+ if (canvas.width > canvas.height) {
836
+ canvas.height *= max / canvas.width;
837
+ canvas.width = max;
838
+ } else {
839
+ canvas.width *= max / canvas.height;
840
+ canvas.height = max;
841
+ }
842
+ } else if (canvas.width > max) {
843
+ canvas.height *= max / canvas.width;
844
+ canvas.width = max;
845
+ } else {
846
+ canvas.width *= max / canvas.height;
847
+ canvas.height = max;
848
+ }
849
+ }
850
+ }
851
+ const context2d = canvas.getContext("2d");
852
+ if (context2d && backgroundColor) {
853
+ context2d.fillStyle = backgroundColor;
854
+ context2d.fillRect(0, 0, canvas.width, canvas.height);
855
+ }
856
+ return { canvas, context2d };
857
+ }
858
+ function cloneCanvas(canvas, context) {
859
+ if (canvas.ownerDocument) {
860
+ try {
861
+ const dataURL = canvas.toDataURL();
862
+ if (dataURL !== "data:,") {
863
+ return createImage(dataURL, canvas.ownerDocument);
864
+ }
865
+ } catch (error) {
866
+ context.log.warn("Failed to clone canvas", error);
867
+ }
868
+ }
869
+ const cloned = canvas.cloneNode(false);
870
+ const ctx = canvas.getContext("2d");
871
+ const clonedCtx = cloned.getContext("2d");
872
+ try {
873
+ if (ctx && clonedCtx) {
874
+ clonedCtx.putImageData(
875
+ ctx.getImageData(0, 0, canvas.width, canvas.height),
876
+ 0,
877
+ 0
878
+ );
879
+ }
880
+ return cloned;
881
+ } catch (error) {
882
+ context.log.warn("Failed to clone canvas", error);
883
+ }
884
+ return cloned;
885
+ }
886
+ function cloneIframe(iframe, context) {
887
+ try {
888
+ if (iframe?.contentDocument?.documentElement) {
889
+ return cloneNode(iframe.contentDocument.documentElement, context);
890
+ }
891
+ } catch (error) {
892
+ context.log.warn("Failed to clone iframe", error);
893
+ }
894
+ return iframe.cloneNode(false);
895
+ }
896
+ function cloneImage(image) {
897
+ const cloned = image.cloneNode(false);
898
+ if (image.currentSrc && image.currentSrc !== image.src) {
899
+ cloned.src = image.currentSrc;
900
+ cloned.srcset = "";
901
+ }
902
+ if (cloned.loading === "lazy") {
903
+ cloned.loading = "eager";
904
+ }
905
+ return cloned;
906
+ }
907
+ async function cloneVideo(video, context) {
908
+ if (video.ownerDocument && !video.currentSrc && video.poster) {
909
+ return createImage(video.poster, video.ownerDocument);
910
+ }
911
+ const cloned = video.cloneNode(false);
912
+ cloned.crossOrigin = "anonymous";
913
+ if (video.currentSrc && video.currentSrc !== video.src) {
914
+ cloned.src = video.currentSrc;
915
+ }
916
+ const ownerDocument = cloned.ownerDocument;
917
+ if (ownerDocument) {
918
+ let canPlay = true;
919
+ await loadMedia(cloned, { onError: () => canPlay = false, onWarn: context.log.warn });
920
+ if (!canPlay) {
921
+ if (video.poster) {
922
+ return createImage(video.poster, video.ownerDocument);
923
+ }
924
+ return cloned;
925
+ }
926
+ cloned.currentTime = video.currentTime;
927
+ await new Promise((resolve) => {
928
+ cloned.addEventListener("seeked", resolve, { once: true });
929
+ });
930
+ const canvas = ownerDocument.createElement("canvas");
931
+ canvas.width = video.offsetWidth;
932
+ canvas.height = video.offsetHeight;
933
+ try {
934
+ const ctx = canvas.getContext("2d");
935
+ if (ctx)
936
+ ctx.drawImage(cloned, 0, 0, canvas.width, canvas.height);
937
+ } catch (error) {
938
+ context.log.warn("Failed to clone video", error);
939
+ if (video.poster) {
940
+ return createImage(video.poster, video.ownerDocument);
941
+ }
942
+ return cloned;
943
+ }
944
+ return cloneCanvas(canvas, context);
945
+ }
946
+ return cloned;
947
+ }
948
+ function cloneElement(node, context) {
949
+ if (isCanvasElement(node)) {
950
+ return cloneCanvas(node, context);
951
+ }
952
+ if (isIFrameElement(node)) {
953
+ return cloneIframe(node, context);
954
+ }
955
+ if (isImageElement(node)) {
956
+ return cloneImage(node);
957
+ }
958
+ if (isVideoElement(node)) {
959
+ return cloneVideo(node, context);
960
+ }
961
+ return node.cloneNode(false);
962
+ }
963
+ function getSandBox(context) {
964
+ let sandbox = context.sandbox;
965
+ if (!sandbox) {
966
+ const { ownerDocument } = context;
967
+ try {
968
+ if (ownerDocument) {
969
+ sandbox = ownerDocument.createElement("iframe");
970
+ sandbox.id = `__SANDBOX__${uuid()}`;
971
+ sandbox.width = "0";
972
+ sandbox.height = "0";
973
+ sandbox.style.visibility = "hidden";
974
+ sandbox.style.position = "fixed";
975
+ ownerDocument.body.appendChild(sandbox);
976
+ sandbox.srcdoc = '<!DOCTYPE html><meta charset="UTF-8"><title></title><body>';
977
+ context.sandbox = sandbox;
978
+ }
979
+ } catch (error) {
980
+ context.log.warn("Failed to getSandBox", error);
981
+ }
982
+ }
983
+ return sandbox;
984
+ }
985
+ var ignoredStyles = [
986
+ "width",
987
+ "height",
988
+ "-webkit-text-fill-color"
989
+ ];
990
+ var includedAttributes = [
991
+ "stroke",
992
+ "fill"
993
+ ];
994
+ function getDefaultStyle(node, pseudoElement, context) {
995
+ const { defaultComputedStyles } = context;
996
+ const nodeName = node.nodeName.toLowerCase();
997
+ const isSvgNode = isSVGElementNode(node) && nodeName !== "svg";
998
+ const attributes = isSvgNode ? includedAttributes.map((name) => [name, node.getAttribute(name)]).filter(([, value]) => value !== null) : [];
999
+ const key = [
1000
+ isSvgNode && "svg",
1001
+ nodeName,
1002
+ attributes.map((name, value) => `${name}=${value}`).join(","),
1003
+ pseudoElement
1004
+ ].filter(Boolean).join(":");
1005
+ if (defaultComputedStyles.has(key))
1006
+ return defaultComputedStyles.get(key);
1007
+ const sandbox = getSandBox(context);
1008
+ const sandboxWindow = sandbox?.contentWindow;
1009
+ if (!sandboxWindow)
1010
+ return /* @__PURE__ */ new Map();
1011
+ const sandboxDocument = sandboxWindow?.document;
1012
+ let root;
1013
+ let el2;
1014
+ if (isSvgNode) {
1015
+ root = sandboxDocument.createElementNS(XMLNS, "svg");
1016
+ el2 = root.ownerDocument.createElementNS(root.namespaceURI, nodeName);
1017
+ attributes.forEach(([name, value]) => {
1018
+ el2.setAttributeNS(null, name, value);
1019
+ });
1020
+ root.appendChild(el2);
1021
+ } else {
1022
+ root = el2 = sandboxDocument.createElement(nodeName);
1023
+ }
1024
+ el2.textContent = " ";
1025
+ sandboxDocument.body.appendChild(root);
1026
+ const computedStyle = sandboxWindow.getComputedStyle(el2, pseudoElement);
1027
+ const styles = /* @__PURE__ */ new Map();
1028
+ for (let len = computedStyle.length, i = 0; i < len; i++) {
1029
+ const name = computedStyle.item(i);
1030
+ if (ignoredStyles.includes(name))
1031
+ continue;
1032
+ styles.set(name, computedStyle.getPropertyValue(name));
1033
+ }
1034
+ sandboxDocument.body.removeChild(root);
1035
+ defaultComputedStyles.set(key, styles);
1036
+ return styles;
1037
+ }
1038
+ function getDiffStyle(style, defaultStyle, includeStyleProperties) {
1039
+ const diffStyle = /* @__PURE__ */ new Map();
1040
+ const prefixs = [];
1041
+ const prefixTree = /* @__PURE__ */ new Map();
1042
+ if (includeStyleProperties) {
1043
+ for (const name of includeStyleProperties) {
1044
+ applyTo(name);
1045
+ }
1046
+ } else {
1047
+ for (let len = style.length, i = 0; i < len; i++) {
1048
+ const name = style.item(i);
1049
+ applyTo(name);
1050
+ }
1051
+ }
1052
+ for (let len = prefixs.length, i = 0; i < len; i++) {
1053
+ prefixTree.get(prefixs[i])?.forEach((value, name) => diffStyle.set(name, value));
1054
+ }
1055
+ function applyTo(name) {
1056
+ const value = style.getPropertyValue(name);
1057
+ const priority = style.getPropertyPriority(name);
1058
+ const subIndex = name.lastIndexOf("-");
1059
+ const prefix = subIndex > -1 ? name.substring(0, subIndex) : void 0;
1060
+ if (prefix) {
1061
+ let map = prefixTree.get(prefix);
1062
+ if (!map) {
1063
+ map = /* @__PURE__ */ new Map();
1064
+ prefixTree.set(prefix, map);
1065
+ }
1066
+ map.set(name, [value, priority]);
1067
+ }
1068
+ if (defaultStyle.get(name) === value && !priority)
1069
+ return;
1070
+ if (prefix) {
1071
+ prefixs.push(prefix);
1072
+ } else {
1073
+ diffStyle.set(name, [value, priority]);
1074
+ }
1075
+ }
1076
+ return diffStyle;
1077
+ }
1078
+ function copyCssStyles(node, cloned, isRoot, context) {
1079
+ const { ownerWindow, includeStyleProperties, currentParentNodeStyle } = context;
1080
+ const clonedStyle = cloned.style;
1081
+ const computedStyle = ownerWindow.getComputedStyle(node);
1082
+ const defaultStyle = getDefaultStyle(node, null, context);
1083
+ currentParentNodeStyle?.forEach((_, key) => {
1084
+ defaultStyle.delete(key);
1085
+ });
1086
+ const style = getDiffStyle(computedStyle, defaultStyle, includeStyleProperties);
1087
+ style.delete("transition-property");
1088
+ style.delete("all");
1089
+ style.delete("d");
1090
+ style.delete("content");
1091
+ if (isRoot) {
1092
+ style.delete("position");
1093
+ style.delete("margin-top");
1094
+ style.delete("margin-right");
1095
+ style.delete("margin-bottom");
1096
+ style.delete("margin-left");
1097
+ style.delete("margin-block-start");
1098
+ style.delete("margin-block-end");
1099
+ style.delete("margin-inline-start");
1100
+ style.delete("margin-inline-end");
1101
+ style.set("box-sizing", ["border-box", ""]);
1102
+ }
1103
+ if (style.get("background-clip")?.[0] === "text") {
1104
+ cloned.classList.add("______background-clip--text");
1105
+ }
1106
+ if (IN_CHROME) {
1107
+ if (!style.has("font-kerning"))
1108
+ style.set("font-kerning", ["normal", ""]);
1109
+ if ((style.get("overflow-x")?.[0] === "hidden" || style.get("overflow-y")?.[0] === "hidden") && style.get("text-overflow")?.[0] === "ellipsis" && node.scrollWidth === node.clientWidth) {
1110
+ style.set("text-overflow", ["clip", ""]);
1111
+ }
1112
+ }
1113
+ for (let len = clonedStyle.length, i = 0; i < len; i++) {
1114
+ clonedStyle.removeProperty(clonedStyle.item(i));
1115
+ }
1116
+ style.forEach(([value, priority], name) => {
1117
+ clonedStyle.setProperty(name, value, priority);
1118
+ });
1119
+ return style;
1120
+ }
1121
+ function copyInputValue(node, cloned) {
1122
+ if (isTextareaElement(node) || isInputElement(node) || isSelectElement(node)) {
1123
+ cloned.setAttribute("value", node.value);
1124
+ }
1125
+ }
1126
+ var pseudoClasses = [
1127
+ "::before",
1128
+ "::after"
1129
+ // '::placeholder', TODO
1130
+ ];
1131
+ var scrollbarPseudoClasses = [
1132
+ "::-webkit-scrollbar",
1133
+ "::-webkit-scrollbar-button",
1134
+ // '::-webkit-scrollbar:horizontal', TODO
1135
+ "::-webkit-scrollbar-thumb",
1136
+ "::-webkit-scrollbar-track",
1137
+ "::-webkit-scrollbar-track-piece",
1138
+ // '::-webkit-scrollbar:vertical', TODO
1139
+ "::-webkit-scrollbar-corner",
1140
+ "::-webkit-resizer"
1141
+ ];
1142
+ function copyPseudoClass(node, cloned, copyScrollbar, context, addWordToFontFamilies) {
1143
+ const { ownerWindow, svgStyleElement, svgStyles, currentNodeStyle } = context;
1144
+ if (!svgStyleElement || !ownerWindow)
1145
+ return;
1146
+ function copyBy(pseudoClass) {
1147
+ const computedStyle = ownerWindow.getComputedStyle(node, pseudoClass);
1148
+ let content = computedStyle.getPropertyValue("content");
1149
+ if (!content || content === "none")
1150
+ return;
1151
+ addWordToFontFamilies?.(content);
1152
+ content = content.replace(/(')|(")|(counter\(.+\))/g, "");
1153
+ const klasses = [uuid()];
1154
+ const defaultStyle = getDefaultStyle(node, pseudoClass, context);
1155
+ currentNodeStyle?.forEach((_, key) => {
1156
+ defaultStyle.delete(key);
1157
+ });
1158
+ const style = getDiffStyle(computedStyle, defaultStyle, context.includeStyleProperties);
1159
+ style.delete("content");
1160
+ style.delete("-webkit-locale");
1161
+ if (style.get("background-clip")?.[0] === "text") {
1162
+ cloned.classList.add("______background-clip--text");
1163
+ }
1164
+ const cloneStyle = [
1165
+ `content: '${content}';`
1166
+ ];
1167
+ style.forEach(([value, priority], name) => {
1168
+ cloneStyle.push(`${name}: ${value}${priority ? " !important" : ""};`);
1169
+ });
1170
+ if (cloneStyle.length === 1)
1171
+ return;
1172
+ try {
1173
+ cloned.className = [cloned.className, ...klasses].join(" ");
1174
+ } catch (err) {
1175
+ context.log.warn("Failed to copyPseudoClass", err);
1176
+ return;
1177
+ }
1178
+ const cssText = cloneStyle.join("\n ");
1179
+ let allClasses = svgStyles.get(cssText);
1180
+ if (!allClasses) {
1181
+ allClasses = [];
1182
+ svgStyles.set(cssText, allClasses);
1183
+ }
1184
+ allClasses.push(`.${klasses[0]}${pseudoClass}`);
1185
+ }
1186
+ pseudoClasses.forEach(copyBy);
1187
+ if (copyScrollbar)
1188
+ scrollbarPseudoClasses.forEach(copyBy);
1189
+ }
1190
+ var excludeParentNodes = /* @__PURE__ */ new Set([
1191
+ "symbol"
1192
+ // test/fixtures/svg.symbol.html
1193
+ ]);
1194
+ async function appendChildNode(node, cloned, child, context, addWordToFontFamilies) {
1195
+ if (isElementNode(child) && (isStyleElement(child) || isScriptElement(child)))
1196
+ return;
1197
+ if (context.filter && !context.filter(child))
1198
+ return;
1199
+ if (excludeParentNodes.has(cloned.nodeName) || excludeParentNodes.has(child.nodeName)) {
1200
+ context.currentParentNodeStyle = void 0;
1201
+ } else {
1202
+ context.currentParentNodeStyle = context.currentNodeStyle;
1203
+ }
1204
+ const childCloned = await cloneNode(child, context, false, addWordToFontFamilies);
1205
+ if (context.isEnable("restoreScrollPosition")) {
1206
+ restoreScrollPosition(node, childCloned);
1207
+ }
1208
+ cloned.appendChild(childCloned);
1209
+ }
1210
+ async function cloneChildNodes(node, cloned, context, addWordToFontFamilies) {
1211
+ let firstChild = node.firstChild;
1212
+ if (isElementNode(node)) {
1213
+ if (node.shadowRoot) {
1214
+ firstChild = node.shadowRoot?.firstChild;
1215
+ context.shadowRoots.push(node.shadowRoot);
1216
+ }
1217
+ }
1218
+ for (let child = firstChild; child; child = child.nextSibling) {
1219
+ if (isCommentNode(child))
1220
+ continue;
1221
+ if (isElementNode(child) && isSlotElement(child) && typeof child.assignedNodes === "function") {
1222
+ const nodes = child.assignedNodes();
1223
+ for (let i = 0; i < nodes.length; i++) {
1224
+ await appendChildNode(node, cloned, nodes[i], context, addWordToFontFamilies);
1225
+ }
1226
+ } else {
1227
+ await appendChildNode(node, cloned, child, context, addWordToFontFamilies);
1228
+ }
1229
+ }
1230
+ }
1231
+ function restoreScrollPosition(node, chlidCloned) {
1232
+ if (!isHTMLElementNode(node) || !isHTMLElementNode(chlidCloned))
1233
+ return;
1234
+ const { scrollTop, scrollLeft } = node;
1235
+ if (!scrollTop && !scrollLeft) {
1236
+ return;
1237
+ }
1238
+ const { transform } = chlidCloned.style;
1239
+ const matrix = new DOMMatrix(transform);
1240
+ const { a, b, c, d } = matrix;
1241
+ matrix.a = 1;
1242
+ matrix.b = 0;
1243
+ matrix.c = 0;
1244
+ matrix.d = 1;
1245
+ matrix.translateSelf(-scrollLeft, -scrollTop);
1246
+ matrix.a = a;
1247
+ matrix.b = b;
1248
+ matrix.c = c;
1249
+ matrix.d = d;
1250
+ chlidCloned.style.transform = matrix.toString();
1251
+ }
1252
+ function applyCssStyleWithOptions(cloned, context) {
1253
+ const { backgroundColor, width, height, style: styles } = context;
1254
+ const clonedStyle = cloned.style;
1255
+ if (backgroundColor)
1256
+ clonedStyle.setProperty("background-color", backgroundColor, "important");
1257
+ if (width)
1258
+ clonedStyle.setProperty("width", `${width}px`, "important");
1259
+ if (height)
1260
+ clonedStyle.setProperty("height", `${height}px`, "important");
1261
+ if (styles) {
1262
+ for (const name in styles) clonedStyle[name] = styles[name];
1263
+ }
1264
+ }
1265
+ var NORMAL_ATTRIBUTE_RE = /^[\w-:]+$/;
1266
+ async function cloneNode(node, context, isRoot = false, addWordToFontFamilies) {
1267
+ const { ownerDocument, ownerWindow, fontFamilies, onCloneEachNode } = context;
1268
+ if (ownerDocument && isTextNode(node)) {
1269
+ if (addWordToFontFamilies && /\S/.test(node.data)) {
1270
+ addWordToFontFamilies(node.data);
1271
+ }
1272
+ return ownerDocument.createTextNode(node.data);
1273
+ }
1274
+ if (ownerDocument && ownerWindow && isElementNode(node) && (isHTMLElementNode(node) || isSVGElementNode(node))) {
1275
+ const cloned2 = await cloneElement(node, context);
1276
+ if (context.isEnable("removeAbnormalAttributes")) {
1277
+ const names = cloned2.getAttributeNames();
1278
+ for (let len = names.length, i = 0; i < len; i++) {
1279
+ const name = names[i];
1280
+ if (!NORMAL_ATTRIBUTE_RE.test(name)) {
1281
+ cloned2.removeAttribute(name);
1282
+ }
1283
+ }
1284
+ }
1285
+ const style = context.currentNodeStyle = copyCssStyles(node, cloned2, isRoot, context);
1286
+ if (isRoot)
1287
+ applyCssStyleWithOptions(cloned2, context);
1288
+ let copyScrollbar = false;
1289
+ if (context.isEnable("copyScrollbar")) {
1290
+ const overflow = [
1291
+ style.get("overflow-x")?.[0],
1292
+ style.get("overflow-y")?.[0]
1293
+ ];
1294
+ copyScrollbar = overflow.includes("scroll") || (overflow.includes("auto") || overflow.includes("overlay")) && (node.scrollHeight > node.clientHeight || node.scrollWidth > node.clientWidth);
1295
+ }
1296
+ const textTransform = style.get("text-transform")?.[0];
1297
+ const families = splitFontFamily(style.get("font-family")?.[0]);
1298
+ const addWordToFontFamilies2 = families ? (word) => {
1299
+ if (textTransform === "uppercase") {
1300
+ word = word.toUpperCase();
1301
+ } else if (textTransform === "lowercase") {
1302
+ word = word.toLowerCase();
1303
+ } else if (textTransform === "capitalize") {
1304
+ word = word[0].toUpperCase() + word.substring(1);
1305
+ }
1306
+ families.forEach((family) => {
1307
+ let fontFamily = fontFamilies.get(family);
1308
+ if (!fontFamily) {
1309
+ fontFamilies.set(family, fontFamily = /* @__PURE__ */ new Set());
1310
+ }
1311
+ word.split("").forEach((text) => fontFamily.add(text));
1312
+ });
1313
+ } : void 0;
1314
+ copyPseudoClass(
1315
+ node,
1316
+ cloned2,
1317
+ copyScrollbar,
1318
+ context,
1319
+ addWordToFontFamilies2
1320
+ );
1321
+ copyInputValue(node, cloned2);
1322
+ if (!isVideoElement(node)) {
1323
+ await cloneChildNodes(
1324
+ node,
1325
+ cloned2,
1326
+ context,
1327
+ addWordToFontFamilies2
1328
+ );
1329
+ }
1330
+ await onCloneEachNode?.(cloned2);
1331
+ return cloned2;
1332
+ }
1333
+ const cloned = node.cloneNode(false);
1334
+ await cloneChildNodes(node, cloned, context);
1335
+ await onCloneEachNode?.(cloned);
1336
+ return cloned;
1337
+ }
1338
+ function destroyContext(context) {
1339
+ context.ownerDocument = void 0;
1340
+ context.ownerWindow = void 0;
1341
+ context.svgStyleElement = void 0;
1342
+ context.svgDefsElement = void 0;
1343
+ context.svgStyles.clear();
1344
+ context.defaultComputedStyles.clear();
1345
+ if (context.sandbox) {
1346
+ try {
1347
+ context.sandbox.remove();
1348
+ } catch (err) {
1349
+ context.log.warn("Failed to destroyContext", err);
1350
+ }
1351
+ context.sandbox = void 0;
1352
+ }
1353
+ context.workers = [];
1354
+ context.fontFamilies.clear();
1355
+ context.fontCssTexts.clear();
1356
+ context.requests.clear();
1357
+ context.tasks = [];
1358
+ context.shadowRoots = [];
1359
+ }
1360
+ function baseFetch(options) {
1361
+ const { url, timeout, responseType, ...requestInit } = options;
1362
+ const controller = new AbortController();
1363
+ const timer = timeout ? setTimeout(() => controller.abort(), timeout) : void 0;
1364
+ return fetch(url, { signal: controller.signal, ...requestInit }).then((response) => {
1365
+ if (!response.ok) {
1366
+ throw new Error("Failed fetch, not 2xx response", { cause: response });
1367
+ }
1368
+ switch (responseType) {
1369
+ case "arrayBuffer":
1370
+ return response.arrayBuffer();
1371
+ case "dataUrl":
1372
+ return response.blob().then(blobToDataUrl);
1373
+ case "text":
1374
+ default:
1375
+ return response.text();
1376
+ }
1377
+ }).finally(() => clearTimeout(timer));
1378
+ }
1379
+ function contextFetch(context, options) {
1380
+ const { url: rawUrl, requestType = "text", responseType = "text", imageDom } = options;
1381
+ let url = rawUrl;
1382
+ const {
1383
+ timeout,
1384
+ acceptOfImage,
1385
+ requests,
1386
+ fetchFn,
1387
+ fetch: {
1388
+ requestInit,
1389
+ bypassingCache,
1390
+ placeholderImage
1391
+ },
1392
+ font,
1393
+ workers,
1394
+ fontFamilies
1395
+ } = context;
1396
+ if (requestType === "image" && (IN_SAFARI || IN_FIREFOX)) {
1397
+ context.drawImageCount++;
1398
+ }
1399
+ let request = requests.get(rawUrl);
1400
+ if (!request) {
1401
+ if (bypassingCache) {
1402
+ if (bypassingCache instanceof RegExp && bypassingCache.test(url)) {
1403
+ url += (/\?/.test(url) ? "&" : "?") + (/* @__PURE__ */ new Date()).getTime();
1404
+ }
1405
+ }
1406
+ const canFontMinify = requestType.startsWith("font") && font && font.minify;
1407
+ const fontTexts = /* @__PURE__ */ new Set();
1408
+ if (canFontMinify) {
1409
+ const families = requestType.split(";")[1].split(",");
1410
+ families.forEach((family) => {
1411
+ if (!fontFamilies.has(family))
1412
+ return;
1413
+ fontFamilies.get(family).forEach((text) => fontTexts.add(text));
1414
+ });
1415
+ }
1416
+ const needFontMinify = canFontMinify && fontTexts.size;
1417
+ const baseFetchOptions = {
1418
+ url,
1419
+ timeout,
1420
+ responseType: needFontMinify ? "arrayBuffer" : responseType,
1421
+ headers: requestType === "image" ? { accept: acceptOfImage } : void 0,
1422
+ ...requestInit
1423
+ };
1424
+ request = {
1425
+ type: requestType,
1426
+ resolve: void 0,
1427
+ reject: void 0,
1428
+ response: null
1429
+ };
1430
+ request.response = (async () => {
1431
+ if (fetchFn && requestType === "image") {
1432
+ const result = await fetchFn(rawUrl);
1433
+ if (result)
1434
+ return result;
1435
+ }
1436
+ if (!IN_SAFARI && rawUrl.startsWith("http") && workers.length) {
1437
+ return new Promise((resolve, reject) => {
1438
+ const worker = workers[requests.size & workers.length - 1];
1439
+ worker.postMessage({ rawUrl, ...baseFetchOptions });
1440
+ request.resolve = resolve;
1441
+ request.reject = reject;
1442
+ });
1443
+ }
1444
+ return baseFetch(baseFetchOptions);
1445
+ })().catch((error) => {
1446
+ requests.delete(rawUrl);
1447
+ if (requestType === "image" && placeholderImage) {
1448
+ context.log.warn("Failed to fetch image base64, trying to use placeholder image", url);
1449
+ return typeof placeholderImage === "string" ? placeholderImage : placeholderImage(imageDom);
1450
+ }
1451
+ throw error;
1452
+ });
1453
+ requests.set(rawUrl, request);
1454
+ }
1455
+ return request.response;
1456
+ }
1457
+ async function replaceCssUrlToDataUrl(cssText, baseUrl, context, isImage) {
1458
+ if (!hasCssUrl(cssText))
1459
+ return cssText;
1460
+ for (const [rawUrl, url] of parseCssUrls(cssText, baseUrl)) {
1461
+ try {
1462
+ const dataUrl = await contextFetch(
1463
+ context,
1464
+ {
1465
+ url,
1466
+ requestType: isImage ? "image" : "text",
1467
+ responseType: "dataUrl"
1468
+ }
1469
+ );
1470
+ cssText = cssText.replace(toRE(rawUrl), `$1${dataUrl}$3`);
1471
+ } catch (error) {
1472
+ context.log.warn("Failed to fetch css data url", rawUrl, error);
1473
+ }
1474
+ }
1475
+ return cssText;
1476
+ }
1477
+ function hasCssUrl(cssText) {
1478
+ return /url\((['"]?)([^'"]+?)\1\)/.test(cssText);
1479
+ }
1480
+ var URL_RE = /url\((['"]?)([^'"]+?)\1\)/g;
1481
+ function parseCssUrls(cssText, baseUrl) {
1482
+ const result = [];
1483
+ cssText.replace(URL_RE, (raw, quotation, url) => {
1484
+ result.push([url, resolveUrl(url, baseUrl)]);
1485
+ return raw;
1486
+ });
1487
+ return result.filter(([url]) => !isDataUrl(url));
1488
+ }
1489
+ function toRE(url) {
1490
+ const escaped = url.replace(/([.*+?^${}()|\[\]\/\\])/g, "\\$1");
1491
+ return new RegExp(`(url\\(['"]?)(${escaped})(['"]?\\))`, "g");
1492
+ }
1493
+ var properties = [
1494
+ "background-image",
1495
+ "border-image-source",
1496
+ "-webkit-border-image",
1497
+ "-webkit-mask-image",
1498
+ "list-style-image"
1499
+ ];
1500
+ function embedCssStyleImage(style, context) {
1501
+ return properties.map((property) => {
1502
+ const value = style.getPropertyValue(property);
1503
+ if (!value || value === "none") {
1504
+ return null;
1505
+ }
1506
+ if (IN_SAFARI || IN_FIREFOX) {
1507
+ context.drawImageCount++;
1508
+ }
1509
+ return replaceCssUrlToDataUrl(value, null, context, true).then((newValue) => {
1510
+ if (!newValue || value === newValue)
1511
+ return;
1512
+ style.setProperty(
1513
+ property,
1514
+ newValue,
1515
+ style.getPropertyPriority(property)
1516
+ );
1517
+ });
1518
+ }).filter(Boolean);
1519
+ }
1520
+ function embedImageElement(cloned, context) {
1521
+ if (isImageElement(cloned)) {
1522
+ const originalSrc = cloned.currentSrc || cloned.src;
1523
+ if (!isDataUrl(originalSrc)) {
1524
+ return [
1525
+ contextFetch(context, {
1526
+ url: originalSrc,
1527
+ imageDom: cloned,
1528
+ requestType: "image",
1529
+ responseType: "dataUrl"
1530
+ }).then((url) => {
1531
+ if (!url)
1532
+ return;
1533
+ cloned.srcset = "";
1534
+ cloned.dataset.originalSrc = originalSrc;
1535
+ cloned.src = url || "";
1536
+ })
1537
+ ];
1538
+ }
1539
+ if (IN_SAFARI || IN_FIREFOX) {
1540
+ context.drawImageCount++;
1541
+ }
1542
+ } else if (isSVGElementNode(cloned) && !isDataUrl(cloned.href.baseVal)) {
1543
+ const originalSrc = cloned.href.baseVal;
1544
+ return [
1545
+ contextFetch(context, {
1546
+ url: originalSrc,
1547
+ imageDom: cloned,
1548
+ requestType: "image",
1549
+ responseType: "dataUrl"
1550
+ }).then((url) => {
1551
+ if (!url)
1552
+ return;
1553
+ cloned.dataset.originalSrc = originalSrc;
1554
+ cloned.href.baseVal = url || "";
1555
+ })
1556
+ ];
1557
+ }
1558
+ return [];
1559
+ }
1560
+ function embedSvgUse(cloned, context) {
1561
+ const { ownerDocument, svgDefsElement } = context;
1562
+ const href = cloned.getAttribute("href") ?? cloned.getAttribute("xlink:href");
1563
+ if (!href)
1564
+ return [];
1565
+ const [svgUrl, id] = href.split("#");
1566
+ if (id) {
1567
+ const query = `#${id}`;
1568
+ const definition = context.shadowRoots.reduce(
1569
+ (res, root) => {
1570
+ return res ?? root.querySelector(`svg ${query}`);
1571
+ },
1572
+ ownerDocument?.querySelector(`svg ${query}`)
1573
+ );
1574
+ if (svgUrl) {
1575
+ cloned.setAttribute("href", query);
1576
+ }
1577
+ if (svgDefsElement?.querySelector(query))
1578
+ return [];
1579
+ if (definition) {
1580
+ svgDefsElement?.appendChild(definition.cloneNode(true));
1581
+ return [];
1582
+ } else if (svgUrl) {
1583
+ return [
1584
+ contextFetch(context, {
1585
+ url: svgUrl,
1586
+ responseType: "text"
1587
+ }).then((svgData) => {
1588
+ svgDefsElement?.insertAdjacentHTML("beforeend", svgData);
1589
+ })
1590
+ ];
1591
+ }
1592
+ }
1593
+ return [];
1594
+ }
1595
+ function embedNode(cloned, context) {
1596
+ const { tasks } = context;
1597
+ if (isElementNode(cloned)) {
1598
+ if (isImageElement(cloned) || isSVGImageElementNode(cloned)) {
1599
+ tasks.push(...embedImageElement(cloned, context));
1600
+ }
1601
+ if (isSVGUseElementNode(cloned)) {
1602
+ tasks.push(...embedSvgUse(cloned, context));
1603
+ }
1604
+ }
1605
+ if (isHTMLElementNode(cloned)) {
1606
+ tasks.push(...embedCssStyleImage(cloned.style, context));
1607
+ }
1608
+ cloned.childNodes.forEach((child) => {
1609
+ embedNode(child, context);
1610
+ });
1611
+ }
1612
+ async function embedWebFont(clone, context) {
1613
+ const {
1614
+ ownerDocument,
1615
+ svgStyleElement,
1616
+ fontFamilies,
1617
+ fontCssTexts,
1618
+ tasks,
1619
+ font
1620
+ } = context;
1621
+ if (!ownerDocument || !svgStyleElement || !fontFamilies.size) {
1622
+ return;
1623
+ }
1624
+ if (font && font.cssText) {
1625
+ const cssText = filterPreferredFormat(font.cssText, context);
1626
+ svgStyleElement.appendChild(ownerDocument.createTextNode(`${cssText}
1627
+ `));
1628
+ } else {
1629
+ const styleSheets = Array.from(ownerDocument.styleSheets).filter((styleSheet) => {
1630
+ try {
1631
+ return "cssRules" in styleSheet && Boolean(styleSheet.cssRules.length);
1632
+ } catch (error) {
1633
+ context.log.warn(`Error while reading CSS rules from ${styleSheet.href}`, error);
1634
+ return false;
1635
+ }
1636
+ });
1637
+ const tempDoc = ownerDocument.implementation.createHTMLDocument("");
1638
+ const tempStyleEl = tempDoc.createElement("style");
1639
+ tempDoc.head.appendChild(tempStyleEl);
1640
+ const tempStyleSheet = tempStyleEl.sheet;
1641
+ await Promise.all(
1642
+ styleSheets.flatMap((styleSheet) => {
1643
+ return Array.from(styleSheet.cssRules).map(async (cssRule) => {
1644
+ if (isCSSImportRule(cssRule)) {
1645
+ const baseUrl = cssRule.href;
1646
+ let cssText = "";
1647
+ try {
1648
+ cssText = await contextFetch(context, {
1649
+ url: baseUrl,
1650
+ requestType: "text",
1651
+ responseType: "text"
1652
+ });
1653
+ } catch (error) {
1654
+ context.log.warn(`Error fetch remote css import from ${baseUrl}`, error);
1655
+ }
1656
+ const replacedCssText = cssText.replace(
1657
+ URL_RE,
1658
+ (raw, quotation, url) => raw.replace(url, resolveUrl(url, baseUrl))
1659
+ );
1660
+ for (const rule of parseCss(replacedCssText)) {
1661
+ try {
1662
+ tempStyleSheet.insertRule(rule, tempStyleSheet.cssRules.length);
1663
+ } catch (error) {
1664
+ context.log.warn("Error inserting rule from remote css import", { rule, error });
1665
+ }
1666
+ }
1667
+ }
1668
+ });
1669
+ })
1670
+ );
1671
+ if (tempStyleSheet.cssRules.length)
1672
+ styleSheets.push(tempStyleSheet);
1673
+ const cssRules = [];
1674
+ styleSheets.forEach((sheet) => {
1675
+ unwrapCssLayers(sheet.cssRules, cssRules);
1676
+ });
1677
+ cssRules.filter((cssRule) => isCssFontFaceRule(cssRule) && hasCssUrl(cssRule.style.getPropertyValue("src")) && splitFontFamily(cssRule.style.getPropertyValue("font-family"))?.some((val) => fontFamilies.has(val))).forEach((value) => {
1678
+ const rule = value;
1679
+ const cssText = fontCssTexts.get(rule.cssText);
1680
+ if (cssText) {
1681
+ svgStyleElement.appendChild(ownerDocument.createTextNode(`${cssText}
1682
+ `));
1683
+ } else {
1684
+ tasks.push(
1685
+ replaceCssUrlToDataUrl(
1686
+ rule.cssText,
1687
+ rule.parentStyleSheet ? rule.parentStyleSheet.href : null,
1688
+ context
1689
+ ).then((cssText2) => {
1690
+ cssText2 = filterPreferredFormat(cssText2, context);
1691
+ fontCssTexts.set(rule.cssText, cssText2);
1692
+ svgStyleElement.appendChild(ownerDocument.createTextNode(`${cssText2}
1693
+ `));
1694
+ })
1695
+ );
1696
+ }
1697
+ });
1698
+ }
1699
+ }
1700
+ var COMMENTS_RE = /(\/\*[\s\S]*?\*\/)/g;
1701
+ var KEYFRAMES_RE = /((@.*?keyframes [\s\S]*?){([\s\S]*?}\s*?)})/gi;
1702
+ function parseCss(source) {
1703
+ if (source == null)
1704
+ return [];
1705
+ const result = [];
1706
+ let cssText = source.replace(COMMENTS_RE, "");
1707
+ while (true) {
1708
+ const matches = KEYFRAMES_RE.exec(cssText);
1709
+ if (!matches)
1710
+ break;
1711
+ result.push(matches[0]);
1712
+ }
1713
+ cssText = cssText.replace(KEYFRAMES_RE, "");
1714
+ const IMPORT_RE = /@import[\s\S]*?url\([^)]*\)[\s\S]*?;/gi;
1715
+ const UNIFIED_RE = new RegExp(
1716
+ // eslint-disable-next-line
1717
+ "((\\s*?(?:\\/\\*[\\s\\S]*?\\*\\/)?\\s*?@media[\\s\\S]*?){([\\s\\S]*?)}\\s*?})|(([\\s\\S]*?){([\\s\\S]*?)})",
1718
+ "gi"
1719
+ );
1720
+ while (true) {
1721
+ let matches = IMPORT_RE.exec(cssText);
1722
+ if (!matches) {
1723
+ matches = UNIFIED_RE.exec(cssText);
1724
+ if (!matches) {
1725
+ break;
1726
+ } else {
1727
+ IMPORT_RE.lastIndex = UNIFIED_RE.lastIndex;
1728
+ }
1729
+ } else {
1730
+ UNIFIED_RE.lastIndex = IMPORT_RE.lastIndex;
1731
+ }
1732
+ result.push(matches[0]);
1733
+ }
1734
+ return result;
1735
+ }
1736
+ var URL_WITH_FORMAT_RE = /url\([^)]+\)\s*format\((["']?)([^"']+)\1\)/g;
1737
+ var FONT_SRC_RE = /src:\s*(?:url\([^)]+\)\s*format\([^)]+\)[,;]\s*)+/g;
1738
+ function filterPreferredFormat(str, context) {
1739
+ const { font } = context;
1740
+ const preferredFormat = font ? font?.preferredFormat : void 0;
1741
+ return preferredFormat ? str.replace(FONT_SRC_RE, (match) => {
1742
+ while (true) {
1743
+ const [src, , format] = URL_WITH_FORMAT_RE.exec(match) || [];
1744
+ if (!format)
1745
+ return "";
1746
+ if (format === preferredFormat)
1747
+ return `src: ${src};`;
1748
+ }
1749
+ }) : str;
1750
+ }
1751
+ function unwrapCssLayers(rules, out = []) {
1752
+ for (const rule of Array.from(rules)) {
1753
+ if (isLayerBlockRule(rule)) {
1754
+ out.push(...unwrapCssLayers(rule.cssRules));
1755
+ } else if ("cssRules" in rule) {
1756
+ unwrapCssLayers(rule.cssRules, out);
1757
+ } else {
1758
+ out.push(rule);
1759
+ }
1760
+ }
1761
+ return out;
1762
+ }
1763
+ var SVG_EXTERNAL_RESOURCE_REGEX = /\bx?link:?href\s*=\s*["'](?!data:)[^"']+["']/i;
1764
+ function svgHasExternalResources(svg) {
1765
+ return SVG_EXTERNAL_RESOURCE_REGEX.test(svg.innerHTML);
1766
+ }
1767
+ async function domToForeignObjectSvg(node, options) {
1768
+ const context = await orCreateContext(node, options);
1769
+ if (isElementNode(context.node) && isSVGElementNode(context.node) && !svgHasExternalResources(context.node))
1770
+ return context.node;
1771
+ const {
1772
+ ownerDocument,
1773
+ log,
1774
+ tasks,
1775
+ svgStyleElement,
1776
+ svgDefsElement,
1777
+ svgStyles,
1778
+ font,
1779
+ progress,
1780
+ autoDestruct,
1781
+ onCloneNode,
1782
+ onEmbedNode,
1783
+ onCreateForeignObjectSvg
1784
+ } = context;
1785
+ log.time("clone node");
1786
+ const clone = await cloneNode(context.node, context, true);
1787
+ if (svgStyleElement && ownerDocument) {
1788
+ let allCssText = "";
1789
+ svgStyles.forEach((klasses, cssText) => {
1790
+ allCssText += `${klasses.join(",\n")} {
1791
+ ${cssText}
1792
+ }
1793
+ `;
1794
+ });
1795
+ svgStyleElement.appendChild(ownerDocument.createTextNode(allCssText));
1796
+ }
1797
+ log.timeEnd("clone node");
1798
+ await onCloneNode?.(clone);
1799
+ if (font !== false && isElementNode(clone)) {
1800
+ log.time("embed web font");
1801
+ await embedWebFont(clone, context);
1802
+ log.timeEnd("embed web font");
1803
+ }
1804
+ log.time("embed node");
1805
+ embedNode(clone, context);
1806
+ const count = tasks.length;
1807
+ let current = 0;
1808
+ const runTask = async () => {
1809
+ while (true) {
1810
+ const task = tasks.pop();
1811
+ if (!task)
1812
+ break;
1813
+ try {
1814
+ await task;
1815
+ } catch (error) {
1816
+ context.log.warn("Failed to run task", error);
1817
+ }
1818
+ progress?.(++current, count);
1819
+ }
1820
+ };
1821
+ progress?.(current, count);
1822
+ await Promise.all([...Array.from({ length: 4 })].map(runTask));
1823
+ log.timeEnd("embed node");
1824
+ await onEmbedNode?.(clone);
1825
+ const svg = createForeignObjectSvg(clone, context);
1826
+ svgDefsElement && svg.insertBefore(svgDefsElement, svg.children[0]);
1827
+ svgStyleElement && svg.insertBefore(svgStyleElement, svg.children[0]);
1828
+ autoDestruct && destroyContext(context);
1829
+ await onCreateForeignObjectSvg?.(svg);
1830
+ return svg;
1831
+ }
1832
+ function createForeignObjectSvg(clone, context) {
1833
+ const { width, height } = context;
1834
+ const svg = createSvg(width, height, clone.ownerDocument);
1835
+ const foreignObject = svg.ownerDocument.createElementNS(svg.namespaceURI, "foreignObject");
1836
+ foreignObject.setAttributeNS(null, "x", "0%");
1837
+ foreignObject.setAttributeNS(null, "y", "0%");
1838
+ foreignObject.setAttributeNS(null, "width", "100%");
1839
+ foreignObject.setAttributeNS(null, "height", "100%");
1840
+ foreignObject.append(clone);
1841
+ svg.appendChild(foreignObject);
1842
+ return svg;
1843
+ }
1844
+ async function domToCanvas(node, options) {
1845
+ const context = await orCreateContext(node, options);
1846
+ const svg = await domToForeignObjectSvg(context);
1847
+ const dataUrl = svgToDataUrl(svg, context.isEnable("removeControlCharacter"));
1848
+ if (!context.autoDestruct) {
1849
+ context.svgStyleElement = createStyleElement(context.ownerDocument);
1850
+ context.svgDefsElement = context.ownerDocument?.createElementNS(XMLNS, "defs");
1851
+ context.svgStyles.clear();
1852
+ }
1853
+ const image = createImage(dataUrl, svg.ownerDocument);
1854
+ return await imageToCanvas(image, context);
1855
+ }
1856
+ async function domToDataUrl(node, options) {
1857
+ const context = await orCreateContext(node, options);
1858
+ const { log, quality, type, dpi } = context;
1859
+ const canvas = await domToCanvas(context);
1860
+ log.time("canvas to data url");
1861
+ let dataUrl = canvas.toDataURL(type, quality);
1862
+ if (["image/png", "image/jpeg"].includes(type) && dpi && SUPPORT_ATOB && SUPPORT_BTOA) {
1863
+ const [format, body] = dataUrl.split(",");
1864
+ let headerLength = 0;
1865
+ let overwritepHYs = false;
1866
+ if (type === "image/png") {
1867
+ const b64Index = detectPhysChunkFromDataUrl(body);
1868
+ if (b64Index >= 0) {
1869
+ headerLength = Math.ceil((b64Index + 28) / 3) * 4;
1870
+ overwritepHYs = true;
1871
+ } else {
1872
+ headerLength = 33 / 3 * 4;
1873
+ }
1874
+ } else if (type === "image/jpeg") {
1875
+ headerLength = 18 / 3 * 4;
1876
+ }
1877
+ const stringHeader = body.substring(0, headerLength);
1878
+ const restOfData = body.substring(headerLength);
1879
+ const headerBytes = window.atob(stringHeader);
1880
+ const uint8Array = new Uint8Array(headerBytes.length);
1881
+ for (let i = 0; i < uint8Array.length; i++) {
1882
+ uint8Array[i] = headerBytes.charCodeAt(i);
1883
+ }
1884
+ const finalArray = type === "image/png" ? changePngDpi(uint8Array, dpi, overwritepHYs) : changeJpegDpi(uint8Array, dpi);
1885
+ const base64Header = window.btoa(String.fromCharCode(...finalArray));
1886
+ dataUrl = [format, ",", base64Header, restOfData].join("");
1887
+ }
1888
+ log.timeEnd("canvas to data url");
1889
+ return dataUrl;
1890
+ }
1891
+ async function domToPng(node, options) {
1892
+ return domToDataUrl(
1893
+ await orCreateContext(node, { ...options, type: "image/png" })
1894
+ );
1895
+ }
1896
+
1897
+ // src/capture.ts
1898
+ var STYLE_KEYS = [
1899
+ "display",
1900
+ "position",
1901
+ "width",
1902
+ "height",
1903
+ "margin",
1904
+ "padding",
1905
+ "color",
1906
+ "background-color",
1907
+ "font-size",
1908
+ "font-weight",
1909
+ "font-family",
1910
+ "border",
1911
+ "border-radius",
1912
+ "box-shadow",
1913
+ "flex",
1914
+ "grid-template-columns",
1915
+ "text-align",
1916
+ "line-height",
1917
+ "opacity"
1918
+ ];
1919
+ function captureElementContext(el2) {
1920
+ const html = el2.outerHTML.length > 6e3 ? el2.outerHTML.slice(0, 6e3) + "\u2026" : el2.outerHTML;
1921
+ const cs = getComputedStyle(el2);
1922
+ const styles = {};
1923
+ for (const k of STYLE_KEYS) {
1924
+ const v = cs.getPropertyValue(k);
1925
+ if (v) styles[k] = v.trim();
1926
+ }
1927
+ return { html, styles };
1928
+ }
1929
+ async function captureScreenshot(el2) {
1930
+ try {
1931
+ return await domToPng(el2, {
1932
+ scale: Math.min(window.devicePixelRatio || 1, 2),
1933
+ backgroundColor: getComputedStyle(document.body).backgroundColor || "#ffffff",
1934
+ filter: (node) => {
1935
+ if (!(node instanceof Element)) return true;
1936
+ if (node.id === "loupe-root") return false;
1937
+ if (node.hasAttribute("data-loupe-redact")) return false;
1938
+ return true;
1939
+ }
1940
+ });
1941
+ } catch (err) {
1942
+ console.warn("[loupe] screenshot capture failed", err);
1943
+ return void 0;
1944
+ }
1945
+ }
1946
+
1947
+ // src/store.ts
1948
+ var LocalStorageAdapter = class {
1949
+ key(projectKey, url) {
1950
+ return `loupe:${projectKey}:${url}`;
1951
+ }
1952
+ readAll(projectKey, url) {
1953
+ try {
1954
+ const raw = localStorage.getItem(this.key(projectKey, url));
1955
+ return raw ? JSON.parse(raw) : [];
1956
+ } catch {
1957
+ return [];
1958
+ }
1959
+ }
1960
+ writeAll(projectKey, url, comments) {
1961
+ localStorage.setItem(this.key(projectKey, url), JSON.stringify(comments));
1962
+ }
1963
+ async list(projectKey, url) {
1964
+ return this.readAll(projectKey, url);
1965
+ }
1966
+ async save(comment) {
1967
+ const all = this.readAll(comment.projectKey, comment.url);
1968
+ all.push(comment);
1969
+ this.writeAll(comment.projectKey, comment.url, all);
1970
+ return comment;
1971
+ }
1972
+ async update(id, patch) {
1973
+ for (let i = 0; i < localStorage.length; i++) {
1974
+ const k = localStorage.key(i);
1975
+ if (!k || !k.startsWith("loupe:")) continue;
1976
+ const list = JSON.parse(localStorage.getItem(k) || "[]");
1977
+ const idx = list.findIndex((c) => c.id === id);
1978
+ if (idx >= 0) {
1979
+ list[idx] = { ...list[idx], ...patch };
1980
+ localStorage.setItem(k, JSON.stringify(list));
1981
+ return;
1982
+ }
1983
+ }
1984
+ }
1985
+ async remove(id) {
1986
+ for (let i = 0; i < localStorage.length; i++) {
1987
+ const k = localStorage.key(i);
1988
+ if (!k || !k.startsWith("loupe:")) continue;
1989
+ const list = JSON.parse(localStorage.getItem(k) || "[]");
1990
+ const next = list.filter((c) => c.id !== id);
1991
+ if (next.length !== list.length) {
1992
+ localStorage.setItem(k, JSON.stringify(next));
1993
+ return;
1994
+ }
1995
+ }
1996
+ }
1997
+ };
1998
+
1999
+ // src/http-adapter.ts
2000
+ var HttpAdapter = class {
2001
+ constructor(base, user, userHmac) {
2002
+ this.base = base;
2003
+ this.user = user;
2004
+ this.userHmac = userHmac;
2005
+ this.base = base.replace(/\/$/, "");
2006
+ }
2007
+ headers() {
2008
+ const h = { "Content-Type": "application/json", "X-Loupe-User": this.user.id };
2009
+ if (this.userHmac) h["X-Loupe-Hmac"] = this.userHmac;
2010
+ return h;
2011
+ }
2012
+ async list(projectKey, url) {
2013
+ const q = new URLSearchParams({ projectKey, url });
2014
+ const res = await fetch(`${this.base}/v1/comments?${q}`, { headers: this.headers() });
2015
+ if (!res.ok) throw new Error(`list failed: ${res.status}`);
2016
+ return await res.json();
2017
+ }
2018
+ async save(comment) {
2019
+ if (comment.screenshot?.startsWith("data:")) {
2020
+ try {
2021
+ const up = await fetch(`${this.base}/v1/blobs`, {
2022
+ method: "POST",
2023
+ headers: this.headers(),
2024
+ body: JSON.stringify({ projectKey: comment.projectKey, data: comment.screenshot })
2025
+ });
2026
+ if (up.ok) comment = { ...comment, screenshot: (await up.json()).url };
2027
+ } catch {
2028
+ }
2029
+ }
2030
+ const res = await fetch(`${this.base}/v1/comments`, {
2031
+ method: "POST",
2032
+ headers: this.headers(),
2033
+ body: JSON.stringify(comment)
2034
+ });
2035
+ if (!res.ok) throw new Error(`save failed: ${res.status}`);
2036
+ return await res.json();
2037
+ }
2038
+ async update(id, patch) {
2039
+ const res = await fetch(`${this.base}/v1/comments/${encodeURIComponent(id)}`, {
2040
+ method: "PATCH",
2041
+ headers: this.headers(),
2042
+ body: JSON.stringify(patch)
2043
+ });
2044
+ if (!res.ok) throw new Error(`update failed: ${res.status}`);
2045
+ }
2046
+ async remove(id) {
2047
+ const res = await fetch(`${this.base}/v1/comments/${encodeURIComponent(id)}`, {
2048
+ method: "DELETE",
2049
+ headers: this.headers()
2050
+ });
2051
+ if (!res.ok && res.status !== 404) throw new Error(`remove failed: ${res.status}`);
2052
+ }
2053
+ };
2054
+
2055
+ // src/app.ts
2056
+ var uid2 = () => crypto.randomUUID ? crypto.randomUUID() : "c_" + Math.abs(hash(String(performance.now()))).toString(36);
2057
+ function hash(s) {
2058
+ let h = 0;
2059
+ for (let i = 0; i < s.length; i++) h = h * 31 + s.charCodeAt(i) | 0;
2060
+ return h;
2061
+ }
2062
+ var LoupeApp = class {
2063
+ constructor(cfg) {
2064
+ this.comments = [];
2065
+ /** comment.id → currently resolved element (or null when detached). */
2066
+ this.resolved = /* @__PURE__ */ new Map();
2067
+ /** comment.id → pin element. */
2068
+ this.pins = /* @__PURE__ */ new Map();
2069
+ this.inspecting = false;
2070
+ this.target = null;
2071
+ this.targetOffset = { x: 0.5, y: 0.5 };
2072
+ this.activeId = null;
2073
+ this.raf = 0;
2074
+ // ---- inspector ------------------------------------------------------------
2075
+ this.onMove = (e) => {
2076
+ if (!this.inspecting) return;
2077
+ const target = this.pick(e.clientX, e.clientY);
2078
+ if (!target) {
2079
+ this.hl.style.display = "none";
2080
+ return;
2081
+ }
2082
+ const r = target.getBoundingClientRect();
2083
+ Object.assign(this.hl.style, {
2084
+ display: "block",
2085
+ left: r.left + "px",
2086
+ top: r.top + "px",
2087
+ width: r.width + "px",
2088
+ height: r.height + "px"
2089
+ });
2090
+ this.hl.firstChild.textContent = target.tagName.toLowerCase() + (target.id ? "#" + target.id : "");
2091
+ };
2092
+ this.onClick = (e) => {
2093
+ if (!this.inspecting) return;
2094
+ const target = this.pick(e.clientX, e.clientY);
2095
+ if (!target) return;
2096
+ e.preventDefault();
2097
+ e.stopPropagation();
2098
+ const r = target.getBoundingClientRect();
2099
+ this.targetOffset = {
2100
+ x: r.width ? clamp((e.clientX - r.left) / r.width) : 0.5,
2101
+ y: r.height ? clamp((e.clientY - r.top) / r.height) : 0.5
2102
+ };
2103
+ this.target = target;
2104
+ this.setInspecting(false);
2105
+ this.openComposer(target, e.clientX, e.clientY);
2106
+ };
2107
+ this.onKey = (e) => {
2108
+ if (e.key === "Escape") {
2109
+ this.setInspecting(false);
2110
+ this.closeComposer();
2111
+ }
2112
+ };
2113
+ /** Reposition every pin, re-resolving anchors whose element has gone. */
2114
+ this.position = () => {
2115
+ for (const c of this.comments) {
2116
+ const pin = this.pins.get(c.id);
2117
+ if (!pin) continue;
2118
+ let elx = this.resolved.get(c.id) ?? null;
2119
+ if (!elx || !elx.isConnected) {
2120
+ const r = resolveAnchor(c.anchor);
2121
+ elx = r?.element ?? null;
2122
+ this.resolved.set(c.id, elx);
2123
+ }
2124
+ if (elx) {
2125
+ const rect = elx.getBoundingClientRect();
2126
+ const px = rect.left + c.offset.x * rect.width;
2127
+ const py = rect.top + c.offset.y * rect.height;
2128
+ const onScreen = rect.bottom > 0 && rect.top < window.innerHeight && rect.width > 0;
2129
+ Object.assign(pin.style, { left: px + "px", top: py + "px", display: onScreen ? "grid" : "none" });
2130
+ pin.classList.remove("detached");
2131
+ } else {
2132
+ pin.style.display = "none";
2133
+ pin.classList.add("detached");
2134
+ }
2135
+ }
2136
+ };
2137
+ this.cfg = cfg;
2138
+ this.store = cfg.apiBase ? new HttpAdapter(cfg.apiBase, cfg.user, cfg.userHmac) : new LocalStorageAdapter();
2139
+ }
2140
+ get url() {
2141
+ return location.pathname + location.search;
2142
+ }
2143
+ async start() {
2144
+ this.buildDom();
2145
+ this.comments = await this.store.list(this.cfg.projectKey, this.url);
2146
+ this.renderPins();
2147
+ this.renderPanel();
2148
+ this.observe();
2149
+ if (this.cfg.autoOpen) this.setInspecting(true);
2150
+ }
2151
+ // ---- DOM construction -----------------------------------------------------
2152
+ buildDom() {
2153
+ this.root = document.createElement("div");
2154
+ this.root.id = "loupe-root";
2155
+ document.body.appendChild(this.root);
2156
+ this.shadow = this.root.attachShadow({ mode: "open" });
2157
+ const style = document.createElement("style");
2158
+ style.textContent = STYLES;
2159
+ this.shadow.appendChild(style);
2160
+ this.overlay = el("div", "overlay");
2161
+ this.hl = el("div", "hl");
2162
+ this.hl.appendChild(el("span", "tip"));
2163
+ this.overlay.appendChild(this.hl);
2164
+ this.shadow.appendChild(this.overlay);
2165
+ this.composer = el("div", "composer");
2166
+ this.shadow.appendChild(this.composer);
2167
+ this.panel = el("div", "panel");
2168
+ this.shadow.appendChild(this.panel);
2169
+ this.toolbar = this.buildToolbar();
2170
+ this.shadow.appendChild(this.toolbar);
2171
+ }
2172
+ buildToolbar() {
2173
+ const bar = el("div", "toolbar");
2174
+ const brand = el("span", "brand", "\u25CE Loupe");
2175
+ const inspectBtn = el("button", "", "\u271B Inspect & comment");
2176
+ inspectBtn.dataset.role = "inspect";
2177
+ inspectBtn.onclick = () => this.setInspecting(!this.inspecting);
2178
+ const listBtn = el("button", "", "\u2630 Comments");
2179
+ this.countEl = el("span", "count", "0");
2180
+ listBtn.appendChild(this.countEl);
2181
+ listBtn.onclick = () => this.togglePanel();
2182
+ bar.append(brand, sep(), inspectBtn, listBtn);
2183
+ return bar;
2184
+ }
2185
+ /** elementFromPoint, ignoring our own UI. */
2186
+ pick(x, y) {
2187
+ const hitHl = this.hl.style.display;
2188
+ this.hl.style.display = "none";
2189
+ const elAt = document.elementFromPoint(x, y);
2190
+ this.hl.style.display = hitHl;
2191
+ if (!elAt) return null;
2192
+ if (elAt.id === "loupe-root" || elAt.closest?.("#loupe-root")) return null;
2193
+ return elAt;
2194
+ }
2195
+ setInspecting(on) {
2196
+ this.inspecting = on;
2197
+ this.hl.style.display = "none";
2198
+ document.body.style.cursor = on ? "crosshair" : "";
2199
+ const btn = this.toolbar.querySelector('[data-role="inspect"]');
2200
+ btn.classList.toggle("on", on);
2201
+ if (on) {
2202
+ document.addEventListener("mousemove", this.onMove, true);
2203
+ document.addEventListener("click", this.onClick, true);
2204
+ document.addEventListener("keydown", this.onKey, true);
2205
+ this.closeComposer();
2206
+ } else {
2207
+ document.removeEventListener("mousemove", this.onMove, true);
2208
+ document.removeEventListener("click", this.onClick, true);
2209
+ }
2210
+ }
2211
+ // ---- composer -------------------------------------------------------------
2212
+ openComposer(target, x, y) {
2213
+ const c = this.composer;
2214
+ c.innerHTML = "";
2215
+ const label = el("div", "target", describe(target));
2216
+ const ta = el("textarea");
2217
+ ta.placeholder = "What should change here?";
2218
+ const row = el("div", "row");
2219
+ const chk = el("label", "chk");
2220
+ const box = document.createElement("input");
2221
+ box.type = "checkbox";
2222
+ box.checked = true;
2223
+ chk.append(box, document.createTextNode("Attach screenshot"));
2224
+ const btns = el("div", "btns");
2225
+ const cancel = el("button", "ghost", "Cancel");
2226
+ cancel.onclick = () => this.closeComposer();
2227
+ const save = el("button", "primary", "Comment");
2228
+ save.disabled = true;
2229
+ ta.oninput = () => {
2230
+ save.disabled = !ta.value.trim();
2231
+ };
2232
+ save.onclick = () => this.submit(target, ta.value.trim(), box.checked);
2233
+ btns.append(cancel, save);
2234
+ row.append(chk, btns);
2235
+ c.append(label, ta, row);
2236
+ const w = 300, h = 190;
2237
+ const left = Math.min(Math.max(8, x + 12), window.innerWidth - w - 8);
2238
+ const top = Math.min(Math.max(8, y + 12), window.innerHeight - h - 8);
2239
+ Object.assign(c.style, { display: "block", left: left + "px", top: top + "px" });
2240
+ ta.focus();
2241
+ }
2242
+ closeComposer() {
2243
+ this.composer.style.display = "none";
2244
+ this.target = null;
2245
+ }
2246
+ async submit(target, body, withShot) {
2247
+ if (!body) return;
2248
+ const saveBtn = this.composer.querySelector(".primary");
2249
+ if (saveBtn) {
2250
+ saveBtn.disabled = true;
2251
+ saveBtn.textContent = "Saving\u2026";
2252
+ }
2253
+ const capture = this.cfg.captureScreenshot ?? captureScreenshot;
2254
+ const screenshot = withShot ? await capture(target) : void 0;
2255
+ const comment = {
2256
+ id: uid2(),
2257
+ projectKey: this.cfg.projectKey,
2258
+ url: this.url,
2259
+ author: this.cfg.user,
2260
+ body,
2261
+ status: "open",
2262
+ anchor: captureAnchor(target),
2263
+ context: captureElementContext(target),
2264
+ offset: this.targetOffset,
2265
+ screenshot,
2266
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2267
+ };
2268
+ await this.store.save(comment);
2269
+ this.comments.push(comment);
2270
+ this.resolved.set(comment.id, target);
2271
+ this.closeComposer();
2272
+ this.renderPins();
2273
+ this.renderPanel();
2274
+ this.flash(comment.id);
2275
+ }
2276
+ // ---- pins + re-anchoring --------------------------------------------------
2277
+ renderPins() {
2278
+ for (const [id, pin] of this.pins) {
2279
+ if (!this.comments.find((c) => c.id === id)) {
2280
+ pin.remove();
2281
+ this.pins.delete(id);
2282
+ }
2283
+ }
2284
+ this.comments.forEach((c, i) => {
2285
+ let pin = this.pins.get(c.id);
2286
+ if (!pin) {
2287
+ pin = el("button", "pin");
2288
+ pin.onclick = () => {
2289
+ this.openPanel();
2290
+ this.flash(c.id);
2291
+ };
2292
+ this.overlay.appendChild(pin);
2293
+ this.pins.set(c.id, pin);
2294
+ }
2295
+ pin.textContent = String(i + 1);
2296
+ pin.classList.toggle("done", c.status === "done");
2297
+ });
2298
+ this.countEl.textContent = String(this.comments.length);
2299
+ this.position();
2300
+ }
2301
+ observe() {
2302
+ const reposition = () => {
2303
+ cancelAnimationFrame(this.raf);
2304
+ this.raf = requestAnimationFrame(this.position);
2305
+ };
2306
+ window.addEventListener("scroll", reposition, true);
2307
+ window.addEventListener("resize", reposition);
2308
+ let debounce = 0;
2309
+ this.mo = new MutationObserver(() => {
2310
+ clearTimeout(debounce);
2311
+ debounce = window.setTimeout(() => {
2312
+ this.position();
2313
+ this.renderPanel();
2314
+ }, 120);
2315
+ });
2316
+ this.mo.observe(document.body, { childList: true, subtree: true, attributes: true, characterData: true });
2317
+ this.tick = window.setInterval(this.position, 800);
2318
+ }
2319
+ // ---- panel ----------------------------------------------------------------
2320
+ openPanel() {
2321
+ this.panel.classList.add("open");
2322
+ this.renderPanel();
2323
+ }
2324
+ togglePanel() {
2325
+ this.panel.classList.toggle("open");
2326
+ this.renderPanel();
2327
+ }
2328
+ renderPanel() {
2329
+ const p = this.panel;
2330
+ p.innerHTML = "";
2331
+ const head = el("div", "phead");
2332
+ head.append(document.createTextNode(`Comments \xB7 ${this.comments.length}`));
2333
+ const x = el("button", "x", "\xD7");
2334
+ x.onclick = () => p.classList.remove("open");
2335
+ head.appendChild(x);
2336
+ p.appendChild(head);
2337
+ const list = el("div", "list");
2338
+ if (!this.comments.length) {
2339
+ list.appendChild(el("div", "empty", "No comments yet. Click \u201CInspect & comment\u201D, then click any element."));
2340
+ }
2341
+ this.comments.forEach((c, i) => list.appendChild(this.itemView(c, i)));
2342
+ p.appendChild(list);
2343
+ }
2344
+ itemView(c, i) {
2345
+ const detached = this.pins.get(c.id)?.classList.contains("detached") && c.status !== "done";
2346
+ const item = el("div", "item");
2347
+ const top = el("div", "top");
2348
+ const num = el("span", "num" + (c.status === "done" ? " done" : detached ? " detached" : ""), String(i + 1));
2349
+ const who = el("span", "who", c.author.name);
2350
+ top.append(num, who);
2351
+ if (c.status === "done") top.appendChild(el("span", "badge done", "done"));
2352
+ else if (detached) top.appendChild(el("span", "badge detached", "element moved/removed"));
2353
+ item.appendChild(top);
2354
+ item.appendChild(el("div", "body", c.body));
2355
+ item.appendChild(el("div", "meta", describeAnchor(c)));
2356
+ if (c.screenshot) {
2357
+ const img = el("img", "shot");
2358
+ img.src = c.screenshot;
2359
+ item.appendChild(img);
2360
+ }
2361
+ const actions = el("div", "actions");
2362
+ const doneBtn = el("button", "", c.status === "done" ? "Reopen" : "Mark done");
2363
+ doneBtn.onclick = async (e) => {
2364
+ e.stopPropagation();
2365
+ const status = c.status === "done" ? "open" : "done";
2366
+ c.status = status;
2367
+ await this.store.update(c.id, { status });
2368
+ this.renderPins();
2369
+ this.renderPanel();
2370
+ };
2371
+ const claudeBtn = el("button", "", "Copy for Claude");
2372
+ claudeBtn.onclick = async (e) => {
2373
+ e.stopPropagation();
2374
+ await this.copyForClaude(c);
2375
+ claudeBtn.textContent = "Copied \u2713";
2376
+ };
2377
+ const del = el("button", "", "Delete");
2378
+ del.onclick = async (e) => {
2379
+ e.stopPropagation();
2380
+ await this.store.remove(c.id);
2381
+ this.comments = this.comments.filter((x) => x.id !== c.id);
2382
+ this.resolved.delete(c.id);
2383
+ this.renderPins();
2384
+ this.renderPanel();
2385
+ };
2386
+ actions.append(doneBtn, claudeBtn, del);
2387
+ item.appendChild(actions);
2388
+ item.onclick = () => this.flash(c.id);
2389
+ return item;
2390
+ }
2391
+ async copyForClaude(c) {
2392
+ const prompt = [
2393
+ `# Product feedback from ${c.author.name}`,
2394
+ ``,
2395
+ `**Comment:** ${c.body}`,
2396
+ `**Page:** ${c.url}`,
2397
+ `**Element:** \`${c.anchor.cssPath}\``,
2398
+ c.anchor.testid ? `**Stable id:** \`${c.anchor.testid}\`` : ``,
2399
+ ``,
2400
+ `## Target element`,
2401
+ "```html",
2402
+ c.context.html,
2403
+ "```",
2404
+ ``,
2405
+ `## Computed styles`,
2406
+ "```json",
2407
+ JSON.stringify(c.context.styles, null, 2),
2408
+ "```"
2409
+ ].filter(Boolean).join("\n");
2410
+ try {
2411
+ await navigator.clipboard.writeText(prompt);
2412
+ } catch {
2413
+ console.log("[loupe] copy failed; prompt:\n" + prompt);
2414
+ }
2415
+ }
2416
+ flash(id) {
2417
+ const pin = this.pins.get(id);
2418
+ if (!pin || pin.classList.contains("detached")) return;
2419
+ for (const p of this.pins.values()) p.classList.remove("active");
2420
+ pin.classList.add("active");
2421
+ const elx = this.resolved.get(id);
2422
+ if (elx) elx.scrollIntoView({ behavior: "smooth", block: "center" });
2423
+ }
2424
+ destroy() {
2425
+ this.setInspecting(false);
2426
+ this.mo?.disconnect();
2427
+ if (this.tick) clearInterval(this.tick);
2428
+ document.removeEventListener("keydown", this.onKey, true);
2429
+ this.root?.remove();
2430
+ }
2431
+ };
2432
+ function el(tag, cls = "", text = "") {
2433
+ const n = document.createElement(tag);
2434
+ if (cls) n.className = cls;
2435
+ if (text) n.textContent = text;
2436
+ return n;
2437
+ }
2438
+ function sep() {
2439
+ return el("span", "sep");
2440
+ }
2441
+ function clamp(n) {
2442
+ return Math.max(0, Math.min(1, n));
2443
+ }
2444
+ function describe(elx) {
2445
+ const testid = elx.getAttribute("data-testid") || elx.getAttribute("data-test");
2446
+ const tag = elx.tagName.toLowerCase();
2447
+ if (testid) return `${tag}[data-testid="${testid}"]`;
2448
+ if (elx.id) return `${tag}#${elx.id}`;
2449
+ const txt = (elx.textContent || "").trim().replace(/\s+/g, " ").slice(0, 32);
2450
+ return txt ? `${tag} \xB7 \u201C${txt}\u201D` : tag;
2451
+ }
2452
+ function describeAnchor(c) {
2453
+ return c.anchor.testid ? `[data-testid="${c.anchor.testid}"]` : c.anchor.cssPath;
2454
+ }
2455
+
2456
+ // src/index.ts
2457
+ var app = null;
2458
+ function init(config) {
2459
+ if (app) return;
2460
+ if (!config?.projectKey) {
2461
+ console.error("[loupe] init requires a projectKey");
2462
+ return;
2463
+ }
2464
+ if (!config.user?.id) {
2465
+ console.error("[loupe] init requires user.id");
2466
+ return;
2467
+ }
2468
+ app = new LoupeApp(config);
2469
+ const boot = () => app.start();
2470
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot);
2471
+ else boot();
2472
+ }
2473
+ function destroy() {
2474
+ app?.destroy();
2475
+ app = null;
2476
+ }
2477
+ return __toCommonJS(src_exports);
2478
+ })();
2479
+ //# sourceMappingURL=index.global.js.map