ui-demo
Playwrightを使用して、WebアプリのUIデモ動画を録画します。デモ・ガイドツアー・スクリーンレコーディング・チュートリアル動画の作成を求められた際に使用します。可視カーソル・自然なテンポ・プロフェッショナルな仕上がりのWebM動画を生成します。
description の原文を見る
使用 Playwright 录制精美的 UI 演示视频。当用户要求创建 Web 应用的演示、导览、屏幕录制或教程视频时使用。生成带有可见光标、自然节奏和专业感的 WebM 视频。
SKILL.md 本文
UI デモビデオレコーダー
Playwright のビデオ録画機能を使用し、カーソルオーバーレイの注入、自然なペース、ナラティブフローを組み合わせて、美しい Web アプリケーションデモビデオを録画します。
ユースケース
- ユーザーが「デモビデオ」「スクリーンキャスト」「操作デモ」または「チュートリアル」の作成をリクエストしている
- ユーザーが機能またはワークフローを視覚的に示したいと考えている
- ユーザーがドキュメント、オンボーディング、またはステークホルダープレゼンテーション用のビデオを必要としている
3 段階のプロセス
すべてのデモは 3 つのフェーズを経る必要があります:探索 → リハーサル → 録画。録画フェーズに直接進まないでください。
フェーズ 1:探索
スクリプトを書く前に、対象ページを探索して実際のコンテンツを理解します。
理由
見たことのないコンテンツのスクリプトは書けません。フィールドが <textarea> ではなく <input> かもしれません。ドロップダウンが <select> ではなくカスタムコンポーネントかもしれません。コメントボックスが @mentions または #tags をサポートしているかもしれません。仮定は静かにデモを破壊します。
メソッド
フローの各ページに移動し、対話的要素をダンプします:
// Run this for each page in the flow BEFORE writing the demo script
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、Markdown、または絵文字をサポートしていますか?プレースホルダーテキストを確認してください。 - 必須フィールド:どのフィールドがフォーム送信をブロックしますか?ラベルの
requiredまたは*を確認し、空のフォームを送信して検証エラーを確認してください。 - 動的コンテンツ:他のフィールドを入力した後にフィールドが表示されますか?
- ボタンラベル:
"Submit"、"Submit Request"、または"Send"など、正確なテキストを確認してください。 - テーブル列ヘッダー:テーブル駆動のモーダルの場合、各
input[type="number"]をその列ヘッダーにマップし、すべての数値入力が同じ意味を表すと仮定しないでください。
出力
スクリプトで正しいセレクターを書くための各ページのフィールドマップ。例:
/purchase-requests/new:
- 予算コード: <select> (ページの最初のドロップダウン、4 つのオプション)
- 希望配送日: <input type="date">
- 背景説明: <textarea> (入力ボックスではない)
- BOM: インラインで編集可能なセル、span.cursor-pointer -> input パターン
- 送信: <button> テキスト="送信"
/purchase-requests/N (詳細):
- コメント: <input placeholder="メッセージを入力..."> @ユーザー および #PR タグをサポート
- 送信: <button> テキスト="送信" (入力前は無効)
フェーズ 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 = `REHEARSAL FAIL: "${label}" not found - selector: ${typeof locator === 'string' ? locator : '(locator object)'}`;
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(' Visible elements:\n ' + found);
return false;
}
console.log(`REHEARSAL OK: "${label}"`);
return true;
}
リハーサルスクリプト構造
const steps = [
{ label: 'Login email field', selector: '#email' },
{ label: 'Login submit', selector: 'button[type="submit"]' },
{ label: 'New Request button', selector: 'button:has-text("New Request")' },
{ label: 'Budget Code select', selector: 'select' },
{ label: 'Delivery date', selector: 'input[type="date"]:visible' },
{ label: 'Description field', selector: 'textarea:visible' },
{ label: 'Add Item button', selector: 'button:has-text("Add Item")' },
{ label: 'Submit button', selector: 'button:has-text("Submit")' },
];
let allOk = true;
for (const step of steps) {
if (!await ensureVisible(page, step.selector, step.label)) {
allOk = false;
}
}
if (!allOk) {
console.error('REHEARSAL FAILED - fix selectors before recording');
process.exit(1);
}
console.log('REHEARSAL PASSED - all selectors verified');
リハーサル失敗時
- 表示される要素ダンプを読む。
- 正しいセレクターを見つける。
- スクリプトを更新する。
- リハーサルを再実行する。
- すべてのセレクターが通過した後にのみ進める。
フェーズ 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(`WARNING: moveAndClick skipped - "${label}" not visible`);
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(`WARNING: 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(`WARNING: typeSlowly skipped - "${label}" not visible`);
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(`WARNING: panElements skipped element ${i} (selector: "${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, 'Step 1 - Logging in');
await showSubtitle(page, 'Step 2 - Dashboard overview');
await showSubtitle(page, '');
ガイドライン:
- 字幕テキストは短くしてください。理想的には 60 文字以内。
- 一貫性のため
Step N - Action形式を使用してください。 - インターフェイスが自分で説明できる場合に字幕を明確にします。
スクリプトテンプレート
'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');
// Paste injectCursor, injectSubtitleBar, showSubtitle, moveAndClick,
// typeSlowly, ensureVisible, and panElements here.
(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();
// Navigate through the flow and run ensureVisible for each selector.
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, 'Step 1 - Logging in');
// login actions
await page.goto(`${BASE_URL}/dashboard`);
await injectCursor(page);
await injectSubtitleBar(page);
await showSubtitle(page, 'Step 2 - Dashboard overview');
// pan dashboard
await showSubtitle(page, 'Step 3 - Main workflow');
// action sequence
await showSubtitle(page, 'Step 4 - Result');
// final reveal
await showSubtitle(page, '');
} catch (err) {
console.error('DEMO 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('Video saved:', dest);
} catch (e) {
console.error('ERROR: Failed to copy video:', e.message);
console.error(' Source:', src);
console.error(' Destination:', dest);
}
}
await browser.close();
}
})();
使用方法:
# フェーズ 2:リハーサル
node demo-script.cjs --rehearse
# フェーズ 3:録画
node demo-script.cjs
録画前のチェックリスト
- [ ] 探索フェーズが完了した
- [ ] リハーサルに合格し、すべてのセレクターが機能している
- [ ] ヘッドレスモードが有効になっている
- [ ] 解像度が
1280x720に設定されている - [ ] 各ナビゲーション後、カーソルと字幕オーバーレイを再注入している
- [ ] 主要な遷移で
showSubtitle(page, 'Step N - ...')を使用している - [ ] すべてのクリックは説明的なラベル付きで
moveAndClickを使用している - [ ] 可視入力は
typeSlowlyを使用している - [ ] サイレントキャプチャなし。ヘルパー関数は警告をログする
- [ ] コンテンツ表示はスムーズなスクロールを使用している
- [ ] 主要な一時停止は視聴者に見える
- [ ] フローはリクエストされたストーリー順序に従う
- [ ] スクリプトはフェーズ 1 で発見された実際のUI を反映している
一般的な落とし穴
- ナビゲーション後カーソルが消える - 再注入してください。
- ビデオが速すぎる - 一時停止を追加してください。
- カーソルが点で矢印ではない - SVG オーバーレイを使用してください。
- カーソルが瞬間移動する - クリック前に移動してください。
- ドロップダウン選択が奇妙に見える - 移動プロセスを表示してからオプションを選択してください。
- モーダルが急に出現する - 確認前に読み取り一時停止を追加してください。
- ビデオファイルのパスがランダム - 安定した出力名にコピーしてください。
- セレクター失敗が埋もれている - サイレント catch ブロックを使用しないでください。
- フィールドタイプが仮定されている - スクリプトを書く前に探索してください。
- 機能が仮定されている - スクリプトを書く前に実際の UI を確認してください。
- プレースホルダー選択値が実数のように見える -
"0"と"Select..."に注意してください。 - ポップアップが個別のビデオを作成する - ポップアップページを明示的にキャプチャし、必要に応じて後で結合してください。
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- affaan-m
- ライセンス
- MIT
- 最終更新
- 不明
Source: https://github.com/affaan-m/everything-claude-code / ライセンス: MIT
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。