clarity-theme 0.1.3 → 0.2.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 (62) hide show
  1. package/README.md +15 -5
  2. package/README.zh-CN.md +15 -5
  3. package/nuxt.config.ts +19 -1
  4. package/package.json +14 -2
  5. package/src/app.config.ts +8 -104
  6. package/src/assets/icons/zhilu.svg +8 -0
  7. package/src/blog.config.ts +71 -0
  8. package/src/components/blog/BlogFooter.vue +3 -3
  9. package/src/components/blog/BlogHeader.global.vue +9 -9
  10. package/src/components/blog/BlogSidebar.vue +3 -3
  11. package/src/components/blog/ThemeToggle.vue +2 -2
  12. package/src/components/content/Alert.vue +2 -2
  13. package/src/components/content/FeedCard.vue +2 -2
  14. package/src/components/content/Mermaid.vue +1 -1
  15. package/src/components/content/ProseCode.vue +4 -32
  16. package/src/components/content/ProsePre.vue +2 -2
  17. package/src/components/content/ProseTable.vue +1 -1
  18. package/src/components/popover/Search.vue +3 -3
  19. package/src/components/post/Comment.vue +3 -15
  20. package/src/components/post/Excerpt.vue +3 -3
  21. package/src/components/post/OrderToggle.vue +3 -3
  22. package/src/components/post/PostFooter.vue +3 -4
  23. package/src/components/post/PostHeader.vue +4 -4
  24. package/src/components/post/PostSurround.vue +2 -2
  25. package/src/components/post/Slide.vue +2 -2
  26. package/src/components/util/Img.vue +1 -1
  27. package/src/components/widget/BlogLog.vue +9 -19
  28. package/src/components/widget/BlogStats.vue +4 -4
  29. package/src/components/widget/BlogTech.vue +16 -17
  30. package/src/components/widget/CommGroup.vue +43 -0
  31. package/src/composables/useArticle.ts +9 -9
  32. package/src/composables/usePagination.ts +2 -2
  33. package/src/composables/useWidgets.ts +2 -0
  34. package/src/config/app.ts +40 -11
  35. package/src/config/content.ts +6 -0
  36. package/src/config/feed.ts +2 -42
  37. package/src/config/index.ts +2 -2
  38. package/src/config/pnpm-workspace.ts +19 -0
  39. package/src/config/public.ts +3 -61
  40. package/src/config/ui.ts +111 -0
  41. package/src/modules/anti-mirror/index.ts +38 -0
  42. package/src/modules/{clarity-config/anti-mirror-client.ts → anti-mirror/runtime/client.ts} +3 -3
  43. package/src/modules/clarity-config/index.ts +354 -93
  44. package/src/modules/clarity-source-layout/index.ts +13 -0
  45. package/src/pages/archive.vue +4 -4
  46. package/src/pages/index.vue +4 -3
  47. package/src/pages/link.vue +5 -5
  48. package/src/pages/preview.vue +2 -2
  49. package/src/server/api/stats.get.ts +9 -19
  50. package/src/server/routes/atom.xml.get.ts +48 -50
  51. package/src/server/routes/subscriptions.opml.get.ts +14 -31
  52. package/src/server/utils/clarity.ts +1 -1
  53. package/src/shared/utils/time.ts +13 -18
  54. package/src/types/article.ts +2 -2
  55. package/src/types/feed.ts +35 -1
  56. package/src/types/upstream-compat.d.ts +17 -0
  57. package/src/types/yaml-modules.d.ts +26 -0
  58. package/src/shared/types/content.ts +0 -30
  59. package/src/shared/utils/clarity.ts +0 -34
  60. package/src/types/modal.ts +0 -16
  61. /package/src/assets/icons/{solar-rewind-back-bold-duotone.svg → SolarRewindBackBoldDuotone.svg} +0 -0
  62. /package/src/assets/icons/{solar-rewind-forward-bold-duotone.svg → SolarRewindForwardBoldDuotone.svg} +0 -0
@@ -1,13 +1,13 @@
1
1
  <script setup lang="ts">
2
- import type { ModalEmits, ModalProps } from '../../types/modal'
2
+ import type { ModalEmits, ModalProps } from '#modals'
3
3
  import MiniSearch from 'minisearch'
4
4
 
5
5
  const props = defineProps<ModalProps>()
6
6
 
