@movk/nuxt-docs 1.13.0 → 1.14.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.
- package/app/app.config.ts +1 -1
- package/app/assets/css/main.css +16 -0
- package/app/assets/icons/LICENSE +14 -0
- package/app/assets/icons/ai.svg +1 -0
- package/app/components/OgImage/Nuxt.vue +2 -4
- package/app/components/content/CommitChangelog.vue +110 -28
- package/app/components/content/ComponentEmits.vue +1 -1
- package/app/components/content/ComponentExample.vue +98 -72
- package/app/components/content/ComponentProps.vue +3 -3
- package/app/components/content/ComponentPropsSchema.vue +1 -1
- package/app/components/content/ComponentSlots.vue +1 -1
- package/app/components/content/HighlightInlineType.vue +1 -1
- package/app/components/content/PageLastCommit.vue +6 -5
- package/app/components/header/HeaderLogo.vue +1 -1
- package/app/composables/cachedParseMarkdown.ts +12 -0
- package/app/composables/fetchComponentExample.ts +5 -22
- package/app/composables/fetchComponentMeta.ts +5 -22
- package/app/mdc.config.ts +12 -0
- package/app/pages/docs/[...slug].vue +8 -2
- package/app/templates/releases.vue +3 -1
- package/app/types/index.d.ts +1 -1
- package/app/utils/shiki-transformer-icon-highlight.ts +89 -0
- package/app/workers/prettier.js +26 -17
- package/modules/ai-chat/index.ts +1 -1
- package/modules/component-example.ts +65 -30
- package/modules/config.ts +24 -1
- package/modules/css.ts +1 -1
- package/nuxt.config.ts +40 -2
- package/nuxt.schema.ts +4 -4
- package/package.json +17 -17
- package/server/api/component-example.get.ts +5 -5
- package/server/api/github/{commits.get.ts → commits.json.get.ts} +7 -4
- package/server/api/github/{last-commit.get.ts → last-commit.json.get.ts} +12 -9
- package/server/api/github/releases.json.get.ts +28 -0
- package/server/mcp/resources/documentation-pages.ts +26 -0
- package/server/mcp/resources/examples.ts +17 -0
- package/server/mcp/tools/get-example.ts +1 -1
- package/server/mcp/tools/list-examples.ts +4 -8
- package/server/mcp/tools/list-getting-started-guides.ts +29 -0
- package/server/routes/raw/[...slug].md.get.ts +3 -5
- package/server/utils/stringifyMinimark.ts +345 -0
- package/server/utils/transformMDC.ts +14 -5
- package/utils/meta.ts +1 -1
package/app/app.config.ts
CHANGED
package/app/assets/css/main.css
CHANGED
|
@@ -22,3 +22,19 @@
|
|
|
22
22
|
:root {
|
|
23
23
|
--ui-container: var(--container-8xl);
|
|
24
24
|
}
|
|
25
|
+
|
|
26
|
+
/* Shiki icon highlight transformer styles */
|
|
27
|
+
.shiki-icon-highlight {
|
|
28
|
+
display: inline-block;
|
|
29
|
+
width: 1.25em;
|
|
30
|
+
height: 1.25em;
|
|
31
|
+
vertical-align: -0.25em;
|
|
32
|
+
margin-right: 0.125em;
|
|
33
|
+
background-color: var(--ui-text-highlighted);
|
|
34
|
+
-webkit-mask-repeat: no-repeat;
|
|
35
|
+
mask-repeat: no-repeat;
|
|
36
|
+
-webkit-mask-size: 100% 100%;
|
|
37
|
+
mask-size: 100% 100%;
|
|
38
|
+
-webkit-mask-image: var(--shiki-icon-url);
|
|
39
|
+
mask-image: var(--shiki-icon-url);
|
|
40
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Copyright © Nucleo
|
|
2
|
+
|
|
3
|
+
Version 1.3, January 3, 2024
|
|
4
|
+
|
|
5
|
+
Nucleo Icons
|
|
6
|
+
|
|
7
|
+
https://nucleoapp.com/
|
|
8
|
+
|
|
9
|
+
- Redistribution of icons is prohibited.
|
|
10
|
+
- Icons are restricted for use only within the product they are bundled with.
|
|
11
|
+
|
|
12
|
+
For more details:
|
|
13
|
+
|
|
14
|
+
https://nucleoapp.com/license
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" id="artificial-intelligence" aria-hidden="true" viewBox="0 0 20 20"><title>artificial-intelligence</title><g fill="currentColor"><polyline fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" points="10.5 15 7 5 6 5 2.5 15"/><line x1="9.586" x2="3.414" y1="12.5" y2="12.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/><polyline fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" points="13.5 9 15.5 9 15.5 15"/><line x1="13.5" x2="17.5" y1="15" y2="15" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/><path fill="currentColor" stroke-width="0" d="m17.6527,3.9647l-1.2005-.4503-.4506-1.2005c-.1562-.4185-.8468-.4185-1.0031,0l-.4506,1.2005-1.2005.4503c-.2086.0785-.3474.2783-.3474.5015s.1388.4231.3474.5015l1.2005.4503.4506,1.2005c.0781.2093.2783.3477.5015.3477s.4234-.1385.5015-.3477l.4506-1.2005,1.2005-.4503c.2086-.0785.3474-.2783.3474-.5015s-.1388-.4231-.3474-.5015h0Z"/></g></svg>
|
|
@@ -3,16 +3,14 @@
|
|
|
3
3
|
* @credits NuxtLabs <https://nuxtlabs.com/>
|
|
4
4
|
* @see https://github.com/nuxt/nuxt.com/blob/main/components/OgImage/Docs.vue
|
|
5
5
|
*/
|
|
6
|
-
import { computed } from 'vue'
|
|
7
|
-
|
|
8
6
|
const {
|
|
9
7
|
title = 'title',
|
|
10
8
|
description = 'description',
|
|
11
9
|
headline = 'headline'
|
|
12
10
|
} = defineProps<{ title?: string, description?: string, headline?: string }>()
|
|
13
11
|
|
|
14
|
-
const computedTitle =
|
|
15
|
-
const computedDescription =
|
|
12
|
+
const computedTitle = (title || '').slice(0, 60)
|
|
13
|
+
const computedDescription = (description || '').slice(0, 200)
|
|
16
14
|
</script>
|
|
17
15
|
|
|
18
16
|
<template>
|
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { camelCase, kebabCase, upperFirst } from '
|
|
2
|
+
import { camelCase, kebabCase, upperFirst } from 'scule'
|
|
3
3
|
|
|
4
4
|
interface Commit {
|
|
5
5
|
sha: string
|
|
6
|
+
date: string
|
|
6
7
|
message: string
|
|
7
8
|
}
|
|
8
9
|
|
|
10
|
+
interface Release {
|
|
11
|
+
tag_name: string
|
|
12
|
+
published_at: string
|
|
13
|
+
html_url: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ReleaseGroup {
|
|
17
|
+
tag: string
|
|
18
|
+
url?: string
|
|
19
|
+
icon?: string
|
|
20
|
+
title: string
|
|
21
|
+
commits: Commit[]
|
|
22
|
+
published_at?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
9
25
|
const props = defineProps<{
|
|
10
26
|
/**
|
|
11
27
|
* 仓库中的文件路径
|
|
@@ -45,7 +61,6 @@ const SHA_SHORT_LENGTH = 5
|
|
|
45
61
|
const { github } = useAppConfig()
|
|
46
62
|
const route = useRoute()
|
|
47
63
|
|
|
48
|
-
// 计算文件路径相关的值
|
|
49
64
|
const routeName = computed(() => route.path.split('/').pop() ?? '')
|
|
50
65
|
const githubUrl = computed(() => (github && typeof github === 'object' ? github.url : ''))
|
|
51
66
|
|
|
@@ -55,7 +70,6 @@ const filePath = computed(() => {
|
|
|
55
70
|
const fileExtension = props.suffix ?? (github && typeof github === 'object' ? github.suffix : 'vue')
|
|
56
71
|
const fileName = props.name ?? routeName.value
|
|
57
72
|
|
|
58
|
-
// 根据 casing 参数转换文件名
|
|
59
73
|
const transformedName = (() => {
|
|
60
74
|
const casing = props.casing ?? (github && typeof github === 'object' ? github.casing : undefined) ?? 'auto'
|
|
61
75
|
|
|
@@ -77,43 +91,111 @@ const filePath = computed(() => {
|
|
|
77
91
|
return `${basePath}/${filePrefix}${transformedName}.${fileExtension}`
|
|
78
92
|
})
|
|
79
93
|
|
|
80
|
-
const { data: commits } =
|
|
94
|
+
const { data: commits } = useLazyFetch<Commit[]>('/api/github/commits.json', {
|
|
81
95
|
key: `commit-changelog-${props.name ?? routeName.value}-${props.author ?? 'all'}`,
|
|
82
|
-
query: { path: [filePath.value], author: props.author }
|
|
96
|
+
query: { path: [filePath.value], author: props.author },
|
|
97
|
+
server: false,
|
|
98
|
+
getCachedData: (key, nuxtApp) => nuxtApp.payload.data[key]
|
|
83
99
|
})
|
|
84
100
|
|
|
85
|
-
|
|
86
|
-
|
|
101
|
+
const { data: releases } = useLazyFetch<Release[]>('/api/github/releases.json', {
|
|
102
|
+
server: false,
|
|
103
|
+
getCachedData: (key, nuxtApp) => nuxtApp.payload.data[key]
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const groupedByRelease = computed<ReleaseGroup[]>(() => {
|
|
87
107
|
if (!commits.value?.length) return []
|
|
88
108
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
109
|
+
const sortedReleases = (releases.value ?? [])
|
|
110
|
+
.filter(r => r.published_at)
|
|
111
|
+
.sort((a, b) => new Date(b.published_at).getTime() - new Date(a.published_at).getTime())
|
|
112
|
+
|
|
113
|
+
const releasesOldestFirst = [...sortedReleases].reverse()
|
|
114
|
+
const groups: ReleaseGroup[] = []
|
|
115
|
+
const unreleased: Commit[] = []
|
|
116
|
+
|
|
117
|
+
for (const commit of commits.value) {
|
|
118
|
+
const commitDate = new Date(commit.date).getTime()
|
|
119
|
+
const release = releasesOldestFirst.find(r => new Date(r.published_at).getTime() >= commitDate)
|
|
120
|
+
|
|
121
|
+
if (release) {
|
|
122
|
+
const majorTag = release.tag_name.replace(/-(alpha|beta|rc)\.\d+$/, '')
|
|
123
|
+
let group = groups.find(g => g.tag === majorTag)
|
|
124
|
+
if (!group) {
|
|
125
|
+
group = { tag: majorTag, title: majorTag, icon: 'i-lucide-tag', published_at: release.published_at, url: release.html_url, commits: [] }
|
|
126
|
+
groups.push(group)
|
|
127
|
+
}
|
|
128
|
+
if (new Date(release.published_at) > new Date(group.published_at!)) {
|
|
129
|
+
group.published_at = release.published_at
|
|
130
|
+
group.url = release.html_url
|
|
131
|
+
}
|
|
132
|
+
group.commits.push(commit)
|
|
133
|
+
} else {
|
|
134
|
+
unreleased.push(commit)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const result: ReleaseGroup[] = []
|
|
139
|
+
if (unreleased.length) {
|
|
140
|
+
result.push({ tag: 'unreleased', title: 'Soon', icon: 'i-lucide-tag', commits: unreleased })
|
|
141
|
+
}
|
|
92
142
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
.replace(/`(.*?)`/g, '<code class="text-xs">$1</code>')
|
|
143
|
+
const uniqueTags = [...new Set(sortedReleases.map(r => r.tag_name.replace(/-(alpha|beta|rc)\.\d+$/, '')))]
|
|
144
|
+
groups.sort((a, b) => uniqueTags.indexOf(a.tag) - uniqueTags.indexOf(b.tag))
|
|
145
|
+
result.push(...groups)
|
|
97
146
|
|
|
98
|
-
|
|
99
|
-
sha: commit.sha,
|
|
100
|
-
formatted: `${commitLink} — ${content}`
|
|
101
|
-
}
|
|
102
|
-
})
|
|
147
|
+
return result
|
|
103
148
|
})
|
|
149
|
+
|
|
150
|
+
function normalizeCommitMessage(commit: Commit) {
|
|
151
|
+
const prefix = `[\`${commit.sha.slice(0, SHA_SHORT_LENGTH)}\`](${githubUrl.value}/commit/${commit.sha})`
|
|
152
|
+
const content = commit.message
|
|
153
|
+
.replace(/#(\d+)/g, `<a href='${githubUrl.value}/issues/$1'>#$1</a>`)
|
|
154
|
+
.replace(/`(.*?)`/g, '<code class="text-xs">$1</code>')
|
|
155
|
+
|
|
156
|
+
return `${prefix} — ${content}`
|
|
157
|
+
}
|
|
104
158
|
</script>
|
|
105
159
|
|
|
106
160
|
<template>
|
|
107
|
-
<div v-if="!
|
|
161
|
+
<div v-if="!commits?.length">
|
|
108
162
|
No recent changes
|
|
109
163
|
</div>
|
|
110
164
|
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
165
|
+
<UTimeline
|
|
166
|
+
v-else
|
|
167
|
+
:items="groupedByRelease"
|
|
168
|
+
size="xs"
|
|
169
|
+
:ui="{ root: '', wrapper: 'mt-0 pb-0', title: 'mb-1.5 flex items-center justify-between' }"
|
|
170
|
+
>
|
|
171
|
+
<template #title="{ item }">
|
|
172
|
+
<UBadge
|
|
173
|
+
v-if="item.tag === 'unreleased'"
|
|
174
|
+
color="neutral"
|
|
175
|
+
variant="subtle"
|
|
176
|
+
:label="item.title"
|
|
177
|
+
class="w-12.5 justify-center"
|
|
178
|
+
/>
|
|
179
|
+
<NuxtLink
|
|
180
|
+
v-else
|
|
181
|
+
:to="item.url"
|
|
182
|
+
target="_blank"
|
|
183
|
+
class="hover:underline"
|
|
184
|
+
>
|
|
185
|
+
<UBadge variant="subtle" :label="item.tag" />
|
|
186
|
+
</NuxtLink>
|
|
187
|
+
|
|
188
|
+
<time v-if="item.published_at" :datetime="item.published_at" class="text-xs text-dimmed font-normal">
|
|
189
|
+
{{ useTimeAgo(new Date(item.published_at)) }}
|
|
190
|
+
</time>
|
|
191
|
+
</template>
|
|
192
|
+
|
|
193
|
+
<template #description="{ item }">
|
|
194
|
+
<ul class="flex flex-col gap-1.5">
|
|
195
|
+
<li v-for="commit of item.commits" :key="commit.sha">
|
|
196
|
+
<MDC :value="normalizeCommitMessage(commit)" class="text-sm [&_code]:text-xs" unwrap="p" />
|
|
197
|
+
</li>
|
|
198
|
+
</ul>
|
|
199
|
+
</template>
|
|
200
|
+
</UTimeline>
|
|
119
201
|
</template>
|
|
@@ -12,7 +12,7 @@ const props = defineProps<{
|
|
|
12
12
|
const route = useRoute()
|
|
13
13
|
const componentName = camelCase(props.slug ?? route.path.split('/').pop() ?? '')
|
|
14
14
|
|
|
15
|
-
const meta = await
|
|
15
|
+
const { data: meta } = await useFetchComponentMeta(componentName as any)
|
|
16
16
|
</script>
|
|
17
17
|
|
|
18
18
|
<template>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { ChipProps } from '@nuxt/ui'
|
|
3
|
-
import { camelCase } from 'scule'
|
|
3
|
+
import { camelCase, upperFirst } from 'scule'
|
|
4
4
|
import { hash } from 'ohash'
|
|
5
5
|
import { useElementSize } from '@vueuse/core'
|
|
6
6
|
import { get, set } from '#ui/utils'
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const props = withDefaults(defineProps<{
|
|
9
9
|
name: string
|
|
10
10
|
class?: any
|
|
11
11
|
/**
|
|
@@ -50,6 +50,7 @@ const { preview = true, source = true, prettier = false, ...props } = defineProp
|
|
|
50
50
|
* 链接到组件的可变属性列表
|
|
51
51
|
*/
|
|
52
52
|
options?: Array<{
|
|
53
|
+
type?: string
|
|
53
54
|
alias?: string
|
|
54
55
|
name: string
|
|
55
56
|
label: string
|
|
@@ -65,7 +66,21 @@ const { preview = true, source = true, prettier = false, ...props } = defineProp
|
|
|
65
66
|
* 是否在包装器上添加 overflow-hidden
|
|
66
67
|
*/
|
|
67
68
|
overflowHidden?: boolean
|
|
68
|
-
|
|
69
|
+
/**
|
|
70
|
+
* 是否添加 background-elevated 到 wrapper
|
|
71
|
+
*/
|
|
72
|
+
elevated?: boolean
|
|
73
|
+
lang?: string
|
|
74
|
+
/**
|
|
75
|
+
* 覆盖用于代码块的文件名
|
|
76
|
+
*/
|
|
77
|
+
filename?: string
|
|
78
|
+
}>(), {
|
|
79
|
+
preview: true,
|
|
80
|
+
source: true,
|
|
81
|
+
prettier: false,
|
|
82
|
+
lang: 'vue'
|
|
83
|
+
})
|
|
69
84
|
|
|
70
85
|
const slots = defineSlots<{
|
|
71
86
|
options(props?: {}): any
|
|
@@ -79,7 +94,11 @@ const { width } = useElementSize(el)
|
|
|
79
94
|
|
|
80
95
|
const camelName = camelCase(props.name)
|
|
81
96
|
|
|
82
|
-
const
|
|
97
|
+
const exampleModules = import.meta.glob('~/components/content/examples/**/*.vue')
|
|
98
|
+
const exampleMatch = Object.entries(exampleModules).find(([path]) => path.endsWith(`/${upperFirst(camelName)}.vue`))
|
|
99
|
+
const resolvedComponent = exampleMatch ? defineAsyncComponent(exampleMatch[1] as any) : undefined
|
|
100
|
+
|
|
101
|
+
const { data } = await useFetchComponentExample(camelName)
|
|
83
102
|
|
|
84
103
|
const componentProps = reactive({ ...(props.props || {}) })
|
|
85
104
|
|
|
@@ -87,7 +106,6 @@ const code = computed(() => {
|
|
|
87
106
|
let code = ''
|
|
88
107
|
|
|
89
108
|
if (props.collapse) {
|
|
90
|
-
// 构建 code-collapse 的属性
|
|
91
109
|
const collapseAttrs = typeof props.collapse === 'object'
|
|
92
110
|
? Object.entries(props.collapse)
|
|
93
111
|
.map(([key, value]) => {
|
|
@@ -104,8 +122,8 @@ const code = computed(() => {
|
|
|
104
122
|
`
|
|
105
123
|
}
|
|
106
124
|
|
|
107
|
-
code +=
|
|
108
|
-
${data?.code ?? ''}
|
|
125
|
+
code += `\`\`\`${props.lang} ${props.preview ? '' : ` [${props.filename ?? data.value?.pascalName}.${props.lang}]`}${props.highlights?.length ? `{${props.highlights.join('-')}}` : ''}
|
|
126
|
+
${data.value?.code ?? ''}
|
|
109
127
|
\`\`\``
|
|
110
128
|
|
|
111
129
|
if (props.collapse) {
|
|
@@ -116,9 +134,9 @@ ${data?.code ?? ''}
|
|
|
116
134
|
return code
|
|
117
135
|
})
|
|
118
136
|
|
|
119
|
-
const { data: ast } =
|
|
120
|
-
if (!prettier) {
|
|
121
|
-
return
|
|
137
|
+
const { data: ast } = useAsyncData(`component-example-${camelName}${hash({ props: componentProps, collapse: props.collapse })}`, async () => {
|
|
138
|
+
if (!props.prettier) {
|
|
139
|
+
return cachedParseMarkdown(code.value)
|
|
122
140
|
}
|
|
123
141
|
|
|
124
142
|
let formatted = ''
|
|
@@ -133,8 +151,8 @@ const { data: ast } = await useAsyncData(`component-example-${camelName}${hash({
|
|
|
133
151
|
formatted = code.value
|
|
134
152
|
}
|
|
135
153
|
|
|
136
|
-
return
|
|
137
|
-
}, { watch: [code] })
|
|
154
|
+
return cachedParseMarkdown(formatted)
|
|
155
|
+
}, { lazy: import.meta.client, watch: [code] })
|
|
138
156
|
|
|
139
157
|
const optionsValues = ref(props.options?.reduce((acc, option) => {
|
|
140
158
|
if (option.name) {
|
|
@@ -167,72 +185,80 @@ const urlSearchParams = computed(() => {
|
|
|
167
185
|
<template>
|
|
168
186
|
<div ref="el" class="my-5" :style="{ '--ui-header-height': '4rem' }">
|
|
169
187
|
<template v-if="preview">
|
|
170
|
-
<div
|
|
171
|
-
<div
|
|
172
|
-
<
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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="{ 'pl-6': option.name.toLowerCase().endsWith('color') }"
|
|
198
|
-
:ui="{ itemLeadingChip: 'w-2' }"
|
|
199
|
-
@update:model-value="set(optionsValues, option.name, $event)"
|
|
188
|
+
<div ref="wrapperContainer" class="relative group/component">
|
|
189
|
+
<div class="border border-muted relative z-1" :class="[{ 'border-b-0 rounded-t-md': props.source, 'rounded-md': !props.source, 'overflow-hidden': props.overflowHidden }]">
|
|
190
|
+
<div v-if="props.options?.length || !!slots.options" class="flex gap-4 p-4 border-b border-muted">
|
|
191
|
+
<slot name="options" />
|
|
192
|
+
|
|
193
|
+
<UFormField
|
|
194
|
+
v-for="option in props.options"
|
|
195
|
+
:key="option.name"
|
|
196
|
+
:label="option.label"
|
|
197
|
+
:name="option.name"
|
|
198
|
+
size="sm"
|
|
199
|
+
class="inline-flex ring ring-accented rounded-sm"
|
|
200
|
+
:ui="{
|
|
201
|
+
wrapper: 'bg-elevated/50 rounded-l-sm flex border-r border-accented',
|
|
202
|
+
label: 'text-muted px-2 py-1.5',
|
|
203
|
+
container: 'mt-0'
|
|
204
|
+
}"
|
|
200
205
|
>
|
|
201
|
-
<
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
206
|
+
<USelectMenu
|
|
207
|
+
v-if="option.items?.length"
|
|
208
|
+
:model-value="get(optionsValues, option.name)"
|
|
209
|
+
:items="option.items"
|
|
210
|
+
:search-input="false"
|
|
211
|
+
:value-key="option.name.toLowerCase().endsWith('color') ? 'value' : undefined"
|
|
212
|
+
color="neutral"
|
|
213
|
+
variant="soft"
|
|
214
|
+
class="rounded-sm rounded-l-none min-w-12"
|
|
215
|
+
:multiple="option.multiple"
|
|
216
|
+
:class="[option.name.toLowerCase().endsWith('color') && 'pl-6']"
|
|
217
|
+
:ui="{ itemLeadingChip: 'w-2' }"
|
|
218
|
+
@update:model-value="set(optionsValues, option.name, $event)"
|
|
219
|
+
>
|
|
220
|
+
<template v-if="option.name.toLowerCase().endsWith('color')" #leading="{ modelValue, ui }">
|
|
221
|
+
<UChip
|
|
222
|
+
inset
|
|
223
|
+
standalone
|
|
224
|
+
:color="(modelValue as any)"
|
|
225
|
+
:size="(ui.itemLeadingChipSize() as ChipProps['size'])"
|
|
226
|
+
class="size-2"
|
|
227
|
+
/>
|
|
228
|
+
</template>
|
|
229
|
+
</USelectMenu>
|
|
230
|
+
<UInput
|
|
231
|
+
v-else
|
|
232
|
+
:model-value="get(optionsValues, option.name)"
|
|
233
|
+
:type="option.type"
|
|
234
|
+
color="neutral"
|
|
235
|
+
variant="soft"
|
|
236
|
+
:ui="{ base: 'rounded-sm rounded-l-none min-w-12' }"
|
|
237
|
+
@update:model-value="set(optionsValues, option.name, $event)"
|
|
238
|
+
/>
|
|
239
|
+
</UFormField>
|
|
240
|
+
</div>
|
|
221
241
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
242
|
+
<iframe
|
|
243
|
+
v-if="iframe"
|
|
244
|
+
v-bind="typeof iframe === 'object' ? iframe : {}"
|
|
245
|
+
:src="`/examples/${name}?${urlSearchParams}`"
|
|
246
|
+
class="relative w-full"
|
|
247
|
+
:class="[props.class, { 'dark:bg-neutral-950/50 rounded-t-md': props.elevated }, !iframeMobile && 'lg:left-1/2 lg:-translate-x-1/2 lg:w-[1024px]']"
|
|
248
|
+
/>
|
|
249
|
+
<div
|
|
250
|
+
v-else-if="resolvedComponent"
|
|
251
|
+
ref="componentContainer"
|
|
252
|
+
class="flex justify-center p-4"
|
|
253
|
+
:class="[props.class, { 'dark:bg-neutral-950/50 rounded-t-md': props.elevated }]"
|
|
254
|
+
>
|
|
255
|
+
<component :is="resolvedComponent" v-bind="{ ...componentProps, ...optionsValues }" />
|
|
256
|
+
</div>
|
|
231
257
|
</div>
|
|
232
258
|
</div>
|
|
233
259
|
</template>
|
|
234
260
|
|
|
235
|
-
<template v-if="source">
|
|
261
|
+
<template v-if="props.source">
|
|
236
262
|
<div v-if="!!slots.code" class="[&_pre]:rounded-t-none! [&_div.my-5]:mt-0!">
|
|
237
263
|
<slot name="code" />
|
|
238
264
|
</div>
|
|
@@ -45,14 +45,14 @@ const { ignore = [
|
|
|
45
45
|
const route = useRoute()
|
|
46
46
|
const camelName = camelCase(slug ?? route.path.split('/').pop() ?? '')
|
|
47
47
|
const componentName = prose ? `Prose${upperFirst(camelName)}` : `${upperFirst(camelName)}`
|
|
48
|
-
const meta = await
|
|
48
|
+
const { data: meta } = await useFetchComponentMeta(componentName as any)
|
|
49
49
|
|
|
50
50
|
const metaProps: ComputedRef<ComponentMeta['props']> = computed(() => {
|
|
51
|
-
if (!meta?.meta?.props?.length) {
|
|
51
|
+
if (!meta.value?.meta?.props?.length) {
|
|
52
52
|
return []
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
return meta.meta.props.filter((prop) => {
|
|
55
|
+
return meta.value.meta.props.filter((prop) => {
|
|
56
56
|
return !ignore?.includes(prop.name)
|
|
57
57
|
}).map((prop) => {
|
|
58
58
|
if (prop.default) {
|
|
@@ -48,7 +48,7 @@ const schemaProps = computed(() => {
|
|
|
48
48
|
</script>
|
|
49
49
|
|
|
50
50
|
<template>
|
|
51
|
-
<ProseCollapsible v-if="schemaProps?.length" class="mt-1 mb-0">
|
|
51
|
+
<ProseCollapsible v-if="schemaProps?.length" :unmount-on-hide="true" class="mt-1 mb-0">
|
|
52
52
|
<ProseUl>
|
|
53
53
|
<ProseLi v-for="schemaProp in schemaProps" :key="schemaProp.name">
|
|
54
54
|
<HighlightInlineType :type="`${schemaProp.name}${schemaProp.required === false ? '?' : ''}: ${schemaProp.type}`" />
|
|
@@ -12,7 +12,7 @@ const props = defineProps<{
|
|
|
12
12
|
const route = useRoute()
|
|
13
13
|
|
|
14
14
|
const componentName = camelCase(props.slug ?? route.path.split('/').pop() ?? '')
|
|
15
|
-
const meta = await
|
|
15
|
+
const { data: meta } = await useFetchComponentMeta(componentName as any)
|
|
16
16
|
</script>
|
|
17
17
|
|
|
18
18
|
<template>
|
|
@@ -24,7 +24,7 @@ const type = computed(() => {
|
|
|
24
24
|
const ast = ref<any>(null)
|
|
25
25
|
|
|
26
26
|
onMounted(async () => {
|
|
27
|
-
ast.value = await
|
|
27
|
+
ast.value = await cachedParseMarkdown(`\`\` ${type.value} \`\`{lang="ts-type"}`)
|
|
28
28
|
})
|
|
29
29
|
</script>
|
|
30
30
|
|
|
@@ -49,12 +49,12 @@ const filePath = computed(() => {
|
|
|
49
49
|
return [rootDir, 'content', `${stem}.${extension}`].filter(Boolean).join('/')
|
|
50
50
|
})
|
|
51
51
|
|
|
52
|
-
const { data: commit } =
|
|
52
|
+
const { data: commit } = useLazyFetch<LastCommit | null>('/api/github/last-commit.json', {
|
|
53
53
|
key: `last-commit-${filePath.value}`,
|
|
54
54
|
query: { path: filePath.value },
|
|
55
55
|
default: () => null,
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
server: false,
|
|
57
|
+
getCachedData: (key, nuxtApp) => nuxtApp.payload.data[key]
|
|
58
58
|
})
|
|
59
59
|
|
|
60
60
|
const commitUrl = computed(() => commit.value?.url ?? '')
|
|
@@ -81,6 +81,7 @@ const authorUrl = computed(() => {
|
|
|
81
81
|
:src="commit.author.avatar"
|
|
82
82
|
alt="Author Avatar"
|
|
83
83
|
size="2xs"
|
|
84
|
+
loading="lazy"
|
|
84
85
|
/>
|
|
85
86
|
<UBadge color="neutral" variant="outline" size="sm">
|
|
86
87
|
{{ commit.author.name || commit.author.login }}
|
|
@@ -103,7 +104,7 @@ const authorUrl = computed(() => {
|
|
|
103
104
|
v-if="commitUrl"
|
|
104
105
|
:to="commitUrl"
|
|
105
106
|
target="_blank"
|
|
106
|
-
class="hover:opacity-80 transition-opacity max-w-
|
|
107
|
+
class="hover:opacity-80 transition-opacity max-w-62.5"
|
|
107
108
|
>
|
|
108
109
|
<UBadge
|
|
109
110
|
color="neutral"
|
|
@@ -119,7 +120,7 @@ const authorUrl = computed(() => {
|
|
|
119
120
|
color="neutral"
|
|
120
121
|
variant="outline"
|
|
121
122
|
size="sm"
|
|
122
|
-
class="max-w-
|
|
123
|
+
class="max-w-62.5 font-mono text-xs"
|
|
123
124
|
>
|
|
124
125
|
<span class="truncate">{{ commit.message }}</span>
|
|
125
126
|
</UBadge>
|
|
@@ -4,6 +4,6 @@ const { header } = useAppConfig()
|
|
|
4
4
|
|
|
5
5
|
<template>
|
|
6
6
|
<NuxtLink :to="header.to">
|
|
7
|
-
<UUser :avatar="{ src: header.avatar, alt: 'Site Logo' }" :name="header.title" />
|
|
7
|
+
<UUser :avatar="{ src: header.avatar, alt: 'Site Logo', loading: 'lazy' }" :name="header.title" />
|
|
8
8
|
</NuxtLink>
|
|
9
9
|
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { markRaw } from 'vue'
|
|
2
|
+
|
|
3
|
+
const _cache = new Map<string, any>()
|
|
4
|
+
|
|
5
|
+
export async function cachedParseMarkdown(markdown: string) {
|
|
6
|
+
const cached = _cache.get(markdown)
|
|
7
|
+
if (cached) return cached
|
|
8
|
+
|
|
9
|
+
const result = markRaw(await parseMarkdown(markdown))
|
|
10
|
+
_cache.set(markdown, result)
|
|
11
|
+
return result
|
|
12
|
+
}
|
|
@@ -1,17 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export async function fetchComponentExample(name: string) {
|
|
4
|
-
const state = useComponentExampleState()
|
|
5
|
-
|
|
6
|
-
if (state.value[name]?.then) {
|
|
7
|
-
await state.value[name]
|
|
8
|
-
return state.value[name]
|
|
9
|
-
}
|
|
10
|
-
if (state.value[name]) {
|
|
11
|
-
return state.value[name]
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// Add to nitro prerender
|
|
1
|
+
export function useFetchComponentExample(name: string) {
|
|
15
2
|
if (import.meta.server) {
|
|
16
3
|
const event = useRequestEvent()
|
|
17
4
|
event?.node.res.setHeader(
|
|
@@ -20,13 +7,9 @@ export async function fetchComponentExample(name: string) {
|
|
|
20
7
|
)
|
|
21
8
|
}
|
|
22
9
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
state.value[name] = {}
|
|
10
|
+
return useAsyncData(`component-example-${name}`, () => $fetch(`/api/component-example/${name}.json`).catch(() => ({})), {
|
|
11
|
+
lazy: import.meta.client,
|
|
12
|
+
dedupe: 'defer',
|
|
13
|
+
getCachedData: (key, nuxtApp) => nuxtApp.payload.data[key]
|
|
28
14
|
})
|
|
29
|
-
|
|
30
|
-
await state.value[name]
|
|
31
|
-
return state.value[name]
|
|
32
15
|
}
|