clarity-theme 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/LICENSE +20 -20
  2. package/README.md +18 -4
  3. package/README.zh-CN.md +18 -4
  4. package/app/app.config.ts +110 -110
  5. package/app/app.vue +5 -5
  6. package/app/components/blog/BlogAside.vue +25 -25
  7. package/app/components/blog/BlogFooter.vue +25 -25
  8. package/app/components/blog/BlogHeader.global.vue +48 -48
  9. package/app/components/blog/BlogPanel.vue +43 -43
  10. package/app/components/blog/BlogSidebar.vue +53 -53
  11. package/app/components/blog/BlogWidget.vue +42 -42
  12. package/app/components/blog/Mask.vue +25 -25
  13. package/app/components/blog/SkipToContent.vue +9 -9
  14. package/app/components/blog/ThemeToggle.vue +22 -22
  15. package/app/components/content/Badge.vue +45 -45
  16. package/app/components/content/Blur.vue +14 -14
  17. package/app/components/content/CardList.vue +8 -8
  18. package/app/components/content/Chat.vue +41 -41
  19. package/app/components/content/Copy.vue +71 -71
  20. package/app/components/content/EmojiClock.vue +49 -49
  21. package/app/components/content/FeedCard.vue +96 -96
  22. package/app/components/content/FeedGroup.vue +61 -61
  23. package/app/components/content/Folding.vue +19 -19
  24. package/app/components/content/Key.vue +143 -143
  25. package/app/components/content/LinkBanner.vue +27 -27
  26. package/app/components/content/LinkCard.vue +28 -28
  27. package/app/components/content/MdTitle.vue +8 -8
  28. package/app/components/content/Mermaid.vue +99 -99
  29. package/app/components/content/MusicScore.vue +63 -63
  30. package/app/components/content/Pic.vue +49 -49
  31. package/app/components/content/Poetry.vue +27 -27
  32. package/app/components/content/ProseA.vue +24 -24
  33. package/app/components/content/ProseCode.vue +16 -16
  34. package/app/components/content/ProsePre.vue +117 -117
  35. package/app/components/content/ProseTable.vue +23 -23
  36. package/app/components/content/Quote.vue +21 -21
  37. package/app/components/content/Tab.vue +32 -32
  38. package/app/components/content/Timeline.vue +33 -33
  39. package/app/components/content/Tip.vue +40 -40
  40. package/app/components/content/VideoEmbed.vue +112 -112
  41. package/app/components/partial/Button.vue +36 -36
  42. package/app/components/partial/DlGroup.vue +31 -31
  43. package/app/components/partial/Dropdown.vue +21 -21
  44. package/app/components/partial/Error.vue +39 -39
  45. package/app/components/partial/Expand.vue +26 -26
  46. package/app/components/partial/IconNavList.vue +24 -24
  47. package/app/components/partial/Pagination.vue +62 -62
  48. package/app/components/partial/RadioGroup.vue +30 -30
  49. package/app/components/partial/Secret.vue +11 -11
  50. package/app/components/partial/Slider.vue +48 -48
  51. package/app/components/partial/Toggle.vue +17 -17
  52. package/app/components/popover/Lightbox.vue +44 -44
  53. package/app/components/popover/Search.vue +143 -143
  54. package/app/components/popover/SearchItem.vue +28 -28
  55. package/app/components/post/Archive.vue +40 -40
  56. package/app/components/post/Article.vue +49 -49
  57. package/app/components/post/Comment.vue +119 -119
  58. package/app/components/post/Excerpt.vue +40 -40
  59. package/app/components/post/OrderToggle.vue +70 -70
  60. package/app/components/post/PostFooter.vue +52 -52
  61. package/app/components/post/PostHeader.vue +66 -66
  62. package/app/components/post/PostSurround.vue +51 -51
  63. package/app/components/post/Slide.vue +85 -85
  64. package/app/components/util/Date.vue +62 -62
  65. package/app/components/util/HydrateSafe.vue +10 -10
  66. package/app/components/util/Img.vue +38 -38
  67. package/app/components/util/Link.vue +11 -11
  68. package/app/components/widget/BlogLog.vue +23 -23
  69. package/app/components/widget/BlogStats.vue +38 -38
  70. package/app/components/widget/BlogTech.vue +57 -57
  71. package/app/components/widget/Empty.vue +3 -3
  72. package/app/components/widget/Toc.vue +67 -67
  73. package/app/composables/useArticle.ts +113 -113
  74. package/app/composables/useAvoid.ts +87 -87
  75. package/app/composables/useCopy.ts +21 -21
  76. package/app/composables/usePagination.ts +70 -70
  77. package/app/composables/useShiki.ts +129 -129
  78. package/app/composables/useToc.ts +57 -57
  79. package/app/composables/useWidgets.ts +56 -56
  80. package/app/error.vue +31 -31
  81. package/app/layouts/default.vue +25 -25
  82. package/app/pages/[...slug].vue +59 -59
  83. package/app/pages/archive.vue +129 -129
  84. package/app/pages/index.vue +76 -76
  85. package/app/pages/link.vue +62 -62
  86. package/app/pages/preview.vue +46 -46
  87. package/app/plugins/easter-egg.ts +23 -23
  88. package/app/plugins/init.ts +6 -6
  89. package/app/plugins/tippy.ts +22 -22
  90. package/app/shiki.config.ts +8 -8
  91. package/app/stores/layout.ts +34 -34
  92. package/app/stores/search.ts +37 -37
  93. package/app/types/article.ts +16 -16
  94. package/app/types/index.ts +14 -14
  95. package/app/types/nav.ts +11 -11
  96. package/app/utils/anim.ts +37 -37
  97. package/app/utils/img.ts +83 -83
  98. package/config/app.ts +61 -61
  99. package/config/content.ts +75 -75
  100. package/config/define.ts +3 -3
  101. package/config/feeds.empty.ts +6 -4
  102. package/config/public.ts +1 -1
  103. package/config/schema.ts +89 -89
  104. package/config/server.ts +1 -1
  105. package/modules/clarity-config/index.ts +162 -159
  106. package/nuxt.config.ts +212 -212
  107. package/package.json +25 -21
  108. package/public/assets/atom.css +274 -274
  109. package/public/assets/atom.xsl +109 -109
  110. package/remark-plugins/rehype-meta-slots.ts +58 -58
  111. package/remark-plugins/remark-code-component.ts +41 -41
  112. package/server/api/stats.get.ts +84 -83
  113. package/server/routes/atom.xml.get.ts +85 -85
  114. package/server/routes/subscriptions.opml.get.ts +64 -64
  115. package/shared/utils/clarity.ts +24 -24
  116. package/shared/utils/icon.ts +218 -218
  117. package/shared/utils/link.ts +52 -52
  118. package/shared/utils/str.ts +88 -88
  119. package/shared/utils/time.ts +115 -115
