feat(export): add ready-to-run avatar demos

This commit is contained in:
smontlouis 2026-08-17 19:36:16 +02:00
parent 2ba720e059
commit 843da29718
7 changed files with 483 additions and 79 deletions

View File

@ -447,6 +447,13 @@ input {
gap: 10px;
}
.export-format-section {
display: grid;
min-width: 0;
gap: 0;
padding: 4px 2px 2px;
}
.export-format-grid > button {
justify-content: flex-start;
min-width: 0;
@ -521,14 +528,6 @@ input {
min-height: 44px;
box-shadow: 0 12px 30px rgb(23 25 29 / 18%);
}
.runtime-export-actions {
display: grid;
gap: 8px;
}
.runtime-export-actions > button {
width: 100%;
min-height: 44px;
}
.runtime-copy-status {
margin: 0;
color: var(--muted-foreground);
@ -538,9 +537,10 @@ input {
.runtime-copy-status[role='alert'] {
color: var(--destructive);
}
.runtime-example-card {
.runtime-quick-start-card,
.runtime-export-card {
display: grid;
gap: 12px;
gap: 16px;
}
.runtime-export-description {
margin: 0;
@ -548,32 +548,95 @@ input {
font-size: 11px;
line-height: 1.5;
}
.runtime-example-heading {
.runtime-quick-start-heading {
display: flex;
align-items: stretch;
flex-direction: column;
align-items: center;
justify-content: space-between;
gap: 12px;
gap: 10px;
}
.runtime-example-heading > div:first-child {
.runtime-quick-start-heading > div,
.runtime-export-heading > div {
display: grid;
min-width: 0;
gap: 2px;
}
.runtime-example-heading small {
.runtime-quick-start-heading h2 {
margin: 0;
font-size: 15px;
letter-spacing: -0.015em;
}
.runtime-quick-start-heading > button {
flex: none;
color: #4168d5;
font-size: 11px;
}
.runtime-quick-start-heading > button > svg {
width: 14px;
height: 14px;
}
.runtime-quick-start-step {
display: grid;
min-width: 0;
gap: 7px;
}
.runtime-quick-start-step > span,
.runtime-export-heading small {
color: var(--muted-foreground);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.runtime-example-heading strong {
.runtime-quick-start-step > code {
display: block;
overflow-x: auto;
padding: 10px 12px;
border: 1px solid #dfe3eb;
border-radius: 9px;
background: #f7f8fb;
color: #3159cb;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 11px;
line-height: 1.5;
white-space: nowrap;
}
.runtime-quick-start-step > pre {
max-width: 100%;
max-height: 245px;
margin: 0;
overflow: auto;
padding: 13px 14px;
border: 1px solid #2d323b;
border-radius: 10px;
background: #17191d;
color: #d8dee9;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 10px;
line-height: 1.65;
white-space: pre;
}
.runtime-export-heading {
display: grid;
gap: 8px;
}
.runtime-export-heading strong {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 11px;
overflow-wrap: anywhere;
}
.runtime-example-actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
gap: 6px;
.runtime-export-actions {
display: grid;
gap: 8px;
}
.runtime-export-actions button {
width: 100%;
min-width: 0;
min-height: 42px;
}
.runtime-export-secondary-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.runtime-token-comment {
color: #a6adbb;
@ -3175,12 +3238,12 @@ p {
.runtime-preview-option-grid {
max-height: none;
}
.runtime-example-heading {
.runtime-quick-start-heading {
align-items: flex-start;
flex-direction: column;
}
.runtime-example-actions {
justify-content: flex-start;
.runtime-quick-start-heading > button {
padding-inline: 0;
}
html,
body,

View File

@ -1,6 +1,7 @@
import { createAvatar } from '@/features/avatar/avatars'
import { parse } from '@babel/parser'
import {
avatarDemoFileName,
createAvatarExportPayload,
generateJavaScriptAvatarHtml,
generateJavaScriptAvatarModule,
@ -10,6 +11,8 @@ import {
generateReactAvatarComponent,
generateReactAvatarPackage,
generateReactAvatarRuntime,
generateReactViteMain,
generateReactVitePackage,
} from '@/features/export/exporter'
import { createAvatarDefinition } from '@/features/avatar/avatarDefinition'
import { resolveAvatarBehavior } from '@/features/avatar/avatars'
@ -42,6 +45,11 @@ describe('avatar export', () => {
)
const payload = createAvatarExportPayload(avatar, initialExpressions, animations)
it('uses distinct filenames for React and ESM demo archives', () => {
expect(avatarDemoFileName('Strobi', 'react')).toBe('strobi-avatar-react.zip')
expect(avatarDemoFileName('Strobi', 'javascript')).toBe('strobi-avatar-esm.zip')
})
it('includes only the selected animations and their referenced expressions', () => {
expect(Object.keys(payload.animations)).toEqual(['idle', 'listening'])
expect(Object.keys(payload.expressions).sort()).toEqual(
@ -172,6 +180,50 @@ describe('avatar export', () => {
expect(contents).not.toContain('AvatarProceduralEngine')
})
it('generates a ready-to-run React TypeScript demo backed by avatar-react', async () => {
const document = loadStudioDocument({ getItem: () => null })
const studioAvatar = document.library.avatars[0]
const definition = createAvatarDefinition({
avatar: studioAvatar,
behavior: resolveAvatarBehavior(studioAvatar, {
expressions: document.expressions,
sequences: document.sequences,
}),
})
expect(definition.ok).toBe(true)
if (!definition.ok) return
const source = generateReactViteMain('../strobi.avatar.json', 'Strobi')
expect(source).toContain("from '@bible-strong/avatar-react'")
expect(source).toContain("from '../strobi.avatar.json'")
expect(source).toContain('createAvatar(definition)')
expect(source).toContain("kind: 'animation'")
expect(source).toContain("kind: 'expression'")
expect(() =>
parse(source, { sourceType: 'module', plugins: ['typescript', 'jsx'] })
).not.toThrow()
const archive = new Uint8Array(
await generateReactVitePackage(definition.value, 'Strobi').arrayBuffer()
)
const contents = new TextDecoder().decode(archive)
expect(storedZipFileNames(archive)).toEqual([
'strobi.avatar.json',
'package.json',
'index.html',
'tsconfig.json',
'vite.config.ts',
'src/main.tsx',
'src/vite-env.d.ts',
'src/styles.css',
'README.md',
])
expect(contents).toContain('"@bible-strong/avatar-react": "0.1.0"')
expect(contents).toContain('npm install')
expect(contents).toContain('npm run dev')
expect(contents).not.toContain('AvatarProceduralEngine')
})
it('generates a typed React component backed by the local runtime', () => {
const source = generateReactAvatarComponent(payload)

View File

@ -7,6 +7,8 @@ import type { AvatarSequence } from '../animation/sequences'
import { standaloneEngineSource } from './standaloneEngine.generated'
import { createStoredZip } from './storedZip'
const avatarRuntimePackageVersion = '0.1.0'
export type AvatarExportAnimation = Pick<
AvatarSequence,
'name' | 'description' | 'playbackMode' | 'blink'
@ -398,7 +400,7 @@ export const generateJavaScriptEsmHtml = (
</div>
</main>
<script type="module">
import { createAvatar } from 'https://esm.sh/@bible-strong/avatar-web@0.1.0'
import { createAvatar } from 'https://esm.sh/@bible-strong/avatar-web@${avatarRuntimePackageVersion}'
const definition = await fetch('./${definitionFileName}').then(response => response.json())
const avatar = createAvatar('#avatar', { definition, size: '100%' })
@ -488,6 +490,210 @@ const avatarComponentName = (name: string) => {
return /^[A-Za-z_$]/.test(identifier) ? identifier : `Avatar${identifier}`
}
export const generateReactViteMain = (
definitionImportPath: string,
avatarName: string
) => `import { createAvatar } from '@bible-strong/avatar-react'
import '@bible-strong/avatar-react/styles.css'
import { StrictMode, useState } from 'react'
import { createRoot } from 'react-dom/client'
import definition from '${definitionImportPath}'
import './styles.css'
const ExportedAvatar = createAvatar(definition)
type AnimationName = keyof typeof definition.animations
type ExpressionName = keyof typeof definition.expressions
type Target =
| { kind: 'animation'; key: AnimationName }
| { kind: 'expression'; key: ExpressionName }
const animations = definition.animationOrder as AnimationName[]
const expressions = definition.expressionOrder as ExpressionName[]
const initialTarget: Target = animations[0]
? { kind: 'animation', key: animations[0] }
: { kind: 'expression', key: expressions[0] ?? ('neutral' as ExpressionName) }
function App() {
const [target, setTarget] = useState<Target>(initialTarget)
return (
<main>
<h1>{${JSON.stringify(avatarName)}}</h1>
<div className="demo">
<section className="stage">
<ExportedAvatar
{...(target.kind === 'animation'
? { animation: target.key }
: { expression: target.key })}
size="100%"
ariaLabel={${JSON.stringify(`${avatarName} avatar`)}}
/>
</section>
<aside className="controls">
<h2>Animations</h2>
<div className="grid">
{animations.map(key => (
<button
key={key}
type="button"
aria-pressed={target.kind === 'animation' && target.key === key}
onClick={() => setTarget({ kind: 'animation', key })}
>
{key}
</button>
))}
</div>
<h2>Expressions</h2>
<div className="grid">
{expressions.map(key => (
<button
key={key}
type="button"
aria-pressed={target.kind === 'expression' && target.key === key}
onClick={() => setTarget({ kind: 'expression', key })}
>
{key}
</button>
))}
</div>
</aside>
</div>
</main>
)
}
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
)
`
const generateReactViteStyles = () => `* { box-sizing: border-box; }
:root { color-scheme: dark; font-family: Inter, system-ui, sans-serif; }
body { margin: 0; min-height: 100vh; padding: 32px; color: #f7f8fa; background: #0d1117; }
main { width: min(980px, 100%); margin: auto; }
h1 { margin: 0 0 24px; }
.demo { display: grid; grid-template-columns: minmax(280px, 1fr) minmax(280px, .8fr); gap: 20px; }
.stage, .controls { border: 1px solid #293140; border-radius: 22px; background: #141a22; }
.stage { display: grid; min-height: 560px; padding: 32px; place-items: center; }
.stage > * { width: min(80%, 460px) !important; height: auto !important; aspect-ratio: 1; }
.controls { padding: 20px; }
h2 { margin: 0 0 12px; font-size: 15px; }
.grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-bottom: 22px; }
button { min-width: 0; padding: 10px; overflow: hidden; color: #dbe3ef; border: 1px solid #354052; border-radius: 10px; background: #1b2330; font: inherit; text-overflow: ellipsis; cursor: pointer; }
button:hover, button[aria-pressed="true"] { color: #fff; border-color: #7392ed; background: #263657; }
@media (max-width: 760px) { body { padding: 16px; } .demo { grid-template-columns: 1fr; } .stage { min-height: 420px; } }
`
export const generateReactViteReadme = (
definitionFileName: string,
avatarName: string
) => `# ${avatarName} · React / TypeScript demo
This ready-to-run Vite project uses the exported ${definitionFileName} definition with
@bible-strong/avatar-react.
## Run the demo
\`\`\`sh
npm install
npm run dev
\`\`\`
Then open the local URL displayed by Vite. The demo includes controls for every exported animation
and expression.
## Production build
\`\`\`sh
npm run build
\`\`\`
The component is created once from the JSON definition with \`createAvatar(definition)\`. TypeScript
derives the accepted animation and expression prop keys from that imported JSON file.
`
export const generateReactVitePackage = (
definition: Readonly<AvatarDefinition>,
avatarName: string
) => {
const definitionFileName = avatarDefinitionFileName(avatarName)
const packageName = `${avatarExportSlug(avatarName)}-avatar-react-demo`
const packageJson = {
name: packageName,
version: '0.0.0',
private: true,
type: 'module',
engines: { node: '>=22.12.0' },
scripts: {
dev: 'vite',
build: 'tsc --noEmit && vite build',
},
dependencies: {
'@bible-strong/avatar-react': avatarRuntimePackageVersion,
react: '^19.0.0',
'react-dom': '^19.0.0',
},
devDependencies: {
'@types/react': '^19.0.0',
'@types/react-dom': '^19.0.0',
'@vitejs/plugin-react': '^6.0.2',
typescript: '~6.0.3',
vite: '^8.0.13',
},
}
const indexHtml = `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>${escapedHtml(avatarName)} · React avatar demo</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
`
const tsconfig = {
compilerOptions: {
target: 'ES2022',
lib: ['ES2022', 'DOM', 'DOM.Iterable'],
strict: true,
module: 'ESNext',
moduleResolution: 'Bundler',
resolveJsonModule: true,
isolatedModules: true,
noEmit: true,
jsx: 'react-jsx',
skipLibCheck: true,
},
include: ['src', 'vite.config.ts'],
}
const viteConfig = `import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
export default defineConfig({ plugins: [react()] })
`
return createStoredZip([
{ name: definitionFileName, content: JSON.stringify(definition, null, 2) },
{ name: 'package.json', content: JSON.stringify(packageJson, null, 2) },
{ name: 'index.html', content: indexHtml },
{ name: 'tsconfig.json', content: JSON.stringify(tsconfig, null, 2) },
{ name: 'vite.config.ts', content: viteConfig },
{
name: 'src/main.tsx',
content: generateReactViteMain(`../${definitionFileName}`, avatarName),
},
{ name: 'src/vite-env.d.ts', content: '/// <reference types="vite/client" />\n' },
{ name: 'src/styles.css', content: generateReactViteStyles() },
{ name: 'README.md', content: generateReactViteReadme(definitionFileName, avatarName) },
])
}
export const generateReactAvatarRuntime = () => `
export type AvatarData<AnimationName extends string = string> = Readonly<{
version: number
@ -665,3 +871,6 @@ export const generateReactAvatarPackage = (payload: AvatarExportPayload) => {
export const avatarExportFileName = (name: string, extension: 'js' | 'tsx' | 'zip') => {
return `${avatarExportSlug(name)}-avatar.${extension}`
}
export const avatarDemoFileName = (name: string, format: 'react' | 'javascript') =>
`${avatarExportSlug(name)}-avatar-${format === 'react' ? 'react' : 'esm'}.zip`

View File

@ -1,6 +1,6 @@
import {
ArrowLeft,
BookOpen,
ArrowRight,
ChevronDown,
ChevronUp,
Copy,
@ -71,6 +71,29 @@ import { RuntimePreviewDialog } from '@/features/studio/components/RuntimePrevie
import { StudioIdentity } from '@/features/studio/components/StudioIdentity'
import type { StudioController } from '@/features/studio/useStudioController'
const reactQuickStartInstall = 'npm install @bible-strong/avatar-react react react-dom'
const webQuickStartInstall = 'npm install @bible-strong/avatar-web'
const reactQuickStartExample = (animationKey: string | undefined) =>
`import { createAvatar } from '@bible-strong/avatar-react'
import '@bible-strong/avatar-react/styles.css'
import definition from './avatar.avatar.json'
const Avatar = createAvatar(definition)
export function App() {
return <Avatar ${animationKey ? `defaultAnimation="${animationKey}"` : 'defaultExpression="neutral"'} />
}`
const webQuickStartExample = (animationKey: string | undefined) =>
`import { createAvatar } from '@bible-strong/avatar-web'
import definition from './avatar.avatar.json'
const avatar = createAvatar('#avatar', {
definition,
${animationKey ? `defaultAnimation: '${animationKey}',` : `defaultExpression: 'neutral',`}
})`
export function StudioInspector({ controller }: { controller: StudioController }) {
const [runtimePreviewOpen, setRuntimePreviewOpen] = useState(false)
const [guideOpen, setGuideOpen] = useState(false)
@ -1637,7 +1660,7 @@ export function StudioInspector({ controller }: { controller: StudioController }
</div>
</InspectorCard>
<InspectorCard>
<section className="export-format-section" aria-label={t('Format')}>
<PanelTitle
title="Format"
subtitle="Choisis l’intégration correspondant à ton projet."
@ -1668,7 +1691,7 @@ export function StudioInspector({ controller }: { controller: StudioController }
</span>
</Button>
</div>
</InspectorCard>
</section>
<InspectorCard>
<div className="preset-header export-animation-header">
@ -1780,10 +1803,41 @@ export function StudioInspector({ controller }: { controller: StudioController }
</InspectorCard>
)}
<InspectorCard className="runtime-example-card">
<div className="runtime-example-heading">
<InspectorCard className="runtime-quick-start-card">
<div className="runtime-quick-start-heading">
<div>
<small>{t('Définition prête à tester')}</small>
<p className="eyebrow">{t('Démarrage rapide')}</p>
<h2>{t('Utiliser cet avatar')}</h2>
</div>
<Button type="button" variant="ghost" onClick={() => setGuideOpen(true)}>
{t('Voir le guide complet')}
<ArrowRight />
</Button>
</div>
<div className="runtime-quick-start-step">
<span>{t('Installation')}</span>
<code>
{exportFormat === 'react' ? reactQuickStartInstall : webQuickStartInstall}
</code>
</div>
<div className="runtime-quick-start-step">
<span>{t('Utilisation minimale')}</span>
<pre tabIndex={0}>
<code>
{exportFormat === 'react'
? reactQuickStartExample(runtimePreviewAnimation)
: webQuickStartExample(runtimePreviewAnimation)}
</code>
</pre>
</div>
</InspectorCard>
<InspectorCard className="runtime-export-card">
<div className="runtime-export-heading">
<div>
<small>{t('Prêt à exporter')}</small>
<strong>
{selectedExportAnimations.length} {t('animations')} ·{' '}
{runtimeDefinitionResult.ok
@ -1792,30 +1846,14 @@ export function StudioInspector({ controller }: { controller: StudioController }
{t('expressions')}
</strong>
</div>
<div className="runtime-example-actions">
<Button
type="button"
variant="outline"
disabled={!runtimeDefinitionResult.ok}
onClick={() => setRuntimePreviewOpen(true)}
>
<Play />
{t('Preview')}
</Button>
<Button type="button" variant="ghost" onClick={() => setGuideOpen(true)}>
<BookOpen />
{t('Guide d’utilisation')}
</Button>
</div>
</div>
{exportFormat === 'javascript' && (
<p className="runtime-export-description">
{t(
'Le ZIP contient le JSON exporté, une démo index.html et son README. La démo charge avatar-web depuis un CDN.'
exportFormat === 'javascript'
? 'Le ZIP contient le JSON exporté, une démo index.html et son README. La démo charge avatar-web depuis un CDN.'
: 'Le ZIP contient le JSON exporté et un projet Vite React TypeScript prêt à lancer avec npm install puis npm run dev.'
)}
</p>
)}
</InspectorCard>
</div>
<div className="runtime-export-actions">
<Button
@ -1831,23 +1869,37 @@ export function StudioInspector({ controller }: { controller: StudioController }
type="button"
variant="outline"
disabled={!runtimeDefinitionResult.ok}
onClick={() => void copyAvatarRuntimeDefinition()}
onClick={downloadAvatarExport}
>
<Copy />
{t('Copier le JSON formaté')}
<Download />
{t(
exportFormat === 'javascript'
? 'Télécharger la démo ESM (.zip)'
: 'Télécharger la démo React (.zip)'
)}
</Button>
{exportFormat === 'javascript' && (
<div className="runtime-export-secondary-actions">
<Button
type="button"
variant="outline"
disabled={!runtimeDefinitionResult.ok}
onClick={downloadAvatarExport}
onClick={() => setRuntimePreviewOpen(true)}
>
<Download />
{t('Télécharger l’intégration ESM (.zip)')}
<Play />
{t('Preview')}
</Button>
<Button
type="button"
variant="outline"
disabled={!runtimeDefinitionResult.ok}
onClick={() => void copyAvatarRuntimeDefinition()}
>
<Copy />
{t('Copier le JSON')}
</Button>
)}
</div>
</div>
</InspectorCard>
{runtimeCopyStatus !== 'idle' && (
<p
className="runtime-copy-status"

View File

@ -83,7 +83,11 @@ import {
} from '@/features/avatar/geometry'
import { defaultExpression } from '@/features/avatar/presets'
import { type SurfaceConfig } from '@/features/avatar/surfaces'
import { avatarExportFileName, generateJavaScriptEsmPackage } from '@/features/export/exporter'
import {
avatarDemoFileName,
generateJavaScriptEsmPackage,
generateReactVitePackage,
} from '@/features/export/exporter'
import {
serializeAvatarSnapshot,
serializePixelSnapshot,
@ -1569,10 +1573,12 @@ export function useStudioController() {
)
}
const downloadAvatarExport = () => {
if (!runtimeDefinitionResult.ok || exportFormat !== 'javascript') return
if (!runtimeDefinitionResult.ok) return
downloadBlob(
generateJavaScriptEsmPackage(runtimeDefinitionResult.value, activeAvatar.name),
avatarExportFileName(activeAvatar.name, 'zip')
exportFormat === 'javascript'
? generateJavaScriptEsmPackage(runtimeDefinitionResult.value, activeAvatar.name)
: generateReactVitePackage(runtimeDefinitionResult.value, activeAvatar.name),
avatarDemoFileName(activeAvatar.name, exportFormat)
)
}
const downloadAvatarRuntimeDefinition = () => {

View File

@ -141,6 +141,7 @@ const english: Record<string, string> = {
'Fix the highlighted keys in the Expressions or Animations editors.',
'Télécharger la définition .avatar.json': 'Download .avatar.json definition',
'Copier le JSON formaté': 'Copy formatted JSON',
'Copier le JSON': 'Copy JSON',
'JSON runtime copié dans le presse-papiers.': 'Runtime JSON copied to the clipboard.',
'Impossible de copier le JSON runtime.': 'Could not copy the runtime JSON.',
Installation: 'Installation',
@ -149,6 +150,7 @@ const english: Record<string, string> = {
'Aperçu avec le package React': 'Preview using the React package',
Preview: 'Preview',
'Définition prête à tester': 'Definition ready to test',
'Démarrage rapide': 'Quick start',
'Preview de la définition exportée': 'Exported definition preview',
'Teste les animations et expressions réellement présentes dans le fichier .avatar.json.':
'Test the animations and expressions actually included in the .avatar.json file.',
@ -167,6 +169,7 @@ const english: Record<string, string> = {
'Expression de départ': 'Starting expression',
'Aucune animation sélectionnée': 'No animation selected',
'Guide d’utilisation': 'Usage guide',
'Voir le guide complet': 'View full usage guide',
'Guide d’utilisation de l’avatar React': 'React avatar usage guide',
'Guide d’utilisation de l’avatar JavaScript': 'JavaScript avatar usage guide',
'Installe le package, crée ton composant et choisis le niveau de contrôle adapté.':
@ -305,7 +308,15 @@ const english: Record<string, string> = {
'Intégration ESM avec le package avatar-web': 'ESM integration with the avatar-web package',
'Le ZIP contient le JSON exporté, une démo index.html et son README. La démo charge avatar-web depuis un CDN.':
'The ZIP contains the exported JSON, an index.html demo and its README. The demo loads avatar-web from a CDN.',
'Le ZIP contient le JSON exporté et un projet Vite React TypeScript prêt à lancer avec npm install puis npm run dev.':
'The ZIP contains the exported JSON and a ready-to-run Vite React TypeScript project. Start it with npm install, then npm run dev.',
'Télécharger l’intégration ESM (.zip)': 'Download ESM integration (.zip)',
'Télécharger la démo React (.zip)': 'Download React demo (.zip)',
'Télécharger la démo ESM (.zip)': 'Download ESM demo (.zip)',
'Télécharger le JSON': 'Download JSON',
'Utilisation minimale': 'Minimal usage',
'Utiliser cet avatar': 'Use this avatar',
'Prêt à exporter': 'Ready to export',
Snapshot: 'Snapshot',
'Mode photo': 'Photo Mode',
'Capture une image statique de l’avatar.': 'Capture a static image of the avatar.',

View File

@ -110,6 +110,7 @@ export const chinese: Record<string, string> = {
'请在“表情”或“动画”编辑器中修正标记的键。',
'Télécharger la définition .avatar.json': '下载 .avatar.json 定义',
'Copier le JSON formaté': '复制格式化的 JSON',
'Copier le JSON': '复制 JSON',
'JSON runtime copié dans le presse-papiers.': '运行时 JSON 已复制到剪贴板。',
'Impossible de copier le JSON runtime.': '无法复制运行时 JSON。',
Installation: '安装',
@ -118,6 +119,7 @@ export const chinese: Record<string, string> = {
'Aperçu avec le package React': '使用 React 包预览',
Preview: '预览',
'Définition prête à tester': '定义已可测试',
'Démarrage rapide': '快速开始',
'Preview de la définition exportée': '导出定义预览',
'Teste les animations et expressions réellement présentes dans le fichier .avatar.json.':
'测试 .avatar.json 文件中实际包含的动画和表情。',
@ -136,6 +138,7 @@ export const chinese: Record<string, string> = {
'Expression de départ': '起始表情',
'Aucune animation sélectionnée': '未选择动画',
'Guide d’utilisation': '使用指南',
'Voir le guide complet': '查看完整使用指南',
'Guide d’utilisation de l’avatar React': 'React 头像使用指南',
'Guide d’utilisation de l’avatar JavaScript': 'JavaScript 头像使用指南',
'Installe le package, crée ton composant et choisis le niveau de contrôle adapté.':
@ -266,7 +269,15 @@ export const chinese: Record<string, string> = {
'Intégration ESM avec le package avatar-web': '使用 avatar-web 包的 ESM 集成',
'Le ZIP contient le JSON exporté, une démo index.html et son README. La démo charge avatar-web depuis un CDN.':
'ZIP 包含导出的 JSON、index.html 演示和 README。演示会从 CDN 加载 avatar-web。',
'Le ZIP contient le JSON exporté et un projet Vite React TypeScript prêt à lancer avec npm install puis npm run dev.':
'ZIP 包含导出的 JSON 和一个可直接运行的 Vite React TypeScript 项目。先运行 npm install,再运行 npm run dev。',
'Télécharger l’intégration ESM (.zip)': '下载 ESM 集成(.zip)',
'Télécharger la démo React (.zip)': '下载 React 演示(.zip)',
'Télécharger la démo ESM (.zip)': '下载 ESM 演示(.zip)',
'Télécharger le JSON': '下载 JSON',
'Utilisation minimale': '最简用法',
'Utiliser cet avatar': '使用此头像',
'Prêt à exporter': '可以导出',
Snapshot: '快照',
'Mode photo': '照片模式',
'Capture une image statique de l’avatar.': '捕获头像的静态图像。',