web-remarq 0.5.0 → 0.7.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/index.js CHANGED
@@ -65,79 +65,151 @@ function destroyViewportListener() {
65
65
  }
66
66
 
67
67
  // src/core/storage.ts
68
- var STORAGE_KEY = "remarq:annotations";
68
+ function migrateAnnotation(legacy) {
69
+ const rawStatus = legacy.status;
70
+ const status = rawStatus === "resolved" ? "verified" : rawStatus;
71
+ if (Array.isArray(legacy.lifecycle) && legacy.lifecycle.length > 0) {
72
+ return __spreadProps(__spreadValues({}, legacy), { status, lifecycle: legacy.lifecycle });
73
+ }
74
+ const createdTs = typeof legacy.timestamp === "number" ? legacy.timestamp : Date.now();
75
+ const lifecycle = [
76
+ { type: "created", actor: "designer", timestamp: createdTs }
77
+ ];
78
+ if (rawStatus === "resolved") {
79
+ lifecycle.push({ type: "migrated", actor: null, timestamp: Date.now() });
80
+ }
81
+ return __spreadProps(__spreadValues({}, legacy), { status, lifecycle });
82
+ }
69
83
  var AnnotationStorage = class {
70
- constructor() {
71
- this.annotations = [];
72
- this.extraFields = {};
73
- this.isMemoryOnly = false;
74
- this.load();
84
+ constructor(adapter) {
85
+ this.adapter = adapter;
86
+ this.cache = [];
87
+ this.ready = this.init();
88
+ }
89
+ get isMemoryOnly() {
90
+ var _a3;
91
+ return (_a3 = this.adapter.isMemoryOnly) != null ? _a3 : false;
75
92
  }
76
93
  getAll() {
77
- return [...this.annotations];
94
+ return [...this.cache];
78
95
  }
79
96
  getByRoute(route) {
80
- return this.annotations.filter((a) => a.route === route);
97
+ return this.cache.filter((a) => a.route === route);
81
98
  }
82
- add(annotation) {
83
- this.annotations.push(annotation);
84
- this.save();
99
+ getById(id) {
100
+ return this.cache.find((a) => a.id === id);
85
101
  }
86
- remove(id) {
87
- this.annotations = this.annotations.filter((a) => a.id !== id);
88
- this.save();
102
+ async add(annotation) {
103
+ this.cache.push(annotation);
104
+ await this.adapter.save(annotation);
89
105
  }
90
- update(id, changes) {
91
- const idx = this.annotations.findIndex((a) => a.id === id);
92
- if (idx !== -1) {
93
- this.annotations[idx] = __spreadValues(__spreadValues({}, this.annotations[idx]), changes);
94
- this.save();
95
- }
106
+ async remove(id) {
107
+ this.cache = this.cache.filter((a) => a.id !== id);
108
+ await this.adapter.remove(id);
96
109
  }
97
- clearAll() {
98
- this.annotations = [];
99
- this.save();
110
+ async update(id, changes) {
111
+ const idx = this.cache.findIndex((a) => a.id === id);
112
+ if (idx === -1) return;
113
+ const updated = __spreadValues(__spreadValues({}, this.cache[idx]), changes);
114
+ this.cache[idx] = updated;
115
+ await this.adapter.save(updated);
116
+ }
117
+ async clearAll() {
118
+ this.cache = [];
119
+ await this.adapter.clear();
100
120
  }
101
121
  exportJSON() {
102
122
  return {
103
123
  version: 1,
104
- annotations: [...this.annotations]
124
+ annotations: [...this.cache]
105
125
  };
106
126
  }
107
- importJSON(data) {
108
- this.annotations = [...data.annotations];
127
+ async importJSON(data) {
128
+ this.cache = data.annotations.map(migrateAnnotation);
109
129
  this.migrateViewportBuckets();
110
- this.save();
130
+ await this.adapter.clear();
131
+ for (const ann of this.cache) {
132
+ await this.adapter.save(ann);
133
+ }
134
+ }
135
+ async init() {
136
+ const data = await this.adapter.load();
137
+ if (data) {
138
+ this.cache = data.annotations.map(migrateAnnotation);
139
+ this.migrateViewportBuckets();
140
+ }
111
141
  }
112
142
  migrateViewportBuckets() {
113
- for (const ann of this.annotations) {
143
+ for (const ann of this.cache) {
114
144
  if (ann.viewportBucket == null && ann.viewport) {
115
145
  const width = parseInt(ann.viewport.split("x")[0], 10);
116
146
  ann.viewportBucket = toBucket(width);
117
147
  }
118
148
  }
119
149
  }
120
- load() {
150
+ };
151
+
152
+ // src/core/local-storage-adapter.ts
153
+ var STORAGE_KEY = "remarq:annotations";
154
+ var LocalStorageAdapter = class {
155
+ constructor() {
156
+ this.isMemoryOnly = false;
157
+ this.extraFields = {};
158
+ this.memoryStore = null;
159
+ }
160
+ async load() {
161
+ if (this.isMemoryOnly) return this.memoryStore;
121
162
  try {
122
163
  const raw = localStorage.getItem(STORAGE_KEY);
123
- if (raw) {
124
- const parsed = JSON.parse(raw);
125
- const _a3 = parsed, { version, annotations } = _a3, rest = __objRest(_a3, ["version", "annotations"]);
126
- this.annotations = annotations != null ? annotations : [];
127
- this.extraFields = rest;
128
- this.migrateViewportBuckets();
129
- }
164
+ if (!raw) return null;
165
+ const parsed = JSON.parse(raw);
166
+ const _a3 = parsed, { version, annotations } = _a3, rest = __objRest(_a3, ["version", "annotations"]);
167
+ this.extraFields = rest;
168
+ const store = {
169
+ version: 1,
170
+ annotations: Array.isArray(annotations) ? annotations : []
171
+ };
172
+ this.memoryStore = store;
173
+ return store;
130
174
  } catch (e) {
131
175
  this.isMemoryOnly = true;
176
+ return this.memoryStore;
177
+ }
178
+ }
179
+ async save(annotation) {
180
+ const store = await this.ensureStore();
181
+ const idx = store.annotations.findIndex((a) => a.id === annotation.id);
182
+ if (idx === -1) {
183
+ store.annotations.push(annotation);
184
+ } else {
185
+ store.annotations[idx] = annotation;
132
186
  }
187
+ this.persist(store);
188
+ }
189
+ async remove(id) {
190
+ const store = await this.ensureStore();
191
+ store.annotations = store.annotations.filter((a) => a.id !== id);
192
+ this.persist(store);
193
+ }
194
+ async clear() {
195
+ const store = await this.ensureStore();
196
+ store.annotations = [];
197
+ this.persist(store);
133
198
  }
134
- save() {
199
+ async ensureStore() {
200
+ if (this.memoryStore) return this.memoryStore;
201
+ const loaded = await this.load();
202
+ if (loaded) return loaded;
203
+ this.memoryStore = { version: 1, annotations: [] };
204
+ return this.memoryStore;
205
+ }
206
+ persist(store) {
135
207
  if (this.isMemoryOnly) return;
136
208
  try {
137
209
  const data = __spreadProps(__spreadValues({
138
210
  version: 1
139
211
  }, this.extraFields), {
140
- annotations: this.annotations
212
+ annotations: store.annotations
141
213
  });
142
214
  localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
143
215
  } catch (e) {
@@ -533,7 +605,16 @@ function generateAgentExport(annotations, viewportBucket) {
533
605
  status: ann.status,
534
606
  timestamp: ann.timestamp,
535
607
  source: resolveSource(ann.fingerprint),
536
- searchHints: buildSearchHints(ann.fingerprint)
608
+ searchHints: buildSearchHints(ann.fingerprint),
609
+ lifecycle: ann.lifecycle.map((ev) => {
610
+ const out = {
611
+ type: ev.type,
612
+ actor: ev.actor,
613
+ timestamp: ev.timestamp
614
+ };
615
+ if (ev.reason !== void 0) out.reason = ev.reason;
616
+ return out;
617
+ })
537
618
  }));
538
619
  return {
539
620
  version: 1,
@@ -543,6 +624,67 @@ function generateAgentExport(annotations, viewportBucket) {
543
624
  };
544
625
  }
545
626
 
627
+ // src/core/lifecycle.ts
628
+ var InvalidTransitionError = class extends Error {
629
+ constructor(from, action) {
630
+ super(`Cannot ${action} from status "${from}"`);
631
+ this.name = "InvalidTransitionError";
632
+ }
633
+ };
634
+ var ACTION_TO_EVENT = {
635
+ acknowledge: "acknowledged",
636
+ claimFix: "fix_claimed",
637
+ verify: "verified",
638
+ reject: "rejected",
639
+ dismiss: "dismissed",
640
+ reopen: "reopened"
641
+ };
642
+ var DEFAULT_ACTOR_BY_EVENT = {
643
+ created: "designer",
644
+ acknowledged: "developer",
645
+ fix_claimed: "agent",
646
+ verified: "developer",
647
+ rejected: "developer",
648
+ dismissed: "developer",
649
+ reopened: "developer",
650
+ migrated: null
651
+ };
652
+ function createEvent(type, opts = {}) {
653
+ var _a3, _b;
654
+ const event = {
655
+ type,
656
+ actor: (_a3 = opts.actor) != null ? _a3 : DEFAULT_ACTOR_BY_EVENT[type],
657
+ timestamp: (_b = opts.timestamp) != null ? _b : Date.now()
658
+ };
659
+ if (opts.actorName !== void 0) event.actorName = opts.actorName;
660
+ if (opts.reason !== void 0) event.reason = opts.reason;
661
+ return event;
662
+ }
663
+ function nextStatus(from, action) {
664
+ switch (action) {
665
+ case "acknowledge":
666
+ return from === "pending" ? "in_progress" : null;
667
+ case "claimFix":
668
+ return from === "pending" || from === "in_progress" ? "fixed_unverified" : null;
669
+ case "verify":
670
+ return from === "fixed_unverified" || from === "in_progress" ? "verified" : null;
671
+ case "reject":
672
+ return from === "fixed_unverified" ? "pending" : null;
673
+ case "dismiss":
674
+ return from === "pending" || from === "in_progress" || from === "fixed_unverified" ? "dismissed" : null;
675
+ case "reopen":
676
+ return from === "dismissed" || from === "verified" ? "pending" : null;
677
+ }
678
+ }
679
+ function transition(annotation, action, opts = {}) {
680
+ const next = nextStatus(annotation.status, action);
681
+ if (next === null) {
682
+ throw new InvalidTransitionError(annotation.status, action);
683
+ }
684
+ const event = createEvent(ACTION_TO_EVENT[action], opts);
685
+ return { status: next, event };
686
+ }
687
+
546
688
  // src/ui/styles.ts
547
689
  var STYLES_ID = "data-remarq-styles";
548
690
  var CSS = `
@@ -557,6 +699,11 @@ var CSS = `
557
699
  --remarq-resolved: #22c55e;
558
700
  --remarq-overlay: rgba(59, 130, 246, 0.15);
559
701
  --remarq-shadow: 0 4px 12px rgba(0,0,0,0.15);
702
+ --remarq-status-pending: #f97316;
703
+ --remarq-status-in-progress: #eab308;
704
+ --remarq-status-fixed-unverified: #3b82f6;
705
+ --remarq-status-verified: #22c55e;
706
+ --remarq-status-dismissed: #6b7280;
560
707
  }
561
708
 
562
709
  [data-remarq-theme="dark"] {
@@ -570,6 +717,11 @@ var CSS = `
570
717
  --remarq-resolved: #4ade80;
571
718
  --remarq-overlay: rgba(96, 165, 250, 0.15);
572
719
  --remarq-shadow: 0 4px 12px rgba(0,0,0,0.4);
720
+ --remarq-status-pending: #fb923c;
721
+ --remarq-status-in-progress: #facc15;
722
+ --remarq-status-fixed-unverified: #60a5fa;
723
+ --remarq-status-verified: #4ade80;
724
+ --remarq-status-dismissed: #9ca3af;
573
725
  }
574
726
 
575
727
  .remarq-toolbar {
@@ -613,19 +765,29 @@ var CSS = `
613
765
 
614
766
  .remarq-badge {
615
767
  position: absolute;
616
- top: -4px;
617
- right: -4px;
618
- min-width: 16px;
619
- height: 16px;
620
- padding: 0 4px;
621
- border-radius: 8px;
768
+ top: -6px;
769
+ right: -6px;
770
+ width: 18px;
771
+ height: 18px;
772
+ padding: 0;
773
+ border-radius: 50%;
622
774
  background: var(--remarq-pending);
623
775
  color: #ffffff;
624
776
  font-size: 10px;
625
777
  font-weight: 600;
778
+ line-height: 1;
626
779
  display: flex;
627
780
  align-items: center;
628
781
  justify-content: center;
782
+ box-sizing: border-box;
783
+ }
784
+
785
+ .remarq-toolbar-badge--verification {
786
+ top: auto;
787
+ bottom: -6px;
788
+ right: -6px;
789
+ background: var(--remarq-status-fixed-unverified);
790
+ cursor: pointer;
629
791
  }
630
792
 
631
793
  .remarq-overlay {
@@ -675,8 +837,11 @@ var CSS = `
675
837
  }
676
838
 
677
839
  .remarq-marker:hover { transform: scale(1.2); }
678
- .remarq-marker[data-status="pending"] { background: var(--remarq-pending); }
679
- .remarq-marker[data-status="resolved"] { background: var(--remarq-resolved); opacity: 0.7; }
840
+ .remarq-marker--pending { background: var(--remarq-status-pending); }
841
+ .remarq-marker--in-progress { background: var(--remarq-status-in-progress); }
842
+ .remarq-marker--fixed-unverified { background: var(--remarq-status-fixed-unverified); }
843
+ .remarq-marker--verified { background: var(--remarq-status-verified); opacity: 0.7; }
844
+ .remarq-marker--dismissed { background: var(--remarq-status-dismissed); opacity: 0.5; }
680
845
 
681
846
  .remarq-popup {
682
847
  position: absolute;
@@ -721,14 +886,30 @@ var CSS = `
721
886
 
722
887
  .remarq-popup-actions {
723
888
  display: flex;
724
- justify-content: flex-end;
725
- gap: 8px;
889
+ flex-direction: column;
890
+ gap: 6px;
726
891
  padding: 8px 12px;
727
892
  border-top: 1px solid var(--remarq-border);
728
893
  }
729
894
 
895
+ .remarq-popup-actions-row {
896
+ display: flex;
897
+ flex-wrap: wrap;
898
+ gap: 6px;
899
+ }
900
+
901
+ .remarq-popup-actions-row--transitions {
902
+ justify-content: flex-start;
903
+ }
904
+
905
+ .remarq-popup-actions-row--utility {
906
+ justify-content: flex-end;
907
+ padding-top: 6px;
908
+ border-top: 1px dashed var(--remarq-border);
909
+ }
910
+
730
911
  .remarq-popup-actions button {
731
- padding: 4px 12px;
912
+ padding: 5px 12px;
732
913
  border: 1px solid var(--remarq-border);
733
914
  border-radius: 4px;
734
915
  background: var(--remarq-bg);
@@ -743,6 +924,19 @@ var CSS = `
743
924
  color: #ffffff;
744
925
  }
745
926
 
927
+ .remarq-popup-utility-btn {
928
+ font-size: 11px !important;
929
+ padding: 3px 10px !important;
930
+ color: var(--remarq-text-secondary) !important;
931
+ background: transparent !important;
932
+ border-color: transparent !important;
933
+ }
934
+
935
+ .remarq-popup-utility-btn:hover {
936
+ background: var(--remarq-bg-secondary) !important;
937
+ color: var(--remarq-text) !important;
938
+ }
939
+
746
940
  .remarq-detached-panel {
747
941
  position: fixed;
748
942
  z-index: 2147483646;
@@ -1001,6 +1195,51 @@ var CSS = `
1001
1195
  color: var(--remarq-text-secondary);
1002
1196
  margin-top: 4px;
1003
1197
  }
1198
+
1199
+ .remarq-popup-history {
1200
+ margin-top: 8px;
1201
+ font-size: 12px;
1202
+ color: var(--remarq-text-secondary);
1203
+ }
1204
+
1205
+ .remarq-popup-history summary {
1206
+ cursor: pointer;
1207
+ padding: 4px 0;
1208
+ user-select: none;
1209
+ }
1210
+
1211
+ .remarq-popup-history-list {
1212
+ list-style: none;
1213
+ margin: 4px 0 0 0;
1214
+ padding: 0;
1215
+ }
1216
+
1217
+ .remarq-popup-history-list li {
1218
+ padding: 2px 0;
1219
+ font-size: 11px;
1220
+ line-height: 1.4;
1221
+ }
1222
+
1223
+ .remarq-popup-reason {
1224
+ width: 100%;
1225
+ min-height: 50px;
1226
+ margin-bottom: 8px;
1227
+ padding: 6px;
1228
+ border: 1px solid var(--remarq-border);
1229
+ border-radius: 4px;
1230
+ background: var(--remarq-bg-secondary);
1231
+ color: var(--remarq-text);
1232
+ font-family: inherit;
1233
+ font-size: 12px;
1234
+ resize: vertical;
1235
+ box-sizing: border-box;
1236
+ }
1237
+
1238
+ .remarq-popup-reason-row {
1239
+ display: flex;
1240
+ justify-content: flex-end;
1241
+ gap: 8px;
1242
+ }
1004
1243
  `;
1005
1244
  function injectStyles() {
1006
1245
  if (document.querySelector(`style[${STYLES_ID}]`)) return;
@@ -1108,8 +1347,19 @@ var Toolbar = class {
1108
1347
  this.inspectBtn = this.createButton("inspect", ICONS.inspect, () => callbacks.onInspect());
1109
1348
  this.badgeEl = document.createElement("span");
1110
1349
  this.badgeEl.className = "remarq-badge";
1350
+ this.badgeEl.title = "Needs attention";
1111
1351
  this.badgeEl.style.display = "none";
1112
1352
  this.inspectBtn.appendChild(this.badgeEl);
1353
+ this.verificationBadgeEl = document.createElement("span");
1354
+ this.verificationBadgeEl.className = "remarq-badge remarq-toolbar-badge--verification";
1355
+ this.verificationBadgeEl.title = "Pending your verification";
1356
+ this.verificationBadgeEl.style.display = "none";
1357
+ this.verificationBadgeEl.addEventListener("click", (e) => {
1358
+ var _a3;
1359
+ e.stopPropagation();
1360
+ (_a3 = callbacks.onVerificationBadgeClick) == null ? void 0 : _a3.call(callbacks);
1361
+ });
1362
+ this.inspectBtn.appendChild(this.verificationBadgeEl);
1113
1363
  this.spacingBtn = this.createButton("spacing", ICONS.spacing, () => callbacks.onSpacingToggle());
1114
1364
  this.spacingBtn.disabled = true;
1115
1365
  const copyBtn = this.createButton("copy", ICONS.copy, () => callbacks.onCopy());
@@ -1156,6 +1406,10 @@ var Toolbar = class {
1156
1406
  this.badgeEl.textContent = String(count);
1157
1407
  this.badgeEl.style.display = count > 0 ? "flex" : "none";
1158
1408
  }
1409
+ setVerificationBadgeCount(count) {
1410
+ this.verificationBadgeEl.textContent = String(count);
1411
+ this.verificationBadgeEl.style.display = count > 0 ? "flex" : "none";
1412
+ }
1159
1413
  getFileInput() {
1160
1414
  return this.fileInput;
1161
1415
  }
@@ -1639,6 +1893,47 @@ var SpacingOverlay = class {
1639
1893
  };
1640
1894
 
1641
1895
  // src/ui/popup.ts
1896
+ var STATUS_LABEL = {
1897
+ pending: "Pending",
1898
+ in_progress: "In progress",
1899
+ fixed_unverified: "Fix claimed",
1900
+ verified: "Verified",
1901
+ dismissed: "Dismissed"
1902
+ };
1903
+ var EVENT_LABEL = {
1904
+ created: "Created",
1905
+ acknowledged: "In progress",
1906
+ fix_claimed: "Fix claimed",
1907
+ verified: "Verified",
1908
+ rejected: "Rejected",
1909
+ dismissed: "Dismissed",
1910
+ reopened: "Reopened",
1911
+ migrated: "Migrated"
1912
+ };
1913
+ function actionsForStatus(status) {
1914
+ switch (status) {
1915
+ case "pending":
1916
+ return [
1917
+ { label: "Acknowledge", action: "acknowledge", primary: true },
1918
+ { label: "Dismiss", action: "dismiss", needsReason: true }
1919
+ ];
1920
+ case "in_progress":
1921
+ return [
1922
+ { label: "Mark verified", action: "verify", primary: true },
1923
+ { label: "Dismiss", action: "dismiss", needsReason: true }
1924
+ ];
1925
+ case "fixed_unverified":
1926
+ return [
1927
+ { label: "Verify", action: "verify", primary: true },
1928
+ { label: "Reject", action: "reject", needsReason: true },
1929
+ { label: "Dismiss", action: "dismiss", needsReason: true }
1930
+ ];
1931
+ case "verified":
1932
+ return [{ label: "Reopen", action: "reopen" }];
1933
+ case "dismissed":
1934
+ return [{ label: "Reopen", action: "reopen" }];
1935
+ }
1936
+ }
1642
1937
  var POPUP_WIDTH = 300;
1643
1938
  var POPUP_MARGIN = 8;
1644
1939
  var Popup = class {
@@ -1647,6 +1942,7 @@ var Popup = class {
1647
1942
  this.popupEl = null;
1648
1943
  this.keyHandler = null;
1649
1944
  this.outsideClickHandler = null;
1945
+ this.pendingEditFlush = null;
1650
1946
  }
1651
1947
  show(info, position, onSubmit, onCancel) {
1652
1948
  this.hide();
@@ -1731,7 +2027,7 @@ var Popup = class {
1731
2027
  popup2.className = "remarq-popup";
1732
2028
  const header = document.createElement("div");
1733
2029
  header.className = "remarq-popup-header";
1734
- header.textContent = `<${info.tag}>${info.text ? ` "${info.text}"` : ""} [${info.status}]`;
2030
+ header.textContent = `<${info.tag}>${info.text ? ` "${info.text}"` : ""} [${STATUS_LABEL[info.status]}]`;
1735
2031
  const body = document.createElement("div");
1736
2032
  body.className = "remarq-popup-body";
1737
2033
  const makeCommentEl = () => {
@@ -1743,38 +2039,10 @@ var Popup = class {
1743
2039
  return el;
1744
2040
  };
1745
2041
  body.appendChild(makeCommentEl());
2042
+ body.appendChild(this.buildLifecycleViewer(info.lifecycle));
1746
2043
  const actions = document.createElement("div");
1747
2044
  actions.className = "remarq-popup-actions";
1748
- if (info.status === "pending") {
1749
- const resolveBtn = document.createElement("button");
1750
- resolveBtn.className = "remarq-primary";
1751
- resolveBtn.textContent = "Resolve";
1752
- resolveBtn.addEventListener("click", () => {
1753
- this.hide();
1754
- callbacks.onResolve();
1755
- });
1756
- actions.appendChild(resolveBtn);
1757
- }
1758
- const copyBtn = document.createElement("button");
1759
- copyBtn.textContent = "Copy";
1760
- copyBtn.addEventListener("click", () => {
1761
- callbacks.onCopy();
1762
- });
1763
- actions.appendChild(copyBtn);
1764
- const deleteBtn = document.createElement("button");
1765
- deleteBtn.textContent = "Delete";
1766
- deleteBtn.addEventListener("click", () => {
1767
- this.hide();
1768
- callbacks.onDelete();
1769
- });
1770
- actions.appendChild(deleteBtn);
1771
- const closeBtn = document.createElement("button");
1772
- closeBtn.textContent = "Close";
1773
- closeBtn.addEventListener("click", () => {
1774
- this.hide();
1775
- callbacks.onClose();
1776
- });
1777
- actions.appendChild(closeBtn);
2045
+ this.renderActionButtons(actions, info, callbacks);
1778
2046
  popup2.appendChild(header);
1779
2047
  popup2.appendChild(body);
1780
2048
  popup2.appendChild(actions);
@@ -1801,7 +2069,103 @@ var Popup = class {
1801
2069
  document.addEventListener("mousedown", this.outsideClickHandler);
1802
2070
  }, 0);
1803
2071
  }
2072
+ buildLifecycleViewer(lifecycle) {
2073
+ var _a3, _b;
2074
+ const details = document.createElement("details");
2075
+ details.className = "remarq-popup-history";
2076
+ const summary = document.createElement("summary");
2077
+ summary.textContent = `History (${lifecycle.length})`;
2078
+ details.appendChild(summary);
2079
+ const list = document.createElement("ul");
2080
+ list.className = "remarq-popup-history-list";
2081
+ for (const ev of lifecycle) {
2082
+ const li = document.createElement("li");
2083
+ const when = new Date(ev.timestamp).toLocaleString();
2084
+ const who = (_a3 = ev.actor) != null ? _a3 : "system";
2085
+ const what = (_b = EVENT_LABEL[ev.type]) != null ? _b : ev.type;
2086
+ let text = `${when} \xB7 ${who} \xB7 ${what}`;
2087
+ if (ev.reason) text += ` \u2014 ${ev.reason}`;
2088
+ li.textContent = text;
2089
+ list.appendChild(li);
2090
+ }
2091
+ details.appendChild(list);
2092
+ return details;
2093
+ }
2094
+ renderActionButtons(container, info, callbacks) {
2095
+ container.replaceChildren();
2096
+ const transitions = document.createElement("div");
2097
+ transitions.className = "remarq-popup-actions-row remarq-popup-actions-row--transitions";
2098
+ for (const def of actionsForStatus(info.status)) {
2099
+ const btn = document.createElement("button");
2100
+ btn.textContent = def.label;
2101
+ if (def.primary) btn.className = "remarq-primary";
2102
+ btn.addEventListener("click", () => {
2103
+ if (def.needsReason) {
2104
+ this.showReasonInput(container, info, callbacks, def);
2105
+ } else {
2106
+ this.hide();
2107
+ callbacks.onTransition(def.action);
2108
+ }
2109
+ });
2110
+ transitions.appendChild(btn);
2111
+ }
2112
+ container.appendChild(transitions);
2113
+ const utility = document.createElement("div");
2114
+ utility.className = "remarq-popup-actions-row remarq-popup-actions-row--utility";
2115
+ const copyBtn = document.createElement("button");
2116
+ copyBtn.className = "remarq-popup-utility-btn";
2117
+ copyBtn.textContent = "Copy";
2118
+ copyBtn.addEventListener("click", () => callbacks.onCopy());
2119
+ utility.appendChild(copyBtn);
2120
+ const deleteBtn = document.createElement("button");
2121
+ deleteBtn.className = "remarq-popup-utility-btn";
2122
+ deleteBtn.textContent = "Delete";
2123
+ deleteBtn.addEventListener("click", () => {
2124
+ this.hide();
2125
+ callbacks.onDelete();
2126
+ });
2127
+ utility.appendChild(deleteBtn);
2128
+ const closeBtn = document.createElement("button");
2129
+ closeBtn.className = "remarq-popup-utility-btn";
2130
+ closeBtn.textContent = "Close";
2131
+ closeBtn.addEventListener("click", () => {
2132
+ this.hide();
2133
+ callbacks.onClose();
2134
+ });
2135
+ utility.appendChild(closeBtn);
2136
+ container.appendChild(utility);
2137
+ }
2138
+ showReasonInput(container, info, callbacks, def) {
2139
+ container.replaceChildren();
2140
+ const textarea = document.createElement("textarea");
2141
+ textarea.placeholder = `Reason for ${def.label.toLowerCase()} (optional)\u2026`;
2142
+ textarea.className = "remarq-popup-reason";
2143
+ container.appendChild(textarea);
2144
+ const row = document.createElement("div");
2145
+ row.className = "remarq-popup-reason-row";
2146
+ const cancel = document.createElement("button");
2147
+ cancel.textContent = "Cancel";
2148
+ cancel.addEventListener("click", () => {
2149
+ this.renderActionButtons(container, info, callbacks);
2150
+ });
2151
+ const submit = document.createElement("button");
2152
+ submit.className = "remarq-primary";
2153
+ submit.textContent = "Submit";
2154
+ submit.addEventListener("click", () => {
2155
+ const reason = textarea.value.trim() || void 0;
2156
+ this.hide();
2157
+ callbacks.onTransition(def.action, reason);
2158
+ });
2159
+ row.appendChild(cancel);
2160
+ row.appendChild(submit);
2161
+ container.appendChild(row);
2162
+ textarea.focus();
2163
+ }
1804
2164
  hide() {
2165
+ if (this.pendingEditFlush) {
2166
+ this.pendingEditFlush();
2167
+ this.pendingEditFlush = null;
2168
+ }
1805
2169
  if (this.popupEl) {
1806
2170
  this.popupEl.remove();
1807
2171
  this.popupEl = null;
@@ -1836,12 +2200,8 @@ var Popup = class {
1836
2200
  commentEl.replaceWith(textarea);
1837
2201
  textarea.focus();
1838
2202
  textarea.selectionStart = textarea.value.length;
1839
- const saveEdit = () => {
1840
- const newComment = textarea.value.trim();
1841
- if (newComment && newComment !== info.comment) {
1842
- info.comment = newComment;
1843
- callbacks.onEdit(newComment);
1844
- }
2203
+ const restoreView = () => {
2204
+ if (!textarea.isConnected) return;
1845
2205
  const restored = document.createElement("div");
1846
2206
  restored.textContent = info.comment;
1847
2207
  restored.style.cursor = "pointer";
@@ -1849,26 +2209,33 @@ var Popup = class {
1849
2209
  restored.addEventListener("click", () => this.enterEditMode(restored, info, callbacks));
1850
2210
  textarea.replaceWith(restored);
1851
2211
  };
2212
+ const commitEdit = () => {
2213
+ this.pendingEditFlush = null;
2214
+ const newComment = textarea.value.trim();
2215
+ if (newComment && newComment !== info.comment) {
2216
+ info.comment = newComment;
2217
+ callbacks.onEdit(newComment);
2218
+ }
2219
+ restoreView();
2220
+ };
2221
+ const cancelEdit = () => {
2222
+ this.pendingEditFlush = null;
2223
+ restoreView();
2224
+ };
2225
+ this.pendingEditFlush = commitEdit;
1852
2226
  textarea.addEventListener("keydown", (e) => {
1853
2227
  if (e.key === "Enter" && !e.shiftKey) {
1854
2228
  e.preventDefault();
1855
- saveEdit();
2229
+ commitEdit();
1856
2230
  }
1857
2231
  if (e.key === "Escape") {
1858
2232
  e.stopPropagation();
1859
- const restored = document.createElement("div");
1860
- restored.textContent = info.comment;
1861
- restored.style.cursor = "pointer";
1862
- restored.title = "Click to edit";
1863
- restored.addEventListener("click", () => this.enterEditMode(restored, info, callbacks));
1864
- textarea.replaceWith(restored);
2233
+ cancelEdit();
1865
2234
  }
1866
2235
  });
1867
2236
  textarea.addEventListener("blur", () => {
1868
2237
  setTimeout(() => {
1869
- if (textarea.isConnected) {
1870
- saveEdit();
1871
- }
2238
+ if (textarea.isConnected) commitEdit();
1872
2239
  }, 50);
1873
2240
  });
1874
2241
  }
@@ -1896,6 +2263,16 @@ var Popup = class {
1896
2263
  };
1897
2264
 
1898
2265
  // src/ui/markers.ts
2266
+ var STATUS_COLOR = {
2267
+ pending: "var(--remarq-status-pending)",
2268
+ in_progress: "var(--remarq-status-in-progress)",
2269
+ fixed_unverified: "var(--remarq-status-fixed-unverified)",
2270
+ verified: "var(--remarq-status-verified)",
2271
+ dismissed: "var(--remarq-status-dismissed)"
2272
+ };
2273
+ function statusClass(status) {
2274
+ return `remarq-marker--${status.replace("_", "-")}`;
2275
+ }
1899
2276
  var MarkerManager = class {
1900
2277
  constructor(container, onClick) {
1901
2278
  this.container = container;
@@ -1908,7 +2285,7 @@ var MarkerManager = class {
1908
2285
  addMarker(annotation, target) {
1909
2286
  this.counter++;
1910
2287
  const markerEl = document.createElement("div");
1911
- markerEl.className = "remarq-marker";
2288
+ markerEl.className = `remarq-marker ${statusClass(annotation.status)}`;
1912
2289
  markerEl.setAttribute("data-status", annotation.status);
1913
2290
  markerEl.setAttribute("data-annotation-id", annotation.id);
1914
2291
  markerEl.textContent = String(this.counter);
@@ -1934,7 +2311,17 @@ var MarkerManager = class {
1934
2311
  const entry = this.markers.get(id);
1935
2312
  if (entry) {
1936
2313
  entry.annotation.status = status;
2314
+ entry.markerEl.className = `remarq-marker ${statusClass(status)}`;
1937
2315
  entry.markerEl.setAttribute("data-status", status);
2316
+ this.applyOutline(entry.target, status);
2317
+ }
2318
+ }
2319
+ scrollToMarker(id) {
2320
+ const entry = this.markers.get(id);
2321
+ if (!entry) return;
2322
+ try {
2323
+ entry.target.scrollIntoView({ behavior: "smooth", block: "center" });
2324
+ } catch (e) {
1938
2325
  }
1939
2326
  }
1940
2327
  clear() {
@@ -1953,7 +2340,7 @@ var MarkerManager = class {
1953
2340
  this.clear();
1954
2341
  }
1955
2342
  applyOutline(target, status) {
1956
- const color = status === "pending" ? "#f97316" : "rgba(34, 197, 94, 0.5)";
2343
+ const color = STATUS_COLOR[status];
1957
2344
  target.style.outline = `2px solid ${color}`;
1958
2345
  target.style.outlineOffset = "2px";
1959
2346
  }
@@ -2300,8 +2687,18 @@ function refreshMarkers() {
2300
2687
  markers.addMarker(ann, el);
2301
2688
  }
2302
2689
  detachedPanel.update(otherBreakpoint, detached);
2303
- const pendingCount = anns.filter((a) => a.status === "pending").length;
2304
- toolbar.setBadgeCount(pendingCount);
2690
+ const needsAttention = anns.filter(
2691
+ (a) => a.status === "pending" || a.status === "in_progress"
2692
+ ).length;
2693
+ const needsVerification = anns.filter((a) => a.status === "fixed_unverified").length;
2694
+ toolbar.setBadgeCount(needsAttention);
2695
+ toolbar.setVerificationBadgeCount(needsVerification);
2696
+ }
2697
+ function jumpToFirstUnverified() {
2698
+ if (!storage || !markers) return;
2699
+ const ann = storage.getByRoute(currentRoute()).find((a) => a.status === "fixed_unverified");
2700
+ if (!ann) return;
2701
+ markers.scrollToMarker(ann.id);
2305
2702
  }
2306
2703
  function scheduleRefresh() {
2307
2704
  if (refreshScheduled) return;
@@ -2334,6 +2731,7 @@ function handleInspectClick(e) {
2334
2731
  classFilter: options.classFilter,
2335
2732
  dataAttribute: options.dataAttribute
2336
2733
  });
2734
+ const now = Date.now();
2337
2735
  const ann = {
2338
2736
  id: generateId(),
2339
2737
  comment,
@@ -2341,8 +2739,9 @@ function handleInspectClick(e) {
2341
2739
  route: currentRoute(),
2342
2740
  viewport: `${window.innerWidth}x${window.innerHeight}`,
2343
2741
  viewportBucket: toBucket(window.innerWidth),
2344
- timestamp: Date.now(),
2345
- status: "pending"
2742
+ timestamp: now,
2743
+ status: "pending",
2744
+ lifecycle: [{ type: "created", actor: "designer", timestamp: now }]
2346
2745
  };
2347
2746
  cacheElement(ann.id, target);
2348
2747
  storage.add(ann);
@@ -2434,7 +2833,8 @@ function handleMarkerClick(annotationId) {
2434
2833
  tag: ann.fingerprint.tagName,
2435
2834
  text: (_a3 = ann.fingerprint.textContent) != null ? _a3 : "",
2436
2835
  comment: ann.comment,
2437
- status: ann.status
2836
+ status: ann.status,
2837
+ lifecycle: ann.lifecycle
2438
2838
  },
2439
2839
  {
2440
2840
  top: window.scrollY + rect.bottom + 8,
@@ -2442,9 +2842,8 @@ function handleMarkerClick(annotationId) {
2442
2842
  anchorBottom: window.scrollY + rect.top - 8
2443
2843
  },
2444
2844
  {
2445
- onResolve: () => {
2446
- storage.update(ann.id, { status: "resolved" });
2447
- refreshMarkers();
2845
+ onTransition: (action, reason) => {
2846
+ applyTransition(ann.id, action, reason ? { reason } : void 0);
2448
2847
  },
2449
2848
  onDelete: () => {
2450
2849
  elementCache.delete(ann.id);
@@ -2458,12 +2857,14 @@ function handleMarkerClick(annotationId) {
2458
2857
  refreshMarkers();
2459
2858
  },
2460
2859
  onCopy: () => {
2461
- const fp = ann.fingerprint;
2860
+ var _a4;
2861
+ const fresh = (_a4 = storage.getById(ann.id)) != null ? _a4 : ann;
2862
+ const fp = fresh.fingerprint;
2462
2863
  const lines = [
2463
- `[${ann.status}] "${ann.comment}"`,
2864
+ `[${fresh.status}] "${fresh.comment}"`,
2464
2865
  `Element: <${fp.tagName}>${fp.textContent ? ` "${fp.textContent}"` : ""}`,
2465
- `Route: ${ann.route}`,
2466
- `Viewport: ${ann.viewportBucket}px`
2866
+ `Route: ${fresh.route}`,
2867
+ `Viewport: ${fresh.viewportBucket}px`
2467
2868
  ];
2468
2869
  if (fp.sourceLocation) lines.push(`Source: ${fp.sourceLocation}`);
2469
2870
  navigator.clipboard.writeText(lines.join("\n")).then(() => {
@@ -2598,6 +2999,16 @@ function copyAgentToClipboard() {
2598
2999
  console.warn("[web-remarq] Clipboard write failed");
2599
3000
  });
2600
3001
  }
3002
+ function applyTransition(id, action, opts = {}) {
3003
+ if (!storage) return;
3004
+ const ann = storage.getById(id);
3005
+ if (!ann) return;
3006
+ const { status, event } = transition(ann, action, opts);
3007
+ const lifecycle = [...ann.lifecycle, event];
3008
+ storage.update(id, { status, lifecycle });
3009
+ markers == null ? void 0 : markers.updateStatus(id, status);
3010
+ refreshMarkers();
3011
+ }
2601
3012
  function setupMutationObserver() {
2602
3013
  mutationObserver = new MutationObserver((mutations) => {
2603
3014
  let hasExternalMutation = false;
@@ -2617,18 +3028,18 @@ function setupMutationObserver() {
2617
3028
  }
2618
3029
  var WebRemarq = {
2619
3030
  init(opts) {
2620
- var _a3;
3031
+ var _a3, _b;
2621
3032
  if (initialized) return;
2622
3033
  options = opts != null ? opts : {};
2623
3034
  try {
2624
3035
  injectStyles();
2625
- storage = new AnnotationStorage();
3036
+ storage = new AnnotationStorage((_a3 = options.storage) != null ? _a3 : new LocalStorageAdapter());
2626
3037
  themeManager = new ThemeManager(document.body, options.theme);
2627
3038
  overlay = new Overlay(themeManager.container);
2628
3039
  spacingOverlay = new SpacingOverlay(themeManager.container);
2629
3040
  popup = new Popup(themeManager.container);
2630
3041
  markers = new MarkerManager(themeManager.container, handleMarkerClick);
2631
- const position = (_a3 = options.position) != null ? _a3 : "bottom-right";
3042
+ const position = (_b = options.position) != null ? _b : "bottom-right";
2632
3043
  detachedPanel = new DetachedPanel(themeManager.container, (id) => {
2633
3044
  elementCache.delete(id);
2634
3045
  storage.remove(id);
@@ -2659,11 +3070,9 @@ var WebRemarq = {
2659
3070
  showToast(themeManager.container, "All annotations cleared");
2660
3071
  },
2661
3072
  onThemeToggle: () => themeManager.toggle(),
2662
- onHelp: () => showShortcutsModal(themeManager.container)
3073
+ onHelp: () => showShortcutsModal(themeManager.container),
3074
+ onVerificationBadgeClick: jumpToFirstUnverified
2663
3075
  }, position);
2664
- if (storage.isMemoryOnly) {
2665
- toolbar.setMemoryWarning(true);
2666
- }
2667
3076
  routeObserver = new RouteObserver();
2668
3077
  unsubRoute = routeObserver.onChange(() => refreshMarkers());
2669
3078
  document.addEventListener("click", handleInspectClick, true);
@@ -2671,8 +3080,13 @@ var WebRemarq = {
2671
3080
  document.addEventListener("keydown", handleInspectKeydown);
2672
3081
  setupMutationObserver();
2673
3082
  initViewportListener(() => refreshMarkers());
3083
+ storage.ready.then(() => {
3084
+ if (storage.isMemoryOnly) {
3085
+ toolbar.setMemoryWarning(true);
3086
+ }
3087
+ refreshMarkers();
3088
+ });
2674
3089
  console.debug(`[web-remarq] Initialized on route: ${currentRoute()}`);
2675
- refreshMarkers();
2676
3090
  initialized = true;
2677
3091
  } catch (err) {
2678
3092
  console.error("[web-remarq] Init failed:", err);
@@ -2751,9 +3165,32 @@ var WebRemarq = {
2751
3165
  elementCache.clear();
2752
3166
  storage == null ? void 0 : storage.clearAll();
2753
3167
  if (initialized) refreshMarkers();
3168
+ },
3169
+ acknowledge(id, opts) {
3170
+ applyTransition(id, "acknowledge", opts);
3171
+ },
3172
+ claimFix(id, opts) {
3173
+ applyTransition(id, "claimFix", opts);
3174
+ },
3175
+ verify(id, opts) {
3176
+ applyTransition(id, "verify", opts);
3177
+ },
3178
+ reject(id, opts) {
3179
+ applyTransition(id, "reject", opts);
3180
+ },
3181
+ dismiss(id, opts) {
3182
+ applyTransition(id, "dismiss", opts);
3183
+ },
3184
+ reopen(id, opts) {
3185
+ applyTransition(id, "reopen", opts);
3186
+ },
3187
+ /** @deprecated Use verify() instead. */
3188
+ markResolved(id) {
3189
+ applyTransition(id, "verify");
2754
3190
  }
2755
3191
  };
2756
3192
  export {
3193
+ LocalStorageAdapter,
2757
3194
  WebRemarq
2758
3195
  };
2759
3196
  //# sourceMappingURL=index.js.map