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,23 @@
1
+ <template>
2
+ <!-- 外层元素用于占位 -->
3
+ <div class="secret-container">
4
+ <div class="secret">
5
+ <slot />
6
+ </div>
7
+ </div>
8
+ </template>
9
+
10
+ <style lang="scss" scoped>
11
+ .secret {
12
+ position: relative;
13
+ opacity: 0;
14
+ transition: all 0.2s var(--secret-delay, 0.5s), color 0.2s;
15
+ z-index: -1;
16
+
17
+ :hover > &,
18
+ :focus-within > & {
19
+ opacity: 1;
20
+ z-index: 0;
21
+ }
22
+ }
23
+ </style>
@@ -0,0 +1,96 @@
1
+ <script setup lang="ts">
2
+ const props = defineProps<{
3
+ label?: string
4
+ springMin?: number
5
+ springMax?: number
6
+ list?: (string | { value: string, label?: string })[]
7
+ }>()
8
+
9
+ const modelValue = defineModel<number>({ required: true })
10
+ const initialValue = modelValue.value
11
+ const id = useId()
12
+ const list = computed(() => props.list?.map(v => typeof v === 'string' ? { value: v } : v))
13
+
14
+ function spring() {
15
+ if (props.springMin !== undefined && modelValue.value < props.springMin) {
16
+ modelValue.value = props.springMin
17
+ }
18
+ if (props.springMax !== undefined && modelValue.value > props.springMax) {
19
+ modelValue.value = props.springMax
20
+ }
21
+ }
22
+ const debounceSpring = useDebounceFn(spring, 500)
23
+ </script>
24
+
25
+ <template>
26
+ <label class="z-slider">
27
+ <slot><span v-if="label" v-text="label" /></slot>
28
+
29
+ <input
30
+ v-bind="$attrs"
31
+ v-model.number="modelValue"
32
+ class="input-slider"
33
+ type="range"
34
+ :list="id"
35
+ @change="debounceSpring"
36
+ @dblclick="modelValue = initialValue"
37
+ >
38
+
39
+ <data class="data-value" :value="modelValue" v-text="modelValue" />
40
+
41
+ <datalist v-if="list" :id="id">
42
+ <option v-for="{ value, label: text } in list" :key="value" :value :label="text" />
43
+ </datalist>
44
+ </label>
45
+ </template>
46
+
47
+ <style lang="scss" scoped>
48
+ .z-slider {
49
+ display: flex;
50
+ align-items: center;
51
+ gap: 0.5em;
52
+ user-select: none;
53
+ }
54
+
55
+ .input-slider[type="range"] {
56
+ flex-grow: 1;
57
+ overflow: hidden;
58
+ height: 1.2em;
59
+ border-radius: 1em;
60
+ background-color: var(--c-bg-soft);
61
+ appearance: none;
62
+
63
+ @supports (-moz-appearance: auto) {
64
+ accent-color: var(--c-primary);
65
+ appearance: auto;
66
+ }
67
+
68
+ &::-webkit-slider-container {
69
+ min-block-size: auto;
70
+ }
71
+
72
+ &::-webkit-slider-runnable-track {
73
+ height: 100%;
74
+ }
75
+
76
+ &::-webkit-slider-thumb {
77
+ height: 100%;
78
+ aspect-ratio: 1;
79
+ border: 0.2em solid var(--c-primary);
80
+ border-radius: 50%;
81
+ box-shadow: -100vw 0 0 calc(100vw - 0.6em) var(--c-primary);
82
+ background-color: var(--c-bg-1);
83
+ transition: all 0.2s;
84
+ appearance: none;
85
+
86
+ :hover > & {
87
+ background-color: var(--c-bg-3);
88
+ }
89
+ }
90
+ }
91
+
92
+ .data-value {
93
+ min-width: var(--slider-value, 3ch);
94
+ text-align: right;
95
+ }
96
+ </style>
@@ -0,0 +1,70 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ label?: string
4
+ }>()
5
+
6
+ const modelValue = defineModel<boolean>()
7
+ </script>
8
+
9
+ <template>
10
+ <label class="z-toggle">
11
+ <input v-model="modelValue" class="input-toggle" name="toggle" type="checkbox" @keypress.enter="modelValue = !modelValue">
12
+ <slot><span v-if="label" v-text="label" /></slot>
13
+ </label>
14
+ </template>
15
+
16
+ <style lang="scss" scoped>
17
+ .z-toggle {
18
+ user-select: none;
19
+ }
20
+
21
+ .input-toggle {
22
+ position: relative;
23
+ appearance: none;
24
+
25
+ &::before {
26
+ content: "";
27
+ display: inline-block;
28
+ position: relative;
29
+ width: 2em;
30
+ height: 1.2em;
31
+ border-radius: 1em;
32
+ outline: 1px solid var(--c-text-1);
33
+ outline-offset: -1px;
34
+ vertical-align: text-bottom;
35
+ transition: background-color 0.2s, outline-color 0.2s;
36
+ }
37
+
38
+ &::after {
39
+ content: "";
40
+ position: absolute;
41
+ inset-inline-start: 0.2em;
42
+ top: calc(50% - 0.4em);
43
+ height: 0.8em;
44
+ aspect-ratio: 1;
45
+ border-radius: 50%;
46
+ background-color: var(--c-text-1);
47
+ transition: all 0.2s;
48
+
49
+ :hover > & {
50
+ filter: contrast(0.6);
51
+ }
52
+ }
53
+
54
+ &:not(:last-child) {
55
+ margin-inline-end: 0.5em;
56
+ }
57
+
58
+ &:checked {
59
+ &::before {
60
+ outline-color: transparent;
61
+ background-color: var(--c-primary);
62
+ }
63
+
64
+ &::after {
65
+ background-color: var(--c-bg-1);
66
+ transform: var(--transform-end-far);
67
+ }
68
+ }
69
+ }
70
+ </style>
@@ -0,0 +1,77 @@
1
+ <script setup lang="ts">
2
+ import type { CSSProperties } from 'vue'
3
+
4
+ defineProps<{
5
+ el: HTMLImageElement
6
+ caption?: string | Component
7
+ open?: boolean
8
+ style?: CSSProperties
9
+ }>()
10
+
11
+ defineEmits<{
12
+ close: []
13
+ }>()
14
+ </script>
15
+
16
+ <template>
17
+ <BikariyaImageViewer
18
+ v-bind="$attrs"
19
+ :target="el"
20
+ :open
21
+ :style
22
+ clamp
23
+ :rate=".8"
24
+ @close="$emit('close')"
25
+ />
26
+
27
+ <Transition>
28
+ <div v-if="open" class="tooltip" :style="{ zIndex: style?.zIndex }">
29
+ <span v-if="caption" class="caption">
30
+ <component :is="() => toValue(caption)" />
31
+ </span>
32
+ <button
33
+ class="close"
34
+ aria-label="关闭灯箱"
35
+ @click="$emit('close')"
36
+ >
37
+ <Icon name="tabler:x" />
38
+ </button>
39
+ </div>
40
+ </Transition>
41
+ </template>
42
+
43
+ <style lang="scss" scoped>
44
+ .tooltip {
45
+ display: flex;
46
+ align-items: center;
47
+ position: fixed;
48
+ inset-inline: 0;
49
+ bottom: clamp(2rem, 10vh, 5rem);
50
+ width: fit-content;
51
+ max-width: min(40rem, 80%);
52
+ margin-inline: auto;
53
+ border: 1px solid var(--c-border);
54
+ border-radius: 0.5em;
55
+ box-shadow: var(--box-shadow-2), var(--box-shadow-3);
56
+ background-color: var(--c-bg-a80);
57
+ backdrop-filter: blur(1rem) saturate(2);
58
+ transition: all var(--delay);
59
+
60
+ &.v-enter-from,
61
+ &.v-leave-to {
62
+ opacity: 0;
63
+ bottom: 0;
64
+ }
65
+
66
+ .caption {
67
+ margin-inline-end: -0.5em;
68
+ padding: 0.5em 1em;
69
+ }
70
+
71
+ .close {
72
+ align-self: stretch;
73
+ padding: 0.5em;
74
+ cursor: pointer;
75
+ }
76
+ }
77
+ </style>
@@ -0,0 +1,214 @@
1
+ <script setup lang="ts">
2
+ import type { ModalEmits, ModalProps } from '../../types/modal'
3
+ import MiniSearch from 'minisearch'
4
+
5
+ const props = defineProps<ModalProps>()
6
+
7
+ defineEmits<ModalEmits>()
8
+
9
+ const clarity = useClarityConfig()
10
+ const segmenter = Intl.Segmenter && new Intl.Segmenter(clarity.site.language, { granularity: 'word' })
11
+
12
+ // await useAsyncData() 会阻塞渲染
13
+ const { data, status } = await useLazyAsyncData(
14
+ 'search',
15
+ () => queryCollectionSearchSections('content', {
16
+ ignoredTags: ['pre'],
17
+ }),
18
+ )
19
+
20
+ const miniSearch = new MiniSearch({
21
+ fields: ['title', 'content'],
22
+ storeFields: ['title', 'titles', 'content', 'level'],
23
+ searchOptions: {
24
+ prefix: true,
25
+ fuzzy: 0.2,
26
+ combineWith: 'AND',
27
+ boost: { title: 3, titles: 2 },
28
+ },
29
+ processTerm: segmenter
30
+ ? term => Array.from(segmenter.segment(term), seg => seg.segment.toLowerCase())
31
+ : undefined,
32
+ })
33
+
34
+ const searchStore = useSearchStore()
35
+ const searchInput = ref<HTMLInputElement>()
36
+
37
+ const { word, debouncedWord } = storeToRefs(searchStore)
38
+ const result = computed(() => {
39
+ void data.value
40
+ return miniSearch.search(debouncedWord.value)
41
+ })
42
+
43
+ const isKeyboardMode = ref(false)
44
+ const listResult = useTemplateRef('list-result')
45
+
46
+ const activeIndex = ref(0)
47
+ const activeItem = computed(() => listResult.value?.children[activeIndex.value] as HTMLLinkElement | undefined)
48
+
49
+ whenever(() => props.open, focusInput)
50
+
51
+ watch(status, (newStatus) => {
52
+ if (newStatus === 'success' && data.value) {
53
+ miniSearch.addAll(data.value)
54
+ }
55
+ })
56
+
57
+ watch(debouncedWord, () => {
58
+ activeIndex.value = 0
59
+ })
60
+
61
+ useEventListener('mousemove', () => isKeyboardMode.value = false)
62
+ useEventListener('keydown', () => isKeyboardMode.value = true)
63
+
64
+ async function focusInput(allSelect = false) {
65
+ await nextTick()
66
+ searchInput.value?.focus()
67
+ if (allSelect)
68
+ searchInput.value?.select()
69
+ }
70
+
71
+ function updateActiveIndex(index: number, isKeyboard = false) {
72
+ focusInput()
73
+ if (index < 0 || index >= result.value?.length)
74
+ return
75
+ activeIndex.value = index
76
+ if (isKeyboard)
77
+ isKeyboardMode.value = true
78
+ if (activeItem.value && isKeyboardMode.value) {
79
+ activeItem.value.scrollIntoView({ block: 'nearest' })
80
+ }
81
+ }
82
+
83
+ function openActiveItem() {
84
+ // 触发 vue-router 点击事件
85
+ activeItem.value?.click()
86
+ }
87
+ </script>
88
+
89
+ <template>
90
+ <Transition name="float-in">
91
+ <div v-if="open" class="blog-search">
92
+ <form class="input" @submit.prevent>
93
+ <Icon v-show="false" name="line-md:loading-alt-loop" />
94
+ <Icon :name="status === 'pending' ? 'line-md:loading-alt-loop' : 'tabler:search'" />
95
+
96
+ <!-- 方向键切换搜索结果不应只在搜索框内触发 -->
97
+ <input
98
+ ref="searchInput"
99
+ v-model="word"
100
+ type="search"
101
+ incremental
102
+ class="search-input"
103
+ placeholder="键入开始搜索"
104
+ @keydown.up.prevent
105
+ @keydown.down.prevent
106
+ >
107
+ </form>
108
+
109
+ <TransitionGroup name="expand">
110
+ <div v-if="debouncedWord && status === 'success' && !result.length" class="no-result">
111
+ 无结果
112
+ </div>
113
+
114
+ <menu
115
+ v-if="result.length"
116
+ ref="list-result"
117
+ :key="result.length < 5 ? result.length : result[0]?.id"
118
+ class="scrollcheck-y search-result"
119
+ >
120
+ <PopoverSearchItem
121
+ v-for="(item, itemIndex) in result"
122
+ :key="item.id"
123
+ v-bind="item"
124
+ :class="{ active: activeIndex === itemIndex }"
125
+ @mousemove="updateActiveIndex(itemIndex)"
126
+ />
127
+ </menu>
128
+
129
+ <div v-if="result.length" class="tip" @click="searchInput?.focus()">
130
+ <Key code="ArrowUp" prevent @press="updateActiveIndex(activeIndex - 1, true)" />
131
+ <Key code="ArrowDown" prevent @press="updateActiveIndex(activeIndex + 1, true)" />
132
+ 切换&emsp;
133
+ <Key code="Enter" icon @press="openActiveItem" />
134
+ 选择&emsp;
135
+ <Key code="Escape" :icon="false" @press="$emit('close')" />
136
+ 关闭
137
+ </div>
138
+ </TransitionGroup>
139
+ </div>
140
+ </Transition>
141
+ </template>
142
+
143
+ <style lang="scss" scoped>
144
+ .blog-search {
145
+ --float-distance: 20vh;
146
+
147
+ contain: paint;
148
+ position: fixed;
149
+ inset: 0;
150
+ width: 90%;
151
+ height: fit-content;
152
+ max-width: $breakpoint-mobile;
153
+ margin: auto;
154
+ border: 1px solid var(--c-primary);
155
+ border-radius: 1em;
156
+ box-shadow: var(--box-shadow-2), var(--box-shadow-3);
157
+ outline: 0.2em solid var(--c-primary-soft);
158
+ background-color: var(--ld-bg-card);
159
+ }
160
+
161
+ .input {
162
+ display: flex;
163
+ align-items: center;
164
+ gap: 1em;
165
+ position: relative;
166
+ padding: 0 1em;
167
+
168
+ > .search-input {
169
+ width: 100%;
170
+ padding: 1em 0;
171
+ outline: none;
172
+ }
173
+ }
174
+
175
+ .no-result {
176
+ // expand 时不要设置 padding
177
+ max-height: 5em;
178
+ line-height: 5em;
179
+ text-align: center;
180
+ color: var(--c-text-3);
181
+ transition: all 0.5s;
182
+ }
183
+
184
+ .search-result {
185
+ max-height: 75vh;
186
+ max-height: 75dvh;
187
+ transition: all 0.5s;
188
+ scroll-padding: var(--fadeout-height);
189
+ }
190
+
191
+ .search-item {
192
+ transition: background-color 0.1s, opacity 0.2s;
193
+ }
194
+
195
+ .tip {
196
+ max-height: 1rem;
197
+ margin: 0 1em 0.5rem;
198
+ font-size: 0.8em;
199
+ text-align: center;
200
+ color: var(--c-text-3);
201
+ transition: all 0.5s;
202
+ }
203
+
204
+ .expand-enter-active,
205
+ .expand-leave-active {
206
+ transition: all 0.5s;
207
+ }
208
+
209
+ .expand-enter-from,
210
+ .expand-leave-to {
211
+ opacity: 0;
212
+ max-height: 0;
213
+ }
214
+ </style>
@@ -0,0 +1,64 @@
1
+ <script setup lang="ts">
2
+ import type { SearchResult } from 'minisearch'
3
+
4
+ interface SearchItem extends SearchResult {
5
+ title: string
6
+ content: string
7
+ titles: string[]
8
+ level: number
9
+ }
10
+
11
+ withDefaults(defineProps<Partial<SearchItem>>(), {
12
+ titles: () => [],
13
+ title: '',
14
+ })
15
+ </script>
16
+
17
+ <template>
18
+ <UtilLink :to="id" class="search-item">
19
+ <hgroup class="text-creative">
20
+ <span v-for="heading in [...titles, title]" :key="heading" class="title" v-html="highlightHtml(heading, queryTerms)" />
21
+ <Icon v-if="level === 1" name="tabler:file-text" />
22
+ </hgroup>
23
+ <p v-if="content" class="content" v-html="highlightHtml(content, queryTerms)" />
24
+ </UtilLink>
25
+ </template>
26
+
27
+ <style lang="scss" scoped>
28
+ .search-item {
29
+ display: block;
30
+ margin: 0.5em;
31
+ padding: 0.5em 0.8em;
32
+ border-radius: 0.5em;
33
+ transition: background-color 0.2s;
34
+
35
+ &.active {
36
+ background-color: var(--c-bg-soft);
37
+ }
38
+ }
39
+
40
+ .title:not(:first-child) {
41
+ opacity: 0.5;
42
+ transition: opacity 0.2s;
43
+
44
+ .active & {
45
+ opacity: 1;
46
+ }
47
+
48
+ &::before {
49
+ content: " > ";
50
+ opacity: 0.5;
51
+ }
52
+ }
53
+
54
+ .title + .iconify {
55
+ margin-inline-start: 0.2em;
56
+ }
57
+
58
+ .content {
59
+ margin-top: 0.2em;
60
+ font-size: 0.8em;
61
+ white-space: pre-wrap;
62
+ color: var(--c-text-2);
63
+ }
64
+ </style>
@@ -0,0 +1,104 @@
1
+ <script setup lang="ts">
2
+ import type { ArticleProps } from '../../types/article'
3
+
4
+ const props = defineProps<{
5
+ to?: string
6
+ useUpdated?: boolean
7
+ showCategory?: boolean
8
+ } & ArticleProps>()
9
+
10
+ const mainDate = computed(() => props.useUpdated ? props.updated : props.date)
11
+ </script>
12
+
13
+ <template>
14
+ <li class="article-item">
15
+ <UtilDate class="dim-hover" :date="mainDate" format="monthDay" />
16
+
17
+ <div class="gradient-card" :style="{ '--c-accent': getCategoryColor(categories?.[0]) }">
18
+ <UtilLink class="article-link scrollbar-hidden scrollcheck-x" :to :title="description">
19
+ <span class="article-title">
20
+ <Icon v-if="showCategory" :name="getCategoryIcon(categories?.[0])" />
21
+ {{ title }}
22
+ </span>
23
+
24
+ <UtilDate
25
+ v-if="date && useUpdated && isTimeDiffSignificant(date, updated)"
26
+ class="dim-hover info"
27
+ :date="date"
28
+ :format="updated && isSameUnit(date, updated, 'year') ? 'monthDay' : 'date'"
29
+ />
30
+
31
+ <ul v-if="tags?.length" class="dim-hover info tag-list">
32
+ <li v-for="tag in tags" :key="tag" v-text="tag" />
33
+ </ul>
34
+ </UtilLink>
35
+ </div>
36
+ </li>
37
+ </template>
38
+
39
+ <style lang="scss" scoped>
40
+ .article-item {
41
+ display: flex;
42
+ align-items: center;
43
+ column-gap: 0.5em;
44
+ min-width: 0;
45
+ margin: var(--archive-item-gap, 0.2em) 0;
46
+ transition: all 0.2s;
47
+ animation: float-in 0.2s var(--delay) backwards;
48
+
49
+ @media (max-width: $breakpoint-mobile) {
50
+ font-size: 0.9em;
51
+ }
52
+
53
+ .dim-hover {
54
+ opacity: 0.4;
55
+ transition: opacity 0.2s;
56
+ }
57
+
58
+ :deep(time) {
59
+ font-variant-numeric: tabular-nums;
60
+ }
61
+
62
+ .info {
63
+ font-size: 0.8em;
64
+ }
65
+
66
+ &:hover,
67
+ &:focus-within {
68
+ .article-title {
69
+ color: var(--c-text);
70
+ }
71
+
72
+ .dim-hover {
73
+ opacity: 1;
74
+ }
75
+ }
76
+ }
77
+
78
+ .gradient-card {
79
+ flex-grow: 1;
80
+ min-width: 0;
81
+ }
82
+
83
+ .article-link {
84
+ --scrollbar-height: 0px;
85
+
86
+ display: flex;
87
+ align-items: baseline;
88
+ gap: 1em;
89
+ padding: 0.3em 0.6em;
90
+ white-space: nowrap;
91
+
92
+ > .tag-list {
93
+ display: flex;
94
+ flex-wrap: nowrap;
95
+ gap: 0.5em;
96
+ margin-left: auto;
97
+
98
+ &::before {
99
+ content: "#";
100
+ opacity: 0.5;
101
+ }
102
+ }
103
+ }
104
+ </style>