diff --git a/.playwright-mcp/streamyard-initial.png b/.playwright-mcp/streamyard-initial.png new file mode 100644 index 0000000..5f5a22f Binary files /dev/null and b/.playwright-mcp/streamyard-initial.png differ diff --git a/docs/STREAMYARD_STUDIO_UI_ANALYSIS.md b/docs/STREAMYARD_STUDIO_UI_ANALYSIS.md new file mode 100644 index 0000000..58a736f --- /dev/null +++ b/docs/STREAMYARD_STUDIO_UI_ANALYSIS.md @@ -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 + diff --git a/docs/STREAMYARD_UI_ANALYSIS.md b/docs/STREAMYARD_UI_ANALYSIS.md new file mode 100644 index 0000000..15d3ac3 --- /dev/null +++ b/docs/STREAMYARD_UI_ANALYSIS.md @@ -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**: + - ` + + ); +} +``` + +**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 + + + + + + +// Tamaños + + + + +// Con íconos + + + + +// Estados + + + +// Full width + +``` + +#### 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 + diff --git a/packages/ui-components/package.json b/packages/avanza-ui/package.json similarity index 77% rename from packages/ui-components/package.json rename to packages/avanza-ui/package.json index c8a61fb..a991ce9 100644 --- a/packages/ui-components/package.json +++ b/packages/avanza-ui/package.json @@ -1,30 +1,31 @@ { "name": "avanza-ui", "version": "1.0.0", - "type": "module", - "description": "Sistema de componentes UI independiente - Inspirado en Tailwind CSS y Vristo", + "description": "Biblioteca de componentes React para AvanzaCast basada en StreamYard y unificada con ui-components", "main": "dist/index.js", "module": "dist/index.esm.js", "types": "dist/index.d.ts", "files": [ - "dist" + "dist", + "src" ], "scripts": { "build": "rollup -c", "dev": "rollup -c -w", + "typecheck": "tsc --noEmit", "test": "vitest", - "test:ui": "vitest --ui", - "type-check": "tsc --noEmit" + "prepublishOnly": "npm run build" }, "keywords": [ - "ui", - "components", "react", + "components", + "ui", "avanzacast", - "design-system" + "studio" ], "author": "AvanzaCast Team", "license": "MIT", + "private": true, "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" @@ -35,8 +36,6 @@ "@rollup/plugin-typescript": "^11.1.6", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", "rollup": "^4.18.0", "rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-postcss": "^4.0.2", @@ -48,4 +47,3 @@ "clsx": "^2.1.1" } } - diff --git a/packages/ui-components/src/components/Accordion.module.css b/packages/avanza-ui/src/components/Accordion.module.css similarity index 100% rename from packages/ui-components/src/components/Accordion.module.css rename to packages/avanza-ui/src/components/Accordion.module.css diff --git a/packages/ui-components/src/components/Accordion.tsx b/packages/avanza-ui/src/components/Accordion.tsx similarity index 100% rename from packages/ui-components/src/components/Accordion.tsx rename to packages/avanza-ui/src/components/Accordion.tsx diff --git a/packages/ui-components/src/components/Alert.module.css b/packages/avanza-ui/src/components/Alert.module.css similarity index 100% rename from packages/ui-components/src/components/Alert.module.css rename to packages/avanza-ui/src/components/Alert.module.css diff --git a/packages/ui-components/src/components/Alert.tsx b/packages/avanza-ui/src/components/Alert.tsx similarity index 100% rename from packages/ui-components/src/components/Alert.tsx rename to packages/avanza-ui/src/components/Alert.tsx diff --git a/packages/ui-components/src/components/Avatar.module.css b/packages/avanza-ui/src/components/Avatar.module.css similarity index 100% rename from packages/ui-components/src/components/Avatar.module.css rename to packages/avanza-ui/src/components/Avatar.module.css diff --git a/packages/ui-components/src/components/Avatar.tsx b/packages/avanza-ui/src/components/Avatar.tsx similarity index 100% rename from packages/ui-components/src/components/Avatar.tsx rename to packages/avanza-ui/src/components/Avatar.tsx diff --git a/packages/ui-components/src/components/Badge.module.css b/packages/avanza-ui/src/components/Badge.module.css similarity index 100% rename from packages/ui-components/src/components/Badge.module.css rename to packages/avanza-ui/src/components/Badge.module.css diff --git a/packages/ui-components/src/components/Badge.tsx b/packages/avanza-ui/src/components/Badge.tsx similarity index 100% rename from packages/ui-components/src/components/Badge.tsx rename to packages/avanza-ui/src/components/Badge.tsx diff --git a/packages/ui-components/src/components/Breadcrumb.module.css b/packages/avanza-ui/src/components/Breadcrumb.module.css similarity index 100% rename from packages/ui-components/src/components/Breadcrumb.module.css rename to packages/avanza-ui/src/components/Breadcrumb.module.css diff --git a/packages/ui-components/src/components/Breadcrumb.tsx b/packages/avanza-ui/src/components/Breadcrumb.tsx similarity index 100% rename from packages/ui-components/src/components/Breadcrumb.tsx rename to packages/avanza-ui/src/components/Breadcrumb.tsx diff --git a/packages/ui-components/src/components/Button.module.css b/packages/avanza-ui/src/components/Button.module.css similarity index 100% rename from packages/ui-components/src/components/Button.module.css rename to packages/avanza-ui/src/components/Button.module.css diff --git a/packages/ui-components/src/components/Button.tsx b/packages/avanza-ui/src/components/Button.tsx similarity index 100% rename from packages/ui-components/src/components/Button.tsx rename to packages/avanza-ui/src/components/Button.tsx diff --git a/packages/avanza-ui/src/components/Button/Button.css b/packages/avanza-ui/src/components/Button/Button.css new file mode 100644 index 0000000..3f9e278 --- /dev/null +++ b/packages/avanza-ui/src/components/Button/Button.css @@ -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; +} + diff --git a/packages/avanza-ui/src/components/Button/Button.tsx b/packages/avanza-ui/src/components/Button/Button.tsx new file mode 100644 index 0000000..a9d83f9 --- /dev/null +++ b/packages/avanza-ui/src/components/Button/Button.tsx @@ -0,0 +1,98 @@ +import React from 'react'; +import './Button.css'; + +export interface ButtonProps extends React.ButtonHTMLAttributes { + /** 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( + ( + { + 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.displayName = 'Button'; + +export default Button; + diff --git a/packages/avanza-ui/src/components/Button/index.ts b/packages/avanza-ui/src/components/Button/index.ts new file mode 100644 index 0000000..749d693 --- /dev/null +++ b/packages/avanza-ui/src/components/Button/index.ts @@ -0,0 +1,4 @@ +export { Button } from './Button'; +export type { ButtonProps } from './Button'; +export { Button as default } from './Button'; + diff --git a/packages/ui-components/src/components/Card.module.css b/packages/avanza-ui/src/components/Card.module.css similarity index 100% rename from packages/ui-components/src/components/Card.module.css rename to packages/avanza-ui/src/components/Card.module.css diff --git a/packages/ui-components/src/components/Card.tsx b/packages/avanza-ui/src/components/Card.tsx similarity index 100% rename from packages/ui-components/src/components/Card.tsx rename to packages/avanza-ui/src/components/Card.tsx diff --git a/packages/ui-components/src/components/Checkbox.module.css b/packages/avanza-ui/src/components/Checkbox.module.css similarity index 100% rename from packages/ui-components/src/components/Checkbox.module.css rename to packages/avanza-ui/src/components/Checkbox.module.css diff --git a/packages/ui-components/src/components/Checkbox.tsx b/packages/avanza-ui/src/components/Checkbox.tsx similarity index 100% rename from packages/ui-components/src/components/Checkbox.tsx rename to packages/avanza-ui/src/components/Checkbox.tsx diff --git a/packages/avanza-ui/src/components/ControlButton.module.css b/packages/avanza-ui/src/components/ControlButton.module.css new file mode 100644 index 0000000..8326b28 --- /dev/null +++ b/packages/avanza-ui/src/components/ControlButton.module.css @@ -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; +} + diff --git a/packages/avanza-ui/src/components/ControlButton.tsx b/packages/avanza-ui/src/components/ControlButton.tsx new file mode 100644 index 0000000..d1e3059 --- /dev/null +++ b/packages/avanza-ui/src/components/ControlButton.tsx @@ -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 = (props) => { + const { + icon, + label, + active = false, + danger = false, + size = 'md', + onClick, + disabled = false, + title, + className, + style, + id, + } = props; + + return ( + + ); +}; + +ControlButton.displayName = 'ControlButton'; + diff --git a/packages/ui-components/src/components/Dropdown.module.css b/packages/avanza-ui/src/components/Dropdown.module.css similarity index 100% rename from packages/ui-components/src/components/Dropdown.module.css rename to packages/avanza-ui/src/components/Dropdown.module.css diff --git a/packages/ui-components/src/components/Dropdown.tsx b/packages/avanza-ui/src/components/Dropdown.tsx similarity index 100% rename from packages/ui-components/src/components/Dropdown.tsx rename to packages/avanza-ui/src/components/Dropdown.tsx diff --git a/packages/ui-components/src/components/Input.module.css b/packages/avanza-ui/src/components/Input.module.css similarity index 100% rename from packages/ui-components/src/components/Input.module.css rename to packages/avanza-ui/src/components/Input.module.css diff --git a/packages/ui-components/src/components/Input.tsx b/packages/avanza-ui/src/components/Input.tsx similarity index 93% rename from packages/ui-components/src/components/Input.tsx rename to packages/avanza-ui/src/components/Input.tsx index b6fd4a1..6db122d 100644 --- a/packages/ui-components/src/components/Input.tsx +++ b/packages/avanza-ui/src/components/Input.tsx @@ -72,11 +72,11 @@ export const Input = React.forwardRef( const wrapperClasses = cn( styles.inputWrapper, styles[size], - error && styles.error, - success && styles.success, - leftIcon && styles.withLeftIcon, - rightIcon && styles.withRightIcon, - fullWidth && styles.fullWidth, + error ? styles.error : undefined, + success ? styles.success : undefined, + leftIcon ? styles.withLeftIcon : undefined, + rightIcon ? styles.withRightIcon : undefined, + fullWidth ? styles.fullWidth : undefined, className ); @@ -136,4 +136,3 @@ export const Input = React.forwardRef( ); Input.displayName = 'Input'; - diff --git a/packages/ui-components/src/components/Modal.module.css b/packages/avanza-ui/src/components/Modal.module.css similarity index 100% rename from packages/ui-components/src/components/Modal.module.css rename to packages/avanza-ui/src/components/Modal.module.css diff --git a/packages/ui-components/src/components/Modal.tsx b/packages/avanza-ui/src/components/Modal.tsx similarity index 100% rename from packages/ui-components/src/components/Modal.tsx rename to packages/avanza-ui/src/components/Modal.tsx diff --git a/packages/ui-components/src/components/Pagination.module.css b/packages/avanza-ui/src/components/Pagination.module.css similarity index 100% rename from packages/ui-components/src/components/Pagination.module.css rename to packages/avanza-ui/src/components/Pagination.module.css diff --git a/packages/ui-components/src/components/Pagination.tsx b/packages/avanza-ui/src/components/Pagination.tsx similarity index 100% rename from packages/ui-components/src/components/Pagination.tsx rename to packages/avanza-ui/src/components/Pagination.tsx diff --git a/packages/ui-components/src/components/Progress.module.css b/packages/avanza-ui/src/components/Progress.module.css similarity index 100% rename from packages/ui-components/src/components/Progress.module.css rename to packages/avanza-ui/src/components/Progress.module.css diff --git a/packages/ui-components/src/components/Progress.tsx b/packages/avanza-ui/src/components/Progress.tsx similarity index 100% rename from packages/ui-components/src/components/Progress.tsx rename to packages/avanza-ui/src/components/Progress.tsx diff --git a/packages/ui-components/src/components/Radio.module.css b/packages/avanza-ui/src/components/Radio.module.css similarity index 100% rename from packages/ui-components/src/components/Radio.module.css rename to packages/avanza-ui/src/components/Radio.module.css diff --git a/packages/ui-components/src/components/Radio.tsx b/packages/avanza-ui/src/components/Radio.tsx similarity index 100% rename from packages/ui-components/src/components/Radio.tsx rename to packages/avanza-ui/src/components/Radio.tsx diff --git a/packages/avanza-ui/src/components/SceneCard.module.css b/packages/avanza-ui/src/components/SceneCard.module.css new file mode 100644 index 0000000..c05eb7f --- /dev/null +++ b/packages/avanza-ui/src/components/SceneCard.module.css @@ -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); +} + diff --git a/packages/avanza-ui/src/components/SceneCard.tsx b/packages/avanza-ui/src/components/SceneCard.tsx new file mode 100644 index 0000000..2cd0aad --- /dev/null +++ b/packages/avanza-ui/src/components/SceneCard.tsx @@ -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 = (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 ( +
+
+ {preview ? ( + preview + ) : ( +
+ 📹 +
+ )} + {active && ( +
+ +
+ )} +
+
+
{title}
+
+
+ ); +}; + +SceneCard.displayName = 'SceneCard'; + diff --git a/packages/ui-components/src/components/Select.module.css b/packages/avanza-ui/src/components/Select.module.css similarity index 100% rename from packages/ui-components/src/components/Select.module.css rename to packages/avanza-ui/src/components/Select.module.css diff --git a/packages/ui-components/src/components/Select.tsx b/packages/avanza-ui/src/components/Select.tsx similarity index 100% rename from packages/ui-components/src/components/Select.tsx rename to packages/avanza-ui/src/components/Select.tsx diff --git a/packages/ui-components/src/components/Spinner.module.css b/packages/avanza-ui/src/components/Spinner.module.css similarity index 100% rename from packages/ui-components/src/components/Spinner.module.css rename to packages/avanza-ui/src/components/Spinner.module.css diff --git a/packages/ui-components/src/components/Spinner.tsx b/packages/avanza-ui/src/components/Spinner.tsx similarity index 100% rename from packages/ui-components/src/components/Spinner.tsx rename to packages/avanza-ui/src/components/Spinner.tsx diff --git a/packages/avanza-ui/src/components/StudioHeader.module.css b/packages/avanza-ui/src/components/StudioHeader.module.css new file mode 100644 index 0000000..747dea2 --- /dev/null +++ b/packages/avanza-ui/src/components/StudioHeader.module.css @@ -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; +} + diff --git a/packages/avanza-ui/src/components/StudioHeader.tsx b/packages/avanza-ui/src/components/StudioHeader.tsx new file mode 100644 index 0000000..d7d66aa --- /dev/null +++ b/packages/avanza-ui/src/components/StudioHeader.tsx @@ -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 = (props) => { + const { + logo, + logoText = 'AC', + title = 'AvanzaCast Studio', + subtitle = 'Estudio de Transmisión', + actions, + className, + style, + id, + } = props; + + return ( +
+
+ {logo || ( +
+ {logoText} +
+ )} +
+
{title}
+
{subtitle}
+
+
+ + {actions && ( +
+ {actions} +
+ )} +
+ ); +}; + +StudioHeader.displayName = 'StudioHeader'; + diff --git a/packages/ui-components/src/components/Switch.module.css b/packages/avanza-ui/src/components/Switch.module.css similarity index 100% rename from packages/ui-components/src/components/Switch.module.css rename to packages/avanza-ui/src/components/Switch.module.css diff --git a/packages/ui-components/src/components/Switch.tsx b/packages/avanza-ui/src/components/Switch.tsx similarity index 100% rename from packages/ui-components/src/components/Switch.tsx rename to packages/avanza-ui/src/components/Switch.tsx diff --git a/packages/ui-components/src/components/Tabs.module.css b/packages/avanza-ui/src/components/Tabs.module.css similarity index 100% rename from packages/ui-components/src/components/Tabs.module.css rename to packages/avanza-ui/src/components/Tabs.module.css diff --git a/packages/ui-components/src/components/Tabs.tsx b/packages/avanza-ui/src/components/Tabs.tsx similarity index 100% rename from packages/ui-components/src/components/Tabs.tsx rename to packages/avanza-ui/src/components/Tabs.tsx diff --git a/packages/ui-components/src/components/Textarea.module.css b/packages/avanza-ui/src/components/Textarea.module.css similarity index 100% rename from packages/ui-components/src/components/Textarea.module.css rename to packages/avanza-ui/src/components/Textarea.module.css diff --git a/packages/ui-components/src/components/Textarea.tsx b/packages/avanza-ui/src/components/Textarea.tsx similarity index 97% rename from packages/ui-components/src/components/Textarea.tsx rename to packages/avanza-ui/src/components/Textarea.tsx index 4637110..fe2c6e0 100644 --- a/packages/ui-components/src/components/Textarea.tsx +++ b/packages/avanza-ui/src/components/Textarea.tsx @@ -110,7 +110,7 @@ export const Textarea = React.forwardRef( {...rest} /> {showCharacterCount && maxLength && ( -
+
{charCount}/{maxLength}
)} @@ -130,4 +130,3 @@ export const Textarea = React.forwardRef( ); Textarea.displayName = 'Textarea'; - diff --git a/packages/ui-components/src/components/Tooltip.module.css b/packages/avanza-ui/src/components/Tooltip.module.css similarity index 100% rename from packages/ui-components/src/components/Tooltip.module.css rename to packages/avanza-ui/src/components/Tooltip.module.css diff --git a/packages/ui-components/src/components/Tooltip.tsx b/packages/avanza-ui/src/components/Tooltip.tsx similarity index 100% rename from packages/ui-components/src/components/Tooltip.tsx rename to packages/avanza-ui/src/components/Tooltip.tsx diff --git a/packages/avanza-ui/src/components/VideoTile.module.css b/packages/avanza-ui/src/components/VideoTile.module.css new file mode 100644 index 0000000..e185de1 --- /dev/null +++ b/packages/avanza-ui/src/components/VideoTile.module.css @@ -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); +} + diff --git a/packages/avanza-ui/src/components/VideoTile.tsx b/packages/avanza-ui/src/components/VideoTile.tsx new file mode 100644 index 0000000..1db996a --- /dev/null +++ b/packages/avanza-ui/src/components/VideoTile.tsx @@ -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 = (props) => { + const { + name, + stream = null, + muted = false, + isLocal = false, + isSpeaking = false, + connectionQuality = 'good', + onToggleMute, + onToggleCamera, + onRemove, + className, + style, + id, + } = props; + + const videoRef = useRef(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 ( +
+ {Array.from({ length: 4 }).map((_, i) => ( +
= filled && styles.inactive)} + style={{ height: `${6 + i * 4}px` }} + /> + ))} +
+ ); + }; + + return ( +
+