@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.
- package/README.md +265 -0
- package/app/app.config.ts +53 -0
- package/app/app.vue +73 -0
- package/app/components/AdsCarbon.vue +3 -0
- package/app/components/Footer.vue +32 -0
- package/app/components/PageHeaderLinks.vue +73 -0
- package/app/components/StarsBg.vue +122 -0
- package/app/components/content/ComponentEmits.vue +43 -0
- package/app/components/content/ComponentExample.vue +247 -0
- package/app/components/content/ComponentProps.vue +105 -0
- package/app/components/content/ComponentPropsLinks.vue +20 -0
- package/app/components/content/ComponentPropsSchema.vue +55 -0
- package/app/components/content/ComponentSlots.vue +50 -0
- package/app/components/content/HeroBackground.vue +65 -0
- package/app/components/content/HighlightInlineType.vue +39 -0
- package/app/components/content/Motion.vue +21 -0
- package/app/components/header/Header.vue +60 -0
- package/app/components/header/HeaderBody.vue +19 -0
- package/app/components/header/HeaderBottom.vue +26 -0
- package/app/components/header/HeaderLogo.vue +57 -0
- package/app/components/theme-picker/ThemePicker.vue +152 -0
- package/app/components/theme-picker/ThemePickerButton.vue +37 -0
- package/app/composables/fetchComponentExample.ts +32 -0
- package/app/composables/fetchComponentMeta.ts +34 -0
- package/app/composables/useCategory.ts +5 -0
- package/app/composables/useHeader.ts +6 -0
- package/app/composables/useNavigation.ts +89 -0
- package/app/error.vue +59 -0
- package/app/layouts/default.vue +3 -0
- package/app/layouts/docs.vue +31 -0
- package/app/pages/docs/[...slug].vue +158 -0
- package/app/pages/index.vue +30 -0
- package/app/pages/releases.vue +92 -0
- package/app/plugins/prettier.ts +67 -0
- package/app/plugins/theme.ts +82 -0
- package/app/types/index.d.ts +37 -0
- package/app/workers/prettier.js +36 -0
- package/content.config.ts +68 -0
- package/modules/component-example.ts +128 -0
- package/modules/component-meta.ts +22 -0
- package/modules/config.ts +79 -0
- package/modules/css.ts +33 -0
- package/nuxt.config.ts +80 -0
- package/package.json +55 -0
- package/server/api/component-example.get.ts +19 -0
- package/server/plugins/llms.ts +24 -0
- package/server/routes/raw/[...slug].md.get.ts +27 -0
- package/utils/git.ts +108 -0
- package/utils/meta.ts +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
[](https://docs.mhaibaraai.cn/)
|
|
2
|
+
|
|
3
|
+
> 一款由 Nuxt UI 和 Nuxt Content 强力驱动的优雅文档主题
|
|
4
|
+
|
|
5
|
+
[![npm version][npm-version-src]][npm-version-href]
|
|
6
|
+
[![npm downloads][npm-downloads-src]][npm-downloads-href]
|
|
7
|
+
[![License][license-src]][license-href]
|
|
8
|
+
[![Nuxt][nuxt-src]][nuxt-href]
|
|
9
|
+
|
|
10
|
+
使用此主题可以快速构建美观、专业的文档网站,内置内容管理、SEO、暗黑模式、全文搜索等功能。
|
|
11
|
+
|
|
12
|
+
- 📖 [在线文档](https://docs.mhaibaraai.cn/)
|
|
13
|
+
|
|
14
|
+
## ✨ 特性
|
|
15
|
+
|
|
16
|
+
此主题集成了一系列旨在优化文档管理体验的强大功能:
|
|
17
|
+
|
|
18
|
+
- ⚡ **基于 Nuxt 4** - 充分利用最新的 Nuxt 框架,实现卓越性能
|
|
19
|
+
- 🎨 **采用 Nuxt UI** - 集成全面的 UI 组件库,开箱即用
|
|
20
|
+
- 📝 **MDC 语法增强** - 支持 Markdown 与 Vue 组件的无缝集成,实现动态内容
|
|
21
|
+
- 🧩 **组件文档自动生成** - 自动生成 Props、Slots、Emits 文档及交互式示例
|
|
22
|
+
- 📚 **智能侧边栏导航** - 根据内容结构自动生成导航
|
|
23
|
+
- 🔍 **全文搜索** - 内置强大的全文搜索功能
|
|
24
|
+
- 🌙 **暗黑模式** - 支持亮色/暗色主题切换
|
|
25
|
+
- 📱 **响应式设计** - 移动优先的响应式布局
|
|
26
|
+
- 🚀 **SEO 优化** - 内置 SEO 优化功能
|
|
27
|
+
- 🎯 **TypeScript 支持** - 完整的 TypeScript 类型支持
|
|
28
|
+
- 🤖 **AI 助手优化** - 为 LLM 优化,提供更好的 AI 辅助文档体验
|
|
29
|
+
|
|
30
|
+
## 🚀 快速开始
|
|
31
|
+
|
|
32
|
+
### 使用模板创建项目
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# 使用此模板创建新项目
|
|
36
|
+
npx nuxi init -t gh:mhaibaraai/movk-nuxt-docs/template my-docs
|
|
37
|
+
|
|
38
|
+
# 进入项目目录
|
|
39
|
+
cd my-docs
|
|
40
|
+
|
|
41
|
+
# 启动开发服务器
|
|
42
|
+
pnpm dev
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 作为 Layer 使用
|
|
46
|
+
|
|
47
|
+
在现有 Nuxt 项目中使用 Movk Nuxt Docs 作为 layer:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# 安装依赖
|
|
51
|
+
pnpm add @movk/nuxt-docs better-sqlite3
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
在 CSS 中导入 Tailwind CSS 和 Nuxt UI
|
|
55
|
+
|
|
56
|
+
```css [~/assets/css/main.css]
|
|
57
|
+
@import 'tailwindcss';
|
|
58
|
+
@import '@nuxt/ui';
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
在 `nuxt.config.ts` 中配置:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
export default defineNuxtConfig({
|
|
65
|
+
extends: ['@movk/nuxt-docs'],
|
|
66
|
+
css: ['~/assets/css/main.css'],
|
|
67
|
+
llms: {
|
|
68
|
+
domain: 'https://docs.mhaibaraai.cn',
|
|
69
|
+
title: 'Movk Nuxt Docs',
|
|
70
|
+
description: '一款优雅的 Nuxt 文档主题'
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 📁 项目结构
|
|
76
|
+
|
|
77
|
+
### 完整项目结构
|
|
78
|
+
|
|
79
|
+
使用模板创建的项目结构:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
my-docs/
|
|
83
|
+
├── app/assets/css/main.css # 全局样式
|
|
84
|
+
├── content/ # Markdown 内容
|
|
85
|
+
│ ├── index.md # 首页
|
|
86
|
+
│ └── docs/ # 文档页面
|
|
87
|
+
├── public/ # 静态资源
|
|
88
|
+
├── nuxt.config.ts # Nuxt 配置
|
|
89
|
+
├── tsconfig.json # TypeScript 配置
|
|
90
|
+
├── package.json # 依赖与脚本
|
|
91
|
+
├── .npmrc # npm 配置
|
|
92
|
+
├── pnpm-workspace.yaml # pnpm 工作区配置
|
|
93
|
+
└── README.md # 项目说明
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Monorepo 结构
|
|
97
|
+
|
|
98
|
+
本项目采用 monorepo 结构:
|
|
99
|
+
|
|
100
|
+
- `/docs` - 官方文档站点
|
|
101
|
+
- `/layer` - Movk Nuxt Docs 主题 layer(`@movk/nuxt-docs`)
|
|
102
|
+
- `/template` - 项目模板
|
|
103
|
+
- `/scripts` - 构建脚本
|
|
104
|
+
|
|
105
|
+
## 📝 内容编写
|
|
106
|
+
|
|
107
|
+
### 基础 Markdown
|
|
108
|
+
|
|
109
|
+
```md
|
|
110
|
+
---
|
|
111
|
+
title: 页面标题
|
|
112
|
+
description: 页面描述
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
# 标题
|
|
116
|
+
|
|
117
|
+
这是一段普通的文本内容。
|
|
118
|
+
|
|
119
|
+
## 二级标题
|
|
120
|
+
|
|
121
|
+
- 列表项 1
|
|
122
|
+
- 列表项 2
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### MDC 语法
|
|
126
|
+
|
|
127
|
+
```md
|
|
128
|
+
::card
|
|
129
|
+
---
|
|
130
|
+
title: 卡片标题
|
|
131
|
+
icon: i-lucide-rocket
|
|
132
|
+
---
|
|
133
|
+
卡片内容
|
|
134
|
+
::
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
了解更多关于 MDC 语法,请查看 [Nuxt Content 文档](https://content.nuxt.com/docs/files/markdown#mdc-syntax)。
|
|
138
|
+
|
|
139
|
+
## 🔌 集成第三方服务
|
|
140
|
+
|
|
141
|
+
本主题不内置任何分析或监控工具,你可以根据需求自由选择。
|
|
142
|
+
|
|
143
|
+
### Vercel Analytics
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
pnpm add @vercel/analytics @vercel/speed-insights
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
创建 `app/plugins/analytics.client.ts`:
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import { Analytics } from '@vercel/analytics/nuxt'
|
|
153
|
+
import { SpeedInsights } from '@vercel/speed-insights/nuxt'
|
|
154
|
+
import { createApp, h } from 'vue'
|
|
155
|
+
|
|
156
|
+
export default defineNuxtPlugin({
|
|
157
|
+
name: 'vercel-analytics',
|
|
158
|
+
enforce: 'post',
|
|
159
|
+
hooks: {
|
|
160
|
+
'app:mounted': () => {
|
|
161
|
+
if (import.meta.dev) return
|
|
162
|
+
|
|
163
|
+
const container = document.createElement('div')
|
|
164
|
+
container.id = 'vercel-analytics'
|
|
165
|
+
document.body.appendChild(container)
|
|
166
|
+
|
|
167
|
+
const app = createApp({
|
|
168
|
+
render: () => h('div', { style: 'display: none;' }, [
|
|
169
|
+
h(Analytics, { debug: false }),
|
|
170
|
+
h(SpeedInsights, { debug: false })
|
|
171
|
+
])
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
app.mount(container)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### 其他工具
|
|
181
|
+
|
|
182
|
+
- **Google Analytics** - [@nuxtjs/google-analytics](https://google-analytics.nuxtjs.org/)
|
|
183
|
+
- **Plausible** - [vue-plausible](https://github.com/moritzsternemann/vue-plausible)
|
|
184
|
+
- **Umami** - [nuxt-umami](https://github.com/ijkml/nuxt-umami)
|
|
185
|
+
|
|
186
|
+
按照各工具的 Nuxt 集成文档在 `plugins` 目录中创建插件即可。
|
|
187
|
+
|
|
188
|
+
## 🛠️ 开发
|
|
189
|
+
|
|
190
|
+
### 本地开发
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
# 克隆项目
|
|
194
|
+
git clone https://github.com/mhaibaraai/movk-nuxt-docs.git
|
|
195
|
+
|
|
196
|
+
# 进入项目目录
|
|
197
|
+
cd movk-nuxt-docs
|
|
198
|
+
|
|
199
|
+
# 安装依赖
|
|
200
|
+
pnpm install
|
|
201
|
+
|
|
202
|
+
# 启动开发服务器
|
|
203
|
+
pnpm dev
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
开发服务器将在 `http://localhost:3000` 启动。
|
|
207
|
+
|
|
208
|
+
### 构建生产版本
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
# 构建应用
|
|
212
|
+
pnpm build
|
|
213
|
+
|
|
214
|
+
# 本地预览生产构建
|
|
215
|
+
pnpm preview
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### 发布
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
# 发布 layer 到 npm
|
|
222
|
+
pnpm release:layer
|
|
223
|
+
|
|
224
|
+
# 发布完整项目
|
|
225
|
+
pnpm release
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## ⚡ 技术栈
|
|
229
|
+
|
|
230
|
+
本项目基于以下优秀的开源项目构建:
|
|
231
|
+
|
|
232
|
+
- [Nuxt 4](https://nuxt.com/) - Web 框架
|
|
233
|
+
- [Nuxt Content](https://content.nuxt.com/) - 基于文件的 CMS
|
|
234
|
+
- [Nuxt UI](https://ui.nuxt.com/) - UI 组件库
|
|
235
|
+
- [Nuxt Image](https://image.nuxt.com/) - 图片优化
|
|
236
|
+
- [Tailwind CSS 4](https://tailwindcss.com/) - CSS 框架
|
|
237
|
+
- [Nuxt SEO](https://nuxtseo.com/) - SEO 优化
|
|
238
|
+
- [Nuxt LLMs](https://github.com/nuxt/llms) - AI 助手优化
|
|
239
|
+
|
|
240
|
+
## 📖 文档
|
|
241
|
+
|
|
242
|
+
访问 [Movk Nuxt Docs 文档](https://docs.mhaibaraai.cn/) 了解详细的使用指南和 API 文档。
|
|
243
|
+
|
|
244
|
+
## 🙏 致谢
|
|
245
|
+
|
|
246
|
+
本项目基于以下优秀项目构建或受其启发:
|
|
247
|
+
|
|
248
|
+
- [Docus](https://docus.dev/) - 由 Nuxt Content 团队开发的文档主题
|
|
249
|
+
- [Nuxt UI Docs Template](https://docs-template.nuxt.dev/) - Nuxt UI 官方文档模板
|
|
250
|
+
|
|
251
|
+
## 📄 许可证
|
|
252
|
+
|
|
253
|
+
[MIT](./LICENSE) License © 2024-PRESENT [YiXuan](https://github.com/mhaibaraai)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
<!-- Badges -->
|
|
257
|
+
|
|
258
|
+
[npm-version-src]: https://img.shields.io/npm/v/@movk/nuxt-docs/latest.svg?style=flat&colorA=020420&colorB=00DC82
|
|
259
|
+
[npm-version-href]: https://npmjs.com/package/@movk/nuxt-docs
|
|
260
|
+
[npm-downloads-src]: https://img.shields.io/npm/dm/@movk/nuxt-docs.svg?style=flat&colorA=020420&colorB=00DC82
|
|
261
|
+
[npm-downloads-href]: https://npm.chart.dev/@movk/nuxt-docs
|
|
262
|
+
[license-src]: https://img.shields.io/badge/License-MIT-blue.svg
|
|
263
|
+
[license-href]: https://npmjs.com/package/@movk/nuxt-docs
|
|
264
|
+
[nuxt-src]: https://img.shields.io/badge/Nuxt-4-00DC82?logo=nuxt.js&logoColor=fff
|
|
265
|
+
[nuxt-href]: https://nuxt.com
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ButtonProps } from '@nuxt/ui'
|
|
2
|
+
|
|
3
|
+
export default defineAppConfig({
|
|
4
|
+
toaster: {
|
|
5
|
+
expand: true,
|
|
6
|
+
position: 'top-right' as const,
|
|
7
|
+
duration: 3000,
|
|
8
|
+
max: 5
|
|
9
|
+
},
|
|
10
|
+
theme: {
|
|
11
|
+
radius: 0.25,
|
|
12
|
+
blackAsPrimary: false
|
|
13
|
+
},
|
|
14
|
+
ui: {
|
|
15
|
+
colors: {
|
|
16
|
+
primary: 'indigo',
|
|
17
|
+
neutral: 'zinc'
|
|
18
|
+
},
|
|
19
|
+
contentNavigation: {
|
|
20
|
+
slots: {
|
|
21
|
+
linkLeadingIcon: 'size-4 mr-1',
|
|
22
|
+
linkTrailing: 'hidden'
|
|
23
|
+
},
|
|
24
|
+
defaultVariants: {
|
|
25
|
+
variant: 'link'
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
pageLinks: {
|
|
29
|
+
slots: {
|
|
30
|
+
linkLeadingIcon: 'size-4',
|
|
31
|
+
linkLabelExternalIcon: 'size-2.5'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
header: {
|
|
36
|
+
title: 'Movk Nuxt Docs',
|
|
37
|
+
to: '/',
|
|
38
|
+
search: true,
|
|
39
|
+
colorMode: true,
|
|
40
|
+
links: [] as ButtonProps[]
|
|
41
|
+
},
|
|
42
|
+
footer: {
|
|
43
|
+
credits: `Copyright © 2024 - ${new Date().getFullYear()}`,
|
|
44
|
+
socials: [] as ButtonProps[]
|
|
45
|
+
},
|
|
46
|
+
toc: {
|
|
47
|
+
title: '页面导航',
|
|
48
|
+
bottom: {
|
|
49
|
+
title: '社区',
|
|
50
|
+
links: [] as ButtonProps[]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
})
|
package/app/app.vue
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import colors from 'tailwindcss/colors'
|
|
3
|
+
|
|
4
|
+
const site = useSiteConfig()
|
|
5
|
+
const appConfig = useAppConfig()
|
|
6
|
+
const colorMode = useColorMode()
|
|
7
|
+
const route = useRoute()
|
|
8
|
+
|
|
9
|
+
const { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('docs', ['category', 'description']))
|
|
10
|
+
const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSections('docs'), {
|
|
11
|
+
server: false
|
|
12
|
+
})
|
|
13
|
+
const color = computed(() => colorMode.value === 'dark' ? (colors as any)[appConfig.ui.colors.neutral][900] : 'white')
|
|
14
|
+
const radius = computed(() => `:root { --ui-radius: ${appConfig.theme.radius}rem; }`)
|
|
15
|
+
const blackAsPrimary = computed(() => appConfig.theme.blackAsPrimary ? `:root { --ui-primary: black; } .dark { --ui-primary: white; }` : ':root {}')
|
|
16
|
+
|
|
17
|
+
useHead({
|
|
18
|
+
meta: [
|
|
19
|
+
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
|
20
|
+
{ key: 'theme-color', name: 'theme-color', content: color }
|
|
21
|
+
],
|
|
22
|
+
style: [
|
|
23
|
+
{ innerHTML: radius, id: 'nuxt-ui-radius', tagPriority: -2 },
|
|
24
|
+
{ innerHTML: blackAsPrimary, id: 'nuxt-ui-black-as-primary', tagPriority: -2 }
|
|
25
|
+
]
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
useSeoMeta({
|
|
29
|
+
titleTemplate: appConfig.seo.titleTemplate,
|
|
30
|
+
title: appConfig.seo.title,
|
|
31
|
+
description: appConfig.seo.description,
|
|
32
|
+
ogSiteName: site.name,
|
|
33
|
+
twitterCard: 'summary_large_image'
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const { rootNavigation } = useNavigation(navigation)
|
|
37
|
+
|
|
38
|
+
provide('navigation', rootNavigation)
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<template>
|
|
42
|
+
<UApp :toaster="appConfig.toaster">
|
|
43
|
+
<NuxtLoadingIndicator color="var(--ui-primary)" :height="2" />
|
|
44
|
+
|
|
45
|
+
<div :class="[route.path.startsWith('/docs/') && 'root']">
|
|
46
|
+
<template v-if="!route.path.startsWith('/examples')">
|
|
47
|
+
<Header />
|
|
48
|
+
</template>
|
|
49
|
+
|
|
50
|
+
<NuxtLayout>
|
|
51
|
+
<NuxtPage />
|
|
52
|
+
</NuxtLayout>
|
|
53
|
+
|
|
54
|
+
<template v-if="!route.path.startsWith('/examples')">
|
|
55
|
+
<Footer />
|
|
56
|
+
|
|
57
|
+
<ClientOnly>
|
|
58
|
+
<LazyUContentSearch :files="files" :navigation="navigation" />
|
|
59
|
+
</ClientOnly>
|
|
60
|
+
</template>
|
|
61
|
+
</div>
|
|
62
|
+
</UApp>
|
|
63
|
+
</template>
|
|
64
|
+
|
|
65
|
+
<style>
|
|
66
|
+
/* Safelist (do not remove): [&>div]:*:my-0 [&>div]:*:w-full h-64 !px-0 !py-0 !pt-0 !pb-0 !p-0 !justify-start !justify-end !min-h-96 h-136 max-h-[341px] */
|
|
67
|
+
|
|
68
|
+
@media (min-width: 1024px) {
|
|
69
|
+
.root {
|
|
70
|
+
--ui-header-height: 112px;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
</style>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const route = useRoute()
|
|
3
|
+
const { footer } = useAppConfig()
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<USeparator :icon="route.path === '/' ? undefined : 'i-simple-icons-nuxtdotjs'" class="h-px" />
|
|
8
|
+
|
|
9
|
+
<UFooter
|
|
10
|
+
:ui="{
|
|
11
|
+
left: 'text-sm text-muted',
|
|
12
|
+
root: 'border-t border-default'
|
|
13
|
+
}"
|
|
14
|
+
>
|
|
15
|
+
<template #left>
|
|
16
|
+
<MDC v-if="footer?.credits" :value="footer.credits" unwrap="p" />
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<template #right>
|
|
20
|
+
<template v-if="footer.socials?.length">
|
|
21
|
+
<UTooltip
|
|
22
|
+
v-for="(link, count) in footer.socials"
|
|
23
|
+
:key="count"
|
|
24
|
+
:text="link.label || (link as any)['aria-label']"
|
|
25
|
+
class="hidden lg:flex"
|
|
26
|
+
>
|
|
27
|
+
<UButton v-bind="{ color: 'neutral', variant: 'ghost', ...link }" />
|
|
28
|
+
</UTooltip>
|
|
29
|
+
</template>
|
|
30
|
+
</template>
|
|
31
|
+
</UFooter>
|
|
32
|
+
</template>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const route = useRoute()
|
|
3
|
+
const toast = useToast()
|
|
4
|
+
const { copy, copied } = useClipboard()
|
|
5
|
+
const site = useSiteConfig()
|
|
6
|
+
|
|
7
|
+
const mdPath = computed(() => `${site.url}/raw${route.path}.md`)
|
|
8
|
+
|
|
9
|
+
const items = [
|
|
10
|
+
{
|
|
11
|
+
label: 'Copy Markdown link',
|
|
12
|
+
icon: 'i-lucide-link',
|
|
13
|
+
onSelect() {
|
|
14
|
+
copy(mdPath.value)
|
|
15
|
+
toast.add({
|
|
16
|
+
title: 'Copied to clipboard',
|
|
17
|
+
icon: 'i-lucide-check-circle'
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
label: 'View as Markdown',
|
|
23
|
+
icon: 'i-simple-icons:markdown',
|
|
24
|
+
target: '_blank',
|
|
25
|
+
to: `/raw${route.path}.md`
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
label: 'Open in ChatGPT',
|
|
29
|
+
icon: 'i-simple-icons:openai',
|
|
30
|
+
target: '_blank',
|
|
31
|
+
to: `https://chatgpt.com/?hints=search&q=${encodeURIComponent(`Read ${mdPath.value} so I can ask questions about it.`)}`
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
label: 'Open in Claude',
|
|
35
|
+
icon: 'i-simple-icons:anthropic',
|
|
36
|
+
target: '_blank',
|
|
37
|
+
to: `https://claude.ai/new?q=${encodeURIComponent(`Read ${mdPath.value} so I can ask questions about it.`)}`
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
async function copyPage() {
|
|
42
|
+
copy(await $fetch<string>(`/raw${route.path}.md`))
|
|
43
|
+
}
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<template>
|
|
47
|
+
<UFieldGroup size="sm">
|
|
48
|
+
<UButton
|
|
49
|
+
label="Copy page"
|
|
50
|
+
:icon="copied ? 'i-lucide-copy-check' : 'i-lucide-copy'"
|
|
51
|
+
color="neutral"
|
|
52
|
+
variant="outline"
|
|
53
|
+
:ui="{
|
|
54
|
+
leadingIcon: [copied ? 'text-primary' : 'text-neutral', 'size-3.5']
|
|
55
|
+
}"
|
|
56
|
+
@click="copyPage"
|
|
57
|
+
/>
|
|
58
|
+
<UDropdownMenu
|
|
59
|
+
size="sm"
|
|
60
|
+
:items="items"
|
|
61
|
+
:content="{
|
|
62
|
+
align: 'end',
|
|
63
|
+
side: 'bottom',
|
|
64
|
+
sideOffset: 8
|
|
65
|
+
}"
|
|
66
|
+
:ui="{
|
|
67
|
+
content: 'w-48'
|
|
68
|
+
}"
|
|
69
|
+
>
|
|
70
|
+
<UButton icon="i-lucide-chevron-down" color="neutral" variant="outline" />
|
|
71
|
+
</UDropdownMenu>
|
|
72
|
+
</UFieldGroup>
|
|
73
|
+
</template>
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { kebabCase } from 'scule'
|
|
3
|
+
|
|
4
|
+
interface Star {
|
|
5
|
+
x: number
|
|
6
|
+
y: number
|
|
7
|
+
size: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { starCount = 300, color = 'var(--ui-primary)', size = { min: 1, max: 2 } } = defineProps<{
|
|
11
|
+
starCount?: number
|
|
12
|
+
color?: string
|
|
13
|
+
size?: { min: number, max: number }
|
|
14
|
+
}>()
|
|
15
|
+
|
|
16
|
+
const route = useRoute()
|
|
17
|
+
|
|
18
|
+
// Generate random star positions and sizes
|
|
19
|
+
function generateStars(count: number): Star[] {
|
|
20
|
+
return Array.from({ length: count }, () => ({
|
|
21
|
+
x: Math.floor(Math.random() * 2000),
|
|
22
|
+
y: Math.floor(Math.random() * 2000),
|
|
23
|
+
size: typeof size === 'number'
|
|
24
|
+
? size
|
|
25
|
+
: Math.random() * (size.max - size.min) + size.min
|
|
26
|
+
}))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Define speed configurations once
|
|
30
|
+
const speedMap = {
|
|
31
|
+
slow: { duration: 200, opacity: 0.5, ratio: 0.3 },
|
|
32
|
+
normal: { duration: 150, opacity: 0.75, ratio: 0.3 },
|
|
33
|
+
fast: { duration: 100, opacity: 1, ratio: 0.4 }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Use a more efficient approach to generate and store stars
|
|
37
|
+
const stars = useState<{ slow: Star[], normal: Star[], fast: Star[] }>(`${kebabCase(route.path)}-stars`, () => {
|
|
38
|
+
return {
|
|
39
|
+
slow: generateStars(Math.floor(starCount * speedMap.slow.ratio)),
|
|
40
|
+
normal: generateStars(Math.floor(starCount * speedMap.normal.ratio)),
|
|
41
|
+
fast: generateStars(Math.floor(starCount * speedMap.fast.ratio))
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// Compute star layers with different speeds and opacities
|
|
46
|
+
const starLayers = computed(() => [
|
|
47
|
+
{ stars: stars.value.fast, ...speedMap.fast },
|
|
48
|
+
{ stars: stars.value.normal, ...speedMap.normal },
|
|
49
|
+
{ stars: stars.value.slow, ...speedMap.slow }
|
|
50
|
+
])
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<template>
|
|
54
|
+
<div class="absolute pointer-events-none z-[-1] inset-y-0 inset-x-5 sm:inset-x-7 lg:inset-x-9 overflow-hidden">
|
|
55
|
+
<div class="stars size-full absolute inset-x-0 top-0">
|
|
56
|
+
<div
|
|
57
|
+
v-for="(layer, index) in starLayers"
|
|
58
|
+
:key="index"
|
|
59
|
+
class="star-layer"
|
|
60
|
+
:style="{
|
|
61
|
+
'--star-duration': `${layer.duration}s`,
|
|
62
|
+
'--star-opacity': layer.opacity,
|
|
63
|
+
'--star-color': color
|
|
64
|
+
}"
|
|
65
|
+
>
|
|
66
|
+
<div
|
|
67
|
+
v-for="(star, starIndex) in layer.stars"
|
|
68
|
+
:key="starIndex"
|
|
69
|
+
class="star absolute rounded-full"
|
|
70
|
+
:style="{
|
|
71
|
+
left: `${star.x}px`,
|
|
72
|
+
top: `${star.y}px`,
|
|
73
|
+
width: `${star.size}px`,
|
|
74
|
+
height: `${star.size}px`,
|
|
75
|
+
backgroundColor: 'var(--star-color)',
|
|
76
|
+
opacity: 'var(--star-opacity)'
|
|
77
|
+
}"
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</template>
|
|
83
|
+
|
|
84
|
+
<style scoped>
|
|
85
|
+
.stars {
|
|
86
|
+
left: 50%;
|
|
87
|
+
transform: translate(-50%);
|
|
88
|
+
-webkit-mask-image: linear-gradient(
|
|
89
|
+
180deg,
|
|
90
|
+
rgba(217, 217, 217, 0) 0%,
|
|
91
|
+
rgba(217, 217, 217, 0.8) 25%,
|
|
92
|
+
#d9d9d9 50%,
|
|
93
|
+
rgba(217, 217, 217, 0.8) 75%,
|
|
94
|
+
rgba(217, 217, 217, 0) 100%
|
|
95
|
+
);
|
|
96
|
+
mask-image: linear-gradient(
|
|
97
|
+
180deg,
|
|
98
|
+
rgba(217, 217, 217, 0) 0%,
|
|
99
|
+
rgba(217, 217, 217, 0.8) 25%,
|
|
100
|
+
#d9d9d9 50%,
|
|
101
|
+
rgba(217, 217, 217, 0.8) 75%,
|
|
102
|
+
rgba(217, 217, 217, 0) 100%
|
|
103
|
+
);
|
|
104
|
+
-webkit-mask-size: cover;
|
|
105
|
+
mask-size: cover;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.star-layer {
|
|
109
|
+
animation: risingStarsAnimation linear infinite;
|
|
110
|
+
animation-duration: var(--star-duration);
|
|
111
|
+
will-change: transform;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@keyframes risingStarsAnimation {
|
|
115
|
+
0% {
|
|
116
|
+
transform: translateY(0);
|
|
117
|
+
}
|
|
118
|
+
100% {
|
|
119
|
+
transform: translateY(-2000px);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
</style>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { camelCase } from 'scule'
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{
|
|
5
|
+
/**
|
|
6
|
+
* The slug of the component to fetch emits for.
|
|
7
|
+
* @defaultValue route path's last segment
|
|
8
|
+
*/
|
|
9
|
+
slug?: string
|
|
10
|
+
}>()
|
|
11
|
+
|
|
12
|
+
const route = useRoute()
|
|
13
|
+
const componentName = camelCase(props.slug ?? route.path.split('/').pop() ?? '')
|
|
14
|
+
|
|
15
|
+
const meta = await fetchComponentMeta(componentName as any)
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<ProseTable>
|
|
20
|
+
<ProseThead>
|
|
21
|
+
<ProseTr>
|
|
22
|
+
<ProseTh>
|
|
23
|
+
Event
|
|
24
|
+
</ProseTh>
|
|
25
|
+
<ProseTh>
|
|
26
|
+
Type
|
|
27
|
+
</ProseTh>
|
|
28
|
+
</ProseTr>
|
|
29
|
+
</ProseThead>
|
|
30
|
+
<ProseTbody>
|
|
31
|
+
<ProseTr v-for="event in (meta?.meta?.events || [])" :key="event.name">
|
|
32
|
+
<ProseTd>
|
|
33
|
+
<ProseCode>
|
|
34
|
+
{{ event.name }}
|
|
35
|
+
</ProseCode>
|
|
36
|
+
</ProseTd>
|
|
37
|
+
<ProseTd>
|
|
38
|
+
<HighlightInlineType v-if="event.type" :type="event.type" />
|
|
39
|
+
</ProseTd>
|
|
40
|
+
</ProseTr>
|
|
41
|
+
</ProseTbody>
|
|
42
|
+
</ProseTable>
|
|
43
|
+
</template>
|