Claude Agent Skill · by Affaan M

Ui Demo

Install Ui Demo skill for Claude Code from affaan-m/everything-claude-code.

Install
Terminal · npx
$npx skills add https://github.com/vercel-labs/agent-skills --skill vercel-react-best-practices
Works with Paperclip

How Ui Demo fits into a Paperclip company.

Ui Demo drops into any Paperclip agent that handles this kind of work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.

S
SaaS FactoryPaired

Pre-configured AI company — 18 agents, 18 skills, one-time purchase.

$27$59
Explore pack
Source file
SKILL.md465 lines
Expand
---name: ui-demodescription: Record polished UI demo videos using Playwright. Use when the user asks to create a demo, walkthrough, screen recording, or tutorial video of a web application. Produces WebM videos with visible cursor, natural pacing, and professional feel.origin: ECC--- # UI Demo Video Recorder Record polished demo videos of web applications using Playwright's video recording with an injected cursor overlay, natural pacing, and storytelling flow. ## When to Use - User asks for a "demo video", "screen recording", "walkthrough", or "tutorial"- User wants to showcase a feature or workflow visually- User needs a video for documentation, onboarding, or stakeholder presentation ## Three-Phase Process Every demo goes through three phases: **Discover -> Rehearse -> Record**. Never skip straight to recording. --- ## Phase 1: Discover Before writing any script, explore the target pages to understand what is actually there. ### Why You cannot script what you have not seen. Fields may be `<input>` not `<textarea>`, dropdowns may be custom components not `<select>`, and comment boxes may support `@mentions` or `#tags`. Assumptions break recordings silently. ### How Navigate to each page in the flow and dump its interactive elements: ```javascript// Run this for each page in the flow BEFORE writing the demo scriptconst 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));``` ### What to look for - **Form fields**: Are they `<select>`, `<input>`, custom dropdowns, or comboboxes?- **Select options**: Dump option values AND text. Placeholders often have `value="0"` or `value=""` which looks non-empty. Use `Array.from(el.options).map(o => ({ value: o.value, text: o.text }))`. Skip options where text includes "Select" or value is `"0"`.- **Rich text**: Does the comment box support `@mentions`, `#tags`, markdown, or emoji? Check placeholder text.- **Required fields**: Which fields block form submission? Check `required`, `*` in labels, and try submitting empty to see validation errors.- **Dynamic content**: Do fields appear after other fields are filled?- **Button labels**: Exact text such as `"Submit"`, `"Submit Request"`, or `"Send"`.- **Table column headers**: For table-driven modals, map each `input[type="number"]` to its column header instead of assuming all numeric inputs mean the same thing. ### Output A field map for each page, used to write correct selectors in the script. Example: ```text/purchase-requests/new:  - Budget Code: <select> (first select on page, 4 options)  - Desired Delivery: <input type="date">  - Context: <textarea> (not input)  - BOM table: inline-editable cells with span.cursor-pointer -> input pattern  - Submit: <button> text="Submit" /purchase-requests/N (detail):  - Comment: <input placeholder="Type a message..."> supports @user and #PR tags  - Send: <button> text="Send" (disabled until input has content)``` --- ## Phase 2: Rehearse Run through all steps without recording. Verify every selector resolves. ### Why Silent selector failures are the main reason demo recordings break. Rehearsal catches them before you waste a recording. ### How Use `ensureVisible`, a wrapper that logs and fails loudly: ```javascriptasync 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;}``` ### Rehearsal script structure ```javascriptconst 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');``` ### When rehearsal fails 1. Read the visible-element dump.2. Find the correct selector.3. Update the script.4. Re-run rehearsal.5. Only proceed when every selector passes. --- ## Phase 3: Record Only after discovery and rehearsal pass should you create the recording. ### Recording Principles #### 1. Storytelling Flow Plan the video as a story. Follow user-specified order, or use this default: - **Entry**: Login or navigate to the starting point- **Context**: Pan the surroundings so viewers orient themselves- **Action**: Perform the main workflow steps- **Variation**: Show a secondary feature such as settings, theme, or localization- **Result**: Show the outcome, confirmation, or new state #### 2. Pacing - After login: `4s`- After navigation: `3s`- After clicking a button: `2s`- Between major steps: `1.5-2s`- After the final action: `3s`- Typing delay: `25-40ms` per character #### 3. Cursor Overlay Inject an SVG arrow cursor that follows mouse movements: ```javascriptasync 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';    });  });}``` Call `injectCursor(page)` after every page navigation because the overlay is destroyed on navigate. #### 4. Mouse Movement Never teleport the cursor. Move to the target before clicking: ```javascriptasync 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;}``` Every call should include a descriptive `label` for debugging. #### 5. Typing Type visibly, not instant-fill: ```javascriptasync 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. Scrolling Use smooth scroll instead of jumps: ```javascriptawait page.evaluate(() => window.scrollTo({ top: 400, behavior: 'smooth' }));await page.waitForTimeout(1500);``` #### 7. Dashboard Panning When showing a dashboard or overview page, move the cursor across key elements: ```javascriptasync 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. Subtitles Inject a subtitle bar at the bottom of the viewport: ```javascriptasync 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);}``` Call `injectSubtitleBar(page)` alongside `injectCursor(page)` after every navigation. Usage pattern: ```javascriptawait showSubtitle(page, 'Step 1 - Logging in');await showSubtitle(page, 'Step 2 - Dashboard overview');await showSubtitle(page, '');``` Guidelines: - Keep subtitle text short, ideally under 60 characters.- Use `Step N - Action` format for consistency.- Clear the subtitle during long pauses where the UI can speak for itself. ## Script Template ```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 () => {  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();  }})();``` Usage: ```bash# Phase 2: Rehearsenode demo-script.cjs --rehearse # Phase 3: Recordnode demo-script.cjs``` ## Checklist Before Recording - [ ] Discovery phase completed- [ ] Rehearsal passes with all selectors OK- [ ] Headless mode enabled- [ ] Resolution set to `1280x720`- [ ] Cursor and subtitle overlays re-injected after every navigation- [ ] `showSubtitle(page, 'Step N - ...')` used at major transitions- [ ] `moveAndClick` used for all clicks with descriptive labels- [ ] `typeSlowly` used for visible input- [ ] No silent catches; helpers log warnings- [ ] Smooth scrolling used for content reveal- [ ] Key pauses are visible to a human viewer- [ ] Flow matches the requested story order- [ ] Script reflects the actual UI discovered in phase 1 ## Common Pitfalls 1. Cursor disappears after navigation - re-inject it.2. Video is too fast - add pauses.3. Cursor is a dot instead of an arrow - use the SVG overlay.4. Cursor teleports - move before clicking.5. Select dropdowns look wrong - show the move, then pick the option.6. Modals feel abrupt - add a read pause before confirming.7. Video file path is random - copy it to a stable output name.8. Selector failures are swallowed - never use silent catch blocks.9. Field types were assumed - discover them first.10. Features were assumed - inspect the actual UI before scripting.11. Placeholder select values look real - watch for `"0"` and `"Select..."`.12. Popups create separate videos - capture popup pages explicitly and merge later if needed.