feat(docs): add sandbox fullscreen mode

This commit is contained in:
Maofeng
2026-09-29 21:04:34 +08:00
parent 4710a56242
commit 9a6ec3a396
2 changed files with 71 additions and 10 deletions
@@ -5,10 +5,12 @@ const minimumFrameHeight = 288
export function SandboxFrame({
children,
fill = false,
reloadKey,
title,
}: {
children: React.ReactNode
fill?: boolean
reloadKey: number
title: string
}) {
@@ -71,7 +73,7 @@ export function SandboxFrame({
}, [reloadKey])
React.useEffect(() => {
if (!mountNode) return
if (!mountNode || fill) return
const frameDocument = mountNode.ownerDocument
const updateHeight = () => {
@@ -87,14 +89,14 @@ export function SandboxFrame({
updateHeight()
return () => resizeObserver.disconnect()
}, [mountNode])
}, [fill, mountNode])
return (
<>
<iframe
ref={frameRef}
className="block w-full border-0 bg-white dark:bg-slate-950"
style={{ height }}
className={`block w-full border-0 bg-white dark:bg-slate-950 ${fill ? "min-h-0 flex-1" : ""}`}
style={{ height: fill ? "100%" : height }}
sandbox="allow-same-origin"
title={title}
/>
+65 -6
View File
@@ -84,23 +84,45 @@ function SandboxViewer({
definition: SandboxDefinition
title?: string
}) {
const rootRef = React.useRef<HTMLElement>(null)
const t = useTranslate()
const filenames = React.useMemo(
() => Object.keys(definition.files),
[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 [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 [fullscreen, setFullscreen] = React.useState(false)
const resolvedTitle = title ?? definition.title ?? definition.id
React.useEffect(() => {
const root = rootRef.current
if (!root) return
const ownerDocument = root.ownerDocument
const syncFullscreen = () => {
setFullscreen(ownerDocument.fullscreenElement === root)
}
syncFullscreen()
ownerDocument.addEventListener("fullscreenchange", syncFullscreen)
return () => {
ownerDocument.removeEventListener("fullscreenchange", syncFullscreen)
}
}, [])
React.useEffect(() => {
let active = true
setPreviewModule(null)
@@ -178,9 +200,26 @@ function SandboxViewer({
window.setTimeout(() => setCopied(false), 1500)
}
const toggleFullscreen = async () => {
const root = rootRef.current
if (!root) return
const ownerDocument = root.ownerDocument
if (ownerDocument.fullscreenElement === root) {
await ownerDocument.exitFullscreen()
return
}
if (ownerDocument.fullscreenElement) {
await ownerDocument.exitFullscreen()
}
await root.requestFullscreen()
}
return (
<section
className={`not-prose my-6 overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-950 ${className ?? ""}`}
ref={rootRef}
className={`not-prose overflow-hidden border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-950 ${fullscreen ? "m-0 flex h-dvh w-dvw flex-col rounded-none border-0" : "my-6 rounded-xl"} ${className ?? ""}`}
aria-label={resolvedTitle}
>
<div className="flex min-h-11 flex-wrap items-center gap-2 border-b border-slate-200 bg-slate-50 px-2 py-1.5 dark:border-slate-800 dark:bg-slate-900/70">
@@ -230,11 +269,28 @@ function SandboxViewer({
<Icon name="lucide:rotate-ccw" className="size-4" />
</SandboxAction>
)}
<SandboxAction
label={t({
zh: fullscreen ? "退出全屏" : "全屏显示",
us: fullscreen ? "Exit fullscreen" : "Enter fullscreen",
})}
pressed={fullscreen}
onClick={() => void toggleFullscreen()}
>
<Icon
name={fullscreen ? "lucide:minimize-2" : "lucide:maximize-2"}
className="size-4"
/>
</SandboxAction>
</div>
</div>
{view === "preview" ? (
<SandboxFrame reloadKey={reloadKey} title={resolvedTitle}>
<SandboxFrame
fill={fullscreen}
reloadKey={reloadKey}
title={resolvedTitle}
>
<SandboxErrorBoundary key={reloadKey} fallback={renderSandboxError}>
{previewError ? (
<SandboxMessage tone="error">
@@ -251,7 +307,7 @@ function SandboxViewer({
</SandboxFrame>
) : (
<div
className={`grid min-h-72 ${filenames.length > 1 ? "md:grid-cols-[12rem_minmax(0,1fr)]" : ""}`}
className={`grid min-h-72 ${fullscreen ? "min-h-0 flex-1 overflow-hidden" : ""} ${filenames.length > 1 ? "md:grid-cols-[12rem_minmax(0,1fr)]" : ""}`}
>
{filenames.length > 1 ? (
<nav
@@ -385,17 +441,20 @@ function SandboxAction({
disabled,
label,
onClick,
pressed,
}: {
children: React.ReactNode
disabled?: boolean
label: string
onClick: VoidFunction
pressed?: boolean
}) {
return (
<button
type="button"
className="inline-flex size-8 items-center justify-center rounded-md text-slate-500 transition-colors hover:bg-slate-200 hover:text-slate-950 disabled:opacity-40 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-100"
aria-label={label}
aria-pressed={pressed}
disabled={disabled}
onClick={onClick}
>