@useclickly/react 0.2.0 → 1.0.1

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 (52) hide show
  1. package/README.md +67 -19
  2. package/dist/Clickly.d.ts +25 -0
  3. package/dist/Clickly.d.ts.map +1 -0
  4. package/dist/hooks/useDraggable.d.ts +28 -0
  5. package/dist/hooks/useDraggable.d.ts.map +1 -0
  6. package/dist/hooks/usePersistedState.d.ts +6 -0
  7. package/dist/hooks/usePersistedState.d.ts.map +1 -0
  8. package/dist/index.cjs +2650 -370
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +3 -3
  11. package/dist/index.d.ts +12 -69
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +2581 -327
  14. package/dist/index.js.map +1 -1
  15. package/dist/internal/AnnotationList.d.ts +11 -0
  16. package/dist/internal/AnnotationList.d.ts.map +1 -0
  17. package/dist/internal/AnnotationPins.d.ts +7 -0
  18. package/dist/internal/AnnotationPins.d.ts.map +1 -0
  19. package/dist/internal/AnnotationPopup.d.ts +5 -0
  20. package/dist/internal/AnnotationPopup.d.ts.map +1 -0
  21. package/dist/internal/ClicklyRoot.d.ts +15 -0
  22. package/dist/internal/ClicklyRoot.d.ts.map +1 -0
  23. package/dist/internal/CollapsedFAB.d.ts +10 -0
  24. package/dist/internal/CollapsedFAB.d.ts.map +1 -0
  25. package/dist/internal/SettingsPopover.d.ts +11 -0
  26. package/dist/internal/SettingsPopover.d.ts.map +1 -0
  27. package/dist/internal/Toolbar.d.ts +8 -0
  28. package/dist/internal/Toolbar.d.ts.map +1 -0
  29. package/dist/internal/globalStyles.d.ts +13 -0
  30. package/dist/internal/globalStyles.d.ts.map +1 -0
  31. package/dist/internal/icons.d.ts +12 -0
  32. package/dist/internal/icons.d.ts.map +1 -0
  33. package/dist/internal/styles.d.ts +7 -0
  34. package/dist/internal/styles.d.ts.map +1 -0
  35. package/dist/output/markdown.d.ts +5 -0
  36. package/dist/output/markdown.d.ts.map +1 -0
  37. package/dist/output/markdown.test.d.ts +2 -0
  38. package/dist/output/markdown.test.d.ts.map +1 -0
  39. package/dist/state/annotations.d.ts +21 -0
  40. package/dist/state/annotations.d.ts.map +1 -0
  41. package/dist/state/annotations.test.d.ts +2 -0
  42. package/dist/state/annotations.test.d.ts.map +1 -0
  43. package/dist/state/settings.d.ts +14 -0
  44. package/dist/state/settings.d.ts.map +1 -0
  45. package/dist/state/settings.test.d.ts +2 -0
  46. package/dist/state/settings.test.d.ts.map +1 -0
  47. package/dist/state/useEngineState.d.ts +7 -0
  48. package/dist/state/useEngineState.d.ts.map +1 -0
  49. package/dist/test/setup.d.ts +7 -0
  50. package/dist/test/setup.d.ts.map +1 -0
  51. package/package.json +12 -12
  52. package/LICENSE +0 -21
