Skip to content

Commit

Permalink
feat: toc
Browse files Browse the repository at this point in the history
Signed-off-by: ZTL-UwU <zhangtianli2006@163.com>
  • Loading branch information
ZTL-UwU committed May 23, 2024
1 parent a33caab commit 3ecd08b
Show file tree
Hide file tree
Showing 12 changed files with 160 additions and 7 deletions.
2 changes: 1 addition & 1 deletion components/content/Alert.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<UiAlert
class="[&:not(:first-child)]:mt-5 transition-all"
:class="[typeTwClass[type], to && 'cursor-pointer hover:bg-muted']"
:class="[typeTwClass[type], to && 'cursor-pointer hover:bg-zinc-50 dark:hover:bg-zinc-900']"
@click="alertClick"
>
<Icon v-if="icon && title" :name="icon" />
Expand Down
27 changes: 27 additions & 0 deletions components/layout/Toc.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<template>
<div class="hidden xl:block">
<UiScrollArea orientation="vertical" class="h-[calc(100vh-6.5rem)] z-30 md:block overflow-y-auto" type="hover">
<p class="mb-2 text-base font-semibold">
On This Page
</p>
<LayoutTocTree :links="toc.links" :level="0" />
</UiScrollArea>
</div>

<div class="block xl:hidden mb-6">
<UiCollapsible>
<UiCollapsibleTrigger>
<UiButton variant="outline">
On This Page
</UiButton>
</UiCollapsibleTrigger>
<UiCollapsibleContent class="text-sm mt-4 border-l pl-4">
<LayoutTocTree :links="toc.links" :level="0" />
</UiCollapsibleContent>
</UiCollapsible>
</div>
</template>

<script setup lang="ts">
const { toc } = useContent();
</script>
34 changes: 34 additions & 0 deletions components/layout/TocTree.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<template>
<ul :class="[level !== 0 && 'pl-4']">
<li v-for="link in links" :key="link.id" class="pt-2">
<NuxtLink
:to="`#${link.id}`"
class="text-muted-foreground hover:text-primary hover:font-medium transition-all"
:class="[activeHeadings.includes(link.id) && 'text-primary font-medium']"
>
{{ link.text }}
</NuxtLink>
<TocTree v-if="link.children" :links="link.children" :level="level + 1" />
</li>
</ul>
</template>

<script setup lang="ts">
import type { TocLink } from '@nuxt/content/types';
defineProps<{
links: TocLink[];
level: number;
}>();
const { activeHeadings, updateHeadings } = useScrollspy();
onNuxtReady(() =>
updateHeadings([
...document.querySelectorAll('.docs-content h1'),
...document.querySelectorAll('.docs-content h2'),
...document.querySelectorAll('.docs-content h3'),
...document.querySelectorAll('.docs-content h4'),
]),
);
</script>
3 changes: 2 additions & 1 deletion components/ui/breadcrumb/BreadcrumbEllipsis.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
:class="cn('flex h-9 w-9 items-center justify-center', props.class)"
>
<slot>
<Icon name="lucide:more-horizontal" class="h-4 w-4" />
<MoreHorizontal class="h-4 w-4" />
</slot>
<span class="sr-only">More</span>
</span>
</template>

