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-soundmenu.demo.js'; // Ensure dees-icon is registered DeesIcon; declare global { interface HTMLElementTagNameMap { 'eco-applauncher-soundmenu': EcoApplauncherSoundmenu; } } export interface IAudioDevice { id: string; name: string; type: 'speaker' | 'headphones' | 'bluetooth' | 'hdmi'; active?: boolean; } @customElement('eco-applauncher-soundmenu') export class EcoApplauncherSoundmenu extends DeesElement { public static demo = demo; public static demoGroup = 'App Launcher'; public static styles = [ cssManager.defaultStyles, css` :host { display: block; position: relative; } .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: 280px; 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 { display: flex; align-items: center; justify-content: space-between; padding: 16px; border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 92%)', 'hsl(240 5% 15%)')}; } .menu-title { font-size: 15px; font-weight: 600; color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')}; display: flex; align-items: center; gap: 10px; } .volume-section { padding: 20px 16px; } .volume-slider-container { display: flex; align-items: center; gap: 12px; } .volume-icon { color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')}; cursor: pointer; transition: color 0.15s ease; } .volume-icon:hover { color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')}; } .volume-icon.muted { color: hsl(0 72% 51%); } .volume-slider { flex: 1; height: 6px; background: ${cssManager.bdTheme('hsl(0 0% 88%)', 'hsl(240 5% 20%)')}; border-radius: 3px; position: relative; cursor: pointer; } .volume-fill { height: 100%; background: hsl(217 91% 60%); border-radius: 3px; transition: width 0.1s ease; } .volume-fill.muted { background: ${cssManager.bdTheme('hsl(0 0% 70%)', 'hsl(0 0% 40%)')}; } .volume-thumb { position: absolute; top: 50%; transform: translate(-50%, -50%); width: 16px; height: 16px; background: white; border-radius: 50%; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); cursor: grab; } .volume-thumb:active { cursor: grabbing; } .volume-percentage { min-width: 36px; text-align: right; font-size: 14px; font-weight: 500; color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')}; } .menu-divider { height: 1px; background: ${cssManager.bdTheme('hsl(0 0% 92%)', 'hsl(240 5% 15%)')}; } .section-title { padding: 12px 16px 8px; font-size: 12px; font-weight: 600; color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 50%)')}; text-transform: uppercase; letter-spacing: 0.5px; } .device-list { max-height: 160px; overflow-y: auto; } .device-item { display: flex; align-items: center; gap: 12px; padding: 10px 16px; cursor: pointer; transition: background 0.15s ease; } .device-item:hover { background: ${cssManager.bdTheme('hsl(0 0% 96%)', 'hsl(240 5% 15%)')}; } .device-item.active { background: ${cssManager.bdTheme('hsl(217 91% 95%)', 'hsl(217 91% 60% / 0.15)')}; } .device-item.active:hover { background: ${cssManager.bdTheme('hsl(217 91% 92%)', 'hsl(217 91% 60% / 0.25)')}; } .device-icon { color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')}; } .device-item.active .device-icon { color: hsl(217 91% 60%); } .device-name { flex: 1; font-size: 14px; color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')}; } .device-check { color: hsl(217 91% 60%); } .menu-footer { padding: 12px 16px; border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 92%)', 'hsl(240 5% 15%)')}; } .settings-link { display: flex; align-items: center; gap: 8px; font-size: 14px; color: hsl(217 91% 60%); cursor: pointer; transition: color 0.15s ease; } .settings-link:hover { color: hsl(217 91% 50%); } `, ]; @property({ type: Boolean, reflect: true }) accessor open = false; @property({ type: Number }) accessor volume = 50; @property({ type: Boolean }) accessor muted = false; @property({ type: Array }) accessor outputDevices: IAudioDevice[] = []; @property({ type: String }) accessor activeDeviceId: string | null = null; private boundHandleClickOutside = this.handleClickOutside.bind(this); private isDragging = false; public render(): TemplateResult { const volumeIcon = this.getVolumeIcon(); return html` `; } private renderDeviceItem(device: IAudioDevice): TemplateResult { const isActive = device.id === this.activeDeviceId; const icon = this.getDeviceIcon(device.type); return html`
this.handleDeviceSelect(device)} > ${device.name} ${isActive ? html` ` : ''}
`; } private getVolumeIcon(): string { if (this.muted || this.volume === 0) return 'lucide:volumeX'; if (this.volume < 33) return 'lucide:volume'; if (this.volume < 66) return 'lucide:volume1'; return 'lucide:volume2'; } private getDeviceIcon(type: IAudioDevice['type']): string { switch (type) { case 'headphones': return 'lucide:headphones'; case 'bluetooth': return 'lucide:bluetooth'; case 'hdmi': return 'lucide:monitor'; case 'speaker': default: return 'lucide:speaker'; } } private handleMuteToggle(): void { this.muted = !this.muted; this.dispatchEvent(new CustomEvent('mute-toggle', { detail: { muted: this.muted }, bubbles: true, composed: true, })); } private handleSliderClick(e: MouseEvent): void { const slider = e.currentTarget as HTMLElement; const rect = slider.getBoundingClientRect(); const percentage = Math.round(((e.clientX - rect.left) / rect.width) * 100); this.setVolume(Math.max(0, Math.min(100, percentage))); } private handleSliderMouseDown(e: MouseEvent): void { this.isDragging = true; const slider = e.currentTarget as HTMLElement; const handleMouseMove = (moveEvent: MouseEvent) => { if (!this.isDragging) return; const rect = slider.getBoundingClientRect(); const percentage = Math.round(((moveEvent.clientX - rect.left) / rect.width) * 100); this.setVolume(Math.max(0, Math.min(100, percentage))); }; const handleMouseUp = () => { this.isDragging = false; document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); } private setVolume(value: number): void { this.volume = value; if (this.muted && value > 0) { this.muted = false; } this.dispatchEvent(new CustomEvent('volume-change', { detail: { volume: this.volume }, bubbles: true, composed: true, })); } private handleDeviceSelect(device: IAudioDevice): void { this.activeDeviceId = device.id; this.dispatchEvent(new CustomEvent('device-select', { detail: { device }, bubbles: true, composed: true, })); } private handleSettingsClick(): void { this.dispatchEvent(new CustomEvent('settings-click', { bubbles: true, composed: true, })); } private handleClickOutside(e: MouseEvent): void { if (this.open && !this.contains(e.target as Node)) { this.open = false; this.dispatchEvent(new CustomEvent('menu-close', { bubbles: true, composed: true, })); } } async connectedCallback(): Promise { await super.connectedCallback(); setTimeout(() => { document.addEventListener('click', this.boundHandleClickOutside); }, 0); } async disconnectedCallback(): Promise { await super.disconnectedCallback(); document.removeEventListener('click', this.boundHandleClickOutside); } }