@useclickly/react 1.0.2 → 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.
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,
@@ -1373,6 +243,18 @@ function Icon({
1373
243
  }
1374
244
  );
1375
245
  }
246
+ var IconFreeze = () => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Icon, { size: 15, children: [
247
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "12", y1: "2", x2: "12", y2: "22" }),
248
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "2", y1: "12", x2: "22", y2: "12" }),
249
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "5", y1: "5", x2: "19", y2: "19" }),
250
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "19", y1: "5", x2: "5", y2: "19" }),
251
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "12", cy: "12", r: "2", fill: "currentColor", stroke: "none" })
252
+ ] });
253
+ var IconInfo = () => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Icon, { size: 14, children: [
254
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
255
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "12", y1: "8", x2: "12", y2: "8", strokeWidth: "2.5" }),
256
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "12", y1: "12", x2: "12", y2: "16" })
257
+ ] });
1376
258
  var IconCursor = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M4 4l6 16 2-7 7-2z" }) });
1377
259
  var IconLayers = () => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Icon, { children: [
1378
260
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M12 2l9 5-9 5-9-5 9-5z" }),
@@ -1400,13 +282,13 @@ var IconClose = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, { childr
1400
282
  var IconCheck = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M5 12l5 5L20 7" }) });
1401
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" }) });
1402
284
 
1403
- // packages/react/src/internal/SettingsPopover.tsx
1404
- var import_react5 = require("react");
285
+ // ../react/src/internal/SettingsPopover.tsx
286
+ var import_react3 = require("react");
1405
287
  var import_jsx_runtime2 = require("react/jsx-runtime");
1406
288
  function SettingsPopover({ anchor, width, onClose }) {
1407
- const ref = (0, import_react5.useRef)(null);
289
+ const ref = (0, import_react3.useRef)(null);
1408
290
  const s = useSettings();
1409
- (0, import_react5.useEffect)(() => {
291
+ (0, import_react3.useEffect)(() => {
1410
292
  const onDown = (e) => {
1411
293
  if (ref.current && e.composedPath().includes(ref.current)) return;
1412
294
  onClose();
@@ -1501,15 +383,15 @@ function SettingsPopover({ anchor, width, onClose }) {
1501
383
  ] });
1502
384
  }
1503
385
 
1504
- // packages/react/src/internal/AnnotationList.tsx
1505
- var import_react6 = require("react");
386
+ // ../react/src/internal/AnnotationList.tsx
387
+ var import_react4 = require("react");
1506
388
  var import_jsx_runtime3 = require("react/jsx-runtime");
1507
389
  function AnnotationList({ anchor, width, onClose }) {
1508
390
  const items = useAnnotationsList();
1509
391
  const remove = useAnnotations((s) => s.remove);
1510
392
  const outputDetail = useSettings((s) => s.outputDetail);
1511
- const ref = (0, import_react6.useRef)(null);
1512
- (0, import_react6.useEffect)(() => {
393
+ const ref = (0, import_react4.useRef)(null);
394
+ (0, import_react4.useEffect)(() => {
1513
395
  const onDown = (e) => {
1514
396
  if (ref.current && e.composedPath().includes(ref.current)) return;
1515
397
  onClose();
@@ -1543,7 +425,7 @@ function AnnotationCard({
1543
425
  outputDetail,
1544
426
  onRemove
1545
427
  }) {
1546
- const [copied, setCopied] = (0, import_react6.useState)(false);
428
+ const [copied, setCopied] = (0, import_react4.useState)(false);
1547
429
  const copyOne = async () => {
1548
430
  const md = formatOne(a, index, outputDetail);
1549
431
  try {
@@ -1589,9 +471,48 @@ function AnnotationCard({
1589
471
  ] });
1590
472
  }
1591
473
 
1592
- // packages/react/src/internal/Toolbar.tsx
474
+ // ../react/src/internal/Toolbar.tsx
1593
475
  var import_jsx_runtime4 = require("react/jsx-runtime");
1594
- var TOOLBAR_SIZE = { width: 320, height: 44 };
476
+ var TOOLBAR_SIZE = { width: 360, height: 44 };
477
+ var SHORTCUTS = [
478
+ { keys: ["\u2318", "\u21E7", "F"], label: "Toggle toolbar" },
479
+ { keys: ["1"], label: "Single select mode" },
480
+ { keys: ["2"], label: "Multi select mode" },
481
+ { keys: ["3"], label: "Area drag mode" },
482
+ { keys: ["\u21B5"], label: "Annotate pinned elements" },
483
+ { keys: ["Esc"], label: "Cancel / clear / close" },
484
+ { keys: ["\u2318", "\u21B5"], label: "Submit annotation" },
485
+ { keys: ["C"], label: "Copy all annotations" },
486
+ { keys: ["X"], label: "Clear all annotations" }
487
+ ];
488
+ function ShortcutsPanel() {
489
+ const [visible, setVisible] = (0, import_react5.useState)(false);
490
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
491
+ "span",
492
+ {
493
+ className: "clickly-tip shortcuts-trigger",
494
+ onMouseEnter: () => setVisible(true),
495
+ onMouseLeave: () => setVisible(false),
496
+ children: [
497
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
498
+ "button",
499
+ {
500
+ className: "clickly-btn icon-only",
501
+ "aria-label": "Show keyboard shortcuts",
502
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(IconInfo, {})
503
+ }
504
+ ),
505
+ visible && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "shortcuts-panel", role: "tooltip", children: [
506
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "shortcuts-title", children: "Keyboard shortcuts" }),
507
+ SHORTCUTS.map((s) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "shortcuts-row", children: [
508
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "shortcuts-label", children: s.label }),
509
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "shortcuts-keys", children: s.keys.map((k) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("kbd", { children: k }, k)) })
510
+ ] }, s.label))
511
+ ] })
512
+ ]
513
+ }
514
+ );
515
+ }
1595
516
  function Tip({
1596
517
  label,
1597
518
  shortcut,
@@ -1605,14 +526,14 @@ function Tip({
1605
526
  ] })
1606
527
  ] });
