clarity-theme 0.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/LICENSE +21 -0
- package/README.md +276 -0
- package/README.zh-CN.md +297 -0
- package/app/app.config.ts +110 -0
- package/app/app.vue +5 -0
- package/app/assets/css/_variable.scss +8 -0
- package/app/assets/css/animation.scss +50 -0
- package/app/assets/css/article.scss +152 -0
- package/app/assets/css/color.scss +58 -0
- package/app/assets/css/font.scss +35 -0
- package/app/assets/css/main.scss +230 -0
- package/app/assets/css/reusable.scss +186 -0
- package/app/assets/icons/solar-rewind-back-bold-duotone.svg +1 -0
- package/app/assets/icons/solar-rewind-forward-bold-duotone.svg +1 -0
- package/app/components/blog/BlogAside.vue +63 -0
- package/app/components/blog/BlogFooter.vue +61 -0
- package/app/components/blog/BlogHeader.global.vue +146 -0
- package/app/components/blog/BlogPanel.vue +79 -0
- package/app/components/blog/BlogSidebar.vue +160 -0
- package/app/components/blog/BlogWidget.vue +134 -0
- package/app/components/blog/Mask.vue +42 -0
- package/app/components/blog/SkipToContent.vue +22 -0
- package/app/components/blog/ThemeToggle.vue +50 -0
- package/app/components/content/Alert.vue +96 -0
- package/app/components/content/Badge.vue +90 -0
- package/app/components/content/Blur.vue +22 -0
- package/app/components/content/CardList.vue +32 -0
- package/app/components/content/Chat.vue +79 -0
- package/app/components/content/Copy.vue +134 -0
- package/app/components/content/EmojiClock.vue +53 -0
- package/app/components/content/FeedCard.vue +212 -0
- package/app/components/content/FeedGroup.vue +130 -0
- package/app/components/content/Folding.vue +69 -0
- package/app/components/content/Key.vue +164 -0
- package/app/components/content/LinkBanner.vue +59 -0
- package/app/components/content/LinkCard.vue +74 -0
- package/app/components/content/MdTitle.vue +26 -0
- package/app/components/content/Mermaid.vue +133 -0
- package/app/components/content/MusicScore.vue +178 -0
- package/app/components/content/Pic.vue +55 -0
- package/app/components/content/Poetry.vue +41 -0
- package/app/components/content/ProseA.vue +40 -0
- package/app/components/content/ProseCode.vue +95 -0
- package/app/components/content/ProsePre.vue +291 -0
- package/app/components/content/ProseTable.vue +70 -0
- package/app/components/content/Quote.vue +51 -0
- package/app/components/content/Tab.vue +100 -0
- package/app/components/content/Timeline.vue +73 -0
- package/app/components/content/Tip.vue +52 -0
- package/app/components/content/VideoEmbed.vue +127 -0
- package/app/components/partial/Button.vue +98 -0
- package/app/components/partial/DlGroup.vue +69 -0
- package/app/components/partial/Dropdown.vue +54 -0
- package/app/components/partial/Error.vue +70 -0
- package/app/components/partial/Expand.vue +51 -0
- package/app/components/partial/IconNavList.vue +42 -0
- package/app/components/partial/Pagination.vue +113 -0
- package/app/components/partial/RadioGroup.vue +77 -0
- package/app/components/partial/Secret.vue +23 -0
- package/app/components/partial/Slider.vue +96 -0
- package/app/components/partial/Toggle.vue +70 -0
- package/app/components/popover/Lightbox.vue +77 -0
- package/app/components/popover/Search.vue +214 -0
- package/app/components/popover/SearchItem.vue +64 -0
- package/app/components/post/Archive.vue +104 -0
- package/app/components/post/Article.vue +138 -0
- package/app/components/post/Comment.vue +275 -0
- package/app/components/post/Excerpt.vue +66 -0
- package/app/components/post/OrderToggle.vue +101 -0
- package/app/components/post/PostFooter.vue +80 -0
- package/app/components/post/PostHeader.vue +147 -0
- package/app/components/post/PostSurround.vue +111 -0
- package/app/components/post/Slide.vue +215 -0
- package/app/components/util/Date.vue +62 -0
- package/app/components/util/HydrateSafe.vue +10 -0
- package/app/components/util/Img.vue +38 -0
- package/app/components/util/Link.vue +11 -0
- package/app/components/widget/BlogLog.vue +27 -0
- package/app/components/widget/BlogStats.vue +39 -0
- package/app/components/widget/BlogTech.vue +65 -0
- package/app/components/widget/Empty.vue +3 -0
- package/app/components/widget/Toc.vue +129 -0
- package/app/composables/useArticle.ts +118 -0
- package/app/composables/useAvoid.ts +87 -0
- package/app/composables/useCopy.ts +21 -0
- package/app/composables/usePagination.ts +70 -0
- package/app/composables/useShiki.ts +129 -0
- package/app/composables/useToc.ts +57 -0
- package/app/composables/useWidgets.ts +56 -0
- package/app/error.vue +34 -0
- package/app/layouts/default.vue +66 -0
- package/app/pages/[...slug].vue +59 -0
- package/app/pages/archive.vue +207 -0
- package/app/pages/index.vue +83 -0
- package/app/pages/link.vue +65 -0
- package/app/pages/preview.vue +59 -0
- package/app/plugins/easter-egg.ts +23 -0
- package/app/plugins/init.ts +6 -0
- package/app/plugins/tippy.ts +22 -0
- package/app/shiki.config.ts +8 -0
- package/app/stores/layout.ts +34 -0
- package/app/stores/search.ts +37 -0
- package/app/types/article.ts +16 -0
- package/app/types/feed.ts +1 -0
- package/app/types/index.ts +14 -0
- package/app/types/modal.ts +16 -0
- package/app/types/nav.ts +11 -0
- package/app/utils/anim.ts +37 -0
- package/app/utils/img.ts +83 -0
- package/config/app.ts +88 -0
- package/config/content.d.mts +1 -0
- package/config/content.mjs +69 -0
- package/config/content.ts +91 -0
- package/config/define.mjs +21 -0
- package/config/define.ts +23 -0
- package/config/feed.ts +42 -0
- package/config/feeds.empty.ts +4 -0
- package/config/index.d.mts +1 -0
- package/config/index.mjs +6 -0
- package/config/index.ts +6 -0
- package/config/public.ts +71 -0
- package/config/schema.d.mts +1 -0
- package/config/schema.mjs +103 -0
- package/config/schema.ts +129 -0
- package/config/server.ts +35 -0
- package/img/index.d.mts +1 -0
- package/img/index.mjs +78 -0
- package/img/index.ts +8 -0
- package/modules/clarity-config/anti-mirror-client.ts +11 -0
- package/modules/clarity-config/index.ts +324 -0
- package/nuxt.config.ts +225 -0
- package/package.json +148 -0
- package/public/assets/atom.css +274 -0
- package/public/assets/atom.xsl +110 -0
- package/public/fonts/AlimamaFangYuanTi.woff2 +0 -0
- package/remark-plugins/rehype-meta-slots.d.mts +1 -0
- package/remark-plugins/rehype-meta-slots.mjs +49 -0
- package/remark-plugins/rehype-meta-slots.ts +58 -0
- package/remark-plugins/remark-code-component.d.mts +1 -0
- package/remark-plugins/remark-code-component.mjs +25 -0
- package/remark-plugins/remark-code-component.ts +41 -0
- package/server/api/stats.get.ts +109 -0
- package/server/routes/atom.xml.get.ts +101 -0
- package/server/routes/subscriptions.opml.get.ts +70 -0
- package/server/utils/clarity.ts +11 -0
- package/shared/types/content.ts +30 -0
- package/shared/utils/clarity.ts +34 -0
- package/shared/utils/icon.ts +218 -0
- package/shared/utils/link.ts +52 -0
- package/shared/utils/str.ts +88 -0
- package/shared/utils/time.ts +116 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type LayoutState = 'none' | 'sidebar' | 'aside' | 'search' | 'lightbox'
|
|
2
|
+
|
|
3
|
+
export const useLayoutStore = defineStore('layout', () => {
|
|
4
|
+
const router = useRouter()
|
|
5
|
+
|
|
6
|
+
const state = ref<LayoutState>('none')
|
|
7
|
+
const avoidTargets = ref<AvoidTarget[]>([])
|
|
8
|
+
|
|
9
|
+
const close = () => state.value = 'none'
|
|
10
|
+
|
|
11
|
+
const toggle = (key: LayoutState) => {
|
|
12
|
+
if (state.value === key)
|
|
13
|
+
return close()
|
|
14
|
+
state.value = key
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
useEventListener('keydown', (e) => {
|
|
18
|
+
if (state.value !== 'none' && e.key === 'Escape') {
|
|
19
|
+
e.preventDefault()
|
|
20
|
+
close()
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
router.beforeEach(() => {
|
|
25
|
+
close()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
state,
|
|
30
|
+
avoidTargets,
|
|
31
|
+
close,
|
|
32
|
+
toggle,
|
|
33
|
+
}
|
|
34
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { LazyPopoverSearch } from '#components'
|
|
2
|
+
|
|
3
|
+
export const useSearchStore = defineStore('search', () => {
|
|
4
|
+
// 搜索框应和侧边栏状态联动
|
|
5
|
+
const layoutStore = useLayoutStore()
|
|
6
|
+
const modalStore = useModalStore()
|
|
7
|
+
|
|
8
|
+
const word = ref('')
|
|
9
|
+
const debouncedWord = refDebounced(word)
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
open: _open,
|
|
13
|
+
close: _close,
|
|
14
|
+
} = modalStore.use(() => h(LazyPopoverSearch, {
|
|
15
|
+
onClose: () => {
|
|
16
|
+
_close()
|
|
17
|
+
layoutStore.close()
|
|
18
|
+
},
|
|
19
|
+
}), {
|
|
20
|
+
unique: true,
|
|
21
|
+
duration: 200,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// 从外部调用时应该操作 layoutStore
|
|
25
|
+
watch(() => layoutStore.state, (state) => {
|
|
26
|
+
if (state !== 'search')
|
|
27
|
+
return _close()
|
|
28
|
+
|
|
29
|
+
word.value = window.getSelection()?.toString().trim() || word.value
|
|
30
|
+
_open()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
word,
|
|
35
|
+
debouncedWord,
|
|
36
|
+
}
|
|
37
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ClarityAppConfig } from '../../config/app'
|
|
2
|
+
import type { ArticleSchema } from '../../config/content'
|
|
3
|
+
import type { MetaSlotsTree } from '../../remark-plugins/rehype-meta-slots'
|
|
4
|
+
|
|
5
|
+
export type ArticleOrderType = keyof ClarityAppConfig['article']['order']
|
|
6
|
+
|
|
7
|
+
export interface ArticleProps extends ArticleSchema {
|
|
8
|
+
path: string
|
|
9
|
+
|
|
10
|
+
meta?: {
|
|
11
|
+
coverDim?: boolean
|
|
12
|
+
coverFilter?: string
|
|
13
|
+
hideInfo?: boolean
|
|
14
|
+
slots?: Record<string, MetaSlotsTree>
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { FeedEntry, FeedGroup } from '../../config/feed'
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 与 @bikariya/modals 的 runtime/types 保持一致的类型定义。
|
|
3
|
+
*
|
|
4
|
+
* 该包 exports 未暴露 runtime/types 子路径,其 `#modals` 虚拟别名在
|
|
5
|
+
* 真实 npm 消费项目中无法被 Vue SFC 编译器稳定解析,因此 Theme 内联维护。
|
|
6
|
+
*/
|
|
7
|
+
export interface ModalProps {
|
|
8
|
+
/** 过渡动画时长 (ms) @default 400 */
|
|
9
|
+
duration?: number
|
|
10
|
+
/** 模态框开关状态 @internal */
|
|
11
|
+
open?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ModalEmits {
|
|
15
|
+
close: []
|
|
16
|
+
}
|
package/app/types/nav.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { toArray } from '@vueuse/core'
|
|
2
|
+
|
|
3
|
+
interface Rect {
|
|
4
|
+
left: number | string
|
|
5
|
+
top: number | string
|
|
6
|
+
width: number | string
|
|
7
|
+
height: number | string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type MaybeArray<T> = T | T[]
|
|
11
|
+
|
|
12
|
+
function toRect(rect: Element | Rect): Rect {
|
|
13
|
+
return rect instanceof Element ? rect.getBoundingClientRect() : rect
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ensurePx = (val: number | string) => typeof val === 'number' ? `${val}px` : val
|
|
17
|
+
|
|
18
|
+
export function animateBetweenRects(
|
|
19
|
+
el: MaybeRefOrGetter<Element>,
|
|
20
|
+
rect: MaybeArray<MaybeRefOrGetter<Element> | Rect>,
|
|
21
|
+
options?: KeyframeAnimationOptions,
|
|
22
|
+
) {
|
|
23
|
+
const rects = toArray(rect).map(r => toRect(toValue(r)))
|
|
24
|
+
|
|
25
|
+
return toValue(el).animate(rects.map(r => ({
|
|
26
|
+
left: ensurePx(r.left),
|
|
27
|
+
top: ensurePx(r.top),
|
|
28
|
+
width: ensurePx(r.width),
|
|
29
|
+
height: ensurePx(r.height),
|
|
30
|
+
})), {
|
|
31
|
+
duration: 100,
|
|
32
|
+
fill: 'forwards',
|
|
33
|
+
...options,
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const getFixedDelay = (s: number, fixed = 2) => ({ '--delay': `${s.toFixed(fixed)}s` })
|
package/app/utils/img.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// @keep-sorted
|
|
2
|
+
const services = {
|
|
3
|
+
baidu: 'https://image.baidu.com/search/down?url=',
|
|
4
|
+
/** https://webp.se/fly/ */
|
|
5
|
+
fly: 'https://fly.webp.se/?url=',
|
|
6
|
+
/** https://wsrv.nl/docs/ */
|
|
7
|
+
weserv: 'https://wsrv.nl/?url=',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type ImgService = keyof typeof services | boolean
|
|
11
|
+
|
|
12
|
+
// https://wsrv.nl/docs/quick-reference.html
|
|
13
|
+
export function getWsrvGhAvatar(name = '', options: Record<string, any> = { size: 92 }) {
|
|
14
|
+
const srcUrl = `github.com/${name}.png?size=${options.size}`
|
|
15
|
+
delete options.size
|
|
16
|
+
|
|
17
|
+
const params = new URLSearchParams(srcUrl)
|
|
18
|
+
Object.entries(options).forEach(([key, value]) => params.set(key, value))
|
|
19
|
+
return services.weserv + params.toString()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// https://docs.webp.se/public-services/github-avatar/
|
|
23
|
+
export function getGithubAvatar(name = '', options = { size: 120 }) {
|
|
24
|
+
return `https://avatars-githubusercontent-webp.webp.se/${name}?s=${options.size}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const getGithubIcon = (name = '') => getWsrvGhAvatar(name, { size: 32, mask: 'circle' })
|
|
28
|
+
|
|
29
|
+
export enum OicqAvatarSize {
|
|
30
|
+
Size1080,
|
|
31
|
+
Size40,
|
|
32
|
+
Size40_,
|
|
33
|
+
Size100,
|
|
34
|
+
Size140,
|
|
35
|
+
Size640,
|
|
36
|
+
Size40__ = 40,
|
|
37
|
+
Size100_ = 100,
|
|
38
|
+
Size640_ = 640,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// https://users.qzone.qq.com/fcg-bin/cgi_get_portrait.fcg?uins=
|
|
42
|
+
export function getOicqAvatar(qq = '', size = OicqAvatarSize.Size140) {
|
|
43
|
+
return `https://q1.qlogo.cn/g?b=qq&nk=${qq}&s=${size}`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export enum QgroupAvatarSize {
|
|
47
|
+
Size640,
|
|
48
|
+
Size100 = 100,
|
|
49
|
+
Size640_ = 640,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getOciqGroupAvatar(group = '', size = QgroupAvatarSize.Size100) {
|
|
53
|
+
return `https://p.qlogo.cn/gh/${group}/${group}/${size}/`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface GstaticFaviconOptions {
|
|
57
|
+
provider?: 'gstatic'
|
|
58
|
+
size?: 16 | 32 | 64 | 96 | 128 | 256 | 512
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface WebpseFaviconOptions {
|
|
62
|
+
provider: 'google' | 'duckduckgo' | 'microlink'
|
|
63
|
+
size?: number
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// https://github.com/microlinkhq/unavatar
|
|
67
|
+
// https://docs.webp.se/public-services/unavatar/
|
|
68
|
+
export function getFavicon(domain: string, options?: GstaticFaviconOptions | WebpseFaviconOptions) {
|
|
69
|
+
const { provider = 'gstatic', size = 32 } = options || {}
|
|
70
|
+
if (provider === 'gstatic')
|
|
71
|
+
return `https://t0.gstatic.cn/faviconV2?client=SOCIAL&fallback_opts=SIZE&url=http://${domain}&size=${size}`
|
|
72
|
+
return `https://unavatar.webp.se/${provider}/${domain}?w=${size}`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getImgUrl(src: string, service?: ImgService | true) {
|
|
76
|
+
if (!service)
|
|
77
|
+
return src
|
|
78
|
+
if (service === true)
|
|
79
|
+
service = 'fly'
|
|
80
|
+
if (service in services)
|
|
81
|
+
return services[service] + src
|
|
82
|
+
return src
|
|
83
|
+
}
|
package/config/app.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { NavItem } from '../app/types/nav'
|
|
2
|
+
import type { ClarityPublicConfig } from './public'
|
|
3
|
+
|
|
4
|
+
export interface NavGroup {
|
|
5
|
+
title: string
|
|
6
|
+
items: NavItem[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Theme 提供的 UI 默认值,全部可由消费项目在 app/app.config.ts 中覆盖 */
|
|
10
|
+
export interface ClarityUiConfig {
|
|
11
|
+
component: {
|
|
12
|
+
alert: {
|
|
13
|
+
defaultStyle: 'card' | 'flat'
|
|
14
|
+
}
|
|
15
|
+
codeblock: {
|
|
16
|
+
triggerRows: number
|
|
17
|
+
collapsedRows: number
|
|
18
|
+
enableIndentGuide: boolean
|
|
19
|
+
indent: number
|
|
20
|
+
tabSize: number
|
|
21
|
+
}
|
|
22
|
+
excerpt: {
|
|
23
|
+
animation: boolean
|
|
24
|
+
caret: string
|
|
25
|
+
}
|
|
26
|
+
slide: {
|
|
27
|
+
showTitle: boolean
|
|
28
|
+
}
|
|
29
|
+
stats: {
|
|
30
|
+
/** 归档页面每年标题对应的年龄 */
|
|
31
|
+
birthYear?: number
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
footer: {
|
|
35
|
+
/** 页脚版权信息,支持 <br> 等内联 HTML */
|
|
36
|
+
copyright: string
|
|
37
|
+
/** 侧边栏底部图标导航 */
|
|
38
|
+
iconNav: NavItem[]
|
|
39
|
+
/** 页脚站点地图 */
|
|
40
|
+
nav: NavGroup[]
|
|
41
|
+
}
|
|
42
|
+
header: {
|
|
43
|
+
/** 左侧栏顶部 Logo,默认使用 site.author.avatar */
|
|
44
|
+
logo: string
|
|
45
|
+
/** 展示标题文本,否则仅展示 Logo */
|
|
46
|
+
showTitle: boolean
|
|
47
|
+
/** 标题下的副标题,默认使用 site.subtitle */
|
|
48
|
+
subtitle: string
|
|
49
|
+
/** 标题后的随机表情 */
|
|
50
|
+
emojiTail: string[]
|
|
51
|
+
}
|
|
52
|
+
link: {
|
|
53
|
+
/** 无订阅源展示静音图标 */
|
|
54
|
+
remindNoFeed: boolean
|
|
55
|
+
/** 友链分组内随机排序 */
|
|
56
|
+
randomInGroup: boolean
|
|
57
|
+
}
|
|
58
|
+
/** 左侧栏导航 */
|
|
59
|
+
nav: NavGroup[]
|
|
60
|
+
pagination: {
|
|
61
|
+
perPage: number
|
|
62
|
+
/** 默认排序方式,需为 article.order 中的键名 */
|
|
63
|
+
sortOrder: string
|
|
64
|
+
/** 允许文章列表正序,开启后排序方式左侧图标可切换顺序 */
|
|
65
|
+
allowAscending: boolean
|
|
66
|
+
}
|
|
67
|
+
themes: Record<'light' | 'system' | 'dark', {
|
|
68
|
+
icon: string
|
|
69
|
+
tip: string
|
|
70
|
+
}>
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type ClarityAppConfig = ClarityPublicConfig & ClarityUiConfig
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* app/app.config.ts 中 `clarity` 键的覆盖输入类型:
|
|
77
|
+
* UI 配置深度可选(数组整体替换,与 defu 行为一致)。
|
|
78
|
+
*
|
|
79
|
+
* 站点级字段(site / article / feed / stats / integrations / features / changelog)
|
|
80
|
+
* 不属于 app.config.ts,请写入 clarity.config.ts。
|
|
81
|
+
*/
|
|
82
|
+
export type ClarityUiConfigInput<T = ClarityUiConfig> = {
|
|
83
|
+
[K in keyof T]?: T[K] extends readonly unknown[]
|
|
84
|
+
? T[K]
|
|
85
|
+
: T[K] extends object
|
|
86
|
+
? ClarityUiConfigInput<T[K]>
|
|
87
|
+
: T[K]
|
|
88
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './content.ts'
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { defineCollection, defineContentConfig } from '@nuxt/content'
|
|
2
|
+
import { defineSitemapSchema } from '@nuxtjs/sitemap/content'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { clarityConfigSchema } from './schema.mjs'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* createClarityContentConfig 运行时实现(JS)。
|
|
8
|
+
*
|
|
9
|
+
* 类型真源在同目录 content.ts;
|
|
10
|
+
* @nuxt/content 的配置加载使用 Node 原生 TS 剥离,
|
|
11
|
+
* 不允许 node_modules 内的 TS 文件,因此运行时链必须是 .mjs。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
function createArticleSchema(config) {
|
|
15
|
+
// 兜底:types 为空时 z.enum([]) 非法,统一回退到默认版式
|
|
16
|
+
const articleTypes = Object.keys(config.article.types)
|
|
17
|
+
const typeValues = articleTypes.length > 0 ? articleTypes : ['tech']
|
|
18
|
+
|
|
19
|
+
return z.object({
|
|
20
|
+
title: z.string().optional(),
|
|
21
|
+
description: z.string().optional(),
|
|
22
|
+
date: z.string().optional(),
|
|
23
|
+
updated: z.string().optional(),
|
|
24
|
+
published: z.string().optional(),
|
|
25
|
+
categories: z.array(z.string()).default([config.article.defaultCategory]),
|
|
26
|
+
tags: z.array(z.string()).default([]),
|
|
27
|
+
type: z.enum(typeValues).optional().default(typeValues[0]),
|
|
28
|
+
|
|
29
|
+
image: z.string().optional(),
|
|
30
|
+
recommend: z.number().optional(),
|
|
31
|
+
references: z.array(z.object({
|
|
32
|
+
title: z.string().optional(),
|
|
33
|
+
link: z.string().optional(),
|
|
34
|
+
})).optional(),
|
|
35
|
+
draft: z.boolean().default(false),
|
|
36
|
+
permalink: z.string().optional(),
|
|
37
|
+
|
|
38
|
+
readingTime: z.object({
|
|
39
|
+
text: z.string(),
|
|
40
|
+
minutes: z.number(),
|
|
41
|
+
time: z.number(),
|
|
42
|
+
words: z.number(),
|
|
43
|
+
}),
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createClarityContentConfig(config) {
|
|
48
|
+
const parsed = clarityConfigSchema.parse(config)
|
|
49
|
+
return defineContentConfig({
|
|
50
|
+
collections: {
|
|
51
|
+
content: defineCollection({
|
|
52
|
+
source: '**',
|
|
53
|
+
type: 'page',
|
|
54
|
+
schema: createArticleSchema(parsed).extend({
|
|
55
|
+
sitemap: defineSitemapSchema({
|
|
56
|
+
name: 'content',
|
|
57
|
+
onUrl: (url, entry) => {
|
|
58
|
+
const lastmod = (entry.updated || entry.published || entry.date)
|
|
59
|
+
if (lastmod) {
|
|
60
|
+
url.lastmod = new Date(lastmod).toLocaleDateString('sv')
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
z,
|
|
64
|
+
}),
|
|
65
|
+
}),
|
|
66
|
+
}),
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { ReadTimeResults } from 'reading-time'
|
|
2
|
+
import type { ClarityConfig } from './schema'
|
|
3
|
+
import { defineCollection, defineContentConfig } from '@nuxt/content'
|
|
4
|
+
import { defineSitemapSchema } from '@nuxtjs/sitemap/content'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
import { clarityConfigSchema } from './schema'
|
|
7
|
+
|
|
8
|
+
export interface ArticleSchema {
|
|
9
|
+
title?: string
|
|
10
|
+
description?: string
|
|
11
|
+
date?: string
|
|
12
|
+
updated?: string
|
|
13
|
+
published?: string
|
|
14
|
+
categories?: string[]
|
|
15
|
+
tags?: string[]
|
|
16
|
+
type?: keyof ClarityConfig['article']['types']
|
|
17
|
+
|
|
18
|
+
image?: string
|
|
19
|
+
recommend?: number
|
|
20
|
+
references?: { title?: string, link?: string }[]
|
|
21
|
+
draft?: boolean
|
|
22
|
+
permalink?: string
|
|
23
|
+
|
|
24
|
+
readingTime?: ReadTimeResults
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createArticleSchema(config: ClarityConfig) {
|
|
28
|
+
// 兜底:types 为空时 z.enum([]) 非法,统一回退到默认版式
|
|
29
|
+
const articleTypes = Object.keys(config.article.types)
|
|
30
|
+
const typeValues = articleTypes.length > 0 ? articleTypes : ['tech']
|
|
31
|
+
|
|
32
|
+
return z.object({
|
|
33
|
+
title: z.string().optional(),
|
|
34
|
+
description: z.string().optional(),
|
|
35
|
+
date: z.string().optional(),
|
|
36
|
+
updated: z.string().optional(),
|
|
37
|
+
published: z.string().optional(),
|
|
38
|
+
categories: z.array(z.string()).default([config.article.defaultCategory]),
|
|
39
|
+
tags: z.array(z.string()).default([]),
|
|
40
|
+
type: z.enum(typeValues as any).optional().default(typeValues[0]),
|
|
41
|
+
|
|
42
|
+
image: z.string().optional(),
|
|
43
|
+
recommend: z.number().optional(),
|
|
44
|
+
references: z.array(z.object({
|
|
45
|
+
title: z.string().optional(),
|
|
46
|
+
link: z.string().optional(),
|
|
47
|
+
})).optional(),
|
|
48
|
+
draft: z.boolean().default(false),
|
|
49
|
+
permalink: z.string().optional(),
|
|
50
|
+
|
|
51
|
+
readingTime: z.object({
|
|
52
|
+
text: z.string(),
|
|
53
|
+
minutes: z.number(),
|
|
54
|
+
time: z.number(),
|
|
55
|
+
words: z.number(),
|
|
56
|
+
}),
|
|
57
|
+
}) satisfies z.ZodType<ArticleSchema>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 在消费项目 content.config.ts 中生成 Clarity Content 集合:
|
|
62
|
+
*
|
|
63
|
+
* ```ts
|
|
64
|
+
* import clarityConfig from './clarity.config'
|
|
65
|
+
* import { createClarityContentConfig } from 'clarity-theme/content'
|
|
66
|
+
* export default createClarityContentConfig(clarityConfig)
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export function createClarityContentConfig(config: ClarityConfig) {
|
|
70
|
+
const parsed = clarityConfigSchema.parse(config)
|
|
71
|
+
return defineContentConfig({
|
|
72
|
+
collections: {
|
|
73
|
+
content: defineCollection({
|
|
74
|
+
source: '**',
|
|
75
|
+
type: 'page',
|
|
76
|
+
schema: createArticleSchema(parsed).extend({
|
|
77
|
+
sitemap: defineSitemapSchema({
|
|
78
|
+
name: 'content',
|
|
79
|
+
onUrl: (url, entry) => {
|
|
80
|
+
const lastmod = (entry.updated || entry.published || entry.date) as string | undefined
|
|
81
|
+
if (lastmod) {
|
|
82
|
+
url.lastmod = new Date(lastmod).toLocaleDateString('sv')
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
z,
|
|
86
|
+
}),
|
|
87
|
+
}),
|
|
88
|
+
}),
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { clarityConfigSchema } from './schema.mjs'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* defineClarityConfig 运行时实现(JS)。
|
|
5
|
+
* 类型真源在同目录 define.ts;用于 Node 原生 TS 剥离受限场景
|
|
6
|
+
* (node_modules 内不允许 TS 文件)与纯 ESM 运行时链。
|
|
7
|
+
*/
|
|
8
|
+
export function defineClarityConfig(config) {
|
|
9
|
+
const result = clarityConfigSchema.safeParse(config)
|
|
10
|
+
if (!result.success) {
|
|
11
|
+
const issues = result.error.issues
|
|
12
|
+
.map(issue => ` - ${issue.path.join('.') || '(根对象)'}: ${issue.message}`)
|
|
13
|
+
.join('\n')
|
|
14
|
+
throw new Error(
|
|
15
|
+
`clarity.config.ts 校验失败:\n${issues}\n`
|
|
16
|
+
+ '未知字段会被拒绝:请检查拼写,或参考 docs/config-api-audit.md 的字段归属表。',
|
|
17
|
+
{ cause: result.error },
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
return result.data
|
|
21
|
+
}
|
package/config/define.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ClarityConfig, ClarityConfigInput } from './schema'
|
|
2
|
+
import { clarityConfigSchema } from './schema'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 定义 Clarity Theme 站点配置
|
|
6
|
+
*
|
|
7
|
+
* 在消费项目根目录的 clarity.config.ts 中使用,
|
|
8
|
+
* 此处会立即完成默认值填充与校验,避免错误配置进入构建流程。
|
|
9
|
+
*/
|
|
10
|
+
export function defineClarityConfig(config: ClarityConfigInput): ClarityConfig {
|
|
11
|
+
const result = clarityConfigSchema.safeParse(config)
|
|
12
|
+
if (!result.success) {
|
|
13
|
+
const issues = result.error.issues
|
|
14
|
+
.map(issue => ` - ${issue.path.join('.') || '(根对象)'}: ${issue.message}`)
|
|
15
|
+
.join('\n')
|
|
16
|
+
throw new Error(
|
|
17
|
+
`clarity.config.ts 校验失败:\n${issues}\n`
|
|
18
|
+
+ '未知字段会被拒绝:请检查拼写,或参考 docs/config-api-audit.md 的字段归属表。',
|
|
19
|
+
{ cause: result.error },
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
return result.data
|
|
23
|
+
}
|
package/config/feed.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Arch } from '../shared/utils/icon'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 友链数据类型:数据由消费项目提供,结构由 Theme 定义。
|
|
5
|
+
*/
|
|
6
|
+
export type { Arch }
|
|
7
|
+
|
|
8
|
+
export interface FeedEntry {
|
|
9
|
+
/** 博客作者,使用作者明确的自称 */
|
|
10
|
+
author: string
|
|
11
|
+
/** 友链卡片的作者名后缀;作者自称明确且较短时,可填简短站名,避免重复作者名 */
|
|
12
|
+
sitenick?: string
|
|
13
|
+
/** 悬浮卡片与订阅源使用的完整站点标题;与作者名相同时省略,默认使用 sitenick 或 author */
|
|
14
|
+
title?: string
|
|
15
|
+
/** 个人简介/博客描述 */
|
|
16
|
+
desc?: string
|
|
17
|
+
/** 博客地址 */
|
|
18
|
+
link: string
|
|
19
|
+
/** 订阅源 */
|
|
20
|
+
feed?: string
|
|
21
|
+
/** 站点小图标 */
|
|
22
|
+
icon: string
|
|
23
|
+
/** 个人头像 */
|
|
24
|
+
avatar: string
|
|
25
|
+
/** 博客技术架构 */
|
|
26
|
+
archs?: Arch[]
|
|
27
|
+
/** 申请/评论日期;主动收录时使用收录日期 */
|
|
28
|
+
date: string
|
|
29
|
+
/** 博主备注 */
|
|
30
|
+
comment?: string
|
|
31
|
+
/** 错误信息 */
|
|
32
|
+
error?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface FeedGroup {
|
|
36
|
+
/** 分组名 */
|
|
37
|
+
name: string
|
|
38
|
+
/** 描述 */
|
|
39
|
+
desc?: string
|
|
40
|
+
/** 友链列表 */
|
|
41
|
+
entries: FeedEntry[]
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './index.ts'
|
package/config/index.mjs
ADDED
package/config/index.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { Nav, NavItem } from '../app/types/nav'
|
|
2
|
+
export type { ClarityAppConfig, ClarityUiConfig, ClarityUiConfigInput, NavGroup } from './app'
|
|
3
|
+
export { defineClarityConfig } from './define'
|
|
4
|
+
export type { Arch, FeedEntry, FeedGroup } from './feed'
|
|
5
|
+
export type { ClarityPublicConfig, ClarityPublicIntegrationsConfig } from './public'
|
|
6
|
+
export * from './schema'
|