@windcoder/astro-theme-orchid 0.0.0-stage → 0.2.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 (83) hide show
  1. package/README.md +118 -2
  2. package/package.json +70 -5
  3. package/src/components/AlbumCard.astro +73 -0
  4. package/src/components/Analytics.astro +22 -0
  5. package/src/components/BaseLayout.astro +188 -0
  6. package/src/components/Calendar.astro +379 -0
  7. package/src/components/CategoryBar.astro +74 -0
  8. package/src/components/Comment.astro +48 -0
  9. package/src/components/EncryptedContent.astro +42 -0
  10. package/src/components/Footer.astro +31 -0
  11. package/src/components/Header.astro +153 -0
  12. package/src/components/License.astro +29 -0
  13. package/src/components/Live2DWidget.astro +262 -0
  14. package/src/components/MusicPlayer.astro +940 -0
  15. package/src/components/OutdatedCard.astro +53 -0
  16. package/src/components/Pagination.astro +94 -0
  17. package/src/components/PhotoCard.astro +22 -0
  18. package/src/components/PostCard.astro +89 -0
  19. package/src/components/PostNav.astro +40 -0
  20. package/src/components/RecommendedPost.astro +166 -0
  21. package/src/components/SakuraEffect.astro +152 -0
  22. package/src/components/Sidebar.astro +334 -0
  23. package/src/components/Toc.astro +27 -0
  24. package/src/components/analytics/GoogleAnalytics.astro +22 -0
  25. package/src/components/analytics/La51Analytics.astro +52 -0
  26. package/src/components/analytics/MicrosoftClarity.astro +18 -0
  27. package/src/components/analytics/UmamiAnalytics.astro +86 -0
  28. package/src/components/comments/Artalk.astro +51 -0
  29. package/src/components/comments/Disqus.astro +43 -0
  30. package/src/components/comments/Giscus.astro +50 -0
  31. package/src/components/comments/Twikoo.astro +65 -0
  32. package/src/components/comments/Waline.astro +36 -0
  33. package/src/i18n/en.ts +164 -0
  34. package/src/i18n/index.ts +18 -0
  35. package/src/i18n/resolve.ts +49 -0
  36. package/src/i18n/zh-CN.ts +184 -0
  37. package/src/icons.ts +71 -0
  38. package/src/index.ts +411 -0
  39. package/src/plugins/plantuml-encoder.ts +95 -0
  40. package/src/plugins/rehype-diagram.ts +218 -0
  41. package/src/plugins/rehype-ec-vt-rerun.ts +80 -0
  42. package/src/plugins/rehype-email.ts +75 -0
  43. package/src/plugins/rehype-github-card.ts +114 -0
  44. package/src/plugins/rehype.ts +284 -0
  45. package/src/plugins/remark-diagram.ts +78 -0
  46. package/src/plugins/remark-directives.ts +100 -0
  47. package/src/plugins/remark-excerpt.ts +29 -0
  48. package/src/plugins/remark-image-grid.ts +97 -0
  49. package/src/plugins/remark-reading-time.ts +38 -0
  50. package/src/plugins/scripts/diagram-panzoom.js +388 -0
  51. package/src/plugins/scripts/plantuml-theme-switch.js +90 -0
  52. package/src/routes/404.astro +14 -0
  53. package/src/routes/_hydrate.ts +34 -0
  54. package/src/routes/about.astro +25 -0
  55. package/src/routes/api/allPostMeta.json.ts +26 -0
  56. package/src/routes/archive.astro +81 -0
  57. package/src/routes/categories/[category].astro +37 -0
  58. package/src/routes/categories/index.astro +29 -0
  59. package/src/routes/friends.astro +83 -0
  60. package/src/routes/gallery/[album].astro +97 -0
  61. package/src/routes/gallery/index.astro +100 -0
  62. package/src/routes/guestbook.astro +37 -0
  63. package/src/routes/index.astro +47 -0
  64. package/src/routes/og/[...slug].ts +281 -0
  65. package/src/routes/page/[page].astro +53 -0
  66. package/src/routes/posts/[...slug].astro +350 -0
  67. package/src/routes/robots.txt.ts +18 -0
  68. package/src/routes/rss.xml.ts +37 -0
  69. package/src/routes/series/[name].astro +66 -0
  70. package/src/routes/series/index.astro +64 -0
  71. package/src/routes/sponsor.astro +84 -0
  72. package/src/routes/tags/[tag].astro +40 -0
  73. package/src/routes/tags/index.astro +29 -0
  74. package/src/schema.ts +1218 -0
  75. package/src/scripts/app.ts +1041 -0
  76. package/src/scripts/share-poster.ts +468 -0
  77. package/src/styles/global.css +4864 -0
  78. package/src/utils/crypto.ts +46 -0
  79. package/src/utils/gallery.ts +76 -0
  80. package/src/utils/lqip.ts +18 -0
  81. package/src/utils/related.ts +101 -0
  82. package/src/utils.ts +233 -0
  83. package/src/vite.ts +105 -0
