feat: rewrite to use app config and rework docs (#143)

Co-authored-by: Daniel Roe <daniel@roe.dev>
Co-authored-by: Sébastien Chopin <seb@nuxt.com>
This commit is contained in:
Benjamin Canac
2023-05-04 14:49:19 +02:00
committed by GitHub
parent 56230ea915
commit 6da0db0113
144 changed files with 10470 additions and 8109 deletions

View File

@@ -0,0 +1,33 @@
<template>
<aside
class="hidden pb-8 overflow-y-auto lg:block lg:self-start lg:top-16 lg:max-h-[calc(100vh-64px)] lg:sticky lg:pr-8 lg:pl-[2px]"
>
<div class="relative">
<div class="sticky top-0 pointer-events-none">
<div class="h-8 bg-white dark:bg-gray-900" />
<div class="bg-white dark:bg-gray-900 relative pointer-events-auto">
<UButton
icon="i-heroicons-magnifying-glass-20-solid"
class="w-full"
color="gray"
@click="isSearchModalOpen = true"
>
Search
<div class="hidden lg:flex items-center gap-1 ml-auto -my-1">
<Shortcut value="meta" />
<Shortcut value="K" />
</div>
</UButton>
</div>
<div class="h-8 bg-gradient-to-b from-white dark:from-gray-900" />
</div>
<DocsAsideLinks />
</div>
</aside>
</template>
<script setup lang="ts">
const { isSearchModalOpen } = useDocs()
</script>

View File

@@ -0,0 +1,31 @@
<template>
<div class="space-y-8">
<div v-for="(group, index) in navigation" :key="index" class="space-y-3">
<div class="text-sm font-semibold text-gray-900 dark:text-gray-200">
<span class="truncate">{{ group.title }}</span>
</div>
<UVerticalNavigation
:links="mapContentLinks(group.children)"
class="mt-1"
:ui="{
wrapper: 'border-l border-gray-200 dark:border-gray-800 space-y-2',
spacing: 'pl-4',
base: 'group text-sm block border-l -ml-px lg:leading-6',
active: 'text-primary-500 dark:text-primary-400 border-current font-semibold',
inactive: 'border-transparent hover:border-gray-400 dark:hover:border-gray-500 text-gray-700 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300'
}"
/>
</div>
</div>
</template>
<script setup lang="ts">
import type { NavItem } from '@nuxt/content/dist/runtime/types'
const { navigation } = useContent() as { navigation: NavItem[] }
function mapContentLinks (links: NavItem[]) {
return links?.map(link => ({ label: link.title, icon: link.icon, to: link._path })) || []
}
</script>

View File

@@ -0,0 +1,37 @@
<template>
<header v-if="page" class="relative border-b border-gray-200 dark:border-gray-800 pb-8 mb-12">
<p class="mb-4 text-sm leading-6 font-semibold text-primary-500 dark:text-primary-400 capitalize">
{{ useLowerCase(page._dir) }}
</p>
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between">
<h1 class="text-3xl sm:text-4xl font-extrabold text-gray-900 tracking-tight dark:text-white">
{{ page.title }}
</h1>
<div class="flex items-center gap-2 mt-4 lg:mt-0">
<UButton
v-if="page.headlessui"
:label="page.headlessui.label"
:to="page.headlessui.to"
icon="i-simple-icons-headlessui"
color="white"
/>
<UButton
v-if="page.github"
label="GitHub"
icon="i-simple-icons-github"
color="white"
:to="`https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/${page._dir}/U${page.title}.vue`"
/>
</div>
</div>
<p v-if="page.description" class="mt-4 text-lg">
{{ page.description }}
</p>
</header>
</template>
<script setup lang="ts">
const { page } = useContent()
</script>

View File

@@ -0,0 +1,11 @@
<template>
<div class="flex items-center justify-between">
<UButton v-if="prev" :label="prev.navigation?.title || prev.title" :to="prev._path" icon="i-heroicons-arrow-small-left-20-solid" color="white" />
<span v-else>&nbsp;</span>
<UButton v-if="next" :label="next.navigation?.title || next.title" :to="next._path" trailing-icon="i-heroicons-arrow-small-right-20-solid" color="white" />
</div>
</template>
<script setup lang="ts">
const { prev, next } = useContent()
</script>

View File

