Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/devtools-ui-kit/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default defineNuxtModule<ModuleOptions>({

nuxt.options.css.unshift(rPath('assets/styles.css'))

if (!options.dev)
if (!options.dev && nuxt.options.unocss)
nuxt.options.unocss = extendUnocssOptions(nuxt.options.unocss)

// eslint-disable-next-line ts/ban-ts-comment
Expand Down
3 changes: 1 addition & 2 deletions packages/devtools/src/module-main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type {} from '@vitejs/devtools-kit'
import type { ServerResponse } from 'node:http'
import type { Nuxt } from 'nuxt/schema'
import type { ViteDevServer } from 'vite'
import type { ModuleOptions, NuxtDevToolsOptions } from './types'
import { existsSync } from 'node:fs'
import fs from 'node:fs/promises'
Expand Down Expand Up @@ -168,7 +167,7 @@ window.__NUXT_DEVTOOLS_TIME_METRIC__.appInit = Date.now()
const ROUTE_ANALYZE = `${ROUTE_PATH}/analyze`

// TODO: Use WS from nitro server when possible
nuxt.hook('vite:serverCreated', (server: ViteDevServer) => {
nuxt.hook('vite:serverCreated', (server) => {
const devtoolsAnalyzeDir = join(nuxt.options.rootDir, 'node_modules/.cache/nuxt-devtools/analyze')

server.middlewares.use(ROUTE_ANALYZE, sirv(devtoolsAnalyzeDir, { single: false, dev: true, dotfiles: true, ignores: false }))
Expand Down
2 changes: 1 addition & 1 deletion packages/devtools/src/webcomponents/.generated/css.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ const PANEL_WIDTH = 400
const PANEL_HEIGHT = 300
const PANEL_MARGIN = 30

const showToast = ref(false)
const toastContent = ref('')
let toastTimer: ReturnType<typeof setTimeout> | undefined

const initX = ref(0)
const initY = ref(0)

Expand Down Expand Up @@ -73,10 +77,149 @@ async function openInEditor() {
emit('openInEditor', file)
// close()
}

function generateUniqueSelector(element: Element | undefined): string {
if (!element)
return ''

const path: string[] = []
let current: Element | null = element

while (current && current !== document.body) {
let selector = current.tagName.toLowerCase()

// Add ID if available
if (current.id) {
selector += `#${CSS.escape(current.id)}`
path.unshift(selector)
break // ID is unique, no need to go further
}

// Add classes
if (current.className && typeof current.className === 'string') {
const classes = current.className.trim().split(/\s+/).filter(Boolean)
if (classes.length > 0)
selector += `.${classes.map(c => CSS.escape(c)).join('.')}`
}

// Add nth-child if there are siblings of the same type
const parent = current.parentElement
if (parent) {
const siblings = Array.from(parent.children).filter(
child => child.tagName === current!.tagName,
)
if (siblings.length > 1) {
const index = siblings.indexOf(current) + 1
selector += `:nth-of-type(${index})`
}
}

path.unshift(selector)
current = current.parentElement
}

if (path.length > 6)
return ''

return path.join(' > ')
}

function buildComponentTree(): string {
if (!props.matched)
return ''

const components: string[] = []
let current: typeof props.matched | undefined = props.matched

// Build the tree by walking up the parent chain
while (current) {
// Try to get component name from vnode
let componentName = 'Unknown'
if (current.vnode) {
const vnode = current.vnode
if (vnode.type) {
if (typeof vnode.type === 'string') {
componentName = vnode.type
}
else if (typeof vnode.type === 'object') {
const typeObj = vnode.type as any
componentName = typeObj.name || typeObj.__name || typeObj.__file?.split('/').pop()?.replace(/\.\w+$/, '') || 'AnonymousComponent'
}
else if (typeof vnode.type === 'function') {
componentName = (vnode.type as any).name || 'FunctionalComponent'
}
}
}

components.unshift(componentName)

// Move to parent
try {
current = current.getParent()
}
catch {
break
}
}

return components.join(' > ')
}

async function copyAgentInfo() {
if (!props.matched)
return

try {
// Gather browser context
const pageUrl = window.location.href
const viewport = `${window.innerWidth}x${window.innerHeight}`
const selectedText = window.getSelection()?.toString()?.trim()

// Extract element information
const filepath = props.matched.pos[0]
const line = props.matched.pos[1]
const column = props.matched.pos[2]
const fileLocation = `${filepath}:${line}:${column}`

// Generate CSS selector
const selector = generateUniqueSelector(props.matched.el)

// Build component tree
const componentTree = buildComponentTree()

// Format the output
const info = [
`Page URL: ${pageUrl}`,
`Viewport: ${viewport}`,
selectedText ? `Selected Text: ${selectedText}` : null,
selector ? `Selector: ${selector}` : null,
componentTree ? `Component Tree: ${componentTree}` : null,
`File: ${fileLocation}`,
].filter(Boolean).join('\n')

// Copy to clipboard
await navigator.clipboard.writeText(info)

// Show toast with preview
toastContent.value = info
showToast.value = true

if (toastTimer)
clearTimeout(toastTimer)
// Hide toast after 6 seconds
toastTimer = setTimeout(() => {
showToast.value = false
}, 6_000)
}
catch (error) {
console.error('Failed to copy agent info:', error)
}
}
</script>

<template>
<div
v-if="props.matched"
ref="el"
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"
:style="style"
Expand All @@ -90,31 +233,64 @@ async function openInEditor() {
>
<div class="nuxt-devtools-inspect-running-border pointer-events-none absolute inset-0 z-10 border-1.5 border-transparent rounded-lg" />
</div> -->
<div ref="draggingEl" class="flex items-center gap-2 p2">
<button
title="Go to parent"
class="flex items-center text-sm font-mono op50 disabled:pointer-events-none hover:text-green6 hover:op100 disabled:op10!"
:disabled="!props.hasParent"
@click="selectParent"
>
<div class="i-ph-arrow-bend-left-up-duotone text-lg" />
</button>
<button
title="Open in editor"
class="flex items-center text-sm font-mono op50 hover:text-green6 hover:op100"
@click="openInEditor"
>
<span v-if="props.matched">{{ props.matched.pos[0] }}:{{ props.matched.pos[1] }}:{{ props.matched.pos[2] }}</span>
<div class="i-ph-arrow-up-right-light mt--2 text-lg" />
</button>
<div class="flex-auto" />
<button
title="Close"
class="flex-none op50 hover:op100"
@click="close"
>
<div class="i-ph-x text-lg" />
</button>
<div ref="draggingEl" class="flex flex-col gap-2 of-hidden p2">
<div class="flex items-center gap-2">
<button
v-if="props.hasParent"
title="Go to parent"
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!"
@click="selectParent"
>
<div class="i-ph-arrow-bend-left-up-duotone text-lg" />
Parent
</button>
<button
title="Open in editor"
class="flex items-center border-1 border-base rounded px1 py0.5 text-sm font-mono op50 hover:text-green6 hover:op100"
@click="openInEditor"
>
<div class="i-ph-arrow-up-right-light text-lg" />
Open
</button>
<button
title="Copy infos for agents"
class="flex items-center border-1 border-base rounded px1 py0.5 text-sm font-mono op50 hover:text-green6 hover:op100"
@click="copyAgentInfo"
>
<div class="i-ph-copy-duotone text-lg" />
Info
</button>
<div class="flex-auto" />
<button
title="Close"
class="flex-none op50 hover:op100"
@click="close"
>
<div class="i-ph-x text-lg" />
</button>
</div>
<div class="grid grid-cols-[max-content_1fr] items-center gap-2">
<!-- File -->
<div class="i-ph-file-duotone flex-none text-lg op50" title="File" />
<span class="break-all text-xs font-mono" title="File location">{{ props.matched.pos[0] }}:{{ props.matched.pos[1] }}:{{ props.matched.pos[2] }}</span>

<!-- Component Tree -->
<div class="i-ph-tree-view-duotone flex-none text-lg op50" title="Component Tree" />
<span class="break-all text-xs font-mono" title="Component Tree">{{ buildComponentTree() }}</span>
</div>
</div>
</div>

<!-- Toast notification -->
<div
v-show="showToast"
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"
:class="showToast ? 'translate-y-0 op100' : 'translate-y-10 op0'"
>
<div class="flex items-center gap-2">
<div class="i-ph-check-circle-duotone text-xl text-green6" />
<span class="font-semibold">Infos for agents copied to clipboard</span>
</div>
<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>
</div>
</template>
Loading