@visual-agentic-dev/react-devtools 1.1.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.mjs ADDED
@@ -0,0 +1,350 @@
1
+ // src/components/DevToolsProvider.tsx
2
+ import {
3
+ createContext,
4
+ useContext,
5
+ useState as useState3,
6
+ useCallback,
7
+ useEffect as useEffect3
8
+ } from "react";
9
+
10
+ // src/overlay/Highlighter.tsx
11
+ import { useEffect, useState } from "react";
12
+ import { jsx } from "react/jsx-runtime";
13
+ var Highlighter = ({
14
+ element,
15
+ color = "rgba(66, 153, 225, 0.3)"
16
+ }) => {
17
+ const [rect, setRect] = useState(null);
18
+ useEffect(() => {
19
+ const update = () => setRect(element.getBoundingClientRect());
20
+ update();
21
+ const observer = new ResizeObserver(update);
22
+ observer.observe(element);
23
+ window.addEventListener("scroll", update, true);
24
+ window.addEventListener("resize", update);
25
+ return () => {
26
+ observer.disconnect();
27
+ window.removeEventListener("scroll", update, true);
28
+ window.removeEventListener("resize", update);
29
+ };
30
+ }, [element]);
31
+ if (!rect) return null;
32
+ return /* @__PURE__ */ jsx(
33
+ "div",
34
+ {
35
+ style: {
36
+ position: "fixed",
37
+ top: rect.top,
38
+ left: rect.left,
39
+ width: rect.width,
40
+ height: rect.height,
41
+ backgroundColor: color,
42
+ border: "2px solid #4299e1",
43
+ pointerEvents: "none",
44
+ zIndex: 999999,
45
+ transition: "all 0.1s ease",
46
+ boxSizing: "border-box"
47
+ },
48
+ "data-vdev-overlay": "highlighter"
49
+ }
50
+ );
51
+ };
52
+
53
+ // src/overlay/SelectionBox.tsx
54
+ import { useEffect as useEffect2, useState as useState2 } from "react";
55
+
56
+ // src/utils/sourceLocator.ts
57
+ function getReactFiber(element) {
58
+ const key = Object.keys(element).find(
59
+ (k) => k.startsWith("__reactFiber$") || k.startsWith("__reactInternalInstance$")
60
+ );
61
+ return key ? element[key] : null;
62
+ }
63
+ function findFiberWithSource(fiber) {
64
+ let current = fiber;
65
+ while (current) {
66
+ if (current._debugSource) {
67
+ return current;
68
+ }
69
+ current = current.return;
70
+ }
71
+ return null;
72
+ }
73
+ function getSourceFromFiber(element) {
74
+ const fiber = getReactFiber(element);
75
+ if (!fiber) return null;
76
+ const fiberWithSource = findFiberWithSource(fiber);
77
+ if (fiberWithSource?._debugSource) {
78
+ const { fileName, lineNumber, columnNumber } = fiberWithSource._debugSource;
79
+ return {
80
+ fileName,
81
+ lineNumber,
82
+ columnNumber: columnNumber || 1
83
+ };
84
+ }
85
+ return null;
86
+ }
87
+ function parseSourceAttr(attrValue) {
88
+ if (!attrValue) return null;
89
+ try {
90
+ const parsed = JSON.parse(attrValue);
91
+ if (typeof parsed.fileName === "string" && typeof parsed.lineNumber === "number" && typeof parsed.columnNumber === "number") {
92
+ return parsed;
93
+ }
94
+ } catch {
95
+ }
96
+ return null;
97
+ }
98
+ function findSourceElement(target, prefix = "vdev") {
99
+ let current = target;
100
+ while (current && current !== document.body) {
101
+ if (getSourceFromFiber(current)) {
102
+ return current;
103
+ }
104
+ current = current.parentElement;
105
+ }
106
+ return target.closest(`[data-${prefix}-file], [data-${prefix}-source]`);
107
+ }
108
+ function getSourceFromElement(element, prefix = "vdev") {
109
+ const fiberSource = getSourceFromFiber(element);
110
+ if (fiberSource) {
111
+ return fiberSource;
112
+ }
113
+ const fileName = element.getAttribute(`data-${prefix}-file`);
114
+ const lineStr = element.getAttribute(`data-${prefix}-line`);
115
+ const colStr = element.getAttribute(`data-${prefix}-col`);
116
+ if (fileName && lineStr) {
117
+ return {
118
+ fileName,
119
+ lineNumber: parseInt(lineStr, 10),
120
+ columnNumber: colStr ? parseInt(colStr, 10) : 1
121
+ };
122
+ }
123
+ const attrValue = element.getAttribute(`data-${prefix}-source`);
124
+ return parseSourceAttr(attrValue);
125
+ }
126
+
127
+ // src/overlay/SelectionBox.tsx
128
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
129
+ var SelectionBox = ({ element, prefix = "vdev" }) => {
130
+ const [rect, setRect] = useState2(null);
131
+ const [source, setSource] = useState2(null);
132
+ useEffect2(() => {
133
+ const update = () => setRect(element.getBoundingClientRect());
134
+ update();
135
+ setSource(getSourceFromElement(element, prefix));
136
+ const observer = new ResizeObserver(update);
137
+ observer.observe(element);
138
+ window.addEventListener("scroll", update, true);
139
+ window.addEventListener("resize", update);
140
+ return () => {
141
+ observer.disconnect();
142
+ window.removeEventListener("scroll", update, true);
143
+ window.removeEventListener("resize", update);
144
+ };
145
+ }, [element, prefix]);
146
+ if (!rect) return null;
147
+ const fileName = source?.fileName.split("/").pop() || "unknown";
148
+ const lineInfo = source ? `${fileName}:${source.lineNumber}` : "";
149
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
150
+ /* @__PURE__ */ jsx2(
151
+ "div",
152
+ {
153
+ style: {
154
+ position: "fixed",
155
+ top: rect.top,
156
+ left: rect.left,
157
+ width: rect.width,
158
+ height: rect.height,
159
+ border: "2px solid #6366f1",
160
+ backgroundColor: "rgba(99, 102, 241, 0.1)",
161
+ pointerEvents: "none",
162
+ zIndex: 999999,
163
+ boxSizing: "border-box"
164
+ },
165
+ "data-vdev-overlay": "selection"
166
+ }
167
+ ),
168
+ lineInfo && /* @__PURE__ */ jsx2(
169
+ "div",
170
+ {
171
+ style: {
172
+ position: "fixed",
173
+ top: Math.max(0, rect.top - 24),
174
+ left: rect.left,
175
+ backgroundColor: "#6366f1",
176
+ color: "white",
177
+ fontSize: "11px",
178
+ fontFamily: "monospace",
179
+ padding: "2px 6px",
180
+ borderRadius: "3px",
181
+ pointerEvents: "none",
182
+ zIndex: 999999,
183
+ whiteSpace: "nowrap"
184
+ },
185
+ "data-vdev-overlay": "label",
186
+ children: lineInfo
187
+ }
188
+ )
189
+ ] });
190
+ };
191
+
192
+ // src/utils/messaging.ts
193
+ var MESSAGE_SOURCE = "vdev-react-sdk";
194
+ function sendToExtension(message) {
195
+ window.postMessage(
196
+ { ...message, source: MESSAGE_SOURCE },
197
+ "*"
198
+ );
199
+ }
200
+ function notifyReady() {
201
+ sendToExtension({ type: "VDEV_SDK_READY" });
202
+ }
203
+ function createMessageHandler(handler) {
204
+ return (event) => {
205
+ if (event.source !== window) return;
206
+ if (event.data?.source === MESSAGE_SOURCE) return;
207
+ if (event.data?.type?.startsWith("VDEV_")) {
208
+ handler(event.data);
209
+ }
210
+ };
211
+ }
212
+
213
+ // src/components/DevToolsProvider.tsx
214
+ import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
215
+ var DevToolsContext = createContext(null);
216
+ var useDevTools = () => {
217
+ const context = useContext(DevToolsContext);
218
+ if (!context) {
219
+ throw new Error("useDevTools must be used within a DevToolsProvider");
220
+ }
221
+ return context;
222
+ };
223
+ var DevToolsProvider = ({
224
+ children,
225
+ enabled = true,
226
+ prefix = "vdev"
227
+ }) => {
228
+ const [isInspecting, setInspecting] = useState3(false);
229
+ const [hoveredElement, setHoveredElement] = useState3(null);
230
+ const [selectedElement, setSelectedElement] = useState3(null);
231
+ const [selectedSource, setSelectedSource] = useState3(null);
232
+ const clearSelection = useCallback(() => {
233
+ setSelectedElement(null);
234
+ setSelectedSource(null);
235
+ }, []);
236
+ useEffect3(() => {
237
+ if (!enabled) return;
238
+ const handler = createMessageHandler((message) => {
239
+ console.log("[DevTools] Received message:", message);
240
+ if (message.type === "VDEV_START_INSPECT") {
241
+ console.log("[DevTools] Starting inspection");
242
+ setInspecting(true);
243
+ clearSelection();
244
+ } else if (message.type === "VDEV_STOP_INSPECT") {
245
+ console.log("[DevTools] Stopping inspection");
246
+ setInspecting(false);
247
+ setHoveredElement(null);
248
+ } else if (message.type === "VDEV_TOGGLE_INSPECT") {
249
+ console.log("[DevTools] Toggling inspection");
250
+ setInspecting((prev) => {
251
+ const newState = !prev;
252
+ if (newState) {
253
+ clearSelection();
254
+ } else {
255
+ setHoveredElement(null);
256
+ }
257
+ sendToExtension({
258
+ type: "VDEV_INSPECT_STATE_CHANGED",
259
+ payload: { isInspecting: newState }
260
+ });
261
+ return newState;
262
+ });
263
+ } else if (message.type === "VDEV_CLEAR_SELECTION") {
264
+ clearSelection();
265
+ }
266
+ });
267
+ window.addEventListener("message", handler);
268
+ notifyReady();
269
+ console.log("[DevTools] SDK Ready, listening for messages");
270
+ return () => window.removeEventListener("message", handler);
271
+ }, [enabled, clearSelection]);
272
+ useEffect3(() => {
273
+ if (!isInspecting || !enabled) return;
274
+ const handleMouseMove = (e) => {
275
+ const target = e.target;
276
+ if (target.hasAttribute("data-vdev-overlay")) return;
277
+ const sourceElement = findSourceElement(target, prefix);
278
+ if (sourceElement && sourceElement !== hoveredElement) {
279
+ console.log("[DevTools] Hovered element found:", sourceElement);
280
+ setHoveredElement(sourceElement);
281
+ }
282
+ };
283
+ const handleClick = (e) => {
284
+ e.preventDefault();
285
+ e.stopPropagation();
286
+ const target = e.target;
287
+ if (target.hasAttribute("data-vdev-overlay")) return;
288
+ const sourceElement = findSourceElement(target, prefix);
289
+ if (sourceElement) {
290
+ const source = getSourceFromElement(sourceElement, prefix);
291
+ setSelectedElement(sourceElement);
292
+ setSelectedSource(source);
293
+ setInspecting(false);
294
+ setHoveredElement(null);
295
+ sendToExtension({
296
+ type: "VDEV_ELEMENT_SELECTED",
297
+ payload: {
298
+ source,
299
+ elementInfo: {
300
+ tagName: sourceElement.tagName.toLowerCase(),
301
+ className: sourceElement.className,
302
+ textContent: sourceElement.textContent?.slice(0, 100) || ""
303
+ }
304
+ }
305
+ });
306
+ }
307
+ };
308
+ document.addEventListener("mousemove", handleMouseMove, true);
309
+ document.addEventListener("click", handleClick, true);
310
+ document.body.style.cursor = "crosshair";
311
+ return () => {
312
+ document.removeEventListener("mousemove", handleMouseMove, true);
313
+ document.removeEventListener("click", handleClick, true);
314
+ document.body.style.cursor = "";
315
+ };
316
+ }, [isInspecting, enabled, hoveredElement, prefix]);
317
+ if (!enabled) {
318
+ return /* @__PURE__ */ jsx3(Fragment2, { children });
319
+ }
320
+ return /* @__PURE__ */ jsxs2(
321
+ DevToolsContext.Provider,
322
+ {
323
+ value: {
324
+ isInspecting,
325
+ setInspecting,
326
+ selectedElement,
327
+ selectedSource,
328
+ clearSelection
329
+ },
330
+ children: [
331
+ children,
332
+ isInspecting && hoveredElement && /* @__PURE__ */ jsx3(Highlighter, { element: hoveredElement }),
333
+ selectedElement && /* @__PURE__ */ jsx3(SelectionBox, { element: selectedElement, prefix })
334
+ ]
335
+ }
336
+ );
337
+ };
338
+ export {
339
+ DevToolsProvider,
340
+ Highlighter,
341
+ SelectionBox,
342
+ createMessageHandler,
343
+ findSourceElement,
344
+ getSourceFromElement,
345
+ notifyReady,
346
+ parseSourceAttr,
347
+ sendToExtension,
348
+ useDevTools
349
+ };
350
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/DevToolsProvider.tsx","../src/overlay/Highlighter.tsx","../src/overlay/SelectionBox.tsx","../src/utils/sourceLocator.ts","../src/utils/messaging.ts"],"sourcesContent":["import React, {\n createContext,\n useContext,\n useState,\n useCallback,\n useEffect,\n type ReactNode\n} from 'react';\nimport { Highlighter } from '../overlay/Highlighter';\nimport { SelectionBox } from '../overlay/SelectionBox';\nimport { getSourceFromElement, findSourceElement } from '../utils/sourceLocator';\nimport { sendToExtension, createMessageHandler, notifyReady } from '../utils/messaging';\nimport type { SourceLocation, VDevMessage } from '../types';\n\ninterface DevToolsContextValue {\n isInspecting: boolean;\n setInspecting: (v: boolean) => void;\n selectedElement: HTMLElement | null;\n selectedSource: SourceLocation | null;\n clearSelection: () => void;\n}\n\nconst DevToolsContext = createContext<DevToolsContextValue | null>(null);\n\nexport const useDevTools = () => {\n const context = useContext(DevToolsContext);\n if (!context) {\n throw new Error('useDevTools must be used within a DevToolsProvider');\n }\n return context;\n};\n\ninterface DevToolsProviderProps {\n children: ReactNode;\n /** Only enable in development mode (default: true) */\n enabled?: boolean;\n /** Attribute prefix (default: 'vdev') */\n prefix?: string;\n}\n\nexport const DevToolsProvider: React.FC<DevToolsProviderProps> = ({\n children,\n enabled = true,\n prefix = 'vdev'\n}) => {\n const [isInspecting, setInspecting] = useState(false);\n const [hoveredElement, setHoveredElement] = useState<HTMLElement | null>(null);\n const [selectedElement, setSelectedElement] = useState<HTMLElement | null>(null);\n const [selectedSource, setSelectedSource] = useState<SourceLocation | null>(null);\n\n const clearSelection = useCallback(() => {\n setSelectedElement(null);\n setSelectedSource(null);\n }, []);\n\n // Listen to messages from extension\n useEffect(() => {\n if (!enabled) return;\n\n const handler = createMessageHandler((message: VDevMessage) => {\n console.log('[DevTools] Received message:', message);\n if (message.type === 'VDEV_START_INSPECT') {\n console.log('[DevTools] Starting inspection');\n setInspecting(true);\n clearSelection();\n } else if (message.type === 'VDEV_STOP_INSPECT') {\n console.log('[DevTools] Stopping inspection');\n setInspecting(false);\n setHoveredElement(null);\n } else if (message.type === 'VDEV_TOGGLE_INSPECT') {\n console.log('[DevTools] Toggling inspection');\n setInspecting(prev => {\n const newState = !prev;\n if (newState) {\n // Starting inspection, clear selection\n clearSelection();\n } else {\n // Stopping inspection, clear hovered\n setHoveredElement(null);\n }\n // Notify extension of state change\n sendToExtension({\n type: 'VDEV_INSPECT_STATE_CHANGED',\n payload: { isInspecting: newState }\n });\n return newState;\n });\n } else if (message.type === 'VDEV_CLEAR_SELECTION') {\n clearSelection();\n }\n });\n\n window.addEventListener('message', handler);\n\n // Notify extension that SDK is ready\n notifyReady();\n console.log('[DevTools] SDK Ready, listening for messages');\n\n return () => window.removeEventListener('message', handler);\n }, [enabled, clearSelection]);\n\n // Handle mouse events in inspect mode\n useEffect(() => {\n if (!isInspecting || !enabled) return;\n\n const handleMouseMove = (e: MouseEvent) => {\n const target = e.target as HTMLElement;\n\n // Ignore our own overlay elements\n if (target.hasAttribute('data-vdev-overlay')) return;\n\n const sourceElement = findSourceElement(target, prefix);\n // console.log('[DevTools] Mouse move', target, sourceElement);\n\n if (sourceElement && sourceElement !== hoveredElement) {\n console.log('[DevTools] Hovered element found:', sourceElement);\n setHoveredElement(sourceElement);\n }\n };\n\n const handleClick = (e: MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n\n const target = e.target as HTMLElement;\n\n // Ignore clicks on overlay\n if (target.hasAttribute('data-vdev-overlay')) return;\n\n const sourceElement = findSourceElement(target, prefix);\n\n if (sourceElement) {\n const source = getSourceFromElement(sourceElement, prefix);\n setSelectedElement(sourceElement);\n setSelectedSource(source);\n setInspecting(false);\n setHoveredElement(null);\n\n // Notify extension of selection\n sendToExtension({\n type: 'VDEV_ELEMENT_SELECTED',\n payload: {\n source,\n elementInfo: {\n tagName: sourceElement.tagName.toLowerCase(),\n className: sourceElement.className,\n textContent: sourceElement.textContent?.slice(0, 100) || '',\n },\n },\n });\n }\n };\n\n // Use capture to intercept before normal handlers\n document.addEventListener('mousemove', handleMouseMove, true);\n document.addEventListener('click', handleClick, true);\n\n // Add cursor style\n document.body.style.cursor = 'crosshair';\n\n return () => {\n document.removeEventListener('mousemove', handleMouseMove, true);\n document.removeEventListener('click', handleClick, true);\n document.body.style.cursor = '';\n };\n }, [isInspecting, enabled, hoveredElement, prefix]);\n\n // Don't render anything if disabled\n if (!enabled) {\n return <>{children}</>;\n }\n\n return (\n <DevToolsContext.Provider\n value={{\n isInspecting,\n setInspecting,\n selectedElement,\n selectedSource,\n clearSelection\n }}\n >\n {children}\n\n {/* Hover highlighter */}\n {isInspecting && hoveredElement && (\n <Highlighter element={hoveredElement} />\n )}\n\n {/* Selection box */}\n {selectedElement && (\n <SelectionBox element={selectedElement} prefix={prefix} />\n )}\n </DevToolsContext.Provider>\n );\n};\n","import React, { useEffect, useState } from 'react';\n\ninterface Props {\n element: HTMLElement;\n color?: string;\n}\n\n/**\n * Highlighter overlay component - shows a blue overlay on hovered elements\n */\nexport const Highlighter: React.FC<Props> = ({\n element,\n color = 'rgba(66, 153, 225, 0.3)'\n}) => {\n const [rect, setRect] = useState<DOMRect | null>(null);\n\n useEffect(() => {\n const update = () => setRect(element.getBoundingClientRect());\n update();\n\n const observer = new ResizeObserver(update);\n observer.observe(element);\n\n window.addEventListener('scroll', update, true);\n window.addEventListener('resize', update);\n\n return () => {\n observer.disconnect();\n window.removeEventListener('scroll', update, true);\n window.removeEventListener('resize', update);\n };\n }, [element]);\n\n if (!rect) return null;\n\n return (\n <div\n style={{\n position: 'fixed',\n top: rect.top,\n left: rect.left,\n width: rect.width,\n height: rect.height,\n backgroundColor: color,\n border: '2px solid #4299e1',\n pointerEvents: 'none',\n zIndex: 999999,\n transition: 'all 0.1s ease',\n boxSizing: 'border-box',\n }}\n data-vdev-overlay=\"highlighter\"\n />\n );\n};\n","import React, { useEffect, useState } from 'react';\nimport type { SourceLocation } from '../types';\nimport { getSourceFromElement } from '../utils/sourceLocator';\n\ninterface Props {\n element: HTMLElement;\n prefix?: string;\n}\n\n/**\n * SelectionBox component - shows a persistent selection box with source info label\n */\nexport const SelectionBox: React.FC<Props> = ({ element, prefix = 'vdev' }) => {\n const [rect, setRect] = useState<DOMRect | null>(null);\n const [source, setSource] = useState<SourceLocation | null>(null);\n\n useEffect(() => {\n const update = () => setRect(element.getBoundingClientRect());\n update();\n\n // Get source info\n setSource(getSourceFromElement(element, prefix));\n\n const observer = new ResizeObserver(update);\n observer.observe(element);\n\n window.addEventListener('scroll', update, true);\n window.addEventListener('resize', update);\n\n return () => {\n observer.disconnect();\n window.removeEventListener('scroll', update, true);\n window.removeEventListener('resize', update);\n };\n }, [element, prefix]);\n\n if (!rect) return null;\n\n // Format file path for display (show only basename)\n const fileName = source?.fileName.split('/').pop() || 'unknown';\n const lineInfo = source ? `${fileName}:${source.lineNumber}` : '';\n\n return (\n <>\n {/* Selection border */}\n <div\n style={{\n position: 'fixed',\n top: rect.top,\n left: rect.left,\n width: rect.width,\n height: rect.height,\n border: '2px solid #6366f1',\n backgroundColor: 'rgba(99, 102, 241, 0.1)',\n pointerEvents: 'none',\n zIndex: 999999,\n boxSizing: 'border-box',\n }}\n data-vdev-overlay=\"selection\"\n />\n\n {/* Label showing file:line */}\n {lineInfo && (\n <div\n style={{\n position: 'fixed',\n top: Math.max(0, rect.top - 24),\n left: rect.left,\n backgroundColor: '#6366f1',\n color: 'white',\n fontSize: '11px',\n fontFamily: 'monospace',\n padding: '2px 6px',\n borderRadius: '3px',\n pointerEvents: 'none',\n zIndex: 999999,\n whiteSpace: 'nowrap',\n }}\n data-vdev-overlay=\"label\"\n >\n {lineInfo}\n </div>\n )}\n </>\n );\n};\n","import type { SourceLocation } from '../types';\n\n/**\n * Get React Fiber node from DOM element\n * React attaches fiber information to DOM elements with internal keys\n */\nfunction getReactFiber(element: HTMLElement): any {\n const key = Object.keys(element).find(\n k => k.startsWith('__reactFiber$') || k.startsWith('__reactInternalInstance$')\n );\n return key ? (element as any)[key] : null;\n}\n\n/**\n * Walk up the fiber tree to find a fiber with _debugSource\n */\nfunction findFiberWithSource(fiber: any): any {\n let current = fiber;\n while (current) {\n if (current._debugSource) {\n return current;\n }\n current = current.return;\n }\n return null;\n}\n\n/**\n * Get source from React Fiber's _debugSource (runtime detection)\n * This is the preferred method as it doesn't require any build plugin\n */\nexport function getSourceFromFiber(element: HTMLElement): SourceLocation | null {\n const fiber = getReactFiber(element);\n if (!fiber) return null;\n\n const fiberWithSource = findFiberWithSource(fiber);\n if (fiberWithSource?._debugSource) {\n const { fileName, lineNumber, columnNumber } = fiberWithSource._debugSource;\n return {\n fileName,\n lineNumber,\n columnNumber: columnNumber || 1\n };\n }\n return null;\n}\n\n/**\n * Parse the data-vdev-source attribute value into a SourceLocation object\n * (Legacy support for projects using the babel/vite plugin)\n */\nexport function parseSourceAttr(attrValue: string | null): SourceLocation | null {\n if (!attrValue) return null;\n\n try {\n const parsed = JSON.parse(attrValue);\n if (\n typeof parsed.fileName === 'string' &&\n typeof parsed.lineNumber === 'number' &&\n typeof parsed.columnNumber === 'number'\n ) {\n return parsed as SourceLocation;\n }\n } catch {\n // Ignore parse errors\n }\n\n return null;\n}\n\n/**\n * Find the closest element with source information\n * First tries React Fiber (runtime), then falls back to data attributes\n */\nexport function findSourceElement(target: HTMLElement, prefix = 'vdev'): HTMLElement | null {\n // First, try to find source from React Fiber (runtime)\n let current: HTMLElement | null = target;\n while (current && current !== document.body) {\n if (getSourceFromFiber(current)) {\n return current;\n }\n current = current.parentElement;\n }\n\n // Fallback: check for data attributes (legacy plugin support)\n return target.closest(`[data-${prefix}-file], [data-${prefix}-source]`) as HTMLElement | null;\n}\n\n/**\n * Get source location from an element\n * Prioritizes React Fiber _debugSource, falls back to data attributes\n */\nexport function getSourceFromElement(element: HTMLElement, prefix = 'vdev'): SourceLocation | null {\n // 1. Try React Fiber _debugSource first (runtime, no plugin needed)\n const fiberSource = getSourceFromFiber(element);\n if (fiberSource) {\n return fiberSource;\n }\n\n // 2. Try new data attribute format (plugin-based)\n const fileName = element.getAttribute(`data-${prefix}-file`);\n const lineStr = element.getAttribute(`data-${prefix}-line`);\n const colStr = element.getAttribute(`data-${prefix}-col`);\n\n if (fileName && lineStr) {\n return {\n fileName,\n lineNumber: parseInt(lineStr, 10),\n columnNumber: colStr ? parseInt(colStr, 10) : 1\n };\n }\n\n // 3. Fallback to legacy format (JSON in single attribute)\n const attrValue = element.getAttribute(`data-${prefix}-source`);\n return parseSourceAttr(attrValue);\n}\n\n","import type { VDevMessage } from '../types';\n\nconst MESSAGE_SOURCE = 'vdev-react-sdk';\n\n/**\n * Send a message to the Chrome extension via window.postMessage\n */\nexport function sendToExtension(message: VDevMessage): void {\n window.postMessage(\n { ...message, source: MESSAGE_SOURCE },\n '*'\n );\n}\n\n/**\n * Notify extension that SDK is ready\n */\nexport function notifyReady(): void {\n sendToExtension({ type: 'VDEV_SDK_READY' });\n}\n\n/**\n * Create a message handler that only processes messages from the extension\n */\nexport function createMessageHandler(\n handler: (message: VDevMessage) => void\n): (event: MessageEvent) => void {\n return (event: MessageEvent) => {\n // Only process messages from the same window\n if (event.source !== window) return;\n\n // Only process messages from extension (not from SDK itself)\n if (event.data?.source === MESSAGE_SOURCE) return;\n\n // Process VDEV messages\n if (event.data?.type?.startsWith('VDEV_')) {\n handler(event.data as VDevMessage);\n }\n };\n}\n"],"mappings":"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","names":["useState","useEffect","useEffect","useState","jsx","useState","useEffect","Fragment","jsx","jsxs","useState","useEffect"]}
@@ -0,0 +1,17 @@
1
+ import { Plugin } from 'vite';
2
+
3
+ interface VdevPluginOptions {
4
+ /** Prefix for the data attribute (default: 'vdev') */
5
+ prefix?: string;
6
+ /** File patterns to exclude */
7
+ exclude?: RegExp[];
8
+ }
9
+ /**
10
+ * Vite plugin to inject source location data attributes into JSX elements.
11
+ * This enables the Visual Dev Tool to locate source code from rendered elements.
12
+ *
13
+ * Works with @vitejs/plugin-react-swc for stable HMR support.
14
+ */
15
+ declare function vdevJsxSource(options?: VdevPluginOptions): Plugin;
16
+
17
+ export { type VdevPluginOptions, vdevJsxSource as default, vdevJsxSource };
@@ -0,0 +1,17 @@
1
+ import { Plugin } from 'vite';
2
+
3
+ interface VdevPluginOptions {
4
+ /** Prefix for the data attribute (default: 'vdev') */
5
+ prefix?: string;
6
+ /** File patterns to exclude */
7
+ exclude?: RegExp[];
8
+ }
9
+ /**
10
+ * Vite plugin to inject source location data attributes into JSX elements.
11
+ * This enables the Visual Dev Tool to locate source code from rendered elements.
12
+ *
13
+ * Works with @vitejs/plugin-react-swc for stable HMR support.
14
+ */
15
+ declare function vdevJsxSource(options?: VdevPluginOptions): Plugin;
16
+
17
+ export { type VdevPluginOptions, vdevJsxSource as default, vdevJsxSource };
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
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
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/vite-plugin/jsx-source.ts
31
+ var jsx_source_exports = {};
32
+ __export(jsx_source_exports, {
33
+ default: () => jsx_source_default,
34
+ vdevJsxSource: () => vdevJsxSource
35
+ });
36
+ module.exports = __toCommonJS(jsx_source_exports);
37
+ var import_magic_string = __toESM(require("magic-string"));
38
+ function vdevJsxSource(options = {}) {
39
+ const prefix = options.prefix || "vdev";
40
+ const exclude = options.exclude || [/node_modules/];
41
+ return {
42
+ name: "vdev-jsx-source",
43
+ // Use 'post' to run after other transforms, avoiding HMR issues
44
+ enforce: "post",
45
+ transform(code, id) {
46
+ if (exclude.some((pattern) => pattern.test(id))) {
47
+ return null;
48
+ }
49
+ if (!/\.[jt]sx$/.test(id)) {
50
+ return null;
51
+ }
52
+ if (!code.includes("jsx(") && !code.includes("jsxs(") && !code.includes("jsxDEV(")) {
53
+ return null;
54
+ }
55
+ const magicString = new import_magic_string.default(code);
56
+ let modified = false;
57
+ const jsxCallRegex = /\b(jsx|jsxs|jsxDEV)\s*\(\s*(?:"([^"]+)"|'([^']+)'|([A-Z][a-zA-Z0-9_$.]*)|([a-z][a-zA-Z0-9_]*))\s*,\s*\{/g;
58
+ const lines = code.split("\n");
59
+ const getLineCol = (index) => {
60
+ let line = 1;
61
+ let lastNewline = -1;
62
+ for (let i = 0; i < index && i < code.length; i++) {
63
+ if (code[i] === "\n") {
64
+ line++;
65
+ lastNewline = i;
66
+ }
67
+ }
68
+ return { line, col: index - lastNewline - 1 };
69
+ };
70
+ let match;
71
+ while ((match = jsxCallRegex.exec(code)) !== null) {
72
+ const openBraceIndex = match.index + match[0].length - 1;
73
+ const { line, col } = getLineCol(match.index);
74
+ const nextChars = code.slice(openBraceIndex + 1, openBraceIndex + 100);
75
+ if (nextChars.includes(`"data-${prefix}-file"`)) {
76
+ continue;
77
+ }
78
+ const injection = `"data-${prefix}-file": "${id}", "data-${prefix}-line": "${line}", "data-${prefix}-col": "${col}", `;
79
+ magicString.appendLeft(openBraceIndex + 1, injection);
80
+ modified = true;
81
+ }
82
+ if (!modified) {
83
+ return null;
84
+ }
85
+ return {
86
+ code: magicString.toString(),
87
+ map: magicString.generateMap({ hires: true })
88
+ };
89
+ }
90
+ };
91
+ }
92
+ var jsx_source_default = vdevJsxSource;
93
+ // Annotate the CommonJS export names for ESM import in node:
94
+ 0 && (module.exports = {
95
+ vdevJsxSource
96
+ });
97
+ //# sourceMappingURL=jsx-source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/vite-plugin/jsx-source.ts"],"sourcesContent":["import type { Plugin, TransformResult } from 'vite';\nimport MagicString from 'magic-string';\n\nexport interface VdevPluginOptions {\n /** Prefix for the data attribute (default: 'vdev') */\n prefix?: string;\n /** File patterns to exclude */\n exclude?: RegExp[];\n}\n\n/**\n * Vite plugin to inject source location data attributes into JSX elements.\n * This enables the Visual Dev Tool to locate source code from rendered elements.\n * \n * Works with @vitejs/plugin-react-swc for stable HMR support.\n */\nexport function vdevJsxSource(options: VdevPluginOptions = {}): Plugin {\n const prefix = options.prefix || 'vdev';\n const exclude = options.exclude || [/node_modules/];\n\n return {\n name: 'vdev-jsx-source',\n // Use 'post' to run after other transforms, avoiding HMR issues\n enforce: 'post',\n\n transform(code: string, id: string): TransformResult | null {\n // Skip excluded files\n if (exclude.some(pattern => pattern.test(id))) {\n return null;\n }\n\n // Only process JSX/TSX files\n if (!/\\.[jt]sx$/.test(id)) {\n return null;\n }\n\n // Skip if no JSX-like patterns (quick check)\n if (!code.includes('jsx(') && !code.includes('jsxs(') && !code.includes('jsxDEV(')) {\n return null;\n }\n\n // After SWC transform, JSX becomes function calls like:\n // jsx(\"div\", { ... }) or jsxDEV(\"div\", { ... }, ...)\n // We inject our data attributes into the props object\n\n const magicString = new MagicString(code);\n let modified = false;\n\n // Match jsx/jsxs/jsxDEV function calls\n // Pattern: jsx(\"tagName\", { props }) or jsx(Component, { props })\n const jsxCallRegex = /\\b(jsx|jsxs|jsxDEV)\\s*\\(\\s*(?:\"([^\"]+)\"|'([^']+)'|([A-Z][a-zA-Z0-9_$.]*)|([a-z][a-zA-Z0-9_]*))\\s*,\\s*\\{/g;\n\n // Track line numbers for source location\n const lines = code.split('\\n');\n const getLineCol = (index: number) => {\n let line = 1;\n let lastNewline = -1;\n for (let i = 0; i < index && i < code.length; i++) {\n if (code[i] === '\\n') {\n line++;\n lastNewline = i;\n }\n }\n return { line, col: index - lastNewline - 1 };\n };\n\n let match;\n while ((match = jsxCallRegex.exec(code)) !== null) {\n const openBraceIndex = match.index + match[0].length - 1;\n const { line, col } = getLineCol(match.index);\n\n // Check if already has our data attribute\n const nextChars = code.slice(openBraceIndex + 1, openBraceIndex + 100);\n if (nextChars.includes(`\"data-${prefix}-file\"`)) {\n continue;\n }\n\n // Inject data attributes as first properties\n const injection = `\"data-${prefix}-file\": \"${id}\", \"data-${prefix}-line\": \"${line}\", \"data-${prefix}-col\": \"${col}\", `;\n\n magicString.appendLeft(openBraceIndex + 1, injection);\n modified = true;\n }\n\n if (!modified) {\n return null;\n }\n\n return {\n code: magicString.toString(),\n map: magicString.generateMap({ hires: true })\n };\n }\n };\n}\n\nexport default vdevJsxSource;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,0BAAwB;AAejB,SAAS,cAAc,UAA6B,CAAC,GAAW;AACnE,QAAM,SAAS,QAAQ,UAAU;AACjC,QAAM,UAAU,QAAQ,WAAW,CAAC,cAAc;AAElD,SAAO;AAAA,IACH,MAAM;AAAA;AAAA,IAEN,SAAS;AAAA,IAET,UAAU,MAAc,IAAoC;AAExD,UAAI,QAAQ,KAAK,aAAW,QAAQ,KAAK,EAAE,CAAC,GAAG;AAC3C,eAAO;AAAA,MACX;AAGA,UAAI,CAAC,YAAY,KAAK,EAAE,GAAG;AACvB,eAAO;AAAA,MACX;AAGA,UAAI,CAAC,KAAK,SAAS,MAAM,KAAK,CAAC,KAAK,SAAS,OAAO,KAAK,CAAC,KAAK,SAAS,SAAS,GAAG;AAChF,eAAO;AAAA,MACX;AAMA,YAAM,cAAc,IAAI,oBAAAA,QAAY,IAAI;AACxC,UAAI,WAAW;AAIf,YAAM,eAAe;AAGrB,YAAM,QAAQ,KAAK,MAAM,IAAI;AAC7B,YAAM,aAAa,CAAC,UAAkB;AAClC,YAAI,OAAO;AACX,YAAI,cAAc;AAClB,iBAAS,IAAI,GAAG,IAAI,SAAS,IAAI,KAAK,QAAQ,KAAK;AAC/C,cAAI,KAAK,CAAC,MAAM,MAAM;AAClB;AACA,0BAAc;AAAA,UAClB;AAAA,QACJ;AACA,eAAO,EAAE,MAAM,KAAK,QAAQ,cAAc,EAAE;AAAA,MAChD;AAEA,UAAI;AACJ,cAAQ,QAAQ,aAAa,KAAK,IAAI,OAAO,MAAM;AAC/C,cAAM,iBAAiB,MAAM,QAAQ,MAAM,CAAC,EAAE,SAAS;AACvD,cAAM,EAAE,MAAM,IAAI,IAAI,WAAW,MAAM,KAAK;AAG5C,cAAM,YAAY,KAAK,MAAM,iBAAiB,GAAG,iBAAiB,GAAG;AACrE,YAAI,UAAU,SAAS,SAAS,MAAM,QAAQ,GAAG;AAC7C;AAAA,QACJ;AAGA,cAAM,YAAY,SAAS,MAAM,YAAY,EAAE,YAAY,MAAM,YAAY,IAAI,YAAY,MAAM,WAAW,GAAG;AAEjH,oBAAY,WAAW,iBAAiB,GAAG,SAAS;AACpD,mBAAW;AAAA,MACf;AAEA,UAAI,CAAC,UAAU;AACX,eAAO;AAAA,MACX;AAEA,aAAO;AAAA,QACH,MAAM,YAAY,SAAS;AAAA,QAC3B,KAAK,YAAY,YAAY,EAAE,OAAO,KAAK,CAAC;AAAA,MAChD;AAAA,IACJ;AAAA,EACJ;AACJ;AAEA,IAAO,qBAAQ;","names":["MagicString"]}
@@ -0,0 +1,62 @@
1
+ // src/vite-plugin/jsx-source.ts
2
+ import MagicString from "magic-string";
3
+ function vdevJsxSource(options = {}) {
4
+ const prefix = options.prefix || "vdev";
5
+ const exclude = options.exclude || [/node_modules/];
6
+ return {
7
+ name: "vdev-jsx-source",
8
+ // Use 'post' to run after other transforms, avoiding HMR issues
9
+ enforce: "post",
10
+ transform(code, id) {
11
+ if (exclude.some((pattern) => pattern.test(id))) {
12
+ return null;
13
+ }
14
+ if (!/\.[jt]sx$/.test(id)) {
15
+ return null;
16
+ }
17
+ if (!code.includes("jsx(") && !code.includes("jsxs(") && !code.includes("jsxDEV(")) {
18
+ return null;
19
+ }
20
+ const magicString = new MagicString(code);
21
+ let modified = false;
22
+ const jsxCallRegex = /\b(jsx|jsxs|jsxDEV)\s*\(\s*(?:"([^"]+)"|'([^']+)'|([A-Z][a-zA-Z0-9_$.]*)|([a-z][a-zA-Z0-9_]*))\s*,\s*\{/g;
23
+ const lines = code.split("\n");
24
+ const getLineCol = (index) => {
25
+ let line = 1;
26
+ let lastNewline = -1;
27
+ for (let i = 0; i < index && i < code.length; i++) {
28
+ if (code[i] === "\n") {
29
+ line++;
30
+ lastNewline = i;
31
+ }
32
+ }
33
+ return { line, col: index - lastNewline - 1 };
34
+ };
35
+ let match;
36
+ while ((match = jsxCallRegex.exec(code)) !== null) {
37
+ const openBraceIndex = match.index + match[0].length - 1;
38
+ const { line, col } = getLineCol(match.index);
39
+ const nextChars = code.slice(openBraceIndex + 1, openBraceIndex + 100);
40
+ if (nextChars.includes(`"data-${prefix}-file"`)) {
41
+ continue;
42
+ }
43
+ const injection = `"data-${prefix}-file": "${id}", "data-${prefix}-line": "${line}", "data-${prefix}-col": "${col}", `;
44
+ magicString.appendLeft(openBraceIndex + 1, injection);
45
+ modified = true;
46
+ }
47
+ if (!modified) {
48
+ return null;
49
+ }
50
+ return {
51
+ code: magicString.toString(),
52
+ map: magicString.generateMap({ hires: true })
53
+ };
54
+ }
55
+ };
56
+ }
57
+ var jsx_source_default = vdevJsxSource;
58
+ export {
59
+ jsx_source_default as default,
60
+ vdevJsxSource
61
+ };
62
+ //# sourceMappingURL=jsx-source.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/vite-plugin/jsx-source.ts"],"sourcesContent":["import type { Plugin, TransformResult } from 'vite';\nimport MagicString from 'magic-string';\n\nexport interface VdevPluginOptions {\n /** Prefix for the data attribute (default: 'vdev') */\n prefix?: string;\n /** File patterns to exclude */\n exclude?: RegExp[];\n}\n\n/**\n * Vite plugin to inject source location data attributes into JSX elements.\n * This enables the Visual Dev Tool to locate source code from rendered elements.\n * \n * Works with @vitejs/plugin-react-swc for stable HMR support.\n */\nexport function vdevJsxSource(options: VdevPluginOptions = {}): Plugin {\n const prefix = options.prefix || 'vdev';\n const exclude = options.exclude || [/node_modules/];\n\n return {\n name: 'vdev-jsx-source',\n // Use 'post' to run after other transforms, avoiding HMR issues\n enforce: 'post',\n\n transform(code: string, id: string): TransformResult | null {\n // Skip excluded files\n if (exclude.some(pattern => pattern.test(id))) {\n return null;\n }\n\n // Only process JSX/TSX files\n if (!/\\.[jt]sx$/.test(id)) {\n return null;\n }\n\n // Skip if no JSX-like patterns (quick check)\n if (!code.includes('jsx(') && !code.includes('jsxs(') && !code.includes('jsxDEV(')) {\n return null;\n }\n\n // After SWC transform, JSX becomes function calls like:\n // jsx(\"div\", { ... }) or jsxDEV(\"div\", { ... }, ...)\n // We inject our data attributes into the props object\n\n const magicString = new MagicString(code);\n let modified = false;\n\n // Match jsx/jsxs/jsxDEV function calls\n // Pattern: jsx(\"tagName\", { props }) or jsx(Component, { props })\n const jsxCallRegex = /\\b(jsx|jsxs|jsxDEV)\\s*\\(\\s*(?:\"([^\"]+)\"|'([^']+)'|([A-Z][a-zA-Z0-9_$.]*)|([a-z][a-zA-Z0-9_]*))\\s*,\\s*\\{/g;\n\n // Track line numbers for source location\n const lines = code.split('\\n');\n const getLineCol = (index: number) => {\n let line = 1;\n let lastNewline = -1;\n for (let i = 0; i < index && i < code.length; i++) {\n if (code[i] === '\\n') {\n line++;\n lastNewline = i;\n }\n }\n return { line, col: index - lastNewline - 1 };\n };\n\n let match;\n while ((match = jsxCallRegex.exec(code)) !== null) {\n const openBraceIndex = match.index + match[0].length - 1;\n const { line, col } = getLineCol(match.index);\n\n // Check if already has our data attribute\n const nextChars = code.slice(openBraceIndex + 1, openBraceIndex + 100);\n if (nextChars.includes(`\"data-${prefix}-file\"`)) {\n continue;\n }\n\n // Inject data attributes as first properties\n const injection = `\"data-${prefix}-file\": \"${id}\", \"data-${prefix}-line\": \"${line}\", \"data-${prefix}-col\": \"${col}\", `;\n\n magicString.appendLeft(openBraceIndex + 1, injection);\n modified = true;\n }\n\n if (!modified) {\n return null;\n }\n\n return {\n code: magicString.toString(),\n map: magicString.generateMap({ hires: true })\n };\n }\n };\n}\n\nexport default vdevJsxSource;\n"],"mappings":";AACA,OAAO,iBAAiB;AAejB,SAAS,cAAc,UAA6B,CAAC,GAAW;AACnE,QAAM,SAAS,QAAQ,UAAU;AACjC,QAAM,UAAU,QAAQ,WAAW,CAAC,cAAc;AAElD,SAAO;AAAA,IACH,MAAM;AAAA;AAAA,IAEN,SAAS;AAAA,IAET,UAAU,MAAc,IAAoC;AAExD,UAAI,QAAQ,KAAK,aAAW,QAAQ,KAAK,EAAE,CAAC,GAAG;AAC3C,eAAO;AAAA,MACX;AAGA,UAAI,CAAC,YAAY,KAAK,EAAE,GAAG;AACvB,eAAO;AAAA,MACX;AAGA,UAAI,CAAC,KAAK,SAAS,MAAM,KAAK,CAAC,KAAK,SAAS,OAAO,KAAK,CAAC,KAAK,SAAS,SAAS,GAAG;AAChF,eAAO;AAAA,MACX;AAMA,YAAM,cAAc,IAAI,YAAY,IAAI;AACxC,UAAI,WAAW;AAIf,YAAM,eAAe;AAGrB,YAAM,QAAQ,KAAK,MAAM,IAAI;AAC7B,YAAM,aAAa,CAAC,UAAkB;AAClC,YAAI,OAAO;AACX,YAAI,cAAc;AAClB,iBAAS,IAAI,GAAG,IAAI,SAAS,IAAI,KAAK,QAAQ,KAAK;AAC/C,cAAI,KAAK,CAAC,MAAM,MAAM;AAClB;AACA,0BAAc;AAAA,UAClB;AAAA,QACJ;AACA,eAAO,EAAE,MAAM,KAAK,QAAQ,cAAc,EAAE;AAAA,MAChD;AAEA,UAAI;AACJ,cAAQ,QAAQ,aAAa,KAAK,IAAI,OAAO,MAAM;AAC/C,cAAM,iBAAiB,MAAM,QAAQ,MAAM,CAAC,EAAE,SAAS;AACvD,cAAM,EAAE,MAAM,IAAI,IAAI,WAAW,MAAM,KAAK;AAG5C,cAAM,YAAY,KAAK,MAAM,iBAAiB,GAAG,iBAAiB,GAAG;AACrE,YAAI,UAAU,SAAS,SAAS,MAAM,QAAQ,GAAG;AAC7C;AAAA,QACJ;AAGA,cAAM,YAAY,SAAS,MAAM,YAAY,EAAE,YAAY,MAAM,YAAY,IAAI,YAAY,MAAM,WAAW,GAAG;AAEjH,oBAAY,WAAW,iBAAiB,GAAG,SAAS;AACpD,mBAAW;AAAA,MACf;AAEA,UAAI,CAAC,UAAU;AACX,eAAO;AAAA,MACX;AAEA,aAAO;AAAA,QACH,MAAM,YAAY,SAAS;AAAA,QAC3B,KAAK,YAAY,YAAY,EAAE,OAAO,KAAK,CAAC;AAAA,MAChD;AAAA,IACJ;AAAA,EACJ;AACJ;AAEA,IAAO,qBAAQ;","names":[]}