@sugarat/theme 0.5.7 → 0.5.9

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.
package/node.d.ts CHANGED
@@ -186,6 +186,27 @@ declare namespace Theme {
186
186
  * @default false
187
187
  */
188
188
  blogInfoCollapsible?: boolean;
189
+ /**
190
+ * 旋转的配置
191
+ */
192
+ hoverSpin?: boolean | HoverSpinConfig;
193
+ }
194
+ interface HoverSpinConfig {
195
+ /**
196
+ * 旋转的加速度
197
+ * @default 180
198
+ */
199
+ accel?: number;
200
+ /**
201
+ * 旋转的最大角速度
202
+ * @default 2160
203
+ */
204
+ maxVel?: number;
205
+ /**
206
+ * 缓停时长
207
+ * @default 1500
208
+ */
209
+ decelDuration?: number;
189
210
  }
190
211
  interface ArticleConfig {
191
212
  /**
package/node.js CHANGED
@@ -41,7 +41,7 @@ module.exports = __toCommonJS(node_exports);
41
41
  // src/utils/node/mdPlugins.ts
42
42
  var import_module = require("module");
43
43
 
44
- // ../../node_modules/.pnpm/vitepress-plugin-tabs@0.2.0_vitepress@https+++pkg.pr.new+vitepress@3d61619_@types+node@_fc84fc5ced1bdf672a18932976f96d0a/node_modules/vitepress-plugin-tabs/dist/index.js
44
+ // ../../node_modules/.pnpm/vitepress-plugin-tabs@0.2.0_vitepress@2.0.0-alpha.15_@types+node@24.5.2_async-validator_06f14e686e1fb3002156f3936c305549/node_modules/vitepress-plugin-tabs/dist/index.js
45
45
  var tabsMarker = "=tabs";
46
46
  var tabsMarkerLen = tabsMarker.length;
47
47
  var ruleBlockTabs = (state, startLine, endLine, silent) => {
@@ -235,7 +235,7 @@ function getFirstImagURLFromMD(content, route) {
235
235
  const paths = (0, import_theme_shared.joinPath)("/", route).split("/");
236
236
  paths.splice(paths.length - 1, 1);
237
237
  const relativePath = url.startsWith("/") ? url : import_node_path.default.join(paths.join("/") || "", url);
238
- return (0, import_theme_shared.joinPath)("/", relativePath);
238
+ return (0, import_theme_shared.joinPath)("/", (0, import_theme_shared.normalizePath)(relativePath));
239
239
  }
240
240
  function debounce(func, delay = 1e3) {
241
241
  let timeoutId;
@@ -549,12 +549,12 @@ function themeReloadPlugin() {
549
549
  function getVitePlugins(cfg = {}) {
550
550
  const plugins = [];
551
551
  plugins.push(cacheAllGitTimestampsPlugin());
552
- plugins.push(coverImgTransform());
553
552
  if (cfg.themeColor) {
554
553
  plugins.push(setThemeScript(cfg.themeColor));
555
554
  }
556
555
  plugins.push(themeReloadPlugin());
557
556
  plugins.push(providePageData(cfg));
557
+ plugins.push(coverImgTransform());
558
558
  if (cfg && cfg.search !== false) {
559
559
  const ops = cfg.search instanceof Object ? cfg.search : {};
560
560
  plugins.push(
@@ -638,13 +638,16 @@ function coverImgTransform() {
638
638
  const value = v.meta[k];
639
639
  if (value && typeof value === "string" && value.startsWith("/")) {
640
640
  const absolutePath = `${vitepressConfig.srcDir}${value}`;
641
- if (relativeMetaMap[absolutePath]) {
642
- Object.assign(v.meta, { [k]: relativeMetaMap[absolutePath][k] });
643
- return;
641
+ if (relativePathMap[absolutePath]) {
642
+ Object.assign(v.meta, {
643
+ [k]: relativeMetaMap[absolutePath][k]
644
+ });
644
645
  }
645
646
  relativePathMap[value] = absolutePath;
646
647
  relativePathMap[absolutePath] = value;
647
- relativeMeta.push(v.meta);
648
+ if (!relativeMeta.includes(v.meta)) {
649
+ relativeMeta.push(v.meta);
650
+ }
648
651
  relativeMetaMap[absolutePath] = v.meta;
649
652
  }
650
653
  });
package/node.mjs CHANGED
@@ -9,7 +9,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
9
9
  // src/utils/node/mdPlugins.ts
10
10
  import { createRequire } from "module";
11
11
 
12
- // ../../node_modules/.pnpm/vitepress-plugin-tabs@0.2.0_vitepress@https+++pkg.pr.new+vitepress@3d61619_@types+node@_fc84fc5ced1bdf672a18932976f96d0a/node_modules/vitepress-plugin-tabs/dist/index.js
12
+ // ../../node_modules/.pnpm/vitepress-plugin-tabs@0.2.0_vitepress@2.0.0-alpha.15_@types+node@24.5.2_async-validator_06f14e686e1fb3002156f3936c305549/node_modules/vitepress-plugin-tabs/dist/index.js
13
13
  var tabsMarker = "=tabs";
14
14
  var tabsMarkerLen = tabsMarker.length;
15
15
  var ruleBlockTabs = (state, startLine, endLine, silent) => {
@@ -179,7 +179,7 @@ import { groupIconMdPlugin } from "vitepress-plugin-group-icons";
179
179
 
180
180
  // src/utils/node/index.ts
181
181
  import path from "node:path";
182
- import { joinPath } from "@sugarat/theme-shared";
182
+ import { joinPath, normalizePath } from "@sugarat/theme-shared";
183
183
  function aliasObjectToArray(obj) {
184
184
  return Object.entries(obj).map(([find, replacement]) => ({
185
185
  find,
@@ -203,7 +203,7 @@ function getFirstImagURLFromMD(content, route) {
203
203
  const paths = joinPath("/", route).split("/");
204
204
  paths.splice(paths.length - 1, 1);
205
205
  const relativePath = url.startsWith("/") ? url : path.join(paths.join("/") || "", url);
206
- return joinPath("/", relativePath);
206
+ return joinPath("/", normalizePath(relativePath));
207
207
  }
208
208
  function debounce(func, delay = 1e3) {
209
209
  let timeoutId;
@@ -301,7 +301,7 @@ function patchOptimizeDeps(config) {
301
301
 
302
302
  // src/utils/node/theme.ts
303
303
  import fs from "node:fs";
304
- import { getDefaultTitle, getFileLastModifyTime, getTextSummary, getVitePressPages, grayMatter, normalizePath, renderDynamicMarkdown } from "@sugarat/theme-shared";
304
+ import { getDefaultTitle, getFileLastModifyTime, getTextSummary, getVitePressPages, grayMatter, normalizePath as normalizePath2, renderDynamicMarkdown } from "@sugarat/theme-shared";
305
305
 
306
306
  // src/utils/client/index.ts
307
307
  function formatDate(d, fmt = "yyyy-MM-dd hh:mm:ss") {
@@ -517,12 +517,12 @@ function themeReloadPlugin() {
517
517
  function getVitePlugins(cfg = {}) {
518
518
  const plugins = [];
519
519
  plugins.push(cacheAllGitTimestampsPlugin());
520
- plugins.push(coverImgTransform());
521
520
  if (cfg.themeColor) {
522
521
  plugins.push(setThemeScript(cfg.themeColor));
523
522
  }
524
523
  plugins.push(themeReloadPlugin());
525
524
  plugins.push(providePageData(cfg));
525
+ plugins.push(coverImgTransform());
526
526
  if (cfg && cfg.search !== false) {
527
527
  const ops = cfg.search instanceof Object ? cfg.search : {};
528
528
  plugins.push(
@@ -606,13 +606,16 @@ function coverImgTransform() {
606
606
  const value = v.meta[k];
607
607
  if (value && typeof value === "string" && value.startsWith("/")) {
608
608
  const absolutePath = `${vitepressConfig.srcDir}${value}`;
609
- if (relativeMetaMap[absolutePath]) {
610
- Object.assign(v.meta, { [k]: relativeMetaMap[absolutePath][k] });
611
- return;
609
+ if (relativePathMap[absolutePath]) {
610
+ Object.assign(v.meta, {
611
+ [k]: relativeMetaMap[absolutePath][k]
612
+ });
612
613
  }
613
614
  relativePathMap[value] = absolutePath;
614
615
  relativePathMap[absolutePath] = value;
615
- relativeMeta.push(v.meta);
616
+ if (!relativeMeta.includes(v.meta)) {
617
+ relativeMeta.push(v.meta);
618
+ }
616
619
  relativeMetaMap[absolutePath] = v.meta;
617
620
  }
618
621
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sugarat/theme",
3
- "version": "0.5.7",
3
+ "version": "0.5.9",
4
4
  "description": "简约风的 Vitepress 博客主题,sugarat vitepress blog theme",
5
5
  "author": "sugar",
6
6
  "license": "MIT",
@@ -53,10 +53,10 @@
53
53
  "vitepress-plugin-group-icons": "1.2.4",
54
54
  "vitepress-plugin-mermaid": "2.0.13",
55
55
  "vitepress-plugin-tabs": "0.2.0",
56
- "vitepress-plugin-pagefind": "0.4.15",
56
+ "@sugarat/theme-shared": "0.0.6",
57
+ "vitepress-plugin-rss": "0.3.4",
57
58
  "vitepress-plugin-announcement": "0.1.5",
58
- "vitepress-plugin-rss": "0.3.2",
59
- "@sugarat/theme-shared": "0.0.6"
59
+ "vitepress-plugin-pagefind": "0.4.16"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@element-plus/icons-vue": "^2.3.1",
@@ -66,7 +66,7 @@
66
66
  "sass": "^1.80.6",
67
67
  "typescript": "^5.4.5",
68
68
  "vite": "^5.4.9",
69
- "vitepress": "https://pkg.pr.new/vitepress@3d61619",
69
+ "vitepress": "2.0.0-alpha.15",
70
70
  "vue": "^3.5.12",
71
71
  "vitepress-plugin-51la": "0.1.0"
72
72
  },
@@ -1,7 +1,8 @@
1
1
  <script setup>
2
2
  import { useData, withBase } from 'vitepress'
3
- import { computed } from 'vue'
4
- import { useBlogConfig, useGlobalAuthor, useHomeConfig } from '../composables/config/blog'
3
+ import { computed, ref } from 'vue'
4
+ import { useGlobalAuthor, useHomeConfig } from '../composables/config/blog'
5
+ import { useHoverSpin } from '../hooks/useHoverSpin'
5
6
 
6
7
  const home = useHomeConfig()
7
8
  const { frontmatter, site } = useData()
@@ -20,11 +21,15 @@ const logo = computed(() =>
20
21
  ?? '/logo.png'
21
22
  )
22
23
  const show = computed(() => author.value || logo.value)
24
+
25
+ const imgRef = ref(null)
26
+
27
+ useHoverSpin(imgRef, home?.value?.hoverSpin)
23
28
  </script>
24
29
 
25
30
  <template>
26
31
  <div v-if="show" class="blog-author">
27
- <img v-if="logo" :src="withBase(logo)" alt="avatar">
32
+ <img v-if="logo" ref="imgRef" :src="withBase(logo)" alt="avatar">
28
33
  <p v-if="author">
29
34
  {{ author }}
30
35
  </p>
@@ -42,12 +47,7 @@ const show = computed(() => author.value || logo.value)
42
47
  height: 100px;
43
48
  border-radius: 50%;
44
49
  background-color: rgba(var(--bg-gradient-home));
45
- }
46
-
47
- img:hover {
48
- transform: rotate(666turn);
49
- transition-duration: 59s;
50
- transition-timing-function: cubic-bezier(.34, 0, .84, 1)
50
+ cursor: pointer;
51
51
  }
52
52
 
53
53
  p {
@@ -1,7 +1,8 @@
1
1
  <script setup lang="ts">
2
2
  import { useData, withBase } from 'vitepress'
3
- import { computed } from 'vue'
3
+ import { computed, ref } from 'vue'
4
4
  import { useHomeConfig } from '../composables/config/blog'
5
+ import { useHoverSpin } from '../hooks/useHoverSpin'
5
6
 
6
7
  const home = useHomeConfig()
7
8
  const { frontmatter, site } = useData()
@@ -13,11 +14,14 @@ const logo = computed(() =>
13
14
  ?? '/logo.png'
14
15
  )
15
16
  const alwaysHide = computed(() => frontmatter.value.blog?.minScreenAvatar === false)
17
+
18
+ const imgRef = ref(null)
19
+ useHoverSpin(imgRef, home?.value?.hoverSpin)
16
20
  </script>
17
21
 
18
22
  <template>
19
23
  <div v-show="!alwaysHide" class="blog-home-header-avatar">
20
- <img :src="withBase(logo)" alt="avatar">
24
+ <img ref="imgRef" :src="withBase(logo)" alt="avatar">
21
25
  </div>
22
26
  </template>
23
27
 
@@ -36,13 +40,14 @@ const alwaysHide = computed(() => frontmatter.value.blog?.minScreenAvatar === fa
36
40
  background-color: transparent;
37
41
  border: 5px solid rgba(var(--bg-gradient-home));
38
42
  box-sizing: border-box;
43
+ cursor: pointer;
39
44
  }
40
45
 
41
- img:hover {
42
- transform: rotate(666turn);
43
- transition-duration: 59s;
44
- transition-timing-function: cubic-bezier(.34, 0, .84, 1)
45
- }
46
+ // img:hover {
47
+ // transform: rotate(666turn);
48
+ // transition-duration: 59s;
49
+ // transition-timing-function: cubic-bezier(.34, 0, .84, 1)
50
+ // }
46
51
  }
47
52
 
48
53
  @media screen and (min-width: 768px) {
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { computed, watch } from 'vue'
2
+ import { computed, nextTick, watch } from 'vue'
3
3
  import { ElPagination } from 'element-plus'
4
4
  import { useData, useRoute, useRouter } from 'vitepress'
5
5
  import {
@@ -70,7 +70,6 @@ function handleUpdatePageNum(current: number) {
70
70
  }
71
71
 
72
72
  const route = useRoute()
73
-
74
73
  function refreshCurrentPage() {
75
74
  if (typeof window === 'undefined')
76
75
  return
@@ -78,7 +77,9 @@ function refreshCurrentPage() {
78
77
  const searchParams = new URLSearchParams(search)
79
78
  const pageNum = Number(searchParams.get(queryPageNumKey)) || 1
80
79
  if (pageNum !== currentPage.value) {
81
- currentPage.value = pageNum
80
+ nextTick(() => {
81
+ currentPage.value = pageNum
82
+ })
82
83
  }
83
84
  }
84
85
  watch(route, () => {
@@ -86,7 +87,7 @@ watch(route, () => {
86
87
  }, { immediate: true })
87
88
 
88
89
  // 未覆盖的场景处理 左上回到首页
89
- router.onAfterRouteChanged = () => {
90
+ router.onAfterRouteChange = () => {
90
91
  refreshCurrentPage()
91
92
  }
92
93
  </script>
@@ -191,6 +191,28 @@ export namespace Theme {
191
191
  * @default false
192
192
  */
193
193
  blogInfoCollapsible?: boolean
194
+ /**
195
+ * 旋转的配置
196
+ */
197
+ hoverSpin?: boolean | HoverSpinConfig
198
+ }
199
+
200
+ export interface HoverSpinConfig {
201
+ /**
202
+ * 旋转的加速度
203
+ * @default 180
204
+ */
205
+ accel?: number
206
+ /**
207
+ * 旋转的最大角速度
208
+ * @default 2160
209
+ */
210
+ maxVel?: number
211
+ /**
212
+ * 缓停时长
213
+ * @default 1500
214
+ */
215
+ decelDuration?: number
194
216
  }
