@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.
- package/LICENSE +674 -0
- package/README.md +66 -0
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/assets/favicon.ico +0 -0
- package/assets/favicon.svg +9 -0
- package/assets/fonts/Inter-300.woff2 +0 -0
- package/assets/fonts/Inter-400.woff2 +0 -0
- package/assets/fonts/Inter-500.woff2 +0 -0
- package/assets/fonts/Inter-600.woff2 +0 -0
- package/assets/fonts/Inter-700.woff2 +0 -0
- package/assets/fonts/JetBrainsMono-400.woff2 +0 -0
- package/assets/fonts/JetBrainsMono-500.woff2 +0 -0
- package/assets/fonts/PlayfairDisplay-400.woff2 +0 -0
- package/assets/fonts/PlayfairDisplay-500.woff2 +0 -0
- package/assets/fonts/PlayfairDisplay-600.woff2 +0 -0
- package/assets/fonts/PlayfairDisplay-700.woff2 +0 -0
- package/components/base/Contact.astro +173 -0
- package/components/base/Hero.astro +228 -0
- package/components/base/Navigation.astro +288 -0
- package/components/base/Projects.astro +207 -0
- package/components/base/Skills.astro +158 -0
- package/components/blog/ArticleProse.astro +217 -0
- package/components/blog/CodeBlockEnhancer.astro +310 -0
- package/components/blog/PostCard.astro +98 -0
- package/components/blog/ProtectedArticle.astro +89 -0
- package/config/theme-verdant.config.mjs +74 -0
- package/layouts/Base.astro +460 -0
- package/lib/palettes.ts +10 -0
- package/licenses/Inter-OFL.txt +92 -0
- package/licenses/JetBrainsMono-OFL.txt +93 -0
- package/licenses/PlayfairDisplay-OFL.txt +191 -0
- package/package.json +68 -0
- package/pages/archive.astro +384 -0
- package/pages/home.astro +2049 -0
- package/pages/page.astro +434 -0
- package/pages/post.astro +1029 -0
- package/scripts/article.ts +42 -0
- package/scripts/articleOrigin.ts +31 -0
- package/scripts/articleTables.ts +15 -0
- package/scripts/blog.ts +26 -0
- package/scripts/codeBlocks.ts +172 -0
- package/scripts/hero.ts +173 -0
- package/scripts/home.ts +12 -0
- package/scripts/homePanels.ts +100 -0
- package/scripts/hotContent.ts +16 -0
- package/scripts/lifecycle.ts +3 -0
- package/scripts/lightbox.ts +14 -0
- package/scripts/postFilters.ts +101 -0
- package/scripts/postList.ts +132 -0
- package/scripts/progress.ts +29 -0
- package/scripts/protectedArticle.ts +49 -0
- package/scripts/protectedToc.ts +29 -0
- package/scripts/site.ts +145 -0
- package/scripts/tagFold.ts +61 -0
- package/scripts/theme.ts +102 -0
- package/scripts/toc.ts +149 -0
- package/settings.ts +21 -0
- package/styles/fonts.css +120 -0
- package/styles/global.css +146 -0
- package/styles/themes/happyhues/effects.css +62 -0
- package/styles/themes/happyhues/index.css +2 -0
- package/styles/themes/happyhues/theme.css +225 -0
- package/theme.mjs +60 -0
- package/utils/blogNavigation.ts +3 -0
- package/utils/homeReturnHint.ts +34 -0
- package/utils/storage.ts +16 -0
- package/utils/types.ts +8 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Navigation - Fixed top navigation bar with mobile hamburger menu.
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Desktop: Horizontal link list with hover underline animation
|
|
7
|
+
* - Mobile: Hamburger toggle with slide-down menu
|
|
8
|
+
* - Smooth scroll navigation to page sections
|
|
9
|
+
* - Keyboard accessible (Escape closes mobile menu)
|
|
10
|
+
*/
|
|
11
|
+
import type { ThemeContext } from '@mintfolio/theme-api';
|
|
12
|
+
import type { VerdantSettings } from '../../settings';
|
|
13
|
+
|
|
14
|
+
/** Public site data supplied by the containing theme page. */
|
|
15
|
+
interface Props { theme: ThemeContext<VerdantSettings>; }
|
|
16
|
+
const { theme } = Astro.props;
|
|
17
|
+
|
|
18
|
+
const navLinks = [
|
|
19
|
+
{ href: '#hero', label: '首页' },
|
|
20
|
+
{ href: '#skills', label: '技能' },
|
|
21
|
+
{ href: '#projects', label: '项目' },
|
|
22
|
+
{ href: '#blog', label: '博客' },
|
|
23
|
+
{ href: '#contact', label: '联系' },
|
|
24
|
+
];
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
<nav class="nav-container glass" role="navigation" aria-label="主导航">
|
|
28
|
+
<div class="nav-inner">
|
|
29
|
+
<!-- Logo -->
|
|
30
|
+
<a href="#hero" class="nav-logo">
|
|
31
|
+
{theme.site.profile.name}
|
|
32
|
+
</a>
|
|
33
|
+
|
|
34
|
+
<!-- Desktop Navigation -->
|
|
35
|
+
<ul class="nav-links">
|
|
36
|
+
{navLinks.map((link) => (
|
|
37
|
+
<li>
|
|
38
|
+
<a href={link.href} class="nav-link">{link.label}</a>
|
|
39
|
+
</li>
|
|
40
|
+
))}
|
|
41
|
+
</ul>
|
|
42
|
+
|
|
43
|
+
<!-- Mobile Menu Toggle -->
|
|
44
|
+
<button
|
|
45
|
+
class="nav-toggle"
|
|
46
|
+
id="mobile-menu-toggle"
|
|
47
|
+
aria-label="切换菜单"
|
|
48
|
+
aria-expanded="false"
|
|
49
|
+
aria-controls="mobile-menu"
|
|
50
|
+
>
|
|
51
|
+
<span class="hamburger-line" aria-hidden="true"></span>
|
|
52
|
+
<span class="hamburger-line" aria-hidden="true"></span>
|
|
53
|
+
<span class="hamburger-line" aria-hidden="true"></span>
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<!-- Mobile Menu -->
|
|
58
|
+
<div id="mobile-menu" class="mobile-menu" aria-hidden="true" aria-labelledby="mobile-menu-toggle">
|
|
59
|
+
<ul class="mobile-nav-links">
|
|
60
|
+
{navLinks.map((link) => (
|
|
61
|
+
<li>
|
|
62
|
+
<a href={link.href} class="mobile-nav-link">{link.label}</a>
|
|
63
|
+
</li>
|
|
64
|
+
))}
|
|
65
|
+
</ul>
|
|
66
|
+
</div>
|
|
67
|
+
</nav>
|
|
68
|
+
|
|
69
|
+
<style>
|
|
70
|
+
.nav-container {
|
|
71
|
+
position: fixed;
|
|
72
|
+
top: 0;
|
|
73
|
+
left: 0;
|
|
74
|
+
right: 0;
|
|
75
|
+
z-index: 1000;
|
|
76
|
+
border-radius: 0;
|
|
77
|
+
border-top: none;
|
|
78
|
+
border-left: none;
|
|
79
|
+
border-right: none;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.nav-inner {
|
|
83
|
+
max-width: var(--max-width);
|
|
84
|
+
margin: 0 auto;
|
|
85
|
+
padding: 1rem 2rem;
|
|
86
|
+
display: flex;
|
|
87
|
+
align-items: center;
|
|
88
|
+
justify-content: space-between;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.nav-logo {
|
|
92
|
+
font-size: 1.25rem;
|
|
93
|
+
font-weight: 600;
|
|
94
|
+
color: var(--color-text);
|
|
95
|
+
text-decoration: none;
|
|
96
|
+
transition: color 0.2s ease;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.nav-logo:hover {
|
|
100
|
+
color: var(--color-primary);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.nav-links {
|
|
104
|
+
display: flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
gap: 2rem;
|
|
107
|
+
list-style: none;
|
|
108
|
+
margin: 0;
|
|
109
|
+
padding: 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.nav-link {
|
|
113
|
+
color: var(--color-text-muted);
|
|
114
|
+
text-decoration: none;
|
|
115
|
+
font-size: 0.9375rem;
|
|
116
|
+
font-weight: 500;
|
|
117
|
+
transition: color 0.2s ease;
|
|
118
|
+
position: relative;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.nav-link:hover {
|
|
122
|
+
color: var(--color-text);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.nav-link::after {
|
|
126
|
+
content: '';
|
|
127
|
+
position: absolute;
|
|
128
|
+
bottom: -4px;
|
|
129
|
+
left: 0;
|
|
130
|
+
width: 0;
|
|
131
|
+
height: 2px;
|
|
132
|
+
background: var(--gradient-primary);
|
|
133
|
+
transition: width 0.2s ease;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.nav-link:hover::after {
|
|
137
|
+
width: 100%;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.nav-toggle {
|
|
141
|
+
display: none;
|
|
142
|
+
flex-direction: column;
|
|
143
|
+
justify-content: center;
|
|
144
|
+
gap: 5px;
|
|
145
|
+
width: 32px;
|
|
146
|
+
height: 32px;
|
|
147
|
+
background: none;
|
|
148
|
+
border: none;
|
|
149
|
+
cursor: pointer;
|
|
150
|
+
padding: 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.hamburger-line {
|
|
154
|
+
display: block;
|
|
155
|
+
width: 100%;
|
|
156
|
+
height: 2px;
|
|
157
|
+
background-color: var(--color-text);
|
|
158
|
+
transition: transform 0.3s ease, opacity 0.3s ease;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.nav-toggle[aria-expanded="true"] .hamburger-line:nth-child(1) {
|
|
162
|
+
transform: translateY(7px) rotate(45deg);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.nav-toggle[aria-expanded="true"] .hamburger-line:nth-child(2) {
|
|
166
|
+
opacity: 0;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.nav-toggle[aria-expanded="true"] .hamburger-line:nth-child(3) {
|
|
170
|
+
transform: translateY(-7px) rotate(-45deg);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.mobile-menu {
|
|
174
|
+
display: none;
|
|
175
|
+
overflow: hidden;
|
|
176
|
+
max-height: 0;
|
|
177
|
+
transition: max-height 0.3s ease;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.mobile-menu.open {
|
|
181
|
+
max-height: 400px;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.mobile-nav-links {
|
|
185
|
+
list-style: none;
|
|
186
|
+
margin: 0;
|
|
187
|
+
padding: 0.5rem 2rem 1.5rem;
|
|
188
|
+
display: flex;
|
|
189
|
+
flex-direction: column;
|
|
190
|
+
gap: 0.5rem;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.mobile-nav-link {
|
|
194
|
+
display: block;
|
|
195
|
+
color: var(--color-text-muted);
|
|
196
|
+
text-decoration: none;
|
|
197
|
+
font-size: 1rem;
|
|
198
|
+
font-weight: 500;
|
|
199
|
+
padding: 0.75rem 0;
|
|
200
|
+
transition: color 0.2s ease;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.mobile-nav-link:hover {
|
|
204
|
+
color: var(--color-text);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
@media (max-width: 768px) {
|
|
208
|
+
.nav-links {
|
|
209
|
+
display: none;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.nav-toggle {
|
|
213
|
+
display: flex;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.mobile-menu {
|
|
217
|
+
display: block;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
</style>
|
|
221
|
+
|
|
222
|
+
<script>
|
|
223
|
+
let cleanupNavigation: (() => void) | null = null;
|
|
224
|
+
|
|
225
|
+
function initNavigation() {
|
|
226
|
+
cleanupNavigation?.();
|
|
227
|
+
|
|
228
|
+
const toggle = document.querySelector<HTMLElement>('.nav-toggle');
|
|
229
|
+
const mobileMenu = document.getElementById('mobile-menu');
|
|
230
|
+
|
|
231
|
+
if (!toggle || !mobileMenu) return;
|
|
232
|
+
|
|
233
|
+
const disposers: Array<() => void> = [];
|
|
234
|
+
const addDisposer = (dispose: () => void) => {
|
|
235
|
+
disposers.push(dispose);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const closeMenu = () => {
|
|
239
|
+
toggle.setAttribute('aria-expanded', 'false');
|
|
240
|
+
mobileMenu.setAttribute('aria-hidden', 'true');
|
|
241
|
+
mobileMenu.classList.remove('open');
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const onToggleClick = () => {
|
|
245
|
+
const isExpanded = toggle.getAttribute('aria-expanded') === 'true';
|
|
246
|
+
toggle.setAttribute('aria-expanded', String(!isExpanded));
|
|
247
|
+
mobileMenu.setAttribute('aria-hidden', String(isExpanded));
|
|
248
|
+
mobileMenu.classList.toggle('open');
|
|
249
|
+
};
|
|
250
|
+
toggle.addEventListener('click', onToggleClick);
|
|
251
|
+
addDisposer(() => toggle.removeEventListener('click', onToggleClick));
|
|
252
|
+
|
|
253
|
+
// Close mobile menu when clicking a link
|
|
254
|
+
const mobileLinks = mobileMenu.querySelectorAll('.mobile-nav-link');
|
|
255
|
+
mobileLinks.forEach((link) => {
|
|
256
|
+
link.addEventListener('click', closeMenu);
|
|
257
|
+
addDisposer(() => link.removeEventListener('click', closeMenu));
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Close mobile menu on escape key
|
|
261
|
+
const onKeydown = (e: KeyboardEvent) => {
|
|
262
|
+
if (e.key === 'Escape' && mobileMenu.classList.contains('open')) {
|
|
263
|
+
closeMenu();
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
document.addEventListener('keydown', onKeydown);
|
|
267
|
+
addDisposer(() => document.removeEventListener('keydown', onKeydown));
|
|
268
|
+
|
|
269
|
+
cleanupNavigation = () => {
|
|
270
|
+
closeMenu();
|
|
271
|
+
disposers.forEach((dispose) => dispose());
|
|
272
|
+
cleanupNavigation = null;
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const handleNavigationBeforeSwap = () => {
|
|
277
|
+
cleanupNavigation?.();
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const lifecycleFlag = 'navigationLifecycleBound';
|
|
281
|
+
if (document.documentElement.dataset[lifecycleFlag] !== '1') {
|
|
282
|
+
document.addEventListener('astro:before-swap', handleNavigationBeforeSwap);
|
|
283
|
+
document.addEventListener('astro:page-load', initNavigation);
|
|
284
|
+
document.documentElement.dataset[lifecycleFlag] = '1';
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
initNavigation();
|
|
288
|
+
</script>
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Projects - Portfolio projects grid section.
|
|
4
|
+
*
|
|
5
|
+
* Displays project cards with:
|
|
6
|
+
* - Title and description
|
|
7
|
+
* - Technology tags
|
|
8
|
+
* - Links to source code (repo) and live demo (link)
|
|
9
|
+
* Responsive grid: 3 columns on large screens, auto-fit otherwise.
|
|
10
|
+
*/
|
|
11
|
+
import type { ThemeContext } from '@mintfolio/theme-api';
|
|
12
|
+
import type { VerdantSettings } from '../../settings';
|
|
13
|
+
|
|
14
|
+
/** Public site data supplied by the containing theme page. */
|
|
15
|
+
interface Props { theme: ThemeContext<VerdantSettings>; }
|
|
16
|
+
import type { Project } from '@mintfolio/theme-api';
|
|
17
|
+
|
|
18
|
+
const { theme } = Astro.props;
|
|
19
|
+
const { projects } = theme.site;
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<section id="projects" class="projects-section">
|
|
23
|
+
<div class="container">
|
|
24
|
+
<h2 class="section-title gradient-text">项目作品</h2>
|
|
25
|
+
|
|
26
|
+
<div class="projects-grid">
|
|
27
|
+
{projects.map((project: Project) => (
|
|
28
|
+
<article class="project-card glass-card">
|
|
29
|
+
<div class="card-content">
|
|
30
|
+
<h3 class="project-title">{project.title}</h3>
|
|
31
|
+
<p class="project-description">{project.description}</p>
|
|
32
|
+
|
|
33
|
+
<div class="project-tags">
|
|
34
|
+
{project.tags.map((tag: string) => (
|
|
35
|
+
<span class="tag">{tag}</span>
|
|
36
|
+
))}
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="project-links">
|
|
40
|
+
{project.repo && (
|
|
41
|
+
<a
|
|
42
|
+
href={project.repo}
|
|
43
|
+
class="project-link"
|
|
44
|
+
target="_blank"
|
|
45
|
+
rel="noopener noreferrer"
|
|
46
|
+
aria-label={`查看 ${project.title} 源码`}
|
|
47
|
+
>
|
|
48
|
+
<svg class="link-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
49
|
+
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/>
|
|
50
|
+
</svg>
|
|
51
|
+
<span>源码</span>
|
|
52
|
+
</a>
|
|
53
|
+
)}
|
|
54
|
+
{project.link && (
|
|
55
|
+
<a
|
|
56
|
+
href={project.link}
|
|
57
|
+
class="project-link"
|
|
58
|
+
target="_blank"
|
|
59
|
+
rel="noopener noreferrer"
|
|
60
|
+
aria-label={`访问 ${project.title} 在线演示`}
|
|
61
|
+
>
|
|
62
|
+
<svg class="link-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
63
|
+
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
|
64
|
+
<polyline points="15 3 21 3 21 9"/>
|
|
65
|
+
<line x1="10" y1="14" x2="21" y2="3"/>
|
|
66
|
+
</svg>
|
|
67
|
+
<span>演示</span>
|
|
68
|
+
</a>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</article>
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</section>
|
|
77
|
+
|
|
78
|
+
<style>
|
|
79
|
+
.projects-section {
|
|
80
|
+
padding: var(--section-padding, 5rem 0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.container {
|
|
84
|
+
max-width: var(--container-max, 1200px);
|
|
85
|
+
margin: 0 auto;
|
|
86
|
+
padding: 0 var(--container-padding, 1.5rem);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.section-title {
|
|
90
|
+
font-size: var(--section-title-size, 2.5rem);
|
|
91
|
+
font-weight: 700;
|
|
92
|
+
text-align: center;
|
|
93
|
+
margin-bottom: var(--section-title-margin, 3rem);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.projects-grid {
|
|
97
|
+
display: grid;
|
|
98
|
+
grid-template-columns: repeat(auto-fit, minmax(min(100%, 350px), 1fr));
|
|
99
|
+
gap: var(--grid-gap, 1.5rem);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.project-card {
|
|
103
|
+
position: relative;
|
|
104
|
+
transition: transform var(--transition-duration, 0.3s) ease, box-shadow var(--transition-duration, 0.3s) ease;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.project-card:hover {
|
|
108
|
+
transform: translateY(-4px);
|
|
109
|
+
box-shadow: var(--card-hover-shadow, 0 20px 40px rgba(0, 0, 0, 0.2));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.card-content {
|
|
113
|
+
padding: var(--card-padding, 1.5rem);
|
|
114
|
+
display: flex;
|
|
115
|
+
flex-direction: column;
|
|
116
|
+
height: 100%;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.project-title {
|
|
120
|
+
font-size: var(--project-title-size, 1.25rem);
|
|
121
|
+
font-weight: 600;
|
|
122
|
+
color: var(--text-primary, #ffffff);
|
|
123
|
+
margin-bottom: 0.75rem;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.project-description {
|
|
127
|
+
font-size: var(--project-desc-size, 0.95rem);
|
|
128
|
+
color: var(--text-secondary, rgba(255, 255, 255, 0.75));
|
|
129
|
+
line-height: 1.6;
|
|
130
|
+
margin-bottom: 1rem;
|
|
131
|
+
flex-grow: 1;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.project-tags {
|
|
135
|
+
display: flex;
|
|
136
|
+
flex-wrap: wrap;
|
|
137
|
+
gap: 0.5rem;
|
|
138
|
+
margin-bottom: 1.25rem;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.tag {
|
|
142
|
+
display: inline-block;
|
|
143
|
+
padding: 0.25rem 0.75rem;
|
|
144
|
+
font-size: var(--tag-size, 0.75rem);
|
|
145
|
+
font-weight: 500;
|
|
146
|
+
color: var(--tag-color, rgba(255, 255, 255, 0.9));
|
|
147
|
+
background: var(--tag-bg, rgba(255, 255, 255, 0.1));
|
|
148
|
+
border: 1px solid var(--tag-border, rgba(255, 255, 255, 0.15));
|
|
149
|
+
border-radius: var(--tag-radius, 9999px);
|
|
150
|
+
transition: background var(--transition-duration, 0.2s) ease;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.tag:hover {
|
|
154
|
+
background: var(--tag-bg-hover, rgba(255, 255, 255, 0.2));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.project-links {
|
|
158
|
+
display: flex;
|
|
159
|
+
gap: 1rem;
|
|
160
|
+
margin-top: auto;
|
|
161
|
+
padding-top: 1rem;
|
|
162
|
+
border-top: 1px solid var(--border-subtle, rgba(255, 255, 255, 0.1));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.project-link {
|
|
166
|
+
display: inline-flex;
|
|
167
|
+
align-items: center;
|
|
168
|
+
gap: 0.5rem;
|
|
169
|
+
font-size: var(--link-size, 0.875rem);
|
|
170
|
+
font-weight: 500;
|
|
171
|
+
color: var(--link-color, rgba(255, 255, 255, 0.8));
|
|
172
|
+
text-decoration: none;
|
|
173
|
+
transition: color var(--transition-duration, 0.2s) ease;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.project-link:hover {
|
|
177
|
+
color: var(--link-color-hover, #ffffff);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.link-icon {
|
|
181
|
+
width: 1.125rem;
|
|
182
|
+
height: 1.125rem;
|
|
183
|
+
flex-shrink: 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* Responsive adjustments */
|
|
187
|
+
@media (max-width: 768px) {
|
|
188
|
+
.projects-section {
|
|
189
|
+
padding: var(--section-padding-mobile, 3rem 0);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.section-title {
|
|
193
|
+
font-size: var(--section-title-size-mobile, 2rem);
|
|
194
|
+
margin-bottom: var(--section-title-margin-mobile, 2rem);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.projects-grid {
|
|
198
|
+
grid-template-columns: 1fr;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
@media (min-width: 1024px) {
|
|
203
|
+
.projects-grid {
|
|
204
|
+
grid-template-columns: repeat(3, 1fr);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
</style>
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Skills - Skills section with progress bar visualization.
|
|
4
|
+
*
|
|
5
|
+
* Displays skills grouped by category (Frontend, Backend, Tools, etc.)
|
|
6
|
+
* with animated progress bars showing proficiency levels (0-100%).
|
|
7
|
+
* Responsive grid: 3 columns on desktop, 2 on tablet, 1 on mobile.
|
|
8
|
+
*/
|
|
9
|
+
import type { ThemeContext } from '@mintfolio/theme-api';
|
|
10
|
+
import type { VerdantSettings } from '../../settings';
|
|
11
|
+
|
|
12
|
+
/** Public site data supplied by the containing theme page. */
|
|
13
|
+
interface Props { theme: ThemeContext<VerdantSettings>; }
|
|
14
|
+
import type { SkillCategory } from '@mintfolio/theme-api';
|
|
15
|
+
|
|
16
|
+
const { theme } = Astro.props;
|
|
17
|
+
const skills: SkillCategory[] = theme.site.skills;
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
<section id="skills" class="skills-section">
|
|
21
|
+
<div class="container">
|
|
22
|
+
<h2 class="section-title gradient-text">技能</h2>
|
|
23
|
+
|
|
24
|
+
<div class="skills-grid">
|
|
25
|
+
{skills.map((category, categoryIndex) => (
|
|
26
|
+
<div class="glass-card skill-category">
|
|
27
|
+
<h3 class="category-name">{category.category}</h3>
|
|
28
|
+
|
|
29
|
+
<div class="skill-list">
|
|
30
|
+
{category.items.map((skill, skillIndex) => (
|
|
31
|
+
<div class="skill-item">
|
|
32
|
+
<div class="skill-header">
|
|
33
|
+
<span class="skill-name">{skill.name}</span>
|
|
34
|
+
<span class="skill-level">{skill.level}%</span>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="progress-track">
|
|
37
|
+
<div
|
|
38
|
+
class="progress-fill"
|
|
39
|
+
style={`--target-width: ${skill.level}%; --delay: ${(categoryIndex * category.items.length + skillIndex) * 0.1}s`}
|
|
40
|
+
></div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
))}
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
))}
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</section>
|
|
50
|
+
|
|
51
|
+
<style>
|
|
52
|
+
.skills-section {
|
|
53
|
+
padding: var(--section-padding, 6rem 2rem);
|
|
54
|
+
position: relative;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.container {
|
|
58
|
+
max-width: var(--max-width, 1200px);
|
|
59
|
+
margin: 0 auto;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.section-title {
|
|
63
|
+
font-size: 2.5rem;
|
|
64
|
+
font-weight: 700;
|
|
65
|
+
text-align: center;
|
|
66
|
+
margin-bottom: 3rem;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.skills-grid {
|
|
70
|
+
display: grid;
|
|
71
|
+
grid-template-columns: repeat(3, 1fr);
|
|
72
|
+
gap: 1.5rem;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@media (max-width: 1024px) {
|
|
76
|
+
.skills-grid {
|
|
77
|
+
grid-template-columns: repeat(2, 1fr);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@media (max-width: 640px) {
|
|
82
|
+
.skills-grid {
|
|
83
|
+
grid-template-columns: 1fr;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.section-title {
|
|
87
|
+
font-size: 2rem;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.skill-category {
|
|
92
|
+
padding: 1.5rem;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.category-name {
|
|
96
|
+
font-size: 1.25rem;
|
|
97
|
+
font-weight: 600;
|
|
98
|
+
color: var(--color-text, #f8fafc);
|
|
99
|
+
margin-bottom: 1.25rem;
|
|
100
|
+
padding-bottom: 0.75rem;
|
|
101
|
+
border-bottom: 1px solid var(--glass-border, rgba(255, 255, 255, 0.1));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.skill-list {
|
|
105
|
+
display: flex;
|
|
106
|
+
flex-direction: column;
|
|
107
|
+
gap: 1rem;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.skill-item {
|
|
111
|
+
display: flex;
|
|
112
|
+
flex-direction: column;
|
|
113
|
+
gap: 0.5rem;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.skill-header {
|
|
117
|
+
display: flex;
|
|
118
|
+
justify-content: space-between;
|
|
119
|
+
align-items: center;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.skill-name {
|
|
123
|
+
font-size: 0.9rem;
|
|
124
|
+
color: var(--color-text, #f8fafc);
|
|
125
|
+
font-weight: 500;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.skill-level {
|
|
129
|
+
font-size: 0.8rem;
|
|
130
|
+
color: var(--color-text-muted, #94a3b8);
|
|
131
|
+
font-weight: 500;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.progress-track {
|
|
135
|
+
height: 6px;
|
|
136
|
+
background: rgba(255, 255, 255, 0.1);
|
|
137
|
+
border-radius: 3px;
|
|
138
|
+
overflow: hidden;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.progress-fill {
|
|
142
|
+
height: 100%;
|
|
143
|
+
width: 0;
|
|
144
|
+
background: linear-gradient(90deg, var(--color-primary, #8b5cf6), var(--color-secondary, #06b6d4));
|
|
145
|
+
border-radius: 3px;
|
|
146
|
+
animation: fillProgress 0.8s ease-out forwards;
|
|
147
|
+
animation-delay: var(--delay, 0s);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
@keyframes fillProgress {
|
|
151
|
+
from {
|
|
152
|
+
width: 0;
|
|
153
|
+
}
|
|
154
|
+
to {
|
|
155
|
+
width: var(--target-width, 0%);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
</style>
|