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.
- package/CHANGELOG.md +66 -0
- package/README.md +8 -0
- package/README_EN.md +8 -0
- package/_config.yml +12 -1
- package/languages/en.yml +0 -2
- package/languages/zh-CN.yml +0 -2
- package/languages/zh-TW.yml +0 -2
- package/layout/_partial/cover/wiki_cover.ejs +9 -3
- package/layout/_partial/main/navbar/article_banner.ejs +9 -0
- package/layout/_partial/main/notebook/notebook_card.ejs +33 -13
- package/layout/_partial/primitives/navigation.ejs +3 -2
- package/layout/_partial/regions/widgets.ejs +1 -1
- package/layout/_partial/sidebar/menu.ejs +3 -0
- package/layout/notebooks.ejs +8 -6
- package/layout/settings.ejs +2 -4
- package/legal/THIRD-PARTY-NOTICES.md +2 -2
- package/package.json +1 -1
- package/scripts/events/lib/notebooks.js +5 -1
- package/scripts/lib/client-assets.js +1 -0
- package/scripts/lib/config-schema.js +2 -2
- package/scripts/lib/contribution-registry.js +14 -0
- package/scripts/lib/hero-effect-registry.js +50 -1
- package/scripts/lib/image-metadata.js +19 -4
- package/scripts/lib/internal-constants.js +1 -0
- package/scripts/lib/models/wiki.js +2 -0
- package/scripts/lib/regions.js +8 -1
- package/scripts/lib/source-config.js +6 -1
- package/scripts/schema/config-rules.js +3 -0
- package/scripts/schema/content-config-rules.js +4 -0
- package/scripts/schema/content-config-schema.js +2 -0
- package/scripts/schema/model-schema.js +4 -0
- package/source/css/_common/base.styl +1 -0
- package/source/css/_common/input.styl +12 -15
- package/source/css/_common/loading.styl +17 -0
- package/source/css/_components/pages/settings.styl +0 -1
- package/source/css/_components/partial/cover.styl +20 -0
- package/source/css/_components/sidebar/nav-area.styl +10 -0
- package/source/css/_components/sidebar/search.styl +65 -1
- package/source/css/_components/widgets/toc.styl +1 -1
- package/source/css/_components/wiki-card.styl +3 -1
- package/source/js/main.js +51 -4
- package/source/js/profile.js +1 -1
- package/source/js/runtime/extensions/hero-parallax.js +70 -0
- package/source/js/runtime/extensions/partial-navigation.js +18 -1
- package/source/js/runtime/extensions/settings.js +2 -3
- package/source/js/runtime/hero-effects/strands.js +302 -0
- package/source/js/runtime/image-color.js +4 -1
package/scripts/lib/regions.js
CHANGED
|
@@ -61,7 +61,10 @@ function resolveRegion(layers, region, options = {}) {
|
|
|
61
61
|
result.brand = false;
|
|
62
62
|
result.menu = [];
|
|
63
63
|
}
|
|
64
|
-
if (region === "leftbar")
|
|
64
|
+
if (region === "leftbar") {
|
|
65
|
+
result.menuColumns = 1;
|
|
66
|
+
result.footer = { actions: [] };
|
|
67
|
+
}
|
|
65
68
|
for (const layer of layers) {
|
|
66
69
|
const current = regionLayer(layer?.[region]);
|
|
67
70
|
if (!current) continue;
|
|
@@ -72,6 +75,9 @@ function resolveRegion(layers, region, options = {}) {
|
|
|
72
75
|
if (["topbar", "leftbar"].includes(region) && Array.isArray(current.menu)) {
|
|
73
76
|
result.menu = current.menu.map(clone);
|
|
74
77
|
}
|
|
78
|
+
if (region === "leftbar" && current.menuColumns != null) {
|
|
79
|
+
result.menuColumns = current.menuColumns;
|
|
80
|
+
}
|
|
75
81
|
if (region === "leftbar" && Array.isArray(current.footer?.actions)) {
|
|
76
82
|
result.footer.actions = current.footer.actions.map(clone);
|
|
77
83
|
}
|
|
@@ -112,6 +118,7 @@ function resolveRegions(options = {}) {
|
|
|
112
118
|
menu: clone(state.menu)
|
|
113
119
|
} : {}),
|
|
114
120
|
...(region === "leftbar" ? {
|
|
121
|
+
menuColumns: state.menuColumns,
|
|
115
122
|
defaultState: options.defaultState || "expanded",
|
|
116
123
|
footer: clone(state.footer)
|
|
117
124
|
} : {})
|
|
@@ -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;
|
|
@@ -156,6 +156,7 @@ const CONFIG_RULES = Object.freeze([
|
|
|
156
156
|
["leftbar.default_state", { values: ["expanded", "collapsed"] }],
|
|
157
157
|
["leftbar.enabled", { type: ["boolean"] }],
|
|
158
158
|
["leftbar.brand", regionBrand(leftbarBrandProperties)],
|
|
159
|
+
["leftbar.menu_columns", { minimum: 1, maximum: 5, validator: "non_negative_integer" }],
|
|
159
160
|
["leftbar.menu", { validator: "menu_items", items: menuItem }],
|
|
160
161
|
["leftbar.footer.actions", { validator: "footer_actions", items: footerAction }],
|
|
161
162
|
["leftbar.widgets", { items: widgetItems }],
|
|
@@ -169,6 +170,7 @@ const CONFIG_RULES = Object.freeze([
|
|
|
169
170
|
["profiles.*.topbar.widgets", { type: ["array", "null"], defaultValue: null, items: widgetItems }],
|
|
170
171
|
["profiles.*.leftbar.enabled", { type: ["boolean", "null"], defaultValue: null }],
|
|
171
172
|
["profiles.*.leftbar.brand", inheritedRegionBrand(leftbarBrandProperties)],
|
|
173
|
+
["profiles.*.leftbar.menu_columns", { type: ["number", "null"], defaultValue: null, minimum: 1, maximum: 5, validator: "nullable_non_negative_integer" }],
|
|
172
174
|
["profiles.*.leftbar.menu", { type: ["array", "null"], defaultValue: null, validator: "menu_items", items: menuItem }],
|
|
173
175
|
["profiles.*.leftbar.footer.actions", { type: ["array", "null"], defaultValue: null, validator: "footer_actions", items: footerAction }],
|
|
174
176
|
["profiles.*.leftbar.widgets", { type: ["array", "null"], defaultValue: null, items: widgetItems }],
|
|
@@ -274,6 +276,7 @@ const CONFIG_RULES = Object.freeze([
|
|
|
274
276
|
["services.contributors.provider", { values: ["github"] }],
|
|
275
277
|
["services.contributors.github.repositories", { validator: "contributor_repositories", items: contributorRepository }],
|
|
276
278
|
["services.github.*", { validator: "absolute_http_url" }],
|
|
279
|
+
["services.gravatar.base_url", { validator: "absolute_http_url" }],
|
|
277
280
|
["services.github_card.provider", { values: ["github_readme_stats"] }],
|
|
278
281
|
["services.github_card.github_readme_stats", { runtimeKey: "github_readme_stats" }],
|
|
279
282
|
["services.github_card.github_readme_stats.endpoint", { type: ["string"], validator: "absolute_http_url" }],
|
|
@@ -127,6 +127,7 @@ const CONTENT_OVERRIDE_DEFINITIONS = [
|
|
|
127
127
|
["leftbar.brand.ghuser", ["string", "null"], literal(null)],
|
|
128
128
|
["leftbar.brand.search", ["boolean", "null"], literal(null)],
|
|
129
129
|
["leftbar.brand.href", ["string", "null"], literal(null)],
|
|
130
|
+
["leftbar.menu_columns", ["number", "null"], literal(null), { minimum: 1, maximum: 5 }],
|
|
130
131
|
["leftbar.menu", ["array", "null"], literal(null), { items: MENU_ITEMS }],
|
|
131
132
|
["leftbar.footer", "object", literal({}), { boundary: "sealed" }],
|
|
132
133
|
["leftbar.footer.actions", ["array", "null"], literal(null), { items: ACTION_ITEMS }],
|
|
@@ -172,6 +173,8 @@ const COLLECTION_TARGET_DEFINITIONS = [
|
|
|
172
173
|
["hero.enabled", ["boolean", "null"], literal(null)],
|
|
173
174
|
["hero.background", "object", literal({}), { boundary: "sealed" }],
|
|
174
175
|
["hero.background.image", ["string", "null"], literal(null)],
|
|
176
|
+
["hero.background.video", ["string", "null"], literal(null)],
|
|
177
|
+
["hero.background.parallax", "number", literal(0.2), { minimum: 0, maximum: 1 }],
|
|
175
178
|
["hero.background.effect", ["object", "null"], literal(null), { boundary: "registered_schema" }],
|
|
176
179
|
["hero.preview", ["object", "null"], literal(null), { boundary: "sealed" }],
|
|
177
180
|
["hero.preview.type", ["string", "null"], literal(null), { values: ["terminal", "image"] }],
|
|
@@ -209,6 +212,7 @@ const FRONT_MATTER_TARGET_DEFINITIONS = [
|
|
|
209
212
|
["tagline", ["string", "null"], literal(null)],
|
|
210
213
|
["banner", "object", literal({}), { boundary: "sealed" }],
|
|
211
214
|
["banner.enabled", ["boolean", "null"], literal(null)],
|
|
215
|
+
["banner.background", ["boolean", "null"], literal(null)],
|
|
212
216
|
["banner.avatar", ["string", "null"], literal(null)],
|
|
213
217
|
["banner.headline", ["string", "null"], literal(null)],
|
|
214
218
|
["banner.tagline", ["string", "null"], literal(null)],
|
|
@@ -166,6 +166,7 @@ function targetNode(target) {
|
|
|
166
166
|
...(target.boundary === "sealed" ? { sealed: true } : {}),
|
|
167
167
|
...(target.values ? { values: clone(target.values) } : {}),
|
|
168
168
|
...(target.minimum !== undefined ? { minimum: target.minimum } : {}),
|
|
169
|
+
...(target.maximum !== undefined ? { maximum: target.maximum } : {}),
|
|
169
170
|
...(target.exclusiveMinimum !== undefined ? { exclusiveMinimum: target.exclusiveMinimum } : {})
|
|
170
171
|
};
|
|
171
172
|
}
|
|
@@ -271,6 +272,7 @@ function decorateSharedSchemas(schema) {
|
|
|
271
272
|
schema.properties[region].properties.brand.properties.href.validator = "nullable_safe_navigation_url";
|
|
272
273
|
schema.properties[region].properties.menu.validator = "menu_items";
|
|
273
274
|
}
|
|
275
|
+
schema.properties.leftbar.properties.menu_columns.validator = "nullable_non_negative_integer";
|
|
274
276
|
schema.properties.leftbar.properties.footer.properties.actions.validator = "footer_actions";
|
|
275
277
|
|
|
276
278
|
const effect = schema.properties.hero?.properties.background?.properties.effect;
|
|
@@ -206,6 +206,7 @@ function regionConfigSchemas(factory) {
|
|
|
206
206
|
schema.type = ["object", "boolean", "null"];
|
|
207
207
|
return schema;
|
|
208
208
|
})(),
|
|
209
|
+
menuColumns: field("number", { example: 1 }),
|
|
209
210
|
menu: array(menuItem, { example: [] }),
|
|
210
211
|
footer: object({
|
|
211
212
|
actions: array(actionItem, { example: [] })
|
|
@@ -232,6 +233,7 @@ function bannerSchema(factory) {
|
|
|
232
233
|
const { field, object } = factory;
|
|
233
234
|
return object({
|
|
234
235
|
enabled: field("boolean", { example: true }),
|
|
236
|
+
background: field("boolean", { example: true }),
|
|
235
237
|
avatar: field("string", { example: "/avatar.webp" }),
|
|
236
238
|
headline: field("string", { example: "开始使用 Stellar" }),
|
|
237
239
|
tagline: field("string", { example: "十分钟搭好站点" })
|
|
@@ -253,6 +255,8 @@ function heroSchema(factory, options = {}) {
|
|
|
253
255
|
enabled: field("boolean", { example: true }),
|
|
254
256
|
background: object({
|
|
255
257
|
image: field("string", { example: "/hero.webp", consumers: options.pathConsumers }),
|
|
258
|
+
video: field("string", { example: "/hero.mp4", consumers: options.pathConsumers }),
|
|
259
|
+
parallax: field("number", { example: 0.2 }),
|
|
256
260
|
effect: object({
|
|
257
261
|
type: field("string", { example: "galaxy" }),
|
|
258
262
|
options: field("object", {
|
|
@@ -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
|
|
@@ -41,3 +41,20 @@ svg.loading
|
|
|
41
41
|
.article-footer
|
|
42
42
|
.loading-wrap
|
|
43
43
|
background: none
|
|
44
|
+
|
|
45
|
+
.loading-wrap.navigation-loading
|
|
46
|
+
position: fixed
|
|
47
|
+
top: calc(env(safe-area-inset-top, 0px) + 1rem)
|
|
48
|
+
left: 50%
|
|
49
|
+
transform: translateX(-50%)
|
|
50
|
+
z-index: 1000
|
|
51
|
+
display: flex
|
|
52
|
+
align-items: center
|
|
53
|
+
gap: .5rem
|
|
54
|
+
padding: .5rem 1rem
|
|
55
|
+
background: var(--card)
|
|
56
|
+
box-shadow: 0 2px 12px rgba(0, 0, 0, .12)
|
|
57
|
+
pointer-events: none
|
|
58
|
+
.lazy-icon
|
|
59
|
+
flex-shrink: 0
|
|
60
|
+
margin: 0
|
|
@@ -78,6 +78,9 @@
|
|
|
78
78
|
inset: 0
|
|
79
79
|
overflow: hidden
|
|
80
80
|
background: var(--background)
|
|
81
|
+
&.has-parallax
|
|
82
|
+
top: var(--hero-parallax-inset, 0%)
|
|
83
|
+
bottom: var(--hero-parallax-inset, 0%)
|
|
81
84
|
&.image
|
|
82
85
|
.wiki-cover-background-image,
|
|
83
86
|
.wiki-cover-background-blur
|
|
@@ -101,6 +104,23 @@
|
|
|
101
104
|
inset: 0
|
|
102
105
|
pointer-events: none
|
|
103
106
|
background: linear-gradient(to bottom, transparent 0%, transparent 80%, var(--background) 100%)
|
|
107
|
+
&.has-video
|
|
108
|
+
background: #000000
|
|
109
|
+
.wiki-cover-background-video
|
|
110
|
+
display: block
|
|
111
|
+
position: absolute
|
|
112
|
+
z-index: 2
|
|
113
|
+
inset: 0
|
|
114
|
+
width: 100%
|
|
115
|
+
height: 100%
|
|
116
|
+
object-fit: cover
|
|
117
|
+
pointer-events: none
|
|
118
|
+
.wiki-cover-background-overlay
|
|
119
|
+
position: absolute
|
|
120
|
+
z-index: 3
|
|
121
|
+
inset: 0
|
|
122
|
+
pointer-events: none
|
|
123
|
+
background: rgba(black, .15)
|
|
104
124
|
&.has-effect
|
|
105
125
|
&:not(.image)
|
|
106
126
|
background: var(--hero-effect-background, #000000)
|
|
@@ -9,3 +9,13 @@
|
|
|
9
9
|
.menu
|
|
10
10
|
.ui-collection__item
|
|
11
11
|
font-size: $fs-15
|
|
12
|
+
|
|
13
|
+
.site-region--leftbar .widget-instance--menu .ui-collection[data-layout='grid']
|
|
14
|
+
--ui-grid-min: 0px
|
|
15
|
+
&[data-variant='icon']
|
|
16
|
+
.ui-collection__item
|
|
17
|
+
padding-inline: 0
|
|
18
|
+
.ui-collection__content
|
|
19
|
+
display: none
|
|
20
|
+
.ui-collection__initial
|
|
21
|
+
display: block
|
|
@@ -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
|
}
|
package/source/js/profile.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
var SUPPORTED_PROVIDERS = ['artalk', 'waline', 'twikoo'];
|
|
3
3
|
var SETTINGS_ICON = 'default:settings';
|
|
4
4
|
var PROFILE_ICON = 'default:profile';
|
|
5
|
-
var GRAVATAR_BASE_URL =
|
|
5
|
+
var GRAVATAR_BASE_URL = window.stellarClientData.ctx.services.gravatar.baseUrl.replace(/\/+$/, '') + '/';
|
|
6
6
|
var localStore = null;
|
|
7
7
|
var sessionStore = null;
|
|
8
8
|
var gravatarHashes = new Map();
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const SELECTOR = '.wiki-hero';
|
|
2
|
+
|
|
3
|
+
export function mount(root, context = {}) {
|
|
4
|
+
const heroes = [...root.querySelectorAll(SELECTOR)];
|
|
5
|
+
if (root.matches?.(SELECTOR)) heroes.unshift(root);
|
|
6
|
+
const cleanups = heroes.map(hero => {
|
|
7
|
+
const background = hero.querySelector('.wiki-cover-background');
|
|
8
|
+
if (!background?.matches('.image, .has-video, .has-effect')) return () => {};
|
|
9
|
+
const configuredStrength = Number(background.getAttribute('data-parallax'));
|
|
10
|
+
const strength = Number.isFinite(configuredStrength) ? Math.max(0, Math.min(1, configuredStrength)) : 0;
|
|
11
|
+
const fades = background.matches('.has-video, .image:not(.has-effect)');
|
|
12
|
+
const documentRef = hero.ownerDocument;
|
|
13
|
+
const windowRef = documentRef.defaultView;
|
|
14
|
+
const motion = windowRef.matchMedia('(prefers-reduced-motion: reduce)');
|
|
15
|
+
let frame = null;
|
|
16
|
+
let visible = true;
|
|
17
|
+
|
|
18
|
+
function update() {
|
|
19
|
+
frame = null;
|
|
20
|
+
const rect = hero.getBoundingClientRect();
|
|
21
|
+
if (fades) {
|
|
22
|
+
const opacity = rect.height > 0 ? Math.max(0, Math.min(1, (rect.top + rect.height) / rect.height)) : 1;
|
|
23
|
+
background.style.opacity = String(opacity);
|
|
24
|
+
}
|
|
25
|
+
if (motion.matches || strength === 0) {
|
|
26
|
+
background.style.removeProperty('transform');
|
|
27
|
+
background.style.removeProperty('--hero-parallax-inset');
|
|
28
|
+
background.classList.remove('has-parallax');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
background.style.setProperty('--hero-parallax-inset', `${-strength * 100}%`);
|
|
32
|
+
background.classList.add('has-parallax');
|
|
33
|
+
const offset = Math.max(-rect.height * strength, Math.min(rect.height * strength, -rect.top * strength));
|
|
34
|
+
background.style.transform = `translate3d(0, ${offset}px, 0)`;
|
|
35
|
+
}
|
|
36
|
+
function schedule() {
|
|
37
|
+
if (frame === null && visible && !documentRef.hidden) frame = windowRef.requestAnimationFrame(update);
|
|
38
|
+
}
|
|
39
|
+
const observer = new windowRef.IntersectionObserver(entries => {
|
|
40
|
+
visible = entries[0].isIntersecting;
|
|
41
|
+
// Commit the endpoint even when a large scroll moves the Hero offscreen.
|
|
42
|
+
if (frame !== null) windowRef.cancelAnimationFrame(frame);
|
|
43
|
+
update();
|
|
44
|
+
});
|
|
45
|
+
observer.observe(hero);
|
|
46
|
+
const resize = new windowRef.ResizeObserver(schedule);
|
|
47
|
+
resize.observe(hero);
|
|
48
|
+
windowRef.addEventListener('scroll', schedule, { passive: true });
|
|
49
|
+
windowRef.addEventListener('resize', schedule);
|
|
50
|
+
documentRef.addEventListener('visibilitychange', schedule);
|
|
51
|
+
motion.addEventListener('change', schedule);
|
|
52
|
+
update();
|
|
53
|
+
return () => {
|
|
54
|
+
if (frame !== null) windowRef.cancelAnimationFrame(frame);
|
|
55
|
+
observer.disconnect();
|
|
56
|
+
resize.disconnect();
|
|
57
|
+
windowRef.removeEventListener('scroll', schedule);
|
|
58
|
+
windowRef.removeEventListener('resize', schedule);
|
|
59
|
+
documentRef.removeEventListener('visibilitychange', schedule);
|
|
60
|
+
motion.removeEventListener('change', schedule);
|
|
61
|
+
background.style.removeProperty('transform');
|
|
62
|
+
background.style.removeProperty('--hero-parallax-inset');
|
|
63
|
+
if (fades) background.style.removeProperty('opacity');
|
|
64
|
+
background.classList.remove('has-parallax');
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
const cleanup = () => cleanups.forEach(dispose => dispose());
|
|
68
|
+
context.onCleanup?.(cleanup);
|
|
69
|
+
return cleanup;
|
|
70
|
+
}
|
|
@@ -79,6 +79,20 @@ export function mount(root, context) {
|
|
|
79
79
|
}
|
|
80
80
|
async function navigate(url, { pop = false, position } = {}) {
|
|
81
81
|
const request = queue.begin();
|
|
82
|
+
const loading = document.createElement('div');
|
|
83
|
+
loading.className = 'loading-wrap navigation-loading';
|
|
84
|
+
loading.setAttribute('role', 'status');
|
|
85
|
+
const icon = document.createElement('div');
|
|
86
|
+
icon.className = 'lazy-icon';
|
|
87
|
+
icon.setAttribute('aria-hidden', 'true');
|
|
88
|
+
loading.append(icon);
|
|
89
|
+
// Fast navigations should finish without flashing a loading indicator.
|
|
90
|
+
const loadingTimer = setTimeout(() => document.body.append(loading), 150);
|
|
91
|
+
const clearLoading = () => {
|
|
92
|
+
clearTimeout(loadingTimer);
|
|
93
|
+
loading.remove();
|
|
94
|
+
};
|
|
95
|
+
request.signal.addEventListener('abort', clearLoading, { once: true });
|
|
82
96
|
let timedOut = false;
|
|
83
97
|
const timeout = setTimeout(() => { timedOut = true; request.abort(); }, 15000);
|
|
84
98
|
if (!pop) remember();
|
|
@@ -134,7 +148,10 @@ export function mount(root, context) {
|
|
|
134
148
|
});
|
|
135
149
|
} catch (error) {
|
|
136
150
|
if ((!request.signal.aborted || timedOut) && !lifetime.signal.aborted) fallback(url, pop);
|
|
137
|
-
} finally {
|
|
151
|
+
} finally {
|
|
152
|
+
clearTimeout(timeout);
|
|
153
|
+
clearLoading();
|
|
154
|
+
}
|
|
138
155
|
}
|
|
139
156
|
|
|
140
157
|
document.addEventListener('click', event => {
|
|
@@ -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) {
|