ContentToc

GitHub à
Une table des matières collante avec surbrillance automatique des liens d
Ce composant est uniquement disponible lorsque le module @nuxt/content est installé.

Utilisation

Utilisez le prop links avec le page?.body?.toc?.links que vous obtenez lors de la récupération d'une page.

<script setup lang="ts">
import type { Collections } from '@nuxt/content'

const route = useRoute()
const { locale } = useI18n()

const contentPath = computed(() => {
  const path = route.path.replace(/\/$/, '')
  if (locale.value === 'en') return path
  return path.replace(`/${locale.value}`, '') || '/'
})

const { data: page } = await useAsyncData(
  `toc-${locale.value}-${contentPath.value}`,
  async () => {
    const collection = `docs_${locale.value}` as keyof Collections
    let content = await queryCollection(collection).path(contentPath.value).first()
    if (!content && locale.value !== 'en') {
      content = await queryCollection('docs_en').path(contentPath.value).first()
    }
    return content
  },
  { watch: [locale] }
)
if (!page.value) {
  throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
}
</script>

<template>
  <UContentToc :links="page?.body?.toc?.links" />
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { Collections } from '@nuxt/content'

const route = useRoute()
const { locale } = useI18n()

const contentPath = computed(() => {
  const path = route.path.replace(/\/$/, '')
  if (locale.value === 'en') return path
  return path.replace(`/${locale.value}`, '') || '/'
})

const { data: page } = await useAsyncData(
  `toc-${locale.value}-${contentPath.value}`,
  async () => {
    const collection = `docs_${locale.value}` as keyof Collections
    let content = await queryCollection(collection).path(contentPath.value).first()
    if (!content && locale.value !== 'en') {
      content = await queryCollection('docs_en').path(contentPath.value).first()
    }
    return content
  },
  { watch: [locale] }
)
if (!page.value) {
  throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
}
</script>

<template>
  <UContentToc :links="page?.body?.toc?.links" />
</template>

Titre

Utilisez la prop title pour modifier le titre de la table des matières.

<script setup lang="ts">
import type { ContentTocLink } from '@nuxt/ui'

const links = ref<ContentTocLink[]>([
  {
    id: 'usage',
    depth: 2,
    text: 'Usage',
    children: [
      {
        id: 'title',
        depth: 3,
        text: 'Title'
      },
      {
        id: 'color',
        depth: 3,
        text: 'Color'
      },
      {
        id: 'highlight',
        depth: 3,
        text: 'Highlight'
      },
      {
        id: 'highlight-color',
        depth: 3,
        text: 'Highlight Color'
      },
      {
        id: 'highlight-variant',
        depth: 3,
        text: 'Highlight Variant'
      }
    ]
  }
])
</script>

<template>
  <UContentToc title="On this page" :links="links" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ContentTocLink } from '@nuxt/ui'

const links = ref<ContentTocLink[]>([
  {
    id: 'usage',
    depth: 2,
    text: 'Usage',
    children: [
      {
        id: 'title',
        depth: 3,
        text: 'Title'
      },
      {
        id: 'color',
        depth: 3,
        text: 'Color'
      },
      {
        id: 'highlight',
        depth: 3,
        text: 'Highlight'
      },
      {
        id: 'highlight-color',
        depth: 3,
        text: 'Highlight Color'
      },
      {
        id: 'highlight-variant',
        depth: 3,
        text: 'Highlight Variant'
      }
    ]
  }
])
</script>

<template>
  <UContentToc title="On this page" :links="links" />
</template>

Couleur

Utilisez le prop color pour changer la couleur des liens.

<script setup lang="ts">
import type { ContentTocLink } from '@nuxt/ui'

const links = ref<ContentTocLink[]>([
  {
    id: 'usage',
    depth: 2,
    text: 'Usage',
    children: [
      {
        id: 'title',
        depth: 3,
        text: 'Title'
      },
      {
        id: 'color',
        depth: 3,
        text: 'Color'
      },
      {
        id: 'highlight',
        depth: 3,
        text: 'Highlight'
      },
      {
        id: 'highlight-color',
        depth: 3,
        text: 'Highlight Color'
      },
      {
        id: 'highlight-variant',
        depth: 3,
        text: 'Highlight Variant'
      }
    ]
  }
])
</script>

<template>
  <UContentToc color="neutral" :links="links" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ContentTocLink } from '@nuxt/ui'

