@wasao/kagemusha 0.2.0 → 0.3.5

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.
Files changed (93) hide show
  1. package/README.md +103 -453
  2. package/dist/commands/capture.js +57 -35
  3. package/dist/commands/capture.js.map +1 -1
  4. package/dist/commands/edit.d.ts +1 -1
  5. package/dist/commands/edit.d.ts.map +1 -1
  6. package/dist/commands/edit.js +46 -8
  7. package/dist/commands/edit.js.map +1 -1
  8. package/dist/commands/init.d.ts.map +1 -1
  9. package/dist/commands/init.js +82 -83
  10. package/dist/commands/init.js.map +1 -1
  11. package/dist/commands/login.d.ts.map +1 -1
  12. package/dist/commands/login.js +14 -18
  13. package/dist/commands/login.js.map +1 -1
  14. package/dist/editor/inject-script/annotations.d.ts +11 -0
  15. package/dist/editor/inject-script/annotations.d.ts.map +1 -0
  16. package/dist/editor/inject-script/annotations.js +409 -0
  17. package/dist/editor/inject-script/annotations.js.map +1 -0
  18. package/dist/editor/inject-script/bridge.d.ts +13 -0
  19. package/dist/editor/inject-script/bridge.d.ts.map +1 -0
  20. package/dist/editor/inject-script/bridge.js +33 -0
  21. package/dist/editor/inject-script/bridge.js.map +1 -0
  22. package/dist/editor/inject-script/crop.d.ts +9 -0
  23. package/dist/editor/inject-script/crop.d.ts.map +1 -0
  24. package/dist/editor/inject-script/crop.js +236 -0
  25. package/dist/editor/inject-script/crop.js.map +1 -0
  26. package/dist/editor/inject-script/dom.d.ts +7 -0
  27. package/dist/editor/inject-script/dom.d.ts.map +1 -0
  28. package/dist/editor/inject-script/dom.js +32 -0
  29. package/dist/editor/inject-script/dom.js.map +1 -0
  30. package/dist/editor/inject-script/index.d.ts +2 -0
  31. package/dist/editor/inject-script/index.d.ts.map +1 -0
  32. package/dist/editor/inject-script/index.js +56 -0
  33. package/dist/editor/inject-script/index.js.map +1 -0
  34. package/dist/editor/inject-script/record.d.ts +5 -0
  35. package/dist/editor/inject-script/record.d.ts.map +1 -0
  36. package/dist/editor/inject-script/record.js +398 -0
  37. package/dist/editor/inject-script/record.js.map +1 -0
  38. package/dist/editor/inject-script/selector.d.ts +6 -0
  39. package/dist/editor/inject-script/selector.d.ts.map +1 -0
  40. package/dist/editor/inject-script/selector.js +112 -0
  41. package/dist/editor/inject-script/selector.js.map +1 -0
  42. package/dist/editor/inject-script/state.d.ts +27 -0
  43. package/dist/editor/inject-script/state.d.ts.map +1 -0
  44. package/dist/editor/inject-script/state.js +26 -0
  45. package/dist/editor/inject-script/state.js.map +1 -0
  46. package/dist/editor/inject-script/svg.d.ts +7 -0
  47. package/dist/editor/inject-script/svg.d.ts.map +1 -0
  48. package/dist/editor/inject-script/svg.js +39 -0
  49. package/dist/editor/inject-script/svg.js.map +1 -0
  50. package/dist/editor/inject-script/toolbar.d.ts +14 -0
  51. package/dist/editor/inject-script/toolbar.d.ts.map +1 -0
  52. package/dist/editor/inject-script/toolbar.js +240 -0
  53. package/dist/editor/inject-script/toolbar.js.map +1 -0
  54. package/dist/editor/inject-script/types.d.ts +102 -0
  55. package/dist/editor/inject-script/types.d.ts.map +1 -0
  56. package/dist/editor/inject-script/types.js +5 -0
  57. package/dist/editor/inject-script/types.js.map +1 -0
  58. package/dist/editor/inject-script.js +1248 -699
  59. package/dist/index.js +9 -2
  60. package/dist/index.js.map +1 -1
  61. package/dist/lib/canonical.d.ts +35 -3
  62. package/dist/lib/canonical.d.ts.map +1 -1
  63. package/dist/lib/canonical.js +85 -25
  64. package/dist/lib/canonical.js.map +1 -1
  65. package/dist/lib/crawl.js +1 -1
  66. package/dist/lib/crawl.js.map +1 -1
  67. package/dist/lib/diff.d.ts +23 -4
  68. package/dist/lib/diff.d.ts.map +1 -1
  69. package/dist/lib/diff.js +5 -6
  70. package/dist/lib/diff.js.map +1 -1
  71. package/dist/lib/page-ready.d.ts +18 -0
  72. package/dist/lib/page-ready.d.ts.map +1 -0
  73. package/dist/lib/page-ready.js +19 -0
  74. package/dist/lib/page-ready.js.map +1 -0
  75. package/dist/lib/playwright-launch.d.ts +4 -0
  76. package/dist/lib/playwright-launch.d.ts.map +1 -0
  77. package/dist/lib/playwright-launch.js +11 -0
  78. package/dist/lib/playwright-launch.js.map +1 -0
  79. package/dist/lib/screenshot.d.ts +4 -1
  80. package/dist/lib/screenshot.d.ts.map +1 -1
  81. package/dist/lib/screenshot.js +37 -7
  82. package/dist/lib/screenshot.js.map +1 -1
  83. package/dist/lib/staging.d.ts +0 -1
  84. package/dist/lib/staging.d.ts.map +1 -1
  85. package/dist/lib/staging.js +0 -3
  86. package/dist/lib/staging.js.map +1 -1
  87. package/dist/types.d.ts +5 -0
  88. package/dist/types.d.ts.map +1 -1
  89. package/package.json +22 -12
  90. package/templates/notify-slack.jq +30 -0
  91. package/dist/editor/inject-script.d.ts +0 -2
  92. package/dist/editor/inject-script.d.ts.map +0 -1
  93. package/dist/editor/inject-script.js.map +0 -1
