@@ -14,6 +14,10 @@ const PANEL_WIDTH = 400
1414const PANEL_HEIGHT = 300
1515const PANEL_MARGIN = 30
1616
17+ const showToast = ref (false )
18+ const toastContent = ref (' ' )
19+ let toastTimer: ReturnType <typeof setTimeout > | undefined
20+
1721const initX = ref (0 )
1822const initY = ref (0 )
1923
@@ -73,10 +77,149 @@ async function openInEditor() {
7377 emit (' openInEditor' , file )
7478 // close()
7579}
80+
81+ function generateUniqueSelector(element : Element | undefined ): string {
82+ if (! element )
83+ return ' '
84+
85+ const path: string [] = []
86+ let current: Element | null = element
87+
88+ while (current && current !== document .body ) {
89+ let selector = current .tagName .toLowerCase ()
90+
91+ // Add ID if available
92+ if (current .id ) {
93+ selector += ` #${CSS .escape (current .id )} `
94+ path .unshift (selector )
95+ break // ID is unique, no need to go further
96+ }
97+
98+ // Add classes
99+ if (current .className && typeof current .className === ' string' ) {
100+ const classes = current .className .trim ().split (/ \s + / ).filter (Boolean )
101+ if (classes .length > 0 )
102+ selector += ` .${classes .map (c => CSS .escape (c )).join (' .' )} `
103+ }
104+
105+ // Add nth-child if there are siblings of the same type
106+ const parent = current .parentElement
107+ if (parent ) {
108+ const siblings = Array .from (parent .children ).filter (
109+ child => child .tagName === current ! .tagName ,
110+ )
111+ if (siblings .length > 1 ) {
112+ const index = siblings .indexOf (current ) + 1
113+ selector += ` :nth-of-type(${index }) `
114+ }
115+ }
116+
117+ path .unshift (selector )
118+ current = current .parentElement
119+ }
120+
121+ if (path .length > 6 )
122+ return ' '
123+
124+ return path .join (' > ' )
125+ }
126+
127+ function buildComponentTree(): string {
128+ if (! props .matched )
129+ return ' '
130+
131+ const components: string [] = []
132+ let current: typeof props .matched | undefined = props .matched
133+
134+ // Build the tree by walking up the parent chain
135+ while (current ) {
136+ // Try to get component name from vnode
137+ let componentName = ' Unknown'
138+ if (current .vnode ) {
139+ const vnode = current .vnode
140+ if (vnode .type ) {
141+ if (typeof vnode .type === ' string' ) {
142+ componentName = vnode .type
143+ }
144+ else if (typeof vnode .type === ' object' ) {
145+ const typeObj = vnode .type as any
146+ componentName = typeObj .name || typeObj .__name || typeObj .__file ?.split (' /' ).pop ()?.replace (/ \. \w + $ / , ' ' ) || ' AnonymousComponent'
147+ }
148+ else if (typeof vnode .type === ' function' ) {
149+ componentName = (vnode .type as any ).name || ' FunctionalComponent'
150+ }
151+ }
152+ }
153+
154+ components .unshift (componentName )
155+
156+ // Move to parent
157+ try {
158+ current = current .getParent ()
159+ }
160+ catch {
161+ break
162+ }
163+ }
164+
165+ return components .join (' > ' )
166+ }
167+
168+ async function copyAgentInfo() {
169+ if (! props .matched )
170+ return
171+
172+ try {
173+ // Gather browser context
174+ const pageUrl = window .location .href
175+ const viewport = ` ${window .innerWidth }x${window .innerHeight } `
176+ const selectedText = window .getSelection ()?.toString ()?.trim ()
177+
178+ // Extract element information
179+ const filepath = props .matched .pos [0 ]
180+ const line = props .matched .pos [1 ]
181+ const column = props .matched .pos [2 ]
182+ const fileLocation = ` ${filepath }:${line }:${column } `
183+
184+ // Generate CSS selector
185+ const selector = generateUniqueSelector (props .matched .el )
186+
187+ // Build component tree
188+ const componentTree = buildComponentTree ()
189+
190+ // Format the output
191+ const info = [
192+ ` Page URL: ${pageUrl } ` ,
193+ ` Viewport: ${viewport } ` ,
194+ selectedText ? ` Selected Text: ${selectedText } ` : null ,
195+ selector ? ` Selector: ${selector } ` : null ,
196+ componentTree ? ` Component Tree: ${componentTree } ` : null ,
197+ ` File: ${fileLocation } ` ,
198+ ].filter (Boolean ).join (' \n ' )
199+
200+ // Copy to clipboard
201+ await navigator .clipboard .writeText (info )
202+
203+ // Show toast with preview
204+ toastContent .value = info
205+ showToast .value = true
206+
207+ if (toastTimer )
208+ clearTimeout (toastTimer )
209+ // Hide toast after 6 seconds
210+ toastTimer = setTimeout (() => {
211+ showToast .value = false
212+ }, 6_000 )
213+ }
214+ catch (error ) {
215+ console .error (' Failed to copy agent info:' , error )
216+ }
217+ }
76218 </script >
77219
78220<template >
79221 <div
222+ v-if =" props.matched"
80223 ref =" el"
81224 class =" fixed relative z-9999999 w-400px flex flex-col of-hidden rounded-lg bg-glass text-sm color-base shadow-lg ring-1 ring-base backdrop-blur duration-200"
82225 :style =" style"
@@ -90,31 +233,64 @@ async function openInEditor() {
90233 >
91234 <div class="nuxt-devtools-inspect-running-border pointer-events-none absolute inset-0 z-10 border-1.5 border-transparent rounded-lg" />
92235 </div> -->
93- <div ref =" draggingEl" class =" flex items-center gap-2 p2" >
94- <button
95- title =" Go to parent"
96- class =" flex items-center text-sm font-mono op50 disabled:pointer-events-none hover:text-green6 hover:op100 disabled:op10!"
97- :disabled =" !props.hasParent"
98- @click =" selectParent"
99- >
100- <div class =" i-ph-arrow-bend-left-up-duotone text-lg" />
101- </button >
102- <button
103- title =" Open in editor"
104- class =" flex items-center text-sm font-mono op50 hover:text-green6 hover:op100"
105- @click =" openInEditor"
106- >
107- <span v-if =" props.matched" >{{ props.matched.pos[0] }}:{{ props.matched.pos[1] }}:{{ props.matched.pos[2] }}</span >
108- <div class =" i-ph-arrow-up-right-light mt--2 text-lg" />
109- </button >
110- <div class =" flex-auto" />
111- <button
112- title =" Close"
113- class =" flex-none op50 hover:op100"
114- @click =" close"
115- >
116- <div class =" i-ph-x text-lg" />
117- </button >
236+ <div ref =" draggingEl" class =" flex flex-col gap-2 of-hidden p2" >
237+ <div class =" flex items-center gap-2" >
238+ <button
239+ v-if =" props.hasParent"
240+ title =" Go to parent"
241+ class =" flex items-center border-1 border-base rounded px1 py0.5 text-sm font-mono op50 disabled:pointer-events-none hover:text-green6 hover:op100 disabled:op10!"
242+ @click =" selectParent"
243+ >
244+ <div class =" i-ph-arrow-bend-left-up-duotone text-lg" />
245+ Parent
246+ </button >
247+ <button
248+ title =" Open in editor"
249+ class =" flex items-center border-1 border-base rounded px1 py0.5 text-sm font-mono op50 hover:text-green6 hover:op100"
250+ @click =" openInEditor"
251+ >
252+ <div class =" i-ph-arrow-up-right-light text-lg" />
253+ Open
254+ </button >
255+ <button
256+ title =" Copy infos for agents"
257+ class =" flex items-center border-1 border-base rounded px1 py0.5 text-sm font-mono op50 hover:text-green6 hover:op100"
258+ @click =" copyAgentInfo"
259+ >
260+ <div class =" i-ph-copy-duotone text-lg" />
261+ Info
262+ </button >
263+ <div class =" flex-auto" />
264+ <button
265+ title =" Close"
266+ class =" flex-none op50 hover:op100"
267+ @click =" close"
268+ >
269+ <div class =" i-ph-x text-lg" />
270+ </button >
271+ </div >
272+ <div class =" grid grid-cols-[max-content_1fr] items-center gap-2" >
273+ <!-- File -->
274+ <div class =" i-ph-file-duotone flex-none text-lg op50" title =" File" />
275+ <span class =" break-all text-xs font-mono" title =" File location" >{{ props.matched.pos[0] }}:{{ props.matched.pos[1] }}:{{ props.matched.pos[2] }}</span >
276+
277+ <!-- Component Tree -->
278+ <div class =" i-ph-tree-view-duotone flex-none text-lg op50" title =" Component Tree" />
279+ <span class =" break-all text-xs font-mono" title =" Component Tree" >{{ buildComponentTree() }}</span >
280+ </div >
281+ </div >
282+ </div >
283+
284+ <!-- Toast notification -->
285+ <div
286+ v-show =" showToast"
287+ class =" fixed bottom-20px right-20px z-99999999 max-w-500px flex flex-col gap-2 rounded-lg bg-glass p4 text-sm color-base shadow-xl ring-1 ring-base backdrop-blur transition-all duration-300"
288+ :class =" showToast ? 'translate-y-0 op100' : 'translate-y-10 op0'"
289+ >
290+ <div class =" flex items-center gap-2" >
291+ <div class =" i-ph-check-circle-duotone text-xl text-green6" />
292+ <span class =" font-semibold" >Infos for agents copied to clipboard</span >
118293 </div >
294+ <pre class =" mt-2 max-h-200px of-auto whitespace-pre-wrap rounded bg-black bg-op-20 p2 text-xs font-mono" >{{ toastContent }}</pre >
119295 </div >
120296</template >
0 commit comments