feat(docs): colocate localized package sandboxes
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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 }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>(
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -22,5 +22,9 @@
|
|||||||
"erasableSyntaxOnly": true,
|
"erasableSyntaxOnly": true,
|
||||||
"noFallthroughCasesInSwitch": true
|
"noFallthroughCasesInSwitch": true
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": [
|
||||||
}
|
"src",
|
||||||
|
"../packages/*/docs/sandboxes/**/*.ts",
|
||||||
|
"../packages/*/docs/sandboxes/**/*.tsx"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.app.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"files": ["src/components/sandbox/sandbox-env.d.ts"],
|
||||||
|
"include": []
|
||||||
|
}
|
||||||
+3
-1
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+6
-5
@@ -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
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../docs/tsconfig.sandbox.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "../../../docs/node_modules/.tmp/appearance-docs.tsbuildinfo"
|
||||||
|
},
|
||||||
|
"include": ["sandboxes/**/*.ts", "sandboxes/**/*.tsx"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user