@@ -1,33 +1,479 @@
1
- // This script is injected into the target page to provide annotation editing.
2
- // It adds a toolbar and SVG overlay layer on top of the actual page.
3
- // This is a plain script (not a module) — no imports/exports.
4
- // Type helpers for window properties set by edit.ts
5
- const _win = window;
6
- const TOOLBAR_HEIGHT_FALLBACK = 48;
7
- let toolbarHeight = TOOLBAR_HEIGHT_FALLBACK;
8
- const svgNS = "http://www.w3.org/2000/svg";
9
- let tool = "rect";
10
- let annotations = [];
11
- let selectedId = null;
12
- let dragState = null;
13
- let nextId = 1;
14
- // Capture state — persisted on save (in page CSS pixels, NOT DPR-scaled)
15
- let captureMode = "fullPage";
16
- let captureCrop = null;
17
- let cropDragState = null;
18
- const HANDLE_SIZE = 10;
19
- const MIN_CROP = 10;
20
- // --- TOOLBAR ---
21
- const toolbar = document.createElement("div");
22
- toolbar.id = "kagemusha-toolbar";
23
- toolbar.innerHTML = `
1
+ "use strict";
2
+ (() => {
3
+ // src/editor/inject-script/dom.ts
4
+ var getMousePos = (e) => ({
5
+ x: e.pageX,
6
+ y: e.pageY
7
+ });
8
+ var measureSvgTextWidth = (svg2, text, fontSize, svgNs) => {
9
+ const tmp = document.createElementNS(svgNs, "text");
10
+ tmp.setAttribute("font-size", String(fontSize));
11
+ tmp.setAttribute("font-family", "-apple-system, sans-serif");
12
+ tmp.textContent = text;
13
+ svg2.appendChild(tmp);
14
+ const width = tmp.getBBox().width;
15
+ tmp.remove();
16
+ return width;
17
+ };
18
+ var showErrorToast = (message, ms = 5e3) => {
19
+ const toast = document.createElement("div");
20
+ toast.setAttribute(
21
+ "style",
22
+ "position:fixed;top:72px;left:50%;transform:translateX(-50%);background:#dc2626;color:#fff;padding:12px 20px;border-radius:8px;font-family:-apple-system,sans-serif;font-size:14px;line-height:1.4;z-index:var(--kg-z-top);box-shadow:0 4px 12px rgba(0,0,0,0.3);max-width:480px;white-space:pre-wrap;text-align:center;"
23
+ );
24
+ toast.textContent = message;
25
+ document.documentElement.appendChild(toast);
26
+ setTimeout(() => toast.remove(), ms);
27
+ };
28
+
29
+ // src/editor/inject-script/state.ts
30
+ var SVG_NS = "http://www.w3.org/2000/svg";
31
+ var HANDLE_SIZE = 10;
32
+ var MIN_CROP = 10;
33
+ var state = {
34
+ tool: "rect",
35
+ annotations: [],
36
+ selectedId: null,
37
+ dragState: null,
38
+ nextId: 1,
39
+ captureMode: "fullPage",
40
+ captureCrop: null,
41
+ cropDragState: null,
42
+ recordedSteps: [],
43
+ recording: false,
44
+ pickerKind: null
45
+ };
46
+ var allocateAnnotationId = () => `a${state.nextId++}`;
47
+
48
+ // src/editor/inject-script/svg.ts
49
+ var svgEl = null;
50
+ var captureGroupEl = null;
51
+ var initSvgLayer = () => {
52
+ const svg2 = document.createElementNS(SVG_NS, "svg");
53
+ svg2.id = "kagemusha-svg-layer";
54
+ svg2.classList.add("drawing");
55
+ document.documentElement.appendChild(svg2);
56
+ const updateSvgSize = () => {
57
+ svg2.setAttribute("width", String(window.innerWidth));
58
+ svg2.setAttribute("height", String(document.documentElement.scrollHeight));
59
+ };
60
+ updateSvgSize();
61
+ window.addEventListener("resize", updateSvgSize);
62
+ const defs = document.createElementNS(SVG_NS, "defs");
63
+ defs.innerHTML = '<marker id="kg-arrowhead" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto" fill="#FF0000"><polygon points="0 0, 10 3.5, 0 7"/></marker>';
64
+ svg2.appendChild(defs);
65
+ const captureGroup = document.createElementNS(SVG_NS, "g");
66
+ captureGroup.id = "kagemusha-capture-group";
67
+ svg2.appendChild(captureGroup);
68
+ svgEl = svg2;
69
+ captureGroupEl = captureGroup;
70
+ return { svg: svg2, captureGroup };
71
+ };
72
+ var getSvg = () => {
73
+ if (!svgEl) throw new Error("SVG layer not initialized");
74
+ return svgEl;
75
+ };
76
+ var getCaptureGroup = () => {
77
+ if (!captureGroupEl) throw new Error("Capture group not initialized");
78
+ return captureGroupEl;
79
+ };
80
+
81
+ // src/editor/inject-script/annotations.ts
82
+ var isLabelInput = (el) => el?.tagName === "INPUT";
83
+ var deselectAll = () => {
84
+ state.selectedId = null;
85
+ getSvg().querySelectorAll(".annotation").forEach((el) => {
86
+ el.classList.remove("selected");
87
+ });
88
+ };
89
+ var selectEl = (el) => {
90
+ deselectAll();
91
+ state.selectedId = el.dataset.id ?? null;
92
+ el.classList.add("selected");
93
+ };
94
+ var deleteSelected = () => {
95
+ if (!state.selectedId) return;
96
+ const el = getSvg().querySelector(`[data-id="${state.selectedId}"]`);
97
+ if (el) el.remove();
98
+ state.annotations = state.annotations.filter(
99
+ (a) => a.id !== state.selectedId
100
+ );
101
+ state.selectedId = null;
102
+ };
103
+ var startMove = (e, id) => {
104
+ e.stopPropagation();
105
+ const el = getSvg().querySelector(`[data-id="${id}"]`);
106
+ if (!el) return;
107
+ selectEl(el);
108
+ const p = getMousePos(e);
109
+ state.dragState = {
110
+ type: "move",
111
+ id,
112
+ el,
113
+ lastX: p.x,
114
+ lastY: p.y
115
+ };
116
+ };
117
+ var createLabelGroup = (id, x, y, text, fontSize = 14) => {
118
+ const g = document.createElementNS(SVG_NS, "g");
119
+ g.classList.add("annotation");
120
+ g.dataset.id = id;
121
+ const bg = document.createElementNS(SVG_NS, "rect");
122
+ const txt = document.createElementNS(SVG_NS, "text");
123
+ txt.textContent = text;
124
+ txt.setAttribute("x", String(x + 6));
125
+ txt.setAttribute("y", String(y + 16));
126
+ txt.setAttribute("fill", "#FF0000");
127
+ txt.setAttribute("font-size", String(fontSize));
128
+ txt.setAttribute("font-family", "-apple-system, sans-serif");
129
+ const tw = measureSvgTextWidth(getSvg(), text, fontSize, SVG_NS) + 12;
130
+ bg.setAttribute("x", String(x));
131
+ bg.setAttribute("y", String(y));
132
+ bg.setAttribute("width", String(tw));
133
+ bg.setAttribute("height", "24");
134
+ bg.setAttribute("fill", "#FFFFFF");
135
+ bg.setAttribute("rx", "4");
136
+ g.appendChild(bg);
137
+ g.appendChild(txt);
138
+ return g;
139
+ };
140
+ var handleMouseDown = (e) => {
141
+ if (e.target?.closest(".annotation")) return false;
142
+ deselectAll();
143
+ const p = getMousePos(e);
144
+ const svg2 = getSvg();
145
+ if (state.tool === "rect") {
146
+ const id = allocateAnnotationId();
147
+ const rect = document.createElementNS(SVG_NS, "rect");
148
+ rect.setAttribute("x", String(p.x));
149
+ rect.setAttribute("y", String(p.y));
150
+ rect.setAttribute("width", "0");
151
+ rect.setAttribute("height", "0");
152
+ rect.setAttribute("fill", "none");
153
+ rect.setAttribute("stroke", "#FF0000");
154
+ rect.setAttribute("stroke-width", "3");
155
+ rect.setAttribute("rx", "4");
156
+ rect.classList.add("annotation");
157
+ rect.dataset.id = id;
158
+ svg2.appendChild(rect);
159
+ state.dragState = {
160
+ type: "create-rect",
161
+ id,
162
+ el: rect,
163
+ sx: p.x,
164
+ sy: p.y
165
+ };
166
+ return true;
167
+ }
168
+ if (state.tool === "arrow") {
169
+ const id = allocateAnnotationId();
170
+ const line = document.createElementNS(SVG_NS, "line");
171
+ line.setAttribute("x1", String(p.x));
172
+ line.setAttribute("y1", String(p.y));
173
+ line.setAttribute("x2", String(p.x));
174
+ line.setAttribute("y2", String(p.y));
175
+ line.setAttribute("stroke", "#FF0000");
176
+ line.setAttribute("stroke-width", "3");
177
+ line.setAttribute("marker-end", "url(#kg-arrowhead)");
178
+ line.classList.add("annotation");
179
+ line.dataset.id = id;
180
+ svg2.appendChild(line);
181
+ state.dragState = {
182
+ type: "create-arrow",
183
+ id,
184
+ el: line,
185
+ sx: p.x,
186
+ sy: p.y
187
+ };
188
+ return true;
189
+ }
190
+ if (state.tool === "label") {
191
+ const id = allocateAnnotationId();
192
+ const input = document.createElement("input");
193
+ input.type = "text";
194
+ input.value = "";
195
+ input.placeholder = "Type label...";
196
+ input.style.cssText = "position:fixed;z-index:var(--kg-z-top);padding:4px 8px;background:#fff;border:none;border-radius:4px;color:#FF0000;font-size:14px;font-family:-apple-system,sans-serif;outline:2px solid #6366f1;min-width:80px;box-shadow:0 2px 8px rgba(0,0,0,0.2);";
197
+ input.style.left = `${e.clientX}px`;
198
+ input.style.top = `${e.clientY}px`;
199
+ document.documentElement.appendChild(input);
200
+ svg2.classList.remove("drawing");
201
+ setTimeout(() => input.focus(), 50);
202
+ let labelFinished = false;
203
+ const finishLabel = () => {
204
+ if (labelFinished) return;
205
+ labelFinished = true;
206
+ const text = input.value.trim();
207
+ input.remove();
208
+ svg2.classList.add("drawing");
209
+ if (!text) return;
210
+ const g = createLabelGroup(id, p.x, p.y, text);
211
+ svg2.appendChild(g);
212
+ state.annotations.push({ id, type: "label", x: p.x, y: p.y, text });
213
+ selectEl(g);
214
+ g.addEventListener("mousedown", (ev) => startMove(ev, id));
215
+ };
216
+ input.addEventListener("keydown", (ev) => {
217
+ if (ev.key === "Enter") finishLabel();
218
+ if (ev.key === "Escape") {
219
+ labelFinished = true;
220
+ input.remove();
221
+ svg2.classList.add("drawing");
222
+ }
223
+ });
224
+ input.addEventListener("blur", finishLabel);
225
+ return true;
226
+ }
227
+ return false;
228
+ };
229
+ var handleMouseMove = (e) => {
230
+ const drag = state.dragState;
231
+ if (!drag) return false;
232
+ const p = getMousePos(e);
233
+ if (drag.type === "create-rect") {
234
+ drag.el.setAttribute("x", String(Math.min(drag.sx ?? 0, p.x)));
235
+ drag.el.setAttribute("y", String(Math.min(drag.sy ?? 0, p.y)));
236
+ drag.el.setAttribute("width", String(Math.abs(p.x - (drag.sx ?? 0))));
237
+ drag.el.setAttribute("height", String(Math.abs(p.y - (drag.sy ?? 0))));
238
+ } else if (drag.type === "create-arrow") {
239
+ drag.el.setAttribute("x2", String(p.x));
240
+ drag.el.setAttribute("y2", String(p.y));
241
+ } else if (drag.type === "move") {
242
+ const a = state.annotations.find((ann) => ann.id === drag.id);
243
+ if (!a) return true;
244
+ const dx = p.x - (drag.lastX ?? 0);
245
+ const dy = p.y - (drag.lastY ?? 0);
246
+ drag.lastX = p.x;
247
+ drag.lastY = p.y;
248
+ if (a.type === "rect") {
249
+ a.x = (a.x ?? 0) + dx;
250
+ a.y = (a.y ?? 0) + dy;
251
+ drag.el.setAttribute("x", String(a.x));
252
+ drag.el.setAttribute("y", String(a.y));
253
+ } else if (a.type === "arrow") {
254
+ a.fromX = (a.fromX ?? 0) + dx;
255
+ a.fromY = (a.fromY ?? 0) + dy;
256
+ a.toX = (a.toX ?? 0) + dx;
257
+ a.toY = (a.toY ?? 0) + dy;
258
+ drag.el.setAttribute("x1", String(a.fromX));
259
+ drag.el.setAttribute("y1", String(a.fromY));
260
+ drag.el.setAttribute("x2", String(a.toX));
261
+ drag.el.setAttribute("y2", String(a.toY));
262
+ } else if (a.type === "label") {
263
+ a.x = (a.x ?? 0) + dx;
264
+ a.y = (a.y ?? 0) + dy;
265
+ const bg = drag.el.querySelector("rect");
266
+ const txt = drag.el.querySelector("text");
267
+ bg?.setAttribute("x", String(a.x));
268
+ bg?.setAttribute("y", String(a.y));
269
+ txt?.setAttribute("x", String(a.x + 6));
270
+ txt?.setAttribute("y", String(a.y + 16));
271
+ }
272
+ }
273
+ return true;
274
+ };
275
+ var handleMouseUp = (e) => {
276
+ const drag = state.dragState;
277
+ if (!drag) return false;
278
+ const p = getMousePos(e);
279
+ if (drag.type === "create-rect") {
280
+ const w = Math.abs(p.x - (drag.sx ?? 0));
281
+ const h = Math.abs(p.y - (drag.sy ?? 0));
282
+ if (w < 5 && h < 5) {
283
+ drag.el.remove();
284
+ } else {
285
+ const a = {
286
+ id: drag.id,
287
+ type: "rect",
288
+ x: Math.min(drag.sx ?? 0, p.x),
289
+ y: Math.min(drag.sy ?? 0, p.y),
290
+ width: w,
291
+ height: h
292
+ };
293
+ state.annotations.push(a);
294
+ selectEl(drag.el);
295
+ const capturedId = drag.id;
296
+ drag.el.addEventListener(
297
+ "mousedown",
298
+ (ev) => startMove(ev, capturedId)
299
+ );
300
+ }
301
+ } else if (drag.type === "create-arrow") {
302
+ const dist = Math.hypot(p.x - (drag.sx ?? 0), p.y - (drag.sy ?? 0));
303
+ if (dist < 5) {
304
+ drag.el.remove();
305
+ } else {
306
+ const a = {
307
+ id: drag.id,
308
+ type: "arrow",
309
+ fromX: drag.sx,
310
+ fromY: drag.sy,
311
+ toX: p.x,
312
+ toY: p.y
313
+ };
314
+ state.annotations.push(a);
315
+ selectEl(drag.el);
316
+ const capturedId = drag.id;
317
+ drag.el.addEventListener(
318
+ "mousedown",
319
+ (ev) => startMove(ev, capturedId)
320
+ );
321
+ }
322
+ }
323
+ state.dragState = null;
324
+ return true;
325
+ };
326
+ var handleKeyDown = (e) => {
327
+ if (e.key === "Delete" || e.key === "Backspace") {
328
+ if (isLabelInput(document.activeElement)) return;
329
+ deleteSelected();
330
+ }
331
+ };
332
+ var loadAnnotations = (decorations) => {
333
+ const dpr = window.devicePixelRatio || 1;
334
+ const svg2 = getSvg();
335
+ for (const d of decorations) {
336
+ const id = allocateAnnotationId();
337
+ if (d.type === "rect" && d.target) {
338
+ const rx = d.target.x / dpr;
339
+ const ry = d.target.y / dpr;
340
+ const rw = d.target.width / dpr;
341
+ const rh = d.target.height / dpr;
342
+ const rect = document.createElementNS(SVG_NS, "rect");
343
+ rect.setAttribute("x", String(rx));
344
+ rect.setAttribute("y", String(ry));
345
+ rect.setAttribute("width", String(rw));
346
+ rect.setAttribute("height", String(rh));
347
+ rect.setAttribute("fill", "none");
348
+ rect.setAttribute("stroke", d.style?.color ?? "#FF0000");
349
+ rect.setAttribute("stroke-width", "3");
350
+ rect.setAttribute("rx", "4");
351
+ rect.classList.add("annotation");
352
+ rect.dataset.id = id;
353
+ svg2.appendChild(rect);
354
+ state.annotations.push({
355
+ id,
356
+ type: "rect",
357
+ x: rx,
358
+ y: ry,
359
+ width: rw,
360
+ height: rh
361
+ });
362
+ rect.addEventListener("mousedown", (ev) => startMove(ev, id));
363
+ } else if (d.type === "arrow" && d.from && d.to) {
364
+ const ax1 = d.from.x / dpr;
365
+ const ay1 = d.from.y / dpr;
366
+ const ax2 = d.to.x / dpr;
367
+ const ay2 = d.to.y / dpr;
368
+ const line = document.createElementNS(SVG_NS, "line");
369
+ line.setAttribute("x1", String(ax1));
370
+ line.setAttribute("y1", String(ay1));
371
+ line.setAttribute("x2", String(ax2));
372
+ line.setAttribute("y2", String(ay2));
373
+ line.setAttribute("stroke", d.style?.color ?? "#FF0000");
374
+ line.setAttribute("stroke-width", "3");
375
+ line.setAttribute("marker-end", "url(#kg-arrowhead)");
376
+ line.classList.add("annotation");
377
+ line.dataset.id = id;
378
+ svg2.appendChild(line);
379
+ state.annotations.push({
380
+ id,
381
+ type: "arrow",
382
+ fromX: ax1,
383
+ fromY: ay1,
384
+ toX: ax2,
385
+ toY: ay2
386
+ });
387
+ line.addEventListener("mousedown", (ev) => startMove(ev, id));
388
+ } else if (d.type === "label" && d.position) {
389
+ const lx = d.position.x / dpr;
390
+ const ly = d.position.y / dpr;
391
+ const fontSize = (d.style?.fontSize ?? 14) / dpr;
392
+ const g = createLabelGroup(id, lx, ly, d.text ?? "", fontSize);
393
+ svg2.appendChild(g);
394
+ state.annotations.push({
395
+ id,
396
+ type: "label",
397
+ x: lx,
398
+ y: ly,
399
+ text: d.text
400
+ });
401
+ g.addEventListener("mousedown", (ev) => startMove(ev, id));
402
+ }
403
+ }
404
+ };
405
+ var serializeAnnotations = () => {
406
+ const dpr = window.devicePixelRatio || 1;
407
+ const s = Math.round;
408
+ return state.annotations.map((a) => {
409
+ if (a.type === "rect") {
410
+ return {
411
+ type: "rect",
412
+ target: {
413
+ x: s((a.x ?? 0) * dpr),
414
+ y: s((a.y ?? 0) * dpr),
415
+ width: s((a.width ?? 0) * dpr),
416
+ height: s((a.height ?? 0) * dpr)
417
+ },
418
+ style: { color: "#FF0000", strokeWidth: s(3 * dpr) }
419
+ };
420
+ }
421
+ if (a.type === "arrow") {
422
+ return {
423
+ type: "arrow",
424
+ from: {
425
+ x: s((a.fromX ?? 0) * dpr),
426
+ y: s((a.fromY ?? 0) * dpr)
427
+ },
428
+ to: {
429
+ x: s((a.toX ?? 0) * dpr),
430
+ y: s((a.toY ?? 0) * dpr)
431
+ },
432
+ style: { color: "#FF0000", strokeWidth: s(3 * dpr) }
433
+ };
434
+ }
435
+ if (a.type === "label") {
436
+ return {
437
+ type: "label",
438
+ text: a.text,
439
+ position: {
440
+ x: s((a.x ?? 0) * dpr),
441
+ y: s((a.y ?? 0) * dpr)
442
+ },
443
+ style: {
444
+ fontSize: s(14 * dpr),
445
+ color: "#FF0000",
446
+ background: "#FFFFFF"
447
+ }
448
+ };
449
+ }
450
+ return null;
451
+ }).filter((d) => d !== null);
452
+ };
453
+
454
+ // src/editor/inject-script/toolbar.ts
455
+ var TOOLBAR_HTML = `
24
456
  <style>
