clarity-theme 0.1.0 → 0.1.1

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 (119) hide show
  1. package/LICENSE +20 -20
  2. package/README.md +18 -4
  3. package/README.zh-CN.md +18 -4
  4. package/app/app.config.ts +110 -110
  5. package/app/app.vue +5 -5
  6. package/app/components/blog/BlogAside.vue +25 -25
  7. package/app/components/blog/BlogFooter.vue +25 -25
  8. package/app/components/blog/BlogHeader.global.vue +48 -48
  9. package/app/components/blog/BlogPanel.vue +43 -43
  10. package/app/components/blog/BlogSidebar.vue +53 -53
  11. package/app/components/blog/BlogWidget.vue +42 -42
  12. package/app/components/blog/Mask.vue +25 -25
  13. package/app/components/blog/SkipToContent.vue +9 -9
  14. package/app/components/blog/ThemeToggle.vue +22 -22
  15. package/app/components/content/Badge.vue +45 -45
  16. package/app/components/content/Blur.vue +14 -14
  17. package/app/components/content/CardList.vue +8 -8
  18. package/app/components/content/Chat.vue +41 -41
  19. package/app/components/content/Copy.vue +71 -71
  20. package/app/components/content/EmojiClock.vue +49 -49
  21. package/app/components/content/FeedCard.vue +96 -96
  22. package/app/components/content/FeedGroup.vue +61 -61
  23. package/app/components/content/Folding.vue +19 -19
  24. package/app/components/content/Key.vue +143 -143
  25. package/app/components/content/LinkBanner.vue +27 -27
  26. package/app/components/content/LinkCard.vue +28 -28
  27. package/app/components/content/MdTitle.vue +8 -8
  28. package/app/components/content/Mermaid.vue +99 -99
  29. package/app/components/content/MusicScore.vue +63 -63
  30. package/app/components/content/Pic.vue +49 -49
  31. package/app/components/content/Poetry.vue +27 -27
  32. package/app/components/content/ProseA.vue +24 -24
  33. package/app/components/content/ProseCode.vue +16 -16
  34. package/app/components/content/ProsePre.vue +117 -117
  35. package/app/components/content/ProseTable.vue +23 -23
  36. package/app/components/content/Quote.vue +21 -21
  37. package/app/components/content/Tab.vue +32 -32
  38. package/app/components/content/Timeline.vue +33 -33
  39. package/app/components/content/Tip.vue +40 -40
  40. package/app/components/content/VideoEmbed.vue +112 -112
  41. package/app/components/partial/Button.vue +36 -36
  42. package/app/components/partial/DlGroup.vue +31 -31
  43. package/app/components/partial/Dropdown.vue +21 -21
  44. package/app/components/partial/Error.vue +39 -39
  45. package/app/components/partial/Expand.vue +26 -26
  46. package/app/components/partial/IconNavList.vue +24 -24
  47. package/app/components/partial/Pagination.vue +62 -62
  48. package/app/components/partial/RadioGroup.vue +30 -30
  49. package/app/components/partial/Secret.vue +11 -11
  50. package/app/components/partial/Slider.vue +48 -48
  51. package/app/components/partial/Toggle.vue +17 -17
  52. package/app/components/popover/Lightbox.vue +44 -44
  53. package/app/components/popover/Search.vue +143 -143
  54. package/app/components/popover/SearchItem.vue +28 -28
  55. package/app/components/post/Archive.vue +40 -40
  56. package/app/components/post/Article.vue +49 -49
  57. package/app/components/post/Comment.vue +119 -119
  58. package/app/components/post/Excerpt.vue +40 -40
  59. package/app/components/post/OrderToggle.vue +70 -70
  60. package/app/components/post/PostFooter.vue +52 -52
  61. package/app/components/post/PostHeader.vue +66 -66
  62. package/app/components/post/PostSurround.vue +51 -51
  63. package/app/components/post/Slide.vue +85 -85
  64. package/app/components/util/Date.vue +62 -62
  65. package/app/components/util/HydrateSafe.vue +10 -10
  66. package/app/components/util/Img.vue +38 -38
  67. package/app/components/util/Link.vue +11 -11
  68. package/app/components/widget/BlogLog.vue +23 -23
  69. package/app/components/widget/BlogStats.vue +38 -38
  70. package/app/components/widget/BlogTech.vue +57 -57
  71. package/app/components/widget/Empty.vue +3 -3
  72. package/app/components/widget/Toc.vue +67 -67
  73. package/app/composables/useArticle.ts +113 -113
  74. package/app/composables/useAvoid.ts +87 -87
  75. package/app/composables/useCopy.ts +21 -21
  76. package/app/composables/usePagination.ts +70 -70
  77. package/app/composables/useShiki.ts +129 -129
  78. package/app/composables/useToc.ts +57 -57
  79. package/app/composables/useWidgets.ts +56 -56
  80. package/app/error.vue +31 -31
  81. package/app/layouts/default.vue +25 -25
  82. package/app/pages/[...slug].vue +59 -59
  83. package/app/pages/archive.vue +129 -129
  84. package/app/pages/index.vue +76 -76
  85. package/app/pages/link.vue +62 -62
  86. package/app/pages/preview.vue +46 -46
  87. package/app/plugins/easter-egg.ts +23 -23
  88. package/app/plugins/init.ts +6 -6
  89. package/app/plugins/tippy.ts +22 -22
  90. package/app/shiki.config.ts +8 -8
  91. package/app/stores/layout.ts +34 -34
  92. package/app/stores/search.ts +37 -37
  93. package/app/types/article.ts +16 -16
  94. package/app/types/index.ts +14 -14
  95. package/app/types/nav.ts +11 -11
  96. package/app/utils/anim.ts +37 -37
  97. package/app/utils/img.ts +83 -83
  98. package/config/app.ts +61 -61
  99. package/config/content.ts +75 -75
  100. package/config/define.ts +3 -3
  101. package/config/feeds.empty.ts +6 -4
  102. package/config/public.ts +1 -1
  103. package/config/schema.ts +89 -89
  104. package/config/server.ts +1 -1
  105. package/modules/clarity-config/index.ts +162 -159
  106. package/nuxt.config.ts +212 -212
  107. package/package.json +25 -21
  108. package/public/assets/atom.css +274 -274
  109. package/public/assets/atom.xsl +109 -109
  110. package/remark-plugins/rehype-meta-slots.ts +58 -58
  111. package/remark-plugins/remark-code-component.ts +41 -41
  112. package/server/api/stats.get.ts +84 -83
  113. package/server/routes/atom.xml.get.ts +85 -85
  114. package/server/routes/subscriptions.opml.get.ts +64 -64
  115. package/shared/utils/clarity.ts +24 -24
  116. package/shared/utils/icon.ts +218 -218
  117. package/shared/utils/link.ts +52 -52
  118. package/shared/utils/str.ts +88 -88
  119. package/shared/utils/time.ts +115 -115
