@useclickly/react 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.
package/dist/index.cjs ADDED
@@ -0,0 +1,1191 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var react = require('react');
5
+ var reactDom = require('react-dom');
6
+ var core = require('@useclickly/core');
7
+ var zustand = require('zustand');
8
+ var shallow = require('zustand/react/shallow');
9
+ var jsxRuntime = require('react/jsx-runtime');
10
+ var nanoid = require('nanoid');
11
+
12
+ // src/Clickly.tsx
13
+ function useDraggable(defaultPos, size) {
14
+ const [position, setPosition] = react.useState(defaultPos);
15
+ const [isDragging, setDragging] = react.useState(false);
16
+ const startRef = react.useRef(null);
17
+ const onPointerDown = react.useCallback(
18
+ (e) => {
19
+ e.preventDefault();
20
+ e.target.setPointerCapture?.(e.pointerId);
21
+ startRef.current = {
22
+ pointer: { x: e.clientX, y: e.clientY },
23
+ pos: position
24
+ };
25
+ setDragging(true);
26
+ },
27
+ [position]
28
+ );
29
+ react.useEffect(() => {
30
+ if (!isDragging) return;
31
+ const onMove = (e) => {
32
+ const s = startRef.current;
33
+ if (!s) return;
34
+ const dx = e.clientX - s.pointer.x;
35
+ const dy = e.clientY - s.pointer.y;
36
+ const maxX = window.innerWidth - size.width - 8;
37
+ const maxY = window.innerHeight - size.height - 8;
38
+ setPosition({
39
+ x: clamp(s.pos.x + dx, 8, Math.max(8, maxX)),
40
+ y: clamp(s.pos.y + dy, 8, Math.max(8, maxY))
41
+ });
42
+ };
43
+ const onUp = () => {
44
+ setDragging(false);
45
+ startRef.current = null;
46
+ };
47
+ window.addEventListener("pointermove", onMove);
48
+ window.addEventListener("pointerup", onUp);
49
+ return () => {
50
+ window.removeEventListener("pointermove", onMove);
51
+ window.removeEventListener("pointerup", onUp);
52
+ };
53
+ }, [isDragging, size.height, size.width]);
54
+ return {
55
+ position,
56
+ setPosition,
57
+ handleProps: {
58
+ onPointerDown,
59
+ style: { cursor: isDragging ? "grabbing" : "grab", touchAction: "none" }
60
+ },
61
+ isDragging
62
+ };
63
+ }
64
+ function clamp(n, lo, hi) {
65
+ return Math.max(lo, Math.min(hi, n));
66
+ }
67
+ function useEngineState(engine) {
68
+ return react.useSyncExternalStore(
69
+ (cb) => engine ? engine.subscribe(cb) : () => {
70
+ },
71
+ () => engine ? engine.getSnapshot() : IDLE,
72
+ () => IDLE
73
+ );
74
+ }
75
+ var IDLE = { kind: "idle" };
76
+ var useAnnotations = zustand.create((set, get) => ({
77
+ byId: {},
78
+ order: [],
79
+ add: (a) => set((s) => ({
80
+ byId: { ...s.byId, [a.id]: a },
81
+ order: s.order.includes(a.id) ? s.order : [...s.order, a.id]
82
+ })),
83
+ remove: (id) => set((s) => {
84
+ if (!(id in s.byId)) return s;
85
+ const next = { ...s.byId };
86
+ delete next[id];
87
+ return { byId: next, order: s.order.filter((x) => x !== id) };
88
+ }),
89
+ update: (id, patch) => set((s) => {
90
+ const cur = s.byId[id];
91
+ if (!cur) return s;
92
+ return { byId: { ...s.byId, [id]: { ...cur, ...patch } } };
93
+ }),
94
+ clear: () => set({ byId: {}, order: [] }),
95
+ list: () => {
96
+ const { byId, order } = get();
97
+ return order.map((id) => byId[id]).filter(Boolean);
98
+ }
99
+ }));
100
+ function useAnnotationsList() {
101
+ return useAnnotations(
102
+ shallow.useShallow((s) => s.order.map((id) => s.byId[id]).filter(Boolean))
103
+ );
104
+ }
105
+ var DEFAULTS = {
106
+ outputDetail: "standard",
107
+ copyOnAdd: true,
108
+ showReactComponents: true,
109
+ markerColor: "#06b6d4"
110
+ };
111
+ var STORAGE_KEY = "clickly:settings";
112
+ function load() {
113
+ if (typeof localStorage === "undefined") return DEFAULTS;
114
+ try {
115
+ const raw = localStorage.getItem(STORAGE_KEY);
116
+ if (!raw) return DEFAULTS;
117
+ const parsed = JSON.parse(raw);
118
+ return { ...DEFAULTS, ...parsed };
119
+ } catch {
120
+ return DEFAULTS;
121
+ }
122
+ }
123
+ function persist(s) {
124
+ if (typeof localStorage === "undefined") return;
125
+ try {
126
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(s));
127
+ } catch {
128
+ }
129
+ }
130
+ var useSettings = zustand.create((set) => ({
131
+ ...load(),
132
+ set: (patch) => set((cur) => {
133
+ const next = { ...cur, ...patch };
134
+ persist(next);
135
+ return next;
136
+ }),
137
+ reset: () => {
138
+ persist(DEFAULTS);
139
+ set(DEFAULTS);
140
+ }
141
+ }));
142
+
143
+ // src/output/markdown.ts
144
+ function annotationsToMarkdown(annotations, detail = "standard") {
145
+ if (!annotations.length) return "(no annotations)";
146
+ return annotations.map((a, i) => formatOne(a, i + 1, detail)).join("\n\n");
147
+ }
148
+ function formatOne(a, index, detail) {
149
+ const lines = [];
150
+ lines.push(`## Annotation #${index}`);
151
+ lines.push(`**Element:** \`${a.element}${a.cssClasses ? "." + a.cssClasses.split(" ").join(".") : ""}\``);
152
+ lines.push(`**Path:** \`${a.elementPath}\``);
153
+ if (detail !== "compact" && a.boundingBox) {
154
+ const b = a.boundingBox;
155
+ lines.push(`**Position:** ${Math.round(b.x)}px, ${Math.round(b.y)}px (${Math.round(b.width)}\xD7${Math.round(b.height)}px)`);
156
+ }
157
+ if (detail === "detailed" || detail === "forensic") {
158
+ if (a.reactComponents) lines.push(`**React:** ${a.reactComponents}`);
159
+ if (a.nearbyText) lines.push(`**Nearby text:** ${truncate(a.nearbyText, 120)}`);
160
+ }
161
+ if (detail === "forensic" && a.computedStyles) {
162
+ lines.push("**Computed styles:**\n```\n" + a.computedStyles + "\n```");
163
+ }
164
+ lines.push(`**Feedback:** ${a.comment}`);
165
+ if (a.severity) lines.push(`**Severity:** ${a.severity}`);
166
+ return lines.join("\n");
167
+ }
168
+ function truncate(s, n) {
169
+ return s.length <= n ? s : s.slice(0, n - 1) + "\u2026";
170
+ }
171
+ function Icon({
172
+ children,
173
+ size = 16
174
+ }) {
175
+ return /* @__PURE__ */ jsxRuntime.jsx(
176
+ "svg",
177
+ {
178
+ width: size,
179
+ height: size,
180
+ viewBox: "0 0 24 24",
181
+ fill: "none",
182
+ stroke: "currentColor",
183
+ strokeWidth: "2",
184
+ strokeLinecap: "round",
185
+ strokeLinejoin: "round",
186
+ "aria-hidden": "true",
187
+ children
188
+ }
189
+ );
190
+ }
191
+ var IconCursor = () => /* @__PURE__ */ jsxRuntime.jsx(Icon, { children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 4l6 16 2-7 7-2z" }) });
192
+ var IconLayers = () => /* @__PURE__ */ jsxRuntime.jsxs(Icon, { children: [
193
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 2l9 5-9 5-9-5 9-5z" }),
194
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 12l9 5 9-5" }),
195
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 17l9 5 9-5" })
196
+ ] });
197
+ var IconSquare = () => /* @__PURE__ */ jsxRuntime.jsx(Icon, { children: /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "4", y: "4", width: "16", height: "16", rx: "2", strokeDasharray: "3 3" }) });
198
+ var IconCopy = () => /* @__PURE__ */ jsxRuntime.jsxs(Icon, { children: [
199
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2" }),
200
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 15V5a2 2 0 0 1 2-2h10" })
201
+ ] });
202
+ var IconTrash = () => /* @__PURE__ */ jsxRuntime.jsx(Icon, { children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6h14z" }) });
203
+ var IconSettings = () => /* @__PURE__ */ jsxRuntime.jsxs(Icon, { children: [
204
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "3" }),
205
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M19.4 15a1.7 1.7 0 0 0 .34 1.87l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.7 1.7 0 0 0-1.87-.34 1.7 1.7 0 0 0-1.03 1.55V21a2 2 0 1 1-4 0v-.09a1.7 1.7 0 0 0-1.11-1.55 1.7 1.7 0 0 0-1.87.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.7 1.7 0 0 0 4.6 15 1.7 1.7 0 0 0 3.04 14H3a2 2 0 1 1 0-4h.09A1.7 1.7 0 0 0 4.64 8.9a1.7 1.7 0 0 0-.34-1.87l-.06-.06A2 2 0 1 1 7.07 4.14l.06.06A1.7 1.7 0 0 0 9 4.54 1.7 1.7 0 0 0 10 3.03V3a2 2 0 1 1 4 0v.09a1.7 1.7 0 0 0 1 1.51 1.7 1.7 0 0 0 1.87-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.7 1.7 0 0 0 19.4 9a1.7 1.7 0 0 0 1.55 1H21a2 2 0 1 1 0 4h-.09a1.7 1.7 0 0 0-1.5 1z" })
206
+ ] });
207
+ var IconGrip = () => /* @__PURE__ */ jsxRuntime.jsxs(Icon, { children: [
208
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9", cy: "6", r: "1" }),
209
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9", cy: "12", r: "1" }),
210
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9", cy: "18", r: "1" }),
211
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "15", cy: "6", r: "1" }),
212
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "15", cy: "12", r: "1" }),
213
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "15", cy: "18", r: "1" })
214
+ ] });
215
+ var IconClose = () => /* @__PURE__ */ jsxRuntime.jsx(Icon, { children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M18 6L6 18M6 6l12 12" }) });
216
+ var IconCheck = () => /* @__PURE__ */ jsxRuntime.jsx(Icon, { children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 12l5 5L20 7" }) });
217
+ var IconList = () => /* @__PURE__ */ jsxRuntime.jsx(Icon, { children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01" }) });
218
+ function SettingsPopover({ anchor, width, onClose }) {
219
+ const ref = react.useRef(null);
220
+ const s = useSettings();
221
+ react.useEffect(() => {
222
+ const onDown = (e) => {
223
+ if (ref.current && !ref.current.contains(e.target)) onClose();
224
+ };
225
+ window.addEventListener("pointerdown", onDown, true);
226
+ return () => window.removeEventListener("pointerdown", onDown, true);
227
+ }, [onClose]);
228
+ const top = Math.max(8, anchor.y - 200);
229
+ const left = Math.min(window.innerWidth - 268, anchor.x + width - 260);
230
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref, className: "clickly-popover", style: { left, top }, children: [
231
+ /* @__PURE__ */ jsxRuntime.jsx("h4", { children: "Settings" }),
232
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "field", children: [
233
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "clickly-detail", children: "Output detail" }),
234
+ /* @__PURE__ */ jsxRuntime.jsxs(
235
+ "select",
236
+ {
237
+ id: "clickly-detail",
238
+ value: s.outputDetail,
239
+ onChange: (e) => s.set({ outputDetail: e.target.value }),
240
+ children: [
241
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "compact", children: "Compact" }),
242
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "standard", children: "Standard" }),
243
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "detailed", children: "Detailed" }),
244
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "forensic", children: "Forensic" })
245
+ ]
246
+ }
247
+ )
248
+ ] }),
249
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "field", children: [
250
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "clickly-copy", children: "Copy on add" }),
251
+ /* @__PURE__ */ jsxRuntime.jsx(
252
+ "input",
253
+ {
254
+ id: "clickly-copy",
255
+ type: "checkbox",
256
+ checked: s.copyOnAdd,
257
+ onChange: (e) => s.set({ copyOnAdd: e.target.checked })
258
+ }
259
+ )
260
+ ] }),
261
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "field", children: [
262
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "clickly-react", children: "React components" }),
263
+ /* @__PURE__ */ jsxRuntime.jsx(
264
+ "input",
265
+ {
266
+ id: "clickly-react",
267
+ type: "checkbox",
268
+ checked: s.showReactComponents,
269
+ onChange: (e) => s.set({ showReactComponents: e.target.checked })
270
+ }
271
+ )
272
+ ] }),
273
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "field", children: [
274
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "clickly-color", children: "Marker color" }),
275
+ /* @__PURE__ */ jsxRuntime.jsx(
276
+ "input",
277
+ {
278
+ id: "clickly-color",
279
+ type: "color",
280
+ value: s.markerColor,
281
+ onChange: (e) => s.set({ markerColor: e.target.value })
282
+ }
283
+ )
284
+ ] })
285
+ ] });
286
+ }
287
+ function AnnotationList({ anchor, width, onClose }) {
288
+ const items = useAnnotationsList();
289
+ const remove = useAnnotations((s) => s.remove);
290
+ const ref = react.useRef(null);
291
+ react.useEffect(() => {
292
+ const onDown = (e) => {
293
+ if (ref.current && !ref.current.contains(e.target)) onClose();
294
+ };
295
+ window.addEventListener("pointerdown", onDown, true);
296
+ return () => window.removeEventListener("pointerdown", onDown, true);
297
+ }, [onClose]);
298
+ const top = Math.max(8, anchor.y - 8 - Math.min(window.innerHeight * 0.5, items.length * 60 + 40));
299
+ const left = Math.min(window.innerWidth - 328, anchor.x);
300
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { ref, className: "clickly-list", style: { left, top }, children: items.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "empty", children: "No annotations yet." }) : items.map((a, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "row", children: [
301
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "num", children: [
302
+ "#",
303
+ i + 1
304
+ ] }),
305
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "body", children: [
306
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "selector", children: a.elementPath }),
307
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: a.comment })
308
+ ] }),
309
+ /* @__PURE__ */ jsxRuntime.jsx("button", { className: "remove", onClick: () => remove(a.id), title: "Remove", children: /* @__PURE__ */ jsxRuntime.jsx(IconClose, {}) })
310
+ ] }, a.id)) });
311
+ }
312
+ var TOOLBAR_SIZE = { width: 360, height: 40 };
313
+ function Toolbar({ engine, onCollapse }) {
314
+ const state = useEngineState(engine);
315
+ const annotations = useAnnotationsList();
316
+ const clearAnnotations = useAnnotations((s) => s.clear);
317
+ const outputDetail = useSettings((s) => s.outputDetail);
318
+ const [showSettings, setShowSettings] = react.useState(false);
319
+ const [showList, setShowList] = react.useState(false);
320
+ const anchorRef = react.useRef(null);
321
+ const { position, handleProps } = useDraggable(
322
+ {
323
+ x: Math.max(8, window.innerWidth - TOOLBAR_SIZE.width - 16),
324
+ y: Math.max(8, window.innerHeight - TOOLBAR_SIZE.height - 16)
325
+ },
326
+ TOOLBAR_SIZE
327
+ );
328
+ const isActive = state.kind !== "idle";
329
+ const currentMode = state.kind === "inspect" || state.kind === "pressed" || state.kind === "dragging" ? state.mode : null;
330
+ const pinnedCount = state.kind === "inspect" || state.kind === "pressed" || state.kind === "dragging" ? state.pinned.length : 0;
331
+ const setMode = (m) => {
332
+ if (!isActive) engine.activate(m);
333
+ else engine.setMode(m);
334
+ };
335
+ const onCopy = async () => {
336
+ const md = annotationsToMarkdown(annotations, outputDetail);
337
+ try {
338
+ await navigator.clipboard.writeText(md);
339
+ } catch {
340
+ }
341
+ };
342
+ const onClear = () => {
343
+ if (annotations.length === 0) return;
344
+ if (confirm(`Clear ${annotations.length} annotation(s)?`)) clearAnnotations();
345
+ };
346
+ return /* @__PURE__ */ jsxRuntime.jsxs(
347
+ "div",
348
+ {
349
+ ref: anchorRef,
350
+ className: "clickly-toolbar",
351
+ style: { left: position.x, top: position.y, width: TOOLBAR_SIZE.width },
352
+ "aria-label": "Clickly toolbar",
353
+ children: [
354
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grip", ...handleProps, title: "Drag to move", children: /* @__PURE__ */ jsxRuntime.jsx(IconGrip, {}) }),
355
+ /* @__PURE__ */ jsxRuntime.jsx(
356
+ "button",
357
+ {
358
+ className: `clickly-btn icon-only${currentMode === "single" ? " is-active" : ""}`,
359
+ onClick: () => setMode("single"),
360
+ title: "Single (1)",
361
+ children: /* @__PURE__ */ jsxRuntime.jsx(IconCursor, {})
362
+ }
363
+ ),
364
+ /* @__PURE__ */ jsxRuntime.jsx(
365
+ "button",
366
+ {
367
+ className: `clickly-btn icon-only${currentMode === "multi" ? " is-active" : ""}`,
368
+ onClick: () => setMode("multi"),
369
+ title: "Multi-select (2 / shift-click)",
370
+ children: /* @__PURE__ */ jsxRuntime.jsx(IconLayers, {})
371
+ }
372
+ ),
373
+ /* @__PURE__ */ jsxRuntime.jsx(
374
+ "button",
375
+ {
376
+ className: `clickly-btn icon-only${currentMode === "area" ? " is-active" : ""}`,
377
+ onClick: () => setMode("area"),
378
+ title: "Area drag (3)",
379
+ children: /* @__PURE__ */ jsxRuntime.jsx(IconSquare, {})
380
+ }
381
+ ),
382
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "divider" }),
383
+ pinnedCount > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
384
+ /* @__PURE__ */ jsxRuntime.jsxs(
385
+ "button",
386
+ {
387
+ className: "clickly-btn primary-pinned",
388
+ onClick: () => engine.annotatePinned(),
389
+ title: `Annotate the ${pinnedCount} pinned element(s) (Enter)`,
390
+ children: [
391
+ /* @__PURE__ */ jsxRuntime.jsx(IconCheck, {}),
392
+ "Annotate (",
393
+ pinnedCount,
394
+ ")"
395
+ ]
396
+ }
397
+ ),
398
+ /* @__PURE__ */ jsxRuntime.jsx(
399
+ "button",
400
+ {
401
+ className: "clickly-btn icon-only",
402
+ onClick: () => engine.clearPinned(),
403
+ title: "Clear pinned (Esc)",
404
+ children: /* @__PURE__ */ jsxRuntime.jsx(IconClose, {})
405
+ }
406
+ ),
407
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "divider" })
408
+ ] }),
409
+ /* @__PURE__ */ jsxRuntime.jsxs(
410
+ "button",
411
+ {
412
+ className: "clickly-btn",
413
+ onClick: () => setShowList((v) => !v),
414
+ title: "Annotations (L)",
415
+ children: [
416
+ /* @__PURE__ */ jsxRuntime.jsx(IconList, {}),
417
+ annotations.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "clickly-counter", children: annotations.length })
418
+ ]
419
+ }
420
+ ),
421
+ /* @__PURE__ */ jsxRuntime.jsx(
422
+ "button",
423
+ {
424
+ className: "clickly-btn icon-only",
425
+ onClick: onCopy,
426
+ title: "Copy markdown (C)",
427
+ disabled: annotations.length === 0,
428
+ children: /* @__PURE__ */ jsxRuntime.jsx(IconCopy, {})
429
+ }
430
+ ),
431
+ /* @__PURE__ */ jsxRuntime.jsx(
432
+ "button",
433
+ {
434
+ className: "clickly-btn icon-only",
435
+ onClick: onClear,
436
+ title: "Clear (X)",
437
+ disabled: annotations.length === 0,
438
+ children: /* @__PURE__ */ jsxRuntime.jsx(IconTrash, {})
439
+ }
440
+ ),
441
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "divider" }),
442
+ /* @__PURE__ */ jsxRuntime.jsx(
443
+ "button",
444
+ {
445
+ className: "clickly-btn icon-only",
446
+ onClick: () => setShowSettings((v) => !v),
447
+ title: "Settings",
448
+ children: /* @__PURE__ */ jsxRuntime.jsx(IconSettings, {})
449
+ }
450
+ ),
451
+ /* @__PURE__ */ jsxRuntime.jsx(
452
+ "button",
453
+ {
454
+ className: "clickly-btn icon-only",
455
+ onClick: onCollapse,
456
+ title: "Collapse (Esc)",
457
+ children: /* @__PURE__ */ jsxRuntime.jsx(IconClose, {})
458
+ }
459
+ ),
460
+ showSettings && /* @__PURE__ */ jsxRuntime.jsx(
461
+ SettingsPopover,
462
+ {
463
+ anchor: { x: position.x, y: position.y },
464
+ width: TOOLBAR_SIZE.width,
465
+ onClose: () => setShowSettings(false)
466
+ }
467
+ ),
468
+ showList && /* @__PURE__ */ jsxRuntime.jsx(
469
+ AnnotationList,
470
+ {
471
+ anchor: { x: position.x, y: position.y },
472
+ width: TOOLBAR_SIZE.width,
473
+ onClose: () => setShowList(false)
474
+ }
475
+ )
476
+ ]
477
+ }
478
+ );
479
+ }
480
+ function CollapsedFAB({ onExpand }) {
481
+ const annotations = useAnnotationsList();
482
+ return /* @__PURE__ */ jsxRuntime.jsxs(
483
+ "button",
484
+ {
485
+ className: "clickly-fab",
486
+ onClick: onExpand,
487
+ title: "Open Clickly (\u2318/Ctrl+Shift+F)",
488
+ "aria-label": "Open Clickly toolbar",
489
+ children: [
490
+ /* @__PURE__ */ jsxRuntime.jsx(IconCursor, {}),
491
+ annotations.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "clickly-fab-badge", children: annotations.length })
492
+ ]
493
+ }
494
+ );
495
+ }
496
+ var POPUP_W = 320;
497
+ var POPUP_H_EST = 200;
498
+ function AnnotationPopup({ engine }) {
499
+ const state = useEngineState(engine);
500
+ const addAnnotation = useAnnotations((s) => s.add);
501
+ const copyOnAdd = useSettings((s) => s.copyOnAdd);
502
+ const outputDetail = useSettings((s) => s.outputDetail);
503
+ const [comment, setComment] = react.useState("");
504
+ const taRef = react.useRef(null);
505
+ react.useEffect(() => {
506
+ if (state.kind === "annotating") {
507
+ setComment("");
508
+ requestAnimationFrame(() => taRef.current?.focus());
509
+ }
510
+ }, [state.kind]);
511
+ const onKey = (e) => {
512
+ if (e.key === "Escape") {
513
+ e.preventDefault();
514
+ cancel();
515
+ }
516
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
517
+ e.preventDefault();
518
+ submit();
519
+ }
520
+ };
521
+ const cancel = () => engine.commit();
522
+ const submit = () => {
523
+ if (state.kind !== "annotating") return;
524
+ const sel = engine.resolveSelection();
525
+ if (!sel) return;
526
+ const elements = sel.kind === "single" ? [sel.element] : sel.elements;
527
+ if (elements.length === 0) return;
528
+ const sharedComment = comment.trim() || "(no comment)";
529
+ const isMulti = sel.kind !== "single";
530
+ for (const el of elements) {
531
+ const md = core.collectMetadata(el, { detail: outputDetail });
532
+ const annotation = {
533
+ id: "ann_" + nanoid.nanoid(8),
534
+ comment: sharedComment,
535
+ element: md.element,
536
+ elementPath: md.elementPath,
537
+ fullPath: md.fullPath,
538
+ timestamp: Date.now(),
539
+ x: md.boundingBox.x + md.boundingBox.width / 2,
540
+ y: md.boundingBox.y + window.scrollY,
541
+ url: location.href,
542
+ boundingBox: md.boundingBox,
543
+ cssClasses: md.cssClasses || void 0,
544
+ computedStyles: stringifyStyles(md.computedStyles),
545
+ accessibility: md.accessibility || void 0,
546
+ nearbyText: md.nearbyText || void 0,
547
+ selectedText: md.selectedText || void 0,
548
+ isFixed: md.isFixed || void 0,
549
+ isMultiSelect: isMulti,
550
+ kind: "feedback",
551
+ status: "pending"
552
+ };
553
+ addAnnotation(annotation);
554
+ }
555
+ if (copyOnAdd) {
556
+ const all = useAnnotations.getState().list();
557
+ navigator.clipboard?.writeText(annotationsToMarkdown(all, outputDetail)).catch(() => void 0);
558
+ }
559
+ engine.commit();
560
+ };
561
+ const placement = react.useMemo(() => {
562
+ if (state.kind !== "annotating") return null;
563
+ const sel = engine.resolveSelection();
564
+ if (!sel) return null;
565
+ const rect = anchorRect(sel);
566
+ if (!rect) return null;
567
+ let top = rect.bottom + 8;
568
+ let left = rect.left;
569
+ if (top + POPUP_H_EST > window.innerHeight) {
570
+ top = Math.max(8, rect.top - POPUP_H_EST - 8);
571
+ }
572
+ if (left + POPUP_W > window.innerWidth) {
573
+ left = Math.max(8, window.innerWidth - POPUP_W - 8);
574
+ }
575
+ return { top, left, label: describeSelection(sel) };
576
+ }, [state, engine]);
577
+ if (state.kind !== "annotating" || !placement) return null;
578
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "clickly-popup", style: { top: placement.top, left: placement.left }, children: [
579
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "target-info", children: [
580
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: placement.label }),
581
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: state.selection.kind })
582
+ ] }),
583
+ /* @__PURE__ */ jsxRuntime.jsx(
584
+ "textarea",
585
+ {
586
+ ref: taRef,
587
+ value: comment,
588
+ placeholder: "Describe the issue or change\u2026 (\u2318/Ctrl + Enter to submit)",
589
+ onChange: (e) => setComment(e.target.value),
590
+ onKeyDown: onKey
591
+ }
592
+ ),
593
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "row", children: [
594
+ /* @__PURE__ */ jsxRuntime.jsx("button", { className: "ghost", onClick: cancel, children: "Cancel" }),
595
+ /* @__PURE__ */ jsxRuntime.jsx("button", { className: "primary", onClick: submit, children: "Add" })
596
+ ] })
597
+ ] });
598
+ }
599
+ function stringifyStyles(styles) {
600
+ const entries = Object.entries(styles);
601
+ if (entries.length === 0) return void 0;
602
+ return entries.map(([k, v]) => `${k}: ${v}`).join("; ");
603
+ }
604
+ function anchorRect(sel) {
605
+ if (!sel) return null;
606
+ if (sel.kind === "single") return sel.element.getBoundingClientRect();
607
+ if (sel.kind === "area") {
608
+ return new DOMRect(sel.rect.x, sel.rect.y, sel.rect.width, sel.rect.height);
609
+ }
610
+ if (sel.elements.length === 0) return null;
611
+ return sel.elements[0].getBoundingClientRect();
612
+ }
613
+ function describeSelection(sel) {
614
+ if (sel.kind === "single") {
615
+ const m = core.collectMetadata(sel.element, { detail: "compact" });
616
+ return m.elementPath;
617
+ }
618
+ if (sel.kind === "area") return `area \xB7 ${sel.elements.length} element(s)`;
619
+ return `${sel.elements.length} element(s)`;
620
+ }
621
+ function AnnotationPins() {
622
+ const annotations = useAnnotationsList();
623
+ const [, setVersion] = react.useState(0);
624
+ react.useEffect(() => {
625
+ let raf = null;
626
+ const update = () => {
627
+ if (raf !== null) return;
628
+ raf = requestAnimationFrame(() => {
629
+ raf = null;
630
+ setVersion((v) => v + 1);
631
+ });
632
+ };
633
+ window.addEventListener("scroll", update, { capture: true, passive: true });
634
+ window.addEventListener("resize", update, { passive: true });
635
+ return () => {
636
+ if (raf !== null) cancelAnimationFrame(raf);
637
+ window.removeEventListener("scroll", update, { capture: true });
638
+ window.removeEventListener("resize", update);
639
+ };
640
+ }, []);
641
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: annotations.map((a, i) => /* @__PURE__ */ jsxRuntime.jsx(Pin, { number: i + 1, annotation: a }, a.id)) });
642
+ }
643
+ function Pin({ number, annotation }) {
644
+ const remove = useAnnotations((s) => s.remove);
645
+ const [hovered, setHovered] = react.useState(false);
646
+ const pos = resolvePosition(annotation);
647
+ if (!pos) return null;
648
+ return /* @__PURE__ */ jsxRuntime.jsxs(
649
+ "div",
650
+ {
651
+ className: "clickly-pin",
652
+ style: { left: pos.x, top: pos.y },
653
+ onMouseEnter: () => setHovered(true),
654
+ onMouseLeave: () => setHovered(false),
655
+ role: "button",
656
+ tabIndex: 0,
657
+ title: annotation.comment,
658
+ children: [
659
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "clickly-pin-num", children: number }),
660
+ hovered && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "clickly-pin-bubble", children: [
661
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "clickly-pin-comment", children: annotation.comment }),
662
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "clickly-pin-meta", children: annotation.elementPath }),
663
+ /* @__PURE__ */ jsxRuntime.jsx(
664
+ "button",
665
+ {
666
+ className: "clickly-pin-remove",
667
+ onClick: (e) => {
668
+ e.stopPropagation();
669
+ remove(annotation.id);
670
+ },
671
+ children: "Remove"
672
+ }
673
+ )
674
+ ] })
675
+ ]
676
+ }
677
+ );
678
+ }
679
+ function resolvePosition(a) {
680
+ if (a.elementPath) {
681
+ try {
682
+ const el = document.querySelector(a.elementPath);
683
+ if (el) {
684
+ const r = el.getBoundingClientRect();
685
+ if (r.width || r.height) return { x: r.right - 11, y: r.top - 11 };
686
+ }
687
+ } catch {
688
+ }
689
+ }
690
+ if (a.boundingBox) {
691
+ return {
692
+ x: a.boundingBox.x + a.boundingBox.width - 11,
693
+ y: a.boundingBox.y - 11
694
+ };
695
+ }
696
+ return null;
697
+ }
698
+ function ClicklyRoot({
699
+ engine,
700
+ host
701
+ }) {
702
+ const [expanded, setExpanded] = react.useState(false);
703
+ const clearAnnotations = useAnnotations((s) => s.clear);
704
+ const outputDetail = useSettings((s) => s.outputDetail);
705
+ const markerColor = useSettings((s) => s.markerColor);
706
+ const engineState = useEngineState(engine);
707
+ react.useEffect(() => {
708
+ host.style.setProperty("--clickly-hover", markerColor);
709
+ }, [host, markerColor]);
710
+ react.useEffect(() => {
711
+ if (expanded) {
712
+ if (engine.getSnapshot().kind === "idle") engine.activate("single");
713
+ } else {
714
+ if (engine.getSnapshot().kind !== "idle") engine.deactivate();
715
+ }
716
+ }, [expanded, engine]);
717
+ react.useEffect(() => {
718
+ const active = engineState.kind !== "idle";
719
+ if (active) document.body.setAttribute("data-clickly-active", "");
720
+ else document.body.removeAttribute("data-clickly-active");
721
+ const mode = engineState.kind === "inspect" || engineState.kind === "pressed" || engineState.kind === "dragging" ? engineState.mode : null;
722
+ if (mode) document.body.setAttribute("data-clickly-mode", mode);
723
+ else document.body.removeAttribute("data-clickly-mode");
724
+ if (engineState.kind === "annotating") {
725
+ document.body.setAttribute("data-clickly-annotating", "");
726
+ } else {
727
+ document.body.removeAttribute("data-clickly-annotating");
728
+ }
729
+ return () => {
730
+ document.body.removeAttribute("data-clickly-active");
731
+ document.body.removeAttribute("data-clickly-mode");
732
+ document.body.removeAttribute("data-clickly-annotating");
733
+ };
734
+ }, [engineState]);
735
+ react.useEffect(() => {
736
+ const onKey = (e) => {
737
+ const tag = e.target?.tagName;
738
+ if (tag === "INPUT" || tag === "TEXTAREA") return;
739
+ if (e.target?.isContentEditable) return;
740
+ const active = document.activeElement;
741
+ if (active && (active.tagName === "TEXTAREA" || active.tagName === "INPUT")) return;
742
+ if (e.key.toLowerCase() === "f" && e.shiftKey && (e.metaKey || e.ctrlKey)) {
743
+ e.preventDefault();
744
+ setExpanded((v) => !v);
745
+ return;
746
+ }
747
+ if (e.key === "Escape") {
748
+ if (engine.getSnapshot().kind === "annotating") return;
749
+ if (expanded) setExpanded(false);
750
+ return;
751
+ }
752
+ if (!expanded) return;
753
+ if (e.key === "1") engine.setMode("single");
754
+ if (e.key === "2") engine.setMode("multi");
755
+ if (e.key === "3") engine.setMode("area");
756
+ if (e.key === "Enter") {
757
+ const s = engine.getSnapshot();
758
+ if (s.kind === "inspect" && s.pinned.length > 0) {
759
+ e.preventDefault();
760
+ engine.annotatePinned();
761
+ }
762
+ }
763
+ if (e.key === "c" || e.key === "C") {
764
+ const list = useAnnotations.getState().list();
765
+ if (list.length === 0) return;
766
+ e.preventDefault();
767
+ navigator.clipboard?.writeText(annotationsToMarkdown(list, outputDetail));
768
+ }
769
+ if (e.key === "x" || e.key === "X") {
770
+ if (useAnnotations.getState().list().length === 0) return;
771
+ e.preventDefault();
772
+ clearAnnotations();
773
+ }
774
+ };
775
+ window.addEventListener("keydown", onKey);
776
+ return () => window.removeEventListener("keydown", onKey);
777
+ }, [clearAnnotations, outputDetail, expanded, engine]);
778
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "clickly-ui", children: [
779
+ /* @__PURE__ */ jsxRuntime.jsx(AnnotationPins, {}),
780
+ expanded ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
781
+ /* @__PURE__ */ jsxRuntime.jsx(Toolbar, { engine, onCollapse: () => setExpanded(false) }),
782
+ /* @__PURE__ */ jsxRuntime.jsx(AnnotationPopup, { engine })
783
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(CollapsedFAB, { onExpand: () => setExpanded(true) })
784
+ ] });
785
+ }
786
+
787
+ // src/internal/styles.ts
788
+ var REACT_UI_CSS = `
789
+ .clickly-ui, .clickly-ui * { box-sizing: border-box; }
790
+
791
+ /* \u2500\u2500\u2500 Floating action button (collapsed state) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
792
+
793
+ .clickly-fab {
794
+ position: fixed;
795
+ right: 16px;
796
+ bottom: 16px;
797
+ width: 48px;
798
+ height: 48px;
799
+ border: none;
800
+ border-radius: 999px;
801
+ background: #0f172a;
802
+ color: #f8fafc;
803
+ display: grid;
804
+ place-items: center;
805
+ cursor: pointer;
806
+ pointer-events: auto;
807
+ user-select: none;
808
+ box-shadow:
809
+ 0 8px 24px rgba(0,0,0,0.30),
810
+ 0 0 0 1px rgba(255,255,255,0.06) inset;
811
+ transition: transform 140ms ease, box-shadow 140ms ease;
812
+ z-index: 1;
813
+ }
814
+ .clickly-fab:hover {
815
+ transform: scale(1.06);
816
+ box-shadow:
817
+ 0 10px 28px rgba(0,0,0,0.36),
818
+ 0 0 0 1px rgba(255,255,255,0.10) inset;
819
+ }
820
+ .clickly-fab:active { transform: scale(0.96); }
821
+ .clickly-fab svg { width: 18px; height: 18px; color: #f8fafc; }
822
+
823
+ .clickly-fab-badge {
824
+ position: absolute;
825
+ top: -2px;
826
+ right: -2px;
827
+ min-width: 18px;
828
+ height: 18px;
829
+ padding: 0 5px;
830
+ background: #f59e0b;
831
+ color: #0f172a;
832
+ font: 600 11px -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
833
+ border-radius: 9px;
834
+ display: grid;
835
+ place-items: center;
836
+ pointer-events: none;
837
+ }
838
+
839
+ /* \u2500\u2500\u2500 Toolbar (expanded state) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
840
+
841
+ .clickly-toolbar {
842
+ position: fixed;
843
+ display: flex;
844
+ align-items: center;
845
+ gap: 4px;
846
+ padding: 4px;
847
+ background: rgba(15, 23, 42, 0.94);
848
+ color: #f8fafc;
849
+ border-radius: 12px;
850
+ font: 13px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
851
+ box-shadow: 0 8px 24px rgba(0,0,0,0.3), 0 0 0 1px rgba(255,255,255,0.06) inset;
852
+ pointer-events: auto;
853
+ user-select: none;
854
+ z-index: 1;
855
+ animation: clickly-fade-in 120ms ease-out;
856
+ }
857
+
858
+ @keyframes clickly-fade-in {
859
+ from { opacity: 0; transform: translateY(4px); }
860
+ to { opacity: 1; transform: translateY(0); }
861
+ }
862
+
863
+ .clickly-toolbar .grip {
864
+ display: grid;
865
+ place-items: center;
866
+ width: 24px;
867
+ height: 28px;
868
+ color: #94a3b8;
869
+ touch-action: none;
870
+ }
871
+
872
+ .clickly-toolbar .divider {
873
+ width: 1px;
874
+ height: 18px;
875
+ background: rgba(255,255,255,0.12);
876
+ margin: 0 2px;
877
+ }
878
+
879
+ .clickly-btn {
880
+ display: inline-flex;
881
+ align-items: center;
882
+ gap: 4px;
883
+ height: 28px;
884
+ padding: 0 8px;
885
+ background: transparent;
886
+ border: none;
887
+ border-radius: 8px;
888
+ color: #cbd5e1;
889
+ cursor: pointer;
890
+ font: inherit;
891
+ }
892
+ .clickly-btn:hover { background: rgba(255,255,255,0.08); color: #fff; }
893
+ .clickly-btn:active { background: rgba(255,255,255,0.14); }
894
+ .clickly-btn.is-active { background: #0ea5e9; color: #fff; }
895
+ .clickly-btn[disabled] { opacity: 0.4; cursor: not-allowed; }
896
+
897
+ .clickly-btn.icon-only {
898
+ width: 28px;
899
+ padding: 0;
900
+ justify-content: center;
901
+ }
902
+
903
+ .clickly-btn.primary-pinned {
904
+ background: #10b981;
905
+ color: #fff;
906
+ font-weight: 500;
907
+ }
908
+ .clickly-btn.primary-pinned:hover {
909
+ background: #059669;
910
+ color: #fff;
911
+ }
912
+
913
+ /* \u2500\u2500\u2500 Popup & popovers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
914
+
915
+ .clickly-popup, .clickly-popover {
916
+ position: fixed;
917
+ background: #fff;
918
+ color: #0f172a;
919
+ border-radius: 10px;
920
+ box-shadow: 0 12px 32px rgba(2,6,23,0.18), 0 0 0 1px rgba(15,23,42,0.06);
921
+ font: 13px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
922
+ pointer-events: auto;
923
+ z-index: 2;
924
+ animation: clickly-fade-in 120ms ease-out;
925
+ }
926
+
927
+ .clickly-popup { width: 320px; padding: 12px; }
928
+
929
+ .clickly-popup .target-info {
930
+ display: flex;
931
+ align-items: center;
932
+ justify-content: space-between;
933
+ gap: 8px;
934
+ margin-bottom: 8px;
935
+ color: #475569;
936
+ font-size: 11px;
937
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
938
+ }
939
+
940
+ .clickly-popup textarea {
941
+ width: 100%;
942
+ min-height: 64px;
943
+ max-height: 160px;
944
+ padding: 8px;
945
+ border: 1px solid #e2e8f0;
946
+ border-radius: 6px;
947
+ resize: vertical;
948
+ font: inherit;
949
+ color: inherit;
950
+ }
951
+ .clickly-popup textarea:focus {
952
+ outline: none;
953
+ border-color: #0ea5e9;
954
+ box-shadow: 0 0 0 3px rgba(14,165,233,0.18);
955
+ }
956
+
957
+ .clickly-popup .row {
958
+ display: flex;
959
+ justify-content: flex-end;
960
+ gap: 6px;
961
+ margin-top: 10px;
962
+ }
963
+
964
+ .clickly-popup .row .ghost {
965
+ background: transparent;
966
+ color: #475569;
967
+ border: 1px solid #e2e8f0;
968
+ }
969
+ .clickly-popup .row .ghost:hover { background: #f8fafc; }
970
+
971
+ .clickly-popup .row .primary {
972
+ background: #0ea5e9;
973
+ color: #fff;
974
+ border: none;
975
+ }
976
+ .clickly-popup .row .primary:hover { background: #0284c7; }
977
+ .clickly-popup .row button {
978
+ height: 28px;
979
+ padding: 0 12px;
980
+ border-radius: 6px;
981
+ font: inherit;
982
+ cursor: pointer;
983
+ }
984
+
985
+ .clickly-popover { width: 260px; padding: 12px; }
986
+ .clickly-popover h4 { margin: 0 0 8px; font-size: 12px; color: #475569; text-transform: uppercase; letter-spacing: 0.05em; }
987
+ .clickly-popover .field { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 0; font-size: 13px; }
988
+ .clickly-popover select { font: inherit; padding: 4px 6px; border-radius: 4px; border: 1px solid #e2e8f0; }
989
+ .clickly-popover input[type="color"] { width: 28px; height: 24px; padding: 0; border: 1px solid #e2e8f0; border-radius: 4px; background: none; }
990
+
991
+ .clickly-counter {
992
+ display: inline-flex;
993
+ align-items: center;
994
+ justify-content: center;
995
+ min-width: 18px;
996
+ height: 18px;
997
+ padding: 0 5px;
998
+ border-radius: 9px;
999
+ background: #f59e0b;
1000
+ color: #1f2937;
1001
+ font-size: 11px;
1002
+ font-weight: 600;
1003
+ margin-left: 4px;
1004
+ }
1005
+
1006
+ /* \u2500\u2500\u2500 Annotation pins (persistent numbered markers) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1007
+
1008
+ .clickly-pin {
1009
+ position: fixed;
1010
+ width: 22px;
1011
+ height: 22px;
1012
+ border-radius: 999px;
1013
+ background: #f59e0b;
1014
+ color: #0f172a;
1015
+ font: 600 12px/22px -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
1016
+ text-align: center;
1017
+ cursor: pointer;
1018
+ pointer-events: auto;
1019
+ user-select: none;
1020
+ box-shadow: 0 2px 6px rgba(0,0,0,0.25), 0 0 0 2px #fff;
1021
+ z-index: 1;
1022
+ transition: transform 100ms ease;
1023
+ }
1024
+ .clickly-pin:hover { transform: scale(1.12); }
1025
+ .clickly-pin-num { display: block; }
1026
+
1027
+ .clickly-pin-bubble {
1028
+ position: absolute;
1029
+ top: 28px;
1030
+ left: 0;
1031
+ width: 240px;
1032
+ padding: 8px 10px;
1033
+ background: #fff;
1034
+ color: #0f172a;
1035
+ border-radius: 8px;
1036
+ box-shadow: 0 8px 24px rgba(2,6,23,0.18), 0 0 0 1px rgba(15,23,42,0.06);
1037
+ font: 13px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
1038
+ text-align: left;
1039
+ cursor: default;
1040
+ z-index: 3;
1041
+ animation: clickly-fade-in 100ms ease-out;
1042
+ }
1043
+ .clickly-pin-comment { word-break: break-word; }
1044
+ .clickly-pin-meta {
1045
+ margin-top: 6px;
1046
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
1047
+ font-size: 11px;
1048
+ color: #475569;
1049
+ word-break: break-all;
1050
+ }
1051
+ .clickly-pin-remove {
1052
+ margin-top: 8px;
1053
+ padding: 4px 8px;
1054
+ border: 1px solid #fecaca;
1055
+ background: #fff;
1056
+ color: #b91c1c;
1057
+ border-radius: 4px;
1058
+ font: 11px inherit;
1059
+ cursor: pointer;
1060
+ }
1061
+ .clickly-pin-remove:hover { background: #fef2f2; }
1062
+
1063
+ /* \u2500\u2500\u2500 Annotation list \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1064
+
1065
+ .clickly-list {
1066
+ position: fixed;
1067
+ max-height: 50vh;
1068
+ width: 320px;
1069
+ overflow-y: auto;
1070
+ background: #fff;
1071
+ border-radius: 10px;
1072
+ box-shadow: 0 12px 32px rgba(2,6,23,0.18), 0 0 0 1px rgba(15,23,42,0.06);
1073
+ color: #0f172a;
1074
+ font: 12px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
1075
+ pointer-events: auto;
1076
+ z-index: 2;
1077
+ animation: clickly-fade-in 120ms ease-out;
1078
+ }
1079
+ .clickly-list .row { display: flex; gap: 8px; padding: 8px 10px; border-bottom: 1px solid #f1f5f9; }
1080
+ .clickly-list .row:last-child { border-bottom: none; }
1081
+ .clickly-list .num { color: #94a3b8; min-width: 18px; }
1082
+ .clickly-list .selector { color: #475569; font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 11px; word-break: break-all; }
1083
+ .clickly-list .body { flex: 1; min-width: 0; }
1084
+ .clickly-list .body p { margin: 2px 0 0; word-break: break-word; }
1085
+ .clickly-list button.remove {
1086
+ background: transparent;
1087
+ border: none;
1088
+ color: #94a3b8;
1089
+ cursor: pointer;
1090
+ padding: 2px;
1091
+ display: grid;
1092
+ place-items: center;
1093
+ }
1094
+ .clickly-list button.remove svg { width: 12px; height: 12px; }
1095
+ .clickly-list button.remove:hover { color: #ef4444; }
1096
+ .clickly-list .empty { padding: 20px; text-align: center; color: #94a3b8; }
1097
+ `;
1098
+
1099
+ // src/internal/globalStyles.ts
1100
+ var GLOBAL_PAGE_CSS = `
1101
+ body[data-clickly-active] {
1102
+ -webkit-user-select: none !important;
1103
+ user-select: none !important;
1104
+ }
1105
+ body[data-clickly-active],
1106
+ body[data-clickly-active] *:not(input):not(textarea):not(select) {
1107
+ cursor: crosshair !important;
1108
+ }
1109
+ body[data-clickly-mode="multi"],
1110
+ body[data-clickly-mode="multi"] *:not(input):not(textarea):not(select) {
1111
+ cursor: copy !important;
1112
+ }
1113
+ /* Restore normal cursor while the annotation popup is open so the user
1114
+ can comfortably type in the textarea + click the buttons. */
1115
+ body[data-clickly-annotating] {
1116
+ cursor: default !important;
1117
+ }
1118
+ `;
1119
+ function Clickly({ className } = {}) {
1120
+ const [mount, setMount] = react.useState(null);
1121
+ react.useEffect(() => {
1122
+ if (typeof window === "undefined" || typeof document === "undefined") return;
1123
+ let shadow = null;
1124
+ let engine = null;
1125
+ let overlay = null;
1126
+ let portal = null;
1127
+ try {
1128
+ shadow = core.createShadowHost({ document });
1129
+ portal = document.createElement("div");
1130
+ portal.setAttribute("data-clickly-react-root", "");
1131
+ shadow.root.appendChild(portal);
1132
+ if (!shadow.root.querySelector("style[data-clickly-react]")) {
1133
+ const style = document.createElement("style");
1134
+ style.setAttribute("data-clickly-react", "");
1135
+ style.textContent = REACT_UI_CSS;
1136
+ shadow.root.appendChild(style);
1137
+ }
1138
+ if (!document.head.querySelector("style[data-clickly-globals]")) {
1139
+ const gstyle = document.createElement("style");
1140
+ gstyle.setAttribute("data-clickly-globals", "");
1141
+ gstyle.textContent = GLOBAL_PAGE_CSS;
1142
+ document.head.appendChild(gstyle);
1143
+ }
1144
+ engine = new core.SelectionEngine({ document, host: shadow.host });
1145
+ overlay = new core.Overlay({
1146
+ engine,
1147
+ root: shadow.root,
1148
+ document,
1149
+ excludeHost: shadow.host
1150
+ });
1151
+ setMount({ shadow, engine, overlay, portal });
1152
+ } catch (err) {
1153
+ console.warn("[clickly] failed to mount", err);
1154
+ overlay?.destroy();
1155
+ engine?.destroy();
1156
+ portal?.remove();
1157
+ shadow?.destroy();
1158
+ }
1159
+ return () => {
1160
+ setMount(null);
1161
+ overlay?.destroy();
1162
+ engine?.destroy();
1163
+ portal?.remove();
1164
+ shadow?.destroy();
1165
+ document.head.querySelector("style[data-clickly-globals]")?.remove();
1166
+ document.body.removeAttribute("data-clickly-active");
1167
+ document.body.removeAttribute("data-clickly-mode");
1168
+ };
1169
+ }, []);
1170
+ react.useEffect(() => {
1171
+ if (!mount || !className) return;
1172
+ mount.shadow.host.className = className;
1173
+ return () => {
1174
+ if (mount.shadow.host.isConnected) mount.shadow.host.className = "";
1175
+ };
1176
+ }, [mount, className]);
1177
+ if (!mount) return null;
1178
+ return reactDom.createPortal(
1179
+ /* @__PURE__ */ jsxRuntime.jsx(ClicklyRoot, { engine: mount.engine, host: mount.shadow.host }),
1180
+ mount.portal
1181
+ );
1182
+ }
1183
+
1184
+ exports.Clickly = Clickly;
1185
+ exports.DEFAULT_SETTINGS = DEFAULTS;
1186
+ exports.annotationsToMarkdown = annotationsToMarkdown;
1187
+ exports.useAnnotations = useAnnotations;
1188
+ exports.useAnnotationsList = useAnnotationsList;
1189
+ exports.useSettings = useSettings;
1190
+ //# sourceMappingURL=index.cjs.map
1191
+ //# sourceMappingURL=index.cjs.map