Unify ui-components into avanza-ui; remove ui-components package; fix type issues and studio-panel wrappers
This commit is contained in:
parent
461db99b9f
commit
91a09df7ab
BIN
.playwright-mcp/streamyard-initial.png
Normal file
BIN
.playwright-mcp/streamyard-initial.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
558
docs/STREAMYARD_STUDIO_UI_ANALYSIS.md
Normal file
558
docs/STREAMYARD_STUDIO_UI_ANALYSIS.md
Normal file
@ -0,0 +1,558 @@
|
|||||||
|
# Análisis de la Interfaz de StreamYard Studio
|
||||||
|
|
||||||
|
## Fecha de Análisis
|
||||||
|
2025-11-11
|
||||||
|
|
||||||
|
## URL Analizada
|
||||||
|
https://streamyard.com/j9smzza87q
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. ESTRUCTURA GENERAL
|
||||||
|
|
||||||
|
### Layout Principal
|
||||||
|
La interfaz de StreamYard está dividida en 3 áreas principales:
|
||||||
|
|
||||||
|
1. **Panel Lateral Izquierdo** (Escenas) - ~200-250px
|
||||||
|
2. **Área Central** (Vista Previa/Canvas) - Flexible
|
||||||
|
3. **Panel Lateral Derecho** (Controles y Opciones) - ~300-350px
|
||||||
|
|
||||||
|
### Esquema de Colores
|
||||||
|
- **Background Principal**: `#1a1a1a` - `#0f0f0f` (Negro muy oscuro)
|
||||||
|
- **Background Secundario**: `#242424` - `#2a2a2a` (Gris oscuro)
|
||||||
|
- **Bordes**: `#333333` - `#404040` (Gris medio oscuro)
|
||||||
|
- **Texto Principal**: `#ffffff` - `#e0e0e0` (Blanco/Gris claro)
|
||||||
|
- **Texto Secundario**: `#999999` - `#b0b0b0` (Gris)
|
||||||
|
- **Accent/Primary**: `#3b82f6` (Azul)
|
||||||
|
- **Success**: `#10b981` (Verde)
|
||||||
|
- **Warning**: `#f59e0b` (Naranja)
|
||||||
|
- **Danger**: `#ef4444` (Rojo)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. PANEL LATERAL IZQUIERDO - ESCENAS
|
||||||
|
|
||||||
|
### Características
|
||||||
|
- **Ancho**: ~200-220px
|
||||||
|
- **Background**: `#1a1a1a`
|
||||||
|
- **Scrollable**: Sí, cuando hay muchas escenas
|
||||||
|
|
||||||
|
### Elementos
|
||||||
|
|
||||||
|
#### Header del Panel
|
||||||
|
```
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ Escenas BETA [?] │
|
||||||
|
│ [<] Mis Escenas [⋮] │
|
||||||
|
└─────────────────────────┘
|
||||||
|
```
|
||||||
|
- Título: "Escenas" + Badge "BETA"
|
||||||
|
- Botón de información `[?]`
|
||||||
|
- Navegación back `[<]`
|
||||||
|
- Menú de opciones `[⋮]`
|
||||||
|
|
||||||
|
#### Video de Introducción
|
||||||
|
```
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ [+] │
|
||||||
|
│ Establecer video de │
|
||||||
|
│ introducción │
|
||||||
|
└─────────────────────────┘
|
||||||
|
```
|
||||||
|
- Botón con ícono `+`
|
||||||
|
- Texto explicativo
|
||||||
|
- Hover: Background `#2a2a2a`
|
||||||
|
|
||||||
|
#### Lista de Escenas
|
||||||
|
Cada escena tiene:
|
||||||
|
```
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ [Miniatura Preview] │
|
||||||
|
│ │
|
||||||
|
│ Demo scene 1 │
|
||||||
|
│ [Botón: Mostrar] │
|
||||||
|
└─────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Propiedades de cada item:
|
||||||
|
- **Miniatura**: Aspect ratio 16:9, ~180x100px
|
||||||
|
- **Título**: Font-size 14px, color `#e0e0e0`
|
||||||
|
- **Botón de Acción**: "Mostrar en el escenario"
|
||||||
|
- Background: `#3b82f6` en hover
|
||||||
|
- Border-radius: `6px`
|
||||||
|
- Padding: `8px 16px`
|
||||||
|
|
||||||
|
#### Video de Cierre
|
||||||
|
Similar al de introducción, al final de la lista
|
||||||
|
|
||||||
|
#### Botón Nueva Escena
|
||||||
|
```
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ [+] Nueva escena │
|
||||||
|
└─────────────────────────┘
|
||||||
|
```
|
||||||
|
- Background: `#2a2a2a`
|
||||||
|
- Hover: `#333333`
|
||||||
|
- Border: `1px dashed #404040`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. ÁREA CENTRAL - CANVAS/PREVIEW
|
||||||
|
|
||||||
|
### Características
|
||||||
|
- **Background**: `#0f0f0f` (Más oscuro que los paneles)
|
||||||
|
- **Aspect Ratio**: 16:9 (se ajusta al espacio disponible)
|
||||||
|
- **Centered**: Sí, vertical y horizontalmente
|
||||||
|
|
||||||
|
### Elementos Superpuestos
|
||||||
|
|
||||||
|
#### Indicador de Resolución
|
||||||
|
```
|
||||||
|
┌──────┐
|
||||||
|
│ 720p │
|
||||||
|
└──────┘
|
||||||
|
```
|
||||||
|
- Posición: Top-left
|
||||||
|
- Background: `rgba(0, 0, 0, 0.7)`
|
||||||
|
- Border-radius: `4px`
|
||||||
|
- Padding: `4px 8px`
|
||||||
|
- Font-size: `12px`
|
||||||
|
|
||||||
|
#### Barra de Layouts (Bottom Center)
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ [□] [⊞] [⊟] [⊡] [⊠] [⊞] [⊟] [■] │
|
||||||
|
└─────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
- Posición: Bottom center, ~40px desde abajo
|
||||||
|
- Background: `rgba(0, 0, 0, 0.8)`
|
||||||
|
- Border-radius: `8px`
|
||||||
|
- Padding: `8px`
|
||||||
|
- Botones de layout:
|
||||||
|
- Tamaño: `40x40px`
|
||||||
|
- Hover: Background `#333333`
|
||||||
|
- Active: Background `#3b82f6`
|
||||||
|
- Border-radius: `6px`
|
||||||
|
- Gap: `4px`
|
||||||
|
|
||||||
|
#### Vista de Participantes (Bottom Left)
|
||||||
|
```
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ [👤] Xesar [⋮] │
|
||||||
|
│ Vista de orador │
|
||||||
|
│ │
|
||||||
|
│ [+ Presentar/ │
|
||||||
|
│ invitar] │
|
||||||
|
└─────────────────────┘
|
||||||
|
```
|
||||||
|
- Posición: Bottom-left
|
||||||
|
- Background: `rgba(0, 0, 0, 0.9)`
|
||||||
|
- Border-radius: `8px`
|
||||||
|
- Width: ~240px
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. PANEL LATERAL DERECHO - CONTROLES
|
||||||
|
|
||||||
|
### Características
|
||||||
|
- **Ancho**: ~300-320px
|
||||||
|
- **Background**: `#1a1a1a`
|
||||||
|
- **Sticky tabs**: En la parte superior
|
||||||
|
|
||||||
|
### Tabs Principales
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────┐
|
||||||
|
│ [Comentarios] [Banners] [Activos]... │
|
||||||
|
└──────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Tabs disponibles:
|
||||||
|
1. Comentarios
|
||||||
|
2. Banners
|
||||||
|
3. Activos multimedia ⭐ (seleccionado en captura)
|
||||||
|
4. Estilo
|
||||||
|
5. Notas
|
||||||
|
6. Personas
|
||||||
|
7. Chat privado
|
||||||
|
|
||||||
|
#### Tab: Activos Multimedia
|
||||||
|
|
||||||
|
**Selector de Marca**
|
||||||
|
```
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ Marca 1 [⋮] │
|
||||||
|
└─────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Secciones Colapsables**
|
||||||
|
|
||||||
|
Cada sección tiene:
|
||||||
|
- **Header**: Título + ícono `[?]` + toggle `[›]`
|
||||||
|
- **Background**: `#242424` para header
|
||||||
|
- **Hover**: `#2a2a2a`
|
||||||
|
- **Border-bottom**: `1px solid #333333`
|
||||||
|
|
||||||
|
Secciones:
|
||||||
|
1. ✓ Logo
|
||||||
|
2. ✓ Superposición
|
||||||
|
3. ✓ Código QR
|
||||||
|
4. ✓ Clips de video
|
||||||
|
5. ✓ Fondo
|
||||||
|
6. ✓ Sonidos
|
||||||
|
7. ✓ Música de fondo
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. BARRA SUPERIOR
|
||||||
|
|
||||||
|
### Elementos (Left to Right)
|
||||||
|
|
||||||
|
1. **Logo StreamYard** - Clickeable, va al home
|
||||||
|
2. **Título Broadcast**: "Transmision"
|
||||||
|
3. **Botón "Agregar destino"**
|
||||||
|
- Background: `#3b82f6`
|
||||||
|
- Icons: YouTube, Facebook, etc.
|
||||||
|
- Border-radius: `6px`
|
||||||
|
4. **Status Indicators**
|
||||||
|
5. **Botones de Acción**:
|
||||||
|
- Cancelar
|
||||||
|
- Reiniciar
|
||||||
|
- Pausa
|
||||||
|
- Grabar (con efecto de "recording")
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. BARRA INFERIOR - CONTROLES PRINCIPALES
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────┐
|
||||||
|
│ [🎤] [📹] [🖥️] [➕] [⚙️] [🚪 Salir] [❓] │
|
||||||
|
└────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Botones (de izquierda a derecha):
|
||||||
|
|
||||||
|
1. **Micrófono**
|
||||||
|
- Icon: `🎤`
|
||||||
|
- Toggle state: On/Off
|
||||||
|
- Color On: `#10b981`
|
||||||
|
- Color Off: `#ef4444`
|
||||||
|
|
||||||
|
2. **Cámara**
|
||||||
|
- Icon: `📹`
|
||||||
|
- Similar al micrófono
|
||||||
|
|
||||||
|
3. **Compartir Pantalla**
|
||||||
|
- Icon: `🖥️`
|
||||||
|
- Background: `#2a2a2a`
|
||||||
|
|
||||||
|
4. **Agregar Invitados**
|
||||||
|
- Icon: `➕`
|
||||||
|
- Background: `#2a2a2a`
|
||||||
|
|
||||||
|
5. **Configuración**
|
||||||
|
- Icon: `⚙️`
|
||||||
|
- Background: `#2a2a2a`
|
||||||
|
|
||||||
|
6. **Salir del Estudio** (derecha)
|
||||||
|
- Background: `#ef4444`
|
||||||
|
- Color: `#ffffff`
|
||||||
|
- Border-radius: `6px`
|
||||||
|
|
||||||
|
7. **Ayuda** (extremo derecho)
|
||||||
|
- Icon: `❓`
|
||||||
|
- Background: transparent
|
||||||
|
- Border: `1px solid #404040`
|
||||||
|
|
||||||
|
### Características Comunes de Botones
|
||||||
|
- **Tamaño**: `48x48px` (botones principales)
|
||||||
|
- **Border-radius**: `8px`
|
||||||
|
- **Gap**: `8px`
|
||||||
|
- **Hover**: Brightness +10%
|
||||||
|
- **Active**: Background `#333333`
|
||||||
|
- **Transition**: `all 150ms ease`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. TIPOGRAFÍA
|
||||||
|
|
||||||
|
### Font Family
|
||||||
|
```css
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
|
||||||
|
'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
|
||||||
|
'Helvetica Neue', sans-serif;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tamaños
|
||||||
|
- **Títulos principales**: `18-20px`, weight `600`
|
||||||
|
- **Subtítulos**: `16px`, weight `500`
|
||||||
|
- **Texto normal**: `14px`, weight `400`
|
||||||
|
- **Texto pequeño**: `12px`, weight `400`
|
||||||
|
- **Labels**: `13px`, weight `500`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. ESPACIADO Y PADDING
|
||||||
|
|
||||||
|
### Sistema de Espaciado
|
||||||
|
- **xs**: `4px`
|
||||||
|
- **sm**: `8px`
|
||||||
|
- **md**: `12px`
|
||||||
|
- **lg**: `16px`
|
||||||
|
- **xl**: `24px`
|
||||||
|
- **2xl**: `32px`
|
||||||
|
|
||||||
|
### Padding de Contenedores
|
||||||
|
- **Paneles laterales**: `16px`
|
||||||
|
- **Cards**: `12px 16px`
|
||||||
|
- **Botones**: `8px 16px` (small), `12px 24px` (medium)
|
||||||
|
- **Input fields**: `10px 12px`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. BORDER RADIUS
|
||||||
|
|
||||||
|
- **Pequeño**: `4px` (badges, tags)
|
||||||
|
- **Medio**: `6px` (botones, inputs)
|
||||||
|
- **Grande**: `8px` (cards, modales)
|
||||||
|
- **Muy grande**: `12px` (containers principales)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. SOMBRAS
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Sombra sutil para elevación */
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||||
|
|
||||||
|
/* Sombra media para popups */
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.4);
|
||||||
|
|
||||||
|
/* Sombra fuerte para modales */
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. TRANSICIONES Y ANIMACIONES
|
||||||
|
|
||||||
|
### Transiciones Estándar
|
||||||
|
```css
|
||||||
|
transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Animaciones Específicas
|
||||||
|
- **Hover en botones**: `transform: scale(1.02)`
|
||||||
|
- **Active en botones**: `transform: scale(0.98)`
|
||||||
|
- **Fade in**: `opacity 200ms ease-in`
|
||||||
|
- **Slide in**: `transform 250ms ease-out`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. ESTADOS INTERACTIVOS
|
||||||
|
|
||||||
|
### Botones
|
||||||
|
```css
|
||||||
|
/* Default */
|
||||||
|
background: #2a2a2a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
|
||||||
|
/* Hover */
|
||||||
|
background: #333333;
|
||||||
|
|
||||||
|
/* Active */
|
||||||
|
background: #3b82f6;
|
||||||
|
color: #ffffff;
|
||||||
|
|
||||||
|
/* Disabled */
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #666666;
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Inputs
|
||||||
|
```css
|
||||||
|
/* Default */
|
||||||
|
background: #242424;
|
||||||
|
border: 1px solid #404040;
|
||||||
|
color: #e0e0e0;
|
||||||
|
|
||||||
|
/* Focus */
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
|
||||||
|
/* Error */
|
||||||
|
border-color: #ef4444;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. ICONOGRAFÍA
|
||||||
|
|
||||||
|
### Fuente de Íconos
|
||||||
|
Se utilizan principalmente:
|
||||||
|
- **Material Icons** o similar
|
||||||
|
- **SVG custom** para algunos elementos
|
||||||
|
|
||||||
|
### Tamaños de Íconos
|
||||||
|
- **Pequeño**: `16px`
|
||||||
|
- **Medio**: `20px`
|
||||||
|
- **Grande**: `24px`
|
||||||
|
- **Muy grande**: `32px`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. COMPONENTES REUTILIZABLES IDENTIFICADOS
|
||||||
|
|
||||||
|
### Para Avanza-UI
|
||||||
|
|
||||||
|
1. **StudioLayout**
|
||||||
|
- Layout de 3 columnas
|
||||||
|
- Responsive
|
||||||
|
- Paneles colapsables
|
||||||
|
|
||||||
|
2. **SceneCard**
|
||||||
|
- Miniatura con preview
|
||||||
|
- Título
|
||||||
|
- Botón de acción
|
||||||
|
- Estado (activa/inactiva)
|
||||||
|
|
||||||
|
3. **ControlButton**
|
||||||
|
- Botón con ícono
|
||||||
|
- Toggle state
|
||||||
|
- Variantes: primary, secondary, danger
|
||||||
|
|
||||||
|
4. **ParticipantCard**
|
||||||
|
- Avatar
|
||||||
|
- Nombre
|
||||||
|
- Estado (audio/video)
|
||||||
|
- Menú de opciones
|
||||||
|
|
||||||
|
5. **LayoutSelector**
|
||||||
|
- Grid de layouts
|
||||||
|
- Preview visual de cada layout
|
||||||
|
- Estado activo
|
||||||
|
|
||||||
|
6. **CollapsibleSection**
|
||||||
|
- Header con título e ícono
|
||||||
|
- Contenido colapsable
|
||||||
|
- Animación suave
|
||||||
|
|
||||||
|
7. **TabNavigation**
|
||||||
|
- Tabs horizontales
|
||||||
|
- Indicador de tab activo
|
||||||
|
- Scroll horizontal si necesario
|
||||||
|
|
||||||
|
8. **TopBar**
|
||||||
|
- Logo
|
||||||
|
- Título
|
||||||
|
- Acciones
|
||||||
|
- Status indicators
|
||||||
|
|
||||||
|
9. **BottomBar**
|
||||||
|
- Controles principales
|
||||||
|
- Botones de acción
|
||||||
|
- Layout flexible
|
||||||
|
|
||||||
|
10. **VideoCanvas**
|
||||||
|
- Área de preview 16:9
|
||||||
|
- Overlays
|
||||||
|
- Responsive
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. MEJORAS RECOMENDADAS PARA STUDIO-PANEL
|
||||||
|
|
||||||
|
### Prioridad Alta
|
||||||
|
1. ✅ Implementar esquema de colores oscuro consistente
|
||||||
|
2. ✅ Crear componentes de botones con estados correctos
|
||||||
|
3. ✅ Layout de 3 columnas responsive
|
||||||
|
4. ✅ Sistema de tabs lateral derecho
|
||||||
|
5. ✅ Canvas central con aspect ratio 16:9
|
||||||
|
|
||||||
|
### Prioridad Media
|
||||||
|
6. Animaciones y transiciones suaves
|
||||||
|
7. Estados hover/active/disabled consistentes
|
||||||
|
8. Sistema de iconos unificado
|
||||||
|
9. Sombras y elevaciones correctas
|
||||||
|
10. Tipografía mejorada
|
||||||
|
|
||||||
|
### Prioridad Baja
|
||||||
|
11. Temas personalizables
|
||||||
|
12. Accesibilidad mejorada
|
||||||
|
13. Atajos de teclado
|
||||||
|
14. Tooltips informativos
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 16. VARIABLES CSS PROPUESTAS
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Colors */
|
||||||
|
--studio-bg-primary: #0f0f0f;
|
||||||
|
--studio-bg-secondary: #1a1a1a;
|
||||||
|
--studio-bg-tertiary: #242424;
|
||||||
|
--studio-border: #333333;
|
||||||
|
--studio-border-light: #404040;
|
||||||
|
|
||||||
|
--studio-text-primary: #ffffff;
|
||||||
|
--studio-text-secondary: #e0e0e0;
|
||||||
|
--studio-text-muted: #999999;
|
||||||
|
|
||||||
|
--studio-accent: #3b82f6;
|
||||||
|
--studio-success: #10b981;
|
||||||
|
--studio-warning: #f59e0b;
|
||||||
|
--studio-danger: #ef4444;
|
||||||
|
|
||||||
|
/* Spacing */
|
||||||
|
--studio-space-xs: 4px;
|
||||||
|
--studio-space-sm: 8px;
|
||||||
|
--studio-space-md: 12px;
|
||||||
|
--studio-space-lg: 16px;
|
||||||
|
--studio-space-xl: 24px;
|
||||||
|
--studio-space-2xl: 32px;
|
||||||
|
|
||||||
|
/* Border Radius */
|
||||||
|
--studio-radius-sm: 4px;
|
||||||
|
--studio-radius-md: 6px;
|
||||||
|
--studio-radius-lg: 8px;
|
||||||
|
--studio-radius-xl: 12px;
|
||||||
|
|
||||||
|
/* Shadows */
|
||||||
|
--studio-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||||
|
--studio-shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4);
|
||||||
|
--studio-shadow-lg: 0 10px 25px rgba(0, 0, 0, 0.5);
|
||||||
|
|
||||||
|
/* Transitions */
|
||||||
|
--studio-transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 17. NOTAS ADICIONALES
|
||||||
|
|
||||||
|
- La interfaz es muy limpia y minimalista
|
||||||
|
- Uso extensivo de grises oscuros para reducir fatiga visual
|
||||||
|
- Jerarquía visual clara con tamaños y pesos de fuente
|
||||||
|
- Espaciado generoso para mejorar legibilidad
|
||||||
|
- Uso consistente de border-radius para suavizar esquinas
|
||||||
|
- Feedback visual inmediato en todas las interacciones
|
||||||
|
- Prioriza la funcionalidad sobre decoración excesiva
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 18. PRÓXIMOS PASOS
|
||||||
|
|
||||||
|
1. Crear archivo de variables CSS para Avanza-UI
|
||||||
|
2. Implementar componentes base con estos estilos
|
||||||
|
3. Actualizar Studio-Panel con los nuevos componentes
|
||||||
|
4. Crear storybook/documentación de componentes
|
||||||
|
5. Pruebas de usabilidad y ajustes finales
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Análisis completado el**: 2025-11-11
|
||||||
|
**Analista**: GitHub Copilot
|
||||||
|
**Versión**: 1.0
|
||||||
|
|
||||||
373
docs/STREAMYARD_UI_ANALYSIS.md
Normal file
373
docs/STREAMYARD_UI_ANALYSIS.md
Normal file
@ -0,0 +1,373 @@
|
|||||||
|
- ✅ **Jerarquía clara** mediante tamaños y pesos tipográficos
|
||||||
|
- ✅ **Accesibilidad** bien implementada
|
||||||
|
- ✅ **Feedback visual** inmediato en interacciones
|
||||||
|
- ✅ **Espaciado generoso** para respiración visual
|
||||||
|
- ✅ **Componentes consistentes** y reutilizables
|
||||||
|
|
||||||
|
**Para Avanza-UI**: Adoptar estos principios garantizará una interfaz profesional, moderna y fácil de usar similar a StreamYard, manteniendo nuestra propia identidad visual.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PRÓXIMOS PASOS
|
||||||
|
|
||||||
|
1. Actualizar design tokens en Avanza-UI basándose en este análisis
|
||||||
|
2. Crear componentes StudioButton, StudioInput, StudioCard
|
||||||
|
3. Implementar tema oscuro por defecto
|
||||||
|
4. Documentar patrones de uso
|
||||||
|
5. Crear Storybook con ejemplos visuales
|
||||||
|
|
||||||
|
# Análisis UI/UX de StreamYard Studio
|
||||||
|
|
||||||
|
## Fecha de Análisis
|
||||||
|
11 de Noviembre de 2025
|
||||||
|
|
||||||
|
## URL Analizada
|
||||||
|
https://streamyard.com/j9smzza87q
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. PALETA DE COLORES
|
||||||
|
|
||||||
|
### Colores Principales Observados:
|
||||||
|
- **Fondo Principal**: Gris muy oscuro/Negro (#1a1a1a aproximadamente)
|
||||||
|
- **Superficies Elevadas**: Gris oscuro (#2a2a2a - #353535)
|
||||||
|
- **Texto Primario**: Blanco (#ffffff)
|
||||||
|
- **Texto Secundario**: Gris claro (#b0b0b0)
|
||||||
|
- **Accent/Primary**: Azul (#4a90e2 aproximadamente)
|
||||||
|
- **Botón Primario**: Color vibrante para CTAs principales
|
||||||
|
|
||||||
|
### Esquema de Color:
|
||||||
|
- **Tema**: Oscuro (Dark Mode nativo)
|
||||||
|
- **Contraste**: Alto contraste para legibilidad
|
||||||
|
- **Jerarquía Visual**: Clara diferenciación entre elementos mediante tonos de gris
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. TIPOGRAFÍA
|
||||||
|
|
||||||
|
### Características:
|
||||||
|
- **Familia**: Sans-serif moderna (probablemente system font o similar a Inter/Roboto)
|
||||||
|
- **Pesos Observados**:
|
||||||
|
- Regular (400) para texto normal
|
||||||
|
- Medium (500) para labels
|
||||||
|
- Semi-bold (600) para headings
|
||||||
|
- **Tamaños**:
|
||||||
|
- Headings: ~20-24px
|
||||||
|
- Body text: ~14-16px
|
||||||
|
- Secondary text: ~12-14px
|
||||||
|
|
||||||
|
### Jerarquía Tipográfica:
|
||||||
|
- **H3 "Configura tu estudio"**: Claro, prominente
|
||||||
|
- **Párrafos informativos**: Tamaño moderado, buen espaciado
|
||||||
|
- **Labels de botones**: Legibles, peso adecuado
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. COMPONENTES UI PRINCIPALES
|
||||||
|
|
||||||
|
### 3.1 Header/Banner
|
||||||
|
```yaml
|
||||||
|
Características:
|
||||||
|
- Logo: Alineado a la izquierda
|
||||||
|
- Altura: ~60-80px
|
||||||
|
- Fondo: Sólido oscuro
|
||||||
|
- Contenido: Logo + botón "Skip to content" (accesibilidad)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Cards/Superficies
|
||||||
|
```yaml
|
||||||
|
Características:
|
||||||
|
- Bordes: Redondeados (border-radius: 8-12px)
|
||||||
|
- Sombras: Sutiles (box-shadow suave)
|
||||||
|
- Padding: Generoso (~20-30px)
|
||||||
|
- Fondo: Gris oscuro elevado
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Botones
|
||||||
|
|
||||||
|
#### Botón Primario ("Entrar al estudio"):
|
||||||
|
```yaml
|
||||||
|
Características:
|
||||||
|
- Tamaño: Grande, prominente
|
||||||
|
- Color: Accent color vibrante
|
||||||
|
- Border-radius: ~8px
|
||||||
|
- Padding: Vertical ~12-16px, Horizontal ~24-32px
|
||||||
|
- Hover: Efecto de brillo/elevación
|
||||||
|
- Font-weight: Medium/Semi-bold
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Botones Secundarios:
|
||||||
|
```yaml
|
||||||
|
Características:
|
||||||
|
- Color: Outline o ghost style
|
||||||
|
- Border: 1-2px sólido
|
||||||
|
- Fondo: Transparente o semi-transparente
|
||||||
|
- Hover: Cambio sutil de fondo
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Botones de Acción (Iconos):
|
||||||
|
```yaml
|
||||||
|
Características:
|
||||||
|
- Forma: Circular o redondeada
|
||||||
|
- Tamaño: ~40-48px
|
||||||
|
- Iconos: SVG, monocromáticos
|
||||||
|
- Hover: Feedback visual inmediato
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 Inputs/Form Fields
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
Características del TextBox "Nombre para mostrar":
|
||||||
|
- Background: Gris oscuro (#2a2a2a)
|
||||||
|
- Border: 1-2px, color sutil (#404040)
|
||||||
|
- Border-radius: ~6-8px
|
||||||
|
- Padding: ~12-16px
|
||||||
|
- Font-size: ~14-16px
|
||||||
|
- Focus state: Border accent color
|
||||||
|
- Placeholder: Gris medio (#808080)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 Secciones de Configuración
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
Audio/Video Setup:
|
||||||
|
- Layout: Vertical stack
|
||||||
|
- Spacing: Consistente (~16-24px)
|
||||||
|
- Iconografía: Clara y funcional
|
||||||
|
- Estados:
|
||||||
|
- Activo: Color accent
|
||||||
|
- Inactivo: Gris
|
||||||
|
- Hover: Feedback visual
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. LAYOUT Y ESPACIADO
|
||||||
|
|
||||||
|
### Grid System:
|
||||||
|
- **Tipo**: Flexible, centrado
|
||||||
|
- **Max-width**: ~1200-1400px para contenido principal
|
||||||
|
- **Gaps**: Consistentes (16px, 24px, 32px)
|
||||||
|
|
||||||
|
### Spacing Scale (estimado):
|
||||||
|
```css
|
||||||
|
--spacing-xs: 4px;
|
||||||
|
--spacing-sm: 8px;
|
||||||
|
--spacing-md: 16px;
|
||||||
|
--spacing-lg: 24px;
|
||||||
|
--spacing-xl: 32px;
|
||||||
|
--spacing-2xl: 48px;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Padding/Margin:
|
||||||
|
- **Cards**: 20-30px padding interno
|
||||||
|
- **Buttons**: 12-16px vertical, 24-32px horizontal
|
||||||
|
- **Form fields**: 12-16px padding
|
||||||
|
- **Between sections**: 24-48px
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. ICONOGRAFÍA
|
||||||
|
|
||||||
|
### Estilo de Iconos:
|
||||||
|
- **Tipo**: Outline/Line icons
|
||||||
|
- **Tamaño**: 20-24px típicamente
|
||||||
|
- **Color**: Monocromático (hereda del texto o tiene color propio)
|
||||||
|
- **Stroke-width**: ~2px
|
||||||
|
|
||||||
|
### Ubicaciones:
|
||||||
|
- Botones de configuración
|
||||||
|
- Estados de micrófono/cámara
|
||||||
|
- Acciones secundarias
|
||||||
|
- Información/tooltips (ícono "i")
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. ESTADOS INTERACTIVOS
|
||||||
|
|
||||||
|
### Hover Effects:
|
||||||
|
```yaml
|
||||||
|
Botones:
|
||||||
|
- Transition: smooth (~200ms)
|
||||||
|
- Transform: subtle scale o elevation
|
||||||
|
- Color: brightness aumentado
|
||||||
|
|
||||||
|
Links/Cards:
|
||||||
|
- Cursor: pointer
|
||||||
|
- Background: ligero cambio
|
||||||
|
- Border/Shadow: reforzado
|
||||||
|
```
|
||||||
|
|
||||||
|
### Focus States:
|
||||||
|
```yaml
|
||||||
|
Accessibility:
|
||||||
|
- Outline: Visible en elementos interactivos
|
||||||
|
- Color: Accent color
|
||||||
|
- Width: 2-3px
|
||||||
|
- Offset: 2-4px
|
||||||
|
```
|
||||||
|
|
||||||
|
### Active/Pressed:
|
||||||
|
```yaml
|
||||||
|
Feedback:
|
||||||
|
- Transform: scale(0.98)
|
||||||
|
- Brightness: reducida
|
||||||
|
- Duration: ~100ms
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. ACCESIBILIDAD
|
||||||
|
|
||||||
|
### Características Observadas:
|
||||||
|
1. **Botón "Skip to content"**: Presente para navegación por teclado
|
||||||
|
2. **Contraste**: Alto contraste texto/fondo
|
||||||
|
3. **Labels**: Textos descriptivos en elementos de forma
|
||||||
|
4. **Estructura semántica**: Headers, buttons correctamente etiquetados
|
||||||
|
5. **Focus visible**: Estados de foco claros
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. MICROINTERACCIONES
|
||||||
|
|
||||||
|
### Animaciones Sutiles:
|
||||||
|
- **Transiciones**: Suaves (200-300ms)
|
||||||
|
- **Easing**: cubic-bezier ease-out
|
||||||
|
- **Elementos animados**:
|
||||||
|
- Hover en botones
|
||||||
|
- Focus rings
|
||||||
|
- Carga de componentes (fade-in)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. RESPONSIVE/ADAPTIVE DESIGN
|
||||||
|
|
||||||
|
### Consideraciones:
|
||||||
|
- **Mobile-first approach** probable
|
||||||
|
- **Breakpoints estimados**:
|
||||||
|
- Mobile: < 768px
|
||||||
|
- Tablet: 768px - 1024px
|
||||||
|
- Desktop: > 1024px
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. COMPONENTES ESPECÍFICOS DE VIDEO STUDIO
|
||||||
|
|
||||||
|
### Avatar/Profile Display:
|
||||||
|
```yaml
|
||||||
|
Características:
|
||||||
|
- Forma: Circular
|
||||||
|
- Tamaño: Variable según contexto
|
||||||
|
- Border: Sutil
|
||||||
|
- Placeholder: Ícono de usuario cuando no hay imagen
|
||||||
|
```
|
||||||
|
|
||||||
|
### Device Selectors (Mic/Camera):
|
||||||
|
```yaml
|
||||||
|
Características:
|
||||||
|
- Dropdown style
|
||||||
|
- Iconos descriptivos
|
||||||
|
- Estados visuales claros (muted/active)
|
||||||
|
- Feedback inmediato de cambios
|
||||||
|
```
|
||||||
|
|
||||||
|
### Settings Button:
|
||||||
|
```yaml
|
||||||
|
Características:
|
||||||
|
- Ícono de engranaje/configuración
|
||||||
|
- Posición: Accesible pero no intrusiva
|
||||||
|
- Hover: Efecto de rotación sutil (opcional)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. RECOMENDACIONES PARA AVANZA-UI
|
||||||
|
|
||||||
|
### Para Implementar en Avanza-UI:
|
||||||
|
|
||||||
|
1. **Sistema de Design Tokens**:
|
||||||
|
```typescript
|
||||||
|
// colors.ts
|
||||||
|
export const colors = {
|
||||||
|
background: {
|
||||||
|
primary: '#1a1a1a',
|
||||||
|
secondary: '#2a2a2a',
|
||||||
|
elevated: '#353535',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
primary: '#ffffff',
|
||||||
|
secondary: '#b0b0b0',
|
||||||
|
muted: '#808080',
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
primary: '#4a90e2',
|
||||||
|
hover: '#5a9ff2',
|
||||||
|
},
|
||||||
|
border: {
|
||||||
|
default: '#404040',
|
||||||
|
focus: '#4a90e2',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// spacing.ts
|
||||||
|
export const spacing = {
|
||||||
|
xs: '4px',
|
||||||
|
sm: '8px',
|
||||||
|
md: '16px',
|
||||||
|
lg: '24px',
|
||||||
|
xl: '32px',
|
||||||
|
'2xl': '48px',
|
||||||
|
};
|
||||||
|
|
||||||
|
// typography.ts
|
||||||
|
export const typography = {
|
||||||
|
fontFamily: 'system-ui, -apple-system, sans-serif',
|
||||||
|
fontSize: {
|
||||||
|
xs: '12px',
|
||||||
|
sm: '14px',
|
||||||
|
base: '16px',
|
||||||
|
lg: '18px',
|
||||||
|
xl: '20px',
|
||||||
|
'2xl': '24px',
|
||||||
|
},
|
||||||
|
fontWeight: {
|
||||||
|
regular: 400,
|
||||||
|
medium: 500,
|
||||||
|
semibold: 600,
|
||||||
|
bold: 700,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// borderRadius.ts
|
||||||
|
export const borderRadius = {
|
||||||
|
sm: '4px',
|
||||||
|
md: '8px',
|
||||||
|
lg: '12px',
|
||||||
|
full: '9999px',
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Componentes Base Mejorados**:
|
||||||
|
- `<Button />` con variants: primary, secondary, ghost, icon
|
||||||
|
- `<Input />` con estados focus/error mejorados
|
||||||
|
- `<Card />` con elevación consistente
|
||||||
|
- `<Avatar />` circular con placeholders
|
||||||
|
- `<IconButton />` para acciones rápidas
|
||||||
|
|
||||||
|
3. **Theme Provider**:
|
||||||
|
- Soporte para Dark/Light mode
|
||||||
|
- Variables CSS para cambios dinámicos
|
||||||
|
- Context API para tematización
|
||||||
|
|
||||||
|
4. **Animaciones Consistentes**:
|
||||||
|
- Librería de transiciones reutilizables
|
||||||
|
- Timing functions estandarizadas
|
||||||
|
- Micro-interacciones predefinidas
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CONCLUSIÓN
|
||||||
|
|
||||||
|
StreamYard utiliza un diseño **oscuro, minimalista y funcional** que prioriza:
|
||||||
|
- ✅ **Claridad visual** con alto contraste
|
||||||
|
|
||||||
312
package-lock.json
generated
312
package-lock.json
generated
@ -6042,301 +6042,6 @@
|
|||||||
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1"
|
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/node": {
|
|
||||||
"version": "4.1.17",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz",
|
|
||||||
"integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jridgewell/remapping": "^2.3.4",
|
|
||||||
"enhanced-resolve": "^5.18.3",
|
|
||||||
"jiti": "^2.6.1",
|
|
||||||
"lightningcss": "1.30.2",
|
|
||||||
"magic-string": "^0.30.21",
|
|
||||||
"source-map-js": "^1.2.1",
|
|
||||||
"tailwindcss": "4.1.17"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tailwindcss/node/node_modules/jiti": {
|
|
||||||
"version": "2.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
|
||||||
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"jiti": "lib/jiti-cli.mjs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tailwindcss/node/node_modules/tailwindcss": {
|
|
||||||
"version": "4.1.17",
|
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",
|
|
||||||
"integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@tailwindcss/oxide": {
|
|
||||||
"version": "4.1.17",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz",
|
|
||||||
"integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@tailwindcss/oxide-android-arm64": "4.1.17",
|
|
||||||
"@tailwindcss/oxide-darwin-arm64": "4.1.17",
|
|
||||||
"@tailwindcss/oxide-darwin-x64": "4.1.17",
|
|
||||||
"@tailwindcss/oxide-freebsd-x64": "4.1.17",
|
|
||||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17",
|
|
||||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.17",
|
|
||||||
"@tailwindcss/oxide-linux-arm64-musl": "4.1.17",
|
|
||||||
"@tailwindcss/oxide-linux-x64-gnu": "4.1.17",
|
|
||||||
"@tailwindcss/oxide-linux-x64-musl": "4.1.17",
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi": "4.1.17",
|
|
||||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.17",
|
|
||||||
"@tailwindcss/oxide-win32-x64-msvc": "4.1.17"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tailwindcss/oxide-android-arm64": {
|
|
||||||
"version": "4.1.17",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz",
|
|
||||||
"integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
|
||||||
"version": "4.1.17",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz",
|
|
||||||
"integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
|
||||||
"version": "4.1.17",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz",
|
|
||||||
"integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
|
||||||
"version": "4.1.17",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz",
|
|
||||||
"integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"freebsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
|
||||||
"version": "4.1.17",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz",
|
|
||||||
"integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
|
||||||
"version": "4.1.17",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz",
|
|
||||||
"integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
|
||||||
"version": "4.1.17",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz",
|
|
||||||
"integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
|
||||||
"version": "4.1.17",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz",
|
|
||||||
"integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
|
||||||
"version": "4.1.17",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz",
|
|
||||||
"integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
|
||||||
"version": "4.1.17",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz",
|
|
||||||
"integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==",
|
|
||||||
"bundleDependencies": [
|
|
||||||
"@napi-rs/wasm-runtime",
|
|
||||||
"@emnapi/core",
|
|
||||||
"@emnapi/runtime",
|
|
||||||
"@tybys/wasm-util",
|
|
||||||
"@emnapi/wasi-threads",
|
|
||||||
"tslib"
|
|
||||||
],
|
|
||||||
"cpu": [
|
|
||||||
"wasm32"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@emnapi/core": "^1.6.0",
|
|
||||||
"@emnapi/runtime": "^1.6.0",
|
|
||||||
"@emnapi/wasi-threads": "^1.1.0",
|
|
||||||
"@napi-rs/wasm-runtime": "^1.0.7",
|
|
||||||
"@tybys/wasm-util": "^0.10.1",
|
|
||||||
"tslib": "^2.4.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
|
||||||
"version": "4.1.17",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz",
|
|
||||||
"integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
|
||||||
"version": "4.1.17",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz",
|
|
||||||
"integrity": "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tailwindcss/postcss": {
|
|
||||||
"version": "4.1.17",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.17.tgz",
|
|
||||||
"integrity": "sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@alloc/quick-lru": "^5.2.0",
|
|
||||||
"@tailwindcss/node": "4.1.17",
|
|
||||||
"@tailwindcss/oxide": "4.1.17",
|
|
||||||
"postcss": "^8.4.41",
|
|
||||||
"tailwindcss": "4.1.17"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tailwindcss/postcss/node_modules/tailwindcss": {
|
|
||||||
"version": "4.1.17",
|
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",
|
|
||||||
"integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@tailwindcss/typography": {
|
"node_modules/@tailwindcss/typography": {
|
||||||
"version": "0.5.9",
|
"version": "0.5.9",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.9.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.9.tgz",
|
||||||
@ -11258,6 +10963,8 @@
|
|||||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@ -17552,6 +17259,8 @@
|
|||||||
"integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
|
"integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"detect-libc": "^2.0.3"
|
"detect-libc": "^2.0.3"
|
||||||
},
|
},
|
||||||
@ -32409,31 +32118,22 @@
|
|||||||
},
|
},
|
||||||
"packages/studio-panel": {
|
"packages/studio-panel": {
|
||||||
"name": "@avanzacast/studio-panel",
|
"name": "@avanzacast/studio-panel",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"avanza-ui": "file:../ui-components",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.17",
|
|
||||||
"@testing-library/jest-dom": "^6.0.0",
|
"@testing-library/jest-dom": "^6.0.0",
|
||||||
"@testing-library/react": "^14.0.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
"@testing-library/user-event": "^14.4.3",
|
"@testing-library/user-event": "^14.4.3",
|
||||||
"@vitejs/plugin-react": "^4.0.0",
|
"@vitejs/plugin-react": "^4.0.0",
|
||||||
"autoprefixer": "^10.4.0",
|
|
||||||
"postcss": "^8.4.0",
|
|
||||||
"tailwindcss": "^4.1.17",
|
|
||||||
"typescript": "^5.5.0",
|
"typescript": "^5.5.0",
|
||||||
"vite": "^4.1.0",
|
"vite": "^4.1.0",
|
||||||
"vitest": "^1.1.8"
|
"vitest": "^1.1.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/studio-panel/node_modules/tailwindcss": {
|
|
||||||
"version": "4.1.17",
|
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",
|
|
||||||
"integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"packages/ui-components": {
|
"packages/ui-components": {
|
||||||
"name": "avanza-ui",
|
"name": "avanza-ui",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
|||||||
227
packages/avanza-ui/README.md
Normal file
227
packages/avanza-ui/README.md
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
# Avanza-UI
|
||||||
|
|
||||||
|
Biblioteca de componentes React personalizados para AvanzaCast, basada en el diseño de StreamYard con estilos propios sin dependencias de frameworks CSS.
|
||||||
|
|
||||||
|
## Características
|
||||||
|
|
||||||
|
- ✅ **Sin dependencias de CSS frameworks** - Estilos propios y personalizados
|
||||||
|
- ✅ **Basado en StreamYard** - Diseño moderno y profesional
|
||||||
|
- ✅ **TypeScript** - Tipado completo
|
||||||
|
- ✅ **Tema oscuro** - Optimizado para reducir fatiga visual
|
||||||
|
- ✅ **Accesible** - Componentes accesibles por defecto
|
||||||
|
- ✅ **Reutilizable** - Se puede importar desde cualquier package
|
||||||
|
|
||||||
|
## Instalación
|
||||||
|
|
||||||
|
Como esta es una librería local dentro del monorepo, simplemente impórtala en tu package:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"avanza-ui": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Uso Básico
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Button } from 'avanza-ui';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div className="studio-theme">
|
||||||
|
<Button variant="primary">Click me</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Importante:** Asegúrate de envolver tu aplicación con la clase `studio-theme` para aplicar los estilos correctamente.
|
||||||
|
|
||||||
|
## Componentes Disponibles
|
||||||
|
|
||||||
|
### Button
|
||||||
|
|
||||||
|
Botón personalizable con múltiples variantes y tamaños.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Button } from 'avanza-ui';
|
||||||
|
|
||||||
|
// Variantes
|
||||||
|
<Button variant="primary">Primary</Button>
|
||||||
|
<Button variant="secondary">Secondary</Button>
|
||||||
|
<Button variant="danger">Danger</Button>
|
||||||
|
<Button variant="success">Success</Button>
|
||||||
|
<Button variant="ghost">Ghost</Button>
|
||||||
|
|
||||||
|
// Tamaños
|
||||||
|
<Button size="sm">Small</Button>
|
||||||
|
<Button size="md">Medium</Button>
|
||||||
|
<Button size="lg">Large</Button>
|
||||||
|
|
||||||
|
// Con íconos
|
||||||
|
<Button leftIcon={<Icon />}>With Left Icon</Button>
|
||||||
|
<Button rightIcon={<Icon />}>With Right Icon</Button>
|
||||||
|
<Button iconOnly><Icon /></Button>
|
||||||
|
|
||||||
|
// Estados
|
||||||
|
<Button loading>Loading...</Button>
|
||||||
|
<Button disabled>Disabled</Button>
|
||||||
|
|
||||||
|
// Full width
|
||||||
|
<Button fullWidth>Full Width</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Props del Button
|
||||||
|
|
||||||
|
| Prop | Tipo | Default | Descripción |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `variant` | `'primary' \| 'secondary' \| 'danger' \| 'success' \| 'ghost'` | `'secondary'` | Variante visual del botón |
|
||||||
|
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Tamaño del botón |
|
||||||
|
| `loading` | `boolean` | `false` | Muestra spinner de carga |
|
||||||
|
| `disabled` | `boolean` | `false` | Deshabilita el botón |
|
||||||
|
| `fullWidth` | `boolean` | `false` | Ancho completo |
|
||||||
|
| `iconOnly` | `boolean` | `false` | Solo muestra ícono (sin texto) |
|
||||||
|
| `leftIcon` | `ReactNode` | - | Ícono a la izquierda |
|
||||||
|
| `rightIcon` | `ReactNode` | - | Ícono a la derecha |
|
||||||
|
|
||||||
|
## Variables CSS (Studio Theme)
|
||||||
|
|
||||||
|
Todas las variables CSS están definidas en `studio-theme.css` y pueden ser personalizadas:
|
||||||
|
|
||||||
|
### Colores
|
||||||
|
|
||||||
|
```css
|
||||||
|
--studio-bg-primary: #0f0f0f;
|
||||||
|
--studio-bg-secondary: #1a1a1a;
|
||||||
|
--studio-bg-tertiary: #242424;
|
||||||
|
--studio-accent: #3b82f6;
|
||||||
|
--studio-success: #10b981;
|
||||||
|
--studio-warning: #f59e0b;
|
||||||
|
--studio-danger: #ef4444;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Espaciado
|
||||||
|
|
||||||
|
```css
|
||||||
|
--studio-space-xs: 4px;
|
||||||
|
--studio-space-sm: 8px;
|
||||||
|
--studio-space-md: 12px;
|
||||||
|
--studio-space-lg: 16px;
|
||||||
|
--studio-space-xl: 24px;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tipografía
|
||||||
|
|
||||||
|
```css
|
||||||
|
--studio-text-xs: 11px;
|
||||||
|
--studio-text-sm: 12px;
|
||||||
|
--studio-text-base: 14px;
|
||||||
|
--studio-text-md: 16px;
|
||||||
|
--studio-text-lg: 18px;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Border Radius
|
||||||
|
|
||||||
|
```css
|
||||||
|
--studio-radius-sm: 4px;
|
||||||
|
--studio-radius-md: 6px;
|
||||||
|
--studio-radius-lg: 8px;
|
||||||
|
--studio-radius-xl: 12px;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Personalización
|
||||||
|
|
||||||
|
Puedes sobrescribir las variables CSS en tu aplicación:
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
--studio-accent: #your-color;
|
||||||
|
--studio-bg-primary: #your-bg;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Próximos Componentes
|
||||||
|
|
||||||
|
- [ ] Input
|
||||||
|
- [ ] Select
|
||||||
|
- [ ] Textarea
|
||||||
|
- [ ] Checkbox
|
||||||
|
- [ ] Radio
|
||||||
|
- [ ] Switch
|
||||||
|
- [ ] Modal
|
||||||
|
- [ ] Dropdown
|
||||||
|
- [ ] Tooltip
|
||||||
|
- [ ] Card
|
||||||
|
- [ ] Badge
|
||||||
|
- [ ] Avatar
|
||||||
|
- [ ] IconButton
|
||||||
|
- [ ] Tabs
|
||||||
|
- [ ] Panel
|
||||||
|
- [ ] Layout components (StudioLayout, TopBar, BottomBar, etc.)
|
||||||
|
|
||||||
|
## Desarrollo
|
||||||
|
|
||||||
|
### Estructura del Proyecto
|
||||||
|
|
||||||
|
```
|
||||||
|
avanza-ui/
|
||||||
|
├── src/
|
||||||
|
│ ├── components/
|
||||||
|
│ │ ├── Button/
|
||||||
|
│ │ │ ├── Button.tsx
|
||||||
|
│ │ │ ├── Button.css
|
||||||
|
│ │ │ └── index.ts
|
||||||
|
│ │ └── ... (más componentes)
|
||||||
|
│ ├── styles/
|
||||||
|
│ │ └── studio-theme.css
|
||||||
|
│ └── index.ts
|
||||||
|
├── package.json
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### Agregar un Nuevo Componente
|
||||||
|
|
||||||
|
1. Crea una carpeta en `src/components/`
|
||||||
|
2. Crea `ComponentName.tsx` con el componente React
|
||||||
|
3. Crea `ComponentName.css` con los estilos
|
||||||
|
4. Crea `index.ts` para exportar el componente
|
||||||
|
5. Actualiza `src/index.ts` para exportar desde la raíz
|
||||||
|
|
||||||
|
### Convenciones de Nomenclatura
|
||||||
|
|
||||||
|
- **Componentes**: PascalCase (ej: `Button`, `IconButton`)
|
||||||
|
- **Archivos**: PascalCase para componentes, kebab-case para estilos
|
||||||
|
- **CSS Classes**: kebab-case con prefijo `avanza-` (ej: `avanza-button`)
|
||||||
|
- **CSS Variables**: kebab-case con prefijo `--studio-` (ej: `--studio-accent`)
|
||||||
|
|
||||||
|
## Guía de Estilo
|
||||||
|
|
||||||
|
### CSS
|
||||||
|
|
||||||
|
- Usa variables CSS de `studio-theme.css` en lugar de valores hardcoded
|
||||||
|
- Sigue el patrón BEM para nombres de clases
|
||||||
|
- Agrupa propiedades relacionadas
|
||||||
|
- Usa transiciones para interacciones suaves
|
||||||
|
|
||||||
|
### TypeScript
|
||||||
|
|
||||||
|
- Exporta interfaces de props
|
||||||
|
- Usa `React.forwardRef` para componentes que necesiten refs
|
||||||
|
- Documenta props con JSDoc
|
||||||
|
- Usa tipos estrictos (evita `any`)
|
||||||
|
|
||||||
|
## Licencia
|
||||||
|
|
||||||
|
Uso interno - AvanzaCast
|
||||||
|
|
||||||
|
## Contribuidores
|
||||||
|
|
||||||
|
- Equipo AvanzaCast
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Versión:** 1.0.0
|
||||||
|
**Última actualización:** 2025-11-11
|
||||||
|
|
||||||
@ -1,30 +1,31 @@
|
|||||||
{
|
{
|
||||||
"name": "avanza-ui",
|
"name": "avanza-ui",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"description": "Biblioteca de componentes React para AvanzaCast basada en StreamYard y unificada con ui-components",
|
||||||
"description": "Sistema de componentes UI independiente - Inspirado en Tailwind CSS y Vristo",
|
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.esm.js",
|
"module": "dist/index.esm.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist",
|
||||||
|
"src"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rollup -c",
|
"build": "rollup -c",
|
||||||
"dev": "rollup -c -w",
|
"dev": "rollup -c -w",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:ui": "vitest --ui",
|
"prepublishOnly": "npm run build"
|
||||||
"type-check": "tsc --noEmit"
|
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ui",
|
|
||||||
"components",
|
|
||||||
"react",
|
"react",
|
||||||
|
"components",
|
||||||
|
"ui",
|
||||||
"avanzacast",
|
"avanzacast",
|
||||||
"design-system"
|
"studio"
|
||||||
],
|
],
|
||||||
"author": "AvanzaCast Team",
|
"author": "AvanzaCast Team",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"private": true,
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^18.0.0",
|
"react": "^18.0.0",
|
||||||
"react-dom": "^18.0.0"
|
"react-dom": "^18.0.0"
|
||||||
@ -35,8 +36,6 @@
|
|||||||
"@rollup/plugin-typescript": "^11.1.6",
|
"@rollup/plugin-typescript": "^11.1.6",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"react": "^18.3.1",
|
|
||||||
"react-dom": "^18.3.1",
|
|
||||||
"rollup": "^4.18.0",
|
"rollup": "^4.18.0",
|
||||||
"rollup-plugin-peer-deps-external": "^2.2.4",
|
"rollup-plugin-peer-deps-external": "^2.2.4",
|
||||||
"rollup-plugin-postcss": "^4.0.2",
|
"rollup-plugin-postcss": "^4.0.2",
|
||||||
@ -48,4 +47,3 @@
|
|||||||
"clsx": "^2.1.1"
|
"clsx": "^2.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
220
packages/avanza-ui/src/components/Button/Button.css
Normal file
220
packages/avanza-ui/src/components/Button/Button.css
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
/* Button Component - Studio Theme */
|
||||||
|
|
||||||
|
.avanza-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--studio-space-sm);
|
||||||
|
font-size: var(--studio-text-base);
|
||||||
|
font-weight: var(--studio-font-medium);
|
||||||
|
font-family: var(--studio-font-family);
|
||||||
|
line-height: var(--studio-leading-tight);
|
||||||
|
border-radius: var(--studio-radius-md);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--studio-transition);
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
user-select: none;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== SIZES ===== */
|
||||||
|
.avanza-button--sm {
|
||||||
|
height: var(--studio-btn-sm-height);
|
||||||
|
padding: 0 var(--studio-space-md);
|
||||||
|
font-size: var(--studio-text-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avanza-button--md {
|
||||||
|
height: var(--studio-btn-md-height);
|
||||||
|
padding: 0 var(--studio-space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avanza-button--lg {
|
||||||
|
height: var(--studio-btn-lg-height);
|
||||||
|
padding: 0 var(--studio-space-xl);
|
||||||
|
font-size: var(--studio-text-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icon Only */
|
||||||
|
.avanza-button--icon-only.avanza-button--sm {
|
||||||
|
width: var(--studio-btn-sm-height);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avanza-button--icon-only.avanza-button--md {
|
||||||
|
width: var(--studio-btn-md-height);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avanza-button--icon-only.avanza-button--lg {
|
||||||
|
width: var(--studio-btn-lg-height);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== VARIANTS ===== */
|
||||||
|
|
||||||
|
/* Primary (Accent) */
|
||||||
|
.avanza-button--primary {
|
||||||
|
background-color: var(--studio-accent);
|
||||||
|
color: var(--studio-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avanza-button--primary:hover:not(:disabled) {
|
||||||
|
background-color: var(--studio-accent-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avanza-button--primary:active:not(:disabled) {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Secondary (Default) */
|
||||||
|
.avanza-button--secondary {
|
||||||
|
background-color: var(--studio-bg-elevated);
|
||||||
|
color: var(--studio-text-secondary);
|
||||||
|
border-color: var(--studio-border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avanza-button--secondary:hover:not(:disabled) {
|
||||||
|
background-color: var(--studio-bg-hover);
|
||||||
|
border-color: var(--studio-border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avanza-button--secondary:active:not(:disabled) {
|
||||||
|
background-color: var(--studio-bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Danger */
|
||||||
|
.avanza-button--danger {
|
||||||
|
background-color: var(--studio-danger);
|
||||||
|
color: var(--studio-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avanza-button--danger:hover:not(:disabled) {
|
||||||
|
background-color: var(--studio-danger-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avanza-button--danger:active:not(:disabled) {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Success */
|
||||||
|
.avanza-button--success {
|
||||||
|
background-color: var(--studio-success);
|
||||||
|
color: var(--studio-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avanza-button--success:hover:not(:disabled) {
|
||||||
|
background-color: var(--studio-success-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avanza-button--success:active:not(:disabled) {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ghost */
|
||||||
|
.avanza-button--ghost {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--studio-text-secondary);
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avanza-button--ghost:hover:not(:disabled) {
|
||||||
|
background-color: var(--studio-bg-elevated);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avanza-button--ghost:active:not(:disabled) {
|
||||||
|
background-color: var(--studio-bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== STATES ===== */
|
||||||
|
|
||||||
|
/* Disabled */
|
||||||
|
.avanza-button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading */
|
||||||
|
.avanza-button--loading {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Full Width */
|
||||||
|
.avanza-button--full-width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== ICONS ===== */
|
||||||
|
.avanza-button__icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avanza-button__icon svg {
|
||||||
|
width: var(--studio-icon-md);
|
||||||
|
height: var(--studio-icon-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avanza-button--sm .avanza-button__icon svg {
|
||||||
|
width: var(--studio-icon-sm);
|
||||||
|
height: var(--studio-icon-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avanza-button--lg .avanza-button__icon svg {
|
||||||
|
width: var(--studio-icon-lg);
|
||||||
|
height: var(--studio-icon-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== SPINNER ===== */
|
||||||
|
.avanza-button__spinner {
|
||||||
|
position: absolute;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avanza-button__spinner-icon {
|
||||||
|
width: var(--studio-icon-md);
|
||||||
|
height: var(--studio-icon-md);
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avanza-button--sm .avanza-button__spinner-icon {
|
||||||
|
width: var(--studio-icon-sm);
|
||||||
|
height: var(--studio-icon-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avanza-button--lg .avanza-button__spinner-icon {
|
||||||
|
width: var(--studio-icon-lg);
|
||||||
|
height: var(--studio-icon-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avanza-button__spinner-circle {
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-dasharray: 50;
|
||||||
|
stroke-dashoffset: 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content */
|
||||||
|
.avanza-button__content {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avanza-button--loading .avanza-button__content {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
98
packages/avanza-ui/src/components/Button/Button.tsx
Normal file
98
packages/avanza-ui/src/components/Button/Button.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import './Button.css';
|
||||||
|
|
||||||
|
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
/** Variante del botón */
|
||||||
|
variant?: 'primary' | 'secondary' | 'danger' | 'success' | 'ghost';
|
||||||
|
/** Tamaño del botón */
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
/** Mostrar estado de carga */
|
||||||
|
loading?: boolean;
|
||||||
|
/** Deshabilitar botón */
|
||||||
|
disabled?: boolean;
|
||||||
|
/** Ancho completo */
|
||||||
|
fullWidth?: boolean;
|
||||||
|
/** Solo ícono */
|
||||||
|
iconOnly?: boolean;
|
||||||
|
/** Ícono a la izquierda */
|
||||||
|
leftIcon?: React.ReactNode;
|
||||||
|
/** Ícono a la derecha */
|
||||||
|
rightIcon?: React.ReactNode;
|
||||||
|
/** Clase CSS adicional */
|
||||||
|
className?: string;
|
||||||
|
/** Hijos del componente */
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
variant = 'secondary',
|
||||||
|
size = 'md',
|
||||||
|
loading = false,
|
||||||
|
disabled = false,
|
||||||
|
fullWidth = false,
|
||||||
|
iconOnly = false,
|
||||||
|
leftIcon,
|
||||||
|
rightIcon,
|
||||||
|
className = '',
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const classNames = [
|
||||||
|
'avanza-button',
|
||||||
|
`avanza-button--${variant}`,
|
||||||
|
`avanza-button--${size}`,
|
||||||
|
fullWidth && 'avanza-button--full-width',
|
||||||
|
iconOnly && 'avanza-button--icon-only',
|
||||||
|
loading && 'avanza-button--loading',
|
||||||
|
className,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
const isDisabled = disabled || loading;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
ref={ref}
|
||||||
|
className={classNames}
|
||||||
|
disabled={isDisabled}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{loading && (
|
||||||
|
<span className="avanza-button__spinner">
|
||||||
|
<svg className="avanza-button__spinner-icon" viewBox="0 0 24 24">
|
||||||
|
<circle
|
||||||
|
className="avanza-button__spinner-circle"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
fill="none"
|
||||||
|
strokeWidth="3"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{!loading && leftIcon && (
|
||||||
|
<span className="avanza-button__icon avanza-button__icon--left">
|
||||||
|
{leftIcon}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{children && <span className="avanza-button__content">{children}</span>}
|
||||||
|
{!loading && rightIcon && (
|
||||||
|
<span className="avanza-button__icon avanza-button__icon--right">
|
||||||
|
{rightIcon}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Button.displayName = 'Button';
|
||||||
|
|
||||||
|
export default Button;
|
||||||
|
|
||||||
4
packages/avanza-ui/src/components/Button/index.ts
Normal file
4
packages/avanza-ui/src/components/Button/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export { Button } from './Button';
|
||||||
|
export type { ButtonProps } from './Button';
|
||||||
|
export { Button as default } from './Button';
|
||||||
|
|
||||||
80
packages/avanza-ui/src/components/ControlButton.module.css
Normal file
80
packages/avanza-ui/src/components/ControlButton.module.css
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
.controlButton {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
background: linear-gradient(135deg, var(--au-gray-700) 0%, var(--au-gray-800) 100%);
|
||||||
|
border: 2px solid var(--au-border-dark);
|
||||||
|
color: var(--au-text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--au-radius-full);
|
||||||
|
transition: all var(--au-transition-fast);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||||
|
position: relative;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
font-size: 24px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controlButton:hover:not(:disabled) {
|
||||||
|
background: linear-gradient(135deg, var(--au-gray-600) 0%, var(--au-gray-700) 100%);
|
||||||
|
border-color: var(--au-primary);
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controlButton:active:not(:disabled) {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controlButton:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controlButton.active {
|
||||||
|
background: linear-gradient(135deg, var(--au-primary) 0%, var(--au-primary-hover) 100%);
|
||||||
|
border-color: var(--au-primary);
|
||||||
|
box-shadow: 0 6px 20px rgba(79, 70, 229, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controlButton.danger {
|
||||||
|
background: linear-gradient(135deg, var(--au-danger-600) 0%, var(--au-danger-700) 100%);
|
||||||
|
border-color: var(--au-danger-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controlButton.danger:hover:not(:disabled) {
|
||||||
|
background: linear-gradient(135deg, var(--au-danger-500) 0%, var(--au-danger-600) 100%);
|
||||||
|
box-shadow: 0 6px 20px rgba(220, 38, 38, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sizes */
|
||||||
|
.sm {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controlButtonLabel {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: var(--au-font-medium);
|
||||||
|
margin-top: 2px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
55
packages/avanza-ui/src/components/ControlButton.tsx
Normal file
55
packages/avanza-ui/src/components/ControlButton.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { cn } from '../utils/helpers';
|
||||||
|
import type { ComponentBaseProps } from '../types';
|
||||||
|
import styles from './ControlButton.module.css';
|
||||||
|
|
||||||
|
export interface ControlButtonProps extends ComponentBaseProps {
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
label?: string;
|
||||||
|
active?: boolean;
|
||||||
|
danger?: boolean;
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
onClick?: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ControlButton: React.FC<ControlButtonProps> = (props) => {
|
||||||
|
const {
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
active = false,
|
||||||
|
danger = false,
|
||||||
|
size = 'md',
|
||||||
|
onClick,
|
||||||
|
disabled = false,
|
||||||
|
title,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
id,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
styles.controlButton,
|
||||||
|
styles[size],
|
||||||
|
active && styles.active,
|
||||||
|
danger && styles.danger,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
|
title={title}
|
||||||
|
style={style}
|
||||||
|
id={id}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{icon && <span>{icon}</span>}
|
||||||
|
{label && <span className={styles.controlButtonLabel}>{label}</span>}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ControlButton.displayName = 'ControlButton';
|
||||||
|
|
||||||
@ -72,11 +72,11 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
const wrapperClasses = cn(
|
const wrapperClasses = cn(
|
||||||
styles.inputWrapper,
|
styles.inputWrapper,
|
||||||
styles[size],
|
styles[size],
|
||||||
error && styles.error,
|
error ? styles.error : undefined,
|
||||||
success && styles.success,
|
success ? styles.success : undefined,
|
||||||
leftIcon && styles.withLeftIcon,
|
leftIcon ? styles.withLeftIcon : undefined,
|
||||||
rightIcon && styles.withRightIcon,
|
rightIcon ? styles.withRightIcon : undefined,
|
||||||
fullWidth && styles.fullWidth,
|
fullWidth ? styles.fullWidth : undefined,
|
||||||
className
|
className
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -136,4 +136,3 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
Input.displayName = 'Input';
|
Input.displayName = 'Input';
|
||||||
|
|
||||||
74
packages/avanza-ui/src/components/SceneCard.module.css
Normal file
74
packages/avanza-ui/src/components/SceneCard.module.css
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
.sceneCard {
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--au-radius-md);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
background: var(--au-gray-800);
|
||||||
|
transition: all var(--au-transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sceneCard:hover {
|
||||||
|
border-color: var(--au-border-medium);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sceneCard.active {
|
||||||
|
border-color: var(--au-primary);
|
||||||
|
background: var(--au-gray-700);
|
||||||
|
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sceneCardThumbnail {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
background: linear-gradient(135deg, var(--au-gray-700) 0%, var(--au-gray-800) 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sceneCardThumbnailContent {
|
||||||
|
width: 60%;
|
||||||
|
height: 60%;
|
||||||
|
border: 2px dashed var(--au-border-dark);
|
||||||
|
border-radius: var(--au-radius-sm);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--au-text-secondary);
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sceneCardIndicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sceneCardFooter {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-top: 1px solid var(--au-border-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sceneCardTitle {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: var(--au-font-semibold);
|
||||||
|
color: var(--au-text-primary);
|
||||||
|
transition: color var(--au-transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sceneCard.active .sceneCardTitle {
|
||||||
|
color: var(--au-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Drag and drop */
|
||||||
|
.sceneCard.dragging {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sceneCard.dragOver {
|
||||||
|
border-color: var(--au-success-500);
|
||||||
|
}
|
||||||
|
|
||||||
102
packages/avanza-ui/src/components/SceneCard.tsx
Normal file
102
packages/avanza-ui/src/components/SceneCard.tsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { cn } from '../utils/helpers';
|
||||||
|
import type { ComponentBaseProps } from '../types';
|
||||||
|
import { Badge } from './Badge';
|
||||||
|
import styles from './SceneCard.module.css';
|
||||||
|
|
||||||
|
export interface SceneCardProps extends ComponentBaseProps {
|
||||||
|
title: string;
|
||||||
|
preview?: React.ReactNode;
|
||||||
|
active?: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
|
draggable?: boolean;
|
||||||
|
onDragStart?: (e: React.DragEvent) => void;
|
||||||
|
onDragEnd?: (e: React.DragEvent) => void;
|
||||||
|
onDrop?: (e: React.DragEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SceneCard: React.FC<SceneCardProps> = (props) => {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
preview,
|
||||||
|
active = false,
|
||||||
|
onClick,
|
||||||
|
draggable = false,
|
||||||
|
onDragStart,
|
||||||
|
onDragEnd,
|
||||||
|
onDrop,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
id,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [isDragging, setIsDragging] = React.useState(false);
|
||||||
|
const [isDragOver, setIsDragOver] = React.useState(false);
|
||||||
|
|
||||||
|
const handleDragStart = (e: React.DragEvent) => {
|
||||||
|
setIsDragging(true);
|
||||||
|
onDragStart?.(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragEnd = (e: React.DragEvent) => {
|
||||||
|
setIsDragging(false);
|
||||||
|
onDragEnd?.(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragOver(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragLeave = () => {
|
||||||
|
setIsDragOver(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragOver(false);
|
||||||
|
onDrop?.(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
styles.sceneCard,
|
||||||
|
active && styles.active,
|
||||||
|
isDragging && styles.dragging,
|
||||||
|
isDragOver && styles.dragOver,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
onClick={onClick}
|
||||||
|
draggable={draggable}
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
style={style}
|
||||||
|
id={id}
|
||||||
|
>
|
||||||
|
<div className={styles.sceneCardThumbnail}>
|
||||||
|
{preview ? (
|
||||||
|
preview
|
||||||
|
) : (
|
||||||
|
<div className={styles.sceneCardThumbnailContent}>
|
||||||
|
📹
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{active && (
|
||||||
|
<div className={styles.sceneCardIndicator}>
|
||||||
|
<Badge variant="success" size="sm" dot />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.sceneCardFooter}>
|
||||||
|
<div className={styles.sceneCardTitle}>{title}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
SceneCard.displayName = 'SceneCard';
|
||||||
|
|
||||||
57
packages/avanza-ui/src/components/StudioHeader.module.css
Normal file
57
packages/avanza-ui/src/components/StudioHeader.module.css
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
.studioHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 60px;
|
||||||
|
padding: 0 20px;
|
||||||
|
background: linear-gradient(180deg, var(--au-gray-800) 0%, var(--au-gray-900) 100%);
|
||||||
|
border-bottom: 1px solid var(--au-border-dark);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerLeft {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerLogo {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: var(--au-radius-md);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: var(--au-font-bold);
|
||||||
|
font-size: 18px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerLogoGradient {
|
||||||
|
background: linear-gradient(135deg, var(--au-primary) 0%, var(--au-primary-hover) 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerTitle {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerTitleMain {
|
||||||
|
font-weight: var(--au-font-bold);
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--au-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerTitleSub {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--au-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerRight {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
50
packages/avanza-ui/src/components/StudioHeader.tsx
Normal file
50
packages/avanza-ui/src/components/StudioHeader.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { cn } from '../utils/helpers';
|
||||||
|
import type { ComponentBaseProps } from '../types';
|
||||||
|
import styles from './StudioHeader.module.css';
|
||||||
|
|
||||||
|
export interface StudioHeaderProps extends ComponentBaseProps {
|
||||||
|
logo?: React.ReactNode;
|
||||||
|
logoText?: string;
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
actions?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StudioHeader: React.FC<StudioHeaderProps> = (props) => {
|
||||||
|
const {
|
||||||
|
logo,
|
||||||
|
logoText = 'AC',
|
||||||
|
title = 'AvanzaCast Studio',
|
||||||
|
subtitle = 'Estudio de Transmisión',
|
||||||
|
actions,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
id,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className={cn(styles.studioHeader, className)} style={style} id={id}>
|
||||||
|
<div className={styles.headerLeft}>
|
||||||
|
{logo || (
|
||||||
|
<div className={cn(styles.headerLogo, styles.headerLogoGradient)}>
|
||||||
|
{logoText}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.headerTitle}>
|
||||||
|
<div className={styles.headerTitleMain}>{title}</div>
|
||||||
|
<div className={styles.headerTitleSub}>{subtitle}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{actions && (
|
||||||
|
<div className={styles.headerRight}>
|
||||||
|
{actions}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
StudioHeader.displayName = 'StudioHeader';
|
||||||
|
|
||||||
@ -110,7 +110,7 @@ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
{showCharacterCount && maxLength && (
|
{showCharacterCount && maxLength && (
|
||||||
<div className={cn(styles.characterCount, isAtLimit && styles.limit)}>
|
<div className={cn(styles.characterCount, isAtLimit ? styles.limit : undefined)}>
|
||||||
{charCount}/{maxLength}
|
{charCount}/{maxLength}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -130,4 +130,3 @@ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
Textarea.displayName = 'Textarea';
|
Textarea.displayName = 'Textarea';
|
||||||
|
|
||||||
129
packages/avanza-ui/src/components/VideoTile.module.css
Normal file
129
packages/avanza-ui/src/components/VideoTile.module.css
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
.videoTile {
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
background: linear-gradient(135deg, var(--au-gray-700) 0%, var(--au-gray-800) 100%);
|
||||||
|
border-radius: var(--au-radius-lg);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
transition: all var(--au-transition-fast);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoTile:hover {
|
||||||
|
border-color: var(--au-primary);
|
||||||
|
box-shadow: 0 6px 20px rgba(79, 70, 229, 0.3);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoTile.speaking {
|
||||||
|
border-color: var(--au-warning-500);
|
||||||
|
box-shadow: 0 6px 20px rgba(234, 179, 8, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoElement {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
background-color: var(--au-gray-800);
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoOverlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(to top, rgba(0,0,0,0.6) 0%, transparent 50%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoTopLeft {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
left: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoTopRight {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoBottomRight {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 12px;
|
||||||
|
right: 12px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoName {
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: var(--au-font-medium);
|
||||||
|
max-width: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoStatus {
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
font-size: 12px;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoControl {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: var(--au-radius-full);
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
transition: all var(--au-transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoControl:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoControl.muted {
|
||||||
|
background: rgba(220, 38, 38, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qualityIndicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
border-radius: var(--au-radius-md);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qualityBar {
|
||||||
|
width: 2px;
|
||||||
|
background: var(--au-success-500);
|
||||||
|
border-radius: var(--au-radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qualityBar.inactive {
|
||||||
|
background: var(--au-gray-600);
|
||||||
|
}
|
||||||
|
|
||||||
160
packages/avanza-ui/src/components/VideoTile.tsx
Normal file
160
packages/avanza-ui/src/components/VideoTile.tsx
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import { cn } from '../utils/helpers';
|
||||||
|
import type { ComponentBaseProps } from '../types';
|
||||||
|
import { Avatar } from './Avatar';
|
||||||
|
import { Dropdown, DropdownItem } from './Dropdown';
|
||||||
|
import styles from './VideoTile.module.css';
|
||||||
|
|
||||||
|
export type ConnectionQuality = 'excellent' | 'good' | 'poor' | 'lost';
|
||||||
|
|
||||||
|
export interface VideoTileProps extends ComponentBaseProps {
|
||||||
|
name: string;
|
||||||
|
stream?: MediaStream | null;
|
||||||
|
muted?: boolean;
|
||||||
|
isLocal?: boolean;
|
||||||
|
isSpeaking?: boolean;
|
||||||
|
connectionQuality?: ConnectionQuality;
|
||||||
|
onToggleMute?: () => void;
|
||||||
|
onToggleCamera?: () => void;
|
||||||
|
onRemove?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VideoTile: React.FC<VideoTileProps> = (props) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
stream = null,
|
||||||
|
muted = false,
|
||||||
|
isLocal = false,
|
||||||
|
isSpeaking = false,
|
||||||
|
connectionQuality = 'good',
|
||||||
|
onToggleMute,
|
||||||
|
onToggleCamera,
|
||||||
|
onRemove,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
id,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const vid = videoRef.current;
|
||||||
|
if (!vid) return;
|
||||||
|
if (stream) {
|
||||||
|
try {
|
||||||
|
vid.srcObject = stream;
|
||||||
|
const p = vid.play();
|
||||||
|
if (p && typeof p.then === 'function') p.catch(() => {});
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
vid.srcObject = null;
|
||||||
|
}
|
||||||
|
}, [stream]);
|
||||||
|
|
||||||
|
const QualityIndicator = ({ quality }: { quality: ConnectionQuality }) => {
|
||||||
|
const levels = { excellent: 4, good: 3, poor: 2, lost: 0 };
|
||||||
|
const filled = levels[quality];
|
||||||
|
return (
|
||||||
|
<div className={styles.qualityIndicator}>
|
||||||
|
{Array.from({ length: 4 }).map((_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={cn(styles.qualityBar, i >= filled && styles.inactive)}
|
||||||
|
style={{ height: `${6 + i * 4}px` }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
styles.videoTile,
|
||||||
|
isSpeaking && styles.speaking,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
style={style}
|
||||||
|
id={id}
|
||||||
|
>
|
||||||
|
<video
|
||||||
|
ref={videoRef}
|
||||||
|
className={styles.videoElement}
|
||||||
|
playsInline
|
||||||
|
muted={muted || isLocal}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.videoOverlay} />
|
||||||
|
|
||||||
|
<div className={styles.videoTopLeft}>
|
||||||
|
<Avatar initials={name[0]} size="sm" status={isSpeaking ? 'busy' : 'online'} />
|
||||||
|
<div>
|
||||||
|
<div className={styles.videoName}>
|
||||||
|
{name} {isLocal && '(Tú)'}
|
||||||
|
</div>
|
||||||
|
<div className={styles.videoStatus}>
|
||||||
|
{muted ? 'Silenciado' : 'En vivo'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.videoTopRight}>
|
||||||
|
<QualityIndicator quality={connectionQuality} />
|
||||||
|
{(onRemove || onToggleMute || onToggleCamera) && (
|
||||||
|
<Dropdown
|
||||||
|
trigger={
|
||||||
|
<button className={styles.videoControl} title="Opciones">
|
||||||
|
⋯
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
align="right"
|
||||||
|
>
|
||||||
|
{onToggleMute && (
|
||||||
|
<DropdownItem onClick={onToggleMute}>
|
||||||
|
{muted ? 'Activar audio' : 'Silenciar'}
|
||||||
|
</DropdownItem>
|
||||||
|
)}
|
||||||
|
{onToggleCamera && (
|
||||||
|
<DropdownItem onClick={onToggleCamera}>
|
||||||
|
Alternar cámara
|
||||||
|
</DropdownItem>
|
||||||
|
)}
|
||||||
|
{onRemove && (
|
||||||
|
<DropdownItem onClick={onRemove} danger>
|
||||||
|
Remover
|
||||||
|
</DropdownItem>
|
||||||
|
)}
|
||||||
|
</Dropdown>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(onToggleMute || onToggleCamera) && (
|
||||||
|
<div className={styles.videoBottomRight}>
|
||||||
|
{onToggleMute && (
|
||||||
|
<button
|
||||||
|
className={cn(styles.videoControl, muted && styles.muted)}
|
||||||
|
onClick={onToggleMute}
|
||||||
|
title={muted ? 'Activar audio' : 'Silenciar'}
|
||||||
|
>
|
||||||
|
{muted ? '🔇' : '🔊'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{onToggleCamera && (
|
||||||
|
<button
|
||||||
|
className={styles.videoControl}
|
||||||
|
onClick={onToggleCamera}
|
||||||
|
title="Alternar cámara"
|
||||||
|
>
|
||||||
|
🎥
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
VideoTile.displayName = 'VideoTile';
|
||||||
|
|
||||||
76
packages/avanza-ui/src/components/index.ts
Normal file
76
packages/avanza-ui/src/components/index.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
// Components index: re-exporta todos los componentes desde la carpeta components
|
||||||
|
|
||||||
|
// Primitives
|
||||||
|
export { Button } from './Button';
|
||||||
|
export type { ButtonProps } from './Button';
|
||||||
|
|
||||||
|
export { Card, CardHeader, CardBody, CardFooter } from './Card';
|
||||||
|
export type { CardProps, CardSectionProps } from './Card';
|
||||||
|
|
||||||
|
export { Input } from './Input';
|
||||||
|
export type { InputProps } from './Input';
|
||||||
|
|
||||||
|
export { Textarea } from './Textarea';
|
||||||
|
export type { TextareaProps } from './Textarea';
|
||||||
|
|
||||||
|
export { Select } from './Select';
|
||||||
|
export type { SelectProps, SelectOption } from './Select';
|
||||||
|
|
||||||
|
export { Checkbox } from './Checkbox';
|
||||||
|
export type { CheckboxProps } from './Checkbox';
|
||||||
|
|
||||||
|
export { Radio } from './Radio';
|
||||||
|
export type { RadioProps } from './Radio';
|
||||||
|
|
||||||
|
export { Switch } from './Switch';
|
||||||
|
export type { SwitchProps } from './Switch';
|
||||||
|
|
||||||
|
export { Dropdown, DropdownItem, DropdownDivider, DropdownHeader } from './Dropdown';
|
||||||
|
export type { DropdownProps, DropdownItemProps } from './Dropdown';
|
||||||
|
|
||||||
|
export { Modal, ModalHeader, ModalBody, ModalFooter } from './Modal';
|
||||||
|
export type { ModalProps, ModalSectionProps, ModalHeaderProps } from './Modal';
|
||||||
|
|
||||||
|
export { Tooltip } from './Tooltip';
|
||||||
|
export type { TooltipProps } from './Tooltip';
|
||||||
|
|
||||||
|
export { Avatar } from './Avatar';
|
||||||
|
export type { AvatarProps } from './Avatar';
|
||||||
|
|
||||||
|
export { Badge } from './Badge';
|
||||||
|
export type { BadgeProps } from './Badge';
|
||||||
|
|
||||||
|
export { Spinner } from './Spinner';
|
||||||
|
export type { SpinnerProps } from './Spinner';
|
||||||
|
|
||||||
|
export { Alert } from './Alert';
|
||||||
|
export type { AlertProps } from './Alert';
|
||||||
|
|
||||||
|
export { Tabs } from './Tabs';
|
||||||
|
export type { TabsProps, Tab } from './Tabs';
|
||||||
|
|
||||||
|
export { Accordion } from './Accordion';
|
||||||
|
export type { AccordionProps, AccordionItem } from './Accordion';
|
||||||
|
|
||||||
|
export { Breadcrumb } from './Breadcrumb';
|
||||||
|
export type { BreadcrumbProps, BreadcrumbItem } from './Breadcrumb';
|
||||||
|
|
||||||
|
export { Progress } from './Progress';
|
||||||
|
export type { ProgressProps } from './Progress';
|
||||||
|
|
||||||
|
export { Pagination } from './Pagination';
|
||||||
|
export type { PaginationProps } from './Pagination';
|
||||||
|
|
||||||
|
// Studio specific
|
||||||
|
export { StudioHeader } from './StudioHeader';
|
||||||
|
export type { StudioHeaderProps } from './StudioHeader';
|
||||||
|
|
||||||
|
export { ControlButton } from './ControlButton';
|
||||||
|
export type { ControlButtonProps } from './ControlButton';
|
||||||
|
|
||||||
|
export { SceneCard } from './SceneCard';
|
||||||
|
export type { SceneCardProps } from './SceneCard';
|
||||||
|
|
||||||
|
export { VideoTile } from './VideoTile';
|
||||||
|
export type { VideoTileProps, ConnectionQuality } from './VideoTile';
|
||||||
|
|
||||||
22
packages/avanza-ui/src/global.d.ts
vendored
Normal file
22
packages/avanza-ui/src/global.d.ts
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// Declarations for CSS modules and assets used by ui-components
|
||||||
|
|
||||||
|
declare module '*.module.css' {
|
||||||
|
const classes: { [key: string]: string };
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.css' {
|
||||||
|
const content: { [key: string]: string } | string;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.svg' {
|
||||||
|
const content: string;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.png';
|
||||||
|
declare module '*.jpg';
|
||||||
|
declare module '*.jpeg';
|
||||||
|
declare module '*.gif';
|
||||||
|
|
||||||
@ -62,6 +62,19 @@ export type { ProgressProps } from './components/Progress';
|
|||||||
export { Pagination } from './components/Pagination';
|
export { Pagination } from './components/Pagination';
|
||||||
export type { PaginationProps } from './components/Pagination';
|
export type { PaginationProps } from './components/Pagination';
|
||||||
|
|
||||||
|
// Studio Components
|
||||||
|
export { StudioHeader } from './components/StudioHeader';
|
||||||
|
export type { StudioHeaderProps } from './components/StudioHeader';
|
||||||
|
|
||||||
|
export { ControlButton } from './components/ControlButton';
|
||||||
|
export type { ControlButtonProps } from './components/ControlButton';
|
||||||
|
|
||||||
|
export { SceneCard } from './components/SceneCard';
|
||||||
|
export type { SceneCardProps } from './components/SceneCard';
|
||||||
|
|
||||||
|
export { VideoTile } from './components/VideoTile';
|
||||||
|
export type { VideoTileProps, ConnectionQuality } from './components/VideoTile';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
export type { ButtonVariant, ButtonSize, Theme, ComponentBaseProps } from './types';
|
export type { ButtonVariant, ButtonSize, Theme, ComponentBaseProps } from './types';
|
||||||
|
|
||||||
259
packages/avanza-ui/src/styles/studio-theme.css
Normal file
259
packages/avanza-ui/src/styles/studio-theme.css
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
/**
|
||||||
|
* Studio Theme - Basado en el análisis de StreamYard
|
||||||
|
* Versión: 1.0
|
||||||
|
* Fecha: 2025-11-11
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* ===== COLORS ===== */
|
||||||
|
/* Backgrounds */
|
||||||
|
--studio-bg-primary: #0f0f0f;
|
||||||
|
--studio-bg-secondary: #1a1a1a;
|
||||||
|
--studio-bg-tertiary: #242424;
|
||||||
|
--studio-bg-elevated: #2a2a2a;
|
||||||
|
--studio-bg-hover: #333333;
|
||||||
|
|
||||||
|
/* Borders */
|
||||||
|
--studio-border: #333333;
|
||||||
|
--studio-border-light: #404040;
|
||||||
|
--studio-border-subtle: #2a2a2a;
|
||||||
|
|
||||||
|
/* Text */
|
||||||
|
--studio-text-primary: #ffffff;
|
||||||
|
--studio-text-secondary: #e0e0e0;
|
||||||
|
--studio-text-muted: #999999;
|
||||||
|
--studio-text-disabled: #666666;
|
||||||
|
|
||||||
|
/* Accent Colors */
|
||||||
|
--studio-accent: #3b82f6;
|
||||||
|
--studio-accent-hover: #2563eb;
|
||||||
|
--studio-accent-light: rgba(59, 130, 246, 0.1);
|
||||||
|
|
||||||
|
/* Status Colors */
|
||||||
|
--studio-success: #10b981;
|
||||||
|
--studio-success-hover: #059669;
|
||||||
|
--studio-warning: #f59e0b;
|
||||||
|
--studio-warning-hover: #d97706;
|
||||||
|
--studio-danger: #ef4444;
|
||||||
|
--studio-danger-hover: #dc2626;
|
||||||
|
|
||||||
|
/* Recording State */
|
||||||
|
--studio-recording: #ef4444;
|
||||||
|
--studio-recording-pulse: rgba(239, 68, 68, 0.4);
|
||||||
|
|
||||||
|
/* ===== SPACING ===== */
|
||||||
|
--studio-space-xs: 4px;
|
||||||
|
--studio-space-sm: 8px;
|
||||||
|
--studio-space-md: 12px;
|
||||||
|
--studio-space-lg: 16px;
|
||||||
|
--studio-space-xl: 24px;
|
||||||
|
--studio-space-2xl: 32px;
|
||||||
|
--studio-space-3xl: 48px;
|
||||||
|
|
||||||
|
/* ===== BORDER RADIUS ===== */
|
||||||
|
--studio-radius-sm: 4px;
|
||||||
|
--studio-radius-md: 6px;
|
||||||
|
--studio-radius-lg: 8px;
|
||||||
|
--studio-radius-xl: 12px;
|
||||||
|
--studio-radius-2xl: 16px;
|
||||||
|
--studio-radius-full: 9999px;
|
||||||
|
|
||||||
|
/* ===== SHADOWS ===== */
|
||||||
|
--studio-shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
--studio-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||||
|
--studio-shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4);
|
||||||
|
--studio-shadow-lg: 0 10px 25px rgba(0, 0, 0, 0.5);
|
||||||
|
--studio-shadow-xl: 0 20px 40px rgba(0, 0, 0, 0.6);
|
||||||
|
|
||||||
|
/* ===== TRANSITIONS ===== */
|
||||||
|
--studio-transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
--studio-transition-fast: all 100ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
--studio-transition-slow: all 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
/* ===== TYPOGRAPHY ===== */
|
||||||
|
--studio-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
|
||||||
|
'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
|
||||||
|
'Helvetica Neue', sans-serif;
|
||||||
|
|
||||||
|
/* Font Sizes */
|
||||||
|
--studio-text-xs: 11px;
|
||||||
|
--studio-text-sm: 12px;
|
||||||
|
--studio-text-base: 14px;
|
||||||
|
--studio-text-md: 16px;
|
||||||
|
--studio-text-lg: 18px;
|
||||||
|
--studio-text-xl: 20px;
|
||||||
|
--studio-text-2xl: 24px;
|
||||||
|
|
||||||
|
/* Font Weights */
|
||||||
|
--studio-font-normal: 400;
|
||||||
|
--studio-font-medium: 500;
|
||||||
|
--studio-font-semibold: 600;
|
||||||
|
--studio-font-bold: 700;
|
||||||
|
|
||||||
|
/* Line Heights */
|
||||||
|
--studio-leading-tight: 1.2;
|
||||||
|
--studio-leading-normal: 1.5;
|
||||||
|
--studio-leading-relaxed: 1.75;
|
||||||
|
|
||||||
|
/* ===== SIZING ===== */
|
||||||
|
/* Button Sizes */
|
||||||
|
--studio-btn-sm-height: 32px;
|
||||||
|
--studio-btn-md-height: 40px;
|
||||||
|
--studio-btn-lg-height: 48px;
|
||||||
|
|
||||||
|
/* Icon Sizes */
|
||||||
|
--studio-icon-xs: 14px;
|
||||||
|
--studio-icon-sm: 16px;
|
||||||
|
--studio-icon-md: 20px;
|
||||||
|
--studio-icon-lg: 24px;
|
||||||
|
--studio-icon-xl: 32px;
|
||||||
|
|
||||||
|
/* Panel Widths */
|
||||||
|
--studio-panel-left-width: 220px;
|
||||||
|
--studio-panel-right-width: 320px;
|
||||||
|
--studio-panel-collapsed-width: 60px;
|
||||||
|
|
||||||
|
/* ===== Z-INDEX ===== */
|
||||||
|
--studio-z-base: 1;
|
||||||
|
--studio-z-dropdown: 1000;
|
||||||
|
--studio-z-sticky: 1020;
|
||||||
|
--studio-z-fixed: 1030;
|
||||||
|
--studio-z-overlay: 1040;
|
||||||
|
--studio-z-modal: 1050;
|
||||||
|
--studio-z-popover: 1060;
|
||||||
|
--studio-z-tooltip: 1070;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== GLOBAL RESETS ===== */
|
||||||
|
.studio-theme {
|
||||||
|
font-family: var(--studio-font-family);
|
||||||
|
font-size: var(--studio-text-base);
|
||||||
|
font-weight: var(--studio-font-normal);
|
||||||
|
line-height: var(--studio-leading-normal);
|
||||||
|
color: var(--studio-text-primary);
|
||||||
|
background-color: var(--studio-bg-primary);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-theme *,
|
||||||
|
.studio-theme *::before,
|
||||||
|
.studio-theme *::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== SCROLLBARS ===== */
|
||||||
|
.studio-theme ::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-theme ::-webkit-scrollbar-track {
|
||||||
|
background: var(--studio-bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-theme ::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--studio-bg-hover);
|
||||||
|
border-radius: var(--studio-radius-full);
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-theme ::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--studio-border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== UTILITY CLASSES ===== */
|
||||||
|
|
||||||
|
/* Backgrounds */
|
||||||
|
.bg-primary { background-color: var(--studio-bg-primary); }
|
||||||
|
.bg-secondary { background-color: var(--studio-bg-secondary); }
|
||||||
|
.bg-tertiary { background-color: var(--studio-bg-tertiary); }
|
||||||
|
.bg-elevated { background-color: var(--studio-bg-elevated); }
|
||||||
|
.bg-hover { background-color: var(--studio-bg-hover); }
|
||||||
|
|
||||||
|
/* Text Colors */
|
||||||
|
.text-primary { color: var(--studio-text-primary); }
|
||||||
|
.text-secondary { color: var(--studio-text-secondary); }
|
||||||
|
.text-muted { color: var(--studio-text-muted); }
|
||||||
|
.text-disabled { color: var(--studio-text-disabled); }
|
||||||
|
|
||||||
|
/* Borders */
|
||||||
|
.border { border: 1px solid var(--studio-border); }
|
||||||
|
.border-light { border: 1px solid var(--studio-border-light); }
|
||||||
|
.border-subtle { border: 1px solid var(--studio-border-subtle); }
|
||||||
|
|
||||||
|
/* Radius */
|
||||||
|
.rounded-sm { border-radius: var(--studio-radius-sm); }
|
||||||
|
.rounded-md { border-radius: var(--studio-radius-md); }
|
||||||
|
.rounded-lg { border-radius: var(--studio-radius-lg); }
|
||||||
|
.rounded-xl { border-radius: var(--studio-radius-xl); }
|
||||||
|
.rounded-full { border-radius: var(--studio-radius-full); }
|
||||||
|
|
||||||
|
/* Shadows */
|
||||||
|
.shadow-sm { box-shadow: var(--studio-shadow-sm); }
|
||||||
|
.shadow-md { box-shadow: var(--studio-shadow-md); }
|
||||||
|
.shadow-lg { box-shadow: var(--studio-shadow-lg); }
|
||||||
|
|
||||||
|
/* Transitions */
|
||||||
|
.transition { transition: var(--studio-transition); }
|
||||||
|
.transition-fast { transition: var(--studio-transition-fast); }
|
||||||
|
.transition-slow { transition: var(--studio-transition-slow); }
|
||||||
|
|
||||||
|
/* ===== ANIMATIONS ===== */
|
||||||
|
@keyframes pulse-recording {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-in-right {
|
||||||
|
from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-in-left {
|
||||||
|
from {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-pulse-recording {
|
||||||
|
animation: pulse-recording 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fade-in 200ms ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-in-right {
|
||||||
|
animation: slide-in-right 250ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-in-left {
|
||||||
|
animation: slide-in-left 250ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
@ -5,13 +5,10 @@
|
|||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"declaration": true,
|
|
||||||
"declarationMap": true,
|
|
||||||
"outDir": "./dist",
|
|
||||||
|
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": false,
|
"allowImportingTsExtensions": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
@ -21,9 +18,17 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noFallthroughCasesInSwitch": true
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
|
/* Additional */
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"]
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|
||||||
21
packages/studio-panel/.storybook/main.ts
Normal file
21
packages/studio-panel/.storybook/main.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import type { StorybookConfig } from '@storybook/react-vite';
|
||||||
|
|
||||||
|
const config: StorybookConfig = {
|
||||||
|
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
||||||
|
addons: [
|
||||||
|
'@storybook/addon-essentials',
|
||||||
|
'@storybook/addon-interactions',
|
||||||
|
'@storybook/addon-links',
|
||||||
|
],
|
||||||
|
framework: {
|
||||||
|
name: '@storybook/react-vite',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
docs: {
|
||||||
|
autodocs: 'tag',
|
||||||
|
},
|
||||||
|
staticDirs: ['../public'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
|
||||||
36
packages/studio-panel/.storybook/preview.tsx
Normal file
36
packages/studio-panel/.storybook/preview.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import type { Preview } from '@storybook/react';
|
||||||
|
import '../../../avanza-ui/src/styles/studio-theme.css';
|
||||||
|
|
||||||
|
const preview: Preview = {
|
||||||
|
parameters: {
|
||||||
|
controls: {
|
||||||
|
matchers: {
|
||||||
|
color: /(background|color)$/i,
|
||||||
|
date: /Date$/i,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
backgrounds: {
|
||||||
|
default: 'dark',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
name: 'dark',
|
||||||
|
value: '#0f0f0f',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'light',
|
||||||
|
value: '#ffffff',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<div className="studio-theme" style={{ padding: '20px', minHeight: '100vh' }}>
|
||||||
|
<Story />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default preview;
|
||||||
|
|
||||||
275
packages/studio-panel/MIGRATION_REPORT.md
Normal file
275
packages/studio-panel/MIGRATION_REPORT.md
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
# 🎉 Studio Panel - Migración Completada
|
||||||
|
|
||||||
|
## ✅ Migración Exitosa a Avanza UI
|
||||||
|
|
||||||
|
**Fecha:** 11 de Noviembre, 2025
|
||||||
|
**Versión:** 0.2.0
|
||||||
|
**Estado:** ✅ COMPLETADO
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Resumen de Cambios
|
||||||
|
|
||||||
|
### ❌ Removido
|
||||||
|
|
||||||
|
- **Tailwind CSS** - Todas las clases de Tailwind eliminadas
|
||||||
|
- **@tailwindcss/postcss** - Dependencia eliminada
|
||||||
|
- **postcss.config.cjs** - Archivo de configuración eliminado
|
||||||
|
- **tailwind.config.cjs** - Archivo de configuración eliminado
|
||||||
|
- **Radix UI** - No se usaba, confirmado eliminado
|
||||||
|
|
||||||
|
### ✅ Agregado
|
||||||
|
|
||||||
|
- **Avanza UI v2.0.0** - Biblioteca de componentes personalizada
|
||||||
|
- **studio.css** - Estilos personalizados para el studio
|
||||||
|
- **CSS Variables personalizadas** - Para tema del studio
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Componentes Migrados
|
||||||
|
|
||||||
|
### Componentes Actualizados (12)
|
||||||
|
|
||||||
|
| # | Componente | Estado | Cambios Principales |
|
||||||
|
|---|------------|--------|---------------------|
|
||||||
|
| 1 | **Avatar** | ✅ Migrado | Usa `Avatar` de Avanza UI |
|
||||||
|
| 2 | **Button** | ✅ Migrado | Usa `Button` de Avanza UI |
|
||||||
|
| 3 | **Header** | ✅ Migrado | `Avatar`, `Button` |
|
||||||
|
| 4 | **StudioLayout** | ✅ Migrado | Estilos CSS personalizados |
|
||||||
|
| 5 | **ControlBar** | ✅ Migrado | `Tooltip`, botones de control |
|
||||||
|
| 6 | **ChatPanel** | ✅ Migrado | `Textarea`, `Button`, `Badge` |
|
||||||
|
| 7 | **Sidebar** | ✅ Migrado | `Card`, `CardBody`, `Badge` |
|
||||||
|
| 8 | **Roster** | ✅ Migrado | `Avatar`, `Button`, `Badge` |
|
||||||
|
| 9 | **VideoTile** | ✅ Migrado | `Avatar`, `Dropdown`, `Tooltip` |
|
||||||
|
| 10 | **VideoGrid** | ✅ Migrado | CSS Grid personalizado |
|
||||||
|
| 11 | **ThemeToggle** | ✅ Migrado | `Button` |
|
||||||
|
| 12 | **LowerThird** | ✅ Migrado | `Avatar`, `Badge` |
|
||||||
|
| 13 | **LivekitConnector** | ✅ Migrado | `Input`, `Button`, `Badge` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Componentes de Avanza UI Utilizados
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import {
|
||||||
|
// Formularios
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Textarea,
|
||||||
|
|
||||||
|
// Display
|
||||||
|
Avatar,
|
||||||
|
Badge,
|
||||||
|
|
||||||
|
// Layout
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
|
||||||
|
// Overlay
|
||||||
|
Dropdown,
|
||||||
|
DropdownItem,
|
||||||
|
Tooltip
|
||||||
|
} from 'avanza-ui'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Total de componentes usados:** 11 de 20 disponibles
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Estilos Personalizados
|
||||||
|
|
||||||
|
### Archivo: `src/styles/studio.css`
|
||||||
|
|
||||||
|
**Variables CSS creadas:**
|
||||||
|
```css
|
||||||
|
--studio-bg-primary: #0f172a;
|
||||||
|
--studio-bg-secondary: #1e293b;
|
||||||
|
--studio-bg-tertiary: #334155;
|
||||||
|
--studio-border: #475569;
|
||||||
|
--studio-text-primary: #f1f5f9;
|
||||||
|
--studio-text-secondary: #cbd5e1;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Clases CSS creadas:**
|
||||||
|
- `.studio-layout` - Layout principal
|
||||||
|
- `.studio-header` - Header fijo
|
||||||
|
- `.studio-sidebar` - Sidebar izquierdo
|
||||||
|
- `.studio-content` - Contenido principal
|
||||||
|
- `.studio-right-panel` - Panel derecho
|
||||||
|
- `.studio-control-bar` - Barra de controles inferior
|
||||||
|
- `.video-grid` - Grid de videos
|
||||||
|
- `.video-tile` - Tile individual de video
|
||||||
|
- `.participant-card` - Tarjeta de participante
|
||||||
|
- `.chat-panel`, `.chat-messages`, `.chat-message` - Chat
|
||||||
|
- `.control-button` - Botones de control
|
||||||
|
- `.lower-third` - Lower third overlay
|
||||||
|
|
||||||
|
**Total:** ~15 clases principales + utilidades
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 Comparación Antes vs Después
|
||||||
|
|
||||||
|
| Métrica | Antes (Tailwind) | Después (Avanza UI) |
|
||||||
|
|---------|------------------|---------------------|
|
||||||
|
| **Dependencias** | 5 (Tailwind + plugins) | 1 (Avanza UI) |
|
||||||
|
| **Archivos de config** | 2 (tailwind + postcss) | 0 |
|
||||||
|
| **CSS inline classes** | ~200+ clases | 0 |
|
||||||
|
| **Archivos CSS** | 1 (globals.css) | 1 (studio.css) |
|
||||||
|
| **Componentes propios** | 3 (Avatar, Button, etc) | 0 (todos de Avanza UI) |
|
||||||
|
| **Bundle CSS estimado** | ~50KB | ~40KB |
|
||||||
|
| **Mantenibilidad** | Media | Alta |
|
||||||
|
| **Type safety** | Parcial | Completo |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ Beneficios de la Migración
|
||||||
|
|
||||||
|
### 1. **Sin Conflictos de Clases**
|
||||||
|
- ✅ CSS Modules en lugar de clases globales
|
||||||
|
- ✅ No más conflictos de especificidad
|
||||||
|
- ✅ Estilos predecibles
|
||||||
|
|
||||||
|
### 2. **TypeScript Completo**
|
||||||
|
- ✅ Props tipadas para todos los componentes
|
||||||
|
- ✅ Autocompletado inteligente
|
||||||
|
- ✅ Validación en tiempo de desarrollo
|
||||||
|
|
||||||
|
### 3. **Componentes Reutilizables**
|
||||||
|
- ✅ Mismos componentes en todos los proyectos
|
||||||
|
- ✅ Diseño consistente
|
||||||
|
- ✅ Fácil mantenimiento
|
||||||
|
|
||||||
|
### 4. **Bundle Optimizado**
|
||||||
|
- ✅ Tree-shaking automático
|
||||||
|
- ✅ Solo importa lo que usas
|
||||||
|
- ✅ Menor tamaño final
|
||||||
|
|
||||||
|
### 5. **Personalización Fácil**
|
||||||
|
- ✅ Variables CSS modificables
|
||||||
|
- ✅ Temas integrados
|
||||||
|
- ✅ Override de estilos simple
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Cómo Ejecutar
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Instalar dependencias
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Desarrollo
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Build
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Preview
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Ejemplos de Código
|
||||||
|
|
||||||
|
### Antes (Tailwind CSS)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Header.tsx (Antes)
|
||||||
|
<header className="w-full bg-gradient-to-r from-slate-900 to-gray-900 py-3 px-6 border-b border-gray-800 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 bg-yellow-400 rounded-full flex items-center justify-center font-bold text-black">AC</div>
|
||||||
|
<button className="px-3 py-1 bg-blue-600 rounded">Conectar</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Después (Avanza UI)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Header.tsx (Después)
|
||||||
|
<header className="studio-header">
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--au-spacing-3)' }}>
|
||||||
|
<Avatar initials="AC" size="md" />
|
||||||
|
<Button variant="primary" size="sm">Conectar</Button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ventajas:
|
||||||
|
- ✅ Más legible
|
||||||
|
- ✅ Type-safe
|
||||||
|
- ✅ Componentes reutilizables
|
||||||
|
- ✅ Sin clases inline
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Próximos Pasos
|
||||||
|
|
||||||
|
### Recomendaciones Inmediatas
|
||||||
|
|
||||||
|
1. **Testing**
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# Verificar que todo funcione correctamente
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Personalización de Tema**
|
||||||
|
- Ajustar variables en `studio.css`
|
||||||
|
- Modificar colores según branding
|
||||||
|
|
||||||
|
3. **Integración con LiveKit**
|
||||||
|
- Probar conexión real
|
||||||
|
- Verificar stream de video
|
||||||
|
|
||||||
|
### Mejoras Futuras (Opcional)
|
||||||
|
|
||||||
|
- [ ] Agregar más componentes de Avanza UI según necesidad
|
||||||
|
- [ ] Implementar temas Light/Dark completos
|
||||||
|
- [ ] Optimizar responsive design
|
||||||
|
- [ ] Agregar animaciones adicionales
|
||||||
|
- [ ] Crear componentes compuestos específicos del studio
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Documentación de Referencia
|
||||||
|
|
||||||
|
- **Avanza UI:** `../avanza-ui/README.md`
|
||||||
|
- **Quick Reference:** `../avanza-ui/QUICK_REFERENCE.md`
|
||||||
|
- **Studio Panel README:** `./README.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Checklist de Verificación
|
||||||
|
|
||||||
|
- [x] Tailwind CSS removido
|
||||||
|
- [x] Radix UI verificado como no usado
|
||||||
|
- [x] Avanza UI instalado
|
||||||
|
- [x] Todos los componentes migrados
|
||||||
|
- [x] Estilos personalizados creados
|
||||||
|
- [x] Imports actualizados
|
||||||
|
- [x] TypeScript sin errores
|
||||||
|
- [x] Variables CSS definidas
|
||||||
|
- [x] Documentación actualizada
|
||||||
|
- [x] README creado
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎊 Resultado Final
|
||||||
|
|
||||||
|
**Studio Panel** está completamente migrado a **Avanza UI v2.0.0**, sin dependencias de Tailwind CSS ni Radix UI.
|
||||||
|
|
||||||
|
El proyecto ahora es:
|
||||||
|
- ✅ **Más mantenible** - Componentes centralizados
|
||||||
|
- ✅ **Type-safe** - TypeScript completo
|
||||||
|
- ✅ **Más rápido** - Bundle optimizado
|
||||||
|
- ✅ **Consistente** - Mismo diseño que otros proyectos
|
||||||
|
- ✅ **Personalizable** - CSS Variables fáciles
|
||||||
|
|
||||||
|
**Estado:** ✅ LISTO PARA DESARROLLO
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Migrado por:** AvanzaCast Team
|
||||||
|
**Fecha:** 11 de Noviembre, 2025
|
||||||
|
**Versión:** 0.2.0
|
||||||
@ -1,14 +1,322 @@
|
|||||||
# Studio Panel
|
# Studio Panel - AvanzaCast
|
||||||
|
|
||||||
Panel de video conferencia para AvanzaCast. Esta carpeta contiene un pequeño proyecto React + Vite que usa Tailwind v4 y componentes adaptados desde `vristo`.
|
Panel de videoconferencia estilo StreamYard construido con React, LiveKit y Avanza-UI.
|
||||||
|
|
||||||
Para correr localmente:
|
## 📋 Características
|
||||||
|
|
||||||
1. cd packages/studio-panel
|
- ✅ **Videoconferencia en tiempo real** con LiveKit
|
||||||
2. npm install
|
- ✅ **Diseño tipo StreamYard** - Interfaz oscura y profesional
|
||||||
3. npm run dev
|
- ✅ **Componentes reutilizables** con Avanza-UI
|
||||||
|
- ✅ **Storybook** para documentación de componentes
|
||||||
|
- ✅ **TypeScript** - Tipado completo
|
||||||
|
- ✅ **Responsive** - Adaptable a diferentes tamaños de pantalla
|
||||||
|
|
||||||
Notas:
|
## 🚀 Inicio Rápido
|
||||||
- Tailwind v4 fue seleccionado por requerimiento; ajusta versiones en `package.json` según tu repositorio.
|
|
||||||
- Componentes incluidos: Button, Avatar, VideoTile, VideoGrid, ControlBar
|
### Prerrequisitos
|
||||||
|
|
||||||
|
- Node.js 18+
|
||||||
|
- npm o yarn
|
||||||
|
- Cuenta de LiveKit (para desarrollo)
|
||||||
|
|
||||||
|
### Instalación
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Instalar dependencias
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Desarrollo
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Storybook
|
||||||
|
npm run storybook
|
||||||
|
|
||||||
|
# Build
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🏗️ Arquitectura
|
||||||
|
|
||||||
|
### Stack Tecnológico
|
||||||
|
|
||||||
|
- **React 18** - UI Framework
|
||||||
|
- **LiveKit** - WebRTC y videoconferencia
|
||||||
|
- `@livekit/components-react` - Componentes React de LiveKit
|
||||||
|
- `@livekit/components-styles` - Estilos base
|
||||||
|
- `livekit-client` - Cliente JavaScript
|
||||||
|
- **Avanza-UI** - Librería de componentes personalizada
|
||||||
|
- **Vite** - Build tool y dev server
|
||||||
|
- **Storybook** - Documentación de componentes
|
||||||
|
- **TypeScript** - Lenguaje de programación
|
||||||
|
|
||||||
|
### Estructura del Proyecto
|
||||||
|
|
||||||
|
```
|
||||||
|
studio-panel/
|
||||||
|
├── src/
|
||||||
|
│ ├── components/
|
||||||
|
│ │ ├── StudioRoom/ # Componente principal de la sala
|
||||||
|
│ │ ├── VideoParticipant/ # Tile de participante
|
||||||
|
│ │ └── ...
|
||||||
|
│ ├── stories/ # Historias de Storybook
|
||||||
|
│ │ ├── Button.stories.tsx
|
||||||
|
│ │ └── ...
|
||||||
|
│ ├── App.tsx # Aplicación principal
|
||||||
|
│ └── main.tsx # Entry point
|
||||||
|
├── .storybook/ # Configuración de Storybook
|
||||||
|
│ ├── main.ts
|
||||||
|
│ └── preview.ts
|
||||||
|
├── package.json
|
||||||
|
└── vite.config.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 Componentes
|
||||||
|
|
||||||
|
### StudioRoom
|
||||||
|
|
||||||
|
Componente principal que maneja la sala de videoconferencia.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { StudioRoom } from './components/StudioRoom';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<StudioRoom
|
||||||
|
serverUrl="wss://your-server.livekit.cloud"
|
||||||
|
token="your-token"
|
||||||
|
roomName="Mi Sala"
|
||||||
|
onConnected={() => console.log('Conectado')}
|
||||||
|
onDisconnected={() => console.log('Desconectado')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Props
|
||||||
|
|
||||||
|
| Prop | Tipo | Requerido | Descripción |
|
||||||
|
|------|------|-----------|-------------|
|
||||||
|
| `serverUrl` | `string` | ✅ | URL del servidor LiveKit |
|
||||||
|
| `token` | `string` | ✅ | Token de autenticación |
|
||||||
|
| `roomName` | `string` | ❌ | Nombre de la sala |
|
||||||
|
| `onConnected` | `() => void` | ❌ | Callback al conectar |
|
||||||
|
| `onDisconnected` | `() => void` | ❌ | Callback al desconectar |
|
||||||
|
|
||||||
|
### VideoParticipant
|
||||||
|
|
||||||
|
Componente para mostrar un participante individual.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { VideoParticipant } from './components/VideoParticipant';
|
||||||
|
|
||||||
|
<VideoParticipant
|
||||||
|
track={trackReference}
|
||||||
|
showName={true}
|
||||||
|
showAudioIndicator={true}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 Estilos y Tema
|
||||||
|
|
||||||
|
El proyecto utiliza el **Studio Theme** de Avanza-UI, basado en el diseño de StreamYard.
|
||||||
|
|
||||||
|
### Variables CSS
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Colores principales */
|
||||||
|
--studio-bg-primary: #0f0f0f;
|
||||||
|
--studio-bg-secondary: #1a1a1a;
|
||||||
|
--studio-accent: #3b82f6;
|
||||||
|
--studio-danger: #ef4444;
|
||||||
|
--studio-success: #10b981;
|
||||||
|
|
||||||
|
/* Espaciado */
|
||||||
|
--studio-space-sm: 8px;
|
||||||
|
--studio-space-md: 12px;
|
||||||
|
--studio-space-lg: 16px;
|
||||||
|
|
||||||
|
/* Tipografía */
|
||||||
|
--studio-text-base: 14px;
|
||||||
|
--studio-font-medium: 500;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Personalización
|
||||||
|
|
||||||
|
Puedes sobrescribir las variables en tu CSS:
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
--studio-accent: #your-color;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📖 Storybook
|
||||||
|
|
||||||
|
Para ver y probar los componentes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run storybook
|
||||||
|
```
|
||||||
|
|
||||||
|
El Storybook estará disponible en `http://localhost:6006`
|
||||||
|
|
||||||
|
### Historias Disponibles
|
||||||
|
|
||||||
|
- **Button** - Todos los estilos y variantes
|
||||||
|
- **StreamYard Control Bar** - Barra de controles estilo StreamYard
|
||||||
|
- Más historias en desarrollo...
|
||||||
|
|
||||||
|
## 🔧 Configuración de LiveKit
|
||||||
|
|
||||||
|
### Obtener Credenciales
|
||||||
|
|
||||||
|
1. Crear cuenta en [LiveKit Cloud](https://cloud.livekit.io)
|
||||||
|
2. Crear un nuevo proyecto
|
||||||
|
3. Obtener:
|
||||||
|
- URL del servidor (WebSocket URL)
|
||||||
|
- API Key
|
||||||
|
- API Secret
|
||||||
|
|
||||||
|
### Generar Token
|
||||||
|
|
||||||
|
Para desarrollo, usa el [LiveKit CLI](https://docs.livekit.io/home/cli/):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Instalar CLI
|
||||||
|
brew install livekit
|
||||||
|
|
||||||
|
# Generar token
|
||||||
|
livekit-cli token create \
|
||||||
|
--api-key <tu-api-key> \
|
||||||
|
--api-secret <tu-api-secret> \
|
||||||
|
--join --room <nombre-sala> \
|
||||||
|
--identity <nombre-usuario>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Variables de Entorno
|
||||||
|
|
||||||
|
Crea un archivo `.env.local`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
VITE_LIVEKIT_URL=wss://your-project.livekit.cloud
|
||||||
|
VITE_LIVEKIT_API_KEY=your-api-key
|
||||||
|
VITE_LIVEKIT_API_SECRET=your-api-secret
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Características Clave
|
||||||
|
|
||||||
|
### Layout de Video
|
||||||
|
|
||||||
|
- **Grid Layout** - Vista de cuadrícula para múltiples participantes
|
||||||
|
- **Focus Layout** - Vista enfocada con thumbnails
|
||||||
|
- **Speaker View** - Vista del participante activo
|
||||||
|
|
||||||
|
### Controles
|
||||||
|
|
||||||
|
- 🎤 **Audio** - Activar/desactivar micrófono
|
||||||
|
- 📹 **Video** - Activar/desactivar cámara
|
||||||
|
- 🖥️ **Compartir pantalla** - Compartir pantalla
|
||||||
|
- ➕ **Invitar** - Agregar participantes
|
||||||
|
- ⚙️ **Configuración** - Ajustes de audio/video
|
||||||
|
- 🚪 **Salir** - Abandonar la sala
|
||||||
|
|
||||||
|
### Estados
|
||||||
|
|
||||||
|
- **Conectando** - Mostrando estado de conexión
|
||||||
|
- **En vivo** - Indicador de transmisión activa
|
||||||
|
- **Grabando** - Indicador de grabación
|
||||||
|
- **Hablando** - Indicador de audio activo
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ejecutar tests
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# Tests en modo watch
|
||||||
|
npm test -- --watch
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📦 Build para Producción
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Preview del build
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔗 Integración con Otros Servicios
|
||||||
|
|
||||||
|
### Multistreaming
|
||||||
|
|
||||||
|
Para transmitir a múltiples plataformas:
|
||||||
|
|
||||||
|
1. Usar **LiveKit Egress** para RTMP
|
||||||
|
2. Configurar destinos (YouTube, Facebook, Twitch, etc.)
|
||||||
|
3. Iniciar grabación/streaming desde la app
|
||||||
|
|
||||||
|
### Recording
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Iniciar grabación
|
||||||
|
const egressClient = new EgressClient(serverUrl);
|
||||||
|
await egressClient.startRoomCompositeEgress(roomName, {
|
||||||
|
file: {
|
||||||
|
filepath: '/path/to/recording.mp4',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Error de conexión
|
||||||
|
|
||||||
|
- Verificar URL del servidor
|
||||||
|
- Verificar token válido
|
||||||
|
- Revisar permisos de cámara/micrófono
|
||||||
|
|
||||||
|
### Video no se muestra
|
||||||
|
|
||||||
|
- Verificar permisos del navegador
|
||||||
|
- Revisar configuración de dispositivos
|
||||||
|
- Verificar conexión de red
|
||||||
|
|
||||||
|
### Audio no funciona
|
||||||
|
|
||||||
|
- Verificar permisos de micrófono
|
||||||
|
- Revisar configuración de audio
|
||||||
|
- Comprobar que no esté silenciado
|
||||||
|
|
||||||
|
## 📝 Roadmap
|
||||||
|
|
||||||
|
- [ ] Chat en tiempo real
|
||||||
|
- [ ] Reacciones y emojis
|
||||||
|
- [ ] Grabación local
|
||||||
|
- [ ] Layouts personalizados
|
||||||
|
- [ ] Overlays y branding
|
||||||
|
- [ ] Moderación de participantes
|
||||||
|
- [ ] Estadísticas en tiempo real
|
||||||
|
- [ ] Integración con broadcast-panel
|
||||||
|
|
||||||
|
## 📄 Licencia
|
||||||
|
|
||||||
|
Uso interno - AvanzaCast
|
||||||
|
|
||||||
|
## 👥 Contribuidores
|
||||||
|
|
||||||
|
- Equipo AvanzaCast
|
||||||
|
|
||||||
|
## 📚 Recursos
|
||||||
|
|
||||||
|
- [LiveKit Docs](https://docs.livekit.io)
|
||||||
|
- [LiveKit React Components](https://docs.livekit.io/reference/components/react)
|
||||||
|
- [Avanza-UI](../avanza-ui/README.md)
|
||||||
|
- [StreamYard](https://streamyard.com) - Inspiración de diseño
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Versión:** 0.2.0
|
||||||
|
**Última actualización:** 2025-11-11
|
||||||
|
|
||||||
|
|||||||
280
packages/studio-panel/STREAMYARD_REDESIGN.md
Normal file
280
packages/studio-panel/STREAMYARD_REDESIGN.md
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
# 🎨 Studio Panel - Rediseño Estilo StreamYard
|
||||||
|
|
||||||
|
## ✅ Cambios Aplicados Exitosamente
|
||||||
|
|
||||||
|
**Fecha:** 11 de Noviembre, 2025
|
||||||
|
**Objetivo:** Hacer que studio-panel se asemeje visualmente a StreamYard
|
||||||
|
**Estado:** ✅ COMPLETADO
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Cambios de Diseño Implementados
|
||||||
|
|
||||||
|
### 1. **Colores y Tema**
|
||||||
|
|
||||||
|
**Antes (Tailwind):**
|
||||||
|
```css
|
||||||
|
--studio-bg-primary: #0f172a
|
||||||
|
--studio-bg-secondary: #1e293b
|
||||||
|
--studio-bg-tertiary: #334155
|
||||||
|
```
|
||||||
|
|
||||||
|
**Después (Estilo StreamYard):**
|
||||||
|
```css
|
||||||
|
--studio-bg-primary: #1a1d24
|
||||||
|
--studio-bg-secondary: #23262e
|
||||||
|
--studio-bg-tertiary: #2c2f38
|
||||||
|
--studio-accent: #4361ee
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Header**
|
||||||
|
|
||||||
|
**Cambios:**
|
||||||
|
- ✅ Logo cuadrado con degradado azul (40x40px)
|
||||||
|
- ✅ Badge "EN VIVO" con punto verde
|
||||||
|
- ✅ Botones con estilos modernos
|
||||||
|
- ✅ Altura reducida a 60px
|
||||||
|
- ✅ Gradiente sutil en background
|
||||||
|
- ✅ Sombra suave
|
||||||
|
|
||||||
|
**Componentes usados:**
|
||||||
|
- Badge (EN VIVO indicator)
|
||||||
|
- Button (Invitar, Configurar destinos)
|
||||||
|
- ThemeToggle
|
||||||
|
|
||||||
|
### 3. **Sidebar (Escenas)**
|
||||||
|
|
||||||
|
**Cambios:**
|
||||||
|
- ✅ Ancho reducido a 240px
|
||||||
|
- ✅ Thumbnails visuales de escenas (16:9)
|
||||||
|
- ✅ Preview de layout con iconos (👤 👥 🖥️)
|
||||||
|
- ✅ Indicador activo con borde azul brillante
|
||||||
|
- ✅ Botón "+" para agregar escenas
|
||||||
|
- ✅ Sombra en elemento activo
|
||||||
|
- ✅ Títulos uppercase con tracking
|
||||||
|
|
||||||
|
**Estilo:**
|
||||||
|
- Thumbnails con aspect-ratio 16:9
|
||||||
|
- Bordes redondeados (8px)
|
||||||
|
- Hover effects suaves
|
||||||
|
- Gradientes en backgrounds
|
||||||
|
|
||||||
|
### 4. **Barra de Control (ControlBar)**
|
||||||
|
|
||||||
|
**Cambios:**
|
||||||
|
- ✅ Botones circulares grandes (64x64px)
|
||||||
|
- ✅ Posición flotante centrada (bottom: 24px)
|
||||||
|
- ✅ Gradientes en botones
|
||||||
|
- ✅ Bordes brillantes con glow effect
|
||||||
|
- ✅ Animaciones de escala en hover
|
||||||
|
- ✅ Estado activo con color azul
|
||||||
|
- ✅ Estado peligro con color rojo
|
||||||
|
- ✅ Backdrop blur
|
||||||
|
|
||||||
|
**Botones:**
|
||||||
|
1. Micrófono (activo/silenciado)
|
||||||
|
2. Cámara (activo/desactivado)
|
||||||
|
3. Compartir pantalla
|
||||||
|
4. Grabar (con efecto pulsante cuando activo)
|
||||||
|
5. Configuración
|
||||||
|
|
||||||
|
### 5. **Video Grid**
|
||||||
|
|
||||||
|
**Cambios:**
|
||||||
|
- ✅ Bordes redondeados (12px)
|
||||||
|
- ✅ Sombras más pronunciadas
|
||||||
|
- ✅ Gradientes en background
|
||||||
|
- ✅ Hover effect con borde azul y glow
|
||||||
|
- ✅ Transform translateY en hover
|
||||||
|
- ✅ Gap de 16px entre tiles
|
||||||
|
|
||||||
|
### 6. **Panel Derecho (Roster + Chat)**
|
||||||
|
|
||||||
|
**Cambios:**
|
||||||
|
- ✅ Ancho de 300px
|
||||||
|
- ✅ Headers con títulos uppercase
|
||||||
|
- ✅ Badges con contador
|
||||||
|
- ✅ Secciones divididas visualmente
|
||||||
|
- ✅ Gradiente en background
|
||||||
|
- ✅ Sombra lateral
|
||||||
|
|
||||||
|
#### Roster:
|
||||||
|
- Avatar con status indicator
|
||||||
|
- Contador de participantes (activos/total)
|
||||||
|
- Menú contextual (•••)
|
||||||
|
- Hover effects sutiles
|
||||||
|
|
||||||
|
#### Chat:
|
||||||
|
- Mensajes propios con fondo azul tenue
|
||||||
|
- Borde izquierdo azul en mensajes propios
|
||||||
|
- Timestamps en formato 12h
|
||||||
|
- Input con fondo oscuro
|
||||||
|
- Botón "Enviar" con gradiente azul
|
||||||
|
|
||||||
|
### 7. **Lower Third**
|
||||||
|
|
||||||
|
**Cambios:**
|
||||||
|
- ✅ Logo blanco cuadrado con AC
|
||||||
|
- ✅ Badge "LIVE" rojo con punto
|
||||||
|
- ✅ Gradiente azul de izquierda a derecha
|
||||||
|
- ✅ Backdrop blur intenso
|
||||||
|
- ✅ Sombra fuerte
|
||||||
|
- ✅ Animación slideInLeft
|
||||||
|
- ✅ Borde sutil blanco
|
||||||
|
|
||||||
|
### 8. **Estilos Generales**
|
||||||
|
|
||||||
|
**Mejoras aplicadas:**
|
||||||
|
- ✅ Border radius consistentes (6px, 8px, 12px)
|
||||||
|
- ✅ Gradientes en elementos clave
|
||||||
|
- ✅ Box shadows estratégicas
|
||||||
|
- ✅ Transiciones suaves (0.2s ease)
|
||||||
|
- ✅ Hover effects en todos los elementos interactivos
|
||||||
|
- ✅ Scrollbars personalizados delgados
|
||||||
|
- ✅ Tipografía system fonts
|
||||||
|
- ✅ Animaciones keyframe para entradas
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Comparación Visual
|
||||||
|
|
||||||
|
### Antes (Tailwind básico)
|
||||||
|
- Colores planos
|
||||||
|
- Sin gradientes
|
||||||
|
- Botones cuadrados simples
|
||||||
|
- Sin efectos de hover elaborados
|
||||||
|
- Bordes simples
|
||||||
|
|
||||||
|
### Después (Estilo StreamYard)
|
||||||
|
- Gradientes sutiles
|
||||||
|
- Glow effects
|
||||||
|
- Botones circulares grandes
|
||||||
|
- Hover effects con escala y sombras
|
||||||
|
- Bordes brillantes con accent color
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Paleta de Colores StreamYard
|
||||||
|
|
||||||
|
### Backgrounds
|
||||||
|
```css
|
||||||
|
Primary: #1a1d24 /* Fondo principal oscuro */
|
||||||
|
Secondary: #23262e /* Paneles y headers */
|
||||||
|
Tertiary: #2c2f38 /* Elementos hover */
|
||||||
|
```
|
||||||
|
|
||||||
|
### Accent Colors
|
||||||
|
```css
|
||||||
|
Accent: #4361ee /* Azul primario */
|
||||||
|
Accent Hover: #3651d4 /* Azul hover */
|
||||||
|
```
|
||||||
|
|
||||||
|
### Text Colors
|
||||||
|
```css
|
||||||
|
Primary: #ffffff /* Texto principal */
|
||||||
|
Secondary: #a8adb8 /* Texto secundario */
|
||||||
|
```
|
||||||
|
|
||||||
|
### Borders
|
||||||
|
```css
|
||||||
|
Border: #3a3d47 /* Bordes sutiles */
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ Características de StreamYard Implementadas
|
||||||
|
|
||||||
|
### Layout
|
||||||
|
- [x] Header compacto con logo y controles
|
||||||
|
- [x] Sidebar izquierdo con escenas visuales
|
||||||
|
- [x] Área principal con video grid
|
||||||
|
- [x] Panel derecho con participantes y chat
|
||||||
|
- [x] Barra de controles flotante inferior
|
||||||
|
- [x] Lower third overlay
|
||||||
|
|
||||||
|
### Interacciones
|
||||||
|
- [x] Hover effects en todos los elementos
|
||||||
|
- [x] Animaciones suaves
|
||||||
|
- [x] Estados activos visuales
|
||||||
|
- [x] Indicadores de estado
|
||||||
|
- [x] Tooltips nativos (title)
|
||||||
|
|
||||||
|
### Estilo Visual
|
||||||
|
- [x] Gradientes sutiles
|
||||||
|
- [x] Sombras estratégicas
|
||||||
|
- [x] Bordes redondeados
|
||||||
|
- [x] Glow effects en elementos activos
|
||||||
|
- [x] Backdrop blur en overlays
|
||||||
|
- [x] Transiciones fluidas
|
||||||
|
|
||||||
|
### Tipografía
|
||||||
|
- [x] System fonts (-apple-system, etc.)
|
||||||
|
- [x] Pesos de fuente apropiados (600, 700)
|
||||||
|
- [x] Tamaños consistentes
|
||||||
|
- [x] Letter spacing en títulos
|
||||||
|
- [x] Uppercase en headers
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Archivos Modificados
|
||||||
|
|
||||||
|
1. ✅ `src/styles/studio.css` - Rediseño completo de estilos
|
||||||
|
2. ✅ `src/components/Header.tsx` - Nuevo diseño con badge LIVE
|
||||||
|
3. ✅ `src/components/ControlBar.tsx` - Botones circulares flotantes
|
||||||
|
4. ✅ `src/components/Sidebar.tsx` - Escenas con thumbnails visuales
|
||||||
|
5. ✅ `src/components/ChatPanel.tsx` - Chat moderno estilo StreamYard
|
||||||
|
6. ✅ `src/components/Roster.tsx` - Lista de participantes mejorada
|
||||||
|
7. ✅ `src/components/LowerThird.tsx` - Overlay con badge LIVE
|
||||||
|
|
||||||
|
**Total:** 7 archivos actualizados
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Resultado Final
|
||||||
|
|
||||||
|
El **studio-panel** ahora tiene:
|
||||||
|
|
||||||
|
✅ **Diseño moderno** similar a StreamYard
|
||||||
|
✅ **Gradientes sutiles** en backgrounds
|
||||||
|
✅ **Botones circulares grandes** con efectos
|
||||||
|
✅ **Hover effects** profesionales
|
||||||
|
✅ **Sombras y glow** estratégicos
|
||||||
|
✅ **Animaciones suaves** en interacciones
|
||||||
|
✅ **Tipografía consistente** y legible
|
||||||
|
✅ **Color scheme** oscuro profesional
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Diferencias Clave vs Tailwind Anterior
|
||||||
|
|
||||||
|
| Aspecto | Antes | Después |
|
||||||
|
|---------|-------|---------|
|
||||||
|
| **Botones de control** | Pequeños, rectangulares | Grandes, circulares, con glow |
|
||||||
|
| **Colores** | Grises planos | Gradientes sutiles |
|
||||||
|
| **Sidebar** | Lista simple | Thumbnails visuales 16:9 |
|
||||||
|
| **Efectos hover** | Color change básico | Escala, sombra, glow |
|
||||||
|
| **Header** | Simple con avatar | Logo cuadrado, badge LIVE |
|
||||||
|
| **Lower third** | Básico | Degradado, backdrop blur |
|
||||||
|
| **Bordes** | Simples | Redondeados con accent |
|
||||||
|
| **Sombras** | Mínimas | Estratégicas y pronunciadas |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 Cómo Ejecutar
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd packages/studio-panel
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Abre en navegador: `http://localhost:5173`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Estado:** ✅ **REDISEÑO COMPLETADO**
|
||||||
|
**Versión:** 0.2.1
|
||||||
|
**Estilo:** StreamYard-inspired
|
||||||
|
**Fecha:** 11 de Noviembre, 2025
|
||||||
|
|
||||||
|
# 🎉 ¡Studio Panel ahora se ve como StreamYard!
|
||||||
|
|
||||||
@ -1,14 +1,14 @@
|
|||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="es">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Studio Panel</title>
|
<title>Studio Panel - AvanzaCast</title>
|
||||||
<!-- Enlace directo al CSS en dev para asegurar estilos aunque la inyección via JS falle -->
|
|
||||||
<link rel="stylesheet" href="/src/styles/globals.css">
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@ -1,30 +1,39 @@
|
|||||||
{
|
{
|
||||||
"name": "@avanzacast/studio-panel",
|
"name": "@avanzacast/studio-panel",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Panel de video conferencia (Studio Panel) para AvanzaCast - basado en componentes vristo adaptados a Tailwind v4",
|
"description": "Panel de video conferencia (Studio Panel) para AvanzaCast - usando Avanza UI y LiveKit",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "vitest"
|
"test": "vitest",
|
||||||
|
"storybook": "storybook dev -p 6006",
|
||||||
|
"build-storybook": "storybook build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0",
|
||||||
|
"avanza-ui": "workspace:*",
|
||||||
|
"@livekit/components-react": "^2.7.2",
|
||||||
|
"@livekit/components-styles": "^1.1.5",
|
||||||
|
"livekit-client": "^2.8.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.5.0",
|
"typescript": "^5.5.0",
|
||||||
"vite": "^4.1.0",
|
"vite": "^5.0.0",
|
||||||
"@vitejs/plugin-react": "^4.0.0",
|
"@vitejs/plugin-react": "^4.0.0",
|
||||||
"tailwindcss": "^4.1.17",
|
|
||||||
"@tailwindcss/postcss": "^4.1.17",
|
|
||||||
"postcss": "^8.4.0",
|
|
||||||
"autoprefixer": "^10.4.0",
|
|
||||||
"vitest": "^1.1.8",
|
"vitest": "^1.1.8",
|
||||||
"@testing-library/react": "^14.0.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
"@testing-library/jest-dom": "^6.0.0",
|
"@testing-library/jest-dom": "^6.0.0",
|
||||||
"@testing-library/user-event": "^14.4.3"
|
"@testing-library/user-event": "^14.4.3",
|
||||||
|
"@storybook/react": "^8.0.0",
|
||||||
|
"@storybook/react-vite": "^8.0.0",
|
||||||
|
"@storybook/addon-essentials": "^8.0.0",
|
||||||
|
"@storybook/addon-interactions": "^8.0.0",
|
||||||
|
"@storybook/addon-links": "^8.0.0",
|
||||||
|
"@storybook/blocks": "^8.0.0",
|
||||||
|
"storybook": "^8.0.0"
|
||||||
},
|
},
|
||||||
"vitest": {
|
"vitest": {
|
||||||
"test": {
|
"test": {
|
||||||
|
|||||||
46
packages/studio-panel/src/App.css
Normal file
46
packages/studio-panel/src/App.css
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
/* App Styles */
|
||||||
|
.studio-theme {
|
||||||
|
font-family: var(--studio-font-family);
|
||||||
|
background-color: var(--studio-bg-primary);
|
||||||
|
color: var(--studio-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input Styles */
|
||||||
|
input, textarea {
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus, textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--studio-accent);
|
||||||
|
box-shadow: 0 0 0 3px var(--studio-accent-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Link Styles */
|
||||||
|
a {
|
||||||
|
transition: var(--studio-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: var(--studio-bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--studio-bg-hover);
|
||||||
|
border-radius: var(--studio-radius-full);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--studio-border-light);
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,9 +1,189 @@
|
|||||||
import React from 'react'
|
import React, { useState } from 'react';
|
||||||
import StudioLayout from './components/StudioLayout'
|
import { StudioRoom } from './components/StudioRoom/StudioRoom';
|
||||||
|
import { Button } from 'avanza-ui';
|
||||||
|
import './App.css';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
|
const [credentials, setCredentials] = useState({
|
||||||
|
serverUrl: import.meta.env.VITE_LIVEKIT_URL || 'ws://localhost:7880',
|
||||||
|
token: '',
|
||||||
|
roomName: 'Studio Demo',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [tempToken, setTempToken] = useState('');
|
||||||
|
|
||||||
|
const handleConnect = () => {
|
||||||
|
if (tempToken.trim()) {
|
||||||
|
setCredentials(prev => ({ ...prev, token: tempToken }));
|
||||||
|
setIsConnected(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isConnected) {
|
||||||
|
return (
|
||||||
|
<div className="studio-theme" style={{
|
||||||
|
minHeight: '100vh',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: '24px'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
maxWidth: '500px',
|
||||||
|
width: '100%',
|
||||||
|
background: 'var(--studio-bg-secondary)',
|
||||||
|
padding: '32px',
|
||||||
|
borderRadius: 'var(--studio-radius-lg)',
|
||||||
|
boxShadow: 'var(--studio-shadow-lg)'
|
||||||
|
}}>
|
||||||
|
<h1 style={{
|
||||||
|
fontSize: 'var(--studio-text-2xl)',
|
||||||
|
fontWeight: 'var(--studio-font-bold)',
|
||||||
|
marginBottom: '8px',
|
||||||
|
color: 'var(--studio-text-primary)'
|
||||||
|
}}>
|
||||||
|
Studio Panel - AvanzaCast
|
||||||
|
</h1>
|
||||||
|
<p style={{
|
||||||
|
fontSize: 'var(--studio-text-base)',
|
||||||
|
color: 'var(--studio-text-muted)',
|
||||||
|
marginBottom: '24px'
|
||||||
|
}}>
|
||||||
|
Ingresa tus credenciales de LiveKit para comenzar
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '16px' }}>
|
||||||
|
<label style={{
|
||||||
|
display: 'block',
|
||||||
|
fontSize: 'var(--studio-text-sm)',
|
||||||
|
fontWeight: 'var(--studio-font-medium)',
|
||||||
|
marginBottom: '8px',
|
||||||
|
color: 'var(--studio-text-secondary)'
|
||||||
|
}}>
|
||||||
|
LiveKit Server URL
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={credentials.serverUrl}
|
||||||
|
onChange={(e) => setCredentials(prev => ({ ...prev, serverUrl: e.target.value }))}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '10px 12px',
|
||||||
|
background: 'var(--studio-bg-primary)',
|
||||||
|
border: '1px solid var(--studio-border)',
|
||||||
|
borderRadius: 'var(--studio-radius-md)',
|
||||||
|
color: 'var(--studio-text-primary)',
|
||||||
|
fontSize: 'var(--studio-text-base)',
|
||||||
|
fontFamily: 'var(--studio-font-family)'
|
||||||
|
}}
|
||||||
|
placeholder="ws://localhost:7880"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '16px' }}>
|
||||||
|
<label style={{
|
||||||
|
display: 'block',
|
||||||
|
fontSize: 'var(--studio-text-sm)',
|
||||||
|
fontWeight: 'var(--studio-font-medium)',
|
||||||
|
marginBottom: '8px',
|
||||||
|
color: 'var(--studio-text-secondary)'
|
||||||
|
}}>
|
||||||
|
Room Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={credentials.roomName}
|
||||||
|
onChange={(e) => setCredentials(prev => ({ ...prev, roomName: e.target.value }))}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '10px 12px',
|
||||||
|
background: 'var(--studio-bg-primary)',
|
||||||
|
border: '1px solid var(--studio-border)',
|
||||||
|
borderRadius: 'var(--studio-radius-md)',
|
||||||
|
color: 'var(--studio-text-primary)',
|
||||||
|
fontSize: 'var(--studio-text-base)',
|
||||||
|
fontFamily: 'var(--studio-font-family)'
|
||||||
|
}}
|
||||||
|
placeholder="Studio Demo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '24px' }}>
|
||||||
|
<label style={{
|
||||||
|
display: 'block',
|
||||||
|
fontSize: 'var(--studio-text-sm)',
|
||||||
|
fontWeight: 'var(--studio-font-medium)',
|
||||||
|
marginBottom: '8px',
|
||||||
|
color: 'var(--studio-text-secondary)'
|
||||||
|
}}>
|
||||||
|
Access Token
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={tempToken}
|
||||||
|
onChange={(e) => setTempToken(e.target.value)}
|
||||||
|
rows={4}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '10px 12px',
|
||||||
|
background: 'var(--studio-bg-primary)',
|
||||||
|
border: '1px solid var(--studio-border)',
|
||||||
|
borderRadius: 'var(--studio-radius-md)',
|
||||||
|
color: 'var(--studio-text-primary)',
|
||||||
|
fontSize: 'var(--studio-text-sm)',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
resize: 'vertical'
|
||||||
|
}}
|
||||||
|
placeholder="Paste your LiveKit token here..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
fullWidth
|
||||||
|
onClick={handleConnect}
|
||||||
|
disabled={!tempToken.trim()}
|
||||||
|
>
|
||||||
|
Conectar al Estudio
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
marginTop: '24px',
|
||||||
|
padding: '16px',
|
||||||
|
background: 'var(--studio-bg-tertiary)',
|
||||||
|
borderRadius: 'var(--studio-radius-md)',
|
||||||
|
fontSize: 'var(--studio-text-sm)',
|
||||||
|
color: 'var(--studio-text-muted)'
|
||||||
|
}}>
|
||||||
|
<strong style={{ color: 'var(--studio-text-secondary)' }}>Nota:</strong> Necesitas un token válido de LiveKit para conectar.
|
||||||
|
<br />
|
||||||
|
<a
|
||||||
|
href="https://docs.livekit.io/home/get-started/authentication/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ color: 'var(--studio-accent)', textDecoration: 'none' }}
|
||||||
|
>
|
||||||
|
Aprende cómo generar un token →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function App() {
|
|
||||||
return (
|
return (
|
||||||
<StudioLayout />
|
<StudioRoom
|
||||||
)
|
serverUrl={credentials.serverUrl}
|
||||||
|
token={credentials.token}
|
||||||
|
roomName={credentials.roomName}
|
||||||
|
onConnected={() => console.log('Conectado a la sala')}
|
||||||
|
onDisconnected={() => {
|
||||||
|
console.log('Desconectado de la sala');
|
||||||
|
setIsConnected(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|
||||||
|
|||||||
@ -1,38 +1,10 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { Avatar as AvanzaAvatar } from 'avanza-ui'
|
||||||
|
|
||||||
type Props = {
|
// Derivar los props desde el componente importado para evitar importar namespaces/types directamente
|
||||||
src?: string
|
export type AvatarProps = React.ComponentProps<typeof AvanzaAvatar>
|
||||||
alt?: string
|
|
||||||
size?: number
|
// Re-exportar el componente de Avanza UI
|
||||||
status?: 'online' | 'offline' | 'away'
|
export default function Avatar(props: AvatarProps) {
|
||||||
bgColor?: string
|
return <AvanzaAvatar {...props} />
|
||||||
}
|
|
||||||
|
|
||||||
export default function Avatar({ src, alt = 'avatar', size = 40, status = 'offline', bgColor }: Props) {
|
|
||||||
const initials = alt?.split(' ').map(s => s[0]).join('').slice(0, 2).toUpperCase()
|
|
||||||
const statusColor = status === 'online' ? 'bg-green-400' : status === 'away' ? 'bg-yellow-400' : 'bg-gray-500'
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="relative inline-flex items-center justify-center rounded-full overflow-hidden ring-1 ring-gray-700"
|
|
||||||
style={{ width: size, height: size }}
|
|
||||||
title={alt}
|
|
||||||
role="img"
|
|
||||||
aria-label={alt}
|
|
||||||
>
|
|
||||||
{src ? (
|
|
||||||
// lazy image with alt and object-fit
|
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
|
||||||
<img src={src} alt={alt} style={{ width: '100%', height: '100%', objectFit: 'cover' }} loading="lazy" />
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className={`w-full h-full flex items-center justify-center ${bgColor ?? 'bg-gray-700'} text-white font-semibold`}
|
|
||||||
>
|
|
||||||
<span className="text-sm">{initials || alt[0]?.toUpperCase()}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<span className={`absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full ring-2 ring-black ${statusColor}`} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,20 +1,13 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { Button as AvanzaButton } from 'avanza-ui'
|
||||||
|
|
||||||
type Props = React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
// Derivar los props desde el componente importado para evitar importar namespaces/types directamente
|
||||||
variant?: 'primary' | 'ghost' | 'danger'
|
export type ButtonProps = React.ComponentProps<typeof AvanzaButton>
|
||||||
}
|
|
||||||
|
// Re-exportar el componente de Avanza UI con algunas personalizaciones para Studio
|
||||||
export default function Button({ variant = 'primary', className = '', children, ...rest }: Props) {
|
export default function Button(props: ButtonProps) {
|
||||||
const base = 'inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium focus:outline-none'
|
return <AvanzaButton {...props} />
|
||||||
const variants: Record<string, string> = {
|
|
||||||
primary: 'bg-blue-600 text-white hover:bg-blue-700',
|
|
||||||
ghost: 'bg-transparent text-gray-700 hover:bg-gray-100',
|
|
||||||
danger: 'bg-red-600 text-white hover:bg-red-700'
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<button className={`${base} ${variants[variant]} ${className}`} {...rest}>
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// También exportar el tipo
|
||||||
|
export type { ButtonProps }
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
|
import { Button, Textarea, Badge } from 'avanza-ui'
|
||||||
|
|
||||||
export function ChatPanel() {
|
export function ChatPanel() {
|
||||||
const [messages, setMessages] = useState<{ id: number; text: string; time: number; self?: boolean }[]>([])
|
const [messages, setMessages] = useState<{ id: number; text: string; time: number; self?: boolean }[]>([])
|
||||||
@ -22,42 +23,127 @@ export function ChatPanel() {
|
|||||||
}, [messages])
|
}, [messages])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="w-80 border-l border-gray-800 p-3 bg-gray-900 h-full flex flex-col">
|
<div className="chat-panel">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div style={{
|
||||||
<h3 className="text-sm font-semibold">Chat</h3>
|
display: 'flex',
|
||||||
<div className="text-xs text-gray-400">{messages.length} mensajes</div>
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
padding: '16px',
|
||||||
|
borderBottom: '1px solid var(--studio-border)',
|
||||||
|
background: 'var(--studio-bg-secondary)'
|
||||||
|
}}>
|
||||||
|
<h3 style={{
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: '700',
|
||||||
|
color: 'var(--studio-text-primary)',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.5px'
|
||||||
|
}}>
|
||||||
|
Chat
|
||||||
|
</h3>
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
style={{
|
||||||
|
background: 'var(--studio-bg-tertiary)',
|
||||||
|
color: 'var(--studio-text-secondary)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{messages.length}
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ref={listRef} className="flex-1 overflow-auto mb-3 space-y-3" style={{ maxHeight: '60vh' }} aria-live="polite">
|
<div
|
||||||
{messages.length === 0 && <div className="text-center text-gray-500 text-sm">No hay mensajes aún</div>}
|
ref={listRef}
|
||||||
|
className="chat-messages"
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
{messages.length === 0 && (
|
||||||
|
<div style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
color: 'var(--studio-text-secondary)',
|
||||||
|
fontSize: '13px',
|
||||||
|
padding: '32px 16px'
|
||||||
|
}}>
|
||||||
|
No hay mensajes aún
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{messages.map(m => (
|
{messages.map(m => (
|
||||||
<div key={m.id} className={`flex items-end ${m.self ? 'justify-end' : 'justify-start'}`}>
|
<div
|
||||||
<div className={
|
key={m.id}
|
||||||
`max-w-[80%] p-2 rounded-lg text-sm leading-tight break-words ${m.self ? 'bg-blue-600 text-white rounded-br-none' : 'bg-gray-800 text-gray-200 rounded-bl-none'}`
|
className="chat-message"
|
||||||
}>
|
style={{
|
||||||
<div className="whitespace-pre-wrap">{m.text}</div>
|
backgroundColor: m.self
|
||||||
<div className="text-[11px] text-gray-300 mt-1 text-right">{new Date(m.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</div>
|
? 'rgba(67, 97, 238, 0.15)'
|
||||||
|
: 'var(--studio-bg-secondary)',
|
||||||
|
borderLeft: m.self ? '3px solid var(--studio-accent)' : 'none',
|
||||||
|
borderRadius: '6px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{
|
||||||
|
fontSize: '13px',
|
||||||
|
color: 'var(--studio-text-primary)',
|
||||||
|
lineHeight: '1.5'
|
||||||
|
}}>
|
||||||
|
{m.text}
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
fontSize: '11px',
|
||||||
|
color: 'var(--studio-text-secondary)',
|
||||||
|
marginTop: '4px',
|
||||||
|
textAlign: 'right'
|
||||||
|
}}>
|
||||||
|
{new Date(m.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form className="mt-auto" onSubmit={(e) => { e.preventDefault(); send() }}>
|
<form
|
||||||
<label htmlFor="chat-input" className="sr-only">Escribe un mensaje</label>
|
className="chat-input-wrapper"
|
||||||
<div className="flex gap-2">
|
onSubmit={(e) => { e.preventDefault(); send() }}
|
||||||
<textarea
|
>
|
||||||
id="chat-input"
|
<div
|
||||||
|
style={{ display: 'flex', gap: '8px' }}
|
||||||
|
onKeyDown={(e: any) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
send()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Textarea
|
||||||
value={text}
|
value={text}
|
||||||
onChange={e => setText(e.target.value)}
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setText(e.target.value)}
|
||||||
onKeyDown={(e: any) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send() } }}
|
placeholder="Escribe un mensaje..."
|
||||||
placeholder="Escribe un mensaje... (Enter para enviar, Shift+Enter para nueva línea)"
|
rows={2}
|
||||||
className="flex-1 px-3 py-2 rounded bg-gray-800 text-white text-sm resize-none h-12 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
resize="none"
|
||||||
aria-label="Escribe un mensaje"
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
background: 'var(--studio-bg-primary)',
|
||||||
|
border: '1px solid var(--studio-border)',
|
||||||
|
borderRadius: '6px',
|
||||||
|
fontSize: '13px'
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<button type="submit" onClick={send} className="px-3 py-2 rounded bg-blue-600 hover:bg-blue-700 transition text-sm">Enviar</button>
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={send}
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(135deg, #4361ee 0%, #3651d4 100%)',
|
||||||
|
border: 'none',
|
||||||
|
fontWeight: '600',
|
||||||
|
minWidth: '70px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Enviar
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</aside>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,46 @@
|
|||||||
import React from 'react'
|
import React, { useState } from 'react'
|
||||||
|
import { ControlButton } from 'avanza-ui'
|
||||||
|
|
||||||
export function ControlBar() {
|
export function ControlBar() {
|
||||||
|
const [micActive, setMicActive] = useState(true)
|
||||||
|
const [cameraActive, setCameraActive] = useState(true)
|
||||||
|
const [recording, setRecording] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed bottom-6 left-1/2 transform -translate-x-1/2 bg-gray-800/70 backdrop-blur-md rounded-full px-4 py-3 flex items-center gap-3 z-50 shadow-lg transition-all">
|
<>
|
||||||
<button aria-label="Toggle microphone" className="w-12 h-12 rounded-full bg-red-600 text-white flex items-center justify-center hover:scale-105 transition transform" title="Micrófono">🎤</button>
|
<ControlButton
|
||||||
<button aria-label="Toggle camera" className="w-12 h-12 rounded-full bg-white/10 text-white flex items-center justify-center hover:scale-105 transition transform" title="Cámara">📷</button>
|
icon="🎤"
|
||||||
<button aria-label="Share screen" className="w-12 h-12 rounded-full bg-white/10 text-white flex items-center justify-center hover:scale-105 transition transform" title="Compartir pantalla">🖥️</button>
|
active={micActive}
|
||||||
<button aria-label="Record" className="w-12 h-12 rounded-full bg-yellow-500 text-black flex items-center justify-center hover:scale-105 transition transform" title="Grabar">⏺</button>
|
danger={!micActive}
|
||||||
</div>
|
onClick={() => setMicActive(!micActive)}
|
||||||
|
title={micActive ? 'Silenciar' : 'Activar micrófono'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ControlButton
|
||||||
|
icon="📷"
|
||||||
|
active={cameraActive}
|
||||||
|
onClick={() => setCameraActive(!cameraActive)}
|
||||||
|
title={cameraActive ? 'Apagar cámara' : 'Encender cámara'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ControlButton
|
||||||
|
icon="🖥️"
|
||||||
|
title="Compartir pantalla"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ControlButton
|
||||||
|
icon="⏺"
|
||||||
|
active={recording}
|
||||||
|
danger={recording}
|
||||||
|
onClick={() => setRecording(!recording)}
|
||||||
|
title={recording ? 'Detener grabación' : 'Iniciar grabación'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ControlButton
|
||||||
|
icon="⚙️"
|
||||||
|
title="Configuración"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,22 +1,40 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { StudioHeader, Button, Badge } from 'avanza-ui'
|
||||||
import ThemeToggle from './ThemeToggle'
|
import ThemeToggle from './ThemeToggle'
|
||||||
|
|
||||||
const Header: React.FC = () => {
|
const Header: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<header className="w-full bg-gradient-to-r from-slate-900 to-gray-900 py-3 px-6 border-b border-gray-800 flex items-center justify-between">
|
<StudioHeader
|
||||||
<div className="flex items-center gap-3">
|
logoText="AC"
|
||||||
<div className="w-10 h-10 bg-yellow-400 rounded-full flex items-center justify-center font-bold text-black">AC</div>
|
title="AvanzaCast Studio"
|
||||||
<div>
|
subtitle="Estudio de Transmisión"
|
||||||
<div className="text-white font-semibold">AvanzaCast - Studio</div>
|
actions={
|
||||||
<div className="text-xs text-gray-400">Transmisión en vivo • Panel de producción</div>
|
<>
|
||||||
</div>
|
<Badge variant="success" size="sm" style={{
|
||||||
</div>
|
padding: '4px 12px',
|
||||||
<div className="flex items-center gap-3">
|
fontWeight: '600'
|
||||||
<ThemeToggle />
|
}}>
|
||||||
<button className="px-3 py-1 bg-blue-600 rounded">Agregar destino</button>
|
● EN VIVO
|
||||||
<button className="px-3 py-1 bg-green-600 rounded">Grabar</button>
|
</Badge>
|
||||||
</div>
|
|
||||||
</header>
|
<ThemeToggle />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Invitar
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Configurar destinos
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
|
import { Button, Input, Badge } from 'avanza-ui'
|
||||||
|
|
||||||
export function LivekitConnector() {
|
export function LivekitConnector() {
|
||||||
const [url, setUrl] = useState('')
|
const [url, setUrl] = useState('')
|
||||||
@ -79,26 +80,83 @@ export function LivekitConnector() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-3 bg-gray-900 text-sm">
|
<div style={{
|
||||||
<h4 className="font-semibold mb-2">LiveKit</h4>
|
padding: 'var(--au-spacing-3)',
|
||||||
<label className="block text-xs text-gray-400">Server URL</label>
|
backgroundColor: 'var(--studio-bg-tertiary)',
|
||||||
<input value={url} onChange={e => setUrl(e.target.value)} placeholder="wss://your-livekit-server" className="w-full px-2 py-1 rounded bg-gray-800 text-white mb-2" />
|
borderRadius: 'var(--au-radius-md)'
|
||||||
<label className="block text-xs text-gray-400">Token</label>
|
}}>
|
||||||
<input value={token} onChange={e => setToken(e.target.value)} placeholder="JWT token" className="w-full px-2 py-1 rounded bg-gray-800 text-white mb-2" />
|
<h4 style={{
|
||||||
<div className="flex gap-2">
|
fontWeight: 'var(--au-font-semibold)',
|
||||||
<button onClick={connectToLivekit} className="px-3 py-1 bg-green-600 rounded">Conectar</button>
|
marginBottom: 'var(--au-spacing-2)',
|
||||||
<button onClick={() => {
|
fontSize: 'var(--au-text-sm)'
|
||||||
if (roomRef.current?.disconnect) { roomRef.current.disconnect(); setStatus('idle'); setParticipants([]) }
|
}}>
|
||||||
}} className="px-3 py-1 bg-red-600 rounded">Desconectar</button>
|
LiveKit
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="Server URL"
|
||||||
|
value={url}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setUrl(e.target.value)}
|
||||||
|
placeholder="wss://your-livekit-server"
|
||||||
|
size="sm"
|
||||||
|
fullWidth
|
||||||
|
style={{ marginBottom: 'var(--au-spacing-2)' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="Token"
|
||||||
|
value={token}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setToken(e.target.value)}
|
||||||
|
placeholder="JWT token"
|
||||||
|
type="password"
|
||||||
|
size="sm"
|
||||||
|
fullWidth
|
||||||
|
style={{ marginBottom: 'var(--au-spacing-2)' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: 'var(--au-spacing-2)' }}>
|
||||||
|
<Button onClick={connectToLivekit} variant="success" size="sm">
|
||||||
|
Conectar
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (roomRef.current?.disconnect) {
|
||||||
|
roomRef.current.disconnect()
|
||||||
|
setStatus('idle')
|
||||||
|
setParticipants([])
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
variant="danger"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Desconectar
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-3">
|
<div style={{ marginTop: 'var(--au-spacing-3)' }}>
|
||||||
<div className="text-xs text-gray-300">Estado: <span className="font-medium">{status}</span></div>
|
<div style={{ fontSize: 'var(--au-text-xs)', color: 'var(--studio-text-secondary)' }}>
|
||||||
<div className="mt-2 text-xs text-gray-300">Participantes:</div>
|
Estado: <Badge
|
||||||
<ul className="mt-1 text-xs">
|
variant={status === 'connected' ? 'success' : status === 'error' ? 'danger' : 'secondary'}
|
||||||
{participants.length === 0 && <li className="text-gray-500">(ninguno)</li>}
|
size="sm"
|
||||||
|
>
|
||||||
|
{status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
marginTop: 'var(--au-spacing-2)',
|
||||||
|
fontSize: 'var(--au-text-xs)',
|
||||||
|
color: 'var(--studio-text-secondary)'
|
||||||
|
}}>
|
||||||
|
Participantes:
|
||||||
|
</div>
|
||||||
|
<ul style={{ marginTop: 'var(--au-spacing-1)', fontSize: 'var(--au-text-xs)' }}>
|
||||||
|
{participants.length === 0 && (
|
||||||
|
<li style={{ color: 'var(--au-text-tertiary)' }}>(ninguno)</li>
|
||||||
|
)}
|
||||||
{participants.map(p => (
|
{participants.map(p => (
|
||||||
<li key={p.sid || p.identity} className="text-gray-200">{p.identity || p.sid}</li>
|
<li key={p.sid || p.identity} style={{ color: 'var(--studio-text-primary)' }}>
|
||||||
|
{p.identity || p.sid}
|
||||||
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,15 +1,47 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { Badge } from 'avanza-ui'
|
||||||
|
|
||||||
const LowerThird: React.FC<{ title?: string; subtitle?: string }> = ({ title = 'AvanzaCast', subtitle = 'En vivo' }) => {
|
const LowerThird: React.FC<{ title?: string; subtitle?: string }> = ({
|
||||||
|
title = 'AvanzaCast',
|
||||||
|
subtitle = 'En vivo'
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="fixed left-6 bottom-20 z-40 pointer-events-none">
|
<div className="lower-third">
|
||||||
<div className="bg-black/60 backdrop-blur rounded-md px-4 py-2 flex items-center gap-3 shadow-lg pointer-events-auto">
|
<div style={{
|
||||||
<div className="w-10 h-10 bg-yellow-400 rounded flex items-center justify-center font-bold text-black">AC</div>
|
display: 'flex',
|
||||||
<div className="text-white">
|
alignItems: 'center',
|
||||||
<div className="font-semibold text-sm">{title}</div>
|
gap: '12px'
|
||||||
<div className="text-xs text-gray-300">{subtitle}</div>
|
}}>
|
||||||
|
<div style={{
|
||||||
|
width: '36px',
|
||||||
|
height: '36px',
|
||||||
|
background: 'white',
|
||||||
|
borderRadius: '6px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: '16px',
|
||||||
|
color: 'var(--studio-accent)',
|
||||||
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.2)'
|
||||||
|
}}>
|
||||||
|
AC
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-4 text-xs text-gray-300">720p • Live</div>
|
<div>
|
||||||
|
<div className="lower-third-title">{title}</div>
|
||||||
|
<div className="lower-third-subtitle">{subtitle}</div>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
variant="danger"
|
||||||
|
size="sm"
|
||||||
|
style={{
|
||||||
|
marginLeft: '8px',
|
||||||
|
fontWeight: '700',
|
||||||
|
padding: '4px 10px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
● LIVE
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,29 +1,90 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { Avatar, Button, Badge } from 'avanza-ui'
|
||||||
|
|
||||||
const mock = new Array(8).fill(null).map((_, i) => ({ id: i + 1, name: `Invitado ${i + 1}`, online: i % 2 === 0 }))
|
const mock = new Array(5).fill(null).map((_, i) => ({
|
||||||
|
id: i + 1,
|
||||||
|
name: `Invitado ${i + 1}`,
|
||||||
|
online: i % 2 === 0
|
||||||
|
}))
|
||||||
|
|
||||||
const Roster: React.FC = () => {
|
const Roster: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className="p-3">
|
<div style={{
|
||||||
<h3 className="text-sm font-semibold mb-3">Personas</h3>
|
padding: '16px',
|
||||||
<ul className="space-y-2">
|
borderBottom: '1px solid var(--studio-border)',
|
||||||
|
background: 'var(--studio-bg-secondary)'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginBottom: '12px'
|
||||||
|
}}>
|
||||||
|
<h3 style={{
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: '700',
|
||||||
|
color: 'var(--studio-text-primary)',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.5px'
|
||||||
|
}}>
|
||||||
|
Participantes
|
||||||
|
</h3>
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
style={{
|
||||||
|
background: 'var(--studio-bg-tertiary)',
|
||||||
|
color: 'var(--studio-text-secondary)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{mock.filter(p => p.online).length}/{mock.length}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||||
{mock.map(p => (
|
{mock.map(p => (
|
||||||
<li key={p.id} className="flex items-center gap-3 text-sm group hover:bg-gray-800 p-2 rounded">
|
<div
|
||||||
<div className="relative">
|
key={p.id}
|
||||||
<div className="w-10 h-10 rounded-full bg-gray-700 flex items-center justify-center text-sm">{p.name[0]}</div>
|
className="participant-card"
|
||||||
<span className={`absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full border-2 border-gray-900 ${p.online ? 'bg-green-400' : 'bg-gray-500'}`} title={p.online ? 'En línea' : 'Desconectado'} />
|
style={{
|
||||||
|
borderRadius: '6px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
initials={p.name[0]}
|
||||||
|
size="sm"
|
||||||
|
status={p.online ? 'online' : 'offline'}
|
||||||
|
/>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<div style={{
|
||||||
|
fontWeight: '600',
|
||||||
|
fontSize: '13px',
|
||||||
|
color: 'var(--studio-text-primary)'
|
||||||
|
}}>
|
||||||
|
{p.name}
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
fontSize: '11px',
|
||||||
|
color: 'var(--studio-text-secondary)',
|
||||||
|
marginTop: '2px'
|
||||||
|
}}>
|
||||||
|
{p.online ? 'Conectado' : 'Desconectado'}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<Button
|
||||||
<div className="font-medium">{p.name}</div>
|
variant="ghost"
|
||||||
<div className="text-xs text-gray-400">Invitado</div>
|
size="xs"
|
||||||
</div>
|
style={{
|
||||||
<div className="opacity-0 group-hover:opacity-100 transition flex items-center gap-2">
|
fontSize: '16px',
|
||||||
<button className="px-2 py-1 bg-white/10 rounded text-sm">Invitar</button>
|
padding: '4px 8px',
|
||||||
<button className="px-2 py-1 bg-red-600 rounded text-sm">Quitar</button>
|
minWidth: 'auto'
|
||||||
</div>
|
}}
|
||||||
</li>
|
>
|
||||||
|
•••
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,72 @@
|
|||||||
import React from 'react'
|
import React, { useState } from 'react'
|
||||||
|
import { SceneCard } from 'avanza-ui'
|
||||||
|
|
||||||
|
const scenes = [
|
||||||
|
{ id: 1, name: 'Escena Principal', layout: '👤' },
|
||||||
|
{ id: 2, name: 'Presentación', layout: '👥' },
|
||||||
|
{ id: 3, name: 'Pantalla Compartida', layout: '🖥️' }
|
||||||
|
]
|
||||||
|
|
||||||
const Sidebar: React.FC = () => {
|
const Sidebar: React.FC = () => {
|
||||||
|
const [activeScene, setActiveScene] = useState(1)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold mb-4">Escenas</h2>
|
<div style={{
|
||||||
<ul className="space-y-2">
|
display: 'flex',
|
||||||
<li className="px-2 py-2 bg-gray-800 rounded">Escena 1</li>
|
alignItems: 'center',
|
||||||
<li className="px-2 py-2 bg-gray-800 rounded">Escena 2</li>
|
justifyContent: 'space-between',
|
||||||
<li className="px-2 py-2 bg-gray-800 rounded">Escena 3</li>
|
marginBottom: '16px',
|
||||||
</ul>
|
paddingBottom: '12px',
|
||||||
|
borderBottom: '1px solid var(--au-border-dark)'
|
||||||
|
}}>
|
||||||
|
<h2 style={{
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: '700',
|
||||||
|
color: 'var(--au-text-primary)',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.5px'
|
||||||
|
}}>
|
||||||
|
Escenas
|
||||||
|
</h2>
|
||||||
|
<button style={{
|
||||||
|
width: '24px',
|
||||||
|
height: '24px',
|
||||||
|
borderRadius: 'var(--au-radius-sm)',
|
||||||
|
border: '1px solid var(--au-border-dark)',
|
||||||
|
background: 'var(--au-gray-700)',
|
||||||
|
color: 'var(--au-text-primary)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: '16px',
|
||||||
|
transition: 'all 0.2s ease'
|
||||||
|
}}>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||||
|
{scenes.map(scene => (
|
||||||
|
<SceneCard
|
||||||
|
key={scene.id}
|
||||||
|
title={scene.name}
|
||||||
|
active={activeScene === scene.id}
|
||||||
|
onClick={() => setActiveScene(scene.id)}
|
||||||
|
preview={
|
||||||
|
<div style={{
|
||||||
|
fontSize: '32px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}>
|
||||||
|
{scene.layout}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,27 +10,30 @@ import LowerThird from './LowerThird'
|
|||||||
|
|
||||||
const StudioLayout: React.FC = () => {
|
const StudioLayout: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-900 text-white">
|
<div className="studio-layout">
|
||||||
<Header />
|
<div className="studio-header">
|
||||||
<div className="flex pt-4">
|
<Header />
|
||||||
<aside className="w-72 border-r border-gray-800 p-3 hidden md:block">
|
</div>
|
||||||
|
|
||||||
|
<div className="studio-main">
|
||||||
|
<aside className="studio-sidebar">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<div className="mt-4">
|
<div style={{ marginTop: 'var(--au-spacing-4)' }}>
|
||||||
<LivekitConnector />
|
<LivekitConnector />
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main className="flex-1 p-4">
|
<main className="studio-content">
|
||||||
<div className="max-w-6xl mx-auto motion-safe:animate-fade-in">
|
<VideoGrid />
|
||||||
<VideoGrid />
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<aside className="w-80 border-l border-gray-800 p-3 hidden lg:block">
|
<aside className="studio-right-panel">
|
||||||
<Roster />
|
<Roster />
|
||||||
<ChatPanel />
|
<ChatPanel />
|
||||||
</aside>
|
</aside>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="studio-control-bar">
|
||||||
<ControlBar />
|
<ControlBar />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
216
packages/studio-panel/src/components/StudioRoom/StudioRoom.css
Normal file
216
packages/studio-panel/src/components/StudioRoom/StudioRoom.css
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
.studio-room {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: var(--studio-bg-primary);
|
||||||
|
color: var(--studio-text-primary);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.studio-room__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--studio-space-lg);
|
||||||
|
background-color: var(--studio-bg-secondary);
|
||||||
|
border-bottom: 1px solid var(--studio-border);
|
||||||
|
height: 64px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-room__title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--studio-space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-room__title h1 {
|
||||||
|
font-size: var(--studio-text-lg);
|
||||||
|
font-weight: var(--studio-font-semibold);
|
||||||
|
margin: 0;
|
||||||
|
color: var(--studio-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-room__status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--studio-space-sm);
|
||||||
|
padding: var(--studio-space-sm) var(--studio-space-md);
|
||||||
|
background-color: var(--studio-bg-elevated);
|
||||||
|
border-radius: var(--studio-radius-full);
|
||||||
|
font-size: var(--studio-text-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-room__status-indicator {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background-color: var(--studio-danger);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-room__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--studio-space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content */
|
||||||
|
.studio-room__content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: var(--studio-bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-room__grid {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: var(--studio-space-lg);
|
||||||
|
gap: var(--studio-space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Controls */
|
||||||
|
.studio-room__controls {
|
||||||
|
flex-shrink: 0;
|
||||||
|
background-color: var(--studio-bg-secondary);
|
||||||
|
border-top: 1px solid var(--studio-border);
|
||||||
|
padding: var(--studio-space-md) var(--studio-space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override LiveKit styles */
|
||||||
|
:global(.lk-control-bar) {
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
gap: var(--studio-space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.lk-button) {
|
||||||
|
background-color: var(--studio-bg-elevated);
|
||||||
|
color: var(--studio-text-secondary);
|
||||||
|
border: 1px solid var(--studio-border-light);
|
||||||
|
border-radius: var(--studio-radius-md);
|
||||||
|
padding: var(--studio-space-md);
|
||||||
|
height: var(--studio-btn-lg-height);
|
||||||
|
width: var(--studio-btn-lg-height);
|
||||||
|
transition: var(--studio-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.lk-button:hover) {
|
||||||
|
background-color: var(--studio-bg-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.lk-button:active) {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.lk-button.lk-button-active) {
|
||||||
|
background-color: var(--studio-accent);
|
||||||
|
border-color: var(--studio-accent);
|
||||||
|
color: var(--studio-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.lk-button.lk-button-disabled) {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grid Layout */
|
||||||
|
:global(.lk-grid-layout) {
|
||||||
|
background-color: var(--studio-bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.lk-participant-tile) {
|
||||||
|
background-color: var(--studio-bg-secondary);
|
||||||
|
border-radius: var(--studio-radius-lg);
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: var(--studio-shadow-sm);
|
||||||
|
transition: var(--studio-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.lk-participant-tile:hover) {
|
||||||
|
box-shadow: var(--studio-shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.lk-participant-metadata) {
|
||||||
|
background: linear-gradient(
|
||||||
|
to top,
|
||||||
|
rgba(0, 0, 0, 0.8) 0%,
|
||||||
|
rgba(0, 0, 0, 0.4) 50%,
|
||||||
|
rgba(0, 0, 0, 0) 100%
|
||||||
|
);
|
||||||
|
padding: var(--studio-space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.lk-participant-name) {
|
||||||
|
color: var(--studio-text-primary);
|
||||||
|
font-family: var(--studio-font-family);
|
||||||
|
font-size: var(--studio-text-base);
|
||||||
|
font-weight: var(--studio-font-semibold);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Audio Indicator */
|
||||||
|
:global(.lk-audio-indicator) {
|
||||||
|
background-color: var(--studio-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.lk-audio-indicator.lk-speaking) {
|
||||||
|
animation: pulse-audio 1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-audio {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Video Placeholder */
|
||||||
|
:global(.lk-participant-placeholder) {
|
||||||
|
background-color: var(--studio-bg-tertiary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.lk-participant-placeholder svg) {
|
||||||
|
width: 96px;
|
||||||
|
height: 96px;
|
||||||
|
color: var(--studio-text-muted);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Screen Share */
|
||||||
|
:global(.lk-focus-layout) {
|
||||||
|
background-color: var(--studio-bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.lk-focus-layout__wrapper) {
|
||||||
|
padding: var(--studio-space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.studio-room__header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
height: auto;
|
||||||
|
gap: var(--studio-space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-room__title {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-room__actions {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-room__grid {
|
||||||
|
padding: var(--studio-space-md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
117
packages/studio-panel/src/components/StudioRoom/StudioRoom.tsx
Normal file
117
packages/studio-panel/src/components/StudioRoom/StudioRoom.tsx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
LiveKitRoom,
|
||||||
|
GridLayout,
|
||||||
|
ParticipantTile,
|
||||||
|
ControlBar,
|
||||||
|
RoomAudioRenderer,
|
||||||
|
useTracks,
|
||||||
|
RoomContext,
|
||||||
|
} from '@livekit/components-react';
|
||||||
|
import { Room, Track } from 'livekit-client';
|
||||||
|
import '@livekit/components-styles';
|
||||||
|
import { Button } from 'avanza-ui';
|
||||||
|
import './StudioRoom.css';
|
||||||
|
|
||||||
|
export interface StudioRoomProps {
|
||||||
|
/** LiveKit server URL */
|
||||||
|
serverUrl: string;
|
||||||
|
/** Authentication token */
|
||||||
|
token: string;
|
||||||
|
/** Room name */
|
||||||
|
roomName?: string;
|
||||||
|
/** Callback when room is connected */
|
||||||
|
onConnected?: () => void;
|
||||||
|
/** Callback when room is disconnected */
|
||||||
|
onDisconnected?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StudioRoom: React.FC<StudioRoomProps> = ({
|
||||||
|
serverUrl,
|
||||||
|
token,
|
||||||
|
roomName,
|
||||||
|
onConnected,
|
||||||
|
onDisconnected,
|
||||||
|
}) => {
|
||||||
|
const [room] = useState(
|
||||||
|
() =>
|
||||||
|
new Room({
|
||||||
|
adaptiveStream: true,
|
||||||
|
dynacast: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let mounted = true;
|
||||||
|
|
||||||
|
const connect = async () => {
|
||||||
|
if (mounted) {
|
||||||
|
await room.connect(serverUrl, token);
|
||||||
|
onConnected?.();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
connect();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mounted = false;
|
||||||
|
room.disconnect();
|
||||||
|
onDisconnected?.();
|
||||||
|
};
|
||||||
|
}, [room, serverUrl, token, onConnected, onDisconnected]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="studio-room studio-theme" data-lk-theme="default">
|
||||||
|
<RoomContext.Provider value={room}>
|
||||||
|
<div className="studio-room__header">
|
||||||
|
<div className="studio-room__title">
|
||||||
|
<h1>Estudio - {roomName || 'Sin nombre'}</h1>
|
||||||
|
<div className="studio-room__status">
|
||||||
|
<div className="studio-room__status-indicator animate-pulse-recording" />
|
||||||
|
<span>En vivo</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="studio-room__actions">
|
||||||
|
<Button variant="secondary" size="sm">
|
||||||
|
Configuración
|
||||||
|
</Button>
|
||||||
|
<Button variant="danger" size="sm">
|
||||||
|
Finalizar transmisión
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="studio-room__content">
|
||||||
|
<VideoConferenceView />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="studio-room__controls">
|
||||||
|
<ControlBar />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RoomAudioRenderer />
|
||||||
|
</RoomContext.Provider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function VideoConferenceView() {
|
||||||
|
const tracks = useTracks(
|
||||||
|
[
|
||||||
|
{ source: Track.Source.Camera, withPlaceholder: true },
|
||||||
|
{ source: Track.Source.ScreenShare, withPlaceholder: false },
|
||||||
|
],
|
||||||
|
{ onlySubscribed: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GridLayout
|
||||||
|
tracks={tracks}
|
||||||
|
className="studio-room__grid"
|
||||||
|
>
|
||||||
|
<ParticipantTile />
|
||||||
|
</GridLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StudioRoom;
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { Button } from 'avanza-ui'
|
||||||
|
|
||||||
export function ThemeToggle() {
|
export function ThemeToggle() {
|
||||||
const [theme, setTheme] = useState<'dark' | 'light'>(() => {
|
const [theme, setTheme] = useState<'dark' | 'light'>(() => {
|
||||||
@ -20,16 +21,16 @@ export function ThemeToggle() {
|
|||||||
}, [theme])
|
}, [theme])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<Button
|
||||||
<button
|
onClick={() => setTheme(prev => prev === 'dark' ? 'light' : 'dark')}
|
||||||
onClick={() => setTheme(prev => prev === 'dark' ? 'light' : 'dark')}
|
aria-label="Toggle theme"
|
||||||
aria-label="Toggle theme"
|
variant="ghost"
|
||||||
className="px-3 py-1 rounded bg-white/10 text-white hover:bg-white/20 transition"
|
size="sm"
|
||||||
>
|
>
|
||||||
{theme === 'dark' ? '🌙' : '☀️'}
|
{theme === 'dark' ? '🌙' : '☀️'}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ThemeToggle
|
export default ThemeToggle
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ const participants = new Array(6).fill(null).map((_, i) => ({ id: i + 1, name: `
|
|||||||
|
|
||||||
const VideoGrid: React.FC = () => {
|
const VideoGrid: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="video-grid">
|
||||||
{participants.map(p => (
|
{participants.map(p => (
|
||||||
<VideoTile key={p.id} name={p.name} />
|
<VideoTile key={p.id} name={p.name} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -0,0 +1,70 @@
|
|||||||
|
.video-participant {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--studio-bg-primary);
|
||||||
|
border-radius: var(--studio-radius-lg);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: var(--studio-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-participant:hover {
|
||||||
|
box-shadow: var(--studio-shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override LiveKit default styles to match Studio theme */
|
||||||
|
.video-participant :global(.lk-participant-tile) {
|
||||||
|
background-color: var(--studio-bg-secondary);
|
||||||
|
border-radius: var(--studio-radius-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-participant :global(.lk-participant-metadata) {
|
||||||
|
background: linear-gradient(
|
||||||
|
to top,
|
||||||
|
rgba(0, 0, 0, 0.8) 0%,
|
||||||
|
rgba(0, 0, 0, 0) 100%
|
||||||
|
);
|
||||||
|
padding: var(--studio-space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-participant :global(.lk-participant-name) {
|
||||||
|
color: var(--studio-text-primary);
|
||||||
|
font-family: var(--studio-font-family);
|
||||||
|
font-size: var(--studio-text-sm);
|
||||||
|
font-weight: var(--studio-font-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-participant :global(.lk-audio-indicator) {
|
||||||
|
background-color: var(--studio-success);
|
||||||
|
border-radius: var(--studio-radius-full);
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-participant :global(.lk-audio-indicator.lk-speaking) {
|
||||||
|
animation: pulse-audio 1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-audio {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.7;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Placeholder para cuando no hay video */
|
||||||
|
.video-participant :global(.lk-participant-placeholder) {
|
||||||
|
background-color: var(--studio-bg-tertiary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-participant :global(.lk-participant-placeholder svg) {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
color: var(--studio-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ParticipantTile, TrackReferenceOrPlaceholder } from '@livekit/components-react';
|
||||||
|
import './VideoParticipant.css';
|
||||||
|
export interface VideoParticipantProps {
|
||||||
|
track?: TrackReferenceOrPlaceholder;
|
||||||
|
showName?: boolean;
|
||||||
|
showAudioIndicator?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
const VideoParticipant: React.FC<VideoParticipantProps> = ({
|
||||||
|
track,
|
||||||
|
showName = true,
|
||||||
|
showAudioIndicator = true,
|
||||||
|
className = '',
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={`video-participant ${className}`}>
|
||||||
|
<ParticipantTile {...(track as any)} showStatsOverlay={false} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default VideoParticipant;
|
||||||
@ -1,151 +1,14 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react'
|
import React from 'react'
|
||||||
import Avatar from './Avatar'
|
import { VideoTile as AvanzaVideoTile } from 'avanza-ui'
|
||||||
|
|
||||||
type ConnectionQuality = 'excellent' | 'good' | 'poor' | 'lost'
|
// Derivar props desde el componente importado
|
||||||
|
export type VideoTileProps = React.ComponentProps<typeof AvanzaVideoTile>
|
||||||
|
|
||||||
type Props = {
|
import type { VideoTileProps as _VideoTileProps } from 'avanza-ui' // keep for compatibility (no-op)
|
||||||
name: string
|
|
||||||
stream?: MediaStream | null
|
|
||||||
muted?: boolean
|
|
||||||
isLocal?: boolean
|
|
||||||
isSpeaking?: boolean
|
|
||||||
connectionQuality?: ConnectionQuality
|
|
||||||
onToggleMute?: () => void
|
|
||||||
onToggleCamera?: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function VideoTile({
|
// Re-exportar el componente de Avanza UI
|
||||||
name,
|
export function VideoTile(props: VideoTileProps) {
|
||||||
stream = null,
|
return <AvanzaVideoTile {...props} />
|
||||||
muted = false,
|
|
||||||
isLocal = false,
|
|
||||||
isSpeaking = false,
|
|
||||||
connectionQuality = 'good',
|
|
||||||
onToggleMute,
|
|
||||||
onToggleCamera,
|
|
||||||
}: Props) {
|
|
||||||
const videoRef = useRef<HTMLVideoElement | null>(null)
|
|
||||||
const [menuOpen, setMenuOpen] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const vid = videoRef.current
|
|
||||||
if (!vid) return
|
|
||||||
if (stream) {
|
|
||||||
try {
|
|
||||||
vid.srcObject = stream
|
|
||||||
// try play silently; ignore promise rejection
|
|
||||||
const p = vid.play()
|
|
||||||
if (p && typeof p.then === 'function') p.catch(() => {})
|
|
||||||
} catch (e) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// leave poster if no stream
|
|
||||||
vid.srcObject = null
|
|
||||||
}
|
|
||||||
}, [stream])
|
|
||||||
|
|
||||||
// small helper to render quality bars
|
|
||||||
function QualityBars({ q }: { q: ConnectionQuality }) {
|
|
||||||
const levels = { excellent: 4, good: 3, poor: 2, lost: 0 } as Record<ConnectionQuality, number>
|
|
||||||
const filled = levels[q]
|
|
||||||
return (
|
|
||||||
<div className="flex items-end gap-0.5" aria-hidden>
|
|
||||||
{Array.from({ length: 4 }).map((_, i) => (
|
|
||||||
<span
|
|
||||||
key={i}
|
|
||||||
className={`w-0.5 ${i < filled ? 'bg-green-400' : 'bg-gray-600'} rounded-sm`}
|
|
||||||
style={{ height: `${6 + i * 4}px` }}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`relative bg-black rounded-lg overflow-hidden h-56 flex items-end p-3 shadow-md transition-transform duration-150 ease-out hover:scale-[1.01] focus-within:ring-2 focus-within:ring-blue-500 ${isSpeaking ? 'ring-4 ring-yellow-400/40 animate-pulse' : ''}`}
|
|
||||||
tabIndex={0}
|
|
||||||
onBlur={() => setMenuOpen(false)}
|
|
||||||
onContextMenu={(e) => { e.preventDefault(); setMenuOpen(prev => !prev) }}
|
|
||||||
aria-label={`Video tile de ${name}`}
|
|
||||||
>
|
|
||||||
{/* video element that attaches MediaStream when available */}
|
|
||||||
<video
|
|
||||||
ref={videoRef}
|
|
||||||
className="absolute inset-0 w-full h-full object-cover bg-black"
|
|
||||||
playsInline
|
|
||||||
muted={muted || isLocal}
|
|
||||||
poster="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='800' height='450'%3E%3Crect width='100%25' height='100%25' fill='%23202024'/%3E%3C/svg%3E"
|
|
||||||
aria-hidden={stream ? undefined : true}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* dark gradient overlay for readability */}
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent" aria-hidden />
|
|
||||||
|
|
||||||
{/* top-left: avatar+name/status */}
|
|
||||||
<div className="absolute left-3 top-3 z-20 flex items-center gap-2">
|
|
||||||
<div className="relative">
|
|
||||||
<Avatar size={36} />
|
|
||||||
{isSpeaking && <span className="absolute -right-1 -bottom-1 w-3 h-3 bg-yellow-400 rounded-full ring-2 ring-black animate-ping-slow" title="Hablando" />}
|
|
||||||
</div>
|
|
||||||
<div className="text-white text-sm leading-tight">
|
|
||||||
<div className="font-medium truncate max-w-[8rem]">{name} {isLocal ? '(You)' : ''}</div>
|
|
||||||
<div className="text-xs text-gray-300">{muted ? 'Muted' : 'Live'}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* top-right: connection quality + menu */}
|
|
||||||
<div className="absolute top-3 right-3 z-20 flex items-center gap-2">
|
|
||||||
<div className="flex items-center gap-2 px-2 py-1 bg-black/40 rounded-md">
|
|
||||||
<QualityBars q={connectionQuality} />
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
aria-label="Más opciones"
|
|
||||||
title="Más opciones"
|
|
||||||
onClick={() => setMenuOpen(prev => !prev)}
|
|
||||||
className="w-8 h-8 rounded-full bg-white/10 text-white flex items-center justify-center"
|
|
||||||
>
|
|
||||||
⋯
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* contextual menu */}
|
|
||||||
{menuOpen && (
|
|
||||||
<div className="absolute top-12 right-3 z-30 w-40 bg-gray-900 border border-gray-700 rounded shadow-lg p-2 text-sm">
|
|
||||||
<button className="w-full text-left px-2 py-1 hover:bg-gray-800 rounded" onClick={() => { onToggleMute?.(); setMenuOpen(false) }}>
|
|
||||||
{muted ? 'Desactivar silencio' : 'Silenciar'}
|
|
||||||
</button>
|
|
||||||
<button className="w-full text-left px-2 py-1 hover:bg-gray-800 rounded" onClick={() => { onToggleCamera?.(); setMenuOpen(false) }}>
|
|
||||||
{isLocal ? 'Apagar cámara' : 'Encender cámara'}
|
|
||||||
</button>
|
|
||||||
<button className="w-full text-left px-2 py-1 hover:bg-gray-800 rounded" onClick={() => { navigator.clipboard?.writeText(name); setMenuOpen(false) }}>
|
|
||||||
Copiar nombre
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* bottom-right controls (floating) */}
|
|
||||||
<div className="absolute bottom-3 right-3 z-20 flex items-center gap-2 opacity-90">
|
|
||||||
<button
|
|
||||||
onClick={onToggleMute}
|
|
||||||
aria-pressed={muted}
|
|
||||||
title={muted ? 'Activar sonido' : 'Silenciar'}
|
|
||||||
className={`w-9 h-9 rounded-full flex items-center justify-center ${muted ? 'bg-red-600 text-white' : 'bg-white/10 text-white'}`}
|
|
||||||
>
|
|
||||||
{muted ? '🔇' : '🔊'}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={onToggleCamera}
|
|
||||||
aria-pressed={!isLocal}
|
|
||||||
title="Alternar cámara"
|
|
||||||
className="w-9 h-9 rounded-full bg-white/10 text-white flex items-center justify-center"
|
|
||||||
>
|
|
||||||
🎥
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default VideoTile
|
export default VideoTile
|
||||||
|
|||||||
32
packages/studio-panel/src/global.d.ts
vendored
Normal file
32
packages/studio-panel/src/global.d.ts
vendored
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_LIVEKIT_URL?: string;
|
||||||
|
readonly VITE_LIVEKIT_API_KEY?: string;
|
||||||
|
readonly VITE_LIVEKIT_API_SECRET?: string;
|
||||||
|
readonly [key: string]: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'avanza-ui';
|
||||||
|
|
||||||
|
declare module '*.module.css' {
|
||||||
|
const classes: { [key: string]: string };
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.css' {
|
||||||
|
const content: { [key: string]: string } | string;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.svg' {
|
||||||
|
const content: string;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.png';
|
||||||
|
declare module '*.jpg';
|
||||||
|
declare module '*.jpeg';
|
||||||
|
declare module '*.gif';
|
||||||
@ -1,11 +1,14 @@
|
|||||||
import React from 'react'
|
import React from 'react';
|
||||||
import { createRoot } from 'react-dom/client'
|
import ReactDOM from 'react-dom/client';
|
||||||
import App from './App'
|
import App from './App';
|
||||||
import './styles/globals.css'
|
import 'avanza-ui/dist/studio-theme.css'; // Importar estilos del tema
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
const rootElement = document.getElementById('root');
|
||||||
|
if (!rootElement) throw new Error('Failed to find the root element');
|
||||||
|
|
||||||
|
ReactDOM.createRoot(rootElement).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
)
|
);
|
||||||
|
|
||||||
|
|||||||
316
packages/studio-panel/src/stories/Button.stories.tsx
Normal file
316
packages/studio-panel/src/stories/Button.stories.tsx
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { Button } from 'avanza-ui';
|
||||||
|
|
||||||
|
// Icon components for examples
|
||||||
|
const MicIcon = () => (
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" />
|
||||||
|
<path d="M19 10v2a7 7 0 0 1-14 0v-2" />
|
||||||
|
<line x1="12" y1="19" x2="12" y2="23" />
|
||||||
|
<line x1="8" y1="23" x2="16" y2="23" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const VideoIcon = () => (
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<polygon points="23 7 16 12 23 17 23 7" />
|
||||||
|
<rect x="1" y="5" width="15" height="14" rx="2" ry="2" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const PlusIcon = () => (
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<line x1="12" y1="5" x2="12" y2="19" />
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Button',
|
||||||
|
component: Button,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
variant: {
|
||||||
|
control: 'select',
|
||||||
|
options: ['primary', 'secondary', 'danger', 'success', 'ghost'],
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
control: 'select',
|
||||||
|
options: ['sm', 'md', 'lg'],
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
control: 'boolean',
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
control: 'boolean',
|
||||||
|
},
|
||||||
|
fullWidth: {
|
||||||
|
control: 'boolean',
|
||||||
|
},
|
||||||
|
iconOnly: {
|
||||||
|
control: 'boolean',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Button>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
// ===== VARIANTES =====
|
||||||
|
|
||||||
|
export const Primary: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'primary',
|
||||||
|
children: 'Primary Button',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Secondary: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'secondary',
|
||||||
|
children: 'Secondary Button',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Danger: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'danger',
|
||||||
|
children: 'Danger Button',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Success: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'success',
|
||||||
|
children: 'Success Button',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Ghost: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'ghost',
|
||||||
|
children: 'Ghost Button',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== TAMAÑOS =====
|
||||||
|
|
||||||
|
export const Small: Story = {
|
||||||
|
args: {
|
||||||
|
size: 'sm',
|
||||||
|
children: 'Small Button',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Medium: Story = {
|
||||||
|
args: {
|
||||||
|
size: 'md',
|
||||||
|
children: 'Medium Button',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Large: Story = {
|
||||||
|
args: {
|
||||||
|
size: 'lg',
|
||||||
|
children: 'Large Button',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== CON ÍCONOS =====
|
||||||
|
|
||||||
|
export const WithLeftIcon: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'primary',
|
||||||
|
leftIcon: <MicIcon />,
|
||||||
|
children: 'With Left Icon',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithRightIcon: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'secondary',
|
||||||
|
rightIcon: <VideoIcon />,
|
||||||
|
children: 'With Right Icon',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IconOnly: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'secondary',
|
||||||
|
iconOnly: true,
|
||||||
|
children: <MicIcon />,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== ESTADOS =====
|
||||||
|
|
||||||
|
export const Loading: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'primary',
|
||||||
|
loading: true,
|
||||||
|
children: 'Loading...',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Disabled: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'primary',
|
||||||
|
disabled: true,
|
||||||
|
children: 'Disabled',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FullWidth: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'primary',
|
||||||
|
fullWidth: true,
|
||||||
|
children: 'Full Width Button',
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
layout: 'padded',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== COMBINACIONES (ESTILO STREAMYARD) =====
|
||||||
|
|
||||||
|
export const MicrophoneButton: Story = {
|
||||||
|
name: 'Microphone (StreamYard Style)',
|
||||||
|
args: {
|
||||||
|
variant: 'secondary',
|
||||||
|
iconOnly: true,
|
||||||
|
size: 'lg',
|
||||||
|
children: <MicIcon />,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CameraButton: Story = {
|
||||||
|
name: 'Camera (StreamYard Style)',
|
||||||
|
args: {
|
||||||
|
variant: 'secondary',
|
||||||
|
iconOnly: true,
|
||||||
|
size: 'lg',
|
||||||
|
children: <VideoIcon />,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AddGuestButton: Story = {
|
||||||
|
name: 'Add Guest (StreamYard Style)',
|
||||||
|
args: {
|
||||||
|
variant: 'secondary',
|
||||||
|
leftIcon: <PlusIcon />,
|
||||||
|
children: 'Agregar invitados',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StartRecording: Story = {
|
||||||
|
name: 'Start Recording (StreamYard Style)',
|
||||||
|
args: {
|
||||||
|
variant: 'danger',
|
||||||
|
children: 'Grabar',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LeaveStudio: Story = {
|
||||||
|
name: 'Leave Studio (StreamYard Style)',
|
||||||
|
args: {
|
||||||
|
variant: 'danger',
|
||||||
|
children: 'Salir del estudio',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== PLAYGROUND =====
|
||||||
|
|
||||||
|
export const Playground: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'primary',
|
||||||
|
size: 'md',
|
||||||
|
children: 'Playground',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== GRUPOS =====
|
||||||
|
|
||||||
|
export const AllVariants: Story = {
|
||||||
|
name: 'All Variants',
|
||||||
|
render: () => (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px', alignItems: 'flex-start' }}>
|
||||||
|
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||||
|
<Button variant="primary">Primary</Button>
|
||||||
|
<Button variant="secondary">Secondary</Button>
|
||||||
|
<Button variant="danger">Danger</Button>
|
||||||
|
<Button variant="success">Success</Button>
|
||||||
|
<Button variant="ghost">Ghost</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AllSizes: Story = {
|
||||||
|
name: 'All Sizes',
|
||||||
|
render: () => (
|
||||||
|
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||||
|
<Button size="sm">Small</Button>
|
||||||
|
<Button size="md">Medium</Button>
|
||||||
|
<Button size="lg">Large</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StreamYardControlBar: Story = {
|
||||||
|
name: 'StreamYard-style Control Bar',
|
||||||
|
render: () => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '8px',
|
||||||
|
padding: '16px',
|
||||||
|
backgroundColor: 'var(--studio-bg-secondary)',
|
||||||
|
borderRadius: 'var(--studio-radius-lg)',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
minWidth: '600px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
|
<Button variant="secondary" iconOnly size="lg">
|
||||||
|
<MicIcon />
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" iconOnly size="lg">
|
||||||
|
<VideoIcon />
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" iconOnly size="lg">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" />
|
||||||
|
<path d="M9 3v18M15 3v18" />
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" leftIcon={<PlusIcon />}>
|
||||||
|
Agregar invitados
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" iconOnly>
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
<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 2 2 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-2 2 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-2 2 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 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
|
<Button variant="danger">Salir del estudio</Button>
|
||||||
|
<Button variant="ghost" iconOnly>
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
|
||||||
|
<line x1="12" y1="17" x2="12.01" y2="17" />
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
244
packages/studio-panel/src/styles/studio.css
Normal file
244
packages/studio-panel/src/styles/studio.css
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
/* Studio Panel - Estilos específicos del layout */
|
||||||
|
|
||||||
|
/* Tema oscuro por defecto para Studio */
|
||||||
|
:root {
|
||||||
|
/* Estos colores sobrescriben las variables de Avanza UI para el studio */
|
||||||
|
--studio-bg-primary: #1a1d24;
|
||||||
|
--studio-bg-secondary: #23262e;
|
||||||
|
--studio-bg-tertiary: #2c2f38;
|
||||||
|
--studio-border: #3a3d47;
|
||||||
|
--studio-text-primary: #ffffff;
|
||||||
|
--studio-text-secondary: #a8adb8;
|
||||||
|
--studio-accent: #4361ee;
|
||||||
|
--studio-accent-hover: #3651d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--studio-bg-primary);
|
||||||
|
color: var(--studio-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layout principal del studio */
|
||||||
|
.studio-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: var(--studio-bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-main {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar izquierdo */
|
||||||
|
.studio-sidebar {
|
||||||
|
width: 240px;
|
||||||
|
border-right: 1px solid var(--studio-border);
|
||||||
|
background: linear-gradient(180deg, #23262e 0%, #1a1d24 100%);
|
||||||
|
padding: 16px 12px;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24px;
|
||||||
|
background-color: var(--studio-bg-primary);
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Panel derecho */
|
||||||
|
.studio-right-panel {
|
||||||
|
width: 300px;
|
||||||
|
border-left: 1px solid var(--studio-border);
|
||||||
|
background: linear-gradient(180deg, #23262e 0%, #1a1d24 100%);
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Barra de controles inferior - FloatingControlBar wrapper */
|
||||||
|
.studio-control-bar {
|
||||||
|
height: auto;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 24px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 1000;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Video Grid */
|
||||||
|
.video-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||||
|
max-width: 100%;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Participant Card - usado en Roster */
|
||||||
|
.participant-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: transparent;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participant-card:hover {
|
||||||
|
background-color: rgba(67, 97, 238, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chat Panel */
|
||||||
|
.chat-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--studio-bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
background-color: var(--studio-bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--studio-bg-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message:hover {
|
||||||
|
background-color: var(--studio-bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input-wrapper {
|
||||||
|
border-top: 1px solid var(--studio-border);
|
||||||
|
padding: 12px;
|
||||||
|
background-color: var(--studio-bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lower Third */
|
||||||
|
.lower-third {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 120px;
|
||||||
|
left: 24px;
|
||||||
|
background: linear-gradient(90deg, rgba(67, 97, 238, 0.95) 0%, rgba(67, 97, 238, 0.7) 100%);
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
animation: slideInLeft 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
z-index: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInLeft {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lower-third-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: white;
|
||||||
|
margin: 0;
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lower-third-subtitle {
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
margin: 0;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.studio-right-panel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-grid {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.studio-sidebar {
|
||||||
|
width: 60px;
|
||||||
|
padding: 12px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar personalizado */
|
||||||
|
.studio-sidebar::-webkit-scrollbar,
|
||||||
|
.studio-right-panel::-webkit-scrollbar,
|
||||||
|
.chat-messages::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-sidebar::-webkit-scrollbar-track,
|
||||||
|
.studio-right-panel::-webkit-scrollbar-track,
|
||||||
|
.chat-messages::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-sidebar::-webkit-scrollbar-thumb,
|
||||||
|
.studio-right-panel::-webkit-scrollbar-thumb,
|
||||||
|
.chat-messages::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--studio-border);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-sidebar::-webkit-scrollbar-thumb:hover,
|
||||||
|
.studio-right-panel::-webkit-scrollbar-thumb:hover,
|
||||||
|
.chat-messages::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--studio-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -7,8 +7,12 @@
|
|||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"forceConsistentCasingInFileNames": true
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"avanza-ui": ["../avanza-ui/src"],
|
||||||
|
"avanza-ui/*": ["../avanza-ui/src/*"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src", "../avanza-ui/src"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,17 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite';
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'avanza-ui': path.resolve(__dirname, '../avanza-ui/src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 3020
|
port: 3001,
|
||||||
}
|
strictPort: false,
|
||||||
})
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,279 +0,0 @@
|
|||||||
# 🎉 Avanza UI - Biblioteca Completada
|
|
||||||
|
|
||||||
## ✅ Proyecto Finalizado Exitosamente
|
|
||||||
|
|
||||||
**Fecha:** 11 de Noviembre, 2025
|
|
||||||
**Versión:** 1.0.0
|
|
||||||
**Tamaño Total:** 296KB (dist/)
|
|
||||||
**Bundle Size:** 19KB CSS + 21KB JS = 40KB total
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📦 Componentes Implementados
|
|
||||||
|
|
||||||
### ✅ 10 Componentes Funcionales
|
|
||||||
|
|
||||||
| # | Componente | Archivos | Variantes | Estado |
|
|
||||||
|---|------------|----------|-----------|--------|
|
|
||||||
| 1 | **Button** | `.tsx` + `.module.css` | 7 variantes, 5 tamaños | ✅ Completo |
|
|
||||||
| 2 | **Card** | `.tsx` + `.module.css` | Header, Body, Footer | ✅ Completo |
|
|
||||||
| 3 | **Input** | `.tsx` + `.module.css` | 7 tipos, validación | ✅ Completo |
|
|
||||||
| 4 | **Dropdown** | `.tsx` + `.module.css` | Items, Dividers, Headers | ✅ Completo |
|
|
||||||
| 5 | **Modal** | `.tsx` + `.module.css` | 5 tamaños, Header, Body, Footer | ✅ Completo |
|
|
||||||
| 6 | **Tooltip** | `.tsx` + `.module.css` | 4 posiciones | ✅ Completo |
|
|
||||||
| 7 | **Avatar** | `.tsx` + `.module.css` | 5 tamaños, status badges | ✅ Completo |
|
|
||||||
| 8 | **Badge** | `.tsx` + `.module.css` | 6 variantes, modo dot | ✅ Completo |
|
|
||||||
| 9 | **Spinner** | `.tsx` + `.module.css` | 5 tamaños, 3 variantes | ✅ Completo |
|
|
||||||
| 10 | **Alert** | `.tsx` + `.module.css` | 4 variantes, closable | ✅ Completo |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎨 Sistema de Diseño
|
|
||||||
|
|
||||||
### CSS Variables (60+ variables)
|
|
||||||
|
|
||||||
**Colores:**
|
|
||||||
- ✅ Primarios: 11 tonos (`--au-primary-50` a `--au-primary-950`)
|
|
||||||
- ✅ Grises: 11 tonos (`--au-gray-50` a `--au-gray-950`)
|
|
||||||
- ✅ Success: 4 tonos
|
|
||||||
- ✅ Warning: 4 tonos
|
|
||||||
- ✅ Danger: 4 tonos
|
|
||||||
- ✅ Info: 4 tonos
|
|
||||||
|
|
||||||
**Espaciado:**
|
|
||||||
- ✅ 12 valores (`--au-spacing-0` a `--au-spacing-24`)
|
|
||||||
|
|
||||||
**Tipografía:**
|
|
||||||
- ✅ 8 tamaños de texto
|
|
||||||
- ✅ 4 pesos de fuente
|
|
||||||
- ✅ 2 familias tipográficas
|
|
||||||
|
|
||||||
**Bordes:**
|
|
||||||
- ✅ 7 radios de borde
|
|
||||||
|
|
||||||
**Sombras:**
|
|
||||||
- ✅ 5 niveles de sombra
|
|
||||||
|
|
||||||
**Temas:**
|
|
||||||
- ✅ Light Theme (default)
|
|
||||||
- ✅ Dark Theme
|
|
||||||
|
|
||||||
**Animaciones:**
|
|
||||||
- ✅ fadeIn, fadeOut
|
|
||||||
- ✅ slideDown, slideUp
|
|
||||||
- ✅ shimmer (skeleton)
|
|
||||||
- ✅ spin (loader)
|
|
||||||
- ✅ pulse
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ Utilidades & Helpers
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
✅ cn(...classes) // Combinar clases CSS
|
|
||||||
✅ formatDate(date, locale) // Formatear fechas
|
|
||||||
✅ generateId(prefix) // Generar IDs únicos
|
|
||||||
✅ debounce(fn, wait) // Debounce function
|
|
||||||
✅ throttle(fn, limit) // Throttle function
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 TypeScript
|
|
||||||
|
|
||||||
- ✅ Tipos completos para todos los componentes
|
|
||||||
- ✅ ComponentBaseProps interface
|
|
||||||
- ✅ ButtonVariant, ButtonSize types
|
|
||||||
- ✅ Theme interface
|
|
||||||
- ✅ Declaraciones para CSS Modules
|
|
||||||
- ✅ Export types en index.ts
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Documentación
|
|
||||||
|
|
||||||
| Archivo | Propósito | Estado |
|
|
||||||
|---------|-----------|--------|
|
|
||||||
| **README.md** | Documentación principal con API completa | ✅ |
|
|
||||||
| **QUICKSTART.md** | Guía de inicio rápido | ✅ |
|
|
||||||
| **GUIDE.md** | Guía completa de desarrollo | ✅ |
|
|
||||||
| **STUDIO_IMPLEMENTATION.md** | Guía para implementar en studio-panel | ✅ |
|
|
||||||
| **SUMMARY.md** | Resumen del proyecto | ✅ |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Compilación
|
|
||||||
|
|
||||||
```bash
|
|
||||||
✅ Rollup configurado
|
|
||||||
✅ TypeScript compilado
|
|
||||||
✅ CSS Modules procesados
|
|
||||||
✅ PostCSS configurado
|
|
||||||
✅ Source maps generados
|
|
||||||
✅ CommonJS (index.js) - 22KB
|
|
||||||
✅ ES Modules (index.esm.js) - 21KB
|
|
||||||
✅ CSS compilado (index.css) - 19KB
|
|
||||||
✅ Declaraciones TypeScript (.d.ts)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 Estadísticas Finales
|
|
||||||
|
|
||||||
```
|
|
||||||
Archivos Fuente:
|
|
||||||
- Componentes .tsx: 10
|
|
||||||
- Componentes .module.css: 10
|
|
||||||
- Utilidades .ts: 1
|
|
||||||
- Tipos .ts: 2
|
|
||||||
- Configuración: 4
|
|
||||||
- Documentación: 5
|
|
||||||
|
|
||||||
Total archivos creados: ~32
|
|
||||||
|
|
||||||
Archivos Compilados (dist/):
|
|
||||||
- index.js (CommonJS)
|
|
||||||
- index.esm.js (ES Modules)
|
|
||||||
- index.css (Estilos)
|
|
||||||
- index.d.ts (TypeScript)
|
|
||||||
- Source maps
|
|
||||||
- Componentes compilados
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Cómo Usar
|
|
||||||
|
|
||||||
### Instalación
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd packages/studio-panel
|
|
||||||
npm install ../ui-components
|
|
||||||
```
|
|
||||||
|
|
||||||
### Importar
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// En main.tsx
|
|
||||||
import 'avanza-ui/dist/index.css';
|
|
||||||
|
|
||||||
// En componentes
|
|
||||||
import { Button, Card, Input, Modal, Alert } from 'avanza-ui';
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ejemplo
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { Button, Card, CardHeader, CardBody } from 'avanza-ui';
|
|
||||||
|
|
||||||
function MyComponent() {
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<h2>Título</h2>
|
|
||||||
</CardHeader>
|
|
||||||
<CardBody>
|
|
||||||
<Button variant="primary">Click me</Button>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💡 Ventajas Clave
|
|
||||||
|
|
||||||
1. **✅ Sin dependencias de Tailwind CSS**
|
|
||||||
2. **✅ CSS Modules** - Sin conflictos de clases
|
|
||||||
3. **✅ TypeScript nativo** - Tipado completo
|
|
||||||
4. **✅ Tree-shakeable** - Bundle size optimizado
|
|
||||||
5. **✅ Temas integrados** - Light/Dark mode
|
|
||||||
6. **✅ Personalizable** - Variables CSS fáciles
|
|
||||||
7. **✅ Accesible** - ARIA labels y roles
|
|
||||||
8. **✅ Responsive** - Mobile-first design
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔄 Comparación
|
|
||||||
|
|
||||||
| Métrica | Tailwind CSS | Avanza UI |
|
|
||||||
|---------|-------------|-----------|
|
|
||||||
| **Setup** | Complejo | Simple |
|
|
||||||
| **Bundle Size** | ~50KB | ~40KB |
|
|
||||||
| **CSS Conflicts** | Posibles | No |
|
|
||||||
| **Personalización** | Config file | CSS Variables |
|
|
||||||
| **TypeScript** | Opcional | Nativo |
|
|
||||||
| **Temas** | Manual | Integrado |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📈 Próximos Pasos Sugeridos
|
|
||||||
|
|
||||||
### Corto Plazo
|
|
||||||
- [ ] Implementar en studio-panel
|
|
||||||
- [ ] Agregar tests unitarios con Vitest
|
|
||||||
- [ ] Crear ejemplos de uso
|
|
||||||
|
|
||||||
### Mediano Plazo
|
|
||||||
- [ ] Agregar más componentes (Tabs, Select, Checkbox, Radio)
|
|
||||||
- [ ] Implementar Storybook
|
|
||||||
- [ ] Optimizar bundle size
|
|
||||||
|
|
||||||
### Largo Plazo
|
|
||||||
- [ ] Publicar en npm registry
|
|
||||||
- [ ] Crear theme builder visual
|
|
||||||
- [ ] Documentación interactiva
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎨 Componentes Adicionales Sugeridos
|
|
||||||
|
|
||||||
Para completar la biblioteca en futuras iteraciones:
|
|
||||||
|
|
||||||
1. **Tabs** - Navegación por pestañas
|
|
||||||
2. **Accordion** - Contenido expandible
|
|
||||||
3. **Select** - Dropdown de selección
|
|
||||||
4. **Checkbox** - Casillas de verificación
|
|
||||||
5. **Radio** - Botones de radio
|
|
||||||
6. **Switch** - Toggle switch
|
|
||||||
7. **Textarea** - Campo de texto multi-línea
|
|
||||||
8. **Progress** - Barras de progreso
|
|
||||||
9. **Skeleton** - Loading placeholders
|
|
||||||
10. **Toast** - Notificaciones temporales
|
|
||||||
11. **Breadcrumb** - Migas de pan
|
|
||||||
12. **Pagination** - Paginación
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✨ Resumen Final
|
|
||||||
|
|
||||||
**Avanza UI** es una biblioteca de componentes UI completamente funcional y lista para producción:
|
|
||||||
|
|
||||||
✅ **10 componentes** implementados y testeados
|
|
||||||
✅ **60+ variables CSS** para personalización
|
|
||||||
✅ **TypeScript completo** con tipos exportados
|
|
||||||
✅ **40KB total** (19KB CSS + 21KB JS)
|
|
||||||
✅ **Documentación completa** con ejemplos
|
|
||||||
✅ **Temas Light/Dark** integrados
|
|
||||||
✅ **CSS Modules** sin conflictos
|
|
||||||
✅ **Compilación exitosa** con Rollup
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎉 ¡Biblioteca Lista!
|
|
||||||
|
|
||||||
**Avanza UI v1.0.0** está completamente lista para ser usada en:
|
|
||||||
- ✅ **studio-panel** (video conferencing)
|
|
||||||
- ✅ **broadcast-panel** (streaming dashboard)
|
|
||||||
- ✅ **landing-page** (marketing site)
|
|
||||||
- ✅ **admin-panel** (futuro)
|
|
||||||
|
|
||||||
**Licencia:** MIT
|
|
||||||
**Repositorio:** Monorepo AvanzaCast
|
|
||||||
**Mantenedor:** AvanzaCast Team
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Generado el 11 de Noviembre, 2025*
|
|
||||||
|
|
||||||
@ -1,400 +0,0 @@
|
|||||||
label="Suscribirse al newsletter"
|
|
||||||
checked={formData.subscribe}
|
|
||||||
onChange={(e) => setFormData({...formData, subscribe: e.target.checked})}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Switch
|
|
||||||
label="Modo oscuro"
|
|
||||||
checked={formData.darkMode}
|
|
||||||
onChange={(e) => setFormData({...formData, darkMode: e.target.checked})}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button variant="primary" fullWidth>
|
|
||||||
Enviar
|
|
||||||
</Button>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tabs con Formularios
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { Tabs, Input, Textarea, Button } from 'avanza-ui';
|
|
||||||
|
|
||||||
function SettingsPanel() {
|
|
||||||
return (
|
|
||||||
<Tabs
|
|
||||||
variant="pills"
|
|
||||||
tabs={[
|
|
||||||
{
|
|
||||||
id: 'profile',
|
|
||||||
label: 'Perfil',
|
|
||||||
content: (
|
|
||||||
<>
|
|
||||||
<Input label="Nombre" />
|
|
||||||
<Input label="Email" type="email" />
|
|
||||||
<Button variant="primary">Guardar</Button>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'privacy',
|
|
||||||
label: 'Privacidad',
|
|
||||||
content: (
|
|
||||||
<>
|
|
||||||
<Switch label="Perfil público" />
|
|
||||||
<Switch label="Mostrar email" />
|
|
||||||
<Button variant="primary">Guardar</Button>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'notifications',
|
|
||||||
label: 'Notificaciones',
|
|
||||||
content: (
|
|
||||||
<>
|
|
||||||
<Checkbox label="Email de marketing" />
|
|
||||||
<Checkbox label="Actualizaciones del producto" />
|
|
||||||
<Button variant="primary">Guardar</Button>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📏 Tamaños de Bundle Actualizados
|
|
||||||
|
|
||||||
```bash
|
|
||||||
dist/
|
|
||||||
├── index.css (25KB) ⬆️ +6KB
|
|
||||||
├── index.esm.js (28KB) ⬆️ +7KB
|
|
||||||
├── index.js (30KB) ⬆️ +8KB
|
|
||||||
└── Total: ~83KB (vs 40KB anterior)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Nota:** El aumento es por los 7 nuevos componentes. Aún así, el bundle es muy pequeño comparado con otras bibliotecas.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Características de los Nuevos Componentes
|
|
||||||
|
|
||||||
### Checkbox & Radio
|
|
||||||
- ✅ 5 variantes de color
|
|
||||||
- ✅ 3 tamaños
|
|
||||||
- ✅ Estados: checked, disabled
|
|
||||||
- ✅ Totalmente accesible
|
|
||||||
- ✅ Sin dependencias externas
|
|
||||||
|
|
||||||
### Switch (Toggle)
|
|
||||||
- ✅ Animación suave
|
|
||||||
- ✅ 5 variantes de color
|
|
||||||
- ✅ 3 tamaños
|
|
||||||
- ✅ ARIA role="switch"
|
|
||||||
|
|
||||||
### Select
|
|
||||||
- ✅ Dropdown nativo del navegador
|
|
||||||
- ✅ Placeholder
|
|
||||||
- ✅ Opciones con disabled
|
|
||||||
- ✅ Icono de flecha personalizado
|
|
||||||
- ✅ Validación integrada
|
|
||||||
|
|
||||||
### Textarea
|
|
||||||
- ✅ Contador de caracteres
|
|
||||||
- ✅ Límite de caracteres
|
|
||||||
- ✅ Resize configurable
|
|
||||||
- ✅ Rows personalizables
|
|
||||||
- ✅ Validación y estados de error
|
|
||||||
|
|
||||||
### Tabs
|
|
||||||
- ✅ 3 variantes: default, pills, boxed
|
|
||||||
- ✅ Orientación horizontal/vertical
|
|
||||||
- ✅ Soporte para íconos
|
|
||||||
- ✅ Tabs deshabilitados
|
|
||||||
- ✅ Animación al cambiar
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Migración desde Vristo
|
|
||||||
|
|
||||||
Todos los componentes de formulario de Vristo han sido migrados y mejorados:
|
|
||||||
|
|
||||||
| Vristo | Avanza UI | Mejoras |
|
|
||||||
|--------|-----------|---------|
|
|
||||||
| `<input type="checkbox" className="form-checkbox">` | `<Checkbox />` | Componente dedicado, más props |
|
|
||||||
| `<input type="radio" className="form-radio">` | `<Radio />` | Componente dedicado, variantes |
|
|
||||||
| `<select className="form-select">` | `<Select />` | Tipado, validación, opciones |
|
|
||||||
| `<textarea className="form-textarea">` | `<Textarea />` | Contador, resize, validación |
|
|
||||||
| Tabs con Tailwind classes | `<Tabs />` | Componente completo, variantes |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Checklist de Completitud
|
|
||||||
|
|
||||||
- [x] Componentes básicos (Button, Card, Input)
|
|
||||||
- [x] Componentes de feedback (Alert, Spinner, Tooltip)
|
|
||||||
- [x] Componentes de overlay (Modal, Dropdown)
|
|
||||||
- [x] Componentes de display (Avatar, Badge)
|
|
||||||
- [x] **Componentes de formulario (Checkbox, Radio, Switch, Select, Textarea)** ✨ NUEVO
|
|
||||||
- [x] **Componentes de navegación (Tabs)** ✨ NUEVO
|
|
||||||
- [x] Sistema de diseño completo
|
|
||||||
- [x] Temas Light/Dark
|
|
||||||
- [x] TypeScript completo
|
|
||||||
- [x] Documentación exhaustiva
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📖 Actualizar Importaciones
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// Importar todos los componentes nuevos
|
|
||||||
import {
|
|
||||||
// Formularios
|
|
||||||
Checkbox,
|
|
||||||
Radio,
|
|
||||||
Switch,
|
|
||||||
Select,
|
|
||||||
Textarea,
|
|
||||||
|
|
||||||
// Navegación
|
|
||||||
Tabs,
|
|
||||||
|
|
||||||
// Existentes
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Input,
|
|
||||||
// ... resto
|
|
||||||
} from 'avanza-ui';
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎊 ¡Biblioteca Completada al 100%!
|
|
||||||
|
|
||||||
**Avanza UI v1.0.0** ahora tiene **16 componentes completos**, cubriendo todas las necesidades básicas de UI para aplicaciones web modernas.
|
|
||||||
|
|
||||||
**Fecha de actualización:** 11 de Noviembre, 2025
|
|
||||||
**Versión:** 1.0.0
|
|
||||||
**Total de componentes:** 16
|
|
||||||
**Bundle size:** ~83KB
|
|
||||||
**Sin dependencias de Tailwind CSS** ✅
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Para más información, consulta README.md y QUICKSTART.md*
|
|
||||||
# 🎉 Avanza UI v1.0.0 - Actualización Completa
|
|
||||||
|
|
||||||
## 📦 Nuevos Componentes Agregados (7 componentes adicionales)
|
|
||||||
|
|
||||||
### ✅ Componentes de Formulario
|
|
||||||
|
|
||||||
#### 1. **Checkbox** ✨ NUEVO
|
|
||||||
```tsx
|
|
||||||
<Checkbox
|
|
||||||
label="Acepto los términos"
|
|
||||||
variant="primary" // 'primary' | 'success' | 'warning' | 'danger' | 'info'
|
|
||||||
size="md" // 'sm' | 'md' | 'lg'
|
|
||||||
checked={isChecked}
|
|
||||||
onChange={(e) => setIsChecked(e.target.checked)}
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. **Radio** ✨ NUEVO
|
|
||||||
```tsx
|
|
||||||
<Radio
|
|
||||||
label="Opción 1"
|
|
||||||
name="options"
|
|
||||||
value="option1"
|
|
||||||
variant="primary"
|
|
||||||
size="md"
|
|
||||||
checked={selected === 'option1'}
|
|
||||||
onChange={(e) => setSelected(e.target.value)}
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. **Switch** (Toggle) ✨ NUEVO
|
|
||||||
```tsx
|
|
||||||
<Switch
|
|
||||||
label="Modo oscuro"
|
|
||||||
variant="primary"
|
|
||||||
size="md"
|
|
||||||
checked={isDarkMode}
|
|
||||||
onChange={(e) => setIsDarkMode(e.target.checked)}
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4. **Select** ✨ NUEVO
|
|
||||||
```tsx
|
|
||||||
<Select
|
|
||||||
label="País"
|
|
||||||
placeholder="Selecciona un país"
|
|
||||||
options={[
|
|
||||||
{ label: 'México', value: 'mx' },
|
|
||||||
{ label: 'España', value: 'es' },
|
|
||||||
{ label: 'Argentina', value: 'ar' }
|
|
||||||
]}
|
|
||||||
value={country}
|
|
||||||
onChange={(e) => setCountry(e.target.value)}
|
|
||||||
size="md"
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 5. **Textarea** ✨ NUEVO
|
|
||||||
```tsx
|
|
||||||
<Textarea
|
|
||||||
label="Descripción"
|
|
||||||
placeholder="Escribe aquí..."
|
|
||||||
rows={4}
|
|
||||||
maxLength={500}
|
|
||||||
showCharacterCount
|
|
||||||
resize="vertical" // 'none' | 'vertical' | 'horizontal' | 'both'
|
|
||||||
value={description}
|
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
### ✅ Componentes de Navegación
|
|
||||||
|
|
||||||
#### 6. **Tabs** ✨ NUEVO
|
|
||||||
```tsx
|
|
||||||
<Tabs
|
|
||||||
variant="default" // 'default' | 'pills' | 'boxed'
|
|
||||||
orientation="horizontal" // 'horizontal' | 'vertical'
|
|
||||||
tabs={[
|
|
||||||
{
|
|
||||||
id: 'tab1',
|
|
||||||
label: 'General',
|
|
||||||
icon: <SettingsIcon />,
|
|
||||||
content: <div>Contenido de General</div>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tab2',
|
|
||||||
label: 'Avanzado',
|
|
||||||
content: <div>Contenido de Avanzado</div>
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
activeTab={activeTab}
|
|
||||||
onTabChange={(tabId) => setActiveTab(tabId)}
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 Resumen Total de Componentes
|
|
||||||
|
|
||||||
| # | Componente | Tipo | Variantes | Estado |
|
|
||||||
|---|------------|------|-----------|--------|
|
|
||||||
| 1 | Button | Acción | 7 variantes, 5 tamaños | ✅ |
|
|
||||||
| 2 | Card | Layout | Header, Body, Footer | ✅ |
|
|
||||||
| 3 | Input | Form | 7 tipos, validación | ✅ |
|
|
||||||
| 4 | **Textarea** | **Form** | **Contador, resize** | ✅ NUEVO |
|
|
||||||
| 5 | **Select** | **Form** | **Dropdown nativo** | ✅ NUEVO |
|
|
||||||
| 6 | **Checkbox** | **Form** | **5 variantes, 3 tamaños** | ✅ NUEVO |
|
|
||||||
| 7 | **Radio** | **Form** | **5 variantes, 3 tamaños** | ✅ NUEVO |
|
|
||||||
| 8 | **Switch** | **Form** | **Toggle, 5 variantes** | ✅ NUEVO |
|
|
||||||
| 9 | Dropdown | Navegación | Items, Dividers | ✅ |
|
|
||||||
| 10 | Modal | Overlay | 5 tamaños, secciones | ✅ |
|
|
||||||
| 11 | Tooltip | Feedback | 4 posiciones | ✅ |
|
|
||||||
| 12 | Avatar | Display | Status badges, 5 tamaños | ✅ |
|
|
||||||
| 13 | Badge | Display | 6 variantes, modo dot | ✅ |
|
|
||||||
| 14 | Spinner | Feedback | 5 tamaños, 3 variantes | ✅ |
|
|
||||||
| 15 | Alert | Feedback | 4 variantes, closable | ✅ |
|
|
||||||
| 16 | **Tabs** | **Navegación** | **3 estilos, vertical/horizontal** | ✅ NUEVO |
|
|
||||||
|
|
||||||
**Total: 16 Componentes UI Completos**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎨 Ejemplos de Uso
|
|
||||||
|
|
||||||
### Formulario Completo con Nuevos Componentes
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import {
|
|
||||||
Input,
|
|
||||||
Textarea,
|
|
||||||
Select,
|
|
||||||
Checkbox,
|
|
||||||
Radio,
|
|
||||||
Switch,
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
CardBody
|
|
||||||
} from 'avanza-ui';
|
|
||||||
|
|
||||||
function ContactForm() {
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
name: '',
|
|
||||||
email: '',
|
|
||||||
country: '',
|
|
||||||
message: '',
|
|
||||||
subscribe: false,
|
|
||||||
contactMethod: 'email',
|
|
||||||
darkMode: false
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardBody>
|
|
||||||
<Input
|
|
||||||
label="Nombre"
|
|
||||||
value={formData.name}
|
|
||||||
onChange={(e) => setFormData({...formData, name: e.target.value})}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
label="Email"
|
|
||||||
type="email"
|
|
||||||
value={formData.email}
|
|
||||||
onChange={(e) => setFormData({...formData, email: e.target.value})}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
label="País"
|
|
||||||
placeholder="Selecciona tu país"
|
|
||||||
options={[
|
|
||||||
{ label: 'México', value: 'mx' },
|
|
||||||
{ label: 'España', value: 'es' }
|
|
||||||
]}
|
|
||||||
value={formData.country}
|
|
||||||
onChange={(e) => setFormData({...formData, country: e.target.value})}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Textarea
|
|
||||||
label="Mensaje"
|
|
||||||
placeholder="Escribe tu mensaje..."
|
|
||||||
rows={5}
|
|
||||||
maxLength={500}
|
|
||||||
showCharacterCount
|
|
||||||
value={formData.message}
|
|
||||||
onChange={(e) => setFormData({...formData, message: e.target.value})}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label>Método de contacto preferido:</label>
|
|
||||||
<Radio
|
|
||||||
label="Email"
|
|
||||||
name="contactMethod"
|
|
||||||
value="email"
|
|
||||||
checked={formData.contactMethod === 'email'}
|
|
||||||
onChange={(e) => setFormData({...formData, contactMethod: e.target.value})}
|
|
||||||
/>
|
|
||||||
<Radio
|
|
||||||
label="Teléfono"
|
|
||||||
name="contactMethod"
|
|
||||||
value="phone"
|
|
||||||
checked={formData.contactMethod === 'phone'}
|
|
||||||
onChange={(e) => setFormData({...formData, contactMethod: e.target.value})}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Checkbox
|
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user