@@ -0,0 +1,153 @@
1
+ ---
2
+ import config from "virtual:theme/config";
3
+ import { t } from "../i18n/index.js";
4
+
5
+ const { title, nav, logo } = config;
6
+
7
+ /** 外链统一属性(§4.16):新窗口 + noopener */
8
+ function externalAttrs(external?: boolean) {
9
+ return external ? { target: "_blank", rel: "noopener noreferrer" } : {};
10
+ }
11
+
12
+ /** 内置页导航高亮 key(body[data-page] ↔ .nav-link[data-nav]) */
13
+ function navKey(href: string): string | null {
14
+ if (href === "/") return "home";
15
+ if (href === "/archive" || href.startsWith("/archive/")) return "archive";
16
+ if (href === "/tags" || href.startsWith("/tags/")) return "tags";
17
+ if (href === "/about" || href.startsWith("/about/")) return "about";
18
+ if (href === "/friends" || href.startsWith("/friends/")) return "friends";
19
+ if (href === "/guestbook" || href.startsWith("/guestbook/"))
20
+ return "guestbook";
21
+ if (href === "/sponsor" || href.startsWith("/sponsor/")) return "sponsor";
22
+ return null;
23
+ }
24
+
25
+ /* hue 调色面板(M3b-b5):预设色板为代码级常量;运行时覆写 --accent-h,s/l 保持配置值 */
26
+ const hueEnabled = config.huePanel.enabled;
27
+ const presetHues = [0, 30, 60, 100, 140, 170, 210, 250, 290, 330];
28
+ ---
29
+
30
+ <header class="site-header">
31
+ <div class="shell header-inner">
32
+ {/* Logo(§4.20):配置非空时标题左侧渲染,与文字同行 */}
33
+ <a class="site-title" href="/">
34
+ {logo && <img class="nav-logo" src={logo} alt={config.title} />}
35
+ {title}
36
+ </a>
37
+ <nav class="site-nav" id="siteNav" aria-label={t('mainNav')}>
38
+ {
39
+ nav.map((item) => {
40
+ const key = item.href ? navKey(item.href) : null;
41
+ /* 子菜单(§4.16):父项有 href 则可点击跳转(hover 展开);无 href 仅作下拉入口 */
42
+ if (item.children.length > 0) {
43
+ return (
44
+ <div class="nav-item has-sub">
45
+ {item.href ? (
46
+ <a class="nav-link" data-nav={key ?? undefined} href={item.href} {...externalAttrs(item.external)}>
47
+ {item.label}
48
+ {/* 父项下拉 chevron(§4.27):对齐 orchid-blog 标识,子菜单展开时旋转 */}
49
+ <svg class="nav-caret" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
50
+ </a>
51
+ ) : (
52
+ <button class="nav-link nav-sub-toggle" type="button" aria-expanded="false">
53
+ {item.label}
54
+ <svg class="nav-caret" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
55
+ </button>
56
+ )}
57
+ <div class="nav-sub">
58
+ {item.children.map((c) => {
59
+ const ck = c.href ? navKey(c.href) : null;
60
+ return (
61
+ <a class="nav-sub-link" data-nav={ck ?? undefined} href={c.href} {...externalAttrs(c.external)}>
62
+ {c.label}
63
+ </a>
64
+ );
65
+ })}
66
+ </div>
67
+ </div>
68
+ );
69
+ }
70
+ return (
71
+ <a class="nav-link" data-nav={key ?? undefined} href={item.href ?? '#'} {...externalAttrs(item.external)}>
72
+ {item.label}
73
+ </a>
74
+ );
75
+ })
76
+ }
77
+ </nav>
78
+ <div class="header-actions">
79
+ {
80
+ /* 搜索(§4.56):置于 actions 首位,其余按钮跟在其后;对齐参考项目形态——
81
+ 桌面为导航栏内联搜索框(输入即搜),移动端退化为 icon 按钮 + 浮层(含输入框);
82
+ 索引由构建期 pagefind 生成,dev 下降级提示 */
83
+ }
84
+ <div class="search-wrap">
85
+ <div class="nav-search" title={t('searchShortcut')}>
86
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
87
+ <input id="searchInput" type="search" placeholder={t('search')} autocomplete="off" enterkeyhint="search" aria-label={t('search')} />
88
+ </div>
89
+ <button class="icon-btn nav-search-toggle" id="searchToggle" type="button" aria-label={t('search')} aria-expanded="false">
90
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
91
+ </button>
92
+ <div class="search-pop" id="searchPop" hidden>
93
+ <div class="search-bar">
94
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
95
+ <input id="searchInputM" type="search" placeholder={t('search')} autocomplete="off" enterkeyhint="search" aria-label={t('search')} />
96
+ </div>
97
+ <div class="search-results" id="searchResults" hidden></div>
98
+ </div>
99
+ </div>
100
+ {
101
+ /* 列表形态切换(§4.34→§4.55):allowSwitch 控制;仅首页/列表页显示(CSS data-page 显隐) */
102
+ }
103
+ {config.postList.allowSwitch && (
104
+ <button
105
+ class="icon-btn layout-switch"
106
+ id="layoutSwitch"
107
+ type="button"
108
+ aria-label={t('layoutSwitch')}
109
+ aria-pressed="false"
110
+ title={t('layoutSwitch')}
111
+ >
112
+ <svg class="ic-list" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/></svg>
113
+ <svg class="ic-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
114
+ </button>
115
+ )}
116
+ {
117
+ hueEnabled && (
118
+ <div class="palette-wrap">
119
+ <button class="icon-btn" id="paletteToggle" type="button" aria-label={t('palette')} aria-expanded="false">
120
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="13.5" cy="6.5" r=".5" fill="currentColor"/><circle cx="17.5" cy="10.5" r=".5" fill="currentColor"/><circle cx="8.5" cy="7.5" r=".5" fill="currentColor"/><circle cx="6.5" cy="12.5" r=".5" fill="currentColor"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"/></svg>
121
+ </button>
122
+ <div class="palette-pop" id="palettePop" hidden>
123
+ <div class="palette-title">{t('paletteTitle')}</div>
124
+ <div class="palette-row">
125
+ {presetHues.map((h) => (
126
+ <button
127
+ class="swatch"
128
+ type="button"
129
+ style={`--sw: hsl(${h} var(--accent-s) var(--accent-l))`}
130
+ data-hue={h}
131
+ aria-label={t('hueValue', { n: h })}
132
+ />
133
+ ))}
134
+ </div>
135
+ <input class="hue-slider" id="hueSlider" type="range" min="0" max="360" step="1" aria-label={t('paletteTitle')} />
136
+ <div class="palette-foot">
137
+ <span class="hue-badge" id="hueBadge" />
138
+ <button class="palette-reset" id="hueReset" type="button">{t('reset')}</button>
139
+ </div>
140
+ </div>
141
+ </div>
142
+ )
143
+ }
144
+ <button class="icon-btn" id="modeToggle" type="button" aria-label={t('toggleMode')}>
145
+ <svg class="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>
146
+ <svg class="i-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>
147
+ </button>
148
+ <button class="icon-btn nav-burger" id="navToggle" type="button" aria-label={t('menu')} aria-expanded="false">
149
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="18" y2="18"/></svg>
150
+ </button>
151
+ </div>
152
+ </div>
153
+ </header>
@@ -0,0 +1,29 @@
1
+ ---
2
+ import config from "virtual:theme/config";
3
+ import { t } from "../i18n/index.js";
4
+
5
+ interface Props {
6
+ /** 文章级覆盖;空则跟随全站配置 */
7
+ licenseName?: string;
8
+ licenseUrl?: string;
9
+ /** 文章级开关(frontmatter license);false 强制隐藏 */
10
+ enabled?: boolean;
11
+ }
12
+
13
+ const { licenseName, licenseUrl, enabled } = Astro.props;
14
+ const enabledFinal = config.license.enabled && (enabled ?? true);
15
+ const name = licenseName ?? config.license.name;
16
+ const url = licenseUrl ?? config.license.url;
17
+ // prefix/suffix 留空时回退语言包默认文案
18
+ const prefix = config.license.prefix || t("licensePrefix");
19
+ const suffix = config.license.suffix || t("licenseSuffix");
20
+ ---
21
+
22
+ {
23
+ enabledFinal && (
24
+ <div class="license">
25
+ {prefix} <a href={url} target="_blank" rel="noopener">{name}</a>{' '}
26
+ {suffix}
27
+ </div>
28
+ )
29
+ }
@@ -0,0 +1,262 @@
1
+ ---
2
+ /**
3
+ * Live2D 看板娘(§4.38):l2d-widget 悬浮模型 + 菜单 + 气泡说话。
4
+ * - 根节点 transition:persist:VT 换页模型 DOM 保留,不重载不闪断,脚本单次初始化
5
+ * - 菜单 action 映射:home / scrollToTop / sleep / switchModel / link(url)
6
+ * - tips 挂到 options.model.tips;库内气泡行内 nowrap 由 MutationObserver 修正
7
+ * - 移动端按断点隐藏:隐藏态不加载模型(省流量),跨过断点自动补拉起
8
+ */
9
+ interface Props {
10
+ live2d: {
11
+ model?:
12
+ | { path: string; volume: number; scale: number; x: number; y: number }
13
+ | Array<{
14
+ path: string;
15
+ volume: number;
16
+ scale: number;
17
+ x: number;
18
+ y: number;
19
+ }>;
20
+ position: "bottom-left" | "bottom-right";
21
+ size: number | { width: number; height: number };
22
+ primaryColor: string;
23
+ transitionDuration?: number;
24
+ transitionType?: "slide" | "fade";
25
+ menus?: {
26
+ items: Array<{
27
+ icon: string;
28
+ label: string;
29
+ action: string;
30
+ url?: string;
31
+ }>;
32
+ align: "left" | "right";
33
+ };
34
+ tips?: {
35
+ enable: boolean;
36
+ welcomeMessage: string[];
37
+ messages: string[];
38
+ duration: number;
39
+ interval: number;
40
+ offset: { x: number; y: number };
41
+ };
42
+ responsive: { hideOnMobile: boolean; mobileBreakpoint: number };
43
+ zIndex: number;
44
+ };
45
+ }
46
+
47
+ const { live2d } = Astro.props;
48
+
49
+ import { t } from "../i18n/index.js";
50
+
51
+ const modelList = Array.isArray(live2d.model)
52
+ ? live2d.model
53
+ : live2d.model
54
+ ? [live2d.model]
55
+ : [];
56
+
57
+ // 未配置菜单时的默认三键(多模型自动追加「切换模型」);自定义 menus 覆盖默认
58
+ const menus = live2d.menus ?? {
59
+ items: [
60
+ { icon: "mdi:home", label: t("l2dHome"), action: "home" },
61
+ { icon: "mdi:arrow-up", label: t("l2dScrollTop"), action: "scrollToTop" },
62
+ { icon: "mdi:bed", label: t("l2dSleep"), action: "sleep" },
63
+ ...(modelList.length > 1
64
+ ? [
65
+ {
66
+ icon: "mdi:swap-horizontal",
67
+ label: t("l2dSwitchModel"),
68
+ action: "switchModel",
69
+ },
70
+ ]
71
+ : []),
72
+ ],
73
+ align: "right" as const,
74
+ };
75
+
76
+ // x/y 转库的 offset: [x, y];volume/scale 恒有值(schema default)
77
+ const models = modelList.map((m) => ({
78
+ path: m.path,
79
+ volume: m.volume,
80
+ scale: m.scale,
81
+ offset: [m.x, m.y] as [number, number],
82
+ }));
83
+ ---
84
+
85
+ <div
86
+ id="l2d-widget-container"
87
+ transition:persist="live2d-widget"
88
+ style={`position:fixed;z-index:${live2d.zIndex};`}
89
+ data-models={JSON.stringify(models)}
90
+ data-position={live2d.position}
91
+ data-size={JSON.stringify(live2d.size)}
92
+ data-primary-color={live2d.primaryColor}
93
+ data-transition-duration={live2d.transitionDuration !== undefined ? String(live2d.transitionDuration) : ""}
94
+ data-transition-type={live2d.transitionType ?? ""}
95
+ data-menus={JSON.stringify(menus)}
96
+ data-tips={JSON.stringify(live2d.tips ?? null)}
97
+ data-responsive={JSON.stringify(live2d.responsive)}
98
+ >
99
+ </div>
100
+
101
+ <script>
102
+ // 模块级初始化标志:动态导入等待期间防 resize/重复调度重入(dataset guard 仅成功后置位)
103
+ let initStarted = false;
104
+ let currentModelIndex = 0;
105
+
106
+ // l2d-widget 气泡行内 white-space:nowrap 不换行;库用行内样式 CSS 盖不动,JS 修正
107
+ function patchTipBubble(el: HTMLElement) {
108
+ if (el.style.whiteSpace === "nowrap") {
109
+ el.style.whiteSpace = "normal";
110
+ el.style.width = "190px";
111
+ }
112
+ }
113
+
114
+ const tipObserver = new MutationObserver((mutations) => {
115
+ const container = document.getElementById("l2d-widget-container");
116
+ for (const m of mutations) {
117
+ for (const node of m.addedNodes) {
118
+ // 只处理挂件容器内的新增节点:VT 换页全页重建 body 时不做全页遍历
119
+ if (!(node instanceof HTMLElement) || !container?.contains(node)) continue;
120
+ patchTipBubble(node);
121
+ node.querySelectorAll<HTMLElement>("*").forEach(patchTipBubble);
122
+ }
123
+ }
124
+ });
125
+ tipObserver.observe(document.body, { childList: true, subtree: true });
126
+
127
+ async function initWidget() {
128
+ const container = document.getElementById("l2d-widget-container");
129
+ if (!container || container.dataset.l2dInit === "1" || initStarted) return;
130
+ initStarted = true;
131
+
132
+ try {
133
+ const models = JSON.parse(container.dataset.models || "[]");
134
+ if (!models.length) {
135
+ initStarted = false;
136
+ return;
137
+ }
138
+
139
+ // 移动端首访不置 guard:跨过断点转桌面端时由 resize 补初始化
140
+ const responsive = JSON.parse(container.dataset.responsive || "{}");
141
+ if (
142
+ responsive.hideOnMobile &&
143
+ window.innerWidth <= (responsive.mobileBreakpoint || 768)
144
+ ) {
145
+ container.style.display = "none";
146
+ initStarted = false;
147
+ return;
148
+ }
149
+
150
+ const options: any = {
151
+ model: models.length === 1 ? models[0] : models,
152
+ position: container.dataset.position || "bottom-left",
153
+ size: JSON.parse(container.dataset.size || "280"),
154
+ };
155
+ const primaryColor = container.dataset.primaryColor;
156
+ if (primaryColor) options.primaryColor = primaryColor;
157
+ const transitionDuration = container.dataset.transitionDuration;
158
+ if (transitionDuration) options.transitionDuration = Number.parseInt(transitionDuration, 10);
159
+ const transitionType = container.dataset.transitionType;
160
+ if (transitionType) options.transitionType = transitionType;
161
+
162
+ // 菜单 action 字符串映射为函数(l2d-widget 会把 widget 实例传给 onClick)
163
+ const menuActions: Record<string, (widget: any) => void> = {
164
+ home: () => (window.location.href = "/"),
165
+ scrollToTop: () => window.scrollTo({ top: 0, behavior: "smooth" }),
166
+ sleep: (widget) => widget.sleep(),
167
+ switchModel: (widget) => {
168
+ // 循环切换:容器 data-models 为准(persist 后 dataset 仍在)
169
+ const all = JSON.parse(container.dataset.models || "[]");
170
+ currentModelIndex = (currentModelIndex + 1) % all.length;
171
+ widget.switchModel(currentModelIndex);
172
+ },
173
+ };
174
+ const menus = JSON.parse(container.dataset.menus || "{}");
175
+ if (menus.items?.length) {
176
+ options.menus = {
177
+ items: menus.items.map((item: any) => ({
178
+ icon: item.icon,
179
+ label: item.label,
180
+ onClick:
181
+ item.action === "link" && item.url
182
+ ? () => window.open(item.url, "_blank", "noopener")
183
+ : menuActions[item.action] || (() => {}),
184
+ })),
185
+ ...(menus.align && { align: menus.align }),
186
+ };
187
+ }
188
+
189
+ // tips 挂到 model.tips:enable:false 显式关闭;空对象不覆盖库默认
190
+ const tips = JSON.parse(container.dataset.tips || "null");
191
+ if (tips) {
192
+ const tipsConfig =
193
+ tips.enable === false
194
+ ? false
195
+ : {
196
+ ...(tips.welcomeMessage?.length && { welcomeMessage: tips.welcomeMessage }),
197
+ ...(tips.messages?.length && { messages: tips.messages }),
198
+ ...(tips.duration !== undefined && { duration: tips.duration }),
199
+ ...(tips.interval !== undefined && { interval: tips.interval }),
200
+ ...(tips.offset && { offset: tips.offset }),
201
+ ...(tips.typing && { typing: tips.typing }),
202
+ };
203
+ if (tipsConfig === false || Object.keys(tipsConfig).length > 0) {
204
+ if (Array.isArray(options.model)) {
205
+ options.model.forEach((m: any) => (m.tips = tipsConfig));
206
+ } else {
207
+ options.model.tips = tipsConfig;
208
+ }
209
+ }
210
+ }
211
+
212
+ // 动态导入:~845KB Cubism/WebGL 引擎拆独立 chunk,首屏不解析不阻塞
213
+ const { createWidget } = await import("l2d-widget");
214
+ // await 期间 VT 换页可能移除了旧容器:不在 DOM 则放弃,待新容器重新初始化
215
+ if (!container.isConnected) {
216
+ initStarted = false;
217
+ return;
218
+ }
219
+
220
+ // 库无视传入 container、硬挂 document.body:VT 换页重建 body 时 canvas 根被连根
221
+ // 拔走,Cubism rAF 继续向脱离 DOM 的 canvas 绘制 → 每次导航泄漏一个 WebGL 上下文。
222
+ // 挂载前后 diff body 子节点,把库新增根节点迁入 persist 容器:换页不死、上下文只建一次
223
+ const before = new Set(document.body.children);
224
+ createWidget(options);
225
+ // guard 成功后才置位:失败/异常路径全部解锁,允许重试
226
+ container.dataset.l2dInit = "1";
227
+ for (const el of [...document.body.children]) {
228
+ if (!before.has(el)) container.appendChild(el);
229
+ }
230
+ } catch (err) {
231
+ console.error("[live2d] 初始化失败:", err);
232
+ initStarted = false;
233
+ }
234
+ }
235
+
236
+ // 响应式:跨过断点隐藏/显示,显示时若尚未初始化则补拉起
237
+ function handleResponsive() {
238
+ const container = document.getElementById("l2d-widget-container");
239
+ if (!container) return;
240
+ const responsive = JSON.parse(container.dataset.responsive || "{}");
241
+ if (!responsive.hideOnMobile) return;
242
+ const hidden = window.innerWidth <= (responsive.mobileBreakpoint || 768);
243
+ container.style.display = hidden ? "none" : "";
244
+ if (!hidden && container.dataset.l2dInit !== "1") initWidget();
245
+ }
246
+
247
+ // 空闲时再初始化:不与首屏渲染/VT 换页抢主线程(不支持时降级 setTimeout)
248
+ function scheduleInit() {
249
+ if (typeof window.requestIdleCallback === "function") {
250
+ window.requestIdleCallback(() => initWidget(), { timeout: 2000 });
251
+ } else {
252
+ setTimeout(initWidget, 200);
253
+ }
254
+ }
255
+
256
+ if (document.readyState === "loading") {
257
+ document.addEventListener("DOMContentLoaded", scheduleInit, { once: true });
258
+ } else {
259
+ scheduleInit();
260
+ }
261
+ window.addEventListener("resize", handleResponsive);
262
+ </script>