@mintfolio/theme-verdant 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 (67) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +66 -0
  3. package/THIRD_PARTY_NOTICES.md +11 -0
  4. package/assets/favicon.ico +0 -0
  5. package/assets/favicon.svg +9 -0
  6. package/assets/fonts/Inter-300.woff2 +0 -0
  7. package/assets/fonts/Inter-400.woff2 +0 -0
  8. package/assets/fonts/Inter-500.woff2 +0 -0
  9. package/assets/fonts/Inter-600.woff2 +0 -0
  10. package/assets/fonts/Inter-700.woff2 +0 -0
  11. package/assets/fonts/JetBrainsMono-400.woff2 +0 -0
  12. package/assets/fonts/JetBrainsMono-500.woff2 +0 -0
  13. package/assets/fonts/PlayfairDisplay-400.woff2 +0 -0
  14. package/assets/fonts/PlayfairDisplay-500.woff2 +0 -0
  15. package/assets/fonts/PlayfairDisplay-600.woff2 +0 -0
  16. package/assets/fonts/PlayfairDisplay-700.woff2 +0 -0
  17. package/components/base/Contact.astro +173 -0
  18. package/components/base/Hero.astro +228 -0
  19. package/components/base/Navigation.astro +288 -0
  20. package/components/base/Projects.astro +207 -0
  21. package/components/base/Skills.astro +158 -0
  22. package/components/blog/ArticleProse.astro +217 -0
  23. package/components/blog/CodeBlockEnhancer.astro +310 -0
  24. package/components/blog/PostCard.astro +98 -0
  25. package/components/blog/ProtectedArticle.astro +89 -0
  26. package/config/theme-verdant.config.mjs +74 -0
  27. package/layouts/Base.astro +460 -0
  28. package/lib/palettes.ts +10 -0
  29. package/licenses/Inter-OFL.txt +92 -0
  30. package/licenses/JetBrainsMono-OFL.txt +93 -0
  31. package/licenses/PlayfairDisplay-OFL.txt +191 -0
  32. package/package.json +68 -0
  33. package/pages/archive.astro +384 -0
  34. package/pages/home.astro +2049 -0
  35. package/pages/page.astro +434 -0
  36. package/pages/post.astro +1029 -0
  37. package/scripts/article.ts +42 -0
  38. package/scripts/articleOrigin.ts +31 -0
  39. package/scripts/articleTables.ts +15 -0
  40. package/scripts/blog.ts +26 -0
  41. package/scripts/codeBlocks.ts +172 -0
  42. package/scripts/hero.ts +173 -0
  43. package/scripts/home.ts +12 -0
  44. package/scripts/homePanels.ts +100 -0
  45. package/scripts/hotContent.ts +16 -0
  46. package/scripts/lifecycle.ts +3 -0
  47. package/scripts/lightbox.ts +14 -0
  48. package/scripts/postFilters.ts +101 -0
  49. package/scripts/postList.ts +132 -0
  50. package/scripts/progress.ts +29 -0
  51. package/scripts/protectedArticle.ts +49 -0
  52. package/scripts/protectedToc.ts +29 -0
  53. package/scripts/site.ts +145 -0
  54. package/scripts/tagFold.ts +61 -0
  55. package/scripts/theme.ts +102 -0
  56. package/scripts/toc.ts +149 -0
  57. package/settings.ts +21 -0
  58. package/styles/fonts.css +120 -0
  59. package/styles/global.css +146 -0
  60. package/styles/themes/happyhues/effects.css +62 -0
  61. package/styles/themes/happyhues/index.css +2 -0
  62. package/styles/themes/happyhues/theme.css +225 -0
  63. package/theme.mjs +60 -0
  64. package/utils/blogNavigation.ts +3 -0
  65. package/utils/homeReturnHint.ts +34 -0
  66. package/utils/storage.ts +16 -0
  67. package/utils/types.ts +8 -0