@@ -1,43 +1,43 @@
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>
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
41
  .chat {
42
42
  margin-inline: 2vw;
43
43
  font-size: 0.9em;
@@ -76,4 +76,4 @@ function render() {
76
76
  }
77
77
  }
78
78
  }
79
- </style>
79
+ </style>
@@ -1,73 +1,73 @@
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>
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
71
  .copy {
72
72
  contain: paint;
73
73
  display: flex;
@@ -131,4 +131,4 @@ onMounted(async () => {
131
131
  }
132
132
  }
133
133
  }
134
- </style>
134
+ </style>
@@ -1,53 +1,53 @@
1
- <script setup lang="ts">
2
- import { Temporal } from 'temporal-polyfill'
3
-
4
- const props = defineProps<{
5
- datetime?: string
6
- rotate?: boolean
7
- }>()
8
-
9
- const emojiStatic = ['🕛', '🕧', '🕐', '🕜', '🕑', '🕝', '🕒', '🕞', '🕓', '🕟', '🕔', '🕠', '🕕', '🕡', '🕖', '🕢', '🕗', '🕣', '🕘', '🕤', '🕙', '🕥', '🕚', '🕦']
10
- const emojiRotate = ['🕛', '🕐', '🕑', '🕒', '🕓', '🕔', '🕕', '🕖', '🕗', '🕘', '🕙', '🕚']
11
-
12
- const now = ref(Temporal.Now.zonedDateTimeISO())
13
-
14
- const datetime = computed(() => props.datetime
15
- ? toZonedTemporal(props.datetime)
16
- : now.value)
17
-
18
- const status = computed(() => {
19
- const { hour, minute } = datetime.value
20
-
21
- if (!props.rotate) {
22
- const emojiIndex = (hour * 2 + Math.round(minute / 30)) % emojiStatic.length
23
- return { emoji: emojiStatic[emojiIndex] }
24
- }
25
-
26
- const minuteAt = Math.round(minute / 5)
27
- const emojiIndex = (hour % 12 - minuteAt + emojiRotate.length) % emojiRotate.length
28
- return { rotate: minuteAt * 30, emoji: emojiRotate[emojiIndex] }
29
- })
30
-
31
- // 定时器只能在客户端运行,否则 nuxt generate 不能自动退出
32
- const { resume } = useIntervalFn(() => {
33
- now.value = Temporal.Now.zonedDateTimeISO()
34
- }, 30000, { immediate: false })
35
-
36
- whenever(() => !props.datetime, resume)
37
- </script>
38
-
39
- <template>
40
- <span
41
- class="emoji-clock"
42
- :class="{ rotate }"
43
- :style="{ '--deg': rotate ? `${status.rotate}deg` : undefined }"
44
- v-text="status.emoji"
45
- />
46
- </template>
47
-
48
- <style lang="scss" scoped>
1
+ <script setup lang="ts">
2
+ import { Temporal } from 'temporal-polyfill'
3
+
4
+ const props = defineProps<{
5
+ datetime?: string
6
+ rotate?: boolean
7
+ }>()
8
+
9
+ const emojiStatic = ['🕛', '🕧', '🕐', '🕜', '🕑', '🕝', '🕒', '🕞', '🕓', '🕟', '🕔', '🕠', '🕕', '🕡', '🕖', '🕢', '🕗', '🕣', '🕘', '🕤', '🕙', '🕥', '🕚', '🕦']
10
+ const emojiRotate = ['🕛', '🕐', '🕑', '🕒', '🕓', '🕔', '🕕', '🕖', '🕗', '🕘', '🕙', '🕚']
11
+
12
+ const now = ref(Temporal.Now.zonedDateTimeISO())
13
+
14
+ const datetime = computed(() => props.datetime
15
+ ? toZonedTemporal(props.datetime)
16
+ : now.value)
17
+
18
+ const status = computed(() => {
19
+ const { hour, minute } = datetime.value
20
+
21
+ if (!props.rotate) {
22
+ const emojiIndex = (hour * 2 + Math.round(minute / 30)) % emojiStatic.length
23
+ return { emoji: emojiStatic[emojiIndex] }
24
+ }
25
+
26
+ const minuteAt = Math.round(minute / 5)
27
+ const emojiIndex = (hour % 12 - minuteAt + emojiRotate.length) % emojiRotate.length
28
+ return { rotate: minuteAt * 30, emoji: emojiRotate[emojiIndex] }
29
+ })
30
+
31
+ // 定时器只能在客户端运行,否则 nuxt generate 不能自动退出
32
+ const { resume } = useIntervalFn(() => {
33
+ now.value = Temporal.Now.zonedDateTimeISO()
34
+ }, 30000, { immediate: false })
35
+
36
+ whenever(() => !props.datetime, resume)
37
+ </script>
38
+
39
+ <template>
40
+ <span
41
+ class="emoji-clock"
42
+ :class="{ rotate }"
43
+ :style="{ '--deg': rotate ? `${status.rotate}deg` : undefined }"
44
+ v-text="status.emoji"
45
+ />
46
+ </template>
47
+
48
+ <style lang="scss" scoped>
49
49
  .emoji-clock.rotate {
50
50
  display: inline-block;
51
51
  transform: rotate(var(--deg, 0deg));
52
52
  }
