hexo-theme-stellar 1.40.0 → 1.42.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 (146) hide show
  1. package/.claude/skills/stellar-theme-dev/SKILL.md +1 -2
  2. package/CHANGELOG.md +90 -0
  3. package/CONTRIBUTING.md +2 -1
  4. package/README.md +50 -21
  5. package/README_EN.md +113 -0
  6. package/_config.yml +40 -12
  7. package/_data/icons.yml +248 -37
  8. package/languages/en.yml +6 -0
  9. package/languages/zh-CN.yml +6 -0
  10. package/languages/zh-TW.yml +6 -0
  11. package/layout/_partial/comments/artalk/script.ejs +25 -8
  12. package/layout/_partial/comments/beaudar/script.ejs +1 -0
  13. package/layout/_partial/comments/twikoo/script.ejs +1 -0
  14. package/layout/_partial/comments/utterances/script.ejs +1 -0
  15. package/layout/_partial/comments/waline/script.ejs +1 -0
  16. package/layout/_partial/cover/index.ejs +0 -3
  17. package/layout/_partial/head.ejs +43 -7
  18. package/layout/_partial/main/article/article_tags.ejs +16 -0
  19. package/layout/_partial/main/navbar/article_banner.ejs +33 -20
  20. package/layout/_partial/main/notebook/note_card.ejs +1 -1
  21. package/layout/_partial/main/notebook/paginator.ejs +2 -2
  22. package/layout/_partial/main/pin_slider.ejs +61 -30
  23. package/layout/_partial/main/post_list/latest_post_card.ejs +25 -0
  24. package/layout/_partial/main/post_list/paginator.ejs +2 -2
  25. package/layout/_partial/main/post_list/post_card.ejs +16 -64
  26. package/layout/_partial/main/post_list/topic_card.ejs +26 -5
  27. package/layout/_partial/menubtn.ejs +2 -2
  28. package/layout/_partial/scripts/bootstrap.ejs +35 -0
  29. package/layout/_partial/scripts/defines.ejs +27 -2
  30. package/layout/_partial/scripts.ejs +39 -7
  31. package/layout/_partial/sidebar/logo.ejs +1 -1
  32. package/layout/_partial/sidebar/menu.ejs +1 -1
  33. package/layout/_partial/sidebar/search.ejs +1 -1
  34. package/layout/_partial/widgets/author.ejs +1 -1
  35. package/layout/_partial/widgets/components/link.ejs +2 -2
  36. package/layout/_partial/widgets/ghuser.ejs +1 -1
  37. package/layout/_partial/widgets/related.ejs +1 -1
  38. package/layout/_partial/widgets/toc.ejs +3 -3
  39. package/layout/_partial/widgets/tree.ejs +1 -1
  40. package/layout/_plugins/adaptive_text.ejs +11 -0
  41. package/layout/_plugins/copycode.ejs +2 -2
  42. package/layout/_plugins/fancybox.ejs +4 -2
  43. package/layout/_plugins/mermaid.ejs +3 -2
  44. package/layout/_plugins/scrollreveal.ejs +22 -16
  45. package/layout/_plugins/search/local_search.ejs +4 -3
  46. package/layout/_plugins/swiper.ejs +2 -1
  47. package/layout/categories.ejs +1 -1
  48. package/layout/index.ejs +10 -2
  49. package/layout/index_topic.ejs +4 -4
  50. package/layout/page.ejs +3 -0
  51. package/layout/tags.ejs +1 -0
  52. package/package.json +3 -3
  53. package/scripts/events/lib/doc_tree.js +12 -208
  54. package/scripts/events/lib/notebooks.js +8 -148
  55. package/scripts/events/lib/utils.js +10 -1
  56. package/scripts/filters/index.js +1 -0
  57. package/scripts/filters/lib/img_lazyload.js +4 -0
  58. package/scripts/filters/lib/img_onerror.js +4 -0
  59. package/scripts/filters/lib/md_table.js +46 -0
  60. package/scripts/generators/search.js +12 -18
  61. package/scripts/generators/stellar-icons.js +47 -0
  62. package/scripts/helpers/ai_label.js +29 -0
  63. package/scripts/helpers/icon.js +2 -2
  64. package/scripts/helpers/json_ld.js +10 -13
  65. package/scripts/helpers/related_posts.js +0 -7
  66. package/scripts/helpers/seo.js +9 -0
  67. package/scripts/helpers/subtitle.js +8 -0
  68. package/scripts/lib/ai_label.js +55 -0
  69. package/scripts/lib/doc_tree.js +267 -0
  70. package/scripts/lib/notebooks.js +181 -0
  71. package/scripts/lib/search_index.js +82 -0
  72. package/scripts/lib/seo.js +79 -0
  73. package/scripts/lib/subtitle.js +24 -0
  74. package/scripts/tags/lib/about.js +2 -2
  75. package/scripts/tags/lib/albums.js +1 -1
  76. package/scripts/tags/lib/banner.js +5 -4
  77. package/scripts/tags/lib/chat.js +49 -49
  78. package/scripts/tags/lib/copy.js +1 -1
  79. package/scripts/tags/lib/friends.js +1 -1
  80. package/scripts/tags/lib/gallery.js +1 -1
  81. package/scripts/tags/lib/hashtag.js +1 -1
  82. package/scripts/tags/lib/image.js +3 -3
  83. package/scripts/tags/lib/posters.js +1 -1
  84. package/scripts/tags/lib/sites.js +2 -2
  85. package/source/css/_common/base.styl +5 -9
  86. package/source/css/_common/cover-overlay.styl +111 -0
  87. package/source/css/_common/device.styl +3 -3
  88. package/source/css/_common/title.styl +17 -0
  89. package/source/css/_components/layout.styl +9 -13
  90. package/source/css/_components/list.styl +135 -64
  91. package/source/css/_components/main.styl +2 -2
  92. package/source/css/_components/md-table.styl +23 -0
  93. package/source/css/_components/pages/archives.styl +5 -22
  94. package/source/css/_components/pages/article-story.styl +5 -14
  95. package/source/css/_components/partial/article-banner.styl +61 -37
  96. package/source/css/_components/partial/article-tags.styl +13 -0
  97. package/source/css/_components/partial/bread-nav.styl +19 -12
  98. package/source/css/_components/partial/navbar.styl +11 -3
  99. package/source/css/_components/partial/post-panel.styl +76 -0
  100. package/source/css/_components/pin-slider.styl +38 -41
  101. package/source/css/_components/sidebar/footer.styl +1 -1
  102. package/source/css/_components/sidebar/logo.styl +2 -2
  103. package/source/css/_components/sidebar/search.styl +25 -5
  104. package/source/css/_components/sidebar/sidebar.styl +7 -7
  105. package/source/css/_components/tag-plugins/banner.styl +53 -11
  106. package/source/css/_components/tag-plugins/friends_posts.styl +5 -86
  107. package/source/css/_components/tag-plugins/gallery.styl +3 -0
  108. package/source/css/_components/tag-plugins/read/paper.styl +3 -3
  109. package/source/css/_components/tag-plugins/read/reel.styl +4 -4
  110. package/source/css/_components/tag-plugins/table.styl +16 -11
  111. package/source/css/_components/tag-plugins/timeline.styl +4 -0
  112. package/source/css/_components/widgets/components.styl +6 -0
  113. package/source/css/_components/widgets/list.styl +6 -0
  114. package/source/css/_components/widgets/timeline.styl +2 -2
  115. package/source/css/_components/widgets/toc.styl +13 -7
  116. package/source/css/_components/widgets/widgets.styl +8 -7
  117. package/source/css/_custom.styl +9 -21
  118. package/source/css/_defines/func.styl +28 -3
  119. package/source/css/_plugins/index.styl +0 -18
  120. package/source/css/_plugins/lazyload.styl +1 -0
  121. package/source/css/{_plugins/comments → comments}/artalk.styl +4 -0
  122. package/source/css/{_plugins/comments → comments}/twikoo.styl +4 -0
  123. package/source/css/{_plugins → plugins}/mermaid.styl +3 -0
  124. package/source/css/{_plugins → plugins}/swiper.styl +5 -1
  125. package/source/js/color.js +325 -0
  126. package/source/js/icons.js +60 -0
  127. package/source/js/main.js +115 -1
  128. package/source/js/plugins/adaptive-text.js +146 -0
  129. package/source/js/search/highlight.js +82 -0
  130. package/source/js/search/local-search.js +294 -103
  131. package/source/js/services/friends_and_posts.js +1 -1
  132. package/source/js/services/timeline.js +1 -1
  133. package/source/js/services/weibo.js +3 -3
  134. package/source/js/services.js +179 -0
  135. package/source/js/tagtree.js +29 -0
  136. package/source/js/theme.js +61 -0
  137. package/{layout/_partial/scripts/utils.ejs → source/js/utils.js} +27 -11
  138. package/layout/_partial/cover/post_cover.ejs +0 -37
  139. package/layout/_partial/scripts/services.ejs +0 -178
  140. package/layout/_partial/scripts/sidebar.ejs +0 -25
  141. package/layout/_partial/scripts/tagtree.ejs +0 -29
  142. package/layout/_partial/scripts/theme.ejs +0 -67
  143. /package/source/css/{_plugins/comments → comments}/beaudar.styl +0 -0
  144. /package/source/css/{_plugins/comments → comments}/utterances.styl +0 -0
  145. /package/source/css/{_plugins/comments → comments}/waline.styl +0 -0
  146. /package/source/css/{_plugins → plugins}/fancybox.styl +0 -0
