@windcoder/astro-theme-orchid 0.2.1 → 0.4.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 (41) hide show
  1. package/README.md +5 -3
  2. package/package.json +4 -2
  3. package/scripts/check-friends.mjs +227 -0
  4. package/scripts/generate-lqips.mjs +234 -20
  5. package/scripts/migrate-posts.mjs +375 -0
  6. package/src/components/AlbumCard.astro +4 -1
  7. package/src/components/Analytics.astro +3 -0
  8. package/src/components/BaseLayout.astro +222 -39
  9. package/src/components/DisplaySettings.astro +383 -0
  10. package/src/components/Header.astro +56 -2
  11. package/src/components/MusicPlayer.astro +45 -5
  12. package/src/components/MusicWidget.astro +265 -0
  13. package/src/components/PhotoCard.astro +13 -1
  14. package/src/components/PostCard.astro +4 -1
  15. package/src/components/SakuraEffect.astro +126 -28
  16. package/src/components/Sidebar.astro +28 -7
  17. package/src/components/analytics/WebVitals.astro +72 -0
  18. package/src/i18n/en.ts +42 -0
  19. package/src/i18n/ja.ts +213 -0
  20. package/src/i18n/ko.ts +212 -0
  21. package/src/i18n/resolve.ts +10 -1
  22. package/src/i18n/ru.ts +213 -0
  23. package/src/i18n/zh-CN.ts +43 -0
  24. package/src/i18n/zh-TW.ts +208 -0
  25. package/src/index.ts +23 -0
  26. package/src/plugins/rehype.ts +76 -4
  27. package/src/routes/bangumi.astro +110 -0
  28. package/src/routes/friends.astro +10 -2
  29. package/src/routes/manifest.webmanifest.ts +33 -0
  30. package/src/routes/page/[page].astro +3 -0
  31. package/src/routes/search.astro +24 -0
  32. package/src/routes/sponsor.astro +17 -4
  33. package/src/routes/sw.js.ts +127 -0
  34. package/src/schema.ts +446 -45
  35. package/src/scripts/app.ts +805 -37
  36. package/src/styles/global.css +939 -18
  37. package/src/utils/gallery.ts +3 -1
  38. package/src/utils/image.ts +79 -0
  39. package/src/utils/lqip.ts +21 -12
  40. package/src/utils/thumb.ts +26 -0
  41. package/src/vite.ts +95 -8
@@ -4,6 +4,7 @@
4
4
  import { Font } from "astro:assets";
5
5
  import { ClientRouter } from "astro:transitions";
6
6
  import Analytics from "virtual:theme/components/Analytics.astro";
7
+ import DisplaySettings from "virtual:theme/components/DisplaySettings.astro";
7
8
  import Footer from "virtual:theme/components/Footer.astro";
8
9
  import Header from "virtual:theme/components/Header.astro";
9
10
  import Live2DWidget from "virtual:theme/components/Live2DWidget.astro";
@@ -11,6 +12,7 @@ import MusicPlayer from "virtual:theme/components/MusicPlayer.astro";
11
12
  import SakuraEffect from "virtual:theme/components/SakuraEffect.astro";
12
13
  import config from "virtual:theme/config";
13
14
  import { t } from "../i18n/index.js";
15
+ import { resolveSiteImages } from "../utils/image.js";
14
16
  import { hexToHsl } from "../utils.js";
15
17
  import "@windcoder/astro-theme-orchid/styles/global.css";
16
18
  /* 灯箱(§4.69):PhotoSwipe 样式随页面 CSS 一起打包,JS 由 app.ts 惰性动态加载 */
@@ -25,6 +27,8 @@ interface Props {
25
27
  page?: string;
26
28
  /** OG 分享图路径(b9.4,文章页传 /og/<slug>.png);绝对链接依赖站点 site 配置 */
27
29
  ogImage?: string;
30
+ /** 整页排除 Pagefind 索引(§4.104 搜索页自身不进索引,防自指噪声) */
31
+ pagefindIgnore?: boolean;
28
32
  }
29
33
 
