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,138 @@
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
+ .article-card {
50
+ container-type: inline-size;
51
+ position: relative;
52
+ margin: 1em 0;
53
+ border-radius: 0.8em;
54
+ color: var(--c-text);
55
+ animation: float-in 0.2s var(--delay) backwards;
56
+
57
+ > article {
58
+ display: grid;
59
+ gap: 0.5em;
60
+ padding: 1em;
61
+ }
62
+ }
63
+
64
+ .article-info {
65
+ display: flex;
66
+ flex-wrap: wrap;
67
+ gap: 0.5em clamp(1em, 5%, 1.5em);
68
+ font-size: 0.8em;
69
+ color: var(--c-text-2);
70
+
71
+ &:empty {
72
+ display: none;
73
+ }
74
+
75
+ .use-updated {
76
+ order: -1;
77
+ }
78
+ }
79
+
80
+ .article-title {
81
+ font-size: 1.2em;
82
+ color: var(--c-text);
83
+ }
84
+
85
+ .article-description {
86
+ font-size: 0.9em;
87
+ color: var(--c-text-2);
88
+ }
89
+
90
+ .article-cover {
91
+ position: absolute;
92
+ opacity: 0.8;
93
+ inset-inline-end: 0;
94
+ top: 0;
95
+ width: calc(40% + 2em);
96
+ height: 100%;
97
+ margin: 0;
98
+ mask-image: linear-gradient(to var(--end), transparent, #FFF 50%);
99
+ transition: opacity 0.2s;
100
+ object-fit: cover;
101
+
102
+ :hover > & {
103
+ opacity: 1;
104
+ }
105
+
106
+ & + article {
107
+ position: relative;
108
+ width: 60%;
109
+ }
110
+
111
+ @mixin cover-narrow {
112
+ position: revert;
113
+ width: 100%;
114
+ height: auto;
115
+ max-width: none;
116
+ max-height: 256px;
117
+ aspect-ratio: 2.4;
118
+ margin-bottom: -10%;
119
+ mask-image: linear-gradient(#FFF 50%, transparent);
120
+
121
+ & + article {
122
+ width: auto;
123
+
124
+ > .article-title {
125
+ text-shadow: 0 0 0.2em var(--ld-bg-card), 0 0 0.5em var(--ld-bg-card), 0 0 1em var(--ld-bg-card);
126
+ }
127
+ }
128
+ }
129
+
130
+ @media (max-width: $breakpoint-phone) {
131
+ @include cover-narrow;
132
+ }
133
+
134
+ @container (max-width: #{$breakpoint-phone}) {
135
+ @include cover-narrow;
136
+ }
137
+ }
138
+ </style>
@@ -0,0 +1,275 @@
1
+ <script setup lang="tsx">
2
+ import type { TippyComponent } from 'vue-tippy'
3
+
4
+ const clarity = useClarityConfig()
5
+ const enabled = computed(() => !!clarity.integrations.twikoo?.envId)
6
+
7
+ const commentEl = useTemplateRef('comment')
8
+ const popoverEl = useTemplateRef<TippyComponent>('popover')
9
+ const popoverJumpTo = ref('')
10
+ const popoverInputEl = useTemplateRef('popover-input')
11
+ const showUndo = ref(false)
12
+
13
+ const popoverBind = ref<TippyComponent['$props']>({})
14
+
15
+ /** 评论区链接守卫 */
16
+ useEventListener(commentEl, 'click', (e) => {
17
+ if (!(e.target instanceof Element))
18
+ return
19
+
20
+ if (e.target.matches('.tk-avatar-img'))
21
+ e.stopPropagation()
22
+
23
+ const popoverTarget = e.target.closest('a[target="_blank"]')
24
+ if (!(popoverTarget instanceof HTMLAnchorElement))
25
+ return
26
+
27
+ e.preventDefault()
28
+ popoverEl.value?.hide()
29
+
30
+ popoverJumpTo.value = safelyDecodeUriComponent(popoverTarget.href)
31
+ popoverBind.value = {
32
+ getReferenceClientRect: () => popoverTarget.getBoundingClientRect(),
33
+ triggerTarget: popoverTarget,
34
+ }
35
+
36
+ nextTick(checkUndoable)
37
+ popoverEl.value?.show()
38
+ }, { capture: true })
39
+
40
+ function checkUndoable() {
41
+ showUndo.value = popoverInputEl.value?.textContent !== popoverJumpTo.value
42
+ }
43
+
44
+ function undo() {
45
+ if (!popoverInputEl.value)
46
+ return
47
+ popoverInputEl.value.textContent = popoverJumpTo.value
48
+ checkUndoable()
49
+ }
50
+
51
+ function confirmOpen() {
52
+ window.open(popoverInputEl.value?.textContent, '_blank')
53
+ }
54
+
55
+ onMounted(() => {
56
+ if (!enabled.value)
57
+ return
58
+
59
+ window.twikoo?.init?.({
60
+ envId: clarity.integrations.twikoo?.envId ?? '',
61
+ // twikoo 会把挂载后的元素变为 #twikoo
62
+ el: '#twikoo',
63
+ })
64
+ })
65
+ </script>
66
+
67
+ <template>
68
+ <section ref="comment" class="z-comment">
69
+ <h3 class="text-creative">
70
+ 评论区
71
+ </h3>
72
+
73
+ <!-- interactive 默认会把气泡移动到 triggerTarget 的父元素上 -->
74
+ <Tooltip
75
+ ref="popover"
76
+ v-bind="popoverBind"
77
+ :append-to="() => commentEl!"
78
+ interactive
79
+ :aria="{ expanded: false }"
80
+ trigger="focusin"
81
+ >
82
+ <template #content>
83
+ <div class="popover-confirm">
84
+ <span
85
+ ref="popover-input"
86
+ class="input"
87
+ contenteditable="plaintext-only"
88
+ spellcheck="false"
89
+ @input="checkUndoable"
90
+ @keydown.enter.prevent="confirmOpen"
91
+ v-text="popoverJumpTo"
92
+ />
93
+
94
+ <button
95
+ v-if="showUndo"
96
+ aria-label="恢复原始内容"
97
+ @click="undo()"
98
+ >
99
+ <Icon name="tabler:arrow-back-up" />
100
+ </button>
101
+
102
+ <ZButton
103
+ primary
104
+ text="访问"
105
+ @click="confirmOpen"
106
+ />
107
+ </div>
108
+ </template>
109
+ </Tooltip>
110
+
111
+ <div v-if="enabled" id="twikoo">
112
+ <p>评论加载中...</p>
113
+ </div>
114
+ <p v-else class="no-comment">
115
+ 本文暂未开启评论
116
+ </p>
117
+ </section>
118
+ </template>
119
+
120
+ <style lang="scss" scoped>
121
+ .z-comment {
122
+ margin: 3rem 1rem;
123
+
124
+ > h3 {
125
+ margin-top: 3rem;
126
+ font-size: 1.25rem;
127
+ }
128
+ }
129
+
130
+ :deep() > [data-tippy-root] > .tippy-box {
131
+ padding: 0;
132
+ }
133
+
134
+ .popover-confirm {
135
+ display: flex;
136
+ align-items: center;
137
+ overflow-wrap: anywhere;
138
+
139
+ > .input {
140
+ min-width: 0;
141
+ padding: 0.3em 0.6em;
142
+ outline: none;
143
+ }
144
+
145
+ > button {
146
+ flex-shrink: 0;
147
+ align-self: stretch;
148
+ padding: 0.3em;
149
+ border-radius: 0 0.5em 0.5em 0;
150
+ }
151
+ }
152
+
153
+ :deep(#twikoo) {
154
+ margin: 2em 0;
155
+
156
+ .tk-admin-container {
157
+ position: fixed;
158
+ z-index: calc(var(--z-index-popover) + 1);
159
+ }
160
+
161
+ .tk-input {
162
+ font-family: var(--font-monospace);
163
+ }
164
+
165
+ .tk-avatar {
166
+ border-radius: 50%;
167
+
168
+ @supports (corner-shape: squircle) {
169
+ corner-shape: superellipse(1.2);
170
+ }
171
+
172
+ &.tk-clickable {
173
+ cursor: auto;
174
+ }
175
+ }
176
+
177
+ .tk-time {
178
+ color: var(--c-text-3);
179
+ }
180
+
181
+ // 防止 a 被 overflow hidden
182
+ .tk-content {
183
+ margin: -0.2em;
184
+ padding: 0.2em;
185
+ }
186
+
187
+ .tk-comments-title, .tk-nick {
188
+ font-family: var(--font-creative);
189
+ }
190
+
191
+ .tk-owo-emotion {
192
+ width: auto;
193
+ height: 1.4em;
194
+ vertical-align: text-bottom;
195
+ }
196
+
197
+ .tk-extras, .tk-footer {
198
+ font-size: 0.7em;
199
+ color: var(--c-text-3);
200
+ }
201
+
202
+ .tk-replies:not(.tk-replies-expand) {
203
+ mask-image: linear-gradient(to top, transparent, #FFF 4em);
204
+ }
205
+
206
+ .tk-expand {
207
+ border-radius: 0.5em;
208
+ transition: background-color 0.1s;
209
+ }
210
+
211
+ .tippy-svg-arrow > svg {
212
+ fill: inherit;
213
+ width: auto;
214
+ height: auto;
215
+ }
216
+ }
217
+
218
+ .no-comment {
219
+ margin: 2em 0;
220
+ color: var(--c-text-3);
221
+ }
222
+
223
+ :deep(:where(.tk-preview-container,.tk-content)) {
224
+ pre {
225
+ overflow: auto;
226
+ border-radius: 0.5em;
227
+ font-size: 0.85em;
228
+ }
229
+
230
+ a {
231
+ margin: -0.1em -0.2em;
232
+ padding: 0.1em 0.2em;
233
+ background: linear-gradient(var(--c-primary-soft), var(--c-primary-soft)) no-repeat center bottom / 100% 0.1em;
234
+ color: var(--c-primary);
235
+ transition: all 0.2s;
236
+
237
+ &:hover {
238
+ border-radius: 0.3em;
239
+ background-size: 100% 100%;
240
+ }
241
+ }
242
+
243
+ p {
244
+ margin: 0.2em 0;
245
+ }
246
+
247
+ img {
248
+ border-radius: 0.5em;
249
+ }
250
+
251
+ menu, ol, ul {
252
+ margin: 0.5em 0;
253
+ padding-inline-start: 1.5em;
254
+ font-size: 0.9rem;
255
+ list-style: revert;
256
+
257
+ > li {
258
+ margin: 0.2em 0;
259
+
260
+ &::marker {
261
+ color: var(--c-primary);
262
+ }
263
+ }
264
+ }
265
+
266
+ blockquote {
267
+ margin: 0.5em 0;
268
+ padding: 0.2em 0.5em;
269
+ border-inline-start: 4px solid var(--c-border);
270
+ border-radius: 4px;
271
+ background-color: var(--c-bg-2);
272
+ font-size: 0.9em;
273
+ }
274
+ }
275
+ </style>
@@ -0,0 +1,66 @@
1
+ <script setup lang="ts">
2
+ import { delay } from 'es-toolkit/promise'
3
+
4
+ const props = defineProps<{
5
+ excerpt: string
6
+ }>()
7
+
8
+ const clarity = useClarityConfig()
9
+
10
+ const excerpt = ref(props.excerpt)
11
+ const caret = ref('')
12
+
13
+ if (clarity.component.excerpt?.animation !== false) {
14
+ excerpt.value = ''
15
+ onMounted(async () => {
16
+ caret.value = clarity.component.excerpt?.caret ?? '_'
17
+ for (const char of props.excerpt) {
18
+ excerpt.value += char
19
+ await delay(50)
20
+ }
21
+ caret.value = ''
22
+ })
23
+ }
24
+
25
+ if (import.meta.dev) {
26
+ watch(() => props.excerpt, (newExcerpt) => {
27
+ excerpt.value = newExcerpt
28
+ })
29
+ }
30
+ </script>
31
+
32
+ <template>
33
+ <div class="md-excerpt gradient-card">
34
+ <span class="dynamic"><Icon name="tabler:sparkles-2" />{{ excerpt }}{{ caret }}</span>
35
+ <span class="static"><Icon name="tabler:sparkles-2" />{{ props.excerpt }}</span>
36
+ </div>
37
+ </template>
38
+
39
+ <style lang="scss" scoped>
40
+ .md-excerpt {
41
+ opacity: 0.6;
42
+ margin: 1rem 0.5rem;
43
+ padding: 0.5rem;
44
+ font-size: 0.9em;
45
+ transition: opacity 0.2s;
46
+
47
+ > .static {
48
+ opacity: 0;
49
+ pointer-events: none;
50
+ user-select: none;
51
+ }
52
+
53
+ > .dynamic {
54
+ position: absolute;
55
+ width: calc(100% - 1rem);
56
+ }
57
+
58
+ .iconify {
59
+ margin-inline-end: 0.3em;
60
+ }
61
+
62
+ &:hover {
63
+ opacity: 1;
64
+ }
65
+ }
66
+ </style>
@@ -0,0 +1,101 @@
1
+ <script setup lang="ts">
2
+ import type { ArticleOrderType } from '../../types/article'
3
+
4
+ const props = defineProps<{
5
+ // 强制允许或禁止升序
6
+ enableAscending?: boolean
7
+ disableAscending?: boolean
8
+ categories?: (string | undefined)[]
9
+ secretDelay?: string
10
+ }>()
11
+
12
+ const clarity = useClarityConfig()
13
+ const orderMap = computed(() => clarity.article.order)
14
+ // 配置文件中允许升序时,且未明确禁用升序时,允许升序
15
+ const allowAscending = computed(() => clarity.pagination.allowAscending ? !props.disableAscending : props.enableAscending)
16
+
17
+ const category = defineModel<string>('category')
18
+ const sortOrder = defineModel<ArticleOrderType>('sortOrder', { default: 'date' })
19
+ const isAscending = defineModel<boolean>('isAscending')
20
+
21
+ function toggleOrder() {
22
+ const orderKeys = Object.keys(orderMap.value) as (ArticleOrderType)[]
23
+ sortOrder.value = orderKeys[(orderKeys.indexOf(sortOrder.value) + 1) % orderKeys.length] || 'date'
24
+ }
25
+
26
+ function toggleDirection() {
27
+ if (!allowAscending.value)
28
+ return
29
+ isAscending.value = !isAscending.value
30
+ }
31
+ </script>
32
+
33
+ <template>
34
+ <div class="order-toggle" :style="{ '--secret-delay': secretDelay }">
35
+ <slot />
36
+
37
+ <ZDropdown trigger="focusin" tabindex="0">
38
+ <button :disabled="!categories">
39
+ <Icon :name="getCategoryIcon(category)" />
40
+ <span class="order-text">{{ category ?? '全部分类' }}</span>
41
+ </button>
42
+
43
+ <template #content="{ hide }">
44
+ <button :class="{ active: !category }" @click="hide(), category = undefined">
45
+ <Icon :name="getCategoryIcon()" />
46
+ <span>全部分类</span>
47
+ </button>
48
+
49
+ <button v-for="item in categories" :key="item" :class="{ active: item === category }" @click="hide(), category = item">
50
+ <Icon :name="getCategoryIcon(item)" />
51
+ <span>{{ item }}</span>
52
+ </button>
53
+ </template>
54
+ </ZDropdown>
55
+
56
+ <span>
57
+ <button v-if="allowAscending" aria-label="切换排序方向" @click="toggleDirection">
58
+ <Icon name="tabler:sort-descending" class="toggle-direction" :class="{ ascending: isAscending }" />
59
+ </button>
60
+
61
+ <button @click="toggleOrder">
62
+ <Icon v-if="!allowAscending" name="tabler:sort-descending" />
63
+ <span class="order-text">{{ orderMap[sortOrder] || sortOrder }}</span>
64
+ </button>
65
+ </span>
66
+ </div>
67
+ </template>
68
+
69
+ <style lang="scss" scoped>
70
+ .order-toggle {
71
+ display: flex;
72
+ gap: 1rem;
73
+ color: var(--c-text-2);
74
+
75
+ :deep(button), :deep(a) {
76
+ transition: color 0.2s;
77
+
78
+ &:hover {
79
+ color: var(--c-primary);
80
+ }
81
+ }
82
+
83
+ .toggle-direction {
84
+ display: inline-block;
85
+ margin-inline-end: 0.1em;
86
+ transition: transform 0.2s;
87
+
88
+ &.ascending {
89
+ transform: scaleY(-1);
90
+ }
91
+ }
92
+ }
93
+
94
+ :deep(.secret-container) {
95
+ margin-inline-end: auto;
96
+ }
97
+
98
+ .iconify + span {
99
+ margin-inline-start: 0.2em;
100
+ }
101
+ </style>
@@ -0,0 +1,80 @@
1
+ <script setup lang="ts">
2
+ import type { ArticleProps } from '../../types/article'
3
+
4
+ defineOptions({ inheritAttrs: false })
5
+ defineProps<ArticleProps>()
6
+
7
+ const [DefineTemplate, ReuseTemplate] = createReusableTemplate<{
8
+ title: string
9
+ }>({ inheritAttrs: false })
10
+
11
+ const clarity = useClarityConfig()
12
+ </script>
13
+
14
+ <template>
15
+ <div class="post-footer">
16
+ <DefineTemplate v-slot="{ $slots, title }">
17
+ <section>
18
+ <div class="title text-creative">
19
+ {{ title }}
20
+ </div>
21
+
22
+ <div class="content">
23
+ <component :is="$slots.default" />
24
+ </div>
25
+ </section>
26
+ </DefineTemplate>
27
+
28
+ <ReuseTemplate v-if="references" title="参考链接">
29
+ <ul>
30
+ <li v-for="{ title, link }, i in references" :key="i">
31
+ <ProseA :href="link || ''">
32
+ {{ title ?? link }}
33
+ </ProseA>
34
+ </li>
35
+ </ul>
36
+ </ReuseTemplate>
37
+
38
+ <ReuseTemplate :title="meta?.slots?.copyright?.props?.title as string || '许可协议'">
39
+ <ContentRenderer v-if="meta?.slots?.copyright" :value="meta?.slots?.copyright" />
40
+ <p v-else>
41
+ 本文采用 <ProseA v-if="clarity.site.copyright?.url" :href="clarity.site.copyright.url">
42
+ {{ clarity.site.copyright.name }}
43
+ </ProseA>
44
+ <span v-else-if="clarity.site.copyright?.name">{{ clarity.site.copyright.name }}</span>
45
+ 许可协议,转载请注明出处。
46
+ </p>
47
+ </ReuseTemplate>
48
+ </div>
49
+ </template>
50
+
51
+ <style lang="scss" scoped>
52
+ .post-footer {
53
+ margin: 2rem 0.5rem;
54
+ border: 1px solid var(--c-border);
55
+ border-radius: 1rem;
56
+ background-color: var(--c-bg-2);
57
+ }
58
+
59
+ section {
60
+ padding: 1rem;
61
+
62
+ & + section {
63
+ border-top: 1px solid var(--c-border);
64
+ }
65
+ }
66
+
67
+ .title {
68
+ font-weight: bold;
69
+ color: var(--c-text);
70
+ }
71
+
72
+ .content {
73
+ margin-top: 0.5em;
74
+ font-size: 0.9rem;
75
+
76
+ li {
77
+ margin: 0.5em 0;
78
+ }
79
+ }
80
+ </style>