401 lines
10 KiB
TypeScript
401 lines
10 KiB
TypeScript
import * as plugins from './00plugins.js';
|
|
|
|
import {
|
|
DeesElement,
|
|
type TemplateResult,
|
|
property,
|
|
customElement,
|
|
html,
|
|
css,
|
|
cssManager,
|
|
state,
|
|
} from '@design.estate/dees-element';
|
|
|
|
@customElement('dees-appui-profiledropdown')
|
|
export class DeesAppuiProfileDropdown extends DeesElement {
|
|
public static demo = () => html`
|
|
<dees-appui-profiledropdown
|
|
.user=${{
|
|
name: 'John Doe',
|
|
email: 'john.doe@example.com',
|
|
avatar: 'https://randomuser.me/api/portraits/men/1.jpg',
|
|
status: 'online' as 'online'
|
|
}}
|
|
.menuItems=${[
|
|
{ name: 'Profile Settings', iconName: 'user', action: async () => console.log('Profile') },
|
|
{ name: 'Account', iconName: 'settings', action: async () => console.log('Account') },
|
|
{ divider: true },
|
|
{ name: 'Help & Support', iconName: 'helpCircle', action: async () => console.log('Help') },
|
|
{ name: 'Keyboard Shortcuts', iconName: 'keyboard', shortcut: 'Cmd+K', action: async () => console.log('Shortcuts') },
|
|
{ divider: true },
|
|
{ name: 'Sign Out', iconName: 'logOut', action: async () => console.log('Sign out') }
|
|
]}
|
|
.isOpen=${true}
|
|
></dees-appui-profiledropdown>
|
|
`;
|
|
|
|
@property({ type: Object })
|
|
public user?: {
|
|
name: string;
|
|
email?: string;
|
|
avatar?: string;
|
|
status?: 'online' | 'offline' | 'busy' | 'away';
|
|
};
|
|
|
|
@property({ type: Array })
|
|
public menuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string } | { divider: true })[] = [];
|
|
|
|
@property({ type: Boolean, reflect: true })
|
|
public isOpen: boolean = false;
|
|
|
|
@property({ type: String })
|
|
public position: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' = 'top-right';
|
|
|
|
public static styles = [
|
|
cssManager.defaultStyles,
|
|
css`
|
|
:host {
|
|
display: block;
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
right: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.dropdown {
|
|
position: absolute;
|
|
min-width: 220px;
|
|
background: ${cssManager.bdTheme('#ffffff', '#000000')};
|
|
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
|
border-radius: 4px;
|
|
box-shadow: ${cssManager.bdTheme(
|
|
'0 4px 12px rgba(0, 0, 0, 0.15)',
|
|
'0 4px 12px rgba(0, 0, 0, 0.3)'
|
|
)};
|
|
z-index: 1000;
|
|
opacity: 0;
|
|
transform: scale(0.95) translateY(-10px);
|
|
transition: opacity 0.2s, transform 0.2s;
|
|
pointer-events: none;
|
|
overflow: hidden;
|
|
font-size: 12px;
|
|
}
|
|
|
|
:host([isopen]) .dropdown {
|
|
opacity: 1;
|
|
transform: scale(1) translateY(0);
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.backdrop {
|
|
display: none;
|
|
}
|
|
|
|
/* Position variants */
|
|
.dropdown.top-right {
|
|
top: 100%;
|
|
right: 0;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.dropdown.top-left {
|
|
top: 100%;
|
|
left: 0;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.dropdown.bottom-right {
|
|
bottom: 100%;
|
|
right: 0;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.dropdown.bottom-left {
|
|
bottom: 100%;
|
|
left: 0;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
/* User section */
|
|
.user-section {
|
|
padding: 12px;
|
|
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
|
}
|
|
|
|
.user-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.user-avatar {
|
|
position: relative;
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 50%;
|
|
background: ${cssManager.bdTheme('#f0f0f0', '#1a1a1a')};
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: ${cssManager.bdTheme('#666', '#999')};
|
|
overflow: hidden;
|
|
}
|
|
|
|
.user-avatar img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.user-status {
|
|
position: absolute;
|
|
bottom: 0;
|
|
right: 0;
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
border: 2px solid ${cssManager.bdTheme('#ffffff', '#000000')};
|
|
}
|
|
|
|
.user-status.online {
|
|
background: #4caf50;
|
|
}
|
|
|
|
.user-status.offline {
|
|
background: #757575;
|
|
}
|
|
|
|
.user-status.busy {
|
|
background: #f44336;
|
|
}
|
|
|
|
.user-status.away {
|
|
background: #ff9800;
|
|
}
|
|
|
|
.user-details {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.user-name {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: ${cssManager.bdTheme('#000', '#fff')};
|
|
line-height: 1.2;
|
|
margin: 0;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.user-email {
|
|
font-size: 11px;
|
|
color: ${cssManager.bdTheme('#666', '#999')};
|
|
margin-top: 2px;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
/* Menu section */
|
|
.menu-section {
|
|
padding: 4px 0;
|
|
}
|
|
|
|
.menu-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 8px 12px;
|
|
cursor: default;
|
|
transition: background 0.1s;
|
|
color: ${cssManager.bdTheme('#333', '#ccc')};
|
|
font-size: 12px;
|
|
line-height: 1;
|
|
user-select: none;
|
|
}
|
|
|
|
.menu-item:hover {
|
|
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.08)')};
|
|
}
|
|
|
|
.menu-item:active {
|
|
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.12)')};
|
|
}
|
|
|
|
.menu-item dees-icon {
|
|
font-size: 14px;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.menu-item-text {
|
|
flex: 1;
|
|
}
|
|
|
|
.menu-shortcut {
|
|
font-size: 11px;
|
|
color: ${cssManager.bdTheme('#999', '#666')};
|
|
margin-left: auto;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.menu-divider {
|
|
height: 1px;
|
|
background: ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
|
margin: 4px 0;
|
|
}
|
|
|
|
/* Backdrop for mobile */
|
|
@media (max-width: 768px) {
|
|
.backdrop {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0, 0, 0, 0.3);
|
|
z-index: 999;
|
|
opacity: 0;
|
|
transition: opacity 0.2s;
|
|
display: none;
|
|
}
|
|
|
|
:host([isopen]) .backdrop {
|
|
display: block;
|
|
opacity: 1;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.dropdown {
|
|
position: fixed;
|
|
top: 50%;
|
|
left: 50%;
|
|
right: auto;
|
|
bottom: auto;
|
|
transform: translate(-50%, -50%) scale(0.95);
|
|
margin: 0;
|
|
max-width: calc(100vw - 32px);
|
|
max-height: calc(100vh - 32px);
|
|
overflow-y: auto;
|
|
}
|
|
|
|
:host([isopen]) .dropdown {
|
|
transform: translate(-50%, -50%) scale(1);
|
|
}
|
|
}
|
|
`,
|
|
];
|
|
|
|
public render(): TemplateResult {
|
|
return html`
|
|
<div class="backdrop" @click=${() => this.close()}></div>
|
|
<div class="dropdown ${this.position}">
|
|
${this.user ? html`
|
|
<div class="user-section">
|
|
<div class="user-info">
|
|
<div class="user-avatar">
|
|
${this.user.avatar
|
|
? html`<img src="${this.user.avatar}" alt="${this.user.name}">`
|
|
: this.getInitials(this.user.name)
|
|
}
|
|
${this.user.status ? html`
|
|
<div class="user-status ${this.user.status}"></div>
|
|
` : ''}
|
|
</div>
|
|
<div class="user-details">
|
|
<div class="user-name">${this.user.name}</div>
|
|
${this.user.email ? html`
|
|
<div class="user-email">${this.user.email}</div>
|
|
` : ''}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
|
|
<div class="menu-section">
|
|
${this.menuItems.map(item => this.renderMenuItem(item))}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private renderMenuItem(item: plugins.tsclass.website.IMenuItem & { shortcut?: string } | { divider: true }): TemplateResult {
|
|
if ('divider' in item && item.divider) {
|
|
return html`<div class="menu-divider"></div>`;
|
|
}
|
|
|
|
const menuItem = item as plugins.tsclass.website.IMenuItem & { iconName?: string; shortcut?: string };
|
|
return html`
|
|
<div class="menu-item" @click=${() => this.handleMenuClick(menuItem)}>
|
|
${menuItem.iconName ? html`
|
|
<dees-icon .icon="${`lucide:${menuItem.iconName}`}"></dees-icon>
|
|
` : ''}
|
|
<span class="menu-item-text">${menuItem.name}</span>
|
|
${menuItem.shortcut ? html`
|
|
<span class="menu-shortcut">${menuItem.shortcut}</span>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private getInitials(name: string): string {
|
|
return name
|
|
.split(' ')
|
|
.map(part => part[0])
|
|
.join('')
|
|
.toUpperCase()
|
|
.slice(0, 2);
|
|
}
|
|
|
|
private async handleMenuClick(item: plugins.tsclass.website.IMenuItem & { iconName?: string; shortcut?: string }) {
|
|
await item.action();
|
|
this.close();
|
|
|
|
// Emit menu-select event
|
|
this.dispatchEvent(new CustomEvent('menu-select', {
|
|
detail: { item },
|
|
bubbles: true,
|
|
composed: true
|
|
}));
|
|
}
|
|
|
|
public open() {
|
|
this.isOpen = true;
|
|
}
|
|
|
|
public close() {
|
|
this.isOpen = false;
|
|
}
|
|
|
|
public toggle() {
|
|
this.isOpen = !this.isOpen;
|
|
}
|
|
|
|
// Handle clicks outside the dropdown
|
|
async connectedCallback() {
|
|
await super.connectedCallback();
|
|
this.handleOutsideClick = this.handleOutsideClick.bind(this);
|
|
document.addEventListener('click', this.handleOutsideClick);
|
|
}
|
|
|
|
async disconnectedCallback() {
|
|
await super.disconnectedCallback();
|
|
document.removeEventListener('click', this.handleOutsideClick);
|
|
}
|
|
|
|
private handleOutsideClick(event: MouseEvent) {
|
|
if (this.isOpen && !this.contains(event.target as Node)) {
|
|
// Check if the click is on the parent element (which contains the profile button)
|
|
const parentElement = this.parentElement;
|
|
if (parentElement && parentElement.contains(event.target as Node)) {
|
|
// Don't close if clicking within the parent element (e.g., on the profile button)
|
|
return;
|
|
}
|
|
this.close();
|
|
}
|
|
}
|
|
} |