update
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
|
||||
export const demo = () => html`
|
||||
<style>
|
||||
.demo-container {
|
||||
padding: 48px;
|
||||
background: hsl(240 10% 4%);
|
||||
min-height: 300px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
<div class="demo-container">
|
||||
<eco-applauncher-powermenu
|
||||
open
|
||||
@power-action=${(e: CustomEvent) => console.log('Power action:', e.detail.action)}
|
||||
@menu-close=${() => console.log('Menu closed')}
|
||||
></eco-applauncher-powermenu>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,261 @@
|
||||
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-powermenu.demo.js';
|
||||
|
||||
// Ensure dees-icon is registered
|
||||
DeesIcon;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'eco-applauncher-powermenu': EcoApplauncherPowermenu;
|
||||
}
|
||||
}
|
||||
|
||||
export type TPowerAction = 'lock' | 'lock-sleep' | 'reboot';
|
||||
|
||||
@customElement('eco-applauncher-powermenu')
|
||||
export class EcoApplauncherPowermenu extends DeesElement {
|
||||
public static demo = demo;
|
||||
public static demoGroup = 'App Launcher';
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:host([open]) {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.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: 200px;
|
||||
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 {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 92%)', 'hsl(240 5% 15%)')};
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
|
||||
}
|
||||
|
||||
.menu-options {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.menu-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
|
||||
}
|
||||
|
||||
.menu-option:hover {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 96%)', 'hsl(240 5% 15%)')};
|
||||
}
|
||||
|
||||
.menu-option:active {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 92%)', 'hsl(240 5% 18%)')};
|
||||
}
|
||||
|
||||
.menu-option.danger {
|
||||
color: ${cssManager.bdTheme('hsl(0 72% 45%)', 'hsl(0 72% 60%)')};
|
||||
}
|
||||
|
||||
.menu-option.danger:hover {
|
||||
background: ${cssManager.bdTheme('hsl(0 72% 97%)', 'hsl(0 50% 15%)')};
|
||||
}
|
||||
|
||||
.option-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 18%)')};
|
||||
}
|
||||
|
||||
.menu-option.danger .option-icon {
|
||||
background: ${cssManager.bdTheme('hsl(0 72% 94%)', 'hsl(0 50% 18%)')};
|
||||
}
|
||||
|
||||
.option-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.option-description {
|
||||
font-size: 11px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 55%)', 'hsl(0 0% 50%)')};
|
||||
}
|
||||
|
||||
.menu-divider {
|
||||
height: 1px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 92%)', 'hsl(240 5% 15%)')};
|
||||
margin: 4px 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
accessor open = false;
|
||||
|
||||
private boundHandleClickOutside = this.handleClickOutside.bind(this);
|
||||
private inactivityTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
private readonly INACTIVITY_TIMEOUT = 60000; // 1 minute
|
||||
private lastActivityTime = 0;
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="menu-container"
|
||||
@click=${(e: MouseEvent) => e.stopPropagation()}
|
||||
@mousedown=${this.resetInactivityTimer}
|
||||
>
|
||||
<div class="menu-header">Power</div>
|
||||
<div class="menu-options">
|
||||
<div class="menu-option" @click=${() => this.handleAction('lock')}>
|
||||
<div class="option-icon">
|
||||
<dees-icon .icon=${'lucide:lock'} .iconSize=${16}></dees-icon>
|
||||
</div>
|
||||
<div class="option-text">
|
||||
<span class="option-label">Lock</span>
|
||||
<span class="option-description">Lock the screen</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="menu-option" @click=${() => this.handleAction('lock-sleep')}>
|
||||
<div class="option-icon">
|
||||
<dees-icon .icon=${'lucide:moon'} .iconSize=${16}></dees-icon>
|
||||
</div>
|
||||
<div class="option-text">
|
||||
<span class="option-label">Lock + Sleep</span>
|
||||
<span class="option-description">Lock and turn off display</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="menu-divider"></div>
|
||||
|
||||
<div class="menu-option danger" @click=${() => this.handleAction('reboot')}>
|
||||
<div class="option-icon">
|
||||
<dees-icon .icon=${'lucide:refreshCw'} .iconSize=${16}></dees-icon>
|
||||
</div>
|
||||
<div class="option-text">
|
||||
<span class="option-label">Reboot</span>
|
||||
<span class="option-description">Restart the system</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private handleAction(action: TPowerAction): void {
|
||||
this.dispatchEvent(new CustomEvent('power-action', {
|
||||
detail: { action },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
this.closeMenu();
|
||||
}
|
||||
|
||||
private handleClickOutside(e: MouseEvent): void {
|
||||
if (this.open && !this.contains(e.target as Node)) {
|
||||
this.closeMenu();
|
||||
}
|
||||
}
|
||||
|
||||
private resetInactivityTimer(): void {
|
||||
const now = Date.now();
|
||||
// Throttle: only reset if 5+ seconds since last reset
|
||||
if (now - this.lastActivityTime < 5000) {
|
||||
return;
|
||||
}
|
||||
this.lastActivityTime = now;
|
||||
this.clearInactivityTimer();
|
||||
if (this.open) {
|
||||
this.inactivityTimeout = setTimeout(() => {
|
||||
this.closeMenu();
|
||||
}, this.INACTIVITY_TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
private clearInactivityTimer(): void {
|
||||
if (this.inactivityTimeout) {
|
||||
clearTimeout(this.inactivityTimeout);
|
||||
this.inactivityTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
private closeMenu(): void {
|
||||
this.open = false;
|
||||
this.dispatchEvent(new CustomEvent('menu-close', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
|
||||
protected updated(changedProperties: Map<string, unknown>): void {
|
||||
if (changedProperties.has('open')) {
|
||||
if (this.open) {
|
||||
this.resetInactivityTimer();
|
||||
} else {
|
||||
this.clearInactivityTimer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
this.clearInactivityTimer();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './eco-applauncher-powermenu.js';
|
||||
@@ -1,21 +1,18 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
import type { IAppIcon } from './eco-applauncher.js';
|
||||
import type { IAppIcon, ILoginConfig, ILoginCredentials, TApplauncherMode } from './eco-applauncher.js';
|
||||
import type { IWifiNetwork } from '../eco-applauncher-wifimenu/index.js';
|
||||
import type { IAudioDevice } from '../eco-applauncher-soundmenu/index.js';
|
||||
import '../../../views/eco-view-settings/eco-view-settings.js';
|
||||
import '../../../views/eco-view-peripherals/eco-view-peripherals.js';
|
||||
import '../../../views/eco-view-saasshare/eco-view-saasshare.js';
|
||||
import '../../../views/eco-view-system/eco-view-system.js';
|
||||
import type { EcoApplauncher } from './eco-applauncher.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') },
|
||||
{ name: 'SaaS Share', icon: 'lucide:share2', view: html`<eco-view-saasshare></eco-view-saasshare>` },
|
||||
{ name: 'System', icon: 'lucide:activity', view: html`<eco-view-system></eco-view-system>` },
|
||||
{ name: 'Peripherals', icon: 'lucide:monitor', view: html`<eco-view-peripherals></eco-view-peripherals>` },
|
||||
{ name: 'Settings', icon: 'lucide:settings', view: html`<eco-view-settings></eco-view-settings>` },
|
||||
];
|
||||
|
||||
const mockNetworks: IWifiNetwork[] = [
|
||||
@@ -32,7 +29,33 @@ const mockAudioDevices: IAudioDevice[] = [
|
||||
{ id: 'hdmi', name: 'LG Monitor', type: 'hdmi' },
|
||||
];
|
||||
|
||||
export const demo = () => html`
|
||||
const loginConfig: ILoginConfig = {
|
||||
allowedMethods: ['pin', 'password', 'qr'],
|
||||
pinLength: 4,
|
||||
welcomeMessage: 'Welcome to EcoBridge',
|
||||
};
|
||||
|
||||
const handleLoginAttempt = (e: CustomEvent) => {
|
||||
const credentials = e.detail as ILoginCredentials;
|
||||
const applauncher = e.target as EcoApplauncher;
|
||||
|
||||
console.log('Login attempt:', credentials);
|
||||
|
||||
// Demo validation: PIN "1234" or password "demo"
|
||||
if (
|
||||
(credentials.method === 'pin' && credentials.value === '1234') ||
|
||||
(credentials.method === 'password' && credentials.value === 'demo')
|
||||
) {
|
||||
console.log('Login successful!');
|
||||
applauncher.setLoginResult(true);
|
||||
} else {
|
||||
console.log('Login failed');
|
||||
applauncher.setLoginResult(false, 'Invalid credentials. Try PIN: 1234 or Password: demo');
|
||||
}
|
||||
};
|
||||
|
||||
// Home mode demo
|
||||
const demoHome = () => html`
|
||||
<style>
|
||||
.demo-container {
|
||||
width: 100%;
|
||||
@@ -41,6 +64,8 @@ export const demo = () => html`
|
||||
</style>
|
||||
<div class="demo-container">
|
||||
<eco-applauncher
|
||||
.mode=${'home' as TApplauncherMode}
|
||||
.loginConfig=${loginConfig}
|
||||
.apps=${mockApps}
|
||||
.batteryLevel=${85}
|
||||
.networkStatus=${'online'}
|
||||
@@ -54,6 +79,9 @@ export const demo = () => html`
|
||||
.muted=${false}
|
||||
.userName=${'John Doe'}
|
||||
.notificationCount=${3}
|
||||
@login-attempt=${handleLoginAttempt}
|
||||
@login-success=${() => console.log('Login success event received')}
|
||||
@login-failure=${(e: CustomEvent) => console.log('Login failure:', e.detail)}
|
||||
@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')}
|
||||
@@ -69,3 +97,37 @@ export const demo = () => html`
|
||||
></eco-applauncher>
|
||||
</div>
|
||||
`;
|
||||
demoHome.demoTitle = 'Home Mode';
|
||||
|
||||
// Login mode demo
|
||||
const demoLogin = () => html`
|
||||
<style>
|
||||
.demo-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<div class="demo-container">
|
||||
<eco-applauncher
|
||||
.mode=${'login' as TApplauncherMode}
|
||||
.loginConfig=${loginConfig}
|
||||
.apps=${mockApps}
|
||||
.batteryLevel=${85}
|
||||
.networkStatus=${'online'}
|
||||
.soundLevel=${70}
|
||||
.networks=${mockNetworks}
|
||||
.connectedNetwork=${'HomeNetwork'}
|
||||
.wifiEnabled=${true}
|
||||
.outputDevices=${mockAudioDevices}
|
||||
.activeDeviceId=${'speakers'}
|
||||
.muted=${false}
|
||||
@login-attempt=${handleLoginAttempt}
|
||||
@login-success=${() => console.log('Login success event received')}
|
||||
@login-failure=${(e: CustomEvent) => console.log('Login failure:', e.detail)}
|
||||
></eco-applauncher>
|
||||
</div>
|
||||
`;
|
||||
demoLogin.demoTitle = 'Login Mode';
|
||||
|
||||
// Export array of demo functions
|
||||
export const demo = [demoHome, demoLogin];
|
||||
|
||||
@@ -14,6 +14,9 @@ import { EcoApplauncherWifimenu, type IWifiNetwork } from '../eco-applauncher-wi
|
||||
import { EcoApplauncherBatterymenu } from '../eco-applauncher-batterymenu/index.js';
|
||||
import { EcoApplauncherSoundmenu, type IAudioDevice } from '../eco-applauncher-soundmenu/index.js';
|
||||
import { EcoApplauncherKeyboard } from '../eco-applauncher-keyboard/index.js';
|
||||
import { EcoApplauncherPowermenu, type TPowerAction } from '../eco-applauncher-powermenu/index.js';
|
||||
import { EcoViewHome } from '../../../views/eco-view-home/index.js';
|
||||
import { EcoViewLogin, type ILoginConfig, type ILoginCredentials } from '../../../views/eco-view-login/index.js';
|
||||
|
||||
// Ensure components are registered
|
||||
DeesIcon;
|
||||
@@ -21,6 +24,9 @@ EcoApplauncherWifimenu;
|
||||
EcoApplauncherBatterymenu;
|
||||
EcoApplauncherSoundmenu;
|
||||
EcoApplauncherKeyboard;
|
||||
EcoApplauncherPowermenu;
|
||||
EcoViewHome;
|
||||
EcoViewLogin;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@@ -28,12 +34,18 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
export type TApplauncherMode = 'login' | 'home';
|
||||
|
||||
export interface IAppIcon {
|
||||
name: string;
|
||||
icon: string;
|
||||
action?: () => void;
|
||||
view?: TemplateResult;
|
||||
}
|
||||
|
||||
export type { ILoginConfig, ILoginCredentials } from '../../../views/eco-view-login/index.js';
|
||||
export type { TPowerAction } from '../eco-applauncher-powermenu/index.js';
|
||||
|
||||
export interface IStatusBarConfig {
|
||||
showTime?: boolean;
|
||||
showNetwork?: boolean;
|
||||
@@ -153,6 +165,10 @@ export class EcoApplauncher extends DeesElement {
|
||||
background: ${cssManager.bdTheme('hsl(220 15% 88%)', 'hsl(240 5% 15%)')};
|
||||
}
|
||||
|
||||
.top-icon-button.active {
|
||||
background: ${cssManager.bdTheme('hsl(220 15% 85%)', 'hsl(240 5% 18%)')};
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
@@ -195,9 +211,6 @@ export class EcoApplauncher extends DeesElement {
|
||||
|
||||
.apps-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -206,7 +219,6 @@ export class EcoApplauncher extends DeesElement {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||
gap: 32px;
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -415,6 +427,18 @@ export class EcoApplauncher extends DeesElement {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.topbar-menu-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.topbar-menu-popup {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.keyboard-area {
|
||||
flex-shrink: 0;
|
||||
height: 0;
|
||||
@@ -430,6 +454,53 @@ export class EcoApplauncher extends DeesElement {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.view-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.view-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 24px;
|
||||
background: ${cssManager.bdTheme('hsl(220 15% 94%)', 'hsl(240 6% 8%)')};
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('hsl(220 15% 88%)', 'hsl(240 5% 15%)')};
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 8px 14px;
|
||||
background: ${cssManager.bdTheme('hsl(220 15% 88%)', 'hsl(240 5% 14%)')};
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')};
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background: ${cssManager.bdTheme('hsl(220 15% 82%)', 'hsl(240 5% 18%)')};
|
||||
}
|
||||
|
||||
.view-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
|
||||
}
|
||||
|
||||
.view-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.apps-area {
|
||||
padding: 24px;
|
||||
@@ -462,6 +533,16 @@ export class EcoApplauncher extends DeesElement {
|
||||
`,
|
||||
];
|
||||
|
||||
@property({ type: String })
|
||||
accessor mode: TApplauncherMode = 'home';
|
||||
|
||||
@property({ type: Object })
|
||||
accessor loginConfig: ILoginConfig = {
|
||||
allowedMethods: ['pin', 'password', 'qr'],
|
||||
pinLength: 4,
|
||||
welcomeMessage: 'Welcome',
|
||||
};
|
||||
|
||||
@property({ type: Array })
|
||||
accessor apps: IAppIcon[] = [];
|
||||
|
||||
@@ -512,9 +593,18 @@ export class EcoApplauncher extends DeesElement {
|
||||
@state()
|
||||
accessor soundMenuOpen = false;
|
||||
|
||||
@state()
|
||||
accessor powerMenuOpen = false;
|
||||
|
||||
@state()
|
||||
accessor keyboardVisible = false;
|
||||
|
||||
@state()
|
||||
accessor activeView: TemplateResult | null = null;
|
||||
|
||||
@state()
|
||||
accessor activeViewName: string | null = null;
|
||||
|
||||
@property({ type: Array })
|
||||
accessor networks: IWifiNetwork[] = [];
|
||||
|
||||
@@ -552,12 +642,8 @@ export class EcoApplauncher extends DeesElement {
|
||||
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>
|
||||
${this.mode === 'login' ? '' : this.renderTopBar()}
|
||||
${this.renderMainContent()}
|
||||
<div class="keyboard-area ${this.keyboardVisible ? 'visible' : ''}">
|
||||
<eco-applauncher-keyboard
|
||||
?visible=${this.keyboardVisible}
|
||||
@@ -585,6 +671,58 @@ export class EcoApplauncher extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderMainContent(): TemplateResult {
|
||||
if (this.mode === 'login') {
|
||||
return this.renderLoginView();
|
||||
}
|
||||
// Home mode
|
||||
if (this.activeView) {
|
||||
return this.renderActiveView();
|
||||
}
|
||||
return this.renderHomeView();
|
||||
}
|
||||
|
||||
private renderLoginView(): TemplateResult {
|
||||
return html`
|
||||
<eco-view-login
|
||||
.config=${this.loginConfig}
|
||||
@login-attempt=${this.handleLoginAttempt}
|
||||
@key-press=${this.handleKeyboardKeyPress}
|
||||
@backspace=${this.handleKeyboardBackspace}
|
||||
style="flex: 1;"
|
||||
></eco-view-login>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderHomeView(): TemplateResult {
|
||||
return html`
|
||||
<eco-view-home
|
||||
.apps=${this.apps}
|
||||
@app-click=${this.handleHomeAppClick}
|
||||
></eco-view-home>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderAppsArea(): TemplateResult {
|
||||
return html`
|
||||
<div class="apps-area">
|
||||
<div class="apps-grid">
|
||||
${this.apps.map((app) => this.renderAppIcon(app))}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderActiveView(): TemplateResult {
|
||||
return html`
|
||||
<div class="view-area">
|
||||
<div class="view-content">
|
||||
${this.activeView}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderAppIcon(app: IAppIcon): TemplateResult {
|
||||
return html`
|
||||
<div class="app-icon" @click=${() => this.handleAppClick(app)}>
|
||||
@@ -607,7 +745,13 @@ export class EcoApplauncher extends DeesElement {
|
||||
return html`
|
||||
<div class="top-bar">
|
||||
<div class="top-left">
|
||||
${this.topBarConfig.showDate ? html`
|
||||
${this.activeView ? html`
|
||||
<div class="back-button" @click=${this.handleBackClick}>
|
||||
<dees-icon .icon=${'lucide:arrowLeft'} .iconSize=${16}></dees-icon>
|
||||
<span>Back</span>
|
||||
</div>
|
||||
<span class="view-title">${this.activeViewName}</span>
|
||||
` : this.topBarConfig.showDate ? html`
|
||||
<span class="top-date">${this.currentDate}</span>
|
||||
` : ''}
|
||||
</div>
|
||||
@@ -636,6 +780,21 @@ export class EcoApplauncher extends DeesElement {
|
||||
${userInitials}
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="topbar-menu-wrapper">
|
||||
<div
|
||||
class="top-icon-button ${this.powerMenuOpen ? 'active' : ''}"
|
||||
@click=${this.handlePowerClick}
|
||||
>
|
||||
<dees-icon .icon=${'lucide:power'} .iconSize=${18}></dees-icon>
|
||||
</div>
|
||||
<div class="topbar-menu-popup">
|
||||
<eco-applauncher-powermenu
|
||||
?open=${this.powerMenuOpen}
|
||||
@menu-close=${this.handlePowerMenuClose}
|
||||
@power-action=${this.handlePowerAction}
|
||||
></eco-applauncher-powermenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -831,15 +990,91 @@ export class EcoApplauncher extends DeesElement {
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
|
||||
// If app has a view, open it inside the applauncher
|
||||
if (app.view) {
|
||||
this.activeView = app.view;
|
||||
this.activeViewName = app.name;
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise execute the action
|
||||
if (app.action) {
|
||||
app.action();
|
||||
}
|
||||
}
|
||||
|
||||
private handleHomeAppClick(e: CustomEvent): void {
|
||||
const app = e.detail.app as IAppIcon;
|
||||
this.handleAppClick(app);
|
||||
}
|
||||
|
||||
private handleLoginAttempt(e: CustomEvent): void {
|
||||
const credentials = e.detail as ILoginCredentials;
|
||||
// Dispatch event for parent to handle validation
|
||||
this.dispatchEvent(new CustomEvent('login-attempt', {
|
||||
detail: credentials,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the login result after validation
|
||||
* @param success Whether login was successful
|
||||
* @param errorMessage Optional error message to display
|
||||
*/
|
||||
public setLoginResult(success: boolean, errorMessage?: string): void {
|
||||
const loginView = this.shadowRoot?.querySelector('eco-view-login') as EcoViewLogin | null;
|
||||
|
||||
if (success) {
|
||||
this.mode = 'home';
|
||||
this.dispatchEvent(new CustomEvent('login-success', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
} else {
|
||||
if (loginView && errorMessage) {
|
||||
loginView.showErrorMessage(errorMessage);
|
||||
}
|
||||
this.dispatchEvent(new CustomEvent('login-failure', {
|
||||
detail: { error: errorMessage },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to login mode
|
||||
*/
|
||||
public showLogin(): void {
|
||||
this.mode = 'login';
|
||||
this.activeView = null;
|
||||
this.activeViewName = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to home mode
|
||||
*/
|
||||
public showHome(): void {
|
||||
this.mode = 'home';
|
||||
}
|
||||
|
||||
private handleBackClick(): void {
|
||||
this.activeView = null;
|
||||
this.activeViewName = null;
|
||||
this.dispatchEvent(new CustomEvent('view-close', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
|
||||
private handleNetworkClick(e: MouseEvent): void {
|
||||
e.stopPropagation();
|
||||
this.batteryMenuOpen = false;
|
||||
this.soundMenuOpen = false;
|
||||
this.powerMenuOpen = false;
|
||||
this.wifiMenuOpen = !this.wifiMenuOpen;
|
||||
}
|
||||
|
||||
@@ -847,6 +1082,7 @@ export class EcoApplauncher extends DeesElement {
|
||||
e.stopPropagation();
|
||||
this.wifiMenuOpen = false;
|
||||
this.soundMenuOpen = false;
|
||||
this.powerMenuOpen = false;
|
||||
this.batteryMenuOpen = !this.batteryMenuOpen;
|
||||
}
|
||||
|
||||
@@ -854,9 +1090,18 @@ export class EcoApplauncher extends DeesElement {
|
||||
e.stopPropagation();
|
||||
this.wifiMenuOpen = false;
|
||||
this.batteryMenuOpen = false;
|
||||
this.powerMenuOpen = false;
|
||||
this.soundMenuOpen = !this.soundMenuOpen;
|
||||
}
|
||||
|
||||
private handlePowerClick(e: MouseEvent): void {
|
||||
e.stopPropagation();
|
||||
this.wifiMenuOpen = false;
|
||||
this.batteryMenuOpen = false;
|
||||
this.soundMenuOpen = false;
|
||||
this.powerMenuOpen = !this.powerMenuOpen;
|
||||
}
|
||||
|
||||
private handleWifiMenuClose(): void {
|
||||
this.wifiMenuOpen = false;
|
||||
}
|
||||
@@ -869,6 +1114,19 @@ export class EcoApplauncher extends DeesElement {
|
||||
this.soundMenuOpen = false;
|
||||
}
|
||||
|
||||
private handlePowerMenuClose(): void {
|
||||
this.powerMenuOpen = false;
|
||||
}
|
||||
|
||||
private handlePowerAction(e: CustomEvent): void {
|
||||
const action = e.detail.action as TPowerAction;
|
||||
this.dispatchEvent(new CustomEvent('power-action', {
|
||||
detail: { action },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
|
||||
private handleVolumeChange(e: CustomEvent): void {
|
||||
this.soundLevel = e.detail.volume;
|
||||
this.dispatchEvent(new CustomEvent('volume-change', {
|
||||
@@ -953,6 +1211,7 @@ export class EcoApplauncher extends DeesElement {
|
||||
this.wifiMenuOpen = false;
|
||||
this.batteryMenuOpen = false;
|
||||
this.soundMenuOpen = false;
|
||||
this.powerMenuOpen = false;
|
||||
}
|
||||
this.dispatchEvent(new CustomEvent('keyboard-toggle', {
|
||||
detail: { visible: this.keyboardVisible },
|
||||
|
||||
@@ -4,5 +4,4 @@ export * from './eco-applauncher-wifimenu/index.js';
|
||||
export * from './eco-applauncher-batterymenu/index.js';
|
||||
export * from './eco-applauncher-soundmenu/index.js';
|
||||
export * from './eco-applauncher-keyboard/index.js';
|
||||
export * from './eco-settings/index.js';
|
||||
export * from './eco-peripherals/index.js';
|
||||
export * from './eco-applauncher-powermenu/index.js';
|
||||
|
||||
31
ts_web/views/eco-view-home/eco-view-home.demo.ts
Normal file
31
ts_web/views/eco-view-home/eco-view-home.demo.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
import type { IAppIcon } from './eco-view-home.js';
|
||||
|
||||
const mockApps: IAppIcon[] = [
|
||||
{ name: 'SaaS Share', icon: 'lucide:share2' },
|
||||
{ name: 'System', icon: 'lucide:activity' },
|
||||
{ name: 'Peripherals', icon: 'lucide:monitor' },
|
||||
{ name: 'Settings', icon: 'lucide:settings' },
|
||||
{ name: 'Files', icon: 'lucide:folder' },
|
||||
{ name: 'Terminal', icon: 'lucide:terminal' },
|
||||
{ name: 'Browser', icon: 'lucide:globe' },
|
||||
{ name: 'Camera', icon: 'lucide:camera' },
|
||||
];
|
||||
|
||||
export const demo = () => html`
|
||||
<style>
|
||||
.demo-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: hsl(240 10% 4%);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
<div class="demo-container">
|
||||
<eco-view-home
|
||||
.apps=${mockApps}
|
||||
@app-click=${(e: CustomEvent) => console.log('App clicked:', e.detail.app)}
|
||||
></eco-view-home>
|
||||
</div>
|
||||
`;
|
||||
157
ts_web/views/eco-view-home/eco-view-home.ts
Normal file
157
ts_web/views/eco-view-home/eco-view-home.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import {
|
||||
customElement,
|
||||
DeesElement,
|
||||
type TemplateResult,
|
||||
html,
|
||||
property,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
import { DeesIcon } from '@design.estate/dees-catalog';
|
||||
|
||||
// Ensure icon component is registered
|
||||
DeesIcon;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'eco-view-home': EcoViewHome;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IAppIcon {
|
||||
name: string;
|
||||
icon: string;
|
||||
action?: () => void;
|
||||
view?: TemplateResult;
|
||||
}
|
||||
|
||||
@customElement('eco-view-home')
|
||||
export class EcoViewHome extends DeesElement {
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.apps-area {
|
||||
padding: 48px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.apps-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||
gap: 32px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, transform 0.15s ease;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.app-icon:hover {
|
||||
background: ${cssManager.bdTheme('hsl(220 15% 92%)', 'hsl(240 5% 12%)')};
|
||||
}
|
||||
|
||||
.app-icon:active {
|
||||
transform: scale(0.95);
|
||||
background: ${cssManager.bdTheme('hsl(220 15% 88%)', 'hsl(240 5% 16%)')};
|
||||
}
|
||||
|
||||
.app-icon-circle {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 16px;
|
||||
background: ${cssManager.bdTheme('hsl(220 15% 90%)', 'hsl(240 5% 15%)')};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 80%)')};
|
||||
}
|
||||
|
||||
.app-icon-circle dees-icon {
|
||||
--dees-icon-size: 28px;
|
||||
}
|
||||
|
||||
.app-icon-name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 25%)', 'hsl(0 0% 85%)')};
|
||||
text-align: center;
|
||||
max-width: 90px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@property({ type: Array })
|
||||
accessor apps: IAppIcon[] = [];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="apps-area">
|
||||
<div class="apps-grid">
|
||||
${this.apps.map((app) => this.renderAppIcon(app))}
|
||||
</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 handleAppClick(app: IAppIcon): void {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('app-click', {
|
||||
detail: { app },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
1
ts_web/views/eco-view-home/index.ts
Normal file
1
ts_web/views/eco-view-home/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './eco-view-home.js';
|
||||
48
ts_web/views/eco-view-login/eco-view-login.demo.ts
Normal file
48
ts_web/views/eco-view-login/eco-view-login.demo.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
import type { ILoginConfig, ILoginCredentials } from './eco-view-login.js';
|
||||
|
||||
const handleLoginAttempt = (e: CustomEvent<ILoginCredentials>) => {
|
||||
const { method, value } = e.detail;
|
||||
console.log(`Login attempt via ${method}:`, value);
|
||||
|
||||
// Demo: Show success for PIN "1234" or password "demo"
|
||||
const loginView = e.target as HTMLElement & { showErrorMessage: (msg: string) => void; clearInput: () => void };
|
||||
|
||||
if ((method === 'pin' && value === '1234') || (method === 'password' && value === 'demo')) {
|
||||
console.log('Login successful!');
|
||||
alert('Login successful! (Demo)');
|
||||
loginView.clearInput();
|
||||
} else {
|
||||
loginView.showErrorMessage('Invalid credentials. Try PIN: 1234 or Password: demo');
|
||||
}
|
||||
};
|
||||
|
||||
const pinOnlyConfig: ILoginConfig = {
|
||||
allowedMethods: ['pin'],
|
||||
pinLength: 4,
|
||||
welcomeMessage: 'Enter PIN',
|
||||
};
|
||||
|
||||
const allMethodsConfig: ILoginConfig = {
|
||||
allowedMethods: ['pin', 'password', 'qr'],
|
||||
pinLength: 6,
|
||||
welcomeMessage: 'Sign In',
|
||||
};
|
||||
|
||||
export const demo = () => html`
|
||||
<style>
|
||||
.demo-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: hsl(240 10% 4%);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
<div class="demo-container">
|
||||
<eco-view-login
|
||||
.config=${allMethodsConfig}
|
||||
@login-attempt=${handleLoginAttempt}
|
||||
></eco-view-login>
|
||||
</div>
|
||||
`;
|
||||
749
ts_web/views/eco-view-login/eco-view-login.ts
Normal file
749
ts_web/views/eco-view-login/eco-view-login.ts
Normal file
@@ -0,0 +1,749 @@
|
||||
import {
|
||||
customElement,
|
||||
DeesElement,
|
||||
type TemplateResult,
|
||||
html,
|
||||
property,
|
||||
css,
|
||||
cssManager,
|
||||
state,
|
||||
} from '@design.estate/dees-element';
|
||||
import { DeesIcon } from '@design.estate/dees-catalog';
|
||||
|
||||
// Ensure icon component is registered
|
||||
DeesIcon;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'eco-view-login': EcoViewLogin;
|
||||
}
|
||||
}
|
||||
|
||||
export type TAuthMethod = 'pin' | 'password' | 'qr';
|
||||
|
||||
export interface ILoginConfig {
|
||||
allowedMethods: TAuthMethod[];
|
||||
pinLength?: number;
|
||||
qrCodeData?: string;
|
||||
logoUrl?: string;
|
||||
welcomeMessage?: string;
|
||||
subtitle?: string;
|
||||
}
|
||||
|
||||
export interface ILoginCredentials {
|
||||
method: TAuthMethod;
|
||||
value: string;
|
||||
}
|
||||
|
||||
@customElement('eco-view-login')
|
||||
export class EcoViewLogin extends DeesElement {
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Left Panel - Branding & Method Selection */
|
||||
.left-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 64px;
|
||||
background: ${cssManager.bdTheme('hsl(220 15% 96%)', 'hsl(240 6% 10%)')};
|
||||
border-right: 1px solid ${cssManager.bdTheme('hsl(220 15% 90%)', 'hsl(240 5% 16%)')};
|
||||
}
|
||||
|
||||
.branding {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 18px;
|
||||
background: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(217 91% 50%)')};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.welcome-message {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 95%)')};
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 16px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 45%)', 'hsl(0 0% 55%)')};
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.method-selector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.method-selector-label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 60%)')};
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.method-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px 20px;
|
||||
background: ${cssManager.bdTheme('white', 'hsl(240 5% 14%)')};
|
||||
border: 2px solid ${cssManager.bdTheme('hsl(220 15% 90%)', 'hsl(240 5% 20%)')};
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.method-option:hover {
|
||||
border-color: ${cssManager.bdTheme('hsl(220 15% 80%)', 'hsl(240 5% 28%)')};
|
||||
background: ${cssManager.bdTheme('hsl(220 15% 98%)', 'hsl(240 5% 16%)')};
|
||||
}
|
||||
|
||||
.method-option.active {
|
||||
border-color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(217 91% 50%)')};
|
||||
background: ${cssManager.bdTheme('hsl(217 91% 97%)', 'hsl(217 91% 15%)')};
|
||||
}
|
||||
|
||||
.method-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 10px;
|
||||
background: ${cssManager.bdTheme('hsl(220 15% 94%)', 'hsl(240 5% 20%)')};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')};
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.method-option.active .method-icon {
|
||||
background: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(217 91% 50%)')};
|
||||
color: white;
|
||||
}
|
||||
|
||||
.method-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.method-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.method-description {
|
||||
font-size: 13px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')};
|
||||
}
|
||||
|
||||
.method-check {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid ${cssManager.bdTheme('hsl(220 15% 85%)', 'hsl(240 5% 25%)')};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.method-option.active .method-check {
|
||||
background: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(217 91% 50%)')};
|
||||
border-color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(217 91% 50%)')};
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Right Panel - Auth Input */
|
||||
.right-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 64px;
|
||||
background: ${cssManager.bdTheme('white', 'hsl(240 6% 6%)')};
|
||||
}
|
||||
|
||||
.auth-content {
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Error message */
|
||||
.error-message {
|
||||
color: hsl(0 72% 51%);
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
padding: 12px 16px;
|
||||
background: hsla(0, 72%, 51%, 0.1);
|
||||
border-radius: 8px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* PIN Input */
|
||||
.pin-display {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.pin-dot {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: ${cssManager.bdTheme('hsl(220 15% 85%)', 'hsl(240 5% 20%)')};
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.pin-dot.filled {
|
||||
background: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(217 91% 50%)')};
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
.pin-dot.error {
|
||||
background: hsl(0 72% 51%);
|
||||
animation: shake 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-4px); }
|
||||
75% { transform: translateX(4px); }
|
||||
}
|
||||
|
||||
.numpad {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.numpad-button {
|
||||
width: 76px;
|
||||
height: 76px;
|
||||
border-radius: 50%;
|
||||
background: ${cssManager.bdTheme('hsl(220 15% 95%)', 'hsl(240 5% 14%)')};
|
||||
border: none;
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 85%)')};
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.numpad-button:hover {
|
||||
background: ${cssManager.bdTheme('hsl(220 15% 90%)', 'hsl(240 5% 20%)')};
|
||||
}
|
||||
|
||||
.numpad-button:active {
|
||||
transform: scale(0.95);
|
||||
background: ${cssManager.bdTheme('hsl(220 15% 85%)', 'hsl(240 5% 24%)')};
|
||||
}
|
||||
|
||||
.numpad-button.action {
|
||||
background: transparent;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.numpad-button.action:hover {
|
||||
background: ${cssManager.bdTheme('hsl(220 15% 95%)', 'hsl(240 5% 14%)')};
|
||||
}
|
||||
|
||||
.numpad-button.submit {
|
||||
background: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(217 91% 50%)')};
|
||||
color: white;
|
||||
}
|
||||
|
||||
.numpad-button.submit:hover {
|
||||
background: ${cssManager.bdTheme('hsl(217 91% 55%)', 'hsl(217 91% 45%)')};
|
||||
}
|
||||
|
||||
/* Password Input */
|
||||
.password-form {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.password-input-wrapper {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.password-input {
|
||||
width: 100%;
|
||||
padding: 18px 52px 18px 18px;
|
||||
font-size: 16px;
|
||||
background: ${cssManager.bdTheme('hsl(220 15% 96%)', 'hsl(240 5% 12%)')};
|
||||
border: 2px solid ${cssManager.bdTheme('hsl(220 15% 88%)', 'hsl(240 5% 20%)')};
|
||||
border-radius: 12px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.password-input:focus {
|
||||
border-color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(217 91% 50%)')};
|
||||
}
|
||||
|
||||
.password-input.error {
|
||||
border-color: hsl(0 72% 51%);
|
||||
animation: shake 0.3s ease;
|
||||
}
|
||||
|
||||
.password-toggle {
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')};
|
||||
padding: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.password-toggle:hover {
|
||||
background: ${cssManager.bdTheme('hsl(220 15% 90%)', 'hsl(240 5% 18%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')};
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
width: 100%;
|
||||
padding: 18px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
background: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(217 91% 50%)')};
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.submit-button:hover {
|
||||
background: ${cssManager.bdTheme('hsl(217 91% 55%)', 'hsl(217 91% 45%)')};
|
||||
}
|
||||
|
||||
.submit-button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* QR Code */
|
||||
.qr-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
width: 220px;
|
||||
height: 220px;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.qr-code img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.qr-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: repeating-linear-gradient(
|
||||
45deg,
|
||||
hsl(0 0% 92%),
|
||||
hsl(0 0% 92%) 10px,
|
||||
hsl(0 0% 88%) 10px,
|
||||
hsl(0 0% 88%) 20px
|
||||
);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: hsl(0 0% 50%);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.qr-instruction {
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')};
|
||||
text-align: center;
|
||||
line-height: 1.6;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 800px) {
|
||||
.login-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
padding: 32px;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('hsl(220 15% 90%)', 'hsl(240 5% 16%)')};
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.branding {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.welcome-message {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.method-selector {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.method-option {
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.method-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.method-check {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@property({ type: Object })
|
||||
accessor config: ILoginConfig = {
|
||||
allowedMethods: ['pin', 'password', 'qr'],
|
||||
pinLength: 4,
|
||||
welcomeMessage: 'Welcome',
|
||||
subtitle: 'Sign in to continue',
|
||||
};
|
||||
|
||||
@state()
|
||||
accessor selectedMethod: TAuthMethod = 'pin';
|
||||
|
||||
@state()
|
||||
accessor pinValue = '';
|
||||
|
||||
@state()
|
||||
accessor passwordValue = '';
|
||||
|
||||
@state()
|
||||
accessor showPassword = false;
|
||||
|
||||
@state()
|
||||
accessor error = '';
|
||||
|
||||
@state()
|
||||
accessor showError = false;
|
||||
|
||||
public render(): TemplateResult {
|
||||
const effectivePinLength = this.config.pinLength || 4;
|
||||
|
||||
return html`
|
||||
<div class="login-container">
|
||||
<div class="left-panel">
|
||||
<div class="branding">
|
||||
${this.config.logoUrl
|
||||
? html`<div class="logo"><img src=${this.config.logoUrl} alt="Logo" /></div>`
|
||||
: html`<div class="logo"><dees-icon .icon=${'lucide:shield'} .iconSize=${32}></dees-icon></div>`
|
||||
}
|
||||
<h1 class="welcome-message">${this.config.welcomeMessage || 'Welcome'}</h1>
|
||||
<p class="subtitle">${this.config.subtitle || 'Sign in to continue'}</p>
|
||||
</div>
|
||||
|
||||
${this.config.allowedMethods.length > 1 ? this.renderMethodSelector() : ''}
|
||||
</div>
|
||||
|
||||
<div class="right-panel">
|
||||
<div class="auth-content">
|
||||
<h2 class="auth-title">${this.getAuthTitle()}</h2>
|
||||
${this.showError ? html`<div class="error-message">${this.error}</div>` : ''}
|
||||
${this.selectedMethod === 'pin' ? this.renderPinInput(effectivePinLength) : ''}
|
||||
${this.selectedMethod === 'password' ? this.renderPasswordInput() : ''}
|
||||
${this.selectedMethod === 'qr' ? this.renderQrCode() : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private getAuthTitle(): string {
|
||||
switch (this.selectedMethod) {
|
||||
case 'pin':
|
||||
return 'Enter your PIN';
|
||||
case 'password':
|
||||
return 'Enter your password';
|
||||
case 'qr':
|
||||
return 'Scan to sign in';
|
||||
default:
|
||||
return 'Sign in';
|
||||
}
|
||||
}
|
||||
|
||||
private renderMethodSelector(): TemplateResult {
|
||||
const methods: Array<{ id: TAuthMethod; icon: string; name: string; description: string }> = [
|
||||
{ id: 'pin', icon: 'lucide:keySquare', name: 'PIN Code', description: 'Quick numeric access' },
|
||||
{ id: 'password', icon: 'lucide:key', name: 'Password', description: 'Traditional password' },
|
||||
{ id: 'qr', icon: 'lucide:qrCode', name: 'QR Code', description: 'Scan with mobile app' },
|
||||
];
|
||||
|
||||
const availableMethods = methods.filter((m) => this.config.allowedMethods.includes(m.id));
|
||||
|
||||
return html`
|
||||
<div class="method-selector">
|
||||
<span class="method-selector-label">Sign in method</span>
|
||||
${availableMethods.map((method) => html`
|
||||
<div
|
||||
class="method-option ${this.selectedMethod === method.id ? 'active' : ''}"
|
||||
@click=${() => this.selectMethod(method.id)}
|
||||
>
|
||||
<div class="method-icon">
|
||||
<dees-icon .icon=${method.icon} .iconSize=${22}></dees-icon>
|
||||
</div>
|
||||
<div class="method-info">
|
||||
<div class="method-name">${method.name}</div>
|
||||
<div class="method-description">${method.description}</div>
|
||||
</div>
|
||||
<div class="method-check">
|
||||
${this.selectedMethod === method.id
|
||||
? html`<dees-icon .icon=${'lucide:check'} .iconSize=${14}></dees-icon>`
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderPinInput(length: number): TemplateResult {
|
||||
return html`
|
||||
<div class="pin-display">
|
||||
${Array.from({ length }, (_, i) => html`
|
||||
<div class="pin-dot ${i < this.pinValue.length ? 'filled' : ''} ${this.showError ? 'error' : ''}"></div>
|
||||
`)}
|
||||
</div>
|
||||
|
||||
<div class="numpad">
|
||||
${[1, 2, 3, 4, 5, 6, 7, 8, 9].map((num) => html`
|
||||
<button class="numpad-button" @click=${() => this.handlePinInput(String(num))}>${num}</button>
|
||||
`)}
|
||||
<button class="numpad-button action" @click=${this.handleBackspace}>
|
||||
<dees-icon .icon=${'lucide:delete'} .iconSize=${24}></dees-icon>
|
||||
</button>
|
||||
<button class="numpad-button" @click=${() => this.handlePinInput('0')}>0</button>
|
||||
<button class="numpad-button action submit" @click=${this.handlePinSubmit}>
|
||||
<dees-icon .icon=${'lucide:arrowRight'} .iconSize=${24}></dees-icon>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderPasswordInput(): TemplateResult {
|
||||
return html`
|
||||
<div class="password-form">
|
||||
<div class="password-input-wrapper">
|
||||
<input
|
||||
class="password-input ${this.showError ? 'error' : ''}"
|
||||
type=${this.showPassword ? 'text' : 'password'}
|
||||
placeholder="Enter your password"
|
||||
.value=${this.passwordValue}
|
||||
@input=${this.handlePasswordInput}
|
||||
@keydown=${this.handlePasswordKeydown}
|
||||
/>
|
||||
<button class="password-toggle" @click=${this.togglePasswordVisibility}>
|
||||
<dees-icon
|
||||
.icon=${this.showPassword ? 'lucide:eyeOff' : 'lucide:eye'}
|
||||
.iconSize=${20}
|
||||
></dees-icon>
|
||||
</button>
|
||||
</div>
|
||||
<button class="submit-button" @click=${this.handlePasswordSubmit}>
|
||||
Sign In
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderQrCode(): TemplateResult {
|
||||
return html`
|
||||
<div class="qr-container">
|
||||
<div class="qr-code">
|
||||
${this.config.qrCodeData
|
||||
? html`<img src=${this.config.qrCodeData} alt="Login QR Code" />`
|
||||
: html`<div class="qr-placeholder">QR Code</div>`
|
||||
}
|
||||
</div>
|
||||
<p class="qr-instruction">
|
||||
Open your authenticator app and scan this code to sign in securely without typing a password.
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private selectMethod(method: TAuthMethod): void {
|
||||
this.selectedMethod = method;
|
||||
this.clearError();
|
||||
this.pinValue = '';
|
||||
this.passwordValue = '';
|
||||
}
|
||||
|
||||
private handlePinInput(digit: string): void {
|
||||
this.clearError();
|
||||
const maxLength = this.config.pinLength || 4;
|
||||
if (this.pinValue.length < maxLength) {
|
||||
this.pinValue += digit;
|
||||
this.dispatchKeyPress(digit);
|
||||
}
|
||||
}
|
||||
|
||||
private handleBackspace(): void {
|
||||
this.clearError();
|
||||
if (this.pinValue.length > 0) {
|
||||
this.pinValue = this.pinValue.slice(0, -1);
|
||||
this.dispatchEvent(new CustomEvent('backspace', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private handlePinSubmit(): void {
|
||||
if (this.pinValue.length === 0) {
|
||||
this.showErrorMessage('Please enter your PIN');
|
||||
return;
|
||||
}
|
||||
|
||||
this.dispatchLoginAttempt('pin', this.pinValue);
|
||||
}
|
||||
|
||||
private handlePasswordInput(e: InputEvent): void {
|
||||
this.clearError();
|
||||
const input = e.target as HTMLInputElement;
|
||||
this.passwordValue = input.value;
|
||||
}
|
||||
|
||||
private handlePasswordKeydown(e: KeyboardEvent): void {
|
||||
if (e.key === 'Enter') {
|
||||
this.handlePasswordSubmit();
|
||||
}
|
||||
}
|
||||
|
||||
private handlePasswordSubmit(): void {
|
||||
if (this.passwordValue.length === 0) {
|
||||
this.showErrorMessage('Please enter your password');
|
||||
return;
|
||||
}
|
||||
|
||||
this.dispatchLoginAttempt('password', this.passwordValue);
|
||||
}
|
||||
|
||||
private togglePasswordVisibility(): void {
|
||||
this.showPassword = !this.showPassword;
|
||||
}
|
||||
|
||||
private dispatchKeyPress(key: string): void {
|
||||
this.dispatchEvent(new CustomEvent('key-press', {
|
||||
detail: { key },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
|
||||
private dispatchLoginAttempt(method: TAuthMethod, value: string): void {
|
||||
this.dispatchEvent(new CustomEvent('login-attempt', {
|
||||
detail: { method, value } as ILoginCredentials,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
|
||||
public showErrorMessage(message: string): void {
|
||||
this.error = message;
|
||||
this.showError = true;
|
||||
}
|
||||
|
||||
public clearError(): void {
|
||||
this.error = '';
|
||||
this.showError = false;
|
||||
}
|
||||
|
||||
public clearInput(): void {
|
||||
this.pinValue = '';
|
||||
this.passwordValue = '';
|
||||
}
|
||||
}
|
||||
1
ts_web/views/eco-view-login/index.ts
Normal file
1
ts_web/views/eco-view-login/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './eco-view-login.js';
|
||||
@@ -4,18 +4,18 @@ export const demo = () => html`
|
||||
<style>
|
||||
.demo-container {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
height: 100%;
|
||||
background: hsl(240 10% 4%);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
<div class="demo-container">
|
||||
<eco-peripherals
|
||||
<eco-view-peripherals
|
||||
.activeCategory=${'all'}
|
||||
@device-select=${(e: CustomEvent) => console.log('Device selected:', e.detail)}
|
||||
@scan-start=${() => console.log('Scanning started')}
|
||||
@scan-complete=${() => console.log('Scanning complete')}
|
||||
></eco-peripherals>
|
||||
></eco-view-peripherals>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
state,
|
||||
} from '@design.estate/dees-element';
|
||||
import { DeesAppuiSecondarymenu, DeesIcon } from '@design.estate/dees-catalog';
|
||||
import type { ISecondaryMenuGroup, ISecondaryMenuItem } from '../../interfaces/secondarymenu.js';
|
||||
import { demo } from './eco-peripherals.demo.js';
|
||||
import type { ISecondaryMenuGroup, ISecondaryMenuItem } from '../../elements/interfaces/secondarymenu.js';
|
||||
import { demo } from './eco-view-peripherals.demo.js';
|
||||
|
||||
// Ensure components are registered
|
||||
DeesAppuiSecondarymenu;
|
||||
@@ -18,7 +18,7 @@ DeesIcon;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'eco-peripherals': EcoPeripherals;
|
||||
'eco-view-peripherals': EcoViewPeripherals;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,10 +47,10 @@ export interface IPeripheralDevice {
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
||||
@customElement('eco-peripherals')
|
||||
export class EcoPeripherals extends DeesElement {
|
||||
@customElement('eco-view-peripherals')
|
||||
export class EcoViewPeripherals extends DeesElement {
|
||||
public static demo = demo;
|
||||
public static demoGroup = 'App Launcher';
|
||||
public static demoGroup = 'Views';
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from './eco-peripherals.js';
|
||||
export * from './eco-view-peripherals.js';
|
||||
|
||||
20
ts_web/views/eco-view-saasshare/eco-view-saasshare.demo.ts
Normal file
20
ts_web/views/eco-view-saasshare/eco-view-saasshare.demo.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
|
||||
export const demo = () => html`
|
||||
<style>
|
||||
.demo-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: hsl(240 10% 4%);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
<div class="demo-container">
|
||||
<eco-view-saasshare
|
||||
.activePanel=${'apps'}
|
||||
@request-approved=${(e: CustomEvent) => console.log('Request approved:', e.detail)}
|
||||
@request-denied=${(e: CustomEvent) => console.log('Request denied:', e.detail)}
|
||||
></eco-view-saasshare>
|
||||
</div>
|
||||
`;
|
||||
1288
ts_web/views/eco-view-saasshare/eco-view-saasshare.ts
Normal file
1288
ts_web/views/eco-view-saasshare/eco-view-saasshare.ts
Normal file
File diff suppressed because it is too large
Load Diff
1
ts_web/views/eco-view-saasshare/index.ts
Normal file
1
ts_web/views/eco-view-saasshare/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './eco-view-saasshare.js';
|
||||
@@ -4,15 +4,15 @@ export const demo = () => html`
|
||||
<style>
|
||||
.demo-container {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
height: 100%;
|
||||
background: hsl(240 10% 4%);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
<div class="demo-container">
|
||||
<eco-settings
|
||||
<eco-view-settings
|
||||
.activePanel=${'general'}
|
||||
></eco-settings>
|
||||
></eco-view-settings>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
state,
|
||||
} from '@design.estate/dees-element';
|
||||
import { DeesAppuiSecondarymenu, DeesIcon } from '@design.estate/dees-catalog';
|
||||
import type { ISecondaryMenuGroup, ISecondaryMenuItem } from '../../interfaces/secondarymenu.js';
|
||||
import { demo } from './eco-settings.demo.js';
|
||||
import type { ISecondaryMenuGroup, ISecondaryMenuItem } from '../../elements/interfaces/secondarymenu.js';
|
||||
import { demo } from './eco-view-settings.demo.js';
|
||||
|
||||
// Ensure components are registered
|
||||
DeesAppuiSecondarymenu;
|
||||
@@ -18,7 +18,7 @@ DeesIcon;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'eco-settings': EcoSettings;
|
||||
'eco-view-settings': EcoViewSettings;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +35,10 @@ export type TSettingsPanel =
|
||||
| 'updates'
|
||||
| 'about';
|
||||
|
||||
@customElement('eco-settings')
|
||||
export class EcoSettings extends DeesElement {
|
||||
@customElement('eco-view-settings')
|
||||
export class EcoViewSettings extends DeesElement {
|
||||
public static demo = demo;
|
||||
public static demoGroup = 'App Launcher';
|
||||
public static demoGroup = 'Views';
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from './eco-settings.js';
|
||||
export * from './eco-view-settings.js';
|
||||
|
||||
18
ts_web/views/eco-view-system/eco-view-system.demo.ts
Normal file
18
ts_web/views/eco-view-system/eco-view-system.demo.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
|
||||
export const demo = () => html`
|
||||
<style>
|
||||
.demo-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: hsl(240 10% 4%);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
<div class="demo-container">
|
||||
<eco-view-system
|
||||
.activePanel=${'overview'}
|
||||
></eco-view-system>
|
||||
</div>
|
||||
`;
|
||||
877
ts_web/views/eco-view-system/eco-view-system.ts
Normal file
877
ts_web/views/eco-view-system/eco-view-system.ts
Normal file
@@ -0,0 +1,877 @@
|
||||
import {
|
||||
customElement,
|
||||
DeesElement,
|
||||
type TemplateResult,
|
||||
html,
|
||||
property,
|
||||
css,
|
||||
cssManager,
|
||||
state,
|
||||
} from '@design.estate/dees-element';
|
||||
import { DeesAppuiSecondarymenu, DeesIcon, DeesStatsGrid } from '@design.estate/dees-catalog';
|
||||
import type { ISecondaryMenuGroup, ISecondaryMenuItem } from '../../elements/interfaces/secondarymenu.js';
|
||||
import { demo } from './eco-view-system.demo.js';
|
||||
|
||||
// Ensure components are registered
|
||||
DeesAppuiSecondarymenu;
|
||||
DeesIcon;
|
||||
DeesStatsGrid;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'eco-view-system': EcoViewSystem;
|
||||
}
|
||||
}
|
||||
|
||||
export type TSystemPanel =
|
||||
| 'overview'
|
||||
| 'cpu'
|
||||
| 'memory'
|
||||
| 'storage'
|
||||
| 'network'
|
||||
| 'processes';
|
||||
|
||||
@customElement('eco-view-system')
|
||||
export class EcoViewSystem extends DeesElement {
|
||||
public static demo = demo;
|
||||
public static demoGroup = 'Views';
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: ${cssManager.bdTheme('#f5f5f7', 'hsl(240 6% 10%)')};
|
||||
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;
|
||||
}
|
||||
|
||||
.system-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
dees-appui-secondarymenu {
|
||||
flex-shrink: 0;
|
||||
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 8%)')};
|
||||
border-right: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 15%)')};
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 32px 48px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.panel-description {
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 50%)')};
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
dees-statsgrid {
|
||||
--dees-statsgrid-gap: 16px;
|
||||
}
|
||||
|
||||
.process-list {
|
||||
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 12%)')};
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')};
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.process-header {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr 1fr 1fr;
|
||||
padding: 12px 16px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(240 5% 14%)')};
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')};
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')};
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.process-row {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr 1fr 1fr;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 15%)')};
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 85%)')};
|
||||
}
|
||||
|
||||
.process-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.process-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.process-value {
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 65%)')};
|
||||
}
|
||||
|
||||
.process-value.high {
|
||||
color: hsl(0 84% 60%);
|
||||
font-weight: 500;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@property({ type: String })
|
||||
accessor activePanel: TSystemPanel = 'overview';
|
||||
|
||||
// Mock system data
|
||||
@state()
|
||||
accessor cpuUsage = 42;
|
||||
|
||||
@state()
|
||||
accessor memoryUsage = 67;
|
||||
|
||||
@state()
|
||||
accessor diskUsage = 54;
|
||||
|
||||
@state()
|
||||
accessor cpuTemp = 58;
|
||||
|
||||
@state()
|
||||
accessor uptime = '14d 7h 32m';
|
||||
|
||||
@state()
|
||||
accessor networkIn = [45, 52, 38, 65, 72, 68, 75, 82, 79, 85, 88, 72];
|
||||
|
||||
@state()
|
||||
accessor networkOut = [32, 28, 35, 42, 38, 45, 52, 48, 55, 62, 58, 65];
|
||||
|
||||
private getMenuGroups(): ISecondaryMenuGroup[] {
|
||||
return [
|
||||
{
|
||||
name: 'Monitor',
|
||||
iconName: 'lucide:activity',
|
||||
items: [
|
||||
{
|
||||
key: 'overview',
|
||||
iconName: 'lucide:layoutDashboard',
|
||||
action: () => this.activePanel = 'overview',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Hardware',
|
||||
iconName: 'lucide:cpu',
|
||||
items: [
|
||||
{
|
||||
key: 'cpu',
|
||||
iconName: 'lucide:cpu',
|
||||
action: () => this.activePanel = 'cpu',
|
||||
},
|
||||
{
|
||||
key: 'memory',
|
||||
iconName: 'lucide:memoryStick',
|
||||
action: () => this.activePanel = 'memory',
|
||||
},
|
||||
{
|
||||
key: 'storage',
|
||||
iconName: 'lucide:hardDrive',
|
||||
action: () => this.activePanel = 'storage',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Network',
|
||||
iconName: 'lucide:network',
|
||||
items: [
|
||||
{
|
||||
key: 'network',
|
||||
iconName: 'lucide:wifi',
|
||||
action: () => this.activePanel = 'network',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Software',
|
||||
iconName: 'lucide:layers',
|
||||
items: [
|
||||
{
|
||||
key: 'processes',
|
||||
iconName: 'lucide:listTree',
|
||||
action: () => this.activePanel = 'processes',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private getSelectedItem(): ISecondaryMenuItem | null {
|
||||
for (const group of this.getMenuGroups()) {
|
||||
for (const item of group.items) {
|
||||
if ('key' in item && item.key === this.activePanel) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="system-container">
|
||||
<dees-appui-secondarymenu
|
||||
.heading=${'System'}
|
||||
.groups=${this.getMenuGroups()}
|
||||
.selectedItem=${this.getSelectedItem()}
|
||||
></dees-appui-secondarymenu>
|
||||
<div class="content">
|
||||
${this.renderActivePanel()}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderActivePanel(): TemplateResult {
|
||||
switch (this.activePanel) {
|
||||
case 'overview':
|
||||
return this.renderOverviewPanel();
|
||||
case 'cpu':
|
||||
return this.renderCpuPanel();
|
||||
case 'memory':
|
||||
return this.renderMemoryPanel();
|
||||
case 'storage':
|
||||
return this.renderStoragePanel();
|
||||
case 'network':
|
||||
return this.renderNetworkPanel();
|
||||
case 'processes':
|
||||
return this.renderProcessesPanel();
|
||||
default:
|
||||
return this.renderOverviewPanel();
|
||||
}
|
||||
}
|
||||
|
||||
private renderOverviewPanel(): TemplateResult {
|
||||
const overviewTiles = [
|
||||
{
|
||||
id: 'cpu',
|
||||
title: 'CPU Usage',
|
||||
value: this.cpuUsage,
|
||||
type: 'gauge' as const,
|
||||
icon: 'lucide:cpu',
|
||||
gaugeOptions: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
thresholds: [
|
||||
{ value: 0, color: 'hsl(142 71% 45%)' },
|
||||
{ value: 60, color: 'hsl(45 93% 47%)' },
|
||||
{ value: 80, color: 'hsl(0 84% 60%)' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'memory',
|
||||
title: 'Memory Usage',
|
||||
value: this.memoryUsage,
|
||||
type: 'gauge' as const,
|
||||
icon: 'lucide:memoryStick',
|
||||
gaugeOptions: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
thresholds: [
|
||||
{ value: 0, color: 'hsl(142 71% 45%)' },
|
||||
{ value: 70, color: 'hsl(45 93% 47%)' },
|
||||
{ value: 85, color: 'hsl(0 84% 60%)' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'disk',
|
||||
title: 'Disk Usage',
|
||||
value: this.diskUsage,
|
||||
type: 'gauge' as const,
|
||||
icon: 'lucide:hardDrive',
|
||||
gaugeOptions: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
thresholds: [
|
||||
{ value: 0, color: 'hsl(142 71% 45%)' },
|
||||
{ value: 75, color: 'hsl(45 93% 47%)' },
|
||||
{ value: 90, color: 'hsl(0 84% 60%)' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'temp',
|
||||
title: 'CPU Temp',
|
||||
value: this.cpuTemp,
|
||||
unit: '°C',
|
||||
type: 'gauge' as const,
|
||||
icon: 'lucide:thermometer',
|
||||
gaugeOptions: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
thresholds: [
|
||||
{ value: 0, color: 'hsl(217 91% 60%)' },
|
||||
{ value: 50, color: 'hsl(142 71% 45%)' },
|
||||
{ value: 70, color: 'hsl(45 93% 47%)' },
|
||||
{ value: 85, color: 'hsl(0 84% 60%)' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'network-in',
|
||||
title: 'Network In',
|
||||
value: '85',
|
||||
unit: 'Mbps',
|
||||
type: 'trend' as const,
|
||||
icon: 'lucide:download',
|
||||
trendData: this.networkIn,
|
||||
color: 'hsl(142 71% 45%)',
|
||||
},
|
||||
{
|
||||
id: 'network-out',
|
||||
title: 'Network Out',
|
||||
value: '65',
|
||||
unit: 'Mbps',
|
||||
type: 'trend' as const,
|
||||
icon: 'lucide:upload',
|
||||
trendData: this.networkOut,
|
||||
color: 'hsl(217 91% 60%)',
|
||||
},
|
||||
{
|
||||
id: 'uptime',
|
||||
title: 'System Uptime',
|
||||
value: this.uptime,
|
||||
type: 'text' as const,
|
||||
icon: 'lucide:clock',
|
||||
color: 'hsl(142 71% 45%)',
|
||||
description: 'Since last reboot',
|
||||
},
|
||||
{
|
||||
id: 'processes',
|
||||
title: 'Processes',
|
||||
value: 247,
|
||||
type: 'number' as const,
|
||||
icon: 'lucide:layers',
|
||||
description: '12 running, 235 sleeping',
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<div class="panel-header">
|
||||
<div class="panel-title">System Overview</div>
|
||||
<div class="panel-description">Real-time system performance metrics</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-section">
|
||||
<dees-statsgrid
|
||||
.tiles=${overviewTiles}
|
||||
.minTileWidth=${220}
|
||||
.gap=${16}
|
||||
></dees-statsgrid>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderCpuPanel(): TemplateResult {
|
||||
const cpuTiles = [
|
||||
{
|
||||
id: 'cpu-total',
|
||||
title: 'Total CPU Usage',
|
||||
value: this.cpuUsage,
|
||||
type: 'gauge' as const,
|
||||
icon: 'lucide:cpu',
|
||||
gaugeOptions: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
thresholds: [
|
||||
{ value: 0, color: 'hsl(142 71% 45%)' },
|
||||
{ value: 60, color: 'hsl(45 93% 47%)' },
|
||||
{ value: 80, color: 'hsl(0 84% 60%)' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'core-0',
|
||||
title: 'Core 0',
|
||||
value: 38,
|
||||
type: 'gauge' as const,
|
||||
gaugeOptions: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
thresholds: [
|
||||
{ value: 0, color: 'hsl(142 71% 45%)' },
|
||||
{ value: 60, color: 'hsl(45 93% 47%)' },
|
||||
{ value: 80, color: 'hsl(0 84% 60%)' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'core-1',
|
||||
title: 'Core 1',
|
||||
value: 52,
|
||||
type: 'gauge' as const,
|
||||
gaugeOptions: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
thresholds: [
|
||||
{ value: 0, color: 'hsl(142 71% 45%)' },
|
||||
{ value: 60, color: 'hsl(45 93% 47%)' },
|
||||
{ value: 80, color: 'hsl(0 84% 60%)' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'core-2',
|
||||
title: 'Core 2',
|
||||
value: 45,
|
||||
type: 'gauge' as const,
|
||||
gaugeOptions: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
thresholds: [
|
||||
{ value: 0, color: 'hsl(142 71% 45%)' },
|
||||
{ value: 60, color: 'hsl(45 93% 47%)' },
|
||||
{ value: 80, color: 'hsl(0 84% 60%)' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'core-3',
|
||||
title: 'Core 3',
|
||||
value: 33,
|
||||
type: 'gauge' as const,
|
||||
gaugeOptions: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
thresholds: [
|
||||
{ value: 0, color: 'hsl(142 71% 45%)' },
|
||||
{ value: 60, color: 'hsl(45 93% 47%)' },
|
||||
{ value: 80, color: 'hsl(0 84% 60%)' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'load-avg',
|
||||
title: 'Load Average',
|
||||
value: '2.45',
|
||||
type: 'trend' as const,
|
||||
icon: 'lucide:activity',
|
||||
trendData: [1.8, 2.1, 2.4, 2.2, 2.5, 2.3, 2.6, 2.4, 2.45],
|
||||
description: '1m: 2.45, 5m: 2.32, 15m: 2.18',
|
||||
},
|
||||
{
|
||||
id: 'cpu-temp',
|
||||
title: 'Temperature',
|
||||
value: this.cpuTemp,
|
||||
unit: '°C',
|
||||
type: 'gauge' as const,
|
||||
icon: 'lucide:thermometer',
|
||||
gaugeOptions: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
thresholds: [
|
||||
{ value: 0, color: 'hsl(217 91% 60%)' },
|
||||
{ value: 50, color: 'hsl(142 71% 45%)' },
|
||||
{ value: 70, color: 'hsl(45 93% 47%)' },
|
||||
{ value: 85, color: 'hsl(0 84% 60%)' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'freq',
|
||||
title: 'Clock Speed',
|
||||
value: '3.2',
|
||||
unit: 'GHz',
|
||||
type: 'number' as const,
|
||||
icon: 'lucide:gauge',
|
||||
description: 'Max: 4.2 GHz',
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<div class="panel-header">
|
||||
<div class="panel-title">CPU</div>
|
||||
<div class="panel-description">Processor usage and performance</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-section">
|
||||
<dees-statsgrid
|
||||
.tiles=${cpuTiles}
|
||||
.minTileWidth=${200}
|
||||
.gap=${16}
|
||||
></dees-statsgrid>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderMemoryPanel(): TemplateResult {
|
||||
const memoryTiles = [
|
||||
{
|
||||
id: 'ram-usage',
|
||||
title: 'RAM Usage',
|
||||
value: this.memoryUsage,
|
||||
type: 'gauge' as const,
|
||||
icon: 'lucide:memoryStick',
|
||||
gaugeOptions: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
thresholds: [
|
||||
{ value: 0, color: 'hsl(142 71% 45%)' },
|
||||
{ value: 70, color: 'hsl(45 93% 47%)' },
|
||||
{ value: 85, color: 'hsl(0 84% 60%)' },
|
||||
],
|
||||
},
|
||||
description: '10.7 GB of 16 GB',
|
||||
},
|
||||
{
|
||||
id: 'swap-usage',
|
||||
title: 'Swap Usage',
|
||||
value: 12,
|
||||
type: 'gauge' as const,
|
||||
icon: 'lucide:hardDrive',
|
||||
gaugeOptions: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
thresholds: [
|
||||
{ value: 0, color: 'hsl(142 71% 45%)' },
|
||||
{ value: 50, color: 'hsl(45 93% 47%)' },
|
||||
{ value: 75, color: 'hsl(0 84% 60%)' },
|
||||
],
|
||||
},
|
||||
description: '0.5 GB of 4 GB',
|
||||
},
|
||||
{
|
||||
id: 'mem-trend',
|
||||
title: 'Memory History',
|
||||
value: '67%',
|
||||
type: 'trend' as const,
|
||||
icon: 'lucide:trendingUp',
|
||||
trendData: [58, 62, 65, 63, 68, 72, 70, 65, 67],
|
||||
description: 'Last hour',
|
||||
},
|
||||
{
|
||||
id: 'cached',
|
||||
title: 'Cached',
|
||||
value: '3.2',
|
||||
unit: 'GB',
|
||||
type: 'number' as const,
|
||||
icon: 'lucide:database',
|
||||
color: 'hsl(217 91% 60%)',
|
||||
},
|
||||
{
|
||||
id: 'buffers',
|
||||
title: 'Buffers',
|
||||
value: '512',
|
||||
unit: 'MB',
|
||||
type: 'number' as const,
|
||||
icon: 'lucide:layers',
|
||||
color: 'hsl(262 83% 58%)',
|
||||
},
|
||||
{
|
||||
id: 'available',
|
||||
title: 'Available',
|
||||
value: '5.3',
|
||||
unit: 'GB',
|
||||
type: 'number' as const,
|
||||
icon: 'lucide:checkCircle',
|
||||
color: 'hsl(142 71% 45%)',
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<div class="panel-header">
|
||||
<div class="panel-title">Memory</div>
|
||||
<div class="panel-description">RAM and swap usage details</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-section">
|
||||
<dees-statsgrid
|
||||
.tiles=${memoryTiles}
|
||||
.minTileWidth=${220}
|
||||
.gap=${16}
|
||||
></dees-statsgrid>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStoragePanel(): TemplateResult {
|
||||
const storageTiles = [
|
||||
{
|
||||
id: 'disk-main',
|
||||
title: 'System Drive',
|
||||
value: this.diskUsage,
|
||||
type: 'percentage' as const,
|
||||
icon: 'lucide:hardDrive',
|
||||
description: '275 GB of 512 GB used',
|
||||
color: 'hsl(217 91% 60%)',
|
||||
},
|
||||
{
|
||||
id: 'disk-data',
|
||||
title: 'Data Drive',
|
||||
value: 38,
|
||||
type: 'percentage' as const,
|
||||
icon: 'lucide:hardDrive',
|
||||
description: '380 GB of 1 TB used',
|
||||
color: 'hsl(142 71% 45%)',
|
||||
},
|
||||
{
|
||||
id: 'read-speed',
|
||||
title: 'Read Speed',
|
||||
value: '245',
|
||||
unit: 'MB/s',
|
||||
type: 'trend' as const,
|
||||
icon: 'lucide:download',
|
||||
trendData: [180, 220, 195, 280, 245, 210, 265, 230, 245],
|
||||
color: 'hsl(142 71% 45%)',
|
||||
},
|
||||
{
|
||||
id: 'write-speed',
|
||||
title: 'Write Speed',
|
||||
value: '128',
|
||||
unit: 'MB/s',
|
||||
type: 'trend' as const,
|
||||
icon: 'lucide:upload',
|
||||
trendData: [95, 110, 85, 145, 120, 105, 138, 115, 128],
|
||||
color: 'hsl(217 91% 60%)',
|
||||
},
|
||||
{
|
||||
id: 'iops-read',
|
||||
title: 'Read IOPS',
|
||||
value: '12.4k',
|
||||
type: 'number' as const,
|
||||
icon: 'lucide:gauge',
|
||||
description: 'Operations/sec',
|
||||
},
|
||||
{
|
||||
id: 'iops-write',
|
||||
title: 'Write IOPS',
|
||||
value: '8.2k',
|
||||
type: 'number' as const,
|
||||
icon: 'lucide:gauge',
|
||||
description: 'Operations/sec',
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<div class="panel-header">
|
||||
<div class="panel-title">Storage</div>
|
||||
<div class="panel-description">Disk usage and I/O performance</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-section">
|
||||
<dees-statsgrid
|
||||
.tiles=${storageTiles}
|
||||
.minTileWidth=${220}
|
||||
.gap=${16}
|
||||
></dees-statsgrid>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderNetworkPanel(): TemplateResult {
|
||||
const networkTiles = [
|
||||
{
|
||||
id: 'download',
|
||||
title: 'Download',
|
||||
value: '85.2',
|
||||
unit: 'Mbps',
|
||||
type: 'trend' as const,
|
||||
icon: 'lucide:download',
|
||||
trendData: this.networkIn,
|
||||
color: 'hsl(142 71% 45%)',
|
||||
},
|
||||
{
|
||||
id: 'upload',
|
||||
title: 'Upload',
|
||||
value: '64.8',
|
||||
unit: 'Mbps',
|
||||
type: 'trend' as const,
|
||||
icon: 'lucide:upload',
|
||||
trendData: this.networkOut,
|
||||
color: 'hsl(217 91% 60%)',
|
||||
},
|
||||
{
|
||||
id: 'latency',
|
||||
title: 'Latency',
|
||||
value: 12,
|
||||
unit: 'ms',
|
||||
type: 'gauge' as const,
|
||||
icon: 'lucide:activity',
|
||||
gaugeOptions: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
thresholds: [
|
||||
{ value: 0, color: 'hsl(142 71% 45%)' },
|
||||
{ value: 30, color: 'hsl(45 93% 47%)' },
|
||||
{ value: 60, color: 'hsl(0 84% 60%)' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'packets-in',
|
||||
title: 'Packets In',
|
||||
value: '1.2M',
|
||||
type: 'number' as const,
|
||||
icon: 'lucide:arrowDownCircle',
|
||||
description: 'Per second',
|
||||
},
|
||||
{
|
||||
id: 'packets-out',
|
||||
title: 'Packets Out',
|
||||
value: '892k',
|
||||
type: 'number' as const,
|
||||
icon: 'lucide:arrowUpCircle',
|
||||
description: 'Per second',
|
||||
},
|
||||
{
|
||||
id: 'connections',
|
||||
title: 'Active Connections',
|
||||
value: 48,
|
||||
type: 'number' as const,
|
||||
icon: 'lucide:link',
|
||||
description: '12 established, 36 waiting',
|
||||
},
|
||||
{
|
||||
id: 'total-down',
|
||||
title: 'Total Downloaded',
|
||||
value: '24.5',
|
||||
unit: 'GB',
|
||||
type: 'number' as const,
|
||||
icon: 'lucide:database',
|
||||
description: 'This session',
|
||||
color: 'hsl(142 71% 45%)',
|
||||
},
|
||||
{
|
||||
id: 'total-up',
|
||||
title: 'Total Uploaded',
|
||||
value: '8.2',
|
||||
unit: 'GB',
|
||||
type: 'number' as const,
|
||||
icon: 'lucide:database',
|
||||
description: 'This session',
|
||||
color: 'hsl(217 91% 60%)',
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<div class="panel-header">
|
||||
<div class="panel-title">Network</div>
|
||||
<div class="panel-description">Network traffic and connectivity</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-section">
|
||||
<dees-statsgrid
|
||||
.tiles=${networkTiles}
|
||||
.minTileWidth=${220}
|
||||
.gap=${16}
|
||||
></dees-statsgrid>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderProcessesPanel(): TemplateResult {
|
||||
const processTiles = [
|
||||
{
|
||||
id: 'total-processes',
|
||||
title: 'Total Processes',
|
||||
value: 247,
|
||||
type: 'number' as const,
|
||||
icon: 'lucide:layers',
|
||||
},
|
||||
{
|
||||
id: 'running',
|
||||
title: 'Running',
|
||||
value: 12,
|
||||
type: 'number' as const,
|
||||
icon: 'lucide:play',
|
||||
color: 'hsl(142 71% 45%)',
|
||||
},
|
||||
{
|
||||
id: 'sleeping',
|
||||
title: 'Sleeping',
|
||||
value: 235,
|
||||
type: 'number' as const,
|
||||
icon: 'lucide:moon',
|
||||
color: 'hsl(217 91% 60%)',
|
||||
},
|
||||
{
|
||||
id: 'threads',
|
||||
title: 'Threads',
|
||||
value: 1842,
|
||||
type: 'number' as const,
|
||||
icon: 'lucide:gitBranch',
|
||||
},
|
||||
];
|
||||
|
||||
const topProcesses = [
|
||||
{ name: 'node', pid: 1234, cpu: 12.5, memory: 8.2 },
|
||||
{ name: 'chrome', pid: 2345, cpu: 8.3, memory: 15.4 },
|
||||
{ name: 'code', pid: 3456, cpu: 5.2, memory: 12.1 },
|
||||
{ name: 'docker', pid: 4567, cpu: 4.8, memory: 6.8 },
|
||||
{ name: 'postgres', pid: 5678, cpu: 3.2, memory: 4.5 },
|
||||
{ name: 'nginx', pid: 6789, cpu: 1.5, memory: 2.1 },
|
||||
{ name: 'redis', pid: 7890, cpu: 0.8, memory: 1.8 },
|
||||
];
|
||||
|
||||
return html`
|
||||
<div class="panel-header">
|
||||
<div class="panel-title">Processes</div>
|
||||
<div class="panel-description">Running processes and resource usage</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-section">
|
||||
<dees-statsgrid
|
||||
.tiles=${processTiles}
|
||||
.minTileWidth=${180}
|
||||
.gap=${16}
|
||||
></dees-statsgrid>
|
||||
</div>
|
||||
|
||||
<div class="stats-section">
|
||||
<div class="section-title">Top Processes by CPU</div>
|
||||
<div class="process-list">
|
||||
<div class="process-header">
|
||||
<span>Process</span>
|
||||
<span>PID</span>
|
||||
<span>CPU %</span>
|
||||
<span>Memory %</span>
|
||||
</div>
|
||||
${topProcesses.map(proc => html`
|
||||
<div class="process-row">
|
||||
<span class="process-name">${proc.name}</span>
|
||||
<span class="process-value">${proc.pid}</span>
|
||||
<span class="process-value ${proc.cpu > 10 ? 'high' : ''}">${proc.cpu}%</span>
|
||||
<span class="process-value ${proc.memory > 10 ? 'high' : ''}">${proc.memory}%</span>
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
1
ts_web/views/eco-view-system/index.ts
Normal file
1
ts_web/views/eco-view-system/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './eco-view-system.js';
|
||||
6
ts_web/views/index.ts
Normal file
6
ts_web/views/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from './eco-view-settings/index.js';
|
||||
export * from './eco-view-peripherals/index.js';
|
||||
export * from './eco-view-saasshare/index.js';
|
||||
export * from './eco-view-system/index.js';
|
||||
export * from './eco-view-home/index.js';
|
||||
export * from './eco-view-login/index.js';
|
||||
Reference in New Issue
Block a user