@@ -1,146 +1,146 @@
1
- <script setup lang="ts">
1
+ <script setup lang="ts">
2
2
  import type { ModalEmits, ModalProps } from '../../types/modal'
3
- import MiniSearch from 'minisearch'
4
-
5
- const props = defineProps<ModalProps>()
6
-
7
- defineEmits<ModalEmits>()
8
-
9
- const clarity = useClarityConfig()
10
- const segmenter = Intl.Segmenter && new Intl.Segmenter(clarity.site.language, { granularity: 'word' })
11
-
12
- // await useAsyncData() 会阻塞渲染
13
- const { data, status } = await useLazyAsyncData(
14
- 'search',
15
- () => queryCollectionSearchSections('content', {
16
- ignoredTags: ['pre'],
17
- }),
18
- )
19
-
20
- const miniSearch = new MiniSearch({
21
- fields: ['title', 'content'],
22
- storeFields: ['title', 'titles', 'content', 'level'],
23
- searchOptions: {
24
- prefix: true,
25
- fuzzy: 0.2,
26
- combineWith: 'AND',
27
- boost: { title: 3, titles: 2 },
28
- },
29
- processTerm: segmenter
30
- ? term => Array.from(segmenter.segment(term), seg => seg.segment.toLowerCase())
31
- : undefined,
32
- })
33
-
34
- const searchStore = useSearchStore()
35
- const searchInput = ref<HTMLInputElement>()
36
-
37
- const { word, debouncedWord } = storeToRefs(searchStore)
38
- const result = computed(() => {
39
- void data.value
40
- return miniSearch.search(debouncedWord.value)
41
- })
42
-
43
- const isKeyboardMode = ref(false)
44
- const listResult = useTemplateRef('list-result')
45
-
46
- const activeIndex = ref(0)
47
- const activeItem = computed(() => listResult.value?.children[activeIndex.value] as HTMLLinkElement | undefined)
48
-
49
- whenever(() => props.open, focusInput)
50
-
51
- watch(status, (newStatus) => {
52
- if (newStatus === 'success' && data.value) {
53
- miniSearch.addAll(data.value)
54
- }
55
- })
56
-
57
- watch(debouncedWord, () => {
58
- activeIndex.value = 0
59
- })
60
-
61
- useEventListener('mousemove', () => isKeyboardMode.value = false)
62
- useEventListener('keydown', () => isKeyboardMode.value = true)
63
-
64
- async function focusInput(allSelect = false) {
65
- await nextTick()
66
- searchInput.value?.focus()
67
- if (allSelect)
68
- searchInput.value?.select()
69
- }
70
-
71
- function updateActiveIndex(index: number, isKeyboard = false) {
72
- focusInput()
73
- if (index < 0 || index >= result.value?.length)
74
- return
75
- activeIndex.value = index
76
- if (isKeyboard)
77
- isKeyboardMode.value = true
78
- if (activeItem.value && isKeyboardMode.value) {
79
- activeItem.value.scrollIntoView({ block: 'nearest' })
80
- }
81
- }
82
-
83
- function openActiveItem() {
84
- // 触发 vue-router 点击事件
85
- activeItem.value?.click()
86
- }
87
- </script>
88
-
89
- <template>
90
- <Transition name="float-in">
91
- <div v-if="open" class="blog-search">
92
- <form class="input" @submit.prevent>
93
- <Icon v-show="false" name="line-md:loading-alt-loop" />
94
- <Icon :name="status === 'pending' ? 'line-md:loading-alt-loop' : 'tabler:search'" />
95
-
96
- <!-- 方向键切换搜索结果不应只在搜索框内触发 -->
97
- <input
98
- ref="searchInput"
99
- v-model="word"
100
- type="search"
101
- incremental
102
- class="search-input"
103
- placeholder="键入开始搜索"
104
- @keydown.up.prevent
105
- @keydown.down.prevent
106
- >
107
- </form>
108
-
109
- <TransitionGroup name="expand">
110
- <div v-if="debouncedWord && status === 'success' && !result.length" class="no-result">
111
- 无结果
112
- </div>
113
-
114
- <menu
115
- v-if="result.length"
116
- ref="list-result"
117
- :key="result.length < 5 ? result.length : result[0]?.id"
118
- class="scrollcheck-y search-result"
119
- >
120
- <PopoverSearchItem
121
- v-for="(item, itemIndex) in result"
122
- :key="item.id"
123
- v-bind="item"
124
- :class="{ active: activeIndex === itemIndex }"
125
- @mousemove="updateActiveIndex(itemIndex)"
126
- />
127
- </menu>
128
-
129
- <div v-if="result.length" class="tip" @click="searchInput?.focus()">
130
- <Key code="ArrowUp" prevent @press="updateActiveIndex(activeIndex - 1, true)" />
131
- <Key code="ArrowDown" prevent @press="updateActiveIndex(activeIndex + 1, true)" />
132
- 切换&emsp;
133
- <Key code="Enter" icon @press="openActiveItem" />
134
- 选择&emsp;
135
- <Key code="Escape" :icon="false" @press="$emit('close')" />
136
- 关闭
137
- </div>
138
- </TransitionGroup>
139
- </div>
140
- </Transition>
141
- </template>
142
-
143
- <style lang="scss" scoped>
3
+ import MiniSearch from 'minisearch'
4
+
5
+ const props = defineProps<ModalProps>()
6
+
7
+ defineEmits<ModalEmits>()
8
+
9
+ const clarity = useClarityConfig()
10
+ const segmenter = Intl.Segmenter && new Intl.Segmenter(clarity.site.language, { granularity: 'word' })
11
+
12
+ // await useAsyncData() 会阻塞渲染
13
+ const { data, status } = await useLazyAsyncData(
14
+ 'search',
15
+ () => queryCollectionSearchSections('content', {
16
+ ignoredTags: ['pre'],
17
+ }),
18
+ )
19
+
20
+ const miniSearch = new MiniSearch({
21
+ fields: ['title', 'content'],
22
+ storeFields: ['title', 'titles', 'content', 'level'],
23
+ searchOptions: {
24
+ prefix: true,
25
+ fuzzy: 0.2,
26
+ combineWith: 'AND',
27
+ boost: { title: 3, titles: 2 },
28
+ },
29
+ processTerm: segmenter
30
+ ? term => Array.from(segmenter.segment(term), seg => seg.segment.toLowerCase())
31
+ : undefined,
32
+ })
33
+
34
+ const searchStore = useSearchStore()
35
+ const searchInput = ref<HTMLInputElement>()
36
+
37
+ const { word, debouncedWord } = storeToRefs(searchStore)
38
+ const result = computed(() => {
39
+ void data.value
40
+ return miniSearch.search(debouncedWord.value)
41
+ })
42
+
43
+ const isKeyboardMode = ref(false)
44
+ const listResult = useTemplateRef('list-result')
45
+
46
+ const activeIndex = ref(0)
47
+ const activeItem = computed(() => listResult.value?.children[activeIndex.value] as HTMLLinkElement | undefined)
48
+
49
+ whenever(() => props.open, focusInput)
50
+
51
+ watch(status, (newStatus) => {
52
+ if (newStatus === 'success' && data.value) {
53
+ miniSearch.addAll(data.value)
54
+ }
55
+ })
56
+
57
+ watch(debouncedWord, () => {
58
+ activeIndex.value = 0
59
+ })
60
+
61
+ useEventListener('mousemove', () => isKeyboardMode.value = false)
62
+ useEventListener('keydown', () => isKeyboardMode.value = true)
63
+
64
+ async function focusInput(allSelect = false) {
65
+ await nextTick()
66
+ searchInput.value?.focus()
67
+ if (allSelect)
68
+ searchInput.value?.select()
69
+ }
70
+
71
+ function updateActiveIndex(index: number, isKeyboard = false) {
72
+ focusInput()
73
+ if (index < 0 || index >= result.value?.length)
74
+ return
75
+ activeIndex.value = index
76
+ if (isKeyboard)
77
+ isKeyboardMode.value = true
78
+ if (activeItem.value && isKeyboardMode.value) {
79
+ activeItem.value.scrollIntoView({ block: 'nearest' })
80
+ }
81
+ }
82
+
83
+ function openActiveItem() {
84
+ // 触发 vue-router 点击事件
85
+ activeItem.value?.click()
86
+ }
87
+ </script>
88
+
89
+ <template>
90
+ <Transition name="float-in">
91
+ <div v-if="open" class="blog-search">
92
+ <form class="input" @submit.prevent>
93
+ <Icon v-show="false" name="line-md:loading-alt-loop" />
94
+ <Icon :name="status === 'pending' ? 'line-md:loading-alt-loop' : 'tabler:search'" />
95
+
96
+ <!-- 方向键切换搜索结果不应只在搜索框内触发 -->
97
+ <input
98
+ ref="searchInput"
99
+ v-model="word"
100
+ type="search"
101
+ incremental
102
+ class="search-input"
103
+ placeholder="键入开始搜索"
104
+ @keydown.up.prevent
105
+ @keydown.down.prevent
106
+ >
107
+ </form>
108
+
109
+ <TransitionGroup name="expand">
110
+ <div v-if="debouncedWord && status === 'success' && !result.length" class="no-result">
111
+ 无结果
112
+ </div>
113
+
114
+ <menu
115
+ v-if="result.length"
116
+ ref="list-result"
117
+ :key="result.length < 5 ? result.length : result[0]?.id"
118
+ class="scrollcheck-y search-result"
119
+ >
120
+ <PopoverSearchItem
121
+ v-for="(item, itemIndex) in result"
122
+ :key="item.id"
123
+ v-bind="item"
124
+ :class="{ active: activeIndex === itemIndex }"
125
+ @mousemove="updateActiveIndex(itemIndex)"
126
+ />
127
+ </menu>
128
+
129
+ <div v-if="result.length" class="tip" @click="searchInput?.focus()">
130
+ <Key code="ArrowUp" prevent @press="updateActiveIndex(activeIndex - 1, true)" />
131
+ <Key code="ArrowDown" prevent @press="updateActiveIndex(activeIndex + 1, true)" />
132
+ 切换&emsp;
133
+ <Key code="Enter" icon @press="openActiveItem" />
134
+ 选择&emsp;
135
+ <Key code="Escape" :icon="false" @press="$emit('close')" />
136
+ 关闭
137
+ </div>
138
+ </TransitionGroup>
139
+ </div>
140
+ </Transition>
141
+ </template>
142
+
143
+ <style lang="scss" scoped>
144
144
  .blog-search {
145
145
  --float-distance: 20vh;
146
146
 
@@ -211,4 +211,4 @@ function openActiveItem() {
211
211
  opacity: 0;
212
212
  max-height: 0;
213
213
  }
214
- </style>
214
+ </style>
@@ -1,30 +1,30 @@
1
- <script setup lang="ts">
2
- import type { SearchResult } from 'minisearch'
3
-
4
- interface SearchItem extends SearchResult {
5
- title: string
6
- content: string
7
- titles: string[]
8
- level: number
9
- }
10
-
11
- withDefaults(defineProps<Partial<SearchItem>>(), {
12
- titles: () => [],
13
- title: '',
14
- })
15
- </script>
16
-
17
- <template>
18
- <UtilLink :to="id" class="search-item">
19
- <hgroup class="text-creative">
20
- <span v-for="heading in [...titles, title]" :key="heading" class="title" v-html="highlightHtml(heading, queryTerms)" />
21
- <Icon v-if="level === 1" name="tabler:file-text" />
22
- </hgroup>
23
- <p v-if="content" class="content" v-html="highlightHtml(content, queryTerms)" />
24
- </UtilLink>
25
- </template>
26
-
27
- <style lang="scss" scoped>
1
+ <script setup lang="ts">
2
+ import type { SearchResult } from 'minisearch'
3
+
4
+ interface SearchItem extends SearchResult {
5
+ title: string
6
+ content: string
7
+ titles: string[]
8
+ level: number
9
+ }
10
+
11
+ withDefaults(defineProps<Partial<SearchItem>>(), {
12
+ titles: () => [],
13
+ title: '',
14
+ })
15
+ </script>
16
+
17
+ <template>
18
+ <UtilLink :to="id" class="search-item">
19
+ <hgroup class="text-creative">
20
+ <span v-for="heading in [...titles, title]" :key="heading" class="title" v-html="highlightHtml(heading, queryTerms)" />
21
+ <Icon v-if="level === 1" name="tabler:file-text" />
22
+ </hgroup>
23
+ <p v-if="content" class="content" v-html="highlightHtml(content, queryTerms)" />
24
+ </UtilLink>
25
+ </template>
26
+
27
+ <style lang="scss" scoped>
28
28
  .search-item {
29
29
  display: block;
30
30
  margin: 0.5em;
@@ -61,4 +61,4 @@ withDefaults(defineProps<Partial<SearchItem>>(), {
61
61
  white-space: pre-wrap;
62
62
  color: var(--c-text-2);
63
63
  }
64
- </style>
64
+ </style>
@@ -1,42 +1,42 @@
1
- <script setup lang="ts">
2
- import type { ArticleProps } from '../../types/article'
3
-
4
- const props = defineProps<{
5
- to?: string
6
- useUpdated?: boolean
7
- showCategory?: boolean
8
- } & ArticleProps>()
9
-
10
- const mainDate = computed(() => props.useUpdated ? props.updated : props.date)
11
- </script>
12
-
13
- <template>
14
- <li class="article-item">
15
- <UtilDate class="dim-hover" :date="mainDate" format="monthDay" />
16
-
17
- <div class="gradient-card" :style="{ '--c-accent': getCategoryColor(categories?.[0]) }">
18
- <UtilLink class="article-link scrollbar-hidden scrollcheck-x" :to :title="description">
19
- <span class="article-title">
20
- <Icon v-if="showCategory" :name="getCategoryIcon(categories?.[0])" />
21
- {{ title }}
22
- </span>
23
-
24
- <UtilDate
25
- v-if="date && useUpdated && isTimeDiffSignificant(date, updated)"
26
- class="dim-hover info"
27
- :date="date"
28
- :format="updated && isSameUnit(date, updated, 'year') ? 'monthDay' : 'date'"
29
- />
30
-
31
- <ul v-if="tags?.length" class="dim-hover info tag-list">
32
- <li v-for="tag in tags" :key="tag" v-text="tag" />
33
- </ul>
34
- </UtilLink>
35
- </div>
36
- </li>
37
- </template>
38
-
39
- <style lang="scss" scoped>
1
+ <script setup lang="ts">
2
+ import type { ArticleProps } from '../../types/article'
3
+
4
+ const props = defineProps<{
5
+ to?: string
6
+ useUpdated?: boolean
7
+ showCategory?: boolean
8
+ } & ArticleProps>()
9
+
10
+ const mainDate = computed(() => props.useUpdated ? props.updated : props.date)
11
+ </script>
12
+
13
+ <template>
14
+ <li class="article-item">
15
+ <UtilDate class="dim-hover" :date="mainDate" format="monthDay" />
16
+
17
+ <div class="gradient-card" :style="{ '--c-accent': getCategoryColor(categories?.[0]) }">
18
+ <UtilLink class="article-link scrollbar-hidden scrollcheck-x" :to :title="description">
19
+ <span class="article-title">
20
+ <Icon v-if="showCategory" :name="getCategoryIcon(categories?.[0])" />
21
+ {{ title }}
22
+ </span>
23
+
24
+ <UtilDate
25
+ v-if="date && useUpdated && isTimeDiffSignificant(date, updated)"
26
+ class="dim-hover info"
27
+ :date="date"
28
+ :format="updated && isSameUnit(date, updated, 'year') ? 'monthDay' : 'date'"
29
+ />
30
+
31
+ <ul v-if="tags?.length" class="dim-hover info tag-list">
32
+ <li v-for="tag in tags" :key="tag" v-text="tag" />
33
+ </ul>
34
+ </UtilLink>
35
+ </div>
36
+ </li>
37
+ </template>
38
+
39
+ <style lang="scss" scoped>
40
40
  .article-item {
41
41
  display: flex;
42
42
  align-items: center;
@@ -101,4 +101,4 @@ const mainDate = computed(() => props.useUpdated ? props.updated : props.date)
101
101
  }
102
102
  }
103
103
  }
104
- </style>
104
+ </style>
@@ -1,51 +1,51 @@
1
- <script setup lang="ts">
2
- import type { ArticleProps } from '../../types/article'
3
-
4
- const props = defineProps<{ useUpdated?: boolean } & ArticleProps>()
5
- const showAllDate = isTimeDiffSignificant(props.date, props.updated)
6
- </script>
7
-
8
- <template>
9
- <UtilLink class="article-card card upraise">
10
- <NuxtImg v-if="image" class="article-cover" :src="image" :alt="title" />
11
- <article>
12
- <h2 class="article-title text-creative">
13
- {{ title }}
14
- </h2>
15
-
16
- <p v-if="description" class="article-description">
17
- {{ description }}
18
- </p>
19
-
20
- <div class="article-info">
21
- <UtilDate
22
- v-if="date && (showAllDate || !useUpdated)"
23
- :date
24
- icon="tabler:pencil-minus"
25
- />
26
-
27
- <UtilDate
28
- v-if="updated && (showAllDate || useUpdated)"
29
- :class="{ 'use-updated': useUpdated }"
30
- :date="updated"
31
- icon="tabler:clock-edit"
32
- />
33
-
34
- <span v-if="categories" :style="{ color: getCategoryColor(categories[0]) }">
35
- <Icon :name="getCategoryIcon(categories[0])" />
36
- {{ categories[0] }}
37
- </span>
38
-
39
- <span v-if="readingTime?.words" class="article-words">
40
- <Icon name="tabler:pilcrow" />
41
- {{ formatNumber(readingTime?.words) }}字
42
- </span>
43
- </div>
44
- </article>
45
- </UtilLink>
46
- </template>
47
-
48
- <style lang="scss" scoped>
1
+ <script setup lang="ts">
2
+ import type { ArticleProps } from '../../types/article'
3
+
4
+ const props = defineProps<{ useUpdated?: boolean } & ArticleProps>()
5
+ const showAllDate = isTimeDiffSignificant(props.date, props.updated)
6
+ </script>
7
+
8
+ <template>
9
+ <UtilLink class="article-card card upraise">
10
+ <NuxtImg v-if="image" class="article-cover" :src="image" :alt="title" />
11
+ <article>
12
+ <h2 class="article-title text-creative">
13
+ {{ title }}
14
+ </h2>
15
+
16
+ <p v-if="description" class="article-description">
17
+ {{ description }}
18
+ </p>
19
+
20
+ <div class="article-info">
21
+ <UtilDate
22
+ v-if="date && (showAllDate || !useUpdated)"
23
+ :date
24
+ icon="tabler:pencil-minus"
25
+ />
26
+
27
+ <UtilDate
28
+ v-if="updated && (showAllDate || useUpdated)"
29
+ :class="{ 'use-updated': useUpdated }"
30
+ :date="updated"
31
+ icon="tabler:clock-edit"
32
+ />
33
+
34
+ <span v-if="categories" :style="{ color: getCategoryColor(categories[0]) }">
35
+ <Icon :name="getCategoryIcon(categories[0])" />
36
+ {{ categories[0] }}
37
+ </span>
38
+
39
+ <span v-if="readingTime?.words" class="article-words">
40
+ <Icon name="tabler:pilcrow" />
41
+ {{ formatNumber(readingTime?.words) }}字
42
+ </span>
43
+ </div>
44
+ </article>
45
+ </UtilLink>
46
+ </template>
47
+
48
+ <style lang="scss" scoped>
49
49
  .article-card {
50
50
  container-type: inline-size;
51
51
  position: relative;
@@ -135,4 +135,4 @@ const showAllDate = isTimeDiffSignificant(props.date, props.updated)
135
135
  @include cover-narrow;
136
136
  }
137
137
  }
138
- </style>
138
+ </style>