opencode-browser-annotation-plugin 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,488 @@
1
+ // Overlay content script: a full-window annotation layer plus a right sidebar,
2
+ // all rendered inside a shadow root so the host page's CSS cannot interfere and
3
+ // ours cannot leak. Toggled via Alt+A (handled by the background worker).
4
+ //
5
+ // Flow: pick an element (hover highlight + click, shadow-DOM aware via
6
+ // composedPath) -> type an instruction in a card -> choose Act/Queue ->
7
+ // Submit sends all cards to the background worker, which POSTs to the plugin.
8
+
9
+ (() => {
10
+ if (window.__ocAnnotationInjected) {
11
+ // Re-injection: just toggle.
12
+ window.dispatchEvent(new CustomEvent("oc-annotation-toggle"));
13
+ return;
14
+ }
15
+ window.__ocAnnotationInjected = true;
16
+
17
+ const STYLE = `
18
+ :host { all: initial; }
19
+ #oc-layer {
20
+ position: fixed; inset: 0; pointer-events: none;
21
+ cursor: crosshair; background: transparent;
22
+ }
23
+ #oc-highlight {
24
+ position: fixed; display: none; pointer-events: none;
25
+ border: 2px solid #2b7cff; background: rgba(43,124,255,0.12);
26
+ border-radius: 3px; box-shadow: 0 0 0 1px rgba(255,255,255,0.6);
27
+ transition: all .04s ease-out;
28
+ }
29
+ #oc-hint {
30
+ position: fixed; top: 16px; left: 50%; transform: translateX(-50%);
31
+ display: none; gap: 10px; align-items: center; pointer-events: none;
32
+ font: 13px system-ui, sans-serif; color: #fff;
33
+ background: rgba(20,22,28,.92); padding: 8px 14px; border-radius: 999px;
34
+ box-shadow: 0 6px 20px rgba(0,0,0,.25);
35
+ }
36
+ #oc-hint .key {
37
+ font-size: 11px; background: rgba(255,255,255,.18);
38
+ padding: 2px 7px; border-radius: 5px;
39
+ }
40
+ #oc-sidebar {
41
+ position: fixed; top: 0; right: 0; height: 100vh; width: 344px;
42
+ pointer-events: auto; display: flex; flex-direction: column;
43
+ background: #fbfbfd; color: #1c1d21;
44
+ font: 14px/1.45 system-ui, -apple-system, sans-serif;
45
+ box-shadow: -8px 0 30px rgba(0,0,0,.16);
46
+ border-left: 1px solid rgba(0,0,0,.08);
47
+ }
48
+ #oc-sidebar header {
49
+ display: flex; align-items: center; justify-content: space-between;
50
+ padding: 14px 16px; border-bottom: 1px solid rgba(0,0,0,.07);
51
+ }
52
+ #oc-sidebar header .title { font-weight: 650; font-size: 14px; letter-spacing: .2px; }
53
+ #oc-close {
54
+ border: none; background: transparent; font-size: 16px; color: #8a8d96;
55
+ cursor: pointer; border-radius: 6px; width: 28px; height: 28px;
56
+ }
57
+ #oc-close:hover { background: rgba(0,0,0,.06); color: #1c1d21; }
58
+ .oc-actions { padding: 12px 16px 4px; }
59
+ button.primary {
60
+ font: inherit; font-weight: 600; width: 100%;
61
+ padding: 9px 12px; border-radius: 9px; cursor: pointer;
62
+ color: #fff; background: #2b7cff; border: 1px solid #2b7cff;
63
+ transition: background .15s, transform .05s;
64
+ }
65
+ button.primary:hover:not(:disabled) { background: #1e6ef0; }
66
+ button.primary:active:not(:disabled) { transform: translateY(1px); }
67
+ button.primary:disabled { opacity: .45; cursor: default; }
68
+ #oc-pick.active { background: #e8551f; border-color: #e8551f; }
69
+ #oc-list { flex: 1; overflow-y: auto; padding: 10px 16px; display: flex; flex-direction: column; gap: 10px; }
70
+ .oc-empty { color: #8a8d96; font-size: 13px; padding: 20px 4px; text-align: center; }
71
+ .oc-card {
72
+ background: #fff; border: 1px solid rgba(0,0,0,.08); border-radius: 12px;
73
+ padding: 10px; box-shadow: 0 1px 3px rgba(0,0,0,.05);
74
+ }
75
+ .oc-card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 7px; }
76
+ .oc-desc {
77
+ font: 12px ui-monospace, monospace; color: #4a4d57;
78
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
79
+ background: #f0f1f4; padding: 2px 7px; border-radius: 6px;
80
+ }
81
+ .oc-rm { color: #b02a2a; cursor: pointer; font-size: 13px; padding: 0 2px; }
82
+ .oc-rm:hover { color: #e03131; }
83
+ .oc-card textarea {
84
+ width: 100%; box-sizing: border-box; resize: vertical; font: inherit;
85
+ border: 1px solid rgba(0,0,0,.12); border-radius: 8px; padding: 7px 9px;
86
+ background: #fff; color: #1c1d21;
87
+ }
88
+ .oc-card textarea:focus { outline: none; border-color: #2b7cff; box-shadow: 0 0 0 3px rgba(43,124,255,.15); }
89
+ .oc-modes { display: flex; gap: 6px; margin-top: 8px; }
90
+ .oc-mode {
91
+ font: inherit; font-size: 12px; font-weight: 600; flex: 1;
92
+ padding: 5px 8px; border-radius: 7px; cursor: pointer;
93
+ background: #f0f1f4; color: #55585f; border: 1px solid transparent;
94
+ }
95
+ .oc-mode:hover { background: #e7e8ec; }
96
+ .oc-mode.sel { background: #e8f0ff; color: #1e6ef0; border-color: #bcd4ff; }
97
+ #oc-sidebar footer { padding: 12px 16px 14px; border-top: 1px solid rgba(0,0,0,.07); position: relative; }
98
+ .oc-status { font-size: 12px; margin-bottom: 9px; display: flex; align-items: center; gap: 7px; }
99
+ .oc-status::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: #b7bac2; flex: none; }
100
+ .oc-status.good::before { background: #1faa5a; }
101
+ .oc-status.warn::before { background: #e0a020; }
102
+ .oc-status.bad::before { background: #e03131; }
103
+ .oc-status.checking::before { background: #7aa7ff; }
104
+ .oc-status.good { color: #178048; }
105
+ .oc-status.warn { color: #9a6a10; }
106
+ .oc-status.bad { color: #b02a2a; }
107
+ .oc-toast {
108
+ position: absolute; left: 16px; right: 16px; bottom: 60px;
109
+ background: #1c1d21; color: #fff; font-size: 12.5px;
110
+ padding: 8px 12px; border-radius: 9px; opacity: 0; transform: translateY(6px);
111
+ transition: opacity .2s, transform .2s; pointer-events: none;
112
+ box-shadow: 0 8px 24px rgba(0,0,0,.22);
113
+ }
114
+ .oc-toast.show { opacity: 1; transform: translateY(0); }
115
+ .oc-toast.bad { background: #b02a2a; }
116
+ `;
117
+
118
+ let host = null; // shadow host element
119
+ let root = null; // shadow root
120
+ let picking = false;
121
+ let hoverEl = null;
122
+ let annotations = []; // { instruction, mode, page, element }
123
+ let statusTimer = null;
124
+
125
+ // ---------- element metadata ----------
126
+
127
+ function bestTestId(el) {
128
+ for (const attr of ["data-testid", "data-test", "data-test-id", "data-cy", "data-qa"]) {
129
+ const v = el.getAttribute && el.getAttribute(attr);
130
+ if (v) return v;
131
+ }
132
+ return undefined;
133
+ }
134
+
135
+ function cssPath(el) {
136
+ if (!(el instanceof Element)) return "";
137
+ if (el.id) return `#${CSS.escape(el.id)}`;
138
+ const parts = [];
139
+ let node = el;
140
+ while (node && node.nodeType === 1 && parts.length < 6) {
141
+ let sel = node.nodeName.toLowerCase();
142
+ if (node.classList && node.classList.length) {
143
+ sel += "." + Array.from(node.classList).slice(0, 3).map((c) => CSS.escape(c)).join(".");
144
+ }
145
+ const parent = node.parentElement;
146
+ if (parent) {
147
+ const sibs = Array.from(parent.children).filter((c) => c.nodeName === node.nodeName);
148
+ if (sibs.length > 1) sel += `:nth-of-type(${sibs.indexOf(node) + 1})`;
149
+ }
150
+ parts.unshift(sel);
151
+ if (node.id) {
152
+ parts[0] = `#${CSS.escape(node.id)}`;
153
+ break;
154
+ }
155
+ node = node.parentElement;
156
+ }
157
+ return parts.join(" > ");
158
+ }
159
+
160
+ function elementMeta(el, inShadow) {
161
+ const r = el.getBoundingClientRect();
162
+ const classes = el.classList ? Array.from(el.classList) : [];
163
+ const meta = {
164
+ selector: cssPath(el),
165
+ tag: el.tagName,
166
+ id: el.id || undefined,
167
+ name: el.getAttribute("name") || undefined,
168
+ testId: bestTestId(el),
169
+ role: el.getAttribute("role") || undefined,
170
+ ariaLabel: el.getAttribute("aria-label") || undefined,
171
+ classes: classes.length ? classes : undefined,
172
+ text: (el.textContent || "").trim().slice(0, 500) || undefined,
173
+ href: el.getAttribute("href") || undefined,
174
+ src: el.getAttribute("src") || undefined,
175
+ bounds: { x: r.left, y: r.top, width: r.width, height: r.height },
176
+ inShadow: Boolean(inShadow),
177
+ inIframe: window.top !== window.self,
178
+ html: (el.outerHTML || "").slice(0, 800),
179
+ };
180
+ for (const k of Object.keys(meta)) if (meta[k] === undefined) delete meta[k];
181
+ return meta;
182
+ }
183
+
184
+ function shortDescriptor(el) {
185
+ const tag = (el.tag || "el").toLowerCase();
186
+ const id = el.testId ? `[data-testid=${el.testId}]` : el.id ? `#${el.id}` : el.ariaLabel ? `[${el.ariaLabel}]` : "";
187
+ return `<${tag}>${id}`;
188
+ }
189
+
190
+ // ---------- picking ----------
191
+
192
+ function pickTarget(e) {
193
+ // composedPath surfaces the real element inside shadow roots; e.target only
194
+ // gives the shadow host.
195
+ const path = e.composedPath ? e.composedPath() : [];
196
+ for (const n of path) {
197
+ if (n instanceof Element && n !== host && !(root && root.contains(n))) {
198
+ const inShadow = n.getRootNode && n.getRootNode() instanceof ShadowRoot;
199
+ return { el: n, inShadow };
200
+ }
201
+ }
202
+ const t = e.target;
203
+ return t instanceof Element ? { el: t, inShadow: false } : null;
204
+ }
205
+
206
+ function positionHighlight(el) {
207
+ const box = root.getElementById("oc-highlight");
208
+ if (!box) return;
209
+ const r = el.getBoundingClientRect();
210
+ Object.assign(box.style, {
211
+ display: "block",
212
+ left: `${r.left}px`,
213
+ top: `${r.top}px`,
214
+ width: `${r.width}px`,
215
+ height: `${r.height}px`,
216
+ });
217
+ }
218
+
219
+ function onMove(e) {
220
+ if (!picking) return;
221
+ const hit = pickTarget(e);
222
+ if (!hit) return;
223
+ hoverEl = hit.el;
224
+ hoverEl.__ocInShadow = hit.inShadow;
225
+ positionHighlight(hoverEl);
226
+ }
227
+
228
+ function onClick(e) {
229
+ if (!picking) return;
230
+ const hit = pickTarget(e);
231
+ if (!hit) return;
232
+ e.preventDefault();
233
+ e.stopPropagation();
234
+ stopPicking();
235
+ addCardForElement(elementMeta(hit.el, hit.inShadow));
236
+ }
237
+
238
+ function onKey(e) {
239
+ if (e.key === "Escape") {
240
+ if (picking) {
241
+ stopPicking();
242
+ } else {
243
+ close();
244
+ }
245
+ }
246
+ }
247
+
248
+ function startPicking() {
249
+ picking = true;
250
+ root.getElementById("oc-layer").style.pointerEvents = "auto";
251
+ root.getElementById("oc-hint").style.display = "flex";
252
+ setPickBtn(true);
253
+ }
254
+
255
+ function stopPicking() {
256
+ picking = false;
257
+ const box = root.getElementById("oc-highlight");
258
+ if (box) box.style.display = "none";
259
+ root.getElementById("oc-layer").style.pointerEvents = "none";
260
+ root.getElementById("oc-hint").style.display = "none";
261
+ setPickBtn(false);
262
+ }
263
+
264
+ function setPickBtn(active) {
265
+ const btn = root.getElementById("oc-pick");
266
+ if (btn) {
267
+ btn.textContent = active ? "Picking… (Esc to cancel)" : "Select element";
268
+ btn.classList.toggle("active", active);
269
+ }
270
+ }
271
+
272
+ // ---------- annotation cards ----------
273
+
274
+ function addCardForElement(element) {
275
+ annotations.push({ instruction: "", mode: "act", page: { url: location.href, title: document.title }, element });
276
+ renderCards();
277
+ // focus the new card's textarea
278
+ const areas = root.querySelectorAll(".oc-card textarea");
279
+ const last = areas[areas.length - 1];
280
+ if (last) last.focus();
281
+ }
282
+
283
+ function renderCards() {
284
+ const list = root.getElementById("oc-list");
285
+ list.innerHTML = "";
286
+ if (annotations.length === 0) {
287
+ const empty = document.createElement("div");
288
+ empty.className = "oc-empty";
289
+ empty.textContent = "No annotations yet. Click “Select element”, then pick something on the page.";
290
+ list.appendChild(empty);
291
+ }
292
+ annotations.forEach((a, i) => {
293
+ const card = document.createElement("div");
294
+ card.className = "oc-card";
295
+ card.innerHTML = `
296
+ <div class="oc-card-head">
297
+ <span class="oc-desc" title="${escapeHtml(a.element.selector || "")}">${escapeHtml(shortDescriptor(a.element))}</span>
298
+ <span class="oc-rm" data-i="${i}" title="Remove">✕</span>
299
+ </div>
300
+ <textarea rows="2" placeholder="Instruction for this element…" data-i="${i}">${escapeHtml(a.instruction)}</textarea>
301
+ <div class="oc-modes" data-i="${i}">
302
+ <button class="oc-mode ${a.mode === "act" ? "sel" : ""}" data-mode="act" data-i="${i}">Act now</button>
303
+ <button class="oc-mode ${a.mode === "queue" ? "sel" : ""}" data-mode="queue" data-i="${i}">Queue</button>
304
+ </div>`;
305
+ list.appendChild(card);
306
+ });
307
+ updateSubmit();
308
+ }
309
+
310
+ function escapeHtml(s) {
311
+ return String(s || "").replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
312
+ }
313
+
314
+ function updateSubmit() {
315
+ const btn = root.getElementById("oc-submit");
316
+ btn.disabled = annotations.length === 0;
317
+ btn.textContent = annotations.length ? `Submit ${annotations.length} to agent` : "Submit to agent";
318
+ }
319
+
320
+ // ---------- status ----------
321
+
322
+ async function refreshStatus() {
323
+ const el = root.getElementById("oc-status");
324
+ el.className = "oc-status checking";
325
+ el.textContent = "Checking connection…";
326
+ chrome.runtime.sendMessage({ type: "oc-status" }, (res) => {
327
+ if (chrome.runtime.lastError || !res) {
328
+ el.className = "oc-status bad";
329
+ el.textContent = "Extension error";
330
+ return;
331
+ }
332
+ if (res.ok && res.data?.ok) {
333
+ if (res.data.activeSession) {
334
+ const name = res.data.sessionTitle || res.data.sessionID || "active session";
335
+ const q = res.data.queued ? ` · ${res.data.queued} queued` : "";
336
+ el.className = "oc-status good";
337
+ el.textContent = `Connected — ${name}${q}`;
338
+ } else {
339
+ el.className = "oc-status warn";
340
+ el.textContent = "Connected — send a message in OpenCode first";
341
+ }
342
+ } else {
343
+ el.className = "oc-status bad";
344
+ el.textContent = "Not connected — check the SSH tunnel";
345
+ }
346
+ });
347
+ }
348
+
349
+ // ---------- submit ----------
350
+
351
+ function collectInstructions() {
352
+ root.querySelectorAll(".oc-card textarea").forEach((ta) => {
353
+ const i = Number(ta.dataset.i);
354
+ if (annotations[i]) annotations[i].instruction = ta.value.trim();
355
+ });
356
+ }
357
+
358
+ function submit() {
359
+ collectInstructions();
360
+ if (annotations.length === 0) return;
361
+ const btn = root.getElementById("oc-submit");
362
+ btn.disabled = true;
363
+ setToast("Submitting…");
364
+ chrome.runtime.sendMessage({ type: "oc-submit", annotations }, (res) => {
365
+ if (chrome.runtime.lastError || !res) {
366
+ setToast("Extension error", true);
367
+ updateSubmit();
368
+ return;
369
+ }
370
+ if (res.ok) {
371
+ const parts = [];
372
+ if (res.injected) parts.push(`${res.injected} sent`);
373
+ if (res.queued) parts.push(`${res.queued} queued`);
374
+ setToast(parts.length ? parts.join(", ") : "Submitted");
375
+ annotations = [];
376
+ renderCards();
377
+ refreshStatus();
378
+ } else {
379
+ setToast(`Failed: ${res.error || "unknown"}`, true);
380
+ updateSubmit();
381
+ }
382
+ });
383
+ }
384
+
385
+ function setToast(text, bad) {
386
+ const t = root.getElementById("oc-toast");
387
+ t.textContent = text;
388
+ t.className = "oc-toast show" + (bad ? " bad" : "");
389
+ clearTimeout(t.__timer);
390
+ t.__timer = setTimeout(() => (t.className = "oc-toast"), 4000);
391
+ }
392
+
393
+ // ---------- UI construction ----------
394
+
395
+ function buildUI() {
396
+ host = document.createElement("div");
397
+ host.id = "oc-annotation-host";
398
+ host.style.cssText = "all: initial; position: fixed; inset: 0; z-index: 2147483647; pointer-events: none;";
399
+ root = host.attachShadow({ mode: "open" });
400
+ root.innerHTML = `
401
+ <style>${STYLE}</style>
402
+ <div id="oc-layer"></div>
403
+ <div id="oc-highlight"></div>
404
+ <div id="oc-hint"><span>Hover an element and click to annotate</span><span class="key">Esc</span></div>
405
+ <aside id="oc-sidebar">
406
+ <header>
407
+ <div class="title">OpenCode Annotation</div>
408
+ <button id="oc-close" title="Close (Esc)">✕</button>
409
+ </header>
410
+ <div class="oc-actions"><button id="oc-pick" class="primary">Select element</button></div>
411
+ <div id="oc-list"></div>
412
+ <footer>
413
+ <div id="oc-status" class="oc-status">…</div>
414
+ <button id="oc-submit" class="primary" disabled>Submit to agent</button>
415
+ <div id="oc-toast" class="oc-toast"></div>
416
+ </footer>
417
+ </aside>`;
418
+ document.documentElement.appendChild(host);
419
+
420
+ root.getElementById("oc-pick").addEventListener("click", () => (picking ? stopPicking() : startPicking()));
421
+ root.getElementById("oc-close").addEventListener("click", close);
422
+ root.getElementById("oc-submit").addEventListener("click", submit);
423
+
424
+ // event delegation for card controls
425
+ root.getElementById("oc-list").addEventListener("click", (e) => {
426
+ const rm = e.target.closest(".oc-rm");
427
+ if (rm) {
428
+ annotations.splice(Number(rm.dataset.i), 1);
429
+ renderCards();
430
+ return;
431
+ }
432
+ const mode = e.target.closest(".oc-mode");
433
+ if (mode) {
434
+ annotations[Number(mode.dataset.i)].mode = mode.dataset.mode;
435
+ renderCards();
436
+ }
437
+ });
438
+
439
+ renderCards();
440
+ refreshStatus();
441
+ statusTimer = setInterval(refreshStatus, 15000);
442
+ }
443
+
444
+ // ---------- lifecycle ----------
445
+
446
+ function open() {
447
+ if (host) {
448
+ host.style.display = "block";
449
+ } else {
450
+ buildUI();
451
+ }
452
+ document.addEventListener("mousemove", onMove, true);
453
+ document.addEventListener("click", onClick, true);
454
+ document.addEventListener("keydown", onKey, true);
455
+ }
456
+
457
+ function close() {
458
+ stopPicking();
459
+ document.removeEventListener("mousemove", onMove, true);
460
+ document.removeEventListener("click", onClick, true);
461
+ document.removeEventListener("keydown", onKey, true);
462
+ if (host) host.style.display = "none";
463
+ if (statusTimer) {
464
+ clearInterval(statusTimer);
465
+ statusTimer = null;
466
+ }
467
+ }
468
+
469
+ let visible = false;
470
+ function toggle() {
471
+ visible = !visible;
472
+ if (visible) {
473
+ open();
474
+ if (statusTimer === null && host) statusTimer = setInterval(refreshStatus, 15000);
475
+ } else {
476
+ close();
477
+ }
478
+ }
479
+
480
+ window.addEventListener("oc-annotation-toggle", toggle);
481
+ chrome.runtime.onMessage.addListener((msg) => {
482
+ if (msg?.type === "oc-toggle") toggle();
483
+ });
484
+
485
+ // First injection opens immediately.
486
+ visible = true;
487
+ open();
488
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-browser-annotation-plugin",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Select an element in your browser, type an instruction, and send it to your OpenCode agent over a loopback + SSH tunnel. Text and element metadata only (no screenshots).",
5
5
  "keywords": [
6
6
  "opencode",
@@ -16,6 +16,9 @@
16
16
  "url": "git+https://github.com/caoool/opencode-browser-annotation-plugin.git"
17
17
  },
18
18
  "type": "module",
19
+ "main": "./dist/plugin.js",
20
+ "module": "./dist/plugin.js",
21
+ "types": "./dist/plugin.d.ts",
19
22
  "exports": {
20
23
  ".": {
21
24
  "types": "./dist/plugin.d.ts",
@@ -1,126 +0,0 @@
1
- // Content script: element picker + annotation capture.
2
- // Injected on demand by the popup. Highlights elements on hover, captures a
3
- // selected element's metadata plus a typed instruction, and stores annotations
4
- // via the background service worker.
5
-
6
- (() => {
7
- if (window.__ocAnnotationActive) return;
8
- window.__ocAnnotationActive = true;
9
-
10
- const HIGHLIGHT_ID = "__oc-annotation-highlight";
11
- let overlay = null;
12
- let current = null;
13
-
14
- function makeOverlay() {
15
- const el = document.createElement("div");
16
- el.id = HIGHLIGHT_ID;
17
- Object.assign(el.style, {
18
- position: "fixed",
19
- pointerEvents: "none",
20
- zIndex: "2147483646",
21
- border: "2px solid #2b7cff",
22
- background: "rgba(43,124,255,0.12)",
23
- borderRadius: "2px",
24
- transition: "all 0.03s ease-out",
25
- display: "none",
26
- });
27
- document.documentElement.appendChild(el);
28
- return el;
29
- }
30
-
31
- function positionOverlay(target) {
32
- if (!overlay) return;
33
- const r = target.getBoundingClientRect();
34
- Object.assign(overlay.style, {
35
- display: "block",
36
- left: `${r.left}px`,
37
- top: `${r.top}px`,
38
- width: `${r.width}px`,
39
- height: `${r.height}px`,
40
- });
41
- }
42
-
43
- function cssPath(el) {
44
- if (!(el instanceof Element)) return "";
45
- if (el.id) return `#${CSS.escape(el.id)}`;
46
- const parts = [];
47
- let node = el;
48
- while (node && node.nodeType === 1 && parts.length < 5) {
49
- let selector = node.nodeName.toLowerCase();
50
- if (node.classList.length) {
51
- selector += "." + Array.from(node.classList).slice(0, 3).map((c) => CSS.escape(c)).join(".");
52
- }
53
- const parent = node.parentElement;
54
- if (parent) {
55
- const siblings = Array.from(parent.children).filter((c) => c.nodeName === node.nodeName);
56
- if (siblings.length > 1) {
57
- selector += `:nth-of-type(${siblings.indexOf(node) + 1})`;
58
- }
59
- }
60
- parts.unshift(selector);
61
- if (node.id) {
62
- parts[0] = `#${CSS.escape(node.id)}`;
63
- break;
64
- }
65
- node = node.parentElement;
66
- }
67
- return parts.join(" > ");
68
- }
69
-
70
- function elementMeta(el) {
71
- const r = el.getBoundingClientRect();
72
- return {
73
- selector: cssPath(el),
74
- tag: el.tagName,
75
- text: (el.textContent || "").trim().slice(0, 500),
76
- role: el.getAttribute("role") || undefined,
77
- ariaLabel: el.getAttribute("aria-label") || undefined,
78
- bounds: { x: r.left, y: r.top, width: r.width, height: r.height },
79
- html: el.outerHTML.slice(0, 800),
80
- };
81
- }
82
-
83
- function cleanup() {
84
- document.removeEventListener("mousemove", onMove, true);
85
- document.removeEventListener("click", onClick, true);
86
- document.removeEventListener("keydown", onKey, true);
87
- if (overlay) overlay.remove();
88
- overlay = null;
89
- window.__ocAnnotationActive = false;
90
- }
91
-
92
- function onMove(e) {
93
- const target = e.target;
94
- if (!target || target === overlay) return;
95
- current = target;
96
- positionOverlay(target);
97
- }
98
-
99
- function onKey(e) {
100
- if (e.key === "Escape") {
101
- e.preventDefault();
102
- cleanup();
103
- }
104
- }
105
-
106
- function onClick(e) {
107
- e.preventDefault();
108
- e.stopPropagation();
109
- const target = current || e.target;
110
- if (!target) return;
111
- const instruction = window.prompt("Instruction for this element:");
112
- if (instruction === null) return; // cancelled; keep picking
113
- const annotation = {
114
- instruction: instruction.trim(),
115
- page: { url: location.href, title: document.title },
116
- element: elementMeta(target),
117
- };
118
- chrome.runtime.sendMessage({ type: "oc-add-annotation", annotation }, () => {});
119
- cleanup();
120
- }
121
-
122
- overlay = makeOverlay();
123
- document.addEventListener("mousemove", onMove, true);
124
- document.addEventListener("click", onClick, true);
125
- document.addEventListener("keydown", onKey, true);
126
- })();