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,74 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ link: string
4
+ title: string
5
+ description?: string
6
+ icon?: string
7
+ mirror?: ImgService
8
+ }>()
9
+ </script>
10
+
11
+ <template>
12
+ <UtilLink :to="link" class="link-card card" :title="joinWith([title, description, link])">
13
+ <div class="link-card-info">
14
+ <div class="link-card-title">
15
+ {{ title }}
16
+ </div>
17
+ <div class="link-card-description">
18
+ {{ description ?? getDomain(link) }}
19
+ </div>
20
+ </div>
21
+ <slot name="icon" class="link-card-icon-slot">
22
+ <UtilImg v-if="icon" class="link-card-icon" :src="icon" :mirror />
23
+ </slot>
24
+ </UtilLink>
25
+ </template>
26
+
27
+ <style lang="scss" scoped>
28
+ .link-card {
29
+ display: flex;
30
+ align-items: center;
31
+ gap: 0.5rem;
32
+ padding: 0.5em 0.8em;
33
+ font-size: 0.9em;
34
+ line-height: 1.4;
35
+
36
+ article & {
37
+ width: 20rem;
38
+ max-width: 90%;
39
+ margin: 2rem auto;
40
+ }
41
+
42
+ // 溢出显示省略号
43
+ .link-card-info {
44
+ flex-grow: 1;
45
+ overflow: hidden;
46
+ }
47
+
48
+ .link-card-title {
49
+ display: -webkit-box;
50
+ overflow: hidden;
51
+ -webkit-box-orient: vertical;
52
+ -webkit-line-clamp: 2;
53
+ line-clamp: 2;
54
+ }
55
+
56
+ // 内部需要是块元素
57
+ .link-card-description {
58
+ overflow: hidden;
59
+ opacity: 0.5;
60
+ margin-top: 0.2em;
61
+ font-size: 0.9em;
62
+ white-space: nowrap;
63
+ text-overflow: ellipsis;
64
+ }
65
+
66
+ .link-card-icon {
67
+ flex-shrink: 0;
68
+ height: 3rem;
69
+ max-width: 5rem;
70
+ border-radius: 0.5rem;
71
+ object-fit: cover;
72
+ }
73
+ }
74
+ </style>
@@ -0,0 +1,26 @@
1
+ <template>
2
+ <div class="md-title">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <style lang="scss" scoped>
8
+ .md-title {
9
+ margin: 0.5em 0;
10
+ font-size: 1.1em;
11
+ font-weight: bold;
12
+
13
+ &::before {
14
+ content: "#";
15
+ position: absolute;
16
+ opacity: 0;
17
+ font-size: 0.8em;
18
+ transform: translate(-0.9em, 0.3em);
19
+ transition: opacity 0.2s;
20
+ }
21
+
22
+ &:hover::before {
23
+ opacity: 0.5;
24
+ }
25
+ }
26
+ </style>
@@ -0,0 +1,133 @@
1
+ <script setup lang="ts">
2
+ const props = defineProps<{
3
+ code: string
4
+ }>()
5
+
6
+ const colorMode = useColorMode()
7
+ const container = useTemplateRef('mermaid')
8
+ const [scroll, toggleScroll] = useToggle(true)
9
+
10
+ const id = useId()
11
+ // mermaid 会移除 DOM 中同 id 的旧图,故每次渲染另起 id,避免切换主题时高度塌陷
12
+ let renderCount = 0
13
+
14
+ // mermaid 体积较大,接近视口时才动态引入;隐藏容器(如 Tab)内不渲染,避免量取到错误的尺寸
15
+ const isVisible = useElementVisibility(container, { rootMargin: '50%' })
16
+ // 可见后即锁定,避免滚出视口时图表被清空
17
+ const shouldRender = ref(false)
18
+ whenever(isVisible, () => shouldRender.value = true, { once: true })
19
+
20
+ const diagram = computedAsync<{ svg?: string, width?: number, error?: string }>(async () => {
21
+ // 异步依赖需在 await 之前读取
22
+ const { code } = props
23
+ const darkMode = colorMode.value === 'dark'
24
+ if (!shouldRender.value)
25
+ return {}
26
+
27
+ try {
28
+ const { default: mermaid } = await import('mermaid')
29
+ // 等待 color-mode 换好根元素类名,以及字体就绪——否则取到旧配色、量出偏窄的文本
30
+ await Promise.all([nextTick(), document.fonts.ready])
31
+
32
+ const style = getComputedStyle(document.documentElement)
33
+ const cssVar = (name: string) => style.getPropertyValue(name)
34
+
35
+ mermaid.initialize({
36
+ fontFamily: 'inherit',
37
+ // 须在 load 事件前关闭,否则 mermaid 会自行扫描并接管页面元素
38
+ startOnLoad: false,
39
+ suppressErrorRendering: true,
40
+ // themeVariables 仅对 base 主题生效,其余主题会重算这些颜色
41
+ theme: 'base',
42
+ themeVariables: {
43
+ darkMode,
44
+ // 不指定则深色下连线被推导为近黑色、边标签被推导为绿色
45
+ background: cssVar('--c-bg'),
46
+ edgeLabelBackground: cssVar('--c-bg-2'),
47
+ // 仅把自带主题的紫色换成博客主题色,其余配色仍由 mermaid 推导
48
+ primaryBorderColor: cssVar('--c-primary'),
49
+ primaryColor: cssVar('--c-primary-soft'),
50
+ primaryTextColor: cssVar('--c-text-1'),
51
+ textColor: cssVar('--c-text-1'),
52
+ },
53
+ })
54
+ const { svg } = await mermaid.render(`${id}-${renderCount++}`, code)
55
+ // 用原始画布宽度保留字号,超宽图表在容器内滚动
56
+ const width = new DOMParser().parseFromString(svg, 'image/svg+xml').querySelector('svg')?.viewBox.baseVal.width
57
+ return { svg, width }
58
+ }
59
+ catch (error) {
60
+ return { error: error instanceof Error ? error.message : String(error) }
61
+ }
62
+ }, {})
63
+ </script>
64
+
65
+ <template>
66
+ <div ref="mermaid" class="mermaid-diagram">
67
+ <Tooltip
68
+ v-if="diagram.svg"
69
+ tag="div"
70
+ interactive
71
+ trigger="mouseenter focusin"
72
+ :hide-on-click="false"
73
+ :delay="500"
74
+ >
75
+ <template #content>
76
+ <Icon v-show="false" :name="scroll ? 'tabler:arrows-horizontal' : 'tabler:arrows-minimize'" />
77
+ <ZButton
78
+ variant="text"
79
+ :icon="scroll ? 'tabler:arrows-minimize' : 'tabler:arrows-horizontal'"
80
+ :text="scroll ? '适应宽度' : '横向滚动'"
81
+ @click="toggleScroll()"
82
+ />
83
+ </template>
84
+ <div class="scrollcheck-x" tabindex="0" role="region" aria-label="Mermaid 图表">
85
+ <div :style="{ minWidth: scroll && diagram.width ? `${diagram.width}px` : undefined }" v-html="diagram.svg" />
86
+ </div>
87
+ </Tooltip>
88
+ <template v-else-if="diagram.error">
89
+ <details class="mermaid-error">
90
+ <summary>图表渲染失败,查看错误详情</summary>
91
+ <pre>{{ diagram.error }}</pre>
92
+ </details>
93
+ <ProsePre :code language="mermaid" meta="wrap" />
94
+ </template>
95
+ </div>
96
+ </template>
97
+
98
+ <style lang="scss" scoped>
99
+ // 不可命名为 .mermaid:mermaid 会按此类名自动扫描并接管元素
100
+ .mermaid-diagram {
101
+ margin: 0.5em 0;
102
+
103
+ // mermaid 在 <body> 下量取文本,此处需与根元素排版一致,否则图形错位
104
+ line-height: 1.4;
105
+
106
+ :deep(svg) {
107
+ display: block;
108
+ height: auto;
109
+ max-width: 100%;
110
+ margin-inline: auto;
111
+ }
112
+
113
+ // 文本标签由 foreignObject 承载,会继承文章的段落样式
114
+ :deep(p) {
115
+ margin: 0;
116
+ }
117
+ }
118
+
119
+ .mermaid-error {
120
+ font-size: 0.85em;
121
+ color: var(--c-text-2);
122
+
123
+ summary {
124
+ color: var(--c-error);
125
+ cursor: pointer;
126
+ }
127
+
128
+ pre {
129
+ overflow-wrap: anywhere;
130
+ white-space: pre-wrap;
131
+ }
132
+ }
133
+ </style>
@@ -0,0 +1,178 @@
1
+ <script setup lang="ts">
2
+ import type { AbcVisualParams, SynthObjectController, SynthVisualOptions, TuneObject } from 'abcjs'
3
+
4
+ const props = defineProps<{
5
+ abc: string
6
+ }>()
7
+
8
+ const container = useTemplateRef('abcjs-container')
9
+ const synthController = useTemplateRef('synth-controller')
10
+
11
+ const tuneObj = ref<TuneObject>()
12
+ const synthObjController = ref<SynthObjectController>()
13
+
14
+ const abcVisualParams: AbcVisualParams = {
15
+ responsive: 'resize',
16
+ }
17
+
18
+ const synthVisualOptions: SynthVisualOptions = {
19
+ displayLoop: true,
20
+ displayRestart: true,
21
+ displayPlay: true,
22
+ displayProgress: true,
23
+ displayWarp: true,
24
+ }
25
+
26
+ async function checkSoundFonts() {
27
+ try {
28
+ const res = await fetch('https://paulrosen.github.io/midi-js-soundfonts/', { method: 'HEAD' })
29
+ return res.ok
30
+ }
31
+ catch (e) {
32
+ console.error('[music-abc] 无法连接 SoundFonts,不启用播放能力\n', e)
33
+ return false
34
+ }
35
+ }
36
+
37
+ onMounted(async () => {
38
+ const { renderAbc, synth } = await import('abcjs')
39
+
40
+ tuneObj.value = renderAbc(container.value!, props.abc, abcVisualParams)[0]
41
+
42
+ if (!synth.supportsAudio() || !(await checkSoundFonts()))
43
+ return
44
+
45
+ synthObjController.value = new synth.SynthController()
46
+ synthObjController.value.load(synthController.value!, null, synthVisualOptions)
47
+ synthObjController.value.setTune(tuneObj.value, false)
48
+ })
49
+
50
+ onUnmounted(() => {
51
+ synthObjController.value?.pause()
52
+ })
53
+ </script>
54
+
55
+ <template>
56
+ <div class="music-score">
57
+ <div ref="abcjs-container" />
58
+ <div ref="synth-controller" />
59
+ </div>
60
+ </template>
61
+
62
+ <style lang="scss" scoped>
63
+ .music-score {
64
+ line-height: 1.4;
65
+
66
+ :deep(.abcjs-inline-audio) {
67
+ display: flex;
68
+ align-items: center;
69
+ gap: 0.5em;
70
+ padding: 0.2em 0.5em;
71
+ border-radius: 4px;
72
+ background-color: var(--c-bg-2);
73
+ font-size: 0.8em;
74
+ font-variant-numeric: tabular-nums;
75
+
76
+ &.abcjs-disabled {
77
+ opacity: 0.5;
78
+ }
79
+
80
+ > .abcjs-btn {
81
+ width: 1em;
82
+ padding: 0.2em;
83
+ box-sizing: content-box;
84
+ background: none;
85
+ line-height: 1;
86
+
87
+ > svg {
88
+ display: block;
89
+ }
90
+
91
+ &:hover, &.abcjs-pushed {
92
+ color: var(--c-primary);
93
+ }
94
+
95
+ g {
96
+ fill: currentcolor;
97
+ stroke: currentcolor;
98
+ }
99
+ }
100
+
101
+ > .abcjs-midi-start {
102
+ > .abcjs-pause-svg, > .abcjs-loading-svg {
103
+ display: none;
104
+ }
105
+
106
+ &.abcjs-pushed, &.abcjs-loading {
107
+ > .abcjs-play-svg {
108
+ display: none;
109
+ }
110
+ }
111
+
112
+ &.abcjs-pushed .abcjs-pause-svg {
113
+ display: block;
114
+ }
115
+ }
116
+
117
+ > .abcjs-midi-progress-background, .abcjs-midi-progress-indicator {
118
+ border: 8px solid transparent;
119
+ border-radius: 12px;
120
+ box-sizing: content-box;
121
+ background-clip: padding-box;
122
+ }
123
+
124
+ > .abcjs-midi-progress-background {
125
+ flex-grow: 1;
126
+ position: relative;
127
+ height: 4px;
128
+ background-color: var(--c-bg-soft);
129
+ }
130
+
131
+ .abcjs-midi-progress-indicator {
132
+ position: absolute;
133
+ top: -10px;
134
+ width: 8px;
135
+ height: 8px;
136
+ margin-left: -10px;
137
+ background-color: var(--c-text-1);
138
+ }
139
+
140
+ .abcjs-midi-clock {
141
+ display: inline-block;
142
+ }
143
+
144
+ .abcjs-tempo-wrapper {
145
+ display: flex;
146
+ align-items: center;
147
+ }
148
+
149
+ input.abcjs-midi-tempo {
150
+ position: relative;
151
+ width: 3em;
152
+ text-align: end;
153
+ }
154
+
155
+ .abcjs-loading .abcjs-loading-svg {
156
+ display: inherit;
157
+ }
158
+
159
+ .abcjs-loading {
160
+ @keyframes abcjs-spin {
161
+ from { transform: rotate(0deg); }
162
+ to { transform: rotate(360deg); }
163
+ }
164
+
165
+ outline: none;
166
+ animation: abcjs-spin 1s linear infinite;
167
+ }
168
+
169
+ .abcjs-loading-svg circle {
170
+ stroke: var(--c-text-1);
171
+ }
172
+
173
+ .abcjs-css-warning {
174
+ display: none;
175
+ }
176
+ }
177
+ }
178
+ </style>
@@ -0,0 +1,55 @@
1
+ <script setup lang="ts">
2
+ import type { UtilImgProps } from '../util/Img.vue'
3
+ import { LazyPopoverLightbox } from '#components'
4
+
5
+ const props = withDefaults(defineProps<UtilImgProps & {
6
+ caption?: string
7
+ zoom?: boolean
8
+ }>(), {
9
+ caption: '',
10
+ zoom: true,
11
+ })
12
+
13
+ const slots = defineSlots<{
14
+ caption: () => any
15
+ }>()
16
+
17
+ const pic = useTemplateRef<HTMLImageElement>('pic')
18
+ const modalStore = useModalStore()
19
+
20
+ const { open } = modalStore.use(
21
+ () => h(LazyPopoverLightbox, {
22
+ el: unrefElement(pic)!,
23
+ caption: props.alt || props.caption || slots.caption,
24
+ }),
25
+ { unique: true },
26
+ )
27
+ </script>
28
+
29
+ <template>
30
+ <!-- <ProseImg> 被 <p> 包裹,服务端渲染时若内含块级元素会自动关闭,导致水合不匹配 -->
31
+ <figure class="image">
32
+ <UtilImg
33
+ ref="pic"
34
+ class="image"
35
+ :style="{ cursor: zoom && 'zoom-in' }"
36
+ :alt="caption || alt"
37
+ :src :width :height :mirror :filter :densities
38
+ @click="zoom && open()"
39
+ />
40
+ <figcaption v-if="caption || $slots.caption" aria-hidden>
41
+ <slot name="caption">
42
+ {{ caption }}
43
+ </slot>
44
+ </figcaption>
45
+ </figure>
46
+ </template>
47
+
48
+ <style lang="scss" scoped>
49
+ figcaption {
50
+ margin-top: -0.5em;
51
+ font-size: 0.8em;
52
+ text-align: center;
53
+ color: var(--c-text-2);
54
+ }
55
+ </style>
@@ -0,0 +1,41 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ title?: string
4
+ author?: string
5
+ footer?: string
6
+ }>()
7
+ </script>
8
+
9
+ <template>
10
+ <section class="poetry">
11
+ <h2 class="poetry-title text-center">
12
+ {{ title }}
13
+ </h2>
14
+ <div class="text-center">
15
+ {{ author }}
16
+ </div>
17
+ <div class="poetry-content">
18
+ <slot />
19
+ </div>
20
+ <div class="text-center">
21
+ {{ footer }}
22
+ </div>
23
+ </section>
24
+ </template>
25
+
26
+ <style lang="scss" scoped>
27
+ .poetry {
28
+ width: fit-content;
29
+ margin: 2em auto;
30
+ white-space: pre-wrap;
31
+
32
+ .poetry-title {
33
+ font-size: 1.2em;
34
+ }
35
+
36
+ :deep(p) {
37
+ width: fit-content;
38
+ margin: 0.5em auto;
39
+ }
40
+ }
41
+ </style>
@@ -0,0 +1,40 @@
1
+ <script setup lang="ts">
2
+ const props = withDefaults(defineProps<{
3
+ href: string
4
+ icon?: string | false
5
+ }>(), {
6
+ icon: undefined,
7
+ })
8
+
9
+ const icon = computed(() => props.icon ?? getDomainIcon(props.href))
10
+ const tip = computed(() => ({
11
+ content: isExtLink(props.href) ? getDomain(props.href) : safelyDecodeUriComponent(props.href),
12
+ inlinePositioning: true,
13
+ }))
14
+ </script>
15
+
16
+ <template>
17
+ <UtilLink v-tip="tip" class="z-link" :to="href">
18
+ <Icon v-if="icon" class="domain-icon" :name="icon" />
19
+ <slot />
20
+ </UtilLink>
21
+ </template>
22
+
23
+ <style lang="scss" scoped>
24
+ .z-link[href] {
25
+ margin: -0.1em -0.2em;
26
+ padding: 0.1em 0.2em;
27
+ background: linear-gradient(var(--c-primary-soft), var(--c-primary-soft)) no-repeat center bottom / 100% 0.1em;
28
+ color: var(--c-primary);
29
+ transition: all 0.2s;
30
+
31
+ &:hover {
32
+ border-radius: 0.3em;
33
+ background-size: 100% 100%;
34
+ }
35
+
36
+ .domain-icon {
37
+ margin-inline-end: 0.1em;
38
+ }
39
+ }
40
+ </style>
@@ -0,0 +1,95 @@
1
+ <script setup lang="ts">
2
+ import type { VNode } from 'vue'
3
+
4
+ const props = defineProps<{
5
+ language?: string
6
+ /**
7
+ * 代码原文。
8
+ * MDC 原生行为中 inline code 通过默认插槽(text VNode)传递,
9
+ * 上游 @nuxtjs/mdc patch(inline code props.code 传入原文)存在时走此 prop,
10
+ * fenced code(ProsePre 内部)也始终通过此 prop 传递。
11
+ */
12
+ code?: string
13
+ copy?: boolean
14
+ }>()
15
+
16
+ const slots = useSlots()
17
+
18
+ /** 从插槽 VNode 中递归提取纯文本 */
19
+ function extractText(vnode: VNode): string {
20
+ if (typeof vnode.children === 'string')
21
+ return vnode.children
22
+ if (Array.isArray(vnode.children))
23
+ return (vnode.children as VNode[]).map(extractText).join('')
24
+ return ''
25
+ }
26
+
27
+ /** 无 patch 时以插槽文本作为原文,有 patch 时优先使用 code prop */
28
+ const rawCode = computed(() => {
29
+ if (props.code !== undefined)
30
+ return props.code
31
+ return (slots.default?.() ?? []).map(extractText).join('')
32
+ })
33
+
34
+ const { copy: copyCode, copied } = useCopy(rawCode.value)
35
+ const shiki = useShiki()
36
+ const codeElement = useTemplateRef('code')
37
+ const highlighted = ref(false)
38
+
39
+ onMounted(async () => {
40
+ if (!props.language)
41
+ return
42
+ await shiki.mountInline(codeElement.value!, rawCode.value, {
43
+ language: props.language,
44
+ transformerOptions: ['ignoreColorizedBrackets'],
45
+ })
46
+ highlighted.value = true
47
+ })
48
+ </script>
49
+
50
+ <template>
51
+ <code ref="code" :class="{ copyable: copy }">
52
+ <template v-if="!language || !highlighted">
53
+ <slot>{{ rawCode }}</slot>
54
+ </template>
55
+ <Icon v-if="copy" v-show="false" name="tabler:check" />
56
+ <button v-if="copy" type="button" class="copy-button" aria-label="复制" @click="copyCode()">
57
+ <Icon :name="copied ? 'tabler:check' : 'tabler:copy'" />
58
+ </button>
59
+ </code>
60
+ </template>
61
+
62
+ <style lang="scss" scoped>
63
+ code {
64
+ margin: 0.1em;
65
+ padding: 0.1rem 0.3em;
66
+ border: 1px solid var(--c-border);
67
+ border-radius: 4px;
68
+ background-color: var(--c-bg-2);
69
+ font-size: 0.85em;
70
+ white-space: break-spaces;
71
+
72
+ &.copyable {
73
+ padding-inline-end: 0.1em;
74
+ }
75
+
76
+ @supports (color: color-mix(in srgb, transparent, transparent)) {
77
+ border-color: color-mix(in srgb, currentcolor 10%, transparent);
78
+ background-color: color-mix(in srgb, currentcolor 5%, transparent);
79
+ }
80
+ }
81
+
82
+ .copy-button {
83
+ display: inline-flex;
84
+ margin-inline-start: 0.2em;
85
+ vertical-align: -0.2em;
86
+ color: var(--c-text-3);
87
+ transition: color 0.2s;
88
+
89
+ &:hover,
90
+ &:focus-visible {
91
+ opacity: 1;
92
+ color: var(--c-primary);
93
+ }
94
+ }
95
+ </style>