aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/docs/components
diff options
context:
space:
mode:
Diffstat (limited to 'docs/components')
-rw-r--r--docs/components/app/AppFooter.vue163
-rw-r--r--docs/components/app/AppHeader.vue112
-rw-r--r--docs/components/app/AppHeaderDialog.vue117
-rw-r--r--docs/components/app/AppHeaderLogo.vue63
-rw-r--r--docs/components/app/AppHeaderNavigation.vue94
-rw-r--r--docs/components/app/AppLayout.vue38
-rw-r--r--docs/components/app/AppLoadingBar.vue106
-rw-r--r--docs/components/app/AppSearch.vue76
-rw-r--r--docs/components/app/AppSocialIcons.vue58
-rw-r--r--docs/components/app/DocumentDrivenNotFound.vue119
-rw-r--r--docs/components/app/Ellipsis.vue66
-rw-r--r--docs/components/app/Logo.vue64
-rw-r--r--docs/components/app/ThemeSelect.vue41
-rw-r--r--docs/components/docs/DocsAside.vue36
-rw-r--r--docs/components/docs/DocsAsideTree.vue212
-rw-r--r--docs/components/docs/DocsPageBottom.vue46
-rw-r--r--docs/components/docs/DocsPageLayout.vue281
-rw-r--r--docs/components/docs/DocsPrevNext.vue140
-rw-r--r--docs/components/docs/DocsToc.vue36
-rw-r--r--docs/components/docs/DocsTocLinks.vue87
-rw-r--r--docs/components/docs/EditOnLink.vue166
-rw-r--r--docs/components/docs/SourceLink.vue21
22 files changed, 2142 insertions, 0 deletions
diff --git a/docs/components/app/AppFooter.vue b/docs/components/app/AppFooter.vue
new file mode 100644
index 0000000..84266eb
--- /dev/null
+++ b/docs/components/app/AppFooter.vue
@@ -0,0 +1,163 @@
+<script setup lang="ts">
+const { config } = useDocus()
+const socialIcons = ref(null)
+const icons = computed(() => config.value?.footer?.iconLinks || [])
+const textLinks = computed(() => config.value?.footer?.textLinks || [])
+const socialIconsCount = computed(() => Object.entries(config.value?.socials || {}).filter(([_, v]) => v).length)
+const nbSocialIcons = computed(() => (socialIcons.value ? socialIconsCount.value : 0))
+</script>
+
+<template>
+ <footer>
+ <Container :fluid="config?.footer?.fluid" padded class="footer-container">
+ <!-- Left -->
+ <div class="left">
+ <a v-if="config?.footer?.credits" :href="config?.footer?.credits?.href || '#'" rel="noopener" target="_blank">
+ <Component :is="config?.footer?.credits?.icon" v-if="config?.footer?.credits?.icon" class="left-icon" />
+ <p v-if="config?.footer?.credits?.text">{{ config.footer.credits.text }}</p>
+ </a>
+ </div>
+
+ <!-- Center -->
+ <div class="center">
+ <NuxtLink
+ v-for="link in textLinks"
+ :key="link.href"
+ class="text-link"
+ :aria-label="link.text"
+ :href="link.href"
+ :target="link.target || '_self'"
+ >
+ {{ link.text }}
+ </NuxtLink>
+ </div>
+
+ <div class="right">
+ <a
+ v-for="icon in icons.slice(0, 6 - nbSocialIcons)"
+ :key="icon.label"
+ class="icon-link"
+ rel="noopener"
+ :aria-label="icon.label"
+ :href="icon.href"
+ target="_blank"
+ >
+ <Icon :name="icon.icon" />
+ </a>
+ <AppSocialIcons ref="socialIcons" />
+ </div>
+ </Container>
+ </footer>
+</template>
+
+<style lang="ts" scoped>
+css({
+ footer: {
+ display: 'flex',
+ minHeight: '{docus.footer.height}',
+ borderTopWidth: '1px',
+ borderTopStyle: 'solid',
+ borderTopColor: '{elements.border.primary.static}',
+ padding: '{docus.footer.padding}',
+
+ '.footer-container': {
+ display: 'grid',
+ gridTemplateColumns: 'repeat(12, minmax(0, 1fr))',
+ justifyItems: 'center',
+ gap: '{space.2}',
+ '@sm': {
+ justifyItems: 'legacy',
+
+ },
+
+ ':deep(.icon)': {
+ width: '{space.4}',
+ height: '{space.4}'
+ },
+
+ a: {
+ color: '{color.gray.500}',
+ '@dark': {
+ color: '{color.gray.400}'
+ },
+ '&:hover': {
+ color: '{color.gray.700}',
+ '@dark': {
+ color: '{color.gray.200}',
+ }
+ },
+ },
+
+ '.left': {
+ gridColumn: 'span 12 / span 12',
+ display: 'flex',
+ py: '{space.4}',
+ order: 1,
+
+ '@sm': {
+ gridColumn: 'span 3 / span 3',
+ order: 0,
+ },
+
+ a: {
+ display: 'flex',
+ alignItems: 'center',
+ },
+
+ p: {
+ fontSize: '{text.xs.fontSize}',
+ lineHeight: '{text.xs.lineHeight}',
+ fontWeight: '{fontWeight.medium}'
+ },
+
+ '&-icon': {
+ flexShrink: 0,
+ width: '{space.4}',
+ height: '{space.4}',
+ fill: 'currentcolor',
+ marginRight: '{space.2}',
+ },
+ },
+
+ '.center': {
+ gridColumn: 'span 12 / span 12',
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+
+ '@sm': {
+ gridColumn: 'span 6 / span 6',
+ flexDirection: 'row',
+ justifyContent: 'center',
+ },
+
+ '.text-link': {
+ padding: '{space.2}',
+ fontSize: '{text.sm.fontSize}',
+ lineHeight: '{text.sm.lineHeight}',
+ fontWeight: '{fontWeight.medium}'
+ }
+
+ },
+
+ '.right': {
+ gridColumn: 'span 12 / span 12',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'flex-end',
+ // marginLeft: 'calc(0px - {space.4})',
+
+ '@sm': {
+ gridColumn: 'span 3 / span 3',
+ marginRight: 'calc(0px - {space.4})',
+ },
+
+ '.icon-link': {
+ display: 'flex',
+ padding: '{space.4}'
+ }
+ },
+ },
+ }
+})
+</style>
diff --git a/docs/components/app/AppHeader.vue b/docs/components/app/AppHeader.vue
new file mode 100644
index 0000000..c5a3fd8
--- /dev/null
+++ b/docs/components/app/AppHeader.vue
@@ -0,0 +1,112 @@
+<script setup lang="ts">
+const { config } = useDocus()
+const { navigation } = useContent()
+const { hasDocSearch } = useDocSearch()
+
+const hasDialog = computed(() => navigation.value?.length > 1 || navigation.value?.[0]?.children?.length)
+
+defineProps({
+ ...variants
+})
+</script>
+
+<template>
+ <header :class="{ 'has-dialog': hasDialog, 'has-doc-search': hasDocSearch }">
+ <Container :fluid="config?.header?.fluid ">
+ <div class="section left">
+ <AppHeaderDialog v-if="hasDialog" />
+ <AppHeaderLogo />
+ </div>
+
+ <div class="section center">
+ <AppHeaderLogo v-if="hasDialog" />
+ <AppHeaderNavigation />
+ </div>
+
+ <div class="section right">
+ <AppSearch v-if="hasDocSearch" />
+ <ThemeSelect />
+ <div class="social-icons">
+ <AppSocialIcons />
+ </div>
+ </div>
+ </Container>
+ </header>
+</template>
+
+<style scoped lang="ts">
+css({
+ ':deep(.icon)': {
+ width: '{space.4}',
+ height: '{space.4}'
+ },
+
+ '.navbar-logo': {
+ '.left &': {
+ '.has-dialog &': {
+ display: 'none',
+ '@lg': {
+ display: 'block'
+ }
+ },
+ },
+ '.center &': {
+ display: 'block',
+ '@lg': {
+ display: 'none'
+ }
+ }
+ },
+
+ header: {
+ backdropFilter: '{elements.backdrop.filter}',
+ position: 'sticky',
+ top: 0,
+ zIndex: 10,
+ width: '100%',
+ borderBottom: '1px solid {elements.border.primary.static}',
+ backgroundColor: '{elements.backdrop.background}',
+ height: '{docus.header.height}',
+
+ '.container': {
+ display: 'grid',
+ height: '100%',
+ gridTemplateColumns: 'repeat(12, minmax(0, 1fr))',
+ gap: '{space.2}'
+ },
+
+ '.section': {
+ display: 'flex',
+ alignItems: 'center',
+ flex: 'none',
+ '&.left': {
+ gridColumn: 'span 4 / span 4',
+ '@lg': {
+ marginLeft: 0
+ },
+ },
+ '&.center': {
+ gridColumn: 'span 4 / span 4',
+ justifyContent: 'center',
+ flex: '1',
+ zIndex: '1'
+ },
+ '&.right': {
+ display: 'flex',
+ gridColumn: 'span 4 / span 4',
+ justifyContent: 'flex-end',
+ alignItems: 'center',
+ flex: 'none',
+ marginRight: 'calc(0px - {space.4})',
+ '.social-icons': {
+ display: 'none',
+ '@md': {
+ display: 'flex',
+ alignItems: 'center',
+ }
+ }
+ }
+ }
+ }
+})
+</style>
diff --git a/docs/components/app/AppHeaderDialog.vue b/docs/components/app/AppHeaderDialog.vue
new file mode 100644
index 0000000..e71b83c
--- /dev/null
+++ b/docs/components/app/AppHeaderDialog.vue
@@ -0,0 +1,117 @@
+<script setup lang="ts">
+const { navigation } = useContent()
+const { config } = useDocus()
+
+const filtered = computed(() => config.value.aside?.exclude || [])
+
+const links = computed(() => {
+ return (navigation.value || []).filter((item: any) => {
+ if (filtered.value.includes(item._path)) { return false }
+ return true
+ })
+})
+
+const { visible, open, close } = useMenu()
+
+watch(visible, v => (v ? open() : close()))
+</script>
+
+<template>
+ <button aria-label="Menu" @click="open">
+ <Icon name="heroicons-outline:menu" aria-hidden="”true”" />
+ </button>
+
+ <!-- eslint-disable-next-line vue/no-multiple-template-root -->
+ <teleport to="body">
+ <nav v-if="visible" class="dialog" @click="close">
+ <div @click.stop>
+ <div class="wrapper">
+ <button aria-label="Menu" @click="close">
+ <Icon name="heroicons-outline:x" aria-hidden="”true”" />
+ </button>
+
+ <div class="icons">
+ <AppSocialIcons />
+ </div>
+ </div>
+
+ <DocsAsideTree :links="links" />
+ </div>
+ </nav>
+ </teleport>
+</template>
+
+<style scoped lang="ts">
+css({
+ button: {
+ position: 'relative',
+ zIndex: '10',
+ display: 'flex',
+ padding: '{space.4} {space.4} {space.4} 0',
+ '@lg': {
+ display: 'none'
+ },
+ color: '{color.gray.500}',
+ '@dark': {
+ color: '{color.gray.400}',
+ },
+ '&:hover': {
+ color: '{color.gray.700}',
+ '@dark': {
+ color: '{color.gray.200}',
+ }
+ },
+ },
+ '.dialog': {
+ position: 'fixed',
+ inset: '0 0 0 0',
+ zIndex: '50',
+ display: 'flex',
+ alignItems: 'flex-start',
+ overflowY: 'auto',
+ backgroundColor: 'rgba(255, 255, 255, 0.5)',
+ backdropFilter: '{elements.backdrop.filter}',
+ '@dark': {
+ backgroundColor: 'rgba(0, 0, 0, 0.5)'
+ },
+ '@lg': {
+ display: 'none'
+ },
+ '.icons': {
+ overflow: 'auto'
+ },
+ // Dialog content
+ '& > div': {
+ maxWidth: '{size.xs}',
+ width: '100%',
+ minHeight: '100%',
+ boxShadow: '{shadow.md}',
+ px: '{space.4}',
+ backgroundColor: '{color.white}',
+ '@dark': {
+ backgroundColor: '{color.black}',
+ },
+ '@sm': {
+ px: '{space.6}',
+ },
+ // Dialog header
+ '& > div': {
+ height: '{docus.header.height}',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ borderBottom: '1px solid transparent',
+ gap: '{space.2}',
+ '.icons': {
+ display: 'flex',
+ alignItems: 'center',
+ }
+ }
+ }
+ },
+ ':deep(.icon)': {
+ width: '{space.4}',
+ height: '{space.4}'
+ }
+})
+</style>
diff --git a/docs/components/app/AppHeaderLogo.vue b/docs/components/app/AppHeaderLogo.vue
new file mode 100644
index 0000000..51b3e62
--- /dev/null
+++ b/docs/components/app/AppHeaderLogo.vue
@@ -0,0 +1,63 @@
+<script setup lang="ts">
+const { config } = useDocus()
+const logo = computed(() => config.value.header?.logo || false)
+const title = computed(() => config.value.header?.title || config.value.title)
+</script>
+
+<template>
+ <NuxtLink class="navbar-logo" to="/" :aria-label="title">
+ <!-- Only Logo -->
+ <span class="logo" v-if="logo">
+ <component :is="logo" v-if="typeof logo === 'string'" />
+ <template v-else-if="logo.light && logo.dark">
+ <img :src="logo.light" alt="" class="light-img">
+ <img :src="logo.dark" alt="" class="dark-img">
+ </template>
+ <Logo v-else-if="logo" />
+ </span>
+
+ <!-- Only title -->
+ <span class="title" v-else>
+ {{ title }}
+ </span>
+ </NuxtLink>
+</template>
+
+<style lang="ts" scoped>
+css({
+ a: {
+ display: 'flex',
+ alignItems: 'center',
+ flex: 'none',
+
+ '.logo': {
+ height: '{docus.header.logo.height}',
+ width: 'auto',
+ 'img, svg': {
+ height: 'inherit',
+ },
+ '.light-img': {
+ display: 'block',
+ '@dark': {
+ display: 'none'
+ }
+ },
+ '.dark-img': {
+ display: 'none',
+ '@dark': {
+ display: 'block'
+ }
+ },
+ },
+
+ '.title': {
+ fontSize: '{docus.header.title.fontSize}',
+ fontWeight: '{docus.header.title.fontWeight}',
+ color: '{docus.header.title.color.static}',
+ '&:hover': {
+ color: '{docus.header.title.color.hover}',
+ }
+ }
+ }
+})
+</style>
diff --git a/docs/components/app/AppHeaderNavigation.vue b/docs/components/app/AppHeaderNavigation.vue
new file mode 100644
index 0000000..6f3ea91
--- /dev/null
+++ b/docs/components/app/AppHeaderNavigation.vue
@@ -0,0 +1,94 @@
+<script setup lang="ts">
+const route = useRoute()
+const { navBottomLink } = useContentHelpers()
+const { navigation } = useContent()
+const { config } = useDocus()
+
+const hasNavigation = computed(() => !!config.value.aside?.level)
+
+const filtered = computed(() => config.value.header?.exclude || [])
+
+const tree = computed(() => {
+ return (navigation.value || []).filter((item: any) => {
+ if (filtered.value.includes(item._path as never)) { return false }
+ return true
+ })
+})
+
+const isActive = (link: any) => (link.exact ? route.fullPath === link._path : route.fullPath.startsWith(link._path))
+</script>
+
+<template>
+ <nav v-if="hasNavigation">
+ <ul>
+ <li
+ v-for="link in tree"
+ :key="link._path"
+ >
+ <NuxtLink
+ class="link"
+ :to="link.redirect? link.redirect : navBottomLink(link)"
+ :class="{ active: isActive(link) }"
+ >
+ <Icon v-if="link.icon && config?.header?.showLinkIcon" :name="link.icon" />
+ {{ link.title }}
+ </NuxtLink>
+ </li>
+ </ul>
+ </nav>
+</template>
+
+<style scoped lang="ts">
+css({
+ nav: {
+ display: 'none',
+ '@lg': {
+ display: 'block'
+ },
+ ul: {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ flex: '1',
+ maxWidth: '100%',
+ truncate: true,
+
+ '& > * + *': {
+ marginLeft: '{space.2}'
+ },
+
+ li: {
+ display: 'inline-flex',
+ gap: '{space.1}',
+ },
+
+ '.link': {
+ display: 'flex',
+ alignItems: 'center',
+ gap: '{space.2}',
+ padding: '{space.2} {space.4}',
+ fontSize: '{fontSize.sm}',
+ borderRadius: '{radii.md}',
+ outline: 'none',
+ transition: 'background 200ms ease',
+
+ svg: {
+ display: 'inline-block'
+ },
+
+ '&:active,&.active,&:hover': {
+ backgroundColor: '{color.gray.100}',
+ '@dark': {
+ backgroundColor: '{color.gray.900}',
+ },
+ },
+
+ '&.active': {
+ boxShadow: 'inset 0 2px 4px 0 rgb(0 0 0 / 0.05)',
+ fontWeight: '{fontWeight.semibold}'
+ }
+ }
+ }
+ }
+})
+</style>
diff --git a/docs/components/app/AppLayout.vue b/docs/components/app/AppLayout.vue
new file mode 100644
index 0000000..97a8d26
--- /dev/null
+++ b/docs/components/app/AppLayout.vue
@@ -0,0 +1,38 @@
+<script setup lang="ts">
+const { config } = useDocus()
+
+useHead({
+ titleTemplate: config.value.titleTemplate,
+ meta: [
+ { name: 'twitter:card', content: 'summary_large_image' }
+ ]
+})
+
+watch(
+ () => config.value.titleTemplate,
+ () => useHead({ titleTemplate: config.value.titleTemplate })
+)
+
+useContentHead(config.value as any)
+</script>
+
+<template>
+ <div class="app-layout">
+ <AppLoadingBar />
+ <AppHeader />
+ <main>
+ <slot />
+ </main>
+ <AppFooter />
+ </div>
+</template>
+
+<style lang="ts" scoped>
+css({
+ '.app-layout': {
+ main: {
+ minHeight: 'calc(100vh - {docus.header.height} - {docus.footer.height})',
+ }
+ }
+})
+</style>
diff --git a/docs/components/app/AppLoadingBar.vue b/docs/components/app/AppLoadingBar.vue
new file mode 100644
index 0000000..51314a8
--- /dev/null
+++ b/docs/components/app/AppLoadingBar.vue
@@ -0,0 +1,106 @@
+<script setup>
+const props = defineProps({
+ throttle: {
+ type: Number,
+ default: 200
+ },
+ duration: {
+ type: Number,
+ default: 2000
+ }
+})
+
+const nuxtApp = useNuxtApp()
+
+// Options & Data
+const data = reactive({
+ percent: 0,
+ show: false,
+ canSucceed: true
+})
+// Local variables
+let _timer = null
+let _throttle = null
+let _cut
+
+// Functions
+function clear () {
+ _timer && clearInterval(_timer)
+ _throttle && clearTimeout(_throttle)
+ _timer = null
+}
+function start () {
+ if (data.show) { return }
+ clear()
+ data.percent = 0
+ data.canSucceed = true
+
+ if (props.throttle) {
+ _throttle = setTimeout(startTimer, props.throttle)
+ } else {
+ startTimer()
+ }
+}
+function increase (num) {
+ data.percent = Math.min(100, Math.floor(data.percent + num))
+}
+function finish () {
+ data.percent = 100
+ hide()
+}
+function hide () {
+ clear()
+ setTimeout(() => {
+ data.show = false
+ setTimeout(() => {
+ data.percent = 0
+ }, 400)
+ }, 500)
+}
+function startTimer () {
+ data.show = true
+ _cut = 10000 / Math.floor(props.duration)
+ _timer = setInterval(() => {
+ increase(_cut)
+ }, 100)
+}
+
+// Hooks
+nuxtApp.hook('content:middleware:start', start)
+nuxtApp.hook('page:start', start)
+nuxtApp.hook('page:finish', finish)
+
+onBeforeUnmount(() => clear)
+</script>
+
+<template>
+ <div
+ class="nuxt-progress"
+ :class="{
+ 'nuxt-progress-failed': !data.canSucceed,
+ }"
+ :style="{
+ width: `${data.percent}%`,
+ left: data.left,
+ opacity: data.show ? 1 : 0,
+ backgroundSize: `${(100 / data.percent) * 100}% auto`,
+ }"
+ />
+</template>
+
+<style lang="ts">
+css({
+ '.nuxt-progress': {
+ height: '{docus.loadingBar.height}',
+ position: 'fixed',
+ top: '0px',
+ left: '0px',
+ right: '0px',
+ width: '0%',
+ opacity: 1,
+ transition: 'width 0.1s, height 0.4s, opacity 0.4s',
+ background: 'repeating-linear-gradient(to right, {docus.loadingBar.gradientColorStop1} 0%, {docus.loadingBar.gradientColorStop2} 50%, {docus.loadingBar.gradientColorStop3} 100%)',
+ zIndex: '999999',
+ }
+})
+</style>
diff --git a/docs/components/app/AppSearch.vue b/docs/components/app/AppSearch.vue
new file mode 100644
index 0000000..d3103aa
--- /dev/null
+++ b/docs/components/app/AppSearch.vue
@@ -0,0 +1,76 @@
+<script setup lang="ts">
+const { element } = useDocSearch()
+
+const onClick = () => element.value.querySelector('button').click()
+</script>
+
+<template>
+ <div class="doc-search" @click="onClick">
+ <button type="button" aria-label="Search">
+ <span class="content">
+ <Icon name="heroicons-outline:search" />
+ <span>Search</span>
+ <span>
+ <kbd>⌘</kbd>
+ <kbd>K</kbd>
+ </span>
+ </span>
+ </button>
+ </div>
+</template>
+
+<style scoped lang="ts">
+css({
+ '.doc-search': {
+ '&:hover': {
+ button: {
+ borderColor: '{color.gray.300}'
+ }
+ },
+ button: {
+ padding: '{space.2} {space.4}',
+ '.content': {
+ borderRadius: '{radii.md}',
+ display: 'flex',
+ alignItems: 'center',
+ color: '{color.gray.500}',
+ borderStyle: 'solid',
+ borderWidth: '1px',
+ borderColor: '{color.gray.100}',
+ fontSize: '{fontSize.xs}',
+ gap: '{space.2}',
+ padding: '{space.rem.375}',
+ '@dark': {
+ color: '{color.gray.400}',
+ borderColor: '{color.gray.900}',
+ },
+ '&:hover': {
+ color: '{color.gray.700}',
+ borderColor: '{color.gray.400}',
+ '@dark': {
+ color: '{color.gray.200}',
+ borderColor: '{color.gray.700}',
+ }
+ },
+ span: {
+ '&:first-child': {
+ display: 'block',
+ fontSize: '{fontSize.xs}',
+ fontWeight: '{fontWeight.medium}',
+ },
+ '&:nth-child(2)': {
+ flex: 'none',
+ display: 'none',
+ fontSize: '{fontSize.xs}',
+ fontWeight: '{fontWeight.semibold}',
+ '@sm': {
+ display: 'block'
+ }
+ }
+ }
+ }
+ },
+
+ }
+})
+</style>
diff --git a/docs/components/app/AppSocialIcons.vue b/docs/components/app/AppSocialIcons.vue
new file mode 100644
index 0000000..c30f513
--- /dev/null
+++ b/docs/components/app/AppSocialIcons.vue
@@ -0,0 +1,58 @@
+<script setup lang="ts">
+const socials = ['twitter', 'facebook', 'instagram', 'youtube', 'github', 'medium']
+
+const { config } = useDocus()
+
+const icons = computed<any>(() => {
+ return Object.entries(config.value.socials || {})
+ .map(([key, value]) => {
+ if (typeof value === 'object') {
+ return value
+ } else if (typeof value === 'string' && value && socials.includes(key)) {
+ return {
+ href: `https://${key}.com/${value}`,
+ icon: `fa-brands:${key}`,
+ label: value
+ }
+ } else {
+ return null
+ }
+ })
+ .filter(Boolean)
+})
+</script>
+
+<template>
+ <NuxtLink
+ v-for="icon in icons"
+ :key="icon.label"
+ rel="noopener noreferrer"
+ :title="icon.label"
+ :aria-label="icon.label"
+ :href="icon.href"
+ target="_blank"
+ >
+ <Icon v-if="icon.icon" :name="icon.icon" />
+ </NuxtLink>
+</template>
+
+<style lang="ts" scoped>
+css({
+ a: {
+ display: 'flex',
+ color: '{color.gray.500}',
+ padding: '{space.4}',
+
+ '@dark': {
+ color: '{color.gray.400}'
+ },
+
+ '&:hover': {
+ color: '{color.gray.700}',
+ '@dark': {
+ color: '{color.gray.200}',
+ }
+ },
+ }
+})
+</style>
diff --git a/docs/components/app/DocumentDrivenNotFound.vue b/docs/components/app/DocumentDrivenNotFound.vue
new file mode 100644
index 0000000..62baa8a
--- /dev/null
+++ b/docs/components/app/DocumentDrivenNotFound.vue
@@ -0,0 +1,119 @@
+<template>
+ <div class="document-driven-not-found not-prose">
+ <main>
+ <p>
+ 404
+ </p>
+ <div class="content">
+ <div class="text-section">
+ <h1>
+ Not Found
+ </h1>
+ <p>
+ This is not the page you're looking for.
+ </p>
+ </div>
+
+ <div class="button-section">
+ <ButtonLink href="/" size="large" variant="primary">
+ Go back home
+ </ButtonLink>
+ </div>
+ </div>
+ </main>
+ </div>
+</template>
+
+<style scoped lang="ts">
+css({
+ '.document-driven-not-found': {
+ display: 'flex',
+ flex: '1',
+ px: '{space.4}',
+ py: '{space.16}',
+ '@sm': {
+ px: '{space.6}',
+ py: '{space.24}',
+ },
+
+ '@md': {
+ display: 'grid',
+ placeItems: 'center',
+ },
+
+ '@lg': {
+ px: '{space.8}',
+ },
+
+ main: {
+ mx: 'auto',
+ maxWidth: '{size.full}',
+
+ '@sm': {
+ display: 'flex',
+ gap: '{space.6}'
+ },
+
+ // 404
+ '& > p': {
+ fontSize: '{fontSize.4xl}',
+ fontWeight: '{fontWeight.bold}',
+ lineHeight: '{lead.tight}',
+ color: '{color.primary.500}',
+ '@sm': {
+ fontSize: '{fontSize.5xl}',
+ }
+ },
+
+ '.content': {
+ '.text-section': {
+ borderColor: '{color.gray.200}',
+ borderLeftStyle: 'solid',
+ borderLeftWidth: '1px',
+ border: 'none',
+
+ '@dark': {
+ borderColor: '{color.gray.800}',
+ },
+
+ '@sm': {
+ borderColor: '{color.gray.200}',
+ borderLeftStyle: 'solid',
+ borderLeftWidth: '1px',
+ paddingLeft: '{space.6}'
+ },
+
+ h1: {
+ fontSize: '{fontSize.4xl}',
+ fontWeight: '{fontWeight.extrabold}',
+ letterSpacing: '{letterSpacing.tight}',
+ color: '{color.gray.900}',
+ '@dark': {
+ color: '{color.gray.200}',
+ },
+ '@sm': {
+ fontSize: '{fontSize.5xl}',
+ },
+ },
+
+ p: {
+ marginTop: '{space.1}',
+ fontSize: '{fontSize.xl}',
+ color: '{color.gray.700}',
+ '@dark': {
+ color: '{color.gray.400}',
+ },
+ }
+ },
+
+ // Button section
+ '.button-section': {
+ marginTop: '{space.10}',
+ flex: 'none',
+ gap: '{space.3}'
+ }
+ }
+ }
+ }
+})
+</style>
diff --git a/docs/components/app/Ellipsis.vue b/docs/components/app/Ellipsis.vue
new file mode 100644
index 0000000..87f4629
--- /dev/null
+++ b/docs/components/app/Ellipsis.vue
@@ -0,0 +1,66 @@
+<script setup lang="ts">
+import type { PropType } from 'vue'
+
+defineProps({
+ width: {
+ type: String,
+ default: '10rem'
+ },
+ height: {
+ type: String,
+ default: '10rem'
+ },
+ zIndex: {
+ type: String,
+ default: '10'
+ },
+ top: {
+ type: String,
+ default: '0'
+ },
+ left: {
+ type: String,
+ default: 'auto'
+ },
+ right: {
+ type: String,
+ default: 'auto'
+ },
+ blur: {
+ type: String,
+ default: '50px'
+ },
+ colors: {
+ type: Array as PropType<string[]>,
+ default: () => ['rgba(0, 71, 225, 0.22)', 'rgba(26, 214, 255, 0.22)', 'rgba(0, 220, 130, 0.22)']
+ }
+})
+</script>
+
+<template>
+ <div class="ellipsis">
+ <div class="ellipsis-item" />
+ </div>
+</template>
+
+<style scoped lang="ts">
+css({
+ '.ellipsis': {
+ pointerEvents: 'none',
+ position: 'absolute',
+ top: (props) => props.top,
+ insetInlineStart: (props) => props.left,
+ insetInlineEnd: (props) => props.right,
+ zIndex: (props) => props.zIndex,
+ width: '-webkit-fill-available',
+ maxWidth: (props) => props.width,
+ height: (props) => props.height,
+ filter: (props) => `blur(${props.blur})`,
+ '.ellipsis-item': {
+ width: '100%',
+ height: '100%',
+ background: (props) => `linear-gradient(97.62deg, ${props?.colors?.[0]} 2.27%, ${props?.colors?.[1]} 50.88%, ${props?.colors?.[2]} 98.48%)`,
+ }
+ }
+})
+</style>
diff --git a/docs/components/app/Logo.vue b/docs/components/app/Logo.vue
new file mode 100644
index 0000000..ee124c9
--- /dev/null
+++ b/docs/components/app/Logo.vue
@@ -0,0 +1,64 @@
+<template>
+ <svg viewBox="0 0 167 44" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path
+ d="M60 34.5945H70.2569C78.3172 34.5945 83.064 29.6369 83.064 21.1959V21.159C83.064 12.7365 78.2986 8 70.2569 8H60V34.5945ZM65.6217 29.987V12.5891H69.5867C74.5197 12.5891 77.3306 15.63 77.3306 21.1775V21.2143C77.3306 26.9645 74.6128 29.987 69.5867 29.987H65.6217Z"
+ fill="currentColor"
+ />
+ <path
+ d="M95.1966 35C101.228 35 105.081 31.1666 105.081 24.716V24.6792C105.081 18.284 101.153 14.4321 95.178 14.4321C89.2211 14.4321 85.312 18.3208 85.312 24.6792V24.716C85.312 31.1481 89.1281 35 95.1966 35ZM95.2152 30.7795C92.5346 30.7795 90.8407 28.5863 90.8407 24.716V24.6792C90.8407 20.8642 92.5532 18.6526 95.178 18.6526C97.8213 18.6526 99.5525 20.8642 99.5525 24.6792V24.716C99.5525 28.5679 97.8585 30.7795 95.2152 30.7795Z"
+ fill="currentColor"
+ />
+ <path
+ d="M116.823 35C122.147 35 125.59 31.7195 125.851 27.4068V27.2962H120.788L120.769 27.4621C120.397 29.4526 119.038 30.7795 116.86 30.7795C114.179 30.7795 112.504 28.5863 112.504 24.716V24.6976C112.504 20.9195 114.179 18.6526 116.841 18.6526C119.094 18.6526 120.415 20.0901 120.751 21.97L120.788 22.1358H125.832V22.0068C125.628 17.7311 122.184 14.4321 116.767 14.4321C110.792 14.4321 106.975 18.3577 106.975 24.6792V24.6976C106.975 31.0744 110.736 35 116.823 35Z"
+ fill="currentColor"
+ />
+ <path
+ d="M135.08 35C138.095 35 140.143 33.5993 141.167 31.3324H141.278V34.5945H146.714V14.8375H141.278V26.1904C141.278 28.7706 139.733 30.5399 137.295 30.5399C134.856 30.5399 133.628 29.0471 133.628 26.4669V14.8375H128.192V27.5911C128.192 32.2171 130.687 35 135.08 35Z"
+ fill="currentColor"
+ />
+ <path
+ d="M158.27 35C163.407 35 167 32.4567 167 28.5863V28.5679C167 25.656 165.287 24.0341 161.118 23.1679L157.73 22.4491C155.719 22.0253 155.031 21.3618 155.031 20.3481V20.3297C155.031 19.058 156.203 18.2287 158.083 18.2287C160.094 18.2287 161.267 19.2976 161.453 20.7167L161.471 20.8642H166.479V20.6799C166.348 17.215 163.389 14.4321 158.083 14.4321C153.001 14.4321 149.725 16.8648 149.725 20.6246V20.643C149.725 23.5918 151.68 25.5085 155.552 26.3195L158.94 27.0198C160.894 27.4437 161.564 28.0519 161.564 29.1024V29.1208C161.564 30.3925 160.317 31.185 158.27 31.185C156.092 31.185 154.919 30.2819 154.565 28.7338L154.528 28.5679H149.223L149.241 28.7338C149.669 32.4751 152.741 35 158.27 35Z"
+ fill="currentColor"
+ />
+ <mask
+ id="mask0_109_36"
+ style="mask-type:alpha"
+ maskUnits="userSpaceOnUse"
+ x="0"
+ y="0"
+ width="44"
+ height="44"
+ >
+ <path
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ d="M0 21.8104C0 9.76479 9.80347 0 21.8967 0C33.99 0 43.7935 9.76479 43.7935 21.8104C43.7935 33.8559 33.99 43.6207 21.8967 43.6207H0V21.8104ZM19.5294 17.0944C22.1442 17.0944 24.2641 19.2059 24.2641 21.8104C24.2641 24.4148 22.1442 26.526 19.5294 26.526C16.9147 26.526 14.7948 24.4148 14.7948 21.8101C14.7948 19.2059 16.9147 17.0944 19.5294 17.0944V17.0944ZM38.4674 21.8104C38.4674 19.2059 36.3475 17.0944 33.7328 17.0944C31.1183 17.0944 28.9984 19.2059 28.9984 21.8104C28.9984 24.4148 31.1183 26.526 33.7328 26.526C36.3475 26.526 38.4674 24.4148 38.4674 21.8101V21.8104Z"
+ fill="white"
+ />
+ </mask>
+ <g mask="url(#mask0_109_36)">
+ <path
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ d="M0 21.8104C0 9.76479 9.80347 0 21.8967 0C33.99 0 43.7935 9.76479 43.7935 21.8104C43.7935 33.8559 33.99 43.6207 21.8967 43.6207H0V21.8104ZM19.5294 17.0944C22.1442 17.0944 24.2641 19.2059 24.2641 21.8104C24.2641 24.4148 22.1442 26.526 19.5294 26.526C16.9147 26.526 14.7948 24.4148 14.7948 21.8101C14.7948 19.2059 16.9147 17.0944 19.5294 17.0944V17.0944ZM38.4674 21.8104C38.4674 19.2059 36.3475 17.0944 33.7328 17.0944C31.1183 17.0944 28.9984 19.2059 28.9984 21.8104C28.9984 24.4148 31.1183 26.526 33.7328 26.526C36.3475 26.526 38.4674 24.4148 38.4674 21.8101V21.8104Z"
+ fill="currentColor"
+ />
+ <path
+ d="M9.9987e-06 43.6206H-0.156525V43.7762H9.9987e-06V43.6206ZM21.8967 -0.156006C9.71704 -0.156006 -0.156525 9.67861 -0.156525 21.8106H0.156545C0.156545 9.8508 9.88991 0.155828 21.8967 0.155828V-0.156006ZM43.9497 21.8103C43.9497 9.67861 34.0764 -0.156006 21.8967 -0.156006V0.155828C33.9036 0.155828 43.6369 9.8508 43.6369 21.8103H43.9497ZM21.8967 43.7762C34.0764 43.7762 43.9497 33.9419 43.9497 21.8103H43.6369C43.6369 33.7697 33.9036 43.4647 21.8967 43.4647V43.7762V43.7762ZM9.9987e-06 43.7762H21.8967V43.4647H9.9987e-06V43.7762ZM-0.156525 21.8103V43.6206H0.156545V21.8103H-0.156525ZM24.4203 21.8103C24.4203 19.1197 22.2306 16.9387 19.5294 16.9387V17.2502C22.0577 17.2502 24.1075 19.2916 24.1075 21.8103H24.4203V21.8103ZM19.5294 26.6816C22.2306 26.6816 24.4203 24.5005 24.4203 21.8103H24.1075C24.1075 24.3286 22.058 26.37 19.5294 26.37V26.6816V26.6816ZM14.6385 21.8106C14.6385 24.5008 16.8282 26.6819 19.5294 26.6819V26.3703C17.0011 26.3703 14.9513 24.3289 14.9513 21.8106H14.6385ZM19.5297 16.9387C16.8285 16.9387 14.6388 19.1197 14.6388 21.8103H14.9516C14.9516 19.2916 17.0011 17.2502 19.5297 17.2502V16.9387ZM33.7331 17.2502C36.2617 17.2502 38.3112 19.2916 38.3112 21.8103H38.624C38.624 19.1197 36.4343 16.9387 33.7331 16.9387V17.2502V17.2502ZM29.155 21.8103C29.155 19.2916 31.2045 17.2502 33.7328 17.2502V16.9387C31.0319 16.9387 28.8422 19.1197 28.8422 21.8103H29.155ZM33.7328 26.37C31.2045 26.37 29.155 24.3286 29.155 21.8103H28.8422C28.8422 24.5005 31.0319 26.6816 33.7328 26.6816V26.37ZM38.3109 21.8106C38.3109 24.3289 36.2614 26.3703 33.7328 26.3703V26.6819C36.434 26.6819 38.6237 24.5008 38.6237 21.8106H38.3109Z"
+ fill="currentColor"
+ />
+ </g>
+ </svg>
+</template>
+
+<style lang="ts" scoped>
+css({
+ svg: {
+ color: '{color.gray.900}',
+ height: 'inherit',
+ '@dark': {
+ color: '{color.gray.100}'
+ },
+ }
+})
+</style>
diff --git a/docs/components/app/ThemeSelect.vue b/docs/components/app/ThemeSelect.vue
new file mode 100644
index 0000000..158b965
--- /dev/null
+++ b/docs/components/app/ThemeSelect.vue
@@ -0,0 +1,41 @@
+<script setup lang="ts">
+const colorMode = useColorMode()
+const onClick = () => {
+ const values = ['system', 'light', 'dark']
+ const index = values.indexOf(colorMode.preference)
+ const next = (index + 1) % values.length
+
+ colorMode.preference = values[next]
+}
+</script>
+
+<template>
+ <button aria-label="Color Mode" @click="onClick">
+ <ColorScheme placeholder="...">
+ <Icon v-if="colorMode.preference === 'dark'" name="uil:moon" />
+ <Icon v-else-if="colorMode.preference === 'light'" name="uil:sun" />
+ <Icon v-else name="uil:desktop" />
+ </ColorScheme>
+ </button>
+</template>
+
+<style lang="ts" scoped>
+css({
+ button: {
+ display: 'flex',
+ padding: '{space.4}',
+
+ color: '{color.gray.500}',
+ '@dark': {
+ color: '{color.gray.400}'
+ },
+
+ '&:hover': {
+ color: '{color.gray.700}',
+ '@dark': {
+ color: '{color.gray.200}',
+ }
+ },
+ }
+})
+</style>
diff --git a/docs/components/docs/DocsAside.vue b/docs/components/docs/DocsAside.vue
new file mode 100644
index 0000000..c084ec7
--- /dev/null
+++ b/docs/components/docs/DocsAside.vue
@@ -0,0 +1,36 @@
+<script setup lang="ts">
+const { tree } = useDocus()
+</script>
+
+<template>
+ <nav>
+ <DocsAsideTree v-if="tree?.length > 0" :links="tree" />
+ <NuxtLink v-else to="/" class="go-back-link">
+ <Icon name="heroicons-outline:arrow-left" class="icon" />
+ <span class="text">Go back</span>
+ </NuxtLink>
+ </nav>
+</template>
+
+<style scoped lang="ts">
+css({
+ '.go-back-link': {
+ display: 'flex',
+ alignItems: 'center',
+ fontSize: '{text.sm.fontSize}',
+ lineHeight: '{text.sm.lineHeight}',
+ cursor: 'pointer',
+ color: '{color.gray.500}',
+ '&:hover': {
+ color: '{color.gray.700}',
+ },
+ '.icon': {
+ width: '{space.4}',
+ height: '{space.4}'
+ },
+ '.text': {
+ marginLeft: '{space.2}'
+ }
+ }
+})
+</style>
diff --git a/docs/components/docs/DocsAsideTree.vue b/docs/components/docs/DocsAsideTree.vue
new file mode 100644
index 0000000..a713ede
--- /dev/null
+++ b/docs/components/docs/DocsAsideTree.vue
@@ -0,0 +1,212 @@
+<script setup lang="ts">
+import type { PropType } from 'vue'
+
+const props = defineProps({
+ links: {
+ type: Array as PropType<any>,
+ default: () => []
+ },
+ level: {
+ type: Number,
+ default: 0
+ },
+ max: {
+ type: Number,
+ default: null
+ },
+ parent: {
+ type: Object as PropType<any>,
+ default: null
+ }
+})
+
+const route = useRoute()
+const { config } = useDocus()
+
+const collapsedMap = useState(`docus-docs-aside-collapse-map-${props.parent?._path || '/'}`, () => {
+ if (props.level === 0) {
+ return {}
+ }
+ return (props.links as any [])
+ .filter(link => !!link.children)
+ .reduce((map, link) => {
+ map[link._path] = true
+ return map
+ }, {})
+})
+
+const isActive = (link: any) => {
+ return route.path === link._path
+}
+
+const isCollapsed = (link: any) => {
+ if (link.children) {
+ // Directory has been toggled manually, use its state
+ if (typeof collapsedMap.value[link._path] !== 'undefined') {
+ return collapsedMap.value[link._path]
+ }
+
+ // Check if aside.collapsed has been set in YML
+ if ([true, false].includes(link?.aside?.collapsed)) { return link.aside.collapsed }
+
+ // Return value grabbed from the link
+ if (link?.collapsed) { return link?.collapsed }
+
+ if (config?.value?.aside?.collapsed) { return config.value.aside?.collapsed }
+ }
+
+ return false
+}
+
+const toggleCollapse = (link: any) => (collapsedMap.value[link._path] = !isCollapsed(link))
+
+const hasNesting = computed(() => props.links.some((link: any) => link.children))
+</script>
+
+<template>
+ <ul class="docs-aside-tree">
+ <li
+ v-for="link in links"
+ :key="link._path"
+ :class="{
+ 'has-parent-icon': parent?.icon,
+ 'has-children': level > 0 && link.children,
+ 'bordered': level > 0 || !hasNesting,
+ 'active': isActive(link),
+ }"
+ >
+ <button v-if="link.children" class="title-collapsible-button" @click="toggleCollapse(link)">
+ <span class="content">
+ <Icon v-if="link?.navigation?.icon || link.icon" :name="link?.navigation?.icon || link.icon" class="icon" />
+ <span>{{ link?.navigation?.title || link.title || link._path }}</span>
+ </span>
+ <span>
+ <Icon :name="isCollapsed(link) ? 'lucide:chevrons-up-down' : 'lucide:chevrons-down-up'" class="collapsible-icon" />
+ </span>
+ </button>
+
+ <NuxtLink
+ v-else
+ :to="link.redirect ? link.redirect : link._path"
+ class="link"
+ :exact="link.exact"
+ :class="{
+ 'padded': level > 0 || !hasNesting,
+ 'active': isActive(link),
+ }"
+ >
+ <span class="content">
+ <Icon v-if="link?.navigation?.icon || link.icon" :name="link?.navigation?.icon || link.icon" class="icon" />
+ <span>{{ link?.navigation?.title || link.title || link._path }}</span>
+ </span>
+ </NuxtLink>
+
+ <DocsAsideTree
+ v-show="!isCollapsed(link)"
+ v-if="link.children?.length && (max === null || level + 1 < max)"
+ :links="link.children"
+ :level="level + 1"
+ :parent="link"
+ :max="max"
+ class="recursive"
+ />
+ </li>
+ </ul>
+</template>
+
+<style scoped lang="ts">
+css({
+ '.docs-aside-tree': {
+ li: {
+ '&.bordered': {
+ borderLeft: '1px solid {elements.border.primary.static}',
+ '&:hover': {
+ borderColor: '{elements.border.primary.hover}'
+ },
+ '&.active': {
+ borderColor: '{color.primary.400}',
+ '@dark': {
+ borderColor: '{color.primary.600}'
+ },
+ },
+ '&.has-children': {
+ paddingLeft: '{space.4}'
+ },
+ '&.has-parent-icon': {
+ marginLeft: '{space.2}'
+ }
+ }
+ },
+ '.recursive': {
+ padding: '{space.2} 0'
+ },
+ '.title-collapsible-button': {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ padding: '{space.rem.375} 0',
+ fontSize: '{text.sm.fontSize}',
+ lineHeight: '{text.sm.lineHeight}',
+ fontWeight: '{fontWeight.semibold}',
+ width: "100%",
+ color: '{color.gray.900}',
+ '@dark': {
+ color: '{color.gray.50}'
+ },
+ '.content': {
+ display: 'flex',
+ alignItems: 'center',
+ '.icon': {
+ width: '{space.4}',
+ height: '{space.4}',
+ marginRight: '{space.2}'
+ }
+ },
+ '.collapsible-icon': {
+ width: '{space.3}',
+ height: '{space.3}',
+ color: '{color.gray.400}',
+ '@dark': {
+ color: '{color.gray.500}',
+ }
+ }
+ },
+ '.link': {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ padding: '{space.rem.375} 0',
+ fontSize: '{text.sm.fontSize}',
+ lineHeight: '{text.sm.lineHeight}',
+ color: '{color.gray.500}',
+ '&:hover': {
+ color: '{color.gray.900}',
+ },
+ '@dark': {
+ '&:not(.active)': {
+ color: '{color.gray.400}',
+ '&:hover': {
+ color: '{color.gray.50}',
+ }
+ }
+ },
+ '&.padded': {
+ paddingLeft: '{space.4}'
+ },
+ '&.active': {
+ color: '{color.primary.500}',
+ fontWeight: '{fontWeight.medium}'
+ },
+ '.content': {
+ display: 'inline-flex',
+ alignItems: 'center'
+ },
+ '.icon': {
+ width: '{space.4}',
+ height: '{space.4}',
+ marginRight: '{space.1}'
+ }
+ }
+ }
+})
+</style>
diff --git a/docs/components/docs/DocsPageBottom.vue b/docs/components/docs/DocsPageBottom.vue
new file mode 100644
index 0000000..cf6b30f
--- /dev/null
+++ b/docs/components/docs/DocsPageBottom.vue
@@ -0,0 +1,46 @@
+<script setup lang="ts">
+const { page } = useContent()
+const { config } = useDocus()
+</script>
+
+<template>
+ <div v-if="page" class="docs-page-bottom">
+ <div v-if="config?.github?.edit" class="edit-link">
+ <Icon name="uil:edit" />
+ <EditOnLink v-slot="{ url }" :page="page">
+ <ProseA :to="url">
+ <span>
+ Edit this page on GitHub
+ </span>
+ </ProseA>
+ </EditOnLink>
+ </div>
+
+ <!-- Need to be supported by @nuxt/content -->
+ <span v-if="page?.mtime">Updated on <b>{{ new Intl.DateTimeFormat('en-US').format(Date.parse(page.mtime)) }}</b></span>
+ </div>
+</template>
+
+<style scoped lang="ts">
+css({
+ '.docs-page-bottom': {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ flexDirection: 'row',
+ gap: '{space.4}',
+ marginTop: '{space.8}',
+ fontSize: '{fontSize.sm}',
+ color: '{color.gray.500}',
+ '@dark': {
+ color: '{color.gray.400}'
+ },
+ '.edit-link': {
+ flex: 1,
+ display: 'flex',
+ alignItems: 'center',
+ gap: '{space.2}'
+ }
+ }
+})
+</style>
diff --git a/docs/components/docs/DocsPageLayout.vue b/docs/components/docs/DocsPageLayout.vue
new file mode 100644
index 0000000..ad5eb93
--- /dev/null
+++ b/docs/components/docs/DocsPageLayout.vue
@@ -0,0 +1,281 @@
+<script setup lang="ts">
+const { page } = useContent()
+const { config, tree } = useDocus()
+const route = useRoute()
+
+const fallbackValue = (value: string, fallback = true) => {
+ if (typeof page.value?.[value] !== 'undefined') { return page.value[value] }
+ return fallback
+}
+
+const hasBody = computed(() => !page.value || page.value?.body?.children?.length > 0)
+const hasToc = computed(() => page.value?.toc !== false && page.value?.body?.toc?.links?.length >= 2)
+
+const hasAside = computed(() => page.value?.aside !== false && (tree.value?.length > 1 || tree.value?.[0]?.children?.length))
+const bottom = computed(() => fallbackValue('bottom', true))
+const isOpen = ref(false)
+
+/*
+** This below is a workaround until Nuxt has a proper support for layouts and Suspense
+*/
+const asideNav = ref<any>(null)
+
+const getParentPath = () => route.path.split('/').slice(0, 2).join('/')
+const asideScroll = useState('asideScroll', () => {
+ return {
+ parentPath: getParentPath(),
+ scrollTop: asideNav.value?.scrollTop || 0
+ }
+})
+
+function watchScrollHeight () {
+ if (!asideNav.value) { return }
+ if (asideNav.value.scrollHeight === 0) {
+ setTimeout(watchScrollHeight, 0)
+ }
+ asideNav.value.scrollTop = asideScroll.value.scrollTop
+}
+
+onMounted(() => {
+ if (asideScroll.value.parentPath !== getParentPath()) {
+ asideScroll.value.parentPath = getParentPath()
+ asideScroll.value.scrollTop = 0
+ } else {
+ watchScrollHeight()
+ }
+})
+
+onBeforeUnmount(() => {
+ if (!asideNav.value) { return }
+ asideScroll.value.scrollTop = asideNav.value.scrollTop
+})
+</script>
+
+<template>
+ <Container
+ :fluid="config?.main?.fluid"
+ :padded="config?.main?.padded"
+ class="docs-page-content"
+ :class="{
+ fluid: config?.main?.fluid,
+ 'has-toc': hasToc,
+ 'has-aside': hasAside,
+ }"
+ >
+ <!-- Aside -->
+ <aside v-if="hasAside" ref="asideNav" class="aside-nav">
+ <DocsAside class="app-aside" />
+ </aside>
+
+ <!-- Page Body -->
+ <article class="page-body">
+ <slot v-if="hasBody" />
+ <Alert v-else type="info">
+ Start writing in <ProseCodeInline>content/{{ page._file }}</ProseCodeInline> to see this page taking shape.
+ </Alert>
+ <template v-if="hasBody && page && bottom">
+ <DocsPageBottom />
+ <DocsPrevNext />
+ </template>
+ </article>
+
+ <!-- TOC -->
+ <div v-if="hasToc" class="toc">
+ <div class="toc-wrapper">
+ <button @click="isOpen = !isOpen">
+ <span class="title">Table of Contents</span>
+ <Icon name="heroicons-outline:chevron-right" class="icon" :class="[isOpen && 'rotate']" />
+ </button>
+
+ <div class="docs-toc-wrapper" :class="[isOpen && 'opened']">
+ <DocsToc @move="isOpen = false" />
+ </div>
+ </div>
+ </div>
+ </Container>
+</template>
+
+<style scoped lang="ts">
+css({
+ '.docs-page-content': {
+ position: 'relative',
+ display: 'flex',
+ flexDirection: 'column-reverse',
+ '@lg': {
+ display: 'grid',
+ gap: '{space.8}',
+ },
+ '&.has-toc': {
+ '@lg': {
+ gridTemplateColumns: 'minmax(320px, 1fr) minmax(250px, 250px)'
+ }
+ },
+ '&.has-aside': {
+ '@lg': {
+ gridTemplateColumns: 'minmax(250px, 250px) minmax(320px, 1fr)'
+ }
+ },
+ '&.has-aside.has-toc': {
+ '@lg': {
+ gridTemplateColumns: 'minmax(250px, 250px) minmax(320px, 1fr) minmax(250px, 250px)'
+ }
+ },
+ },
+ '.aside-nav': {
+ display: 'none',
+ overflowY: 'auto',
+ '@lg': {
+ display: 'block',
+ position: 'sticky',
+ top: '{docus.header.height}',
+ // gridColumn: 'span 2/span 2',
+ alignSelf: 'flex-start',
+ height: 'calc(100vh - {docus.header.height})',
+ py: '{space.8}',
+ paddingRight: '{space.8}',
+ '.fluid &&': {
+ borderRight: '1px solid {elements.border.primary.static}',
+ }
+ }
+ },
+ '.page-body': {
+ position: 'relative',
+ display: 'flex',
+ flexDirection: "column",
+ flex: '1 1 0%',
+ py: '{space.8}',
+ width: '100%',
+ // maxWidth: '{docus.readableLine}',
+ mx: 'auto',
+ '.has-toc &&': {
+ paddingTop: '{space.12}',
+ '@lg': {
+ paddingTop: '{space.8}',
+ }
+ },
+ '@lg': {
+ marginTop: 0,
+ // gridColumnStart: 2,
+ },
+ // `.not-prose` can be useful if creating <h1> with a component (404 page is an example)
+ ':deep(h1:not(.not-prose):first-child)': {
+ marginTop: 0,
+ fontSize: '{text.4xl.fontSize}',
+ lineHeight: '{text.4xl.lineHeight}',
+ '@sm': {
+ fontSize: '{text.5xl.fontSize}',
+ lineHeight: '{text.5xl.lineHeight}',
+ }
+ },
+ // `.not-prose` can be useful if creating <h1> with a component (404 page is an example)
+ ':deep(h1:not(.not-prose)first-child + p)': {
+ marginTop: 0,
+ marginBottom: '{space.8}',
+ paddingBottom: '{space.8}',
+ borderBottom: '1px solid {elements.border.primary.static}',
+ color: '{color.gray.500}',
+ '@sm': {
+ fontSize: '{text.lg.fontSize}',
+ lineHeight: '{text.lg.lineHeight}',
+ },
+ '@dark': {
+ color: '{color.gray.400}',
+ },
+ a: {
+ color: '{color.gray.700}',
+ '@dark': {
+ color: '{color.gray.200}',
+ },
+ "&:hover": {
+ borderColor: '{color.gray.700}'
+ }
+ }
+ },
+ '.docs-prev-next': {
+ marginTop: '{space.4}'
+ }
+ },
+ '.toc': {
+ position: 'sticky',
+ top: '{docus.header.height}',
+ display: 'flex',
+ mx: 'calc(0px - {space.4})',
+ overflow: 'auto',
+ borderBottom: '1px solid {elements.border.primary.static}',
+ '@sm': {
+ mx: 'calc(0px - {space.6})',
+ },
+ '@lg': {
+ mx: 0,
+ alignSelf: 'flex-start',
+ py: '{space.8}',
+ px: '{space.8}',
+ height: 'calc(100vh - {docus.header.height})',
+ maxHeight: 'none',
+ borderBottom: 'none',
+ '.fluid &&': {
+ borderLeft: '1px solid {elements.border.primary.static}',
+ }
+ },
+ '.toc-wrapper': {
+ width: '100%',
+ height: '100%',
+ backdropFilter: '{elements.backdrop.filter}',
+ backgroundColor: '{elements.backdrop.background}',
+ '@lg': {
+ backgroundColor: 'transparent',
+ backdropFilter: 'none'
+ },
+ button: {
+ display: 'flex',
+ alignItems: 'center',
+ width: '100%',
+ height: '100%',
+ py: '{space.4}',
+ px: '{space.4}',
+ '@sm': {
+ px: '{space.6}',
+ },
+ '@lg': {
+ display: 'none'
+ },
+ '.title': {
+ fontSize: '{text.sm.fontSize}',
+ lineHeight: '{text.sm.lineHeight}',
+ fontWeight: '{fontWeight.semibold}',
+ marginRight: '{space.1}',
+ },
+ '.icon': {
+ width: '{space.4}',
+ height: '{space.4}',
+ transition: 'transform 100ms',
+ '&.rotate': {
+ transform: 'rotate(90deg)'
+ }
+ }
+ },
+ '.docs-toc-wrapper': {
+ display: 'none',
+ marginBottom: '{space.4}',
+ '&.opened': {
+ display: 'block',
+ px: '{space.4}',
+ maxHeight: '50vh',
+ overflow: 'auto',
+ '@sm': {
+ px: '{space.6}',
+ },
+ '@lg': {
+ maxHeight: 'none',
+ px: 0,
+ },
+ },
+ '@lg': {
+ marginTop: 0,
+ display: 'block'
+ }
+ }
+ }
+ }
+})
+</style>
diff --git a/docs/components/docs/DocsPrevNext.vue b/docs/components/docs/DocsPrevNext.vue
new file mode 100644
index 0000000..7f68dd0
--- /dev/null
+++ b/docs/components/docs/DocsPrevNext.vue
@@ -0,0 +1,140 @@
+<script setup lang="ts">
+import { upperFirst } from 'scule'
+
+const { prev, next, navigation } = useContent()
+const { navDirFromPath } = useContentHelpers()
+
+const directory = (link: any) => {
+ const nav = navDirFromPath(link._path, navigation.value || [])
+
+ if (nav && nav[0]) {
+ return nav[0]?._path ?? ''
+ } else {
+ const dirs = link.split('/')
+ const directory = dirs.length > 1 ? dirs[dirs.length - 2] : ''
+ return directory.split('-').map(upperFirst).join(' ')
+ }
+}
+</script>
+
+<template>
+ <div v-if="prev || next" class="docs-prev-next">
+ <NuxtLink
+ v-if="prev && prev._path"
+ :to="prev._path"
+ class="prev"
+ >
+ <Icon name="heroicons-outline:arrow-sm-left" class="icon" />
+ <div class="wrapper">
+ <span v-if="directory(prev._path)" class="directory">
+ {{ directory(prev._path) }}
+ </span>
+ <span class="title">{{ prev.title }}</span>
+ </div>
+ </NuxtLink>
+
+ <span v-else />
+
+ <NuxtLink
+ v-if="next && next._path"
+ :to="next._path"
+ class="next"
+ >
+ <div class="wrapper">
+ <span v-if="directory(next._path)" class="directory">
+ {{ directory(next._path) }}
+ </span>
+ <span class="title">{{ next.title }}</span>
+ </div>
+ <Icon name="heroicons-outline:arrow-sm-right" class="icon" />
+ </NuxtLink>
+ </div>
+</template>
+
+<style scoped lang="ts">
+css({
+ '.docs-prev-next': {
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'space-between',
+ gap: '{space.3}',
+ '@sm': {
+ flexDirection: 'row',
+ alignItems: 'center'
+ },
+ a: {
+ position: 'relative',
+ minWidth: '0px',
+ padding: '{space.3}',
+ border: '1px solid {elements.border.primary.static}',
+ borderRadius: '{radii.md}',
+ '&:hover': {
+ backgroundColor: '{color.gray.50}',
+ borderColor: '{color.gray.50}',
+ color: '{color.primary.500}',
+ },
+ '@dark': {
+ '&:hover': {
+ backgroundColor: '{color.gray.900}',
+ borderColor: '{color.gray.900}',
+ }
+ },
+ '&.prev': {
+ textAlign: 'left',
+ display: 'flex',
+ gap: '{space.3}',
+ '.directory': {
+ display: 'block',
+ marginBottom: '{space.1}',
+ fontSize: '{text.xs.fontSize}',
+ lineHeight: '{text.xs.lineHeight}',
+ fontWeight: '{fontWeight.medium}',
+ color: '{color.gray.500}',
+ truncate: true
+ },
+ '@sm': {
+ '.wrapper': {
+ alignItems: 'flex-end'
+ }
+ }
+ },
+ '&.next': {
+ textAlign: 'right',
+ display: 'flex',
+ justifyContent: 'flex-end',
+ gap: '{space.3}',
+ '.directory': {
+ display: 'block',
+ marginBottom: '{space.1}',
+ fontSize: '{text.xs.fontSize}',
+ lineHeight: '{text.xs.lineHeight}',
+ fontWeight: '{fontWeight.medium}',
+ color: '{color.gray.500}',
+ truncate: true
+ },
+ '@sm': {
+ '.wrapper': {
+ alignItems: 'flex-start'
+ }
+ }
+ },
+ '.wrapper': {
+ display: 'flex',
+ flexDirection: 'column',
+ },
+ '.icon': {
+ alignSelf: 'flex-end',
+ flexShrink: 0,
+ width: '{space.5}',
+ height: '{space.5}'
+ },
+ '.title': {
+ flex: '1 1 0%',
+ fontWeight: '{fontWeight.medium}',
+ lineHeight: '{lead.5}',
+ truncate: true
+ }
+ }
+ }
+})
+</style>
diff --git a/docs/components/docs/DocsToc.vue b/docs/components/docs/DocsToc.vue
new file mode 100644
index 0000000..83deb8a
--- /dev/null
+++ b/docs/components/docs/DocsToc.vue
@@ -0,0 +1,36 @@
+<script setup lang="ts">
+const { toc } = useContent()
+const emit = defineEmits(['move'])
+</script>
+
+<template>
+ <div class="docs-toc">
+ <template v-if="toc?.links?.length">
+ <div class="docs-toc-title">
+ <span>Table of Contents</span>
+ </div>
+
+ <DocsTocLinks :links="toc.links" @move="emit('move')" />
+ </template>
+ </div>
+</template>
+
+<style scoped lang="ts">
+css({
+ '.docs-toc': {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '8px',
+ '.docs-toc-title': {
+ display: 'none',
+ '@lg': {
+ display: 'block',
+ overflow: 'hidden',
+ fontSize: '{text.sm.fontSize}',
+ lineHeight: '{text.sm.lineHeight}',
+ fontWeight: '{fontWeight.semibold}'
+ }
+ }
+ }
+})
+</style>
diff --git a/docs/components/docs/DocsTocLinks.vue b/docs/components/docs/DocsTocLinks.vue
new file mode 100644
index 0000000..4ed37b7
--- /dev/null
+++ b/docs/components/docs/DocsTocLinks.vue
@@ -0,0 +1,87 @@
+<script setup lang="ts">
+import type { PropType } from 'vue'
+import type { TocLink } from '@nuxt/content/dist/runtime/types'
+
+defineProps({
+ links: {
+ type: Array as PropType<TocLink[]>,
+ default: () => []
+ }
+})
+
+const emit = defineEmits(['move'])
+
+const router = useRouter()
+
+const { activeHeadings, updateHeadings } = useScrollspy()
+
+if (process.client) {
+ setTimeout(() => {
+ updateHeadings([
+ ...document.querySelectorAll('.document-driven-page h1, .docus-content h1'),
+ ...document.querySelectorAll('.document-driven-page h2, .docus-content h2'),
+ ...document.querySelectorAll('.document-driven-page h3, .docus-content h3'),
+ ...document.querySelectorAll('.document-driven-page h4, .docus-content h4')
+ ])
+ }, 300)
+}
+
+function scrollToHeading (id: string) {
+ router.push(`#${id}`)
+ emit('move', id)
+}
+
+function childMove(id: string) {
+ emit('move', id)
+}
+</script>
+
+<template>
+ <ul class="docs-toc-links">
+ <li v-for="link in links" :key="link.text" :class="[`depth-${link.depth}`]">
+ <a
+ :href="`#${link.id}`"
+ :class="[activeHeadings.includes(link.id) && 'active']"
+ @click.prevent="scrollToHeading(link.id)"
+ >
+ {{ link.text }}
+ </a>
+ <DocsTocLinks v-if="link.children" :links="link.children" @move="childMove($event)" />
+ </li>
+ </ul>
+</template>
+
+<style scoped lang="ts">
+css({
+ '.docs-toc-links': {
+ '.depth-3': {
+ paddingLeft: '{space.3}'
+ },
+ '.depth-4': {
+ paddingLeft: '{space.6}'
+ },
+ a: {
+ display: 'block',
+ padding: '{space.1} 0',
+ fontSize: '{text.sm.fontSize}',
+ lineHeight: '{text.sm.lineHeight}',
+ color: '{color.gray.500}',
+ truncate: true,
+ '@lg': {
+ paddingRight: '{space.3}'
+ },
+ '&:not(.active):hover': {
+ color: '{color.gray.900}',
+ },
+ '@dark': {
+ '&:not(.active):hover': {
+ color: '{color.gray.400}',
+ },
+ },
+ '&.active': {
+ color: '{color.primary.500}'
+ }
+ }
+ }
+})
+</style>
diff --git a/docs/components/docs/EditOnLink.vue b/docs/components/docs/EditOnLink.vue
new file mode 100644
index 0000000..c1e7dfd
--- /dev/null
+++ b/docs/components/docs/EditOnLink.vue
@@ -0,0 +1,166 @@
+<script lang="ts">
+import { joinURL } from 'ufo'
+import type { PropType } from 'vue'
+import { computed, defineComponent, useSlots } from 'vue'
+import { useAppConfig } from '#imports'
+
+export default defineComponent({
+ props: {
+ /**
+ * Repository owner.
+ */
+ owner: {
+ type: String,
+ default: () => useAppConfig()?.docus?.github?.owner,
+ required: false
+ },
+ /**
+ * Repository name.
+ */
+ repo: {
+ type: String,
+ default: () => useAppConfig()?.docus?.github?.repo,
+ required: false
+ },
+ /**
+ * The branch to use for the edit link.
+ */
+ branch: {
+ type: String,
+ default: () => useAppConfig()?.docus?.github?.branch,
+ required: false
+ },
+ /**
+ * A base directory to append to the source path.
+ *
+ * Won't be used if `page` is set.
+ */
+ dir: {
+ type: String,
+ default: () => useAppConfig()?.docus?.github?.dir,
+ required: false
+ },
+ /**
+ * Source file path.
+ *
+ * Won't be used if `page` is set.
+ */
+ source: {
+ type: String,
+ required: false,
+ default: undefined
+ },
+ /**
+ * Use page from @nuxt/content.
+ */
+ page: {
+ type: Object as PropType<any>,
+ required: false,
+ default: undefined
+ },
+ /**
+ * Content directory (to be used with `page`)
+ */
+ contentDir: {
+ type: String,
+ required: false,
+ default: () => useAppConfig()?.docus?.github?.dir || 'content'
+ },
+ /**
+ * Send to an edit page or not.
+ */
+ edit: {
+ type: Boolean,
+ required: false,
+ default: () => useAppConfig()?.docus?.github?.edit
+ }
+ },
+ setup (props) {
+ if (!props.owner || !props.repo || !props.branch) {
+ throw new Error('If you want to use `GithubLink` component, you must specify: `owner`, `repo` and `branch`.')
+ }
+
+ const source = computed(() => {
+ let { repo, owner, branch, contentDir } = props
+ let prefix = ''
+
+ // Resolve source from content sources
+ if (useAppConfig()?.public?.content) {
+ let source
+ const { sources } = useAppConfig().public.content
+
+ for (const key in sources || []) {
+ if (props.page._id.startsWith(key)) {
+ source = sources[key]
+ break
+ }
+ }
+
+ if (source?.driver === 'github') {
+ repo = source.repo || props.repo || ''
+ owner = source.owner || props.owner || ''
+ branch = source.branch || props.branch || 'main'
+ contentDir = source.dir || props.contentDir || ''
+ prefix = source.prefix || ''
+ }
+ }
+
+ return { repo, owner, branch, contentDir, prefix }
+ })
+
+ const base = computed(() => joinURL('https://github.com', `${source.value.owner}/${source.value.repo}`))
+
+ const path = computed(() => {
+ const dirParts: string[] = []
+
+ // @nuxt/content support
+ // Create the URL from a document data.
+ if (props?.page?._path) {
+ // Use content dir
+ if (source.value.contentDir) { dirParts.push(source.value.contentDir) }
+
+ // Get page file from page data
+ dirParts.push(props.page._file.substring(source.value.prefix.length))
+
+ return dirParts
+ }
+
+ // Use props dir
+ if (props.dir) {
+ dirParts.push(props.dir)
+ }
+
+ // Use props source
+ if (props.source) {
+ dirParts.push(props.source)
+ }
+
+ return dirParts
+ })
+
+ /**
+ * Create edit link.
+ */
+ const url = computed(() => {
+ const parts = [base.value]
+
+ if (props.edit) { parts.push('edit') } else { parts.push('tree') }
+
+ parts.push(source?.value?.branch || '', ...path.value)
+
+ return parts.filter(Boolean).join('/')
+ })
+
+ return {
+ url
+ }
+ },
+ render (ctx: any) {
+ const { url } = ctx
+
+ const slots = useSlots()
+
+ return slots?.default?.({ url })
+ }
+})
+</script>
diff --git a/docs/components/docs/SourceLink.vue b/docs/components/docs/SourceLink.vue
new file mode 100644
index 0000000..7e803e0
--- /dev/null
+++ b/docs/components/docs/SourceLink.vue
@@ -0,0 +1,21 @@
+<script setup lang="ts">
+defineProps({
+ source: {
+ type: String,
+ required: true,
+ },
+})
+</script>
+
+<template>
+ <ProseP>
+ <!--
+ <GithubLink v-slot="data" :source="source" :edit="false">
+ <NuxtLink :href="data?.url" target="_blank" rel="noopener" class="hover:text-primary-500 flex items-center gap-1 text-sm font-semibold">
+ <Icon name="fa-brands:github" class="mr-1 h-5 w-5" />
+ <span>Show Source</span>
+ </NuxtLink>
+ </GithubLink>
+ -->
+ </ProseP>
+</template>