mcpField
B
basic-custom-app-build-w-video

basic-custom-app-build-w-video

github.com/stahura開発24166 回インストール26 週間前に更新
リポジトリ

basic-custom-app-build-w-video について

Domo カスタムアプリの構築や引き継ぎと、Remotion によるデモ動画制作を扱う開発手順書です。決定論的な合成サンプルデータと Remotion 向けの UI 制約により、実際の Domo データなしでアプリのコンポーネントを動画に描画できます。

主な機能

  1. 01Domo アプリ構築フェーズの案内
  2. 02Remotion 対応スタイルの指針
  3. 03決定論的な合成サンプルデータ
  4. 04アプリのコンポーネントを動画に描画
  5. 05プロパティ駆動のビジュアル状態

活用シーン

  1. 01関係者向けデモを伴う Domo アプリの新規構築
  2. 02既存アプリのビジュアルデモ作成
  3. 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 つを追加します。

  1. Remotion に対応したスタイル制約をフェーズ 0 から適用し、アプリの UI を Remotion で手戻りなくキャプチャしてアニメーション化できるようにします。
  2. サンプルデータの生成(フェーズ 3A)でリアルなモックデータを作成し、ライブの Domo データセットや認証情報なしで、デモにデータが表示された UI をレンダリングできるようにします。
  3. デモ動画の作成(フェーズ 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.md
  • rules/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 — サンプルデータの生成

これは、デモの準備を整えるために不可欠な追加項目です。現実的なサンプルデータを生成し、次の条件を満たします。

  1. デモで表示するすべてのビュー(ダッシュボード、詳細パネル、チャート、テーブル、チャットメッセージなど)にデータを入力します。
  2. 実際の顧客/ユーザーデータを含めません — すべての名前、メールアドレス、ID、指標は合成データです。
  3. 決定論的です — 同じシードを使うと同じデータが生成されるため、スクリーンショットや 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 の各データセットエイリアスについて:
  1. Phase 3 のデータセットのフィールドマップに一致する、型付き配列をエクスポートする TypeScript ファイルを作成します。
  2. 現実的なデータを 20~100 行生成します(チャートやテーブルを説得力のある形で表示するのに十分な量)。
  3. 推測ではなく、データセットのスキーマにある実際のフィールド名を使用します。
  4. 興味深い視覚パターン(傾向、外れ値、カテゴリの分布)が生まれる値の範囲を含めます。
// 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 によって制御してください。

コメント