package/languages/en.yml CHANGED
@@ -39,6 +39,11 @@ meta:
39
39
  contributors: Page Contributors
40
40
  word_count: '%s words'
41
41
  reading_time: '%s min read'
42
+ ai_label:
43
+ manual: Written entirely by a human
44
+ reviewed: AI-reviewed
45
+ polished: AI-polished
46
+ generated: AI-generated
42
47
  date_suffix:
43
48
  just: Just
44
49
  min: minutes ago
@@ -70,6 +75,7 @@ symbol:
70
75
  colon: ": "
71
76
  brackets_l: "("
72
77
  brackets_r: ")"
78
+ page: "Page %s"
73
79
 
74
80
  tag_plugins:
75
81
  mbti:
@@ -39,6 +39,11 @@ meta:
39
39
  contributors: 本文贡献者
40
40
  word_count: '%s 字'
41
41
  reading_time: '预计阅读 %s 分钟'
42
+ ai_label:
43
+ manual: 本文完全由人类完成
44
+ reviewed: 已 AI 审核
45
+ polished: 已 AI 润色
46
+ generated: 由 AI 生成
42
47
  date_suffix:
43
48
  just: 刚刚
44
49
  min: 分钟前
@@ -70,6 +75,7 @@ symbol:
70
75
  colon: ":"
