astro-annotate 0.2.1 → 0.4.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 CHANGED
@@ -21,70 +21,6 @@ var OVERLAY_STYLES = `
21
21
  padding: 0;
22
22
  }
23
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
24
  /* Element Highlight */
89
25
  .aa-highlight {
90
26
  position: fixed;
@@ -119,6 +55,7 @@ var OVERLAY_STYLES = `
119
55
  background: #fff;
120
56
  border-radius: 12px;
121
57
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
58
+ border: 1px solid rgba(0, 0, 0, 0.08);
122
59
  width: 340px;
123
60
  overflow: hidden;
124
61
  }
@@ -269,6 +206,7 @@ var OVERLAY_STYLES = `
269
206
  background: #fff;
270
207
  border-radius: 12px;
271
208
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
209
+ border: 1px solid rgba(0, 0, 0, 0.08);
272
210
  width: 320px;
273
211
  overflow: hidden;
274
212
  }
@@ -358,11 +296,288 @@ var OVERLAY_STYLES = `
358
296
  color: #fff;
359
297
  }
360
298
 
299
+ /* Annotation Panel */
300
+ .aa-panel {
301
+ position: fixed;
302
+ top: 16px;
303
+ right: 16px;
304
+ width: 360px;
305
+ height: calc(100vh - 32px);
306
+ background: #fff;
307
+ box-shadow: -4px 0 24px rgba(0, 0, 0, 0.15);
308
+ border: 1px solid rgba(0, 0, 0, 0.08);
309
+ border-radius: 12px;
310
+ overflow: hidden;
311
+ display: flex;
312
+ flex-direction: column;
313
+ z-index: 2147483647;
314
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
315
+ font-size: 14px;
316
+ color: #1a1a2e;
317
+ }
318
+
319
+ .aa-panel.aa-panel-left {
320
+ right: auto;
321
+ left: 16px;
322
+ box-shadow: 4px 0 24px rgba(0, 0, 0, 0.15);
323
+ }
324
+
325
+ .aa-panel-header {
326
+ background: #1a1a2e;
327
+ color: #fff;
328
+ padding: 14px 16px;
329
+ display: flex;
330
+ justify-content: space-between;
331
+ align-items: center;
332
+ flex-shrink: 0;
333
+ }
334
+
335
+ .aa-panel-title {
336
+ font-size: 14px;
337
+ font-weight: 600;
338
+ }
339
+
340
+ .aa-panel-header-actions {
341
+ display: flex;
342
+ gap: 4px;
343
+ }
344
+
345
+ .aa-panel-header-actions button {
346
+ background: none;
347
+ border: none;
348
+ color: #fff;
349
+ cursor: pointer;
350
+ font-size: 16px;
351
+ width: 28px;
352
+ height: 28px;
353
+ display: flex;
354
+ align-items: center;
355
+ justify-content: center;
356
+ border-radius: 4px;
357
+ opacity: 0.7;
358
+ }
359
+
360
+ .aa-panel-header-actions button:hover {
361
+ opacity: 1;
362
+ background: rgba(255, 255, 255, 0.1);
363
+ }
364
+
365
+ .aa-panel-filters {
366
+ display: flex;
367
+ background: #f8f8fa;
368
+ border-bottom: 1px solid #e0e0e0;
369
+ flex-shrink: 0;
370
+ }
371
+
372
+ .aa-panel-filters button {
373
+ flex: 1;
374
+ padding: 10px 8px;
375
+ background: none;
376
+ border: none;
377
+ border-bottom: 2px solid transparent;
378
+ cursor: pointer;
379
+ font-size: 12px;
380
+ font-weight: 500;
381
+ color: #666;
382
+ transition: color 0.15s, border-color 0.15s;
383
+ }
384
+
385
+ .aa-panel-filters button:hover {
386
+ color: #1a1a2e;
387
+ background: #f0f0f2;
388
+ }
389
+
390
+ .aa-panel-filters button.aa-active {
391
+ color: #e94560;
392
+ border-bottom-color: #e94560;
393
+ }
394
+
395
+ .aa-panel-bulk {
396
+ padding: 10px 16px;
397
+ border-bottom: 1px solid #e0e0e0;
398
+ flex-shrink: 0;
399
+ }
400
+
401
+ .aa-panel-bulk-btn {
402
+ width: 100%;
403
+ padding: 7px 12px;
404
+ border: 1px solid #2ecc71;
405
+ border-radius: 6px;
406
+ background: transparent;
407
+ color: #2ecc71;
408
+ font-size: 12px;
409
+ font-weight: 500;
410
+ cursor: pointer;
411
+ transition: all 0.15s;
412
+ }
413
+
414
+ .aa-panel-bulk-btn:hover {
415
+ background: #2ecc71;
416
+ color: #fff;
417
+ }
418
+
419
+ .aa-panel-bulk-btn[disabled] {
420
+ opacity: 0.6;
421
+ cursor: not-allowed;
422
+ }
423
+
424
+ .aa-panel-list {
425
+ flex: 1;
426
+ overflow-y: auto;
427
+ }
428
+
429
+ .aa-panel-item {
430
+ padding: 12px 16px;
431
+ border-bottom: 1px solid #f0f0f0;
432
+ transition: background 0.1s;
433
+ }
434
+
435
+ .aa-panel-item:hover {
436
+ background: #fafafa;
437
+ }
438
+
439
+ .aa-panel-item.aa-panel-item-resolved {
440
+ opacity: 0.6;
441
+ }
442
+
443
+ .aa-panel-item-header {
444
+ display: flex;
445
+ align-items: center;
446
+ gap: 8px;
447
+ margin-bottom: 4px;
448
+ font-size: 12px;
449
+ }
450
+
451
+ .aa-panel-item-number {
452
+ font-weight: 700;
453
+ color: #e94560;
454
+ }
455
+
456
+ .aa-panel-item-number.aa-panel-item-number-resolved {
457
+ color: #2ecc71;
458
+ }
459
+
460
+ .aa-panel-item-author {
461
+ font-weight: 500;
462
+ }
463
+
464
+ .aa-panel-item-time {
465
+ color: #999;
466
+ margin-left: auto;
467
+ font-size: 11px;
468
+ }
469
+
470
+ .aa-panel-item-selector {
471
+ font-family: 'SF Mono', Monaco, monospace;
472
+ font-size: 11px;
473
+ color: #666;
474
+ background: #f5f5f5;
475
+ padding: 3px 6px;
476
+ border-radius: 3px;
477
+ margin-bottom: 6px;
478
+ overflow: hidden;
479
+ text-overflow: ellipsis;
480
+ white-space: nowrap;
481
+ }
482
+
483
+ .aa-panel-item-text {
484
+ font-size: 13px;
485
+ white-space: pre-wrap;
486
+ word-break: break-word;
487
+ margin-bottom: 8px;
488
+ color: #333;
489
+ }
490
+
491
+ .aa-panel-item-actions {
492
+ display: flex;
493
+ gap: 6px;
494
+ }
495
+
496
+ .aa-panel-item-actions button {
497
+ padding: 3px 10px;
498
+ border: 1px solid #e0e0e0;
499
+ border-radius: 4px;
500
+ background: #fff;
501
+ font-size: 11px;
502
+ cursor: pointer;
503
+ transition: all 0.15s;
504
+ color: #555;
505
+ }
506
+
507
+ .aa-panel-item-actions button:hover {
508
+ background: #f5f5f5;
509
+ border-color: #ccc;
510
+ }
511
+
512
+ .aa-panel-edit-textarea {
513
+ width: 100%;
514
+ min-height: 60px;
515
+ max-height: 40vh;
516
+ padding: 8px;
517
+ border: 1px solid #e94560;
518
+ border-radius: 4px;
519
+ font-size: 13px;
520
+ font-family: inherit;
521
+ resize: vertical;
522
+ outline: none;
523
+ overflow-y: auto;
524
+ margin-bottom: 6px;
525
+ }
526
+
527
+ .aa-panel-empty {
528
+ padding: 40px 16px;
529
+ text-align: center;
530
+ color: #999;
531
+ font-size: 13px;
532
+ }
533
+
534
+ /* Floating Action Button */
535
+ .aa-panel-fab {
536
+ position: fixed;
537
+ bottom: 72px;
538
+ right: 16px;
539
+ width: 32px;
540
+ height: 32px;
541
+ border-radius: 50%;
542
+ background: #1a1a2e;
543
+ color: #fff;
544
+ cursor: pointer;
545
+ border: 1px solid rgba(255, 255, 255, 0.15);
546
+ display: flex;
547
+ align-items: center;
548
+ justify-content: center;
549
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
550
+ z-index: 2147483647;
551
+ transition: background 0.15s;
552
+ }
553
+
554
+ .aa-panel-fab:hover {
555
+ background: #2a2a40;
556
+ }
557
+
558
+ .aa-panel-fab-badge {
559
+ position: absolute;
560
+ top: -4px;
561
+ right: -4px;
562
+ background: #e94560;
563
+ color: #fff;
564
+ font-size: 9px;
565
+ font-weight: 700;
566
+ width: 16px;
567
+ height: 16px;
568
+ border-radius: 50%;
569
+ display: flex;
570
+ align-items: center;
571
+ justify-content: center;
572
+ line-height: 1;
573
+ }
574
+
361
575
  /* Dark mode */
