feat(docs): enhance code presentation
This commit is contained in:
@@ -5,6 +5,7 @@ import { useTranslate } from "../../lib/locale"
|
||||
import { getSandboxDefinition } from "./registry"
|
||||
import { SandboxErrorBoundary } from "./sandbox-error-boundary"
|
||||
import { SandboxFrame } from "./sandbox-frame"
|
||||
import { highlightSource, type HighlightedSource } from "./highlight-source"
|
||||
import type {
|
||||
SandboxDefinition,
|
||||
SandboxPreviewModule,
|
||||
@@ -13,6 +14,8 @@ import type {
|
||||
|
||||
type SandboxView = "code" | "preview"
|
||||
|
||||
const codeFontSizes = [12, 13, 14, 16] as const
|
||||
|
||||
export type SandboxProps = {
|
||||
className?: string
|
||||
example: string
|
||||
@@ -87,14 +90,13 @@ function SandboxViewer({
|
||||
[definition.files]
|
||||
)
|
||||
const [view, setView] = React.useState<SandboxView>("preview")
|
||||
const [selectedFile, setSelectedFile] = React.useState(
|
||||
definition.defaultFile ?? filenames[0] ?? ""
|
||||
)
|
||||
const [previewModule, setPreviewModule] =
|
||||
React.useState<SandboxPreviewModule | null>(null)
|
||||
const [selectedFile, setSelectedFile] = React.useState(definition.defaultFile ?? filenames[0] ?? "")
|
||||
const [previewModule, setPreviewModule] = React.useState<SandboxPreviewModule | null>(null)
|
||||
const [previewError, setPreviewError] = React.useState<Error | null>(null)
|
||||
const [source, setSource] = React.useState<string | null>(null)
|
||||
const [sourceError, setSourceError] = React.useState<Error | null>(null)
|
||||
const [highlightedSource, setHighlightedSource] = React.useState<HighlightedSource | null>(null)
|
||||
const [codeFontSizeIndex, setCodeFontSizeIndex] = React.useState(1)
|
||||
const [reloadKey, setReloadKey] = React.useState(0)
|
||||
const [copied, setCopied] = React.useState(false)
|
||||
const resolvedTitle = title ?? definition.title ?? definition.id
|
||||
@@ -131,6 +133,7 @@ function SandboxViewer({
|
||||
let active = true
|
||||
setSource(null)
|
||||
setSourceError(null)
|
||||
setHighlightedSource(null)
|
||||
resolveSource(file).then(
|
||||
(value) => {
|
||||
if (active) setSource(value)
|
||||
@@ -145,6 +148,25 @@ function SandboxViewer({
|
||||
}
|
||||
}, [definition.files, selectedFile, view])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!source || view !== "code") return
|
||||
|
||||
let active = true
|
||||
setHighlightedSource(null)
|
||||
highlightSource(source, selectedFile).then(
|
||||
(value) => {
|
||||
if (active) setHighlightedSource(value)
|
||||
},
|
||||
() => {
|
||||
if (active) setHighlightedSource(null)
|
||||
}
|
||||
)
|
||||
|
||||
return () => {
|
||||
active = false
|
||||
}
|
||||
}, [selectedFile, source, view])
|
||||
|
||||
React.useEffect(() => {
|
||||
setSelectedFile(definition.defaultFile ?? filenames[0] ?? "")
|
||||
}, [definition.defaultFile, filenames])
|
||||
@@ -182,16 +204,24 @@ function SandboxViewer({
|
||||
|
||||
<div className="ml-auto flex items-center gap-1">
|
||||
{view === "code" ? (
|
||||
<SandboxAction
|
||||
disabled={!source}
|
||||
label={t({ zh: "复制源码", us: "Copy source" })}
|
||||
onClick={() => void copySource()}
|
||||
>
|
||||
<Icon
|
||||
name={copied ? "lucide:check" : "lucide:copy"}
|
||||
className="size-4"
|
||||
<>
|
||||
<CodeFontSizeControl
|
||||
decreaseLabel={t({ zh: "减小字号", us: "Decrease font size" })}
|
||||
increaseLabel={t({ zh: "增大字号", us: "Increase font size" })}
|
||||
index={codeFontSizeIndex}
|
||||
onIndexChange={setCodeFontSizeIndex}
|
||||
/>
|
||||
</SandboxAction>
|
||||
<SandboxAction
|
||||
disabled={!source}
|
||||
label={t({ zh: "复制源码", us: "Copy source" })}
|
||||
onClick={() => void copySource()}
|
||||
>
|
||||
<Icon
|
||||
name={copied ? "lucide:check" : "lucide:copy"}
|
||||
className="size-4"
|
||||
/>
|
||||
</SandboxAction>
|
||||
</>
|
||||
) : (
|
||||
<SandboxAction
|
||||
label={t({ zh: "重置预览", us: "Reset preview" })}
|
||||
@@ -241,11 +271,21 @@ function SandboxViewer({
|
||||
))}
|
||||
</nav>
|
||||
) : null}
|
||||
<pre className="m-0 min-w-0 overflow-auto bg-slate-950 p-4 text-sm leading-6 text-slate-100">
|
||||
<code>
|
||||
{sourceError
|
||||
? sourceError.message
|
||||
: (source ?? t({ zh: "正在加载源码…", us: "Loading source…" }))}
|
||||
<pre
|
||||
className="m-0 min-w-0 overflow-auto bg-slate-50 p-4 font-mono text-slate-800 dark:bg-slate-950 dark:text-slate-100"
|
||||
style={{
|
||||
fontSize: `${codeFontSizes[codeFontSizeIndex]}px`,
|
||||
lineHeight: 1.55,
|
||||
}}
|
||||
>
|
||||
<code className="block min-w-max">
|
||||
{sourceError ? (
|
||||
sourceError.message
|
||||
) : highlightedSource ? (
|
||||
<HighlightedCode source={highlightedSource} />
|
||||
) : (
|
||||
(source ?? t({ zh: "正在加载源码…", us: "Loading source…" }))
|
||||
)}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
@@ -254,6 +294,71 @@ function SandboxViewer({
|
||||
)
|
||||
}
|
||||
|
||||
function CodeFontSizeControl({
|
||||
decreaseLabel,
|
||||
increaseLabel,
|
||||
index,
|
||||
onIndexChange,
|
||||
}: {
|
||||
decreaseLabel: string
|
||||
increaseLabel: string
|
||||
index: number
|
||||
onIndexChange: React.Dispatch<React.SetStateAction<number>>
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="inline-flex h-8 items-center rounded-md border border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900"
|
||||
aria-label={`${codeFontSizes[index]}px`}
|
||||
>
|
||||
<SandboxAction
|
||||
disabled={index === 0}
|
||||
label={decreaseLabel}
|
||||
onClick={() => onIndexChange((value) => Math.max(0, value - 1))}
|
||||
>
|
||||
<Icon name="lucide:minus" className="size-3.5" />
|
||||
</SandboxAction>
|
||||
<span className="w-9 text-center font-mono text-[11px] text-slate-500 tabular-nums dark:text-slate-400">
|
||||
{codeFontSizes[index]}
|
||||
</span>
|
||||
<SandboxAction
|
||||
disabled={index === codeFontSizes.length - 1}
|
||||
label={increaseLabel}
|
||||
onClick={() =>
|
||||
onIndexChange((value) =>
|
||||
Math.min(codeFontSizes.length - 1, value + 1)
|
||||
)
|
||||
}
|
||||
>
|
||||
<Icon name="lucide:plus" className="size-3.5" />
|
||||
</SandboxAction>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function HighlightedCode({ source }: { source: HighlightedSource }) {
|
||||
return source.map((line, lineIndex) => (
|
||||
<React.Fragment key={line.offset}>
|
||||
<span className="sandbox-code-line">
|
||||
{line.tokens.map((token) => (
|
||||
<span
|
||||
key={token.offset}
|
||||
className="sandbox-code-token"
|
||||
style={
|
||||
{
|
||||
"--sandbox-token-dark": token.dark,
|
||||
"--sandbox-token-light": token.light,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
{token.content}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
{lineIndex < source.length - 1 ? "\n" : null}
|
||||
</React.Fragment>
|
||||
))
|
||||
}
|
||||
|
||||
function SandboxTab({
|
||||
active,
|
||||
children,
|
||||
|
||||
Reference in New Issue
Block a user