AvanzaCast/docs/studio_ux.md

3035 lines
116 KiB
Markdown

¡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.
```html
<!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.
```css
/* 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.
```javascript
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:**
1. **HTML5 (`index.html`):**
* **Estructura Semántica:** Utiliza `<header>`, `<main>`, `<aside>`, `<section>` para una estructura clara.
* **Controles Orientados a la UX:**
* `data-action` atributos en botones para indicar su propósito y facilitar el manejo de eventos en JS.
* `data-setting` atributos para elementos de configuración que afectan el estado.
* `data-scene-id`, `data-participant-id` para identificar elementos específicos.
* `beta-tag` y `new-tag` para destacar características nuevas/en beta.
* `premium-badge` para indicar funciones de pago.
* **Modales:** Estructura HTML para los modales de `Configuración` y `Selección de Medios`, con `modal-backdrop` para 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-tooltip` en varios elementos para proporcionar información adicional al pasar el ratón, mejorando la discoverability y learnability.
2. **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 `.active` para 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-tooltip` que 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.
3. **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 el `appState`. Esto proporciona el feedback visual instantáneo.
* `updateRightSidebarUI()`: Sincroniza los controles del panel derecho (sliders, toggles, active-states de botones) con el `appState`.
* **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()` y `closeModal()` para gestionar la visibilidad del backdrop y los modales, y para controlar el `overflow` del `body` para evitar el scroll accidental detrás del modal.
* **Navegación por Pestañas y Subpestañas:** Lógica para cambiar la clase `active` en 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"` actualiza `appState.brandColor` y `renderLiveView()` cambia el `--primary-color` en CSS.
* Toggles: `showNamesToggle` actualiza `appState.showNames`, lo que afecta la visibilidad del nombre del participante en la vista previa.
* Selección de logo/superposición/fondo: Actualizan `appState` y `renderLiveView()` 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 `appState` y cambian el icono de `active` en los botones.
* **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.
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.