71
76
  brackets_l: "("
72
77
  brackets_r: ")"
78
+ page: "第 %s 页"
73
79
 
74
80
  tag_plugins:
75
81
  mbti:
@@ -39,6 +39,11 @@ meta:
39
39
  contributors: 本文貢獻者
40
40
  word_count: '%s 字'
41
41
  reading_time: '預計閱讀 %s 分鐘'
42
+ ai_label:
43
+ manual: 本文完全由人類完成
44
+ reviewed: 已 AI 審核
45
+ polished: 已 AI 潤飾
46
+ generated: 由 AI 生成
42
47
  date_suffix:
43
48
  just: 剛剛
44
49
  min: 分鐘前
@@ -70,6 +75,7 @@ symbol:
70
75
  colon: ":"
71
76
  brackets_l: "("
72
77
  brackets_r: ")"
78
+ page: "第 %s 頁"
73
79
 
74
80
  tag_plugins:
75
81
  mbti:
@@ -4,8 +4,13 @@
4
4
  function initArtalk() {
5
5
  const el = document.getElementById('artalk_container');
6
6
  if (!el) return;
7
+
8
+ // artalk 邮件通知链接携带 ?atk_comment=<id> / #atk-comment-<id> 定位目标,
9
+ // 命中时跳过视口懒加载立即初始化,让 Artalk 自身的 list-goto 逻辑完成定位。
10
+ const hasAtkTarget = /[?&]atk_comment=\d+/.test(window.location.search) || /#atk-comment-\d+/.test(window.location.hash);
7
11
 
8
12
  function load_artalk() {
13
+ utils.css('<%- url_for('/css/comments/artalk.css') %>?v=<%- stellar_info('version') %>');
9
14
  utils.css('<%- theme.comments.artalk.css %>');
10
15
  utils.js('<%- theme.comments.artalk.js %>', {defer: true}).then(function () {
11
16
  // 监听 Artalk 事件
@@ -13,6 +18,17 @@
13
18
  // Artalk.use((ctx) => {
14
19
  // ctx.on('list-loaded', () => window.lazyLoadInstance?.update())
15
20
  // })
21
+ // 邮件通知链接以 ?atk_comment=<id> 定位,Artalk 在 list-loaded 时才重新读取 URL,
22
+ // 固定延时清理会与之竞态(#598)。先把评论 id 改写到 hash(Artalk 读取优先级高于 query),
23
+ // 同时立即移除 atk_comment 查询参数,避免其 hash 监听干扰目录定位;
24
+ // atk_notify_key 保留到 list-loaded 完成定位与已读回执后再清理。
25
+ const atkComment = window.location.search.match(/[?&]atk_comment=(\d+)/);
26
+ if (atkComment && !/#atk-comment-\d+/.test(window.location.hash)) {
27
+ const params = new URLSearchParams(window.location.search);
28
+ const notifyKey = params.get('atk_notify_key');
29
+ const query = notifyKey ? '?atk_notify_key=' + encodeURIComponent(notifyKey) : '';
30
+ window.history.replaceState(null, '', window.location.pathname + query + '#atk-comment-' + atkComment[1]);
31
+ }
16
32
  const path = el.getAttribute('comment_id') ?? decodeURI(window.location.pathname);
17
33
  const artalk = Artalk.init({
18
34
  el: '#artalk_container',
@@ -39,18 +55,19 @@
39
55
  },
40
56
  <% } %>
41
57
  });
42
- // artalk 邮件通知链接携带 ?atk_comment=... 等查询参数,
43
- // 其 hash 监听会与主题目录定位冲突(#598):
44
- // 初始化并完成评论定位后清理查询参数,避免后续目录/锚点跳转被干扰。
45
- if (window.location.search.indexOf('atk_') !== -1) {
46
- window.setTimeout(function () {
47
- window.history.replaceState(null, '', window.location.pathname + window.location.hash);
48
- }, 300);
58
+ // list-loaded 时 Artalk 已读取 atk_comment / atk_notify_key 并触发定位与已读回执,
59
+ // 此时再清理残留的 ?atk_* 参数,避免固定延时与异步加载竞态(#598)。
60
+ if (hasAtkTarget && window.location.search.indexOf('atk_') !== -1) {
61
+ artalk.on('list-loaded', function () {
62
+ window.setTimeout(function () {
63
+ window.history.replaceState(null, '', window.location.pathname + window.location.hash);
64
+ }, 0);
65
+ });
49
66
  }
50
67
  });
51
68
  }
