hexo-theme-stellar 2.0.0-rc.5 → 2.0.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 (47) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +8 -0
  3. package/README_EN.md +8 -0
  4. package/_config.yml +12 -1
  5. package/languages/en.yml +0 -2
  6. package/languages/zh-CN.yml +0 -2
  7. package/languages/zh-TW.yml +0 -2
  8. package/layout/_partial/cover/wiki_cover.ejs +9 -3
  9. package/layout/_partial/main/navbar/article_banner.ejs +9 -0
  10. package/layout/_partial/main/notebook/notebook_card.ejs +33 -13
  11. package/layout/_partial/primitives/navigation.ejs +3 -2
  12. package/layout/_partial/regions/widgets.ejs +1 -1
  13. package/layout/_partial/sidebar/menu.ejs +3 -0
  14. package/layout/notebooks.ejs +8 -6
  15. package/layout/settings.ejs +2 -4
  16. package/legal/THIRD-PARTY-NOTICES.md +2 -2
  17. package/package.json +1 -1
  18. package/scripts/events/lib/notebooks.js +5 -1
  19. package/scripts/lib/client-assets.js +1 -0
  20. package/scripts/lib/config-schema.js +2 -2
  21. package/scripts/lib/contribution-registry.js +14 -0
  22. package/scripts/lib/hero-effect-registry.js +50 -1
  23. package/scripts/lib/image-metadata.js +19 -4
  24. package/scripts/lib/internal-constants.js +1 -0
  25. package/scripts/lib/models/wiki.js +2 -0
  26. package/scripts/lib/regions.js +8 -1
  27. package/scripts/lib/source-config.js +6 -1
  28. package/scripts/schema/config-rules.js +3 -0
  29. package/scripts/schema/content-config-rules.js +4 -0
  30. package/scripts/schema/content-config-schema.js +2 -0
  31. package/scripts/schema/model-schema.js +4 -0
  32. package/source/css/_common/base.styl +1 -0
  33. package/source/css/_common/input.styl +12 -15
  34. package/source/css/_common/loading.styl +17 -0
  35. package/source/css/_components/pages/settings.styl +0 -1
  36. package/source/css/_components/partial/cover.styl +20 -0
  37. package/source/css/_components/sidebar/nav-area.styl +10 -0
  38. package/source/css/_components/sidebar/search.styl +65 -1
  39. package/source/css/_components/widgets/toc.styl +1 -1
  40. package/source/css/_components/wiki-card.styl +3 -1
  41. package/source/js/main.js +51 -4
  42. package/source/js/profile.js +1 -1
  43. package/source/js/runtime/extensions/hero-parallax.js +70 -0
  44. package/source/js/runtime/extensions/partial-navigation.js +18 -1
  45. package/source/js/runtime/extensions/settings.js +2 -3
  46. package/source/js/runtime/hero-effects/strands.js +302 -0
  47. package/source/js/runtime/image-color.js +4 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,71 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.0
