webconsulting-create-documentation
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesewebconsulting — Create Documentation
webconsulting — 文档创建方案
End-to-end workflow for building product documentation: help pages, AI-generated illustrations, narrated product tour videos (Remotion + ElevenLabs TTS + Suno AI music), and GitHub README visual documentation.
构建产品文档的端到端工作流:帮助页面、AI生成插图、带旁白的产品导览视频(Remotion + ElevenLabs TTS + Suno AI音乐),以及GitHub README可视化文档。
Prerequisites
前置条件
Required
必需依赖
| Dependency | Install | Purpose |
|---|---|---|
| Node.js 20+ | Pre-installed | Runtime |
| Remotion | | Video composition |
| qrcode.react | | QR codes in end card |
| tsx | | Run TypeScript scripts |
| 依赖项 | 安装方式 | 用途 |
|---|---|---|
| Node.js 20+ | 预先安装 | 运行时环境 |
| Remotion | | 视频合成 |
| qrcode.react | | 末尾卡片中的二维码生成 |
| tsx | | 运行TypeScript脚本 |
Optional (for production quality)
可选依赖(用于生产级质量)
| Dependency | Install | Purpose |
|---|---|---|
| ElevenLabs API key | elevenlabs.io | Premium TTS narration (Jony Ive voice) |
| ffmpeg | | Convert audio to MP3 (smaller files) |
macOS | Built-in on macOS | Fallback TTS (Daniel voice, outputs WAV) |
| Suno API key | api.box | AI-generated background music |
| 依赖项 | 安装方式 | 用途 |
|---|---|---|
| ElevenLabs API密钥 | elevenlabs.io | 高级TTS旁白(如Jony Ive风格语音) |
| ffmpeg | | 将音频转换为MP3(减小文件体积) |
macOS | macOS系统内置 | 备用TTS(Daniel语音,输出WAV格式) |
| Suno API密钥 | api.box | AI生成背景音乐 |
ElevenLabs Setup
ElevenLabs 配置
- Create a free account at elevenlabs.io
- Navigate to Profile + API key in the sidebar
- Copy your API key
- Add to your file:
.env
bash
ELEVENLABS_API_KEY=your-api-key-hereAlso add to (commented out) for team documentation:
.env.examplebash
undefined- 在elevenlabs.io创建免费账号
- 在侧边栏进入Profile + API key页面
- 复制你的API密钥
- 添加到文件中:
.env
bash
ELEVENLABS_API_KEY=your-api-key-here同时将其添加到文件中(注释状态),用于团队文档共享:
.env.examplebash
undefinedELEVENLABS_API_KEY=your-elevenlabs-api-key
ELEVENLABS_API_KEY=your-elevenlabs-api-key
**Voice selection**: The default voice is "Daniel" (`onwK4e9ZLuTAKqWW03F9`) — British, calm.
Browse [elevenlabs.io/voice-library](https://elevenlabs.io/voice-library) and update
the `VOICE_ID` in `scripts/generate-narration.ts` if a better match is found.
**Free tier limits**: ~10,000 characters/month. The full narration script is ~600 characters,
so you can regenerate ~16 times per month on the free tier.
**语音选择**:默认语音为"Daniel"(ID:`onwK4e9ZLuTAKqWW03F9`)——英式口音,语调沉稳。可以浏览[elevenlabs.io/voice-library](https://elevenlabs.io/voice-library),如果找到更匹配的语音,更新`scripts/generate-narration.ts`中的`VOICE_ID`即可。
**免费版限制**:每月约10,000字符额度。完整旁白脚本约600字符,因此免费版每月可重新生成约16次。Workflow Overview
工作流概览
1. Create help page → React component with collapsible sections
2. Generate illustrations → AI image generation tool (GenerateImage)
3. Build Remotion video → Animated scenes with narration subtitles
4. Generate TTS audio → ElevenLabs API or macOS `say` fallback
5. Generate music → Suno AI background music (optional)
6. Render final video → npm run remotion:render
7. GitHub README → Embed screenshots + video in README.md1. 创建帮助页面 → 基于React组件实现可折叠章节
2. 生成插图 → 使用AI图像生成工具(GenerateImage)
3. 构建Remotion视频 → 包含旁白字幕的动画场景
4. 生成TTS音频 → 使用ElevenLabs API或macOS `say`备用方案
5. 生成背景音乐 → 使用Suno AI生成背景音乐(可选)
6. 渲染最终视频 → 执行npm run remotion:render
7. 完善GitHub README → 在README.md中嵌入截图和视频Phase 1: Help Page
阶段1:帮助页面
Create an in-app help page at .
src/app/(dashboard)/help/page.tsx在路径创建应用内帮助页面。
src/app/(dashboard)/help/page.tsxStructure pattern
结构模式
tsx
// Collapsible section wrapper
function HelpSection({ id, icon, title, description, children, defaultOpen }) {
const [open, setOpen] = useState(defaultOpen);
return (
<Card id={id}>
<button onClick={() => setOpen(o => !o)}>
{/* Header with icon, title, description, chevron */}
</button>
{open && <CardContent>{children}</CardContent>}
</Card>
);
}
// Step-by-step instruction
function Step({ number, title, children }) { /* numbered step */ }
// Pro tip callout
function Tip({ children }) { /* highlighted tip box */ }
// Screenshot placeholder
function Screenshot({ src, alt, caption }) { /* Next/Image with caption */ }tsx
// 可折叠章节封装组件
function HelpSection({ id, icon, title, description, children, defaultOpen }) {
const [open, setOpen] = useState(defaultOpen);
return (
<Card id={id}>
<button onClick={() => setOpen(o => !o)}>
{/* 包含图标、标题、描述和展开/收起箭头的头部 */}
</button>
{open && <CardContent>{children}</CardContent>}
</Card>
);
}
// 分步说明组件
function Step({ number, title, children }) { /* 带编号的步骤 */ }
// 提示框组件
function Tip({ children }) { /* 高亮显示的提示框 */ }
// 截图占位组件
function Screenshot({ src, alt, caption }) { /* 带标题的Next/Image组件 */ }Key sections to include
关键章节内容
- Table of Contents — auto-generated from section data with anchor links
- Getting Started — first-time user orientation (default open)
- Feature sections — one per major feature (Dashboard, Clients, Extensions, etc.)
- Product Video — embedded element pointing to rendered MP4
<video> - FAQ / Troubleshooting — common questions
- 目录 — 从章节数据自动生成,包含锚点链接
- 快速入门 — 面向首次用户的引导内容(默认展开)
- 功能章节 — 每个主要功能对应一个章节(如仪表盘、客户管理、扩展功能等)
- 产品视频 — 嵌入指向渲染完成的MP4文件的元素
<video> - 常见问题/故障排除 — 汇总用户常见问题
Writing guidelines
写作指南
- Target audience: users with low technical knowledge
- Write as a senior technical writer — clear, friendly, no jargon
- Every section has numbered steps with screenshots
- Include "Tip" callouts for pro tips
- Use Tailwind v4 syntax (not
mt-3!)!mt-3
- 目标受众:技术水平较低的用户
- 写作风格:以资深技术文档作者的视角,内容清晰、友好,无专业黑话
- 每个章节都包含带截图的编号步骤
- 加入"提示"框提供专业技巧
- 使用Tailwind v4语法(如而非
mt-3!)!mt-3
Phase 2: Screenshots / Illustrations
阶段2:截图/插图
Since browser automation for screenshots can be unreliable, use AI-generated illustrations.
由于浏览器自动化截图的可靠性较低,建议使用AI生成插图。
Using the GenerateImage tool
使用GenerateImage工具
GenerateImage({
description: "Modern flat illustration of a monitoring dashboard with
stat cards showing Total Clients, Extensions, Security Issues.
Dark theme, clean UI, subtle orange accents.",
filename: "dashboard.png"
})Place generated images in :
public/help/public/help/
├── dashboard.png
├── clients.png
├── wizard.png
└── product-tour.mp4 (rendered later)Reference in the help page:
tsx
<Screenshot src="/help/dashboard.png" alt="Dashboard overview" caption="The main dashboard" />GenerateImage({
description: "Modern flat illustration of a monitoring dashboard with
stat cards showing Total Clients, Extensions, Security Issues.
Dark theme, clean UI, subtle orange accents.",
filename: "dashboard.png"
})将生成的图片放置在目录下:
public/help/public/help/
├── dashboard.png
├── clients.png
├── wizard.png
└── product-tour.mp4 (后续渲染生成)在帮助页面中引用:
tsx
<Screenshot src="/help/dashboard.png" alt="Dashboard overview" caption="The main dashboard" />Phase 3: Remotion Product Video
阶段3:Remotion产品视频
Directory structure
目录结构
remotion/
├── index.ts # registerRoot entry point
├── Root.tsx # Composition definitions
└── ProductTour.tsx # All scenes + narration scriptremotion/
├── index.ts # registerRoot入口文件
├── Root.tsx # 合成定义
└── ProductTour.tsx # 所有场景和旁白脚本Composition setup (Root.tsx
)
Root.tsx合成配置(Root.tsx
)
Root.tsxtsx
import { Composition } from "remotion";
import { ProductTour } from "./ProductTour";
export const RemotionRoot = () => (
<Composition
id="ProductTour"
component={ProductTour}
durationInFrames={30 * 45} // 45 seconds at 30fps
fps={30}
width={1920}
height={1080}
/>
);tsx
import { Composition } from "remotion";
import { ProductTour } from "./ProductTour";
export const RemotionRoot = () => (
<Composition
id="ProductTour"
component={ProductTour}
durationInFrames={30 * 45} // 30fps下时长45秒
fps={30}
width={1920}
height={1080}
/>
);Scene pattern
场景模式
Each scene follows this structure:
tsx
function SceneComponent() {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
return (
<AbsoluteFill style={{ backgroundColor: DARK_BG, padding: 80 }}>
<SceneAudio scene="scene-name" /> {/* TTS audio */}
<FadeInText fontSize={56}>Title</FadeInText> {/* Animated heading */}
{/* Scene-specific content with spring/interpolate animations */}
<NarrationSubtitle text={NARRATION_SCRIPT.sceneName} /> {/* Subtitles */}
</AbsoluteFill>
);
}每个场景遵循以下结构:
tsx
function SceneComponent() {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
return (
<AbsoluteFill style={{ backgroundColor: DARK_BG, padding: 80 }}>
<SceneAudio scene="scene-name" /> {/* TTS音频 */}
<FadeInText fontSize={56}>Title</FadeInText> {/* 动画标题 */}
{/* 场景特定内容,包含弹簧/插值动画 */}
<NarrationSubtitle text={NARRATION_SCRIPT.sceneName} /> {/* 字幕 */}
</AbsoluteFill>
);
}Recommended scenes (7 total, ~75 seconds)
推荐场景(共7个,约75秒)
Durations are dynamic — driven by narration audio length (see Phase 5).
The video includes a 1.5s lead-in (dark + exponential volume ramp) and
a 4s end card with social links that fades to black.
| # | Scene | ~Duration | Content |
|---|---|---|---|
| — | Lead-in | 1.5s | Dark screen, music fades in (exponential t^2.5) |
| 1 | Intro | 13s | Logo + tagline + brand identity |
| 2 | Dashboard | 12s | Stat cards with spring animations |
| 3 | Clients | 12s | Client list with slide-in rows |
| 4 | Wizard | 11s | Step indicator with progressive activation |
| 5 | Security | 11s | Feature grid with staggered fade-in |
| 6 | Outro | 11s | Logo + CTA + credits + website (highlight blog) |
| 7 | End Card | 7s | 4 QR codes (website, GitHub, YouTube, X) + scale-up finale |
时长为动态值 — 由旁白音频长度决定(见阶段5)。视频包含1.5秒的开场过渡(黑屏+指数级音量渐变)和4秒的末尾卡片(包含社交链接,渐变为黑屏)。
| 序号 | 场景 | 预估时长 | 内容 |
|---|---|---|---|
| — | 开场过渡 | 1.5秒 | 黑屏,背景音乐淡入(指数曲线t^2.5) |
| 1 | 介绍 | 13秒 | Logo+标语+品牌标识 |
| 2 | 仪表盘 | 12秒 | 带弹簧动画的统计卡片 |
| 3 | 客户管理 | 12秒 | 带滑入动画的客户列表 |
| 4 | 向导流程 | 11秒 | 逐步激活的步骤指示器 |
| 5 | 安全功能 | 11秒 | 带交错淡入动画的功能网格 |
| 6 | 结尾 | 11秒 | Logo+行动号召+致谢+官网链接(突出博客板块) |
| 7 | 末尾卡片 | 7秒 | 4个二维码(官网、GitHub、YouTube、X)+ 缩放收尾动画 |
Lead-in and end card
开场过渡与末尾卡片
The lead-in uses an exponential volume curve () because
human hearing follows a logarithmic scale (Weber-Fechner law). A linear ramp
sounds "sudden" in the middle; the power curve feels perceptually smooth.
Math.pow(t, 2.5)The end card shows 4 QR codes with social/website links (staggered slide-up).
No fade-to-black — the QR codes stay fully visible so viewers can scan them.
In the final 2 seconds, the entire card scales up dramatically (1.0 → 1.5)
with the T3 Monitoring logo, creating a confident, cinematic close.
The "Find us on" heading scales up in sync but also moves upward (-120px)
so it doesn't get covered by the expanding QR code row.
QR codes are rendered inline using ().
qrcode.reactnpm install qrcode.reacttsx
export const LEAD_IN_FRAMES = Math.round(30 * 1.5); // 45 frames
export const END_CARD_FRAMES = Math.round(30 * 7); // 210 framesThese are added to in for the composition duration.
TOTAL_FRAMESRoot.tsx开场过渡使用指数音量曲线(),因为人类听觉遵循对数规律(韦伯-费希纳定律)。线性渐变会让中间部分听起来“突兀”,而幂曲线能带来感知上的平滑过渡。
Math.pow(t, 2.5)末尾卡片展示4个社交/官网链接的二维码(交错滑入)。无渐黑效果 — 二维码保持完全可见,方便观众扫码。在最后2秒,整个卡片从1.0放大到1.5,同时T3 Monitoring Logo同步缩放,营造自信、电影感的收尾效果。“关注我们”标题同步放大并向上移动(-120px),避免被扩展的二维码行遮挡。
二维码使用()内联渲染。
qrcode.reactnpm install qrcode.reacttsx
export const LEAD_IN_FRAMES = Math.round(30 * 1.5); // 45帧
export const END_CARD_FRAMES = Math.round(30 * 7); // 210帧这些值会添加到的中,用于合成时长计算。
Root.tsxTOTAL_FRAMESEnd card links
末尾卡片链接
| # | Platform | Handle | Highlight |
|---|---|---|---|
| 1 | Website | webconsulting.at | Orange border, |
| 2 | GitHub | @dirnbauer | — |
| 3 | YouTube | @webconsulting-curt | — |
| 4 | X | @KDirnbauer | — |
The website card is emphasized: orange handle text, orange QR border,
and a pill badge below — glass-style container with orange border,
glow shadow, and "Tutorials & deep dives" label.
/blog| 序号 | 平台 | 账号 | 突出样式 |
|---|---|---|---|
| 1 | 官网 | webconsulting.at | 橙色边框,显示 |
| 2 | GitHub | @dirnbauer | — |
| 3 | YouTube | @webconsulting-curt | — |
| 4 | X | @KDirnbauer | — |
官网卡片会被重点突出:橙色账号文本、橙色二维码边框,下方带有标签 — 玻璃态容器+橙色边框+发光阴影+“教程与深度解析”说明。
/blogQR code pattern
二维码模式
tsx
import { QRCodeSVG } from "qrcode.react";
<QRCodeSVG value="https://webconsulting.at" size={140}
bgColor="#ffffff" fgColor="#0f172a" level="M" />Each social card shows: platform icon + handle, QR code (white bg, dark fg,
rounded container), URL text, and optional highlight callout.
tsx
import { QRCodeSVG } from "qrcode.react";
<QRCodeSVG value="https://webconsulting.at" size={140}
bgColor="#ffffff" fgColor="#0f172a" level="M" />每个社交卡片包含:平台图标+账号、二维码(白色背景、深色前景、圆角容器)、URL文本,以及可选的突出标注。
Animation toolkit
动画工具集
tsx
// Fade + slide up
const opacity = interpolate(frame - delay, [0, fps * 0.6], [0, 1], {
extrapolateLeft: "clamp", extrapolateRight: "clamp",
});
// Spring bounce for cards
const scale = spring({ fps, frame: frame - i * 8, config: { damping: 80 } });
// Staggered slide-in for list items
const slideX = interpolate(frame - i * 10, [0, fps * 0.5], [100, 0], {
extrapolateLeft: "clamp", extrapolateRight: "clamp",
});tsx
// 淡入+上滑
const opacity = interpolate(frame - delay, [0, fps * 0.6], [0, 1], {
extrapolateLeft: "clamp", extrapolateRight: "clamp",
});
// 卡片弹簧弹跳效果
const scale = spring({ fps, frame: frame - i * 8, config: { damping: 80 } });
// 列表项交错滑入
const slideX = interpolate(frame - i * 10, [0, fps * 0.5], [100, 0], {
extrapolateLeft: "clamp", extrapolateRight: "clamp",
});Phase 4: Narration (Jony Ive Style)
阶段4:旁白(Jony Ive风格)
Voice characteristics
语音特征
- Tone: Calm, deliberate, aspirational
- Pace: ~140 words per minute (slow, measured)
- Style: Short declarative sentences. Dramatic pauses. Marketing-punchy.
- Influence: Sir Jonathan Ive — think Apple product launch presentations
- 语调:沉稳、从容、有感染力
- 语速:约140词/分钟(缓慢、有节奏感)
- 风格:短句、陈述句。带有戏剧性停顿。营销感十足。
- 参考风格:乔纳森·伊夫爵士 — 参考苹果产品发布会的旁白风格
Writing narration copy
旁白文案写作
BAD: "This is a dashboard that shows you statistics about your clients."
GOOD: "Your dashboard. Beautifully considered. Every metric, thoughtfully
placed, to give you immediate clarity."反面示例: "This is a dashboard that shows you statistics about your clients."
正面示例: "Your dashboard. Beautifully considered. Every metric, thoughtfully
placed, to give you immediate clarity."Export the narration script (single source of truth)
导出旁白脚本(单一数据源)
Define all narration text in as an exported constant.
This is the single source of truth for both subtitles and audio generation:
ProductTour.tsxtsx
// remotion/ProductTour.tsx
export const NARRATION_SCRIPT: Record<string, string> = {
intro: "Imagine seeing every TYPO3 installation you manage...",
dashboard: "Your dashboard gives you the full picture...",
// ... one entry per scene
};Critical: The generate script imports this constant — never duplicate the text:
tsx
// scripts/generate-narration.ts
import { NARRATION_SCRIPT } from "../remotion/ProductTour.js";
const NARRATION = NARRATION_SCRIPT; // Same text for audio + subtitlesThis guarantees spoken audio always matches the on-screen subtitles.
If you edit , just re-run and
— both audio and subtitles update automatically.
NARRATION_SCRIPTnpm run narration:generatenpm run remotion:render在中定义所有旁白文本为导出常量。这是字幕和音频生成的单一数据源:
ProductTour.tsxtsx
// remotion/ProductTour.tsx
export const NARRATION_SCRIPT: Record<string, string> = {
intro: "Imagine seeing every TYPO3 installation you manage...",
dashboard: "Your dashboard gives you the full picture...",
// ...每个场景对应一条文案
};关键注意事项:生成脚本必须导入该常量 — 绝对不要重复编写文本:
tsx
// scripts/generate-narration.ts
import { NARRATION_SCRIPT } from "../remotion/ProductTour.js";
const NARRATION = NARRATION_SCRIPT; // 音频和字幕使用相同文本这能保证语音旁白与屏幕字幕始终保持一致。如果修改,只需重新运行和 — 音频和字幕会自动更新。
NARRATION_SCRIPTnpm run narration:generatenpm run remotion:renderNarrationSubtitle component
NarrationSubtitle组件
Cinematic lower-third with word-by-word reveal, slide-up entrance, and active-word glow.
Always reads from — update the script and subtitles auto-sync.
NARRATION_SCRIPTtsx
function NarrationSubtitle({ text }: { text: string }) {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Slide-up + fade entrance (0.4s → 0.9s)
const fadeIn = interpolate(frame, [fps * 0.4, fps * 0.9], [0, 1], { ... });
const slideUp = interpolate(frame, [fps * 0.4, fps * 0.9], [20, 0], { ... });
const words = text.split(" ");
const framesPerWord = fps / 2.8; // calm pace, ~2.8 words/sec
return (
<div style={{ position: "absolute", bottom: 60, opacity: fadeIn,
transform: `translateY(${slideUp}px)` }}>
<div style={{
maxWidth: 1100, padding: "24px 56px",
background: "linear-gradient(135deg, rgba(0,0,0,0.72), rgba(20,20,30,0.68))",
borderRadius: 20, border: "1px solid rgba(255,255,255,0.08)",
boxShadow: "0 8px 32px rgba(0,0,0,0.4), 0 0 80px rgba(255,120,50,0.04)",
}}>
{/* Subtle top accent line */}
<div style={{ position: "absolute", top: 0, left: 40, right: 40, height: 1,
background: "linear-gradient(90deg, transparent, rgba(255,255,255,0.15), transparent)" }} />
{words.map((word, i) => {
const wordDelay = fps * 0.5 + i * framesPerWord;
const wordOpacity = interpolate(frame, [wordDelay, wordDelay + framesPerWord * 0.5],
[0.25, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
const isActive = frame >= wordDelay && frame < wordDelay + framesPerWord * 1.2;
return (
<span key={i} style={{
fontSize: 30, fontWeight: 300, color: "#fff",
fontFamily: "'SF Pro Display', 'Inter', system-ui",
opacity: isActive ? 1 : wordOpacity, letterSpacing: "0.02em",
textShadow: isActive ? "0 0 20px rgba(255,255,255,0.15)" : "none",
}}>{word}</span>
);
})}
</div>
</div>
);
}电影感的下方字幕条,支持逐词显示、滑入入场动画和当前词高亮效果。始终从读取文本 — 修改脚本后,字幕会自动同步更新。
NARRATION_SCRIPTtsx
function NarrationSubtitle({ text }: { text: string }) {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// 滑入+淡入入场动画(0.4秒→0.9秒)
const fadeIn = interpolate(frame, [fps * 0.4, fps * 0.9], [0, 1], { ... });
const slideUp = interpolate(frame, [fps * 0.4, fps * 0.9], [20, 0], { ... });
const words = text.split(" ");
const framesPerWord = fps / 2.8; // 沉稳语速,约2.8词/秒
return (
<div style={{ position: "absolute", bottom: 60, opacity: fadeIn,
transform: `translateY(${slideUp}px)` }}>
<div style={{
maxWidth: 1100, padding: "24px 56px",
background: "linear-gradient(135deg, rgba(0,0,0,0.72), rgba(20,20,30,0.68))",
borderRadius: 20, border: "1px solid rgba(255,255,255,0.08)",
boxShadow: "0 8px 32px rgba(0,0,0,0.4), 0 0 80px rgba(255,120,50,0.04)",
}}>
{/* 顶部细微装饰线 */}
<div style={{ position: "absolute", top: 0, left: 40, right: 40, height: 1,
background: "linear-gradient(90deg, transparent, rgba(255,255,255,0.15), transparent)" }} />
{words.map((word, i) => {
const wordDelay = fps * 0.5 + i * framesPerWord;
const wordOpacity = interpolate(frame, [wordDelay, wordDelay + framesPerWord * 0.5],
[0.25, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
const isActive = frame >= wordDelay && frame < wordDelay + framesPerWord * 1.2;
return (
<span key={i} style={{
fontSize: 30, fontWeight: 300, color: "#fff",
fontFamily: "'SF Pro Display', 'Inter', system-ui",
opacity: isActive ? 1 : wordOpacity, letterSpacing: "0.02em",
textShadow: isActive ? "0 0 20px rgba(255,255,255,0.15)" : "none",
}}>{word}</span>
);
})}
</div>
</div>
);
}Subtitle styling principles
字幕设计原则
- Auto-sync: Component reads directly — change the script text and the subtitles update automatically on next render
NARRATION_SCRIPT[scene] - Slide-up entrance: Subtitles slide up 20px while fading in (0.4s–0.9s), feels organic
- Glass morphism container: Dark gradient background with subtle border and box-shadow, plus a thin accent line at the top for depth
- Word-by-word reveal: Each word fades from 0.25 to 1.0 opacity at ~2.8 words/second
- Active word glow: The currently spoken word gets full opacity + a soft white
text-shadow - Typography: SF Pro Display / Inter, weight 300 (light), 30px, generous letter-spacing
- Positioning: 60px from bottom, centered, max-width 1100px
- 自动同步:组件直接读取— 修改脚本文本后,下次渲染时字幕会自动更新
NARRATION_SCRIPT[scene] - 滑入入场:字幕在淡入的同时向上滑动20px(0.4秒–0.9秒),过渡自然
- 玻璃态容器:深色渐变背景+细微边框和阴影,顶部带有细装饰线增加层次感
- 逐词显示:每个单词从0.25淡入到1.0,速度约为2.8词/秒
- 当前词高亮:正在朗读的单词保持完全不透明度+柔和白色
text-shadow - 排版:SF Pro Display / Inter字体,字重300(轻量),字号30px,字母间距适中
- 定位:距离底部60px,居中显示,最大宽度1100px
When to update subtitles
字幕更新时机
Subtitles auto-update whenever you change in .
The full pipeline after editing narration text:
NARRATION_SCRIPTProductTour.tsxbash
undefined每当修改中的时,字幕会自动更新。修改旁白文本后的完整流程:
ProductTour.tsxNARRATION_SCRIPTbash
undefined1. Edit NARRATION_SCRIPT in remotion/ProductTour.tsx
1. 在remotion/ProductTour.tsx中编辑NARRATION_SCRIPT
2. Regenerate audio (subtitles already match the new text)
2. 重新生成音频(字幕已与新文本匹配)
npm run narration:generate
npm run narration:generate
3. Re-render video
3. 重新渲染视频
npm run remotion:render
undefinednpm run remotion:render
undefinedPhase 5: TTS Audio Generation
阶段5:TTS音频生成
Script: scripts/generate-narration.ts
scripts/generate-narration.ts脚本:scripts/generate-narration.ts
scripts/generate-narration.tsThe script supports two backends and auto-generates
so the video composition always uses the correct file extension.
remotion/narration-format.ts| Backend | Trigger | Output | Quality |
|---|---|---|---|
| ElevenLabs | | | Production |
macOS | No API key, macOS detected | | Development preview |
bash
undefined该脚本支持两种后端,并自动生成,确保视频合成始终使用正确的文件扩展名。
remotion/narration-format.ts| 后端 | 触发条件 | 输出格式 | 质量 |
|---|---|---|---|
| ElevenLabs | | | 生产级 |
macOS | 无API密钥且检测到macOS系统 | | 开发预览级 |
bash
undefinedProduction: use ElevenLabs (add key to .env first)
生产环境:使用ElevenLabs(需先在.env中添加密钥)
npm run narration:generate
npm run narration:generate
Development: macOS fallback (no key needed)
开发环境:使用macOS备用方案(无需密钥)
npm run narration:generate
The script automatically:
1. Cleans old audio files (prevents mixing formats)
2. Generates audio files in `public/narration/`
3. Probes each file's duration using `afinfo` (macOS) or `ffprobe`
4. Writes `remotion/narration-format.ts` with the correct extension (`"mp3"` or `"wav"`)
5. Writes `remotion/narration-durations.ts` with per-scene frame counts (audio + 2s padding)
6. The Remotion composition imports both configs for synced playbacknpm run narration:generate
该脚本会自动执行以下操作:
1. 清理旧音频文件(避免格式混合)
2. 在`public/narration/`目录下生成音频文件
3. 使用`afinfo`(macOS)或`ffprobe`探测每个文件的时长
4. 写入`remotion/narration-format.ts`,包含正确的扩展名(`"mp3"`或`"wav"`)
5. 写入`remotion/narration-durations.ts`,包含每个场景的帧数(音频时长+2秒缓冲)
6. Remotion合成会导入这两个配置文件,实现同步播放ElevenLabs voice settings for Jony Ive style
实现Jony Ive风格的ElevenLabs语音设置
json
{
"voice_id": "onwK4e9ZLuTAKqWW03F9",
"model_id": "eleven_multilingual_v2",
"voice_settings": {
"stability": 0.75,
"similarity_boost": 0.70,
"style": 0.35,
"use_speaker_boost": true
}
}json
{
"voice_id": "onwK4e9ZLuTAKqWW03F9",
"model_id": "eleven_multilingual_v2",
"voice_settings": {
"stability": 0.75,
"similarity_boost": 0.70,
"style": 0.35,
"use_speaker_boost": true
}
}Audio file location
音频文件位置
public/narration/
├── intro.mp3 (or .wav)
├── dashboard.mp3
├── clients.mp3
├── wizard.mp3
├── security.mp3
└── outro.mp3
remotion/
├── narration-format.ts ← auto-generated, audio extension ("mp3" | "wav")
└── narration-durations.ts ← auto-generated, per-scene frame counts + totalpublic/narration/
├── intro.mp3 (或.wav)
├── dashboard.mp3
├── clients.mp3
├── wizard.mp3
├── security.mp3
└── outro.mp3
remotion/
├── narration-format.ts ← 自动生成,音频扩展名("mp3" | "wav")
└── narration-durations.ts ← 自动生成,每个场景的帧数+总时长Audio-video sync (critical)
音视频同步(关键)
Scene durations are driven by audio length, not hardcoded. The generate script:
- Probes each audio file's duration (e.g., intro = 8.5s)
- Adds 2s padding for visual animation breathing room
- Writes frame count:
ceil((8.5 + 2) * 30) = 317 frames - imports these and uses them for
ProductTour.tsxdurationsSequence
Each scene also has:
- Audio fade-out (last 1s) — prevents abrupt cut-off between scenes
- Visual fade-to-black (last 0.5s) — smooth scene transitions
- Total video duration is computed from sum of all scene frames
场景时长由音频长度决定,而非硬编码。生成脚本会:
- 探测每个音频文件的时长(例如:介绍场景=8.5秒)
- 添加2秒缓冲,用于视觉动画的过渡空间
- 计算帧数:
ceil((8.5 + 2) * 30) = 317帧 - 导入这些值,用于
ProductTour.tsx组件的时长设置Sequence
每个场景还包含:
- 音频淡出(最后1秒) — 避免场景切换时音频突兀中断
- 视觉渐黑(最后0.5秒) — 实现平滑的场景过渡
- 总视频时长由所有场景帧数之和计算得出
Important: format config
重要提示:格式配置
staticFile()narration-format.tsnpm run narration:generatenpm run remotion:renderRemotion中的在文件缺失时不会抛出错误 — 它会返回一个在渲染时404的URL。因此生成脚本会写入,确保视频仅引用实际存在的文件。在运行前,务必先运行。
staticFile()narration-format.tsnpm run remotion:rendernpm run narration:generateFull narration script (prompt)
完整旁白脚本(提示词)
This is the exact text sent to ElevenLabs. Copy and adapt for your product:
The narration text lives in as .
The generate script imports it — never edit narration text in two places.
remotion/ProductTour.tsxNARRATION_SCRIPTTo see the current script, read in .
To change it, edit only that file, then re-run:
NARRATION_SCRIPTProductTour.tsxbash
npm run narration:generate # Regenerates audio from the updated text
npm run remotion:render # Subtitles already read from the same source这是发送给ElevenLabs的完整文本。可根据你的产品需求进行调整:
旁白文本存储在的中。生成脚本会导入该文本 — 绝对不要在两个地方编辑旁白文本。
remotion/ProductTour.tsxNARRATION_SCRIPT查看当前脚本,请阅读中的。如需修改,仅编辑该文件,然后重新运行:
ProductTour.tsxNARRATION_SCRIPTbash
npm run narration:generate # 根据更新后的文本重新生成音频
npm run remotion:render # 字幕已自动读取同一数据源Phase 5b: Background Music (Suno AI)
阶段5b:背景音乐(Suno AI)
Setup
配置步骤
- Get a Suno API key at api.box (Suno API provider)
- Add to :
.envSUNO_API_KEY=your-key
- 在api.box获取Suno API密钥(Suno API提供商)
- 添加到文件中:
.envSUNO_API_KEY=your-key
Generate music
生成背景音乐
bash
npm run music:generatebash
npm run music:generateOutput: public/music/background.mp3
输出文件:public/music/background.mp3
Config: remotion/music-config.ts (HAS_BACKGROUND_MUSIC = true)
配置文件:remotion/music-config.ts(设置HAS_BACKGROUND_MUSIC = true)
The script:
1. Sends a prompt to Suno's API (model V4_5ALL, instrumental only)
2. Polls for completion (typically 30-60 seconds)
3. Downloads the MP3 to `public/music/background.mp3`
4. Writes `remotion/music-config.ts` so the video knows music exists
该脚本会:
1. 向Suno API发送提示词(模型V4_5ALL,仅器乐)
2. 轮询任务完成状态(通常需要30-60秒)
3. 将MP3文件下载到`public/music/background.mp3`
4. 写入`remotion/music-config.ts`,告知视频背景音乐已存在Music prompt (adapt for your product)
音乐提示词(可根据产品调整)
Uplifting neoclassical underscore for a modern technology product video.
Bright piano arpeggios in a major key over gentle pizzicato strings and
a soft cello melody. Light, optimistic, and forward-moving — like a TED
talk opening or an Apple product reveal. Subtle modern electronic texture
underneath the classical instruments. No heavy drums, no vocals, no sudden
drops. Tempo around 100 BPM. Engaging but never distracting — true
background music that lifts the mood without competing with narration.
Think Max Richter meets Ólafur Arnalds with a hint of Nils Frahm.
Duration should be around 90 seconds.Key prompt principles:
- Specify major key to avoid melancholic/minor tonality
- Name classic instruments (piano, strings, cello) for warmth and sophistication
- Add modern electronic texture to keep it contemporary
- Reference real composers (Max Richter, Ólafur Arnalds, Nils Frahm) for style anchoring
- Emphasize "background music" and "never distracting" — Suno tends to overpower otherwise
- Set tempo (100 BPM) — too slow feels sad, too fast feels hectic
Uplifting neoclassical underscore for a modern technology product video.
Bright piano arpeggios in a major key over gentle pizzicato strings and
a soft cello melody. Light, optimistic, and forward-moving — like a TED
talk opening or an Apple product reveal. Subtle modern electronic texture
underneath the classical instruments. No heavy drums, no vocals, no sudden
drops. Tempo around 100 BPM. Engaging but never distracting — true
background music that lifts the mood without competing with narration.
Think Max Richter meets Ólafur Arnalds with a hint of Nils Frahm.
Duration should be around 90 seconds.提示词关键原则:
- 指定大调,避免忧郁/小调调性
- 提及经典乐器(钢琴、弦乐、大提琴),增加温暖感和精致感
- 添加现代电子纹理,保持内容与时俱进
- 引用真实作曲家(Max Richter、Ólafur Arnalds、Nils Frahm),锚定风格方向
- 强调**“背景音乐”和“绝不干扰旁白”** — 否则Suno生成的音乐可能会盖过旁白
- 设置** tempo(100 BPM)** — 过慢会显得悲伤,过快会显得杂乱
Volume ducking
音量闪避
The component in handles automatic
volume ducking — music plays at 0.15 volume between scenes and ducks to
0.06 when narration is active. Transitions are smooth 0.5s ramps.
BackgroundMusicProductTour.tsx- Fade in: first 2s of video
- Fade out: last 3s of video
- Loop: music loops if shorter than the video
ProductTour.tsxBackgroundMusic- 淡入:视频前2秒
- 淡出:视频最后3秒
- 循环:如果音乐时长短于视频,会自动循环播放
Without Suno API key
无Suno API密钥的情况
If no key is set, the video renders without background music (narration only).
You can also manually place any MP3 at and set
in .
public/music/background.mp3HAS_BACKGROUND_MUSIC = trueremotion/music-config.ts如果未设置密钥,视频会在无背景音乐的情况下渲染(仅保留旁白)。你也可以手动将任意MP3文件放置在,并在中设置。
public/music/background.mp3remotion/music-config.tsHAS_BACKGROUND_MUSIC = truePhase 6: Render & Deploy
阶段6:渲染与部署
bash
undefinedbash
undefinedFull pipeline: narration → music → render
完整流程:旁白 → 音乐 → 渲染
npm run narration:generate # ElevenLabs narration (or macOS fallback)
npm run music:generate # Suno background music (optional)
npm run remotion:render # Final MP4 with both audio layers
npm run narration:generate # ElevenLabs旁白(或macOS备用方案)
npm run music:generate # Suno背景音乐(可选)
npm run remotion:render # 包含两层音频的最终MP4文件
Preview in Remotion Studio
在Remotion Studio中预览
npm run remotion:studio
npm run remotion:studio
Output: public/help/product-tour.mp4
输出文件:public/help/product-tour.mp4
The help page references the video:
帮助页面中引用该视频:
<video src="/help/product-tour.mp4" controls />
<video src="/help/product-tour.mp4" controls />
undefinedundefinedpackage.json scripts
package.json脚本配置
json
{
"remotion:studio": "npx remotion studio remotion/index.ts",
"remotion:render": "npx remotion render remotion/index.ts ProductTour public/help/product-tour.mp4",
"narration:generate": "tsx scripts/generate-narration.ts",
"music:generate": "tsx scripts/generate-music.ts"
}json
{
"remotion:studio": "npx remotion studio remotion/index.ts",
"remotion:render": "npx remotion render remotion/index.ts ProductTour public/help/product-tour.mp4",
"narration:generate": "tsx scripts/generate-narration.ts",
"music:generate": "tsx scripts/generate-music.ts"
}Phase 7: GitHub README Documentation
阶段7:GitHub README文档
Use the same assets (illustrations, video) to enrich your GitHub with
visual documentation. This makes the repository professional and immediately
communicates value to contributors and potential users.
README.md使用现有资源(插图、视频)丰富GitHub的,添加可视化文档。这会让仓库看起来更专业,能立即向贡献者和潜在用户传达产品价值。
README.mdStrategy
策略
README.md documentation flow:
1. Re-use existing illustrations from public/help/
2. Generate additional screenshots/GIFs specific to README sections
3. Upload video to GitHub release or external host (too large for git)
4. Embed everything with standard Markdown + HTMLREADME.md文档流程:
1. 复用public/help/目录下的现有插图
2. 生成针对README章节的额外截图/GIF动图
3. 将视频上传到GitHub Release或外部托管平台(文件过大,不适合存入git)
4. 使用标准Markdown+HTML嵌入所有资源Embedding screenshots in README
在README中嵌入截图
GitHub renders standard Markdown images. Place screenshots in a or
directory and reference them with relative paths:
docs/.github/markdown
undefinedGitHub支持标准Markdown图片语法。将截图放置在或目录下,使用相对路径引用:
docs/.github/markdown
undefinedDashboard
Dashboard

The main dashboard shows all monitored TYPO3 installations at a glance,
with health scores and security alerts.
**Best practices for README screenshots:**
- Use 1200px width (GitHub caps display at ~900px, 2× for retina)
- Prefer light theme or show both with `<picture>` for dark mode support
- Add a 1px border or subtle shadow so screenshots don't bleed into the page
- Keep file sizes under 500KB (PNG with compression or JPEG at 85%)
The main dashboard shows all monitored TYPO3 installations at a glance,
with health scores and security alerts.
**README截图最佳实践:**
- 使用1200px宽度(GitHub显示上限约为900px,2倍分辨率适配视网膜屏幕)
- 优先使用浅色主题,或使用`<picture>`标签同时支持深色/浅色模式
- 添加1px边框或细微阴影,避免截图与页面背景融合
- 控制文件大小在500KB以内(使用压缩PNG或85%质量的JPEG)Dark/light theme images
深色/浅色主题适配图片
GitHub supports with media queries:
<picture>prefers-color-schemehtml
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/screenshots/dashboard-dark.png">
<source media="(prefers-color-scheme: light)" srcset="docs/screenshots/dashboard-light.png">
<img alt="Dashboard overview" src="docs/screenshots/dashboard-light.png" width="800">
</picture>GitHub支持使用标签结合媒体查询:
<picture>prefers-color-schemehtml
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/screenshots/dashboard-dark.png">
<source media="(prefers-color-scheme: light)" srcset="docs/screenshots/dashboard-light.png">
<img alt="Dashboard overview" src="docs/screenshots/dashboard-light.png" width="800">
</picture>Generating screenshots for README
为README生成截图
You have two approaches:
1. AI-generated illustrations (fast, always available):
GenerateImage({
description: "Clean screenshot-style illustration of a TYPO3 monitoring
dashboard. Dark theme, stat cards showing Total Clients: 42,
Healthy: 38, Warning: 3, Critical: 1. Sidebar navigation visible.
Modern, polished UI. 1200×675px, 16:9 aspect ratio.",
filename: "docs/screenshots/dashboard.png"
})2. Real screenshots via Playwright (pixel-perfect):
typescript
// scripts/capture-screenshots.ts
import { chromium } from "playwright";
async function captureScreenshots() {
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1200, height: 675 } });
// Login if needed
await page.goto("http://localhost:3001");
// Capture each page
const pages = [
{ url: "/", name: "dashboard" },
{ url: "/clients", name: "clients" },
{ url: "/extensions", name: "extensions" },
{ url: "/help", name: "help" },
];
for (const p of pages) {
await page.goto(`http://localhost:3001${p.url}`);
await page.waitForTimeout(2000); // Let animations settle
await page.screenshot({ path: `docs/screenshots/${p.name}.png` });
// Also capture dark mode
await page.emulateMedia({ colorScheme: "dark" });
await page.screenshot({ path: `docs/screenshots/${p.name}-dark.png` });
await page.emulateMedia({ colorScheme: "light" });
}
await browser.close();
}
captureScreenshots();Add to :
package.jsonjson
"screenshots:capture": "tsx scripts/capture-screenshots.ts"有两种实现方式:
1. AI生成插图(快速、随时可用):
GenerateImage({
description: "Clean screenshot-style illustration of a TYPO3 monitoring
dashboard. Dark theme, stat cards showing Total Clients: 42,
Healthy: 38, Warning: 3, Critical: 1. Sidebar navigation visible.
Modern, polished UI. 1200×675px, 16:9 aspect ratio.",
filename: "docs/screenshots/dashboard.png"
})2. 使用Playwright生成真实截图(像素级精准):
typescript
// scripts/capture-screenshots.ts
import { chromium } from "playwright";
async function captureScreenshots() {
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1200, height: 675 } });
// 如需登录,在此处添加登录逻辑
await page.goto("http://localhost:3001");
// 捕获每个页面的截图
const pages = [
{ url: "/", name: "dashboard" },
{ url: "/clients", name: "clients" },
{ url: "/extensions", name: "extensions" },
{ url: "/help", name: "help" },
];
for (const p of pages) {
await page.goto(`http://localhost:3001${p.url}`);
await page.waitForTimeout(2000); // 等待动画完成
await page.screenshot({ path: `docs/screenshots/${p.name}.png` });
// 同时捕获深色模式截图
await page.emulateMedia({ colorScheme: "dark" });
await page.screenshot({ path: `docs/screenshots/${p.name}-dark.png` });
await page.emulateMedia({ colorScheme: "light" });
}
await browser.close();
}
captureScreenshots();添加到:
package.jsonjson
"screenshots:capture": "tsx scripts/capture-screenshots.ts"Animated GIFs for README
为README生成动画GIF
Short GIFs are more engaging than static screenshots for interactive features
(wizard, sorting, filtering). Use Playwright to record and to convert:
ffmpegbash
undefined短GIF动图比静态截图更适合展示交互功能(如向导流程、排序、筛选)。使用Playwright录制视频,再用转换为GIF:
ffmpegbash
undefinedRecord a video clip with Playwright (outputs .webm)
使用Playwright录制视频片段(输出.webm格式)
Then convert to GIF:
然后转换为GIF:
ffmpeg -i recording.webm -vf "fps=12,scale=800:-1:flags=lanczos" -loop 0 docs/screenshots/wizard.gif
Keep GIFs under 5MB. For larger animations, use MP4 with GitHub's video support.ffmpeg -i recording.webm -vf "fps=12,scale=800:-1:flags=lanczos" -loop 0 docs/screenshots/wizard.gif
控制GIF文件大小在5MB以内。对于较大的动画,可使用MP4格式,利用GitHub的视频支持功能。Embedding the product tour video
在README中嵌入产品导览视频
GitHub Markdown supports video since 2021. You have three options:
Option A: Upload to GitHub Release (recommended for open source)
bash
undefinedGitHub从2021年开始支持Markdown视频嵌入。有三种可选方案:
方案A:上传到GitHub Release(开源项目推荐)
bash
undefinedCreate a release and attach the video
创建Release并上传视频
gh release create v1.0.0 public/help/product-tour.mp4
--title "v1.0.0" --notes "Initial release"
--title "v1.0.0" --notes "Initial release"
Then link in README:
```markdowngh release create v1.0.0 public/help/product-tour.mp4
--title "v1.0.0" --notes "Initial release"
--title "v1.0.0" --notes "Initial release"
然后在README中链接:
```markdownProduct Tour
Product Tour
GitHub auto-embeds the video player when the URL is on its own line.
**Option B: Drag-and-drop into GitHub Issue/PR**
1. Create a GitHub issue or PR
2. Drag the MP4 into the comment editor
3. Copy the resulting `user-attachments` URL
4. Paste into README
```markdown
当URL单独占一行时,GitHub会自动嵌入视频播放器。
**方案B:拖放到GitHub Issue/PR中**
1. 创建GitHub Issue或PR
2. 将MP4文件拖放到评论编辑器中
3. 复制生成的`user-attachments`链接
4. 粘贴到README中
```markdownProduct Tour
Product Tour
**Option C: External host (YouTube, Vimeo)**
If the video is large (>25MB) or you want analytics, upload to YouTube/Vimeo
and embed a thumbnail with a play button:
```markdown
**方案C:外部托管平台(YouTube、Vimeo)**
如果视频文件过大(>25MB)或需要统计数据,可上传到YouTube/Vimeo,然后嵌入带播放按钮的缩略图:
```markdownProduct Tour
Product Tour
Full README template with documentation
包含文档的完整README模板
Here's a recommended structure integrating all visual assets:
markdown
undefined以下是推荐的README结构,整合了所有可视化资源:
markdown
undefinedProject Name
Project Name
<picture> <source media="(prefers-color-scheme: dark)" srcset="docs/screenshots/hero-dark.png"> <img alt="Project screenshot" src="docs/screenshots/hero-light.png" width="800"> </picture>One-line description — aspirational, concise.
<picture> <source media="(prefers-color-scheme: dark)" srcset="docs/screenshots/hero-dark.png"> <img alt="Project screenshot" src="docs/screenshots/hero-light.png" width="800"> </picture>一句话描述 — 有吸引力、简洁明了。
Product Tour
Product Tour
Features
Features
Dashboard
Dashboard
Description of the dashboard...
仪表盘功能描述...Client Management
Client Management
Description of client features...
客户管理功能描述...Quick Start
Quick Start
...installation instructions...
undefined...安装说明...
undefinedUpdating documentation on changes
UI变更时更新文档
When the UI changes, re-run the documentation pipeline:
bash
undefined当UI发生变更时,重新运行文档生成流程:
bash
undefined1. Capture fresh screenshots (if using Playwright)
1. 捕获新的截图(如果使用Playwright)
npm run screenshots:capture
npm run screenshots:capture
2. Regenerate illustrations (if using AI)
2. 重新生成插图(如果使用AI)
→ Use GenerateImage tool with updated descriptions
→ 使用GenerateImage工具,更新描述文本
3. Update narration if features changed
3. 如果功能变更,更新旁白文本
→ Edit NARRATION_SCRIPT in remotion/ProductTour.tsx
→ 编辑remotion/ProductTour.tsx中的NARRATION_SCRIPT
npm run narration:generate
npm run narration:generate
4. Re-render the product tour video
4. 重新渲染产品导览视频
npm run remotion:render
npm run remotion:render
5. Upload new video to GitHub release
5. 将新视频上传到GitHub Release
gh release upload v1.x.x public/help/product-tour.mp4 --clobber
gh release upload v1.x.x public/help/product-tour.mp4 --clobber
6. Commit updated screenshots
6. 提交更新后的截图
git add docs/screenshots/ README.md
git commit -m "docs: update screenshots and video for vX.Y.Z"
undefinedgit add docs/screenshots/ README.md
git commit -m "docs: update screenshots and video for vX.Y.Z"
undefinedDirectory structure for README documentation
README文档的目录结构
project/
├── README.md ← Main documentation with embedded visuals
├── docs/
│ ├── screenshots/
│ │ ├── dashboard.png ← Light theme screenshot
│ │ ├── dashboard-dark.png ← Dark theme screenshot
│ │ ├── clients.png
│ │ ├── wizard.gif ← Animated walkthrough
│ │ ├── hero-light.png ← Hero image (light)
│ │ ├── hero-dark.png ← Hero image (dark)
│ │ └── video-thumbnail.png ← Video poster frame
│ └── SECURITY.md
├── public/help/
│ └── product-tour.mp4 ← Rendered Remotion video
└── scripts/
├── capture-screenshots.ts ← Playwright screenshot automation
├── generate-narration.ts ← TTS generation
└── generate-music.ts ← Background music generationproject/
├── README.md ← 主文档,嵌入所有可视化资源
├── docs/
│ ├── screenshots/
│ │ ├── dashboard.png ← 浅色主题截图
│ │ ├── dashboard-dark.png ← 深色主题截图
│ │ ├── clients.png
│ │ ├── wizard.gif ← 动画演示
│ │ ├── hero-light.png ← 首图(浅色)
│ │ ├── hero-dark.png ← 首图(深色)
│ │ └── video-thumbnail.png ← 视频封面图
│ └── SECURITY.md
├── public/help/
│ └── product-tour.mp4 ← 渲染完成的Remotion视频
└── scripts/
├── capture-screenshots.ts ← Playwright截图自动化脚本
├── generate-narration.ts ← TTS生成脚本
└── generate-music.ts ← 背景音乐生成脚本Checklist
检查清单
Documentation Creation:
- [ ] Help page component created at src/app/(dashboard)/help/page.tsx
- [ ] Sidebar navigation updated with Help link
- [ ] AI-generated illustrations in public/help/
- [ ] Remotion project set up in remotion/
- [ ] Narration script written (Jony Ive style)
- [ ] NarrationSubtitle component added to video
- [ ] TTS audio generated (ElevenLabs or macOS fallback)
- [ ] Video rendered to public/help/product-tour.mp4
- [ ] Video embedded in help page
- [ ] All content reviewed by technical writer persona
GitHub README Documentation:
- [ ] Screenshots captured (AI-generated or Playwright)
- [ ] Dark/light variants created with <picture> tags
- [ ] Product tour video uploaded to GitHub release
- [ ] README.md updated with embedded visuals
- [ ] Animated GIFs created for interactive features (optional)
- [ ] Documentation update pipeline documented in scripts文档创建:
- [ ] 已在src/app/(dashboard)/help/page.tsx创建帮助页面组件
- [ ] 已更新侧边栏导航,添加帮助页面链接
- [ ] 已在public/help/目录下生成AI插图
- [ ] 已在remotion/目录下配置Remotion项目
- [ ] 已编写旁白脚本(Jony Ive风格)
- [ ] 已在视频中添加NarrationSubtitle组件
- [ ] 已生成TTS音频(ElevenLabs或macOS备用方案)
- [ ] 已将视频渲染到public/help/product-tour.mp4
- [ ] 已在帮助页面中嵌入视频
- [ ] 所有内容已通过技术文档作者视角审核
GitHub README文档:
- [ ] 已捕获截图(AI生成或Playwright生成)
- [ ] 已使用<picture>标签创建深色/浅色模式变体
- [ ] 已将产品导览视频上传到GitHub Release
- [ ] 已更新README.md,嵌入所有可视化资源
- [ ] 已为交互功能创建动画GIF(可选)
- [ ] 已在脚本中记录文档更新流程Additional resources
额外资源
- For narration examples and full script, see narration-examples.md
- Remotion docs: remotion.dev/docs
- ElevenLabs docs: elevenlabs.io/docs
- GitHub Markdown reference: docs.github.com/en/get-started/writing-on-github
- GitHub dark mode: github.blog/changelog/2022-05-19-specify-theme-context-for-images-in-markdown
<picture>
- 旁白示例和完整脚本,请查看narration-examples.md
- Remotion文档:remotion.dev/docs
- ElevenLabs文档:elevenlabs.io/docs
- GitHub Markdown参考:docs.github.com/en/get-started/writing-on-github
- GitHub 深色模式支持:github.blog/changelog/2022-05-19-specify-theme-context-for-images-in-markdown
<picture>
Video & Animation for Documentation (Remotion + GSAP)
文档视频与动画(Remotion + GSAP)
When generating visual documentation, integrating Remotion with GSAP allows for complex, sequenced, and dynamic visual storytelling.
生成可视化文档时,将Remotion与GSAP结合使用,可实现复杂、有序且动态的视觉叙事效果。
Prerequisites
前置条件
GSAP must be installed alongside your existing Remotion setup. GSAP's core is free for all use cases; premium plugins require a paid license.
Install GSAP core:
bash
npm install gsapOptional premium plugins (require GSAP Club license):
bash
undefinedGSAP需与现有Remotion配置一起安装。GSAP核心库可免费用于所有场景;高级插件需要付费许可证。
安装GSAP核心库:
bash
npm install gsap可选高级插件(需要GSAP Club许可证):
bash
undefinedInstall from GSAP's private registry (requires auth token)
从GSAP私有仓库安装(需要授权令牌)
npm install gsap@npm:@gsap/shockingly
Premium plugins used in this guide:
- `MorphSVGPlugin` -- morph between SVG shapes (chart transitions)
- `SplitText` -- split text into characters/words for kinetic typography
- `Flip` -- animate layout changes (code diff walkthroughs)
- `MotionPathPlugin` -- animate along SVG paths (included free since GSAP 3.x)
**Register plugins** in your Remotion entry point (e.g., `src/Root.tsx` or the component file):
```tsx
import gsap from "gsap";
import { MotionPathPlugin } from "gsap/MotionPathPlugin";
gsap.registerPlugin(MotionPathPlugin);
// Add other plugins as needed:
// gsap.registerPlugin(Flip, MorphSVGPlugin, SplitText);Remotion compatibility notes:
- GSAP works in Remotion's rendering pipeline because it manipulates DOM elements directly, which Remotion captures frame-by-frame.
- Always use for proper React cleanup on unmount.
gsap.context() - Never use GSAP's in Remotion -- there is no scroll context during rendering.
ScrollTrigger
npm install gsap@npm:@gsap/shockingly
本指南中使用的高级插件:
- `MorphSVGPlugin` -- 在SVG形状之间进行变形动画(如图表过渡)
- `SplitText` -- 将文本拆分为字符/单词,实现动态排版
- `Flip` -- 为布局变更添加动画(如代码差异演示)
- `MotionPathPlugin` -- 沿SVG路径动画(从GSAP 3.x开始免费提供)
**注册插件**在Remotion入口文件中(如`src/Root.tsx`或组件文件):
```tsx
import gsap from "gsap";
import { MotionPathPlugin } from "gsap/MotionPathPlugin";
gsap.registerPlugin(MotionPathPlugin);
// 根据需要添加其他插件:
// gsap.registerPlugin(Flip, MorphSVGPlugin, SplitText);Remotion兼容性说明:
- GSAP可在Remotion渲染流程中正常工作,因为它直接操作DOM元素,Remotion会逐帧捕获这些元素。
- 务必使用确保React卸载时正确清理。
gsap.context() - 绝对不要在Remotion中使用GSAP的-- 渲染过程中不存在滚动上下文。
ScrollTrigger
Core Concept: Deterministic Syncing
核心概念:确定性同步
Remotion is frame-based; GSAP is time-based. To ensure videos render correctly without tearing or skipping during export, never let GSAP run on its own timer. Map Remotion's to a strictly paused GSAP timeline.
useCurrentFrame()tsx
import { useCurrentFrame, useVideoConfig } from "remotion";
import { useEffect, useMemo, useRef } from "react";
import gsap from "gsap";
export const GsapSequence = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const containerRef = useRef<HTMLDivElement>(null);
// 1. Create a strictly paused timeline
const timeline = useMemo(() => gsap.timeline({ paused: true }), []);
// 2. Build your animation sequence (use GSAP seconds)
useEffect(() => {
let ctx = gsap.context(() => {
timeline.to(".box", { x: 200, rotation: 360, duration: 2, stagger: 0.2 });
}, containerRef);
return () => ctx.revert(); // React cleanup
}, [timeline]);
// 3. THE MAGIC: Sync GSAP playhead to Remotion frame
useEffect(() => {
timeline.seek(frame / fps);
}, [frame, fps, timeline]);
return (
<div ref={containerRef} className="box-container">
<div className="box">Item 1</div>
<div className="box">Item 2</div>
</div>
);
};Remotion是基于帧的;GSAP是基于时间的。为确保视频导出时无撕裂或跳帧,绝对不要让GSAP使用自身的计时器。将Remotion的映射到严格暂停的GSAP时间轴。
useCurrentFrame()tsx
import { useCurrentFrame, useVideoConfig } from "remotion";
import { useEffect, useMemo, useRef } from "react";
import gsap from "gsap";
export const GsapSequence = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const containerRef = useRef<HTMLDivElement>(null);
// 1. 创建严格暂停的时间轴
const timeline = useMemo(() => gsap.timeline({ paused: true }), []);
// 2. 构建动画序列(使用GSAP秒数)
useEffect(() => {
let ctx = gsap.context(() => {
timeline.to(".box", { x: 200, rotation: 360, duration: 2, stagger: 0.2 });
}, containerRef);
return () => ctx.revert(); // React清理逻辑
}, [timeline]);
// 3. 关键:将GSAP播放头与Remotion帧同步
useEffect(() => {
timeline.seek(frame / fps);
}, [frame, fps, timeline]);
return (
<div ref={containerRef} className="box-container">
<div className="box">Item 1</div>
<div className="box">Item 2</div>
</div>
);
};Creative Directives for AI Agents
AI Agent创作指南
When prompted to create visual documentation, utilize the following animation patterns:
当需要创建可视化文档时,可使用以下动画模式:
1. Diagram Animations
1. 图表动画
- Data Flow Tracing: Use GSAP or animate
MotionPathPluginto show data packets moving along SVG lines between architecture nodes (e.g., Load Balancer → DB).stroke-dashoffset - Node Pulses: Add glowing/pulsing loops (and
box-shadow) to servers/components when they become "active" in the explanation.scale - Elastic Reveals: Animate nodes popping into existence with and a
ease: "back.out(1.5)".stagger: 0.1
- 数据流追踪:使用GSAP 或动画
MotionPathPlugin,展示数据包沿架构节点间的SVG线条移动(如负载均衡器→数据库)。stroke-dashoffset - 节点脉动:当服务器/组件在讲解中变为“活跃”状态时,添加发光/脉动循环动画(和
box-shadow)。scale - 弹性显示:使用和
ease: "back.out(1.5)",让节点以弹出的方式出现。stagger: 0.1
2. Animation of Screenshots
2. 截图动画
- 3D Exploded View: Separate UI components into layers (background, modal, button) and rotate the parent container in 3D (,
rotateX: 60deg). Animate therotateZ: -45degof inner layers to pull the interface apart conceptually.translateZ - Glass Morph Spotlight: Smoothly zoom into an active UI area using a circle, while dimming and applying a CSS blur (
clip-path) to the rest of the screenshot.backdrop-filter: blur(10px) - Simulated User Journeys: Animate a custom SVG cursor moving across the UI, synchronizing button scaling and ripple effects to simulate clicks.
- 3D分解视图:将UI组件拆分为多个图层(背景、模态框、按钮),然后旋转父容器到3D视角(,
rotateX: 60deg)。动画内层的rotateZ: -45deg属性,将界面“拆解”开来,帮助用户理解层级关系。translateZ - 玻璃态聚光灯:使用圆形平滑放大到UI的活跃区域,同时调暗截图其余部分并应用CSS模糊(
clip-path)。backdrop-filter: blur(10px) - 模拟用户操作:动画自定义SVG光标在UI上移动,同步按钮缩放和波纹效果,模拟用户点击操作。
3. Infographics & Tables
3. 信息图与表格
- Staggered Row Reveals: Slide and fade in table rows one-by-one (,
stagger: 0.1,x: -20) to pace the viewer's reading.opacity: 0 - Data Odometers: Animate a numeric object's value from 0 to target (using to format text), making financial or performance numbers tick up dynamically.
onUpdate - Cell Highlighting: Animate a floating background div that smoothly translates its coordinate from row to row to highlight active talking points.
y
- 交错行显示:逐行滑入并淡入表格行(,
stagger: 0.1,x: -20),控制用户的阅读节奏。opacity: 0 - 数字计数器:动画数值从0增长到目标值(使用格式化文本),让财务或性能数据动态跳动。
onUpdate - 单元格高亮:动画浮动背景div,平滑改变其坐标,在不同行之间移动,突出当前讲解的内容。
y
4. Animations of Charts
4. 图表动画
- Spring-loaded Bar Charts: Set CSS . Animate SVG
transform-origin: bottom<rect>from 0 to target with GSAP'sscaleYeasing for a playful reveal.elastic.out - Progressive Charts: Animate SVG circles with and
stroke-dasharrayto draw out pie chart segments proportionally over time.stroke-dashoffset - Morphing Data: Use to seamlessly transition a bar chart into a line chart, visually connecting two different ways of looking at the same data.
MorphSVGPlugin
- 弹簧加载柱状图:设置CSS 。使用GSAP的
transform-origin: bottom缓动函数,动画SVGelastic.out的<rect>从0到目标值,实现活泼的显示效果。scaleY - 渐进式图表:动画SVG圆形的和
stroke-dasharray,按比例逐步绘制饼图扇区。stroke-dashoffset - 数据变形:使用将柱状图无缝过渡为折线图,直观展示同一数据的不同呈现方式。
MorphSVGPlugin
5. Code & Text
5. 代码与文本
- Code Diff Walkthroughs: Use GSAP's plugin. When code changes, removed lines shrink and glow red, new lines expand and glow green, and unchanged lines glide to their new line numbers smoothly.
Flip - Kinetic Typography: Use GSAP to stagger headers character-by-character.
SplitText
- 代码差异演示:使用GSAP的插件。当代码变更时,删除的行收缩并显示红色发光效果,新增的行展开并显示绿色发光效果,未变更的行平滑移动到新的行号位置。
Flip - 动态排版:使用GSAP 将标题按字符逐个交错显示。
SplitText
Concrete Code Examples (Remotion + GSAP Patterns)
具体代码示例(Remotion + GSAP模式)
Use these blueprints as starting points when generating Remotion + GSAP code to ensure correct CSS contexts, React lifecycle management, and frame syncing.
以下是可复用的代码蓝图,可作为生成Remotion + GSAP代码的起点,确保正确的CSS上下文、React生命周期管理和帧同步。
Example 1: 3D Exploded Screenshot (UI Layers)
示例1:3D分解截图(UI图层)
Requires a strict CSS 3D context. The parent needs , and the container needs .
perspectivetransformStyle: "preserve-3d"tsx
import { useCurrentFrame, useVideoConfig } from "remotion";
import { useEffect, useMemo, useRef } from "react";
import gsap from "gsap";
export const ExplodedScreenshot = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const containerRef = useRef<HTMLDivElement>(null);
const tl = useMemo(() => gsap.timeline({ paused: true }), []);
useEffect(() => {
let ctx = gsap.context(() => {
// 1. Tilt the whole container into isometric space
tl.to(".wrapper", { rotateX: 60, rotateZ: -45, duration: 1, ease: "power2.inOut" })
// 2. Explode the layers upward on the Z-axis (Using "<" to sync with rotation end)
.to(".ui-layer", {
z: (index) => (index + 1) * 80, // Stagger height based on index
boxShadow: "0px 20px 40px rgba(0,0,0,0.4)",
duration: 1.5,
ease: "back.out(1.2)",
stagger: 0.2
}, "-=0.2");
}, containerRef);
return () => ctx.revert(); // Crucial for React StrictMode cleanup
}, [tl]);
// Deterministic Frame Sync
useEffect(() => { tl.seek(frame / fps); }, [frame, fps, tl]);
return (
<div ref={containerRef} style={{ perspective: 1000, width: "100%", height: "100%" }}>
<div className="wrapper" style={{ transformStyle: "preserve-3d", position: "relative", width: 800, height: 600 }}>
<img src="/bg-layer.png" className="ui-layer" style={{ position: "absolute", inset: 0 }} />
<img src="/modal-layer.png" className="ui-layer" style={{ position: "absolute", inset: 0 }} />
<img src="/dropdown-layer.png" className="ui-layer" style={{ position: "absolute", inset: 0 }} />
</div>
</div>
);
};需要严格的CSS 3D上下文。父容器需要属性,子容器需要属性。
perspectivetransformStyle: "preserve-3d"tsx
import { useCurrentFrame, useVideoConfig } from "remotion";
import { useEffect, useMemo, useRef } from "react";
import gsap from "gsap";
export const ExplodedScreenshot = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const containerRef = useRef<HTMLDivElement>(null);
const tl = useMemo(() => gsap.timeline({ paused: true }), []);
useEffect(() => {
let ctx = gsap.context(() => {
// 1. 将整个容器倾斜到等轴测视角
tl.to(".wrapper", { rotateX: 60, rotateZ: -45, duration: 1, ease: "power2.inOut" })
// 2. 将图层沿Z轴向上“拆解”(使用"<"与旋转动画同步结束)
.to(".ui-layer", {
z: (index) => (index + 1) * 80, // 根据索引设置交错高度
boxShadow: "0px 20px 40px rgba(0,0,0,0.4)",
duration: 1.5,
ease: "back.out(1.2)",
stagger: 0.2
}, "-=0.2");
}, containerRef);
return () => ctx.revert(); // React StrictMode清理的关键
}, [tl]);
// 确定性帧同步
useEffect(() => { tl.seek(frame / fps); }, [frame, fps, tl]);
return (
<div ref={containerRef} style={{ perspective: 1000, width: "100%", height: "100%" }}>
<div className="wrapper" style={{ transformStyle: "preserve-3d", position: "relative", width: 800, height: 600 }}>
<img src="/bg-layer.png" className="ui-layer" style={{ position: "absolute", inset: 0 }} />
<img src="/modal-layer.png" className="ui-layer" style={{ position: "absolute", inset: 0 }} />
<img src="/dropdown-layer.png" className="ui-layer" style={{ position: "absolute", inset: 0 }} />
</div>
</div>
);
};Example 2: SVG Architecture Diagram (Drawing Data Flows)
示例2:SVG架构图(绘制数据流)
Use native SVG and GSAP to draw connection lines between nodes without needing external plugins.
strokeDasharraystrokeDashoffsettsx
import { useCurrentFrame, useVideoConfig } from "remotion";
import { useEffect, useMemo, useRef } from "react";
import gsap from "gsap";
export const ArchitectureFlow = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const svgRef = useRef<SVGSVGElement>(null);
const tl = useMemo(() => gsap.timeline({ paused: true }), []);
useEffect(() => {
let ctx = gsap.context(() => {
// Setup: ensure path is hidden initially (Assuming path length ~500)
gsap.set(".connection-line", { strokeDasharray: 500, strokeDashoffset: 500 });
// 1. Pop the nodes in
tl.from(".node", { scale: 0, transformOrigin: "center", duration: 0.5, stagger: 0.3, ease: "back.out(1.5)" })
// 2. Draw the line between them
.to(".connection-line", { strokeDashoffset: 0, duration: 1.2, ease: "power2.inOut" })
// 3. Pulse the receiving node to indicate data arrival
.to(".node-db", { scale: 1.15, repeat: 1, yoyo: true, duration: 0.2, fill: "#10b981" });
}, svgRef);
return () => ctx.revert();
}, [tl]);
useEffect(() => { tl.seek(frame / fps); }, [frame, fps, tl]);
return (
<svg ref={svgRef} width="800" height="400" viewBox="0 0 800 400">
{/* Connecting Line */}
<path className="connection-line" d="M 100 200 C 300 200, 500 200, 700 200" stroke="#cbd5e1" strokeWidth="6" fill="none" />
{/* Nodes */}
<circle className="node node-client" cx="100" cy="200" r="40" fill="#3b82f6" />
<rect className="node node-db" x="660" y="160" width="80" height="80" rx="12" fill="#6366f1" />
</svg>
);
};使用原生SVG 和GSAP 绘制节点间的连接线,无需外部插件。
strokeDasharraystrokeDashoffsettsx
import { useCurrentFrame, useVideoConfig } from "remotion";
import { useEffect, useMemo, useRef } from "react";
import gsap from "gsap";
export const ArchitectureFlow = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const svgRef = useRef<SVGSVGElement>(null);
const tl = useMemo(() => gsap.timeline({ paused: true }), []);
useEffect(() => {
let ctx = gsap.context(() => {
// 初始化:确保路径初始状态隐藏(假设路径长度约为500)
gsap.set(".connection-line", { strokeDasharray: 500, strokeDashoffset: 500 });
// 1. 节点弹出显示
tl.from(".node", { scale: 0, transformOrigin: "center", duration: 0.5, stagger: 0.3, ease: "back.out(1.5)" })
// 2. 绘制节点间的连接线
.to(".connection-line", { strokeDashoffset: 0, duration: 1.2, ease: "power2.inOut" })
// 3. 接收节点脉动,指示数据到达
.to(".node-db", { scale: 1.15, repeat: 1, yoyo: true, duration: 0.2, fill: "#10b981" });
}, svgRef);
return () => ctx.revert();
}, [tl]);
useEffect(() => { tl.seek(frame / fps); }, [frame, fps, tl]);
return (
<svg ref={svgRef} width="800" height="400" viewBox="0 0 800 400">
{/* 连接线 */}
<path className="connection-line" d="M 100 200 C 300 200, 500 200, 700 200" stroke="#cbd5e1" strokeWidth="6" fill="none" />
{/* 节点 */}
<circle className="node node-client" cx="100" cy="200" r="40" fill="#3b82f6" />
<rect className="node node-db" x="660" y="160" width="80" height="80" rx="12" fill="#6366f1" />
</svg>
);
};Example 3: Number Odometer (For Infographics & Tables)
示例3:数字计数器(用于信息图与表格)
To animate a number counting up (e.g., "0" to "10,000") smoothly, animate a dummy object and use to safely set React state synchronously with the frame.
onUpdatetsx
import { useCurrentFrame, useVideoConfig } from "remotion";
import { useEffect, useMemo, useState } from "react";
import gsap from "gsap";
export const AnimatedNumber = ({ targetValue = 10000 }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const [displayValue, setDisplayValue] = useState(0);
const tl = useMemo(() => gsap.timeline({ paused: true }), []);
useEffect(() => {
const counter = { val: 0 };
// gsap.context isn't strictly necessary for JS objects, but good for consistency
let ctx = gsap.context(() => {
tl.to(counter, {
val: targetValue,
duration: 2,
ease: "power2.out",
onUpdate: () => {
// Update React state safely as GSAP manipulates the dummy object
setDisplayValue(Math.round(counter.val));
}
});
});
return () => ctx.revert();
}, [tl, targetValue]);
useEffect(() => { tl.seek(frame / fps); }, [frame, fps, tl]);
return (
<div style={{ fontSize: "64px", fontFamily: "monospace", fontWeight: "bold" }}>
${displayValue.toLocaleString()}
</div>
);
};要实现数字从0平滑增长到目标值(如“0”到“10,000”),可动画一个虚拟对象,并使用安全地同步设置React状态。
onUpdatetsx
import { useCurrentFrame, useVideoConfig } from "remotion";
import { useEffect, useMemo, useState } from "react";
import gsap from "gsap";
export const AnimatedNumber = ({ targetValue = 10000 }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const [displayValue, setDisplayValue] = useState(0);
const tl = useMemo(() => gsap.timeline({ paused: true }), []);
useEffect(() => {
const counter = { val: 0 };
// 对于JS对象,gsap.context并非必需,但保持一致性更好
let ctx = gsap.context(() => {
tl.to(counter, {
val: targetValue,
duration: 2,
ease: "power2.out",
onUpdate: () => {
// 当GSAP操作虚拟对象时,安全更新React状态
setDisplayValue(Math.round(counter.val));
}
});
});
return () => ctx.revert();
}, [tl, targetValue]);
useEffect(() => { tl.seek(frame / fps); }, [frame, fps, tl]);
return (
<div style={{ fontSize: "64px", fontFamily: "monospace", fontWeight: "bold" }}>
${displayValue.toLocaleString()}
</div>
);
};