This commit is contained in:
2026-01-12 10:57:54 +00:00
parent c55cd25a88
commit 72900086cd
63 changed files with 3963 additions and 5078 deletions

View File

@@ -0,0 +1,261 @@
import {
customElement,
DeesElement,
type TemplateResult,
html,
property,
css,
cssManager,
} from '@design.estate/dees-element';
import { DeesIcon } from '@design.estate/dees-catalog';
import { demo } from './eco-applauncher-powermenu.demo.js';
// Ensure dees-icon is registered
DeesIcon;
declare global {
interface HTMLElementTagNameMap {
'eco-applauncher-powermenu': EcoApplauncherPowermenu;
}
}
export type TPowerAction = 'lock' | 'lock-sleep' | 'reboot';
@customElement('eco-applauncher-powermenu')
export class EcoApplauncherPowermenu extends DeesElement {
public static demo = demo;
public static demoGroup = 'App Launcher';
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
position: relative;
pointer-events: none;
}
:host([open]) {
pointer-events: auto;
}
.menu-container {
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 10%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 20%)')};
border-radius: 12px;
box-shadow: ${cssManager.bdTheme(
'0 8px 32px rgba(0, 0, 0, 0.15)',
'0 8px 32px rgba(0, 0, 0, 0.4)'
)};
min-width: 200px;
overflow: hidden;
opacity: 0;
transform: scale(0.95) translateY(-8px);
transition: all 0.2s ease-out;
pointer-events: none;
}
:host([open]) .menu-container {
opacity: 1;
transform: scale(1) translateY(0);
pointer-events: auto;
}
.menu-header {
padding: 12px 16px;
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 92%)', 'hsl(240 5% 15%)')};
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
}
.menu-options {
padding: 8px 0;
}
.menu-option {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
cursor: pointer;
transition: background 0.15s ease;
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
}
.menu-option:hover {
background: ${cssManager.bdTheme('hsl(0 0% 96%)', 'hsl(240 5% 15%)')};
}
.menu-option:active {
background: ${cssManager.bdTheme('hsl(0 0% 92%)', 'hsl(240 5% 18%)')};
}
.menu-option.danger {
color: ${cssManager.bdTheme('hsl(0 72% 45%)', 'hsl(0 72% 60%)')};
}
.menu-option.danger:hover {
background: ${cssManager.bdTheme('hsl(0 72% 97%)', 'hsl(0 50% 15%)')};
}
.option-icon {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 6px;
background: ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 18%)')};
}
.menu-option.danger .option-icon {
background: ${cssManager.bdTheme('hsl(0 72% 94%)', 'hsl(0 50% 18%)')};
}
.option-text {
display: flex;
flex-direction: column;
gap: 2px;
}
.option-label {
font-size: 14px;
font-weight: 500;
}
.option-description {
font-size: 11px;
color: ${cssManager.bdTheme('hsl(0 0% 55%)', 'hsl(0 0% 50%)')};
}
.menu-divider {
height: 1px;
background: ${cssManager.bdTheme('hsl(0 0% 92%)', 'hsl(240 5% 15%)')};
margin: 4px 0;
}
`,
];
@property({ type: Boolean, reflect: true })
accessor open = false;
private boundHandleClickOutside = this.handleClickOutside.bind(this);
private inactivityTimeout: ReturnType<typeof setTimeout> | null = null;
private readonly INACTIVITY_TIMEOUT = 60000; // 1 minute
private lastActivityTime = 0;
public render(): TemplateResult {
return html`
<div class="menu-container"
@click=${(e: MouseEvent) => e.stopPropagation()}
@mousedown=${this.resetInactivityTimer}
>
<div class="menu-header">Power</div>
<div class="menu-options">
<div class="menu-option" @click=${() => this.handleAction('lock')}>
<div class="option-icon">
<dees-icon .icon=${'lucide:lock'} .iconSize=${16}></dees-icon>
</div>
<div class="option-text">
<span class="option-label">Lock</span>
<span class="option-description">Lock the screen</span>
</div>
</div>
<div class="menu-option" @click=${() => this.handleAction('lock-sleep')}>
<div class="option-icon">
<dees-icon .icon=${'lucide:moon'} .iconSize=${16}></dees-icon>
</div>
<div class="option-text">
<span class="option-label">Lock + Sleep</span>
<span class="option-description">Lock and turn off display</span>
</div>
</div>
<div class="menu-divider"></div>
<div class="menu-option danger" @click=${() => this.handleAction('reboot')}>
<div class="option-icon">
<dees-icon .icon=${'lucide:refreshCw'} .iconSize=${16}></dees-icon>
</div>
<div class="option-text">
<span class="option-label">Reboot</span>
<span class="option-description">Restart the system</span>
</div>
</div>
</div>
</div>
`;
}
private handleAction(action: TPowerAction): void {
this.dispatchEvent(new CustomEvent('power-action', {
detail: { action },
bubbles: true,
composed: true,
}));
this.closeMenu();
}
private handleClickOutside(e: MouseEvent): void {
if (this.open && !this.contains(e.target as Node)) {
this.closeMenu();
}
}
private resetInactivityTimer(): void {
const now = Date.now();
// Throttle: only reset if 5+ seconds since last reset
if (now - this.lastActivityTime < 5000) {
return;
}
this.lastActivityTime = now;
this.clearInactivityTimer();
if (this.open) {
this.inactivityTimeout = setTimeout(() => {
this.closeMenu();
}, this.INACTIVITY_TIMEOUT);
}
}
private clearInactivityTimer(): void {
if (this.inactivityTimeout) {
clearTimeout(this.inactivityTimeout);
this.inactivityTimeout = null;
}
}
private closeMenu(): void {
this.open = false;
this.dispatchEvent(new CustomEvent('menu-close', {
bubbles: true,
composed: true,
}));
}
protected updated(changedProperties: Map<string, unknown>): void {
if (changedProperties.has('open')) {
if (this.open) {
this.resetInactivityTimer();
} else {
this.clearInactivityTimer();
}
}
}
async connectedCallback(): Promise<void> {
await super.connectedCallback();
setTimeout(() => {
document.addEventListener('click', this.boundHandleClickOutside);
}, 0);
}
async disconnectedCallback(): Promise<void> {
await super.disconnectedCallback();
document.removeEventListener('click', this.boundHandleClickOutside);
this.clearInactivityTimer();
}
}