Communitygithub.com

ui-demo

Enregistrez des vidéos de démonstration UI soignées avec Playwright. Idéal lorsque l'utilisateur demande une démo, une procédure pas à pas, un enregistrement d'écran ou un tutoriel d'application web. Produit des vidéos WebM avec curseur visible, rythme naturel et aspect professionnel.

Qu'est-ce que ui-demo ?

Cet outil, ou 'skill', guide l'utilisateur ou l'agent IA à travers un processus en trois phases (Découvrir, Répéter, Enregistrer) pour créer des vidéos de démonstration d'interface utilisateur professionnelles. Il est conçu pour des plateformes comme Claude, Cursor, Codex, et aide à automatiser l'enregistrement de vidéos de workflows web en utilisant Playwright. L'outil résout les problèmes courants comme les sélecteurs qui échouent silencieusement en forçant une phase de répétition. Il produit des vidéos WebM avec un curseur SVG superposé, un rythme naturel (avec des délais configurés pour les actions comme la navigation ou la frappe) et un flux narratif, incluant des sous-titres intégrés. En somme, il transforme une simple capture d'écran en une démonstration structurée, fiable et visuellement attrayante pour la documentation, l'intégration ou les présentations.

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

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Enregistreur de Vidéo de Démonstration UI

Enregistrez des vidéos de démonstration soignées d'applications web en utilisant l'enregistrement vidéo de Playwright avec un curseur superposé, un rythme naturel et un flux narratif.

Quand l'utiliser

  • L'utilisateur demande une "vidéo de démo", un "enregistrement d'écran", une "procédure pas à pas" ou un "tutoriel"
  • L'utilisateur souhaite présenter visuellement une fonctionnalité ou un workflow
  • L'utilisateur a besoin d'une vidéo pour la documentation, l'intégration ou une présentation aux parties prenantes

Processus en Trois Phases

Chaque démonstration passe par trois phases : Découvrir -> Répéter -> Enregistrer. Ne sautez jamais directement à l'enregistrement.


Phase 1 : Découvrir

Avant d'écrire un script, explorez les pages cibles pour comprendre ce qui s'y trouve réellement.

Pourquoi

Vous ne pouvez pas scripter ce que vous n'avez pas vu. Les champs peuvent être des <input> et non des <textarea>, les menus déroulants peuvent être des composants personnalisés et non des <select>, et les zones de commentaires peuvent prendre en charge les @mentions ou les #tags. Les suppositions brisent les enregistrements en silence.

Comment

Naviguez vers chaque page du flux et listez ses éléments interactifs :

// Exécutez ceci pour chaque page du flux AVANT d'écrire le script de démo
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));

Que rechercher

  • Champs de formulaire : Sont-ils des <select>, <input>, des menus déroulants personnalisés ou des combobox ?
  • Options de sélection : Listez les valeurs ET le texte des options. Les espaces réservés ont souvent value="0" ou value="" qui semble non vide. Utilisez Array.from(el.options).map(o => ({ value: o.value, text: o.text })). Ignorez les options dont le texte inclut "Sélectionner" ou dont la valeur est "0".
  • Texte enrichi : La zone de commentaire prend-elle en charge les @mentions, les #tags, le markdown ou les émojis ? Vérifiez le texte de l'espace réservé.
  • Champs obligatoires : Quels champs bloquent la soumission du formulaire ? Vérifiez required, * dans les étiquettes, et essayez de soumettre un formulaire vide pour voir les erreurs de validation.
  • Contenu dynamique : Des champs apparaissent-ils après que d'autres champs soient remplis ?
  • Étiquettes des boutons : Texte exact comme "Soumettre", "Soumettre la demande" ou "Envoyer".
  • En-têtes de colonnes de tableau : Pour les modaux pilotés par tableau, faites correspondre chaque input[type="number"] à son en-tête de colonne au lieu de supposer que toutes les entrées numériques signifient la même chose.

Résultat

Un plan de champ pour chaque page, utilisé pour écrire des sélecteurs corrects dans le script. Exemple :

/purchase-requests/new:
  - Budget Code: <select> (premier select sur la page, 4 options)
  - Desired Delivery: <input type="date">
  - Context: <textarea> (pas input)
  - BOM table: cellules modifiables en ligne avec span.cursor-pointer -> modèle d'input
  - Submit: <button> text="Soumettre"

/purchase-requests/N (détail):
  - Comment: <input placeholder="Tapez un message..."> prend en charge @user et #PR tags
  - Send: <button> text="Envoyer" (désactivé jusqu'à ce que l'input ait du contenu)

Phase 2 : Répéter

Exécutez toutes les étapes sans enregistrer. Vérifiez que chaque sélecteur se résout.

Pourquoi

Les échecs silencieux de sélecteurs sont la raison principale pour laquelle les enregistrements de démonstration échouent. La répétition les détecte avant de gaspiller un enregistrement.

Comment

