10 Commits

Author SHA1 Message Date
b5dc6204ce v3.38.0
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-12 18:10:20 +00:00
b800740d5d feat(eco-view-system): add extended system metrics and display formatted total network usage in eco system view 2026-01-12 18:10:20 +00:00
5d27406967 v3.37.0
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-12 15:16:01 +00:00
ee45fb01a2 feat(elements): add eco-provider-frame and dataprovider interfaces; improve virtual keyboard interactions; add demos, exports and bump dev dependencies 2026-01-12 15:16:01 +00:00
bf4fcfac71 3.36.4
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-12 14:00:36 +00:00
ab601bfd7a 3.36.3
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-12 13:54:44 +00:00
c0ef860bd6 3.36.2
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-12 13:38:08 +00:00
debf9829af v3.36.1
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-12 13:31:56 +00:00
e27141c686 fix(): no changes 2026-01-12 13:31:56 +00:00
e346d582ee fix(applauncher): call action after setting view, not instead 2026-01-12 13:31:35 +00:00
21 changed files with 4455 additions and 456 deletions

View File

@@ -1,5 +1,27 @@
# Changelog # Changelog
## 2026-01-12 - 3.38.0 - feat(eco-view-system)
add extended system metrics and display formatted total network usage in eco system view
- Added numerous new observable properties for richer system telemetry: cpuPhysicalCores, cpuSpeedMax, memoryAvailable, memoryCached, memoryBuffers, swapTotal, swapUsed, diskTotal, diskUsed, diskFree, networkRxSec, networkTxSec, networkRxTotal, networkTxTotal, distro, and coreLoads.
- Reworked network/usage UI: removed several temporary network/latency cards and replaced them with Total Downloaded/Uploaded cards that use formatBytes(...) and show totals since boot.
- Bumped dependency @design.estate/dees-catalog from ^3.34.1 to ^3.35.0.
## 2026-01-12 - 3.37.0 - feat(elements)
add eco-provider-frame and dataprovider interfaces; improve virtual keyboard interactions; add demos, exports and bump dev dependencies
- Add new eco-provider-frame web component (iframe-based provider protocol) with messaging API (sendData, requestData, ping) and events for provider-ready, features-changed, provider-response, provider-error
- Introduce comprehensive dataprovider TypeScript interfaces (ts_web/elements/interfaces/dataprovider.ts) describing messages, payloads, provider state and store schema
- Improve virtual keyboard and launcher interactions: add tabindex, preventDefault on mousedown, stopPropagation on pointer handlers, remove key focus outline to fix touch/mouse behavior
- Add demos and new exports for provider-frame, browser and scan views/components (demo files and index exports)
- Update build/dev config: add npmextra.json tsbundle config and bump dependencies: @design.estate/dees-catalog -> ^3.34.1, @git.zone/tsbuild -> ^4.1.0, @git.zone/tsbundle -> ^2.8.1, @types/node -> ^25.0.6
## 2026-01-12 - 3.36.1 - fix()
no changes
- No files changed in this commit
- No release required
## 2026-01-12 - 3.36.0 - feat(eco-view-peripherals) ## 2026-01-12 - 3.36.0 - feat(eco-view-peripherals)
encapsulate networkRanges as internal @state and add public getter/setter methods encapsulate networkRanges as internal @state and add public getter/setter methods

View File

