mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
230 lines
7.2 KiB
TypeScript
230 lines
7.2 KiB
TypeScript
import './houseplan-card';
|
|
|
|
interface HouseplanCardElement extends HTMLElement {
|
|
hass?: unknown;
|
|
panelHost: boolean;
|
|
narrow: boolean | null;
|
|
setConfig(config: { type: 'custom:houseplan-card' }): void;
|
|
}
|
|
|
|
interface HomeAssistantPanelHost {
|
|
localize?: (key: string) => string | undefined;
|
|
}
|
|
|
|
/** Properties Home Assistant assigns to a custom panel element, in its order. */
|
|
const PRE_UPGRADE_PROPERTIES = ['panel', 'hass', 'narrow', 'route'] as const;
|
|
|
|
/** Home Assistant custom-panel shell; the existing card owns all product state. */
|
|
export class HouseplanPanel extends HTMLElement {
|
|
private _hass?: HomeAssistantPanelHost;
|
|
private _narrow = false;
|
|
private _route?: unknown;
|
|
private _panel?: unknown;
|
|
private _card?: HouseplanCardElement;
|
|
private _menuButton?: HTMLButtonElement;
|
|
|
|
public constructor() {
|
|
super();
|
|
this.attachShadow({ mode: 'open' });
|
|
this._adoptPreUpgradeProperties();
|
|
}
|
|
|
|
public connectedCallback(): void {
|
|
this._adoptPreUpgradeProperties();
|
|
this._ensureShell();
|
|
}
|
|
|
|
/** Home Assistant creates the panel element and assigns `hass`/`narrow`/
|
|
* `route`/`panel` as soon as the module script fires `load`; with the
|
|
* top-level `await import('./houseplan-card.js')` in this entry the class is
|
|
* defined a moment later, so those values land as OWN data properties that
|
|
* shadow the accessors below for the life of the element (#488). Re-route
|
|
* them through the accessors once, in HA's own assignment order. */
|
|
private _adoptPreUpgradeProperties(): void {
|
|
for (const key of PRE_UPGRADE_PROPERTIES) {
|
|
if (!Object.prototype.hasOwnProperty.call(this, key)) continue;
|
|
const value = (this as unknown as Record<string, unknown>)[key];
|
|
delete (this as unknown as Record<string, unknown>)[key];
|
|
(this as unknown as Record<string, unknown>)[key] = value;
|
|
}
|
|
}
|
|
|
|
public get hass(): HomeAssistantPanelHost | undefined { return this._hass; }
|
|
public set hass(value: HomeAssistantPanelHost | undefined) {
|
|
this._hass = value;
|
|
this._ensureShell();
|
|
if (this._card) this._card.hass = value;
|
|
this._syncMenuLabel();
|
|
}
|
|
|
|
public get narrow(): boolean { return this._narrow; }
|
|
public set narrow(value: boolean) {
|
|
this._narrow = Boolean(value);
|
|
this.toggleAttribute('narrow', this._narrow);
|
|
if (this._card) this._card.narrow = this._narrow;
|
|
}
|
|
|
|
public get route(): unknown { return this._route; }
|
|
public set route(value: unknown) { this._route = value; }
|
|
|
|
public get panel(): unknown { return this._panel; }
|
|
public set panel(value: unknown) { this._panel = value; }
|
|
|
|
private _ensureShell(): void {
|
|
if (this._card || !this.shadowRoot) return;
|
|
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
:host {
|
|
display: block;
|
|
width: 100%;
|
|
/* HA mounts a custom panel inside <ha-panel-custom>, a block with
|
|
* safe-area padding and NO height, so a percentage here resolves to
|
|
* auto and the stage collapses to 0 px (#488). Take the viewport
|
|
* like HA's own iframe panels do, minus the insets HA pads with. */
|
|
height: 100vh;
|
|
height: calc(100dvh - var(--safe-area-inset-top, 0px) - var(--safe-area-inset-bottom, 0px));
|
|
min-width: 0;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
color: var(--primary-text-color);
|
|
background: var(--primary-background-color);
|
|
}
|
|
.page {
|
|
width: 100%;
|
|
height: 100%;
|
|
min-width: 0;
|
|
min-height: 0;
|
|
display: grid;
|
|
grid-template-rows: auto minmax(0, 1fr);
|
|
overflow: hidden;
|
|
}
|
|
.appbar {
|
|
box-sizing: border-box;
|
|
min-width: 0;
|
|
min-height: var(--header-height, 56px);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 0 16px;
|
|
background: var(--app-header-background-color, var(--primary-background-color));
|
|
color: var(--app-header-text-color, var(--primary-text-color));
|
|
border-bottom: 1px solid var(--divider-color);
|
|
z-index: 1;
|
|
}
|
|
.toolbar {
|
|
min-width: 0;
|
|
width: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
.menu {
|
|
box-sizing: border-box;
|
|
width: 44px;
|
|
height: 44px;
|
|
min-width: 44px;
|
|
min-height: 44px;
|
|
margin: 0;
|
|
padding: 0;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: 0;
|
|
border-radius: 50%;
|
|
color: inherit;
|
|
background: transparent;
|
|
cursor: pointer;
|
|
}
|
|
.menu:focus-visible {
|
|
outline: 2px solid var(--primary-color);
|
|
outline-offset: 2px;
|
|
}
|
|
@media (hover: hover) {
|
|
.menu:hover { background: var(--secondary-background-color); }
|
|
}
|
|
.menu ha-icon { --mdc-icon-size: 24px; }
|
|
.title {
|
|
min-width: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
font-size: 20px;
|
|
font-weight: 500;
|
|
line-height: 1.2;
|
|
}
|
|
.title ha-icon {
|
|
--mdc-icon-size: 24px;
|
|
color: var(--primary-color);
|
|
}
|
|
.content {
|
|
min-width: 0;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
}
|
|
houseplan-card {
|
|
width: 100%;
|
|
height: 100%;
|
|
min-width: 0;
|
|
min-height: 0;
|
|
}
|
|
`;
|
|
|
|
const page = document.createElement('div');
|
|
page.className = 'page';
|
|
const appbar = document.createElement('header');
|
|
appbar.className = 'appbar';
|
|
const toolbar = document.createElement('div');
|
|
toolbar.className = 'toolbar';
|
|
toolbar.setAttribute('role', 'toolbar');
|
|
|
|
const menu = document.createElement('button');
|
|
menu.className = 'menu';
|
|
menu.dataset.hp = 'panel-menu';
|
|
menu.type = 'button';
|
|
menu.innerHTML = '<ha-icon icon="mdi:menu"></ha-icon>';
|
|
menu.addEventListener('click', () => {
|
|
menu.dispatchEvent(new CustomEvent('hass-toggle-menu', {
|
|
bubbles: true,
|
|
composed: true,
|
|
}));
|
|
});
|
|
this._menuButton = menu;
|
|
|
|
const title = document.createElement('div');
|
|
title.className = 'title';
|
|
title.dataset.hp = 'panel-title';
|
|
title.setAttribute('role', 'heading');
|
|
title.setAttribute('aria-level', '1');
|
|
title.innerHTML = '<ha-icon icon="mdi:floor-plan"></ha-icon><span>House Plan</span>';
|
|
toolbar.append(menu, title);
|
|
appbar.append(toolbar);
|
|
|
|
const content = document.createElement('main');
|
|
content.className = 'content';
|
|
const card = document.createElement('houseplan-card') as HouseplanCardElement;
|
|
card.panelHost = true;
|
|
card.narrow = this._narrow;
|
|
card.setConfig({ type: 'custom:houseplan-card' });
|
|
if (this._hass !== undefined) card.hass = this._hass;
|
|
content.append(card);
|
|
page.append(appbar, content);
|
|
this.shadowRoot.append(style, page);
|
|
this._card = card;
|
|
this._syncMenuLabel();
|
|
}
|
|
|
|
private _syncMenuLabel(): void {
|
|
if (!this._menuButton) return;
|
|
const label = this._hass?.localize?.('ui.common.menu')
|
|
|| this._hass?.localize?.('ui.panel.lovelace.menu')
|
|
|| 'Menu';
|
|
this._menuButton.setAttribute('aria-label', label);
|
|
this._menuButton.title = label;
|
|
}
|
|
}
|
|
|
|
if (!customElements.get('houseplan-panel')) {
|
|
customElements.define('houseplan-panel', HouseplanPanel);
|
|
}
|