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,186 @@
1
+ // 一部分具有卡片样式的组件复用了此类
2
+ .card {
3
+ contain: paint;
4
+ display: block;
5
+ border-radius: 0.5em;
6
+ box-shadow: var(--box-shadow-1);
7
+ background-color: var(--ld-bg-card);
8
+ transition: all 0.2s;
9
+
10
+ &.upraise:hover {
11
+ box-shadow: var(--box-shadow-3);
12
+ transform: translateY(-2px);
13
+ }
14
+ }
15
+
16
+ .gradient-card {
17
+ position: relative;
18
+ border-radius: 0.5rem;
19
+ z-index: 0;
20
+
21
+ &::before {
22
+ content: "";
23
+ position: absolute;
24
+ opacity: 0;
25
+ inset: 0;
26
+ border: 2px solid transparent;
27
+ border-radius: inherit;
28
+ background: linear-gradient(var(--c-bg-a80), var(--c-bg-a80)) padding-box, linear-gradient(45deg, var(--c-accent), var(--c-primary-soft)) border-box;
29
+ transition: opacity 0.2s;
30
+ z-index: -1;
31
+ }
32
+
33
+ &:hover, &:focus, &.active {
34
+ color: var(--c-text-1);
35
+
36
+ &::before {
37
+ opacity: 1;
38
+ }
39
+ }
40
+ }
41
+
42
+ .scrollbar-hidden.scrollbar-hidden {
43
+ --scrollbar-height: 0px;
44
+ --guessed-scrollbar-width: 0px;
45
+
46
+ scrollbar-width: none;
47
+
48
+ &::-webkit-scrollbar {
49
+ display: none;
50
+ }
51
+ }
52
+
53
+ // 横向滚动遮罩
54
+ // PURE CSS MAGIC!
55
+ .scrollcheck-x, .scrollcheck-y {
56
+ scrollbar-width: thin;
57
+
58
+ &::-webkit-scrollbar {
59
+ width: 6px;
60
+ height: 6px;
61
+ background-color: var(--c-bg-2);
62
+ cursor: default;
63
+ }
64
+
65
+ &::-webkit-scrollbar-thumb {
66
+ border-radius: 4px;
67
+ background-color: var(--c-bg-soft);
68
+ cursor: pointer;
69
+ }
70
+ }
71
+
72
+ .scrollcheck-x {
73
+ --start-offset: 0px; // 如果填写 0 会在 calc() 时出错
74
+ --scrollbar-height: 8px;
75
+ --fadeout-width: 2em;
76
+
77
+ overflow-x: auto;
78
+ animation: scrollcheck-x;
79
+ animation-timeline: scroll(x self);
80
+ }
81
+
82
+ @keyframes scrollcheck-x {
83
+ 0% {
84
+ mask-image:
85
+ linear-gradient(to var(--end), #FFF calc(100% - var(--fadeout-width)), transparent),
86
+ linear-gradient(to top, #FFF var(--scrollbar-height), transparent var(--scrollbar-height));
87
+ }
88
+
89
+ 0.01%, 99.99% {
90
+ mask-image:
91
+ linear-gradient(to var(--end), #FFF var(--start-offset), transparent var(--start-offset), #FFF calc(var(--start-offset) + var(--fadeout-width)), #FFF calc(100% - var(--fadeout-width)), transparent),
92
+ linear-gradient(to top, #FFF var(--scrollbar-height), transparent var(--scrollbar-height));
93
+ }
94
+
95
+ 100% {
96
+ mask-image:
97
+ linear-gradient(to var(--end), #FFF var(--start-offset), transparent var(--start-offset), #FFF calc(var(--start-offset) + var(--fadeout-width))),
98
+ linear-gradient(to top, #FFF var(--scrollbar-height), transparent var(--scrollbar-height));
99
+ }
100
+ }
101
+
102
+ .scrollcheck-y {
103
+ // 如果填写 0 会在 calc() 时出错
104
+ --top-offset: 0px;
105
+ --guessed-scrollbar-width: 8px;
106
+ --fadeout-height: 2em;
107
+
108
+ overflow-y: auto;
109
+ animation: scrollcheck-y;
110
+ animation-timeline: scroll(y self);
111
+ }
112
+
113
+ @keyframes scrollcheck-y {
114
+ 0% {
115
+ mask-image:
116
+ linear-gradient(to bottom, #FFF calc(100% - var(--fadeout-height)), transparent),
117
+ linear-gradient(to var(--start), #FFF var(--guessed-scrollbar-width), transparent var(--guessed-scrollbar-width));
118
+ }
119
+
120
+ 0.01%, 99.99% {
121
+ mask-image:
122
+ linear-gradient(to bottom, #FFF var(--top-offset), transparent var(--top-offset), #FFF calc(var(--top-offset) + var(--fadeout-height)), #FFF calc(100% - var(--fadeout-height)), transparent),
123
+ linear-gradient(to var(--start), #FFF var(--guessed-scrollbar-width), transparent var(--guessed-scrollbar-width));
124
+ }
125
+
126
+ 100% {
127
+ mask-image:
128
+ linear-gradient(to bottom, #FFF var(--top-offset), transparent var(--top-offset), #FFF calc(var(--top-offset) + var(--fadeout-height))),
129
+ linear-gradient(to var(--start), #FFF var(--guessed-scrollbar-width), transparent var(--guessed-scrollbar-width));
130
+ }
131
+ }
132
+
133
+ @each $name, $size in $breakpoints {
134
+ @media not (max-width: $size) {
135
+ .#{$name}-only {
136
+ display: none !important; /* stylelint-disable-line declaration-no-important */
137
+ }
138
+ }
139
+
140
+ @media (max-width: $size) {
141
+ .#{$name}-hidden {
142
+ display: none !important; /* stylelint-disable-line declaration-no-important */
143
+ }
144
+ }
145
+ }
146
+
147
+ .proper-height {
148
+ min-height: 70vh;
149
+ }
150
+
151
+ .round-cobblestone {
152
+ corner-shape: superellipse(1.2);
153
+ }
154
+
155
+ .text-center {
156
+ text-align: center;
157
+ }
158
+
159
+ .text-creative, .text-tech {
160
+ font-family: var(--font-creative);
161
+ font-weight: 550;
162
+ -webkit-font-smoothing: antialiased; // macOS下"DOUYINSANSBOLD-GB"过粗
163
+ }
164
+
165
+ .text-story {
166
+ font-family: var(--font-serif);
167
+ }
168
+
169
+ .text-repeat {
170
+ text-shadow: 0.1em 0 var(--c-bg-soft), 0.2em 0 var(--c-bg-soft), 0.3em 0 var(--c-bg-soft), 0.4em 0 var(--c-bg-soft), 0.5em 0 var(--c-bg-soft);
171
+ }
172
+
173
+ .text-zoom {
174
+ display: inline-block;
175
+ transform-origin: left bottom;
176
+
177
+ @supports (animation-timeline: view()) {
178
+ animation: zoom 1s both;
179
+ animation-timeline: view();
180
+ }
181
+
182
+ @keyframes zoom {
183
+ from { transform: scale(0.8); }
184
+ to { transform: scale(1.25); }
185
+ }
186
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="460" height="460" viewBox="0 0 24 24"><path fill="currentColor" fill-rule="evenodd" d="M22 6.426v11.148c0 1.847-1.6 3.015-2.903 2.118L13 15.232V8.768l6.097-4.46C20.399 3.411 22 4.58 22 6.426" clip-rule="evenodd" opacity=".5"/><path fill="currentColor" d="M13 7.123v9.754c0 1.616-1.467 2.638-2.661 1.853L2.92 13.853c-1.228-.807-1.228-2.899 0-3.706l7.42-4.877c1.193-.785 2.66.237 2.66 1.853"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="460" height="460" viewBox="0 0 24 24"><path fill="currentColor" fill-rule="evenodd" d="M2 6.426v11.148c0 1.847 1.6 3.015 2.903 2.118L11 15.232V8.768l-6.097-4.46C3.601 3.411 2 4.58 2 6.426" clip-rule="evenodd" opacity=".5"/><path fill="currentColor" d="M11 7.123v9.754c0 1.616 1.467 2.638 2.661 1.853l7.418-4.877c1.228-.807 1.228-2.899 0-3.706L13.66 5.27C12.467 4.485 11 5.507 11 7.123"/></svg>
@@ -0,0 +1,63 @@
1
+ <script setup lang="ts">
2
+ const layoutStore = useLayoutStore()
3
+
4
+ // 页面完成异步 setup 后才注册具名插槽;水合前等待,避免重复插入 SSR 侧栏。
5
+ const layoutSlots = inject<{ ready: Promise<void> }>(Symbol.for('dxup:layout-slots'))
6
+ if (layoutSlots)
7
+ await layoutSlots.ready
8
+ </script>
9
+
10
+ <template>
11
+ <BlogMask
12
+ :show="layoutStore.state === 'aside'"
13
+ class="widescreen-only"
14
+ @click="layoutStore.close()"
15
+ />
16
+
17
+ <!-- 不能用 Transition 实现弹出收起动画,因为宽屏状态始终显示 -->
18
+ <!-- 如果为空数组则隐藏 -->
19
+ <aside id="blog-aside" :class="{ show: layoutStore.state === 'aside' }">
20
+ <slot />
21
+ </aside>
22
+ </template>
23
+
24
+ <style lang="scss" scoped>
25
+ #blog-aside {
26
+ display: flex;
27
+ flex-direction: column;
28
+ gap: 1rem;
29
+ overflow: auto;
30
+ padding: 0.5rem;
31
+ z-index: var(--z-index-popover);
32
+
33
+ @media (max-width: $breakpoint-widescreen) {
34
+ position: fixed;
35
+ inset-inline-end: 0;
36
+ top: 0;
37
+ width: 320px;
38
+ height: auto;
39
+ max-width: 100%;
40
+ max-height: 100%;
41
+ transform: var(--transform-end-far);
42
+ transition: transform 0.2s;
43
+
44
+ // TODO 留 padding-bottom 避让 BlogPanel
45
+
46
+ > :deep(.blog-widget) {
47
+ padding: 0.5rem;
48
+ border-radius: 1rem;
49
+ box-shadow: var(--box-shadow-1), var(--box-shadow-2);
50
+ background-color: var(--ld-bg-blur);
51
+ backdrop-filter: blur(0.5rem);
52
+ }
53
+
54
+ &.show {
55
+ transform: none;
56
+ }
57
+ }
58
+
59
+ &:empty {
60
+ display: none;
61
+ }
62
+ }
63
+ </style>
@@ -0,0 +1,61 @@
1
+ <script setup lang="ts">
2
+ const clarity = useClarityConfig()
3
+ </script>
4
+
5
+ <template>
6
+ <footer class="blog-footer">
7
+ <nav class="footer-nav">
8
+ <div v-for="(group, groupIndex) in clarity.footer.nav" :key="groupIndex">
9
+ <hgroup class="text-creative" v-text="group.title" />
10
+ <menu>
11
+ <li v-for="(item, itemIndex) in group.items" :key="itemIndex">
12
+ <UtilLink :to="item.url">
13
+ <Icon :name="item.icon" />
14
+ <span class="nav-text">{{ item.text }}</span>
15
+ </UtilLink>
16
+ </li>
17
+ </menu>
18
+ </div>
19
+ </nav>
20
+ <p v-html="clarity.footer.copyright" />
21
+ </footer>
22
+ </template>
23
+
24
+ <style lang="scss" scoped>
25
+ .blog-footer {
26
+ margin: 3rem 1rem;
27
+ font-size: 0.9em;
28
+ color: var(--c-text-2);
29
+
30
+ .footer-nav {
31
+ display: flex;
32
+ flex-wrap: wrap;
33
+ gap: 5vw clamp(2rem, 5%, 5vw);
34
+ padding-block: 3rem;
35
+
36
+ hgroup {
37
+ margin: 0.5em;
38
+ }
39
+
40
+ a {
41
+ display: flex;
42
+ align-items: center;
43
+ gap: 0.3em;
44
+ width: fit-content;
45
+ padding: 0.3em 0.5em;
46
+ border-radius: 0.5em;
47
+ font-size: 0.9em;
48
+ transition: background-color 0.2s, color 0.1s;
49
+
50
+ &:hover {
51
+ background-color: var(--c-bg-soft);
52
+ color: var(--c-text);
53
+ }
54
+ }
55
+ }
56
+
57
+ p {
58
+ margin: 0.5em;
59
+ }
60
+ }
61
+ </style>
@@ -0,0 +1,146 @@
1
+ <script setup lang="ts">
2
+ withDefaults(defineProps<{
3
+ tag?: string
4
+ }>(), {
5
+ tag: 'div',
6
+ })
7
+ const clarity = useClarityConfig()
8
+ </script>
9
+
10
+ <template>
11
+ <UtilLink class="blog-header">
12
+ <div v-if="clarity.header.emojiTail" class="emoji-tail">
13
+ <span
14
+ v-for="(emoji, emojiIndex) in clarity.header.emojiTail"
15
+ :key="emojiIndex"
16
+ class="split-char"
17
+ :style="getFixedDelay(emojiIndex * .6 - 3)"
18
+ v-text="emoji"
19
+ />
20
+ </div>
21
+
22
+ <NuxtImg
23
+ :src="clarity.header.logo"
24
+ class="blog-logo round-cobblestone"
25
+ :class="{ circle: clarity.header.showTitle }"
26
+ :alt="clarity.site.title"
27
+ />
28
+
29
+ <div v-if="clarity.header.showTitle" class="blog-text">
30
+ <component :is="tag" class="header-title">
31
+ <span
32
+ v-for="(char, charIndex) in clarity.site.title"
33
+ :key="charIndex"
34
+ class="split-char"
35
+ :style="getFixedDelay((charIndex + 1) * .1)"
36
+ v-text="char"
37
+ />
38
+ </component>
39
+
40
+ <div class="header-subtitle">
41
+ {{ clarity.header.subtitle }}
42
+ </div>
43
+ </div>
44
+ </UtilLink>
45
+ </template>
46
+
47
+ <style lang="scss" scoped>
48
+ .blog-header {
49
+ contain: layout;
50
+ display: flex;
51
+ align-items: center;
52
+ gap: 0.5em;
53
+ position: relative;
54
+ margin: clamp(1rem, 2rem, 5vh) 1rem min(1rem, 5vh);
55
+ line-height: 1.4;
56
+ color: var(--c-text);
57
+ user-select: none;
58
+ }
59
+
60
+ .blog-logo {
61
+ height: 3em;
62
+
63
+ &.circle {
64
+ width: 3em;
65
+ border-radius: 50%;
66
+ box-shadow: var(--box-shadow-1), var(--box-shadow-3);
67
+ }
68
+ }
69
+
70
+ @font-face {
71
+ font-family: AlimamaFangYuanTi;
72
+ src: url("/fonts/AlimamaFangYuanTi.woff2");
73
+ }
74
+
75
+ .header-title {
76
+ font-family: AlimamaFangYuanTi, "Noto Sans SC", sans-serif;
77
+ font-size: 1.5em;
78
+ font-synthesis: none;
79
+ font-variation-settings: "wght" 600, "BEVL" 100;
80
+
81
+ > .split-char {
82
+ animation: 3.14s infinite alternate vf-weight, 2.72s infinite alternate vf-bevel;
83
+ animation-delay: var(--delay);
84
+ animation-play-state: paused;
85
+ }
86
+ }
87
+
88
+ .header-subtitle {
89
+ opacity: 0.5;
90
+ font-size: 0.8em;
91
+ }
92
+
93
+ @keyframes vf-weight {
94
+ 0% { font-weight: 600; }
95
+ 38.2% { font-weight: 300; }
96
+ 100% { font-weight: 900; }
97
+ }
98
+
99
+ @keyframes vf-bevel {
100
+ from { font-variation-settings: "BEVL" 100; }
101
+ to { font-variation-settings: "BEVL" 1; }
102
+ }
103
+
104
+ .emoji-tail {
105
+ display: grid;
106
+ grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
107
+ align-content: center;
108
+ justify-items: center;
109
+ position: absolute;
110
+ opacity: 0.2;
111
+ inset: 0;
112
+ font-size: 4rem;
113
+ transition: opacity 1s;
114
+ filter: blur(2px);
115
+ pointer-events: none;
116
+ z-index: -2;
117
+
118
+ > .split-char {
119
+ animation: 5s infinite alternate emoji-floating;
120
+ animation-delay: var(--delay);
121
+ animation-play-state: paused;
122
+ }
123
+ }
124
+
125
+ .blog-header:hover {
126
+ .emoji-tail {
127
+ opacity: 0.5;
128
+ }
129
+
130
+ .split-char {
131
+ animation-play-state: running;
132
+ }
133
+ }
134
+
135
+ @keyframes emoji-floating {
136
+ 50% {
137
+ transform: translate(-12px, -4px) scale(1.2);
138
+ filter: blur(4px);
139
+ }
140
+
141
+ 100% {
142
+ transform: translate(-4px, -12px) scale(0.9);
143
+ filter: blur(1px);
144
+ }
145
+ }
146
+ </style>
@@ -0,0 +1,79 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ hasAside?: boolean
4
+ }>()
5
+
6
+ const layoutStore = useLayoutStore()
7
+ const { avoidTargets } = storeToRefs(layoutStore)
8
+
9
+ const panelRef = useTemplateRef('blog-panel')
10
+ const { transform } = useAvoidTransform(panelRef, avoidTargets)
11
+ </script>
12
+
13
+ <template>
14
+ <div
15
+ id="blog-panel"
16
+ ref="blog-panel"
17
+ :class="{ 'has-active': layoutStore.state !== 'none' }"
18
+ :style="{ transform }"
19
+ >
20
+ <button
21
+ v-if="hasAside"
22
+ class="toggle-aside widescreen-only"
23
+ :class="{ active: layoutStore.state === 'aside' }"
24
+ aria-label="切换侧边栏"
25
+ @click="layoutStore.toggle('aside')"
26
+ >
27
+ <Icon class="rtl-flip" name="tabler:align-right" />
28
+ </button>
29
+
30
+ <Icon v-show="false" name="tabler:layout-sidebar-filled" />
31
+ <button
32
+ class="toggle-sidebar mobile-only"
33
+ :class="{ active: layoutStore.state === 'sidebar' }"
34
+ aria-label="切换菜单"
35
+ @click="layoutStore.toggle('sidebar')"
36
+ >
37
+ <Icon class="rtl-flip" :name="layoutStore.state === 'sidebar' ? 'tabler:layout-sidebar-filled' : 'tabler:layout-sidebar'" />
38
+ </button>
39
+ </div>
40
+ </template>
41
+
42
+ <style lang="scss" scoped>
43
+ #blog-panel {
44
+ contain: paint;
45
+ position: fixed;
46
+ inset-inline-end: min(1rem, 5%);
47
+ bottom: min(2rem, 5%);
48
+ border-radius: 0.5rem;
49
+ background-color: var(--c-bg-a50);
50
+ backdrop-filter: blur(0.5rem);
51
+ font-size: 1.4rem;
52
+ transition: transform 0.1s;
53
+ z-index: var(--z-index-popover);
54
+
55
+ @media (max-height: $breakpoint-phone) {
56
+ display: flex;
57
+ }
58
+
59
+ &.has-active {
60
+ box-shadow: var(--box-shadow-1), var(--box-shadow-3);
61
+ }
62
+ }
63
+
64
+ button {
65
+ display: block;
66
+ padding: 0.5rem;
67
+ transition: all 0.2s;
68
+
69
+ &:hover {
70
+ background-color: var(--c-bg-a80);
71
+ color: var(--c-primary);
72
+ }
73
+
74
+ &.active {
75
+ background-color: var(--ld-bg-active);
76
+ color: var(--c-primary);
77
+ }
78
+ }
79
+ </style>
@@ -0,0 +1,160 @@
1
+ <script setup lang="ts">
2
+ const clarity = useClarityConfig()
3
+ const layoutStore = useLayoutStore()
4
+ const searchStore = useSearchStore()
5
+
6
+ const { text } = useTextSelection()
7
+ const debouncedSelection = refDebounced(text)
8
+ </script>
9
+
10
+ <template>
11
+ <BlogMask
12
+ :show="layoutStore.state === 'sidebar'"
13
+ class="mobile-only"
14
+ @click="layoutStore.close()"
15
+ />
16
+
17
+ <!-- 不能用 Transition 实现弹出收起动画,因为半宽屏状态始终显示 -->
18
+ <aside id="blog-sidebar" :class="{ show: layoutStore.state === 'sidebar' }">
19
+ <BlogHeader class="sidebar-header" to="/" />
20
+
21
+ <nav class="sidebar-nav scrollcheck-y">
22
+ <div class="search-btn sidebar-nav-item gradient-card" @click="layoutStore.toggle('search')">
23
+ <Icon name="tabler:search" />
24
+ <span class="nav-text">{{ debouncedSelection || searchStore.word || '搜索' }}</span>
25
+ <Key class="keycut" code="K" cmd prevent @press="layoutStore.toggle('search')" />
26
+ </div>
27
+
28
+ <template v-for="(group, groupIndex) in clarity.nav" :key="groupIndex">
29
+ <h3 v-if="group.title">
30
+ {{ group.title }}
31
+ </h3>
32
+
33
+ <menu>
34
+ <li v-for="(item, itemIndex) in group.items" :key="itemIndex">
35
+ <UtilLink :to="item.url" class="sidebar-nav-item">
36
+ <Icon :name="item.icon" />
37
+ <span class="nav-text">{{ item.text }}</span>
38
+ <Icon v-if="isExtLink(item.url)" class="external-tip" name="tabler:arrow-up-right" />
39
+ </UtilLink>
40
+ </li>
41
+ </menu>
42
+ </template>
43
+ </nav>
44
+
45
+ <footer class="sidebar-footer">
46
+ <BlogThemeToggle />
47
+ <ZIconNavList :list="clarity.footer.iconNav" />
48
+ </footer>
49
+ </aside>
50
+ </template>
51
+
52
+ <style lang="scss" scoped>
53
+ #blog-sidebar {
54
+ display: flex;
55
+ flex-direction: column;
56
+ color: var(--c-text-2);
57
+
58
+ &:hover {
59
+ color: currentcolor;
60
+ }
61
+
62
+ @media (max-width: $breakpoint-mobile) {
63
+ position: fixed;
64
+ inset-inline-start: 0;
65
+ width: 320px;
66
+ max-width: 100%;
67
+ background-color: var(--ld-bg-blur);
68
+ backdrop-filter: blur(0.5rem);
69
+ color: currentcolor;
70
+ transform: var(--transform-start-far);
71
+ transition: transform 0.2s;
72
+ z-index: var(--z-index-popover);
73
+
74
+ &.show {
75
+ box-shadow: var(--box-shadow-1), var(--box-shadow-3);
76
+ transform: none;
77
+ }
78
+ }
79
+ }
80
+
81
+ .sidebar-nav {
82
+ flex-grow: 1;
83
+ padding: 0 5%;
84
+ font-size: 0.9em;
85
+
86
+ h3 {
87
+ margin: 2em 0 1em 1em;
88
+ font: inherit;
89
+ color: var(--c-text-2);
90
+ }
91
+
92
+ li {
93
+ margin: 0.5em 0;
94
+ }
95
+ }
96
+
97
+ .sidebar-nav-item {
98
+ display: flex;
99
+ align-items: center;
100
+ gap: 0.5em;
101
+ padding: 0.5em 1em;
102
+ border-radius: 0.5em;
103
+ transition: all 0.2s;
104
+
105
+ &:hover,
106
+ &.router-link-active {
107
+ background-color: var(--c-bg-soft);
108
+ color: var(--c-text);
109
+ }
110
+
111
+ &.router-link-active::after {
112
+ content: "⦁";
113
+ width: 1em;
114
+ text-align: center;
115
+ color: var(--c-text-3);
116
+ }
117
+
118
+ > .iconify {
119
+ font-size: 1.5em;
120
+ }
121
+
122
+ > .nav-text {
123
+ flex-grow: 1;
124
+ overflow: hidden;
125
+ white-space: nowrap;
126
+ text-overflow: ellipsis;
127
+ }
128
+
129
+ > .external-tip {
130
+ opacity: 0.5;
131
+ font-size: 1em;
132
+ }
133
+ }
134
+
135
+ .search-btn {
136
+ opacity: 0.5;
137
+ margin: 1rem 0;
138
+ outline: 2px solid var(--c-border);
139
+ outline-offset: -2px;
140
+ cursor: text;
141
+ user-select: none;
142
+
143
+ &:hover {
144
+ opacity: 1;
145
+ outline-color: transparent;
146
+ background-color: transparent;
147
+ }
148
+ }
149
+
150
+ .sidebar-footer {
151
+ --gap: clamp(0.5rem, 3vh, 1rem);
152
+
153
+ display: grid;
154
+ gap: var(--gap);
155
+ padding: var(--gap);
156
+ font-size: 0.8em;
157
+ text-align: center;
158
+ color: var(--c-text-2);
159
+ }
160
+ </style>