fix(elements/applauncher): add eco app launcher components, wifi/sound/battery menus, demos and new eco-screensaver; replace dees-screensaver (breaking API change)
BIN
.playwright-mcp/applauncher-battery-menu-fixed.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
.playwright-mcp/applauncher-battery-menu-open.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
.playwright-mcp/applauncher-initial.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
.playwright-mcp/applauncher-wifi-menu-open.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
.playwright-mcp/eco-applauncher-bright.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
.playwright-mcp/eco-applauncher-dark.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
.playwright-mcp/eco-batterymenu-bright.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
.playwright-mcp/eco-batterymenu-dark.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
.playwright-mcp/eco-soundmenu-dark.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
.playwright-mcp/eco-wifimenu-bright.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
.playwright-mcp/eco-wifimenu-dark.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
@@ -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
|
||||
|
||||
|
||||
@@ -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.'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
|
||||
export const demo = () => html`
|
||||
<style>
|
||||
.demo-container {
|
||||
padding: 48px;
|
||||
background: hsl(240 10% 4%);
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
<div class="demo-container">
|
||||
<eco-applauncher-batterymenu
|
||||
open
|
||||
.batteryLevel=${85}
|
||||
.isCharging=${false}
|
||||
.batterySaverEnabled=${false}
|
||||
.timeRemaining=${'2h 30m remaining'}
|
||||
@battery-saver-toggle=${(e: CustomEvent) => console.log('Battery saver:', e.detail)}
|
||||
@settings-click=${() => console.log('Settings clicked')}
|
||||
></eco-applauncher-batterymenu>
|
||||
</div>
|
||||
`;
|
||||
@@ -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`
|
||||
<div class="menu-container">
|
||||
<div class="battery-display">
|
||||
<div class="battery-visual">
|
||||
<div class="battery-icon">
|
||||
<div
|
||||
class="battery-fill ${fillClass} ${this.isCharging ? 'charging' : ''}"
|
||||
style="width: ${this.batteryLevel}%"
|
||||
></div>
|
||||
</div>
|
||||
${this.isCharging ? html`
|
||||
<dees-icon class="charging-icon" .icon=${'lucide:zap'} .iconSize=${24}></dees-icon>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="battery-percentage">${this.batteryLevel}%</div>
|
||||
<div class="battery-status">
|
||||
${this.isCharging ? html`
|
||||
<dees-icon .icon=${'lucide:plug'} .iconSize=${14}></dees-icon>
|
||||
Charging
|
||||
` : this.timeRemaining ? html`
|
||||
<dees-icon .icon=${'lucide:clock'} .iconSize=${14}></dees-icon>
|
||||
${this.timeRemaining}
|
||||
` : html`
|
||||
<dees-icon .icon=${'lucide:battery'} .iconSize=${14}></dees-icon>
|
||||
On Battery
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="menu-divider"></div>
|
||||
|
||||
<div class="menu-option" @click=${this.handleBatterySaverToggle}>
|
||||
<div class="option-label">
|
||||
<dees-icon .icon=${'lucide:leaf'} .iconSize=${18}></dees-icon>
|
||||
<div>
|
||||
<div>Battery Saver</div>
|
||||
<div class="option-description">Extends battery life</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="toggle-switch ${this.batterySaverEnabled ? 'active' : ''}"
|
||||
@click=${(e: Event) => e.stopPropagation()}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="menu-footer">
|
||||
<div class="settings-link" @click=${this.handleSettingsClick}>
|
||||
<dees-icon .icon=${'lucide:settings'} .iconSize=${14}></dees-icon>
|
||||
Power Settings...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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<void> {
|
||||
await super.connectedCallback();
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.boundHandleClickOutside);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
async disconnectedCallback(): Promise<void> {
|
||||
await super.disconnectedCallback();
|
||||
document.removeEventListener('click', this.boundHandleClickOutside);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './eco-applauncher-batterymenu.js';
|
||||
@@ -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`
|
||||
<style>
|
||||
.demo-container {
|
||||
padding: 48px;
|
||||
background: hsl(240 10% 4%);
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
<div class="demo-container">
|
||||
<eco-applauncher-soundmenu
|
||||
open
|
||||
.volume=${70}
|
||||
.muted=${false}
|
||||
.outputDevices=${mockDevices}
|
||||
.activeDeviceId=${'speakers'}
|
||||
@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)}
|
||||
@settings-click=${() => console.log('Settings clicked')}
|
||||
></eco-applauncher-soundmenu>
|
||||
</div>
|
||||
`;
|
||||
@@ -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`
|
||||
<div class="menu-container">
|
||||
<div class="menu-header">
|
||||
<span class="menu-title">
|
||||
<dees-icon .icon=${volumeIcon} .iconSize=${18}></dees-icon>
|
||||
Sound
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="volume-section">
|
||||
<div class="volume-slider-container">
|
||||
<dees-icon
|
||||
class="volume-icon ${this.muted ? 'muted' : ''}"
|
||||
.icon=${this.muted ? 'lucide:volumeX' : 'lucide:volume2'}
|
||||
.iconSize=${20}
|
||||
@click=${this.handleMuteToggle}
|
||||
></dees-icon>
|
||||
<div
|
||||
class="volume-slider"
|
||||
@click=${this.handleSliderClick}
|
||||
@mousedown=${this.handleSliderMouseDown}
|
||||
>
|
||||
<div
|
||||
class="volume-fill ${this.muted ? 'muted' : ''}"
|
||||
style="width: ${this.muted ? 0 : this.volume}%"
|
||||
></div>
|
||||
<div
|
||||
class="volume-thumb"
|
||||
style="left: ${this.muted ? 0 : this.volume}%"
|
||||
></div>
|
||||
</div>
|
||||
<span class="volume-percentage">${this.muted ? 0 : this.volume}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${this.outputDevices.length > 0 ? html`
|
||||
<div class="menu-divider"></div>
|
||||
<div class="section-title">Output</div>
|
||||
<div class="device-list">
|
||||
${this.outputDevices.map((device) => this.renderDeviceItem(device))}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="menu-footer">
|
||||
<div class="settings-link" @click=${this.handleSettingsClick}>
|
||||
<dees-icon .icon=${'lucide:settings'} .iconSize=${14}></dees-icon>
|
||||
Sound Settings...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderDeviceItem(device: IAudioDevice): TemplateResult {
|
||||
const isActive = device.id === this.activeDeviceId;
|
||||
const icon = this.getDeviceIcon(device.type);
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="device-item ${isActive ? 'active' : ''}"
|
||||
@click=${() => this.handleDeviceSelect(device)}
|
||||
>
|
||||
<dees-icon class="device-icon" .icon=${icon} .iconSize=${18}></dees-icon>
|
||||
<span class="device-name">${device.name}</span>
|
||||
${isActive ? html`
|
||||
<dees-icon class="device-check" .icon=${'lucide:check'} .iconSize=${16}></dees-icon>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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<void> {
|
||||
await super.connectedCallback();
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.boundHandleClickOutside);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
async disconnectedCallback(): Promise<void> {
|
||||
await super.disconnectedCallback();
|
||||
document.removeEventListener('click', this.boundHandleClickOutside);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './eco-applauncher-soundmenu.js';
|
||||
@@ -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`
|
||||
<style>
|
||||
.demo-container {
|
||||
padding: 48px;
|
||||
background: hsl(240 10% 4%);
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
<div class="demo-container">
|
||||
<eco-applauncher-wifimenu
|
||||
open
|
||||
.networks=${mockNetworks}
|
||||
.connectedNetwork=${'HomeNetwork'}
|
||||
.wifiEnabled=${true}
|
||||
@wifi-toggle=${(e: CustomEvent) => console.log('WiFi toggle:', e.detail)}
|
||||
@network-select=${(e: CustomEvent) => console.log('Network selected:', e.detail)}
|
||||
@settings-click=${() => console.log('Settings clicked')}
|
||||
></eco-applauncher-wifimenu>
|
||||
</div>
|
||||
`;
|
||||
@@ -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`
|
||||
<div class="menu-container">
|
||||
<div class="menu-header">
|
||||
<span class="menu-title">
|
||||
<dees-icon .icon=${'lucide:wifi'} .iconSize=${18}></dees-icon>
|
||||
Wi-Fi
|
||||
</span>
|
||||
<div
|
||||
class="toggle-switch ${this.wifiEnabled ? 'active' : ''}"
|
||||
@click=${this.handleToggleWifi}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
${this.wifiEnabled ? this.renderNetworkList() : this.renderDisabledMessage()}
|
||||
|
||||
<div class="menu-footer">
|
||||
<div class="settings-link" @click=${this.handleSettingsClick}>
|
||||
<dees-icon .icon=${'lucide:settings'} .iconSize=${14}></dees-icon>
|
||||
Wi-Fi Settings...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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`
|
||||
<div class="network-list">
|
||||
${sortedNetworks.map((network) => this.renderNetworkItem(network))}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderNetworkItem(network: IWifiNetwork): TemplateResult {
|
||||
const isConnected = network.ssid === this.connectedNetwork;
|
||||
const signalBars = this.getSignalBars(network.signalStrength);
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="network-item ${isConnected ? 'connected' : ''}"
|
||||
@click=${() => this.handleNetworkSelect(network)}
|
||||
>
|
||||
<div class="signal-bars">
|
||||
${[1, 2, 3, 4].map((bar) => html`
|
||||
<div class="signal-bar ${bar <= signalBars ? 'active' : ''}"></div>
|
||||
`)}
|
||||
</div>
|
||||
<div class="network-info">
|
||||
<div class="network-name">${network.ssid}</div>
|
||||
${isConnected ? html`<div class="network-status">Connected</div>` : ''}
|
||||
</div>
|
||||
${network.secured ? html`
|
||||
<dees-icon class="network-secured" .icon=${'lucide:lock'} .iconSize=${14}></dees-icon>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderDisabledMessage(): TemplateResult {
|
||||
return html`
|
||||
<div class="disabled-message">
|
||||
Wi-Fi is turned off
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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<void> {
|
||||
await super.connectedCallback();
|
||||
// Delay to prevent immediate close when clicking to open
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.boundHandleClickOutside);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
async disconnectedCallback(): Promise<void> {
|
||||
await super.disconnectedCallback();
|
||||
document.removeEventListener('click', this.boundHandleClickOutside);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './eco-applauncher-wifimenu.js';
|
||||
@@ -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`
|
||||
<style>
|
||||
.demo-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<div class="demo-container">
|
||||
<eco-applauncher
|
||||
.apps=${mockApps}
|
||||
.batteryLevel=${85}
|
||||
.networkStatus=${'online'}
|
||||
.soundLevel=${70}
|
||||
.networks=${mockNetworks}
|
||||
.connectedNetwork=${'HomeNetwork'}
|
||||
.wifiEnabled=${true}
|
||||
.timeRemaining=${'2h 30m remaining'}
|
||||
.outputDevices=${mockAudioDevices}
|
||||
.activeDeviceId=${'speakers'}
|
||||
.muted=${false}
|
||||
.userName=${'John Doe'}
|
||||
.notificationCount=${3}
|
||||
@wifi-toggle=${(e: CustomEvent) => 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')}
|
||||
></eco-applauncher>
|
||||
</div>
|
||||
`;
|
||||
@@ -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<typeof setInterval> | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.updateTime();
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="launcher-container">
|
||||
${this.renderTopBar()}
|
||||
<div class="apps-area">
|
||||
<div class="apps-grid">
|
||||
${this.apps.map((app) => this.renderAppIcon(app))}
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-bar">
|
||||
<div class="status-left">
|
||||
${this.statusConfig.showNetwork ? this.renderNetworkStatusWithMenu() : ''}
|
||||
${this.statusConfig.showBattery ? this.renderBatteryStatusWithMenu() : ''}
|
||||
${this.statusConfig.showSound ? this.renderSoundStatusWithMenu() : ''}
|
||||
</div>
|
||||
<div class="status-right">
|
||||
${this.statusConfig.showTime ? html`
|
||||
<span class="status-time">${this.currentTime}</span>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderAppIcon(app: IAppIcon): TemplateResult {
|
||||
return html`
|
||||
<div class="app-icon" @click=${() => this.handleAppClick(app)}>
|
||||
<div class="app-icon-circle">
|
||||
<dees-icon .icon=${app.icon} .iconSize=${28}></dees-icon>
|
||||
</div>
|
||||
<span class="app-icon-name">${app.name}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderTopBar(): TemplateResult {
|
||||
const userInitials = this.userName
|
||||
.split(' ')
|
||||
.map((n) => n[0])
|
||||
.join('')
|
||||
.slice(0, 2)
|
||||
.toUpperCase();
|
||||
|
||||
return html`
|
||||
<div class="top-bar">
|
||||
<div class="top-left">
|
||||
${this.topBarConfig.showDate ? html`
|
||||
<span class="top-date">${this.currentDate}</span>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="top-center">
|
||||
${this.topBarConfig.showSearch ? html`
|
||||
<div class="search-box" @click=${this.handleSearchClick}>
|
||||
<dees-icon .icon=${'lucide:search'} .iconSize=${16}></dees-icon>
|
||||
<span class="search-text">Search apps...</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="top-right">
|
||||
${this.topBarConfig.showNotifications ? html`
|
||||
<div
|
||||
class="top-icon-button notification-badge"
|
||||
@click=${this.handleNotificationsClick}
|
||||
>
|
||||
<dees-icon .icon=${'lucide:bell'} .iconSize=${18}></dees-icon>
|
||||
${this.notificationCount > 0 ? html`
|
||||
<span class="badge">${this.notificationCount > 99 ? '99+' : this.notificationCount}</span>
|
||||
` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
${this.topBarConfig.showUser ? html`
|
||||
<div class="user-avatar" @click=${this.handleUserClick}>
|
||||
${userInitials}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderNetworkStatusWithMenu(): TemplateResult {
|
||||
const bars = this.getNetworkBars();
|
||||
return html`
|
||||
<div class="status-item-wrapper">
|
||||
<div
|
||||
class="status-item clickable ${this.wifiMenuOpen ? 'active' : ''}"
|
||||
@click=${this.handleNetworkClick}
|
||||
>
|
||||
<div class="network-indicator">
|
||||
${[1, 2, 3, 4].map((bar) => html`
|
||||
<div class="network-bar ${bar <= bars ? 'active' : ''}"></div>
|
||||
`)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-popup">
|
||||
<eco-applauncher-wifimenu
|
||||
?open=${this.wifiMenuOpen}
|
||||
.networks=${this.networks}
|
||||
.connectedNetwork=${this.connectedNetwork}
|
||||
.wifiEnabled=${this.wifiEnabled}
|
||||
@menu-close=${this.handleWifiMenuClose}
|
||||
@wifi-toggle=${this.handleWifiToggle}
|
||||
@network-select=${this.handleNetworkSelect}
|
||||
@settings-click=${this.handleWifiSettingsClick}
|
||||
></eco-applauncher-wifimenu>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderNetworkStatus(): TemplateResult {
|
||||
const bars = this.getNetworkBars();
|
||||
return html`
|
||||
<div class="status-item">
|
||||
<div class="network-indicator">
|
||||
${[1, 2, 3, 4].map((bar) => html`
|
||||
<div class="network-bar ${bar <= bars ? 'active' : ''}"></div>
|
||||
`)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderBatteryStatusWithMenu(): TemplateResult {
|
||||
const level = typeof this.batteryLevel === 'number' ? this.batteryLevel : 100;
|
||||
const isCharging = this.batteryLevel === 'charging' || this.isCharging;
|
||||
const isLow = level < 20;
|
||||
|
||||
return html`
|
||||
<div class="status-item-wrapper">
|
||||
<div
|
||||
class="status-item battery-indicator clickable ${this.batteryMenuOpen ? 'active' : ''}"
|
||||
@click=${this.handleBatteryClick}
|
||||
>
|
||||
<div class="battery-bar">
|
||||
<div
|
||||
class="battery-fill ${isLow ? 'low' : ''} ${isCharging ? 'charging' : ''}"
|
||||
style="width: ${level}%"
|
||||
></div>
|
||||
</div>
|
||||
${isCharging
|
||||
? html`<dees-icon .icon=${'lucide:zap'} .iconSize=${14}></dees-icon>`
|
||||
: html`<span>${level}%</span>`
|
||||
}
|
||||
</div>
|
||||
<div class="menu-popup">
|
||||
<eco-applauncher-batterymenu
|
||||
?open=${this.batteryMenuOpen}
|
||||
.batteryLevel=${level}
|
||||
.isCharging=${isCharging}
|
||||
.batterySaverEnabled=${this.batterySaverEnabled}
|
||||
.timeRemaining=${this.timeRemaining}
|
||||
@menu-close=${this.handleBatteryMenuClose}
|
||||
@battery-saver-toggle=${this.handleBatterySaverToggle}
|
||||
@settings-click=${this.handleBatterySettingsClick}
|
||||
></eco-applauncher-batterymenu>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderBatteryStatus(): TemplateResult {
|
||||
const level = typeof this.batteryLevel === 'number' ? this.batteryLevel : 100;
|
||||
const isCharging = this.batteryLevel === 'charging';
|
||||
const isLow = level < 20;
|
||||
|
||||
return html`
|
||||
<div class="status-item battery-indicator">
|
||||
<div class="battery-bar">
|
||||
<div
|
||||
class="battery-fill ${isLow ? 'low' : ''} ${isCharging ? 'charging' : ''}"
|
||||
style="width: ${level}%"
|
||||
></div>
|
||||
</div>
|
||||
${isCharging
|
||||
? html`<dees-icon .icon=${'lucide:zap'} .iconSize=${14}></dees-icon>`
|
||||
: html`<span>${level}%</span>`
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderSoundStatusWithMenu(): TemplateResult {
|
||||
const activeBars = Math.ceil((this.soundLevel / 100) * 3);
|
||||
const soundIcon = this.muted || this.soundLevel === 0 ? 'lucide:volumeX' : 'lucide:volume2';
|
||||
|
||||
return html`
|
||||
<div class="status-item-wrapper">
|
||||
<div
|
||||
class="status-item sound-indicator clickable ${this.soundMenuOpen ? 'active' : ''}"
|
||||
@click=${this.handleSoundClick}
|
||||
>
|
||||
<dees-icon .icon=${soundIcon} .iconSize=${16}></dees-icon>
|
||||
<div class="sound-bars">
|
||||
${[1, 2, 3].map((bar) => html`
|
||||
<div
|
||||
class="sound-bar"
|
||||
style="opacity: ${bar <= activeBars && !this.muted ? 1 : 0.3}"
|
||||
></div>
|
||||
`)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-popup">
|
||||
<eco-applauncher-soundmenu
|
||||
?open=${this.soundMenuOpen}
|
||||
.volume=${this.soundLevel}
|
||||
.muted=${this.muted}
|
||||
.outputDevices=${this.outputDevices}
|
||||
.activeDeviceId=${this.activeDeviceId}
|
||||
@menu-close=${this.handleSoundMenuClose}
|
||||
@volume-change=${this.handleVolumeChange}
|
||||
@mute-toggle=${this.handleMuteToggle}
|
||||
@device-select=${this.handleDeviceSelect}
|
||||
@settings-click=${this.handleSoundSettingsClick}
|
||||
></eco-applauncher-soundmenu>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderSoundStatus(): TemplateResult {
|
||||
const activeBars = Math.ceil((this.soundLevel / 100) * 3);
|
||||
const soundIcon = this.soundLevel === 0 ? 'lucide:volumeX' : 'lucide:volume2';
|
||||
return html`
|
||||
<div class="status-item sound-indicator">
|
||||
<dees-icon .icon=${soundIcon} .iconSize=${16}></dees-icon>
|
||||
<div class="sound-bars">
|
||||
${[1, 2, 3].map((bar) => html`
|
||||
<div
|
||||
class="sound-bar"
|
||||
style="opacity: ${bar <= activeBars ? 1 : 0.3}"
|
||||
></div>
|
||||
`)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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<void> {
|
||||
await super.connectedCallback();
|
||||
this.updateTime();
|
||||
this.timeUpdateInterval = setInterval(() => this.updateTime(), 1000);
|
||||
}
|
||||
|
||||
async disconnectedCallback(): Promise<void> {
|
||||
await super.disconnectedCallback();
|
||||
if (this.timeUpdateInterval) {
|
||||
clearInterval(this.timeUpdateInterval);
|
||||
this.timeUpdateInterval = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './eco-applauncher.js';
|
||||
5
ts_web/elements/00group-applauncher/index.ts
Normal file
@@ -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';
|
||||
@@ -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;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from './dees-screensaver.js';
|
||||
18
ts_web/elements/eco-screensaver/eco-screensaver.demo.ts
Normal file
@@ -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`
|
||||
<eco-screensaver .delay=${2000}></eco-screensaver>
|
||||
<div style="padding: 24px; color: #888;">
|
||||
Screensaver activates after 2 seconds of inactivity.<br>
|
||||
Move mouse or press keys to reset timer.
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
@@ -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`<div style="padding: 24px; color: #888;">Screensaver will activate in 2 seconds...</div>`;
|
||||
};
|
||||
@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<DeesScreensaver> {
|
||||
if (DeesScreensaver.instance) {
|
||||
DeesScreensaver.instance.active = true;
|
||||
return DeesScreensaver.instance;
|
||||
public static async show(): Promise<EcoScreensaver> {
|
||||
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<typeof setTimeout> | null = null;
|
||||
private boundResetDelayTimer: () => void;
|
||||
private boundShowHint: () => void;
|
||||
private hintEl: HTMLElement | null = null;
|
||||
private hintTimeoutId: ReturnType<typeof setTimeout> | 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 {
|
||||
<span class="date" style="color: ${this.currentColor};">${this.currentDate}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hint">Click to exit screensaver</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
await super.disconnectedCallback();
|
||||
this.stopAnimation();
|
||||
this.stopTimeUpdate();
|
||||
this.stopDelayTimer();
|
||||
this.removeActivityListeners();
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string, unknown>): 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();
|
||||
}
|
||||
};
|
||||
|
||||
1
ts_web/elements/eco-screensaver/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './eco-screensaver.js';
|
||||
@@ -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';
|
||||
|
||||