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.
Files changed (151) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +276 -0
  3. package/README.zh-CN.md +297 -0
  4. package/app/app.config.ts +110 -0
  5. package/app/app.vue +5 -0
  6. package/app/assets/css/_variable.scss +8 -0
  7. package/app/assets/css/animation.scss +50 -0
  8. package/app/assets/css/article.scss +152 -0
  9. package/app/assets/css/color.scss +58 -0
  10. package/app/assets/css/font.scss +35 -0
  11. package/app/assets/css/main.scss +230 -0
  12. package/app/assets/css/reusable.scss +186 -0
  13. package/app/assets/icons/solar-rewind-back-bold-duotone.svg +1 -0
  14. package/app/assets/icons/solar-rewind-forward-bold-duotone.svg +1 -0
  15. package/app/components/blog/BlogAside.vue +63 -0
  16. package/app/components/blog/BlogFooter.vue +61 -0
  17. package/app/components/blog/BlogHeader.global.vue +146 -0
  18. package/app/components/blog/BlogPanel.vue +79 -0
  19. package/app/components/blog/BlogSidebar.vue +160 -0
  20. package/app/components/blog/BlogWidget.vue +134 -0
  21. package/app/components/blog/Mask.vue +42 -0
  22. package/app/components/blog/SkipToContent.vue +22 -0
  23. package/app/components/blog/ThemeToggle.vue +50 -0
  24. package/app/components/content/Alert.vue +96 -0
  25. package/app/components/content/Badge.vue +90 -0
  26. package/app/components/content/Blur.vue +22 -0
  27. package/app/components/content/CardList.vue +32 -0
  28. package/app/components/content/Chat.vue +79 -0
  29. package/app/components/content/Copy.vue +134 -0
  30. package/app/components/content/EmojiClock.vue +53 -0
  31. package/app/components/content/FeedCard.vue +212 -0
  32. package/app/components/content/FeedGroup.vue +130 -0
  33. package/app/components/content/Folding.vue +69 -0
  34. package/app/components/content/Key.vue +164 -0
  35. package/app/components/content/LinkBanner.vue +59 -0
  36. package/app/components/content/LinkCard.vue +74 -0
  37. package/app/components/content/MdTitle.vue +26 -0
  38. package/app/components/content/Mermaid.vue +133 -0
  39. package/app/components/content/MusicScore.vue +178 -0
  40. package/app/components/content/Pic.vue +55 -0
  41. package/app/components/content/Poetry.vue +41 -0
  42. package/app/components/content/ProseA.vue +40 -0
  43. package/app/components/content/ProseCode.vue +95 -0
  44. package/app/components/content/ProsePre.vue +291 -0
  45. package/app/components/content/ProseTable.vue +70 -0
  46. package/app/components/content/Quote.vue +51 -0
  47. package/app/components/content/Tab.vue +100 -0
  48. package/app/components/content/Timeline.vue +73 -0
  49. package/app/components/content/Tip.vue +52 -0
  50. package/app/components/content/VideoEmbed.vue +127 -0
  51. package/app/components/partial/Button.vue +98 -0
  52. package/app/components/partial/DlGroup.vue +69 -0
  53. package/app/components/partial/Dropdown.vue +54 -0
  54. package/app/components/partial/Error.vue +70 -0
  55. package/app/components/partial/Expand.vue +51 -0
  56. package/app/components/partial/IconNavList.vue +42 -0
  57. package/app/components/partial/Pagination.vue +113 -0
  58. package/app/components/partial/RadioGroup.vue +77 -0
  59. package/app/components/partial/Secret.vue +23 -0
  60. package/app/components/partial/Slider.vue +96 -0
  61. package/app/components/partial/Toggle.vue +70 -0
  62. package/app/components/popover/Lightbox.vue +77 -0
  63. package/app/components/popover/Search.vue +214 -0
  64. package/app/components/popover/SearchItem.vue +64 -0
  65. package/app/components/post/Archive.vue +104 -0
  66. package/app/components/post/Article.vue +138 -0
  67. package/app/components/post/Comment.vue +275 -0
  68. package/app/components/post/Excerpt.vue +66 -0
  69. package/app/components/post/OrderToggle.vue +101 -0
  70. package/app/components/post/PostFooter.vue +80 -0
  71. package/app/components/post/PostHeader.vue +147 -0
  72. package/app/components/post/PostSurround.vue +111 -0
  73. package/app/components/post/Slide.vue +215 -0
  74. package/app/components/util/Date.vue +62 -0
  75. package/app/components/util/HydrateSafe.vue +10 -0
  76. package/app/components/util/Img.vue +38 -0
  77. package/app/components/util/Link.vue +11 -0
  78. package/app/components/widget/BlogLog.vue +27 -0
  79. package/app/components/widget/BlogStats.vue +39 -0
  80. package/app/components/widget/BlogTech.vue +65 -0
  81. package/app/components/widget/Empty.vue +3 -0
  82. package/app/components/widget/Toc.vue +129 -0
  83. package/app/composables/useArticle.ts +118 -0
  84. package/app/composables/useAvoid.ts +87 -0
  85. package/app/composables/useCopy.ts +21 -0
  86. package/app/composables/usePagination.ts +70 -0
  87. package/app/composables/useShiki.ts +129 -0
  88. package/app/composables/useToc.ts +57 -0
  89. package/app/composables/useWidgets.ts +56 -0
  90. package/app/error.vue +34 -0
  91. package/app/layouts/default.vue +66 -0
  92. package/app/pages/[...slug].vue +59 -0
  93. package/app/pages/archive.vue +207 -0
  94. package/app/pages/index.vue +83 -0
  95. package/app/pages/link.vue +65 -0
  96. package/app/pages/preview.vue +59 -0
  97. package/app/plugins/easter-egg.ts +23 -0
  98. package/app/plugins/init.ts +6 -0
  99. package/app/plugins/tippy.ts +22 -0
  100. package/app/shiki.config.ts +8 -0
  101. package/app/stores/layout.ts +34 -0
  102. package/app/stores/search.ts +37 -0
  103. package/app/types/article.ts +16 -0
  104. package/app/types/feed.ts +1 -0
  105. package/app/types/index.ts +14 -0
  106. package/app/types/modal.ts +16 -0
  107. package/app/types/nav.ts +11 -0
  108. package/app/utils/anim.ts +37 -0
  109. package/app/utils/img.ts +83 -0
  110. package/config/app.ts +88 -0
  111. package/config/content.d.mts +1 -0
  112. package/config/content.mjs +69 -0
  113. package/config/content.ts +91 -0
  114. package/config/define.mjs +21 -0
  115. package/config/define.ts +23 -0
  116. package/config/feed.ts +42 -0
  117. package/config/feeds.empty.ts +4 -0
  118. package/config/index.d.mts +1 -0
  119. package/config/index.mjs +6 -0
  120. package/config/index.ts +6 -0
  121. package/config/public.ts +71 -0
  122. package/config/schema.d.mts +1 -0
  123. package/config/schema.mjs +103 -0
  124. package/config/schema.ts +129 -0
  125. package/config/server.ts +35 -0
  126. package/img/index.d.mts +1 -0
  127. package/img/index.mjs +78 -0
  128. package/img/index.ts +8 -0
  129. package/modules/clarity-config/anti-mirror-client.ts +11 -0
  130. package/modules/clarity-config/index.ts +324 -0
  131. package/nuxt.config.ts +225 -0
  132. package/package.json +148 -0
  133. package/public/assets/atom.css +274 -0
  134. package/public/assets/atom.xsl +110 -0
  135. package/public/fonts/AlimamaFangYuanTi.woff2 +0 -0
  136. package/remark-plugins/rehype-meta-slots.d.mts +1 -0
  137. package/remark-plugins/rehype-meta-slots.mjs +49 -0
  138. package/remark-plugins/rehype-meta-slots.ts +58 -0
  139. package/remark-plugins/remark-code-component.d.mts +1 -0
  140. package/remark-plugins/remark-code-component.mjs +25 -0
  141. package/remark-plugins/remark-code-component.ts +41 -0
  142. package/server/api/stats.get.ts +109 -0
  143. package/server/routes/atom.xml.get.ts +101 -0
  144. package/server/routes/subscriptions.opml.get.ts +70 -0
  145. package/server/utils/clarity.ts +11 -0
  146. package/shared/types/content.ts +30 -0
  147. package/shared/utils/clarity.ts +34 -0
  148. package/shared/utils/icon.ts +218 -0
  149. package/shared/utils/link.ts +52 -0
  150. package/shared/utils/str.ts +88 -0
  151. 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,14 @@
