AvanzaCast/docs/broadcast_panel_ux.md

1545 lines
58 KiB
Markdown

**INFORME TÉCNICO COMPLEMENTARIO (Análisis de UX del Video)**
**I. Estructura y Componentes Lógicos (Actualización):**
Se confirman los componentes previamente definidos (`PageContainer`, `Sidebar`, `Header`, `HeroSection`, `TransmissionsTable`) y se añaden los siguientes componentes lógicos clave basados en la interactividad observada:
* **`Modal.tsx` (Componente Reutilizable):** Un componente genérico para la superposición de modales.
* **Jerarquía DOM:** Un `div` principal para la capa de superposición (`overlay`), y un `div` anidado para el contenido del modal (`modal-content`). Un botón de cierre (`x`) y un `h2` para el título.
* **Medidas Clave:**
* Overlay: `position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); z-index: 2000; display: flex; justify-content: center; align-items: center;`.
* Modal Content: `background-color: var(--surface-color); border-radius: 8px; box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.15); max-width: 500px; width: 90%; max-height: 90vh; overflow-y: auto; position: relative; padding: 24px;`.
* Botón de cierre (x): `position: absolute; top: 16px; right: 16px;`.
* **`DropdownMenu.tsx` (Componente Reutilizable):** Para el menú "Mi cuenta".
* **Jerarquía DOM:** Un `div` principal que se posiciona absolutamente. `ul` con `li` para los elementos del menú.
* **Medidas Clave:**
* Contenedor: `position: absolute; top: 100%; right: 0; background-color: var(--surface-color); border-radius: 8px; box-shadow: 0px 4px 12px var(--shadow-light); min-width: 250px; z-index: 1500;`.
* Items: `padding: 10px 16px; font-size: 14px;`.
* **`ToggleSwitch.tsx` (Componente Reutilizable):** Para las opciones de encendido/apagado.
* **Jerarquía DOM:** Un `label` que contiene un `input[type="checkbox"]` oculto y un `span` para el "slider" visual.
* **Medidas Clave:** `width: 40px; height: 22px; border-radius: 11px;` para el slider. `circle` o `handle` de `18px` de diámetro.
* **`TabbedContent.tsx` (Componente Lógico para Pestañas):** Se observa en "Configuración del equipo" y dentro del modal "Agregar destino".
* **Jerarquía DOM:** Contenedor de `button` para las pestañas y un `div` para el contenido activo.
* **`FormInput.tsx`, `FormDropdown.tsx`, `FormCheckbox.tsx` (Componentes de Formulario):** Se utilizarán para estandarizar los campos de entrada en los modales y páginas de configuración.
**II. Métricas Pixel-Perfect (Actualización):**
* **Tipografía Adicional:**
* **Títulos de Modal (h2):** `font-size: 20px; font-weight: 600;`.
* **Texto dentro de Modales/Formularios:**
* Labels: `font-size: 14px; font-weight: 500;`.
* Input Text: `font-size: 14px; font-weight: 400;`.
* **Elementos de Dropdown:** `font-size: 14px; font-weight: 400;`.
* **Paleta de Colores Adicional:**
* **Overlay de Modal:** `rgba(0, 0, 0, 0.5)` - Negro semitransparente.
* **Fondo de Toggle (off):** `var(--border-light)` (gris claro).
* **Fondo de Toggle (on):** `var(--primary-blue)`.
* **Espaciado y Bordeado Adicional:**
* **Padding de Modal:** `24px` alrededor del contenido.
* **Input Fields:** `padding: 10px 12px; border-radius: 6px; border: 1px solid var(--border-light);`.
* **Toggle Switch:** Dimensiones ya mencionadas. El "handle" del toggle tiene `border-radius: 50%; background-color: white; box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1);`.
* **Dropdown Separadores:** `border-bottom: 1px solid var(--border-light);` para separar grupos de elementos en el menú "Mi cuenta".
* **Tooltip:** `background-color: var(--text-primary); color: var(--surface-color); padding: 6px 10px; border-radius: 4px; font-size: 12px;`.
**III. Funcionalidad y Scripts Internos (Actualización y Expansión):**
Se confirma y expande la funcionalidad descrita previamente, añadiendo los siguientes puntos clave:
1. **Manejo Centralizado de Modales:**
* Se implementará un estado global (posiblemente a través de `useState` en `PageContainer` o un contexto simple si se requiere compartir estado entre más componentes) para controlar la visibilidad y el tipo de modal a mostrar.
* Cada modal tendrá su propia lógica interna para manejar formularios, opciones y acciones, pero su apertura y cierre serán gestionados por el componente padre o el contexto.
* Se observó que los modales pueden tener contenido complejo como **tabs (`Agregar destino` modal)** y **videos (`¡Obtén StreamYard On-Air!`)**.
* **Cierre de Modal:** Los modales se cierran al hacer clic en el botón 'x' o al presionar la tecla `Escape`.
2. **Manejo de Dropdown "Mi cuenta":**
* El componente `Header` gestionará el estado de visibilidad del dropdown "Mi cuenta" utilizando `useState`.
* Un `useEffect` con un *event listener* para `click` en `document` se usará para cerrar el dropdown cuando se haga clic fuera de él.
* Los elementos del dropdown son enlaces de navegación que pueden cambiar la ruta o abrir sub-secciones dentro de la aplicación.
3. **Implementación de Toggle Switches:**
* Los componentes `ToggleSwitch` serán reutilizables y gestionarán su propio estado `checked` (`useState`).
* Se pasarán *props* `onChange` para que los componentes padres puedan reaccionar a los cambios de estado (ej. "Eliminar grabaciones automáticas", "Grabaciones locales").
4. **Navegación en el Sidebar y Pestañas de Contenido:**
* La barra lateral sigue un patrón de "enlaces activos", que se actualizará al hacer clic en ellos, reflejando el cambio de ruta en la URL o el contenido cargado en el área principal.
* En secciones como "Configuración del equipo" y el modal "Agregar destino", se utilizará `useState` para gestionar la pestaña activa, cambiando el contenido renderizado (`renderizado condicional`).
5. **Formularios Interactivos:**
* Modales como "Crear transmisión en vivo" incluyen campos de texto, radio buttons y dropdowns. Estos necesitarán gestión de estado (`useState`) para sus valores y validación básica `(inline validation, if complex)`.
* El campo de destino en "Crear transmisión en vivo" muestra avatares de plataformas y un botón de "añadir", que abre otro modal (`Agregar destino`). Esto implica una interacción entre modales o un flujo de apertura en cadena.
* **Tooltips al Hover:** El video muestra un tooltip al pasar el cursor sobre los avatares de destino, indicando el nombre de la plataforma.
6. **Redirecciones Externas:**
* El enlace "Estado del sistema" en el sidebar y el icono de notificación en el header (`Mi cuenta`) redirigen a URLs externas, lo que se implementará con `target="_blank" rel="noopener noreferrer"` para abrir en una nueva pestaña.
7. **Estado de Carga (Loading States):**
* Aunque breve, se observa un *spinner* de carga al navegar a la "Biblioteca", sugiriendo la necesidad de implementar estados de carga para mejorar la experiencia del usuario durante la recuperación de datos.
8. **Gestión de Rutas/Vistas:**
* Dada la navegación entre "Inicio", "Biblioteca", "Destinos", "Miembros", "Configuración del equipo", "Referidos" y "Estado del sistema", la aplicación requerirá un sistema de enrutamiento (ej. React Router DOM) para manejar las diferentes vistas y mantener el estado de la URL. Esto no se implementará directamente en los componentes dados, pero es una consideración crucial para la aplicación completa.
---
**GENERACIÓN DE CÓDIGO (Componentes Adicionales y Modificaciones)**
Para integrar la nueva funcionalidad, se necesitarán algunos ajustes y componentes adicionales.
**1. `src/components/PageContainer.tsx` (Actualización - Añadir control de Modal)**
```tsx
import React, { useState } from 'react';
import styles from './PageContainer.module.css';
import Sidebar from './Sidebar/Sidebar';
import Header from './Header/Header';
import HeroSection from './HeroSection/HeroSection';
import TransmissionsTable from './TransmissionsTable/TransmissionsTable';
import Modal from '../components/Modal/Modal'; // Nuevo componente Modal
import CreateRecordingModal from '../components/Modals/CreateRecordingModal'; // Modal específico
import StreamYardOnAirModal from '../components/Modals/StreamYardOnAirModal'; // Modal específico
import CreateLiveStreamModal from '../components/Modals/CreateLiveStreamModal'; // Modal específico
// Definir tipos para el estado del modal
type ModalType = 'createRecording' | 'streamYardOnAir' | 'createLiveStream' | null;
const PageContainer: React.FC = () => {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const [activeModal, setActiveModal] = useState<ModalType>(null);
const [activeSidebarLink, setActiveSidebarLink] = useState<string>('Inicio'); // Para simular navegación
const toggleTheme = () => {
setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
};
const openModal = (modalType: ModalType) => {
setActiveModal(modalType);
};
const closeModal = () => {
setActiveModal(null);
};
const handleSidebarLinkClick = (linkId: string) => {
setActiveSidebarLink(linkId);
// En una aplicación real, esto manejaría el enrutamiento (ej. history.push('/${linkId.toLowerCase()}'))
// Para esta simulación, solo cambiaremos el contenido si es "Inicio"
if (linkId !== 'Inicio') {
console.log(`Navegando a: ${linkId}`);
// Simular carga de contenido diferente aquí si es necesario para otros links
}
};
return (
<div className={`${styles.pageContainer} ${theme === 'dark' ? styles.themeDark : styles.themeLight}`}>
<Sidebar activeLink={activeSidebarLink} onLinkClick={handleSidebarLinkClick} />
<div className={styles.mainContent}>
<Header currentTheme={theme} onToggleTheme={toggleTheme} />
<div className={styles.contentWrapper}>
{/* Renderizado condicional del contenido principal basado en el link activo */}
{activeSidebarLink === 'Inicio' && (
<>
{/* Se pasa la función openModal a HeroSection */}
<HeroSection onOpenModal={openModal} />
<TransmissionsTable />
</>
)}
{activeSidebarLink === 'Configuracion' && (
<div>
<h2 className={styles.tempSectionTitle}>Configuración del equipo</h2>
{/* Aquí iría el componente de configuración con tabs General/Facturación */}
</div>
)}
{activeSidebarLink === 'Referidos' && (
<div>
<h2 className={styles.tempSectionTitle}>Referidos</h2>
{/* Aquí iría el componente de referidos */}
</div>
)}
{activeSidebarLink === 'Biblioteca' && (
<div>
<h2 className={styles.tempSectionTitle}>Biblioteca</h2>
<p className={styles.noContentMessage}>Actualmente no tienes grabaciones.</p>
<p className={styles.noContentMessage}>Tus grabaciones aparecerán aquí</p>
</div>
)}
{activeSidebarLink === 'Destinos' && (
<div>
<h2 className={styles.tempSectionTitle}>Destinos</h2>
{/* Aquí iría el componente de destinos, con botón para abrir modal "Agregar destino" */}
</div>
)}
{activeSidebarLink === 'Miembros' && (
<div>
<h2 className={styles.tempSectionTitle}>Miembros</h2>
{/* Aquí iría el componente de miembros */}
</div>
)}
{/* Otros contenidos para el sidebar */}
</div>
<button className={styles.helpButton}>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-help-circle">
<circle cx="12" cy="12" r="10"></circle>
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
Ayuda
</button>
</div>
{/* Renderizado de modales condicional */}
{activeModal === 'createRecording' && (
<Modal title="Crea una grabación" onClose={closeModal}>
<CreateRecordingModal />
</Modal>
)}
{activeModal === 'streamYardOnAir' && (
<Modal title="¡Obtén StreamYard On-Air!" onClose={closeModal}>
<StreamYardOnAirModal />
</Modal>
)}
{activeModal === 'createLiveStream' && (
<Modal title="Crear transmisión en vivo" onClose={closeModal}>
<CreateLiveStreamModal />
</Modal>
)}
</div>
);
};
export default PageContainer;
```
**2. `src/components/PageContainer.module.css` (Actualización - Añadir estilos genéricos)**
```css
/* ... (estilos existentes) ... */
.tempSectionTitle {
font-size: 22px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 24px;
}
.noContentMessage {
font-size: 16px;
color: var(--text-secondary);
text-align: center;
margin-top: 50px;
}
/* Media Queries para responsividad */
@media (max-width: 768px) {
/* ... (estilos existentes) ... */
}
```
**3. `src/components/Sidebar/Sidebar.tsx` (Actualización - Añadir `onLinkClick` prop)**
```tsx
import React from 'react';
import styles from './Sidebar.module.css';
interface SidebarProps {
activeLink: string;
onLinkClick: (linkId: string) => void;
}
const Sidebar: React.FC<SidebarProps> = ({ activeLink, onLinkClick }) => {
const navItems = [
{ id: 'Inicio', label: 'Inicio', icon: <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-home"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg> },
{ id: 'Biblioteca', label: 'Biblioteca', icon: <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-video"><path d="M23 7l-7 5V2l7 5z"></path><path d="M22 17H7a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h15v16z"></path></svg> },
{ id: 'Destinos', label: 'Destinos', icon: <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-share-2"><circle cx="18" cy="5" r="3"></circle><circle cx="6" cy="12" r="3"></circle><circle cx="18" cy="19" r="3"></circle><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line></svg> },
{ id: 'Miembros', label: 'Miembros', icon: <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-users"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg> },
];
const secondaryItems = [
{ id: 'Referidos', label: 'Referidos', icon: <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-gift"><polyline points="20 12 20 22 4 22 4 12"></polyline><rect x="2" y="7" width="20" height="5"></rect><line x1="12" y1="22" x2="12" y2="7"></line><path d="M12 7H7.5a2.5 2 0 0 1 0-5C11 2 12 7 12 7z"></path><path d="M12 7h4.5a2.5 2 0 0 0 0-5C13 2 12 7 12 7z"></path></svg> },
{ id: 'Configuracion', label: 'Configuración del equipo', icon: <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-settings"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0-.33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2v-2a2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg> },
{ id: 'Estado del sistema', label: 'Estado del sistema', icon: <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-bar-chart-2"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg> },
];
return (
<aside className={styles.sidebar}>
<div className={styles.logoSection}>
<img src="https://static.streamyard.com/images/logo-icon.svg" alt="StreamYard Logo" className={styles.logoIcon} />
<span className={styles.logoText}>StreamYard</span>
</div>
<nav className={styles.navMenu}>
<ul className={styles.navList}>
{navItems.map((item) => (
<li key={item.id} className={`${styles.navItem} ${activeLink === item.id ? styles.activeLink : ''}`}>
<a
href="#"
onClick={(e) => {
e.preventDefault();
onLinkClick(item.id);
}}
className={styles.navLink}
>
{item.icon}
<span>{item.label}</span>
</a>
</li>
))}
</ul>
<div className={styles.secondaryNavGroup}>
<ul className={styles.navList}>
{secondaryItems.map((item) => (
<li key={item.id} className={`${styles.navItem} ${activeLink === item.id ? styles.activeLink : ''}`}>
{/* Condicional para enlaces externos */}
{item.id === 'Estado del sistema' ? (
<a
href="https://status.streamyard.com/"
target="_blank"
rel="noopener noreferrer"
className={styles.navLink}
>
{item.icon}
<span>{item.label}</span>
</a>
) : (
<a
href="#"
onClick={(e) => {
e.preventDefault();
onLinkClick(item.id);
}}
className={styles.navLink}
>
{item.icon}
<span>{item.label}</span>
</a>
)}
</li>
))}
</ul>
</div>
</nav>
<div className={styles.storageInfo}>
<h4 className={styles.storageTitle}>Almacenamiento <span className={styles.infoIcon}>?</span></h4>
<div className={styles.progressBarContainer}>
<div className={styles.progressBarFill} style={{ width: '0%' }}></div>
</div>
<p className={styles.storageUsage}>0 de 5 horas</p>
<a href="#" className={styles.addMoreLink}>Agregar más</a>
</div>
</aside>
);
};
export default Sidebar;
```
**4. `src/components/Header/Header.tsx` (Actualización - Añadir `DropdownMenu` y `Tooltip`)**
```tsx
import React, { useState, useRef, useEffect } from 'react';
import styles from './Header.module.css';
import DropdownMenu from './DropdownMenu'; // Nuevo componente DropdownMenu
interface HeaderProps {
currentTheme: 'light' | 'dark';
onToggleTheme: () => void;
}
const Header: React.FC<HeaderProps> = ({ currentTheme, onToggleTheme }) => {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const userMenuRef = useRef<HTMLDivElement>(null);
// Cierra el dropdown si se hace clic fuera
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node) &&
userMenuRef.current &&
!userMenuRef.current.contains(event.target as Node)
) {
setIsDropdownOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
const handleUserMenuClick = () => {
setIsDropdownOpen((prev) => !prev);
};
const handleNotificationClick = () => {
window.open('https://new.streamyard.com/', '_blank', 'noopener noreferrer');
};
return (
<header className={styles.header}>
<div className={styles.headerActions}>
<button className={styles.planButton}>Mejora tu plan</button>
<div className={styles.themeToggleGroup}>
<button
className={`${styles.themeToggleButton} ${currentTheme === 'light' ? styles.active : ''}`}
onClick={onToggleTheme}
aria-label="Toggle light theme"
data-tooltip={currentTheme === 'light' ? 'Modo claro' : 'Modo automático'} // Tooltip dinámico
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-sun">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
<span className={styles.tooltip}>Modo claro</span> {/* Tooltip */}
</button>
<button
className={`${styles.themeToggleButton} ${currentTheme === 'dark' ? styles.active : ''}`}
onClick={onToggleTheme}
aria-label="Toggle dark theme"
data-tooltip={currentTheme === 'dark' ? 'Modo oscuro' : 'Modo automático'} // Tooltip dinámico
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-moon">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
<span className={styles.tooltip}>Modo oscuro</span> {/* Tooltip */}
</button>
</div>
<button className={styles.notificationButton} onClick={handleNotificationClick}>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-bell">
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
</svg>
<span className={styles.notificationDot}></span>
</button>
<div className={styles.userMenuWrapper}>
<div className={styles.userMenu} onClick={handleUserMenuClick} ref={userMenuRef}>
<span>Mi cuenta</span>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-chevron-down">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</div>
{isDropdownOpen && <DropdownMenu ref={dropdownRef} />}
</div>
</div>
</header>
);
};
export default Header;
```
**5. `src/components/Header/Header.module.css` (Actualización - Añadir estilos para Dropdown y Tooltip)**
```css
/* ... (estilos existentes) ... */
.themeToggleButton {
/* ... (estilos existentes) ... */
position: relative; /* Para el tooltip */
}
.themeToggleButton .tooltip {
visibility: hidden;
background-color: var(--text-primary);
color: var(--surface-color);
text-align: center;
border-radius: 4px;
padding: 6px 10px;
position: absolute;
z-index: 1;
bottom: -35px; /* Posicionar debajo del botón */
left: 50%;
transform: translateX(-50%);
opacity: 0;
transition: opacity 0.3s;
font-size: 12px;
white-space: nowrap; /* Evita que el texto se rompa */
}
.themeToggleButton:hover .tooltip {
visibility: visible;
opacity: 1;
}
.userMenuWrapper {
position: relative; /* Contenedor para posicionar el dropdown */
}
.userMenu {
/* ... (estilos existentes) ... */
}
/* Media Queries para responsividad */
@media (max-width: 768px) {
/* ... (estilos existentes) ... */
.themeToggleGroup {
display: none; /* Ocultar el selector de tema en móviles */
}
/* Ajustes para el tooltip en móviles si se decide mostrar */
.themeToggleButton .tooltip {
display: none; /* Ocultar tooltips en móviles por simplicidad */
}
}
```
**6. `src/components/Header/DropdownMenu.tsx` (Nuevo componente DropdownMenu)**
```tsx
import React, { forwardRef } from 'react';
import styles from './DropdownMenu.module.css';
const DropdownMenu = forwardRef<HTMLDivElement>((props, ref) => {
return (
<div className={styles.dropdownMenu} ref={ref}>
<ul className={styles.menuList}>
<li className={styles.menuItemHeader}>nexttv.stream@gmail.com</li>
<li className={styles.menuItem}><a href="#">Referidos</a></li>
<li className={styles.menuItem}><a href="#">Configuración del equipo</a></li>
<li className={styles.menuItem}><a href="#">Facturación</a></li>
<li className={styles.menuItem}><a href="#">Configuración de la cuenta</a></li>
<li className={styles.menuItemSeparator}></li> {/* Separador visual */}
<li className={styles.menuItem}><a href="#">Contacto</a></li>
<li className={styles.menuItem}><a href="#">Mis tickets de soporte</a></li>
<li className={styles.menuItem}><a href="#">Centro de ayuda</a></li>
<li className={styles.menuItem}><a href="#">Recursos para socios</a></li>
<li className={styles.menuItemSeparator}></li> {/* Separador visual */}
<li className={styles.menuItem}><a href="#">Términos de servicio</a></li>
<li className={styles.menuItem}><a href="#">Política de privacidad</a></li>
<li className={styles.menuItemSeparator}></li> {/* Separador visual */}
<li className={styles.menuItem}><a href="#">Cerrar sesión</a></li>
<li className={styles.menuItemButton}>
<button className={styles.helpButtonDropdown}>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-help-circle">
<circle cx="12" cy="12" r="10"></circle>
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
Ayuda
</button>
</li>
</ul>
</div>
);
});
export default DropdownMenu;
```
**7. `src/components/Header/DropdownMenu.module.css` (Nuevo CSS para DropdownMenu)**
```css
.dropdownMenu {
position: absolute;
top: 100%; /* Posiciona debajo del elemento padre */
right: 0;
background-color: var(--surface-color);
border-radius: 8px;
box-shadow: 0px 4px 12px var(--shadow-light); /* Sombra para el dropdown */
min-width: 250px;
z-index: 1500;
padding: 8px 0; /* Padding interno */
margin-top: 10px; /* Separación del botón "Mi cuenta" */
}
.menuList {
list-style: none;
padding: 0;
margin: 0;
}
.menuItemHeader {
padding: 10px 16px;
font-size: 14px;
font-weight: 500;
color: var(--text-primary);
margin-bottom: 8px; /* Espacio antes del primer separador */
}
.menuItem a {
display: block;
padding: 10px 16px;
font-size: 14px;
color: var(--text-primary);
text-decoration: none;
transition: background-color 0.15s ease-in-out;
}
.menuItem a:hover {
background-color: var(--active-bg-light); /* Fondo azul claro al hover */
}
.menuItemSeparator {
height: 1px;
background-color: var(--border-light);
margin: 8px 0;
}
.menuItemButton {
padding: 8px 16px;
text-align: center;
}
.helpButtonDropdown {
background-color: var(--primary-blue);
color: #ffffff;
padding: 10px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
width: 100%; /* Ocupa todo el ancho */
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.helpButtonDropdown:hover {
background-color: #146bdc; /* Azul un poco más oscuro al hacer hover */
}
```
**8. `src/components/HeroSection/HeroSection.tsx` (Actualización - Añadir `onOpenModal` prop)**
```tsx
import React from 'react';
import styles from './HeroSection.module.css';
import { ModalType } from '../../components/PageContainer'; // Importar ModalType
interface CardProps {
icon: React.ReactNode;
title: string;
onClick: () => void; // Añadir prop onClick
}
const CreateCard: React.FC<CardProps> = ({ icon, title, onClick }) => {
return (
<div className={styles.createCard} onClick={onClick}> {/* Añadir onClick */}
<div className={styles.cardIcon}>{icon}</div>
<p className={styles.cardTitle}>{title}</p>
</div>
);
};
interface HeroSectionProps {
onOpenModal: (modalType: ModalType) => void;
}
const HeroSection: React.FC<HeroSectionProps> = ({ onOpenModal }) => {
return (
<section className={styles.heroSection}>
<h2 className={styles.sectionTitle}>Crear</h2>
<div className={styles.cardGrid}>
<CreateCard
icon={<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-video"><polygon points="23 7 16 12 23 17 23 7"></polygon><rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect></svg>}
title="Transmisión en vivo"
onClick={() => onOpenModal('createLiveStream')} // Abrir modal específico
/>
<CreateCard
icon={<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-radio"><circle cx="12" cy="12" r="2"></circle><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"></path></svg>}
title="Grabación"
onClick={() => onOpenModal('createRecording')} // Abrir modal específico
/>
<CreateCard
icon={<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-monitor"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg>}
title="Seminario web On-Air"
onClick={() => onOpenModal('streamYardOnAir')} // Abrir modal específico
/>
</div>
</section>
);
};
export default HeroSection;
```
**9. `src/components/Modal/Modal.tsx` (Nuevo componente genérico de Modal)**
```tsx
import React, { useEffect } from 'react';
import styles from './Modal.module.css';
interface ModalProps {
title: string;
onClose: () => void;
children: React.ReactNode;
}
const Modal: React.FC<ModalProps> = ({ title, onClose, children }) => {
// Cierra el modal al presionar la tecla Escape
useEffect(() => {
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose();
}
};
document.addEventListener('keydown', handleEscape);
return () => {
document.removeEventListener('keydown', handleEscape);
};
}, [onClose]);
return (
<div className={styles.modalOverlay} onClick={onClose}>
<div className={styles.modalContent} onClick={(e) => e.stopPropagation()}> {/* Evita cerrar al hacer clic dentro */}
<button className={styles.closeButton} onClick={onClose} aria-label="Cerrar">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-x">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
<h2 className={styles.modalTitle}>{title}</h2>
<div className={styles.modalBody}>
{children}
</div>
</div>
</div>
);
};
export default Modal;
```
**10. `src/components/Modal/Modal.module.css` (Nuevo CSS para Modal)**
```css
.modalOverlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5); /* Overlay semitransparente */
display: flex;
justify-content: center;
align-items: center;
z-index: 2000; /* Asegura que esté por encima de todo */
backdrop-filter: blur(2px); /* Efecto de desenfoque */
}
.modalContent {
background-color: var(--surface-color);
border-radius: 8px;
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.15); /* Sombra más prominente */
max-width: 500px; /* Ancho máximo */
width: 90%; /* Ajuste responsivo */
max-height: 90vh; /* Altura máxima con scroll */
overflow-y: auto;
position: relative;
padding: 24px;
}
.closeButton {
position: absolute;
top: 16px;
right: 16px;
background-color: transparent;
border: none;
cursor: pointer;
color: var(--text-secondary);
padding: 0;
width: 32px;
height: 32px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
transition: background-color 0.2s ease-in-out;
}
.closeButton:hover {
background-color: var(--border-light);
}
.modalTitle {
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 24px;
margin-top: 0;
padding-right: 40px; /* Espacio para el botón de cerrar */
}
.modalBody {
/* Estilos para el contenido dentro del modal */
font-size: 14px;
color: var(--text-primary);
line-height: 1.6;
}
/* Media Queries para responsividad */
@media (max-width: 768px) {
.modalContent {
width: 95%;
padding: 16px;
}
.modalTitle {
font-size: 18px;
margin-bottom: 16px;
}
.closeButton {
top: 10px;
right: 10px;
width: 28px;
height: 28px;
}
}
```
**11. `src/components/Forms/ToggleSwitch.tsx` (Nuevo componente ToggleSwitch)**
```tsx
import React from 'react';
import styles from './ToggleSwitch.module.css';
interface ToggleSwitchProps {
id: string;
label: string;
checked: boolean;
onChange: (checked: boolean) => void;
description?: string;
}
const ToggleSwitch: React.FC<ToggleSwitchProps> = ({ id, label, checked, onChange, description }) => {
return (
<div className={styles.toggleWrapper}>
<label htmlFor={id} className={styles.toggleLabel}>
{label}
{description && <span className={styles.toggleDescription}>{description}</span>}
<input
type="checkbox"
id={id}
checked={checked}
onChange={(e) => onChange(e.target.checked)}
className={styles.toggleInput}
/>
<span className={styles.toggleSlider}></span>
</label>
</div>
);
};
export default ToggleSwitch;
```
**12. `src/components/Forms/ToggleSwitch.module.css` (Nuevo CSS para ToggleSwitch)**
```css
.toggleWrapper {
display: flex;
align-items: center;
margin-bottom: 16px;
}
.toggleLabel {
display: flex;
align-items: center;
cursor: pointer;
font-size: 14px;
color: var(--text-primary);
position: relative;
}
.toggleDescription {
font-size: 12px;
color: var(--text-secondary);
margin-left: 8px;
flex-basis: 100%; /* Permite que la descripción ocupe su propio espacio si es larga */
}
.toggleInput {
opacity: 0;
width: 0;
height: 0;
}
.toggleSlider {
position: relative;
display: inline-block;
width: 40px; /* Ancho del slider */
height: 22px; /* Altura del slider */
background-color: var(--border-light); /* Fondo cuando está OFF */
border-radius: 11px; /* Forma de píldora */
transition: background-color 0.2s ease;
margin-left: 12px;
flex-shrink: 0; /* Evita que el slider se encoja */
}
.toggleSlider:before {
content: "";
position: absolute;
height: 18px; /* Diámetro del "handle" */
width: 18px; /* Diámetro del "handle" */
left: 2px;
bottom: 2px;
background-color: white;
border-radius: 50%;
transition: transform 0.2s ease, box-shadow 0.2s ease;
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1); /* Sombra del handle */
}
.toggleInput:checked + .toggleSlider {
background-color: var(--primary-blue); /* Fondo cuando está ON */
}
.toggleInput:checked + .toggleSlider:before {
transform: translateX(18px); /* Mueve el handle a la derecha */
}
```
**13. `src/components/Modals/CreateRecordingModal.tsx` (Nuevo componente para modal "Crea una grabación")**
```tsx
import React, { useState } from 'react';
import styles from './ModalContent.module.css'; // Usar un CSS genérico para contenido de modal
import ToggleSwitch from '../Forms/ToggleSwitch';
const CreateRecordingModal: React.FC = () => {
const [recordingTitle, setRecordingTitle] = useState('');
const [localRecordings, setLocalRecordings] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log('Crear grabación:', { recordingTitle, localRecordings });
// Lógica para crear grabación
};
return (
<form onSubmit={handleSubmit}>
<div className={styles.formGroup}>
<label htmlFor="recordingTitle" className={styles.formLabel}>Título de la grabación</label>
<input
type="text"
id="recordingTitle"
className={styles.formInput}
value={recordingTitle}
onChange={(e) => setRecordingTitle(e.target.value)}
placeholder="Mi nueva grabación"
required
/>
</div>
<ToggleSwitch
id="localRecordings"
label="Grabaciones locales?"
description="Grabaciones de la mejor calidad con archivos de audio y video individuales para cada participante, necesario para las grabaciones en 4K Ultra HD."
checked={localRecordings}
onChange={setLocalRecordings}
/>
<div className={styles.modalActions}>
<button type="submit" className={styles.primaryButton}>Crear grabación</button>
</div>
</form>
);
};
export default CreateRecordingModal;
```
**14. `src/components/Modals/StreamYardOnAirModal.tsx` (Nuevo componente para modal "Obtén StreamYard On-Air!")**
```tsx
import React from 'react';
import styles from './ModalContent.module.css';
const StreamYardOnAirModal: React.FC = () => {
return (
<div>
<p className={styles.modalDescription}>
Organizo un seminario web, una transmisión en vivo o un evento en StreamYard, o insértalo en tu sitio web. Además, tienes la opción de recopilar direcciones de correo electrónico a través de un formulario de registro.
</p>
{/* Simulación del video */}
<div className={styles.videoPlaceholder}>
<img src="https://static.streamyard.com/images/hero_img.svg" alt="StreamYard On-Air Video" className={styles.videoThumbnail} />
{/* Aquí iría el reproductor de video real */}
</div>
<h3 className={styles.planFeatureTitle}>Advanced</h3>
<p className={styles.planFeatureDescription}>Para profesionales que quieren llevar su contenido a otro nivel.</p>
<ul className={styles.featureList}>
<li className={styles.featureItem}>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={styles.checkIcon}><polyline points="20 6 9 17 4 12"></polyline></svg>
Webinars en vivo
</li>
<li className={styles.featureItem}>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={styles.checkIcon}><polyline points="20 6 9 17 4 12"></polyline></svg>
Alta definición completa (1080p)
</li>
<li className={styles.featureItem}>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={styles.checkIcon}><polyline points="20 6 9 17 4 12"></polyline></svg>
Grabaciones locales en 4K (2160p)
</li>
<li className={styles.featureItem}>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={styles.checkIcon}><polyline points="20 6 9 17 4 12"></polyline></svg>
Logos, Superposiciones, Clips de video, Fondos
</li>
<li className={styles.featureItem}>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={styles.checkIcon}><polyline points="20 6 9 17 4 12"></polyline></svg>
Transmisión múltiple - 8 destinos
</li>
<li className={styles.featureItem}>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={styles.checkIcon}><polyline points="20 6 9 17 4 12"></polyline></svg>
Transmisión y grabación ilimitados
</li>
<li className={styles.featureItem}>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={styles.checkIcon}><polyline points="20 6 9 17 4 12"></polyline></svg>
y mucho más
</li>
</ul>
<div className={styles.modalActions}>
<button className={styles.primaryButton}>Continuar</button>
<a href="#" className={styles.secondaryLink}>Mostrar otros planes</a>
<p className={styles.commitmentText}>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={styles.checkIcon}><polyline points="20 6 9 17 4 12"></polyline></svg>
Sin compromisos y fácil de cancelar.
</p>
</div>
</div>
);
};
export default StreamYardOnAirModal;
```
**15. `src/components/Modals/CreateLiveStreamModal.tsx` (Nuevo componente para modal "Crear transmisión en vivo")**
```tsx
import React, { useState } from 'react';
import styles from './ModalContent.module.css'; // Usar CSS genérico para contenido de modal
const CreateLiveStreamModal: React.FC = () => {
const [source, setSource] = useState<'studio' | 'preRecorded'>('studio');
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [addReferenceMessage, setAddReferenceMessage] = useState(false);
const [privacy, setPrivacy] = useState('Publica');
const [category, setCategory] = useState('');
const [scheduleLater, setScheduleLater] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log('Crear transmisión en vivo:', {
source, title, description, addReferenceMessage, privacy, category, scheduleLater
});
// Lógica para crear transmisión en vivo
};
return (
<form onSubmit={handleSubmit}>
<div className={styles.formGroup}>
<label className={styles.formLabel}>Fuente <span className={styles.infoIcon}>?</span></label>
<div className={styles.radioGroup}>
<label className={styles.radioLabel}>
<input
type="radio"
name="source"
value="studio"
checked={source === 'studio'}
onChange={() => setSource('studio')}
className={styles.radioInput}
/>
Estudio
</label>
<label className={styles.radioLabel}>
<input
type="radio"
name="source"
value="preRecorded"
checked={source === 'preRecorded'}
onChange={() => setSource('preRecorded')}
className={styles.radioInput}
/>
Video pregrabado
</label>
</div>
</div>
<div className={styles.formGroup}>
<label className={styles.formLabel}>Selecciona los destinos</label>
<div className={styles.destinationsRow}>
{/* Avatar de destino simulado */}
<div className={styles.destinationAvatar} data-tooltip="Da Nation Freedom">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="#E5E7EB" stroke="none" className="avatar"><circle cx="12" cy="12" r="10"></circle><text x="50%" y="50%" dominantBaseline="middle" textAnchor="middle" fill="#6B7280" fontSize="12px">a</text></svg>
<span className={styles.tooltip}>Da Nation Freedom</span> {/* Tooltip */}
</div>
<button type="button" className={styles.addDestinationButton}>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-plus">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</button>
<a href="#" className={styles.secondaryLink}>Omitir por ahora</a>
</div>
</div>
<div className={styles.formGroup}>
<label htmlFor="streamTitle" className={styles.formLabel}>Título</label>
<input
type="text"
id="streamTitle"
className={styles.formInput}
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
<span className={styles.charCount}>0/100</span>
</div>
<div className={styles.formGroup}>
<label htmlFor="streamDescription" className={styles.formLabel}>Descripción</label>
<textarea
id="streamDescription"
className={styles.formTextarea}
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Cuéntanos un poco sobre esta transmisión en vivo"
></textarea>
<span className={styles.charCount}>0/5000</span>
</div>
<div className={styles.formGroup}>
<label className={styles.checkboxLabel}>
<input
type="checkbox"
checked={addReferenceMessage}
onChange={(e) => setAddReferenceMessage(e.target.checked)}
className={styles.checkboxInput}
/>
Agrega el mensaje de referencia a la descripción <span className={styles.infoIcon}>?</span>
<p className={styles.checkboxDescription}>Gana $25 por cada crédito de referencia exitoso.</p>
</label>
</div>
<div className={styles.formGroup}>
<label htmlFor="privacy" className={styles.formLabel}>Privacidad</label>
<select
id="privacy"
className={styles.formSelect}
value={privacy}
onChange={(e) => setPrivacy(e.target.value)}
>
<option value="Publica">Pública</option>
<option value="Privada">Privada</option>
<option value="NoListada">No listada</option>
</select>
</div>
<div className={styles.formGroup}>
<label htmlFor="category" className={styles.formLabel}>Categoría</label>
<select
id="category"
className={styles.formSelect}
value={category}
onChange={(e) => setCategory(e.target.value)}
>
<option value="">Seleccionar categoría</option>
{/* Opciones de categoría */}
</select>
</div>
<div className={styles.formGroup}>
<label className={styles.checkboxLabel}>
<input
type="checkbox"
checked={scheduleLater}
onChange={(e) => setScheduleLater(e.target.checked)}
className={styles.checkboxInput}
/>
Programar para más tarde
</label>
</div>
<div className={styles.modalActions}>
<button type="submit" className={styles.primaryButton}>Crear transmisión en vivo</button>
<p className={styles.smallText}>Esta transmisión no se grabará en StreamYard. Para grabar, tendrás que <a href="#" className={styles.secondaryLink}>pasarte a un plan superior</a>.</p>
</div>
</form>
);
};
export default CreateLiveStreamModal;
```
**16. `src/components/Modals/ModalContent.module.css` (Nuevo CSS genérico para el contenido de los modales de formulario)**
```css
.formGroup {
margin-bottom: 20px;
}
.formLabel {
display: block;
font-size: 14px;
font-weight: 500;
color: var(--text-primary);
margin-bottom: 8px;
display: flex; /* Para el icono de info */
align-items: center;
gap: 4px;
}
.formInput,
.formTextarea,
.formSelect {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--border-light);
border-radius: 6px;
font-size: 14px;
color: var(--text-primary);
background-color: var(--surface-color);
box-sizing: border-box; /* Asegura que padding y borde no aumenten el ancho */
transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
.formInput:focus,
.formTextarea:focus,
.formSelect:focus {
border-color: var(--primary-blue);
box-shadow: 0 0 0 2px rgba(24, 118, 242, 0.2); /* Sombra de foco azul */
outline: none;
}
.formTextarea {
min-height: 80px;
resize: vertical;
}
.charCount {
display: block;
font-size: 12px;
color: var(--text-secondary);
text-align: right;
margin-top: 4px;
}
.radioGroup {
display: flex;
gap: 20px;
}
.radioLabel {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 14px;
color: var(--text-primary);
}
.radioInput {
/* Estilos básicos, se puede personalizar con CSS avanzado para radios custom */
transform: scale(1.1); /* Ligeramente más grande */
accent-color: var(--primary-blue); /* Color del radio button */
}
.checkboxLabel {
display: flex;
align-items: flex-start; /* Alinea el texto con el checkbox */
gap: 8px;
cursor: pointer;
font-size: 14px;
color: var(--text-primary);
}
.checkboxInput {
margin-top: 3px; /* Ajuste visual */
transform: scale(1.1);
accent-color: var(--primary-blue);
flex-shrink: 0; /* Evita que el checkbox se encoja */
}
.checkboxDescription {
font-size: 12px;
color: var(--text-secondary);
margin: 0;
line-height: 1.4;
flex-grow: 1; /* Permite que la descripción ocupe el espacio restante */
}
.modalActions {
margin-top: 30px;
text-align: right;
border-top: 1px solid var(--border-light);
padding-top: 20px;
display: flex;
flex-direction: column;
gap: 15px;
}
.primaryButton {
background-color: var(--primary-blue);
color: #ffffff;
padding: 12px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: background-color 0.2s ease-in-out;
width: auto; /* Para que no ocupe todo el ancho */
align-self: flex-end; /* Alinea a la derecha en columna */
}
.primaryButton:hover {
background-color: #146bdc;
}
.secondaryLink {
color: var(--primary-blue);
text-decoration: none;
font-size: 14px;
font-weight: 500;
align-self: flex-end;
}
.secondaryLink:hover {
text-decoration: underline;
}
.infoIcon {
background-color: var(--border-light);
border-radius: 50%;
width: 16px;
height: 16px;
display: flex;
justify-content: center;
align-items: center;
font-size: 10px;
color: var(--text-secondary);
font-weight: 500;
cursor: help; /* Indica que hay más información */
}
.modalDescription {
font-size: 15px;
line-height: 1.6;
color: var(--text-primary);
margin-bottom: 20px;
}
.videoPlaceholder {
background-color: #000; /* Fondo negro para simular el video */
width: 100%;
padding-bottom: 56.25%; /* Ratio 16:9 */
position: relative;
border-radius: 8px;
overflow: hidden; /* Asegura que la imagen no se salga */
margin-bottom: 20px;
}
.videoThumbnail {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.planFeatureTitle {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
margin-top: 24px;
margin-bottom: 8px;
}
.planFeatureDescription {
font-size: 14px;
color: var(--text-secondary);
line-height: 1.5;
margin-bottom: 20px;
}
.featureList {
list-style: none;
padding: 0;
margin: 0 0 20px 0;
}
.featureItem {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: var(--text-primary);
margin-bottom: 8px;
}
.checkIcon {
color: #28a745; /* Verde para los checks */
flex-shrink: 0;
}
.commitmentText {
font-size: 13px;
color: var(--text-secondary);
display: flex;
align-items: center;
gap: 6px;
margin-top: 10px;
align-self: flex-end; /* Alinea a la derecha en columna */
}
.destinationsRow {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
}
.destinationAvatar {
width: 32px;
height: 32px;
border-radius: 50%;
background-color: var(--border-light);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
position: relative; /* Para el tooltip */
cursor: pointer;
}
/* Tooltip para avatares de destino */
.destinationAvatar .tooltip {
visibility: hidden;
background-color: var(--text-primary);
color: var(--surface-color);
text-align: center;
border-radius: 4px;
padding: 6px 10px;
position: absolute;
z-index: 10;
bottom: 120%; /* Posicionar encima del avatar */
left: 50%;
transform: translateX(-50%);
opacity: 0;
transition: opacity 0.3s;
font-size: 12px;
white-space: nowrap;
}
.destinationAvatar:hover .tooltip {
visibility: visible;
opacity: 1;
}
.addDestinationButton {
background-color: transparent;
border: 1px solid var(--border-light);
border-radius: 50%;
width: 36px;
height: 36px;
display: flex;
justify-content: center;
align-items: center;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s ease-in-out;
}
.addDestinationButton:hover {
border-color: var(--primary-blue);
color: var(--primary-blue);
background-color: var(--active-bg-light);
}
.smallText {
font-size: 12px;
color: var(--text-secondary);
text-align: right;
}
/* Media Queries para responsividad */
@media (max-width: 768px) {
.formLabel {
font-size: 13px;
}
.formInput,
.formTextarea,
.formSelect,
.radioLabel,
.checkboxLabel {
font-size: 13px;
}
.radioGroup {
flex-direction: column; /* Apilar radios en móviles */
gap: 10px;
}
.charCount, .checkboxDescription, .smallText {
font-size: 11px;
}
.modalActions {
padding-top: 15px;
gap: 10px;
}
.primaryButton, .secondaryLink, .commitmentText {
align-self: center; /* Centrar botones y enlaces en columna en móvil */
width: 100%; /* Ocupar todo el ancho */
text-align: center;
}
.primaryButton {
padding: 10px 15px;
font-size: 13px;
}
.secondaryLink {
font-size: 13px;
}
.featureList {
font-size: 13px;
}
.destinationAvatar {
width: 28px;
height: 28px;
}
.destinationAvatar .tooltip {
display: none; /* Ocultar tooltips en móviles */
}
.addDestinationButton {
width: 32px;
height: 32px;
}
}
```