362
576
  @media (prefers-color-scheme: dark) {
363
577
  .aa-form-container, .aa-pin-detail {
364
578
  background: #2d2d3f;
365
579
  color: #e0e0e0;
580
+ border-color: rgba(255, 255, 255, 0.1);
366
581
  }
367
582
 
368
583
  .aa-input, .aa-textarea {
@@ -393,57 +608,98 @@ var OVERLAY_STYLES = `
393
608
  .aa-status-btn:hover {
394
609
  background: #404060;
395
610
  }
396
- }
397
- `;
398
611
 
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";
612
+ .aa-panel {
613
+ background: #2d2d3f;
614
+ color: #e0e0e0;
615
+ box-shadow: -4px 0 24px rgba(0, 0, 0, 0.4);
616
+ border-color: rgba(255, 255, 255, 0.1);
617
+ }
618
+
619
+ .aa-panel.aa-panel-left {
620
+ box-shadow: 4px 0 24px rgba(0, 0, 0, 0.4);
621
+ }
622
+
623
+ .aa-panel-filters {
624
+ background: #252538;
625
+ border-bottom-color: #404060;
626
+ }
627
+
628
+ .aa-panel-filters button {
629
+ color: #aaa;
630
+ }
631
+
632
+ .aa-panel-filters button:hover {
633
+ color: #e0e0e0;
634
+ background: #353550;
635
+ }
636
+
637
+ .aa-panel-bulk {
638
+ border-bottom-color: #404060;
639
+ }
640
+
641
+ .aa-panel-bulk-btn {
642
+ color: #2ecc71;
643
+ border-color: #2ecc71;
644
+ background: transparent;
645
+ }
646
+
647
+ .aa-panel-bulk-btn:hover {
648
+ background: #2ecc71;
649
+ color: #fff;
650
+ }
651
+
652
+ .aa-panel-item {
653
+ border-bottom-color: #404060;
654
+ }
655
+
656
+ .aa-panel-item:hover {
657
+ background: #353550;
658
+ }
659
+
660
+ .aa-panel-item-time {
661
+ color: #888;
662
+ }
663
+
664
+ .aa-panel-item-selector {
665
+ background: #1a1a2e;
666
+ color: #aaa;
667
+ }
668
+
669
+ .aa-panel-item-text {
670
+ color: #ddd;
671
+ }
672
+
673
+ .aa-panel-item-actions button {
674
+ border-color: #404060;
675
+ color: #ccc;
676
+ background: #2d2d3f;
677
+ }
678
+
679
+ .aa-panel-item-actions button:hover {
680
+ background: #404060;
681
+ }
682
+
683
+ .aa-panel-edit-textarea {
684
+ background: #1a1a2e;
685
+ color: #e0e0e0;
686
+ border-color: #e94560;
687
+ }
688
+
689
+ .aa-panel-empty {
690
+ color: #888;
691
+ }
692
+
693
+ .aa-panel-fab {
694
+ background: #2d2d3f;
695
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
696
+ }
697
+
698
+ .aa-panel-fab:hover {
699
+ background: #404060;
437
700
  }
438
701
  }
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
- };
702
+ `;
447
703
 
448
704
  // src/client/selector.ts
449
705
  var ASTRO_CLASS_RE = /^astro-[a-zA-Z0-9]+$/;
@@ -575,6 +831,11 @@ var Highlighter = class {
575
831
  }
576
832
  };
577
833
 
834
+ // src/client/utils.ts
835
+ function escapeHtml(str) {
836
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
837
+ }
838
+
578
839
  // src/client/form.ts
579
840
  var AnnotationForm = class {
580
841
  constructor(shadowRoot, onSubmitted, onClosed, devMode) {
@@ -612,7 +873,7 @@ var AnnotationForm = class {
612
873
  <div class="aa-form-header">
613
874
  <div>
614
875
  <div class="aa-form-header-title">New Annotation</div>
615
- <div class="aa-form-header-selector">${this.escapeHtml(selector)}</div>
876
+ <div class="aa-form-header-selector">${escapeHtml(selector)}</div>
616
877
  </div>
617
878
  <button class="aa-form-close" data-action="close">&times;</button>
618
879
  </div>
@@ -681,9 +942,6 @@ var AnnotationForm = class {
681
942
  isVisible() {
682
943
  return this.container.style.display !== "none";
683
944
  }
684
- escapeHtml(str) {
685
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
686
- }
687
945
  destroy() {
688
946
  this.container.remove();
689
947
  }
@@ -702,7 +960,9 @@ var PinManager = class {
702
960
  pins = [];
703
961
  detailPopup;
704
962
  onChanged;
705
- render(annotations) {
963
+ panelSide = null;
964
+ render(annotations, panelSide = null) {
965
+ this.panelSide = panelSide;
706
966
  this.clearPins();
707
967
  annotations.forEach((annotation, index) => {
708
968
  const el = document.querySelector(annotation.selector);
@@ -713,8 +973,22 @@ var PinManager = class {
713
973
  const updatePosition = () => {
714
974
  const rect = el.getBoundingClientRect();
715
975
  pin.style.position = "fixed";
716
- pin.style.top = `${Math.max(0, rect.top - 10)}px`;
717
- pin.style.left = `${Math.max(0, rect.right - 24)}px`;
976
+ const pinTop = Math.max(0, rect.top - 10);
977
+ let pinLeft;
978
+ if (this.panelSide === "right") {
979
+ pinLeft = Math.max(10, rect.left - 32);
980
+ } else {
981
+ pinLeft = Math.max(10, rect.right - 24);
982
+ }
983
+ const fabLeft = window.innerWidth - 48;
984
+ const fabTop = window.innerHeight - 104;
985
+ const fabRight = window.innerWidth - 16;
986
+ const fabBottom = window.innerHeight - 72;
987
+ if (pinTop + 28 > fabTop && pinTop < fabBottom && pinLeft + 28 > fabLeft && pinLeft < fabRight) {
988
+ pinLeft = fabLeft - 32;
989
+ }
990
+ pin.style.top = `${pinTop}px`;
991
+ pin.style.left = `${pinLeft}px`;
718
992
  };
719
993
  updatePosition();
720
994
  pin.addEventListener("click", (e) => {
@@ -746,15 +1020,15 @@ var PinManager = class {
746
1020
  this.detailPopup.innerHTML = `
