Compare commits

...

2 Commits

Author SHA1 Message Date
Paul Bottein
f804c1979a Fix look and feel 2026-01-14 15:07:28 +01:00
Paul Bottein
ecb9228dc6 Add heading badge button 2026-01-14 14:35:45 +01:00
6 changed files with 373 additions and 2 deletions

View File

@@ -153,7 +153,7 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
flex-direction: row;
justify-content: space-between;
align-items: center;
overflow: hidden;
overflow: visible;
gap: var(--ha-space-2);
}
.content:hover ha-icon-next {

View File

@@ -1,3 +1,4 @@
import "../heading-badges/hui-button-heading-badge";
import "../heading-badges/hui-entity-heading-badge";
import {
@@ -6,7 +7,7 @@ import {
} from "./create-element-base";
import type { LovelaceHeadingBadgeConfig } from "../heading-badges/types";
const ALWAYS_LOADED_TYPES = new Set(["error", "entity"]);
const ALWAYS_LOADED_TYPES = new Set(["error", "entity", "button"]);
export const createHeadingBadgeElement = (config: LovelaceHeadingBadgeConfig) =>
createLovelaceElement(

View File

@@ -0,0 +1,218 @@
import { mdiEye, mdiGestureTap } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { any, array, assert, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-form/ha-form";
import type {
HaFormSchema,
SchemaUnion,
} from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import type { Condition } from "../../common/validate-condition";
import type { ButtonHeadingBadgeConfig } from "../../heading-badges/types";
import type { LovelaceGenericElementEditor } from "../../types";
import "../conditions/ha-card-conditions-editor";
import { configElementStyle } from "../config-elements/config-elements-style";
import { actionConfigStruct } from "../structs/action-struct";
const buttonConfigStruct = object({
type: optional(string()),
text: optional(string()),
icon: optional(string()),
color: optional(string()),
tap_action: optional(actionConfigStruct),
hold_action: optional(actionConfigStruct),
double_tap_action: optional(actionConfigStruct),
visibility: optional(array(any())),
});
@customElement("hui-button-heading-badge-editor")
export class HuiButtonHeadingBadgeEditor
extends LitElement
implements LovelaceGenericElementEditor
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean }) public preview = false;
@state() private _config?: ButtonHeadingBadgeConfig;
public setConfig(config: ButtonHeadingBadgeConfig): void {
assert(config, buttonConfigStruct);
this._config = config;
}
private _schema = memoizeOne(
() =>
[
{
name: "text",
selector: { text: {} },
},
{
name: "",
type: "grid",
schema: [
{
name: "icon",
selector: { icon: {} },
},
{
name: "color",
selector: {
ui_color: {},
},
},
],
},
{
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
default_action: "none",
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: (["hold_action", "double_tap_action"] as const).map(
(action) => ({
name: action,
selector: {
ui_action: {
default_action: "none" as const,
},
},
})
),
},
],
},
] as const satisfies readonly HaFormSchema[]
);
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
const schema = this._schema();
const conditions = this._config.visibility ?? [];
return html`
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
<ha-expansion-panel outlined>
<ha-svg-icon slot="leading-icon" .path=${mdiEye}></ha-svg-icon>
<h3 slot="header">
${this.hass!.localize(
"ui.panel.lovelace.editor.card.heading.button_config.visibility"
)}
</h3>
<div class="content">
<p class="intro">
${this.hass.localize(
"ui.panel.lovelace.editor.card.heading.button_config.visibility_explanation"
)}
</p>
<ha-card-conditions-editor
.hass=${this.hass}
.conditions=${conditions}
@value-changed=${this._conditionChanged}
>
</ha-card-conditions-editor>
</div>
</ha-expansion-panel>
`;
}
private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
if (!this._config || !this.hass) {
return;
}
const config = ev.detail.value as ButtonHeadingBadgeConfig;
fireEvent(this, "config-changed", { config });
}
private _conditionChanged(ev: CustomEvent): void {
ev.stopPropagation();
if (!this._config || !this.hass) {
return;
}
const conditions = ev.detail.value as Condition[];
const newConfig: ButtonHeadingBadgeConfig = {
...this._config,
visibility: conditions,
};
if (newConfig.visibility?.length === 0) {
delete newConfig.visibility;
}
fireEvent(this, "config-changed", { config: newConfig });
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
switch (schema.name) {
case "text":
case "color":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.heading.button_config.${schema.name}`
);
default:
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
);
}
};
static get styles() {
return [
configElementStyle,
css`
.container {
display: flex;
flex-direction: column;
}
ha-form {
display: block;
margin-bottom: 24px;
}
.intro {
margin: 0;
color: var(--secondary-text-color);
margin-bottom: 8px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-button-heading-badge-editor": HuiButtonHeadingBadgeEditor;
}
}

View File

@@ -0,0 +1,136 @@
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { classMap } from "lit/directives/class-map";
import { computeCssColor } from "../../../common/color/compute-color";
import "../../../components/ha-control-button";
import "../../../components/ha-icon";
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import type { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
import type {
LovelaceHeadingBadge,
LovelaceHeadingBadgeEditor,
} from "../types";
import type { ButtonHeadingBadgeConfig } from "./types";
const DEFAULT_ACTIONS: Pick<
ButtonHeadingBadgeConfig,
"tap_action" | "hold_action" | "double_tap_action"
> = {
tap_action: { action: "none" },
hold_action: { action: "none" },
double_tap_action: { action: "none" },
};
@customElement("hui-button-heading-badge")
export class HuiButtonHeadingBadge
extends LitElement
implements LovelaceHeadingBadge
{
public static async getConfigElement(): Promise<LovelaceHeadingBadgeEditor> {
await import("../editor/heading-badge-editor/hui-button-heading-badge-editor");
return document.createElement("hui-button-heading-badge-editor");
}
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: ButtonHeadingBadgeConfig;
@property({ type: Boolean }) public preview = false;
public setConfig(config: ButtonHeadingBadgeConfig): void {
this._config = {
...DEFAULT_ACTIONS,
...config,
};
}
get hasAction() {
return (
hasAction(this._config?.tap_action) ||
hasAction(this._config?.hold_action) ||
hasAction(this._config?.double_tap_action)
);
}
private _handleAction(ev: ActionHandlerEvent) {
handleAction(this, this.hass!, this._config!, ev.detail.action!);
}
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
const config = this._config;
const color = config.color ? computeCssColor(config.color) : undefined;
const style = { "--color": color };
return html`
<ha-control-button
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config!.hold_action),
hasDoubleClick: hasAction(this._config!.double_tap_action),
})}
style=${styleMap(style)}
.label=${config.text}
class=${classMap({ colored: !!color, "with-text": !!config.text })}
>
<span class="content">
${config.icon
? html`<ha-icon .icon=${config.icon}></ha-icon>`
: nothing}
${config.text
? html`<span class="text">${config.text}</span>`
: nothing}
</span>
</ha-control-button>
`;
}
static styles = css`
ha-control-button {
--control-button-border-radius: var(
--ha-heading-badge-border-radius,
var(--ha-border-radius-pill)
);
--control-button-padding: 0;
--mdc-icon-size: var(--ha-heading-badge-icon-size, 14px);
width: auto;
height: var(--ha-heading-badge-size, 26px);
min-width: var(--ha-heading-badge-size, 26px);
font-size: var(--ha-font-size-s);
}
ha-control-button.with-text {
--control-button-padding: 0 var(--ha-space-2);
}
ha-control-button.colored {
--control-button-icon-color: var(--color);
--control-button-background-color: var(--color);
--control-button-focus-color: var(--color);
--ha-ripple-color: var(--color);
}
.content {
display: flex;
flex-direction: row;
align-items: center;
white-space: nowrap;
}
.text {
padding: 0 var(--ha-space-1);
line-height: 1;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"hui-button-heading-badge": HuiButtonHeadingBadge;
}
}

View File

@@ -26,3 +26,13 @@ export interface EntityHeadingBadgeConfig extends LovelaceHeadingBadgeConfig {
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
}
export interface ButtonHeadingBadgeConfig extends LovelaceHeadingBadgeConfig {
type: "button";
text?: string;
icon?: string;
color?: string;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
}

View File

@@ -8198,6 +8198,12 @@
"state": "[%key:ui::panel::lovelace::editor::badge::entity::displayed_elements_options::state%]"
}
},
"button_config": {
"Text": "Text",
"color": "Color",
"visibility": "Visibility",
"visibility_explanation": "The button will be shown when ALL conditions below are fulfilled. If no conditions are set, the button will always be shown."
},
"default_heading": "Kitchen"
},
"map": {