const links = ref<ContentTocLink[]>([
  {
    id: 'usage',
    depth: 2,
    text: 'Usage',
    children: [
      {
        id: 'title',
        depth: 3,
        text: 'Title'
      },
      {
        id: 'color',
        depth: 3,
        text: 'Color'
      },
      {
        id: 'highlight',
        depth: 3,
        text: 'Highlight'
      },
      {
        id: 'highlight-color',
        depth: 3,
        text: 'Highlight Color'
      },
      {
        id: 'highlight-variant',
        depth: 3,
        text: 'Highlight Variant'
      }
    ]
  }
])
</script>

<template>
  <UContentToc color="neutral" :links="links" />
</template>

highlight

Utilisez le prop highlight pour afficher une bordure surlignée pour l'élément actif.

<script setup lang="ts">
import type { ContentTocLink } from '@nuxt/ui'

const links = ref<ContentTocLink[]>([
  {
    id: 'usage',
    depth: 2,
    text: 'Usage',
    children: [
      {
        id: 'title',
        depth: 3,
        text: 'Title'
      },
      {
        id: 'color',
        depth: 3,
        text: 'Color'
      },
      {
        id: 'highlight',
        depth: 3,
        text: 'Highlight'
      },
      {
        id: 'highlight-color',
        depth: 3,
        text: 'Highlight Color'
      },
      {
        id: 'highlight-variant',
        depth: 3,
        text: 'Highlight Variant'
      }
    ]
  }
])
</script>

<template>
  <UContentToc highlight :links="links" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ContentTocLink } from '@nuxt/ui'

const links = ref<ContentTocLink[]>([
  {
    id: 'usage',
    depth: 2,
    text: 'Usage',
    children: [
      {
        id: 'title',
        depth: 3,
        text: 'Title'
      },
      {
        id: 'color',
        depth: 3,
        text: 'Color'
      },
      {
        id: 'highlight',
        depth: 3,
        text: 'Highlight'
      },
      {
        id: 'highlight-color',
        depth: 3,
        text: 'Highlight Color'
      },
      {
        id: 'highlight-variant',
        depth: 3,
        text: 'Highlight Variant'
      }
    ]
  }
])
</script>

<template>
  <UContentToc highlight :links="links" />
</template>

XPH121xHighlight Couleur

Utilisez la prop highlight-color pour changer la couleur de la surbrillance. Elle est par défaut la prop color.

<script setup lang="ts">
import type { ContentTocLink } from '@nuxt/ui'

const links = ref<ContentTocLink[]>([
  {
    id: 'usage',
    depth: 2,
    text: 'Usage',
    children: [
      {
        id: 'title',
        depth: 3,
        text: 'Title'
      },
      {
        id: 'color',
        depth: 3,
        text: 'Color'
      },
      {
        id: 'highlight',
        depth: 3,
        text: 'Highlight'
      },
      {
        id: 'highlight-color',
        depth: 3,
        text: 'Highlight Color'
      },
      {
        id: 'highlight-variant',
        depth: 3,
        text: 'Highlight Variant'
      }
    ]
  }
])
</script>

<template>
  <UContentToc highlight highlight-color="neutral" :links="links" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ContentTocLink } from '@nuxt/ui'

const links = ref<ContentTocLink[]>([
  {
    id: 'usage',
    depth: 2,
    text: 'Usage',
    children: [
      {
        id: 'title',
        depth: 3,
        text: 'Title'
      },
      {
        id: 'color',
        depth: 3,
        text: 'Color'
      },
      {
        id: 'highlight',
        depth: 3,
        text: 'Highlight'
      },
      {
        id: 'highlight-color',
        depth: 3,
        text: 'Highlight Color'
      },
      {
        id: 'highlight-variant',
        depth: 3,
        text: 'Highlight Variant'
      }
    ]
  }
])
</script>

<template>
  <UContentToc highlight highlight-color="neutral" :links="links" />
</template>

Highlight Variant: badge{label="4.6+" class="align-text-top"}

Utilisez la prop highlight-variant pour modifier le style de la surbrillance. Par défaut à straight.

<script setup lang="ts">
import type { ContentTocLink } from '@nuxt/ui'

