@movk/nuxt-docs 1.3.12 → 1.4.1

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/README.md CHANGED
@@ -159,47 +159,6 @@ icon: i-lucide-rocket
159
159
 
160
160
  了解更多关于 MDC 语法,请查看 [Nuxt Content 文档](https://content.nuxt.com/docs/files/markdown#mdc-syntax)。
161
161
 
162
- ## 🔌 集成第三方服务
163
-
164
- 本主题不内置任何分析或监控工具,你可以根据需求自由选择。
165
-
166
- ### Vercel Analytics
167
-
168
- ```bash [Terminal]
169
- pnpm add @vercel/analytics @vercel/speed-insights
170
- ```
171
-
172
- 创建 `app/plugins/analytics.client.ts`:
173
-
174
- ```typescript [app/plugins/analytics.client.ts]
175
- import { Analytics } from '@vercel/analytics/nuxt'
176
- import { SpeedInsights } from '@vercel/speed-insights/nuxt'
177
- import { createApp, h } from 'vue'
178
-
179
- export default defineNuxtPlugin({
180
- name: 'vercel-analytics',
181
- enforce: 'post',
182
- hooks: {
183
- 'app:mounted': () => {
184
- if (import.meta.dev) return
185
-
186
- const container = document.createElement('div')
187
- container.id = 'vercel-analytics'
188
- document.body.appendChild(container)
189
-
190
- const app = createApp({
191
- render: () => h('div', { style: 'display: none;' }, [
192
- h(Analytics, { debug: false }),
193
- h(SpeedInsights, { debug: false })
194
- ])
195
- })
196
-
197
- app.mount(container)
198
- }
199
- }
200
- })
201
- ```
202
-
203
162
  ### 其他工具
204
163
 
205
164
  - **Google Analytics** - [@nuxtjs/google-analytics](https://google-analytics.nuxtjs.org/)
package/app/app.config.ts CHANGED
@@ -2,19 +2,21 @@ import type { ButtonProps } from '@nuxt/ui'
2
2
 
3
3
  export default defineAppConfig({
4
4
  toaster: {
5
- expand: true,
6
- position: 'top-right' as const,
7
- duration: 3000,
8
- max: 5
5
+ position: 'bottom-right' as const,
6
+ duration: 5000,
7
+ max: 5,
8
+ expand: true
9
9
  },
10
10
  theme: {
11
11
  radius: 0.25,
12
- blackAsPrimary: false
12
+ blackAsPrimary: false,
13
+ icons: 'lucide',
14
+ font: 'Public Sans'
13
15
  },
14
16
  ui: {
15
17
  colors: {
16
- primary: 'indigo',
17
- neutral: 'zinc'
18
+ primary: 'green',
19
+ neutral: 'slate'
18
20
  },
19
21
  contentNavigation: {
20
22
  slots: {
@@ -32,7 +34,9 @@ export default defineAppConfig({
32
34
  }
33
35
  }
34
36
  },
37
+ vercelAnalytics: false,
35
38
  header: {
39
+ avatar: 'https://docs.mhaibaraai.cn/avatar.png',
36
40
  title: 'Movk Nuxt Docs',
37
41
  to: '/',
38
42
  search: true,
package/app/app.vue CHANGED
@@ -1,5 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import colors from 'tailwindcss/colors'
3
+ import { Analytics } from '@vercel/analytics/nuxt'
4
+ import { SpeedInsights } from '@vercel/speed-insights/nuxt'
3
5
 
4
6
  const site = useSiteConfig()
5
7
  const appConfig = useAppConfig()
@@ -13,6 +15,7 @@ const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSe
13
15
  const color = computed(() => colorMode.value === 'dark' ? (colors as any)[appConfig.ui.colors.neutral][900] : 'white')
14
16
  const radius = computed(() => `:root { --ui-radius: ${appConfig.theme.radius}rem; }`)
15
17
  const blackAsPrimary = computed(() => appConfig.theme.blackAsPrimary ? `:root { --ui-primary: black; } .dark { --ui-primary: white; }` : ':root {}')
18
+ const font = computed(() => `:root { --font-sans: '${appConfig.theme.font}', sans-serif; }`)
16
19
 
17
20
  useHead({
18
21
  meta: [
@@ -21,7 +24,8 @@ useHead({
21
24
  ],
22
25
  style: [
23
26
  { innerHTML: radius, id: 'nuxt-ui-radius', tagPriority: -2 },
24
- { innerHTML: blackAsPrimary, id: 'nuxt-ui-black-as-primary', tagPriority: -2 }
27
+ { innerHTML: blackAsPrimary, id: 'nuxt-ui-black-as-primary', tagPriority: -2 },
28
+ { innerHTML: font, id: 'nuxt-ui-font', tagPriority: -2 }
25
29
  ]
26
30
  })
27
31
 
@@ -41,6 +45,8 @@ provide('navigation', rootNavigation)
41
45
  <template>
42
46
  <UApp :toaster="appConfig.toaster">
43
47
  <NuxtLoadingIndicator color="var(--ui-primary)" :height="2" />
48
+ <Analytics v-if="appConfig.vercelAnalytics" />
49
+ <SpeedInsights v-if="appConfig.vercelAnalytics" />
44
50
 
45
51
  <div :class="{ root: route.path.startsWith('/docs/') }">
46
52
  <template v-if="!route.path.startsWith('/examples')">
@@ -55,7 +61,7 @@ provide('navigation', rootNavigation)
55
61
  <Footer />
56
62
 
57
63
  <ClientOnly>
58
- <LazyUContentSearch :files="files" :navigation="rootNavigation" :fuse="{ resultLimit: 1000 }"/>
64
+ <LazyUContentSearch :files="files" :navigation="rootNavigation" :fuse="{ resultLimit: 1000 }" />
59
65
  </ClientOnly>
60
66
  </template>
61
67
  </div>
@@ -3,6 +3,8 @@ const route = useRoute()
3
3
  const toast = useToast()
4
4
  const { copy, copied } = useClipboard()
5
5
  const site = useSiteConfig()
6
+ const { vercelAnalytics } = useAppConfig()
7
+ const { track } = useAnalytics()
6
8
 
7
9
  const mdPath = computed(() => `${site.url}/raw${route.path}.md`)
8
10
 
@@ -11,6 +13,7 @@ const items = [
11
13
  label: 'Copy Markdown link',
12
14
  icon: 'i-lucide-link',
13
15
  onSelect() {
16
+ if (vercelAnalytics) track ('Page Action', { action: 'Copy Markdown Link' })
14
17
  copy(mdPath.value)
15
18
  toast.add({
16
19
  title: 'Copied to clipboard',
@@ -22,23 +25,33 @@ const items = [
22
25
  label: 'View as Markdown',
23
26
  icon: 'i-simple-icons:markdown',
24
27
  target: '_blank',
25
- to: `/raw${route.path}.md`
28
+ to: `/raw${route.path}.md`,
29
+ onSelect() {
30
+ if (vercelAnalytics) track('Page Action', { action: 'View as Markdown' })
31
+ }
26
32
  },
27
33
  {
28
34
  label: 'Open in ChatGPT',
29
35
  icon: 'i-simple-icons:openai',
30
36
  target: '_blank',
31
- to: `https://chatgpt.com/?hints=search&q=${encodeURIComponent(`Read ${mdPath.value} so I can ask questions about it.`)}`
37
+ to: `https://chatgpt.com/?hints=search&q=${encodeURIComponent(`Read ${mdPath.value} so I can ask questions about it.`)}`,
38
+ onSelect() {
39
+ if (vercelAnalytics) track('Page Action', { action: 'Open in ChatGPT' })
40
+ }
32
41
  },
33
42
  {
34
43
  label: 'Open in Claude',
35
44
  icon: 'i-simple-icons:anthropic',
36
45
  target: '_blank',
37
- to: `https://claude.ai/new?q=${encodeURIComponent(`Read ${mdPath.value} so I can ask questions about it.`)}`
46
+ to: `https://claude.ai/new?q=${encodeURIComponent(`Read ${mdPath.value} so I can ask questions about it.`)}`,
47
+ onSelect() {
48
+ if (vercelAnalytics) track('Page Action', { action: 'Open in Claude' })
49
+ }
38
50
  }
39
51
  ]
40
52
 
41
53
  async function copyPage() {
54
+ if (vercelAnalytics) track('Page Action', { action: 'Copy Page Content' })
42
55
  copy(await $fetch<string>(`/raw${route.path}.md`))
43
56
  }
44
57
  </script>
@@ -55,7 +55,7 @@ const filePath = computed(() => {
55
55
 
56
56
  const { data: commits } = await useLazyFetch<Commit[]>('/api/github/commits', {
57
57
  key: `commit-changelog-${props.name ?? routeName.value}-${props.author ?? 'all'}`,
58
- query: { path: filePath.value, author: props.author }
58
+ query: { path: [filePath.value], author: props.author }
59
59
  })
60
60
 
61
61
  // 格式化提交消息
@@ -3,55 +3,7 @@ const { header } = useAppConfig()
3
3
  </script>
4
4
 
5
5
  <template>
6
- <NuxtLink
7
- :to="header.to"
8
- class="flex items-center gap-2 font-bold text-xl text-highlighted min-w-0 focus-visible:outline-primary shrink-0"
9
- :aria-label="header.title"
10
- >
11
- <svg
12
- width="256"
13
- height="256"
14
- viewBox="0 0 256 256"
15
- fill="none"
16
- xmlns="http://www.w3.org/2000/svg"
17
- class="w-auto h-10 shrink-0"
18
- >
19
- <circle
20
- cx="128"
21
- cy="128"
22
- r="120"
23
- fill="var(--ui-primary)"
24
- />
25
- <g fill="white" opacity="0.95">
26
- <path
27
- d="M38.5 175.6 c-1.5 -1 -1.9 -2.7 -1.9 -7.7 0 -5.5 0.4 -7.1 3.1 -11.4 1.8 -2.7 4.8 -6.3 6.8 -7.8 1.9 -1.5 3.5 -3.2 3.5 -3.8 0 -1.2 -5.8 -0.4 -11.5 1.6 -11.7 4.1 -16.3 3.7 -19 -1.5 -2.6 -5 -1.2 -8.4 4.2 -11 6.6 -3.1 10.4 -3.5 14.3 -1.5 2.1 1.1 4.1 1.4 5.9 0.9 1.4 -0.4 3.5 -0.9 4.6 -1.1 4.9 -0.9 12.3 -3.8 16 -6.3 13.7 -9.4 18.6 -11.7 34 -16 6.6 -1.9 11.9 -2.4 31.3 -3.1 l23.4 -0.8 8.4 -8.5 c5.7 -5.8 9.5 -8.8 11.9 -9.5 1.9 -0.5 5.8 -2.4 8.7 -4.1 4.5 -2.8 5.4 -3 7.5 -1.9 2 1.1 2.9 0.9 6.2 -0.9 6.4 -3.6 7.6 -2.5 4.4 4.3 l-1.7 3.5 3.3 4.1 c2.8 3.5 3.2 4.6 2.7 7.7 -0.3 2 0 5.1 0.6 6.8 1.8 5 -0.4 8.9 -6.3 11.6 -2.9 1.4 -4.5 2.6 -4 3.3 0.4 0.5 1.6 3.5 2.6 6.5 2.8 8.8 1.9 8.2 13.5 8.5 10.2 0.2 21.7 1.9 25.3 3.7 2 1 2.3 5.1 0.3 6.7 -0.7 0.6 -5.1 1.5 -9.7 2.1 -4.6 0.5 -12.7 1.9 -17.9 3 -8.5 1.8 -12.8 2 -42 1.9 -17.9 -0.1 -35 -0.3 -38 -0.5 -3 -0.1 -10.7 -0.6 -17.1 -1 -9.7 -0.6 -12.4 -0.4 -17 1.1 -7.2 2.3 -7.3 2.4 -5.5 4.4 2.2 2.5 3.1 7.8 1.6 9.6 -2.7 3.3 -9.3 1.2 -17.4 -5.5 -5.6 -4.7 -8.8 -5 -15.6 -1.7 -4.4 2.2 -4.5 2.3 -4.8 7.6 -0.2 3.7 -0.9 5.9 -2 6.8 -2.3 1.7 -10.3 1.6 -12.7 -0.1z"
28
- />
29
- </g>
30
- <g fill="white" opacity="0.98">
31
- <path
32
- d="M142 209.3 c-0.7 -1.6 -2.3 -5.7 -3.5 -9.3 -1.3 -3.6 -2.6 -7.2 -3 -8.1 -0.4 -1.1 0 -1.8 1.4 -2.2 1.7 -0.4 2.4 0.4 4.3 5.4 1.3 3.2 2.5 5.9 2.8 5.9 0.3 0 1.9 -2.6 3.5 -5.8 2.1 -4 3.7 -5.8 5.3 -6 1.2 -0.2 2.2 -0.1 2.2 0.3 0 0.5 -10.8 21.5 -11.5 22.3 -0.1 0.1 -0.8 -1 -1.5 -2.5z"
33
- />
34
- <path
35
- d="M84.5 201.3 c1 -12.9 1.6 -13.8 4.8 -6.9 1.5 3.1 2.9 5.6 3.3 5.6 0.3 0 3.1 -2.2 6.2 -5 3 -2.7 5.7 -4.8 5.9 -4.7 0.3 0.4 3.1 19.1 2.9 19.4 -0.1 0.2 -1.3 0.4 -2.7 0.5 -2.2 0.3 -2.4 0 -2.7 -4.9 -0.2 -2.9 -0.6 -5.3 -0.9 -5.3 -0.3 0 -2.3 1.4 -4.4 3.1 -3.7 2.9 -4.1 3 -5.8 1.5 -1.7 -1.6 -1.9 -1.6 -2.5 -0.1 -0.3 0.9 -0.6 2.8 -0.6 4.1 0 1.8 -0.5 2.4 -2.1 2.4 -2 0 -2.1 -0.3 -1.4 -9.7z"
36
- />
37
- <path
38
- d="M115.9 209.5 c-5.9 -3.2 -6.4 -12.8 -0.9 -17.7 3.4 -3.1 5.6 -3.4 10.3 -1.5 4.3 1.8 5.9 4 6 7.9 0.1 6.4 -1.2 10.4 -3.9 11.6 -3.3 1.6 -8.3 1.4 -11.5 -0.3z m9.6 -4.5 c2.1 -2.4 2.4 -7.2 0.5 -9.4 -2 -2.5 -7.7 -2.1 -9.1 0.6 -3.9 7.3 3.3 14.7 8.6 8.8z"
39
- />
40
- <path
41
- d="M158 200 c0 -11 0 -11 2.4 -11 2.3 0 2.4 0.3 1.9 3.6 -0.6 3.5 -0.5 3.6 1.6 2.5 1.2 -0.7 2.7 -2 3.3 -3.1 0.9 -1.4 1.5 -1.6 2.6 -0.8 1 0.9 0.6 1.9 -2.5 5 l-3.8 3.9 2.5 1.9 c1.4 1.1 3.7 2.5 5.3 3.2 3 1.4 3.3 2.1 1.4 3.6 -0.9 0.8 -2.4 0.3 -5.7 -1.9 l-4.5 -3.1 -0.3 3.6 c-0.3 2.8 -0.8 3.6 -2.3 3.6 -1.8 0 -1.9 -0.8 -1.9 -11z"
42
- />
43
- </g>
44
- <circle
45
- cx="128"
46
- cy="128"
47
- r="118"
48
- stroke="white"
49
- stroke-width="1.5"
50
- fill="none"
51
- opacity="0.15"
52
- />
53
- </svg>
54
-
55
- <p class="font-medium text-highlighted">{{ header.title }}</p>
6
+ <NuxtLink :to="header.to">
7
+ <UUser :avatar="{ src: header.avatar }" :name="header.title" />
56
8
  </NuxtLink>
57
9
  </template>
@@ -1,11 +1,26 @@
1
1
  <script setup lang="ts">
2
2
  import { omit } from '@movk/core'
3
3
  import colors from 'tailwindcss/colors'
4
+ import { useClipboard } from '@vueuse/core'
5
+ import { themeIcons } from '../../utils/theme'
4
6
 
5
7
  const appConfig = useAppConfig()
6
8
  const colorMode = useColorMode()
7
9
  const site = useSiteConfig()
8
10
 
11
+ const { track } = useAnalytics()
12
+
13
+ const open = ref(false)
14
+
15
+ watch(open, (isOpen) => {
16
+ if (isOpen && appConfig.vercelAnalytics) {
17
+ track('Theme Picker Opened')
18
+ }
19
+ })
20
+
21
+ const { copy: copyCSS, copied: copiedCSS } = useClipboard()
22
+ const { copy: copyAppConfig, copied: copiedAppConfig } = useClipboard()
23
+
9
24
  const neutralColors = ['slate', 'gray', 'zinc', 'neutral', 'stone']
10
25
  const neutral = computed({
11
26
  get() {
@@ -14,6 +29,7 @@ const neutral = computed({
14
29
  set(option) {
15
30
  appConfig.ui.colors.neutral = option
16
31
  window.localStorage.setItem(`${site.name}-ui-neutral`, appConfig.ui.colors.neutral)
32
+ if (appConfig.vercelAnalytics) track('Theme Changed', { setting: 'neutral', value: option })
17
33
  }
18
34
  })
19
35
 
@@ -27,6 +43,7 @@ const primary = computed({
27
43
  appConfig.ui.colors.primary = option
28
44
  window.localStorage.setItem(`${site.name}-ui-primary`, appConfig.ui.colors.primary)
29
45
  setBlackAsPrimary(false)
46
+ if (appConfig.vercelAnalytics) track('Theme Changed', { setting: 'primary', value: option })
30
47
  }
31
48
  })
32
49
 
@@ -38,6 +55,7 @@ const radius = computed({
38
55
  set(option) {
39
56
  appConfig.theme.radius = option
40
57
  window.localStorage.setItem(`${site.name}-ui-radius`, String(appConfig.theme.radius))
58
+ if (appConfig.vercelAnalytics) track('Theme Changed', { setting: 'radius', value: option })
41
59
  }
42
60
  })
43
61
 
@@ -52,18 +70,155 @@ const mode = computed({
52
70
  },
53
71
  set(option) {
54
72
  colorMode.preference = option
73
+ if (appConfig.vercelAnalytics) track('Theme Changed', { setting: 'color mode', value: option })
55
74
  }
56
75
  })
57
76
 
58
77
  function setBlackAsPrimary(value: boolean) {
59
78
  appConfig.theme.blackAsPrimary = value
60
79
  window.localStorage.setItem(`${site.name}-ui-black-as-primary`, String(value))
80
+ if (appConfig.vercelAnalytics) track('Theme Changed', { setting: 'black as primary', value })
81
+ }
82
+
83
+ const fonts = ['Public Sans', 'DM Sans', 'Geist', 'Inter', 'Poppins', 'Outfit', 'Raleway']
84
+ const font = computed({
85
+ get() {
86
+ return appConfig.theme.font
87
+ },
88
+ set(option) {
89
+ appConfig.theme.font = option
90
+ window.localStorage.setItem(`${site.name}-ui-font`, appConfig.theme.font)
91
+ if (appConfig.vercelAnalytics) track('Theme Changed', { setting: 'font', value: option })
92
+ }
93
+ })
94
+
95
+ const icons = [{
96
+ label: 'Lucide',
97
+ icon: 'i-lucide-feather',
98
+ value: 'lucide'
99
+ }, {
100
+ label: 'Phosphor',
101
+ icon: 'i-ph-phosphor-logo',
102
+ value: 'phosphor'
103
+ }, {
104
+ label: 'Tabler',
105
+ icon: 'i-tabler-brand-tabler',
106
+ value: 'tabler'
107
+ }]
108
+ const icon = computed({
109
+ get() {
110
+ return appConfig.theme.icons
111
+ },
112
+ set(option) {
113
+ appConfig.theme.icons = option
114
+ appConfig.ui.icons = themeIcons[option as keyof typeof themeIcons] as any
115
+ window.localStorage.setItem(`${site.name}-ui-icons`, appConfig.theme.icons)
116
+ if (appConfig.vercelAnalytics) track('Theme Changed', { setting: 'icons', value: option })
117
+ }
118
+ })
119
+
120
+ const hasCSSChanges = computed(() => {
121
+ return appConfig.theme.radius !== 0.25
122
+ || appConfig.theme.blackAsPrimary
123
+ || appConfig.theme.font !== 'Public Sans'
124
+ })
125
+
126
+ const hasAppConfigChanges = computed(() => {
127
+ return appConfig.ui.colors.primary !== 'green'
128
+ || appConfig.ui.colors.neutral !== 'slate'
129
+ || appConfig.theme.icons !== 'lucide'
130
+ })
131
+
132
+ function exportCSS() {
133
+ if (appConfig.vercelAnalytics) track('Theme Exported', { type: 'css' })
134
+
135
+ const lines = [
136
+ '@import "tailwindcss";',
137
+ '@import "@nuxt/ui";'
138
+ ]
139
+
140
+ if (appConfig.theme.font !== 'Public Sans') {
141
+ lines.push('', '@theme {', ` --font-sans: '${appConfig.theme.font}', sans-serif;`, '}')
142
+ }
143
+
144
+ const rootLines: string[] = []
145
+ if (appConfig.theme.radius !== 0.25) {
146
+ rootLines.push(` --ui-radius: ${appConfig.theme.radius}rem;`)
147
+ }
148
+ if (appConfig.theme.blackAsPrimary) {
149
+ rootLines.push(' --ui-primary: black;')
150
+ }
151
+
152
+ if (rootLines.length) {
153
+ lines.push('', ':root {', ...rootLines, '}')
154
+ }
155
+
156
+ if (appConfig.theme.blackAsPrimary) {
157
+ lines.push('', '.dark {', ' --ui-primary: white;', '}')
158
+ }
159
+
160
+ copyCSS(lines.join('\n'))
161
+ }
162
+
163
+ function exportAppConfig() {
164
+ if (appConfig.vercelAnalytics) track('Theme Exported', { type: 'appConfig' })
165
+
166
+ const config: Record<string, any> = {}
167
+
168
+ if (appConfig.ui.colors.primary !== 'green' || appConfig.ui.colors.neutral !== 'slate') {
169
+ config.ui = { colors: {} }
170
+ if (appConfig.ui.colors.primary !== 'green') {
171
+ config.ui.colors.primary = appConfig.ui.colors.primary
172
+ }
173
+ if (appConfig.ui.colors.neutral !== 'slate') {
174
+ config.ui.colors.neutral = appConfig.ui.colors.neutral
175
+ }
176
+ }
177
+
178
+ if (appConfig.theme.icons !== 'lucide') {
179
+ const iconSet = appConfig.theme.icons
180
+ const icons = themeIcons[iconSet as keyof typeof themeIcons]
181
+ config.ui = config.ui || {}
182
+ config.ui.icons = icons
183
+ }
184
+
185
+ const configString = JSON.stringify(config, null, 2)
186
+ .replace(/"([^"]+)":/g, '$1:')
187
+ .replace(/"/g, '\'')
188
+
189
+ const output = `export default defineAppConfig(${configString})`
190
+
191
+ copyAppConfig(output)
192
+ }
193
+
194
+ function resetTheme() {
195
+ if (appConfig.vercelAnalytics) track('Theme Reset')
196
+
197
+ // Reset without triggering individual tracking events
198
+ appConfig.ui.colors.primary = 'green'
199
+ window.localStorage.removeItem(`${site.name}-ui-primary`)
200
+
201
+ appConfig.ui.colors.neutral = 'slate'
202
+ window.localStorage.removeItem(`${site.name}-ui-neutral`)
203
+
204
+ appConfig.theme.radius = 0.25
205
+ window.localStorage.removeItem(`${site.name}-ui-radius`)
206
+
207
+ appConfig.theme.font = 'Public Sans'
208
+ window.localStorage.removeItem(`${site.name}-ui-font`)
209
+
210
+ appConfig.theme.icons = 'lucide'
211
+ appConfig.ui.icons = themeIcons.lucide as any
212
+ window.localStorage.removeItem(`${site.name}-ui-icons`)
213
+
214
+ appConfig.theme.blackAsPrimary = false
215
+ window.localStorage.removeItem(`${site.name}-ui-black-as-primary`)
61
216
  }
62
217
  </script>
63
218
 
64
219
  <template>
65
- <UPopover :ui="{ content: 'w-72 px-6 py-4 flex flex-col gap-4' }">
66
- <template #default="{ open }">
220
+ <UPopover v-model:open="open" :ui="{ content: 'w-72 px-6 py-4 flex flex-col gap-4 overflow-y-auto max-h-[calc(100vh-5rem)]' }">
221
+ <template #default>
67
222
  <UButton
68
223
  icon="i-lucide-swatch-book"
69
224
  color="neutral"
@@ -76,8 +231,19 @@ function setBlackAsPrimary(value: boolean) {
76
231
 
77
232
  <template #content>
78
233
  <fieldset>
79
- <legend class="text-[11px] leading-none font-semibold mb-2">
234
+ <legend class="text-[11px] leading-none font-semibold mb-2 select-none flex items-center gap-1">
80
235
  Primary
236
+
237
+ <UButton
238
+ to="https://ui.nuxt.com/docs/getting-started/theme/css-variables#colors"
239
+ size="xs"
240
+ color="neutral"
241
+ variant="link"
242
+ target="_blank"
243
+ icon="i-lucide-circle-help"
244
+ class="p-0 -my-0.5"
245
+ :ui="{ leadingIcon: 'size-3' }"
246
+ />
81
247
  </legend>
82
248
 
83
249
  <div class="grid grid-cols-3 gap-1 -mx-2">
@@ -99,8 +265,19 @@ function setBlackAsPrimary(value: boolean) {
99
265
  </fieldset>
100
266
 
101
267
  <fieldset>
102
- <legend class="text-[11px] leading-none font-semibold mb-2">
268
+ <legend class="text-[11px] leading-none font-semibold mb-2 select-none flex items-center gap-1">
103
269
  Neutral
270
+
271
+ <UButton
272
+ to="https://ui.nuxt.com/docs/getting-started/theme/css-variables#text"
273
+ size="xs"
274
+ color="neutral"
275
+ variant="link"
276
+ target="_blank"
277
+ icon="i-lucide-circle-help"
278
+ class="p-0 -my-0.5"
279
+ :ui="{ leadingIcon: 'size-3' }"
280
+ />
104
281
  </legend>
105
282
 
106
283
  <div class="grid grid-cols-3 gap-1 -mx-2">
@@ -116,8 +293,19 @@ function setBlackAsPrimary(value: boolean) {
116
293
  </fieldset>
117
294
 
118
295
  <fieldset>
119
- <legend class="text-[11px] leading-none font-semibold mb-2">
296
+ <legend class="text-[11px] leading-none font-semibold mb-2 select-none flex items-center gap-1">
120
297
  Radius
298
+
299
+ <UButton
300
+ to="https://ui.nuxt.com/docs/getting-started/theme/css-variables#radius"
301
+ size="xs"
302
+ color="neutral"
303
+ variant="link"
304
+ target="_blank"
305
+ icon="i-lucide-circle-help"
306
+ class="p-0 -my-0.5"
307
+ :ui="{ leadingIcon: 'size-3' }"
308
+ />
121
309
  </legend>
122
310
 
123
311
  <div class="grid grid-cols-5 gap-1 -mx-2">
@@ -133,8 +321,77 @@ function setBlackAsPrimary(value: boolean) {
133
321
  </fieldset>
134
322
 
135
323
  <fieldset>
136
- <legend class="text-[11px] leading-none font-semibold mb-2">
137
- Theme
324
+ <legend class="text-[11px] leading-none font-semibold mb-2 select-none flex items-center gap-1">
325
+ Font
326
+
327
+ <UButton
328
+ to="https://ui.nuxt.com/docs/getting-started/integrations/fonts"
329
+ size="xs"
330
+ color="neutral"
331
+ variant="link"
332
+ target="_blank"
333
+ icon="i-lucide-circle-help"
334
+ class="p-0 -my-0.5"
335
+ :ui="{ leadingIcon: 'size-3' }"
336
+ />
337
+ </legend>
338
+
339
+ <div class="-mx-2">
340
+ <USelect
341
+ v-model="font"
342
+ size="sm"
343
+ color="neutral"
344
+ icon="i-lucide-type"
345
+ :items="fonts"
346
+ class="w-full ring-default rounded-sm hover:bg-elevated/50 text-[11px] data-[state=open]:bg-elevated/50"
347
+ :ui="{ trailingIcon: 'group-data-[state=open]:rotate-180 transition-transform duration-200' }"
348
+ />
349
+ </div>
350
+ </fieldset>
351
+
352
+ <fieldset>
353
+ <legend class="text-[11px] leading-none font-semibold mb-2 select-none flex items-center gap-1">
354
+ Icons
355
+
356
+ <UButton
357
+ to="https://ui.nuxt.com/docs/getting-started/integrations/icons"
358
+ size="xs"
359
+ color="neutral"
360
+ variant="link"
361
+ target="_blank"
362
+ icon="i-lucide-circle-help"
363
+ class="p-0 -my-0.5"
364
+ :ui="{ leadingIcon: 'size-3' }"
365
+ />
366
+ </legend>
367
+
368
+ <div class="-mx-2">
369
+ <USelect
370
+ v-model="icon"
371
+ size="sm"
372
+ color="neutral"
373
+ :icon="icons.find(i => i.value === icon)?.icon"
374
+ :items="icons"
375
+ class="w-full ring-default rounded-sm hover:bg-elevated/50 capitalize text-[11px] data-[state=open]:bg-elevated/50"
376
+ :ui="{ item: 'capitalize text-[11px]', trailingIcon: 'group-data-[state=open]:rotate-180 transition-transform duration-200' }"
377
+ />
378
+ </div>
379
+ </fieldset>
380
+
381
+ <fieldset>
382
+ <legend class="text-[11px] leading-none font-semibold mb-2 select-none flex items-center gap-1">
383
+ Color Mode
384
+
385
+ <UButton
386
+ to="https://ui.nuxt.com/docs/getting-started/integrations/color-mode"
387
+ size="xs"
388
+ color="neutral"
389
+ variant="link"
390
+ target="_blank"
391
+ icon="i-lucide-circle-help"
392
+ class="p-0 -my-0.5"
393
+ :ui="{ leadingIcon: 'size-3' }"
394
+ />
138
395
  </legend>
139
396
 
140
397
  <div class="grid grid-cols-3 gap-1 -mx-2">
@@ -147,6 +404,45 @@ function setBlackAsPrimary(value: boolean) {
147
404
  />
148
405
  </div>
149
406
  </fieldset>
407
+
408
+ <fieldset v-if="hasCSSChanges || hasAppConfigChanges">
409
+ <legend class="text-[11px] leading-none font-semibold mb-2 select-none">
410
+ Export
411
+ </legend>
412
+
413
+ <div class="flex items-center justify-between gap-1 -mx-2">
414
+ <UButton
415
+ v-if="hasCSSChanges"
416
+ color="neutral"
417
+ variant="soft"
418
+ size="sm"
419
+ label="main.css"
420
+ class="flex-1 text-[11px]"
421
+ :icon="copiedCSS ? 'i-lucide-copy-check' : 'i-lucide-copy'"
422
+ @click="exportCSS"
423
+ />
424
+ <UButton
425
+ v-if="hasAppConfigChanges"
426
+ color="neutral"
427
+ variant="soft"
428
+ size="sm"
429
+ label="app.config.ts"
430
+ :icon="copiedAppConfig ? 'i-lucide-copy-check' : 'i-lucide-copy'"
431
+ class="flex-1 text-[11px]"
432
+ @click="exportAppConfig"
433
+ />
434
+ <UTooltip text="Reset theme">
435
+ <UButton
436
+ color="neutral"
437
+ variant="outline"
438
+ size="sm"
439
+ icon="i-lucide-rotate-ccw"
440
+ class="ms-auto ring-default hover:bg-elevated/50"
441
+ @click="resetTheme"
442
+ />
443
+ </UTooltip>
444
+ </div>
445
+ </fieldset>
150
446
  </template>
151
447
  </UPopover>
152
448
  </template>
@@ -0,0 +1,7 @@
1
+ import { track } from '@vercel/analytics'
2
+
3
+ export function useAnalytics() {
4
+ return {
5
+ track
6
+ }
7
+ }
package/app/error.vue CHANGED
@@ -18,6 +18,7 @@ const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSe
18
18
  const color = computed(() => colorMode.value === 'dark' ? (colors as any)[appConfig.ui.colors.neutral][900] : 'white')
19
19
  const radius = computed(() => `:root { --ui-radius: ${appConfig.theme.radius}rem; }`)
20
20
  const blackAsPrimary = computed(() => appConfig.theme.blackAsPrimary ? `:root { --ui-primary: black; } .dark { --ui-primary: white; }` : ':root {}')
21
+ const font = computed(() => `:root { --font-sans: '${appConfig.theme.font}', sans-serif; }`)
21
22
 
22
23
  useHead({
23
24
  meta: [
@@ -26,7 +27,8 @@ useHead({
26
27
  ],
27
28
  style: [
28
29
  { innerHTML: radius, id: 'nuxt-ui-radius', tagPriority: -2 },
29
- { innerHTML: blackAsPrimary, id: 'nuxt-ui-black-as-primary', tagPriority: -2 }
30
+ { innerHTML: blackAsPrimary, id: 'nuxt-ui-black-as-primary', tagPriority: -2 },
31
+ { innerHTML: font, id: 'nuxt-ui-font', tagPriority: -2 }
30
32
  ]
31
33
  })
32
34
 
@@ -1,3 +1,5 @@
1
+ import { themeIcons } from '../utils/theme'
2
+
1
3
  export default defineNuxtPlugin({
2
4
  enforce: 'post',
3
5
  setup() {
@@ -26,12 +28,32 @@ export default defineNuxtPlugin({
26
28
  }
27
29
  }
28
30
 
31
+ function updateFont() {
32
+ const font = localStorage.getItem(`${site.name}-ui-font`)
33
+ if (font) {
34
+ appConfig.theme.font = font
35
+ }
36
+ }
37
+
29
38
  updateColor('primary')
30
39
  updateColor('neutral')
31
40
  updateRadius()
32
41
  updateBlackAsPrimary()
42
+ updateFont()
33
43
  }
34
44
 
45
+ onNuxtReady(() => {
46
+ function updateIcons() {
47
+ const icons = localStorage.getItem(`${site.name}-ui-icons`)
48
+ if (icons) {
49
+ appConfig.theme.icons = icons
50
+ appConfig.ui.icons = themeIcons[icons as keyof typeof themeIcons] as any
51
+ }
52
+ }
53
+
54
+ updateIcons()
55
+ })
56
+
35
57
  if (import.meta.server) {
36
58
  useHead({
37
59
  script: [{
@@ -75,6 +97,13 @@ export default defineNuxtPlugin({
75
97
  document.querySelector('style#nuxt-ui-black-as-primary').innerHTML = '';
76
98
  }
77
99
  `.replace(/\s+/g, ' ')
100
+ }, {
101
+ innerHTML: `
102
+ if (localStorage.getItem('${site.name}-ui-font')) {
103
+ const font = localStorage.getItem('${site.name}-ui-font');
104
+ document.querySelector('style#nuxt-ui-font').innerHTML = ':root { --font-sans: \\'' + font + '\\', sans-serif; }';
105
+ }
106
+ `.replace(/\s+/g, ' ')
78
107
  }]
79
108
  })
80
109
  }
@@ -2,6 +2,7 @@ import type { ButtonProps } from '@nuxt/ui'
2
2
 
3
3
  declare module 'nuxt/schema' {
4
4
  interface AppConfig {
5
+ vercelAnalytics: boolean
5
6
  seo: {
6
7
  titleTemplate: string
7
8
  title: string
@@ -9,6 +10,7 @@ declare module 'nuxt/schema' {
9
10
  }
10
11
  header: {
11
12
  title: string
13
+ avatar: string
12
14
  to: string
13
15
  search: boolean
14
16
  colorMode: boolean
@@ -0,0 +1,136 @@
1
+ export const themeIcons = {
2
+ lucide: {
3
+ arrowDown: 'i-lucide-arrow-down',
4
+ arrowLeft: 'i-lucide-arrow-left',
5
+ arrowRight: 'i-lucide-arrow-right',
6
+ arrowUp: 'i-lucide-arrow-up',
7
+ caution: 'i-lucide-circle-alert',
8
+ check: 'i-lucide-check',
9
+ chevronDoubleLeft: 'i-lucide-chevrons-left',
10
+ chevronDoubleRight: 'i-lucide-chevrons-right',
11
+ chevronDown: 'i-lucide-chevron-down',
12
+ chevronLeft: 'i-lucide-chevron-left',
13
+ chevronRight: 'i-lucide-chevron-right',
14
+ chevronUp: 'i-lucide-chevron-up',
15
+ close: 'i-lucide-x',
16
+ copy: 'i-lucide-copy',
17
+ copyCheck: 'i-lucide-copy-check',
18
+ dark: 'i-lucide-moon',
19
+ drag: 'i-lucide-grip-vertical',
20
+ ellipsis: 'i-lucide-ellipsis',
21
+ error: 'i-lucide-circle-x',
22
+ external: 'i-lucide-arrow-up-right',
23
+ eye: 'i-lucide-eye',
24
+ eyeOff: 'i-lucide-eye-off',
25
+ file: 'i-lucide-file',
26
+ folder: 'i-lucide-folder',
27
+ folderOpen: 'i-lucide-folder-open',
28
+ hash: 'i-lucide-hash',
29
+ info: 'i-lucide-info',
30
+ light: 'i-lucide-sun',
31
+ loading: 'i-lucide-loader-circle',
32
+ menu: 'i-lucide-menu',
33
+ minus: 'i-lucide-minus',
34
+ panelClose: 'i-lucide-panel-left-close',
35
+ panelOpen: 'i-lucide-panel-left-open',
36
+ plus: 'i-lucide-plus',
37
+ reload: 'i-lucide-rotate-ccw',
38
+ search: 'i-lucide-search',
39
+ stop: 'i-lucide-square',
40
+ success: 'i-lucide-circle-check',
41
+ system: 'i-lucide-monitor',
42
+ tip: 'i-lucide-lightbulb',
43
+ upload: 'i-lucide-upload',
44
+ warning: 'i-lucide-triangle-alert'
45
+ },
46
+ phosphor: {
47
+ arrowDown: 'i-ph-arrow-down',
48
+ arrowLeft: 'i-ph-arrow-left',
49
+ arrowRight: 'i-ph-arrow-right',
50
+ arrowUp: 'i-ph-arrow-up',
51
+ caution: 'i-ph-warning-circle',
52
+ check: 'i-ph-check',
53
+ chevronDoubleLeft: 'i-ph-caret-double-left',
54
+ chevronDoubleRight: 'i-ph-caret-double-right',
55
+ chevronDown: 'i-ph-caret-down',
56
+ chevronLeft: 'i-ph-caret-left',
57
+ chevronRight: 'i-ph-caret-right',
58
+ chevronUp: 'i-ph-caret-up',
59
+ close: 'i-ph-x',
60
+ copy: 'i-ph-copy',
61
+ copyCheck: 'i-ph-check-circle',
62
+ dark: 'i-ph-moon',
63
+ drag: 'i-ph-dots-six-vertical',
64
+ ellipsis: 'i-ph-dots-three',
65
+ error: 'i-ph-x-circle',
66
+ external: 'i-ph-arrow-up-right',
67
+ eye: 'i-ph-eye',
68
+ eyeOff: 'i-ph-eye-slash',
69
+ file: 'i-ph-file',
70
+ folder: 'i-ph-folder',
71
+ folderOpen: 'i-ph-folder-open',
72
+ hash: 'i-ph-hash',
73
+ info: 'i-ph-info',
74
+ light: 'i-ph-sun',
75
+ loading: 'i-ph-circle-notch',
76
+ menu: 'i-ph-list',
77
+ minus: 'i-ph-minus',
78
+ panelClose: 'i-ph-caret-left',
79
+ panelOpen: 'i-ph-caret-right',
80
+ plus: 'i-ph-plus',
81
+ reload: 'i-ph-arrow-counter-clockwise',
82
+ search: 'i-ph-magnifying-glass',
83
+ stop: 'i-ph-square',
84
+ success: 'i-ph-check-circle',
85
+ system: 'i-ph-monitor',
86
+ tip: 'i-ph-lightbulb',
87
+ upload: 'i-ph-upload',
88
+ warning: 'i-ph-warning'
89
+ },
90
+ tabler: {
91
+ arrowDown: 'i-tabler-arrow-down',
92
+ arrowLeft: 'i-tabler-arrow-left',
93
+ arrowRight: 'i-tabler-arrow-right',
94
+ arrowUp: 'i-tabler-arrow-up',
95
+ caution: 'i-tabler-alert-square-rounded',
96
+ check: 'i-tabler-check',
97
+ chevronDoubleLeft: 'i-tabler-chevrons-left',
98
+ chevronDoubleRight: 'i-tabler-chevrons-right',
99
+ chevronDown: 'i-tabler-chevron-down',
100
+ chevronLeft: 'i-tabler-chevron-left',
101
+ chevronRight: 'i-tabler-chevron-right',
102
+ chevronUp: 'i-tabler-chevron-up',
103
+ close: 'i-tabler-x',
104
+ copy: 'i-tabler-copy',
105
+ copyCheck: 'i-tabler-copy-check',
106
+ dark: 'i-tabler-moon',
107
+ drag: 'i-tabler-grip-vertical',
108
+ ellipsis: 'i-tabler-dots',
109
+ error: 'i-tabler-square-rounded-x',
110
+ external: 'i-tabler-external-link',
111
+ eye: 'i-tabler-eye',
112
+ eyeOff: 'i-tabler-eye-off',
113
+ file: 'i-tabler-file',
114
+ folder: 'i-tabler-folder',
115
+ folderOpen: 'i-tabler-folder-open',
116
+ hash: 'i-tabler-hash',
117
+ info: 'i-tabler-info-square-rounded',
118
+ light: 'i-tabler-sun',
119
+ loading: 'i-tabler-loader-2',
120
+ menu: 'i-tabler-menu',
121
+ minus: 'i-tabler-minus',
122
+ panelClose: 'i-tabler-layout-sidebar-left-collapse',
123
+ panelOpen: 'i-tabler-layout-sidebar-left-expand',
124
+ plus: 'i-tabler-plus',
125
+ reload: 'i-tabler-reload',
126
+ search: 'i-tabler-search',
127
+ stop: 'i-tabler-player-stop',
128
+ success: 'i-tabler-square-rounded-check',
129
+ system: 'i-tabler-device-desktop',
130
+ tip: 'i-tabler-bulb',
131
+ upload: 'i-tabler-upload',
132
+ warning: 'i-tabler-alert-triangle'
133
+ }
134
+ }
135
+
136
+ export type ThemeIcons = keyof typeof themeIcons
package/nuxt.config.ts CHANGED
@@ -73,6 +73,17 @@ export default defineNuxtConfig({
73
73
  }
74
74
  }
75
75
  },
76
+ fonts: {
77
+ families: [
78
+ { name: 'Public Sans', provider: 'google', global: true },
79
+ { name: 'DM Sans', provider: 'google', global: true },
80
+ { name: 'Geist', provider: 'google', global: true },
81
+ { name: 'Inter', provider: 'google', global: true },
82
+ { name: 'Poppins', provider: 'google', global: true },
83
+ { name: 'Outfit', provider: 'google', global: true },
84
+ { name: 'Raleway', provider: 'google', global: true }
85
+ ]
86
+ },
76
87
  icon: {
77
88
  provider: 'iconify'
78
89
  },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@movk/nuxt-docs",
3
3
  "type": "module",
4
- "version": "1.3.12",
4
+ "version": "1.4.1",
5
5
  "private": false,
6
6
  "description": "An elegant documentation theme for Nuxt, powered by Nuxt UI and Nuxt Content.",
7
7
  "author": "YiXuan <mhaibaraai@gmail.com>",
@@ -27,16 +27,18 @@
27
27
  "README.md"
28
28
  ],
29
29
  "dependencies": {
30
- "@iconify-json/lucide": "^1.2.79",
31
- "@iconify-json/simple-icons": "^1.2.62",
30
+ "@iconify-json/lucide": "^1.2.81",
31
+ "@iconify-json/simple-icons": "^1.2.63",
32
32
  "@iconify-json/vscode-icons": "^1.2.37",
33
33
  "@movk/core": "^1.0.2",
34
34
  "@nuxt/content": "^3.9.0",
35
35
  "@nuxt/image": "^2.0.0",
36
36
  "@nuxt/kit": "^4.2.2",
37
- "@nuxt/ui": "^4.2.1",
38
- "@nuxtjs/seo": "^3.2.2",
37
+ "@nuxt/ui": "^4.3.0",
38
+ "@nuxtjs/seo": "^3.3.0",
39
39
  "@octokit/rest": "^22.0.1",
40
+ "@vercel/analytics": "^1.6.1",
41
+ "@vercel/speed-insights": "^1.3.1",
40
42
  "@vueuse/core": "^14.1.0",
41
43
  "@vueuse/nuxt": "^14.1.0",
42
44
  "defu": "^6.1.4",
@@ -50,7 +52,7 @@
50
52
  "pkg-types": "^2.3.0",
51
53
  "prettier": "^3.7.4",
52
54
  "scule": "^1.3.0",
53
- "tailwindcss": "^4.1.17",
55
+ "tailwindcss": "^4.1.18",
54
56
  "ufo": "^1.6.1"
55
57
  }
56
58
  }
@@ -5,8 +5,10 @@ export default defineCachedEventHandler(async (event) => {
5
5
  return []
6
6
  }
7
7
 
8
- const { path, author } = getQuery(event) as { path: string, author?: string }
9
- if (!path) {
8
+ const { path, author } = getQuery(event) as { path: string | string[], author: string }
9
+ const paths = Array.isArray(path) ? path : [path]
10
+
11
+ if (!paths.length || !paths[0]) {
10
12
  throw createError({
11
13
  statusCode: 400,
12
14
  statusMessage: 'Path is required'
@@ -15,26 +17,41 @@ export default defineCachedEventHandler(async (event) => {
15
17
 
16
18
  const { github } = useAppConfig()
17
19
  const octokit = new Octokit({ auth: process.env.NUXT_GITHUB_TOKEN })
18
- const commits = await octokit.paginate(octokit.rest.repos.listCommits, {
19
- sha: github.branch,
20
- owner: github.owner,
21
- repo: github.name,
22
- path,
23
- since: github.since,
24
- per_page: github.per_page,
25
- until: github.until,
26
- ...(author && { author })
27
- })
28
20
 
29
- return commits.map(commit => ({
30
- sha: commit.sha,
31
- date: commit.commit.author?.date ?? '',
32
- message: (commit.commit.message?.split('\n')[0] ?? '')
33
- }))
21
+ const allCommits = await Promise.all(
22
+ paths.map(path =>
23
+ octokit.paginate(octokit.rest.repos.listCommits, {
24
+ sha: github.branch,
25
+ owner: github.owner,
26
+ repo: github.name,
27
+ path,
28
+ since: github.since,
29
+ per_page: github.per_page,
30
+ until: github.until,
31
+ author
32
+ })
33
+ )
34
+ )
35
+
36
+ const uniqueCommits = new Map<string, { sha: string, date: string, message: string }>()
37
+ for (const commits of allCommits) {
38
+ for (const commit of commits) {
39
+ if (!uniqueCommits.has(commit.sha)) {
40
+ uniqueCommits.set(commit.sha, {
41
+ sha: commit.sha,
42
+ date: commit.commit.author?.date ?? '',
43
+ message: (commit.commit.message?.split('\n')[0] ?? '')
44
+ })
45
+ }
46
+ }
47
+ }
48
+
49
+ return Array.from(uniqueCommits.values()).sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
34
50
  }, {
35
51
  maxAge: 60 * 60,
36
52
  getKey: (event) => {
37
53
  const { path, author } = getQuery(event)
38
- return `commits-${path}${author ? `-${author}` : ''}`
54
+ const paths = Array.isArray(path) ? path : [path]
55
+ return `commits-${paths.join(',')}${author ? `-${author}` : ''}`
39
56
  }
40
57
  })