@@ -0,0 +1,89 @@
1
+ ---
2
+ import type { ArticleBody } from '@mintfolio/theme-api/astro';
3
+ import ArticleProse from './ArticleProse.astro';
4
+
5
+ interface Props {
6
+ /** Only Core's opaque protected body may cross into this password form. */
7
+ body: Extract<ArticleBody, { kind: 'protected' }>;
8
+ }
9
+ const { body } = Astro.props;
10
+ const { postId, payload } = body;
11
+ ---
12
+
13
+ <section class="protected-post" data-protected-post data-post-id={postId} data-state="locked">
14
+ <div class="password-gate">
15
+ <span class="gate-icon" aria-hidden="true">🔒</span>
16
+ <p class="gate-eyebrow">PRIVATE NOTE</p>
17
+ <h2>这篇文章已加密</h2>
18
+ <p class="gate-description">输入文章密码,继续阅读。</p>
19
+ <form class="password-form" method="post">
20
+ <label for="article-password">文章密码</label>
21
+ <div class="password-actions">
22
+ <input id="article-password" type="password" autocomplete="current-password" required disabled
23
+ aria-describedby="password-status" placeholder="请输入密码" />
24
+ <button type="submit" disabled>解锁文章</button>
25
+ </div>
26
+ <p id="password-status" class="password-status" role="status" aria-live="polite"></p>
27
+ </form>
28
+ <noscript><p>请启用 JavaScript 后输入密码解锁。</p></noscript>
29
+ </div>
30
+ <div class="unlocked-bar" hidden>
31
+ <span>🔓 文章已解锁</span>
32
+ <button type="button" class="lock-article">重新锁定</button>
33
+ </div>
34
+ <div class="protected-article-content" hidden tabindex="-1"></div>
35
+ <template data-prose-template><ArticleProse /></template>
36
+ <slot name="toc-template" />
37
+ <script is:inline type="application/json" class="encrypted-article-data" set:html={JSON.stringify(payload)} />
38
+ </section>
39
+
40
+
41
+ <style>
42
+ .password-gate {
43
+ padding: clamp(1.5rem, 5vw, 3rem);
44
+ border: 1px solid var(--color-border);
45
+ border-radius: var(--radius-lg);
46
+ background: radial-gradient(ellipse at top right, var(--color-play-soft), transparent 65%), var(--color-surface);
47
+ box-shadow: 6px 6px 0 var(--color-primary-soft);
48
+ text-align: center;
49
+ }
50
+ .gate-icon { display: inline-grid; place-items: center; width: 3.5rem; height: 3.5rem; border-radius: 1rem; background: var(--color-play-soft); font-size: 1.5rem; }
51
+ .gate-eyebrow { margin-top: 1rem; color: var(--color-primary); font-size: 0.7rem; font-weight: 700; letter-spacing: 0.18em; }
52
+ h2 { margin: 0.55rem 0; color: var(--color-text); font-size: clamp(1.4rem, 4vw, 1.9rem); }
53
+ .gate-description { color: var(--color-text-muted); }
54
+ .password-form { max-width: 27rem; margin: 1.8rem auto 0; text-align: left; }
55
+ label { display: block; margin-bottom: 0.5rem; font-size: 0.85rem; font-weight: 600; }
56
+ .password-actions { display: flex; gap: 0.6rem; }
57
+ input { min-width: 0; flex: 1; padding: 0.8rem 1rem; border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-bg); color: var(--color-text); font: inherit; }
58
+ input:focus-visible, button:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 3px; }
59
+ button {
60
+ --button-bg: var(--color-primary);
61
+ padding: 0.8rem 1rem;
62
+ border: 0;
63
+ border-radius: var(--radius-md);
64
+ background: var(--button-bg);
65
+ color: var(--color-on-primary);
66
+ font: inherit;
67
+ font-weight: 600;
68
+ white-space: nowrap;
69
+ cursor: pointer;
70
+ touch-action: manipulation;
71
+ transition: background-color var(--transition-fast), opacity var(--transition-fast), transform var(--transition-fast);
72
+ }
73
+ @media (hover: hover) and (pointer: fine) {
74
+ button:hover:not(:disabled) { background: color-mix(in srgb, var(--button-bg) 90%, var(--color-text) 10%); }
75
+ }
76
+ @media (prefers-reduced-motion: no-preference) {
77
+ button:active:not(:disabled) { transform: scale(0.98); }
78
+ }
79
+ button:disabled { opacity: 0.6; cursor: wait; }
80
+ .password-status { min-height: 1.5em; margin-top: 0.7rem; color: var(--color-text-muted); font-size: 0.85rem; }
81
+ .unlocked-bar { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 1rem; color: var(--color-text-muted); font-size: 0.85rem; }
82
+ .lock-article { --button-bg: var(--color-primary-soft); padding: 0.45rem 0.8rem; color: var(--color-primary); }
83
+ [hidden] { display: none !important; }
84
+ @media (max-width: 420px) { .password-actions { flex-direction: column; } }
85
+ </style>
86
+
87
+ <script>
88
+ import '../../scripts/protectedArticle';
89
+ </script>
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Mintfolio Verdant 主题设置。
3
+ * 在 theme.config.mjs 中选择 @mintfolio/theme-verdant 后,本文件自动生效。
4
+ * 修改后保存即可;开发服务会重新加载配置。重复安装或生成不会覆盖本文件。
5
+ * 网站名称、域名、个人资料、社交链接与项目仍在 site.config.ts 中设置。
6
+ * 如果 theme.config.mjs 还包含旧的 settings,同名设置以旧的内联值为准。
7
+ */
8
+
9
+ /** @satisfies {import('@mintfolio/theme-verdant/settings').VerdantSettings} */
10
+ export default {
11
+ // 初始明暗模式:'auto' 跟随系统、'light' 浅色、'dark' 深色。
12
+ // 访客通过主题面板保存过的个人偏好,会优先于这里的初始设置。
13
+ initialMode: 'auto',
14
+
15
+ // 初始配色,使用字符串编号:
16
+ // '1' 青柠薄荷 '2' 橘子汽水 '3' 晴空海盐 '4' 葡萄跳跳糖
17
+ // '5' 莓果奶昔 '6' 向日葵 '7' 海岛假日 '8' 午夜游乐场
18
+ initialPalette: '1',
19
+
20
+ // 首页首次显示的文章数量,范围 1–100;其余文章通过“加载更多”显示。
21
+ homePageSize: 6,
22
+
23
+ // 博客归档页首次显示的文章数量,范围 1–100。
24
+ archivePageSize: 8,
25
+
26
+ // 可选的 Google Analytics 测量 ID,例如 'G-XXXXXXXXXX';留空不加载统计。
27
+ analyticsId: '',
28
+
29
+ sidebar: {
30
+ // 各侧栏区域的显示开关。联系方式和最近动态的数据由站点内容提供。
31
+ sections: {
32
+ contact: true, // 联系方式:使用 site.config.ts 中的 social 等资料。
33
+ activity: true, // 最近动态。
34
+ tools: true, // 下方配置的推荐工具列表。
35
+ },
36
+
37
+ // 推荐工具列表;可按示例增加多项,空列表不显示工具条目。
38
+ tools: [
39
+ // {
40
+ // name: '工具名称',
41
+ // description: '用一句话介绍这个工具',
42
+ // url: 'https://example.com',
43
+ // },
44
+ ],
45
+
46
+ // 侧栏引言;启用后填写内容和作者。
47
+ quote: {
48
+ enabled: false,
49
+ text: '',
50
+ author: '',
51
+ },
52
+ },
53
+
54
+ home: {
55
+ // 首页推荐文章。填写已有文章 ID,最多展示前三篇找到的文章。
56
+ hotContent: {
57
+ enabled: false,
58
+ items: [
59
+ // {
60
+ // postId: 'hello', // 对应 content/blog/hello.md 的文章 ID。
61
+ // image: '', // 留空显示占位图;也可填写 '/images/hello.webp'。
62
+ // },
63
+ ],
64
+ },
65
+ },
66
+
67
+ article: {
68
+ // 普通文章末尾的插图;开启后 src 留空会使用站点头像。
69
+ footerImage: {
70
+ enabled: false,
71
+ src: '', // 例如 '/images/article-footer.webp',文件放在 public/images/ 中。
72
+ },
73
+ },
74
+ };
@@ -0,0 +1,460 @@
1
+ ---
2
+ /**
3
+ * Base - Verdant Theme document layout.
4
+ *
5
+ * Provides the shared page structure including:
6
+ * - HTML head with meta tags, favicon, and Google Analytics
7
+ * - Theme preloading script to prevent flash of unstyled content
8
+ * - Floating UI elements (theme toggle, back-to-top button, home return hint)
9
+ * - Page loading progress bar
10
+ * - Accessibility skip link and live announcer
11
+ * - View Transitions support via ClientRouter
12
+ *
13
+ * Receives public theme/page props. SEO is resolved by Core, while the document
14
+ * shell, palette controls, and optional client navigation remain theme-owned.
15
+ */
16
+ import { PALETTES } from '../lib/palettes';
17
+ import { ClientRouter } from 'astro:transitions';
18
+ import '../styles/fonts.css';
19
+ import '../styles/global.css';
20
+ import '../styles/themes/happyhues/index.css';
21
+
22
+ import SeoHead from '@mintfolio/theme-api/SeoHead.astro';
23
+ import type { PageProps } from '@mintfolio/theme-api/astro';
24
+ import type { VerdantSettings } from '../settings';
25
+ import faviconSvg from '../assets/favicon.svg?url';
26
+ import faviconIco from '../assets/favicon.ico?url';
27
+
28
+ /** Owns the Verdant Theme document only; other themes provide their own layout. */
29
+ interface Props extends PageProps<VerdantSettings> {}
30
+ const { theme, page } = Astro.props;
31
+ ---
32
+
33
+ <!doctype html>
34
+ <html lang={page.seo.language} data-hide-scrollbars="true">
35
+ <head>
36
+ <meta charset="utf-8" />
37
+ <link rel="icon" type="image/svg+xml" href={faviconSvg} />
38
+ <link rel="icon" href={faviconIco} />
39
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
40
+ <meta name="theme-color" id="browser-theme-color" content="#f6f8ef" transition:persist="browser-theme-color" />
41
+ <SeoHead seo={page.seo} />
42
+ <meta name="generator" content={Astro.generator} />
43
+ <ClientRouter />
44
+ <noscript>
45
+ <style is:inline>
46
+ .post-card.initially-hidden { display: block !important; }
47
+ .hero { position: relative !important; }
48
+ .content-wrapper { margin-top: 0 !important; }
49
+ #load-more-wrap, .scroll-indicator { display: none !important; }
50
+ @media (max-width: 768px) {
51
+ .theme-panel { right: 1rem; bottom: calc(1.5rem + 108px); }
52
+ }
53
+ </style>
54
+ </noscript>
55
+
56
+ <!-- 主题预加载脚本 - 避免闪烁 -->
57
+ <script is:inline define:vars={{ initialMode: theme.settings.initialMode, initialPalette: theme.settings.initialPalette }}>
58
+ (() => {
59
+ let mode = initialMode;
60
+ let palette = initialPalette;
61
+ try {
62
+ const savedMode = localStorage.getItem('theme-mode');
63
+ const savedPalette = localStorage.getItem('theme-palette');
64
+ if (['auto', 'light', 'dark'].includes(savedMode)) mode = savedMode;
65
+ if (/^[1-8]$/.test(savedPalette ?? '')) palette = savedPalette;
66
+ } catch { /* Storage is optional in privacy-restricted browsers. */ }
67
+ const resolved = mode === 'auto' ? (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : mode;
68
+ document.documentElement.dataset.theme = `${resolved}-${palette}`;
69
+ })();
70
+ </script>
71
+
72
+ {theme.settings.analyticsId && <>
73
+ <script is:inline async src={'https://www.googletagmanager.com/gtag/js?id=' + encodeURIComponent(theme.settings.analyticsId)}></script>
74
+ <script is:inline define:vars={{ analyticsId: theme.settings.analyticsId }}>
75
+ window.dataLayer = window.dataLayer || [];
76
+ function gtag(){dataLayer.push(arguments);}
77
+ gtag('js', new Date());
78
+ gtag('config', analyticsId);
79
+ </script>
80
+ </>}
81
+ </head>
82
+ <body data-hide-scrollbars="true" data-page-kind={page.kind} data-home-href={theme.urls.home()} data-archive-href={theme.urls.archive()} data-initial-mode={theme.settings.initialMode} data-initial-palette={theme.settings.initialPalette}>
83
+ <!-- Styles are loaded here; match browser chrome before painting page content. -->
84
+ <script is:inline>
85
+ (() => {
86
+ const meta = document.getElementById('browser-theme-color');
87
+ const background = getComputedStyle(document.documentElement).backgroundColor;
88
+ if (meta) meta.setAttribute('content', background);
89
+ })();
90
+ </script>
91
+ <div class="browser-bottom-background" aria-hidden="true"></div>
92
+
93
+ <!-- 页面加载进度条 -->
94
+ <div class="page-loading-progress" id="page-loading-progress" aria-hidden="true"></div>
95
+
96
+ <!-- 跳过链接 -->
97
+ <a href="#content" class="skip-link">跳转到主要内容</a>
98
+ <slot />
99
+
100
+ <!-- 无障碍状态通知区域 -->
101
+ <div id="a11y-announcer" class="sr-only" aria-live="polite" aria-atomic="true"></div>
102
+
103
+ <div class="ui-float-right" id="ui-float-right">
104
+ <button class="theme-toggle" id="theme-toggle" popovertarget="theme-panel" aria-label="切换主题" aria-expanded="false" aria-controls="theme-panel">
105
+ <svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18" aria-hidden="true">
106
+ <circle cx="12" cy="12" r="5"/>
107
+ <path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
108
+ </svg>
109
+ <svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18" aria-hidden="true">
110
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
111
+ </svg>
112
+ </button>
113
+ <div class="theme-panel" id="theme-panel" popover="auto" role="dialog" aria-label="主题设置" aria-hidden="true">
114
+ <div class="theme-section">
115
+ <span class="theme-label" id="theme-mode-label">模式</span>
116
+ <div class="theme-modes" role="radiogroup" aria-labelledby="theme-mode-label">
117
+ <button class="mode-btn" role="radio" aria-checked="true" data-mode="auto">自动</button>
118
+ <button class="mode-btn" role="radio" aria-checked="false" data-mode="light">浅色</button>
119
+ <button class="mode-btn" role="radio" aria-checked="false" data-mode="dark">深色</button>
120
+ </div>
121
+ </div>
122
+ <div class="theme-section">
123
+ <span class="theme-label" id="theme-palette-label">配色</span>
124
+ <div class="theme-palettes" id="theme-palettes" role="radiogroup" aria-labelledby="theme-palette-label">
125
+ {PALETTES.map((palette) => (
126
+ <button class="palette-btn" role="radio" aria-checked="false" aria-label={palette.name} data-palette={palette.id} title={palette.name}>
127
+ <span class="palette-preview" aria-hidden="true">
128
+ {palette.colors.map((color) => <span style={{ background: color }}></span>)}
129
+ </span>
130
+ <span class="palette-name">{palette.name}</span>
131
+ </button>
132
+ ))}
133
+ </div>
134
+ </div>
135
+ </div>
136
+ <button class="back-to-top" id="back-to-top" aria-label="回到顶部">
137
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20">
138
+ <path d="M12 19V5M5 12l7-7 7 7"/>
139
+ </svg>
140
+ </button>
141
+ </div>
142
+
143
+ <div class="home-return-hint-layer" id="home-return-hint-layer" aria-hidden="true">
144
+ <button
145
+ class="home-return-hint"
146
+ id="home-return-hint"
147
+ type="button"
148
+ aria-label="回到上面的欢迎区"
149
+ >
150
+ 想回到上面的欢迎区?点这里就行
151
+ </button>
152
+ </div>
153
+
154
+ <style>
155
+ /* Page Loading Progress Bar */
156
+ .page-loading-progress {
157
+ position: fixed;
158
+ top: 0;
159
+ left: 0;
160
+ width: 0%;
161
+ height: 2px;
162
+ background: linear-gradient(90deg, var(--color-primary), var(--color-secondary));
163
+ z-index: 9999;
164
+ transition: width 0.2s ease-out, opacity 0.3s ease;
165
+ box-shadow: 0 0 10px color-mix(in srgb, var(--color-primary) 50%, transparent);
166
+ pointer-events: none;
167
+ }
168
+
169
+ .page-loading-progress.loading {
170
+ width: 70%;
171
+ animation: loading-pulse 1.5s ease-in-out infinite;
172
+ }
173
+
174
+ .page-loading-progress.complete {
175
+ width: 100%;
176
+ opacity: 0;
177
+ }
178
+
179
+ @keyframes loading-pulse {
180
+ 0%, 100% { opacity: 1; }
181
+ 50% { opacity: 0.7; }
182
+ }
183
+
184
+ :root[data-hide-scrollbars="true"],
185
+ body[data-hide-scrollbars="true"],
186
+ body[data-hide-scrollbars="true"] * {
187
+ scrollbar-width: none;
188
+ -ms-overflow-style: none;
189
+ }
190
+
191
+ :root[data-hide-scrollbars="true"]::-webkit-scrollbar,
192
+ body[data-hide-scrollbars="true"]::-webkit-scrollbar,
193
+ body[data-hide-scrollbars="true"] *::-webkit-scrollbar {
194
+ width: 0;
195
+ height: 0;
196
+ display: none;
197
+ }
198
+
199
+ .ui-float-right {
200
+ position: fixed;
201
+ bottom: 2rem;
202
+ right: 2rem;
203
+ z-index: 200;
204
+ display: flex;
205
+ flex-direction: column;
206
+ align-items: center;
207
+ gap: 0.75rem;
208
+ opacity: 0;
209
+ transform: translateX(20px);
210
+ transition: transform var(--transition-base), opacity var(--transition-base);
211
+ pointer-events: none;
212
+ }
213
+
214
+ .ui-float-right.visible {
215
+ opacity: 1;
216
+ transform: translateX(0);
217
+ pointer-events: auto;
218
+ }
219
+
220
+ .theme-toggle, .back-to-top {
221
+ width: 44px;
222
+ height: 44px;
223
+ border-radius: 50%;
224
+ background: var(--color-surface);
225
+ border: 1px solid var(--color-border);
226
+ color: var(--color-text);
227
+ cursor: pointer;
228
+ display: flex;
229
+ align-items: center;
230
+ justify-content: center;
231
+ box-shadow: var(--shadow-md);
232
+ transition: all var(--transition-base);
233
+ }
234
+
235
+ .theme-toggle:hover, .back-to-top:hover {
236
+ transform: scale(1.05);
237
+ box-shadow: var(--shadow-lg);
238
+ }
239
+
240
+ .back-to-top {
241
+ background: var(--color-secondary);
242
+ color: var(--color-bg);
243
+ border: none;
244
+ }
245
+
246
+ .home-return-hint-layer {
247
+ position: fixed;
248
+ right: calc(2rem + 44px + 0.85rem);
249
+ bottom: 2rem;
250
+ z-index: 199;
251
+ pointer-events: none;
252
+ }
253
+
254
+ .home-return-hint {
255
+ max-width: min(28rem, calc(100vw - 8rem));
256
+ opacity: 0;
257
+ transform: translate3d(18px, 0, 0) scale(0.96);
258
+ padding: 0.78rem 1rem;
259
+ border: 1px solid color-mix(in srgb, var(--color-secondary) 30%, var(--color-border));
260
+ border-radius: 999px;
261
+ background: color-mix(in srgb, var(--color-surface) 92%, white 8%);
262
+ color: var(--color-text);
263
+ box-shadow: var(--shadow-md);
264
+ font-size: 0.84rem;
265
+ line-height: 1.35;
266
+ white-space: nowrap;
267
+ overflow: hidden;
268
+ cursor: pointer;
269
+ transition:
270
+ opacity 0.26s ease,
271
+ transform 0.32s cubic-bezier(0.16, 1, 0.3, 1),
272
+ filter 0.26s ease,
273
+ box-shadow var(--transition-fast),
274
+ border-color var(--transition-fast);
275
+ filter: blur(2px);
276
+ }
277
+
278
+ .home-return-hint-layer.visible {
279
+ pointer-events: auto;
280
+ }
281
+
282
+ .home-return-hint-layer.visible .home-return-hint {
283
+ opacity: 1;
284
+ transform: translate3d(0, 0, 0) scale(1);
285
+ filter: blur(0);
286
+ }
287
+
288
+ .home-return-hint-layer.hiding .home-return-hint {
289
+ opacity: 0;
290
+ transform: translate3d(12px, 0, 0) scale(0.985);
291
+ filter: blur(1px);
292
+ }
293
+
294
+ .home-return-hint:hover {
295
+ box-shadow: var(--shadow-lg);
296
+ border-color: color-mix(in srgb, var(--color-secondary) 55%, var(--color-border));
297
+ }
298
+
299
+ .icon-moon { display: none; }
300
+ [data-theme^="dark"] .icon-sun { display: none; }
301
+ [data-theme^="dark"] .icon-moon { display: block; }
302
+
303
+ @media (prefers-color-scheme: dark) {
304
+ :root:not([data-theme]) .icon-sun { display: none; }
305
+ :root:not([data-theme]) .icon-moon { display: block; }
306
+ }
307
+
308
+ .theme-panel {
309
+ visibility: hidden;
310
+ position: fixed;
311
+ inset: auto;
312
+ margin: 0;
313
+ bottom: calc(2rem + 108px);
314
+ right: 2rem;
315
+ color: var(--color-text);
316
+ background: var(--color-surface);
317
+ border: 1px solid var(--color-border);
318
+ border-radius: var(--radius-lg);
319
+ padding: 1rem;
320
+ width: 300px;
321
+ max-width: calc(100vw - 2rem);
322
+ max-height: calc(100dvh - 10rem);
323
+ overflow-y: auto;
324
+ overscroll-behavior: contain;
325
+ box-shadow: var(--shadow-lg);
326
+ opacity: 0;
327
+ transform: translateY(8px);
328
+ pointer-events: none;
329
+ transition: opacity var(--transition-fast), transform var(--transition-fast);
330
+ }
331
+
332
+ .theme-panel:popover-open {
333
+ visibility: visible;
334
+ opacity: 1;
335
+ transform: translateY(0);
336
+ pointer-events: auto;
337
+ }
338
+
339
+ @starting-style {
340
+ .theme-panel:popover-open {
341
+ opacity: 0;
342
+ transform: translateY(8px);
343
+ }
344
+ }
345
+
346
+ .theme-section {
347
+ margin-bottom: 0.75rem;
348
+ }
349
+
350
+ .theme-section:last-child {
351
+ margin-bottom: 0;
352
+ }
353
+
354
+ .theme-label {
355
+ display: block;
356
+ font-size: 0.7rem;
357
+ color: var(--color-text-muted);
358
+ text-transform: uppercase;
359
+ letter-spacing: 0.05em;
360
+ margin-bottom: 0.5rem;
361
+ }
362
+
363
+ .theme-modes {
364
+ display: flex;
365
+ gap: 0.25rem;
366
+ }
367
+
368
+ .mode-btn {
369
+ flex: 1;
370
+ padding: 0.375rem 0.5rem;
371
+ font-size: 0.75rem;
372
+ border: 1px solid var(--color-border);
373
+ border-radius: var(--radius-sm);
374
+ background: transparent;
375
+ color: var(--color-text-muted);
376
+ cursor: pointer;
377
+ transition: all var(--transition-fast);
378
+ font-family: inherit;
379
+ }
380
+
381
+ .mode-btn:hover {
382
+ border-color: var(--color-secondary);
383
+ color: var(--color-text);
384
+ }
385
+
386
+ .mode-btn.active,
387
+ .mode-btn[aria-checked="true"] {
388
+ background: var(--color-secondary);
389
+ color: var(--color-bg);
390
+ border-color: var(--color-secondary);
391
+ }
392
+
393
+ .theme-palettes {
394
+ display: grid;
395
+ grid-template-columns: repeat(2, minmax(0, 1fr));
396
+ gap: 0.5rem;
397
+ }
398
+
399
+ .palette-btn {
400
+ display: flex;
401
+ flex-direction: column;
402
+ gap: 0.4rem;
403
+ align-items: stretch;
404
+ min-height: 66px;
405
+ border-radius: var(--radius-sm);
406
+ border: 2px solid transparent;
407
+ background: var(--color-bg);
408
+ color: var(--color-text);
409
+ cursor: pointer;
410
+ padding: 7px;
411
+ transition: all var(--transition-fast);
412
+ }
413
+
414
+ .palette-btn:hover {
415
+ border-color: var(--color-border);
416
+ }
417
+
418
+ .palette-btn.active {
419
+ border-color: var(--color-primary);
420
+ background: var(--color-tag-bg);
421
+ }
422
+
423
+ .palette-preview {
424
+ display: flex;
425
+ width: 100%;
426
+ height: 22px;
427
+ border-radius: 5px;
428
+ overflow: hidden;
429
+ }
430
+
431
+ .palette-preview span { flex: 1; }
432
+ .palette-preview span:first-child { flex: 2; }
433
+ .palette-name { font-size: 0.7rem; line-height: 1.4; }
434
+
435
+ @media (max-width: 600px) {
436
+ .ui-float-right {
437
+ bottom: 1.5rem;
438
+ right: 1rem;
439
+ }
440
+
441
+ .home-return-hint-layer {
442
+ right: calc(1rem + 44px + 0.7rem);
443
+ bottom: 1.5rem;
444
+ }
445
+
446
+ .home-return-hint {
447
+ max-width: min(70vw, 220px);
448
+ white-space: normal;
449
+ }
450
+ }
451
+ @media (max-width: 768px) {
452
+ .theme-panel { right: 1rem; bottom: calc(1.5rem + 108px); }
453
+ }
454
+ </style>
455
+
456
+ <script>
457
+ import '../scripts/site';
458
+ </script>
459
+ </body>
460
+ </html>
@@ -0,0 +1,10 @@
1
+ export const PALETTES = [
2
+ { id: '1', name: '青柠薄荷', colors: ['#08765f', '#ff795f', '#e1ee79'] },
3
+ { id: '2', name: '橘子汽水', colors: ['#b7441c', '#526ce0', '#ffc65c'] },
4
+ { id: '3', name: '晴空海盐', colors: ['#3454cd', '#ff9163', '#cddd76'] },
5
+ { id: '4', name: '葡萄跳跳糖', colors: ['#7441bc', '#bbd94f', '#ffadca'] },
6
+ { id: '5', name: '莓果奶昔', colors: ['#ba375e', '#73cec2', '#f5c75c'] },
7
+ { id: '6', name: '向日葵', colors: ['#91600c', '#4364cc', '#f2c94c'] },
8
+ { id: '7', name: '海岛假日', colors: ['#087387', '#f48652', '#aadad0'] },
9
+ { id: '8', name: '午夜游乐场', colors: ['#394860', '#ffbb3b', '#a4b8ec'] },
10
+ ] as const;