hexo-theme-stellar 1.37.0 → 1.38.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 (67) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/_config.yml +13 -8
  3. package/languages/en.yml +37 -0
  4. package/languages/zh-CN.yml +37 -0
  5. package/languages/zh-TW.yml +37 -0
  6. package/layout/_partial/comments/layout.ejs +2 -1
  7. package/layout/_partial/main/navbar/article_banner.ejs +11 -5
  8. package/layout/_partial/main/post_list/post_card.ejs +1 -1
  9. package/layout/_partial/scripts/defines.ejs +2 -1
  10. package/layout/_partial/scripts/utils.ejs +58 -15
  11. package/layout/_partial/sidebar/grad-def.ejs +26 -0
  12. package/layout/_partial/sidebar/index_leftbar.ejs +25 -10
  13. package/layout/_partial/sidebar/logo.ejs +21 -0
  14. package/layout/_partial/sidebar/menu.ejs +11 -5
  15. package/layout/_partial/widgets/toc.ejs +12 -8
  16. package/layout/_plugins/scrollreveal.ejs +17 -2
  17. package/layout/index.ejs +27 -1
  18. package/layout/notebooks.ejs +3 -1
  19. package/layout/notes.ejs +3 -1
  20. package/package.json +1 -1
  21. package/scripts/tags/index.js +1 -0
  22. package/scripts/tags/lib/mbti.js +169 -0
  23. package/source/css/_common/canonical.styl +9 -10
  24. package/source/css/_common/highlight.styl +0 -2
  25. package/source/css/_common/html.styl +2 -1
  26. package/source/css/_common/svg.styl +9 -1
  27. package/source/css/_components/list.styl +36 -16
  28. package/source/css/_components/md.styl +0 -1
  29. package/source/css/_components/pages/article-story.styl +1 -3
  30. package/source/css/_components/partial/article-banner.styl +17 -11
  31. package/source/css/_components/partial/article-footer.styl +0 -1
  32. package/source/css/_components/partial/navbar.styl +3 -3
  33. package/source/css/_components/sidebar/logo.styl +26 -0
  34. package/source/css/_components/sidebar/menu.styl +53 -30
  35. package/source/css/_components/sidebar/nav-area.styl +1 -1
  36. package/source/css/_components/sidebar/search.styl +46 -7
  37. package/source/css/_components/sidebar/sidebar.styl +5 -1
  38. package/source/css/_components/tag-plugins/banner.styl +2 -1
  39. package/source/css/_components/tag-plugins/chat.styl +3 -1
  40. package/source/css/_components/tag-plugins/folding.styl +1 -2
  41. package/source/css/_components/tag-plugins/friends.styl +1 -0
  42. package/source/css/_components/tag-plugins/friends_posts.styl +2 -1
  43. package/source/css/_components/tag-plugins/gallery.styl +1 -2
  44. package/source/css/_components/tag-plugins/grid.styl +1 -2
  45. package/source/css/_components/tag-plugins/image.styl +0 -2
  46. package/source/css/_components/tag-plugins/link.styl +0 -1
  47. package/source/css/_components/tag-plugins/mbti.styl +80 -0
  48. package/source/css/_components/tag-plugins/note.styl +1 -2
  49. package/source/css/_components/tag-plugins/okr.styl +1 -2
  50. package/source/css/_components/tag-plugins/sites.styl +0 -1
  51. package/source/css/_components/tag-plugins/timeline.styl +1 -6
  52. package/source/css/_components/widgets/components.styl +14 -7
  53. package/source/css/_components/widgets/ghuser.styl +2 -1
  54. package/source/css/_components/widgets/list.styl +20 -8
  55. package/source/css/_components/widgets/markdown.styl +4 -1
  56. package/source/css/_components/widgets/related.styl +6 -4
  57. package/source/css/_components/widgets/toc.styl +2 -2
  58. package/source/css/_components/widgets/widgets.styl +6 -9
  59. package/source/css/_custom.styl +2 -1
  60. package/source/css/_defines/const.styl +1 -1
  61. package/source/css/_defines/func.styl +13 -1
  62. package/source/css/_defines/theme_base.styl +22 -1
  63. package/source/css/_plugins/comments/artalk.styl +2 -2
  64. package/source/css/_plugins/comments/twikoo.styl +0 -2
  65. package/source/css/_plugins/tianli_gpt.styl +3 -0
  66. package/source/js/main.js +39 -17
  67. package/source/js/services/memos.js +12 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.38.0