747
1021
  <div class="aa-pin-detail-header">
748
1022
  <div>
749
- <div class="aa-form-header-title">#${index + 1} \u2014 ${this.escapeHtml(annotation.author)}</div>
1023
+ <div class="aa-form-header-title">#${index + 1} \u2014 ${escapeHtml(annotation.author)}</div>
750
1024
  <div class="aa-pin-detail-meta">${date} \xB7 ${annotation.device} \xB7 ${annotation.status}</div>
751
1025
  </div>
752
1026
  <button class="aa-form-close" data-action="close-detail">&times;</button>
753
1027
  </div>
754
1028
  <div class="aa-pin-detail-body">
755
- <div class="aa-pin-detail-text">${this.escapeHtml(annotation.text)}</div>
1029
+ <div class="aa-pin-detail-text">${escapeHtml(annotation.text)}</div>
756
1030
  <div class="aa-pin-detail-info">
757
- <div class="aa-pin-detail-selector">${this.escapeHtml(annotation.selector)}</div>
1031
+ <div class="aa-pin-detail-selector">${escapeHtml(annotation.selector)}</div>
758
1032
  </div>
759
1033
  </div>
760
1034
  <div class="aa-pin-detail-actions">
@@ -801,26 +1075,346 @@ var PinManager = class {
801
1075
  this.pins = [];
802
1076
  this.hideDetail();
803
1077
  }
