astro-annotate 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.
package/dist/client.js ADDED
@@ -0,0 +1,920 @@
1
+ import {
2
+ API_ANNOTATIONS,
3
+ SHADOW_ROOT_ID
4
+ } from "./chunk-QG2SIJB2.js";
5
+
6
+ // src/client/styles.ts
7
+ var OVERLAY_STYLES = `
8
+ :host {
9
+ all: initial;
10
+ position: fixed;
11
+ z-index: 2147483647;
12
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
13
+ font-size: 14px;
14
+ line-height: 1.5;
15
+ color: #1a1a2e;
16
+ }
17
+
18
+ *, *::before, *::after {
19
+ box-sizing: border-box;
20
+ margin: 0;
21
+ padding: 0;
22
+ }
23
+
24
+ /* Toolbar */
25
+ .aa-toolbar {
26
+ position: fixed;
27
+ bottom: 20px;
28
+ right: 20px;
29
+ display: flex;
30
+ align-items: center;
31
+ gap: 8px;
32
+ background: #1a1a2e;
33
+ color: #fff;
34
+ padding: 8px 16px;
35
+ border-radius: 50px;
36
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
37
+ border: 1.5px solid rgba(255, 255, 255, 0.2);
38
+ cursor: pointer;
39
+ user-select: none;
40
+ transition: all 0.2s ease;
41
+ }
42
+
43
+ .aa-toolbar:hover {
44
+ background: #16213e;
45
+ transform: translateY(-1px);
46
+ box-shadow: 0 6px 24px rgba(0, 0, 0, 0.35);
47
+ border-color: rgba(255, 255, 255, 0.35);
48
+ }
49
+
50
+ .aa-toolbar.aa-active {
51
+ background: #e94560;
52
+ }
53
+
54
+ .aa-toolbar.aa-active:hover {
55
+ background: #c73e54;
56
+ }
57
+
58
+ .aa-toolbar-icon {
59
+ width: 20px;
60
+ height: 20px;
61
+ flex-shrink: 0;
62
+ }
63
+
64
+ .aa-toolbar-label {
65
+ font-size: 13px;
66
+ font-weight: 500;
67
+ white-space: nowrap;
68
+ }
69
+
70
+ .aa-badge {
71
+ background: #e94560;
72
+ color: #fff;
73
+ font-size: 11px;
74
+ font-weight: 700;
75
+ min-width: 20px;
76
+ height: 20px;
77
+ border-radius: 10px;
78
+ display: flex;
79
+ align-items: center;
80
+ justify-content: center;
81
+ padding: 0 6px;
82
+ }
83
+
84
+ .aa-toolbar.aa-active .aa-badge {
85
+ background: rgba(255, 255, 255, 0.3);
86
+ }
87
+
88
+ /* Element Highlight */
89
+ .aa-highlight {
90
+ position: fixed;
91
+ pointer-events: none;
92
+ border: 2px solid #e94560;
93
+ background: rgba(233, 69, 96, 0.08);
94
+ border-radius: 3px;
95
+ z-index: 2147483646;
96
+ transition: all 0.1s ease;
97
+ }
98
+
99
+ .aa-highlight-label {
100
+ position: absolute;
101
+ top: -26px;
102
+ left: -2px;
103
+ background: #e94560;
104
+ color: #fff;
105
+ font-size: 11px;
106
+ font-weight: 500;
107
+ padding: 2px 8px;
108
+ border-radius: 3px 3px 0 0;
109
+ white-space: nowrap;
110
+ max-width: 300px;
111
+ overflow: hidden;
112
+ text-overflow: ellipsis;
113
+ }
114
+
115
+ /* Annotation Form */
116
+ .aa-form-container {
117
+ position: fixed;
118
+ z-index: 2147483647;
119
+ background: #fff;
120
+ border-radius: 12px;
121
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
122
+ width: 340px;
123
+ overflow: hidden;
124
+ }
125
+
126
+ .aa-form-header {
127
+ background: #1a1a2e;
128
+ color: #fff;
129
+ padding: 12px 16px;
130
+ display: flex;
131
+ justify-content: space-between;
132
+ align-items: center;
133
+ }
134
+
135
+ .aa-form-header-title {
136
+ font-size: 13px;
137
+ font-weight: 600;
138
+ }
139
+
140
+ .aa-form-header-selector {
141
+ font-size: 11px;
142
+ opacity: 0.7;
143
+ font-family: 'SF Mono', Monaco, monospace;
144
+ max-width: 200px;
145
+ overflow: hidden;
146
+ text-overflow: ellipsis;
147
+ white-space: nowrap;
148
+ }
149
+
150
+ .aa-form-close {
151
+ background: none;
152
+ border: none;
153
+ color: #fff;
154
+ cursor: pointer;
155
+ font-size: 18px;
156
+ line-height: 1;
157
+ opacity: 0.7;
158
+ padding: 0 0 0 8px;
159
+ }
160
+
161
+ .aa-form-close:hover {
162
+ opacity: 1;
163
+ }
164
+
165
+ .aa-form-body {
166
+ padding: 16px;
167
+ display: flex;
168
+ flex-direction: column;
169
+ gap: 12px;
170
+ }
171
+
172
+ .aa-input, .aa-textarea {
173
+ width: 100%;
174
+ padding: 8px 12px;
175
+ border: 1px solid #e0e0e0;
176
+ border-radius: 6px;
177
+ font-size: 13px;
178
+ font-family: inherit;
179
+ outline: none;
180
+ transition: border-color 0.15s;
181
+ }
182
+
183
+ .aa-input:focus, .aa-textarea:focus {
184
+ border-color: #e94560;
185
+ }
186
+
187
+ .aa-textarea {
188
+ resize: vertical;
189
+ min-height: 80px;
190
+ }
191
+
192
+ .aa-form-actions {
193
+ display: flex;
194
+ gap: 8px;
195
+ justify-content: flex-end;
196
+ }
197
+
198
+ .aa-btn {
199
+ padding: 8px 16px;
200
+ border-radius: 6px;
201
+ font-size: 13px;
202
+ font-weight: 500;
203
+ cursor: pointer;
204
+ border: none;
205
+ transition: all 0.15s;
206
+ }
207
+
208
+ .aa-btn-primary {
209
+ background: #e94560;
210
+ color: #fff;
211
+ }
212
+
213
+ .aa-btn-primary:hover {
214
+ background: #c73e54;
215
+ }
216
+
217
+ .aa-btn-primary:disabled {
218
+ background: #ccc;
219
+ cursor: not-allowed;
220
+ }
221
+
222
+ .aa-btn-secondary {
223
+ background: #f0f0f0;
224
+ color: #333;
225
+ }
226
+
227
+ .aa-btn-secondary:hover {
228
+ background: #e0e0e0;
229
+ }
230
+
231
+ /* Pins */
232
+ .aa-pin {
233
+ position: absolute;
234
+ width: 28px;
235
+ height: 28px;
236
+ border-radius: 50% 50% 50% 0;
237
+ background: #e94560;
238
+ color: #fff;
239
+ display: flex;
240
+ align-items: center;
241
+ justify-content: center;
242
+ font-size: 12px;
243
+ font-weight: 700;
244
+ cursor: pointer;
245
+ transform: rotate(-45deg);
246
+ box-shadow: 0 2px 8px rgba(233, 69, 96, 0.4);
247
+ transition: transform 0.15s, box-shadow 0.15s;
248
+ z-index: 2147483645;
249
+ }
250
+
251
+ .aa-pin:hover {
252
+ transform: rotate(-45deg) scale(1.15);
253
+ box-shadow: 0 4px 12px rgba(233, 69, 96, 0.5);
254
+ }
255
+
256
+ .aa-pin.aa-resolved {
257
+ background: #2ecc71;
258
+ box-shadow: 0 2px 8px rgba(46, 204, 113, 0.4);
259
+ }
260
+
261
+ .aa-pin-number {
262
+ transform: rotate(45deg);
263
+ }
264
+
265
+ /* Pin Detail Popup */
266
+ .aa-pin-detail {
267
+ position: fixed;
268
+ z-index: 2147483647;
269
+ background: #fff;
270
+ border-radius: 12px;
271
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
272
+ width: 320px;
273
+ overflow: hidden;
274
+ }
275
+
276
+ .aa-pin-detail-header {
277
+ background: #1a1a2e;
278
+ color: #fff;
279
+ padding: 12px 16px;
280
+ display: flex;
281
+ justify-content: space-between;
282
+ align-items: center;
283
+ }
284
+
285
+ .aa-pin-detail-meta {
286
+ font-size: 11px;
287
+ opacity: 0.7;
288
+ }
289
+
290
+ .aa-pin-detail-body {
291
+ padding: 16px;
292
+ }
293
+
294
+ .aa-pin-detail-text {
295
+ font-size: 14px;
296
+ margin-bottom: 12px;
297
+ white-space: pre-wrap;
298
+ word-break: break-word;
299
+ }
300
+
301
+ .aa-pin-detail-info {
302
+ font-size: 12px;
303
+ color: #888;
304
+ display: flex;
305
+ flex-direction: column;
306
+ gap: 4px;
307
+ }
308
+
309
+ .aa-pin-detail-selector {
310
+ font-family: 'SF Mono', Monaco, monospace;
311
+ font-size: 11px;
312
+ color: #666;
313
+ background: #f5f5f5;
314
+ padding: 4px 8px;
315
+ border-radius: 4px;
316
+ overflow: hidden;
317
+ text-overflow: ellipsis;
318
+ white-space: nowrap;
319
+ }
320
+
321
+ .aa-pin-detail-actions {
322
+ padding: 0 16px 16px;
323
+ display: flex;
324
+ gap: 8px;
325
+ }
326
+
327
+ .aa-status-btn {
328
+ padding: 4px 12px;
329
+ border-radius: 4px;
330
+ font-size: 12px;
331
+ cursor: pointer;
332
+ border: 1px solid #e0e0e0;
333
+ background: #fff;
334
+ transition: all 0.15s;
335
+ }
336
+
337
+ .aa-status-btn:hover {
338
+ background: #f5f5f5;
339
+ }
340
+
341
+ .aa-status-btn.aa-resolve {
342
+ color: #2ecc71;
343
+ border-color: #2ecc71;
344
+ }
345
+
346
+ .aa-status-btn.aa-resolve:hover {
347
+ background: #2ecc71;
348
+ color: #fff;
349
+ }
350
+
351
+ .aa-status-btn.aa-reopen {
352
+ color: #e94560;
353
+ border-color: #e94560;
354
+ }
355
+
356
+ .aa-status-btn.aa-reopen:hover {
357
+ background: #e94560;
358
+ color: #fff;
359
+ }
360
+
361
+ /* Dark mode */
362
+ @media (prefers-color-scheme: dark) {
363
+ .aa-form-container, .aa-pin-detail {
364
+ background: #2d2d3f;
365
+ color: #e0e0e0;
366
+ }
367
+
368
+ .aa-input, .aa-textarea {
369
+ background: #1a1a2e;
370
+ border-color: #404060;
371
+ color: #e0e0e0;
372
+ }
373
+
374
+ .aa-btn-secondary {
375
+ background: #404060;
376
+ color: #e0e0e0;
377
+ }
378
+
379
+ .aa-btn-secondary:hover {
380
+ background: #505070;
381
+ }
382
+
383
+ .aa-pin-detail-selector {
384
+ background: #1a1a2e;
385
+ color: #aaa;
386
+ }
387
+
388
+ .aa-status-btn {
389
+ background: #2d2d3f;
390
+ border-color: #404060;
391
+ }
392
+
393
+ .aa-status-btn:hover {
394
+ background: #404060;
395
+ }
396
+ }
397
+ `;
398
+
399
+ // src/client/toolbar.ts
400
+ var ANNOTATE_ICON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="aa-toolbar-icon"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><line x1="9" y1="9" x2="15" y2="9"/><line x1="12" y1="6" x2="12" y2="12"/></svg>`;
401
+ var Toolbar = class {
402
+ el;
403
+ label;
404
+ badge;
405
+ active = false;
406
+ onToggle;
407
+ constructor(shadowRoot, onToggle) {
408
+ this.onToggle = onToggle;
409
+ this.el = document.createElement("div");
410
+ this.el.className = "aa-toolbar";
411
+ this.el.innerHTML = ANNOTATE_ICON;
412
+ this.label = document.createElement("span");
413
+ this.label.className = "aa-toolbar-label";
414
+ this.label.textContent = "Annotate";
415
+ this.el.appendChild(this.label);
416
+ this.badge = document.createElement("span");
417
+ this.badge.className = "aa-badge";
418
+ this.badge.textContent = "0";
419
+ this.badge.style.display = "none";
420
+ this.el.appendChild(this.badge);
421
+ this.el.addEventListener("click", () => {
422
+ this.toggle();
423
+ });
424
+ shadowRoot.appendChild(this.el);
425
+ }
426
+ toggle() {
427
+ this.active = !this.active;
428
+ this.el.classList.toggle("aa-active", this.active);
429
+ this.label.textContent = this.active ? "Stop" : "Annotate";
430
+ this.onToggle(this.active);
431
+ }
432
+ deactivate() {
433
+ if (this.active) {
434
+ this.active = false;
435
+ this.el.classList.remove("aa-active");
436
+ this.label.textContent = "Annotate";
437
+ }
438
+ }
439
+ updateCount(count) {
440
+ this.badge.textContent = String(count);
441
+ this.badge.style.display = count > 0 ? "flex" : "none";
442
+ }
443
+ destroy() {
444
+ this.el.remove();
445
+ }
446
+ };
447
+
448
+ // src/client/selector.ts
449
+ var ASTRO_CLASS_RE = /^astro-[a-zA-Z0-9]+$/;
450
+ var IGNORED_TAGS = /* @__PURE__ */ new Set(["html", "body", "head"]);
451
+ function isAstroClass(cls) {
452
+ return ASTRO_CLASS_RE.test(cls);
453
+ }
454
+ function getStableClasses(el) {
455
+ return Array.from(el.classList).filter((cls) => !isAstroClass(cls));
456
+ }
457
+ function isUnique(selector) {
458
+ try {
459
+ return document.querySelectorAll(selector).length === 1;
460
+ } catch {
461
+ return false;
462
+ }
463
+ }
464
+ function escapeSelector(value) {
465
+ return CSS.escape(value);
466
+ }
467
+ function generateSelector(target) {
468
+ if (IGNORED_TAGS.has(target.tagName.toLowerCase())) {
469
+ return target.tagName.toLowerCase();
470
+ }
471
+ if (target.id && !/^[0-9]/.test(target.id) && !target.id.includes(":")) {
472
+ const sel = `#${escapeSelector(target.id)}`;
473
+ if (isUnique(sel)) return sel;
474
+ }
475
+ const testId = target.getAttribute("data-testid");
476
+ if (testId) {
477
+ const sel = `[data-testid="${escapeSelector(testId)}"]`;
478
+ if (isUnique(sel)) return sel;
479
+ }
480
+ const tag = target.tagName.toLowerCase();
481
+ const classes = getStableClasses(target);
482
+ if (classes.length > 0) {
483
+ const classSel = classes.map((c) => `.${escapeSelector(c)}`).join("");
484
+ const sel = `${tag}${classSel}`;
485
+ if (isUnique(sel)) return sel;
486
+ }
487
+ const path = [];
488
+ let current = target;
489
+ while (current && !IGNORED_TAGS.has(current.tagName.toLowerCase())) {
490
+ let segment = current.tagName.toLowerCase();
491
+ if (current.id && !/^[0-9]/.test(current.id) && !current.id.includes(":")) {
492
+ segment = `#${escapeSelector(current.id)}`;
493
+ path.unshift(segment);
494
+ const sel2 = path.join(" > ");
495
+ if (isUnique(sel2)) return sel2;
496
+ break;
497
+ }
498
+ const cls = getStableClasses(current);
499
+ if (cls.length > 0) {
500
+ segment += cls.map((c) => `.${escapeSelector(c)}`).join("");
501
+ }
502
+ const parent = current.parentElement;
503
+ if (parent) {
504
+ const siblings = Array.from(parent.children).filter(
505
+ (s) => s.tagName === current.tagName
506
+ );
507
+ if (siblings.length > 1) {
508
+ const index = siblings.indexOf(current) + 1;
509
+ segment += `:nth-child(${index})`;
510
+ }
511
+ }
512
+ path.unshift(segment);
513
+ const sel = path.join(" > ");
514
+ if (isUnique(sel)) return sel;
515
+ current = parent;
516
+ }
517
+ return path.join(" > ") || tag;
518
+ }
519
+ function getElementLabel(el) {
520
+ const tag = el.tagName.toLowerCase();
521
+ const classes = getStableClasses(el);
522
+ const classStr = classes.length > 0 ? `.${classes.slice(0, 3).join(".")}` : "";
523
+ return `<${tag}${classStr}>`;
524
+ }
525
+ function getElementText(el) {
526
+ const text = el.textContent?.trim() || "";
527
+ return text.slice(0, 200);
528
+ }
529
+
530
+ // src/client/highlighter.ts
531
+ var Highlighter = class {
532
+ constructor(shadowRoot) {
533
+ this.shadowRoot = shadowRoot;
534
+ this.highlight = document.createElement("div");
535
+ this.highlight.className = "aa-highlight";
536
+ this.highlight.style.display = "none";
537
+ this.label = document.createElement("div");
538
+ this.label.className = "aa-highlight-label";
539
+ this.highlight.appendChild(this.label);
540
+ this.shadowRoot.appendChild(this.highlight);
541
+ }
542
+ highlight;
543
+ label;
544
+ currentTarget = null;
545
+ isOwnElement(el) {
546
+ const root = document.getElementById(SHADOW_ROOT_ID);
547
+ return root !== null && (root === el || root.contains(el));
548
+ }
549
+ onMouseMove = (e) => {
550
+ const target = e.target;
551
+ if (!target || this.isOwnElement(target)) {
552
+ this.hide();
553
+ return;
554
+ }
555
+ if (target === this.currentTarget) return;
556
+ this.currentTarget = target;
557
+ const rect = target.getBoundingClientRect();
558
+ this.highlight.style.display = "block";
559
+ this.highlight.style.top = `${rect.top}px`;
560
+ this.highlight.style.left = `${rect.left}px`;
561
+ this.highlight.style.width = `${rect.width}px`;
562
+ this.highlight.style.height = `${rect.height}px`;
563
+ this.label.textContent = getElementLabel(target);
564
+ };
565
+ hide() {
566
+ this.highlight.style.display = "none";
567
+ this.currentTarget = null;
568
+ }
569
+ getTarget() {
570
+ return this.currentTarget;
571
+ }
572
+ destroy() {
573
+ this.highlight.remove();
574
+ }
575
+ };
576
+
577
+ // src/client/form.ts
578
+ var AnnotationForm = class {
579
+ constructor(shadowRoot, onSubmitted) {
580
+ this.shadowRoot = shadowRoot;
581
+ this.container = document.createElement("div");
582
+ this.container.className = "aa-form-container";
583
+ this.container.style.display = "none";
584
+ this.shadowRoot.appendChild(this.container);
585
+ this.onSubmitted = onSubmitted;
586
+ }
587
+ container;
588
+ onSubmitted;
589
+ show(target) {
590
+ const selector = generateSelector(target);
591
+ const elementTag = target.tagName.toLowerCase();
592
+ const elementText = getElementText(target);
593
+ const rect = target.getBoundingClientRect();
594
+ let top = rect.bottom + 8;
595
+ let left = rect.left;
596
+ if (top + 300 > window.innerHeight) {
597
+ top = rect.top - 308;
598
+ }
599
+ if (left + 340 > window.innerWidth) {
600
+ left = window.innerWidth - 350;
601
+ }
602
+ if (top < 10) top = 10;
603
+ if (left < 10) left = 10;
604
+ this.container.style.top = `${top}px`;
605
+ this.container.style.left = `${left}px`;
606
+ this.container.style.display = "block";
607
+ this.container.innerHTML = `
608
+ <div class="aa-form-header">
609
+ <div>
610
+ <div class="aa-form-header-title">New Annotation</div>
611
+ <div class="aa-form-header-selector">${this.escapeHtml(selector)}</div>
612
+ </div>
613
+ <button class="aa-form-close" data-action="close">&times;</button>
614
+ </div>
615
+ <div class="aa-form-body">
616
+ <input class="aa-input" type="text" placeholder="Your name" data-field="author" value="" />
617
+ <textarea class="aa-textarea" placeholder="What should be changed?" data-field="text"></textarea>
618
+ <div class="aa-form-actions">
619
+ <button class="aa-btn aa-btn-secondary" data-action="close">Cancel</button>
620
+ <button class="aa-btn aa-btn-primary" data-action="submit">Submit</button>
621
+ </div>
622
+ </div>
623
+ `;
624
+ const textarea = this.container.querySelector('[data-field="text"]');
625
+ setTimeout(() => textarea?.focus(), 50);
626
+ this.container.querySelectorAll('[data-action="close"]').forEach((btn) => {
627
+ btn.addEventListener("click", () => this.hide());
628
+ });
629
+ const submitBtn = this.container.querySelector('[data-action="submit"]');
630
+ submitBtn.addEventListener("click", () => {
631
+ this.submit(selector, elementTag, elementText);
632
+ });
633
+ textarea.addEventListener("keydown", (e) => {
634
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
635
+ this.submit(selector, elementTag, elementText);
636
+ }
637
+ });
638
+ }
639
+ async submit(selector, elementTag, elementText) {
640
+ const text = this.container.querySelector('[data-field="text"]')?.value?.trim();
641
+ const author = this.container.querySelector('[data-field="author"]')?.value?.trim();
642
+ if (!text) return;
643
+ const submitBtn = this.container.querySelector('[data-action="submit"]');
644
+ submitBtn.disabled = true;
645
+ submitBtn.textContent = "Saving...";
646
+ const payload = {
647
+ page: window.location.pathname,
648
+ selector,
649
+ elementTag,
650
+ elementText,
651
+ viewport: { width: window.innerWidth, height: window.innerHeight },
652
+ device: window.innerWidth < 768 ? "mobile" : window.innerWidth < 1024 ? "tablet" : "desktop",
653
+ text,
654
+ author: author || "Anonymous"
655
+ };
656
+ try {
657
+ const res = await fetch(API_ANNOTATIONS, {
658
+ method: "POST",
659
+ headers: { "Content-Type": "application/json" },
660
+ body: JSON.stringify(payload)
661
+ });
662
+ if (!res.ok) throw new Error("Failed to save");
663
+ this.hide();
664
+ this.onSubmitted();
665
+ } catch (err) {
666
+ submitBtn.disabled = false;
667
+ submitBtn.textContent = "Submit";
668
+ console.error("[astro-annotate] Failed to save annotation:", err);
669
+ }
670
+ }
671
+ hide() {
672
+ this.container.style.display = "none";
673
+ this.container.innerHTML = "";
674
+ }
675
+ isVisible() {
676
+ return this.container.style.display !== "none";
677
+ }
678
+ escapeHtml(str) {
679
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
680
+ }
681
+ destroy() {
682
+ this.container.remove();
683
+ }
684
+ };
685
+
686
+ // src/client/pin.ts
687
+ var PinManager = class {
688
+ constructor(shadowRoot, onChanged) {
689
+ this.shadowRoot = shadowRoot;
690
+ this.detailPopup = document.createElement("div");
691
+ this.detailPopup.className = "aa-pin-detail";
692
+ this.detailPopup.style.display = "none";
693
+ this.shadowRoot.appendChild(this.detailPopup);
694
+ this.onChanged = onChanged;
695
+ }
696
+ pins = [];
697
+ detailPopup;
698
+ onChanged;
699
+ render(annotations) {
700
+ this.clearPins();
701
+ annotations.forEach((annotation, index) => {
702
+ const el = document.querySelector(annotation.selector);
703
+ if (!el) return;
704
+ const pin = document.createElement("div");
705
+ pin.className = `aa-pin${annotation.status === "resolved" ? " aa-resolved" : ""}`;
706
+ pin.innerHTML = `<span class="aa-pin-number">${index + 1}</span>`;
707
+ const updatePosition = () => {
708
+ const rect = el.getBoundingClientRect();
709
+ pin.style.position = "fixed";
710
+ pin.style.top = `${Math.max(0, rect.top - 10)}px`;
711
+ pin.style.left = `${Math.max(0, rect.right - 24)}px`;
712
+ };
713
+ updatePosition();
714
+ pin.addEventListener("click", (e) => {
715
+ e.stopPropagation();
716
+ this.showDetail(annotation, index, el);
717
+ });
718
+ this.shadowRoot.appendChild(pin);
719
+ this.pins.push(pin);
720
+ const observer = new IntersectionObserver(() => updatePosition(), { threshold: 0 });
721
+ observer.observe(el);
722
+ });
723
+ }
724
+ showDetail(annotation, index, el) {
725
+ const rect = el.getBoundingClientRect();
726
+ let top = rect.bottom + 8;
727
+ let left = rect.left;
728
+ if (top + 250 > window.innerHeight) {
729
+ top = rect.top - 258;
730
+ }
731
+ if (left + 320 > window.innerWidth) {
732
+ left = window.innerWidth - 330;
733
+ }
734
+ if (top < 10) top = 10;
735
+ if (left < 10) left = 10;
736
+ this.detailPopup.style.top = `${top}px`;
737
+ this.detailPopup.style.left = `${left}px`;
738
+ this.detailPopup.style.display = "block";
739
+ const date = new Date(annotation.timestamp).toLocaleString();
740
+ this.detailPopup.innerHTML = `
741
+ <div class="aa-pin-detail-header">
742
+ <div>
743
+ <div class="aa-form-header-title">#${index + 1} \u2014 ${this.escapeHtml(annotation.author)}</div>
744
+ <div class="aa-pin-detail-meta">${date} \xB7 ${annotation.device} \xB7 ${annotation.status}</div>
745
+ </div>
746
+ <button class="aa-form-close" data-action="close-detail">&times;</button>
747
+ </div>
748
+ <div class="aa-pin-detail-body">
749
+ <div class="aa-pin-detail-text">${this.escapeHtml(annotation.text)}</div>
750
+ <div class="aa-pin-detail-info">
751
+ <div class="aa-pin-detail-selector">${this.escapeHtml(annotation.selector)}</div>
752
+ </div>
753
+ </div>
754
+ <div class="aa-pin-detail-actions">
755
+ ${this.getStatusButtons(annotation)}
756
+ </div>
757
+ `;
758
+ this.detailPopup.querySelector('[data-action="close-detail"]')?.addEventListener("click", () => {
759
+ this.hideDetail();
760
+ });
761
+ this.detailPopup.querySelectorAll("[data-status]").forEach((btn) => {
762
+ btn.addEventListener("click", () => {
763
+ const status = btn.dataset.status;
764
+ this.updateStatus(annotation.id, status);
765
+ });
766
+ });
767
+ }
768
+ getStatusButtons(annotation) {
769
+ if (annotation.status === "open") {
770
+ return `
771
+ <button class="aa-status-btn aa-resolve" data-status="resolved">Done</button>
772
+ `;
773
+ }
774
+ return `<button class="aa-status-btn aa-reopen" data-status="open">Reopen</button>`;
775
+ }
776
+ async updateStatus(id, status) {
777
+ try {
778
+ const res = await fetch(`${API_ANNOTATIONS}/${id}`, {
779
+ method: "PATCH",
780
+ headers: { "Content-Type": "application/json" },
781
+ body: JSON.stringify({ status })
782
+ });
783
+ if (!res.ok) throw new Error("Failed to update");
784
+ this.hideDetail();
785
+ this.onChanged();
786
+ } catch (err) {
787
+ console.error("[astro-annotate] Failed to update annotation:", err);
788
+ }
789
+ }
790
+ hideDetail() {
791
+ this.detailPopup.style.display = "none";
792
+ }
793
+ clearPins() {
794
+ this.pins.forEach((pin) => pin.remove());
795
+ this.pins = [];
796
+ this.hideDetail();
797
+ }
798
+ escapeHtml(str) {
799
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
800
+ }
801
+ destroy() {
802
+ this.clearPins();
803
+ this.detailPopup.remove();
804
+ }
805
+ };
806
+
807
+ // src/client/overlay.ts
808
+ var Overlay = class {
809
+ host;
810
+ shadowRoot;
811
+ toolbar;
812
+ highlighter;
813
+ form;
814
+ pinManager;
815
+ active = false;
816
+ annotations = [];
817
+ constructor() {
818
+ this.host = document.createElement("div");
819
+ this.host.id = SHADOW_ROOT_ID;
820
+ document.body.appendChild(this.host);
821
+ this.shadowRoot = this.host.attachShadow({ mode: "open" });
822
+ const style = document.createElement("style");
823
+ style.textContent = OVERLAY_STYLES;
824
+ this.shadowRoot.appendChild(style);
825
+ this.toolbar = new Toolbar(this.shadowRoot, (active) => this.setActive(active));
826
+ this.highlighter = new Highlighter(this.shadowRoot);
827
+ this.form = new AnnotationForm(this.shadowRoot, () => this.onAnnotationCreated());
828
+ this.pinManager = new PinManager(this.shadowRoot, () => this.loadAnnotations());
829
+ this.loadAnnotations();
830
+ document.addEventListener("click", (e) => {
831
+ const target = e.target;
832
+ if (!this.host.contains(target)) {
833
+ this.pinManager.hideDetail();
834
+ }
835
+ });
836
+ let scrollTimeout;
837
+ window.addEventListener("scroll", () => {
838
+ clearTimeout(scrollTimeout);
839
+ scrollTimeout = setTimeout(() => this.renderPins(), 50);
840
+ }, { passive: true });
841
+ window.addEventListener("resize", () => {
842
+ this.renderPins();
843
+ }, { passive: true });
844
+ }
845
+ setActive(active) {
846
+ this.active = active;
847
+ if (active) {
848
+ this.form.hide();
849
+ this.pinManager.hideDetail();
850
+ document.addEventListener("mousemove", this.highlighter.onMouseMove);
851
+ document.addEventListener("click", this.onElementClick);
852
+ } else {
853
+ document.removeEventListener("mousemove", this.highlighter.onMouseMove);
854
+ document.removeEventListener("click", this.onElementClick);
855
+ this.highlighter.hide();
856
+ this.form.hide();
857
+ }
858
+ }
859
+ onElementClick = (e) => {
860
+ const target = e.target;
861
+ if (this.host.contains(target) || this.host === target) return;
862
+ e.preventDefault();
863
+ e.stopPropagation();
864
+ this.highlighter.hide();
865
+ document.removeEventListener("mousemove", this.highlighter.onMouseMove);
866
+ this.form.show(target);
867
+ };
868
+ async loadAnnotations() {
869
+ try {
870
+ const page = window.location.pathname;
871
+ const res = await fetch(`${API_ANNOTATIONS}?page=${encodeURIComponent(page)}`);
872
+ if (!res.ok) return;
873
+ const data = await res.json();
874
+ this.annotations = data.annotations || [];
875
+ this.toolbar.updateCount(this.annotations.filter((a) => a.status === "open").length);
876
+ this.renderPins();
877
+ } catch {
878
+ }
879
+ }
880
+ renderPins() {
881
+ this.pinManager.render(this.annotations);
882
+ }
883
+ onAnnotationCreated() {
884
+ this.toolbar.deactivate();
885
+ this.setActive(false);
886
+ this.loadAnnotations();
887
+ }
888
+ destroy() {
889
+ this.setActive(false);
890
+ this.toolbar.destroy();
891
+ this.highlighter.destroy();
892
+ this.form.destroy();
893
+ this.pinManager.destroy();
894
+ this.host.remove();
895
+ }
896
+ };
897
+
898
+ // src/client/index.ts
899
+ var overlay = null;
900
+ function init() {
901
+ const existing = document.getElementById(SHADOW_ROOT_ID);
902
+ if (existing) {
903
+ existing.remove();
904
+ overlay = null;
905
+ }
906
+ overlay = new Overlay();
907
+ }
908
+ document.addEventListener("astro:page-load", init);
909
+ if (document.readyState === "loading") {
910
+ document.addEventListener("DOMContentLoaded", () => {
911
+ if (!document.getElementById(SHADOW_ROOT_ID)) {
912
+ init();
913
+ }
914
+ });
915
+ } else {
916
+ if (!document.getElementById(SHADOW_ROOT_ID)) {
917
+ init();
918
+ }
919
+ }
920
+ //# sourceMappingURL=client.js.map