@wsxjs/wsx-press 0.0.18

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,262 @@
1
+ /**
2
+ * @jsxImportSource @wsxjs/wsx-core
3
+ * DocTOC Component
4
+ *
5
+ * Table of Contents component that displays TOC data generated by wsx-press plugin.
6
+ */
7
+
8
+ import { LightComponent, autoRegister, state } from "@wsxjs/wsx-core";
9
+ import { createLogger } from "@wsxjs/wsx-logger";
10
+ import { RouterUtils } from "@wsxjs/wsx-router";
11
+ import type { TOCItem } from "../../types";
12
+ import styles from "./DocTOC.css?inline";
13
+
14
+ const logger = createLogger("DocTOC");
15
+
16
+ /**
17
+ * TOC 数据缓存(全局共享)
18
+ */
19
+ const tocCache: {
20
+ data: Record<string, TOCItem[]> | null;
21
+ promise: Promise<Record<string, TOCItem[]>> | null;
22
+ } = {
23
+ data: null,
24
+ promise: null,
25
+ };
26
+
27
+ /**
28
+ * 加载 TOC 数据(带缓存)
29
+ */
30
+ async function loadTOCData(): Promise<Record<string, TOCItem[]>> {
31
+ if (tocCache.data) {
32
+ return tocCache.data;
33
+ }
34
+
35
+ if (tocCache.promise) {
36
+ return tocCache.promise;
37
+ }
38
+
39
+ tocCache.promise = (async () => {
40
+ try {
41
+ const response = await fetch("/.wsx-press/docs-toc.json");
42
+ if (!response.ok) {
43
+ throw new Error(`Failed to load TOC data: ${response.statusText}`);
44
+ }
45
+ const data = (await response.json()) as Record<string, TOCItem[]>;
46
+ tocCache.data = data;
47
+ tocCache.promise = null;
48
+ return data;
49
+ } catch (error) {
50
+ tocCache.promise = null;
51
+ throw error;
52
+ }
53
+ })();
54
+
55
+ return tocCache.promise;
56
+ }
57
+
58
+ /**
59
+ * DocTOC Component
60
+ *
61
+ * Displays table of contents using TOC data generated by wsx-press plugin.
62
+ */
63
+ @autoRegister({ tagName: "wsx-doc-toc" })
64
+ export default class DocTOC extends LightComponent {
65
+ @state private items: TOCItem[] = [];
66
+ @state private activeId: string = "";
67
+
68
+ private observer: IntersectionObserver | null = null;
69
+ private headingElements: Map<string, HTMLElement> = new Map();
70
+ private routeChangeUnsubscribe: (() => void) | null = null;
71
+
72
+ constructor() {
73
+ super({
74
+ styles,
75
+ styleName: "wsx-doc-toc",
76
+ });
77
+ }
78
+
79
+ protected async onConnected() {
80
+ // 加载 TOC 数据
81
+ await this.loadTOC();
82
+
83
+ // 监听路由变化
84
+ this.routeChangeUnsubscribe = RouterUtils.onRouteChange(() => {
85
+ this.loadTOC();
86
+ });
87
+ }
88
+
89
+ protected onDisconnected() {
90
+ if (this.observer) {
91
+ this.observer.disconnect();
92
+ this.observer = null;
93
+ }
94
+ if (this.routeChangeUnsubscribe) {
95
+ this.routeChangeUnsubscribe();
96
+ this.routeChangeUnsubscribe = null;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * 加载当前文档的 TOC 数据
102
+ */
103
+ private async loadTOC(): Promise<void> {
104
+ try {
105
+ const routeInfo = RouterUtils.getCurrentRoute();
106
+ const params = routeInfo.params as { category?: string; page?: string };
107
+
108
+ if (!params.category || !params.page) {
109
+ this.items = [];
110
+ return;
111
+ }
112
+
113
+ const docPath = `${params.category}/${params.page}`;
114
+ const tocData = await loadTOCData();
115
+ this.items = tocData[docPath] || [];
116
+
117
+ // 为标题元素设置 ID(如果还没有)
118
+ requestAnimationFrame(() => {
119
+ this.setupHeadingIds();
120
+ // 在标题 ID 设置完成后,重新设置滚动观察器
121
+ this.setupScrollObserver();
122
+ });
123
+ } catch (error) {
124
+ logger.error("Failed to load TOC", error);
125
+ this.items = [];
126
+ }
127
+ }
128
+
129
+ /**
130
+ * 为文档中的标题元素设置 ID
131
+ */
132
+ private setupHeadingIds(): void {
133
+ const docContent = document.querySelector(".doc-content");
134
+ if (!docContent) {
135
+ return;
136
+ }
137
+
138
+ // 查找所有标题元素(包括 wsx-marked-heading 和标准 h1-h6)
139
+ const headings = docContent.querySelectorAll("h1, h2, h3, h4, h5, h6, wsx-marked-heading");
140
+ this.headingElements.clear();
141
+
142
+ headings.forEach((heading) => {
143
+ // 获取标题文本
144
+ const text = heading.textContent?.trim() || "";
145
+ if (!text) return;
146
+
147
+ // 生成 ID
148
+ const id = this.generateId(text);
149
+
150
+ // 设置 ID
151
+ if (heading instanceof HTMLElement) {
152
+ heading.id = id;
153
+ this.headingElements.set(id, heading);
154
+ }
155
+ });
156
+ }
157
+
158
+ /**
159
+ * 生成锚点 ID
160
+ */
161
+ private generateId(text: string): string {
162
+ return text
163
+ .toLowerCase()
164
+ .replace(/[^\w\s-]/g, "")
165
+ .replace(/\s+/g, "-")
166
+ .replace(/-+/g, "-")
167
+ .trim();
168
+ }
169
+
170
+ /**
171
+ * 设置滚动观察器,高亮当前可见的标题
172
+ */
173
+ private setupScrollObserver(): void {
174
+ if (!("IntersectionObserver" in window)) {
175
+ return;
176
+ }
177
+
178
+ this.observer = new IntersectionObserver(
179
+ (entries) => {
180
+ // 找到最接近顶部的可见标题
181
+ const visibleEntries = entries.filter((entry) => entry.isIntersecting);
182
+ if (visibleEntries.length > 0) {
183
+ // 按位置排序,选择最接近顶部的
184
+ visibleEntries.sort((a, b) => {
185
+ const aTop = a.boundingClientRect.top;
186
+ const bTop = b.boundingClientRect.top;
187
+ return Math.abs(aTop) - Math.abs(bTop);
188
+ });
189
+ this.activeId = visibleEntries[0].target.id;
190
+ }
191
+ },
192
+ {
193
+ rootMargin: "-100px 0px -80% 0px",
194
+ threshold: 0,
195
+ }
196
+ );
197
+
198
+ // 观察所有标题元素
199
+ this.headingElements.forEach((element) => {
200
+ this.observer?.observe(element);
201
+ });
202
+ }
203
+
204
+ /**
205
+ * 处理 TOC 项点击
206
+ */
207
+ private handleTOCClick = (id: string, e: Event): void => {
208
+ e.preventDefault();
209
+ const element = this.headingElements.get(id);
210
+ if (element) {
211
+ element.scrollIntoView({ behavior: "smooth", block: "start" });
212
+ // 更新 URL hash(不触发滚动)
213
+ window.history.replaceState(null, "", `#${id}`);
214
+ }
215
+ };
216
+
217
+ /**
218
+ * 渲染 TOC 项
219
+ */
220
+ private renderTOCItem(item: TOCItem): HTMLElement {
221
+ const isActive = this.activeId === item.id;
222
+ const hasChildren = item.children.length > 0;
223
+
224
+ return (
225
+ <li class={`doc-toc-item doc-toc-item-level-${item.level}`}>
226
+ <a
227
+ href={`#${item.id}`}
228
+ class={`doc-toc-link ${isActive ? "active" : ""}`}
229
+ onClick={(e) => this.handleTOCClick(item.id, e)}
230
+ >
231
+ {item.text}
232
+ </a>
233
+ {hasChildren && (
234
+ <ul class="doc-toc-list doc-toc-sublist">
235
+ {item.children.map((child) => this.renderTOCItem(child))}
236
+ </ul>
237
+ )}
238
+ </li>
239
+ ) as HTMLElement;
240
+ }
241
+
242
+ render() {
243
+ if (this.items.length === 0) {
244
+ return (
245
+ <aside class="doc-toc">
246
+ <div class="doc-toc-empty">暂无目录</div>
247
+ </aside>
248
+ );
249
+ }
250
+
251
+ return (
252
+ <aside class="doc-toc">
253
+ <h3 class="doc-toc-title">目录</h3>
254
+ <nav class="doc-toc-nav">
255
+ <ul class="doc-toc-list">
256
+ {this.items.map((item) => this.renderTOCItem(item))}
257
+ </ul>
258
+ </nav>
259
+ </aside>
260
+ );
261
+ }
262
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @wsxjs/wsx-press/client
3
+ * Client-side components entry point
4
+ *
5
+ * IMPORTANT: Import components directly (not just export) to ensure
6
+ * @autoRegister decorators are executed and components are registered.
7
+ */
8
+
9
+ // Import DocPage component (triggers @autoRegister decorator)
10
+ import "./components/DocPage.wsx";
11
+
12
+ // Import DocSearch component (triggers @autoRegister decorator)
13
+ import "./components/DocSearch.wsx";
14
+
15
+ // Import DocLayout component (triggers @autoRegister decorator)
16
+ import "./components/DocLayout.wsx";
17
+
18
+ // Import DocSidebar component (triggers @autoRegister decorator)
19
+ import "./components/DocSidebar.wsx";
20
+
21
+ // Import DocTOC component (triggers @autoRegister decorator)
22
+ import "./components/DocTOC.wsx";
23
+
24
+ // Export components for type checking and explicit usage
25
+ export { default as DocPage } from "./components/DocPage.wsx";
26
+ export { default as DocSearch } from "./components/DocSearch.wsx";
27
+ export { default as DocLayout } from "./components/DocLayout.wsx";
28
+ export { default as DocSidebar } from "./components/DocSidebar.wsx";
29
+ export { default as DocTOC } from "./components/DocTOC.wsx";
30
+
31
+ // Components will be exported here as they are implemented
32
+ // export { DocBreadcrumb } from "./components/DocBreadcrumb.wsx";
@@ -0,0 +1,242 @@
1
+ /**
2
+ * WSX-Press 代码高亮样式
3
+ * 基于 GitHub 主题
4
+ */
5
+
6
+ /* ===== 代码块容器 ===== */
7
+ .wsx-press-code-block {
8
+ position: relative;
9
+ margin: var(--wsx-press-spacing-lg) 0;
10
+ }
11
+
12
+ .wsx-press-code-block pre {
13
+ margin: 0;
14
+ border-radius: var(--wsx-press-radius-md);
15
+ }
16
+
17
+ /* ===== 语言标签 ===== */
18
+ .wsx-press-code-block[data-language]::before {
19
+ content: attr(data-language);
20
+ position: absolute;
21
+ top: var(--wsx-press-spacing-sm);
22
+ right: var(--wsx-press-spacing-md);
23
+ padding: 0.2em 0.6em;
24
+ font-family: var(--wsx-press-font-mono);
25
+ font-size: var(--wsx-press-font-size-xs);
26
+ color: var(--wsx-press-text-tertiary);
27
+ background: var(--wsx-press-bg-tertiary);
28
+ border: 1px solid var(--wsx-press-border);
29
+ border-radius: var(--wsx-press-radius-sm);
30
+ text-transform: uppercase;
31
+ letter-spacing: 0.5px;
32
+ }
33
+
34
+ /* ===== 行号 ===== */
35
+ .wsx-press-code-line-numbers {
36
+ display: flex;
37
+ }
38
+
39
+ .wsx-press-code-line-numbers .line-numbers {
40
+ flex-shrink: 0;
41
+ padding-right: var(--wsx-press-spacing-md);
42
+ margin-right: var(--wsx-press-spacing-md);
43
+ border-right: 1px solid var(--wsx-press-border);
44
+ color: var(--wsx-press-text-tertiary);
45
+ user-select: none;
46
+ text-align: right;
47
+ }
48
+
49
+ .wsx-press-code-line-numbers .line-content {
50
+ flex: 1;
51
+ overflow-x: auto;
52
+ }
53
+
54
+ /* ===== 高亮行 ===== */
55
+ .wsx-press-code-line.highlighted {
56
+ background: var(--wsx-press-bg-tertiary);
57
+ border-left: 4px solid var(--wsx-press-accent);
58
+ padding-left: calc(var(--wsx-press-spacing-sm) - 4px);
59
+ margin-left: calc(var(--wsx-press-spacing-sm) * -1);
60
+ }
61
+
62
+ /* ===== 差异视图 ===== */
63
+ .wsx-press-code-line.diff-add {
64
+ background: rgba(46, 160, 67, 0.15);
65
+ border-left: 4px solid #2ea043;
66
+ }
67
+
68
+ .wsx-press-code-line.diff-remove {
69
+ background: rgba(248, 81, 73, 0.15);
70
+ border-left: 4px solid #f85149;
71
+ }
72
+
73
+ /* ===== 语法高亮(亮色模式) ===== */
74
+ .wsx-press-code .token.comment,
75
+ .wsx-press-code .token.prolog,
76
+ .wsx-press-code .token.doctype,
77
+ .wsx-press-code .token.cdata {
78
+ color: #6a737d;
79
+ }
80
+
81
+ .wsx-press-code .token.punctuation {
82
+ color: #24292e;
83
+ }
84
+
85
+ .wsx-press-code .token.property,
86
+ .wsx-press-code .token.tag,
87
+ .wsx-press-code .token.boolean,
88
+ .wsx-press-code .token.number,
89
+ .wsx-press-code .token.constant,
90
+ .wsx-press-code .token.symbol,
91
+ .wsx-press-code .token.deleted {
92
+ color: #005cc5;
93
+ }
94
+
95
+ .wsx-press-code .token.selector,
96
+ .wsx-press-code .token.attr-name,
97
+ .wsx-press-code .token.string,
98
+ .wsx-press-code .token.char,
99
+ .wsx-press-code .token.builtin,
100
+ .wsx-press-code .token.inserted {
101
+ color: #032f62;
102
+ }
103
+
104
+ .wsx-press-code .token.operator,
105
+ .wsx-press-code .token.entity,
106
+ .wsx-press-code .token.url,
107
+ .wsx-press-code .language-css .token.string,
108
+ .wsx-press-code .style .token.string {
109
+ color: #d73a49;
110
+ }
111
+
112
+ .wsx-press-code .token.atrule,
113
+ .wsx-press-code .token.attr-value,
114
+ .wsx-press-code .token.keyword {
115
+ color: #d73a49;
116
+ }
117
+
118
+ .wsx-press-code .token.function,
119
+ .wsx-press-code .token.class-name {
120
+ color: #6f42c1;
121
+ }
122
+
123
+ .wsx-press-code .token.regex,
124
+ .wsx-press-code .token.important,
125
+ .wsx-press-code .token.variable {
126
+ color: #e36209;
127
+ }
128
+
129
+ /* ===== 语法高亮(暗色模式) ===== */
130
+ @media (prefers-color-scheme: dark) {
131
+ .wsx-press-code .token.comment,
132
+ .wsx-press-code .token.prolog,
133
+ .wsx-press-code .token.doctype,
134
+ .wsx-press-code .token.cdata {
135
+ color: #8b949e;
136
+ }
137
+
138
+ .wsx-press-code .token.punctuation {
139
+ color: #c9d1d9;
140
+ }
141
+
142
+ .wsx-press-code .token.property,
143
+ .wsx-press-code .token.tag,
144
+ .wsx-press-code .token.boolean,
145
+ .wsx-press-code .token.number,
146
+ .wsx-press-code .token.constant,
147
+ .wsx-press-code .token.symbol,
148
+ .wsx-press-code .token.deleted {
149
+ color: #79c0ff;
150
+ }
151
+
152
+ .wsx-press-code .token.selector,
153
+ .wsx-press-code .token.attr-name,
154
+ .wsx-press-code .token.string,
155
+ .wsx-press-code .token.char,
156
+ .wsx-press-code .token.builtin,
157
+ .wsx-press-code .token.inserted {
158
+ color: #a5d6ff;
159
+ }
160
+
161
+ .wsx-press-code .token.operator,
162
+ .wsx-press-code .token.entity,
163
+ .wsx-press-code .token.url,
164
+ .wsx-press-code .language-css .token.string,
165
+ .wsx-press-code .style .token.string {
166
+ color: #ff7b72;
167
+ }
168
+
169
+ .wsx-press-code .token.atrule,
170
+ .wsx-press-code .token.attr-value,
171
+ .wsx-press-code .token.keyword {
172
+ color: #ff7b72;
173
+ }
174
+
175
+ .wsx-press-code .token.function,
176
+ .wsx-press-code .token.class-name {
177
+ color: #d2a8ff;
178
+ }
179
+
180
+ .wsx-press-code .token.regex,
181
+ .wsx-press-code .token.important,
182
+ .wsx-press-code .token.variable {
183
+ color: #ffa657;
184
+ }
185
+ }
186
+
187
+ [data-theme="dark"] {
188
+ .wsx-press-code .token.comment,
189
+ .wsx-press-code .token.prolog,
190
+ .wsx-press-code .token.doctype,
191
+ .wsx-press-code .token.cdata {
192
+ color: #8b949e;
193
+ }
194
+
195
+ .wsx-press-code .token.punctuation {
196
+ color: #c9d1d9;
197
+ }
198
+
199
+ .wsx-press-code .token.property,
200
+ .wsx-press-code .token.tag,
201
+ .wsx-press-code .token.boolean,
202
+ .wsx-press-code .token.number,
203
+ .wsx-press-code .token.constant,
204
+ .wsx-press-code .token.symbol,
205
+ .wsx-press-code .token.deleted {
206
+ color: #79c0ff;
207
+ }
208
+
209
+ .wsx-press-code .token.selector,
210
+ .wsx-press-code .token.attr-name,
211
+ .wsx-press-code .token.string,
212
+ .wsx-press-code .token.char,
213
+ .wsx-press-code .token.builtin,
214
+ .wsx-press-code .token.inserted {
215
+ color: #a5d6ff;
216
+ }
217
+
218
+ .wsx-press-code .token.operator,
219
+ .wsx-press-code .token.entity,
220
+ .wsx-press-code .token.url,
221
+ .wsx-press-code .language-css .token.string,
222
+ .wsx-press-code .style .token.string {
223
+ color: #ff7b72;
224
+ }
225
+
226
+ .wsx-press-code .token.atrule,
227
+ .wsx-press-code .token.attr-value,
228
+ .wsx-press-code .token.keyword {
229
+ color: #ff7b72;
230
+ }
231
+
232
+ .wsx-press-code .token.function,
233
+ .wsx-press-code .token.class-name {
234
+ color: #d2a8ff;
235
+ }
236
+
237
+ .wsx-press-code .token.regex,
238
+ .wsx-press-code .token.important,
239
+ .wsx-press-code .token.variable {
240
+ color: #ffa657;
241
+ }
242
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * WSX-Press 样式入口
3
+ * 导入所有全局样式
4
+ */
5
+
6
+ /* 基础样式 */
7
+ @import './reset.css';
8
+ @import './theme.css';
9
+
10
+ /* 内容样式 */
11
+ @import './typography.css';
12
+ @import './code.css';
@@ -0,0 +1,116 @@
1
+ /**
2
+ * WSX-Press CSS Reset
3
+ * 轻量级重置,确保跨浏览器一致性
4
+ */
5
+
6
+ /* ===== Box sizing ===== */
7
+ .wsx-press-reset *,
8
+ .wsx-press-reset *::before,
9
+ .wsx-press-reset *::after {
10
+ box-sizing: border-box;
11
+ }
12
+
13
+ /* ===== 移除默认 margin ===== */
14
+ .wsx-press-reset h1,
15
+ .wsx-press-reset h2,
16
+ .wsx-press-reset h3,
17
+ .wsx-press-reset h4,
18
+ .wsx-press-reset h5,
19
+ .wsx-press-reset h6,
20
+ .wsx-press-reset p,
21
+ .wsx-press-reset ul,
22
+ .wsx-press-reset ol,
23
+ .wsx-press-reset dl,
24
+ .wsx-press-reset dd,
25
+ .wsx-press-reset blockquote,
26
+ .wsx-press-reset figure,
27
+ .wsx-press-reset pre {
28
+ margin: 0;
29
+ }
30
+
31
+ /* ===== 移除列表样式 ===== */
32
+ .wsx-press-reset ul,
33
+ .wsx-press-reset ol {
34
+ padding: 0;
35
+ list-style: none;
36
+ }
37
+
38
+ /* ===== 重置 blockquote ===== */
39
+ .wsx-press-reset blockquote {
40
+ padding: 0;
41
+ quotes: none;
42
+ }
43
+
44
+ /* ===== 表格重置 ===== */
45
+ .wsx-press-reset table {
46
+ border-collapse: collapse;
47
+ border-spacing: 0;
48
+ }
49
+
50
+ /* ===== 按钮重置 ===== */
51
+ .wsx-press-reset button {
52
+ background: none;
53
+ border: none;
54
+ padding: 0;
55
+ font: inherit;
56
+ color: inherit;
57
+ cursor: pointer;
58
+ }
59
+
60
+ .wsx-press-reset button:disabled {
61
+ cursor: not-allowed;
62
+ }
63
+
64
+ /* ===== 输入框重置 ===== */
65
+ .wsx-press-reset input,
66
+ .wsx-press-reset textarea,
67
+ .wsx-press-reset select {
68
+ font: inherit;
69
+ color: inherit;
70
+ }
71
+
72
+ /* ===== 链接重置 ===== */
73
+ .wsx-press-reset a {
74
+ color: inherit;
75
+ text-decoration: inherit;
76
+ }
77
+
78
+ /* ===== 图片重置 ===== */
79
+ .wsx-press-reset img,
80
+ .wsx-press-reset svg {
81
+ display: block;
82
+ max-width: 100%;
83
+ }
84
+
85
+ /* ===== Code 重置 ===== */
86
+ .wsx-press-reset code,
87
+ .wsx-press-reset kbd,
88
+ .wsx-press-reset samp,
89
+ .wsx-press-reset pre {
90
+ font-family: var(--wsx-press-font-mono, monospace);
91
+ }
92
+
93
+ /* ===== 滚动条样式(可选) ===== */
94
+ .wsx-press-reset ::-webkit-scrollbar {
95
+ width: 8px;
96
+ height: 8px;
97
+ }
98
+
99
+ .wsx-press-reset ::-webkit-scrollbar-track {
100
+ background: var(--wsx-press-bg-secondary);
101
+ }
102
+
103
+ .wsx-press-reset ::-webkit-scrollbar-thumb {
104
+ background: var(--wsx-press-border-hover);
105
+ border-radius: var(--wsx-press-radius-sm);
106
+ }
107
+
108
+ .wsx-press-reset ::-webkit-scrollbar-thumb:hover {
109
+ background: var(--wsx-press-text-tertiary);
110
+ }
111
+
112
+ /* Firefox 滚动条 */
113
+ .wsx-press-reset * {
114
+ scrollbar-width: thin;
115
+ scrollbar-color: var(--wsx-press-border-hover) var(--wsx-press-bg-secondary);
116
+ }