@windcoder/astro-theme-orchid 0.3.0 → 0.4.1

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/src/schema.ts CHANGED
@@ -37,6 +37,207 @@ const sidebarWidgetBase = z.object({
37
37
  specificConfig: z.object({}).default({}),
38
38
  });
39
39
 
40
+ /**
41
+ * 统一壁纸体系(§4.98):原 banner(§4.42)+ background(§4.92)合并为单段配置,
42
+ * 四种模式 html[data-wallpaper-mode] 驱动:banner 横幅 / fullscreen 全屏首屏 /
43
+ * overlay 覆盖透明垫底 / none 纯色不渲染。
44
+ * 图源遵循三格式约定(§4.97):"/" 开头按 public 路径原样输出;http(s)://、//、data:
45
+ * 外链原样输出;其余相对路径相对站点 src/ 目录解析(构建期查表,支持图片/视频,
46
+ * §4.108:不写死 assets/ 前缀)。
47
+ * 提取为模块级 const 以便使用函数默认值 .default(() => wallpaperSchema.parse({})):
48
+ * Zod 的 .default(x) 在输入缺失时直接取 x、不做二次 parse(v3/v4 行为一致),
49
+ * 本段嵌套深达 4 层,最外层再写一份完整字面量维护成本过高;改用 parse({}) 在
50
+ * 运行期递归补齐全部内层默认值(内层各段仍按惯例写显式完整字面量)。
51
+ */
52
+ const wallpaperSchema = z.object({
53
+ /** 壁纸模式:banner 横幅 / fullscreen 全屏首屏 / overlay 覆盖透明垫底 / none 纯色 */
54
+ mode: z.enum(["banner", "fullscreen", "overlay", "none"]).default("banner"),
55
+ /**
56
+ * 是否允许访客在导航栏循环切换壁纸模式(banner→fullscreen→overlay→none),
57
+ * 选择持久化到 localStorage('qy-wallpaper'),优先于本配置的 mode。
58
+ */
59
+ switchable: z.boolean().default(false),
60
+ /** 视频壁纸播放器总开关:false 时不渲染 video 元素与播放控制按钮 */
61
+ playerEnable: z.boolean().default(false),
62
+ /** 壁纸图源与视频源(三格式约定见 §4.97) */
63
+ src: z
64
+ .object({
65
+ /** 桌面端图源:多图数组构建期随机取一或开启轮播;空数组 = 无壁纸图 */
66
+ desktop: z.array(z.string()).default([]),
67
+ /** 移动端图源(<768px):留空数组则移动端复用 desktop 层全宽显示 */
68
+ mobile: z.array(z.string()).default([]),
69
+ /** 视频源列表(playerEnable 开启时生效) */
70
+ playerUrl: z.array(z.string()).default([]),
71
+ })
72
+ .default({ desktop: [], mobile: [], playerUrl: [] }),
73
+ /** 各模式共用的行为配置 */
74
+ common: z
75
+ .object({
76
+ /** 壁纸蒙层不透明度(0-1),压暗壁纸保证前景可读 */
77
+ dimOpacity: z.number().min(0).max(1).default(0.2),
78
+ /** 暗色模式蒙层不透明度(0-1),暗色下压更暗 */
79
+ darkDimOpacity: z.number().min(0).max(1).default(0.65),
80
+ /** 视频播放顺序:order 按列表顺序 / random 随机 */
81
+ playerMode: z.enum(["order", "random"]).default("random"),
82
+ /** 首页壁纸文案:banner/fullscreen 叠加渲染在壁纸上;无壁纸时复用为独立 hero 区块 */
83
+ homeText: z
84
+ .object({
85
+ /** 文案总开关:false 时标题与副句都不渲染 */
86
+ enable: z.boolean().default(true),
87
+ /** 主标题,<em> 标签内文字以强调色渲染;留空不渲染 */
88
+ title: z.string().default(""),
89
+ /** 主标题字号(css 长度) */
90
+ titleSize: z.string().default("3.8rem"),
91
+ /** 副句:单条或多条(打字机逐条打字;关闭打字机时多条构建期随机取一条静态展示) */
92
+ subtitle: z.union([z.string(), z.array(z.string())]).default([]),
93
+ /** 副句字号(css 长度) */
94
+ subtitleSize: z.string().default("1.5rem"),
95
+ /** 打字机效果(作用于副句) */
96
+ typewriter: z
97
+ .object({
98
+ /** 是否启用打字机(false 时副句静态展示) */
99
+ enable: z.boolean().default(true),
100
+ /** 打字速度 ms/字素 */
101
+ speed: z.number().int().min(10).default(100),
102
+ /** 删除速度 ms/字素 */
103
+ deleteSpeed: z.number().int().min(10).default(50),
104
+ /** 打完一条后的停留 ms */
105
+ pauseTime: z.number().int().min(0).default(2000),
106
+ })
107
+ .default({
108
+ enable: true,
109
+ speed: 100,
110
+ deleteSpeed: 50,
111
+ pauseTime: 2000,
112
+ }),
113
+ })
114
+ .default({
115
+ enable: true,
116
+ title: "",
117
+ titleSize: "3.8rem",
118
+ subtitle: [],
119
+ subtitleSize: "1.5rem",
120
+ typewriter: {
121
+ enable: true,
122
+ speed: 100,
123
+ deleteSpeed: 50,
124
+ pauseTime: 2000,
125
+ },
126
+ }),
127
+ /** 导航栏随壁纸的透明形态 */
128
+ navbar: z
129
+ .object({
130
+ /**
131
+ * semi 顶部半透明+滚动后实底 / semifull 顶部全透明+滚动后半透明 /
132
+ * full 恒全透明;壁纸不可见(none 模式或 homeOnly 下非首页)时强制实底
133
+ */
134
+ transparentMode: z.enum(["semi", "semifull", "full"]).default("semi"),
135
+ /** 透明态是否启用背景模糊 */
136
+ enableBlur: z.boolean().default(true),
137
+ /** 透明态背景模糊半径 px */
138
+ blur: z.number().min(0).max(20).default(5),
139
+ })
140
+ .default({ transparentMode: "semi", enableBlur: true, blur: 5 }),
141
+ /** 底部波浪装饰:true 全端开启 / false 关闭 / 对象分端控制(默认移动端关闭参考站惯例) */
142
+ waves: z
143
+ .union([
144
+ z.boolean(),
145
+ z.object({
146
+ desktop: z.boolean().default(true),
147
+ mobile: z.boolean().default(true),
148
+ }),
149
+ ])
150
+ .default(false),
151
+ /** 底部渐变装饰:true 全端开启 / false 关闭 / 对象分端控制 + 高度 */
152
+ gradient: z
153
+ .union([
154
+ z.boolean(),
155
+ z.object({
156
+ desktop: z.boolean().default(true),
157
+ mobile: z.boolean().default(true),
158
+ /** 渐变层高度(css 长度) */
159
+ height: z.string().default("10%"),
160
+ }),
161
+ ])
162
+ .default(false),
163
+ /** 多图轮播(单图无效果);视频播放中自动暂停轮播 */
164
+ carousel: z
165
+ .object({
166
+ /**
167
+ * false:构建期随机取一张(零 JS);true:全列表渲染 + JS 定时轮换。
168
+ * 轮播仅在 banner/fullscreen/overlay 三种图片模式生效。
169
+ */
170
+ enable: z.boolean().default(false),
171
+ /** 轮换间隔 ms */
172
+ interval: z.number().int().min(1000).default(5000),
173
+ /** 过渡效果:fade 淡入淡出 / zoom 缩放 / slide 滑动 / kenburns 缓慢推近 */
174
+ transitionEffect: z
175
+ .enum(["fade", "zoom", "slide", "kenburns"])
176
+ .default("fade"),
177
+ })
178
+ .default({ enable: false, interval: 5000, transitionEffect: "fade" }),
179
+ })
180
+ .default({
181
+ dimOpacity: 0.2,
182
+ darkDimOpacity: 0.65,
183
+ playerMode: "random",
184
+ homeText: {
185
+ enable: true,
186
+ title: "",
187
+ titleSize: "3.8rem",
188
+ subtitle: [],
189
+ subtitleSize: "1.5rem",
190
+ typewriter: {
191
+ enable: true,
192
+ speed: 100,
193
+ deleteSpeed: 50,
194
+ pauseTime: 2000,
195
+ },
196
+ },
197
+ navbar: { transparentMode: "semi", enableBlur: true, blur: 5 },
198
+ waves: false,
199
+ gradient: false,
200
+ carousel: { enable: false, interval: 5000, transitionEffect: "fade" },
201
+ }),
202
+ /** banner 模式专属(横幅形态) */
203
+ banner: z
204
+ .object({
205
+ /** 横幅高度(vh),默认 60:壁纸从导航栏顶铺满至分类导航条处渐隐,非窄条 */
206
+ height: z.number().int().min(10).max(90).default(60),
207
+ /** true 仅首页显示;false 全站显示(overlay 天然全站,忽略此项) */
208
+ homeOnly: z.boolean().default(true),
209
+ /** 图片 object-position(center / center 30% 等) */
210
+ position: z.string().default("center"),
211
+ })
212
+ .default({ height: 60, homeOnly: true, position: "center" }),
213
+ /** fullscreen 模式专属 */
214
+ fullscreen: z
215
+ .object({
216
+ /** 图片 object-position(center / center 30% 等) */
217
+ position: z.string().default("center"),
218
+ /**
219
+ * 全屏布局(§4.103):classic 经典 = 滚下后正文区纯色背景(默认,旧行为);
220
+ * hero = 滚下后正文区无标题文字,壁纸 fixed 铺满 + 模糊渐变过渡,
221
+ * 卡片浮于模糊壁纸上(复用 overlay 的 blur/cardOpacity 参数)
222
+ */
223
+ layout: z.enum(["classic", "hero"]).default("classic"),
224
+ })
225
+ .default({ position: "center", layout: "classic" }),
226
+ /** overlay 模式专属:fixed 垫底,正文卡片半透明透出壁纸 */
227
+ overlay: z
228
+ .object({
229
+ /** 壁纸层 z-index(默认 -1,位于 body 背景之上、内容之下) */
230
+ zIndex: z.number().default(-1),
231
+ /** 壁纸整体不透明度(0-1),配合蒙层控制透出强度 */
232
+ opacity: z.number().min(0).max(1).default(0.8),
233
+ /** 壁纸模糊半径 px(0-20),模糊时自动轻微放大防止边缘露底 */
234
+ blur: z.number().min(0).max(20).default(0),
235
+ /** 正文卡片背景不透明度(0-1):越小壁纸透出越多 */
236
+ cardOpacity: z.number().min(0).max(1).default(0.5),
237
+ })
238
+ .default({ zIndex: -1, opacity: 0.8, blur: 0, cardOpacity: 0.5 }),
239
+ });
240
+
40
241
  /**
41
242
  * 主题配置 schema(astro.config.ts 中 defineTheme({...}) 的入参)
42
243
  * 后续新增配置项一律给 default,保证老站点升级后无需改动配置
@@ -54,7 +255,7 @@ export const themeConfigSchema = z.object({
54
255
  * 文章页由 frontmatter tags 覆盖。留空则不输出该 meta。
55
256
  */
