web-remarq 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,1397 @@
1
+ "use strict";
2
+ var WebRemarq = (() => {
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
+ WebRemarq: () => WebRemarq
25
+ });
26
+
27
+ // src/core/storage.ts
28
+ var STORAGE_KEY = "remarq:annotations";
29
+ var AnnotationStorage = class {
30
+ constructor() {
31
+ this.annotations = [];
32
+ this.extraFields = {};
33
+ this.isMemoryOnly = false;
34
+ this.load();
35
+ }
36
+ getAll() {
37
+ return [...this.annotations];
38
+ }
39
+ getByRoute(route) {
40
+ return this.annotations.filter((a) => a.route === route);
41
+ }
42
+ add(annotation) {
43
+ this.annotations.push(annotation);
44
+ this.save();
45
+ }
46
+ remove(id) {
47
+ this.annotations = this.annotations.filter((a) => a.id !== id);
48
+ this.save();
49
+ }
50
+ update(id, changes) {
51
+ const idx = this.annotations.findIndex((a) => a.id === id);
52
+ if (idx !== -1) {
53
+ this.annotations[idx] = { ...this.annotations[idx], ...changes };
54
+ this.save();
55
+ }
56
+ }
57
+ clearAll() {
58
+ this.annotations = [];
59
+ this.save();
60
+ }
61
+ exportJSON() {
62
+ return {
63
+ version: 1,
64
+ annotations: [...this.annotations]
65
+ };
66
+ }
67
+ importJSON(data) {
68
+ this.annotations = [...data.annotations];
69
+ this.save();
70
+ }
71
+ load() {
72
+ try {
73
+ const raw = localStorage.getItem(STORAGE_KEY);
74
+ if (raw) {
75
+ const parsed = JSON.parse(raw);
76
+ const { version, annotations, ...rest } = parsed;
77
+ this.annotations = annotations ?? [];
78
+ this.extraFields = rest;
79
+ }
80
+ } catch {
81
+ this.isMemoryOnly = true;
82
+ }
83
+ }
84
+ save() {
85
+ if (this.isMemoryOnly) return;
86
+ try {
87
+ const data = {
88
+ version: 1,
89
+ ...this.extraFields,
90
+ annotations: this.annotations
91
+ };
92
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
93
+ } catch {
94
+ this.isMemoryOnly = true;
95
+ }
96
+ }
97
+ };
98
+
99
+ // src/core/hash-detect.ts
100
+ var CSS_MODULES_RE = /^(.+)__([a-zA-Z0-9]{3,})$/;
101
+ var STYLED_COMPONENTS_RE = /^sc-/;
102
+ var EMOTION_RE = /^css-[a-zA-Z0-9]+$/;
103
+ var PURE_HASH_RE = /^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z0-9]{8,}$/;
104
+ function isHashedClass(className) {
105
+ if (STYLED_COMPONENTS_RE.test(className)) return true;
106
+ if (EMOTION_RE.test(className)) return true;
107
+ if (CSS_MODULES_RE.test(className)) return true;
108
+ if (PURE_HASH_RE.test(className)) return true;
109
+ return false;
110
+ }
111
+ function stripHash(className) {
112
+ const match = className.match(CSS_MODULES_RE);
113
+ if (match) {
114
+ const prefix = className.slice(0, className.lastIndexOf("__"));
115
+ return prefix;
116
+ }
117
+ return className;
118
+ }
119
+ function filterClasses(classes, classFilter) {
120
+ const result = [];
121
+ for (const cls of classes) {
122
+ if (STYLED_COMPONENTS_RE.test(cls)) continue;
123
+ if (EMOTION_RE.test(cls)) continue;
124
+ if (PURE_HASH_RE.test(cls)) continue;
125
+ let stable = stripHash(cls);
126
+ if (classFilter && !classFilter(stable)) continue;
127
+ result.push(stable);
128
+ }
129
+ return result;
130
+ }
131
+
132
+ // src/core/fingerprint.ts
133
+ var TEXT_MAX_LENGTH = 50;
134
+ function createFingerprint(el, options2) {
135
+ const dataAttr = options2?.dataAttribute ?? "data-annotate";
136
+ return {
137
+ dataAnnotate: el.getAttribute(dataAttr) ?? null,
138
+ dataTestId: el.getAttribute("data-testid") ?? el.getAttribute("data-test") ?? el.getAttribute("data-cy") ?? null,
139
+ id: getStableId(el),
140
+ tagName: el.tagName.toLowerCase(),
141
+ textContent: getTextContent(el),
142
+ role: el.getAttribute("role") ?? null,
143
+ ariaLabel: el.getAttribute("aria-label") ?? null,
144
+ stableClasses: filterClasses(
145
+ Array.from(el.classList),
146
+ options2?.classFilter
147
+ ),
148
+ domPath: buildDomPath(el),
149
+ siblingIndex: getSiblingIndex(el),
150
+ parentAnchor: findParentAnchor(el, dataAttr)
151
+ };
152
+ }
153
+ function getStableId(el) {
154
+ const id = el.id;
155
+ if (!id) return null;
156
+ if (isHashedClass(id)) return null;
157
+ return id;
158
+ }
159
+ function getTextContent(el) {
160
+ const text = el.textContent?.trim() ?? null;
161
+ if (!text) return null;
162
+ return text.length > TEXT_MAX_LENGTH ? text.slice(0, TEXT_MAX_LENGTH) : text;
163
+ }
164
+ function buildDomPath(el) {
165
+ const parts = [];
166
+ let current = el;
167
+ while (current && current !== document.body && parts.length < 5) {
168
+ parts.unshift(current.tagName.toLowerCase());
169
+ current = current.parentElement;
170
+ }
171
+ return parts.join(" > ");
172
+ }
173
+ function getSiblingIndex(el) {
174
+ const parent = el.parentElement;
175
+ if (!parent) return 0;
176
+ const children = Array.from(parent.children);
177
+ return children.indexOf(el);
178
+ }
179
+ function findParentAnchor(el, dataAttr) {
180
+ let current = el.parentElement;
181
+ while (current && current !== document.body) {
182
+ const value = current.getAttribute(dataAttr);
183
+ if (value) return value;
184
+ current = current.parentElement;
185
+ }
186
+ return null;
187
+ }
188
+
189
+ // src/core/matcher.ts
190
+ var MATCH_THRESHOLD = 50;
191
+ function levenshteinSimilarity(a, b) {
192
+ if (a === b) return 1;
193
+ if (!a.length || !b.length) return 0;
194
+ const matrix = [];
195
+ for (let i = 0; i <= a.length; i++) {
196
+ matrix[i] = [i];
197
+ }
198
+ for (let j = 0; j <= b.length; j++) {
199
+ matrix[0][j] = j;
200
+ }
201
+ for (let i = 1; i <= a.length; i++) {
202
+ for (let j = 1; j <= b.length; j++) {
203
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
204
+ matrix[i][j] = Math.min(
205
+ matrix[i - 1][j] + 1,
206
+ matrix[i][j - 1] + 1,
207
+ matrix[i - 1][j - 1] + cost
208
+ );
209
+ }
210
+ }
211
+ const distance = matrix[a.length][b.length];
212
+ return 1 - distance / Math.max(a.length, b.length);
213
+ }
214
+ function textSimilarity(a, b) {
215
+ if (!a || !b) return 0;
216
+ const na = a.trim().toLowerCase();
217
+ const nb = b.trim().toLowerCase();
218
+ if (na === nb) return 1;
219
+ if (na.includes(nb) || nb.includes(na)) return 1;
220
+ return levenshteinSimilarity(na, nb);
221
+ }
222
+ function jaccardSimilarity(a, b) {
223
+ if (!a.length && !b.length) return 0;
224
+ const setA = new Set(a);
225
+ const setB = new Set(b);
226
+ let intersection = 0;
227
+ for (const item of setA) {
228
+ if (setB.has(item)) intersection++;
229
+ }
230
+ const union = (/* @__PURE__ */ new Set([...a, ...b])).size;
231
+ return union === 0 ? 0 : intersection / union;
232
+ }
233
+ function scoreCandidate(el, fp, dataAttr) {
234
+ let score = 0;
235
+ const elAnnotate = el.getAttribute(dataAttr);
236
+ if (fp.dataAnnotate && elAnnotate === fp.dataAnnotate) {
237
+ score += 100;
238
+ }
239
+ const elText = el.textContent?.trim().slice(0, 50) ?? null;
240
+ const textSim = textSimilarity(fp.textContent, elText);
241
+ if (textSim > 0.7) {
242
+ score += textSim * 35;
243
+ }
244
+ if (fp.role && el.getAttribute("role") === fp.role && fp.ariaLabel && el.getAttribute("aria-label") === fp.ariaLabel) {
245
+ score += 30;
246
+ }
247
+ if (fp.parentAnchor) {
248
+ let parent2 = el.parentElement;
249
+ while (parent2 && parent2 !== document.body) {
250
+ if (parent2.getAttribute(dataAttr) === fp.parentAnchor) {
251
+ score += 15;
252
+ break;
253
+ }
254
+ parent2 = parent2.parentElement;
255
+ }
256
+ }
257
+ if (fp.stableClasses.length > 0) {
258
+ const elClasses = Array.from(el.classList);
259
+ const jaccard = jaccardSimilarity(fp.stableClasses, elClasses);
260
+ score += jaccard * 15;
261
+ }
262
+ if (fp.domPath) {
263
+ const elPath = buildDomPath2(el);
264
+ const pathSim = levenshteinSimilarity(fp.domPath, elPath);
265
+ score += pathSim * 15;
266
+ }
267
+ const parent = el.parentElement;
268
+ if (parent) {
269
+ const idx = Array.from(parent.children).indexOf(el);
270
+ if (idx === fp.siblingIndex) {
271
+ score += 5;
272
+ }
273
+ }
274
+ return score;
275
+ }
276
+ function buildDomPath2(el) {
277
+ const parts = [];
278
+ let current = el;
279
+ while (current && current !== document.body && parts.length < 5) {
280
+ parts.unshift(current.tagName.toLowerCase());
281
+ current = current.parentElement;
282
+ }
283
+ return parts.join(" > ");
284
+ }
285
+ function matchElement(fp, options2) {
286
+ const dataAttr = options2?.dataAttribute ?? "data-annotate";
287
+ if (fp.dataAnnotate) {
288
+ const el = document.querySelector(`[${dataAttr}="${fp.dataAnnotate}"]`);
289
+ if (el) return el;
290
+ }
291
+ if (fp.dataTestId) {
292
+ const el = document.querySelector(
293
+ `[data-testid="${fp.dataTestId}"], [data-test="${fp.dataTestId}"], [data-cy="${fp.dataTestId}"]`
294
+ );
295
+ if (el) return el;
296
+ }
297
+ if (fp.id) {
298
+ const el = document.getElementById(fp.id);
299
+ if (el) return el;
300
+ }
301
+ const candidates = document.querySelectorAll(fp.tagName);
302
+ let bestEl = null;
303
+ let bestScore = 0;
304
+ for (const candidate of candidates) {
305
+ const score = scoreCandidate(candidate, fp, dataAttr);
306
+ if (score > bestScore) {
307
+ bestScore = score;
308
+ bestEl = candidate;
309
+ }
310
+ }
311
+ return bestScore >= MATCH_THRESHOLD ? bestEl : null;
312
+ }
313
+
314
+ // src/ui/styles.ts
315
+ var STYLES_ID = "data-remarq-styles";
316
+ var CSS = `
317
+ [data-remarq-theme="light"] {
318
+ --remarq-bg: #ffffff;
319
+ --remarq-bg-secondary: #f5f5f5;
320
+ --remarq-text: #1a1a1a;
321
+ --remarq-text-secondary: #666666;
322
+ --remarq-border: #e2e8f0;
323
+ --remarq-accent: #3b82f6;
324
+ --remarq-pending: #f97316;
325
+ --remarq-resolved: #22c55e;
326
+ --remarq-overlay: rgba(59, 130, 246, 0.15);
327
+ --remarq-shadow: 0 4px 12px rgba(0,0,0,0.15);
328
+ }
329
+
330
+ [data-remarq-theme="dark"] {
331
+ --remarq-bg: #1e1e1e;
332
+ --remarq-bg-secondary: #2a2a2a;
333
+ --remarq-text: #e5e5e5;
334
+ --remarq-text-secondary: #999999;
335
+ --remarq-border: #333333;
336
+ --remarq-accent: #60a5fa;
337
+ --remarq-pending: #fb923c;
338
+ --remarq-resolved: #4ade80;
339
+ --remarq-overlay: rgba(96, 165, 250, 0.15);
340
+ --remarq-shadow: 0 4px 12px rgba(0,0,0,0.4);
341
+ }
342
+
343
+ .remarq-toolbar {
344
+ position: fixed;
345
+ bottom: 16px;
346
+ right: 16px;
347
+ z-index: 2147483647;
348
+ display: flex;
349
+ gap: 4px;
350
+ padding: 8px;
351
+ background: var(--remarq-bg);
352
+ border: 1px solid var(--remarq-border);
353
+ border-radius: 8px;
354
+ box-shadow: var(--remarq-shadow);
355
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
356
+ font-size: 13px;
357
+ color: var(--remarq-text);
358
+ }
359
+
360
+ .remarq-toolbar.remarq-minimized { padding: 4px; }
361
+
362
+ .remarq-toolbar-btn {
363
+ display: flex;
364
+ align-items: center;
365
+ justify-content: center;
366
+ width: 32px;
367
+ height: 32px;
368
+ border: none;
369
+ border-radius: 6px;
370
+ background: transparent;
371
+ color: var(--remarq-text);
372
+ cursor: pointer;
373
+ position: relative;
374
+ }
375
+
376
+ .remarq-toolbar-btn:hover { background: var(--remarq-bg-secondary); }
377
+ .remarq-toolbar-btn.remarq-active { background: var(--remarq-accent); color: #ffffff; }
378
+
379
+ .remarq-badge {
380
+ position: absolute;
381
+ top: -4px;
382
+ right: -4px;
383
+ min-width: 16px;
384
+ height: 16px;
385
+ padding: 0 4px;
386
+ border-radius: 8px;
387
+ background: var(--remarq-pending);
388
+ color: #ffffff;
389
+ font-size: 10px;
390
+ font-weight: 600;
391
+ display: flex;
392
+ align-items: center;
393
+ justify-content: center;
394
+ }
395
+
396
+ .remarq-overlay {
397
+ position: fixed;
398
+ pointer-events: none;
399
+ background: var(--remarq-overlay);
400
+ border: 2px solid var(--remarq-accent);
401
+ border-radius: 2px;
402
+ z-index: 2147483646;
403
+ transition: all 0.05s ease-out;
404
+ }
405
+
406
+ .remarq-tooltip {
407
+ position: fixed;
408
+ z-index: 2147483647;
409
+ padding: 4px 8px;
410
+ background: var(--remarq-bg);
411
+ border: 1px solid var(--remarq-border);
412
+ border-radius: 4px;
413
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
414
+ font-size: 12px;
415
+ color: var(--remarq-text);
416
+ box-shadow: var(--remarq-shadow);
417
+ pointer-events: none;
418
+ white-space: nowrap;
419
+ max-width: 300px;
420
+ overflow: hidden;
421
+ text-overflow: ellipsis;
422
+ }
423
+
424
+ .remarq-marker {
425
+ position: absolute;
426
+ z-index: 2147483645;
427
+ width: 24px;
428
+ height: 24px;
429
+ border-radius: 50%;
430
+ display: flex;
431
+ align-items: center;
432
+ justify-content: center;
433
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
434
+ font-size: 11px;
435
+ font-weight: 700;
436
+ color: #ffffff;
437
+ cursor: pointer;
438
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2);
439
+ transition: transform 0.1s ease;
440
+ }
441
+
442
+ .remarq-marker:hover { transform: scale(1.2); }
443
+ .remarq-marker[data-status="pending"] { background: var(--remarq-pending); }
444
+ .remarq-marker[data-status="resolved"] { background: var(--remarq-resolved); opacity: 0.7; }
445
+
446
+ .remarq-popup {
447
+ position: fixed;
448
+ z-index: 2147483647;
449
+ width: 300px;
450
+ background: var(--remarq-bg);
451
+ border: 1px solid var(--remarq-border);
452
+ border-radius: 8px;
453
+ box-shadow: var(--remarq-shadow);
454
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
455
+ font-size: 13px;
456
+ color: var(--remarq-text);
457
+ }
458
+
459
+ .remarq-popup-header {
460
+ padding: 8px 12px;
461
+ border-bottom: 1px solid var(--remarq-border);
462
+ font-size: 12px;
463
+ color: var(--remarq-text-secondary);
464
+ overflow: hidden;
465
+ text-overflow: ellipsis;
466
+ white-space: nowrap;
467
+ }
468
+
469
+ .remarq-popup-body { padding: 12px; }
470
+
471
+ .remarq-popup textarea {
472
+ width: 100%;
473
+ min-height: 60px;
474
+ padding: 8px;
475
+ border: 1px solid var(--remarq-border);
476
+ border-radius: 4px;
477
+ background: var(--remarq-bg-secondary);
478
+ color: var(--remarq-text);
479
+ font-family: inherit;
480
+ font-size: 13px;
481
+ resize: vertical;
482
+ box-sizing: border-box;
483
+ }
484
+
485
+ .remarq-popup textarea:focus { outline: none; border-color: var(--remarq-accent); }
486
+
487
+ .remarq-popup-actions {
488
+ display: flex;
489
+ justify-content: flex-end;
490
+ gap: 8px;
491
+ padding: 8px 12px;
492
+ border-top: 1px solid var(--remarq-border);
493
+ }
494
+
495
+ .remarq-popup-actions button {
496
+ padding: 4px 12px;
497
+ border: 1px solid var(--remarq-border);
498
+ border-radius: 4px;
499
+ background: var(--remarq-bg);
500
+ color: var(--remarq-text);
501
+ cursor: pointer;
502
+ font-size: 12px;
503
+ }
504
+
505
+ .remarq-popup-actions button.remarq-primary {
506
+ background: var(--remarq-accent);
507
+ border-color: var(--remarq-accent);
508
+ color: #ffffff;
509
+ }
510
+
511
+ .remarq-detached-panel {
512
+ position: fixed;
513
+ bottom: 60px;
514
+ right: 16px;
515
+ z-index: 2147483646;
516
+ width: 280px;
517
+ max-height: 300px;
518
+ overflow-y: auto;
519
+ background: var(--remarq-bg);
520
+ border: 1px solid var(--remarq-border);
521
+ border-radius: 8px;
522
+ box-shadow: var(--remarq-shadow);
523
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
524
+ font-size: 13px;
525
+ color: var(--remarq-text);
526
+ }
527
+
528
+ .remarq-detached-header {
529
+ padding: 8px 12px;
530
+ border-bottom: 1px solid var(--remarq-border);
531
+ font-weight: 600;
532
+ font-size: 12px;
533
+ color: var(--remarq-text-secondary);
534
+ }
535
+
536
+ .remarq-detached-item {
537
+ padding: 8px 12px;
538
+ border-bottom: 1px solid var(--remarq-border);
539
+ display: flex;
540
+ justify-content: space-between;
541
+ align-items: flex-start;
542
+ gap: 8px;
543
+ }
544
+
545
+ .remarq-detached-item:last-child { border-bottom: none; }
546
+ .remarq-detached-info { flex: 1; min-width: 0; }
547
+ .remarq-detached-comment { margin-bottom: 4px; }
548
+
549
+ .remarq-detached-element {
550
+ font-size: 11px;
551
+ color: var(--remarq-text-secondary);
552
+ overflow: hidden;
553
+ text-overflow: ellipsis;
554
+ white-space: nowrap;
555
+ }
556
+
557
+ .remarq-detached-delete {
558
+ border: none;
559
+ background: none;
560
+ color: var(--remarq-text-secondary);
561
+ cursor: pointer;
562
+ padding: 2px;
563
+ font-size: 14px;
564
+ line-height: 1;
565
+ flex-shrink: 0;
566
+ }
567
+
568
+ .remarq-detached-delete:hover { color: #ef4444; }
569
+
570
+ .remarq-export-menu {
571
+ position: absolute;
572
+ bottom: 100%;
573
+ right: 0;
574
+ margin-bottom: 4px;
575
+ background: var(--remarq-bg);
576
+ border: 1px solid var(--remarq-border);
577
+ border-radius: 6px;
578
+ box-shadow: var(--remarq-shadow);
579
+ overflow: hidden;
580
+ }
581
+
582
+ .remarq-export-menu button {
583
+ display: block;
584
+ width: 100%;
585
+ padding: 8px 16px;
586
+ border: none;
587
+ background: transparent;
588
+ color: var(--remarq-text);
589
+ cursor: pointer;
590
+ font-size: 12px;
591
+ text-align: left;
592
+ white-space: nowrap;
593
+ }
594
+
595
+ .remarq-export-menu button:hover { background: var(--remarq-bg-secondary); }
596
+ `;
597
+ function injectStyles() {
598
+ if (document.querySelector(`style[${STYLES_ID}]`)) return;
599
+ try {
600
+ const style = document.createElement("style");
601
+ style.setAttribute(STYLES_ID, "");
602
+ style.textContent = CSS;
603
+ document.head.appendChild(style);
604
+ } catch {
605
+ try {
606
+ const blob = new Blob([CSS], { type: "text/css" });
607
+ const link = document.createElement("link");
608
+ link.rel = "stylesheet";
609
+ link.href = URL.createObjectURL(blob);
610
+ link.setAttribute(STYLES_ID, "");
611
+ document.head.appendChild(link);
612
+ } catch {
613
+ console.warn("[web-remarq] Could not inject styles");
614
+ }
615
+ }
616
+ }
617
+ function removeStyles() {
618
+ const el = document.querySelector(`[${STYLES_ID}]`);
619
+ el?.remove();
620
+ }
621
+
622
+ // src/ui/theme.ts
623
+ var THEME_KEY = "remarq:theme";
624
+ var ThemeManager = class {
625
+ constructor(parent, initialTheme) {
626
+ const persisted = this.loadTheme();
627
+ this.theme = initialTheme ?? persisted ?? "light";
628
+ this.container = document.createElement("div");
629
+ this.container.setAttribute("data-remarq-theme", this.theme);
630
+ parent.appendChild(this.container);
631
+ this.persist();
632
+ }
633
+ getTheme() {
634
+ return this.theme;
635
+ }
636
+ setTheme(theme) {
637
+ this.theme = theme;
638
+ this.container.setAttribute("data-remarq-theme", theme);
639
+ this.persist();
640
+ }
641
+ toggle() {
642
+ this.setTheme(this.theme === "light" ? "dark" : "light");
643
+ }
644
+ destroy() {
645
+ this.container.remove();
646
+ }
647
+ persist() {
648
+ try {
649
+ localStorage.setItem(THEME_KEY, this.theme);
650
+ } catch {
651
+ }
652
+ }
653
+ loadTheme() {
654
+ try {
655
+ const value = localStorage.getItem(THEME_KEY);
656
+ if (value === "light" || value === "dark") return value;
657
+ } catch {
658
+ }
659
+ return null;
660
+ }
661
+ };
662
+
663
+ // src/ui/toolbar.ts
664
+ var ICONS = {
665
+ inspect: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="7" cy="7" r="4"/><line x1="10" y1="10" x2="14" y2="14"/></svg>',
666
+ export: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 2v8M4 6l4-4 4 4M2 12h12"/></svg>',
667
+ import: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 10V2M4 6l4 4 4-4M2 12h12"/></svg>',
668
+ clear: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 4l8 8M12 4l-8 8"/></svg>',
669
+ theme: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="3"/><path d="M8 1v2M8 13v2M1 8h2M13 8h2"/></svg>',
670
+ minimize: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 8h8"/></svg>'
671
+ };
672
+ var Toolbar = class {
673
+ constructor(container, callbacks) {
674
+ this.container = container;
675
+ this.callbacks = callbacks;
676
+ this.exportMenu = null;
677
+ this.minimized = false;
678
+ this.buttons = [];
679
+ this.toolbarEl = document.createElement("div");
680
+ this.toolbarEl.className = "remarq-toolbar";
681
+ this.inspectBtn = this.createButton("inspect", ICONS.inspect, () => callbacks.onInspect());
682
+ this.badgeEl = document.createElement("span");
683
+ this.badgeEl.className = "remarq-badge";
684
+ this.badgeEl.style.display = "none";
685
+ this.inspectBtn.appendChild(this.badgeEl);
686
+ const exportBtn = this.createButton("export", ICONS.export, (e) => this.toggleExportMenu(e));
687
+ this.fileInput = document.createElement("input");
688
+ this.fileInput.type = "file";
689
+ this.fileInput.accept = ".json";
690
+ this.fileInput.style.display = "none";
691
+ this.fileInput.addEventListener("change", () => {
692
+ callbacks.onImport();
693
+ this.fileInput.value = "";
694
+ });
695
+ const importBtn = this.createButton("import", ICONS.import, () => this.fileInput.click());
696
+ const clearBtn = this.createButton("clear", ICONS.clear, () => callbacks.onClear());
697
+ const themeBtn = this.createButton("theme", ICONS.theme, () => callbacks.onThemeToggle());
698
+ const minimizeBtn = this.createButton("minimize", ICONS.minimize, () => this.toggleMinimize());
699
+ this.buttons = [this.inspectBtn, exportBtn, importBtn, clearBtn, themeBtn];
700
+ this.toolbarEl.appendChild(this.inspectBtn);
701
+ this.toolbarEl.appendChild(exportBtn);
702
+ this.toolbarEl.appendChild(importBtn);
703
+ this.toolbarEl.appendChild(clearBtn);
704
+ this.toolbarEl.appendChild(themeBtn);
705
+ this.toolbarEl.appendChild(minimizeBtn);
706
+ this.toolbarEl.appendChild(this.fileInput);
707
+ container.appendChild(this.toolbarEl);
708
+ }
709
+ setInspectActive(active) {
710
+ this.inspectBtn.classList.toggle("remarq-active", active);
711
+ }
712
+ setBadgeCount(count) {
713
+ this.badgeEl.textContent = String(count);
714
+ this.badgeEl.style.display = count > 0 ? "flex" : "none";
715
+ }
716
+ getFileInput() {
717
+ return this.fileInput;
718
+ }
719
+ setMemoryWarning(show) {
720
+ this.toolbarEl.title = show ? "localStorage unavailable \u2014 annotations stored in memory only" : "";
721
+ }
722
+ destroy() {
723
+ this.closeExportMenu();
724
+ this.toolbarEl.remove();
725
+ }
726
+ createButton(action, icon, handler) {
727
+ const btn = document.createElement("button");
728
+ btn.className = "remarq-toolbar-btn";
729
+ btn.setAttribute("data-remarq-action", action);
730
+ btn.innerHTML = icon;
731
+ btn.addEventListener("click", handler);
732
+ return btn;
733
+ }
734
+ toggleMinimize() {
735
+ this.minimized = !this.minimized;
736
+ this.toolbarEl.classList.toggle("remarq-minimized", this.minimized);
737
+ for (const btn of this.buttons) {
738
+ btn.style.display = this.minimized ? "none" : "";
739
+ }
740
+ }
741
+ toggleExportMenu(e) {
742
+ if (this.exportMenu) {
743
+ this.closeExportMenu();
744
+ return;
745
+ }
746
+ this.exportMenu = document.createElement("div");
747
+ this.exportMenu.className = "remarq-export-menu";
748
+ const mdBtn = document.createElement("button");
749
+ mdBtn.textContent = "Markdown (clipboard)";
750
+ mdBtn.addEventListener("click", () => {
751
+ this.callbacks.onExportMd();
752
+ this.closeExportMenu();
753
+ });
754
+ const jsonBtn = document.createElement("button");
755
+ jsonBtn.textContent = "JSON (file)";
756
+ jsonBtn.addEventListener("click", () => {
757
+ this.callbacks.onExportJson();
758
+ this.closeExportMenu();
759
+ });
760
+ this.exportMenu.appendChild(mdBtn);
761
+ this.exportMenu.appendChild(jsonBtn);
762
+ const exportBtn = e.currentTarget;
763
+ exportBtn.style.position = "relative";
764
+ exportBtn.appendChild(this.exportMenu);
765
+ }
766
+ closeExportMenu() {
767
+ if (this.exportMenu) {
768
+ this.exportMenu.remove();
769
+ this.exportMenu = null;
770
+ }
771
+ }
772
+ };
773
+
774
+ // src/ui/overlay.ts
775
+ var Overlay = class {
776
+ constructor(container) {
777
+ this.container = container;
778
+ this.overlayEl = document.createElement("div");
779
+ this.overlayEl.className = "remarq-overlay";
780
+ this.overlayEl.style.display = "none";
781
+ this.tooltipEl = document.createElement("div");
782
+ this.tooltipEl.className = "remarq-tooltip";
783
+ this.tooltipEl.style.display = "none";
784
+ container.appendChild(this.overlayEl);
785
+ container.appendChild(this.tooltipEl);
786
+ }
787
+ show(target) {
788
+ try {
789
+ const rect = target.getBoundingClientRect();
790
+ this.overlayEl.style.display = "block";
791
+ this.overlayEl.style.top = `${rect.top}px`;
792
+ this.overlayEl.style.left = `${rect.left}px`;
793
+ this.overlayEl.style.width = `${rect.width}px`;
794
+ this.overlayEl.style.height = `${rect.height}px`;
795
+ const tag = target.tagName.toLowerCase();
796
+ const text = target.textContent?.trim().slice(0, 30) || "";
797
+ const dataAnnotate = target.getAttribute("data-annotate");
798
+ let label = `<${tag}>`;
799
+ if (text) label += ` "${text}"`;
800
+ if (dataAnnotate) label += ` [${dataAnnotate}]`;
801
+ this.tooltipEl.textContent = label;
802
+ this.tooltipEl.style.display = "block";
803
+ this.tooltipEl.style.top = `${rect.top - 28}px`;
804
+ this.tooltipEl.style.left = `${rect.left}px`;
805
+ } catch {
806
+ this.hide();
807
+ }
808
+ }
809
+ updateTooltipPosition(x, y) {
810
+ this.tooltipEl.style.left = `${x + 12}px`;
811
+ this.tooltipEl.style.top = `${y - 28}px`;
812
+ }
813
+ hide() {
814
+ this.overlayEl.style.display = "none";
815
+ this.tooltipEl.style.display = "none";
816
+ }
817
+ destroy() {
818
+ this.overlayEl.remove();
819
+ this.tooltipEl.remove();
820
+ }
821
+ };
822
+
823
+ // src/ui/popup.ts
824
+ var Popup = class {
825
+ constructor(container) {
826
+ this.container = container;
827
+ this.popupEl = null;
828
+ this.keyHandler = null;
829
+ }
830
+ show(info, position, onSubmit, onCancel) {
831
+ this.hide();
832
+ const popup2 = document.createElement("div");
833
+ popup2.className = "remarq-popup";
834
+ popup2.style.top = `${position.top}px`;
835
+ popup2.style.left = `${position.left}px`;
836
+ const header = document.createElement("div");
837
+ header.className = "remarq-popup-header";
838
+ header.textContent = `<${info.tag}>${info.text ? ` "${info.text}"` : ""}`;
839
+ const body = document.createElement("div");
840
+ body.className = "remarq-popup-body";
841
+ const textarea = document.createElement("textarea");
842
+ textarea.placeholder = "Add your comment...";
843
+ body.appendChild(textarea);
844
+ const actions = document.createElement("div");
845
+ actions.className = "remarq-popup-actions";
846
+ const cancelBtn = document.createElement("button");
847
+ cancelBtn.textContent = "Cancel";
848
+ cancelBtn.addEventListener("click", () => {
849
+ this.hide();
850
+ onCancel();
851
+ });
852
+ const addBtn = document.createElement("button");
853
+ addBtn.className = "remarq-primary";
854
+ addBtn.textContent = "Add";
855
+ addBtn.addEventListener("click", () => {
856
+ const comment = textarea.value.trim();
857
+ if (!comment) return;
858
+ this.hide();
859
+ onSubmit(comment);
860
+ });
861
+ actions.appendChild(cancelBtn);
862
+ actions.appendChild(addBtn);
863
+ popup2.appendChild(header);
864
+ popup2.appendChild(body);
865
+ popup2.appendChild(actions);
866
+ this.container.appendChild(popup2);
867
+ this.popupEl = popup2;
868
+ this.keyHandler = (e) => {
869
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
870
+ const comment = textarea.value.trim();
871
+ if (!comment) return;
872
+ this.hide();
873
+ onSubmit(comment);
874
+ }
875
+ if (e.key === "Escape") {
876
+ this.hide();
877
+ onCancel();
878
+ }
879
+ };
880
+ document.addEventListener("keydown", this.keyHandler);
881
+ requestAnimationFrame(() => textarea.focus());
882
+ }
883
+ showDetail(info, position, callbacks) {
884
+ this.hide();
885
+ const popup2 = document.createElement("div");
886
+ popup2.className = "remarq-popup";
887
+ popup2.style.top = `${position.top}px`;
888
+ popup2.style.left = `${position.left}px`;
889
+ const header = document.createElement("div");
890
+ header.className = "remarq-popup-header";
891
+ header.textContent = `<${info.tag}>${info.text ? ` "${info.text}"` : ""} [${info.status}]`;
892
+ const body = document.createElement("div");
893
+ body.className = "remarq-popup-body";
894
+ body.textContent = info.comment;
895
+ const actions = document.createElement("div");
896
+ actions.className = "remarq-popup-actions";
897
+ if (info.status === "pending") {
898
+ const resolveBtn = document.createElement("button");
899
+ resolveBtn.className = "remarq-primary";
900
+ resolveBtn.textContent = "Resolve";
901
+ resolveBtn.addEventListener("click", () => {
902
+ this.hide();
903
+ callbacks.onResolve();
904
+ });
905
+ actions.appendChild(resolveBtn);
906
+ }
907
+ const deleteBtn = document.createElement("button");
908
+ deleteBtn.textContent = "Delete";
909
+ deleteBtn.addEventListener("click", () => {
910
+ this.hide();
911
+ callbacks.onDelete();
912
+ });
913
+ actions.appendChild(deleteBtn);
914
+ const closeBtn = document.createElement("button");
915
+ closeBtn.textContent = "Close";
916
+ closeBtn.addEventListener("click", () => {
917
+ this.hide();
918
+ callbacks.onClose();
919
+ });
920
+ actions.appendChild(closeBtn);
921
+ popup2.appendChild(header);
922
+ popup2.appendChild(body);
923
+ popup2.appendChild(actions);
924
+ this.container.appendChild(popup2);
925
+ this.popupEl = popup2;
926
+ this.keyHandler = (e) => {
927
+ if (e.key === "Escape") {
928
+ this.hide();
929
+ callbacks.onClose();
930
+ }
931
+ };
932
+ document.addEventListener("keydown", this.keyHandler);
933
+ }
934
+ hide() {
935
+ if (this.popupEl) {
936
+ this.popupEl.remove();
937
+ this.popupEl = null;
938
+ }
939
+ if (this.keyHandler) {
940
+ document.removeEventListener("keydown", this.keyHandler);
941
+ this.keyHandler = null;
942
+ }
943
+ }
944
+ destroy() {
945
+ this.hide();
946
+ }
947
+ };
948
+
949
+ // src/ui/markers.ts
950
+ var MarkerManager = class {
951
+ constructor(container, onClick) {
952
+ this.container = container;
953
+ this.onClick = onClick;
954
+ this.markers = /* @__PURE__ */ new Map();
955
+ this.rafId = null;
956
+ this.counter = 0;
957
+ this.startPositionLoop();
958
+ }
959
+ addMarker(annotation, target) {
960
+ this.counter++;
961
+ const markerEl = document.createElement("div");
962
+ markerEl.className = "remarq-marker";
963
+ markerEl.setAttribute("data-status", annotation.status);
964
+ markerEl.setAttribute("data-annotation-id", annotation.id);
965
+ markerEl.textContent = String(this.counter);
966
+ markerEl.title = annotation.comment;
967
+ markerEl.addEventListener("click", () => {
968
+ this.onClick?.(annotation.id);
969
+ });
970
+ this.container.appendChild(markerEl);
971
+ this.markers.set(annotation.id, { annotation, target, markerEl });
972
+ this.updatePosition(annotation.id);
973
+ }
974
+ removeMarker(id) {
975
+ const entry = this.markers.get(id);
976
+ if (entry) {
977
+ entry.markerEl.remove();
978
+ this.markers.delete(id);
979
+ }
980
+ }
981
+ updateStatus(id, status) {
982
+ const entry = this.markers.get(id);
983
+ if (entry) {
984
+ entry.annotation.status = status;
985
+ entry.markerEl.setAttribute("data-status", status);
986
+ }
987
+ }
988
+ clear() {
989
+ for (const entry of this.markers.values()) {
990
+ entry.markerEl.remove();
991
+ }
992
+ this.markers.clear();
993
+ this.counter = 0;
994
+ }
995
+ destroy() {
996
+ if (this.rafId !== null) {
997
+ cancelAnimationFrame(this.rafId);
998
+ this.rafId = null;
999
+ }
1000
+ this.clear();
1001
+ }
1002
+ updatePosition(id) {
1003
+ const entry = this.markers.get(id);
1004
+ if (!entry) return;
1005
+ try {
1006
+ const rect = entry.target.getBoundingClientRect();
1007
+ entry.markerEl.style.top = `${window.scrollY + rect.top - 12}px`;
1008
+ entry.markerEl.style.left = `${window.scrollX + rect.right - 12}px`;
1009
+ } catch {
1010
+ }
1011
+ }
1012
+ startPositionLoop() {
1013
+ const update = () => {
1014
+ for (const id of this.markers.keys()) {
1015
+ this.updatePosition(id);
1016
+ }
1017
+ this.rafId = requestAnimationFrame(update);
1018
+ };
1019
+ this.rafId = requestAnimationFrame(update);
1020
+ }
1021
+ };
1022
+
1023
+ // src/ui/detached-panel.ts
1024
+ var DetachedPanel = class {
1025
+ constructor(container, onDelete) {
1026
+ this.container = container;
1027
+ this.onDelete = onDelete;
1028
+ this.panelEl = null;
1029
+ }
1030
+ update(annotations) {
1031
+ this.remove();
1032
+ if (annotations.length === 0) return;
1033
+ const panel = document.createElement("div");
1034
+ panel.className = "remarq-detached-panel";
1035
+ const header = document.createElement("div");
1036
+ header.className = "remarq-detached-header";
1037
+ header.textContent = `Detached (${annotations.length})`;
1038
+ panel.appendChild(header);
1039
+ for (const ann of annotations) {
1040
+ const item = document.createElement("div");
1041
+ item.className = "remarq-detached-item";
1042
+ const info = document.createElement("div");
1043
+ info.className = "remarq-detached-info";
1044
+ const comment = document.createElement("div");
1045
+ comment.className = "remarq-detached-comment";
1046
+ comment.textContent = ann.comment;
1047
+ const elDesc = document.createElement("div");
1048
+ elDesc.className = "remarq-detached-element";
1049
+ const fp = ann.fingerprint;
1050
+ let desc = `<${fp.tagName}>`;
1051
+ if (fp.textContent) desc += ` "${fp.textContent}"`;
1052
+ if (fp.dataAnnotate) desc += ` [${fp.dataAnnotate}]`;
1053
+ elDesc.textContent = desc;
1054
+ info.appendChild(comment);
1055
+ info.appendChild(elDesc);
1056
+ const deleteBtn = document.createElement("button");
1057
+ deleteBtn.className = "remarq-detached-delete";
1058
+ deleteBtn.textContent = "\xD7";
1059
+ deleteBtn.addEventListener("click", () => {
1060
+ this.onDelete?.(ann.id);
1061
+ });
1062
+ item.appendChild(info);
1063
+ item.appendChild(deleteBtn);
1064
+ panel.appendChild(item);
1065
+ }
1066
+ this.container.appendChild(panel);
1067
+ this.panelEl = panel;
1068
+ }
1069
+ destroy() {
1070
+ this.remove();
1071
+ }
1072
+ remove() {
1073
+ if (this.panelEl) {
1074
+ this.panelEl.remove();
1075
+ this.panelEl = null;
1076
+ }
1077
+ }
1078
+ };
1079
+
1080
+ // src/spa.ts
1081
+ var RouteObserver = class {
1082
+ constructor() {
1083
+ this.listeners = /* @__PURE__ */ new Set();
1084
+ this.originalPushState = history.pushState.bind(history);
1085
+ this.originalReplaceState = history.replaceState.bind(history);
1086
+ this.boundOnPopState = () => this.notify();
1087
+ this.boundOnHashChange = () => this.notify();
1088
+ this.patchHistory();
1089
+ window.addEventListener("popstate", this.boundOnPopState);
1090
+ window.addEventListener("hashchange", this.boundOnHashChange);
1091
+ }
1092
+ currentRoute() {
1093
+ return location.pathname + location.hash;
1094
+ }
1095
+ onChange(listener) {
1096
+ this.listeners.add(listener);
1097
+ return () => {
1098
+ this.listeners.delete(listener);
1099
+ };
1100
+ }
1101
+ destroy() {
1102
+ window.removeEventListener("popstate", this.boundOnPopState);
1103
+ window.removeEventListener("hashchange", this.boundOnHashChange);
1104
+ history.pushState = this.originalPushState;
1105
+ history.replaceState = this.originalReplaceState;
1106
+ this.listeners.clear();
1107
+ }
1108
+ notify() {
1109
+ const route = this.currentRoute();
1110
+ for (const listener of this.listeners) {
1111
+ listener(route);
1112
+ }
1113
+ }
1114
+ patchHistory() {
1115
+ const self = this;
1116
+ history.pushState = function(...args) {
1117
+ self.originalPushState.apply(this, args);
1118
+ self.notify();
1119
+ };
1120
+ history.replaceState = function(...args) {
1121
+ self.originalReplaceState.apply(this, args);
1122
+ self.notify();
1123
+ };
1124
+ }
1125
+ };
1126
+
1127
+ // src/web-remarq.ts
1128
+ var initialized = false;
1129
+ var options = {};
1130
+ var storage;
1131
+ var themeManager;
1132
+ var toolbar;
1133
+ var overlay;
1134
+ var popup;
1135
+ var markers;
1136
+ var detachedPanel;
1137
+ var routeObserver;
1138
+ var inspecting = false;
1139
+ var mutationObserver = null;
1140
+ var unsubRoute = null;
1141
+ function currentRoute() {
1142
+ return location.pathname + location.hash;
1143
+ }
1144
+ function generateId() {
1145
+ return `ann-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1146
+ }
1147
+ function refreshMarkers() {
1148
+ markers.clear();
1149
+ const detached = [];
1150
+ const route = currentRoute();
1151
+ const anns = storage.getByRoute(route);
1152
+ for (const ann of anns) {
1153
+ const el = matchElement(ann.fingerprint, { dataAttribute: options.dataAttribute });
1154
+ if (el) {
1155
+ markers.addMarker(ann, el);
1156
+ } else {
1157
+ detached.push(ann);
1158
+ }
1159
+ }
1160
+ detachedPanel.update(detached);
1161
+ const pendingCount = anns.filter((a) => a.status === "pending").length;
1162
+ toolbar.setBadgeCount(pendingCount);
1163
+ }
1164
+ function handleInspectClick(e) {
1165
+ if (!inspecting) return;
1166
+ e.preventDefault();
1167
+ e.stopPropagation();
1168
+ const target = e.target;
1169
+ if (!target || target.closest("[data-remarq-theme]")) return;
1170
+ overlay.hide();
1171
+ setInspecting(false);
1172
+ const rect = target.getBoundingClientRect();
1173
+ popup.show(
1174
+ {
1175
+ tag: target.tagName.toLowerCase(),
1176
+ text: target.textContent?.trim().slice(0, 30) ?? ""
1177
+ },
1178
+ {
1179
+ top: rect.bottom + 8,
1180
+ left: rect.left
1181
+ },
1182
+ (comment) => {
1183
+ const fp = createFingerprint(target, {
1184
+ classFilter: options.classFilter,
1185
+ dataAttribute: options.dataAttribute
1186
+ });
1187
+ const ann = {
1188
+ id: generateId(),
1189
+ comment,
1190
+ fingerprint: fp,
1191
+ route: currentRoute(),
1192
+ timestamp: Date.now(),
1193
+ status: "pending"
1194
+ };
1195
+ storage.add(ann);
1196
+ refreshMarkers();
1197
+ },
1198
+ () => {
1199
+ }
1200
+ );
1201
+ }
1202
+ function handleInspectHover(e) {
1203
+ if (!inspecting) return;
1204
+ const target = e.target;
1205
+ if (!target || target.closest("[data-remarq-theme]")) return;
1206
+ overlay.show(target);
1207
+ overlay.updateTooltipPosition(e.clientX, e.clientY);
1208
+ }
1209
+ function handleInspectKeydown(e) {
1210
+ if (e.key === "Escape" && inspecting) {
1211
+ setInspecting(false);
1212
+ overlay.hide();
1213
+ }
1214
+ }
1215
+ function setInspecting(value) {
1216
+ inspecting = value;
1217
+ toolbar.setInspectActive(value);
1218
+ if (!value) overlay.hide();
1219
+ }
1220
+ function handleMarkerClick(annotationId) {
1221
+ const ann = storage.getAll().find((a) => a.id === annotationId);
1222
+ if (!ann) return;
1223
+ const el = matchElement(ann.fingerprint, { dataAttribute: options.dataAttribute });
1224
+ if (!el) return;
1225
+ const rect = el.getBoundingClientRect();
1226
+ popup.showDetail(
1227
+ {
1228
+ tag: ann.fingerprint.tagName,
1229
+ text: ann.fingerprint.textContent ?? "",
1230
+ comment: ann.comment,
1231
+ status: ann.status
1232
+ },
1233
+ { top: rect.bottom + 8, left: rect.left },
1234
+ {
1235
+ onResolve: () => {
1236
+ storage.update(ann.id, { status: "resolved" });
1237
+ refreshMarkers();
1238
+ },
1239
+ onDelete: () => {
1240
+ storage.remove(ann.id);
1241
+ refreshMarkers();
1242
+ },
1243
+ onClose: () => {
1244
+ }
1245
+ }
1246
+ );
1247
+ }
1248
+ function exportMarkdown() {
1249
+ const route = currentRoute();
1250
+ const anns = storage.getByRoute(route);
1251
+ if (!anns.length) return;
1252
+ const lines = [`## Annotations \u2014 ${route} (${anns.length})`, ""];
1253
+ anns.forEach((ann, i) => {
1254
+ const fp = ann.fingerprint;
1255
+ let desc = `<${fp.tagName}>`;
1256
+ if (fp.textContent) desc += ` "${fp.textContent}"`;
1257
+ if (fp.parentAnchor) desc += ` (${fp.parentAnchor})`;
1258
+ if (fp.dataAnnotate) desc += fp.parentAnchor ? ` > ${fp.dataAnnotate}` : ` (${fp.dataAnnotate})`;
1259
+ lines.push(`${i + 1}. [${ann.status}] ${desc}: "${ann.comment}"`);
1260
+ });
1261
+ const text = lines.join("\n");
1262
+ try {
1263
+ navigator.clipboard.writeText(text);
1264
+ } catch {
1265
+ console.warn("[web-remarq] Clipboard write failed");
1266
+ }
1267
+ }
1268
+ function exportJSON() {
1269
+ const data = storage.exportJSON();
1270
+ const json = JSON.stringify(data, null, 2);
1271
+ const blob = new Blob([json], { type: "application/json" });
1272
+ const url = URL.createObjectURL(blob);
1273
+ const a = document.createElement("a");
1274
+ a.href = url;
1275
+ a.download = `remarq-annotations-${Date.now()}.json`;
1276
+ a.click();
1277
+ URL.revokeObjectURL(url);
1278
+ }
1279
+ function setupMutationObserver() {
1280
+ mutationObserver = new MutationObserver((mutations) => {
1281
+ for (const m of mutations) {
1282
+ if (m.target instanceof HTMLElement && m.target.closest("[data-remarq-theme]")) return;
1283
+ }
1284
+ refreshMarkers();
1285
+ });
1286
+ mutationObserver.observe(document.body, {
1287
+ childList: true,
1288
+ subtree: true,
1289
+ attributes: true,
1290
+ attributeFilter: ["id", "class", "data-annotate", "data-testid", "data-test", "data-cy"]
1291
+ });
1292
+ }
1293
+ var WebRemarq = {
1294
+ init(opts) {
1295
+ if (initialized) return;
1296
+ options = opts ?? {};
1297
+ try {
1298
+ injectStyles();
1299
+ storage = new AnnotationStorage();
1300
+ themeManager = new ThemeManager(document.body, options.theme);
1301
+ overlay = new Overlay(themeManager.container);
1302
+ popup = new Popup(themeManager.container);
1303
+ markers = new MarkerManager(themeManager.container, handleMarkerClick);
1304
+ detachedPanel = new DetachedPanel(themeManager.container, (id) => {
1305
+ storage.remove(id);
1306
+ refreshMarkers();
1307
+ });
1308
+ toolbar = new Toolbar(themeManager.container, {
1309
+ onInspect: () => setInspecting(!inspecting),
1310
+ onExportMd: exportMarkdown,
1311
+ onExportJson: exportJSON,
1312
+ onImport: () => {
1313
+ const file = toolbar.getFileInput().files?.[0];
1314
+ if (file) {
1315
+ WebRemarq.import(file);
1316
+ }
1317
+ },
1318
+ onClear: () => {
1319
+ storage.clearAll();
1320
+ refreshMarkers();
1321
+ },
1322
+ onThemeToggle: () => themeManager.toggle()
1323
+ });
1324
+ if (storage.isMemoryOnly) {
1325
+ toolbar.setMemoryWarning(true);
1326
+ }
1327
+ routeObserver = new RouteObserver();
1328
+ unsubRoute = routeObserver.onChange(() => refreshMarkers());
1329
+ document.addEventListener("click", handleInspectClick, true);
1330
+ document.addEventListener("mousemove", handleInspectHover);
1331
+ document.addEventListener("keydown", handleInspectKeydown);
1332
+ setupMutationObserver();
1333
+ refreshMarkers();
1334
+ initialized = true;
1335
+ } catch (err) {
1336
+ console.error("[web-remarq] Init failed:", err);
1337
+ }
1338
+ },
1339
+ destroy() {
1340
+ if (!initialized) return;
1341
+ try {
1342
+ document.removeEventListener("click", handleInspectClick, true);
1343
+ document.removeEventListener("mousemove", handleInspectHover);
1344
+ document.removeEventListener("keydown", handleInspectKeydown);
1345
+ mutationObserver?.disconnect();
1346
+ mutationObserver = null;
1347
+ unsubRoute?.();
1348
+ routeObserver?.destroy();
1349
+ markers?.destroy();
1350
+ detachedPanel?.destroy();
1351
+ popup?.destroy();
1352
+ overlay?.destroy();
1353
+ toolbar?.destroy();
1354
+ themeManager?.destroy();
1355
+ removeStyles();
1356
+ inspecting = false;
1357
+ initialized = false;
1358
+ } catch (err) {
1359
+ console.error("[web-remarq] Destroy failed:", err);
1360
+ }
1361
+ },
1362
+ setTheme(theme) {
1363
+ themeManager?.setTheme(theme);
1364
+ },
1365
+ export(format) {
1366
+ if (format === "md") exportMarkdown();
1367
+ else exportJSON();
1368
+ },
1369
+ async import(file) {
1370
+ const text = await file.text();
1371
+ const data = JSON.parse(text);
1372
+ storage.importJSON(data);
1373
+ refreshMarkers();
1374
+ const allAnns = storage.getAll();
1375
+ let matched = 0;
1376
+ let detached = 0;
1377
+ for (const ann of allAnns) {
1378
+ if (matchElement(ann.fingerprint, { dataAttribute: options.dataAttribute })) {
1379
+ matched++;
1380
+ } else {
1381
+ detached++;
1382
+ }
1383
+ }
1384
+ return { total: allAnns.length, matched, detached };
1385
+ },
1386
+ getAnnotations(route) {
1387
+ if (!storage) return [];
1388
+ return route ? storage.getByRoute(route) : storage.getAll();
1389
+ },
1390
+ clearAll() {
1391
+ storage?.clearAll();
1392
+ if (initialized) refreshMarkers();
1393
+ }
1394
+ };
1395
+ return __toCommonJS(src_exports);
1396
+ })();
1397
+ //# sourceMappingURL=web-remarq.global.global.js.map