feat: Mejora del componente Dropdown y ajustes en estilos de la tabla de transmisiones
This commit is contained in:
parent
4575e3ce46
commit
343ba1675e
@ -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;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
))}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user