@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
package/scripts/toc.ts ADDED
@@ -0,0 +1,149 @@
1
+ import { createTocController, type PageScope } from '@mintfolio/core/client';
2
+
3
+ export function initTocDrawer(scope: PageScope): void {
4
+ const toggle = document.getElementById('toc-toggle');
5
+ const overlay = document.getElementById('toc-overlay');
6
+ const tocCard = document.getElementById('toc-card');
7
+ const progressBar = tocCard?.querySelector<HTMLElement>('.toc-progress');
8
+ const tocLinks = Array.from(document.querySelectorAll<HTMLAnchorElement>('.toc-link'));
9
+ const floatRight = document.getElementById('ui-float-right');
10
+
11
+ if (!toggle || !overlay || !tocCard || tocLinks.length === 0 || !floatRight) {
12
+ return;
13
+ }
14
+
15
+ // 将 TOC 按钮移动到 ui-float-right 容器中
16
+ if (!floatRight.contains(toggle)) {
17
+ toggle.style.display = '';
18
+ floatRight.insertBefore(toggle, floatRight.firstChild);
19
+ }
20
+
21
+ const addDisposer = scope.add;
22
+
23
+ // Check if desktop mode
24
+ const isDesktop = () => window.innerWidth >= 1320;
25
+
26
+ // State: desktop default open, mobile/tablet default closed
27
+ let isOpen = isDesktop();
28
+
29
+ // Desktop: toggle sidebar visibility
30
+ const toggleDesktop = () => {
31
+ isOpen = !isOpen;
32
+ tocCard.classList.toggle('hidden', !isOpen);
33
+ toggle.classList.toggle('active', isOpen);
34
+ toggle.setAttribute('aria-expanded', String(isOpen));
35
+ };
36
+
37
+ // Mobile/Tablet: open drawer
38
+ const openDrawer = () => {
39
+ isOpen = true;
40
+ tocCard.classList.add('open');
41
+ overlay.classList.add('open');
42
+ toggle.classList.add('active');
43
+ toggle.setAttribute('aria-expanded', 'true');
44
+ document.body.style.overflow = 'hidden';
45
+ };
46
+
47
+ // Mobile/Tablet: close drawer
48
+ const closeDrawer = () => {
49
+ isOpen = false;
50
+ tocCard.classList.remove('open');
51
+ overlay.classList.remove('open');
52
+ toggle.classList.remove('active');
53
+ toggle.setAttribute('aria-expanded', 'false');
54
+ document.body.style.overflow = '';
55
+ };
56
+
57
+ // Toggle handler
58
+ const handleToggle = () => {
59
+ if (isDesktop()) {
60
+ toggleDesktop();
61
+ } else {
62
+ if (isOpen) {
63
+ closeDrawer();
64
+ } else {
65
+ openDrawer();
66
+ }
67
+ }
68
+ };
69
+
70
+ // Initialize state based on screen size
71
+ const initializeState = () => {
72
+ if (isDesktop()) {
73
+ // Desktop: default open
74
+ isOpen = true;
75
+ tocCard.classList.remove('hidden');
76
+ tocCard.classList.remove('open');
77
+ overlay.classList.remove('open');
78
+ toggle.classList.add('active');
79
+ toggle.setAttribute('aria-expanded', 'true');
80
+ document.body.style.overflow = '';
81
+ } else {
82
+ // Mobile/Tablet: default closed (使用 CSS 默认的隐藏状态)
83
+ isOpen = false;
84
+ tocCard.classList.remove('open');
85
+ tocCard.classList.remove('hidden');
86
+ overlay.classList.remove('open');
87
+ toggle.classList.remove('active');
88
+ toggle.setAttribute('aria-expanded', 'false');
89
+ document.body.style.overflow = '';
90
+ }
91
+ };
92
+
93
+ // Set initial state
94
+ initializeState();
95
+
96
+ // Event listeners
97
+ toggle.addEventListener('click', handleToggle);
98
+ addDisposer(() => toggle.removeEventListener('click', handleToggle));
99
+
100
+ overlay.addEventListener('click', closeDrawer);
101
+ addDisposer(() => overlay.removeEventListener('click', closeDrawer));
102
+
103
+ const headings = tocLinks.map((link) => {
104
+ try { return document.getElementById(decodeURIComponent(new URL(link.href).hash.slice(1))); }
105
+ catch { return null; }
106
+ }).filter((heading): heading is HTMLElement => heading !== null);
107
+ const toc = createTocController({
108
+ headings, links: tocLinks, activeClass: 'active', scrollActiveLink: true,
109
+ offset: () => window.innerWidth <= 768 ? 100 : 120,
110
+ onNavigate: () => { if (!isDesktop()) closeDrawer(); },
111
+ onProgress: (value) => {
112
+ tocCard.style.setProperty('--reading-progress', value + '%');
113
+ progressBar?.setAttribute('aria-valuenow', String(Math.round(value)));
114
+ },
115
+ signal: scope.signal,
116
+ });
117
+
118
+ // Escape key handler
119
+ const onEscClose = (e: KeyboardEvent) => {
120
+ if (e.key === 'Escape' && isOpen && !isDesktop()) {
121
+ closeDrawer();
122
+ toggle.focus();
123
+ }
124
+ };
125
+ document.addEventListener('keydown', onEscClose);
126
+ addDisposer(() => document.removeEventListener('keydown', onEscClose));
127
+
128
+ // Resize handler - reinitialize on breakpoint change
129
+ let wasDesktop = isDesktop();
130
+ const onResize = () => {
131
+ const nowDesktop = isDesktop();
132
+ if (wasDesktop !== nowDesktop) {
133
+ initializeState();
134
+ wasDesktop = nowDesktop;
135
+ }
136
+ toc.refresh();
137
+ };
138
+ window.addEventListener('resize', onResize);
139
+ addDisposer(() => window.removeEventListener('resize', onResize));
140
+
141
+ // Initial updates
142
+ scope.frame(() => toc.refresh());
143
+
144
+ addDisposer(() => {
145
+ document.body.style.overflow = '';
146
+ // Relocking can remove the article without a page swap, including this relocated control.
147
+ toggle.remove();
148
+ });
149
+ }
package/settings.ts ADDED
@@ -0,0 +1,21 @@
1
+ import type { PublicImage } from '@mintfolio/theme-api';
2
+
3
+ /** Verdant Theme settings, fully populated and validated by its own manifest. */
4
+ export interface VerdantSettings extends Record<string, unknown> {
5
+ /** Visitor choices in localStorage take precedence over these initial appearance values. */
6
+ initialMode: 'auto' | 'light' | 'dark';
7
+ initialPalette: string;
8
+ /** Number of cards initially visible before the visitor loads more. */
9
+ homePageSize: number;
10
+ archivePageSize: number;
11
+ analyticsId: string;
12
+ /** Optional sidebar presentation; Core contains no Verdant-specific configuration. */
13
+ sidebar: {
14
+ sections: { contact: boolean; activity: boolean; tools: boolean };
15
+ tools: Array<{ name: string; description: string; url: string }>;
16
+ quote: { enabled: boolean; text: string; author: string };
17
+ };
18
+ /** Explicitly curated post identifiers and display images; Core resolves their public posts. */
19
+ home: { hotContent: { enabled: boolean; items: Array<{ postId: string; image: string | PublicImage }> } };
20
+ article: { footerImage: { enabled: boolean; src: string | PublicImage } };
21
+ }
@@ -0,0 +1,120 @@
1
+ @font-face {
2
+ font-family: 'Inter';
3
+ font-style: normal;
4
+ font-weight: 300;
5
+ font-display: swap;
6
+ src:
7
+ local('Inter Light'),
8
+ local('Inter-Light'),
9
+ url('../assets/fonts/Inter-300.woff2') format('woff2');
10
+ }
11
+
12
+ @font-face {
13
+ font-family: 'Inter';
14
+ font-style: normal;
15
+ font-weight: 400;
16
+ font-display: swap;
17
+ src:
18
+ local('Inter Regular'),
19
+ local('Inter-Regular'),
20
+ url('../assets/fonts/Inter-400.woff2') format('woff2');
21
+ }
22
+
23
+ @font-face {
24
+ font-family: 'Inter';
25
+ font-style: normal;
26
+ font-weight: 500;
27
+ font-display: swap;
28
+ src:
29
+ local('Inter Medium'),
30
+ local('Inter-Medium'),
31
+ url('../assets/fonts/Inter-500.woff2') format('woff2');
32
+ }
33
+
34
+ @font-face {
35
+ font-family: 'Inter';
36
+ font-style: normal;
37
+ font-weight: 600;
38
+ font-display: swap;
39
+ src:
40
+ local('Inter SemiBold'),
41
+ local('Inter-SemiBold'),
42
+ url('../assets/fonts/Inter-600.woff2') format('woff2');
43
+ }
44
+
45
+ @font-face {
46
+ font-family: 'Inter';
47
+ font-style: normal;
48
+ font-weight: 700;
49
+ font-display: swap;
50
+ src:
51
+ local('Inter Bold'),
52
+ local('Inter-Bold'),
53
+ url('../assets/fonts/Inter-700.woff2') format('woff2');
54
+ }
55
+
56
+ @font-face {
57
+ font-family: 'JetBrains Mono';
58
+ font-style: normal;
59
+ font-weight: 400;
60
+ font-display: swap;
61
+ src:
62
+ local('JetBrains Mono'),
63
+ local('JetBrainsMono-Regular'),
64
+ url('../assets/fonts/JetBrainsMono-400.woff2') format('woff2');
65
+ }
66
+
67
+ @font-face {
68
+ font-family: 'JetBrains Mono';
69
+ font-style: normal;
70
+ font-weight: 500;
71
+ font-display: swap;
72
+ src:
73
+ local('JetBrains Mono Medium'),
74
+ local('JetBrainsMono-Medium'),
75
+ url('../assets/fonts/JetBrainsMono-500.woff2') format('woff2');
76
+ }
77
+
78
+ @font-face {
79
+ font-family: 'Playfair Display';
80
+ font-style: normal;
81
+ font-weight: 400;
82
+ font-display: swap;
83
+ src:
84
+ local('Playfair Display Regular'),
85
+ local('PlayfairDisplay-Regular'),
86
+ url('../assets/fonts/PlayfairDisplay-400.woff2') format('woff2');
87
+ }
88
+
89
+ @font-face {
90
+ font-family: 'Playfair Display';
91
+ font-style: normal;
92
+ font-weight: 500;
93
+ font-display: swap;
94
+ src:
95
+ local('Playfair Display Medium'),
96
+ local('PlayfairDisplay-Medium'),
97
+ url('../assets/fonts/PlayfairDisplay-500.woff2') format('woff2');
98
+ }
99
+
100
+ @font-face {
101
+ font-family: 'Playfair Display';
102
+ font-style: normal;
103
+ font-weight: 600;
104
+ font-display: swap;
105
+ src:
106
+ local('Playfair Display SemiBold'),
107
+ local('PlayfairDisplay-SemiBold'),
108
+ url('../assets/fonts/PlayfairDisplay-600.woff2') format('woff2');
109
+ }
110
+
111
+ @font-face {
112
+ font-family: 'Playfair Display';
113
+ font-style: normal;
114
+ font-weight: 700;
115
+ font-display: swap;
116
+ src:
117
+ local('Playfair Display Bold'),
118
+ local('PlayfairDisplay-Bold'),
119
+ url('../assets/fonts/PlayfairDisplay-700.woff2') format('woff2');
120
+ }
@@ -0,0 +1,146 @@
1
+ @import "tailwindcss";
2
+
3
+ body:has(dialog:modal) { overflow: hidden; }
4
+
5
+ html[data-restoring-scroll='true'] { scroll-behavior: auto; }
6
+
7
+ @media (prefers-reduced-motion: reduce) {
8
+ html:root { scroll-behavior: auto; }
9
+ }
10
+
11
+ *,
12
+ *::before,
13
+ *::after {
14
+ box-sizing: border-box;
15
+ }
16
+
17
+ html {
18
+ --browser-safe-area-max-bottom: env(safe-area-max-inset-bottom, 36px);
19
+ background-color: var(--color-bg);
20
+ scroll-behavior: smooth;
21
+ -webkit-font-smoothing: antialiased;
22
+ -moz-osx-font-smoothing: grayscale;
23
+ scrollbar-width: none;
24
+ -ms-overflow-style: none;
25
+ }
26
+
27
+ html::-webkit-scrollbar,
28
+ body::-webkit-scrollbar,
29
+ *::-webkit-scrollbar {
30
+ width: 0;
31
+ height: 0;
32
+ display: none;
33
+ }
34
+
35
+ body,
36
+ * {
37
+ scrollbar-width: none;
38
+ -ms-overflow-style: none;
39
+ }
40
+
41
+ body {
42
+ font-family: var(--font-family);
43
+ background-color: var(--color-bg);
44
+ color: var(--color-text);
45
+ line-height: 1.7;
46
+ overflow-x: hidden;
47
+ }
48
+
49
+ ::selection {
50
+ background: var(--color-primary);
51
+ color: var(--color-on-primary);
52
+ }
53
+
54
+ img {
55
+ max-width: 100%;
56
+ height: auto;
57
+ display: block;
58
+ }
59
+
60
+ a {
61
+ color: inherit;
62
+ text-decoration: none;
63
+ }
64
+
65
+ button {
66
+ font-family: inherit;
67
+ cursor: pointer;
68
+ }
69
+
70
+ /* Paint a stable background behind Android's gesture bar as Chrome's chin retracts.
71
+ Keep its height constant; only bottom follows the changing safe-area inset. */
72
+ .browser-bottom-background,
73
+ dialog:modal::after {
74
+ position: fixed;
75
+ left: 0;
76
+ right: 0;
77
+ bottom: calc(env(safe-area-inset-bottom, 0px) - var(--browser-safe-area-max-bottom));
78
+ height: var(--browser-safe-area-max-bottom);
79
+ background-color: var(--color-bg);
80
+ pointer-events: none;
81
+ z-index: 10000;
82
+ }
83
+
84
+ /* Modal dialogs paint above the document, so they need the same backing. */
85
+ dialog:modal::after {
86
+ content: '';
87
+ }
88
+
89
+ input,
90
+ textarea {
91
+ font-family: inherit;
92
+ }
93
+
94
+ @media (prefers-reduced-motion: reduce) {
95
+ *,
96
+ *::before,
97
+ *::after {
98
+ animation-duration: 0.01ms !important;
99
+ animation-iteration-count: 1 !important;
100
+ transition-duration: 0.01ms !important;
101
+ scroll-behavior: auto !important;
102
+ }
103
+ }
104
+
105
+ /* ===== 焦点可见样式 ===== */
106
+ :focus-visible {
107
+ outline: 3px solid var(--color-primary);
108
+ outline-offset: 2px;
109
+ }
110
+
111
+ :focus:not(:focus-visible) {
112
+ outline: none;
113
+ }
114
+
115
+ /* ===== 跳过链接 ===== */
116
+ .skip-link {
117
+ position: absolute;
118
+ top: -100%;
119
+ left: 50%;
120
+ transform: translateX(-50%);
121
+ z-index: 9999;
122
+ padding: 0.75rem 1.5rem;
123
+ background: var(--color-secondary);
124
+ color: var(--color-bg);
125
+ font-weight: 600;
126
+ border-radius: var(--radius-md);
127
+ text-decoration: none;
128
+ transition: top 0.2s ease;
129
+ }
130
+
131
+ .skip-link:focus {
132
+ top: 1rem;
133
+ }
134
+
135
+ /* ===== 屏幕阅读器辅助类 ===== */
136
+ .sr-only {
137
+ position: absolute;
138
+ width: 1px;
139
+ height: 1px;
140
+ padding: 0;
141
+ margin: -1px;
142
+ overflow: hidden;
143
+ clip: rect(0, 0, 0, 0);
144
+ white-space: nowrap;
145
+ border: 0;
146
+ }
@@ -0,0 +1,62 @@
1
+ @layer components {
2
+ .card {
3
+ background: var(--color-surface);
4
+ border: 1px solid var(--color-border);
5
+ border-radius: var(--radius-md);
6
+ box-shadow: var(--shadow-sm);
7
+ transition: all var(--transition-base);
8
+ }
9
+
10
+ .card:hover {
11
+ box-shadow: var(--shadow-md);
12
+ transform: translateY(-2px);
13
+ }
14
+
15
+ .tag {
16
+ display: inline-block;
17
+ background: var(--color-tag-bg);
18
+ color: var(--color-tag-text);
19
+ padding: 0.25rem 0.75rem;
20
+ border-radius: var(--radius-full);
21
+ font-size: 0.75rem;
22
+ font-weight: 500;
23
+ }
24
+
25
+ .section-title {
26
+ color: var(--color-text-headline);
27
+ font-size: 2rem;
28
+ font-weight: 700;
29
+ letter-spacing: -0.02em;
30
+ }
31
+
32
+ .search-input {
33
+ width: 100%;
34
+ padding: 0.75rem 1rem;
35
+ padding-left: 2.5rem;
36
+ border: 1px solid var(--color-border);
37
+ border-radius: var(--radius-md);
38
+ background: var(--color-surface);
39
+ color: var(--color-text);
40
+ font-size: 0.95rem;
41
+ transition: all var(--transition-fast);
42
+ }
43
+
44
+ .search-input:focus {
45
+ outline: none;
46
+ border-color: var(--color-primary);
47
+ box-shadow: 0 0 0 3px rgba(255, 216, 3, 0.2);
48
+ }
49
+
50
+ .search-input::placeholder {
51
+ color: var(--color-text-muted);
52
+ }
53
+
54
+ .glass-card {
55
+ background: color-mix(in srgb, var(--color-surface) 78%, transparent);
56
+ border: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent);
57
+ backdrop-filter: blur(12px) saturate(120%);
58
+ -webkit-backdrop-filter: blur(12px) saturate(120%);
59
+ box-shadow: var(--shadow-sm);
60
+ transition: all var(--transition-base);
61
+ }
62
+ }
@@ -0,0 +1,2 @@
1
+ @import './theme.css';
2
+ @import './effects.css';