/** * @license * Copyright 2025-2026 Open Home Foundation * SPDX-License-Identifier: Apache-2.0 */ import "@material/web/button/outlined-button"; import "@material/web/divider/divider"; import "@material/web/iconbutton/icon-button"; import "@material/web/list/list"; import "@material/web/list/list-item"; import { consume } from "@lit/context"; import { MatterClient } from "@matter-server/ws-client"; import { mdiArrowLeft, mdiBrightnessAuto, mdiCog, mdiHome, mdiLogout, mdiWeatherNight, mdiWeatherSunny } from "@mdi/js"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { clientContext, tickContext } from "../../client/client-context.js"; import { showSettingsDialog } from "../../components/dialogs/settings/show-settings-dialog.js"; import "../../components/ha-svg-icon"; import { DevModeService } from "../../util/dev-mode-service.js"; import { reducedMotionStyles } from "../../util/shared-styles.js"; import { EffectiveTheme, ThemePreference, ThemeService } from "../../util/theme-service.js"; interface HeaderAction { label: string; icon: string; action: void; } export type ActiveView = "nodes" | "thread" | "wifi"; @customElement("dashboard-header") export class DashboardHeader extends LitElement { @property() public backButton?: string; @property() public actions?: HeaderAction[]; @property() public activeView?: ActiveView; @property({ type: Boolean }) public hasThreadDevices?: boolean; @property({ type: Boolean }) public hasWifiDevices?: boolean; @consume({ context: clientContext }) public client?: MatterClient; @consume({ context: tickContext, subscribe: true }) protected _tick = 0; @state() private _themePreference: ThemePreference = ThemeService.preference; @state() private _effectiveTheme: EffectiveTheme = ThemeService.effectiveTheme; @state() private _devMode = DevModeService.active; private _unsubscribeTheme?: () => void; private _unsubscribeDevMode?: () => void; override connectedCallback() { super.connectedCallback(); this._unsubscribeTheme = ThemeService.subscribe(theme => { this._effectiveTheme = theme; this._themePreference = ThemeService.preference; }); this._unsubscribeDevMode = DevModeService.subscribe(active => { this._devMode = active; }); } override disconnectedCallback() { super.disconnectedCallback(); this._unsubscribeTheme?.(); this._unsubscribeDevMode?.(); } private _goBack() { if (this.backButton) { location.hash = this.backButton; } } private _goHome() { location.hash = "#"; } private _cycleTheme() { ThemeService.cycleTheme(); } private _openSettings() { showSettingsDialog(); } private _getThemeIcon(): string { switch (this._themePreference) { case "light": return mdiWeatherSunny; case "dark": return mdiWeatherNight; case "system": return mdiBrightnessAuto; } } private _getThemeTooltip(): string { switch (this._themePreference) { case "light": return "Theme: Light"; case "dark": return "Theme: Dark"; case "system": return `Theme: System (${this._effectiveTheme})`; } } private _renderNavTabs() { if (this.activeView === undefined) { return nothing; } // Only show tabs if at least one network type has devices const showThreadTab = this.hasThreadDevices === true; const showWifiTab = this.hasWifiDevices === true; // Don't show nav tabs if no network devices exist if (!showThreadTab && !showWifiTab) { return nothing; } return html` `; } protected override render() { return html`
${this.backButton ? html` ` : ""}
${this.title ?? ""}
${this._renderNavTabs()}
${this.actions?.map(action => { return html` `; })} ${this._devMode && this.client ? html` ` : nothing} ${this.client ? html` ` : nothing} ${this.client && !this.client.isProduction ? html` ` : nothing}
`; } static override styles = [ reducedMotionStyles, css` .header { background-color: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); --icon-primary-color: var(--md-sys-color-on-primary); font-weight: 400; display: flex; align-items: center; padding-left: 18px; padding-right: 8px; min-height: 48px; } md-icon-button { margin-right: 8px; } .title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .actions { display: flex; max-width: 100%; align-items: center; } .nav-tabs { display: flex; margin-left: 24px; gap: 4px; } .nav-tab { padding: 8px 16px; color: var(--md-sys-color-on-primary); text-decoration: none; font-size: 0.875rem; font-weight: 500; border-radius: 4px 4px 0 0; opacity: 0.7; transition: opacity 0.2s; } .nav-tab:hover { opacity: 0.9; } .nav-tab.active { opacity: 1; background-color: rgba(255, 255, 255, 0.15); border-bottom: 2px solid var(--md-sys-color-on-primary); } .dev-badge { font-family: var(--monospace-font); font-size: 0.7rem; font-weight: 600; letter-spacing: 0.08em; padding: 4px 8px; margin-right: 8px; border-radius: 4px; background: var(--dev-color); color: var(--dev-on-color); border: none; cursor: pointer; line-height: 1; } .dev-badge:hover { background: color-mix(in srgb, var(--dev-color) 88%, white); } .dev-badge:focus-visible { outline: 2px solid var(--dev-on-color); outline-offset: 2px; } `, ]; }