Communitygithub.com

ui-demo

Graba videos de demostración de UI pulidos usando Playwright. Úsalo cuando el usuario pida crear un demo, tutorial, grabación de pantalla o video instructivo de una aplicación web. Produce videos WebM con cursor visible, ritmo natural y aspecto profesional.

¿Qué es ui-demo?

Este skill está diseñado para que un agente de IA (como Claude Code, Cursor o Codex) pueda grabar videos de demostración de interfaces web de alta calidad usando Playwright. Su valor principal está en un proceso estructurado de tres fases (Descubrir, Ensayar, Grabar) que evita errores comunes y garantiza un resultado pulido. La herramienta es ideal para crear videos para documentación, onboarding, presentaciones o tutoriales. El proceso comienza con la fase de Descubrimiento, donde se inspecciona la página para mapear sus elementos interactivos reales (campos, botones, menús). Luego se pasa al Ensayo, verificando que todos los selectores funcionen correctamente antes de grabar. Finalmente, en la fase de Grabación se produce el video con técnicas profesionales: superposición de cursor SVG animado, subtítulos descriptivos, movimientos de mouse suaves, escritura visible con demora realista y un flujo narrativo con ritmo controlado. El resultado es un video WebM (por defecto 1280x720) que incluye el cursor, transiciones pausadas y una sensación profesional, ideal para compartir con stakeholders o incluir en documentación.

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

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Grabador de Videos Demostrativos de UI

Graba videos de demostración pulidos de aplicaciones web usando la grabación de video de Playwright con una superposición de cursor inyectada, ritmo natural y flujo narrativo.

Cuándo Usarlo

  • El usuario pide un "video demo", "grabación de pantalla", "tutorial" o "walkthrough"
  • El usuario quiere mostrar visualmente una funcionalidad o flujo de trabajo
  • El usuario necesita un video para documentación, onboarding o presentación a los interesados

Proceso de Tres Fases

Cada demo pasa por tres fases: Descubrir -> Ensayar -> Grabar. Nunca vayas directamente a grabar.


Fase 1: Descubrir

Antes de escribir cualquier script, explora las páginas de destino para entender lo que realmente hay.

Por qué

No puedes escribir un script para lo que no has visto. Los campos pueden ser <input> y no <textarea>, los menús desplegables pueden ser componentes personalizados y no <select>, y los cuadros de comentarios pueden soportar @menciones o #etiquetas. Las suposiciones rompen las grabaciones en silencio.

Cómo

Navega a cada página en el flujo y vuelca sus elementos interactivos:

// Ejecuta esto para cada página en el flujo ANTES de escribir el script del demo
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));

Qué buscar

  • Campos de formulario: ¿Son <select>, <input>, menús desplegables personalizados o comboboxes?
  • Opciones de selección: Vuelca los valores y el texto de las opciones. Los placeholders a menudo tienen value="0" o value="" que parece no vacío. Usa Array.from(el.options).map(o => ({ value: o.value, text: o.text })). Omite opciones donde el texto incluya "Seleccionar" o el valor sea "0".
  • Texto enriquecido: ¿El cuadro de comentarios soporta @menciones, #etiquetas, markdown o emojis? Verifica el texto del placeholder.
  • Campos obligatorios: ¿Qué campos bloquean el envío del formulario? Verifica required, * en las etiquetas e intenta enviar vacío para ver errores de validación.
  • Contenido dinámico: ¿Aparecen campos después de llenar otros?
  • Etiquetas de botones: Texto exacto como "Enviar", "Enviar Solicitud" o "Enviar".
  • Encabezados de columnas de tabla: Para modales basados en tablas, mapea cada input[type="number"] a su encabezado de columna en lugar de asumir que todas las entradas numéricas significan lo mismo.

Salida

Un mapa de campos para cada página, usado para escribir selectores correctos en el script. Ejemplo:

/purchase-requests/new:
  - Código Presupuestario: <select> (primer select en la página, 4 opciones)
  - Fecha de Entrega Deseada: <input type="date">
  - Contexto: <textarea> (no input)
  - Tabla BOM: celdas editables en línea con span.cursor-pointer -> patrón input
  - Enviar: <button> text="Enviar"

/purchase-requests/N (detalle):
  - Comentario: <input placeholder="Escribe un mensaje..."> soporta @usuario y #PR
  - Enviar: <button> text="Enviar" (deshabilitado hasta que la entrada tenga contenido)

Fase 2: Ensayar

Ejecuta todos los pasos sin grabar. Verifica que cada selector se resuelva.

Por qué

Los fallos silenciosos de selectores son la razón principal por la que las grabaciones de demo se rompen. El ensayo los detecta antes de desperdiciar una grabación.

Cómo

Usa ensureVisible, un envoltorio que registra y falla ruidosamente:

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 = `ENSAYO FALLÓ: "${label}" no encontrado - selector: ${typeof locator === 'string' ? locator : '(objeto localizador)'}`;
    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 visibles:\n  ' + found);
    return false;
  }
  console.log(`ENSAYO OK: "${label}"`);
  return true;
}

Estructura del script de ensayo