457
+ /* INT_MAX guarantees kagemusha UI sits above any host SPA dialog / modal. */
458
+ :root {
459
+ --kg-z-top: 2147483647;
460
+ --kg-z-below-top: 2147483646;
461
+ }
25
462
  #kagemusha-toolbar {
26
- position: fixed; top: 0; left: 0; right: 0; z-index: 999999;
463
+ position: fixed; top: 16px; left: 16px; z-index: var(--kg-z-top);
27
464
  background: #16213e; padding: 8px 16px; display: flex; align-items: center; gap: 10px;
28
- box-shadow: 0 2px 8px rgba(0,0,0,0.3); font-family: -apple-system, sans-serif;
465
+ box-shadow: 0 4px 16px rgba(0,0,0,0.35); font-family: -apple-system, sans-serif;
29
466
  flex-wrap: nowrap; overflow-x: auto;
467
+ border-radius: 10px;
468
+ max-width: calc(100vw - 32px);
469
+ }
470
+ #kagemusha-toolbar .kg-drag-handle {
471
+ cursor: move; padding: 4px 6px; margin: -2px 2px -2px -10px;
472
+ color: #888; font-size: 14px; line-height: 1; user-select: none;
473
+ border-right: 1px solid #2a2a4e;
30
474
  }
475
+ #kagemusha-toolbar .kg-drag-handle:hover { color: #fff; }
476
+ #kagemusha-toolbar.kg-dragging { opacity: 0.95; cursor: grabbing; }
31
477
  #kagemusha-toolbar button {
32
478
  padding: 6px 12px; border: 1px solid #444; border-radius: 6px;
33
479
  background: #1a1a2e; color: #fff; font-size: 13px; cursor: pointer;
@@ -35,6 +481,27 @@ toolbar.innerHTML = `
35
481
  #kagemusha-toolbar button:hover { background: #2a2a4e; }
36
482
  #kagemusha-toolbar button.active { background: #6366f1; border-color: #6366f1; }
37
483
  #kagemusha-toolbar button.cap-btn.active { background: #0ea5e9; border-color: #0ea5e9; }
484
+ #kagemusha-toolbar button#kg-record.active { background: #ef4444; border-color: #ef4444; }
485
+ #kagemusha-toolbar button.picking {
486
+ background: #0ea5e9; border-color: #0ea5e9; color: #fff;
487
+ box-shadow: 0 0 0 2px rgba(14,165,233,0.4);
488
+ animation: kg-picking-pulse 1.2s ease-in-out infinite;
489
+ }
490
+ @keyframes kg-picking-pulse {
491
+ 0%, 100% { box-shadow: 0 0 0 2px rgba(14,165,233,0.4); }
492
+ 50% { box-shadow: 0 0 0 6px rgba(14,165,233,0.1); }
493
+ }
494
+ #kagemusha-toolbar button:disabled { opacity: 0.4; cursor: not-allowed; }
495
+ #kagemusha-toolbar #kg-rec-group { display: none; align-items: center; gap: 10px; }
496
+ #kagemusha-toolbar #kg-rec-group.visible { display: flex; }
497
+ #kagemusha-toolbar #kg-steps-toggle.has-steps { background: #6366f1; border-color: #6366f1; }
498
+ #kagemusha-toolbar #kg-steps-toggle.open { background: #6366f1; border-color: #6366f1; }
499
+ .kagemusha-picker-outline {
500
+ position: fixed; border: 2px solid #0ea5e9;
501
+ background: rgba(14,165,233,0.08); pointer-events: none;
502
+ z-index: var(--kg-z-below-top); border-radius: 2px;
503
+ transition: all 60ms ease-out;
504
+ }
38
505
  #kagemusha-toolbar .sep { width: 1px; height: 24px; background: #444; }
39
506
  #kagemusha-toolbar .title { color: #888; font-size: 13px; }
40
507
  #kagemusha-toolbar .group-label { color: #7a89b0; font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; margin-right: -4px; }
@@ -42,7 +509,7 @@ toolbar.innerHTML = `
42
509
  #kagemusha-toolbar .save-btn:hover { background: #16a34a; }
43
510
  #kagemusha-svg-layer {
44
511
  position: absolute; top: 0; left: 0; width: 100%;
45
- z-index: 999998; pointer-events: none;
512
+ z-index: var(--kg-z-below-top); pointer-events: none;
46
513
  }
47
514
  #kagemusha-svg-layer.drawing { pointer-events: auto; cursor: crosshair; }
48
515
  #kagemusha-svg-layer.cropping { pointer-events: auto; cursor: crosshair; }
@@ -60,125 +527,146 @@ toolbar.innerHTML = `
60
527
  .kagemusha-hint {
61
528
  position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%);
62
529
  color: #fff; background: rgba(0,0,0,0.7); padding: 6px 16px; border-radius: 8px;
63
- font-size: 12px; z-index: 999999; font-family: -apple-system, sans-serif;
530
+ font-size: 12px; z-index: var(--kg-z-top); font-family: -apple-system, sans-serif;
64
531
  }
65
532
  </style>
66
- <span class="title">🥷</span>
533
+ <span class="kg-drag-handle" title="Drag to move toolbar">\u22EE\u22EE</span>
534
+ <span class="title">\u{1F977}</span>
535
+ <div class="sep"></div>
67
536
  <span class="group-label">Capture</span>
68
- <button id="kg-cap-full" class="cap-btn active">📷 Full</button>
69
- <button id="kg-cap-crop" class="cap-btn">✂️ Crop</button>
537
+ <button id="kg-cap-full" class="cap-btn active">\u{1F4F7} Full</button>
538
+ <button id="kg-cap-crop" class="cap-btn">\u2702\uFE0F Crop</button>
70
539
  <div class="sep"></div>
71
540
  <span class="group-label">Annotate</span>
72
- <button id="kg-tool-rect" class="active">▭ Rect</button>
73
- <button id="kg-tool-arrow">→ Arrow</button>
541
+ <button id="kg-tool-rect" class="active">\u25AD Rect</button>
542
+ <button id="kg-tool-arrow">\u2192 Arrow</button>
74
543
  <button id="kg-tool-label">T Label</button>
75
544
  <div class="sep"></div>
76
- <button id="kg-delete">🗑 Delete</button>
77
- <button class="save-btn" id="kg-save">💾 Save</button>
545
+ <span class="group-label">Pre-steps</span>
546
+ <button id="kg-record">\u{1F534} Record</button>
547
+ <div id="kg-rec-group">
548
+ <button id="kg-rec-wait">+ Wait</button>
549
+ <button id="kg-rec-wfs">+ WaitForSelector</button>
550
+ <button id="kg-rec-hover">+ Hover</button>
551
+ </div>
552
+ <button id="kg-steps-toggle">\u{1F4CB} Steps (0)</button>
553
+ <div class="sep"></div>
554
+ <button id="kg-delete">\u{1F5D1} Delete</button>
555
+ <button class="save-btn" id="kg-save">\u{1F4BE} Save</button>
78
556
  `;
