gui/ui/src/components/provider-host.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

96 lines
2.5 KiB
TypeScript

// SPDX-Licence-Identifier: EUPL-1.2
import {
Component,
CUSTOM_ELEMENTS_SCHEMA,
ElementRef,
Input,
OnChanges,
OnInit,
Renderer2,
ViewChild,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ApiConfigService } from '../services/api-config.service';
import { ProviderDiscoveryService } from '../services/provider-discovery.service';
/**
* ProviderHostComponent renders any custom element by tag name using
* Angular's Renderer2 for safe DOM manipulation. It reads the :provider
* route parameter to look up the element tag from the discovery service.
*/
@Component({
selector: 'provider-host',
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: '<div #container class="provider-host"></div>',
styles: [
`
:host {
display: block;
width: 100%;
height: 100%;
}
.provider-host {
width: 100%;
height: 100%;
}
`,
],
})
export class ProviderHostComponent implements OnInit, OnChanges {
/** The custom element tag to render. Can be set via input or route param. */
@Input() tag = '';
/** API URL attribute passed to the custom element. */
@Input() apiUrl = '';
@ViewChild('container', { static: true }) container!: ElementRef;
constructor(
private renderer: Renderer2,
private route: ActivatedRoute,
private apiConfig: ApiConfigService,
private providerService: ProviderDiscoveryService,
) {}
ngOnInit(): void {
this.route.params.subscribe((params) => {
const providerName = params['provider'];
if (providerName) {
const provider = this.providerService
.providers()
.find((p) => p.name.toLowerCase() === providerName.toLowerCase());
if (provider?.element?.tag) {
this.tag = provider.element.tag;
this.renderElement();
}
}
});
}
ngOnChanges(): void {
this.renderElement();
}
private renderElement(): void {
if (!this.tag || !this.container) {
return;
}
const native = this.container.nativeElement;
// Clear previous element safely
while (native.firstChild) {
this.renderer.removeChild(native, native.firstChild);
}
// Create and append the custom element
const el = this.renderer.createElement(this.tag);
const url = this.apiUrl || this.apiConfig.baseUrl;
if (url) {
this.renderer.setAttribute(el, 'api-url', url);
}
this.renderer.appendChild(native, el);
}
}