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 {
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 {
@ -53,6 +62,22 @@
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 {
height: 1px;
background-color: rgba(255,255,255,0.03);

View File

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

View File

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

View File

@ -13,11 +13,55 @@ const Header: React.FC = () => {
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 = [
// 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',
icon: <MdPerson size={18} />,
onClick: () => console.log('Ir a perfil')
onClick: () => console.log('Ir a perfil'),
divider: true // separator after the header
},
{
label: 'Ayuda',
@ -76,7 +120,7 @@ const Header: React.FC = () => {
<Dropdown
trigger={
<div className={styles.userMenu}>
<span className={styles.userEmail}>nextv.stream@gmail.com</span>
<span className={styles.userTitleMenu}>Mi cuenta</span>
</div>
}
items={userMenuItems}

View File

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