7
7
  defineEmits<ModalEmits>()
8
8
 
9
- const clarity = useClarityConfig()
10
- const segmenter = Intl.Segmenter && new Intl.Segmenter(clarity.site.language, { granularity: 'word' })
9
+ const appConfig = useAppConfig()
10
+ const segmenter = Intl.Segmenter && new Intl.Segmenter(appConfig.language, { granularity: 'word' })
11
11
 
12
12
  // await useAsyncData() 会阻塞渲染
13
13
  const { data, status } = await useLazyAsyncData(
@@ -1,8 +1,7 @@
1
1
  <script setup lang="tsx">
2
2
  import type { TippyComponent } from 'vue-tippy'
3
3
 
4
- const clarity = useClarityConfig()
5
- const enabled = computed(() => !!clarity.integrations.twikoo?.envId)
4
+ const appConfig = useAppConfig()
6
5
 
7
6
  const commentEl = useTemplateRef('comment')
8
7
  const popoverEl = useTemplateRef<TippyComponent>('popover')
@@ -53,11 +52,8 @@ function confirmOpen() {
53
52
  }
54
53
 
55
54
  onMounted(() => {
56
- if (!enabled.value)
57
- return
58
-
59
55
  window.twikoo?.init?.({
60
- envId: clarity.integrations.twikoo?.envId ?? '',
56
+ envId: appConfig.twikoo?.envId,
61
57
  // twikoo 会把挂载后的元素变为 #twikoo
62
58
  el: '#twikoo',
63
59
  })
@@ -108,12 +104,9 @@ onMounted(() => {
108
104
  </template>
109
105
  </Tooltip>
110
106
 
111
- <div v-if="enabled" id="twikoo">
107
+ <div id="twikoo">
112
108
  <p>评论加载中...</p>
113
109
  </div>
114
- <p v-else class="no-comment">
115
- 本文暂未开启评论
116
- </p>
117
110
  </section>
118
111
  </template>
119
112
 
@@ -215,11 +208,6 @@ onMounted(() => {
215
208
  }
216
209
  }
217
210
 
218
- .no-comment {
219
- margin: 2em 0;
220
- color: var(--c-text-3);
221
- }
222
-
223
211
  :deep(:where(.tk-preview-container,.tk-content)) {
224
212
  pre {
225
213
  overflow: auto;
@@ -5,15 +5,15 @@ const props = defineProps<{
5
5
  excerpt: string
6
6
  }>()
7
7
 
8
- const clarity = useClarityConfig()
8
+ const appConfig = useAppConfig()
9
9
 
10
10
  const excerpt = ref(props.excerpt)
11
11
  const caret = ref('')
12
12
 
13
- if (clarity.component.excerpt?.animation !== false) {
13
+ if (appConfig.component.excerpt?.animation !== false) {
14
14
  excerpt.value = ''
15
15
  onMounted(async () => {
16
- caret.value = clarity.component.excerpt?.caret ?? '_'
16
+ caret.value = appConfig.component.excerpt?.caret ?? '_'
17
17
  for (const char of props.excerpt) {
18
18
  excerpt.value += char
19
19
  await delay(50)
@@ -9,10 +9,10 @@ const props = defineProps<{
9
9
  secretDelay?: string
10
10
  }>()
11
11
 
12
- const clarity = useClarityConfig()
13
- const orderMap = computed(() => clarity.article.order)
12
+ const appConfig = useAppConfig()
13
+ const orderMap = computed(() => appConfig.article.order)
14
14
  // 配置文件中允许升序时,且未明确禁用升序时,允许升序
15
- const allowAscending = computed(() => clarity.pagination.allowAscending ? !props.disableAscending : props.enableAscending)
15
+ const allowAscending = computed(() => appConfig.pagination.allowAscending ? !props.disableAscending : props.enableAscending)
16
16
 
17
17
  const category = defineModel<string>('category')
18
18
  const sortOrder = defineModel<ArticleOrderType>('sortOrder', { default: 'date' })
@@ -8,7 +8,7 @@ const [DefineTemplate, ReuseTemplate] = createReusableTemplate<{
8
8
  title: string
9
9
  }>({ inheritAttrs: false })
10
10
 
11
- const clarity = useClarityConfig()
11
+ const appConfig = useAppConfig()
12
12
  </script>
13
13
 
14
14
  <template>
@@ -38,10 +38,9 @@ const clarity = useClarityConfig()
38
38
  <ReuseTemplate :title="meta?.slots?.copyright?.props?.title as string || '许可协议'">
39
39
  <ContentRenderer v-if="meta?.slots?.copyright" :value="meta?.slots?.copyright" />
40
40
  <p v-else>
41
- 本文采用 <ProseA v-if="clarity.site.copyright?.url" :href="clarity.site.copyright.url">
42
- {{ clarity.site.copyright.name }}
41
+ 本文采用 <ProseA :href="appConfig.copyright.url">
42
+ {{ appConfig.copyright.name }}
43
43
  </ProseA>
44
- <span v-else-if="clarity.site.copyright?.name">{{ clarity.site.copyright.name }}</span>
45
44
  许可协议,转载请注明出处。
46
45
  </p>
47
46
  </ReuseTemplate>
@@ -4,13 +4,13 @@ import type { ArticleProps } from '../../types/article'
4
4
  defineOptions({ inheritAttrs: false })
5
5
  const props = defineProps<ArticleProps>()
6
6
 
7
- const clarity = useClarityConfig()
7
+ const appConfig = useAppConfig()
8
8
 
9
9
  const coverFilter = computed(() => props.meta?.coverFilter || (props.meta?.coverDim && 'brightness(0.75)') || undefined)
10
10
 
11
- const shareText = `【${clarity.site.title}】${props.title}\n\n${
11
+ const shareText = `【${appConfig.title}】${props.title}\n\n${
12
12
  props.description ? `${props.description}\n\n` : ''}${
13
- new URL(props.path!, clarity.site.url).href}`
13
+ new URL(props.path!, appConfig.url).href}`
14
14
 
15
15
  const { copy, copied } = useCopy(shareText)
16
16
  </script>
@@ -38,7 +38,7 @@ const { copy, copied } = useCopy(shareText)
38
38
  />
39
39
 
40
40
  <UtilDate
41
- v-if="updated && isTimeDiffSignificant(date, updated, undefined, 1)"
41
+ v-if="updated && isTimeDiffSignificant(date, updated, 1)"
42
42
  v-tip
43
43
  :tip-transform="d => `修改于${d}`"
44
44
  :date="updated"
@@ -36,11 +36,11 @@ const [DefineTemplate, ReuseTemplate] = createReusableTemplate<{
36
36
 
37
37
  <div v-if="prev || next" class="surround-post" dir="ltr">
38
38
  <ReuseTemplate
39
- :post="next" icon="clarity:solar-rewind-back-bold-duotone"
39
+ :post="next" icon="zi:solar-rewind-back-bold-duotone"
40
40
  fallback-icon="line-md:coffee-twotone-loop" fallback-text="新故事即将发生"
41
41
  />
42
42
  <ReuseTemplate
43
- :post="prev" icon="clarity:solar-rewind-forward-bold-duotone"
43
+ :post="prev" icon="zi:solar-rewind-forward-bold-duotone"
44
44
  fallback-icon="line-md:construction-twotone" fallback-text="已抵达博客尽头"
45
45
  align-end
46
46
  />
@@ -6,8 +6,8 @@ import { WheelGesturesPlugin } from 'embla-carousel-wheel-gestures'
6
6
 
7
7
  defineProps<{ list: ArticleProps[] }>()
8
8
 
9
- const clarity = useClarityConfig()
10
- const compConf = computed(() => clarity.component.slide)
9
+ const appConfig = useAppConfig()
10
+ const compConf = computed(() => appConfig.component.slide)
11
11
 
12
12
  // @keep-sorted
13
13
  const [carouselEl, carouselApi] = emblaCarouselVue({
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
- import { joinURL, withLeadingSlash, withTrailingSlash } from 'ufo'
3
2
  import ImageComponent from '#build/mdc-image-component.mjs'
3
+ import { joinURL, withLeadingSlash, withTrailingSlash } from 'ufo'
4
4
 
5
5
  export interface UtilImgProps {
6
6
  src: string
@@ -1,27 +1,17 @@
1
1
  <script setup lang="ts">
2
- const clarity = useClarityConfig()
2
+ const { timeEstablished } = useAppConfig()
3
3
 
4
- const blogLog = computed(() => {
5
- const entries = clarity.changelog.map(({ date, text }) => ({ label: date, value: text }))
6
- if (clarity.site.established) {
7
- entries.push({ label: clarity.site.established, value: '发布第一篇文章' })
8
- }
9
- return entries
10
- })
4
+ const blogLog = [
5
+ { label: '2025-07-26', value: '重构到 Nuxt 4 + Nuxt Content v3,启用 zhilu.site 域名' },
6
+ { label: '2024-08-11', value: '重构到 Nuxt 3 + Nuxt Content v2' },
7
+ { label: '2023-05-24', value: '更换框架为 Hexo,升级博客' },
8
+ { label: '2020-08-24', value: '使用 zhilu.cyou 域名' },
9
+ { label: timeEstablished, value: '发布第一篇文章' },
10
+ ]
11
11
  </script>
12
12
 
13
13
  <template>
14
14
  <BlogWidget card title="更新日志">
15
- <ZDlGroup v-if="blogLog.length" size="large" :items="blogLog" />
16
- <p v-else class="no-log">
17
- 暂无更新日志,可在 clarity.config.ts 的 changelog 中配置。
18
- </p>
15
+ <ZDlGroup size="large" :items="blogLog" />
19
16
  </BlogWidget>
20
17
  </template>
21
-
22
- <style lang="scss" scoped>
23
- .no-log {
24
- padding: 1em;
25
- color: var(--c-text-3);
26
- }
27
- </style>
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { UtilDate } from '#components'
3
3
 
4
- const clarity = useClarityConfig()
4
+ const appConfig = useAppConfig()
5
5
  const runtimeConfig = useRuntimeConfig()
6
6
 
7
7
  // 响应头不正确时,stats.value 可能会是字符串,首次属性访问可能为 undefined
@@ -16,8 +16,8 @@ const yearlyTip = computed(() => Object
16
16
 
17
17
  const blogStats = [{
18
18
  label: '运营时长',
19
- value: clarity.site.established ? timeElapse(clarity.site.established) : '--',
20
- tip: clarity.site.established ? `博客于${clarity.site.established}上线` : '未配置建站日期',
19
+ value: timeElapse(appConfig.timeEstablished),
20
+ tip: `博客于${appConfig.timeEstablished}上线`,
21
21
  }, {
22
22
  label: '上次更新',
23
23
  value: () => h(UtilDate, {
@@ -26,7 +26,7 @@ const blogStats = [{
26
26
  tipPrefix: '构建于',
27
27
  }),
28
28
  }, {
29
- label: clarity.stats.postsOnly ? '文章字数' : '总字数',
29
+ label: appConfig.stats.includePaths.length ? '文章字数' : '总字数',
30
30
  value: computed(() => formatNumber(stats.value?.total?.words) || '--'),
31
31
  tip: yearlyTip,
32
32
  }]
@@ -1,17 +1,11 @@
1
1
  <script setup lang="ts">
2
2
  import { Icon } from '#components'
3
+ import { merge } from 'es-toolkit/object'
4
+ import { packageManager, version } from '~~/package.json'
5
+ import pnpmWorkspace from '~~/pnpm-workspace.yaml'
3
6
 
4
- const clarity = useClarityConfig()
5
- const runtimeConfig = useRuntimeConfig()
6
- const buildInfo = runtimeConfig.public.clarity as {
7
- theme: string
8
- themeVersion: string
9
- siteVersion: string
10
- sitePackageManager: string
11
- nuxtVersion: string
12
- vueVersion: string
13
- }
14
- const { public: { arch, ci, nodeVersion, platform } } = runtimeConfig
7
+ const appConfig = useAppConfig()
8
+ const { public: { arch, ci, nodeVersion, platform } } = useRuntimeConfig()
15
9
 
16
10
  const ciPlatform = computed(() => {
17
11
  const iconName = ciIcons[ci]
@@ -25,20 +19,25 @@ const ciPlatform = computed(() => {
25
19
  return h('span', {}, [iconNode, ` ${ci.split(' ')[0]}`])
26
20
  })
27
21
 
22
+ // @ts-expect-error pnpm-workspace.yaml 无类型定义
23
+ const packages = merge(...Object.values(pnpmWorkspace.catalogs))
24
+ const [pm, pmVersion] = packageManager.split('@') as [string, string]
25
+
28
26
  const service = computed(() => ([
29
27
  ...ci ? [{ label: '构建平台', value: ciPlatform }] : [],
30
28
  { label: '图片存储', value: () => [h(Icon, { name: 'devicon:cloudflare' }), ' R2'] },
31
29
  { label: '软件协议', value: 'MIT' },
32
- { label: '文章许可', value: clarity.site.copyright?.abbr || '未配置' },
33
- { label: '规范域名', value: getDomain(clarity.site.url) },
30
+ { label: '文章许可', value: appConfig.copyright.abbr },
31
+ { label: '规范域名', value: getDomain(appConfig.url) },
34
32
  ]))
35
33
 
36
34
  const techstack = computed(() => ([
37
- { label: 'Blog', value: buildInfo.siteVersion || '--' },
38
- { label: 'Theme', value: `${buildInfo.theme} ${buildInfo.themeVersion}` },
39
- { label: 'Vue', value: buildInfo.vueVersion },
40
- { label: 'Nuxt', value: buildInfo.nuxtVersion },
35
+ { label: 'Blog', value: version },
36
+ { label: 'Vue', value: packages.vue },
37
+ { label: 'Nuxt', value: packages.nuxt },
38
+ { label: 'Content', value: packages['@nuxt/content'] },
41
39
  { label: 'Node', value: nodeVersion },
40
+ { label: pm, value: pmVersion },
42
41
  { label: 'OS', value: platform },
43
42
  { label: 'Arch', value: arch },
44
43
  ]))
@@ -0,0 +1,43 @@
1
+ <template>
2
+ <BlogWidget
3
+ card
4
+ dim
5
+ title="博客/技术社区"
6
+ bg-img="https://p.qlogo.cn/gh/169994096/169994096/100/"
7
+ bg-aside
8
+ >
9
+ <div class="title text-creative">
10
+ 纸网接入点
11
+ </div>
12
+
13
+ <Tip copy icon :tip-options="{ placement: 'left' }">
14
+ <Icon name="ri:qq-fill" />
15
+ <span>169994096</span>
16
+ </Tip>
17
+ </BlogWidget>
18
+ </template>
19
+
20
+ <style lang="scss" scoped>
21
+ .title {
22
+ background-clip: text;
23
+ background-image: linear-gradient(60deg, var(--c-accent) -30%, var(--c-primary), var(--c-text-1));
24
+ background-position: 100% 0;
25
+ background-size: 200%;
26
+ font-size: 1.8em;
27
+ color: transparent;
28
+ transition: background-position 0.2s;
29
+
30
+ .blog-widget:hover & {
31
+ background-position: 0 0;
32
+ }
33
+ }
34
+
35
+ .tip {
36
+ font-size: 0.9em;
37
+ line-height: 2;
38
+ }
39
+
40
+ :deep(.bg-img).bg-img.bg-img {
41
+ opacity: 0.5;
42
+ }
43
+ </style>
@@ -1,5 +1,5 @@
1
+ import type { ContentCollectionItem } from '@nuxt/content'
1
2
  import type { MetaSlotsTree } from '../remark-plugins/rehype-meta-slots'
2
- import type { ClarityContentRow } from '../shared/types/content'
3
3
  import type { ArticleOrderType, ArticleProps } from '../types/article'
4
4
  import { orderBy } from 'es-toolkit/array'
5
5
 
@@ -7,7 +7,7 @@ import { orderBy } from 'es-toolkit/array'
7
7
  export function useArticle(path?: MaybeRefOrGetter<string | undefined>) {
8
8
  const route = useRoute()
9
9
  const dataKey = computed(() => `content:${toValue(path) ?? route.path}`)
10
- const post = computed(() => useNuxtData<ClarityContentRow | null | undefined>(dataKey.value).data.value)
10
+ const post = computed(() => useNuxtData<ContentCollectionItem | null | undefined>(dataKey.value).data.value)
11
11
 
12
12
  return {
13
13
  dataKey,
@@ -64,12 +64,12 @@ interface UseArticleSortOptions {
64
64
  }
65
65
 
66
66
  export function useArticleSort(list: MaybeRefOrGetter<ArticleProps[]>, options?: UseArticleSortOptions) {
67
- const clarity = useClarityConfig()
67
+ const appConfig = useAppConfig()
68
68
  const {
69
69
  bindDirectionQuery,
70
70
  bindOrderQuery,
71
71
  initialAscend = false,
72
- initialOrder = (clarity.pagination.sortOrder as ArticleOrderType) || 'date',
72
+ initialOrder = appConfig.pagination.sortOrder || 'date',
73
73
  } = options || {}
74
74
 
75
75
  const sortOrder = bindOrderQuery
@@ -87,7 +87,7 @@ export function useArticleSort(list: MaybeRefOrGetter<ArticleProps[]>, options?:
87
87
 
88
88
  const listSorted = computed(() => orderBy(
89
89
  toValue(list),
90
- [sortOrder.value, 'date'] as any,
90
+ [sortOrder.value, 'date'],
91
91
  [isAscending.value ? 'asc' : 'desc'],
92
92
  ))
93
93
 
@@ -99,13 +99,13 @@ export function useArticleSort(list: MaybeRefOrGetter<ArticleProps[]>, options?:
99
99
  }
100
100
 
101
101
  export function getCategoryIcon(category?: string) {
102
- const clarity = useClarityConfig()
103
- return clarity.article.categories[category!]?.icon ?? 'tabler:folder'
102
+ const appConfig = useAppConfig()
103
+ return appConfig.article.categories[category!]?.icon ?? 'tabler:folder'
104
104
  }
105
105
 
106
106
  export function getCategoryColor(category?: string) {
107
- const clarity = useClarityConfig()
108
- return clarity.article.categories[category!]?.color
107
+ const appConfig = useAppConfig()
108
+ return appConfig.article.categories[category!]?.color
109
109
  }
110
110
 
111
111
  interface GetPostTypeClassNameOptions {
@@ -5,10 +5,10 @@ interface UsePaginationOptions {
5
5
  }
6
6
 
7
7
  export default function usePagination<T>(list: MaybeRefOrGetter<T[]>, options?: UsePaginationOptions) {
8
- const clarity = useClarityConfig()
8
+ const appConfig = useAppConfig()
9
9
  const {
10
10
  initialPage = 1,
11
- perPage = clarity.pagination.perPage || 10,
11
+ perPage = appConfig.pagination.perPage || 10,
12
12
  bindQuery,
13
13
  } = options || {}
14
14
 
@@ -5,6 +5,7 @@ import {
5
5
  LazyWidgetBlogLog,
6
6
  LazyWidgetBlogStats,
7
7
  LazyWidgetBlogTech,
8
+ LazyWidgetCommGroup,
8
9
  LazyWidgetEmpty,
9
10
  LazyWidgetToc,
10
11
  } from '#components'
@@ -14,6 +15,7 @@ const rawWidgets = {
14
15
  LazyWidgetBlogLog,
15
16
  LazyWidgetBlogStats,
16
17
  LazyWidgetBlogTech,
18
+ LazyWidgetCommGroup,
17
19
  LazyWidgetEmpty,
18
20
  LazyWidgetToc,
19
21
  }
package/src/config/app.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { NavItem } from '../types/nav'
2
- import type { ClarityPublicConfig } from './public'
2
+ import type { ClarityPublicIntegrationsConfig } from './public'
3
+ import type { ClarityArticleConfig, ClarityAuthor, ClarityFeedConfig, ClarityHeadScript, ClarityStatsConfig } from './schema'
3
4
 
4
5
  export interface NavGroup {
5
6
  title: string
@@ -27,8 +28,10 @@ export interface ClarityUiConfig {
27
28
  showTitle: boolean
28
29
  }
29
30
  stats: {
30
- /** 归档页面每年标题对应的年龄 */
31
- birthYear?: number
31
+ /** 归档页面每年标题对应的年龄,0 表示不展示 */
32
+ birthYear: number
33
+ /** blog-stats widget 的预置文本 */
34
+ wordCount: string
32
35
  }
33
36
  }
34
37
  footer: {
@@ -70,19 +73,45 @@ export interface ClarityUiConfig {
70
73
  }>
71
74
  }
72
75
 
73
- export type ClarityAppConfig = ClarityPublicConfig & ClarityUiConfig
74
-
75
76
  /**
76
- * app/app.config.ts 中 `clarity` 键的覆盖输入类型:
77
- * UI 配置深度可选(数组整体替换,与 defu 行为一致)。
77
+ * CustomAppConfig 中 article 的声明形状。
78
78
  *
79
- * 站点级字段(site / article / feed / stats / integrations / features / changelog)
80
- * 不属于 app.config.ts,请写入 clarity.config.ts。
79
+ * 必须是 interface(而非 type 别名):MergedAppConfig 对 extends Record 的
80
+ * 类型会进入递归合并,Record<string, string> 的索引签名会把 order 键类型
81
+ * 碾碎成 keyof any;interface 不带隐式索引签名,直接整体采用本类型。
82
+ */
83
+ export interface ClarityAppConfigArticle extends ClarityArticleConfig {
84
+ categories: Record<string, { icon?: string, color?: string }>
85
+ }
86
+
87
+ /**
88
+ * 上游 blog-v3 的扁平 AppConfig 形状(blog.config.ts 展开进 app.config.ts)。
89
+ * Clarity 由 clarity-config 模块注入同一形状,上游同步组件保持 byte-identical。
81
90
  */
82
- export type ClarityUiConfigInput<T = ClarityUiConfig> = {
91
+ export interface ClarityFlatAppConfig extends ClarityUiConfig {
92
+ title: string
93
+ subtitle: string
94
+ description: string
95
+ author: ClarityAuthor
96
+ copyright: { abbr: string, name: string, url: string }
97
+ favicon: string
98
+ language: string
99
+ timeEstablished: string
100
+ timeZone: string
101
+ url: string
102
+ defaultCategory: string
103
+ article: ClarityArticleConfig
104
+ feed: ClarityFeedConfig
105
+ scripts: ClarityHeadScript[]
106
+ stats: ClarityStatsConfig
107
+ twikoo?: ClarityPublicIntegrationsConfig['twikoo']
108
+ }
109
+
110
+ /** 消费项目 app/app.config.ts 顶层扁平键的覆盖输入类型(深层可选,数组整体替换) */
111
+ export type ClarityFlatAppConfigInput<T = ClarityFlatAppConfig> = {
83
112
  [K in keyof T]?: T[K] extends readonly unknown[]
84
113
  ? T[K]
85
114
  : T[K] extends object
86
- ? ClarityUiConfigInput<T[K]>
115
+ ? ClarityFlatAppConfigInput<T[K]>
87
116
  : T[K]
88
117
  }
@@ -22,6 +22,12 @@ export interface ArticleSchema {
22
22
  permalink?: string
23
23
 
24
24
  readingTime?: ReadTimeResults
25
+
26
+ /**
27
+ * 开放索引签名:clarity.config.ts 的 article.order 允许自定义排序键,
28
+ * 上游归档页通过 article[sortOrder] 索引(上游依赖其配置字面量类型)。
29
+ */
30
+ [key: string]: unknown
25
31
  }
26
32
 
27
33
  function createArticleSchema(config: ClarityConfig) {
@@ -1,42 +1,2 @@
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
- }
1
+ export type { Arch } from '../shared/utils/icon'
2
+ export type { FeedEntry, FeedGroup } from '../types/feed'
@@ -1,6 +1,6 @@
1
1
  export type { Nav, NavItem } from '../types/nav'
2
- export type { ClarityAppConfig, ClarityUiConfig, ClarityUiConfigInput, NavGroup } from './app'
2
+ export type { ClarityUiConfig, NavGroup } from './app'
3
3
  export { defineClarityConfig } from './define'
4
4
  export type { Arch, FeedEntry, FeedGroup } from './feed'
5
- export type { ClarityPublicConfig, ClarityPublicIntegrationsConfig } from './public'
5
+ export type { ClarityPublicIntegrationsConfig } from './public'
6
6
  export * from './schema'
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Theme 内置的技术栈版本数据(上游 BlogTech 通过 ~~/pnpm-workspace.yaml
3
+ * 的 catalogs 展示 Vue / Nuxt / Nuxt Content 版本)。
4
+ *
5
+ * 分组结构与上游 blog-v3 的 pnpm-workspace.yaml 保持一致:上游 BlogTech
6
+ * 使用 es-toolkit 的 merge(...Object.values(catalogs))(二元签名),
7
+ * 至少需要两个分组才能正常展开。
8
+ */
9
+ export const fallbackPnpmWorkspace = {
10
+ catalogs: {
11
+ content: {
12
+ '@nuxt/content': '^3.16.0',
13
+ },
14
+ framework: {
15
+ nuxt: '^4.5.2',
16
+ vue: '^3.5.42',
17
+ },
18
+ },
19
+ } as const