Files
catalog/ts_web/elements/00group-applauncher/eco-applauncher/eco-applauncher.ts

1312 lines
34 KiB
TypeScript
Raw Normal View History

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';
2026-01-06 09:47:03 +00:00
import { EcoApplauncherKeyboard } from '../eco-applauncher-keyboard/index.js';
2026-01-12 10:57:54 +00:00
import { EcoApplauncherPowermenu, type TPowerAction } from '../eco-applauncher-powermenu/index.js';
import { EcoViewHome } from '../../../views/eco-view-home/index.js';
import { EcoViewLogin, type ILoginConfig, type ILoginCredentials } from '../../../views/eco-view-login/index.js';
// Ensure components are registered
DeesIcon;
EcoApplauncherWifimenu;
EcoApplauncherBatterymenu;
EcoApplauncherSoundmenu;
2026-01-06 09:47:03 +00:00
EcoApplauncherKeyboard;
2026-01-12 10:57:54 +00:00
EcoApplauncherPowermenu;
EcoViewHome;
EcoViewLogin;
declare global {
interface HTMLElementTagNameMap {
'eco-applauncher': EcoApplauncher;
}
}
2026-01-12 10:57:54 +00:00
export type TApplauncherMode = 'login' | 'home';
export interface IAppIcon {
name: string;
icon: string;
action?: () => void;
2026-01-12 10:57:54 +00:00
view?: TemplateResult;
}
2026-01-12 10:57:54 +00:00
export type { ILoginConfig, ILoginCredentials } from '../../../views/eco-view-login/index.js';
export type { TPowerAction } from '../eco-applauncher-powermenu/index.js';
export interface IStatusBarConfig {
showTime?: boolean;
showNetwork?: boolean;
showBattery?: boolean;
showSound?: boolean;
2026-01-06 09:47:03 +00:00
showKeyboard?: 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%)')};
}
2026-01-12 10:57:54 +00:00
.top-icon-button.active {
background: ${cssManager.bdTheme('hsl(220 15% 85%)', 'hsl(240 5% 18%)')};
}
.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;
padding: 48px;
overflow-y: auto;
}
.apps-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 32px;
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;
2026-01-06 09:47:03 +00:00
pointer-events: none;
}
2026-01-12 10:57:54 +00:00
.topbar-menu-wrapper {
position: relative;
}
.topbar-menu-popup {
position: absolute;
top: calc(100% + 8px);
right: 0;
z-index: 100;
pointer-events: none;
}
2026-01-06 09:47:03 +00:00
.keyboard-area {
flex-shrink: 0;
height: 0;
overflow: hidden;
transition: height 0.25s ease;
background: ${cssManager.bdTheme('hsl(220 15% 92%)', 'hsl(240 6% 12%)')};
z-index: 1000;
position: relative;
}
.keyboard-area.visible {
height: 220px;
overflow: visible;
}
2026-01-12 10:57:54 +00:00
.view-area {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.view-header {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 24px;
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%)')};
flex-shrink: 0;
}
.back-button {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 8px 14px;
background: ${cssManager.bdTheme('hsl(220 15% 88%)', 'hsl(240 5% 14%)')};
border-radius: 8px;
cursor: pointer;
transition: background 0.15s ease;
color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')};
font-size: 13px;
font-weight: 500;
}
.back-button:hover {
background: ${cssManager.bdTheme('hsl(220 15% 82%)', 'hsl(240 5% 18%)')};
}
.view-title {
font-size: 16px;
font-weight: 600;
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
}
.view-content {
flex: 1;
overflow: auto;
}
@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;
}
}
`,
];
2026-01-12 10:57:54 +00:00
@property({ type: String })
accessor mode: TApplauncherMode = 'home';
@property({ type: Object })
accessor loginConfig: ILoginConfig = {
allowedMethods: ['pin', 'password', 'qr'],
pinLength: 4,
welcomeMessage: 'Welcome',
};
@property({ type: Array })
accessor apps: IAppIcon[] = [];
@property({ type: Object })
accessor statusConfig: IStatusBarConfig = {
showTime: true,
showNetwork: true,
showBattery: true,
showSound: true,
2026-01-06 09:47:03 +00:00
showKeyboard: 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;
2026-01-12 10:57:54 +00:00
@state()
accessor powerMenuOpen = false;
2026-01-06 09:47:03 +00:00
@state()
accessor keyboardVisible = false;
2026-01-12 10:57:54 +00:00
@state()
accessor activeView: TemplateResult | null = null;
@state()
accessor activeViewName: string | null = null;
@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<typeof setInterval> | null = null;
constructor() {
super();
this.updateTime();
}
public render(): TemplateResult {
return html`
<div class="launcher-container">
2026-01-12 10:57:54 +00:00
${this.mode === 'login' ? '' : this.renderTopBar()}
${this.renderMainContent()}
2026-01-06 09:47:03 +00:00
<div class="keyboard-area ${this.keyboardVisible ? 'visible' : ''}">
<eco-applauncher-keyboard
?visible=${this.keyboardVisible}
@key-press=${this.handleKeyboardKeyPress}
@backspace=${this.handleKeyboardBackspace}
@enter=${this.handleKeyboardEnter}
@space=${this.handleKeyboardSpace}
@arrow=${this.handleKeyboardArrow}
></eco-applauncher-keyboard>
</div>
<div class="status-bar">
<div class="status-left">
2026-01-06 09:47:03 +00:00
${this.statusConfig.showKeyboard ? this.renderKeyboardToggle() : ''}
${this.statusConfig.showNetwork ? this.renderNetworkStatusWithMenu() : ''}
${this.statusConfig.showBattery ? this.renderBatteryStatusWithMenu() : ''}
${this.statusConfig.showSound ? this.renderSoundStatusWithMenu() : ''}
</div>
<div class="status-right">
${this.statusConfig.showTime ? html`
<span class="status-time">${this.currentTime}</span>
` : ''}
</div>
</div>
</div>
`;
}
2026-01-12 10:57:54 +00:00
private renderMainContent(): TemplateResult {
if (this.mode === 'login') {
return this.renderLoginView();
}
// Home mode
if (this.activeView) {
return this.renderActiveView();
}
return this.renderHomeView();
}
private renderLoginView(): TemplateResult {
return html`
<eco-view-login
.config=${this.loginConfig}
@login-attempt=${this.handleLoginAttempt}
@key-press=${this.handleKeyboardKeyPress}
@backspace=${this.handleKeyboardBackspace}
style="flex: 1;"
></eco-view-login>
`;
}
private renderHomeView(): TemplateResult {
return html`
<eco-view-home
.apps=${this.apps}
@app-click=${this.handleHomeAppClick}
></eco-view-home>
`;
}
private renderAppsArea(): TemplateResult {
return html`
<div class="apps-area">
<div class="apps-grid">
${this.apps.map((app) => this.renderAppIcon(app))}
</div>
</div>
`;
}
private renderActiveView(): TemplateResult {
return html`
<div class="view-area">
<div class="view-content">
${this.activeView}
</div>
</div>
`;
}
private renderAppIcon(app: IAppIcon): TemplateResult {
return html`
<div class="app-icon" @click=${() => this.handleAppClick(app)}>
<div class="app-icon-circle">
<dees-icon .icon=${app.icon} .iconSize=${28}></dees-icon>
</div>
<span class="app-icon-name">${app.name}</span>
</div>
`;
}
private renderTopBar(): TemplateResult {
const userInitials = this.userName
.split(' ')
.map((n) => n[0])
.join('')
.slice(0, 2)
.toUpperCase();
return html`
<div class="top-bar">
<div class="top-left">
2026-01-12 10:57:54 +00:00
${this.activeView ? html`
<div class="back-button" @click=${this.handleBackClick}>
<dees-icon .icon=${'lucide:arrowLeft'} .iconSize=${16}></dees-icon>
<span>Back</span>
</div>
<span class="view-title">${this.activeViewName}</span>
` : this.topBarConfig.showDate ? html`
<span class="top-date">${this.currentDate}</span>
` : ''}
</div>
<div class="top-center">
${this.topBarConfig.showSearch ? html`
<div class="search-box" @click=${this.handleSearchClick}>
<dees-icon .icon=${'lucide:search'} .iconSize=${16}></dees-icon>
<span class="search-text">Search apps...</span>
</div>
` : ''}
</div>
<div class="top-right">
${this.topBarConfig.showNotifications ? html`
<div
class="top-icon-button notification-badge"
@click=${this.handleNotificationsClick}
>
<dees-icon .icon=${'lucide:bell'} .iconSize=${18}></dees-icon>
${this.notificationCount > 0 ? html`
<span class="badge">${this.notificationCount > 99 ? '99+' : this.notificationCount}</span>
` : ''}
</div>
` : ''}
${this.topBarConfig.showUser ? html`
<div class="user-avatar" @click=${this.handleUserClick}>
${userInitials}
</div>
` : ''}
2026-01-12 10:57:54 +00:00
<div class="topbar-menu-wrapper">
<div
class="top-icon-button ${this.powerMenuOpen ? 'active' : ''}"
@click=${this.handlePowerClick}
>
<dees-icon .icon=${'lucide:power'} .iconSize=${18}></dees-icon>
</div>
<div class="topbar-menu-popup">
<eco-applauncher-powermenu
?open=${this.powerMenuOpen}
@menu-close=${this.handlePowerMenuClose}
@power-action=${this.handlePowerAction}
></eco-applauncher-powermenu>
</div>
</div>
</div>
</div>
`;
}
private renderNetworkStatusWithMenu(): TemplateResult {
const bars = this.getNetworkBars();
return html`
<div class="status-item-wrapper">
<div
class="status-item clickable ${this.wifiMenuOpen ? 'active' : ''}"
@click=${this.handleNetworkClick}
>
<div class="network-indicator">
${[1, 2, 3, 4].map((bar) => html`
<div class="network-bar ${bar <= bars ? 'active' : ''}"></div>
`)}
</div>
</div>
<div class="menu-popup">
<eco-applauncher-wifimenu
?open=${this.wifiMenuOpen}
.networks=${this.networks}
.connectedNetwork=${this.connectedNetwork}
.wifiEnabled=${this.wifiEnabled}
@menu-close=${this.handleWifiMenuClose}
@wifi-toggle=${this.handleWifiToggle}
@network-select=${this.handleNetworkSelect}
@settings-click=${this.handleWifiSettingsClick}
></eco-applauncher-wifimenu>
</div>
</div>
`;
}
private renderNetworkStatus(): TemplateResult {
const bars = this.getNetworkBars();
return html`
<div class="status-item">
<div class="network-indicator">
${[1, 2, 3, 4].map((bar) => html`
<div class="network-bar ${bar <= bars ? 'active' : ''}"></div>
`)}
</div>
</div>
`;
}
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`
<div class="status-item-wrapper">
<div
class="status-item battery-indicator clickable ${this.batteryMenuOpen ? 'active' : ''}"
@click=${this.handleBatteryClick}
>
<div class="battery-bar">
<div
class="battery-fill ${isLow ? 'low' : ''} ${isCharging ? 'charging' : ''}"
style="width: ${level}%"
></div>
</div>
${isCharging
? html`<dees-icon .icon=${'lucide:zap'} .iconSize=${14}></dees-icon>`
: html`<span>${level}%</span>`
}
</div>
<div class="menu-popup">
<eco-applauncher-batterymenu
?open=${this.batteryMenuOpen}
.batteryLevel=${level}
.isCharging=${isCharging}
.batterySaverEnabled=${this.batterySaverEnabled}
.timeRemaining=${this.timeRemaining}
@menu-close=${this.handleBatteryMenuClose}
@battery-saver-toggle=${this.handleBatterySaverToggle}
@settings-click=${this.handleBatterySettingsClick}
></eco-applauncher-batterymenu>
</div>
</div>
`;
}
private renderBatteryStatus(): TemplateResult {
const level = typeof this.batteryLevel === 'number' ? this.batteryLevel : 100;
const isCharging = this.batteryLevel === 'charging';
const isLow = level < 20;
return html`
<div class="status-item battery-indicator">
<div class="battery-bar">
<div
class="battery-fill ${isLow ? 'low' : ''} ${isCharging ? 'charging' : ''}"
style="width: ${level}%"
></div>
</div>
${isCharging
? html`<dees-icon .icon=${'lucide:zap'} .iconSize=${14}></dees-icon>`
: html`<span>${level}%</span>`
}
</div>
`;
}
private renderSoundStatusWithMenu(): TemplateResult {
const activeBars = Math.ceil((this.soundLevel / 100) * 3);
const soundIcon = this.muted || this.soundLevel === 0 ? 'lucide:volumeX' : 'lucide:volume2';
return html`
<div class="status-item-wrapper">
<div
class="status-item sound-indicator clickable ${this.soundMenuOpen ? 'active' : ''}"
@click=${this.handleSoundClick}
>
<dees-icon .icon=${soundIcon} .iconSize=${16}></dees-icon>
<div class="sound-bars">
${[1, 2, 3].map((bar) => html`
<div
class="sound-bar"
style="opacity: ${bar <= activeBars && !this.muted ? 1 : 0.3}"
></div>
`)}
</div>
</div>
<div class="menu-popup">
<eco-applauncher-soundmenu
?open=${this.soundMenuOpen}
.volume=${this.soundLevel}
.muted=${this.muted}
.outputDevices=${this.outputDevices}
.activeDeviceId=${this.activeDeviceId}
@menu-close=${this.handleSoundMenuClose}
@volume-change=${this.handleVolumeChange}
@mute-toggle=${this.handleMuteToggle}
@device-select=${this.handleDeviceSelect}
@settings-click=${this.handleSoundSettingsClick}
></eco-applauncher-soundmenu>
</div>
</div>
`;
}
private renderSoundStatus(): TemplateResult {
const activeBars = Math.ceil((this.soundLevel / 100) * 3);
const soundIcon = this.soundLevel === 0 ? 'lucide:volumeX' : 'lucide:volume2';
return html`
<div class="status-item sound-indicator">
<dees-icon .icon=${soundIcon} .iconSize=${16}></dees-icon>
<div class="sound-bars">
${[1, 2, 3].map((bar) => html`
<div
class="sound-bar"
style="opacity: ${bar <= activeBars ? 1 : 0.3}"
></div>
`)}
</div>
</div>
`;
}
2026-01-06 09:47:03 +00:00
private renderKeyboardToggle(): TemplateResult {
return html`
<div
class="status-item clickable ${this.keyboardVisible ? 'active' : ''}"
@click=${this.handleKeyboardToggle}
>
<dees-icon .icon=${'lucide:keyboard'} .iconSize=${18}></dees-icon>
</div>
`;
}
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,
})
);
2026-01-12 10:57:54 +00:00
// If app has a view, open it inside the applauncher
if (app.view) {
this.activeView = app.view;
this.activeViewName = app.name;
return;
}
// Otherwise execute the action
if (app.action) {
app.action();
}
}
2026-01-12 10:57:54 +00:00
private handleHomeAppClick(e: CustomEvent): void {
const app = e.detail.app as IAppIcon;
this.handleAppClick(app);
}
private handleLoginAttempt(e: CustomEvent): void {
const credentials = e.detail as ILoginCredentials;
// Dispatch event for parent to handle validation
this.dispatchEvent(new CustomEvent('login-attempt', {
detail: credentials,
bubbles: true,
composed: true,
}));
}
/**
* Set the login result after validation
* @param success Whether login was successful
* @param errorMessage Optional error message to display
*/
public setLoginResult(success: boolean, errorMessage?: string): void {
const loginView = this.shadowRoot?.querySelector('eco-view-login') as EcoViewLogin | null;
if (success) {
this.mode = 'home';
this.dispatchEvent(new CustomEvent('login-success', {
bubbles: true,
composed: true,
}));
} else {
if (loginView && errorMessage) {
loginView.showErrorMessage(errorMessage);
}
this.dispatchEvent(new CustomEvent('login-failure', {
detail: { error: errorMessage },
bubbles: true,
composed: true,
}));
}
}
/**
* Switch to login mode
*/
public showLogin(): void {
this.mode = 'login';
this.activeView = null;
this.activeViewName = null;
}
/**
* Switch to home mode
*/
public showHome(): void {
this.mode = 'home';
}
private handleBackClick(): void {
this.activeView = null;
this.activeViewName = null;
this.dispatchEvent(new CustomEvent('view-close', {
bubbles: true,
composed: true,
}));
}
private handleNetworkClick(e: MouseEvent): void {
e.stopPropagation();
this.batteryMenuOpen = false;
this.soundMenuOpen = false;
2026-01-12 10:57:54 +00:00
this.powerMenuOpen = false;
this.wifiMenuOpen = !this.wifiMenuOpen;
}
private handleBatteryClick(e: MouseEvent): void {
e.stopPropagation();
this.wifiMenuOpen = false;
this.soundMenuOpen = false;
2026-01-12 10:57:54 +00:00
this.powerMenuOpen = false;
this.batteryMenuOpen = !this.batteryMenuOpen;
}
private handleSoundClick(e: MouseEvent): void {
e.stopPropagation();
this.wifiMenuOpen = false;
this.batteryMenuOpen = false;
2026-01-12 10:57:54 +00:00
this.powerMenuOpen = false;
this.soundMenuOpen = !this.soundMenuOpen;
}
2026-01-12 10:57:54 +00:00
private handlePowerClick(e: MouseEvent): void {
e.stopPropagation();
this.wifiMenuOpen = false;
this.batteryMenuOpen = false;
this.soundMenuOpen = false;
this.powerMenuOpen = !this.powerMenuOpen;
}
private handleWifiMenuClose(): void {
this.wifiMenuOpen = false;
}
private handleBatteryMenuClose(): void {
this.batteryMenuOpen = false;
}
private handleSoundMenuClose(): void {
this.soundMenuOpen = false;
}
2026-01-12 10:57:54 +00:00
private handlePowerMenuClose(): void {
this.powerMenuOpen = false;
}
private handlePowerAction(e: CustomEvent): void {
const action = e.detail.action as TPowerAction;
this.dispatchEvent(new CustomEvent('power-action', {
detail: { action },
bubbles: true,
composed: true,
}));
}
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,
}));
}
2026-01-06 09:47:03 +00:00
private handleKeyboardToggle(): void {
this.keyboardVisible = !this.keyboardVisible;
// Close all menus when opening keyboard
if (this.keyboardVisible) {
this.wifiMenuOpen = false;
this.batteryMenuOpen = false;
this.soundMenuOpen = false;
2026-01-12 10:57:54 +00:00
this.powerMenuOpen = false;
2026-01-06 09:47:03 +00:00
}
this.dispatchEvent(new CustomEvent('keyboard-toggle', {
detail: { visible: this.keyboardVisible },
bubbles: true,
composed: true,
}));
}
private handleKeyboardKeyPress(e: CustomEvent): void {
this.dispatchEvent(new CustomEvent('keyboard-key-press', {
detail: e.detail,
bubbles: true,
composed: true,
}));
}
private handleKeyboardBackspace(): void {
this.dispatchEvent(new CustomEvent('keyboard-backspace', {
bubbles: true,
composed: true,
}));
}
private handleKeyboardEnter(): void {
this.dispatchEvent(new CustomEvent('keyboard-enter', {
bubbles: true,
composed: true,
}));
}
private handleKeyboardSpace(): void {
this.dispatchEvent(new CustomEvent('keyboard-space', {
bubbles: true,
composed: true,
}));
}
private handleKeyboardArrow(e: CustomEvent): void {
this.dispatchEvent(new CustomEvent('keyboard-arrow', {
detail: e.detail,
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<void> {
await super.connectedCallback();
this.updateTime();
this.timeUpdateInterval = setInterval(() => this.updateTime(), 1000);
}
async disconnectedCallback(): Promise<void> {
await super.disconnectedCallback();
if (this.timeUpdateInterval) {
clearInterval(this.timeUpdateInterval);
this.timeUpdateInterval = null;
}
}
}