feat: Mejora del componente Dropdown y ajustes en estilos de la tabla de transmisiones

This commit is contained in:
Cesar Mendivil 2025-11-05 18:12:54 -07:00
parent 4575e3ce46
commit 343ba1675e
7 changed files with 260 additions and 105 deletions

View File

@ -5,18 +5,35 @@
.dropdownMenu {
position: absolute;
top: calc(100% + 8px);
top: calc(100% + 6px);
right: 0;
background-color: var(--surface-color);
border: 1px solid rgba(255,255,255,0.04);
border: 1px solid rgba(0,0,0,0.04);
border-radius: 8px;
box-shadow: 0 12px 30px rgba(2,6,23,0.6);
min-width: 260px;
padding: 8px 0;
/* lighter shadow so it's less pronounced */
box-shadow: 0 6px 14px rgba(2,6,23,0.10);
/* match the create-card min width (220px) so menus align visually */
width: 220px;
padding: 4px 0;
z-index: 1200;
animation: slideDown 0.18s cubic-bezier(.2,.9,.2,1);
}
/* caret (little pointer) under the trigger */
.dropdownMenu::before {
content: '';
position: absolute;
top: -6px;
/* horizontal offset from the right edge; configurable via --dropdown-caret-right */
right: var(--dropdown-caret-right, 16px);
width: 12px;
height: 12px;
background: var(--surface-color);
transform: rotate(45deg);
border-left: 1px solid rgba(0,0,0,0.04);
border-top: 1px solid rgba(0,0,0,0.04);
}
@keyframes slideDown {
from {
opacity: 0;
@ -32,8 +49,8 @@
width: 100%;
display: flex;
align-items: center;
gap: 12px;
padding: 10px 16px;
gap: 10px;
padding: 8px 14px;
background: transparent;
border: none;
color: var(--text-primary);
@ -46,6 +63,23 @@
background-color: var(--border-light);
}
/* Ensure delete item doesn't take native button appearance and stays menu-like */
.dropdownItem.deleteItem,
.dropdownItem.deleteItem:hover {
background: transparent;
border: none;
color: var(--danger-600);
}
.dropdownItem.deleteItem:hover {
background-color: rgba(234,67,53,0.04);
}
.dropdownItem:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(59,130,246,0.12);
}
/* ensure header (non-interactive) doesn't get hover background */
.dropdownHeader:hover {
background: transparent;
@ -66,7 +100,7 @@
display: flex;
align-items: center;
gap: 12px;
padding: 10px 16px;
padding: 8px 14px;
color: var(--text-primary);
font-size: 14px;
opacity: 0.90; /* user requested opacity */
@ -80,6 +114,37 @@
.divider {
height: 1px;
background-color: rgba(255,255,255,0.03);
background-color: var(--border-light);
margin: 8px 0;
}
/* slightly reduce icon size and align spacing like the screenshot */
.dropdownItem .icon {
display: flex;
align-items: center;
font-size: 16px;
color: var(--text-secondary);
}
/* when a container has .deleteItem, ensure child icon and label inherit danger color */
.deleteItem .icon,
.deleteItem span {
color: var(--danger-600);
}
/* make delete icon sit inside a small rounded red box like the screenshot */
.deleteItem .icon {
background: rgba(234,67,53,0.08); /* subtle red bg */
padding: 4px;
border-radius: 6px;
display: inline-flex;
align-items: center;
justify-content: center;
}
/* tighten divider */
.divider {
height: 1px;
background-color: rgba(0,0,0,0.04);
margin: 6px 0;
}

View File

@ -46,39 +46,54 @@ export const Dropdown: React.FC<DropdownProps> = ({ trigger, items }) => {
<div className={styles.dropdownMenu}>
{items.map((item, index) => (
<React.Fragment key={index}>
{item.divider && <div className={styles.divider} />}
{item.disabled ? (
<div className={styles.dropdownHeader} {...(item.containerProps || {})}>
{item.icon && <span className={styles.icon}>{item.icon}</span>}
{
// merge className for the header label
(() => {
const lp = item.labelProps || {};
const { className: lpClassName, ...lpOther } = lp as any;
const classes = [styles.dropdownHeaderLabel, lpClassName].filter(Boolean).join(' ');
return <span className={classes} {...lpOther}>{item.label}</span>;
})()
}
</div>
{item.divider ? (
<div className={styles.divider} />
) : item.disabled ? (
(() => {
const cp = item.containerProps || {};
const { className: cpClassName, ...cpRest } = cp as any;
const headerClasses = [styles.dropdownHeader, cpClassName].filter(Boolean).join(' ');
return (
<div className={headerClasses} {...cpRest}>
{item.icon && <span className={styles.icon}>{item.icon}</span>}
{
// merge className for the header label
(() => {
const lp = item.labelProps || {};
const { className: lpClassName, ...lpOther } = lp as any;
const classes = [styles.dropdownHeaderLabel, lpClassName].filter(Boolean).join(' ');
return <span className={classes} {...lpOther}>{item.label}</span>;
})()
}
</div>
);
})()
) : (
<button
className={styles.dropdownItem}
onClick={() => {
item.onClick && item.onClick();
setIsOpen(false);
}}
{...(item.containerProps || {})}
>
{item.icon && <span className={styles.icon}>{item.icon}</span>}
{
(() => {
const lp = item.labelProps || {};
const { className: lpClassName, ...lpOther } = lp as any;
const classes = [lpClassName].filter(Boolean).join(' ');
return <span className={classes} {...lpOther}>{item.label}</span>;
})()
}
</button>
(() => {
const cp = item.containerProps || {};
const { className: cpClassName, ...cpRest } = cp as any;
const btnClasses = [styles.dropdownItem, cpClassName].filter(Boolean).join(' ');
return (
<button
className={btnClasses}
onClick={() => {
item.onClick && item.onClick();
setIsOpen(false);
}}
{...cpRest}
>
{item.icon && <span className={styles.icon}>{item.icon}</span>}
{
(() => {
const lp = item.labelProps || {};
const { className: lpClassName, ...lpOther } = lp as any;
const classes = [lpClassName].filter(Boolean).join(' ');
return <span className={classes} {...lpOther}>{item.label}</span>;
})()
}
</button>
);
})()
)}
</React.Fragment>
))}

View File

@ -115,10 +115,11 @@
}
.submitButton {
/* Match header .planButton visual: transparent with primary border, fill on hover */
padding: 10px 20px;
background-color: var(--primary-blue);
border: none;
color: white;
background-color: transparent;
border: 1px solid var(--primary-blue);
color: var(--primary-blue);
border-radius: 6px;
font-size: 14px;
font-weight: 500;
@ -127,7 +128,8 @@
}
.submitButton:hover {
background-color: var(--primary-blue-hover);
background-color: var(--primary-blue);
color: white;
}
@keyframes fadeIn {

View File

@ -19,9 +19,28 @@
overflow-y: auto;
}
/* two-column container: left column holds the two sections, right column reserved for secondary content
--left-col-width can be adjusted by container or page to control the left column width */
.wrapContent {
display: grid;
grid-template-columns: var(--left-col-width, 980px) 1fr;
gap: 24px;
align-items: start;
}
.leftStack {
display: flex;
flex-direction: column;
gap: 24px;
}
.rightStack {
display: block;
}
.createGrid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 16px;
}
@ -58,6 +77,16 @@
.mainContent {
margin-left: 0;
}
.wrapContent {
grid-template-columns: 1fr;
}
}
/* wider screens show the create cards in columns; keeps flexibility */
@media (max-width: 768px) {
.createGrid {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {

View File

@ -59,56 +59,64 @@ const PageContainer: React.FC = () => {
<div className={styles.mainContent}>
<Header />
<main className={styles.contentWrapper}>
{/* Sección Crear */}
<section style={{ marginBottom: '40px' }}>
<h2 style={{ fontSize: '22px', fontWeight: 600, marginBottom: '18px' }}>Crear</h2>
{isLoading ? (
<div className={styles.createGrid}>
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
</div>
) : (
<div className={styles.createGrid}>
<button
onClick={() => setIsModalOpen(true)}
className={styles.createCard}
>
<div className={styles.createIconBox}>
<MdVideocam size={20} style={{ color: 'var(--primary-blue)' }} />
<div className={styles.wrapContent}>
<div className={styles.leftStack}>
{/* Sección Crear */}
<section style={{ marginBottom: '0' }}>
<h2 style={{ fontSize: '22px', fontWeight: 600, marginBottom: '18px' }}>Crear</h2>
{isLoading ? (
<div className={styles.createGrid}>
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
</div>
<span>Transmisión en vivo</span>
</button>
) : (
<div className={styles.createGrid}>
<button
onClick={() => setIsModalOpen(true)}
className={styles.createCard}
>
<div className={styles.createIconBox}>
<MdVideocam size={20} style={{ color: 'var(--primary-blue)' }} />
</div>
<span>Transmisión en vivo</span>
</button>
<button className={styles.createCard}>
<div className={styles.createIconBox}>
<MdFiberManualRecord size={20} style={{ color: '#ea4335' }} />
<button className={styles.createCard}>
<div className={styles.createIconBox}>
<MdFiberManualRecord size={20} style={{ color: '#ea4335' }} />
</div>
<span>Grabación</span>
</button>
<button className={styles.createCard}>
<div className={styles.createIconBox}>
<MdSchool size={20} style={{ color: '#34a853' }} />
</div>
<span>Seminario web On-Air</span>
</button>
</div>
<span>Grabación</span>
</button>
)}
</section>
<button className={styles.createCard}>
<div className={styles.createIconBox}>
<MdSchool size={20} style={{ color: '#34a853' }} />
</div>
<span>Seminario web On-Air</span>
</button>
</div>
)}
</section>
{/* Sección Transmisiones y grabaciones */}
<section>
<h2 style={{ fontSize: '22px', fontWeight: 600, marginBottom: '24px' }}>
Transmisiones y grabaciones
</h2>
<TransmissionsTable
transmissions={transmissions}
onDelete={handleDelete}
onUpdate={handleUpdate}
isLoading={isLoading}
/>
</section>
</div>
{/* Sección Transmisiones y grabaciones */}
<section>
<h2 style={{ fontSize: '22px', fontWeight: 600, marginBottom: '24px' }}>
Transmisiones y grabaciones
</h2>
<TransmissionsTable
transmissions={transmissions}
onDelete={handleDelete}
onUpdate={handleUpdate}
isLoading={isLoading}
/>
</section>
<div className={styles.rightStack}>
{/* espacio para contenido secundario o ads, vacío por ahora */}
</div>
</div>
<NewTransmissionModal
open={isModalOpen}

View File

@ -35,7 +35,8 @@
background-color: var(--surface-color);
border: 1px solid var(--border-light);
border-radius: 8px;
overflow: hidden;
/* allow dropdowns to overflow the table wrapper so menus aren't clipped */
overflow: visible;
}
.transmissionsTable {
@ -58,7 +59,7 @@
}
.tableRow:hover {
background-color: var(--active-bg-light);
border-bottom: 1px solid var(--muted-200);
}
.tableCell {
@ -68,6 +69,18 @@
border-bottom: 1px solid var(--border-light);
}
.dangerLabel {
color: var(--danger-600);
}
.enterButton {
background: var(--brand-600);
color: white;
padding: 0.6rem 1rem;
border-radius: 8px;
font-weight: 600;
}
.tableRow:last-child .tableCell {
border-bottom: none;
}
@ -115,11 +128,20 @@
background: var(--surface-color);
border: 1px solid var(--border-light);
border-radius: 12px;
padding: 8px 12px;
padding: 12px 16px;
/* subtle elevation to match mock */
box-shadow: 0 6px 18px rgba(2,6,23,0.04);
/* constrain width to visually match the Create cards and center */
max-width: 980px;
margin: 0 auto;
}
.enterStudioButton {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 8px 16px;
min-height: 40px;
background-color: transparent;
border: 1px solid var(--primary-blue);
color: var(--primary-blue);
@ -127,8 +149,8 @@
font-size: 14px;
font-weight: 500;
cursor: pointer;
margin-right: 8px;
transition: all 0.2s ease;
margin-right: 12px;
transition: all 0.18s ease;
}
.enterStudioButton:hover {
@ -160,6 +182,10 @@
font-style: italic;
}
.deleteItem {
color: var(--danger-600);
}
@media (max-width: 768px) {
.tableWrapper {
overflow-x: auto;

View File

@ -1,5 +1,6 @@
import React, { useState } from 'react'
import { MdMoreVert, MdVideocam } from 'react-icons/md'
import { MdMoreVert, MdVideocam, MdPersonAdd, MdEdit, MdOpenInNew, MdDelete } from 'react-icons/md'
import { Dropdown } from './Dropdown'
import { FaYoutube, FaFacebook, FaTwitch, FaLinkedin } from 'react-icons/fa'
import { SkeletonTable } from './Skeleton'
import styles from './TransmissionsTable.module.css'
@ -105,14 +106,23 @@ const TransmissionsTable: React.FC<Props> = ({ transmissions, onDelete, onUpdate
</div>
</div>
<div className={styles.actionsCell}>
<button aria-label={`Entrar al estudio ${t.title}`} className={styles.enterStudioButton} onClick={() => {/* enter studio logic placeholder */}}>
Entrar al estudio
</button>
<button aria-label={`Más opciones ${t.title}`} className={styles.moreOptionsButton}>
<MdMoreVert size={20} />
</button>
</div>
<div className={styles.actionsCell}>
<button aria-label={`Entrar al estudio ${t.title}`} className={styles.enterStudioButton} onClick={() => {/* enter studio logic placeholder */}}>
Entrar al estudio
</button>
<Dropdown
trigger={<button className={styles.moreOptionsButton} aria-label={`Más opciones ${t.title}`}><MdMoreVert size={20} /></button>}
items={[
{ label: 'Agregar invitados', icon: <MdPersonAdd size={16} />, onClick: () => console.log('Agregar invitados', t.id) },
{ label: 'Editar', icon: <MdEdit size={16} />, onClick: () => {/* editar */} },
{ divider: true, label: '', disabled: false },
{ label: 'Ver en YouTube', icon: <MdOpenInNew size={16} />, onClick: () => {/* abrir */} },
{ divider: true, label: '', disabled: false },
{ label: 'Eliminar transmisión', icon: <MdDelete size={16} />, onClick: () => onDelete(t.id), containerProps: { className: styles.deleteItem }, labelProps: { className: styles.dangerLabel } }
]}
/>
</div>
</div>
</div>
</td>