Communitygithub.com

UIデモビデオレコーダー

Playwrightを使用して、洗練されたUIデモ動画を録画します。ユーザーがデモ、ウォークスルー、画面録画、チュートリアル動画の作成を依頼した場合に使用します。WebM形式で、カーソル表示、自然なペース、プロフェッショナルな仕上がりの動画を生成します。

UIデモビデオレコーダー とは?

このスキルは、Playwrightを活用してWebアプリケーションの高品質なデモ動画を録画するためのものです。Discover(発見)、Rehearse(リハーサル)、Record(録画)の3つのフェーズで構成され、録画前にページのインタラクティブ要素を調査し、セレクタの動作確認を行うことで、録画の失敗を防止します。録画時には、ストーリーテリングの流れに沿った演出、自然なページング、カーソルオーバーレイの表示、動画のオプション機能により、プロフェッショナルな仕上がりを実現します。Claude CodeやCursorなどのAIエージェント開発環境に対応し、ユーザーが「デモ動画が欲しい」「ウォークスルーを作って」と依頼した際に自動的にワークフローを生成・実行します。

対応~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/affaan-m/ECC/tree/main/skills/ui-demo

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

UIデモ動画レコーダー

Playwrightの動画録画機能と注入されたカーソルオーバーレイ、自然なペース、ストーリーテリングの流れを使って、洗練されたWebアプリケーションのデモ動画を録画します。

使用すべき時

  • ユーザーが「デモ動画」「画面録画」「チュートリアル」「使い方ガイド」などを要求した時
  • ユーザーが機能やワークフローを視覚的に見せたい時
  • ドキュメント、オンボーディング、関係者へのプレゼンテーション用の動画が必要な時

3フェーズプロセス

すべてのデモは3つのフェーズを経ます:発見 -> リハーサル -> 録画。 すぐに録画に進んではいけません。


フェーズ 1: 発見

スクリプトを書く前に、対象ページを探索して実際に何があるかを理解します。

理由

見たことがないものをスクリプト化することはできません。フィールドが<textarea>ではなく<input>だったり、ドロップダウンがカスタムコンポーネントで<select>ではなかったり、コメントボックスが@mentionsや#tagsをサポートしていたりする場合があります。仮定は録画を黙って壊します。

方法

フロー内の各ページに移動し、インタラクティブ要素をダンプします:

// デモスクリプトを書く前に、フロー内の各ページでこれを実行
const fields = await page.evaluate(() => {
  const els = [];
  document.querySelectorAll('input, select, textarea, button, [contenteditable]').forEach(el => {
    if (el.offsetParent !== null) {
      els.push({
        tag: el.tagName,
        type: el.type || '',
        name: el.name || '',
        placeholder: el.placeholder || '',
        text: el.textContent?.trim().substring(0, 40) || '',
        contentEditable: el.contentEditable === 'true',
        role: el.getAttribute('role') || '',
      });
    }
  });
  return els;
});
console.log(JSON.stringify(fields, null, 2));

注目すべき点

  • フォームフィールド: <select>、<input>、カスタムドロップダウン、コンボボックスのどれですか?
  • セレクトオプション: オプションの値とテキストの両方をダンプします。プレースホルダーはvalue="0"やvalue=""であることが多く、空でないように見えます。Array.from(el.options).map(o => ({ value: o.value, text: o.text }))を使用します。テキストに「選択」が含まれているか、値が"0"であるオプションはスキップします。
  • リッチテキスト: コメントボックスは@mentions、#tags、マークダウン、絵文字をサポートしていますか?プレースホルダーテキストを確認します。
  • 必須フィールド: どのフィールドがフォーム送信をブロックしますか?required、ラベルの*を確認し、空の状態で送信してバリデーションエラーを確認します。
  • 動的コンテンツ: 他のフィールドが入力された後にフィールドが表示されますか?
  • ボタンラベル: 「送信」「提出リクエスト」「送る」など、正確なテキスト。
  • テーブル列ヘッダー: テーブル駆動のモーダルの場合、すべての数値入力が同じ意味を持つと想定する代わりに、各input[type="number"]をその列ヘッダーにマッピングします。

