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 { .dropdownMenu {
position: absolute; position: absolute;
top: calc(100% + 8px); top: calc(100% + 6px);
right: 0; right: 0;
background-color: var(--surface-color); 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; border-radius: 8px;
box-shadow: 0 12px 30px rgba(2,6,23,0.6); /* lighter shadow so it's less pronounced */
min-width: 260px; box-shadow: 0 6px 14px rgba(2,6,23,0.10);
padding: 8px 0; /* match the create-card min width (220px) so menus align visually */
width: 220px;
padding: 4px 0;
z-index: 1200; z-index: 1200;
animation: slideDown 0.18s cubic-bezier(.2,.9,.2,1); 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 { @keyframes slideDown {
from { from {
opacity: 0; opacity: 0;
@ -32,8 +49,8 @@
width: 100%; width: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 10px;
padding: 10px 16px; padding: 8px 14px;
background: transparent; background: transparent;
border: none; border: none;
color: var(--text-primary); color: var(--text-primary);
@ -46,6 +63,23 @@
background-color: var(--border-light); 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 */ /* ensure header (non-interactive) doesn't get hover background */
.dropdownHeader:hover { .dropdownHeader:hover {
background: transparent; background: transparent;
@ -66,7 +100,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
padding: 10px 16px; padding: 8px 14px;
color: var(--text-primary); color: var(--text-primary);
font-size: 14px; font-size: 14px;
opacity: 0.90; /* user requested opacity */ opacity: 0.90; /* user requested opacity */
@ -80,6 +114,37 @@
.divider { .divider {
height: 1px; height: 1px;
background-color: rgba(255,255,255,0.03); background-color: var(--border-light);
margin: 8px 0; 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}> <div className={styles.dropdownMenu}>
{items.map((item, index) => ( {items.map((item, index) => (
<React.Fragment key={index}> <React.Fragment key={index}>
{item.divider && <div className={styles.divider} />} {item.divider ? (
{item.disabled ? ( <div className={styles.divider} />
<div className={styles.dropdownHeader} {...(item.containerProps || {})}> ) : item.disabled ? (
{item.icon && <span className={styles.icon}>{item.icon}</span>} (() => {
{ const cp = item.containerProps || {};
// merge className for the header label const { className: cpClassName, ...cpRest } = cp as any;
(() => { const headerClasses = [styles.dropdownHeader, cpClassName].filter(Boolean).join(' ');
const lp = item.labelProps || {}; return (
const { className: lpClassName, ...lpOther } = lp as any; <div className={headerClasses} {...cpRest}>
const classes = [styles.dropdownHeaderLabel, lpClassName].filter(Boolean).join(' '); {item.icon && <span className={styles.icon}>{item.icon}</span>}
return <span className={classes} {...lpOther}>{item.label}</span>; {
})() // merge className for the header label
} (() => {
</div> 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} const cp = item.containerProps || {};
onClick={() => { const { className: cpClassName, ...cpRest } = cp as any;
item.onClick && item.onClick(); const btnClasses = [styles.dropdownItem, cpClassName].filter(Boolean).join(' ');
setIsOpen(false); return (
}} <button
{...(item.containerProps || {})} className={btnClasses}
> onClick={() => {
{item.icon && <span className={styles.icon}>{item.icon}</span>} item.onClick && item.onClick();
{ setIsOpen(false);
(() => { }}
const lp = item.labelProps || {}; {...cpRest}
const { className: lpClassName, ...lpOther } = lp as any; >
const classes = [lpClassName].filter(Boolean).join(' '); {item.icon && <span className={styles.icon}>{item.icon}</span>}
return <span className={classes} {...lpOther}>{item.label}</span>; {
})() (() => {
} const lp = item.labelProps || {};
</button> const { className: lpClassName, ...lpOther } = lp as any;
const classes = [lpClassName].filter(Boolean).join(' ');
return <span className={classes} {...lpOther}>{item.label}</span>;
})()
}
</button>
);
})()
)} )}
</React.Fragment> </React.Fragment>
))} ))}

View File

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

View File

@ -19,9 +19,28 @@
overflow-y: auto; 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 { .createGrid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 16px; gap: 16px;
} }
@ -58,6 +77,16 @@
.mainContent { .mainContent {
margin-left: 0; 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) { @media (max-width: 768px) {

View File

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

View File

@ -35,7 +35,8 @@
background-color: var(--surface-color); background-color: var(--surface-color);
border: 1px solid var(--border-light); border: 1px solid var(--border-light);
border-radius: 8px; border-radius: 8px;
overflow: hidden; /* allow dropdowns to overflow the table wrapper so menus aren't clipped */
overflow: visible;
} }
.transmissionsTable { .transmissionsTable {
@ -58,7 +59,7 @@
} }
.tableRow:hover { .tableRow:hover {
background-color: var(--active-bg-light); border-bottom: 1px solid var(--muted-200);
} }
.tableCell { .tableCell {
@ -68,6 +69,18 @@
border-bottom: 1px solid var(--border-light); 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 { .tableRow:last-child .tableCell {
border-bottom: none; border-bottom: none;
} }
@ -115,11 +128,20 @@
background: var(--surface-color); background: var(--surface-color);
border: 1px solid var(--border-light); border: 1px solid var(--border-light);
border-radius: 12px; 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 { .enterStudioButton {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 8px 16px; padding: 8px 16px;
min-height: 40px;
background-color: transparent; background-color: transparent;
border: 1px solid var(--primary-blue); border: 1px solid var(--primary-blue);
color: var(--primary-blue); color: var(--primary-blue);
@ -127,8 +149,8 @@
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
margin-right: 8px; margin-right: 12px;
transition: all 0.2s ease; transition: all 0.18s ease;
} }
.enterStudioButton:hover { .enterStudioButton:hover {
@ -160,6 +182,10 @@
font-style: italic; font-style: italic;
} }
.deleteItem {
color: var(--danger-600);
}
@media (max-width: 768px) { @media (max-width: 768px) {
.tableWrapper { .tableWrapper {
overflow-x: auto; overflow-x: auto;

View File

@ -1,5 +1,6 @@
import React, { useState } from 'react' 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 { FaYoutube, FaFacebook, FaTwitch, FaLinkedin } from 'react-icons/fa'
import { SkeletonTable } from './Skeleton' import { SkeletonTable } from './Skeleton'
import styles from './TransmissionsTable.module.css' import styles from './TransmissionsTable.module.css'
@ -105,14 +106,23 @@ const TransmissionsTable: React.FC<Props> = ({ transmissions, onDelete, onUpdate
</div> </div>
</div> </div>
<div className={styles.actionsCell}> <div className={styles.actionsCell}>
<button aria-label={`Entrar al estudio ${t.title}`} className={styles.enterStudioButton} onClick={() => {/* enter studio logic placeholder */}}> <button aria-label={`Entrar al estudio ${t.title}`} className={styles.enterStudioButton} onClick={() => {/* enter studio logic placeholder */}}>
Entrar al estudio Entrar al estudio
</button> </button>
<button aria-label={`Más opciones ${t.title}`} className={styles.moreOptionsButton}>
<MdMoreVert size={20} /> <Dropdown
</button> trigger={<button className={styles.moreOptionsButton} aria-label={`Más opciones ${t.title}`}><MdMoreVert size={20} /></button>}
</div> 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>
</div> </div>
</td> </td>