30
34
  const {
@@ -33,23 +37,90 @@ const {
33
37
  keywords = config.keywords,
34
38
  page,
35
39
  ogImage,
40
+ pagefindIgnore,
36
41
  } = Astro.props;
37
42
  const pageTitle = title ? `${title} · ${config.title}` : config.title;
38
43
  const { h, s, l } = hexToHsl(config.accentColor);
39
- /* 首页 banner(§4.42,hero+壁纸合并单配置):数组时构建期随机选一张(每页构建各自取,零 JS);
40
- enabled=false 或 src 未配置不渲染图 */
41
- const bannerSrc = Array.isArray(config.banner.src)
42
- ? config.banner.src.length
43
- ? config.banner.src[Math.floor(Math.random() * config.banner.src.length)]
44
- : ""
45
- : config.banner.src || "";
46
- const showBanner =
47
- config.banner.enabled &&
48
- Boolean(bannerSrc) &&
49
- (!config.banner.homeOnly || page === "home");
50
- /* hero 文案(§4.42):quote 留空不渲染;有 banner 时叠加其上,否则渲染为列表上方独立区块 */
51
- const heroEnabled = config.banner.enabled && Boolean(config.banner.quote);
52
- const heroOnBanner = heroEnabled && showBanner && page === "home";
44
+ /* 统一壁纸体系(§4.98):banner+background 合并为 wallpaper 四模式,单一 #wp-stage
45
+ 容器 + html[data-wallpaper-mode] 属性驱动四种形态(banner/fullscreen/overlay/none)。
46
+ 图源走三格式解析(§4.97):相对站点 src/ 的路径构建期查表转产物 URL,public/外链原样 */
47
+ const wp = config.wallpaper;
48
+ const wpDesktopAll = await resolveSiteImages(wp.src.desktop);
49
+ const wpMobileAll = await resolveSiteImages(wp.src.mobile);
50
+ /* 轮播关:构建期各端随机一张(每页构建各自取,零 JS);轮播开:全列表渲染交 JS 轮换 */
51
+ const wpCarouselOn = wp.common.carousel.enable;
52
+ const wpPick = (arr: string[]): string =>
53
+ arr[Math.floor(Math.random() * arr.length)];
54
+ const stageDesktop = wpCarouselOn
55
+ ? wpDesktopAll
56
+ : wpDesktopAll.length
57
+ ? [wpPick(wpDesktopAll)]
58
+ : [];
59
+ const stageMobile = wpCarouselOn
60
+ ? wpMobileAll
61
+ : wpMobileAll.length
62
+ ? [wpPick(wpMobileAll)]
63
+ : [];
64
+ /* 实际首页判定:仅根路径 / 为 true(分页 /page/2/ 虽 page=home 但非首页)。
65
+ 非首页 fullscreen 降级(§4.98):classic→banner(横幅高度,跨页可见)、
66
+ hero→overlay(透明背景,无顶部全屏区),保持首页大气全屏、内页轻量的层次 */
67
+ const isActualHome = page === "home" && Astro.url.pathname === "/";
68
+ const fsLayout = wp.fullscreen.layout;
69
+ const visualMode =
70
+ wp.mode === "fullscreen" && !isActualHome
71
+ ? fsLayout === "hero"
72
+ ? "overlay"
73
+ : "banner"
74
+ : wp.mode;
75
+ /* stage 渲染门槛:有图且模式非 none。非首页也渲染 stage(banner 模式展示页面标题
76
+ 横幅,fullscreen 降级为 banner/overlay),homeOnly 仅控制壁纸图是否在非首页
77
+ 显示,不再隐藏整个 stage——保证非首页有页面标题横幅且刷新不丢失样式 */
78
+ const stageMode =
79
+ visualMode === "none" || stageDesktop.length === 0 ? null : visualMode;
80
+ const stageHidden = false;
81
+ const stageVisible = Boolean(stageMode);
82
+ /* hero 文案(§4.98):首页用 homeText 配置(标题+副标题/打字机),非首页用页面
83
+ 传入的 title(归档→"归档"、文章→文章标题等)。仅 stage 可见时渲染,叠加在
84
+ 壁纸之上;overlay/none 模式 stage 为 fixed 垫底或不存在,CSS 隐藏 .wp-hero */
85
+ const homeText = wp.common.homeText;
86
+ const wpSubs = Array.isArray(homeText.subtitle)
87
+ ? homeText.subtitle
88
+ : homeText.subtitle
89
+ ? [homeText.subtitle]
90
+ : [];
91
+ const hasHeroText =
92
+ homeText.enable && (Boolean(homeText.title) || wpSubs.length > 0);
93
+ const typerOn = homeText.typewriter.enable && wpSubs.length > 0;
94
+ const staticSub = typerOn ? "" : wpSubs.length ? wpPick(wpSubs) : "";
95
+ /* hero 渲染条件:首页需 homeText 有内容;非首页需有 title。两种情况都要求
96
+ stage 存在(banner/fullscreen 模式) */
97
+ const heroTitle = isActualHome ? homeText.title : title;
98
+ const heroSub = isActualHome ? staticSub : "";
99
+ const heroTyperOn = isActualHome && typerOn;
100
+ const heroInStage = Boolean(stageMode) && (isActualHome ? hasHeroText : Boolean(title));
101
+ /* 底部装饰分端归一化:布尔简写展开为两端同值 */
102
+ const wpWaves =
103
+ typeof wp.common.waves === "boolean"
104
+ ? { desktop: wp.common.waves, mobile: wp.common.waves }
105
+ : wp.common.waves;
106
+ const wpGradient =
107
+ typeof wp.common.gradient === "boolean"
108
+ ? {
109
+ desktop: wp.common.gradient,
110
+ mobile: wp.common.gradient,
111
+ height: "10%",
112
+ }
113
+ : wp.common.gradient;
114
+ /* 视频源:仅支持 public 路径与外链(原样输出;视频文件不入构建管线) */
115
+ const wpVideos = wp.playerEnable ? wp.src.playerUrl : [];
116
+ /* 导航栏透明形态(§4.98 navbar):壁纸不可见(none/无图/隐藏)时 SSR 直接实底 */
117
+ const wpNavMode = stageVisible ? wp.common.navbar.transparentMode : "solid";
118
+ /* 显示设置面板(§4.103):enable 时渲染齿轮与浮层;面板开启且壁纸模式组可见
119
+ 即视为允许访客切换模式(与 wallpaper.switchable 取并集,data-wp-switchable
120
+ 驱动 boot/app.ts 的模式覆写判定) */
121
+ const dsEnable = config.displaySettings.enable;
122
+ const wpSwitchAllowed =
123
+ wp.switchable || (dsEnable && config.displaySettings.wallpaperModeSwitchable);
53
124
  /* 字体角色映射(§4.66):配置了 role 的字体生成 html:root 规则,把主题三角色
54
125
  变量重定向为 var(字体变量), 原角色回退栈(取自 global.css :root 默认值,
55
126
  字体未覆盖的字形如中文自然回落)。html:root Specificity (0,1,1) 高于
@@ -70,7 +141,21 @@ const fontRoleCss = config.fonts
70
141
  ---
71
142
 
72
143
  <!doctype html>
73
- <html lang={config.lang} style={`--accent-h:${h};--accent-s:${s}%;--accent-l:${l}%;--page-width:${config.pageWidth}px`} data-accent-h={h} data-color-mode={config.colorMode}>
144
+ <html
145
+ lang={config.lang}
146
+ style={`--accent-h:${h};--accent-s:${s}%;--accent-l:${l}%;--page-width:${config.pageWidth}px${config.darkMode.adaptExternalImages ? ";--img-adapt-b:.85" : ""};--wp-dim:${wp.common.dimOpacity};--wp-dim-dark:${wp.common.darkDimOpacity};--wp-card-opacity:${Math.round(wp.overlay.cardOpacity * 100)}%;--wp-nav-blur:${wp.common.navbar.enableBlur ? wp.common.navbar.blur : 0}px;--banner-h:${wp.banner.height}vh;--banner-pos:${wp.banner.position};--fs-pos:${wp.fullscreen.position};--wp-grad-h:${wpGradient.height};--wp-overlay-opacity:${wp.overlay.opacity};--wp-overlay-blur:${wp.overlay.blur}px;--wp-overlay-scale:${(1 + wp.overlay.blur / 100).toFixed(3)}`}
147
+ data-accent-h={h}
148
+ data-color-mode={config.colorMode}
149
+ data-wallpaper-mode={wp.mode ?? "none"}
150
+ data-wallpaper-nav={wpNavMode}
151
+ data-wp-nav-config={wp.common.navbar.transparentMode}
152
+ data-wp-switchable={wpSwitchAllowed ? "true" : "false"}
153
+ data-wp-fs-layout={wp.fullscreen.layout}
154
+ data-wp-is-home={isActualHome ? "true" : undefined}
155
+ data-wp-stage-hidden={stageHidden ? "true" : undefined}
156
+ data-wp-hero={heroInStage ? "visible" : undefined}
157
+ data-ds-enable={dsEnable ? "true" : "false"}
158
+ >
74
159
  <head>
75
160
  <meta charset="utf-8" />
76
161
  <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -97,6 +182,8 @@ const fontRoleCss = config.fonts
97
182
  <link rel="alternate" type="application/rss+xml" title={config.title} href={Astro.site ? new URL("rss.xml", Astro.site).href : "/rss.xml"} />
98
183
  <meta name="theme-color" media="(prefers-color-scheme: light)" content="#f6f4ef" />
99
184
  <meta name="theme-color" media="(prefers-color-scheme: dark)" content="#131516" />
185
+ {/* PWA(§4.95):manifest 清单链接,端点由 pwa.enabled 门控注入 */}
186
+ {config.pwa.enabled && <link rel="manifest" href="/manifest.webmanifest" />}
100
187
  {/* OG/Twitter meta(b9.4):ogImage 仅文章页传入;绝对链接依赖 site 配置 */}