@@ -0,0 +1,172 @@
<template>
<UModal
v-model="isSearchModalOpen"
:ui="{
spacing: 'sm:p-4',
rounded: 'sm:rounded-lg',
width: 'sm:max-w-3xl',
height: 'h-screen sm:h-[28rem]'
}"
>
<UCommandPalette
ref="commandPaletteRef"
:groups="groups"
command-attribute="title"
:fuse="{
fuseOptions: { ignoreLocation: true, includeMatches: true, minMatchCharLength: 2, threshold: 0, keys: ['title', 'description', 'children.children.value', 'children.children.children.value'] },
resultLimit: 10
}"
@update:model-value="onSelect"
@close="isSearchModalOpen = false"
/>
</UModal>
</template>
<script setup lang="ts">
import type { Command } from '../../../src/runtime/types'
const { navigation } = useContent()
const router = useRouter()
const { usingInput } = useShortcuts()
const { isSearchModalOpen } = useDocs()
const commandPaletteRef = ref<HTMLElement & { query: Ref<string>, results: { item: Command }[] }>()
const { data: files } = await useLazyAsyncData('search', () => queryContent().where({ _type: 'markdown' }).find(), { default: () => [] })
// Computed
const defaultGroups = computed(() => navigation.value.map(item => ({
key: item._path,
label: item.title,
commands: files.value.filter(file => file._path.startsWith(item._path)).map(file => ({
id: file._id,
title: file.navigation?.title || file.title,
to: file._path,
suffix: file.description,
icon: file.icon
}))
})))
const queryGroups = computed(() => navigation.value.map(item => ({
key: item._path,
label: item.title,
commands: files.value.filter(file => file._path.startsWith(item._path)).flatMap((file) => {
return [{
id: file._id,
title: file.navigation?.title || file.title,
to: file._path,
description: file.description,
icon: file.icon
},
// @ts-ignore
...Object.entries(groupByHeading(file.body.children)).map(([hash, { title, children }]) => ({
id: `${file._path}${hash}`,
title,
prefix: `${file.navigation?.title || file.title} ->`,
prefixClass: 'text-gray-700 dark:text-gray-200',
to: `${file._path}${hash}`,
children: concatChildren(children),
icon: file.icon
}))]
})
})))
const groups = computed(() => commandPaletteRef.value?.query ? queryGroups.value : defaultGroups.value)
// avoid conflicts between multiple meta_k shortcuts
const canToggleModal = computed(() => isSearchModalOpen.value || !usingInput.value)
// Methods
function remapChildren (children: any[]) {
return children?.map((grandChild) => {
if (['code-inline', 'em', 'a', 'strong'].includes(grandChild.tag)) {
return { type: 'text', value: grandChild.children.find(child => child.type === 'text')?.value || '' }
}
return grandChild
})
}
function concatChildren (children: any[]) {
return children.map((child) => {
if (['alert'].includes(child.tag)) {
child.children = concatChildren(child.children)
}
if (child.tag === 'p') {
child.children = remapChildren(child.children)
child.children = child.children?.reduce((acc, grandChild) => {
if (grandChild.type === 'text') {
if (acc.length && acc[acc.length - 1].type === 'text') {
acc[acc.length - 1].value += grandChild.value
} else {
acc.push(grandChild)
}
} else {
acc.push(grandChild)
}
return acc
}, [])
}
if (['style'].includes(child.tag)) {
return null
}
return child
})
}
function groupByHeading (children: any[]) {
const groups = {} // grouped by path
let hash = '' // file.page with potential `#anchor` concat
let title: string | null
for (const node of children) {
// if heading found, udpate current path
if (['h2', 'h3'].includes(node.tag)) {
// find heading text value
title = node.children?.find(child => child.type === 'text')?.value
if (title) {
hash = `#${node.props.id}`
}
}
// push to existing/new group based on path
if (groups[hash]) {
groups[hash].children.push(node)
} else {
groups[hash] = { children: [node], title }
}
}
return groups
}
function onSelect (option) {
isSearchModalOpen.value = false
if (option.click) {
option.click()
} else if (option.to) {
router.push(option.to)
} else if (option.href) {
window.open(option.href, '_blank')
}
}
// Shortcuts
defineShortcuts({
meta_k: {
usingInput: true,
whenever: [canToggleModal],
handler: () => {
isSearchModalOpen.value = !isSearchModalOpen.value
}
},
escape: {
usingInput: true,
whenever: [isSearchModalOpen],
handler: () => { isSearchModalOpen.value = false }
}
})
</script>

View File

@@ -0,0 +1,19 @@
<template>
<div v-if="toc" class="sticky top-16 bg-white/75 dark:bg-gray-900/75 backdrop-blur group lg:self-start -mx-4 sm:-mx-6 lg:mx-0 px-4 sm:px-6 lg:pl-8 lg:pr-0">
<div class="py-3 lg:py-8 border-b border-dashed border-gray-200 dark:border-gray-800 lg:border-0">
<button class="flex items-center gap-2" tabindex="-1" @click="isTocOpen = !isTocOpen">
<span class="text-sm text-slate-900 font-semibold text-sm leading-6 dark:text-slate-100 truncate">Table of Contents</span>
<UIcon name="i-heroicons-chevron-right-20-solid" class="lg:hidden w-4 h-4 transition-transform duration-100 transform text-gray-400 dark:text-gray-500" :class="[isTocOpen ? 'rotate-90' : 'rotate-0']" />
</button>
<DocsTocLinks class="mt-2 lg:mt-4" :links="toc.links" :class="[isTocOpen ? 'lg:block' : 'hidden lg:block']" />
</div>
</div>
</template>
<script setup lang="ts">
const { toc } = useContent()
const isTocOpen = ref(false)
</script>

View File

@@ -0,0 +1,49 @@
<template>
<ul>
<li v-for="link in links" :key="link.text" :class="{ 'ml-3': link.depth === 3 }">
<a
:href="`#${link.id}`"
class="block py-1 font-medium text-sm"
:class="[activeHeadings.includes(link.id) ? 'text-primary-500 dark:text-primary-400' : 'hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300']"
@click.prevent="scrollToHeading(link.id)"
>
{{ link.text }}
</a>
<DocsTocLinks v-if="link.children" :links="link.children" />
</li>
</ul>
</template>
<script setup lang="ts">
import type { TocLink } from '@nuxt/content/dist/runtime/types'
defineProps({
links: {
type: Array as PropType<TocLink[]>,
default: () => []
}
})
const emit = defineEmits(['move'])
const route = useRoute()
const router = useRouter()
const { activeHeadings, updateHeadings } = useScrollspy()
watch(() => route.path, () => {
setTimeout(() => {
if (process.client) {
updateHeadings([
...document.querySelectorAll('h2'),
...document.querySelectorAll('h3')
])
}
}, 300)
}, { immediate: true })
const scrollToHeading = (id: string) => {
router.push(`#${id}`)
emit('move', id)
}
</script>