@@ -1,4 +1,14 @@
{ {
"@git.zone/tsbundle": {
"bundles": [
{
"from": "ts_web/index.ts",
"to": "dist_bundle/bundle.js",
"bundler": "esbuild",
"production": true
}
]
},
"@git.zone/cli": { "@git.zone/cli": {
"projectType": "wcc", "projectType": "wcc",
"module": { "module": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ecobridge.xyz/catalog", "name": "@ecobridge.xyz/catalog",
"version": "3.36.0", "version": "3.38.0",
"private": false, "private": false,
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.", "description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
"main": "dist_ts_web/index.js", "main": "dist_ts_web/index.js",
@@ -15,7 +15,7 @@
"author": "Lossless GmbH", "author": "Lossless GmbH",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@design.estate/dees-catalog": "^3.33.0", "@design.estate/dees-catalog": "^3.35.0",
"@design.estate/dees-domtools": "^2.3.7", "@design.estate/dees-domtools": "^2.3.7",
"@design.estate/dees-element": "^2.1.5", "@design.estate/dees-element": "^2.1.5",
"@push.rocks/smartpromise": "^4.2.3", "@push.rocks/smartpromise": "^4.2.3",
@@ -23,12 +23,12 @@
}, },
"devDependencies": { "devDependencies": {
"@design.estate/dees-wcctools": "^3.7.1", "@design.estate/dees-wcctools": "^3.7.1",
"@git.zone/tsbuild": "^4.0.2", "@git.zone/tsbuild": "^4.1.0",
"@git.zone/tsbundle": "^2.6.3", "@git.zone/tsbundle": "^2.8.1",
"@git.zone/tstest": "^3.1.4", "@git.zone/tstest": "^3.1.4",
"@git.zone/tswatch": "^2.3.13", "@git.zone/tswatch": "^2.3.13",
"@push.rocks/projectinfo": "^5.0.2", "@push.rocks/projectinfo": "^5.0.2",
"@types/node": "^25.0.3" "@types/node": "^25.0.6"
}, },
"files": [ "files": [
"ts/**/*", "ts/**/*",

1814
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@ecobridge.xyz/catalog', name: '@ecobridge.xyz/catalog',
version: '3.36.0', version: '3.38.0',
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.' description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
} }

View File

@@ -211,6 +211,10 @@ export class EcoApplauncherKeyboard extends DeesElement {
background: ${cssManager.bdTheme('hsl(220 15% 92%)', 'hsl(240 5% 28%)')}; background: ${cssManager.bdTheme('hsl(220 15% 92%)', 'hsl(240 5% 28%)')};
} }
.key:focus {
outline: none;
}
.key.special { .key.special {
background: ${cssManager.bdTheme('hsl(220 10% 88%)', 'hsl(240 5% 16%)')}; background: ${cssManager.bdTheme('hsl(220 10% 88%)', 'hsl(240 5% 16%)')};
font-size: 16px; font-size: 16px;
@@ -375,10 +379,12 @@ export class EcoApplauncherKeyboard extends DeesElement {
return html` return html`
<div <div
class="key ${type} ${widthClass} ${isActive ? 'active' : ''}" class="key ${type} ${widthClass} ${isActive ? 'active' : ''}"
tabindex="-1"
@pointerdown=${(e: PointerEvent) => this.handlePointerDown(e, config)} @pointerdown=${(e: PointerEvent) => this.handlePointerDown(e, config)}
@pointerup=${(e: PointerEvent) => this.handlePointerUp(e, config)} @pointerup=${(e: PointerEvent) => this.handlePointerUp(e, config)}
@pointerleave=${(e: PointerEvent) => this.handlePointerLeave(e, config)} @pointerleave=${(e: PointerEvent) => this.handlePointerLeave(e, config)}
@pointermove=${(e: PointerEvent) => this.handlePointerMove(e, config)} @pointermove=${(e: PointerEvent) => this.handlePointerMove(e, config)}
@mousedown=${(e: MouseEvent) => e.preventDefault()}
> >
${displayValue} ${displayValue}
</div> </div>
@@ -418,6 +424,7 @@ export class EcoApplauncherKeyboard extends DeesElement {
private handlePointerDown(e: PointerEvent, config: IKeyConfig): void { private handlePointerDown(e: PointerEvent, config: IKeyConfig): void {
e.preventDefault(); e.preventDefault();
e.stopPropagation();
const target = e.currentTarget as HTMLElement; const target = e.currentTarget as HTMLElement;
target.setPointerCapture(e.pointerId); target.setPointerCapture(e.pointerId);
@@ -483,6 +490,7 @@ export class EcoApplauncherKeyboard extends DeesElement {
private handlePointerUp(e: PointerEvent, config: IKeyConfig): void { private handlePointerUp(e: PointerEvent, config: IKeyConfig): void {
e.preventDefault(); e.preventDefault();
e.stopPropagation();
this.clearLongPressTimer(); this.clearLongPressTimer();
this.keyPreview = null; this.keyPreview = null;

View File

@@ -644,7 +644,11 @@ export class EcoApplauncher extends DeesElement {
<div class="launcher-container"> <div class="launcher-container">
${this.mode === 'login' ? '' : this.renderTopBar()} ${this.mode === 'login' ? '' : this.renderTopBar()}
${this.renderMainContent()} ${this.renderMainContent()}
<div class="keyboard-area ${this.keyboardVisible ? 'visible' : ''}"> <div
class="keyboard-area ${this.keyboardVisible ? 'visible' : ''}"
tabindex="-1"
@mousedown=${(e: MouseEvent) => e.preventDefault()}
>
<eco-applauncher-keyboard <eco-applauncher-keyboard
?visible=${this.keyboardVisible} ?visible=${this.keyboardVisible}
@key-press=${this.handleKeyboardKeyPress} @key-press=${this.handleKeyboardKeyPress}
@@ -962,7 +966,9 @@ export class EcoApplauncher extends DeesElement {
return html` return html`
<div <div
class="status-item clickable ${this.keyboardVisible ? 'active' : ''}" class="status-item clickable ${this.keyboardVisible ? 'active' : ''}"
tabindex="-1"
@click=${this.handleKeyboardToggle} @click=${this.handleKeyboardToggle}
@mousedown=${(e: MouseEvent) => e.preventDefault()}
> >
<dees-icon .icon=${'lucide:keyboard'} .iconSize=${18}></dees-icon> <dees-icon .icon=${'lucide:keyboard'} .iconSize=${18}></dees-icon>
</div> </div>
@@ -995,10 +1001,9 @@ export class EcoApplauncher extends DeesElement {
if (app.view) { if (app.view) {
this.activeView = app.view; this.activeView = app.view;
this.activeViewName = app.name; this.activeViewName = app.name;
return;
} }
// Otherwise execute the action // Execute the action (runs after setting view for initialization callbacks)
if (app.action) { if (app.action) {
app.action(); app.action();
} }

View File

@@ -0,0 +1,46 @@
import { html } from '@design.estate/dees-element';
export const demo = () => html`
<style>
.demo-container {
width: 100%;
height: 400px;
background: hsl(240 10% 4%);
border-radius: 12px;
overflow: hidden;
padding: 16px;
box-sizing: border-box;
}
.demo-info {
margin-bottom: 16px;
padding: 12px;
background: hsl(240 6% 12%);
border-radius: 8px;
color: hsl(0 0% 70%);
font-size: 14px;
}
.demo-frame {
width: 100%;
height: calc(100% - 80px);
}
</style>
<div class="demo-container">
<div class="demo-info">
The provider frame loads external web apps that implement the ecobridge provider protocol.
In this demo, no provider URL is set, so it shows the loading state.
</div>
<div class="demo-frame">
<eco-provider-frame
providerId="demo-provider"
providerName="Demo Provider"
providerUrl=""
@provider-ready=${(e: CustomEvent) => console.log('Provider ready:', e.detail)}
@provider-features-changed=${(e: CustomEvent) => console.log('Features changed:', e.detail)}
@provider-response=${(e: CustomEvent) => console.log('Response:', e.detail)}
@provider-error=${(e: CustomEvent) => console.log('Error:', e.detail)}
></eco-provider-frame>
</div>
</div>
`;

View File

@@ -0,0 +1,502 @@
import {
customElement,
DeesElement,
type TemplateResult,
html,
property,
css,
cssManager,
state,
query,
} from '@design.estate/dees-element';
import { DeesIcon } from '@design.estate/dees-catalog';
import type {
IDataProvider,
IProviderMessage,
IEcobridgeMessage,
IProviderReadyPayload,
IProviderResponsePayload,
IProviderDataOfferPayload,
IFeatureChangeRequest,
TProviderFeature,
TProviderStatus,
ISendDataPayload,
IRequestDataPayload,
} from '../interfaces/dataprovider.js';
import { demo } from './eco-provider-frame.demo.js';
// Ensure components are registered
DeesIcon;
declare global {
interface HTMLElementTagNameMap {
'eco-provider-frame': EcoProviderFrame;
}
}
@customElement('eco-provider-frame')
export class EcoProviderFrame extends DeesElement {
public static demo = demo;
public static demoGroup = 'Elements';
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
width: 100%;
height: 100%;
position: relative;
}
.provider-frame-container {
width: 100%;
height: 100%;
position: relative;
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 8%)')};
border-radius: 8px;
overflow: hidden;
}
iframe {
width: 100%;
height: 100%;
border: none;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
background: ${cssManager.bdTheme('rgba(255,255,255,0.9)', 'rgba(0,0,0,0.8)')};
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 60%)')};
z-index: 10;
}
.loading-overlay dees-icon {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.error-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
background: ${cssManager.bdTheme('rgba(255,255,255,0.95)', 'rgba(0,0,0,0.9)')};
color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 70%)')};
z-index: 10;
padding: 32px;
text-align: center;
}
.error-overlay .error-icon {
color: hsl(0 72% 50%);
}
.error-title {
font-size: 16px;
font-weight: 600;
color: hsl(0 72% 50%);
}
.error-message {
font-size: 14px;
max-width: 400px;
line-height: 1.5;
}
.retry-button {
margin-top: 8px;
padding: 10px 20px;
font-size: 14px;
font-weight: 500;
background: hsl(217 91% 60%);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background 0.15s ease;
}
.retry-button:hover {
background: hsl(217 91% 55%);
}
.disconnected-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
background: ${cssManager.bdTheme('rgba(255,255,255,0.95)', 'rgba(0,0,0,0.9)')};
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
z-index: 10;
}
`,
];
@property({ type: String })
accessor providerId = '';
@property({ type: String })
accessor providerUrl = '';
@property({ type: String })
accessor providerName = '';
@property({ type: Array })
accessor confirmedFeatures: TProviderFeature[] = [];
@property({ type: Boolean })
accessor sandboxed = true;
@state()
accessor status: TProviderStatus = 'loading';
@state()
accessor error: string | null = null;
@state()
accessor currentFeatures: TProviderFeature[] = [];
@query('iframe')
accessor iframe: HTMLIFrameElement | null;
private messageHandler: ((e: MessageEvent) => void) | null = null;
private pendingRequests: Map<string, {
resolve: (value: IProviderResponsePayload) => void;
reject: (error: Error) => void;
timeout: ReturnType<typeof setTimeout>;
}> = new Map();
async connectedCallback(): Promise<void> {
await super.connectedCallback();
this.setupMessageListener();
}
async disconnectedCallback(): Promise<void> {
await super.disconnectedCallback();
this.cleanupMessageListener();
this.clearPendingRequests();
}
private setupMessageListener(): void {
this.messageHandler = (event: MessageEvent) => {
this.handleProviderMessage(event);
};
window.addEventListener('message', this.messageHandler);
}
private cleanupMessageListener(): void {
if (this.messageHandler) {
window.removeEventListener('message', this.messageHandler);
this.messageHandler = null;
}
}
private clearPendingRequests(): void {
for (const [requestId, pending] of this.pendingRequests) {
clearTimeout(pending.timeout);
pending.reject(new Error('Provider disconnected'));
}
this.pendingRequests.clear();
}
public render(): TemplateResult {
return html`
<div class="provider-frame-container">
${this.providerUrl ? html`
<iframe
src=${this.providerUrl}
sandbox=${this.sandboxed ? 'allow-scripts allow-same-origin allow-forms' : ''}
@load=${this.handleIframeLoad}
@error=${this.handleIframeError}
></iframe>
` : ''}
${this.renderOverlay()}
</div>
`;
}
private renderOverlay(): TemplateResult | null {
switch (this.status) {
case 'loading':
return html`
<div class="loading-overlay">
<dees-icon .icon=${'lucide:loader'} .iconSize=${32}></dees-icon>
<span>Loading provider...</span>
</div>
`;
case 'error':
return html`
<div class="error-overlay">
<dees-icon class="error-icon" .icon=${'lucide:alertCircle'} .iconSize=${48}></dees-icon>
<span class="error-title">Connection Error</span>
<span class="error-message">${this.error || 'Failed to connect to provider'}</span>
<button class="retry-button" @click=${this.retry}>Retry</button>
</div>
`;
case 'disconnected':
return html`
<div class="disconnected-overlay">
<dees-icon .icon=${'lucide:unplug'} .iconSize=${32}></dees-icon>
<span>Provider disconnected</span>
</div>
`;
default:
return null;
}
}
private handleIframeLoad(): void {
// Start waiting for provider-ready message
// If not received within timeout, show error
setTimeout(() => {
if (this.status === 'loading') {
this.status = 'error';
this.error = 'Provider did not respond. Make sure it implements the ecobridge provider protocol.';
}
}, 10000); // 10 second timeout for provider to declare ready
}
private handleIframeError(): void {
this.status = 'error';
this.error = 'Failed to load provider URL';
}
private handleProviderMessage(event: MessageEvent): void {
// Verify origin matches provider URL
if (!this.providerUrl) return;
try {
const providerOrigin = new URL(this.providerUrl).origin;
if (event.origin !== providerOrigin) return;
} catch {
return;
}
const message = event.data as IProviderMessage;
if (!message || typeof message.type !== 'string') return;
// Only accept messages from this provider
if (message.providerId && message.providerId !== this.providerId) return;
switch (message.type) {
case 'provider-ready':
this.handleProviderReady(message.payload as IProviderReadyPayload);
break;
case 'provider-features':
this.handleFeaturesUpdate(message.payload as { features: TProviderFeature[] });
break;
case 'provider-response':
this.handleProviderResponse(message.requestId!, message.payload as IProviderResponsePayload);
break;
case 'provider-error':
this.handleProviderError(message.payload as { error: string });
break;
case 'data-offer':
this.handleDataOffer(message.payload as IProviderDataOfferPayload);
break;
}
}
private handleProviderReady(payload: IProviderReadyPayload): void {
this.currentFeatures = payload.features || [];
// Check for feature changes
if (this.confirmedFeatures.length > 0) {
const added = this.currentFeatures.filter(f => !this.confirmedFeatures.includes(f));
const removed = this.confirmedFeatures.filter(f => !this.currentFeatures.includes(f));
if (added.length > 0 || removed.length > 0) {
const request: IFeatureChangeRequest = {
providerId: this.providerId,
providerName: payload.name || this.providerName,
addedFeatures: added,
removedFeatures: removed,
currentFeatures: this.currentFeatures,
};
this.dispatchEvent(new CustomEvent('provider-features-changed', {
detail: { request },
bubbles: true,
composed: true,
}));
}
}
this.status = 'connected';
const provider: IDataProvider = {
id: this.providerId,
name: payload.name || this.providerName,
url: this.providerUrl,
features: this.currentFeatures,
confirmedFeatures: this.confirmedFeatures,
icon: payload.icon,
lastSeen: new Date(),
status: 'connected',
enabled: true,
};
this.dispatchEvent(new CustomEvent('provider-ready', {
detail: { provider },
bubbles: true,
composed: true,
}));
}
private handleFeaturesUpdate(payload: { features: TProviderFeature[] }): void {
const previousFeatures = [...this.currentFeatures];
this.currentFeatures = payload.features || [];
const added = this.currentFeatures.filter(f => !previousFeatures.includes(f));
const removed = previousFeatures.filter(f => !this.currentFeatures.includes(f));
if (added.length > 0 || removed.length > 0) {
const request: IFeatureChangeRequest = {
providerId: this.providerId,
providerName: this.providerName,
addedFeatures: added,
removedFeatures: removed,
currentFeatures: this.currentFeatures,
};
this.dispatchEvent(new CustomEvent('provider-features-changed', {
detail: { request },
bubbles: true,
composed: true,
}));
}
}
private handleProviderResponse(requestId: string, response: IProviderResponsePayload): void {
const pending = this.pendingRequests.get(requestId);
if (pending) {
clearTimeout(pending.timeout);
this.pendingRequests.delete(requestId);
pending.resolve(response);
}
this.dispatchEvent(new CustomEvent('provider-response', {
detail: { requestId, response },
bubbles: true,
composed: true,
}));
}
private handleProviderError(payload: { error: string }): void {
this.dispatchEvent(new CustomEvent('provider-error', {
detail: { error: payload.error },
bubbles: true,
composed: true,
}));
}
private handleDataOffer(offer: IProviderDataOfferPayload): void {
this.dispatchEvent(new CustomEvent('provider-data-offer', {
detail: { offer },
bubbles: true,
composed: true,
}));
}
// Public API
public retry(): void {
this.status = 'loading';
this.error = null;
if (this.iframe) {
this.iframe.src = this.providerUrl;
}
}
public sendMessage(message: IEcobridgeMessage): void {
if (!this.iframe?.contentWindow) {
console.warn('Cannot send message: iframe not ready');
return;
}
try {
const origin = new URL(this.providerUrl).origin;
this.iframe.contentWindow.postMessage(message, origin);
} catch (e) {
console.error('Failed to send message to provider:', e);
}
}
public async sendData(data: ISendDataPayload, timeoutMs = 30000): Promise<IProviderResponsePayload> {
const requestId = crypto.randomUUID();
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
this.pendingRequests.delete(requestId);
reject(new Error('Request timeout'));
}, timeoutMs);
this.pendingRequests.set(requestId, { resolve, reject, timeout });
this.sendMessage({
type: 'send-data',
requestId,
payload: data,
});
});
}
public async requestData(request: IRequestDataPayload, timeoutMs = 30000): Promise<IProviderResponsePayload> {
const requestId = crypto.randomUUID();
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
this.pendingRequests.delete(requestId);
reject(new Error('Request timeout'));
}, timeoutMs);
this.pendingRequests.set(requestId, { resolve, reject, timeout });
this.sendMessage({
type: 'request-data',
requestId,
payload: request,
});
});
}
public ping(): void {
this.sendMessage({
type: 'ping',
requestId: crypto.randomUUID(),
});
}
public hasFeature(feature: TProviderFeature): boolean {
return this.confirmedFeatures.includes(feature);
}
public getStatus(): TProviderStatus {
return this.status;
}
}

View File

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

View File

@@ -6,3 +6,4 @@ export * from './00group-applauncher/index.js';
// Standalone Components // Standalone Components
export * from './eco-screensaver/index.js'; export * from './eco-screensaver/index.js';
export * from './eco-provider-frame/index.js';

View File

@@ -0,0 +1,158 @@
/**
* Data Provider System Interfaces
*
* Data providers are external web apps that can be registered to provide
* or receive data from ecobridge applications. They communicate via
* postMessage API in sandboxed iframes.
*/
// Provider feature types - what capabilities a provider can offer
export type TProviderFeature =
| 'scan-destination' // Can receive scanned documents
| 'media-source' // Can provide media URLs for playback
| 'document-storage' // Can store/retrieve documents
| 'print-destination'; // Can receive print jobs
// Provider connection status
export type TProviderStatus = 'connected' | 'disconnected' | 'loading' | 'error';
/**
* Data Provider configuration and state
*/
export interface IDataProvider {
id: string;
name: string;
url: string;
features: TProviderFeature[]; // Features declared by provider
confirmedFeatures: TProviderFeature[]; // Features user has approved
icon?: string; // Base64 data URL or icon URL
lastSeen: Date;
status: TProviderStatus;
enabled: boolean;
}
/**
* Provider registration payload (sent by provider on ready)
*/
export interface IProviderRegistration {
name: string;
features: TProviderFeature[];
icon?: string;
version?: string;
}
/**
* Message types from Provider to Ecobridge
*/
export type TProviderToEcobridgeMessageType =
| 'provider-ready' // Provider loaded and declaring features
| 'provider-features' // Provider updating its features
| 'provider-response' // Response to an ecobridge request
| 'provider-error' // Error occurred in provider
| 'data-offer'; // Provider offering data (e.g., media URL)
/**
* Message from Provider to Ecobridge
*/
export interface IProviderMessage {
type: TProviderToEcobridgeMessageType;
providerId: string;
requestId?: string; // For responses to specific requests
payload: unknown;
}
/**
* Provider ready message payload
*/
export interface IProviderReadyPayload {
name: string;
features: TProviderFeature[];
icon?: string;
version?: string;
}
/**
* Provider response payload
*/
export interface IProviderResponsePayload {
success: boolean;
data?: unknown;
error?: string;
}
/**
* Provider data offer payload (e.g., for media-source)
*/
export interface IProviderDataOfferPayload {
type: 'media-url' | 'document' | 'file-list';
data: unknown;
metadata?: Record<string, unknown>;
}
/**
* Message types from Ecobridge to Provider
*/
export type TEcobridgeToProviderMessageType =
| 'request-features' // Ask provider to declare features
| 'send-data' // Send data to provider (e.g., scan)
| 'request-data' // Request data from provider (e.g., media URL)
| 'ping'; // Health check
/**
* Message from Ecobridge to Provider
*/
export interface IEcobridgeMessage {
type: TEcobridgeToProviderMessageType;
requestId: string;
payload?: unknown;
}
/**
* Send data payload (e.g., sending scan to provider)
*/
export interface ISendDataPayload {
dataType: 'scan' | 'document' | 'print-job';
data: string; // Base64 encoded data
format: string; // File format (pdf, jpeg, etc.)
filename?: string;
metadata?: Record<string, unknown>;
}
/**
* Request data payload (e.g., requesting media from provider)
*/
export interface IRequestDataPayload {
dataType: 'media-url' | 'document' | 'file-list';
filter?: Record<string, unknown>;
}
/**
* Feature change event - when provider's features differ from confirmed
*/
export interface IFeatureChangeRequest {
providerId: string;
providerName: string;
addedFeatures: TProviderFeature[];
removedFeatures: TProviderFeature[];
currentFeatures: TProviderFeature[];
}
/**
* Provider store schema for persistence
*/
export interface IProviderStore {
providers: IDataProvider[];
lastUpdated: Date;
}
/**
* Events dispatched by eco-provider-frame
*/
export interface IProviderFrameEvents {
'provider-ready': CustomEvent<{ provider: IDataProvider }>;
'provider-features-changed': CustomEvent<{ request: IFeatureChangeRequest }>;
'provider-response': CustomEvent<{ requestId: string; response: IProviderResponsePayload }>;
'provider-data-offer': CustomEvent<{ offer: IProviderDataOfferPayload }>;
'provider-error': CustomEvent<{ error: string }>;
'provider-disconnected': CustomEvent<{ providerId: string }>;
}

View File

@@ -3,3 +3,4 @@ export * from './appbarmenuitem.js';
export * from './menugroup.js'; export * from './menugroup.js';
export * from './appconfig.js'; export * from './appconfig.js';
export * from './secondarymenu.js'; export * from './secondarymenu.js';
export * from './dataprovider.js';

View File

@@ -0,0 +1,34 @@
import { html } from '@design.estate/dees-element';
export const demo = () => html`
<style>
.demo-container {
width: 100%;
height: 100%;
min-height: 600px;
background: hsl(240 10% 4%);
border-radius: 12px;
overflow: hidden;
}
</style>
<div class="demo-container">
<eco-view-browser
.bookmarks=${[
{ id: 'bm-1', title: 'Google', url: 'https://www.google.com', createdAt: new Date() },
{ id: 'bm-2', title: 'GitHub', url: 'https://github.com', createdAt: new Date() },
{ id: 'bm-3', title: 'Stack Overflow', url: 'https://stackoverflow.com', createdAt: new Date() },
{ id: 'bm-4', title: 'MDN Web Docs', url: 'https://developer.mozilla.org', createdAt: new Date() },
]}
.showBookmarksBar=${true}
@navigate=${(e: CustomEvent) => console.log('Navigate:', e.detail)}
@bookmark-add=${(e: CustomEvent) => console.log('Bookmark add:', e.detail)}
@bookmark-remove=${(e: CustomEvent) => console.log('Bookmark remove:', e.detail)}
@browser-back=${() => console.log('Browser back')}
@browser-forward=${() => console.log('Browser forward')}
@browser-refresh=${() => console.log('Browser refresh')}
@browser-stop=${() => console.log('Browser stop')}
@new-tab=${() => console.log('New tab')}
@open-devtools=${() => console.log('Open devtools')}
></eco-view-browser>
</div>
`;

View File

@@ -0,0 +1,959 @@
import {
customElement,
DeesElement,
type TemplateResult,
html,
property,
css,
cssManager,
state,
} from '@design.estate/dees-element';
import { DeesIcon } from '@design.estate/dees-catalog';
import { demo } from './eco-view-browser.demo.js';
// Ensure components are registered
DeesIcon;
declare global {
interface HTMLElementTagNameMap {
'eco-view-browser': EcoViewBrowser;
}
}
// Types
export interface IBookmark {
id: string;
title: string;
url: string;
favicon?: string;
createdAt: Date;
}
export interface IBrowserState {
url: string;
title: string;
canGoBack: boolean;
canGoForward: boolean;
isLoading: boolean;
}
export type TBrowserPanel = 'browser' | 'bookmarks';
@customElement('eco-view-browser')
export class EcoViewBrowser 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('#ffffff', 'hsl(240 6% 8%)')};
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;
}
.browser-container {
display: flex;
flex-direction: column;
height: 100%;
}
.toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: ${cssManager.bdTheme('#f5f5f7', 'hsl(240 6% 10%)')};
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 15%)')};
}
.nav-buttons {
display: flex;
gap: 4px;
}
.nav-button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
border-radius: 6px;
background: transparent;
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')};
cursor: pointer;
transition: all 0.15s ease;
}
.nav-button:hover:not(:disabled) {
background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')};
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
}
.nav-button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.nav-button.loading dees-icon {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.url-container {
flex: 1;
display: flex;
align-items: center;
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 15%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(240 5% 25%)')};
border-radius: 8px;
padding: 0 12px;
height: 36px;
transition: border-color 0.15s ease;
}
.url-container:focus-within {
border-color: hsl(217 91% 60%);
}
.url-icon {
margin-right: 8px;
opacity: 0.5;
}
.url-input {
flex: 1;
border: none;
background: transparent;
font-size: 14px;
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 95%)')};
outline: none;
}
.url-input::placeholder {
color: ${cssManager.bdTheme('hsl(0 0% 60%)', 'hsl(0 0% 50%)')};
}
.bookmark-button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
border-radius: 6px;
background: transparent;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
cursor: pointer;
transition: all 0.15s ease;
}
.bookmark-button:hover {
background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')};
color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')};
}
.bookmark-button.bookmarked {
color: hsl(45 93% 47%);
}
.bookmark-button.bookmarked:hover {
color: hsl(45 93% 40%);
}
.menu-button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
border-radius: 6px;
background: transparent;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
cursor: pointer;
transition: all 0.15s ease;
}
.menu-button:hover {
background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')};
color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')};
}
.webview-container {
flex: 1;
position: relative;
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 5%)')};
overflow: hidden;
}
.webview-wrapper {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
webview {
width: 100%;
height: 100%;
border: none;
display: flex;
}
.placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 50%)')};
gap: 16px;
}
.placeholder dees-icon {
opacity: 0.3;
}
.placeholder-text {
font-size: 14px;
}
.placeholder-hint {
font-size: 12px;
opacity: 0.7;
}
.bookmarks-bar {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 12px;
background: ${cssManager.bdTheme('#fafafa', 'hsl(240 6% 9%)')};
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 92%)', 'hsl(240 5% 13%)')};
overflow-x: auto;
}
.bookmarks-bar::-webkit-scrollbar {
height: 4px;
}
.bookmarks-bar::-webkit-scrollbar-track {
background: transparent;
}
.bookmarks-bar::-webkit-scrollbar-thumb {
background: ${cssManager.bdTheme('hsl(0 0% 80%)', 'hsl(240 5% 25%)')};
border-radius: 2px;
}
.bookmark-chip {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 15%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 20%)')};
border-radius: 6px;
font-size: 12px;
color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')};
cursor: pointer;
white-space: nowrap;
transition: all 0.15s ease;
}
.bookmark-chip:hover {
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(240 5% 18%)')};
border-color: ${cssManager.bdTheme('hsl(0 0% 80%)', 'hsl(240 5% 25%)')};
}
.bookmark-chip .favicon {
width: 14px;
height: 14px;
border-radius: 2px;
}
.dropdown-menu {
position: absolute;
top: 100%;
right: 0;
margin-top: 4px;
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 15%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 25%)')};
border-radius: 8px;
box-shadow: 0 4px 16px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')};
min-width: 180px;
z-index: 100;
overflow: hidden;
}
.dropdown-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
font-size: 14px;
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
cursor: pointer;
transition: background 0.1s ease;
}
.dropdown-item:hover {
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(240 5% 20%)')};
}
.dropdown-item.destructive {
color: hsl(0 72% 50%);
}
.dropdown-item.destructive:hover {
background: hsl(0 72% 95%);
}
.dropdown-divider {
height: 1px;
background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 20%)')};
margin: 4px 0;
}
.menu-container {
position: relative;
}
/* Bookmarks panel styles */
.bookmarks-panel {
padding: 32px 48px;
overflow-y: auto;
height: 100%;
}
.panel-header {
margin-bottom: 24px;
}
.panel-title {
font-size: 28px;
font-weight: 600;
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
margin: 0 0 8px 0;
}
.panel-description {
font-size: 14px;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
margin: 0;
}
.bookmarks-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.bookmark-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 12%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')};
border-radius: 8px;
cursor: pointer;
transition: all 0.15s ease;
}
.bookmark-item:hover {
border-color: hsl(217 91% 60%);
transform: translateX(2px);
}
.bookmark-item .favicon {
width: 20px;
height: 20px;
border-radius: 4px;
flex-shrink: 0;
}
.bookmark-info {
flex: 1;
min-width: 0;
}
.bookmark-title {
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bookmark-url {
font-size: 12px;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bookmark-delete {
opacity: 0;
transition: opacity 0.15s ease;
}
.bookmark-item:hover .bookmark-delete {
opacity: 1;
}
.bookmark-delete-btn {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: none;
border-radius: 4px;
background: transparent;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
cursor: pointer;
}
.bookmark-delete-btn:hover {
background: hsl(0 72% 95%);
color: hsl(0 72% 50%);
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
}
.empty-state dees-icon {
margin-bottom: 16px;
opacity: 0.4;
}
.empty-state p {
font-size: 14px;
margin: 0;
}
.loading-bar {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg,
hsl(217 91% 60%) 0%,
hsl(217 91% 70%) 50%,
hsl(217 91% 60%) 100%
);
background-size: 200% 100%;
animation: loading 1.5s linear infinite;
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
`,
];
@property({ type: Array })
accessor bookmarks: IBookmark[] = [];
@property({ type: Boolean })
accessor showBookmarksBar = true;
@property({ type: String })
accessor defaultUrl = '';
@state()
accessor activePanel: TBrowserPanel = 'browser';
@state()
accessor currentUrl = '';
@state()
accessor inputUrl = '';
@state()
accessor pageTitle = '';
@state()
accessor canGoBack = false;
@state()
accessor canGoForward = false;
@state()
accessor isLoading = false;
@state()
accessor showMenu = false;
private isElectron = false;
private webviewEventsBound = false;
async connectedCallback(): Promise<void> {
await super.connectedCallback();
// Check if we're in Electron - webview tag only works in Electron
// Since nodeIntegration is disabled, we check for the existence of electronAPI
// or if we're in a file:// protocol (Electron loads from file://)
this.isElectron = typeof window !== 'undefined' && (
typeof (window as any).electronAPI !== 'undefined' ||
window.location.protocol === 'file:' ||
navigator.userAgent.includes('Electron')
);
if (this.defaultUrl) {
this.inputUrl = this.defaultUrl;
}
}
protected updated(changedProperties: Map<string, any>): void {
super.updated(changedProperties);
// Bind webview events after it's rendered
this.bindWebviewEvents();
}
private bindWebviewEvents(): void {
if (!this.isElectron || this.webviewEventsBound) return;
const webview = this.shadowRoot?.querySelector('webview') as any;
if (!webview) return;
this.webviewEventsBound = true;
webview.addEventListener('did-start-loading', () => {
this.isLoading = true;
});
webview.addEventListener('did-stop-loading', () => {
this.isLoading = false;
this.updateNavigationState();
});
webview.addEventListener('did-navigate', (e: any) => {
this.currentUrl = e.url || this.currentUrl;
this.inputUrl = this.currentUrl;
this.updateNavigationState();
});
webview.addEventListener('did-navigate-in-page', (e: any) => {
if (e.isMainFrame) {
this.currentUrl = e.url || this.currentUrl;
this.inputUrl = this.currentUrl;
}
});
webview.addEventListener('page-title-updated', (e: any) => {
this.pageTitle = e.title || '';
});
webview.addEventListener('page-favicon-updated', (e: any) => {
// Could store favicon for bookmarks
});
webview.addEventListener('dom-ready', () => {
this.updateNavigationState();
});
console.log('Webview events bound');
}
public render(): TemplateResult {
return html`
<div class="browser-container">
${this.renderToolbar()}
${this.showBookmarksBar && this.bookmarks.length > 0 ? this.renderBookmarksBar() : ''}
${this.activePanel === 'browser' ? this.renderBrowserContent() : this.renderBookmarksPanel()}
</div>
`;
}
private renderToolbar(): TemplateResult {
return html`
<div class="toolbar">
<div class="nav-buttons">
<button
class="nav-button"
title="Back"
?disabled=${!this.canGoBack}
@click=${this.handleBack}
>
<dees-icon .icon=${'lucide:arrowLeft'} .iconSize=${18}></dees-icon>
</button>
<button
class="nav-button"
title="Forward"
?disabled=${!this.canGoForward}
@click=${this.handleForward}
>
<dees-icon .icon=${'lucide:arrowRight'} .iconSize=${18}></dees-icon>
</button>
<button
class="nav-button ${this.isLoading ? 'loading' : ''}"
title=${this.isLoading ? 'Stop' : 'Refresh'}
@click=${this.isLoading ? this.handleStop : this.handleRefresh}
>
<dees-icon
.icon=${this.isLoading ? 'lucide:loader' : 'lucide:refreshCw'}
.iconSize=${18}
></dees-icon>
</button>
</div>
<div class="url-container">
<dees-icon class="url-icon" .icon=${'lucide:globe'} .iconSize=${16}></dees-icon>
<input
class="url-input"
type="text"
placeholder="Enter URL or search..."
.value=${this.inputUrl}
@input=${(e: InputEvent) => this.inputUrl = (e.target as HTMLInputElement).value}
@keydown=${this.handleUrlKeydown}
/>
</div>
<button
class="bookmark-button ${this.isCurrentPageBookmarked() ? 'bookmarked' : ''}"
title=${this.isCurrentPageBookmarked() ? 'Remove bookmark' : 'Add bookmark'}
@click=${this.toggleBookmark}
?disabled=${!this.currentUrl}
>
<dees-icon
.icon=${this.isCurrentPageBookmarked() ? 'lucide:star' : 'lucide:star'}
.iconSize=${18}
></dees-icon>
</button>
<div class="menu-container">
<button
class="menu-button"
title="Menu"
@click=${() => this.showMenu = !this.showMenu}
>
<dees-icon .icon=${'lucide:moreVertical'} .iconSize=${18}></dees-icon>
</button>
${this.showMenu ? this.renderMenu() : ''}
</div>
</div>
`;
}
private renderMenu(): TemplateResult {
return html`
<div class="dropdown-menu" @mouseleave=${() => this.showMenu = false}>
<div class="dropdown-item" @click=${() => { this.activePanel = 'bookmarks'; this.showMenu = false; }}>
<dees-icon .icon=${'lucide:bookmark'} .iconSize=${18}></dees-icon>
Bookmarks
</div>
<div class="dropdown-item" @click=${this.handleNewTab}>
<dees-icon .icon=${'lucide:plus'} .iconSize=${18}></dees-icon>
New Tab
</div>
<div class="dropdown-divider"></div>
<div class="dropdown-item" @click=${this.handleOpenDevTools}>
<dees-icon .icon=${'lucide:code'} .iconSize=${18}></dees-icon>
Developer Tools
</div>
<div class="dropdown-divider"></div>
<div class="dropdown-item" @click=${this.handleCopyUrl}>
<dees-icon .icon=${'lucide:copy'} .iconSize=${18}></dees-icon>
Copy URL
</div>
</div>
`;
}
private renderBookmarksBar(): TemplateResult {
return html`
<div class="bookmarks-bar">
${this.bookmarks.slice(0, 10).map(bookmark => html`
<div
class="bookmark-chip"
title=${bookmark.url}
@click=${() => this.navigate(bookmark.url)}
>
${bookmark.favicon
? html`<img class="favicon" src=${bookmark.favicon} alt="" />`
: html`<dees-icon .icon=${'lucide:globe'} .iconSize=${14}></dees-icon>`
}
${bookmark.title || new URL(bookmark.url).hostname}
</div>
`)}
</div>
`;
}
private renderBrowserContent(): TemplateResult {
return html`
<div class="webview-container">
${this.isLoading ? html`<div class="loading-bar"></div>` : ''}
<div class="webview-wrapper">
${this.currentUrl ? this.renderWebview() : this.renderPlaceholder()}
</div>
</div>
`;
}
private renderWebview(): TemplateResult {
// In Electron, we use webview tag. In browser, we show a notice.
if (this.isElectron) {
// Reset event binding flag when URL changes so events get rebound
// Note: webview events are bound in updated() lifecycle
return html`
<webview
src=${this.currentUrl}
style="width: 100%; height: 100%;"
allowpopups
></webview>
`;
} else {
// Fallback for non-Electron environment (demo/testing)
return html`
<div class="placeholder">
<dees-icon .icon=${'lucide:monitor'} .iconSize=${48}></dees-icon>
<span class="placeholder-text">Webview requires Electron environment</span>
<span class="placeholder-hint">URL: ${this.currentUrl}</span>
</div>
`;
}
}
private renderPlaceholder(): TemplateResult {
return html`
<div class="placeholder">
<dees-icon .icon=${'lucide:globe'} .iconSize=${64}></dees-icon>
<span class="placeholder-text">Enter a URL to start browsing</span>
<span class="placeholder-hint">or select a bookmark</span>
</div>
`;
}
private renderBookmarksPanel(): TemplateResult {
return html`
<div class="bookmarks-panel">
<div class="panel-header">
<h2 class="panel-title">Bookmarks</h2>
<p class="panel-description">Your saved websites</p>
</div>
${this.bookmarks.length > 0
? html`
<div class="bookmarks-list">
${this.bookmarks.map(bookmark => html`
<div class="bookmark-item" @click=${() => this.handleBookmarkClick(bookmark)}>
${bookmark.favicon
? html`<img class="favicon" src=${bookmark.favicon} alt="" />`
: html`<dees-icon .icon=${'lucide:globe'} .iconSize=${20}></dees-icon>`
}
<div class="bookmark-info">
<div class="bookmark-title">${bookmark.title || bookmark.url}</div>
<div class="bookmark-url">${bookmark.url}</div>
</div>
<div class="bookmark-delete">
<button
class="bookmark-delete-btn"
title="Delete bookmark"
@click=${(e: Event) => { e.stopPropagation(); this.removeBookmark(bookmark.id); }}
>
<dees-icon .icon=${'lucide:trash2'} .iconSize=${16}></dees-icon>
</button>
</div>
</div>
`)}
</div>
`
: html`
<div class="empty-state">
<dees-icon .icon=${'lucide:bookmark'} .iconSize=${48}></dees-icon>
<p>No bookmarks yet. Star a page to save it here.</p>
</div>
`
}
</div>
`;
}
// Navigation methods
private navigate(url: string): void {
if (!url) return;
// Add protocol if missing
if (!/^https?:\/\//i.test(url)) {
// Check if it looks like a domain
if (/^[a-zA-Z0-9][a-zA-Z0-9-]*\.[a-zA-Z]{2,}/.test(url)) {
url = 'https://' + url;
} else {
// Treat as search query
url = `https://www.google.com/search?q=${encodeURIComponent(url)}`;
}
}
this.currentUrl = url;
this.inputUrl = url;
this.isLoading = true;
this.dispatchEvent(new CustomEvent('navigate', {
detail: { url },
bubbles: true,
composed: true,
}));
}
private handleUrlKeydown(e: KeyboardEvent): void {
if (e.key === 'Enter') {
this.navigate(this.inputUrl);
}
}
private get webview(): any {
return this.shadowRoot?.querySelector('webview');
}
private handleBack(): void {
const wv = this.webview;
if (wv && this.isElectron) {
wv.goBack();
}
this.dispatchEvent(new CustomEvent('browser-back', { bubbles: true, composed: true }));
}
private handleForward(): void {
const wv = this.webview;
if (wv && this.isElectron) {
wv.goForward();
}
this.dispatchEvent(new CustomEvent('browser-forward', { bubbles: true, composed: true }));
}
private handleRefresh(): void {
const wv = this.webview;
if (wv && this.isElectron) {
wv.reload();
}
this.dispatchEvent(new CustomEvent('browser-refresh', { bubbles: true, composed: true }));
}
private handleStop(): void {
const wv = this.webview;
if (wv && this.isElectron) {
wv.stop();
}
this.isLoading = false;
this.dispatchEvent(new CustomEvent('browser-stop', { bubbles: true, composed: true }));
}
private handleFaviconUpdate(e: any): void {
// Could update current page favicon for bookmarks
}
private updateNavigationState(): void {
if (this.webview && this.isElectron) {
this.canGoBack = (this.webview as any).canGoBack?.() || false;
this.canGoForward = (this.webview as any).canGoForward?.() || false;
}
}
// Bookmark methods
private isCurrentPageBookmarked(): boolean {
return this.bookmarks.some(b => b.url === this.currentUrl);
}
private toggleBookmark(): void {
if (!this.currentUrl) return;
if (this.isCurrentPageBookmarked()) {
const bookmark = this.bookmarks.find(b => b.url === this.currentUrl);
if (bookmark) {
this.removeBookmark(bookmark.id);
}
} else {
this.addBookmark();
}
}
private addBookmark(): void {
const bookmark: IBookmark = {
id: crypto.randomUUID(),
title: this.pageTitle || this.currentUrl,
url: this.currentUrl,
createdAt: new Date(),
};
this.dispatchEvent(new CustomEvent('bookmark-add', {
detail: { bookmark },
bubbles: true,
composed: true,
}));
}
private removeBookmark(id: string): void {
this.dispatchEvent(new CustomEvent('bookmark-remove', {
detail: { id },
bubbles: true,
composed: true,
}));
}
private handleBookmarkClick(bookmark: IBookmark): void {
this.navigate(bookmark.url);
this.activePanel = 'browser';
}
// Menu actions
private handleNewTab(): void {
this.showMenu = false;
this.dispatchEvent(new CustomEvent('new-tab', { bubbles: true, composed: true }));
}
private handleOpenDevTools(): void {
this.showMenu = false;
if (this.webview && this.isElectron) {
(this.webview as any).openDevTools();
}
this.dispatchEvent(new CustomEvent('open-devtools', { bubbles: true, composed: true }));
}
private handleCopyUrl(): void {
this.showMenu = false;
if (this.currentUrl) {
navigator.clipboard.writeText(this.currentUrl);
}
}
// Public API
public goTo(url: string): void {
this.navigate(url);
}
public setBookmarks(bookmarks: IBookmark[]): void {
this.bookmarks = bookmarks;
}
public getState(): IBrowserState {
return {
url: this.currentUrl,
title: this.pageTitle,
canGoBack: this.canGoBack,
canGoForward: this.canGoForward,
isLoading: this.isLoading,
};
}
}

View File

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

View File

@@ -0,0 +1,30 @@
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-scan
.scanners=${[
{ id: 'scanner-1', name: 'HP ScanJet Pro', address: '192.168.1.100', status: 'online', capabilities: { resolutions: [150, 300, 600], formats: ['pdf', 'jpeg', 'png'], colorModes: ['color', 'grayscale'], sources: ['flatbed', 'adf'] } },
{ id: 'scanner-2', name: 'Canon imageFORMULA', address: '192.168.1.101', status: 'online', capabilities: { resolutions: [200, 300, 400, 600], formats: ['pdf', 'jpeg', 'tiff'], colorModes: ['color', 'grayscale', 'blackwhite'], sources: ['flatbed'] } },
{ id: 'scanner-3', name: 'Epson WorkForce', address: '192.168.1.102', status: 'offline' },
]}
.providers=${[
{ id: 'provider-1', name: 'Cloud Storage', icon: 'lucide:cloud' },
{ id: 'provider-2', name: 'Google Drive', icon: 'lucide:hardDrive' },
]}
@scanner-select=${(e: CustomEvent) => console.log('Scanner selected:', e.detail)}
@scan-request=${(e: CustomEvent) => console.log('Scan requested:', e.detail)}
@save-local=${(e: CustomEvent) => console.log('Save local:', e.detail)}
@send-to-provider=${(e: CustomEvent) => console.log('Send to provider:', e.detail)}
></eco-view-scan>
</div>
`;

View File

@@ -0,0 +1,839 @@
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 } from '../../elements/interfaces/secondarymenu.js';
import { demo } from './eco-view-scan.demo.js';
// Ensure components are registered
DeesAppuiSecondarymenu;
DeesIcon;
declare global {
interface HTMLElementTagNameMap {
'eco-view-scan': EcoViewScan;
}
}
// Types
export type TScanFormat = 'pdf' | 'jpeg' | 'png' | 'tiff';
export type TScanColorMode = 'color' | 'grayscale' | 'blackwhite';
export type TScanSource = 'flatbed' | 'adf' | 'adf-duplex';
export type TScanPanel = 'scan' | 'history' | 'settings';
export interface IScanSettings {
format: TScanFormat;
resolution: number;
colorMode: TScanColorMode;
source: TScanSource;
}
export interface IScannedDocument {
id: string;
timestamp: Date;
format: TScanFormat;
data: string; // base64
thumbnail?: string;
size: number;
name?: string;
}
export interface IScannerInfo {
id: string;
name: string;
address: string;
status: 'online' | 'offline' | 'busy' | 'error';
capabilities?: {
resolutions: number[];
formats: TScanFormat[];
colorModes: TScanColorMode[];
sources: TScanSource[];
};
}
export interface IDataProviderInfo {
id: string;
name: string;
icon?: string;
}
@customElement('eco-view-scan')
export class EcoViewScan 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;
}
.scan-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;
display: flex;
flex-direction: column;
}
.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%)')};
}
.scanner-selector {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 24px;
}
.scanner-selector label {
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')};
}
.scanner-selector select {
flex: 1;
max-width: 300px;
padding: 10px 14px;
font-size: 14px;
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(240 5% 25%)')};
border-radius: 8px;
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 15%)')};
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 95%)')};
cursor: pointer;
}
.scanner-selector select:focus {
outline: none;
border-color: hsl(217 91% 60%);
}
.preview-area {
flex: 1;
min-height: 300px;
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 12%)')};
border: 2px dashed ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(240 5% 25%)')};
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
margin-bottom: 24px;
}
.preview-area.has-image {
border-style: solid;
}
.preview-placeholder {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
color: ${cssManager.bdTheme('hsl(0 0% 60%)', 'hsl(0 0% 50%)')};
}
.preview-placeholder dees-icon {
opacity: 0.5;
}
.preview-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.action-bar {
display: flex;
gap: 12px;
margin-bottom: 32px;
}
.action-button {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
font-size: 14px;
font-weight: 500;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.15s ease;
}
.action-button.primary {
background: hsl(217 91% 60%);
color: white;
}
.action-button.primary:hover:not(:disabled) {
background: hsl(217 91% 55%);
}
.action-button.secondary {
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(240 5% 20%)')};
color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 90%)')};
}
.action-button.secondary:hover:not(:disabled) {
background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 25%)')};
}
.action-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.action-button.scanning dees-icon {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.dropdown-container {
position: relative;
}
.dropdown-menu {
position: absolute;
bottom: 100%;
left: 0;
margin-bottom: 4px;
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 15%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 25%)')};
border-radius: 8px;
box-shadow: 0 4px 16px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')};
min-width: 200px;
z-index: 100;
overflow: hidden;
}
.dropdown-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
font-size: 14px;
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
cursor: pointer;
transition: background 0.1s ease;
}
.dropdown-item:hover {
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(240 5% 20%)')};
}
.dropdown-item dees-icon {
opacity: 0.7;
}
.settings-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;
padding: 20px;
margin-bottom: 20px;
}
.settings-section-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 16px;
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
}
.setting-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 92%)', 'hsl(240 5% 18%)')};
}
.setting-row:last-child {
border-bottom: none;
}
.setting-label {
font-size: 14px;
color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')};
}
.setting-control select {
padding: 8px 12px;
font-size: 14px;
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(240 5% 25%)')};
border-radius: 6px;
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 15%)')};
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 95%)')};
cursor: pointer;
}
.history-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 16px;
}
.history-item {
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 12%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')};
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: all 0.15s ease;
}
.history-item:hover {
border-color: hsl(217 91% 60%);
transform: translateY(-2px);
box-shadow: 0 4px 12px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')};
}
.history-thumbnail {
width: 100%;
aspect-ratio: 1;
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(240 5% 18%)')};
display: flex;
align-items: center;
justify-content: center;
}
.history-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
}
.history-info {
padding: 10px;
}
.history-name {
font-size: 13px;
font-weight: 500;
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.history-date {
font-size: 12px;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
}
.empty-state dees-icon {
margin-bottom: 16px;
opacity: 0.4;
}
.empty-state p {
font-size: 14px;
margin: 0;
}
`,
];
@property({ type: Array })
accessor scanners: IScannerInfo[] = [];
@property({ type: Array })
accessor providers: IDataProviderInfo[] = [];
@state()
accessor activePanel: TScanPanel = 'scan';
@state()
accessor selectedScannerId: string | null = null;
@state()
accessor isScanning = false;
@state()
accessor currentPreview: string | null = null;
@state()
accessor currentDocument: IScannedDocument | null = null;
@state()
accessor scanHistory: IScannedDocument[] = [];
@state()
accessor settings: IScanSettings = {
format: 'pdf',
resolution: 300,
colorMode: 'color',
source: 'flatbed',
};
@state()
accessor showSendToMenu = false;
private get selectedScanner(): IScannerInfo | null {
return this.scanners.find(s => s.id === this.selectedScannerId) || null;
}
private get availableResolutions(): number[] {
return this.selectedScanner?.capabilities?.resolutions || [150, 300, 600];
}
private get availableFormats(): TScanFormat[] {
return this.selectedScanner?.capabilities?.formats || ['pdf', 'jpeg', 'png'];
}
private get availableColorModes(): TScanColorMode[] {
return this.selectedScanner?.capabilities?.colorModes || ['color', 'grayscale'];
}
private get availableSources(): TScanSource[] {
return this.selectedScanner?.capabilities?.sources || ['flatbed'];
}
public render(): TemplateResult {
return html`
<div class="scan-container">
<dees-appui-secondarymenu
.menuGroups=${this.getMenuGroups()}
.selectedKey=${this.activePanel}
></dees-appui-secondarymenu>
<div class="content">
${this.renderContent()}
</div>
</div>
`;
}
private getMenuGroups(): ISecondaryMenuGroup[] {
return [
{
name: 'SCAN',
iconName: 'lucide:scan',
items: [
{
key: 'scan',
label: 'New Scan',
iconName: 'lucide:plus',
action: () => this.activePanel = 'scan',
},
],
},
{
name: 'HISTORY',
iconName: 'lucide:history',
items: [
{
key: 'history',
label: 'Recent Scans',
iconName: 'lucide:clock',
action: () => this.activePanel = 'history',
badge: this.scanHistory.length > 0 ? this.scanHistory.length : undefined,
},
],
},
{
name: 'OPTIONS',
iconName: 'lucide:settings',
items: [
{
key: 'settings',
label: 'Scan Settings',
iconName: 'lucide:sliders',
action: () => this.activePanel = 'settings',
},
],
},
];
}
private renderContent(): TemplateResult {
switch (this.activePanel) {
case 'scan':
return this.renderScanPanel();
case 'history':
return this.renderHistoryPanel();
case 'settings':
return this.renderSettingsPanel();
default:
return this.renderScanPanel();
}
}
private renderScanPanel(): TemplateResult {
return html`
<div class="panel-header">
<div class="panel-header-left">
<h2 class="panel-title">Scan Document</h2>
<p class="panel-description">Select a scanner and start scanning</p>
</div>
</div>
<div class="scanner-selector">
<label>Scanner:</label>
<select
.value=${this.selectedScannerId || ''}
@change=${(e: Event) => this.handleScannerChange(e)}
>
<option value="">Select a scanner...</option>
${this.scanners.map(scanner => html`
<option value=${scanner.id} ?selected=${scanner.id === this.selectedScannerId}>
${scanner.name} ${scanner.status !== 'online' ? `(${scanner.status})` : ''}
</option>
`)}
</select>
</div>
<div class="preview-area ${this.currentPreview ? 'has-image' : ''}">
${this.currentPreview
? html`<img class="preview-image" src=${this.currentPreview} alt="Scan preview" />`
: html`
<div class="preview-placeholder">
<dees-icon .icon=${'lucide:scan'} .iconSize=${48}></dees-icon>
<span>Scan preview will appear here</span>
</div>
`
}
</div>
<div class="action-bar">
<button
class="action-button primary ${this.isScanning ? 'scanning' : ''}"
?disabled=${!this.selectedScannerId || this.isScanning}
@click=${this.handleScan}
>
<dees-icon .icon=${this.isScanning ? 'lucide:loader' : 'lucide:scan'} .iconSize=${18}></dees-icon>
${this.isScanning ? 'Scanning...' : 'Scan'}
</button>
<div class="dropdown-container">
<button
class="action-button secondary"
?disabled=${!this.currentDocument}
@click=${() => this.showSendToMenu = !this.showSendToMenu}
>
<dees-icon .icon=${'lucide:send'} .iconSize=${18}></dees-icon>
Send To
<dees-icon .icon=${'lucide:chevronUp'} .iconSize=${14}></dees-icon>
</button>
${this.showSendToMenu ? this.renderSendToMenu() : ''}
</div>
<button
class="action-button secondary"
?disabled=${!this.currentDocument}
@click=${this.handleSaveLocal}
>
<dees-icon .icon=${'lucide:download'} .iconSize=${18}></dees-icon>
Save
</button>
</div>
`;
}
private renderSendToMenu(): TemplateResult {
return html`
<div class="dropdown-menu" @mouseleave=${() => this.showSendToMenu = false}>
${this.providers.length > 0
? this.providers.map(provider => html`
<div class="dropdown-item" @click=${() => this.handleSendToProvider(provider.id)}>
<dees-icon .icon=${provider.icon || 'lucide:cloud'} .iconSize=${18}></dees-icon>
${provider.name}
</div>
`)
: html`
<div class="dropdown-item" style="opacity: 0.5; cursor: default;">
<dees-icon .icon=${'lucide:info'} .iconSize=${18}></dees-icon>
No providers configured
</div>
`
}
</div>
`;
}
private renderHistoryPanel(): TemplateResult {
return html`
<div class="panel-header">
<div class="panel-header-left">
<h2 class="panel-title">Recent Scans</h2>
<p class="panel-description">View and manage your scanned documents</p>
</div>
</div>
${this.scanHistory.length > 0
? html`
<div class="history-grid">
${this.scanHistory.map(doc => html`
<div class="history-item" @click=${() => this.handleHistoryItemClick(doc)}>
<div class="history-thumbnail">
${doc.thumbnail
? html`<img src=${doc.thumbnail} alt=${doc.name || 'Scan'} />`
: html`<dees-icon .icon=${'lucide:file'} .iconSize=${32}></dees-icon>`
}
</div>
<div class="history-info">
<div class="history-name">${doc.name || `Scan ${doc.id.slice(0, 8)}`}</div>
<div class="history-date">${this.formatDate(doc.timestamp)}</div>
</div>
</div>
`)}
</div>
`
: html`
<div class="empty-state">
<dees-icon .icon=${'lucide:inbox'} .iconSize=${48}></dees-icon>
<p>No scans yet. Start scanning to see your documents here.</p>
</div>
`
}
`;
}
private renderSettingsPanel(): TemplateResult {
return html`
<div class="panel-header">
<div class="panel-header-left">
<h2 class="panel-title">Scan Settings</h2>
<p class="panel-description">Configure default scan options</p>
</div>
</div>
<div class="settings-section">
<div class="settings-section-title">Output Settings</div>
<div class="setting-row">
<span class="setting-label">Format</span>
<div class="setting-control">
<select
.value=${this.settings.format}
@change=${(e: Event) => this.updateSetting('format', (e.target as HTMLSelectElement).value as TScanFormat)}
>
${this.availableFormats.map(fmt => html`
<option value=${fmt} ?selected=${fmt === this.settings.format}>
${fmt.toUpperCase()}
</option>
`)}
</select>
</div>
</div>
<div class="setting-row">
<span class="setting-label">Resolution (DPI)</span>
<div class="setting-control">
<select
.value=${String(this.settings.resolution)}
@change=${(e: Event) => this.updateSetting('resolution', parseInt((e.target as HTMLSelectElement).value))}
>
${this.availableResolutions.map(res => html`
<option value=${res} ?selected=${res === this.settings.resolution}>
${res} DPI
</option>
`)}
</select>
</div>
</div>
<div class="setting-row">
<span class="setting-label">Color Mode</span>
<div class="setting-control">
<select
.value=${this.settings.colorMode}
@change=${(e: Event) => this.updateSetting('colorMode', (e.target as HTMLSelectElement).value as TScanColorMode)}
>
${this.availableColorModes.map(mode => html`
<option value=${mode} ?selected=${mode === this.settings.colorMode}>
${this.getColorModeLabel(mode)}
</option>
`)}
</select>
</div>
</div>
<div class="setting-row">
<span class="setting-label">Source</span>
<div class="setting-control">
<select
.value=${this.settings.source}
@change=${(e: Event) => this.updateSetting('source', (e.target as HTMLSelectElement).value as TScanSource)}
>
${this.availableSources.map(src => html`
<option value=${src} ?selected=${src === this.settings.source}>
${this.getSourceLabel(src)}
</option>
`)}
</select>
</div>
</div>
</div>
`;
}
private handleScannerChange(e: Event): void {
const select = e.target as HTMLSelectElement;
this.selectedScannerId = select.value || null;
this.dispatchEvent(new CustomEvent('scanner-select', {
detail: { scannerId: this.selectedScannerId },
bubbles: true,
composed: true,
}));
}
private async handleScan(): Promise<void> {
if (!this.selectedScannerId || this.isScanning) return;
this.isScanning = true;
this.dispatchEvent(new CustomEvent('scan-request', {
detail: {
scannerId: this.selectedScannerId,
settings: this.settings,
},
bubbles: true,
composed: true,
}));
}
public setScanResult(result: { data: string; format: TScanFormat; thumbnail?: string }): void {
const doc: IScannedDocument = {
id: crypto.randomUUID(),
timestamp: new Date(),
format: result.format,
data: result.data,
thumbnail: result.thumbnail,
size: result.data.length,
};
this.currentDocument = doc;
this.currentPreview = result.thumbnail || `data:image/${result.format};base64,${result.data}`;
this.scanHistory = [doc, ...this.scanHistory.slice(0, 19)]; // Keep last 20
this.isScanning = false;
}
public setScanError(error: string): void {
this.isScanning = false;
console.error('Scan error:', error);
// Could dispatch error event or show toast
}
private handleSaveLocal(): void {
if (!this.currentDocument) return;
this.dispatchEvent(new CustomEvent('save-local', {
detail: { document: this.currentDocument },
bubbles: true,
composed: true,
}));
}
private handleSendToProvider(providerId: string): void {
if (!this.currentDocument) return;
this.showSendToMenu = false;
this.dispatchEvent(new CustomEvent('send-to-provider', {
detail: {
providerId,
document: this.currentDocument,
},
bubbles: true,
composed: true,
}));
}
private handleHistoryItemClick(doc: IScannedDocument): void {
this.currentDocument = doc;
this.currentPreview = doc.thumbnail || `data:image/${doc.format};base64,${doc.data}`;
this.activePanel = 'scan';
}
private updateSetting<K extends keyof IScanSettings>(key: K, value: IScanSettings[K]): void {
this.settings = { ...this.settings, [key]: value };
this.dispatchEvent(new CustomEvent('settings-change', {
detail: { settings: this.settings },
bubbles: true,
composed: true,
}));
}
private getColorModeLabel(mode: TScanColorMode): string {
const labels: Record<TScanColorMode, string> = {
color: 'Color',
grayscale: 'Grayscale',
blackwhite: 'Black & White',
};
return labels[mode];
}
private getSourceLabel(source: TScanSource): string {
const labels: Record<TScanSource, string> = {
flatbed: 'Flatbed',
adf: 'Document Feeder',
'adf-duplex': 'Document Feeder (Duplex)',
};
return labels[source];
}
private formatDate(date: Date): string {
return new Intl.DateTimeFormat('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
}).format(date);
}
}

View File

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

View File

@@ -149,27 +149,177 @@ export class EcoViewSystem extends DeesElement {
@property({ type: String }) @property({ type: String })
accessor activePanel: TSystemPanel = 'overview'; accessor activePanel: TSystemPanel = 'overview';
// Mock system data // System data (can be set externally)
@state() @property({ type: Number })
accessor cpuUsage = 42; accessor cpuUsage = 0;
@property({ type: Number })
accessor memoryUsage = 0;
@property({ type: Number })
accessor diskUsage = 0;
@property({ type: Number })
accessor cpuTemp = 0;
@property({ type: String })
accessor uptime = '--';
@property({ type: Number })
accessor cpuCores = 0;
@property({ type: Number })
accessor cpuPhysicalCores = 0;
@property({ type: String })
accessor cpuModel = 'Unknown';
@property({ type: Number })
accessor cpuSpeed = 0;
@property({ type: Number })
accessor cpuSpeedMax = 0;
@property({ type: Number })
accessor memoryTotal = 0;
@property({ type: Number })
accessor memoryUsed = 0;
@property({ type: Number })
accessor memoryFree = 0;
@property({ type: Number })
accessor memoryAvailable = 0;
@property({ type: Number })
accessor memoryCached = 0;
@property({ type: Number })
accessor memoryBuffers = 0;
@property({ type: Number })
accessor swapTotal = 0;
@property({ type: Number })
accessor swapUsed = 0;
@property({ type: Number })
accessor diskTotal = 0;
@property({ type: Number })
accessor diskUsed = 0;
@property({ type: Number })
accessor diskFree = 0;
@property({ type: Number })
accessor networkRxSec = 0;
@property({ type: Number })
accessor networkTxSec = 0;
@property({ type: Number })
accessor networkRxTotal = 0;
@property({ type: Number })
accessor networkTxTotal = 0;
@property({ type: String })
accessor hostname = 'Unknown';
@property({ type: String })
accessor platform = 'Unknown';
@property({ type: String })
accessor distro = '';
@property({ type: Array })
accessor loadAvg: number[] = [0, 0, 0];
@property({ type: Array })
accessor coreLoads: number[] = [];
@state() @state()
accessor memoryUsage = 67; accessor networkInHistory: number[] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
@state() @state()
accessor diskUsage = 54; accessor networkOutHistory: number[] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
@state() // Public method to update metrics from backend
accessor cpuTemp = 58; public setMetrics(metrics: {
cpu: { usage: number; cores: number; physicalCores?: number; model: string; speed: number; speedMax?: number; loadAvg: number[]; coreLoads?: number[] };
memory: { total: number; used: number; free: number; available?: number; usagePercent: number; swapTotal?: number; swapUsed?: number; cached?: number; buffers?: number };
disk?: { total: number; used: number; free: number; usagePercent: number };
network?: { rxSec: number; txSec: number; rxTotal: number; txTotal: number };
system: { platform: string; distro?: string; hostname: string; uptimeFormatted: string };
}): void {
// CPU metrics
this.cpuUsage = metrics.cpu.usage;
this.cpuCores = metrics.cpu.cores;
this.cpuPhysicalCores = metrics.cpu.physicalCores || metrics.cpu.cores;
this.cpuModel = metrics.cpu.model;
this.cpuSpeed = metrics.cpu.speed;
this.cpuSpeedMax = metrics.cpu.speedMax || metrics.cpu.speed;
this.loadAvg = metrics.cpu.loadAvg;
this.coreLoads = metrics.cpu.coreLoads || [];
@state() // Memory metrics
accessor uptime = '14d 7h 32m'; this.memoryUsage = metrics.memory.usagePercent;
this.memoryTotal = metrics.memory.total;
this.memoryUsed = metrics.memory.used;
this.memoryFree = metrics.memory.free;
this.memoryAvailable = metrics.memory.available || metrics.memory.free;
this.memoryCached = metrics.memory.cached || 0;
this.memoryBuffers = metrics.memory.buffers || 0;
this.swapTotal = metrics.memory.swapTotal || 0;
this.swapUsed = metrics.memory.swapUsed || 0;
@state() // Disk metrics
accessor networkIn = [45, 52, 38, 65, 72, 68, 75, 82, 79, 85, 88, 72]; if (metrics.disk) {
this.diskUsage = metrics.disk.usagePercent;
this.diskTotal = metrics.disk.total;
this.diskUsed = metrics.disk.used;
this.diskFree = metrics.disk.free;
}
@state() // Network metrics
accessor networkOut = [32, 28, 35, 42, 38, 45, 52, 48, 55, 62, 58, 65]; if (metrics.network) {
this.networkRxSec = metrics.network.rxSec;
this.networkTxSec = metrics.network.txSec;
this.networkRxTotal = metrics.network.rxTotal;
this.networkTxTotal = metrics.network.txTotal;
// Update history for trend charts
this.networkInHistory = [...this.networkInHistory.slice(1), metrics.network.rxSec];
this.networkOutHistory = [...this.networkOutHistory.slice(1), metrics.network.txSec];
}
// System metrics
this.hostname = metrics.system.hostname;
this.platform = metrics.system.platform;
this.distro = metrics.system.distro || '';
this.uptime = metrics.system.uptimeFormatted;
}
// Method to set CPU temperature separately (may not always be available)
public setTemperature(temp: { main: number; cores?: number[]; max?: number }): void {
this.cpuTemp = temp.main || 0;
}
// Method to set processes data
public setProcesses(data: { total: number; running: number; sleeping: number; list: any[] }): void {
// Could store and display process data if needed
}
// Helper to format bytes
private formatBytes(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
private getMenuGroups(): ISecondaryMenuGroup[] { private getMenuGroups(): ISecondaryMenuGroup[] {
return [ return [
@@ -275,6 +425,15 @@ export class EcoViewSystem extends DeesElement {
} }
} }
// Helper to format bytes per second as speed
private formatSpeed(bytesPerSec: number): string {
if (bytesPerSec === 0) return '0 B/s';
const k = 1024;
const sizes = ['B/s', 'KB/s', 'MB/s', 'GB/s'];
const i = Math.floor(Math.log(bytesPerSec) / Math.log(k));
return parseFloat((bytesPerSec / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
private renderOverviewPanel(): TemplateResult { private renderOverviewPanel(): TemplateResult {
const overviewTiles = [ const overviewTiles = [
{ {
@@ -283,6 +442,7 @@ export class EcoViewSystem extends DeesElement {
value: this.cpuUsage, value: this.cpuUsage,
type: 'gauge' as const, type: 'gauge' as const,
icon: 'lucide:cpu', icon: 'lucide:cpu',
description: this.cpuModel !== 'Unknown' ? `${this.cpuPhysicalCores} cores @ ${this.cpuSpeed} GHz` : undefined,
gaugeOptions: { gaugeOptions: {
min: 0, min: 0,
max: 100, max: 100,
@@ -299,6 +459,7 @@ export class EcoViewSystem extends DeesElement {
value: this.memoryUsage, value: this.memoryUsage,
type: 'gauge' as const, type: 'gauge' as const,
icon: 'lucide:memoryStick', icon: 'lucide:memoryStick',
description: this.memoryTotal ? `${this.formatBytes(this.memoryUsed)} of ${this.formatBytes(this.memoryTotal)}` : undefined,
gaugeOptions: { gaugeOptions: {
min: 0, min: 0,
max: 100, max: 100,
@@ -315,6 +476,7 @@ export class EcoViewSystem extends DeesElement {
value: this.diskUsage, value: this.diskUsage,
type: 'gauge' as const, type: 'gauge' as const,
icon: 'lucide:hardDrive', icon: 'lucide:hardDrive',
description: this.diskTotal ? `${this.formatBytes(this.diskUsed)} of ${this.formatBytes(this.diskTotal)}` : undefined,
gaugeOptions: { gaugeOptions: {
min: 0, min: 0,
max: 100, max: 100,
@@ -346,22 +508,22 @@ export class EcoViewSystem extends DeesElement {
{ {
id: 'network-in', id: 'network-in',
title: 'Network In', title: 'Network In',
value: '85', value: this.formatSpeed(this.networkRxSec),
unit: 'Mbps',
type: 'trend' as const, type: 'trend' as const,
icon: 'lucide:download', icon: 'lucide:download',
trendData: this.networkIn, trendData: this.networkInHistory,
color: 'hsl(142 71% 45%)', color: 'hsl(142 71% 45%)',
description: `Total: ${this.formatBytes(this.networkRxTotal)}`,
}, },
{ {
id: 'network-out', id: 'network-out',
title: 'Network Out', title: 'Network Out',
value: '65', value: this.formatSpeed(this.networkTxSec),
unit: 'Mbps',
type: 'trend' as const, type: 'trend' as const,
icon: 'lucide:upload', icon: 'lucide:upload',
trendData: this.networkOut, trendData: this.networkOutHistory,
color: 'hsl(217 91% 60%)', color: 'hsl(217 91% 60%)',
description: `Total: ${this.formatBytes(this.networkTxTotal)}`,
}, },
{ {
id: 'uptime', id: 'uptime',
@@ -373,12 +535,12 @@ export class EcoViewSystem extends DeesElement {
description: 'Since last reboot', description: 'Since last reboot',
}, },
{ {
id: 'processes', id: 'hostname',
title: 'Processes', title: 'Hostname',
value: 247, value: this.hostname,
type: 'number' as const, type: 'text' as const,
icon: 'lucide:layers', icon: 'lucide:server',
description: '12 running, 235 sleeping', description: this.distro ? `${this.distro} - ${this.cpuCores} threads` : `${this.platform} - ${this.cpuCores} threads`,
}, },
]; ];
@@ -399,6 +561,13 @@ export class EcoViewSystem extends DeesElement {
} }
private renderCpuPanel(): TemplateResult { private renderCpuPanel(): TemplateResult {
// Generate cores data for cpuCores tile type
const coresData = this.coreLoads.map((load, index) => ({
id: index,
usage: load,
label: `Core ${index}`,
}));
const cpuTiles = [ const cpuTiles = [
{ {
id: 'cpu-total', id: 'cpu-total',
@@ -406,6 +575,7 @@ export class EcoViewSystem extends DeesElement {
value: this.cpuUsage, value: this.cpuUsage,
type: 'gauge' as const, type: 'gauge' as const,
icon: 'lucide:cpu', icon: 'lucide:cpu',
description: `${this.cpuCores} threads on ${this.cpuPhysicalCores} cores`,
gaugeOptions: { gaugeOptions: {
min: 0, min: 0,
max: 100, max: 100,
@@ -417,73 +587,21 @@ export class EcoViewSystem extends DeesElement {
}, },
}, },
{ {
id: 'core-0', id: 'cpu-cores',
title: 'Core 0', title: 'Core Usage',
value: 38, value: this.cpuUsage,
type: 'gauge' as const, type: 'cpuCores' as const,
gaugeOptions: { coresData: coresData,
min: 0, columnSpan: 2,
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', id: 'load-avg',
title: 'Load Average', title: 'Load Average',
value: '2.45', value: this.loadAvg[0]?.toFixed(2) || '0',
type: 'trend' as const, type: 'trend' as const,
icon: 'lucide:activity', icon: 'lucide:activity',
trendData: [1.8, 2.1, 2.4, 2.2, 2.5, 2.3, 2.6, 2.4, 2.45], trendData: this.loadAvg,
description: '1m: 2.45, 5m: 2.32, 15m: 2.18', description: `1m: ${this.loadAvg[0]?.toFixed(2)}, 5m: ${this.loadAvg[1]?.toFixed(2)}, 15m: ${this.loadAvg[2]?.toFixed(2)}`,
}, },
{ {
id: 'cpu-temp', id: 'cpu-temp',
@@ -506,18 +624,18 @@ export class EcoViewSystem extends DeesElement {
{ {
id: 'freq', id: 'freq',
title: 'Clock Speed', title: 'Clock Speed',
value: '3.2', value: this.cpuSpeed.toFixed(1),
unit: 'GHz', unit: 'GHz',
type: 'number' as const, type: 'number' as const,
icon: 'lucide:gauge', icon: 'lucide:gauge',
description: 'Max: 4.2 GHz', description: this.cpuSpeedMax ? `Max: ${this.cpuSpeedMax.toFixed(1)} GHz` : undefined,
}, },
]; ];
return html` return html`
<div class="panel-header"> <div class="panel-header">
<div class="panel-title">CPU</div> <div class="panel-title">CPU</div>
<div class="panel-description">Processor usage and performance</div> <div class="panel-description">${this.cpuModel}</div>
</div> </div>
<div class="stats-section"> <div class="stats-section">
@@ -531,6 +649,8 @@ export class EcoViewSystem extends DeesElement {
} }
private renderMemoryPanel(): TemplateResult { private renderMemoryPanel(): TemplateResult {
const swapUsagePercent = this.swapTotal > 0 ? Math.round((this.swapUsed / this.swapTotal) * 100) : 0;
const memoryTiles = [ const memoryTiles = [
{ {
id: 'ram-usage', id: 'ram-usage',
@@ -547,12 +667,12 @@ export class EcoViewSystem extends DeesElement {
{ value: 85, color: 'hsl(0 84% 60%)' }, { value: 85, color: 'hsl(0 84% 60%)' },
], ],
}, },
description: '10.7 GB of 16 GB', description: `${this.formatBytes(this.memoryUsed)} of ${this.formatBytes(this.memoryTotal)}`,
}, },
{ {
id: 'swap-usage', id: 'swap-usage',
title: 'Swap Usage', title: 'Swap Usage',
value: 12, value: swapUsagePercent,
type: 'gauge' as const, type: 'gauge' as const,
icon: 'lucide:hardDrive', icon: 'lucide:hardDrive',
gaugeOptions: { gaugeOptions: {
@@ -564,41 +684,38 @@ export class EcoViewSystem extends DeesElement {
{ value: 75, color: 'hsl(0 84% 60%)' }, { value: 75, color: 'hsl(0 84% 60%)' },
], ],
}, },
description: '0.5 GB of 4 GB', description: this.swapTotal > 0 ? `${this.formatBytes(this.swapUsed)} of ${this.formatBytes(this.swapTotal)}` : 'No swap',
}, },
{ {
id: 'mem-trend', id: 'mem-trend',
title: 'Memory History', title: 'Memory History',
value: '67%', value: `${this.memoryUsage}%`,
type: 'trend' as const, type: 'trend' as const,
icon: 'lucide:trendingUp', icon: 'lucide:trendingUp',
trendData: [58, 62, 65, 63, 68, 72, 70, 65, 67], trendData: [58, 62, 65, 63, 68, 72, 70, 65, this.memoryUsage],
description: 'Last hour', description: 'Recent usage',
}, },
{ {
id: 'cached', id: 'cached',
title: 'Cached', title: 'Cached',
value: '3.2', value: this.formatBytes(this.memoryCached),
unit: 'GB', type: 'text' as const,
type: 'number' as const,
icon: 'lucide:database', icon: 'lucide:database',
color: 'hsl(217 91% 60%)', color: 'hsl(217 91% 60%)',
}, },
{ {
id: 'buffers', id: 'buffers',
title: 'Buffers', title: 'Buffers',
value: '512', value: this.formatBytes(this.memoryBuffers),
unit: 'MB', type: 'text' as const,
type: 'number' as const,
icon: 'lucide:layers', icon: 'lucide:layers',
color: 'hsl(262 83% 58%)', color: 'hsl(262 83% 58%)',
}, },
{ {
id: 'available', id: 'available',
title: 'Available', title: 'Available',
value: '5.3', value: this.formatBytes(this.memoryAvailable),
unit: 'GB', type: 'text' as const,
type: 'number' as const,
icon: 'lucide:checkCircle', icon: 'lucide:checkCircle',
color: 'hsl(142 71% 45%)', color: 'hsl(142 71% 45%)',
}, },
@@ -623,58 +740,39 @@ export class EcoViewSystem extends DeesElement {
private renderStoragePanel(): TemplateResult { private renderStoragePanel(): TemplateResult {
const storageTiles = [ const storageTiles = [
{ {
id: 'disk-main', id: 'disk-total',
title: 'System Drive', title: 'Total Storage',
value: this.diskUsage, value: this.diskUsage,
type: 'percentage' as const, type: 'percentage' as const,
icon: 'lucide:hardDrive', icon: 'lucide:hardDrive',
description: '275 GB of 512 GB used', description: `${this.formatBytes(this.diskUsed)} of ${this.formatBytes(this.diskTotal)} used`,
color: 'hsl(217 91% 60%)', color: 'hsl(217 91% 60%)',
}, },
{ {
id: 'disk-data', id: 'disk-free',
title: 'Data Drive', title: 'Free Space',
value: 38, value: this.formatBytes(this.diskFree),
type: 'percentage' as const, type: 'text' as const,
icon: 'lucide:hardDrive', icon: 'lucide:hardDrive',
description: '380 GB of 1 TB used', description: 'Available storage',
color: 'hsl(142 71% 45%)', color: 'hsl(142 71% 45%)',
}, },
{ {
id: 'read-speed', id: 'disk-used',
title: 'Read Speed', title: 'Used Space',
value: '245', value: this.formatBytes(this.diskUsed),
unit: 'MB/s', type: 'text' as const,
type: 'trend' as const, icon: 'lucide:database',
icon: 'lucide:download', description: 'Currently in use',
trendData: [180, 220, 195, 280, 245, 210, 265, 230, 245], color: 'hsl(45 93% 47%)',
color: 'hsl(142 71% 45%)',
}, },
{ {
id: 'write-speed', id: 'disk-total-size',
title: 'Write Speed', title: 'Total Capacity',
value: '128', value: this.formatBytes(this.diskTotal),
unit: 'MB/s', type: 'text' as const,
type: 'trend' as const, icon: 'lucide:server',
icon: 'lucide:upload', description: 'All filesystems',
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',
}, },
]; ];
@@ -698,83 +796,38 @@ export class EcoViewSystem extends DeesElement {
const networkTiles = [ const networkTiles = [
{ {
id: 'download', id: 'download',
title: 'Download', title: 'Download Speed',
value: '85.2', value: this.formatSpeed(this.networkRxSec),
unit: 'Mbps',
type: 'trend' as const, type: 'trend' as const,
icon: 'lucide:download', icon: 'lucide:download',
trendData: this.networkIn, trendData: this.networkInHistory,
color: 'hsl(142 71% 45%)', color: 'hsl(142 71% 45%)',
}, },
{ {
id: 'upload', id: 'upload',
title: 'Upload', title: 'Upload Speed',
value: '64.8', value: this.formatSpeed(this.networkTxSec),
unit: 'Mbps',
type: 'trend' as const, type: 'trend' as const,
icon: 'lucide:upload', icon: 'lucide:upload',
trendData: this.networkOut, trendData: this.networkOutHistory,
color: 'hsl(217 91% 60%)', 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', id: 'total-down',
title: 'Total Downloaded', title: 'Total Downloaded',
value: '24.5', value: this.formatBytes(this.networkRxTotal),
unit: 'GB', type: 'text' as const,
type: 'number' as const, icon: 'lucide:arrowDownCircle',
icon: 'lucide:database', description: 'Since boot',
description: 'This session',
color: 'hsl(142 71% 45%)', color: 'hsl(142 71% 45%)',
}, },
{ {
id: 'total-up', id: 'total-up',
title: 'Total Uploaded', title: 'Total Uploaded',
value: '8.2', value: this.formatBytes(this.networkTxTotal),
unit: 'GB', type: 'text' as const,
type: 'number' as const, icon: 'lucide:arrowUpCircle',
icon: 'lucide:database', description: 'Since boot',
description: 'This session',
color: 'hsl(217 91% 60%)', color: 'hsl(217 91% 60%)',
}, },
]; ];

View File

@@ -4,3 +4,5 @@ export * from './eco-view-saasshare/index.js';
export * from './eco-view-system/index.js'; export * from './eco-view-system/index.js';
export * from './eco-view-home/index.js'; export * from './eco-view-home/index.js';
export * from './eco-view-login/index.js'; export * from './eco-view-login/index.js';
export * from './eco-view-scan/index.js';
export * from './eco-view-browser/index.js';