53
- </style>
53
+ </style>
@@ -1,98 +1,98 @@
1
- <script setup lang="ts">
2
- import type { CSSProperties } from 'vue'
3
- import type { FeedEntry } from '../../types/feed'
4
- // eslint-disable-next-line unused-imports/no-unused-imports
5
- import { Temporal } from 'temporal-polyfill'
6
-
7
- const props = defineProps<FeedEntry>()
8
-
9
- const clarity = useClarityConfig()
10
- const route = useRoute()
11
- const isInspect = computed(() => import.meta.dev && route.query.inspect !== undefined)
12
-
13
- const title = computed(() => props.title ?? props.sitenick ?? props.author)
14
- const domainTip = computed(() => getDomainType(getMainDomain(props.link, true)))
15
- const domainIcon = computed(() => getDomainIcon(props.link))
16
-
17
- function getInspectStyle(src: string): CSSProperties {
18
- src = getMainDomain(src)
19
- let color = 'red'
20
-
21
- if (src === getMainDomain(props.link))
22
- color = 'transparent' // 来自源站
23
- else if (src === 'gstatic.cn' || src === 'webp.se')
24
- color = 'yellow' // 来自API获取
25
- else if (src === 'qlogo.cn')
26
- color = 'lightblue' // 来自QQ头像
27
- else if (src === ('wsrv.nl'))
28
- color = 'lightgreen' // 来自GitHub头像
29
-
30
- return {
31
- border: `2px solid ${color}`,
32
- boxSizing: 'content-box',
33
- }
34
- }
35
- </script>
36
-
37
- <template>
38
- <Tooltip :delay="200" interactive ^role="link" hide-on-click="toggle">
39
- <UtilLink
40
- class="feed-card gradient-card"
41
- :to="error ? undefined : link"
42
- rel="noopener"
43
- :data-error="error"
44
- >
45
- <div class="avatar" :title="feed ? undefined : '无订阅源'">
46
- <ClientOnly v-if="isInspect">
47
- <span style="position: absolute; left: 100%; white-space: nowrap;" v-text="title" />
48
- <NuxtImg :src="icon" :title="icon" :style="getInspectStyle(icon)" />
49
- <NuxtImg :src="avatar" :title="avatar" :style="getInspectStyle(avatar)" />
50
- </ClientOnly>
51
-
52
- <NuxtImg v-else class="round-cobblestone" :src="avatar" :alt="author" loading="lazy" />
53
- <Icon v-if="clarity.link.remindNoFeed && !feed" class="no-feed" name="tabler:bell-off" />
54
- </div>
55
-
56
- <span class="author">{{ author }}</span>
57
- <span class="sitenick">{{ sitenick }}</span>
58
- </UtilLink>
59
-
60
- <template #content>
61
- <div class="site-content">
62
- <NuxtImg class="site-icon" :src="icon" :alt="title" loading="lazy" />
63
-
64
- <div class="site-info">
65
- <h3 class="text-creative">
66
- {{ title }}
67
- </h3>
68
-
69
- <code class="domain" :title="domainTip">
70
- <span>{{ getDomain(link) }}</span>
71
- <Icon v-if="domainIcon" class="domain-mark" :name="domainIcon" />
72
- </code>
73
- </div>
74
-
75
- <Icon
76
- v-for="arch in archs" :key="arch"
77
- class="arch" :name="getArchIcon(arch)" :title="arch"
78
- />
79
- </div>
80
- <div class="desc-content">
81
- <div class="date">
82
- {{ Temporal.PlainDate.from(date).toLocaleString() }}
83
- </div>
84
-
85
- <p>{{ error ?? desc }}</p>
86
-
87
- <p v-if="comment">
88
- <Icon name="tabler:message-dots" /> {{ comment }}
89
- </p>
90
- </div>
91
- </template>
92
- </Tooltip>
93
- </template>
94
-
95
- <style lang="scss" scoped>
1
+ <script setup lang="ts">
2
+ import type { CSSProperties } from 'vue'
3
+ import type { FeedEntry } from '../../types/feed'
4
+ // eslint-disable-next-line unused-imports/no-unused-imports
5
+ import { Temporal } from 'temporal-polyfill'
6
+
7
+ const props = defineProps<FeedEntry>()
8
+
9
+ const clarity = useClarityConfig()
10
+ const route = useRoute()
11
+ const isInspect = computed(() => import.meta.dev && route.query.inspect !== undefined)
12
+
13
+ const title = computed(() => props.title ?? props.sitenick ?? props.author)
14
+ const domainTip = computed(() => getDomainType(getMainDomain(props.link, true)))
15
+ const domainIcon = computed(() => getDomainIcon(props.link))
16
+
17
+ function getInspectStyle(src: string): CSSProperties {
18
+ src = getMainDomain(src)
19
+ let color = 'red'
20
+
21
+ if (src === getMainDomain(props.link))
22
+ color = 'transparent' // 来自源站
23
+ else if (src === 'gstatic.cn' || src === 'webp.se')
24
+ color = 'yellow' // 来自API获取
25
+ else if (src === 'qlogo.cn')
26
+ color = 'lightblue' // 来自QQ头像
27
+ else if (src === ('wsrv.nl'))
28
+ color = 'lightgreen' // 来自GitHub头像
29
+
30
+ return {
31
+ border: `2px solid ${color}`,
32
+ boxSizing: 'content-box',
33
+ }
34
+ }
35
+ </script>
36
+
37
+ <template>
38
+ <Tooltip :delay="200" interactive ^role="link" hide-on-click="toggle">
39
+ <UtilLink
40
+ class="feed-card gradient-card"
41
+ :to="error ? undefined : link"
42
+ rel="noopener"
43
+ :data-error="error"
44
+ >
45
+ <div class="avatar" :title="feed ? undefined : '无订阅源'">
46
+ <ClientOnly v-if="isInspect">
47
+ <span style="position: absolute; left: 100%; white-space: nowrap;" v-text="title" />
48
+ <NuxtImg :src="icon" :title="icon" :style="getInspectStyle(icon)" />
49
+ <NuxtImg :src="avatar" :title="avatar" :style="getInspectStyle(avatar)" />
50
+ </ClientOnly>
51
+
52
+ <NuxtImg v-else class="round-cobblestone" :src="avatar" :alt="author" loading="lazy" />
53
+ <Icon v-if="clarity.link.remindNoFeed && !feed" class="no-feed" name="tabler:bell-off" />
54
+ </div>
55
+
56
+ <span class="author">{{ author }}</span>
57
+ <span class="sitenick">{{ sitenick }}</span>
58
+ </UtilLink>
59
+
60
+ <template #content>
61
+ <div class="site-content">
62
+ <NuxtImg class="site-icon" :src="icon" :alt="title" loading="lazy" />
63
+
64
+ <div class="site-info">
65
+ <h3 class="text-creative">
66
+ {{ title }}
67
+ </h3>
68
+
69
+ <code class="domain" :title="domainTip">
70
+ <span>{{ getDomain(link) }}</span>
71
+ <Icon v-if="domainIcon" class="domain-mark" :name="domainIcon" />
72
+ </code>
73
+ </div>
74
+
75
+ <Icon
76
+ v-for="arch in archs" :key="arch"
77
+ class="arch" :name="getArchIcon(arch)" :title="arch"
78
+ />
79
+ </div>
80
+ <div class="desc-content">
81
+ <div class="date">
82
+ {{ Temporal.PlainDate.from(date).toLocaleString() }}
83
+ </div>
84
+
85
+ <p>{{ error ?? desc }}</p>
86
+
87
+ <p v-if="comment">
88
+ <Icon name="tabler:message-dots" /> {{ comment }}
89
+ </p>
90
+ </div>
91
+ </template>
92
+ </Tooltip>
93
+ </template>
94
+
95
+ <style lang="scss" scoped>
96
96
  .feed-card {
97
97
  display: flex;
98
98
  align-items: center;
@@ -209,4 +209,4 @@ function getInspectStyle(src: string): CSSProperties {
209
209
  pointer-events: none;
210
210
  }
211
211
  }
