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