101
188
  <meta property="og:type" content={ogImage ? 'article' : 'website'} />
102
189
  <meta property="og:title" content={pageTitle} />
@@ -122,42 +209,136 @@ const fontRoleCss = config.fonts
122
209
  <script is:inline>
123
210
  (function(){var m=localStorage.getItem('qy-mode');var d=document.documentElement.dataset.colorMode||'system';if(m==='dark'||(!m&&d==='dark')||(!m&&d==='system'&&matchMedia('(prefers-color-scheme: dark)').matches))document.documentElement.classList.add('dark');var h=localStorage.getItem('qy-hue');if(h)document.documentElement.style.setProperty('--accent-h',h);})();
124
211
  </script>
212
+ <!-- 壁纸模式 boot(§4.98):switchable 开启时首帧前应用访客偏好(localStorage 优先于
213
+ SSR 配置),none 等形态即时生效防闪烁;VT swap 后 html 属性随新页重建,由 app.ts
214
+ 的 astro:after-swap 监听重应用。stage hidden 属性清理交给 initWallpaper。
215
+ §4.103 扩展:显示设置面板开启(data-ds-enable)时追加恢复全屏布局/壁纸设置显隐/
216
+ 透明三件套/樱花开关(overlay 三变量在 html style 上,boot 直接覆写即时生效) -->
217
+ <script is:inline>
218
+ (function(){try{var d=document.documentElement;var ls=localStorage;if(d.dataset.wpSwitchable==='true'){var m=ls.getItem('qy-wallpaper');if(m&&['banner','fullscreen','overlay','none'].indexOf(m)>=0&&m!==d.dataset.wallpaperMode)d.dataset.wallpaperMode=m;}if(d.dataset.dsEnable==='true'){var l=ls.getItem('qy-wp-fs-layout');if(l==='classic'||l==='hero')d.dataset.wpFsLayout=l;var P=[['qy-wp-home-text','wpHomeText'],['qy-wp-carousel','wpCarousel'],['qy-wp-waves','wpWaves'],['qy-wp-gradient','wpGradient'],['qy-sakura','wpSakura']];for(var i=0;i<P.length;i++){var v=ls.getItem(P[i][0]);if(v==='0'||v==='1')d.dataset[P[i][1]]=v==='1'?'on':'off';}var op=parseFloat(ls.getItem('qy-wp-opacity')||'');if(!isNaN(op))d.style.setProperty('--wp-overlay-opacity',String(op));var bl=parseFloat(ls.getItem('qy-wp-blur')||'');if(!isNaN(bl)){d.style.setProperty('--wp-overlay-blur',bl+'px');d.style.setProperty('--wp-overlay-scale',(1+bl/100).toFixed(3));}var co=parseFloat(ls.getItem('qy-wp-card-opacity')||'');if(!isNaN(co))d.style.setProperty('--wp-card-opacity',Math.round(co*100)+'%');}}catch(e){}})();
219
+ </script>
220
+ {/* PWA(§4.95):SW 注册。load 后注册不抢占首屏;is:inline + define:vars 在
221
+ ClientRouter VT swap 时不重复执行,全站仅注册一次;注册失败静默(离线兜底
222
+ 属增强能力,不影响正常浏览) */}
223
+ {config.pwa.enabled && (
224
+ <script is:inline define:vars={{ swUrl: "/sw.js" }}>
225
+ if ("serviceWorker" in navigator) {
226
+ window.addEventListener("load", function () {
227
+ navigator.serviceWorker.register(swUrl).catch(function () {});
228
+ });
229
+ }
230
+ </script>
231
+ )}
125
232
  </head>