804
- escapeHtml(str) {
805
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
806
- }
807
1078
  destroy() {
808
1079
  this.clearPins();
809
1080
  this.detailPopup.remove();
810
1081
  }
811
1082
  };
812
1083
 
1084
+ // src/client/panel.ts
1085
+ var AnnotationPanel = class {
1086
+ constructor(shadowRoot, onChanged, onVisibilityChanged = () => {
1087
+ }) {
1088
+ this.shadowRoot = shadowRoot;
1089
+ this.onChanged = onChanged;
1090
+ this.onVisibilityChanged = onVisibilityChanged;
1091
+ this.container = document.createElement("div");
1092
+ this.container.className = "aa-panel";
1093
+ this.container.style.display = "none";
1094
+ this.container.addEventListener("click", this.onClick);
1095
+ this.container.addEventListener("keydown", this.onKeyDown);
1096
+ this.shadowRoot.appendChild(this.container);
1097
+ this.fab = document.createElement("button");
1098
+ this.fab.className = "aa-panel-fab";
1099
+ this.fab.addEventListener("click", () => this.toggle());
1100
+ this.shadowRoot.appendChild(this.fab);
1101
+ this.renderFab();
1102
+ }
1103
+ container;
1104
+ fab;
1105
+ visible = false;
1106
+ filter = "open";
1107
+ side = "right";
1108
+ editingId = null;
1109
+ annotations = [];
1110
+ indexMap = /* @__PURE__ */ new Map();
1111
+ onChanged;
1112
+ onVisibilityChanged;
1113
+ show() {
1114
+ this.visible = true;
1115
+ this.container.style.display = "flex";
1116
+ this.render();
1117
+ this.onVisibilityChanged();
1118
+ }
1119
+ hide() {
1120
+ this.visible = false;
1121
+ this.editingId = null;
1122
+ this.container.style.display = "none";
1123
+ this.onVisibilityChanged();
1124
+ }
1125
+ toggle() {
1126
+ if (this.visible) {
1127
+ this.hide();
1128
+ } else {
1129
+ this.show();
1130
+ }
1131
+ }
1132
+ isVisible() {
1133
+ return this.visible;
1134
+ }
1135
+ isEditing() {
1136
+ return this.editingId !== null;
1137
+ }
1138
+ getSide() {
1139
+ return this.side;
1140
+ }
1141
+ getState() {
1142
+ return { visible: this.visible, filter: this.filter, side: this.side };
1143
+ }
1144
+ restoreState(state) {
1145
+ this.filter = state.filter || "open";
1146
+ this.side = state.side || "right";
1147
+ if (state.visible) {
1148
+ this.show();
1149
+ }
1150
+ }
1151
+ update(annotations) {
1152
+ this.annotations = annotations;
1153
+ this.rebuildIndexMap();
1154
+ this.renderFab();
1155
+ if (this.visible) {
1156
+ this.render();
1157
+ }
1158
+ }
1159
+ destroy() {
1160
+ this.container.removeEventListener("click", this.onClick);
1161
+ this.container.removeEventListener("keydown", this.onKeyDown);
1162
+ this.container.remove();
1163
+ this.fab.remove();
1164
+ }
1165
+ // --- Private ---
1166
+ rebuildIndexMap() {
1167
+ this.indexMap.clear();
1168
+ this.annotations.forEach((a, i) => {
1169
+ this.indexMap.set(a.id, i + 1);
1170
+ });
1171
+ }
1172
+ getFiltered() {
1173
+ if (this.filter === "all") return this.annotations;
1174
+ return this.annotations.filter((a) => a.status === this.filter);
1175
+ }
1176
+ countByStatus(status) {
1177
+ return this.annotations.filter((a) => a.status === status).length;
1178
+ }
1179
+ render() {
1180
+ const filtered = this.getFiltered();
1181
+ const openCount = this.countByStatus("open");
1182
+ const resolvedCount = this.countByStatus("resolved");
1183
+ const totalCount = this.annotations.length;
1184
+ this.container.className = `aa-panel${this.side === "left" ? " aa-panel-left" : ""}`;
1185
+ const sideIcon = this.side === "right" ? '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><line x1="3" y1="3" x2="3" y2="13"/><polyline points="12,5 8,8 12,11"/></svg>' : '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><line x1="13" y1="3" x2="13" y2="13"/><polyline points="4,5 8,8 4,11"/></svg>';
1186
+ this.container.innerHTML = `
1187
+ <div class="aa-panel-header">
1188
+ <span class="aa-panel-title">Annotations (${totalCount})</span>
1189
+ <div class="aa-panel-header-actions">
1190
+ <button data-action="toggle-side" title="Move panel">${sideIcon}</button>
1191
+ <button data-action="close-panel" title="Close panel">&times;</button>
1192
+ </div>
1193
+ </div>
1194
+ <div class="aa-panel-filters">
1195
+ <button data-action="filter" data-filter="open" class="${this.filter === "open" ? "aa-active" : ""}">Open (${openCount})</button>
1196
+ <button data-action="filter" data-filter="resolved" class="${this.filter === "resolved" ? "aa-active" : ""}">Resolved (${resolvedCount})</button>
1197
+ <button data-action="filter" data-filter="all" class="${this.filter === "all" ? "aa-active" : ""}">All (${totalCount})</button>
1198
+ </div>
1199
+ ${this.filter === "open" && openCount > 0 ? `
1200
+ <div class="aa-panel-bulk">
1201
+ <button class="aa-panel-bulk-btn" data-action="bulk-resolve">Mark all as done</button>
1202
+ </div>
1203
+ ` : ""}
1204
+ <div class="aa-panel-list">
1205
+ ${filtered.length > 0 ? filtered.map((a) => this.renderItem(a)).join("") : '<div class="aa-panel-empty">No annotations match this filter.</div>'}
1206
+ </div>
1207
+ `;
1208
+ if (this.editingId) {
1209
+ const textarea = this.container.querySelector(".aa-panel-edit-textarea");
1210
+ if (textarea) {
1211
+ setTimeout(() => {
1212
+ textarea.focus();
1213
+ textarea.style.height = "auto";
1214
+ textarea.style.height = Math.min(textarea.scrollHeight, window.innerHeight * 0.4) + "px";
1215
+ }, 30);
1216
+ }
1217
+ }
1218
+ }
1219
+ renderItem(annotation) {
1220
+ const num = this.indexMap.get(annotation.id) ?? 0;
1221
+ const isResolved = annotation.status === "resolved";
1222
+ const isEditing = this.editingId === annotation.id;
1223
+ const numberClass = isResolved ? "aa-panel-item-number aa-panel-item-number-resolved" : "aa-panel-item-number";
1224
+ const textBlock = isEditing ? `<textarea class="aa-panel-edit-textarea" data-id="${annotation.id}">${escapeHtml(annotation.text)}</textarea>
1225
+ <div class="aa-panel-item-actions">
1226
+ <button data-action="save-edit" data-id="${annotation.id}">Save</button>
1227
+ <button data-action="cancel-edit" data-id="${annotation.id}">Cancel</button>
1228
+ </div>` : `<div class="aa-panel-item-text">${escapeHtml(annotation.text)}</div>
1229
+ <div class="aa-panel-item-actions">
1230
+ <button data-action="locate" data-id="${annotation.id}" data-selector="${escapeHtml(annotation.selector)}">Locate</button>
1231
+ <button data-action="edit-inline" data-id="${annotation.id}">Edit</button>
1232
+ ${isResolved ? `<button data-action="reopen" data-id="${annotation.id}">Reopen</button>` : `<button data-action="resolve" data-id="${annotation.id}">Done</button>`}
1233
+ </div>`;
1234
+ return `
1235
+ <div class="aa-panel-item${isResolved ? " aa-panel-item-resolved" : ""}" data-id="${annotation.id}">
1236
+ <div class="aa-panel-item-header">
1237
+ <span class="${numberClass}">#${num}</span>
1238
+ <span class="aa-panel-item-author">${escapeHtml(annotation.author)}</span>
1239
+ <span class="aa-panel-item-time">${this.formatTimeAgo(annotation.timestamp)}</span>
1240
+ </div>
1241
+ <div class="aa-panel-item-selector">${escapeHtml(annotation.selector)}</div>
1242
+ ${textBlock}
1243
+ </div>
1244
+ `;
1245
+ }
1246
+ renderFab() {
1247
+ const openCount = this.countByStatus("open");
1248
+ const badge = openCount > 0 ? `<span class="aa-panel-fab-badge">${openCount}</span>` : "";
1249
+ this.fab.innerHTML = `
1250
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
1251
+ <line x1="2" y1="4" x2="14" y2="4"/><line x1="2" y1="8" x2="14" y2="8"/><line x1="2" y1="12" x2="10" y2="12"/>
1252
+ </svg>
1253
+ ${badge}
1254
+ `;
1255
+ }
1256
+ // --- Event delegation ---
1257
+ onClick = (e) => {
1258
+ const target = e.target;
1259
+ const actionEl = target.closest("[data-action]");
1260
+ if (!actionEl) return;
1261
+ const action = actionEl.dataset.action;
1262
+ const id = actionEl.dataset.id;
1263
+ switch (action) {
1264
+ case "close-panel":
1265
+ this.hide();
1266
+ break;
1267
+ case "toggle-side":
1268
+ this.side = this.side === "right" ? "left" : "right";
1269
+ this.render();
1270
+ this.onVisibilityChanged();
1271
+ break;
1272
+ case "filter":
1273
+ this.filter = actionEl.dataset.filter || "all";
1274
+ this.editingId = null;
1275
+ this.render();
1276
+ break;
1277
+ case "bulk-resolve":
1278
+ this.bulkResolve(actionEl);
1279
+ break;
1280
+ case "locate":
1281
+ if (actionEl.dataset.selector) this.locateElement(actionEl.dataset.selector);
1282
+ break;
1283
+ case "edit-inline":
1284
+ if (id) {
1285
+ this.editingId = id;
1286
+ this.render();
1287
+ }
1288
+ break;
1289
+ case "cancel-edit":
1290
+ this.editingId = null;
1291
+ this.render();
1292
+ break;
1293
+ case "save-edit":
1294
+ if (id) this.saveEdit(id);
1295
+ break;
1296
+ case "resolve":
1297
+ if (id) this.updateStatus(id, "resolved");
1298
+ break;
1299
+ case "reopen":
1300
+ if (id) this.updateStatus(id, "open");
1301
+ break;
1302
+ }
1303
+ };
1304
+ onKeyDown = (e) => {
1305
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey) && this.editingId) {
1306
+ e.preventDefault();
1307
+ this.saveEdit(this.editingId);
1308
+ }
1309
+ };
1310
+ // --- Actions ---
1311
+ async updateStatus(id, status) {
1312
+ try {
1313
+ const res = await fetch(`${API_ANNOTATIONS}/${id}`, {
1314
+ method: "PATCH",
1315
+ headers: { "Content-Type": "application/json" },
1316
+ body: JSON.stringify({ status })
1317
+ });
1318
+ if (!res.ok) throw new Error("Failed to update");
1319
+ this.onChanged();
1320
+ } catch (err) {
1321
+ console.error("[astro-annotate] Failed to update annotation:", err);
1322
+ }
1323
+ }
1324
+ async saveEdit(id) {
1325
+ const textarea = this.container.querySelector(`.aa-panel-edit-textarea[data-id="${id}"]`);
1326
+ if (!textarea) return;
1327
+ const text = textarea.value.trim();
1328
+ if (!text) return;
1329
+ try {
1330
+ const res = await fetch(`${API_ANNOTATIONS}/${id}`, {
1331
+ method: "PATCH",
1332
+ headers: { "Content-Type": "application/json" },
1333
+ body: JSON.stringify({ text })
1334
+ });
1335
+ if (!res.ok) throw new Error("Failed to update");
1336
+ this.editingId = null;
1337
+ this.onChanged();
1338
+ } catch (err) {
1339
+ console.error("[astro-annotate] Failed to update annotation text:", err);
1340
+ }
1341
+ }
1342
+ async bulkResolve(btn) {
1343
+ const openAnnotations = this.annotations.filter((a) => a.status === "open");
1344
+ const total = openAnnotations.length;
1345
+ if (total === 0) return;
1346
+ btn.setAttribute("disabled", "");
1347
+ for (let i = 0; i < openAnnotations.length; i++) {
1348
+ btn.textContent = `Resolving ${i + 1}/${total}...`;
1349
+ try {
1350
+ const res = await fetch(`${API_ANNOTATIONS}/${openAnnotations[i].id}`, {
1351
+ method: "PATCH",
1352
+ headers: { "Content-Type": "application/json" },
1353
+ body: JSON.stringify({ status: "resolved" })
1354
+ });
1355
+ if (!res.ok) throw new Error("Failed to update");
1356
+ } catch (err) {
1357
+ console.error("[astro-annotate] Failed to resolve annotation:", err);
1358
+ }
1359
+ }
1360
+ this.onChanged();
1361
+ }
1362
+ locateElement(selector) {
1363
+ const el = document.querySelector(selector);
1364
+ if (!el) return;
1365
+ el.scrollIntoView({ behavior: "instant", block: "center" });
1366
+ const flash = document.createElement("div");
1367
+ const rect = el.getBoundingClientRect();
1368
+ Object.assign(flash.style, {
1369
+ position: "fixed",
1370
+ top: `${rect.top}px`,
1371
+ left: `${rect.left}px`,
1372
+ width: `${rect.width}px`,
1373
+ height: `${rect.height}px`,
1374
+ border: "2px solid #e94560",
1375
+ background: "rgba(233, 69, 96, 0.12)",
1376
+ borderRadius: "3px",
1377
+ pointerEvents: "none",
1378
+ zIndex: "2147483646",
1379
+ transition: "opacity 0.6s ease"
1380
+ });
1381
+ this.shadowRoot.appendChild(flash);
1382
+ setTimeout(() => {
1383
+ flash.style.opacity = "0";
1384
+ }, 700);
1385
+ setTimeout(() => {
1386
+ flash.remove();
1387
+ }, 1300);
1388
+ }
1389
+ // --- Helpers ---
1390
+ formatTimeAgo(timestamp) {
1391
+ const now = Date.now();
1392
+ const then = new Date(timestamp).getTime();
1393
+ if (isNaN(then)) return "unknown";
1394
+ const diffSec = Math.floor((now - then) / 1e3);
1395
+ if (diffSec < 60) return "just now";
1396
+ const diffMin = Math.floor(diffSec / 60);
1397
+ if (diffMin < 60) return `${diffMin}m ago`;
1398
+ const diffHr = Math.floor(diffMin / 60);
1399
+ if (diffHr < 24) return `${diffHr}h ago`;
1400
+ const diffDay = Math.floor(diffHr / 24);
1401
+ if (diffDay < 30) return `${diffDay}d ago`;
1402
+ return new Date(timestamp).toLocaleDateString();
1403
+ }
1404
+ };
1405
+
813
1406
  // src/client/overlay.ts