<script lang="ts" setup>
import type { HTMLAttributes } from 'vue';
import { MoreHorizontal } from 'lucide-vue-next';
import { cn } from '@/lib/utils';
const props = defineProps<{
Expand Down
3 changes: 2 additions & 1 deletion components/ui/breadcrumb/BreadcrumbSeparator.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
:class="cn('[&>svg]:size-3.5', props.class)"
>
<slot>
<Icon name="lucide:chevron-right" class="mb-1" />
<ChevronRight />
</slot>
</li>
</template>

<script lang="ts" setup>
import type { HTMLAttributes } from 'vue';
import { ChevronRight } from 'lucide-vue-next';
import { cn } from '@/lib/utils';
const props = defineProps<{
Expand Down
15 changes: 15 additions & 0 deletions components/ui/collapsible/Collapsible.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<template>
<CollapsibleRoot v-slot="{ open }" v-bind="forwarded">
<slot :open="open" />
</CollapsibleRoot>
</template>

<script setup lang="ts">
import { CollapsibleRoot, useForwardPropsEmits } from 'radix-vue';
import type { CollapsibleRootEmits, CollapsibleRootProps } from 'radix-vue';
const props = defineProps<CollapsibleRootProps>();
const emits = defineEmits<CollapsibleRootEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
11 changes: 11 additions & 0 deletions components/ui/collapsible/CollapsibleContent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<template>
<CollapsibleContent v-bind="props" class="overflow-hidden transition-all data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down">
<slot />
</CollapsibleContent>
</template>

<script setup lang="ts">
import { CollapsibleContent, type CollapsibleContentProps } from 'radix-vue';
const props = defineProps<CollapsibleContentProps>();
</script>
11 changes: 11 additions & 0 deletions components/ui/collapsible/CollapsibleTrigger.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<template>
<CollapsibleTrigger v-bind="props">
<slot />
</CollapsibleTrigger>
</template>

<script setup lang="ts">
import { CollapsibleTrigger, type CollapsibleTriggerProps } from 'radix-vue';
const props = defineProps<CollapsibleTriggerProps>();
</script>
3 changes: 3 additions & 0 deletions components/ui/collapsible/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as Collapsible } from './Collapsible.vue';
export { default as CollapsibleTrigger } from './CollapsibleTrigger.vue';
export { default as CollapsibleContent } from './CollapsibleContent.vue';
45 changes: 45 additions & 0 deletions composables/useScrollspy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Credit: nuxt-themes/docus

import type { Ref } from 'vue';

/**
* Scrollspy allows you to watch visible headings in a specific page.
* Useful for table of contents live style updates.
*/
export function useScrollspy() {
const observer = ref() as Ref<IntersectionObserver>;
const visibleHeadings = ref([]) as Ref<string[]>;
const activeHeadings = ref([]) as Ref<string[]>;

const observerCallback = (entries: IntersectionObserverEntry[]) =>
entries.forEach((entry) => {
const id = entry.target.id;

if (entry.isIntersecting)
visibleHeadings.value.push(id);
else visibleHeadings.value = visibleHeadings.value.filter(t => t !== id);
});

const updateHeadings = (headings: Element[]) =>
headings.forEach((heading) => {
observer.value.observe(heading);
});

watch(visibleHeadings, (val, oldVal) => {
if (val.length === 0)
activeHeadings.value = oldVal;
else activeHeadings.value = val;
}, { deep: true });

// Create intersection observer
onBeforeMount(() => (observer.value = new IntersectionObserver(observerCallback)));

// Destroy it
onBeforeUnmount(() => observer.value?.disconnect());

return {
visibleHeadings,
activeHeadings,
updateHeadings,
};
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@
"shiki": "^1.6.0",
"vue-tsc": "^2.0.19"
}
}
}
11 changes: 8 additions & 3 deletions pages/[...slug].vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
<LayoutAside />
<main class="relative py-6 lg:gap-10 lg:py-8 xl:grid xl:grid-cols-[1fr_250px]">
<div class="mx-auto w-full min-w-0">
<div class="xl:hidden">
<LayoutToc />
</div>
<LayoutBreadcrumb class="mb-4" />

<div class="space-y-2 mb-6">
<ProseH1>
{{ page?.title }}
Expand All @@ -14,26 +18,27 @@
{{ page?.description }}
</p>
</div>

<Alert
v-if="!page?.body || page?.body?.children?.length === 0"
title="Empty Page"
icon="lucide:circle-x"
>
Start writing in <ProseCodeInline>content/{{ page._file }}</ProseCodeInline> to see this page taking shape.
</Alert>

<ContentRenderer
v-else
:key="page._id"
:value="page"
class="docs-content"
/>

<LayoutPrevNext />
</div>
<div class="hidden text-sm xl:block">
<div class="sticky top-16 -mt-10 h-[calc(100vh-3.5rem)] overflow-hidden pt-6">
<div class="w-10">
TOC
</div>
<LayoutToc />
</div>
</div>
</main>
Expand Down

0 comments on commit 3ecd08b

Please sign in to comment.