**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(null); const [activeSidebarLink, setActiveSidebarLink] = useState('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 (
{/* Renderizado condicional del contenido principal basado en el link activo */} {activeSidebarLink === 'Inicio' && ( <> {/* Se pasa la función openModal a HeroSection */} )} {activeSidebarLink === 'Configuracion' && (

Configuración del equipo

{/* Aquí iría el componente de configuración con tabs General/Facturación */}
)} {activeSidebarLink === 'Referidos' && (

Referidos

{/* Aquí iría el componente de referidos */}
)} {activeSidebarLink === 'Biblioteca' && (

Biblioteca

Actualmente no tienes grabaciones.

Tus grabaciones aparecerán aquí

)} {activeSidebarLink === 'Destinos' && (

Destinos

{/* Aquí iría el componente de destinos, con botón para abrir modal "Agregar destino" */}
)} {activeSidebarLink === 'Miembros' && (

Miembros

{/* Aquí iría el componente de miembros */}
)} {/* Otros contenidos para el sidebar */}
{/* Renderizado de modales condicional */} {activeModal === 'createRecording' && ( )} {activeModal === 'streamYardOnAir' && ( )} {activeModal === 'createLiveStream' && ( )}
); }; 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 = ({ activeLink, onLinkClick }) => { const navItems = [ { id: 'Inicio', label: 'Inicio', icon: }, { id: 'Biblioteca', label: 'Biblioteca', icon: }, { id: 'Destinos', label: 'Destinos', icon: }, { id: 'Miembros', label: 'Miembros', icon: }, ]; const secondaryItems = [ { id: 'Referidos', label: 'Referidos', icon: }, { id: 'Configuracion', label: 'Configuración del equipo', icon: }, { id: 'Estado del sistema', label: 'Estado del sistema', icon: }, ]; return ( ); }; 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 = ({ currentTheme, onToggleTheme }) => { const [isDropdownOpen, setIsDropdownOpen] = useState(false); const dropdownRef = useRef(null); const userMenuRef = useRef(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 (
Mi cuenta
{isDropdownOpen && }
); }; 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((props, ref) => { return ( ); }); 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 = ({ icon, title, onClick }) => { return (
{/* Añadir onClick */}
{icon}

{title}

); }; interface HeroSectionProps { onOpenModal: (modalType: ModalType) => void; } const HeroSection: React.FC = ({ onOpenModal }) => { return (

Crear

} title="Transmisión en vivo" onClick={() => onOpenModal('createLiveStream')} // Abrir modal específico /> } title="Grabación" onClick={() => onOpenModal('createRecording')} // Abrir modal específico /> } title="Seminario web On-Air" onClick={() => onOpenModal('streamYardOnAir')} // Abrir modal específico />
); }; 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 = ({ 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 (
e.stopPropagation()}> {/* Evita cerrar al hacer clic dentro */}

{title}

{children}
); }; 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 = ({ id, label, checked, onChange, description }) => { return (
); }; 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 (
setRecordingTitle(e.target.value)} placeholder="Mi nueva grabación" required />
); }; 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 (

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.

{/* Simulación del video */}
StreamYard On-Air Video {/* Aquí iría el reproductor de video real */}

Advanced

Para profesionales que quieren llevar su contenido a otro nivel.

  • Webinars en vivo
  • Alta definición completa (1080p)
  • Grabaciones locales en 4K (2160p)
  • Logos, Superposiciones, Clips de video, Fondos
  • Transmisión múltiple - 8 destinos
  • Transmisión y grabación ilimitados
  • y mucho más
Mostrar otros planes

Sin compromisos y fácil de cancelar.

); }; 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 (
{/* Avatar de destino simulado */}
a Da Nation Freedom {/* Tooltip */}
Omitir por ahora
setTitle(e.target.value)} required /> 0/100
0/5000

Esta transmisión no se grabará en StreamYard. Para grabar, tendrás que pasarte a un plan superior.

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