116 KiB
¡Excelente! Basándome en el análisis UX detallado del video, aquí te presento una propuesta de código para HTML5, CSS3 y JavaScript que busca replicar los principios de diseño y experiencia de usuario observados en la interfaz de StreamYard.
Nos enfocaremos en:
- Modularidad HTML: Estructura clara y semántica.
- Variables CSS: Para mantener la consistencia en estilos y facilitar cambios.
- Flexbox/Grid: Para layouts complejos y responsivos.
- JavaScript: Para gestionar el estado de la UI, proporcionar feedback instantáneo, manejar modales, tabs y simular el drag-and-drop.
1. HTML5 (index.html)
Esta estructura HTML refleja los principales paneles y componentes, con atributos data- para facilitar la interacción con JavaScript.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>StreamYard UI - Propuesta UX</title>
<link rel="stylesheet" href="style.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
</head>
<body>
<div class="app-container">
<!-- Top Header Bar -->
<header class="top-header">
<div class="header-left">
<span class="app-title">Transmisión</span>
<span class="beta-tag">BETA</span>
</div>
<div class="header-right">
<button class="btn btn-secondary" data-action="edit-broadcast">Editor</button>
<div class="user-avatar">
<img src="https://via.placeholder.com/30" alt="User">
<i class="fas fa-caret-down"></i>
</div>
<button class="btn btn-primary live-button">
<i class="fas fa-video"></i> Transmitir en vivo
</button>
</div>
</header>
<!-- Main Content Area -->
<main class="main-content">
<!-- Left Sidebar - Scenes -->
<aside class="sidebar-left">
<div class="sidebar-section">
<h3>Escenas <span class="beta-tag">BETA</span></h3>
</div>
<div class="sidebar-section scenes-list" id="scenes-list">
<h4>Mis Escenas</h4>
<!-- Scene items will be rendered here by JS -->
<div class="scene-item active" data-scene-id="1" draggable="true">
<img src="https://via.placeholder.com/120x67/333333/ffffff?text=Scene+1" alt="Scene Thumbnail">
<span class="scene-name">Demo scene 1</span>
<div class="scene-options">
<i class="fas fa-ellipsis-v"></i>
<div class="dropdown-menu">
<button data-action="show-on-stage">Mostrar en el escenario</button>
<button>Renombrar</button>
<button class="danger">Eliminar</button>
</div>
</div>
</div>
<div class="scene-item" data-scene-id="2" draggable="true">
<img src="https://via.placeholder.com/120x67/555555/ffffff?text=Scene+2" alt="Scene Thumbnail">
<span class="scene-name">Demo scene 2</span>
<div class="scene-options">
<i class="fas fa-ellipsis-v"></i>
<div class="dropdown-menu">
<button data-action="show-on-stage">Mostrar en el escenario</button>
<button>Renombrar</button>
<button class="danger">Eliminar</button>
</div>
</div>
</div>
<button class="btn btn-add-scene" data-action="add-scene">
<i class="fas fa-plus"></i> Nueva escena
</button>
</div>
<div class="sidebar-section actions-list">
<div class="action-item" data-action="intro-video">
<i class="fas fa-play-circle"></i> Establecer video de introducción
</div>
<div class="action-item" data-action="outro-video">
<i class="fas fa-stop-circle"></i> Establecer video de cierre
</div>
</div>
</aside>
<!-- Central Live Area -->
<section class="live-area">
<div class="video-player">
<div class="video-overlay">
<span class="quality">720p</span>
<span class="streamyard-logo">Producido con <img src="https://via.placeholder.com/80x20/ffffff/000000?text=StreamYard" alt="StreamYard"></span>
</div>
<img id="live-video-stream" src="https://via.placeholder.com/800x450/1e2430/ffffff?text=Vista+Previa+de+la+Transmision" alt="Live Video">
<!-- Dynamic elements like logo, overlay, background will be rendered here by JS -->
<img id="stage-overlay" class="stage-element" src="" alt="Superposición" style="display: none;">
<img id="stage-logo" class="stage-element" src="" alt="Logo" style="display: none;">
<div id="stage-background" class="stage-element" style="display: none;"></div>
<div id="participant-feed" class="participant-feed">
<!-- Participants will be dynamically added here -->
<div class="participant-name-tag" id="main-participant-tag" style="display: none;">Cesar Mendivil</div>
</div>
</div>
<div class="participants-controls">
<div class="participants-grid" id="participants-grid">
<div class="participant-card active" data-participant-id="cesar">
<img src="https://via.placeholder.com/60/777777/ffffff?text=CM" alt="Cesar Mendivil">
<span>Cesar Mendivil</span>
</div>
<div class="participant-card add-card" data-action="add-participant">
<i class="fas fa-plus"></i>
<span>Invitar</span>
</div>
</div>
<div class="interaction-buttons">
<button class="btn-icon active" data-mic-toggle data-tooltip="Silenciar/Activar micrófono"><i class="fas fa-microphone"></i></button>
<button class="btn-icon active" data-cam-toggle data-tooltip="Encender/Apagar cámara"><i class="fas fa-video"></i></button>
<button class="btn-icon" data-action="share-screen" data-tooltip="Compartir pantalla"><i class="fas fa-desktop"></i></button>
<button class="btn-icon" data-action="open-settings" data-tooltip="Configuración"><i class="fas fa-cog"></i></button>
<button class="btn-presentar" data-action="present-or-invite">Presentar o invitar</button>
</div>
</div>
<div class="bottom-controls">
<button class="btn-icon" data-mic-toggle data-tooltip="Silenciar/Activar micrófono"><i class="fas fa-microphone"></i></button>
<button class="btn-icon" data-cam-toggle data-tooltip="Encender/Apagar cámara"><i class="fas fa-video"></i></button>
<button class="btn-icon" data-action="share-screen" data-tooltip="Compartir pantalla"><i class="fas fa-desktop"></i></sbutton>
<button class="btn-icon" data-action="open-settings" data-tooltip="Configuración"><i class="fas fa-cog"></i></button>
<button class="btn-icon" data-action="chat" data-tooltip="Chat del estudio"><i class="fas fa-comment-dots"></i></button>
<button class="btn-icon" data-action="help" data-tooltip="Ayuda"><i class="fas fa-question-circle"></i> ¿Necesitas ayuda?</button>
<button class="btn btn-danger leave-button" data-action="leave-studio" data-tooltip="Abandonar estudio"><i class="fas fa-phone-slash"></i></button>
</div>
</section>
<!-- Right Sidebar - Settings -->
<aside class="sidebar-right">
<div class="brand-header">
<select class="dropdown-select" data-setting="brand-select">
<option value="brand1">Marca 1</option>
<option value="brand2">Marca 2</option>
</select>
<div class="brand-actions">
<i class="fas fa-pen" data-tooltip="Editar marca"></i>
<i class="fas fa-ellipsis-v" data-tooltip="Más opciones"></i>
</div>
</div>
<div class="card settings-card">
<h4>Pack de Halloween <span class="new-tag">NUEVO</span></h4>
<p>6 overlays, 9 fondos</p>
<button class="btn btn-secondary">Añadir</button>
<i class="fas fa-trash-alt icon-action" data-tooltip="Eliminar pack"></i>
</div>
<div class="sidebar-tabs">
<div class="tab-item active" data-tab="estilo"><i class="fas fa-palette"></i> Estilo</div>
<div class="tab-item" data-tab="multimedia"><i class="fas fa-photo-video"></i> Activos multimedia</div>
</div>
<!-- Style Tab Content -->
<div class="tab-content active" id="estilo">
<div class="sidebar-section">
<h4>Ajustes preestablecidos <i class="fas fa-question-circle icon-help" data-tooltip="Selecciona un preset de estilo para la transmisión"></i></h4>
<div class="presets-grid" data-setting="presets">
<div class="preset-item active" data-preset="preset1"></div>
<div class="preset-item" data-preset="preset2"></div>
<div class="preset-item" data-preset="preset3"></div>
</div>
</div>
<div class="sidebar-section">
<h4>Color de la marca <i class="fas fa-question-circle icon-help" data-tooltip="Define el color principal de los elementos de tu marca"></i></h4>
<div class="color-picker">
<input type="color" value="#ff0040" class="color-input" data-setting="brand-color">
<span id="brand-color-value">#ff0040</span>
</div>
</div>
<div class="sidebar-section">
<h4>Tema <i class="fas fa-question-circle icon-help" data-tooltip="Elige el estilo visual de los elementos en pantalla"></i></h4>
<div class="theme-buttons" data-setting="theme">
<button class="btn-theme" data-theme="bubble">Bubble</button>
<button class="btn-theme" data-theme="classic">Classic</button>
<button class="btn-theme" data-theme="minimal">Minimal</button>
<button class="btn-theme active" data-theme="block">Block</button>
</div>
</div>
<div class="sidebar-section">
<div class="toggle-switch-container">
<span>Mostrar nombres</span>
<label class="switch">
<input type="checkbox" checked data-setting="show-names">
<span class="slider round"></span>
</label>
</div>
<div class="toggle-switch-container">
<span>Mostrar títulos</span>
<label class="switch">
<input type="checkbox" data-setting="show-titles">
<span class="slider round"></span>
</label>
</div>
</div>
</div>
<!-- Multimedia Tab Content -->
<div class="tab-content" id="multimedia">
<div class="sidebar-section">
<h4>Logo <i class="fas fa-question-circle icon-help" data-tooltip="Añade el logo de tu marca a la transmisión"></i></h4>
<div class="logo-uploader">
<div class="logo-preview" id="logo-preview">
<img src="https://via.placeholder.com/50/aaaaaa/ffffff?text=Logo" alt="Logo">
</div>
<button class="btn btn-secondary" data-action="upload-logo"><i class="fas fa-plus"></i></button>
<input type="file" id="logo-file-input" accept="image/*" style="display: none;">
</div>
<div class="logo-position" data-setting="logo-position">
<button class="btn-icon active" data-position="top-left" data-tooltip="Logo arriba izquierda"></button>
<button class="btn-icon" data-position="top-right" data-tooltip="Logo arriba derecha"></button>
</div>
</div>
<div class="sidebar-section">
<h4>Superposición <i class="fas fa-question-circle icon-help" data-tooltip="Añade gráficos de superposición a tu transmisión"></i></h4>
<div class="overlay-grid" data-setting="overlay">
<div class="overlay-item active" data-overlay="none" data-tooltip="Ninguna"></div>
<div class="overlay-item" data-overlay="streamyard-live" data-img="https://via.placeholder.com/100x56/ff0040/ffffff?text=EN+VIVO" data-tooltip="En Vivo con StreamYard"></div>
<div class="overlay-item add-more" data-action="open-overlay-modal"><i class="fas fa-plus"></i> Más</div>
</div>
</div>
<div class="sidebar-section">
<h4>Código QR <i class="fas fa-question-circle icon-help" data-tooltip="Genera un código QR para compartir enlaces"></i></h4>
<div id="qr-code-generator">
<button class="btn btn-secondary" data-action="add-qr-code"><i class="fas fa-plus"></i> Nuevo código QR</button>
<div class="qr-code-form" style="display: none;">
<input type="text" placeholder="Título" data-qr-title>
<input type="text" placeholder="https://website.com" data-qr-url>
<button class="btn btn-primary" data-action="create-qr">Crear</button>
<button class="btn btn-secondary" data-action="cancel-qr">Cancelar</button>
</div>
</div>
</div>
<div class="sidebar-section">
<h4>Clips de video <i class="fas fa-question-circle icon-help" data-tooltip="Videos de introducción, conclusión o cuenta regresiva"></i></h4>
<div class="video-clips-grid">
<div class="video-clip-item" data-action="intro-video-clip" data-tooltip="Video de introducción">
<i class="fas fa-plus"></i> Video de introducción
<span class="premium-badge"></span>
</div>
<div class="video-clip-item" data-action="outro-video-clip" data-tooltip="Video de conclusión">
<i class="fas fa-plus"></i> Video de conclusión
<span class="premium-badge"></span>
</div>
</div>
<div class="toggle-switch-container">
<span>Bucle</span>
<label class="switch">
<input type="checkbox" data-setting="video-loop">
<span class="slider round"></span>
</label>
</div>
<div class="countdown-options">
<div class="countdown-item active" data-countdown="30">30</div>
<div class="countdown-item" data-countdown="60">60</div>
<div class="countdown-item add-more" data-action="add-custom-countdown"><i class="fas fa-plus"></i> Más</div>
</div>
</div>
<div class="sidebar-section">
<h4>Fondo <i class="fas fa-question-circle icon-help" data-tooltip="Elige un fondo para tu transmisión"></i></h4>
<div class="background-grid" data-setting="background-video">
<div class="background-item active" data-background="none" data-tooltip="Ninguno"></div>
<div class="background-item" data-background="dark-bubbles" data-img="https://via.placeholder.com/100x56/111111/ffffff?text=Dark" data-tooltip="Burbujas oscuras"></div>
<div class="background-item" data-background="blue-waves" data-img="https://via.placeholder.com/100x56/007bff/ffffff?text=Waves" data-tooltip="Olas azules"></div>
<div class="background-item add-more" data-action="open-background-modal"><i class="fas fa-plus"></i> Más</div>
</div>
</div>
<div class="sidebar-section">
<h4>Sonidos <i class="fas fa-question-circle icon-help" data-tooltip="Añade efectos de sonido a tu transmisión"></i></h4>
<div class="sound-volume-control">
<i class="fas fa-volume-up"></i>
<input type="range" min="0" max="100" value="70" class="slider-volume" data-setting="sound-volume">
<i class="fas fa-redo-alt" data-tooltip="Reestablecer volumen"></i>
</div>
<div class="sound-effects-grid" data-setting="sound-effects">
<button class="btn-theme" data-sound="applause">Aplausos Intensos</button>
<button class="btn-theme" data-sound="rebound">Rebote</button>
<button class="btn-theme" data-sound="heartbeat">Latido Rápido</button>
<button class="btn-theme" data-sound="bongos">Redoble de Bongos</button>
<button class="btn-theme" data-sound="gong">Gong</button>
<button class="btn-theme add-more" data-action="more-sounds"><i class="fas fa-plus"></i> Más sonidos</button>
</div>
</div>
<div class="sidebar-section">
<h4>Música de fondo <i class="fas fa-question-circle icon-help" data-tooltip="Elige música de fondo para tu transmisión"></i></h4>
<div class="sound-volume-control">
<i class="fas fa-volume-up"></i>
<input type="range" min="0" max="100" value="50" class="slider-volume" data-setting="music-volume">
<i class="fas fa-redo-alt" data-tooltip="Reestablecer volumen"></i>
</div>
<div class="music-effects-grid" data-setting="music-effects">
<button class="btn-theme">Acoustic cinematic</button>
<button class="btn-theme">Dance pop</button>
<button class="btn-theme">Soñar despierto</button>
<button class="btn-theme">Alimientand o a los...</button>
<button class="btn-theme">En el espacio</button>
<button class="btn-theme">Lofi</button>
<button class="btn-theme">Manejo de noche</button>
<button class="btn-theme">Rock</button>
<button class="btn-theme add-more" data-action="more-music"><i class="fas fa-plus"></i> Más música</button>
</div>
</div>
</div>
</aside>
<!-- Far Right Vertical Bar -->
<aside class="sidebar-far-right">
<div class="far-right-item active" data-panel="comentarios" data-tooltip="Ver comentarios de espectadores"><i class="fas fa-comment-alt"></i> Comentarios</div>
<div class="far-right-item" data-panel="banners" data-tooltip="Gestionar banners de texto"><i class="fas fa-ad"></i> Banners</div>
<div class="far-right-item" data-panel="activos-multimedia" data-tooltip="Activos multimedia"><i class="fas fa-photo-video"></i> Activos multimedia</div>
<div class="far-right-item" data-panel="estilo" data-tooltip="Ajustar estilos visuales"><i class="fas fa-palette"></i> Estilo</div>
<div class="far-right-item" data-panel="notas" data-tooltip="Tomar notas durante la transmisión"><i class="fas fa-sticky-note"></i> Notas</div>
<div class="far-right-item" data-panel="personas" data-tooltip="Gestionar participantes"><i class="fas fa-users"></i> Personas</div>
<div class="far-right-item" data-panel="chat-privado" data-tooltip="Chat privado del estudio"><i class="fas fa-comments"></i> Chat privado</div>
</aside>
</main>
</div>
<!-- Modals -->
<div class="modal-backdrop" id="modal-backdrop"></div>
<!-- Configuration Modal (as seen in video) -->
<div class="modal" id="config-modal">
<div class="modal-header">
<h3>Configuración</h3>
<button class="close-modal" data-action="close-modal"><i class="fas fa-times"></i></button>
</div>
<div class="modal-content-flex">
<nav class="modal-nav">
<div class="modal-nav-item active" data-config-tab="general"><i class="fas fa-cog"></i> General</div>
<div class="modal-nav-item" data-config-tab="camera"><i class="fas fa-video"></i> Cámara</div>
<div class="modal-nav-item" data-config-tab="audio"><i class="fas fa-microphone"></i> Audio</div>
<div class="modal-nav-item" data-config-tab="visual-effects"><i class="fas fa-star"></i> Efectos visuales</div>
<div class="modal-nav-item" data-config-tab="recording"><i class="fas fa-record-vinyl"></i> Grabación</div>
<div class="modal-nav-item" data-config-tab="shortcuts"><i class="fas fa-keyboard"></i> Teclas de acceso rápido</div>
<div class="modal-nav-item" data-config-tab="designs"><i class="fas fa-th-large"></i> Diseños</div>
<div class="modal-nav-item" data-config-tab="guests"><i class="fas fa-users"></i> Invitados</div>
</nav>
<div class="modal-settings-content">
<!-- General Tab -->
<div class="config-tab-content active" id="general">
<h4>General</h4>
<div class="setting-row">
<span>Resolución</span>
<select>
<option>Alta definición (720p)</option>
<option>Full HD (1080p) - Premium</option>
</select>
</div>
<div class="setting-row">
<label class="checkbox-container">
<input type="checkbox" checked>
<span class="checkmark"></span> Mostrar insignia de resolución en el escenario
</label>
<i class="fas fa-question-circle icon-help" data-tooltip="Muestra una pequeña etiqueta con la resolución de la transmisión."></i>
</div>
<div class="setting-row">
<span>Orientación</span>
<label class="radio-container">
<input type="radio" name="orientation" value="horizontal" checked>
<span class="radiomark"></span> Posición horizontal
</label>
<label class="radio-container">
<input type="radio" name="orientation" value="vertical">
<span class="radiomark"></span> Posición vertical
</label>
</div>
<div class="setting-row">
<label class="checkbox-container">
<input type="checkbox" checked>
<span class="checkmark"></span> Mostrar mensajes informativos en el escenario
</label>
</div>
<div class="setting-row">
<label class="checkbox-container">
<input type="checkbox" checked>
<span class="checkmark"></span> Mover los videos hacia arriba para que se vean los comentarios/banners
</label>
</div>
<div class="setting-row">
<label class="checkbox-container">
<input type="checkbox" checked>
<span class="checkmark"></span> Avatares de audio
</label>
</div>
<div class="setting-row">
<label class="checkbox-container">
<input type="checkbox" checked>
<span class="checkmark"></span> Agregar automáticamente medios presentados al escenario
</label>
</div>
<div class="setting-row">
<span>Apariencia</span>
<label class="radio-container">
<input type="radio" name="appearance" value="auto" checked>
<span class="radiomark"></span> Auto
</label>
<label class="radio-container">
<input type="radio" name="appearance" value="light">
<span class="radiomark"></span> Claro
</label>
<label class="radio-container">
<input type="radio" name="appearance" value="dark">
<span class="radiomark"></span> Oscuro
</label>
</div>
</div>
<!-- Camera Tab -->
<div class="config-tab-content" id="camera">
<h4>Cámara</h4>
<div class="setting-row-img">
<img src="https://via.placeholder.com/200x112/555555/ffffff?text=Cámara" alt="Cámara">
<div class="camera-settings">
<div class="setting-row">
<span>Cámara</span>
<select>
<option>UVC Camera (tefe432i)</option>
<option>Webcam integrada</option>
</select>
</div>
<div class="setting-row">
<span>Resolución de la cámara</span>
<select>
<option>Definición estándar (480p)</option>
<option>Alta definición (720p)</option>
</select>
</div>
</div>
</div>
<div class="setting-row">
<label class="checkbox-container">
<input type="checkbox" checked>
<span class="checkmark"></span> Reflejar mi cámara
</label>
<i class="fas fa-question-circle icon-help" data-tooltip="Invierte la imagen de tu cámara para que te veas como en un espejo."></i>
</div>
<p class="small-text">Atención, si tienes una superposición que cubre gran parte de la pantalla, es posible que te cubras a ti mismo. <a href="#">Más información</a></p>
<div class="setting-row">
<label class="checkbox-container">
<input type="checkbox">
<span class="checkmark"></span> (Avanzado) Modo de firewall restrictivo. Por favor, lea <a href="#">estas instrucciones</a> primero.
</label>
</div>
<p class="small-text-muted">Esta configuración no se puede cambiar mientras esté en el estudio.</p>
</div>
<!-- Audio Tab -->
<div class="config-tab-content" id="audio">
<h4>Audio</h4>
<div class="setting-row">
<span>Micrófono</span>
<select>
<option>Microphone Array (Realtek(R) Audio)</option>
<option>Auriculares</option>
</select>
<span class="audio-level"></span>
</div>
<div class="setting-row">
<span>Altavoz</span>
<select>
<option>Default - Headphones (Swiss Code Sec)</option>
<option>Altavoces internos</option>
</select>
<button class="btn btn-secondary">Prueba</button>
</div>
<div class="setting-row">
<label class="checkbox-container">
<input type="checkbox">
<span class="checkmark"></span> Cancelación de eco
</label>
</div>
<div class="setting-row">
<label class="checkbox-container">
<input type="checkbox">
<span class="checkmark"></span> Reducir ruido de fondo del micrófono
</label>
</div>
<div class="setting-row">
<label class="checkbox-container">
<input type="checkbox">
<span class="checkmark"></span> Audio estéreo
</label>
</div>
<p class="small-text-muted indent">La cancelación de eco debe estar desactivada para usar audio estéreo</p>
<div class="setting-row">
<label class="checkbox-container">
<input type="checkbox" checked>
<span class="checkmark"></span> Ajustar automáticamente el volumen del micrófono
</label>
<i class="fas fa-question-circle icon-help" data-tooltip="Ajusta el volumen del micrófono para que se escuche de manera óptima."></i>
</div>
</div>
<!-- Visual Effects Tab -->
<div class="config-tab-content" id="visual-effects">
<h4>Efectos visuales</h4>
<div class="alert-box">
<i class="fas fa-exclamation-triangle"></i>
<p>Nota: La función de pantalla verde utiliza un poco más de recursos informáticos que otras funciones. Cuanto más potente sea tu computadora, mejor funcionará.</p>
<button class="btn btn-primary">Entendido, ¡vamos a probarlo!</button>
</div>
</div>
<!-- Recording Tab -->
<div class="config-tab-content" id="recording">
<h4>Grabación</h4>
<div class="setting-row">
<span>Grabación local</span>
<label class="checkbox-container">
<input type="checkbox">
<span class="checkmark"></span> Grabaciones de audio y video individuales de alta calidad para cada invitado. <a href="#">Más información</a>
</label>
</div>
<div class="setting-row">
<label class="checkbox-container">
<input type="checkbox">
<span class="checkmark"></span> Grabar de manera local para cada participante
</label>
</div>
<div class="info-box">
<i class="fas fa-info-circle"></i>
<p>Con el plan Gratuito, las transmisiones en vivo no se graban en StreamYard, pero sí se graban en las redes sociales. <a href="#">Pásate a un plan superior</a> para grabar tus transmisiones en vivo o crear una grabación.</p>
</div>
</div>
<!-- Shortcuts Tab -->
<div class="config-tab-content" id="shortcuts">
<h4>Teclas de acceso rápido</h4>
<div class="shortcuts-grid">
<div class="shortcut-item">
<span>Silenciar/activar el micrófono</span>
<input type="text" value="CTRL+D" readonly>
<i class="fas fa-times" data-tooltip="Eliminar atajo"></i>
</div>
<div class="shortcut-item">
<span>Encendido/apagado de la cámara</span>
<input type="text" value="CTRL+E" readonly>
<i class="fas fa-times" data-tooltip="Eliminar atajo"></i>
</div>
<div class="shortcut-item">
<span>Compartir pantalla</span>
<input type="text" value="SHIFT+S" readonly>
<i class="fas fa-times" data-tooltip="Eliminar atajo"></i>
</div>
<div class="shortcut-item">
<span>Compartir video</span>
<input type="text" value="SHIFT+V" readonly>
<i class="fas fa-times" data-tooltip="Eliminar atajo"></i>
</div>
<div class="shortcut-item">
<span>Compartir imagen</span>
<input type="text" value="SHIFT+I" readonly>
<i class="fas fa-times" data-tooltip="Eliminar atajo"></i>
</div>
<div class="shortcut-item">
<span>Reproducir/pausar video compartido</span>
<input type="text" value="No establecido" readonly>
<i class="fas fa-times" data-tooltip="Eliminar atajo"></i>
</div>
<div class="shortcut-item">
<span>Compartir la segunda cámara</span>
<input type="text" value="No establecido" readonly>
<i class="fas fa-times" data-tooltip="Eliminar atajo"></i>
</div>
<div class="shortcut-item">
<span>Siguiente diapositiva</span>
<input type="text" value="RIGHT" readonly>
<i class="fas fa-times" data-tooltip="Eliminar atajo"></i>
</div>
<div class="shortcut-item">
<span>Diapositiva anterior</span>
<input type="text" value="LEFT" readonly>
<i class="fas fa-times" data-tooltip="Eliminar atajo"></i>
</div>
</div>
</div>
<!-- Designs Tab -->
<div class="config-tab-content" id="designs">
<h4>Diseños</h4>
<div class="setting-row">
<span>Selecciona qué diseños te gustaría usar</span>
</div>
<div class="design-checkbox-grid">
<label class="checkbox-container design-checkbox">
<input type="checkbox" checked>
<span class="checkmark"></span>
<i class="fas fa-user-circle"></i> Diseño recortado
</label>
<label class="checkbox-container design-checkbox">
<input type="checkbox" checked>
<span class="checkmark"></span>
<i class="fas fa-users"></i> Diseño de grupo
</label>
<label class="checkbox-container design-checkbox">
<input type="checkbox" checked>
<span class="checkmark"></span>
<i class="fas fa-user"></i> Diseño de los focos
</label>
<label class="checkbox-container design-checkbox">
<input type="checkbox" checked>
<span class="checkmark"></span>
<i class="fas fa-newspaper"></i> Diseño de noticias
</label>
<label class="checkbox-container design-checkbox">
<input type="checkbox" checked>
<span class="checkmark"></span>
<i class="fas fa-tv"></i> Diseño de pantalla
</label>
<label class="checkbox-container design-checkbox">
<input type="checkbox" checked>
<span class="checkmark"></span>
<i class="fas fa-images"></i> Diseño de imagen en imagen
</label>
<label class="checkbox-container design-checkbox">
<input type="checkbox" checked>
<span class="checkmark"></span>
<i class="fas fa-film"></i> Diseño de cine
</label>
<label class="checkbox-container design-checkbox">
<input type="checkbox" checked>
<span class="checkmark"></span>
<i class="fas fa-user"></i> Individual Central
</label>
</div>
</div>
<!-- Guests Tab -->
<div class="config-tab-content" id="guests">
<h4>Invitados</h4>
<div class="setting-row">
<label class="checkbox-container">
<input type="checkbox" checked>
<span class="checkmark"></span> Los invitados pueden transmitir esto a sus propios destinos
</label>
<i class="fas fa-question-circle icon-help" data-tooltip="Permite a tus invitados retransmitir tu estudio en sus propias plataformas."></i>
</div>
<div class="setting-row">
<label class="checkbox-container">
<input type="checkbox" checked>
<span class="checkmark"></span> Reproducir un sonido cuando los invitados entren
</label>
</div>
<div class="setting-row">
<label class="checkbox-container">
<input type="checkbox">
<span class="checkmark"></span> Los invitados deben autenticarse
</label>
</div>
<div class="setting-row">
<span>Invitados vetados</span>
<textarea placeholder="No hay invitados vetados"></textarea>
</div>
<div class="setting-row">
<label class="checkbox-container">
<input type="checkbox">
<span class="checkmark"></span> Sala verde
</label>
<i class="fas fa-question-circle icon-help" data-tooltip="Una sala de espera privada para tus invitados antes de salir en directo."></i>
</div>
<div class="info-box">
<i class="fas fa-info-circle"></i>
<p>Pásate al plan Teams o al plan de Negocio para tener acceso a la sala verde y asegurarte de que tus invitados tengan todo listo para la transmisión en vivo.</p>
</div>
</div>
</div>
</div>
</div>
<!-- Background/Overlay Selection Modal -->
<div class="modal" id="media-selection-modal">
<div class="modal-header">
<h3 id="media-modal-title">Fondo</h3>
<button class="close-modal" data-action="close-modal"><i class="fas fa-times"></i></button>
</div>
<div class="modal-content">
<div class="upload-area">
<i class="fas fa-cloud-upload-alt"></i>
<p>Arrastra y suelta archivos y videos para subir</p>
<button class="btn btn-secondary">Agregar archivos</button>
<p class="small-text-muted">Tamaño recomendado: 1280 x 720 | Tamaño máximo del archivo de video: 200 MB</p>
<p class="small-text-muted">¿No quieres agregar uno propio? ¡Prueba uno de estos!</p>
</div>
<div class="media-tabs">
<button class="media-tab-item active" data-media-type="images">Imágenes</button>
<button class="media-tab-item" data-media-type="videos">Videos</button>
</div>
<div class="media-grid" id="media-grid">
<!-- Media items will be rendered here by JS -->
<div class="media-item" data-media-src="https://via.placeholder.com/100x56/007bff/ffffff?text=Blue+Waves" data-media-type="image">
<img src="https://via.placeholder.com/100x56/007bff/ffffff?text=Blue+Waves" alt="Olas Azules">
<span>Olas Azules</span>
</div>
<div class="media-item" data-media-src="https://via.placeholder.com/100x56/333333/ffffff?text=Dark+Mtns" data-media-type="image">
<img src="https://via.placeholder.com/100x56/333333/ffffff?text=Dark+Mtns" alt="Montañas Negras">
<span>Montañas Negras</span>
</div>
<div class="media-item" data-media-src="https://via.placeholder.com/100x56/a055ff/ffffff?text=Purple+Waves" data-media-type="image">
<img src="https://via.placeholder.com/100x56/a055ff/ffffff?text=Purple+Waves" alt="Olas Moradas">
<span>Olas Moradas</span>
</div>
<div class="media-item" data-media-src="https://via.placeholder.com/100x56/00ffff/ffffff?text=Cyan+Sky" data-media-type="image">
<img src="https://via.placeholder.com/100x56/00ffff/ffffff?text=Cyan+Sky" alt="Cielo Cian">
<span>Cielo Cian</span>
</div>
<div class="media-item" data-media-src="https://via.placeholder.com/100x56/00aaff/ffffff?text=Blue+Pattern" data-media-type="image">
<img src="https://via.placeholder.com/100x56/00aaff/ffffff?text=Blue+Pattern" alt="Olas Cian">
<span>Olas Cian</span>
</div>
<div class="media-item" data-media-src="https://via.placeholder.com/100x56/ff00cc/ffffff?text=Gradient" data-media-type="image">
<img src="https://via.placeholder.com/100x56/ff00cc/ffffff?text=Gradient" alt="Gradiente Azul Rosa">
<span>Gradiente Azul Rosa</span>
</div>
<!-- Example video media items -->
<div class="media-item" data-media-src="https://via.placeholder.com/100x56/0088ff/ffffff?text=Ocean+Blue" data-media-type="video">
<img src="https://via.placeholder.com/100x56/0088ff/ffffff?text=Ocean+Blue" alt="Océano azul">
<span>Océano azul</span>
</div>
<div class="media-item" data-media-src="https://via.placeholder.com/100x56/00cc00/ffffff?text=Green+Waves" data-media-type="video">
<img src="https://via.placeholder.com/100x56/00cc00/ffffff?text=Green+Waves" alt="Olas verdes">
<span>Olas verdes</span>
</div>
<div class="media-item" data-media-src="https://via.placeholder.com/100x56/ff0088/ffffff?text=Pink+Waves" data-media-type="video">
<img src="https://via.placeholder.com/100x56/ff0088/ffffff?text=Pink+Waves" alt="Olas rosadas">
<span>Olas rosadas</span>
</div>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
2. CSS3 (style.css)
Manteniendo las variables y un diseño moderno, se añaden estilos para los nuevos componentes como modales y tooltips.
/* Variables CSS para consistencia (Reiterado para contexto completo) */
:root {
--primary-color: #ff0040; /* Rojo/Rosa distintivo */
--secondary-color: #3f475a; /* Gris oscuro para botones secundarios */
--background-dark: #1e2430; /* Fondo principal oscuro */
--background-medium: #2a313e; /* Fondos de tarjetas y secciones */
--text-light: #e0e6f0; /* Texto claro */
--text-muted: #8c96a3; /* Texto secundario/desactivado */
--border-color: #3a4459; /* Color de borde sutil */
--shadow-light: rgba(0, 0, 0, 0.1);
--spacing-xs: 0.5rem;
--spacing-sm: 0.75rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
}
body {
font-family: 'Inter', sans-serif;
margin: 0;
padding: 0;
background-color: var(--background-dark);
color: var(--text-light);
overflow: hidden; /* Evita scroll en el body principal */
font-size: 14px; /* Ajuste base para legibilidad */
}
.app-container {
display: flex;
flex-direction: column;
height: 100vh;
}
/* --- Top Header Bar --- */
.top-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-md) var(--spacing-lg);
background-color: var(--background-medium);
border-bottom: 1px solid var(--border-color);
box-shadow: 0 2px 4px var(--shadow-light);
z-index: 100;
}
.header-left {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.app-title {
font-weight: 600;
font-size: 1.1rem;
}
.beta-tag {
background-color: rgba(var(--primary-color), 0.2);
color: var(--primary-color);
padding: 2px 8px;
border-radius: var(--radius-sm);
font-size: 0.7rem;
font-weight: 500;
}
.header-right {
display: flex;
align-items: center;
gap: var(--spacing-md);
}
.user-avatar {
display: flex;
align-items: center;
gap: var(--spacing-xs);
cursor: pointer;
}
.user-avatar img {
width: 30px;
height: 30px;
border-radius: 50%;
object-fit: cover;
border: 1px solid var(--border-color);
}
.live-button {
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
/* --- Main Content Area --- */
.main-content {
display: flex;
flex: 1; /* Ocupa el espacio restante */
overflow: hidden; /* Oculta el desbordamiento dentro del main */
}
/* --- Left Sidebar --- */
.sidebar-left {
width: 250px;
background-color: var(--background-dark);
border-right: 1px solid var(--border-color);
padding: var(--spacing-md) 0;
display: flex;
flex-direction: column;
overflow-y: auto; /* Permite scroll si el contenido es largo */
flex-shrink: 0; /* No se encoge */
}
.sidebar-section {
padding: var(--spacing-md) var(--spacing-lg);
border-bottom: 1px solid var(--border-color);
}
.sidebar-section:last-child {
border-bottom: none;
}
.sidebar-left h3 {
margin-top: 0;
margin-bottom: var(--spacing-sm);
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.sidebar-left h4 {
color: var(--text-muted);
font-size: 0.9rem;
margin-top: 0;
margin-bottom: var(--spacing-sm);
}
.scenes-list {
flex-grow: 1; /* Permite que la lista de escenas crezca */
}
.scene-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-xs) var(--spacing-sm);
margin-bottom: var(--spacing-sm);
border-radius: var(--radius-md);
cursor: pointer;
transition: background-color 0.2s ease;
position: relative; /* Para el menú de opciones */
}
.scene-item:hover {
background-color: var(--background-medium);
}
.scene-item.active {
background-color: var(--primary-color);
color: white;
}
.scene-item.active .scene-name {
color: white;
}
.scene-item img {
width: 60px; /* Reducido para mejor ajuste */
height: 34px; /* Proporcional a 16:9 */
border-radius: var(--radius-sm);
object-fit: cover;
border: 1px solid var(--border-color);
}
.scene-item .scene-name {
flex-grow: 1;
color: var(--text-light);
}
.scene-item .scene-options {
position: relative;
padding: 0 5px;
}
.scene-item .scene-options i {
color: var(--text-muted);
cursor: pointer;
}
.scene-item .scene-options .dropdown-menu {
display: none;
position: absolute;
top: 100%;
right: 0;
background-color: var(--background-medium);
border-radius: var(--radius-sm);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
min-width: 150px;
z-index: 10;
}
.scene-item .scene-options .dropdown-menu.active {
display: block;
}
.scene-item .scene-options .dropdown-menu button {
display: block;
width: 100%;
text-align: left;
padding: 8px 12px;
background: none;
border: none;
color: var(--text-light);
cursor: pointer;
}
.scene-item .scene-options .dropdown-menu button:hover {
background-color: var(--secondary-color);
}
.scene-item .scene-options .dropdown-menu button.danger {
color: #dc3545;
}
.scene-item .scene-options .dropdown-menu button.danger:hover {
background-color: rgba(220, 53, 69, 0.2);
}
.btn-add-scene {
width: 100%;
margin-top: var(--spacing-md);
background-color: var(--secondary-color);
border-color: var(--secondary-color);
color: var(--text-light);
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-xs);
}
.action-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) 0;
color: var(--text-light);
cursor: pointer;
transition: color 0.2s ease;
}
.action-item:hover {
color: var(--primary-color);
}
/* --- Central Live Area --- */
.live-area {
flex: 1; /* Ocupa todo el espacio central disponible */
background-color: #000; /* Fondo para el video */
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
padding: var(--spacing-md);
gap: var(--spacing-md);
overflow-y: auto; /* Permite scroll si el contenido es demasiado alto */
}
.video-player {
position: relative;
width: 100%;
max-width: 900px; /* Max width for the video player */
aspect-ratio: 16 / 9; /* Standard video aspect ratio */
background-color: var(--background-dark); /* Color de fondo mientras no hay video */
border-radius: var(--radius-lg);
overflow: hidden;
margin-bottom: var(--spacing-md);
display: flex;
justify-content: center;
align-items: center;
}
.video-player img#live-video-stream {
width: 100%;
height: 100%;
object-fit: cover;
position: absolute;
top: 0;
left: 0;
}
.video-overlay {
position: absolute;
top: var(--spacing-sm);
left: var(--spacing-sm);
right: var(--spacing-sm);
display: flex;
justify-content: space-between;
color: white;
font-size: 0.8rem;
z-index: 10;
}
.streamyard-logo {
display: flex;
align-items: center;
gap: 5px;
}
.streamyard-logo img {
height: 15px;
width: auto;
}
.stage-element {
position: absolute;
z-index: 5; /* Asegurar que estén sobre el video */
}
#stage-overlay {
top: var(--spacing-sm);
left: var(--spacing-sm);
width: 200px; /* Ejemplo de tamaño */
height: auto;
object-fit: contain;
}
#stage-logo {
top: var(--spacing-sm);
right: var(--spacing-sm);
width: 80px; /* Ejemplo de tamaño */
height: auto;
object-fit: contain;
}
#stage-background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: cover;
background-position: center;
z-index: 1;
}
.participant-feed {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100%; /* Ocupa todo el espacio para posicionar */
display: flex;
justify-content: center;
align-items: flex-end; /* Posicionar los elementos de participantes */
z-index: 5;
}
.participant-name-tag {
background-color: var(--primary-color);
color: white;
padding: 5px 10px;
border-radius: var(--radius-sm);
font-size: 0.85rem;
margin-bottom: 20px; /* Ejemplo de posición */
}
.participants-controls {
width: 100%;
max-width: 900px;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-md);
background-color: var(--background-medium);
padding: var(--spacing-md);
border-radius: var(--radius-lg);
}
.participants-grid {
display: flex;
gap: var(--spacing-md);
justify-content: center;
flex-wrap: wrap; /* Permite que los participantes se envuelvan */
}
.participant-card {
display: flex;
flex-direction: column;
align-items: center;
background-color: var(--background-dark);
padding: var(--spacing-sm);
border-radius: var(--radius-md);
border: 2px solid transparent;
cursor: pointer;
transition: all 0.2s ease;
}
.participant-card.active {
border-color: var(--primary-color);
}
.participant-card img {
width: 60px;
height: 60px;
border-radius: 50%;
object-fit: cover;
margin-bottom: var(--spacing-xs);
}
.participant-card span {
font-size: 0.85rem;
}
.participant-card.add-card {
justify-content: center;
font-size: 1.5rem;
color: var(--text-muted);
border: 2px dashed var(--border-color);
display: flex;
flex-direction: column;
gap: 5px;
font-size: 0.85rem;
}
.participant-card.add-card i {
font-size: 1.5rem;
}
.participant-card.add-card:hover {
border-color: var(--primary-color);
color: var(--primary-color);
}
.interaction-buttons {
display: flex;
gap: var(--spacing-sm);
margin-top: var(--spacing-sm);
}
.btn-presentar {
background-color: var(--primary-color);
color: white;
padding: var(--spacing-xs) var(--spacing-md);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-weight: 500;
transition: background-color 0.2s ease;
}
.btn-presentar:hover {
background-color: #e00038;
}
.bottom-controls {
display: flex;
justify-content: center;
align-items: center;
gap: var(--spacing-md);
background-color: var(--background-medium);
padding: var(--spacing-md) var(--spacing-lg);
border-radius: var(--radius-lg);
margin-top: var(--spacing-md);
}
.leave-button {
background-color: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
.leave-button:hover {
background-color: #e00038;
}
/* --- Right Sidebar - Settings --- */
.sidebar-right {
width: 320px; /* Ancho más amplio para los ajustes */
background-color: var(--background-dark);
border-left: 1px solid var(--border-color);
padding: var(--spacing-md) 0;
display: flex;
flex-direction: column;
overflow-y: auto;
flex-shrink: 0;
}
.brand-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-sm) var(--spacing-lg);
border-bottom: 1px solid var(--border-color);
margin-bottom: var(--spacing-md);
}
.brand-actions {
display: flex;
gap: var(--spacing-md);
color: var(--text-muted);
}
.brand-actions i {
cursor: pointer;
}
.brand-actions i:hover {
color: var(--primary-color);
}
.dropdown-select {
background-color: var(--background-medium);
color: var(--text-light);
border: 1px solid var(--border-color);
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius-sm);
appearance: none; /* Elimina estilos nativos del select */
-webkit-appearance: none;
-moz-appearance: none;
cursor: pointer;
}
.dropdown-select:focus {
outline: none;
border-color: var(--primary-color);
}
.card {
background-color: var(--background-medium);
border-radius: var(--radius-lg);
padding: var(--spacing-md);
margin: 0 var(--spacing-md) var(--spacing-md);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
position: relative;
}
.settings-card h4 {
margin-top: 0;
margin-bottom: var(--spacing-xs);
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.new-tag, .premium-badge {
background-color: var(--primary-color);
color: white;
padding: 2px 8px;
border-radius: var(--radius-sm);
font-size: 0.7rem;
font-weight: 500;
}
.premium-badge::before {
content: "Premium";
}
.settings-card p {
color: var(--text-muted);
font-size: 0.85rem;
margin-bottom: var(--spacing-md);
}
.settings-card .btn-secondary {
padding: 8px 16px;
font-size: 0.9rem;
}
.settings-card .icon-action {
position: absolute;
top: var(--spacing-md);
right: var(--spacing-md);
color: var(--text-muted);
cursor: pointer;
}
.settings-card .icon-action:hover {
color: var(--primary-color);
}
.sidebar-tabs {
display: flex;
justify-content: space-around;
padding: 0 var(--spacing-md);
border-bottom: 1px solid var(--border-color);
margin-bottom: var(--spacing-md);
}
.tab-item {
flex: 1;
text-align: center;
padding: var(--spacing-sm) 0;
cursor: pointer;
color: var(--text-muted);
border-bottom: 2px solid transparent;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-xs);
}
.tab-item:hover {
color: var(--text-light);
}
.tab-item.active {
color: var(--primary-color);
border-bottom-color: var(--primary-color);
}
.tab-content {
display: none; /* Por defecto oculto */
padding: 0 var(--spacing-lg);
}
.tab-content.active {
display: block; /* Muestra el activo */
}
.icon-help {
color: var(--text-muted);
font-size: 0.8rem;
margin-left: var(--spacing-xs);
cursor: help;
}
.presets-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--spacing-sm);
margin-top: var(--spacing-sm);
}
.preset-item {
aspect-ratio: 1 / 1;
background-color: var(--background-medium);
border: 2px solid var(--border-color);
border-radius: var(--radius-md);
cursor: pointer;
transition: border-color 0.2s ease;
}
.preset-item:hover, .preset-item.active {
border-color: var(--primary-color);
}
.color-picker {
display: flex;
align-items: center;
gap: var(--spacing-sm);
margin-top: var(--spacing-sm);
}
.color-input {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
width: 30px;
height: 30px;
background-color: transparent;
border: none;
cursor: pointer;
}
.color-input::-webkit-color-swatch {
border-radius: 50%;
border: 2px solid var(--border-color);
}
.color-input::-moz-color-swatch {
border-radius: 50%;
border: 2px solid var(--border-color);
}
.theme-buttons {
display: flex;
gap: var(--spacing-xs);
margin-top: var(--spacing-sm);
flex-wrap: wrap;
}
.btn-theme {
background-color: var(--background-medium);
color: var(--text-light);
border: 1px solid var(--border-color);
padding: 8px 12px;
border-radius: var(--radius-sm);
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.9rem;
}
.btn-theme:hover, .btn-theme.active {
background-color: var(--primary-color);
border-color: var(--primary-color);
color: white;
}
/* Toggle Switch */
.toggle-switch-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: var(--spacing-md);
}
.switch {
position: relative;
display: inline-block;
width: 40px;
height: 20px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--secondary-color);
transition: 0.4s;
border-radius: 20px;
}
.slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 2px;
bottom: 2px;
background-color: white;
transition: 0.4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: var(--primary-color);
}
input:focus + .slider {
box-shadow: 0 0 1px var(--primary-color);
}
input:checked + .slider:before {
transform: translateX(20px);
}
/* Multimedia Tab Specifics */
.logo-uploader {
display: flex;
align-items: center;
gap: var(--spacing-md);
margin-top: var(--spacing-sm);
}
.logo-preview {
width: 60px;
height: 60px;
background-color: var(--background-dark);
border: 2px dashed var(--border-color);
border-radius: var(--radius-md);
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
.logo-preview img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.logo-position {
display: flex;
gap: var(--spacing-sm);
margin-top: var(--spacing-md);
}
.logo-position .btn-icon {
width: 40px;
height: 40px;
background-color: var(--background-medium);
border: 1px solid var(--border-color);
color: var(--text-light);
border-radius: var(--radius-sm);
font-size: 0.9rem;
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.logo-position .btn-icon[data-position="top-left"]::before { content: "\f02d"; font-family: 'Font Awesome 5 Free'; font-weight: 900; position: absolute; top: 5px; left: 5px; }
.logo-position .btn-icon[data-position="top-right"]::before { content: "\f02d"; font-family: 'Font Awesome 5 Free'; font-weight: 900; position: absolute; top: 5px; right: 5px; }
.logo-position .btn-icon.active {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.overlay-grid, .background-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--spacing-sm);
margin-top: var(--spacing-sm);
}
.overlay-item, .background-item {
aspect-ratio: 16/9;
background-color: var(--background-medium);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.2s ease;
display: flex;
justify-content: center;
align-items: center;
position: relative;
overflow: hidden;
}
.overlay-item.active, .background-item.active {
border-color: var(--primary-color);
}
.overlay-item:hover, .background-item:hover {
border-color: var(--primary-color);
}
.overlay-item.add-more, .background-item.add-more {
font-size: 1.2rem;
color: var(--text-muted);
flex-direction: column;
gap: 5px;
border-style: dashed;
}
.overlay-item.add-more i, .background-item.add-more i {
font-size: 1.5rem;
color: var(--primary-color);
}
.overlay-item img, .background-item img {
width: 100%;
height: 100%;
object-fit: cover;
}
#qr-code-generator .qr-code-form {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
margin-top: var(--spacing-sm);
}
#qr-code-generator input {
background-color: var(--background-medium);
border: 1px solid var(--border-color);
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius-sm);
color: var(--text-light);
}
#qr-code-generator input::placeholder {
color: var(--text-muted);
}
#qr-code-generator button {
margin-top: var(--spacing-xs);
}
.video-clips-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--spacing-sm);
margin-top: var(--spacing-sm);
}
.video-clip-item {
background-color: var(--background-medium);
border: 1px dashed var(--border-color);
border-radius: var(--radius-md);
padding: var(--spacing-md);
text-align: center;
font-size: 0.9rem;
color: var(--text-muted);
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-xs);
position: relative;
}
.video-clip-item i {
font-size: 1.2rem;
color: var(--primary-color);
}
.video-clip-item:hover {
border-color: var(--primary-color);
color: var(--primary-color);
}
.video-clip-item .premium-badge {
position: absolute;
top: 5px;
right: 5px;
font-size: 0.6rem;
padding: 2px 5px;
background-color: #f7b000; /* Color premium */
}
.countdown-options {
display: flex;
gap: var(--spacing-xs);
margin-top: var(--spacing-md);
}
.countdown-item {
background-color: var(--background-medium);
border: 1px solid var(--border-color);
padding: 8px 12px;
border-radius: var(--radius-sm);
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.9rem;
}
.countdown-item.active {
background-color: var(--primary-color);
border-color: var(--primary-color);
color: white;
}
.countdown-item:hover {
background-color: var(--secondary-color);
border-color: var(--secondary-color);
}
.countdown-item.add-more {
border-style: dashed;
color: var(--primary-color);
}
.countdown-item.add-more i {
font-size: 1rem;
}
.sound-volume-control {
display: flex;
align-items: center;
gap: var(--spacing-sm);
margin-top: var(--spacing-sm);
}
.slider-volume {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 5px;
background: var(--secondary-color);
outline: none;
opacity: 0.7;
transition: opacity .2s;
border-radius: 5px;
}
.slider-volume:hover {
opacity: 1;
}
.slider-volume::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 15px;
height: 15px;
border-radius: 50%;
background: var(--primary-color);
cursor: pointer;
}
.slider-volume::-moz-range-thumb {
width: 15px;
height: 15px;
border-radius: 50%;
background: var(--primary-color);
cursor: pointer;
}
.sound-effects-grid, .music-effects-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--spacing-xs);
margin-top: var(--spacing-sm);
}
/* --- Far Right Vertical Bar --- */
.sidebar-far-right {
width: 60px; /* Ancho fijo para la barra vertical de íconos */
background-color: var(--background-medium);
border-left: 1px solid var(--border-color);
display: flex;
flex-direction: column;
align-items: center;
padding: var(--spacing-lg) 0;
gap: var(--spacing-lg);
flex-shrink: 0;
}
.far-right-item {
color: var(--text-muted);
font-size: 0.8rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
cursor: pointer;
transition: color 0.2s ease;
width: 100%;
padding: var(--spacing-xs) 0;
}
.far-right-item i {
font-size: 1.2rem;
}
.far-right-item:hover, .far-right-item.active {
color: var(--primary-color);
}
/* --- Common Button Styles --- */
.btn {
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-sm);
cursor: pointer;
font-weight: 500;
transition: background-color 0.2s ease, border-color 0.2s ease;
font-size: 0.9rem;
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--spacing-xs);
border: 1px solid transparent;
}
.btn-primary {
background-color: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
.btn-primary:hover {
background-color: #e00038;
border-color: #e00038;
}
.btn-secondary {
background-color: var(--secondary-color);
color: var(--text-light);
border-color: var(--secondary-color);
}
.btn-secondary:hover {
background-color: #4a546d;
border-color: #4a546d;
}
.btn-danger {
background-color: #dc3545;
color: white;
border-color: #dc3545;
}
.btn-danger:hover {
background-color: #c82333;
border-color: #bd2130;
}
.btn-icon {
background: none;
border: none;
color: var(--text-light);
font-size: 1.1rem;
cursor: pointer;
padding: var(--spacing-xs);
border-radius: var(--radius-sm);
transition: background-color 0.2s ease, color 0.2s ease;
}
.btn-icon:hover {
background-color: var(--secondary-color);
color: var(--primary-color);
}
.btn-icon.active {
color: var(--primary-color);
}
/* --- Modals --- */
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
display: none; /* Hidden by default */
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal {
background-color: white;
border-radius: var(--radius-lg);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
display: none; /* Hidden by default */
flex-direction: column;
position: relative;
max-width: 90%;
max-height: 90%;
overflow: hidden;
color: #333; /* Texto oscuro para modal */
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-md) var(--spacing-lg);
border-bottom: 1px solid #eee;
}
.modal-header h3 {
margin: 0;
font-size: 1.2rem;
}
.close-modal {
background: none;
border: none;
font-size: 1.2rem;
cursor: pointer;
color: #888;
}
.close-modal:hover {
color: #333;
}
.modal-content {
padding: var(--spacing-lg);
overflow-y: auto; /* Scroll dentro del contenido del modal */
}
/* Specific styles for Configuration Modal */
#config-modal {
width: 800px;
height: 550px;
display: flex;
flex-direction: column;
}
.modal-content-flex {
display: flex;
flex: 1;
}
.modal-nav {
width: 200px;
border-right: 1px solid #eee;
padding: var(--spacing-lg) 0;
flex-shrink: 0;
}
.modal-nav-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-lg);
cursor: pointer;
color: #666;
transition: background-color 0.2s ease, color 0.2s ease;
}
.modal-nav-item:hover {
background-color: #f5f5f5;
}
.modal-nav-item.active {
background-color: #e6f2ff; /* Azul claro para activo */
color: var(--primary-color);
font-weight: 500;
}
.modal-settings-content {
flex: 1;
padding: var(--spacing-lg);
overflow-y: auto;
}
.config-tab-content {
display: none;
}
.config-tab-content.active {
display: block;
}
.setting-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--spacing-md);
font-size: 0.95rem;
}
.setting-row span {
flex-shrink: 0;
margin-right: var(--spacing-md);
}
.setting-row select, .setting-row input[type="text"] {
flex-grow: 1;
padding: var(--spacing-xs);
border: 1px solid #ccc;
border-radius: var(--radius-sm);
}
.setting-row .btn {
margin-left: var(--spacing-sm);
}
.checkbox-container, .radio-container {
display: block;
position: relative;
padding-left: 25px;
margin-bottom: 12px;
cursor: pointer;
font-size: 0.95rem;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.checkbox-container input, .radio-container input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
.checkmark, .radiomark {
position: absolute;
top: 0;
left: 0;
height: 18px;
width: 18px;
background-color: #eee;
border-radius: 3px;
}
.radio-container .radiomark {
border-radius: 50%;
}
.checkbox-container:hover input ~ .checkmark,
.radio-container:hover input ~ .radiomark {
background-color: #ccc;
}
.checkbox-container input:checked ~ .checkmark,
.radio-container input:checked ~ .radiomark {
background-color: var(--primary-color);
}
.checkmark:after {
content: "";
position: absolute;
display: none;
}
.checkbox-container input:checked ~ .checkmark:after {
display: block;
}
.checkbox-container .checkmark:after {
left: 6px;
top: 3px;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 2px 2px 0;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.radiomark:after {
content: "";
position: absolute;
display: none;
}
.radio-container input:checked ~ .radiomark:after {
display: block;
}
.radio-container .radiomark:after {
top: 5px;
left: 5px;
width: 8px;
height: 8px;
border-radius: 50%;
background: white;
}
.small-text {
font-size: 0.85rem;
color: #666;
margin-top: -10px;
margin-bottom: var(--spacing-md);
}
.small-text a {
color: var(--primary-color);
text-decoration: none;
}
.small-text-muted {
font-size: 0.85rem;
color: #999;
}
.small-text-muted.indent {
margin-left: 25px; /* Para alinear con checkboxes */
}
.setting-row-img {
display: flex;
gap: var(--spacing-lg);
margin-bottom: var(--spacing-md);
align-items: flex-start;
}
.setting-row-img img {
width: 150px;
height: 84px;
object-fit: cover;
border-radius: var(--radius-sm);
}
.camera-settings {
flex-grow: 1;
}
.camera-settings .setting-row {
margin-bottom: var(--spacing-sm);
}
.alert-box {
background-color: #fff3cd; /* Amarillo claro */
color: #856404; /* Texto amarillo oscuro */
border: 1px solid #ffeeba;
border-radius: var(--radius-md);
padding: var(--spacing-md);
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: var(--spacing-sm);
margin-top: var(--spacing-md);
}
.alert-box i {
font-size: 1.5rem;
}
.alert-box button {
background-color: #ffc107; /* Amarillo oscuro para botón */
border-color: #ffc107;
color: #333;
}
.alert-box button:hover {
background-color: #e0a800;
}
.info-box {
background-color: #d1ecf1; /* Azul claro */
color: #0c5460; /* Texto azul oscuro */
border: 1px solid #bee5eb;
border-radius: var(--radius-md);
padding: var(--spacing-md);
display: flex;
align-items: center;
gap: var(--spacing-sm);
margin-top: var(--spacing-md);
}
.info-box i {
font-size: 1.2rem;
flex-shrink: 0;
}
.info-box p {
margin: 0;
font-size: 0.9rem;
}
.shortcuts-grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--spacing-sm);
margin-top: var(--spacing-md);
}
.shortcut-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.shortcut-item span {
flex-grow: 1;
}
.shortcut-item input {
width: 120px;
text-align: center;
background-color: #f0f0f0;
border: 1px solid #ddd;
padding: 5px 10px;
border-radius: var(--radius-sm);
}
.shortcut-item i {
color: #999;
cursor: pointer;
}
.shortcut-item i:hover {
color: #dc3545;
}
.design-checkbox-grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--spacing-sm);
margin-top: var(--spacing-md);
}
.design-checkbox {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding-left: 30px; /* Ajustar padding para ícono */
}
.design-checkbox .checkmark {
top: 5px;
}
.design-checkbox i {
font-size: 1.1rem;
margin-right: 5px;
color: #666;
}
#guests textarea {
width: 100%;
min-height: 80px;
background-color: #f0f0f0;
border: 1px solid #ddd;
border-radius: var(--radius-sm);
padding: var(--spacing-sm);
resize: vertical;
font-family: 'Inter', sans-serif;
font-size: 0.9rem;
color: #333;
margin-top: var(--spacing-xs);
}
/* Specific styles for Media Selection Modal */
#media-selection-modal {
width: 800px;
height: 600px;
}
.upload-area {
background-color: #f0f0f0;
border: 2px dashed #ddd;
border-radius: var(--radius-md);
padding: var(--spacing-lg);
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-md);
}
.upload-area i {
font-size: 3rem;
color: #ccc;
}
.upload-area p {
margin: 0;
color: #666;
}
.media-tabs {
display: flex;
border-bottom: 1px solid #eee;
margin-bottom: var(--spacing-md);
}
.media-tab-item {
background: none;
border: none;
padding: var(--spacing-sm) var(--spacing-md);
cursor: pointer;
color: #666;
font-size: 1rem;
border-bottom: 2px solid transparent;
transition: all 0.2s ease;
}
.media-tab-item:hover {
color: #333;
}
.media-tab-item.active {
color: var(--primary-color);
border-bottom-color: var(--primary-color);
font-weight: 500;
}
.media-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: var(--spacing-sm);
overflow-y: auto;
flex: 1;
}
.media-item {
border: 1px solid #eee;
border-radius: var(--radius-md);
overflow: hidden;
cursor: pointer;
transition: all 0.2s ease;
text-align: center;
padding-bottom: var(--spacing-xs);
}
.media-item:hover {
border-color: var(--primary-color);
box-shadow: 0 0 5px rgba(var(--primary-color), 0.5);
}
.media-item img {
width: 100%;
height: 70px; /* Altura fija para miniaturas */
object-fit: cover;
display: block;
margin-bottom: 5px;
}
.media-item span {
font-size: 0.8rem;
color: #333;
}
/* --- Tooltip Styles --- */
[data-tooltip] {
position: relative;
cursor: help;
}
[data-tooltip]:hover::after {
content: attr(data-tooltip);
position: absolute;
background-color: var(--background-medium);
color: var(--text-light);
padding: 6px 10px;
border-radius: var(--radius-sm);
font-size: 0.75rem;
white-space: nowrap;
z-index: 1001;
bottom: calc(100% + 8px); /* Posición arriba del elemento */
left: 50%;
transform: translateX(-50%);
pointer-events: none; /* Asegura que el tooltip no interfiera con el mouse */
opacity: 0;
animation: fade-in 0.2s forwards;
}
[data-tooltip]:hover::before {
content: '';
position: absolute;
bottom: calc(100% + 4px); /* Triángulo debajo del tooltip */
left: 50%;
transform: translateX(-50%);
border-width: 4px;
border-style: solid;
border-color: var(--background-medium) transparent transparent transparent;
z-index: 1001;
pointer-events: none;
opacity: 0;
animation: fade-in 0.2s forwards;
}
/* Animación para el tooltip */
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
/* Scrollbar personalizado */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--background-dark);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: var(--secondary-color);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--border-color);
}
3. JavaScript (script.js)
Este script implementa la lógica de UX para el feedback instantáneo, la gestión de modales, pestañas, y la simulación de algunas funcionalidades.
document.addEventListener('DOMContentLoaded', () => {
// --- Global State Management (Conceptual) ---
const appState = {
currentSceneId: '1',
activeBrand: 'brand1',
brandColor: '#ff0040',
activeTheme: 'block',
showNames: true,
showTitles: false,
logoSrc: 'https://via.placeholder.com/50/aaaaaa/ffffff?text=Logo',
logoPosition: 'top-left',
activeOverlay: 'none',
activeBackground: 'none',
activeBackgroundImg: '', // To store actual image src
soundVolume: 70,
musicVolume: 50,
micOn: true,
camOn: true,
participantsOnStage: {
'cesar': {
name: 'Cesar Mendivil',
onStage: true,
mic: true,
cam: true,
image: 'https://via.placeholder.com/60/777777/ffffff?text=CM'
},
// Add other participants here as needed
},
// State for active right sidebar panel
activeRightPanel: 'activos-multimedia', // Default to multimedia for demo
activeConfigModalTab: 'general',
qrCodeFormVisible: false,
};
// --- DOM Elements Cache ---
const elements = {
// Main layout
scenesList: document.getElementById('scenes-list'),
liveVideoStream: document.getElementById('live-video-stream'),
participantFeed: document.getElementById('participant-feed'),
mainParticipantTag: document.getElementById('main-participant-tag'),
// Right sidebar
rightSidebarTabs: document.querySelectorAll('.sidebar-tabs .tab-item'),
rightSidebarContents: document.querySelectorAll('.sidebar-right .tab-content'),
brandColorInput: document.querySelector('[data-setting="brand-color"]'),
brandColorValue: document.getElementById('brand-color-value'),
themeButtons: document.querySelectorAll('[data-setting="theme"] .btn-theme'),
showNamesToggle: document.querySelector('[data-setting="show-names"]'),
showTitlesToggle: document.querySelector('[data-setting="show-titles"]'),
logoPreview: document.getElementById('logo-preview').querySelector('img'),
logoFileInput: document.getElementById('logo-file-input'),
uploadLogoBtn: document.querySelector('[data-action="upload-logo"]'),
logoPositionButtons: document.querySelectorAll('[data-setting="logo-position"] .btn-icon'),
overlayItems: document.querySelectorAll('[data-setting="overlay"] .overlay-item'),
backgroundItems: document.querySelectorAll('[data-setting="background-video"] .background-item'),
stageOverlay: document.getElementById('stage-overlay'),
stageLogo: document.getElementById('stage-logo'),
stageBackground: document.getElementById('stage-background'),
qrCodeGenerator: document.getElementById('qr-code-generator'),
addQrCodeBtn: document.querySelector('[data-action="add-qr-code"]'),
qrCodeForm: document.querySelector('.qr-code-form'),
createQrBtn: document.querySelector('[data-action="create-qr"]'),
cancelQrBtn: document.querySelector('[data-action="cancel-qr"]'),
qrTitleInput: document.querySelector('[data-qr-title]'),
qrUrlInput: document.querySelector('[data-qr-url]'),
soundVolumeSlider: document.querySelector('[data-setting="sound-volume"]'),
musicVolumeSlider: document.querySelector('[data-setting="music-volume"]'),
// Modals
modalBackdrop: document.getElementById('modal-backdrop'),
configModal: document.getElementById('config-modal'),
configModalCloseBtn: document.querySelector('#config-modal [data-action="close-modal"]'),
configModalNavItems: document.querySelectorAll('.modal-nav-item'),
configModalContents: document.querySelectorAll('.config-tab-content'),
mediaSelectionModal: document.getElementById('media-selection-modal'),
mediaModalTitle: document.getElementById('media-modal-title'),
mediaGrid: document.getElementById('media-grid'),
mediaModalCloseBtn: document.querySelector('#media-selection-modal [data-action="close-modal"]'),
mediaTabItems: document.querySelectorAll('.media-tab-item'),
// Far right panel
farRightPanelItems: document.querySelectorAll('.sidebar-far-right .far-right-item'),
// Controls
micToggles: document.querySelectorAll('[data-mic-toggle]'),
camToggles: document.querySelectorAll('[data-cam-toggle]'),
participantCards: document.querySelectorAll('.participants-grid .participant-card'),
// Scene options dropdown
sceneOptionsToggles: document.querySelectorAll('.scene-options i.fa-ellipsis-v'),
};
// --- Core UI Update Functions ---
/**
* Renders the current active scene in the main video player area.
* Updates background, overlay, logo, and participant visibility based on appState.
*/
function renderLiveView() {
// Update Brand Color (for elements like name tags)
document.documentElement.style.setProperty('--primary-color', appState.brandColor);
// Update Logo
if (appState.logoSrc && appState.activeLogo !== 'none') { // 'none' could be a conceptual value to hide
elements.stageLogo.src = appState.logoSrc;
elements.stageLogo.style.display = 'block';
// Simple positioning based on state
if (appState.logoPosition === 'top-left') {
elements.stageLogo.style.top = 'var(--spacing-sm)';
elements.stageLogo.style.left = 'var(--spacing-sm)';
elements.stageLogo.style.right = 'auto';
} else if (appState.logoPosition === 'top-right') {
elements.stageLogo.style.top = 'var(--spacing-sm)';
elements.stageLogo.style.right = 'var(--spacing-sm)';
elements.stageLogo.style.left = 'auto';
}
} else {
elements.stageLogo.style.display = 'none';
}
// Update Overlay
if (appState.activeOverlay === 'streamyard-live') {
elements.stageOverlay.src = 'https://via.placeholder.com/100x56/ff0040/ffffff?text=EN+VIVO';
elements.stageOverlay.style.display = 'block';
} else {
elements.stageOverlay.style.display = 'none';
}
// Update Background
if (appState.activeBackground !== 'none' && appState.activeBackgroundImg) {
elements.stageBackground.style.backgroundImage = `url(${appState.activeBackgroundImg})`;
elements.stageBackground.style.display = 'block';
} else {
elements.stageBackground.style.backgroundImage = 'none';
elements.stageBackground.style.display = 'none';
}
// Update participant visibility and name tag
const mainParticipant = appState.participantsOnStage['cesar']; // Assuming 'cesar' is the main one
if (mainParticipant && mainParticipant.onStage) {
elements.liveVideoStream.src = mainParticipant.image.replace('60', '800x450'); // Simulate full-size video
if (appState.showNames) {
elements.mainParticipantTag.textContent = mainParticipant.name;
elements.mainParticipantTag.style.display = 'block';
} else {
elements.mainParticipantTag.style.display = 'none';
}
} else {
elements.liveVideoStream.src = 'https://via.placeholder.com/800x450/1e2430/ffffff?text=Vista+Previa+de+la+Transmision'; // Default placeholder
elements.mainParticipantTag.style.display = 'none';
}
// Update mic/cam icons for active participant (if any)
elements.micToggles.forEach(btn => {
if (appState.micOn) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
elements.camToggles.forEach(btn => {
if (appState.camOn) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
console.log("Live view rendered with state:", appState);
}
/**
* Updates the UI elements in the right sidebar based on current appState.
*/
function updateRightSidebarUI() {
// Brand Color
elements.brandColorInput.value = appState.brandColor;
elements.brandColorValue.textContent = appState.brandColor;
// Theme Buttons
elements.themeButtons.forEach(btn => {
if (btn.dataset.theme === appState.activeTheme) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
// Show Names Toggle
elements.showNamesToggle.checked = appState.showNames;
// Show Titles Toggle
elements.showTitlesToggle.checked = appState.showTitles;
// Logo
elements.logoPreview.src = appState.logoSrc;
elements.logoPositionButtons.forEach(btn => {
if (btn.dataset.position === appState.logoPosition) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
// Overlay
elements.overlayItems.forEach(item => {
if (item.dataset.overlay === appState.activeOverlay) {
item.classList.add('active');
} else {
item.classList.remove('active');
}
});
// Background
elements.backgroundItems.forEach(item => {
if (item.dataset.background === appState.activeBackground) {
item.classList.add('active');
} else {
item.classList.remove('active');
}
});
// QR Code form visibility
if (appState.qrCodeFormVisible) {
elements.qrCodeForm.style.display = 'flex';
} else {
elements.qrCodeForm.style.display = 'none';
}
// Sound Volumes
elements.soundVolumeSlider.value = appState.soundVolume;
elements.musicVolumeSlider.value = appState.musicVolume;
}
// --- Event Listeners and Handlers ---
// Right Sidebar Tabs (Estilo, Multimedia)
elements.rightSidebarTabs.forEach(tab => {
tab.addEventListener('click', () => {
elements.rightSidebarTabs.forEach(item => item.classList.remove('active'));
tab.classList.add('active');
elements.rightSidebarContents.forEach(content => content.classList.remove('active'));
const targetContent = document.getElementById(tab.dataset.tab);
if (targetContent) {
targetContent.classList.add('active');
}
appState.activeRightPanel = tab.dataset.tab; // Update state
console.log(`Changed to right sidebar tab: ${appState.activeRightPanel}`);
});
});
// Brand Color Picker
elements.brandColorInput.addEventListener('input', (event) => {
appState.brandColor = event.target.value;
renderLiveView();
updateRightSidebarUI(); // Update value span
});
// Theme Buttons
elements.themeButtons.forEach(button => {
button.addEventListener('click', () => {
appState.activeTheme = button.dataset.theme;
updateRightSidebarUI();
// In a real app, this would trigger visual theme changes
console.log(`Theme changed to: ${appState.activeTheme}`);
});
});
// Show Names Toggle
elements.showNamesToggle.addEventListener('change', (event) => {
appState.showNames = event.target.checked;
renderLiveView();
});
// Show Titles Toggle
elements.showTitlesToggle.addEventListener('change', (event) => {
appState.showTitles = event.target.checked;
renderLiveView();
});
// Logo Upload
elements.uploadLogoBtn.addEventListener('click', () => {
elements.logoFileInput.click();
});
elements.logoFileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
appState.logoSrc = e.target.result;
renderLiveView();
updateRightSidebarUI();
};
reader.readAsDataURL(file);
}
});
// Logo Position Buttons
elements.logoPositionButtons.forEach(button => {
button.addEventListener('click', () => {
appState.logoPosition = button.dataset.position;
renderLiveView();
updateRightSidebarUI();
});
});
// Overlay Selection
elements.overlayItems.forEach(item => {
item.addEventListener('click', () => {
appState.activeOverlay = item.dataset.overlay;
renderLiveView();
updateRightSidebarUI(); // To update active class
console.log(`Overlay changed to: ${appState.activeOverlay}`);
});
});
// Background Selection
elements.backgroundItems.forEach(item => {
item.addEventListener('click', () => {
appState.activeBackground = item.dataset.background;
appState.activeBackgroundImg = item.dataset.img || ''; // Store image URL
renderLiveView();
updateRightSidebarUI(); // To update active class
console.log(`Background changed to: ${appState.activeBackground}`);
});
});
// QR Code Form Toggles
elements.addQrCodeBtn.addEventListener('click', () => {
appState.qrCodeFormVisible = true;
updateRightSidebarUI();
});
elements.cancelQrBtn.addEventListener('click', () => {
appState.qrCodeFormVisible = false;
elements.qrTitleInput.value = '';
elements.qrUrlInput.value = '';
updateRightSidebarUI();
});
elements.createQrBtn.addEventListener('click', () => {
const title = elements.qrTitleInput.value;
const url = elements.qrUrlInput.value;
if (title && url) {
console.log(`Created QR Code: Title - ${title}, URL - ${url}`);
// In a real app, generate QR code and add to UI
appState.qrCodeFormVisible = false;
elements.qrTitleInput.value = '';
elements.qrUrlInput.value = '';
updateRightSidebarUI();
} else {
alert('Por favor, ingresa un título y una URL para el código QR.');
}
});
// Sound Volume Slider
elements.soundVolumeSlider.addEventListener('input', (event) => {
appState.soundVolume = event.target.value;
console.log(`Sound volume: ${appState.soundVolume}`);
// In a real app, adjust actual audio volume
});
// Music Volume Slider
elements.musicVolumeSlider.addEventListener('input', (event) => {
appState.musicVolume = event.target.value;
console.log(`Music volume: ${appState.musicVolume}`);
// In a real app, adjust actual music volume
});
// --- Scenes List (Left Sidebar) ---
elements.scenesList.addEventListener('click', (event) => {
const sceneItem = event.target.closest('.scene-item');
if (sceneItem && !event.target.closest('.scene-options')) { // Avoid activating scene when clicking options
elements.scenesList.querySelectorAll('.scene-item').forEach(item => item.classList.remove('active'));
sceneItem.classList.add('active');
appState.currentSceneId = sceneItem.dataset.sceneId;
renderLiveView(); // Re-render live view for new scene
console.log(`Active scene changed to: ${appState.currentSceneId}`);
}
});
// Scene Options Dropdown Toggle
elements.scenesList.addEventListener('click', (event) => {
const ellipsisIcon = event.target.closest('.scene-options i.fa-ellipsis-v');
if (ellipsisIcon) {
const dropdown = ellipsisIcon.nextElementSibling;
dropdown.classList.toggle('active');
// Close other dropdowns
elements.scenesList.querySelectorAll('.dropdown-menu').forEach(menu => {
if (menu !== dropdown) {
menu.classList.remove('active');
}
});
} else if (!event.target.closest('.dropdown-menu')) {
// Close all dropdowns if click is outside
elements.scenesList.querySelectorAll('.dropdown-menu').forEach(menu => menu.classList.remove('active'));
}
const showOnStageBtn = event.target.closest('[data-action="show-on-stage"]');
if(showOnStageBtn) {
const sceneItem = showOnStageBtn.closest('.scene-item');
elements.scenesList.querySelectorAll('.scene-item').forEach(item => item.classList.remove('active'));
sceneItem.classList.add('active');
appState.currentSceneId = sceneItem.dataset.sceneId;
renderLiveView();
console.log(`Scene ${appState.currentSceneId} shown on stage.`);
showOnStageBtn.closest('.dropdown-menu').classList.remove('active'); // Close dropdown after action
}
});
// --- Participants Controls (Central Area) ---
elements.participantCards.forEach(card => {
card.addEventListener('click', () => {
if (card.dataset.participantId) {
const participantId = card.dataset.participantId;
if (appState.participantsOnStage[participantId]) {
const currentStatus = appState.participantsOnStage[participantId].onStage;
appState.participantsOnStage[participantId].onStage = !currentStatus; // Toggle on/off stage
card.classList.toggle('active', !currentStatus);
renderLiveView();
console.log(`Participant ${participantId} toggled stage status to: ${!currentStatus}`);
}
} else if (card.dataset.action === 'add-participant') {
console.log('Open invite participant modal.');
// Placeholder for opening an invite modal
}
});
});
elements.micToggles.forEach(btn => {
btn.addEventListener('click', () => {
appState.micOn = !appState.micOn;
renderLiveView();
console.log(`Microphone toggled: ${appState.micOn ? 'On' : 'Off'}`);
});
});
elements.camToggles.forEach(btn => {
btn.addEventListener('click', () => {
appState.camOn = !appState.camOn;
renderLiveView();
console.log(`Camera toggled: ${appState.camOn ? 'On' : 'Off'}`);
});
});
// --- Far Right Panel (Main Navigation) ---
elements.farRightPanelItems.forEach(item => {
item.addEventListener('click', () => {
elements.farRightPanelItems.forEach(i => i.classList.remove('active'));
item.classList.add('active');
appState.activeRightPanel = item.dataset.panel;
// Simulate changing the right sidebar content
// Here you would dynamically load or show/hide different full panels in sidebar-right
// For this demo, we'll just log the change.
console.log(`Far right panel changed to: ${appState.activeRightPanel}`);
// To demonstrate, we can switch the right sidebar's main tab if it matches
const correspondingTab = document.querySelector(`.sidebar-tabs .tab-item[data-tab="${appState.activeRightPanel}"]`);
if (correspondingTab) {
correspondingTab.click(); // Programmatically click the tab
} else {
// If it's a different panel (like "Comentarios"), you'd load a different content structure
// For now, ensure only multimedia/estilo tabs are handled.
// Or you could make the right-sidebar itself dynamic, replacing its content entirely.
elements.rightSidebarTabs.forEach(tab => tab.classList.remove('active'));
elements.rightSidebarContents.forEach(content => content.classList.remove('active'));
// And then load specific content for, e.g., 'comentarios'
}
});
});
// --- Modals ---
function openModal(modalElement) {
elements.modalBackdrop.style.display = 'flex';
modalElement.style.display = 'flex';
// Add a class to body to prevent scrolling
document.body.style.overflow = 'hidden';
}
function closeModal() {
elements.modalBackdrop.style.display = 'none';
elements.configModal.style.display = 'none';
elements.mediaSelectionModal.style.display = 'none';
document.body.style.overflow = ''; // Restore body scroll
}
// Open Config Modal
document.querySelector('[data-action="open-settings"]').addEventListener('click', () => {
openModal(elements.configModal);
// Ensure default tab is active on open
elements.configModalNavItems.forEach(item => item.classList.remove('active'));
elements.configModalContents.forEach(content => content.classList.remove('active'));
document.querySelector(`.modal-nav-item[data-config-tab="${appState.activeConfigModalTab}"]`).classList.add('active');
document.getElementById(appState.activeConfigModalTab).classList.add('active');
});
// Close Modals
elements.modalBackdrop.addEventListener('click', (event) => {
if (event.target === elements.modalBackdrop) { // Only close if clicking backdrop directly
closeModal();
}
});
elements.configModalCloseBtn.addEventListener('click', closeModal);
elements.mediaModalCloseBtn.addEventListener('click', closeModal);
// Config Modal Navigation
elements.configModalNavItems.forEach(navItem => {
navItem.addEventListener('click', () => {
elements.configModalNavItems.forEach(item => item.classList.remove('active'));
navItem.classList.add('active');
elements.configModalContents.forEach(content => content.classList.remove('active'));
const targetTabId = navItem.dataset.configTab;
document.getElementById(targetTabId).classList.add('active');
appState.activeConfigModalTab = targetTabId; // Update state
console.log(`Config modal tab changed to: ${targetTabId}`);
});
});
// Open Media Selection Modal (Background)
document.querySelector('[data-action="open-background-modal"]').addEventListener('click', () => {
elements.mediaModalTitle.textContent = 'Fondo';
openModal(elements.mediaSelectionModal);
// Load background media specific items
elements.mediaGrid.innerHTML = ''; // Clear previous items
// Filter media-items by type 'image' or 'video' if applicable
document.querySelectorAll('.media-item[data-media-type="image"]').forEach(item => {
const clone = item.cloneNode(true);
clone.addEventListener('click', (event) => {
const src = event.currentTarget.dataset.mediaSrc;
const backgroundName = event.currentTarget.querySelector('span').textContent;
appState.activeBackground = backgroundName;
appState.activeBackgroundImg = src;
renderLiveView();
updateRightSidebarUI(); // To update the smaller grid in sidebar
closeModal();
console.log(`Selected background: ${backgroundName}`);
});
elements.mediaGrid.appendChild(clone);
});
// Set images tab as active for backgrounds
elements.mediaTabItems.forEach(item => item.classList.remove('active'));
document.querySelector('.media-tab-item[data-media-type="images"]').classList.add('active');
});
// Open Media Selection Modal (Overlay)
document.querySelector('[data-action="open-overlay-modal"]').addEventListener('click', () => {
elements.mediaModalTitle.textContent = 'Superposición';
openModal(elements.mediaSelectionModal);
// Load overlay media specific items (similar to background but might be different assets)
elements.mediaGrid.innerHTML = ''; // Clear previous items
document.querySelectorAll('.media-item[data-media-type="image"]').forEach(item => { // Re-using image assets for demo
const clone = item.cloneNode(true);
clone.addEventListener('click', (event) => {
const src = event.currentTarget.dataset.mediaSrc;
const overlayName = event.currentTarget.querySelector('span').textContent;
appState.activeOverlay = overlayName; // Update with selected overlay
// Potentially store a specific overlay image if needed
renderLiveView();
updateRightSidebarUI();
closeModal();
console.log(`Selected overlay: ${overlayName}`);
});
elements.mediaGrid.appendChild(clone);
});
elements.mediaTabItems.forEach(item => item.classList.remove('active'));
document.querySelector('.media-tab-item[data-media-type="images"]').classList.add('active');
});
// Media Modal Tabs (Images/Videos)
elements.mediaTabItems.forEach(tab => {
tab.addEventListener('click', () => {
elements.mediaTabItems.forEach(item => item.classList.remove('active'));
tab.classList.add('active');
const mediaType = tab.dataset.mediaType;
// Filter/re-render media grid based on type
elements.mediaGrid.innerHTML = '';
document.querySelectorAll(`.media-item[data-media-type="${mediaType}"]`).forEach(item => {
const clone = item.cloneNode(true);
// Re-attach click listener for new clones if necessary, or use delegation
elements.mediaGrid.appendChild(clone);
});
console.log(`Media modal tab changed to: ${mediaType}`);
});
});
// --- Drag-and-Drop for Scenes (Conceptual) ---
let draggedScene = null;
elements.scenesList.addEventListener('dragstart', (event) => {
draggedScene = event.target.closest('.scene-item');
if (draggedScene) {
event.dataTransfer.effectAllowed = 'move';
// Optional: Store some data about the dragged item
event.dataTransfer.setData('text/plain', draggedScene.dataset.sceneId);
setTimeout(() => {
draggedScene.classList.add('dragging');
}, 0);
}
});
elements.scenesList.addEventListener('dragover', (event) => {
event.preventDefault(); // Allow drop
const targetItem = event.target.closest('.scene-item');
if (targetItem && targetItem !== draggedScene) {
const rect = targetItem.getBoundingClientRect();
const midpoint = rect.y + rect.height / 2;
if (event.clientY < midpoint) {
elements.scenesList.insertBefore(draggedScene, targetItem);
} else {
elements.scenesList.insertBefore(draggedScene, targetItem.nextSibling);
}
}
});
elements.scenesList.addEventListener('dragend', () => {
if (draggedScene) {
draggedScene.classList.remove('dragging');
draggedScene = null;
// In a real app, update the order in appState or backend here.
console.log("Scenes reordered. Update backend/state.");
}
});
// --- Initialization ---
function initializeUI() {
// Set initial active states for right sidebar and far right panel
elements.rightSidebarTabs[1].click(); // Activate "Activos multimedia" tab initially
elements.farRightPanelItems[2].click(); // Activate "Activos multimedia" in far right
renderLiveView(); // Render the initial state of the live view
updateRightSidebarUI(); // Update sidebar UI elements
console.log("UI initialized.");
}
initializeUI();
});
Explicación de la Propuesta UX en el Código:
-
HTML5 (
index.html):- Estructura Semántica: Utiliza
<header>,<main>,<aside>,<section>para una estructura clara. - Controles Orientados a la UX:
data-actionatributos en botones para indicar su propósito y facilitar el manejo de eventos en JS.data-settingatributos para elementos de configuración que afectan el estado.data-scene-id,data-participant-idpara identificar elementos específicos.beta-tagynew-tagpara destacar características nuevas/en beta.premium-badgepara indicar funciones de pago.
- Modales: Estructura HTML para los modales de
ConfiguraciónySelección de Medios, conmodal-backdroppara el overlay. - Elementos Dinámicos: Placeholders (
<img id="live-video-stream">,<div id="participant-feed">) donde JavaScript inyectará o modificará el contenido para reflejar los cambios en tiempo real. - Tooltips: Atributos
data-tooltipen varios elementos para proporcionar información adicional al pasar el ratón, mejorando la discoverability y learnability.
- Estructura Semántica: Utiliza
-
CSS3 (
style.css):- Variables CSS: Reafirma el uso de
--primary-color,--background-dark, etc., para la coherencia visual y facilidad de mantenimiento, directamente aplicando los colores y espaciados identificados en el análisis pixel-perfect. - Layout Adaptativo: Flexbox para la mayoría de los contenedores (top-header, main-content, sidebars, participant-controls), y Grid para componentes específicos (presets-grid, overlay-grid, etc.) para organizar los elementos de manera eficiente.
- Estados Visuales Claros: Clases
.activepara resaltar elementos seleccionados (escenas, pestañas, temas, presets). - Diseño de Componentes: Estilos detallados para botones, toggles, dropdowns, input de color, sliders de volumen, checkbox/radio personalizados para mantener una interfaz limpia y moderna.
- Tooltips Estilizados: CSS para el
data-tooltipque asegura que los tooltips sean discretos, legibles y aparezcan de forma consistente. - Modales: Estilos para el backdrop, el contenedor del modal y su contenido, incluyendo la navegación lateral del modal de configuración, replicando la apariencia del video.
- Scrollbars Personalizados: Un toque extra para mantener la estética oscura en los scrollbars.
- Variables CSS: Reafirma el uso de
-
JavaScript (
script.js):- Gestión de Estado Centralizada (
appState): Un objeto global que mantiene el estado actual de la aplicación (escena activa, color de marca, logo, superposiciones, estado de micrófono/cámara, etc.). Esto es crucial para un feedback visual instantáneo, ya que la UI se actualiza en función de este estado. - Funciones de Renderizado y Actualización:
renderLiveView(): La función más importante para la UX. Se encarga de actualizar el área central de transmisión (cambio de fondo, superposiciones, logos, visibilidad de nombres de participantes) inmediatamente después de un cambio en elappState. Esto proporciona el feedback visual instantáneo.updateRightSidebarUI(): Sincroniza los controles del panel derecho (sliders, toggles, active-states de botones) con elappState.
- Delegación de Eventos: Se utilizan event listeners en contenedores padre (ej.,
scenesList,participantsList) para manejar clics en elementos hijos que pueden ser añadidos o removidos dinámicamente, optimizando el rendimiento. - Manejo de Modales: Funciones
openModal()ycloseModal()para gestionar la visibilidad del backdrop y los modales, y para controlar eloverflowdelbodypara evitar el scroll accidental detrás del modal. - Navegación por Pestañas y Subpestañas: Lógica para cambiar la clase
activeen las pestañas y mostrar/ocultar el contenido correspondiente en el sidebar derecho y dentro del modal de configuración. - Simulación de Drag-and-Drop: Una implementación básica para reordenar las escenas, mostrando cómo se manejaría visualmente el arrastre y la inserción.
- Interacciones Específicas:
- Cambio de color de marca: El
input type="color"actualizaappState.brandColoryrenderLiveView()cambia el--primary-coloren CSS. - Toggles:
showNamesToggleactualizaappState.showNames, lo que afecta la visibilidad del nombre del participante en la vista previa. - Selección de logo/superposición/fondo: Actualizan
appStateyrenderLiveView()cambia las imágenes en el área central. - QR Code form: Lógica para mostrar/ocultar el formulario.
- Controles de micrófono/cámara: Actualizan
appStatey cambian el icono deactiveen los botones.
- Cambio de color de marca: El
- Inicialización (
initializeUI): Asegura que la UI se cargue en un estado predecible y activo, activando el primer tab y panel por defecto, y renderizando la vista en vivo inicial.
- Gestión de Estado Centralizada (
Esta propuesta de código es un punto de partida sólido para construir una interfaz con la misma atención a los detalles de UX que se observa en el video de StreamYard. La clave es la estrecha relación entre la gestión del estado en JavaScript y la actualización visual instantánea del DOM a través de CSS.