hexo-theme-stellar 1.41.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 (66) hide show
  1. package/.claude/skills/stellar-theme-dev/SKILL.md +1 -2
  2. package/CHANGELOG.md +49 -0
  3. package/CONTRIBUTING.md +2 -1
  4. package/README.md +50 -21
  5. package/README_EN.md +113 -0
  6. package/_config.yml +8 -3
  7. package/_data/icons.yml +0 -2
  8. package/layout/_partial/cover/index.ejs +0 -3
  9. package/layout/_partial/main/navbar/article_banner.ejs +30 -27
  10. package/layout/_partial/main/pin_slider.ejs +59 -28
  11. package/layout/_partial/main/post_list/latest_post_card.ejs +25 -0
  12. package/layout/_partial/main/post_list/post_card.ejs +16 -64
  13. package/layout/_partial/main/post_list/topic_card.ejs +26 -5
  14. package/layout/_partial/scripts/bootstrap.ejs +35 -0
  15. package/layout/_partial/scripts/defines.ejs +2 -1
  16. package/layout/_partial/scripts.ejs +9 -0
  17. package/layout/_partial/widgets/toc.ejs +2 -2
  18. package/layout/_plugins/adaptive_text.ejs +11 -0
  19. package/layout/_plugins/copycode.ejs +2 -2
  20. package/layout/_plugins/mermaid.ejs +1 -1
  21. package/layout/_plugins/scrollreveal.ejs +22 -16
  22. package/layout/_plugins/swiper.ejs +1 -1
  23. package/layout/index.ejs +10 -2
  24. package/layout/index_topic.ejs +4 -4
  25. package/package.json +3 -3
  26. package/scripts/generators/stellar-icons.js +1 -1
  27. package/scripts/helpers/subtitle.js +8 -0
  28. package/scripts/lib/subtitle.js +24 -0
  29. package/scripts/tags/lib/banner.js +4 -3
  30. package/source/css/_common/cover-overlay.styl +111 -0
  31. package/source/css/_common/title.styl +17 -0
  32. package/source/css/_components/layout.styl +9 -13
  33. package/source/css/_components/list.styl +135 -64
  34. package/source/css/_components/main.styl +2 -2
  35. package/source/css/_components/pages/archives.styl +4 -1
  36. package/source/css/_components/pages/article-story.styl +1 -10
  37. package/source/css/_components/partial/article-banner.styl +61 -37
  38. package/source/css/_components/partial/article-tags.styl +4 -1
  39. package/source/css/_components/partial/bread-nav.styl +7 -14
  40. package/source/css/_components/partial/navbar.styl +1 -1
  41. package/source/css/_components/partial/post-panel.styl +76 -0
  42. package/source/css/_components/pin-slider.styl +38 -41
  43. package/source/css/_components/sidebar/footer.styl +1 -1
  44. package/source/css/_components/sidebar/logo.styl +2 -2
  45. package/source/css/_components/sidebar/search.styl +1 -1
  46. package/source/css/_components/sidebar/sidebar.styl +7 -7
  47. package/source/css/_components/tag-plugins/banner.styl +53 -11
  48. package/source/css/_components/tag-plugins/friends_posts.styl +5 -86
  49. package/source/css/_components/tag-plugins/gallery.styl +3 -0
  50. package/source/css/_components/tag-plugins/read/paper.styl +3 -3
  51. package/source/css/_components/tag-plugins/read/reel.styl +4 -4
  52. package/source/css/_components/tag-plugins/timeline.styl +4 -0
  53. package/source/css/_components/widgets/timeline.styl +2 -2
  54. package/source/css/_components/widgets/toc.styl +12 -6
  55. package/source/css/_components/widgets/widgets.styl +8 -7
  56. package/source/css/_custom.styl +8 -9
  57. package/source/css/_plugins/lazyload.styl +1 -0
  58. package/source/js/color.js +325 -0
  59. package/source/js/main.js +2 -2
  60. package/source/js/plugins/adaptive-text.js +146 -0
  61. package/source/js/services/friends_and_posts.js +1 -1
  62. package/source/js/services/timeline.js +1 -1
  63. package/source/js/services/weibo.js +1 -1
  64. package/source/js/services.js +1 -1
  65. package/source/js/utils.js +14 -1
  66. package/layout/_partial/cover/post_cover.ejs +0 -37
