feat: Mejora del componente Dropdown con soporte para encabezados no interactivos y transiciones de fondo

This commit is contained in:
Cesar Mendivil 2025-11-05 17:00:41 -07:00
parent 01178e9532
commit 4575e3ce46
5 changed files with 117 additions and 18 deletions

View File

@ -43,7 +43,16 @@
} }
.dropdownItem:hover { .dropdownItem:hover {
background-color: rgba(255,255,255,0.03); background-color: var(--border-light);
}
/* ensure header (non-interactive) doesn't get hover background */
.dropdownHeader:hover {
background: transparent;
}
.dropdownItem {
transition: background-color 0.12s ease;
} }
.dropdownItem .icon { .dropdownItem .icon {
@ -53,6 +62,22 @@
color: var(--text-secondary); color: var(--text-secondary);
} }
.dropdownHeader {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 16px;
color: var(--text-primary);
font-size: 14px;
opacity: 0.90; /* user requested opacity */
cursor: default;
}
.dropdownHeaderLabel {
opacity: 0.90;
color: var(--text-primary);
}
.divider { .divider {
height: 1px; height: 1px;
background-color: rgba(255,255,255,0.03); background-color: rgba(255,255,255,0.03);

View File

@ -4,8 +4,11 @@ import styles from './Dropdown.module.css';
interface DropdownItem { interface DropdownItem {
label: string; label: string;
icon?: React.ReactNode; icon?: React.ReactNode;
onClick: () => void; onClick?: () => void;
divider?: boolean; divider?: boolean;
disabled?: boolean; // non-interactive header-like item
containerProps?: React.HTMLAttributes<HTMLElement>;
labelProps?: React.HTMLAttributes<HTMLSpanElement>;
} }
interface DropdownProps { interface DropdownProps {
@ -44,16 +47,39 @@ export const Dropdown: React.FC<DropdownProps> = ({ trigger, items }) => {
{items.map((item, index) => ( {items.map((item, index) => (
<React.Fragment key={index}> <React.Fragment key={index}>
{item.divider && <div className={styles.divider} />} {item.divider && <div className={styles.divider} />}
<button {item.disabled ? (
className={styles.dropdownItem} <div className={styles.dropdownHeader} {...(item.containerProps || {})}>
onClick={() => { {item.icon && <span className={styles.icon}>{item.icon}</span>}
item.onClick(); {
setIsOpen(false); // merge className for the header label
}} (() => {
> const lp = item.labelProps || {};
{item.icon && <span className={styles.icon}>{item.icon}</span>} const { className: lpClassName, ...lpOther } = lp as any;
<span>{item.label}</span> const classes = [styles.dropdownHeaderLabel, lpClassName].filter(Boolean).join(' ');
</button> 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>
)}
</React.Fragment> </React.Fragment>
))} ))}
</div> </div>

View File

@ -114,7 +114,12 @@
} }
.userEmail { .userEmail {
opacity: 0.95; opacity: 0.90;
font-size: 14px;
color: var(--text-secondary);
}
.userTitleMenu {
font-size: 14px; font-size: 14px;
color: var(--text-secondary); color: var(--text-secondary);
} }

View File

@ -13,11 +13,55 @@ const Header: React.FC = () => {
window.location.href = '/auth/login' window.location.href = '/auth/login'
} }
// Read mock user from localStorage (if set elsewhere in the app)
const storedUser = typeof window !== 'undefined' ? localStorage.getItem('mock_user') : null
let email = 'Usuario'
let avatarUrl: string | null = null
let displayName: string | null = null
try {
if (storedUser) {
const parsed = JSON.parse(storedUser)
email = parsed.email || storedUser || 'Usuario'
avatarUrl = parsed.avatar || parsed.photo || parsed.picture || null
displayName = parsed.name || parsed.fullname || null
}
} catch (e) {
email = storedUser || 'Usuario'
}
const computeInitials = (nameOrEmail: string) => {
if (!nameOrEmail) return 'U'
const name = nameOrEmail.split('@')[0]
const parts = name.split(/[^A-Za-z0-9]+/).filter(Boolean)
if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase()
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase()
return nameOrEmail.slice(0, 2).toUpperCase()
}
const initials = computeInitials(displayName || email)
const avatarElement = avatarUrl ? (
<img src={avatarUrl} alt="avatar" style={{ width: 28, height: 28, borderRadius: '50%', objectFit: 'cover' }} />
) : (
<div style={{ width: 28, height: 28, borderRadius: '50%', background: 'var(--primary-blue)', color: 'white', display: 'grid', placeItems: 'center', fontSize: 12, fontWeight: 700 }}>
{initials}
</div>
)
const userMenuItems = [ const userMenuItems = [
// non-interactive header with email
{
label: email,
icon: avatarElement,
disabled: true,
containerProps: { 'data-test': 'user-email', id: 'user-email' },
labelProps: { style: { opacity: 0.7 } }
},
{ {
label: 'Mi perfil', label: 'Mi perfil',
icon: <MdPerson size={18} />, icon: <MdPerson size={18} />,
onClick: () => console.log('Ir a perfil') onClick: () => console.log('Ir a perfil'),
divider: true // separator after the header
}, },
{ {
label: 'Ayuda', label: 'Ayuda',
@ -76,7 +120,7 @@ const Header: React.FC = () => {
<Dropdown <Dropdown
trigger={ trigger={
<div className={styles.userMenu}> <div className={styles.userMenu}>
<span className={styles.userEmail}>nextv.stream@gmail.com</span> <span className={styles.userTitleMenu}>Mi cuenta</span>
</div> </div>
} }
items={userMenuItems} items={userMenuItems}

View File

@ -42,7 +42,8 @@ const Sidebar: React.FC<SidebarProps> = ({ activeLink = 'inicio' }) => {
))} ))}
</ul> </ul>
{/* Secondary Navigation moved closer to storage */} </nav>
{/* Secondary Navigation moved closer to storage */}
<div className={styles.secondaryNavGroup}> <div className={styles.secondaryNavGroup}>
<ul className={styles.navList}> <ul className={styles.navList}>
{secondaryNavItems.map(item => ( {secondaryNavItems.map(item => (
@ -55,8 +56,6 @@ const Sidebar: React.FC<SidebarProps> = ({ activeLink = 'inicio' }) => {
))} ))}
</ul> </ul>
</div> </div>
</nav>
{/* Storage Info */} {/* Storage Info */}
<div className={styles.storageInfo}> <div className={styles.storageInfo}>
<div className={styles.storageTitle}> <div className={styles.storageTitle}>