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,134 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ title?: string
4
+ card?: boolean
5
+ shrink?: boolean
6
+ grayscale?: boolean
7
+ dim?: boolean
8
+ bgImg?: string
9
+ bgAside?: boolean
10
+ }>()
11
+
12
+ const body = useTemplateRef('widget-body')
13
+
14
+ defineExpose({ body })
15
+ </script>
16
+
17
+ <template>
18
+ <section
19
+ class="blog-widget"
20
+ :class="{ shrink, grayscale, dim }"
21
+ >
22
+ <hgroup class="widget-header text-creative">
23
+ <slot name="title">
24
+ {{ title }}
25
+ </slot>
26
+ <span v-if="$slots.action" class="seperator" />
27
+ <slot name="action" />
28
+ </hgroup>
29
+
30
+ <div
31
+ ref="widget-body"
32
+ class="widget-body"
33
+ :class="{ 'widget-card': card, 'with-bg': bgImg, 'scrollcheck-y scrollbar-hidden': shrink }"
34
+ >
35
+ <NuxtImg v-if="bgImg" class="bg-img" :class="{ 'bg-right': bgAside }" :src="bgImg" alt="" />
36
+ <slot />
37
+ </div>
38
+ </section>
39
+ </template>
40
+
41
+ <style lang="scss" scoped>
42
+ .blog-widget {
43
+ flex-shrink: 1;
44
+ font-size: 0.9em;
45
+
46
+ &.shrink {
47
+ display: flex;
48
+ flex-direction: column;
49
+ overflow: auto;
50
+ }
51
+
52
+ &.grayscale :where(.iconify, img) {
53
+ transition: filter 0.2s;
54
+ filter: grayscale(0.8);
55
+
56
+ #blog-aside:hover &,
57
+ &:focus-within,
58
+ #blog-aside.show & {
59
+ filter: grayscale(0);
60
+ }
61
+ }
62
+
63
+ &.dim {
64
+ opacity: 0.3;
65
+ transition: opacity 0.2s;
66
+
67
+ #blog-aside:hover &,
68
+ &:focus-within,
69
+ #blog-aside.show & {
70
+ opacity: 1;
71
+ }
72
+ }
73
+ }
74
+
75
+ .widget-header {
76
+ display: flex;
77
+ align-items: center;
78
+ gap: 0.5rem;
79
+ padding: 0.5rem;
80
+ color: var(--c-text-2);
81
+
82
+ &:empty {
83
+ display: none;
84
+ }
85
+
86
+ > .seperator {
87
+ flex-grow: 1;
88
+ }
89
+
90
+ > :deep(a) {
91
+ transition: color 0.2s;
92
+
93
+ &[href]:hover {
94
+ color: var(--c-primary);
95
+ }
96
+ }
97
+ }
98
+
99
+ .widget-body {
100
+ overscroll-behavior: contain;
101
+
102
+ &.with-bg {
103
+ contain: paint; // overflow hidden + position relative
104
+ z-index: 0;
105
+
106
+ > .bg-img {
107
+ position: absolute;
108
+ opacity: 0.2;
109
+ inset: 0;
110
+ width: 100%;
111
+ height: 100%;
112
+ object-fit: cover;
113
+ pointer-events: none;
114
+ z-index: -1;
115
+
116
+ &.bg-right {
117
+ inset-inline-start: 50%;
118
+ width: 50%;
119
+ mask-image: linear-gradient(to var(--end), transparent, #FFF 50%);
120
+ }
121
+ }
122
+ }
123
+
124
+ &.widget-card {
125
+ padding: 0.5rem 0.8rem;
126
+ border-radius: 0.8rem;
127
+ background-color: var(--c-bg-2);
128
+
129
+ :deep(p) {
130
+ padding: 0.2em 0;
131
+ }
132
+ }
133
+ }
134
+ </style>
@@ -0,0 +1,42 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ blur?: boolean | string
4
+ zIndex?: number
5
+ }>()
6
+
7
+ const show = defineModel<boolean>('show')
8
+ </script>
9
+
10
+ <template>
11
+ <Transition>
12
+ <div
13
+ v-if="show"
14
+ class="bg-mask"
15
+ :style="{
16
+ '--z-index-popover': zIndex,
17
+ '--bdfilter': blur ? `blur(${blur === true ? '4px' : blur})` : 'none',
18
+ }"
19
+ @click="show = false"
20
+ />
21
+ </Transition>
22
+ </template>
23
+
24
+ <style lang="scss" scoped>
25
+ .bg-mask {
26
+ position: fixed;
27
+ inset: 0;
28
+ background-color: #0003;
29
+ backdrop-filter: var(--bdfilter);
30
+ z-index: var(--z-index-popover);
31
+ }
32
+
33
+ .v-enter-from,
34
+ .v-leave-to {
35
+ opacity: 0;
36
+ }
37
+
38
+ .v-enter-active,
39
+ .v-leave-active {
40
+ transition: all var(--delay);
41
+ }
42
+ </style>
@@ -0,0 +1,22 @@
1
+ <template>
2
+ <!-- 若使用 NuxtLink 则键盘焦点不会切换 -->
3
+ <a href="#main-content" class="skip-link gradient-card active">
4
+ 跳转到主要内容 / Skip to content
5
+ </a>
6
+ </template>
7
+
8
+ <style lang="scss" scoped>
9
+ .skip-link {
10
+ position: fixed;
11
+ top: -100%;
12
+ width: 80%;
13
+ margin: 1rem;
14
+ padding: 0.5rem;
15
+ text-align: center;
16
+ z-index: calc(infinity);
17
+ }
18
+
19
+ .skip-link:focus {
20
+ top: auto;
21
+ }
22
+ </style>
@@ -0,0 +1,50 @@
1
+ <script setup lang="ts">
2
+ const clarity = useClarityConfig()
3
+ const colorMode = useColorMode()
4
+ </script>
5
+
6
+ <template>
7
+ <div class="theme-toggle">
8
+ <button
9
+ v-for="(themeData, themeName) in clarity.themes"
10
+ :key="themeName"
11
+ v-tip="themeData.tip"
12
+ :aria-label="themeData.tip"
13
+ :class="{ active: colorMode.preference === themeName }"
14
+ @click="colorMode.preference = themeName"
15
+ >
16
+ <Icon :name="themeData.icon" />
17
+ </button>
18
+ </div>
19
+ </template>
20
+
21
+ <style lang="scss" scoped>
22
+ .theme-toggle {
23
+ display: flex;
24
+ gap: 3px;
25
+ width: fit-content;
26
+ margin: 0 auto;
27
+ padding: 2px;
28
+ border: 1px solid var(--c-border);
29
+ border-radius: 1rem;
30
+ background-color: var(--c-bg-2);
31
+
32
+ > button {
33
+ padding: 4px 1rem;
34
+ border-radius: 1rem;
35
+ transition: all 0.1s;
36
+
37
+ &:hover {
38
+ background-color: var(--c-bg-soft);
39
+ color: var(--c-text-1);
40
+ }
41
+
42
+ &.active {
43
+ box-shadow: var(--box-shadow-2);
44
+ background-color: var(--ld-bg-card);
45
+ color: var(--c-text-1);
46
+ cursor: auto;
47
+ }
48
+ }
49
+ }
50
+ </style>
@@ -0,0 +1,96 @@
1
+ <script setup lang="ts">
2
+ const props = withDefaults(defineProps<{
3
+ type?: keyof typeof typeMap
4
+ card?: boolean
5
+ flat?: boolean
6
+ icon?: string
7
+ color?: string
8
+ title?: string
9
+ text?: string
10
+ }>(), {
11
+ type: 'tip',
12
+ })
13
+
14
+ const clarity = useClarityConfig()
15
+ const card = computed(() => clarity.component.alert.defaultStyle === 'flat' ? props.card : !props.flat)
16
+
17
+ const typeMap = {
18
+ tip: {
19
+ icon: 'tabler:note',
20
+ color: '#3A7',
21
+ title: '提醒',
22
+ },
23
+ info: {
24
+ icon: 'tabler:info-circle',
25
+ // 使用 currentColor 会导致 --c-primary-soft 颜色混合错误
26
+ color: 'var(--c-text-1)',
27
+ title: '信息',
28
+ },
29
+ question: {
30
+ icon: 'tabler:help-circle',
31
+ color: '#3AF',
32
+ title: '问题',
33
+ },
34
+ warning: {
35
+ icon: 'tabler:alert-triangle',
36
+ color: '#F80',
37
+ title: '警告',
38
+ },
39
+ error: {
40
+ icon: 'tabler:circle-x',
41
+ color: '#F33',
42
+ title: '错误',
43
+ },
44
+ }
45
+
46
+ const icon = computed(() => props.icon || typeMap[props.type].icon)
47
+ const color = computed(() => props.color || typeMap[props.type].color)
48
+ const title = computed(() => props.title || typeMap[props.type].title)
49
+ </script>
50
+
51
+ <template>
52
+ <div class="alert" :class="{ card }" :style="{ '--c-primary': color }">
53
+ <div class="alert-title">
54
+ <Icon :name="icon" />
55
+ <slot name="title">
56
+ {{ title }}
57
+ </slot>
58
+ </div>
59
+ <slot><p>{{ text }}</p></slot>
60
+ </div>
61
+ </template>
62
+
63
+ <style lang="scss" scoped>
64
+ .alert {
65
+ margin: 1em 0;
66
+ padding: 0.2em 0.8em;
67
+ border-radius: 0.5em;
68
+ background-color: var(--c-bg-2);
69
+ font-size: 0.9em;
70
+
71
+ @supports (color: color-mix(in srgb, transparent, transparent)) {
72
+ --c-primary-soft: color-mix(in srgb, var(--c-primary) 15%, transparent);
73
+ --c-bg-2: color-mix(in srgb, var(--c-primary) 8%, var(--ld-bg-card));
74
+ }
75
+
76
+ &.card {
77
+ background-color: var(--ld-bg-card);
78
+ background-image:
79
+ radial-gradient(circle at 4em -25em, var(--c-primary), transparent 30em),
80
+ linear-gradient(var(--c-primary) -2000%, transparent);
81
+ }
82
+
83
+ .alert-title {
84
+ display: flex;
85
+ align-items: center;
86
+ gap: 0.5em;
87
+ margin: 0.5em 0;
88
+ font-weight: bold;
89
+ color: var(--c-primary);
90
+
91
+ :deep(p) {
92
+ margin: 0;
93
+ }
94
+ }
95
+ }
96
+ </style>
@@ -0,0 +1,90 @@
1
+ <script setup lang="ts">
2
+ // 即使 boolean 可选,其值也不会是 undefined
3
+ const props = defineProps<{
4
+ img?: string
5
+ text?: string
6
+ link?: string
7
+ round?: boolean
8
+ square?: boolean
9
+ }>()
10
+
11
+ const img = computed(() => {
12
+ if (props.img)
13
+ return props.img
14
+ const ghUsername = getGithubUsername(props.link)
15
+ if (ghUsername)
16
+ return getGithubAvatar(ghUsername)
17
+ if (props.link && isExtLink(props.link))
18
+ return getFavicon(getDomain(props.link))
19
+ return ''
20
+ })
21
+
22
+ // 有图时默认为圆形样式,除非指定为方形
23
+ // 无图时默认为方形样式,除非指定为圆形
24
+ const round = computed(() => img.value ? !props.square : props.round)
25
+
26
+ const tip = computed(() => {
27
+ if (!props.link)
28
+ return ''
29
+ if (isExtLink(props.link))
30
+ return getDomain(props.link)
31
+ return safelyDecodeUriComponent(props.link)
32
+ })
33
+ </script>
34
+
35
+ <template>
36
+ <UtilLink v-tip="tip" class="badge" :class="{ round }" :to="link">
37
+ <NuxtImg v-if="img" class="badge-icon" :src="img" alt="" densities="1x" />
38
+ <span class="badge-text">
39
+ <slot>{{ text }}</slot>
40
+ </span>
41
+ </UtilLink>
42
+ </template>
43
+
44
+ <style lang="scss" scoped>
45
+ // 对齐到基线,同时保持图片垂直居中
46
+ .badge {
47
+ display: inline-flex;
48
+ align-items: baseline;
49
+ margin: 0.1em;
50
+ border: 1px solid var(--c-border);
51
+ border-radius: 4px;
52
+ box-sizing: content-box;
53
+ background-color: var(--c-bg-2);
54
+ font-size: 0.875em;
55
+ transition: color 0.2s;
56
+
57
+ @supports (color: color-mix(in srgb, transparent, transparent)) {
58
+ border-color: color-mix(in srgb, currentcolor 10%, transparent);
59
+ background-color: color-mix(in srgb, currentcolor 5%, transparent);
60
+ color: color-mix(in srgb, currentcolor 80%, transparent);
61
+ }
62
+
63
+ &[href]:hover {
64
+ color: var(--c-text);
65
+ }
66
+
67
+ &.round, &.round > .badge-icon {
68
+ border-radius: 0.8em;
69
+ }
70
+ }
71
+
72
+ .badge-icon {
73
+ align-self: normal;
74
+ height: 1.6em;
75
+ border-radius: 3.5px;
76
+
77
+ + .badge-text {
78
+ margin-inline-start: -0.1em;
79
+ }
80
+ }
81
+
82
+ .badge-text {
83
+ padding: 0.2em 0.4em;
84
+ line-height: 1.2;
85
+
86
+ &:empty {
87
+ display: none;
88
+ }
89
+ }
90
+ </style>
@@ -0,0 +1,22 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ text?: string
4
+ }>()
5
+ </script>
6
+
7
+ <template>
8
+ <span class="blur">
9
+ <slot>{{ text }}</slot>
10
+ </span>
11
+ </template>
12
+
13
+ <style lang="scss" scoped>
14
+ .blur {
15
+ transition: filter 0.2s;
16
+ filter: blur(4px);
17
+
18
+ &:hover {
19
+ filter: blur(0);
20
+ }
21
+ }
22
+ </style>
@@ -0,0 +1,32 @@
1
+ <template>
2
+ <div class="card-list">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <style lang="scss" scoped>
8
+ :deep() {
9
+ :where(ol, ul):not([class]) {
10
+ margin: 0;
11
+ padding-inline-start: 0;
12
+ list-style: none;
13
+
14
+ > li {
15
+ margin: 0;
16
+ }
17
+ }
18
+
19
+ > ol, > ul {
20
+ display: grid;
21
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
22
+ gap: 0.5em;
23
+
24
+ > li {
25
+ padding: 0.5em 0.8em;
26
+ border-radius: 0.5em;
27
+ box-shadow: var(--box-shadow-2);
28
+ background-color: var(--ld-bg-card);
29
+ }
30
+ }
31
+ }
32
+ </style>
@@ -0,0 +1,79 @@
1
+ <script lang="tsx" setup>
2
+ const slots = defineSlots<{
3
+ default: () => VNode[]
4
+ }>()
5
+
6
+ const chatRegex = /^\{(?<control>\.|:)?(?<caption>.*)\}$/
7
+
8
+ function getControlClass(control?: string) {
9
+ if (control === '.')
10
+ return 'chat-myself'
11
+ if (control === ':')
12
+ return 'chat-system'
13
+ return ''
14
+ }
15
+
16
+ function render() {
17
+ const slotContent = slots.default()
18
+ if (!slotContent)
19
+ return <span>无会话内容</span>
20
+
21
+ return slotContent.map((node: VNode) => {
22
+ // WARN: 此处使用了非标准的 v-slot:default
23
+ const textContent = (node.children as any)?.default?.()[0].children
24
+ const matchGroups = typeof textContent === 'string'
25
+ ? textContent.match(chatRegex)?.groups
26
+ : undefined
27
+ return matchGroups
28
+ ? <dt class={`chat-caption ${getControlClass(matchGroups.control)}`}>{matchGroups.caption}</dt>
29
+ : <dd class="chat-body">{node}</dd>
30
+ })
31
+ }
32
+ </script>
33
+
34
+ <template>
35
+ <dl class="chat">
36
+ <render />
37
+ </dl>
38
+ </template>
39
+
40
+ <style lang="scss" scoped>
41
+ .chat {
42
+ margin-inline: 2vw;
43
+ font-size: 0.9em;
44
+ }
45
+
46
+ :deep() {
47
+ > .chat-caption {
48
+ opacity: 0.8;
49
+ font-size: 0.9em;
50
+ }
51
+
52
+ > .chat-body {
53
+ overflow: hidden; // BFC
54
+ width: fit-content;
55
+ max-width: 90%;
56
+ margin-bottom: 1em;
57
+ padding: 0 1em;
58
+ border-radius: 1em;
59
+ border-start-start-radius: 0.2em;
60
+ background-color: var(--c-bg-2);
61
+ }
62
+
63
+ > .chat-system {
64
+ margin-bottom: 1em;
65
+ text-align: center;
66
+ }
67
+
68
+ > .chat-myself {
69
+ text-align: end;
70
+
71
+ & + .chat-body {
72
+ margin-inline-start: auto;
73
+ border-radius: 1em;
74
+ border-start-end-radius: 0.2em;
75
+ background-color: var(--c-primary-soft);
76
+ }
77
+ }
78
+ }
79
+ </style>
@@ -0,0 +1,134 @@
1
+ <script setup lang="ts">
2
+ const props = withDefaults(defineProps<{
3
+ prompt?: string | boolean
4
+ code?: string
5
+ lang?: string
6
+ }>(), {
7
+ prompt: '$',
8
+ })
9
+
10
+ // prompt 传入空字符串会变成 true
11
+ const showPrompt = computed(() => props.prompt !== true)
12
+ const language = computed(() => props.lang ?? getPromptLanguage(props.prompt))
13
+
14
+ const showUndo = ref(false)
15
+ const codeInput = useTemplateRef('code-input')
16
+ const shiki = useShiki()
17
+
18
+ const { copy, copied } = useCopy(codeInput)
19
+
20
+ function undo() {
21
+ if (!codeInput.value)
22
+ return
23
+ codeInput.value.textContent = props.code ?? ''
24
+ // 触发 shiki 高亮
25
+ codeInput.value.dispatchEvent(new Event('input'))
26
+ showUndo.value = false
27
+ }
28
+
29
+ function preventLineBreak(event: InputEvent) {
30
+ const { data, inputType } = event
31
+ if (data?.includes('\n') || inputType === 'insertLineBreak') {
32
+ event.preventDefault()
33
+ }
34
+ }
35
+
36
+ function checkUndoable(event: InputEvent) {
37
+ showUndo.value = props.code !== (event.target as Element).textContent
38
+ }
39
+
40
+ onMounted(async () => {
41
+ await shiki.mountPlain(codeInput.value!, language.value)
42
+ })
43
+ </script>
44
+
45
+ <template>
46
+ <code class="copy">
47
+ <span v-if="showPrompt" class="prompt">{{ prompt }}</span>
48
+
49
+ <div
50
+ ref="code-input"
51
+ contenteditable="plaintext-only"
52
+ class="code scrollcheck-x"
53
+ spellcheck="false"
54
+ @beforeinput="preventLineBreak"
55
+ @input="checkUndoable"
56
+ v-text="code"
57
+ />
58
+
59
+ <button v-if="showUndo" class="operation" aria-label="恢复原始内容" @click="undo">
60
+ <Icon name="tabler:arrow-back-up" />
61
+ </button>
62
+
63
+ <Icon v-show="false" name="tabler:check" />
64
+ <button class="operation" aria-label="复制" @click="copy()">
65
+ <Icon :name="copied ? 'tabler:check' : 'tabler:copy'" />
66
+ </button>
67
+ </code>
68
+ </template>
69
+
70
+ <style lang="scss" scoped>
71
+ .copy {
72
+ contain: paint;
73
+ display: flex;
74
+ overflow: auto; // prompt 溢出时滚动
75
+ margin: 0.5rem 0;
76
+ border: 1px solid var(--c-border);
77
+ border-radius: 4px;
78
+ background-color: var(--ld-bg-card);
79
+ font-size: 0.8rem;
80
+ line-height: 2.5;
81
+ transition: border-color 0.2s;
82
+
83
+ &:focus-within {
84
+ border-color: var(--c-primary);
85
+ outline: 0.2em solid var(--c-primary-soft);
86
+
87
+ .prompt {
88
+ border-inline-end-color: var(--c-primary);
89
+ background-color: var(--c-primary-soft);
90
+ color: var(--c-primary);
91
+ }
92
+ }
93
+
94
+ .prompt {
95
+ flex-shrink: 0;
96
+ padding: 0 1em;
97
+ border-inline-end: 1px solid var(--c-border);
98
+ background-color: var(--c-bg-2);
99
+ color: var(--c-text-2);
100
+ transition: all 0.2s;
101
+ }
102
+
103
+ .code {
104
+ --fadeout-width: 3ch;
105
+ --scrollbar-height: 4px;
106
+
107
+ flex-grow: 1;
108
+ overflow: auto;
109
+ padding: 0 1em;
110
+ outline: none;
111
+ white-space: nowrap;
112
+ scrollbar-color: auto;
113
+ scrollbar-width: auto;
114
+
115
+ &::-webkit-scrollbar {
116
+ height: 4px;
117
+ background-color: transparent;
118
+ }
119
+ }
120
+
121
+ .operation {
122
+ flex-shrink: 0;
123
+ height: 2.5em;
124
+ margin-inline-start: -0.5em;
125
+ padding: 0.5em;
126
+ color: var(--c-text-2);
127
+ transition: color, 0.2s;
128
+
129
+ &:hover {
130
+ color: var(--c-primary);
131
+ }
132
+ }
133
+ }
134
+ </style>