1607
528
  }
1608
- function Toolbar({ engine, onCollapse }) {
529
+ function Toolbar({ engine, onCollapse, isClosing, onCloseEnd, frozen, onFreezeToggle }) {
1609
530
  const state = useEngineState(engine);
1610
531
  const annotations = useAnnotationsList();
1611
532
  const clearAnnotations = useAnnotations((s) => s.clear);
1612
533
  const outputDetail = useSettings((s) => s.outputDetail);
1613
- const [showSettings, setShowSettings] = (0, import_react7.useState)(false);
1614
- const [showList, setShowList] = (0, import_react7.useState)(false);
1615
- const anchorRef = (0, import_react7.useRef)(null);
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);
1616
537
  const { position, handleProps } = useDraggable(
1617
538
  {
1618
539
  x: Math.max(8, window.innerWidth - TOOLBAR_SIZE.width - 16),
@@ -1638,8 +559,9 @@ function Toolbar({ engine, onCollapse }) {
1638
559
  "div",
1639
560
  {
1640
561
  ref: anchorRef,
1641
- className: "clickly-toolbar",
562
+ className: `clickly-toolbar${isClosing ? " is-closing" : ""}`,
1642
563
  style: { left: position.x, top: position.y, width: TOOLBAR_SIZE.width },
564
+ onAnimationEnd: isClosing ? onCloseEnd : void 0,
1643
565
  "aria-label": "Clickly toolbar",
1644
566
  children: [
1645
567
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Tip, { label: "Move", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
@@ -1695,6 +617,16 @@ function Toolbar({ engine, onCollapse }) {
1695
617
  ) }),
1696
618
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "divider" })
1697
619
  ] }),
