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` + + ` : ''} +
+
+ ${this.topBarConfig.showNotifications ? html` +
+ + ${this.notificationCount > 0 ? html` + ${this.notificationCount > 99 ? '99+' : this.notificationCount} + ` : ''} +
+ ` : ''} + ${this.topBarConfig.showUser ? html` +
+ ${userInitials} +
+ ` : ''} +
+
+ `; + } + + private renderNetworkStatusWithMenu(): TemplateResult { + const bars = this.getNetworkBars(); + return html` +
+
+
+ ${[1, 2, 3, 4].map((bar) => html` +
+ `)} +
+
+ +
+ `; + } + + private renderNetworkStatus(): TemplateResult { + const bars = this.getNetworkBars(); + return html` +
+
+ ${[1, 2, 3, 4].map((bar) => html` +
+ `)} +
+
+ `; + } + + private renderBatteryStatusWithMenu(): TemplateResult { + const level = typeof this.batteryLevel === 'number' ? this.batteryLevel : 100; + const isCharging = this.batteryLevel === 'charging' || this.isCharging; + const isLow = level < 20; + + return html` +
+
+
+
+
+ ${isCharging + ? html`` + : html`${level}%` + } +
+ +
+ `; + } + + private renderBatteryStatus(): TemplateResult { + const level = typeof this.batteryLevel === 'number' ? this.batteryLevel : 100; + const isCharging = this.batteryLevel === 'charging'; + const isLow = level < 20; + + return html` +
+
+
+
+ ${isCharging + ? html`` + : html`${level}%` + } +
+ `; + } + + private renderSoundStatusWithMenu(): TemplateResult { + const activeBars = Math.ceil((this.soundLevel / 100) * 3); + const soundIcon = this.muted || this.soundLevel === 0 ? 'lucide:volumeX' : 'lucide:volume2'; + + return html` +
+
+ +
+ ${[1, 2, 3].map((bar) => html` +
+ `)} +
+
+ +
+ `; + } + + private renderSoundStatus(): TemplateResult { + const activeBars = Math.ceil((this.soundLevel / 100) * 3); + const soundIcon = this.soundLevel === 0 ? 'lucide:volumeX' : 'lucide:volume2'; + return html` +
+ +
+ ${[1, 2, 3].map((bar) => html` +
+ `)} +
+
+ `; + } + + private getNetworkBars(): number { + switch (this.networkStatus) { + case 'online': + return 4; + case 'connecting': + return 2; + case 'offline': + return 0; + default: + return 4; + } + } + + private handleAppClick(app: IAppIcon): void { + this.dispatchEvent( + new CustomEvent('app-click', { + detail: { app }, + bubbles: true, + composed: true, + }) + ); + if (app.action) { + app.action(); + } + } + + private handleNetworkClick(e: MouseEvent): void { + e.stopPropagation(); + this.batteryMenuOpen = false; + this.soundMenuOpen = false; + this.wifiMenuOpen = !this.wifiMenuOpen; + } + + private handleBatteryClick(e: MouseEvent): void { + e.stopPropagation(); + this.wifiMenuOpen = false; + this.soundMenuOpen = false; + this.batteryMenuOpen = !this.batteryMenuOpen; + } + + private handleSoundClick(e: MouseEvent): void { + e.stopPropagation(); + this.wifiMenuOpen = false; + this.batteryMenuOpen = false; + this.soundMenuOpen = !this.soundMenuOpen; + } + + private handleWifiMenuClose(): void { + this.wifiMenuOpen = false; + } + + private handleBatteryMenuClose(): void { + this.batteryMenuOpen = false; + } + + private handleSoundMenuClose(): void { + this.soundMenuOpen = false; + } + + private handleVolumeChange(e: CustomEvent): void { + this.soundLevel = e.detail.volume; + this.dispatchEvent(new CustomEvent('volume-change', { + detail: e.detail, + bubbles: true, + composed: true, + })); + } + + private handleMuteToggle(e: CustomEvent): void { + this.muted = e.detail.muted; + this.dispatchEvent(new CustomEvent('mute-toggle', { + detail: e.detail, + bubbles: true, + composed: true, + })); + } + + private handleDeviceSelect(e: CustomEvent): void { + this.activeDeviceId = e.detail.device.id; + this.dispatchEvent(new CustomEvent('device-select', { + detail: e.detail, + bubbles: true, + composed: true, + })); + } + + private handleSoundSettingsClick(): void { + this.soundMenuOpen = false; + this.dispatchEvent(new CustomEvent('sound-settings-click', { + bubbles: true, + composed: true, + })); + } + + private handleWifiToggle(e: CustomEvent): void { + this.wifiEnabled = e.detail.enabled; + this.dispatchEvent(new CustomEvent('wifi-toggle', { + detail: e.detail, + bubbles: true, + composed: true, + })); + } + + private handleNetworkSelect(e: CustomEvent): void { + this.dispatchEvent(new CustomEvent('network-select', { + detail: e.detail, + bubbles: true, + composed: true, + })); + } + + private handleWifiSettingsClick(): void { + this.wifiMenuOpen = false; + this.dispatchEvent(new CustomEvent('wifi-settings-click', { + bubbles: true, + composed: true, + })); + } + + private handleBatterySaverToggle(e: CustomEvent): void { + this.batterySaverEnabled = e.detail.enabled; + this.dispatchEvent(new CustomEvent('battery-saver-toggle', { + detail: e.detail, + bubbles: true, + composed: true, + })); + } + + private handleBatterySettingsClick(): void { + this.batteryMenuOpen = false; + this.dispatchEvent(new CustomEvent('battery-settings-click', { + bubbles: true, + composed: true, + })); + } + + private handleSearchClick(): void { + this.dispatchEvent(new CustomEvent('search-click', { + bubbles: true, + composed: true, + })); + } + + private handleNotificationsClick(): void { + this.dispatchEvent(new CustomEvent('notifications-click', { + bubbles: true, + composed: true, + })); + } + + private handleUserClick(): void { + this.dispatchEvent(new CustomEvent('user-click', { + bubbles: true, + composed: true, + })); + } + + private updateTime(): void { + const now = new Date(); + const hours = now.getHours(); + const minutes = String(now.getMinutes()).padStart(2, '0'); + const ampm = hours >= 12 ? 'PM' : 'AM'; + const displayHours = hours % 12 || 12; + this.currentTime = `${displayHours}:${minutes} ${ampm}`; + + // Update date + const options: Intl.DateTimeFormatOptions = { + weekday: 'short', + month: 'short', + day: 'numeric', + }; + this.currentDate = now.toLocaleDateString('en-US', options); + } + + async connectedCallback(): Promise { + await super.connectedCallback(); + this.updateTime(); + this.timeUpdateInterval = setInterval(() => this.updateTime(), 1000); + } + + async disconnectedCallback(): Promise { + await super.disconnectedCallback(); + if (this.timeUpdateInterval) { + clearInterval(this.timeUpdateInterval); + this.timeUpdateInterval = null; + } + } +} 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';