@coffic/cosy-ui 0.1.29 → 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/README.md +67 -23
- package/dist/app.css +1 -0
- package/dist/assets/logo-rounded.png +0 -0
- package/dist/assets/logo.png +0 -0
- package/dist/components/base/Alert.astro +186 -0
- package/dist/components/base/Button.astro +103 -0
- package/dist/components/base/Image.astro +291 -0
- package/dist/components/base/Link.astro +131 -0
- package/dist/components/base/README.md +53 -0
- package/dist/components/base/index.ts +6 -0
- package/dist/components/containers/Container.astro +103 -0
- package/dist/components/containers/Main.astro +167 -0
- package/dist/components/containers/Section.astro +145 -0
- package/dist/components/containers/index.ts +3 -0
- package/dist/components/data-display/Blog.astro +195 -0
- package/dist/components/data-display/README.md +37 -0
- package/dist/components/data-display/TeamMember.astro +135 -0
- package/dist/components/data-display/TeamMembers.astro +101 -0
- package/dist/components/data-display/index.ts +3 -0
- package/dist/components/display/Banner.astro +57 -0
- package/dist/components/display/Card.astro +135 -0
- package/dist/components/display/CodeBlock.astro +147 -0
- package/dist/components/display/CodeExample.astro +330 -0
- package/dist/components/display/Hero.astro +119 -0
- package/dist/components/display/Modal.astro +115 -0
- package/dist/components/display/README.md +32 -0
- package/dist/components/display/index.ts +6 -0
- package/dist/components/icons/AlertTriangle.astro +35 -0
- package/dist/components/icons/CalendarIcon.astro +38 -0
- package/dist/components/icons/CheckCircle.astro +36 -0
- package/dist/components/icons/CheckIcon.astro +38 -0
- package/dist/components/icons/ClipboardIcon.astro +39 -0
- package/dist/components/icons/CloseIcon.astro +38 -0
- package/dist/components/icons/ErrorIcon.astro +35 -0
- package/dist/components/icons/GithubIcon.astro +31 -0
- package/dist/components/icons/InfoCircle.astro +37 -0
- package/dist/components/icons/InfoIcon.astro +38 -0
- package/dist/components/icons/LinkIcon.astro +39 -0
- package/dist/components/icons/LinkedinIcon.astro +31 -0
- package/dist/components/icons/MenuIcon.astro +41 -0
- package/dist/components/icons/SearchIcon.astro +40 -0
- package/dist/components/icons/SocialIcon.astro +100 -0
- package/dist/components/icons/SuccessIcon.astro +35 -0
- package/dist/components/icons/SunCloudyIcon.astro +45 -0
- package/dist/components/icons/TwitterIcon.astro +31 -0
- package/dist/components/icons/UserIcon.astro +35 -0
- package/dist/components/icons/WarningIcon.astro +38 -0
- package/dist/components/icons/XCircle.astro +37 -0
- package/dist/components/icons/index.ts +23 -0
- package/dist/components/layouts/BaseLayout.astro +144 -0
- package/dist/components/layouts/DashboardLayout.astro +660 -0
- package/dist/components/layouts/DefaultLayout.astro +170 -0
- package/dist/components/layouts/DocumentationLayout.astro +469 -0
- package/dist/components/layouts/Flex.astro +138 -0
- package/dist/components/layouts/Footer.astro +284 -0
- package/dist/components/layouts/Grid.astro +182 -0
- package/dist/components/layouts/Header.astro +114 -0
- package/dist/components/layouts/LandingLayout.astro +388 -0
- package/dist/components/layouts/README.md +37 -0
- package/dist/components/layouts/Stack.astro +149 -0
- package/dist/components/layouts/index.ts +6 -0
- package/dist/components/navigation/LanguageSwitcher.astro +81 -0
- package/dist/components/navigation/README.md +24 -0
- package/dist/components/navigation/TableOfContents.astro +352 -0
- package/dist/components/navigation/ThemeSwitcher.astro +89 -0
- package/dist/components/navigation/index.ts +3 -0
- package/dist/components/typography/Article.astro +144 -0
- package/dist/components/typography/Heading.astro +205 -0
- package/dist/components/typography/README.md +29 -0
- package/dist/components/typography/Text.astro +187 -0
- package/dist/components/typography/index.ts +3 -0
- package/dist/index.ts +9 -0
- package/dist/integration.ts +14 -0
- package/dist/style.ts +1 -0
- package/{src → dist}/types/footer.ts +1 -0
- package/dist/utils/theme.ts +55 -0
- package/package.json +65 -59
- package/index.ts +0 -18
- package/src/components/Alert.astro +0 -78
- package/src/components/Article.astro +0 -11
- package/src/components/Banner.astro +0 -49
- package/src/components/Blog.astro +0 -115
- package/src/components/Button.astro +0 -49
- package/src/components/Card.astro +0 -113
- package/src/components/CodeBlock.astro +0 -186
- package/src/components/Footer.astro +0 -148
- package/src/components/Header.astro +0 -305
- package/src/components/Hero.astro +0 -69
- package/src/components/Image.astro +0 -251
- package/src/components/Link.astro +0 -82
- package/src/components/Modal.astro +0 -67
- package/src/components/SocialIcon.astro +0 -36
- package/src/components/TeamMember.astro +0 -68
- package/src/components/TeamMembers.astro +0 -43
- package/src/env.d.ts +0 -0
- /package/{src/components → dist/components/base}/ThemeItem.astro +0 -0
- /package/{src → dist}/utils/social.ts +0 -0
@@ -0,0 +1,81 @@
|
|
1
|
+
---
|
2
|
+
import Button from '../base/Button.astro';
|
3
|
+
import Link from '../base/Link.astro';
|
4
|
+
|
5
|
+
interface Language {
|
6
|
+
code: string;
|
7
|
+
name: string;
|
8
|
+
}
|
9
|
+
|
10
|
+
interface Props {
|
11
|
+
/**
|
12
|
+
* 自定义类名
|
13
|
+
*/
|
14
|
+
class?: string;
|
15
|
+
|
16
|
+
/**
|
17
|
+
* 语言列表
|
18
|
+
* @default [{ code: 'zh-cn', name: '简体中文' }, { code: 'en', name: 'English' }]
|
19
|
+
*/
|
20
|
+
languages?: Language[];
|
21
|
+
|
22
|
+
/**
|
23
|
+
* 当前语言
|
24
|
+
* @default 'zh-cn'
|
25
|
+
*/
|
26
|
+
currentLocale?: string;
|
27
|
+
}
|
28
|
+
|
29
|
+
const {
|
30
|
+
class: className,
|
31
|
+
languages = [
|
32
|
+
{ code: 'zh-cn', name: '简体中文' },
|
33
|
+
{ code: 'en', name: 'English' },
|
34
|
+
],
|
35
|
+
currentLocale = 'zh-cn',
|
36
|
+
} = Astro.props;
|
37
|
+
|
38
|
+
const currentLanguageName =
|
39
|
+
languages.find((lang: Language) => lang.code === currentLocale)?.name || '简体中文';
|
40
|
+
|
41
|
+
// 为每个语言生成对应的URL
|
42
|
+
function generateLanguageUrl(langCode: string): string {
|
43
|
+
const currentPath = Astro.url.pathname;
|
44
|
+
const pathParts = currentPath.split('/').filter(Boolean);
|
45
|
+
const firstPathPart = pathParts[0];
|
46
|
+
const supportedLanguages = languages.map((lang: Language) => lang.code);
|
47
|
+
const isFirstPartLang = supportedLanguages.includes(firstPathPart);
|
48
|
+
|
49
|
+
if (isFirstPartLang) {
|
50
|
+
pathParts[0] = langCode;
|
51
|
+
return '/' + pathParts.join('/');
|
52
|
+
} else {
|
53
|
+
return '/' + langCode + (currentPath === '/' ? '' : currentPath);
|
54
|
+
}
|
55
|
+
}
|
56
|
+
---
|
57
|
+
|
58
|
+
<div class:list={["dropdown-end dropdown", className]}>
|
59
|
+
<Button variant="ghost" size="sm" class="p-1">
|
60
|
+
{currentLanguageName}
|
61
|
+
</Button>
|
62
|
+
<ul
|
63
|
+
tabindex={0}
|
64
|
+
class="dropdown-content menu w-40 rounded-box bg-slate-900 p-2 shadow-lg dark:bg-slate-800"
|
65
|
+
>
|
66
|
+
{
|
67
|
+
languages.map((lang: Language) => (
|
68
|
+
<li>
|
69
|
+
<Link
|
70
|
+
href={generateLanguageUrl(lang.code)}
|
71
|
+
class:list={[
|
72
|
+
{ active: lang.code === currentLocale }
|
73
|
+
]}
|
74
|
+
>
|
75
|
+
{lang.name}
|
76
|
+
</Link>
|
77
|
+
</li>
|
78
|
+
))
|
79
|
+
}
|
80
|
+
</ul>
|
81
|
+
</div>
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# 导航组件 (Navigation Components)
|
2
|
+
|
3
|
+
这个目录包含了与网站导航和用户界面导航相关的组件。
|
4
|
+
|
5
|
+
## 设计原则
|
6
|
+
|
7
|
+
1. **直观性**:导航应该清晰易懂,用户能够直观地理解如何使用
|
8
|
+
2. **一致性**:导航行为和样式应该在整个应用中保持一致
|
9
|
+
3. **响应式**:适应不同的屏幕尺寸和设备类型
|
10
|
+
|
11
|
+
## 包含的组件
|
12
|
+
|
13
|
+
- `LanguageSwitcher.astro`: 语言切换组件
|
14
|
+
- `ThemeSwitcher.astro`: 主题切换组件
|
15
|
+
- `TableOfContents.astro`: 目录导航组件
|
16
|
+
|
17
|
+
## 使用指南
|
18
|
+
|
19
|
+
导航组件通常需要配置相应的数据和回调函数:
|
20
|
+
|
21
|
+
```astro
|
22
|
+
<LanguageSwitcher languages={['zh', 'en']} />
|
23
|
+
<TableOfContents items={tocItems} />
|
24
|
+
```
|
@@ -0,0 +1,352 @@
|
|
1
|
+
---
|
2
|
+
/**
|
3
|
+
* @component TableOfContents
|
4
|
+
*
|
5
|
+
* @description
|
6
|
+
* TableOfContents 组件是一个目录导航组件,用于显示页面内容的标题结构。
|
7
|
+
* 它会自动检测页面中的标题元素,生成目录列表,并在用户滚动页面时高亮当前可见的标题。
|
8
|
+
*
|
9
|
+
* @design
|
10
|
+
* 设计理念:
|
11
|
+
* 1. 导航辅助 - 帮助用户快速了解页面结构和导航到特定内容
|
12
|
+
* 2. 上下文感知 - 自动高亮当前阅读位置,提供阅读进度反馈
|
13
|
+
* 3. 视觉层次 - 通过缩进和样式区分不同级别的标题
|
14
|
+
* 4. 响应式设计 - 在小屏幕设备上自动隐藏,优化空间利用
|
15
|
+
*
|
16
|
+
* @usage
|
17
|
+
* 基本用法:
|
18
|
+
* ```astro
|
19
|
+
* <TableOfContents />
|
20
|
+
* ```
|
21
|
+
*
|
22
|
+
* 自定义标题和选择器:
|
23
|
+
* ```astro
|
24
|
+
* <TableOfContents
|
25
|
+
* title="章节导航"
|
26
|
+
* selector="h2, h3, h4"
|
27
|
+
* maxDepth={4}
|
28
|
+
* />
|
29
|
+
* ```
|
30
|
+
*
|
31
|
+
* 非固定位置:
|
32
|
+
* ```astro
|
33
|
+
* <TableOfContents fixed={false} />
|
34
|
+
* ```
|
35
|
+
*
|
36
|
+
* 指定内容容器:
|
37
|
+
* ```astro
|
38
|
+
* <TableOfContents containerSelector=".article-content" />
|
39
|
+
* ```
|
40
|
+
*/
|
41
|
+
|
42
|
+
// 导入样式
|
43
|
+
import '../../app.css';
|
44
|
+
|
45
|
+
interface Props {
|
46
|
+
/**
|
47
|
+
* 是否固定在右侧
|
48
|
+
* @default true
|
49
|
+
*/
|
50
|
+
fixed?: boolean;
|
51
|
+
/**
|
52
|
+
* 自定义类名
|
53
|
+
*/
|
54
|
+
class?: string;
|
55
|
+
/**
|
56
|
+
* 标题选择器
|
57
|
+
* @default "h2, h3"
|
58
|
+
*/
|
59
|
+
selector?: string;
|
60
|
+
/**
|
61
|
+
* 最大深度
|
62
|
+
* @default 3
|
63
|
+
*/
|
64
|
+
maxDepth?: number;
|
65
|
+
/**
|
66
|
+
* 标题文本
|
67
|
+
* @default "目录"
|
68
|
+
*/
|
69
|
+
title?: string;
|
70
|
+
/**
|
71
|
+
* 内容容器选择器,用于限制标题搜索范围
|
72
|
+
* @default "main"
|
73
|
+
*/
|
74
|
+
containerSelector?: string;
|
75
|
+
}
|
76
|
+
|
77
|
+
const {
|
78
|
+
fixed = true,
|
79
|
+
class: className = '',
|
80
|
+
selector = 'h2, h3',
|
81
|
+
maxDepth = 3,
|
82
|
+
title = '目录',
|
83
|
+
containerSelector = 'main',
|
84
|
+
} = Astro.props;
|
85
|
+
|
86
|
+
// 生成唯一ID,确保多个TOC实例不会冲突
|
87
|
+
const tocId = `toc-${Math.random().toString(36).substring(2, 9)}`;
|
88
|
+
---
|
89
|
+
|
90
|
+
<div class={`toc ${fixed ? 'toc-fixed' : ''} ${className}`}>
|
91
|
+
<div class="toc-container">
|
92
|
+
<div class="toc-title">{title}</div>
|
93
|
+
<ul class="toc-list" id={tocId}>
|
94
|
+
<!-- 目录项将通过 JavaScript 动态生成 -->
|
95
|
+
<li class="toc-placeholder">加载中...</li>
|
96
|
+
</ul>
|
97
|
+
</div>
|
98
|
+
</div>
|
99
|
+
|
100
|
+
<script define:vars={{ selector, maxDepth, tocId, containerSelector }}>
|
101
|
+
// 在页面加载完成后生成目录
|
102
|
+
function generateTOC() {
|
103
|
+
// 使用指定的容器选择器查找内容容器
|
104
|
+
const container = document.querySelector(containerSelector);
|
105
|
+
if (!container) return;
|
106
|
+
|
107
|
+
// 只查找容器内的标题,而不是整个页面
|
108
|
+
const headings = container.querySelectorAll(selector);
|
109
|
+
const tocList = document.getElementById(tocId);
|
110
|
+
|
111
|
+
if (!tocList || headings.length === 0) return;
|
112
|
+
|
113
|
+
// 清空占位符
|
114
|
+
tocList.innerHTML = '';
|
115
|
+
|
116
|
+
// 用于存储已使用的 ID,确保唯一性
|
117
|
+
const usedIds = new Set();
|
118
|
+
|
119
|
+
// 为每个标题创建目录项
|
120
|
+
headings.forEach((heading) => {
|
121
|
+
// 如果标题没有 ID 或 ID 为空,则生成一个
|
122
|
+
if (!heading.id || heading.id.trim() === '') {
|
123
|
+
// 从标题文本生成 ID
|
124
|
+
let newId = generateIdFromText(heading.textContent || '');
|
125
|
+
|
126
|
+
// 确保 ID 唯一
|
127
|
+
let counter = 0;
|
128
|
+
let uniqueId = newId;
|
129
|
+
while (usedIds.has(uniqueId) || document.getElementById(uniqueId)) {
|
130
|
+
counter++;
|
131
|
+
uniqueId = `${newId}-${counter}`;
|
132
|
+
}
|
133
|
+
|
134
|
+
// 设置标题的 ID
|
135
|
+
heading.id = uniqueId;
|
136
|
+
}
|
137
|
+
|
138
|
+
// 记录已使用的 ID
|
139
|
+
usedIds.add(heading.id);
|
140
|
+
|
141
|
+
const level = parseInt(heading.tagName.substring(1));
|
142
|
+
if (level > maxDepth) return;
|
143
|
+
|
144
|
+
const listItem = document.createElement('li');
|
145
|
+
listItem.className = `toc-item toc-level-${level}`;
|
146
|
+
listItem.dataset.headingId = heading.id; // 添加数据属性,便于后续查找
|
147
|
+
|
148
|
+
const link = document.createElement('a');
|
149
|
+
link.href = `#${heading.id}`;
|
150
|
+
link.textContent = heading.textContent;
|
151
|
+
link.className = 'toc-link';
|
152
|
+
|
153
|
+
// 添加前缀标记,增强视觉层次
|
154
|
+
if (level === 3) {
|
155
|
+
const prefix = document.createElement('span');
|
156
|
+
prefix.className = 'toc-prefix';
|
157
|
+
prefix.innerHTML = '└─ ';
|
158
|
+
link.prepend(prefix);
|
159
|
+
}
|
160
|
+
|
161
|
+
listItem.appendChild(link);
|
162
|
+
tocList.appendChild(listItem);
|
163
|
+
|
164
|
+
// 添加点击事件,平滑滚动到目标位置
|
165
|
+
link.addEventListener('click', (e) => {
|
166
|
+
e.preventDefault();
|
167
|
+
const targetId = link.getAttribute('href').substring(1);
|
168
|
+
const targetElement = document.getElementById(targetId);
|
169
|
+
|
170
|
+
if (targetElement) {
|
171
|
+
// 滚动到目标位置,并添加一些偏移以避免被固定导航遮挡
|
172
|
+
const offset = 80; // 可根据实际情况调整
|
173
|
+
const targetPosition = targetElement.getBoundingClientRect().top + window.scrollY - offset;
|
174
|
+
|
175
|
+
window.scrollTo({
|
176
|
+
top: targetPosition,
|
177
|
+
behavior: 'smooth'
|
178
|
+
});
|
179
|
+
|
180
|
+
// 更新 URL,但不跳转
|
181
|
+
history.pushState(null, null, `#${targetId}`);
|
182
|
+
|
183
|
+
// 添加高亮效果
|
184
|
+
setTimeout(() => {
|
185
|
+
highlightActiveHeading();
|
186
|
+
}, 500);
|
187
|
+
}
|
188
|
+
});
|
189
|
+
});
|
190
|
+
|
191
|
+
// 如果没有找到标题,显示提示信息
|
192
|
+
if (tocList.children.length === 0) {
|
193
|
+
const listItem = document.createElement('li');
|
194
|
+
listItem.textContent = '没有找到标题';
|
195
|
+
listItem.className = 'toc-empty';
|
196
|
+
tocList.appendChild(listItem);
|
197
|
+
}
|
198
|
+
|
199
|
+
// 添加活动标题高亮
|
200
|
+
highlightActiveHeading();
|
201
|
+
|
202
|
+
// 使用节流函数优化滚动事件
|
203
|
+
window.addEventListener('scroll', throttle(highlightActiveHeading, 100));
|
204
|
+
}
|
205
|
+
|
206
|
+
// 从文本生成有效的 ID
|
207
|
+
function generateIdFromText(text) {
|
208
|
+
return text
|
209
|
+
.trim()
|
210
|
+
.toLowerCase()
|
211
|
+
.replace(/[\s\n]+/g, '-') // 将空格和换行替换为连字符
|
212
|
+
.replace(/[^\w\u4e00-\u9fa5-]/g, '') // 只保留字母、数字、中文和连字符
|
213
|
+
.replace(/^-+|-+$/g, '') // 移除开头和结尾的连字符
|
214
|
+
.replace(/-{2,}/g, '-') // 将多个连字符替换为单个
|
215
|
+
|| 'heading'; // 如果结果为空,则使用默认值
|
216
|
+
}
|
217
|
+
|
218
|
+
// 节流函数,限制函数调用频率
|
219
|
+
function throttle(func, delay) {
|
220
|
+
let lastCall = 0;
|
221
|
+
return function(...args) {
|
222
|
+
const now = new Date().getTime();
|
223
|
+
if (now - lastCall < delay) {
|
224
|
+
return;
|
225
|
+
}
|
226
|
+
lastCall = now;
|
227
|
+
return func(...args);
|
228
|
+
};
|
229
|
+
}
|
230
|
+
|
231
|
+
// 高亮当前可见的标题
|
232
|
+
function highlightActiveHeading() {
|
233
|
+
const container = document.querySelector(containerSelector);
|
234
|
+
if (!container) return;
|
235
|
+
|
236
|
+
const headings = Array.from(container.querySelectorAll(selector));
|
237
|
+
if (headings.length === 0) return;
|
238
|
+
|
239
|
+
// 获取视口高度和滚动位置
|
240
|
+
const viewportHeight = window.innerHeight;
|
241
|
+
const scrollTop = window.scrollY;
|
242
|
+
|
243
|
+
// 计算视口中心位置
|
244
|
+
const viewportMiddle = scrollTop + viewportHeight / 3;
|
245
|
+
|
246
|
+
let activeHeadingId = null;
|
247
|
+
let closestHeading = null;
|
248
|
+
let closestDistance = Infinity;
|
249
|
+
|
250
|
+
// 找到最接近视口中心的标题
|
251
|
+
for (const heading of headings) {
|
252
|
+
const headingTop = heading.getBoundingClientRect().top + scrollTop;
|
253
|
+
const distance = Math.abs(headingTop - viewportMiddle);
|
254
|
+
|
255
|
+
if (distance < closestDistance) {
|
256
|
+
closestDistance = distance;
|
257
|
+
closestHeading = heading;
|
258
|
+
}
|
259
|
+
}
|
260
|
+
|
261
|
+
// 如果找到了最接近的标题,使用它的ID
|
262
|
+
if (closestHeading) {
|
263
|
+
activeHeadingId = closestHeading.id;
|
264
|
+
|
265
|
+
// 为当前活动标题添加一个临时的高亮效果
|
266
|
+
headings.forEach(h => h.classList.remove('current-heading'));
|
267
|
+
closestHeading.classList.add('current-heading');
|
268
|
+
|
269
|
+
// 5秒后移除高亮效果
|
270
|
+
setTimeout(() => {
|
271
|
+
closestHeading.classList.remove('current-heading');
|
272
|
+
}, 5000);
|
273
|
+
} else if (headings.length > 0) {
|
274
|
+
// 如果没有找到,使用第一个标题
|
275
|
+
activeHeadingId = headings[0].id;
|
276
|
+
}
|
277
|
+
|
278
|
+
// 更新活动链接样式
|
279
|
+
const tocLinks = document.querySelectorAll(`#${tocId} .toc-link`);
|
280
|
+
let hasActiveLink = false;
|
281
|
+
|
282
|
+
tocLinks.forEach(link => {
|
283
|
+
const href = link.getAttribute('href').substring(1);
|
284
|
+
|
285
|
+
// 清除所有高亮
|
286
|
+
link.classList.remove('toc-active', 'toc-parent-active');
|
287
|
+
|
288
|
+
// 添加当前活动标题的高亮
|
289
|
+
if (href === activeHeadingId) {
|
290
|
+
link.classList.add('toc-active');
|
291
|
+
hasActiveLink = true;
|
292
|
+
|
293
|
+
// 添加一个动画效果,使高亮更明显
|
294
|
+
link.animate([
|
295
|
+
{ transform: 'translateX(-5px)' },
|
296
|
+
{ transform: 'translateX(0)' }
|
297
|
+
], {
|
298
|
+
duration: 300,
|
299
|
+
easing: 'ease-out'
|
300
|
+
});
|
301
|
+
|
302
|
+
// 如果是 h3,也高亮相关的 h2
|
303
|
+
const h3Item = link.closest('.toc-level-3');
|
304
|
+
if (h3Item) {
|
305
|
+
// 找到前面最近的 h2
|
306
|
+
let prevItem = h3Item.previousElementSibling;
|
307
|
+
while (prevItem) {
|
308
|
+
if (prevItem.classList.contains('toc-level-2')) {
|
309
|
+
const h2Link = prevItem.querySelector('.toc-link');
|
310
|
+
if (h2Link) h2Link.classList.add('toc-parent-active');
|
311
|
+
break;
|
312
|
+
}
|
313
|
+
prevItem = prevItem.previousElementSibling;
|
314
|
+
}
|
315
|
+
}
|
316
|
+
}
|
317
|
+
});
|
318
|
+
|
319
|
+
// 如果没有找到活动链接,尝试高亮第一个标题
|
320
|
+
if (!hasActiveLink && tocLinks.length > 0) {
|
321
|
+
tocLinks[0].classList.add('toc-active');
|
322
|
+
}
|
323
|
+
|
324
|
+
// 确保活动链接在视图中可见
|
325
|
+
const activeLink = document.querySelector(`#${tocId} .toc-active`);
|
326
|
+
if (activeLink && tocList) {
|
327
|
+
const tocContainer = document.querySelector('.toc-fixed');
|
328
|
+
if (tocContainer) {
|
329
|
+
const linkTop = activeLink.offsetTop;
|
330
|
+
const containerScrollTop = tocContainer.scrollTop;
|
331
|
+
const containerHeight = tocContainer.clientHeight;
|
332
|
+
|
333
|
+
// 如果链接不在可视区域内,滚动到可见位置
|
334
|
+
if (linkTop < containerScrollTop || linkTop > containerScrollTop + containerHeight) {
|
335
|
+
tocContainer.scrollTop = linkTop - containerHeight / 2;
|
336
|
+
}
|
337
|
+
}
|
338
|
+
}
|
339
|
+
}
|
340
|
+
|
341
|
+
// 页面加载时初始化
|
342
|
+
document.addEventListener('astro:page-load', generateTOC);
|
343
|
+
// 初始加载时也初始化
|
344
|
+
generateTOC();
|
345
|
+
|
346
|
+
// 添加resize事件监听,在窗口大小变化时重新计算高亮
|
347
|
+
window.addEventListener('resize', throttle(highlightActiveHeading, 100));
|
348
|
+
|
349
|
+
// 立即触发一次高亮计算
|
350
|
+
setTimeout(highlightActiveHeading, 500);
|
351
|
+
</script>
|
352
|
+
|
@@ -0,0 +1,89 @@
|
|
1
|
+
---
|
2
|
+
import Button from '../base/Button.astro';
|
3
|
+
import SunCloudyIcon from '../icons/SunCloudyIcon.astro';
|
4
|
+
|
5
|
+
interface Props {
|
6
|
+
/**
|
7
|
+
* 自定义类名
|
8
|
+
*/
|
9
|
+
class?: string;
|
10
|
+
}
|
11
|
+
|
12
|
+
const { class: className } = Astro.props;
|
13
|
+
|
14
|
+
const themes = [
|
15
|
+
{ id: 'default', name: 'Default' },
|
16
|
+
{ id: 'light', name: 'Light' },
|
17
|
+
{ id: 'dark', name: 'Dark' },
|
18
|
+
{ id: 'pastel', name: 'Pastel' },
|
19
|
+
{ id: 'lemonade', name: 'Lemonade' },
|
20
|
+
{ id: 'cupcake', name: 'Cupcake' },
|
21
|
+
{ id: 'nord', name: 'Nord' },
|
22
|
+
{ id: 'business', name: 'Business' },
|
23
|
+
{ id: 'luxury', name: 'Luxury' },
|
24
|
+
];
|
25
|
+
---
|
26
|
+
|
27
|
+
<div class:list={["dropdown-end dropdown", className]}>
|
28
|
+
<Button variant="ghost" size="sm" class="p-1">
|
29
|
+
<SunCloudyIcon class="h-5 w-5" slot="icon-left" />
|
30
|
+
</Button>
|
31
|
+
<ul
|
32
|
+
tabindex="0"
|
33
|
+
class="dropdown-content menu w-56 rounded-box bg-neutral-900 p-2 shadow-lg dark:bg-neutral-800"
|
34
|
+
>
|
35
|
+
{themes.map((theme) => (
|
36
|
+
<li>
|
37
|
+
<button
|
38
|
+
class="theme-item"
|
39
|
+
data-theme={theme.id}
|
40
|
+
data-active={false}
|
41
|
+
>
|
42
|
+
{theme.name}
|
43
|
+
</button>
|
44
|
+
</li>
|
45
|
+
))}
|
46
|
+
</ul>
|
47
|
+
</div>
|
48
|
+
|
49
|
+
<script>
|
50
|
+
import { createThemeManager } from '../../utils/theme';
|
51
|
+
|
52
|
+
const themeManager = createThemeManager();
|
53
|
+
|
54
|
+
function updateActiveTheme() {
|
55
|
+
const currentTheme = document.documentElement.getAttribute('data-theme') || 'default';
|
56
|
+
document.querySelectorAll('.theme-item').forEach((item) => {
|
57
|
+
const isActive = item.getAttribute('data-theme') === currentTheme;
|
58
|
+
item.setAttribute('data-active', String(isActive));
|
59
|
+
if (isActive) {
|
60
|
+
item.classList.add('active');
|
61
|
+
} else {
|
62
|
+
item.classList.remove('active');
|
63
|
+
}
|
64
|
+
});
|
65
|
+
}
|
66
|
+
|
67
|
+
// 初始化主题切换按钮
|
68
|
+
document.querySelectorAll('.theme-item').forEach((item) => {
|
69
|
+
item.addEventListener('click', () => {
|
70
|
+
const theme = item.getAttribute('data-theme');
|
71
|
+
if (theme) {
|
72
|
+
themeManager.setTheme(theme);
|
73
|
+
updateActiveTheme();
|
74
|
+
}
|
75
|
+
});
|
76
|
+
});
|
77
|
+
|
78
|
+
// 初始加载时初始化
|
79
|
+
document.addEventListener('DOMContentLoaded', () => {
|
80
|
+
themeManager.initialize();
|
81
|
+
updateActiveTheme();
|
82
|
+
});
|
83
|
+
|
84
|
+
// Astro view transitions 后重新初始化
|
85
|
+
document.addEventListener('astro:after-swap', () => {
|
86
|
+
themeManager.initialize();
|
87
|
+
updateActiveTheme();
|
88
|
+
});
|
89
|
+
</script>
|
@@ -0,0 +1,144 @@
|
|
1
|
+
---
|
2
|
+
/**
|
3
|
+
* @component Article
|
4
|
+
*
|
5
|
+
* @description
|
6
|
+
* Article 组件用于展示格式化的文章内容,提供了一套完整的排版样式,使文章内容更易于阅读和理解。
|
7
|
+
* 该组件适用于博客文章、文档页面、新闻内容等需要良好排版的场景。
|
8
|
+
*
|
9
|
+
* @design
|
10
|
+
* 设计理念:
|
11
|
+
* 1. 可读性优先 - 使用合理的行高、字体大小和间距,提高长文本的阅读体验
|
12
|
+
* 2. 层次分明 - 通过不同级别标题的样式区分,建立清晰的内容层次结构
|
13
|
+
* 3. 响应式设计 - 在不同设备上保持良好的阅读体验
|
14
|
+
* 4. 暗黑模式支持 - 自动适应系统的暗黑模式设置
|
15
|
+
*
|
16
|
+
* 视觉特点:
|
17
|
+
* - 合理的文本行高和段落间距
|
18
|
+
* - 标题使用较粗的字重和较小的行高
|
19
|
+
* - 列表项有适当的缩进和间距
|
20
|
+
* - 引用块使用左侧边框和斜体样式
|
21
|
+
* - 代码块和行内代码有不同的背景色
|
22
|
+
* - 表格有清晰的边框和背景色区分
|
23
|
+
*
|
24
|
+
* @usage
|
25
|
+
* 基本用法:
|
26
|
+
* ```astro
|
27
|
+
* <Article>
|
28
|
+
* <h1>文章标题</h1>
|
29
|
+
* <p>这是一段文章内容...</p>
|
30
|
+
* </Article>
|
31
|
+
* ```
|
32
|
+
*
|
33
|
+
* 添加自定义类名:
|
34
|
+
* ```astro
|
35
|
+
* <Article class="my-custom-article">
|
36
|
+
* <h2>带自定义类的文章</h2>
|
37
|
+
* <p>这是一段文章内容...</p>
|
38
|
+
* </Article>
|
39
|
+
* ```
|
40
|
+
*
|
41
|
+
* 设置文章宽度:
|
42
|
+
* ```astro
|
43
|
+
* <Article width="narrow">
|
44
|
+
* <h2>窄宽度文章</h2>
|
45
|
+
* <p>这是一段文章内容...</p>
|
46
|
+
* </Article>
|
47
|
+
* ```
|
48
|
+
*
|
49
|
+
* 完整示例:
|
50
|
+
* ```astro
|
51
|
+
* <Article>
|
52
|
+
* <h1>文章标题</h1>
|
53
|
+
* <p>这是一段介绍性文字,可以包含<a href="#">链接</a>和<code>行内代码</code>。</p>
|
54
|
+
*
|
55
|
+
* <h2>二级标题</h2>
|
56
|
+
* <p>这是二级标题下的段落内容。</p>
|
57
|
+
*
|
58
|
+
* <blockquote>
|
59
|
+
* 这是一段引用内容,可以用来强调重要信息或引用他人的话。
|
60
|
+
* </blockquote>
|
61
|
+
*
|
62
|
+
* <h3>三级标题</h3>
|
63
|
+
* <ul>
|
64
|
+
* <li>无序列表项一</li>
|
65
|
+
* <li>无序列表项二</li>
|
66
|
+
* </ul>
|
67
|
+
*
|
68
|
+
* <h4>四级标题</h4>
|
69
|
+
* <ol>
|
70
|
+
* <li>有序列表项一</li>
|
71
|
+
* <li>有序列表项二</li>
|
72
|
+
* </ol>
|
73
|
+
*
|
74
|
+
* <pre><code>// 这是一个代码块
|
75
|
+
* function example() {
|
76
|
+
* return "示例代码";
|
77
|
+
* }</code></pre>
|
78
|
+
* </Article>
|
79
|
+
* ```
|
80
|
+
*
|
81
|
+
* @props
|
82
|
+
* @prop {string} [class] - 自定义 CSS 类名,会与组件内置的类名合并
|
83
|
+
* @prop {string} [width="medium"] - 文章宽度,可选值为 "narrow"(窄), "medium"(中等), "wide"(宽), "full"(全宽)
|
84
|
+
*
|
85
|
+
* @slots
|
86
|
+
* @slot default - 文章内容,可以包含任何 HTML 元素
|
87
|
+
*
|
88
|
+
* @cssFeatures
|
89
|
+
* 组件支持的 HTML 元素样式:
|
90
|
+
* - 标题 (h1-h6):不同级别的标题有不同的大小和间距
|
91
|
+
* - 段落 (p):适当的行高和段落间距
|
92
|
+
* - 列表 (ul, ol, li):合理的缩进和项目间距
|
93
|
+
* - 链接 (a):带下划线的彩色文本
|
94
|
+
* - 引用块 (blockquote):带左侧边框的斜体文本
|
95
|
+
* - 代码 (code, pre):带背景色的等宽字体
|
96
|
+
* - 图片 (img):自适应宽度
|
97
|
+
* - 水平线 (hr):简洁的分隔线
|
98
|
+
* - 表格 (table, th, td):清晰的表格样式
|
99
|
+
*
|
100
|
+
* @accessibility
|
101
|
+
* - 使用语义化的 article 元素作为容器
|
102
|
+
* - 保持足够的颜色对比度,确保在不同模式下的可读性
|
103
|
+
* - 支持系统的暗黑模式设置
|
104
|
+
*/
|
105
|
+
|
106
|
+
// 导入样式
|
107
|
+
import '../../app.css';
|
108
|
+
|
109
|
+
export interface Props {
|
110
|
+
/**
|
111
|
+
* 文章的类名
|
112
|
+
*/
|
113
|
+
class?: string;
|
114
|
+
|
115
|
+
/**
|
116
|
+
* 类名列表
|
117
|
+
*/
|
118
|
+
'class:list'?: any;
|
119
|
+
|
120
|
+
/**
|
121
|
+
* 内联样式
|
122
|
+
*/
|
123
|
+
style?: string;
|
124
|
+
|
125
|
+
/**
|
126
|
+
* 文章宽度
|
127
|
+
* @default "medium"
|
128
|
+
*/
|
129
|
+
width?: "narrow" | "medium" | "wide" | "full";
|
130
|
+
}
|
131
|
+
|
132
|
+
const {
|
133
|
+
class: className = '',
|
134
|
+
width = "medium",
|
135
|
+
style = ""
|
136
|
+
} = Astro.props;
|
137
|
+
|
138
|
+
// 根据宽度选项设置类名
|
139
|
+
const widthClass = `article-width-${width}`;
|
140
|
+
---
|
141
|
+
|
142
|
+
<article class:list={['article', widthClass, className]} style={style}>
|
143
|
+
<slot />
|
144
|
+
</article>
|