@alisamadiillc/devtools 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1658 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var react = require('react');
5
+ var reactDom = require('react-dom');
6
+ var jsxRuntime = require('react/jsx-runtime');
7
+
8
+ // src/devtools.tsx
9
+
10
+ // src/utils/element-info.ts
11
+ function isDevToolsElement(el) {
12
+ if (!el) return false;
13
+ if (el.hasAttribute("data-devtools-ignore")) return true;
14
+ return isDevToolsElement(el.parentElement);
15
+ }
16
+ function getCssSelector(element) {
17
+ if (element.id) return `#${element.id}`;
18
+ const parts = [];
19
+ let current = element;
20
+ while (current && current !== document.body && current !== document.documentElement) {
21
+ const tag = current.tagName.toLowerCase();
22
+ if (current.id) {
23
+ parts.unshift(`#${current.id}`);
24
+ break;
25
+ }
26
+ const parent = current.parentElement;
27
+ if (parent) {
28
+ const index = Array.from(parent.children).indexOf(current) + 1;
29
+ parts.unshift(`${tag}:nth-child(${index})`);
30
+ } else {
31
+ parts.unshift(tag);
32
+ }
33
+ current = current.parentElement;
34
+ }
35
+ return parts.join(" > ");
36
+ }
37
+ function getElementLabel(element) {
38
+ const tag = element.tagName.toLowerCase();
39
+ const classes = Array.from(element.classList).filter((c) => !c.startsWith("_") && c.length < 30).slice(0, 2);
40
+ if (element.id) return `${tag}#${element.id}`;
41
+ if (classes.length > 0) return `${tag}.${classes.join(".")}`;
42
+ return tag;
43
+ }
44
+
45
+ // src/components/editor.tsx
46
+ function generateId() {
47
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
48
+ return crypto.randomUUID();
49
+ }
50
+ return Date.now().toString(36) + Math.random().toString(36).slice(2);
51
+ }
52
+ function Editor({ element, onTrackOriginal, onAddChange }) {
53
+ const fileInputRef = react.useRef(null);
54
+ const changeIdRef = react.useRef(generateId());
55
+ const isImage = element.tagName.toLowerCase() === "img";
56
+ const htmlEl = element;
57
+ const setupTextEditing = react.useCallback(() => {
58
+ if (isImage) return;
59
+ const originalText = htmlEl.textContent || "";
60
+ onTrackOriginal(element, "text", originalText);
61
+ htmlEl.contentEditable = "true";
62
+ htmlEl.style.outline = "2px solid rgba(59, 130, 246, 0.5)";
63
+ htmlEl.style.outlineOffset = "2px";
64
+ htmlEl.style.cursor = "text";
65
+ htmlEl.focus();
66
+ const handleInput = () => {
67
+ const newText = htmlEl.textContent || "";
68
+ if (newText !== originalText) {
69
+ onAddChange({
70
+ id: changeIdRef.current,
71
+ selector: getCssSelector(element),
72
+ element: getElementLabel(element),
73
+ type: "text",
74
+ original: originalText,
75
+ modified: newText,
76
+ note: ""
77
+ });
78
+ }
79
+ };
80
+ htmlEl.addEventListener("input", handleInput);
81
+ return () => {
82
+ htmlEl.removeEventListener("input", handleInput);
83
+ };
84
+ }, [element, htmlEl, isImage, onTrackOriginal, onAddChange]);
85
+ const setupImageEditing = react.useCallback(() => {
86
+ if (!isImage) return;
87
+ const imgEl = element;
88
+ const originalSrc = imgEl.src;
89
+ onTrackOriginal(element, "image", originalSrc);
90
+ const input = document.createElement("input");
91
+ input.type = "file";
92
+ input.accept = "image/*";
93
+ input.style.display = "none";
94
+ document.body.appendChild(input);
95
+ fileInputRef.current = input;
96
+ const wrapper = document.createElement("div");
97
+ wrapper.setAttribute("data-devtools-ignore", "");
98
+ wrapper.style.cssText = `
99
+ position: absolute;
100
+ display: flex;
101
+ align-items: center;
102
+ justify-content: center;
103
+ background: rgba(0,0,0,0.5);
104
+ backdrop-filter: blur(4px);
105
+ border-radius: 4px;
106
+ cursor: pointer;
107
+ z-index: 99998;
108
+ `;
109
+ const updatePosition = () => {
110
+ const rect = imgEl.getBoundingClientRect();
111
+ wrapper.style.top = `${rect.top + window.scrollY}px`;
112
+ wrapper.style.left = `${rect.left + window.scrollX}px`;
113
+ wrapper.style.width = `${rect.width}px`;
114
+ wrapper.style.height = `${rect.height}px`;
115
+ };
116
+ updatePosition();
117
+ const btn = document.createElement("button");
118
+ btn.textContent = "Replace Image";
119
+ btn.setAttribute("data-devtools-ignore", "");
120
+ btn.style.cssText = `
121
+ padding: 8px 16px;
122
+ border-radius: 8px;
123
+ border: 1px solid rgba(255,255,255,0.2);
124
+ background: rgba(255,255,255,0.1);
125
+ color: #fff;
126
+ font-size: 13px;
127
+ cursor: pointer;
128
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
129
+ `;
130
+ wrapper.appendChild(btn);
131
+ document.body.appendChild(wrapper);
132
+ btn.addEventListener("click", () => input.click());
133
+ input.addEventListener("change", () => {
134
+ const file = input.files?.[0];
135
+ if (!file) return;
136
+ const reader = new FileReader();
137
+ reader.onload = (e) => {
138
+ const dataUrl = e.target?.result;
139
+ imgEl.src = dataUrl;
140
+ onAddChange({
141
+ id: changeIdRef.current,
142
+ selector: getCssSelector(element),
143
+ element: getElementLabel(element),
144
+ type: "image",
145
+ original: originalSrc,
146
+ modified: `[replaced with ${file.name}]`,
147
+ note: ""
148
+ });
149
+ wrapper.remove();
150
+ };
151
+ reader.readAsDataURL(file);
152
+ });
153
+ return () => {
154
+ wrapper.remove();
155
+ input.remove();
156
+ };
157
+ }, [element, isImage, onTrackOriginal, onAddChange]);
158
+ react.useEffect(() => {
159
+ const cleanupText = setupTextEditing();
160
+ const cleanupImage = setupImageEditing();
161
+ return () => {
162
+ cleanupText?.();
163
+ cleanupImage?.();
164
+ };
165
+ }, [setupTextEditing, setupImageEditing]);
166
+ return null;
167
+ }
168
+
169
+ // src/utils/styles.ts
170
+ var FONT_FAMILY = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif';
171
+ var v = {
172
+ bg: "var(--card)",
173
+ fg: "var(--card-foreground)",
174
+ muted: "var(--muted)",
175
+ mutedFg: "var(--muted-foreground)",
176
+ border: "var(--border)",
177
+ primary: "var(--primary)",
178
+ primaryFg: "var(--primary-foreground)",
179
+ accent: "var(--accent)",
180
+ accentFg: "var(--accent-foreground)"
181
+ };
182
+ var styles = {
183
+ toolbar: {
184
+ position: "fixed",
185
+ bottom: 16,
186
+ right: 16,
187
+ display: "flex",
188
+ alignItems: "center",
189
+ gap: 2,
190
+ padding: 4,
191
+ borderRadius: 12,
192
+ background: v.bg,
193
+ border: `1px solid ${v.border}`,
194
+ color: v.fg,
195
+ fontSize: 13,
196
+ fontFamily: FONT_FAMILY,
197
+ boxShadow: "0 8px 32px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0,0,0,0.04)",
198
+ lineHeight: 1
199
+ },
200
+ toolbarButton: {
201
+ display: "flex",
202
+ alignItems: "center",
203
+ gap: 6,
204
+ padding: "8px 12px",
205
+ borderRadius: 8,
206
+ border: "none",
207
+ background: "transparent",
208
+ color: v.mutedFg,
209
+ fontSize: 12,
210
+ fontFamily: FONT_FAMILY,
211
+ cursor: "pointer",
212
+ transition: "all 150ms ease",
213
+ whiteSpace: "nowrap",
214
+ lineHeight: 1
215
+ },
216
+ toolbarButtonActive: {
217
+ background: v.accent,
218
+ color: v.accentFg
219
+ },
220
+ toolbarDivider: {
221
+ width: 1,
222
+ height: 20,
223
+ background: v.border,
224
+ margin: "0 2px"
225
+ },
226
+ badge: {
227
+ display: "inline-flex",
228
+ alignItems: "center",
229
+ justifyContent: "center",
230
+ minWidth: 18,
231
+ height: 18,
232
+ padding: "0 5px",
233
+ borderRadius: 999,
234
+ background: v.primary,
235
+ color: v.primaryFg,
236
+ fontSize: 10,
237
+ fontWeight: 600,
238
+ lineHeight: 1
239
+ },
240
+ highlightRect: {
241
+ position: "fixed",
242
+ pointerEvents: "none",
243
+ border: "2px solid rgba(59, 130, 246, 0.6)",
244
+ background: "rgba(59, 130, 246, 0.08)",
245
+ borderRadius: 4,
246
+ transition: "all 60ms ease-out"
247
+ },
248
+ elementLabel: {
249
+ position: "fixed",
250
+ pointerEvents: "none",
251
+ padding: "4px 8px",
252
+ borderRadius: 6,
253
+ background: v.bg,
254
+ border: `1px solid ${v.border}`,
255
+ color: v.primary,
256
+ fontSize: 11,
257
+ fontFamily: FONT_FAMILY,
258
+ fontWeight: 500,
259
+ whiteSpace: "nowrap",
260
+ lineHeight: 1,
261
+ boxShadow: "0 2px 8px rgba(0,0,0,0.08)"
262
+ },
263
+ toast: {
264
+ position: "fixed",
265
+ bottom: 72,
266
+ right: 16,
267
+ padding: "10px 16px",
268
+ borderRadius: 10,
269
+ background: v.bg,
270
+ border: `1px solid ${v.border}`,
271
+ color: v.fg,
272
+ fontSize: 13,
273
+ fontFamily: FONT_FAMILY,
274
+ boxShadow: "0 4px 20px rgba(0, 0, 0, 0.1)",
275
+ transition: "all 300ms ease",
276
+ lineHeight: 1.4
277
+ },
278
+ primaryButton: {
279
+ display: "flex",
280
+ alignItems: "center",
281
+ gap: 6,
282
+ padding: "8px 16px",
283
+ borderRadius: 8,
284
+ border: "none",
285
+ background: v.primary,
286
+ color: v.primaryFg,
287
+ fontSize: 13,
288
+ fontFamily: FONT_FAMILY,
289
+ fontWeight: 500,
290
+ cursor: "pointer",
291
+ transition: "all 150ms ease",
292
+ lineHeight: 1
293
+ },
294
+ secondaryButton: {
295
+ display: "flex",
296
+ alignItems: "center",
297
+ gap: 6,
298
+ padding: "8px 16px",
299
+ borderRadius: 8,
300
+ border: `1px solid ${v.border}`,
301
+ background: v.muted,
302
+ color: v.mutedFg,
303
+ fontSize: 13,
304
+ fontFamily: FONT_FAMILY,
305
+ fontWeight: 500,
306
+ cursor: "pointer",
307
+ transition: "all 150ms ease",
308
+ lineHeight: 1
309
+ },
310
+ viewPanel: {
311
+ position: "fixed",
312
+ bottom: 64,
313
+ right: 16,
314
+ width: 320,
315
+ maxHeight: 400,
316
+ overflowY: "auto",
317
+ padding: 12,
318
+ borderRadius: 12,
319
+ background: v.bg,
320
+ border: `1px solid ${v.border}`,
321
+ boxShadow: "0 8px 32px rgba(0, 0, 0, 0.12)",
322
+ fontFamily: FONT_FAMILY,
323
+ color: v.fg,
324
+ animation: "devtools-slide-up 200ms ease forwards"
325
+ },
326
+ viewPanelItem: {
327
+ display: "flex",
328
+ flexDirection: "column",
329
+ gap: 4,
330
+ padding: "8px 10px",
331
+ borderRadius: 8,
332
+ background: "transparent",
333
+ marginBottom: 4,
334
+ cursor: "default",
335
+ transition: "background 150ms ease"
336
+ },
337
+ viewPanelItemHover: {
338
+ background: v.accent
339
+ },
340
+ notePopup: {
341
+ position: "fixed",
342
+ width: 280,
343
+ padding: 12,
344
+ borderRadius: 10,
345
+ background: v.bg,
346
+ border: `1px solid ${v.border}`,
347
+ boxShadow: "0 8px 32px rgba(0, 0, 0, 0.12)",
348
+ fontFamily: FONT_FAMILY,
349
+ color: v.fg,
350
+ animation: "devtools-fade-in 150ms ease forwards"
351
+ },
352
+ noteTextarea: {
353
+ width: "100%",
354
+ height: 60,
355
+ padding: 8,
356
+ borderRadius: 6,
357
+ border: `1px solid ${v.border}`,
358
+ background: v.muted,
359
+ color: v.fg,
360
+ fontSize: 12,
361
+ fontFamily: FONT_FAMILY,
362
+ resize: "vertical",
363
+ outline: "none",
364
+ boxSizing: "border-box"
365
+ },
366
+ resetButton: {
367
+ position: "fixed",
368
+ width: 22,
369
+ height: 22,
370
+ borderRadius: "50%",
371
+ border: `1px solid ${v.border}`,
372
+ background: "rgba(239, 68, 68, 0.9)",
373
+ color: "#fff",
374
+ fontSize: 12,
375
+ cursor: "pointer",
376
+ display: "flex",
377
+ alignItems: "center",
378
+ justifyContent: "center",
379
+ lineHeight: 1,
380
+ padding: 0,
381
+ boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)",
382
+ transition: "all 150ms ease"
383
+ }
384
+ };
385
+ var KEYFRAMES = `
386
+ @keyframes devtools-fade-in {
387
+ from { opacity: 0; transform: translateY(8px) scale(0.96); }
388
+ to { opacity: 1; transform: translateY(0) scale(1); }
389
+ }
390
+ @keyframes devtools-fade-out {
391
+ from { opacity: 1; transform: translateY(0) scale(1); }
392
+ to { opacity: 0; transform: translateY(8px) scale(0.96); }
393
+ }
394
+ @keyframes devtools-pulse {
395
+ 0%, 100% { opacity: 1; }
396
+ 50% { opacity: 0.5; }
397
+ }
398
+ @keyframes devtools-slide-up {
399
+ from { opacity: 0; transform: translateY(16px); }
400
+ to { opacity: 1; transform: translateY(0); }
401
+ }
402
+ @keyframes devtools-shake {
403
+ 0%, 100% { transform: translateX(0); }
404
+ 10%, 50%, 90% { transform: translateX(-4px); }
405
+ 30%, 70% { transform: translateX(4px); }
406
+ }
407
+ `;
408
+ function ElementResetButton({
409
+ element,
410
+ onReset,
411
+ zIndex
412
+ }) {
413
+ const [pos, setPos] = react.useState(null);
414
+ react.useEffect(() => {
415
+ const update = () => {
416
+ const rect = element.getBoundingClientRect();
417
+ setPos({
418
+ top: rect.top - 10,
419
+ left: rect.right - 10
420
+ });
421
+ };
422
+ update();
423
+ window.addEventListener("scroll", update, true);
424
+ window.addEventListener("resize", update);
425
+ return () => {
426
+ window.removeEventListener("scroll", update, true);
427
+ window.removeEventListener("resize", update);
428
+ };
429
+ }, [element]);
430
+ if (!pos) return null;
431
+ return /* @__PURE__ */ jsxRuntime.jsx(
432
+ "button",
433
+ {
434
+ "data-devtools-ignore": true,
435
+ onClick: (e) => {
436
+ e.stopPropagation();
437
+ onReset();
438
+ },
439
+ style: {
440
+ ...styles.resetButton,
441
+ top: pos.top,
442
+ left: pos.left,
443
+ zIndex: zIndex + 1
444
+ },
445
+ onMouseEnter: (e) => {
446
+ e.currentTarget.style.background = "rgba(239, 68, 68, 1)";
447
+ e.currentTarget.style.transform = "scale(1.1)";
448
+ },
449
+ onMouseLeave: (e) => {
450
+ e.currentTarget.style.background = "rgba(239, 68, 68, 0.9)";
451
+ e.currentTarget.style.transform = "scale(1)";
452
+ },
453
+ title: "Reset this element",
454
+ children: "\xD7"
455
+ }
456
+ );
457
+ }
458
+ function Inspector({ rect, label, zIndex }) {
459
+ if (!rect) return null;
460
+ const labelTop = rect.top - 28;
461
+ const showLabelAbove = labelTop > 8;
462
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
463
+ /* @__PURE__ */ jsxRuntime.jsx(
464
+ "div",
465
+ {
466
+ "data-devtools-ignore": true,
467
+ style: {
468
+ ...styles.highlightRect,
469
+ zIndex: zIndex - 1,
470
+ top: rect.top,
471
+ left: rect.left,
472
+ width: rect.width,
473
+ height: rect.height
474
+ }
475
+ }
476
+ ),
477
+ label && /* @__PURE__ */ jsxRuntime.jsx(
478
+ "div",
479
+ {
480
+ "data-devtools-ignore": true,
481
+ style: {
482
+ ...styles.elementLabel,
483
+ zIndex: zIndex - 1,
484
+ left: rect.left,
485
+ top: showLabelAbove ? labelTop : rect.top + rect.height + 4
486
+ },
487
+ children: label
488
+ }
489
+ )
490
+ ] });
491
+ }
492
+ function NotePopup({
493
+ element,
494
+ initialNote,
495
+ onSave,
496
+ onClose,
497
+ zIndex
498
+ }) {
499
+ const [note, setNote] = react.useState(initialNote);
500
+ const [position, setPosition] = react.useState({
501
+ top: 0,
502
+ left: 0
503
+ });
504
+ react.useEffect(() => {
505
+ const rect = element.getBoundingClientRect();
506
+ const spaceBelow = window.innerHeight - rect.bottom;
507
+ const popupHeight = 160;
508
+ const top = spaceBelow > popupHeight + 8 ? rect.bottom + 8 : rect.top - popupHeight - 8;
509
+ setPosition({
510
+ top: Math.max(8, Math.min(top, window.innerHeight - popupHeight - 8)),
511
+ left: Math.max(8, Math.min(rect.left, window.innerWidth - 296))
512
+ });
513
+ }, [element]);
514
+ return /* @__PURE__ */ jsxRuntime.jsxs(
515
+ "div",
516
+ {
517
+ "data-devtools-ignore": true,
518
+ style: {
519
+ ...styles.notePopup,
520
+ top: position.top,
521
+ left: position.left,
522
+ zIndex: zIndex + 1
523
+ },
524
+ children: [
525
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 12, color: "var(--muted-foreground)", marginBottom: 8 }, children: "Describe the change you want:" }),
526
+ /* @__PURE__ */ jsxRuntime.jsx(
527
+ "textarea",
528
+ {
529
+ "data-devtools-ignore": true,
530
+ value: note,
531
+ onChange: (e) => setNote(e.target.value),
532
+ placeholder: "e.g., Make this text larger, change the color...",
533
+ autoFocus: true,
534
+ style: styles.noteTextarea,
535
+ onKeyDown: (e) => {
536
+ e.stopPropagation();
537
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
538
+ onSave(note);
539
+ }
540
+ if (e.key === "Escape") {
541
+ onClose();
542
+ }
543
+ }
544
+ }
545
+ ),
546
+ /* @__PURE__ */ jsxRuntime.jsxs(
547
+ "div",
548
+ {
549
+ style: {
550
+ display: "flex",
551
+ gap: 6,
552
+ justifyContent: "flex-end",
553
+ marginTop: 8
554
+ },
555
+ children: [
556
+ /* @__PURE__ */ jsxRuntime.jsx(
557
+ "button",
558
+ {
559
+ "data-devtools-ignore": true,
560
+ onClick: onClose,
561
+ style: styles.secondaryButton,
562
+ children: "Skip"
563
+ }
564
+ ),
565
+ /* @__PURE__ */ jsxRuntime.jsx(
566
+ "button",
567
+ {
568
+ "data-devtools-ignore": true,
569
+ onClick: () => onSave(note),
570
+ style: styles.primaryButton,
571
+ children: "Save"
572
+ }
573
+ )
574
+ ]
575
+ }
576
+ ),
577
+ /* @__PURE__ */ jsxRuntime.jsx(
578
+ "div",
579
+ {
580
+ style: {
581
+ fontSize: 10,
582
+ color: "var(--muted-foreground)",
583
+ marginTop: 6,
584
+ textAlign: "right"
585
+ },
586
+ children: "\u2318+Enter to save"
587
+ }
588
+ )
589
+ ]
590
+ }
591
+ );
592
+ }
593
+ var warningStyle = {
594
+ background: "rgba(251, 146, 60, 0.15)",
595
+ border: "1px solid rgba(251, 146, 60, 0.4)",
596
+ color: "#c2410c"
597
+ };
598
+ function Toast({
599
+ message,
600
+ variant = "success",
601
+ duration = 3e3,
602
+ linkUrl,
603
+ linkText,
604
+ onDone,
605
+ zIndex,
606
+ position
607
+ }) {
608
+ const [visible, setVisible] = react.useState(true);
609
+ react.useEffect(() => {
610
+ const timer = setTimeout(() => {
611
+ setVisible(false);
612
+ setTimeout(onDone, 300);
613
+ }, duration);
614
+ return () => clearTimeout(timer);
615
+ }, [duration, onDone]);
616
+ const positionStyle = position === "bottom-left" ? { left: 16, right: "auto" } : { right: 16, left: "auto" };
617
+ const isWarning = variant === "warning";
618
+ return /* @__PURE__ */ jsxRuntime.jsxs(
619
+ "div",
620
+ {
621
+ "data-devtools-ignore": true,
622
+ style: {
623
+ ...styles.toast,
624
+ ...positionStyle,
625
+ ...isWarning ? warningStyle : {},
626
+ zIndex: zIndex + 1,
627
+ opacity: visible ? 1 : 0,
628
+ transform: visible ? "translateY(0)" : "translateY(16px)",
629
+ animation: isWarning ? "devtools-shake 0.5s ease" : "devtools-fade-in 200ms ease forwards",
630
+ display: "flex",
631
+ alignItems: "center",
632
+ gap: 8,
633
+ flexWrap: "wrap"
634
+ },
635
+ children: [
636
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: isWarning ? "\u26A0" : "\u2713" }),
637
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: message }),
638
+ linkUrl && /* @__PURE__ */ jsxRuntime.jsx(
639
+ "a",
640
+ {
641
+ href: linkUrl,
642
+ target: "_blank",
643
+ rel: "noopener noreferrer",
644
+ "data-devtools-ignore": true,
645
+ style: {
646
+ color: "inherit",
647
+ textDecoration: "underline",
648
+ textUnderlineOffset: 2,
649
+ fontWeight: 500,
650
+ cursor: "pointer"
651
+ },
652
+ children: linkText || "Submit here \u2192"
653
+ }
654
+ )
655
+ ]
656
+ }
657
+ );
658
+ }
659
+ function ToolbarIcon({ d, size = 14 }) {
660
+ return /* @__PURE__ */ jsxRuntime.jsx(
661
+ "svg",
662
+ {
663
+ width: size,
664
+ height: size,
665
+ viewBox: "0 0 24 24",
666
+ fill: "none",
667
+ stroke: "currentColor",
668
+ strokeWidth: 2,
669
+ strokeLinecap: "round",
670
+ strokeLinejoin: "round",
671
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { d })
672
+ }
673
+ );
674
+ }
675
+ function hoverIn(e, overrides) {
676
+ Object.assign(e.currentTarget.style, {
677
+ background: "var(--accent)",
678
+ color: "var(--accent-foreground)",
679
+ ...overrides
680
+ });
681
+ }
682
+ function hoverOut(e, overrides) {
683
+ Object.assign(e.currentTarget.style, {
684
+ background: "transparent",
685
+ color: "var(--muted-foreground)",
686
+ ...overrides
687
+ });
688
+ }
689
+ function ResetButton({ onReset }) {
690
+ const [confirming, setConfirming] = react.useState(false);
691
+ const timerRef = react.useRef();
692
+ const handleClick = react.useCallback(() => {
693
+ if (confirming) {
694
+ clearTimeout(timerRef.current);
695
+ setConfirming(false);
696
+ onReset();
697
+ } else {
698
+ setConfirming(true);
699
+ timerRef.current = setTimeout(() => setConfirming(false), 3e3);
700
+ }
701
+ }, [confirming, onReset]);
702
+ react.useEffect(() => {
703
+ return () => clearTimeout(timerRef.current);
704
+ }, []);
705
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
706
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles.toolbarDivider }),
707
+ /* @__PURE__ */ jsxRuntime.jsxs(
708
+ "button",
709
+ {
710
+ onClick: handleClick,
711
+ style: {
712
+ ...styles.toolbarButton,
713
+ ...confirming ? { background: "rgba(239, 68, 68, 0.15)", color: "#f87171" } : {}
714
+ },
715
+ onMouseEnter: (e) => {
716
+ if (!confirming) hoverIn(e, {
717
+ background: "rgba(239, 68, 68, 0.1)",
718
+ color: "#f87171"
719
+ });
720
+ },
721
+ onMouseLeave: (e) => {
722
+ if (!confirming) hoverOut(e);
723
+ },
724
+ title: confirming ? "Click again to confirm reset" : "Reset all changes",
725
+ children: [
726
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarIcon, { d: "M1 4v6h6M23 20v-6h-6M20.49 9A9 9 0 005.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 013.51 15" }),
727
+ confirming && /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 11 }, children: "Confirm?" })
728
+ ]
729
+ }
730
+ )
731
+ ] });
732
+ }
733
+ function HideButton({ onHide }) {
734
+ const [confirming, setConfirming] = react.useState(false);
735
+ const timerRef = react.useRef();
736
+ const handleClick = react.useCallback(() => {
737
+ if (confirming) {
738
+ clearTimeout(timerRef.current);
739
+ setConfirming(false);
740
+ onHide();
741
+ } else {
742
+ setConfirming(true);
743
+ timerRef.current = setTimeout(() => setConfirming(false), 3e3);
744
+ }
745
+ }, [confirming, onHide]);
746
+ react.useEffect(() => {
747
+ return () => clearTimeout(timerRef.current);
748
+ }, []);
749
+ return /* @__PURE__ */ jsxRuntime.jsxs(
750
+ "button",
751
+ {
752
+ onClick: handleClick,
753
+ style: {
754
+ ...styles.toolbarButton,
755
+ ...confirming ? { background: "rgba(239, 68, 68, 0.15)", color: "#f87171" } : {}
756
+ },
757
+ onMouseEnter: (e) => {
758
+ if (!confirming) hoverIn(e);
759
+ },
760
+ onMouseLeave: (e) => {
761
+ if (!confirming) hoverOut(e);
762
+ },
763
+ title: confirming ? "Click again to confirm" : "Hide devtools",
764
+ children: [
765
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarIcon, { d: "M18 6L6 18M6 6l12 12" }),
766
+ confirming && /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 11 }, children: "Confirm?" })
767
+ ]
768
+ }
769
+ );
770
+ }
771
+ function Toolbar({
772
+ state,
773
+ changesCount,
774
+ onSelectElement,
775
+ onCopyFeedback,
776
+ onView,
777
+ onReset,
778
+ onHide,
779
+ onBack,
780
+ isViewing,
781
+ requestUrl,
782
+ zIndex,
783
+ position
784
+ }) {
785
+ const positionStyle = position === "bottom-left" ? { left: 16, right: "auto" } : { right: 16, left: "auto" };
786
+ const isInspecting = state === "inspecting";
787
+ return /* @__PURE__ */ jsxRuntime.jsxs(
788
+ "div",
789
+ {
790
+ "data-devtools-ignore": true,
791
+ style: {
792
+ ...styles.toolbar,
793
+ ...positionStyle,
794
+ zIndex,
795
+ animation: "devtools-fade-in 200ms ease forwards"
796
+ },
797
+ children: [
798
+ state === "editing" && /* @__PURE__ */ jsxRuntime.jsx(
799
+ "button",
800
+ {
801
+ onClick: onBack,
802
+ style: styles.toolbarButton,
803
+ onMouseEnter: (e) => hoverIn(e),
804
+ onMouseLeave: (e) => hoverOut(e),
805
+ title: "Back",
806
+ children: /* @__PURE__ */ jsxRuntime.jsx(ToolbarIcon, { d: "M19 12H5M12 19l-7-7 7-7" })
807
+ }
808
+ ),
809
+ /* @__PURE__ */ jsxRuntime.jsxs(
810
+ "button",
811
+ {
812
+ onClick: onSelectElement,
813
+ style: {
814
+ ...styles.toolbarButton,
815
+ ...isInspecting ? styles.toolbarButtonActive : {}
816
+ },
817
+ onMouseEnter: (e) => {
818
+ if (!isInspecting) hoverIn(e);
819
+ },
820
+ onMouseLeave: (e) => {
821
+ if (!isInspecting) hoverOut(e);
822
+ },
823
+ title: "Select an element to edit",
824
+ children: [
825
+ /* @__PURE__ */ jsxRuntime.jsx(
826
+ ToolbarIcon,
827
+ {
828
+ d: "M3 3h7v7H3zM14 3h7v7h-7zM14 14h7v7h-7zM3 14h7v7H3z",
829
+ size: 14
830
+ }
831
+ ),
832
+ "Select"
833
+ ]
834
+ }
835
+ ),
836
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles.toolbarDivider }),
837
+ /* @__PURE__ */ jsxRuntime.jsxs(
838
+ "button",
839
+ {
840
+ onClick: onView,
841
+ style: {
842
+ ...styles.toolbarButton,
843
+ ...isViewing ? styles.toolbarButtonActive : {}
844
+ },
845
+ onMouseEnter: (e) => {
846
+ if (!isViewing) hoverIn(e);
847
+ },
848
+ onMouseLeave: (e) => {
849
+ if (!isViewing) hoverOut(e);
850
+ },
851
+ title: "View all feedback",
852
+ children: [
853
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarIcon, { d: "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8zM12 9a3 3 0 100 6 3 3 0 000-6z" }),
854
+ "View",
855
+ changesCount > 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.badge, children: changesCount })
856
+ ]
857
+ }
858
+ ),
859
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles.toolbarDivider }),
860
+ /* @__PURE__ */ jsxRuntime.jsxs(
861
+ "button",
862
+ {
863
+ onClick: onCopyFeedback,
864
+ style: {
865
+ ...styles.toolbarButton,
866
+ ...changesCount > 0 ? { color: "var(--card-foreground)" } : { opacity: 0.5, cursor: "default" }
867
+ },
868
+ disabled: changesCount === 0,
869
+ onMouseEnter: (e) => {
870
+ if (changesCount > 0) hoverIn(e);
871
+ },
872
+ onMouseLeave: (e) => {
873
+ if (changesCount > 0)
874
+ hoverOut(e, { color: "var(--card-foreground)" });
875
+ },
876
+ title: "Copy feedback to clipboard",
877
+ children: [
878
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarIcon, { d: "M16 4h2a2 2 0 012 2v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6a2 2 0 012-2h2M9 2h6a1 1 0 011 1v1a1 1 0 01-1 1H9a1 1 0 01-1-1V3a1 1 0 011-1z" }),
879
+ "Copy"
880
+ ]
881
+ }
882
+ ),
883
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles.toolbarDivider }),
884
+ /* @__PURE__ */ jsxRuntime.jsxs(
885
+ "a",
886
+ {
887
+ href: requestUrl,
888
+ target: "_blank",
889
+ rel: "noopener noreferrer",
890
+ "data-devtools-ignore": true,
891
+ style: {
892
+ ...styles.toolbarButton,
893
+ textDecoration: "none",
894
+ color: "var(--card-foreground)"
895
+ },
896
+ onMouseEnter: (e) => hoverIn(e),
897
+ onMouseLeave: (e) => hoverOut(e, { color: "var(--card-foreground)" }),
898
+ title: "Submit feedback request",
899
+ children: [
900
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarIcon, { d: "M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z" }),
901
+ "Submit"
902
+ ]
903
+ }
904
+ ),
905
+ changesCount > 0 && /* @__PURE__ */ jsxRuntime.jsx(ResetButton, { onReset }),
906
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles.toolbarDivider }),
907
+ /* @__PURE__ */ jsxRuntime.jsx(HideButton, { onHide })
908
+ ]
909
+ }
910
+ );
911
+ }
912
+ function ViewPanel({
913
+ changes,
914
+ onDelete,
915
+ onHover,
916
+ zIndex,
917
+ position
918
+ }) {
919
+ const [hoveredId, setHoveredId] = react.useState(null);
920
+ const positionStyle = position === "bottom-left" ? { left: 16, right: "auto" } : { right: 16, left: "auto" };
921
+ return /* @__PURE__ */ jsxRuntime.jsxs(
922
+ "div",
923
+ {
924
+ "data-devtools-ignore": true,
925
+ style: {
926
+ ...styles.viewPanel,
927
+ ...positionStyle,
928
+ zIndex
929
+ },
930
+ children: [
931
+ /* @__PURE__ */ jsxRuntime.jsx(
932
+ "div",
933
+ {
934
+ style: {
935
+ fontSize: 13,
936
+ fontWeight: 600,
937
+ color: "#e5e5e5",
938
+ marginBottom: 8,
939
+ display: "flex",
940
+ justifyContent: "space-between",
941
+ alignItems: "center"
942
+ },
943
+ children: /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { color: "var(--card-foreground)" }, children: [
944
+ "Feedback (",
945
+ changes.length,
946
+ ")"
947
+ ] })
948
+ }
949
+ ),
950
+ changes.length === 0 && /* @__PURE__ */ jsxRuntime.jsxs(
951
+ "div",
952
+ {
953
+ style: {
954
+ fontSize: 12,
955
+ color: "var(--muted-foreground)",
956
+ padding: "24px 0",
957
+ textAlign: "center"
958
+ },
959
+ children: [
960
+ "No changes recorded yet.",
961
+ /* @__PURE__ */ jsxRuntime.jsx("br", {}),
962
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 11, color: "var(--muted-foreground)" }, children: "Click any element to start." })
963
+ ]
964
+ }
965
+ ),
966
+ changes.map((change) => /* @__PURE__ */ jsxRuntime.jsxs(
967
+ "div",
968
+ {
969
+ "data-devtools-ignore": true,
970
+ onMouseEnter: () => {
971
+ setHoveredId(change.id);
972
+ onHover(change);
973
+ },
974
+ onMouseLeave: () => {
975
+ setHoveredId(null);
976
+ onHover(null);
977
+ },
978
+ style: {
979
+ ...styles.viewPanelItem,
980
+ ...hoveredId === change.id ? styles.viewPanelItemHover : {}
981
+ },
982
+ children: [
983
+ /* @__PURE__ */ jsxRuntime.jsxs(
984
+ "div",
985
+ {
986
+ style: {
987
+ display: "flex",
988
+ justifyContent: "space-between",
989
+ alignItems: "center"
990
+ },
991
+ children: [
992
+ /* @__PURE__ */ jsxRuntime.jsx(
993
+ "span",
994
+ {
995
+ style: {
996
+ color: "var(--primary)",
997
+ fontSize: 11,
998
+ fontFamily: "monospace",
999
+ overflow: "hidden",
1000
+ textOverflow: "ellipsis",
1001
+ whiteSpace: "nowrap",
1002
+ maxWidth: 240
1003
+ },
1004
+ children: change.element
1005
+ }
1006
+ ),
1007
+ /* @__PURE__ */ jsxRuntime.jsx(
1008
+ "button",
1009
+ {
1010
+ "data-devtools-ignore": true,
1011
+ onClick: (e) => {
1012
+ e.stopPropagation();
1013
+ onDelete(change.id);
1014
+ },
1015
+ style: {
1016
+ background: "none",
1017
+ border: "none",
1018
+ color: "var(--muted-foreground)",
1019
+ cursor: "pointer",
1020
+ fontSize: 14,
1021
+ padding: "0 2px",
1022
+ lineHeight: 1,
1023
+ flexShrink: 0
1024
+ },
1025
+ onMouseEnter: (e) => {
1026
+ e.currentTarget.style.color = "#f87171";
1027
+ },
1028
+ onMouseLeave: (e) => {
1029
+ e.currentTarget.style.color = "#666";
1030
+ },
1031
+ title: "Remove this feedback",
1032
+ children: "\xD7"
1033
+ }
1034
+ )
1035
+ ]
1036
+ }
1037
+ ),
1038
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { fontSize: 11, color: "var(--muted-foreground)" }, children: [
1039
+ change.type === "text" ? "Text" : "Image",
1040
+ " change",
1041
+ change.modified && change.type === "text" && /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { color: "var(--muted-foreground)" }, children: [
1042
+ " ",
1043
+ '\u2014 "',
1044
+ change.modified.slice(0, 40),
1045
+ change.modified.length > 40 ? "..." : "",
1046
+ '"'
1047
+ ] })
1048
+ ] }),
1049
+ change.note && /* @__PURE__ */ jsxRuntime.jsxs(
1050
+ "div",
1051
+ {
1052
+ style: {
1053
+ fontSize: 11,
1054
+ color: "var(--muted-foreground)",
1055
+ fontStyle: "italic",
1056
+ marginTop: 2
1057
+ },
1058
+ children: [
1059
+ '"',
1060
+ change.note,
1061
+ '"'
1062
+ ]
1063
+ }
1064
+ )
1065
+ ]
1066
+ },
1067
+ change.id
1068
+ ))
1069
+ ]
1070
+ }
1071
+ );
1072
+ }
1073
+ function useElementSelector(active) {
1074
+ const [hoveredElement, setHoveredElement] = react.useState(null);
1075
+ const [rect, setRect] = react.useState(null);
1076
+ const [label, setLabel] = react.useState("");
1077
+ const [selectedElement, setSelectedElement] = react.useState(null);
1078
+ const clearSelection = react.useCallback(() => {
1079
+ setSelectedElement(null);
1080
+ }, []);
1081
+ react.useEffect(() => {
1082
+ if (!active) {
1083
+ setHoveredElement(null);
1084
+ setRect(null);
1085
+ setLabel("");
1086
+ return;
1087
+ }
1088
+ const handleMouseMove = (e) => {
1089
+ if (isDevToolsElement(e.target)) {
1090
+ setHoveredElement(null);
1091
+ setRect(null);
1092
+ setLabel("");
1093
+ return;
1094
+ }
1095
+ const elements = document.elementsFromPoint(e.clientX, e.clientY);
1096
+ const target = elements.find((el) => !isDevToolsElement(el));
1097
+ if (!target || target === document.body || target === document.documentElement) {
1098
+ setHoveredElement(null);
1099
+ setRect(null);
1100
+ setLabel("");
1101
+ return;
1102
+ }
1103
+ setHoveredElement(target);
1104
+ const bounds = target.getBoundingClientRect();
1105
+ setRect({
1106
+ top: bounds.top,
1107
+ left: bounds.left,
1108
+ width: bounds.width,
1109
+ height: bounds.height
1110
+ });
1111
+ setLabel(getElementLabel(target));
1112
+ };
1113
+ const handleClick = (e) => {
1114
+ if (isDevToolsElement(e.target)) return;
1115
+ e.preventDefault();
1116
+ e.stopPropagation();
1117
+ const elements = document.elementsFromPoint(e.clientX, e.clientY);
1118
+ const target = elements.find((el) => !isDevToolsElement(el));
1119
+ if (target && target !== document.body && target !== document.documentElement) {
1120
+ setSelectedElement(target);
1121
+ }
1122
+ };
1123
+ const handleScroll = () => {
1124
+ if (hoveredElement) {
1125
+ const bounds = hoveredElement.getBoundingClientRect();
1126
+ setRect({
1127
+ top: bounds.top,
1128
+ left: bounds.left,
1129
+ width: bounds.width,
1130
+ height: bounds.height
1131
+ });
1132
+ }
1133
+ };
1134
+ document.addEventListener("mousemove", handleMouseMove, true);
1135
+ document.addEventListener("click", handleClick, true);
1136
+ window.addEventListener("scroll", handleScroll, true);
1137
+ return () => {
1138
+ document.removeEventListener("mousemove", handleMouseMove, true);
1139
+ document.removeEventListener("click", handleClick, true);
1140
+ window.removeEventListener("scroll", handleScroll, true);
1141
+ };
1142
+ }, [active, hoveredElement]);
1143
+ return { hoveredElement, rect, label, selectedElement, clearSelection };
1144
+ }
1145
+ var STORAGE_PREFIX = "devtools-feedback";
1146
+ function getStorageKey() {
1147
+ return `${STORAGE_PREFIX}-${window.location.pathname}`;
1148
+ }
1149
+ function loadPersisted() {
1150
+ try {
1151
+ const raw = localStorage.getItem(getStorageKey());
1152
+ if (!raw) return null;
1153
+ return JSON.parse(raw);
1154
+ } catch {
1155
+ return null;
1156
+ }
1157
+ }
1158
+ function persist(data) {
1159
+ try {
1160
+ localStorage.setItem(getStorageKey(), JSON.stringify(data));
1161
+ } catch {
1162
+ }
1163
+ }
1164
+ function clearPersisted() {
1165
+ localStorage.removeItem(getStorageKey());
1166
+ }
1167
+ function generateId2() {
1168
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
1169
+ return crypto.randomUUID();
1170
+ }
1171
+ return Date.now().toString(36) + Math.random().toString(36).slice(2);
1172
+ }
1173
+ function useFeedback() {
1174
+ const [changes, setChanges] = react.useState(() => {
1175
+ if (typeof window === "undefined") return [];
1176
+ return loadPersisted()?.changes ?? [];
1177
+ });
1178
+ const originalsRef = react.useRef(/* @__PURE__ */ new Map());
1179
+ react.useEffect(() => {
1180
+ persist({ changes });
1181
+ }, [changes]);
1182
+ const trackOriginal = react.useCallback(
1183
+ (element, type, value) => {
1184
+ if (!originalsRef.current.has(element)) {
1185
+ originalsRef.current.set(element, { type, value });
1186
+ }
1187
+ },
1188
+ []
1189
+ );
1190
+ const addChange = react.useCallback((change) => {
1191
+ setChanges((prev) => {
1192
+ const existing = prev.findIndex(
1193
+ (c) => c.selector === change.selector && c.type === change.type
1194
+ );
1195
+ if (existing !== -1) {
1196
+ const updated = [...prev];
1197
+ updated[existing] = { ...change, id: prev[existing].id };
1198
+ return updated;
1199
+ }
1200
+ return [...prev, { ...change, id: change.id || generateId2() }];
1201
+ });
1202
+ }, []);
1203
+ const removeChange = react.useCallback((changeId) => {
1204
+ setChanges((prev) => prev.filter((c) => c.id !== changeId));
1205
+ }, []);
1206
+ const updateNote = react.useCallback((changeId, note) => {
1207
+ setChanges(
1208
+ (prev) => prev.map((c) => c.id === changeId ? { ...c, note } : c)
1209
+ );
1210
+ }, []);
1211
+ const resetElement = react.useCallback(
1212
+ (changeId) => {
1213
+ const change = changes.find((c) => c.id === changeId);
1214
+ if (!change) return;
1215
+ const el = document.querySelector(change.selector);
1216
+ if (el) {
1217
+ if (change.modified) {
1218
+ const original = originalsRef.current.get(el);
1219
+ if (original) {
1220
+ if (original.type === "text") {
1221
+ el.textContent = original.value;
1222
+ } else if (original.type === "image") {
1223
+ el.src = original.value;
1224
+ }
1225
+ }
1226
+ }
1227
+ originalsRef.current.delete(el);
1228
+ el.contentEditable = "false";
1229
+ el.style.outline = "";
1230
+ el.style.outlineOffset = "";
1231
+ el.style.cursor = "";
1232
+ }
1233
+ setChanges((prev) => prev.filter((c) => c.id !== changeId));
1234
+ },
1235
+ [changes]
1236
+ );
1237
+ const getPayload = react.useCallback(() => {
1238
+ return {
1239
+ url: window.location.href,
1240
+ path: window.location.pathname,
1241
+ changes,
1242
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1243
+ };
1244
+ }, [changes]);
1245
+ const reset = react.useCallback(() => {
1246
+ originalsRef.current.forEach((original, element) => {
1247
+ const change = changes.find(
1248
+ (c) => document.querySelector(c.selector) === element
1249
+ );
1250
+ if (change?.modified) {
1251
+ if (original.type === "text") {
1252
+ element.textContent = original.value;
1253
+ } else if (original.type === "image") {
1254
+ element.src = original.value;
1255
+ }
1256
+ }
1257
+ element.contentEditable = "false";
1258
+ element.style.outline = "";
1259
+ element.style.outlineOffset = "";
1260
+ element.style.cursor = "";
1261
+ });
1262
+ originalsRef.current.clear();
1263
+ changes.forEach((change) => {
1264
+ const el = document.querySelector(change.selector);
1265
+ if (el) {
1266
+ el.style.outline = "";
1267
+ el.style.outlineOffset = "";
1268
+ el.contentEditable = "false";
1269
+ el.style.cursor = "";
1270
+ }
1271
+ });
1272
+ setChanges([]);
1273
+ clearPersisted();
1274
+ }, [changes]);
1275
+ return {
1276
+ changes,
1277
+ trackOriginal,
1278
+ addChange,
1279
+ removeChange,
1280
+ updateNote,
1281
+ resetElement,
1282
+ getPayload,
1283
+ reset
1284
+ };
1285
+ }
1286
+ function useLocalStorageFlag(key) {
1287
+ const [value, setValue] = react.useState(() => {
1288
+ if (typeof window === "undefined") return false;
1289
+ return localStorage.getItem(key) === "true";
1290
+ });
1291
+ react.useEffect(() => {
1292
+ const onStorage = (e) => {
1293
+ if (e.key === key) setValue(e.newValue === "true");
1294
+ };
1295
+ window.addEventListener("storage", onStorage);
1296
+ const interval = setInterval(() => {
1297
+ const current = localStorage.getItem(key) === "true";
1298
+ setValue((prev) => prev !== current ? current : prev);
1299
+ }, 1e3);
1300
+ return () => {
1301
+ window.removeEventListener("storage", onStorage);
1302
+ clearInterval(interval);
1303
+ };
1304
+ }, [key]);
1305
+ const setFlag = react.useCallback(
1306
+ (val) => {
1307
+ if (val === null || val === false) {
1308
+ localStorage.removeItem(key);
1309
+ setValue(false);
1310
+ } else {
1311
+ localStorage.setItem(key, "true");
1312
+ setValue(true);
1313
+ }
1314
+ },
1315
+ [key]
1316
+ );
1317
+ return [value, setFlag];
1318
+ }
1319
+
1320
+ // src/utils/clipboard.ts
1321
+ function formatFeedbackMarkdown(payload) {
1322
+ const lines = [
1323
+ "## Client Feedback",
1324
+ "",
1325
+ `**Page**: ${payload.url}`,
1326
+ `**Path**: ${payload.path}`,
1327
+ `**Date**: ${payload.timestamp}`,
1328
+ ""
1329
+ ];
1330
+ if (payload.changes.length > 0) {
1331
+ lines.push("### Changes Requested", "");
1332
+ payload.changes.forEach((change, i) => {
1333
+ const num = i + 1;
1334
+ const typeLabel = change.type === "text" ? "Text Change" : "Image Change";
1335
+ lines.push(`${num}. **${typeLabel}** on \`${change.selector}\``);
1336
+ lines.push(` - **Element**: \`${change.element}\``);
1337
+ lines.push(` - **Current**: "${change.original}"`);
1338
+ lines.push(` - **Change to**: "${change.modified}"`);
1339
+ if (change.note) {
1340
+ lines.push(` - **Note**: "${change.note}"`);
1341
+ }
1342
+ lines.push("");
1343
+ });
1344
+ }
1345
+ return lines.join("\n");
1346
+ }
1347
+ async function copyFeedbackToClipboard(payload) {
1348
+ const markdown = formatFeedbackMarkdown(payload);
1349
+ try {
1350
+ await navigator.clipboard.writeText(markdown);
1351
+ return true;
1352
+ } catch {
1353
+ return false;
1354
+ }
1355
+ }
1356
+ var DEFAULT_REQUEST_URL = "https://portal.alisamadii.com/requests";
1357
+ function DevTools({
1358
+ storageKey = "devtools-enabled",
1359
+ position = "bottom-right",
1360
+ zIndex = 99999,
1361
+ requestUrl = DEFAULT_REQUEST_URL
1362
+ }) {
1363
+ const [enabled, setEnabled] = useLocalStorageFlag(storageKey);
1364
+ const [state, setState] = react.useState("hidden");
1365
+ const [mounted, setMounted] = react.useState(false);
1366
+ const [toast, setToast] = react.useState(null);
1367
+ const [selectedElement, setSelectedElement] = react.useState(null);
1368
+ const [highlightedRect, setHighlightedRect] = react.useState(
1369
+ null
1370
+ );
1371
+ const stylesInjected = react.useRef(false);
1372
+ const feedback = useFeedback();
1373
+ const feedbackResetRef = react.useRef(feedback.reset);
1374
+ feedbackResetRef.current = feedback.reset;
1375
+ const inspector = useElementSelector(state === "inspecting");
1376
+ react.useEffect(() => {
1377
+ if (stylesInjected.current) return;
1378
+ if (typeof document === "undefined") return;
1379
+ const existing = document.querySelector("[data-devtools-styles]");
1380
+ if (existing) return;
1381
+ const style = document.createElement("style");
1382
+ style.setAttribute("data-devtools-styles", "");
1383
+ style.textContent = KEYFRAMES;
1384
+ document.head.appendChild(style);
1385
+ stylesInjected.current = true;
1386
+ return () => {
1387
+ style.remove();
1388
+ stylesInjected.current = false;
1389
+ };
1390
+ }, []);
1391
+ react.useEffect(() => {
1392
+ setMounted(true);
1393
+ }, []);
1394
+ react.useEffect(() => {
1395
+ if (!mounted || !enabled) return;
1396
+ feedback.changes.forEach((change) => {
1397
+ const el = document.querySelector(change.selector);
1398
+ if (el) {
1399
+ el.style.outline = "2px solid rgba(59, 130, 246, 0.5)";
1400
+ el.style.outlineOffset = "2px";
1401
+ }
1402
+ });
1403
+ }, [mounted, enabled]);
1404
+ react.useEffect(() => {
1405
+ if (enabled && state === "hidden") {
1406
+ setState("inspecting");
1407
+ } else if (!enabled && state !== "hidden") {
1408
+ setState("hidden");
1409
+ feedbackResetRef.current();
1410
+ setSelectedElement(null);
1411
+ }
1412
+ }, [enabled, state]);
1413
+ const MAX_FEEDBACK = 5;
1414
+ react.useEffect(() => {
1415
+ if (inspector.selectedElement) {
1416
+ const el = inspector.selectedElement;
1417
+ const selector = getCssSelector(el);
1418
+ const existing = feedback.changes.find((c) => c.selector === selector);
1419
+ if (!existing && feedback.changes.length >= MAX_FEEDBACK) {
1420
+ setToast({
1421
+ message: `Limit reached (${MAX_FEEDBACK}). Copy feedback & create a ClickUp task, then reset to continue.`,
1422
+ variant: "warning"
1423
+ });
1424
+ inspector.clearSelection();
1425
+ return;
1426
+ }
1427
+ setSelectedElement(el);
1428
+ setState("editing");
1429
+ if (!existing) {
1430
+ const isImage = el.tagName.toLowerCase() === "img";
1431
+ feedback.addChange({
1432
+ id: "",
1433
+ selector,
1434
+ element: getElementLabel(el),
1435
+ type: isImage ? "image" : "text",
1436
+ original: isImage ? el.src : (el.textContent || "").trim().slice(0, 200),
1437
+ modified: "",
1438
+ note: ""
1439
+ });
1440
+ }
1441
+ inspector.clearSelection();
1442
+ }
1443
+ }, [inspector.selectedElement, inspector.clearSelection]);
1444
+ react.useEffect(() => {
1445
+ if (state === "hidden") return;
1446
+ const handleLinkClick = (e) => {
1447
+ const target = e.target;
1448
+ if (isDevToolsElement(target)) return;
1449
+ const anchor = target.closest("a");
1450
+ if (anchor) {
1451
+ e.preventDefault();
1452
+ e.stopPropagation();
1453
+ }
1454
+ };
1455
+ document.addEventListener("click", handleLinkClick, true);
1456
+ return () => document.removeEventListener("click", handleLinkClick, true);
1457
+ }, [state]);
1458
+ const handleHide = react.useCallback(() => {
1459
+ feedbackResetRef.current();
1460
+ setSelectedElement(null);
1461
+ setEnabled(null);
1462
+ }, [setEnabled]);
1463
+ const handleStartInspecting = react.useCallback(() => {
1464
+ setState((s) => s === "inspecting" ? "idle" : "inspecting");
1465
+ setSelectedElement(null);
1466
+ setHighlightedRect(null);
1467
+ }, []);
1468
+ const handleBack = react.useCallback(() => {
1469
+ setSelectedElement(null);
1470
+ setState("inspecting");
1471
+ }, []);
1472
+ const handleCopyFeedback = react.useCallback(async () => {
1473
+ const payload = feedback.getPayload();
1474
+ const success = await copyFeedbackToClipboard(payload);
1475
+ if (success) {
1476
+ setToast({
1477
+ message: "Feedback copied!",
1478
+ variant: "success",
1479
+ linkUrl: requestUrl,
1480
+ linkText: "Submit here \u2192"
1481
+ });
1482
+ } else {
1483
+ setToast({ message: "Failed to copy \u2014 try again", variant: "warning" });
1484
+ }
1485
+ }, [feedback, requestUrl]);
1486
+ const handleReset = react.useCallback(() => {
1487
+ feedbackResetRef.current();
1488
+ setSelectedElement(null);
1489
+ setState("inspecting");
1490
+ }, []);
1491
+ const handleView = react.useCallback(() => {
1492
+ setState((s) => s === "viewing" ? "inspecting" : "viewing");
1493
+ setSelectedElement(null);
1494
+ setHighlightedRect(null);
1495
+ }, []);
1496
+ const scrollRafRef = react.useRef(0);
1497
+ const handleViewHover = react.useCallback((change) => {
1498
+ cancelAnimationFrame(scrollRafRef.current);
1499
+ if (!change) {
1500
+ setHighlightedRect(null);
1501
+ return;
1502
+ }
1503
+ const el = document.querySelector(change.selector);
1504
+ if (!el) {
1505
+ setHighlightedRect(null);
1506
+ return;
1507
+ }
1508
+ el.scrollIntoView({ behavior: "smooth", block: "center" });
1509
+ let frames = 0;
1510
+ const updateRect = () => {
1511
+ const r = el.getBoundingClientRect();
1512
+ setHighlightedRect({
1513
+ top: r.top,
1514
+ left: r.left,
1515
+ width: r.width,
1516
+ height: r.height
1517
+ });
1518
+ frames++;
1519
+ if (frames < 60) {
1520
+ scrollRafRef.current = requestAnimationFrame(updateRect);
1521
+ }
1522
+ };
1523
+ updateRect();
1524
+ }, []);
1525
+ const handleDeleteChange = react.useCallback(
1526
+ (changeId) => {
1527
+ feedback.resetElement(changeId);
1528
+ },
1529
+ [feedback.resetElement]
1530
+ );
1531
+ const handleSaveNote = react.useCallback(
1532
+ (note) => {
1533
+ if (selectedElement) {
1534
+ const selector = getCssSelector(selectedElement);
1535
+ const change = feedback.changes.find((c) => c.selector === selector);
1536
+ if (change) {
1537
+ feedback.updateNote(change.id, note);
1538
+ }
1539
+ }
1540
+ setSelectedElement(null);
1541
+ setState("inspecting");
1542
+ },
1543
+ [selectedElement, feedback.changes, feedback.updateNote]
1544
+ );
1545
+ const handleNoteClose = react.useCallback(() => {
1546
+ if (selectedElement) {
1547
+ const selector = getCssSelector(selectedElement);
1548
+ const change = feedback.changes.find((c) => c.selector === selector);
1549
+ if (change && !change.note && !change.modified) {
1550
+ feedback.resetElement(change.id);
1551
+ }
1552
+ }
1553
+ setSelectedElement(null);
1554
+ setState("inspecting");
1555
+ }, [selectedElement, feedback.changes, feedback.resetElement]);
1556
+ const handleElementReset = react.useCallback(() => {
1557
+ if (selectedElement) {
1558
+ const selector = getCssSelector(selectedElement);
1559
+ const change = feedback.changes.find((c) => c.selector === selector);
1560
+ if (change) {
1561
+ feedback.resetElement(change.id);
1562
+ }
1563
+ }
1564
+ setSelectedElement(null);
1565
+ setState("inspecting");
1566
+ }, [selectedElement, feedback.changes, feedback.resetElement]);
1567
+ if (!mounted || !enabled || typeof window === "undefined") return null;
1568
+ const showToolbar = state !== "hidden";
1569
+ const showInspector = state === "inspecting";
1570
+ return reactDom.createPortal(
1571
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-devtools-ignore": true, children: [
1572
+ showToolbar && /* @__PURE__ */ jsxRuntime.jsx(
1573
+ Toolbar,
1574
+ {
1575
+ state,
1576
+ changesCount: feedback.changes.length,
1577
+ onSelectElement: handleStartInspecting,
1578
+ onCopyFeedback: handleCopyFeedback,
1579
+ onView: handleView,
1580
+ onReset: handleReset,
1581
+ onHide: handleHide,
1582
+ onBack: handleBack,
1583
+ isViewing: state === "viewing",
1584
+ requestUrl,
1585
+ zIndex,
1586
+ position
1587
+ }
1588
+ ),
1589
+ showInspector && /* @__PURE__ */ jsxRuntime.jsx(
1590
+ Inspector,
1591
+ {
1592
+ rect: inspector.rect,
1593
+ label: inspector.label,
1594
+ zIndex
1595
+ }
1596
+ ),
1597
+ highlightedRect && state === "viewing" && /* @__PURE__ */ jsxRuntime.jsx(Inspector, { rect: highlightedRect, label: "", zIndex }),
1598
+ selectedElement && state === "editing" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1599
+ /* @__PURE__ */ jsxRuntime.jsx(
1600
+ Editor,
1601
+ {
1602
+ element: selectedElement,
1603
+ onTrackOriginal: feedback.trackOriginal,
1604
+ onAddChange: feedback.addChange
1605
+ }
1606
+ ),
1607
+ /* @__PURE__ */ jsxRuntime.jsx(
1608
+ NotePopup,
1609
+ {
1610
+ element: selectedElement,
1611
+ initialNote: feedback.changes.find(
1612
+ (c) => c.selector === getCssSelector(selectedElement)
1613
+ )?.note ?? "",
1614
+ onSave: handleSaveNote,
1615
+ onClose: handleNoteClose,
1616
+ zIndex
1617
+ }
1618
+ ),
1619
+ /* @__PURE__ */ jsxRuntime.jsx(
1620
+ ElementResetButton,
1621
+ {
1622
+ element: selectedElement,
1623
+ onReset: handleElementReset,
1624
+ zIndex
1625
+ }
1626
+ )
1627
+ ] }),
1628
+ state === "viewing" && /* @__PURE__ */ jsxRuntime.jsx(
1629
+ ViewPanel,
1630
+ {
1631
+ changes: feedback.changes,
1632
+ onDelete: handleDeleteChange,
1633
+ onHover: handleViewHover,
1634
+ zIndex,
1635
+ position
1636
+ }
1637
+ ),
1638
+ toast && /* @__PURE__ */ jsxRuntime.jsx(
1639
+ Toast,
1640
+ {
1641
+ message: toast.message,
1642
+ variant: toast.variant,
1643
+ duration: toast.linkUrl ? 6e3 : toast.variant === "warning" ? 5e3 : 3e3,
1644
+ linkUrl: toast.linkUrl,
1645
+ linkText: toast.linkText,
1646
+ onDone: () => setToast(null),
1647
+ zIndex,
1648
+ position
1649
+ }
1650
+ )
1651
+ ] }),
1652
+ document.body
1653
+ );
1654
+ }
1655
+
1656
+ exports.DevTools = DevTools;
1657
+ //# sourceMappingURL=index.cjs.map
1658
+ //# sourceMappingURL=index.cjs.map