feat: Add profile dropdown component and integrate with appbar for user menu
This commit is contained in:
@ -11,11 +11,13 @@ import {
|
|||||||
|
|
||||||
import * as domtools from '@design.estate/dees-domtools';
|
import * as domtools from '@design.estate/dees-domtools';
|
||||||
import * as interfaces from './interfaces/index.js';
|
import * as interfaces from './interfaces/index.js';
|
||||||
|
import * as plugins from './00plugins.js';
|
||||||
import { demoFunc } from './dees-appui-appbar.demo.js';
|
import { demoFunc } from './dees-appui-appbar.demo.js';
|
||||||
|
|
||||||
// Import required components
|
// Import required components
|
||||||
import './dees-icon.js';
|
import './dees-icon.js';
|
||||||
import './dees-windowcontrols.js';
|
import './dees-windowcontrols.js';
|
||||||
|
import './dees-appui-profiledropdown.js';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@ -44,10 +46,14 @@ export class DeesAppuiBar extends DeesElement {
|
|||||||
@property({ type: Object })
|
@property({ type: Object })
|
||||||
public user?: {
|
public user?: {
|
||||||
name: string;
|
name: string;
|
||||||
|
email?: string;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
status?: 'online' | 'offline' | 'busy' | 'away';
|
status?: 'online' | 'offline' | 'busy' | 'away';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
public profileMenuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string } | { divider: true })[] = [];
|
||||||
|
|
||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
public showSearch: boolean = false;
|
public showSearch: boolean = false;
|
||||||
|
|
||||||
@ -64,6 +70,9 @@ export class DeesAppuiBar extends DeesElement {
|
|||||||
@state()
|
@state()
|
||||||
private focusedDropdownItem: number = -1;
|
private focusedDropdownItem: number = -1;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private isProfileDropdownOpen: boolean = false;
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
css`
|
css`
|
||||||
@ -102,7 +111,7 @@ export class DeesAppuiBar extends DeesElement {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
-webkit-app-region: no-drag;
|
-webkit-app-region: no-drag;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
cursor: pointer;
|
cursor: default;
|
||||||
outline: none;
|
outline: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -162,7 +171,7 @@ export class DeesAppuiBar extends DeesElement {
|
|||||||
|
|
||||||
.dropdown-item {
|
.dropdown-item {
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
cursor: pointer;
|
cursor: default;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@ -206,7 +215,7 @@ export class DeesAppuiBar extends DeesElement {
|
|||||||
|
|
||||||
.breadcrumb-item {
|
.breadcrumb-item {
|
||||||
color: ${cssManager.bdTheme('#00000080', '#ffffff80')};
|
color: ${cssManager.bdTheme('#00000080', '#ffffff80')};
|
||||||
cursor: pointer;
|
cursor: default;
|
||||||
transition: color 0.2s;
|
transition: color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -229,7 +238,7 @@ export class DeesAppuiBar extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.search-icon {
|
.search-icon {
|
||||||
cursor: pointer;
|
cursor: default;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
}
|
}
|
||||||
@ -242,7 +251,7 @@ export class DeesAppuiBar extends DeesElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
cursor: pointer;
|
cursor: default;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
@ -413,11 +422,12 @@ export class DeesAppuiBar extends DeesElement {
|
|||||||
${this.showSearch ? html`
|
${this.showSearch ? html`
|
||||||
<dees-icon
|
<dees-icon
|
||||||
class="search-icon"
|
class="search-icon"
|
||||||
.iconName=${'search'}
|
.icon=${'lucide:search'}
|
||||||
@click=${this.handleSearchClick}
|
@click=${this.handleSearchClick}
|
||||||
></dees-icon>
|
></dees-icon>
|
||||||
` : ''}
|
` : ''}
|
||||||
${this.user ? html`
|
${this.user ? html`
|
||||||
|
<div style="position: relative;">
|
||||||
<div class="user-info" @click=${this.handleUserClick}>
|
<div class="user-info" @click=${this.handleUserClick}>
|
||||||
<div class="user-avatar">
|
<div class="user-avatar">
|
||||||
${this.user.avatar ?
|
${this.user.avatar ?
|
||||||
@ -430,6 +440,14 @@ export class DeesAppuiBar extends DeesElement {
|
|||||||
</div>
|
</div>
|
||||||
<span>${this.user.name}</span>
|
<span>${this.user.name}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<dees-appui-profiledropdown
|
||||||
|
.user=${this.user}
|
||||||
|
.menuItems=${this.profileMenuItems}
|
||||||
|
.isOpen=${this.isProfileDropdownOpen}
|
||||||
|
.position=${'top-right'}
|
||||||
|
@menu-select=${(e: CustomEvent) => this.handleProfileMenuSelect(e)}
|
||||||
|
></dees-appui-profiledropdown>
|
||||||
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -536,12 +554,26 @@ export class DeesAppuiBar extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private handleUserClick() {
|
private handleUserClick() {
|
||||||
|
this.isProfileDropdownOpen = !this.isProfileDropdownOpen;
|
||||||
|
|
||||||
|
// Also emit the event for backward compatibility
|
||||||
this.dispatchEvent(new CustomEvent('user-menu-open', {
|
this.dispatchEvent(new CustomEvent('user-menu-open', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
composed: true
|
composed: true
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleProfileMenuSelect(e: CustomEvent) {
|
||||||
|
this.isProfileDropdownOpen = false;
|
||||||
|
|
||||||
|
// Re-emit the event
|
||||||
|
this.dispatchEvent(new CustomEvent('profile-menu-select', {
|
||||||
|
detail: e.detail,
|
||||||
|
bubbles: true,
|
||||||
|
composed: true
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
async connectedCallback() {
|
async connectedCallback() {
|
||||||
await super.connectedCallback();
|
await super.connectedCallback();
|
||||||
@ -564,6 +596,7 @@ export class DeesAppuiBar extends DeesElement {
|
|||||||
// Close all dropdowns when clicking outside
|
// Close all dropdowns when clicking outside
|
||||||
this.activeMenu = null;
|
this.activeMenu = null;
|
||||||
this.focusedDropdownItem = -1;
|
this.focusedDropdownItem = -1;
|
||||||
|
// Note: Profile dropdown handles its own outside clicks
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleDropdownKeydown(e: KeyboardEvent, items: interfaces.IAppBarMenuItem[], _parentId: string) {
|
private handleDropdownKeydown(e: KeyboardEvent, items: interfaces.IAppBarMenuItem[], _parentId: string) {
|
||||||
|
@ -3,6 +3,7 @@ import type { DeesAppuiBase } from './dees-appui-base.js';
|
|||||||
import type { IAppBarMenuItem } from './interfaces/appbarmenuitem.js';
|
import type { IAppBarMenuItem } from './interfaces/appbarmenuitem.js';
|
||||||
import type { ITab } from './interfaces/tab.js';
|
import type { ITab } from './interfaces/tab.js';
|
||||||
import type { ISelectionOption } from './interfaces/selectionoption.js';
|
import type { ISelectionOption } from './interfaces/selectionoption.js';
|
||||||
|
import * as plugins from './00plugins.js';
|
||||||
import '@design.estate/dees-wcctools/demotools';
|
import '@design.estate/dees-wcctools/demotools';
|
||||||
|
|
||||||
export const demoFunc = () => {
|
export const demoFunc = () => {
|
||||||
@ -86,6 +87,17 @@ export const demoFunc = () => {
|
|||||||
{ key: 'Metrics', iconName: 'lineChart', action: () => console.log('Metrics tab') },
|
{ key: 'Metrics', iconName: 'lineChart', action: () => console.log('Metrics tab') },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Profile menu items
|
||||||
|
const profileMenuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string } | { divider: true })[] = [
|
||||||
|
{ name: 'Profile Settings', iconName: 'user', action: async () => console.log('Profile settings') },
|
||||||
|
{ name: 'Account', iconName: 'settings', action: async () => console.log('Account settings') },
|
||||||
|
{ 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') }
|
||||||
|
];
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<dees-demowrapper>
|
<dees-demowrapper>
|
||||||
<style>
|
<style>
|
||||||
@ -106,8 +118,10 @@ export const demoFunc = () => {
|
|||||||
.appbarBreadcrumbs=${'Dashboard'}
|
.appbarBreadcrumbs=${'Dashboard'}
|
||||||
.appbarUser=${{
|
.appbarUser=${{
|
||||||
name: 'Jane Smith',
|
name: 'Jane Smith',
|
||||||
|
email: 'jane.smith@example.com',
|
||||||
status: 'online' as 'online' | 'offline' | 'busy' | 'away'
|
status: 'online' as 'online' | 'offline' | 'busy' | 'away'
|
||||||
}}
|
}}
|
||||||
|
.appbarProfileMenuItems=${profileMenuItems}
|
||||||
.appbarShowWindowControls=${true}
|
.appbarShowWindowControls=${true}
|
||||||
.appbarShowSearch=${true}
|
.appbarShowSearch=${true}
|
||||||
.mainmenuTabs=${mainMenuTabs}
|
.mainmenuTabs=${mainMenuTabs}
|
||||||
@ -117,6 +131,7 @@ export const demoFunc = () => {
|
|||||||
@appbar-breadcrumb-navigate=${(e: CustomEvent) => console.log('Breadcrumb:', e.detail)}
|
@appbar-breadcrumb-navigate=${(e: CustomEvent) => console.log('Breadcrumb:', e.detail)}
|
||||||
@appbar-search-click=${() => console.log('Search clicked')}
|
@appbar-search-click=${() => console.log('Search clicked')}
|
||||||
@appbar-user-menu-open=${() => console.log('User menu opened')}
|
@appbar-user-menu-open=${() => console.log('User menu opened')}
|
||||||
|
@appbar-profile-menu-select=${(e: CustomEvent) => console.log('Profile menu selected:', e.detail)}
|
||||||
@mainmenu-tab-select=${(e: CustomEvent) => console.log('Tab selected:', e.detail)}
|
@mainmenu-tab-select=${(e: CustomEvent) => console.log('Tab selected:', e.detail)}
|
||||||
@mainselector-option-select=${(e: CustomEvent) => console.log('Option selected:', e.detail)}
|
@mainselector-option-select=${(e: CustomEvent) => console.log('Option selected:', e.detail)}
|
||||||
>
|
>
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
state,
|
state,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
import * as interfaces from './interfaces/index.js';
|
import * as interfaces from './interfaces/index.js';
|
||||||
|
import * as plugins from './00plugins.js';
|
||||||
import type { DeesAppuiBar } from './dees-appui-appbar.js';
|
import type { DeesAppuiBar } from './dees-appui-appbar.js';
|
||||||
import type { DeesAppuiMainmenu } from './dees-appui-mainmenu.js';
|
import type { DeesAppuiMainmenu } from './dees-appui-mainmenu.js';
|
||||||
import type { DeesAppuiMainselector } from './dees-appui-mainselector.js';
|
import type { DeesAppuiMainselector } from './dees-appui-mainselector.js';
|
||||||
@ -44,10 +45,14 @@ export class DeesAppuiBase extends DeesElement {
|
|||||||
@property({ type: Object })
|
@property({ type: Object })
|
||||||
public appbarUser?: {
|
public appbarUser?: {
|
||||||
name: string;
|
name: string;
|
||||||
|
email?: string;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
status?: 'online' | 'offline' | 'busy' | 'away';
|
status?: 'online' | 'offline' | 'busy' | 'away';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
public appbarProfileMenuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string } | { divider: true })[] = [];
|
||||||
|
|
||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
public appbarShowSearch: boolean = false;
|
public appbarShowSearch: boolean = false;
|
||||||
|
|
||||||
@ -115,11 +120,13 @@ export class DeesAppuiBase extends DeesElement {
|
|||||||
.breadcrumbSeparator=${this.appbarBreadcrumbSeparator}
|
.breadcrumbSeparator=${this.appbarBreadcrumbSeparator}
|
||||||
.showWindowControls=${this.appbarShowWindowControls}
|
.showWindowControls=${this.appbarShowWindowControls}
|
||||||
.user=${this.appbarUser}
|
.user=${this.appbarUser}
|
||||||
|
.profileMenuItems=${this.appbarProfileMenuItems}
|
||||||
.showSearch=${this.appbarShowSearch}
|
.showSearch=${this.appbarShowSearch}
|
||||||
@menu-select=${(e: CustomEvent) => this.handleAppbarMenuSelect(e)}
|
@menu-select=${(e: CustomEvent) => this.handleAppbarMenuSelect(e)}
|
||||||
@breadcrumb-navigate=${(e: CustomEvent) => this.handleAppbarBreadcrumbNavigate(e)}
|
@breadcrumb-navigate=${(e: CustomEvent) => this.handleAppbarBreadcrumbNavigate(e)}
|
||||||
@search-click=${() => this.handleAppbarSearchClick()}
|
@search-click=${() => this.handleAppbarSearchClick()}
|
||||||
@user-menu-open=${() => this.handleAppbarUserMenuOpen()}
|
@user-menu-open=${() => this.handleAppbarUserMenuOpen()}
|
||||||
|
@profile-menu-select=${(e: CustomEvent) => this.handleAppbarProfileMenuSelect(e)}
|
||||||
></dees-appui-appbar>
|
></dees-appui-appbar>
|
||||||
<div class="maingrid">
|
<div class="maingrid">
|
||||||
<dees-appui-mainmenu
|
<dees-appui-mainmenu
|
||||||
@ -182,6 +189,14 @@ export class DeesAppuiBase extends DeesElement {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleAppbarProfileMenuSelect(e: CustomEvent) {
|
||||||
|
this.dispatchEvent(new CustomEvent('appbar-profile-menu-select', {
|
||||||
|
detail: e.detail,
|
||||||
|
bubbles: true,
|
||||||
|
composed: true
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
// Event handlers for mainmenu
|
// Event handlers for mainmenu
|
||||||
private handleMainmenuTabSelect(e: CustomEvent) {
|
private handleMainmenuTabSelect(e: CustomEvent) {
|
||||||
this.mainmenuSelectedTab = e.detail.tab;
|
this.mainmenuSelectedTab = e.detail.tab;
|
||||||
|
401
ts_web/elements/dees-appui-profiledropdown.ts
Normal file
401
ts_web/elements/dees-appui-profiledropdown.ts
Normal file
@ -0,0 +1,401 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -78,7 +78,7 @@ export class DeesAppuiTabs extends DeesElement {
|
|||||||
.tab {
|
.tab {
|
||||||
color: ${cssManager.bdTheme('#666', '#a0a0a0')};
|
color: ${cssManager.bdTheme('#666', '#a0a0a0')};
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
cursor: pointer;
|
cursor: default;
|
||||||
transition: color 0.1s;
|
transition: color 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ export * from './dees-appui-base.js';
|
|||||||
export * from './dees-appui-maincontent.js';
|
export * from './dees-appui-maincontent.js';
|
||||||
export * from './dees-appui-mainmenu.js';
|
export * from './dees-appui-mainmenu.js';
|
||||||
export * from './dees-appui-mainselector.js';
|
export * from './dees-appui-mainselector.js';
|
||||||
|
export * from './dees-appui-profiledropdown.js';
|
||||||
export * from './dees-appui-tabs.js';
|
export * from './dees-appui-tabs.js';
|
||||||
export * from './dees-appui-view.js';
|
export * from './dees-appui-view.js';
|
||||||
export * from './dees-badge.js';
|
export * from './dees-badge.js';
|
||||||
|
Reference in New Issue
Block a user