Utilisez ensureVisible, un wrapper qui enregistre et échoue bruyamment :

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 = `REPETITION ÉCHOUÉE : "${label}" non trouvé - sélecteur : ${typeof locator === 'string' ? locator : '(objet 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('  Éléments visibles :\n  ' + found);
    return false;
  }
  console.log(`RÉPÉTITION OK : "${label}"`);
  return true;
}

Structure du script de répétition

const steps = [
  { label: 'Champ email de connexion', selector: '#email' },
  { label: 'Bouton de soumission de connexion', selector: 'button[type="submit"]' },
  { label: 'Bouton Nouvelle demande', selector: 'button:has-text("Nouvelle demande")' },
  { label: 'Select du code budgétaire', selector: 'select' },
  { label: 'Date de livraison', selector: 'input[type="date"]:visible' },
  { label: 'Champ de description', selector: 'textarea:visible' },
  { label: 'Bouton Ajouter un article', selector: 'button:has-text("Ajouter un article")' },
  { label: 'Bouton Soumettre', selector: 'button:has-text("Soumettre")' },
];

let allOk = true;
for (const step of steps) {
  if (!await ensureVisible(page, step.selector, step.label)) {
    allOk = false;
  }
}
if (!allOk) {
  console.error('RÉPÉTITION ÉCHOUÉE - corrigez les sélecteurs avant d\'enregistrer');
  process.exit(1);
}
console.log('RÉPÉTITION RÉUSSIE - tous les sélecteurs vérifiés');

En cas d'échec de la répétition

  1. Lisez la liste des éléments visibles.
  2. Trouvez le sélecteur correct.
  3. Mettez à jour le script.
  4. Relancez la répétition.
  5. Ne procédez que lorsque tous les sélecteurs passent.

Phase 3 : Enregistrer

Ne créez l'enregistrement qu'après que la phase de découverte et de répétition soit réussie.

Principes d'enregistrement

1. Flux narratif

Planifiez la vidéo comme une histoire. Suivez l'ordre spécifié par l'utilisateur, ou utilisez celui par défaut :

  • Entrée : Connexion ou navigation vers le point de départ
  • Contexte : Panoramique des environs pour que les spectateurs s'orientent
  • Action : Effectuez les étapes principales du workflow
  • Variation : Montrez une fonctionnalité secondaire comme les paramètres, le thème ou la localisation
  • Résultat : Montrez le résultat, la confirmation ou le nouvel état

2. Rythme

  • Après la connexion : 4s
  • Après la navigation : 3s
  • Après avoir cliqué sur un bouton : 2s
  • Entre les étapes principales : 1.5-2s
  • Après l'action finale : 3s
  • Délai de frappe : 25-40ms par caractère

3. Superposition du curseur

Injectez un curseur fléché SVG qui suit les mouvements de la souris :

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

Appelez injectCursor(page) après chaque navigation car la superposition est détruite lors de la navigation.

4. Mouvement de la souris

Ne téléportez jamais le curseur. Déplacez-le vers la cible avant de cliquer :

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(`AVERTISSEMENT : moveAndClick ignoré - "${label}" non 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(`AVERTISSEMENT : moveAndClick a échoué sur "${label}" : ${e.message}`);
    return false;
  }
  await page.waitForTimeout(postClickDelay);
  return true;
}

Chaque appel doit inclure un label descriptif pour le débogage.

5. Saisie

Tapez visiblement, pas un remplissage instantané :

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

6. Défilement

Utilisez un défilement fluide au lieu de sauts :

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

7. Panoramique du tableau de bord

Lorsque vous montrez un tableau de bord ou une page de présentation, déplacez le curseur sur les éléments clés :

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(`AVERTISSEMENT : panElements a ignoré l'élément ${i} (sélecteur : "${selector}") : ${e.message}`);
    }
  }
}

8. Sous-titres

Injectez une barre de sous-titres en bas de la fenêtre d'affichage :

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

Appelez injectSubtitleBar(page) avec injectCursor(page) après chaque navigation.

Modèle d'utilisation :

await showSubtitle(page, 'Étape 1 - Connexion');
await showSubtitle(page, 'Étape 2 - Aperçu du tableau de bord');
await showSubtitle(page, '');

Directives :

  • Gardez le texte du sous-titre court, idéalement moins de 60 caractères.
  • Utilisez le format Étape N - Action pour la cohérence.
  • Effacez le sous-titre pendant les longues pauses où l'interface utilisateur peut parler d'elle-même.

Modèle 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');

// Collez injectCursor, injectSubtitleBar, showSubtitle, moveAndClick,
// typeSlowly, ensureVisible, et panElements ici.

(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();
    // Naviguez à travers le flux et exécutez ensureVisible pour chaque sélecteur.
    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, 'Étape 1 - Connexion');
    // actions de connexion

    await page.goto(`${BASE_URL}/dashboard`);
    await injectCursor(page);
    await injectSubtitleBar(page);
    await showSubtitle(page, 'Étape 2 - Aperçu du tableau de bord');
    // panoramique du tableau de bord

    await showSubtitle(page, 'Étape 3 - Workflow principal');
    // séquence d'actions

    await showSubtitle(page, 'Étape 4 - Résultat');
    // révélation finale
    await showSubtitle(page, '');
  } catch (err) {
    console.error('ERREUR DE DÉMO :', 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('Vidéo sauvegardée :', dest);
      } catch (e) {
        console.error('ERREUR : Échec de la copie de la vidéo :', e.message);
        console.error('  Source :', src);
        console.error('  Destination :', dest);
      }
    }
    await browser.close();
  }
})();

Utilisation :

# Phase 2 : Répéter
node demo-script.cjs --rehearse

# Phase 3 : Enregistrer
node demo-script.cjs

Liste de Vérification Avant l'Enregistrement

  • Phase de découverte terminée
  • La répétition réussit avec tous les sélecteurs OK
  • Mode headless activé
  • Résolution réglée sur 1280x720
  • Superpositions du curseur et des sous-titres réinjectées après chaque navigation
  • showSubtitle(page, 'Étape N - ...') utilisé aux transitions majeures
  • moveAndClick utilisé pour tous les clics avec des étiquettes descriptives
  • typeSlowly utilisé pour la saisie visible
  • Pas de captures silencieuses ;

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 associés