nuxt-og-image 6.5.2 → 6.6.0
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/chunks/tw4.cjs +1 -1
- package/dist/chunks/tw4.mjs +1 -1
- package/dist/chunks/uno.cjs +1 -1
- package/dist/chunks/uno.mjs +1 -1
- package/dist/devtools/components/og-image/AddComponentDialog.vue +85 -0
- package/dist/devtools/components/og-image/BlueskyCardRenderer.vue +134 -0
- package/dist/devtools/components/og-image/CreateOgImageDialog.vue +56 -0
- package/dist/devtools/components/og-image/DiscordCardRenderer.vue +125 -0
- package/dist/devtools/components/og-image/FacebookCardRenderer.vue +128 -0
- package/dist/devtools/components/og-image/IFrameLoader.vue +93 -0
- package/dist/devtools/components/og-image/ImageLoader.vue +197 -0
- package/dist/devtools/components/og-image/LinkedInCardRenderer.vue +100 -0
- package/dist/devtools/components/og-image/RendererSelectModal.vue +356 -0
- package/dist/devtools/components/og-image/SlackCardRenderer.vue +140 -0
- package/dist/devtools/components/og-image/TemplateComponentPreview.vue +186 -0
- package/dist/devtools/components/og-image/TwitterCardRenderer.vue +170 -0
- package/dist/devtools/components/og-image/WhatsAppRenderer.vue +294 -0
- package/dist/devtools/lib/og-image/keys.ts +8 -0
- package/dist/devtools/lib/og-image/og-image.ts +536 -0
- package/dist/devtools/lib/og-image/renderer-select.ts +3 -0
- package/dist/devtools/lib/og-image/rpc-types.ts +2 -0
- package/dist/devtools/lib/og-image/rpc.ts +73 -0
- package/dist/devtools/lib/og-image/runtime-types.ts +10 -0
- package/dist/devtools/lib/og-image/shared/urlEncoding.ts +502 -0
- package/dist/devtools/lib/og-image/shared.ts +30 -0
- package/dist/devtools/lib/og-image/templates.ts +9 -0
- package/dist/devtools/lib/og-image/types.ts +38 -0
- package/dist/devtools/lib/og-image/util/logic.ts +21 -0
- package/dist/devtools/nuxt.config.ts +6 -0
- package/dist/devtools/pages/og-image/debug.vue +32 -0
- package/dist/devtools/pages/og-image/docs.vue +3 -0
- package/dist/devtools/pages/og-image/index.vue +1682 -0
- package/dist/devtools/pages/og-image/templates.vue +150 -0
- package/dist/devtools/pages/og-image.vue +184 -0
- package/dist/module.cjs +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +1 -1
- package/dist/runtime/server/og-image/core/plugins/imageSrc.js +2 -2
- package/dist/runtime/server/og-image/core/style-attr.d.ts +8 -0
- package/dist/runtime/server/og-image/core/style-attr.js +34 -0
- package/dist/runtime/server/og-image/core/vnodes.d.ts +2 -1
- package/dist/runtime/server/og-image/core/vnodes.js +3 -27
- package/dist/shared/{nuxt-og-image.CgPzmzQY.cjs → nuxt-og-image.CfTPCtaS.cjs} +38 -24
- package/dist/shared/{nuxt-og-image.DGAMxBol.mjs → nuxt-og-image.DdbTs-xp.mjs} +37 -23
- package/package.json +17 -18
- package/dist/devtools/200.html +0 -1
- package/dist/devtools/404.html +0 -1
- package/dist/devtools/_fonts/4ppnHhMi-pBsWSPo7mY0avYxlDoAg1N3PTzCwXLZ5rA-d9oibkGnTd1JL3tc_xnaVgBLYmOB8kjrK2cvZaqwj9s.woff2 +0 -0
- package/dist/devtools/_fonts/PV2hrQG6wq5BlIPDjdL1IcOflycaghyt5MHzlBqZtlo-lb_WexLz3VZqfTN0oi554iBH5tT2j2UFEV-XErCAS3E.woff2 +0 -0
- package/dist/devtools/_fonts/VE4cDVCv5MxbFM7ZLoLCGbIpNd71zhp7MDI9lmN5Y7I-xZyDYCUVrd6LV8eVGF3Um3UZjBFuUtDGtvdyTBBRYBo.woff2 +0 -0
- package/dist/devtools/_fonts/fVoGbnMbBFd5L9BBp9fUPavUSkZ_EmsQNSyadkT-108-U4T0khaeLQSIhtt9eVvaCEKJjtWJ4ioRJOf8hvqkWY0.woff2 +0 -0
- package/dist/devtools/_fonts/lQAxeCEs1R0Lw-H9XRU1RlOARQN8J6npRsPjyEDMe5s-_DUSLEkO3tKTuun_gSnDLoQPVEnpOnyqZMOw0ByZ6PA.woff2 +0 -0
- package/dist/devtools/_fonts/lntlqNHKLV2n82yTwMde70QqOjcfLE2XJ5oKZ3vRPWc-z6TxpIZQdWXztWLr9_OFWqt_WJJoeGtuK_-XQMZGQwE.woff2 +0 -0
- package/dist/devtools/_nuxt/B9jrmesR.js +0 -1
- package/dist/devtools/_nuxt/BA-4cUNc.js +0 -1
- package/dist/devtools/_nuxt/BOEXnX7x.js +0 -3
- package/dist/devtools/_nuxt/BWKJ0Uxb.js +0 -30
- package/dist/devtools/_nuxt/Bdtz4ZK9.js +0 -4
- package/dist/devtools/_nuxt/C5797Ieg.js +0 -1
- package/dist/devtools/_nuxt/C5jzcy9i.js +0 -1
- package/dist/devtools/_nuxt/CCTv7mmB.js +0 -1
- package/dist/devtools/_nuxt/CP0tQR2M.js +0 -1
- package/dist/devtools/_nuxt/C_JnDlx-.js +0 -1
- package/dist/devtools/_nuxt/CdmciVPJ.js +0 -1
- package/dist/devtools/_nuxt/CqjbkMN9.js +0 -3
- package/dist/devtools/_nuxt/CuJezOxb.js +0 -1
- package/dist/devtools/_nuxt/D4EkL0XJ.js +0 -6
- package/dist/devtools/_nuxt/DKaW7clv.js +0 -1
- package/dist/devtools/_nuxt/DSl3mlLY.js +0 -2
- package/dist/devtools/_nuxt/DYta7Mdi.js +0 -3
- package/dist/devtools/_nuxt/DevtoolsSection.CcOMr_vO.css +0 -1
- package/dist/devtools/_nuxt/DevtoolsSnippet.BhrTdbXn.css +0 -1
- package/dist/devtools/_nuxt/E8AZ6HoH.js +0 -1
- package/dist/devtools/_nuxt/IFrameLoader.k_861Nnq.css +0 -1
- package/dist/devtools/_nuxt/O5eLyffU.js +0 -1
- package/dist/devtools/_nuxt/builds/latest.json +0 -1
- package/dist/devtools/_nuxt/builds/meta/a36bc212-7179-4aa2-a534-6366229bd7b1.json +0 -1
- package/dist/devtools/_nuxt/entry.CcrXpo2y.css +0 -2
- package/dist/devtools/_nuxt/fira-code.Bc8wnsZt.woff2 +0 -0
- package/dist/devtools/_nuxt/hubot-sans.DLGyhQVu.woff2 +0 -0
- package/dist/devtools/_nuxt/pages.Ci_xvfJ8.css +0 -1
- package/dist/devtools/_nuxt/renderer-select.COGJ4ZQe.css +0 -1
- package/dist/devtools/_nuxt/templates.BByln3BG.css +0 -1
- package/dist/devtools/_nuxt/wMBdlVF-.js +0 -152
- package/dist/devtools/debug/index.html +0 -1
- package/dist/devtools/docs/index.html +0 -1
- package/dist/devtools/index.html +0 -1
- package/dist/devtools/templates/index.html +0 -1
|
@@ -0,0 +1,1682 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { useHead } from 'nuxt/app'
|
|
3
|
+
import { hasProductionUrl, previewSource, productionUrl } from 'nuxtseo-layer-devtools/composables/state'
|
|
4
|
+
import { withHttps } from 'ufo'
|
|
5
|
+
import { computed, reactive, ref, watch } from 'vue'
|
|
6
|
+
import { useOgImage } from '../../lib/og-image/og-image'
|
|
7
|
+
import { RendererSelectDialogPromise } from '../../lib/og-image/renderer-select'
|
|
8
|
+
import { isConnectionFailed, isFallbackMode } from '../../lib/og-image/rpc'
|
|
9
|
+
|
|
10
|
+
const RE_SINGLE_QUOTE = /'/g
|
|
11
|
+
const RE_VUE_FILENAME = /[^/]+\.vue$/
|
|
12
|
+
const RE_HEX_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i
|
|
13
|
+
|
|
14
|
+
const {
|
|
15
|
+
globalDebug,
|
|
16
|
+
isDebugLoading,
|
|
17
|
+
isCustomOgImage,
|
|
18
|
+
isValidDebugError,
|
|
19
|
+
hasDefinedOgImage,
|
|
20
|
+
fetchError,
|
|
21
|
+
aspectRatio,
|
|
22
|
+
imageFormat,
|
|
23
|
+
socialPreview,
|
|
24
|
+
imageColorMode,
|
|
25
|
+
src,
|
|
26
|
+
whatsappInlineSrc,
|
|
27
|
+
socialPreviewTitle,
|
|
28
|
+
socialPreviewDescription,
|
|
29
|
+
socialSiteUrl,
|
|
30
|
+
slackSocialPreviewSiteName,
|
|
31
|
+
activeComponentName,
|
|
32
|
+
activeComponent,
|
|
33
|
+
activeComponentRelativePath,
|
|
34
|
+
isOgImageTemplate,
|
|
35
|
+
renderer,
|
|
36
|
+
isComponentCompatibleWithRenderer,
|
|
37
|
+
sidePanelOpen,
|
|
38
|
+
isPageScreenshot,
|
|
39
|
+
currentPageFile,
|
|
40
|
+
allImageKeys,
|
|
41
|
+
description,
|
|
42
|
+
hasMadeChanges,
|
|
43
|
+
options,
|
|
44
|
+
propEditor,
|
|
45
|
+
ogImageKey,
|
|
46
|
+
toggleSocialPreview: _toggleSocialPreview,
|
|
47
|
+
generateLoadTime,
|
|
48
|
+
openImage,
|
|
49
|
+
openCurrentPageFile,
|
|
50
|
+
openCurrentComponent,
|
|
51
|
+
componentNames,
|
|
52
|
+
ejectComponent,
|
|
53
|
+
createComponent,
|
|
54
|
+
addExistingComponent,
|
|
55
|
+
resetProps,
|
|
56
|
+
updateProps,
|
|
57
|
+
refreshSources,
|
|
58
|
+
} = useOgImage()
|
|
59
|
+
|
|
60
|
+
const EXCLUDED_PROPS = new Set(['colorMode'])
|
|
61
|
+
const editableProps = computed(() =>
|
|
62
|
+
Object.keys(propEditor.value).filter(k => !EXCLUDED_PROPS.has(k)),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
function editProp(key: string, value: unknown) {
|
|
66
|
+
propEditor.value = { ...propEditor.value, [key]: value }
|
|
67
|
+
updateProps({ [key]: value })
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function inferPropType(value: unknown): 'color' | 'number' | 'json' | 'string' {
|
|
71
|
+
if (typeof value === 'number')
|
|
72
|
+
return 'number'
|
|
73
|
+
if (typeof value === 'string' && RE_HEX_COLOR.test(value))
|
|
74
|
+
return 'color'
|
|
75
|
+
if (typeof value === 'string')
|
|
76
|
+
return 'string'
|
|
77
|
+
return 'json'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const rendererIcons: Record<string, string> = {
|
|
81
|
+
satori: 'logos:vercel-icon',
|
|
82
|
+
browser: 'logos:chrome',
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const activeFormatLabel = computed(() => {
|
|
86
|
+
const ext = imageFormat.value === 'jpeg' ? 'JPG' : imageFormat.value?.toUpperCase() || 'PNG'
|
|
87
|
+
const name = renderer.value === 'browser' ? 'Browser' : renderer.value === 'takumi' ? 'Takumi' : 'Satori'
|
|
88
|
+
return `${name} - ${ext}`
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const socialItems = [
|
|
92
|
+
{ label: 'Raw', icon: 'carbon:image', value: '' },
|
|
93
|
+
// Optical size adjustments: thin logos scale up, dense filled shapes scale down
|
|
94
|
+
{ label: 'Twitter / X', icon: 'simple-icons:x', value: 'twitter', iconScale: 0.8 },
|
|
95
|
+
{ label: 'Facebook', icon: 'simple-icons:facebook', value: 'facebook', iconScale: 0.92 },
|
|
96
|
+
{ label: 'LinkedIn', icon: 'simple-icons:linkedin', value: 'linkedin', iconScale: 0.92 },
|
|
97
|
+
{ label: 'Discord', icon: 'simple-icons:discord', value: 'discord', iconScale: 0.95 },
|
|
98
|
+
{ label: 'Slack', icon: 'simple-icons:slack', value: 'slack' },
|
|
99
|
+
{ label: 'WhatsApp', icon: 'simple-icons:whatsapp', value: 'whatsapp', iconScale: 0.92 },
|
|
100
|
+
{ label: 'Bluesky', icon: 'simple-icons:bluesky', value: 'bluesky', iconScale: 0.92 },
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
// Size variants per social platform — different display contexts show the card at different widths
|
|
104
|
+
interface SizeVariant { label: string, width: number }
|
|
105
|
+
const socialSizeVariants: Record<string, SizeVariant[]> = {
|
|
106
|
+
twitter: [
|
|
107
|
+
{ label: 'Feed', width: 504 },
|
|
108
|
+
{ label: 'Detail', width: 600 },
|
|
109
|
+
{ label: 'Mobile', width: 335 },
|
|
110
|
+
],
|
|
111
|
+
facebook: [
|
|
112
|
+
{ label: 'Feed', width: 500 },
|
|
113
|
+
{ label: 'Mobile', width: 320 },
|
|
114
|
+
],
|
|
115
|
+
linkedin: [
|
|
116
|
+
{ label: 'Feed', width: 552 },
|
|
117
|
+
{ label: 'Mobile', width: 335 },
|
|
118
|
+
],
|
|
119
|
+
discord: [
|
|
120
|
+
{ label: 'Embed', width: 432 },
|
|
121
|
+
{ label: 'Mobile', width: 300 },
|
|
122
|
+
],
|
|
123
|
+
slack: [
|
|
124
|
+
{ label: 'Desktop', width: 500 },
|
|
125
|
+
{ label: 'Compact', width: 360 },
|
|
126
|
+
],
|
|
127
|
+
whatsapp: [
|
|
128
|
+
{ label: 'Default', width: 600 },
|
|
129
|
+
{ label: 'Squared', width: 600 },
|
|
130
|
+
],
|
|
131
|
+
bluesky: [
|
|
132
|
+
{ label: 'Post', width: 513 },
|
|
133
|
+
{ label: 'With link', width: 487 },
|
|
134
|
+
{ label: 'Mobile', width: 335 },
|
|
135
|
+
],
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const activeSizeVariant = ref<Record<string, number>>({})
|
|
139
|
+
|
|
140
|
+
const currentSizeVariants = computed(() => socialSizeVariants[socialPreview.value] || [])
|
|
141
|
+
const currentSizeWidth = computed(() => {
|
|
142
|
+
const variants = currentSizeVariants.value
|
|
143
|
+
if (!variants.length)
|
|
144
|
+
return undefined
|
|
145
|
+
const idx = activeSizeVariant.value[socialPreview.value] ?? 0
|
|
146
|
+
return variants[idx]?.width
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// Raw preview draggable resize
|
|
150
|
+
const rawResizeWidth = ref<number | null>(null)
|
|
151
|
+
const isRawDragging = ref(false)
|
|
152
|
+
const rawPreviewRef = ref<HTMLElement | null>(null)
|
|
153
|
+
|
|
154
|
+
const isRawMode = computed(() => !socialPreview.value)
|
|
155
|
+
|
|
156
|
+
const previewMaxWidth = computed(() => {
|
|
157
|
+
if (isRawMode.value && rawResizeWidth.value)
|
|
158
|
+
return `${rawResizeWidth.value}px`
|
|
159
|
+
if (currentSizeWidth.value)
|
|
160
|
+
return `${currentSizeWidth.value}px`
|
|
161
|
+
return undefined
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
const whatsappSquared = computed(() => {
|
|
165
|
+
const idx = activeSizeVariant.value.whatsapp ?? 0
|
|
166
|
+
return socialPreview.value === 'whatsapp' && idx === 1
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
function onRawHandlePointerDown(e: PointerEvent) {
|
|
170
|
+
e.preventDefault()
|
|
171
|
+
const el = rawPreviewRef.value
|
|
172
|
+
if (!el)
|
|
173
|
+
return
|
|
174
|
+
isRawDragging.value = true
|
|
175
|
+
const startX = e.clientX
|
|
176
|
+
const startWidth = el.getBoundingClientRect().width
|
|
177
|
+
|
|
178
|
+
function onMove(ev: PointerEvent) {
|
|
179
|
+
// Handle is centered, so distance from center = half the width change
|
|
180
|
+
const dx = ev.clientX - startX
|
|
181
|
+
rawResizeWidth.value = Math.max(200, Math.round(startWidth + dx * 2))
|
|
182
|
+
}
|
|
183
|
+
function onUp() {
|
|
184
|
+
isRawDragging.value = false
|
|
185
|
+
document.removeEventListener('pointermove', onMove)
|
|
186
|
+
document.removeEventListener('pointerup', onUp)
|
|
187
|
+
}
|
|
188
|
+
document.addEventListener('pointermove', onMove)
|
|
189
|
+
document.addEventListener('pointerup', onUp)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const protoTab = ref('meta-tags')
|
|
193
|
+
const protoTabs = computed(() => {
|
|
194
|
+
const tabs = [
|
|
195
|
+
{ label: 'Meta Tags', value: 'meta-tags' },
|
|
196
|
+
{ label: 'OG Image Props', value: 'og-props' },
|
|
197
|
+
{ label: 'Fonts', value: 'fonts' },
|
|
198
|
+
]
|
|
199
|
+
if (!socialPreview.value)
|
|
200
|
+
return tabs.filter(t => t.value !== 'meta-tags')
|
|
201
|
+
return tabs
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
watch(socialPreview, (val) => {
|
|
205
|
+
if (!val && protoTab.value === 'meta-tags')
|
|
206
|
+
protoTab.value = 'og-props'
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
const isTwitterMode = computed(() => socialPreview.value === 'twitter')
|
|
210
|
+
const metaLabelPrefix = computed(() => isTwitterMode.value ? 'Twitter' : 'OG')
|
|
211
|
+
|
|
212
|
+
interface FontFileEntry {
|
|
213
|
+
key: string
|
|
214
|
+
family: string
|
|
215
|
+
weight: number
|
|
216
|
+
style: string
|
|
217
|
+
src: string
|
|
218
|
+
label: string
|
|
219
|
+
loaded: boolean
|
|
220
|
+
subsetCount: number
|
|
221
|
+
subsets: string[]
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const fontFiles = computed<FontFileEntry[]>(() => {
|
|
225
|
+
const available = globalDebug.value?.availableFonts || []
|
|
226
|
+
const resolved = globalDebug.value?.resolvedFonts || []
|
|
227
|
+
if (!available.length && !resolved.length)
|
|
228
|
+
return []
|
|
229
|
+
const source = available.length ? available : resolved
|
|
230
|
+
// Build set of resolved keys for loaded check
|
|
231
|
+
const resolvedKeys = new Set(resolved.map((f: any) => `${f.family}-${f.weight}-${f.style}`))
|
|
232
|
+
// Count subsets per family+weight+style and collect subset names
|
|
233
|
+
const subsetMap = new Map<string, string[]>()
|
|
234
|
+
for (const f of source) {
|
|
235
|
+
const key = `${f.family}-${f.weight}-${f.style}`
|
|
236
|
+
const subs = subsetMap.get(key) || []
|
|
237
|
+
if (f.subset)
|
|
238
|
+
subs.push(f.subset)
|
|
239
|
+
subsetMap.set(key, subs)
|
|
240
|
+
}
|
|
241
|
+
// Dedupe by family+weight+style (multiple unicode-range subsets)
|
|
242
|
+
const seen = new Set<string>()
|
|
243
|
+
const entries: FontFileEntry[] = []
|
|
244
|
+
for (const f of source) {
|
|
245
|
+
const key = `${f.family}-${f.weight}-${f.style}`
|
|
246
|
+
if (seen.has(key))
|
|
247
|
+
continue
|
|
248
|
+
seen.add(key)
|
|
249
|
+
const subs = subsetMap.get(key) || []
|
|
250
|
+
entries.push({
|
|
251
|
+
key,
|
|
252
|
+
family: f.family,
|
|
253
|
+
weight: f.weight,
|
|
254
|
+
style: f.style,
|
|
255
|
+
src: f.src,
|
|
256
|
+
label: `${f.family} ${f.weight}${f.style === 'italic' ? 'i' : ''}`,
|
|
257
|
+
loaded: resolvedKeys.has(key),
|
|
258
|
+
subsetCount: Math.max(subs.length, 1),
|
|
259
|
+
subsets: subs,
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
// Sort: by family, then weight, then style
|
|
263
|
+
entries.sort((a, b) => a.family.localeCompare(b.family) || a.weight - b.weight || a.style.localeCompare(b.style))
|
|
264
|
+
return entries
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
/** Total byte size of all resolved font files per family */
|
|
268
|
+
const fontFamilySizes = computed(() => {
|
|
269
|
+
const resolved = globalDebug.value?.resolvedFonts || []
|
|
270
|
+
const sizes = new Map<string, number>()
|
|
271
|
+
for (const f of resolved) {
|
|
272
|
+
if (f.size)
|
|
273
|
+
sizes.set(f.family, (sizes.get(f.family) || 0) + f.size)
|
|
274
|
+
}
|
|
275
|
+
return sizes
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
function formatBytes(bytes: number): string {
|
|
279
|
+
if (bytes < 1024)
|
|
280
|
+
return `${bytes} B`
|
|
281
|
+
const kb = bytes / 1024
|
|
282
|
+
return kb < 1000 ? `${kb.toFixed(1)} kB` : `${(kb / 1024).toFixed(1)} MB`
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const fontFamilyNames = computed(() => [...new Set(fontFiles.value.map(f => f.family))])
|
|
286
|
+
const resolvedFamilyNames = computed(() => [...new Set(fontFiles.value.filter(f => f.loaded).map(f => f.family))])
|
|
287
|
+
|
|
288
|
+
const detectedFontRequirements = computed(() => globalDebug.value?.fontRequirements)
|
|
289
|
+
|
|
290
|
+
const unresolvedFamilies = computed(() => {
|
|
291
|
+
const detected = detectedFontRequirements.value?.families
|
|
292
|
+
if (!detected?.length)
|
|
293
|
+
return []
|
|
294
|
+
const available = new Set(fontFamilyNames.value)
|
|
295
|
+
return detected.filter((f: string) => !available.has(f))
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
const fontFaceCss = computed(() => {
|
|
299
|
+
const rules: string[] = []
|
|
300
|
+
const seen = new Set<string>()
|
|
301
|
+
for (const f of fontFiles.value) {
|
|
302
|
+
if (seen.has(f.family))
|
|
303
|
+
continue
|
|
304
|
+
seen.add(f.family)
|
|
305
|
+
rules.push(`@font-face { font-family: 'ogp-${f.family}'; src: url('${f.src}'); font-display: swap; }`)
|
|
306
|
+
}
|
|
307
|
+
return rules.join('\n')
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
useHead({ style: computed(() => fontFaceCss.value ? [fontFaceCss.value] : []) })
|
|
311
|
+
|
|
312
|
+
const fontOverride = ref('')
|
|
313
|
+
|
|
314
|
+
function applyFontOverride(family: string) {
|
|
315
|
+
const next = fontOverride.value === family ? '' : family
|
|
316
|
+
fontOverride.value = next
|
|
317
|
+
hasMadeChanges.value = true
|
|
318
|
+
updateProps({ fontFamily: next || undefined })
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const metaOverrides = reactive({
|
|
322
|
+
ogTitle: '',
|
|
323
|
+
twitterTitle: '',
|
|
324
|
+
siteName: '',
|
|
325
|
+
description: '',
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
const effectiveTitle = computed(() => {
|
|
329
|
+
if (socialPreview.value === 'twitter' && metaOverrides.twitterTitle)
|
|
330
|
+
return metaOverrides.twitterTitle
|
|
331
|
+
return metaOverrides.ogTitle || socialPreviewTitle.value
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
const effectiveDescription = computed(() => {
|
|
335
|
+
return metaOverrides.description || socialPreviewDescription.value
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
const effectiveSiteName = computed(() => {
|
|
339
|
+
return metaOverrides.siteName || slackSocialPreviewSiteName.value
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
const effectiveSiteUrl = computed(() => {
|
|
343
|
+
return metaOverrides.siteName || socialSiteUrl.value
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
function updateMetaField(field: keyof typeof metaOverrides, value: string) {
|
|
347
|
+
metaOverrides[field] = value
|
|
348
|
+
hasMadeChanges.value = true
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const hasMetaOverrides = computed(() => Object.values(metaOverrides).some(Boolean))
|
|
352
|
+
|
|
353
|
+
const seoMetaSnippet = computed(() => {
|
|
354
|
+
const entries: string[] = []
|
|
355
|
+
if (metaOverrides.ogTitle)
|
|
356
|
+
entries.push(` ogTitle: '${metaOverrides.ogTitle.replace(RE_SINGLE_QUOTE, '\\\'')}'`)
|
|
357
|
+
if (metaOverrides.description)
|
|
358
|
+
entries.push(` ogDescription: '${metaOverrides.description.replace(RE_SINGLE_QUOTE, '\\\'')}'`)
|
|
359
|
+
if (metaOverrides.siteName)
|
|
360
|
+
entries.push(` ogSiteName: '${metaOverrides.siteName.replace(RE_SINGLE_QUOTE, '\\\'')}'`)
|
|
361
|
+
if (metaOverrides.twitterTitle)
|
|
362
|
+
entries.push(` twitterTitle: '${metaOverrides.twitterTitle.replace(RE_SINGLE_QUOTE, '\\\'')}'`)
|
|
363
|
+
if (!entries.length)
|
|
364
|
+
return ''
|
|
365
|
+
return `useSeoMeta({\n${entries.join(',\n')}\n})`
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
function resetAll() {
|
|
369
|
+
resetProps(true)
|
|
370
|
+
fontOverride.value = ''
|
|
371
|
+
Object.assign(metaOverrides, { ogTitle: '', twitterTitle: '', siteName: '', description: '' })
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const productionHostname = computed(() => {
|
|
375
|
+
try {
|
|
376
|
+
return new URL(productionUrl.value).hostname
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
return productionUrl.value
|
|
380
|
+
}
|
|
381
|
+
})
|
|
382
|
+
</script>
|
|
383
|
+
|
|
384
|
+
<template>
|
|
385
|
+
<div class="preview-container card animate-fade-up">
|
|
386
|
+
<!-- Demo mode when devtools connection fails -->
|
|
387
|
+
<div v-if="isConnectionFailed" class="h-full flex flex-col">
|
|
388
|
+
<DevtoolsAlert variant="warning">
|
|
389
|
+
Could not connect to devtools. Showing demo preview.
|
|
390
|
+
</DevtoolsAlert>
|
|
391
|
+
<div class="flex-1 flex items-center justify-center p-6 sm:p-8">
|
|
392
|
+
<div class="w-full max-w-2xl">
|
|
393
|
+
<TwitterCardRenderer title="My Page Title" :aspect-ratio="1200 / 630">
|
|
394
|
+
<template #domain>
|
|
395
|
+
<span>From example.com</span>
|
|
396
|
+
</template>
|
|
397
|
+
<div class="w-full h-full bg-gradient-to-br from-emerald-500 to-sky-500 flex items-center justify-center">
|
|
398
|
+
<div class="text-white text-center p-6">
|
|
399
|
+
<div class="text-2xl sm:text-4xl font-bold mb-2">
|
|
400
|
+
OG Image Preview
|
|
401
|
+
</div>
|
|
402
|
+
<div class="text-sm sm:text-lg opacity-80">
|
|
403
|
+
Connect devtools to see your actual image
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
</TwitterCardRenderer>
|
|
408
|
+
<p class="text-center text-sm text-[var(--color-text-muted)] mt-4">
|
|
409
|
+
Open this page in Nuxt DevTools to preview your OG images.
|
|
410
|
+
</p>
|
|
411
|
+
</div>
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
414
|
+
|
|
415
|
+
<!-- Custom OG Image (prebuilt URL) -->
|
|
416
|
+
<div v-else-if="isCustomOgImage" class="h-full flex flex-col">
|
|
417
|
+
<DevtoolsToolbar variant="minimal">
|
|
418
|
+
<span class="text-[var(--color-text-subtle)]">Prebuilt:</span>
|
|
419
|
+
<code class="text-[var(--color-text-muted)]">{{ options?.url }}</code>
|
|
420
|
+
</DevtoolsToolbar>
|
|
421
|
+
<div class="flex-1 flex items-center justify-center p-6 sm:p-8">
|
|
422
|
+
<ImageLoader
|
|
423
|
+
:src="src"
|
|
424
|
+
:aspect-ratio="aspectRatio"
|
|
425
|
+
@load="generateLoadTime"
|
|
426
|
+
@refresh="refreshSources"
|
|
427
|
+
/>
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
<!-- Loading state -->
|
|
432
|
+
<div v-else-if="isDebugLoading" class="h-full flex items-center justify-center p-6 sm:p-8">
|
|
433
|
+
<DevtoolsEmptyState icon="carbon:image-search" title="Loading OG Image…" class="animate-pulse" />
|
|
434
|
+
</div>
|
|
435
|
+
|
|
436
|
+
<!-- Server error (e.g. font loading failure) -->
|
|
437
|
+
<div v-else-if="fetchError" class="h-full flex items-center justify-center p-6 sm:p-8">
|
|
438
|
+
<DevtoolsEmptyState
|
|
439
|
+
icon="carbon:warning"
|
|
440
|
+
title="OG Image Error"
|
|
441
|
+
:description="fetchError.message"
|
|
442
|
+
variant="error"
|
|
443
|
+
class="animate-scale-in"
|
|
444
|
+
>
|
|
445
|
+
<details v-if="fetchError.stack?.length" class="text-left w-full">
|
|
446
|
+
<summary class="text-xs text-[var(--color-text-subtle)] cursor-pointer hover:text-[var(--color-text-muted)]">
|
|
447
|
+
Stack trace
|
|
448
|
+
</summary>
|
|
449
|
+
<pre class="mt-2 text-xs text-[var(--color-text-subtle)] overflow-auto max-h-48 p-3 rounded-lg bg-[var(--color-surface-sunken)] border border-[var(--color-border-subtle)]">{{ fetchError.stack.join('\n') }}</pre>
|
|
450
|
+
</details>
|
|
451
|
+
</DevtoolsEmptyState>
|
|
452
|
+
</div>
|
|
453
|
+
|
|
454
|
+
<!-- Missing defineOgImage error -->
|
|
455
|
+
<div v-else-if="isValidDebugError || !hasDefinedOgImage" class="h-full flex items-center justify-center p-6 sm:p-8">
|
|
456
|
+
<DevtoolsEmptyState
|
|
457
|
+
icon="carbon:image-search"
|
|
458
|
+
title="No OG Image Defined"
|
|
459
|
+
class="animate-scale-in"
|
|
460
|
+
>
|
|
461
|
+
<template #description>
|
|
462
|
+
This page has no <code class="inline-code">defineOgImage()</code> in
|
|
463
|
+
<UButton variant="link" class="cursor-pointer" @click="openCurrentPageFile">
|
|
464
|
+
{{ currentPageFile }}
|
|
465
|
+
</UButton>
|
|
466
|
+
</template>
|
|
467
|
+
|
|
468
|
+
<!-- Existing components: let user pick one -->
|
|
469
|
+
<div v-if="componentNames.length" class="mb-4 flex flex-col items-center gap-3">
|
|
470
|
+
<div class="flex flex-wrap justify-center gap-2">
|
|
471
|
+
<UButton
|
|
472
|
+
v-for="c in componentNames"
|
|
473
|
+
:key="c.pascalName"
|
|
474
|
+
color="primary"
|
|
475
|
+
variant="soft"
|
|
476
|
+
size="sm"
|
|
477
|
+
@click="addExistingComponent(c.pascalName)"
|
|
478
|
+
>
|
|
479
|
+
{{ c.pascalName }}
|
|
480
|
+
</UButton>
|
|
481
|
+
</div>
|
|
482
|
+
<span class="text-xs text-[var(--color-text-subtle)]">or</span>
|
|
483
|
+
<UButton variant="link" size="xs" class="text-[var(--color-text-subtle)]" @click="createComponent()">
|
|
484
|
+
Create new component
|
|
485
|
+
</UButton>
|
|
486
|
+
</div>
|
|
487
|
+
|
|
488
|
+
<!-- No components: create is the primary CTA -->
|
|
489
|
+
<div v-else class="mb-4">
|
|
490
|
+
<UButton color="primary" size="md" @click="createComponent()">
|
|
491
|
+
<UIcon name="carbon:add" class="w-4 h-4" />
|
|
492
|
+
Create OG Image Component
|
|
493
|
+
</UButton>
|
|
494
|
+
</div>
|
|
495
|
+
|
|
496
|
+
<div v-if="globalDebug?.runtimeConfig?.hasNuxtContent" class="text-sm text-[var(--color-text-subtle)]">
|
|
497
|
+
Using Nuxt Content? See the <a href="https://nuxtseo.com/docs/integrations/content" target="_blank" class="text-[var(--seo-green)] hover:underline">integration guide</a>
|
|
498
|
+
</div>
|
|
499
|
+
<a v-else href="https://nuxtseo.com/og-image/getting-started/getting-familar-with-nuxt-og-image" target="_blank" class="text-sm text-[var(--seo-green)] hover:underline inline-flex items-center gap-1">
|
|
500
|
+
Learn more
|
|
501
|
+
<UIcon name="carbon:arrow-right" class="w-3 h-3" />
|
|
502
|
+
</a>
|
|
503
|
+
</DevtoolsEmptyState>
|
|
504
|
+
</div>
|
|
505
|
+
|
|
506
|
+
<!-- Main preview UI -->
|
|
507
|
+
<div v-else class="h-full flex flex-col">
|
|
508
|
+
<!-- Fallback mode banner -->
|
|
509
|
+
<DevtoolsAlert v-if="isFallbackMode" variant="info">
|
|
510
|
+
Fallback mode: Connected to localhost:3000
|
|
511
|
+
</DevtoolsAlert>
|
|
512
|
+
|
|
513
|
+
<!-- Production preview banner -->
|
|
514
|
+
<DevtoolsAlert v-if="previewSource === 'production' && hasProductionUrl" variant="production">
|
|
515
|
+
Previewing from <strong>{{ productionUrl }}</strong>
|
|
516
|
+
<template #action>
|
|
517
|
+
<UButton variant="link" size="xs" @click="previewSource = 'local'">
|
|
518
|
+
Switch to local
|
|
519
|
+
</UButton>
|
|
520
|
+
</template>
|
|
521
|
+
</DevtoolsAlert>
|
|
522
|
+
|
|
523
|
+
<!-- Renderer incompatibility banner -->
|
|
524
|
+
<DevtoolsAlert v-if="!isComponentCompatibleWithRenderer && activeComponent" variant="warning">
|
|
525
|
+
Component <code class="inline-code">{{ activeComponentName }}</code> uses {{ activeComponent.renderer }} renderer.
|
|
526
|
+
<NuxtLink to="/templates" class="text-[var(--seo-green)] hover:underline ml-1">
|
|
527
|
+
Select a {{ renderer }} template
|
|
528
|
+
</NuxtLink>
|
|
529
|
+
</DevtoolsAlert>
|
|
530
|
+
|
|
531
|
+
<!-- Top toolbar -->
|
|
532
|
+
<DevtoolsToolbar>
|
|
533
|
+
<!-- Left: Renderer + Format controls -->
|
|
534
|
+
<div class="flex items-center gap-2 sm:gap-3 flex-wrap">
|
|
535
|
+
<UButton color="neutral" variant="ghost" size="xs" aria-label="Change renderer and format" @click="RendererSelectDialogPromise.start()">
|
|
536
|
+
<img v-if="renderer === 'takumi'" src="https://takumi.kane.tw/logo.svg" class="w-3.5 h-3.5" width="14" height="14" alt="">
|
|
537
|
+
<UIcon v-else :name="rendererIcons[renderer] || 'logos:vercel-icon'" class="w-3.5 h-3.5" />
|
|
538
|
+
<span class="hidden sm:inline">{{ activeFormatLabel }}</span>
|
|
539
|
+
<UIcon name="carbon:chevron-down" class="w-3 h-3 opacity-60" />
|
|
540
|
+
</UButton>
|
|
541
|
+
</div>
|
|
542
|
+
|
|
543
|
+
<!-- Center: Component info -->
|
|
544
|
+
<div v-if="!isPageScreenshot" class="component-info hidden md:flex">
|
|
545
|
+
<UButton
|
|
546
|
+
variant="link"
|
|
547
|
+
class="component-path-link cursor-pointer"
|
|
548
|
+
:disabled="isOgImageTemplate"
|
|
549
|
+
@click="openCurrentComponent"
|
|
550
|
+
>
|
|
551
|
+
<span v-if="activeComponentRelativePath"><span class="text-[var(--color-text-muted)] hidden min-[850px]:inline">{{ activeComponentRelativePath.replace(RE_VUE_FILENAME, '') }}</span><span class="text-[var(--color-text)]">{{ activeComponentRelativePath.match(RE_VUE_FILENAME)?.[0] || activeComponentName }}</span></span>
|
|
552
|
+
<span v-else class="text-[var(--color-text)]">{{ activeComponentName }}</span>
|
|
553
|
+
</UButton>
|
|
554
|
+
<UButton
|
|
555
|
+
v-if="isOgImageTemplate"
|
|
556
|
+
variant="link"
|
|
557
|
+
size="xs"
|
|
558
|
+
class="cursor-pointer text-[var(--color-text-subtle)]"
|
|
559
|
+
@click="ejectComponent(activeComponentName)"
|
|
560
|
+
>
|
|
561
|
+
Eject
|
|
562
|
+
</UButton>
|
|
563
|
+
</div>
|
|
564
|
+
<div v-else class="component-info hidden md:flex">
|
|
565
|
+
<span class="text-[var(--color-text-subtle)]">Page Screenshot</span>
|
|
566
|
+
</div>
|
|
567
|
+
|
|
568
|
+
<!-- Right: Props toggle -->
|
|
569
|
+
<div class="flex items-center gap-2">
|
|
570
|
+
<!-- Production URL indicator -->
|
|
571
|
+
<UTooltip v-if="previewSource === 'production' && hasProductionUrl" :text="productionUrl" :delay-duration="200">
|
|
572
|
+
<span class="devtools-production-badge">
|
|
573
|
+
<span class="devtools-production-dot" />
|
|
574
|
+
<span class="hidden sm:inline text-xs">{{ productionHostname }}</span>
|
|
575
|
+
</span>
|
|
576
|
+
</UTooltip>
|
|
577
|
+
|
|
578
|
+
<UButton
|
|
579
|
+
v-if="!isPageScreenshot"
|
|
580
|
+
:variant="sidePanelOpen ? 'soft' : 'ghost'"
|
|
581
|
+
:color="sidePanelOpen ? 'primary' : 'neutral'"
|
|
582
|
+
size="xs"
|
|
583
|
+
icon="carbon:settings-adjust"
|
|
584
|
+
class="props-toggle"
|
|
585
|
+
@click="sidePanelOpen = !sidePanelOpen"
|
|
586
|
+
>
|
|
587
|
+
<span class="hidden sm:inline">Debug</span>
|
|
588
|
+
</UButton>
|
|
589
|
+
</div>
|
|
590
|
+
</DevtoolsToolbar>
|
|
591
|
+
|
|
592
|
+
<!-- Social preview tabs -->
|
|
593
|
+
<div class="px-3 sm:px-4 border-b border-[var(--color-border)] bg-[var(--color-surface-elevated)]">
|
|
594
|
+
<UTabs
|
|
595
|
+
v-model="socialPreview"
|
|
596
|
+
:items="socialItems"
|
|
597
|
+
:content="false"
|
|
598
|
+
size="xs"
|
|
599
|
+
variant="link"
|
|
600
|
+
color="neutral"
|
|
601
|
+
>
|
|
602
|
+
<template #leading="{ item, ui }">
|
|
603
|
+
<UIcon
|
|
604
|
+
:name="item.icon"
|
|
605
|
+
:class="ui.leadingIcon"
|
|
606
|
+
:style="item.iconScale ? { transform: `scale(${item.iconScale})` } : undefined"
|
|
607
|
+
/>
|
|
608
|
+
</template>
|
|
609
|
+
</UTabs>
|
|
610
|
+
</div>
|
|
611
|
+
|
|
612
|
+
<!-- Size variant toggles + image key selector -->
|
|
613
|
+
<div v-if="currentSizeVariants.length || allImageKeys.length > 1" class="size-variant-bar">
|
|
614
|
+
<template v-if="currentSizeVariants.length">
|
|
615
|
+
<span class="size-variant-label">Width</span>
|
|
616
|
+
<div class="size-variant-group">
|
|
617
|
+
<button
|
|
618
|
+
v-for="(variant, idx) in currentSizeVariants"
|
|
619
|
+
:key="variant.label"
|
|
620
|
+
class="size-variant-btn"
|
|
621
|
+
:class="{ active: (activeSizeVariant[socialPreview] ?? 0) === idx }"
|
|
622
|
+
@click="activeSizeVariant[socialPreview] = idx"
|
|
623
|
+
>
|
|
624
|
+
<span class="size-variant-name">{{ variant.label }}</span>
|
|
625
|
+
<span class="size-variant-px">{{ variant.width }}px</span>
|
|
626
|
+
</button>
|
|
627
|
+
</div>
|
|
628
|
+
</template>
|
|
629
|
+
|
|
630
|
+
<template v-if="allImageKeys.length > 1">
|
|
631
|
+
<span class="size-variant-label">Image</span>
|
|
632
|
+
<div class="size-variant-group">
|
|
633
|
+
<button
|
|
634
|
+
v-for="key in allImageKeys"
|
|
635
|
+
:key="key"
|
|
636
|
+
class="size-variant-btn"
|
|
637
|
+
:class="{ active: (ogImageKey || 'og') === key }"
|
|
638
|
+
@click="ogImageKey = key"
|
|
639
|
+
>
|
|
640
|
+
<span class="size-variant-name">{{ key }}</span>
|
|
641
|
+
</button>
|
|
642
|
+
</div>
|
|
643
|
+
</template>
|
|
644
|
+
</div>
|
|
645
|
+
|
|
646
|
+
<!-- Preview area -->
|
|
647
|
+
<div class="preview-area panel-grids" :class="{ 'preview-area--panel-open': sidePanelOpen && !isPageScreenshot }">
|
|
648
|
+
<div ref="rawPreviewRef" class="preview-content" :class="{ 'preview-content--no-transition': isRawDragging }" :style="previewMaxWidth ? { maxWidth: previewMaxWidth } : undefined">
|
|
649
|
+
<!-- Twitter/X preview -->
|
|
650
|
+
<TwitterCardRenderer v-if="socialPreview === 'twitter'" :title="effectiveTitle" :aspect-ratio="aspectRatio">
|
|
651
|
+
<template #domain>
|
|
652
|
+
<a target="_blank" :href="withHttps(socialSiteUrl)">From {{ effectiveSiteUrl }}</a>
|
|
653
|
+
</template>
|
|
654
|
+
<ImageLoader
|
|
655
|
+
v-if="imageFormat !== 'html'"
|
|
656
|
+
:src="src"
|
|
657
|
+
:aspect-ratio="aspectRatio"
|
|
658
|
+
@load="generateLoadTime"
|
|
659
|
+
@click="openImage"
|
|
660
|
+
@refresh="refreshSources"
|
|
661
|
+
/>
|
|
662
|
+
<IFrameLoader
|
|
663
|
+
v-else
|
|
664
|
+
:src="src"
|
|
665
|
+
:aspect-ratio="aspectRatio"
|
|
666
|
+
@load="generateLoadTime"
|
|
667
|
+
@refresh="refreshSources"
|
|
668
|
+
/>
|
|
669
|
+
</TwitterCardRenderer>
|
|
670
|
+
|
|
671
|
+
<!-- Facebook preview -->
|
|
672
|
+
<FacebookCardRenderer v-else-if="socialPreview === 'facebook'">
|
|
673
|
+
<template #siteName>
|
|
674
|
+
{{ effectiveSiteUrl }}
|
|
675
|
+
</template>
|
|
676
|
+
<template #title>
|
|
677
|
+
{{ effectiveTitle }}
|
|
678
|
+
</template>
|
|
679
|
+
<template #description>
|
|
680
|
+
{{ effectiveDescription }}
|
|
681
|
+
</template>
|
|
682
|
+
<ImageLoader
|
|
683
|
+
v-if="imageFormat !== 'html'"
|
|
684
|
+
:src="src"
|
|
685
|
+
:aspect-ratio="aspectRatio"
|
|
686
|
+
@load="generateLoadTime"
|
|
687
|
+
@refresh="refreshSources"
|
|
688
|
+
/>
|
|
689
|
+
<IFrameLoader
|
|
690
|
+
v-else
|
|
691
|
+
:src="src"
|
|
692
|
+
:aspect-ratio="aspectRatio"
|
|
693
|
+
@load="generateLoadTime"
|
|
694
|
+
@refresh="refreshSources"
|
|
695
|
+
/>
|
|
696
|
+
</FacebookCardRenderer>
|
|
697
|
+
|
|
698
|
+
<!-- LinkedIn preview -->
|
|
699
|
+
<LinkedInCardRenderer v-else-if="socialPreview === 'linkedin'">
|
|
700
|
+
<template #siteName>
|
|
701
|
+
{{ effectiveSiteUrl }}
|
|
702
|
+
</template>
|
|
703
|
+
<template #title>
|
|
704
|
+
{{ effectiveTitle }}
|
|
705
|
+
</template>
|
|
706
|
+
<ImageLoader
|
|
707
|
+
v-if="imageFormat !== 'html'"
|
|
708
|
+
:src="src"
|
|
709
|
+
:aspect-ratio="aspectRatio"
|
|
710
|
+
@load="generateLoadTime"
|
|
711
|
+
@refresh="refreshSources"
|
|
712
|
+
/>
|
|
713
|
+
<IFrameLoader
|
|
714
|
+
v-else
|
|
715
|
+
:src="src"
|
|
716
|
+
:aspect-ratio="aspectRatio"
|
|
717
|
+
@load="generateLoadTime"
|
|
718
|
+
@refresh="refreshSources"
|
|
719
|
+
/>
|
|
720
|
+
</LinkedInCardRenderer>
|
|
721
|
+
|
|
722
|
+
<!-- Discord preview -->
|
|
723
|
+
<DiscordCardRenderer v-else-if="socialPreview === 'discord'">
|
|
724
|
+
<template #siteName>
|
|
725
|
+
{{ effectiveSiteName }}
|
|
726
|
+
</template>
|
|
727
|
+
<template #title>
|
|
728
|
+
{{ effectiveTitle }}
|
|
729
|
+
</template>
|
|
730
|
+
<template #description>
|
|
731
|
+
{{ effectiveDescription }}
|
|
732
|
+
</template>
|
|
733
|
+
<ImageLoader
|
|
734
|
+
v-if="imageFormat !== 'html'"
|
|
735
|
+
:src="src"
|
|
736
|
+
:aspect-ratio="aspectRatio"
|
|
737
|
+
@load="generateLoadTime"
|
|
738
|
+
@refresh="refreshSources"
|
|
739
|
+
/>
|
|
740
|
+
<IFrameLoader
|
|
741
|
+
v-else
|
|
742
|
+
:src="src"
|
|
743
|
+
:aspect-ratio="aspectRatio"
|
|
744
|
+
@load="generateLoadTime"
|
|
745
|
+
@refresh="refreshSources"
|
|
746
|
+
/>
|
|
747
|
+
</DiscordCardRenderer>
|
|
748
|
+
|
|
749
|
+
<!-- Slack preview -->
|
|
750
|
+
<SlackCardRenderer v-else-if="socialPreview === 'slack'">
|
|
751
|
+
<template #favIcon>
|
|
752
|
+
<img :src="`https://www.google.com/s2/favicons?domain=${encodeURIComponent(socialSiteUrl)}&sz=30`" width="30" height="30" alt="">
|
|
753
|
+
</template>
|
|
754
|
+
<template #siteName>
|
|
755
|
+
{{ effectiveSiteName }}
|
|
756
|
+
</template>
|
|
757
|
+
<template #title>
|
|
758
|
+
{{ effectiveTitle }}
|
|
759
|
+
</template>
|
|
760
|
+
<template #description>
|
|
761
|
+
{{ effectiveDescription }}
|
|
762
|
+
</template>
|
|
763
|
+
<ImageLoader
|
|
764
|
+
v-if="imageFormat !== 'html'"
|
|
765
|
+
:src="src"
|
|
766
|
+
:aspect-ratio="aspectRatio"
|
|
767
|
+
@load="generateLoadTime"
|
|
768
|
+
@refresh="refreshSources"
|
|
769
|
+
/>
|
|
770
|
+
<IFrameLoader
|
|
771
|
+
v-else
|
|
772
|
+
:src="src"
|
|
773
|
+
:aspect-ratio="aspectRatio"
|
|
774
|
+
@load="generateLoadTime"
|
|
775
|
+
@refresh="refreshSources"
|
|
776
|
+
/>
|
|
777
|
+
</SlackCardRenderer>
|
|
778
|
+
|
|
779
|
+
<!-- WhatsApp preview -->
|
|
780
|
+
<WhatsAppRenderer v-else-if="socialPreview === 'whatsapp'" :squared="whatsappSquared">
|
|
781
|
+
<template #siteName>
|
|
782
|
+
{{ effectiveSiteName }}
|
|
783
|
+
</template>
|
|
784
|
+
<template #title>
|
|
785
|
+
{{ effectiveTitle }}
|
|
786
|
+
</template>
|
|
787
|
+
<template #description>
|
|
788
|
+
{{ effectiveDescription }}
|
|
789
|
+
</template>
|
|
790
|
+
<template #url>
|
|
791
|
+
{{ effectiveSiteUrl }}
|
|
792
|
+
</template>
|
|
793
|
+
<template v-if="whatsappInlineSrc !== src" #inlineImage>
|
|
794
|
+
<img
|
|
795
|
+
:src="whatsappInlineSrc"
|
|
796
|
+
alt=""
|
|
797
|
+
>
|
|
798
|
+
</template>
|
|
799
|
+
<img
|
|
800
|
+
v-if="imageFormat !== 'html'"
|
|
801
|
+
:src="src"
|
|
802
|
+
alt=""
|
|
803
|
+
@load="generateLoadTime({ timeTaken: '0', sizeKb: '' })"
|
|
804
|
+
>
|
|
805
|
+
</WhatsAppRenderer>
|
|
806
|
+
|
|
807
|
+
<!-- Bluesky preview -->
|
|
808
|
+
<BlueskyCardRenderer v-else-if="socialPreview === 'bluesky'">
|
|
809
|
+
<template #siteName>
|
|
810
|
+
{{ effectiveSiteUrl }}
|
|
811
|
+
</template>
|
|
812
|
+
<template #title>
|
|
813
|
+
{{ effectiveTitle }}
|
|
814
|
+
</template>
|
|
815
|
+
<template #description>
|
|
816
|
+
{{ effectiveDescription }}
|
|
817
|
+
</template>
|
|
818
|
+
<ImageLoader
|
|
819
|
+
v-if="imageFormat !== 'html'"
|
|
820
|
+
:src="src"
|
|
821
|
+
:aspect-ratio="aspectRatio"
|
|
822
|
+
@load="generateLoadTime"
|
|
823
|
+
@refresh="refreshSources"
|
|
824
|
+
/>
|
|
825
|
+
<IFrameLoader
|
|
826
|
+
v-else
|
|
827
|
+
:src="src"
|
|
828
|
+
:aspect-ratio="aspectRatio"
|
|
829
|
+
@load="generateLoadTime"
|
|
830
|
+
@refresh="refreshSources"
|
|
831
|
+
/>
|
|
832
|
+
</BlueskyCardRenderer>
|
|
833
|
+
|
|
834
|
+
<!-- Raw preview with resize handle -->
|
|
835
|
+
<div v-else class="raw-preview-wrapper">
|
|
836
|
+
<div class="raw-preview" :class="{ 'raw-preview--dragging': isRawDragging }">
|
|
837
|
+
<ImageLoader
|
|
838
|
+
v-if="imageFormat !== 'html'"
|
|
839
|
+
:src="src"
|
|
840
|
+
:aspect-ratio="aspectRatio"
|
|
841
|
+
@load="generateLoadTime"
|
|
842
|
+
@refresh="refreshSources"
|
|
843
|
+
/>
|
|
844
|
+
<IFrameLoader
|
|
845
|
+
v-else
|
|
846
|
+
:src="src"
|
|
847
|
+
:aspect-ratio="aspectRatio"
|
|
848
|
+
@load="generateLoadTime"
|
|
849
|
+
@refresh="refreshSources"
|
|
850
|
+
/>
|
|
851
|
+
</div>
|
|
852
|
+
<!-- Drag handle — double-click resets -->
|
|
853
|
+
<div
|
|
854
|
+
class="raw-resize-handle"
|
|
855
|
+
@pointerdown="onRawHandlePointerDown"
|
|
856
|
+
@dblclick="rawResizeWidth = null"
|
|
857
|
+
>
|
|
858
|
+
<div class="raw-resize-grip" />
|
|
859
|
+
</div>
|
|
860
|
+
<!-- Width readout -->
|
|
861
|
+
<div v-if="rawResizeWidth" class="raw-resize-readout">
|
|
862
|
+
{{ rawResizeWidth }}px
|
|
863
|
+
</div>
|
|
864
|
+
</div>
|
|
865
|
+
|
|
866
|
+
<!-- Status line -->
|
|
867
|
+
<div v-if="description" class="status-line">
|
|
868
|
+
{{ description }}
|
|
869
|
+
</div>
|
|
870
|
+
</div>
|
|
871
|
+
</div>
|
|
872
|
+
|
|
873
|
+
<!-- Floating Props Panel -->
|
|
874
|
+
<Transition
|
|
875
|
+
enter-active-class="transition duration-200 ease-out"
|
|
876
|
+
enter-from-class="opacity-0 translate-x-4"
|
|
877
|
+
enter-to-class="opacity-100 translate-x-0"
|
|
878
|
+
leave-active-class="transition duration-150 ease-in"
|
|
879
|
+
leave-from-class="opacity-100 translate-x-0"
|
|
880
|
+
leave-to-class="opacity-0 translate-x-4"
|
|
881
|
+
>
|
|
882
|
+
<DevtoolsPanel v-if="sidePanelOpen && !isPageScreenshot" title="Debug" class="props-panel" @close="sidePanelOpen = false">
|
|
883
|
+
<template #actions>
|
|
884
|
+
<UButton
|
|
885
|
+
v-if="hasMadeChanges"
|
|
886
|
+
@click="resetAll()"
|
|
887
|
+
>
|
|
888
|
+
Reset
|
|
889
|
+
</UButton>
|
|
890
|
+
</template>
|
|
891
|
+
<div class="px-3 pt-2 border-b border-[var(--color-border)]">
|
|
892
|
+
<UTabs
|
|
893
|
+
v-model="protoTab"
|
|
894
|
+
:items="protoTabs"
|
|
895
|
+
:content="false"
|
|
896
|
+
size="xs"
|
|
897
|
+
variant="link"
|
|
898
|
+
color="neutral"
|
|
899
|
+
/>
|
|
900
|
+
</div>
|
|
901
|
+
|
|
902
|
+
<!-- Meta Tags Tab -->
|
|
903
|
+
<div v-if="protoTab === 'meta-tags'">
|
|
904
|
+
<div class="props-field">
|
|
905
|
+
<div class="props-field-label">
|
|
906
|
+
<span>{{ metaLabelPrefix }} Title</span>
|
|
907
|
+
<UTooltip :text="isTwitterMode ? 'The twitter:title meta tag. Controls the title shown on Twitter/X cards.' : 'The og:title meta tag. Controls the title shown when shared on social platforms.'" :delay-duration="0">
|
|
908
|
+
<UIcon name="carbon:help" class="w-3.5 h-3.5 text-[var(--color-text-subtle)] cursor-help" />
|
|
909
|
+
</UTooltip>
|
|
910
|
+
</div>
|
|
911
|
+
<UInput
|
|
912
|
+
:model-value="isTwitterMode ? (metaOverrides.twitterTitle || socialPreviewTitle) : (metaOverrides.ogTitle || socialPreviewTitle)"
|
|
913
|
+
size="xs"
|
|
914
|
+
:name="isTwitterMode ? 'twitter-title' : 'og-title'"
|
|
915
|
+
autocomplete="off"
|
|
916
|
+
:placeholder="isTwitterMode ? 'twitter:title…' : 'og:title…'"
|
|
917
|
+
@update:model-value="updateMetaField(isTwitterMode ? 'twitterTitle' : 'ogTitle', $event as string)"
|
|
918
|
+
/>
|
|
919
|
+
</div>
|
|
920
|
+
|
|
921
|
+
<div class="props-field">
|
|
922
|
+
<div class="props-field-label">
|
|
923
|
+
<span>{{ metaLabelPrefix }} Description</span>
|
|
924
|
+
<UTooltip :text="isTwitterMode ? 'The twitter:description meta tag. Controls the description shown on Twitter/X cards.' : 'The og:description meta tag. Controls the description shown when shared on social platforms.'" :delay-duration="0">
|
|
925
|
+
<UIcon name="carbon:help" class="w-3.5 h-3.5 text-[var(--color-text-subtle)] cursor-help" />
|
|
926
|
+
</UTooltip>
|
|
927
|
+
</div>
|
|
928
|
+
<UInput
|
|
929
|
+
:model-value="metaOverrides.description || socialPreviewDescription"
|
|
930
|
+
size="xs"
|
|
931
|
+
:name="isTwitterMode ? 'twitter-description' : 'og-description'"
|
|
932
|
+
autocomplete="off"
|
|
933
|
+
:placeholder="isTwitterMode ? 'twitter:description…' : 'og:description…'"
|
|
934
|
+
@update:model-value="updateMetaField('description', $event as string)"
|
|
935
|
+
/>
|
|
936
|
+
</div>
|
|
937
|
+
|
|
938
|
+
<div class="props-field">
|
|
939
|
+
<div class="props-field-label">
|
|
940
|
+
<span>{{ socialPreview === 'discord' ? 'OG Site Name' : 'OG URL' }}</span>
|
|
941
|
+
<UTooltip :text="socialPreview === 'discord' ? 'The og:site_name meta tag. Discord uses this for the provider name above the title.' : 'The og:url meta tag. The canonical URL shown in social card previews.'" :delay-duration="0">
|
|
942
|
+
<UIcon name="carbon:help" class="w-3.5 h-3.5 text-[var(--color-text-subtle)] cursor-help" />
|
|
943
|
+
</UTooltip>
|
|
944
|
+
</div>
|
|
945
|
+
<UInput
|
|
946
|
+
:model-value="metaOverrides.siteName || slackSocialPreviewSiteName"
|
|
947
|
+
size="xs"
|
|
948
|
+
:name="socialPreview === 'discord' ? 'og-site-name' : 'og-url'"
|
|
949
|
+
autocomplete="off"
|
|
950
|
+
:placeholder="socialPreview === 'discord' ? 'og:site_name…' : 'og:url…'"
|
|
951
|
+
@update:model-value="updateMetaField('siteName', $event as string)"
|
|
952
|
+
/>
|
|
953
|
+
</div>
|
|
954
|
+
</div>
|
|
955
|
+
|
|
956
|
+
<!-- OG Image Props Tab -->
|
|
957
|
+
<div v-else-if="protoTab === 'og-props'">
|
|
958
|
+
<div class="props-field">
|
|
959
|
+
<div class="props-field-label">
|
|
960
|
+
<span>Color Mode</span>
|
|
961
|
+
<UTooltip text="Changes the color mode passed to the OG image renderer." :delay-duration="0">
|
|
962
|
+
<UIcon name="carbon:help" class="w-3.5 h-3.5 text-[var(--color-text-subtle)] cursor-help" />
|
|
963
|
+
</UTooltip>
|
|
964
|
+
</div>
|
|
965
|
+
<UButton
|
|
966
|
+
size="xs"
|
|
967
|
+
color="neutral"
|
|
968
|
+
variant="soft"
|
|
969
|
+
:icon="imageColorMode === 'dark' ? 'carbon:moon' : 'carbon:sun'"
|
|
970
|
+
@click="imageColorMode = imageColorMode === 'dark' ? 'light' : 'dark'"
|
|
971
|
+
>
|
|
972
|
+
{{ imageColorMode === 'dark' ? 'Dark' : 'Light' }}
|
|
973
|
+
</UButton>
|
|
974
|
+
</div>
|
|
975
|
+
|
|
976
|
+
<div v-if="editableProps.length" class="prop-editor">
|
|
977
|
+
<div v-for="key in editableProps" :key="key" class="prop-field">
|
|
978
|
+
<label class="prop-label">{{ key }}</label>
|
|
979
|
+
<input
|
|
980
|
+
v-if="inferPropType(propEditor[key]) === 'color'"
|
|
981
|
+
type="color"
|
|
982
|
+
:value="propEditor[key]"
|
|
983
|
+
class="prop-input prop-input-color"
|
|
984
|
+
@input="editProp(key, ($event.target as HTMLInputElement).value)"
|
|
985
|
+
>
|
|
986
|
+
<input
|
|
987
|
+
v-else-if="inferPropType(propEditor[key]) === 'number'"
|
|
988
|
+
type="number"
|
|
989
|
+
:value="propEditor[key]"
|
|
990
|
+
class="prop-input"
|
|
991
|
+
@input="editProp(key, Number(($event.target as HTMLInputElement).value))"
|
|
992
|
+
>
|
|
993
|
+
<textarea
|
|
994
|
+
v-else-if="inferPropType(propEditor[key]) === 'json'"
|
|
995
|
+
:value="JSON.stringify(propEditor[key], null, 2)"
|
|
996
|
+
class="prop-input prop-input-json"
|
|
997
|
+
rows="3"
|
|
998
|
+
@change="(() => {
|
|
999
|
+
try { editProp(key, JSON.parse(($event.target as HTMLTextAreaElement).value)) }
|
|
1000
|
+
catch {
|
|
1001
|
+
// Invalid JSON is expected while editing; keep the previous prop value.
|
|
1002
|
+
}
|
|
1003
|
+
})()"
|
|
1004
|
+
/>
|
|
1005
|
+
<input
|
|
1006
|
+
v-else
|
|
1007
|
+
type="text"
|
|
1008
|
+
:value="propEditor[key]"
|
|
1009
|
+
class="prop-input"
|
|
1010
|
+
@input="editProp(key, ($event.target as HTMLInputElement).value)"
|
|
1011
|
+
>
|
|
1012
|
+
</div>
|
|
1013
|
+
</div>
|
|
1014
|
+
<div v-else class="prop-editor-empty">
|
|
1015
|
+
No props defined
|
|
1016
|
+
</div>
|
|
1017
|
+
</div>
|
|
1018
|
+
|
|
1019
|
+
<!-- Fonts Tab -->
|
|
1020
|
+
<div v-else-if="protoTab === 'fonts'" class="fonts-tab">
|
|
1021
|
+
<!-- Detected requirements — compact summary bar -->
|
|
1022
|
+
<div v-if="detectedFontRequirements" class="fonts-detected">
|
|
1023
|
+
<div class="fonts-detected-inner">
|
|
1024
|
+
<span class="fonts-detected-label">Detected</span>
|
|
1025
|
+
<span v-for="w in detectedFontRequirements.weights" :key="`w-${w}`" class="fonts-chip">{{ w }}</span>
|
|
1026
|
+
<span v-for="s in detectedFontRequirements.styles" :key="`s-${s}`" class="fonts-chip">{{ s }}</span>
|
|
1027
|
+
<template v-if="detectedFontRequirements.families?.length">
|
|
1028
|
+
<span class="fonts-detected-sep" />
|
|
1029
|
+
<span
|
|
1030
|
+
v-for="f in detectedFontRequirements.families"
|
|
1031
|
+
:key="f"
|
|
1032
|
+
class="fonts-chip"
|
|
1033
|
+
:class="unresolvedFamilies.includes(f) ? 'fonts-chip--error' : 'fonts-chip--family'"
|
|
1034
|
+
>
|
|
1035
|
+
<UIcon v-if="unresolvedFamilies.includes(f)" name="carbon:warning-filled" class="w-2.5 h-2.5 shrink-0" />
|
|
1036
|
+
{{ f }}
|
|
1037
|
+
</span>
|
|
1038
|
+
</template>
|
|
1039
|
+
</div>
|
|
1040
|
+
</div>
|
|
1041
|
+
|
|
1042
|
+
<!-- Resolved fonts actually used for rendering -->
|
|
1043
|
+
<div v-if="resolvedFamilyNames.length" class="fonts-detected fonts-resolved">
|
|
1044
|
+
<div class="fonts-detected-inner">
|
|
1045
|
+
<span class="fonts-detected-label">Rendering</span>
|
|
1046
|
+
<span v-for="f in resolvedFamilyNames" :key="f" class="fonts-chip fonts-chip--family">{{ f }}</span>
|
|
1047
|
+
</div>
|
|
1048
|
+
</div>
|
|
1049
|
+
|
|
1050
|
+
<!-- Font specimen list -->
|
|
1051
|
+
<div v-if="fontFiles.length" class="fonts-specimen">
|
|
1052
|
+
<template v-for="family in fontFamilyNames" :key="family">
|
|
1053
|
+
<button
|
|
1054
|
+
class="fonts-family-row"
|
|
1055
|
+
:class="{ active: fontOverride === family }"
|
|
1056
|
+
@click="applyFontOverride(family)"
|
|
1057
|
+
>
|
|
1058
|
+
<span class="fonts-family-name" :style="{ fontFamily: `'ogp-${family}', sans-serif` }">{{ family }}</span>
|
|
1059
|
+
<span class="fonts-family-meta">
|
|
1060
|
+
<span v-if="fontFamilySizes.get(family)" class="fonts-family-size">{{ formatBytes(fontFamilySizes.get(family)!) }}</span>
|
|
1061
|
+
<UIcon v-if="fontOverride === family" name="carbon:checkmark-filled" class="fonts-family-check" />
|
|
1062
|
+
</span>
|
|
1063
|
+
</button>
|
|
1064
|
+
<div class="fonts-variants">
|
|
1065
|
+
<div
|
|
1066
|
+
v-for="f in fontFiles.filter(ff => ff.family === family)"
|
|
1067
|
+
:key="f.key"
|
|
1068
|
+
class="fonts-variant"
|
|
1069
|
+
:class="{ loaded: f.loaded }"
|
|
1070
|
+
>
|
|
1071
|
+
<span class="fonts-variant-dot" />
|
|
1072
|
+
<span class="fonts-variant-label">{{ f.weight }}{{ f.style === 'italic' ? 'i' : '' }}</span>
|
|
1073
|
+
<span v-if="f.subsetCount > 1" class="fonts-variant-count">×{{ f.subsetCount }}</span>
|
|
1074
|
+
<template v-if="f.subsets.length">
|
|
1075
|
+
<span v-for="s in f.subsets" :key="s" class="fonts-subset-chip">{{ s }}</span>
|
|
1076
|
+
</template>
|
|
1077
|
+
</div>
|
|
1078
|
+
</div>
|
|
1079
|
+
</template>
|
|
1080
|
+
</div>
|
|
1081
|
+
|
|
1082
|
+
<div v-else class="fonts-empty">
|
|
1083
|
+
No fonts resolved. Install <code class="inline-code">@nuxt/fonts</code> to enable.
|
|
1084
|
+
</div>
|
|
1085
|
+
</div>
|
|
1086
|
+
|
|
1087
|
+
<!-- useSeoMeta snippet for meta tag overrides -->
|
|
1088
|
+
<DevtoolsSnippet
|
|
1089
|
+
v-if="protoTab === 'meta-tags' && hasMetaOverrides"
|
|
1090
|
+
label="useSeoMeta"
|
|
1091
|
+
:code="seoMetaSnippet"
|
|
1092
|
+
lang="js"
|
|
1093
|
+
/>
|
|
1094
|
+
|
|
1095
|
+
<UAlert
|
|
1096
|
+
v-else-if="hasMadeChanges && protoTab !== 'meta-tags'"
|
|
1097
|
+
color="warning"
|
|
1098
|
+
variant="subtle"
|
|
1099
|
+
icon="carbon:warning"
|
|
1100
|
+
title="Unsaved changes"
|
|
1101
|
+
description="These changes are for preview only and won't persist."
|
|
1102
|
+
class="mx-3 my-2"
|
|
1103
|
+
/>
|
|
1104
|
+
</DevtoolsPanel>
|
|
1105
|
+
</Transition>
|
|
1106
|
+
</div>
|
|
1107
|
+
</div>
|
|
1108
|
+
</template>
|
|
1109
|
+
|
|
1110
|
+
<style scoped>
|
|
1111
|
+
.preview-container {
|
|
1112
|
+
height: calc(100vh - 100px);
|
|
1113
|
+
min-height: 500px;
|
|
1114
|
+
overflow: hidden;
|
|
1115
|
+
display: flex;
|
|
1116
|
+
flex-direction: column;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
.preview-container:hover {
|
|
1120
|
+
border-color: var(--color-border);
|
|
1121
|
+
box-shadow: none;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
@media (max-height: 600px) {
|
|
1125
|
+
.preview-container {
|
|
1126
|
+
height: 100vh;
|
|
1127
|
+
min-height: 0;
|
|
1128
|
+
border-radius: 0;
|
|
1129
|
+
border-left: 0;
|
|
1130
|
+
border-right: 0;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
/* Preview source toggle */
|
|
1135
|
+
/* Component info */
|
|
1136
|
+
.component-info {
|
|
1137
|
+
position: absolute;
|
|
1138
|
+
left: 50%;
|
|
1139
|
+
top: 50%;
|
|
1140
|
+
transform: translate(-50%, -50%);
|
|
1141
|
+
display: flex;
|
|
1142
|
+
align-items: center;
|
|
1143
|
+
gap: 0.5rem;
|
|
1144
|
+
font-size: 0.8125rem;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
.component-path-link {
|
|
1148
|
+
font-size: 0.8125rem;
|
|
1149
|
+
font-family: var(--font-mono, ui-monospace, monospace);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
/* Size variant bar */
|
|
1153
|
+
.size-variant-bar {
|
|
1154
|
+
display: flex;
|
|
1155
|
+
align-items: center;
|
|
1156
|
+
gap: 0.5rem;
|
|
1157
|
+
padding: 0.375rem 0.75rem;
|
|
1158
|
+
border-bottom: 1px solid var(--color-border);
|
|
1159
|
+
background: var(--color-surface-elevated);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
@media (min-width: 640px) {
|
|
1163
|
+
.size-variant-bar {
|
|
1164
|
+
padding: 0.375rem 1rem;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
.size-variant-label {
|
|
1169
|
+
font-size: 0.6875rem;
|
|
1170
|
+
color: var(--color-text-subtle);
|
|
1171
|
+
letter-spacing: 0.02em;
|
|
1172
|
+
white-space: nowrap;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
.size-variant-group {
|
|
1176
|
+
display: flex;
|
|
1177
|
+
gap: 1px;
|
|
1178
|
+
background: var(--color-border);
|
|
1179
|
+
border-radius: 6px;
|
|
1180
|
+
overflow: hidden;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
.size-variant-btn {
|
|
1184
|
+
display: flex;
|
|
1185
|
+
align-items: center;
|
|
1186
|
+
gap: 0.25rem;
|
|
1187
|
+
padding: 0.25rem 0.5rem;
|
|
1188
|
+
font-size: 0.6875rem;
|
|
1189
|
+
line-height: 1;
|
|
1190
|
+
background: var(--color-surface);
|
|
1191
|
+
color: var(--color-text-muted);
|
|
1192
|
+
border: none;
|
|
1193
|
+
cursor: pointer;
|
|
1194
|
+
transition: background 120ms ease, color 120ms ease;
|
|
1195
|
+
white-space: nowrap;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
.size-variant-btn:hover {
|
|
1199
|
+
background: var(--color-surface-sunken);
|
|
1200
|
+
color: var(--color-text);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
.size-variant-btn.active {
|
|
1204
|
+
background: var(--color-surface-sunken);
|
|
1205
|
+
color: var(--color-text);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
.size-variant-name {
|
|
1209
|
+
font-weight: 500;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
.size-variant-px {
|
|
1213
|
+
font-family: var(--font-mono, ui-monospace, monospace);
|
|
1214
|
+
font-size: 0.625rem;
|
|
1215
|
+
opacity: 0.6;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
.size-variant-btn.active .size-variant-px {
|
|
1219
|
+
opacity: 0.8;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/* Preview area */
|
|
1223
|
+
.preview-area {
|
|
1224
|
+
flex: 1;
|
|
1225
|
+
display: flex;
|
|
1226
|
+
align-items: center;
|
|
1227
|
+
justify-content: center;
|
|
1228
|
+
padding: 1rem;
|
|
1229
|
+
overflow: auto;
|
|
1230
|
+
position: relative;
|
|
1231
|
+
transition: padding 200ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
@media (min-width: 640px) {
|
|
1235
|
+
.preview-area {
|
|
1236
|
+
padding: 1.5rem;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
.preview-area--panel-open {
|
|
1240
|
+
padding-right: 20rem;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
.preview-content {
|
|
1245
|
+
width: 100%;
|
|
1246
|
+
max-width: 42rem;
|
|
1247
|
+
margin: 0 auto;
|
|
1248
|
+
transition: max-width 200ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
.preview-content--no-transition {
|
|
1252
|
+
transition: none;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
@media (max-height: 600px) {
|
|
1256
|
+
.preview-area {
|
|
1257
|
+
padding: 0.5rem;
|
|
1258
|
+
overflow: hidden;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
.preview-content {
|
|
1262
|
+
max-width: 100%;
|
|
1263
|
+
/* Scale cards to fit available height — toolbar ~40px, tabs ~36px, padding 16px */
|
|
1264
|
+
max-height: calc(100vh - 160px);
|
|
1265
|
+
display: flex;
|
|
1266
|
+
flex-direction: column;
|
|
1267
|
+
justify-content: center;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
/* Constrain all card renderers to fit viewport height */
|
|
1271
|
+
.preview-content :deep(img) {
|
|
1272
|
+
max-height: calc(100vh - 280px);
|
|
1273
|
+
width: auto;
|
|
1274
|
+
object-fit: contain;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
.preview-content :deep(.discord-image),
|
|
1278
|
+
.preview-content :deep(.facebook-image),
|
|
1279
|
+
.preview-content :deep(.linkedin-image),
|
|
1280
|
+
.preview-content :deep(.bluesky-image),
|
|
1281
|
+
.preview-content :deep(.slack-image) {
|
|
1282
|
+
max-height: calc(100vh - 280px);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
.raw-preview-wrapper {
|
|
1287
|
+
position: relative;
|
|
1288
|
+
width: 100%;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
.raw-preview {
|
|
1292
|
+
width: 100%;
|
|
1293
|
+
border-radius: var(--radius-lg);
|
|
1294
|
+
overflow: hidden;
|
|
1295
|
+
box-shadow: 0 4px 24px oklch(0% 0 0 / 0.08);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
.raw-preview--dragging {
|
|
1299
|
+
user-select: none;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
.dark .raw-preview {
|
|
1303
|
+
box-shadow: 0 4px 24px oklch(0% 0 0 / 0.3);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/* Resize drag handle — right edge */
|
|
1307
|
+
.raw-resize-handle {
|
|
1308
|
+
position: absolute;
|
|
1309
|
+
top: 0;
|
|
1310
|
+
right: -14px;
|
|
1311
|
+
width: 28px;
|
|
1312
|
+
height: 100%;
|
|
1313
|
+
cursor: col-resize;
|
|
1314
|
+
display: flex;
|
|
1315
|
+
align-items: center;
|
|
1316
|
+
justify-content: center;
|
|
1317
|
+
z-index: 2;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
.raw-resize-grip {
|
|
1321
|
+
width: 4px;
|
|
1322
|
+
height: 32px;
|
|
1323
|
+
border-radius: 2px;
|
|
1324
|
+
background: var(--color-border);
|
|
1325
|
+
transition: background 150ms ease, height 150ms ease;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
.raw-resize-handle:hover .raw-resize-grip,
|
|
1329
|
+
.raw-preview--dragging ~ .raw-resize-handle .raw-resize-grip {
|
|
1330
|
+
background: var(--color-text-subtle);
|
|
1331
|
+
height: 48px;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
/* Width readout pill */
|
|
1335
|
+
.raw-resize-readout {
|
|
1336
|
+
position: absolute;
|
|
1337
|
+
bottom: -24px;
|
|
1338
|
+
left: 50%;
|
|
1339
|
+
transform: translateX(-50%);
|
|
1340
|
+
font-family: var(--font-mono, ui-monospace, monospace);
|
|
1341
|
+
font-size: 0.625rem;
|
|
1342
|
+
color: var(--color-text-subtle);
|
|
1343
|
+
white-space: nowrap;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
/* Status line */
|
|
1347
|
+
.status-line {
|
|
1348
|
+
margin-top: 1rem;
|
|
1349
|
+
text-align: center;
|
|
1350
|
+
font-size: 0.75rem;
|
|
1351
|
+
color: var(--color-text-subtle);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
@media (max-height: 600px) {
|
|
1355
|
+
.status-line {
|
|
1356
|
+
margin-top: 0.375rem;
|
|
1357
|
+
font-size: 0.6875rem;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
/* Props panel — positioning only, visuals from DevtoolsPanel */
|
|
1362
|
+
.props-panel {
|
|
1363
|
+
position: absolute;
|
|
1364
|
+
right: 0.75rem;
|
|
1365
|
+
top: 7rem;
|
|
1366
|
+
bottom: 0.75rem;
|
|
1367
|
+
width: 18rem;
|
|
1368
|
+
z-index: 10;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
@media (min-width: 640px) {
|
|
1372
|
+
.props-panel {
|
|
1373
|
+
right: 1rem;
|
|
1374
|
+
top: 8rem;
|
|
1375
|
+
bottom: 1rem;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
@media (max-height: 600px) {
|
|
1380
|
+
.props-panel {
|
|
1381
|
+
top: 5.5rem;
|
|
1382
|
+
bottom: 0.25rem;
|
|
1383
|
+
right: 0.25rem;
|
|
1384
|
+
border-radius: var(--radius-md);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
.props-field {
|
|
1389
|
+
padding: 0.375rem 0.75rem;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
.props-field-label {
|
|
1393
|
+
display: flex;
|
|
1394
|
+
align-items: center;
|
|
1395
|
+
gap: 0.375rem;
|
|
1396
|
+
font-size: 0.6875rem;
|
|
1397
|
+
font-weight: 500;
|
|
1398
|
+
color: var(--color-text-muted);
|
|
1399
|
+
margin-bottom: 0.25rem;
|
|
1400
|
+
text-transform: uppercase;
|
|
1401
|
+
letter-spacing: 0.03em;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
/* Fonts tab */
|
|
1405
|
+
.fonts-tab {
|
|
1406
|
+
display: flex;
|
|
1407
|
+
flex-direction: column;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
/* Detected requirements — compact summary bar */
|
|
1411
|
+
.fonts-detected {
|
|
1412
|
+
padding: 0.5rem 0.75rem;
|
|
1413
|
+
border-bottom: 1px solid var(--color-border-subtle);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
.fonts-detected-inner {
|
|
1417
|
+
display: flex;
|
|
1418
|
+
align-items: center;
|
|
1419
|
+
flex-wrap: wrap;
|
|
1420
|
+
gap: 0.25rem;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
.fonts-detected-label {
|
|
1424
|
+
font-family: var(--font-mono);
|
|
1425
|
+
font-size: 0.5625rem;
|
|
1426
|
+
font-weight: 600;
|
|
1427
|
+
text-transform: uppercase;
|
|
1428
|
+
letter-spacing: 0.05em;
|
|
1429
|
+
color: var(--color-text-subtle);
|
|
1430
|
+
margin-right: 0.125rem;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
.fonts-chip {
|
|
1434
|
+
display: inline-flex;
|
|
1435
|
+
align-items: center;
|
|
1436
|
+
gap: 0.1875rem;
|
|
1437
|
+
padding: 0.0625rem 0.3125rem;
|
|
1438
|
+
font-family: var(--font-mono);
|
|
1439
|
+
font-size: 0.625rem;
|
|
1440
|
+
line-height: 1.4;
|
|
1441
|
+
border-radius: 3px;
|
|
1442
|
+
background: var(--color-surface-sunken);
|
|
1443
|
+
border: 1px solid var(--color-border-subtle);
|
|
1444
|
+
color: var(--color-text-muted);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
.fonts-chip--family {
|
|
1448
|
+
color: var(--seo-green);
|
|
1449
|
+
border-color: oklch(65% 0.2 145 / 0.2);
|
|
1450
|
+
background: oklch(65% 0.2 145 / 0.06);
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
.dark .fonts-chip--family {
|
|
1454
|
+
background: oklch(65% 0.2 145 / 0.1);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
.fonts-chip--error {
|
|
1458
|
+
color: oklch(55% 0.2 25);
|
|
1459
|
+
border-color: oklch(55% 0.2 25 / 0.25);
|
|
1460
|
+
background: oklch(55% 0.2 25 / 0.08);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
.dark .fonts-chip--error {
|
|
1464
|
+
color: oklch(75% 0.15 25);
|
|
1465
|
+
background: oklch(55% 0.2 25 / 0.12);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
.fonts-resolved {
|
|
1469
|
+
border-bottom: none;
|
|
1470
|
+
padding-top: 0;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
.fonts-detected + .fonts-resolved {
|
|
1474
|
+
padding-top: 0;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
.fonts-detected-sep {
|
|
1478
|
+
width: 1px;
|
|
1479
|
+
height: 0.75rem;
|
|
1480
|
+
background: var(--color-border);
|
|
1481
|
+
margin: 0 0.125rem;
|
|
1482
|
+
flex-shrink: 0;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
/* Font specimen list */
|
|
1486
|
+
.fonts-specimen {
|
|
1487
|
+
display: flex;
|
|
1488
|
+
flex-direction: column;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
.fonts-family-row {
|
|
1492
|
+
display: flex;
|
|
1493
|
+
align-items: center;
|
|
1494
|
+
justify-content: space-between;
|
|
1495
|
+
width: 100%;
|
|
1496
|
+
padding: 0.375rem 0.75rem;
|
|
1497
|
+
background: transparent;
|
|
1498
|
+
border: none;
|
|
1499
|
+
cursor: pointer;
|
|
1500
|
+
transition: background 150ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
.fonts-family-row:hover {
|
|
1504
|
+
background: var(--color-surface-sunken);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
.fonts-family-row.active {
|
|
1508
|
+
background: oklch(65% 0.2 145 / 0.06);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
.dark .fonts-family-row.active {
|
|
1512
|
+
background: oklch(65% 0.2 145 / 0.08);
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
.fonts-family-name {
|
|
1516
|
+
font-size: 0.8125rem;
|
|
1517
|
+
font-weight: 500;
|
|
1518
|
+
color: var(--color-text);
|
|
1519
|
+
line-height: 1.3;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
.fonts-family-row.active .fonts-family-name {
|
|
1523
|
+
color: var(--seo-green);
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
.fonts-family-meta {
|
|
1527
|
+
display: flex;
|
|
1528
|
+
align-items: center;
|
|
1529
|
+
gap: 0.375rem;
|
|
1530
|
+
flex-shrink: 0;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
.fonts-family-size {
|
|
1534
|
+
font-family: var(--font-mono);
|
|
1535
|
+
font-size: 0.5625rem;
|
|
1536
|
+
color: var(--color-text-subtle);
|
|
1537
|
+
letter-spacing: 0.01em;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
.fonts-family-check {
|
|
1541
|
+
width: 0.875rem;
|
|
1542
|
+
height: 0.875rem;
|
|
1543
|
+
color: var(--seo-green);
|
|
1544
|
+
flex-shrink: 0;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
.fonts-variants {
|
|
1548
|
+
display: flex;
|
|
1549
|
+
flex-wrap: wrap;
|
|
1550
|
+
gap: 0.375rem 0.625rem;
|
|
1551
|
+
padding: 0.125rem 0.75rem 0.5rem 1rem;
|
|
1552
|
+
border-bottom: 1px solid var(--color-border-subtle);
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
.fonts-variant {
|
|
1556
|
+
display: flex;
|
|
1557
|
+
align-items: baseline;
|
|
1558
|
+
gap: 0.25rem;
|
|
1559
|
+
flex-wrap: wrap;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
.fonts-variant-dot {
|
|
1563
|
+
width: 5px;
|
|
1564
|
+
height: 5px;
|
|
1565
|
+
border-radius: 50%;
|
|
1566
|
+
flex-shrink: 0;
|
|
1567
|
+
background: var(--color-border);
|
|
1568
|
+
align-self: center;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
.fonts-variant.loaded .fonts-variant-dot {
|
|
1572
|
+
background: var(--seo-green);
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
.fonts-variant-label {
|
|
1576
|
+
font-family: var(--font-mono);
|
|
1577
|
+
font-size: 0.625rem;
|
|
1578
|
+
color: var(--color-text-subtle);
|
|
1579
|
+
line-height: 1;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
.fonts-variant.loaded .fonts-variant-label {
|
|
1583
|
+
color: var(--color-text-muted);
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
.fonts-variant-count {
|
|
1587
|
+
font-family: var(--font-mono);
|
|
1588
|
+
font-size: 0.5625rem;
|
|
1589
|
+
color: var(--color-text-subtle);
|
|
1590
|
+
opacity: 0.7;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
.fonts-subset-chip {
|
|
1594
|
+
font-family: var(--font-mono);
|
|
1595
|
+
font-size: 0.5rem;
|
|
1596
|
+
line-height: 1;
|
|
1597
|
+
padding: 0.0625rem 0.1875rem;
|
|
1598
|
+
border-radius: 2px;
|
|
1599
|
+
background: var(--color-surface-sunken);
|
|
1600
|
+
border: 1px solid var(--color-border-subtle);
|
|
1601
|
+
color: var(--color-text-subtle);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
.fonts-variant.loaded .fonts-subset-chip {
|
|
1605
|
+
color: var(--color-text-muted);
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
.fonts-empty {
|
|
1609
|
+
padding: 1rem 0.75rem;
|
|
1610
|
+
font-size: 0.75rem;
|
|
1611
|
+
color: var(--color-text-subtle);
|
|
1612
|
+
text-align: center;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
/* Prop editor */
|
|
1616
|
+
.prop-editor {
|
|
1617
|
+
display: flex;
|
|
1618
|
+
flex-direction: column;
|
|
1619
|
+
gap: 0.375rem;
|
|
1620
|
+
padding: 0.5rem 0.75rem;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
.prop-field {
|
|
1624
|
+
display: flex;
|
|
1625
|
+
flex-direction: column;
|
|
1626
|
+
gap: 0.125rem;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
.prop-label {
|
|
1630
|
+
font-family: var(--font-mono);
|
|
1631
|
+
font-size: 0.6875rem;
|
|
1632
|
+
color: var(--color-text-subtle);
|
|
1633
|
+
letter-spacing: 0.01em;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
.prop-input {
|
|
1637
|
+
font-family: var(--font-mono);
|
|
1638
|
+
font-size: 0.75rem;
|
|
1639
|
+
padding: 0.25rem 0.375rem;
|
|
1640
|
+
border-radius: var(--radius-sm);
|
|
1641
|
+
border: 1px solid var(--color-border-subtle);
|
|
1642
|
+
background: var(--color-surface-sunken);
|
|
1643
|
+
color: var(--color-text);
|
|
1644
|
+
outline: none;
|
|
1645
|
+
transition: border-color 150ms;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
.prop-input:focus {
|
|
1649
|
+
border-color: var(--seo-green);
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
.prop-input-color {
|
|
1653
|
+
width: 2.5rem;
|
|
1654
|
+
height: 1.5rem;
|
|
1655
|
+
padding: 0.125rem;
|
|
1656
|
+
cursor: pointer;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
.prop-input-json {
|
|
1660
|
+
resize: vertical;
|
|
1661
|
+
min-height: 2.5rem;
|
|
1662
|
+
line-height: 1.4;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
.prop-editor-empty {
|
|
1666
|
+
padding: 1rem 0.75rem;
|
|
1667
|
+
font-size: 0.75rem;
|
|
1668
|
+
color: var(--color-text-subtle);
|
|
1669
|
+
text-align: center;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
/* Inline code */
|
|
1673
|
+
.inline-code {
|
|
1674
|
+
padding: 0.125rem 0.375rem;
|
|
1675
|
+
border-radius: var(--radius-sm);
|
|
1676
|
+
background: var(--color-surface-sunken);
|
|
1677
|
+
border: 1px solid var(--color-border-subtle);
|
|
1678
|
+
font-family: var(--font-mono);
|
|
1679
|
+
font-size: 0.8125rem;
|
|
1680
|
+
color: var(--seo-green);
|
|
1681
|
+
}
|
|
1682
|
+
</style>
|