feat(elements): add eco-provider-frame and dataprovider interfaces; improve virtual keyboard interactions; add demos, exports and bump dev dependencies
This commit is contained in:
@@ -211,6 +211,10 @@ export class EcoApplauncherKeyboard extends DeesElement {
|
||||
background: ${cssManager.bdTheme('hsl(220 15% 92%)', 'hsl(240 5% 28%)')};
|
||||
}
|
||||
|
||||
.key:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.key.special {
|
||||
background: ${cssManager.bdTheme('hsl(220 10% 88%)', 'hsl(240 5% 16%)')};
|
||||
font-size: 16px;
|
||||
@@ -375,10 +379,12 @@ export class EcoApplauncherKeyboard extends DeesElement {
|
||||
return html`
|
||||
<div
|
||||
class="key ${type} ${widthClass} ${isActive ? 'active' : ''}"
|
||||
tabindex="-1"
|
||||
@pointerdown=${(e: PointerEvent) => this.handlePointerDown(e, config)}
|
||||
@pointerup=${(e: PointerEvent) => this.handlePointerUp(e, config)}
|
||||
@pointerleave=${(e: PointerEvent) => this.handlePointerLeave(e, config)}
|
||||
@pointermove=${(e: PointerEvent) => this.handlePointerMove(e, config)}
|
||||
@mousedown=${(e: MouseEvent) => e.preventDefault()}
|
||||
>
|
||||
${displayValue}
|
||||
</div>
|
||||
@@ -418,6 +424,7 @@ export class EcoApplauncherKeyboard extends DeesElement {
|
||||
|
||||
private handlePointerDown(e: PointerEvent, config: IKeyConfig): void {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const target = e.currentTarget as HTMLElement;
|
||||
target.setPointerCapture(e.pointerId);
|
||||
|
||||
@@ -483,6 +490,7 @@ export class EcoApplauncherKeyboard extends DeesElement {
|
||||
|
||||
private handlePointerUp(e: PointerEvent, config: IKeyConfig): void {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.clearLongPressTimer();
|
||||
this.keyPreview = null;
|
||||
|
||||
|
||||
@@ -644,7 +644,11 @@ export class EcoApplauncher extends DeesElement {
|
||||
<div class="launcher-container">
|
||||
${this.mode === 'login' ? '' : this.renderTopBar()}
|
||||
${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
|
||||
?visible=${this.keyboardVisible}
|
||||
@key-press=${this.handleKeyboardKeyPress}
|
||||
@@ -962,7 +966,9 @@ export class EcoApplauncher extends DeesElement {
|
||||
return html`
|
||||
<div
|
||||
class="status-item clickable ${this.keyboardVisible ? 'active' : ''}"
|
||||
tabindex="-1"
|
||||
@click=${this.handleKeyboardToggle}
|
||||
@mousedown=${(e: MouseEvent) => e.preventDefault()}
|
||||
>
|
||||
<dees-icon .icon=${'lucide:keyboard'} .iconSize=${18}></dees-icon>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
502
ts_web/elements/eco-provider-frame/eco-provider-frame.ts
Normal file
502
ts_web/elements/eco-provider-frame/eco-provider-frame.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/eco-provider-frame/index.ts
Normal file
1
ts_web/elements/eco-provider-frame/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './eco-provider-frame.js';
|
||||
@@ -6,3 +6,4 @@ export * from './00group-applauncher/index.js';
|
||||
|
||||
// Standalone Components
|
||||
export * from './eco-screensaver/index.js';
|
||||
export * from './eco-provider-frame/index.js';
|
||||
|
||||
158
ts_web/elements/interfaces/dataprovider.ts
Normal file
158
ts_web/elements/interfaces/dataprovider.ts
Normal 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 }>;
|
||||
}
|
||||
@@ -3,3 +3,4 @@ export * from './appbarmenuitem.js';
|
||||
export * from './menugroup.js';
|
||||
export * from './appconfig.js';
|
||||
export * from './secondarymenu.js';
|
||||
export * from './dataprovider.js';
|
||||
|
||||
Reference in New Issue
Block a user