nuxtseo-layer-devtools 0.2.5 → 0.2.7
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.
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { onClickOutside } from '@vueuse/core'
|
|
3
3
|
import { computed, ref } from 'vue'
|
|
4
4
|
import { colorMode } from '../composables/rpc'
|
|
5
|
-
import { hasProductionUrl, isProductionMode, previewSource, productionUrl } from '../composables/state'
|
|
5
|
+
import { hasProductionUrl, isConnected, isProductionMode, isStandalone, path, previewSource, productionUrl, standaloneUrl } from '../composables/state'
|
|
6
6
|
|
|
7
7
|
export interface DevtoolsNavItem {
|
|
8
8
|
value: string
|
|
@@ -70,6 +70,21 @@ function selectMode(mode: 'local' | 'production') {
|
|
|
70
70
|
previewSource.value = mode
|
|
71
71
|
modeDropdownOpen.value = false
|
|
72
72
|
}
|
|
73
|
+
|
|
74
|
+
const standaloneHostname = computed(() => {
|
|
75
|
+
try {
|
|
76
|
+
return new URL(standaloneUrl.value).host
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return standaloneUrl.value
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const showStandaloneSetup = computed(() => !isConnected.value && !isStandalone.value)
|
|
84
|
+
|
|
85
|
+
function disconnectStandalone() {
|
|
86
|
+
standaloneUrl.value = ''
|
|
87
|
+
}
|
|
73
88
|
</script>
|
|
74
89
|
|
|
75
90
|
<template>
|
|
@@ -106,7 +121,8 @@ function selectMode(mode: 'local' | 'production') {
|
|
|
106
121
|
>
|
|
107
122
|
v{{ version }}
|
|
108
123
|
</UBadge>
|
|
109
|
-
|
|
124
|
+
<!-- Mode dropdown: embedded with production URL -->
|
|
125
|
+
<div v-if="hasProductionUrl && !isStandalone" ref="modeDropdownRef" class="mode-dropdown-wrapper">
|
|
110
126
|
<button type="button" class="devtools-mode-btn" @click="modeDropdownOpen = !modeDropdownOpen">
|
|
111
127
|
<UIcon :name="isProductionMode ? 'carbon:cloud' : 'carbon:laptop'" class="w-3.5 h-3.5" />
|
|
112
128
|
<span class="hidden sm:inline">{{ isProductionMode ? 'Production' : 'Local' }}</span>
|
|
@@ -135,6 +151,16 @@ function selectMode(mode: 'local' | 'production') {
|
|
|
135
151
|
</div>
|
|
136
152
|
</Transition>
|
|
137
153
|
</div>
|
|
154
|
+
<!-- Standalone mode indicator -->
|
|
155
|
+
<div v-if="isStandalone" class="standalone-indicator">
|
|
156
|
+
<UIcon name="carbon:plug" class="w-3.5 h-3.5 text-[var(--seo-green)]" />
|
|
157
|
+
<span class="text-xs font-mono">{{ standaloneHostname }}</span>
|
|
158
|
+
<UTooltip text="Disconnect">
|
|
159
|
+
<button type="button" class="standalone-disconnect" @click="disconnectStandalone">
|
|
160
|
+
<UIcon name="carbon:close" class="w-3 h-3" />
|
|
161
|
+
</button>
|
|
162
|
+
</UTooltip>
|
|
163
|
+
</div>
|
|
138
164
|
</div>
|
|
139
165
|
</div>
|
|
140
166
|
|
|
@@ -218,11 +244,28 @@ function selectMode(mode: 'local' | 'production') {
|
|
|
218
244
|
</div>
|
|
219
245
|
</header>
|
|
220
246
|
|
|
247
|
+
<!-- Standalone path input -->
|
|
248
|
+
<div v-if="isStandalone" class="standalone-path-bar">
|
|
249
|
+
<div class="standalone-path-inner">
|
|
250
|
+
<UIcon name="carbon:document" class="w-3.5 h-3.5 text-[var(--color-text-muted)]" />
|
|
251
|
+
<span class="text-xs text-[var(--color-text-muted)]">Path:</span>
|
|
252
|
+
<input
|
|
253
|
+
:value="path"
|
|
254
|
+
type="text"
|
|
255
|
+
class="standalone-path-input"
|
|
256
|
+
placeholder="/"
|
|
257
|
+
@change="path = ($event.target as HTMLInputElement).value"
|
|
258
|
+
@keydown.enter="($event.target as HTMLInputElement).blur()"
|
|
259
|
+
>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
221
263
|
<!-- Main Content -->
|
|
222
264
|
<div class="devtools-main">
|
|
223
265
|
<main class="devtools-main-content">
|
|
224
|
-
<
|
|
225
|
-
<
|
|
266
|
+
<DevtoolsStandaloneConnect v-if="showStandaloneSetup" />
|
|
267
|
+
<DevtoolsLoading v-show="!showStandaloneSetup && loading" />
|
|
268
|
+
<div v-show="!showStandaloneSetup && !loading">
|
|
226
269
|
<slot />
|
|
227
270
|
</div>
|
|
228
271
|
</main>
|
|
@@ -285,4 +328,64 @@ function selectMode(mode: 'local' | 'production') {
|
|
|
285
328
|
opacity: 0;
|
|
286
329
|
transform: translateY(-4px);
|
|
287
330
|
}
|
|
331
|
+
|
|
332
|
+
.standalone-indicator {
|
|
333
|
+
display: flex;
|
|
334
|
+
align-items: center;
|
|
335
|
+
gap: 0.375rem;
|
|
336
|
+
padding: 0.25rem 0.5rem;
|
|
337
|
+
border-radius: var(--radius-sm);
|
|
338
|
+
background: oklch(from var(--seo-green) l c h / 0.1);
|
|
339
|
+
border: 1px solid oklch(from var(--seo-green) l c h / 0.2);
|
|
340
|
+
font-size: 0.75rem;
|
|
341
|
+
color: var(--color-text-muted);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.standalone-disconnect {
|
|
345
|
+
display: flex;
|
|
346
|
+
align-items: center;
|
|
347
|
+
justify-content: center;
|
|
348
|
+
width: 1.25rem;
|
|
349
|
+
height: 1.25rem;
|
|
350
|
+
border-radius: var(--radius-sm);
|
|
351
|
+
color: var(--color-text-muted);
|
|
352
|
+
cursor: pointer;
|
|
353
|
+
transition: background 100ms, color 100ms;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.standalone-disconnect:hover {
|
|
357
|
+
background: var(--color-surface-sunken);
|
|
358
|
+
color: var(--color-text);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.standalone-path-bar {
|
|
362
|
+
border-bottom: 1px solid var(--color-border);
|
|
363
|
+
background: var(--color-surface);
|
|
364
|
+
padding: 0.375rem 1rem;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.standalone-path-inner {
|
|
368
|
+
display: flex;
|
|
369
|
+
align-items: center;
|
|
370
|
+
gap: 0.5rem;
|
|
371
|
+
max-width: 80rem;
|
|
372
|
+
margin: 0 auto;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.standalone-path-input {
|
|
376
|
+
flex: 1;
|
|
377
|
+
background: var(--color-surface-sunken);
|
|
378
|
+
border: 1px solid var(--color-border);
|
|
379
|
+
border-radius: var(--radius-sm);
|
|
380
|
+
padding: 0.25rem 0.5rem;
|
|
381
|
+
font-size: 0.75rem;
|
|
382
|
+
font-family: var(--font-mono);
|
|
383
|
+
color: var(--color-text);
|
|
384
|
+
outline: none;
|
|
385
|
+
transition: border-color 150ms;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.standalone-path-input:focus {
|
|
389
|
+
border-color: var(--seo-green);
|
|
390
|
+
}
|
|
288
391
|
</style>
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="flex items-center justify-center py-20">
|
|
2
|
+
<div class="flex flex-col items-center justify-center gap-3 py-20">
|
|
3
3
|
<UIcon
|
|
4
4
|
name="carbon:circle-dash"
|
|
5
5
|
class="animate-spin text-3xl text-[var(--color-text-muted)]"
|
|
6
6
|
/>
|
|
7
|
+
<p class="text-sm text-[var(--color-text-muted)]">
|
|
8
|
+
Connecting to devtools…
|
|
9
|
+
</p>
|
|
7
10
|
</div>
|
|
8
11
|
</template>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import { standaloneUrl } from '../composables/state'
|
|
4
|
+
|
|
5
|
+
const urlInput = ref(standaloneUrl.value || 'http://localhost:3000')
|
|
6
|
+
const error = ref('')
|
|
7
|
+
const connecting = ref(false)
|
|
8
|
+
|
|
9
|
+
async function connect() {
|
|
10
|
+
error.value = ''
|
|
11
|
+
connecting.value = true
|
|
12
|
+
const url = urlInput.value.replace(/\/+$/, '')
|
|
13
|
+
try {
|
|
14
|
+
// Verify the dev server is reachable
|
|
15
|
+
await fetch(url, { mode: 'no-cors', signal: AbortSignal.timeout(5000) })
|
|
16
|
+
standaloneUrl.value = url
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
error.value = `Could not reach ${url}. Is the dev server running?`
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
connecting.value = false
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<div class="standalone-connect">
|
|
29
|
+
<div class="standalone-connect-card">
|
|
30
|
+
<UIcon name="carbon:plug" class="text-4xl text-[var(--seo-green)]" />
|
|
31
|
+
<h2 class="text-lg font-semibold">
|
|
32
|
+
Connect to Dev Server
|
|
33
|
+
</h2>
|
|
34
|
+
<p class="text-sm text-[var(--color-text-muted)] text-center max-w-sm leading-relaxed">
|
|
35
|
+
Running in standalone mode. Enter the URL of your Nuxt dev server to start inspecting.
|
|
36
|
+
</p>
|
|
37
|
+
<form class="standalone-connect-form" @submit.prevent="connect">
|
|
38
|
+
<UInput
|
|
39
|
+
v-model="urlInput"
|
|
40
|
+
placeholder="http://localhost:3000"
|
|
41
|
+
size="lg"
|
|
42
|
+
icon="carbon:link"
|
|
43
|
+
class="flex-1"
|
|
44
|
+
:disabled="connecting"
|
|
45
|
+
/>
|
|
46
|
+
<UButton
|
|
47
|
+
type="submit"
|
|
48
|
+
icon="carbon:connect"
|
|
49
|
+
size="lg"
|
|
50
|
+
:loading="connecting"
|
|
51
|
+
>
|
|
52
|
+
Connect
|
|
53
|
+
</UButton>
|
|
54
|
+
</form>
|
|
55
|
+
<p v-if="error" class="text-xs text-[var(--color-warning)] text-center max-w-sm">
|
|
56
|
+
{{ error }}
|
|
57
|
+
</p>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</template>
|
|
61
|
+
|
|
62
|
+
<style scoped>
|
|
63
|
+
.standalone-connect {
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
justify-content: center;
|
|
67
|
+
min-height: 60vh;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.standalone-connect-card {
|
|
71
|
+
display: flex;
|
|
72
|
+
flex-direction: column;
|
|
73
|
+
align-items: center;
|
|
74
|
+
gap: 1rem;
|
|
75
|
+
padding: 2.5rem;
|
|
76
|
+
border-radius: var(--radius-lg);
|
|
77
|
+
border: 1px solid var(--color-border);
|
|
78
|
+
background: var(--color-surface-elevated);
|
|
79
|
+
max-width: 28rem;
|
|
80
|
+
width: 100%;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.standalone-connect-form {
|
|
84
|
+
display: flex;
|
|
85
|
+
gap: 0.5rem;
|
|
86
|
+
width: 100%;
|
|
87
|
+
}
|
|
88
|
+
</style>
|
package/composables/rpc.ts
CHANGED
|
@@ -2,7 +2,9 @@ import type { NuxtDevtoolsClient } from '@nuxt/devtools-kit/types'
|
|
|
2
2
|
import type { $Fetch } from 'nitropack/types'
|
|
3
3
|
import type { Ref } from 'vue'
|
|
4
4
|
import { onDevtoolsClientConnected } from '@nuxt/devtools-kit/iframe-client'
|
|
5
|
-
import {
|
|
5
|
+
import { ofetch } from 'ofetch'
|
|
6
|
+
import { ref, watch, watchEffect } from 'vue'
|
|
7
|
+
import { isConnected, refreshSources, standaloneUrl } from './state'
|
|
6
8
|
|
|
7
9
|
export const appFetch: Ref<$Fetch | undefined> = ref()
|
|
8
10
|
export const devtools: Ref<NuxtDevtoolsClient | undefined> = ref()
|
|
@@ -16,25 +18,45 @@ export interface DevtoolsConnectionOptions {
|
|
|
16
18
|
/**
|
|
17
19
|
* Initialize the base devtools connection.
|
|
18
20
|
* Call this in your module's devtools client setup.
|
|
21
|
+
*
|
|
22
|
+
* Supports two modes:
|
|
23
|
+
* - **Embedded**: running inside Nuxt DevTools iframe (automatic)
|
|
24
|
+
* - **Standalone**: running directly in a browser tab with a manual dev server URL
|
|
19
25
|
*/
|
|
20
26
|
export function useDevtoolsConnection(options: DevtoolsConnectionOptions = {}): void {
|
|
21
|
-
|
|
22
|
-
// @ts-expect-error untyped
|
|
23
|
-
appFetch.value = client.host.app.$fetch
|
|
24
|
-
watchEffect(() => {
|
|
25
|
-
colorMode.value = client.host.app.colorMode.value
|
|
26
|
-
})
|
|
27
|
-
devtools.value = client.devtools
|
|
28
|
-
options.onConnected?.(client)
|
|
27
|
+
const inIframe = window.parent !== window
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
// Embedded mode: connect via devtools-kit iframe client
|
|
30
|
+
if (inIframe) {
|
|
31
|
+
onDevtoolsClientConnected(async (client) => {
|
|
32
|
+
isConnected.value = true
|
|
33
|
+
// @ts-expect-error untyped
|
|
34
|
+
appFetch.value = client.host.app.$fetch
|
|
35
|
+
watchEffect(() => {
|
|
36
|
+
colorMode.value = client.host.app.colorMode.value
|
|
35
37
|
})
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
devtools.value = client.devtools
|
|
39
|
+
options.onConnected?.(client)
|
|
40
|
+
|
|
41
|
+
if (options.onRouteChange) {
|
|
42
|
+
const $route = client.host.nuxt.vueApp.config.globalProperties?.$route
|
|
43
|
+
options.onRouteChange($route)
|
|
44
|
+
const removeAfterEach = client.host.nuxt.$router.afterEach((route: any) => {
|
|
45
|
+
options.onRouteChange!(route)
|
|
46
|
+
})
|
|
47
|
+
// Clean up when devtools client disconnects
|
|
48
|
+
client.host.nuxt.hook('app:unmount', removeAfterEach)
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Standalone mode: create appFetch from manually entered URL
|
|
54
|
+
watch(() => standaloneUrl.value, (url) => {
|
|
55
|
+
if (url && !isConnected.value) {
|
|
56
|
+
appFetch.value = ofetch.create({ baseURL: url }) as unknown as $Fetch
|
|
57
|
+
// Use system color scheme preference
|
|
58
|
+
colorMode.value = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
|
59
|
+
refreshSources()
|
|
38
60
|
}
|
|
39
|
-
})
|
|
61
|
+
}, { immediate: true })
|
|
40
62
|
}
|
package/composables/state.ts
CHANGED
|
@@ -9,7 +9,11 @@ export const path = ref('/')
|
|
|
9
9
|
export const query = ref()
|
|
10
10
|
export const base = ref('/')
|
|
11
11
|
|
|
12
|
-
export const host = computed(() =>
|
|
12
|
+
export const host = computed(() => {
|
|
13
|
+
if (isStandalone.value)
|
|
14
|
+
return standaloneUrl.value
|
|
15
|
+
return withBase(base.value, `${window.location.protocol}//${hostname}`)
|
|
16
|
+
})
|
|
13
17
|
|
|
14
18
|
export const refreshSources = useDebounceFn(() => {
|
|
15
19
|
refreshTime.value = Date.now()
|
|
@@ -31,3 +35,8 @@ export const hasProductionUrl = computed(() => {
|
|
|
31
35
|
})
|
|
32
36
|
|
|
33
37
|
export const isProductionMode = computed(() => previewSource.value === 'production' && hasProductionUrl.value)
|
|
38
|
+
|
|
39
|
+
// Standalone mode state
|
|
40
|
+
export const standaloneUrl = useLocalStorage<string>('nuxt-seo:standalone-url', '')
|
|
41
|
+
export const isConnected = ref(false)
|
|
42
|
+
export const isStandalone = computed(() => !isConnected.value && !!standaloneUrl.value)
|
package/nuxt.config.ts
CHANGED
|
@@ -8,10 +8,15 @@ export default defineNuxtConfig({
|
|
|
8
8
|
modules: [
|
|
9
9
|
'@nuxt/fonts',
|
|
10
10
|
'@nuxt/ui',
|
|
11
|
+
'@vueuse/nuxt',
|
|
11
12
|
],
|
|
12
13
|
|
|
13
14
|
css: [resolve('./assets/css/global.css')],
|
|
14
15
|
|
|
16
|
+
robots: false,
|
|
17
|
+
content: false,
|
|
18
|
+
sitemap: false,
|
|
19
|
+
|
|
15
20
|
fonts: {
|
|
16
21
|
families: [
|
|
17
22
|
{ name: 'Hubot Sans' },
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxtseo-layer-devtools",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.7",
|
|
5
5
|
"description": "Shared Nuxt layer for Nuxt SEO devtools clients.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"@nuxt/ui": "^4.6.0",
|
|
32
32
|
"@shikijs/langs": "^4.0.2",
|
|
33
33
|
"@shikijs/themes": "^4.0.2",
|
|
34
|
-
"@vueuse/
|
|
34
|
+
"@vueuse/nuxt": "^14.2.1",
|
|
35
|
+
"ofetch": "^1.5.1",
|
|
35
36
|
"shiki": "^4.0.2",
|
|
36
37
|
"ufo": "^1.6.3"
|
|
37
38
|
},
|