const steps = [
  { label: 'Campo de email de inicio de sesión', selector: '#email' },
  { label: 'Botón de inicio de sesión', selector: 'button[type="submit"]' },
  { label: 'Botón Nueva Solicitud', selector: 'button:has-text("Nueva Solicitud")' },
  { label: 'Selector de Código Presupuestario', selector: 'select' },
  { label: 'Fecha de entrega', selector: 'input[type="date"]:visible' },
  { label: 'Campo de descripción', selector: 'textarea:visible' },
  { label: 'Botón Agregar Artículo', selector: 'button:has-text("Agregar Artículo")' },
  { label: 'Botón 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('ENSAYO FALLÓ - corrige los selectores antes de grabar');
  process.exit(1);
}
console.log('ENSAYO PASÓ - todos los selectores verificados');

Cuando el ensayo falla

  1. Lee el volcado de elementos visibles.
  2. Encuentra el selector correcto.
  3. Actualiza el script.
  4. Vuelve a ejecutar el ensayo.
  5. Solo procede cuando todos los selectores pasen.

Fase 3: Grabar

Solo después de que la fase de descubrimiento y el ensayo pasen, debes crear la grabación.

Principios de Grabación

1. Flujo Narrativo

Planifica el video como una historia. Sigue el orden especificado por el usuario, o usa este orden por defecto:

  • Entrada: Inicio de sesión o navegación al punto de inicio
  • Contexto: Muestra el entorno para que los espectadores se orienten
  • Acción: Realiza los pasos principales del flujo de trabajo
  • Variación: Muestra una funcionalidad secundaria como configuración, tema o localización
  • Resultado: Muestra el resultado, confirmación o nuevo estado

2. Ritmo

  • Después del inicio de sesión: 4s
  • Después de la navegación: 3s
  • Después de hacer clic en un botón: 2s
  • Entre pasos principales: 1.5-2s
  • Después de la acción final: 3s
  • Retraso de escritura: 25-40ms por carácter

3. Superposición de Cursor

Inyecta un cursor de flecha SVG que sigue los movimientos del 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';
    });
  });
}

Llama a injectCursor(page) después de cada navegación a una página porque la superposición se destruye al navegar.

4. Movimiento del Mouse

Nunca teletransportes el cursor. Muévelo al objetivo antes de hacer clic:

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(`ADVERTENCIA: moveAndClick omitido - "${label}" no 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(`ADVERTENCIA: moveAndClick falló en "${label}": ${e.message}`);
    return false;
  }
  await page.waitForTimeout(postClickDelay);
  return true;
}

Cada llamada debe incluir una label descriptiva para depuración.

5. Escritura

Escribe visiblemente, no con llenado instantáneo:

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(`ADVERTENCIA: typeSlowly omitido - "${label}" no visible`);
    return false;
  }
  await moveAndClick(page, el, label);
  await el.fill('');
  await el.pressSequentially(text, { delay: charDelay });
  await page.waitForTimeout(500);
  return true;
}

6. Desplazamiento

Usa desplazamiento suave en lugar de saltos:

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

7. Recorrido de Paneles

Al mostrar un panel o página de resumen, mueve el cursor sobre los elementos clave:

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(`ADVERTENCIA: panElements omitió el elemento ${i} (selector: "${selector}"): ${e.message}`);
    }
  }
}

8. Subtítulos

Inyecta una barra de subtítulos en la parte inferior de la ventana:

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

Llama a injectSubtitleBar(page) junto con injectCursor(page) después de cada navegación.

Patrón de uso:

await showSubtitle(page, 'Paso 1 - Iniciando sesión');
await showSubtitle(page, 'Paso 2 - Resumen del panel');
await showSubtitle(page, '');

Directrices:

  • Mantén el texto del subtítulo corto, idealmente menos de 60 caracteres.
  • Usa el formato Paso N - Acción para consistencia.
  • Limpia el subtítulo durante pausas largas donde la UI puede hablar por sí misma.

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

// Pega aquí injectCursor, injectSubtitleBar, showSubtitle, moveAndClick,
// typeSlowly, ensureVisible y 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();
    // Navega por el flujo y ejecuta ensureVisible para cada 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, 'Paso 1 - Iniciando sesión');
    // acciones de inicio de sesión

    await page.goto(`${BASE_URL}/dashboard`);
    await injectCursor(page);
    await injectSubtitleBar(page);
    await showSubtitle(page, 'Paso 2 - Resumen del panel');
    // recorre el panel

    await showSubtitle(page, 'Paso 3 - Flujo de trabajo principal');
    // secuencia de acciones

    await showSubtitle(page, 'Paso 4 - Resultado');
    // revelación final
    await showSubtitle(page, '');
  } catch (err) {
    console.error('ERROR DEL DEMO:', 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 guardado:', dest);
      } catch (e) {
        console.error('ERROR: No se pudo copiar el video:', e.message);
        console.error('  Origen:', src);
        console.error('  Destino:', dest);
      }
    }
    await browser.close();
  }
})();

Uso:

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

# Fase 3: Grabar
node demo-script.cjs

Lista de Verificación Antes de Grabar

  • Fase de descubrimiento completada
  • Ensayo pasa con todos los selectores OK
  • Modo headless habilitado
  • Resolución configurada a 1280x720
  • Superposiciones de cursor y subtítulos reinyectadas después de cada navegación
  • showSubtitle(page, 'Paso N - ...') usado en transiciones principales
  • moveAndClick usado para todos los clics con etiquetas descriptivas
  • typeSlowly usado para entrada visible
  • Sin capturas silenciosas; usa try/catch con registro de advertencia
  • panElements usado para páginas de panel/resumen
  • Tiempos de espera post-acción aplicados (2-3s para pasos principales, 1-2s para clics, 4s después de navegación/inicio de sesión)

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.

Skills relacionados