This commit is contained in:
2026-01-12 14:34:56 +00:00
parent 2b87d63121
commit 2d4846cfed
12 changed files with 1173 additions and 566 deletions

View File

@@ -1,6 +1,6 @@
/**
* EcoOS Displays View
* Display management with enable/disable/primary controls
* Card-based display management
*/
import {
@@ -12,6 +12,7 @@ import {
css,
type TemplateResult,
} from '@design.estate/dees-element';
import { DeesButton, DeesPanel, DeesBadge } from '@design.estate/dees-catalog';
import { sharedStyles } from '../styles/shared.js';
import type { IDisplayInfo } from '../../ts_interfaces/display.js';
@@ -24,73 +25,96 @@ export class EcoosDisplays extends DeesElement {
@state()
private accessor loading: boolean = false;
@state()
private accessor message: string = '';
@state()
private accessor messageError: boolean = false;
public static styles = [
sharedStyles,
css`
:host {
display: block;
padding: 20px;
padding: 16px;
}
.display-item {
.display-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
}
.display-card {
opacity: 1;
transition: opacity 0.2s ease;
}
.display-card.disabled {
opacity: 0.5;
}
.display-meta {
font-size: var(--text-xs);
color: var(--text-tertiary);
margin-bottom: 12px;
}
.badge-row {
margin-bottom: 12px;
}
.actions-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 12px 0;
border-bottom: 1px solid var(--ecoos-border);
flex-wrap: wrap;
}
.display-item:last-child {
border-bottom: none;
.message-bar {
margin-top: 16px;
padding: 8px 12px;
border-radius: 6px;
font-size: var(--text-sm);
}
.display-info {
flex: 1;
min-width: 150px;
.message-bar.success {
background: hsla(142.1, 76.2%, 36.3%, 0.15);
color: var(--success);
}
.display-name {
font-weight: 500;
font-size: 14px;
.message-bar.error {
background: hsla(0, 84.2%, 60.2%, 0.15);
color: var(--error);
}
.display-details {
font-size: 11px;
color: var(--ecoos-text-dim);
.empty-state {
text-align: center;
padding: 32px;
color: var(--text-tertiary);
}
.display-actions {
display: flex;
gap: 4px;
align-items: center;
.disabled-section {
margin-top: 16px;
}
.display-actions .btn {
padding: 4px 12px;
margin: 0;
font-size: 11px;
}
.section-header {
font-size: 12px;
color: var(--ecoos-text-dim);
margin: 16px 0 8px 0;
.disabled-header {
font-size: var(--text-sm);
color: var(--text-tertiary);
margin-bottom: 12px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
gap: 6px;
}
.section-header:hover {
color: var(--ecoos-text);
.disabled-header::before {
content: '▶';
font-size: 8px;
transition: transform 150ms ease;
}
.collapsed-content {
display: none;
}
.collapsed-content.expanded {
display: block;
.disabled-header.open::before {
transform: rotate(90deg);
}
`,
];
@@ -99,102 +123,109 @@ export class EcoosDisplays extends DeesElement {
const enabledDisplays = this.displays.filter(d => d.active);
const disabledDisplays = this.displays.filter(d => !d.active);
if (this.displays.length === 0) {
return html`
<div class="empty-state">
No displays detected
</div>
`;
}
return html`
<div class="card">
<div class="card-title">Displays</div>
${this.displays.length === 0
? html`<div style="color: var(--ecoos-text-dim)">No displays detected</div>`
: html`
<!-- Enabled Displays -->
${enabledDisplays.map(d => this.renderDisplayItem(d))}
<!-- Disabled Displays -->
${disabledDisplays.length > 0 ? html`
<details style="margin-top: 12px;">
<summary class="section-header">
Disabled Displays (${disabledDisplays.length})
</summary>
<div style="margin-top: 8px;">
${disabledDisplays.map(d => this.renderDisplayItem(d))}
</div>
</details>
` : ''}
`
}
<div class="display-grid">
${enabledDisplays.map(d => this.renderDisplayCard(d))}
</div>
${disabledDisplays.length > 0 ? html`
<details class="disabled-section">
<summary class="disabled-header">
Disabled Displays (${disabledDisplays.length})
</summary>
<div class="display-grid" style="margin-top: 12px;">
${disabledDisplays.map(d => this.renderDisplayCard(d))}
</div>
</details>
` : ''}
${this.message ? html`
<div class="message-bar ${this.messageError ? 'error' : 'success'}">${this.message}</div>
` : ''}
`;
}
private renderDisplayItem(display: IDisplayInfo): TemplateResult {
private renderDisplayCard(display: IDisplayInfo): TemplateResult {
return html`
<div class="display-item">
<div class="display-info">
<div class="display-name">${display.name}</div>
<div class="display-details">
${display.width}x${display.height} @ ${display.refreshRate}Hz
${display.make !== 'Unknown' ? `${display.make}` : ''}
<dees-panel
class="display-card ${display.active ? '' : 'disabled'}"
.title=${display.name}
.subtitle=${`${display.width}×${display.height} @ ${display.refreshRate}Hz`}
.variant=${display.active ? 'default' : 'ghost'}
>
${display.make && display.make !== 'Unknown' ? html`
<div class="display-meta">${display.make}${display.model ? ` ${display.model}` : ''}</div>
` : ''}
${display.isPrimary ? html`
<div class="badge-row">
<dees-badge .type=${'primary'}>Primary</dees-badge>
</div>
</div>
<div class="display-actions">
${display.isPrimary
? html`<span class="device-default">Primary</span>`
: display.active
? html`
<button
class="btn btn-primary"
@click=${() => this.setKioskDisplay(display.name)}
?disabled=${this.loading}
>
Set Primary
</button>
`
: ''
}
<button
class="btn ${display.active ? 'btn-danger' : 'btn-primary'}"
` : ''}
<div class="actions-row">
${display.active && !display.isPrimary ? html`
<dees-button
.type=${'default'}
.text=${'Set Primary'}
.disabled=${this.loading}
@click=${() => this.setPrimary(display.name)}
></dees-button>
` : ''}
<dees-button
.type=${'default'}
.status=${display.active ? 'error' : 'success'}
.text=${display.active ? 'Disable' : 'Enable'}
.disabled=${this.loading}
@click=${() => this.toggleDisplay(display.name, !display.active)}
?disabled=${this.loading}
>
${display.active ? 'Disable' : 'Enable'}
</button>
></dees-button>
</div>
</div>
</dees-panel>
`;
}
private async toggleDisplay(name: string, enable: boolean): Promise<void> {
this.loading = true;
this.message = '';
try {
const action = enable ? 'enable' : 'disable';
const response = await fetch(`/api/displays/${encodeURIComponent(name)}/${action}`, {
method: 'POST',
});
const result = await response.json();
if (!result.success) {
alert(result.message);
}
this.message = result.message;
this.messageError = !result.success;
this.dispatchEvent(new CustomEvent('refresh-displays'));
} catch (error) {
alert(`Error: ${error}`);
this.message = `Error: ${error}`;
this.messageError = true;
} finally {
this.loading = false;
}
}
private async setKioskDisplay(name: string): Promise<void> {
private async setPrimary(name: string): Promise<void> {
this.loading = true;
this.message = '';
try {
const response = await fetch(`/api/displays/${encodeURIComponent(name)}/primary`, {
method: 'POST',
});
const result = await response.json();
if (!result.success) {
alert(result.message);
}
this.message = result.message;
this.messageError = !result.success;
this.dispatchEvent(new CustomEvent('refresh-displays'));
} catch (error) {
alert(`Error: ${error}`);
this.message = `Error: ${error}`;
this.messageError = true;
} finally {
this.loading = false;
}