package/dist/index.js CHANGED
@@ -1,13 +1,1028 @@
1
- "use client";
2
- import { useState, useEffect, useSyncExternalStore, useRef, useMemo, useCallback } from 'react';
3
- import { createPortal } from 'react-dom';
4
- import { createShadowHost, SelectionEngine, Overlay, collectMetadata } from '@useclickly/core';
5
- import { create } from 'zustand';
6
- import { useShallow } from 'zustand/react/shallow';
7
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
8
- import { nanoid } from 'nanoid';
9
-
10
- // src/Clickly.tsx
1
+ // packages/react/src/Clickly.tsx
2
+ import { useEffect as useEffect7, useState as useState7 } from "react";
3
+ import { createPortal } from "react-dom";
4
+
5
+ // packages/core/dist/index.js
6
+ function pickElementAt(doc, x, y, excludeHost) {
7
+ if (typeof doc.elementsFromPoint !== "function") return null;
8
+ const chain = doc.elementsFromPoint(x, y);
9
+ for (const el of chain) {
10
+ if (!isInExcludedSubtree(el, excludeHost)) return el;
11
+ }
12
+ return null;
13
+ }
14
+ function pickElementsInRect(root, rect, excludeHost) {
15
+ const out = [];
16
+ const stack = [root];
17
+ while (stack.length) {
18
+ const el = stack.pop();
19
+ if (isInExcludedSubtree(el, excludeHost)) continue;
20
+ const box = el.getBoundingClientRect();
21
+ if (box.width > 0 && box.height > 0 && containedIn(box, rect)) {
22
+ out.push(el);
23
+ }
24
+ for (let i = 0; i < el.children.length; i++) {
25
+ const child = el.children[i];
26
+ if (child) stack.push(child);
27
+ }
28
+ }
29
+ return out;
30
+ }
31
+ function containedIn(el, sel) {
32
+ return el.left >= sel.x && el.top >= sel.y && el.right <= sel.x + sel.width && el.bottom <= sel.y + sel.height;
33
+ }
34
+ function isInExcludedSubtree(el, host) {
35
+ if (!host) return false;
36
+ let cur = el;
37
+ while (cur) {
38
+ if (cur === host) return true;
39
+ cur = cur.parentNode;
40
+ }
41
+ return false;
42
+ }
43
+ function manhattan(a, b) {
44
+ return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
45
+ }
46
+ function rectFromPoints(start, current) {
47
+ const x = Math.min(start.x, current.x);
48
+ const y = Math.min(start.y, current.y);
49
+ const width = Math.abs(current.x - start.x);
50
+ const height = Math.abs(current.y - start.y);
51
+ return { x, y, width, height };
52
+ }
53
+ var DRAG_THRESHOLD_PX = 12;
54
+ var initialState = { kind: "idle" };
55
+ function reduce(state, event) {
56
+ if (event.type === "DEACTIVATE") return { kind: "idle" };
57
+ if (event.type === "ESCAPE") {
58
+ if (state.kind === "idle") return state;
59
+ if (state.kind === "annotating") return resumeInspect(
60
+ [],
61
+ /* mode */
62
+ void 0
63
+ );
64
+ if (state.kind === "inspect" && state.pinned.length > 0) {
65
+ return { ...state, pinned: [], hoverTarget: null };
66
+ }
67
+ return resumeInspect("pinned" in state ? state.pinned : []);
68
+ }
69
+ if (event.type === "CLEAR_PINNED") {
70
+ if (state.kind === "idle") return state;
71
+ if (state.kind === "inspect") return { ...state, pinned: [] };
72
+ return state;
73
+ }
74
+ switch (state.kind) {
75
+ case "idle":
76
+ if (event.type === "ACTIVATE") {
77
+ return {
78
+ kind: "inspect",
79
+ mode: event.mode ?? "single",
80
+ hoverTarget: null,
81
+ pinned: []
82
+ };
83
+ }
84
+ return state;
85
+ case "inspect":
86
+ if (event.type === "MODE_CHANGE") return { ...state, mode: event.mode };
87
+ if (event.type === "POINTER_MOVE") return { ...state, hoverTarget: event.target };
88
+ if (event.type === "POINTER_DOWN") {
89
+ return {
90
+ kind: "pressed",
91
+ mode: state.mode,
92
+ start: event.point,
93
+ target: event.target,
94
+ additive: event.additive,
95
+ pinned: state.pinned
96
+ };
97
+ }
98
+ if (event.type === "ANNOTATE_PINNED") {
99
+ if (state.pinned.length === 0) return state;
100
+ return {
101
+ kind: "annotating",
102
+ selection: { kind: "multi", elements: state.pinned },
103
+ pinned: state.pinned
104
+ };
105
+ }
106
+ return state;
107
+ case "pressed":
108
+ if (event.type === "POINTER_MOVE") {
109
+ if (manhattan(state.start, event.point) >= DRAG_THRESHOLD_PX) {
110
+ return {
111
+ kind: "dragging",
112
+ mode: state.mode,
113
+ start: state.start,
114
+ current: event.point,
115
+ pinned: state.pinned
116
+ };
117
+ }
118
+ return state;
119
+ }
120
+ if (event.type === "POINTER_UP") {
121
+ if (!state.target) {
122
+ return resumeInspect(state.pinned, state.mode);
123
+ }
124
+ if (state.additive || state.mode === "multi") {
125
+ const nextPinned = togglePinned(state.pinned, state.target);
126
+ return {
127
+ kind: "inspect",
128
+ mode: state.mode,
129
+ hoverTarget: state.target,
130
+ pinned: nextPinned
131
+ };
132
+ }
133
+ return {
134
+ kind: "annotating",
135
+ selection: { kind: "single", element: state.target },
136
+ pinned: state.pinned
137
+ };
138
+ }
139
+ return state;
140
+ case "dragging":
141
+ if (event.type === "POINTER_MOVE") return { ...state, current: event.point };
142
+ if (event.type === "POINTER_UP") {
143
+ const rect = rectFromPoints(state.start, state.current);
144
+ const selection = { kind: "area", rect, elements: [] };
145
+ return { kind: "annotating", selection, pinned: state.pinned };
146
+ }
147
+ return state;
148
+ case "annotating":
149
+ if (event.type === "COMMIT") {
150
+ return resumeInspect([]);
151
+ }
152
+ return state;
153
+ }
154
+ }
155
+ function resumeInspect(pinned, mode) {
156
+ return {
157
+ kind: "inspect",
158
+ mode: mode ?? "single",
159
+ hoverTarget: null,
160
+ pinned
161
+ };
162
+ }
163
+ function togglePinned(pinned, el) {
164
+ const idx = pinned.indexOf(el);
165
+ if (idx === -1) return [...pinned, el];
166
+ const next = pinned.slice();
167
+ next.splice(idx, 1);
168
+ return next;
169
+ }
170
+ var SelectionEngine = class {
171
+ state = initialState;
172
+ listeners = /* @__PURE__ */ new Set();
173
+ doc;
174
+ host;
175
+ raf;
176
+ caf;
177
+ searchRoot;
178
+ pendingPointer = null;
179
+ rafHandle = null;
180
+ /** Guard for the pointermove RAF coalescer. Separate from `rafHandle`
181
+ * because a synchronous `raf` (used in tests) returns a handle the cb
182
+ * has already invalidated — boolean is the safe sentinel. */
183
+ rafPending = false;
184
+ attached = false;
185
+ boundHandlers = [];
186
+ constructor(deps = {}) {
187
+ this.doc = deps.document ?? (typeof document !== "undefined" ? document : null);
188
+ if (!this.doc) {
189
+ throw new Error("SelectionEngine: no Document available (pass `deps.document`).");
190
+ }
191
+ this.host = deps.host ?? null;
192
+ this.raf = deps.raf ?? ((cb) => typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame(cb) : setTimeout(() => cb(performance.now()), 16));
193
+ this.caf = deps.caf ?? ((h) => {
194
+ if (typeof cancelAnimationFrame !== "undefined") cancelAnimationFrame(h);
195
+ else clearTimeout(h);
196
+ });
197
+ this.searchRoot = deps.searchRoot ?? this.doc.body;
198
+ }
199
+ /* ─── Subscribable<EngineState> ───────────────────────────────── */
200
+ subscribe(listener) {
201
+ this.listeners.add(listener);
202
+ return () => this.listeners.delete(listener);
203
+ }
204
+ getSnapshot() {
205
+ return this.state;
206
+ }
207
+ /* ─── Public control ──────────────────────────────────────────── */
208
+ activate(mode) {
209
+ perfMark("clickly:engine:activate");
210
+ this.dispatch(mode ? { type: "ACTIVATE", mode } : { type: "ACTIVATE" });
211
+ this.attach();
212
+ }
213
+ deactivate() {
214
+ perfMark("clickly:engine:deactivate");
215
+ this.detach();
216
+ this.dispatch({ type: "DEACTIVATE" });
217
+ }
218
+ setMode(mode) {
219
+ this.dispatch({ type: "MODE_CHANGE", mode });
220
+ }
221
+ commit() {
222
+ this.dispatch({ type: "COMMIT" });
223
+ }
224
+ clearPinned() {
225
+ this.dispatch({ type: "CLEAR_PINNED" });
226
+ }
227
+ /** Open the annotation popup populated with everything currently pinned. */
228
+ annotatePinned() {
229
+ this.dispatch({ type: "ANNOTATE_PINNED" });
230
+ }
231
+ /**
232
+ * Returns the resolved selection from the current annotating state, with
233
+ * area-mode element enumeration filled in (the reducer leaves it empty).
234
+ * Returns null if not currently annotating.
235
+ */
236
+ resolveSelection() {
237
+ if (this.state.kind !== "annotating") return null;
238
+ const sel = this.state.selection;
239
+ if (sel.kind !== "area") return sel;
240
+ const elements = pickElementsInRect(this.searchRoot, sel.rect, this.host);
241
+ return { ...sel, elements };
242
+ }
243
+ /* ─── Lifecycle ───────────────────────────────────────────────── */
244
+ attach() {
245
+ if (this.attached) return;
246
+ this.attached = true;
247
+ const win = this.doc.defaultView ?? globalThis;
248
+ this.bind(this.doc, "pointermove", this.onPointerMove, { passive: true });
249
+ this.bind(this.doc, "pointerdown", this.onPointerDown);
250
+ this.bind(this.doc, "pointerup", this.onPointerUp);
251
+ this.bind(win, "keydown", this.onKeyDown);
252
+ }
253
+ detach() {
254
+ if (!this.attached) return;
255
+ for (const [t, ev, fn, opts] of this.boundHandlers) t.removeEventListener(ev, fn, opts);
256
+ this.boundHandlers = [];
257
+ this.attached = false;
258
+ if (this.rafHandle !== null) {
259
+ this.caf(this.rafHandle);
260
+ this.rafHandle = null;
261
+ }
262
+ this.rafPending = false;
263
+ }
264
+ destroy() {
265
+ this.detach();
266
+ this.listeners.clear();
267
+ }
268
+ bind(target, ev, fn, opts) {
269
+ target.addEventListener(ev, fn, opts);
270
+ this.boundHandlers.push([target, ev, fn, opts]);
271
+ }
272
+ /* ─── DOM event handlers ──────────────────────────────────────── */
273
+ onPointerMove = (e) => {
274
+ this.pendingPointer = { x: e.clientX, y: e.clientY };
275
+ if (this.rafPending) return;
276
+ this.rafPending = true;
277
+ this.rafHandle = this.raf(() => {
278
+ this.rafPending = false;
279
+ this.rafHandle = null;
280
+ const pt = this.pendingPointer;
281
+ this.pendingPointer = null;
282
+ if (!pt) return;
283
+ const target = pickElementAt(this.doc, pt.x, pt.y, this.host);
284
+ this.dispatch({ type: "POINTER_MOVE", point: pt, target });
285
+ });
286
+ };
287
+ onPointerDown = (e) => {
288
+ if (this.host && e.composedPath().includes(this.host)) return;
289
+ const target = pickElementAt(this.doc, e.clientX, e.clientY, this.host);
290
+ this.dispatch({
291
+ type: "POINTER_DOWN",
292
+ point: { x: e.clientX, y: e.clientY },
293
+ target,
294
+ additive: e.shiftKey || e.metaKey || e.ctrlKey
295
+ });
296
+ };
297
+ onPointerUp = (e) => {
298
+ this.dispatch({ type: "POINTER_UP", point: { x: e.clientX, y: e.clientY } });
299
+ };
300
+ onKeyDown = (e) => {
301
+ if (e.key === "Escape") this.dispatch({ type: "ESCAPE" });
302
+ };
303
+ /* ─── Reducer plumbing ────────────────────────────────────────── */
304
+ dispatch(event) {
305
+ const next = reduce(this.state, event);
306
+ if (next === this.state) return;
307
+ this.state = next;
308
+ for (const l of this.listeners) l(next);
309
+ }
310
+ };
311
+ function perfMark(name) {
312
+ if (typeof performance !== "undefined" && typeof performance.mark === "function") {
313
+ try {
314
+ performance.mark(name);
315
+ } catch {
316
+ }
317
+ }
318
+ }
319
+ var OVERLAY_CSS = `
320
+ :host {
321
+ --clickly-hover: #06b6d4;
322
+ --clickly-pinned: #f59e0b;
323
+ --clickly-selected: #10b981;
324
+ --clickly-marquee-stroke: #10b981;
325
+ --clickly-marquee-fill: rgba(16, 185, 129, 0.10);
326
+ --clickly-label-bg: rgba(15, 23, 42, 0.92);
327
+ --clickly-label-fg: #f8fafc;
328
+ --clickly-shadow: 0 0 0 1px rgba(255,255,255,0.5);
329
+
330
+ all: initial;
331
+ position: fixed;
332
+ inset: 0;
333
+ z-index: 2147483647;
334
+ pointer-events: none;
335
+ contain: layout style paint;
336
+ isolation: isolate;
337
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
338
+ }
339
+
340
+ .layer {
341
+ position: fixed;
342
+ left: 0;
343
+ top: 0;
344
+ width: 0;
345
+ height: 0;
346
+ pointer-events: none;
347
+ will-change: transform, width, height, opacity;
348
+ }
349
+
350
+ .marker {
351
+ position: fixed;
352
+ left: 0;
353
+ top: 0;
354
+ box-sizing: border-box;
355
+ border-radius: 2px;
356
+ pointer-events: none;
357
+ will-change: transform, width, height, opacity;
358
+ transition: opacity 80ms linear;
359
+ }
360
+
361
+ .marker[hidden] { display: none; }
362
+
363
+ .marker.hover { box-shadow: 0 0 0 2px var(--clickly-hover), var(--clickly-shadow); }
364
+ .marker.pinned { box-shadow: 0 0 0 2px var(--clickly-pinned), var(--clickly-shadow); }
365
+ .marker.selected { box-shadow: 0 0 0 2px var(--clickly-selected), var(--clickly-shadow); }
366
+
367
+ /* Marquee \u2014 used during drag (dashed) AND for the committed union
368
+ box around multi/area selections (solid). */
369
+ .marker.marquee {
370
+ border: 2px dashed var(--clickly-marquee-stroke);
371
+ background: var(--clickly-marquee-fill);
372
+ border-radius: 8px;
373
+ }
374
+ .marker.marquee.is-committed {
375
+ border-style: solid;
376
+ box-shadow: 0 0 0 1px rgba(16, 185, 129, 0.18);
377
+ }
378
+
379
+ .label {
380
+ position: fixed;
381
+ left: 0;
382
+ top: 0;
383
+ padding: 2px 6px;
384
+ background: var(--clickly-label-bg);
385
+ color: var(--clickly-label-fg);
386
+ font-size: 11px;
387
+ line-height: 1.4;
388
+ border-radius: 4px;
389
+ white-space: nowrap;
390
+ pointer-events: none;
391
+ user-select: none;
392
+ max-width: 50vw;
393
+ overflow: hidden;
394
+ text-overflow: ellipsis;
395
+ }
396
+ `;
397
+ var HOST_TAG = "clickly-root";
398
+ function createShadowHost(deps = {}) {
399
+ const doc = deps.document ?? (typeof document !== "undefined" ? document : null);
400
+ if (!doc) throw new Error("createShadowHost: no Document available");
401
+ const existing = doc.querySelector(HOST_TAG);
402
+ const host = existing ?? doc.createElement(HOST_TAG);
403
+ if (!existing) doc.body.appendChild(host);
404
+ const root = host.shadowRoot ?? host.attachShadow({ mode: "open" });
405
+ if (!root.querySelector("style[data-clickly]")) {
406
+ const style = doc.createElement("style");
407
+ style.setAttribute("data-clickly", "");
408
+ style.textContent = OVERLAY_CSS;
409
+ root.appendChild(style);
410
+ }
411
+ return {
412
+ host,
413
+ root,
414
+ destroy() {
415
+ host.remove();
416
+ }
417
+ };
418
+ }
419
+ var OverlayRenderer = class {
420
+ root;
421
+ document;
422
+ layer;
423
+ hover;
424
+ hoverLabel;
425
+ /** Marquee rect — used both for live drag AND for the union box
426
+ * drawn around multi-element selections after commit. */
427
+ marquee;
428
+ /** Pinned/selected single-element rings (recycled across renders). */
429
+ pinnedPool = [];
430
+ selectedPool = [];
431
+ /** Last rendered state — used to re-render on scroll without a state change. */
432
+ lastState = null;
433
+ constructor(root, document2) {
434
+ this.root = root;
435
+ this.document = document2 ?? root.ownerDocument ?? (typeof globalThis !== "undefined" && globalThis.document ? globalThis.document : null);
436
+ if (!this.document) throw new Error("OverlayRenderer: no Document available");
437
+ this.layer = this.div("layer");
438
+ this.root.appendChild(this.layer);
439
+ this.hover = this.div("marker hover");
440
+ this.hover.hidden = true;
441
+ this.layer.appendChild(this.hover);
442
+ this.hoverLabel = this.div("label");
443
+ this.hoverLabel.hidden = true;
444
+ this.layer.appendChild(this.hoverLabel);
445
+ this.marquee = this.div("marker marquee");
446
+ this.marquee.hidden = true;
447
+ this.layer.appendChild(this.marquee);
448
+ this.document.addEventListener("scroll", this.onScroll, {
449
+ passive: true,
450
+ capture: true
451
+ });
452
+ }
453
+ onScroll = () => {
454
+ if (this.lastState) this.renderState(this.lastState);
455
+ };
456
+ render(state) {
457
+ this.lastState = state;
458
+ this.renderState(state);
459
+ }
460
+ renderState(state) {
461
+ this.renderSingle(this.hover, state.hover);
462
+ if (state.hover) {
463
+ this.hoverLabel.hidden = false;
464
+ this.hoverLabel.textContent = describeElement(state.hover);
465
+ const rect = state.hover.getBoundingClientRect();
466
+ const labelY = rect.top - 20 < 0 ? rect.bottom + 4 : rect.top - 20;
467
+ moveTo(this.hoverLabel, rect.left, labelY);
468
+ } else {
469
+ this.hoverLabel.hidden = true;
470
+ }
471
+ this.renderList(this.pinnedPool, state.pinned, "marker pinned");
472
+ const sel = state.selection ?? [];
473
+ const isMultiUnion = sel.length > 1 || state.marquee !== null && sel.length === 0;
474
+ if (sel.length === 1) {
475
+ this.renderList(this.selectedPool, [sel[0]], "marker selected");
476
+ } else {
477
+ this.renderList(this.selectedPool, [], "marker selected");
478
+ }
479
+ const marqueeRect = state.marquee ?? (sel.length > 1 ? unionOf(sel) : null);
480
+ if (marqueeRect) {
481
+ this.marquee.classList.toggle("is-committed", state.marquee === null);
482
+ this.placeRect(this.marquee, marqueeRect);
483
+ this.marquee.hidden = false;
484
+ } else {
485
+ this.marquee.hidden = true;
486
+ }
487
+ void isMultiUnion;
488
+ }
489
+ destroy() {
490
+ this.document.removeEventListener("scroll", this.onScroll, { capture: true });
491
+ this.lastState = null;
492
+ this.layer.remove();
493
+ }
494
+ /* ─── Internals ───────────────────────────────────────────────── */
495
+ renderSingle(node, target) {
496
+ if (!target) {
497
+ node.hidden = true;
498
+ return;
499
+ }
500
+ const r = target.getBoundingClientRect();
501
+ if (r.width === 0 && r.height === 0) {
502
+ node.hidden = true;
503
+ return;
504
+ }
505
+ this.placeRect(node, { x: r.left, y: r.top, width: r.width, height: r.height });
506
+ node.hidden = false;
507
+ }
508
+ renderList(pool, targets, className) {
509
+ while (pool.length < targets.length) {
510
+ const el = this.div(className);
511
+ el.hidden = true;
512
+ this.layer.appendChild(el);
513
+ pool.push(el);
514
+ }
515
+ for (let i = 0; i < pool.length; i++) {
516
+ const node = pool[i];
517
+ const target = targets[i];
518
+ if (!target) {
519
+ node.hidden = true;
520
+ continue;
521
+ }
522
+ const r = target.getBoundingClientRect();
523
+ if (r.width === 0 && r.height === 0) {
524
+ node.hidden = true;
525
+ continue;
526
+ }
527
+ this.placeRect(node, { x: r.left, y: r.top, width: r.width, height: r.height });
528
+ node.hidden = false;
529
+ }
530
+ }
531
+ placeRect(node, rect) {
532
+ moveTo(node, rect.x, rect.y);
533
+ node.style.width = `${rect.width}px`;
534
+ node.style.height = `${rect.height}px`;
535
+ }
536
+ div(className) {
537
+ const el = this.document.createElement("div");
538
+ el.className = className;
539
+ return el;
540
+ }
541
+ };
542
+ function moveTo(node, x, y) {
543
+ const tx = Math.round(x);
544
+ const ty = Math.round(y);
545
+ node.style.transform = `translate3d(${tx}px, ${ty}px, 0)`;
546
+ }
547
+ var TAG_LABELS = {
548
+ p: "paragraph",
549
+ h1: "heading",
550
+ h2: "heading",
551
+ h3: "heading",
552
+ h4: "heading",
553
+ h5: "heading",
554
+ h6: "heading",
555
+ a: "link",
556
+ button: "button",
557
+ input: "input",
558
+ textarea: "textarea",
559
+ select: "select",
560
+ img: "image",
561
+ video: "video",
562
+ audio: "audio",
563
+ form: "form",
564
+ nav: "nav",
565
+ header: "header",
566
+ footer: "footer",
567
+ main: "main",
568
+ section: "section",
569
+ article: "article",
570
+ aside: "aside",
571
+ ul: "list",
572
+ ol: "list",
573
+ li: "list item",
574
+ table: "table",
575
+ thead: "table head",
576
+ tbody: "table body",
577
+ tr: "table row",
578
+ td: "cell",
579
+ th: "header cell",
580
+ span: "span",
581
+ div: "div",
582
+ label: "label",
583
+ code: "code",
584
+ pre: "code block",
585
+ blockquote: "quote",
586
+ strong: "bold",
587
+ em: "italic",
588
+ kbd: "key",
589
+ svg: "svg",
590
+ canvas: "canvas"
591
+ };
592
+ function describeElement(el) {
593
+ const tag = el.tagName.toLowerCase();
594
+ const type = TAG_LABELS[tag] ?? tag;
595
+ const text = (el.textContent ?? "").replace(/\s+/g, " ").trim();
596
+ if (text.length > 0) {
597
+ const preview = text.length > 48 ? text.slice(0, 48) + "\u2026" : text;
598
+ return `${type}: "${preview}"`;
599
+ }
600
+ if (el.id) return `${type}: #${el.id}`;
601
+ const cls = el.classList[0];
602
+ if (cls) return `${type}: .${cls}`;
603
+ return type;
604
+ }
605
+ function unionOf(elements) {
606
+ let minX = Infinity;
607
+ let minY = Infinity;
608
+ let maxX = -Infinity;
609
+ let maxY = -Infinity;
610
+ let any = false;
611
+ for (const el of elements) {
612
+ const r = el.getBoundingClientRect();
613
+ if (r.width === 0 && r.height === 0) continue;
614
+ any = true;
615
+ if (r.left < minX) minX = r.left;
616
+ if (r.top < minY) minY = r.top;
617
+ if (r.right > maxX) maxX = r.right;
618
+ if (r.bottom > maxY) maxY = r.bottom;
619
+ }
620
+ if (!any) return null;
621
+ const PAD = 4;
622
+ return {
623
+ x: minX - PAD,
624
+ y: minY - PAD,
625
+ width: maxX - minX + PAD * 2,
626
+ height: maxY - minY + PAD * 2
627
+ };
628
+ }
629
+ var emptyRenderState = {
630
+ hover: null,
631
+ pinned: [],
632
+ selection: null,
633
+ marquee: null
634
+ };
635
+ var Overlay = class {
636
+ renderer;
637
+ engine;
638
+ doc;
639
+ win;
640
+ searchRoot;
641
+ excludeHost;
642
+ raf;
643
+ caf;
644
+ state = emptyRenderState;
645
+ unsubscribe = null;
646
+ rafHandle = null;
647
+ /** Guard for scheduleRender re-entry. Tracked separately from `rafHandle`
648
+ * because a synchronous `raf` (used in tests) returns a handle the cb has
649
+ * already invalidated — boolean is the safe sentinel. */
650
+ renderPending = false;
651
+ destroyed = false;
652
+ bound = [];
653
+ constructor(opts) {
654
+ this.engine = opts.engine;
655
+ this.doc = opts.document ?? opts.root.ownerDocument ?? null;
656
+ if (!this.doc) throw new Error("Overlay: no Document available");
657
+ this.win = this.doc.defaultView ?? globalThis;
658
+ this.searchRoot = opts.searchRoot ?? this.doc.body;
659
+ this.excludeHost = opts.excludeHost ?? null;
660
+ this.raf = opts.raf ?? ((cb) => typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame(cb) : setTimeout(() => cb(performance.now()), 16));
661
+ this.caf = opts.caf ?? ((h) => {
662
+ if (typeof cancelAnimationFrame !== "undefined") cancelAnimationFrame(h);
663
+ else clearTimeout(h);
664
+ });
665
+ this.renderer = new OverlayRenderer(opts.root, this.doc);
666
+ this.bindEvent(this.doc, "scroll", this.onReposition, { passive: true, capture: true });
667
+ this.bindEvent(this.win, "resize", this.onReposition, { passive: true });
668
+ this.unsubscribe = this.engine.subscribe((s) => this.onEngineState(s));
669
+ this.onEngineState(this.engine.getSnapshot());
670
+ }
671
+ destroy() {
672
+ this.destroyed = true;
673
+ if (this.unsubscribe) this.unsubscribe();
674
+ this.unsubscribe = null;
675
+ if (this.rafHandle !== null) this.caf(this.rafHandle);
676
+ this.rafHandle = null;
677
+ for (const [t, ev, fn, opts] of this.bound) t.removeEventListener(ev, fn, opts);
678
+ this.bound = [];
679
+ this.renderer.destroy();
680
+ }
681
+ /* ─── Internals ───────────────────────────────────────────────── */
682
+ onReposition = () => {
683
+ if (this.destroyed) return;
684
+ if (hasAnythingToTrack(this.state)) this.scheduleRender();
685
+ };
686
+ onEngineState(s) {
687
+ this.state = this.derive(s);
688
+ this.scheduleRender();
689
+ }
690
+ /** Engine state → render state. */
691
+ derive(s) {
692
+ switch (s.kind) {
693
+ case "idle":
694
+ return emptyRenderState;
695
+ case "inspect":
696
+ return { hover: s.hoverTarget, pinned: s.pinned, selection: null, marquee: null };
697
+ case "pressed":
698
+ return { hover: s.target, pinned: s.pinned, selection: null, marquee: null };
699
+ case "dragging": {
700
+ const rect = rectFromPoints(s.start, s.current);
701
+ return { hover: null, pinned: s.pinned, selection: null, marquee: rect };
702
+ }
703
+ case "annotating": {
704
+ const sel = s.selection;
705
+ const elements = sel.kind === "area" ? pickElementsInRect(this.searchRoot, sel.rect, this.excludeHost) : sel.kind === "multi" ? sel.elements : [sel.element];
706
+ return { hover: null, pinned: s.pinned, selection: elements, marquee: null };
707
+ }
708
+ }
709
+ }
710
+ scheduleRender() {
711
+ if (this.destroyed || this.renderPending) return;
712
+ this.renderPending = true;
713
+ this.rafHandle = this.raf(() => {
714
+ this.renderPending = false;
715
+ this.rafHandle = null;
716
+ if (this.destroyed) return;
717
+ this.renderer.render(this.state);
718
+ });
719
+ }
720
+ bindEvent(target, ev, fn, opts) {
721
+ target.addEventListener(ev, fn, opts);
722
+ this.bound.push([target, ev, fn, opts]);
723
+ }
724
+ };
725
+ function hasAnythingToTrack(s) {
726
+ return s.hover !== null || s.pinned.length > 0 || s.selection !== null && s.selection.length > 0 || s.marquee !== null;
727
+ }
728
+ var FIBER_PROP_PREFIX = "__reactFiber$";
729
+ function getFiber(node) {
730
+ if (!node) return null;
731
+ for (const key of Object.keys(node)) {
732
+ if (key.startsWith(FIBER_PROP_PREFIX)) return node[key];
733
+ }
734
+ return null;
735
+ }
736
+ function getComponentChain(node) {
737
+ const fiber = getFiber(node);
738
+ if (!fiber) return [];
739
+ const names = [];
740
+ let cur = fiber;
741
+ while (cur) {
742
+ const name = getComponentName(cur);
743
+ if (name) names.unshift(name);
744
+ cur = cur.return;
745
+ }
746
+ return dedupeConsecutive(names);
747
+ }
748
+ function getComponentName(fiber) {
749
+ const t = fiber.type;
750
+ if (!t) return null;
751
+ if (typeof t === "string") return null;
752
+ if (typeof t === "function") {
753
+ const fn = t;
754
+ return fn.displayName || fn.name || null;
755
+ }
756
+ if (typeof t === "object") {
757
+ const o = t;
758
+ if (o.displayName) return o.displayName;
759
+ if (o.render) return o.render.displayName || o.render.name || null;
760
+ if (o.type) return o.type.displayName || o.type.name || null;
761
+ }
762
+ return null;
763
+ }
764
+ function dedupeConsecutive(names) {
765
+ const out = [];
766
+ for (const n of names) {
767
+ if (out[out.length - 1] !== n) out.push(n);
768
+ }
769
+ return out;
770
+ }
771
+ function getSourceInfo(node) {
772
+ const fiber = getFiber(node);
773
+ if (!fiber) return null;
774
+ let cur = fiber._debugOwner;
775
+ while (cur) {
776
+ if (cur._debugSource) return cur._debugSource;
777
+ cur = cur._debugOwner;
778
+ }
779
+ cur = fiber;
780
+ while (cur) {
781
+ if (cur._debugSource) return cur._debugSource;
782
+ cur = cur.return;
783
+ }
784
+ return null;
785
+ }
786
+ function collectAccessibility(el) {
787
+ const parts = [];
788
+ const role = el.getAttribute("role");
789
+ if (role) parts.push(`role=${role}`);
790
+ for (const attr of Array.from(el.attributes)) {
791
+ if (attr.name.startsWith("aria-") && attr.value) {
792
+ parts.push(`${attr.name}=${attr.value}`);
793
+ }
794
+ }
795
+ const tabindex = el.getAttribute("tabindex");
796
+ if (tabindex !== null && tabindex !== "") parts.push(`tabindex=${tabindex}`);
797
+ const title = el.getAttribute("title");
798
+ if (title) parts.push(`title=${title}`);
799
+ return parts.join("; ");
800
+ }
801
+ var GROUPS = {
802
+ layout: [
803
+ "display",
804
+ "position",
805
+ "top",
806
+ "right",
807
+ "bottom",
808
+ "left",
809
+ "width",
810
+ "height",
811
+ "margin",
812
+ "padding",
813
+ "box-sizing"
814
+ ],
815
+ visual: [
816
+ "color",
817
+ "background-color",
818
+ "border",
819
+ "border-radius",
820
+ "outline"
821
+ ],
822
+ text: [
823
+ "font-family",
824
+ "font-size",
825
+ "font-weight",
826
+ "line-height",
827
+ "letter-spacing",
828
+ "text-align",
829
+ "white-space"
830
+ ],
831
+ flexgrid: [
832
+ "flex-direction",
833
+ "justify-content",
834
+ "align-items",
835
+ "gap",
836
+ "grid-template-columns",
837
+ "grid-template-rows"
838
+ ],
839
+ effects: ["transform", "box-shadow", "filter"],
840
+ misc: ["cursor", "z-index", "overflow", "opacity", "transition", "animation"]
841
+ };
842
+ var TIER_GROUPS = {
843
+ compact: [],
844
+ standard: ["layout", "visual", "text"],
845
+ detailed: ["layout", "visual", "text", "flexgrid", "effects"],
846
+ forensic: ["layout", "visual", "text", "flexgrid", "effects", "misc"]
847
+ };
848
+ function collectComputedStyles(el, detail) {
849
+ if (detail === "compact") return {};
850
+ const doc = el.ownerDocument;
851
+ const win = doc?.defaultView ?? globalThis;
852
+ if (typeof win.getComputedStyle !== "function") return {};
853
+ const cs = win.getComputedStyle(el);
854
+ const props = /* @__PURE__ */ new Set();
855
+ for (const g of TIER_GROUPS[detail]) {
856
+ for (const p of GROUPS[g]) props.add(p);
857
+ }
858
+ const out = {};
859
+ for (const p of props) {
860
+ const v = cs.getPropertyValue(p);
861
+ if (!v) continue;
862
+ const trimmed = v.trim();
863
+ if (isUninterestingDefault(p, trimmed)) continue;
864
+ out[p] = trimmed;
865
+ }
866
+ return out;
867
+ }
868
+ function isUninterestingDefault(prop, value) {
869
+ if (!value || value === "none" || value === "auto" || value === "normal") return true;
870
+ if (value === "0px" || value === "0%") return true;
871
+ if (value === "rgba(0, 0, 0, 0)") return true;
872
+ if (prop === "color" || prop === "background-color") {
873
+ return value === "rgba(0, 0, 0, 0)";
874
+ }
875
+ return false;
876
+ }
877
+ var SHORT_MAX_DEPTH = 5;
878
+ var FULL_MAX_DEPTH = 8;
879
+ function buildSelector(target, doc = target.ownerDocument ?? document) {
880
+ if (target.id && isStableId(target.id)) {
881
+ const escaped = cssEscape(target.id);
882
+ const id = `#${escaped}`;
883
+ return { short: id, full: id };
884
+ }
885
+ const segments = [];
886
+ let cur = target;
887
+ let short = null;
888
+ for (let depth = 0; cur && depth < FULL_MAX_DEPTH; depth++) {
889
+ segments.unshift(segmentFor(cur));
890
+ const candidate = segments.join(" > ");
891
+ if (short === null && depth < SHORT_MAX_DEPTH && isUnique(doc, candidate)) {
892
+ short = candidate;
893
+ }
894
+ cur = cur.parentElement;
895
+ if (!cur || cur === doc.documentElement || cur.tagName.toLowerCase() === "html") break;
896
+ }
897
+ const full = segments.join(" > ");
898
+ return { short: short ?? full, full };
899
+ }
900
+ function segmentFor(el) {
901
+ const tag = el.tagName.toLowerCase();
902
+ if (el.id && isStableId(el.id)) return `${tag}#${cssEscape(el.id)}`;
903
+ const classes = Array.from(el.classList).filter(isUseableClass).slice(0, 3);
904
+ let segment = classes.length ? `${tag}.${classes.map(cssEscape).join(".")}` : tag;
905
+ const parent = el.parentElement;
906
+ if (parent) {
907
+ const sameTag = Array.from(parent.children).filter(
908
+ (c) => c.tagName === el.tagName
909
+ );
910
+ if (sameTag.length > 1) {
911
+ const idx = sameTag.indexOf(el) + 1;
912
+ if (idx > 0) segment += `:nth-of-type(${idx})`;
913
+ }
914
+ }
915
+ return segment;
916
+ }
917
+ function isUseableClass(c) {
918
+ if (!c) return false;
919
+ if (c.length > 30) return false;
920
+ if (/^css-[a-z0-9]+$/i.test(c)) return false;
921
+ if (/^jsx-\d+$/i.test(c)) return false;
922
+ if (/^_.+_[a-z0-9]{5,}$/i.test(c)) return false;
923
+ if (/^\d/.test(c)) return false;
924
+ return true;
925
+ }
926
+ function isStableId(id) {
927
+ if (/^:[a-z]\d+:/i.test(id)) return false;
928
+ if (/^radix-/i.test(id)) return false;
929
+ if (/^headlessui-/i.test(id)) return false;
930
+ return true;
931
+ }
932
+ function isUnique(doc, selector) {
933
+ try {
934
+ return doc.querySelectorAll(selector).length === 1;
935
+ } catch {
936
+ return false;
937
+ }
938
+ }
939
+ function cssEscape(s) {
940
+ const g = globalThis;
941
+ if (g.CSS?.escape) return g.CSS.escape(s);
942
+ return s.replace(/[^a-zA-Z0-9_-]/g, (c) => "\\" + c);
943
+ }
944
+ var NEARBY_MAX = 200;
945
+ function collectNearbyText(el) {
946
+ const own = readText(el);
947
+ if (own) return truncate(own, NEARBY_MAX);
948
+ const parent = el.parentElement;
949
+ if (parent) {
950
+ const t = readText(parent);
951
+ if (t) return truncate(t, NEARBY_MAX);
952
+ }
953
+ return "";
954
+ }
955
+ function collectSelectedText(doc = document) {
956
+ const win = doc.defaultView ?? globalThis;
957
+ if (typeof win.getSelection !== "function") return "";
958
+ const sel = win.getSelection();
959
+ if (!sel) return "";
960
+ return sel.toString().trim();
961
+ }
962
+ function readText(el) {
963
+ const t = el.innerText ?? el.textContent ?? "";
964
+ return t.replace(/\s+/g, " ").trim();
965
+ }
966
+ function truncate(s, n) {
967
+ return s.length <= n ? s : s.slice(0, n - 1) + "\u2026";
968
+ }
969
+ var POSITIONED_FIXED = /* @__PURE__ */ new Set(["fixed", "sticky"]);
970
+ function collectMetadata(el, options = {}) {
971
+ perfMark2("clickly:metadata:collect");
972
+ const detail = options.detail ?? "standard";
973
+ const includeReact = options.includeReact !== false && detail !== "compact";
974
+ const doc = options.document ?? el.ownerDocument ?? document;
975
+ const { short, full } = buildSelector(el, doc);
976
+ const rect = el.getBoundingClientRect();
977
+ const boundingBox = { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
978
+ const reactComponents = includeReact ? getComponentChain(el).join(" > ") : "";
979
+ const source = includeReact ? getSourceInfo(el) : null;
980
+ return {
981
+ element: el.tagName.toLowerCase(),
982
+ elementPath: short,
983
+ fullPath: full,
984
+ cssClasses: typeof el.className === "string" ? el.className.trim() : "",
985
+ computedStyles: collectComputedStyles(el, detail),
986
+ accessibility: collectAccessibility(el),
987
+ nearbyText: collectNearbyText(el),
988
+ selectedText: options.selectedText ?? collectSelectedText(doc),
989
+ boundingBox,
990
+ isFixed: hasFixedAncestor(el),
991
+ reactComponents,
992
+ sourceFile: source?.fileName ?? "",
993
+ sourceLine: source?.lineNumber ?? 0,
994
+ sourceColumn: source?.columnNumber ?? 0
995
+ };
996
+ }
997
+ function perfMark2(name) {
998
+ if (typeof performance !== "undefined" && typeof performance.mark === "function") {
999
+ try {
1000
+ performance.mark(name);
1001
+ } catch {
1002
+ }
1003
+ }
1004
+ }
1005
+ function hasFixedAncestor(el) {
1006
+ const doc = el.ownerDocument;
1007
+ const win = doc?.defaultView ?? globalThis;
1008
+ if (typeof win.getComputedStyle !== "function") return false;
1009
+ let cur = el;
1010
+ for (let i = 0; cur && i < 8; i++) {
1011
+ const pos = win.getComputedStyle(cur).getPropertyValue("position");
1012
+ if (POSITIONED_FIXED.has(pos)) return true;
1013
+ cur = cur.parentElement;
1014
+ }
1015
+ return false;
1016
+ }
1017
+
1018
+ // packages/react/src/internal/ClicklyRoot.tsx
1019
+ import { useEffect as useEffect6, useState as useState6 } from "react";
1020
+
1021
+ // packages/react/src/internal/Toolbar.tsx
1022
+ import { useRef as useRef4, useState as useState3 } from "react";
1023
+
1024
+ // packages/react/src/hooks/useDraggable.ts
1025
+ import { useCallback, useEffect, useRef, useState } from "react";
11
1026
  function useDraggable(defaultPos, size) {
12
1027
  const [position, setPosition] = useState(defaultPos);
13
1028
  const [isDragging, setDragging] = useState(false);
@@ -62,6 +1077,9 @@ function useDraggable(defaultPos, size) {
62
1077
  function clamp(n, lo, hi) {
63
1078
  return Math.max(lo, Math.min(hi, n));
64
1079
  }
1080
+
1081
+ // packages/react/src/state/useEngineState.ts
1082
+ import { useSyncExternalStore } from "react";
65
1083
  function useEngineState(engine) {
66
1084
  return useSyncExternalStore(
67
1085
  (cb) => engine ? engine.subscribe(cb) : () => {
@@ -71,6 +1089,119 @@ function useEngineState(engine) {
71
1089
  );
72
1090
  }
73
1091
  var IDLE = { kind: "idle" };
1092
+
1093
+ // node_modules/.pnpm/zustand@5.0.14_@types+react@18.3.31_react@18.3.1/node_modules/zustand/esm/vanilla.mjs
1094
+ var createStoreImpl = (createState) => {
1095
+ let state;
1096
+ const listeners = /* @__PURE__ */ new Set();
1097
+ const setState = (partial, replace) => {
1098
+ const nextState = typeof partial === "function" ? partial(state) : partial;
1099
+ if (!Object.is(nextState, state)) {
1100
+ const previousState = state;
1101
+ state = (replace != null ? replace : typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState);
1102
+ listeners.forEach((listener) => listener(state, previousState));
1103
+ }
1104
+ };
1105
+ const getState = () => state;
1106
+ const getInitialState = () => initialState2;
1107
+ const subscribe = (listener) => {
1108
+ listeners.add(listener);
1109
+ return () => listeners.delete(listener);
1110
+ };
1111
+ const api = { setState, getState, getInitialState, subscribe };
1112
+ const initialState2 = state = createState(setState, getState, api);
1113
+ return api;
1114
+ };
1115
+ var createStore = ((createState) => createState ? createStoreImpl(createState) : createStoreImpl);
1116
+
1117
+ // node_modules/.pnpm/zustand@5.0.14_@types+react@18.3.31_react@18.3.1/node_modules/zustand/esm/react.mjs
1118
+ import React from "react";
1119
+ var identity = (arg) => arg;
1120
+ function useStore(api, selector = identity) {
1121
+ const slice = React.useSyncExternalStore(
1122
+ api.subscribe,
1123
+ React.useCallback(() => selector(api.getState()), [api, selector]),
1124
+ React.useCallback(() => selector(api.getInitialState()), [api, selector])
1125
+ );
1126
+ React.useDebugValue(slice);
1127
+ return slice;
1128
+ }
1129
+ var createImpl = (createState) => {
1130
+ const api = createStore(createState);
1131
+ const useBoundStore = (selector) => useStore(api, selector);
1132
+ Object.assign(useBoundStore, api);
1133
+ return useBoundStore;
1134
+ };
1135
+ var create = ((createState) => createState ? createImpl(createState) : createImpl);
1136
+
1137
+ // node_modules/.pnpm/zustand@5.0.14_@types+react@18.3.31_react@18.3.1/node_modules/zustand/esm/react/shallow.mjs
1138
+ import React2 from "react";
1139
+
1140
+ // node_modules/.pnpm/zustand@5.0.14_@types+react@18.3.31_react@18.3.1/node_modules/zustand/esm/vanilla/shallow.mjs
1141
+ var isIterable = (obj) => Symbol.iterator in obj;
1142
+ var hasIterableEntries = (value) => (
1143
+ // HACK: avoid checking entries type
1144
+ "entries" in value
1145
+ );
1146
+ var compareEntries = (valueA, valueB) => {
1147
+ const mapA = valueA instanceof Map ? valueA : new Map(valueA.entries());
1148
+ const mapB = valueB instanceof Map ? valueB : new Map(valueB.entries());
1149
+ if (mapA.size !== mapB.size) {
1150
+ return false;
1151
+ }
1152
+ for (const [key, value] of mapA) {
1153
+ if (!mapB.has(key) || !Object.is(value, mapB.get(key))) {
1154
+ return false;
1155
+ }
1156
+ }
1157
+ return true;
1158
+ };
1159
+ var compareIterables = (valueA, valueB) => {
1160
+ const iteratorA = valueA[Symbol.iterator]();
1161
+ const iteratorB = valueB[Symbol.iterator]();
1162
+ let nextA = iteratorA.next();
1163
+ let nextB = iteratorB.next();
1164
+ while (!nextA.done && !nextB.done) {
1165
+ if (!Object.is(nextA.value, nextB.value)) {
1166
+ return false;
1167
+ }
1168
+ nextA = iteratorA.next();
1169
+ nextB = iteratorB.next();
1170
+ }
1171
+ return !!nextA.done && !!nextB.done;
1172
+ };
1173
+ function shallow(valueA, valueB) {
1174
+ if (Object.is(valueA, valueB)) {
1175
+ return true;
1176
+ }
1177
+ if (typeof valueA !== "object" || valueA === null || typeof valueB !== "object" || valueB === null) {
1178
+ return false;
1179
+ }
1180
+ if (Object.getPrototypeOf(valueA) !== Object.getPrototypeOf(valueB)) {
1181
+ return false;
1182
+ }
1183
+ if (isIterable(valueA) && isIterable(valueB)) {
1184
+ if (hasIterableEntries(valueA) && hasIterableEntries(valueB)) {
1185
+ return compareEntries(valueA, valueB);
1186
+ }
1187
+ return compareIterables(valueA, valueB);
1188
+ }
1189
+ return compareEntries(
1190
+ { entries: () => Object.entries(valueA) },
1191
+ { entries: () => Object.entries(valueB) }
1192
+ );
1193
+ }
1194
+
1195
+ // node_modules/.pnpm/zustand@5.0.14_@types+react@18.3.31_react@18.3.1/node_modules/zustand/esm/react/shallow.mjs
1196
+ function useShallow(selector) {
1197
+ const prev = React2.useRef(void 0);
1198
+ return (state) => {
1199
+ const next = selector(state);
1200
+ return shallow(prev.current, next) ? prev.current : prev.current = next;
1201
+ };
1202
+ }
1203
+
1204
+ // packages/react/src/state/annotations.ts
74
1205
  var useAnnotations = create((set, get) => ({
75
1206
  byId: {},
76
1207
  order: [],
@@ -100,6 +1231,8 @@ function useAnnotationsList() {
100
1231
  useShallow((s) => s.order.map((id) => s.byId[id]).filter(Boolean))
101
1232
  );
102
1233
  }
1234
+
1235
+ // packages/react/src/state/settings.ts
103
1236
  var DEFAULTS = {
104
1237
  outputDetail: "standard",
105
1238
  copyOnAdd: true,
@@ -138,7 +1271,7 @@ var useSettings = create((set) => ({
138
1271
  }
139
1272
  }));
140
1273
 
141
- // src/output/markdown.ts
1274
+ // packages/react/src/output/markdown.ts
142
1275
  function annotationsToMarkdown(annotations, detail = "standard") {
143
1276
  if (!annotations.length) return "(no annotations)";
144
1277
  return annotations.map((a, i) => formatOne(a, i + 1, detail)).join("\n\n");
@@ -146,26 +1279,39 @@ function annotationsToMarkdown(annotations, detail = "standard") {
146
1279
  function formatOne(a, index, detail) {
147
1280
  const lines = [];
148
1281
  lines.push(`## Annotation #${index}`);
149
- lines.push(`**Element:** \`${a.element}${a.cssClasses ? "." + a.cssClasses.split(" ").join(".") : ""}\``);
1282
+ lines.push(
1283
+ `**Element:** \`${a.element}${a.cssClasses ? "." + a.cssClasses.split(" ").join(".") : ""}\``
1284
+ );
150
1285
  lines.push(`**Path:** \`${a.elementPath}\``);
151
1286
  if (detail !== "compact" && a.boundingBox) {
152
1287
  const b = a.boundingBox;
153
- lines.push(`**Position:** ${Math.round(b.x)}px, ${Math.round(b.y)}px (${Math.round(b.width)}\xD7${Math.round(b.height)}px)`);
1288
+ lines.push(
1289
+ `**Position:** ${Math.round(b.x)}px, ${Math.round(b.y)}px (${Math.round(b.width)}\xD7${Math.round(b.height)}px)`
1290
+ );
1291
+ }
1292
+ if (detail !== "compact" && a.sourceFile) {
1293
+ lines.push(`**Source:** \`${a.sourceFile}:${a.sourceLine ?? "?"}\``);
154
1294
  }
155
1295
  if (detail === "detailed" || detail === "forensic") {
156
1296
  if (a.reactComponents) lines.push(`**React:** ${a.reactComponents}`);
157
- if (a.nearbyText) lines.push(`**Nearby text:** ${truncate(a.nearbyText, 120)}`);
1297
+ if (a.nearbyText) lines.push(`**Nearby text:** ${truncate2(a.nearbyText, 120)}`);
158
1298
  }
159
1299
  if (detail === "forensic" && a.computedStyles) {
160
- lines.push("**Computed styles:**\n```\n" + a.computedStyles + "\n```");
1300
+ lines.push("**Computed styles:**\n```css\n" + a.computedStyles + "\n```");
161
1301
  }
162
1302
  lines.push(`**Feedback:** ${a.comment}`);
1303
+ if (a.suggestedCss) {
1304
+ lines.push("**Suggested CSS:**\n```css\n" + a.suggestedCss + "\n```");
1305
+ }
163
1306
  if (a.severity) lines.push(`**Severity:** ${a.severity}`);
164
1307
  return lines.join("\n");
165
1308
  }
166
- function truncate(s, n) {
1309
+ function truncate2(s, n) {
167
1310
  return s.length <= n ? s : s.slice(0, n - 1) + "\u2026";
168
1311
  }
1312
+
1313
+ // packages/react/src/internal/icons.tsx
1314
+ import { jsx, jsxs } from "react/jsx-runtime";
169
1315
  function Icon({
170
1316
  children,
171
1317
  size = 16
@@ -192,7 +1338,6 @@ var IconLayers = () => /* @__PURE__ */ jsxs(Icon, { children: [
192
1338
  /* @__PURE__ */ jsx("path", { d: "M3 12l9 5 9-5" }),
193
1339
  /* @__PURE__ */ jsx("path", { d: "M3 17l9 5 9-5" })
194
1340
  ] });
195
- var IconSquare = () => /* @__PURE__ */ jsx(Icon, { children: /* @__PURE__ */ jsx("rect", { x: "4", y: "4", width: "16", height: "16", rx: "2", strokeDasharray: "3 3" }) });
196
1341
  var IconCopy = () => /* @__PURE__ */ jsxs(Icon, { children: [
197
1342
  /* @__PURE__ */ jsx("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2" }),
198
1343
  /* @__PURE__ */ jsx("path", { d: "M5 15V5a2 2 0 0 1 2-2h10" })
@@ -213,109 +1358,220 @@ var IconGrip = () => /* @__PURE__ */ jsxs(Icon, { children: [
213
1358
  var IconClose = () => /* @__PURE__ */ jsx(Icon, { children: /* @__PURE__ */ jsx("path", { d: "M18 6L6 18M6 6l12 12" }) });
214
1359
  var IconCheck = () => /* @__PURE__ */ jsx(Icon, { children: /* @__PURE__ */ jsx("path", { d: "M5 12l5 5L20 7" }) });
215
1360
  var IconList = () => /* @__PURE__ */ jsx(Icon, { children: /* @__PURE__ */ jsx("path", { d: "M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01" }) });
1361
+
1362
+ // packages/react/src/internal/SettingsPopover.tsx
1363
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
1364
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
216
1365
  function SettingsPopover({ anchor, width, onClose }) {
217
- const ref = useRef(null);
1366
+ const ref = useRef2(null);
218
1367
  const s = useSettings();
219
- useEffect(() => {
1368
+ useEffect2(() => {
220
1369
  const onDown = (e) => {
221
- if (ref.current && !ref.current.contains(e.target)) onClose();
1370
+ if (ref.current && e.composedPath().includes(ref.current)) return;
1371
+ onClose();
222
1372
  };
223
1373
  window.addEventListener("pointerdown", onDown, true);
224
1374
  return () => window.removeEventListener("pointerdown", onDown, true);
225
1375
  }, [onClose]);
226
- const top = Math.max(8, anchor.y - 200);
227
- const left = Math.min(window.innerWidth - 268, anchor.x + width - 260);
228
- return /* @__PURE__ */ jsxs("div", { ref, className: "clickly-popover", style: { left, top }, children: [
229
- /* @__PURE__ */ jsx("h4", { children: "Settings" }),
230
- /* @__PURE__ */ jsxs("div", { className: "field", children: [
231
- /* @__PURE__ */ jsx("label", { htmlFor: "clickly-detail", children: "Output detail" }),
232
- /* @__PURE__ */ jsxs(
1376
+ const PANEL_H = 330;
1377
+ const top = Math.max(8, anchor.y - PANEL_H - 12);
1378
+ const left = Math.min(window.innerWidth - 300, Math.max(8, anchor.x + width - 284));
1379
+ return /* @__PURE__ */ jsxs2("div", { ref, className: "clickly-settings", style: { left, top }, children: [
1380
+ /* @__PURE__ */ jsxs2("div", { className: "settings-header", children: [
1381
+ /* @__PURE__ */ jsx2("span", { className: "settings-title", children: "Settings" }),
1382
+ /* @__PURE__ */ jsx2("button", { className: "settings-close", onClick: onClose, "aria-label": "Close settings", children: /* @__PURE__ */ jsx2(IconClose, {}) })
1383
+ ] }),
1384
+ /* @__PURE__ */ jsxs2("div", { className: "settings-section", children: [
1385
+ /* @__PURE__ */ jsxs2("label", { className: "settings-label", htmlFor: "clickly-detail", children: [
1386
+ "Output detail",
1387
+ /* @__PURE__ */ jsx2("span", { className: "settings-hint", children: "How much metadata is captured" })
1388
+ ] }),
1389
+ /* @__PURE__ */ jsxs2(
233
1390
  "select",
234
1391
  {
235
1392
  id: "clickly-detail",
1393
+ className: "settings-select",
236
1394
  value: s.outputDetail,
237
1395
  onChange: (e) => s.set({ outputDetail: e.target.value }),
238
1396
  children: [
239
- /* @__PURE__ */ jsx("option", { value: "compact", children: "Compact" }),
240
- /* @__PURE__ */ jsx("option", { value: "standard", children: "Standard" }),
241
- /* @__PURE__ */ jsx("option", { value: "detailed", children: "Detailed" }),
242
- /* @__PURE__ */ jsx("option", { value: "forensic", children: "Forensic" })
1397
+ /* @__PURE__ */ jsx2("option", { value: "compact", children: "Compact" }),
1398
+ /* @__PURE__ */ jsx2("option", { value: "standard", children: "Standard" }),
1399
+ /* @__PURE__ */ jsx2("option", { value: "detailed", children: "Detailed" }),
1400
+ /* @__PURE__ */ jsx2("option", { value: "forensic", children: "Forensic" })
243
1401
  ]
244
1402
  }
245
1403
  )
246
1404
  ] }),
247
- /* @__PURE__ */ jsxs("div", { className: "field", children: [
248
- /* @__PURE__ */ jsx("label", { htmlFor: "clickly-copy", children: "Copy on add" }),
249
- /* @__PURE__ */ jsx(
250
- "input",
251
- {
252
- id: "clickly-copy",
253
- type: "checkbox",
254
- checked: s.copyOnAdd,
255
- onChange: (e) => s.set({ copyOnAdd: e.target.checked })
256
- }
257
- )
258
- ] }),
259
- /* @__PURE__ */ jsxs("div", { className: "field", children: [
260
- /* @__PURE__ */ jsx("label", { htmlFor: "clickly-react", children: "React components" }),
261
- /* @__PURE__ */ jsx(
262
- "input",
263
- {
264
- id: "clickly-react",
265
- type: "checkbox",
266
- checked: s.showReactComponents,
267
- onChange: (e) => s.set({ showReactComponents: e.target.checked })
268
- }
269
- )
1405
+ /* @__PURE__ */ jsx2("div", { className: "settings-divider" }),
1406
+ /* @__PURE__ */ jsxs2("div", { className: "settings-section", children: [
1407
+ /* @__PURE__ */ jsxs2("div", { className: "settings-row", children: [
1408
+ /* @__PURE__ */ jsxs2("div", { className: "settings-row-label", children: [
1409
+ "Copy on add",
1410
+ /* @__PURE__ */ jsx2("span", { className: "settings-hint", children: "Auto-copy markdown when annotating" })
1411
+ ] }),
1412
+ /* @__PURE__ */ jsxs2("label", { className: "clickly-toggle", children: [
1413
+ /* @__PURE__ */ jsx2(
1414
+ "input",
1415
+ {
1416
+ type: "checkbox",
1417
+ checked: s.copyOnAdd,
1418
+ onChange: (e) => s.set({ copyOnAdd: e.target.checked })
1419
+ }
1420
+ ),
1421
+ /* @__PURE__ */ jsx2("span", { className: "toggle-track" })
1422
+ ] })
1423
+ ] }),
1424
+ /* @__PURE__ */ jsxs2("div", { className: "settings-row", children: [
1425
+ /* @__PURE__ */ jsxs2("div", { className: "settings-row-label", children: [
1426
+ "React components",
1427
+ /* @__PURE__ */ jsx2("span", { className: "settings-hint", children: "Include component tree in output" })
1428
+ ] }),
1429
+ /* @__PURE__ */ jsxs2("label", { className: "clickly-toggle", children: [
1430
+ /* @__PURE__ */ jsx2(
1431
+ "input",
1432
+ {
1433
+ type: "checkbox",
1434
+ checked: s.showReactComponents,
1435
+ onChange: (e) => s.set({ showReactComponents: e.target.checked })
1436
+ }
1437
+ ),
1438
+ /* @__PURE__ */ jsx2("span", { className: "toggle-track" })
1439
+ ] })
1440
+ ] })
270
1441
  ] }),
271
- /* @__PURE__ */ jsxs("div", { className: "field", children: [
272
- /* @__PURE__ */ jsx("label", { htmlFor: "clickly-color", children: "Marker color" }),
273
- /* @__PURE__ */ jsx(
274
- "input",
275
- {
276
- id: "clickly-color",
277
- type: "color",
278
- value: s.markerColor,
279
- onChange: (e) => s.set({ markerColor: e.target.value })
280
- }
281
- )
282
- ] })
283
- ] });
1442
+ /* @__PURE__ */ jsx2("div", { className: "settings-divider" }),
1443
+ /* @__PURE__ */ jsx2("div", { className: "settings-section", children: /* @__PURE__ */ jsxs2("div", { className: "settings-row", children: [
1444
+ /* @__PURE__ */ jsxs2("div", { className: "settings-row-label", children: [
1445
+ "Marker color",
1446
+ /* @__PURE__ */ jsx2("span", { className: "settings-hint", children: "Color used for annotation pins" })
1447
+ ] }),
1448
+ /* @__PURE__ */ jsxs2("label", { className: "settings-color-wrap", "aria-label": "Marker color", children: [
1449
+ /* @__PURE__ */ jsx2("span", { className: "color-swatch", style: { background: s.markerColor } }),
1450
+ /* @__PURE__ */ jsx2(
1451
+ "input",
1452
+ {
1453
+ type: "color",
1454
+ value: s.markerColor,
1455
+ onChange: (e) => s.set({ markerColor: e.target.value })
1456
+ }
1457
+ )
1458
+ ] })
1459
+ ] }) })
1460
+ ] });
284
1461
  }
1462
+
1463
+ // packages/react/src/internal/AnnotationList.tsx
1464
+ import { useEffect as useEffect3, useRef as useRef3, useState as useState2 } from "react";
1465
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
285
1466
  function AnnotationList({ anchor, width, onClose }) {
286
1467
  const items = useAnnotationsList();
287
1468
  const remove = useAnnotations((s) => s.remove);
288
- const ref = useRef(null);
289
- useEffect(() => {
1469
+ const outputDetail = useSettings((s) => s.outputDetail);
1470
+ const ref = useRef3(null);
1471
+ useEffect3(() => {
290
1472
  const onDown = (e) => {
291
- if (ref.current && !ref.current.contains(e.target)) onClose();
1473
+ if (ref.current && e.composedPath().includes(ref.current)) return;
1474
+ onClose();
292
1475
  };
293
1476
  window.addEventListener("pointerdown", onDown, true);
294
1477
  return () => window.removeEventListener("pointerdown", onDown, true);
295
1478
  }, [onClose]);
296
- const top = Math.max(8, anchor.y - 8 - Math.min(window.innerHeight * 0.5, items.length * 60 + 40));
297
- const left = Math.min(window.innerWidth - 328, anchor.x);
298
- return /* @__PURE__ */ jsx("div", { ref, className: "clickly-list", style: { left, top }, children: items.length === 0 ? /* @__PURE__ */ jsx("div", { className: "empty", children: "No annotations yet." }) : items.map((a, i) => /* @__PURE__ */ jsxs("div", { className: "row", children: [
299
- /* @__PURE__ */ jsxs("span", { className: "num", children: [
300
- "#",
301
- i + 1
1479
+ const estimatedHeight = Math.min(window.innerHeight * 0.55, items.length * 88 + 48);
1480
+ const top = Math.max(8, anchor.y - estimatedHeight - 12);
1481
+ const left = Math.min(window.innerWidth - 336, Math.max(8, anchor.x));
1482
+ return /* @__PURE__ */ jsxs3("div", { ref, className: "clickly-list", style: { left, top }, children: [
1483
+ /* @__PURE__ */ jsxs3("div", { className: "list-header", children: [
1484
+ /* @__PURE__ */ jsx3("span", { className: "list-title", children: "Annotations" }),
1485
+ /* @__PURE__ */ jsx3("span", { className: "list-count", children: items.length })
302
1486
  ] }),
303
- /* @__PURE__ */ jsxs("div", { className: "body", children: [
304
- /* @__PURE__ */ jsx("div", { className: "selector", children: a.elementPath }),
305
- /* @__PURE__ */ jsx("p", { children: a.comment })
1487
+ items.length === 0 ? /* @__PURE__ */ jsx3("div", { className: "list-empty", children: "No annotations yet." }) : /* @__PURE__ */ jsx3("div", { className: "list-items", children: items.map((a, i) => /* @__PURE__ */ jsx3(
1488
+ AnnotationCard,
1489
+ {
1490
+ annotation: a,
1491
+ index: i + 1,
1492
+ outputDetail,
1493
+ onRemove: () => remove(a.id)
1494
+ },
1495
+ a.id
1496
+ )) })
1497
+ ] });
1498
+ }
1499
+ function AnnotationCard({
1500
+ annotation: a,
1501
+ index,
1502
+ outputDetail,
1503
+ onRemove
1504
+ }) {
1505
+ const [copied, setCopied] = useState2(false);
1506
+ const copyOne = async () => {
1507
+ const md = formatOne(a, index, outputDetail);
1508
+ try {
1509
+ await navigator.clipboard.writeText(md);
1510
+ setCopied(true);
1511
+ setTimeout(() => setCopied(false), 1500);
1512
+ } catch {
1513
+ }
1514
+ };
1515
+ return /* @__PURE__ */ jsxs3("div", { className: "list-card", children: [
1516
+ /* @__PURE__ */ jsxs3("div", { className: "list-card-header", children: [
1517
+ /* @__PURE__ */ jsxs3("span", { className: "list-card-num", children: [
1518
+ "#",
1519
+ index
1520
+ ] }),
1521
+ /* @__PURE__ */ jsx3("span", { className: "list-card-path", children: a.elementPath }),
1522
+ /* @__PURE__ */ jsxs3("div", { className: "list-card-actions", children: [
1523
+ /* @__PURE__ */ jsx3(
1524
+ "button",
1525
+ {
1526
+ className: `list-action-btn${copied ? " copied" : ""}`,
1527
+ onClick: copyOne,
1528
+ title: `Copy annotation #${index} (${outputDetail})`,
1529
+ children: copied ? /* @__PURE__ */ jsx3("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx3("polyline", { points: "20 6 9 17 4 12" }) }) : /* @__PURE__ */ jsx3(IconCopy, {})
1530
+ }
1531
+ ),
1532
+ /* @__PURE__ */ jsx3(
1533
+ "button",
1534
+ {
1535
+ className: "list-action-btn list-action-delete",
1536
+ onClick: onRemove,
1537
+ title: "Remove annotation",
1538
+ children: /* @__PURE__ */ jsx3(IconClose, {})
1539
+ }
1540
+ )
1541
+ ] })
306
1542
  ] }),
307
- /* @__PURE__ */ jsx("button", { className: "remove", onClick: () => remove(a.id), title: "Remove", children: /* @__PURE__ */ jsx(IconClose, {}) })
308
- ] }, a.id)) });
1543
+ /* @__PURE__ */ jsx3("p", { className: "list-card-comment", children: a.comment }),
1544
+ a.suggestedCss && /* @__PURE__ */ jsxs3("div", { className: "list-card-css", children: [
1545
+ /* @__PURE__ */ jsx3("span", { className: "list-card-css-label", children: "CSS changes" }),
1546
+ /* @__PURE__ */ jsx3("pre", { className: "list-card-css-code", children: a.suggestedCss })
1547
+ ] })
1548
+ ] });
1549
+ }
1550
+
1551
+ // packages/react/src/internal/Toolbar.tsx
1552
+ import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1553
+ var TOOLBAR_SIZE = { width: 320, height: 44 };
1554
+ function Tip({
1555
+ label,
1556
+ shortcut,
1557
+ children
1558
+ }) {
1559
+ return /* @__PURE__ */ jsxs4("span", { className: "clickly-tip", children: [
1560
+ children,
1561
+ /* @__PURE__ */ jsxs4("span", { className: "tip-bubble", "aria-hidden": "true", children: [
1562
+ label,
1563
+ shortcut && /* @__PURE__ */ jsx4("kbd", { children: shortcut })
1564
+ ] })
1565
+ ] });
309
1566
  }
310
- var TOOLBAR_SIZE = { width: 360, height: 40 };
311
1567
  function Toolbar({ engine, onCollapse }) {
312
1568
  const state = useEngineState(engine);
313
1569
  const annotations = useAnnotationsList();
314
1570
  const clearAnnotations = useAnnotations((s) => s.clear);
315
1571
  const outputDetail = useSettings((s) => s.outputDetail);
316
- const [showSettings, setShowSettings] = useState(false);
317
- const [showList, setShowList] = useState(false);
318
- const anchorRef = useRef(null);
1572
+ const [showSettings, setShowSettings] = useState3(false);
1573
+ const [showList, setShowList] = useState3(false);
1574
+ const anchorRef = useRef4(null);
319
1575
  const { position, handleProps } = useDraggable(
320
1576
  {
321
1577
  x: Math.max(8, window.innerWidth - TOOLBAR_SIZE.width - 16),
@@ -337,11 +1593,7 @@ function Toolbar({ engine, onCollapse }) {
337
1593
  } catch {
338
1594
  }
339
1595
  };
340
- const onClear = () => {
341
- if (annotations.length === 0) return;
342
- if (confirm(`Clear ${annotations.length} annotation(s)?`)) clearAnnotations();
343
- };
344
- return /* @__PURE__ */ jsxs(
1596
+ return /* @__PURE__ */ jsxs4(
345
1597
  "div",
346
1598
  {
347
1599
  ref: anchorRef,
@@ -349,113 +1601,112 @@ function Toolbar({ engine, onCollapse }) {
349
1601
  style: { left: position.x, top: position.y, width: TOOLBAR_SIZE.width },
350
1602
  "aria-label": "Clickly toolbar",
351
1603
  children: [
352
- /* @__PURE__ */ jsx("div", { className: "grip", ...handleProps, title: "Drag to move", children: /* @__PURE__ */ jsx(IconGrip, {}) }),
353
- /* @__PURE__ */ jsx(
1604
+ /* @__PURE__ */ jsx4(Tip, { label: "Move", children: /* @__PURE__ */ jsx4(
1605
+ "div",
1606
+ {
1607
+ className: "grip",
1608
+ ...handleProps,
1609
+ role: "separator",
1610
+ "aria-orientation": "vertical",
1611
+ children: /* @__PURE__ */ jsx4(IconGrip, {})
1612
+ }
1613
+ ) }),
1614
+ /* @__PURE__ */ jsx4(Tip, { label: "Select", shortcut: "1", children: /* @__PURE__ */ jsx4(
354
1615
  "button",
355
1616
  {
356
1617
  className: `clickly-btn icon-only${currentMode === "single" ? " is-active" : ""}`,
357
1618
  onClick: () => setMode("single"),
358
- title: "Single (1)",
359
- children: /* @__PURE__ */ jsx(IconCursor, {})
1619
+ "aria-label": "Single-element selection mode",
1620
+ "aria-pressed": currentMode === "single",
1621
+ children: /* @__PURE__ */ jsx4(IconCursor, {})
360
1622
  }
361
- ),
362
- /* @__PURE__ */ jsx(
1623
+ ) }),
1624
+ /* @__PURE__ */ jsx4(Tip, { label: "Multi-select", shortcut: "2", children: /* @__PURE__ */ jsx4(
363
1625
  "button",
364
1626
  {
365
1627
  className: `clickly-btn icon-only${currentMode === "multi" ? " is-active" : ""}`,
366
1628
  onClick: () => setMode("multi"),
367
- title: "Multi-select (2 / shift-click)",
368
- children: /* @__PURE__ */ jsx(IconLayers, {})
369
- }
370
- ),
371
- /* @__PURE__ */ jsx(
372
- "button",
373
- {
374
- className: `clickly-btn icon-only${currentMode === "area" ? " is-active" : ""}`,
375
- onClick: () => setMode("area"),
376
- title: "Area drag (3)",
377
- children: /* @__PURE__ */ jsx(IconSquare, {})
1629
+ "aria-label": "Multi-element selection mode",
1630
+ "aria-pressed": currentMode === "multi",
1631
+ children: /* @__PURE__ */ jsx4(IconLayers, {})
378
1632
  }
379
- ),
380
- /* @__PURE__ */ jsx("div", { className: "divider" }),
381
- pinnedCount > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
382
- /* @__PURE__ */ jsxs(
1633
+ ) }),
1634
+ /* @__PURE__ */ jsx4("div", { className: "divider" }),
1635
+ pinnedCount > 0 && /* @__PURE__ */ jsxs4(Fragment, { children: [
1636
+ /* @__PURE__ */ jsx4(Tip, { label: `Annotate ${pinnedCount} element(s)`, shortcut: "\u21B5", children: /* @__PURE__ */ jsxs4(
383
1637
  "button",
384
1638
  {
385
1639
  className: "clickly-btn primary-pinned",
386
1640
  onClick: () => engine.annotatePinned(),
387
- title: `Annotate the ${pinnedCount} pinned element(s) (Enter)`,
388
1641
  children: [
389
- /* @__PURE__ */ jsx(IconCheck, {}),
390
- "Annotate (",
391
- pinnedCount,
392
- ")"
1642
+ /* @__PURE__ */ jsx4(IconCheck, {}),
1643
+ /* @__PURE__ */ jsx4("span", { children: pinnedCount })
393
1644
  ]
394
1645
  }
395
- ),
396
- /* @__PURE__ */ jsx(
1646
+ ) }),
1647
+ /* @__PURE__ */ jsx4(Tip, { label: "Clear selection", shortcut: "Esc", children: /* @__PURE__ */ jsx4(
397
1648
  "button",
398
1649
  {
399
1650
  className: "clickly-btn icon-only",
400
1651
  onClick: () => engine.clearPinned(),
401
- title: "Clear pinned (Esc)",
402
- children: /* @__PURE__ */ jsx(IconClose, {})
1652
+ children: /* @__PURE__ */ jsx4(IconClose, {})
403
1653
  }
404
- ),
405
- /* @__PURE__ */ jsx("div", { className: "divider" })
1654
+ ) }),
1655
+ /* @__PURE__ */ jsx4("div", { className: "divider" })
406
1656
  ] }),
407
- /* @__PURE__ */ jsxs(
1657
+ /* @__PURE__ */ jsx4(Tip, { label: "Annotations", shortcut: "L", children: /* @__PURE__ */ jsxs4(
408
1658
  "button",
409
1659
  {
410
- className: "clickly-btn",
1660
+ className: "clickly-btn icon-only",
411
1661
  onClick: () => setShowList((v) => !v),
412
- title: "Annotations (L)",
1662
+ "aria-label": "Show annotation list",
413
1663
  children: [
414
- /* @__PURE__ */ jsx(IconList, {}),
415
- annotations.length > 0 && /* @__PURE__ */ jsx("span", { className: "clickly-counter", children: annotations.length })
1664
+ /* @__PURE__ */ jsx4(IconList, {}),
1665
+ annotations.length > 0 && /* @__PURE__ */ jsx4("span", { className: "clickly-counter", children: annotations.length })
416
1666
  ]
417
1667
  }
418
- ),
419
- /* @__PURE__ */ jsx(
1668
+ ) }),
1669
+ /* @__PURE__ */ jsx4(Tip, { label: "Copy feedback", shortcut: "C", children: /* @__PURE__ */ jsx4(
420
1670
  "button",
421
1671
  {
422
1672
  className: "clickly-btn icon-only",
423
1673
  onClick: onCopy,
424
- title: "Copy markdown (C)",
1674
+ "aria-label": "Copy all annotations as markdown",
425
1675
  disabled: annotations.length === 0,
426
- children: /* @__PURE__ */ jsx(IconCopy, {})
1676
+ children: /* @__PURE__ */ jsx4(IconCopy, {})
427
1677
  }
428
- ),
429
- /* @__PURE__ */ jsx(
1678
+ ) }),
1679
+ /* @__PURE__ */ jsx4(Tip, { label: "Clear all", shortcut: "X", children: /* @__PURE__ */ jsx4(
430
1680
  "button",
431
1681
  {
432
1682
  className: "clickly-btn icon-only",
433
- onClick: onClear,
434
- title: "Clear (X)",
1683
+ onClick: () => clearAnnotations(),
1684
+ "aria-label": "Clear all annotations",
435
1685
  disabled: annotations.length === 0,
436
- children: /* @__PURE__ */ jsx(IconTrash, {})
1686
+ children: /* @__PURE__ */ jsx4(IconTrash, {})
437
1687
  }
438
- ),
439
- /* @__PURE__ */ jsx("div", { className: "divider" }),
440
- /* @__PURE__ */ jsx(
1688
+ ) }),
1689
+ /* @__PURE__ */ jsx4("div", { className: "divider" }),
1690
+ /* @__PURE__ */ jsx4(Tip, { label: "Settings", children: /* @__PURE__ */ jsx4(
441
1691
  "button",
442
1692
  {
443
- className: "clickly-btn icon-only",
1693
+ className: `clickly-btn icon-only${showSettings ? " is-active" : ""}`,
444
1694
  onClick: () => setShowSettings((v) => !v),
445
- title: "Settings",
446
- children: /* @__PURE__ */ jsx(IconSettings, {})
1695
+ "aria-label": "Open settings",
1696
+ "aria-expanded": showSettings,
1697
+ children: /* @__PURE__ */ jsx4(IconSettings, {})
447
1698
  }
448
- ),
449
- /* @__PURE__ */ jsx(
1699
+ ) }),
1700
+ /* @__PURE__ */ jsx4(Tip, { label: "Close", shortcut: "Esc", children: /* @__PURE__ */ jsx4(
450
1701
  "button",
451
1702
  {
452
1703
  className: "clickly-btn icon-only",
453
1704
  onClick: onCollapse,
454
- title: "Collapse (Esc)",
455
- children: /* @__PURE__ */ jsx(IconClose, {})
1705
+ "aria-label": "Collapse Clickly toolbar",
1706
+ children: /* @__PURE__ */ jsx4(IconClose, {})
456
1707
  }
457
- ),
458
- showSettings && /* @__PURE__ */ jsx(
1708
+ ) }),
1709
+ showSettings && /* @__PURE__ */ jsx4(
459
1710
  SettingsPopover,
460
1711
  {
461
1712
  anchor: { x: position.x, y: position.y },
@@ -463,7 +1714,7 @@ function Toolbar({ engine, onCollapse }) {
463
1714
  onClose: () => setShowSettings(false)
464
1715
  }
465
1716
  ),
466
- showList && /* @__PURE__ */ jsx(
1717
+ showList && /* @__PURE__ */ jsx4(
467
1718
  AnnotationList,
468
1719
  {
469
1720
  anchor: { x: position.x, y: position.y },
@@ -475,9 +1726,12 @@ function Toolbar({ engine, onCollapse }) {
475
1726
  }
476
1727
  );
477
1728
  }
1729
+
1730
+ // packages/react/src/internal/CollapsedFAB.tsx
1731
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
478
1732
  function CollapsedFAB({ onExpand }) {
479
1733
  const annotations = useAnnotationsList();
480
- return /* @__PURE__ */ jsxs(
1734
+ return /* @__PURE__ */ jsxs5(
481
1735
  "button",
482
1736
  {
483
1737
  className: "clickly-fab",
@@ -485,27 +1739,75 @@ function CollapsedFAB({ onExpand }) {
485
1739
  title: "Open Clickly (\u2318/Ctrl+Shift+F)",
486
1740
  "aria-label": "Open Clickly toolbar",
487
1741
  children: [
488
- /* @__PURE__ */ jsx(IconCursor, {}),
489
- annotations.length > 0 && /* @__PURE__ */ jsx("span", { className: "clickly-fab-badge", children: annotations.length })
1742
+ /* @__PURE__ */ jsx5(IconCursor, {}),
1743
+ annotations.length > 0 && /* @__PURE__ */ jsx5("span", { className: "clickly-fab-badge", children: annotations.length })
490
1744
  ]
491
1745
  }
492
1746
  );
493
1747
  }
1748
+
1749
+ // packages/react/src/internal/AnnotationPopup.tsx
1750
+ import { useCallback as useCallback2, useEffect as useEffect4, useRef as useRef5, useState as useState4 } from "react";
1751
+ import { nanoid } from "nanoid";
1752
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
494
1753
  var POPUP_W = 320;
495
- var POPUP_H_EST = 200;
1754
+ var POPUP_H_EST = 240;
496
1755
  function AnnotationPopup({ engine }) {
497
1756
  const state = useEngineState(engine);
498
1757
  const addAnnotation = useAnnotations((s) => s.add);
499
1758
  const copyOnAdd = useSettings((s) => s.copyOnAdd);
500
1759
  const outputDetail = useSettings((s) => s.outputDetail);
501
- const [comment, setComment] = useState("");
502
- const taRef = useRef(null);
503
- useEffect(() => {
1760
+ const [comment, setComment] = useState4("");
1761
+ const [showStyles, setShowStyles] = useState4(false);
1762
+ const [editMode, setEditMode] = useState4(false);
1763
+ const [styles, setStyles] = useState4({});
1764
+ const [editedStyles, setEditedStyles] = useState4({});
1765
+ const targetElRef = useRef5(null);
1766
+ const taRef = useRef5(null);
1767
+ const popupRef = useRef5(null);
1768
+ useEffect4(() => {
504
1769
  if (state.kind === "annotating") {
505
1770
  setComment("");
1771
+ setShowStyles(false);
1772
+ setEditMode(false);
1773
+ setEditedStyles({});
1774
+ const sel = engine.resolveSelection();
1775
+ const el = sel?.kind === "single" ? sel.element : sel?.kind === "multi" || sel?.kind === "area" ? sel.elements[0] : null;
1776
+ targetElRef.current = el instanceof HTMLElement ? el : null;
1777
+ if (el) {
1778
+ setStyles(collectComputedStyles(el, "standard"));
1779
+ } else {
1780
+ setStyles({});
1781
+ }
506
1782
  requestAnimationFrame(() => taRef.current?.focus());
507
1783
  }
508
- }, [state.kind]);
1784
+ }, [state.kind, engine]);
1785
+ useEffect4(() => {
1786
+ if (state.kind !== "annotating") return;
1787
+ const onDown = (e) => {
1788
+ if (popupRef.current && e.composedPath().includes(popupRef.current)) return;
1789
+ engine.commit();
1790
+ };
1791
+ window.addEventListener("pointerdown", onDown, true);
1792
+ return () => window.removeEventListener("pointerdown", onDown, true);
1793
+ }, [state.kind, engine]);
1794
+ const onStyleChange = (prop, value) => {
1795
+ setEditedStyles((prev) => ({ ...prev, [prop]: value }));
1796
+ targetElRef.current?.style.setProperty(prop, value);
1797
+ };
1798
+ const revertProp = (prop) => {
1799
+ targetElRef.current?.style.removeProperty(prop);
1800
+ setEditedStyles((prev) => {
1801
+ const next = { ...prev };
1802
+ delete next[prop];
1803
+ return next;
1804
+ });
1805
+ };
1806
+ const cancel = () => {
1807
+ revertAll(targetElRef.current, editedStyles);
1808
+ setEditedStyles({});
1809
+ engine.commit();
1810
+ };
509
1811
  const onKey = (e) => {
510
1812
  if (e.key === "Escape") {
511
1813
  e.preventDefault();
@@ -516,17 +1818,20 @@ function AnnotationPopup({ engine }) {
516
1818
  submit();
517
1819
  }
518
1820
  };
519
- const cancel = () => engine.commit();
520
1821
  const submit = () => {
521
1822
  if (state.kind !== "annotating") return;
522
1823
  const sel = engine.resolveSelection();
523
1824
  if (!sel) return;
524
1825
  const elements = sel.kind === "single" ? [sel.element] : sel.elements;
525
1826
  if (elements.length === 0) return;
1827
+ const cssDiff = buildCssDiff(styles, editedStyles);
526
1828
  const sharedComment = comment.trim() || "(no comment)";
1829
+ revertAll(targetElRef.current, editedStyles);
1830
+ setEditedStyles({});
527
1831
  const isMulti = sel.kind !== "single";
1832
+ const showReact = useSettings.getState().showReactComponents;
528
1833
  for (const el of elements) {
529
- const md = collectMetadata(el, { detail: outputDetail });
1834
+ const md = collectMetadata(el, { detail: outputDetail, includeReact: showReact });
530
1835
  const annotation = {
531
1836
  id: "ann_" + nanoid(8),
532
1837
  comment: sharedComment,
@@ -544,9 +1849,14 @@ function AnnotationPopup({ engine }) {
544
1849
  nearbyText: md.nearbyText || void 0,
545
1850
  selectedText: md.selectedText || void 0,
546
1851
  isFixed: md.isFixed || void 0,
1852
+ reactComponents: md.reactComponents || void 0,
1853
+ sourceFile: md.sourceFile || void 0,
1854
+ sourceLine: md.sourceLine || void 0,
1855
+ sourceColumn: md.sourceColumn || void 0,
547
1856
  isMultiSelect: isMulti,
548
1857
  kind: "feedback",
549
- status: "pending"
1858
+ status: "pending",
1859
+ suggestedCss: cssDiff || void 0
550
1860
  };
551
1861
  addAnnotation(annotation);
552
1862
  }
@@ -556,70 +1866,233 @@ function AnnotationPopup({ engine }) {
556
1866
  }
557
1867
  engine.commit();
558
1868
  };
559
- const placement = useMemo(() => {
560
- if (state.kind !== "annotating") return null;
1869
+ const [placement, setPlacement] = useState4(null);
1870
+ const calcPlacement = useCallback2(() => {
1871
+ if (state.kind !== "annotating") {
1872
+ setPlacement(null);
1873
+ return;
1874
+ }
561
1875
  const sel = engine.resolveSelection();
562
- if (!sel) return null;
1876
+ if (!sel) {
1877
+ setPlacement(null);
1878
+ return;
1879
+ }
563
1880
  const rect = anchorRect(sel);
564
- if (!rect) return null;
565
- let top = rect.bottom + 8;
566
- let left = rect.left;
567
- if (top + POPUP_H_EST > window.innerHeight) {
568
- top = Math.max(8, rect.top - POPUP_H_EST - 8);
1881
+ if (!rect) {
1882
+ setPlacement(null);
1883
+ return;
569
1884
  }
570
- if (left + POPUP_W > window.innerWidth) {
571
- left = Math.max(8, window.innerWidth - POPUP_W - 8);
1885
+ const vw = window.innerWidth;
1886
+ const vh = window.innerHeight;
1887
+ const label = describeSelection(sel);
1888
+ const elementArea = rect.width * rect.height;
1889
+ if (elementArea > vw * vh * 0.7) {
1890
+ setPlacement({
1891
+ top: Math.round((vh - POPUP_H_EST) / 2),
1892
+ left: Math.round((vw - POPUP_W) / 2),
1893
+ label
1894
+ });
1895
+ return;
572
1896
  }
573
- return { top, left, label: describeSelection(sel) };
1897
+ let top = rect.bottom + 8;
1898
+ let left = rect.left;
1899
+ if (top + POPUP_H_EST > vh) top = Math.max(8, rect.top - POPUP_H_EST - 8);
1900
+ if (left + POPUP_W > vw) left = Math.max(8, vw - POPUP_W - 8);
1901
+ setPlacement({ top, left, label });
574
1902
  }, [state, engine]);
1903
+ useEffect4(() => {
1904
+ calcPlacement();
1905
+ }, [calcPlacement]);
1906
+ useEffect4(() => {
1907
+ if (state.kind !== "annotating") return;
1908
+ const onScroll = () => calcPlacement();
1909
+ window.addEventListener("scroll", onScroll, { capture: true, passive: true });
1910
+ return () => window.removeEventListener("scroll", onScroll, { capture: true });
1911
+ }, [state.kind, calcPlacement]);
575
1912
  if (state.kind !== "annotating" || !placement) return null;
576
- return /* @__PURE__ */ jsxs("div", { className: "clickly-popup", style: { top: placement.top, left: placement.left }, children: [
577
- /* @__PURE__ */ jsxs("div", { className: "target-info", children: [
578
- /* @__PURE__ */ jsx("span", { children: placement.label }),
579
- /* @__PURE__ */ jsx("span", { children: state.selection.kind })
1913
+ const styleEntries = Object.entries(styles);
1914
+ const hasEdits = Object.keys(editedStyles).length > 0;
1915
+ return /* @__PURE__ */ jsxs6("div", { ref: popupRef, className: "clickly-popup", style: { top: placement.top, left: placement.left }, children: [
1916
+ /* @__PURE__ */ jsxs6(
1917
+ "div",
1918
+ {
1919
+ className: "popup-header",
1920
+ role: "button",
1921
+ "aria-expanded": showStyles,
1922
+ onClick: () => setShowStyles((v) => !v),
1923
+ children: [
1924
+ /* @__PURE__ */ jsx6("span", { className: "popup-chevron", children: showStyles ? "\u25BE" : "\u203A" }),
1925
+ /* @__PURE__ */ jsx6("span", { className: "popup-label", children: placement.label }),
1926
+ showStyles && /* @__PURE__ */ jsx6(
1927
+ "button",
1928
+ {
1929
+ className: `popup-edit-toggle${editMode ? " is-editing" : ""}`,
1930
+ onClick: (e) => {
1931
+ e.stopPropagation();
1932
+ setEditMode((v) => !v);
1933
+ },
1934
+ title: editMode ? "Done editing" : "Edit CSS live",
1935
+ "aria-label": editMode ? "Exit CSS edit mode" : "Edit CSS values live",
1936
+ children: editMode ? (
1937
+ // checkmark when in edit mode
1938
+ /* @__PURE__ */ jsx6("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx6("polyline", { points: "20 6 9 17 4 12" }) })
1939
+ ) : (
1940
+ // pencil icon
1941
+ /* @__PURE__ */ jsxs6("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1942
+ /* @__PURE__ */ jsx6("path", { d: "M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" }),
1943
+ /* @__PURE__ */ jsx6("path", { d: "M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" })
1944
+ ] })
1945
+ )
1946
+ }
1947
+ )
1948
+ ]
1949
+ }
1950
+ ),
1951
+ showStyles && styleEntries.length > 0 && /* @__PURE__ */ jsxs6("div", { className: "popup-styles", children: [
1952
+ styleEntries.map(([prop, origVal]) => {
1953
+ const currentVal = editedStyles[prop] ?? origVal;
1954
+ const changed = editedStyles[prop] !== void 0 && editedStyles[prop] !== origVal;
1955
+ return /* @__PURE__ */ jsxs6("div", { className: `style-row${changed ? " style-row--changed" : ""}`, children: [
1956
+ /* @__PURE__ */ jsx6("span", { className: "style-key", children: prop }),
1957
+ editMode ? /* @__PURE__ */ jsx6(
1958
+ "input",
1959
+ {
1960
+ className: "style-val-input",
1961
+ value: currentVal,
1962
+ onChange: (e) => onStyleChange(prop, e.target.value),
1963
+ spellCheck: false,
1964
+ "aria-label": prop
1965
+ }
1966
+ ) : /* @__PURE__ */ jsx6("span", { className: "style-val", children: currentVal }),
1967
+ changed && /* @__PURE__ */ jsx6(
1968
+ "button",
1969
+ {
1970
+ className: "style-revert-btn",
1971
+ onClick: () => revertProp(prop),
1972
+ title: "Revert to original",
1973
+ children: "\u21A9"
1974
+ }
1975
+ )
1976
+ ] }, prop);
1977
+ }),
1978
+ hasEdits && /* @__PURE__ */ jsx6("div", { className: "style-hint", children: "Changes are live on page. Cancel reverts them." })
580
1979
  ] }),
581
- /* @__PURE__ */ jsx(
1980
+ /* @__PURE__ */ jsx6(
582
1981
  "textarea",
583
1982
  {
584
1983
  ref: taRef,
585
1984
  value: comment,
586
1985
  placeholder: "Describe the issue or change\u2026 (\u2318/Ctrl + Enter to submit)",
1986
+ "aria-label": "Annotation comment",
587
1987
  onChange: (e) => setComment(e.target.value),
588
1988
  onKeyDown: onKey
589
1989
  }
590
1990
  ),
591
- /* @__PURE__ */ jsxs("div", { className: "row", children: [
592
- /* @__PURE__ */ jsx("button", { className: "ghost", onClick: cancel, children: "Cancel" }),
593
- /* @__PURE__ */ jsx("button", { className: "primary", onClick: submit, children: "Add" })
1991
+ /* @__PURE__ */ jsxs6("div", { className: "row", children: [
1992
+ /* @__PURE__ */ jsx6("button", { className: "ghost", onClick: cancel, children: "Cancel" }),
1993
+ /* @__PURE__ */ jsx6("button", { className: "primary", onClick: submit, children: "Add" })
594
1994
  ] })
595
1995
  ] });
596
1996
  }
597
- function stringifyStyles(styles) {
598
- const entries = Object.entries(styles);
1997
+ function revertAll(el, edits) {
1998
+ if (!el) return;
1999
+ for (const prop of Object.keys(edits)) {
2000
+ el.style.removeProperty(prop);
2001
+ }
2002
+ }
2003
+ function buildCssDiff(original, edited) {
2004
+ const lines = [];
2005
+ for (const [prop, val] of Object.entries(edited)) {
2006
+ if (val !== original[prop]) {
2007
+ lines.push(` ${prop}: ${val}; /* was: ${original[prop] ?? "unset"} */`);
2008
+ }
2009
+ }
2010
+ if (lines.length === 0) return "";
2011
+ return `{
2012
+ ${lines.join("\n")}
2013
+ }`;
2014
+ }
2015
+ function stringifyStyles(s) {
2016
+ const entries = Object.entries(s);
599
2017
  if (entries.length === 0) return void 0;
600
2018
  return entries.map(([k, v]) => `${k}: ${v}`).join("; ");
601
2019
  }
602
2020
  function anchorRect(sel) {
603
2021
  if (!sel) return null;
604
2022
  if (sel.kind === "single") return sel.element.getBoundingClientRect();
605
- if (sel.kind === "area") {
606
- return new DOMRect(sel.rect.x, sel.rect.y, sel.rect.width, sel.rect.height);
607
- }
2023
+ if (sel.kind === "area") return new DOMRect(sel.rect.x, sel.rect.y, sel.rect.width, sel.rect.height);
608
2024
  if (sel.elements.length === 0) return null;
609
2025
  return sel.elements[0].getBoundingClientRect();
610
2026
  }
611
- function describeSelection(sel) {
612
- if (sel.kind === "single") {
613
- const m = collectMetadata(sel.element, { detail: "compact" });
614
- return m.elementPath;
2027
+ var TAG_LABELS2 = {
2028
+ p: "paragraph",
2029
+ h1: "heading",
2030
+ h2: "heading",
2031
+ h3: "heading",
2032
+ h4: "heading",
2033
+ h5: "heading",
2034
+ h6: "heading",
2035
+ a: "link",
2036
+ button: "button",
2037
+ input: "input",
2038
+ textarea: "textarea",
2039
+ select: "select",
2040
+ img: "image",
2041
+ video: "video",
2042
+ audio: "audio",
2043
+ form: "form",
2044
+ nav: "nav",
2045
+ header: "header",
2046
+ footer: "footer",
2047
+ main: "main",
2048
+ section: "section",
2049
+ article: "article",
2050
+ aside: "aside",
2051
+ ul: "list",
2052
+ ol: "list",
2053
+ li: "list item",
2054
+ table: "table",
2055
+ td: "cell",
2056
+ th: "header cell",
2057
+ span: "span",
2058
+ div: "div",
2059
+ label: "label",
2060
+ code: "code",
2061
+ pre: "code block",
2062
+ blockquote: "quote",
2063
+ strong: "bold",
2064
+ em: "italic",
2065
+ kbd: "key",
2066
+ svg: "svg",
2067
+ canvas: "canvas"
2068
+ };
2069
+ function describeElement2(el) {
2070
+ const tag = el.tagName.toLowerCase();
2071
+ const type = TAG_LABELS2[tag] ?? tag;
2072
+ const text = (el.textContent ?? "").replace(/\s+/g, " ").trim();
2073
+ if (text.length > 0) {
2074
+ const preview = text.length > 48 ? text.slice(0, 48) + "\u2026" : text;
2075
+ return `${type}: "${preview}"`;
615
2076
  }
2077
+ if (el.id) return `${type}: #${el.id}`;
2078
+ const cls = el.classList[0];
2079
+ if (cls) return `${type}: .${cls}`;
2080
+ return type;
2081
+ }
2082
+ function describeSelection(sel) {
2083
+ if (sel.kind === "single") return describeElement2(sel.element);
616
2084
  if (sel.kind === "area") return `area \xB7 ${sel.elements.length} element(s)`;
2085
+ if (sel.elements.length === 1) return describeElement2(sel.elements[0]);
617
2086
  return `${sel.elements.length} element(s)`;
618
2087
  }
2088
+
2089
+ // packages/react/src/internal/AnnotationPins.tsx
2090
+ import { useEffect as useEffect5, useRef as useRef6, useState as useState5 } from "react";
2091
+ import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
619
2092
  function AnnotationPins() {
620
2093
  const annotations = useAnnotationsList();
621
- const [, setVersion] = useState(0);
622
- useEffect(() => {
2094
+ const [, setVersion] = useState5(0);
2095
+ useEffect5(() => {
623
2096
  let raf = null;
624
2097
  const update = () => {
625
2098
  if (raf !== null) return;
@@ -636,40 +2109,151 @@ function AnnotationPins() {
636
2109
  window.removeEventListener("resize", update);
637
2110
  };
638
2111
  }, []);
639
- return /* @__PURE__ */ jsx(Fragment, { children: annotations.map((a, i) => /* @__PURE__ */ jsx(Pin, { number: i + 1, annotation: a }, a.id)) });
2112
+ return /* @__PURE__ */ jsx7(Fragment2, { children: annotations.map((a, i) => /* @__PURE__ */ jsx7(Pin, { number: i + 1, annotation: a }, a.id)) });
2113
+ }
2114
+ var TAG_LABELS3 = {
2115
+ p: "paragraph",
2116
+ h1: "heading",
2117
+ h2: "heading",
2118
+ h3: "heading",
2119
+ h4: "heading",
2120
+ h5: "heading",
2121
+ h6: "heading",
2122
+ a: "link",
2123
+ button: "button",
2124
+ input: "input",
2125
+ textarea: "textarea",
2126
+ select: "select",
2127
+ img: "image",
2128
+ video: "video",
2129
+ audio: "audio",
2130
+ form: "form",
2131
+ nav: "nav",
2132
+ header: "header",
2133
+ footer: "footer",
2134
+ main: "main",
2135
+ section: "section",
2136
+ article: "article",
2137
+ aside: "aside",
2138
+ ul: "list",
2139
+ ol: "list",
2140
+ li: "list item",
2141
+ table: "table",
2142
+ td: "cell",
2143
+ th: "header cell",
2144
+ span: "span",
2145
+ div: "div",
2146
+ label: "label",
2147
+ code: "code",
2148
+ pre: "code block",
2149
+ blockquote: "quote"
2150
+ };
2151
+ function pinLabel(annotation) {
2152
+ const raw = (annotation.element ?? "").toLowerCase();
2153
+ const tag = raw.split(/[.#\s]/)[0] ?? "";
2154
+ return (TAG_LABELS3[tag] ?? tag) || "element";
640
2155
  }
641
2156
  function Pin({ number, annotation }) {
642
2157
  const remove = useAnnotations((s) => s.remove);
643
- const [hovered, setHovered] = useState(false);
2158
+ const update = useAnnotations((s) => s.update);
2159
+ const [hovered, setHovered] = useState5(false);
2160
+ const [editing, setEditing] = useState5(false);
2161
+ const [draft, setDraft] = useState5(annotation.comment);
2162
+ const taRef = useRef6(null);
2163
+ const editRef = useRef6(null);
644
2164
  const pos = resolvePosition(annotation);
645
2165
  if (!pos) return null;
646
- return /* @__PURE__ */ jsxs(
2166
+ const label = pinLabel(annotation);
2167
+ const openEdit = () => {
2168
+ setDraft(annotation.comment);
2169
+ setEditing(true);
2170
+ setHovered(false);
2171
+ requestAnimationFrame(() => taRef.current?.focus());
2172
+ };
2173
+ const closeEdit = () => setEditing(false);
2174
+ const save = () => {
2175
+ const trimmed = draft.trim();
2176
+ if (trimmed) update(annotation.id, { comment: trimmed });
2177
+ closeEdit();
2178
+ };
2179
+ const onKeyDown = (e) => {
2180
+ if (e.key === "Escape") closeEdit();
2181
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) save();
2182
+ };
2183
+ useEffect5(() => {
2184
+ if (!editing) return;
2185
+ const onDown = (e) => {
2186
+ if (editRef.current && e.composedPath().includes(editRef.current)) return;
2187
+ closeEdit();
2188
+ };
2189
+ window.addEventListener("pointerdown", onDown, true);
2190
+ return () => window.removeEventListener("pointerdown", onDown, true);
2191
+ }, [editing]);
2192
+ return /* @__PURE__ */ jsxs7(
647
2193
  "div",
648
2194
  {
649
2195
  className: "clickly-pin",
650
2196
  style: { left: pos.x, top: pos.y },
651
- onMouseEnter: () => setHovered(true),
2197
+ onMouseEnter: () => {
2198
+ if (!editing) setHovered(true);
2199
+ },
652
2200
  onMouseLeave: () => setHovered(false),
2201
+ onClick: openEdit,
653
2202
  role: "button",
654
2203
  tabIndex: 0,
655
- title: annotation.comment,
2204
+ "aria-label": `Annotation ${number}: ${annotation.comment}`,
656
2205
  children: [
657
- /* @__PURE__ */ jsx("span", { className: "clickly-pin-num", children: number }),
658
- hovered && /* @__PURE__ */ jsxs("div", { className: "clickly-pin-bubble", children: [
659
- /* @__PURE__ */ jsx("div", { className: "clickly-pin-comment", children: annotation.comment }),
660
- /* @__PURE__ */ jsx("div", { className: "clickly-pin-meta", children: annotation.elementPath }),
661
- /* @__PURE__ */ jsx(
662
- "button",
663
- {
664
- className: "clickly-pin-remove",
665
- onClick: (e) => {
666
- e.stopPropagation();
667
- remove(annotation.id);
668
- },
669
- children: "Remove"
670
- }
671
- )
672
- ] })
2206
+ /* @__PURE__ */ jsx7("span", { className: "clickly-pin-num", children: number }),
2207
+ hovered && !editing && /* @__PURE__ */ jsxs7("div", { className: "pin-preview", children: [
2208
+ /* @__PURE__ */ jsxs7("div", { className: "pin-preview-meta", children: [
2209
+ label,
2210
+ ": ",
2211
+ annotation.elementPath
2212
+ ] }),
2213
+ /* @__PURE__ */ jsx7("div", { className: "pin-preview-comment", children: annotation.comment })
2214
+ ] }),
2215
+ editing && /* @__PURE__ */ jsxs7(
2216
+ "div",
2217
+ {
2218
+ ref: editRef,
2219
+ className: "pin-edit",
2220
+ onClick: (e) => e.stopPropagation(),
2221
+ children: [
2222
+ /* @__PURE__ */ jsx7("div", { className: "pin-edit-header", children: /* @__PURE__ */ jsxs7("span", { className: "pin-edit-label", children: [
2223
+ label,
2224
+ ": ",
2225
+ /* @__PURE__ */ jsx7("span", { className: "pin-edit-path", children: annotation.elementPath })
2226
+ ] }) }),
2227
+ /* @__PURE__ */ jsx7(
2228
+ "textarea",
2229
+ {
2230
+ ref: taRef,
2231
+ className: "pin-edit-textarea",
2232
+ value: draft,
2233
+ onChange: (e) => setDraft(e.target.value),
2234
+ onKeyDown,
2235
+ placeholder: "Describe the issue\u2026",
2236
+ rows: 3
2237
+ }
2238
+ ),
2239
+ /* @__PURE__ */ jsxs7("div", { className: "pin-edit-actions", children: [
2240
+ /* @__PURE__ */ jsx7(
2241
+ "button",
2242
+ {
2243
+ className: "pin-edit-delete",
2244
+ onClick: () => remove(annotation.id),
2245
+ "aria-label": "Delete annotation",
2246
+ children: /* @__PURE__ */ jsx7(IconTrash, {})
2247
+ }
2248
+ ),
2249
+ /* @__PURE__ */ jsxs7("div", { className: "pin-edit-right", children: [
2250
+ /* @__PURE__ */ jsx7("button", { className: "pin-edit-cancel", onClick: closeEdit, children: "Cancel" }),
2251
+ /* @__PURE__ */ jsx7("button", { className: "pin-edit-save", onClick: save, children: "Save" })
2252
+ ] })
2253
+ ] })
2254
+ ]
2255
+ }
2256
+ )
673
2257
  ]
674
2258
  }
675
2259
  );
@@ -693,26 +2277,29 @@ function resolvePosition(a) {
693
2277
  }
694
2278
  return null;
695
2279
  }
2280
+
2281
+ // packages/react/src/internal/ClicklyRoot.tsx
2282
+ import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
696
2283
  function ClicklyRoot({
697
2284
  engine,
698
2285
  host
699
2286
  }) {
700
- const [expanded, setExpanded] = useState(false);
2287
+ const [expanded, setExpanded] = useState6(false);
701
2288
  const clearAnnotations = useAnnotations((s) => s.clear);
702
2289
  const outputDetail = useSettings((s) => s.outputDetail);
703
2290
  const markerColor = useSettings((s) => s.markerColor);
704
2291
  const engineState = useEngineState(engine);
705
- useEffect(() => {
2292
+ useEffect6(() => {
706
2293
  host.style.setProperty("--clickly-hover", markerColor);
707
2294
  }, [host, markerColor]);
708
- useEffect(() => {
2295
+ useEffect6(() => {
709
2296
  if (expanded) {
710
2297
  if (engine.getSnapshot().kind === "idle") engine.activate("single");
711
2298
  } else {
712
2299
  if (engine.getSnapshot().kind !== "idle") engine.deactivate();
713
2300
  }
714
2301
  }, [expanded, engine]);
715
- useEffect(() => {
2302
+ useEffect6(() => {
716
2303
  const active = engineState.kind !== "idle";
717
2304
  if (active) document.body.setAttribute("data-clickly-active", "");
718
2305
  else document.body.removeAttribute("data-clickly-active");
@@ -730,7 +2317,7 @@ function ClicklyRoot({
730
2317
  document.body.removeAttribute("data-clickly-annotating");
731
2318
  };
732
2319
  }, [engineState]);
733
- useEffect(() => {
2320
+ useEffect6(() => {
734
2321
  const onKey = (e) => {
735
2322
  const tag = e.target?.tagName;
736
2323
  if (tag === "INPUT" || tag === "TEXTAREA") return;
@@ -773,16 +2360,16 @@ function ClicklyRoot({
773
2360
  window.addEventListener("keydown", onKey);
774
2361
  return () => window.removeEventListener("keydown", onKey);
775
2362
  }, [clearAnnotations, outputDetail, expanded, engine]);
776
- return /* @__PURE__ */ jsxs("div", { className: "clickly-ui", children: [
777
- /* @__PURE__ */ jsx(AnnotationPins, {}),
778
- expanded ? /* @__PURE__ */ jsxs(Fragment, { children: [
779
- /* @__PURE__ */ jsx(Toolbar, { engine, onCollapse: () => setExpanded(false) }),
780
- /* @__PURE__ */ jsx(AnnotationPopup, { engine })
781
- ] }) : /* @__PURE__ */ jsx(CollapsedFAB, { onExpand: () => setExpanded(true) })
2363
+ return /* @__PURE__ */ jsxs8("div", { className: "clickly-ui", children: [
2364
+ /* @__PURE__ */ jsx8(AnnotationPins, {}),
2365
+ expanded ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
2366
+ /* @__PURE__ */ jsx8(Toolbar, { engine, onCollapse: () => setExpanded(false) }),
2367
+ /* @__PURE__ */ jsx8(AnnotationPopup, { engine })
2368
+ ] }) : /* @__PURE__ */ jsx8(CollapsedFAB, { onExpand: () => setExpanded(true) })
782
2369
  ] });
783
2370
  }
784
2371
 
785
- // src/internal/styles.ts
2372
+ // packages/react/src/internal/styles.ts
786
2373
  var REACT_UI_CSS = `
787
2374
  .clickly-ui, .clickly-ui * { box-sizing: border-box; }
788
2375
 
@@ -840,72 +2427,151 @@ var REACT_UI_CSS = `
840
2427
  position: fixed;
841
2428
  display: flex;
842
2429
  align-items: center;
843
- gap: 4px;
844
- padding: 4px;
845
- background: rgba(15, 23, 42, 0.94);
2430
+ gap: 2px;
2431
+ padding: 6px;
2432
+ height: 44px;
2433
+ background: rgba(9, 14, 28, 0.97);
846
2434
  color: #f8fafc;
847
- border-radius: 12px;
2435
+ border-radius: 16px;
848
2436
  font: 13px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
849
- box-shadow: 0 8px 24px rgba(0,0,0,0.3), 0 0 0 1px rgba(255,255,255,0.06) inset;
2437
+ box-shadow:
2438
+ 0 16px 40px rgba(0,0,0,0.45),
2439
+ 0 0 0 1px rgba(255,255,255,0.08) inset,
2440
+ 0 1px 0 rgba(255,255,255,0.06) inset;
850
2441
  pointer-events: auto;
851
2442
  user-select: none;
852
2443
  z-index: 1;
853
- animation: clickly-fade-in 120ms ease-out;
2444
+ animation: clickly-fade-in 150ms cubic-bezier(0.16, 1, 0.3, 1);
854
2445
  }
855
2446
 
856
2447
  @keyframes clickly-fade-in {
857
- from { opacity: 0; transform: translateY(4px); }
858
- to { opacity: 1; transform: translateY(0); }
2448
+ from { opacity: 0; transform: translateY(6px) scale(0.97); }
2449
+ to { opacity: 1; transform: translateY(0) scale(1); }
859
2450
  }
860
2451
 
861
2452
  .clickly-toolbar .grip {
862
2453
  display: grid;
863
2454
  place-items: center;
864
- width: 24px;
865
- height: 28px;
866
- color: #94a3b8;
2455
+ width: 22px;
2456
+ height: 32px;
2457
+ color: #475569;
867
2458
  touch-action: none;
2459
+ cursor: grab;
2460
+ border-radius: 6px;
2461
+ transition: color 120ms ease;
868
2462
  }
2463
+ .clickly-toolbar .grip:hover { color: #94a3b8; }
2464
+ .clickly-toolbar .grip:active { cursor: grabbing; }
869
2465
 
870
2466
  .clickly-toolbar .divider {
871
2467
  width: 1px;
872
- height: 18px;
873
- background: rgba(255,255,255,0.12);
874
- margin: 0 2px;
2468
+ height: 20px;
2469
+ background: rgba(255,255,255,0.08);
2470
+ margin: 0 3px;
2471
+ flex-shrink: 0;
875
2472
  }
876
2473
 
877
2474
  .clickly-btn {
878
2475
  display: inline-flex;
879
2476
  align-items: center;
2477
+ justify-content: center;
880
2478
  gap: 4px;
881
- height: 28px;
2479
+ height: 32px;
882
2480
  padding: 0 8px;
883
2481
  background: transparent;
884
2482
  border: none;
885
- border-radius: 8px;
886
- color: #cbd5e1;
2483
+ border-radius: 10px;
2484
+ color: #94a3b8;
887
2485
  cursor: pointer;
888
2486
  font: inherit;
2487
+ transition: background 100ms ease, color 100ms ease;
2488
+ position: relative;
2489
+ }
2490
+ .clickly-btn:hover { background: rgba(255,255,255,0.09); color: #e2e8f0; }
2491
+ .clickly-btn:active { background: rgba(255,255,255,0.15); color: #fff; transform: scale(0.96); }
2492
+ .clickly-btn.is-active {
2493
+ background: #0ea5e9;
2494
+ color: #fff;
2495
+ box-shadow: 0 0 0 1px rgba(14,165,233,0.4), 0 2px 8px rgba(14,165,233,0.3);
889
2496
  }
890
- .clickly-btn:hover { background: rgba(255,255,255,0.08); color: #fff; }
891
- .clickly-btn:active { background: rgba(255,255,255,0.14); }
892
- .clickly-btn.is-active { background: #0ea5e9; color: #fff; }
893
- .clickly-btn[disabled] { opacity: 0.4; cursor: not-allowed; }
2497
+ .clickly-btn.is-active:hover { background: #0284c7; }
2498
+ .clickly-btn[disabled] { opacity: 0.28; cursor: not-allowed; pointer-events: none; }
894
2499
 
895
2500
  .clickly-btn.icon-only {
896
- width: 28px;
2501
+ width: 32px;
897
2502
  padding: 0;
898
- justify-content: center;
899
2503
  }
900
2504
 
901
2505
  .clickly-btn.primary-pinned {
902
2506
  background: #10b981;
903
2507
  color: #fff;
2508
+ font-weight: 600;
2509
+ font-size: 12px;
2510
+ padding: 0 10px;
2511
+ gap: 5px;
2512
+ box-shadow: 0 0 0 1px rgba(16,185,129,0.4), 0 2px 8px rgba(16,185,129,0.25);
2513
+ }
2514
+ .clickly-btn.primary-pinned:hover { background: #059669; }
2515
+
2516
+ /* \u2500\u2500\u2500 Tooltip \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2517
+
2518
+ .clickly-tip {
2519
+ position: relative;
2520
+ display: inline-flex;
2521
+ align-items: center;
2522
+ justify-content: center;
2523
+ /* Ensures the absolute-positioned counter badge clips correctly */
2524
+ isolation: isolate;
2525
+ }
2526
+
2527
+ .clickly-tip .tip-bubble {
2528
+ position: absolute;
2529
+ bottom: calc(100% + 10px);
2530
+ left: 50%;
2531
+ transform: translateX(-50%);
2532
+ background: rgba(15, 23, 42, 0.98);
2533
+ color: #f1f5f9;
2534
+ font-size: 12px;
904
2535
  font-weight: 500;
2536
+ white-space: nowrap;
2537
+ padding: 5px 10px;
2538
+ border-radius: 8px;
2539
+ pointer-events: none;
2540
+ opacity: 0;
2541
+ transition: opacity 80ms ease;
2542
+ transition-delay: 200ms;
2543
+ box-shadow: 0 4px 16px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.07) inset;
2544
+ display: flex;
2545
+ align-items: center;
2546
+ gap: 6px;
2547
+ z-index: 10;
905
2548
  }
906
- .clickly-btn.primary-pinned:hover {
907
- background: #059669;
908
- color: #fff;
2549
+
2550
+ /* Arrow */
2551
+ .clickly-tip .tip-bubble::after {
2552
+ content: "";
2553
+ position: absolute;
2554
+ top: 100%;
2555
+ left: 50%;
2556
+ transform: translateX(-50%);
2557
+ border: 5px solid transparent;
2558
+ border-top-color: rgba(15, 23, 42, 0.98);
2559
+ }
2560
+
2561
+ .clickly-tip:hover .tip-bubble { opacity: 1; }
2562
+
2563
+ .clickly-tip .tip-bubble kbd {
2564
+ display: inline-flex;
2565
+ align-items: center;
2566
+ justify-content: center;
2567
+ min-width: 18px;
2568
+ height: 18px;
2569
+ padding: 0 4px;
2570
+ background: rgba(255,255,255,0.12);
2571
+ border: 1px solid rgba(255,255,255,0.10);
2572
+ border-radius: 4px;
2573
+ font: 11px/1 ui-monospace, "SF Mono", Menlo, monospace;
2574
+ color: #94a3b8;
909
2575
  }
910
2576
 
911
2577
  /* \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 */
@@ -922,17 +2588,159 @@ var REACT_UI_CSS = `
922
2588
  animation: clickly-fade-in 120ms ease-out;
923
2589
  }
924
2590
 
925
- .clickly-popup { width: 320px; padding: 12px; }
2591
+ .clickly-popup {
2592
+ width: 320px;
2593
+ padding: 12px;
2594
+ max-height: calc(100vh - 80px);
2595
+ overflow: hidden;
2596
+ display: flex;
2597
+ flex-direction: column;
2598
+ }
926
2599
 
927
- .clickly-popup .target-info {
2600
+ /* Collapsible header row \u2014 shows element label + CSS toggle */
2601
+ .clickly-popup .popup-header {
928
2602
  display: flex;
929
2603
  align-items: center;
930
- justify-content: space-between;
931
- gap: 8px;
2604
+ gap: 5px;
932
2605
  margin-bottom: 8px;
2606
+ padding: 4px 2px;
2607
+ border-radius: 4px;
2608
+ cursor: pointer;
2609
+ user-select: none;
933
2610
  color: #475569;
934
2611
  font-size: 11px;
935
2612
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
2613
+ transition: background 80ms ease;
2614
+ }
2615
+ .clickly-popup .popup-header:hover { background: #f1f5f9; }
2616
+
2617
+ .clickly-popup .popup-chevron {
2618
+ font-size: 12px;
2619
+ color: #94a3b8;
2620
+ flex-shrink: 0;
2621
+ width: 12px;
2622
+ text-align: center;
2623
+ }
2624
+
2625
+ .clickly-popup .popup-label {
2626
+ flex: 1;
2627
+ overflow: hidden;
2628
+ white-space: nowrap;
2629
+ text-overflow: ellipsis;
2630
+ }
2631
+
2632
+ /* Edit-mode toggle button \u2014 top-right of CSS panel header */
2633
+ .clickly-popup .popup-edit-toggle {
2634
+ flex-shrink: 0;
2635
+ display: grid;
2636
+ place-items: center;
2637
+ width: 20px;
2638
+ height: 20px;
2639
+ background: transparent;
2640
+ border: 1px solid #e2e8f0;
2641
+ border-radius: 5px;
2642
+ color: #94a3b8;
2643
+ cursor: pointer;
2644
+ padding: 0;
2645
+ margin-left: auto;
2646
+ transition: background 100ms ease, color 100ms ease, border-color 100ms ease;
2647
+ }
2648
+ .clickly-popup .popup-edit-toggle:hover {
2649
+ background: #f1f5f9;
2650
+ color: #475569;
2651
+ border-color: #cbd5e1;
2652
+ }
2653
+ .clickly-popup .popup-edit-toggle.is-editing {
2654
+ background: #0ea5e9;
2655
+ color: #fff;
2656
+ border-color: #0ea5e9;
2657
+ }
2658
+ .clickly-popup .popup-edit-toggle.is-editing:hover {
2659
+ background: #0284c7;
2660
+ }
2661
+
2662
+ /* Computed-styles panel */
2663
+ .clickly-popup .popup-styles {
2664
+ margin-bottom: 8px;
2665
+ padding: 8px;
2666
+ background: #f8fafc;
2667
+ border: 1px solid #e2e8f0;
2668
+ border-radius: 6px;
2669
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
2670
+ font-size: 11px;
2671
+ max-height: 160px;
2672
+ overflow-y: auto;
2673
+ overscroll-behavior: contain;
2674
+ }
2675
+
2676
+ .clickly-popup .style-row {
2677
+ display: flex;
2678
+ align-items: center;
2679
+ gap: 6px;
2680
+ line-height: 1.7;
2681
+ border-radius: 4px;
2682
+ padding: 0 2px;
2683
+ transition: background 80ms ease;
2684
+ }
2685
+
2686
+ .clickly-popup .style-row--changed {
2687
+ background: rgba(245, 158, 11, 0.10);
2688
+ }
2689
+
2690
+ .clickly-popup .style-key {
2691
+ color: #7c3aed;
2692
+ flex-shrink: 0;
2693
+ min-width: 90px;
2694
+ font-size: 11px;
2695
+ }
2696
+
2697
+ /* Editable value input \u2014 live CSS preview */
2698
+ .clickly-popup .style-val-input {
2699
+ flex: 1;
2700
+ min-width: 0;
2701
+ background: transparent;
2702
+ border: none;
2703
+ border-bottom: 1px solid transparent;
2704
+ color: #0f172a;
2705
+ font: 11px/1.7 ui-monospace, "SF Mono", Menlo, monospace;
2706
+ padding: 0;
2707
+ outline: none;
2708
+ transition: border-color 100ms ease;
2709
+ word-break: break-all;
2710
+ }
2711
+ .clickly-popup .style-val-input:focus {
2712
+ border-bottom-color: #0ea5e9;
2713
+ background: rgba(14, 165, 233, 0.05);
2714
+ border-radius: 2px 2px 0 0;
2715
+ }
2716
+
2717
+ /* Revert button shown only on changed rows */
2718
+ .clickly-popup .style-revert-btn {
2719
+ flex-shrink: 0;
2720
+ background: transparent;
2721
+ border: none;
2722
+ color: #94a3b8;
2723
+ font-size: 13px;
2724
+ cursor: pointer;
2725
+ padding: 0 2px;
2726
+ line-height: 1;
2727
+ border-radius: 3px;
2728
+ transition: color 80ms ease, background 80ms ease;
2729
+ }
2730
+ .clickly-popup .style-revert-btn:hover {
2731
+ color: #f59e0b;
2732
+ background: rgba(245,158,11,0.12);
2733
+ }
2734
+
2735
+ /* Hint text at bottom of CSS panel when edits exist */
2736
+ .clickly-popup .style-hint {
2737
+ margin-top: 6px;
2738
+ padding-top: 6px;
2739
+ border-top: 1px solid #e2e8f0;
2740
+ font-size: 10.5px;
2741
+ color: #94a3b8;
2742
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
2743
+ line-height: 1.4;
936
2744
  }
937
2745
 
938
2746
  .clickly-popup textarea {
@@ -980,121 +2788,560 @@ var REACT_UI_CSS = `
980
2788
  cursor: pointer;
981
2789
  }
982
2790
 
2791
+ /* Old popover kept for any legacy use */
983
2792
  .clickly-popover { width: 260px; padding: 12px; }
984
- .clickly-popover h4 { margin: 0 0 8px; font-size: 12px; color: #475569; text-transform: uppercase; letter-spacing: 0.05em; }
985
- .clickly-popover .field { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 0; font-size: 13px; }
986
- .clickly-popover select { font: inherit; padding: 4px 6px; border-radius: 4px; border: 1px solid #e2e8f0; }
987
- .clickly-popover input[type="color"] { width: 28px; height: 24px; padding: 0; border: 1px solid #e2e8f0; border-radius: 4px; background: none; }
2793
+
2794
+ /* \u2500\u2500\u2500 Settings panel (redesigned) \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 */
2795
+
2796
+ .clickly-settings {
2797
+ position: fixed;
2798
+ width: 284px;
2799
+ background: #fff;
2800
+ border-radius: 14px;
2801
+ box-shadow: 0 16px 48px rgba(2,6,23,0.20), 0 0 0 1px rgba(15,23,42,0.07);
2802
+ font: 13px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
2803
+ color: #0f172a;
2804
+ pointer-events: auto;
2805
+ z-index: 2;
2806
+ animation: clickly-fade-in 150ms cubic-bezier(0.16, 1, 0.3, 1);
2807
+ overflow: hidden;
2808
+ }
2809
+
2810
+ .settings-header {
2811
+ display: flex;
2812
+ align-items: center;
2813
+ justify-content: space-between;
2814
+ padding: 14px 14px 12px;
2815
+ border-bottom: 1px solid #f1f5f9;
2816
+ }
2817
+
2818
+ .settings-title {
2819
+ font-size: 13px;
2820
+ font-weight: 600;
2821
+ color: #0f172a;
2822
+ letter-spacing: -0.01em;
2823
+ }
2824
+
2825
+ .settings-close {
2826
+ display: grid;
2827
+ place-items: center;
2828
+ width: 24px;
2829
+ height: 24px;
2830
+ background: #f1f5f9;
2831
+ border: none;
2832
+ border-radius: 6px;
2833
+ color: #64748b;
2834
+ cursor: pointer;
2835
+ padding: 0;
2836
+ transition: background 100ms, color 100ms;
2837
+ }
2838
+ .settings-close:hover { background: #e2e8f0; color: #0f172a; }
2839
+ .settings-close svg { width: 13px; height: 13px; }
2840
+
2841
+ .settings-section { padding: 10px 14px; }
2842
+
2843
+ .settings-divider {
2844
+ height: 1px;
2845
+ background: #f1f5f9;
2846
+ margin: 0;
2847
+ }
2848
+
2849
+ .settings-label {
2850
+ display: flex;
2851
+ flex-direction: column;
2852
+ gap: 2px;
2853
+ font-size: 13px;
2854
+ font-weight: 500;
2855
+ color: #1e293b;
2856
+ margin-bottom: 8px;
2857
+ }
2858
+
2859
+ .settings-hint {
2860
+ font-size: 11.5px;
2861
+ font-weight: 400;
2862
+ color: #94a3b8;
2863
+ }
2864
+
2865
+ .settings-select {
2866
+ width: 100%;
2867
+ padding: 7px 10px;
2868
+ background: #f8fafc;
2869
+ border: 1px solid #e2e8f0;
2870
+ border-radius: 8px;
2871
+ font: inherit;
2872
+ font-size: 13px;
2873
+ color: #1e293b;
2874
+ cursor: pointer;
2875
+ appearance: none;
2876
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
2877
+ background-repeat: no-repeat;
2878
+ background-position: right 10px center;
2879
+ padding-right: 32px;
2880
+ transition: border-color 120ms, box-shadow 120ms;
2881
+ }
2882
+ .settings-select:focus {
2883
+ outline: none;
2884
+ border-color: #0ea5e9;
2885
+ box-shadow: 0 0 0 3px rgba(14,165,233,0.15);
2886
+ background-color: #fff;
2887
+ }
2888
+
2889
+ .settings-row {
2890
+ display: flex;
2891
+ align-items: center;
2892
+ justify-content: space-between;
2893
+ gap: 12px;
2894
+ padding: 6px 0;
2895
+ }
2896
+ .settings-row + .settings-row {
2897
+ border-top: 1px solid #f8fafc;
2898
+ }
2899
+
2900
+ .settings-row-label {
2901
+ display: flex;
2902
+ flex-direction: column;
2903
+ gap: 2px;
2904
+ font-size: 13px;
2905
+ font-weight: 500;
2906
+ color: #1e293b;
2907
+ min-width: 0;
2908
+ }
2909
+
2910
+ /* Toggle switch */
2911
+ .clickly-toggle {
2912
+ position: relative;
2913
+ flex-shrink: 0;
2914
+ width: 38px;
2915
+ height: 22px;
2916
+ cursor: pointer;
2917
+ }
2918
+ .clickly-toggle input {
2919
+ position: absolute;
2920
+ opacity: 0;
2921
+ width: 0;
2922
+ height: 0;
2923
+ }
2924
+ .toggle-track {
2925
+ position: absolute;
2926
+ inset: 0;
2927
+ background: #e2e8f0;
2928
+ border-radius: 11px;
2929
+ transition: background 180ms ease;
2930
+ }
2931
+ .toggle-track::after {
2932
+ content: "";
2933
+ position: absolute;
2934
+ top: 3px;
2935
+ left: 3px;
2936
+ width: 16px;
2937
+ height: 16px;
2938
+ background: #fff;
2939
+ border-radius: 50%;
2940
+ box-shadow: 0 1px 4px rgba(0,0,0,0.2);
2941
+ transition: transform 180ms cubic-bezier(0.34, 1.56, 0.64, 1);
2942
+ }
2943
+ .clickly-toggle input:checked + .toggle-track {
2944
+ background: #0ea5e9;
2945
+ }
2946
+ .clickly-toggle input:checked + .toggle-track::after {
2947
+ transform: translateX(16px);
2948
+ }
2949
+
2950
+ /* Color picker */
2951
+ .settings-color-wrap {
2952
+ display: flex;
2953
+ align-items: center;
2954
+ gap: 0;
2955
+ cursor: pointer;
2956
+ border-radius: 8px;
2957
+ overflow: hidden;
2958
+ border: 1px solid #e2e8f0;
2959
+ flex-shrink: 0;
2960
+ }
2961
+ .settings-color-wrap input[type="color"] {
2962
+ position: absolute;
2963
+ opacity: 0;
2964
+ width: 0;
2965
+ height: 0;
2966
+ pointer-events: none;
2967
+ }
2968
+ .color-swatch {
2969
+ display: block;
2970
+ width: 32px;
2971
+ height: 24px;
2972
+ border-radius: 7px;
2973
+ border: 1px solid rgba(0,0,0,0.08);
2974
+ transition: transform 100ms ease;
2975
+ }
2976
+ .settings-color-wrap:hover .color-swatch { transform: scale(1.08); }
988
2977
 
989
2978
  .clickly-counter {
990
- display: inline-flex;
2979
+ /* Float as a badge \u2014 positioned absolutely so it doesn't widen the button */
2980
+ position: absolute;
2981
+ top: -5px;
2982
+ right: -5px;
2983
+ min-width: 16px;
2984
+ height: 16px;
2985
+ padding: 0 4px;
2986
+ border-radius: 8px;
2987
+ background: #f59e0b;
2988
+ color: #0f172a;
2989
+ font-size: 10px;
2990
+ font-weight: 700;
2991
+ display: flex;
991
2992
  align-items: center;
992
2993
  justify-content: center;
993
- min-width: 18px;
994
- height: 18px;
995
- padding: 0 5px;
996
- border-radius: 9px;
997
- background: #f59e0b;
998
- color: #1f2937;
999
- font-size: 11px;
1000
- font-weight: 600;
1001
- margin-left: 4px;
2994
+ pointer-events: none;
2995
+ border: 1.5px solid rgba(9, 14, 28, 0.97);
1002
2996
  }
1003
2997
 
1004
2998
  /* \u2500\u2500\u2500 Annotation pins (persistent numbered markers) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1005
2999
 
1006
3000
  .clickly-pin {
1007
3001
  position: fixed;
1008
- width: 22px;
1009
- height: 22px;
3002
+ width: 24px;
3003
+ height: 24px;
1010
3004
  border-radius: 999px;
1011
- background: #f59e0b;
1012
- color: #0f172a;
1013
- font: 600 12px/22px -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
3005
+ background: #10b981;
3006
+ color: #fff;
3007
+ font: 700 11px/24px -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
1014
3008
  text-align: center;
1015
3009
  cursor: pointer;
1016
3010
  pointer-events: auto;
1017
3011
  user-select: none;
1018
- box-shadow: 0 2px 6px rgba(0,0,0,0.25), 0 0 0 2px #fff;
1019
- z-index: 1;
1020
- transition: transform 100ms ease;
3012
+ box-shadow: 0 2px 8px rgba(16,185,129,0.4), 0 0 0 2px #fff;
3013
+ z-index: 10;
3014
+ transition: transform 120ms cubic-bezier(0.34,1.56,0.64,1), box-shadow 120ms ease;
3015
+ }
3016
+ .clickly-pin:hover {
3017
+ transform: scale(1.18);
3018
+ box-shadow: 0 4px 16px rgba(16,185,129,0.5), 0 0 0 2px #fff;
1021
3019
  }
1022
- .clickly-pin:hover { transform: scale(1.12); }
1023
3020
  .clickly-pin-num { display: block; }
1024
3021
 
1025
- .clickly-pin-bubble {
3022
+ /* \u2500\u2500\u2500 Pin hover preview (dark tooltip) \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 */
3023
+
3024
+ .pin-preview {
1026
3025
  position: absolute;
1027
- top: 28px;
1028
- left: 0;
1029
- width: 240px;
3026
+ right: calc(100% + 10px);
3027
+ top: 50%;
3028
+ transform: translateY(-50%);
3029
+ width: 220px;
1030
3030
  padding: 8px 10px;
1031
- background: #fff;
1032
- color: #0f172a;
1033
- border-radius: 8px;
1034
- box-shadow: 0 8px 24px rgba(2,6,23,0.18), 0 0 0 1px rgba(15,23,42,0.06);
1035
- font: 13px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
3031
+ background: rgba(9, 14, 28, 0.96);
3032
+ color: #f1f5f9;
3033
+ border-radius: 10px;
3034
+ box-shadow: 0 8px 24px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.07) inset;
3035
+ font: 12px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
1036
3036
  text-align: left;
1037
3037
  cursor: default;
1038
- z-index: 3;
3038
+ pointer-events: none;
3039
+ z-index: 11;
1039
3040
  animation: clickly-fade-in 100ms ease-out;
3041
+ white-space: normal;
1040
3042
  }
1041
- .clickly-pin-comment { word-break: break-word; }
1042
- .clickly-pin-meta {
1043
- margin-top: 6px;
3043
+
3044
+ .pin-preview-meta {
3045
+ font-size: 10.5px;
3046
+ color: #64748b;
1044
3047
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
1045
- font-size: 11px;
3048
+ margin-bottom: 4px;
3049
+ overflow: hidden;
3050
+ white-space: nowrap;
3051
+ text-overflow: ellipsis;
3052
+ }
3053
+
3054
+ .pin-preview-comment {
3055
+ font-size: 12px;
3056
+ color: #e2e8f0;
3057
+ word-break: break-word;
3058
+ display: -webkit-box;
3059
+ -webkit-line-clamp: 3;
3060
+ -webkit-box-orient: vertical;
3061
+ overflow: hidden;
3062
+ }
3063
+
3064
+ /* \u2500\u2500\u2500 Pin edit popup \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 */
3065
+
3066
+ .pin-edit {
3067
+ position: absolute;
3068
+ right: calc(100% + 10px);
3069
+ top: 50%;
3070
+ transform: translateY(-50%);
3071
+ width: 260px;
3072
+ background: #fff;
3073
+ border-radius: 12px;
3074
+ box-shadow: 0 12px 40px rgba(2,6,23,0.20), 0 0 0 1px rgba(15,23,42,0.07);
3075
+ font: 13px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
3076
+ color: #0f172a;
3077
+ text-align: left;
3078
+ cursor: default;
3079
+ z-index: 11;
3080
+ animation: clickly-fade-in 120ms cubic-bezier(0.16,1,0.3,1);
3081
+ overflow: hidden;
3082
+ }
3083
+
3084
+ .pin-edit-header {
3085
+ padding: 10px 12px 8px;
3086
+ border-bottom: 1px solid #f1f5f9;
3087
+ }
3088
+
3089
+ .pin-edit-label {
3090
+ font-size: 11.5px;
3091
+ font-weight: 600;
1046
3092
  color: #475569;
1047
- word-break: break-all;
1048
3093
  }
1049
- .clickly-pin-remove {
1050
- margin-top: 8px;
1051
- padding: 4px 8px;
1052
- border: 1px solid #fecaca;
3094
+
3095
+ .pin-edit-path {
3096
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
3097
+ font-weight: 400;
3098
+ color: #94a3b8;
3099
+ font-size: 10.5px;
3100
+ overflow: hidden;
3101
+ white-space: nowrap;
3102
+ text-overflow: ellipsis;
3103
+ display: inline-block;
3104
+ max-width: 160px;
3105
+ vertical-align: bottom;
3106
+ }
3107
+
3108
+ .pin-edit-textarea {
3109
+ display: block;
3110
+ width: 100%;
3111
+ min-height: 72px;
3112
+ padding: 10px 12px;
3113
+ border: none;
3114
+ border-bottom: 1px solid #f1f5f9;
3115
+ resize: vertical;
3116
+ font: 13px/1.5 inherit;
3117
+ color: #0f172a;
1053
3118
  background: #fff;
1054
- color: #b91c1c;
1055
- border-radius: 4px;
1056
- font: 11px inherit;
3119
+ box-sizing: border-box;
3120
+ }
3121
+ .pin-edit-textarea:focus {
3122
+ outline: none;
3123
+ background: #f8fafc;
3124
+ }
3125
+
3126
+ .pin-edit-actions {
3127
+ display: flex;
3128
+ align-items: center;
3129
+ justify-content: space-between;
3130
+ padding: 8px 10px;
3131
+ gap: 6px;
3132
+ }
3133
+
3134
+ .pin-edit-right {
3135
+ display: flex;
3136
+ gap: 6px;
3137
+ }
3138
+
3139
+ .pin-edit-delete {
3140
+ display: grid;
3141
+ place-items: center;
3142
+ width: 30px;
3143
+ height: 30px;
3144
+ background: transparent;
3145
+ border: 1px solid #fee2e2;
3146
+ border-radius: 8px;
3147
+ color: #ef4444;
3148
+ cursor: pointer;
3149
+ padding: 0;
3150
+ transition: background 100ms, border-color 100ms;
3151
+ }
3152
+ .pin-edit-delete:hover { background: #fef2f2; border-color: #fca5a5; }
3153
+ .pin-edit-delete svg { width: 14px; height: 14px; }
3154
+
3155
+ .pin-edit-cancel, .pin-edit-save {
3156
+ height: 30px;
3157
+ padding: 0 12px;
3158
+ border-radius: 8px;
3159
+ font: 12px/1 inherit;
3160
+ font-weight: 500;
1057
3161
  cursor: pointer;
3162
+ border: none;
3163
+ transition: background 100ms;
1058
3164
  }
1059
- .clickly-pin-remove:hover { background: #fef2f2; }
3165
+
3166
+ .pin-edit-cancel {
3167
+ background: #f1f5f9;
3168
+ color: #475569;
3169
+ }
3170
+ .pin-edit-cancel:hover { background: #e2e8f0; }
3171
+
3172
+ .pin-edit-save {
3173
+ background: #10b981;
3174
+ color: #fff;
3175
+ }
3176
+ .pin-edit-save:hover { background: #059669; }
1060
3177
 
1061
3178
  /* \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 */
1062
3179
 
1063
3180
  .clickly-list {
1064
3181
  position: fixed;
1065
- max-height: 50vh;
1066
3182
  width: 320px;
1067
- overflow-y: auto;
3183
+ max-height: 55vh;
1068
3184
  background: #fff;
1069
- border-radius: 10px;
1070
- box-shadow: 0 12px 32px rgba(2,6,23,0.18), 0 0 0 1px rgba(15,23,42,0.06);
3185
+ border-radius: 14px;
3186
+ box-shadow: 0 16px 48px rgba(2,6,23,0.20), 0 0 0 1px rgba(15,23,42,0.07);
1071
3187
  color: #0f172a;
1072
- font: 12px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
3188
+ font: 13px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
1073
3189
  pointer-events: auto;
1074
3190
  z-index: 2;
1075
- animation: clickly-fade-in 120ms ease-out;
3191
+ animation: clickly-fade-in 150ms cubic-bezier(0.16, 1, 0.3, 1);
3192
+ display: flex;
3193
+ flex-direction: column;
3194
+ overflow: hidden;
3195
+ }
3196
+
3197
+ .list-header {
3198
+ display: flex;
3199
+ align-items: center;
3200
+ gap: 8px;
3201
+ padding: 12px 14px 10px;
3202
+ border-bottom: 1px solid #f1f5f9;
3203
+ flex-shrink: 0;
3204
+ }
3205
+
3206
+ .list-title {
3207
+ font-size: 13px;
3208
+ font-weight: 600;
3209
+ color: #0f172a;
3210
+ letter-spacing: -0.01em;
3211
+ flex: 1;
3212
+ }
3213
+
3214
+ .list-count {
3215
+ min-width: 20px;
3216
+ height: 20px;
3217
+ padding: 0 6px;
3218
+ background: #f1f5f9;
3219
+ color: #475569;
3220
+ font-size: 11px;
3221
+ font-weight: 600;
3222
+ border-radius: 10px;
3223
+ display: grid;
3224
+ place-items: center;
3225
+ }
3226
+
3227
+ .list-items {
3228
+ overflow-y: auto;
3229
+ overscroll-behavior: contain;
3230
+ flex: 1;
3231
+ }
3232
+
3233
+ .list-empty {
3234
+ padding: 24px;
3235
+ text-align: center;
3236
+ color: #94a3b8;
3237
+ font-size: 12px;
3238
+ }
3239
+
3240
+ /* \u2500\u2500\u2500 Annotation card \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 */
3241
+
3242
+ .list-card {
3243
+ padding: 10px 12px;
3244
+ border-bottom: 1px solid #f8fafc;
3245
+ transition: background 80ms ease;
3246
+ }
3247
+ .list-card:last-child { border-bottom: none; }
3248
+ .list-card:hover { background: #fafafa; }
3249
+
3250
+ .list-card-header {
3251
+ display: flex;
3252
+ align-items: center;
3253
+ gap: 6px;
3254
+ margin-bottom: 4px;
3255
+ }
3256
+
3257
+ .list-card-num {
3258
+ font-size: 11px;
3259
+ font-weight: 700;
3260
+ color: #94a3b8;
3261
+ flex-shrink: 0;
3262
+ min-width: 20px;
1076
3263
  }
1077
- .clickly-list .row { display: flex; gap: 8px; padding: 8px 10px; border-bottom: 1px solid #f1f5f9; }
1078
- .clickly-list .row:last-child { border-bottom: none; }
1079
- .clickly-list .num { color: #94a3b8; min-width: 18px; }
1080
- .clickly-list .selector { color: #475569; font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 11px; word-break: break-all; }
1081
- .clickly-list .body { flex: 1; min-width: 0; }
1082
- .clickly-list .body p { margin: 2px 0 0; word-break: break-word; }
1083
- .clickly-list button.remove {
3264
+
3265
+ .list-card-path {
3266
+ flex: 1;
3267
+ min-width: 0;
3268
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
3269
+ font-size: 10.5px;
3270
+ color: #475569;
3271
+ white-space: nowrap;
3272
+ overflow: hidden;
3273
+ text-overflow: ellipsis;
3274
+ }
3275
+
3276
+ .list-card-actions {
3277
+ display: flex;
3278
+ gap: 3px;
3279
+ flex-shrink: 0;
3280
+ }
3281
+
3282
+ .list-action-btn {
3283
+ display: grid;
3284
+ place-items: center;
3285
+ width: 24px;
3286
+ height: 24px;
1084
3287
  background: transparent;
1085
3288
  border: none;
3289
+ border-radius: 6px;
1086
3290
  color: #94a3b8;
1087
3291
  cursor: pointer;
1088
- padding: 2px;
1089
- display: grid;
1090
- place-items: center;
3292
+ padding: 0;
3293
+ transition: background 80ms ease, color 80ms ease;
3294
+ }
3295
+ .list-action-btn svg { width: 12px; height: 12px; }
3296
+ .list-action-btn:hover { background: #f1f5f9; color: #475569; }
3297
+ .list-action-btn.copied { color: #10b981; }
3298
+ .list-action-btn.list-action-delete:hover { background: #fef2f2; color: #ef4444; }
3299
+
3300
+ .list-card-comment {
3301
+ font-size: 12px;
3302
+ color: #1e293b;
3303
+ line-height: 1.45;
3304
+ margin: 0 0 6px;
3305
+ word-break: break-word;
3306
+ display: -webkit-box;
3307
+ -webkit-line-clamp: 2;
3308
+ -webkit-box-orient: vertical;
3309
+ overflow: hidden;
3310
+ }
3311
+
3312
+ /* CSS changes badge */
3313
+ .list-card-css {
3314
+ background: rgba(124, 58, 237, 0.05);
3315
+ border: 1px solid rgba(124, 58, 237, 0.12);
3316
+ border-radius: 6px;
3317
+ padding: 5px 8px;
3318
+ margin-top: 4px;
3319
+ }
3320
+
3321
+ .list-card-css-label {
3322
+ display: block;
3323
+ font-size: 10px;
3324
+ font-weight: 600;
3325
+ color: #7c3aed;
3326
+ text-transform: uppercase;
3327
+ letter-spacing: 0.04em;
3328
+ margin-bottom: 3px;
3329
+ }
3330
+
3331
+ .list-card-css-code {
3332
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
3333
+ font-size: 10px;
3334
+ color: #475569;
3335
+ line-height: 1.5;
3336
+ margin: 0;
3337
+ white-space: pre-wrap;
3338
+ word-break: break-all;
3339
+ max-height: 60px;
3340
+ overflow: hidden;
1091
3341
  }
1092
- .clickly-list button.remove svg { width: 12px; height: 12px; }
1093
- .clickly-list button.remove:hover { color: #ef4444; }
1094
- .clickly-list .empty { padding: 20px; text-align: center; color: #94a3b8; }
1095
3342
  `;
1096
3343
 
1097
- // src/internal/globalStyles.ts
3344
+ // packages/react/src/internal/globalStyles.ts
1098
3345
  var GLOBAL_PAGE_CSS = `
1099
3346
  body[data-clickly-active] {
1100
3347
  -webkit-user-select: none !important;
@@ -1114,9 +3361,12 @@ body[data-clickly-annotating] {
1114
3361
  cursor: default !important;
1115
3362
  }
1116
3363
  `;
3364
+
3365
+ // packages/react/src/Clickly.tsx
3366
+ import { jsx as jsx9 } from "react/jsx-runtime";
1117
3367
  function Clickly({ className } = {}) {
1118
- const [mount, setMount] = useState(null);
1119
- useEffect(() => {
3368
+ const [mount, setMount] = useState7(null);
3369
+ useEffect7(() => {
1120
3370
  if (typeof window === "undefined" || typeof document === "undefined") return;
1121
3371
  let shadow = null;
1122
3372
  let engine = null;
@@ -1165,7 +3415,7 @@ function Clickly({ className } = {}) {
1165
3415
  document.body.removeAttribute("data-clickly-mode");
1166
3416
  };
1167
3417
  }, []);
1168
- useEffect(() => {
3418
+ useEffect7(() => {
1169
3419
  if (!mount || !className) return;
1170
3420
  mount.shadow.host.className = className;
1171
3421
  return () => {
@@ -1174,11 +3424,15 @@ function Clickly({ className } = {}) {
1174
3424
  }, [mount, className]);
1175
3425
  if (!mount) return null;
1176
3426
  return createPortal(
1177
- /* @__PURE__ */ jsx(ClicklyRoot, { engine: mount.engine, host: mount.shadow.host }),
3427
+ /* @__PURE__ */ jsx9(ClicklyRoot, { engine: mount.engine, host: mount.shadow.host }),
1178
3428
  mount.portal
1179
3429
  );
1180
3430
  }
1181
-
1182
- export { Clickly, DEFAULTS as DEFAULT_SETTINGS, annotationsToMarkdown, useAnnotations, useAnnotationsList, useSettings };
1183
- //# sourceMappingURL=index.js.map
1184
- //# sourceMappingURL=index.js.map
3431
+ export {
3432
+ Clickly,
3433
+ DEFAULTS as DEFAULT_SETTINGS,
3434
+ annotationsToMarkdown,
3435
+ useAnnotations,
3436
+ useAnnotationsList,
3437
+ useSettings
3438
+ };