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,127 @@
1
+ <script setup lang="ts">
2
+ const props = withDefaults(defineProps<{
3
+ /** 视频类型,数字过长时需通过双引号传入字符串 */
4
+ type?: 'raw' | 'bilibili' | 'bilibili-nano' | 'youtube' | 'douyin' | 'douyin-wide' | 'tiktok'
5
+ id: string
6
+ autoplay?: boolean
7
+ /** 视频宽高比,如 `16 / 9` `1.6` */
8
+ ratio?: string | number
9
+ /** 仅 type 为 `raw` 时可用 */
10
+ poster?: string
11
+ /** 视频宽度 */
12
+ width?: string
13
+ /** 视频高度 */
14
+ height?: string
15
+ /** CSS Zoom 比例 */
16
+ zoom?: number
17
+ }>(), {
18
+ type: 'raw',
19
+ height: '80vh',
20
+ })
21
+
22
+ const ratio = computed(() => {
23
+ if (props.ratio) {
24
+ return props.ratio
25
+ }
26
+ switch (props.type) {
27
+ case 'raw':
28
+ return undefined
29
+ case 'douyin':
30
+ return '27 / 56'
31
+ case 'douyin-wide':
32
+ return '1198 / 731'
33
+ default:
34
+ return '16 / 9'
35
+ }
36
+ })
37
+
38
+ const src = computed(() => {
39
+ switch (props.type) {
40
+ case 'bilibili':
41
+ return `https://player.bilibili.com/player.html?bvid=${props.id}&autoplay=${props.autoplay}`
42
+ case 'bilibili-nano':
43
+ return `https://www.bilibili.com/blackboard/newplayer.html?bvid=${props.id}&autoplay=${props.autoplay}`
44
+ case 'youtube':
45
+ return `https://www.youtube.com/embed/${props.id}?rel=0&disablekb=1&playsinline=1&autoplay=${props.autoplay}`
46
+ case 'douyin':
47
+ return `https://open.douyin.com/player/video?vid=${props.id}`
48
+ case 'douyin-wide':
49
+ return `https://open.douyin.com/player/video?vid=${props.id}`
50
+ case 'tiktok':
51
+ return `https://www.tiktok.com/embed/v3/${props.id}`
52
+ default:
53
+ return props.id
54
+ }
55
+ })
56
+
57
+ const videoPlayer = useTemplateRef('video-player')
58
+
59
+ // 抖音会预检测宽度是否小于 730px,再返回竖屏或横屏模式
60
+ // douyin-wide 需要阻塞加载,等进入视口后通过 zoom: 0.1 触发横屏模式
61
+ // 已优化为 loading="lazy"
62
+ const zoom = ref<number>()
63
+ const zoomFactorForFixed = {
64
+ 'douyin': 0.0031,
65
+ 'douyin-wide': 0.00134,
66
+ }
67
+
68
+ function useZoomHelper(resizeType: keyof typeof zoomFactorForFixed) {
69
+ useResizeObserver(videoPlayer, ([entry]) => {
70
+ if (!entry)
71
+ return
72
+ const { width } = entry.contentRect
73
+ const zoomFactor = zoomFactorForFixed[resizeType]
74
+ zoom.value = width * zoomFactor
75
+ })
76
+ }
77
+
78
+ onMounted(() => {
79
+ if (props.type in zoomFactorForFixed)
80
+ useZoomHelper(props.type as keyof typeof zoomFactorForFixed)
81
+ })
82
+ </script>
83
+
84
+ <template>
85
+ <div
86
+ ref="video-player"
87
+ class="video"
88
+ :style="{
89
+ aspectRatio: ratio,
90
+ maxWidth: width,
91
+ maxHeight: height,
92
+ }"
93
+ >
94
+ <video
95
+ v-if="type === 'raw'"
96
+ :poster
97
+ :src
98
+ controls
99
+ />
100
+ <iframe
101
+ v-else
102
+ :src
103
+ :style="{ zoom }"
104
+ scrolling="no"
105
+ loading="lazy"
106
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; fullscreen; gyroscope; picture-in-picture"
107
+ />
108
+ </div>
109
+ </template>
110
+
111
+ <style lang="scss" scoped>
112
+ .video {
113
+ contain: paint;
114
+ border-radius: 0.8rem;
115
+ box-shadow: var(--box-shadow-2);
116
+
117
+ article & {
118
+ margin: 2rem auto;
119
+ }
120
+ }
121
+
122
+ iframe, video {
123
+ display: block;
124
+ width: 100%;
125
+ height: 100%;
126
+ }
127
+ </style>
@@ -0,0 +1,98 @@
1
+ <script setup lang="ts">
2
+ import { UtilLink } from '#components'
3
+
4
+ export interface ButtonProps {
5
+ variant?: 'button' | 'text'
6
+ icon?: string
7
+ text?: string
8
+ to?: string
9
+ desc?: string
10
+ primary?: boolean
11
+ }
12
+
13
+ withDefaults(defineProps<ButtonProps>(), {
14
+ variant: 'button',
15
+ })
16
+ </script>
17
+
18
+ <template>
19
+ <component
20
+ :is="to ? UtilLink : 'button'"
21
+ :to
22
+ class="z-button"
23
+ :class="[variant, { primary }]"
24
+ >
25
+ <div class="button-main">
26
+ <Icon v-if="icon" :name="icon" />
27
+ <slot>{{ text }}</slot>
28
+ </div>
29
+ <div v-if="desc" class="button-desc">
30
+ {{ desc }}
31
+ </div>
32
+ </component>
33
+ </template>
34
+
35
+ <style lang="scss" scoped>
36
+ .z-button {
37
+ display: inline-block;
38
+ transition: color 0.1s, background-color 0.2s;
39
+
40
+ &.button {
41
+ padding: 0.4em 0.6em;
42
+ border: 1px solid var(--c-bg-soft);
43
+ border-radius: 0.5em;
44
+ box-shadow: var(--box-shadow-1);
45
+ background-color: var(--ld-bg-card);
46
+ line-height: 1.2;
47
+ vertical-align: middle;
48
+ cursor: pointer;
49
+
50
+ &.primary {
51
+ background-color: var(--c-primary);
52
+ color: var(--c-bg);
53
+ }
54
+
55
+ &:hover {
56
+ box-shadow: var(--box-shadow-2);
57
+ background-color: var(--c-bg-2);
58
+ color: var(--c-text);
59
+ }
60
+
61
+ &:active {
62
+ background-color: var(--ld-shadow);
63
+ }
64
+
65
+ &:disabled {
66
+ background-color: var(--c-bg-1);
67
+ }
68
+ }
69
+
70
+ &.text {
71
+ &:hover {
72
+ color: var(--c-primary);
73
+ }
74
+ }
75
+
76
+ &:disabled {
77
+ color: var(--c-text-3);
78
+ cursor: not-allowed;
79
+ }
80
+
81
+ & + .button {
82
+ margin-inline-start: 0.8em;
83
+ }
84
+ }
85
+
86
+ .button-main {
87
+ display: flex;
88
+ align-items: center;
89
+ justify-content: center;
90
+ gap: 0.2em;
91
+ }
92
+
93
+ .button-desc {
94
+ font-size: 0.75em;
95
+ text-align: center;
96
+ color: var(--c-text-2);
97
+ }
98
+ </style>
@@ -0,0 +1,69 @@
1
+ <script setup lang="ts">
2
+ import type { VNodeChild } from 'vue'
3
+
4
+ interface DlItem {
5
+ label: string
6
+ value: MaybeRefOrGetter<VNodeChild>
7
+ tip?: MaybeRefOrGetter<string>
8
+ }
9
+
10
+ withDefaults(defineProps<{
11
+ items: DlItem[]
12
+ size?: 'small' | 'medium' | 'large'
13
+ }>(), {
14
+ size: 'medium',
15
+ })
16
+ </script>
17
+
18
+ <template>
19
+ <dl class="dl-group" :class="size">
20
+ <div v-for="{ label, value, tip } in items" :key="label">
21
+ <dt>{{ label }}</dt>
22
+ <dd :title="toValue(tip)">
23
+ <!-- 支持 string, Ref<String>, VNode, () => VNode -->
24
+ <component :is="() => toValue(value)" />
25
+ </dd>
26
+ </div>
27
+ </dl>
28
+ </template>
29
+
30
+ <style lang="scss" scoped>
31
+ .dl-group {
32
+ > div {
33
+ padding: 0.2em 0;
34
+
35
+ > dt {
36
+ font-size: 0.9em;
37
+ color: var(--c-text-2);
38
+ }
39
+ }
40
+ }
41
+
42
+ .dl-group.small {
43
+ display: flex;
44
+ flex-wrap: wrap;
45
+ gap: 0.5em 1em;
46
+ text-align: center;
47
+
48
+ > div {
49
+ flex: 1;
50
+ white-space: nowrap;
51
+ }
52
+ }
53
+
54
+ .dl-group.medium {
55
+ display: grid;
56
+ grid-template-columns: auto auto;
57
+ gap: 0.4em 8%;
58
+ padding: 0.2em 0;
59
+
60
+ > div {
61
+ display: contents;
62
+
63
+ > dt {
64
+ font-size: inherit;
65
+ text-align: end;
66
+ }
67
+ }
68
+ }
69
+ </style>
@@ -0,0 +1,54 @@
1
+ <!-- https://vue-tippy.netlify.app/props/ -->
2
+ <!-- 如果要点击触发,请使用 trigger="focusin" 并添加 tabindex="0" -->
3
+
4
+ <template>
5
+ <Tooltip
6
+ class="dropdown"
7
+ interactive
8
+ placement="bottom"
9
+ :arrow="false"
10
+ :hide-on-click="false"
11
+ :offset="[0, 0]"
12
+ >
13
+ <slot />
14
+ <template #content="{ hide }">
15
+ <slot name="content" :hide />
16
+ </template>
17
+ </Tooltip>
18
+ </template>
19
+
20
+ <style lang="scss" scoped>
21
+ // https://vue-tippy.netlify.app/props#appendto
22
+ // Tooltip 位于组件根部时,interactive tippy 会插入到父组件
23
+ :deep() ~ [data-tippy-root] > .tippy-box {
24
+ padding: 0.3em;
25
+ font-size: inherit;
26
+
27
+ &[data-placement="top"] {
28
+ --c-fill: var(--c-bg-1);
29
+ }
30
+ }
31
+
32
+ :deep() ~ [data-tippy-root] .tippy-content {
33
+ display: grid;
34
+
35
+ button {
36
+ padding: 0.3em 0.5em;
37
+ border-radius: 0.3em;
38
+ text-align: start;
39
+ color: var(--c-text-1);
40
+ transition: color 0.1s, background-color 0.2s;
41
+ cursor: pointer;
42
+
43
+ &:hover {
44
+ background-color: var(--c-bg-soft);
45
+ color: var(--c-text-1);
46
+ }
47
+
48
+ &.active {
49
+ background-color: var(--c-primary-soft);
50
+ color: var(--c-primary);
51
+ }
52
+ }
53
+ }
54
+ </style>
@@ -0,0 +1,70 @@
1
+ <script setup lang="ts">
2
+ import type { BundledLanguage } from 'shiki'
3
+
4
+ withDefaults(defineProps<{
5
+ icon?: string
6
+ title?: string
7
+ message?: string
8
+ code?: string
9
+ language?: BundledLanguage
10
+ }>(), {
11
+ icon: 'line-md:cloud-alt-print-twotone-loop',
12
+ language: 'log',
13
+ })
14
+ </script>
15
+
16
+ <template>
17
+ <div class="error proper-height">
18
+ <div />
19
+ <Icon class="error-icon" :name="icon" />
20
+ <div class="error-title" v-html="title" />
21
+
22
+ <div class="operation">
23
+ <slot name="operation" />
24
+ </div>
25
+
26
+ <ProsePre
27
+ v-if="code"
28
+ :filename="message"
29
+ :language
30
+ :code
31
+ meta="wrap"
32
+ />
33
+
34
+ <slot />
35
+ </div>
36
+ </template>
37
+
38
+ <style lang="scss" scoped>
39
+ .error {
40
+ display: flex;
41
+ flex-direction: column;
42
+ align-items: center;
43
+ justify-content: center;
44
+ gap: 2rem;
45
+
46
+ > .error-icon {
47
+ font-size: 5rem;
48
+ color: var(--c-text-3);
49
+ }
50
+
51
+ > .error-title {
52
+ font-size: 1.5rem;
53
+ word-break: break-all;
54
+ color: var(--c-text-3);
55
+
56
+ > :deep(pre) {
57
+ font-size: 1rem;
58
+ white-space: pre-wrap;
59
+ }
60
+ }
61
+
62
+ > .z-codeblock {
63
+ max-width: 100%;
64
+
65
+ :deep(.shiki) {
66
+ background-color: transparent !important; /* stylelint-disable-line declaration-no-important */
67
+ }
68
+ }
69
+ }
70
+ </style>
@@ -0,0 +1,51 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ name?: string
4
+ inPlace?: boolean
5
+ }>()
6
+
7
+ const expand = defineModel<boolean>()
8
+ </script>
9
+
10
+ <template>
11
+ <div class="z-expand">
12
+ <Transition name="collapse">
13
+ <div v-show="expand" class="expand-content">
14
+ <slot />
15
+ </div>
16
+ </Transition>
17
+
18
+ <button class="toggle-btn" :class="{ 'in-place': inPlace }" @click="expand = !expand">
19
+ <Icon class="toggle-icon" :class="{ expand }" name="tabler:chevrons-down" />
20
+ <span>{{ `${expand ? '收起' : '展开'}${name}` }}</span>
21
+ </button>
22
+ </div>
23
+ </template>
24
+
25
+ <style lang="scss" scoped>
26
+ .z-expand {
27
+ display: flex;
28
+ flex-direction: column;
29
+ }
30
+
31
+ .toggle-btn {
32
+ display: block;
33
+ width: fit-content;
34
+ margin: 0 auto;
35
+ padding: 0.3em;
36
+ font-size: 0.9em;
37
+ color: var(--c-text-2);
38
+
39
+ &.in-place {
40
+ order: -1;
41
+ }
42
+ }
43
+
44
+ .toggle-icon {
45
+ transition: transform 0.2s;
46
+
47
+ &.expand {
48
+ transform: scaleY(-1);
49
+ }
50
+ }
51
+ </style>
@@ -0,0 +1,42 @@
1
+ <script setup lang="ts">
2
+ import type { NavItem } from '../../types/nav'
3
+
4
+ defineProps<{
5
+ list: NavItem[]
6
+ }>()
7
+ </script>
8
+
9
+ <template>
10
+ <menu>
11
+ <UtilLink
12
+ v-for="{ text, icon, url } in list"
13
+ :key="text"
14
+ v-tip="text"
15
+ :to="url"
16
+ :aria-label="text"
17
+ >
18
+ <Icon :name="icon" />
19
+ </UtilLink>
20
+ </menu>
21
+ </template>
22
+
23
+ <style lang="scss" scoped>
24
+ menu {
25
+ display: flex;
26
+ justify-content: center;
27
+
28
+ a {
29
+ padding: 0.5em;
30
+ border-radius: 2em;
31
+ transition: background-color 0.2s;
32
+
33
+ &:hover {
34
+ background-color: var(--c-bg-soft);
35
+ }
36
+
37
+ .iconify {
38
+ display: block;
39
+ }
40
+ }
41
+ }
42
+ </style>
@@ -0,0 +1,113 @@
1
+ <script setup lang="ts">
2
+ const props = defineProps<{
3
+ totalPages: number
4
+ expandPages?: number
5
+ sticky?: boolean
6
+ avoid?: boolean
7
+ }>()
8
+
9
+ const page = defineModel<number>({ required: true })
10
+ const pageArr = computed(() => getPaginationIndicator(page.value, props.totalPages, props.expandPages ?? 2))
11
+
12
+ const paginationEl = useTemplateRef('pagination')
13
+ const anchorEl = useTemplateRef('pagination-anchor')
14
+ const expand = useElementVisibility(anchorEl)
15
+
16
+ if (props.avoid) {
17
+ useAvoidTarget(paginationEl, toRef(props, 'avoid'))
18
+ }
19
+ </script>
20
+
21
+ <template>
22
+ <nav
23
+ ref="pagination"
24
+ class="pagination"
25
+ :class="{ sticky, expand }"
26
+ :aria-label="`第${page}页,共${totalPages}页`"
27
+ :style="{ '--collapsed-width': `${pageArr.length * 2 + 6}em` }"
28
+ >
29
+ <ZButton
30
+ :disabled="page <= 1"
31
+ class="pagination-button rtl-flip"
32
+ icon="tabler:arrow-left"
33
+ aria-label="上一页"
34
+ @click="page--"
35
+ />
36
+ <template v-for="i in pageArr" :key="i">
37
+ <button
38
+ v-if="Number.isFinite(i)"
39
+ class="pagination-num"
40
+ :class="{ active: i === page }"
41
+ :aria-label="`第${i}页`"
42
+ @click="page = i"
43
+ v-text="i"
44
+ />
45
+ <!-- TODO: 点击后自主选择目标页面 -->
46
+ <button v-else disabled class="pagination-num">
47
+
48
+ </button>
49
+ </template>
50
+ <ZButton
51
+ :disabled="page >= totalPages"
52
+ class="pagination-button rtl-flip"
53
+ icon="tabler:arrow-right"
54
+ aria-label="下一页"
55
+ @click="page++"
56
+ />
57
+ </nav>
58
+ <div ref="pagination-anchor" />
59
+ </template>
60
+
61
+ <style lang="scss" scoped>
62
+ .pagination {
63
+ display: flex;
64
+ max-width: calc(100vw);
65
+ margin: 1rem auto;
66
+ border: 1px solid var(--c-border);
67
+ border-radius: 0.5rem;
68
+ box-shadow: var(--box-shadow-1);
69
+ background-color: var(--ld-bg-card);
70
+ transition: max-width 0.2s var(--max-bezier-to-full);
71
+ font-variant-numeric: tabular-nums;
72
+
73
+ &.sticky {
74
+ position: sticky;
75
+ bottom: min(2em, 5%);
76
+
77
+ &:not(.expand) {
78
+ max-width: var(--collapsed-width);
79
+ transition-timing-function: var(--max-bezier-to-collapse);
80
+ }
81
+ }
82
+
83
+ > .pagination-button {
84
+ border: none;
85
+ border-radius: 0;
86
+ box-shadow: none;
87
+
88
+ &:first-child {
89
+ margin-inline-end: auto;
90
+ border-radius: 0.5rem 0 0 0.5rem;
91
+ }
92
+
93
+ &:last-child {
94
+ margin-inline-start: auto;
95
+ border-radius: 0 0.5rem 0.5rem 0;
96
+ }
97
+ }
98
+
99
+ > .pagination-num {
100
+ width: 3em;
101
+ transition: background-color 0.2s;
102
+
103
+ &:hover { background-color: var(--c-border); }
104
+
105
+ &:disabled { pointer-events: none; }
106
+
107
+ &.active {
108
+ background-color: var(--c-primary-soft);
109
+ color: var(--c-primary);
110
+ }
111
+ }
112
+ }
113
+ </style>
@@ -0,0 +1,77 @@
1
+ <script setup lang="ts" generic="T">
2
+ interface RadioItem {
3
+ label?: string
4
+ value: T
5
+ }
6
+
7
+ defineProps<{
8
+ items: RadioItem[]
9
+ }>()
10
+
11
+ const modelValue = defineModel<T>()
12
+ </script>
13
+
14
+ <template>
15
+ <div class="z-radio-group">
16
+ <button
17
+ v-for="{ label, value }, i in items"
18
+ :key="i"
19
+ type="button"
20
+ class="radio-item"
21
+ :class="{ active: value === modelValue }"
22
+ @click="modelValue = value"
23
+ >
24
+ {{ label || value }}
25
+ </button>
26
+ </div>
27
+ </template>
28
+
29
+ <style lang="scss" scoped>
30
+ .z-radio-group {
31
+ display: flex;
32
+ margin: 1em 0;
33
+ }
34
+
35
+ .radio-item {
36
+ flex-grow: 1;
37
+ position: relative;
38
+ padding: 0.2em;
39
+ border: 1px solid transparent;
40
+ background-color: var(--c-bg-2);
41
+ transition: all 0.2s;
42
+
43
+ &:first-child {
44
+ border-start-start-radius: 0.5em;
45
+ border-end-start-radius: 0.5em;
46
+ }
47
+
48
+ &:last-child {
49
+ border-start-end-radius: 0.5em;
50
+ border-end-end-radius: 0.5em;
51
+ }
52
+
53
+ &:hover {
54
+ background-color: var(--c-bg-1);
55
+ color: var(--c-primary);
56
+ }
57
+
58
+ &:focus {
59
+ z-index: 1;
60
+ }
61
+
62
+ &.active {
63
+ border-color: var(--c-primary);
64
+ background-color: var(--ld-bg-card);
65
+ color: var(--c-primary);
66
+ }
67
+
68
+ &:not(.active) + &:not(.active)::before {
69
+ content: "";
70
+ position: absolute;
71
+ inset-inline-start: -1px;
72
+ top: 25%;
73
+ bottom: 25%;
74
+ border-inline-start: 1px solid var(--c-bg-soft);
75
+ }
76
+ }
77
+ </style>