feat: add header components and version switcher for docs
This commit is contained in:
103
docs/components/AppHeader.vue
Normal file
103
docs/components/AppHeader.vue
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useDocusI18n } from '#imports'
|
||||||
|
|
||||||
|
const appConfig = useAppConfig()
|
||||||
|
const site = useSiteConfig()
|
||||||
|
|
||||||
|
const { localePath, isEnabled, locales } = useDocusI18n()
|
||||||
|
const { currentVersion, isOldVersion, loadVersions } = useVersions()
|
||||||
|
|
||||||
|
onMounted(() => loadVersions())
|
||||||
|
|
||||||
|
const links = computed(() => appConfig.github && appConfig.github.url
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
'icon': 'i-simple-icons-github',
|
||||||
|
'to': appConfig.github.url,
|
||||||
|
'target': '_blank',
|
||||||
|
'aria-label': 'GitHub',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="sticky top-0 z-50">
|
||||||
|
<!-- Version Warning Banner -->
|
||||||
|
<div
|
||||||
|
v-if="isOldVersion"
|
||||||
|
class="bg-amber-100 dark:bg-amber-900/50 text-amber-800 dark:text-amber-200 px-4 py-2 text-center text-sm border-b border-amber-200 dark:border-amber-800"
|
||||||
|
>
|
||||||
|
You are viewing documentation for Comments {{ currentVersion }}.
|
||||||
|
<a
|
||||||
|
href="/comments/"
|
||||||
|
class="underline font-medium hover:text-amber-900 dark:hover:text-amber-100"
|
||||||
|
>
|
||||||
|
View the latest version →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Original Docus Header -->
|
||||||
|
<UHeader
|
||||||
|
:ui="{ center: 'flex-1' }"
|
||||||
|
:to="localePath('/')"
|
||||||
|
:title="appConfig.header?.title || site.name"
|
||||||
|
>
|
||||||
|
<AppHeaderCenter />
|
||||||
|
|
||||||
|
<template #title>
|
||||||
|
<AppHeaderLogo class="h-6 w-auto shrink-0" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #right>
|
||||||
|
<AppVersionSwitcher />
|
||||||
|
<AppHeaderCTA />
|
||||||
|
|
||||||
|
<template v-if="isEnabled && locales.length > 1">
|
||||||
|
<ClientOnly>
|
||||||
|
<LanguageSelect />
|
||||||
|
|
||||||
|
<template #fallback>
|
||||||
|
<div class="h-8 w-8 animate-pulse bg-neutral-200 dark:bg-neutral-800 rounded-md" />
|
||||||
|
</template>
|
||||||
|
</ClientOnly>
|
||||||
|
|
||||||
|
<USeparator
|
||||||
|
orientation="vertical"
|
||||||
|
class="h-8"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<UContentSearchButton class="lg:hidden" />
|
||||||
|
|
||||||
|
<ClientOnly>
|
||||||
|
<UColorModeButton />
|
||||||
|
|
||||||
|
<template #fallback>
|
||||||
|
<div class="h-8 w-8 animate-pulse bg-neutral-200 dark:bg-neutral-800 rounded-md" />
|
||||||
|
</template>
|
||||||
|
</ClientOnly>
|
||||||
|
|
||||||
|
<template v-if="links?.length">
|
||||||
|
<UButton
|
||||||
|
v-for="(link, index) of links"
|
||||||
|
:key="index"
|
||||||
|
v-bind="{ color: 'neutral', variant: 'ghost', ...link }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #toggle="{ open, toggle }">
|
||||||
|
<IconMenuToggle
|
||||||
|
:open="open"
|
||||||
|
class="lg:hidden"
|
||||||
|
@click="toggle"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<AppHeaderBody />
|
||||||
|
</template>
|
||||||
|
</UHeader>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
16
docs/components/AppHeaderLogo.vue
Normal file
16
docs/components/AppHeaderLogo.vue
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const appConfig = useAppConfig()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UColorModeImage
|
||||||
|
v-if="appConfig.docus?.header?.logo?.dark || appConfig.docus?.header?.logo?.light"
|
||||||
|
:light="appConfig.docus?.header?.logo?.light || appConfig.docus?.header?.logo?.dark"
|
||||||
|
:dark="appConfig.docus?.header?.logo?.dark || appConfig.docus?.header?.logo?.light"
|
||||||
|
:alt="appConfig.docus?.header?.logo?.alt || appConfig.docus?.title"
|
||||||
|
class="h-8 w-auto shrink-0"
|
||||||
|
/>
|
||||||
|
<span v-else class="text-lg font-semibold">
|
||||||
|
{{ appConfig.docus?.title || 'Comments' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
38
docs/components/AppVersionSwitcher.vue
Normal file
38
docs/components/AppVersionSwitcher.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const { versions, currentVersion, currentTitle, loadVersions } = useVersions()
|
||||||
|
|
||||||
|
onMounted(() => loadVersions())
|
||||||
|
|
||||||
|
function switchVersion(version: { version: string; path: string }): void {
|
||||||
|
if (version.version !== currentVersion) {
|
||||||
|
window.location.href = version.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="versions.length > 1" class="relative" @click.stop>
|
||||||
|
<UPopover>
|
||||||
|
<UButton
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
:label="currentTitle"
|
||||||
|
trailing-icon="i-lucide-chevron-down"
|
||||||
|
/>
|
||||||
|
<template #content>
|
||||||
|
<div class="p-1">
|
||||||
|
<button
|
||||||
|
v-for="version in versions"
|
||||||
|
:key="version.version"
|
||||||
|
class="w-full px-3 py-2 text-left text-sm rounded hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center gap-2"
|
||||||
|
:class="{ 'font-medium text-primary': version.version === currentVersion }"
|
||||||
|
@click="switchVersion(version)"
|
||||||
|
>
|
||||||
|
{{ version.title }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UPopover>
|
||||||
|
</div>
|
||||||
|
<UBadge v-else-if="currentVersion" variant="subtle" color="neutral">{{ currentVersion }}</UBadge>
|
||||||
|
</template>
|
||||||
60
docs/composables/useVersions.ts
Normal file
60
docs/composables/useVersions.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
interface Version {
|
||||||
|
version: string
|
||||||
|
title: string
|
||||||
|
path: string
|
||||||
|
branch: string
|
||||||
|
isLatest: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const versions = ref<Version[]>([])
|
||||||
|
const isLoaded = ref(false)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
|
export function useVersions() {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const currentVersion = config.public.docsVersion || '1.x'
|
||||||
|
|
||||||
|
async function loadVersions() {
|
||||||
|
if (isLoaded.value || isLoading.value) return
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/comments/versions.json')
|
||||||
|
if (res.ok) {
|
||||||
|
versions.value = await res.json()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to load versions.json:', e)
|
||||||
|
} finally {
|
||||||
|
isLoaded.value = true
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestVersion = computed(() =>
|
||||||
|
versions.value.find(v => v.isLatest)
|
||||||
|
)
|
||||||
|
|
||||||
|
const currentVersionInfo = computed(() =>
|
||||||
|
versions.value.find(v => v.version === currentVersion)
|
||||||
|
)
|
||||||
|
|
||||||
|
const isOldVersion = computed(() => {
|
||||||
|
if (!isLoaded.value) return false
|
||||||
|
return currentVersionInfo.value?.isLatest === false
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentTitle = computed(() =>
|
||||||
|
currentVersionInfo.value?.title || currentVersion
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
versions,
|
||||||
|
currentVersion,
|
||||||
|
currentTitle,
|
||||||
|
latestVersion,
|
||||||
|
isOldVersion,
|
||||||
|
isLoaded,
|
||||||
|
loadVersions,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user