@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.
@@ -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 ? '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>
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 || headings.length === 0) return;
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-item toc-level-${level}`;
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 = 'toc-link';
176
+ link.className = 'text-base-content/80 hover:text-primary transition-colors';
152
177
 
153
- // 添加前缀标记,增强视觉层次
178
+ // 添加缩进
154
179
  if (level === 3) {
155
- const prefix = document.createElement('span');
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 === 0) {
193
- const listItem = document.createElement('li');
194
- listItem.textContent = '没有找到标题';
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 === 0) return;
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} .toc-link`);
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('toc-active', 'toc-parent-active');
300
+ link.classList.remove('text-primary', 'font-medium');
287
301
 
288
302
  // 添加当前活动标题的高亮
289
303
  if (href === activeHeadingId) {
290
- link.classList.add('toc-active');
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('toc-active');
320
+ tocLinks[0].classList.add('text-primary', 'font-medium');
322
321
  }
323
322
 
324
323
  // 确保活动链接在视图中可见
325
- const activeLink = document.querySelector(`#${tocId} .toc-active`);
324
+ const activeLink = document.querySelector(`#${tocId} a.text-primary`);
326
325
  if (activeLink && tocList) {
327
- const tocContainer = document.querySelector('.toc-fixed');
328
- if (tocContainer) {
326
+ const tocScrollContainer = document.querySelector('.fixed');
327
+ if (tocScrollContainer) {
329
328
  const linkTop = activeLink.offsetTop;
330
- const containerScrollTop = tocContainer.scrollTop;
331
- const containerHeight = tocContainer.clientHeight;
329
+ const containerScrollTop = tocScrollContainer.scrollTop;
330
+ const containerHeight = tocScrollContainer.clientHeight;
332
331
 
333
332
  // 如果链接不在可视区域内,滚动到可见位置
334
333
  if (linkTop < containerScrollTop || linkTop > containerScrollTop + containerHeight) {
335
- tocContainer.scrollTop = linkTop - containerHeight / 2;
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(highlightActiveHeading, 100));
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 widthClass = `article-width-${width}`;
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={['article', widthClass, className]} style={style}>
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.2.2",
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": "vite build && tsx scripts/post-build.ts"
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
+ }