This commit is contained in:
2026-01-06 10:17:05 +00:00
parent 942896a3ef
commit fafa98bc19
7 changed files with 2061 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
import { html } from '@design.estate/dees-element';
export const demo = () => html`
<style>
.demo-container {
width: 100%;
height: 600px;
background: hsl(240 10% 4%);
border-radius: 12px;
overflow: hidden;
}
</style>
<div class="demo-container">
<eco-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>
</div>
`;

View File

@@ -0,0 +1,840 @@
import {
customElement,
DeesElement,
type TemplateResult,
html,
property,
css,
cssManager,
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';
// Ensure components are registered
DeesAppuiSecondarymenu;
DeesIcon;
declare global {
interface HTMLElementTagNameMap {
'eco-peripherals': EcoPeripherals;
}
}
export type TPeripheralCategory =
| 'all'
| 'printers'
| 'scanners'
| 'speakers'
| 'storage'
| 'power'
| 'cameras'
| 'streaming'
| 'usb';
export type TConnectionType = 'network' | 'usb' | 'bluetooth';
export interface IPeripheralDevice {
id: string;
name: string;
type: TPeripheralCategory;
connectionType: TConnectionType;
status: 'online' | 'offline' | 'busy' | 'error';
ip?: string;
manufacturer?: string;
model?: string;
isDefault?: boolean;
}
@customElement('eco-peripherals')
export class EcoPeripherals 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('#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;
}
.peripherals-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: 24px;
display: flex;
align-items: center;
justify-content: space-between;
}
.panel-header-left {
display: flex;
flex-direction: column;
gap: 4px;
}
.panel-title {
font-size: 28px;
font-weight: 600;
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
}
.panel-description {
font-size: 14px;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
}
.scan-button {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: hsl(217 91% 60%);
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background 0.15s ease;
}
.scan-button:hover {
background: hsl(217 91% 55%);
}
.scan-button.scanning {
background: ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(240 5% 25%)')};
cursor: not-allowed;
}
.scan-button.scanning dees-icon {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.device-section {
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 12%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')};
border-radius: 12px;
margin-bottom: 24px;
overflow: hidden;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 15%)')};
}
.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;
}
.device-count {
font-size: 12px;
color: ${cssManager.bdTheme('hsl(0 0% 60%)', 'hsl(0 0% 50%)')};
background: ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 20%)')};
padding: 2px 8px;
border-radius: 10px;
}
.device-list {
padding: 8px 0;
}
.device-item {
display: flex;
align-items: center;
gap: 16px;
padding: 14px 20px;
cursor: pointer;
transition: background 0.15s ease;
}
.device-item:hover {
background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(240 5% 14%)')};
}
.device-icon-wrapper {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
background: ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 18%)')};
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')};
}
.device-icon-wrapper.online {
background: ${cssManager.bdTheme('hsl(142 71% 93%)', 'hsl(142 71% 45% / 0.15)')};
color: hsl(142 71% 35%);
}
.device-icon-wrapper.busy {
background: ${cssManager.bdTheme('hsl(47 100% 93%)', 'hsl(47 100% 50% / 0.15)')};
color: hsl(47 100% 40%);
}
.device-icon-wrapper.error {
background: ${cssManager.bdTheme('hsl(0 72% 93%)', 'hsl(0 72% 51% / 0.15)')};
color: hsl(0 72% 45%);
}
.device-info {
flex: 1;
min-width: 0;
}
.device-name {
font-size: 15px;
font-weight: 500;
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
display: flex;
align-items: center;
gap: 8px;
}
.default-badge {
font-size: 10px;
font-weight: 600;
color: hsl(217 91% 60%);
background: ${cssManager.bdTheme('hsl(217 91% 95%)', 'hsl(217 91% 60% / 0.15)')};
padding: 2px 6px;
border-radius: 4px;
text-transform: uppercase;
}
.device-details {
font-size: 13px;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')};
margin-top: 2px;
display: flex;
align-items: center;
gap: 12px;
}
.device-detail {
display: flex;
align-items: center;
gap: 4px;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.status-indicator.online {
background: hsl(142 71% 45%);
}
.status-indicator.offline {
background: ${cssManager.bdTheme('hsl(0 0% 70%)', 'hsl(0 0% 40%)')};
}
.status-indicator.busy {
background: hsl(47 100% 50%);
}
.status-indicator.error {
background: hsl(0 72% 51%);
}
.device-actions {
display: flex;
align-items: center;
gap: 8px;
}
.action-button {
padding: 8px 12px;
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(240 5% 25%)')};
border-radius: 6px;
background: transparent;
color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')};
font-size: 13px;
cursor: pointer;
transition: all 0.15s ease;
}
.action-button:hover {
background: ${cssManager.bdTheme('hsl(0 0% 96%)', 'hsl(240 5% 18%)')};
border-color: ${cssManager.bdTheme('hsl(0 0% 75%)', 'hsl(240 5% 35%)')};
}
.action-button.primary {
background: hsl(217 91% 60%);
border-color: hsl(217 91% 60%);
color: white;
}
.action-button.primary:hover {
background: hsl(217 91% 55%);
}
.empty-state {
padding: 48px 20px;
text-align: center;
}
.empty-icon {
color: ${cssManager.bdTheme('hsl(0 0% 75%)', 'hsl(0 0% 35%)')};
margin-bottom: 16px;
}
.empty-title {
font-size: 16px;
font-weight: 500;
color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')};
margin-bottom: 8px;
}
.empty-description {
font-size: 14px;
color: ${cssManager.bdTheme('hsl(0 0% 55%)', 'hsl(0 0% 50%)')};
}
.connection-type {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 11px;
padding: 2px 6px;
border-radius: 4px;
background: ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 20%)')};
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
}
`,
];
@property({ type: String })
accessor activeCategory: TPeripheralCategory = 'all';
@state()
accessor isScanning = false;
@state()
accessor devices: IPeripheralDevice[] = [
// Mock printers
{
id: 'printer-1',
name: 'HP LaserJet Pro',
type: 'printers',
connectionType: 'network',
status: 'online',
ip: '192.168.1.50',
manufacturer: 'HP',
model: 'LaserJet Pro M404n',
isDefault: true,
},
{
id: 'printer-2',
name: 'Brother MFC-L2750DW',
type: 'printers',
connectionType: 'network',
status: 'online',
ip: '192.168.1.51',
manufacturer: 'Brother',
model: 'MFC-L2750DW',
},
{
id: 'printer-3',
name: 'Canon PIXMA',
type: 'printers',
connectionType: 'usb',
status: 'offline',
manufacturer: 'Canon',
model: 'PIXMA TR8620',
},
// Mock scanners
{
id: 'scanner-1',
name: 'Epson Perfection V600',
type: 'scanners',
connectionType: 'usb',
status: 'online',
manufacturer: 'Epson',
model: 'Perfection V600',
},
// Mock speakers
{
id: 'speaker-1',
name: 'Sonos One',
type: 'speakers',
connectionType: 'network',
status: 'online',
ip: '192.168.1.60',
manufacturer: 'Sonos',
model: 'One (Gen 2)',
},
{
id: 'speaker-2',
name: 'HomePod mini',
type: 'speakers',
connectionType: 'network',
status: 'online',
ip: '192.168.1.61',
manufacturer: 'Apple',
model: 'HomePod mini',
},
// Mock NAS
{
id: 'nas-1',
name: 'Synology DS920+',
type: 'storage',
connectionType: 'network',
status: 'online',
ip: '192.168.1.100',
manufacturer: 'Synology',
model: 'DS920+',
},
// Mock UPS
{
id: 'ups-1',
name: 'APC Back-UPS Pro',
type: 'power',
connectionType: 'usb',
status: 'online',
manufacturer: 'APC',
model: 'Back-UPS Pro 1500',
},
// Mock cameras
{
id: 'camera-1',
name: 'Logitech C920',
type: 'cameras',
connectionType: 'usb',
status: 'online',
manufacturer: 'Logitech',
model: 'C920 HD Pro',
isDefault: true,
},
{
id: 'camera-2',
name: 'Ring Indoor Cam',
type: 'cameras',
connectionType: 'network',
status: 'online',
ip: '192.168.1.70',
manufacturer: 'Ring',
model: 'Indoor Cam',
},
// Mock streaming devices
{
id: 'streaming-1',
name: 'Living Room Apple TV',
type: 'streaming',
connectionType: 'network',
status: 'online',
ip: '192.168.1.80',
manufacturer: 'Apple',
model: 'Apple TV 4K',
},
{
id: 'streaming-2',
name: 'Bedroom Chromecast',
type: 'streaming',
connectionType: 'network',
status: 'online',
ip: '192.168.1.81',
manufacturer: 'Google',
model: 'Chromecast with Google TV',
},
// Mock USB devices
{
id: 'usb-1',
name: 'SanDisk Ultra',
type: 'usb',
connectionType: 'usb',
status: 'online',
manufacturer: 'SanDisk',
model: 'Ultra USB 3.0 128GB',
},
];
private getMenuGroups(): ISecondaryMenuGroup[] {
const allCount = this.devices.length;
const getCount = (type: TPeripheralCategory) =>
this.devices.filter(d => d.type === type).length;
return [
{
name: 'Devices',
iconName: 'lucide:monitor',
items: [
{
key: 'all',
iconName: 'lucide:layoutGrid',
action: () => this.activeCategory = 'all',
badge: allCount,
},
],
},
{
name: 'Output',
iconName: 'lucide:printer',
items: [
{
key: 'printers',
iconName: 'lucide:printer',
action: () => this.activeCategory = 'printers',
badge: getCount('printers') || undefined,
},
{
key: 'speakers',
iconName: 'lucide:speaker',
action: () => this.activeCategory = 'speakers',
badge: getCount('speakers') || undefined,
},
],
},
{
name: 'Input',
iconName: 'lucide:scan',
items: [
{
key: 'scanners',
iconName: 'lucide:scan',
action: () => this.activeCategory = 'scanners',
badge: getCount('scanners') || undefined,
},
{
key: 'cameras',
iconName: 'lucide:camera',
action: () => this.activeCategory = 'cameras',
badge: getCount('cameras') || undefined,
},
],
},
{
name: 'Network',
iconName: 'lucide:network',
items: [
{
key: 'storage',
iconName: 'lucide:hardDrive',
action: () => this.activeCategory = 'storage',
badge: getCount('storage') || undefined,
},
{
key: 'streaming',
iconName: 'lucide:cast',
action: () => this.activeCategory = 'streaming',
badge: getCount('streaming') || undefined,
},
],
},
{
name: 'Other',
iconName: 'lucide:plug',
items: [
{
key: 'power',
iconName: 'lucide:batteryCharging',
action: () => this.activeCategory = 'power',
badge: getCount('power') || undefined,
},
{
key: 'usb',
iconName: 'lucide:usb',
action: () => this.activeCategory = 'usb',
badge: getCount('usb') || undefined,
},
],
},
];
}
private getSelectedItem(): ISecondaryMenuItem | null {
for (const group of this.getMenuGroups()) {
for (const item of group.items) {
if ('key' in item && item.key === this.activeCategory) {
return item;
}
}
}
return null;
}
private getFilteredDevices(): IPeripheralDevice[] {
if (this.activeCategory === 'all') {
return this.devices;
}
return this.devices.filter(d => d.type === this.activeCategory);
}
private getCategoryTitle(): string {
const titles: Record<TPeripheralCategory, string> = {
all: 'All Devices',
printers: 'Printers',
scanners: 'Scanners',
speakers: 'Speakers',
storage: 'Network Storage',
power: 'Power Devices',
cameras: 'Cameras',
streaming: 'Streaming Devices',
usb: 'USB Devices',
};
return titles[this.activeCategory];
}
private getCategoryDescription(): string {
const descriptions: Record<TPeripheralCategory, string> = {
all: 'View and manage all connected peripherals',
printers: 'Manage printers and print queues',
scanners: 'Configure scanners and scanning options',
speakers: 'Network speakers and audio devices',
storage: 'NAS devices and network storage',
power: 'UPS and power management devices',
cameras: 'Webcams and security cameras',
streaming: 'Apple TV, Chromecast, and streaming devices',
usb: 'USB storage and connected devices',
};
return descriptions[this.activeCategory];
}
private getDeviceIcon(device: IPeripheralDevice): string {
const icons: Record<TPeripheralCategory, string> = {
all: 'lucide:monitor',
printers: 'lucide:printer',
scanners: 'lucide:scan',
speakers: 'lucide:speaker',
storage: 'lucide:hardDrive',
power: 'lucide:batteryCharging',
cameras: 'lucide:camera',
streaming: 'lucide:cast',
usb: 'lucide:usb',
};
return icons[device.type];
}
private getConnectionIcon(type: TConnectionType): string {
const icons: Record<TConnectionType, string> = {
network: 'lucide:wifi',
usb: 'lucide:usb',
bluetooth: 'lucide:bluetooth',
};
return icons[type];
}
private async handleScan(): Promise<void> {
if (this.isScanning) return;
this.isScanning = true;
this.dispatchEvent(new CustomEvent('scan-start', {
bubbles: true,
composed: true,
}));
// Simulate scanning
await new Promise(resolve => setTimeout(resolve, 3000));
this.isScanning = false;
this.dispatchEvent(new CustomEvent('scan-complete', {
bubbles: true,
composed: true,
}));
}
private handleDeviceClick(device: IPeripheralDevice): void {
this.dispatchEvent(new CustomEvent('device-select', {
detail: { device },
bubbles: true,
composed: true,
}));
}
private handleSetDefault(device: IPeripheralDevice, e: Event): void {
e.stopPropagation();
this.devices = this.devices.map(d => ({
...d,
isDefault: d.type === device.type ? d.id === device.id : d.isDefault,
}));
this.dispatchEvent(new CustomEvent('device-set-default', {
detail: { device },
bubbles: true,
composed: true,
}));
}
public render(): TemplateResult {
return html`
<div class="peripherals-container">
<dees-appui-secondarymenu
.heading=${'Peripherals'}
.groups=${this.getMenuGroups()}
.selectedItem=${this.getSelectedItem()}
></dees-appui-secondarymenu>
<div class="content">
${this.renderContent()}
</div>
</div>
`;
}
private renderContent(): TemplateResult {
const devices = this.getFilteredDevices();
return html`
<div class="panel-header">
<div class="panel-header-left">
<div class="panel-title">${this.getCategoryTitle()}</div>
<div class="panel-description">${this.getCategoryDescription()}</div>
</div>
<button
class="scan-button ${this.isScanning ? 'scanning' : ''}"
@click=${this.handleScan}
?disabled=${this.isScanning}
>
<dees-icon .icon=${this.isScanning ? 'lucide:loader2' : 'lucide:radar'} .iconSize=${16}></dees-icon>
${this.isScanning ? 'Scanning...' : 'Scan for Devices'}
</button>
</div>
${this.activeCategory === 'all'
? this.renderGroupedDevices(devices)
: this.renderDeviceList(devices)
}
`;
}
private renderGroupedDevices(devices: IPeripheralDevice[]): TemplateResult {
const groups = new Map<TPeripheralCategory, IPeripheralDevice[]>();
for (const device of devices) {
const existing = groups.get(device.type) || [];
existing.push(device);
groups.set(device.type, existing);
}
const categoryLabels: Record<TPeripheralCategory, string> = {
all: 'All',
printers: 'Printers',
scanners: 'Scanners',
speakers: 'Speakers',
storage: 'Network Storage',
power: 'Power Devices',
cameras: 'Cameras',
streaming: 'Streaming',
usb: 'USB Devices',
};
return html`
${Array.from(groups.entries()).map(([category, categoryDevices]) => html`
<div class="device-section">
<div class="section-header">
<span class="section-title">${categoryLabels[category]}</span>
<span class="device-count">${categoryDevices.length}</span>
</div>
<div class="device-list">
${categoryDevices.map(device => this.renderDeviceItem(device))}
</div>
</div>
`)}
`;
}
private renderDeviceList(devices: IPeripheralDevice[]): TemplateResult {
if (devices.length === 0) {
return this.renderEmptyState();
}
return html`
<div class="device-section">
<div class="section-header">
<span class="section-title">Discovered Devices</span>
<span class="device-count">${devices.length}</span>
</div>
<div class="device-list">
${devices.map(device => this.renderDeviceItem(device))}
</div>
</div>
`;
}
private renderDeviceItem(device: IPeripheralDevice): TemplateResult {
return html`
<div class="device-item" @click=${() => this.handleDeviceClick(device)}>
<div class="device-icon-wrapper ${device.status}">
<dees-icon .icon=${this.getDeviceIcon(device)} .iconSize=${24}></dees-icon>
</div>
<div class="device-info">
<div class="device-name">
${device.name}
${device.isDefault ? html`<span class="default-badge">Default</span>` : ''}
</div>
<div class="device-details">
<div class="device-detail">
<span class="status-indicator ${device.status}"></span>
${device.status.charAt(0).toUpperCase() + device.status.slice(1)}
</div>
${device.ip ? html`
<div class="device-detail">${device.ip}</div>
` : ''}
<span class="connection-type">
<dees-icon .icon=${this.getConnectionIcon(device.connectionType)} .iconSize=${10}></dees-icon>
${device.connectionType.toUpperCase()}
</span>
</div>
</div>
<div class="device-actions">
${!device.isDefault && (device.type === 'printers' || device.type === 'cameras') ? html`
<button class="action-button" @click=${(e: Event) => this.handleSetDefault(device, e)}>
Set Default
</button>
` : ''}
<dees-icon .icon=${'lucide:chevronRight'} .iconSize=${16}></dees-icon>
</div>
</div>
`;
}
private renderEmptyState(): TemplateResult {
return html`
<div class="device-section">
<div class="empty-state">
<dees-icon class="empty-icon" .icon=${'lucide:searchX'} .iconSize=${48}></dees-icon>
<div class="empty-title">No devices found</div>
<div class="empty-description">
Click "Scan for Devices" to discover ${this.getCategoryTitle().toLowerCase()} on your network or connected via USB.
</div>
</div>
</div>
`;
}
}

View File

@@ -0,0 +1 @@
export * from './eco-peripherals.js';

View File

@@ -0,0 +1,18 @@
import { html } from '@design.estate/dees-element';
export const demo = () => html`
<style>
.demo-container {
width: 100%;
height: 600px;
background: hsl(240 10% 4%);
border-radius: 12px;
overflow: hidden;
}
</style>
<div class="demo-container">
<eco-settings
.activePanel=${'general'}
></eco-settings>
</div>
`;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
export * from './eco-settings.js';

View File

@@ -4,3 +4,5 @@ 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';