@useclickly/react 1.0.0 → 1.0.2

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 (48) hide show
  1. package/dist/Clickly.d.ts +25 -0
  2. package/dist/Clickly.d.ts.map +1 -0
  3. package/dist/hooks/useDraggable.d.ts +28 -0
  4. package/dist/hooks/useDraggable.d.ts.map +1 -0
  5. package/dist/hooks/usePersistedState.d.ts +6 -0
  6. package/dist/hooks/usePersistedState.d.ts.map +1 -0
  7. package/dist/index.cjs +2667 -386
  8. package/dist/index.d.ts +12 -69
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +2597 -342
  11. package/dist/internal/AnnotationList.d.ts +11 -0
  12. package/dist/internal/AnnotationList.d.ts.map +1 -0
  13. package/dist/internal/AnnotationPins.d.ts +2 -0
  14. package/dist/internal/AnnotationPins.d.ts.map +1 -0
  15. package/dist/internal/AnnotationPopup.d.ts +5 -0
  16. package/dist/internal/AnnotationPopup.d.ts.map +1 -0
  17. package/dist/internal/ClicklyRoot.d.ts +15 -0
  18. package/dist/internal/ClicklyRoot.d.ts.map +1 -0
  19. package/dist/internal/CollapsedFAB.d.ts +10 -0
  20. package/dist/internal/CollapsedFAB.d.ts.map +1 -0
  21. package/dist/internal/SettingsPopover.d.ts +11 -0
  22. package/dist/internal/SettingsPopover.d.ts.map +1 -0
  23. package/dist/internal/Toolbar.d.ts +8 -0
  24. package/dist/internal/Toolbar.d.ts.map +1 -0
  25. package/dist/internal/globalStyles.d.ts +13 -0
  26. package/dist/internal/globalStyles.d.ts.map +1 -0
  27. package/dist/internal/icons.d.ts +12 -0
  28. package/dist/internal/icons.d.ts.map +1 -0
  29. package/dist/internal/styles.d.ts +7 -0
  30. package/dist/internal/styles.d.ts.map +1 -0
  31. package/dist/output/markdown.d.ts +5 -0
  32. package/dist/output/markdown.d.ts.map +1 -0
  33. package/dist/output/markdown.test.d.ts +2 -0
  34. package/dist/output/markdown.test.d.ts.map +1 -0
  35. package/dist/state/annotations.d.ts +21 -0
  36. package/dist/state/annotations.d.ts.map +1 -0
  37. package/dist/state/annotations.test.d.ts +2 -0
  38. package/dist/state/annotations.test.d.ts.map +1 -0
  39. package/dist/state/settings.d.ts +14 -0
  40. package/dist/state/settings.d.ts.map +1 -0
  41. package/dist/state/settings.test.d.ts +2 -0
  42. package/dist/state/settings.test.d.ts.map +1 -0
  43. package/dist/state/useEngineState.d.ts +7 -0
  44. package/dist/state/useEngineState.d.ts.map +1 -0
  45. package/dist/test/setup.d.ts +7 -0
  46. package/dist/test/setup.d.ts.map +1 -0
  47. package/package.json +12 -12
  48. 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");
@@ -161,18 +1294,24 @@ function formatOne(a, index, detail) {
161
1294
  }
162
1295
  if (detail === "detailed" || detail === "forensic") {
163
1296
  if (a.reactComponents) lines.push(`**React:** ${a.reactComponents}`);
164
- if (a.nearbyText) lines.push(`**Nearby text:** ${truncate(a.nearbyText, 120)}`);
1297
+ if (a.nearbyText) lines.push(`**Nearby text:** ${truncate2(a.nearbyText, 120)}`);
165
1298
  }
166
1299
  if (detail === "forensic" && a.computedStyles) {
167
- lines.push("**Computed styles:**\n```\n" + a.computedStyles + "\n```");
1300
+ lines.push("**Computed styles:**\n```css\n" + a.computedStyles + "\n```");
168
1301
  }
169
1302
  lines.push(`**Feedback:** ${a.comment}`);
1303
+ if (a.suggestedCss) {
1304
+ lines.push("**Suggested CSS:**\n```css\n" + a.suggestedCss + "\n```");
1305
+ }
170
1306
  if (a.severity) lines.push(`**Severity:** ${a.severity}`);
171
1307
  return lines.join("\n");
172
1308
  }
