feat: Mejora del componente Dropdown con soporte para encabezados no interactivos y transiciones de fondo
This commit is contained in:
parent
01178e9532
commit
4575e3ce46
@ -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);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user