814
1407
  var Overlay = class {
815
1408
  host;
816
1409
  shadowRoot;
817
- toolbar;
818
1410
  highlighter;
819
1411
  form;
820
1412
  pinManager;
1413
+ panel;
821
1414
  active = false;
822
1415
  devMode = !!window.__ASTRO_ANNOTATE_DEV__;
823
1416
  annotations = [];
1417
+ lastOpenedUI = null;
824
1418
  constructor() {
825
1419
  this.host = document.createElement("div");
826
1420
  this.host.id = SHADOW_ROOT_ID;
@@ -829,7 +1423,6 @@ var Overlay = class {
829
1423
  const style = document.createElement("style");
830
1424
  style.textContent = OVERLAY_STYLES;
831
1425
  this.shadowRoot.appendChild(style);
832
- this.toolbar = new Toolbar(this.shadowRoot, (active) => this.setActive(active));
833
1426
  this.highlighter = new Highlighter(this.shadowRoot);
834
1427
  this.form = new AnnotationForm(
835
1428
  this.shadowRoot,
@@ -838,6 +1431,12 @@ var Overlay = class {
838
1431
  this.devMode
839
1432
  );
840
1433
  this.pinManager = new PinManager(this.shadowRoot, () => this.loadAnnotations());
1434
+ this.panel = new AnnotationPanel(
1435
+ this.shadowRoot,
1436
+ () => this.loadAnnotations(),
1437
+ () => this.renderPins()
1438
+ );
1439
+ window.addEventListener("aa:toggle", this.onToolbarToggle);
841
1440
  this.loadAnnotations();
842
1441
  document.addEventListener("click", (e) => {
843
1442
  const target = e.target;
@@ -869,6 +1468,9 @@ var Overlay = class {
869
1468
  this.form.hide();
870
1469
  }
871
1470
  }
1471
+ onToolbarToggle = ((e) => {
1472
+ this.setActive(e.detail.active);
1473
+ });
872
1474
  onElementClick = (e) => {
873
1475
  const target = e.target;
874
1476
  if (this.host.contains(target) || this.host === target) return;
@@ -878,23 +1480,47 @@ var Overlay = class {
878
1480
  this.highlighter.hide();
879
1481
  document.removeEventListener("mousemove", this.highlighter.onMouseMove);
880
1482
  this.form.show(target);
1483
+ this.lastOpenedUI = "form";
881
1484
  };
882
1485
  onKeyDown = (e) => {
883
1486
  const active = document.activeElement;
884
1487
  const isExternalInput = active && (active.tagName === "INPUT" || active.tagName === "TEXTAREA" || active.isContentEditable) && active !== this.host && !this.host.contains(active);
885
1488
  if (e.altKey && e.code === "KeyC" && !isExternalInput) {
886
1489
  e.preventDefault();
887
- this.toolbar.toggle();
1490
+ const newActive = !this.active;
1491
+ this.setActive(newActive);
1492
+ window.dispatchEvent(new CustomEvent("aa:state-changed", { detail: { active: newActive } }));
1493
+ return;
1494
+ }
1495
+ if (e.altKey && e.code === "KeyL" && !isExternalInput) {
1496
+ if (this.panel.isEditing()) return;
1497
+ e.preventDefault();
1498
+ this.panel.toggle();
1499
+ this.lastOpenedUI = this.panel.isVisible() ? "panel" : null;
888
1500
  return;
889
1501
  }
890
1502
  if (e.key === "Escape") {
1503
+ if (this.lastOpenedUI === "form" && this.form.isVisible()) {
1504
+ this.form.hide();
1505
+ this.lastOpenedUI = this.panel.isVisible() ? "panel" : null;
1506
+ return;
1507
+ }
1508
+ if (this.lastOpenedUI === "panel" && this.panel.isVisible()) {
1509
+ this.panel.hide();
1510
+ this.lastOpenedUI = this.form.isVisible() ? "form" : null;
1511
+ return;
1512
+ }
1513
+ if (this.panel.isVisible()) {
1514
+ this.panel.hide();
1515
+ return;
1516
+ }
891
1517
  if (this.form.isVisible()) {
892
1518
  this.form.hide();
893
1519
  return;
894
1520
  }
895
1521
  if (this.active) {
896
- this.toolbar.deactivate();
897
1522
  this.setActive(false);
1523
+ window.dispatchEvent(new CustomEvent("aa:state-changed", { detail: { active: false } }));
898
1524
  return;
899
1525
  }
900
1526
  }
@@ -906,13 +1532,16 @@ var Overlay = class {
906
1532
  if (!res.ok) return;
907
1533
  const data = await res.json();
908
1534
  this.annotations = data.annotations || [];
909
- this.toolbar.updateCount(this.annotations.filter((a) => a.status === "open").length);
1535
+ const openCount = this.annotations.filter((a) => a.status === "open").length;
1536
+ window.dispatchEvent(new CustomEvent("aa:count", { detail: { count: openCount } }));
910
1537
  this.renderPins();
1538
+ this.panel.update(this.annotations);
911
1539
  } catch {
912
1540
  }
913
1541
  }
914
1542
  renderPins() {
915
- this.pinManager.render(this.annotations);
1543
+ const panelSide = this.panel.isVisible() ? this.panel.getSide() : null;
1544
+ this.pinManager.render(this.annotations, panelSide);
916
1545
  }
917
1546
  onAnnotationCreated() {
918
1547
  this.loadAnnotations();
@@ -922,26 +1551,53 @@ var Overlay = class {
922
1551
  document.addEventListener("mousemove", this.highlighter.onMouseMove);
923
1552
  }
924
1553
  }
1554
+ getPanelState() {
1555
+ return this.panel.getState();
1556
+ }
1557
+ restorePanelState(state) {
1558
+ this.panel.restoreState(state);
1559
+ }
925
1560
  destroy() {
926
1561
  document.removeEventListener("keydown", this.onKeyDown);
1562
+ window.removeEventListener("aa:toggle", this.onToolbarToggle);
1563
+ if (this.active) {
1564
+ window.dispatchEvent(new CustomEvent("aa:state-changed", { detail: { active: false } }));
1565
+ }
927
1566
  this.setActive(false);
928
- this.toolbar.destroy();
929
1567
  this.highlighter.destroy();
930
1568
  this.form.destroy();
931
1569
  this.pinManager.destroy();
1570
+ this.panel.destroy();
932
1571
  this.host.remove();
933
1572
  }
934
1573
  };
935
1574
 
936
1575
  // src/client/index.ts
937
1576
  var overlay = null;
1577
+ var PANEL_STATE_KEY = "aa-panel-state";
1578
+ function savePanelState(state) {
1579
+ sessionStorage.setItem(PANEL_STATE_KEY, JSON.stringify(state));
1580
+ }
938
1581
  function init() {
939
1582
  if (overlay) {
1583
+ savePanelState(overlay.getPanelState());
940
1584
  overlay.destroy();
941
1585
  overlay = null;
942
1586
  }
943
1587
  overlay = new Overlay();
1588
+ const saved = sessionStorage.getItem(PANEL_STATE_KEY);
1589
+ if (saved) {
1590
+ try {
1591
+ overlay.restorePanelState(JSON.parse(saved));
1592
+ } catch {
1593
+ }
1594
+ }
944
1595
  }
1596
+ window.addEventListener("beforeunload", () => {
1597
+ if (overlay) {
1598
+ savePanelState(overlay.getPanelState());
1599
+ }
1600
+ });
945
1601
  document.addEventListener("astro:page-load", init);
946
1602
  if (document.readyState === "loading") {
947
1603
  document.addEventListener("DOMContentLoaded", () => {