173
- function truncate(s, n) {
1309
+ function truncate2(s, n) {
174
1310
  return s.length <= n ? s : s.slice(0, n - 1) + "\u2026";
175
1311
  }
1312
+
1313
+ // packages/react/src/internal/icons.tsx
1314
+ import { jsx, jsxs } from "react/jsx-runtime";
176
1315
  function Icon({
177
1316
  children,
178
1317
  size = 16
@@ -199,7 +1338,6 @@ var IconLayers = () => /* @__PURE__ */ jsxs(Icon, { children: [
199
1338
  /* @__PURE__ */ jsx("path", { d: "M3 12l9 5 9-5" }),
200
1339
  /* @__PURE__ */ jsx("path", { d: "M3 17l9 5 9-5" })
201
1340
  ] });
202
- var IconSquare = () => /* @__PURE__ */ jsx(Icon, { children: /* @__PURE__ */ jsx("rect", { x: "4", y: "4", width: "16", height: "16", rx: "2", strokeDasharray: "3 3" }) });
203
1341
  var IconCopy = () => /* @__PURE__ */ jsxs(Icon, { children: [
204
1342
  /* @__PURE__ */ jsx("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2" }),
205
1343
  /* @__PURE__ */ jsx("path", { d: "M5 15V5a2 2 0 0 1 2-2h10" })
@@ -220,109 +1358,220 @@ var IconGrip = () => /* @__PURE__ */ jsxs(Icon, { children: [
220
1358
  var IconClose = () => /* @__PURE__ */ jsx(Icon, { children: /* @__PURE__ */ jsx("path", { d: "M18 6L6 18M6 6l12 12" }) });
221
1359
  var IconCheck = () => /* @__PURE__ */ jsx(Icon, { children: /* @__PURE__ */ jsx("path", { d: "M5 12l5 5L20 7" }) });
222
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";
223
1365
  function SettingsPopover({ anchor, width, onClose }) {
224
- const ref = useRef(null);
1366
+ const ref = useRef2(null);
225
1367
  const s = useSettings();
226
- useEffect(() => {
1368
+ useEffect2(() => {
227
1369
  const onDown = (e) => {
228
- if (ref.current && !ref.current.contains(e.target)) onClose();
1370
+ if (ref.current && e.composedPath().includes(ref.current)) return;
1371
+ onClose();
229
1372
  };
230
1373
  window.addEventListener("pointerdown", onDown, true);
231
1374
  return () => window.removeEventListener("pointerdown", onDown, true);
232
1375
  }, [onClose]);
233
- const top = Math.max(8, anchor.y - 200);
234
- const left = Math.min(window.innerWidth - 268, anchor.x + width - 260);
235
- return /* @__PURE__ */ jsxs("div", { ref, className: "clickly-popover", style: { left, top }, children: [
236
- /* @__PURE__ */ jsx("h4", { children: "Settings" }),
237
- /* @__PURE__ */ jsxs("div", { className: "field", children: [
238
- /* @__PURE__ */ jsx("label", { htmlFor: "clickly-detail", children: "Output detail" }),
239
- /* @__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(
240
1390
  "select",
241
1391
  {
242
1392
  id: "clickly-detail",
1393
+ className: "settings-select",
243
1394
  value: s.outputDetail,
244
1395
  onChange: (e) => s.set({ outputDetail: e.target.value }),
245
1396
  children: [
246
- /* @__PURE__ */ jsx("option", { value: "compact", children: "Compact" }),
247
- /* @__PURE__ */ jsx("option", { value: "standard", children: "Standard" }),
248
- /* @__PURE__ */ jsx("option", { value: "detailed", children: "Detailed" }),
249
- /* @__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" })
250
1401
  ]
251
1402
  }
252
1403
  )
253
1404
  ] }),
254
- /* @__PURE__ */ jsxs("div", { className: "field", children: [
255
- /* @__PURE__ */ jsx("label", { htmlFor: "clickly-copy", children: "Copy on add" }),
256
- /* @__PURE__ */ jsx(
257
- "input",
258
- {
259
- id: "clickly-copy",
260
- type: "checkbox",
261
- checked: s.copyOnAdd,
262
- onChange: (e) => s.set({ copyOnAdd: e.target.checked })
263
- }
264
- )
265
- ] }),
266
- /* @__PURE__ */ jsxs("div", { className: "field", children: [
267
- /* @__PURE__ */ jsx("label", { htmlFor: "clickly-react", children: "React components" }),
268
- /* @__PURE__ */ jsx(
269
- "input",
270
- {
271
- id: "clickly-react",
272
- type: "checkbox",
273
- checked: s.showReactComponents,
274
- onChange: (e) => s.set({ showReactComponents: e.target.checked })
275
- }
276
- )
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
+ ] })
277
1441
  ] }),
278
- /* @__PURE__ */ jsxs("div", { className: "field", children: [
279
- /* @__PURE__ */ jsx("label", { htmlFor: "clickly-color", children: "Marker color" }),
280
- /* @__PURE__ */ jsx(
281
- "input",
282
- {
283
- id: "clickly-color",
284
- type: "color",
285
- value: s.markerColor,
286
- onChange: (e) => s.set({ markerColor: e.target.value })
287
- }
288
- )
289
- ] })
290
- ] });
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
+ ] });
291
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";
292
1466
  function AnnotationList({ anchor, width, onClose }) {
293
1467
  const items = useAnnotationsList();
294
1468
  const remove = useAnnotations((s) => s.remove);
295
- const ref = useRef(null);
296
- useEffect(() => {
1469
+ const outputDetail = useSettings((s) => s.outputDetail);
1470
+ const ref = useRef3(null);
1471
+ useEffect3(() => {
297
1472
  const onDown = (e) => {
298
- if (ref.current && !ref.current.contains(e.target)) onClose();
1473
+ if (ref.current && e.composedPath().includes(ref.current)) return;
1474
+ onClose();
299
1475
  };
300
1476
  window.addEventListener("pointerdown", onDown, true);
301
1477
  return () => window.removeEventListener("pointerdown", onDown, true);
302
1478
  }, [onClose]);
303
- const top = Math.max(8, anchor.y - 8 - Math.min(window.innerHeight * 0.5, items.length * 60 + 40));
304
- const left = Math.min(window.innerWidth - 328, anchor.x);
305
- 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: [
306
- /* @__PURE__ */ jsxs("span", { className: "num", children: [
307
- "#",
308
- 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 })
309
1486
  ] }),
310
- /* @__PURE__ */ jsxs("div", { className: "body", children: [
311
- /* @__PURE__ */ jsx("div", { className: "selector", children: a.elementPath }),
312
- /* @__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
+ ] })
313
1542
  ] }),
314
- /* @__PURE__ */ jsx("button", { className: "remove", onClick: () => remove(a.id), title: "Remove", children: /* @__PURE__ */ jsx(IconClose, {}) })
315
- ] }, 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
+ ] });
316
1566
  }
317
- var TOOLBAR_SIZE = { width: 360, height: 40 };
318
1567
  function Toolbar({ engine, onCollapse }) {
319
1568
  const state = useEngineState(engine);
320
1569
  const annotations = useAnnotationsList();
321
1570
  const clearAnnotations = useAnnotations((s) => s.clear);
322
1571
  const outputDetail = useSettings((s) => s.outputDetail);
323
- const [showSettings, setShowSettings] = useState(false);
324
- const [showList, setShowList] = useState(false);
325
- const anchorRef = useRef(null);
1572
+ const [showSettings, setShowSettings] = useState3(false);
1573
+ const [showList, setShowList] = useState3(false);
1574
+ const anchorRef = useRef4(null);
326
1575
  const { position, handleProps } = useDraggable(
327
1576
  {
328
1577
  x: Math.max(8, window.innerWidth - TOOLBAR_SIZE.width - 16),
@@ -344,11 +1593,7 @@ function Toolbar({ engine, onCollapse }) {
344
1593
  } catch {
345
1594
  }
346
1595
  };
347
- const onClear = () => {
348
- if (annotations.length === 0) return;
349
- if (confirm(`Clear ${annotations.length} annotation(s)?`)) clearAnnotations();
350
- };
351
- return /* @__PURE__ */ jsxs(
1596
+ return /* @__PURE__ */ jsxs4(
352
1597
  "div",
353
1598
  {
354
1599
  ref: anchorRef,
@@ -356,134 +1601,112 @@ function Toolbar({ engine, onCollapse }) {
356
1601
  style: { left: position.x, top: position.y, width: TOOLBAR_SIZE.width },
357
1602
  "aria-label": "Clickly toolbar",
358
1603
  children: [
359
- /* @__PURE__ */ jsx(
1604
+ /* @__PURE__ */ jsx4(Tip, { label: "Move", children: /* @__PURE__ */ jsx4(
360
1605
  "div",
361
1606
  {
362
1607
  className: "grip",
363
1608
  ...handleProps,
364
- title: "Drag to move",
365
1609
  role: "separator",
366
1610
  "aria-orientation": "vertical",
367
- children: /* @__PURE__ */ jsx(IconGrip, {})
1611
+ children: /* @__PURE__ */ jsx4(IconGrip, {})
368
1612
  }
369
- ),
370
- /* @__PURE__ */ jsx(
1613
+ ) }),
1614
+ /* @__PURE__ */ jsx4(Tip, { label: "Select", shortcut: "1", children: /* @__PURE__ */ jsx4(
371
1615
  "button",
372
1616
  {
373
1617
  className: `clickly-btn icon-only${currentMode === "single" ? " is-active" : ""}`,
374
1618
  onClick: () => setMode("single"),
375
- title: "Single (1)",
376
1619
  "aria-label": "Single-element selection mode",
377
1620
  "aria-pressed": currentMode === "single",
378
- children: /* @__PURE__ */ jsx(IconCursor, {})
1621
+ children: /* @__PURE__ */ jsx4(IconCursor, {})
379
1622
  }
380
- ),
381
- /* @__PURE__ */ jsx(
1623
+ ) }),
1624
+ /* @__PURE__ */ jsx4(Tip, { label: "Multi-select", shortcut: "2", children: /* @__PURE__ */ jsx4(
382
1625
  "button",
383
1626
  {
384
1627
  className: `clickly-btn icon-only${currentMode === "multi" ? " is-active" : ""}`,
385
1628
  onClick: () => setMode("multi"),
386
- title: "Multi-select (2 / shift-click)",
387
1629
  "aria-label": "Multi-element selection mode",
388
1630
  "aria-pressed": currentMode === "multi",
389
- children: /* @__PURE__ */ jsx(IconLayers, {})
390
- }
391
- ),
392
- /* @__PURE__ */ jsx(
393
- "button",
394
- {
395
- className: `clickly-btn icon-only${currentMode === "area" ? " is-active" : ""}`,
396
- onClick: () => setMode("area"),
397
- title: "Area drag (3)",
398
- "aria-label": "Area drag-selection mode",
399
- "aria-pressed": currentMode === "area",
400
- children: /* @__PURE__ */ jsx(IconSquare, {})
1631
+ children: /* @__PURE__ */ jsx4(IconLayers, {})
401
1632
  }
402
- ),
403
- /* @__PURE__ */ jsx("div", { className: "divider" }),
404
- pinnedCount > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
405
- /* @__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(
406
1637
  "button",
407
1638
  {
408
1639
  className: "clickly-btn primary-pinned",
409
1640
  onClick: () => engine.annotatePinned(),
410
- title: `Annotate the ${pinnedCount} pinned element(s) (Enter)`,
411
1641
  children: [
412
- /* @__PURE__ */ jsx(IconCheck, {}),
413
- "Annotate (",
414
- pinnedCount,
415
- ")"
1642
+ /* @__PURE__ */ jsx4(IconCheck, {}),
1643
+ /* @__PURE__ */ jsx4("span", { children: pinnedCount })
416
1644
  ]
417
1645
  }
418
- ),
419
- /* @__PURE__ */ jsx(
1646
+ ) }),
1647
+ /* @__PURE__ */ jsx4(Tip, { label: "Clear selection", shortcut: "Esc", children: /* @__PURE__ */ jsx4(
420
1648
  "button",
421
1649
  {
422
1650
  className: "clickly-btn icon-only",
423
1651
  onClick: () => engine.clearPinned(),
424
- title: "Clear pinned (Esc)",
425
- children: /* @__PURE__ */ jsx(IconClose, {})
1652
+ children: /* @__PURE__ */ jsx4(IconClose, {})
426
1653
  }
427
- ),
428
- /* @__PURE__ */ jsx("div", { className: "divider" })
1654
+ ) }),
1655
+ /* @__PURE__ */ jsx4("div", { className: "divider" })
429
1656
  ] }),
430
- /* @__PURE__ */ jsxs(
1657
+ /* @__PURE__ */ jsx4(Tip, { label: "Annotations", shortcut: "L", children: /* @__PURE__ */ jsxs4(
431
1658
  "button",
432
1659
  {
433
- className: "clickly-btn",
1660
+ className: "clickly-btn icon-only",
434
1661
  onClick: () => setShowList((v) => !v),
435
- title: "Annotations (L)",
1662
+ "aria-label": "Show annotation list",
436
1663
  children: [
437
- /* @__PURE__ */ jsx(IconList, {}),
438
- 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 })
439
1666
  ]
440
1667
  }
441
- ),
442
- /* @__PURE__ */ jsx(
1668
+ ) }),
1669
+ /* @__PURE__ */ jsx4(Tip, { label: "Copy feedback", shortcut: "C", children: /* @__PURE__ */ jsx4(
443
1670
  "button",
444
1671
  {
445
1672
  className: "clickly-btn icon-only",
446
1673
  onClick: onCopy,
447
- title: "Copy markdown (C)",
448
1674
  "aria-label": "Copy all annotations as markdown",
449
1675
  disabled: annotations.length === 0,
450
- children: /* @__PURE__ */ jsx(IconCopy, {})
1676
+ children: /* @__PURE__ */ jsx4(IconCopy, {})
451
1677
  }
452
- ),
453
- /* @__PURE__ */ jsx(
1678
+ ) }),
1679
+ /* @__PURE__ */ jsx4(Tip, { label: "Clear all", shortcut: "X", children: /* @__PURE__ */ jsx4(
454
1680
  "button",
455
1681
  {
456
1682
  className: "clickly-btn icon-only",
457
- onClick: onClear,
458
- title: "Clear (X)",
1683
+ onClick: () => clearAnnotations(),
459
1684
  "aria-label": "Clear all annotations",
460
1685
  disabled: annotations.length === 0,
461
- children: /* @__PURE__ */ jsx(IconTrash, {})
1686
+ children: /* @__PURE__ */ jsx4(IconTrash, {})
462
1687
  }
463
- ),
464
- /* @__PURE__ */ jsx("div", { className: "divider" }),
465
- /* @__PURE__ */ jsx(
1688
+ ) }),
1689
+ /* @__PURE__ */ jsx4("div", { className: "divider" }),
1690
+ /* @__PURE__ */ jsx4(Tip, { label: "Settings", children: /* @__PURE__ */ jsx4(
466
1691
  "button",
467
1692
  {
468
- className: "clickly-btn icon-only",
1693
+ className: `clickly-btn icon-only${showSettings ? " is-active" : ""}`,
469
1694
  onClick: () => setShowSettings((v) => !v),
470
- title: "Settings",
471
1695
  "aria-label": "Open settings",
472
1696
  "aria-expanded": showSettings,
473
- children: /* @__PURE__ */ jsx(IconSettings, {})
1697
+ children: /* @__PURE__ */ jsx4(IconSettings, {})
474
1698
  }
475
- ),
476
- /* @__PURE__ */ jsx(
1699
+ ) }),
1700
+ /* @__PURE__ */ jsx4(Tip, { label: "Close", shortcut: "Esc", children: /* @__PURE__ */ jsx4(
477
1701
  "button",
478
1702
  {
479
1703
  className: "clickly-btn icon-only",
480
1704
  onClick: onCollapse,
481
- title: "Collapse (Esc)",
482
1705
  "aria-label": "Collapse Clickly toolbar",
483
- children: /* @__PURE__ */ jsx(IconClose, {})
1706
+ children: /* @__PURE__ */ jsx4(IconClose, {})
484
1707
  }
485
- ),
486
- showSettings && /* @__PURE__ */ jsx(
1708
+ ) }),
1709
+ showSettings && /* @__PURE__ */ jsx4(
487
1710
  SettingsPopover,
488
1711
  {
489
1712
  anchor: { x: position.x, y: position.y },
@@ -491,7 +1714,7 @@ function Toolbar({ engine, onCollapse }) {
491
1714
  onClose: () => setShowSettings(false)
492
1715
  }
493
1716
  ),
494
- showList && /* @__PURE__ */ jsx(
1717
+ showList && /* @__PURE__ */ jsx4(
495
1718
  AnnotationList,
496
1719
  {
497
1720
  anchor: { x: position.x, y: position.y },
@@ -503,9 +1726,12 @@ function Toolbar({ engine, onCollapse }) {
503
1726
  }
504
1727
  );
505
1728
  }
1729
+
1730
+ // packages/react/src/internal/CollapsedFAB.tsx
1731
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
506
1732
  function CollapsedFAB({ onExpand }) {
507
1733
  const annotations = useAnnotationsList();
508
- return /* @__PURE__ */ jsxs(
1734
+ return /* @__PURE__ */ jsxs5(
509
1735
  "button",
510
1736
  {
511
1737
  className: "clickly-fab",
@@ -513,27 +1739,75 @@ function CollapsedFAB({ onExpand }) {
513
1739
  title: "Open Clickly (\u2318/Ctrl+Shift+F)",
514
1740
  "aria-label": "Open Clickly toolbar",
515
1741
  children: [
516
- /* @__PURE__ */ jsx(IconCursor, {}),
517
- 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 })
518
1744
  ]
519
1745
  }
520
1746
  );
521
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";
522
1753
  var POPUP_W = 320;
523
- var POPUP_H_EST = 200;
1754
+ var POPUP_H_EST = 240;
524
1755
  function AnnotationPopup({ engine }) {
525
1756
  const state = useEngineState(engine);
526
1757
  const addAnnotation = useAnnotations((s) => s.add);
527
1758
  const copyOnAdd = useSettings((s) => s.copyOnAdd);
528
1759
  const outputDetail = useSettings((s) => s.outputDetail);
529
- const [comment, setComment] = useState("");
530
- const taRef = useRef(null);
531
- 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(() => {
532
1769
  if (state.kind === "annotating") {
533
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
+ }
534
1782
  requestAnimationFrame(() => taRef.current?.focus());
535
1783
  }
536
- }, [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
+ };
537
1811
  const onKey = (e) => {
538
1812
  if (e.key === "Escape") {
539
1813
  e.preventDefault();
@@ -544,14 +1818,16 @@ function AnnotationPopup({ engine }) {
544
1818
  submit();
545
1819
  }
546
1820
  };
547
- const cancel = () => engine.commit();
548
1821
  const submit = () => {
549
1822
  if (state.kind !== "annotating") return;
550
1823
  const sel = engine.resolveSelection();
551
1824
  if (!sel) return;
552
1825
  const elements = sel.kind === "single" ? [sel.element] : sel.elements;
553
1826
  if (elements.length === 0) return;
1827
+ const cssDiff = buildCssDiff(styles, editedStyles);
554
1828
  const sharedComment = comment.trim() || "(no comment)";
1829
+ revertAll(targetElRef.current, editedStyles);
1830
+ setEditedStyles({});
555
1831
  const isMulti = sel.kind !== "single";
556
1832
  const showReact = useSettings.getState().showReactComponents;
557
1833
  for (const el of elements) {
@@ -579,7 +1855,8 @@ function AnnotationPopup({ engine }) {
579
1855
  sourceColumn: md.sourceColumn || void 0,
580
1856
  isMultiSelect: isMulti,
581
1857
  kind: "feedback",
582
- status: "pending"
1858
+ status: "pending",
1859
+ suggestedCss: cssDiff || void 0
583
1860
  };
584
1861
  addAnnotation(annotation);
585
1862
  }
@@ -589,29 +1866,118 @@ function AnnotationPopup({ engine }) {
589
1866
  }
590
1867
  engine.commit();
591
1868
  };
592
- const placement = useMemo(() => {
593
- 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
+ }
594
1875
  const sel = engine.resolveSelection();
595
- if (!sel) return null;
1876
+ if (!sel) {
1877
+ setPlacement(null);
1878
+ return;
1879
+ }
596
1880
  const rect = anchorRect(sel);
597
- if (!rect) return null;
598
- let top = rect.bottom + 8;
599
- let left = rect.left;
600
- if (top + POPUP_H_EST > window.innerHeight) {
601
- top = Math.max(8, rect.top - POPUP_H_EST - 8);
1881
+ if (!rect) {
1882
+ setPlacement(null);
1883
+ return;
602
1884
  }
603
- if (left + POPUP_W > window.innerWidth) {
604
- 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;
605
1896
  }
606
- 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 });
607
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]);
608
1912
  if (state.kind !== "annotating" || !placement) return null;
609
- return /* @__PURE__ */ jsxs("div", { className: "clickly-popup", style: { top: placement.top, left: placement.left }, children: [
610
- /* @__PURE__ */ jsxs("div", { className: "target-info", children: [
611
- /* @__PURE__ */ jsx("span", { children: placement.label }),
612
- /* @__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." })
613
1979
  ] }),
614
- /* @__PURE__ */ jsx(
1980
+ /* @__PURE__ */ jsx6(
615
1981
  "textarea",
616
1982
  {
617
1983
  ref: taRef,
@@ -622,38 +1988,111 @@ function AnnotationPopup({ engine }) {
622
1988
  onKeyDown: onKey
623
1989
  }
624
1990
  ),
625
- /* @__PURE__ */ jsxs("div", { className: "row", children: [
626
- /* @__PURE__ */ jsx("button", { className: "ghost", onClick: cancel, children: "Cancel" }),
627
- /* @__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" })
628
1994
  ] })
629
1995
  ] });
630
1996
  }
631
- function stringifyStyles(styles) {
632
- 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);
633
2017
  if (entries.length === 0) return void 0;
634
2018
  return entries.map(([k, v]) => `${k}: ${v}`).join("; ");
635
2019
  }
636
2020
  function anchorRect(sel) {
637
2021
  if (!sel) return null;
638
2022
  if (sel.kind === "single") return sel.element.getBoundingClientRect();
639
- if (sel.kind === "area") {
640
- return new DOMRect(sel.rect.x, sel.rect.y, sel.rect.width, sel.rect.height);
641
- }
2023
+ if (sel.kind === "area") return new DOMRect(sel.rect.x, sel.rect.y, sel.rect.width, sel.rect.height);
642
2024
  if (sel.elements.length === 0) return null;
643
2025
  return sel.elements[0].getBoundingClientRect();
644
2026
  }
645
- function describeSelection(sel) {
646
- if (sel.kind === "single") {
647
- const m = collectMetadata(sel.element, { detail: "compact" });
648
- 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}"`;
649
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);
650
2084
  if (sel.kind === "area") return `area \xB7 ${sel.elements.length} element(s)`;
2085
+ if (sel.elements.length === 1) return describeElement2(sel.elements[0]);
651
2086
  return `${sel.elements.length} element(s)`;
652
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";
653
2092
  function AnnotationPins() {
654
2093
  const annotations = useAnnotationsList();
655
- const [, setVersion] = useState(0);
656
- useEffect(() => {
2094
+ const [, setVersion] = useState5(0);
2095
+ useEffect5(() => {
657
2096
  let raf = null;
658
2097
  const update = () => {
659
2098
  if (raf !== null) return;
@@ -670,40 +2109,205 @@ function AnnotationPins() {
670
2109
  window.removeEventListener("resize", update);
671
2110
  };
672
2111
  }, []);
673
- 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";
2155
+ }
2156
+ function parseStyles(raw) {
2157
+ if (!raw) return [];
2158
+ return raw.split(";").map((s) => s.trim()).filter(Boolean).map((s) => {
2159
+ const colon = s.indexOf(":");
2160
+ if (colon === -1) return null;
2161
+ return [s.slice(0, colon).trim(), s.slice(colon + 1).trim()];
2162
+ }).filter(Boolean);
674
2163
  }
675
2164
  function Pin({ number, annotation }) {
676
2165
  const remove = useAnnotations((s) => s.remove);
677
- const [hovered, setHovered] = useState(false);
2166
+ const update = useAnnotations((s) => s.update);
2167
+ const [hovered, setHovered] = useState5(false);
2168
+ const [editing, setEditing] = useState5(false);
2169
+ const [draft, setDraft] = useState5(annotation.comment);
2170
+ const [showStyles, setShowStyles] = useState5(false);
2171
+ const taRef = useRef6(null);
2172
+ const editRef = useRef6(null);
2173
+ const styleEntries = parseStyles(annotation.computedStyles);
2174
+ const label = pinLabel(annotation);
2175
+ const headerLabel = annotation.isMultiSelect ? `${label} (multi-select)` : `${label}: "${annotation.elementPath}"`;
2176
+ const openEdit = () => {
2177
+ setDraft(annotation.comment);
2178
+ setEditing(true);
2179
+ setHovered(false);
2180
+ requestAnimationFrame(() => taRef.current?.focus());
2181
+ };
2182
+ const closeEdit = () => {
2183
+ setEditing(false);
2184
+ setShowStyles(false);
2185
+ };
2186
+ const save = () => {
2187
+ const trimmed = draft.trim();
2188
+ if (trimmed) update(annotation.id, { comment: trimmed });
2189
+ closeEdit();
2190
+ };
2191
+ const onKeyDown = (e) => {
2192
+ if (e.key === "Escape") closeEdit();
2193
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) save();
2194
+ };
2195
+ useEffect5(() => {
2196
+ if (!editing) return;
2197
+ const onDown = (e) => {
2198
+ if (editRef.current && e.composedPath().includes(editRef.current)) return;
2199
+ closeEdit();
2200
+ };
2201
+ window.addEventListener("pointerdown", onDown, true);
2202
+ return () => window.removeEventListener("pointerdown", onDown, true);
2203
+ }, [editing]);
2204
+ useEffect5(() => {
2205
+ if (!editing) return;
2206
+ let el = null;
2207
+ if (annotation.elementPath) {
2208
+ try {
2209
+ el = document.querySelector(annotation.elementPath);
2210
+ } catch {
2211
+ }
2212
+ }
2213
+ if (!el) return;
2214
+ const prevOutline = el.style.outline;
2215
+ const prevOffset = el.style.outlineOffset;
2216
+ el.style.outline = "2px solid #10b981";
2217
+ el.style.outlineOffset = "3px";
2218
+ return () => {
2219
+ el.style.outline = prevOutline;
2220
+ el.style.outlineOffset = prevOffset;
2221
+ };
2222
+ }, [editing, annotation.elementPath]);
678
2223
  const pos = resolvePosition(annotation);
679
2224
  if (!pos) return null;
680
- return /* @__PURE__ */ jsxs(
2225
+ return /* @__PURE__ */ jsxs7(
681
2226
  "div",
682
2227
  {
683
2228
  className: "clickly-pin",
684
2229
  style: { left: pos.x, top: pos.y },
685
- onMouseEnter: () => setHovered(true),
2230
+ onMouseEnter: () => {
2231
+ if (!editing) setHovered(true);
2232
+ },
686
2233
  onMouseLeave: () => setHovered(false),
2234
+ onClick: openEdit,
687
2235
  role: "button",
688
2236
  tabIndex: 0,
689
- title: annotation.comment,
2237
+ "aria-label": `Annotation ${number}: ${annotation.comment}`,
690
2238
  children: [
691
- /* @__PURE__ */ jsx("span", { className: "clickly-pin-num", children: number }),
692
- hovered && /* @__PURE__ */ jsxs("div", { className: "clickly-pin-bubble", children: [
693
- /* @__PURE__ */ jsx("div", { className: "clickly-pin-comment", children: annotation.comment }),
694
- /* @__PURE__ */ jsx("div", { className: "clickly-pin-meta", children: annotation.elementPath }),
695
- /* @__PURE__ */ jsx(
696
- "button",
697
- {
698
- className: "clickly-pin-remove",
699
- onClick: (e) => {
700
- e.stopPropagation();
701
- remove(annotation.id);
702
- },
703
- children: "Remove"
704
- }
705
- )
706
- ] })
2239
+ /* @__PURE__ */ jsx7("span", { className: "clickly-pin-num", children: number }),
2240
+ hovered && !editing && /* @__PURE__ */ jsxs7("div", { className: "pin-preview", children: [
2241
+ /* @__PURE__ */ jsxs7("div", { className: "pin-preview-meta", children: [
2242
+ label,
2243
+ ": ",
2244
+ annotation.elementPath
2245
+ ] }),
2246
+ /* @__PURE__ */ jsx7("div", { className: "pin-preview-comment", children: annotation.comment })
2247
+ ] }),
2248
+ editing && /* @__PURE__ */ jsxs7(
2249
+ "div",
2250
+ {
2251
+ ref: editRef,
2252
+ className: "pin-edit",
2253
+ onClick: (e) => e.stopPropagation(),
2254
+ children: [
2255
+ /* @__PURE__ */ jsxs7(
2256
+ "div",
2257
+ {
2258
+ className: "popup-header",
2259
+ role: "button",
2260
+ "aria-expanded": showStyles,
2261
+ onClick: (e) => {
2262
+ e.stopPropagation();
2263
+ setShowStyles((v) => !v);
2264
+ },
2265
+ children: [
2266
+ /* @__PURE__ */ jsx7("span", { className: "popup-chevron", children: showStyles ? "\u25BE" : "\u203A" }),
2267
+ /* @__PURE__ */ jsx7("span", { className: "popup-label", children: headerLabel })
2268
+ ]
2269
+ }
2270
+ ),
2271
+ showStyles && styleEntries.length > 0 && /* @__PURE__ */ jsxs7("div", { className: "popup-styles", children: [
2272
+ styleEntries.map(([k, v]) => /* @__PURE__ */ jsxs7("div", { className: "style-row", children: [
2273
+ /* @__PURE__ */ jsx7("span", { className: "style-key", children: k }),
2274
+ /* @__PURE__ */ jsx7("span", { className: "style-val", children: v })
2275
+ ] }, k)),
2276
+ annotation.suggestedCss && /* @__PURE__ */ jsxs7("div", { className: "style-hint", style: { color: "#7c3aed", borderTopColor: "rgba(124,58,237,0.2)" }, children: [
2277
+ /* @__PURE__ */ jsx7("strong", { children: "Suggested changes:" }),
2278
+ /* @__PURE__ */ jsx7("br", {}),
2279
+ annotation.suggestedCss
2280
+ ] })
2281
+ ] }),
2282
+ /* @__PURE__ */ jsx7(
2283
+ "textarea",
2284
+ {
2285
+ ref: taRef,
2286
+ value: draft,
2287
+ onChange: (e) => setDraft(e.target.value),
2288
+ onKeyDown,
2289
+ placeholder: "Describe the issue\u2026 (\u2318/Ctrl + Enter to save)",
2290
+ "aria-label": "Annotation comment"
2291
+ }
2292
+ ),
2293
+ /* @__PURE__ */ jsxs7("div", { className: "pin-edit-actions", children: [
2294
+ /* @__PURE__ */ jsx7(
2295
+ "button",
2296
+ {
2297
+ className: "pin-edit-delete",
2298
+ onClick: () => remove(annotation.id),
2299
+ "aria-label": "Delete annotation",
2300
+ children: /* @__PURE__ */ jsx7(IconTrash, {})
2301
+ }
2302
+ ),
2303
+ /* @__PURE__ */ jsxs7("div", { className: "row", style: { margin: 0, flex: 1, justifyContent: "flex-end" }, children: [
2304
+ /* @__PURE__ */ jsx7("button", { className: "ghost", onClick: closeEdit, children: "Cancel" }),
2305
+ /* @__PURE__ */ jsx7("button", { className: "primary", onClick: save, children: "Save" })
2306
+ ] })
2307
+ ] })
2308
+ ]
2309
+ }
2310
+ )
707
2311
  ]
708
2312
  }
709
2313
  );
@@ -720,33 +2324,33 @@ function resolvePosition(a) {
720
2324
  }
721
2325
  }
722
2326
  if (a.boundingBox) {
723
- return {
724
- x: a.boundingBox.x + a.boundingBox.width - 11,
725
- y: a.boundingBox.y - 11
726
- };
2327
+ return { x: a.boundingBox.x + a.boundingBox.width - 11, y: a.boundingBox.y - 11 };
727
2328
  }
728
2329
  return null;
729
2330
  }
2331
+
2332
+ // packages/react/src/internal/ClicklyRoot.tsx
2333
+ import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
730
2334
  function ClicklyRoot({
731
2335
  engine,
732
2336
  host
733
2337
  }) {
734
- const [expanded, setExpanded] = useState(false);
2338
+ const [expanded, setExpanded] = useState6(false);
735
2339
  const clearAnnotations = useAnnotations((s) => s.clear);
736
2340
  const outputDetail = useSettings((s) => s.outputDetail);
737
2341
  const markerColor = useSettings((s) => s.markerColor);
738
2342
  const engineState = useEngineState(engine);
739
- useEffect(() => {
2343
+ useEffect6(() => {
740
2344
  host.style.setProperty("--clickly-hover", markerColor);
741
2345
  }, [host, markerColor]);
742
- useEffect(() => {
2346
+ useEffect6(() => {
743
2347
  if (expanded) {
744
2348
  if (engine.getSnapshot().kind === "idle") engine.activate("single");
745
2349
  } else {
746
2350
  if (engine.getSnapshot().kind !== "idle") engine.deactivate();
747
2351
  }
748
2352
  }, [expanded, engine]);
749
- useEffect(() => {
2353
+ useEffect6(() => {
750
2354
  const active = engineState.kind !== "idle";
751
2355
  if (active) document.body.setAttribute("data-clickly-active", "");
752
2356
  else document.body.removeAttribute("data-clickly-active");
@@ -764,7 +2368,7 @@ function ClicklyRoot({
764
2368
  document.body.removeAttribute("data-clickly-annotating");
765
2369
  };
766
2370
  }, [engineState]);
767
- useEffect(() => {
2371
+ useEffect6(() => {
768
2372
  const onKey = (e) => {
769
2373
  const tag = e.target?.tagName;
770
2374
  if (tag === "INPUT" || tag === "TEXTAREA") return;
@@ -807,16 +2411,16 @@ function ClicklyRoot({
807
2411
  window.addEventListener("keydown", onKey);
808
2412
  return () => window.removeEventListener("keydown", onKey);
809
2413
  }, [clearAnnotations, outputDetail, expanded, engine]);
810
- return /* @__PURE__ */ jsxs("div", { className: "clickly-ui", children: [
811
- /* @__PURE__ */ jsx(AnnotationPins, {}),
812
- expanded ? /* @__PURE__ */ jsxs(Fragment, { children: [
813
- /* @__PURE__ */ jsx(Toolbar, { engine, onCollapse: () => setExpanded(false) }),
814
- /* @__PURE__ */ jsx(AnnotationPopup, { engine })
815
- ] }) : /* @__PURE__ */ jsx(CollapsedFAB, { onExpand: () => setExpanded(true) })
2414
+ return /* @__PURE__ */ jsxs8("div", { className: "clickly-ui", children: [
2415
+ /* @__PURE__ */ jsx8(AnnotationPins, {}),
2416
+ expanded ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
2417
+ /* @__PURE__ */ jsx8(Toolbar, { engine, onCollapse: () => setExpanded(false) }),
2418
+ /* @__PURE__ */ jsx8(AnnotationPopup, { engine })
2419
+ ] }) : /* @__PURE__ */ jsx8(CollapsedFAB, { onExpand: () => setExpanded(true) })
816
2420
  ] });
817
2421
  }
818
2422
 
819
- // src/internal/styles.ts
2423
+ // packages/react/src/internal/styles.ts
820
2424
  var REACT_UI_CSS = `
821
2425
  .clickly-ui, .clickly-ui * { box-sizing: border-box; }
822
2426
 
@@ -874,72 +2478,151 @@ var REACT_UI_CSS = `
874
2478
  position: fixed;
875
2479
  display: flex;
876
2480
  align-items: center;
877
- gap: 4px;
878
- padding: 4px;
879
- background: rgba(15, 23, 42, 0.94);
2481
+ gap: 2px;
2482
+ padding: 6px;
2483
+ height: 44px;
2484
+ background: rgba(9, 14, 28, 0.97);
880
2485
  color: #f8fafc;
881
- border-radius: 12px;
2486
+ border-radius: 16px;
882
2487
  font: 13px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
883
- box-shadow: 0 8px 24px rgba(0,0,0,0.3), 0 0 0 1px rgba(255,255,255,0.06) inset;
2488
+ box-shadow:
2489
+ 0 16px 40px rgba(0,0,0,0.45),
2490
+ 0 0 0 1px rgba(255,255,255,0.08) inset,
2491
+ 0 1px 0 rgba(255,255,255,0.06) inset;
884
2492
  pointer-events: auto;
885
2493
  user-select: none;
886
2494
  z-index: 1;
887
- animation: clickly-fade-in 120ms ease-out;
2495
+ animation: clickly-fade-in 150ms cubic-bezier(0.16, 1, 0.3, 1);
888
2496
  }
889
2497
 
890
2498
  @keyframes clickly-fade-in {
891
- from { opacity: 0; transform: translateY(4px); }
892
- to { opacity: 1; transform: translateY(0); }
2499
+ from { opacity: 0; transform: translateY(6px) scale(0.97); }
2500
+ to { opacity: 1; transform: translateY(0) scale(1); }
893
2501
  }
894
2502
 
895
2503
  .clickly-toolbar .grip {
896
2504
  display: grid;
897
2505
  place-items: center;
898
- width: 24px;
899
- height: 28px;
900
- color: #94a3b8;
2506
+ width: 22px;
2507
+ height: 32px;
2508
+ color: #475569;
901
2509
  touch-action: none;
2510
+ cursor: grab;
2511
+ border-radius: 6px;
2512
+ transition: color 120ms ease;
902
2513
  }
2514
+ .clickly-toolbar .grip:hover { color: #94a3b8; }
2515
+ .clickly-toolbar .grip:active { cursor: grabbing; }
903
2516
 
904
2517
  .clickly-toolbar .divider {
905
2518
  width: 1px;
906
- height: 18px;
907
- background: rgba(255,255,255,0.12);
908
- margin: 0 2px;
2519
+ height: 20px;
2520
+ background: rgba(255,255,255,0.08);
2521
+ margin: 0 3px;
2522
+ flex-shrink: 0;
909
2523
  }
910
2524
 
911
2525
  .clickly-btn {
912
2526
  display: inline-flex;
913
2527
  align-items: center;
2528
+ justify-content: center;
914
2529
  gap: 4px;
915
- height: 28px;
2530
+ height: 32px;
916
2531
  padding: 0 8px;
917
2532
  background: transparent;
918
2533
  border: none;
919
- border-radius: 8px;
920
- color: #cbd5e1;
2534
+ border-radius: 10px;
2535
+ color: #94a3b8;
921
2536
  cursor: pointer;
922
2537
  font: inherit;
2538
+ transition: background 100ms ease, color 100ms ease;
2539
+ position: relative;
2540
+ }
2541
+ .clickly-btn:hover { background: rgba(255,255,255,0.09); color: #e2e8f0; }
2542
+ .clickly-btn:active { background: rgba(255,255,255,0.15); color: #fff; transform: scale(0.96); }
2543
+ .clickly-btn.is-active {
2544
+ background: #0ea5e9;
2545
+ color: #fff;
2546
+ box-shadow: 0 0 0 1px rgba(14,165,233,0.4), 0 2px 8px rgba(14,165,233,0.3);
923
2547
  }
924
- .clickly-btn:hover { background: rgba(255,255,255,0.08); color: #fff; }
925
- .clickly-btn:active { background: rgba(255,255,255,0.14); }
926
- .clickly-btn.is-active { background: #0ea5e9; color: #fff; }
927
- .clickly-btn[disabled] { opacity: 0.4; cursor: not-allowed; }
2548
+ .clickly-btn.is-active:hover { background: #0284c7; }
2549
+ .clickly-btn[disabled] { opacity: 0.28; cursor: not-allowed; pointer-events: none; }
928
2550
 
929
2551
  .clickly-btn.icon-only {
930
- width: 28px;
2552
+ width: 32px;
931
2553
  padding: 0;
932
- justify-content: center;
933
2554
  }
934
2555
 
935
2556
  .clickly-btn.primary-pinned {
936
2557
  background: #10b981;
937
2558
  color: #fff;
2559
+ font-weight: 600;
2560
+ font-size: 12px;
2561
+ padding: 0 10px;
2562
+ gap: 5px;
2563
+ box-shadow: 0 0 0 1px rgba(16,185,129,0.4), 0 2px 8px rgba(16,185,129,0.25);
2564
+ }
2565
+ .clickly-btn.primary-pinned:hover { background: #059669; }
2566
+
2567
+ /* \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 */
2568
+
2569
+ .clickly-tip {
2570
+ position: relative;
2571
+ display: inline-flex;
2572
+ align-items: center;
2573
+ justify-content: center;
2574
+ /* Ensures the absolute-positioned counter badge clips correctly */
2575
+ isolation: isolate;
2576
+ }
2577
+
2578
+ .clickly-tip .tip-bubble {
2579
+ position: absolute;
2580
+ bottom: calc(100% + 10px);
2581
+ left: 50%;
2582
+ transform: translateX(-50%);
2583
+ background: rgba(15, 23, 42, 0.98);
2584
+ color: #f1f5f9;
2585
+ font-size: 12px;
938
2586
  font-weight: 500;
2587
+ white-space: nowrap;
2588
+ padding: 5px 10px;
2589
+ border-radius: 8px;
2590
+ pointer-events: none;
2591
+ opacity: 0;
2592
+ transition: opacity 80ms ease;
2593
+ transition-delay: 200ms;
2594
+ box-shadow: 0 4px 16px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.07) inset;
2595
+ display: flex;
2596
+ align-items: center;
2597
+ gap: 6px;
2598
+ z-index: 10;
939
2599
  }
940
- .clickly-btn.primary-pinned:hover {
941
- background: #059669;
942
- color: #fff;
2600
+
2601
+ /* Arrow */
2602
+ .clickly-tip .tip-bubble::after {
2603
+ content: "";
2604
+ position: absolute;
2605
+ top: 100%;
2606
+ left: 50%;
2607
+ transform: translateX(-50%);
2608
+ border: 5px solid transparent;
2609
+ border-top-color: rgba(15, 23, 42, 0.98);
2610
+ }
2611
+
2612
+ .clickly-tip:hover .tip-bubble { opacity: 1; }
2613
+
2614
+ .clickly-tip .tip-bubble kbd {
2615
+ display: inline-flex;
2616
+ align-items: center;
2617
+ justify-content: center;
2618
+ min-width: 18px;
2619
+ height: 18px;
2620
+ padding: 0 4px;
2621
+ background: rgba(255,255,255,0.12);
2622
+ border: 1px solid rgba(255,255,255,0.10);
2623
+ border-radius: 4px;
2624
+ font: 11px/1 ui-monospace, "SF Mono", Menlo, monospace;
2625
+ color: #94a3b8;
943
2626
  }
944
2627
 
945
2628
  /* \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 */
@@ -956,20 +2639,181 @@ var REACT_UI_CSS = `
956
2639
  animation: clickly-fade-in 120ms ease-out;
957
2640
  }
958
2641
 
959
- .clickly-popup { width: 320px; padding: 12px; }
2642
+ .clickly-popup {
2643
+ width: 320px;
2644
+ padding: 12px;
2645
+ max-height: calc(100vh - 80px);
2646
+ overflow: hidden;
2647
+ display: flex;
2648
+ flex-direction: column;
2649
+ }
960
2650
 
961
- .clickly-popup .target-info {
2651
+ /* \u2500\u2500\u2500 Shared inner styles \u2014 apply to both popup and pin-edit \u2500\u2500\u2500 */
2652
+
2653
+ .clickly-popup .popup-header,
2654
+ .pin-edit .popup-header {
962
2655
  display: flex;
963
2656
  align-items: center;
964
- justify-content: space-between;
965
- gap: 8px;
2657
+ gap: 5px;
966
2658
  margin-bottom: 8px;
2659
+ padding: 4px 2px;
2660
+ border-radius: 4px;
2661
+ cursor: pointer;
2662
+ user-select: none;
967
2663
  color: #475569;
968
2664
  font-size: 11px;
969
2665
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
2666
+ transition: background 80ms ease;
2667
+ }
2668
+ .clickly-popup .popup-header:hover,
2669
+ .pin-edit .popup-header:hover { background: #f1f5f9; }
2670
+
2671
+ .clickly-popup .popup-chevron,
2672
+ .pin-edit .popup-chevron {
2673
+ font-size: 12px;
2674
+ color: #94a3b8;
2675
+ flex-shrink: 0;
2676
+ width: 12px;
2677
+ text-align: center;
2678
+ }
2679
+
2680
+ .clickly-popup .popup-label,
2681
+ .pin-edit .popup-label {
2682
+ flex: 1;
2683
+ overflow: hidden;
2684
+ white-space: nowrap;
2685
+ text-overflow: ellipsis;
2686
+ }
2687
+
2688
+ /* Edit-mode toggle button \u2014 top-right of CSS panel header */
2689
+ .clickly-popup .popup-edit-toggle {
2690
+ flex-shrink: 0;
2691
+ display: grid;
2692
+ place-items: center;
2693
+ width: 20px;
2694
+ height: 20px;
2695
+ background: transparent;
2696
+ border: 1px solid #e2e8f0;
2697
+ border-radius: 5px;
2698
+ color: #94a3b8;
2699
+ cursor: pointer;
2700
+ padding: 0;
2701
+ margin-left: auto;
2702
+ transition: background 100ms ease, color 100ms ease, border-color 100ms ease;
2703
+ }
2704
+ .clickly-popup .popup-edit-toggle:hover {
2705
+ background: #f1f5f9;
2706
+ color: #475569;
2707
+ border-color: #cbd5e1;
2708
+ }
2709
+ .clickly-popup .popup-edit-toggle.is-editing {
2710
+ background: #0ea5e9;
2711
+ color: #fff;
2712
+ border-color: #0ea5e9;
2713
+ }
2714
+ .clickly-popup .popup-edit-toggle.is-editing:hover {
2715
+ background: #0284c7;
2716
+ }
2717
+
2718
+ /* Computed-styles panel */
2719
+ .clickly-popup .popup-styles,
2720
+ .pin-edit .popup-styles {
2721
+ margin-bottom: 8px;
2722
+ padding: 8px;
2723
+ background: #f8fafc;
2724
+ border: 1px solid #e2e8f0;
2725
+ border-radius: 6px;
2726
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
2727
+ font-size: 11px;
2728
+ max-height: 160px;
2729
+ overflow-y: auto;
2730
+ overscroll-behavior: contain;
2731
+ }
2732
+
2733
+ .clickly-popup .style-row,
2734
+ .pin-edit .style-row {
2735
+ display: flex;
2736
+ align-items: center;
2737
+ gap: 6px;
2738
+ line-height: 1.7;
2739
+ border-radius: 4px;
2740
+ padding: 0 2px;
2741
+ transition: background 80ms ease;
2742
+ }
2743
+
2744
+ .clickly-popup .style-row--changed,
2745
+ .pin-edit .style-row--changed {
2746
+ background: rgba(245, 158, 11, 0.10);
2747
+ }
2748
+
2749
+ .clickly-popup .style-key,
2750
+ .pin-edit .style-key {
2751
+ color: #7c3aed;
2752
+ flex-shrink: 0;
2753
+ min-width: 90px;
2754
+ font-size: 11px;
2755
+ }
2756
+
2757
+ .clickly-popup .style-val,
2758
+ .pin-edit .style-val {
2759
+ color: #0f172a;
2760
+ font-size: 11px;
2761
+ word-break: break-all;
2762
+ flex: 1;
2763
+ }
2764
+
2765
+ /* Editable value input \u2014 live CSS preview */
2766
+ .clickly-popup .style-val-input {
2767
+ flex: 1;
2768
+ min-width: 0;
2769
+ background: transparent;
2770
+ border: none;
2771
+ border-bottom: 1px solid transparent;
2772
+ color: #0f172a;
2773
+ font: 11px/1.7 ui-monospace, "SF Mono", Menlo, monospace;
2774
+ padding: 0;
2775
+ outline: none;
2776
+ transition: border-color 100ms ease;
2777
+ word-break: break-all;
2778
+ }
2779
+ .clickly-popup .style-val-input:focus {
2780
+ border-bottom-color: #0ea5e9;
2781
+ background: rgba(14, 165, 233, 0.05);
2782
+ border-radius: 2px 2px 0 0;
2783
+ }
2784
+
2785
+ /* Revert button shown only on changed rows */
2786
+ .clickly-popup .style-revert-btn {
2787
+ flex-shrink: 0;
2788
+ background: transparent;
2789
+ border: none;
2790
+ color: #94a3b8;
2791
+ font-size: 13px;
2792
+ cursor: pointer;
2793
+ padding: 0 2px;
2794
+ line-height: 1;
2795
+ border-radius: 3px;
2796
+ transition: color 80ms ease, background 80ms ease;
2797
+ }
2798
+ .clickly-popup .style-revert-btn:hover {
2799
+ color: #f59e0b;
2800
+ background: rgba(245,158,11,0.12);
970
2801
  }
971
2802
 
972
- .clickly-popup textarea {
2803
+ /* Hint text at bottom of CSS panel */
2804
+ .clickly-popup .style-hint,
2805
+ .pin-edit .style-hint {
2806
+ margin-top: 6px;
2807
+ padding-top: 6px;
2808
+ border-top: 1px solid #e2e8f0;
2809
+ font-size: 10.5px;
2810
+ color: #94a3b8;
2811
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
2812
+ line-height: 1.4;
2813
+ }
2814
+
2815
+ .clickly-popup textarea,
2816
+ .pin-edit textarea {
973
2817
  width: 100%;
974
2818
  min-height: 64px;
975
2819
  max-height: 160px;
@@ -980,33 +2824,40 @@ var REACT_UI_CSS = `
980
2824
  font: inherit;
981
2825
  color: inherit;
982
2826
  }
983
- .clickly-popup textarea:focus {
2827
+ .clickly-popup textarea:focus,
2828
+ .pin-edit textarea:focus {
984
2829
  outline: none;
985
2830
  border-color: #0ea5e9;
986
2831
  box-shadow: 0 0 0 3px rgba(14,165,233,0.18);
987
2832
  }
988
2833
 
989
- .clickly-popup .row {
2834
+ .clickly-popup .row,
2835
+ .pin-edit .row {
990
2836
  display: flex;
991
2837
  justify-content: flex-end;
992
2838
  gap: 6px;
993
2839
  margin-top: 10px;
994
2840
  }
995
2841
 
996
- .clickly-popup .row .ghost {
2842
+ .clickly-popup .row .ghost,
2843
+ .pin-edit .row .ghost {
997
2844
  background: transparent;
998
2845
  color: #475569;
999
2846
  border: 1px solid #e2e8f0;
1000
2847
  }
1001
- .clickly-popup .row .ghost:hover { background: #f8fafc; }
2848
+ .clickly-popup .row .ghost:hover,
2849
+ .pin-edit .row .ghost:hover { background: #f8fafc; }
1002
2850
 
1003
- .clickly-popup .row .primary {
2851
+ .clickly-popup .row .primary,
2852
+ .pin-edit .row .primary {
1004
2853
  background: #0ea5e9;
1005
2854
  color: #fff;
1006
2855
  border: none;
1007
2856
  }
1008
- .clickly-popup .row .primary:hover { background: #0284c7; }
1009
- .clickly-popup .row button {
2857
+ .clickly-popup .row .primary:hover,
2858
+ .pin-edit .row .primary:hover { background: #0284c7; }
2859
+ .clickly-popup .row button,
2860
+ .pin-edit .row button {
1010
2861
  height: 28px;
1011
2862
  padding: 0 12px;
1012
2863
  border-radius: 6px;
@@ -1014,121 +2865,518 @@ var REACT_UI_CSS = `
1014
2865
  cursor: pointer;
1015
2866
  }
1016
2867
 
2868
+ /* Old popover kept for any legacy use */
1017
2869
  .clickly-popover { width: 260px; padding: 12px; }
1018
- .clickly-popover h4 { margin: 0 0 8px; font-size: 12px; color: #475569; text-transform: uppercase; letter-spacing: 0.05em; }
1019
- .clickly-popover .field { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 0; font-size: 13px; }
1020
- .clickly-popover select { font: inherit; padding: 4px 6px; border-radius: 4px; border: 1px solid #e2e8f0; }
1021
- .clickly-popover input[type="color"] { width: 28px; height: 24px; padding: 0; border: 1px solid #e2e8f0; border-radius: 4px; background: none; }
2870
+
2871
+ /* \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 */
2872
+
2873
+ .clickly-settings {
2874
+ position: fixed;
2875
+ width: 284px;
2876
+ background: #fff;
2877
+ border-radius: 14px;
2878
+ box-shadow: 0 16px 48px rgba(2,6,23,0.20), 0 0 0 1px rgba(15,23,42,0.07);
2879
+ font: 13px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
2880
+ color: #0f172a;
2881
+ pointer-events: auto;
2882
+ z-index: 2;
2883
+ animation: clickly-fade-in 150ms cubic-bezier(0.16, 1, 0.3, 1);
2884
+ overflow: hidden;
2885
+ }
2886
+
2887
+ .settings-header {
2888
+ display: flex;
2889
+ align-items: center;
2890
+ justify-content: space-between;
2891
+ padding: 14px 14px 12px;
2892
+ border-bottom: 1px solid #f1f5f9;
2893
+ }
2894
+
2895
+ .settings-title {
2896
+ font-size: 13px;
2897
+ font-weight: 600;
2898
+ color: #0f172a;
2899
+ letter-spacing: -0.01em;
2900
+ }
2901
+
2902
+ .settings-close {
2903
+ display: grid;
2904
+ place-items: center;
2905
+ width: 24px;
2906
+ height: 24px;
2907
+ background: #f1f5f9;
2908
+ border: none;
2909
+ border-radius: 6px;
2910
+ color: #64748b;
2911
+ cursor: pointer;
2912
+ padding: 0;
2913
+ transition: background 100ms, color 100ms;
2914
+ }
2915
+ .settings-close:hover { background: #e2e8f0; color: #0f172a; }
2916
+ .settings-close svg { width: 13px; height: 13px; }
2917
+
2918
+ .settings-section { padding: 10px 14px; }
2919
+
2920
+ .settings-divider {
2921
+ height: 1px;
2922
+ background: #f1f5f9;
2923
+ margin: 0;
2924
+ }
2925
+
2926
+ .settings-label {
2927
+ display: flex;
2928
+ flex-direction: column;
2929
+ gap: 2px;
2930
+ font-size: 13px;
2931
+ font-weight: 500;
2932
+ color: #1e293b;
2933
+ margin-bottom: 8px;
2934
+ }
2935
+
2936
+ .settings-hint {
2937
+ font-size: 11.5px;
2938
+ font-weight: 400;
2939
+ color: #94a3b8;
2940
+ }
2941
+
2942
+ .settings-select {
2943
+ width: 100%;
2944
+ padding: 7px 10px;
2945
+ background: #f8fafc;
2946
+ border: 1px solid #e2e8f0;
2947
+ border-radius: 8px;
2948
+ font: inherit;
2949
+ font-size: 13px;
2950
+ color: #1e293b;
2951
+ cursor: pointer;
2952
+ appearance: none;
2953
+ 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");
2954
+ background-repeat: no-repeat;
2955
+ background-position: right 10px center;
2956
+ padding-right: 32px;
2957
+ transition: border-color 120ms, box-shadow 120ms;
2958
+ }
2959
+ .settings-select:focus {
2960
+ outline: none;
2961
+ border-color: #0ea5e9;
2962
+ box-shadow: 0 0 0 3px rgba(14,165,233,0.15);
2963
+ background-color: #fff;
2964
+ }
2965
+
2966
+ .settings-row {
2967
+ display: flex;
2968
+ align-items: center;
2969
+ justify-content: space-between;
2970
+ gap: 12px;
2971
+ padding: 6px 0;
2972
+ }
2973
+ .settings-row + .settings-row {
2974
+ border-top: 1px solid #f8fafc;
2975
+ }
2976
+
2977
+ .settings-row-label {
2978
+ display: flex;
2979
+ flex-direction: column;
2980
+ gap: 2px;
2981
+ font-size: 13px;
2982
+ font-weight: 500;
2983
+ color: #1e293b;
2984
+ min-width: 0;
2985
+ }
2986
+
2987
+ /* Toggle switch */
2988
+ .clickly-toggle {
2989
+ position: relative;
2990
+ flex-shrink: 0;
2991
+ width: 38px;
2992
+ height: 22px;
2993
+ cursor: pointer;
2994
+ }
2995
+ .clickly-toggle input {
2996
+ position: absolute;
2997
+ opacity: 0;
2998
+ width: 0;
2999
+ height: 0;
3000
+ }
3001
+ .toggle-track {
3002
+ position: absolute;
3003
+ inset: 0;
3004
+ background: #e2e8f0;
3005
+ border-radius: 11px;
3006
+ transition: background 180ms ease;
3007
+ }
3008
+ .toggle-track::after {
3009
+ content: "";
3010
+ position: absolute;
3011
+ top: 3px;
3012
+ left: 3px;
3013
+ width: 16px;
3014
+ height: 16px;
3015
+ background: #fff;
3016
+ border-radius: 50%;
3017
+ box-shadow: 0 1px 4px rgba(0,0,0,0.2);
3018
+ transition: transform 180ms cubic-bezier(0.34, 1.56, 0.64, 1);
3019
+ }
3020
+ .clickly-toggle input:checked + .toggle-track {
3021
+ background: #0ea5e9;
3022
+ }
3023
+ .clickly-toggle input:checked + .toggle-track::after {
3024
+ transform: translateX(16px);
3025
+ }
3026
+
3027
+ /* Color picker */
3028
+ .settings-color-wrap {
3029
+ display: flex;
3030
+ align-items: center;
3031
+ gap: 0;
3032
+ cursor: pointer;
3033
+ border-radius: 8px;
3034
+ overflow: hidden;
3035
+ border: 1px solid #e2e8f0;
3036
+ flex-shrink: 0;
3037
+ }
3038
+ .settings-color-wrap input[type="color"] {
3039
+ position: absolute;
3040
+ opacity: 0;
3041
+ width: 0;
3042
+ height: 0;
3043
+ pointer-events: none;
3044
+ }
3045
+ .color-swatch {
3046
+ display: block;
3047
+ width: 32px;
3048
+ height: 24px;
3049
+ border-radius: 7px;
3050
+ border: 1px solid rgba(0,0,0,0.08);
3051
+ transition: transform 100ms ease;
3052
+ }
3053
+ .settings-color-wrap:hover .color-swatch { transform: scale(1.08); }
1022
3054
 
1023
3055
  .clickly-counter {
1024
- display: inline-flex;
3056
+ /* Float as a badge \u2014 positioned absolutely so it doesn't widen the button */
3057
+ position: absolute;
3058
+ top: -5px;
3059
+ right: -5px;
3060
+ min-width: 16px;
3061
+ height: 16px;
3062
+ padding: 0 4px;
3063
+ border-radius: 8px;
3064
+ background: #f59e0b;
3065
+ color: #0f172a;
3066
+ font-size: 10px;
3067
+ font-weight: 700;
3068
+ display: flex;
1025
3069
  align-items: center;
1026
3070
  justify-content: center;
1027
- min-width: 18px;
1028
- height: 18px;
1029
- padding: 0 5px;
1030
- border-radius: 9px;
1031
- background: #f59e0b;
1032
- color: #1f2937;
1033
- font-size: 11px;
1034
- font-weight: 600;
1035
- margin-left: 4px;
3071
+ pointer-events: none;
3072
+ border: 1.5px solid rgba(9, 14, 28, 0.97);
1036
3073
  }
1037
3074
 
1038
3075
  /* \u2500\u2500\u2500 Annotation pins (persistent numbered markers) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1039
3076
 
1040
3077
  .clickly-pin {
1041
3078
  position: fixed;
1042
- width: 22px;
1043
- height: 22px;
3079
+ width: 24px;
3080
+ height: 24px;
1044
3081
  border-radius: 999px;
1045
- background: #f59e0b;
1046
- color: #0f172a;
1047
- font: 600 12px/22px -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
3082
+ background: #10b981;
3083
+ color: #fff;
3084
+ font: 700 11px/24px -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
1048
3085
  text-align: center;
1049
3086
  cursor: pointer;
1050
3087
  pointer-events: auto;
1051
3088
  user-select: none;
1052
- box-shadow: 0 2px 6px rgba(0,0,0,0.25), 0 0 0 2px #fff;
1053
- z-index: 1;
1054
- transition: transform 100ms ease;
3089
+ box-shadow: 0 2px 8px rgba(16,185,129,0.4), 0 0 0 2px #fff;
3090
+ z-index: 10;
3091
+ transition: transform 120ms cubic-bezier(0.34,1.56,0.64,1), box-shadow 120ms ease;
3092
+ }
3093
+ .clickly-pin:hover {
3094
+ transform: scale(1.18);
3095
+ box-shadow: 0 4px 16px rgba(16,185,129,0.5), 0 0 0 2px #fff;
1055
3096
  }
1056
- .clickly-pin:hover { transform: scale(1.12); }
1057
3097
  .clickly-pin-num { display: block; }
1058
3098
 
1059
- .clickly-pin-bubble {
3099
+ /* \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 */
3100
+
3101
+ .pin-preview {
1060
3102
  position: absolute;
1061
- top: 28px;
1062
- left: 0;
1063
- width: 240px;
3103
+ right: calc(100% + 10px);
3104
+ top: 50%;
3105
+ transform: translateY(-50%);
3106
+ width: 220px;
1064
3107
  padding: 8px 10px;
1065
- background: #fff;
1066
- color: #0f172a;
1067
- border-radius: 8px;
1068
- box-shadow: 0 8px 24px rgba(2,6,23,0.18), 0 0 0 1px rgba(15,23,42,0.06);
1069
- font: 13px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
3108
+ background: rgba(9, 14, 28, 0.96);
3109
+ color: #f1f5f9;
3110
+ border-radius: 10px;
3111
+ box-shadow: 0 8px 24px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.07) inset;
3112
+ font: 12px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
1070
3113
  text-align: left;
1071
3114
  cursor: default;
1072
- z-index: 3;
3115
+ pointer-events: none;
3116
+ z-index: 11;
1073
3117
  animation: clickly-fade-in 100ms ease-out;
3118
+ white-space: normal;
1074
3119
  }
1075
- .clickly-pin-comment { word-break: break-word; }
1076
- .clickly-pin-meta {
1077
- margin-top: 6px;
3120
+
3121
+ .pin-preview-meta {
3122
+ font-size: 10.5px;
3123
+ color: #64748b;
1078
3124
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
1079
- font-size: 11px;
1080
- color: #475569;
1081
- word-break: break-all;
3125
+ margin-bottom: 4px;
3126
+ overflow: hidden;
3127
+ white-space: nowrap;
3128
+ text-overflow: ellipsis;
3129
+ }
3130
+
3131
+ .pin-preview-comment {
3132
+ font-size: 12px;
3133
+ color: #e2e8f0;
3134
+ word-break: break-word;
3135
+ display: -webkit-box;
3136
+ -webkit-line-clamp: 3;
3137
+ -webkit-box-orient: vertical;
3138
+ overflow: hidden;
1082
3139
  }
1083
- .clickly-pin-remove {
1084
- margin-top: 8px;
1085
- padding: 4px 8px;
1086
- border: 1px solid #fecaca;
3140
+
3141
+ /* \u2500\u2500\u2500 Pin edit popup \u2014 same white card as .clickly-popup \u2500\u2500\u2500\u2500\u2500\u2500 */
3142
+
3143
+ .pin-edit {
3144
+ /* Positioning: floats to the LEFT of the pin */
3145
+ position: absolute;
3146
+ right: calc(100% + 12px);
3147
+ top: 50%;
3148
+ transform: translateY(-50%);
3149
+ /* Appearance: identical to .clickly-popup */
3150
+ width: 300px;
3151
+ padding: 12px;
1087
3152
  background: #fff;
1088
- color: #b91c1c;
1089
- border-radius: 4px;
1090
- font: 11px inherit;
3153
+ color: #0f172a;
3154
+ border-radius: 10px;
3155
+ box-shadow: 0 12px 32px rgba(2,6,23,0.18), 0 0 0 1px rgba(15,23,42,0.06);
3156
+ font: 13px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
3157
+ text-align: left;
3158
+ cursor: default;
3159
+ z-index: 11;
3160
+ animation: clickly-fade-in 120ms cubic-bezier(0.16,1,0.3,1);
3161
+ /* Allow inner popup-styles to scroll */
3162
+ max-height: 80vh;
3163
+ overflow: hidden;
3164
+ display: flex;
3165
+ flex-direction: column;
3166
+ }
3167
+
3168
+ /* Reuse .clickly-popup textarea inside pin-edit */
3169
+ .pin-edit textarea {
3170
+ width: 100%;
3171
+ min-height: 64px;
3172
+ max-height: 120px;
3173
+ padding: 8px;
3174
+ border: 1px solid #e2e8f0;
3175
+ border-radius: 6px;
3176
+ resize: vertical;
3177
+ font: inherit;
3178
+ color: inherit;
3179
+ background: #fff;
3180
+ box-sizing: border-box;
3181
+ }
3182
+ .pin-edit textarea:focus {
3183
+ outline: none;
3184
+ border-color: #0ea5e9;
3185
+ box-shadow: 0 0 0 3px rgba(14,165,233,0.18);
3186
+ }
3187
+
3188
+ /* Actions row */
3189
+ .pin-edit-actions {
3190
+ display: flex;
3191
+ align-items: center;
3192
+ gap: 6px;
3193
+ margin-top: 10px;
3194
+ }
3195
+
3196
+ .pin-edit-delete {
3197
+ display: grid;
3198
+ place-items: center;
3199
+ width: 28px;
3200
+ height: 28px;
3201
+ flex-shrink: 0;
3202
+ background: transparent;
3203
+ border: 1px solid #fee2e2;
3204
+ border-radius: 6px;
3205
+ color: #ef4444;
1091
3206
  cursor: pointer;
3207
+ padding: 0;
3208
+ transition: background 100ms, border-color 100ms;
1092
3209
  }
1093
- .clickly-pin-remove:hover { background: #fef2f2; }
3210
+ .pin-edit-delete:hover { background: #fef2f2; border-color: #fca5a5; }
3211
+ .pin-edit-delete svg { width: 13px; height: 13px; }
1094
3212
 
1095
3213
  /* \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 */
1096
3214
 
1097
3215
  .clickly-list {
1098
3216
  position: fixed;
1099
- max-height: 50vh;
1100
3217
  width: 320px;
1101
- overflow-y: auto;
3218
+ max-height: 55vh;
1102
3219
  background: #fff;
1103
- border-radius: 10px;
1104
- box-shadow: 0 12px 32px rgba(2,6,23,0.18), 0 0 0 1px rgba(15,23,42,0.06);
3220
+ border-radius: 14px;
3221
+ box-shadow: 0 16px 48px rgba(2,6,23,0.20), 0 0 0 1px rgba(15,23,42,0.07);
1105
3222
  color: #0f172a;
1106
- font: 12px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
3223
+ font: 13px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
1107
3224
  pointer-events: auto;
1108
3225
  z-index: 2;
1109
- animation: clickly-fade-in 120ms ease-out;
3226
+ animation: clickly-fade-in 150ms cubic-bezier(0.16, 1, 0.3, 1);
3227
+ display: flex;
3228
+ flex-direction: column;
3229
+ overflow: hidden;
1110
3230
  }
1111
- .clickly-list .row { display: flex; gap: 8px; padding: 8px 10px; border-bottom: 1px solid #f1f5f9; }
1112
- .clickly-list .row:last-child { border-bottom: none; }
1113
- .clickly-list .num { color: #94a3b8; min-width: 18px; }
1114
- .clickly-list .selector { color: #475569; font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 11px; word-break: break-all; }
1115
- .clickly-list .body { flex: 1; min-width: 0; }
1116
- .clickly-list .body p { margin: 2px 0 0; word-break: break-word; }
1117
- .clickly-list button.remove {
3231
+
3232
+ .list-header {
3233
+ display: flex;
3234
+ align-items: center;
3235
+ gap: 8px;
3236
+ padding: 12px 14px 10px;
3237
+ border-bottom: 1px solid #f1f5f9;
3238
+ flex-shrink: 0;
3239
+ }
3240
+
3241
+ .list-title {
3242
+ font-size: 13px;
3243
+ font-weight: 600;
3244
+ color: #0f172a;
3245
+ letter-spacing: -0.01em;
3246
+ flex: 1;
3247
+ }
3248
+
3249
+ .list-count {
3250
+ min-width: 20px;
3251
+ height: 20px;
3252
+ padding: 0 6px;
3253
+ background: #f1f5f9;
3254
+ color: #475569;
3255
+ font-size: 11px;
3256
+ font-weight: 600;
3257
+ border-radius: 10px;
3258
+ display: grid;
3259
+ place-items: center;
3260
+ }
3261
+
3262
+ .list-items {
3263
+ overflow-y: auto;
3264
+ overscroll-behavior: contain;
3265
+ flex: 1;
3266
+ }
3267
+
3268
+ .list-empty {
3269
+ padding: 24px;
3270
+ text-align: center;
3271
+ color: #94a3b8;
3272
+ font-size: 12px;
3273
+ }
3274
+
3275
+ /* \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 */
3276
+
3277
+ .list-card {
3278
+ padding: 10px 12px;
3279
+ border-bottom: 1px solid #f8fafc;
3280
+ transition: background 80ms ease;
3281
+ }
3282
+ .list-card:last-child { border-bottom: none; }
3283
+ .list-card:hover { background: #fafafa; }
3284
+
3285
+ .list-card-header {
3286
+ display: flex;
3287
+ align-items: center;
3288
+ gap: 6px;
3289
+ margin-bottom: 4px;
3290
+ }
3291
+
3292
+ .list-card-num {
3293
+ font-size: 11px;
3294
+ font-weight: 700;
3295
+ color: #94a3b8;
3296
+ flex-shrink: 0;
3297
+ min-width: 20px;
3298
+ }
3299
+
3300
+ .list-card-path {
3301
+ flex: 1;
3302
+ min-width: 0;
3303
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
3304
+ font-size: 10.5px;
3305
+ color: #475569;
3306
+ white-space: nowrap;
3307
+ overflow: hidden;
3308
+ text-overflow: ellipsis;
3309
+ }
3310
+
3311
+ .list-card-actions {
3312
+ display: flex;
3313
+ gap: 3px;
3314
+ flex-shrink: 0;
3315
+ }
3316
+
3317
+ .list-action-btn {
3318
+ display: grid;
3319
+ place-items: center;
3320
+ width: 24px;
3321
+ height: 24px;
1118
3322
  background: transparent;
1119
3323
  border: none;
3324
+ border-radius: 6px;
1120
3325
  color: #94a3b8;
1121
3326
  cursor: pointer;
1122
- padding: 2px;
1123
- display: grid;
1124
- place-items: center;
3327
+ padding: 0;
3328
+ transition: background 80ms ease, color 80ms ease;
3329
+ }
3330
+ .list-action-btn svg { width: 12px; height: 12px; }
3331
+ .list-action-btn:hover { background: #f1f5f9; color: #475569; }
3332
+ .list-action-btn.copied { color: #10b981; }
3333
+ .list-action-btn.list-action-delete:hover { background: #fef2f2; color: #ef4444; }
3334
+
3335
+ .list-card-comment {
3336
+ font-size: 12px;
3337
+ color: #1e293b;
3338
+ line-height: 1.45;
3339
+ margin: 0 0 6px;
3340
+ word-break: break-word;
3341
+ display: -webkit-box;
3342
+ -webkit-line-clamp: 2;
3343
+ -webkit-box-orient: vertical;
3344
+ overflow: hidden;
3345
+ }
3346
+
3347
+ /* CSS changes badge */
3348
+ .list-card-css {
3349
+ background: rgba(124, 58, 237, 0.05);
3350
+ border: 1px solid rgba(124, 58, 237, 0.12);
3351
+ border-radius: 6px;
3352
+ padding: 5px 8px;
3353
+ margin-top: 4px;
3354
+ }
3355
+
3356
+ .list-card-css-label {
3357
+ display: block;
3358
+ font-size: 10px;
3359
+ font-weight: 600;
3360
+ color: #7c3aed;
3361
+ text-transform: uppercase;
3362
+ letter-spacing: 0.04em;
3363
+ margin-bottom: 3px;
3364
+ }
3365
+
3366
+ .list-card-css-code {
3367
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
3368
+ font-size: 10px;
3369
+ color: #475569;
3370
+ line-height: 1.5;
3371
+ margin: 0;
3372
+ white-space: pre-wrap;
3373
+ word-break: break-all;
3374
+ max-height: 60px;
3375
+ overflow: hidden;
1125
3376
  }
1126
- .clickly-list button.remove svg { width: 12px; height: 12px; }
1127
- .clickly-list button.remove:hover { color: #ef4444; }
1128
- .clickly-list .empty { padding: 20px; text-align: center; color: #94a3b8; }
1129
3377
  `;
1130
3378
 
1131
- // src/internal/globalStyles.ts
3379
+ // packages/react/src/internal/globalStyles.ts
1132
3380
  var GLOBAL_PAGE_CSS = `
1133
3381
  body[data-clickly-active] {
1134
3382
  -webkit-user-select: none !important;
@@ -1148,9 +3396,12 @@ body[data-clickly-annotating] {
1148
3396
  cursor: default !important;
1149
3397
  }
1150
3398
  `;
3399
+
3400
+ // packages/react/src/Clickly.tsx
3401
+ import { jsx as jsx9 } from "react/jsx-runtime";
1151
3402
  function Clickly({ className } = {}) {
1152
- const [mount, setMount] = useState(null);
1153
- useEffect(() => {
3403
+ const [mount, setMount] = useState7(null);
3404
+ useEffect7(() => {
1154
3405
  if (typeof window === "undefined" || typeof document === "undefined") return;
1155
3406
  let shadow = null;
1156
3407
  let engine = null;
@@ -1199,7 +3450,7 @@ function Clickly({ className } = {}) {
1199
3450
  document.body.removeAttribute("data-clickly-mode");
1200
3451
  };
1201
3452
  }, []);
1202
- useEffect(() => {
3453
+ useEffect7(() => {
1203
3454
  if (!mount || !className) return;
1204
3455
  mount.shadow.host.className = className;
1205
3456
  return () => {
@@ -1208,11 +3459,15 @@ function Clickly({ className } = {}) {
1208
3459
  }, [mount, className]);
1209
3460
  if (!mount) return null;
1210
3461
  return createPortal(
1211
- /* @__PURE__ */ jsx(ClicklyRoot, { engine: mount.engine, host: mount.shadow.host }),
3462
+ /* @__PURE__ */ jsx9(ClicklyRoot, { engine: mount.engine, host: mount.shadow.host }),
1212
3463
  mount.portal
1213
3464
  );
1214
3465
  }
1215
-
1216
- export { Clickly, DEFAULTS as DEFAULT_SETTINGS, annotationsToMarkdown, useAnnotations, useAnnotationsList, useSettings };
1217
- //# sourceMappingURL=index.js.map
1218
- //# sourceMappingURL=index.js.map
3466
+ export {
3467
+ Clickly,
3468
+ DEFAULTS as DEFAULT_SETTINGS,
3469
+ annotationsToMarkdown,
3470
+ useAnnotations,
3471
+ useAnnotationsList,
3472
+ useSettings
3473
+ };