126
233
  <!-- 客户端动态文案载体(i18n,M3b-b5):app.ts 读取,VT swap 后随新 body 重建 -->
127
- <script type="application/json" id="qy-ui-labels" set:html={JSON.stringify({ close: t('close'), prevImage: t('prevImage'), nextImage: t('nextImage'), copied: t('copied'), fullscreen: t('fullscreen'), slideshow: t('slideshow'), thumbs: t('thumbs'), sharePoster: t('sharePoster'), generatingPoster: t('generatingPoster'), copyLink: t('copyLink'), savePoster: t('savePoster'), scanToRead: t('scanToRead'), author: t('author'), searchLoading: t('searchLoading'), searchNoResults: t('searchNoResults'), searchUnavailable: t('searchUnavailable'), searchTypeSomething: t('searchTypeSomething') })} />
128
- <body data-page={page}>
234
+ <script type="application/json" id="qy-ui-labels" set:html={JSON.stringify({ close: t('close'), prevImage: t('prevImage'), nextImage: t('nextImage'), copied: t('copied'), fullscreen: t('fullscreen'), slideshow: t('slideshow'), thumbs: t('thumbs'), sharePoster: t('sharePoster'), generatingPoster: t('generatingPoster'), copyLink: t('copyLink'), savePoster: t('savePoster'), scanToRead: t('scanToRead'), author: t('author'), searchLoading: t('searchLoading'), searchNoResults: t('searchNoResults'), searchUnavailable: t('searchUnavailable'), searchTypeSomething: t('searchTypeSomething'), searchMore: t('searchMore'), searchResultsTotal: t('searchResultsTotal'), prevPage: t('prevPage'), nextPage: t('nextPage'), pageOf: t('pageOf'), wpPlay: t('wpPlay'), wpPause: t('wpPause'), wpPrev: t('wpPrev'), wpNext: t('wpNext'), wpMode: t('wpMode') })} />
235
+ <body data-page={page} data-pagefind-ignore={pagefindIgnore ? "" : undefined}>
129
236
  <a class="skip-link" href="#main">{t('skipToContent')}</a>
