Skip to content

Commit 33fcbd8

Browse files
committed
feat(docs): enhance demo system with LiveDemo and MultiFileFrame, improve code highlighting, and refactor components
1 parent 7788a54 commit 33fcbd8

File tree

12 files changed

+829
-101
lines changed

12 files changed

+829
-101
lines changed

packages/next-docs-plugin/src/components/Code.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ const Code: FC<
2727
const container = event.currentTarget.closest('.code-block');
2828
return container?.querySelector('pre code')?.textContent ?? '';
2929
}
30-
console.log('props', props);
3130

3231
return (
3332
<code

packages/next-docs-plugin/src/components/ComponentPreview.tsx

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,22 @@ import { AnimatePresence, motion } from 'framer-motion';
55
import { Check, Copy } from 'lucide-react';
66
import React, { useEffect, useState } from 'react';
77

8-
import { Segment } from '@/components/s';
8+
import { Segment } from '@/components/segment';
99

10-
import { highlight } from '../lib/shiki';
10+
import { highlightCode } from '../lib/shiki';
1111

1212
interface Props {
13+
children: React.ReactNode;
14+
1315
code: string;
14-
demo: React.ReactNode;
1516
height?: number | string;
1617
lang?: string;
1718
name: string;
1819
tabs?: { label: string; value: string }[];
1920
title?: string;
2021
}
2122

22-
export const ComponentPreview: React.FC<Props> = ({ code, demo, height = 360, lang = 'tsx', name, tabs, title }) => {
23+
const ComponentPreview: React.FC<Props> = ({ children, code, height = 360, lang = 'tsx', name, tabs, title }) => {
2324
const [active, setActive] = useState(tabs?.[0]?.value ?? 'preview');
2425
const [copied, setCopied] = useState(false);
2526
const [html, setHtml] = useState('');
@@ -29,7 +30,9 @@ export const ComponentPreview: React.FC<Props> = ({ code, demo, height = 360, la
2930
useEffect(() => {
3031
let canceled = false;
3132
setIsLoading(true);
32-
highlight(code.trim(), lang).then(result => {
33+
highlightCode(code, lang).then(result => {
34+
console.log('result', result);
35+
3336
if (!canceled) {
3437
setHtml(result);
3538
setIsLoading(false);
@@ -51,19 +54,19 @@ export const ComponentPreview: React.FC<Props> = ({ code, demo, height = 360, la
5154
};
5255

5356
return (
54-
<div className="my-8 overflow-hidden rounded-lg border border-border/50 bg-background/50 shadow-sm">
57+
<div className="my-8 overflow-hidden prose rounded-lg border border-border/50 bg-background/50 shadow-sm">
5558
{/* Header */}
5659
<div className="flex items-center justify-between border-b border-border/50 px-4 py-2 bg-muted/40">
5760
<span className="font-medium text-sm text-foreground truncate">{title ?? name}</span>
5861
<Segment
5962
value={active}
60-
options={
63+
items={
6164
tabs ?? [
6265
{ label: 'Preview', value: 'preview' },
6366
{ label: 'Code', value: 'code' }
6467
]
6568
}
66-
onChange={setActive}
69+
onValueChange={setActive}
6770
/>
6871
</div>
6972

@@ -79,7 +82,7 @@ export const ComponentPreview: React.FC<Props> = ({ code, demo, height = 360, la
7982
style={{ height }}
8083
transition={{ duration: 0.2 }}
8184
>
82-
{demo}
85+
{children}
8386
</motion.div>
8487
) : (
8588
<motion.div
@@ -104,8 +107,11 @@ export const ComponentPreview: React.FC<Props> = ({ code, demo, height = 360, la
104107

105108
{/* Code */}
106109
<div
107-
className="overflow-auto text-sm leading-relaxed font-mono not-prose p-4"
108110
dangerouslySetInnerHTML={{ __html: html }}
111+
className={cn(
112+
'h-[calc(100vh-380px)] overflow-auto',
113+
'[&_pre]:m-0 [&_pre]:whitespace-pre [&_code]:whitespace-pre'
114+
)}
109115
/>
110116
{isLoading && (
111117
<div className="absolute inset-0 flex items-center justify-center text-muted-foreground text-sm">
@@ -118,3 +124,5 @@ export const ComponentPreview: React.FC<Props> = ({ code, demo, height = 360, la
118124
</div>
119125
);
120126
};
127+
128+
export default ComponentPreview;
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
'use server';
2+
3+
import { readFile } from 'node:fs/promises';
4+
import path from 'node:path';
5+
6+
import { Suspense } from 'react';
7+
8+
import { DemoFrame } from './DemoFrame';
9+
10+
interface DemoProps {
11+
children?: React.ReactNode;
12+
highlight?: string;
13+
src?: string;
14+
title?: string;
15+
}
16+
17+
/**
18+
* Demo 组件 - 通过 rehype 插件编译时处理
19+
*
20+
* 使用方式:
21+
* <Demo src="@/demos/button-basic.tsx" title="基础按钮" />
22+
*
23+
* 编译后会变成:
24+
* import ButtonBasic from '@/demos/button-basic.tsx';
25+
* <Demo src="@/demos/button-basic.tsx" title="基础按钮">
26+
* <ButtonBasic />
27+
* </Demo>
28+
*/
29+
export default async function Demo({ children, highlight, src, title }: DemoProps) {
30+
if (!src) {
31+
return <div className="text-red-500">Demo 组件需要 src 属性</div>;
32+
}
33+
34+
// 读取源代码
35+
const code = await readSourceCode(src);
36+
const filename = path.basename(src);
37+
38+
return (
39+
<DemoFrame
40+
code={code}
41+
filename={filename}
42+
highlight={highlight}
43+
lang={getLanguage(src)}
44+
title={title ?? filename}
45+
exportFiles={{
46+
'index.html': `<!doctype html><html><body><div id="root"></div><script type="module" src="src/App.tsx"></script></body></html>`,
47+
'package.json': JSON.stringify(
48+
{
49+
dependencies: { react: '^19.0.0', 'react-dom': '^19.0.0' },
50+
devDependencies: { typescript: '^5.5.0', vite: '^5.0.0' },
51+
name: 'demo',
52+
private: true,
53+
scripts: { build: 'tsc && vite build', dev: 'vite', preview: 'vite preview' },
54+
type: 'module'
55+
},
56+
null,
57+
2
58+
),
59+
'src/App.tsx': code
60+
}}
61+
preview={
62+
<Suspense fallback={<div className="p-6 text-sm text-muted-foreground">Loading...</div>}>{children}</Suspense>
63+
}
64+
>
65+
{children}
66+
</DemoFrame>
67+
);
68+
}
69+
70+
/**
71+
* 读取源代码文件
72+
*/
73+
async function readSourceCode(src: string): Promise<string> {
74+
const absPath = resolvePath(src);
75+
try {
76+
return await readFile(absPath, 'utf-8');
77+
} catch (error) {
78+
console.error(`Failed to read demo file: ${absPath}`, error);
79+
return `// Error: Could not read file ${src}\nexport default function Demo() {\n return <div>File not found</div>;\n}`;
80+
}
81+
}
82+
83+
/**
84+
* 解析文件路径
85+
*/
86+
function resolvePath(src: string): string {
87+
if (src.startsWith('@/')) {
88+
return path.join(process.cwd(), src.slice(2));
89+
}
90+
if (src.startsWith('/')) {
91+
return src;
92+
}
93+
return path.resolve(process.cwd(), src);
94+
}
95+
96+
/**
97+
* 获取文件语言类型
98+
*/
99+
function getLanguage(src: string): string {
100+
const ext = path.extname(src).slice(1);
101+
return ['tsx', 'jsx', 'ts', 'js'].includes(ext) ? ext : 'tsx';
102+
}

0 commit comments

Comments
 (0)