Compare commits

..

56 Commits

Author SHA1 Message Date
Aidan Timson
83512e62f5 Remove 2025-10-23 11:16:09 +01:00
Aidan Timson
aa010bc6f0 Use unknown 2025-10-23 11:11:32 +01:00
Aidan Timson
19d6743f8c Show warning 2025-10-23 11:11:32 +01:00
Aidan Timson
e7f816b982 Cleanup 2025-10-23 11:11:32 +01:00
Aidan Timson
944ab1b3ce Less any 2025-10-23 11:11:31 +01:00
Aidan Timson
918e0f8383 Docs 2025-10-23 11:11:31 +01:00
Aidan Timson
146c2654b3 Docs 2025-10-23 11:11:31 +01:00
Aidan Timson
8af8d6cd3f Typescript error 2025-10-23 11:11:31 +01:00
Aidan Timson
cf93fb7091 Remove 2025-10-23 11:11:31 +01:00
Aidan Timson
3ce7b42dc3 Check for parent 2025-10-23 11:11:31 +01:00
Aidan Timson
91f5a8beca Remove duplicate implementation 2025-10-23 11:11:31 +01:00
Aidan Timson
50fc5645ae Cleanup 2025-10-23 11:11:31 +01:00
Aidan Timson
bacc478e4a Override method 2025-10-23 11:11:31 +01:00
Aidan Timson
e7bb2cc10c Remove duplicate implementations 2025-10-23 11:11:31 +01:00
Aidan Timson
7b37e9e030 Format 2025-10-23 11:11:31 +01:00
Aidan Timson
5da2abd720 Comments 2025-10-23 11:11:31 +01:00
Aidan Timson
61b34507ed Add guard 2025-10-23 11:11:31 +01:00
Aidan Timson
49f916428d Fix leak 2025-10-23 11:11:31 +01:00
Aidan Timson
71b568076c Simplify 2025-10-23 11:11:31 +01:00
Aidan Timson
4af4d86c53 Remove duplicate transitions (non view transitions) 2025-10-23 11:11:31 +01:00
Aidan Timson
13f6d2af1f Remove unused code 2025-10-23 11:11:31 +01:00
Aidan Timson
9f1fd06def Cleanup 2025-10-23 11:11:31 +01:00
Aidan Timson
d2f354ed71 Move duplicated logic into mixin 2025-10-23 11:11:31 +01:00
Aidan Timson
d612e29b31 Flip logic 2025-10-23 11:11:31 +01:00
Aidan Timson
6656fe7122 Setup other layouts 2025-10-23 11:11:31 +01:00
Aidan Timson
ab4f7cef2b Fix 2025-10-23 11:11:31 +01:00
Aidan Timson
ae929d57b6 Fix 2025-10-23 11:11:31 +01:00
Aidan Timson
6f8516aa4a Cleanup 2025-10-23 11:11:31 +01:00
Aidan Timson
74aa390229 Fix 2025-10-23 11:11:31 +01:00
Aidan Timson
944ed9f000 Rename 2025-10-23 11:11:31 +01:00
Aidan Timson
d4a02dddf0 Cleanup 2025-10-23 11:11:31 +01:00
Aidan Timson
59b56822b8 Cleanup 2025-10-23 11:11:31 +01:00
Aidan Timson
0d0eb737c6 Rename, zero for reduced motion 2025-10-23 11:11:31 +01:00
Aidan Timson
5338192c97 Show on loaded 2025-10-23 11:11:31 +01:00
Aidan Timson
04e9d1bec3 Rename 2025-10-23 11:11:31 +01:00
Aidan Timson
1bfbd1ec09 Fade out launch screen 2025-10-23 11:11:31 +01:00
Aidan Timson
afebe1d588 Allow transition name to be provided by caller 2025-10-23 11:11:31 +01:00
Aidan Timson
d0c527943d Use generic transition names 2025-10-23 11:11:31 +01:00
Aidan Timson
8f50e2c025 Switch to mixin 2025-10-23 11:11:31 +01:00
Aidan Timson
a9219a8779 Cleanup 2025-10-23 11:11:31 +01:00
Aidan Timson
2a135c50ce Order 2025-10-23 11:11:30 +01:00
Aidan Timson
36b11dbbcd Revert 2025-10-23 11:11:30 +01:00
Aidan Timson
37ea0a11fa Remove sidebar code 2025-10-23 11:11:30 +01:00
Aidan Timson
e9ab1c27d2 POC: view transitions 2025-10-23 11:11:30 +01:00
Aidan Timson
1ec0ff46c9 Respect reduced motion 2025-10-23 11:11:30 +01:00
Aidan Timson
2b0fd53349 Add to hui views 2025-10-23 11:11:30 +01:00
Aidan Timson
8c7643c524 Add animations 2025-10-23 11:11:30 +01:00
Aidan Timson
ff32bae8ea Cleanup 2025-10-23 11:11:30 +01:00
Aidan Timson
72cc53d960 Use index based delay 2025-10-23 11:11:30 +01:00
Aidan Timson
2b6ce8c34e Faster 2025-10-23 11:11:30 +01:00
Aidan Timson
f61ebe36b9 Fade in menu button 2025-10-23 11:11:30 +01:00
Aidan Timson
2c8e3762c6 Move 2025-10-23 11:11:30 +01:00
Aidan Timson
89b86d0d69 Cap stagger at 8 items 2025-10-23 11:11:30 +01:00
Aidan Timson
c60d038828 Animate sidebar 2025-10-23 11:11:30 +01:00
Aidan Timson
f9e2d4ef95 Set base themable animation durations 2025-10-23 11:11:30 +01:00
Aidan Timson
2609133f54 Create fade in slide down shared animation 2025-10-23 11:11:30 +01:00
46 changed files with 1116 additions and 1609 deletions

View File

@@ -203,7 +203,7 @@
"husky": "9.1.7",
"jsdom": "27.0.1",
"jszip": "3.10.1",
"lint-staged": "16.2.5",
"lint-staged": "16.2.4",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0",
@@ -217,7 +217,7 @@
"terser-webpack-plugin": "5.3.14",
"ts-lit-plugin": "2.0.2",
"typescript": "5.9.3",
"typescript-eslint": "8.46.2",
"typescript-eslint": "8.46.1",
"vite-tsconfig-paths": "5.1.4",
"vitest": "3.2.4",
"webpack-stats-plugin": "1.1.3",

View File

