@movk/nuxt-docs 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +265 -0
  2. package/app/app.config.ts +53 -0
  3. package/app/app.vue +73 -0
  4. package/app/components/AdsCarbon.vue +3 -0
  5. package/app/components/Footer.vue +32 -0
  6. package/app/components/PageHeaderLinks.vue +73 -0
  7. package/app/components/StarsBg.vue +122 -0
  8. package/app/components/content/ComponentEmits.vue +43 -0
  9. package/app/components/content/ComponentExample.vue +247 -0
  10. package/app/components/content/ComponentProps.vue +105 -0
  11. package/app/components/content/ComponentPropsLinks.vue +20 -0
  12. package/app/components/content/ComponentPropsSchema.vue +55 -0
  13. package/app/components/content/ComponentSlots.vue +50 -0
  14. package/app/components/content/HeroBackground.vue +65 -0
  15. package/app/components/content/HighlightInlineType.vue +39 -0
  16. package/app/components/content/Motion.vue +21 -0
  17. package/app/components/header/Header.vue +60 -0
  18. package/app/components/header/HeaderBody.vue +19 -0
  19. package/app/components/header/HeaderBottom.vue +26 -0
  20. package/app/components/header/HeaderLogo.vue +57 -0
  21. package/app/components/theme-picker/ThemePicker.vue +152 -0
  22. package/app/components/theme-picker/ThemePickerButton.vue +37 -0
  23. package/app/composables/fetchComponentExample.ts +32 -0
  24. package/app/composables/fetchComponentMeta.ts +34 -0
  25. package/app/composables/useCategory.ts +5 -0
  26. package/app/composables/useHeader.ts +6 -0
  27. package/app/composables/useNavigation.ts +89 -0
  28. package/app/error.vue +59 -0
  29. package/app/layouts/default.vue +3 -0
  30. package/app/layouts/docs.vue +31 -0
  31. package/app/pages/docs/[...slug].vue +158 -0
  32. package/app/pages/index.vue +30 -0
  33. package/app/pages/releases.vue +92 -0
  34. package/app/plugins/prettier.ts +67 -0
  35. package/app/plugins/theme.ts +82 -0
  36. package/app/types/index.d.ts +37 -0
  37. package/app/workers/prettier.js +36 -0
  38. package/content.config.ts +68 -0
  39. package/modules/component-example.ts +128 -0
  40. package/modules/component-meta.ts +22 -0
  41. package/modules/config.ts +79 -0
  42. package/modules/css.ts +33 -0
  43. package/nuxt.config.ts +80 -0
  44. package/package.json +55 -0
  45. package/server/api/component-example.get.ts +19 -0
  46. package/server/plugins/llms.ts +24 -0
  47. package/server/routes/raw/[...slug].md.get.ts +27 -0
  48. package/utils/git.ts +108 -0
  49. package/utils/meta.ts +29 -0