52
69
 
53
- util.viewportLazyload(el, load_artalk);
70
+ util.viewportLazyload(el, load_artalk, !hasAtkTarget);
54
71
  }
55
72
 
56
73
  // Initialize on page load
@@ -6,6 +6,7 @@
6
6
  if (!el) return;
7
7
 
8
8
  function load_beaudar() {
9
+ utils.css('<%- url_for('/css/comments/beaudar.css') %>?v=<%- stellar_info('version') %>');
9
10
  try {
10
11
  el.innerHTML = '';
11
12
  } catch (error) {
@@ -6,6 +6,7 @@
6
6
  if (!el) return;
7
7
 
8
8
  function load_twikoo() {
9
+ utils.css('<%- url_for('/css/comments/twikoo.css') %>?v=<%- stellar_info('version') %>');
9
10
  utils.js('<%- theme.comments.twikoo.js %>', {defer: true}).then(function () {
10
11
  const path = el.getAttribute('comment_id') ?? decodeURI(window.location.pathname);
11
12
  twikoo.init(Object.assign(<%- JSON.stringify(theme.comments.twikoo) %>, {
@@ -6,6 +6,7 @@
6
6
  if (!el) return;
7
7
 
8
8
  function load_utterances() {
9
+ utils.css('<%- url_for('/css/comments/utterances.css') %>?v=<%- stellar_info('version') %>');
9
10
  try {
10
11
  el.innerHTML = '';
11
12
  } catch (error) {
@@ -7,6 +7,7 @@
7
7
  if (!el) return;
8
8
 
9
9
  function load_waline() {
10
+ utils.css('<%- url_for('/css/comments/waline.css') %>?v=<%- stellar_info('version') %>');
10
11
  utils.css('<%- theme.comments.waline.css %>');
11
12
  utils.css('<%- theme.comments.waline.meta_css %>');
12
13
 
@@ -3,9 +3,6 @@ function layoutCover() {
3
3
  if (page.wiki) {
4
4
  return partial('wiki_cover')
5
5
  }
6
- // if (page.layout === 'post') {
7
- // return partial('post_cover')
8
- // }
9
6
  return ''
10
7
  }
11
8
  %>
@@ -1,13 +1,28 @@
1
1
  <%
2
+ function strip_wiki_title(title, wiki) {
3
+ if (!wiki) { return title; }
4
+ var t = String(title || '');
5
+ if (t.toLowerCase() === String(wiki).toLowerCase()) { return ''; }
6
+ // wiki 名中的空格/连字符在标题中可能写为另一种形态(如 cloud shell / cloud-shell)
7
+ var flex = String(wiki).replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/[\s-]+/g, '[\\s-]*');
8
+ var m = t.match(new RegExp('^' + flex + '[\\s-]*[::\\-](.*)$', 'i'));
9
+ if (m && m[1]) {
10
+ return m[1].trim();
11
+ }
12
+ return t;
13
+ }
14
+
2
15
  function generate_title() {
3
16
  if (page.wiki) {
4
17
  const proj = theme.wiki.tree[page.wiki]
5
18
  const wiki = proj?.name || page.wiki
6
19
  if (page.title) {
7
- return wiki + __('symbol.colon') + page.title + ' - ' + config.title;
8
- } else {
9
- return wiki + ' - ' + config.title;
20
+ var stripped = strip_wiki_title(page.title, wiki)
21
+ if (stripped) {
22
+ return wiki + __('symbol.colon') + stripped + ' - ' + config.title;
23
+ }
10
24
  }
25
+ return wiki + ' - ' + config.title;
11
26
  } else {
12
27
  if (page.title) {
13
28
  return page.title + ' - ' + config.title;
@@ -17,6 +32,9 @@ function generate_title() {
17
32
  return __('btn.tag') + __('symbol.colon') + page.tag + ' - ' + config.title;
18
33
  }
19
34
  }
35
+ if (is_home() && page.current && page.current > 1) {
36
+ return config.title + ' - ' + __('symbol.page', page.current);
37
+ }
20
38
  return config.title;
21
39
  }
22
40
  function generate_description() {
@@ -75,7 +93,7 @@ function og_args() {
75
93
  args.description = proj.description;
76
94
  }
77
95
  }
78
- args.image = config.avatar || (config.email ? gravatar(config.email) : null);
96
+ args.image = page.cover || page.banner || first_content_image(page.content) || config.avatar || (config.email ? gravatar(config.email) : null);
79
97
  return Object.assign(args, page.open_graph);
80
98
  }
81
99
 
@@ -92,9 +110,6 @@ function generate_og_title() {
92
110
  }
93
111
 
94
112
  function generate_og_site_name() {
95
- if (page.layout === 'post' && page.title) {
96
- return generate_og_title();
97
- }
98
113
  return escape_meta_content(strip_html(config.title));
99
114
  }
100
115
 
@@ -207,4 +222,25 @@ function generate_canonical() {
207
222
 
208
223
  <%- json_ld() %>
209
224
  <%- custom_inject() %>
225
+
226
+ <!-- icons.yml 驱动的 CSS 装饰图标 -->
227
+ <%
228
+ const iconCssVars = (() => {
229
+ const map = {
230
+ '--icon-h3-left': 'default:arrow-left',
231
+ '--icon-h3-right': 'default:arrow-right',
232
+ '--icon-quote-left': 'quot:quote-left',
233
+ '--icon-quote-right': 'quot:quote-right'
234
+ };
235
+ let vars = [];
236
+ for (const [name, key] of Object.entries(map)) {
237
+ const raw = theme.icons && theme.icons[key];
238
+ if (raw) vars.push(` ${name}: url("data:image/svg+xml,${encodeURIComponent(raw)}");`);
239
+ }
240
+ return vars.length > 0 ? `:root {\n${vars.join('\n')}\n}` : '';
241
+ })();
242
+ %>
243
+ <% if (iconCssVars) { %>
244
+ <style><%- iconCssVars %></style>
245
+ <% } %>
210
246
  </head>
@@ -0,0 +1,16 @@
1
+ <%# 文章标签行(layout: post,位于 article-footer 之前) %>
2
+ <%
3
+ function layoutDiv() {
4
+ var el = '';
5
+ if (!page.tags || page.tags.length === 0) {
6
+ return el;
7
+ }
8
+ el += `<div class="article-tags${scrollreveal(' ')}">`
9
+ page.tags.forEach((tag, i) => {
10
+ el += `<a class="tag" href="${pretty_url(tag.path)}">${icon('default:hashtag')}${escape_html(tag.name)}</a>`
11
+ })
12
+ el += '</div>'
13
+ return el;
14
+ }
15
+ %>
16
+ <%- layoutDiv(); %>
@@ -19,17 +19,18 @@ if (banner.subtitle == null) {
19
19
  banner.subtitle = page.subtitle
20
20
  }
21
21
 
22
- function layoutBreadcrumb() {
22
+ // 上块:第一行(左侧面包屑 + 右侧阅读时长 / AI 标签)+ 第二行(日期信息,靠左)
23
+ function layoutTop() {
23
24
  if (page.breadcrumb === false) {
24
- return `<div class="top"></div>`
25
+ return ''
25
26
  }
26
27
  var el = ''
27
- // 1.main
28
+ // 1.上块容器
28
29
  el += `<div class="top bread-nav footnote">`
29
- // 2.left
30
- el += `<div class="left">`
31
- // 3.left.top: 面包屑导航
32
- el += `<div class="flex-row" id="breadcrumb">`
30
+ // 2.第一行
31
+ el += `<div class="top-row">`
32
+ // 2.1 左:面包屑导航
33
+ el += `<div class="left" id="breadcrumb">`
33
34
  // 首页
34
35
  el += `<a class="cap breadcrumb" href="${pretty_url(config.root)}">${__("btn.home")}</a>`
35
36
  if (theme.wiki.tree[page.wiki]) {
@@ -41,20 +42,30 @@ function layoutBreadcrumb() {
41
42
  } else {
42
43
  el += partial('breadcrumb/page')
43
44
  }
44
- // 字数和预计阅读时长,置于面包屑行右侧(#302)
45
+ el += `</div>`
46
+ // 2.2 右:字数和预计阅读时长 + AI 成分标签(仅在有内容时渲染)
47
+ var rightHtml = ''
48
+ // 字数和预计阅读时长,置于第一行右侧(#302)
45
49
  if (page.layout == 'post' && theme.article.reading_time) {
46
50
  var _wc = word_count(page)
47
51
  if (_wc > 0) {
48
- el += `<span class="cap reading right">${__("meta.word_count", _wc)} · ${__("meta.reading_time", reading_time(page))}</span>`
52
+ rightHtml += `<span class="cap reading">${__("meta.word_count", _wc)} · ${__("meta.reading_time", reading_time(page))}</span>`
49
53
  }
50
54
  }
51
- // end 3.left.top
55
+ // AI 成分标签:置于第一行最右(阅读时长右侧);
56
+ // banner 含图片时不用配置色,继承默认文字色(--text-banner)
57
+ const bannerHasImage = banner.url && banner.url.length > 0
58
+ rightHtml += ai_label(page.ai_label, 'cap', bannerHasImage)
59
+ if (rightHtml.length > 0) {
60
+ el += `<div class="right">${rightHtml}</div>`
61
+ }
62
+ // end 2.第一行
52
63
  el += `</div>`
53
- // 3.left.bottom
64
+ // 3.第二行:日期信息,靠左
65
+ el += `<div class="meta-row">`
54
66
  el += partial('dateinfo')
55
- // end 2.left
56
67
  el += `</div>`
57
- // end 1.main
68
+ // end 1.上块
58
69
  el += `</div>`
59
70
  return el
60
71
  }
@@ -108,26 +119,28 @@ function layoutBottom() {
108
119
 
109
120
  function layoutDiv() {
110
121
  const bottom = layoutBottom()
111
- if (page.breadcrumb === false && bottom.length == 0) {
122
+ const top = layoutTop()
123
+ if (top.length == 0 && bottom.length == 0) {
112
124
  return ``
113
125
  }
114
- const top = layoutBreadcrumb()
115
126
  var bannerStyle = ``
116
- var contentStyle = ``
127
+ var adaptiveAttr = ``
128
+ var maskHtml = ``
117
129
  var el = ``
118
130
  if (banner.url?.length > 0) {
119
131
  bannerStyle += ` style="--bg-url:url('${banner.url.replace(/'/g, '%27')}')"`
132
+ // 背景图上方文字颜色自适应
133
+ adaptiveAttr = ` data-text-adaptive="split"`
120
134
  }
121
135
  el += `<div class="article-banner-wrap${scrollreveal(' ')}">`
122
136
  el += `<div class="article banner top"${bannerStyle}>`
123
137
  if (banner.url?.length > 0) {
124
138
  el += `<img class="lazy bg" data-src="${banner.url}">`
125
- if (banner.color) {
126
- contentStyle += ' style="--text-banner:' + banner.color + '"'
127
- }
139
+ maskHtml = `<div class="banner-mask banner-mask-top" aria-hidden="true"></div><div class="banner-mask banner-mask-bottom" aria-hidden="true"></div>`
128
140
  }
129
141
  el += `
130
- <div class="content"${contentStyle}>
142
+ <div class="content"${adaptiveAttr}>
143
+ ${maskHtml}
131
144
  ${top}
132
145
  ${bottom}
133
146
  </div>
@@ -19,7 +19,7 @@
19
19
  </div>
20
20
  <div class="meta cap">
21
21
  <% if (note.pin) { %>
22
- <span class="pin"><%- icon('solar:pin-linear') %></span>
22
+ <span class="pin"><%- icon('default:pin') %></span>
23
23
  <% } %>
24
24
  <span class="cap" id="post-meta">
25
25
  <%- icon('default:calendar') %>
@@ -1,8 +1,8 @@
1
1
  <% if ((page.total || 0) > 1) { %>
2
2
  <div class="paginator-wrap dis-select<%= scrollreveal(' ') %>">
3
3
  <%- paginator({
4
- prev_text: icon('solar:double-alt-arrow-left-bold-duotone'),
5
- next_text: icon('solar:double-alt-arrow-right-bold-duotone'),
4
+ prev_text: icon('default:arrow-left'),
5
+ next_text: icon('default:arrow-right'),
6
6
  force_prev_next: true,
7
7
  escape: false,
8
8
  }) %>
@@ -49,25 +49,13 @@ pinItems.sort(function(a, b) {
49
49
  return pinWeight(b) - pinWeight(a)
50
50
  })
51
51
 
52
- // 文章幻灯片:固定「标题 + 一行小字」结构,宽高比统一取自 pin_slider.ratio。
53
- // 标题:poster.headline > title;小字:poster.caption > description > excerpt(截断 50 字)。
54
- // 样式与 poster 卡片 cover-info 一致;无封面时为纯白卡片(文字按普通文章颜色)。
55
- function postSubtitle(post) {
56
- if (post.poster != null && post.poster.caption != null && post.poster.caption.length > 0) {
57
- return post.poster.caption
58
- }
59
- if (post.description != null && post.description.length > 0) {
60
- return post.description
61
- }
62
- var text = post.excerpt || post.content || ''
63
- text = strip_html(text).replace(/\s+/g, ' ').trim()
64
- return text.length > 0 ? truncate(text, {length: 50}) : ''
65
- }
66
-
52
+ // 文章幻灯片:固定「标题 + 一行小字」结构。
53
+ // 标题:post.title;小字由 subtitle() helper 统一取值(subtitle > description > excerpt 前 50 字)。
54
+ // 样式与 hero 卡片 cover-info 一致;无封面时为纯白卡片(文字按普通文章颜色)。
67
55
  function postSlide(post) {
68
56
  var noCover = !(post.cover != null && post.cover.length > 0)
69
- var headline = (post.poster != null && post.poster.headline != null && post.poster.headline.length > 0) ? post.poster.headline : post.title
70
- var subtitle = postSubtitle(post)
57
+ var headline = post.title
58
+ var sub = subtitle(post)
71
59
  var el = ''
72
60
  el += '<a class="pin-slide post-slide' + (noCover ? ' no-cover' : '') + '" href="' + escape_html(pretty_url(post.link || post.path)) + '"'
73
61
  if (!noCover) {
@@ -77,10 +65,11 @@ function postSlide(post) {
77
65
  if (!noCover) {
78
66
  el += '<img class="pin-slide-bg" src="' + escape_html(post.cover) + '" alt=""/>'
79
67
  }
80
- el += '<div class="pin-slide-text">'
68
+ // 有封面时文字颜色自适应(大字 contrast、小字 theme);无封面保持普通文章文字颜色
69
+ el += '<div class="pin-slide-text"' + (noCover ? '' : ' data-text-adaptive="split"') + '>'
81
70
  el += '<div class="pin-slide-headline">' + escape_html(headline) + '</div>'
82
- if (subtitle.length > 0) {
83
- el += '<div class="pin-slide-caption">' + escape_html(subtitle) + '</div>'
71
+ if (sub.length > 0) {
72
+ el += '<div class="pin-slide-caption">' + escape_html(sub) + '</div>'
84
73
  }
85
74
  el += '</div>'
86
75
  el += '</a>'
@@ -100,10 +89,9 @@ function wikiSlide(proj) {
100
89
  var title = proj.title || proj.name || proj.id
101
90
  var excerpt = proj.description || ''
102
91
  var el = ''
103
- el += '<a class="pin-slide" href="' + escape_html(pretty_url(proj.homepage?.path || '/')) + '">'
92
+ el += '<a class="pin-slide" href="' + escape_html(pretty_url(proj.homepage?.path || '/')) + '" style="--pin-cover-url:url(\'' + escape_html(img.replace(/'/g, '%27')) + '\')">'
104
93
  el += '<img class="pin-slide-bg" src="' + escape_html(img) + '" alt=""/>'
105
- el += '<div class="pin-slide-mask"></div>'
106
- el += '<div class="pin-slide-info">'
94
+ el += '<div class="pin-slide-info" data-text-adaptive="split">'
107
95
  el += '<div class="pin-slide-caps">' + projectChips(proj.tags) + '</div>'
108
96
  el += '<div class="pin-slide-title">' + escape_html(title) + '</div>'
109
97
  if (excerpt.length > 0) {
@@ -137,20 +125,21 @@ if (pinItems.length > 0) {
137
125
  el += '</div>'
138
126
  if (dotsHtml.length > 0) {
139
127
  el += '<div class="pin-slider-dots">' + dotsHtml + '</div>'
140
- el += '<button type="button" class="pin-slider-nav prev">' + icon('solar:double-alt-arrow-left-bold-duotone') + '</button>'
141
- el += '<button type="button" class="pin-slider-nav next">' + icon('solar:double-alt-arrow-right-bold-duotone') + '</button>'
128
+ el += '<button type="button" class="pin-slider-nav prev">' + icon('default:arrow-left') + '</button>'
129
+ el += '<button type="button" class="pin-slider-nav next">' + icon('default:arrow-right') + '</button>'
142
130
  }
143
131
  el += '</div>'
144
132
  %>
145
133
  <%- el %>
146
134
  <script>
147
135
  function initWhenReady() {
148
- if (typeof utils !== 'undefined' && utils.initPlugin) {
149
- utils.initPlugin(initPinSlider, 'pin-slider');
136
+ if (typeof stellar === 'object' && typeof stellar.initPlugin === 'function') {
137
+ stellar.initPlugin(initPinSlider, 'pin-slider');
150
138
  } else {
151
- document.addEventListener('DOMContentLoaded', function() {
152
- if (typeof utils !== 'undefined' && utils.initPlugin) {
153
- utils.initPlugin(initPinSlider, 'pin-slider');
139
+ // pin_slider 内联脚本渲染在 scripts/bootstrap 之前,等待 DOMContentLoaded 后注册
140
+ document.addEventListener('DOMContentLoaded', function () {
141
+ if (typeof stellar === 'object' && typeof stellar.initPlugin === 'function') {
142
+ stellar.initPlugin(initPinSlider, 'pin-slider');
154
143
  }
155
144
  });
156
145
  }
@@ -163,6 +152,10 @@ function initPinSlider() {
163
152
  var slides = slider.querySelectorAll('.pin-slide');
164
153
  var count = slides.length;
165
154
  if (!track || count <= 1) {
155
+ // 单张幻灯片:直接把封面同步到 .pin-slider 自身背景(跟随 corner-shape)
156
+ if (slides[0]) {
157
+ slider.style.setProperty('--pin-cover-url', slides[0].style.getPropertyValue('--pin-cover-url') || '');
158
+ }
166
159
  slider.style.visibility = 'visible';
167
160
  return;
168
161
  }
@@ -185,6 +178,11 @@ function initPinSlider() {
185
178
  function goTo(n) {
186
179
  index = (n + count) % count;
187
180
  track.style.transform = 'translateX(-' + (index * 100) + '%)';
181
+ // 背景图由 .pin-slider 自身背景承载(跟随 corner-shape),随当前 slide 同步
182
+ var active = slides[index];
183
+ if (active) {
184
+ slider.style.setProperty('--pin-cover-url', active.style.getPropertyValue('--pin-cover-url') || '');
185
+ }
188
186
  var dots = slider.querySelector('.pin-slider-dots');
189
187
  var items = dots ? dots.children : [];
190
188
  for (var i = 0; i < count; i++) {
@@ -198,9 +196,39 @@ function initPinSlider() {
198
196
  }
199
197
  }
200
198
  savePinIndex(group, contentKey, index, count);
199
+ updateNavColor();
201
200
  }
202
201
  function next() { goTo(index + 1); }
203
202
  function prev() { goTo(index - 1); }
203
+
204
+ // 左右箭头颜色随当前幻灯片封面自适应(contrast:深色背景白箭头、浅色背景深箭头)
205
+ var navColorRetries = 0;
206
+ function updateNavColor() {
207
+ var navs = slider.querySelectorAll('.pin-slider-nav');
208
+ if (navs.length === 0) return;
209
+ if (!window.stellar || !window.stellar.color || !window.resolveAdaptiveBackdrop) {
210
+ // 懒加载的 color.js / 插件尚未就绪时重试
211
+ if (navColorRetries < 25) {
212
+ navColorRetries++;
213
+ setTimeout(updateNavColor, 200);
214
+ }
215
+ return;
216
+ }
217
+ var slide = slides[index];
218
+ if (!slide) return;
219
+ var cover = slide.style.getPropertyValue('--pin-cover-url');
220
+ if (!cover) return;
221
+ var m = cover.match(/url\((['"]?)(.*?)\1\)/i);
222
+ if (!m) return;
223
+ stellar.color.getAverageColor(m[2], { background: window.resolveAdaptiveBackdrop(slide) }).then(function (rgb) {
224
+ if (!rgb) return;
225
+ var color = stellar.color.adaptiveTextColor(rgb, { style: 'contrast' });
226
+ for (var i = 0; i < navs.length; i++) {
227
+ navs[i].style.setProperty('--text-banner', color);
228
+ }
229
+ });
230
+ }
231
+ window.refreshPinNavColor = updateNavColor;
204
232
  function start() {
205
233
  if (reduced) return;
206
234
  slider.classList.remove('paused');
@@ -294,6 +322,9 @@ function initPinSlider() {
294
322
  navNext.removeEventListener('click', next);
295
323
  }
296
324
  document.removeEventListener('visibilitychange', onVis);
325
+ if (window.refreshPinNavColor === updateNavColor) {
326
+ window.refreshPinNavColor = null;
327
+ }
297
328
  };
298
329
  }
299
330
  function restorePinIndex(group, contentKey, count) {
@@ -0,0 +1,25 @@
1
+ <%
2
+ // 最新文章封面卡片(公共组件):整卡跳转到最新文章,背景为封面图,底部渐变模糊文字区。
3
+ // 入参:href(跳转链接)、background(背景图 URL)、label(专栏名/分类名)、post({title, date})
4
+ // 专栏列表页与未来首页等场景复用;无 post 时仅渲染 label。
5
+ function layoutDiv() {
6
+ var el = '';
7
+ var bg = background || theme.default.topic;
8
+ // 注意:不能用 `var href` 承接,var 提升会遮蔽 partial 传入的 href 参数
9
+ var link = pretty_url(href || '/');
10
+ el += '<a class="cover" position="bottom" style="--cover-url:url(\'' + escape_html(bg.replace(/'/g, '%27')) + '\')" href="' + link + '">';
11
+ el += '<img src="' + escape_html(bg) + '" alt=""/>';
12
+ el += '<div class="cover-info" position="bottom" data-text-adaptive="split">';
13
+ if (typeof label !== 'undefined' && label) {
14
+ el += '<div class="text topic">' + escape_html(label) + '</div>';
15
+ }
16
+ if (post) {
17
+ el += '<div class="text headline">' + escape_html(post.title) + '</div>';
18
+ el += '<div class="text caption">' + escape_html(date(post.date, config.date_format)) + '</div>';
19
+ }
20
+ el += '</div>';
21
+ el += '</a>';
22
+ return el;
23
+ }
24
+ %>
25
+ <%- layoutDiv() %>
@@ -1,8 +1,8 @@
1
1
  <% if ((is_home() || is_category() || is_tag()) && page.total > 1) { %>
2
2
  <div class='paginator-wrap dis-select<%- scrollreveal(' ') %>'>
3
3
  <%- paginator({
4
- prev_text: icon('solar:double-alt-arrow-left-bold-duotone'),
5
- next_text: icon('solar:double-alt-arrow-right-bold-duotone'),
4
+ prev_text: icon('default:arrow-left'),
5
+ next_text: icon('default:arrow-right'),
6
6
  force_prev_next: true,
7
7
  escape: false
8
8
  }) %>