gui/ui/src/components/status-bar.component.ts
Snider 0dcc42c7fb
Some checks failed
Security Scan / security (push) Failing after 9s
Test / test (push) Failing after 1m28s
feat(ui): add app shell framework with provider discovery
Port the HLCRF application frame from lthn-desktop into core/gui/ui/ as a
reusable Angular framework. Adds:

- ApplicationFrameComponent: header, collapsible sidebar, content area, footer
- SystemTrayFrameComponent: 380x480 frameless panel with provider status cards
- ProviderDiscoveryService: fetches GET /api/v1/providers, loads custom elements
- ProviderHostComponent: renders any custom element by tag via Renderer2
- ProviderNavComponent: dynamic sidebar navigation from provider discovery
- StatusBarComponent: footer with time, version, provider count, WS status
- WebSocketService: persistent connection with auto-reconnect
- ApiConfigService: configurable API base URL
- TranslationService: key-value i18n with API fallback

Navigation is dynamic (populated from providers), sidebar shows icons-only
in collapsed mode with expand on click, dark mode supported throughout.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-14 12:41:33 +00:00

122 lines
3.1 KiB
TypeScript

// SPDX-Licence-Identifier: EUPL-1.2
import { Component, Input, OnDestroy, OnInit, signal } from '@angular/core';
import { ProviderDiscoveryService } from '../services/provider-discovery.service';
import { WebSocketService } from '../services/websocket.service';
/**
* StatusBarComponent renders the footer bar showing time, version,
* provider count, and connection status.
*/
@Component({
selector: 'status-bar',
standalone: true,
template: `
<footer class="status-bar">
<div class="status-left">
<span class="status-item version">{{ version }}</span>
<span class="status-item providers">
<i class="fa-regular fa-puzzle-piece"></i>
{{ providerCount() }} providers
</span>
</div>
<div class="status-right">
<span class="status-item connection" [class.connected]="wsConnected()">
<span class="status-dot"></span>
{{ wsConnected() ? 'Connected' : 'Disconnected' }}
</span>
<span class="status-item time">{{ time() }}</span>
</div>
</footer>
`,
styles: [
`
.status-bar {
position: fixed;
inset-inline: 0;
bottom: 0;
z-index: 40;
height: 2.5rem;
border-top: 1px solid rgb(229 231 235);
background: #ffffff;
display: flex;
align-items: center;
justify-content: space-between;
padding-inline: 1rem;
}
:host-context(.dark) .status-bar {
border-color: rgba(255, 255, 255, 0.1);
background: rgb(17 24 39);
}
.status-left,
.status-right {
display: flex;
align-items: center;
gap: 1rem;
}
.status-item {
font-size: 0.875rem;
color: rgb(107 114 128);
}
:host-context(.dark) .status-item {
color: rgb(156 163 175);
}
.status-item i {
margin-right: 0.25rem;
}
.status-dot {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
background: rgb(107 114 128);
margin-right: 0.375rem;
}
.connection.connected .status-dot {
background: rgb(34 197 94);
box-shadow: 0 0 4px rgb(34 197 94);
}
.time {
font-family: 'JetBrains Mono', 'Fira Code', monospace;
}
`,
],
})
export class StatusBarComponent implements OnInit, OnDestroy {
@Input() version = 'v0.1.0';
@Input() sidebarWidth = '5rem';
readonly time = signal('');
private intervalId: ReturnType<typeof setInterval> | undefined;
constructor(
private providerService: ProviderDiscoveryService,
private wsService: WebSocketService,
) {}
readonly providerCount = () => this.providerService.providers().length;
readonly wsConnected = () => this.wsService.connected();
ngOnInit(): void {
this.updateTime();
this.intervalId = setInterval(() => this.updateTime(), 1000);
}
ngOnDestroy(): void {
if (this.intervalId) {
clearInterval(this.intervalId);
}
}
private updateTime(): void {
this.time.set(new Date().toLocaleTimeString());
}
}