@useclickly/react 1.0.3 → 1.0.4

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 (3) hide show
  1. package/dist/index.cjs +179 -1264
  2. package/dist/index.js +147 -1215
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,1027 +1,19 @@
1
- // packages/react/src/Clickly.tsx
2
- import { useEffect as useEffect8, useState as useState7 } from "react";
1
+ // ../react/src/Clickly.tsx
2
+ import { useEffect as useEffect7, useState as useState7 } from "react";
3
3
  import { createPortal } from "react-dom";
4
+ import {
5
+ Overlay,
6
+ SelectionEngine,
7
+ createShadowHost
8
+ } from "@useclickly/core";
4
9
 
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 useEffect7, useState as useState6 } from "react";
10
+ // ../react/src/internal/ClicklyRoot.tsx
11
+ import { useEffect as useEffect6, useState as useState6 } from "react";
1020
12
 
1021
- // packages/react/src/internal/Toolbar.tsx
1022
- import { useEffect as useEffect4, useRef as useRef4, useState as useState3 } from "react";
13
+ // ../react/src/internal/Toolbar.tsx
14
+ import { useRef as useRef4, useState as useState3 } from "react";
1023
15
 
1024
- // packages/react/src/hooks/useDraggable.ts
16
+ // ../react/src/hooks/useDraggable.ts
1025
17
  import { useCallback, useEffect, useRef, useState } from "react";
