Communitygithub.com

ui-demo

Grave vídeos de demonstração de UI polidos usando Playwright. Use quando o usuário pedir para criar um demo, passo a passo, gravação de tela ou vídeo tutoriais de uma aplicação web. Produz vídeos WebM com cursor visível, ritmo natural e aparência profissional.

O que é ui-demo?

O UI Demo Video Recorder é uma habilidade voltada para agentes de IA que automatiza a criação de vídeos de demonstração de interfaces web com qualidade profissional. Ela utiliza o Playwright para gravar a tela, injetando um cursor SVG personalizado e uma barra de legendas para criar vídeos WebM com ritmo natural e fluxo narrativo. O processo é dividido em três fases: Descoberta (levantamento de elementos interativos), Ensaio (verificação de seletores) e Gravação (execução com overlays e delays). A habilidade é especialmente útil para criar vídeos para documentação, onboarding ou apresentações para stakeholders, garantindo que os seletores estejam corretos antes da gravação. Ela suporta comandos como `--rehearse` para teste e oferece funções como `moveAndClick`, `typeSlowly` e `panElements` para movimentação suave do mouse e digitação visível.

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

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

Gravador de Vídeos de Demonstração de UI

Grave vídeos de demonstração polidos de aplicações web usando a gravação de vídeo do Playwright com um overlay de cursor injetado, ritmo natural e fluxo narrativo.

Quando Usar

  • O usuário pede um "vídeo de demonstração", "gravação de tela", "passo a passo" ou "tutorial"
  • O usuário quer mostrar visualmente um recurso ou fluxo de trabalho
  • O usuário precisa de um vídeo para documentação, integração ou apresentação para partes interessadas

Processo de Três Fases

Cada demonstração passa por três fases: Descobrir -> Ensaia -> Gravar. Nunca pule direto para a gravação.


Fase 1: Descobrir

Antes de escrever qualquer script, explore as páginas alvo para entender o que realmente existe.

Por que

Você não pode roteirizar o que não viu. Campos podem ser <input> em vez de <textarea>, menus suspensos podem ser componentes personalizados em vez de <select>, e caixas de comentários podem suportar @menções ou #tags. Suposições quebram as gravações silenciosamente.

Como

Navegue para cada página no fluxo e despeje seus elementos interativos:

// Execute isso para cada página no fluxo ANTES de escrever o script de demonstração
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));

O que procurar

  • Campos de formulário: São <select>, <input>, menus suspensos personalizados ou comboboxes?
  • Opções de seleção: Despeje os valores E os textos das opções. Placeholders frequentemente têm value="0" ou value="" que parecem não vazios. Use Array.from(el.options).map(o => ({ value: o.value, text: o.text })). Pule opções onde o texto inclui "Selecionar" ou o valor é "0".
  • Texto rico: A caixa de comentários suporta @menções, #tags, markdown ou emoji? Verifique o texto do placeholder.
  • Campos obrigatórios: Quais campos bloqueiam o envio do formulário? Verifique required, * nos rótulos e tente enviar vazio para ver erros de validação.
  • Conteúdo dinâmico: Os campos aparecem após outros campos serem preenchidos?
  • Rótulos dos botões: Texto exato como "Enviar", "Enviar Solicitação" ou "Enviar".
  • Cabeçalhos de colunas da tabela: Para modais baseados em tabela, mapeie cada input[type="number"] para seu cabeçalho de coluna em vez de assumir que todas as entradas numéricas significam a mesma coisa.

Saída

Um mapa de campos para cada página, usado para escrever seletores corretos no script. Exemplo:

/purchase-requests/new:
  - Código Orçamentário: <select> (primeiro select na página, 4 opções)
  - Entrega Desejada: <input type="date">
  - Contexto: <textarea> (não input)
  - Tabela BOM: células editáveis inline com span.cursor-pointer -> padrão input
  - Enviar: <button> text="Enviar"

/purchase-requests/N (detalhe):
  - Comentário: <input placeholder="Digite uma mensagem..."> suporta tags @user e #PR
  - Enviar: <button> text="Enviar" (desabilitado até que o input tenha conteúdo)

Fase 2: Ensaia

Execute todas as etapas sem gravar. Verifique se cada seletor resolve.

Por que

Falhas silenciosas de seletores são a principal razão pela qual as gravações de demonstração quebram. O ensaio as pega antes que você perca uma gravação.

Como