195
217
 
196
218
  export interface ArticleConfig {
@@ -0,0 +1,114 @@
1
+ import { type Ref, onMounted, onUnmounted } from 'vue'
2
+ import type { Theme } from '../composables/config/index'
3
+
4
+ /**
5
+ * 元素悬停旋转
6
+ * @param elRef 元素引用
7
+ * @param accel 加速度
8
+ * @param maxVel 最大角速度
9
+ * @param decelDuration 缓停时长
10
+ */
11
+ export function useHoverSpin(elRef: Ref<HTMLElement | null>, hoverSpinConfig?: Theme.HoverSpinConfig | boolean) {
12
+ const { accel = 180, maxVel = 2160, decelDuration = 1500 } = hoverSpinConfig === true ? {} : hoverSpinConfig || {}
13
+ let rafId = 0
14
+ let lastTs = 0
15
+ let rotation = 0
16
+ let velocity = 0
17
+ let decelStart = 0
18
+ let decelStartRotation = 0
19
+ let decelTargetRotation = 0
20
+ let decelBias = 0
21
+ let isDecel = false
22
+ let hovering = false
23
+
24
+ function animate(ts: number) {
25
+ if (!lastTs)
26
+ lastTs = ts
27
+ const dt = (ts - lastTs) / 1000
28
+ lastTs = ts
29
+
30
+ if (hovering) {
31
+ isDecel = false
32
+ decelStart = 0
33
+ velocity = Math.min(velocity + accel * dt, maxVel)
34
+ rotation += velocity * dt
35
+ }
36
+ else if (isDecel) {
37
+ const t = Math.min((ts - decelStart) / decelDuration, 1)
38
+ const durSec = decelDuration / 1000
39
+ const base = velocity * durSec * (t - t * t / 2)
40
+ const smooth = 3 * t * t - 2 * t * t * t
41
+ rotation = decelStartRotation + base + decelBias * smooth
42
+ if (t >= 1) {
43
+ decelTargetRotation = 0
44
+ decelStartRotation = 0
45
+ decelBias = 0
46
+ rotation = decelTargetRotation
47
+ velocity = 0
48
+ isDecel = false
49
+ if (rafId) {
50
+ cancelAnimationFrame(rafId)
51
+ rafId = 0
52
+ }
53
+ const el2 = elRef.value as HTMLElement | null
54
+ if (el2)
55
+ el2.style.transform = `rotate(${rotation}deg)`
56
+ return
57
+ }
58
+ }
59
+
60
+ const el = elRef.value as HTMLElement | null
61
+ if (el)
62
+ el.style.transform = `rotate(${rotation}deg)`
63
+ if (hovering || isDecel) {
64
+ rafId = requestAnimationFrame(animate)
65
+ }
66
+ else if (rafId) {
67
+ cancelAnimationFrame(rafId)
68
+ rafId = 0
69
+ }
70
+ }
71
+
72
+ function onEnter() {
73
+ hovering = true
74
+ if (!rafId) {
75
+ lastTs = 0
76
+ rafId = requestAnimationFrame(animate)
77
+ }
78
+ }
79
+
80
+ function onLeave() {
81
+ hovering = false
82
+ isDecel = true
83
+ decelStart = performance.now()
84
+ decelStartRotation = rotation
85
+ const durSec = decelDuration / 1000
86
+ const S = velocity * durSec / 2
87
+ const minTarget = decelStartRotation + S
88
+ decelTargetRotation = Math.ceil(minTarget / 360) * 360
89
+ decelBias = (decelTargetRotation - decelStartRotation) - S
90
+ lastTs = 0
91
+ }
92
+
93
+ onMounted(() => {
94
+ if (hoverSpinConfig === false)
95
+ return
96
+ const el = elRef.value
97
+ if (!el)
98
+ return
99
+ el.addEventListener('mouseenter', onEnter)
100
+ el.addEventListener('mouseleave', onLeave)
101
+ })
102
+
103
+ onUnmounted(() => {
104
+ if (hoverSpinConfig === false)
105
+ return
106
+ const el = elRef.value
107
+ if (el) {
108
+ el.removeEventListener('mouseenter', onEnter)
109
+ el.removeEventListener('mouseleave', onLeave)
110
+ }
111
+ if (rafId)
112
+ cancelAnimationFrame(rafId)
113
+ })
114
+ }
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path'
2
- import { joinPath } from '@sugarat/theme-shared'
2
+ import { joinPath, normalizePath } from '@sugarat/theme-shared'
3
3
 
4
4
  export function aliasObjectToArray(obj: Record<string, string>) {
5
5
  return Object.entries(obj).map(([find, replacement]) => ({
@@ -34,8 +34,7 @@ export function getFirstImagURLFromMD(content: string, route: string) {
34
34
  const paths = joinPath('/', route).split('/')
35
35
  paths.splice(paths.length - 1, 1)
36
36
  const relativePath = url.startsWith('/') ? url : path.join(paths.join('/') || '', url)
37
-
38
- return joinPath('/', relativePath)
37
+ return joinPath('/', normalizePath(relativePath))
39
38
  }
40
39
 
41
40
  export function debounce(func: any, delay = 1000) {
@@ -65,7 +65,7 @@ export async function getArticleMeta(filepath: string, route: string, timeZone =
65
65
  ])
66
66
 
67
67
  // 获取摘要信息
68
- // TODO:摘要生成优化
68
+ // TODO:摘要生成优化,支持AI?
69
69
  meta.description
70
70
  = meta.description || getTextSummary(content, 100) || excerpt
71
71
 
@@ -18,9 +18,6 @@ export function getVitePlugins(cfg: Partial<Theme.BlogConfig> = {}) {
18
18
  // 缓存所有文章的 git 提交时间
19
19
  plugins.push(cacheAllGitTimestampsPlugin())
20
20
 
21
- // 处理 cover image 的路径(暂只支持自动识别的文章首图)
22
- plugins.push(coverImgTransform())
23
-
24
21
  // 处理自定义主题色
25
22
  if (cfg.themeColor) {
26
23
  plugins.push(setThemeScript(cfg.themeColor))
@@ -31,6 +28,8 @@ export function getVitePlugins(cfg: Partial<Theme.BlogConfig> = {}) {
31
28
  // 主题 pageData生成
32
29
  plugins.push(providePageData(cfg))
33
30
 
31
+ // 处理 cover image 的路径(暂只支持自动识别的文章首图)
32
+ plugins.push(coverImgTransform())
34
33
  // 内置 pagefind
35
34
  if (cfg && cfg.search !== false) {
36
35
  const ops = cfg.search instanceof Object ? cfg.search : {}
@@ -165,15 +164,18 @@ export function coverImgTransform() {
165
164
  if (value && typeof value === 'string' && value.startsWith('/')) {
166
165
  const absolutePath = `${vitepressConfig.srcDir}${value}`
167
166
 
168
- // 复用已经映射后的值
169
- if (relativeMetaMap[absolutePath]) {
170
- Object.assign(v.meta, { [k]: relativeMetaMap[absolutePath][k] })
171
- return
167
+ // 已经映射后的值
168
+ if (relativePathMap[absolutePath]) {
169
+ Object.assign(v.meta, {
170
+ [k]: relativeMetaMap[absolutePath][k]
171
+ })
172
172
  }
173
173
 
174
174
  relativePathMap[value] = absolutePath
175
175
  relativePathMap[absolutePath] = value
176
- relativeMeta.push(v.meta)
176
+ if (!relativeMeta.includes(v.meta)) {
177
+ relativeMeta.push(v.meta)
178
+ }
177
179
  relativeMetaMap[absolutePath] = v.meta
178
180
  }
179
181
  })