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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions layer/app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,21 @@ import * as nuxtUiLocales from '@nuxt/ui/locale'
import { transformNavigation } from './utils/navigation'
import { useDocusColorMode } from './composables/useDocusColorMode'
import { useSubNavigation } from './composables/useSubNavigation'
import { useVersion } from './composables/useVersion'
import { useCollectionName } from './composables/useCollectionName'

const appConfig = useAppConfig()
const { seo } = appConfig
const { forced: forcedColorMode } = useDocusColorMode()
const site = useSiteConfig()
const { locale, locales, isEnabled, switchLocalePath } = useDocusI18n()
const { isEnabled: isAssistantEnabled, panelWidth: assistantPanelWidth, shouldPushContent } = useAssistant()
const { version, isVersioned } = useVersion()

const nuxtUiLocale = computed(() => nuxtUiLocales[locale.value as keyof typeof nuxtUiLocales] || nuxtUiLocales.en)
const lang = computed(() => nuxtUiLocale.value.code)
const dir = computed(() => nuxtUiLocale.value.dir)
const collectionName = computed(() => isEnabled.value ? `docs_${locale.value}` : 'docs')
const collectionName = useCollectionName('docs')

useHead({
meta: [
Expand Down Expand Up @@ -50,12 +53,12 @@ if (isEnabled.value) {
}

const { data: navigation } = await useAsyncData(() => `navigation_${collectionName.value}`, () => queryCollectionNavigation(collectionName.value as keyof PageCollections), {
transform: (data: ContentNavigationItem[]) => transformNavigation(data, isEnabled.value, locale.value),
watch: [locale],
transform: (data: ContentNavigationItem[]) => transformNavigation(data, isEnabled.value, locale.value, isVersioned.value, version.value),
watch: [locale, version],
})
const { data: files } = useLazyAsyncData(`search_${collectionName.value}`, () => queryCollectionSearchSections(collectionName.value as keyof PageCollections), {
server: false,
watch: [locale],
watch: [locale, version],
})

provide('navigation', navigation)
Expand Down
56 changes: 56 additions & 0 deletions layer/app/components/VersionSelect.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<script setup lang="ts">
import { useVersion } from '../composables/useVersion'

const { version, versions, switchVersion } = useVersion()

const currentVersion = computed(() => versions.find(v => v.value === version.value))

const items = computed(() => versions.map(v => ({
label: v.label,
icon: v.value === version.value ? 'i-lucide-check' : undefined,
active: v.value === version.value,
slot: 'version' as const,
tag: v.tag,
onSelect: () => switchVersion(v.value),
})))
</script>

<template>
<UDropdownMenu
:items="items"
:content="{ align: 'start' }"
:ui="{
content: 'w-(--reka-dropdown-menu-trigger-width)',
}"
>
<button class="flex w-full items-center justify-between gap-2 rounded-md px-2.5 py-2 text-sm hover:bg-elevated transition-colors cursor-pointer">
<div class="flex items-center gap-2 min-w-0">
<UIcon
name="i-lucide-book-open"
class="size-4 shrink-0 text-primary"
/>
<span class="font-medium text-highlighted truncate">{{ currentVersion?.label || version }}</span>
<UBadge
v-if="currentVersion?.tag"
:label="currentVersion.tag"
variant="subtle"
size="sm"
/>
</div>
<UIcon
name="i-lucide-chevrons-up-down"
class="size-4 shrink-0 text-muted"
/>
</button>

<template #version="{ item }">
<span class="truncate">{{ item.label }}</span>
<UBadge
v-if="item.tag"
:label="item.tag"
variant="subtle"
size="sm"
/>
</template>
</UDropdownMenu>
</template>
17 changes: 17 additions & 0 deletions layer/app/composables/useCollectionName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { computed } from 'vue'

export const useCollectionName = (type: 'docs' | 'landing' = 'docs') => {
const { version, isVersioned } = useVersion()
const { locale, isEnabled: isI18n } = useDocusI18n()

return computed(() => {
let name: string = type
if (type === 'docs' && isVersioned.value && version.value) {
name += `_${version.value}`
}
if (isI18n.value) {
name += `_${locale.value}`
}
return name
})
}
105 changes: 105 additions & 0 deletions layer/app/composables/useVersion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { useRuntimeConfig, useCookie, useRoute, navigateTo } from '#imports'
import { ref, computed } from 'vue'

export interface VersionItem {
label: string
value: string
tag?: string
}

export interface VersionsRuntimeConfig {
strategy: 'prefix' | 'state'
default: string
items: VersionItem[]
}

export const useVersion = () => {
const config = useRuntimeConfig().public
const versionsConfig = (config.docus as { versions?: VersionsRuntimeConfig } | undefined)?.versions
const isVersioned = ref(!!versionsConfig?.items?.length)

if (!isVersioned.value || !versionsConfig) {
return {
isVersioned,
version: ref(''),
versions: [] as VersionItem[],
switchVersion: (_v: string) => {},
versionStrategy: 'prefix' as const,
}
}

const items = versionsConfig.items
const strategy = versionsConfig.strategy || 'prefix'
const defaultVersion = versionsConfig.default || items[0]!.value

const resolveVersionFromRoute = (): string => {
if (strategy !== 'prefix') return defaultVersion
const route = useRoute()
const segments = route.path.split('/').filter(Boolean)

const { isEnabled: isI18n } = useDocusI18n()
const startIdx = isI18n.value ? 1 : 0

const candidate = segments[startIdx]
if (candidate && items.some(v => v.value === candidate)) {
return candidate
}
return defaultVersion
}

const versionCookie = strategy === 'state'
? useCookie<string>('docus-version', { default: () => defaultVersion })
: null

const version = computed({
get: () => {
if (strategy === 'state') {
return versionCookie!.value || defaultVersion
}
return resolveVersionFromRoute()
},
set: (v: string) => {
if (strategy === 'state' && versionCookie) {
versionCookie.value = v
}
},
})

const switchVersion = (targetVersion: string) => {
if (!items.some(v => v.value === targetVersion)) return

if (strategy === 'state') {
version.value = targetVersion
reloadNuxtApp()
return
}

const route = useRoute()
const currentVersion = version.value
const currentPath = route.path

let newPath: string
if (currentVersion && currentPath.includes(`/${currentVersion}`)) {
newPath = currentPath.replace(`/${currentVersion}`, `/${targetVersion}`)
}
else {
const { isEnabled: isI18n, locale } = useDocusI18n()
if (isI18n.value) {
newPath = currentPath.replace(`/${locale.value}`, `/${locale.value}/${targetVersion}`)
}
else {
newPath = `/${targetVersion}${currentPath}`
}
}

navigateTo(newPath)
}

return {
isVersioned,
version,
versions: items,
switchVersion,
versionStrategy: strategy,
}
}
9 changes: 6 additions & 3 deletions layer/app/error.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import type { ContentNavigationItem, PageCollections } from '@nuxt/content'
import * as nuxtUiLocales from '@nuxt/ui/locale'
import { transformNavigation } from './utils/navigation'
import { useDocusColorMode } from './composables/useDocusColorMode'
import { useVersion } from './composables/useVersion'
import { useCollectionName } from './composables/useCollectionName'

const props = defineProps<{
error: NuxtError
}>()

const { forced: forcedColorMode } = useDocusColorMode()
const { locale, locales, isEnabled, t, switchLocalePath } = useDocusI18n()
const { version, isVersioned } = useVersion()

const nuxtUiLocale = computed(() => nuxtUiLocales[locale.value as keyof typeof nuxtUiLocales] || nuxtUiLocales.en)
const lang = computed(() => nuxtUiLocale.value.code)
Expand Down Expand Up @@ -47,11 +50,11 @@ if (isEnabled.value) {
})
}