@@ -0,0 +1,247 @@
1
+ <script setup lang="ts">
2
+ import type { ChipProps } from '@nuxt/ui'
3
+ import { camelCase } from 'scule'
4
+ import { hash } from 'ohash'
5
+ import { useElementSize } from '@vueuse/core'
6
+ import { get, set } from '#ui/utils'
7
+
8
+ const { preview = true, source = true, prettier = true, ...props } = defineProps<{
9
+ name: string
10
+ class?: any
11
+ /**
12
+ * Whether to render the component in an iframe
13
+ * @defaultValue false
14
+ */
15
+ iframe?: boolean | { [key: string]: any }
16
+ /**
17
+ * Whether to display the component in a mobile-sized iframe viewport
18
+ * @defaultValue false
19
+ */
20
+ iframeMobile?: boolean
21
+ props?: { [key: string]: any }
22
+ /**
23
+ * Whether to format the code with Prettier
24
+ * @defaultValue false
25
+ */
26
+ prettier?: boolean
27
+ /**
28
+ * Whether to collapse the code block
29
+ * @defaultValue false
30
+ */
31
+ collapse?: boolean | {
32
+ icon?: string
33
+ name?: string
34
+ openText?: string
35
+ closeText?: string
36
+ open?: boolean
37
+ }
38
+ /**
39
+ * Whether to show the preview
40
+ * When `false`, the filename will be shown instead
41
+ * @defaultValue true
42
+ */
43
+ preview?: boolean
44
+ /**
45
+ * Whether to show the source code
46
+ * @defaultValue true
47
+ */
48
+ source?: boolean
49
+ /**
50
+ * A list of variable props to link to the component.
51
+ */
52
+ options?: Array<{
53
+ alias?: string
54
+ name: string
55
+ label: string
56
+ items?: any[]
57
+ default: any
58
+ multiple?: boolean
59
+ }>
60
+ /**
61
+ * A list of line numbers to highlight in the code block
62
+ */
63
+ highlights?: number[]
64
+ /**
65
+ * Whether to add overflow-hidden to wrapper
66
+ */
67
+ overflowHidden?: boolean
68
+ }>()
69
+
70
+ const slots = defineSlots<{
71
+ options(props?: {}): any
72
+ code(props?: {}): any
73
+ }>()
74
+
75
+ const el = ref<HTMLElement | null>(null)
76
+ const { $prettier } = useNuxtApp()
77
+
78
+ const { width } = useElementSize(el)
79
+
80
+ const camelName = camelCase(props.name)
81
+
82
+ const data = await fetchComponentExample(camelName)
83
+
84
+ const componentProps = reactive({ ...(props.props || {}) })
85
+
86
+ const code = computed(() => {
87
+ let code = ''
88
+
89
+ if (props.collapse) {
90
+ // 构建 code-collapse 的属性
91
+ const collapseAttrs = typeof props.collapse === 'object'
92
+ ? Object.entries(props.collapse)
93
+ .map(([key, value]) => {
94
+ if (typeof value === 'boolean') {
95
+ return value ? key : ''
96
+ }
97
+ return `${key}="${value}"`
98
+ })
99
+ .filter(Boolean)
100
+ .join(' ')
101
+ : ''
102
+
103
+ code += `::code-collapse${collapseAttrs ? `{${collapseAttrs}}` : ''}
104
+ `
105
+ }
106
+
107
+ code += `\`\`\`vue ${preview ? '' : ` [${data.pascalName}.vue]`}${props.highlights?.length ? `{${props.highlights.join('-')}}` : ''}
108
+ ${data?.code ?? ''}
109
+ \`\`\``
110
+
111
+ if (props.collapse) {
112
+ code += `
113
+ ::`
114
+ }
115
+
116
+ return code
117
+ })
118
+
119
+ const { data: ast } = await useAsyncData(`component-example-${camelName}${hash({ props: componentProps, collapse: props.collapse })}`, async () => {
120
+ if (!prettier) {
121
+ return parseMarkdown(code.value)
122
+ }
123
+
124
+ let formatted = ''
125
+ try {
126
+ formatted = await $prettier.format(code.value, {
127
+ trailingComma: 'none',
128
+ semi: false,
129
+ singleQuote: true,
130
+ printWidth: 100
131
+ })
132
+ } catch {
133
+ formatted = code.value
134
+ }
135
+
136
+ return parseMarkdown(formatted)
137
+ }, { watch: [code] })
138
+
139
+ const optionsValues = ref(props.options?.reduce((acc, option) => {
140
+ if (option.name) {
141
+ acc[option.alias || option.name] = option.default
142
+ }
143
+ if (option.name.toLowerCase().endsWith('color') && option.items?.length) {
144
+ option.items = option.items.map((item: any) => ({
145
+ label: item,
146
+ value: item,
147
+ chip: { color: item }
148
+ }))
149
+ }
150
+ return acc
151
+ }, {} as Record<string, any>) || {})
152
+
153
+ const urlSearchParams = computed(() => {
154
+ const params = {
155
+ ...optionsValues.value,
156
+ ...componentProps
157
+ }
158
+
159
+ if (!props.iframeMobile) {
160
+ params.width = Math.round(width.value).toString()
161
+ }
162
+
163
+ return new URLSearchParams(params).toString()
164
+ })
165
+ </script>
166
+
167
+ <template>
168
+ <div ref="el" class="my-5" :style="{ '--ui-header-height': '4rem' }">
169
+ <template v-if="preview">
170
+ <div class="border border-muted relative z-[1]" :class="[{ 'border-b-0 rounded-t-md': source, 'rounded-md': !source, 'overflow-hidden': props.overflowHidden }]">
171
+ <div v-if="props.options?.length || !!slots.options" class="flex gap-4 p-4 border-b border-muted">
172
+ <slot name="options" />
173
+
174
+ <UFormField
175
+ v-for="option in props.options"
176
+ :key="option.name"
177
+ :label="option.label"
178
+ :name="option.name"
179
+ size="sm"
180
+ class="inline-flex ring ring-accented rounded-sm"
181
+ :ui="{
182
+ wrapper: 'bg-elevated/50 rounded-l-sm flex border-r border-accented',
183
+ label: 'text-muted px-2 py-1.5',
184
+ container: 'mt-0'
185
+ }"
186
+ >
187
+ <USelectMenu
188
+ v-if="option.items?.length"
189
+ :model-value="get(optionsValues, option.name)"
190
+ :items="option.items"
191
+ :search-input="false"
192
+ :value-key="option.name.toLowerCase().endsWith('color') ? 'value' : undefined"
193
+ color="neutral"
194
+ variant="soft"
195
+ class="rounded-sm rounded-l-none min-w-12"
196
+ :multiple="option.multiple"
197
+ :class="[option.name.toLowerCase().endsWith('color') && 'pl-6']"
198
+ :ui="{ itemLeadingChip: 'size-2' }"
199
+ @update:model-value="set(optionsValues, option.name, $event)"
200
+ >
201
+ <template v-if="option.name.toLowerCase().endsWith('color')" #leading="{ modelValue, ui }">
202
+ <UChip
203
+ inset
204
+ standalone
205
+ :color="(modelValue as any)"
206
+ :size="(ui.itemLeadingChipSize() as ChipProps['size'])"
207
+ class="size-2"
208
+ />
209
+ </template>
210
+ </USelectMenu>
211
+ <UInput
212
+ v-else
213
+ :model-value="get(optionsValues, option.name)"
214
+ color="neutral"
215
+ variant="soft"
216
+ :ui="{ base: 'rounded-sm rounded-l-none min-w-12' }"
217
+ @update:model-value="set(optionsValues, option.name, $event)"
218
+ />
219
+ </UFormField>
220
+ </div>
221
+
222
+ <iframe
223
+ v-if="iframe"
224
+ v-bind="typeof iframe === 'object' ? iframe : {}"
225
+ :src="`/examples/${name}?${urlSearchParams}`"
226
+ class="relative w-full"
227
+ :class="[props.class, !iframeMobile && 'lg:left-1/2 lg:-translate-x-1/2 lg:w-[1024px]']"
228
+ />
229
+ <div v-else class="flex justify-center p-4" :class="props.class">
230
+ <component :is="camelName" v-bind="{ ...componentProps, ...optionsValues }" />
231
+ </div>
232
+ </div>
233
+ </template>
234
+
235
+ <template v-if="source">
236
+ <div v-if="!!slots.code" class="[&_pre]:!rounded-t-none [&_div.my-5]:!mt-0">
237
+ <slot name="code" />
238
+ </div>
239
+ <MDCRenderer
240
+ v-else-if="ast"
241
+ :body="ast.body"
242
+ :data="ast.data"
243
+ class="[&_pre]:!rounded-t-none [&_div.my-5]:!mt-0"
244
+ />
245
+ </template>
246
+ </div>
247
+ </template>
@@ -0,0 +1,105 @@
1
+ <script setup lang="ts">
2
+ import type { ComponentMeta } from 'vue-component-meta'
3
+ import { camelCase, kebabCase } from 'scule'
4
+
5
+ const { ignore = [], slug } = defineProps<{
6
+ /**
7
+ * The slug of the component to fetch props for.
8
+ * @defaultValue route path's last segment
9
+ */
10
+ slug?: string
11
+ /**
12
+ * An array of prop names to ignore.
13
+ */
14
+ ignore?: string[]
15
+ }>()
16
+
17
+ const route = useRoute()
18
+
19
+ const componentName = camelCase(slug ?? route.path.split('/').pop() ?? '')
20
+ const meta = await fetchComponentMeta(componentName as any)
21
+
22
+ const metaProps: ComputedRef<ComponentMeta['props']> = computed(() => {
23
+ if (!meta?.meta?.props?.length) {
24
+ return []
25
+ }
26
+
27
+ return meta.meta.props.filter((prop) => {
28
+ return !ignore?.includes(prop.name)
29
+ }).map((prop) => {
30
+ if (prop.default) {
31
+ prop.default = prop.default.replace(' as never', '').replace(/^"(.*)"$/, '\'$1\'')
32
+ } else {
33
+ const tag = prop.tags?.find(tag => tag.name === 'defaultValue')?.text
34
+ if (tag) {
35
+ prop.default = tag
36
+ }
37
+ }
38
+
39
+ // @ts-expect-error - Type is not correct
40
+ prop.type = !prop.type.startsWith('boolean') && prop.schema?.kind === 'enum' && Object.keys(prop.schema.schema)?.length ? Object.values(prop.schema.schema).map(schema => schema?.type ? schema.type : schema).join(' | ') : prop.type
41
+ return prop
42
+ }).sort((a, b) => {
43
+ if (a.name === 'as') {
44
+ return -1
45
+ }
46
+
47
+ if (b.name === 'as') {
48
+ return 1
49
+ }
50
+
51
+ if (a.name === 'ui') {
52
+ return 1
53
+ }
54
+
55
+ if (b.name === 'ui') {
56
+ return -1
57
+ }
58
+
59
+ return 0
60
+ })
61
+ })
62
+ </script>
63
+
64
+ <template>
65
+ <ProseTable>
66
+ <ProseThead>
67
+ <ProseTr>
68
+ <ProseTh>
69
+ Prop
70
+ </ProseTh>
71
+ <ProseTh>
72
+ Default
73
+ </ProseTh>
74
+ <ProseTh>
75
+ Type
76
+ </ProseTh>
77
+ </ProseTr>
78
+ </ProseThead>
79
+ <ProseTbody>
80
+ <ProseTr v-for="prop in metaProps" :key="prop.name">
81
+ <ProseTd>
82
+ <ProseCode>
83
+ {{ prop.name }}
84
+ </ProseCode>
85
+ </ProseTd>
86
+ <ProseTd>
87
+ <HighlightInlineType v-if="prop.default" :type="prop.default" />
88
+ </ProseTd>
89
+ <ProseTd>
90
+ <HighlightInlineType v-if="prop.type" :type="prop.type" />
91
+
92
+ <MDC
93
+ v-if="prop.description"
94
+ :value="prop.description"
95
+ class="text-toned mt-1"
96
+ :cache-key="`${kebabCase(route.path)}-${prop.name}-description`"
97
+ />
98
+
99
+ <ComponentPropsLinks v-if="prop.tags?.length" :prop="prop" />
100
+ <ComponentPropsSchema v-if="prop.schema" :prop="prop" :ignore="ignore" />
101
+ </ProseTd>
102
+ </ProseTr>
103
+ </ProseTbody>
104
+ </ProseTable>
105
+ </template>
@@ -0,0 +1,20 @@
1
+ <script setup lang="ts">
2
+ import { kebabCase } from 'scule'
3
+ import type { PropertyMeta } from 'vue-component-meta'
4
+
5
+ const props = defineProps<{
6
+ prop: PropertyMeta
7
+ }>()
8
+
9
+ const route = useRoute()
10
+
11
+ const links = computed(() => props.prop.tags?.filter((tag: any) => tag.name === 'link' || tag.name === 'see'))
12
+ </script>
13
+
14
+ <template>
15
+ <ProseUl v-if="links?.length">
16
+ <ProseLi v-for="(link, index) in links" :key="index">
17
+ <MDC :value="link.text ?? ''" class="my-1" :cache-key="`${kebabCase(route.path)}-${prop.name}-link-${index}`" />
18
+ </ProseLi>
19
+ </ProseUl>
20
+ </template>
@@ -0,0 +1,55 @@
1
+ <script setup lang="ts">
2
+ import { kebabCase } from 'scule'
3
+ import type { PropertyMeta } from 'vue-component-meta'
4
+
5
+ const props = defineProps<{
6
+ prop: PropertyMeta
7
+ ignore?: string[]
8
+ }>()
9
+
10
+ const route = useRoute()
11
+
12
+ function getSchemaProps(schema: PropertyMeta['schema']): any {
13
+ if (!schema || typeof schema === 'string' || !schema.schema) {
14
+ return []
15
+ }
16
+
17
+ if (schema.kind === 'object') {
18
+ return Object.values(schema.schema).filter(prop => !props.ignore?.includes(prop.name))
19
+ }
20
+
21
+ return (Array.isArray(schema.schema) ? schema.schema : Object.values(schema.schema)).flatMap(getSchemaProps as any)
22
+ }
23
+
24
+ const schemaProps = computed(() => {
25
+ return getSchemaProps(props.prop.schema).map((prop: any) => {
26
+ const defaultValue = prop.default ?? prop.tags?.find((tag: any) => tag.name === 'defaultValue')?.text
27
+ let description = prop.description
28
+ if (defaultValue) {
29
+ description = description ? `${description} Defaults to \`${defaultValue}\`{lang="ts-type"}.` : `Defaults to \`${defaultValue}\`{lang="ts-type"}.`
30
+ }
31
+
32
+ return {
33
+ ...prop,
34
+ description
35
+ }
36
+ })
37
+ })
38
+ </script>
39
+
40
+ <template>
41
+ <ProseCollapsible v-if="schemaProps?.length" class="mt-1 mb-0">
42
+ <ProseUl>
43
+ <ProseLi v-for="schemaProp in schemaProps" :key="schemaProp.name">
44
+ <HighlightInlineType :type="`${schemaProp.name}${schemaProp.required === false ? '?' : ''}: ${schemaProp.type}`" />
45
+
46
+ <MDC
47
+ v-if="schemaProp.description"
48
+ :value="schemaProp.description"
49
+ class="text-muted my-1"
50
+ :cache-key="`${kebabCase(route.path)}-${prop.name}-${schemaProp.name}-description`"
51
+ />
52
+ </ProseLi>
53
+ </ProseUl>
54
+ </ProseCollapsible>
55
+ </template>
@@ -0,0 +1,50 @@
1
+ <script setup lang="ts">
2
+ import { camelCase, kebabCase } from 'scule'
3
+
4
+ const props = defineProps<{
5
+ /**
6
+ * The slug of the component to fetch slots for.
7
+ * @defaultValue route path's last segment
8
+ */
9
+ slug?: string
10
+ }>()
11
+
12
+ const route = useRoute()
13
+
14
+ const componentName = camelCase(props.slug ?? route.path.split('/').pop() ?? '')
15
+ const meta = await fetchComponentMeta(componentName as any)
16
+ </script>
17
+
18
+ <template>
19
+ <ProseTable>
20
+ <ProseThead>
21
+ <ProseTr>
22
+ <ProseTh>
23
+ Slot
24
+ </ProseTh>
25
+ <ProseTh>
26
+ Type
27
+ </ProseTh>
28
+ </ProseTr>
29
+ </ProseThead>
30
+ <ProseTbody>
31
+ <ProseTr v-for="slot in (meta?.meta?.slots || [])" :key="slot.name">
32
+ <ProseTd>
33
+ <ProseCode>
34
+ {{ slot.name }}
35
+ </ProseCode>
36
+ </ProseTd>
37
+ <ProseTd>
38
+ <HighlightInlineType v-if="slot.type" :type="slot.type" />
39
+
40
+ <MDC
41
+ v-if="slot.description"
42
+ :value="slot.description"
43
+ class="text-toned mt-1"
44
+ :cache-key="`${kebabCase(route.path)}-${slot.name}-description`"
45
+ />
46
+ </ProseTd>
47
+ </ProseTr>
48
+ </ProseTbody>
49
+ </ProseTable>
50
+ </template>
@@ -0,0 +1,65 @@
1
+ <script setup lang="ts">
2
+ const { isLoading } = useLoadingIndicator()
3
+
4
+ const route = useRoute()
5
+ const heroBackgroundClass = computed(() => route.meta.heroBackground || '')
6
+ const appear = ref(false)
7
+ const appeared = ref(false)
8
+
9
+ onMounted(() => {
10
+ setTimeout(() => {
11
+ appear.value = true
12
+ setTimeout(() => {
13
+ appeared.value = true
14
+ }, 1000)
15
+ }, 0)
16
+ })
17
+ </script>
18
+
19
+ <template>
20
+ <div
21
+ class="absolute w-full -top-px transition-all text-primary shrink-0"
22
+ :class="[
23
+ isLoading ? 'animate-pulse' : (appear ? heroBackgroundClass : 'opacity-0'),
24
+ appeared ? 'duration-[400ms]' : 'duration-1000'
25
+ ]"
26
+ >
27
+ <svg
28
+ viewBox="0 0 1440 181"
29
+ fill="none"
30
+ xmlns="http://www.w3.org/2000/svg"
31
+ class="pointer-events-none"
32
+ >
33
+ <mask id="path-1-inside-1_414_5526" fill="white">
34
+ <path d="M0 0H1440V181H0V0Z" />
35
+ </mask>
36
+ <path d="M0 0H1440V181H0V0Z" fill="url(#paint0_linear_414_5526)" fill-opacity="0.22" />
37
+ <path d="M0 2H1440V-2H0V2Z" fill="url(#paint1_linear_414_5526)" mask="url(#path-1-inside-1_414_5526)" />
38
+ <defs>
39
+ <linearGradient
40
+ id="paint0_linear_414_5526"
41
+ x1="720"
42
+ y1="0"
43
+ x2="720"
44
+ y2="181"
45
+ gradientUnits="userSpaceOnUse"
46
+ >
47
+ <stop stop-color="currentColor" />
48
+ <stop offset="1" stop-color="currentColor" stop-opacity="0" />
49
+ </linearGradient>
50
+ <linearGradient
51
+ id="paint1_linear_414_5526"
52
+ x1="0"
53
+ y1="90.5"
54
+ x2="1440"
55
+ y2="90.5"
56
+ gradientUnits="userSpaceOnUse"
57
+ >
58
+ <stop stop-color="currentColor" stop-opacity="0" />
59
+ <stop offset="0.395" stop-color="currentColor" />
60
+ <stop offset="1" stop-color="currentColor" stop-opacity="0" />
61
+ </linearGradient>
62
+ </defs>
63
+ </svg>
64
+ </div>
65
+ </template>
@@ -0,0 +1,39 @@
1
+ <!-- https://github.com/nuxt/ui/blob/v4/docs/app/components/content/HighlightInlineType.vue -->
2
+ <script setup lang="ts">
3
+ import { hash } from 'ohash'
4
+
5
+ const props = defineProps<{
6
+ type: string
7
+ }>()
8
+
9
+ const type = computed(() => {
10
+ let type = props.type
11
+ if (type.includes(', "as" | "asChild" | "forceMount">')) {
12
+ type = type.replace(`, "as" | "asChild" | "forceMount">`, ``).replace('Omit<', '')
13
+ }
14
+ if (type.includes(', "as" | "asChild">')) {
15
+ type = type.replace(', "as" | "asChild">', '').replace('Omit<', '')
16
+ }
17
+ if (type.startsWith('undefined |')) {
18
+ type = type.replace('undefined |', '')
19
+ }
20
+ if (type.endsWith('| undefined')) {
21
+ type = type.replace('| undefined', '')
22
+ }
23
+
24
+ return type
25
+ })
26
+
27
+ const { data: ast } = await useAsyncData(`hightlight-inline-code-${hash(type.value).slice(0, 10)}`, () => parseMarkdown(`\`${type.value}\`{lang="ts-type"}`), {
28
+ watch: [type]
29
+ })
30
+ </script>
31
+
32
+ <template>
33
+ <ClientOnly>
34
+ <MDCRenderer v-if="ast?.body" :body="ast.body" :data="ast.data || {}" />
35
+ <template #fallback>
36
+ <ProseCode>{{ type }}</ProseCode>
37
+ </template>
38
+ </ClientOnly>
39
+ </template>
@@ -0,0 +1,21 @@
1
+ <script lang="ts" setup>
2
+ import type { Options } from 'motion-v'
3
+ import { Motion } from 'motion-v'
4
+
5
+ const {
6
+ initial = { scale: 1.1, opacity: 0, filter: 'blur(20px)' },
7
+ animate = { scale: 1, opacity: 1, filter: 'blur(0px)' },
8
+ transition = { duration: 0.6, delay: 0.1 }
9
+ } = defineProps<Options>()
10
+ </script>
11
+
12
+ <template>
13
+ <Motion
14
+ :initial="initial"
15
+ :animate="animate"
16
+ :transition="transition"
17
+ v-bind="$attrs"
18
+ >
19
+ <slot />
20
+ </Motion>
21
+ </template>
@@ -0,0 +1,60 @@
1
+ <script setup lang="ts">
2
+ import type { ButtonProps } from '@nuxt/ui'
3
+
4
+ const route = useRoute()
5
+ const { desktopLinks } = useHeader()
6
+ const { header, github } = useAppConfig()
7
+
8
+ const links = computed<ButtonProps[]>(() => github && github.url
9
+ ? [
10
+ {
11
+ 'icon': 'i-simple-icons-github',
12
+ 'to': github.url,
13
+ 'target': '_blank',
14
+ 'aria-label': 'GitHub'
15
+ },
16
+ ...header?.links || []
17
+ ]
18
+ : header.links)
19
+ </script>
20
+
21
+ <template>
22
+ <UHeader :ui="{ left: 'min-w-0' }" class="flex flex-col">
23
+ <template #left>
24
+ <HeaderLogo />
25
+ </template>
26
+
27
+ <UNavigationMenu :items="desktopLinks" variant="link" />
28
+
29
+ <template #right>
30
+ <ThemePicker />
31
+
32
+ <UTooltip text="Search" :kbds="['meta', 'K']">
33
+ <UContentSearchButton v-if="header?.search" />
34
+ </UTooltip>
35
+
36
+ <UTooltip text="Color Mode">
37
+ <UColorModeButton v-if="header?.colorMode" aria-label="Color Mode" />
38
+ </UTooltip>
39
+
40
+ <template v-if="links?.length">
41
+ <UTooltip
42
+ v-for="(link, count) in links"
43
+ :key="count"
44
+ :text="link.label || (link as any)['aria-label']"
45
+ class="hidden lg:flex"
46
+ >
47
+ <UButton v-bind="{ color: 'neutral', variant: 'ghost', ...link }" />
48
+ </UTooltip>
49
+ </template>
50
+ </template>
51
+
52
+ <template #body>
53
+ <HeaderBody />
54
+ </template>
55
+
56
+ <template v-if="route.path.startsWith('/docs/')" #bottom>
57
+ <HeaderBottom />
58
+ </template>
59
+ </UHeader>
60
+ </template>
@@ -0,0 +1,19 @@
1
+ <script setup lang="ts">
2
+ import type { ContentNavigationItem } from '@nuxt/content'
3
+
4
+ const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
5
+
6
+ const route = useRoute()
7
+ const { mobileLinks } = useHeader()
8
+ const { navigationByCategory } = useNavigation(navigation!)
9
+ </script>
10
+
11
+ <template>
12
+ <UNavigationMenu orientation="vertical" :items="mobileLinks" class="-mx-2.5" />
13
+
14
+ <template v-if="route.path.startsWith('/docs/')">
15
+ <USeparator type="dashed" class="mt-4 mb-6" />
16
+
17
+ <UContentNavigation :navigation="navigationByCategory" highlight :ui="{ linkTrailingBadge: 'font-semibold uppercase' }" />
18
+ </template>
19
+ </template>