@@ -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,47 +1,16 @@
1
1
  <%
2
- const poster = post.poster;
3
2
  var obj = {
4
3
  image: post.cover
5
4
  };
6
- if (poster) {
7
- obj.headline = poster.headline;
8
- obj.topic = poster.topic;
9
- obj.caption = poster.caption;
10
- obj.color = poster.color;
11
- }
12
5
  function div_default() {
13
6
  var el = '';
14
7
  el += '<article class="md-text">';
15
8
 
16
- // 封面
17
- if (obj.image || theme.article.auto_cover) {
18
- var cover_url;
19
- if (obj.image != undefined) {
20
- if (obj.image.includes('/')) {
21
- cover_url = obj.image;
22
- } else {
23
- cover_url = 'https://source.unsplash.com/1280x640/?' + obj.image;
24
- }
25
- } else {
26
- // 自动以 tags 作为关键词搜索封面
27
- if (post.tags) {
28
- var params = '';
29
- post.tags.reverse().forEach((tag, i) => {
30
- if (i > 0) {
31
- params += ',';
32
- }
33
- params += tag.name;
34
- });
35
- cover_url = 'https://source.unsplash.com/1280x640/?' + params;
36
- } else {
37
- cover_url = 'https://source.unsplash.com/random/1280x640';
38
- }
39
- }
40
- if (cover_url) {
41
- el += '<div class="post-cover">';
42
- el += '<img src="' + escape_html(cover_url) + '"/>';
43
- el += '</div>';
44
- }
9
+ // 封面:仅当显式 cover 为完整 URL 时渲染(Unsplash 自动封面接口已失效,不再生成)
10
+ if (obj.image && obj.image.includes('/')) {
11
+ el += '<div class="post-cover">';
12
+ el += '<img src="' + escape_html(obj.image) + '"/>';
13
+ el += '</div>';
45
14
  }
46
15
 
47
16
  // 标题
@@ -100,7 +69,7 @@ function div_default() {
100
69
  if (i >= 5) {
101
70
  return;
102
71
  }
103
- el += `<span class="cap tag-chip">#${escape_html(tag.name)}</span>`;
72
+ el += `<span class="cap tag-chip">${icon('default:hashtag')}${escape_html(tag.name)}</span>`;
104
73
  });
105
74
  el += '</span>';
106
75
  }
@@ -110,39 +79,22 @@ function div_default() {
110
79
  }