130
237
  <Header />
131
- <!-- 首页 banner(§4.42,hero+壁纸合并单配置):Header 之后下沉横幅 + 黑蒙层;enabled=false / src 未配置 / homeOnly 非首页不渲染 -->
238
+ <!-- 统一壁纸 stage(§4.98):单容器承载图/视频/蒙层/装饰/hero,四种形态由
239
+ html[data-wallpaper-mode] 驱动(CSS 切换 banner/fullscreen/overlay/none);
240
+ 无图或 none 模式不渲染;homeOnly 非首页渲染但标 hidden(访客切换即解除)。
241
+ stage 位于 Header 之后(DOM 序),banner/fullscreen 的 margin-top: -header-h
242
+ 将 stage 上缘拉入 sticky Header 底下,壁纸从视口顶铺满而非独立于导航栏之上 -->
132
243
  {
133
- showBanner && (
244
+ stageMode && (
134
245
  <div
135
- class="wallpaper-banner"
136
- style={`--banner-h:${config.banner.height}vh;--banner-dim:${config.banner.dimOpacity};--banner-pos:${config.banner.position}`}
246
+ class="wp-stage"
247
+ style={`--wp-hero-title-size:${homeText.titleSize};--wp-hero-sub-size:${homeText.subtitleSize}`}
248
+ data-carousel={wpCarouselOn ? "true" : "false"}
249
+ data-carousel-interval={wp.common.carousel.interval}
250
+ data-effect={wp.common.carousel.transitionEffect}
251
+ data-wallpaper-hidden={stageHidden ? "" : undefined}
252
+ aria-hidden={stageHidden ? "true" : undefined}
137
253
  >
138
- <img src={bannerSrc} alt="" aria-hidden="true" loading="eager" />
139
- <div class="wallpaper-dim" aria-hidden="true" />
140
- {/* hero 文案叠加(§4.42):quote 配置时主句/副句居中显示在 banner 上 */}
141
- {heroOnBanner && (
142
- <div class="wallpaper-hero reveal">
143
- <h1 class="hero-quote" set:html={config.banner.quote} />
144
- {config.banner.sub && <p class="hero-sub">{config.banner.sub}</p>}
254
+ <div class="wp-images wp-desktop" aria-hidden="true">
255
+ {stageDesktop.map((src, i) => (
256
+ <img
257
+ class={`wp-img${i === 0 ? " active" : ""}`}
258
+ src={src}
259
+ alt=""
260
+ loading="eager"
261
+ decoding="async"
262
+ />
263
+ ))}
264
+ </div>
265
+ {stageMobile.length > 0 && (
266
+ <div class="wp-images wp-mobile" aria-hidden="true">
267
+ {stageMobile.map((src, i) => (
268
+ <img
269
+ class={`wp-img${i === 0 ? " active" : ""}`}
270
+ src={src}
271
+ alt=""
272
+ loading="eager"
273
+ decoding="async"
274
+ />
275
+ ))}
276
+ </div>
277
+ )}
278
+ {wpVideos.length > 0 && (
279
+ <video
280
+ class="wp-video"
281
+ muted
282
+ loop
283
+ playsinline
284
+ preload="none"
285
+ data-player-urls={JSON.stringify(wpVideos)}
286
+ data-player-mode={wp.common.playerMode}
287
+ />
288
+ )}
289
+ <div class="wp-dim" aria-hidden="true" />
290
+ {wpGradient.desktop && <div class="wp-gradient is-desktop" aria-hidden="true" />}
291
+ {wpGradient.mobile && <div class="wp-gradient is-mobile" aria-hidden="true" />}
292
+ {wpWaves.desktop && (
293
+ <div class="wp-waves is-desktop" aria-hidden="true">
294
+ <svg class="wave w1" viewBox="0 0 2880 120" preserveAspectRatio="none"><path d="M0,64 C240,96 480,32 720,64 C960,96 1200,32 1440,64 C1680,96 1920,32 2160,64 C2400,96 2640,32 2880,64 L2880,120 L0,120 Z" /></svg>
295
+ <svg class="wave w2" viewBox="0 0 2880 120" preserveAspectRatio="none"><path d="M0,80 C240,48 480,112 720,80 C960,48 1200,112 1440,80 C1680,48 1920,112 2160,80 C2400,48 2640,112 2880,80 L2880,120 L0,120 Z" /></svg>
296
+ </div>
297
+ )}
298
+ {wpWaves.mobile && (
299
+ <div class="wp-waves is-mobile" aria-hidden="true">
300
+ <svg class="wave w1" viewBox="0 0 2880 120" preserveAspectRatio="none"><path d="M0,64 C240,96 480,32 720,64 C960,96 1200,32 1440,64 C1680,96 1920,32 2160,64 C2400,96 2640,32 2880,64 L2880,120 L0,120 Z" /></svg>
301
+ <svg class="wave w2" viewBox="0 0 2880 120" preserveAspectRatio="none"><path d="M0,80 C240,48 480,112 720,80 C960,48 1200,112 1440,80 C1680,48 1920,112 2160,80 C2400,48 2640,112 2880,80 L2880,120 L0,120 Z" /></svg>
145
302
  </div>
146
303
  )}
304
+ {heroInStage && (
305
+ <div class="wp-hero reveal">
306
+ {heroTitle && <h1 class="hero-quote" set:html={heroTitle} />}
307
+ {heroTyperOn ? (
308
+ <p class="hero-sub">
309
+ <span
310
+ class="typewriter"
311
+ id="wpTyper"
312
+ data-subtitles={JSON.stringify(wpSubs)}
313
+ data-speed={homeText.typewriter.speed}
314
+ data-delete-speed={homeText.typewriter.deleteSpeed}
315
+ data-pause-time={homeText.typewriter.pauseTime}
316
+ />
317
+ </p>
318
+ ) : (
319
+ heroSub && <p class="hero-sub">{heroSub}</p>
320
+ )}
321
+ </div>
322
+ )}
323
+ {/* 全屏首屏滚动提示(§4.98):仅 fullscreen 模式可见(CSS 门控,SSR 始终渲染,
324
+ 访客切换到 fullscreen 即可用);点击平滑滚过首屏;
325
+ hero 布局滚动后随 .wp-hero 一起淡出(共用 wp-scrolled 规则) */}
326
+ <button
327
+ type="button"
328
+ class="wp-scroll-hint"
329
+ aria-label={t("scrollDown")}
330
+ onclick="document.querySelector('.site-main')?.scrollIntoView({behavior:'smooth'})"
331
+ >
332
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
333
+ <path d="M12 5v14" />
334
+ <path d="m19 12-7 7-7-7" />
335
+ </svg>
336
+ </button>
147
337
  </div>
148
338
  )
149
339
  }
150
340
  <main id="main" class="site-main">
151
341
  <div class="shell">
152
- {/* hero 独立区块形态(§4.42):无 banner 图时的首页 hero 文案,保持原列表上方位置 */}
153
- {
154
- heroEnabled && page === "home" && !heroOnBanner && (
155
- <section class="hero reveal">
156
- <h1 class="hero-quote" set:html={config.banner.quote} />
157
- {config.banner.sub && <p class="hero-sub">{config.banner.sub}</p>}
158
- </section>
159
- )
160
- }
161
342
  <slot />
162
343
  </div>
163
344
  </main>
@@ -169,14 +350,16 @@ const fontRoleCss = config.fonts
169
350
  <Footer />
170
351
  <!-- 站点统计(P1-16):ID 全空零输出,挂 body 尾与 orchid-blog 口径一致 -->
171
352
  <Analytics />
172
- <!-- 樱花飘落特效(§4.30):sakura.enabled 开启才渲染 -->
173
- {config.sakura.enabled && <SakuraEffect density={config.sakura.density} />}
353
+ <!-- 樱花飘落特效(§4.30/§4.101 八维参数):sakura.enabled 开启才渲染 -->
354
+ {config.sakura.enabled && <SakuraEffect cfg={config.sakura} />}
174
355
  <!-- 悬浮音乐播放器(§4.36):music.enabled 且歌单非空才渲染 -->
175
356
  {config.music.enabled && config.music.playlist.length > 0 && (
176
357
  <MusicPlayer music={config.music} />
177
358
  )}
178
359
  <!-- Live2D 看板娘(§4.38):live2d.enabled 且配置了 model 才渲染 -->
179
360
  {config.live2d.enabled && config.live2d.model && <Live2DWidget live2d={config.live2d} />}
361
+ <!-- 显示设置面板(§4.103):齿轮入口的三 Tab 浮层,enable 才渲染 -->
362
+ {dsEnable && <DisplaySettings />}
180
363
  <button class="back-top" id="backTop" type="button" aria-label={t('backToTop')} hidden>
181
364
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m18 15-6-6-6 6"/></svg>
182
365
  </button>