@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.cjs CHANGED
@@ -1,9 +1,7 @@
1
1
  "use strict";
2
- var __create = Object.create;
3
2
  var __defProp = Object.defineProperty;
4
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
6
  var __export = (target, all) => {
9
7
  for (var name in all)
@@ -17,17 +15,9 @@ var __copyProps = (to, from, except, desc) => {
17
15
  }
18
16
  return to;
19
17
  };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
19
 
30
- // packages/react/src/index.ts
20
+ // ../react/src/index.ts
31
21
  var index_exports = {};
32
22
  __export(index_exports, {
33
23
  Clickly: () => Clickly,
@@ -39,1030 +29,18 @@ __export(index_exports, {
39
29
  });
40
30
  module.exports = __toCommonJS(index_exports);
41
31
 
42
- // packages/react/src/Clickly.tsx
43
- var import_react11 = require("react");
32
+ // ../react/src/Clickly.tsx
33
+ var import_react9 = require("react");
44
34
  var import_react_dom = require("react-dom");
35
+ var import_core2 = require("@useclickly/core");
45
36
 
46
- // packages/core/dist/index.js
47
- function pickElementAt(doc, x, y, excludeHost) {
48
- if (typeof doc.elementsFromPoint !== "function") return null;
49
- const chain = doc.elementsFromPoint(x, y);
50
- for (const el of chain) {
51
- if (!isInExcludedSubtree(el, excludeHost)) return el;
52
- }
53
- return null;
54
- }
55
- function pickElementsInRect(root, rect, excludeHost) {
56
- const out = [];
57
- const stack = [root];
58
- while (stack.length) {
59
- const el = stack.pop();
60
- if (isInExcludedSubtree(el, excludeHost)) continue;
61
- const box = el.getBoundingClientRect();
62
- if (box.width > 0 && box.height > 0 && containedIn(box, rect)) {
63
- out.push(el);
64
- }
65
- for (let i = 0; i < el.children.length; i++) {
66
- const child = el.children[i];
67
- if (child) stack.push(child);
68
- }
69
- }
70
- return out;
71
- }
72
- function containedIn(el, sel) {
73
- return el.left >= sel.x && el.top >= sel.y && el.right <= sel.x + sel.width && el.bottom <= sel.y + sel.height;
74
- }
75
- function isInExcludedSubtree(el, host) {
76
- if (!host) return false;
77
- let cur = el;
78
- while (cur) {
79
- if (cur === host) return true;
80
- cur = cur.parentNode;
81
- }
82
- return false;
83
- }
84
- function manhattan(a, b) {
85
- return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
86
- }
87
- function rectFromPoints(start, current) {
88
- const x = Math.min(start.x, current.x);
89
- const y = Math.min(start.y, current.y);
90
- const width = Math.abs(current.x - start.x);
91
- const height = Math.abs(current.y - start.y);
92
- return { x, y, width, height };
93
- }
94
- var DRAG_THRESHOLD_PX = 12;
95
- var initialState = { kind: "idle" };
96
- function reduce(state, event) {
97
- if (event.type === "DEACTIVATE") return { kind: "idle" };
98
- if (event.type === "ESCAPE") {
99
- if (state.kind === "idle") return state;
100
- if (state.kind === "annotating") return resumeInspect(
101
- [],
102
- /* mode */
103
- void 0
104
- );
105
- if (state.kind === "inspect" && state.pinned.length > 0) {
106
- return { ...state, pinned: [], hoverTarget: null };
107
- }
108
- return resumeInspect("pinned" in state ? state.pinned : []);
109
- }
110
- if (event.type === "CLEAR_PINNED") {
111
- if (state.kind === "idle") return state;
112
- if (state.kind === "inspect") return { ...state, pinned: [] };
113
- return state;
114
- }
115
- switch (state.kind) {
116
- case "idle":
117
- if (event.type === "ACTIVATE") {
118
- return {
119
- kind: "inspect",
120
- mode: event.mode ?? "single",
121
- hoverTarget: null,
122
- pinned: []
123
- };
124
- }
125
- return state;
126
- case "inspect":
127
- if (event.type === "MODE_CHANGE") return { ...state, mode: event.mode };
128
- if (event.type === "POINTER_MOVE") return { ...state, hoverTarget: event.target };
129
- if (event.type === "POINTER_DOWN") {
130
- return {
131
- kind: "pressed",
132
- mode: state.mode,
133
- start: event.point,
134
- target: event.target,
135
- additive: event.additive,
136
- pinned: state.pinned
137
- };
138
- }
139
- if (event.type === "ANNOTATE_PINNED") {
140
- if (state.pinned.length === 0) return state;
141
- return {
142
- kind: "annotating",
143
- selection: { kind: "multi", elements: state.pinned },
144
- pinned: state.pinned
145
- };
146
- }
147
- return state;
148
- case "pressed":
149
- if (event.type === "POINTER_MOVE") {
150
- if (manhattan(state.start, event.point) >= DRAG_THRESHOLD_PX) {
151
- return {
152
- kind: "dragging",
153
- mode: state.mode,
154
- start: state.start,
155
- current: event.point,
156
- pinned: state.pinned
157
- };
158
- }
159
- return state;
160
- }
161
- if (event.type === "POINTER_UP") {
162
- if (!state.target) {
163
- return resumeInspect(state.pinned, state.mode);
164
- }
165
- if (state.additive || state.mode === "multi") {
166
- const nextPinned = togglePinned(state.pinned, state.target);
167
- return {
168
- kind: "inspect",
169
- mode: state.mode,
170
- hoverTarget: state.target,
171
- pinned: nextPinned
172
- };
173
- }
174
- return {
175
- kind: "annotating",
176
- selection: { kind: "single", element: state.target },
177
- pinned: state.pinned
178
- };
179
- }
180
- return state;
181
- case "dragging":
182
- if (event.type === "POINTER_MOVE") return { ...state, current: event.point };
183
- if (event.type === "POINTER_UP") {
184
- const rect = rectFromPoints(state.start, state.current);
185
- const selection = { kind: "area", rect, elements: [] };
186
- return { kind: "annotating", selection, pinned: state.pinned };
187
- }
188
- return state;
189
- case "annotating":
190
- if (event.type === "COMMIT") {
191
- return resumeInspect([]);
192
- }
193
- return state;
194
- }
195
- }
196
- function resumeInspect(pinned, mode) {
197
- return {
198
- kind: "inspect",
199
- mode: mode ?? "single",
200
- hoverTarget: null,
201
- pinned
202
- };
203
- }
204
- function togglePinned(pinned, el) {
205
- const idx = pinned.indexOf(el);
206
- if (idx === -1) return [...pinned, el];
207
- const next = pinned.slice();
208
- next.splice(idx, 1);
209
- return next;
210
- }
211
- var SelectionEngine = class {
212
- state = initialState;
213
- listeners = /* @__PURE__ */ new Set();
214
- doc;
215
- host;
216
- raf;
217
- caf;
218
- searchRoot;
219
- pendingPointer = null;
220
- rafHandle = null;
221
- /** Guard for the pointermove RAF coalescer. Separate from `rafHandle`
222
- * because a synchronous `raf` (used in tests) returns a handle the cb
223
- * has already invalidated — boolean is the safe sentinel. */
224
- rafPending = false;
225
- attached = false;
226
- boundHandlers = [];
227
- constructor(deps = {}) {
228
- this.doc = deps.document ?? (typeof document !== "undefined" ? document : null);
229
- if (!this.doc) {
230
- throw new Error("SelectionEngine: no Document available (pass `deps.document`).");
231
- }
232
- this.host = deps.host ?? null;
233
- this.raf = deps.raf ?? ((cb) => typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame(cb) : setTimeout(() => cb(performance.now()), 16));
234
- this.caf = deps.caf ?? ((h) => {
235
- if (typeof cancelAnimationFrame !== "undefined") cancelAnimationFrame(h);
236
- else clearTimeout(h);
237
- });
238
- this.searchRoot = deps.searchRoot ?? this.doc.body;
239
- }
240
- /* ─── Subscribable<EngineState> ───────────────────────────────── */
241
- subscribe(listener) {
242
- this.listeners.add(listener);
243
- return () => this.listeners.delete(listener);
244
- }
245
- getSnapshot() {
246
- return this.state;
247
- }
248
- /* ─── Public control ──────────────────────────────────────────── */
249
- activate(mode) {
250
- perfMark("clickly:engine:activate");
251
- this.dispatch(mode ? { type: "ACTIVATE", mode } : { type: "ACTIVATE" });
252
- this.attach();
253
- }
254
- deactivate() {
255
- perfMark("clickly:engine:deactivate");
256
- this.detach();
257
- this.dispatch({ type: "DEACTIVATE" });
258
- }
259
- setMode(mode) {
260
- this.dispatch({ type: "MODE_CHANGE", mode });
261
- }
262
- commit() {
263
- this.dispatch({ type: "COMMIT" });
264
- }
265
- clearPinned() {
266
- this.dispatch({ type: "CLEAR_PINNED" });
267
- }
268
- /** Open the annotation popup populated with everything currently pinned. */
269
- annotatePinned() {
270
- this.dispatch({ type: "ANNOTATE_PINNED" });
271
- }
272
- /**
273
- * Returns the resolved selection from the current annotating state, with
274
- * area-mode element enumeration filled in (the reducer leaves it empty).
275
- * Returns null if not currently annotating.
276
- */
277
- resolveSelection() {
278
- if (this.state.kind !== "annotating") return null;
279
- const sel = this.state.selection;
280
- if (sel.kind !== "area") return sel;
281
- const elements = pickElementsInRect(this.searchRoot, sel.rect, this.host);
282
- return { ...sel, elements };
283
- }
284
- /* ─── Lifecycle ───────────────────────────────────────────────── */
285
- attach() {
286
- if (this.attached) return;
287
- this.attached = true;
288
- const win = this.doc.defaultView ?? globalThis;
289
- this.bind(this.doc, "pointermove", this.onPointerMove, { passive: true });
290
- this.bind(this.doc, "pointerdown", this.onPointerDown);
291
- this.bind(this.doc, "pointerup", this.onPointerUp);
292
- this.bind(win, "keydown", this.onKeyDown);
293
- }
294
- detach() {
295
- if (!this.attached) return;
296
- for (const [t, ev, fn, opts] of this.boundHandlers) t.removeEventListener(ev, fn, opts);
297
- this.boundHandlers = [];
298
- this.attached = false;
299
- if (this.rafHandle !== null) {
300
- this.caf(this.rafHandle);
301
- this.rafHandle = null;
302
- }
303
- this.rafPending = false;
304
- }
305
- destroy() {
306
- this.detach();
307
- this.listeners.clear();
308
- }
309
- bind(target, ev, fn, opts) {
310
- target.addEventListener(ev, fn, opts);
311
- this.boundHandlers.push([target, ev, fn, opts]);
312
- }
313
- /* ─── DOM event handlers ──────────────────────────────────────── */
314
- onPointerMove = (e) => {
315
- this.pendingPointer = { x: e.clientX, y: e.clientY };
316
- if (this.rafPending) return;
317
- this.rafPending = true;
318
- this.rafHandle = this.raf(() => {
319
- this.rafPending = false;
320
- this.rafHandle = null;
321
- const pt = this.pendingPointer;
322
- this.pendingPointer = null;
323
- if (!pt) return;
324
- const target = pickElementAt(this.doc, pt.x, pt.y, this.host);
325
- this.dispatch({ type: "POINTER_MOVE", point: pt, target });
326
- });
327
- };
328
- onPointerDown = (e) => {
329
- if (this.host && e.composedPath().includes(this.host)) return;
330
- const target = pickElementAt(this.doc, e.clientX, e.clientY, this.host);
331
- this.dispatch({
332
- type: "POINTER_DOWN",
333
- point: { x: e.clientX, y: e.clientY },
334
- target,
335
- additive: e.shiftKey || e.metaKey || e.ctrlKey
336
- });
337
- };
338
- onPointerUp = (e) => {
339
- this.dispatch({ type: "POINTER_UP", point: { x: e.clientX, y: e.clientY } });
340
- };
341
- onKeyDown = (e) => {
342
- if (e.key === "Escape") this.dispatch({ type: "ESCAPE" });
343
- };
344
- /* ─── Reducer plumbing ────────────────────────────────────────── */
345
- dispatch(event) {
346
- const next = reduce(this.state, event);
347
- if (next === this.state) return;
348
- this.state = next;
349
- for (const l of this.listeners) l(next);
350
- }
351
- };
352
- function perfMark(name) {
353
- if (typeof performance !== "undefined" && typeof performance.mark === "function") {
354
- try {
355
- performance.mark(name);
356
- } catch {
357
- }
358
- }
359
- }
360
- var OVERLAY_CSS = `
361
- :host {
362
- --clickly-hover: #06b6d4;
363
- --clickly-pinned: #f59e0b;
364
- --clickly-selected: #10b981;
365
- --clickly-marquee-stroke: #10b981;
366
- --clickly-marquee-fill: rgba(16, 185, 129, 0.10);
367
- --clickly-label-bg: rgba(15, 23, 42, 0.92);
368
- --clickly-label-fg: #f8fafc;
369
- --clickly-shadow: 0 0 0 1px rgba(255,255,255,0.5);
370
-
371
- all: initial;
372
- position: fixed;
373
- inset: 0;
374
- z-index: 2147483647;
375
- pointer-events: none;
376
- contain: layout style paint;
377
- isolation: isolate;
378
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
379
- }
380
-
381
- .layer {
382
- position: fixed;
383
- left: 0;
384
- top: 0;
385
- width: 0;
386
- height: 0;
387
- pointer-events: none;
388
- will-change: transform, width, height, opacity;
389
- }
390
-
391
- .marker {
392
- position: fixed;
393
- left: 0;
394
- top: 0;
395
- box-sizing: border-box;
396
- border-radius: 2px;
397
- pointer-events: none;
398
- will-change: transform, width, height, opacity;
399
- transition: opacity 80ms linear;
400
- }
401
-
402
- .marker[hidden] { display: none; }
403
-
404
- .marker.hover { box-shadow: 0 0 0 2px var(--clickly-hover), var(--clickly-shadow); }
405
- .marker.pinned { box-shadow: 0 0 0 2px var(--clickly-pinned), var(--clickly-shadow); }
406
- .marker.selected { box-shadow: 0 0 0 2px var(--clickly-selected), var(--clickly-shadow); }
407
-
408
- /* Marquee \u2014 used during drag (dashed) AND for the committed union
409
- box around multi/area selections (solid). */
410
- .marker.marquee {
411
- border: 2px dashed var(--clickly-marquee-stroke);
412
- background: var(--clickly-marquee-fill);
413
- border-radius: 8px;
414
- }
415
- .marker.marquee.is-committed {
416
- border-style: solid;
417
- box-shadow: 0 0 0 1px rgba(16, 185, 129, 0.18);
418
- }
419
-
420
- .label {
421
- position: fixed;
422
- left: 0;
423
- top: 0;
424
- padding: 2px 6px;
425
- background: var(--clickly-label-bg);
426
- color: var(--clickly-label-fg);
427
- font-size: 11px;
428
- line-height: 1.4;
429
- border-radius: 4px;
430
- white-space: nowrap;
431
- pointer-events: none;
432
- user-select: none;
433
- max-width: 50vw;
434
- overflow: hidden;
435
- text-overflow: ellipsis;
436
- }
437
- `;
438
- var HOST_TAG = "clickly-root";
439
- function createShadowHost(deps = {}) {
440
- const doc = deps.document ?? (typeof document !== "undefined" ? document : null);
441
- if (!doc) throw new Error("createShadowHost: no Document available");
442
- const existing = doc.querySelector(HOST_TAG);
443
- const host = existing ?? doc.createElement(HOST_TAG);
444
- if (!existing) doc.body.appendChild(host);
445
- const root = host.shadowRoot ?? host.attachShadow({ mode: "open" });
446
- if (!root.querySelector("style[data-clickly]")) {
447
- const style = doc.createElement("style");
448
- style.setAttribute("data-clickly", "");
449
- style.textContent = OVERLAY_CSS;
450
- root.appendChild(style);
451
- }
452
- return {
453
- host,
454
- root,
455
- destroy() {
456
- host.remove();
457
- }
458
- };
459
- }
460
- var OverlayRenderer = class {
461
- root;
462
- document;
463
- layer;
464
- hover;
465
- hoverLabel;
466
- /** Marquee rect — used both for live drag AND for the union box
467
- * drawn around multi-element selections after commit. */
468
- marquee;
469
- /** Pinned/selected single-element rings (recycled across renders). */
470
- pinnedPool = [];
471
- selectedPool = [];
472
- /** Last rendered state — used to re-render on scroll without a state change. */
473
- lastState = null;
474
- constructor(root, document2) {
475
- this.root = root;
476
- this.document = document2 ?? root.ownerDocument ?? (typeof globalThis !== "undefined" && globalThis.document ? globalThis.document : null);
477
- if (!this.document) throw new Error("OverlayRenderer: no Document available");
478
- this.layer = this.div("layer");
479
- this.root.appendChild(this.layer);
480
- this.hover = this.div("marker hover");
481
- this.hover.hidden = true;
482
- this.layer.appendChild(this.hover);
483
- this.hoverLabel = this.div("label");
484
- this.hoverLabel.hidden = true;
485
- this.layer.appendChild(this.hoverLabel);
486
- this.marquee = this.div("marker marquee");
487
- this.marquee.hidden = true;
488
- this.layer.appendChild(this.marquee);
489
- this.document.addEventListener("scroll", this.onScroll, {
490
- passive: true,
491
- capture: true
492
- });
493
- }
494
- onScroll = () => {
495
- if (this.lastState) this.renderState(this.lastState);
496
- };
497
- render(state) {
498
- this.lastState = state;
499
- this.renderState(state);
500
- }
501
- renderState(state) {
502
- this.renderSingle(this.hover, state.hover);
503
- if (state.hover) {
504
- this.hoverLabel.hidden = false;
505
- this.hoverLabel.textContent = describeElement(state.hover);
506
- const rect = state.hover.getBoundingClientRect();
507
- const labelY = rect.top - 20 < 0 ? rect.bottom + 4 : rect.top - 20;
508
- moveTo(this.hoverLabel, rect.left, labelY);
509
- } else {
510
- this.hoverLabel.hidden = true;
511
- }
512
- this.renderList(this.pinnedPool, state.pinned, "marker pinned");
513
- const sel = state.selection ?? [];
514
- const isMultiUnion = sel.length > 1 || state.marquee !== null && sel.length === 0;
515
- if (sel.length === 1) {
516
- this.renderList(this.selectedPool, [sel[0]], "marker selected");
517
- } else {
518
- this.renderList(this.selectedPool, [], "marker selected");
519
- }
520
- const marqueeRect = state.marquee ?? (sel.length > 1 ? unionOf(sel) : null);
521
- if (marqueeRect) {
522
- this.marquee.classList.toggle("is-committed", state.marquee === null);
523
- this.placeRect(this.marquee, marqueeRect);
524
- this.marquee.hidden = false;
525
- } else {
526
- this.marquee.hidden = true;
527
- }
528
- void isMultiUnion;
529
- }
530
- destroy() {
531
- this.document.removeEventListener("scroll", this.onScroll, { capture: true });
532
- this.lastState = null;
533
- this.layer.remove();
534
- }
535
- /* ─── Internals ───────────────────────────────────────────────── */
536
- renderSingle(node, target) {
537
- if (!target) {
538
- node.hidden = true;
539
- return;
540
- }
541
- const r = target.getBoundingClientRect();
542
- if (r.width === 0 && r.height === 0) {
543
- node.hidden = true;
544
- return;
545
- }
546
- this.placeRect(node, { x: r.left, y: r.top, width: r.width, height: r.height });
547
- node.hidden = false;
548
- }
549
- renderList(pool, targets, className) {
550
- while (pool.length < targets.length) {
551
- const el = this.div(className);
552
- el.hidden = true;
553
- this.layer.appendChild(el);
554
- pool.push(el);
555
- }
556
- for (let i = 0; i < pool.length; i++) {
557
- const node = pool[i];
558
- const target = targets[i];
559
- if (!target) {
560
- node.hidden = true;
561
- continue;
562
- }
563
- const r = target.getBoundingClientRect();
564
- if (r.width === 0 && r.height === 0) {
565
- node.hidden = true;
566
- continue;
567
- }
568
- this.placeRect(node, { x: r.left, y: r.top, width: r.width, height: r.height });
569
- node.hidden = false;
570
- }
571
- }
572
- placeRect(node, rect) {
573
- moveTo(node, rect.x, rect.y);
574
- node.style.width = `${rect.width}px`;
575
- node.style.height = `${rect.height}px`;
576
- }
577
- div(className) {
578
- const el = this.document.createElement("div");
579
- el.className = className;
580
- return el;
581
- }
582
- };
583
- function moveTo(node, x, y) {
584
- const tx = Math.round(x);
585
- const ty = Math.round(y);
586
- node.style.transform = `translate3d(${tx}px, ${ty}px, 0)`;
587
- }
588
- var TAG_LABELS = {
589
- p: "paragraph",
590
- h1: "heading",
591
- h2: "heading",
592
- h3: "heading",
593
- h4: "heading",
594
- h5: "heading",
595
- h6: "heading",
596
- a: "link",
597
- button: "button",
598
- input: "input",
599
- textarea: "textarea",
600
- select: "select",
601
- img: "image",
602
- video: "video",
603
- audio: "audio",
604
- form: "form",
605
- nav: "nav",
606
- header: "header",
607
- footer: "footer",
608
- main: "main",
609
- section: "section",
610
- article: "article",
611
- aside: "aside",
612
- ul: "list",
613
- ol: "list",
614
- li: "list item",
615
- table: "table",
616
- thead: "table head",
617
- tbody: "table body",
618
- tr: "table row",
619
- td: "cell",
620
- th: "header cell",
621
- span: "span",
622
- div: "div",
623
- label: "label",
624
- code: "code",
625
- pre: "code block",
626
- blockquote: "quote",
627
- strong: "bold",
628
- em: "italic",
629
- kbd: "key",
630
- svg: "svg",
631
- canvas: "canvas"
632
- };
633
- function describeElement(el) {
634
- const tag = el.tagName.toLowerCase();
635
- const type = TAG_LABELS[tag] ?? tag;
636
- const text = (el.textContent ?? "").replace(/\s+/g, " ").trim();
637
- if (text.length > 0) {
638
- const preview = text.length > 48 ? text.slice(0, 48) + "\u2026" : text;
639
- return `${type}: "${preview}"`;
640
- }
641
- if (el.id) return `${type}: #${el.id}`;
642
- const cls = el.classList[0];
643
- if (cls) return `${type}: .${cls}`;
644
- return type;
645
- }
646
- function unionOf(elements) {
647
- let minX = Infinity;
648
- let minY = Infinity;
649
- let maxX = -Infinity;
650
- let maxY = -Infinity;
651
- let any = false;
652
- for (const el of elements) {
653
- const r = el.getBoundingClientRect();
654
- if (r.width === 0 && r.height === 0) continue;
655
- any = true;
656
- if (r.left < minX) minX = r.left;
657
- if (r.top < minY) minY = r.top;
658
- if (r.right > maxX) maxX = r.right;
659
- if (r.bottom > maxY) maxY = r.bottom;
660
- }
661
- if (!any) return null;
662
- const PAD = 4;
663
- return {
664
- x: minX - PAD,
665
- y: minY - PAD,
666
- width: maxX - minX + PAD * 2,
667
- height: maxY - minY + PAD * 2
668
- };
669
- }
670
- var emptyRenderState = {
671
- hover: null,
672
- pinned: [],
673
- selection: null,
674
- marquee: null
675
- };
676
- var Overlay = class {
677
- renderer;
678
- engine;
679
- doc;
680
- win;
681
- searchRoot;
682
- excludeHost;
683
- raf;
684
- caf;
685
- state = emptyRenderState;
686
- unsubscribe = null;
687
- rafHandle = null;
688
- /** Guard for scheduleRender re-entry. Tracked separately from `rafHandle`
689
- * because a synchronous `raf` (used in tests) returns a handle the cb has
690
- * already invalidated — boolean is the safe sentinel. */
691
- renderPending = false;
692
- destroyed = false;
693
- bound = [];
694
- constructor(opts) {
695
- this.engine = opts.engine;
696
- this.doc = opts.document ?? opts.root.ownerDocument ?? null;
697
- if (!this.doc) throw new Error("Overlay: no Document available");
698
- this.win = this.doc.defaultView ?? globalThis;
699
- this.searchRoot = opts.searchRoot ?? this.doc.body;
700
- this.excludeHost = opts.excludeHost ?? null;
701
- this.raf = opts.raf ?? ((cb) => typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame(cb) : setTimeout(() => cb(performance.now()), 16));
702
- this.caf = opts.caf ?? ((h) => {
703
- if (typeof cancelAnimationFrame !== "undefined") cancelAnimationFrame(h);
704
- else clearTimeout(h);
705
- });
706
- this.renderer = new OverlayRenderer(opts.root, this.doc);
707
- this.bindEvent(this.doc, "scroll", this.onReposition, { passive: true, capture: true });
708
- this.bindEvent(this.win, "resize", this.onReposition, { passive: true });
709
- this.unsubscribe = this.engine.subscribe((s) => this.onEngineState(s));
710
- this.onEngineState(this.engine.getSnapshot());
711
- }
712
- destroy() {
713
- this.destroyed = true;
714
- if (this.unsubscribe) this.unsubscribe();
715
- this.unsubscribe = null;
716
- if (this.rafHandle !== null) this.caf(this.rafHandle);
717
- this.rafHandle = null;
718
- for (const [t, ev, fn, opts] of this.bound) t.removeEventListener(ev, fn, opts);
719
- this.bound = [];
720
- this.renderer.destroy();
721
- }
722
- /* ─── Internals ───────────────────────────────────────────────── */
723
- onReposition = () => {
724
- if (this.destroyed) return;
725
- if (hasAnythingToTrack(this.state)) this.scheduleRender();
726
- };
727
- onEngineState(s) {
728
- this.state = this.derive(s);
729
- this.scheduleRender();
730
- }
731
- /** Engine state → render state. */
732
- derive(s) {
733
- switch (s.kind) {
734
- case "idle":
735
- return emptyRenderState;
736
- case "inspect":
737
- return { hover: s.hoverTarget, pinned: s.pinned, selection: null, marquee: null };
738
- case "pressed":
739
- return { hover: s.target, pinned: s.pinned, selection: null, marquee: null };
740
- case "dragging": {
741
- const rect = rectFromPoints(s.start, s.current);
742
- return { hover: null, pinned: s.pinned, selection: null, marquee: rect };
743
- }
744
- case "annotating": {
745
- const sel = s.selection;
746
- const elements = sel.kind === "area" ? pickElementsInRect(this.searchRoot, sel.rect, this.excludeHost) : sel.kind === "multi" ? sel.elements : [sel.element];
747
- return { hover: null, pinned: s.pinned, selection: elements, marquee: null };
748
- }
749
- }
750
- }
751
- scheduleRender() {
752
- if (this.destroyed || this.renderPending) return;
753
- this.renderPending = true;
754
- this.rafHandle = this.raf(() => {
755
- this.renderPending = false;
756
- this.rafHandle = null;
757
- if (this.destroyed) return;
758
- this.renderer.render(this.state);
759
- });
760
- }
761
- bindEvent(target, ev, fn, opts) {
762
- target.addEventListener(ev, fn, opts);
763
- this.bound.push([target, ev, fn, opts]);
764
- }
765
- };
766
- function hasAnythingToTrack(s) {
767
- return s.hover !== null || s.pinned.length > 0 || s.selection !== null && s.selection.length > 0 || s.marquee !== null;
768
- }
769
- var FIBER_PROP_PREFIX = "__reactFiber$";
770
- function getFiber(node) {
771
- if (!node) return null;
772
- for (const key of Object.keys(node)) {
773
- if (key.startsWith(FIBER_PROP_PREFIX)) return node[key];
774
- }
775
- return null;
776
- }
777
- function getComponentChain(node) {
778
- const fiber = getFiber(node);
779
- if (!fiber) return [];
780
- const names = [];
781
- let cur = fiber;
782
- while (cur) {
783
- const name = getComponentName(cur);
784
- if (name) names.unshift(name);
785
- cur = cur.return;
786
- }
787
- return dedupeConsecutive(names);
788
- }
789
- function getComponentName(fiber) {
790
- const t = fiber.type;
791
- if (!t) return null;
792
- if (typeof t === "string") return null;
793
- if (typeof t === "function") {
794
- const fn = t;
795
- return fn.displayName || fn.name || null;
796
- }
797
- if (typeof t === "object") {
798
- const o = t;
799
- if (o.displayName) return o.displayName;
800
- if (o.render) return o.render.displayName || o.render.name || null;
801
- if (o.type) return o.type.displayName || o.type.name || null;
802
- }
803
- return null;
804
- }
805
- function dedupeConsecutive(names) {
806
- const out = [];
807
- for (const n of names) {
808
- if (out[out.length - 1] !== n) out.push(n);
809
- }
810
- return out;
811
- }
812
- function getSourceInfo(node) {
813
- const fiber = getFiber(node);
814
- if (!fiber) return null;
815
- let cur = fiber._debugOwner;
816
- while (cur) {
817
- if (cur._debugSource) return cur._debugSource;
818
- cur = cur._debugOwner;
819
- }
820
- cur = fiber;
821
- while (cur) {
822
- if (cur._debugSource) return cur._debugSource;
823
- cur = cur.return;
824
- }
825
- return null;
826
- }
827
- function collectAccessibility(el) {
828
- const parts = [];
829
- const role = el.getAttribute("role");
830
- if (role) parts.push(`role=${role}`);
831
- for (const attr of Array.from(el.attributes)) {
832
- if (attr.name.startsWith("aria-") && attr.value) {
833
- parts.push(`${attr.name}=${attr.value}`);
834
- }
835
- }
836
- const tabindex = el.getAttribute("tabindex");
837
- if (tabindex !== null && tabindex !== "") parts.push(`tabindex=${tabindex}`);
838
- const title = el.getAttribute("title");
839
- if (title) parts.push(`title=${title}`);
840
- return parts.join("; ");
841
- }
842
- var GROUPS = {
843
- layout: [
844
- "display",
845
- "position",
846
- "top",
847
- "right",
848
- "bottom",
849
- "left",
850
- "width",
851
- "height",
852
- "margin",
853
- "padding",
854
- "box-sizing"
855
- ],
856
- visual: [
857
- "color",
858
- "background-color",
859
- "border",
860
- "border-radius",
861
- "outline"
862
- ],
863
- text: [
864
- "font-family",
865
- "font-size",
866
- "font-weight",
867
- "line-height",
868
- "letter-spacing",
869
- "text-align",
870
- "white-space"
871
- ],
872
- flexgrid: [
873
- "flex-direction",
874
- "justify-content",
875
- "align-items",
876
- "gap",
877
- "grid-template-columns",
878
- "grid-template-rows"
879
- ],
880
- effects: ["transform", "box-shadow", "filter"],
881
- misc: ["cursor", "z-index", "overflow", "opacity", "transition", "animation"]
882
- };
883
- var TIER_GROUPS = {
884
- compact: [],
885
- standard: ["layout", "visual", "text"],
886
- detailed: ["layout", "visual", "text", "flexgrid", "effects"],
887
- forensic: ["layout", "visual", "text", "flexgrid", "effects", "misc"]
888
- };
889
- function collectComputedStyles(el, detail) {
890
- if (detail === "compact") return {};
891
- const doc = el.ownerDocument;
892
- const win = doc?.defaultView ?? globalThis;
893
- if (typeof win.getComputedStyle !== "function") return {};
894
- const cs = win.getComputedStyle(el);
895
- const props = /* @__PURE__ */ new Set();
896
- for (const g of TIER_GROUPS[detail]) {
897
- for (const p of GROUPS[g]) props.add(p);
898
- }
899
- const out = {};
900
- for (const p of props) {
901
- const v = cs.getPropertyValue(p);
902
- if (!v) continue;
903
- const trimmed = v.trim();
904
- if (isUninterestingDefault(p, trimmed)) continue;
905
- out[p] = trimmed;
906
- }
907
- return out;
908
- }
909
- function isUninterestingDefault(prop, value) {
910
- if (!value || value === "none" || value === "auto" || value === "normal") return true;
911
- if (value === "0px" || value === "0%") return true;
912
- if (value === "rgba(0, 0, 0, 0)") return true;
913
- if (prop === "color" || prop === "background-color") {
914
- return value === "rgba(0, 0, 0, 0)";
915
- }
916
- return false;
917
- }
918
- var SHORT_MAX_DEPTH = 5;
919
- var FULL_MAX_DEPTH = 8;
920
- function buildSelector(target, doc = target.ownerDocument ?? document) {
921
- if (target.id && isStableId(target.id)) {
922
- const escaped = cssEscape(target.id);
923
- const id = `#${escaped}`;
924
- return { short: id, full: id };
925
- }
926
- const segments = [];
927
- let cur = target;
928
- let short = null;
929
- for (let depth = 0; cur && depth < FULL_MAX_DEPTH; depth++) {
930
- segments.unshift(segmentFor(cur));
931
- const candidate = segments.join(" > ");
932
- if (short === null && depth < SHORT_MAX_DEPTH && isUnique(doc, candidate)) {
933
- short = candidate;
934
- }
935
- cur = cur.parentElement;
936
- if (!cur || cur === doc.documentElement || cur.tagName.toLowerCase() === "html") break;
937
- }
938
- const full = segments.join(" > ");
939
- return { short: short ?? full, full };
940
- }
941
- function segmentFor(el) {
942
- const tag = el.tagName.toLowerCase();
943
- if (el.id && isStableId(el.id)) return `${tag}#${cssEscape(el.id)}`;
944
- const classes = Array.from(el.classList).filter(isUseableClass).slice(0, 3);
945
- let segment = classes.length ? `${tag}.${classes.map(cssEscape).join(".")}` : tag;
946
- const parent = el.parentElement;
947
- if (parent) {
948
- const sameTag = Array.from(parent.children).filter(
949
- (c) => c.tagName === el.tagName
950
- );
951
- if (sameTag.length > 1) {
952
- const idx = sameTag.indexOf(el) + 1;
953
- if (idx > 0) segment += `:nth-of-type(${idx})`;
954
- }
955
- }
956
- return segment;
957
- }
958
- function isUseableClass(c) {
959
- if (!c) return false;
960
- if (c.length > 30) return false;
961
- if (/^css-[a-z0-9]+$/i.test(c)) return false;
962
- if (/^jsx-\d+$/i.test(c)) return false;
963
- if (/^_.+_[a-z0-9]{5,}$/i.test(c)) return false;
964
- if (/^\d/.test(c)) return false;
965
- return true;
966
- }
967
- function isStableId(id) {
968
- if (/^:[a-z]\d+:/i.test(id)) return false;
969
- if (/^radix-/i.test(id)) return false;
970
- if (/^headlessui-/i.test(id)) return false;
971
- return true;
972
- }
973
- function isUnique(doc, selector) {
974
- try {
975
- return doc.querySelectorAll(selector).length === 1;
976
- } catch {
977
- return false;
978
- }
979
- }
980
- function cssEscape(s) {
981
- const g = globalThis;
982
- if (g.CSS?.escape) return g.CSS.escape(s);
983
- return s.replace(/[^a-zA-Z0-9_-]/g, (c) => "\\" + c);
984
- }
985
- var NEARBY_MAX = 200;
986
- function collectNearbyText(el) {
987
- const own = readText(el);
988
- if (own) return truncate(own, NEARBY_MAX);
989
- const parent = el.parentElement;
990
- if (parent) {
991
- const t = readText(parent);
992
- if (t) return truncate(t, NEARBY_MAX);
993
- }
994
- return "";
995
- }
996
- function collectSelectedText(doc = document) {
997
- const win = doc.defaultView ?? globalThis;
998
- if (typeof win.getSelection !== "function") return "";
999
- const sel = win.getSelection();
1000
- if (!sel) return "";
1001
- return sel.toString().trim();
1002
- }
1003
- function readText(el) {
1004
- const t = el.innerText ?? el.textContent ?? "";
1005
- return t.replace(/\s+/g, " ").trim();
1006
- }
1007
- function truncate(s, n) {
1008
- return s.length <= n ? s : s.slice(0, n - 1) + "\u2026";
1009
- }
1010
- var POSITIONED_FIXED = /* @__PURE__ */ new Set(["fixed", "sticky"]);
1011
- function collectMetadata(el, options = {}) {
1012
- perfMark2("clickly:metadata:collect");
1013
- const detail = options.detail ?? "standard";
1014
- const includeReact = options.includeReact !== false && detail !== "compact";
1015
- const doc = options.document ?? el.ownerDocument ?? document;
1016
- const { short, full } = buildSelector(el, doc);
1017
- const rect = el.getBoundingClientRect();
1018
- const boundingBox = { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
1019
- const reactComponents = includeReact ? getComponentChain(el).join(" > ") : "";
1020
- const source = includeReact ? getSourceInfo(el) : null;
1021
- return {
1022
- element: el.tagName.toLowerCase(),
1023
- elementPath: short,
1024
- fullPath: full,
1025
- cssClasses: typeof el.className === "string" ? el.className.trim() : "",
1026
- computedStyles: collectComputedStyles(el, detail),
1027
- accessibility: collectAccessibility(el),
1028
- nearbyText: collectNearbyText(el),
1029
- selectedText: options.selectedText ?? collectSelectedText(doc),
1030
- boundingBox,
1031
- isFixed: hasFixedAncestor(el),
1032
- reactComponents,
1033
- sourceFile: source?.fileName ?? "",
1034
- sourceLine: source?.lineNumber ?? 0,
1035
- sourceColumn: source?.columnNumber ?? 0
1036
- };
1037
- }
1038
- function perfMark2(name) {
1039
- if (typeof performance !== "undefined" && typeof performance.mark === "function") {
1040
- try {
1041
- performance.mark(name);
1042
- } catch {
1043
- }
1044
- }
1045
- }
1046
- function hasFixedAncestor(el) {
1047
- const doc = el.ownerDocument;
1048
- const win = doc?.defaultView ?? globalThis;
1049
- if (typeof win.getComputedStyle !== "function") return false;
1050
- let cur = el;
1051
- for (let i = 0; cur && i < 8; i++) {
1052
- const pos = win.getComputedStyle(cur).getPropertyValue("position");
1053
- if (POSITIONED_FIXED.has(pos)) return true;
1054
- cur = cur.parentElement;
1055
- }
1056
- return false;
1057
- }
1058
-
1059
- // packages/react/src/internal/ClicklyRoot.tsx
1060
- var import_react10 = require("react");
37
+ // ../react/src/internal/ClicklyRoot.tsx
38
+ var import_react8 = require("react");
1061
39
 
1062
- // packages/react/src/internal/Toolbar.tsx
1063
- var import_react7 = require("react");
40
+ // ../react/src/internal/Toolbar.tsx
41
+ var import_react5 = require("react");
1064
42
 
1065
- // packages/react/src/hooks/useDraggable.ts
43
+ // ../react/src/hooks/useDraggable.ts
1066
44
  var import_react = require("react");
1067
45
  function useDraggable(defaultPos, size) {
1068
46
  const [position, setPosition] = (0, import_react.useState)(defaultPos);
@@ -1119,7 +97,7 @@ function clamp(n, lo, hi) {
1119
97
  return Math.max(lo, Math.min(hi, n));
1120
98
  }
1121
99
 
1122
- // packages/react/src/state/useEngineState.ts
100
+ // ../react/src/state/useEngineState.ts
1123
101
  var import_react2 = require("react");
1124
102
  function useEngineState(engine) {
1125
103
  return (0, import_react2.useSyncExternalStore)(
@@ -1131,119 +109,10 @@ function useEngineState(engine) {
1131
109
  }
1132
110
  var IDLE = { kind: "idle" };
1133
111
 
1134
- // node_modules/.pnpm/zustand@5.0.14_@types+react@18.3.31_react@18.3.1/node_modules/zustand/esm/vanilla.mjs
1135
- var createStoreImpl = (createState) => {
1136
- let state;
1137
- const listeners = /* @__PURE__ */ new Set();
1138
- const setState = (partial, replace) => {
1139
- const nextState = typeof partial === "function" ? partial(state) : partial;
1140
- if (!Object.is(nextState, state)) {
1141
- const previousState = state;
1142
- state = (replace != null ? replace : typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState);
1143
- listeners.forEach((listener) => listener(state, previousState));
1144
- }
1145
- };
1146
- const getState = () => state;
1147
- const getInitialState = () => initialState2;
1148
- const subscribe = (listener) => {
1149
- listeners.add(listener);
1150
- return () => listeners.delete(listener);
1151
- };
1152
- const api = { setState, getState, getInitialState, subscribe };
1153
- const initialState2 = state = createState(setState, getState, api);
1154
- return api;
1155
- };
1156
- var createStore = ((createState) => createState ? createStoreImpl(createState) : createStoreImpl);
1157
-
1158
- // node_modules/.pnpm/zustand@5.0.14_@types+react@18.3.31_react@18.3.1/node_modules/zustand/esm/react.mjs
1159
- var import_react3 = __toESM(require("react"), 1);
1160
- var identity = (arg) => arg;
1161
- function useStore(api, selector = identity) {
1162
- const slice = import_react3.default.useSyncExternalStore(
1163
- api.subscribe,
1164
- import_react3.default.useCallback(() => selector(api.getState()), [api, selector]),
1165
- import_react3.default.useCallback(() => selector(api.getInitialState()), [api, selector])
1166
- );
1167
- import_react3.default.useDebugValue(slice);
1168
- return slice;
1169
- }
1170
- var createImpl = (createState) => {
1171
- const api = createStore(createState);
1172
- const useBoundStore = (selector) => useStore(api, selector);
1173
- Object.assign(useBoundStore, api);
1174
- return useBoundStore;
1175
- };
1176
- var create = ((createState) => createState ? createImpl(createState) : createImpl);
1177
-
1178
- // node_modules/.pnpm/zustand@5.0.14_@types+react@18.3.31_react@18.3.1/node_modules/zustand/esm/react/shallow.mjs
1179
- var import_react4 = __toESM(require("react"), 1);
1180
-
1181
- // node_modules/.pnpm/zustand@5.0.14_@types+react@18.3.31_react@18.3.1/node_modules/zustand/esm/vanilla/shallow.mjs
1182
- var isIterable = (obj) => Symbol.iterator in obj;
1183
- var hasIterableEntries = (value) => (
1184
- // HACK: avoid checking entries type
1185
- "entries" in value
1186
- );
1187
- var compareEntries = (valueA, valueB) => {
1188
- const mapA = valueA instanceof Map ? valueA : new Map(valueA.entries());
1189
- const mapB = valueB instanceof Map ? valueB : new Map(valueB.entries());
1190
- if (mapA.size !== mapB.size) {
1191
- return false;
1192
- }
1193
- for (const [key, value] of mapA) {
1194
- if (!mapB.has(key) || !Object.is(value, mapB.get(key))) {
1195
- return false;
1196
- }
1197
- }
1198
- return true;
1199
- };
1200
- var compareIterables = (valueA, valueB) => {
1201
- const iteratorA = valueA[Symbol.iterator]();
1202
- const iteratorB = valueB[Symbol.iterator]();
1203
- let nextA = iteratorA.next();
1204
- let nextB = iteratorB.next();
1205
- while (!nextA.done && !nextB.done) {
1206
- if (!Object.is(nextA.value, nextB.value)) {
1207
- return false;
1208
- }
1209
- nextA = iteratorA.next();
1210
- nextB = iteratorB.next();
1211
- }
1212
- return !!nextA.done && !!nextB.done;
1213
- };
1214
- function shallow(valueA, valueB) {
1215
- if (Object.is(valueA, valueB)) {
1216
- return true;
1217
- }
1218
- if (typeof valueA !== "object" || valueA === null || typeof valueB !== "object" || valueB === null) {
1219
- return false;
1220
- }
1221
- if (Object.getPrototypeOf(valueA) !== Object.getPrototypeOf(valueB)) {
1222
- return false;
1223
- }
1224
- if (isIterable(valueA) && isIterable(valueB)) {
1225
- if (hasIterableEntries(valueA) && hasIterableEntries(valueB)) {
1226
- return compareEntries(valueA, valueB);
1227
- }
1228
- return compareIterables(valueA, valueB);
1229
- }
1230
- return compareEntries(
1231
- { entries: () => Object.entries(valueA) },
1232
- { entries: () => Object.entries(valueB) }
1233
- );
1234
- }
1235
-
1236
- // node_modules/.pnpm/zustand@5.0.14_@types+react@18.3.31_react@18.3.1/node_modules/zustand/esm/react/shallow.mjs
1237
- function useShallow(selector) {
1238
- const prev = import_react4.default.useRef(void 0);
1239
- return (state) => {
1240
- const next = selector(state);
1241
- return shallow(prev.current, next) ? prev.current : prev.current = next;
1242
- };
1243
- }
1244
-
1245
- // packages/react/src/state/annotations.ts
1246
- var useAnnotations = create((set, get) => ({
112
+ // ../react/src/state/annotations.ts
113
+ var import_zustand = require("zustand");
114
+ var import_shallow = require("zustand/react/shallow");
115
+ var useAnnotations = (0, import_zustand.create)((set, get) => ({
1247
116
  byId: {},
1248
117
  order: [],
1249
118
  add: (a) => set((s) => ({
@@ -1269,11 +138,12 @@ var useAnnotations = create((set, get) => ({
1269
138
  }));
1270
139
  function useAnnotationsList() {
1271
140
  return useAnnotations(
1272
- useShallow((s) => s.order.map((id) => s.byId[id]).filter(Boolean))
141
+ (0, import_shallow.useShallow)((s) => s.order.map((id) => s.byId[id]).filter(Boolean))
1273
142
  );
1274
143
  }
1275
144
 
1276
- // packages/react/src/state/settings.ts
145
+ // ../react/src/state/settings.ts
146
+ var import_zustand2 = require("zustand");
1277
147
  var DEFAULTS = {
1278
148
  outputDetail: "standard",
1279
149
  copyOnAdd: true,
@@ -1299,7 +169,7 @@ function persist(s) {
1299
169
  } catch {
1300
170
  }
1301
171
  }
1302
- var useSettings = create((set) => ({
172
+ var useSettings = (0, import_zustand2.create)((set) => ({
1303
173
  ...load(),
1304
174
  set: (patch) => set((cur) => {
1305
175
  const next = { ...cur, ...patch };
@@ -1312,7 +182,7 @@ var useSettings = create((set) => ({
1312
182
  }
1313
183
  }));
1314
184
 
1315
- // packages/react/src/output/markdown.ts
185
+ // ../react/src/output/markdown.ts
1316
186
  function annotationsToMarkdown(annotations, detail = "standard") {
1317
187
  if (!annotations.length) return "(no annotations)";
1318
188
  return annotations.map((a, i) => formatOne(a, i + 1, detail)).join("\n\n");
@@ -1335,7 +205,7 @@ function formatOne(a, index, detail) {
1335
205
  }
1336
206
  if (detail === "detailed" || detail === "forensic") {
1337
207
  if (a.reactComponents) lines.push(`**React:** ${a.reactComponents}`);
1338
- if (a.nearbyText) lines.push(`**Nearby text:** ${truncate2(a.nearbyText, 120)}`);
208
+ if (a.nearbyText) lines.push(`**Nearby text:** ${truncate(a.nearbyText, 120)}`);
1339
209
  }
1340
210
  if (detail === "forensic" && a.computedStyles) {
1341
211
  lines.push("**Computed styles:**\n```css\n" + a.computedStyles + "\n```");
@@ -1347,11 +217,11 @@ function formatOne(a, index, detail) {
1347
217
  if (a.severity) lines.push(`**Severity:** ${a.severity}`);
1348
218
  return lines.join("\n");
1349
219
  }
1350
- function truncate2(s, n) {
220
+ function truncate(s, n) {
1351
221
  return s.length <= n ? s : s.slice(0, n - 1) + "\u2026";
1352
222
  }
1353
223
 
1354
- // packages/react/src/internal/icons.tsx
224
+ // ../react/src/internal/icons.tsx
1355
225
  var import_jsx_runtime = require("react/jsx-runtime");
1356
226
  function Icon({
1357
227
  children,
@@ -1412,13 +282,13 @@ var IconClose = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, { childr
1412
282
  var IconCheck = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M5 12l5 5L20 7" }) });
1413
283
  var IconList = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01" }) });
1414
284
 
1415
- // packages/react/src/internal/SettingsPopover.tsx
1416
- var import_react5 = require("react");
285
+ // ../react/src/internal/SettingsPopover.tsx
286
+ var import_react3 = require("react");
1417
287
  var import_jsx_runtime2 = require("react/jsx-runtime");
1418
288
  function SettingsPopover({ anchor, width, onClose }) {
1419
- const ref = (0, import_react5.useRef)(null);
289
+ const ref = (0, import_react3.useRef)(null);
1420
290
  const s = useSettings();
1421
- (0, import_react5.useEffect)(() => {
291
+ (0, import_react3.useEffect)(() => {
1422
292
  const onDown = (e) => {
1423
293
  if (ref.current && e.composedPath().includes(ref.current)) return;
1424
294
  onClose();
@@ -1513,15 +383,15 @@ function SettingsPopover({ anchor, width, onClose }) {
1513
383
  ] });
1514
384
  }
1515
385
 
1516
- // packages/react/src/internal/AnnotationList.tsx
1517
- var import_react6 = require("react");
386
+ // ../react/src/internal/AnnotationList.tsx
387
+ var import_react4 = require("react");
1518
388
  var import_jsx_runtime3 = require("react/jsx-runtime");
1519
389
  function AnnotationList({ anchor, width, onClose }) {
1520
390
  const items = useAnnotationsList();
1521
391
  const remove = useAnnotations((s) => s.remove);
1522
392
  const outputDetail = useSettings((s) => s.outputDetail);
1523
- const ref = (0, import_react6.useRef)(null);
1524
- (0, import_react6.useEffect)(() => {
393
+ const ref = (0, import_react4.useRef)(null);
394
+ (0, import_react4.useEffect)(() => {
1525
395
  const onDown = (e) => {
1526
396
  if (ref.current && e.composedPath().includes(ref.current)) return;
1527
397
  onClose();
@@ -1555,7 +425,7 @@ function AnnotationCard({
1555
425
  outputDetail,
1556
426
  onRemove
1557
427
  }) {
1558
- const [copied, setCopied] = (0, import_react6.useState)(false);
428
+ const [copied, setCopied] = (0, import_react4.useState)(false);
1559
429
  const copyOne = async () => {
1560
430
  const md = formatOne(a, index, outputDetail);
1561
431
  try {
@@ -1601,7 +471,7 @@ function AnnotationCard({
1601
471
  ] });
1602
472
  }
1603
473
 
1604
- // packages/react/src/internal/Toolbar.tsx
474
+ // ../react/src/internal/Toolbar.tsx
1605
475
  var import_jsx_runtime4 = require("react/jsx-runtime");
1606
476
  var TOOLBAR_SIZE = { width: 360, height: 44 };
1607
477
  var SHORTCUTS = [
@@ -1616,7 +486,7 @@ var SHORTCUTS = [
1616
486
  { keys: ["X"], label: "Clear all annotations" }
1617
487
  ];
1618
488
  function ShortcutsPanel() {
1619
- const [visible, setVisible] = (0, import_react7.useState)(false);
489
+ const [visible, setVisible] = (0, import_react5.useState)(false);
1620
490
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1621
491
  "span",
1622
492
  {
@@ -1656,45 +526,14 @@ function Tip({
1656
526
  ] })
1657
527
  ] });
1658
528
  }
1659
- function Toolbar({ engine, onCollapse }) {
529
+ function Toolbar({ engine, onCollapse, isClosing, onCloseEnd, frozen, onFreezeToggle }) {
1660
530
  const state = useEngineState(engine);
1661
531
  const annotations = useAnnotationsList();
1662
532
  const clearAnnotations = useAnnotations((s) => s.clear);
1663
533
  const outputDetail = useSettings((s) => s.outputDetail);
1664
- const [showSettings, setShowSettings] = (0, import_react7.useState)(false);
1665
- const [showList, setShowList] = (0, import_react7.useState)(false);
1666
- const [frozen, setFrozen] = (0, import_react7.useState)(false);
1667
- const anchorRef = (0, import_react7.useRef)(null);
1668
- (0, import_react7.useEffect)(() => {
1669
- const STYLE_ID = "clickly-freeze-animations";
1670
- const gsap = window.gsap;
1671
- if (frozen) {
1672
- if (!document.getElementById(STYLE_ID)) {
1673
- const el = document.createElement("style");
1674
- el.id = STYLE_ID;
1675
- el.textContent = `
1676
- *, *::before, *::after {
1677
- animation-play-state: paused !important;
1678
- transition-duration: 0ms !important;
1679
- transition-delay: 0ms !important;
1680
- }
1681
- `;
1682
- document.head.appendChild(el);
1683
- }
1684
- if (gsap?.globalTimeline) {
1685
- gsap.globalTimeline.pause();
1686
- }
1687
- } else {
1688
- document.getElementById(STYLE_ID)?.remove();
1689
- if (gsap?.globalTimeline) {
1690
- gsap.globalTimeline.resume();
1691
- }
1692
- }
1693
- return () => {
1694
- document.getElementById(STYLE_ID)?.remove();
1695
- if (gsap?.globalTimeline) gsap.globalTimeline.resume();
1696
- };
1697
- }, [frozen]);
534
+ const [showSettings, setShowSettings] = (0, import_react5.useState)(false);
535
+ const [showList, setShowList] = (0, import_react5.useState)(false);
536
+ const anchorRef = (0, import_react5.useRef)(null);
1698
537
  const { position, handleProps } = useDraggable(
1699
538
  {
1700
539
  x: Math.max(8, window.innerWidth - TOOLBAR_SIZE.width - 16),
@@ -1720,8 +559,9 @@ function Toolbar({ engine, onCollapse }) {
1720
559
  "div",
1721
560
  {
1722
561
  ref: anchorRef,
1723
- className: "clickly-toolbar",
562
+ className: `clickly-toolbar${isClosing ? " is-closing" : ""}`,
1724
563
  style: { left: position.x, top: position.y, width: TOOLBAR_SIZE.width },
564
+ onAnimationEnd: isClosing ? onCloseEnd : void 0,
1725
565
  "aria-label": "Clickly toolbar",
1726
566
  children: [
1727
567
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Tip, { label: "Move", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
@@ -1781,7 +621,7 @@ function Toolbar({ engine, onCollapse }) {
1781
621
  "button",
1782
622
  {
1783
623
  className: `clickly-btn icon-only${frozen ? " is-freeze" : ""}`,
1784
- onClick: () => setFrozen((v) => !v),
624
+ onClick: onFreezeToggle,
1785
625
  "aria-label": frozen ? "Unfreeze page animations" : "Freeze page animations",
1786
626
  "aria-pressed": frozen,
1787
627
  children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(IconFreeze, {})
@@ -1861,7 +701,7 @@ function Toolbar({ engine, onCollapse }) {
1861
701
  );
1862
702
  }
1863
703
 
1864
- // packages/react/src/internal/CollapsedFAB.tsx
704
+ // ../react/src/internal/CollapsedFAB.tsx
1865
705
  var import_jsx_runtime5 = require("react/jsx-runtime");
1866
706
  function CollapsedFAB({ onExpand }) {
1867
707
  const annotations = useAnnotationsList();
@@ -1880,9 +720,24 @@ function CollapsedFAB({ onExpand }) {
1880
720
  );
1881
721
  }
1882
722
 
1883
- // packages/react/src/internal/AnnotationPopup.tsx
1884
- var import_react8 = require("react");
1885
- var import_nanoid = require("nanoid");
723
+ // ../react/src/internal/AnnotationPopup.tsx
724
+ var import_react6 = require("react");
725
+
726
+ // ../../node_modules/.pnpm/nanoid@5.1.16/node_modules/nanoid/url-alphabet/index.js
727
+ var urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
728
+
729
+ // ../../node_modules/.pnpm/nanoid@5.1.16/node_modules/nanoid/index.browser.js
730
+ var nanoid = (size = 21) => {
731
+ let id = "";
732
+ let bytes = crypto.getRandomValues(new Uint8Array(size |= 0));
733
+ while (size--) {
734
+ id += urlAlphabet[bytes[size] & 63];
735
+ }
736
+ return id;
737
+ };
738
+
739
+ // ../react/src/internal/AnnotationPopup.tsx
740
+ var import_core = require("@useclickly/core");
1886
741
  var import_jsx_runtime6 = require("react/jsx-runtime");
1887
742
  var POPUP_W = 320;
1888
743
  var POPUP_H_EST = 240;
@@ -1891,15 +746,15 @@ function AnnotationPopup({ engine }) {
1891
746
  const addAnnotation = useAnnotations((s) => s.add);
1892
747
  const copyOnAdd = useSettings((s) => s.copyOnAdd);
1893
748
  const outputDetail = useSettings((s) => s.outputDetail);
1894
- const [comment, setComment] = (0, import_react8.useState)("");
1895
- const [showStyles, setShowStyles] = (0, import_react8.useState)(false);
1896
- const [editMode, setEditMode] = (0, import_react8.useState)(false);
1897
- const [styles, setStyles] = (0, import_react8.useState)({});
1898
- const [editedStyles, setEditedStyles] = (0, import_react8.useState)({});
1899
- const targetElRef = (0, import_react8.useRef)(null);
1900
- const taRef = (0, import_react8.useRef)(null);
1901
- const popupRef = (0, import_react8.useRef)(null);
1902
- (0, import_react8.useEffect)(() => {
749
+ const [comment, setComment] = (0, import_react6.useState)("");
750
+ const [showStyles, setShowStyles] = (0, import_react6.useState)(false);
751
+ const [editMode, setEditMode] = (0, import_react6.useState)(false);
752
+ const [styles, setStyles] = (0, import_react6.useState)({});
753
+ const [editedStyles, setEditedStyles] = (0, import_react6.useState)({});
754
+ const targetElRef = (0, import_react6.useRef)(null);
755
+ const taRef = (0, import_react6.useRef)(null);
756
+ const popupRef = (0, import_react6.useRef)(null);
757
+ (0, import_react6.useEffect)(() => {
1903
758
  if (state.kind === "annotating") {
1904
759
  setComment("");
1905
760
  setShowStyles(false);
@@ -1909,14 +764,14 @@ function AnnotationPopup({ engine }) {
1909
764
  const el = sel?.kind === "single" ? sel.element : sel?.kind === "multi" || sel?.kind === "area" ? sel.elements[0] : null;
1910
765
  targetElRef.current = el instanceof HTMLElement ? el : null;
1911
766
  if (el) {
1912
- setStyles(collectComputedStyles(el, "standard"));
767
+ setStyles((0, import_core.collectComputedStyles)(el, "standard"));
1913
768
  } else {
1914
769
  setStyles({});
1915
770
  }
1916
771
  requestAnimationFrame(() => taRef.current?.focus());
1917
772
  }
1918
773
  }, [state.kind, engine]);
1919
- (0, import_react8.useEffect)(() => {
774
+ (0, import_react6.useEffect)(() => {
1920
775
  if (state.kind !== "annotating") return;
1921
776
  const onDown = (e) => {
1922
777
  if (popupRef.current && e.composedPath().includes(popupRef.current)) return;
@@ -1965,9 +820,9 @@ function AnnotationPopup({ engine }) {
1965
820
  const isMulti = sel.kind !== "single";
1966
821
  const showReact = useSettings.getState().showReactComponents;
1967
822
  for (const el of elements) {
1968
- const md = collectMetadata(el, { detail: outputDetail, includeReact: showReact });
823
+ const md = (0, import_core.collectMetadata)(el, { detail: outputDetail, includeReact: showReact });
1969
824
  const annotation = {
1970
- id: "ann_" + (0, import_nanoid.nanoid)(8),
825
+ id: "ann_" + nanoid(8),
1971
826
  comment: sharedComment,
1972
827
  element: md.element,
1973
828
  elementPath: md.elementPath,
@@ -2000,8 +855,8 @@ function AnnotationPopup({ engine }) {
2000
855
  }
2001
856
  engine.commit();
2002
857
  };
2003
- const [placement, setPlacement] = (0, import_react8.useState)(null);
2004
- const calcPlacement = (0, import_react8.useCallback)(() => {
858
+ const [placement, setPlacement] = (0, import_react6.useState)(null);
859
+ const calcPlacement = (0, import_react6.useCallback)(() => {
2005
860
  if (state.kind !== "annotating") {
2006
861
  setPlacement(null);
2007
862
  return;
@@ -2034,10 +889,10 @@ function AnnotationPopup({ engine }) {
2034
889
  if (left + POPUP_W > vw) left = Math.max(8, vw - POPUP_W - 8);
2035
890
  setPlacement({ top, left, label });
2036
891
  }, [state, engine]);
2037
- (0, import_react8.useEffect)(() => {
892
+ (0, import_react6.useEffect)(() => {
2038
893
  calcPlacement();
2039
894
  }, [calcPlacement]);
2040
- (0, import_react8.useEffect)(() => {
895
+ (0, import_react6.useEffect)(() => {
2041
896
  if (state.kind !== "annotating") return;
2042
897
  const onScroll = () => calcPlacement();
2043
898
  window.addEventListener("scroll", onScroll, { capture: true, passive: true });
@@ -2158,7 +1013,7 @@ function anchorRect(sel) {
2158
1013
  if (sel.elements.length === 0) return null;
2159
1014
  return sel.elements[0].getBoundingClientRect();
2160
1015
  }
2161
- var TAG_LABELS2 = {
1016
+ var TAG_LABELS = {
2162
1017
  p: "paragraph",
2163
1018
  h1: "heading",
2164
1019
  h2: "heading",
@@ -2200,9 +1055,9 @@ var TAG_LABELS2 = {
2200
1055
  svg: "svg",
2201
1056
  canvas: "canvas"
2202
1057
  };
2203
- function describeElement2(el) {
1058
+ function describeElement(el) {
2204
1059
  const tag = el.tagName.toLowerCase();
2205
- const type = TAG_LABELS2[tag] ?? tag;
1060
+ const type = TAG_LABELS[tag] ?? tag;
2206
1061
  const text = (el.textContent ?? "").replace(/\s+/g, " ").trim();
2207
1062
  if (text.length > 0) {
2208
1063
  const preview = text.length > 48 ? text.slice(0, 48) + "\u2026" : text;
@@ -2214,19 +1069,19 @@ function describeElement2(el) {
2214
1069
  return type;
2215
1070
  }
2216
1071
  function describeSelection(sel) {
2217
- if (sel.kind === "single") return describeElement2(sel.element);
1072
+ if (sel.kind === "single") return describeElement(sel.element);
2218
1073
  if (sel.kind === "area") return `area \xB7 ${sel.elements.length} element(s)`;
2219
- if (sel.elements.length === 1) return describeElement2(sel.elements[0]);
1074
+ if (sel.elements.length === 1) return describeElement(sel.elements[0]);
2220
1075
  return `${sel.elements.length} element(s)`;
2221
1076
  }
2222
1077
 
2223
- // packages/react/src/internal/AnnotationPins.tsx
2224
- var import_react9 = require("react");
1078
+ // ../react/src/internal/AnnotationPins.tsx
1079
+ var import_react7 = require("react");
2225
1080
  var import_jsx_runtime7 = require("react/jsx-runtime");
2226
1081
  function AnnotationPins() {
2227
1082
  const annotations = useAnnotationsList();
2228
- const [, setVersion] = (0, import_react9.useState)(0);
2229
- (0, import_react9.useEffect)(() => {
1083
+ const [, setVersion] = (0, import_react7.useState)(0);
1084
+ (0, import_react7.useEffect)(() => {
2230
1085
  let raf = null;
2231
1086
  const update = () => {
2232
1087
  if (raf !== null) return;
@@ -2245,7 +1100,7 @@ function AnnotationPins() {
2245
1100
  }, []);
2246
1101
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_jsx_runtime7.Fragment, { children: annotations.map((a, i) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Pin, { number: i + 1, annotation: a }, a.id)) });
2247
1102
  }
2248
- var TAG_LABELS3 = {
1103
+ var TAG_LABELS2 = {
2249
1104
  p: "paragraph",
2250
1105
  h1: "heading",
2251
1106
  h2: "heading",
@@ -2285,7 +1140,7 @@ var TAG_LABELS3 = {
2285
1140
  function pinLabel(annotation) {
2286
1141
  const raw = (annotation.element ?? "").toLowerCase();
2287
1142
  const tag = raw.split(/[.#\s]/)[0] ?? "";
2288
- return (TAG_LABELS3[tag] ?? tag) || "element";
1143
+ return (TAG_LABELS2[tag] ?? tag) || "element";
2289
1144
  }
2290
1145
  function parseStyles(raw) {
2291
1146
  if (!raw) return [];
@@ -2298,12 +1153,12 @@ function parseStyles(raw) {
2298
1153
  function Pin({ number, annotation }) {
2299
1154
  const remove = useAnnotations((s) => s.remove);
2300
1155
  const update = useAnnotations((s) => s.update);
2301
- const [hovered, setHovered] = (0, import_react9.useState)(false);
2302
- const [editing, setEditing] = (0, import_react9.useState)(false);
2303
- const [draft, setDraft] = (0, import_react9.useState)(annotation.comment);
2304
- const [showStyles, setShowStyles] = (0, import_react9.useState)(false);
2305
- const taRef = (0, import_react9.useRef)(null);
2306
- const editRef = (0, import_react9.useRef)(null);
1156
+ const [hovered, setHovered] = (0, import_react7.useState)(false);
1157
+ const [editing, setEditing] = (0, import_react7.useState)(false);
1158
+ const [draft, setDraft] = (0, import_react7.useState)(annotation.comment);
1159
+ const [showStyles, setShowStyles] = (0, import_react7.useState)(false);
1160
+ const taRef = (0, import_react7.useRef)(null);
1161
+ const editRef = (0, import_react7.useRef)(null);
2307
1162
  const styleEntries = parseStyles(annotation.computedStyles);
2308
1163
  const label = pinLabel(annotation);
2309
1164
  const headerLabel = annotation.isMultiSelect ? `${label} (multi-select)` : `${label}: "${annotation.elementPath}"`;
@@ -2326,7 +1181,7 @@ function Pin({ number, annotation }) {
2326
1181
  if (e.key === "Escape") closeEdit();
2327
1182
  if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) save();
2328
1183
  };
2329
- (0, import_react9.useEffect)(() => {
1184
+ (0, import_react7.useEffect)(() => {
2330
1185
  if (!editing) return;
2331
1186
  const onDown = (e) => {
2332
1187
  if (editRef.current && e.composedPath().includes(editRef.current)) return;
@@ -2335,7 +1190,7 @@ function Pin({ number, annotation }) {
2335
1190
  window.addEventListener("pointerdown", onDown, true);
2336
1191
  return () => window.removeEventListener("pointerdown", onDown, true);
2337
1192
  }, [editing]);
2338
- (0, import_react9.useEffect)(() => {
1193
+ (0, import_react7.useEffect)(() => {
2339
1194
  if (!editing) return;
2340
1195
  let el = null;
2341
1196
  if (annotation.elementPath) {
@@ -2463,28 +1318,63 @@ function resolvePosition(a) {
2463
1318
  return null;
2464
1319
  }
2465
1320
 
2466
- // packages/react/src/internal/ClicklyRoot.tsx
1321
+ // ../react/src/internal/ClicklyRoot.tsx
2467
1322
  var import_jsx_runtime8 = require("react/jsx-runtime");
2468
1323
  function ClicklyRoot({
2469
1324
  engine,
2470
1325
  host
2471
1326
  }) {
2472
- const [expanded, setExpanded] = (0, import_react10.useState)(false);
1327
+ const [expanded, setExpanded] = (0, import_react8.useState)(false);
1328
+ const [toolbarClosing, setToolbarClosing] = (0, import_react8.useState)(false);
1329
+ const [frozen, setFrozen] = (0, import_react8.useState)(false);
1330
+ const collapse = () => {
1331
+ setToolbarClosing(true);
1332
+ };
1333
+ const onToolbarCloseEnd = () => {
1334
+ setToolbarClosing(false);
1335
+ setExpanded(false);
1336
+ };
2473
1337
  const clearAnnotations = useAnnotations((s) => s.clear);
2474
1338
  const outputDetail = useSettings((s) => s.outputDetail);
2475
1339
  const markerColor = useSettings((s) => s.markerColor);
2476
1340
  const engineState = useEngineState(engine);
2477
- (0, import_react10.useEffect)(() => {
1341
+ (0, import_react8.useEffect)(() => {
2478
1342
  host.style.setProperty("--clickly-hover", markerColor);
2479
1343
  }, [host, markerColor]);
2480
- (0, import_react10.useEffect)(() => {
1344
+ (0, import_react8.useEffect)(() => {
1345
+ const STYLE_ID = "clickly-freeze-animations";
1346
+ const gsap = window.gsap;
1347
+ if (frozen) {
1348
+ if (!document.getElementById(STYLE_ID)) {
1349
+ const el = document.createElement("style");
1350
+ el.id = STYLE_ID;
1351
+ el.textContent = `
1352
+ *, *::before, *::after {
1353
+ animation-play-state: paused !important;
1354
+ transition-duration: 0ms !important;
1355
+ transition-delay: 0ms !important;
1356
+ }
1357
+ `;
1358
+ document.head.appendChild(el);
1359
+ }
1360
+ if (gsap?.globalTimeline) gsap.globalTimeline.pause();
1361
+ } else {
1362
+ document.getElementById(STYLE_ID)?.remove();
1363
+ if (gsap?.globalTimeline) gsap.globalTimeline.resume();
1364
+ }
1365
+ return () => {
1366
+ document.getElementById(STYLE_ID)?.remove();
1367
+ if (gsap?.globalTimeline) gsap.globalTimeline.resume();
1368
+ };
1369
+ }, [frozen]);
1370
+ (0, import_react8.useEffect)(() => {
2481
1371
  if (expanded) {
2482
1372
  if (engine.getSnapshot().kind === "idle") engine.activate("single");
2483
1373
  } else {
2484
1374
  if (engine.getSnapshot().kind !== "idle") engine.deactivate();
2485
1375
  }
2486
1376
  }, [expanded, engine]);
2487
- (0, import_react10.useEffect)(() => {
1377
+ (0, import_react8.useEffect)(() => {
2488
1378
  const active = engineState.kind !== "idle";
2489
1379
  if (active) document.body.setAttribute("data-clickly-active", "");
2490
1380
  else document.body.removeAttribute("data-clickly-active");
@@ -2502,7 +1392,7 @@ function ClicklyRoot({
2502
1392
  document.body.removeAttribute("data-clickly-annotating");
2503
1393
  };
2504
1394
  }, [engineState]);
2505
- (0, import_react10.useEffect)(() => {
1395
+ (0, import_react8.useEffect)(() => {
2506
1396
  const onKey = (e) => {
2507
1397
  const tag = e.target?.tagName;
2508
1398
  if (tag === "INPUT" || tag === "TEXTAREA") return;
@@ -2511,12 +1401,13 @@ function ClicklyRoot({
2511
1401
  if (active && (active.tagName === "TEXTAREA" || active.tagName === "INPUT")) return;
2512
1402
  if (e.key.toLowerCase() === "f" && e.shiftKey && (e.metaKey || e.ctrlKey)) {
2513
1403
  e.preventDefault();
2514
- setExpanded((v) => !v);
1404
+ if (expanded) collapse();
1405
+ else setExpanded(true);
2515
1406
  return;
2516
1407
  }
2517
1408
  if (e.key === "Escape") {
2518
1409
  if (engine.getSnapshot().kind === "annotating") return;
2519
- if (expanded) setExpanded(false);
1410
+ if (expanded) collapse();
2520
1411
  return;
2521
1412
  }
2522
1413
  if (!expanded) return;
@@ -2547,14 +1438,24 @@ function ClicklyRoot({
2547
1438
  }, [clearAnnotations, outputDetail, expanded, engine]);
2548
1439
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "clickly-ui", children: [
2549
1440
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(AnnotationPins, {}),
2550
- expanded ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
2551
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Toolbar, { engine, onCollapse: () => setExpanded(false) }),
2552
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(AnnotationPopup, { engine })
1441
+ expanded || toolbarClosing ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
1442
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1443
+ Toolbar,
1444
+ {
1445
+ engine,
1446
+ onCollapse: collapse,
1447
+ isClosing: toolbarClosing,
1448
+ onCloseEnd: onToolbarCloseEnd,
1449
+ frozen,
1450
+ onFreezeToggle: () => setFrozen((v) => !v)
1451
+ }
1452
+ ),
1453
+ !toolbarClosing && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(AnnotationPopup, { engine })
2553
1454
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CollapsedFAB, { onExpand: () => setExpanded(true) })
2554
1455
  ] });
2555
1456
  }
2556
1457
 
2557
- // packages/react/src/internal/styles.ts
1458
+ // ../react/src/internal/styles.ts
2558
1459
  var REACT_UI_CSS = `
2559
1460
  .clickly-ui, .clickly-ui * { box-sizing: border-box; }
2560
1461
 
@@ -2580,6 +1481,11 @@ var REACT_UI_CSS = `
2580
1481
  0 0 0 1px rgba(255,255,255,0.06) inset;
2581
1482
  transition: transform 140ms ease, box-shadow 140ms ease;
2582
1483
  z-index: 1;
1484
+ animation: clickly-fab-open 280ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
1485
+ }
1486
+ @keyframes clickly-fab-open {
1487
+ from { opacity: 0; transform: scale(0.5); }
1488
+ to { opacity: 1; transform: scale(1); }
2583
1489
  }
2584
1490
  .clickly-fab:hover {
2585
1491
  transform: scale(1.06);
@@ -2626,12 +1532,21 @@ var REACT_UI_CSS = `
2626
1532
  pointer-events: auto;
2627
1533
  user-select: none;
2628
1534
  z-index: 1;
2629
- animation: clickly-fade-in 150ms cubic-bezier(0.16, 1, 0.3, 1);
1535
+ transform-origin: bottom right;
1536
+ animation: clickly-toolbar-open 240ms cubic-bezier(0.16, 1, 0.3, 1) both;
1537
+ }
1538
+ .clickly-toolbar.is-closing {
1539
+ animation: clickly-toolbar-close 200ms cubic-bezier(0.4, 0, 1, 1) both;
1540
+ pointer-events: none;
2630
1541
  }
2631
1542
 
2632
- @keyframes clickly-fade-in {
2633
- from { opacity: 0; transform: translateY(6px) scale(0.97); }
2634
- to { opacity: 1; transform: translateY(0) scale(1); }
1543
+ @keyframes clickly-toolbar-open {
1544
+ from { opacity: 0; transform: scale(0.82) translateY(10px); }
1545
+ to { opacity: 1; transform: scale(1) translateY(0); }
1546
+ }
1547
+ @keyframes clickly-toolbar-close {
1548
+ from { opacity: 1; transform: scale(1) translateY(0); }
1549
+ to { opacity: 0; transform: scale(0.82) translateY(10px); }
2635
1550
  }
2636
1551
 
2637
1552
  .clickly-toolbar .grip {
@@ -3603,7 +2518,7 @@ var REACT_UI_CSS = `
3603
2518
  }
3604
2519
  `;
3605
2520
 
3606
- // packages/react/src/internal/globalStyles.ts
2521
+ // ../react/src/internal/globalStyles.ts
3607
2522
  var GLOBAL_PAGE_CSS = `
3608
2523
  body[data-clickly-active] {
3609
2524
  -webkit-user-select: none !important;
@@ -3624,18 +2539,18 @@ body[data-clickly-annotating] {
3624
2539
  }
3625
2540
  `;
3626
2541
 
3627
- // packages/react/src/Clickly.tsx
2542
+ // ../react/src/Clickly.tsx
3628
2543
  var import_jsx_runtime9 = require("react/jsx-runtime");
3629
2544
  function Clickly({ className } = {}) {
3630
- const [mount, setMount] = (0, import_react11.useState)(null);
3631
- (0, import_react11.useEffect)(() => {
2545
+ const [mount, setMount] = (0, import_react9.useState)(null);
2546
+ (0, import_react9.useEffect)(() => {
3632
2547
  if (typeof window === "undefined" || typeof document === "undefined") return;
3633
2548
  let shadow = null;
3634
2549
  let engine = null;
3635
2550
  let overlay = null;
3636
2551
  let portal = null;
3637
2552
  try {
3638
- shadow = createShadowHost({ document });
2553
+ shadow = (0, import_core2.createShadowHost)({ document });
3639
2554
  portal = document.createElement("div");
3640
2555
  portal.setAttribute("data-clickly-react-root", "");
3641
2556
  shadow.root.appendChild(portal);
@@ -3651,8 +2566,8 @@ function Clickly({ className } = {}) {
3651
2566
  gstyle.textContent = GLOBAL_PAGE_CSS;
3652
2567
  document.head.appendChild(gstyle);
3653
2568
  }
3654
- engine = new SelectionEngine({ document, host: shadow.host });
3655
- overlay = new Overlay({
2569
+ engine = new import_core2.SelectionEngine({ document, host: shadow.host });
2570
+ overlay = new import_core2.Overlay({
3656
2571
  engine,
3657
2572
  root: shadow.root,
3658
2573
  document,
@@ -3677,7 +2592,7 @@ function Clickly({ className } = {}) {
3677
2592
  document.body.removeAttribute("data-clickly-mode");
3678
2593
  };
3679
2594
  }, []);
3680
- (0, import_react11.useEffect)(() => {
2595
+ (0, import_react9.useEffect)(() => {
3681
2596
  if (!mount || !className) return;
3682
2597
  mount.shadow.host.className = className;
3683
2598
  return () => {