Compare commits

...

9 Commits

Author SHA1 Message Date
Aidan Timson
bf519348b6 Not sure this matters 2026-03-12 14:40:05 +00:00
Aidan Timson
18872aef8a Move 2026-03-12 14:32:11 +00:00
Aidan Timson
9dd7040e62 Cleanup 2026-03-12 13:01:41 +00:00
Aidan Timson
5192612dc9 Improve tile 2026-03-12 13:00:00 +00:00
Aidan Timson
9227b84494 Tile closer 2026-03-12 12:56:00 +00:00
Aidan Timson
4bfbc33f88 Basic editor 2026-03-12 12:35:58 +00:00
Aidan Timson
1b3a998c67 Prefabs 2026-03-12 12:31:41 +00:00
Aidan Timson
6a2bdf1c07 Basic card 2026-03-12 12:15:22 +00:00
Aidan Timson
01acec1858 Base component 2026-03-12 12:14:16 +00:00
6 changed files with 223 additions and 24 deletions

View File

@@ -0,0 +1,80 @@
import "@home-assistant/webawesome/dist/components/skeleton/skeleton";
import { css, html, LitElement } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-skeleton-tile")
export class HaSkeletonTile extends LitElement {
protected render() {
return html`
<div class="tile">
<wa-skeleton class="icon" effect="sheen"></wa-skeleton>
<div class="info">
<wa-skeleton class="primary" effect="sheen"></wa-skeleton>
<wa-skeleton class="secondary" effect="sheen"></wa-skeleton>
</div>
</div>
`;
}
static styles = css`
wa-skeleton {
width: 100%;
height: 100%;
--color: var(--ha-skeleton-color, var(--secondary-background-color));
--sheen-color: var(
--ha-skeleton-sheen-color,
color-mix(
in srgb,
var(--ha-skeleton-color, var(--secondary-background-color)) 70%,
white
)
);
}
wa-skeleton::part(indicator) {
border-radius: var(--ha-border-radius-sm);
}
.tile {
display: flex;
align-items: center;
gap: var(--ha-space-4);
height: 100%;
}
.icon {
width: 30px;
height: 30px;
flex: none;
}
.icon::part(indicator) {
border-radius: var(--ha-border-radius-circle);
}
.info {
display: flex;
flex: 1;
min-width: 0;
flex-direction: column;
gap: 5px;
justify-content: center;
}
.primary {
height: 18px;
width: min(68%, 240px);
}
.secondary {
height: 14px;
width: min(22%, 84px);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-skeleton-tile": HaSkeletonTile;
}
}

View File

@@ -0,0 +1,97 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../components/ha-card";
import "../../../components/skeletons/skeleton-card-tile";
import type { HaFormSchema } from "../../../components/ha-form/types";
import type { HomeAssistant } from "../../../types";
import type {
LovelaceCard,
LovelaceConfigForm,
LovelaceGridOptions,
} from "../types";
import type { SkeletonCardConfig } from "./types";
const SCHEMA = [
{
name: "mode",
selector: {
select: {
mode: "dropdown",
options: ["tile"],
},
},
},
] as const satisfies readonly HaFormSchema[];
@customElement("hui-skeleton-card")
export class HuiSkeletonCard extends LitElement implements LovelaceCard {
public static getConfigForm(): LovelaceConfigForm {
return {
schema: [...SCHEMA],
computeLabel: (schema, localize) =>
schema.name === "mode"
? localize("ui.panel.lovelace.editor.card.skeleton.mode")
: undefined,
};
}
@property({ attribute: false }) public hass?: HomeAssistant;
private _config?: SkeletonCardConfig;
public getCardSize(): number {
switch (this._config?.mode ?? "tile") {
case "tile":
default:
return 1;
}
}
public getGridOptions(): LovelaceGridOptions {
switch (this._config?.mode ?? "tile") {
case "tile":
default:
return {
columns: 6,
rows: 1,
min_columns: 6,
min_rows: 1,
};
}
}
public setConfig(config: SkeletonCardConfig): void {
this._config = {
mode: "tile",
...config,
};
}
protected render() {
return html`
<ha-card>
<ha-skeleton-tile></ha-skeleton-tile>
</ha-card>
`;
}
static styles = css`
:host {
display: block;
height: 100%;
}
ha-card {
height: 100%;
padding: var(--ha-space-2);
height: 100%;
box-sizing: border-box;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"hui-skeleton-card": HuiSkeletonCard;
}
}

View File

