diff --git a/.playwright-mcp/applauncher-battery-menu-fixed.png b/.playwright-mcp/applauncher-battery-menu-fixed.png
new file mode 100644
index 0000000..e95988b
Binary files /dev/null and b/.playwright-mcp/applauncher-battery-menu-fixed.png differ
diff --git a/.playwright-mcp/applauncher-battery-menu-open.png b/.playwright-mcp/applauncher-battery-menu-open.png
new file mode 100644
index 0000000..a2ec45b
Binary files /dev/null and b/.playwright-mcp/applauncher-battery-menu-open.png differ
diff --git a/.playwright-mcp/applauncher-initial.png b/.playwright-mcp/applauncher-initial.png
new file mode 100644
index 0000000..fbc0bbb
Binary files /dev/null and b/.playwright-mcp/applauncher-initial.png differ
diff --git a/.playwright-mcp/applauncher-wifi-menu-open.png b/.playwright-mcp/applauncher-wifi-menu-open.png
new file mode 100644
index 0000000..06e2230
Binary files /dev/null and b/.playwright-mcp/applauncher-wifi-menu-open.png differ
diff --git a/.playwright-mcp/eco-applauncher-bright.png b/.playwright-mcp/eco-applauncher-bright.png
new file mode 100644
index 0000000..053c21a
Binary files /dev/null and b/.playwright-mcp/eco-applauncher-bright.png differ
diff --git a/.playwright-mcp/eco-applauncher-dark.png b/.playwright-mcp/eco-applauncher-dark.png
new file mode 100644
index 0000000..89c97e9
Binary files /dev/null and b/.playwright-mcp/eco-applauncher-dark.png differ
diff --git a/.playwright-mcp/eco-batterymenu-bright.png b/.playwright-mcp/eco-batterymenu-bright.png
new file mode 100644
index 0000000..269282e
Binary files /dev/null and b/.playwright-mcp/eco-batterymenu-bright.png differ
diff --git a/.playwright-mcp/eco-batterymenu-dark.png b/.playwright-mcp/eco-batterymenu-dark.png
new file mode 100644
index 0000000..2a6633a
Binary files /dev/null and b/.playwright-mcp/eco-batterymenu-dark.png differ
diff --git a/.playwright-mcp/eco-soundmenu-dark.png b/.playwright-mcp/eco-soundmenu-dark.png
new file mode 100644
index 0000000..6643295
Binary files /dev/null and b/.playwright-mcp/eco-soundmenu-dark.png differ
diff --git a/.playwright-mcp/eco-wifimenu-bright.png b/.playwright-mcp/eco-wifimenu-bright.png
new file mode 100644
index 0000000..62f2e6d
Binary files /dev/null and b/.playwright-mcp/eco-wifimenu-bright.png differ
diff --git a/.playwright-mcp/eco-wifimenu-dark.png b/.playwright-mcp/eco-wifimenu-dark.png
new file mode 100644
index 0000000..2ba692a
Binary files /dev/null and b/.playwright-mcp/eco-wifimenu-dark.png differ
diff --git a/changelog.md b/changelog.md
index 28b196c..3f81623 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,14 @@
# Changelog
+## 2026-01-06 - 3.34.1 - fix(elements/applauncher)
+add eco app launcher components, wifi/sound/battery menus, demos and new eco-screensaver; replace dees-screensaver (breaking API change)
+
+- Add eco-applauncher group and subcomponents: eco-applauncher, eco-applauncher-wifimenu, eco-applauncher-soundmenu, eco-applauncher-batterymenu
+- Add demos for applauncher, wifi menu, sound menu and battery menu; include mock data for networks/devices/apps
+- Introduce eco-screensaver component and demo; remove legacy dees-screensaver implementation and export
+- Update elements index and z-index key (replace 'dees-screensaver' with 'eco-screensaver')
+- Menus dispatch events and include interactive behavior: wifi-toggle, network-select, volume-change, device-select, battery-saver-toggle, settings-click, menu-close
+
## 2026-01-06 - 3.34.0 - feat(dees-screensaver)
improve screensaver activation, visuals, and dismissal animations
diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts
index 190c57c..b558c15 100644
--- a/ts_web/00_commitinfo_data.ts
+++ b/ts_web/00_commitinfo_data.ts
@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@ecobridge.xyz/catalog',
- version: '3.34.0',
+ version: '3.34.1',
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
}
diff --git a/ts_web/elements/00group-applauncher/eco-applauncher-batterymenu/eco-applauncher-batterymenu.demo.ts b/ts_web/elements/00group-applauncher/eco-applauncher-batterymenu/eco-applauncher-batterymenu.demo.ts
new file mode 100644
index 0000000..453724e
--- /dev/null
+++ b/ts_web/elements/00group-applauncher/eco-applauncher-batterymenu/eco-applauncher-batterymenu.demo.ts
@@ -0,0 +1,24 @@
+import { html } from '@design.estate/dees-element';
+
+export const demo = () => html`
+
+
+ console.log('Battery saver:', e.detail)}
+ @settings-click=${() => console.log('Settings clicked')}
+ >
+
+`;
diff --git a/ts_web/elements/00group-applauncher/eco-applauncher-batterymenu/eco-applauncher-batterymenu.ts b/ts_web/elements/00group-applauncher/eco-applauncher-batterymenu/eco-applauncher-batterymenu.ts
new file mode 100644
index 0000000..3aa74c3
--- /dev/null
+++ b/ts_web/elements/00group-applauncher/eco-applauncher-batterymenu/eco-applauncher-batterymenu.ts
@@ -0,0 +1,346 @@
+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-batterymenu.demo.js';
+
+// Ensure dees-icon is registered
+DeesIcon;
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'eco-applauncher-batterymenu': EcoApplauncherBatterymenu;
+ }
+}
+
+@customElement('eco-applauncher-batterymenu')
+export class EcoApplauncherBatterymenu 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;
+ }
+
+ .battery-display {
+ padding: 24px 16px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 12px;
+ }
+
+ .battery-visual {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .battery-icon {
+ position: relative;
+ width: 80px;
+ height: 36px;
+ border: 2px solid ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 70%)')};
+ border-radius: 6px;
+ overflow: hidden;
+ }
+
+ .battery-icon::after {
+ content: '';
+ position: absolute;
+ right: -6px;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 4px;
+ height: 14px;
+ background: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 70%)')};
+ border-radius: 0 2px 2px 0;
+ }
+
+ .battery-fill {
+ height: 100%;
+ transition: width 0.3s ease, background 0.3s ease;
+ }
+
+ .battery-fill.good {
+ background: hsl(142 71% 45%);
+ }
+
+ .battery-fill.medium {
+ background: hsl(47 100% 50%);
+ }
+
+ .battery-fill.low {
+ background: hsl(0 72% 51%);
+ }
+
+ .battery-fill.charging {
+ background: linear-gradient(
+ 90deg,
+ hsl(142 71% 45%) 0%,
+ hsl(142 71% 55%) 50%,
+ hsl(142 71% 45%) 100%
+ );
+ background-size: 200% 100%;
+ animation: charging-pulse 1.5s ease-in-out infinite;
+ }
+
+ @keyframes charging-pulse {
+ 0% { background-position: 100% 0; }
+ 100% { background-position: -100% 0; }
+ }
+
+ .charging-icon {
+ color: hsl(47 100% 50%);
+ }
+
+ .battery-percentage {
+ font-size: 32px;
+ font-weight: 600;
+ color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
+ }
+
+ .battery-status {
+ font-size: 14px;
+ color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ }
+
+ .menu-divider {
+ height: 1px;
+ background: ${cssManager.bdTheme('hsl(0 0% 92%)', 'hsl(240 5% 15%)')};
+ }
+
+ .menu-option {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 14px 16px;
+ cursor: pointer;
+ transition: background 0.15s ease;
+ }
+
+ .menu-option:hover {
+ background: ${cssManager.bdTheme('hsl(0 0% 96%)', 'hsl(240 5% 15%)')};
+ }
+
+ .option-label {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ font-size: 14px;
+ color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
+ }
+
+ .option-description {
+ font-size: 12px;
+ color: ${cssManager.bdTheme('hsl(0 0% 60%)', 'hsl(0 0% 50%)')};
+ margin-top: 2px;
+ }
+
+ .toggle-switch {
+ position: relative;
+ width: 44px;
+ height: 24px;
+ background: ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(240 5% 25%)')};
+ border-radius: 12px;
+ cursor: pointer;
+ transition: background 0.2s ease;
+ flex-shrink: 0;
+ }
+
+ .toggle-switch.active {
+ background: hsl(217 91% 60%);
+ }
+
+ .toggle-switch::after {
+ content: '';
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ width: 20px;
+ height: 20px;
+ background: white;
+ border-radius: 50%;
+ transition: transform 0.2s ease;
+ box-shadow: ${cssManager.bdTheme('0 1px 3px rgba(0,0,0,0.2)', 'none')};
+ }
+
+ .toggle-switch.active::after {
+ transform: translateX(20px);
+ }
+
+ .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 batteryLevel = 100;
+
+ @property({ type: Boolean })
+ accessor isCharging = false;
+
+ @property({ type: Boolean })
+ accessor batterySaverEnabled = false;
+
+ @property({ type: String })
+ accessor timeRemaining: string | null = null;
+
+ private boundHandleClickOutside = this.handleClickOutside.bind(this);
+
+ public render(): TemplateResult {
+ const fillClass = this.getFillClass();
+
+ return html`
+
+ `;
+ }
+
+ private getFillClass(): string {
+ if (this.batteryLevel > 50) return 'good';
+ if (this.batteryLevel > 20) return 'medium';
+ return 'low';
+ }
+
+ private handleBatterySaverToggle(): void {
+ this.batterySaverEnabled = !this.batterySaverEnabled;
+ this.dispatchEvent(new CustomEvent('battery-saver-toggle', {
+ detail: { enabled: this.batterySaverEnabled },
+ 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);
+ }
+}
diff --git a/ts_web/elements/00group-applauncher/eco-applauncher-batterymenu/index.ts b/ts_web/elements/00group-applauncher/eco-applauncher-batterymenu/index.ts
new file mode 100644
index 0000000..01edbf5
--- /dev/null
+++ b/ts_web/elements/00group-applauncher/eco-applauncher-batterymenu/index.ts
@@ -0,0 +1 @@
+export * from './eco-applauncher-batterymenu.js';
diff --git a/ts_web/elements/00group-applauncher/eco-applauncher-soundmenu/eco-applauncher-soundmenu.demo.ts b/ts_web/elements/00group-applauncher/eco-applauncher-soundmenu/eco-applauncher-soundmenu.demo.ts
new file mode 100644
index 0000000..578c196
--- /dev/null
+++ b/ts_web/elements/00group-applauncher/eco-applauncher-soundmenu/eco-applauncher-soundmenu.demo.ts
@@ -0,0 +1,33 @@
+import { html } from '@design.estate/dees-element';
+import type { IAudioDevice } from './eco-applauncher-soundmenu.js';
+
+const mockDevices: IAudioDevice[] = [
+ { id: 'speakers', name: 'Built-in Speakers', type: 'speaker' },
+ { id: 'headphones', name: 'AirPods Pro', type: 'bluetooth' },
+ { id: 'hdmi', name: 'LG Monitor', type: 'hdmi' },
+];
+
+export const demo = () => html`
+
+
+ console.log('Volume:', e.detail)}
+ @mute-toggle=${(e: CustomEvent) => console.log('Mute:', e.detail)}
+ @device-select=${(e: CustomEvent) => console.log('Device:', e.detail)}
+ @settings-click=${() => console.log('Settings clicked')}
+ >
+
+`;
diff --git a/ts_web/elements/00group-applauncher/eco-applauncher-soundmenu/eco-applauncher-soundmenu.ts b/ts_web/elements/00group-applauncher/eco-applauncher-soundmenu/eco-applauncher-soundmenu.ts
new file mode 100644
index 0000000..e00906c
--- /dev/null
+++ b/ts_web/elements/00group-applauncher/eco-applauncher-soundmenu/eco-applauncher-soundmenu.ts
@@ -0,0 +1,427 @@
+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);
+ }
+}
diff --git a/ts_web/elements/00group-applauncher/eco-applauncher-soundmenu/index.ts b/ts_web/elements/00group-applauncher/eco-applauncher-soundmenu/index.ts
new file mode 100644
index 0000000..8ff3999
--- /dev/null
+++ b/ts_web/elements/00group-applauncher/eco-applauncher-soundmenu/index.ts
@@ -0,0 +1 @@
+export * from './eco-applauncher-soundmenu.js';
diff --git a/ts_web/elements/00group-applauncher/eco-applauncher-wifimenu/eco-applauncher-wifimenu.demo.ts b/ts_web/elements/00group-applauncher/eco-applauncher-wifimenu/eco-applauncher-wifimenu.demo.ts
new file mode 100644
index 0000000..1df0ad8
--- /dev/null
+++ b/ts_web/elements/00group-applauncher/eco-applauncher-wifimenu/eco-applauncher-wifimenu.demo.ts
@@ -0,0 +1,33 @@
+import { html } from '@design.estate/dees-element';
+import type { IWifiNetwork } from './eco-applauncher-wifimenu.js';
+
+const mockNetworks: IWifiNetwork[] = [
+ { ssid: 'HomeNetwork', signalStrength: 95, secured: true },
+ { ssid: 'OfficeWiFi', signalStrength: 75, secured: true },
+ { ssid: 'CoffeeShop_Guest', signalStrength: 60, secured: false },
+ { ssid: 'Neighbor_5G', signalStrength: 40, secured: true },
+ { ssid: 'WeakSignal', signalStrength: 15, secured: true },
+];
+
+export const demo = () => html`
+
+
+ console.log('WiFi toggle:', e.detail)}
+ @network-select=${(e: CustomEvent) => console.log('Network selected:', e.detail)}
+ @settings-click=${() => console.log('Settings clicked')}
+ >
+
+`;
diff --git a/ts_web/elements/00group-applauncher/eco-applauncher-wifimenu/eco-applauncher-wifimenu.ts b/ts_web/elements/00group-applauncher/eco-applauncher-wifimenu/eco-applauncher-wifimenu.ts
new file mode 100644
index 0000000..4048ec0
--- /dev/null
+++ b/ts_web/elements/00group-applauncher/eco-applauncher-wifimenu/eco-applauncher-wifimenu.ts
@@ -0,0 +1,355 @@
+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-wifimenu.demo.js';
+
+// Ensure dees-icon is registered
+DeesIcon;
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'eco-applauncher-wifimenu': EcoApplauncherWifimenu;
+ }
+}
+
+export interface IWifiNetwork {
+ ssid: string;
+ signalStrength: number; // 0-100
+ secured: boolean;
+ connected?: boolean;
+}
+
+@customElement('eco-applauncher-wifimenu')
+export class EcoApplauncherWifimenu 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;
+ }
+
+ .toggle-switch {
+ position: relative;
+ width: 44px;
+ height: 24px;
+ background: ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(240 5% 25%)')};
+ border-radius: 12px;
+ cursor: pointer;
+ transition: background 0.2s ease;
+ }
+
+ .toggle-switch.active {
+ background: hsl(217 91% 60%);
+ }
+
+ .toggle-switch::after {
+ content: '';
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ width: 20px;
+ height: 20px;
+ background: white;
+ border-radius: 50%;
+ transition: transform 0.2s ease;
+ box-shadow: ${cssManager.bdTheme('0 1px 3px rgba(0,0,0,0.2)', 'none')};
+ }
+
+ .toggle-switch.active::after {
+ transform: translateX(20px);
+ }
+
+ .network-list {
+ max-height: 240px;
+ overflow-y: auto;
+ }
+
+ .network-item {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 16px;
+ cursor: pointer;
+ transition: background 0.15s ease;
+ }
+
+ .network-item:hover {
+ background: ${cssManager.bdTheme('hsl(0 0% 96%)', 'hsl(240 5% 15%)')};
+ }
+
+ .network-item.connected {
+ background: ${cssManager.bdTheme('hsl(217 91% 95%)', 'hsl(217 91% 60% / 0.15)')};
+ }
+
+ .network-item.connected:hover {
+ background: ${cssManager.bdTheme('hsl(217 91% 92%)', 'hsl(217 91% 60% / 0.25)')};
+ }
+
+ .signal-bars {
+ display: flex;
+ align-items: flex-end;
+ gap: 2px;
+ height: 16px;
+ width: 20px;
+ }
+
+ .signal-bar {
+ width: 4px;
+ background: ${cssManager.bdTheme('hsl(0 0% 80%)', 'hsl(0 0% 40%)')};
+ border-radius: 1px;
+ transition: background 0.2s ease;
+ }
+
+ .signal-bar.active {
+ background: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 90%)')};
+ }
+
+ .signal-bar:nth-child(1) { height: 4px; }
+ .signal-bar:nth-child(2) { height: 8px; }
+ .signal-bar:nth-child(3) { height: 12px; }
+ .signal-bar:nth-child(4) { height: 16px; }
+
+ .network-info {
+ flex: 1;
+ min-width: 0;
+ }
+
+ .network-name {
+ font-size: 14px;
+ font-weight: 500;
+ color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .network-status {
+ font-size: 12px;
+ color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
+ margin-top: 2px;
+ }
+
+ .network-secured {
+ color: ${cssManager.bdTheme('hsl(0 0% 60%)', 'hsl(0 0% 50%)')};
+ }
+
+ .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%);
+ }
+
+ .disabled-message {
+ padding: 32px 16px;
+ text-align: center;
+ color: ${cssManager.bdTheme('hsl(0 0% 60%)', 'hsl(0 0% 50%)')};
+ font-size: 14px;
+ }
+ `,
+ ];
+
+ @property({ type: Boolean, reflect: true })
+ accessor open = false;
+
+ @property({ type: Array })
+ accessor networks: IWifiNetwork[] = [];
+
+ @property({ type: String })
+ accessor connectedNetwork: string | null = null;
+
+ @property({ type: Boolean })
+ accessor wifiEnabled = true;
+
+ private boundHandleClickOutside = this.handleClickOutside.bind(this);
+
+ public render(): TemplateResult {
+ return html`
+
+ `;
+ }
+
+ private renderNetworkList(): TemplateResult {
+ const sortedNetworks = [...this.networks].sort((a, b) => {
+ // Connected network first, then by signal strength
+ if (a.ssid === this.connectedNetwork) return -1;
+ if (b.ssid === this.connectedNetwork) return 1;
+ return b.signalStrength - a.signalStrength;
+ });
+
+ return html`
+
+ ${sortedNetworks.map((network) => this.renderNetworkItem(network))}
+
+ `;
+ }
+
+ private renderNetworkItem(network: IWifiNetwork): TemplateResult {
+ const isConnected = network.ssid === this.connectedNetwork;
+ const signalBars = this.getSignalBars(network.signalStrength);
+
+ return html`
+ this.handleNetworkSelect(network)}
+ >
+
+ ${[1, 2, 3, 4].map((bar) => html`
+
+ `)}
+
+
+
${network.ssid}
+ ${isConnected ? html`
Connected
` : ''}
+
+ ${network.secured ? html`
+
+ ` : ''}
+
+ `;
+ }
+
+ private renderDisabledMessage(): TemplateResult {
+ return html`
+
+ Wi-Fi is turned off
+
+ `;
+ }
+
+ private getSignalBars(strength: number): number {
+ if (strength >= 75) return 4;
+ if (strength >= 50) return 3;
+ if (strength >= 25) return 2;
+ return 1;
+ }
+
+ private handleToggleWifi(): void {
+ this.wifiEnabled = !this.wifiEnabled;
+ this.dispatchEvent(new CustomEvent('wifi-toggle', {
+ detail: { enabled: this.wifiEnabled },
+ bubbles: true,
+ composed: true,
+ }));
+ }
+
+ private handleNetworkSelect(network: IWifiNetwork): void {
+ this.dispatchEvent(new CustomEvent('network-select', {
+ detail: { network },
+ 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();
+ // Delay to prevent immediate close when clicking to open
+ setTimeout(() => {
+ document.addEventListener('click', this.boundHandleClickOutside);
+ }, 0);
+ }
+
+ async disconnectedCallback(): Promise {
+ await super.disconnectedCallback();
+ document.removeEventListener('click', this.boundHandleClickOutside);
+ }
+}
diff --git a/ts_web/elements/00group-applauncher/eco-applauncher-wifimenu/index.ts b/ts_web/elements/00group-applauncher/eco-applauncher-wifimenu/index.ts
new file mode 100644
index 0000000..8585a56
--- /dev/null
+++ b/ts_web/elements/00group-applauncher/eco-applauncher-wifimenu/index.ts
@@ -0,0 +1 @@
+export * from './eco-applauncher-wifimenu.js';
diff --git a/ts_web/elements/00group-applauncher/eco-applauncher/eco-applauncher.demo.ts b/ts_web/elements/00group-applauncher/eco-applauncher/eco-applauncher.demo.ts
new file mode 100644
index 0000000..9ccd9b5
--- /dev/null
+++ b/ts_web/elements/00group-applauncher/eco-applauncher/eco-applauncher.demo.ts
@@ -0,0 +1,71 @@
+import { html } from '@design.estate/dees-element';
+import type { IAppIcon } from './eco-applauncher.js';
+import type { IWifiNetwork } from '../eco-applauncher-wifimenu/index.js';
+import type { IAudioDevice } from '../eco-applauncher-soundmenu/index.js';
+
+const mockApps: IAppIcon[] = [
+ { name: 'Settings', icon: 'lucide:settings', action: () => console.log('Settings clicked') },
+ { name: 'Browser', icon: 'lucide:globe', action: () => console.log('Browser clicked') },
+ { name: 'Terminal', icon: 'lucide:terminal', action: () => console.log('Terminal clicked') },
+ { name: 'Files', icon: 'lucide:folder', action: () => console.log('Files clicked') },
+ { name: 'Calendar', icon: 'lucide:calendar', action: () => console.log('Calendar clicked') },
+ { name: 'Mail', icon: 'lucide:mail', action: () => console.log('Mail clicked') },
+ { name: 'Music', icon: 'lucide:music', action: () => console.log('Music clicked') },
+ { name: 'Photos', icon: 'lucide:image', action: () => console.log('Photos clicked') },
+ { name: 'Notes', icon: 'lucide:fileText', action: () => console.log('Notes clicked') },
+ { name: 'Calculator', icon: 'lucide:calculator', action: () => console.log('Calculator clicked') },
+ { name: 'Weather', icon: 'lucide:cloudSun', action: () => console.log('Weather clicked') },
+ { name: 'Maps', icon: 'lucide:map', action: () => console.log('Maps clicked') },
+];
+
+const mockNetworks: IWifiNetwork[] = [
+ { ssid: 'HomeNetwork', signalStrength: 95, secured: true },
+ { ssid: 'OfficeWiFi', signalStrength: 75, secured: true },
+ { ssid: 'CoffeeShop_Guest', signalStrength: 60, secured: false },
+ { ssid: 'Neighbor_5G', signalStrength: 40, secured: true },
+ { ssid: 'WeakSignal', signalStrength: 15, secured: true },
+];
+
+const mockAudioDevices: IAudioDevice[] = [
+ { id: 'speakers', name: 'Built-in Speakers', type: 'speaker' },
+ { id: 'headphones', name: 'AirPods Pro', type: 'bluetooth' },
+ { id: 'hdmi', name: 'LG Monitor', type: 'hdmi' },
+];
+
+export const demo = () => html`
+
+
+ console.log('WiFi toggle:', e.detail)}
+ @network-select=${(e: CustomEvent) => console.log('Network selected:', e.detail)}
+ @wifi-settings-click=${() => console.log('WiFi settings clicked')}
+ @battery-saver-toggle=${(e: CustomEvent) => console.log('Battery saver:', e.detail)}
+ @battery-settings-click=${() => console.log('Battery settings clicked')}
+ @volume-change=${(e: CustomEvent) => console.log('Volume:', e.detail)}
+ @mute-toggle=${(e: CustomEvent) => console.log('Mute:', e.detail)}
+ @device-select=${(e: CustomEvent) => console.log('Device:', e.detail)}
+ @sound-settings-click=${() => console.log('Sound settings clicked')}
+ @search-click=${() => console.log('Search clicked')}
+ @notifications-click=${() => console.log('Notifications clicked')}
+ @user-click=${() => console.log('User clicked')}
+ >
+
+`;
diff --git a/ts_web/elements/00group-applauncher/eco-applauncher/eco-applauncher.ts b/ts_web/elements/00group-applauncher/eco-applauncher/eco-applauncher.ts
new file mode 100644
index 0000000..ff75592
--- /dev/null
+++ b/ts_web/elements/00group-applauncher/eco-applauncher/eco-applauncher.ts
@@ -0,0 +1,955 @@
+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`
+
+
+ Search apps...
+
+ ` : ''}
+
+
+ ${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`
+
+
+
+
+ `;
+ }
+
+ 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`
+
+
+
+
+ `;
+ }
+
+ 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`
+
+
+
+
+ `;
+ }
+
+ 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;
+ }
+ }
+}
diff --git a/ts_web/elements/00group-applauncher/eco-applauncher/index.ts b/ts_web/elements/00group-applauncher/eco-applauncher/index.ts
new file mode 100644
index 0000000..3007e56
--- /dev/null
+++ b/ts_web/elements/00group-applauncher/eco-applauncher/index.ts
@@ -0,0 +1 @@
+export * from './eco-applauncher.js';
diff --git a/ts_web/elements/00group-applauncher/index.ts b/ts_web/elements/00group-applauncher/index.ts
new file mode 100644
index 0000000..32bbea4
--- /dev/null
+++ b/ts_web/elements/00group-applauncher/index.ts
@@ -0,0 +1,5 @@
+// App Launcher Components
+export * from './eco-applauncher/index.js';
+export * from './eco-applauncher-wifimenu/index.js';
+export * from './eco-applauncher-batterymenu/index.js';
+export * from './eco-applauncher-soundmenu/index.js';
diff --git a/ts_web/elements/00zindex.ts b/ts_web/elements/00zindex.ts
index 9344e49..c43efe0 100644
--- a/ts_web/elements/00zindex.ts
+++ b/ts_web/elements/00zindex.ts
@@ -46,5 +46,5 @@ export function getZIndex(category: keyof typeof zIndexLayers, subcategory?: str
// Z-index assignments for components
export const componentZIndex = {
- 'dees-screensaver': zIndexLayers.overlay.screensaver,
+ 'eco-screensaver': zIndexLayers.overlay.screensaver,
} as const;
diff --git a/ts_web/elements/dees-screensaver/index.ts b/ts_web/elements/dees-screensaver/index.ts
deleted file mode 100644
index 9ff042f..0000000
--- a/ts_web/elements/dees-screensaver/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './dees-screensaver.js';
diff --git a/ts_web/elements/eco-screensaver/eco-screensaver.demo.ts b/ts_web/elements/eco-screensaver/eco-screensaver.demo.ts
new file mode 100644
index 0000000..d5b6292
--- /dev/null
+++ b/ts_web/elements/eco-screensaver/eco-screensaver.demo.ts
@@ -0,0 +1,18 @@
+import { html } from '@design.estate/dees-element';
+import { EcoScreensaver } from './eco-screensaver.js';
+
+export const demo = () => {
+ // Clean up any existing instance first
+ if (EcoScreensaver.instance) {
+ EcoScreensaver.instance.remove();
+ EcoScreensaver.instance = null;
+ }
+
+ return html`
+
+
+ Screensaver activates after 2 seconds of inactivity.
+ Move mouse or press keys to reset timer.
+
+ `;
+};
diff --git a/ts_web/elements/dees-screensaver/dees-screensaver.ts b/ts_web/elements/eco-screensaver/eco-screensaver.ts
similarity index 70%
rename from ts_web/elements/dees-screensaver/dees-screensaver.ts
rename to ts_web/elements/eco-screensaver/eco-screensaver.ts
index d1cdaf7..9e7f513 100644
--- a/ts_web/elements/dees-screensaver/dees-screensaver.ts
+++ b/ts_web/elements/eco-screensaver/eco-screensaver.ts
@@ -9,10 +9,11 @@ import {
state,
} from '@design.estate/dees-element';
import { zIndexLayers } from '../00zindex.js';
+import { demo } from './eco-screensaver.demo.js';
declare global {
interface HTMLElementTagNameMap {
- 'dees-screensaver': DeesScreensaver;
+ 'eco-screensaver': EcoScreensaver;
}
}
@@ -26,56 +27,36 @@ const colors = [
'hsl(142 71% 45%)', // green-500
];
-@customElement('dees-screensaver')
-export class DeesScreensaver extends DeesElement {
- public static demo = () => {
- // Clean up any existing instance first
- if (DeesScreensaver.instance) {
- DeesScreensaver.instance.remove();
- DeesScreensaver.instance = null;
- }
-
- // Create screensaver element immediately but inactive
- const screensaver = document.createElement('dees-screensaver') as DeesScreensaver;
- document.body.appendChild(screensaver);
- DeesScreensaver.instance = screensaver;
-
- // Activate after 2 seconds to show the animation
- setTimeout(() => {
- if (DeesScreensaver.instance === screensaver) {
- screensaver.active = true;
- }
- }, 2000);
-
- return html`Screensaver will activate in 2 seconds...
`;
- };
+@customElement('eco-screensaver')
+export class EcoScreensaver extends DeesElement {
+ public static demo = demo;
// Instance management
- private static instance: DeesScreensaver | null = null;
+ public static instance: EcoScreensaver | null = null;
- public static async show(): Promise {
- if (DeesScreensaver.instance) {
- DeesScreensaver.instance.active = true;
- return DeesScreensaver.instance;
+ public static async show(): Promise {
+ if (EcoScreensaver.instance) {
+ EcoScreensaver.instance.active = true;
+ return EcoScreensaver.instance;
}
- const screensaver = new DeesScreensaver();
+ const screensaver = new EcoScreensaver();
screensaver.active = true;
document.body.appendChild(screensaver);
- DeesScreensaver.instance = screensaver;
+ EcoScreensaver.instance = screensaver;
return screensaver;
}
public static hide(): void {
- if (DeesScreensaver.instance) {
- DeesScreensaver.instance.active = false;
+ if (EcoScreensaver.instance) {
+ EcoScreensaver.instance.active = false;
}
}
public static destroy(): void {
- if (DeesScreensaver.instance) {
- DeesScreensaver.instance.remove();
- DeesScreensaver.instance = null;
+ if (EcoScreensaver.instance) {
+ EcoScreensaver.instance.remove();
+ EcoScreensaver.instance = null;
}
}
@@ -201,12 +182,39 @@ export class DeesScreensaver extends DeesElement {
font-size: 14px;
}
}
+
+ .hint {
+ position: fixed;
+ bottom: 32px;
+ left: 50%;
+ transform: translateX(-50%) translateY(20px);
+ padding: 12px 24px;
+ background: hsl(240 6% 15%);
+ border: 1px solid hsl(240 5% 26%);
+ border-radius: 8px;
+ color: hsl(0 0% 90%);
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+ font-size: 14px;
+ font-weight: 500;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.3s ease, transform 0.3s ease;
+ z-index: 10;
+ }
+
+ .hint.visible {
+ opacity: 1;
+ transform: translateX(-50%) translateY(0);
+ }
`,
];
@property({ type: Boolean, reflect: true })
accessor active = false;
+ @property({ type: Number })
+ accessor delay = 0; // milliseconds before activation (0 = no delay)
+
@state()
accessor currentTime = '';
@@ -230,10 +238,18 @@ export class DeesScreensaver extends DeesElement {
private timeContainerEl: HTMLElement | null = null;
private vignetteEl: HTMLElement | null = null;
private contentEl: HTMLElement | null = null;
+ private delayTimeoutId: ReturnType | null = null;
+ private boundResetDelayTimer: () => void;
+ private boundShowHint: () => void;
+ private hintEl: HTMLElement | null = null;
+ private hintTimeoutId: ReturnType | null = null;
+ private hintVisible = false;
constructor() {
super();
this.updateTime();
+ this.boundResetDelayTimer = this.resetDelayTimer.bind(this);
+ this.boundShowHint = this.showHint.bind(this);
}
public render(): TemplateResult {
@@ -246,6 +262,7 @@ export class DeesScreensaver extends DeesElement {
${this.currentDate}
+ Click to exit screensaver
`;
}
@@ -253,18 +270,28 @@ export class DeesScreensaver extends DeesElement {
this.timeContainerEl = this.shadowRoot?.querySelector('.time-container') as HTMLElement;
this.vignetteEl = this.shadowRoot?.querySelector('.vignette') as HTMLElement;
this.contentEl = this.shadowRoot?.querySelector('.screensaver-content') as HTMLElement;
+ this.hintEl = this.shadowRoot?.querySelector('.hint') as HTMLElement;
}
async connectedCallback(): Promise {
await super.connectedCallback();
- this.startAnimation();
- this.startTimeUpdate();
+
+ // If delay is set, start the delay timer and listen for activity
+ if (this.delay > 0 && !this.active) {
+ this.startDelayTimer();
+ this.addActivityListeners();
+ } else if (this.active) {
+ this.startAnimation();
+ this.startTimeUpdate();
+ }
}
async disconnectedCallback(): Promise {
await super.disconnectedCallback();
this.stopAnimation();
this.stopTimeUpdate();
+ this.stopDelayTimer();
+ this.removeActivityListeners();
}
updated(changedProperties: Map): void {
@@ -276,9 +303,15 @@ export class DeesScreensaver extends DeesElement {
this.contentEl.style.maskImage = '';
this.contentEl.style.webkitMaskImage = '';
}
+ // Hide hint when freshly activated
+ this.hideHint();
+ // Listen for mouse movement to show hint
+ window.addEventListener('mousemove', this.boundShowHint);
this.startAnimation();
this.startTimeUpdate();
} else {
+ window.removeEventListener('mousemove', this.boundShowHint);
+ this.hideHint();
this.stopAnimation();
this.stopTimeUpdate();
}
@@ -393,6 +426,71 @@ export class DeesScreensaver extends DeesElement {
}
}
+ private startDelayTimer(): void {
+ this.stopDelayTimer();
+ this.delayTimeoutId = setTimeout(() => {
+ this.removeActivityListeners();
+ this.active = true;
+ }, this.delay);
+ }
+
+ private stopDelayTimer(): void {
+ if (this.delayTimeoutId) {
+ clearTimeout(this.delayTimeoutId);
+ this.delayTimeoutId = null;
+ }
+ }
+
+ private resetDelayTimer(): void {
+ if (this.delay > 0 && !this.active) {
+ this.startDelayTimer();
+ }
+ }
+
+ private addActivityListeners(): void {
+ window.addEventListener('mousemove', this.boundResetDelayTimer);
+ window.addEventListener('keydown', this.boundResetDelayTimer);
+ window.addEventListener('click', this.boundResetDelayTimer);
+ window.addEventListener('touchstart', this.boundResetDelayTimer);
+ window.addEventListener('scroll', this.boundResetDelayTimer);
+ }
+
+ private removeActivityListeners(): void {
+ window.removeEventListener('mousemove', this.boundResetDelayTimer);
+ window.removeEventListener('keydown', this.boundResetDelayTimer);
+ window.removeEventListener('click', this.boundResetDelayTimer);
+ window.removeEventListener('touchstart', this.boundResetDelayTimer);
+ window.removeEventListener('scroll', this.boundResetDelayTimer);
+ }
+
+ private showHint(): void {
+ if (!this.active || this.hintVisible) return;
+
+ this.hintVisible = true;
+ if (this.hintEl) {
+ this.hintEl.classList.add('visible');
+ }
+
+ // Auto-hide after 3 seconds
+ if (this.hintTimeoutId) {
+ clearTimeout(this.hintTimeoutId);
+ }
+ this.hintTimeoutId = setTimeout(() => {
+ this.hideHint();
+ }, 3000);
+ }
+
+ private hideHint(): void {
+ this.hintVisible = false;
+ if (this.hintEl) {
+ this.hintEl.classList.remove('visible');
+ }
+ if (this.hintTimeoutId) {
+ clearTimeout(this.hintTimeoutId);
+ this.hintTimeoutId = null;
+ }
+ }
+
private handleClick(event: MouseEvent | TouchEvent): void {
// Get click/touch position
let x: number, y: number;
@@ -434,7 +532,7 @@ export class DeesScreensaver extends DeesElement {
} else {
// Animation complete - remove screensaver
this.active = false;
- DeesScreensaver.destroy();
+ EcoScreensaver.destroy();
}
};
diff --git a/ts_web/elements/eco-screensaver/index.ts b/ts_web/elements/eco-screensaver/index.ts
new file mode 100644
index 0000000..458156b
--- /dev/null
+++ b/ts_web/elements/eco-screensaver/index.ts
@@ -0,0 +1 @@
+export * from './eco-screensaver.js';
diff --git a/ts_web/elements/index.ts b/ts_web/elements/index.ts
index 32092ff..7964ef4 100644
--- a/ts_web/elements/index.ts
+++ b/ts_web/elements/index.ts
@@ -1,5 +1,8 @@
export * from './00zindex.js';
export * from './00theme.js';
+// Component Groups
+export * from './00group-applauncher/index.js';
+
// Standalone Components
-export * from './dees-screensaver/index.js';
+export * from './eco-screensaver/index.js';