@@ -88,19 +88,9 @@ export class HaChartBase extends LitElement {
private _lastTapTime?: number;
private _shouldResizeChart = false;
// @ts-ignore
private _resizeController = new ResizeController(this, {
callback: () => {
if (this.chart) {
if (!this.chart.getZr().animation.isFinished()) {
this._shouldResizeChart = true;
} else {
this.chart.resize();
}
}
},
callback: () => this.chart?.resize(),
});
private _loading = false;
@@ -376,7 +366,6 @@ export class HaChartBase extends LitElement {
if (!this.options?.dataZoom) {
this.chart.getZr().on("dblclick", this._handleClickZoom);
}
this.chart.on("finished", this._handleChartRenderFinished);
if (this._isTouchDevice) {
this.chart.getZr().on("click", (e: ECElementEvent) => {
if (!e.zrByTouch) {
@@ -956,13 +945,6 @@ export class HaChartBase extends LitElement {
});
}
private _handleChartRenderFinished = () => {
if (this._shouldResizeChart) {
this.chart?.resize();
this._shouldResizeChart = false;
}
};
static styles = css`
:host {
display: block;

View File

@@ -1,39 +1,23 @@
import "@material/mwc-menu/mwc-menu-surface";
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { IFuseOptions } from "fuse.js";
import Fuse from "fuse.js";
import { mdiDragHorizontalVariant } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import type { PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { computeDomain } from "../../common/entity/compute_domain";
import {
STATE_DISPLAY_SPECIAL_CONTENT,
STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS,
} from "../../state-display/state-display";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../chips/ha-assist-chip";
import "../chips/ha-chip-set";
import "../chips/ha-input-chip";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-sortable";
interface StateContentOption {
primary: string;
value: string;
}
const rowRenderer: ComboBoxLitRenderer<StateContentOption> = (item) => html`
<ha-combo-box-item type="button">
<span slot="headline">${item.primary}</span>
</ha-combo-box-item>
`;
import "../chips/ha-input-chip";
import "../chips/ha-chip-set";
import type { HaComboBox } from "../ha-combo-box";
const HIDDEN_ATTRIBUTES = [
"access_token",
@@ -90,7 +74,7 @@ const HIDDEN_ATTRIBUTES = [
];
@customElement("ha-entity-state-content-picker")
export class HaStateContentPicker extends LitElement {
class HaEntityStatePicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entityId?: string;
@@ -111,28 +95,26 @@ export class HaStateContentPicker extends LitElement {
@property() public helper?: string;
@query(".container", true) private _container?: HTMLDivElement;
@state() private _opened = false;
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
@state() private _opened = false;
protected shouldUpdate(changedProps: PropertyValues) {
return !(!changedProps.has("_opened") && this._opened);
}
private _editIndex?: number;
private _options = memoizeOne(
private options = memoizeOne(
(entityId?: string, stateObj?: HassEntity, allowName?: boolean) => {
const domain = entityId ? computeDomain(entityId) : undefined;
return [
{
primary: this.hass.localize(
"ui.components.state-content-picker.state"
),
label: this.hass.localize("ui.components.state-content-picker.state"),
value: "state",
},
...(allowName
? [
{
primary: this.hass.localize(
label: this.hass.localize(
"ui.components.state-content-picker.name"
),
value: "name",
@@ -140,13 +122,13 @@ export class HaStateContentPicker extends LitElement {
]
: []),
{
primary: this.hass.localize(
label: this.hass.localize(
"ui.components.state-content-picker.last_changed"
),
value: "last_changed",
},
{
primary: this.hass.localize(
label: this.hass.localize(
"ui.components.state-content-picker.last_updated"
),
value: "last_updated",
@@ -155,7 +137,7 @@ export class HaStateContentPicker extends LitElement {
? STATE_DISPLAY_SPECIAL_CONTENT.filter((content) =>
STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS[domain]?.includes(content)
).map((content) => ({
primary: this.hass.localize(
label: this.hass.localize(
`ui.components.state-content-picker.${content}`
),
value: content,
@@ -164,201 +146,108 @@ export class HaStateContentPicker extends LitElement {
...Object.keys(stateObj?.attributes ?? {})
.filter((a) => !HIDDEN_ATTRIBUTES.includes(a))
.map((attribute) => ({
primary: this.hass.formatEntityAttributeName(stateObj!, attribute),
value: attribute,
label: this.hass.formatEntityAttributeName(stateObj!, attribute),
})),
] satisfies StateContentOption[];
];
}
);
private _filter = "";
protected render() {
if (!this.hass) {
return nothing;
}
const value = this._value;
const stateObj = this.entityId
? this.hass.states[this.entityId]
: undefined;
const options = this._options(this.entityId, stateObj, this.allowName);
const options = this.options(this.entityId, stateObj, this.allowName);
const optionItems = options.filter(
(option) => !this._value.includes(option.value)
);
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
<div class="container ${this.disabled ? "disabled" : ""}">
<ha-sortable
no-style
@item-moved=${this._moveItem}
.disabled=${this.disabled}
handle-selector="button.primary.action"
filter=".add"
>
<ha-chip-set>
${repeat(
this._value,
(item) => item,
(item: string, idx) => {
const label = options.find((o) => o.value === item)?.primary;
const isValid = !!label;
return html`
<ha-input-chip
data-idx=${idx}
@remove=${this._removeItem}
@click=${this._editItem}
.label=${label || item}
.selected=${!this.disabled}
.disabled=${this.disabled}
class=${!isValid ? "invalid" : ""}
>
<ha-svg-icon
slot="icon"
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
</ha-input-chip>
`;
}
)}
${this.disabled
? nothing
: html`
<ha-assist-chip
@click=${this._addItem}
.disabled=${this.disabled}
label=${this.hass.localize(
"ui.components.entity.entity-state-content-picker.add"
)}
class="add"
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-assist-chip>
`}
</ha-chip-set>
</ha-sortable>
${value?.length
? html`
<ha-sortable
no-style
@item-moved=${this._moveItem}
.disabled=${this.disabled}
handle-selector="button.primary.action"
>
<ha-chip-set>
${repeat(
this._value,
(item) => item,
(item, idx) => {
const label =
options.find((option) => option.value === item)?.label ||
item;
return html`
<ha-input-chip
.idx=${idx}
@remove=${this._removeItem}
.label=${label}
selected
>
<ha-svg-icon
slot="icon"
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
${label}
</ha-input-chip>
`;
}
)}
</ha-chip-set>
</ha-sortable>
`
: nothing}
<mwc-menu-surface
.open=${this._opened}
@closed=${this._onClosed}
@opened=${this._onOpened}
@input=${stopPropagation}
.anchor=${this._container}
>
<ha-combo-box
.hass=${this.hass}
.value=${""}
.autofocus=${this.autofocus}
.disabled=${this.disabled || !this.entityId}
.required=${this.required && !value.length}
.helper=${this.helper}
.items=${options}
allow-custom-value
item-id-path="value"
item-value-path="value"
item-label-path="primary"
.renderer=${rowRenderer}
@opened-changed=${this._openedChanged}
@value-changed=${this._comboBoxValueChanged}
@filter-changed=${this._filterChanged}
>
</ha-combo-box>
</mwc-menu-surface>
</div>
<ha-combo-box
item-value-path="value"
item-label-path="label"
.hass=${this.hass}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required && !value.length}
.value=${""}
.items=${optionItems}
allow-custom-value
@filter-changed=${this._filterChanged}
@value-changed=${this._comboBoxValueChanged}
@opened-changed=${this._openedChanged}
></ha-combo-box>
`;
}
private _onClosed(ev) {
ev.stopPropagation();
this._opened = false;
this._editIndex = undefined;
}
private async _onOpened(ev) {
if (!this._opened) {
return;
}
ev.stopPropagation();
this._opened = true;
await this._comboBox?.focus();
await this._comboBox?.open();
}
private async _addItem(ev) {
ev.stopPropagation();
this._opened = true;
}
private async _editItem(ev) {
ev.stopPropagation();
const idx = parseInt(ev.currentTarget.dataset.idx, 10);
this._editIndex = idx;
this._opened = true;
}
private get _value() {
return !this.value ? [] : ensureArray(this.value);
}
private _toValue = memoizeOne((value: string[]): typeof this.value => {
if (value.length === 0) {
return undefined;
}
if (value.length === 1) {
return value[0];
}
return value;
});
private _openedChanged(ev: ValueChangedEvent<boolean>) {
const open = ev.detail.value;
if (open) {
const options = this._comboBox.items || [];
const initialValue =
this._editIndex != null ? this._value[this._editIndex] : "";
const filteredItems = this._filterSelectedOptions(options, initialValue);
this._comboBox.filteredItems = filteredItems;
this._comboBox.setInputValue(initialValue);
} else {
this._opened = false;
}
this._opened = ev.detail.value;
this._comboBox.filteredItems = this._comboBox.items;
}
private _filterSelectedOptions = (
options: StateContentOption[],
current?: string
) => {
const value = this._value;
private _filterChanged(ev?: CustomEvent): void {
this._filter = ev?.detail.value || "";
return options.filter(
(option) => !value.includes(option.value) || option.value === current
);
};
const filteredItems = this._comboBox.items?.filter((item) => {
const label = item.label || item.value;
return label.toLowerCase().includes(this._filter?.toLowerCase());
});
private _filterChanged(ev: ValueChangedEvent<string>) {
const input = ev.detail.value;
const filter = input?.toLowerCase() || "";
const options = this._comboBox.items || [];
const currentValue =
this._editIndex != null ? this._value[this._editIndex] : "";
this._comboBox.filteredItems = this._filterSelectedOptions(
options,
currentValue
);
if (!filter) {
return;
if (this._filter) {
filteredItems?.unshift({ label: this._filter, value: this._filter });
}
const fuseOptions: IFuseOptions<StateContentOption> = {
keys: ["primary", "secondary", "value"],
isCaseSensitive: false,
minMatchCharLength: Math.min(filter.length, 2),
threshold: 0.2,
ignoreDiacritics: true,
};
const fuse = new Fuse(this._comboBox.filteredItems, fuseOptions);
const filteredItems = fuse.search(filter).map((result) => result.item);
this._comboBox.filteredItems = filteredItems;
}
@@ -371,40 +260,43 @@ export class HaStateContentPicker extends LitElement {
newValue.splice(newIndex, 0, element);
this._setValue(newValue);
await this.updateComplete;
this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
this._filterChanged();
}
private async _removeItem(ev) {
ev.stopPropagation();
const value = [...this._value];
const idx = parseInt(ev.target.dataset.idx, 10);
value.splice(idx, 1);
const value: string[] = [...this._value];
value.splice(ev.target.idx, 1);
this._setValue(value);
await this.updateComplete;
this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
this._filterChanged();
}
private _comboBoxValueChanged(ev: ValueChangedEvent<string>): void {
private _comboBoxValueChanged(ev: CustomEvent): void {
ev.stopPropagation();
const value = ev.detail.value;
const newValue = ev.detail.value;
if (this.disabled || value === "") {
if (this.disabled || newValue === "") {
return;
}
const newValue = [...this._value];
const currentValue = this._value;
if (this._editIndex != null) {
newValue[this._editIndex] = value;
} else {
newValue.push(value);
if (currentValue.includes(newValue)) {
return;
}
this._setValue(newValue);
setTimeout(() => {
this._filterChanged();
this._comboBox.setInputValue("");
}, 0);
this._setValue([...currentValue, newValue]);
}
private _setValue(value: string[]) {
const newValue = this._toValue(value);
const newValue =
value.length === 0 ? undefined : value.length === 1 ? value[0] : value;
this.value = newValue;
fireEvent(this, "value-changed", {
value: newValue,
@@ -414,64 +306,10 @@ export class HaStateContentPicker extends LitElement {
static styles = css`
:host {
position: relative;
width: 100%;
}
.container {
position: relative;
background-color: var(--mdc-text-field-fill-color, whitesmoke);
border-radius: var(--ha-border-radius-sm);
border-end-end-radius: var(--ha-border-radius-square);
border-end-start-radius: var(--ha-border-radius-square);
}
.container:after {
display: block;
content: "";
position: absolute;
pointer-events: none;
bottom: 0;
left: 0;
right: 0;
height: 1px;
width: 100%;
background-color: var(
--mdc-text-field-idle-line-color,
rgba(0, 0, 0, 0.42)
);
transform:
height 180ms ease-in-out,
background-color 180ms ease-in-out;
}
.container.disabled:after {
background-color: var(
--mdc-text-field-disabled-line-color,
rgba(0, 0, 0, 0.42)
);
}
.container:focus-within:after {
height: 2px;
background-color: var(--mdc-theme-primary);
}
label {
display: block;
margin: 0 0 var(--ha-space-2);
}
.add {
order: 1;
}
mwc-menu-surface {
--mdc-menu-min-width: 100%;
}
ha-chip-set {
padding: var(--ha-space-2) var(--ha-space-2);
}
.invalid {
text-decoration: line-through;
padding: 8px 0;
}
.sortable-fallback {
@@ -491,6 +329,6 @@ export class HaStateContentPicker extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"ha-entity-state-content-picker": HaStateContentPicker;
"ha-entity-state-content-picker": HaEntityStatePicker;
}
}

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 { haStyleScrollbar } from "../resources/styles";
import { customElement, property, state } from "lit/decorators";
export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300;
@@ -14,12 +13,6 @@ export class HaBottomSheet extends LitElement {
@state() private _drawerOpen = false;
@query("#drawer") private _drawer!: HTMLElement;
private _resizeStartY = 0;
private _resizeDelta = 0;
private _handleAfterHide() {
this.open = false;
const ev = new Event("closed", {
@@ -39,143 +32,58 @@ 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">
<slot></slot>
</div>
<slot></slot>
</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;
}
static styles = css`
wa-drawer {
--wa-color-surface-raised: transparent;
--spacing: 0;
--size: var(--ha-bottom-sheet-height, auto);
--show-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
--hide-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
}
wa-drawer::part(dialog) {
max-height: var(--ha-bottom-sheet-max-height, 90vh);
align-items: center;
}
wa-drawer::part(body) {
max-width: var(--ha-bottom-sheet-max-width);
width: 100%;
border-top-left-radius: var(
--ha-bottom-sheet-border-radius,
var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
);
border-top-right-radius: var(
--ha-bottom-sheet-border-radius,
var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
);
background-color: var(
--ha-bottom-sheet-surface-background,
var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff)),
);
padding: var(
--ha-bottom-sheet-padding,
0 var(--safe-area-inset-right) var(--safe-area-inset-bottom)
var(--safe-area-inset-left)
);
}
this._startResizing(ev.touches[0].clientY);
};
private _startResizing(clientY: number) {
// register event listeners for drag handling
document.addEventListener("touchmove", this._handleMouseMove, {
passive: false,
});
document.addEventListener("touchend", this._endResizing);
document.addEventListener("touchcancel", this._endResizing);
this._resizeStartY = clientY;
}
private _handleMouseMove = (ev: TouchEvent) => {
this._resizeDelta = this._resizeStartY - ev.touches[0].clientY;
if (this._resizeDelta < 0) {
ev.preventDefault();
requestAnimationFrame(() => {
this.style.setProperty(
"--dialog-transform",
`translateY(${this._resizeDelta * -1}px)`
);
});
:host([flexcontent]) wa-drawer::part(body) {
display: flex;
}
};
private _endResizing = () => {
this._unregisterResizeHandlers();
if (this._resizeDelta > -50) {
this.style.removeProperty("--dialog-transform");
return;
}
this._drawerOpen = false;
};
private _unregisterResizeHandlers = () => {
document.removeEventListener("touchmove", this._handleMouseMove);
document.removeEventListener("touchend", this._unregisterResizeHandlers);
document.removeEventListener("touchcancel", this._unregisterResizeHandlers);
};
disconnectedCallback() {
super.disconnectedCallback();
this._unregisterResizeHandlers();
}
static styles = [
haStyleScrollbar,
css`
wa-drawer {
--wa-color-surface-raised: transparent;
--spacing: 0;
--size: var(--ha-bottom-sheet-height, auto);
--show-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
--hide-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
}
wa-drawer::part(dialog) {
max-height: var(--ha-bottom-sheet-max-height, 90vh);
align-items: center;
transform: var(--dialog-transform);
}
wa-drawer::part(body) {
max-width: var(--ha-bottom-sheet-max-width);
width: 100%;
border-top-left-radius: var(
--ha-bottom-sheet-border-radius,
var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
);
border-top-right-radius: var(
--ha-bottom-sheet-border-radius,
var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
);
background-color: var(
--ha-bottom-sheet-surface-background,
var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff)),
);
padding: var(
--ha-bottom-sheet-padding,
0 var(--safe-area-inset-right) var(--safe-area-inset-bottom)
var(--safe-area-inset-left)
);
}
:host([flexcontent]) wa-drawer::part(body) {
display: flex;
flex-direction: column;
}
:host([flexcontent]) .body {
flex: 1;
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)
);
}
`,
];
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-bottom-sheet": HaBottomSheet;
}
interface HASSDomEvents {
"bottom-sheet-lock-resize-changed": boolean;
}
}

View File

@@ -31,9 +31,6 @@ export class HaButtonToggleGroup extends LitElement {
@property({ type: Boolean, reflect: true, attribute: "no-wrap" })
public nowrap = false;
@property({ type: Boolean, reflect: true, attribute: "full-width" })
public fullWidth = false;
@property() public variant:
| "brand"
| "neutral"
@@ -41,13 +38,6 @@ export class HaButtonToggleGroup extends LitElement {
| "warning"
| "danger" = "brand";
@property({ attribute: "active-variant" }) public activeVariant?:
| "brand"
| "neutral"
| "success"
| "warning"
| "danger";
protected render(): TemplateResult {
return html`
<wa-button-group childSelector="ha-button">
@@ -56,9 +46,7 @@ export class HaButtonToggleGroup extends LitElement {
html`<ha-button
iconTag="ha-svg-icon"
class="icon"
.variant=${this.active !== button.value || !this.activeVariant
? this.variant
: this.activeVariant}
.variant=${this.variant}
.size=${this.size}
.value=${button.value}
@click=${this._handleClick}
@@ -90,19 +78,6 @@ export class HaButtonToggleGroup extends LitElement {
:host([no-wrap]) wa-button-group::part(base) {
flex-wrap: nowrap;
}
wa-button-group {
padding: var(--ha-button-toggle-group-padding);
}
:host([full-width]) wa-button-group,
:host([full-width]) wa-button-group::part(base) {
width: 100%;
}
:host([full-width]) ha-button {
flex: 1;
}
`;
}

View File

@@ -6,9 +6,6 @@ export class HaDialogHeader extends LitElement {
@property({ type: String, attribute: "subtitle-position" })
public subtitlePosition: "above" | "below" = "below";
@property({ type: Boolean, reflect: true, attribute: "show-border" })
public showBorder = false;
protected render() {
const titleSlot = html`<div class="header-title">
<slot name="title"></slot>

View File

@@ -52,10 +52,9 @@ export class HaObjectSelector extends LitElement {
const translationKey = this.selector.object?.translation_key;
if (this.localizeValue && translationKey) {
const label =
this.localizeValue(`${translationKey}.fields.${schema.name}.name`) ||
// Fallback for backward compatibility
this.localizeValue(`${translationKey}.fields.${schema.name}`);
const label = this.localizeValue(
`${translationKey}.fields.${schema.name}`
);
if (label) {
return label;
}
@@ -63,20 +62,6 @@ export class HaObjectSelector extends LitElement {
return this.selector.object?.fields?.[schema.name]?.label || schema.name;
};
private _computeHelper = (schema: HaFormSchema): string => {
const translationKey = this.selector.object?.translation_key;
if (this.localizeValue && translationKey) {
const helper = this.localizeValue(
`${translationKey}.fields.${schema.name}.description`
);
if (helper) {
return helper;
}
}
return this.selector.object?.fields?.[schema.name]?.description || "";
};
private _renderItem(item: any, index: number) {
const labelField =
this.selector.object!.label_field ||
@@ -229,7 +214,6 @@ export class HaObjectSelector extends LitElement {
schema: this._schema(this.selector),
data: {},
computeLabel: this._computeLabel,
computeHelper: this._computeHelper,
submitText: this.hass.localize("ui.common.add"),
});

View File

@@ -36,7 +36,7 @@ export class HaSelectorUiStateContent extends SubscribeMixin(LitElement) {
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
.allowName=${this.selector.ui_state_content?.allow_name || false}
.allowName=${this.selector.ui_state_content?.allow_name}
></ha-entity-state-content-picker>
`;
}

View File

@@ -162,9 +162,7 @@ class HaServicePicker extends LitElement {
const description =
this.hass.localize(
`component.${domain}.services.${service}.description`
) ||
services[domain][service].description ||
"";
) || services[domain][service].description;
items.push({
id: serviceId,

View File

@@ -2,13 +2,26 @@ import { TopAppBarFixedBase } from "@material/mwc-top-app-bar-fixed/mwc-top-app-
import { styles } from "@material/mwc-top-app-bar/mwc-top-app-bar.css";
import { css } from "lit";
import { customElement, property } from "lit/decorators";
import { ViewTransitionMixin } from "../mixins/view-transition-mixin";
import { haStyleViewTransitions } from "../resources/styles";
@customElement("ha-top-app-bar-fixed")
export class HaTopAppBarFixed extends TopAppBarFixedBase {
export class HaTopAppBarFixed extends ViewTransitionMixin(TopAppBarFixedBase) {
@property({ type: Boolean, reflect: true }) public narrow = false;
@property({ type: Boolean, reflect: true, attribute: "content-loading" })
public contentLoading = true;
protected override onLoadTransition(): void {
// Use reflected property since we can't add class to base component's rendered elements
this.startViewTransition(() => {
this.contentLoading = false;
});
}
static override styles = [
styles,
haStyleViewTransitions,
css`
header {
padding-top: var(--safe-area-inset-top);
@@ -23,6 +36,10 @@ export class HaTopAppBarFixed extends TopAppBarFixedBase {
);
padding-bottom: var(--safe-area-inset-bottom);
padding-right: var(--safe-area-inset-right);
view-transition-name: layout-fade-in;
}
:host([content-loading]) .mdc-top-app-bar--fixed-adjust {
opacity: 0;
}
:host([narrow]) .mdc-top-app-bar--fixed-adjust {
padding-left: var(--safe-area-inset-left);

View File

@@ -10,14 +10,15 @@ import { html, css, nothing } from "lit";
import { property, query, customElement } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styles } from "@material/mwc-top-app-bar/mwc-top-app-bar.css";
import { haStyleScrollbar } from "../resources/styles";
import { ViewTransitionMixin } from "../mixins/view-transition-mixin";
import { haStyleScrollbar, haStyleViewTransitions } from "../resources/styles";
export const passiveEventOptionsIfSupported = supportsPassiveEventListener
? { passive: true }
: undefined;
@customElement("ha-two-pane-top-app-bar-fixed")
export class TopAppBarBaseBase extends BaseElement {
export class TopAppBarBaseBase extends ViewTransitionMixin(BaseElement) {
protected override mdcFoundation!: MDCFixedTopAppBarFoundation;
protected override mdcFoundationClass = MDCFixedTopAppBarFoundation;
@@ -144,7 +145,12 @@ export class TopAppBarBaseBase extends BaseElement {
: nothing}
<div class="main">
${this.pane ? html`<div class="shadow-container"></div>` : nothing}
<div class="content">
<div
class=${classMap({
content: true,
loading: !this._loaded,
})}
>
<slot></slot>
</div>
</div>
@@ -245,6 +251,7 @@ export class TopAppBarBaseBase extends BaseElement {
static override styles = [
styles,
haStyleScrollbar,
haStyleViewTransitions,
css`
header {
padding-top: var(--safe-area-inset-top);
@@ -341,6 +348,10 @@ export class TopAppBarBaseBase extends BaseElement {
.mdc-top-app-bar--pane .content {
height: 100%;
overflow: auto;
view-transition-name: layout-fade-in;
}
.content.loading {
opacity: 0;
}
.mdc-top-app-bar__title {
font-size: var(--ha-font-size-xl);

View File

@@ -1,18 +1,12 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "@home-assistant/webawesome/dist/components/dialog/dialog";
import { mdiClose } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import {
customElement,
eventOptions,
property,
query,
state,
} from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-dialog-header";
import "./ha-icon-button";
import type { HomeAssistant } from "../types";
import { fireEvent } from "../common/dom/fire_event";
import { haStyleScrollbar } from "../resources/styles";
export type DialogWidth = "small" | "medium" | "large" | "full";
@@ -96,11 +90,6 @@ export class HaWaDialog extends LitElement {
@state()
private _open = false;
@query(".body") public bodyContainer!: HTMLDivElement;
@state()
private _bodyScrolled = false;
protected updated(
changedProperties: Map<string | number | symbol, unknown>
): void {
@@ -118,14 +107,10 @@ export class HaWaDialog extends LitElement {
.lightDismiss=${!this.preventScrimClose}
without-header
@wa-show=${this._handleShow}
@wa-after-show=${this._handleAfterShow}
@wa-after-hide=${this._handleAfterHide}
>
<slot name="header">
<ha-dialog-header
.subtitlePosition=${this.headerSubtitlePosition}
.showBorder=${this._bodyScrolled}
>
<ha-dialog-header .subtitlePosition=${this.headerSubtitlePosition}>
<slot name="headerNavigationIcon" slot="navigationIcon">
<ha-icon-button
data-dialog="close"
@@ -144,7 +129,7 @@ export class HaWaDialog extends LitElement {
<slot name="headerActionItems" slot="actionItems"></slot>
</ha-dialog-header>
</slot>
<div class="body ha-scrollbar" @scroll=${this._handleBodyScroll}>
<div class="body ha-scrollbar">
<slot></slot>
</div>
<slot name="footer" slot="footer"></slot>
@@ -161,10 +146,6 @@ export class HaWaDialog extends LitElement {
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
};
private _handleAfterShow = () => {
fireEvent(this, "after-show");
};
private _handleAfterHide = () => {
this._open = false;
fireEvent(this, "closed");
@@ -175,11 +156,6 @@ export class HaWaDialog extends LitElement {
this._open = false;
}
@eventOptions({ passive: true })
private _handleBodyScroll(ev: Event) {
this._bodyScrolled = (ev.target as HTMLDivElement).scrollTop > 0;
}
static styles = [
haStyleScrollbar,
css`
@@ -196,7 +172,7 @@ export class HaWaDialog extends LitElement {
)
)
);
--width: min(var(--ha-dialog-width-md, 580px), var(--full-width));
--width: var(--ha-dialog-width-md, min(580px, var(--full-width)));
--spacing: var(--dialog-content-padding, var(--ha-space-6));
--show-duration: var(--ha-dialog-show-duration, 200ms);
--hide-duration: var(--ha-dialog-hide-duration, 200ms);
@@ -217,11 +193,11 @@ export class HaWaDialog extends LitElement {
}
:host([width="small"]) wa-dialog {
--width: min(var(--ha-dialog-width-sm, 320px), var(--full-width));
--width: var(--ha-dialog-width-sm, min(320px, var(--full-width)));
}
:host([width="large"]) wa-dialog {
--width: min(var(--ha-dialog-width-lg, 720px), var(--full-width));
--width: var(--ha-dialog-width-lg, min(720px, var(--full-width)));
}
:host([width="full"]) wa-dialog {
@@ -235,7 +211,6 @@ export class HaWaDialog extends LitElement {
--ha-dialog-max-height,
calc(100% - var(--ha-space-20))
);
min-height: var(--ha-dialog-min-height);
position: var(--dialog-surface-position, relative);
margin-top: var(--dialog-surface-margin-top, auto);
display: flex;
@@ -309,7 +284,6 @@ export class HaWaDialog extends LitElement {
}
:host([flexcontent]) .body {
max-width: 100%;
flex: 1;
display: flex;
flex-direction: column;
}
@@ -338,7 +312,6 @@ declare global {
interface HASSDomEvents {
opened: undefined;
"after-show": undefined;
closed: undefined;
}
}

View File

@@ -15,7 +15,6 @@ import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { until } from "lit/directives/until";
import { fireEvent } from "../../common/dom/fire_event";
import { slugify } from "../../common/string/slugify";
import { debounce } from "../../common/util/debounce";
import { isUnavailableState } from "../../data/entity";
import type {
@@ -694,12 +693,10 @@ export class HaMediaPlayerBrowse extends LitElement {
`
: ""}
</div>
<ha-tooltip .for="grid-${slugify(child.title)}" distance="-4">
<ha-tooltip .for="grid-${child.title}" distance="-4">
${child.title}
</ha-tooltip>
<div .id="grid-${slugify(child.title)}" class="title">
${child.title}
</div>
<div .id="grid-${child.title}" class="title">${child.title}</div>
</ha-card>
</div>
`;

View File

@@ -1,15 +1,17 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { mdiClose } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
import type { HomeAssistant } from "../../../types";
import "../../ha-dialog-header";
import "../../ha-icon-button";
import "../../ha-icon-next";
import "../../ha-md-dialog";
import type { HaMdDialog } from "../../ha-md-dialog";
import "../../ha-md-list";
import "../../ha-md-list-item";
import "../../ha-svg-icon";
import "../../ha-wa-dialog";
import "../ha-target-picker-item-row";
import type { TargetDetailsDialogParams } from "./show-dialog-target-details";
@@ -19,15 +21,14 @@ class DialogTargetDetails extends LitElement implements HassDialog {
@state() private _params?: TargetDetailsDialogParams;
@state() private _opened = false;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public showDialog(params: TargetDetailsDialogParams): void {
this._params = params;
this._opened = true;
}
public closeDialog() {
this._opened = false;
this._dialog?.close();
return true;
}
@@ -42,31 +43,58 @@ class DialogTargetDetails extends LitElement implements HassDialog {
}
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._opened}
header-title=${this.hass.localize(
"ui.components.target-picker.target_details"
)}
header-subtitle=${`${this.hass.localize(
`ui.components.target-picker.type.${this._params.type}`
)}:
${this._params.title}`}
@closed=${this._dialogClosed}
>
<ha-target-picker-item-row
.hass=${this.hass}
.type=${this._params.type}
.itemId=${this._params.itemId}
.deviceFilter=${this._params.deviceFilter}
.entityFilter=${this._params.entityFilter}
.includeDomains=${this._params.includeDomains}
.includeDeviceClasses=${this._params.includeDeviceClasses}
expand
></ha-target-picker-item-row>
</ha-wa-dialog>
<ha-md-dialog open @closed=${this._dialogClosed}>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
@click=${this.closeDialog}
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
<span slot="title"
>${this.hass.localize(
"ui.components.target-picker.target_details"
)}</span
>
<span slot="subtitle"
>${this.hass.localize(
`ui.components.target-picker.type.${this._params.type}`
)}:
${this._params.title}</span
>
</ha-dialog-header>
<div slot="content">
<ha-target-picker-item-row
.hass=${this.hass}
.type=${this._params.type}
.itemId=${this._params.itemId}
.deviceFilter=${this._params.deviceFilter}
.entityFilter=${this._params.entityFilter}
.includeDomains=${this._params.includeDomains}
.includeDeviceClasses=${this._params.includeDeviceClasses}
expand
></ha-target-picker-item-row>
</div>
</ha-md-dialog>
`;
}
static styles = css`
ha-md-dialog {
min-width: 400px;
max-height: 90%;
--dialog-content-padding: var(--ha-space-2) var(--ha-space-6)
max(var(--safe-area-inset-bottom, var(--ha-space-0)), var(--ha-space-8));
}
@media all and (max-width: 600px), all and (max-height: 500px) {
ha-md-dialog {
--md-dialog-container-shape: var(--ha-space-0);
min-width: 100%;
min-height: 100%;
}
}
`;
}
declare global {

View File

@@ -162,12 +162,11 @@ export class HaTargetPickerItemRow extends LitElement {
<div slot="headline">${name}</div>
${context && !this.hideContext
? html`<span slot="supporting-text">${context}</span>`
: nothing}
${this._domainName && this.subEntry
? html`<span slot="supporting-text" class="domain"
>${this._domainName}</span
>`
: nothing}
: this._domainName && this.subEntry
? html`<span slot="supporting-text" class="domain"
>${this._domainName}</span
>`
: nothing}
${!this.subEntry && entries && showEntities
? html`
<div slot="end" class="summary">
@@ -232,11 +231,9 @@ export class HaTargetPickerItemRow extends LitElement {
const rows1 =
(nextType === "area"
? entries?.referenced_areas
: nextType === "device" && this.type !== "label"
: nextType === "device"
? entries?.referenced_devices
: this.type !== "label"
? entries?.referenced_entities
: []) || [];
: entries?.referenced_entities) || [];
const devicesInAreas = [] as string[];
@@ -287,13 +284,9 @@ export class HaTargetPickerItemRow extends LitElement {
const entityRows =
this.type === "label" && entries
? entries.referenced_entities.filter((entity_id) => {
const entity = this.hass.entities[entity_id];
return (
entity.labels.includes(this.itemId) &&
!entries.referenced_devices.includes(entity.device_id || "")
);
})
? entries.referenced_entities.filter((entity_id) =>
this.hass.entities[entity_id].labels.includes(this.itemId)
)
: nextType === "device" && entries
? entries.referenced_entities.filter(
(entity_id) =>
@@ -419,6 +412,7 @@ export class HaTargetPickerItemRow extends LitElement {
const device = this.hass.devices[device_id];
if (
!hiddenAreaIds.includes(device.area_id || "") &&
(this.type !== "label" || device.labels.includes(this.itemId)) &&
deviceMeetsFilter(
device,
this.hass.entities,
@@ -675,14 +669,6 @@ export class HaTargetPickerItemRow extends LitElement {
button.link:focus {
text-decoration: underline;
}
.domain {
width: fit-content;
border-radius: var(--ha-border-radius-md);
background-color: var(--ha-color-fill-neutral-quiet-resting);
padding: var(--ha-space-1);
font-family: var(--ha-font-family-code);
}
`,
];
}

View File

@@ -6,6 +6,8 @@ import {
mdiCallSplit,
mdiCodeBraces,
mdiDevices,
mdiDotsHorizontal,
mdiExcavator,
mdiFormatListNumbered,
mdiGestureDoubleTap,
mdiHandBackRight,
@@ -14,10 +16,10 @@ import {
mdiRoomService,
mdiShuffleDisabled,
mdiTimerOutline,
mdiTools,
mdiTrafficLight,
} from "@mdi/js";
import type { AutomationElementGroupCollection } from "./automation";
import type { Action } from "./script";
import type { AutomationElementGroup } from "./automation";
export const ACTION_ICONS = {
condition: mdiAbTesting,
@@ -46,73 +48,37 @@ export const YAML_ONLY_ACTION_TYPES = new Set<keyof typeof ACTION_ICONS>([
"variables",
]);
export const ACTION_COLLECTIONS: AutomationElementGroupCollection[] = [
{
groups: {
device_id: {},
serviceGroups: {},
export const ACTION_GROUPS: AutomationElementGroup = {
device_id: {},
helpers: {
icon: mdiTools,
members: {},
},
building_blocks: {
icon: mdiExcavator,
members: {
condition: {},
delay: {},
wait_template: {},
wait_for_trigger: {},
repeat_count: {},
repeat_while: {},
repeat_until: {},
repeat_for_each: {},
choose: {},
if: {},
stop: {},
sequence: {},
parallel: {},
variables: {},
},
},
{
titleKey: "ui.panel.config.automation.editor.actions.groups.helpers.label",
groups: {
helpers: {},
},
},
{
titleKey: "ui.panel.config.automation.editor.actions.groups.other.label",
groups: {
other: {
icon: mdiDotsHorizontal,
members: {
event: {},
service: {},
set_conversation_response: {},
other: {},
},
},
] as const;
export const ACTION_BUILDING_BLOCKS_GROUP = {
condition: {},
delay: {},
wait_template: {},
wait_for_trigger: {},
repeat_count: {},
repeat_while: {},
repeat_until: {},
repeat_for_each: {},
choose: {},
if: {},
stop: {},
sequence: {},
parallel: {},
variables: {},
};
// These will be replaced with the correct action
export const VIRTUAL_ACTIONS: Partial<
Record<keyof typeof ACTION_BUILDING_BLOCKS_GROUP, Action>
> = {
repeat_count: {
repeat: {
count: 2,
sequence: [],
},
},
repeat_while: {
repeat: {
while: [],
sequence: [],
},
},
repeat_until: {
repeat: {
until: [],
sequence: [],
},
},
repeat_for_each: {
repeat: {
for_each: {},
sequence: [],
},
},
} as const;

View File

@@ -4,7 +4,6 @@ import type {
} from "home-assistant-js-websocket";
import { ensureArray } from "../common/array/ensure-array";
import { navigate } from "../common/navigate";
import type { LocalizeKeys } from "../common/translations/localize";
import { createSearchParam } from "../common/url/search-params";
import type { Context, HomeAssistant } from "../types";
import type { BlueprintInput } from "./blueprint";
@@ -294,11 +293,6 @@ export interface ShorthandNotCondition extends ShorthandBaseCondition {
not: Condition[];
}
export interface AutomationElementGroupCollection {
titleKey?: LocalizeKeys;
groups: AutomationElementGroup;
}
export type AutomationElementGroup = Record<
string,
{ icon?: string; members?: AutomationElementGroup }

View File

@@ -3,6 +3,8 @@ import {
mdiClockOutline,
mdiCodeBraces,
mdiDevices,
mdiDotsHorizontal,
mdiExcavator,
mdiGateOr,
mdiIdentifier,
mdiMapClock,
@@ -13,7 +15,7 @@ import {
mdiStateMachine,
mdiWeatherSunny,
} from "@mdi/js";
import type { AutomationElementGroupCollection } from "./automation";
import type { AutomationElementGroup } from "./automation";
export const CONDITION_ICONS = {
device: mdiDevices,
@@ -29,31 +31,25 @@ export const CONDITION_ICONS = {
zone: mdiMapMarkerRadius,
};
export const CONDITION_COLLECTIONS: AutomationElementGroupCollection[] = [
{
groups: {
device: {},
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
time_location: {
icon: mdiMapClock,
members: { sun: {}, time: {}, zone: {} },
},
},
export const CONDITION_GROUPS: AutomationElementGroup = {
device: {},
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
time_location: {
icon: mdiMapClock,
members: { sun: {}, time: {}, zone: {} },
},
{
titleKey: "ui.panel.config.automation.editor.conditions.groups.other.label",
groups: {
building_blocks: {
icon: mdiExcavator,
members: { and: {}, or: {}, not: {} },
},
other: {
icon: mdiDotsHorizontal,
members: {
template: {},
trigger: {},
},
},
] as const;
export const CONDITION_BUILDING_BLOCKS_GROUP = {
and: {},
or: {},
not: {},
};
} as const;
export const CONDITION_BUILDING_BLOCKS = ["and", "or", "not"];

View File

@@ -76,7 +76,7 @@ export const floorCompare =
const floorA = entries?.[a];
const floorB = entries?.[b];
if (floorA && floorB && floorA.level !== floorB.level) {
return (floorB.level ?? -9999) - (floorA.level ?? -9999);
return (floorA.level ?? 9999) - (floorB.level ?? 9999);
}
const nameA = floorA?.name ?? a;
const nameB = floorB?.name ?? b;

View File

@@ -352,7 +352,6 @@ export interface NumberSelector {
interface ObjectSelectorField {
selector: Selector;
label?: string;
description?: string;
required?: boolean;
}

View File

@@ -4,6 +4,7 @@ import {
mdiClockOutline,
mdiCodeBraces,
mdiDevices,
mdiDotsHorizontal,
mdiFormatListBulleted,
mdiGestureDoubleTap,
mdiMapClock,
@@ -22,7 +23,7 @@ import {
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
import type {
AutomationElementGroupCollection,
AutomationElementGroup,
Trigger,
TriggerList,
} from "./automation";
@@ -48,26 +49,16 @@ export const TRIGGER_ICONS = {
list: mdiFormatListBulleted,
};
export const TRIGGER_COLLECTIONS: AutomationElementGroupCollection[] = [
{
groups: {
device: {},
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
time_location: {
icon: mdiMapClock,
members: {
calendar: {},
sun: {},
time: {},
time_pattern: {},
zone: {},
},
},
},
export const TRIGGER_GROUPS: AutomationElementGroup = {
device: {},
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
time_location: {
icon: mdiMapClock,
members: { calendar: {}, sun: {}, time: {}, time_pattern: {}, zone: {} },
},
{
titleKey: "ui.panel.config.automation.editor.triggers.groups.other.label",
groups: {
other: {
icon: mdiDotsHorizontal,
members: {
event: {},
geo_location: {},
homeassistant: {},
@@ -79,7 +70,7 @@ export const TRIGGER_COLLECTIONS: AutomationElementGroupCollection[] = [
persistent_notification: {},
},
},
] as const;
} as const;
export const isTriggerList = (trigger: Trigger): trigger is TriggerList =>
"triggers" in trigger;

View File

@@ -37,6 +37,7 @@
flex-direction: column;
justify-content: center;
align-items: center;
view-transition-name: layout-fade-out;
}
#ha-launch-screen svg {
width: 112px;

View File

@@ -61,6 +61,7 @@ class HassLoadingScreen extends LitElement {
display: block;
height: 100%;
background-color: var(--primary-background-color);
view-transition-name: layout-fade-out;
}
.toolbar {
display: flex;

View File

@@ -3,6 +3,7 @@ import { ReactiveElement } from "lit";
import { property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { navigate } from "../common/navigate";
import { ViewTransitionMixin } from "../mixins/view-transition-mixin";
import type { Route } from "../types";
const extractPage = (path: string, defaultPage: string) => {
@@ -43,7 +44,7 @@ export interface RouterOptions {
// Time to wait for code to load before we show loading screen.
const LOADING_SCREEN_THRESHOLD = 400; // ms
export class HassRouterPage extends ReactiveElement {
export class HassRouterPage extends ViewTransitionMixin(ReactiveElement) {
@property({ attribute: false }) public route?: Route;
protected routerOptions!: RouterOptions;
@@ -310,16 +311,18 @@ export class HassRouterPage extends ReactiveElement {
page: string,
routeOptions: RouteOptions
) {
if (this.lastChild) {
this.removeChild(this.lastChild);
}
this.startViewTransition(() => {
if (this.lastChild) {
this.removeChild(this.lastChild);
}
const panelEl = this._cache[page] || this.createElement(routeOptions.tag);
this.updatePageEl(panelEl);
this.appendChild(panelEl);
const panelEl = this._cache[page] || this.createElement(routeOptions.tag);
this.updatePageEl(panelEl);
this.appendChild(panelEl);
if (routerOptions.cacheAll || routeOptions.cache) {
this._cache[page] = panelEl;
}
if (routerOptions.cacheAll || routeOptions.cache) {
this._cache[page] = panelEl;
}
});
}
}

View File

@@ -1,15 +1,17 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, eventOptions, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { restoreScroll } from "../common/decorators/restore-scroll";
import { goBack } from "../common/navigate";
import "../components/ha-icon-button-arrow-prev";
import "../components/ha-menu-button";
import { haStyleScrollbar } from "../resources/styles";
import { ViewTransitionMixin } from "../mixins/view-transition-mixin";
import { haStyleScrollbar, haStyleViewTransitions } from "../resources/styles";
import type { HomeAssistant } from "../types";
@customElement("hass-subpage")
class HassSubpage extends LitElement {
class HassSubpage extends ViewTransitionMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public header?: string;
@@ -60,7 +62,14 @@ class HassSubpage extends LitElement {
<slot name="toolbar-icon"></slot>
</div>
</div>
<div class="content ha-scrollbar" @scroll=${this._saveScrollPos}>
<div
class=${classMap({
content: true,
"ha-scrollbar": true,
loading: !this._loaded,
})}
@scroll=${this._saveScrollPos}
>
<slot></slot>
</div>
<div id="fab">
@@ -85,6 +94,7 @@ class HassSubpage extends LitElement {
static get styles(): CSSResultGroup {
return [
haStyleScrollbar,
haStyleViewTransitions,
css`
:host {
display: block;
@@ -167,6 +177,10 @@ class HassSubpage extends LitElement {
overflow-y: auto;
overflow: auto;
-webkit-overflow-scrolling: touch;
view-transition-name: layout-fade-in;
}
.content.loading {
opacity: 0;
}
:host([narrow]) .content {
width: calc(

View File

@@ -11,7 +11,8 @@ import "../components/ha-icon-button-arrow-prev";
import "../components/ha-menu-button";
import "../components/ha-svg-icon";
import "../components/ha-tab";
import { haStyleScrollbar } from "../resources/styles";
import { ViewTransitionMixin } from "../mixins/view-transition-mixin";
import { haStyleScrollbar, haStyleViewTransitions } from "../resources/styles";
import type { HomeAssistant, Route } from "../types";
export interface PageNavigation {
@@ -29,7 +30,7 @@ export interface PageNavigation {
}
@customElement("hass-tabs-subpage")
class HassTabsSubpage extends LitElement {
class HassTabsSubpage extends ViewTransitionMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public supervisor = false;
@@ -185,7 +186,12 @@ class HassTabsSubpage extends LitElement {
</div>`
: nothing}
<div
class="content ha-scrollbar ${classMap({ tabs: showTabs })}"
class=${classMap({
content: true,
"ha-scrollbar": true,
tabs: showTabs,
loading: !this._loaded,
})}
@scroll=${this._saveScrollPos}
>
<slot></slot>
@@ -214,6 +220,7 @@ class HassTabsSubpage extends LitElement {
static get styles(): CSSResultGroup {
return [
haStyleScrollbar,
haStyleViewTransitions,
css`
:host {
display: block;
@@ -332,6 +339,10 @@ class HassTabsSubpage extends LitElement {
margin-bottom: var(--safe-area-inset-bottom);
overflow: auto;
-webkit-overflow-scrolling: touch;
view-transition-name: layout-fade-in;
}
.content.loading {
opacity: 0;
}
:host([narrow]) .content {
margin-left: var(--safe-area-inset-left);

View File

@@ -0,0 +1,201 @@
import type { PropertyValues, ReactiveElement } from "lit";
import { state } from "lit/decorators";
/**
* Abstract constructor type for a class that extends a reactive element
* @param T - The type of the reactive element
* @returns The abstract constructor
*/
type AbstractConstructor<T extends ReactiveElement> = abstract new (
...args: any[]
) => T;
/**
* ViewTransitionMixin - Adds view transition support to reactive elements
*
* This mixin provides automatic fade-in transitions when content loads using the
* View Transition API. User preferences are respected for reduced motion.
* Falls back to synchronous updates for browsers that don't support the API.
*
* @example
* Basic usage:
* ```typescript
* @customElement("my-component")
* class MyComponent extends ViewTransitionMixin(LitElement) {
* render() {
* return html`
* <div class=${classMap({ content: true, loading: !this._loaded })}>
* <slot></slot>
* </div>
* `;
* }
*
* static styles = css`
* .content {
* view-transition-name: layout-fade-in;
* }
* .content.loading {
* opacity: 0; // Hidden during initial load for transition
* }
* `;
* }
* ```
*
* @example
* Triggering transitions manually:
* ```typescript
* private _switchView() {
* this.startViewTransition(() => {
* // DOM updates here will be animated
* this.currentView = newView;
* });
* }
* ```
*
* @example
* Custom load behavior:
* ```typescript
* protected override onLoadTransition(): void {
* // Custom logic before triggering transition
* this.startViewTransition(() => {
* this._loaded = true;
* this._additionalSetup();
* });
* }
* ```
*
* Features:
* - Automatic fade-in transition when slotted content loads
* - Provides `_loaded` state property for conditional rendering
* - `startViewTransition()` method for manual transitions
* - Respects prefers-reduced-motion user preference
* - Falls back gracefully when View Transition API unavailable
* - Automatic cleanup of event listeners
*
* The mixin monitors the default slot and triggers `onLoadTransition()` when
* content is available. Override `onLoadTransition()` to customize this behavior.
*/
export const ViewTransitionMixin = <
T extends AbstractConstructor<ReactiveElement>,
>(
superClass: T
) => {
abstract class ViewTransitionClass extends superClass {
/**
* Reference to the default (unnamed) slot element for monitoring content changes.
* Used to detect when slotted content is available to trigger load transitions.
*/
private _slot?: HTMLSlotElement;
/**
* Prevents multiple slotchange events from triggering the transition more than once.
* Once content loads and transition starts, this flag ensures it won't retrigger.
*/
private _transitionTriggered = false;
/**
* State property indicating whether content has finished loading.
* Use this in templates with the loading class pattern to hide content until ready.
*/
@state() protected _loaded = false;
/**
* Trigger a view transition if supported by the browser
* @param updateCallback - Callback function that updates the DOM
* @returns Promise that resolves when the transition is complete
*/
protected async startViewTransition(
updateCallback: () => void | Promise<void>
): Promise<void> {
if (
!document.startViewTransition ||
window.matchMedia("(prefers-reduced-motion: reduce)").matches
) {
// Fallback: update without view transition
await updateCallback();
return;
}
const transition = document.startViewTransition(async () => {
await updateCallback();
});
try {
await transition.finished;
} catch {
// View transition failed - this is non-critical, continue silently
}
}
/**
* Callback executed when content is ready to transition in.
*
* Called automatically when:
* - The default slot receives content (slotchange event)
* - No slot exists in the component (triggers immediately after firstUpdated)
*
* Default implementation sets `_loaded = true` within a view transition.
* Override this method to add custom logic before or during the transition,
* but ensure you call `startViewTransition()` to maintain transition behavior.
*/
protected onLoadTransition(): void {
this.startViewTransition(() => {
this._loaded = true;
});
}
/**
* Check if slot has content and trigger transition if it does
*/
private _checkSlotContent = (): void => {
// Guard against multiple slotchange events triggering the transition multiple times
if (this._transitionTriggered) {
return;
}
if (this._slot) {
const elements = this._slot.assignedElements();
if (elements.length > 0) {
this._transitionTriggered = true;
this.onLoadTransition();
}
}
};
/**
* Automatically apply view transition on first render
* @param changedProperties - Properties that changed
*/
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
// Wait for slotted content to be ready, then trigger transition
// Only monitor the default (unnamed) slot - named slots are for specific purposes
this._slot = this.shadowRoot?.querySelector("slot:not([name])") as
| HTMLSlotElement
| undefined;
if (this._slot) {
this._checkSlotContent();
this._slot.addEventListener("slotchange", this._checkSlotContent);
} else {
// Start transition immediately if no slot is found
this.onLoadTransition();
}
}
/**
* Cleanup event listeners when component is removed from the DOM.
* Removes the slotchange listener.
*/
override disconnectedCallback(): void {
super.disconnectedCallback();
if (this._slot) {
this._slot.removeEventListener("slotchange", this._checkSlotContent);
this._slot = undefined;
this._transitionTriggered = false;
this._loaded = false;
}
}
}
return ViewTransitionClass;
};

View File

@@ -1,10 +1,9 @@
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple";
import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit";
import { LitElement, html, nothing } from "lit";
import { customElement, property, queryAll, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { ensureArray } from "../../../../common/array/ensure-array";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
@@ -16,18 +15,19 @@ import {
ACTION_BUILDING_BLOCKS,
getService,
isService,
VIRTUAL_ACTIONS,
} from "../../../../data/action";
import type { AutomationClipboard } from "../../../../data/automation";
import type { Action } from "../../../../data/script";
import type { HomeAssistant } from "../../../../types";
import {
PASTE_VALUE,
VIRTUAL_ACTIONS,
showAddAutomationElementDialog,
} from "../show-add-automation-element-dialog";
import { automationRowsStyles } from "../styles";
import type HaAutomationActionRow from "./ha-automation-action-row";
import { getAutomationActionType } from "./ha-automation-action-row";
import { ensureArray } from "../../../../common/array/ensure-array";
@customElement("ha-automation-action")
export default class HaAutomationAction extends LitElement {
@@ -136,6 +136,17 @@ export default class HaAutomationAction extends LitElement {
"ui.panel.config.automation.editor.actions.add"
)}
</ha-button>
<ha-button
.disabled=${this.disabled}
@click=${this._addActionBuildingBlockDialog}
appearance="plain"
.size=${this.root ? "medium" : "small"}
>
<ha-svg-icon .path=${mdiPlus} slot="start"></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.add_building_block"
)}
</ha-button>
</div>
</div>
</ha-sortable>
@@ -211,6 +222,15 @@ export default class HaAutomationAction extends LitElement {
});
}
private _addActionBuildingBlockDialog() {
showAddAutomationElementDialog(this, {
type: "action",
add: this._addAction,
clipboardItem: getAutomationActionType(this._clipboard?.action),
group: "building_blocks",
});
}
private _addAction = (action: string) => {
let actions: Action[];
if (action === PASTE_VALUE) {

File diff suppressed because it is too large Load Diff

View File

@@ -214,6 +214,17 @@ export default class HaAutomationCondition extends LitElement {
"ui.panel.config.automation.editor.conditions.add"
)}
</ha-button>
<ha-button
.disabled=${this.disabled}
appearance="plain"
.size=${this.root ? "medium" : "small"}
@click=${this._addConditionBuildingBlockDialog}
>
<ha-svg-icon .path=${mdiPlus} slot="start"></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.add_building_block"
)}
</ha-button>
</div>
</div>
</ha-sortable>
@@ -231,6 +242,15 @@ export default class HaAutomationCondition extends LitElement {
});
}
private _addConditionBuildingBlockDialog() {
showAddAutomationElementDialog(this, {
type: "condition",
add: this._addCondition,
clipboardItem: this._clipboard?.condition?.condition,
group: "building_blocks",
});
}
private _addCondition = (value) => {
let conditions: Condition[];
if (value === PASTE_VALUE) {

View File

@@ -36,8 +36,7 @@ export default class HaAutomationSidebar extends LitElement {
@property({ type: Boolean }) public narrow = false;
@property({ type: Number, attribute: "sidebar-key" })
public sidebarKey?: number;
@property({ attribute: "sidebar-key" }) public sidebarKey?: string;
@state() private _yamlMode = false;

View File

@@ -101,7 +101,7 @@ export class HaManualAutomationEditor extends LitElement {
@state() private _sidebarConfig?: SidebarConfig;
@state() private _sidebarKey = 0;
@state() private _sidebarKey?: string;
@storage({
key: "automation-sidebar-width",
@@ -350,9 +350,7 @@ export class HaManualAutomationEditor extends LitElement {
// deselect previous selected row
this._sidebarConfig?.close?.();
this._sidebarConfig = ev.detail;
// be sure the sidebar editor is recreated
this._sidebarKey++;
this._sidebarKey = JSON.stringify(this._sidebarConfig);
await this._sidebarElement?.updateComplete;
this._sidebarElement?.focus();
@@ -377,7 +375,6 @@ export class HaManualAutomationEditor extends LitElement {
return;
}
this._sidebarConfig?.close();
this._sidebarKey = 0;
}
}

View File

@@ -1,11 +1,45 @@
import { fireEvent } from "../../../common/dom/fire_event";
import type { ACTION_GROUPS } from "../../../data/action";
import type { ActionType } from "../../../data/script";
export const PASTE_VALUE = "__paste__";
// These will be replaced with the correct action
export const VIRTUAL_ACTIONS: Record<
keyof (typeof ACTION_GROUPS)["building_blocks"]["members"],
ActionType
> = {
repeat_count: {
repeat: {
count: 2,
sequence: [],
},
},
repeat_while: {
repeat: {
while: [],
sequence: [],
},
},
repeat_until: {
repeat: {
until: [],
sequence: [],
},
},
repeat_for_each: {
repeat: {
for_each: {},
sequence: [],
},
},
} as const;
export interface AddAutomationElementDialogParams {
type: "trigger" | "condition" | "action";
add: (key: string) => void;
clipboardItem: string | undefined;
group?: string;
}
const loadDialog = () => import("./add-automation-element-dialog");

View File

@@ -44,8 +44,7 @@ export default class HaAutomationSidebarAction extends LitElement {
@property({ type: Boolean }) public narrow = false;
@property({ type: Number, attribute: "sidebar-key" })
public sidebarKey?: number;
@property({ attribute: "sidebar-key" }) public sidebarKey?: string;
@state() private _warnings?: string[];

View File

@@ -44,8 +44,7 @@ export default class HaAutomationSidebarCondition extends LitElement {
@property({ type: Boolean }) public narrow = false;
@property({ type: Number, attribute: "sidebar-key" })
public sidebarKey?: number;
@property({ attribute: "sidebar-key" }) public sidebarKey?: string;
@state() private _warnings?: string[];

View File

@@ -26,8 +26,7 @@ export default class HaAutomationSidebarScriptFieldSelector extends LitElement {
@property({ type: Boolean }) public narrow = false;
@property({ type: Number, attribute: "sidebar-key" })
public sidebarKey?: number;
@property({ attribute: "sidebar-key" }) public sidebarKey?: string;
@state() private _warnings?: string[];

View File

@@ -25,8 +25,7 @@ export default class HaAutomationSidebarScriptField extends LitElement {
@property({ type: Boolean }) public narrow = false;
@property({ type: Number, attribute: "sidebar-key" })
public sidebarKey?: number;
@property({ attribute: "sidebar-key" }) public sidebarKey?: string;
@state() private _warnings?: string[];

View File

@@ -37,8 +37,7 @@ export default class HaAutomationSidebarTrigger extends LitElement {
@property({ type: Boolean }) public narrow = false;
@property({ type: Number, attribute: "sidebar-key" })
public sidebarKey?: number;
@property({ attribute: "sidebar-key" }) public sidebarKey?: string;
@state() private _warnings?: string[];

View File

@@ -89,7 +89,7 @@ export class HaManualScriptEditor extends LitElement {
@state() private _sidebarConfig?: SidebarConfig;
@state() private _sidebarKey = 0;
@state() private _sidebarKey?: string;
@storage({
key: "automation-sidebar-width",
@@ -512,9 +512,7 @@ export class HaManualScriptEditor extends LitElement {
// deselect previous selected row
this._sidebarConfig?.close?.();
this._sidebarConfig = ev.detail;
// be sure the sidebar editor is recreated
this._sidebarKey++;
this._sidebarKey = JSON.stringify(this._sidebarConfig);
await this._sidebarElement?.updateComplete;
this._sidebarElement?.focus();
@@ -539,7 +537,6 @@ export class HaManualScriptEditor extends LitElement {
return;
}
this._sidebarConfig?.close();
this._sidebarKey = 0;
}
}

View File

@@ -72,7 +72,8 @@ import {
} from "../../dialogs/quick-bar/show-dialog-quick-bar";
import { showShortcutsDialog } from "../../dialogs/shortcuts/show-shortcuts-dialog";
import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
import { haStyle } from "../../resources/styles";
import { ViewTransitionMixin } from "../../mixins/view-transition-mixin";
import { haStyle, haStyleViewTransitions } from "../../resources/styles";
import type { HomeAssistant, PanelInfo } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
import { showToast } from "../../util/toast";
@@ -114,7 +115,7 @@ interface SubActionItem {
}
@customElement("hui-root")
class HUIRoot extends LitElement {
class HUIRoot extends ViewTransitionMixin(LitElement) {
@property({ attribute: false }) public panel?: PanelInfo<LovelacePanelConfig>;
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -496,6 +497,7 @@ class HUIRoot extends LitElement {
class=${classMap({
"edit-mode": this._editMode,
narrow: this.narrow,
loading: !this._loaded,
})}
>
<div class="header">
@@ -1165,43 +1167,45 @@ class HUIRoot extends LitElement {
// Recreate a new element to clear the applied themes.
const root = this._viewRoot;
if (root.lastChild) {
root.removeChild(root.lastChild);
}
this.startViewTransition(() => {
if (root.lastChild) {
root.removeChild(root.lastChild);
}
if (viewIndex === "hass-unused-entities") {
const unusedEntities = document.createElement("hui-unused-entities");
// Wait for promise to resolve so that the element has been upgraded.
import("./editor/unused-entities/hui-unused-entities").then(() => {
unusedEntities.hass = this.hass!;
unusedEntities.lovelace = this.lovelace!;
unusedEntities.narrow = this.narrow;
});
root.appendChild(unusedEntities);
return;
}
if (viewIndex === "hass-unused-entities") {
const unusedEntities = document.createElement("hui-unused-entities");
// Wait for promise to resolve so that the element has been upgraded.
import("./editor/unused-entities/hui-unused-entities").then(() => {
unusedEntities.hass = this.hass!;
unusedEntities.lovelace = this.lovelace!;
unusedEntities.narrow = this.narrow;
});
root.appendChild(unusedEntities);
return;
}
let view;
const viewConfig = this.config.views[viewIndex];
let view;
const viewConfig = this.config.views[viewIndex];
if (!viewConfig) {
this.lovelace!.setEditMode(true);
return;
}
if (!viewConfig) {
this.lovelace!.setEditMode(true);
return;
}
if (!force && this._viewCache![viewIndex]) {
view = this._viewCache![viewIndex];
} else {
view = document.createElement("hui-view");
view.index = viewIndex;
this._viewCache![viewIndex] = view;
}
if (!force && this._viewCache![viewIndex]) {
view = this._viewCache![viewIndex];
} else {
view = document.createElement("hui-view");
view.index = viewIndex;
this._viewCache![viewIndex] = view;
}
view.lovelace = this.lovelace;
view.hass = this.hass;
view.narrow = this.narrow;
view.lovelace = this.lovelace;
view.hass = this.hass;
view.narrow = this.narrow;
root.appendChild(view);
root.appendChild(view);
});
}
private _openShortcutDialog(ev: Event) {
@@ -1212,12 +1216,21 @@ class HUIRoot extends LitElement {
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleViewTransitions,
css`
:host {
-ms-user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
}
@media (prefers-reduced-motion: no-preference) {
::view-transition-new(hui-root-container) {
animation: fade-in var(--ha-animation-layout-duration) ease-out;
animation-delay: var(--ha-animation-layout-delay-base);
}
}
.header {
background-color: var(--app-header-background-color);
color: var(--app-header-text-color, white);
@@ -1406,6 +1419,10 @@ class HUIRoot extends LitElement {
padding-right: var(--safe-area-inset-right);
padding-inline-end: var(--safe-area-inset-right);
padding-bottom: var(--safe-area-inset-bottom);
view-transition-name: hui-root-container;
}
.loading hui-view-container {
opacity: 0;
}
.narrow hui-view-container {
padding-left: var(--safe-area-inset-left);
@@ -1414,6 +1431,7 @@ class HUIRoot extends LitElement {
hui-view-container > * {
flex: 1 1 100%;
max-width: 100%;
view-transition-name: layout-fade-in;
}
/**
* In edit mode we have the tab bar on a new line *

View File

@@ -199,3 +199,56 @@ export const baseEntrypointStyles = css`
width: 100vw;
}
`;
export const haStyleViewTransitions = css`
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@media (prefers-reduced-motion: no-preference) {
/* Prevent root cross-fade during view transitions (pseudo-element) */
::view-transition-old(root) {
animation: none;
}
::view-transition-new(root) {
animation: none;
}
/* Elements leaving the view (loading screen) */
::view-transition-group(layout-fade-out) {
animation-duration: var(--ha-animation-layout-duration);
animation-timing-function: ease-out;
}
::view-transition-old(layout-fade-out) {
animation: fade-out var(--ha-animation-layout-duration) ease-out;
}
::view-transition-new(layout-fade-out) {
animation: none;
}
/* New content entering (panels, subpages)
Uses base delay to be less abrupt and allow for elements to render */
::view-transition-group(layout-fade-in) {
animation-duration: var(--ha-animation-layout-duration);
animation-timing-function: ease-out;
}
::view-transition-new(layout-fade-in) {
animation: fade-in var(--ha-animation-layout-duration) ease-out;
animation-delay: var(--ha-animation-layout-delay-base);
}
}
`;

View File

@@ -42,6 +42,17 @@ export const coreStyles = css`
--ha-space-18: 72px;
--ha-space-19: 76px;
--ha-space-20: 80px;
/* Animation timing */
--ha-animation-layout-duration: 350ms;
--ha-animation-layout-delay-base: 100ms;
}
@media (prefers-reduced-motion: reduce) {
html {
--ha-animation-layout-duration: 0ms;
--ha-animation-layout-delay-base: 0ms;
}
}
`;

View File

@@ -676,9 +676,6 @@
},
"entity-state-picker": {
"state": "State"
},
"entity-state-content-picker": {
"add": "Add"
}
},
"target-picker": {
@@ -3918,6 +3915,7 @@
"edit_yaml": "Edit in YAML",
"edit_ui": "Edit in visual editor",
"copy_to_clipboard": "Copy to clipboard",
"search_in": "Search · {group}",
"unknown_entity": "unknown entity",
"edit_unknown_device": "Editor not available for unknown device",
"switch_ui_yaml_error": "There are currently YAML errors in the automation, and it cannot be parsed. Switching to UI mode may cause pending changes to be lost. Press cancel to correct any errors before proceeding to prevent loss of pending changes, or continue if you are sure.",
@@ -3932,7 +3930,6 @@
"item_pasted": "{item} pasted",
"ctrl": "Ctrl",
"del": "Del",
"blocks": "Blocks",
"triggers": {
"name": "Triggers",
"header": "When",
@@ -3940,7 +3937,7 @@
"learn_more": "Learn more about triggers",
"triggered": "Triggered",
"add": "Add trigger",
"empty_search": "No triggers found for {term}",
"search": "Search trigger",
"id": "Trigger ID",
"id_helper": "Helps identify each run based on which trigger fired.",
"optional": "Optional",
@@ -3961,16 +3958,14 @@
"trigger": "Trigger",
"copied_to_clipboard": "Trigger copied to clipboard",
"cut_to_clipboard": "Trigger cut to clipboard",
"select": "Select a trigger",
"groups": {
"device": {
"label": "Device"
},
"entity": {
"label": "Entity"
"label": "Entity",
"description": "When something happens to an entity."
},
"time_location": {
"label": "Time and location"
"label": "Time and location",
"description": "When someone enters or leaves a zone, or at a specific time."
},
"other": {
"label": "Other triggers"
@@ -4203,7 +4198,7 @@
"description": "All conditions added here need to be satisfied for the automation to run. A condition can be satisfied or not at any given time, for example: ''If {user} is home''. You can use building blocks to create more complex conditions.",
"learn_more": "Learn more about conditions",
"add": "Add condition",
"empty_search": "No conditions and blocks found for {term}",
"search": "Search condition",
"add_building_block": "Add building block",
"test": "Test",
"testing_error": "Condition did not pass",
@@ -4225,22 +4220,21 @@
"condition": "Condition",
"copied_to_clipboard": "Condition copied to clipboard",
"cut_to_clipboard": "Condition cut to clipboard",
"select": "Select a condition",
"groups": {
"device": {
"label": "Device"
},
"entity": {
"label": "Entity"
"label": "Entity",
"description": "If an entity is in a specific state."
},
"time_location": {
"label": "Time and location"
"label": "Time and location",
"description": "If someone is in a zone or if the current time is before or after a specified time."
},
"other": {
"label": "Other conditions"
},
"building_blocks": {
"label": "Building blocks"
"label": "Building blocks",
"description": "Build more complex conditions."
}
},
"type": {
@@ -4371,7 +4365,7 @@
"description": "All actions added here will be performed in sequence when the automation runs. An action usually controls one of your areas, devices, or entities, for example: 'Turn on the lights'. You can use building blocks to create more complex sequences of actions.",
"learn_more": "Learn more about actions",
"add": "Add action",
"empty_search": "No actions and blocks found for {term}",
"search": "Search action",
"add_building_block": "Add building block",
"invalid_action": "Invalid action",
"run": "Run action",
@@ -4395,11 +4389,7 @@
"action": "Action",
"copied_to_clipboard": "Action copied to clipboard",
"cut_to_clipboard": "Action cut to clipboard",
"select": "Select an action",
"groups": {
"device_id": {
"label": "Device"
},
"helpers": {
"label": "Helpers"
},
@@ -4407,7 +4397,8 @@
"label": "Other actions"
},
"building_blocks": {
"label": "Building blocks"
"label": "Building blocks",
"description": "Build more complex sequences of actions."
}
},
"type": {

View File

@@ -3,8 +3,21 @@ import { render } from "lit";
export const removeLaunchScreen = () => {
const launchScreenElement = document.getElementById("ha-launch-screen");
if (launchScreenElement) {
launchScreenElement.parentElement!.removeChild(launchScreenElement);
if (!launchScreenElement?.parentElement) {
return;
}
// Use View Transition API if available and user doesn't prefer reduced motion
if (
document.startViewTransition &&
!window.matchMedia("(prefers-reduced-motion: reduce)").matches
) {
document.startViewTransition(() => {
launchScreenElement.parentElement?.removeChild(launchScreenElement);
});
} else {
// Fallback: Direct removal without transition
launchScreenElement.parentElement.removeChild(launchScreenElement);
}
};

View File

@@ -26,7 +26,7 @@ describe("floorCompare", () => {
});
describe("floorCompare(entries)", () => {
it("sorts by level descending (highest to lowest), then by name", () => {
it("sorts by level, then by name", () => {
const entries = {
floor1: { name: "Ground Floor", level: 0 } as FloorRegistryEntry,
floor2: { name: "First Floor", level: 1 } as FloorRegistryEntry,
@@ -35,13 +35,13 @@ describe("floorCompare", () => {
const floors = ["floor1", "floor2", "floor3"];
expect(floors.sort(floorCompare(entries))).toEqual([
"floor2",
"floor1",
"floor3",
"floor1",
"floor2",
]);
});
it("treats null level as -9999, placing it at the end", () => {
it("treats null level as 9999, placing it at the end", () => {
const entries = {
floor1: { name: "Ground Floor", level: 0 } as FloorRegistryEntry,
floor2: { name: "First Floor", level: 1 } as FloorRegistryEntry,
@@ -50,8 +50,8 @@ describe("floorCompare", () => {
const floors = ["floor2", "floor3", "floor1"];
expect(floors.sort(floorCompare(entries))).toEqual([
"floor2",
"floor1",
"floor2",
"floor3",
]);
});

156
yarn.lock
View File

@@ -4966,106 +4966,106 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/eslint-plugin@npm:8.46.2":
version: 8.46.2
resolution: "@typescript-eslint/eslint-plugin@npm:8.46.2"
"@typescript-eslint/eslint-plugin@npm:8.46.1":
version: 8.46.1
resolution: "@typescript-eslint/eslint-plugin@npm:8.46.1"
dependencies:
"@eslint-community/regexpp": "npm:^4.10.0"
"@typescript-eslint/scope-manager": "npm:8.46.2"
"@typescript-eslint/type-utils": "npm:8.46.2"
"@typescript-eslint/utils": "npm:8.46.2"
"@typescript-eslint/visitor-keys": "npm:8.46.2"
"@typescript-eslint/scope-manager": "npm:8.46.1"
"@typescript-eslint/type-utils": "npm:8.46.1"
"@typescript-eslint/utils": "npm:8.46.1"
"@typescript-eslint/visitor-keys": "npm:8.46.1"
graphemer: "npm:^1.4.0"
ignore: "npm:^7.0.0"
natural-compare: "npm:^1.4.0"
ts-api-utils: "npm:^2.1.0"
peerDependencies:
"@typescript-eslint/parser": ^8.46.2
"@typescript-eslint/parser": ^8.46.1
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/00c659fcc04c185e6cdfb6c7e52beae1935f1475fef4079193a719f93858b6255e07b4764fc7104e9524a4d0b7652e63616b93e7f112f1cba4e983d10383e224
checksum: 10/9fd8c279584e11c7dcfcac6dddc4dde8719f8fe79349f5a2d0473ffcee198dd543a5311b24c601228ae03cc1a47b29118261bcf45f7f697c8ba1e4289fda4096
languageName: node
linkType: hard
"@typescript-eslint/parser@npm:8.46.2":
version: 8.46.2
resolution: "@typescript-eslint/parser@npm:8.46.2"
"@typescript-eslint/parser@npm:8.46.1":
version: 8.46.1
resolution: "@typescript-eslint/parser@npm:8.46.1"
dependencies:
"@typescript-eslint/scope-manager": "npm:8.46.2"
"@typescript-eslint/types": "npm:8.46.2"
"@typescript-eslint/typescript-estree": "npm:8.46.2"
"@typescript-eslint/visitor-keys": "npm:8.46.2"
"@typescript-eslint/scope-manager": "npm:8.46.1"
"@typescript-eslint/types": "npm:8.46.1"
"@typescript-eslint/typescript-estree": "npm:8.46.1"
"@typescript-eslint/visitor-keys": "npm:8.46.1"
debug: "npm:^4.3.4"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/2ee394d880b5a9372ecf50ddbf70f66e9ecc16691a210dd40b5b152310a539005dfed13105e0adc81f1a9f49d86f7b78ddf3bf8d777fe84c179eb6a8be2fa56c
checksum: 10/4edcb49bb001e9a0e72155c4181f941be00c603bf277c283d4185dca528e9642da927032e8d2671c444ca1904c7f51743029b4b48c12e94d39df2dac49d7d3ff
languageName: node
linkType: hard
"@typescript-eslint/project-service@npm:8.46.2":
version: 8.46.2
resolution: "@typescript-eslint/project-service@npm:8.46.2"
"@typescript-eslint/project-service@npm:8.46.1":
version: 8.46.1
resolution: "@typescript-eslint/project-service@npm:8.46.1"
dependencies:
"@typescript-eslint/tsconfig-utils": "npm:^8.46.2"
"@typescript-eslint/types": "npm:^8.46.2"
"@typescript-eslint/tsconfig-utils": "npm:^8.46.1"
"@typescript-eslint/types": "npm:^8.46.1"
debug: "npm:^4.3.4"
peerDependencies:
typescript: ">=4.8.4 <6.0.0"
checksum: 10/76ba446f86e83b4afd6dacbebc9a0737b5a3e0500a0712b37fea4f0141dcf4c9238e8e5a9a649cf609a4624cc575431506a2a56432aaa18d4c3a8cf2df9d1480
checksum: 10/d63cbb88524be85ba626c4969bdec1cd5c1ab64b6ebdd565a45698e700efb764f192db1cdc3322f4d63d3acd8d0a36e2685b89bdfa2edf50fda3c2d0cb6efdd7
languageName: node
linkType: hard
"@typescript-eslint/scope-manager@npm:8.46.2":
version: 8.46.2
resolution: "@typescript-eslint/scope-manager@npm:8.46.2"
"@typescript-eslint/scope-manager@npm:8.46.1":
version: 8.46.1
resolution: "@typescript-eslint/scope-manager@npm:8.46.1"
dependencies:
"@typescript-eslint/types": "npm:8.46.2"
"@typescript-eslint/visitor-keys": "npm:8.46.2"
checksum: 10/6a8a9b644ff57ca9e992348553f19f6e010d76ff4872d972d333a16952e93cce4bf5096a1fefe1af8b452bce963fde6c78410d15817e673b75176ec3241949e9
"@typescript-eslint/types": "npm:8.46.1"
"@typescript-eslint/visitor-keys": "npm:8.46.1"
checksum: 10/3d73812087a17be84184cc68143d4dca7602b8cd4bf5ad334e541d4b3acf5c65c58935369dcf66ab81b38014fe0c6bc57ac2f655fdd69b3e24161a827b86bd34
languageName: node
linkType: hard
"@typescript-eslint/tsconfig-utils@npm:8.46.2, @typescript-eslint/tsconfig-utils@npm:^8.46.2":
version: 8.46.2
resolution: "@typescript-eslint/tsconfig-utils@npm:8.46.2"
"@typescript-eslint/tsconfig-utils@npm:8.46.1, @typescript-eslint/tsconfig-utils@npm:^8.46.1":
version: 8.46.1
resolution: "@typescript-eslint/tsconfig-utils@npm:8.46.1"
peerDependencies:
typescript: ">=4.8.4 <6.0.0"
checksum: 10/e459d131ca646cca6ad164593ca7e8c45ad3daa103a24e1e57fd47b5c1e5b5418948b749f02baa42e61103a496fc80d32ddd1841c11495bbcf37808b88bb0ef4
checksum: 10/f033d68a53f62c7cc4c09e5697dd9b7fa34a3c3e79133e0b14ca582821869b77e81d3942b91535f6ef789ffaaad31eef1e1ace20518e7de0935a55a16120fae7
languageName: node
linkType: hard
"@typescript-eslint/type-utils@npm:8.46.2":
version: 8.46.2
resolution: "@typescript-eslint/type-utils@npm:8.46.2"
"@typescript-eslint/type-utils@npm:8.46.1":
version: 8.46.1
resolution: "@typescript-eslint/type-utils@npm:8.46.1"
dependencies:
"@typescript-eslint/types": "npm:8.46.2"
"@typescript-eslint/typescript-estree": "npm:8.46.2"
"@typescript-eslint/utils": "npm:8.46.2"
"@typescript-eslint/types": "npm:8.46.1"
"@typescript-eslint/typescript-estree": "npm:8.46.1"
"@typescript-eslint/utils": "npm:8.46.1"
debug: "npm:^4.3.4"
ts-api-utils: "npm:^2.1.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/db5d3d782b44d31f828ebdbec44550c6f94fdcfac1164f59e3922f6413feed749d93df3977625fd5949aaff5c691cf4603a7cd93eaf7b19b9cf6fd91537fb8c7
checksum: 10/db989c1f55624b34da24eaf0dc230ee696a1f2a614ea95a8dd3b8635ad47d748140be2345ed7afcee844dfabd41129f5a8ca583b1a4d6ecc7d581f89c5e508e2
languageName: node
linkType: hard
"@typescript-eslint/types@npm:8.46.2, @typescript-eslint/types@npm:^8.46.2":
version: 8.46.2
resolution: "@typescript-eslint/types@npm:8.46.2"
checksum: 10/c641453c868b730ef64bd731cc47b19e1a5e45c090dfe9542ecd15b24c5a7b6dc94a8ef4e548b976aabcd1ca9dec1b766e417454b98ea59079795eb008226b38
"@typescript-eslint/types@npm:8.46.1, @typescript-eslint/types@npm:^8.46.1":
version: 8.46.1
resolution: "@typescript-eslint/types@npm:8.46.1"
checksum: 10/d162ddf6d77d8c9bdfca942da5de5fb4ba80efa740b14077482b5a71282f1d05e1b1dd393ae810eb2923ca9c845bd26b4a9d2dbf25d43dd5d9cb6e20c2a1db46
languageName: node
linkType: hard
"@typescript-eslint/typescript-estree@npm:8.46.2":
version: 8.46.2
resolution: "@typescript-eslint/typescript-estree@npm:8.46.2"
"@typescript-eslint/typescript-estree@npm:8.46.1":
version: 8.46.1
resolution: "@typescript-eslint/typescript-estree@npm:8.46.1"
dependencies:
"@typescript-eslint/project-service": "npm:8.46.2"
"@typescript-eslint/tsconfig-utils": "npm:8.46.2"
"@typescript-eslint/types": "npm:8.46.2"
"@typescript-eslint/visitor-keys": "npm:8.46.2"
"@typescript-eslint/project-service": "npm:8.46.1"
"@typescript-eslint/tsconfig-utils": "npm:8.46.1"
"@typescript-eslint/types": "npm:8.46.1"
"@typescript-eslint/visitor-keys": "npm:8.46.1"
debug: "npm:^4.3.4"
fast-glob: "npm:^3.3.2"
is-glob: "npm:^4.0.3"
@@ -5074,32 +5074,32 @@ __metadata:
ts-api-utils: "npm:^2.1.0"
peerDependencies:
typescript: ">=4.8.4 <6.0.0"
checksum: 10/4d2149ad97e7f7e2e4cf466932f52f38e90414d47341c5938e497fd0826d403db9896bbd5cc08e7488ad0d0ffb3817e6f18e9f0c623d8a8cda09af204f81aab8
checksum: 10/af068a14d6d0b4849e9f0e52b7ddcd24c266f099528c7b62ff2bebebc0fb82d07439bf6dc565b27cf2fed0af0aaae618aae220676d0fb041c93ec2a8163f0da1
languageName: node
linkType: hard
"@typescript-eslint/utils@npm:8.46.2":
version: 8.46.2
resolution: "@typescript-eslint/utils@npm:8.46.2"
"@typescript-eslint/utils@npm:8.46.1":
version: 8.46.1
resolution: "@typescript-eslint/utils@npm:8.46.1"
dependencies:
"@eslint-community/eslint-utils": "npm:^4.7.0"
"@typescript-eslint/scope-manager": "npm:8.46.2"
"@typescript-eslint/types": "npm:8.46.2"
"@typescript-eslint/typescript-estree": "npm:8.46.2"
"@typescript-eslint/scope-manager": "npm:8.46.1"
"@typescript-eslint/types": "npm:8.46.1"
"@typescript-eslint/typescript-estree": "npm:8.46.1"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/91f6216f858161c3f59b2e035e0abce68fcdc9fbe45cb693a111c11ce5352c42fe0b1145a91e538c5459ff81b5e3741a4b38189b97e0e1a756567b6467c7b6c9
checksum: 10/a8fed8aebd34a559c5abd780649edd6be632531e4930b19642f0fdc862b77bff463ef200e8ced48ba489c3fceee7443b6735c87b918b97b98e95e842cd8a38b5
languageName: node
linkType: hard
"@typescript-eslint/visitor-keys@npm:8.46.2":
version: 8.46.2
resolution: "@typescript-eslint/visitor-keys@npm:8.46.2"
"@typescript-eslint/visitor-keys@npm:8.46.1":
version: 8.46.1
resolution: "@typescript-eslint/visitor-keys@npm:8.46.1"
dependencies:
"@typescript-eslint/types": "npm:8.46.2"
"@typescript-eslint/types": "npm:8.46.1"
eslint-visitor-keys: "npm:^4.2.1"
checksum: 10/4352629a33bc1619dc78d55eaec382be4c7e1059af02660f62bfdb22933021deaf98504d4030b8db74ec122e6d554e9015341f87aed729fb70fae613f12f55a4
checksum: 10/eed1c5ce08d2743bd2ec95a33f2118a67596b1b9fa5bf6a3d84ed09ca66e09af3cc91ef3e302c2222e5882e13576340532b586030b3652ce046eb218cd4508b7
languageName: node
linkType: hard
@@ -9422,7 +9422,7 @@ __metadata:
leaflet: "npm:1.9.4"
leaflet-draw: "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
leaflet.markercluster: "npm:1.5.3"
lint-staged: "npm:16.2.5"
lint-staged: "npm:16.2.4"
lit: "npm:3.3.1"
lit-analyzer: "npm:2.0.3"
lit-html: "npm:3.3.1"
@@ -9452,7 +9452,7 @@ __metadata:
tinykeys: "npm:3.0.0"
ts-lit-plugin: "npm:2.0.2"
typescript: "npm:5.9.3"
typescript-eslint: "npm:8.46.2"
typescript-eslint: "npm:8.46.1"
ua-parser-js: "npm:2.0.6"
vite-tsconfig-paths: "npm:5.1.4"
vitest: "npm:3.2.4"
@@ -10761,9 +10761,9 @@ __metadata:
languageName: node
linkType: hard
"lint-staged@npm:16.2.5":
version: 16.2.5
resolution: "lint-staged@npm:16.2.5"
"lint-staged@npm:16.2.4":
version: 16.2.4
resolution: "lint-staged@npm:16.2.4"
dependencies:
commander: "npm:^14.0.1"
listr2: "npm:^9.0.4"
@@ -10774,7 +10774,7 @@ __metadata:
yaml: "npm:^2.8.1"
bin:
lint-staged: bin/lint-staged.js
checksum: 10/9a1019d2d95646bb7acb0a43ec1f89427f2066defa7fc89f82c71b25fdc55d1c16e1ac44afbed1440c37615c4bbaeafd687bfb0adf1a08ae5bc9d1015325ca88
checksum: 10/e4ce8e6b07fc2c1d96962dafaab483271b2359b6f22f74324ba0f827ad6383caa2800651379f36b2570cfa74b27354e0db9316be69795636a0c53fa3fd599b79
languageName: node
linkType: hard
@@ -14435,18 +14435,18 @@ __metadata:
languageName: node
linkType: hard
"typescript-eslint@npm:8.46.2":
version: 8.46.2
resolution: "typescript-eslint@npm:8.46.2"
"typescript-eslint@npm:8.46.1":
version: 8.46.1
resolution: "typescript-eslint@npm:8.46.1"
dependencies:
"@typescript-eslint/eslint-plugin": "npm:8.46.2"
"@typescript-eslint/parser": "npm:8.46.2"
"@typescript-eslint/typescript-estree": "npm:8.46.2"
"@typescript-eslint/utils": "npm:8.46.2"
"@typescript-eslint/eslint-plugin": "npm:8.46.1"
"@typescript-eslint/parser": "npm:8.46.1"
"@typescript-eslint/typescript-estree": "npm:8.46.1"
"@typescript-eslint/utils": "npm:8.46.1"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/cd1bbc5d33c0369f70032165224badf1a8a9f95f39c891e4f71c78ceea9e7b2d71e0516d8b38177a11217867f387788f3fa126381418581409e7a76cdfdfe909
checksum: 10/ba6914cc4006390908de9e3de295c2f7110461175a818608d198e2d1529e726c32d778fe9e224ea30464ba2c4a43c05f534d2dbc5aabf297354a2aa49a2e1cd6
languageName: node
linkType: hard