hexo-theme-stellar 2.0.0-rc.6 → 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.
- package/CHANGELOG.md +40 -0
- package/_config.yml +6 -1
- package/languages/en.yml +0 -2
- package/languages/zh-CN.yml +0 -2
- package/languages/zh-TW.yml +0 -2
- package/layout/_partial/main/navbar/article_banner.ejs +9 -0
- package/layout/_partial/main/notebook/notebook_card.ejs +33 -13
- package/layout/notebooks.ejs +8 -6
- package/layout/settings.ejs +2 -4
- package/package.json +1 -1
- package/scripts/events/lib/notebooks.js +5 -1
- package/scripts/lib/image-metadata.js +19 -4
- package/scripts/lib/source-config.js +6 -1
- package/scripts/schema/content-config-rules.js +1 -0
- package/scripts/schema/model-schema.js +1 -0
- package/source/css/_common/base.styl +1 -0
- package/source/css/_common/input.styl +12 -15
- package/source/css/_components/pages/settings.styl +0 -1
- package/source/css/_components/sidebar/search.styl +65 -1
- package/source/css/_components/wiki-card.styl +3 -1
- package/source/js/main.js +51 -4
- package/source/js/runtime/extensions/settings.js +2 -3
- package/source/js/runtime/image-color.js +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
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
|
+
|
|
3
43
|
## 2.0.0-rc.6
|
|
4
44
|
|
|
5
45
|
> 发布日期:2026-09-18
|
package/_config.yml
CHANGED
|
@@ -210,7 +210,7 @@ profiles:
|
|
|
210
210
|
# 配置博客列表页的附加导航。
|
|
211
211
|
listing_nav:
|
|
212
212
|
# 控制是否显示附加导航。
|
|
213
|
-
enabled:
|
|
213
|
+
enabled: true
|
|
214
214
|
# 设置附加导航标签;空数组表示不添加标签。
|
|
215
215
|
tabs: []
|
|
216
216
|
|
|
@@ -460,6 +460,11 @@ article:
|
|
|
460
460
|
style: tech
|
|
461
461
|
# 设置段落首行缩进策略;可选值为 auto、always 或 never。
|
|
462
462
|
paragraph_indent: auto
|
|
463
|
+
# 配置文章页顶部横幅。
|
|
464
|
+
banner:
|
|
465
|
+
# 控制文章页横幅是否使用页面 cover 作为背景图;false 表示只显示纯色横幅。
|
|
466
|
+
# 可在文章 Front Matter 用 banner.background 覆盖此默认值。
|
|
467
|
+
background: true
|
|
463
468
|
# 配置文章列表卡片。
|
|
464
469
|
listing:
|
|
465
470
|
# 设置置顶文章布局;可选值为 carousel 或 flat。
|
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
|
package/languages/zh-CN.yml
CHANGED
package/languages/zh-TW.yml
CHANGED
|
@@ -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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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>
|
package/layout/notebooks.ejs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
<%#
|
|
2
|
-
|
|
3
|
-
|
|
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('
|
|
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
|
-
|
|
10
|
-
|
|
11
|
+
<% } %>
|
|
12
|
+
</div>
|
package/layout/settings.ejs
CHANGED
|
@@ -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>
|
package/package.json
CHANGED
|
@@ -137,9 +137,13 @@ module.exports = (ctx, pipeline) => {
|
|
|
137
137
|
id: collectionId,
|
|
138
138
|
href: baseDir,
|
|
139
139
|
name: identity.name,
|
|
140
|
-
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: {
|
|
@@ -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
|
|
95
|
-
// covers use their actual output without maintaining
|
|
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
|
-
$(
|
|
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
|
-
$(
|
|
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)];
|
|
@@ -7,6 +7,11 @@ const snapshots = new WeakMap();
|
|
|
7
7
|
|
|
8
8
|
function sourcePathForData(key) { return `source/_data/${key}.yml`; }
|
|
9
9
|
function sourcePathForPage(page) { return page.source ? `source/${page.source}` : (page.path || "<page>"); }
|
|
10
|
+
// hexo-front-matter 只识别 LF;Hexo 宿主通过 hexo-fs 读取源码时会去除 BOM 并规范化 CRLF,
|
|
11
|
+
// 直接读取源码必须保持一致,否则 Windows 上的 CRLF 内容会丢失全部 Front Matter。
|
|
12
|
+
function readSourceText(file) {
|
|
13
|
+
return fs.readFileSync(file, "utf8").replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
|
|
14
|
+
}
|
|
10
15
|
function sourceCache(ctx) {
|
|
11
16
|
if (!snapshots.has(ctx)) snapshots.set(ctx, new Map());
|
|
12
17
|
return snapshots.get(ctx);
|
|
@@ -29,7 +34,7 @@ function readFrontMatter(ctx, page) {
|
|
|
29
34
|
const stamp = [stat.ino, stat.size, stat.mtimeNs, stat.ctimeNs].join(":");
|
|
30
35
|
const previous = cache.get(page.source);
|
|
31
36
|
if (previous?.stamp === stamp) return previous.config;
|
|
32
|
-
const config = frontMatter.parse(
|
|
37
|
+
const config = frontMatter.parse(readSourceText(file));
|
|
33
38
|
delete config._content;
|
|
34
39
|
cache.set(page.source, { stamp, config });
|
|
35
40
|
return config;
|
|
@@ -212,6 +212,7 @@ const FRONT_MATTER_TARGET_DEFINITIONS = [
|
|
|
212
212
|
["tagline", ["string", "null"], literal(null)],
|
|
213
213
|
["banner", "object", literal({}), { boundary: "sealed" }],
|
|
214
214
|
["banner.enabled", ["boolean", "null"], literal(null)],
|
|
215
|
+
["banner.background", ["boolean", "null"], literal(null)],
|
|
215
216
|
["banner.avatar", ["string", "null"], literal(null)],
|
|
216
217
|
["banner.headline", ["string", "null"], literal(null)],
|
|
217
218
|
["banner.tagline", ["string", "null"], literal(null)],
|
|
@@ -233,6 +233,7 @@ function bannerSchema(factory) {
|
|
|
233
233
|
const { field, object } = factory;
|
|
234
234
|
return object({
|
|
235
235
|
enabled: field("boolean", { example: true }),
|
|
236
|
+
background: field("boolean", { example: true }),
|
|
236
237
|
avatar: field("string", { example: "/avatar.webp" }),
|
|
237
238
|
headline: field("string", { example: "开始使用 Stellar" }),
|
|
238
239
|
tagline: field("string", { example: "十分钟搭好站点" })
|
|
@@ -9,6 +9,8 @@ input.copy-area
|
|
|
9
9
|
font-weight: $fw-700
|
|
10
10
|
color: var(--text-p3)
|
|
11
11
|
|
|
12
|
+
// 圆环与圆点画在同一个背景层上:元素落在非整数像素位置时,border 圆环与内部盒子会被
|
|
13
|
+
// 分别对齐,Firefox 下圆点最多偏离圆心 1 个物理像素,因此不用 border 绘制圆环。
|
|
12
14
|
input.ui-radio
|
|
13
15
|
-webkit-appearance: none
|
|
14
16
|
-moz-appearance: none
|
|
@@ -19,25 +21,20 @@ input.ui-radio
|
|
|
19
21
|
width: 16px
|
|
20
22
|
height: 16px
|
|
21
23
|
margin: 0
|
|
22
|
-
border:
|
|
24
|
+
border: 0
|
|
23
25
|
border-radius: 50%
|
|
24
26
|
corner-shape: round
|
|
25
|
-
|
|
27
|
+
--radio-ring: var(--text-p3)
|
|
28
|
+
--radio-dot-size: 0
|
|
29
|
+
// 外环:16px 内取 2px 环宽(半径 8px → 6px);圆点:8px(半径 4px)
|
|
30
|
+
background-image: radial-gradient(circle closest-side, transparent 0 5.875px, var(--radio-ring) 6.125px 100%), radial-gradient(circle closest-side, var(--theme) 0 3.875px, transparent 4.125px)
|
|
31
|
+
background-position: center, center
|
|
32
|
+
background-repeat: no-repeat, no-repeat
|
|
33
|
+
background-size: 16px 16px, var(--radio-dot-size) var(--radio-dot-size)
|
|
26
34
|
cursor: pointer
|
|
27
|
-
&:before
|
|
28
|
-
content: ""
|
|
29
|
-
display: block
|
|
30
|
-
width: 8px
|
|
31
|
-
height: 8px
|
|
32
|
-
margin: 2px
|
|
33
|
-
border-radius: 50%
|
|
34
|
-
background: var(--theme)
|
|
35
|
-
transform: scale(0)
|
|
36
35
|
&:checked
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
background: var(--theme)
|
|
40
|
-
transform: scale(1)
|
|
36
|
+
--radio-ring: var(--theme)
|
|
37
|
+
--radio-dot-size: 8px
|
|
41
38
|
&:focus-visible
|
|
42
39
|
outline: 2px solid var(--theme)
|
|
43
40
|
outline-offset: 2px
|
|
@@ -252,10 +252,62 @@ html[data-leftbar-state='collapsed'] .site-shell:not([data-drawer='leftbar']) .s
|
|
|
252
252
|
background: var(--card)
|
|
253
253
|
box-shadow: $boxshadow-card-float
|
|
254
254
|
overflow: hidden
|
|
255
|
+
// 打开动画随 [open] 触发;关闭动画由 main.js 添加 .is-closing 并在动画结束后调用 close()
|
|
256
|
+
&[open]
|
|
257
|
+
animation: search-dialog-in 0.24s ease-out backwards
|
|
258
|
+
&.is-closing
|
|
259
|
+
animation: search-dialog-out 0.18s ease-in both
|
|
255
260
|
&::backdrop
|
|
256
261
|
background: rgba(black, .38)
|
|
257
262
|
backdrop-filter: blur(8px)
|
|
258
263
|
-webkit-backdrop-filter: blur(8px)
|
|
264
|
+
animation: search-backdrop-in 0.24s ease-out backwards
|
|
265
|
+
&.is-closing::backdrop
|
|
266
|
+
animation: search-backdrop-out 0.18s ease-in both
|
|
267
|
+
|
|
268
|
+
@keyframes search-dialog-in
|
|
269
|
+
from
|
|
270
|
+
opacity: 0
|
|
271
|
+
transform: translateY(8px) scale(.97)
|
|
272
|
+
to
|
|
273
|
+
opacity: 1
|
|
274
|
+
transform: none
|
|
275
|
+
|
|
276
|
+
@keyframes search-dialog-out
|
|
277
|
+
from
|
|
278
|
+
opacity: 1
|
|
279
|
+
transform: none
|
|
280
|
+
to
|
|
281
|
+
opacity: 0
|
|
282
|
+
transform: translateY(8px) scale(.97)
|
|
283
|
+
|
|
284
|
+
@keyframes search-backdrop-in
|
|
285
|
+
from
|
|
286
|
+
opacity: 0
|
|
287
|
+
to
|
|
288
|
+
opacity: 1
|
|
289
|
+
|
|
290
|
+
@keyframes search-backdrop-out
|
|
291
|
+
from
|
|
292
|
+
opacity: 1
|
|
293
|
+
to
|
|
294
|
+
opacity: 0
|
|
295
|
+
|
|
296
|
+
@keyframes search-dialog-in-fluid
|
|
297
|
+
from
|
|
298
|
+
opacity: 0
|
|
299
|
+
transform: translateY(16px)
|
|
300
|
+
to
|
|
301
|
+
opacity: 1
|
|
302
|
+
transform: none
|
|
303
|
+
|
|
304
|
+
@keyframes search-dialog-out-fluid
|
|
305
|
+
from
|
|
306
|
+
opacity: 1
|
|
307
|
+
transform: none
|
|
308
|
+
to
|
|
309
|
+
opacity: 0
|
|
310
|
+
transform: translateY(16px)
|
|
259
311
|
|
|
260
312
|
.search-dialog__panel
|
|
261
313
|
box-sizing: border-box
|
|
@@ -359,6 +411,9 @@ html[data-leftbar-state='collapsed'] .site-shell:not([data-drawer='leftbar']) .s
|
|
|
359
411
|
|
|
360
412
|
html[data-search-open]
|
|
361
413
|
overflow: hidden
|
|
414
|
+
// 锁定滚动会移除常驻滚动条槽位,按 main.js 实测宽度补位,避免页面内容宽度变化后重排
|
|
415
|
+
body
|
|
416
|
+
padding-right: var(--stellar-scrollbar-gutter, 0px)
|
|
362
417
|
|
|
363
418
|
@media screen and (max-width: $device-tablet)
|
|
364
419
|
.search-dialog
|
|
@@ -367,6 +422,11 @@ html[data-search-open]
|
|
|
367
422
|
max-height: none
|
|
368
423
|
border: 0
|
|
369
424
|
border-radius: 0
|
|
425
|
+
// 全屏浮层不做缩放,避免露出被裁切的边缘
|
|
426
|
+
&[open]
|
|
427
|
+
animation-name: search-dialog-in-fluid
|
|
428
|
+
&.is-closing
|
|
429
|
+
animation-name: search-dialog-out-fluid
|
|
370
430
|
.search-dialog__panel
|
|
371
431
|
height: 100%
|
|
372
432
|
padding: unquote('max(12px, env(safe-area-inset-top)) max(12px, env(safe-area-inset-right)) max(12px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left))')
|
|
@@ -375,6 +435,10 @@ html[data-search-open]
|
|
|
375
435
|
|
|
376
436
|
@media (prefers-reduced-motion: reduce)
|
|
377
437
|
.search-dialog,
|
|
378
|
-
.search-dialog
|
|
438
|
+
.search-dialog[open],
|
|
439
|
+
.search-dialog.is-closing,
|
|
440
|
+
.search-dialog::backdrop,
|
|
441
|
+
.search-dialog[open]::backdrop,
|
|
442
|
+
.search-dialog.is-closing::backdrop
|
|
379
443
|
animation: none
|
|
380
444
|
transition: none
|
|
@@ -185,7 +185,9 @@
|
|
|
185
185
|
height: 2.5rem
|
|
186
186
|
app-icon()
|
|
187
187
|
background: var(--block)
|
|
188
|
-
|
|
188
|
+
// 未配置 icon 时的默认图标:颜色跟随封面平均色自适应(深色封面显示为接近白色)。
|
|
189
|
+
// --text-banner 只在有封面时由 adaptive-text 写入 .wiki-card-info,无封面时回退到次要文字色。
|
|
190
|
+
color: var(--text-banner, var(--text-p2))
|
|
189
191
|
img
|
|
190
192
|
width: 100%
|
|
191
193
|
height: 100%
|
package/source/js/main.js
CHANGED
|
@@ -103,6 +103,10 @@ const rightbarDrawerQuery = '(max-width: 1180px)';
|
|
|
103
103
|
let shellDrawerTrigger = null;
|
|
104
104
|
let searchDialogTrigger = null;
|
|
105
105
|
let searchDialogRestoreFocus = true;
|
|
106
|
+
// 退场动画的安全兜底时长,需大于 search.styl 中 search-dialog-out 的 0.18s
|
|
107
|
+
const searchCloseFallbackDelay = 260;
|
|
108
|
+
let searchCloseTimer = null;
|
|
109
|
+
let searchCloseListener = null;
|
|
106
110
|
let shellInputModality = 'pointer';
|
|
107
111
|
|
|
108
112
|
function regionElement(region) {
|
|
@@ -244,6 +248,11 @@ function applySearchScope(dialog, value, refresh = true) {
|
|
|
244
248
|
}
|
|
245
249
|
}
|
|
246
250
|
|
|
251
|
+
function searchMotionEnabled() {
|
|
252
|
+
if (typeof window.matchMedia !== 'function') return true;
|
|
253
|
+
return !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
254
|
+
}
|
|
255
|
+
|
|
247
256
|
function configureSearchScope(dialog, trigger) {
|
|
248
257
|
const group = dialog?.querySelector('.search-dialog__scope');
|
|
249
258
|
if (!group) return false;
|
|
@@ -265,10 +274,30 @@ function configureSearchScope(dialog, trigger) {
|
|
|
265
274
|
return true;
|
|
266
275
|
}
|
|
267
276
|
|
|
268
|
-
function
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
277
|
+
function clearSearchClosePending(dialog) {
|
|
278
|
+
if (searchCloseTimer !== null) {
|
|
279
|
+
clearTimeout(searchCloseTimer);
|
|
280
|
+
searchCloseTimer = null;
|
|
281
|
+
}
|
|
282
|
+
if (searchCloseListener) {
|
|
283
|
+
dialog.removeEventListener('animationend', searchCloseListener);
|
|
284
|
+
searchCloseListener = null;
|
|
285
|
+
}
|
|
286
|
+
dialog.classList.remove('is-closing');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 锁定页面滚动会移除常驻滚动条,先把槽位宽度写进 CSS 变量供补位使用
|
|
290
|
+
function syncSearchScrollbarGutter() {
|
|
291
|
+
const root = document.documentElement;
|
|
292
|
+
if (root.hasAttribute('data-search-open')) return;
|
|
293
|
+
const gutter = Math.max(0, window.innerWidth - root.clientWidth);
|
|
294
|
+
root.style.setProperty('--stellar-scrollbar-gutter', gutter + 'px');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// 关闭动画播放期间浮层仍处于打开状态,退出动画结束或兜底超时后才真正 close 并恢复焦点
|
|
298
|
+
function finishSearchClose(dialog) {
|
|
299
|
+
clearSearchClosePending(dialog);
|
|
300
|
+
if (dialog.open) dialog.close();
|
|
272
301
|
document.documentElement.removeAttribute('data-search-open');
|
|
273
302
|
if (searchDialogRestoreFocus && searchDialogTrigger?.focus) searchDialogTrigger.focus();
|
|
274
303
|
else if (searchDialogTrigger?.blur) searchDialogTrigger.blur();
|
|
@@ -276,12 +305,29 @@ function closeSearch() {
|
|
|
276
305
|
searchDialogRestoreFocus = true;
|
|
277
306
|
}
|
|
278
307
|
|
|
308
|
+
function closeSearch() {
|
|
309
|
+
const dialog = searchDialogElement();
|
|
310
|
+
if (!dialog?.open || searchCloseTimer !== null) return;
|
|
311
|
+
if (!searchMotionEnabled()) {
|
|
312
|
+
finishSearchClose(dialog);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
searchCloseListener = function (event) {
|
|
316
|
+
if (event.animationName !== 'search-dialog-out') return;
|
|
317
|
+
finishSearchClose(dialog);
|
|
318
|
+
};
|
|
319
|
+
dialog.classList.add('is-closing');
|
|
320
|
+
dialog.addEventListener('animationend', searchCloseListener);
|
|
321
|
+
searchCloseTimer = setTimeout(function () { finishSearchClose(dialog); }, searchCloseFallbackDelay);
|
|
322
|
+
}
|
|
323
|
+
|
|
279
324
|
function openSearch(trigger, restoreFocus = true) {
|
|
280
325
|
const dialog = searchDialogElement();
|
|
281
326
|
const input = dialog?.querySelector('.search-input');
|
|
282
327
|
const wrapper = dialog?.querySelector('.search-wrapper');
|
|
283
328
|
const result = dialog?.querySelector('.search-result');
|
|
284
329
|
if (!dialog || !input) return;
|
|
330
|
+
clearSearchClosePending(dialog);
|
|
285
331
|
searchDialogTrigger = trigger || document.activeElement;
|
|
286
332
|
searchDialogRestoreFocus = restoreFocus;
|
|
287
333
|
input.value = '';
|
|
@@ -294,6 +340,7 @@ function openSearch(trigger, restoreFocus = true) {
|
|
|
294
340
|
}
|
|
295
341
|
result?.replaceChildren();
|
|
296
342
|
if (!dialog.open) dialog.showModal();
|
|
343
|
+
syncSearchScrollbarGutter();
|
|
297
344
|
document.documentElement.setAttribute('data-search-open', '');
|
|
298
345
|
input.focus();
|
|
299
346
|
}
|
|
@@ -90,7 +90,6 @@ export function mount(root, context) {
|
|
|
90
90
|
const provider = String(page.dataset.provider || '').toLowerCase();
|
|
91
91
|
const api = globalThis.window?.stellarProfile;
|
|
92
92
|
const form = page.querySelector('[data-profile-form]');
|
|
93
|
-
const note = page.querySelector('[data-provider-note]');
|
|
94
93
|
const status = page.querySelector('[data-profile-status]');
|
|
95
94
|
const avatar = page.querySelector('[data-profile-avatar]');
|
|
96
95
|
const avatarFallback = page.querySelector('[data-profile-avatar-fallback]');
|
|
@@ -174,8 +173,8 @@ export function mount(root, context) {
|
|
|
174
173
|
if (urlInput) urlInput.value = identity.url || '';
|
|
175
174
|
if (form) form.hidden = !supported;
|
|
176
175
|
if (logoutButton) logoutButton.hidden = !supported;
|
|
177
|
-
if (
|
|
178
|
-
|
|
176
|
+
if (supported) renderAvatar(identity);
|
|
177
|
+
else if (avatar) avatar.hidden = true;
|
|
179
178
|
}
|
|
180
179
|
|
|
181
180
|
function onSubmit(event) {
|
|
@@ -195,7 +195,10 @@ export function readImageColor(image, options = {}) {
|
|
|
195
195
|
function onLoad() {
|
|
196
196
|
if (options.signal?.aborted || source(image) !== url) return finish(null);
|
|
197
197
|
const displayed = normalize(image.currentSrc || image.getAttribute('src'), image);
|
|
198
|
-
|
|
198
|
+
// 懒加载占位图(src 仍是 1×1 data URI):显示像素不可用,直接 return 会让本 Promise 永久挂起
|
|
199
|
+
// (懒加载运行时没执行时,真图永远不会被换进来),自适应文字与 --wiki-border-color 就全都缺失。
|
|
200
|
+
// 改为按 URL 单独取色,保证颜色不依赖懒加载是否跑通。
|
|
201
|
+
if (displayed !== url) return finish(waitForSample(sampleWithCors(image, url, size, key), options.signal));
|
|
199
202
|
if (!image.naturalWidth || !image.naturalHeight) return finish(null);
|
|
200
203
|
const existing = read(key);
|
|
201
204
|
const raw = existing || sample(image, size);
|