@coffic/cosy-ui 0.2.2 → 0.3.0-beta.1
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 +1 -6
- package/dist/app.css +1 -1
- package/dist/components/display/CodeExample.astro +39 -206
- package/dist/components/layouts/DocumentationLayout.astro +28 -53
- package/dist/components/layouts/Footer.astro +25 -16
- package/dist/components/layouts/Header.astro +53 -10
- package/dist/components/layouts/Sidebar.astro +119 -0
- package/dist/components/navigation/TableOfContents.astro +64 -61
- package/dist/components/typography/Article.astro +19 -3
- package/dist/utils/path.ts +15 -0
- package/package.json +11 -4
@@ -0,0 +1,119 @@
|
|
1
|
+
---
|
2
|
+
/**
|
3
|
+
* Sidebar组件
|
4
|
+
*
|
5
|
+
* 用于文档页面的侧边栏导航
|
6
|
+
*
|
7
|
+
* @example
|
8
|
+
* ```astro
|
9
|
+
* ---
|
10
|
+
* import Sidebar from './Sidebar.astro';
|
11
|
+
*
|
12
|
+
* const sidebarItems = [
|
13
|
+
* { title: "入门", items: [
|
14
|
+
* { href: "/docs/getting-started", text: "快速开始" },
|
15
|
+
* { href: "/docs/installation", text: "安装" }
|
16
|
+
* ]}
|
17
|
+
* ];
|
18
|
+
* ---
|
19
|
+
*
|
20
|
+
* <Sidebar sidebarItems={sidebarItems} currentPath="/docs/getting-started" />
|
21
|
+
* ```
|
22
|
+
*/
|
23
|
+
|
24
|
+
import { isPathMatch } from '../../utils/path';
|
25
|
+
import "../../app.css"
|
26
|
+
|
27
|
+
export interface SidebarItem {
|
28
|
+
href: string;
|
29
|
+
text: string;
|
30
|
+
items?: SidebarItem[];
|
31
|
+
}
|
32
|
+
|
33
|
+
export interface SidebarSection {
|
34
|
+
title: string;
|
35
|
+
items: SidebarItem[];
|
36
|
+
}
|
37
|
+
|
38
|
+
export interface Props {
|
39
|
+
/**
|
40
|
+
* 侧边栏项目
|
41
|
+
*/
|
42
|
+
sidebarItems: SidebarSection[];
|
43
|
+
|
44
|
+
/**
|
45
|
+
* 当前路径
|
46
|
+
*/
|
47
|
+
currentPath: string;
|
48
|
+
}
|
49
|
+
|
50
|
+
const { sidebarItems, currentPath } = Astro.props;
|
51
|
+
---
|
52
|
+
|
53
|
+
<aside class="w-64 border-r border-base-300 shrink-0">
|
54
|
+
<nav class="p-4 sticky top-16">
|
55
|
+
{sidebarItems.map((section: SidebarSection) => (
|
56
|
+
<div class="mb-6">
|
57
|
+
<h3 class="font-bold mb-2 text-base-content/70">{section.title}</h3>
|
58
|
+
<ul class="menu bg-base-200 rounded-box w-56">
|
59
|
+
{section.items.map((item: SidebarItem) => {
|
60
|
+
const isActive = isPathMatch(currentPath, item.href);
|
61
|
+
return (
|
62
|
+
<li>
|
63
|
+
<a
|
64
|
+
href={item.href}
|
65
|
+
class:list={[
|
66
|
+
"hover:bg-base-300",
|
67
|
+
{ "menu-active": isActive }
|
68
|
+
]}
|
69
|
+
>
|
70
|
+
{item.text}
|
71
|
+
</a>
|
72
|
+
{item.items && (
|
73
|
+
<ul>
|
74
|
+
{item.items.map((subitem: SidebarItem) => {
|
75
|
+
const isSubActive = isPathMatch(currentPath, subitem.href);
|
76
|
+
return (
|
77
|
+
<li>
|
78
|
+
<a
|
79
|
+
href={subitem.href}
|
80
|
+
class:list={[
|
81
|
+
"hover:bg-base-300",
|
82
|
+
{ "active": isSubActive }
|
83
|
+
]}
|
84
|
+
>
|
85
|
+
{subitem.text}
|
86
|
+
</a>
|
87
|
+
{subitem.items && (
|
88
|
+
<ul>
|
89
|
+
{subitem.items.map((subsubitem: SidebarItem) => {
|
90
|
+
const isSubSubActive = isPathMatch(currentPath, subsubitem.href);
|
91
|
+
return (
|
92
|
+
<li>
|
93
|
+
<a
|
94
|
+
href={subsubitem.href}
|
95
|
+
class:list={[
|
96
|
+
"hover:bg-base-300",
|
97
|
+
{ "active": isSubSubActive }
|
98
|
+
]}
|
99
|
+
>
|
100
|
+
{subsubitem.text}
|
101
|
+
</a>
|
102
|
+
</li>
|
103
|
+
);
|
104
|
+
})}
|
105
|
+
</ul>
|
106
|
+
)}
|
107
|
+
</li>
|
108
|
+
);
|
109
|
+
})}
|
110
|
+
</ul>
|
111
|
+
)}
|
112
|
+
</li>
|
113
|
+
);
|
114
|
+
})}
|
115
|
+
</ul>
|
116
|
+
</div>
|
117
|
+
))}
|
118
|
+
</nav>
|
119
|
+
</aside>
|
@@ -5,6 +5,7 @@
|
|
5
5
|
* @description
|
6
6
|
* TableOfContents 组件是一个目录导航组件,用于显示页面内容的标题结构。
|
7
7
|
* 它会自动检测页面中的标题元素,生成目录列表,并在用户滚动页面时高亮当前可见的标题。
|
8
|
+
* 当页面只有一个标题或没有足够的标题结构时,组件会自动隐藏。
|
8
9
|
*
|
9
10
|
* @design
|
10
11
|
* 设计理念:
|
@@ -12,6 +13,7 @@
|
|
12
13
|
* 2. 上下文感知 - 自动高亮当前阅读位置,提供阅读进度反馈
|
13
14
|
* 3. 视觉层次 - 通过缩进和样式区分不同级别的标题
|
14
15
|
* 4. 响应式设计 - 在小屏幕设备上自动隐藏,优化空间利用
|
16
|
+
* 5. 智能显示 - 当页面结构不需要目录时自动隐藏
|
15
17
|
*
|
16
18
|
* @usage
|
17
19
|
* 基本用法:
|
@@ -72,6 +74,11 @@ interface Props {
|
|
72
74
|
* @default "main"
|
73
75
|
*/
|
74
76
|
containerSelector?: string;
|
77
|
+
/**
|
78
|
+
* 显示目录所需的最少标题数量
|
79
|
+
* @default 2
|
80
|
+
*/
|
81
|
+
minHeadings?: number;
|
75
82
|
}
|
76
83
|
|
77
84
|
const {
|
@@ -81,34 +88,52 @@ const {
|
|
81
88
|
maxDepth = 3,
|
82
89
|
title = '目录',
|
83
90
|
containerSelector = 'main',
|
91
|
+
minHeadings = 2,
|
84
92
|
} = Astro.props;
|
85
93
|
|
86
94
|
// 生成唯一ID,确保多个TOC实例不会冲突
|
87
95
|
const tocId = `toc-${Math.random().toString(36).substring(2, 9)}`;
|
88
96
|
---
|
89
97
|
|
90
|
-
<div class={`toc ${fixed ? '
|
91
|
-
<div class="
|
92
|
-
<div class="
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
98
|
+
<div class={`toc-container ${fixed ? 'fixed top-20 right-4 w-64' : 'w-full max-w-xs'} ${className}`} id={`${tocId}-container`} style="display: none;">
|
99
|
+
<div class="card bg-base-100 shadow-xl">
|
100
|
+
<div class="card-body p-4">
|
101
|
+
<div class="card-title text-lg font-bold mb-2">{title}</div>
|
102
|
+
<ul class="menu menu-sm" id={tocId}>
|
103
|
+
<!-- 目录项将通过 JavaScript 动态生成 -->
|
104
|
+
<li class="text-base-content/60">加载中...</li>
|
105
|
+
</ul>
|
106
|
+
</div>
|
97
107
|
</div>
|
98
108
|
</div>
|
99
109
|
|
100
|
-
<script define:vars={{ selector, maxDepth, tocId, containerSelector }}>
|
110
|
+
<script define:vars={{ selector, maxDepth, tocId, containerSelector, minHeadings }}>
|
101
111
|
// 在页面加载完成后生成目录
|
102
112
|
function generateTOC() {
|
103
113
|
// 使用指定的容器选择器查找内容容器
|
104
114
|
const container = document.querySelector(containerSelector);
|
105
115
|
if (!container) return;
|
106
116
|
|
107
|
-
//
|
108
|
-
const headings = container.querySelectorAll(selector)
|
117
|
+
// 排除 CodeExample 组件中的标题
|
118
|
+
const headings = Array.from(container.querySelectorAll(selector)).filter(heading => {
|
119
|
+
// 检查标题是否在 CodeExample 组件内
|
120
|
+
const isInCodeExample = heading.closest('.code-example') !== null;
|
121
|
+
return !isInCodeExample;
|
122
|
+
});
|
123
|
+
|
109
124
|
const tocList = document.getElementById(tocId);
|
125
|
+
const tocContainer = document.getElementById(`${tocId}-container`);
|
110
126
|
|
111
|
-
if (!tocList ||
|
127
|
+
if (!tocList || !tocContainer) return;
|
128
|
+
|
129
|
+
// 如果标题数量少于最小要求,保持隐藏状态并退出
|
130
|
+
if (headings.length < minHeadings) {
|
131
|
+
tocContainer.style.display = 'none';
|
132
|
+
return;
|
133
|
+
}
|
134
|
+
|
135
|
+
// 显示目录容器
|
136
|
+
tocContainer.style.display = 'block';
|
112
137
|
|
113
138
|
// 清空占位符
|
114
139
|
tocList.innerHTML = '';
|
@@ -142,20 +167,17 @@ const tocId = `toc-${Math.random().toString(36).substring(2, 9)}`;
|
|
142
167
|
if (level > maxDepth) return;
|
143
168
|
|
144
169
|
const listItem = document.createElement('li');
|
145
|
-
listItem.className = `toc-
|
146
|
-
listItem.dataset.headingId = heading.id;
|
170
|
+
listItem.className = `toc-level-${level}`;
|
171
|
+
listItem.dataset.headingId = heading.id;
|
147
172
|
|
148
173
|
const link = document.createElement('a');
|
149
174
|
link.href = `#${heading.id}`;
|
150
175
|
link.textContent = heading.textContent;
|
151
|
-
link.className = '
|
176
|
+
link.className = 'text-base-content/80 hover:text-primary transition-colors';
|
152
177
|
|
153
|
-
//
|
178
|
+
// 添加缩进
|
154
179
|
if (level === 3) {
|
155
|
-
|
156
|
-
prefix.className = 'toc-prefix';
|
157
|
-
prefix.innerHTML = '└─ ';
|
158
|
-
link.prepend(prefix);
|
180
|
+
link.style.paddingLeft = '1.5rem';
|
159
181
|
}
|
160
182
|
|
161
183
|
listItem.appendChild(link);
|
@@ -188,12 +210,10 @@ const tocId = `toc-${Math.random().toString(36).substring(2, 9)}`;
|
|
188
210
|
});
|
189
211
|
});
|
190
212
|
|
191
|
-
//
|
192
|
-
if (tocList.children.length
|
193
|
-
|
194
|
-
|
195
|
-
listItem.className = 'toc-empty';
|
196
|
-
tocList.appendChild(listItem);
|
213
|
+
// 如果没有找到标题或标题数量不足,隐藏目录
|
214
|
+
if (tocList.children.length < minHeadings) {
|
215
|
+
tocContainer.style.display = 'none';
|
216
|
+
return;
|
197
217
|
}
|
198
218
|
|
199
219
|
// 添加活动标题高亮
|
@@ -230,11 +250,14 @@ const tocId = `toc-${Math.random().toString(36).substring(2, 9)}`;
|
|
230
250
|
|
231
251
|
// 高亮当前可见的标题
|
232
252
|
function highlightActiveHeading() {
|
253
|
+
const tocContainer = document.getElementById(`${tocId}-container`);
|
254
|
+
if (!tocContainer || tocContainer.style.display === 'none') return;
|
255
|
+
|
233
256
|
const container = document.querySelector(containerSelector);
|
234
257
|
if (!container) return;
|
235
258
|
|
236
259
|
const headings = Array.from(container.querySelectorAll(selector));
|
237
|
-
if (headings.length
|
260
|
+
if (headings.length < minHeadings) return;
|
238
261
|
|
239
262
|
// 获取视口高度和滚动位置
|
240
263
|
const viewportHeight = window.innerHeight;
|
@@ -261,33 +284,24 @@ const tocId = `toc-${Math.random().toString(36).substring(2, 9)}`;
|
|
261
284
|
// 如果找到了最接近的标题,使用它的ID
|
262
285
|
if (closestHeading) {
|
263
286
|
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
287
|
} else if (headings.length > 0) {
|
274
288
|
// 如果没有找到,使用第一个标题
|
275
289
|
activeHeadingId = headings[0].id;
|
276
290
|
}
|
277
291
|
|
278
292
|
// 更新活动链接样式
|
279
|
-
const tocLinks = document.querySelectorAll(`#${tocId}
|
293
|
+
const tocLinks = document.querySelectorAll(`#${tocId} a`);
|
280
294
|
let hasActiveLink = false;
|
281
295
|
|
282
296
|
tocLinks.forEach(link => {
|
283
297
|
const href = link.getAttribute('href').substring(1);
|
284
298
|
|
285
299
|
// 清除所有高亮
|
286
|
-
link.classList.remove('
|
300
|
+
link.classList.remove('text-primary', 'font-medium');
|
287
301
|
|
288
302
|
// 添加当前活动标题的高亮
|
289
303
|
if (href === activeHeadingId) {
|
290
|
-
link.classList.add('
|
304
|
+
link.classList.add('text-primary', 'font-medium');
|
291
305
|
hasActiveLink = true;
|
292
306
|
|
293
307
|
// 添加一个动画效果,使高亮更明显
|
@@ -298,41 +312,26 @@ const tocId = `toc-${Math.random().toString(36).substring(2, 9)}`;
|
|
298
312
|
duration: 300,
|
299
313
|
easing: 'ease-out'
|
300
314
|
});
|
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
315
|
}
|
317
316
|
});
|
318
317
|
|
319
318
|
// 如果没有找到活动链接,尝试高亮第一个标题
|
320
319
|
if (!hasActiveLink && tocLinks.length > 0) {
|
321
|
-
tocLinks[0].classList.add('
|
320
|
+
tocLinks[0].classList.add('text-primary', 'font-medium');
|
322
321
|
}
|
323
322
|
|
324
323
|
// 确保活动链接在视图中可见
|
325
|
-
const activeLink = document.querySelector(`#${tocId} .
|
324
|
+
const activeLink = document.querySelector(`#${tocId} a.text-primary`);
|
326
325
|
if (activeLink && tocList) {
|
327
|
-
const
|
328
|
-
if (
|
326
|
+
const tocScrollContainer = document.querySelector('.fixed');
|
327
|
+
if (tocScrollContainer) {
|
329
328
|
const linkTop = activeLink.offsetTop;
|
330
|
-
const containerScrollTop =
|
331
|
-
const containerHeight =
|
329
|
+
const containerScrollTop = tocScrollContainer.scrollTop;
|
330
|
+
const containerHeight = tocScrollContainer.clientHeight;
|
332
331
|
|
333
332
|
// 如果链接不在可视区域内,滚动到可见位置
|
334
333
|
if (linkTop < containerScrollTop || linkTop > containerScrollTop + containerHeight) {
|
335
|
-
|
334
|
+
tocScrollContainer.scrollTop = linkTop - containerHeight / 2;
|
336
335
|
}
|
337
336
|
}
|
338
337
|
}
|
@@ -344,9 +343,13 @@ const tocId = `toc-${Math.random().toString(36).substring(2, 9)}`;
|
|
344
343
|
generateTOC();
|
345
344
|
|
346
345
|
// 添加resize事件监听,在窗口大小变化时重新计算高亮
|
347
|
-
window.addEventListener('resize', throttle(
|
346
|
+
window.addEventListener('resize', throttle(() => {
|
347
|
+
generateTOC(); // 重新生成TOC,以防窗口大小变化导致标题可见性变化
|
348
|
+
highlightActiveHeading();
|
349
|
+
}, 100));
|
348
350
|
|
349
351
|
// 立即触发一次高亮计算
|
352
|
+
setTimeout(generateTOC, 100);
|
350
353
|
setTimeout(highlightActiveHeading, 500);
|
351
354
|
</script>
|
352
355
|
|
@@ -135,10 +135,26 @@ const {
|
|
135
135
|
style = ""
|
136
136
|
} = Astro.props;
|
137
137
|
|
138
|
-
//
|
139
|
-
const
|
138
|
+
// 根据宽度设置对应的 Tailwind 类名
|
139
|
+
const widthClasses = {
|
140
|
+
narrow: 'max-w-2xl',
|
141
|
+
medium: 'max-w-4xl',
|
142
|
+
wide: 'max-w-6xl',
|
143
|
+
full: 'w-full'
|
144
|
+
}[width];
|
145
|
+
|
146
|
+
// 基础样式类
|
147
|
+
const baseClasses = [
|
148
|
+
'prose', // 使用 Tailwind Typography 插件的基础类
|
149
|
+
'prose-slate',
|
150
|
+
'w-full',
|
151
|
+
'mx-auto',
|
152
|
+
'dark:prose-invert', // 暗黑模式支持,
|
153
|
+
widthClasses,
|
154
|
+
className
|
155
|
+
];
|
140
156
|
---
|
141
157
|
|
142
|
-
<article class:list={
|
158
|
+
<article class:list={baseClasses} style={style}>
|
143
159
|
<slot />
|
144
160
|
</article>
|
@@ -0,0 +1,15 @@
|
|
1
|
+
/**
|
2
|
+
* 判断当前路径是否匹配目标路径
|
3
|
+
* @param currentPath 当前路径
|
4
|
+
* @param targetPath 目标路径
|
5
|
+
* @returns 是否匹配
|
6
|
+
*/
|
7
|
+
export function isPathMatch(currentPath: string, targetPath: string): boolean {
|
8
|
+
const debug = true
|
9
|
+
|
10
|
+
if (debug) {
|
11
|
+
console.log("🍋 isPathMatch", currentPath, targetPath);
|
12
|
+
}
|
13
|
+
|
14
|
+
return currentPath === targetPath || currentPath.endsWith(targetPath) || ("/" + currentPath).endsWith(targetPath);
|
15
|
+
}
|
package/package.json
CHANGED
@@ -1,11 +1,14 @@
|
|
1
1
|
{
|
2
2
|
"name": "@coffic/cosy-ui",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.3.0-beta.1",
|
4
4
|
"description": "An astro component library",
|
5
5
|
"author": {
|
6
6
|
"name": "nookery",
|
7
7
|
"url": "https://github.com/nookery"
|
8
8
|
},
|
9
|
+
"repository": {
|
10
|
+
"url": "https://github.com/CofficLab/cosy-ui"
|
11
|
+
},
|
9
12
|
"license": "MIT",
|
10
13
|
"keywords": [
|
11
14
|
"astro-integration",
|
@@ -28,8 +31,10 @@
|
|
28
31
|
"index.ts"
|
29
32
|
],
|
30
33
|
"scripts": {
|
31
|
-
"dev": "astro dev",
|
32
|
-
"build": "
|
34
|
+
"dev": "astro dev --host 0.0.0.0",
|
35
|
+
"build": "pnpm build:ui && pnpm build:docs",
|
36
|
+
"build:ui": "vite build && tsx scripts/post-build.ts",
|
37
|
+
"build:docs": "astro build"
|
33
38
|
},
|
34
39
|
"type": "module",
|
35
40
|
"peerDependencies": {
|
@@ -41,7 +46,9 @@
|
|
41
46
|
},
|
42
47
|
"devDependencies": {
|
43
48
|
"@astrojs/check": "^0.9.4",
|
49
|
+
"@astrojs/mdx": "^4.2.0",
|
44
50
|
"@astrojs/ts-plugin": "^1.10.4",
|
51
|
+
"@tailwindcss/typography": "^0.5.16",
|
45
52
|
"@tailwindcss/vite": "^4.0.14",
|
46
53
|
"@types/chai": "^5.2.0",
|
47
54
|
"@types/eslint": "^9.6.1",
|
@@ -65,4 +72,4 @@
|
|
65
72
|
"typescript": "^5.8.2",
|
66
73
|
"vite": "^6.2.2"
|
67
74
|
}
|
68
|
-
}
|
75
|
+
}
|