Use ensureVisible, um wrapper que registra e falha alto:

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 = `FALHA NO ENSAIO: "${label}" não encontrado - seletor: ${typeof locator === 'string' ? locator : '(objeto 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('  Elementos visíveis:\n  ' + found);
    return false;
  }
  console.log(`ENSAIO OK: "${label}"`);
  return true;
}

Estrutura do script de ensaio

const steps = [
  { label: 'Campo de email de login', selector: '#email' },
  { label: 'Botão de envio de login', selector: 'button[type="submit"]' },
  { label: 'Botão Nova Solicitação', selector: 'button:has-text("Nova Solicitação")' },
  { label: 'Selecionar Código Orçamentário', selector: 'select' },
  { label: 'Data de entrega', selector: 'input[type="date"]:visible' },
  { label: 'Campo de descrição', selector: 'textarea:visible' },
  { label: 'Botão Adicionar Item', selector: 'button:has-text("Adicionar Item")' },
  { label: 'Botão Enviar', selector: 'button:has-text("Enviar")' },
];

let allOk = true;
for (const step of steps) {
  if (!await ensureVisible(page, step.selector, step.label)) {
    allOk = false;
  }
}
if (!allOk) {
  console.error('ENSAIO FALHOU - corrija os seletores antes de gravar');
  process.exit(1);
}
console.log('ENSAIO PASSOU - todos os seletores verificados');

Quando o ensaio falha

  1. Leia o despejo de elementos visíveis.
  2. Encontre o seletor correto.
  3. Atualize o script.
  4. Execute o ensaio novamente.
  5. Prossiga apenas quando todos os seletores passarem.

Fase 3: Gravar

Somente após a descoberta e o ensaio passarem, crie a gravação.

Princípios de Gravação

1. Fluxo Narrativo

Planeje o vídeo como uma história. Siga a ordem especificada pelo usuário ou use este padrão:

  • Entrada: Faça login ou navegue até o ponto de partida
  • Contexto: Passeie pelos arredores para que os espectadores se orientem
  • Ação: Execute as principais etapas do fluxo de trabalho
  • Variação: Mostre um recurso secundário como configurações, tema ou localização
  • Resultado: Mostre o resultado, confirmação ou novo estado

2. Ritmo

  • Após o login: 4s
  • Após a navegação: 3s
  • Após clicar em um botão: 2s
  • Entre etapas principais: 1.5-2s
  • Após a ação final: 3s
  • Atraso de digitação: 25-40ms por caractere

3. Overlay de Cursor

Injete um cursor de seta SVG que segue os movimentos do mouse:

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';
    });
  });
}

Chame injectCursor(page) após cada navegação de página, pois o overlay é destruído ao navegar.

4. Movimento do Mouse

Nunca teletransporte o cursor. Mova-se para o alvo antes de clicar:

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(`AVISO: moveAndClick pulado - "${label}" não visível`);
    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(`AVISO: moveAndClick falhou em "${label}": ${e.message}`);
    return false;
  }
  await page.waitForTimeout(postClickDelay);
  return true;
}

Cada chamada deve incluir um label descritivo para depuração.

5. Digitação

Digite visivelmente, não preencha instantaneamente:

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(`AVISO: typeSlowly pulado - "${label}" não visível`);
    return false;
  }
  await moveAndClick(page, el, label);
  await el.fill('');
  await el.pressSequentially(text, { delay: charDelay });
  await page.waitForTimeout(500);
  return true;
}

6. Rolagem

Use rolagem suave em vez de saltos:

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

7. Passeio pelo Painel

Ao mostrar um painel ou página de visão geral, mova o cursor pelos elementos principais:

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(`AVISO: panElements pulou elemento ${i} (seletor: "${selector}"): ${e.message}`);
    }
  }
}

8. Legendas

Injete uma barra de legenda na parte inferior da janela de visualização:

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);
}

Chame injectSubtitleBar(page) junto com injectCursor(page) após cada navegação.

Padrão de uso:

await showSubtitle(page, 'Passo 1 - Fazendo login');
await showSubtitle(page, 'Passo 2 - Visão geral do painel');
await showSubtitle(page, '');

Diretrizes:

  • Mantenha o texto da legenda curto, idealmente abaixo de 60 caracteres.
  • Use o formato Passo N - Ação para consistência.
  • Limpe a legenda durante pausas longas onde a UI pode falar por si mesma.

Modelo de Script

'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-FUNCIONALIDADE.webm';
const REHEARSAL = process.argv.includes('--rehearse');

// Cole injectCursor, injectSubtitleBar, showSubtitle, moveAndClick,
// typeSlowly, ensureVisible e panElements aqui.

(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();
    // Navegue pelo fluxo e execute ensureVisible para cada seletor.
    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, 'Passo 1 - Fazendo login');
    // ações de login

    await page.goto(`${BASE_URL}/dashboard`);
    await injectCursor(page);
    await injectSubtitleBar(page);
    await showSubtitle(page, 'Passo 2 - Visão geral do painel');
    // passear pelo painel

    await showSubtitle(page, 'Passo 3 - Fluxo de trabalho principal');
    // sequência de ações

    await showSubtitle(page, 'Passo 4 - Resultado');
    // revelação final
    await showSubtitle(page, '');
  } catch (err) {
    console.error('ERRO NA DEMONSTRAÇÃO:', 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('Vídeo salvo:', dest);
      } catch (e) {
        console.error('ERRO: Falha ao copiar vídeo:', e.message);
        console.error('  Origem:', src);
        console.error('  Destino:', dest);
      }
    }
    await browser.close();
  }
})();

Uso:

# Fase 2: Ensaia
node demo-script.cjs --rehearse

# Fase 3: Gravar
node demo-script.cjs

Lista de Verificação Antes de Gravar

  • Fase de descoberta concluída
  • Ensaio passa com todos os seletores OK
  • Modo headless ativado
  • Resolução configurada para 1280x720
  • Overlays de cursor e legenda reinjetados após cada navegação
  • showSubtitle(page, 'Passo N - ...') usado nas principais transições
  • moveAndClick usado para todos os cliques com rótulos descritivos
  • typeSlowly usado para entrada visível
  • Nenhum catch silencioso;

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.

Habilidades Relacionadas