1026
18
  function useDraggable(defaultPos, size) {
1027
19
  const [position, setPosition] = useState(defaultPos);
@@ -1078,7 +70,7 @@ function clamp(n, lo, hi) {
1078
70
  return Math.max(lo, Math.min(hi, n));
1079
71
  }
1080
72
 
1081
- // packages/react/src/state/useEngineState.ts
73
+ // ../react/src/state/useEngineState.ts
1082
74
  import { useSyncExternalStore } from "react";
1083
75
  function useEngineState(engine) {
1084
76
  return useSyncExternalStore(
@@ -1090,118 +82,9 @@ function useEngineState(engine) {
1090
82
  }
1091
83
  var IDLE = { kind: "idle" };
1092
84
 
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
85
+ // ../react/src/state/annotations.ts
86
+ import { create } from "zustand";
87
+ import { useShallow } from "zustand/react/shallow";
1205
88
  var useAnnotations = create((set, get) => ({
1206
89
  byId: {},
1207
90
  order: [],
@@ -1232,7 +115,8 @@ function useAnnotationsList() {
1232
115
  );
1233
116
  }
1234
117
 
1235
- // packages/react/src/state/settings.ts
118
+ // ../react/src/state/settings.ts
119
+ import { create as create2 } from "zustand";
1236
120
  var DEFAULTS = {
1237
121
  outputDetail: "standard",
1238
122
  copyOnAdd: true,
@@ -1258,7 +142,7 @@ function persist(s) {
1258
142
  } catch {
1259
143
  }
1260
144
  }
1261
- var useSettings = create((set) => ({
145
+ var useSettings = create2((set) => ({
1262
146
  ...load(),
1263
147
  set: (patch) => set((cur) => {
1264
148
  const next = { ...cur, ...patch };
@@ -1271,7 +155,7 @@ var useSettings = create((set) => ({
1271
155
  }
1272
156
  }));
1273
157
 
1274
- // packages/react/src/output/markdown.ts
158
+ // ../react/src/output/markdown.ts
1275
159
  function annotationsToMarkdown(annotations, detail = "standard") {
1276
160
  if (!annotations.length) return "(no annotations)";
1277
161
  return annotations.map((a, i) => formatOne(a, i + 1, detail)).join("\n\n");
@@ -1294,7 +178,7 @@ function formatOne(a, index, detail) {
1294
178
  }
1295
179
  if (detail === "detailed" || detail === "forensic") {
1296
180
  if (a.reactComponents) lines.push(`**React:** ${a.reactComponents}`);
1297
- if (a.nearbyText) lines.push(`**Nearby text:** ${truncate2(a.nearbyText, 120)}`);
181
+ if (a.nearbyText) lines.push(`**Nearby text:** ${truncate(a.nearbyText, 120)}`);
1298
182
  }
1299
183
  if (detail === "forensic" && a.computedStyles) {
1300
184
  lines.push("**Computed styles:**\n```css\n" + a.computedStyles + "\n```");
@@ -1306,11 +190,11 @@ function formatOne(a, index, detail) {
1306
190
  if (a.severity) lines.push(`**Severity:** ${a.severity}`);
1307
191
  return lines.join("\n");
1308
192
  }
1309
- function truncate2(s, n) {
193
+ function truncate(s, n) {
1310
194
  return s.length <= n ? s : s.slice(0, n - 1) + "\u2026";
1311
195
  }
1312
196
 
1313
- // packages/react/src/internal/icons.tsx
197
+ // ../react/src/internal/icons.tsx
1314
198
  import { jsx, jsxs } from "react/jsx-runtime";
1315
199
  function Icon({
1316
200
  children,
@@ -1371,7 +255,7 @@ var IconClose = () => /* @__PURE__ */ jsx(Icon, { children: /* @__PURE__ */ jsx(
1371
255
  var IconCheck = () => /* @__PURE__ */ jsx(Icon, { children: /* @__PURE__ */ jsx("path", { d: "M5 12l5 5L20 7" }) });
1372
256
  var IconList = () => /* @__PURE__ */ jsx(Icon, { children: /* @__PURE__ */ jsx("path", { d: "M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01" }) });
1373
257
 
1374
- // packages/react/src/internal/SettingsPopover.tsx
258
+ // ../react/src/internal/SettingsPopover.tsx
1375
259
  import { useEffect as useEffect2, useRef as useRef2 } from "react";
1376
260
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1377
261
  function SettingsPopover({ anchor, width, onClose }) {
@@ -1472,7 +356,7 @@ function SettingsPopover({ anchor, width, onClose }) {
1472
356
  ] });
1473
357
  }
1474
358
 
1475
- // packages/react/src/internal/AnnotationList.tsx
359
+ // ../react/src/internal/AnnotationList.tsx
1476
360
  import { useEffect as useEffect3, useRef as useRef3, useState as useState2 } from "react";
1477
361
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1478
362
  function AnnotationList({ anchor, width, onClose }) {
@@ -1560,7 +444,7 @@ function AnnotationCard({
1560
444
  ] });
1561
445
  }
1562
446
 
1563
- // packages/react/src/internal/Toolbar.tsx
447
+ // ../react/src/internal/Toolbar.tsx
1564
448
  import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1565
449
  var TOOLBAR_SIZE = { width: 360, height: 44 };
1566
450
  var SHORTCUTS = [
@@ -1615,45 +499,14 @@ function Tip({
1615
499
  ] })
1616
500
  ] });
1617
501
  }
1618
- function Toolbar({ engine, onCollapse }) {
502
+ function Toolbar({ engine, onCollapse, isClosing, onCloseEnd, frozen, onFreezeToggle }) {
1619
503
  const state = useEngineState(engine);
1620
504
  const annotations = useAnnotationsList();
1621
505
  const clearAnnotations = useAnnotations((s) => s.clear);
1622
506
  const outputDetail = useSettings((s) => s.outputDetail);
1623
507
  const [showSettings, setShowSettings] = useState3(false);
1624
508
  const [showList, setShowList] = useState3(false);
1625
- const [frozen, setFrozen] = useState3(false);
1626
509
  const anchorRef = useRef4(null);
1627
- useEffect4(() => {
1628
- const STYLE_ID = "clickly-freeze-animations";
1629
- const gsap = window.gsap;
1630
- if (frozen) {
1631
- if (!document.getElementById(STYLE_ID)) {
1632
- const el = document.createElement("style");
1633
- el.id = STYLE_ID;
1634
- el.textContent = `
1635
- *, *::before, *::after {
1636
- animation-play-state: paused !important;
1637
- transition-duration: 0ms !important;
1638
- transition-delay: 0ms !important;
1639
- }
1640
- `;
1641
- document.head.appendChild(el);
1642
- }
1643
- if (gsap?.globalTimeline) {
1644
- gsap.globalTimeline.pause();
1645
- }
1646
- } else {
1647
- document.getElementById(STYLE_ID)?.remove();
1648
- if (gsap?.globalTimeline) {
1649
- gsap.globalTimeline.resume();
1650
- }
1651
- }
1652
- return () => {
1653
- document.getElementById(STYLE_ID)?.remove();
1654
- if (gsap?.globalTimeline) gsap.globalTimeline.resume();
1655
- };
1656
- }, [frozen]);
1657
510
  const { position, handleProps } = useDraggable(
1658
511
  {
1659
512
  x: Math.max(8, window.innerWidth - TOOLBAR_SIZE.width - 16),
@@ -1679,8 +532,9 @@ function Toolbar({ engine, onCollapse }) {
1679
532
  "div",
1680
533
  {
1681
534
  ref: anchorRef,
1682
- className: "clickly-toolbar",
535
+ className: `clickly-toolbar${isClosing ? " is-closing" : ""}`,
1683
536
  style: { left: position.x, top: position.y, width: TOOLBAR_SIZE.width },
537
+ onAnimationEnd: isClosing ? onCloseEnd : void 0,
1684
538
  "aria-label": "Clickly toolbar",
1685
539
  children: [
1686
540
  /* @__PURE__ */ jsx4(Tip, { label: "Move", children: /* @__PURE__ */ jsx4(
@@ -1740,7 +594,7 @@ function Toolbar({ engine, onCollapse }) {
1740
594
  "button",
1741
595
  {
1742
596
  className: `clickly-btn icon-only${frozen ? " is-freeze" : ""}`,
1743
- onClick: () => setFrozen((v) => !v),
597
+ onClick: onFreezeToggle,
1744
598
  "aria-label": frozen ? "Unfreeze page animations" : "Freeze page animations",
1745
599
  "aria-pressed": frozen,
1746
600
  children: /* @__PURE__ */ jsx4(IconFreeze, {})
@@ -1820,7 +674,7 @@ function Toolbar({ engine, onCollapse }) {
1820
674
  );
1821
675
  }
1822
676
 
1823
- // packages/react/src/internal/CollapsedFAB.tsx
677
+ // ../react/src/internal/CollapsedFAB.tsx
1824
678
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1825
679
  function CollapsedFAB({ onExpand }) {
1826
680
  const annotations = useAnnotationsList();
@@ -1839,9 +693,27 @@ function CollapsedFAB({ onExpand }) {
1839
693
  );
1840
694
  }
1841
695
 
1842
- // packages/react/src/internal/AnnotationPopup.tsx
1843
- import { useCallback as useCallback2, useEffect as useEffect5, useRef as useRef5, useState as useState4 } from "react";
1844
- import { nanoid } from "nanoid";
696
+ // ../react/src/internal/AnnotationPopup.tsx
697
+ import { useCallback as useCallback2, useEffect as useEffect4, useRef as useRef5, useState as useState4 } from "react";
698
+
699
+ // ../../node_modules/.pnpm/nanoid@5.1.16/node_modules/nanoid/url-alphabet/index.js
700
+ var urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
701
+
702
+ // ../../node_modules/.pnpm/nanoid@5.1.16/node_modules/nanoid/index.browser.js
703
+ var nanoid = (size = 21) => {
704
+ let id = "";
705
+ let bytes = crypto.getRandomValues(new Uint8Array(size |= 0));
706
+ while (size--) {
707
+ id += urlAlphabet[bytes[size] & 63];
708
+ }
709
+ return id;
710
+ };
711
+
712
+ // ../react/src/internal/AnnotationPopup.tsx
713
+ import {
714
+ collectMetadata,
715
+ collectComputedStyles
716
+ } from "@useclickly/core";
1845
717
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1846
718
  var POPUP_W = 320;
1847
719
  var POPUP_H_EST = 240;
@@ -1858,7 +730,7 @@ function AnnotationPopup({ engine }) {
1858
730
  const targetElRef = useRef5(null);
1859
731
  const taRef = useRef5(null);
1860
732
  const popupRef = useRef5(null);
1861
- useEffect5(() => {
733
+ useEffect4(() => {
1862
734
  if (state.kind === "annotating") {
1863
735
  setComment("");
1864
736
  setShowStyles(false);
@@ -1875,7 +747,7 @@ function AnnotationPopup({ engine }) {
1875
747
  requestAnimationFrame(() => taRef.current?.focus());
1876
748
  }
1877
749
  }, [state.kind, engine]);
1878
- useEffect5(() => {
750
+ useEffect4(() => {
1879
751
  if (state.kind !== "annotating") return;
1880
752
  const onDown = (e) => {
1881
753
  if (popupRef.current && e.composedPath().includes(popupRef.current)) return;
@@ -1993,10 +865,10 @@ function AnnotationPopup({ engine }) {
1993
865
  if (left + POPUP_W > vw) left = Math.max(8, vw - POPUP_W - 8);
1994
866
  setPlacement({ top, left, label });
1995
867
  }, [state, engine]);
1996
- useEffect5(() => {
868
+ useEffect4(() => {
1997
869
  calcPlacement();
1998
870
  }, [calcPlacement]);
1999
- useEffect5(() => {
871
+ useEffect4(() => {
2000
872
  if (state.kind !== "annotating") return;
2001
873
  const onScroll = () => calcPlacement();
2002
874
  window.addEventListener("scroll", onScroll, { capture: true, passive: true });
@@ -2117,7 +989,7 @@ function anchorRect(sel) {
2117
989
  if (sel.elements.length === 0) return null;
2118
990
  return sel.elements[0].getBoundingClientRect();
2119
991
  }
2120
- var TAG_LABELS2 = {
992
+ var TAG_LABELS = {
2121
993
  p: "paragraph",
2122
994
  h1: "heading",
2123
995
  h2: "heading",
@@ -2159,9 +1031,9 @@ var TAG_LABELS2 = {
2159
1031
  svg: "svg",
2160
1032
  canvas: "canvas"
2161
1033
  };
2162
- function describeElement2(el) {
1034
+ function describeElement(el) {
2163
1035
  const tag = el.tagName.toLowerCase();
2164
- const type = TAG_LABELS2[tag] ?? tag;
1036
+ const type = TAG_LABELS[tag] ?? tag;
2165
1037
  const text = (el.textContent ?? "").replace(/\s+/g, " ").trim();
2166
1038
  if (text.length > 0) {
2167
1039
  const preview = text.length > 48 ? text.slice(0, 48) + "\u2026" : text;
@@ -2173,19 +1045,19 @@ function describeElement2(el) {
2173
1045
  return type;
2174
1046
  }
2175
1047
  function describeSelection(sel) {
2176
- if (sel.kind === "single") return describeElement2(sel.element);
1048
+ if (sel.kind === "single") return describeElement(sel.element);
2177
1049
  if (sel.kind === "area") return `area \xB7 ${sel.elements.length} element(s)`;
2178
- if (sel.elements.length === 1) return describeElement2(sel.elements[0]);
1050
+ if (sel.elements.length === 1) return describeElement(sel.elements[0]);
2179
1051
  return `${sel.elements.length} element(s)`;
2180
1052
  }
2181
1053
 
2182
- // packages/react/src/internal/AnnotationPins.tsx
2183
- import { useEffect as useEffect6, useRef as useRef6, useState as useState5 } from "react";
1054
+ // ../react/src/internal/AnnotationPins.tsx
1055
+ import { useEffect as useEffect5, useRef as useRef6, useState as useState5 } from "react";
2184
1056
  import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2185
1057
  function AnnotationPins() {
2186
1058
  const annotations = useAnnotationsList();
2187
1059
  const [, setVersion] = useState5(0);
2188
- useEffect6(() => {
1060
+ useEffect5(() => {
2189
1061
  let raf = null;
2190
1062
  const update = () => {
2191
1063
  if (raf !== null) return;
@@ -2204,7 +1076,7 @@ function AnnotationPins() {
2204
1076
  }, []);
2205
1077
  return /* @__PURE__ */ jsx7(Fragment2, { children: annotations.map((a, i) => /* @__PURE__ */ jsx7(Pin, { number: i + 1, annotation: a }, a.id)) });
2206
1078
  }
2207
- var TAG_LABELS3 = {
1079
+ var TAG_LABELS2 = {
2208
1080
  p: "paragraph",
2209
1081
  h1: "heading",
2210
1082
  h2: "heading",
@@ -2244,7 +1116,7 @@ var TAG_LABELS3 = {
2244
1116
  function pinLabel(annotation) {
2245
1117
  const raw = (annotation.element ?? "").toLowerCase();
2246
1118
  const tag = raw.split(/[.#\s]/)[0] ?? "";
2247
- return (TAG_LABELS3[tag] ?? tag) || "element";
1119
+ return (TAG_LABELS2[tag] ?? tag) || "element";
2248
1120
  }
2249
1121
  function parseStyles(raw) {
2250
1122
  if (!raw) return [];
@@ -2285,7 +1157,7 @@ function Pin({ number, annotation }) {
2285
1157
  if (e.key === "Escape") closeEdit();
2286
1158
  if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) save();
2287
1159
  };
2288
- useEffect6(() => {
1160
+ useEffect5(() => {
2289
1161
  if (!editing) return;
2290
1162
  const onDown = (e) => {
2291
1163
  if (editRef.current && e.composedPath().includes(editRef.current)) return;
@@ -2294,7 +1166,7 @@ function Pin({ number, annotation }) {
2294
1166
  window.addEventListener("pointerdown", onDown, true);
2295
1167
  return () => window.removeEventListener("pointerdown", onDown, true);
2296
1168
  }, [editing]);
2297
- useEffect6(() => {
1169
+ useEffect5(() => {
2298
1170
  if (!editing) return;
2299
1171
  let el = null;
2300
1172
  if (annotation.elementPath) {
@@ -2422,28 +1294,63 @@ function resolvePosition(a) {
2422
1294
  return null;
2423
1295
  }
2424
1296
 
2425
- // packages/react/src/internal/ClicklyRoot.tsx
1297
+ // ../react/src/internal/ClicklyRoot.tsx
2426
1298
  import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
2427
1299
  function ClicklyRoot({
2428
1300
  engine,
2429
1301
  host
2430
1302
  }) {
2431
1303
  const [expanded, setExpanded] = useState6(false);
1304
+ const [toolbarClosing, setToolbarClosing] = useState6(false);
1305
+ const [frozen, setFrozen] = useState6(false);
1306
+ const collapse = () => {
1307
+ setToolbarClosing(true);
1308
+ };
1309
+ const onToolbarCloseEnd = () => {
1310
+ setToolbarClosing(false);
1311
+ setExpanded(false);
1312
+ };
2432
1313
  const clearAnnotations = useAnnotations((s) => s.clear);
2433
1314
  const outputDetail = useSettings((s) => s.outputDetail);
2434
1315
  const markerColor = useSettings((s) => s.markerColor);
2435
1316
  const engineState = useEngineState(engine);
2436
- useEffect7(() => {
1317
+ useEffect6(() => {
2437
1318
  host.style.setProperty("--clickly-hover", markerColor);
2438
1319
  }, [host, markerColor]);
2439
- useEffect7(() => {
1320
+ useEffect6(() => {
1321
+ const STYLE_ID = "clickly-freeze-animations";
1322
+ const gsap = window.gsap;
1323
+ if (frozen) {
1324
+ if (!document.getElementById(STYLE_ID)) {
1325
+ const el = document.createElement("style");
1326
+ el.id = STYLE_ID;
1327
+ el.textContent = `
1328
+ *, *::before, *::after {
1329
+ animation-play-state: paused !important;
1330
+ transition-duration: 0ms !important;
1331
+ transition-delay: 0ms !important;
1332
+ }
1333
+ `;
1334
+ document.head.appendChild(el);
1335
+ }
1336
+ if (gsap?.globalTimeline) gsap.globalTimeline.pause();
1337
+ } else {
1338
+ document.getElementById(STYLE_ID)?.remove();
1339
+ if (gsap?.globalTimeline) gsap.globalTimeline.resume();
1340
+ }
1341
+ return () => {
1342
+ document.getElementById(STYLE_ID)?.remove();
1343
+ if (gsap?.globalTimeline) gsap.globalTimeline.resume();
1344
+ };
1345
+ }, [frozen]);
1346
+ useEffect6(() => {
2440
1347
  if (expanded) {
2441
1348
  if (engine.getSnapshot().kind === "idle") engine.activate("single");
2442
1349
  } else {
2443
1350
  if (engine.getSnapshot().kind !== "idle") engine.deactivate();
2444
1351
  }
2445
1352
  }, [expanded, engine]);
2446
- useEffect7(() => {
1353
+ useEffect6(() => {
2447
1354
  const active = engineState.kind !== "idle";
2448
1355
  if (active) document.body.setAttribute("data-clickly-active", "");
2449
1356
  else document.body.removeAttribute("data-clickly-active");
@@ -2461,7 +1368,7 @@ function ClicklyRoot({
2461
1368
  document.body.removeAttribute("data-clickly-annotating");
2462
1369
  };
2463
1370
  }, [engineState]);
2464
- useEffect7(() => {
1371
+ useEffect6(() => {
2465
1372
  const onKey = (e) => {
2466
1373
  const tag = e.target?.tagName;
2467
1374
  if (tag === "INPUT" || tag === "TEXTAREA") return;
@@ -2470,12 +1377,13 @@ function ClicklyRoot({
2470
1377
  if (active && (active.tagName === "TEXTAREA" || active.tagName === "INPUT")) return;
2471
1378
  if (e.key.toLowerCase() === "f" && e.shiftKey && (e.metaKey || e.ctrlKey)) {
2472
1379
  e.preventDefault();
2473
- setExpanded((v) => !v);
1380
+ if (expanded) collapse();
1381
+ else setExpanded(true);
2474
1382
  return;
2475
1383
  }
2476
1384
  if (e.key === "Escape") {
2477
1385
  if (engine.getSnapshot().kind === "annotating") return;
2478
- if (expanded) setExpanded(false);
1386
+ if (expanded) collapse();
2479
1387
  return;
2480
1388
  }
2481
1389
  if (!expanded) return;
@@ -2506,14 +1414,24 @@ function ClicklyRoot({
2506
1414
  }, [clearAnnotations, outputDetail, expanded, engine]);
2507
1415
  return /* @__PURE__ */ jsxs8("div", { className: "clickly-ui", children: [
2508
1416
  /* @__PURE__ */ jsx8(AnnotationPins, {}),
2509
- expanded ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
2510
- /* @__PURE__ */ jsx8(Toolbar, { engine, onCollapse: () => setExpanded(false) }),
2511
- /* @__PURE__ */ jsx8(AnnotationPopup, { engine })
1417
+ expanded || toolbarClosing ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
1418
+ /* @__PURE__ */ jsx8(
1419
+ Toolbar,
1420
+ {
1421
+ engine,
1422
+ onCollapse: collapse,
1423
+ isClosing: toolbarClosing,
1424
+ onCloseEnd: onToolbarCloseEnd,
1425
+ frozen,
1426
+ onFreezeToggle: () => setFrozen((v) => !v)
1427
+ }
1428
+ ),
1429
+ !toolbarClosing && /* @__PURE__ */ jsx8(AnnotationPopup, { engine })
2512
1430
  ] }) : /* @__PURE__ */ jsx8(CollapsedFAB, { onExpand: () => setExpanded(true) })
2513
1431
  ] });
2514
1432
  }
2515
1433
 
2516
- // packages/react/src/internal/styles.ts
1434
+ // ../react/src/internal/styles.ts
2517
1435
  var REACT_UI_CSS = `
2518
1436
  .clickly-ui, .clickly-ui * { box-sizing: border-box; }
2519
1437
 
@@ -2539,6 +1457,11 @@ var REACT_UI_CSS = `
2539
1457
  0 0 0 1px rgba(255,255,255,0.06) inset;
2540
1458
  transition: transform 140ms ease, box-shadow 140ms ease;
2541
1459
  z-index: 1;
1460
+ animation: clickly-fab-open 280ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
1461
+ }
1462
+ @keyframes clickly-fab-open {
1463
+ from { opacity: 0; transform: scale(0.5); }
1464
+ to { opacity: 1; transform: scale(1); }
2542
1465
  }
2543
1466
  .clickly-fab:hover {
2544
1467
  transform: scale(1.06);
@@ -2585,12 +1508,21 @@ var REACT_UI_CSS = `
2585
1508
  pointer-events: auto;
2586
1509
  user-select: none;
2587
1510
  z-index: 1;
2588
- animation: clickly-fade-in 150ms cubic-bezier(0.16, 1, 0.3, 1);
1511
+ transform-origin: bottom right;
1512
+ animation: clickly-toolbar-open 240ms cubic-bezier(0.16, 1, 0.3, 1) both;
1513
+ }
1514
+ .clickly-toolbar.is-closing {
1515
+ animation: clickly-toolbar-close 200ms cubic-bezier(0.4, 0, 1, 1) both;
1516
+ pointer-events: none;
2589
1517
  }
2590
1518
 
2591
- @keyframes clickly-fade-in {
2592
- from { opacity: 0; transform: translateY(6px) scale(0.97); }
2593
- to { opacity: 1; transform: translateY(0) scale(1); }
1519
+ @keyframes clickly-toolbar-open {
1520
+ from { opacity: 0; transform: scale(0.82) translateY(10px); }
1521
+ to { opacity: 1; transform: scale(1) translateY(0); }
1522
+ }
1523
+ @keyframes clickly-toolbar-close {
1524
+ from { opacity: 1; transform: scale(1) translateY(0); }
1525
+ to { opacity: 0; transform: scale(0.82) translateY(10px); }
2594
1526
  }
2595
1527
 
2596
1528
  .clickly-toolbar .grip {
@@ -3562,7 +2494,7 @@ var REACT_UI_CSS = `
3562
2494
  }
3563
2495
  `;
3564
2496
 
3565
- // packages/react/src/internal/globalStyles.ts
2497
+ // ../react/src/internal/globalStyles.ts
3566
2498
  var GLOBAL_PAGE_CSS = `
3567
2499
  body[data-clickly-active] {
3568
2500
  -webkit-user-select: none !important;
@@ -3583,11 +2515,11 @@ body[data-clickly-annotating] {
3583
2515
  }
3584
2516
  `;
3585
2517
 
3586
- // packages/react/src/Clickly.tsx
2518
+ // ../react/src/Clickly.tsx
3587
2519
  import { jsx as jsx9 } from "react/jsx-runtime";
3588
2520
  function Clickly({ className } = {}) {
3589
2521
  const [mount, setMount] = useState7(null);
3590
- useEffect8(() => {
2522
+ useEffect7(() => {
3591
2523
  if (typeof window === "undefined" || typeof document === "undefined") return;
3592
2524
  let shadow = null;
3593
2525
  let engine = null;
@@ -3636,7 +2568,7 @@ function Clickly({ className } = {}) {
3636
2568
  document.body.removeAttribute("data-clickly-mode");
3637
2569
  };
3638
2570
  }, []);
3639
- useEffect8(() => {
2571
+ useEffect7(() => {
3640
2572
  if (!mount || !className) return;
3641
2573
  mount.shadow.host.className = className;
3642
2574
  return () => {