import { customElement, DeesElement, type TemplateResult, html, property, css, cssManager, state, } from '@design.estate/dees-element'; import { DeesIcon } from '@design.estate/dees-catalog'; import { demo } from './eco-applauncher.demo.js'; import { EcoApplauncherWifimenu, type IWifiNetwork } from '../eco-applauncher-wifimenu/index.js'; import { EcoApplauncherBatterymenu } from '../eco-applauncher-batterymenu/index.js'; import { EcoApplauncherSoundmenu, type IAudioDevice } from '../eco-applauncher-soundmenu/index.js'; // Ensure components are registered DeesIcon; EcoApplauncherWifimenu; EcoApplauncherBatterymenu; EcoApplauncherSoundmenu; declare global { interface HTMLElementTagNameMap { 'eco-applauncher': EcoApplauncher; } } export interface IAppIcon { name: string; icon: string; action?: () => void; } export interface IStatusBarConfig { showTime?: boolean; showNetwork?: boolean; showBattery?: boolean; showSound?: boolean; } export interface ITopBarConfig { showSearch?: boolean; showDate?: boolean; showNotifications?: boolean; showUser?: boolean; } export type TNetworkStatus = 'online' | 'offline' | 'connecting'; export type TBatteryStatus = number | 'charging'; @customElement('eco-applauncher') export class EcoApplauncher extends DeesElement { public static demo = demo; public static demoGroup = 'App Launcher'; public static styles = [ cssManager.defaultStyles, css` :host { display: block; width: 100%; height: 100%; background: ${cssManager.bdTheme('hsl(220 20% 97%)', 'hsl(240 10% 4%)')}; color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')}; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; position: relative; overflow: hidden; } .launcher-container { display: flex; flex-direction: column; height: 100%; } .top-bar { height: 48px; background: ${cssManager.bdTheme('hsl(220 15% 94%)', 'hsl(240 6% 8%)')}; border-bottom: 1px solid ${cssManager.bdTheme('hsl(220 15% 88%)', 'hsl(240 5% 15%)')}; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; flex-shrink: 0; } .top-left { display: flex; align-items: center; gap: 16px; } .top-center { display: flex; align-items: center; gap: 8px; } .top-right { display: flex; align-items: center; gap: 16px; } .top-date { font-size: 14px; font-weight: 500; color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')}; } .search-box { display: flex; align-items: center; gap: 8px; padding: 8px 16px; background: ${cssManager.bdTheme('hsl(220 15% 90%)', 'hsl(240 5% 12%)')}; border-radius: 20px; min-width: 200px; cursor: pointer; transition: background 0.15s ease; } .search-box:hover { background: ${cssManager.bdTheme('hsl(220 15% 86%)', 'hsl(240 5% 15%)')}; } .search-box dees-icon { color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 50%)')}; } .search-text { font-size: 14px; color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 50%)')}; } .top-icon-button { display: flex; align-items: center; justify-content: center; width: 36px; height: 36px; border-radius: 50%; cursor: pointer; transition: background 0.15s ease; color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')}; } .top-icon-button:hover { background: ${cssManager.bdTheme('hsl(220 15% 88%)', 'hsl(240 5% 15%)')}; } .user-avatar { width: 32px; height: 32px; border-radius: 50%; background: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(217 91% 50%)')}; display: flex; align-items: center; justify-content: center; color: white; font-size: 14px; font-weight: 600; cursor: pointer; transition: transform 0.15s ease; } .user-avatar:hover { transform: scale(1.05); } .notification-badge { position: relative; } .notification-badge .badge { position: absolute; top: -4px; right: -4px; min-width: 16px; height: 16px; padding: 0 4px; background: hsl(0 72% 51%); color: white; font-size: 10px; font-weight: 600; border-radius: 8px; display: flex; align-items: center; justify-content: center; } .apps-area { flex: 1; display: flex; align-items: center; justify-content: center; padding: 48px; overflow-y: auto; } .apps-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 32px; max-width: 800px; width: 100%; } .app-icon { display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 16px; border-radius: 16px; cursor: pointer; transition: background 0.2s ease, transform 0.15s ease; user-select: none; -webkit-tap-highlight-color: transparent; } .app-icon:hover { background: ${cssManager.bdTheme('hsl(220 15% 92%)', 'hsl(240 5% 12%)')}; } .app-icon:active { transform: scale(0.95); background: ${cssManager.bdTheme('hsl(220 15% 88%)', 'hsl(240 5% 16%)')}; } .app-icon-circle { width: 64px; height: 64px; border-radius: 16px; background: ${cssManager.bdTheme('hsl(220 15% 90%)', 'hsl(240 5% 15%)')}; display: flex; align-items: center; justify-content: center; color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 80%)')}; } .app-icon-circle dees-icon { --dees-icon-size: 28px; } .app-icon-name { font-size: 13px; font-weight: 500; color: ${cssManager.bdTheme('hsl(0 0% 25%)', 'hsl(0 0% 85%)')}; text-align: center; max-width: 90px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .status-bar { height: 48px; background: ${cssManager.bdTheme('hsl(220 15% 94%)', 'hsl(240 6% 8%)')}; border-top: 1px solid ${cssManager.bdTheme('hsl(220 15% 88%)', 'hsl(240 5% 15%)')}; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; flex-shrink: 0; } .status-left { display: flex; align-items: center; gap: 20px; } .status-right { display: flex; align-items: center; gap: 20px; } .status-item { display: flex; align-items: center; gap: 8px; font-size: 13px; color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')}; } .status-icon { font-size: 16px; opacity: 0.8; } .status-time { font-size: 14px; font-weight: 500; color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')}; } .battery-indicator { display: flex; align-items: center; gap: 6px; } .battery-bar { width: 24px; height: 12px; border: 1.5px solid ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')}; border-radius: 3px; position: relative; overflow: hidden; } .battery-bar::after { content: ''; position: absolute; right: -4px; top: 50%; transform: translateY(-50%); width: 2px; height: 6px; background: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')}; border-radius: 0 1px 1px 0; } .battery-fill { height: 100%; background: hsl(142 71% 45%); transition: width 0.3s ease; } .battery-fill.low { background: hsl(0 72% 51%); } .battery-fill.charging { background: hsl(47 100% 50%); } .network-indicator { display: flex; align-items: flex-end; gap: 2px; height: 14px; } .network-bar { width: 3px; background: ${cssManager.bdTheme('hsl(0 0% 75%)', 'hsl(0 0% 40%)')}; border-radius: 1px; transition: background 0.2s ease; } .network-bar.active { background: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 90%)')}; } .network-bar:nth-child(1) { height: 4px; } .network-bar:nth-child(2) { height: 7px; } .network-bar:nth-child(3) { height: 10px; } .network-bar:nth-child(4) { height: 14px; } .sound-indicator { display: flex; align-items: center; gap: 4px; } .sound-bars { display: flex; align-items: flex-end; gap: 2px; height: 12px; } .sound-bar { width: 2px; background: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 90%)')}; border-radius: 1px; } .sound-bar:nth-child(1) { height: 4px; } .sound-bar:nth-child(2) { height: 8px; } .sound-bar:nth-child(3) { height: 12px; } .status-item-wrapper { position: relative; } .status-item.clickable { cursor: pointer; padding: 4px 8px; margin: -4px -8px; border-radius: 6px; transition: background 0.15s ease; } .status-item.clickable:hover { background: ${cssManager.bdTheme('hsl(220 15% 88%)', 'hsl(240 5% 15%)')}; } .status-item.clickable.active { background: ${cssManager.bdTheme('hsl(220 15% 85%)', 'hsl(240 5% 18%)')}; } .menu-popup { position: absolute; bottom: calc(100% + 8px); left: 0; z-index: 100; } @media (max-width: 600px) { .apps-area { padding: 24px; } .apps-grid { grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); gap: 16px; } .app-icon-circle { width: 56px; height: 56px; font-size: 24px; } .app-icon-name { font-size: 12px; } .status-bar { padding: 0 16px; } .status-left, .status-right { gap: 12px; } } `, ]; @property({ type: Array }) accessor apps: IAppIcon[] = []; @property({ type: Object }) accessor statusConfig: IStatusBarConfig = { showTime: true, showNetwork: true, showBattery: true, showSound: true, }; @property({ type: Object }) accessor topBarConfig: ITopBarConfig = { showSearch: true, showDate: true, showNotifications: true, showUser: true, }; @property({ type: String }) accessor userName = 'User'; @property({ type: Number }) accessor notificationCount = 0; @property({ type: Number }) accessor batteryLevel: TBatteryStatus = 100; @property({ type: String }) accessor networkStatus: TNetworkStatus = 'online'; @property({ type: Number }) accessor soundLevel = 50; @state() accessor currentTime = ''; @state() accessor currentDate = ''; @state() accessor wifiMenuOpen = false; @state() accessor batteryMenuOpen = false; @state() accessor soundMenuOpen = false; @property({ type: Array }) accessor networks: IWifiNetwork[] = []; @property({ type: String }) accessor connectedNetwork: string | null = null; @property({ type: Boolean }) accessor wifiEnabled = true; @property({ type: Boolean }) accessor isCharging = false; @property({ type: Boolean }) accessor batterySaverEnabled = false; @property({ type: String }) accessor timeRemaining: string | null = null; @property({ type: Array }) accessor outputDevices: IAudioDevice[] = []; @property({ type: String }) accessor activeDeviceId: string | null = null; @property({ type: Boolean }) accessor muted = false; private timeUpdateInterval: ReturnType | null = null; constructor() { super(); this.updateTime(); } public render(): TemplateResult { return html`
${this.renderTopBar()}
${this.apps.map((app) => this.renderAppIcon(app))}
${this.statusConfig.showNetwork ? this.renderNetworkStatusWithMenu() : ''} ${this.statusConfig.showBattery ? this.renderBatteryStatusWithMenu() : ''} ${this.statusConfig.showSound ? this.renderSoundStatusWithMenu() : ''}
${this.statusConfig.showTime ? html` ${this.currentTime} ` : ''}
`; } private renderAppIcon(app: IAppIcon): TemplateResult { return html`
this.handleAppClick(app)}>
${app.name}
`; } private renderTopBar(): TemplateResult { const userInitials = this.userName .split(' ') .map((n) => n[0]) .join('') .slice(0, 2) .toUpperCase(); return html`
${this.topBarConfig.showDate ? html` ${this.currentDate} ` : ''}
${this.topBarConfig.showSearch ? html` ` : ''}
${this.topBarConfig.showNotifications ? html`
${this.notificationCount > 0 ? html` ${this.notificationCount > 99 ? '99+' : this.notificationCount} ` : ''}
` : ''} ${this.topBarConfig.showUser ? html`
${userInitials}
` : ''}
`; } private renderNetworkStatusWithMenu(): TemplateResult { const bars = this.getNetworkBars(); return html`
${[1, 2, 3, 4].map((bar) => html`
`)}
`; } private renderNetworkStatus(): TemplateResult { const bars = this.getNetworkBars(); return html`
${[1, 2, 3, 4].map((bar) => html`
`)}
`; } private renderBatteryStatusWithMenu(): TemplateResult { const level = typeof this.batteryLevel === 'number' ? this.batteryLevel : 100; const isCharging = this.batteryLevel === 'charging' || this.isCharging; const isLow = level < 20; return html`
${isCharging ? html`` : html`${level}%` }
`; } private renderBatteryStatus(): TemplateResult { const level = typeof this.batteryLevel === 'number' ? this.batteryLevel : 100; const isCharging = this.batteryLevel === 'charging'; const isLow = level < 20; return html`
${isCharging ? html`` : html`${level}%` }
`; } private renderSoundStatusWithMenu(): TemplateResult { const activeBars = Math.ceil((this.soundLevel / 100) * 3); const soundIcon = this.muted || this.soundLevel === 0 ? 'lucide:volumeX' : 'lucide:volume2'; return html`
${[1, 2, 3].map((bar) => html`
`)}
`; } private renderSoundStatus(): TemplateResult { const activeBars = Math.ceil((this.soundLevel / 100) * 3); const soundIcon = this.soundLevel === 0 ? 'lucide:volumeX' : 'lucide:volume2'; return html`
${[1, 2, 3].map((bar) => html`
`)}
`; } private getNetworkBars(): number { switch (this.networkStatus) { case 'online': return 4; case 'connecting': return 2; case 'offline': return 0; default: return 4; } } private handleAppClick(app: IAppIcon): void { this.dispatchEvent( new CustomEvent('app-click', { detail: { app }, bubbles: true, composed: true, }) ); if (app.action) { app.action(); } } private handleNetworkClick(e: MouseEvent): void { e.stopPropagation(); this.batteryMenuOpen = false; this.soundMenuOpen = false; this.wifiMenuOpen = !this.wifiMenuOpen; } private handleBatteryClick(e: MouseEvent): void { e.stopPropagation(); this.wifiMenuOpen = false; this.soundMenuOpen = false; this.batteryMenuOpen = !this.batteryMenuOpen; } private handleSoundClick(e: MouseEvent): void { e.stopPropagation(); this.wifiMenuOpen = false; this.batteryMenuOpen = false; this.soundMenuOpen = !this.soundMenuOpen; } private handleWifiMenuClose(): void { this.wifiMenuOpen = false; } private handleBatteryMenuClose(): void { this.batteryMenuOpen = false; } private handleSoundMenuClose(): void { this.soundMenuOpen = false; } private handleVolumeChange(e: CustomEvent): void { this.soundLevel = e.detail.volume; this.dispatchEvent(new CustomEvent('volume-change', { detail: e.detail, bubbles: true, composed: true, })); } private handleMuteToggle(e: CustomEvent): void { this.muted = e.detail.muted; this.dispatchEvent(new CustomEvent('mute-toggle', { detail: e.detail, bubbles: true, composed: true, })); } private handleDeviceSelect(e: CustomEvent): void { this.activeDeviceId = e.detail.device.id; this.dispatchEvent(new CustomEvent('device-select', { detail: e.detail, bubbles: true, composed: true, })); } private handleSoundSettingsClick(): void { this.soundMenuOpen = false; this.dispatchEvent(new CustomEvent('sound-settings-click', { bubbles: true, composed: true, })); } private handleWifiToggle(e: CustomEvent): void { this.wifiEnabled = e.detail.enabled; this.dispatchEvent(new CustomEvent('wifi-toggle', { detail: e.detail, bubbles: true, composed: true, })); } private handleNetworkSelect(e: CustomEvent): void { this.dispatchEvent(new CustomEvent('network-select', { detail: e.detail, bubbles: true, composed: true, })); } private handleWifiSettingsClick(): void { this.wifiMenuOpen = false; this.dispatchEvent(new CustomEvent('wifi-settings-click', { bubbles: true, composed: true, })); } private handleBatterySaverToggle(e: CustomEvent): void { this.batterySaverEnabled = e.detail.enabled; this.dispatchEvent(new CustomEvent('battery-saver-toggle', { detail: e.detail, bubbles: true, composed: true, })); } private handleBatterySettingsClick(): void { this.batteryMenuOpen = false; this.dispatchEvent(new CustomEvent('battery-settings-click', { bubbles: true, composed: true, })); } private handleSearchClick(): void { this.dispatchEvent(new CustomEvent('search-click', { bubbles: true, composed: true, })); } private handleNotificationsClick(): void { this.dispatchEvent(new CustomEvent('notifications-click', { bubbles: true, composed: true, })); } private handleUserClick(): void { this.dispatchEvent(new CustomEvent('user-click', { bubbles: true, composed: true, })); } private updateTime(): void { const now = new Date(); const hours = now.getHours(); const minutes = String(now.getMinutes()).padStart(2, '0'); const ampm = hours >= 12 ? 'PM' : 'AM'; const displayHours = hours % 12 || 12; this.currentTime = `${displayHours}:${minutes} ${ampm}`; // Update date const options: Intl.DateTimeFormatOptions = { weekday: 'short', month: 'short', day: 'numeric', }; this.currentDate = now.toLocaleDateString('en-US', options); } async connectedCallback(): Promise { await super.connectedCallback(); this.updateTime(); this.timeUpdateInterval = setInterval(() => this.updateTime(), 1000); } async disconnectedCallback(): Promise { await super.disconnectedCallback(); if (this.timeUpdateInterval) { clearInterval(this.timeUpdateInterval); this.timeUpdateInterval = null; } } }