56
257
  keywords: z.array(z.string()).default([]),
57
- /** 导航栏 Logo(§4.20):标题左侧图片 URL(public 路径或外链),留空只渲染文字标题 */
258
+ /** 导航栏 Logo(§4.20):三格式图源(§4.97:public / 外链原样,相对路径相对站点 src/ 解析),留空只渲染文字标题 */
58
259
  logo: z.string().default(""),
59
260
  /** 作者名 */
60
261
  author: z.string().default(""),
@@ -76,61 +277,12 @@ export const themeConfigSchema = z.object({
76
277
  */
77
278
  pageWidth: z.number().int().min(800).max(1920).default(1600),
78
279
  /**
79
- * 首页 banner(§4.42,hero+壁纸合并单配置):enabled 总开关,false 时图与文案都不渲染。
80
- * src 配置时渲染 Header 下方下沉横幅(数组构建期随机选一张,零 JS)+ 黑蒙层;
81
- * quote 配置时渲染 hero 文案——有 banner 叠加其上,无 banner 为列表上方独立区块。
82
- */
83
- banner: z
84
- .object({
85
- /** 总开关:false 时即使配了 src/quote 也不渲染 */
86
- enabled: z.boolean().default(true),
87
- /** 图源(public 路径或外链):单图 URL 或多图数组(构建期随机取一);不配置则无图 */
88
- src: z.union([z.string(), z.array(z.string())]).optional(),
89
- /** banner 高度(vh) */
90
- height: z.number().int().min(10).max(90).default(32),
91
- /** 黑蒙层不透明度(0-1),深浅模式同色,保证文字可读 */
92
- dimOpacity: z.number().min(0).max(1).default(0.2),
93
- /** 图片 object-position(center / center 30% 等) */
94
- position: z.string().default("center"),
95
- /** true 仅首页显示,false 全站显示 */
96
- homeOnly: z.boolean().default(true),
97
- /** 主句,<em> 标签内文字会以强调色渲染;留空不渲染 hero 文案 */
98
- quote: z.string().default(""),
99
- /** 副句 */
100
- sub: z.string().default(""),
101
- })
102
- .default({
103
- enabled: true,
104
- height: 32,
105
- dimOpacity: 0.2,
106
- position: "center",
107
- homeOnly: true,
108
- quote: "",
109
- sub: "",
110
- }),
111
- /**
112
- * 全站背景壁纸(§4.92):body 级 fixed 视口背景,与顶部 banner(§4.42)相互独立。
113
- * 经遮罩压暗后从页缝与卡片间隙透出;正文卡片保持不透明保证可读。
280
+ * 统一壁纸体系(§4.98,breaking:替代原 banner §4.42 + background §4.92 两段)。
281
+ * banner 横幅 / fullscreen 全屏 / overlay 覆盖透明 / none 纯色四模式,
282
+ * 详见模块级 wallpaperSchema 注释。函数默认值在 wallpaper 键整体缺失时
283
+ * 递归补齐全部内层默认值。
114
284
  */
115
- background: z
116
- .object({
117
- /** 总开关:false 时即使配了 src 也不渲染(默认关) */
118
- enabled: z.boolean().default(false),
119
- /** 图源(public 路径或外链):单图 URL 或多图数组(构建期随机取一) */
120
- src: z.union([z.string(), z.array(z.string())]).optional(),
121
- /** 浅色模式遮罩不透明度(0-1),压暗壁纸保证正文可读 */
122
- dimOpacity: z.number().min(0).max(1).default(0.45),
123
- /** 暗色模式遮罩不透明度(0-1),暗色下压更暗 */
124
- darkDimOpacity: z.number().min(0).max(1).default(0.65),
125
- /** 壁纸模糊半径 px(0-20):模糊时自动轻微放大防止边缘露底 */
126
- blur: z.number().min(0).max(20).default(0),
127
- })
128
- .default({
129
- enabled: false,
130
- dimOpacity: 0.45,
131
- darkDimOpacity: 0.65,
132
- blur: 0,
133
- }),
285
+ wallpaper: wallpaperSchema.default(() => wallpaperSchema.parse({})),
134
286
  /**
135
287
  * 番组追番页(§4.93):构建期从 bgm.tv v0 API 拉取用户「在看」动画收藏,
136
288
  * 渲染静态 /bangumi 页。enabled 与 userId 同时满足才注入路由(默认关)。
@@ -255,7 +407,7 @@ export const themeConfigSchema = z.object({
255
407
  */
256
408
  profile: z
257
409
  .object({
258
- /** 头像图 URL(留空渲染昵称首字 fallback) */
410
+ /** 头像图(三格式图源 §4.97;留空渲染昵称首字 fallback) */
259
411
  avatar: z.string().default(""),
260
412
  name: z.string().default(""),
261
413
  bio: z.string().default(""),
@@ -384,6 +536,63 @@ export const themeConfigSchema = z.object({
384
536
  adaptExternalImages: z.boolean().default(false),
385
537
  })
386
538
  .default({ adaptExternalImages: false }),
539
+ /**
540
+ * 显示设置面板(§4.103,对齐 Firefly):导航栏齿轮入口的三 Tab 浮层
541
+ * (外观/壁纸/特效),集中管理主题色、壁纸模式、全屏布局、壁纸设置、
542
+ * 透明设置与樱花特效的访客级开关,选择持久化 localStorage。
543
+ * enable=false(默认)不渲染齿轮与面板,维持 Header 独立调色板/壁纸切换按钮。
544
+ * 各 switchable 控制面板内对应项是否对访客可见;无可见项的 Tab 自动隐藏。
545
+ * 注意 default 必须写显式完整字面量:Zod v4 的 .default(x) 在输入缺失时
546
+ * 直接取 x、不做二次 parse,内层字段默认值不会补齐。
547
+ */
548
+ displaySettings: z
549
+ .object({
550
+ /** 总开关:false(默认)不渲染齿轮入口与面板,维持现有独立按钮 */
551
+ enable: z.boolean().default(false),
552
+ /** 外观 Tab:主题色(迁入现有 hue 调色板) */
553
+ themeColorSwitchable: z.boolean().default(true),
554
+ /** 壁纸 Tab:壁纸模式四选一 */
555
+ wallpaperModeSwitchable: z.boolean().default(true),
556
+ /** 壁纸 Tab:全屏布局(经典/Hero),仅 fullscreen 模式下显示 */
557
+ fullscreenLayoutSwitchable: z.boolean().default(true),
558
+ /** 壁纸 Tab:首页壁纸标题开关(需 homeText.enable 且有文案) */
559
+ bannerTitleSwitchable: z.boolean().default(true),
560
+ /** 壁纸 Tab:壁纸轮播开关(需 carousel.enable,访客只能关不能开) */
561
+ bannerCarouselSwitchable: z.boolean().default(true),
562
+ /** 壁纸 Tab:水波纹动画开关(需 waves 配置开启) */
563
+ wavesSwitchable: z.boolean().default(true),
564
+ /** 壁纸 Tab:渐变过渡开关(需 gradient 配置开启) */
565
+ gradientSwitchable: z.boolean().default(true),
566
+ /**
567
+ * 壁纸 Tab:透明设置滑块组。布尔 = 总开关;对象分项控制。
568
+ * opacity 滑块仅 overlay 模式显示;blur/cardOpacity 在 overlay
569
+ * 与 fullscreen-hero 布局下都生效。
570
+ */
571
+ overlaySwitchable: z
572
+ .union([
573
+ z.boolean(),
574
+ z.object({
575
+ opacity: z.boolean().default(true),
576
+ blur: z.boolean().default(true),
577
+ cardOpacity: z.boolean().default(true),
578
+ }),
579
+ ])
580
+ .default({ opacity: true, blur: true, cardOpacity: true }),
581
+ /** 特效 Tab:樱花特效开关(需 sakura.enabled) */
582
+ sakuraSwitchable: z.boolean().default(true),
583
+ })
584
+ .default({
585
+ enable: false,
586
+ themeColorSwitchable: true,
587
+ wallpaperModeSwitchable: true,
588
+ fullscreenLayoutSwitchable: true,
589
+ bannerTitleSwitchable: true,
590
+ bannerCarouselSwitchable: true,
591
+ wavesSwitchable: true,
592
+ gradientSwitchable: true,
593
+ overlaySwitchable: { opacity: true, blur: true, cardOpacity: true },
594
+ sakuraSwitchable: true,
595
+ }),
387
596
  /**
388
597
  * 代码高亮(§4.61):Expressive Code(Astro 生态标准方案)接管代码块渲染,
389
598
  * 替代 §4.17 自研 shiki 管线与 §4.37 自研折叠。light/dark 为 EC 支持的 Shiki
@@ -511,7 +720,7 @@ export const themeConfigSchema = z.object({
511
720
  z.object({
512
721
  title: z.string(),
513
722
  site: z.string().url(),
514
- /** 头像图(外链或 public 路径;留空用字头像) */
723
+ /** 头像图(三格式图源 §4.97;留空用字头像) */
515
724
  image: z.string().default(""),
516
725
  desc: z.string().default(""),
517
726
  tags: z.array(z.string()).default([]),
@@ -534,7 +743,7 @@ export const themeConfigSchema = z.object({
534
743
  /**
535
744
  * 侧栏 widget 体系(P2 侧栏改造):
536
745
  * - widgets 数组即渲染顺序,可自由增删/排序/开关;类型:
537
- * announcement / categories / tags / friends / calendar / toc / siteInfo / advertisement / profile
746
+ * announcement / categories / tags / friends / calendar / toc / siteInfo / advertisement / profile / music
538
747
  * - 公共配置(开关/显隐/位置)配置在数组项上;组件专属参数统一收进 item 的
539
748
  * specificConfig 入口(§4.52,对齐参考项目):tags.specificConfig.count、
540
749
  * friends.specificConfig.count/random、calendar.specificConfig.postTooltip
@@ -562,7 +771,7 @@ export const themeConfigSchema = z.object({
562
771
  type: z.literal("tags"),
563
772
  specificConfig: z
564
773
  .object({ count: z.number().int().min(0).default(12) })
565
- .default({}),
774
+ .default({ count: 12 }),
566
775
  }),
567
776
  /**
568
777
  * 友链 widget:specificConfig.count 展示数量;random=true 构建期随机取样,
@@ -575,7 +784,7 @@ export const themeConfigSchema = z.object({
575
784
  count: z.number().int().min(1).default(3),
576
785
  random: z.boolean().default(false),
577
786
  })
578
- .default({}),
787
+ .default({ count: 3, random: false }),
579
788
  }),
580
789
  sidebarWidgetBase.extend({
581
790
  type: z.literal("calendar"),
@@ -584,9 +793,14 @@ export const themeConfigSchema = z.object({
584
793
  /** 日历下方文章列表条目悬停显示全文 title(§4.41);默认关 */
585
794
  postTooltip: z.boolean().default(false),
586
795
  })
587
- .default({}),
796
+ .default({ postTooltip: false }),
588
797
  }),
589
798
  sidebarWidgetBase.extend({ type: z.literal("toc") }),
799
+ /**
800
+ * 音乐 widget(§4.100):与 MusicPlayer 共用 config.music 歌单,
801
+ * 播放器未启用或歌单为空时不渲染;播放状态经 CustomEvent 联动
802
+ */
803
+ sidebarWidgetBase.extend({ type: z.literal("music") }),
590
804
  sidebarWidgetBase.extend({ type: z.literal("siteInfo") }),
591
805
  sidebarWidgetBase.extend({ type: z.literal("advertisement") }),
592
806
  sidebarWidgetBase.extend({ type: z.literal("profile") }),
@@ -595,9 +809,20 @@ export const themeConfigSchema = z.object({
595
809
  .default([
596
810
  { type: "announcement" },
597
811
  { type: "categories", position: "sticky" },
598
- { type: "tags", position: "sticky" },
599
- { type: "friends" },
600
- { type: "calendar", hideOnMobile: true, position: "sticky" },
812
+ // 注意:schema default 值不会二次 parse(default 原样返回),
813
+ // 因此数组项的 specificConfig 必须显式写全,运行时才有值
814
+ {
815
+ type: "tags",
816
+ position: "sticky",
817
+ specificConfig: { count: 12 },
818
+ },
819
+ { type: "friends", specificConfig: { count: 3, random: false } },
820
+ {
821
+ type: "calendar",
822
+ hideOnMobile: true,
823
+ position: "sticky",
824
+ specificConfig: { postTooltip: false },
825
+ },
601
826
  {
602
827
  type: "toc",
603
828
  postOnly: true,
@@ -613,9 +838,14 @@ export const themeConfigSchema = z.object({
613
838
  widgets: [
614
839
  { type: "announcement" },
615
840
  { type: "categories", position: "sticky" },
616
- { type: "tags", position: "sticky" },
617
- { type: "friends" },
618
- { type: "calendar", hideOnMobile: true, position: "sticky" },
841
+ { type: "tags", position: "sticky", specificConfig: { count: 12 } },
842
+ { type: "friends", specificConfig: { count: 3, random: false } },
843
+ {
844
+ type: "calendar",
845
+ hideOnMobile: true,
846
+ position: "sticky",
847
+ specificConfig: { postTooltip: false },
848
+ },
619
849
  { type: "toc", postOnly: true, hideOnMobile: true, position: "sticky" },
620
850
  { type: "siteInfo" },
621
851
  { type: "advertisement" },
@@ -637,7 +867,7 @@ export const themeConfigSchema = z.object({
637
867
  name: z.string(),
638
868
  /** 文本/emoji 角标(主题不内置图标依赖) */
639
869
  icon: z.string().default(""),
640
- /** 收款码图片(public 路径或外链) */
870
+ /** 收款码图片(三格式图源 §4.97) */
641
871
  image: z.string().default(""),
642
872
  /** 跳转链接(爱发电/ko-fi 等) */
643
873
  link: z.string().default(""),
@@ -651,6 +881,7 @@ export const themeConfigSchema = z.object({
651
881
  .array(
652
882
  z.object({
653
883
  name: z.string(),
884
+ /** 头像图(三格式图源 §4.97;留空用字头像) */
654
885
  avatar: z.string().default(""),
655
886
  amount: z.string().default(""),
656
887
  date: z.string().default(""),
@@ -822,7 +1053,7 @@ export const themeConfigSchema = z.object({
822
1053
  .object({
823
1054
  enabled: z.boolean().default(false),
824
1055
  title: z.string().default("广告"),
825
- /** 广告图(public 路径或外链,留空跳过) */
1056
+ /** 广告图(三格式图源 §4.97,留空跳过) */
826
1057
  image: z.string().default(""),
827
1058
  /** 点击广告图跳转的链接(留空则图不可点) */
828
1059
  imageLink: z.string().default(""),
@@ -1050,6 +1281,7 @@ export const themeConfigSchema = z.object({
1050
1281
  /**
1051
1282
  * favicon 图标列表(§4.30):数组非空时替换默认 /favicon.svg;
1052
1283
  * theme 指定 "light"/"dark" 时按系统深浅色经 media 切换(可配双图标)。
1284
+ * src 仅支持 public 路径 / 外链(不参与 src/assets 三格式解析,ico 无优化需求)。
1053
1285
  */
1054
1286
  favicon: z
1055
1287
  .array(
@@ -1079,13 +1311,74 @@ export const themeConfigSchema = z.object({
1079
1311
  * 樱花飘落特效(§4.30):全站 canvas 粒子(程序化花瓣,零贴图资产),
1080
1312
  * prefers-reduced-motion 自动停用,页签隐藏暂停。
1081
1313
  */
1314
+ /**
1315
+ * 樱花飘落特效(§4.30 / §4.101 八维参数对齐 Firefly):全站 canvas 粒子
1316
+ * (程序化花瓣,零贴图资产),prefers-reduced-motion 自动停用,页签隐藏暂停。
1317
+ * 注意:外层 default 必须全量展开嵌套值——zod 的 default 原样返回不二次 parse,
1318
+ * 写 `.default({})` 会导致深层字段运行时缺失。
1319
+ */
1082
1320
  sakura: z
1083
1321
  .object({
1084
1322
  enabled: z.boolean().default(false),
1085
1323
  /** 同屏花瓣数 */
1086
1324
  density: z.number().int().min(1).max(200).default(24),
1325
+ /** 飘落轮数:-1 无限循环;>=1 时落满轮数后不再重生,花瓣清空即停止动画 */
1326
+ limitTimes: z.number().int().min(-1).default(-1),
1327
+ /** canvas 层级(z-index),默认 100(低于 Header/悬浮球) */
1328
+ zIndex: z.number().int().default(100),
1329
+ /** 花瓣尺寸倍数区间(相对基准尺寸随机取值) */
1330
+ size: z
1331
+ .object({
1332
+ min: z.number().min(0.1).default(0.5),
1333
+ max: z.number().min(0.1).default(1.1),
1334
+ })
1335
+ .default({ min: 0.5, max: 1.1 }),
1336
+ /** 花瓣透明度区间 */
1337
+ opacity: z
1338
+ .object({
1339
+ min: z.number().min(0).max(1).default(0.3),
1340
+ max: z.number().min(0).max(1).default(0.9),
1341
+ })
1342
+ .default({ min: 0.3, max: 0.9 }),
1343
+ /** 运动速度:horizontal 水平漂移 px/帧(负值向左)、vertical 下落 px/帧、rotation 每帧自转弧度、fadeSpeed 临终淡出速度(不应大于 opacity.min) */
1344
+ speed: z
1345
+ .object({
1346
+ horizontal: z
1347
+ .object({
1348
+ min: z.number().default(-1.7),
1349
+ max: z.number().default(-1.2),
1350
+ })
1351
+ .default({ min: -1.7, max: -1.2 }),
1352
+ vertical: z
1353
+ .object({
1354
+ min: z.number().default(1.5),
1355
+ max: z.number().default(2.2),
1356
+ })
1357
+ .default({ min: 1.5, max: 2.2 }),
1358
+ rotation: z.number().default(0.03),
1359
+ fadeSpeed: z.number().default(0.03),
1360
+ })
1361
+ .default({
1362
+ horizontal: { min: -1.7, max: -1.2 },
1363
+ vertical: { min: 1.5, max: 2.2 },
1364
+ rotation: 0.03,
1365
+ fadeSpeed: 0.03,
1366
+ }),
1087
1367
  })
1088
- .default({ enabled: false, density: 24 }),
1368
+ .default({
1369
+ enabled: false,
1370
+ density: 24,
1371
+ limitTimes: -1,
1372
+ zIndex: 100,
1373
+ size: { min: 0.5, max: 1.1 },
1374
+ opacity: { min: 0.3, max: 0.9 },
1375
+ speed: {
1376
+ horizontal: { min: -1.7, max: -1.2 },
1377
+ vertical: { min: 1.5, max: 2.2 },
1378
+ rotation: 0.03,
1379
+ fadeSpeed: 0.03,
1380
+ },
1381
+ }),
1089
1382
  /**
1090
1383
  * 音乐播放器(§4.36):local 模式,自研播放内核(audio + LRC 解析),
1091
1384
  * 根节点 transition:persist 跨页不断播。meting 在线模式预留后续扩展。
@@ -1110,7 +1403,7 @@ export const themeConfigSchema = z.object({
1110
1403
  artist: z.string().default(""),
1111
1404
  /** 音频地址(public 路径或外链) */
1112
1405
  url: z.string(),
1113
- /** 封面图(可选,缺省渲染唱片图标) */
1406
+ /** 封面图(三格式图源 §4.97,可选,缺省渲染唱片图标) */
1114
1407
  cover: z.string().optional(),
1115
1408
  /** 歌词:.lrc 文件路径或内联 LRC 文本(可选,缺省无歌词) */
1116
1409
  lrc: z.string().optional(),