1
+ declare global {
2
+ interface Window {
3
+ twikoo?: {
4
+ init: (options: {
5
+ envId: string
6
+ el: string
7
+ region?: string
8
+ path?: string
9
+ lang?: string
10
+ }) => void
11
+ version: string
12
+ }
13
+ }
14
+ }
@@ -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
+ }
@@ -0,0 +1,11 @@
1
+ export interface NavItem {
2
+ icon: string
3
+ text: string
4
+ url: string
5
+ external?: boolean
6
+ }
7
+
8
+ export type Nav = {
9
+ title: string
10
+ items: NavItem[]
11
+ }[]
@@ -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` })
@@ -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
+ }
@@ -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,4 @@
1
+ import type { FeedGroup } from '../app/types/feed'
2
+
3
+ /** 消费项目未提供 feeds.ts 时的空数据兜底 */
4
+ export default [] satisfies FeedGroup[]
@@ -0,0 +1 @@
1
+ export * from './index.ts'
@@ -0,0 +1,6 @@
1
+ /**
2
+ * `clarity-theme/config` 运行时实现(JS)。
3
+ * 类型真源在同目录 index.ts;类型与运行时分离的原因见 define.mjs。
4
+ */
5
+ export { defineClarityConfig } from './define.mjs'
6
+ export * from './schema.mjs'
@@ -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'