4
+
5
+ > 发布日期:2026-09-30
6
+
7
+ ### 新功能
8
+
9
+ - 配置、Collection YAML 与页面 Front Matter 采用声明式严格 Schema,字段类型、枚举与未知字段在构建前由 Doctor 报告来源与字段路径。
10
+ - Post、Topic、Wiki、Notebook、Note 与索引页共用同一内容模型与构建管线;`listed/searchable` 分别控制聚合展示与搜索收录,Article 与 Notebook 的列表、横幅、许可、分享、标签和排序默认值收敛到内容模型。
11
+ - 页面外壳统一为 Topbar、Leftbar、Rightbar Region,Brand、Menu、Widget、Footer 和 13 个 Profile 使用结构化配置与明确的覆盖语义;Leftbar 菜单支持 1–5 列,Tree、Linklist、Related 与右栏目录支持折叠,右栏随左栏收起显示紧凑目录。
12
+ - 新增 `card/glass/minimal/flat` 外观预设、明暗模式切换、颜色/渐变/排版/形状/背景设置,以及独立设置页。
13
+ - 浏览器功能改为原生 ES 模块按需加载,搜索、评论、动态效果、数据服务与请求缓存拥有明确的加载、取消与卸载生命周期;共享脚本、样式与图标不再逐页内联。
14
+ - 搜索改为注册式 provider(内置 `local` 与 `algolia`),入口位于 Leftbar Brand;本地搜索按章节组织结果并分批查询、渲染,浮层带开合转场与滚动条槽位补位,支持 `Command+K` / `Ctrl+K`。
15
+ - Wiki Hero 支持图片与视频背景、滚动视差,以及 `galaxy`、`light-rays`、`ferrofluid`、`Strands` 可选效果;标签目录新增 `gist`。
16
+ - 图片在 `hexo server` / `hexo generate` 时增量提取尺寸与 HSLA 平均色并持久缓存,补全生成 HTML 且不修改 Markdown 或原图;新增 `hexo stellar images`(含 `--dry-run/--page/--refresh`)。
17
+ - 新增 `hexo stellar doctor` 与 `hexo stellar new note`;目录驱动的完整 Blueprint 改由 Stellar Examples 仓库维护。
18
+ - 文章横幅背景、访客头像镜像 `services.gravatar.base_url`、Reveal 的 `duration/interval/distance/blur` 与卡片 Spotlight/Tilt 均可独立配置。
19
+
20
+ ### 行为与样式变化
21
+
22
+ - 内容横幅与列表共用页面 `cover`;`article.banner.background`(默认 `true`)与 Front Matter `banner.background: false` 控制内容页横幅是否使用封面作为背景,关闭时只影响内容页,列表卡片、SEO 与分享仍使用 `cover`。博客列表页附加导航默认开启。
23
+ - 笔记本总索引卡片复用 Wiki 卡片的封面与项目栏视觉:`listing.cover` 作为封面、`listing.headline` 只取显式配置的大标题、`listing.caption` 作为项目副标题,未配置封面时降级为纯色卡片。
24
+ - 正文图片统一 `max-width: 100%` 与 `height: auto`,按原图比例约束高度;图片元数据只采集会被主题消费的图片,分享二维码、评论、侧边栏与页脚等页面框架图片不触发下载或写入缓存。
25
+ - 侧栏集合、Navbar、Dropdown、Grid、文章卡片、下载按钮与 Topbar 对齐改用统一的形状与状态反馈;设置页、单选框、标题装饰、导航下划线、目录指示条与移动端留白一并收敛。
26
+ - 运行环境升级为 Node.js 22+、Hexo 8+;源码读取兼容 Windows CRLF 与 BOM,避免 Front Matter 整体丢失。
27
+
28
+ ### 升级注意(配置变更与破坏性改动)
29
+
30
+ - v1 的 `logo/menubar/site_tree/style/tag_plugins/plugins/data_services/search.service` 等结构不会被兼容读取;请按[公开迁移说明](https://xaoxuu.com/wiki/stellar/migration/v1-to-v2/)逐项迁移到 `topbar/leftbar/rightbar/footer/profiles/appearance/tags/features/services/search`。
31
+ - Collection 与 Front Matter 需要迁移为 `collection/route/navigation/listing/visibility/banner/article/comments/render/seo` 等分组字段;页面 `banner.image` 改为顶层 `cover`,横幅不再使用独立比例配置。未知字段、错误类型与非法枚举会在构建早期失败。
32
+ - 分享服务由 `[wechat, weibo, email, link]` 改为 `qrcode/weibo/x/telegram/whatsapp/email`,配置位置为 `article.footer.share` 或内容 `footer.share`。
33
+ - `plugins.card_hover.enable` 改为独立的 `features.card_hover.spotlight` 与 `features.card_hover.tilt`,默认均为 `false`;`plugins.adaptive_text` 改为固定开启的内置行为。
34
+ - Site Info、Rating、Vote 的服务 endpoint 默认留空,需要自行部署并填写地址;依赖这些服务的站点升级后请显式配置。
35
+ - `features.partial_navigation.enabled` 与 `features.image_optimization.enabled` 默认开启,首次处理远程图片会增加构建时间;Hero 背景默认启用 0.2 强度视差,设为 0 关闭位移。
36
+ - `about`、`users` 标签以及对应的 `chat_users.yml`、`fcircle` 适配器已移除;`tip` 改为单标签 `{% tip 词句 pop:注解 %}`,`quot` 支持直接图标键与 `color`。
37
+ - 博客列表页附加导航 `profiles.blog_index.listing_nav.enabled` 默认开启;语言文件不再使用 `settings.profile.provider_managed` 与 `settings.profile.provider_unknown`。
38
+ - 浏览器后处理必须原样保留 `public/js/runtime/**/*.js` 的 ESM 语义、相对 import 与 JavaScript MIME 类型。
39
+ - v2 源码从仓库 `main` 安装并需要执行 `npm install --prefix themes/stellar`;npm 安装的版本以实际公开版本为准。
40
+
41
+ Full Changelog: [1.44.0...2.0.0](https://github.com/xaoxuu/hexo-theme-stellar/compare/1.44.0...2.0.0)
42
+
43
+ ## 2.0.0-rc.6
44
+
45
+ > 发布日期:2026-09-18
46
+
47
+ 本节记录从公开候选 `2.0.0-rc.5` 到 `2.0.0-rc.6` 最终文件树的净变化。
48
+
49
+ ### 新功能
50
+
51
+ - Wiki Hero 新增视频背景 `hero.background.video`,静音循环自动播放,图片可用作 poster;设置视频时不加载动态效果。
52
+ - Hero 新增滚动视差 `hero.background.parallax`,范围 0–1、默认 0.2,设为 0 关闭位移;图片与视频背景随滚动淡出,减少动态效果偏好下关闭视差位移。
53
+ - 新增 Strands WebGL 背景效果,支持光带配色、形态、发光及玻璃折射参数。
54
+ - Leftbar 新增 `menu_columns`,支持 1–5 列,默认 1;1–2 列显示图标与标题,3–5 列仅显示图标并保留无障碍名称。支持 Profile、Collection、Page 覆盖。
55
+ - 新增 `services.gravatar.base_url`,侧栏与设置页共享访客头像镜像地址,默认 `https://gravatar.com/avatar/`;自定义服务需兼容 SHA-256 并包含 `/avatar/` 路径。
56
+
57
+ ### 修复与改进
58
+
59
+ - 同集合页面切换等待超过 150ms 时显示加载反馈,完成、失败或取消后清理,快速切换不闪烁。
60
+ - 修复目录活动指示条在 CSS 压缩后高度丢失的问题。
61
+ - 中英文安装说明补充 Windows PowerShell Blueprint 安装命令。
62
+
63
+ ### 升级注意
64
+
65
+ - 现有 Hero 背景默认启用 0.2 强度视差;如需关闭位移,显式设置 `hero.background.parallax: 0`。
66
+
67
+ Full Changelog: [2.0.0-rc.5...2.0.0-rc.6](https://github.com/xaoxuu/hexo-theme-stellar/compare/2.0.0-rc.5...2.0.0-rc.6)
68
+
3
69
  ## 2.0.0-rc.5
4
70
 
5
71
  > 发布日期:2026-09-12
package/README.md CHANGED
@@ -49,10 +49,18 @@
49
49
 
50
50
  准备好 Git 和 npm 后,一条命令即可启动交互式蓝图安装器:
51
51
 
52
+ macOS / Linux:
53
+
52
54
  ```bash
53
55
  sh -c "$(curl -fsSL https://github.com/xaoxuu/hexo-theme-stellar-examples/raw/main/install.sh)"
54
56
  ```
55
57
 
58
+ Windows PowerShell 7+(`pwsh`):
59
+
60
+ ```powershell
61
+ & ([scriptblock]::Create((Invoke-RestMethod https://github.com/xaoxuu/hexo-theme-stellar-examples/raw/main/install.ps1)))
62
+ ```
63
+
56
64
  安装器会读取示例仓库的 [`blueprints.json`](https://github.com/xaoxuu/hexo-theme-stellar-examples/blob/main/blueprints.json),展示当前可用的蓝图,并依次确认蓝图、项目目录、依赖安装和创建计划。
57
65
 
58
66
  ### 从 npm 安装
package/README_EN.md CHANGED
@@ -49,10 +49,18 @@ Your writing preserves ideas while external data stays current. Remote content,
49
49
 
50
50
  With Git and npm available, launch the interactive Blueprint installer with one command:
51
51
 
52
+ macOS / Linux:
53
+
52
54
  ```bash
53
55
  sh -c "$(curl -fsSL https://github.com/xaoxuu/hexo-theme-stellar-examples/raw/main/install.sh)"
54
56
  ```
55
57
 
58
+ Windows PowerShell 7+ (`pwsh`):
59
+
60
+ ```powershell
61
+ & ([scriptblock]::Create((Invoke-RestMethod https://github.com/xaoxuu/hexo-theme-stellar-examples/raw/main/install.ps1)))
62
+ ```
63
+
56
64
  The installer reads the example repository's [`blueprints.json`](https://github.com/xaoxuu/hexo-theme-stellar-examples/blob/main/blueprints.json), presents the available Blueprints, then confirms the Blueprint, project directory, dependency installation, and creation plan.
57
65
 
58
66
  ### Install from npm
package/_config.yml CHANGED
@@ -58,6 +58,8 @@ leftbar:
58
58
  tagline: null
59
59
  # 设置品牌链接。
60
60
  href: /
61
+ # 设置 Leftbar 菜单列数(1–5);1–2 列显示图标与标题,3–5 列仅显示居中的图标。
62
+ menu_columns: 1
61
63
  # 设置 Leftbar 菜单项;数组会整体替换,空数组表示不显示菜单项。
62
64
  menu:
63
65
  # 声明默认的博客入口。
@@ -208,7 +210,7 @@ profiles:
208
210
  # 配置博客列表页的附加导航。
209
211
  listing_nav:
210
212
  # 控制是否显示附加导航。
211
- enabled: false
213
+ enabled: true
212
214
  # 设置附加导航标签;空数组表示不添加标签。
213
215
  tabs: []
214
216
 
@@ -458,6 +460,11 @@ article:
458
460
  style: tech
459
461
  # 设置段落首行缩进策略;可选值为 auto、always 或 never。
460
462
  paragraph_indent: auto
463
+ # 配置文章页顶部横幅。
464
+ banner:
465
+ # 控制文章页横幅是否使用页面 cover 作为背景图;false 表示只显示纯色横幅。
466
+ # 可在文章 Front Matter 用 banner.background 覆盖此默认值。
467
+ background: true
461
468
  # 配置文章列表卡片。
462
469
  listing:
463
470
  # 设置置顶文章布局;可选值为 carousel 或 flat。
@@ -930,6 +937,10 @@ features:
930
937
 
931
938
  # 配置主题使用的数据服务。
932
939
  services:
940
+ # 配置访客身份头像使用的 Gravatar 服务(侧边栏与设置页共享)。
941
+ gravatar:
942
+ # 设置兼容 SHA-256 的头像镜像地址,包含 /avatar/ 路径;末尾斜杠可省略。
943
+ base_url: https://gravatar.com/avatar/
933
944
  # 配置所有动态 Markdown 内容共享的 Marked 客户端。
934
945
  markdown:
935
946
  # 自定义脚本地址;null 使用默认资源。
package/languages/en.yml CHANGED
@@ -98,8 +98,6 @@ settings:
98
98
  url: URL
99
99
  save: Save
100
100
  logout: Log out
101
- provider_managed: '%s manages comment identity outside the theme. Profile editing and logout are unavailable here.'
102
- provider_unknown: The current comment provider
103
101
  cache:
104
102
  title: Cache
105
103
  search: Search cache
@@ -98,8 +98,6 @@ settings:
98
98
  url: URL
99
99
  save: 保存
100
100
  logout: 退出登录
101
- provider_managed: '%s 的评论身份不由主题管理,无法在此编辑或退出。'
102
- provider_unknown: 当前评论服务
103
101
  cache:
104
102
  title: 缓存
105
103
  search: 搜索缓存
@@ -98,8 +98,6 @@ settings:
98
98
  url: URL
99
99
  save: 儲存
100
100
  logout: 登出
101
- provider_managed: '%s 的留言身分不由主題管理,無法在此編輯或登出。'
102
- provider_unknown: 目前留言服務
103
101
  cache:
104
102
  title: 快取
105
103
  search: 搜尋快取
@@ -13,6 +13,7 @@ function layoutWikiCover() {
13
13
  }
14
14
  var backgroundConfig = cover.background || {};
15
15
  var background = typeof backgroundConfig.image === 'string' && backgroundConfig.image.length > 0 ? backgroundConfig.image : '';
16
+ var video = typeof backgroundConfig.video === 'string' ? backgroundConfig.video.trim() : '';
16
17
  var effect = backgroundConfig.effect || {};
17
18
  var preview = cover.preview || {};
18
19
  var commands = Array.isArray(preview.commands) ? preview.commands.filter(function (item) {
@@ -25,7 +26,7 @@ function layoutWikiCover() {
25
26
  var actions = Array.isArray(cover.actions) ? cover.actions.filter(function (item) {
26
27
  return item && item.title && item.url;
27
28
  }) : [];
28
- var hasEffect = typeof effect.type === 'string' && effect.type.length > 0;
29
+ var hasEffect = !video && typeof effect.type === 'string' && effect.type.length > 0;
29
30
  var isImageBackground = background.length > 0;
30
31
  var effectOptions = effect.options && typeof effect.options === 'object' && !Array.isArray(effect.options) ? effect.options : {};
31
32
  var effectRuntime = effect.runtime && typeof effect.runtime === 'object' && !Array.isArray(effect.runtime) ? effect.runtime : {};
@@ -37,18 +38,23 @@ function layoutWikiCover() {
37
38
  var effectRuntimeJson = escape_html(JSON.stringify(effectRuntime));
38
39
  var projectName = cover.projectName;
39
40
  var adaptiveTextAttr = '';
40
- if (isImageBackground) {
41
+ if (video) {
42
+ adaptiveTextAttr = ' data-text-adaptive="split" style="--adaptive-background:#000000"';
43
+ } else if (isImageBackground) {
41
44
  adaptiveTextAttr = ' data-text-adaptive="split" style="--cover-url:url(\'' + escape_html(background.replace(/'/g, '%27')) + '\')"';
42
45
  } else if (hasEffect) {
43
46
  adaptiveTextAttr = ' data-text-adaptive="split" style="--adaptive-background:' + escape_html(effectBackground) + '"';
44
47
  }
45
48
 
46
49
  el += '<div class="cover-content wiki wiki-hero' + (hasPreview ? '' : ' without-preview') + '">';
47
- el += '<div class="wiki-cover-background' + (hasEffect ? ' has-effect' : '') + (respectsReducedMotion ? ' respect-reduced-motion' : '') + (isImageBackground ? ' image' : '') + '"' + (hasEffect ? ' style="--hero-effect-background:' + escape_html(effectBackground) + '"' : '') + ' aria-hidden="true">';
50
+ el += '<div class="wiki-cover-background' + (hasEffect ? ' has-effect' : '') + (respectsReducedMotion ? ' respect-reduced-motion' : '') + (isImageBackground ? ' image' : '') + (video ? ' has-video' : '') + '" data-parallax="' + escape_html(String(backgroundConfig.parallax)) + '"' + (hasEffect ? ' style="--hero-effect-background:' + escape_html(effectBackground) + '"' : '') + ' aria-hidden="true">';
48
51
  if (isImageBackground) {
49
52
  el += '<img class="wiki-cover-background-image" src="' + escape_html(background) + '" alt="" />';
50
53
  el += '<img class="wiki-cover-background-blur" src="' + escape_html(background) + '" alt="" />';
51
54
  }
55
+ if (video) {
56
+ el += '<video class="wiki-cover-background-video" src="' + escape_html(video) + '"' + (isImageBackground ? ' poster="' + escape_html(background) + '"' : '') + ' autoplay muted loop playsinline preload="metadata" disablepictureinpicture tabindex="-1"></video>';
57
+ }
52
58
  if (hasEffect) {
53
59
  el += '<canvas data-hero-effect="' + escape_html(effect.type) + '" data-effect-resource="' + effectResourceJson + '" data-effect-options="' + effectOptionsJson + '" data-effect-runtime="' + effectRuntimeJson + '"></canvas>';
54
60
  }
@@ -18,6 +18,15 @@ var banner = Object.assign({}, page.author
18
18
  : postViewModel ? postViewModel.item.presentation.banner : pageConfig.banner || {})
19
19
 
20
20
  var bannerImage = page.author ? page.author.cover : renderViewModel ? renderViewModel.item.cover : pageConfig.cover
21
+ // 横幅背景图由 Front Matter banner.background 覆盖全局默认值;作者页不受全局开关影响。
22
+ // 关闭时仅影响内容页横幅,列表卡片、SEO 与分享仍使用 cover。
23
+ var bannerBackground = banner.background
24
+ if (bannerBackground == null && !page.author) {
25
+ bannerBackground = stellar_config('article.banner.background')
26
+ }
27
+ if (bannerBackground === false) {
28
+ bannerImage = ''
29
+ }
21
30
 
22
31
  // 标题
23
32
  if (banner.headline == null) {
@@ -1,17 +1,37 @@
1
- <%# 笔记本信息卡片 %>
1
+ <%# 笔记本卡片:复用 Wiki 卡片的 .wiki-card 视觉(cover 同图模糊层、底部信息层与项目栏) %>
2
2
  <% var listing = render.listing %>
3
- <article class="md-text">
4
- <div class="preview">
5
- <% if (listing.icon) { %>
6
- <img src="<%= listing.icon %>" alt="icon"/>
7
- <% } else { %>
8
- <%- icon('default:documents') %>
9
- <% } %>
10
- </div>
11
- <div class="excerpt">
12
- <h2 class="post-title"><%= listing.headline || listing.name %></h2>
13
- <% if (listing.description) { %>
14
- <p><%= listing.description %></p>
3
+ <% var cover = listing.cover || '' %>
4
+ <% var headline = listing.headline || '' %>
5
+ <% var caption = listing.caption || '' %>
6
+ <%# 整个属性必须原样输出:EJS 转义输出会把属性里的引号变成实体,属性值带上一对引号,自适应颜色就失效 %>
7
+ <% var adaptiveAttr = cover ? ' data-text-adaptive="split"' : '' %>
8
+ <article class="wiki-card-content">
9
+ <div class="wiki-card-cover<%= cover ? '' : ' no-cover' %>" position="bottom">
10
+ <% if (cover) { %>
11
+ <img src="<%= cover %>" alt=""/>
15
12
  <% } %>
13
+ <div class="wiki-card-info" position="bottom"<%- adaptiveAttr %>>
14
+ <%# headline 是可选的大标题,只在显式配置时渲染;name 固定在底部项目栏 %>
15
+ <% if (headline) { %>
16
+ <div class="wiki-card-summary">
17
+ <h2 class="headline"><%= headline %></h2>
18
+ </div>
19
+ <% } %>
20
+ <div class="wiki-project">
21
+ <div class="project-icon">
22
+ <% if (listing.icon) { %>
23
+ <img src="<%= listing.icon %>" alt=""/>
24
+ <% } else { %>
25
+ <%- icon('default:documents') %>
26
+ <% } %>
27
+ </div>
28
+ <div class="project-text">
29
+ <div class="project-name"><%= listing.name %></div>
30
+ <% if (caption) { %>
31
+ <div class="project-subtitle"><%= caption %></div>
32
+ <% } %>
33
+ </div>
34
+ </div>
35
+ </div>
16
36
  </div>
17
37
  </article>
@@ -5,8 +5,9 @@ if (!allowedPlacements.includes(placement)) {
5
5
  }
6
6
  var navigationItems = Array.isArray(entries) ? entries : []
7
7
  var navigationBody = partial('_partial/components/collection', {options: {
8
- layout: 'list',
9
- variant: 'nav',
8
+ layout: placement === 'leftbar' && typeof columns !== 'undefined' && columns > 1 ? 'grid' : 'list',
9
+ columns: typeof columns !== 'undefined' ? columns : 1,
10
+ variant: placement === 'leftbar' && typeof columns !== 'undefined' && columns > 2 ? 'icon' : 'nav',
10
11
  items: navigationItems
11
12
  }})
12
13
  var html = ''
@@ -69,7 +69,7 @@ function stack(items, zone, options) {
69
69
  <% if (leftbarBrandBody.trim().length > 0) { %><div class="widget-instance widget-instance--brand" data-widget-id="leftbar:fixed:brand" data-presentation="leftbar" data-leftbar-rail><%- leftbarBrandBody %></div><% } %>
70
70
  </header>
71
71
  <div class="site-region__body site-region__zone site-region__zone--body">
72
- <% if (menuItems.length > 0) { %><div class="widget-instance widget-instance--menu" data-widget-id="leftbar:fixed:menu" data-presentation="leftbar" data-leftbar-rail><%- partial('../sidebar/menu', {where: 'leftbar', menu: menuItems, activeMenu: activeViewModel?.item.navigation.menu, viewModel: activeViewModel}) %></div><% } %>
72
+ <% if (menuItems.length > 0) { %><div class="widget-instance widget-instance--menu" data-widget-id="leftbar:fixed:menu" data-presentation="leftbar" data-leftbar-rail><%- partial('../sidebar/menu', {where: 'leftbar', menu: menuItems, menuColumns: regionState.menuColumns, activeMenu: activeViewModel?.item.navigation.menu, viewModel: activeViewModel}) %></div><% } %>
73
73
  <%- stack(regionWidgets, 'body') %>
74
74
  </div>
75
75
  <footer class="site-region__footer">
@@ -9,6 +9,7 @@ function layoutDiv() {
9
9
  var pathMenu = menu_active_by_path(page.path, menuItems)
10
10
  if (pathMenu != null) currentMenu = pathMenu
11
11
  }
12
+ var iconLayout = where === 'leftbar' && typeof menuColumns !== 'undefined' && menuColumns > 2
12
13
  var entries = []
13
14
  for (var menuItem of menuItems) {
14
15
  if (menuItem == null || menuItem.id == null || menuItem.url == null || menuItem.title == null) {
@@ -18,6 +19,7 @@ function layoutDiv() {
18
19
  href: pretty_url(menuItem.url),
19
20
  title: __(menuItem.title),
20
21
  icon: menuItem.icon,
22
+ attrs: iconLayout ? {'aria-label': __(menuItem.title)} : {},
21
23
  theme: menuItem.accent,
22
24
  active: menuItem.id == currentMenu,
23
25
  className: menuItem.accent?.length > 0 ? 'is-themed' : ''
@@ -25,6 +27,7 @@ function layoutDiv() {
25
27
  }
26
28
  return partial('_partial/primitives/navigation', {
27
29
  placement: ['main', 'topbar', 'rightbar'].includes(where) ? where : 'leftbar',
30
+ columns: where === 'leftbar' && typeof menuColumns !== 'undefined' ? menuColumns : 1,
28
31
  entries: entries
29
32
  })
30
33
  }
@@ -1,10 +1,12 @@
1
- <%# 笔记本列表页主体部分 %>
2
- <% for (var notebook of page.notebookIndex.items) { %>
3
- <div class="post-list wiki">
1
+ <%# 笔记本列表页主体部分:复用 Wiki 列表的网格与卡片组件 %>
2
+ <div class="post-list wiki">
3
+ <% for (var notebook of page.notebookIndex.items) { %>
4
+ <% var cover = notebook.cover || '' %>
5
+ <% var coverStyle = cover ? ` style="--cover-url:url('${escape_html(cover.replace(/'/g, '%27'))}')"` : '' %>
4
6
  <div class="post-card-wrap<%= scrollreveal(' ') %>">
5
- <a class="<%- ui_classes('post-card wiki', 'hoverCard') %>" href="<%= pretty_url(notebook.href) %>">
7
+ <a class="<%- ui_classes('wiki-card', 'hoverCard', cover ? '' : 'no-cover') %>"<%- coverStyle %> href="<%= pretty_url(notebook.href) %>">
6
8
  <%- include('_partial/main/notebook/notebook_card', { render: {listing: notebook} }) %>
7
9
  </a>
8
10
  </div>
9
- </div>
10
- <% } %>
11
+ <% } %>
12
+ </div>
@@ -29,6 +29,7 @@ function linkAttributes(url) {
29
29
  <h1><%- escape_html(__('settings.title')) %></h1>
30
30
  </header>
31
31
 
32
+ <% if (providerSupported) { %>
32
33
  <section class="settings-section" aria-labelledby="settings-profile">
33
34
  <h2 id="settings-profile"><%- escape_html(__('settings.profile.title')) %></h2>
34
35
  <div class="settings-section__body">
@@ -38,9 +39,6 @@ function linkAttributes(url) {
38
39
  <span data-profile-avatar-fallback hidden><%- icon('default:profile', '', true) %></span>
39
40
  </div>
40
41
  <div class="settings-profile__content">
41
- <% if (!providerSupported) { %>
42
- <p data-provider-note class="settings-provider-note"><%- escape_html(__('settings.profile.provider_managed', provider || __('settings.profile.provider_unknown'))) %></p>
43
- <% } else { %>
44
42
  <form data-profile-form class="settings-form" novalidate>
45
43
  <label>
46
44
  <span><%- escape_html(__('settings.profile.nickname')) %></span>
@@ -60,11 +58,11 @@ function linkAttributes(url) {
60
58
  </div>
61
59
  </form>
62
60
  <p class="settings-result" data-profile-status role="status" aria-live="polite"></p>
63
- <% } %>
64
61
  </div>
65
62
  </div>
66
63
  </div>
67
64
  </section>
65
+ <% } %>
68
66
 
69
67
  <section class="settings-section" aria-labelledby="settings-cache">
70
68
  <h2 id="settings-cache"><%- escape_html(__('settings.cache.title')) %></h2>
@@ -5,10 +5,10 @@ This project includes or adapts portions of the following third-party software.
5
5
  ## React Bits
6
6
 
7
7
  - Project: [React Bits](https://github.com/DavidHDev/react-bits)
8
- - Components: Ferrofluid, Galaxy and Spotlight Card
8
+ - Components: Ferrofluid, Galaxy, Strands and Spotlight Card
9
9
  - License: MIT
10
10
  - Copyright: Copyright (c) 2026 David Haz
11
- - Usage: Adapted for the Wiki Ferrofluid and Galaxy WebGL backgrounds and Card Hover spotlight layer.
11
+ - Usage: Adapted for the Wiki Ferrofluid, Galaxy and Strands WebGL backgrounds and Card Hover spotlight layer.
12
12
 
13
13
  ### MIT License
14
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hexo-theme-stellar",
3
- "version": "2.0.0-rc.5",
3
+ "version": "2.0.0",
4
4
  "description": "An all-in-one Hexo theme for blogs, project documentation, knowledge bases, series, and notebooks.",
5
5
  "main": "package.json",
6
6
  "scripts": {
@@ -137,9 +137,13 @@ module.exports = (ctx, pipeline) => {
137
137
  id: collectionId,
138
138
  href: baseDir,
139
139
  name: identity.name,
140
- headline: identity.headline,
140
+ // 大标题只取显式配置的 headline;identity.headline 会回退到 name,
141
+ // 直接透传会让「删掉 headline」仍然渲染出同名大标题。
142
+ headline: typeof collectionConfig.headline === "string" ? collectionConfig.headline : "",
143
+ caption: identity.tagline || identity.description,
141
144
  description: identity.description,
142
145
  icon: identity.icon || "",
146
+ cover: collection.cover || "",
143
147
  order: collection.listing.order,
144
148
  listed: collection.visibility.listed !== false,
145
149
  navigation: {
@@ -22,6 +22,7 @@ function clientAssets(hexo) {
22
22
  'default:settings', 'copy:copy', 'weibo:repeat', 'weibo:like'];
23
23
  const clientIcons = Object.fromEntries(keys.map(key => [key, (icons[key] || '').replace(/<!--[\s\S]*?-->/g, '')]));
24
24
  const services = JSON.parse(JSON.stringify(INTERNAL.assets.services));
25
+ services.gravatar = { baseUrl: config.services.gravatar.baseUrl };
25
26
  services.siteinfo.api = resolveServiceProvider(config.services.siteInfo)?.endpoint || null;
26
27
  const provider = config.search.provider;
27
28
  const search = provider ? { service: provider === 'local' ? 'local_search' : provider } : null;
@@ -256,11 +256,11 @@ function validateEffectOptions(node, definition, options, source, path, issues)
256
256
  issues.push(issue("invalid_value", source, optionPath, valueType(value), "six-digit hexadecimal color", node.migration));
257
257
  } else if (rule.type === "hex_color_array" && (
258
258
  !Array.isArray(value)
259
- || value.length < 1
259
+ || value.length < (rule.minItems ?? 1)
260
260
  || value.length > 8
261
261
  || value.some(item => typeof item !== "string" || !/^#?[a-f\d]{6}$/i.test(item))
262
262
  )) {
263
- issues.push(issue("invalid_value", source, optionPath, valueType(value), "one to eight six-digit hexadecimal colors", node.migration));
263
+ issues.push(issue("invalid_value", source, optionPath, valueType(value), `${rule.minItems ?? 1} to eight six-digit hexadecimal colors`, node.migration));
264
264
  }
265
265
  }
266
266
  }
@@ -139,6 +139,20 @@ const CONTRIBUTIONS = defineContributions([
139
139
  });
140
140
  }
141
141
  },
142
+ {
143
+ id: "hero-parallax",
144
+ scope: "region",
145
+ kind: "feature",
146
+ entry: runtimeEntry("/js/runtime/extensions/hero-parallax.js"),
147
+ resources: [],
148
+ activation: selector(".wiki-hero"),
149
+ schema: null,
150
+ i18n: null,
151
+ docs: { category: "Components", path: "docs/knowledge/03-内容系统/wiki-docs.md" },
152
+ tests: [RUNTIME_TEST, RUNTIME_CONSUMPTION_TEST],
153
+ defaultsOwner: null,
154
+ project: () => configResult({})
155
+ },
142
156
  {
143
157
  id: "hero-effect",
144
158
  scope: "region",
@@ -32,7 +32,7 @@ function optionMatches(rule, value) {
32
32
  if (rule.type === "hex_color") return typeof value === "string" && /^#?[a-f\d]{6}$/i.test(value);
33
33
  if (rule.type === "hex_color_array") {
34
34
  return Array.isArray(value)
35
- && value.length >= 1
35
+ && value.length >= (rule.minItems ?? 1)
36
36
  && value.length <= 8
37
37
  && value.every(item => typeof item === "string" && /^#?[a-f\d]{6}$/i.test(item));
38
38
  }
@@ -91,6 +91,55 @@ const LIGHT_RAYS_ORIGINS = Object.freeze([
91
91
  ]);
92
92
 
93
93
  const DEFINITIONS = defineHeroEffects([
94
+ {
95
+ id: "strands",
96
+ label: "Strands",
97
+ module: INTERNAL.assets.heroEffects.strands,
98
+ options: {
99
+ colors: { type: "hex_color_array", minItems: 0 },
100
+ count: { type: "number" },
101
+ speed: { type: "number" },
102
+ amplitude: { type: "number" },
103
+ waviness: { type: "number" },
104
+ thickness: { type: "number" },
105
+ glow: { type: "number" },
106
+ taper: { type: "number" },
107
+ spread: { type: "number" },
108
+ hueShift: { type: "number" },
109
+ intensity: { type: "number" },
110
+ saturation: { type: "number" },
111
+ opacity: { type: "number" },
112
+ scale: { type: "number" },
113
+ glass: { type: "boolean" },
114
+ refraction: { type: "number" },
115
+ dispersion: { type: "number" },
116
+ glassSize: { type: "number" }
117
+ },
118
+ defaults: {
119
+ colors: ["#F97316", "#7C3AED", "#06B6D4"],
120
+ count: 3,
121
+ speed: 0.5,
122
+ amplitude: 1,
123
+ waviness: 1,
124
+ thickness: 0.7,
125
+ glow: 2.6,
126
+ taper: 3,
127
+ spread: 1,
128
+ hueShift: 0,
129
+ intensity: 0.6,
130
+ saturation: 1.5,
131
+ opacity: 1,
132
+ scale: 1.5,
133
+ glass: false,
134
+ refraction: 1,
135
+ dispersion: 1,
136
+ glassSize: 1
137
+ },
138
+ presentation() {
139
+ return { background: "#000000" };
140
+ }
141
+ },
142
+
94
143
  {
95
144
  id: "galaxy",
96
145
  label: "Galaxy",
@@ -13,6 +13,20 @@ const MAX_BYTES = 32 * 1024 * 1024;
13
13
  const RETRY_DELAY = 24 * 60 * 60 * 1000;
14
14
  const stores = new WeakMap();
15
15
 
16
+ // The store serves two runtime consumers: article content images (dimensions
17
+ // for aspect ratio) and average-color backgrounds for adaptive text. Scan the
18
+ // rendered article region plus the adaptive-text owners, matching
19
+ // source/js/plugins/adaptive-text.js, so page chrome (share QR codes, comments,
20
+ // sidebars, footers) never triggers downloads or store entries.
21
+ const METADATA_IMAGE_SELECTOR = [
22
+ 'article.md-text.content img',
23
+ '.banner img',
24
+ '.cover img',
25
+ '.wiki-card-cover img',
26
+ '.pin-slide img',
27
+ '.wiki-hero img'
28
+ ].join(', ');
29
+
16
30
  function metadataPath(ctx) {
17
31
  return path.join(ctx.source_dir || path.join(ctx.base_dir || process.cwd(), 'source'), '_data', 'caches', DATA_FILE);
18
32
  }
@@ -91,12 +105,13 @@ function complete(entry) {
91
105
  return (dimensionsReady(entry?.dimensions) || entry?.dimensions?.status === 'unsupported') && colorDone;
92
106
  }
93
107
 
94
- // Scan rendered img elements, so Markdown, tags, Front Matter and data-driven
95
- // covers use their actual output without maintaining another source-field list.
108
+ // Scan rendered img elements in scope for the store, so Markdown, tags, Front
109
+ // Matter and data-driven covers use their actual output without maintaining
110
+ // another source-field list, while page chrome stays out of scope.
96
111
  function collectImages(html, base) {
97
112
  const urls = new Set();
98
113
  const $ = load(html, null, false);
99
- $('img').each((_, element) => {
114
+ $(METADATA_IMAGE_SELECTOR).each((_, element) => {
100
115
  const img = $(element);
101
116
  const url = imageUrl(img.attr('data-src') || img.attr('src'), base);
102
117
  if (url) urls.add(url);
@@ -247,7 +262,7 @@ function enrichImages(html, ctx, locals = {}) {
247
262
  let changed = false;
248
263
  const autoRatio = (ctx.stellar?.config?.features?.lazyLoading?.autoAspectRatio
249
264
  ?? ctx.theme?.config?.features?.lazy_loading?.auto_aspect_ratio) !== false;
250
- $('img').each((_, image) => {
265
+ $(METADATA_IMAGE_SELECTOR).each((_, image) => {
251
266
  const img = $(image);
252
267
  const src = img.attr('data-src') || img.attr('src');
253
268
  const entry = store.images[imageUrl(src, base)];
@@ -57,6 +57,7 @@ module.exports = deepFreeze({
57
57
  settings: "/js/runtime/extensions/settings.js"
58
58
  },
59
59
  heroEffects: {
60
+ strands: "/js/runtime/hero-effects/strands.js",
60
61
  ferrofluid: "/js/runtime/hero-effects/ferrofluid.js",
61
62
  galaxy: "/js/runtime/hero-effects/galaxy.js",
62
63
  lightRays: "/js/runtime/hero-effects/light-rays.js"
@@ -7,6 +7,7 @@ const { firstContentImage } = require("../seo");
7
7
  const { caption } = require("../caption");
8
8
  const { wikiReadmeHtml } = require("../wiki_readme");
9
9
  const { filterShareServices } = require("../share-services");
10
+ const { COLLECTION_CONFIG_SCHEMA } = require("../../schema/content-config-schema");
10
11
  const { projectHeroEffect } = require("../hero-effect-registry");
11
12
  const { normalizeThemeComments } = require("../comments");
12
13
  const { articleFooterDefaults, articlePresentationDefaults, collectionFooterDefaults, requireContentConfig } = require("../content-defaults");
@@ -14,6 +15,7 @@ const { cloneValue, deepFreeze, pick, mergeConfig, toContentNavigation, collecti
14
15
 
15
16
  function heroBackground(value) {
16
17
  const background = cloneValue(value || {});
18
+ background.parallax ??= COLLECTION_CONFIG_SCHEMA.properties.hero.properties.background.properties.parallax.default.value;
17
19
  if (background.effect) background.effect = projectHeroEffect(background.effect);
18
20
  return background;
19
21
  }