const collectionName = computed(() => isEnabled.value ? `docs_${locale.value}` : 'docs')
const collectionName = useCollectionName('docs')

const { data: navigation } = await useAsyncData(`navigation_${collectionName.value}`, () => queryCollectionNavigation(collectionName.value as keyof PageCollections), {
transform: (data: ContentNavigationItem[]) => transformNavigation(data, isEnabled.value, locale.value),
watch: [locale],
transform: (data: ContentNavigationItem[]) => transformNavigation(data, isEnabled.value, locale.value, isVersioned.value, version.value),
watch: [locale, version],
})
const { data: files } = useLazyAsyncData(`search_${collectionName.value}`, () => queryCollectionSearchSections(collectionName.value as keyof PageCollections), {
server: false,
Expand Down
13 changes: 13 additions & 0 deletions layer/app/layouts/docs.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
<script setup lang="ts">
import { useVersion } from '../composables/useVersion'

const { isVersioned, versions } = useVersion()
</script>

<template>
<UMain>
<UContainer>
<UPage>
<template #left>
<UPageAside>
<template v-if="isVersioned && versions.length > 1">
<VersionSelect />
<USeparator
type="dashed"
class="my-3"
/>
</template>
<DocsAsideLeftTop />
<DocsAsideLeftBody />
</UPageAside>
Expand Down
5 changes: 3 additions & 2 deletions layer/app/pages/[[lang]]/[...slug].vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@
import { kebabCase } from 'scule'
import type { ContentNavigationItem, Collections, DocsCollectionItem } from '@nuxt/content'
import { findPageHeadline } from '@nuxt/content/utils'
import { useCollectionName } from '../../composables/useCollectionName'

definePageMeta({
layout: 'docs',
})

const route = useRoute()
const { locale, isEnabled, t } = useDocusI18n()
const { t } = useDocusI18n()
const appConfig = useAppConfig()
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
const { shouldPushContent: shouldHideToc } = useAssistant()

const collectionName = computed(() => isEnabled.value ? `docs_${locale.value}` : 'docs')
const collectionName = useCollectionName('docs')

const [{ data: page }, { data: surround }] = await Promise.all([
useAsyncData(kebabCase(route.path), () => queryCollection(collectionName.value as keyof Collections).path(route.path).first() as Promise<DocsCollectionItem>),
Expand Down
5 changes: 2 additions & 3 deletions layer/app/templates/landing.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
<script setup lang="ts">
import type { Collections } from '@nuxt/content'
import { useCollectionName } from '../composables/useCollectionName'

const route = useRoute()
const { locale, isEnabled } = useDocusI18n()

// Dynamic collection name based on i18n status
const collectionName = computed(() => isEnabled.value ? `landing_${locale.value}` : 'landing')
const collectionName = useCollectionName('landing')

const { data: page } = await useAsyncData(collectionName.value, () => queryCollection(collectionName.value as keyof Collections).path(route.path).first())
if (!page.value) {
Expand Down
24 changes: 17 additions & 7 deletions layer/app/utils/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,32 @@ export const flattenNavigation = (items?: ContentNavigationItem[]): ContentNavig
) || []

/**
* Transform navigation data by stripping locale and docs levels
* Transform navigation data by stripping locale, version, and docs levels
*/
export function transformNavigation(
data: ContentNavigationItem[],
isI18nEnabled: boolean,
locale?: string,
isVersioned?: boolean,
version?: string,
): ContentNavigationItem[] {
let result = data

if (isI18nEnabled && locale) {
// i18n: first strip locale level, then check for docs level
const localeResult = data.find(item => item.path === `/${locale}`)?.children || data
return localeResult.find(item => item.path === `/${locale}/docs`)?.children || localeResult
result = result.find(item => item.path === `/${locale}`)?.children || result
}
else {
// non-i18n: strip docs level if exists
return data.find(item => item.path === '/docs')?.children || data

if (isVersioned && version) {
result = result.find(item => item.path.endsWith(`/${version}`))?.children || result
}

const docsPrefix = isI18nEnabled && locale
? (isVersioned && version ? `/${locale}/${version}/docs` : `/${locale}/docs`)
: (isVersioned && version ? `/${version}/docs` : '/docs')

result = result.find(item => item.path === docsPrefix)?.children || result

return result
}

export interface BreadcrumbItem {
Expand Down
Loading
Loading