出力

各ページのフィールドマップ。スクリプトで正しいセレクターを書くために使用します。例:

/purchase-requests/new:
  - 予算コード: <select> (ページの最初のselect、4つのオプション)
  - 希望納期: <input type="date">
  - コンテキスト: <textarea> (inputではない)
  - BOMテーブル: インライン編集可能セルとspan.cursor-pointer -> inputパターン
  - 提出: <button> text="提出"

/purchase-requests/N (詳細):
  - コメント: <input placeholder="メッセージを入力..."> @userと#PRタグをサポート
  - 送信: <button> text="送信" (入力に内容が入るまで無効)

フェーズ 2: リハーサル

録画せずにすべてのステップを実行します。すべてのセレクターが解決されることを確認します。

理由

サイレントセレクターエラーは、デモ録画が壊れる主な理由です。リハーサルは、録画を無駄にする前にそれらをキャッチします。

方法

ensureVisibleを使用します。これはログを出力し、エラーが発生した場合に大きな音を出します:

async function ensureVisible(page, locator, label) {
  const el = typeof locator === 'string' ? page.locator(locator).first() : locator;
  const visible = await el.isVisible().catch(() => false);
  if (!visible) {
    const msg = `リハーサル失敗: "${label}" が見つかりません - セレクター: ${typeof locator === 'string' ? locator : '(ロケーターオブジェクト)'}`;
    console.error(msg);
    const found = await page.evaluate(() => {
      return Array.from(document.querySelectorAll('button, input, select, textarea, a'))
        .filter(el => el.offsetParent !== null)
        .map(el => `${el.tagName}[${el.type || ''}] "${el.textContent?.trim().substring(0, 30)}"`)
        .join('\n  ');
    });
    console.error('  表示されている要素:\n  ' + found);
    return false;
  }
  console.log(`リハーサルOK: "${label}"`);
  return true;
}

リハーサルスクリプトの構造

const steps = [
  { label: 'ログインメールフィールド', selector: '#email' },
  { label: 'ログイン送信', selector: 'button[type="submit"]' },
  { label: '新規リクエストボタン', selector: 'button:has-text("新規リクエスト")' },
  { label: '予算コード選択', selector: 'select' },
  { label: '納期', selector: 'input[type="date"]:visible' },
  { label: '説明フィールド', selector: 'textarea:visible' },
  { label: 'アイテム追加ボタン', selector: 'button:has-text("アイテム追加")' },
  { label: '提出ボタン', selector: 'button:has-text("提出")' },
];

let allOk = true;
for (const step of steps) {
  if (!await ensureVisible(page, step.selector, step.label)) {
    allOk = false;
  }
}
if (!allOk) {
  console.error('リハーサル失敗 - 録画前にセレクターを修正してください');
  process.exit(1);
}
console.log('リハーサル成功 - すべてのセレクターが確認されました');

リハーサルが失敗した場合

  1. 表示要素のダンプを読みます。
  2. 正しいセレクターを見つけます。
  3. スクリプトを更新します。
  4. リハーサルを再実行します。
  5. すべてのセレクターが通った場合のみ進みます。

フェーズ 3: 録画

発見とリハーサルが通った後にのみ、録画を作成します。

録画の原則

1. ストーリーテリングの流れ

動画をストーリーとして計画します。ユーザー指定の順序に従うか、以下のデフォルトを使用します:

  • エントリー: ログインまたは開始点に移動
  • コンテキスト: 周囲をパンして、視聴者が方向を把握できるようにします
  • アクション: メインのワークフローステップを実行
  • バリエーション: 設定、テーマ、ローカリゼーションなどのセカンダリ機能を表示
  • 結果: 結果、確認、または新しい状態を表示

2. ペーシング

  • ログイン後: 4s
  • ナビゲーション後: 3s
  • ボタンをクリックした後: 2s
  • 主要なステップ間: 1.5-2s
  • 最後のアクション後: 3s
  • タイピング遅延: 1文字あたり25-40ms

3. カーソルオーバーレイ