4
+
5
+ > 发布日期:2026-08-11
6
+
7
+ ### 新功能
8
+ - 新增连续曲率圆角配置 `style.corner-shape`(默认 `superellipse(1.2)`):所有带圆角的位置统一使用曲率连续的 squircle 曲线,自动适配容器宽高;文章卡片与横幅的渐变模糊改为「同图模糊层」实现以兼容连续曲率(仅 Chromium 139+ 原生支持,其余浏览器自动回退普通圆角,可设 `round` 关闭)
9
+ - 左栏导航列表化,wiki 内页增加返回按钮与搜索条交互优化
10
+ - 菜单栏激活/悬停图标与圆点支持可配置角度的主题色渐变(`style.gradient.angle`)
11
+ - 首页支持可配置评论区(默认关闭,`site_tree.home.comments`)
12
+ - 新增 mbti 标签
13
+
14
+ ### 修复
15
+ - 补全 npm-publish workflow 的 GH_TOKEN,修复 GitHub Release 创建失败
16
+ - GitHub Release 正文不再包含 CHANGELOG 发布日期行
17
+ - 克隆站提示源站从 encoded 反解,防止批量替换域名误导
18
+ - 侧栏内部滚动容器组件 scrollreveal 不显示
19
+ - 仅当页面包含 toc 或评论时显示 toc 操作按钮
20
+ - wiki 封面「开始使用」按钮平滑滚动,#start 锚点贴顶定位
21
+
22
+ ### 重构
23
+ - 移除官方备用站提示的关闭按钮与配置项
24
+
25
+ ### 优化
26
+ - 动态数据组件缓存优化:同 URL 请求去重、memos 用户详情缓存、stale 兜底
27
+
28
+ ### 样式
29
+ - 侧栏光照效果覆盖相关卡片、返回胶囊与 widget 操作按钮
30
+ - 侧栏导航区边距调整
31
+ - 左栏激活书签等彩色图标使用主题色渐变
32
+ - 侧栏高亮项顶部叠加白色光照渐变与高光边
33
+ - 左侧栏底部留白调整为 32px
34
+ - 作者页与错误页侧栏不再显示时间线
35
+ - 移动端顶部导航吸顶距离调整为 8px
36
+
37
+ ### 升级注意(配置变更与破坏性改动)
38
+ - 新增 `style.corner-shape`(默认 `superellipse(1.2)`):连续曲率圆角仅 Chromium 139+ 生效,其余浏览器自动回退普通圆角;设置 `round` 可关闭
39
+ - 新增 `style.gradient.angle` 与 `site_tree.home.comments` 配置
40
+ - 移除 `canonical.closeEnable`、`canonical.closeText` 配置项:官方备用站提示不再支持手动关闭
41
+
42
+ Full Changelog: [1.37.0...1.38.0](https://github.com/xaoxuu/hexo-theme-stellar/compare/1.37.0...1.38.0)
43
+
3
44
  ## 1.37.0
4
45
 
5
46
  > 发布日期:2026-08-09
package/_config.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  ######## Stellar info ########
2
2
  stellar:
3
- version: '1.37.0'
3
+ version: '1.38.0'
4
4
  homepage: 'https://xaoxuu.com/wiki/stellar/'
5
5
  repo: 'https://github.com/xaoxuu/hexo-theme-stellar'
6
6
  main_css: /css/main.css
@@ -17,8 +17,6 @@ preconnect:
17
17
  # 一旦设置源站地址,非源站地址将不会被SEO收录,并且访问时弹出提示
18
18
  # 如果访问地址不在备用站主机列表,则警告信息为非法克隆
19
19
  canonical:
20
- closeEnable: true # 是否显示关闭提示按钮
21
- closeText: 关闭提示 # 关闭提示按钮的文字描述
22
20
  originalHost: # 主站点域名主机,例如 xaoxuu.com
23
21
  officialHosts: # 官方主机列表,每行一个,例如 xaoxuu.vercel.app
24
22
  - localhost
@@ -40,7 +38,6 @@ logo:
40
38
 
41
39
  # 侧边栏主功能导航菜单
42
40
  menubar:
43
- columns: 4 # 一行多少个
44
41
  items: # 可按照自己需求增加,符合以下格式即可
45
42
  # id: 页面中高亮的 menu_id
46
43
  # theme: 高亮时的颜色,仅 svg 中 fill="currentColor" 时有效
@@ -77,6 +74,10 @@ site_tree:
77
74
  home:
78
75
  leftbar: welcome, recent
79
76
  rightbar:
77
+ # 首页评论区,默认不显示。可设为 true 使用全局评论服务,
78
+ # 或参考页面 front-matter 覆盖评论参数:
79
+ # service、comment_title、comment_id 及 beaudar/utterances/giscus/twikoo/waline/artalk 各服务参数。
80
+ comments:
80
81
  # 博客列表页配置
81
82
  index_blog:
82
83
  base_dir: blog # 只影响自动生成的页面路径
@@ -138,14 +139,14 @@ site_tree:
138
139
  author:
139
140
  base_dir: author # 只影响自动生成的页面路径
140
141
  menu_id: post
141
- leftbar: recent, timeline
142
- rightbar: timeline
142
+ leftbar: recent
143
+ rightbar:
143
144
  # 错误页配置
144
145
  error_page:
145
146
  menu_id: post
146
147
  '404': '/404.html'
147
- leftbar: recent, timeline
148
- rightbar: timeline
148
+ leftbar: recent
149
+ rightbar:
149
150
  # 其它自定义页面配置 layout: page
150
151
  page:
151
152
  leftbar: recent
@@ -530,6 +531,7 @@ data_cache:
530
531
  waline: 600
531
532
  artalk: 600
532
533
  memos: 600
534
+ memos-user: 86400
533
535
  sites: 86400
534
536
  friends: 86400
535
537
  friends_and_posts: 86400
@@ -672,6 +674,7 @@ style:
672
674
  image-l: 24px # 非技术类文章插图圆角
673
675
  image: 16px # 技术类文章插图圆角
674
676
  image-s: 8px # 评论区图片圆角(部分评论支持)
677
+ corner-shape: superellipse(1.5) # 连续曲率圆角(仅 Chromium 139+ 支持,其余浏览器自动回退普通圆角):superellipse(1.x) 开启,round 关闭
675
678
  color:
676
679
  # 主题色配置(不会根据明暗动态调整,请设置为通用的颜色)
677
680
  theme: 'hsl(192 98% 55%)' # 主题色
@@ -688,6 +691,8 @@ style:
688
691
  error: 加载失败,请稍后重试。
689
692
  gradient: # https://webgradients.com/
690
693
  start: 'linear-gradient(to right, hsl(215, 95%, 64%), hsl(195, 95%, 60%), hsl(165, 95%, 56%), hsl(165, 95%, 56%), hsl(195 95% 60%), hsl(215, 95%, 64%))'
694
+ searchbar: 'linear-gradient(to right, #04f3ff, #08ffc6, #ddf730, #ffbd19, #ff1fe0, #c418ff, #3b5bff, #04f3ff)' # 搜索条激活时底部横条的渐变色(彩虹,收尾经蓝色平滑过渡)
695
+ angle: 210deg # 主题色渐变的角度(0=向上,顺时针;225deg = 右上角 → 左下角)
691
696
  leftbar:
692
697
  # 可以设置:纯色/渐变色/图片作为背景
693
698
  # 设置为纯色时,可以分别设置浅色和深色模式背景色
package/languages/en.yml CHANGED
@@ -68,3 +68,40 @@ symbol:
68
68
  colon: ": "
69
69
  brackets_l: "("
70
70
  brackets_r: ")"
71
+
72
+ tag_plugins:
73
+ mbti:
74
+ title: 'My MBTI Type: %s (%s)'
75
+ types:
76
+ INTJ: Architect
77
+ INTP: Logician
78
+ ENTJ: Commander
79
+ ENTP: Debater
80
+ INFJ: Advocate
81
+ INFP: Mediator
82
+ ENFJ: Protagonist
83
+ ENFP: Campaigner
84
+ ISTJ: Logistician
85
+ ISFJ: Defender
86
+ ESTJ: Executive
87
+ ESFJ: Consul
88
+ ISTP: Virtuoso
89
+ ISFP: Adventurer
90
+ ESTP: Entrepreneur
91
+ ESFP: Entertainer
92
+ traits:
93
+ energy:
94
+ left: Extraverted
95
+ right: Introverted
96
+ mind:
97
+ left: Intuitive
98
+ right: Observant
99
+ nature:
100
+ left: Thinking
101
+ right: Feeling
102
+ tactics:
103
+ left: Judging
104
+ right: Prospecting
105
+ identity:
106
+ left: Assertive
107
+ right: Turbulent
@@ -68,3 +68,40 @@ symbol:
68
68
  colon: ":"
69
69
  brackets_l: "("
70
70
  brackets_r: ")"
71
+
72
+ tag_plugins:
73
+ mbti:
74
+ title: 我的 MBTI 人格:%s(%s)
75
+ types:
76
+ INTJ: 架构师
77
+ INTP: 逻辑学家
78
+ ENTJ: 指挥官
79
+ ENTP: 辩论家
80
+ INFJ: 提倡者
81
+ INFP: 调停者
82
+ ENFJ: 主人公
83
+ ENFP: 活动家
84
+ ISTJ: 物流师
85
+ ISFJ: 守卫者
86
+ ESTJ: 管理者
87
+ ESFJ: 执政官
88
+ ISTP: 鉴赏家
89
+ ISFP: 冒险家
90
+ ESTP: 企业家
91
+ ESFP: 表演者
92
+ traits:
93
+ energy:
94
+ left: 外向
95
+ right: 内向
96
+ mind:
97
+ left: 天马行空
98
+ right: 求真务实
99
+ nature:
100
+ left: 理性思考
101
+ right: 情感细腻
102
+ tactics:
103
+ left: 运筹帷幄
104
+ right: 随机应变
105
+ identity:
106
+ left: 自信果断
107
+ right: 情绪易波动
@@ -68,3 +68,40 @@ symbol:
68
68
  colon: ":"
69
69
  brackets_l: "("
70
70
  brackets_r: ")"
71
+
72
+ tag_plugins:
73
+ mbti:
74
+ title: 我的 MBTI 人格:%s(%s)
75
+ types:
76
+ INTJ: 架構師
77
+ INTP: 邏輯學家
78
+ ENTJ: 指揮官
79
+ ENTP: 辯論家
80
+ INFJ: 提倡者
81
+ INFP: 調停者
82
+ ENFJ: 主人公
83
+ ENFP: 活動家
84
+ ISTJ: 物流師
85
+ ISFJ: 守衛者
86
+ ESTJ: 管理者
87
+ ESFJ: 執政官
88
+ ISTP: 鑑賞家
89
+ ISFP: 冒險家
90
+ ESTP: 企業家
91
+ ESFP: 表演者
92
+ traits:
93
+ energy:
94
+ left: 外向
95
+ right: 內向
96
+ mind:
97
+ left: 天馬行空
98
+ right: 求真務實
99
+ nature:
100
+ left: 理性思考
101
+ right: 情感細膩
102
+ tactics:
103
+ left: 運籌帷幄
104
+ right: 隨機應變
105
+ identity:
106
+ left: 自信果斷
107
+ right: 情緒易波動
@@ -6,7 +6,7 @@ if (cmt.service) {
6
6
  cmt[cmt.service] = Object.assign({}, theme.comments[cmt.service]);
7
7
  }
8
8
  var loadComment = false;
9
- if (theme.comments.service && theme.comments.service.length > 0) {
9
+ if (cmt.service && cmt.service.length > 0) {
10
10
  if (page.comments == undefined || page.comments != false) {
11
11
  loadComment = true;
12
12
  }
@@ -34,6 +34,7 @@ if (cmt.service && page[cmt.service]) {
34
34
  Object.assign(cmt[cmt.service], page[cmt.service]);
35
35
  }
36
36
  page.cmt = cmt;
37
+ page.cmt_rendered = loadComment;
37
38
  %>
38
39
  <% if (loadComment) { %>
39
40
  <div class="related-wrap md-text<%- scrollreveal(' ') %>" id="comments">
@@ -105,23 +105,29 @@ function layoutDiv() {
105
105
  return ``
106
106
  }
107
107
  const top = layoutBreadcrumb()
108
- var style = ``
108
+ var bannerStyle = ``
109
+ var contentStyle = ``
109
110
  var el = ``
110
- el += `<div class="article banner${scrollreveal(' ')} top">`
111
+ if (banner.url?.length > 0) {
112
+ bannerStyle += ` style="--bg-url:url('${banner.url.replace(/'/g, '%27')}')"`
113
+ }
114
+ el += `<div class="article-banner-wrap${scrollreveal(' ')}">`
115
+ el += `<div class="article banner top"${bannerStyle}>`
111
116
  if (banner.url?.length > 0) {
112
117
  el += `<img class="lazy bg" data-src="${banner.url}">`
113
118
  if (banner.color) {
114
- style += ' style="--text-banner:' + banner.color + '"'
119
+ contentStyle += ' style="--text-banner:' + banner.color + '"'
115
120
  }
116
121
  }
117
122
  el += `
118
- <div class="content"${style}>
123
+ <div class="content"${contentStyle}>
119
124
  ${top}
120
125
  ${bottom}
121
126
  </div>
122
127
  `
123
128
  el += `</div>`
129
+ el += `</div>`
124
130
  return el
125
131
  }
126
132
  %>
127
- <%- layoutDiv() %>
133
+ <%- layoutDiv() %>
@@ -109,7 +109,7 @@ function div_photo() {
109
109
  position = 'bottom';
110
110
  }
111
111
  }
112
- el += '<div class="cover" position="' + position + '">';
112
+ el += '<div class="cover" position="' + position + '" style="--cover-url:url(\'' + obj.image.replace(/'/g, '%27') + '\')">';
113
113
  el += '<img src="' + obj.image + '"/>';
114
114
  if (position.length > 0) {
115
115
  el += '<div class="cover-info" position="' + position + '"';
@@ -31,7 +31,8 @@
31
31
  }
32
32
  const anchor = document.getElementById(anchorId);
33
33
  if (anchor) {
34
- const offset = 32; // 与 TOC 点击滚动保持一致
34
+ // #start 锚点贴顶,不预留 offset;其余锚点与 TOC 点击滚动保持一致
35
+ const offset = anchorId === 'start' ? 0 : 32;
35
36
  const targetY = () => anchor.getBoundingClientRect().top + window.scrollY - offset;
36
37
  let timer = null;
37
38
  let lastY = null;
@@ -321,6 +321,31 @@
321
321
  // 动态数据本地缓存:TTL 未过期直接命中;过期后先显示缓存再后台刷新(stale-while-revalidate)
322
322
  _cacheEnabled: true,
323
323
  _cacheMaxEntryBytes: 200 * 1024,
324
+ // 同 URL 并发请求去重:页面内多个组件请求同一接口时共享一次 fetch
325
+ _pendingRequests: {},
326
+ _fetchShared: (url, options) => {
327
+ const key = (options && options.method || 'GET') + ' ' + url;
328
+ if (utils._pendingRequests[key]) {
329
+ return utils._pendingRequests[key].then(resp => resp.clone());
330
+ }
331
+ const promise = fetch(url, options).then(resp => {
332
+ if (!resp.ok) throw new Error('响应失败');
333
+ return resp;
334
+ }).finally(() => {
335
+ delete utils._pendingRequests[key];
336
+ });
337
+ utils._pendingRequests[key] = promise;
338
+ // 每个调用方各自 clone,避免 Response body 被消费后其他调用方取不到数据
339
+ return promise.then(resp => resp.clone());
340
+ },
341
+ // 延迟执行:缓存写入等非关键操作让出主线程
342
+ _defer: (fn) => {
343
+ if (window.requestIdleCallback) {
344
+ window.requestIdleCallback(fn, { timeout: 3000 });
345
+ } else {
346
+ setTimeout(fn, 0);
347
+ }
348
+ },
324
349
  cache: {
325
350
  prefix: 'Stellar.data_cache.v1.',
326
351
  enabled: () => !!def.data_cache?.enable && utils._cacheEnabled,
@@ -500,21 +525,21 @@
500
525
  }
501
526
  }, 5000);
502
527
 
503
- fetch(url).then(resp => {
528
+ utils._fetchShared(url).then(resp => {
504
529
  if (timedOut) return;
505
530
  clearTimeout(timeout);
506
-
507
- if (!resp.ok) throw new Error('响应失败');
508
531
  return resp;
509
532
  }).then(data => {
510
533
  if (timedOut) return;
511
534
  // 标记元素实例为已加载
512
535
  if (el) utils._loadedElements.add(el);
513
- // 写入缓存(克隆响应,避免影响回调读取)
536
+ // 写入缓存(克隆响应,避免影响回调读取;延迟执行,避免阻塞渲染)
514
537
  if (cacheable && data.ok) {
515
- data.clone().text().then(text => {
516
- utils.cache.set(url, text, data.headers.get('Content-Type') || 'application/json', ttl);
517
- }).catch(() => {});
538
+ utils._defer(() => {
539
+ data.clone().text().then(text => {
540
+ utils.cache.set(url, text, data.headers.get('Content-Type') || 'application/json', ttl);
541
+ }).catch(() => {});
542
+ });
518
543
  }
519
544
  // 后台刷新渲染前恢复初始结构,避免缓存渲染的内容重复
520
545
  if (el && initialHTML !== null) {
@@ -562,30 +587,48 @@
562
587
 
563
588
  return new Promise((resolve, reject) => {
564
589
  let retryCount = 0;
590
+ // 与 request 保持一致:最终失败时回退 stale 缓存,避免闪失败态
591
+ const fallback = () => {
592
+ if (cached) {
593
+ resolve(utils.cache.toResponse(cached));
594
+ } else {
595
+ reject('timeout');
596
+ }
597
+ };
565
598
 
566
599
  const tryRequest = () => {
567
600
  let timedOut = false;
568
601
  const timer = setTimeout(() => {
569
602
  timedOut = true;
570
- if (++retryCount > maxRetry) reject('timeout');
603
+ if (++retryCount > maxRetry) fallback();
571
604
  else tryRequest();
572
605
  }, timeout);
573
606
 
574
- fetch(url, options)
607
+ utils._fetchShared(url, options)
575
608
  .then(resp => {
576
609
  clearTimeout(timer);
577
- if (!resp.ok) throw new Error('bad response');
610
+ if (timedOut) return;
578
611
  if (cacheable) {
579
- resp.clone().text().then(text => {
580
- utils.cache.set(url, text, resp.headers.get('Content-Type') || 'application/json', ttl);
581
- }).catch(() => {});
612
+ utils._defer(() => {
613
+ resp.clone().text().then(text => {
614
+ utils.cache.set(url, text, resp.headers.get('Content-Type') || 'application/json', ttl);
615
+ }).catch(() => {});
616
+ });
582
617
  }
583
618
  resolve(resp);
584
619
  })
585
620
  .catch(err => {
586
621
  clearTimeout(timer);
587
- if (++retryCount > maxRetry) reject(err);
588
- else setTimeout(tryRequest, 500);
622
+ if (timedOut) return;
623
+ if (++retryCount > maxRetry) {
624
+ if (cached) {
625
+ resolve(utils.cache.toResponse(cached));
626
+ } else {
627
+ reject(err);
628
+ }
629
+ } else {
630
+ setTimeout(tryRequest, 500);
631
+ }
589
632
  });
590
633
  };
591
634
 
@@ -0,0 +1,26 @@
1
+ <%
2
+ // 隐藏的 SVG 线性渐变定义,配合 --item-grad 使用(用法见 menu.ejs 与 theme_base.styl 注释)。
3
+ // 渐变角度来自 style.gradient.angle(CSS 角度,0=向上、顺时针),与 CSS 渐变方向保持一致;
4
+ // 颜色使用 --item-theme(默认 var(--theme))与 --item-theme-light 两个 CSS 变量,
5
+ // 因此同一份定义可被任意元素复用,局部覆盖 --item-theme 的元素需同时设置 --item-theme-light。
6
+ var gradAngle = 225
7
+ if (theme.style && theme.style.gradient && theme.style.gradient.angle) {
8
+ gradAngle = parseFloat(theme.style.gradient.angle)
9
+ if (isNaN(gradAngle)) {
10
+ gradAngle = 225
11
+ }
12
+ }
13
+ var gradRad = gradAngle * Math.PI / 180
14
+ var gradVX = Math.sin(gradRad)
15
+ var gradVY = -Math.cos(gradRad)
16
+ var gradProj = [[0,0],[1,0],[0,1],[1,1]].map(function(c) {
17
+ return c[0] * gradVX + c[1] * gradVY
18
+ })
19
+ var gradMin = Math.min.apply(null, gradProj)
20
+ var gradMax = Math.max.apply(null, gradProj)
21
+ var gradX1 = Math.round((0.5 + gradVX * gradMin) * 1000) / 10
22
+ var gradY1 = Math.round((0.5 + gradVY * gradMin) * 1000) / 10
23
+ var gradX2 = Math.round((0.5 + gradVX * gradMax) * 1000) / 10
24
+ var gradY2 = Math.round((0.5 + gradVY * gradMax) * 1000) / 10
25
+ %>
26
+ <svg class="grad-def" width="0" height="0" aria-hidden="true" focusable="false"><defs><linearGradient id="<%= id %>" x1="<%= gradX1 %>%" y1="<%= gradY1 %>%" x2="<%= gradX2 %>%" y2="<%= gradY2 %>%"><stop offset="20%" style="stop-color:var(--item-theme-light)"/><stop offset="60%" style="stop-color:var(--item-theme)"/></linearGradient></defs></svg>
@@ -78,16 +78,11 @@ function layoutTitle(main, url, sub) {
78
78
  function layoutWidgets() {
79
79
  var el = '';
80
80
  el += '<div class="widgets">';
81
+ // 左栏共用的 SVG 渐变定义(供激活书签等彩色图标使用,见 grad-def.ejs)
82
+ el += partial('grad-def', {id: 'leftbar-active-icon-grad'})
83
+ // 导航区放进组件滚动区,一起滚动
84
+ el += layoutNavArea()
81
85
  if (page.leftbar) {
82
- var search = {}
83
- if (page.search) {
84
- search = page.search
85
- } else if (wiki?.search) {
86
- search = wiki.search
87
- } else if (topic?.search) {
88
- search = topic.search
89
- }
90
- el += partial('search', {item: search})
91
86
  page.leftbar.forEach((w, i) => {
92
87
  let name = ''
93
88
  let widget = {}
@@ -110,6 +105,20 @@ function layoutWidgets() {
110
105
  el += '</div>';
111
106
  return el;
112
107
  }
108
+ function layoutSearch() {
109
+ if (!page.leftbar) {
110
+ return ''
111
+ }
112
+ var search = {}
113
+ if (page.search) {
114
+ search = page.search
115
+ } else if (wiki?.search) {
116
+ search = wiki.search
117
+ } else if (topic?.search) {
118
+ search = topic.search
119
+ }
120
+ return partial('search', {item: search})
121
+ }
113
122
  function layoutFooterDiv() {
114
123
  const { social } = theme.footer
115
124
  if (social == null) {
@@ -154,6 +163,12 @@ function layoutNavArea() {
154
163
  if (page.menu == false) {
155
164
  return el
156
165
  }
166
+ // wiki 内容页默认隐藏导航区,page.menu 或 wiki.menu 为 true 时显示
167
+ if (page.wiki?.length > 0) {
168
+ if (page.menu !== true && wiki?.menu !== true) {
169
+ return el
170
+ }
171
+ }
157
172
  el += `<div class="nav-area">`
158
173
  el += partial('menu', {where: 'sidebar'})
159
174
  el += `</div>`
@@ -161,6 +176,6 @@ function layoutNavArea() {
161
176
  }
162
177
  %>
163
178
  <%- layoutLogo() %>
164
- <%- layoutNavArea() %>
179
+ <%- layoutSearch() %>
165
180
  <%- layoutWidgets() %>
166
181
  <%- layoutFooterDiv() %>
@@ -37,6 +37,26 @@ function layoutTitle(main, url, sub) {
37
37
  return el
38
38
  }
39
39
 
40
+ function layoutBack() {
41
+ if (where != 'sidebar' || !page.wiki) {
42
+ return ''
43
+ }
44
+ // 开关:page 的 wiki_home 优先,其次 wiki 定义的 wiki_home,默认显示
45
+ const wiki = theme.wiki.tree[page.wiki]
46
+ const enabled = page.wiki_home ?? wiki?.wiki_home ?? true
47
+ if (enabled === false) {
48
+ return ''
49
+ }
50
+ var el = ''
51
+ el += `<div class="wiki-home-wrap">`
52
+ el += `<a class="wiki-home cap" href="${pretty_url(theme.site_tree.index_wiki.base_dir)}">`
53
+ el += icon('default:goback')
54
+ el += `<span>${__('btn.all_wiki')}</span>`
55
+ el += `</a>`
56
+ el += `</div>`
57
+ return el
58
+ }
59
+
40
60
  function layoutDiv() {
41
61
  if (where == 'main') {
42
62
  if (page.header == false) {
@@ -75,4 +95,5 @@ function layoutDiv() {
75
95
  }
76
96
  %>
77
97
 
98
+ <%- layoutBack() %>
78
99
  <%- layoutDiv() %>
@@ -6,16 +6,22 @@ function layoutDiv() {
6
6
  if (item == undefined || item.id == null || item.url == null) {
7
7
  continue
8
8
  }
9
- el += `<a class="nav-item${item.id == page.menu_id ? ' active' : ''}" title="${item.title}" href="${pretty_url(item.url)}"`
9
+ var active = item.id == page.menu_id
10
+ el += `<a class="nav-item${active ? ' active' : ''}${item.theme?.length > 0 ? ' themed' : ''}"`
11
+ var style = `--item-grad:url(#menu-grad-${item.id})`
10
12
  if (item.theme?.length > 0) {
11
- el += ` style="color:${item.theme}"`
13
+ style += `;--item-theme:${item.theme}`
12
14
  }
13
- el += `>`
15
+ el += ` style="${style}"`
16
+ el += ` title="${item.title}" href="${pretty_url(item.url)}">`
14
17
  if (item.icon?.length > 0) {
15
18
  el += icon(item.icon, 'no-lazy')
16
- } else {
17
- el += `<span>${__(item.title)}</span>`
18
19
  }
20
+ el += `<span>${__(item.title)}</span>`
21
+ if (active) {
22
+ el += `<span class="active-dot"></span>`
23
+ }
24
+ el += partial('grad-def', {id: 'menu-grad-' + item.id})
19
25
  el += `</a>`
20
26
  }
21
27
  el += `</nav>`
@@ -37,9 +37,15 @@ function layoutToc(fallback) {
37
37
 
38
38
  function layoutDiv(fallback) {
39
39
  const tocBody = layoutTocBody()
40
+ const hasToc = tocBody.trim().length > 0
41
+ // 页面是否实际渲染评论区,由 _partial/comments/layout.ejs 标记
42
+ const hasComments = page.cmt_rendered === true
43
+ if (!hasToc && !hasComments) {
44
+ return ''
45
+ }
40
46
  var el = ''
41
47
  el += `<widget class="widget-wrapper${scrollreveal(' ')} toc" id="data-toc" collapse="${item.collapse}">`
42
- if (tocBody.trim().length > 0) {
48
+ if (hasToc) {
43
49
  el += layoutTocHeader()
44
50
  el += `<div class="widget-body">`
45
51
  el += tocBody
@@ -50,13 +56,11 @@ function layoutDiv(fallback) {
50
56
  el += icon('default:upup')
51
57
  el += `<span>${__('btn.top')}</span>`
52
58
  el += `</a>`
53
- if (theme.comments.service && theme.comments.service.length > 0) {
54
- if (page.comments == undefined || page.comments != false) {
55
- el += `<a class="buttom" onclick="util.scrollComment()">`
56
- el += icon('default:tocomment')
57
- el += `<span>${__('btn.comments')}</span>`
58
- el += `</a>`
59
- }
59
+ if (hasComments) {
60
+ el += `<a class="buttom" onclick="util.scrollComment()">`
61
+ el += icon('default:tocomment')
62
+ el += `<span>${__('btn.comments')}</span>`
63
+ el += `</a>`
60
64
  }
61
65
  el += `</div>`
62
66
  el += `</widget>`
@@ -18,11 +18,26 @@
18
18
  return null;
19
19
  };
20
20
 
21
+ // 最近的滚动容器(overflow-y: auto/scroll)
22
+ // 吸顶容器内可能存在内部滚动容器(如侧栏 .widgets),
23
+ // 若直接用吸顶容器做 reveal 容器,折叠区下方的元素永远不会进入视口,导致一直不显示。
24
+ const findScrollContainer = (el) => {
25
+ let node = el.parentElement;
26
+ while (node && node.nodeType === 1) {
27
+ const oy = window.getComputedStyle(node).overflowY;
28
+ if (oy === 'auto' || oy === 'scroll') return node;
29
+ node = node.parentElement;
30
+ }
31
+ return null;
32
+ };
33
+
21
34
  const pinnedGroups = new Map(); // 容器 -> 元素列表
22
35
  const targets = [];
23
36
  els.forEach((el) => {
24
- const container = findPinnedContainer(el);
25
- if (container) {
37
+ const pinned = findPinnedContainer(el);
38
+ if (pinned) {
39
+ // 优先使用最近的滚动容器,其次才用吸顶容器
40
+ const container = findScrollContainer(el) || pinned;
26
41
  const group = pinnedGroups.get(container) || [];
27
42
  group.push(el);
28
43
  pinnedGroups.set(container, group);