const links = ref<ContentTocLink[]>([
  {
    id: 'usage',
    depth: 2,
    text: 'Usage',
    children: [
      {
        id: 'title',
        depth: 3,
        text: 'Title'
      },
      {
        id: 'color',
        depth: 3,
        text: 'Color'
      },
      {
        id: 'highlight',
        depth: 3,
        text: 'Highlight'
      },
      {
        id: 'highlight-color',
        depth: 3,
        text: 'Highlight Color'
      },
      {
        id: 'highlight-variant',
        depth: 3,
        text: 'Highlight Variant'
      }
    ]
  },
  {
    id: 'examples',
    depth: 2,
    text: 'Examples',
    children: [
      {
        id: 'within-a-page',
        depth: 3,
        text: 'Within a Page'
      }
    ]
  },
  {
    id: 'api',
    depth: 2,
    text: 'API',
    children: [
      {
        id: 'props',
        depth: 3,
        text: 'Props'
      },
      {
        id: 'slots',
        depth: 3,
        text: 'Slots'
      },
      {
        id: 'emits',
        depth: 3,
        text: 'Emits'
      }
    ]
  },
  {
    id: 'theme',
    depth: 2,
    text: 'Theme'
  }
])
</script>

<template>
  <UContentToc highlight highlight-color="primary" highlight-variant="circuit" :links="links" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { ContentTocLink } from '@nuxt/ui'

const links = ref<ContentTocLink[]>([
  {
    id: 'usage',
    depth: 2,
    text: 'Usage',
    children: [
      {
        id: 'title',
        depth: 3,
        text: 'Title'
      },
      {
        id: 'color',
        depth: 3,
        text: 'Color'
      },
      {
        id: 'highlight',
        depth: 3,
        text: 'Highlight'
      },
      {
        id: 'highlight-color',
        depth: 3,
        text: 'Highlight Color'
      },
      {
        id: 'highlight-variant',
        depth: 3,
        text: 'Highlight Variant'
      }
    ]
  },
  {
    id: 'examples',
    depth: 2,
    text: 'Examples',
    children: [
      {
        id: 'within-a-page',
        depth: 3,
        text: 'Within a Page'
      }
    ]
  },
  {
    id: 'api',
    depth: 2,
    text: 'API',
    children: [
      {
        id: 'props',
        depth: 3,
        text: 'Props'
      },
      {
        id: 'slots',
        depth: 3,
        text: 'Slots'
      },
      {
        id: 'emits',
        depth: 3,
        text: 'Emits'
      }
    ]
  },
  {
    id: 'theme',
    depth: 2,
    text: 'Theme'
  }
])
</script>

<template>
  <UContentToc highlight highlight-color="primary" highlight-variant="circuit" :links="links" />
</template>

Exemples

Dans une page

Utilisez le composant ContentToc dans une page pour afficher la table des matières:

pages/[...slug].vue
<script setup lang="ts">
const route = useRoute()

const { data: page } = await useAsyncData(route.path, () => queryCollection('docs').path(route.path).first())
if (!page.value) {
  throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
}
</script>

<template>
  <UPage v-if="page">
    <UPageHeader :title="page.title" />

    <UPageBody>
      <ContentRenderer v-if="page.body" :value="page" />

      <USeparator v-if="surround?.filter(Boolean).length" />

      <UContentSurround :surround="(surround as any)" />
    </UPageBody>

    <template v-if="page?.body?.toc?.links?.length" #right>
      <UContentToc :links="page.body.toc.links" />
    </template>
  </UPage>
</template>

API

Props

Prop Default Type
as
'nav'
any

The element or component this component should render as.

trailingIcon
appConfig.ui.icons.chevronDown
any

The icon displayed to collapse the content.

title
t('contentToc.title')
string

The title of the table of contents.

color
'primary'
"primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral"
highlight
false
boolean

Display a line next to the active link.

highlightColor
'primary'
"primary" | "secondary" | "success" | "info" | "warning" | "error" | "neutral"
highlightVariant
'straight'
"straight" | "circuit"

The variant of the highlight indicator.

links
T[]
defaultOpen
boolean

The open state of the collapsible when it is initially rendered.
Use when you do not need to control its open state.

open
boolean

The controlled open state of the collapsible. Can be binded with v-model.

ui
{ root?: SlotClass; container?: SlotClass; top?: SlotClass; bottom?: SlotClass; trigger?: SlotClass; title?: SlotClass; trailing?: SlotClass; trailingIcon?: SlotClass; content?: SlotClass; list?: SlotClass; listWithChildren?: SlotClass; item?: SlotClass; itemWithChildren?: SlotClass; link?: SlotClass; linkText?: SlotClass; indicator?: SlotClass; indicatorLine?: SlotClass; indicatorActive?: SlotClass; }

Slots