@@ -12,6 +12,7 @@ import { computeDomain } from "../../../common/entity/compute_domain";
import { stateActive } from "../../../common/entity/state_active";
import { stateColorCss } from "../../../common/entity/state_color";
import "../../../components/ha-card";
import "../../../components/skeletons/skeleton-card-tile";
import "../../../components/ha-state-icon";
import "../../../components/tile/ha-tile-badge";
import "../../../components/tile/ha-tile-container";
@@ -27,7 +28,6 @@ import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-ent
import { findEntities } from "../common/find-entities";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import type {
LovelaceCard,
LovelaceCardEditor,
@@ -237,37 +237,44 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
return features;
});
private _shouldRenderSkeleton(stateObj?: HassEntity) {
return !this.hass || !stateObj;
}
private _renderSkeleton() {
return html`
<ha-card class="skeleton">
<ha-skeleton-tile></ha-skeleton-tile>
</ha-card>
`;
}
protected render() {
if (!this._config || !this.hass) {
if (!this._config) {
return nothing;
}
const entityId = this._config.entity;
const stateObj = entityId ? this.hass.states[entityId] : undefined;
if (!stateObj) {
return html`
<hui-warning .hass=${this.hass}>
${createEntityNotFoundWarning(this.hass, this._config.entity)}
</hui-warning>
`;
const entityId = this._config.entity;
const stateObj = entityId ? this.hass?.states[entityId] : undefined;
if (this._shouldRenderSkeleton(stateObj)) {
return this._renderSkeleton();
}
const name = computeLovelaceEntityName(
this.hass,
stateObj,
this._config.name
);
const hass = this.hass!;
const active = stateActive(stateObj);
const color = this._computeStateColor(stateObj, this._config.color);
const domain = computeDomain(stateObj.entity_id);
const name = computeLovelaceEntityName(hass, stateObj!, this._config.name);
const active = stateActive(stateObj!);
const color = this._computeStateColor(stateObj!, this._config.color);
const domain = computeDomain(stateObj!.entity_id);
const stateDisplay = this._config.hide_state
? nothing
: html`
<state-display
.stateObj=${stateObj}
.hass=${this.hass}
.hass=${hass}
.content=${this._config.state_content}
>
</state-display>
@@ -278,7 +285,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
};
const imageUrl = this._config.show_entity_picture
? this._getImageUrl(stateObj)
? this._getImageUrl(stateObj!)
: undefined;
const featurePosition = this._featurePosition(this._config);
@@ -308,7 +315,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
.interactive=${this._hasIconAction}
.imageUrl=${imageUrl}
data-domain=${ifDefined(domain)}
data-state=${ifDefined(stateObj?.state)}
data-state=${ifDefined(stateObj!.state)}
class=${classMap({ image: hasImage })}
>
${hasImage
@@ -318,10 +325,10 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
slot="icon"
.icon=${this._config.icon}
.stateObj=${stateObj}
.hass=${this.hass}
.hass=${hass}
></ha-state-icon>
`}
${renderTileBadge(stateObj, this.hass)}
${renderTileBadge(stateObj!, hass)}
</ha-tile-icon>
<ha-tile-info slot="info">
<span slot="primary" class="primary">${name}</span>
@@ -333,7 +340,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
? html`
<hui-card-features
slot="features"
.hass=${this.hass}
.hass=${hass}
.context=${this._featureContext}
.color=${this._config.color}
.features=${features}
@@ -354,6 +361,10 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
ha-card.active {
--tile-color: var(--state-icon-color);
}
ha-card.skeleton {
box-sizing: border-box;
padding: 10px;
}
ha-tile-badge {
position: absolute;
top: 3px;

View File

@@ -600,6 +600,11 @@ export interface SensorCardConfig extends LovelaceCardConfig {
};
}
export interface SkeletonCardConfig extends LovelaceCardConfig {
type: "skeleton";
mode?: "tile";
}
export interface TodoListCardConfig extends LovelaceCardConfig {
title?: string;
theme?: string;

View File

@@ -95,6 +95,7 @@ const LAZY_LOAD_TYPES = {
picture: () => import("../cards/hui-picture-card"),
"plant-status": () => import("../cards/hui-plant-status-card"),
"recovery-mode": () => import("../cards/hui-recovery-mode-card"),
skeleton: () => import("../cards/hui-skeleton-card"),
"toggle-group": () => import("../cards/hui-toggle-group-card"),
"todo-list": () => import("../cards/hui-todo-list-card"),
"shopping-list": () => import("../cards/hui-shopping-list-card"),

View File

@@ -9179,6 +9179,11 @@
}
}
},
"skeleton": {
"name": "Skeleton",
"description": "This card displays a placeholder skeleton layout.",
"mode": "Mode"
},
"media-control": {
"name": "Media control",
"description": "This card is used to display media player entities on an interface with easy to use controls."