111
80
  function div_photo() {
112
81
  var el = '';
113
- var position = '';
114
- if (obj.headline || obj.topic || obj.caption) {
115
- if (obj.topic) {
116
- position = 'top';
117
- } else {
118
- position = 'bottom';
119
- }
120
- }
121
- el += '<div class="cover" position="' + position + '" style="--cover-url:url(\'' + escape_html(obj.image.replace(/'/g, '%27')) + '\')">';
82
+ // 小字取值统一由 subtitle() helper 提供:subtitle > description > excerpt 前 50 字
83
+ var caption = subtitle(post);
84
+ // hero 卡片文字区固定 bottom:标题 + 一行小字,不再有 top 布局
85
+ el += '<div class="cover" position="bottom" style="--cover-url:url(\'' + escape_html(obj.image.replace(/'/g, '%27')) + '\')">';
122
86
  el += '<img src="' + escape_html(obj.image) + '"/>';
123
- if (position.length > 0) {
124
- el += '<div class="cover-info" position="' + position + '"';
125
- if (obj.color) {
126
- el += 'style="color:' + escape_html(obj.color) + '"';
127
- }
128
- el += '>';
129
- if (obj.topic) {
130
- el += '<div class="text topic">' + escape_html(obj.topic) + '</div>';
131
- }
132
- if (obj.headline) {
133
- el += '<div class="text headline">' + escape_html(obj.headline) + '</div>';
134
- }
135
- if (obj.caption) {
136
- el += '<div class="text caption">' + escape_html(obj.caption) + '</div>';
137
- }
138
- el += '</div>';
139
-
87
+ el += '<div class="cover-info" position="bottom" data-text-adaptive="split">';
88
+ el += '<div class="text headline">' + escape_html(post.title ? post.title : date(post.date, config.date_format)) + '</div>';
89
+ if (caption.length > 0) {
90
+ el += '<div class="text caption">' + escape_html(caption) + '</div>';
140
91
  }
141
92
  el += '</div>';
93
+ el += '</div>';
142
94
  return el;
143
95
  }
144
96
  function div() {
145
- if (obj.image && obj.image.length > 0 && obj.headline != undefined) {
97
+ if (theme.article.card_style == 'hero' && obj.image && obj.image.length > 0) {
146
98
  return div_photo();
147
99
  }
148
100
  return div_default();
@@ -1,14 +1,35 @@
1
1
  <%
2
2
  function layoutDiv() {
3
3
  var el = '';
4
+ var bg = topic.cover || topic.icon || theme.default.topic;
5
+ var label = topic.title || topic.name;
6
+ var latest = topic.homepage;
4
7
  el += '<article class="md-text">';
5
- el += `<div class="preview"><img src="${topic.icon ? topic.icon : theme.default.topic}"/></div>`
6
- el += '<div class="excerpt">';
7
- el += '<h2 class="post-title">' + (topic.title || topic.name) + '</h2>';
8
+ // 专栏标题(story 文章 h2 样式,置于卡片外)
9
+ el += '<h2 class="topic-title">' + escape_html(label) + '</h2>';
10
+ // 专栏描述(标题下方)
8
11
  if (topic.description) {
9
- el += '<p>' + topic.description + '</p>';
12
+ el += '<p class="topic-desc">' + escape_html(topic.description) + '</p>';
13
+ }
14
+ // 最新文章卡片(整卡跳转最新文章)
15
+ el += partial('_partial/main/post_list/latest_post_card', {
16
+ href: pretty_url(latest ? (latest.path || '/') : '/'),
17
+ background: bg,
18
+ post: latest
19
+ });
20
+ // 其他文章(排除最新,最多 3 条,按发布时间倒序)
21
+ if (topic.pages && topic.pages.length > 1) {
22
+ el += '<div class="post-panel">';
23
+ el += '<div class="posts">';
24
+ topic.pages.slice(1, 4).forEach(function(post) {
25
+ el += '<a class="post-link" href="' + pretty_url(post.path) + '">';
26
+ el += '<span class="title">' + escape_html(post.title) + '</span>';
27
+ el += '<span class="date">' + escape_html(date(post.date, config.date_format)) + '</span>';
28
+ el += '</a>';
29
+ });
30
+ el += '</div>';
31
+ el += '</div>';
10
32
  }
11
- el += '</div>';
12
33
  el += '</article>';
13
34
  return el;
14
35
  }
@@ -0,0 +1,35 @@
1
+ <script>
2
+ // 插件注册队列:utils.js 尚未就绪时先缓存注册请求,就绪后统一补跑,
3
+ // 保证解析期注册的插件不因加载时序丢失。
4
+ window.stellar = window.stellar || {};
5
+ window.stellar._pluginQueue = window.stellar._pluginQueue || [];
6
+ window.stellar.initPlugin = function (fn, name, options) {
7
+ if (typeof utils === 'object' && typeof utils.initPlugin === 'function') {
8
+ return utils.initPlugin(fn, name, options);
9
+ }
10
+ window.stellar._pluginQueue.push({ fn: fn, name: name, options: options || {} });
11
+ };
12
+ window.stellar._flushPlugins = function () {
13
+ var pending = window.stellar._pluginQueue.splice(0);
14
+ if (typeof utils !== 'object' || typeof utils.initPlugin !== 'function') return;
15
+ for (var i = 0; i < pending.length; i++) {
16
+ try {
17
+ utils.initPlugin(pending[i].fn, pending[i].name, pending[i].options);
18
+ } catch (e) {
19
+ console.error('[Stellar] 插件补注册失败:', e);
20
+ }
21
+ }
22
+ };
23
+ document.addEventListener('DOMContentLoaded', function () {
24
+ if (typeof utils === 'object') return;
25
+ // utils 未就绪时补载;加载成功后由 utils.js 内部补跑插件队列
26
+ var s = document.createElement('script');
27
+ s.src = '<%- url_for('/js/utils.js') %>?v=<%- stellar_info('version') %>';
28
+ s.async = false;
29
+ s.onerror = function () {
30
+ console.error('[Stellar] 无法加载 /js/utils.js,已启用内容兜底显示(sr-fallback)');
31
+ document.documentElement.classList.add('sr-fallback');
32
+ };
33
+ document.head.appendChild(s);
34
+ });
35
+ </script>
@@ -195,7 +195,8 @@
195
195
  if (!window.location.search) return;
196
196
  if (!new URLSearchParams(window.location.search).has('kw')) return;
197
197
  document.addEventListener('DOMContentLoaded', function () {
198
- // utils 为全局词法绑定(顶层 const),不能通过 window.utils 访问
198
+ // utils /js/utils.js 定义(同时暴露 window.utils);
199
+ // 未就绪时由 bootstrap 队列兜底,此处按就绪状态判断
199
200
  if (typeof utils === 'object' && typeof utils.js === 'function') {
200
201
  utils.js('<%- url_for('/js/search/highlight.js') %>', { defer: true }).catch(function () {});
201
202
  }
@@ -34,8 +34,17 @@ function tagtreeWidgetOnPage() {
34
34
  %>
35
35
  <%- partial('scripts/defines') %>
36
36
 
37
+ <%- partial('scripts/bootstrap') %>
38
+
37
39
  <!-- core: utils 必须同步加载(页尾内联插件片段在解析期即调用 utils.initPlugin) -->
38
40
  <script src="<%- url_for('/js/utils.js') %>?v=<%- stellar_info('version') %>"></script>
41
+ <script>
42
+ // 解析期检测 utils 是否已就绪;未就绪时同步补载,
43
+ // 保证「utils 先于页尾插件片段定义」的不变量。
44
+ if (typeof utils === 'undefined') {
45
+ document.write('<script src="<%- url_for('/js/utils.js') %>?v=<%- stellar_info('version') %>"><\/script>');
46
+ }
47
+ </script>
39
48
  <script defer src="<%- url_for('/js/stellar-icons.js') %>?v=<%- stellar_info('version') %>"></script>
40
49
  <!-- 非首屏图标异步加载:占位符替换依赖构建期生成的 js/icons/{ns}.json -->
41
50
  <script defer src="<%- url_for('/js/icons.js') %>?v=<%- stellar_info('version') %>" data-version="<%- stellar_info('version') %>"></script>
@@ -60,12 +60,12 @@ function layoutDiv(fallback) {
60
60
  }
61
61
  el += `<div class="widget-footer">`
62
62
  el += `<a class="top" onclick="util.scrollTop()">`
63
- el += icon('default:upup')
63
+ el += icon('default:upup', '', true)
64
64
  el += `<span>${__('btn.top')}</span>`
65
65
  el += `</a>`
66
66
  if (hasComments) {
67
67
  el += `<a class="buttom" onclick="util.scrollComment()">`
68
- el += icon('default:tocomment')
68
+ el += icon('default:tocomment', '', true)
69
69
  el += `<span>${__('btn.comments')}</span>`
70
70
  el += `</a>`
71
71
  }
@@ -0,0 +1,11 @@
1
+ <script>
2
+ stellar.initPlugin(() => {
3
+ const els = Array.prototype.slice.call(document.querySelectorAll('[data-text-adaptive]'));
4
+ if (els.length === 0) return;
5
+ utils.js('/js/color.js').then(() => utils.js('/js/plugins/adaptive-text.js')).then(() => {
6
+ if (typeof applyAdaptiveText === 'function') {
7
+ applyAdaptiveText(els);
8
+ }
9
+ });
10
+ }, 'adaptive-text');
11
+ </script>
@@ -1,5 +1,5 @@
1
1
  <script>
2
- utils.initPlugin(() => {
2
+ stellar.initPlugin(() => {
3
3
  const codeElements = document.querySelectorAll('.code');
4
4
  if (codeElements.length > 0) {
5
5
  ctx.copycode = {
@@ -14,4 +14,4 @@
14
14
  });
15
15
  }
16
16
  }, 'copycode');
17
- </script>
17
+ </script>
@@ -1,6 +1,6 @@
1
1
  <script defer type="text/javascript" src="<%- conf.js %>"></script>
2
2
  <script>
3
- utils.initPlugin(() => {
3
+ stellar.initPlugin(() => {
4
4
  const els = document.querySelectorAll('.mermaid');
5
5
  if (els.length > 0) {
6
6
  utils.css(`<%- url_for('/css/plugins/mermaid.css') %>?v=<%- stellar_info('version') %>`);
@@ -1,7 +1,20 @@
1
1
  <script>
2
- utils.initPlugin(() => {
3
- const els = Array.prototype.slice.call(document.querySelectorAll('.slide-up'));
4
- if (els.length === 0) return;
2
+ (function () {
3
+ // 兜底看门狗独立于插件初始化:3 秒内未完成 reveal 时强制显示 .slide-up 内容
4
+ let fallbackApplied = false;
5
+ const revealFallback = () => {
6
+ if (fallbackApplied) return;
7
+ fallbackApplied = true;
8
+ document.documentElement.classList.add('sr-fallback');
9
+ };
10
+ const watchdog = setTimeout(() => revealFallback(), 3000);
11
+
12
+ stellar.initPlugin(() => {
13
+ const els = Array.prototype.slice.call(document.querySelectorAll('.slide-up'));
14
+ if (els.length === 0) {
15
+ clearTimeout(watchdog);
16
+ return;
17
+ }
5
18
 
6
19
  // 吸顶/固定定位容器内的元素始终在视口内(或由容器控制显隐),
7
20
  // 但 ScrollReveal 按文档坐标判断可见性,会误判这类元素为不可见,
@@ -45,15 +58,10 @@
45
58
  targets.push(el);
46
59
  }
47
60
  });
48
- if (pinnedGroups.size === 0 && targets.length === 0) return;
49
-
50
- // 加载/初始化失败时兜底显示内容,避免整页空白
51
- let fallbackApplied = false;
52
- const revealFallback = () => {
53
- if (fallbackApplied) return;
54
- fallbackApplied = true;
55
- document.documentElement.classList.add('sr-fallback');
56
- };
61
+ if (pinnedGroups.size === 0 && targets.length === 0) {
62
+ clearTimeout(watchdog);
63
+ return;
64
+ }
57
65
 
58
66
  const slideUp = {
59
67
  distance: `<%- conf.distance %>`,
@@ -64,9 +72,6 @@
64
72
  easing: "ease-out"
65
73
  };
66
74
 
67
- // 看门狗:CDN 长时间无响应时强制显示内容
68
- const watchdog = setTimeout(() => revealFallback(), 3000);
69
-
70
75
  utils.js(`<%- conf.js %>`, { defer: true })
71
76
  .then(() => {
72
77
  if (fallbackApplied) return;
@@ -92,5 +97,6 @@
92
97
  console.error('[Plugin scrollreveal] 加载失败:', err);
93
98
  revealFallback();
94
99
  });
95
- }, 'scrollreveal');
100
+ }, 'scrollreveal');
101
+ })();
96
102
  </script>
@@ -1,5 +1,5 @@
1
1
  <script>
2
- utils.initPlugin(() => {
2
+ stellar.initPlugin(() => {
3
3
  const swiper_api = document.getElementById('swiper-api');
4
4
  if (swiper_api != undefined) {
5
5
  utils.css(`<%- url_for('/css/plugins/swiper.css') %>?v=<%- stellar_info('version') %>`);
package/layout/index.ejs CHANGED
@@ -76,11 +76,19 @@ if (pinFlat && is_home_first_page()) {
76
76
  function layout_post_card(layout, post, content) {
77
77
  var el = '';
78
78
  var layout = layout;
79
- if (layout == 'post' && post.cover != undefined && post.poster != undefined) {
79
+ var hero = false;
80
+ if (layout == 'post' && post.cover != undefined && theme.article.card_style == 'hero') {
80
81
  layout += ' photo';
82
+ hero = true;
81
83
  }
82
84
  el += `<div class="post-card-wrap${scrollreveal(' ')}">`;
83
- el += `<a class="post-card ${layout}" href="${pretty_url(post.link || post.path)}">`
85
+ if (hero) {
86
+ // hero 封面卡片:背景图由 .post-card 自身背景承载(跟随 corner-shape 圆角裁剪),
87
+ // 子图保留用于 hover 变暗/放大滤镜
88
+ el += `<a class="post-card ${layout}" style="--cover-url:url('${escape_html(post.cover.replace(/'/g, '%27'))}')" href="${pretty_url(post.link || post.path)}">`
89
+ } else {
90
+ el += `<a class="post-card ${layout}" href="${pretty_url(post.link || post.path)}">`
91
+ }
84
92
  el += content;
85
93
  el += '</a>';
86
94
  el += '</div>';
@@ -8,9 +8,9 @@ function layout_topic_list(partial) {
8
8
  continue
9
9
  }
10
10
  el += `<div class="post-card-wrap${scrollreveal(' ')}">`
11
- el += `<a class="post-card wiki topic" href="${pretty_url(topic.homepage?.path || '/')}">`
11
+ el += `<div class="post-card topic">`
12
12
  el += partial(topic)
13
- el += `</a>`
13
+ el += `</div>`
14
14
  el += `</div>`
15
15
  }
16
16
  return el
@@ -18,7 +18,7 @@ function layout_topic_list(partial) {
18
18
  function layoutDiv() {
19
19
  var el = ''
20
20
  el += partial('_partial/main/navbar/nav_tabs_blog')
21
- el += `<div class="post-list wiki topic">`;
21
+ el += `<div class="post-list topic">`;
22
22
  el += layout_topic_list(function(topic) {
23
23
  return partial('_partial/main/post_list/topic_card', {topic: topic})
24
24
  })
@@ -27,4 +27,4 @@ function layoutDiv() {
27
27
  }
28
28
  %>
29
29
 
30
- <%- layoutDiv() %>
30
+ <%- layoutDiv() %>
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "hexo-theme-stellar",
3
- "version": "1.41.0",
3
+ "version": "1.42.0",
4
4
  "description": "Elegant and powerful theme for Hexo.",
5
5
  "main": "package.json",
6
6
  "scripts": {
7
7
  "lint": "eslint .",
8
- "test": "node ci/check-require-decls.js && node --test",
9
- "check": "npm run lint && npm test && python3 docs/knowledge/tools/verify.py",
8
+ "test": "node ci/check-require-decls.js && node --test && node ci/check-spec-refs.js",
9
+ "check": "npm run lint && npm test && python3 docs/knowledge/tools/verify.py && node ci/check-release-docs.js",
10
10
  "release": "node release.js",
11
11
  "release:dry": "node release.js --dry-run"
12
12
  },
@@ -5,7 +5,7 @@
5
5
  // 与 layout/_partial/scripts/defines.ejs 中旧的白名单保持一致(客户端通过 ctx.icons 读取)。
6
6
  hexo.extend.generator.register('stellar_icons', function () {
7
7
  const icons = (this.theme.config && this.theme.config.icons) || {};
8
- const keys = ['weibo:comment', 'default:loading-spinner', 'default:warning', 'weibo:repeat', 'weibo:like'];
8
+ const keys = ['default:tocomment', 'default:loading-spinner', 'default:warning', 'weibo:repeat', 'weibo:like'];
9
9
  const out = {};
10
10
  // 去除 SVG 注释(避免 <!-- / </script> 解析风险),再转义 <
11
11
  for (const k of keys) {
@@ -0,0 +1,8 @@
1
+ /* global hexo */
2
+ 'use strict';
3
+
4
+ const { subtitle } = require('../lib/subtitle');
5
+
6
+ hexo.extend.helper.register('subtitle', function(post) {
7
+ return subtitle(post);
8
+ });
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ const { stripHTML } = require('hexo-util');
4
+
5
+ // 文章一行小字取值(hero 卡片与置顶轮播共用,未来可复用于其它场景):
6
+ // subtitle > description > excerpt/content 前 length 字(去 HTML、压缩空白、截断;省略号由 CSS 单行处理),空则返回空串。
7
+ function subtitle(post, length = 50) {
8
+ if (post == null) {
9
+ return '';
10
+ }
11
+ if (post.subtitle != null && String(post.subtitle).length > 0) {
12
+ return post.subtitle;
13
+ }
14
+ if (post.description != null && String(post.description).length > 0) {
15
+ return post.description;
16
+ }
17
+ const text = stripHTML(post.excerpt || post.content || '').replace(/\s+/g, ' ').trim();
18
+ if (text.length === 0) {
19
+ return '';
20
+ }
21
+ return text.length > length ? text.slice(0, length) : text;
22
+ }
23
+
24
+ module.exports = { subtitle };
@@ -11,12 +11,13 @@
11
11
 
12
12
  module.exports = ctx => function(args, content) {
13
13
  args = ctx.args.map(args, ['bg', 'avatar', 'link'], ['title', 'subtitle'])
14
+ var bg = args.bg ? args.bg : ctx.theme.config.default.banner
14
15
  var el = ''
15
- el += `<div class="tag-plugin banner">`
16
+ el += `<div class="tag-plugin banner" style="--bg-url:url('${bg.replace(/'/g, '%27')}')">`
16
17
  // bg
17
- el += `<img class="lazy bg" data-src="${args.bg ? args.bg : ctx.theme.config.default.banner}">`
18
+ el += `<img class="lazy bg" data-src="${bg}">`
18
19
  // content
19
- el += `<div class="content">`
20
+ el += `<div class="content" data-text-adaptive="split">`
20
21
  // content.top
21
22
  el += `<div class="top">`
22
23
  // content.top.back
@@ -0,0 +1,111 @@
1
+ // 背景图覆盖层通用能力:同图渐变模糊层 + 黑色渐变蒙版 + hover 变暗
2
+ // $url-var: 背景图 CSS 变量(--cover-url / --pin-cover-url / --bg-url),模糊层取同图背景
3
+ // $sides: top | bottom | both —— 文字所在边缘;both 依赖宿主内的 .banner-mask-top/bottom 蒙版元素
4
+ // $layer: 覆盖层宿主选择器(相对当前元素),缺省为当前元素自身;图片等 hover 目标由调用方按 DOM 自行接入
5
+ cover-overlay($url-var, $sides = bottom, $layer = null)
6
+ $host = $layer ? $layer : '&'
7
+ // 统一默认观感变量(与文章列表 hero 封面一致)
8
+ --img-br: 100%
9
+ --img-sat: 100%
10
+ --blur-px: 1em
11
+ --blur-height: 128px
12
+ --blur-sat: 120%
13
+ --cover-zoom: 1.05
14
+ // hover 整体变暗:亮度 75%、饱和度 120%,模糊层饱和度提升至 200%
15
+ &:hover
16
+ --img-br: 75%
17
+ --img-sat: 120%
18
+ --blur-sat: 200%
19
+ if $sides == both
20
+ // 双侧::before 顶部模糊层 + :after 底部模糊层,黑色蒙版由 .banner-mask-top/bottom 承担
21
+ {$host}:before, {$host}:after
22
+ content: ''
23
+ position: absolute
24
+ top: 0
25
+ left: 0
26
+ width: 100%
27
+ height: 100%
28
+ z-index: 0
29
+ background-image: $url-var
30
+ background-size: cover
31
+ background-position: center
32
+ filter: brightness(var(--img-br)) saturate(var(--img-sat)) blur(var(--blur-px)) saturate(var(--blur-sat))
33
+ // 常驻恒等 transform:hover 放大时 Safari 正确应用父级圆角裁剪(同 list.styl 注释)
34
+ transform: translateZ(0)
35
+ pointer-events: none
36
+ transition: all .2s ease-out, transform .5s ease-out
37
+ -moz-transition: all .2s ease-out, transform .5s ease-out
38
+ -webkit-transition: all .2s ease-out, transform .5s ease-out
39
+ -o-transition: all .2s ease-out, transform .5s ease-out
40
+ {$host}:before
41
+ mask: linear-gradient(black, rgba(black, 0.75) calc(var(--blur-height-top, var(--blur-height)) * 0.5), transparent var(--blur-height-top, var(--blur-height)))
42
+ -webkit-mask: linear-gradient(black, rgba(black, 0.75) calc(var(--blur-height-top, var(--blur-height)) * 0.5), transparent var(--blur-height-top, var(--blur-height)))
43
+ {$host}:after
44
+ mask: linear-gradient(to top, black, rgba(black, 0.75) calc(var(--blur-height-bottom, var(--blur-height)) * 0.5), transparent var(--blur-height-bottom, var(--blur-height)))
45
+ -webkit-mask: linear-gradient(to top, black, rgba(black, 0.75) calc(var(--blur-height-bottom, var(--blur-height)) * 0.5), transparent var(--blur-height-bottom, var(--blur-height)))
46
+ if $layer
47
+ &:hover
48
+ {$host}:before, {$host}:after
49
+ transform: translateZ(0) scale(var(--cover-zoom))
50
+ else
51
+ {$host}:hover:before, {$host}:hover:after
52
+ transform: translateZ(0) scale(var(--cover-zoom))
53
+ // 黑色渐变蒙版:文字所在边缘不透明度约 0.25,垂直中线为 0
54
+ {$host} .banner-mask
55
+ position: absolute
56
+ inset: 0
57
+ z-index: 1
58
+ pointer-events: none
59
+ &.banner-mask-top
60
+ background-image: linear-gradient(rgba(0,0,0,0.25), rgba(0,0,0,0) 50%)
61
+ &.banner-mask-bottom
62
+ background-image: linear-gradient(rgba(0,0,0,0) 50%, rgba(0,0,0,0.25))
63
+ else
64
+ // 单侧::before 同图模糊层 + :after 黑色渐变蒙版
65
+ {$host}:before
66
+ content: ''
67
+ position: absolute
68
+ top: 0
69
+ left: 0
70
+ width: 100%
71
+ height: 100%
72
+ z-index: 1
73
+ background-image: $url-var
74
+ background-size: cover
75
+ background-position: center
76
+ filter: brightness(var(--img-br)) saturate(var(--img-sat)) blur(var(--blur-px)) saturate(var(--blur-sat))
77
+ // 常驻恒等 transform:hover 放大时 Safari 正确应用父级圆角裁剪(同 list.styl 注释)
78
+ transform: translateZ(0)
79
+ pointer-events: none
80
+ transition: all .2s ease-out, transform .5s ease-out
81
+ -moz-transition: all .2s ease-out, transform .5s ease-out
82
+ -webkit-transition: all .2s ease-out, transform .5s ease-out
83
+ -o-transition: all .2s ease-out, transform .5s ease-out
84
+ if $sides == top
85
+ {$host}:before
86
+ mask: linear-gradient(black, rgba(black, 0.75) calc(var(--blur-height-top, var(--blur-height)) * 0.5), transparent var(--blur-height-top, var(--blur-height)))
87
+ -webkit-mask: linear-gradient(black, rgba(black, 0.75) calc(var(--blur-height-top, var(--blur-height)) * 0.5), transparent var(--blur-height-top, var(--blur-height)))
88
+ else
89
+ {$host}:before
90
+ mask: linear-gradient(to top, black, rgba(black, 0.75) calc(var(--blur-height-bottom, var(--blur-height)) * 0.5), transparent var(--blur-height-bottom, var(--blur-height)))
91
+ -webkit-mask: linear-gradient(to top, black, rgba(black, 0.75) calc(var(--blur-height-bottom, var(--blur-height)) * 0.5), transparent var(--blur-height-bottom, var(--blur-height)))
92
+ if $layer
93
+ &:hover
94
+ {$host}:before
95
+ transform: translateZ(0) scale(var(--cover-zoom))
96
+ else
97
+ {$host}:hover:before
98
+ transform: translateZ(0) scale(var(--cover-zoom))
99
+ {$host}:after
100
+ content: ''
101
+ position: absolute
102
+ top: 0
103
+ left: 0
104
+ width: 100%
105
+ height: 100%
106
+ z-index: 1
107
+ pointer-events: none
108
+ if $sides == top
109
+ background-image: linear-gradient(rgba(0,0,0,0.25), rgba(0,0,0,0) 50%)
110
+ else
111
+ background-image: linear-gradient(rgba(0,0,0,0) 50%, rgba(0,0,0,0.25))
@@ -15,6 +15,23 @@ h5
15
15
  h6
16
16
  font-size: 'calc(%s + 0px)' % var(--fsp)
17
17
 
18
+ // story 文章段落标题(h2):居中 + 两侧 accent 斜杠装饰
19
+ // 专栏列表页专栏标题(.topic-title)与 story 文章 h2 共用
20
+ story-title()
21
+ color: var(--text)
22
+ line-height: 1.2
23
+ text-align: center
24
+ &:before,&:after
25
+ content: ''
26
+ display: inline-block
27
+ width: 12px
28
+ height: 1.2em
29
+ background: $c-accent
30
+ transform: skewX(-20deg)
31
+ margin: 0 0.5em
32
+ vertical-align: top
33
+ position: relative
34
+
18
35
  // 次级段落字号
19
36
  .fs15
20
37
  --fsp: $fs-15