Skip to main content

ui-demo

Playwrightを使用して美しいUIデモ動画を録画する。ユーザーがWebアプリのデモ、ウォークスルー、スクリーン録画、またはチュートリアル動画の作成を求める場合に使用する。可視カーソル、自然なリズム、プロフェッショナルな仕上がりのWebM動画を生成する。

Ir para a instalação

Informações da origem

Repositório
affaan-m/ECC
Última atividade na origem
17 de maio de 2026 às 06:31
Idioma detectado do SKILL.md
japonês
Estrelas
260.918
Forks
39.042

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
name
ui-demo
description
Playwrightを使用して美しいUIデモ動画を録画する。ユーザーがWebアプリのデモ、ウォークスルー、スクリーン録画、またはチュートリアル動画の作成を求める場合に使用する。可視カーソル、自然なリズム、プロフェッショナルな仕上がりのWebM動画を生成する。
origin
ECC
# UI デモ動画レコーダー Playwrightの動画録画機能を使用して、注入されたカーソルオーバーレイ、自然なリズム、ナラティブフローを備えた美しいWebアプリのデモ動画を録画する。 ## 使用場面 * ユーザーが「デモ動画」「スクリーン録画」「操作デモ」または「チュートリアル」を求める場合 * ユーザーが機能またはワークフローを視覚的に見せたい場合 * ユーザーがドキュメント、オンボーディング、ステークホルダーへのデモのために動画が必要な場合 ## 3フェーズのプロセス すべてのデモは **探索 -> リハーサル -> 録画** の3つのフェーズを経る。録画フェーズに直接ジャンプしない。 *** ## フェーズ 1:探索 スクリプトを書く前に、ターゲットページを探索して実際の内容を把握する。 ### なぜか 見たことのない内容のスクリプトは書けない。フィールドが `<textarea>` ではなく `<input>` の場合、ドロップダウンが `<select>` ではなくカスタムコンポーネントの場合、コメントボックスが `@mentions` や `#tags` をサポートしている場合があある。仮定は録画を静かに壊す。 ### 方法 フローの各ページに移動し、インタラクティブな要素をダンプする: ```javascript // 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"]` をその列ヘッダーにマッピングする。すべての数値入力が同じ意味を持つと仮定しない。 ### 出力 スクリプトに正しいセレクターを書くために使用する、ページごとのフィールドマッピング。例: ```text /purchase-requests/new: - 予算コード: <select>(ページの最初のドロップダウン、4オプション) - 希望納期: <input type="date"> - 背景説明: <textarea>(inputではない) - BOMテーブル: インライン編集可能なセル、span.cursor-pointer -> inputパターン - 送信: <button> テキスト="送信" /purchase-requests/N(詳細): - コメント: <input placeholder="メッセージを入力...">、@ユーザーと#PRタグに対応 - 送信: <button> テキスト="送信"(入力前は無効) ``` *** ## フェーズ 2:リハーサル 録画せずにすべてのステップを実行する。各セレクターが解決されることを確認する。 ### なぜか セレクターの失敗は、デモ録画が壊れる最大の原因。リハーサルは録画を無駄にする前に問題を発見する。 ### 方法 `ensureVisible` を使用する——ログを記録して大きくエラーを報告するラッパー: ```javascript 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; } ``` ### リハーサルスクリプトの構造 ```javascript 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'); ``` ### リハーサルが失敗した場合 1. 可視要素のダンプを読む。 2. 正しいセレクターを見つける。 3. スクリプトを更新する。 4. リハーサルを再実行する。 5. すべてのセレクターが通過した後のみ続行する。 *** ## フェーズ 3:録画 探索とリハーサルが通過した後にのみ、録画を作成する。 ### 録画の原則 #### 1. ナラティブフロー 動画をストーリーとして計画する。ユーザーが指定した順序に従うか、このデフォルト順序を使用する: * **エントリー**:ログインまたは開始点へのナビゲーション * **コンテキスト**:周囲を確認して、視聴者がどこにいるか理解できるようにする * **アクション**:主要なワークフローステップを実行する * **バリアント**:設定、テーマ、ローカライゼーションなどの補助機能を表示する * **結果**:結果、確認、または新しい状態を表示する #### 2. リズム * ログイン後:`4秒` * ナビゲーション後:`3秒` * ボタンクリック後:`2秒` * 主要なステップ間:`1.5〜2秒` * 最終アクション後:`3秒` * 入力の遅延:文字ごとに `25〜40ms` #### 3. カーソルオーバーレイ マウスの動きを追うSVGの矢印カーソルを注入する: ```javascript 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. マウスの動き カーソルを瞬間移動させない。クリック前にターゲットに移動する: ```javascript 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. 入力 瞬時に入力するのではなく、目に見えるように入力する: ```javascript 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. スクロール ジャンプではなくスムーズスクロールを使用する: ```javascript await page.evaluate(() => window.scrollTo({ top: 400, behavior: 'smooth' })); await page.waitForTimeout(1500); ``` #### 7. ダッシュボードパン ダッシュボードや概要ページを表示する場合、主要な要素の上にカーソルを移動させる: ```javascript 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. 字幕 ビューポートの下部に字幕バーを注入する: ```javascript 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); } ``` ナビゲーションのたびに `injectSubtitleBar(page)` を `injectCursor(page)` と一緒に呼び出す。 使用パターン: ```javascript await showSubtitle(page, 'Step 1 - Logging in'); await showSubtitle(page, 'Step 2 - Dashboard overview'); await showSubtitle(page, ''); ``` ガイドライン: * 字幕テキストは短く、60文字以内が望ましい。 * 一貫性のために `Step N - Action` 形式を使用する。 * 長い一時停止でインターフェースが自己説明的な場合は字幕をクリアする。 ## スクリプトテンプレート ```javascript '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 () => {
Ver no GitHub
Este SKILL.md e muito grande, entao o SkillsMP mostra aqui apenas a primeira secao. Ver no GitHub