212
- </style>
212
+ </style>
@@ -1,63 +1,63 @@
1
- <script setup lang="ts">
2
- import type { FeedEntry, FeedGroup } from '../../types/feed'
3
- import { shuffle } from 'es-toolkit/array'
4
-
5
- const props = defineProps<FeedGroup & { shuffle?: boolean }>()
6
- const route = useRoute()
7
- const entries = ref(props.entries)
8
-
9
- // 友链浮现随机延迟
10
- function getCardDelay(feed: FeedEntry) {
11
- let hash = 0
12
- for (const char of feed.link) {
13
- hash = hash * 31 + char.charCodeAt(0)
14
- }
15
- return (hash % 1000) / 1000
16
- }
17
-
18
- const shuffleEntries = () => entries.value = shuffle(entries.value)
19
- const unshuffleEntries = () => entries.value = props.entries
20
-
21
- onMounted(() => {
22
- if (props.shuffle && route.query.shuffle !== 'false')
23
- shuffleEntries()
24
- })
25
-
26
- if (import.meta.dev) {
27
- watch(() => props.entries, (newEntries) => {
28
- entries.value = newEntries
29
- })
30
- }
31
- </script>
32
-
33
- <template>
34
- <section class="feed-group">
35
- <h3 class="feed-title">
36
- <button
37
- v-if="props.shuffle"
38
- role="button"
39
- title="点击随机排序,按住修饰键点击可取消随机排序"
40
- @click="unshuffleEntries"
41
- @click.exact="shuffleEntries"
42
- v-text="name"
43
- />
44
- <span v-else v-text="name" />
45
- </h3>
46
- <p class="feed-desc" v-text="desc" />
47
-
48
- <TransitionGroup tag="menu" class="feed-list" name="float-in">
49
- <li
50
- v-for="entry in entries"
51
- :key="entry.link"
52
- :style="`--delay: ${getCardDelay(entry)}s;`"
53
- >
54
- <FeedCard v-bind="entry" />
55
- </li>
56
- </TransitionGroup>
57
- </section>
58
- </template>
59
-
60
- <style lang="scss" scoped>
1
+ <script setup lang="ts">
2
+ import type { FeedEntry, FeedGroup } from '../../types/feed'
3
+ import { shuffle } from 'es-toolkit/array'
4
+
5
+ const props = defineProps<FeedGroup & { shuffle?: boolean }>()
6
+ const route = useRoute()
7
+ const entries = ref(props.entries)
8
+
9
+ // 友链浮现随机延迟
10
+ function getCardDelay(feed: FeedEntry) {
11
+ let hash = 0
12
+ for (const char of feed.link) {
13
+ hash = hash * 31 + char.charCodeAt(0)
14
+ }
15
+ return (hash % 1000) / 1000
16
+ }
17
+
18
+ const shuffleEntries = () => entries.value = shuffle(entries.value)
19
+ const unshuffleEntries = () => entries.value = props.entries
20
+
21
+ onMounted(() => {
22
+ if (props.shuffle && route.query.shuffle !== 'false')
23
+ shuffleEntries()
24
+ })
25
+
26
+ if (import.meta.dev) {
27
+ watch(() => props.entries, (newEntries) => {
28
+ entries.value = newEntries
29
+ })
30
+ }
31
+ </script>
32
+
33
+ <template>
34
+ <section class="feed-group">
35
+ <h3 class="feed-title">
36
+ <button
37
+ v-if="props.shuffle"
38
+ role="button"
39
+ title="点击随机排序,按住修饰键点击可取消随机排序"
40
+ @click="unshuffleEntries"
41
+ @click.exact="shuffleEntries"
42
+ v-text="name"
43
+ />
44
+ <span v-else v-text="name" />
45
+ </h3>
46
+ <p class="feed-desc" v-text="desc" />
47
+
48
+ <TransitionGroup tag="menu" class="feed-list" name="float-in">
49
+ <li
50
+ v-for="entry in entries"
51
+ :key="entry.link"
52
+ :style="`--delay: ${getCardDelay(entry)}s;`"
53
+ >
54
+ <FeedCard v-bind="entry" />
55
+ </li>
56
+ </TransitionGroup>
57
+ </section>
58
+ </template>
59
+
60
+ <style lang="scss" scoped>
61
61
  .feed-group {
62
62
  container-type: inline-size;
63
63
  margin: 2em 1em;
@@ -127,4 +127,4 @@ if (import.meta.dev) {
127
127
  contain: paint; // 防止移动时出现滚动条
128
128
  pointer-events: none; // 阻止触发错误定位的气泡
129
129
  }
130
- </style>
130
+ </style>