マウスの動きに追従するSVG矢印カーソルを注入します:

async function injectCursor(page) {
  await page.evaluate(() => {
    if (document.getElementById('demo-cursor')) return;
    const cursor = document.createElement('div');
    cursor.id = 'demo-cursor';
    cursor.innerHTML = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M5 3L19 12L12 13L9 20L5 3Z" fill="white" stroke="black" stroke-width="1.5" stroke-linejoin="round"/>
    </svg>`;
    cursor.style.cssText = `
      position: fixed; z-index: 999999; pointer-events: none;
      width: 24px; height: 24px;
      transition: left 0.1s, top 0.1s;
      filter: drop-shadow(1px 1px 2px rgba(0,0,0,0.3));
    `;
    cursor.style.left = '0px';
    cursor.style.top = '0px';
    document.body.appendChild(cursor);
    document.addEventListener('mousemove', (e) => {
      cursor.style.left = e.clientX + 'px';
      cursor.style.top = e.clientY + 'px';
    });
  });
}

オーバーレイはナビゲーション時に破棄されるため、ページナビゲーションのたびにinjectCursor(page)を呼び出します。

4. マウスの動き

カーソルをテレポートさせないでください。クリックする前にターゲットに移動します:

async function moveAndClick(page, locator, label, opts = {}) {
  const { postClickDelay = 800, ...clickOpts } = opts;
  const el = typeof locator === 'string' ? page.locator(locator).first() : locator;
  const visible = await el.isVisible().catch(() => false);
  if (!visible) {
    console.error(`警告: moveAndClick skipped - "${label}" が表示されていません`);
    return false;
  }
  try {
    await el.scrollIntoViewIfNeeded();
    await page.waitForTimeout(300);
    const box = await el.boundingBox();
    if (box) {
      await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2, { steps: 10 });
      await page.waitForTimeout(400);
    }
    await el.click(clickOpts);
  } catch (e) {
    console.error(`警告: moveAndClick failed on "${label}": ${e.message}`);
    return false;
  }
  await page.waitForTimeout(postClickDelay);
  return true;
}

すべての呼び出しには、デバッグ用の説明的なlabelを含める必要があります。

5. タイピング

即時入力ではなく、可視的にタイプします:

async function typeSlowly(page, locator, text, label, charDelay = 35) {
  const el = typeof locator === 'string' ? page.locator(locator).first() : locator;
  const visible = await el.isVisible().catch(() => false);
  if (!visible) {
    console.error(`警告: typeSlowly skipped - "${label}" が表示されていません`);
    return false;
  }
  await moveAndClick(page, el, label);
  await el.fill('');
  await el.pressSequentially(text, { delay: charDelay });
  await page.waitForTimeout(500);
  return true;
}

6. スクロール

ジャンプではなくスムーズスクロールを使用します:

await page.evaluate(() => window.scrollTo({ top: 400, behavior: 'smooth' }));
await page.waitForTimeout(1500);

7. ダッシュボードパンニング

ダッシュボードや概要ページを表示するときは、主要な要素間でカーソルを動かします:

async function panElements(page, selector, maxCount = 6) {
  const elements = await page.locator(selector).all();
  for (let i = 0; i < Math.min(elements.length, maxCount); i++) {
    try {
      const box = await elements[i].boundingBox();
      if (box && box.y < 700) {
        await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2, { steps: 8 });
        await page.waitForTimeout(600);
      }
    } catch (e) {
      console.warn(`警告: panElements skipped element ${i} (セレクター: "${selector}"): ${e.message}`);
    }
  }
}

8. 字幕

ビューポートの下部に字幕バーを注入します:

async function injectSubtitleBar(page) {
  await page.evaluate(() => {
    if (document.getElementById('demo-subtitle')) return;
    const bar = document.createElement('div');
    bar.id = 'demo-subtitle';
    bar.style.cssText = `
      position: fixed; bottom: 0; left: 0; right: 0; z-index: 999998;
      text-align: center; padding: 12px 24px;
      background: rgba(0, 0, 0, 0.75);
      color: white; font-family: -apple-system, "Segoe UI", sans-serif;
      font-size: 16px; font-weight: 500; letter-spacing: 0.3px;
      transition: opacity 0.3s;
      pointer-events: none;
    `;
    bar.textContent = '';
    bar.style.opacity = '0';
    document.body.appendChild(bar);
  });
}

async function showSubtitle(page, text) {
  await page.evaluate((t) => {
    const bar = document.getElementById('demo-subtitle');
    if (!bar) return;
    if (t) {
      bar.textContent = t;
      bar.style.opacity = '1';
    } else {
      bar.style.opacity = '0';
    }
  }, text);
  if (text) await page.waitForTimeout(800);
}

ナビゲーションのたびにinjectCursor(page)と一緒にinjectSubtitleBar(page)を呼び出します。

使用パターン:

await showSubtitle(page, 'ステップ 1 - ログイン');
await showSubtitle(page, 'ステップ 2 - ダッシュボード概要');
await showSubtitle(page, '');

ガイドライン:

  • 字幕テキストは短く、理想的には60文字未満にします。
  • 一貫性のためにステップ N - アクションの形式を使用します。
  • UIがそれ自体を物語る長い一時停止中は字幕をクリアします。

スクリプトテンプレート

'use strict';
const { chromium } = require('playwright');
const path = require('path');
const fs = require('fs');

const BASE_URL = process.env.QA_BASE_URL || 'http://localhost:3000';
const VIDEO_DIR = path.join(__dirname, 'screenshots');
const OUTPUT_NAME = 'demo-FEATURE.webm';
const REHEARSAL = process.argv.includes('--rehearse');

// injectCursor, injectSubtitleBar, showSubtitle, moveAndClick,
// typeSlowly, ensureVisible, panElements をここに貼り付けます。

(async () => {
  const browser = await chromium.launch({ headless: true });

  if (REHEARSAL) {
    const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
    const page = await context.newPage();
    // フローをナビゲートし、各セレクターに対して ensureVisible を実行します。
    await browser.close();
    return;
  }

  const context = await browser.newContext({
    recordVideo: { dir: VIDEO_DIR, size: { width: 1280, height: 720 } },
    viewport: { width: 1280, height: 720 }
  });
  const page = await context.newPage();

  try {
    await injectCursor(page);
    await injectSubtitleBar(page);

    await showSubtitle(page, 'ステップ 1 - ログイン');
    // ログインアクション

    await page.goto(`${BASE_URL}/dashboard`);
    await injectCursor(page);
    await injectSubtitleBar(page);
    await showSubtitle(page, 'ステップ 2 - ダッシュボード概要');
    // ダッシュボードをパン

    await showSubtitle(page, 'ステップ 3 - メインワークフロー');
    // アクションシーケンス

    await showSubtitle(page, 'ステップ 4 - 結果');
    // 最終表示
    await showSubtitle(page, '');
  } catch (err) {
    console.error('デモエラー:', err.message);
  } finally {
    await context.close();
    const video = page.video();
    if (video) {
      const src = await video.path();
      const dest = path.join(VIDEO_DIR, OUTPUT_NAME);
      try {
        fs.copyFileSync(src, dest);
        console.log('動画を保存:', dest);
      } catch (e) {
        console.error('エラー: 動画のコピーに失敗:', e.message);
        console.error('  ソース:', src);
        console.error('  出力先:', dest);
      }
    }
    await browser.close();
  }
})();

使い方:

# フェーズ 2: リハーサル
node demo-script.cjs --rehearse

# フェーズ 3: 録画
node demo-script.cjs

録画前のチェックリスト

  • 発見フェーズが完了している
  • リハーサルがすべてのセレクターでOKになっている
  • ヘッドレスモードが有効になっている
  • 解像度が 1280x720 に設定されている
  • カーソルと字幕のオーバーレイが各ナビゲーション後に再注入されている
  • 主要な遷移で showSubtitle(page, 'ステップ N - ...') が使用されている
  • すべてのクリックで moveAndClick が説明的なラベルとともに使用されている
  • 可視入力で typeSlowly が使用されている
  • 無音のキャッチはありません。

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

関連スキル