feat(docs): colocate localized package sandboxes

This commit is contained in:
Maofeng
2026-09-29 22:13:17 +08:00
parent e34675a931
commit 8b880a19e2
14 changed files with 130 additions and 41 deletions
+2
View File
@@ -7,6 +7,8 @@ export {
export { export {
defineSandbox, defineSandbox,
type SandboxDefinition, type SandboxDefinition,
type SandboxLocalizedText,
type SandboxPreviewProps,
type SandboxPreviewModule, type SandboxPreviewModule,
type SandboxSource, type SandboxSource,
type SandboxSourceModule, type SandboxSourceModule,
+4 -1
View File
@@ -5,7 +5,10 @@ type SandboxManifestModule = {
} }
const manifestModules = import.meta.glob<SandboxManifestModule>( const manifestModules = import.meta.glob<SandboxManifestModule>(
"../../sandboxes/*/*/sandbox.ts", [
"../../sandboxes/*/*/sandbox.ts",
"../../../../packages/*/docs/sandboxes/*/sandbox.ts",
],
{ eager: true } { eager: true }
) )
+4
View File
@@ -0,0 +1,4 @@
declare module "*?raw" {
const source: string
export default source
}
@@ -1,16 +1,20 @@
import React from "react" import React from "react"
import { createPortal } from "react-dom" import { createPortal } from "react-dom"
import type { DocLocale } from "../../content/types"
const minimumFrameHeight = 288 const minimumFrameHeight = 288
export function SandboxFrame({ export function SandboxFrame({
children, children,
fill = false, fill = false,
locale,
reloadKey, reloadKey,
title, title,
}: { }: {
children: React.ReactNode children: React.ReactNode
fill?: boolean fill?: boolean
locale: DocLocale
reloadKey: number reloadKey: number
title: string title: string
}) { }) {
@@ -56,6 +60,7 @@ export function SandboxFrame({
document.documentElement.style.colorScheme document.documentElement.style.colorScheme
} }
syncDocumentTheme() syncDocumentTheme()
frameDocument.documentElement.lang = locale
const themeObserver = new MutationObserver(syncDocumentTheme) const themeObserver = new MutationObserver(syncDocumentTheme)
themeObserver.observe(document.documentElement, { themeObserver.observe(document.documentElement, {
@@ -70,7 +75,7 @@ export function SandboxFrame({
themeObserver.disconnect() themeObserver.disconnect()
setMountNode(null) setMountNode(null)
} }
}, [reloadKey]) }, [locale, reloadKey])
React.useEffect(() => { React.useEffect(() => {
if (!mountNode || fill) return if (!mountNode || fill) return
+29 -7
View File
@@ -1,13 +1,15 @@
import React from "react" import React from "react"
import { Icon } from "@icones/react" import { Icon } from "@icones/react"
import { useTranslate } from "../../lib/locale" import { useLocale, useTranslate } from "../../lib/locale"
import { getSandboxDefinition } from "./registry" import { getSandboxDefinition } from "./registry"
import { SandboxErrorBoundary } from "./sandbox-error-boundary" import { SandboxErrorBoundary } from "./sandbox-error-boundary"
import { SandboxFrame } from "./sandbox-frame" import { SandboxFrame } from "./sandbox-frame"
import { highlightSource, type HighlightedSource } from "./highlight-source" import { highlightSource, type HighlightedSource } from "./highlight-source"
import type { import type {
SandboxDefinition, SandboxDefinition,
SandboxLocalizedText,
SandboxPreviewProps,
SandboxPreviewModule, SandboxPreviewModule,
SandboxSource, SandboxSource,
} from "./types" } from "./types"
@@ -19,7 +21,7 @@ const codeFontSizes = [12, 13, 14, 16] as const
export type SandboxProps = { export type SandboxProps = {
className?: string className?: string
example: string example: string
title?: string title?: SandboxLocalizedText
} }
export function Sandbox({ className, example, title }: SandboxProps) { export function Sandbox({ className, example, title }: SandboxProps) {
@@ -51,7 +53,7 @@ export type SingleFileSandboxProps = {
component: React.ComponentType component: React.ComponentType
filename?: string filename?: string
source: string source: string
title?: string title?: SandboxLocalizedText
} }
export function SingleFileSandbox({ export function SingleFileSandbox({
@@ -64,7 +66,9 @@ export function SingleFileSandbox({
const definition = React.useMemo<SandboxDefinition>( const definition = React.useMemo<SandboxDefinition>(
() => ({ () => ({
defaultFile: filename, defaultFile: filename,
entry: async () => ({ default: component }), entry: async () => ({
default: component as React.ComponentType<SandboxPreviewProps>,
}),
files: { [filename]: source }, files: { [filename]: source },
id: `single-file:${filename}`, id: `single-file:${filename}`,
title, title,
@@ -82,9 +86,10 @@ function SandboxViewer({
}: { }: {
className?: string className?: string
definition: SandboxDefinition definition: SandboxDefinition
title?: string title?: SandboxLocalizedText
}) { }) {
const rootRef = React.useRef<HTMLElement>(null) const rootRef = React.useRef<HTMLElement>(null)
const locale = useLocale()
const t = useTranslate() const t = useTranslate()
const filenames = React.useMemo( const filenames = React.useMemo(
() => Object.keys(definition.files), () => Object.keys(definition.files),
@@ -105,7 +110,11 @@ function SandboxViewer({
const [reloadKey, setReloadKey] = React.useState(0) const [reloadKey, setReloadKey] = React.useState(0)
const [copied, setCopied] = React.useState(false) const [copied, setCopied] = React.useState(false)
const [fullscreen, setFullscreen] = React.useState(false) const [fullscreen, setFullscreen] = React.useState(false)
const resolvedTitle = title ?? definition.title ?? definition.id const resolvedTitle = resolveSandboxText(
title ?? definition.title,
locale,
definition.id
)
React.useEffect(() => { React.useEffect(() => {
const root = rootRef.current const root = rootRef.current
@@ -288,6 +297,7 @@ function SandboxViewer({
{view === "preview" ? ( {view === "preview" ? (
<SandboxFrame <SandboxFrame
fill={fullscreen} fill={fullscreen}
locale={locale}
reloadKey={reloadKey} reloadKey={reloadKey}
title={resolvedTitle} title={resolvedTitle}
> >
@@ -297,7 +307,10 @@ function SandboxViewer({
{previewError.message} {previewError.message}
</SandboxMessage> </SandboxMessage>
) : previewModule ? ( ) : previewModule ? (
React.createElement(previewModule.default, { key: reloadKey }) React.createElement(previewModule.default, {
key: reloadKey,
locale,
})
) : ( ) : (
<SandboxMessage> <SandboxMessage>
{t({ zh: "正在加载预览…", us: "Loading preview…" })} {t({ zh: "正在加载预览…", us: "Loading preview…" })}
@@ -350,6 +363,15 @@ function SandboxViewer({
) )
} }
function resolveSandboxText(
value: SandboxLocalizedText | undefined,
locale: "en-US" | "zh-Hans",
fallback: string
) {
if (typeof value === "string") return value
return value?.[locale] ?? fallback
}
function CodeFontSizeControl({ function CodeFontSizeControl({
decreaseLabel, decreaseLabel,
increaseLabel, increaseLabel,
+10 -2
View File
@@ -1,7 +1,15 @@
import type { ComponentType } from "react" import type { ComponentType } from "react"
import type { DocLocale } from "../../content/types"
export type SandboxLocalizedText = string | Readonly<Record<DocLocale, string>>
export type SandboxPreviewProps = {
locale: DocLocale
}
export type SandboxPreviewModule = { export type SandboxPreviewModule = {
default: ComponentType default: ComponentType<SandboxPreviewProps>
} }
export type SandboxSourceModule = { export type SandboxSourceModule = {
@@ -17,7 +25,7 @@ export type SandboxDefinition = {
entry: () => Promise<SandboxPreviewModule> entry: () => Promise<SandboxPreviewModule>
files: Readonly<Record<string, SandboxSource>> files: Readonly<Record<string, SandboxSource>>
id: string id: string
title?: string title?: SandboxLocalizedText
} }
export function defineSandbox<const Definition extends SandboxDefinition>( export function defineSandbox<const Definition extends SandboxDefinition>(
+2
View File
@@ -5,6 +5,8 @@
@import "@workspace/page/globals.css"; @import "@workspace/page/globals.css";
@import "@workspace/search/globals.css"; @import "@workspace/search/globals.css";
@source "../../packages/*/docs/**/*.{mdx,ts,tsx}";
@custom-variant dark (&:where(.dark, .dark *)); @custom-variant dark (&:where(.dark, .dark *));
@layer base { @layer base {
@@ -1,22 +0,0 @@
import React from "react"
import type { ThemeColorConfig } from "@workspace/appearance/theme"
import { AccentPicker } from "./AccentPicker"
export default function ThemeColorsExample() {
const [config, setConfig] = React.useState<ThemeColorConfig>({
accentColor: "indigo",
baseColor: "neutral",
})
return (
<div className="mx-auto flex max-w-xl flex-col gap-6">
<AccentPicker config={config} onConfigChange={setConfig} />
<AccentPicker
config={config}
variant="swatch"
onConfigChange={setConfig}
/>
</div>
)
}
+6 -2
View File
@@ -22,5 +22,9 @@
"erasableSyntaxOnly": true, "erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true
}, },
"include": ["src"] "include": [
} "src",
"../packages/*/docs/sandboxes/**/*.ts",
"../packages/*/docs/sandboxes/**/*.tsx"
]
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.app.json",
"compilerOptions": {
"types": []
},
"files": ["src/components/sandbox/sandbox-env.d.ts"],
"include": []
}
@@ -8,15 +8,17 @@ import type { ThemeColorConfig } from "@workspace/appearance/theme"
export function AccentPicker({ export function AccentPicker({
config, config,
onConfigChange, onConfigChange,
title,
variant, variant,
}: { }: {
config: ThemeColorConfig config: ThemeColorConfig
onConfigChange: (config: ThemeColorConfig) => void onConfigChange: (config: ThemeColorConfig) => void
title: string
variant?: ThemeColorPickerVariant variant?: ThemeColorPickerVariant
}) { }) {
return ( return (
<ThemeColorPicker <ThemeColorPicker
title="Accent color" title={title}
swatches={accentColorSwatches} swatches={accentColorSwatches}
value={config.accentColor} value={config.accentColor}
variant={variant} variant={variant}
@@ -0,0 +1,43 @@
import React from "react"
import { I18nProvider } from "@workspace/i18n"
import { messages as englishMessages } from "@workspace/appearance/locales/en-US"
import { messages as chineseMessages } from "@workspace/appearance/locales/zh-Hans"
import type { ThemeColorConfig } from "@workspace/appearance/theme"
import { AccentPicker } from "./AccentPicker"
const catalogs = {
"en-US": englishMessages,
"zh-Hans": chineseMessages,
} as const
export default function ThemeColorsExample({
locale,
}: {
locale: "en-US" | "zh-Hans"
}) {
const [config, setConfig] = React.useState<ThemeColorConfig>({
accentColor: "indigo",
baseColor: "neutral",
})
const title = locale === "zh-Hans" ? "强调色" : "Accent color"
return (
<I18nProvider catalogs={catalogs} locale={locale}>
<div className="mx-auto flex max-w-xl flex-col gap-6">
<AccentPicker
config={config}
title={title}
onConfigChange={setConfig}
/>
<AccentPicker
config={config}
title={title}
variant="swatch"
onConfigChange={setConfig}
/>
</div>
</I18nProvider>
)
}
@@ -1,12 +1,13 @@
import { defineSandbox } from "../../../components/sandbox" export default {
export default defineSandbox({
id: "appearance/theme-colors", id: "appearance/theme-colors",
title: "Controlled theme colors", title: {
"en-US": "Controlled theme colors",
"zh-Hans": "受控主题颜色",
},
entry: () => import("./App"), entry: () => import("./App"),
files: { files: {
"App.tsx": () => import("./App.tsx?raw"), "App.tsx": () => import("./App.tsx?raw"),
"AccentPicker.tsx": () => import("./AccentPicker.tsx?raw"), "AccentPicker.tsx": () => import("./AccentPicker.tsx?raw"),
}, },
defaultFile: "App.tsx", defaultFile: "App.tsx",
}) } satisfies import("../../../../../docs/src/components/sandbox/types").SandboxDefinition
+7
View File
@@ -0,0 +1,7 @@
{
"extends": "../../../docs/tsconfig.sandbox.json",
"compilerOptions": {
"tsBuildInfoFile": "../../../docs/node_modules/.tmp/appearance-docs.tsbuildinfo"
},
"include": ["sandboxes/**/*.ts", "sandboxes/**/*.tsx"]
}