Compare commits

..

2 Commits

Author SHA1 Message Date
Aidan Timson
1aa55b3071 Add close button when not confirmation/alert dialog 2025-10-28 10:15:03 +00:00
Aidan Timson
aa21eff508 Migrate generic dialog-box to ha-wa-dialog 2025-10-28 10:15:03 +00:00
20 changed files with 125 additions and 678 deletions

2
.nvmrc
View File

@@ -1 +1 @@
22.21.1
22.21.0

View File

@@ -5,14 +5,14 @@ subtitle: Dialogs provide important prompts in a user flow.
# Material Design 3
Our dialogs are based on the latest version of Material Design. Please note that we have made some well-considered adjustments to these guidelines. Specs and guidelines can be found on its [website](https://m3.material.io/components/dialogs/overview).
Our dialogs are based on the latest version of Material Design. Please note that we have made some well-considered adjustments to these guideliness. Specs and guidelines can be found on its [website](https://m3.material.io/components/dialogs/overview).
# Guidelines
## Design
- Dialogs have a max width of 560px. Alert and confirmation dialogs have a fixed width of 320px. If you need more width, consider a dedicated page instead.
- The close X-icon is on the top left, on all screen sizes. Except for alert and confirmation dialogs, they only have buttons and no X-icon. This is different compared to the Material guidelines.
- Dialogs have a max width of 560px. Alert and confirmation dialogs got a fixed width of 320px. If you need more width, consider a dedicated page instead.
- The close X-icon is on the top left, on all screen sizes. Except for alert and confirmation dialogs, they only have buttons and no X-icon. This is different compared to the Material guideliness.
- Dialogs can't be closed with ESC or clicked outside of the dialog when there is a form that the user needs to fill out. Instead it will animate "no" by a little shake.
- Extra icon buttons are on the top right, for example help, settings and expand dialog. More than 2 icon buttons, they will be in an overflow menu.
- The submit button is grouped with a cancel button at the bottom right, on all screen sizes. Fullscreen mobile dialogs have them sticky at the bottom.
@@ -26,7 +26,7 @@ Our dialogs are based on the latest version of Material Design. Please note that
- A best practice is to always use a title, even if it is optional by Material guidelines.
- People mainly read the title and a button. Put the most important information in those two.
- Try to avoid user generated content in the title, this could make the title unreadably long.
- Try to avoid user generated content in the title, this could make the title unreadable long.
- If users become unsure, they read the description. Make sure this explains what will happen.
- Strive for minimalism.

View File

@@ -1,116 +0,0 @@
export interface SwipeGestureResult {
velocity: number;
delta: number;
isSwipe: boolean;
isDownwardSwipe: boolean;
}
export interface SwipeGestureConfig {
velocitySwipeThreshold?: number;
movementTimeThreshold?: number;
}
const VELOCITY_SWIPE_THRESHOLD = 0.5; // px/ms
const MOVEMENT_TIME_THRESHOLD = 100; // ms
/**
* Recognizes swipe gestures and calculates velocity for touch interactions.
* Tracks touch movement and provides velocity-based and position-based gesture detection.
*/
export class SwipeGestureRecognizer {
private _startY = 0;
private _delta = 0;
private _startTime = 0;
private _lastY = 0;
private _lastTime = 0;
private _velocityThreshold: number;
private _movementTimeThreshold: number;
constructor(config: SwipeGestureConfig = {}) {
this._velocityThreshold =
config.velocitySwipeThreshold ?? VELOCITY_SWIPE_THRESHOLD; // px/ms
this._movementTimeThreshold =
config.movementTimeThreshold ?? MOVEMENT_TIME_THRESHOLD; // ms
}
/**
* Initialize gesture tracking with starting touch position
*/
public start(clientY: number): void {
const now = Date.now();
this._startY = clientY;
this._startTime = now;
this._lastY = clientY;
this._lastTime = now;
this._delta = 0;
}
/**
* Update gesture state during movement
* Returns the current delta (negative when dragging down)
*/
public move(clientY: number): number {
const now = Date.now();
this._delta = this._startY - clientY;
this._lastY = clientY;
this._lastTime = now;
return this._delta;
}
/**
* Calculate final gesture result when touch ends
*/
public end(): SwipeGestureResult {
const velocity = this.getVelocity();
const hasSignificantVelocity = Math.abs(velocity) > this._velocityThreshold;
return {
velocity,
delta: this._delta,
isSwipe: hasSignificantVelocity,
isDownwardSwipe: velocity > 0,
};
}
/**
* Get current drag delta (negative when dragging down)
*/
public getDelta(): number {
return this._delta;
}
/**
* Calculate velocity based on recent movement
* Returns 0 if no recent movement detected
* Positive velocity means downward swipe
*/
public getVelocity(): number {
const now = Date.now();
const timeSinceLastMove = now - this._lastTime;
// Only consider velocity if the last movement was recent
if (timeSinceLastMove >= this._movementTimeThreshold) {
return 0;
}
const timeDelta = this._lastTime - this._startTime;
return timeDelta > 0 ? (this._lastY - this._startY) / timeDelta : 0;
}
/**
* Reset all tracking state
*/
public reset(): void {
this._startY = 0;
this._delta = 0;
this._startTime = 0;
this._lastY = 0;
this._lastTime = 0;
}
}

View File

@@ -1 +0,0 @@
export const ANY_STATE_VALUE = "__ANY_STATE_IGNORE_ATTRIBUTES__";

View File

@@ -4,7 +4,6 @@ import { customElement, property } from "lit/decorators";
import { keyed } from "lit/directives/keyed";
import { repeat } from "lit/directives/repeat";
import { fireEvent } from "../../common/dom/fire_event";
import { ANY_STATE_VALUE } from "./const";
import { ensureArray } from "../../common/array/ensure-array";
import type { HomeAssistant } from "../../types";
import "./ha-entity-state-picker";
@@ -58,7 +57,6 @@ export class HaEntityStatesPicker extends LitElement {
const value = this.value || [];
const hide = [...(this.hideStates || []), ...value];
const hideValue = value.includes(ANY_STATE_VALUE);
return html`
${repeat(
@@ -86,7 +84,7 @@ export class HaEntityStatesPicker extends LitElement {
`
)}
<div>
${(this.disabled && value.length) || hideValue
${this.disabled && value.length
? nothing
: keyed(
value.length,

View File

@@ -1,7 +1,6 @@
import "@home-assistant/webawesome/dist/components/drawer/drawer";
import { css, html, LitElement, type PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { SwipeGestureRecognizer } from "../common/util/swipe-gesture-recognizer";
import { customElement, property, state } from "lit/decorators";
import { haStyleScrollbar } from "../resources/styles";
export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300;
@@ -15,12 +14,6 @@ export class HaBottomSheet extends LitElement {
@state() private _drawerOpen = false;
@query("#drawer") private _drawer!: HTMLElement;
private _gestureRecognizer = new SwipeGestureRecognizer();
private _isDragging = false;
private _handleAfterHide() {
this.open = false;
const ev = new Event("closed", {
@@ -40,132 +33,19 @@ export class HaBottomSheet extends LitElement {
render() {
return html`
<wa-drawer
id="drawer"
placement="bottom"
.open=${this._drawerOpen}
@wa-after-hide=${this._handleAfterHide}
without-header
@touchstart=${this._handleTouchStart}
>
<slot name="header"></slot>
<div id="body" class="body ha-scrollbar">
<div class="body ha-scrollbar">
<slot></slot>
</div>
</wa-drawer>
`;
}
private _handleTouchStart = (ev: TouchEvent) => {
// Check if any element inside drawer in the composed path has scrollTop > 0
for (const path of ev.composedPath()) {
const el = path as HTMLElement;
if (el === this._drawer) {
break;
}
if (el.scrollTop > 0) {
return;
}
}
this._startResizing(ev.touches[0].clientY);
};
private _startResizing(clientY: number) {
// register event listeners for drag handling
document.addEventListener("touchmove", this._handleTouchMove, {
passive: false,
});
document.addEventListener("touchend", this._handleTouchEnd);
document.addEventListener("touchcancel", this._handleTouchEnd);
this._gestureRecognizer.start(clientY);
}
private _handleTouchMove = (ev: TouchEvent) => {
const currentY = ev.touches[0].clientY;
const delta = this._gestureRecognizer.move(currentY);
if (delta < 0) {
ev.preventDefault();
this._isDragging = true;
requestAnimationFrame(() => {
if (this._isDragging) {
this.style.setProperty(
"--dialog-transform",
`translateY(${delta * -1}px)`
);
}
});
}
};
private _animateSnapBack() {
// Add transition for smooth animation
this.style.setProperty(
"--dialog-transition",
`transform ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms ease-out`
);
// Reset transform to snap back
this.style.removeProperty("--dialog-transform");
// Remove transition after animation completes
setTimeout(() => {
this.style.removeProperty("--dialog-transition");
}, BOTTOM_SHEET_ANIMATION_DURATION_MS);
}
private _handleTouchEnd = () => {
this._unregisterResizeHandlers();
this._isDragging = false;
const result = this._gestureRecognizer.end();
// If velocity exceeds threshold, use velocity direction to determine action
if (result.isSwipe) {
if (result.isDownwardSwipe) {
// Downward swipe - close the bottom sheet
this._drawerOpen = false;
} else {
// Upward swipe - keep open and animate back
this._animateSnapBack();
}
return;
}
// If velocity is below threshold, use position-based logic
// Get the drawer height to calculate 50% threshold
const drawerBody = this._drawer.shadowRoot?.querySelector(
'[part="body"]'
) as HTMLElement;
const drawerHeight = drawerBody?.offsetHeight || 0;
// delta is negative when dragging down
// Close if dragged down past 50% of the drawer height
if (
drawerHeight > 0 &&
result.delta < 0 &&
Math.abs(result.delta) > drawerHeight * 0.5
) {
this._drawerOpen = false;
} else {
this._animateSnapBack();
}
};
private _unregisterResizeHandlers = () => {
document.removeEventListener("touchmove", this._handleTouchMove);
document.removeEventListener("touchend", this._handleTouchEnd);
document.removeEventListener("touchcancel", this._handleTouchEnd);
};
disconnectedCallback() {
super.disconnectedCallback();
this._unregisterResizeHandlers();
this._isDragging = false;
}
static styles = [
haStyleScrollbar,
css`
@@ -179,8 +59,6 @@ export class HaBottomSheet extends LitElement {
wa-drawer::part(dialog) {
max-height: var(--ha-bottom-sheet-max-height, 90vh);
align-items: center;
transform: var(--dialog-transform);
transition: var(--dialog-transition);
}
wa-drawer::part(body) {
max-width: var(--ha-bottom-sheet-max-width);
@@ -212,11 +90,6 @@ export class HaBottomSheet extends LitElement {
max-width: 100%;
display: flex;
flex-direction: column;
padding: var(
--ha-bottom-sheet-padding,
0 var(--safe-area-inset-right) var(--safe-area-inset-bottom)
var(--safe-area-inset-left)
);
}
`,
];

View File

@@ -435,9 +435,9 @@ export const convertStatisticsToHistory = (
Object.entries(orderedStatistics).forEach(([key, value]) => {
const entityHistoryStates: EntityHistoryState[] = value.map((e) => ({
s: e.mean != null ? e.mean.toString() : e.state!.toString(),
lc: e.end / 1000,
lc: e.start / 1000,
a: {},
lu: e.end / 1000,
lu: e.start / 1000,
}));
statsHistoryStates[key] = entityHistoryStates;
});

View File

@@ -1,15 +1,15 @@
import { mdiAlertOutline } from "@mdi/js";
import { mdiAlertOutline, mdiClose } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-button";
import "../../components/ha-dialog-footer";
import "../../components/ha-dialog-header";
import "../../components/ha-md-dialog";
import type { HaMdDialog } from "../../components/ha-md-dialog";
import "../../components/ha-svg-icon";
import "../../components/ha-textfield";
import type { HaTextField } from "../../components/ha-textfield";
import "../../components/ha-wa-dialog";
import type { HomeAssistant } from "../../types";
import type { DialogBoxParams } from "./show-dialog-box";
@@ -19,12 +19,12 @@ class DialogBox extends LitElement {
@state() private _params?: DialogBoxParams;
@state() private _open = false;
@state() private _closeState?: "canceled" | "confirmed";
@query("ha-textfield") private _textField?: HaTextField;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
private _closePromise?: Promise<void>;
private _closeResolve?: () => void;
@@ -34,6 +34,7 @@ class DialogBox extends LitElement {
await this._closePromise;
}
this._params = params;
this._open = true;
}
public closeDialog(): boolean {
@@ -60,16 +61,25 @@ class DialogBox extends LitElement {
this.hass.localize("ui.dialogs.generic.default_confirmation_title"));
return html`
<ha-md-dialog
open
.disableCancelAction=${confirmPrompt}
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
?prevent-scrim-close=${confirmPrompt}
@closed=${this._dialogClosed}
type="alert"
aria-labelledby="dialog-box-title"
aria-describedby="dialog-box-description"
>
<div slot="headline">
<span .title=${dialogTitle} id="dialog-box-title">
<ha-dialog-header slot="header">
${!confirmPrompt
? html`<slot name="headerNavigationIcon" slot="navigationIcon">
<ha-icon-button
data-dialog="close"
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
.path=${mdiClose}
></ha-icon-button
></slot>`
: nothing}
<span slot="title" id="dialog-box-title">
${this._params.warning
? html`<ha-svg-icon
.path=${mdiAlertOutline}
@@ -78,13 +88,13 @@ class DialogBox extends LitElement {
: nothing}
${dialogTitle}
</span>
</div>
<div slot="content" id="dialog-box-description">
</ha-dialog-header>
<div id="dialog-box-description">
${this._params.text ? html` <p>${this._params.text}</p> ` : ""}
${this._params.prompt
? html`
<ha-textfield
dialogInitialFocus
autofocus
value=${ifDefined(this._params.defaultValue)}
.placeholder=${this._params.placeholder}
.label=${this._params.inputLabel
@@ -99,10 +109,11 @@ class DialogBox extends LitElement {
`
: ""}
</div>
<div slot="actions">
<ha-dialog-footer slot="footer">
${confirmPrompt
? html`
<ha-button
slot="secondaryAction"
@click=${this._dismiss}
?autofocus=${!this._params.prompt && this._params.destructive}
appearance="plain"
@@ -114,6 +125,7 @@ class DialogBox extends LitElement {
`
: nothing}
<ha-button
slot="primaryAction"
@click=${this._confirm}
?autofocus=${!this._params.prompt && !this._params.destructive}
variant=${this._params.destructive ? "danger" : "brand"}
@@ -122,8 +134,8 @@ class DialogBox extends LitElement {
? this._params.confirmText
: this.hass.localize("ui.common.ok")}
</ha-button>
</div>
</ha-md-dialog>
</ha-dialog-footer>
</ha-wa-dialog>
`;
}
@@ -148,8 +160,7 @@ class DialogBox extends LitElement {
}
private _closeDialog() {
fireEvent(this, "dialog-closed", { dialog: this.localName });
this._dialog?.close();
this._open = false;
this._closePromise = new Promise((resolve) => {
this._closeResolve = resolve;
});
@@ -162,6 +173,7 @@ class DialogBox extends LitElement {
}
this._closeState = undefined;
this._params = undefined;
this._open = false;
this._closeResolve?.();
this._closeResolve = undefined;
}

View File

@@ -19,7 +19,6 @@ import { ensureArray } from "../../../../../common/array/ensure-array";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { hasTemplate } from "../../../../../common/string/has-template";
import type { StateTrigger } from "../../../../../data/automation";
import { ANY_STATE_VALUE } from "../../../../../components/entity/const";
import type { HomeAssistant } from "../../../../../types";
import { baseTriggerStruct, forDictStruct } from "../../structs";
import type { TriggerElement } from "../ha-automation-trigger-row";
@@ -37,12 +36,14 @@ const stateTriggerStruct = assign(
trigger: literal("state"),
entity_id: optional(union([string(), array(string())])),
attribute: optional(string()),
from: optional(union([nullable(string()), array(string())])),
to: optional(union([nullable(string()), array(string())])),
from: optional(nullable(string())),
to: optional(nullable(string())),
for: optional(union([number(), string(), forDictStruct])),
})
);
const ANY_STATE_VALUE = "__ANY_STATE_IGNORE_ATTRIBUTES__";
@customElement("ha-automation-trigger-state")
export class HaStateTrigger extends LitElement implements TriggerElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -56,12 +57,7 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
}
private _schema = memoizeOne(
(
localize: LocalizeFunc,
attribute: string | undefined,
hideInFrom: string[],
hideInTo: string[]
) =>
(localize: LocalizeFunc, attribute) =>
[
{
name: "entity_id",
@@ -135,7 +131,6 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
},
selector: {
state: {
multiple: true,
extra_options: (attribute
? []
: [
@@ -147,7 +142,6 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
},
]) as any,
attribute: attribute,
hide_states: hideInFrom,
},
},
},
@@ -158,7 +152,6 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
},
selector: {
state: {
multiple: true,
extra_options: (attribute
? []
: [
@@ -170,7 +163,6 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
},
]) as any,
attribute: attribute,
hide_states: hideInTo,
},
},
},
@@ -215,15 +207,13 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
entity_id: ensureArray(this.trigger.entity_id),
for: trgFor,
};
data.to = this._normalizeStates(this.trigger.to, data.attribute);
data.from = this._normalizeStates(this.trigger.from, data.attribute);
const schema = this._schema(
this.hass.localize,
this.trigger.attribute,
data.to,
data.from
);
if (!data.attribute && data.to === null) {
data.to = ANY_STATE_VALUE;
}
if (!data.attribute && data.from === null) {
data.from = ANY_STATE_VALUE;
}
const schema = this._schema(this.hass.localize, this.trigger.attribute);
return html`
<ha-form
@@ -241,58 +231,22 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
ev.stopPropagation();
const newTrigger = ev.detail.value;
newTrigger.to = this._applyAnyStateExclusive(
newTrigger.to,
newTrigger.attribute
);
newTrigger.from = this._applyAnyStateExclusive(
newTrigger.from,
newTrigger.attribute
);
if (newTrigger.to === ANY_STATE_VALUE) {
newTrigger.to = newTrigger.attribute ? undefined : null;
}
if (newTrigger.from === ANY_STATE_VALUE) {
newTrigger.from = newTrigger.attribute ? undefined : null;
}
Object.keys(newTrigger).forEach((key) => {
const val = newTrigger[key];
if (
val === undefined ||
val === "" ||
(Array.isArray(val) && val.length === 0)
) {
delete newTrigger[key];
}
});
Object.keys(newTrigger).forEach((key) =>
newTrigger[key] === undefined || newTrigger[key] === ""
? delete newTrigger[key]
: {}
);
fireEvent(this, "value-changed", { value: newTrigger });
}
private _applyAnyStateExclusive(
val: string | string[] | null | undefined,
attribute?: string
): string | string[] | null | undefined {
const anyStateSelected = Array.isArray(val)
? val.includes(ANY_STATE_VALUE)
: val === ANY_STATE_VALUE;
if (anyStateSelected) {
// Any state is exclusive: null if no attribute, undefined if attribute
return attribute ? undefined : null;
}
return val;
}
private _normalizeStates(
value: string | string[] | null | undefined,
attribute?: string
): string[] {
// If no attribute is selected and backend value is null,
// expose it as the special ANY state option in the UI.
if (!attribute && value === null) {
return [ANY_STATE_VALUE];
}
if (value === undefined || value === null) {
return [];
}
return ensureArray(value);
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string =>

View File

@@ -313,14 +313,9 @@ class HaPanelHistory extends LitElement {
return;
}
const statsStartDate = new Date(this._startDate);
// History uses the end datapoint of the statistic, so if we want the
// graph to start at 7AM, need to fetch the statistic from 6AM.
statsStartDate.setHours(statsStartDate.getHours() - 1);
const statistics = await fetchStatistics(
this.hass!,
statsStartDate,
this._startDate,
this._endDate,
statisticIds,
"hour",

View File

@@ -1,126 +0,0 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-control-number-buttons";
import { isUnavailableState } from "../../../data/entity";
import {
MediaPlayerEntityFeature,
type MediaPlayerEntity,
} from "../../../data/media-player";
import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
LovelaceCardFeatureContext,
MediaPlayerVolumeButtonsCardFeatureConfig,
} from "./types";
import { clamp } from "../../../common/number/clamp";
export const supportsMediaPlayerVolumeButtonsCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return (
domain === "media_player" &&
supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_SET)
);
};
@customElement("hui-media-player-volume-buttons-card-feature")
class HuiMediaPlayerVolumeButtonsCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: MediaPlayerVolumeButtonsCardFeatureConfig;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id] as
| MediaPlayerEntity
| undefined;
}
static getStubConfig(): MediaPlayerVolumeButtonsCardFeatureConfig {
return {
type: "media-player-volume-buttons",
step: 5,
};
}
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
await import(
"../editor/config-elements/hui-media-player-volume-buttons-card-feature-editor"
);
return document.createElement(
"hui-media-player-volume-buttons-card-feature-editor"
);
}
public setConfig(config: MediaPlayerVolumeButtonsCardFeatureConfig): void {
if (!config) {
throw new Error("Invalid configuration");
}
this._config = config;
}
protected render() {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsMediaPlayerVolumeButtonsCardFeature(this.hass, this.context)
) {
return nothing;
}
const position =
this._stateObj.attributes.volume_level != null
? Math.round(this._stateObj.attributes.volume_level * 100)
: undefined;
return html`
<ha-control-number-buttons
.disabled=${!this._stateObj || isUnavailableState(this._stateObj.state)}
.locale=${this.hass.locale}
min="0"
max="100"
.step=${this._config.step ?? 5}
.value=${position}
unit="%"
@value-changed=${this._valueChanged}
></ha-control-number-buttons>
`;
}
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
this.hass!.callService("media_player", "volume_set", {
entity_id: this._stateObj!.entity_id,
volume_level: clamp(ev.detail.value, 0, 100) / 100,
});
}
static get styles() {
return cardFeatureStyles;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-media-player-volume-buttons-card-feature": HuiMediaPlayerVolumeButtonsCardFeature;
}
}

View File

@@ -50,11 +50,6 @@ export interface MediaPlayerVolumeSliderCardFeatureConfig {
type: "media-player-volume-slider";
}
export interface MediaPlayerVolumeButtonsCardFeatureConfig {
type: "media-player-volume-buttons";
step?: number;
}
export interface FanDirectionCardFeatureConfig {
type: "fan-direction";
}
@@ -257,7 +252,6 @@ export type LovelaceCardFeatureConfig =
| LockCommandsCardFeatureConfig
| LockOpenDoorCardFeatureConfig
| MediaPlayerPlaybackCardFeatureConfig
| MediaPlayerVolumeButtonsCardFeatureConfig
| MediaPlayerVolumeSliderCardFeatureConfig
| NumericInputCardFeatureConfig
| SelectOptionsCardFeatureConfig

View File

@@ -1,6 +1,6 @@
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, state } from "lit/decorators";
import { DOMAINS_TOGGLE } from "../../../common/const";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { computeDomain } from "../../../common/entity/compute_domain";
@@ -20,11 +20,9 @@ import type {
import type {
LovelaceCard,
LovelaceCardEditor,
LovelaceGridOptions,
LovelaceHeaderFooter,
} from "../types";
import type { EntitiesCardConfig } from "./types";
import { haStyleScrollbar } from "../../../resources/styles";
export const computeShowHeaderToggle = <
T extends EntityConfig | LovelaceRowConfig,
@@ -77,8 +75,6 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
private _hass?: HomeAssistant;
@property({ attribute: false }) public layout?: string;
private _configEntities?: LovelaceRowConfig[];
private _showHeaderToggle?: boolean;
@@ -143,14 +139,6 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
return size;
}
public getGridOptions(): LovelaceGridOptions {
return {
columns: 12,
min_columns: 6,
min_rows: this._config?.title || this._showHeaderToggle ? 3 : 2,
};
}
public setConfig(config: EntitiesCardConfig): void {
if (!config.entities || !Array.isArray(config.entities)) {
throw new Error("Entities must be specified");
@@ -245,7 +233,7 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
`}
</h1>
`}
<div id="states" class="card-content ha-scrollbar">
<div id="states" class="card-content">
${this._configEntities!.map((entityConf) =>
this._renderEntity(entityConf)
)}
@@ -258,73 +246,69 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
`;
}
static styles = [
haStyleScrollbar,
css`
ha-card {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.card-header {
display: flex;
justify-content: space-between;
}
static styles = css`
ha-card {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.card-header {
display: flex;
justify-content: space-between;
}
.card-header .name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-header .name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#states {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--entities-card-row-gap, var(--card-row-gap, 8px));
overflow-y: auto;
}
#states {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--entities-card-row-gap, var(--card-row-gap, 8px));
}
#states > div > * {
overflow: clip visible;
}
#states > div > * {
overflow: clip visible;
}
#states > div {
position: relative;
}
#states > div {
position: relative;
}
.icon {
padding: 0px 18px 0px 8px;
}
.icon {
padding: 0px 18px 0px 8px;
}
.header {
border-top-left-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
border-top-right-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
margin-bottom: 16px;
overflow: hidden;
}
.header {
border-top-left-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
border-top-right-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
margin-bottom: 16px;
overflow: hidden;
}
.footer {
border-bottom-left-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
border-bottom-right-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
margin-top: -16px;
overflow: hidden;
}
`,
];
.footer {
border-bottom-left-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
border-bottom-right-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
margin-top: -16px;
overflow: hidden;
}
`;
private _renderEntity(entityConf: LovelaceRowConfig): TemplateResult {
const element = createRowElement(

View File

@@ -162,7 +162,7 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
private async _fetchStatistics(sensorNumericDeviceClasses: string[]) {
const now = new Date();
const start = new Date();
start.setHours(start.getHours() - this._hoursToShow - 1);
start.setHours(start.getHours() - this._hoursToShow);
const statistics = await fetchStatistics(
this.hass!,

View File

@@ -14,11 +14,7 @@ import { findEntities } from "../common/find-entities";
import { processConfigEntities } from "../common/process-config-entities";
import "../components/hui-warning";
import type { EntityConfig } from "../entity-rows/types";
import type {
LovelaceCard,
LovelaceCardEditor,
LovelaceGridOptions,
} from "../types";
import type { LovelaceCard, LovelaceCardEditor } from "../types";
import type { LogbookCardConfig } from "./types";
import { resolveEntityIDs } from "../../../data/selector";
import { ensureArray } from "../../../common/array/ensure-array";
@@ -68,15 +64,6 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
return 9 + (this._config?.title ? 1 : 0);
}
public getGridOptions(): LovelaceGridOptions {
return {
rows: 6,
columns: 12,
min_columns: 6,
min_rows: this._config?.title ? 4 : 3,
};
}
public validateTarget(
config: LogbookCardConfig
): HassServiceTarget | undefined {
@@ -202,10 +189,6 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
>
<div class="content">
<ha-logbook
class=${classMap({
"is-grid": this.layout === "grid",
"is-panel": this.layout === "panel",
})}
.hass=${this.hass}
.time=${this._time}
.entityIds=${this._getEntityIds()}
@@ -229,7 +212,6 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
}
.content {
height: 100%;
padding: 0 16px 16px;
}
@@ -242,11 +224,6 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
display: block;
}
ha-logbook.is-grid,
ha-logbook.is-panel {
height: 100%;
}
:host([ispanel]) .content,
:host([ispanel]) ha-logbook {
height: 100%;

View File

@@ -23,7 +23,6 @@ import "../card-features/hui-light-color-temp-card-feature";
import "../card-features/hui-lock-commands-card-feature";
import "../card-features/hui-lock-open-door-card-feature";
import "../card-features/hui-media-player-playback-card-feature";
import "../card-features/hui-media-player-volume-buttons-card-feature";
import "../card-features/hui-media-player-volume-slider-card-feature";
import "../card-features/hui-numeric-input-card-feature";
import "../card-features/hui-select-options-card-feature";
@@ -73,7 +72,6 @@ const TYPES = new Set<LovelaceCardFeatureConfig["type"]>([
"lock-commands",
"lock-open-door",
"media-player-playback",
"media-player-volume-buttons",
"media-player-volume-slider",
"numeric-input",
"select-options",

View File

@@ -48,7 +48,6 @@ import { supportsLightColorTempCardFeature } from "../../card-features/hui-light
import { supportsLockCommandsCardFeature } from "../../card-features/hui-lock-commands-card-feature";
import { supportsLockOpenDoorCardFeature } from "../../card-features/hui-lock-open-door-card-feature";
import { supportsMediaPlayerPlaybackCardFeature } from "../../card-features/hui-media-player-playback-card-feature";
import { supportsMediaPlayerVolumeButtonsCardFeature } from "../../card-features/hui-media-player-volume-buttons-card-feature";
import { supportsMediaPlayerVolumeSliderCardFeature } from "../../card-features/hui-media-player-volume-slider-card-feature";
import { supportsNumericInputCardFeature } from "../../card-features/hui-numeric-input-card-feature";
import { supportsSelectOptionsCardFeature } from "../../card-features/hui-select-options-card-feature";
@@ -103,7 +102,6 @@ const UI_FEATURE_TYPES = [
"lock-commands",
"lock-open-door",
"media-player-playback",
"media-player-volume-buttons",
"media-player-volume-slider",
"numeric-input",
"select-options",
@@ -133,7 +131,6 @@ const EDITABLES_FEATURE_TYPES = new Set<UiFeatureTypes>([
"fan-preset-modes",
"humidifier-modes",
"lawn-mower-commands",
"media-player-volume-buttons",
"numeric-input",
"select-options",
"trend-graph",
@@ -174,7 +171,6 @@ const SUPPORTS_FEATURE_TYPES: Record<
"lock-commands": supportsLockCommandsCardFeature,
"lock-open-door": supportsLockOpenDoorCardFeature,
"media-player-playback": supportsMediaPlayerPlaybackCardFeature,
"media-player-volume-buttons": supportsMediaPlayerVolumeButtonsCardFeature,
"media-player-volume-slider": supportsMediaPlayerVolumeSliderCardFeature,
"numeric-input": supportsNumericInputCardFeature,
"select-options": supportsSelectOptionsCardFeature,

View File

@@ -1,86 +0,0 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import type {
LovelaceCardFeatureContext,
MediaPlayerVolumeButtonsCardFeatureConfig,
} from "../../card-features/types";
import type { LovelaceCardFeatureEditor } from "../../types";
@customElement("hui-media-player-volume-buttons-card-feature-editor")
export class HuiMediaPlayerVolumeButtonsCardFeatureEditor
extends LitElement
implements LovelaceCardFeatureEditor
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: MediaPlayerVolumeButtonsCardFeatureConfig;
public setConfig(config: MediaPlayerVolumeButtonsCardFeatureConfig): void {
this._config = config;
}
private _schema = memoizeOne(
() =>
[
{
name: "step",
selector: {
number: {
mode: "slider",
step: 1,
min: 1,
max: 100,
unit_of_measurement: "%",
},
},
},
] as const
);
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
const data: MediaPlayerVolumeButtonsCardFeatureConfig = {
type: "media-player-volume-buttons",
step: this._config.step ?? 5,
};
const schema = this._schema();
return html`
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) =>
this.hass!.localize(
`ui.panel.lovelace.editor.features.types.media-player-volume-buttons.${schema.name}`
);
}
declare global {
interface HTMLElementTagNameMap {
"hui-media-player-volume-buttons-card-feature-editor": HuiMediaPlayerVolumeButtonsCardFeatureEditor;
}
}

View File

@@ -92,7 +92,6 @@ export class HomeMainViewStrategy extends ReactiveElement {
? floor.name
: hass.localize("ui.panel.lovelace.strategy.home.areas"),
heading_style: "title",
icon: floor.icon,
},
...cards,
],

View File

@@ -8154,10 +8154,6 @@
"media-player-playback": {
"label": "Media player playback controls"
},
"media-player-volume-buttons": {
"label": "Media player volume buttons",
"step": "Step size"
},
"media-player-volume-slider": {
"label": "Media player volume slider"
},