mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-17 21:48:07 +01:00
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:
33
docs/components/docs/DocsAside.vue
Normal file
33
docs/components/docs/DocsAside.vue
Normal 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>
|
||||
31
docs/components/docs/DocsAsideLinks.vue
Normal file
31
docs/components/docs/DocsAsideLinks.vue
Normal 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>
|
||||
37
docs/components/docs/DocsPageHeader.vue
Normal file
37
docs/components/docs/DocsPageHeader.vue
Normal 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>
|
||||
11
docs/components/docs/DocsPrevNext.vue
Normal file
11
docs/components/docs/DocsPrevNext.vue
Normal 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> </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>
|
||||
172
docs/components/docs/DocsSearch.vue
Normal file
172
docs/components/docs/DocsSearch.vue
Normal 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>
|
||||
19
docs/components/docs/DocsToc.vue
Normal file
19
docs/components/docs/DocsToc.vue
Normal 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>
|
||||
49
docs/components/docs/DocsTocLinks.vue
Normal file
49
docs/components/docs/DocsTocLinks.vue
Normal 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>
|
||||
Reference in New Issue
Block a user