79
- document.body.appendChild(toolbar);
80
- toolbarHeight =
81
- Math.ceil(toolbar.getBoundingClientRect().height) || TOOLBAR_HEIGHT_FALLBACK;
82
- document.body.style.paddingTop = `${toolbarHeight}px`;
83
- const hint = document.createElement("div");
84
- hint.className = "kagemusha-hint";
85
- hint.textContent =
86
- "Click and drag to add annotations. Click to select. Press Delete to remove.";
87
- document.body.appendChild(hint);
88
- // --- SVG LAYER ---
89
- const svg = document.createElementNS(svgNS, "svg");
90
- svg.id = "kagemusha-svg-layer";
91
- svg.classList.add("drawing");
92
- document.body.appendChild(svg);
93
- const updateSvgSize = () => {
94
- svg.setAttribute("width", String(window.innerWidth));
95
- svg.setAttribute("height", String(document.documentElement.scrollHeight));
96
- };
97
- updateSvgSize();
98
- window.addEventListener("resize", updateSvgSize);
99
- const defs = document.createElementNS(svgNS, "defs");
100
- defs.innerHTML =
101
- '<marker id="kg-arrowhead" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto" fill="#FF0000"><polygon points="0 0, 10 3.5, 0 7"/></marker>';
102
- svg.appendChild(defs);
103
- // Capture layer goes BEFORE annotations so it renders behind them
104
- const captureGroup = document.createElementNS(svgNS, "g");
105
- captureGroup.id = "kagemusha-capture-group";
106
- svg.appendChild(captureGroup);
107
- // --- HELPERS ---
108
- const getPos = (e) => ({ x: e.pageX, y: e.pageY });
109
- const deselectAll = () => {
110
- selectedId = null;
111
- svg.querySelectorAll(".annotation").forEach((el) => {
112
- el.classList.remove("selected");
557
+ var svgRef = null;
558
+ var cropOnEnter = () => {
559
+ };
560
+ var cropOnExit = () => {
561
+ };
562
+ var callbacks = null;
563
+ var initToolbar = (cb, svg2) => {
564
+ callbacks = cb;
565
+ svgRef = svg2;
566
+ const toolbar = document.createElement("div");
567
+ toolbar.id = "kagemusha-toolbar";
568
+ toolbar.innerHTML = TOOLBAR_HTML;
569
+ document.documentElement.appendChild(toolbar);
570
+ const hint = document.createElement("div");
571
+ hint.className = "kagemusha-hint";
572
+ hint.textContent = "Click and drag to add annotations. Click to select. Press Delete to remove.";
573
+ document.documentElement.appendChild(hint);
574
+ document.getElementById("kg-tool-rect")?.addEventListener("click", () => setTool("rect"));
575
+ document.getElementById("kg-tool-arrow")?.addEventListener("click", () => setTool("arrow"));
576
+ document.getElementById("kg-tool-label")?.addEventListener("click", () => setTool("label"));
577
+ document.getElementById("kg-cap-full")?.addEventListener("click", () => setCaptureMode("fullPage"));
578
+ document.getElementById("kg-cap-crop")?.addEventListener(
579
+ "click",
580
+ () => setCaptureMode("crop", { resetSelection: !state.captureCrop })
581
+ );
582
+ document.getElementById("kg-delete")?.addEventListener("click", () => cb.onDelete());
583
+ document.getElementById("kg-save")?.addEventListener("click", () => cb.onSave());
584
+ wireDragHandle(toolbar);
585
+ };
586
+ var wireDragHandle = (toolbar) => {
587
+ const handle = toolbar.querySelector(".kg-drag-handle");
588
+ if (!handle) return;
589
+ let dragging = false;
590
+ let offsetX = 0;
591
+ let offsetY = 0;
592
+ handle.addEventListener("mousedown", (e) => {
593
+ const m = e;
594
+ m.preventDefault();
595
+ const rect = toolbar.getBoundingClientRect();
596
+ offsetX = m.clientX - rect.left;
597
+ offsetY = m.clientY - rect.top;
598
+ dragging = true;
599
+ toolbar.classList.add("kg-dragging");
600
+ document.body.style.userSelect = "none";
113
601
  });
114
- };
115
- const selectEl = (el) => {
116
- deselectAll();
117
- selectedId = el.dataset.id ?? null;
118
- el.classList.add("selected");
119
- };
120
- const deleteSelected = () => {
121
- if (!selectedId)
122
- return;
123
- const el = svg.querySelector(`[data-id="${selectedId}"]`);
124
- if (el)
125
- el.remove();
126
- annotations = annotations.filter((a) => a.id !== selectedId);
127
- selectedId = null;
128
- };
129
- const startMove = (e, id) => {
130
- e.stopPropagation();
131
- const el = svg.querySelector(`[data-id="${id}"]`);
132
- if (!el)
133
- return;
134
- selectEl(el);
135
- const p = getPos(e);
136
- dragState = {
137
- type: "move",
138
- id,
139
- el: el,
140
- lastX: p.x,
141
- lastY: p.y,
142
- };
143
- };
144
- const measureTextWidth = (text, fontSize) => {
145
- const tmp = document.createElementNS(svgNS, "text");
146
- tmp.setAttribute("font-size", String(fontSize));
147
- tmp.setAttribute("font-family", "-apple-system, sans-serif");
148
- tmp.textContent = text;
149
- svg.appendChild(tmp);
150
- const width = tmp.getBBox().width;
151
- tmp.remove();
152
- return width;
153
- };
154
- const createLabelGroup = (id, x, y, text, fontSize = 14) => {
155
- const g = document.createElementNS(svgNS, "g");
156
- g.classList.add("annotation");
157
- g.dataset.id = id;
158
- const bg = document.createElementNS(svgNS, "rect");
159
- const txt = document.createElementNS(svgNS, "text");
160
- txt.textContent = text;
161
- txt.setAttribute("x", String(x + 6));
162
- txt.setAttribute("y", String(y + 16));
163
- txt.setAttribute("fill", "#FF0000");
164
- txt.setAttribute("font-size", String(fontSize));
165
- txt.setAttribute("font-family", "-apple-system, sans-serif");
166
- const tw = measureTextWidth(text, fontSize) + 12;
167
- bg.setAttribute("x", String(x));
168
- bg.setAttribute("y", String(y));
169
- bg.setAttribute("width", String(tw));
170
- bg.setAttribute("height", "24");
171
- bg.setAttribute("fill", "#FFFFFF");
172
- bg.setAttribute("rx", "4");
173
- g.appendChild(bg);
174
- g.appendChild(txt);
175
- return g;
176
- };
177
- // --- CAPTURE HELPERS ---
178
- const clearCaptureVisual = () => {
179
- captureGroup.replaceChildren();
180
- };
181
- const HANDLE_POSITIONS = [
602
+ document.addEventListener("mousemove", (e) => {
603
+ if (!dragging) return;
604
+ const x = Math.max(
605
+ 0,
606
+ Math.min(window.innerWidth - toolbar.offsetWidth, e.clientX - offsetX)
607
+ );
608
+ const y = Math.max(
609
+ 0,
610
+ Math.min(window.innerHeight - toolbar.offsetHeight, e.clientY - offsetY)
611
+ );
612
+ toolbar.style.left = `${x}px`;
613
+ toolbar.style.top = `${y}px`;
614
+ });
615
+ document.addEventListener("mouseup", () => {
616
+ if (!dragging) return;
617
+ dragging = false;
618
+ toolbar.classList.remove("kg-dragging");
619
+ document.body.style.userSelect = "";
620
+ });
621
+ };
622
+ var updateCaptureUi = () => {
623
+ document.querySelectorAll("#kagemusha-toolbar .cap-btn").forEach((b) => {
624
+ b.classList.remove("active");
625
+ });
626
+ const activeId = state.captureMode === "fullPage" ? "kg-cap-full" : "kg-cap-crop";
627
+ document.getElementById(activeId)?.classList.add("active");
628
+ if (svgRef) {
629
+ svgRef.classList.toggle("cropping", state.tool === "crop");
630
+ svgRef.classList.toggle(
631
+ "drawing",
632
+ state.tool === "rect" || state.tool === "arrow" || state.tool === "label"
633
+ );
634
+ }
635
+ };
636
+ var setTool = (t) => {
637
+ state.tool = t;
638
+ document.querySelectorAll(
639
+ "#kg-tool-rect, #kg-tool-arrow, #kg-tool-label"
640
+ ).forEach((b) => {
641
+ b.classList.remove("active");
642
+ });
643
+ if (t === "rect" || t === "arrow" || t === "label") {
644
+ document.getElementById(`kg-tool-${t}`)?.classList.add("active");
645
+ }
646
+ updateCaptureUi();
647
+ callbacks?.onToolChange(t);
648
+ };
649
+ var setCaptureMode = (mode, opts = {}) => {
650
+ state.captureMode = mode;
651
+ if (mode === "fullPage") {
652
+ state.captureCrop = null;
653
+ cropOnExit();
654
+ setTool("rect");
655
+ } else {
656
+ if (opts.resetSelection) state.captureCrop = null;
657
+ cropOnEnter();
658
+ state.tool = "crop";
659
+ updateCaptureUi();
660
+ }
661
+ callbacks?.onToolChange(state.tool);
662
+ };
663
+ var registerCropHooks = (onEnter, onExit) => {
664
+ cropOnEnter = onEnter;
665
+ cropOnExit = onExit;
666
+ };
667
+
668
+ // src/editor/inject-script/crop.ts
669
+ var HANDLE_POSITIONS = [
182
670
  { handle: "nw", pos: (c) => ({ cx: c.x, cy: c.y }) },
183
671
  { handle: "n", pos: (c) => ({ cx: c.x + c.w / 2, cy: c.y }) },
184
672
  { handle: "ne", pos: (c) => ({ cx: c.x + c.w, cy: c.y }) },
@@ -186,583 +674,644 @@ const HANDLE_POSITIONS = [
186
674
  { handle: "se", pos: (c) => ({ cx: c.x + c.w, cy: c.y + c.h }) },
187
675
  { handle: "s", pos: (c) => ({ cx: c.x + c.w / 2, cy: c.y + c.h }) },
188
676
  { handle: "sw", pos: (c) => ({ cx: c.x, cy: c.y + c.h }) },
189
- { handle: "w", pos: (c) => ({ cx: c.x, cy: c.y + c.h / 2 }) },
190
- ];
191
- const redrawCropVisual = () => {
192
- if (captureMode !== "crop" || !captureCrop) {
193
- clearCaptureVisual();
194
- return;
195
- }
196
- clearCaptureVisual();
197
- const r = document.createElementNS(svgNS, "rect");
198
- r.setAttribute("x", String(captureCrop.x));
199
- r.setAttribute("y", String(captureCrop.y));
200
- r.setAttribute("width", String(captureCrop.w));
201
- r.setAttribute("height", String(captureCrop.h));
677
+ { handle: "w", pos: (c) => ({ cx: c.x, cy: c.y + c.h / 2 }) }
678
+ ];
679
+ var clearVisual = () => {
680
+ getCaptureGroup().replaceChildren();
681
+ };
682
+ var redrawVisual = () => {
683
+ if (state.captureMode !== "crop" || !state.captureCrop) {
684
+ clearVisual();
685
+ return;
686
+ }
687
+ clearVisual();
688
+ const g = getCaptureGroup();
689
+ const c = state.captureCrop;
690
+ const r = document.createElementNS(SVG_NS, "rect");
691
+ r.setAttribute("x", String(c.x));
692
+ r.setAttribute("y", String(c.y));
693
+ r.setAttribute("width", String(c.w));
694
+ r.setAttribute("height", String(c.h));
202
695
  r.setAttribute("class", "capture-crop-box");
203
- captureGroup.appendChild(r);
696
+ g.appendChild(r);
204
697
  for (const { handle, pos } of HANDLE_POSITIONS) {
205
- const { cx, cy } = pos(captureCrop);
206
- const h = document.createElementNS(svgNS, "rect");
207
- h.setAttribute("x", String(cx - HANDLE_SIZE / 2));
208
- h.setAttribute("y", String(cy - HANDLE_SIZE / 2));
209
- h.setAttribute("width", String(HANDLE_SIZE));
210
- h.setAttribute("height", String(HANDLE_SIZE));
211
- h.setAttribute("class", `crop-handle ${handle}`);
212
- h.dataset.handle = handle;
213
- captureGroup.appendChild(h);
214
- }
215
- };
216
- const updateCaptureUi = () => {
217
- document
218
- .querySelectorAll("#kagemusha-toolbar .cap-btn")
219
- .forEach((b) => {
220
- b.classList.remove("active");
221
- });
222
- const activeId = captureMode === "fullPage" ? "kg-cap-full" : "kg-cap-crop";
223
- document.getElementById(activeId)?.classList.add("active");
224
- svg.classList.toggle("cropping", tool === "crop");
225
- svg.classList.toggle("drawing", tool === "rect" || tool === "arrow" || tool === "label");
226
- };
227
- const setCaptureMode = (mode, opts = {}) => {
228
- captureMode = mode;
229
- if (mode === "fullPage") {
230
- captureCrop = null;
231
- tool = "rect";
232
- clearCaptureVisual();
233
- }
234
- else {
235
- if (opts.resetSelection)
236
- captureCrop = null;
237
- tool = "crop";
238
- redrawCropVisual();
239
- }
240
- // Mark the matching annotation tool button too
241
- document
242
- .querySelectorAll("#kg-tool-rect, #kg-tool-arrow, #kg-tool-label")
243
- .forEach((b) => {
244
- b.classList.remove("active");
245
- });
246
- if (tool === "rect" || tool === "arrow" || tool === "label") {
247
- document.getElementById(`kg-tool-${tool}`)?.classList.add("active");
698
+ const { cx, cy } = pos(c);
699
+ const h = document.createElementNS(SVG_NS, "rect");
700
+ h.setAttribute("x", String(cx - HANDLE_SIZE / 2));
701
+ h.setAttribute("y", String(cy - HANDLE_SIZE / 2));
702
+ h.setAttribute("width", String(HANDLE_SIZE));
703
+ h.setAttribute("height", String(HANDLE_SIZE));
704
+ h.setAttribute("class", `crop-handle ${handle}`);
705
+ h.dataset.handle = handle;
706
+ g.appendChild(h);
248
707
  }
249
- updateCaptureUi();
250
- deselectAll();
251
- };
252
- // --- TOOLS ---
253
- const setTool = (t) => {
254
- tool = t;
255
- document
256
- .querySelectorAll("#kg-tool-rect, #kg-tool-arrow, #kg-tool-label")
257
- .forEach((b) => {
258
- b.classList.remove("active");
259
- });
260
- document.getElementById(`kg-tool-${t}`)?.classList.add("active");
261
- // Switching to an annotation tool exits capture-edit interaction
262
- // (capture settings themselves are preserved).
263
- updateCaptureUi();
264
- deselectAll();
265
- };
266
- document
267
- .getElementById("kg-tool-rect")
268
- ?.addEventListener("click", () => setTool("rect"));
269
- document
270
- .getElementById("kg-tool-arrow")
271
- ?.addEventListener("click", () => setTool("arrow"));
272
- document
273
- .getElementById("kg-tool-label")
274
- ?.addEventListener("click", () => setTool("label"));
275
- document
276
- .getElementById("kg-cap-full")
277
- ?.addEventListener("click", () => setCaptureMode("fullPage"));
278
- document
279
- .getElementById("kg-cap-crop")
280
- ?.addEventListener("click", () => setCaptureMode("crop", { resetSelection: !captureCrop }));
281
- document.getElementById("kg-delete")?.addEventListener("click", deleteSelected);
282
- document.addEventListener("keydown", (e) => {
283
- if (e.key === "Delete" || e.key === "Backspace") {
284
- if (document.activeElement?.tagName === "INPUT")
285
- return;
286
- deleteSelected();
287
- }
288
- });
289
- // --- MOUSE: CREATE ---
290
- svg.addEventListener("mousedown", (e) => {
291
- if (e.target?.closest(".annotation"))
292
- return;
293
- const p = getPos(e);
294
- if (tool === "crop") {
295
- e.preventDefault();
296
- deselectAll();
297
- const target = e.target;
298
- const handleAttr = target.dataset?.handle;
299
- // Resize from a handle
300
- if (handleAttr && captureCrop) {
301
- cropDragState = {
302
- kind: "resize",
303
- handle: handleAttr,
304
- sx: p.x,
305
- sy: p.y,
306
- orig: { ...captureCrop },
307
- };
308
- return;
309
- }
310
- // Move the existing crop body
311
- if (target?.classList?.contains("capture-crop-box") && captureCrop) {
312
- cropDragState = {
313
- kind: "move",
314
- sx: p.x,
315
- sy: p.y,
316
- orig: { ...captureCrop },
317
- };
318
- return;
319
- }
320
- // Otherwise: drag-to-create new crop (replaces existing)
321
- cropDragState = { kind: "create", sx: p.x, sy: p.y };
322
- captureCrop = null;
323
- clearCaptureVisual();
324
- const r = document.createElementNS(svgNS, "rect");
325
- r.setAttribute("x", String(p.x));
326
- r.setAttribute("y", String(p.y));
327
- r.setAttribute("width", "0");
328
- r.setAttribute("height", "0");
329
- r.setAttribute("class", "capture-crop-box");
330
- captureGroup.appendChild(r);
331
- return;
708
+ };
709
+ var initCrop = () => {
710
+ registerCropHooks(
711
+ // onEnter: redraw existing selection (if any)
712
+ () => redrawVisual(),
713
+ // onExit: clear visualization
714
+ () => clearVisual()
715
+ );
716
+ };
717
+ var handleMouseDown2 = (e) => {
718
+ if (state.tool !== "crop") return false;
719
+ e.preventDefault();
720
+ const p = getMousePos(e);
721
+ const target = e.target;
722
+ const handleAttr = target.dataset?.handle;
723
+ if (handleAttr && state.captureCrop) {
724
+ state.cropDragState = {
725
+ kind: "resize",
726
+ handle: handleAttr,
727
+ sx: p.x,
728
+ sy: p.y,
729
+ orig: { ...state.captureCrop }
730
+ };
731
+ return true;
332
732
  }
333
- deselectAll();
334
- if (tool === "rect") {
335
- const id = `a${nextId++}`;
336
- const rect = document.createElementNS(svgNS, "rect");
337
- rect.setAttribute("x", String(p.x));
338
- rect.setAttribute("y", String(p.y));
339
- rect.setAttribute("width", "0");
340
- rect.setAttribute("height", "0");
341
- rect.setAttribute("fill", "none");
342
- rect.setAttribute("stroke", "#FF0000");
343
- rect.setAttribute("stroke-width", "3");
344
- rect.setAttribute("rx", "4");
345
- rect.classList.add("annotation");
346
- rect.dataset.id = id;
347
- svg.appendChild(rect);
348
- dragState = { type: "create-rect", id, el: rect, sx: p.x, sy: p.y };
349
- }
350
- else if (tool === "arrow") {
351
- const id = `a${nextId++}`;
352
- const line = document.createElementNS(svgNS, "line");
353
- line.setAttribute("x1", String(p.x));
354
- line.setAttribute("y1", String(p.y));
355
- line.setAttribute("x2", String(p.x));
356
- line.setAttribute("y2", String(p.y));
357
- line.setAttribute("stroke", "#FF0000");
358
- line.setAttribute("stroke-width", "3");
359
- line.setAttribute("marker-end", "url(#kg-arrowhead)");
360
- line.classList.add("annotation");
361
- line.dataset.id = id;
362
- svg.appendChild(line);
363
- dragState = { type: "create-arrow", id, el: line, sx: p.x, sy: p.y };
364
- }
365
- else if (tool === "label") {
366
- const id = `a${nextId++}`;
367
- const input = document.createElement("input");
368
- input.type = "text";
369
- input.value = "";
370
- input.placeholder = "Type label...";
371
- input.style.cssText =
372
- "position:fixed;z-index:9999999;padding:4px 8px;background:#fff;border:none;border-radius:4px;color:#FF0000;font-size:14px;font-family:-apple-system,sans-serif;outline:2px solid #6366f1;min-width:80px;box-shadow:0 2px 8px rgba(0,0,0,0.2);";
373
- input.style.left = `${e.clientX}px`;
374
- input.style.top = `${e.clientY}px`;
375
- document.body.appendChild(input);
376
- svg.classList.remove("drawing");
377
- setTimeout(() => input.focus(), 50);
378
- let labelFinished = false;
379
- const finishLabel = () => {
380
- if (labelFinished)
381
- return;
382
- labelFinished = true;
383
- const text = input.value.trim();
384
- input.remove();
385
- svg.classList.add("drawing");
386
- if (!text)
387
- return;
388
- const g = createLabelGroup(id, p.x, p.y, text);
389
- svg.appendChild(g);
390
- annotations.push({ id, type: "label", x: p.x, y: p.y, text });
391
- selectEl(g);
392
- g.addEventListener("mousedown", (ev) => startMove(ev, id));
393
- };
394
- input.addEventListener("keydown", (ev) => {
395
- if (ev.key === "Enter")
396
- finishLabel();
397
- if (ev.key === "Escape") {
398
- labelFinished = true;
399
- input.remove();
400
- svg.classList.add("drawing");
401
- }
402
- });
403
- input.addEventListener("blur", finishLabel);
404
- }
405
- });
406
- // --- MOUSE: DRAG ---
407
- document.addEventListener("mousemove", (e) => {
408
- if (cropDragState) {
409
- const p = getPos(e);
410
- if (cropDragState.kind === "create") {
411
- const x = Math.min(cropDragState.sx, p.x);
412
- const y = Math.min(cropDragState.sy, p.y);
413
- const w = Math.abs(p.x - cropDragState.sx);
414
- const h = Math.abs(p.y - cropDragState.sy);
415
- const r = captureGroup.firstChild;
416
- if (r) {
417
- r.setAttribute("x", String(x));
418
- r.setAttribute("y", String(y));
419
- r.setAttribute("width", String(w));
420
- r.setAttribute("height", String(h));
421
- }
422
- return;
423
- }
424
- if (cropDragState.kind === "move") {
425
- const dx = p.x - cropDragState.sx;
426
- const dy = p.y - cropDragState.sy;
427
- captureCrop = {
428
- x: cropDragState.orig.x + dx,
429
- y: cropDragState.orig.y + dy,
430
- w: cropDragState.orig.w,
431
- h: cropDragState.orig.h,
432
- };
433
- redrawCropVisual();
434
- return;
435
- }
436
- // resize
437
- const dx = p.x - cropDragState.sx;
438
- const dy = p.y - cropDragState.sy;
439
- const o = cropDragState.orig;
440
- let nx = o.x;
441
- let ny = o.y;
442
- let nw = o.w;
443
- let nh = o.h;
444
- const h = cropDragState.handle;
445
- if (h.includes("w")) {
446
- nx = o.x + dx;
447
- nw = o.w - dx;
448
- }
449
- if (h.includes("e")) {
450
- nw = o.w + dx;
451
- }
452
- if (h.includes("n")) {
453
- ny = o.y + dy;
454
- nh = o.h - dy;
455
- }
456
- if (h.includes("s")) {
457
- nh = o.h + dy;
458
- }
459
- // Keep min size and prevent flip-through
460
- if (nw < MIN_CROP) {
461
- if (h.includes("w"))
462
- nx = o.x + (o.w - MIN_CROP);
463
- nw = MIN_CROP;
464
- }
465
- if (nh < MIN_CROP) {
466
- if (h.includes("n"))
467
- ny = o.y + (o.h - MIN_CROP);
468
- nh = MIN_CROP;
469
- }
470
- captureCrop = { x: nx, y: ny, w: nw, h: nh };
471
- redrawCropVisual();
472
- return;
473
- }
474
- if (!dragState)
475
- return;
476
- const p = getPos(e);
477
- if (dragState.type === "create-rect") {
478
- dragState.el.setAttribute("x", String(Math.min(dragState.sx ?? 0, p.x)));
479
- dragState.el.setAttribute("y", String(Math.min(dragState.sy ?? 0, p.y)));
480
- dragState.el.setAttribute("width", String(Math.abs(p.x - (dragState.sx ?? 0))));
481
- dragState.el.setAttribute("height", String(Math.abs(p.y - (dragState.sy ?? 0))));
482
- }
483
- else if (dragState.type === "create-arrow") {
484
- dragState.el.setAttribute("x2", String(p.x));
485
- dragState.el.setAttribute("y2", String(p.y));
486
- }
487
- else if (dragState.type === "move") {
488
- const a = annotations.find((ann) => ann.id === dragState?.id);
489
- if (!a)
490
- return;
491
- const dx = p.x - (dragState.lastX ?? 0);
492
- const dy = p.y - (dragState.lastY ?? 0);
493
- dragState.lastX = p.x;
494
- dragState.lastY = p.y;
495
- if (a.type === "rect") {
496
- a.x = (a.x ?? 0) + dx;
497
- a.y = (a.y ?? 0) + dy;
498
- dragState.el.setAttribute("x", String(a.x));
499
- dragState.el.setAttribute("y", String(a.y));
500
- }
501
- else if (a.type === "arrow") {
502
- a.fromX = (a.fromX ?? 0) + dx;
503
- a.fromY = (a.fromY ?? 0) + dy;
504
- a.toX = (a.toX ?? 0) + dx;
505
- a.toY = (a.toY ?? 0) + dy;
506
- dragState.el.setAttribute("x1", String(a.fromX));
507
- dragState.el.setAttribute("y1", String(a.fromY));
508
- dragState.el.setAttribute("x2", String(a.toX));
509
- dragState.el.setAttribute("y2", String(a.toY));
510
- }
511
- else if (a.type === "label") {
512
- a.x = (a.x ?? 0) + dx;
513
- a.y = (a.y ?? 0) + dy;
514
- const bg = dragState.el.querySelector("rect");
515
- const txt = dragState.el.querySelector("text");
516
- bg?.setAttribute("x", String(a.x));
517
- bg?.setAttribute("y", String(a.y));
518
- txt?.setAttribute("x", String(a.x + 6));
519
- txt?.setAttribute("y", String(a.y + 16));
520
- }
733
+ if (target?.classList?.contains("capture-crop-box") && state.captureCrop) {
734
+ state.cropDragState = {
735
+ kind: "move",
736
+ sx: p.x,
737
+ sy: p.y,
738
+ orig: { ...state.captureCrop }
739
+ };
740
+ return true;
521
741
  }
522
- });
523
- // --- MOUSE: UP ---
524
- document.addEventListener("mouseup", (e) => {
525
- if (cropDragState) {
526
- if (cropDragState.kind === "create") {
527
- const p = getPos(e);
528
- const w = Math.abs(p.x - cropDragState.sx);
529
- const h = Math.abs(p.y - cropDragState.sy);
530
- if (w < 5 || h < 5) {
531
- cropDragState = null;
532
- redrawCropVisual();
533
- return;
534
- }
535
- captureCrop = {
536
- x: Math.min(cropDragState.sx, p.x),
537
- y: Math.min(cropDragState.sy, p.y),
538
- w,
539
- h,
540
- };
541
- }
542
- // move / resize: state is already applied via redrawCropVisual on mousemove
543
- cropDragState = null;
544
- redrawCropVisual();
545
- return;
546
- }
547
- if (!dragState)
548
- return;
549
- const p = getPos(e);
550
- if (dragState.type === "create-rect") {
551
- const w = Math.abs(p.x - (dragState.sx ?? 0));
552
- const h = Math.abs(p.y - (dragState.sy ?? 0));
553
- if (w < 5 && h < 5) {
554
- dragState.el.remove();
555
- }
556
- else {
557
- const a = {
558
- id: dragState.id,
559
- type: "rect",
560
- x: Math.min(dragState.sx ?? 0, p.x),
561
- y: Math.min(dragState.sy ?? 0, p.y),
562
- width: w,
563
- height: h,
564
- };
565
- annotations.push(a);
566
- selectEl(dragState.el);
567
- const capturedId = dragState.id;
568
- dragState.el.addEventListener("mousedown", (ev) => startMove(ev, capturedId));
569
- }
742
+ state.cropDragState = { kind: "create", sx: p.x, sy: p.y };
743
+ state.captureCrop = null;
744
+ clearVisual();
745
+ const r = document.createElementNS(SVG_NS, "rect");
746
+ r.setAttribute("x", String(p.x));
747
+ r.setAttribute("y", String(p.y));
748
+ r.setAttribute("width", "0");
749
+ r.setAttribute("height", "0");
750
+ r.setAttribute("class", "capture-crop-box");
751
+ getCaptureGroup().appendChild(r);
752
+ return true;
753
+ };
754
+ var handleMouseMove2 = (e) => {
755
+ const cds = state.cropDragState;
756
+ if (!cds) return false;
757
+ const p = getMousePos(e);
758
+ if (cds.kind === "create") {
759
+ const x = Math.min(cds.sx, p.x);
760
+ const y = Math.min(cds.sy, p.y);
761
+ const w = Math.abs(p.x - cds.sx);
762
+ const h2 = Math.abs(p.y - cds.sy);
763
+ const r = getCaptureGroup().firstChild;
764
+ if (r) {
765
+ r.setAttribute("x", String(x));
766
+ r.setAttribute("y", String(y));
767
+ r.setAttribute("width", String(w));
768
+ r.setAttribute("height", String(h2));
769
+ }
770
+ return true;
570
771
  }
571
- else if (dragState.type === "create-arrow") {
572
- const dist = Math.hypot(p.x - (dragState.sx ?? 0), p.y - (dragState.sy ?? 0));
573
- if (dist < 5) {
574
- dragState.el.remove();
575
- }
576
- else {
577
- const a = {
578
- id: dragState.id,
579
- type: "arrow",
580
- fromX: dragState.sx,
581
- fromY: dragState.sy,
582
- toX: p.x,
583
- toY: p.y,
584
- };
585
- annotations.push(a);
586
- selectEl(dragState.el);
587
- const capturedId = dragState.id;
588
- dragState.el.addEventListener("mousedown", (ev) => startMove(ev, capturedId));
589
- }
772
+ if (cds.kind === "move") {
773
+ const dx2 = p.x - cds.sx;
774
+ const dy2 = p.y - cds.sy;
775
+ state.captureCrop = {
776
+ x: cds.orig.x + dx2,
777
+ y: cds.orig.y + dy2,
778
+ w: cds.orig.w,
779
+ h: cds.orig.h
780
+ };
781
+ redrawVisual();
782
+ return true;
590
783
  }
591
- dragState = null;
592
- });
593
- // --- LOAD EXISTING ---
594
- _win.__kagemusha_loadAnnotations = (decorations) => {
595
- const dpr = window.devicePixelRatio || 1;
596
- for (const d of decorations) {
597
- const id = `a${nextId++}`;
598
- if (d.type === "rect" && d.target) {
599
- const rx = d.target.x / dpr;
600
- const ry = d.target.y / dpr + toolbarHeight;
601
- const rw = d.target.width / dpr;
602
- const rh = d.target.height / dpr;
603
- const rect = document.createElementNS(svgNS, "rect");
604
- rect.setAttribute("x", String(rx));
605
- rect.setAttribute("y", String(ry));
606
- rect.setAttribute("width", String(rw));
607
- rect.setAttribute("height", String(rh));
608
- rect.setAttribute("fill", "none");
609
- rect.setAttribute("stroke", d.style?.color ?? "#FF0000");
610
- rect.setAttribute("stroke-width", "3");
611
- rect.setAttribute("rx", "4");
612
- rect.classList.add("annotation");
613
- rect.dataset.id = id;
614
- svg.appendChild(rect);
615
- annotations.push({
616
- id,
617
- type: "rect",
618
- x: rx,
619
- y: ry,
620
- width: rw,
621
- height: rh,
622
- });
623
- rect.addEventListener("mousedown", (ev) => startMove(ev, id));
624
- }
625
- else if (d.type === "arrow" && d.from && d.to) {
626
- const ax1 = d.from.x / dpr;
627
- const ay1 = d.from.y / dpr + toolbarHeight;
628
- const ax2 = d.to.x / dpr;
629
- const ay2 = d.to.y / dpr + toolbarHeight;
630
- const line = document.createElementNS(svgNS, "line");
631
- line.setAttribute("x1", String(ax1));
632
- line.setAttribute("y1", String(ay1));
633
- line.setAttribute("x2", String(ax2));
634
- line.setAttribute("y2", String(ay2));
635
- line.setAttribute("stroke", d.style?.color ?? "#FF0000");
636
- line.setAttribute("stroke-width", "3");
637
- line.setAttribute("marker-end", "url(#kg-arrowhead)");
638
- line.classList.add("annotation");
639
- line.dataset.id = id;
640
- svg.appendChild(line);
641
- annotations.push({
642
- id,
643
- type: "arrow",
644
- fromX: ax1,
645
- fromY: ay1,
646
- toX: ax2,
647
- toY: ay2,
648
- });
649
- line.addEventListener("mousedown", (ev) => startMove(ev, id));
650
- }
651
- else if (d.type === "label" && d.position) {
652
- const lx = d.position.x / dpr;
653
- const ly = d.position.y / dpr + toolbarHeight;
654
- const fontSize = (d.style?.fontSize ?? 14) / dpr;
655
- const g = createLabelGroup(id, lx, ly, d.text ?? "", fontSize);
656
- svg.appendChild(g);
657
- annotations.push({ id, type: "label", x: lx, y: ly, text: d.text });
658
- g.addEventListener("mousedown", (ev) => startMove(ev, id));
659
- }
784
+ const dx = p.x - cds.sx;
785
+ const dy = p.y - cds.sy;
786
+ const o = cds.orig;
787
+ let nx = o.x;
788
+ let ny = o.y;
789
+ let nw = o.w;
790
+ let nh = o.h;
791
+ const h = cds.handle;
792
+ if (h.includes("w")) {
793
+ nx = o.x + dx;
794
+ nw = o.w - dx;
660
795
  }
661
- };
662
- // --- LOAD EXISTING CAPTURE CONFIG ---
663
- _win.__kagemusha_loadCapture = (capture) => {
664
- if (!capture ||
665
- !["fullPage", "crop"].includes(capture.mode ?? "")) {
666
- // Unknown / legacy mode (e.g. removed "selector") — fall back to fullPage
667
- setCaptureMode("fullPage");
668
- setTool("rect");
669
- return;
796
+ if (h.includes("e")) {
797
+ nw = o.w + dx;
798
+ }
799
+ if (h.includes("n")) {
800
+ ny = o.y + dy;
801
+ nh = o.h - dy;
802
+ }
803
+ if (h.includes("s")) {
804
+ nh = o.h + dy;
805
+ }
806
+ if (nw < MIN_CROP) {
807
+ if (h.includes("w")) nx = o.x + (o.w - MIN_CROP);
808
+ nw = MIN_CROP;
809
+ }
810
+ if (nh < MIN_CROP) {
811
+ if (h.includes("n")) ny = o.y + (o.h - MIN_CROP);
812
+ nh = MIN_CROP;
813
+ }
814
+ state.captureCrop = { x: nx, y: ny, w: nw, h: nh };
815
+ redrawVisual();
816
+ return true;
817
+ };
818
+ var handleMouseUp2 = (e) => {
819
+ const cds = state.cropDragState;
820
+ if (!cds) return false;
821
+ if (cds.kind === "create") {
822
+ const p = getMousePos(e);
823
+ const w = Math.abs(p.x - cds.sx);
824
+ const h = Math.abs(p.y - cds.sy);
825
+ if (w < 5 || h < 5) {
826
+ state.cropDragState = null;
827
+ redrawVisual();
828
+ return true;
829
+ }
830
+ state.captureCrop = {
831
+ x: Math.min(cds.sx, p.x),
832
+ y: Math.min(cds.sy, p.y),
833
+ w,
834
+ h
835
+ };
836
+ }
837
+ state.cropDragState = null;
838
+ redrawVisual();
839
+ return true;
840
+ };
841
+ var loadCapture = (capture, setMode) => {
842
+ if (!capture || !["fullPage", "crop"].includes(capture.mode)) {
843
+ setMode("fullPage");
844
+ return;
670
845
  }
671
846
  if (capture.mode === "fullPage") {
672
- setCaptureMode("fullPage");
673
- // Leave annotation tool active after load
674
- setTool("rect");
675
- return;
847
+ setMode("fullPage");
848
+ return;
676
849
  }
677
850
  if (capture.mode === "crop") {
678
- // crop is stored in page CSS pixels (not DPR-scaled); shift by toolbar for display
679
- const sx = capture.crop.start.x;
680
- const sy = capture.crop.start.y + toolbarHeight;
681
- const ex = capture.crop.end.x;
682
- const ey = capture.crop.end.y + toolbarHeight;
683
- captureCrop = { x: sx, y: sy, w: ex - sx, h: ey - sy };
684
- captureMode = "crop";
685
- tool = "rect";
686
- redrawCropVisual();
687
- updateCaptureUi();
688
- }
689
- };
690
- // --- SAVE ---
691
- const save = () => {
692
- if (captureMode === "crop" && !captureCrop) {
693
- alert("Crop mode is active but no area is drawn.\nDrag to define an area, or switch to Full Page.");
694
- return;
851
+ const sx = capture.crop.start.x;
852
+ const sy = capture.crop.start.y;
853
+ const ex = capture.crop.end.x;
854
+ const ey = capture.crop.end.y;
855
+ state.captureCrop = { x: sx, y: sy, w: ex - sx, h: ey - sy };
856
+ state.captureMode = "crop";
857
+ setMode("crop");
858
+ redrawVisual();
695
859
  }
696
- const dpr = window.devicePixelRatio || 1;
697
- const s = Math.round;
698
- const decorations = annotations
699
- .map((a) => {
700
- if (a.type === "rect") {
701
- return {
702
- type: "rect",
703
- target: {
704
- x: s((a.x ?? 0) * dpr),
705
- y: s(((a.y ?? 0) - toolbarHeight) * dpr),
706
- width: s((a.width ?? 0) * dpr),
707
- height: s((a.height ?? 0) * dpr),
708
- },
709
- style: { color: "#FF0000", strokeWidth: s(3 * dpr) },
710
- };
711
- }
712
- if (a.type === "arrow") {
713
- return {
714
- type: "arrow",
715
- from: {
716
- x: s((a.fromX ?? 0) * dpr),
717
- y: s(((a.fromY ?? 0) - toolbarHeight) * dpr),
718
- },
719
- to: {
720
- x: s((a.toX ?? 0) * dpr),
721
- y: s(((a.toY ?? 0) - toolbarHeight) * dpr),
722
- },
723
- style: { color: "#FF0000", strokeWidth: s(3 * dpr) },
724
- };
860
+ };
861
+ var serializeCapture = () => {
862
+ if (state.captureMode === "crop" && state.captureCrop) {
863
+ const c = state.captureCrop;
864
+ return {
865
+ mode: "crop",
866
+ crop: {
867
+ start: { x: Math.round(c.x), y: Math.round(c.y) },
868
+ end: {
869
+ x: Math.round(c.x + c.w),
870
+ y: Math.round(c.y + c.h)
871
+ }
725
872
  }
726
- if (a.type === "label") {
727
- return {
728
- type: "label",
729
- text: a.text,
730
- position: {
731
- x: s((a.x ?? 0) * dpr),
732
- y: s(((a.y ?? 0) - toolbarHeight) * dpr),
733
- },
734
- style: {
735
- fontSize: s(14 * dpr),
736
- color: "#FF0000",
737
- background: "#FFFFFF",
738
- },
739
- };
873
+ };
874
+ }
875
+ return { mode: "fullPage" };
876
+ };
877
+
878
+ // src/editor/inject-script/selector.ts
879
+ var escapeQuotes = (s) => s.replace(/"/g, '\\"');
880
+ var inferRole = (el) => {
881
+ const tag = el.tagName;
882
+ if (tag === "BUTTON") return "button";
883
+ if (tag === "A" && el.hasAttribute("href")) return "link";
884
+ if (tag === "INPUT") {
885
+ const type = el.type;
886
+ if (type === "button" || type === "submit") return "button";
887
+ if (type === "checkbox") return "checkbox";
888
+ if (type === "radio") return "radio";
889
+ }
890
+ return null;
891
+ };
892
+ var isInteractiveAncestor = (el) => {
893
+ let cur = el;
894
+ while (cur) {
895
+ if (cur.tagName === "BUTTON" || cur.tagName === "A" || cur.getAttribute("role") === "button" || cur.getAttribute("role") === "link" || cur.hasAttribute("data-testid")) {
896
+ return cur;
897
+ }
898
+ cur = cur.parentElement;
899
+ }
900
+ return null;
901
+ };
902
+ var cssPath = (el) => {
903
+ const parts = [];
904
+ let cur = el;
905
+ let depth = 0;
906
+ while (cur && depth < 4 && cur !== document.body) {
907
+ const node = cur;
908
+ const tag = node.tagName;
909
+ let segment = tag.toLowerCase();
910
+ const id = node.id;
911
+ if (id && /^[a-zA-Z][\w-]*$/.test(id)) {
912
+ parts.unshift(`#${id}`);
913
+ break;
914
+ }
915
+ const parent = node.parentElement;
916
+ if (parent) {
917
+ const siblings = Array.from(parent.children).filter(
918
+ (c) => c.tagName === tag
919
+ );
920
+ if (siblings.length > 1) {
921
+ const idx = siblings.indexOf(node) + 1;
922
+ segment += `:nth-of-type(${idx})`;
740
923
  }
741
- return null;
742
- })
743
- .filter((d) => d !== null);
744
- let capture;
745
- if (captureMode === "crop" && captureCrop) {
746
- // Store in page CSS pixels (Playwright's clip is in CSS pixels, not DPR)
747
- capture = {
748
- mode: "crop",
749
- crop: {
750
- start: {
751
- x: Math.round(captureCrop.x),
752
- y: Math.round(captureCrop.y - toolbarHeight),
753
- },
754
- end: {
755
- x: Math.round(captureCrop.x + captureCrop.w),
756
- y: Math.round(captureCrop.y + captureCrop.h - toolbarHeight),
757
- },
758
- },
759
- };
924
+ }
925
+ parts.unshift(segment);
926
+ cur = parent;
927
+ depth++;
928
+ }
929
+ return parts.join(" > ");
930
+ };
931
+ var computeSelector = (raw) => {
932
+ const el = isInteractiveAncestor(raw) ?? raw;
933
+ const testId = el.getAttribute("data-testid");
934
+ if (testId) {
935
+ return {
936
+ selector: `[data-testid="${escapeQuotes(testId)}"]`,
937
+ quality: "good"
938
+ };
939
+ }
940
+ const ariaLabel = el.getAttribute("aria-label");
941
+ if (ariaLabel) {
942
+ return {
943
+ selector: `[aria-label="${escapeQuotes(ariaLabel)}"]`,
944
+ quality: "good"
945
+ };
946
+ }
947
+ const role = el.getAttribute("role") ?? inferRole(el);
948
+ const text = el.textContent?.trim();
949
+ if (role && text && text.length > 0 && text.length < 50) {
950
+ return { selector: `text="${escapeQuotes(text)}"`, quality: "good" };
951
+ }
952
+ if (text && text.length > 0 && text.length < 50 && !/\n/.test(text)) {
953
+ return { selector: `text="${escapeQuotes(text)}"`, quality: "good" };
954
+ }
955
+ return { selector: cssPath(el), quality: "fallback" };
956
+ };
957
+
958
+ // src/editor/inject-script/record.ts
959
+ var KAGEMUSHA_UI_SELECTORS = [
960
+ "#kagemusha-toolbar",
961
+ "#kagemusha-svg-layer",
962
+ ".kagemusha-hint",
963
+ ".kagemusha-steps-panel",
964
+ ".kagemusha-prompt"
965
+ ];
966
+ var isOwnUi = (el) => {
967
+ if (!(el instanceof Element)) return false;
968
+ return KAGEMUSHA_UI_SELECTORS.some((s) => el.closest(s) !== null);
969
+ };
970
+ var svgRef2 = null;
971
+ var panelEl = null;
972
+ var pickerOutlineEl = null;
973
+ var panelOpen = false;
974
+ var ensurePanel = () => {
975
+ if (panelEl) return panelEl;
976
+ const div = document.createElement("div");
977
+ div.className = "kagemusha-steps-panel";
978
+ div.setAttribute(
979
+ "style",
980
+ // Anchored just below the toolbar (top: 60px) so it's visible without
981
+ // scrolling. Fills available vertical space minus a small bottom margin.
982
+ "position:fixed;top:60px;right:16px;width:340px;max-height:calc(100vh - 80px);overflow-y:auto;background:#1a1a2e;color:#fff;padding:12px 16px;border-radius:8px;font-family:-apple-system,sans-serif;font-size:12px;line-height:1.5;z-index:var(--kg-z-top);box-shadow:0 4px 16px rgba(0,0,0,0.4);display:none;"
983
+ );
984
+ document.documentElement.appendChild(div);
985
+ panelEl = div;
986
+ return div;
987
+ };
988
+ var renderPanel = () => {
989
+ const panel = ensurePanel();
990
+ updateToggleButton();
991
+ if (!panelOpen) {
992
+ panel.style.display = "none";
993
+ return;
994
+ }
995
+ panel.style.display = "block";
996
+ const header = state.recording ? `<div style="color:#ef4444;font-weight:600;margin-bottom:6px;">\u{1F4F9} Recording... (${state.recordedSteps.length} steps)</div>` : `<div style="color:#7a89b0;font-size:10px;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:6px;">Steps (${state.recordedSteps.length})</div>`;
997
+ const rows = state.recordedSteps.map((s, i) => `<div>${i + 1}. ${renderStepLine(s)}</div>`).join("");
998
+ panel.innerHTML = `${header}${rows || '<div style="color:#888;">(no steps)</div>'}`;
999
+ };
1000
+ var updateToggleButton = () => {
1001
+ const btn = document.getElementById("kg-steps-toggle");
1002
+ if (!btn) return;
1003
+ const n = state.recordedSteps.length;
1004
+ const recording = state.recording;
1005
+ btn.textContent = recording ? `\u{1F4F9} Steps (${n})` : n > 0 ? `\u{1F4CB} Steps (${n})` : "\u{1F4CB} Steps (0)";
1006
+ btn.classList.toggle("has-steps", n > 0 && !panelOpen);
1007
+ btn.classList.toggle("open", panelOpen);
1008
+ };
1009
+ var openPanel = () => {
1010
+ panelOpen = true;
1011
+ renderPanel();
1012
+ };
1013
+ var closePanel = () => {
1014
+ panelOpen = false;
1015
+ renderPanel();
1016
+ };
1017
+ var togglePanel = () => {
1018
+ if (panelOpen) closePanel();
1019
+ else openPanel();
1020
+ };
1021
+ var escapeHtml = (v) => v.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1022
+ var optBadge = (s) => s.optional ? ' <span style="color:#7a89b0;font-size:10px;background:#2a2a4e;padding:1px 5px;border-radius:3px;margin-left:4px;">optional</span>' : "";
1023
+ var renderStepLine = (s) => {
1024
+ switch (s.action) {
1025
+ case "click":
1026
+ return `<b>click</b> ${escapeHtml(s.selector)}${optBadge(s)}`;
1027
+ case "type":
1028
+ return `<b>type</b> ${escapeHtml(s.selector)} \u2192 "${escapeHtml(s.text)}"${optBadge(s)}`;
1029
+ case "select":
1030
+ return `<b>select</b> ${escapeHtml(s.selector)} \u2192 "${escapeHtml(s.value)}"${optBadge(s)}`;
1031
+ case "hover":
1032
+ return `<b>hover</b> ${escapeHtml(s.selector)}${optBadge(s)}`;
1033
+ case "wait":
1034
+ return `<b>wait</b> ${s.ms}ms`;
1035
+ case "waitForSelector":
1036
+ return `<b>waitForSelector</b> ${escapeHtml(s.selector)}${s.timeout ? ` (${s.timeout}ms)` : ""}${optBadge(s)}`;
1037
+ default:
1038
+ return `<b>${s.action}</b>`;
1039
+ }
1040
+ };
1041
+ var updateToolbarLockState = () => {
1042
+ const lock = state.recording;
1043
+ for (const id of [
1044
+ "kg-tool-rect",
1045
+ "kg-tool-arrow",
1046
+ "kg-tool-label",
1047
+ "kg-cap-full",
1048
+ "kg-cap-crop",
1049
+ "kg-delete"
1050
+ ]) {
1051
+ const btn = document.getElementById(id);
1052
+ if (btn) btn.disabled = lock;
1053
+ }
1054
+ const group = document.getElementById("kg-rec-group");
1055
+ if (group) group.classList.toggle("visible", lock);
1056
+ const recBtn = document.getElementById("kg-record");
1057
+ if (recBtn) {
1058
+ recBtn.textContent = state.recording ? "\u23F9 Stop" : "\u{1F534} Record";
1059
+ recBtn.classList.toggle("active", state.recording);
1060
+ }
1061
+ if (svgRef2) {
1062
+ svgRef2.style.pointerEvents = state.recording ? "none" : "";
1063
+ }
1064
+ };
1065
+ var setRecording = (on) => {
1066
+ if (on) {
1067
+ if (state.recordedSteps.length > 0) {
1068
+ const ok = window.confirm(
1069
+ `Recording will replace the existing ${state.recordedSteps.length} step(s). Continue?`
1070
+ );
1071
+ if (!ok) return;
1072
+ }
1073
+ state.recordedSteps = [];
1074
+ state.recording = true;
1075
+ } else {
1076
+ state.recording = false;
1077
+ cancelPicker();
1078
+ }
1079
+ updateToolbarLockState();
1080
+ renderPanel();
1081
+ };
1082
+ var pickerButtonId = (kind) => kind === "hover" ? "kg-rec-hover" : "kg-rec-wfs";
1083
+ var startPicker = (kind) => {
1084
+ if (!state.recording) return;
1085
+ state.pickerKind = kind;
1086
+ document.getElementById(pickerButtonId(kind))?.classList.add("picking");
1087
+ if (svgRef2) svgRef2.style.cursor = "crosshair";
1088
+ showPrompt(
1089
+ kind === "hover" ? "Hover an element, click to confirm. ESC to cancel." : "Hover an element, click to confirm. ESC to cancel."
1090
+ );
1091
+ };
1092
+ var cancelPicker = () => {
1093
+ if (state.pickerKind) {
1094
+ document.getElementById(pickerButtonId(state.pickerKind))?.classList.remove("picking");
760
1095
  }
761
- else {
762
- capture = { mode: "fullPage" };
1096
+ state.pickerKind = null;
1097
+ if (svgRef2) svgRef2.style.cursor = "";
1098
+ hidePrompt();
1099
+ hidePickerOutline();
1100
+ };
1101
+ var ensurePickerOutline = () => {
1102
+ if (pickerOutlineEl) return pickerOutlineEl;
1103
+ const div = document.createElement("div");
1104
+ div.className = "kagemusha-picker-outline";
1105
+ div.style.display = "none";
1106
+ document.documentElement.appendChild(div);
1107
+ pickerOutlineEl = div;
1108
+ return div;
1109
+ };
1110
+ var showPickerOutline = (rect) => {
1111
+ const el = ensurePickerOutline();
1112
+ el.style.display = "block";
1113
+ el.style.left = `${rect.left}px`;
1114
+ el.style.top = `${rect.top}px`;
1115
+ el.style.width = `${rect.width}px`;
1116
+ el.style.height = `${rect.height}px`;
1117
+ };
1118
+ var hidePickerOutline = () => {
1119
+ if (pickerOutlineEl) pickerOutlineEl.style.display = "none";
1120
+ };
1121
+ var promptEl = null;
1122
+ var showPrompt = (message) => {
1123
+ if (!promptEl) {
1124
+ const div = document.createElement("div");
1125
+ div.className = "kagemusha-prompt";
1126
+ div.setAttribute(
1127
+ "style",
1128
+ "position:fixed;top:60px;left:50%;transform:translateX(-50%);background:#0ea5e9;color:#fff;padding:10px 18px;border-radius:8px;font-family:-apple-system,sans-serif;font-size:13px;z-index:var(--kg-z-top);box-shadow:0 4px 12px rgba(0,0,0,0.3);"
1129
+ );
1130
+ document.documentElement.appendChild(div);
1131
+ promptEl = div;
763
1132
  }
764
- _win.__kagemusha_save(JSON.stringify({ decorations, capture }));
765
- };
766
- document.getElementById("kg-save")?.addEventListener("click", save);
767
- export {};
768
- //# sourceMappingURL=inject-script.js.map
1133
+ promptEl.textContent = message;
1134
+ promptEl.style.display = "block";
1135
+ };
1136
+ var hidePrompt = () => {
1137
+ if (promptEl) promptEl.style.display = "none";
1138
+ };
1139
+ var onClickCapture = (e) => {
1140
+ if (!state.recording) return;
1141
+ if (isOwnUi(e.target)) return;
1142
+ const el = e.target;
1143
+ if (!el) return;
1144
+ if (state.pickerKind) {
1145
+ e.preventDefault();
1146
+ e.stopPropagation();
1147
+ const kind = state.pickerKind;
1148
+ const sel2 = computeSelector(el);
1149
+ if (kind === "hover") {
1150
+ state.recordedSteps.push({ action: "hover", selector: sel2.selector });
1151
+ } else {
1152
+ state.recordedSteps.push({
1153
+ action: "waitForSelector",
1154
+ selector: sel2.selector
1155
+ });
1156
+ }
1157
+ cancelPicker();
1158
+ renderPanel();
1159
+ return;
1160
+ }
1161
+ const sel = computeSelector(el);
1162
+ state.recordedSteps.push({
1163
+ action: "click",
1164
+ selector: sel.selector,
1165
+ optional: true
1166
+ });
1167
+ renderPanel();
1168
+ };
1169
+ var onChangeCapture = (e) => {
1170
+ if (!state.recording) return;
1171
+ if (isOwnUi(e.target)) return;
1172
+ const target = e.target;
1173
+ if (!target) return;
1174
+ if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") {
1175
+ const input = target;
1176
+ const type = input.type;
1177
+ if (type === "checkbox" || type === "radio") return;
1178
+ const sel = computeSelector(input);
1179
+ state.recordedSteps.push({
1180
+ action: "type",
1181
+ selector: sel.selector,
1182
+ text: input.value,
1183
+ optional: true
1184
+ });
1185
+ renderPanel();
1186
+ return;
1187
+ }
1188
+ if (target.tagName === "SELECT") {
1189
+ const select = target;
1190
+ const sel = computeSelector(select);
1191
+ state.recordedSteps.push({
1192
+ action: "select",
1193
+ selector: sel.selector,
1194
+ value: select.value,
1195
+ optional: true
1196
+ });
1197
+ renderPanel();
1198
+ }
1199
+ };
1200
+ var onKeyDownCapture = (e) => {
1201
+ if (state.pickerKind && e.key === "Escape") {
1202
+ cancelPicker();
1203
+ }
1204
+ };
1205
+ var onMouseMoveCapture = (e) => {
1206
+ if (!state.pickerKind) return;
1207
+ if (isOwnUi(e.target)) {
1208
+ hidePickerOutline();
1209
+ return;
1210
+ }
1211
+ const el = e.target;
1212
+ if (!el) return;
1213
+ showPickerOutline(el.getBoundingClientRect());
1214
+ };
1215
+ var onOutsideClick = (e) => {
1216
+ if (!panelOpen) return;
1217
+ const target = e.target;
1218
+ if (!target) return;
1219
+ if (target.closest(".kagemusha-steps-panel")) return;
1220
+ if (target.closest("#kg-steps-toggle")) return;
1221
+ closePanel();
1222
+ };
1223
+ var promptForWaitMs = () => {
1224
+ const raw = window.prompt("Wait for how many milliseconds?", "3000");
1225
+ if (raw === null) return;
1226
+ const ms = Number.parseInt(raw, 10);
1227
+ if (Number.isNaN(ms) || ms <= 0) return;
1228
+ state.recordedSteps.push({ action: "wait", ms });
1229
+ renderPanel();
1230
+ };
1231
+ var initRecord = (svg2) => {
1232
+ svgRef2 = svg2;
1233
+ ensurePanel();
1234
+ document.getElementById("kg-record")?.addEventListener("click", () => setRecording(!state.recording));
1235
+ document.getElementById("kg-rec-wait")?.addEventListener("click", () => promptForWaitMs());
1236
+ document.getElementById("kg-rec-wfs")?.addEventListener("click", () => startPicker("waitForSelector"));
1237
+ document.getElementById("kg-rec-hover")?.addEventListener("click", () => startPicker("hover"));
1238
+ document.getElementById("kg-steps-toggle")?.addEventListener("click", (e) => {
1239
+ e.stopPropagation();
1240
+ togglePanel();
1241
+ });
1242
+ document.addEventListener("click", onClickCapture, true);
1243
+ document.addEventListener("change", onChangeCapture, true);
1244
+ document.addEventListener("keydown", onKeyDownCapture, true);
1245
+ document.addEventListener("mousemove", onMouseMoveCapture, true);
1246
+ document.addEventListener("click", onOutsideClick);
1247
+ updateToolbarLockState();
1248
+ renderPanel();
1249
+ };
1250
+ var loadSteps = (steps) => {
1251
+ state.recordedSteps = [...steps];
1252
+ renderPanel();
1253
+ };
1254
+ var serializeSteps = () => [...state.recordedSteps];
1255
+
1256
+ // src/editor/inject-script/bridge.ts
1257
+ var save = () => {
1258
+ if (state.captureMode === "crop" && !state.captureCrop) {
1259
+ showErrorToast(
1260
+ "Crop mode is active but no area is drawn.\nDrag to define an area, or switch to Full Page."
1261
+ );
1262
+ return;
1263
+ }
1264
+ const decorations = serializeAnnotations();
1265
+ const capture = serializeCapture();
1266
+ const beforeCapture = serializeSteps();
1267
+ window.__kagemusha_save(
1268
+ JSON.stringify({ decorations, capture, beforeCapture })
1269
+ );
1270
+ };
1271
+ var initBridge = () => {
1272
+ window.__kagemusha_loadAnnotations = (decorations) => {
1273
+ loadAnnotations(decorations);
1274
+ };
1275
+ window.__kagemusha_loadCapture = (capture) => {
1276
+ loadCapture(capture, (mode) => setCaptureMode(mode));
1277
+ };
1278
+ window.__kagemusha_loadSteps = (steps) => {
1279
+ loadSteps(steps);
1280
+ };
1281
+ return { save };
1282
+ };
1283
+
1284
+ // src/editor/inject-script/index.ts
1285
+ document.documentElement.style.overflow = "hidden";
1286
+ var { svg } = initSvgLayer();
1287
+ var bridgeSave = () => {
1288
+ };
1289
+ initToolbar(
1290
+ {
1291
+ onToolChange: () => {
1292
+ },
1293
+ onSave: () => bridgeSave(),
1294
+ onDelete: () => deleteSelected()
1295
+ },
1296
+ svg
1297
+ );
1298
+ initCrop();
1299
+ initRecord(svg);
1300
+ ({ save: bridgeSave } = initBridge());
1301
+ svg.addEventListener("mousedown", (e) => {
1302
+ if (handleMouseDown2(e)) return;
1303
+ handleMouseDown(e);
1304
+ });
1305
+ document.addEventListener("mousemove", (e) => {
1306
+ if (handleMouseMove2(e)) return;
1307
+ handleMouseMove(e);
1308
+ });
1309
+ document.addEventListener("mouseup", (e) => {
1310
+ if (handleMouseUp2(e)) return;
1311
+ handleMouseUp(e);
1312
+ });
1313
+ document.addEventListener(
1314
+ "keydown",
1315
+ (e) => handleKeyDown(e)
1316
+ );
1317
+ })();