Slot Type
leading
{ open: boolean; ui: object; }
default
{ open: boolean; }
trailing
{ open: boolean; ui: object; }
content
{ links: T[]; }
link
{ link: T; }
top
{ links?: T[] | undefined; }
bottom
{ links?: T[] | undefined; }

Emits

Event Type
update:open
[value: boolean]
move
[id: string]

Thème

app.config.ts
export default defineAppConfig({
  ui: {
    contentToc: {
      slots: {
        root: 'sticky top-(--ui-header-height) z-10 bg-default/75 lg:bg-[initial] backdrop-blur-sm -mx-4 px-4 sm:px-6 sm:-mx-6 lg:ms-0 flex flex-col overflow-y-auto max-h-[calc(100vh-var(--ui-header-height))]',
        container: 'pt-4 sm:pt-6 pb-2.5 sm:pb-4.5 lg:py-8 border-b border-dashed border-default lg:border-0 flex flex-col lg:min-h-0',
        top: 'lg:shrink-0',
        bottom: 'hidden lg:flex lg:flex-col lg:shrink-0 gap-6',
        trigger: 'group text-sm font-semibold flex-1 flex items-center gap-1.5 py-1.5 -mt-1.5 rounded-sm outline-primary/25 focus-visible:outline-3 lg:shrink-0',
        title: 'truncate',
        trailing: 'ms-auto inline-flex gap-1.5 items-center',
        trailingIcon: 'size-5 transform transition-transform duration-200 ease-out motion-reduce:transition-none shrink-0 group-data-[state=open]:rotate-180 lg:hidden',
        content: 'relative data-[state=open]:animate-[collapsible-down_200ms_var(--ease-out)] data-[state=closed]:animate-[collapsible-up_200ms_var(--ease-out)] data-[state=closed]:overflow-hidden focus:outline-none lg:min-h-[min(var(--list-height,8rem),8rem)] lg:overflow-y-auto lg:scrollbar-none',
        list: 'min-w-0',
        listWithChildren: 'ms-3',
        item: 'min-w-0',
        itemWithChildren: '',
        link: 'group relative text-sm flex items-center rounded-sm outline-primary/25 focus-visible:outline-3 py-1',
        linkText: 'truncate',
        indicator: '',
        indicatorLine: '',
        indicatorActive: ''
      },
      variants: {
        color: {
          primary: '',
          secondary: '',
          success: '',
          info: '',
          warning: '',
          error: '',
          neutral: ''
        },
        highlightColor: {
          primary: {
            indicatorActive: 'bg-primary'
          },
          secondary: {
            indicatorActive: 'bg-secondary'
          },
          success: {
            indicatorActive: 'bg-success'
          },
          info: {
            indicatorActive: 'bg-info'
          },
          warning: {
            indicatorActive: 'bg-warning'
          },
          error: {
            indicatorActive: 'bg-error'
          },
          neutral: {
            indicatorActive: 'bg-inverted'
          }
        },
        active: {
          false: {
            link: [
              'text-muted hover:text-default',
              'transition-colors'
            ]
          }
        },
        highlight: {
          true: ''
        },
        highlightVariant: {
          straight: '',
          circuit: ''
        },
        body: {
          true: {
            bottom: 'mt-6'
          }
        }
      },
      compoundVariants: [
        {
          color: 'primary',
          active: true,
          class: {
            link: 'text-primary'
          }
        },
        {
          color: 'neutral',
          active: true,
          class: {
            link: 'text-highlighted'
          }
        },
        {
          highlight: true,
          highlightVariant: 'straight',
          class: {
            list: 'ms-2.5 ps-4 border-s border-default',
            item: '-ms-px',
            indicator: 'absolute ms-2.5 transition-[translate,height] duration-200 ease-out motion-reduce:transition-none h-(--indicator-size) translate-y-(--indicator-position) w-px rounded-full',
            indicatorLine: 'hidden',
            indicatorActive: 'w-full h-full'
          }
        },
        {
          highlight: true,
          highlightVariant: 'circuit',
          class: {
            list: 'ps-6.5',
            item: '-ms-px',
            itemWithChildren: 'ps-px',
            indicator: 'absolute ms-2.5 start-0 top-0 rtl:-scale-x-100',
            indicatorLine: 'absolute inset-0 bg-(--ui-border)',
            indicatorActive: 'absolute w-full h-(--indicator-size) translate-y-(--indicator-position) transition-[translate,height] duration-200 ease-out motion-reduce:transition-none'
          }
        }
      ],
      defaultVariants: {
        color: 'primary',
        highlightColor: 'primary',
        highlightVariant: 'straight'
      }
    }
  }
})
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      ui: {
        contentToc: {
          slots: {
            root: 'sticky top-(--ui-header-height) z-10 bg-default/75 lg:bg-[initial] backdrop-blur-sm -mx-4 px-4 sm:px-6 sm:-mx-6 lg:ms-0 flex flex-col overflow-y-auto max-h-[calc(100vh-var(--ui-header-height))]',
            container: 'pt-4 sm:pt-6 pb-2.5 sm:pb-4.5 lg:py-8 border-b border-dashed border-default lg:border-0 flex flex-col lg:min-h-0',
            top: 'lg:shrink-0',
            bottom: 'hidden lg:flex lg:flex-col lg:shrink-0 gap-6',
            trigger: 'group text-sm font-semibold flex-1 flex items-center gap-1.5 py-1.5 -mt-1.5 rounded-sm outline-primary/25 focus-visible:outline-3 lg:shrink-0',
            title: 'truncate',
            trailing: 'ms-auto inline-flex gap-1.5 items-center',
            trailingIcon: 'size-5 transform transition-transform duration-200 ease-out motion-reduce:transition-none shrink-0 group-data-[state=open]:rotate-180 lg:hidden',
            content: 'relative data-[state=open]:animate-[collapsible-down_200ms_var(--ease-out)] data-[state=closed]:animate-[collapsible-up_200ms_var(--ease-out)] data-[state=closed]:overflow-hidden focus:outline-none lg:min-h-[min(var(--list-height,8rem),8rem)] lg:overflow-y-auto lg:scrollbar-none',
            list: 'min-w-0',
            listWithChildren: 'ms-3',
            item: 'min-w-0',
            itemWithChildren: '',
            link: 'group relative text-sm flex items-center rounded-sm outline-primary/25 focus-visible:outline-3 py-1',
            linkText: 'truncate',
            indicator: '',
            indicatorLine: '',
            indicatorActive: ''
          },
          variants: {
            color: {
              primary: '',
              secondary: '',
              success: '',
              info: '',
              warning: '',
              error: '',
              neutral: ''
            },
            highlightColor: {
              primary: {
                indicatorActive: 'bg-primary'
              },
              secondary: {
                indicatorActive: 'bg-secondary'
              },
              success: {
                indicatorActive: 'bg-success'
              },
              info: {
                indicatorActive: 'bg-info'
              },
              warning: {
                indicatorActive: 'bg-warning'
              },
              error: {
                indicatorActive: 'bg-error'
              },
              neutral: {
                indicatorActive: 'bg-inverted'
              }
            },
            active: {
              false: {
                link: [
                  'text-muted hover:text-default',
                  'transition-colors'
                ]
              }
            },
            highlight: {
              true: ''
            },
            highlightVariant: {
              straight: '',
              circuit: ''
            },
            body: {
              true: {
                bottom: 'mt-6'
              }
            }
          },
          compoundVariants: [
            {
              color: 'primary',
              active: true,
              class: {
                link: 'text-primary'
              }
            },
            {
              color: 'neutral',
              active: true,
              class: {
                link: 'text-highlighted'
              }
            },
            {
              highlight: true,
              highlightVariant: 'straight',
              class: {
                list: 'ms-2.5 ps-4 border-s border-default',
                item: '-ms-px',
                indicator: 'absolute ms-2.5 transition-[translate,height] duration-200 ease-out motion-reduce:transition-none h-(--indicator-size) translate-y-(--indicator-position) w-px rounded-full',
                indicatorLine: 'hidden',
                indicatorActive: 'w-full h-full'
              }
            },
            {
              highlight: true,
              highlightVariant: 'circuit',
              class: {
                list: 'ps-6.5',
                item: '-ms-px',
                itemWithChildren: 'ps-px',
                indicator: 'absolute ms-2.5 start-0 top-0 rtl:-scale-x-100',
                indicatorLine: 'absolute inset-0 bg-(--ui-border)',
                indicatorActive: 'absolute w-full h-(--indicator-size) translate-y-(--indicator-position) transition-[translate,height] duration-200 ease-out motion-reduce:transition-none'
              }
            }
          ],
          defaultVariants: {
            color: 'primary',
            highlightColor: 'primary',
            highlightVariant: 'straight'
          }
        }
      }
    })
  ]
})
Some colors in compoundVariants are omitted for readability. Check out the source code on GitHub.

Changelog écrit

No recent changes