basic-custom-app-build-w-video について
Domo カスタムアプリの構築や引き継ぎと、Remotion によるデモ動画制作を扱う開発手順書です。決定論的な合成サンプルデータと Remotion 向けの UI 制約により、実際の Domo データなしでアプリのコンポーネントを動画に描画できます。
主な機能
- 01Domo アプリ構築フェーズの案内
- 02Remotion 対応スタイルの指針
- 03決定論的な合成サンプルデータ
- 04アプリのコンポーネントを動画に描画
- 05プロパティ駆動のビジュアル状態
活用シーン
- 01関係者向けデモを伴う Domo アプリの新規構築
- 02既存アプリのビジュアルデモ作成
- 03ライブデータセットや認証情報なしでの画面描画
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ stahura/ domo-ai-vibe-rules --skill basic-custom-app-build-w-videoインストール後、Claude Code で/basic-custom-app-build-w-videoを入力して読み込まれたか確認します。
詳細
Domo アプリ+デモ動画の構築プレイブック
使用する場面
- ステークホルダーへの説明、ドキュメント、またはマーケティング用のデモ動画が必要な、新しい Domo カスタムアプリを構築する場合。
- 既存のアプリを引き継ぎ、正規化と並行してビジュアルデモを作成する場合。
basic-custom-app-build との違い
このプレイブックでは、標準的な Domo アプリの構築に次の 3 つを追加します。
- Remotion に対応したスタイル制約をフェーズ 0 から適用し、アプリの UI を Remotion で手戻りなくキャプチャしてアニメーション化できるようにします。
- サンプルデータの生成(フェーズ 3A)でリアルなモックデータを作成し、ライブの Domo データセットや認証情報なしで、デモにデータが表示された UI をレンダリングできるようにします。
- デモ動画の作成(フェーズ 10)では Remotion を使い、完成したアプリを紹介する 10~15 秒の洗練された動画を作成します。
進捗チェックリスト
このチェックリストをコピーし、作業の進捗に合わせて更新してください。
Demo-Ready Build Progress:
- [ ] Phase 0 : Rules loaded (platform + Remotion styling constraints)
- [ ] Phase 1 : Manifest & contracts
- [ ] Phase 2 : App shell (domo.js)
- [ ] Phase 3 : Data access
- [ ] Phase 3A: Sample data generation
- [ ] Phase 4 : App storage (if needed)
- [ ] Phase 5 : Toolkit clients (if needed)
- [ ] Phase 6 : Feature skills (AI / Code Engine / Workflow — as needed)
- [ ] Phase 7 : UI build (Remotion-safe styling)
- [ ] Phase 8 : Performance review
- [ ] Phase 9 : Build & publish
- [ ] Phase 10: Demo video (Remotion)
- [ ] Phase 11: Verification
フェーズ 0 — ルールの読み込み(常時適用)
コードを書く前に適用します。
rules/core-custom-apps-rule.mdrules/custom-app-gotchas.md
Remotion のスタイル制約(すべてのフェーズで適用)
これらのルールにより、構築するすべての UI コンポーネントを、手戻りなくスクリーンショットでキャプチャするか、Remotion コンポジション内で直接レンダリングできるようにします。
| ルール | 理由 |
|---|---|
CSS の transition-* または animation-* プロパティを使用しない | Remotion は CSS による動きをキャプチャできません。デモのアニメーションはすべて useCurrentFrame() で制御します。静的なスタイル(色、境界線、影、静止時の変形)は問題ありません。 |
Tailwind の transition-* または animate-* ユーティリティクラスを使用しない | 同じ理由です。それ以外の Tailwind ユーティリティはすべて使用でき、推奨されます。 |
レイアウトにはインラインの style オブジェクトまたは Tailwind を使用する | Remotion は React コンポーネントをフレームにレンダリングします。CSS モジュールやスコープ付きスタイルは機能しますが、CSS-in-JS のランタイムライブラリ(styled-components、Emotion)は Remotion のレンダリングパイプラインでハイドレーションの問題を引き起こす可能性があります。インラインスタイルまたは Tailwind を優先してください。 |
Domo アプリでは通常どおり <img> を使用する | アプリのコードでは、標準の <img> タグを使用してください。Remotion のデモでは、シーンを構成する際に独自の <Img> コンポーネントを使用します。この制約の目的は、Remotion のフレームキャプチャでは動作しない遅延読み込みや交差オブザーバーのパターンに依存しないことです。 |
| ビジュアルの状態を決定的に保つ | デモで表示するすべての UI 状態は、ユーザー操作の履歴やタイマーではなく、プロパティやデータだけから再現できる必要があります。サンプルデータを渡すことで、デモに表示したいビジュアルがそのまま得られるようにコンポーネントを構築してください。 |
| 1920x1080 向けにデザインする | これはデモ動画のネイティブ解像度であるため、アプリのレイアウトが 1920x1080 で適切に表示されるようにしてください。レスポンシブデザインも問題ありませんが、1080p のブレークポイントを確認してください。 |
フェーズ 1 — マニフェストとコントラクト
manifest を使用します。
外部リソースのマッピングをすべて最初に定義します。データセット、コレクション、ワークフロー、Code Engine パッケージが対象です。それ以外はすべて、これらの定義に依存します。
エイリアスには安全な名前(^[A-Za-z][A-Za-z0-9]*$)のみを使用し、スペースや特殊文字は使わないでください。
ルートフォルダーに既存の thumbnail.png があるか確認し、アプリの public/ フォルダーにコピーします。
既存アプリの引き継ぎですか? 変更する前に、現在の manifest.json を実際のコードでの使用状況と照らし合わせて監査してください。
フェーズ 2 — アプリのシェル
domo-js を使用します。
基本設定を行います。ryuu.js のインポート、domo.navigate() によるナビゲーション、イベントリスナー、環境情報が対象です。
DA CLI のユーザー: ユーザーから明示的に依頼された場合は、スキャフォールディングにも da-cli を使用します。
フェーズ 3 — データアクセス
dataset-query(主に使用)と data-api(ルーティングの概要)を使用します。
@domoinc/query でクエリを構築します。UI やデータマッピングのロジックを書く前に、manifest.json の datasetsMapping にあるすべてのデータセットの実際のスキーマを取得し、明示的なフィールドマップを作成してください。
生 SQL が必要ですか? sql-query を使用できますが、SQL はページフィルターを無視することに注意してください。
フェーズ 3A — サンプルデータの生成
これは、デモの準備を整えるために不可欠な追加項目です。現実的なサンプルデータを生成し、次の条件を満たします。
- デモで表示するすべてのビュー(ダッシュボード、詳細パネル、チャート、テーブル、チャットメッセージなど)にデータを入力します。
- 実際の顧客/ユーザーデータを含めません — すべての名前、メールアドレス、ID、指標は合成データです。
- 決定論的です — 同じシードを使うと同じデータが生成されるため、スクリーンショットや Remotion のキャプチャを再現できます。
実装方法
アプリ内に src/sample-data/ ディレクトリを作成します。
src/sample-data/
index.ts # Re-exports all sample datasets; single import point
datasets/
<alias>.ts # One file per manifest dataset alias, exporting typed arrays
README.md # Documents the shape and purpose of each sample dataset
manifest.json の各データセットエイリアスについて:
- Phase 3 のデータセットのフィールドマップに一致する、型付き配列をエクスポートする TypeScript ファイルを作成します。
- 現実的なデータを 20~100 行生成します(チャートやテーブルを説得力のある形で表示するのに十分な量)。
- 推測ではなく、データセットのスキーマにある実際のフィールド名を使用します。
- 興味深い視覚パターン(傾向、外れ値、カテゴリの分布)が生まれる値の範囲を含めます。
// src/sample-data/datasets/sales.ts
export type SalesRow = {
Date: string;
Region: string;
Product: string;
Revenue: number;
Units: number;
};
export const salesData: SalesRow[] = [
{ Date: "2026-01-15", Region: "West", Product: "Widget A", Revenue: 12400, Units: 62 },
{ Date: "2026-01-16", Region: "East", Product: "Widget B", Revenue: 8750, Units: 35 },
// ... 20-100 rows with realistic distribution
];
サンプルデータをアプリに組み込む
ライブの Domo クエリとサンプルデータを切り替えるデータアクセス層を作成します。
// src/data/use-dataset.ts
import { salesData } from "../sample-data/datasets/sales";
const USE_SAMPLE_DATA = !window.__DOMO_ENV__; // true outside Domo runtime
export async function fetchSalesData(): Promise<SalesRow[]> {
if (USE_SAMPLE_DATA) {
return salesData;
}
// Real query via @domoinc/query
return new Query().select([...]).fetch("sales");
}
これにより、次のことが保証されます。
- Domo では: アプリは通常どおり実際のデータセットにクエリを実行します。
- ローカル/Remotion では: 認証情報なしで、サンプルデータを使ってアプリがレンダリングされます。
- セキュリティ: デモ動画に実データが含まれることはありません。
データセット以外のソース向けサンプルデータ
| ソース | サンプルの方針 |
|---|---|
| AppDB コレクション | サンプルドキュメントの配列を src/sample-data/collections/<name>.ts にエクスポートします |
| AI の応答 | 定型応答文字列を src/sample-data/ai-responses.ts にエクスポートします |
| ユーザーの ID | モックのユーザーオブジェクトをエクスポートします:{ displayName: "Alex Demo", email: "[email protected]", role: "Admin" } |
| ワークフローの結果 | サンプルのステータスオブジェクトを src/sample-data/workflows/<name>.ts にエクスポートします |
フェーズ 4 — アプリストレージ(必要な場合)
ストレージを作成する必要がある場合は、appdb と appdb-collection-create を使用します。
アプリがデータセットの読み取りのみを行う場合は、このフェーズをスキップします。
フェーズ 5 — ツールキットクライアント(必要な場合)
toolkit を使用します。
価値がある場合は、型付きの @domoinc/toolkit クライアントを使用します。
フェーズ 6 — 機能スキル(必要に応じて)
アプリが実際に必要とするスキルだけを読み込みます。
| 必要な機能 | スキル |
|---|---|
| AI によるテキスト生成またはテキストから SQL への変換 | ai-service-layer |
| サーバーサイド関数(シークレット、外部 API) | code-engine + code-engine-create + code-engine-update |
| 自動化ワークフローの起動 | workflow |
これらの機能が必要ない場合は、このフェーズをスキップします。
フェーズ 7 — UI の構築(Remotion に安全なスタイリング)
アプリの UI コンポーネントを構築します。これは、Phase 0 の Remotion 制約を適用した、標準的な React 開発です。
- すべてのスタイリングに Tailwind ユーティリティまたはインラインの
styleオブジェクトを使用します。 transition-*、animate-*、CSS キーフレームアニメーションは使用しません。- すべての視覚状態は、props とサンプルデータ(Phase 3A)から再現できるようにします。
- 1920x1080 でレイアウトを確認します。
- デモで表示するすべての「シーン」(ローディング状態、データが入力されたダッシュボード、詳細ビューなど)は、ユーザークリックをシミュレートするのではなく、異なるサンプルデータを渡して表示できるようにします。
デモに適したコンポーネント設計
各視覚状態がデータによって決まるように、コンポーネントを構成します。
// Good: demo can render any state by passing props
<ChatPanel messages={sampleMessages} isTyping={false} />
<ChatPanel messages={sampleMessages} isTyping={true} />
// Bad: state is internal and requires user interaction to reach
<ChatPanel /> // internally manages messages via useState
アプリ全体をこの方法でリファクタリングする必要はありません。デモで表示したいビューだけを対象にします。主要な視覚状態を 3~5 個、props によって切り替えられるバリエーションとして公開します。
フェーズ 8 — パフォーマンスのレビュー
performance を使用します。
すべてのクエリを確認します。データセット全体の取得、集計の欠落、不要な列がないかをチェックします。
フェーズ 9 — ビルドと公開
publish を使用します。
npm run build -> cd dist -> domo publish。初回の公開時は、生成された id をソースのマニフェストに戻して記入します。
フェーズ 10 — デモ動画(Remotion)
デモ動画では、スクリーンショット、タイトルカード、マーケティング用スライドではなく、実際のアプリコンポーネントを Remotion 内で直接レンダリングします。視聴者には、サンプルデータが入力された実際の UI を、アニメーション付きのスクロールやビューの切り替えで表示し、アプリのすべての部分を見せます。
10.1 — Remotion プロジェクトをスキャフォールドする
アプリのソースと同じ階層に demo/ ディレクトリを作成します。
my-app/
src/ # Domo app source
demo/ # Remotion demo project
src/
Root.tsx
DemoVideo.tsx
index.ts
remotion.config.ts # Webpack alias to import app components
package.json
package.json
デモプロジェクトには、Remotion で実際のコンポーネントをレンダリングできるように、アプリで使用している UI 依存関係(例:recharts)を含める必要があります。
{
"dependencies": {
"@remotion/cli": "^4.0.0",
"@remotion/transitions": "^4.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"recharts": "^2.12.0",
"remotion": "^4.0.0"
}
}
アプリで使用しているその他の UI ライブラリ(チャートライブラリ、UI コンポーネントキットなど)も追加してください。
remotion.config.ts — アプリのインポート用 Webpack エイリアス
これは重要です。デモコードからアプリの src/ を直接インポートできるよう、@app エイリアスを設定します。
import { Config } from '@remotion/cli/config';
import path from 'path';
Config.overrideWebpackConfig((currentConfiguration) => ({
...currentConfiguration,
resolve: {
...currentConfiguration.resolve,
alias: {
...currentConfiguration.resolve?.alias,
'@app': path.resolve(process.cwd(), '..', 'src'),
},
},
}));
index.ts — Remotion のエントリーポイント
import { registerRoot } from 'remotion';
import { RemotionRoot } from './Root';
registerRoot(RemotionRoot);
tsconfig.json — パスマッピング
{
"compilerOptions": {
"paths": { "@app/*": ["../src/*"] },
"baseUrl": "."
},
"include": ["src", "../src"]
}
10.2 — AnimatedCursor コンポーネント
位置間を移動し、クリック時にパルスを表示する再利用可能なカーソルを作成します。アプリを誰かが操作しているように見せられます。
demo/src/components/AnimatedCursor.tsx に配置します。
import React from 'react';
import { useCurrentFrame, useVideoConfig, interpolate, spring, Easing } from 'remotion';
export type CursorWaypoint = {
x: number; // Pixel position from left
y: number; // Pixel position from top
atFrame: number; // Frame at which cursor should arrive here
click?: boolean; // Show a click pulse when arriving
};
type Props = {
waypoints: CursorWaypoint[];
size?: number; // Default 20
};
export const AnimatedCursor: React.FC<Props> = ({ waypoints, size = 20 }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
if (waypoints.length === 0) return null;
// Find which two waypoints we're interpolating between
let fromWp = waypoints[0];
let toWp = waypoints[0];
for (let i = 0; i < waypoints.length - 1; i++) {
if (frame >= waypoints[i].atFrame) {
fromWp = waypoints[i];
toWp = waypoints[i + 1];
}
}
if (frame >= waypoints[waypoints.length - 1].atFrame) {
fromWp = waypoints[waypoints.length - 1];
toWp = fromWp;
}
const easing = Easing.inOut(Easing.quad);
const x = fromWp === toWp ? fromWp.x
: interpolate(frame, [fromWp.atFrame, toWp.atFrame], [fromWp.x, toWp.x],
{ extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing });
const y = fromWp === toWp ? fromWp.y
: interpolate(frame, [fromWp.atFrame, toWp.atFrame], [fromWp.y, toWp.y],
{ extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing });
// Click pulse at waypoints marked click: true
let pulseOpacity = 0;
let pulseScale = 0;
for (const wp of waypoints) {
if (wp.click && frame >= wp.atFrame && frame < wp.atFrame + 20) {
const p = spring({ frame: frame - wp.atFrame, fps, config: { damping: 20, stiffness: 200 } });
pulseScale = interpolate(p, [0, 1], [0.5, 1.5]);
pulseOpacity = interpolate(frame - wp.atFrame, [0, 20], [0.5, 0], { extrapolateRight: 'clamp' });
}
}
return (
<div style={{ position: 'absolute', inset: 0, pointerEvents: 'none', zIndex: 9999 }}>
{pulseOpacity > 0 && (
<div style={{
position: 'absolute', left: x - 15, top: y - 15,
width: 30, height: 30, borderRadius: '50%',
border: '2px solid rgba(255,255,255,0.8)',
transform: `scale(${pulseScale})`, opacity: pulseOpacity,
}} />
)}
<svg style={{ position: 'absolute', left: x, top: y, width: size, height: size,
filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.5))' }} viewBox="0 0 24 24" fill="none">
<path d="M5 3L19 12L12 12L8 21L5 3Z" fill="white" stroke="#1e293b"
strokeWidth="1.5" strokeLinejoin="round" />
</svg>
</div>
);
};
10.3 — Root.tsx
import { Composition } from 'remotion';
import { DemoVideo } from './DemoVideo';
export const RemotionRoot = () => (
<Composition
id="AppDemo"
component={DemoVideo}
durationInFrames={300} // 10s at 30fps
fps={30}
width={1920}
height={1080}
/>
);
10.4 — アプリの種類別デモパターン
アプリに合ったパターンを選び、必要に応じて組み合わせてください。どのパターンも、@app/ 経由で実際のアプリコンポーネントをインポートします。
パターン A:単一ビューのスクロール(ダッシュボード、テーブル、長いフォーム)
コンポーネントをレンダリングし、translateY をアニメーションさせてスクロールします。
import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate, Easing } from 'remotion';
import { Dashboard } from '@app/App';
import { sampleData } from '@app/sample-data';
import { AnimatedCursor } from './components/AnimatedCursor';
export const DemoVideo: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const scrollY = interpolate(frame, [2 * fps, 6 * fps], [0, -500], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
easing: Easing.inOut(Easing.quad),
});
return (
<AbsoluteFill style={{ backgroundColor: '#0f172a', overflow: 'hidden' }}>
<div style={{ transform: `translateY(${scrollY}px)`, width: '100%' }}>
<Dashboard data={sampleData} />
</div>
<AnimatedCursor waypoints={[
{ x: 700, y: 400, atFrame: 0 },
{ x: 700, y: 500, atFrame: Math.round(3 * fps) },
{ x: 500, y: 450, atFrame: Math.round(6 * fps) },
]} />
</AbsoluteFill>
);
};
パターン B:タブ切り替え/複数ビューのアプリ(最も一般的)
<Sequence> を使ってビューを切り替えます。カーソルがタブ/ボタンへ移動してクリックパルスを表示し、次の Sequence で新しいビューをレンダリングします。
import { AbsoluteFill, Sequence, useCurrentFrame, useVideoConfig, interpolate, Easing } from 'remotion';
import { Dashboard } from '@app/App';
import { sampleData } from '@app/sample-data';
import { AnimatedCursor } from './components/AnimatedCursor';
const OverviewScene: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const scrollY = interpolate(frame, [2 * fps, 4 * fps], [0, -350], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
easing: Easing.inOut(Easing.quad),
});
return (
<AbsoluteFill style={{ overflow: 'hidden' }}>
<div style={{ transform: `translateY(${scrollY}px)`, width: '100%' }}>
<Dashboard data={sampleData} activeTab="overview" />
</div>
<AnimatedCursor waypoints={[
{ x: 700, y: 400, atFrame: 0 },
{ x: 900, y: 450, atFrame: Math.round(3 * fps) },
// End near the target tab button
{ x: 1195, y: 48, atFrame: Math.round(4.5 * fps) },
]} />
</AbsoluteFill>
);
};
const DetailScene: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const scrollY = interpolate(frame, [2 * fps, 4 * fps], [0, -200], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
easing: Easing.inOut(Easing.quad),
});
return (
<AbsoluteFill style={{ overflow: 'hidden' }}>
<div style={{ transform: `translateY(${scrollY}px)`, width: '100%' }}>
<Dashboard data={sampleData} activeTab="detail" selectedItem={sampleData.items[0]} />
</div>
<AnimatedCursor waypoints={[
{ x: 1195, y: 48, atFrame: 0, click: true }, // Click the tab
{ x: 600, y: 300, atFrame: Math.round(0.8 * fps) },
{ x: 500, y: 400, atFrame: Math.round(3 * fps) },
]} />
</AbsoluteFill>
);
};
export const DemoVideo: React.FC = () => {
const { fps } = useVideoConfig();
return (
<AbsoluteFill>
<Sequence from={0} durationInFrames={5 * fps} premountFor={fps}>
<OverviewScene />
</Sequence>
<Sequence from={5 * fps} durationInFrames={5 * fps} premountFor={fps}>
<DetailScene />
</Sequence>
</AbsoluteFill>
);
};
重要: シーン 1 の最後のカーソルの経由点と、シーン 2 の最初の経由点は、同じ位置(タブボタン)にしてください。これにより、カットをまたいでシームレスな「クリック」効果を作れます。
パターン C:AI チャット/会話型アプリ
フレームベースの配列スライスを使い、メッセージを段階的に増やしながらチャットコンポーネントをレンダリングします。実際の操作を必要とせず、メッセージが「届く」様子を表現できます。
import { AbsoluteFill, useCurrentFrame, useVideoConfig } from 'remotion';
import { ChatPanel } from '@app/components/ChatPanel';
import { sampleMessages } from '@app/sample-data';
import { AnimatedCursor } from './components/AnimatedCursor';
// Pre-define which messages are visible at which frame
const messageTimeline = [
{ visibleCount: 2, atFrame: 0 }, // Start with 2 messages already visible
{ visibleCount: 3, atFrame: 60 }, // User message appears at 2s
{ visibleCount: 3, typingIndicator: true, atFrame: 90 }, // AI typing at 3s
{ visibleCount: 4, atFrame: 120 }, // AI response appears at 4s
{ visibleCount: 5, atFrame: 180 }, // Another user message at 6s
{ visibleCount: 5, typingIndicator: true, atFrame: 210 },
{ visibleCount: 6, atFrame: 240 }, // Final AI response at 8s
];
export const DemoVideo: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Find current state based on frame
let currentState = messageTimeline[0];
for (const state of messageTimeline) {
if (frame >= state.atFrame) currentState = state;
}
const visibleMessages = sampleMessages.slice(0, currentState.visibleCount);
return (
<AbsoluteFill style={{ overflow: 'hidden' }}>
<ChatPanel
messages={visibleMessages}
isTyping={currentState.typingIndicator ?? false}
/>
<AnimatedCursor waypoints={[
{ x: 960, y: 800, atFrame: 0 },
// Cursor moves to input area before "sending" a message
{ x: 700, y: 950, atFrame: 50, click: true },
{ x: 960, y: 600, atFrame: 90 }, // Watches AI type
{ x: 700, y: 950, atFrame: 170, click: true }, // Sends another
{ x: 960, y: 500, atFrame: 240 }, // Reads response
]} />
</AbsoluteFill>
);
};
チャットアプリでの重要事項: ChatPanel コンポーネントは、messages と isTyping を props として受け取る必要があります(フェーズ 7 で適用されます)。フェーズ 3A のサンプルデータには、現実的な会話スレッドを含めてください。
パターン D:ボタン/操作の多いアプリ(ディスパッチボード、フォーム)
異なる prop の状態を順番にレンダリングして、ボタンのクリックをシミュレートします。カーソルをボタンに合わせてクリックパルスを表示し、「クリック後」の状態に切り替えます。
// Scene 1: Empty form state, cursor fills in a field
<Sequence from={0} durationInFrames={4 * fps}>
<FormView formData={emptyForm} />
<AnimatedCursor waypoints={[
{ x: 500, y: 300, atFrame: 0 },
{ x: 500, y: 300, atFrame: 30, click: true },
]} />
</Sequence>
// Scene 2: Filled form, cursor clicks submit
<Sequence from={4 * fps} durationInFrames={3 * fps}>
<FormView formData={filledForm} />
<AnimatedCursor waypoints={[
{ x: 500, y: 600, atFrame: 0 },
{ x: 500, y: 600, atFrame: 30, click: true }, // Click "Submit"
]} />
</Sequence>
// Scene 3: Success state
<Sequence from={7 * fps} durationInFrames={3 * fps}>
<FormView formData={filledForm} submitSuccess={true} />
</Sequence>
10.5 — カーソルの経由点を配置する際のヒント
- 座標の確認:
npx remotion studioを実行してプレビューにカーソルを合わせ、ボタン/タブ/要素が表示される位置のピクセル座標を記録します。 - Sequence 間で位置を合わせる: シーン 1 の最後にカーソルでタブを「クリック」する場合、シーン 2 での最初の位置も同じ座標にしてください。これにより、シームレスなトランジションになります。
- 移動速度: 移動時間は 30〜60 フレーム(1〜2 秒)にします。速すぎるとせわしなく、遅すぎると退屈に見えます。
- アニメーションを多用しない: 1 シーンあたりのカーソル移動は 2〜3 回で十分です。カーソルは注意を誘導するものであり、気を散らすものであってはいけません。
10.6 — プレビューとレンダリング
cd demo
npx remotion studio # Preview in browser
npx remotion render AppDemo # Render to out/AppDemo.mp4
フェーズ 11 — 検証
公開後、次の点を確認します。
- Domo でアプリがコンソールエラーなしに読み込まれる
- すべてのデータセットエイリアスが解決される(データ呼び出しで 404 が発生しない)
- AppDB コレクションがカード UI に接続されている(使用する場合)
- ページフィルターが正しく反映される(アプリをダッシュボードに埋め込む場合)
- ナビゲーションに
<a href>ではなくdomo.navigate()を使用している - サムネイルが
public/フォルダーにコピーされている - Domo の外で実行したときにサンプルデータが正しくレンダリングされる(undefined/null エラーが発生しない)
- デモ動画が空白フレームやアセットの欠落なしにレンダリングされる(
npx remotion render AppDemo) - デモ動画にデータが表示された UI が映る — 空の状態や読み込み中のスピナーではない
ビルド時のガードレール
- クライアントサイドのみとします。SSR/サーバールート/サーバーコンポーネントは使用しません。
- Vite の
base: './'を使用します。 - リライトを意図的に処理する場合を除き、
HashRouterを優先します。 domo.env.*は利便性のためだけに使用し、信頼性の判断には検証済みのアイデンティティを使用します。- デモでキャプチャするすべてのコンポーネントで、CSS トランジションやアニメーションを使用しないでください。
- サンプルデータに実際の顧客データを一切含めないでください。
- デモのすべての視覚状態は、操作ではなく prop によって制御してください。