@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
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
|
+
}
|
package/styles/fonts.css
ADDED
|
@@ -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
|
+
}
|