@yl_lowcode/docs-theme 0.0.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/demo.vue ADDED
@@ -0,0 +1,267 @@
1
+ <script setup lang="ts">
2
+ // @ts-ignore
3
+ import { ref, onMounted, onBeforeUnmount } from "vue";
4
+ import { withBase } from 'vitepress'
5
+
6
+ // @ts-ignore
7
+ const props = defineProps<{
8
+ component: any;
9
+ code: { raw: string; html: string; srcPath: string };
10
+ title?: string;
11
+ }>();
12
+
13
+ const previewRef = ref<HTMLElement | null>(null);
14
+ const expanded = ref(false);
15
+ const copied = ref(false);
16
+ let previewRoot: any = null;
17
+ let copyTimer: ReturnType<typeof setTimeout> | null = null;
18
+
19
+ async function mountPreview() {
20
+ if (!previewRef.value) return;
21
+ const [{ default: React }, { createRoot }] = await Promise.all([
22
+ import("react"),
23
+ import("react-dom/client"),
24
+ ]);
25
+ previewRoot?.unmount();
26
+ previewRoot = createRoot(previewRef.value);
27
+ previewRoot.render(React.createElement(props.component));
28
+ }
29
+
30
+ function openLive() {
31
+ const url = `/playground.html?demo=${props.code.srcPath}`;
32
+ window.open(withBase(url), "_blank");
33
+ }
34
+
35
+ function toggleExpand() {
36
+ expanded.value = !expanded.value;
37
+ }
38
+
39
+ async function copyCode() {
40
+ await navigator.clipboard.writeText(props.code.raw);
41
+ copied.value = true;
42
+ if (copyTimer) clearTimeout(copyTimer);
43
+ copyTimer = setTimeout(() => {
44
+ copied.value = false;
45
+ }, 2000);
46
+ }
47
+
48
+ onMounted(() => mountPreview());
49
+ onBeforeUnmount(() => previewRoot?.unmount());
50
+ </script>
51
+
52
+ <template>
53
+ <div class="aui-demo">
54
+ <!-- 预览区 -->
55
+ <div class="aui-demo-preview" ref="previewRef" />
56
+
57
+ <!-- 底部操作栏 -->
58
+ <div class="aui-demo-footer">
59
+ <span class="aui-demo-desc">{{ title ?? "" }}</span>
60
+ <div class="aui-demo-actions">
61
+ <!-- 在线调试(新 Tab) -->
62
+ <button class="aui-action-btn" title="在线调试" @click="openLive">
63
+ <svg viewBox="0 0 1024 1024" width="16" height="16">
64
+ <path
65
+ d="M746.222933 102.239573l-359.799466 330.820267L185.347413 281.4976 102.2464 329.864533l198.20544 182.132054-198.20544 182.132053 83.101013 48.510293 201.076054-151.558826 359.799466 330.676906 175.527254-85.251413V187.4944z m0 217.57952v384.341334l-255.040853-192.177494z"
66
+ fill="currentColor"
67
+ />
68
+ </svg>
69
+ </button>
70
+ <!-- 展开/收起代码 -->
71
+ <button
72
+ class="aui-action-btn"
73
+ :class="{ active: expanded }"
74
+ :title="expanded ? '收起代码' : '展开代码'"
75
+ @click="toggleExpand"
76
+ >
77
+ <svg
78
+ width="15"
79
+ height="15"
80
+ viewBox="0 0 24 24"
81
+ fill="none"
82
+ stroke="currentColor"
83
+ stroke-width="2"
84
+ stroke-linecap="round"
85
+ stroke-linejoin="round"
86
+ >
87
+ <polyline v-if="!expanded" points="6 9 12 15 18 9" />
88
+ <polyline v-else points="18 15 12 9 6 15" />
89
+ </svg>
90
+ </button>
91
+ </div>
92
+ </div>
93
+
94
+ <!-- 代码区(可展开) -->
95
+ <div v-show="expanded" class="aui-demo-code">
96
+ <span class="aui-lang-label">tsx</span>
97
+ <button
98
+ class="aui-copy-btn"
99
+ :class="{ copied }"
100
+ @click="copyCode"
101
+ title="复制代码"
102
+ >
103
+ <span v-if="copied" class="aui-copy-label">已复制</span>
104
+ <svg
105
+ v-if="copied"
106
+ viewBox="0 0 24 24"
107
+ width="20"
108
+ height="20"
109
+ fill="none"
110
+ stroke="currentColor"
111
+ stroke-width="2"
112
+ stroke-linecap="round"
113
+ stroke-linejoin="round"
114
+ >
115
+ <path
116
+ d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"
117
+ />
118
+ <rect x="9" y="3" width="6" height="4" rx="1" />
119
+ <polyline points="9 12 11 14 15 10" />
120
+ </svg>
121
+ <svg
122
+ v-else
123
+ viewBox="0 0 24 24"
124
+ width="20"
125
+ height="20"
126
+ fill="none"
127
+ stroke="currentColor"
128
+ stroke-width="2"
129
+ stroke-linecap="round"
130
+ stroke-linejoin="round"
131
+ >
132
+ <path
133
+ d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"
134
+ />
135
+ <rect x="9" y="3" width="6" height="4" rx="1" />
136
+ </svg>
137
+ </button>
138
+ <div v-html="code.html" />
139
+ </div>
140
+ </div>
141
+ </template>
142
+
143
+ <style scoped>
144
+ .aui-demo {
145
+ border: 1px solid var(--vp-c-divider);
146
+ border-radius: 10px;
147
+ overflow: hidden;
148
+ margin: 20px 0;
149
+ }
150
+
151
+ .aui-demo-preview {
152
+ padding: 32px 24px;
153
+ min-height: 80px;
154
+ background: var(--vp-c-bg);
155
+ }
156
+
157
+ .aui-demo-footer {
158
+ display: flex;
159
+ align-items: center;
160
+ justify-content: space-between;
161
+ gap: 12px;
162
+ padding: 6px 8px 6px 16px;
163
+ min-height: 40px;
164
+ border-top: 1px solid var(--vp-c-divider);
165
+ background: var(--vp-c-bg-soft);
166
+ }
167
+
168
+ .aui-demo-desc {
169
+ flex: 1;
170
+ font-size: 13px;
171
+ color: var(--vp-c-text-2);
172
+ line-height: 1.6;
173
+ }
174
+
175
+ .aui-demo-actions {
176
+ flex-shrink: 0;
177
+ display: flex;
178
+ align-items: center;
179
+ gap: 4px;
180
+ }
181
+
182
+ .aui-action-btn {
183
+ display: flex;
184
+ align-items: center;
185
+ justify-content: center;
186
+ width: 28px;
187
+ height: 28px;
188
+ border: none;
189
+ border-radius: 6px;
190
+ background: transparent;
191
+ color: var(--vp-c-text-3);
192
+ cursor: pointer;
193
+ transition: background 0.15s, color 0.15s;
194
+ }
195
+
196
+ .aui-action-btn:hover {
197
+ background: var(--vp-c-bg-mute);
198
+ color: var(--vp-c-text-1);
199
+ }
200
+
201
+ .aui-action-btn.active {
202
+ background: var(--vp-c-brand-soft);
203
+ color: var(--vp-c-brand-1);
204
+ }
205
+
206
+ .aui-demo-code {
207
+ position: relative;
208
+ border-top: 1px solid var(--vp-c-divider);
209
+ }
210
+
211
+ .aui-demo-code :deep(pre) {
212
+ margin: 0;
213
+ border-radius: 0;
214
+ }
215
+
216
+ .aui-lang-label {
217
+ position: absolute;
218
+ top: 2px;
219
+ right: 12px;
220
+ z-index: 2;
221
+ font-size: 12px;
222
+ font-weight: 500;
223
+ user-select: none;
224
+ color: var(--vp-code-lang-color);
225
+ transition: color 0.4s, opacity 0.4s;
226
+ }
227
+
228
+ .aui-demo-code:hover .aui-lang-label {
229
+ opacity: 0;
230
+ }
231
+
232
+ .aui-copy-btn {
233
+ position: absolute;
234
+ top: 8px;
235
+ right: 8px;
236
+ z-index: 10;
237
+ display: flex;
238
+ align-items: center;
239
+ gap: 6px;
240
+ padding: 6px 10px;
241
+ border: 1px solid var(--vp-c-divider);
242
+ border-radius: 8px;
243
+ background: var(--vp-c-bg);
244
+ color: var(--vp-c-text-2);
245
+ font-size: 13px;
246
+ cursor: pointer;
247
+ transition: color 0.15s, border-color 0.15s, background 0.15s,
248
+ box-shadow 0.15s;
249
+ opacity: 0;
250
+ }
251
+
252
+ .aui-demo-code:hover .aui-copy-btn {
253
+ opacity: 1;
254
+ }
255
+
256
+ .aui-copy-btn:hover {
257
+ color: var(--vp-c-text-1);
258
+ border-color: var(--vp-c-brand-1);
259
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
260
+ }
261
+
262
+ .aui-copy-btn.copied {
263
+ opacity: 1;
264
+ color: var(--vp-c-brand-1);
265
+ border-color: var(--vp-c-brand-1);
266
+ }
267
+ </style>
package/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ import DefaultTheme from "vitepress/theme";
2
+ import Layout from "./layout.vue";
3
+ import Demo from "./demo.vue";
4
+ import "./styles/vars.css";
5
+ import "./styles/custom.css";
6
+
7
+ export default {
8
+ extends: DefaultTheme,
9
+ Layout,
10
+ enhanceApp({ app }: { app: import("vue").App }) {
11
+ app.component("Demo", Demo);
12
+ },
13
+ };
package/layout.vue ADDED
@@ -0,0 +1,263 @@
1
+ <script setup lang="ts">
2
+ // @ts-ignore
3
+ import { useData } from "vitepress";
4
+ // @ts-ignore
5
+ import DefaultTheme from "vitepress/theme";
6
+ // @ts-ignore
7
+ import Playground from "./playground.vue";
8
+ // @ts-ignore
9
+ import { nextTick, provide } from "vue";
10
+
11
+ const { frontmatter, isDark } = useData();
12
+
13
+ const enableTransitions = () =>
14
+ "startViewTransition" in document &&
15
+ window.matchMedia("(prefers-reduced-motion: no-preference)").matches;
16
+
17
+ provide("toggle-appearance", async ({ clientX: x, clientY: y }: MouseEvent) => {
18
+ if (!enableTransitions()) {
19
+ isDark.value = !isDark.value;
20
+ return;
21
+ }
22
+
23
+ const clipPath = [
24
+ `circle(0px at ${x}px ${y}px)`,
25
+ `circle(${Math.hypot(
26
+ Math.max(x, innerWidth - x),
27
+ Math.max(y, innerHeight - y)
28
+ )}px at ${x}px ${y}px)`,
29
+ ];
30
+ // @ts-ignore
31
+ await document.startViewTransition(async () => {
32
+ isDark.value = !isDark.value;
33
+ await nextTick();
34
+ }).ready;
35
+
36
+ document.documentElement.animate(
37
+ { clipPath: isDark.value ? clipPath.reverse() : clipPath },
38
+ {
39
+ duration: 300,
40
+ easing: "ease-in",
41
+ fill: "forwards",
42
+ pseudoElement: `::view-transition-${isDark.value ? "old" : "new"}(root)`,
43
+ }
44
+ );
45
+ });
46
+ </script>
47
+
48
+ <template>
49
+ <Playground v-if="frontmatter.layout === 'playground'" />
50
+ <DefaultTheme.Layout v-else>
51
+ <template #nav-bar-content-after>
52
+ <slot name="nav-bar-content-after" />
53
+ </template>
54
+ <template #sidebar-nav-before>
55
+ <slot name="sidebar-nav-before" />
56
+ </template>
57
+ </DefaultTheme.Layout>
58
+ </template>
59
+
60
+ <style>
61
+ ::view-transition-old(root),
62
+ ::view-transition-new(root) {
63
+ animation: none;
64
+ mix-blend-mode: normal;
65
+ }
66
+
67
+ ::view-transition-old(root),
68
+ .dark::view-transition-new(root) {
69
+ z-index: 1;
70
+ }
71
+
72
+ ::view-transition-new(root),
73
+ .dark::view-transition-old(root) {
74
+ z-index: 9999;
75
+ }
76
+
77
+ .VPSwitchAppearance {
78
+ width: 22px !important;
79
+ }
80
+
81
+ .VPSwitchAppearance .check {
82
+ transform: none !important;
83
+ }
84
+
85
+ /** 自定义样式 */
86
+ h3 {
87
+ margin-bottom: 16px !important;
88
+ }
89
+
90
+ table {
91
+ display: table !important;
92
+ margin: 0 !important;
93
+ }
94
+
95
+ a {
96
+ text-decoration: none !important;
97
+ }
98
+
99
+ .vp-doc li+li {
100
+ margin-top: 0 !important;
101
+ }
102
+
103
+ .vp-doc th,
104
+ .vp-doc td {
105
+ border: none;
106
+ }
107
+
108
+ .vp-doc ul,
109
+ .vp-doc ol {
110
+ padding-left: 0 !important;
111
+ margin: 0 !important;
112
+ }
113
+
114
+ .vp-doc blockquote {
115
+ border-left: 2px solid var(--primary-color);
116
+ padding-left: 10px;
117
+ color: var(--vp-c-text-1) !important;
118
+ }
119
+
120
+ #api+table {
121
+ width: 100%;
122
+ }
123
+
124
+ blockquote+table {
125
+ width: 100%;
126
+ }
127
+
128
+ p.text {
129
+ margin-bottom: 1em;
130
+ }
131
+
132
+ h2.text {
133
+ margin-top: 20px;
134
+ margin-bottom: 0.5em;
135
+ }
136
+
137
+ /* 调整样式 */
138
+ body {
139
+ --vp-code-font-size: 12px !important;
140
+ }
141
+
142
+ .VPDoc.has-aside .content-container {
143
+ max-width: 940px;
144
+ }
145
+
146
+ .VPDoc .content {
147
+ min-width: 940px;
148
+ padding: 0;
149
+ }
150
+
151
+ .VPDoc .content .content-container {
152
+ max-width: 100% !important;
153
+ }
154
+
155
+ .VPDoc .prev-next {
156
+ padding-bottom: 40px !important;
157
+ }
158
+
159
+ .VPDocAside .content {
160
+ padding-left: 10px;
161
+ }
162
+
163
+ .VPHomeHero .text {
164
+ margin-top: 12px !important;
165
+ }
166
+
167
+ #VPContent.has-sidebar {
168
+ padding-right: 0;
169
+ padding-left: 260px;
170
+ }
171
+
172
+ .VPSidebar {
173
+ padding-left: 60px !important;
174
+ width: 280px !important;
175
+ }
176
+
177
+ .VPNavBar.has-sidebar .container>.title {
178
+ padding-left: 60px !important;
179
+ width: 280px !important;
180
+ }
181
+
182
+ .VPNavBar.has-sidebar .content {
183
+ padding-left: 280px !important;
184
+ }
185
+
186
+ .VPNavBar.has-sidebar .divider {
187
+ padding-left: 300px !important;
188
+ }
189
+
190
+ .content-container {
191
+ margin-left: 30px !important;
192
+ }
193
+
194
+ .vp-doc td,
195
+ .vp-doc th {
196
+ font-size: 12px;
197
+ }
198
+
199
+ .custom-block {
200
+ padding: 12px 30px !important;
201
+ }
202
+
203
+ .custom-block-title {
204
+ display: none;
205
+ }
206
+
207
+ .VPHomeHero .image-bg {
208
+ background-image: linear-gradient(-45deg, #646cffcc 50%, #ffd14785 50%);
209
+ filter: blur(68px);
210
+ }
211
+
212
+ .vp-doc tr:nth-child(2n) {
213
+ background-color: transparent;
214
+ }
215
+
216
+ .vp-doc h2 {
217
+ border-top: none;
218
+ margin: 10px;
219
+ }
220
+
221
+ h2.text {
222
+ margin-top: 0;
223
+ }
224
+
225
+ :root {
226
+ --soui-brand-6: #197afa;
227
+ --soui-font-14: 12px;
228
+ --soui-tabs-tab-font-size: 12px;
229
+ --soui-14-regular: 12px;
230
+ --vp-c-brand-1: var(--soui-brand-6);
231
+ --vp-c-brand-2: var(--soui-brand-6);
232
+ --vp-button-brand-bg: var(--soui-brand-6);
233
+ }
234
+
235
+ body {
236
+ font-size: 12px;
237
+ }
238
+
239
+ .VPNavBar.has-sidebar .content {
240
+ padding-right: 40px !important;
241
+ }
242
+
243
+ h2 {
244
+ margin-left: 0 !important;
245
+ }
246
+
247
+ .VPImage.logo {
248
+ border-radius: 50%;
249
+ height: 30px;
250
+ }
251
+
252
+ .vp-doc[class*=" _interview"] .custom-block {
253
+ padding: 16px !important;
254
+ }
255
+
256
+ .vp-doc[class*=" _interview"] .custom-block summary {
257
+ margin-bottom: 0 !important;
258
+ }
259
+
260
+ .vp-doc[class*=" _interview"] .custom-block p {
261
+ margin: 0 !important;
262
+ }
263
+ </style>
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@yl_lowcode/docs-theme",
3
+ "version": "0.0.1",
4
+ "description": "共享 VitePress 文档主题(React Demo 支持)",
5
+ "type": "module",
6
+ "main": "index.ts",
7
+ "exports": {
8
+ ".": "./index.ts",
9
+ "./plugins/shiki-raw": "./plugins/shiki-raw.js"
10
+ },
11
+ "files": [
12
+ "index.ts",
13
+ "layout.vue",
14
+ "demo.vue",
15
+ "playground.vue",
16
+ "plugins",
17
+ "styles"
18
+ ],
19
+ "dependencies": {
20
+ "sucrase": "3.35.1",
21
+ "@yl_lowcode/editor": "0.0.1"
22
+ },
23
+ "peerDependencies": {
24
+ "react": "^18.0.0",
25
+ "react-dom": "^18.0.0",
26
+ "vitepress": ">=1.6.0",
27
+ "vue": "^3.4.0"
28
+ },
29
+ "license": "MIT",
30
+ "publishConfig": {
31
+ "registry": "https://registry.npmjs.org/",
32
+ "access": "public"
33
+ },
34
+ "scripts": {}
35
+ }
package/playground.vue ADDED
@@ -0,0 +1,235 @@
1
+ <script setup lang="ts">
2
+ // @ts-ignore
3
+ import { ref, inject, onMounted, onBeforeUnmount } from "vue";
4
+
5
+ /**
6
+ * 通用 Playground 组件
7
+ *
8
+ * 使用方需通过 provide 注入以下配置:
9
+ * provide("playground-config", {
10
+ * codeGlobs: import.meta.glob("../../demos/**\/*.tsx", { query: "?raw", import: "default" }),
11
+ * loadModules: () => import("@yl_lowcode/editor").then(m => ({ "@yl_lowcode/editor": m })),
12
+ * })
13
+ */
14
+ const config = inject<{
15
+ codeGlobs: Record<string, () => Promise<any>>;
16
+ loadModules: () => Promise<Record<string, any>>;
17
+ }>("playground-config");
18
+
19
+ const containerRef = ref<HTMLElement | null>(null);
20
+ let reactRoot: any = null;
21
+
22
+ onMounted(async () => {
23
+ if (!containerRef.value || !config) return;
24
+
25
+ const params = new URLSearchParams(window.location.search);
26
+ const demo = params.get("demo");
27
+ if (!demo) return;
28
+
29
+ // 在 globs 中查找匹配的 demo 文件
30
+ const matchKey = Object.keys(config.codeGlobs).find((k) => k.includes(`/${demo}.tsx`));
31
+ if (!matchKey) return;
32
+
33
+ const rawCode = (await config.codeGlobs[matchKey]()).raw as string;
34
+ const [{ default: React }, { createRoot }, { transform }, userModules] = await Promise.all([
35
+ import("react"),
36
+ import("react-dom/client"),
37
+ import("sucrase"),
38
+ config.loadModules(),
39
+ ]);
40
+
41
+ // 编译函数:将 TSX 代码转换为可执行模块
42
+ function compileCode(code: string, moduleMap: Record<string, any>) {
43
+ try {
44
+ const normalizedCode = /\bimport\s+React\b/.test(code)
45
+ ? code
46
+ : `import React from "react";\n${code}`;
47
+ const { code: compiled } = transform(normalizedCode, {
48
+ transforms: ["typescript", "jsx", "imports"],
49
+ jsxRuntime: "classic",
50
+ });
51
+ if (!compiled) return { component: null, error: "编译无输出" };
52
+
53
+ const require = (name: string) => {
54
+ const resolved = moduleMap[name];
55
+ if (resolved !== undefined) return resolved;
56
+ throw new Error(`未找到模块: "${name}"`);
57
+ };
58
+
59
+ const exports = {} as Record<string, unknown>;
60
+ new Function("require", "module", "exports", compiled)(
61
+ require,
62
+ { exports },
63
+ exports,
64
+ );
65
+
66
+ const Component = (exports.default ?? exports) as React.ComponentType;
67
+ const component = typeof Component === "function" ? Component : null;
68
+ return { component, error: component ? null : "export default 不是函数" };
69
+ } catch (err: unknown) {
70
+ const error = err instanceof Error ? err.message : String(err);
71
+ return { component: null, error };
72
+ }
73
+ }
74
+
75
+ // ErrorBoundary
76
+ class ErrorBoundary extends React.Component<
77
+ React.PropsWithChildren<{}>,
78
+ { error: string | null }
79
+ > {
80
+ constructor(props: React.PropsWithChildren<{}>) {
81
+ super(props);
82
+ this.state = { error: null };
83
+ }
84
+ static getDerivedStateFromError(err: unknown) {
85
+ return { error: err instanceof Error ? err.message : String(err) };
86
+ }
87
+ render() {
88
+ if (this.state.error) {
89
+ return React.createElement(
90
+ "div",
91
+ {
92
+ style: {
93
+ margin: 0,
94
+ padding: "12px 16px",
95
+ color: "#ef4444",
96
+ fontSize: "12px",
97
+ fontFamily: "monospace",
98
+ borderBottom: "1px solid rgba(239,68,68,.2)",
99
+ background: "rgba(239,68,68,.05)",
100
+ whiteSpace: "pre-wrap",
101
+ wordBreak: "break-all",
102
+ maxHeight: "160px",
103
+ overflow: "auto",
104
+ },
105
+ },
106
+ "Runtime error: " + this.state.error,
107
+ );
108
+ }
109
+ return (this.props as any).children;
110
+ }
111
+ }
112
+
113
+ // 合并模块映射(react 始终提供)
114
+ const moduleMap: Record<string, any> = {
115
+ react: React,
116
+ ...userModules,
117
+ };
118
+
119
+ function App() {
120
+ const [code, setCode] = React.useState(rawCode);
121
+ const [Preview, setPreview] = React.useState<React.ComponentType | null>(
122
+ () => compileCode(rawCode, moduleMap).component,
123
+ );
124
+ const [error, setError] = React.useState<string | null>(
125
+ () => compileCode(rawCode, moduleMap).error,
126
+ );
127
+
128
+ const handleCodeChange = React.useCallback((newCode: string) => {
129
+ setCode(newCode);
130
+ const { component, error: err } = compileCode(newCode, moduleMap);
131
+ setPreview(() => component);
132
+ setError(err);
133
+ }, []);
134
+
135
+ // 动态获取 Monaco 组件(从 moduleMap 中取)
136
+ const MonacoEditor = userModules["@yl_lowcode/editor"]?.Monaco;
137
+
138
+ return React.createElement(
139
+ "div",
140
+ { style: { display: "flex", height: "100vh", overflow: "hidden" } },
141
+ // 左侧:Monaco 编辑器
142
+ React.createElement(
143
+ "div",
144
+ {
145
+ style: {
146
+ flex: 1,
147
+ borderRight: "1px solid var(--vp-c-divider)",
148
+ overflow: "hidden",
149
+ },
150
+ },
151
+ MonacoEditor
152
+ ? React.createElement(MonacoEditor, {
153
+ value: code,
154
+ language: "javascript",
155
+ style: { height: "100%" },
156
+ onChange: handleCodeChange,
157
+ })
158
+ : React.createElement("textarea", {
159
+ value: code,
160
+ onChange: (e: any) => handleCodeChange(e.target.value),
161
+ style: { width: "100%", height: "100%", fontFamily: "monospace", padding: "16px" },
162
+ }),
163
+ ),
164
+ // 右侧:预览 + 错误
165
+ React.createElement(
166
+ "div",
167
+ {
168
+ style: {
169
+ flex: 1,
170
+ display: "flex",
171
+ flexDirection: "column",
172
+ overflow: "hidden",
173
+ },
174
+ },
175
+ error &&
176
+ React.createElement(
177
+ "div",
178
+ {
179
+ style: {
180
+ margin: 0,
181
+ padding: "12px 16px",
182
+ color: "#ef4444",
183
+ fontSize: "12px",
184
+ fontFamily: "monospace",
185
+ borderBottom: "1px solid rgba(239,68,68,.2)",
186
+ background: "rgba(239,68,68,.05)",
187
+ whiteSpace: "pre-wrap",
188
+ wordBreak: "break-all",
189
+ maxHeight: "160px",
190
+ overflow: "auto",
191
+ },
192
+ },
193
+ error,
194
+ ),
195
+ React.createElement(
196
+ ErrorBoundary,
197
+ { key: code },
198
+ React.createElement(
199
+ "div",
200
+ { style: { flex: 1, padding: "24px", overflow: "auto" } },
201
+ Preview && React.createElement(Preview),
202
+ ),
203
+ ),
204
+ ),
205
+ );
206
+ }
207
+
208
+ reactRoot = createRoot(containerRef.value);
209
+ reactRoot.render(React.createElement(App));
210
+ });
211
+
212
+ onBeforeUnmount(() => {
213
+ reactRoot?.unmount();
214
+ });
215
+ </script>
216
+
217
+ <template>
218
+ <div class="playground-page">
219
+ <div ref="containerRef" class="playground-container" />
220
+ </div>
221
+ </template>
222
+
223
+ <style scoped>
224
+ .playground-page {
225
+ width: 100vw;
226
+ height: 100vh;
227
+ overflow: hidden;
228
+ background: var(--vp-c-bg);
229
+ }
230
+
231
+ .playground-container {
232
+ width: 100%;
233
+ height: 100%;
234
+ }
235
+ </style>
@@ -0,0 +1,41 @@
1
+ import { readFileSync } from "fs";
2
+ import { createMarkdownRenderer } from "vitepress";
3
+
4
+ let mdPromise = null;
5
+
6
+ function getMd(srcDir, options) {
7
+ if (!mdPromise) {
8
+ mdPromise = createMarkdownRenderer(srcDir, options);
9
+ }
10
+ return mdPromise;
11
+ }
12
+
13
+ export function shikiRawPlugin(options) {
14
+ let srcDir = process.cwd();
15
+
16
+ return {
17
+ name: "vite-plugin-shiki-raw",
18
+ enforce: "pre",
19
+ configResolved(config) {
20
+ srcDir = config.root;
21
+ },
22
+ async load(id) {
23
+ if (!id.endsWith("?raw")) return null;
24
+
25
+ const filePath = id.slice(0, -4);
26
+ if (!filePath.endsWith(".tsx")) return null;
27
+
28
+ const raw = readFileSync(filePath, "utf-8");
29
+ const md = await getMd(srcDir, options);
30
+ const html =
31
+ typeof md.renderAsync === "function"
32
+ ? await md.renderAsync("```tsx\n" + raw + "\n```")
33
+ : md.render("```tsx\n" + raw + "\n```");
34
+
35
+ const demosIdx = filePath.indexOf("/demos/");
36
+ const srcPath = demosIdx >= 0 ? filePath.slice(demosIdx + 7, -4) : "";
37
+
38
+ return `export default ${JSON.stringify({ raw, html, srcPath })}`;
39
+ },
40
+ };
41
+ }
@@ -0,0 +1,48 @@
1
+ /* 覆盖 VitePress 默认变量,接入主题色 */
2
+ :root {
3
+ --vp-c-brand-1: var(--aui-accent);
4
+ --vp-c-brand-2: var(--aui-accent-light);
5
+ --vp-c-brand-3: var(--aui-accent-dark);
6
+ --vp-c-brand-soft: rgba(99, 102, 241, 0.1);
7
+ }
8
+
9
+ .dark .shiki span {
10
+ color: var(--shiki-dark, inherit);
11
+ font-size: 13px;
12
+ }
13
+
14
+ html:not(.dark) .shiki span {
15
+ color: var(--shiki-light, inherit);
16
+ font-size: 13px;
17
+ }
18
+
19
+ .vp-doc .language-tsx {
20
+ margin: 0 !important;
21
+ border-radius: 0 !important;
22
+ }
23
+
24
+ .vp-doc .shiki-themes {
25
+ padding: 10px 0 !important;
26
+ }
27
+
28
+ .vp-doc img {
29
+ margin: 0 !important;
30
+ }
31
+
32
+ /* demo 代码展示区:复用 VitePress 代码块,隐藏多余 UI */
33
+ .aui-demo-code .language-tsx {
34
+ margin: 0;
35
+ }
36
+
37
+ .aui-demo-code .copy,
38
+ .aui-demo-code .lang {
39
+ display: none !important;
40
+ }
41
+
42
+ .aui-demo-code pre.shiki {
43
+ margin: 0;
44
+ padding: 20px 24px;
45
+ font-size: 13px;
46
+ line-height: 1.7;
47
+ border-radius: 0;
48
+ }
@@ -0,0 +1,55 @@
1
+ /* 文档主题 CSS 变量 */
2
+
3
+ :root {
4
+ /* 高亮色(可动态修改) */
5
+ --aui-accent: #197afa;
6
+ --aui-accent-light: #3891fb;
7
+ --aui-accent-dark: #0563da;
8
+ --aui-accent-bg: rgba(25, 122, 250, 0.1);
9
+
10
+ /* 亮色主题 */
11
+ --aui-bg: #ffffff;
12
+ --aui-bg-soft: #f6f6f7;
13
+ --aui-bg-mute: #ebebed;
14
+ --aui-border: #e2e2e3;
15
+ --aui-text: #213547;
16
+ --aui-text-2: #4a5270;
17
+ --aui-text-3: #8b8b8f;
18
+
19
+ /* 组件尺寸 */
20
+ --aui-radius-sm: 4px;
21
+ --aui-radius: 6px;
22
+ --aui-radius-lg: 10px;
23
+
24
+ /* 间距 */
25
+ --aui-space-1: 4px;
26
+ --aui-space-2: 8px;
27
+ --aui-space-3: 12px;
28
+ --aui-space-4: 16px;
29
+
30
+ /* 字体 */
31
+ --aui-font-sm: 13px;
32
+ --aui-font-base: 14px;
33
+ --aui-font-lg: 16px;
34
+
35
+ /* 阴影 */
36
+ --aui-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
37
+ --aui-shadow-md:
38
+ 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
39
+ --aui-shadow-focus: 0 0 0 3px rgba(25, 122, 250, 0.3);
40
+ }
41
+
42
+ .dark {
43
+ --aui-bg: #0c0e13;
44
+ --aui-bg-soft: #161b27;
45
+ --aui-bg-mute: #1e2536;
46
+ --aui-border: #2a3147;
47
+ --aui-text: #e8ecf8;
48
+ --aui-text-2: #9ba8c9;
49
+ --aui-text-3: #5a6480;
50
+ --aui-accent-bg: rgba(25, 122, 250, 0.18);
51
+ --aui-shadow: 0 1px 3px rgba(0, 0, 0, 0.4), 0 1px 2px rgba(0, 0, 0, 0.3);
52
+ --aui-shadow-md:
53
+ 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.3);
54
+ --aui-shadow-focus: 0 0 0 3px rgba(25, 122, 250, 0.4);
55
+ }