620
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Tip, { label: frozen ? "Unfreeze animations" : "Freeze animations", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
621
+ "button",
622
+ {
623
+ className: `clickly-btn icon-only${frozen ? " is-freeze" : ""}`,
624
+ onClick: onFreezeToggle,
625
+ "aria-label": frozen ? "Unfreeze page animations" : "Freeze page animations",
626
+ "aria-pressed": frozen,
627
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(IconFreeze, {})
628
+ }
629
+ ) }),
1698
630
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Tip, { label: "Annotations", shortcut: "L", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1699
631
  "button",
1700
632
  {
@@ -1728,6 +660,7 @@ function Toolbar({ engine, onCollapse }) {
1728
660
  }
1729
661
  ) }),
1730
662
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "divider" }),
663
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ShortcutsPanel, {}),
1731
664
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Tip, { label: "Settings", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1732
665
  "button",
1733
666
  {
@@ -1768,7 +701,7 @@ function Toolbar({ engine, onCollapse }) {
1768
701
  );
1769
702
  }
1770
703
 
1771
- // packages/react/src/internal/CollapsedFAB.tsx
704
+ // ../react/src/internal/CollapsedFAB.tsx
1772
705
  var import_jsx_runtime5 = require("react/jsx-runtime");
1773
706
  function CollapsedFAB({ onExpand }) {
1774
707
  const annotations = useAnnotationsList();
@@ -1787,9 +720,24 @@ function CollapsedFAB({ onExpand }) {
1787
720
  );
1788
721
  }
1789
722
 
1790
- // packages/react/src/internal/AnnotationPopup.tsx
1791
- var import_react8 = require("react");
1792
- 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");
1793
741
  var import_jsx_runtime6 = require("react/jsx-runtime");
1794
742
  var POPUP_W = 320;
1795
743
  var POPUP_H_EST = 240;
@@ -1798,15 +746,15 @@ function AnnotationPopup({ engine }) {
1798
746
  const addAnnotation = useAnnotations((s) => s.add);
1799
747
  const copyOnAdd = useSettings((s) => s.copyOnAdd);
1800
748
  const outputDetail = useSettings((s) => s.outputDetail);
1801
- const [comment, setComment] = (0, import_react8.useState)("");
1802
- const [showStyles, setShowStyles] = (0, import_react8.useState)(false);
1803
- const [editMode, setEditMode] = (0, import_react8.useState)(false);
1804
- const [styles, setStyles] = (0, import_react8.useState)({});
1805
- const [editedStyles, setEditedStyles] = (0, import_react8.useState)({});
1806
- const targetElRef = (0, import_react8.useRef)(null);
1807
- const taRef = (0, import_react8.useRef)(null);
1808
- const popupRef = (0, import_react8.useRef)(null);
1809
- (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)(() => {
1810
758
  if (state.kind === "annotating") {
1811
759
  setComment("");
1812
760
  setShowStyles(false);
@@ -1816,14 +764,14 @@ function AnnotationPopup({ engine }) {
1816
764
  const el = sel?.kind === "single" ? sel.element : sel?.kind === "multi" || sel?.kind === "area" ? sel.elements[0] : null;
1817
765
  targetElRef.current = el instanceof HTMLElement ? el : null;
1818
766
  if (el) {
1819
- setStyles(collectComputedStyles(el, "standard"));
767
+ setStyles((0, import_core.collectComputedStyles)(el, "standard"));
1820
768
  } else {
1821
769
  setStyles({});
1822
770
  }
1823
771
  requestAnimationFrame(() => taRef.current?.focus());
1824
772
  }
1825
773
  }, [state.kind, engine]);
1826
- (0, import_react8.useEffect)(() => {
774
+ (0, import_react6.useEffect)(() => {
1827
775
  if (state.kind !== "annotating") return;
1828
776
  const onDown = (e) => {
1829
777
  if (popupRef.current && e.composedPath().includes(popupRef.current)) return;
@@ -1872,9 +820,9 @@ function AnnotationPopup({ engine }) {
1872
820
  const isMulti = sel.kind !== "single";
1873
821
  const showReact = useSettings.getState().showReactComponents;
1874
822
  for (const el of elements) {
1875
- const md = collectMetadata(el, { detail: outputDetail, includeReact: showReact });
823
+ const md = (0, import_core.collectMetadata)(el, { detail: outputDetail, includeReact: showReact });
1876
824
  const annotation = {
1877
- id: "ann_" + (0, import_nanoid.nanoid)(8),
825
+ id: "ann_" + nanoid(8),
1878
826
  comment: sharedComment,
1879
827
  element: md.element,
1880
828
  elementPath: md.elementPath,
@@ -1907,8 +855,8 @@ function AnnotationPopup({ engine }) {
1907
855
  }
1908
856
  engine.commit();
1909
857
  };
1910
- const [placement, setPlacement] = (0, import_react8.useState)(null);
1911
- const calcPlacement = (0, import_react8.useCallback)(() => {
858
+ const [placement, setPlacement] = (0, import_react6.useState)(null);
859
+ const calcPlacement = (0, import_react6.useCallback)(() => {
1912
860
  if (state.kind !== "annotating") {
1913
861
  setPlacement(null);
1914
862
  return;
@@ -1941,10 +889,10 @@ function AnnotationPopup({ engine }) {
1941
889
  if (left + POPUP_W > vw) left = Math.max(8, vw - POPUP_W - 8);
1942
890
  setPlacement({ top, left, label });
1943
891
  }, [state, engine]);
1944
- (0, import_react8.useEffect)(() => {
892
+ (0, import_react6.useEffect)(() => {
1945
893
  calcPlacement();
1946
894
  }, [calcPlacement]);
1947
- (0, import_react8.useEffect)(() => {
895
+ (0, import_react6.useEffect)(() => {
1948
896
  if (state.kind !== "annotating") return;
1949
897
  const onScroll = () => calcPlacement();
1950
898
  window.addEventListener("scroll", onScroll, { capture: true, passive: true });
@@ -2065,7 +1013,7 @@ function anchorRect(sel) {
2065
1013
  if (sel.elements.length === 0) return null;
2066
1014
  return sel.elements[0].getBoundingClientRect();
2067
1015
  }
2068
- var TAG_LABELS2 = {
1016
+ var TAG_LABELS = {
2069
1017
  p: "paragraph",
2070
1018
  h1: "heading",
2071
1019
  h2: "heading",
@@ -2107,9 +1055,9 @@ var TAG_LABELS2 = {
2107
1055
  svg: "svg",
2108
1056
  canvas: "canvas"
2109
1057
  };
2110
- function describeElement2(el) {
1058
+ function describeElement(el) {
2111
1059
  const tag = el.tagName.toLowerCase();
2112
- const type = TAG_LABELS2[tag] ?? tag;
1060
+ const type = TAG_LABELS[tag] ?? tag;
2113
1061
  const text = (el.textContent ?? "").replace(/\s+/g, " ").trim();
2114
1062
  if (text.length > 0) {
2115
1063
  const preview = text.length > 48 ? text.slice(0, 48) + "\u2026" : text;
@@ -2121,19 +1069,19 @@ function describeElement2(el) {
2121
1069
  return type;
2122
1070
  }
2123
1071
  function describeSelection(sel) {
2124
- if (sel.kind === "single") return describeElement2(sel.element);
1072
+ if (sel.kind === "single") return describeElement(sel.element);
2125
1073
  if (sel.kind === "area") return `area \xB7 ${sel.elements.length} element(s)`;
2126
- if (sel.elements.length === 1) return describeElement2(sel.elements[0]);
1074
+ if (sel.elements.length === 1) return describeElement(sel.elements[0]);
2127
1075
  return `${sel.elements.length} element(s)`;
2128
1076
  }
2129
1077
 
2130
- // packages/react/src/internal/AnnotationPins.tsx
2131
- var import_react9 = require("react");
1078
+ // ../react/src/internal/AnnotationPins.tsx
1079
+ var import_react7 = require("react");
2132
1080
  var import_jsx_runtime7 = require("react/jsx-runtime");
2133
1081
  function AnnotationPins() {
2134
1082
  const annotations = useAnnotationsList();
2135
- const [, setVersion] = (0, import_react9.useState)(0);
2136
- (0, import_react9.useEffect)(() => {
1083
+ const [, setVersion] = (0, import_react7.useState)(0);
1084
+ (0, import_react7.useEffect)(() => {
2137
1085
  let raf = null;
2138
1086
  const update = () => {
2139
1087
  if (raf !== null) return;
@@ -2152,7 +1100,7 @@ function AnnotationPins() {
2152
1100
  }, []);
2153
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)) });
2154
1102
  }
2155
- var TAG_LABELS3 = {
1103
+ var TAG_LABELS2 = {
2156
1104
  p: "paragraph",
2157
1105
  h1: "heading",
2158
1106
  h2: "heading",
@@ -2192,7 +1140,7 @@ var TAG_LABELS3 = {
2192
1140
  function pinLabel(annotation) {
2193
1141
  const raw = (annotation.element ?? "").toLowerCase();
2194
1142
  const tag = raw.split(/[.#\s]/)[0] ?? "";
2195
- return (TAG_LABELS3[tag] ?? tag) || "element";
1143
+ return (TAG_LABELS2[tag] ?? tag) || "element";
2196
1144
  }
2197
1145
  function parseStyles(raw) {
2198
1146
  if (!raw) return [];
@@ -2205,12 +1153,12 @@ function parseStyles(raw) {
2205
1153
  function Pin({ number, annotation }) {
2206
1154
  const remove = useAnnotations((s) => s.remove);
2207
1155
  const update = useAnnotations((s) => s.update);
2208
- const [hovered, setHovered] = (0, import_react9.useState)(false);
2209
- const [editing, setEditing] = (0, import_react9.useState)(false);
2210
- const [draft, setDraft] = (0, import_react9.useState)(annotation.comment);
2211
- const [showStyles, setShowStyles] = (0, import_react9.useState)(false);
2212
- const taRef = (0, import_react9.useRef)(null);
2213
- 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);
2214
1162
  const styleEntries = parseStyles(annotation.computedStyles);
2215
1163
  const label = pinLabel(annotation);
2216
1164
  const headerLabel = annotation.isMultiSelect ? `${label} (multi-select)` : `${label}: "${annotation.elementPath}"`;
@@ -2233,7 +1181,7 @@ function Pin({ number, annotation }) {
2233
1181
  if (e.key === "Escape") closeEdit();
2234
1182
  if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) save();
2235
1183
  };
2236
- (0, import_react9.useEffect)(() => {
1184
+ (0, import_react7.useEffect)(() => {
2237
1185
  if (!editing) return;
2238
1186
  const onDown = (e) => {
2239
1187
  if (editRef.current && e.composedPath().includes(editRef.current)) return;
@@ -2242,7 +1190,7 @@ function Pin({ number, annotation }) {
2242
1190
  window.addEventListener("pointerdown", onDown, true);
2243
1191
  return () => window.removeEventListener("pointerdown", onDown, true);
2244
1192
  }, [editing]);
2245
- (0, import_react9.useEffect)(() => {
1193
+ (0, import_react7.useEffect)(() => {
2246
1194
  if (!editing) return;
2247
1195
  let el = null;
2248
1196
  if (annotation.elementPath) {
@@ -2370,28 +1318,63 @@ function resolvePosition(a) {
2370
1318
  return null;
2371
1319
  }
2372
1320
 
2373
- // packages/react/src/internal/ClicklyRoot.tsx
1321
+ // ../react/src/internal/ClicklyRoot.tsx
2374
1322
  var import_jsx_runtime8 = require("react/jsx-runtime");
2375
1323
  function ClicklyRoot({
2376
1324
  engine,
2377
1325
  host
2378
1326
  }) {
2379
- 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
+ };
2380
1337
  const clearAnnotations = useAnnotations((s) => s.clear);
2381
1338
  const outputDetail = useSettings((s) => s.outputDetail);
2382
1339
  const markerColor = useSettings((s) => s.markerColor);
2383
1340
  const engineState = useEngineState(engine);
2384
- (0, import_react10.useEffect)(() => {
1341
+ (0, import_react8.useEffect)(() => {
2385
1342
  host.style.setProperty("--clickly-hover", markerColor);
2386
1343
  }, [host, markerColor]);
2387
- (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)(() => {
2388
1371
  if (expanded) {
2389
1372
  if (engine.getSnapshot().kind === "idle") engine.activate("single");
2390
1373
  } else {
2391
1374
  if (engine.getSnapshot().kind !== "idle") engine.deactivate();
2392
1375
  }
2393
1376
  }, [expanded, engine]);
2394
- (0, import_react10.useEffect)(() => {
1377
+ (0, import_react8.useEffect)(() => {
2395
1378
  const active = engineState.kind !== "idle";
2396
1379
  if (active) document.body.setAttribute("data-clickly-active", "");
2397
1380
  else document.body.removeAttribute("data-clickly-active");
@@ -2409,7 +1392,7 @@ function ClicklyRoot({
2409
1392
  document.body.removeAttribute("data-clickly-annotating");
2410
1393
  };
2411
1394
  }, [engineState]);
2412
- (0, import_react10.useEffect)(() => {
1395
+ (0, import_react8.useEffect)(() => {
2413
1396
  const onKey = (e) => {
2414
1397
  const tag = e.target?.tagName;
2415
1398
  if (tag === "INPUT" || tag === "TEXTAREA") return;
@@ -2418,12 +1401,13 @@ function ClicklyRoot({
2418
1401
  if (active && (active.tagName === "TEXTAREA" || active.tagName === "INPUT")) return;
2419
1402
  if (e.key.toLowerCase() === "f" && e.shiftKey && (e.metaKey || e.ctrlKey)) {
2420
1403
  e.preventDefault();
2421
- setExpanded((v) => !v);
1404
+ if (expanded) collapse();
1405
+ else setExpanded(true);
2422
1406
  return;
2423
1407
  }
2424
1408
  if (e.key === "Escape") {
2425
1409
  if (engine.getSnapshot().kind === "annotating") return;
2426
- if (expanded) setExpanded(false);
1410
+ if (expanded) collapse();
2427
1411
  return;
2428
1412
  }
2429
1413
  if (!expanded) return;
@@ -2454,14 +1438,24 @@ function ClicklyRoot({
2454
1438
  }, [clearAnnotations, outputDetail, expanded, engine]);
2455
1439
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "clickly-ui", children: [
2456
1440
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(AnnotationPins, {}),
2457
- expanded ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
2458
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Toolbar, { engine, onCollapse: () => setExpanded(false) }),
2459
- /* @__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 })
2460
1454
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CollapsedFAB, { onExpand: () => setExpanded(true) })
2461
1455
  ] });
2462
1456
  }
2463
1457
 
2464
- // packages/react/src/internal/styles.ts
1458
+ // ../react/src/internal/styles.ts
2465
1459
  var REACT_UI_CSS = `
2466
1460
  .clickly-ui, .clickly-ui * { box-sizing: border-box; }
2467
1461
 
@@ -2487,6 +1481,11 @@ var REACT_UI_CSS = `
2487
1481
  0 0 0 1px rgba(255,255,255,0.06) inset;
2488
1482
  transition: transform 140ms ease, box-shadow 140ms ease;
2489
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); }
2490
1489
  }
2491
1490
  .clickly-fab:hover {
2492
1491
  transform: scale(1.06);
@@ -2533,12 +1532,21 @@ var REACT_UI_CSS = `
2533
1532
  pointer-events: auto;
2534
1533
  user-select: none;
2535
1534
  z-index: 1;
2536
- 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;
2537
1541
  }
2538
1542
 
2539
- @keyframes clickly-fade-in {
2540
- from { opacity: 0; transform: translateY(6px) scale(0.97); }
2541
- 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); }
2542
1550
  }
2543
1551
 
2544
1552
  .clickly-toolbar .grip {
@@ -2589,6 +1597,14 @@ var REACT_UI_CSS = `
2589
1597
  .clickly-btn.is-active:hover { background: #0284c7; }
2590
1598
  .clickly-btn[disabled] { opacity: 0.28; cursor: not-allowed; pointer-events: none; }
2591
1599
 
1600
+ /* Freeze-animations active state \u2014 icy blue */
1601
+ .clickly-btn.is-freeze {
1602
+ background: rgba(99, 179, 237, 0.18);
1603
+ color: #63b3ed;
1604
+ box-shadow: 0 0 0 1px rgba(99,179,237,0.35), 0 2px 8px rgba(99,179,237,0.2);
1605
+ }
1606
+ .clickly-btn.is-freeze:hover { background: rgba(99, 179, 237, 0.26); }
1607
+
2592
1608
  .clickly-btn.icon-only {
2593
1609
  width: 32px;
2594
1610
  padding: 0;
@@ -2666,6 +1682,91 @@ var REACT_UI_CSS = `
2666
1682
  color: #94a3b8;
2667
1683
  }
2668
1684
 
1685
+ /* \u2500\u2500\u2500 Shortcuts panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1686
+
1687
+ .shortcuts-trigger {
1688
+ position: relative;
1689
+ display: inline-flex;
1690
+ align-items: center;
1691
+ justify-content: center;
1692
+ }
1693
+
1694
+ .shortcuts-panel {
1695
+ position: absolute;
1696
+ bottom: calc(100% + 12px);
1697
+ left: 50%;
1698
+ transform: translateX(-50%);
1699
+ width: 260px;
1700
+ background: rgba(9, 14, 28, 0.97);
1701
+ border: 1px solid rgba(255,255,255,0.08);
1702
+ border-radius: 12px;
1703
+ box-shadow: 0 16px 40px rgba(0,0,0,0.45);
1704
+ padding: 10px;
1705
+ z-index: 10;
1706
+ animation: clickly-fade-in 100ms ease-out;
1707
+ pointer-events: none;
1708
+ }
1709
+
1710
+ /* Arrow pointing down */
1711
+ .shortcuts-panel::after {
1712
+ content: "";
1713
+ position: absolute;
1714
+ top: 100%;
1715
+ left: 50%;
1716
+ transform: translateX(-50%);
1717
+ border: 6px solid transparent;
1718
+ border-top-color: rgba(9, 14, 28, 0.97);
1719
+ }
1720
+
1721
+ .shortcuts-title {
1722
+ font: 600 11px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
1723
+ color: #64748b;
1724
+ text-transform: uppercase;
1725
+ letter-spacing: 0.06em;
1726
+ padding: 2px 4px 8px;
1727
+ border-bottom: 1px solid rgba(255,255,255,0.06);
1728
+ margin-bottom: 6px;
1729
+ }
1730
+
1731
+ .shortcuts-row {
1732
+ display: flex;
1733
+ align-items: center;
1734
+ justify-content: space-between;
1735
+ padding: 4px 4px;
1736
+ border-radius: 6px;
1737
+ gap: 8px;
1738
+ }
1739
+ .shortcuts-row:hover { background: rgba(255,255,255,0.04); }
1740
+
1741
+ .shortcuts-label {
1742
+ font: 12px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
1743
+ color: #94a3b8;
1744
+ flex: 1;
1745
+ min-width: 0;
1746
+ }
1747
+
1748
+ .shortcuts-keys {
1749
+ display: flex;
1750
+ gap: 3px;
1751
+ align-items: center;
1752
+ flex-shrink: 0;
1753
+ }
1754
+
1755
+ .shortcuts-keys kbd {
1756
+ display: inline-flex;
1757
+ align-items: center;
1758
+ justify-content: center;
1759
+ min-width: 20px;
1760
+ height: 20px;
1761
+ padding: 0 5px;
1762
+ background: rgba(255,255,255,0.08);
1763
+ border: 1px solid rgba(255,255,255,0.10);
1764
+ border-bottom-width: 2px;
1765
+ border-radius: 5px;
1766
+ font: 11px/1 ui-monospace, "SF Mono", Menlo, monospace;
1767
+ color: #e2e8f0;
1768
+ }
1769
+
2669
1770
  /* \u2500\u2500\u2500 Popup & popovers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2670
1771
 
2671
1772
  .clickly-popup, .clickly-popover {
@@ -3417,7 +2518,7 @@ var REACT_UI_CSS = `
3417
2518
  }
3418
2519
  `;
3419
2520
 
3420
- // packages/react/src/internal/globalStyles.ts
2521
+ // ../react/src/internal/globalStyles.ts
3421
2522
  var GLOBAL_PAGE_CSS = `
3422
2523
  body[data-clickly-active] {
3423
2524
  -webkit-user-select: none !important;
@@ -3438,18 +2539,18 @@ body[data-clickly-annotating] {
3438
2539
  }
3439
2540
  `;
3440
2541
 
3441
- // packages/react/src/Clickly.tsx
2542
+ // ../react/src/Clickly.tsx
3442
2543
  var import_jsx_runtime9 = require("react/jsx-runtime");
3443
2544
  function Clickly({ className } = {}) {
3444
- const [mount, setMount] = (0, import_react11.useState)(null);
3445
- (0, import_react11.useEffect)(() => {
2545
+ const [mount, setMount] = (0, import_react9.useState)(null);
2546
+ (0, import_react9.useEffect)(() => {
3446
2547
  if (typeof window === "undefined" || typeof document === "undefined") return;
3447
2548
  let shadow = null;
3448
2549
  let engine = null;
3449
2550
  let overlay = null;
3450
2551
  let portal = null;
3451
2552
  try {
3452
- shadow = createShadowHost({ document });
2553
+ shadow = (0, import_core2.createShadowHost)({ document });
3453
2554
  portal = document.createElement("div");
3454
2555
  portal.setAttribute("data-clickly-react-root", "");
3455
2556
  shadow.root.appendChild(portal);
@@ -3465,8 +2566,8 @@ function Clickly({ className } = {}) {
3465
2566
  gstyle.textContent = GLOBAL_PAGE_CSS;
3466
2567
  document.head.appendChild(gstyle);
3467
2568
  }
3468
- engine = new SelectionEngine({ document, host: shadow.host });
3469
- overlay = new Overlay({
2569
+ engine = new import_core2.SelectionEngine({ document, host: shadow.host });
2570
+ overlay = new import_core2.Overlay({
3470
2571
  engine,
3471
2572
  root: shadow.root,
3472
2573
  document,
@@ -3491,7 +2592,7 @@ function Clickly({ className } = {}) {
3491
2592
  document.body.removeAttribute("data-clickly-mode");
3492
2593
  };
3493
2594
  }, []);
3494
- (0, import_react11.useEffect)(() => {
2595
+ (0, import_react9.useEffect)(() => {
3495
2596
  if (!mount || !className) return;
3496
2597
  mount.shadow.host.className = className;
3497
2598
  return () => {