Compare commits

..

31 Commits

Author SHA1 Message Date
Wendelin
459a0bd178 Merge branch 'add-automation-new-design' of github.com:home-assistant/frontend into bottom-sheet-swipe-close 2025-10-16 16:31:27 +02:00
Wendelin
d808c63c55 Merge branch 'dev' of github.com:home-assistant/frontend into add-automation-new-design 2025-10-16 16:29:10 +02:00
Aidan Timson
f32ca9be29 Set header bar min height and make sure items are centered (#27542) 2025-10-16 16:14:08 +02:00
Aidan Timson
8c4c4157a8 Remove unnecessary on-surface-default semantic color (#27536) 2025-10-16 16:07:26 +02:00
Wendelin
c8419d4c3d Improve target picker section title (#27539) 2025-10-16 15:37:04 +02:00
Wendelin
80135af9ec Fix bottom sheet padding 2025-10-16 15:29:27 +02:00
Wendelin
848b737a92 update target picker selector 2025-10-16 15:20:08 +02:00
Wendelin
50d4fae14a Merge branch 'add-automation-new-design' of github.com:home-assistant/frontend into bottom-sheet-swipe-close 2025-10-16 15:16:36 +02:00
Wendelin
1da3570a6f fix translations, scroll issues, RTL 2025-10-16 15:01:57 +02:00
Paul Bottein
089316b8ae Fix duplicated name in entity name picker and fix missing entity id support (#27538) 2025-10-16 15:47:47 +03:00
Wendelin
6c4046c842 add swipe to close for bottom sheet 2025-10-16 14:07:41 +02:00
Wendelin
8d03ac5f64 Fix target picker device/floor icon (#27515)
* Fix device icon alginment

* Expand target item group if new target is in there

* Remove sticky header animation

* Fix type attribute

* Fix floor icon

* Reflect collapsed

* fix 0 entities target

* Improve empty search
2025-10-16 14:09:05 +03:00
Wendelin
e0e1f6f920 use popover with trap focus (#27533)
* use popover with focus trap

* update attribute

* Use new WA
2025-10-16 14:03:00 +03:00
Paul Bottein
d4c98cae3a Update drag icon (#27514) 2025-10-16 11:33:25 +02:00
renovate[bot]
46d0eb4f44 Update dependency @codemirror/view to v6.38.6 (#27531)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-16 10:35:26 +02:00
Wendelin
e97b04b5b9 Fix device translation 2025-10-16 10:19:28 +02:00
Wendelin
6dbfb60825 Add keybindings and blocks search separation 2025-10-16 09:44:46 +02:00
karwosts
07812f8d84 Support media-source links for view background (#27522) 2025-10-16 08:42:39 +03:00
Paul Bottein
96f54d348f Fix entity badge name (#27520) 2025-10-16 08:38:42 +03:00
Paul Bottein
6084ab116f Use empty string for no name instead of empty array for name (#27523) 2025-10-16 08:35:38 +03:00
Simon Lamon
6b7acd8d3b Only show backup ad when cloud is enabled (#27524) 2025-10-16 08:34:23 +03:00
Wendelin
ba2881b0bf Add max-height 2025-10-15 17:00:06 +02:00
Wendelin
e42be13682 fix height 2025-10-15 16:53:32 +02:00
Wendelin
bf8dacb7a4 Add tabs 2025-10-15 16:50:18 +02:00
karwosts
e35b155c66 Delete image selector (#27519) 2025-10-15 13:32:10 +00:00
Paul Bottein
437d02c12f Group dashboards by type (#27517)
Group dashboard by type
2025-10-15 16:11:37 +03:00
Wendelin
a23fc8fcba revert merge 2025-10-14 17:07:48 +02:00
Wendelin
0ba88246a1 Merge branch 'dev' of github.com:home-assistant/frontend into add-automation-new-design 2025-10-14 17:01:30 +02:00
Wendelin
6529b31b65 WIP new add dialog 2025-10-14 16:59:46 +02:00
Wendelin
866518477f Merge branch 'dev' of github.com:home-assistant/frontend into add-automation-new-design 2025-10-13 13:13:41 +02:00
Wendelin
f831f876de WIP new add automation element 2025-10-10 14:48:18 +02:00
57 changed files with 1478 additions and 1078 deletions

View File

@@ -34,7 +34,7 @@
"@codemirror/legacy-modes": "6.5.2", "@codemirror/legacy-modes": "6.5.2",
"@codemirror/search": "6.5.11", "@codemirror/search": "6.5.11",
"@codemirror/state": "6.5.2", "@codemirror/state": "6.5.2",
"@codemirror/view": "6.38.5", "@codemirror/view": "6.38.6",
"@date-fns/tz": "1.4.1", "@date-fns/tz": "1.4.1",
"@egjs/hammerjs": "2.0.17", "@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.18.2", "@formatjs/intl-datetimeformat": "6.18.2",
@@ -52,7 +52,7 @@
"@fullcalendar/list": "6.1.19", "@fullcalendar/list": "6.1.19",
"@fullcalendar/luxon3": "6.1.19", "@fullcalendar/luxon3": "6.1.19",
"@fullcalendar/timegrid": "6.1.19", "@fullcalendar/timegrid": "6.1.19",
"@home-assistant/webawesome": "3.0.0-beta.6.ha.4", "@home-assistant/webawesome": "3.0.0-beta.6.ha.5",
"@lezer/highlight": "1.2.1", "@lezer/highlight": "1.2.1",
"@lit-labs/motion": "1.0.9", "@lit-labs/motion": "1.0.9",
"@lit-labs/observers": "2.0.6", "@lit-labs/observers": "2.0.6",

View File

@@ -1,4 +1,4 @@
import { mdiDrag, mdiEye, mdiEyeOff } from "@mdi/js"; import { mdiDragHorizontalVariant, mdiEye, mdiEyeOff } from "@mdi/js";
import type { CSSResultGroup } from "lit"; import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
@@ -129,7 +129,7 @@ export class DialogDataTableSettings extends LitElement {
${canMove && isVisible ${canMove && isVisible
? html`<ha-svg-icon ? html`<ha-svg-icon
class="handle" class="handle"
.path=${mdiDrag} .path=${mdiDragHorizontalVariant}
slot="graphic" slot="graphic"
></ha-svg-icon>` ></ha-svg-icon>`
: nothing} : nothing}

View File

@@ -1,13 +1,13 @@
import { mdiDrag } from "@mdi/js"; import { mdiDragHorizontalVariant } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { isValidEntityId } from "../../common/entity/valid_entity_id"; import { isValidEntityId } from "../../common/entity/valid_entity_id";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
import type { HomeAssistant, ValueChangedEvent } from "../../types"; import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-sortable"; import "../ha-sortable";
import "./ha-entity-picker"; import "./ha-entity-picker";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
@customElement("ha-entities-picker") @customElement("ha-entities-picker")
class HaEntitiesPicker extends LitElement { class HaEntitiesPicker extends LitElement {
@@ -118,7 +118,7 @@ class HaEntitiesPicker extends LitElement {
? html` ? html`
<ha-svg-icon <ha-svg-icon
class="entity-handle" class="entity-handle"
.path=${mdiDrag} .path=${mdiDragHorizontalVariant}
></ha-svg-icon> ></ha-svg-icon>
` `
: nothing} : nothing}

View File

@@ -1,5 +1,5 @@
import "@material/mwc-menu/mwc-menu-surface"; import "@material/mwc-menu/mwc-menu-surface";
import { mdiDrag, mdiPlus } from "@mdi/js"; import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { IFuseOptions } from "fuse.js"; import type { IFuseOptions } from "fuse.js";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
@@ -86,8 +86,8 @@ export class HaEntityNamePicker extends LitElement {
private _editIndex?: number; private _editIndex?: number;
private _validOptions = memoizeOne((entityId?: string) => { private _validTypes = memoizeOne((entityId?: string) => {
const options = new Set<string>(); const options = new Set<string>(["text"]);
if (!entityId) { if (!entityId) {
return options; return options;
} }
@@ -119,22 +119,22 @@ export class HaEntityNamePicker extends LitElement {
return []; return [];
} }
const options = this._validOptions(entityId); const types = this._validTypes(entityId);
const items = ( const items = (
["entity", "device", "area", "floor"] as const ["entity", "device", "area", "floor"] as const
).map<EntityNameOption>((name) => { ).map<EntityNameOption>((name) => {
const stateObj = this.hass.states[entityId]; const stateObj = this.hass.states[entityId];
const isValid = options.has(name); const isValid = types.has(name);
const primary = this.hass.localize( const primary = this.hass.localize(
`ui.components.entity.entity-name-picker.types.${name}` `ui.components.entity.entity-name-picker.types.${name}`
); );
const secondary = const secondary =
stateObj && isValid (stateObj && isValid
? this.hass.formatEntityName(stateObj, { type: name }) ? this.hass.formatEntityName(stateObj, { type: name })
: this.hass.localize( : this.hass.localize(
`ui.components.entity.entity-name-picker.types.${name}_missing` as LocalizeKeys `ui.components.entity.entity-name-picker.types.${name}_missing` as LocalizeKeys
) || "-"; )) || "-";
return { return {
primary, primary,
@@ -169,9 +169,9 @@ export class HaEntityNamePicker extends LitElement {
}; };
protected render() { protected render() {
const value = this._value; const value = this._items;
const options = this._getOptions(this.entityId); const options = this._getOptions(this.entityId);
const validOptions = this._validOptions(this.entityId); const validTypes = this._validTypes(this.entityId);
return html` return html`
${this.label ? html`<label>${this.label}</label>` : nothing} ${this.label ? html`<label>${this.label}</label>` : nothing}
@@ -185,12 +185,11 @@ export class HaEntityNamePicker extends LitElement {
> >
<ha-chip-set> <ha-chip-set>
${repeat( ${repeat(
this._value, this._items,
(item) => item, (item) => item,
(item: EntityNameItem, idx) => { (item: EntityNameItem, idx) => {
const label = this._formatItem(item); const label = this._formatItem(item);
const isValid = const isValid = validTypes.has(item.type);
item.type === "text" || validOptions.has(item.type);
return html` return html`
<ha-input-chip <ha-input-chip
data-idx=${idx} data-idx=${idx}
@@ -201,7 +200,10 @@ export class HaEntityNamePicker extends LitElement {
.disabled=${this.disabled} .disabled=${this.disabled}
class=${!isValid ? "invalid" : ""} class=${!isValid ? "invalid" : ""}
> >
<ha-svg-icon slot="icon" .path=${mdiDrag}></ha-svg-icon> <ha-svg-icon
slot="icon"
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
<span>${label}</span> <span>${label}</span>
</ha-input-chip> </ha-input-chip>
`; `;
@@ -235,7 +237,7 @@ export class HaEntityNamePicker extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.value=${""} .value=${""}
.autofocus=${this.autofocus} .autofocus=${this.autofocus}
.disabled=${this.disabled || !this.entityId} .disabled=${this.disabled}
.required=${this.required && !value.length} .required=${this.required && !value.length}
.helper=${this.helper} .helper=${this.helper}
.items=${options} .items=${options}
@@ -282,13 +284,16 @@ export class HaEntityNamePicker extends LitElement {
this._opened = true; this._opened = true;
} }
private get _value(): EntityNameItem[] { private get _items(): EntityNameItem[] {
return this._toItems(this.value); return this._toItems(this.value);
} }
private _toItems = memoizeOne((value?: typeof this.value) => { private _toItems = memoizeOne((value?: typeof this.value) => {
if (typeof value === "string") { if (typeof value === "string") {
return [{ type: "text", text: value } as const]; if (value === "") {
return [];
}
return [{ type: "text", text: value } satisfies EntityNameItem];
} }
return value ? ensureArray(value) : []; return value ? ensureArray(value) : [];
}); });
@@ -296,7 +301,7 @@ export class HaEntityNamePicker extends LitElement {
private _toValue = memoizeOne( private _toValue = memoizeOne(
(items: EntityNameItem[]): typeof this.value => { (items: EntityNameItem[]): typeof this.value => {
if (items.length === 0) { if (items.length === 0) {
return []; return "";
} }
if (items.length === 1) { if (items.length === 1) {
const item = items[0]; const item = items[0];
@@ -312,19 +317,21 @@ export class HaEntityNamePicker extends LitElement {
const options = this._comboBox.items || []; const options = this._comboBox.items || [];
const initialItem = const initialItem =
this._editIndex != null ? this._value[this._editIndex] : undefined; this._editIndex != null ? this._items[this._editIndex] : undefined;
const initialValue = initialItem ? formatOptionValue(initialItem) : ""; const initialValue = initialItem ? formatOptionValue(initialItem) : "";
const filteredItems = this._filterSelectedOptions(options, initialValue); const filteredItems = this._filterSelectedOptions(options, initialValue);
if (initialItem && initialItem.type === "text" && initialItem.text) { if (initialItem?.type === "text" && initialItem.text) {
filteredItems.push(this._customNameOption(initialItem.text)); filteredItems.push(this._customNameOption(initialItem.text));
} }
this._comboBox.filteredItems = filteredItems; this._comboBox.filteredItems = filteredItems;
this._comboBox.setInputValue(initialValue); this._comboBox.setInputValue(initialValue);
} else { } else {
this._opened = false; this._opened = false;
this._comboBox.setInputValue("");
} }
} }
@@ -332,15 +339,16 @@ export class HaEntityNamePicker extends LitElement {
options: EntityNameOption[], options: EntityNameOption[],
current?: string current?: string
) => { ) => {
const value = this._value; const items = this._items;
const types = value.map((item) => item.type) as string[]; const excludedValues = new Set(
items
.filter((item) => UNIQUE_TYPES.has(item.type))
.map((item) => formatOptionValue(item))
);
const filteredOptions = options.filter( const filteredOptions = options.filter(
(option) => (option) => !excludedValues.has(option.value) || option.value === current
!UNIQUE_TYPES.has(option.value) ||
!types.includes(option.value) ||
option.value === current
); );
return filteredOptions; return filteredOptions;
}; };
@@ -351,16 +359,14 @@ export class HaEntityNamePicker extends LitElement {
const options = this._comboBox.items || []; const options = this._comboBox.items || [];
const currentItem = const currentItem =
this._editIndex != null ? this._value[this._editIndex] : undefined; this._editIndex != null ? this._items[this._editIndex] : undefined;
const currentValue = currentItem ? formatOptionValue(currentItem) : ""; const currentValue = currentItem ? formatOptionValue(currentItem) : "";
this._comboBox.filteredItems = this._filterSelectedOptions( let filteredItems = this._filterSelectedOptions(options, currentValue);
options,
currentValue
);
if (!filter) { if (!filter) {
this._comboBox.filteredItems = filteredItems;
return; return;
} }
@@ -372,9 +378,8 @@ export class HaEntityNamePicker extends LitElement {
ignoreDiacritics: true, ignoreDiacritics: true,
}; };
const fuse = new Fuse(this._comboBox.filteredItems, fuseOptions); const fuse = new Fuse(filteredItems, fuseOptions);
const filteredItems = fuse.search(filter).map((result) => result.item); filteredItems = fuse.search(filter).map((result) => result.item);
filteredItems.push(this._customNameOption(input)); filteredItems.push(this._customNameOption(input));
this._comboBox.filteredItems = filteredItems; this._comboBox.filteredItems = filteredItems;
} }
@@ -382,7 +387,7 @@ export class HaEntityNamePicker extends LitElement {
private async _moveItem(ev: CustomEvent) { private async _moveItem(ev: CustomEvent) {
ev.stopPropagation(); ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail; const { oldIndex, newIndex } = ev.detail;
const value = this._value; const value = this._items;
const newValue = value.concat(); const newValue = value.concat();
const element = newValue.splice(oldIndex, 1)[0]; const element = newValue.splice(oldIndex, 1)[0];
newValue.splice(newIndex, 0, element); newValue.splice(newIndex, 0, element);
@@ -393,7 +398,7 @@ export class HaEntityNamePicker extends LitElement {
private async _removeItem(ev) { private async _removeItem(ev) {
ev.stopPropagation(); ev.stopPropagation();
const value = [...this._value]; const value = [...this._items];
const idx = parseInt(ev.target.dataset.idx, 10); const idx = parseInt(ev.target.dataset.idx, 10);
value.splice(idx, 1); value.splice(idx, 1);
this._setValue(value); this._setValue(value);
@@ -411,7 +416,7 @@ export class HaEntityNamePicker extends LitElement {
const item: EntityNameItem = parseOptionValue(value); const item: EntityNameItem = parseOptionValue(value);
const newValue = [...this._value]; const newValue = [...this._items];
if (this._editIndex != null) { if (this._editIndex != null) {
newValue[this._editIndex] = item; newValue[this._editIndex] = item;

View File

@@ -1,4 +1,4 @@
import { mdiDrag } from "@mdi/js"; import { mdiDragHorizontalVariant } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit"; import type { PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
@@ -195,7 +195,10 @@ class HaEntityStatePicker extends LitElement {
.label=${label} .label=${label}
selected selected
> >
<ha-svg-icon slot="icon" .path=${mdiDrag}></ha-svg-icon> <ha-svg-icon
slot="icon"
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
${label} ${label}
</ha-input-chip> </ha-input-chip>
`; `;

View File

@@ -1,4 +1,4 @@
import { mdiDrag, mdiTextureBox } from "@mdi/js"; import { mdiDragHorizontalVariant, mdiTextureBox } from "@mdi/js";
import type { TemplateResult } from "lit"; import type { TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
@@ -105,7 +105,7 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
<ha-svg-icon <ha-svg-icon
class="handle" class="handle"
slot="icons" slot="icons"
.path=${mdiDrag} .path=${mdiDragHorizontalVariant}
></ha-svg-icon> ></ha-svg-icon>
`} `}
<ha-items-display-editor <ha-items-display-editor

View File

@@ -1,6 +1,13 @@
import "@home-assistant/webawesome/dist/components/drawer/drawer"; import "@home-assistant/webawesome/dist/components/drawer/drawer";
import { css, html, LitElement, type PropertyValues } from "lit"; import { css, html, LitElement, type PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators"; import {
customElement,
eventOptions,
property,
query,
state,
} from "lit/decorators";
import { haStyleScrollbar } from "../resources/styles";
export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300; export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300;
@@ -13,6 +20,16 @@ export class HaBottomSheet extends LitElement {
@state() private _drawerOpen = false; @state() private _drawerOpen = false;
@query(".body") public bodyContainer!: HTMLDivElement;
private _lockResize = false;
private _lockResizeByChild = false;
private _resizeStartY = 0;
private _resizeDelta = 0;
private _handleAfterHide() { private _handleAfterHide() {
this.open = false; this.open = false;
const ev = new Event("closed", { const ev = new Event("closed", {
@@ -36,54 +53,144 @@ export class HaBottomSheet extends LitElement {
.open=${this._drawerOpen} .open=${this._drawerOpen}
@wa-after-hide=${this._handleAfterHide} @wa-after-hide=${this._handleAfterHide}
without-header without-header
@touchstart=${this._handleTouchStart}
> >
<slot></slot> <slot name="header"></slot>
<div
class="body ha-scrollbar"
@scroll=${this._handleScroll}
@bottom-sheet-lock-resize-changed=${this._handleLockResizeByChild}
>
<slot></slot>
</div>
</wa-drawer> </wa-drawer>
`; `;
} }
static styles = css` @eventOptions({ passive: true })
wa-drawer { private _handleScroll(ev: Event) {
--wa-color-surface-raised: transparent; const target = ev.target as HTMLElement;
--spacing: 0; this._lockResize = target.scrollTop > 0;
--size: var(--ha-bottom-sheet-height, auto); }
--show-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
--hide-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms; private _handleLockResizeByChild = (ev: CustomEvent<boolean>) => {
this._lockResizeByChild = ev.detail;
if (this._lockResizeByChild) {
this._endResizing();
} }
wa-drawer::part(dialog) { };
max-height: var(--ha-bottom-sheet-max-height, 90vh);
align-items: center; private _handleTouchStart = (ev: TouchEvent) => {
} if (this._lockResize || this._lockResizeByChild) {
wa-drawer::part(body) { return;
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) { this._startResizing(ev.touches[0].clientY);
display: flex; };
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)`
);
});
} }
`; };
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)),
);
}
: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 { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ha-bottom-sheet": HaBottomSheet; "ha-bottom-sheet": HaBottomSheet;
} }
interface HASSDomEvents {
"bottom-sheet-lock-resize-changed": boolean;
}
} }

View File

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

@@ -49,12 +49,16 @@ export class HaDialogHeader extends LitElement {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
padding: 4px; padding: 0 var(--ha-space-1);
box-sizing: border-box; box-sizing: border-box;
} }
.header-content { .header-content {
flex: 1; flex: 1;
padding: 10px 4px; padding: 10px var(--ha-space-1);
display: flex;
flex-direction: column;
justify-content: center;
min-height: var(--ha-space-12);
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -63,7 +67,7 @@ export class HaDialogHeader extends LitElement {
.header-title { .header-title {
height: var( height: var(
--ha-dialog-header-title-height, --ha-dialog-header-title-height,
calc(var(--ha-font-size-xl) + 4px) calc(var(--ha-font-size-xl) + var(--ha-space-1))
); );
font-size: var(--ha-font-size-xl); font-size: var(--ha-font-size-xl);
line-height: var(--ha-line-height-condensed); line-height: var(--ha-line-height-condensed);
@@ -76,19 +80,19 @@ export class HaDialogHeader extends LitElement {
} }
@media all and (min-width: 450px) and (min-height: 500px) { @media all and (min-width: 450px) and (min-height: 500px) {
.header-bar { .header-bar {
padding: 16px; padding: 0 var(--ha-space-2);
} }
} }
.header-navigation-icon { .header-navigation-icon {
flex: none; flex: none;
min-width: 8px; min-width: var(--ha-space-2);
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
} }
.header-action-items { .header-action-items {
flex: none; flex: none;
min-width: 8px; min-width: var(--ha-space-2);
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View File

@@ -1,5 +1,5 @@
import { ResizeController } from "@lit-labs/observers/resize-controller"; import { ResizeController } from "@lit-labs/observers/resize-controller";
import { mdiDrag, mdiEye, mdiEyeOff } from "@mdi/js"; import { mdiDragHorizontalVariant, mdiEye, mdiEyeOff } from "@mdi/js";
import type { TemplateResult } from "lit"; import type { TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
@@ -178,7 +178,7 @@ export class HaItemDisplayEditor extends LitElement {
? this._dragHandleKeydown ? this._dragHandleKeydown
: undefined} : undefined}
class="handle" class="handle"
.path=${mdiDrag} .path=${mdiDragHorizontalVariant}
slot="end" slot="end"
></ha-svg-icon> ></ha-svg-icon>
` `

View File

@@ -1,152 +0,0 @@
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { ImageSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-icon-button";
import "../ha-textarea";
import "../ha-textfield";
import "../ha-picture-upload";
import "../ha-radio";
import "../ha-formfield";
import type { HaPictureUpload } from "../ha-picture-upload";
import { URL_PREFIX } from "../../data/image_upload";
@customElement("ha-selector-image")
export class HaImageSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public value?: any;
@property() public name?: string;
@property() public label?: string;
@property() public placeholder?: string;
@property() public helper?: string;
@property({ attribute: false }) public selector!: ImageSelector;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
@state() private showUpload = false;
protected firstUpdated(changedProps): void {
super.firstUpdated(changedProps);
if (!this.value || this.value.startsWith(URL_PREFIX)) {
this.showUpload = true;
}
}
protected render() {
return html`
<div>
<label>
${this.hass.localize(
"ui.components.selectors.image.select_image_with_label",
{
label:
this.label ||
this.hass.localize("ui.components.selectors.image.image"),
}
)}
<ha-formfield
.label=${this.hass.localize("ui.components.selectors.image.upload")}
>
<ha-radio
name="mode"
value="upload"
.checked=${this.showUpload}
@change=${this._radioGroupPicked}
></ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.hass.localize("ui.components.selectors.image.url")}
>
<ha-radio
name="mode"
value="url"
.checked=${!this.showUpload}
@change=${this._radioGroupPicked}
></ha-radio>
</ha-formfield>
</label>
${!this.showUpload
? html`
<ha-textfield
.name=${this.name}
.value=${this.value || ""}
.placeholder=${this.placeholder || ""}
.helper=${this.helper}
helperPersistent
.disabled=${this.disabled}
@input=${this._handleChange}
.label=${this.label || ""}
.required=${this.required}
></ha-textfield>
`
: html`
<ha-picture-upload
.hass=${this.hass}
.value=${this.value?.startsWith(URL_PREFIX) ? this.value : null}
.original=${this.selector.image?.original}
.cropOptions=${this.selector.image?.crop}
select-media
@change=${this._pictureChanged}
></ha-picture-upload>
`}
</div>
`;
}
private _radioGroupPicked(ev): void {
this.showUpload = ev.target.value === "upload";
}
private _pictureChanged(ev) {
const value = (ev.target as HaPictureUpload).value;
fireEvent(this, "value-changed", { value: value ?? undefined });
}
private _handleChange(ev) {
let value = ev.target.value;
if (this.value === value) {
return;
}
if (value === "" && !this.required) {
value = undefined;
}
fireEvent(this, "value-changed", { value });
}
static styles = css`
:host {
display: block;
position: relative;
}
div {
display: flex;
flex-direction: column;
}
label {
display: flex;
flex-direction: column;
}
ha-textarea,
ha-textfield {
width: 100%;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-image": HaImageSelector;
}
}

View File

@@ -1,4 +1,9 @@
import { mdiClose, mdiDelete, mdiDrag, mdiPencil } from "@mdi/js"; import {
mdiClose,
mdiDelete,
mdiDragHorizontalVariant,
mdiPencil,
} from "@mdi/js";
import { css, html, LitElement, nothing, type PropertyValues } from "lit"; import { css, html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
@@ -92,7 +97,7 @@ export class HaObjectSelector extends LitElement {
? html` ? html`
<ha-svg-icon <ha-svg-icon
class="handle" class="handle"
.path=${mdiDrag} .path=${mdiDragHorizontalVariant}
slot="start" slot="start"
></ha-svg-icon> ></ha-svg-icon>
` `

View File

@@ -1,4 +1,4 @@
import { mdiDrag } from "@mdi/js"; import { mdiDragHorizontalVariant } from "@mdi/js";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
@@ -197,7 +197,7 @@ export class HaSelectSelector extends LitElement {
? html` ? html`
<ha-svg-icon <ha-svg-icon
slot="icon" slot="icon"
.path=${mdiDrag} .path=${mdiDragHorizontalVariant}
></ha-svg-icon> ></ha-svg-icon>
` `
: nothing} : nothing}

View File

@@ -34,7 +34,6 @@ const LOAD_ELEMENTS = {
file: () => import("./ha-selector-file"), file: () => import("./ha-selector-file"),
floor: () => import("./ha-selector-floor"), floor: () => import("./ha-selector-floor"),
label: () => import("./ha-selector-label"), label: () => import("./ha-selector-label"),
image: () => import("./ha-selector-image"),
background: () => import("./ha-selector-background"), background: () => import("./ha-selector-background"),
language: () => import("./ha-selector-language"), language: () => import("./ha-selector-language"),
navigation: () => import("./ha-selector-navigation"), navigation: () => import("./ha-selector-navigation"),

View File

@@ -36,8 +36,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public value?: HassServiceTarget; @property({ attribute: false }) public value?: HassServiceTarget;
@property() public label?: string;
@property() public helper?: string; @property() public helper?: string;
@property({ type: Boolean, reflect: true }) public compact = false; @property({ type: Boolean, reflect: true }) public compact = false;
@@ -101,7 +99,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
(floor_id) => html` (floor_id) => html`
<ha-target-picker-value-chip <ha-target-picker-value-chip
.hass=${this.hass} .hass=${this.hass}
.type=${"floor"} type="floor"
.itemId=${floor_id} .itemId=${floor_id}
@remove-target-item=${this._handleRemove} @remove-target-item=${this._handleRemove}
@expand-target-item=${this._handleExpand} @expand-target-item=${this._handleExpand}
@@ -114,7 +112,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
(area_id) => html` (area_id) => html`
<ha-target-picker-value-chip <ha-target-picker-value-chip
.hass=${this.hass} .hass=${this.hass}
.type=${"area"} type="area"
.itemId=${area_id} .itemId=${area_id}
@remove-target-item=${this._handleRemove} @remove-target-item=${this._handleRemove}
@expand-target-item=${this._handleExpand} @expand-target-item=${this._handleExpand}
@@ -127,7 +125,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
(device_id) => html` (device_id) => html`
<ha-target-picker-value-chip <ha-target-picker-value-chip
.hass=${this.hass} .hass=${this.hass}
.type=${"device"} type="device"
.itemId=${device_id} .itemId=${device_id}
@remove-target-item=${this._handleRemove} @remove-target-item=${this._handleRemove}
@expand-target-item=${this._handleExpand} @expand-target-item=${this._handleExpand}
@@ -140,7 +138,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
(entity_id) => html` (entity_id) => html`
<ha-target-picker-value-chip <ha-target-picker-value-chip
.hass=${this.hass} .hass=${this.hass}
.type=${"entity"} type="entity"
.itemId=${entity_id} .itemId=${entity_id}
@remove-target-item=${this._handleRemove} @remove-target-item=${this._handleRemove}
@expand-target-item=${this._handleExpand} @expand-target-item=${this._handleExpand}
@@ -153,7 +151,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
(label_id) => html` (label_id) => html`
<ha-target-picker-value-chip <ha-target-picker-value-chip
.hass=${this.hass} .hass=${this.hass}
.type=${"label"} type="label"
.itemId=${label_id} .itemId=${label_id}
@remove-target-item=${this._handleRemove} @remove-target-item=${this._handleRemove}
@expand-target-item=${this._handleExpand} @expand-target-item=${this._handleExpand}
@@ -173,7 +171,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
type="entity" type="entity"
.hass=${this.hass} .hass=${this.hass}
.items=${{ entity: ensureArray(this.value?.entity_id) }} .items=${{ entity: ensureArray(this.value?.entity_id) }}
.collapsed=${this.compact}
.deviceFilter=${this.deviceFilter} .deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter} .entityFilter=${this.entityFilter}
.includeDomains=${this.includeDomains} .includeDomains=${this.includeDomains}
@@ -189,7 +186,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
type="device" type="device"
.hass=${this.hass} .hass=${this.hass}
.items=${{ device: ensureArray(this.value?.device_id) }} .items=${{ device: ensureArray(this.value?.device_id) }}
.collapsed=${this.compact}
.deviceFilter=${this.deviceFilter} .deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter} .entityFilter=${this.entityFilter}
.includeDomains=${this.includeDomains} .includeDomains=${this.includeDomains}
@@ -208,7 +204,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
floor: ensureArray(this.value?.floor_id), floor: ensureArray(this.value?.floor_id),
area: ensureArray(this.value?.area_id), area: ensureArray(this.value?.area_id),
}} }}
.collapsed=${this.compact}
.deviceFilter=${this.deviceFilter} .deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter} .entityFilter=${this.entityFilter}
.includeDomains=${this.includeDomains} .includeDomains=${this.includeDomains}
@@ -224,7 +219,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
type="label" type="label"
.hass=${this.hass} .hass=${this.hass}
.items=${{ label: ensureArray(this.value?.label_id) }} .items=${{ label: ensureArray(this.value?.label_id) }}
.collapsed=${this.compact}
.deviceFilter=${this.deviceFilter} .deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter} .entityFilter=${this.entityFilter}
.includeDomains=${this.includeDomains} .includeDomains=${this.includeDomains}
@@ -277,6 +271,12 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
auto-size-padding="16" auto-size-padding="16"
@wa-after-show=${this._showSelector} @wa-after-show=${this._showSelector}
@wa-after-hide=${this._hidePicker} @wa-after-hide=${this._hidePicker}
trap-focus
role="dialog"
aria-modal="true"
aria-label=${this.hass.localize(
"ui.components.target-picker.add_target"
)}
> >
${this._renderTargetSelector()} ${this._renderTargetSelector()}
</wa-popover> </wa-popover>
@@ -287,6 +287,11 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
.open=${this._pickerWrapperOpen} .open=${this._pickerWrapperOpen}
@wa-after-show=${this._showSelector} @wa-after-show=${this._showSelector}
@closed=${this._hidePicker} @closed=${this._hidePicker}
role="dialog"
aria-modal="true"
aria-label=${this.hass.localize(
"ui.components.target-picker.add_target"
)}
> >
${this._renderTargetSelector(true)} ${this._renderTargetSelector(true)}
</ha-bottom-sheet>` </ha-bottom-sheet>`
@@ -394,6 +399,12 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
} }
: { [typeId]: id }, : { [typeId]: id },
}); });
this.shadowRoot
?.querySelector(
`ha-target-picker-item-group[type='${this._newTarget?.type}']`
)
?.removeAttribute("collapsed");
} }
private _handleTargetPicked = async ( private _handleTargetPicked = async (

View File

@@ -2,26 +2,13 @@ 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 { styles } from "@material/mwc-top-app-bar/mwc-top-app-bar.css";
import { css } from "lit"; import { css } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { ViewTransitionMixin } from "../mixins/view-transition-mixin";
import { haStyleViewTransitions } from "../resources/styles";
@customElement("ha-top-app-bar-fixed") @customElement("ha-top-app-bar-fixed")
export class HaTopAppBarFixed extends ViewTransitionMixin(TopAppBarFixedBase) { export class HaTopAppBarFixed extends TopAppBarFixedBase {
@property({ type: Boolean, reflect: true }) public narrow = false; @property({ type: Boolean, reflect: true }) public narrow = false;
@property({ type: Boolean, reflect: true, attribute: "content-loading" })
public contentLoading = true;
protected onLoadTransition(): void {
// Trigger the transition when content is slotted
this.startViewTransition(() => {
this.contentLoading = false;
});
}
static override styles = [ static override styles = [
styles, styles,
haStyleViewTransitions,
css` css`
header { header {
padding-top: var(--safe-area-inset-top); padding-top: var(--safe-area-inset-top);
@@ -36,10 +23,6 @@ export class HaTopAppBarFixed extends ViewTransitionMixin(TopAppBarFixedBase) {
); );
padding-bottom: var(--safe-area-inset-bottom); padding-bottom: var(--safe-area-inset-bottom);
padding-right: var(--safe-area-inset-right); 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 { :host([narrow]) .mdc-top-app-bar--fixed-adjust {
padding-left: var(--safe-area-inset-left); padding-left: var(--safe-area-inset-left);

View File

@@ -7,18 +7,17 @@ import type { MDCTopAppBarAdapter } from "@material/top-app-bar/adapter";
import { strings } from "@material/top-app-bar/constants"; import { strings } from "@material/top-app-bar/constants";
import MDCFixedTopAppBarFoundation from "@material/top-app-bar/fixed/foundation"; import MDCFixedTopAppBarFoundation from "@material/top-app-bar/fixed/foundation";
import { html, css, nothing } from "lit"; import { html, css, nothing } from "lit";
import { property, query, customElement, state } from "lit/decorators"; import { property, query, customElement } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { styles } from "@material/mwc-top-app-bar/mwc-top-app-bar.css"; import { styles } from "@material/mwc-top-app-bar/mwc-top-app-bar.css";
import { ViewTransitionMixin } from "../mixins/view-transition-mixin"; import { haStyleScrollbar } from "../resources/styles";
import { haStyleScrollbar, haStyleViewTransitions } from "../resources/styles";
export const passiveEventOptionsIfSupported = supportsPassiveEventListener export const passiveEventOptionsIfSupported = supportsPassiveEventListener
? { passive: true } ? { passive: true }
: undefined; : undefined;
@customElement("ha-two-pane-top-app-bar-fixed") @customElement("ha-two-pane-top-app-bar-fixed")
export class TopAppBarBaseBase extends ViewTransitionMixin(BaseElement) { export class TopAppBarBaseBase extends BaseElement {
protected override mdcFoundation!: MDCFixedTopAppBarFoundation; protected override mdcFoundation!: MDCFixedTopAppBarFoundation;
protected override mdcFoundationClass = MDCFixedTopAppBarFoundation; protected override mdcFoundationClass = MDCFixedTopAppBarFoundation;
@@ -49,15 +48,6 @@ export class TopAppBarBaseBase extends ViewTransitionMixin(BaseElement) {
@query(".pane .ha-scrollbar") private _paneElement?: HTMLElement; @query(".pane .ha-scrollbar") private _paneElement?: HTMLElement;
@state() private _loaded = false;
protected onLoadTransition(): void {
// Trigger the transition when content is slotted
this.startViewTransition(() => {
this._loaded = true;
});
}
@property({ attribute: false, type: Object }) @property({ attribute: false, type: Object })
get scrollTarget() { get scrollTarget() {
return this._scrollTarget || window; return this._scrollTarget || window;
@@ -154,12 +144,7 @@ export class TopAppBarBaseBase extends ViewTransitionMixin(BaseElement) {
: nothing} : nothing}
<div class="main"> <div class="main">
${this.pane ? html`<div class="shadow-container"></div>` : nothing} ${this.pane ? html`<div class="shadow-container"></div>` : nothing}
<div <div class="content">
class=${classMap({
content: true,
loading: !this._loaded,
})}
>
<slot></slot> <slot></slot>
</div> </div>
</div> </div>
@@ -260,7 +245,6 @@ export class TopAppBarBaseBase extends ViewTransitionMixin(BaseElement) {
static override styles = [ static override styles = [
styles, styles,
haStyleScrollbar, haStyleScrollbar,
haStyleViewTransitions,
css` css`
header { header {
padding-top: var(--safe-area-inset-top); padding-top: var(--safe-area-inset-top);
@@ -357,10 +341,6 @@ export class TopAppBarBaseBase extends ViewTransitionMixin(BaseElement) {
.mdc-top-app-bar--pane .content { .mdc-top-app-bar--pane .content {
height: 100%; height: 100%;
overflow: auto; overflow: auto;
view-transition-name: layout-fade-in;
}
.content.loading {
opacity: 0;
} }
.mdc-top-app-bar__title { .mdc-top-app-bar__title {
font-size: var(--ha-font-size-xl); font-size: var(--ha-font-size-xl);

View File

@@ -1,12 +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 "@home-assistant/webawesome/dist/components/dialog/dialog";
import { mdiClose } from "@mdi/js"; import { mdiClose } from "@mdi/js";
import "./ha-dialog-header"; import { css, html, LitElement, nothing } from "lit";
import "./ha-icon-button"; import { customElement, property, query, state } from "lit/decorators";
import type { HomeAssistant } from "../types";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-dialog-header";
import "./ha-icon-button";
export type DialogWidth = "small" | "medium" | "large" | "full"; export type DialogWidth = "small" | "medium" | "large" | "full";
@@ -90,6 +90,8 @@ export class HaWaDialog extends LitElement {
@state() @state()
private _open = false; private _open = false;
@query(".body") public bodyContainer!: HTMLDivElement;
protected updated( protected updated(
changedProperties: Map<string | number | symbol, unknown> changedProperties: Map<string | number | symbol, unknown>
): void { ): void {
@@ -107,6 +109,7 @@ export class HaWaDialog extends LitElement {
.lightDismiss=${!this.preventScrimClose} .lightDismiss=${!this.preventScrimClose}
without-header without-header
@wa-show=${this._handleShow} @wa-show=${this._handleShow}
@wa-after-show=${this._handleAfterShow}
@wa-after-hide=${this._handleAfterHide} @wa-after-hide=${this._handleAfterHide}
> >
<slot name="header"> <slot name="header">
@@ -146,6 +149,10 @@ export class HaWaDialog extends LitElement {
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus(); (this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
}; };
private _handleAfterShow = () => {
fireEvent(this, "after-show");
};
private _handleAfterHide = () => { private _handleAfterHide = () => {
this._open = false; this._open = false;
fireEvent(this, "closed"); fireEvent(this, "closed");
@@ -172,7 +179,7 @@ export class HaWaDialog extends LitElement {
) )
) )
); );
--width: var(--ha-dialog-width-md, min(580px, var(--full-width))); --width: min(var(--ha-dialog-width-md, 580px), var(--full-width));
--spacing: var(--dialog-content-padding, var(--ha-space-6)); --spacing: var(--dialog-content-padding, var(--ha-space-6));
--show-duration: var(--ha-dialog-show-duration, 200ms); --show-duration: var(--ha-dialog-show-duration, 200ms);
--hide-duration: var(--ha-dialog-hide-duration, 200ms); --hide-duration: var(--ha-dialog-hide-duration, 200ms);
@@ -193,11 +200,11 @@ export class HaWaDialog extends LitElement {
} }
:host([width="small"]) wa-dialog { :host([width="small"]) wa-dialog {
--width: var(--ha-dialog-width-sm, min(320px, var(--full-width))); --width: min(var(--ha-dialog-width-sm, 320px), var(--full-width));
} }
:host([width="large"]) wa-dialog { :host([width="large"]) wa-dialog {
--width: var(--ha-dialog-width-lg, min(720px, var(--full-width))); --width: min(var(--ha-dialog-width-lg, 720px), var(--full-width));
} }
:host([width="full"]) wa-dialog { :host([width="full"]) wa-dialog {
@@ -211,6 +218,7 @@ export class HaWaDialog extends LitElement {
--ha-dialog-max-height, --ha-dialog-max-height,
calc(100% - var(--ha-space-20)) calc(100% - var(--ha-space-20))
); );
min-height: var(--ha-dialog-min-height);
position: var(--dialog-surface-position, relative); position: var(--dialog-surface-position, relative);
margin-top: var(--dialog-surface-margin-top, auto); margin-top: var(--dialog-surface-margin-top, auto);
display: flex; display: flex;
@@ -247,10 +255,7 @@ export class HaWaDialog extends LitElement {
.header-title { .header-title {
margin: 0; margin: 0;
margin-bottom: 0; margin-bottom: 0;
color: var( color: var(--ha-dialog-header-title-color, var(--primary-text-color));
--ha-dialog-header-title-color,
var(--ha-color-on-surface-default, var(--primary-text-color))
);
font-size: var( font-size: var(
--ha-dialog-header-title-font-size, --ha-dialog-header-title-font-size,
var(--ha-font-size-2xl) var(--ha-font-size-2xl)
@@ -287,6 +292,7 @@ export class HaWaDialog extends LitElement {
} }
:host([flexcontent]) .body { :host([flexcontent]) .body {
max-width: 100%; max-width: 100%;
flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@@ -315,6 +321,7 @@ declare global {
interface HASSDomEvents { interface HASSDomEvents {
opened: undefined; opened: undefined;
"after-show": undefined;
closed: undefined; closed: undefined;
} }
} }

View File

@@ -18,7 +18,7 @@ export class HaTargetPickerItemGroup extends LitElement {
Record<TargetType, string[]> Record<TargetType, string[]>
>; >;
@property({ type: Boolean }) public collapsed = false; @property({ type: Boolean, reflect: true }) public collapsed = false;
@property({ attribute: false }) @property({ attribute: false })
public deviceFilter?: HaDevicePickerDeviceFilterFunc; public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@@ -50,7 +50,11 @@ export class HaTargetPickerItemGroup extends LitElement {
} }
}); });
return html`<ha-expansion-panel .expanded=${!this.collapsed} left-chevron> return html`<ha-expansion-panel
.expanded=${!this.collapsed}
left-chevron
@expanded-changed=${this._expandedChanged}
>
<div slot="header" class="heading"> <div slot="header" class="heading">
${this.hass.localize( ${this.hass.localize(
`ui.components.target-picker.selected.${this.type}`, `ui.components.target-picker.selected.${this.type}`,
@@ -78,6 +82,10 @@ export class HaTargetPickerItemGroup extends LitElement {
</ha-expansion-panel>`; </ha-expansion-panel>`;
} }
private _expandedChanged(ev: CustomEvent) {
this.collapsed = !ev.detail.expanded;
}
static styles = css` static styles = css`
:host { :host {
display: block; display: block;

View File

@@ -130,7 +130,7 @@ export class HaTargetPickerItemRow extends LitElement {
return html` return html`
<ha-md-list-item type="text"> <ha-md-list-item type="text">
<div slot="start"> <div class="icon" slot="start">
${this.subEntry ${this.subEntry
? html` ? html`
<div class="horizontal-line-wrapper"> <div class="horizontal-line-wrapper">
@@ -172,7 +172,9 @@ export class HaTargetPickerItemRow extends LitElement {
((entries && (showEntities || showDevices)) || this._domainName) ((entries && (showEntities || showDevices)) || this._domainName)
? html` ? html`
<div slot="end" class="summary"> <div slot="end" class="summary">
${showEntities && !this.expand ${showEntities &&
!this.expand &&
entries?.referenced_entities.length
? html`<button class="main link" @click=${this._openDetails}> ? html`<button class="main link" @click=${this._openDetails}>
${this.hass.localize( ${this.hass.localize(
"ui.components.target-picker.entities_count", "ui.components.target-picker.entities_count",
@@ -606,6 +608,11 @@ export class HaTargetPickerItemRow extends LitElement {
state-badge { state-badge {
color: var(--ha-color-on-neutral-quiet); color: var(--ha-color-on-neutral-quiet);
} }
.icon {
display: flex;
}
img { img {
width: 24px; width: 24px;
height: 24px; height: 24px;
@@ -629,9 +636,6 @@ export class HaTargetPickerItemRow extends LitElement {
font-size: var(--ha-font-size-s); font-size: var(--ha-font-size-s);
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
.domain {
font-family: var(--ha-font-family-code);
}
.entries-tree { .entries-tree {
display: flex; display: flex;

View File

@@ -520,6 +520,7 @@ export class HaTargetPickerSelector extends LitElement {
id=${`list-item-${index}`} id=${`list-item-${index}`}
tabindex="-1" tabindex="-1"
.type=${type === "empty" ? "text" : "button"} .type=${type === "empty" ? "text" : "button"}
class=${type === "empty" ? "empty" : ""}
@click=${this._handlePickTarget} @click=${this._handlePickTarget}
.targetType=${type} .targetType=${type}
.targetId=${type !== "empty" ? item.id : undefined} .targetId=${type !== "empty" ? item.id : undefined}
@@ -574,9 +575,7 @@ export class HaTargetPickerSelector extends LitElement {
})} })}
/> />
` `
: type === "area" && : type === "floor"
(item as FloorComboBoxItem).type === "floor" &&
(item as FloorComboBoxItem).floor
? html`<ha-floor-icon ? html`<ha-floor-icon
slot="start" slot="start"
.floor=${(item as FloorComboBoxItem).floor!} .floor=${(item as FloorComboBoxItem).floor!}
@@ -836,7 +835,7 @@ export class HaTargetPickerSelector extends LitElement {
id: EMPTY_SEARCH, id: EMPTY_SEARCH,
primary: this.hass.localize( primary: this.hass.localize(
"ui.components.target-picker.no_target_found", "ui.components.target-picker.no_target_found",
{ term: html`<span class="search-term">"${searchTerm}"</span>` } { term: html`<div><b>${searchTerm}</b></div>` }
), ),
}); });
} else if (items.length === 0) { } else if (items.length === 0) {
@@ -969,6 +968,9 @@ export class HaTargetPickerSelector extends LitElement {
private _onScrollList(ev) { private _onScrollList(ev) {
const top = ev.target.scrollTop ?? 0; const top = ev.target.scrollTop ?? 0;
this._listScrolled = top > 0; this._listScrolled = top > 0;
if (this.mode === "dialog") {
fireEvent(this, "bottom-sheet-lock-resize-changed", this._listScrolled);
}
} }
private _resetSelectedItem() { private _resetSelectedItem() {
@@ -1020,10 +1022,14 @@ export class HaTargetPickerSelector extends LitElement {
padding: var(--ha-space-1) var(--ha-space-2); padding: var(--ha-space-1) var(--ha-space-2);
font-weight: var(--ha-font-weight-bold); font-weight: var(--ha-font-weight-bold);
color: var(--secondary-text-color); color: var(--secondary-text-color);
min-height: var(--ha-space-6);
display: flex;
align-items: center;
} }
.title { .title {
width: 100%; width: 100%;
min-height: var(--ha-space-8);
} }
:host([mode="dialog"]) .title { :host([mode="dialog"]) .title {
@@ -1055,7 +1061,6 @@ export class HaTargetPickerSelector extends LitElement {
.filter-header { .filter-header {
opacity: 0; opacity: 0;
transition: opacity 300ms ease-in;
position: absolute; position: absolute;
top: 1px; top: 1px;
width: calc(100% - var(--ha-space-8)); width: calc(100% - var(--ha-space-8));
@@ -1083,9 +1088,8 @@ export class HaTargetPickerSelector extends LitElement {
width: 100%; width: 100%;
} }
.search-term { .empty {
color: var(--primary-color); text-align: center;
font-weight: var(--ha-font-weight-medium);
} }
`, `,
]; ];

View File

@@ -6,8 +6,6 @@ import {
mdiCallSplit, mdiCallSplit,
mdiCodeBraces, mdiCodeBraces,
mdiDevices, mdiDevices,
mdiDotsHorizontal,
mdiExcavator,
mdiFormatListNumbered, mdiFormatListNumbered,
mdiGestureDoubleTap, mdiGestureDoubleTap,
mdiHandBackRight, mdiHandBackRight,
@@ -16,10 +14,10 @@ import {
mdiRoomService, mdiRoomService,
mdiShuffleDisabled, mdiShuffleDisabled,
mdiTimerOutline, mdiTimerOutline,
mdiTools,
mdiTrafficLight, mdiTrafficLight,
} from "@mdi/js"; } from "@mdi/js";
import type { AutomationElementGroup } from "./automation"; import type { AutomationElementGroupCollection } from "./automation";
import type { Action } from "./script";
export const ACTION_ICONS = { export const ACTION_ICONS = {
condition: mdiAbTesting, condition: mdiAbTesting,
@@ -48,37 +46,73 @@ export const YAML_ONLY_ACTION_TYPES = new Set<keyof typeof ACTION_ICONS>([
"variables", "variables",
]); ]);
export const ACTION_GROUPS: AutomationElementGroup = { export const ACTION_COLLECTIONS: AutomationElementGroupCollection[] = [
device_id: {}, {
helpers: { groups: {
icon: mdiTools, device_id: {},
members: {}, serviceGroups: {},
},
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: {},
}, },
}, },
other: { {
icon: mdiDotsHorizontal, titleKey: "ui.panel.config.automation.editor.actions.groups.helpers.label",
members: { groups: {
helpers: {},
},
},
{
titleKey: "ui.panel.config.automation.editor.actions.groups.other.label",
groups: {
event: {}, event: {},
service: {}, service: {},
set_conversation_response: {}, 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; } as const;

View File

@@ -218,6 +218,7 @@ export const getAreasAndFloors = (
type: "floor", type: "floor",
primary: floorName, primary: floorName,
floor: floor, floor: floor,
icon: floor.icon || undefined,
search_labels: [ search_labels: [
floor.floor_id, floor.floor_id,
floorName, floorName,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,8 +11,7 @@ import "../components/ha-icon-button-arrow-prev";
import "../components/ha-menu-button"; import "../components/ha-menu-button";
import "../components/ha-svg-icon"; import "../components/ha-svg-icon";
import "../components/ha-tab"; import "../components/ha-tab";
import { ViewTransitionMixin } from "../mixins/view-transition-mixin"; import { haStyleScrollbar } from "../resources/styles";
import { haStyleScrollbar, haStyleViewTransitions } from "../resources/styles";
import type { HomeAssistant, Route } from "../types"; import type { HomeAssistant, Route } from "../types";
export interface PageNavigation { export interface PageNavigation {
@@ -30,7 +29,7 @@ export interface PageNavigation {
} }
@customElement("hass-tabs-subpage") @customElement("hass-tabs-subpage")
class HassTabsSubpage extends ViewTransitionMixin(LitElement) { class HassTabsSubpage extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public supervisor = false; @property({ type: Boolean }) public supervisor = false;
@@ -62,18 +61,9 @@ class HassTabsSubpage extends ViewTransitionMixin(LitElement) {
@state() private _activeTab?: PageNavigation; @state() private _activeTab?: PageNavigation;
@state() private _loaded = false;
// @ts-ignore // @ts-ignore
@restoreScroll(".content") private _savedScrollPos?: number; @restoreScroll(".content") private _savedScrollPos?: number;
protected onLoadTransition(): void {
// Trigger the transition when content is slotted
this.startViewTransition(() => {
this._loaded = true;
});
}
private _getTabs = memoizeOne( private _getTabs = memoizeOne(
( (
tabs: PageNavigation[], tabs: PageNavigation[],
@@ -195,12 +185,7 @@ class HassTabsSubpage extends ViewTransitionMixin(LitElement) {
</div>` </div>`
: nothing} : nothing}
<div <div
class=${classMap({ class="content ha-scrollbar ${classMap({ tabs: showTabs })}"
content: true,
"ha-scrollbar": true,
tabs: showTabs,
loading: !this._loaded,
})}
@scroll=${this._saveScrollPos} @scroll=${this._saveScrollPos}
> >
<slot></slot> <slot></slot>
@@ -229,7 +214,6 @@ class HassTabsSubpage extends ViewTransitionMixin(LitElement) {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyleScrollbar, haStyleScrollbar,
haStyleViewTransitions,
css` css`
:host { :host {
display: block; display: block;
@@ -348,10 +332,6 @@ class HassTabsSubpage extends ViewTransitionMixin(LitElement) {
margin-bottom: var(--safe-area-inset-bottom); margin-bottom: var(--safe-area-inset-bottom);
overflow: auto; overflow: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
view-transition-name: layout-fade-in;
}
.content.loading {
opacity: 0;
} }
:host([narrow]) .content { :host([narrow]) .content {
margin-left: var(--safe-area-inset-left); margin-left: var(--safe-area-inset-left);

View File

@@ -1,94 +0,0 @@
import type { PropertyValues, ReactiveElement } from "lit";
type AbstractConstructor<T = object> = abstract new (...args: any[]) => T;
export const ViewTransitionMixin = <
T extends AbstractConstructor<ReactiveElement>,
>(
superClass: T
) => {
abstract class ViewTransitionClass extends superClass {
private _slot?: HTMLSlotElement;
private _transitionTriggered = 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 (_error) {
// View transition skipped
}
}
/**
* Optional callback to execute during the load transition
*/
protected onLoadTransition?(): void;
/**
* 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
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?.();
}
}
override disconnectedCallback(): void {
super.disconnectedCallback();
if (this._slot) {
this._slot.removeEventListener("slotchange", this._checkSlotContent);
}
}
}
return ViewTransitionClass;
};

View File

@@ -1,9 +1,10 @@
import { mdiDrag, mdiPlus } from "@mdi/js"; import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import type { PropertyValues } from "lit"; import type { PropertyValues } from "lit";
import { LitElement, html, nothing } from "lit"; import { html, LitElement, nothing } from "lit";
import { customElement, property, queryAll, state } from "lit/decorators"; import { customElement, property, queryAll, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
import { ensureArray } from "../../../../common/array/ensure-array";
import { storage } from "../../../../common/decorators/storage"; import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation"; import { stopPropagation } from "../../../../common/dom/stop_propagation";
@@ -15,19 +16,18 @@ import {
ACTION_BUILDING_BLOCKS, ACTION_BUILDING_BLOCKS,
getService, getService,
isService, isService,
VIRTUAL_ACTIONS,
} from "../../../../data/action"; } from "../../../../data/action";
import type { AutomationClipboard } from "../../../../data/automation"; import type { AutomationClipboard } from "../../../../data/automation";
import type { Action } from "../../../../data/script"; import type { Action } from "../../../../data/script";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { import {
PASTE_VALUE, PASTE_VALUE,
VIRTUAL_ACTIONS,
showAddAutomationElementDialog, showAddAutomationElementDialog,
} from "../show-add-automation-element-dialog"; } from "../show-add-automation-element-dialog";
import { automationRowsStyles } from "../styles"; import { automationRowsStyles } from "../styles";
import type HaAutomationActionRow from "./ha-automation-action-row"; import type HaAutomationActionRow from "./ha-automation-action-row";
import { getAutomationActionType } from "./ha-automation-action-row"; import { getAutomationActionType } from "./ha-automation-action-row";
import { ensureArray } from "../../../../common/array/ensure-array";
@customElement("ha-automation-action") @customElement("ha-automation-action")
export default class HaAutomationAction extends LitElement { export default class HaAutomationAction extends LitElement {
@@ -115,7 +115,9 @@ export default class HaAutomationAction extends LitElement {
@click=${stopPropagation} @click=${stopPropagation}
.index=${idx} .index=${idx}
> >
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon> <ha-svg-icon
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
</div> </div>
` `
: nothing} : nothing}
@@ -134,17 +136,6 @@ export default class HaAutomationAction extends LitElement {
"ui.panel.config.automation.editor.actions.add" "ui.panel.config.automation.editor.actions.add"
)} )}
</ha-button> </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>
</div> </div>
</ha-sortable> </ha-sortable>
@@ -220,15 +211,6 @@ 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) => { private _addAction = (action: string) => {
let actions: Action[]; let actions: Action[];
if (action === PASTE_VALUE) { if (action === PASTE_VALUE) {

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
import { mdiDrag, mdiPlus } from "@mdi/js"; import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import type { PropertyValues } from "lit"; import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit"; import { html, LitElement, nothing } from "lit";
@@ -193,7 +193,9 @@ export default class HaAutomationCondition extends LitElement {
@click=${stopPropagation} @click=${stopPropagation}
.index=${idx} .index=${idx}
> >
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon> <ha-svg-icon
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
</div> </div>
` `
: nothing} : nothing}
@@ -212,17 +214,6 @@ export default class HaAutomationCondition extends LitElement {
"ui.panel.config.automation.editor.conditions.add" "ui.panel.config.automation.editor.conditions.add"
)} )}
</ha-button> </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>
</div> </div>
</ha-sortable> </ha-sortable>
@@ -240,15 +231,6 @@ 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) => { private _addCondition = (value) => {
let conditions: Condition[]; let conditions: Condition[];
if (value === PASTE_VALUE) { if (value === PASTE_VALUE) {

View File

@@ -1,4 +1,4 @@
import { mdiDrag, mdiPlus } from "@mdi/js"; import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import type { PropertyValues } from "lit"; import type { PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
@@ -100,7 +100,9 @@ export default class HaAutomationOption extends LitElement {
@click=${stopPropagation} @click=${stopPropagation}
.index=${idx} .index=${idx}
> >
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon> <ha-svg-icon
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
</div> </div>
` `
: nothing} : nothing}

View File

@@ -1,45 +1,11 @@
import { fireEvent } from "../../../common/dom/fire_event"; 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__"; 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 { export interface AddAutomationElementDialogParams {
type: "trigger" | "condition" | "action"; type: "trigger" | "condition" | "action";
add: (key: string) => void; add: (key: string) => void;
clipboardItem: string | undefined; clipboardItem: string | undefined;
group?: string;
} }
const loadDialog = () => import("./add-automation-element-dialog"); const loadDialog = () => import("./add-automation-element-dialog");

View File

@@ -1,4 +1,4 @@
import { mdiDrag, mdiPlus } from "@mdi/js"; import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import type { PropertyValues } from "lit"; import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit"; import { html, LitElement, nothing } from "lit";
@@ -110,7 +110,9 @@ export default class HaAutomationTrigger extends LitElement {
@click=${stopPropagation} @click=${stopPropagation}
.index=${idx} .index=${idx}
> >
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon> <ha-svg-icon
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
</div> </div>
` `
: nothing} : nothing}

View File

@@ -244,7 +244,8 @@ class HaConfigBackupSettings extends LitElement {
` `
: nothing} : nothing}
</div> </div>
${!this.cloudStatus?.logged_in ${!this.cloudStatus?.logged_in &&
isComponentLoaded(this.hass, "cloud")
? html`<ha-card class="cloud-info"> ? html`<ha-card class="cloud-info">
<div class="cloud-header"> <div class="cloud-header">
<img <img
@@ -279,7 +280,10 @@ class HaConfigBackupSettings extends LitElement {
"ui.panel.config.voice_assistants.assistants.cloud.sign_in" "ui.panel.config.voice_assistants.assistants.cloud.sign_in"
)} )}
</ha-button> </ha-button>
<ha-button href="/config/cloud/register"> <ha-button
href="/config/cloud/register"
appearance="filled"
>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.voice_assistants.assistants.cloud.try_one_month" "ui.panel.config.voice_assistants.assistants.cloud.try_one_month"
)} )}

View File

@@ -1,4 +1,10 @@
import { mdiDelete, mdiDevices, mdiDrag, mdiPencil, mdiPlus } from "@mdi/js"; import {
mdiDelete,
mdiDevices,
mdiDragHorizontalVariant,
mdiPencil,
mdiPlus,
} from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit"; import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit"; import { css, html, LitElement } from "lit";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
@@ -89,7 +95,9 @@ export class EnergyDeviceSettings extends LitElement {
(device) => html` (device) => html`
<div class="row" .device=${device}> <div class="row" .device=${device}>
<div class="handle"> <div class="handle">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon> <ha-svg-icon
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
</div> </div>
<span class="content" <span class="content"
>${device.name || >${device.name ||

View File

@@ -1,4 +1,4 @@
import { mdiDelete, mdiDrag } from "@mdi/js"; import { mdiDelete, mdiDragHorizontalVariant } from "@mdi/js";
import type { CSSResultGroup } from "lit"; import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
@@ -111,7 +111,9 @@ class HaInputSelectForm extends LitElement {
<ha-list-item class="option" hasMeta> <ha-list-item class="option" hasMeta>
<div class="optioncontent"> <div class="optioncontent">
<div class="handle"> <div class="handle">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon> <ha-svg-icon
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
</div> </div>
${option} ${option}
</div> </div>

View File

@@ -9,7 +9,6 @@ import {
import type { PropertyValues } from "lit"; import type { PropertyValues } from "lit";
import { LitElement, html, nothing } from "lit"; import { LitElement, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import memoize from "memoize-one"; import memoize from "memoize-one";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import { storage } from "../../../../common/decorators/storage"; import { storage } from "../../../../common/decorators/storage";
@@ -62,7 +61,7 @@ type DataTableItem = Pick<
> & { > & {
default: boolean; default: boolean;
filename: string; filename: string;
iconColor?: string; type: string;
}; };
@customElement("ha-config-lovelace-dashboards") @customElement("ha-config-lovelace-dashboards")
@@ -107,6 +106,20 @@ export class HaConfigLovelaceDashboards extends LitElement {
}) })
private _activeHiddenColumns?: string[]; private _activeHiddenColumns?: string[];
@storage({
key: "lovelace-dashboards-table-grouping",
state: false,
subscribe: false,
})
private _activeGrouping?: string = "type";
@storage({
key: "lovelace-dashboards-table-collapsed",
state: false,
subscribe: false,
})
private _activeCollapsed: string[] = [];
public willUpdate() { public willUpdate() {
if (!this.hasUpdated) { if (!this.hasUpdated) {
this.hass.loadFragmentTranslation("lovelace"); this.hass.loadFragmentTranslation("lovelace");
@@ -132,15 +145,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
template: (dashboard) => template: (dashboard) =>
dashboard.icon dashboard.icon
? html` ? html`
<ha-icon <ha-icon slot="item-icon" .icon=${dashboard.icon}></ha-icon>
slot="item-icon"
.icon=${dashboard.icon}
style=${ifDefined(
dashboard.iconColor
? `color: ${dashboard.iconColor}`
: undefined
)}
></ha-icon>
` `
: nothing, : nothing,
}, },
@@ -177,6 +182,15 @@ export class HaConfigLovelaceDashboards extends LitElement {
}, },
}; };
columns.type = {
title: localize(
"ui.panel.config.lovelace.dashboards.picker.headers.type"
),
sortable: true,
groupable: true,
filterable: true,
};
columns.mode = { columns.mode = {
title: localize( title: localize(
"ui.panel.config.lovelace.dashboards.picker.headers.conf_mode" "ui.panel.config.lovelace.dashboards.picker.headers.conf_mode"
@@ -287,7 +301,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
url_path: "lovelace", url_path: "lovelace",
mode: defaultMode, mode: defaultMode,
filename: defaultMode === "yaml" ? "ui-lovelace.yaml" : "", filename: defaultMode === "yaml" ? "ui-lovelace.yaml" : "",
iconColor: "var(--primary-color)", type: this._localizeType("built_in"),
}, },
]; ];
if (isComponentLoaded(this.hass, "energy")) { if (isComponentLoaded(this.hass, "energy")) {
@@ -298,9 +312,9 @@ export class HaConfigLovelaceDashboards extends LitElement {
mode: "storage", mode: "storage",
url_path: "energy", url_path: "energy",
filename: "", filename: "",
iconColor: "var(--orange-color)",
default: false, default: false,
require_admin: false, require_admin: false,
type: this._localizeType("built_in"),
}); });
} }
@@ -312,9 +326,9 @@ export class HaConfigLovelaceDashboards extends LitElement {
mode: "storage", mode: "storage",
url_path: "light", url_path: "light",
filename: "", filename: "",
iconColor: "var(--amber-color)",
default: false, default: false,
require_admin: false, require_admin: false,
type: this._localizeType("built_in"),
}); });
} }
@@ -326,9 +340,9 @@ export class HaConfigLovelaceDashboards extends LitElement {
mode: "storage", mode: "storage",
url_path: "safety", url_path: "safety",
filename: "", filename: "",
iconColor: "var(--blue-grey-color)",
default: false, default: false,
require_admin: false, require_admin: false,
type: this._localizeType("built_in"),
}); });
} }
@@ -340,9 +354,9 @@ export class HaConfigLovelaceDashboards extends LitElement {
mode: "storage", mode: "storage",
url_path: "climate", url_path: "climate",
filename: "", filename: "",
iconColor: "var(--deep-orange-color)",
default: false, default: false,
require_admin: false, require_admin: false,
type: this._localizeType("built_in"),
}); });
} }
@@ -351,16 +365,25 @@ export class HaConfigLovelaceDashboards extends LitElement {
.sort((a, b) => .sort((a, b) =>
stringCompare(a.title, b.title, this.hass.locale.language) stringCompare(a.title, b.title, this.hass.locale.language)
) )
.map((dashboard) => ({ .map(
filename: "", (dashboard) =>
...dashboard, ({
default: defaultUrlPath === dashboard.url_path, filename: "",
})) ...dashboard,
default: defaultUrlPath === dashboard.url_path,
type: this._localizeType("user_created"),
}) satisfies DataTableItem
)
); );
return result; return result;
} }
); );
private _localizeType = (type: "user_created" | "built_in") =>
this.hass.localize(
`ui.panel.config.lovelace.dashboards.picker.type.${type}`
);
protected render() { protected render() {
if (!this.hass || this._dashboards === undefined) { if (!this.hass || this._dashboards === undefined) {
return html` <hass-loading-screen></hass-loading-screen> `; return html` <hass-loading-screen></hass-loading-screen> `;
@@ -380,9 +403,13 @@ export class HaConfigLovelaceDashboards extends LitElement {
this.hass.localize this.hass.localize
)} )}
.data=${this._getItems(this._dashboards, this.hass.defaultPanel)} .data=${this._getItems(this._dashboards, this.hass.defaultPanel)}
.initialGroupColumn=${this._activeGrouping}
.initialCollapsedGroups=${this._activeCollapsed}
.initialSorting=${this._activeSorting} .initialSorting=${this._activeSorting}
.columnOrder=${this._activeColumnOrder} .columnOrder=${this._activeColumnOrder}
.hiddenColumns=${this._activeHiddenColumns} .hiddenColumns=${this._activeHiddenColumns}
@grouping-changed=${this._handleGroupingChanged}
@collapsed-changed=${this._handleCollapseChanged}
@columns-changed=${this._handleColumnsChanged} @columns-changed=${this._handleColumnsChanged}
@sorting-changed=${this._handleSortingChanged} @sorting-changed=${this._handleSortingChanged}
.filter=${this._filter} .filter=${this._filter}
@@ -443,13 +470,13 @@ export class HaConfigLovelaceDashboards extends LitElement {
} }
private _canDelete(urlPath: string) { private _canDelete(urlPath: string) {
return !["lovelace", "energy", "light", "security", "climate"].includes( return !["lovelace", "energy", "light", "safety", "climate"].includes(
urlPath urlPath
); );
} }
private _canEdit(urlPath: string) { private _canEdit(urlPath: string) {
return !["light", "security", "climate"].includes(urlPath); return !["light", "safety", "climate"].includes(urlPath);
} }
private _handleDelete = async (item: DataTableItem) => { private _handleDelete = async (item: DataTableItem) => {
@@ -571,6 +598,14 @@ export class HaConfigLovelaceDashboards extends LitElement {
this._activeColumnOrder = ev.detail.columnOrder; this._activeColumnOrder = ev.detail.columnOrder;
this._activeHiddenColumns = ev.detail.hiddenColumns; this._activeHiddenColumns = ev.detail.hiddenColumns;
} }
private _handleGroupingChanged(ev: CustomEvent) {
this._activeGrouping = ev.detail.value;
}
private _handleCollapseChanged(ev: CustomEvent) {
this._activeCollapsed = ev.detail.value;
}
} }
declare global { declare global {

View File

@@ -11,7 +11,6 @@ import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { stateActive } from "../../../common/entity/state_active"; import { stateActive } from "../../../common/entity/state_active";
import { stateColorCss } from "../../../common/entity/state_color"; import { stateColorCss } from "../../../common/entity/state_color";
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
import "../../../components/ha-badge"; import "../../../components/ha-badge";
import "../../../components/ha-ripple"; import "../../../components/ha-ripple";
import "../../../components/ha-state-icon"; import "../../../components/ha-state-icon";
@@ -20,6 +19,7 @@ import { cameraUrlWithWidthHeight } from "../../../data/camera";
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive"; import { actionHandler } from "../common/directives/action-handler-directive";
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
import { findEntities } from "../common/find-entities"; import { findEntities } from "../common/find-entities";
import { handleAction } from "../common/handle-action"; import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action"; import { hasAction } from "../common/has-action";
@@ -162,11 +162,7 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
if (!stateObj) { if (!stateObj) {
return html` return html`
<ha-badge .label=${entityId} class="error"> <ha-badge .label=${entityId} class="error">
<ha-svg-icon <ha-svg-icon slot="icon" .path=${mdiAlertCircle}></ha-svg-icon>
slot="icon"
.hass=${this.hass}
.path=${mdiAlertCircle}
></ha-svg-icon>
${this.hass.localize("ui.badge.entity.not_found")} ${this.hass.localize("ui.badge.entity.not_found")}
</ha-badge> </ha-badge>
`; `;
@@ -179,22 +175,22 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
"--badge-color": color, "--badge-color": color,
}; };
const stateDisplay = html`
<state-display
.stateObj=${stateObj}
.hass=${this.hass}
.content=${this._config.state_content}
.name=${this._config.name}
>
</state-display>
`;
const name = computeLovelaceEntityName( const name = computeLovelaceEntityName(
this.hass, this.hass,
stateObj, stateObj,
this._config.name this._config.name
); );
const stateDisplay = html`
<state-display
.stateObj=${stateObj}
.hass=${this.hass}
.content=${this._config.state_content}
.name=${name}
>
</state-display>
`;
const showState = this._config.show_state; const showState = this._config.show_state;
const showName = this._config.show_name; const showName = this._config.show_name;
const showIcon = this._config.show_icon; const showIcon = this._config.show_icon;

View File

@@ -5,7 +5,7 @@ import {
mdiDelete, mdiDelete,
mdiDeleteSweep, mdiDeleteSweep,
mdiDotsVertical, mdiDotsVertical,
mdiDrag, mdiDragHorizontalVariant,
mdiPlus, mdiPlus,
mdiSort, mdiSort,
} from "@mdi/js"; } from "@mdi/js";
@@ -522,7 +522,7 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
"ui.panel.lovelace.cards.todo-list.drag_and_drop" "ui.panel.lovelace.cards.todo-list.drag_and_drop"
)} )}
class="reorderButton handle" class="reorderButton handle"
.path=${mdiDrag} .path=${mdiDragHorizontalVariant}
slot="meta" slot="meta"
> >
</ha-svg-icon> </ha-svg-icon>

View File

@@ -1,4 +1,4 @@
import { mdiClose, mdiDrag, mdiPencil } from "@mdi/js"; import { mdiClose, mdiDragHorizontalVariant, mdiPencil } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
@@ -66,7 +66,11 @@ export class HuiEntityEditor extends LitElement {
return html` return html`
<ha-md-list-item class="item"> <ha-md-list-item class="item">
<ha-svg-icon class="handle" .path=${mdiDrag} slot="start"></ha-svg-icon> <ha-svg-icon
class="handle"
.path=${mdiDragHorizontalVariant}
slot="start"
></ha-svg-icon>
<div slot="headline" class="label">${primary}</div> <div slot="headline" class="label">${primary}</div>
${secondary ${secondary
@@ -152,7 +156,9 @@ export class HuiEntityEditor extends LitElement {
(entityConf, index) => html` (entityConf, index) => html`
<div class="entity" data-entity-id=${entityConf.entity}> <div class="entity" data-entity-id=${entityConf.entity}>
<div class="handle"> <div class="handle">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon> <ha-svg-icon
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
</div> </div>
<ha-entity-picker <ha-entity-picker
.hass=${this.hass} .hass=${this.hass}

View File

@@ -1,4 +1,4 @@
import { mdiDelete, mdiDrag, mdiPencil } from "@mdi/js"; import { mdiDelete, mdiDragHorizontalVariant, mdiPencil } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit"; import type { CSSResultGroup, TemplateResult } from "lit";
import { LitElement, css, html } from "lit"; import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
@@ -31,7 +31,7 @@ export class HuiSectionEditMode extends LitElement {
<ha-svg-icon <ha-svg-icon
aria-hidden="true" aria-hidden="true"
class="handle" class="handle"
.path=${mdiDrag} .path=${mdiDragHorizontalVariant}
></ha-svg-icon> ></ha-svg-icon>
<ha-icon-button <ha-icon-button
.label=${this.hass.localize("ui.common.edit")} .label=${this.hass.localize("ui.common.edit")}

View File

@@ -1,4 +1,9 @@
import { mdiDelete, mdiDrag, mdiPencil, mdiPlus } from "@mdi/js"; import {
mdiDelete,
mdiDragHorizontalVariant,
mdiPencil,
mdiPlus,
} from "@mdi/js";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
@@ -345,7 +350,9 @@ export class HuiCardFeaturesEditor extends LitElement {
return html` return html`
<div class="feature"> <div class="feature">
<div class="handle"> <div class="handle">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon> <ha-svg-icon
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
</div> </div>
<div class="feature-content"> <div class="feature-content">
<div> <div>

View File

@@ -1,5 +1,10 @@
import "@material/mwc-menu/mwc-menu-surface"; import "@material/mwc-menu/mwc-menu-surface";
import { mdiDelete, mdiDrag, mdiPencil, mdiPlus } from "@mdi/js"; import {
mdiDelete,
mdiDragHorizontalVariant,
mdiPencil,
mdiPlus,
} from "@mdi/js";
import type { ComboBoxLightOpenedChangedEvent } from "@vaadin/combo-box/vaadin-combo-box-light"; import type { ComboBoxLightOpenedChangedEvent } from "@vaadin/combo-box/vaadin-combo-box-light";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
@@ -86,7 +91,9 @@ export class HuiHeadingBadgesEditor extends LitElement {
return html` return html`
<div class="badge"> <div class="badge">
<div class="handle"> <div class="handle">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon> <ha-svg-icon
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
</div> </div>
<div class="badge-content"> <div class="badge-content">
<span>${label}</span> <span>${label}</span>

View File

@@ -1,4 +1,4 @@
import { mdiClose, mdiDrag, mdiPencil } from "@mdi/js"; import { mdiClose, mdiDragHorizontalVariant, mdiPencil } from "@mdi/js";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
@@ -59,7 +59,7 @@ export class HuiEntitiesCardRowEditor extends LitElement {
(entityConf, index) => html` (entityConf, index) => html`
<div class="entity"> <div class="entity">
<div class="handle"> <div class="handle">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon> <ha-svg-icon .path=${mdiDragHorizontalVariant}></ha-svg-icon>
</div> </div>
${entityConf.type ${entityConf.type
? html` ? html`

View File

@@ -72,8 +72,7 @@ import {
} from "../../dialogs/quick-bar/show-dialog-quick-bar"; } from "../../dialogs/quick-bar/show-dialog-quick-bar";
import { showShortcutsDialog } from "../../dialogs/shortcuts/show-shortcuts-dialog"; import { showShortcutsDialog } from "../../dialogs/shortcuts/show-shortcuts-dialog";
import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog"; import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
import { ViewTransitionMixin } from "../../mixins/view-transition-mixin"; import { haStyle } from "../../resources/styles";
import { haStyle, haStyleViewTransitions } from "../../resources/styles";
import type { HomeAssistant, PanelInfo } from "../../types"; import type { HomeAssistant, PanelInfo } from "../../types";
import { documentationUrl } from "../../util/documentation-url"; import { documentationUrl } from "../../util/documentation-url";
import { showToast } from "../../util/toast"; import { showToast } from "../../util/toast";
@@ -115,7 +114,7 @@ interface SubActionItem {
} }
@customElement("hui-root") @customElement("hui-root")
class HUIRoot extends ViewTransitionMixin(LitElement) { class HUIRoot extends LitElement {
@property({ attribute: false }) public panel?: PanelInfo<LovelacePanelConfig>; @property({ attribute: false }) public panel?: PanelInfo<LovelacePanelConfig>;
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -131,8 +130,6 @@ class HUIRoot extends ViewTransitionMixin(LitElement) {
@state() private _curView?: number | "hass-unused-entities"; @state() private _curView?: number | "hass-unused-entities";
@state() private _loaded = false;
private _viewCache?: Record<string, HUIView>; private _viewCache?: Record<string, HUIView>;
private _viewScrollPositions: Record<string, number> = {}; private _viewScrollPositions: Record<string, number> = {};
@@ -156,10 +153,6 @@ class HUIRoot extends ViewTransitionMixin(LitElement) {
); );
} }
protected onLoadTransition(): void {
this._loaded = true;
}
private _renderActionItems(): TemplateResult { private _renderActionItems(): TemplateResult {
const result: TemplateResult[] = []; const result: TemplateResult[] = [];
if (this._editMode) { if (this._editMode) {
@@ -500,7 +493,6 @@ class HUIRoot extends ViewTransitionMixin(LitElement) {
class=${classMap({ class=${classMap({
"edit-mode": this._editMode, "edit-mode": this._editMode,
narrow: this.narrow, narrow: this.narrow,
loading: !this._loaded,
})} })}
> >
<div class="header"> <div class="header">
@@ -1170,45 +1162,43 @@ class HUIRoot extends ViewTransitionMixin(LitElement) {
// Recreate a new element to clear the applied themes. // Recreate a new element to clear the applied themes.
const root = this._viewRoot; const root = this._viewRoot;
this.startViewTransition(() => { if (root.lastChild) {
if (root.lastChild) { root.removeChild(root.lastChild);
root.removeChild(root.lastChild); }
}
if (viewIndex === "hass-unused-entities") { if (viewIndex === "hass-unused-entities") {
const unusedEntities = document.createElement("hui-unused-entities"); const unusedEntities = document.createElement("hui-unused-entities");
// Wait for promise to resolve so that the element has been upgraded. // Wait for promise to resolve so that the element has been upgraded.
import("./editor/unused-entities/hui-unused-entities").then(() => { import("./editor/unused-entities/hui-unused-entities").then(() => {
unusedEntities.hass = this.hass!; unusedEntities.hass = this.hass!;
unusedEntities.lovelace = this.lovelace!; unusedEntities.lovelace = this.lovelace!;
unusedEntities.narrow = this.narrow; unusedEntities.narrow = this.narrow;
}); });
root.appendChild(unusedEntities); root.appendChild(unusedEntities);
return; return;
} }
let view; let view;
const viewConfig = this.config.views[viewIndex]; const viewConfig = this.config.views[viewIndex];
if (!viewConfig) { if (!viewConfig) {
this.lovelace!.setEditMode(true); this.lovelace!.setEditMode(true);
return; return;
} }
if (!force && this._viewCache![viewIndex]) { if (!force && this._viewCache![viewIndex]) {
view = this._viewCache![viewIndex]; view = this._viewCache![viewIndex];
} else { } else {
view = document.createElement("hui-view"); view = document.createElement("hui-view");
view.index = viewIndex; view.index = viewIndex;
this._viewCache![viewIndex] = view; this._viewCache![viewIndex] = view;
} }
view.lovelace = this.lovelace; view.lovelace = this.lovelace;
view.hass = this.hass; view.hass = this.hass;
view.narrow = this.narrow; view.narrow = this.narrow;
root.appendChild(view); root.appendChild(view);
});
} }
private _openShortcutDialog(ev: Event) { private _openShortcutDialog(ev: Event) {
@@ -1219,21 +1209,12 @@ class HUIRoot extends ViewTransitionMixin(LitElement) {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,
haStyleViewTransitions,
css` css`
:host { :host {
-ms-user-select: none; -ms-user-select: none;
-webkit-user-select: none; -webkit-user-select: none;
-moz-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 { .header {
background-color: var(--app-header-background-color); background-color: var(--app-header-background-color);
color: var(--app-header-text-color, white); color: var(--app-header-text-color, white);
@@ -1422,10 +1403,6 @@ class HUIRoot extends ViewTransitionMixin(LitElement) {
padding-right: var(--safe-area-inset-right); padding-right: var(--safe-area-inset-right);
padding-inline-end: var(--safe-area-inset-right); padding-inline-end: var(--safe-area-inset-right);
padding-bottom: var(--safe-area-inset-bottom); padding-bottom: var(--safe-area-inset-bottom);
view-transition-name: hui-root-container;
}
.loading hui-view-container {
opacity: 0;
} }
.narrow hui-view-container { .narrow hui-view-container {
padding-left: var(--safe-area-inset-left); padding-left: var(--safe-area-inset-left);
@@ -1434,7 +1411,6 @@ class HUIRoot extends ViewTransitionMixin(LitElement) {
hui-view-container > * { hui-view-container > * {
flex: 1 1 100%; flex: 1 1 100%;
max-width: 100%; max-width: 100%;
view-transition-name: layout-fade-in;
} }
/** /**
* In edit mode we have the tab bar on a new line * * In edit mode we have the tab bar on a new line *

View File

@@ -1,8 +1,12 @@
import { css, LitElement, nothing } from "lit"; import { css, LitElement, nothing } from "lit";
import type { PropertyValues } from "lit"; import type { PropertyValues } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import type { LovelaceViewBackgroundConfig } from "../../../data/lovelace/config/view"; import type { LovelaceViewBackgroundConfig } from "../../../data/lovelace/config/view";
import {
isMediaSourceContentId,
resolveMediaSource,
} from "../../../data/media_source";
@customElement("hui-view-background") @customElement("hui-view-background")
export class HUIViewBackground extends LitElement { export class HUIViewBackground extends LitElement {
@@ -13,10 +17,27 @@ export class HUIViewBackground extends LitElement {
| LovelaceViewBackgroundConfig | LovelaceViewBackgroundConfig
| undefined; | undefined;
@state({ attribute: false }) resolvedImage?: string;
protected render() { protected render() {
return nothing; return nothing;
} }
private _fetchMedia() {
const backgroundImage =
typeof this.background === "string"
? this.background
: this.background?.image;
if (backgroundImage && isMediaSourceContentId(backgroundImage)) {
resolveMediaSource(this.hass, backgroundImage).then((result) => {
this.resolvedImage = result.url;
});
} else {
this.resolvedImage = undefined;
}
}
private _applyTheme() { private _applyTheme() {
const computedStyles = getComputedStyle(this); const computedStyles = getComputedStyle(this);
const themeBackground = computedStyles.getPropertyValue( const themeBackground = computedStyles.getPropertyValue(
@@ -52,13 +73,19 @@ export class HUIViewBackground extends LitElement {
background?: string | LovelaceViewBackgroundConfig background?: string | LovelaceViewBackgroundConfig
) { ) {
if (typeof background === "object" && background.image) { if (typeof background === "object" && background.image) {
if (isMediaSourceContentId(background.image) && !this.resolvedImage) {
return null;
}
const alignment = background.alignment ?? "center"; const alignment = background.alignment ?? "center";
const size = background.size ?? "cover"; const size = background.size ?? "cover";
const repeat = background.repeat ?? "no-repeat"; const repeat = background.repeat ?? "no-repeat";
return `${alignment} / ${size} ${repeat} url('${this.hass.hassUrl(background.image)}')`; return `${alignment} / ${size} ${repeat} url('${this.hass.hassUrl(this.resolvedImage || background.image)}')`;
} }
if (typeof background === "string") { if (typeof background === "string") {
return background; if (isMediaSourceContentId(background) && !this.resolvedImage) {
return null;
}
return this.resolvedImage || background;
} }
return null; return null;
} }
@@ -90,6 +117,10 @@ export class HUIViewBackground extends LitElement {
if (changedProperties.has("background")) { if (changedProperties.has("background")) {
this._applyTheme(); this._applyTheme();
this._fetchMedia();
}
if (changedProperties.has("resolvedImage")) {
this._applyTheme();
} }
} }

View File

@@ -199,56 +199,3 @@ export const baseEntrypointStyles = css`
width: 100vw; 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

@@ -155,7 +155,6 @@ export const semanticColorStyles = css`
/* Surfaces */ /* Surfaces */
--ha-color-surface-default: var(--ha-color-neutral-95); --ha-color-surface-default: var(--ha-color-neutral-95);
--ha-color-on-surface-default: var(--ha-color-neutral-05);
} }
`; `;
@@ -287,6 +286,5 @@ export const darkSemanticColorStyles = css`
/* Surfaces */ /* Surfaces */
--ha-color-surface-default: var(--ha-color-neutral-10); --ha-color-surface-default: var(--ha-color-neutral-10);
--ha-color-on-surface-default: var(--ha-color-neutral-95);
} }
`; `;

View File

@@ -42,17 +42,6 @@ export const coreStyles = css`
--ha-space-18: 72px; --ha-space-18: 72px;
--ha-space-19: 76px; --ha-space-19: 76px;
--ha-space-20: 80px; --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

@@ -3455,12 +3455,17 @@
"require_admin": "Admin only", "require_admin": "Admin only",
"sidebar": "In sidebar", "sidebar": "In sidebar",
"filename": "Filename", "filename": "Filename",
"url": "Open" "url": "Open",
"type": "Type"
}, },
"open": "Open", "open": "Open",
"edit": "Edit", "edit": "Edit",
"delete": "Delete", "delete": "Delete",
"add_dashboard": "Add dashboard" "add_dashboard": "Add dashboard",
"type": {
"user_created": "User created",
"built_in": "Built-in"
}
}, },
"confirm_delete_title": "Delete {dashboard_title}?", "confirm_delete_title": "Delete {dashboard_title}?",
"confirm_delete_text": "This dashboard will be permanently deleted.", "confirm_delete_text": "This dashboard will be permanently deleted.",
@@ -3910,7 +3915,6 @@
"edit_yaml": "Edit in YAML", "edit_yaml": "Edit in YAML",
"edit_ui": "Edit in visual editor", "edit_ui": "Edit in visual editor",
"copy_to_clipboard": "Copy to clipboard", "copy_to_clipboard": "Copy to clipboard",
"search_in": "Search · {group}",
"unknown_entity": "unknown entity", "unknown_entity": "unknown entity",
"edit_unknown_device": "Editor not available for unknown device", "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.", "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.",
@@ -3923,6 +3927,7 @@
"item_pasted": "{item} pasted", "item_pasted": "{item} pasted",
"ctrl": "Ctrl", "ctrl": "Ctrl",
"del": "Del", "del": "Del",
"blocks": "Blocks",
"triggers": { "triggers": {
"name": "Triggers", "name": "Triggers",
"header": "When", "header": "When",
@@ -3930,7 +3935,7 @@
"learn_more": "Learn more about triggers", "learn_more": "Learn more about triggers",
"triggered": "Triggered", "triggered": "Triggered",
"add": "Add trigger", "add": "Add trigger",
"search": "Search trigger", "empty_search": "No triggers found for {term}",
"id": "Trigger ID", "id": "Trigger ID",
"id_helper": "Helps identify each run based on which trigger fired.", "id_helper": "Helps identify each run based on which trigger fired.",
"optional": "Optional", "optional": "Optional",
@@ -3951,14 +3956,16 @@
"trigger": "Trigger", "trigger": "Trigger",
"copied_to_clipboard": "Trigger copied to clipboard", "copied_to_clipboard": "Trigger copied to clipboard",
"cut_to_clipboard": "Trigger cut to clipboard", "cut_to_clipboard": "Trigger cut to clipboard",
"select": "Select a trigger",
"groups": { "groups": {
"device": {
"label": "Device"
},
"entity": { "entity": {
"label": "Entity", "label": "Entity"
"description": "When something happens to an entity."
}, },
"time_location": { "time_location": {
"label": "Time and location", "label": "Time and location"
"description": "When someone enters or leaves a zone, or at a specific time."
}, },
"other": { "other": {
"label": "Other triggers" "label": "Other triggers"
@@ -4191,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.", "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", "learn_more": "Learn more about conditions",
"add": "Add condition", "add": "Add condition",
"search": "Search condition", "empty_search": "No conditions and blocks found for {term}",
"add_building_block": "Add building block", "add_building_block": "Add building block",
"test": "Test", "test": "Test",
"testing_error": "Condition did not pass", "testing_error": "Condition did not pass",
@@ -4213,21 +4220,22 @@
"condition": "Condition", "condition": "Condition",
"copied_to_clipboard": "Condition copied to clipboard", "copied_to_clipboard": "Condition copied to clipboard",
"cut_to_clipboard": "Condition cut to clipboard", "cut_to_clipboard": "Condition cut to clipboard",
"select": "Select a condition",
"groups": { "groups": {
"device": {
"label": "Device"
},
"entity": { "entity": {
"label": "Entity", "label": "Entity"
"description": "If an entity is in a specific state."
}, },
"time_location": { "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": { "other": {
"label": "Other conditions" "label": "Other conditions"
}, },
"building_blocks": { "building_blocks": {
"label": "Building blocks", "label": "Building blocks"
"description": "Build more complex conditions."
} }
}, },
"type": { "type": {
@@ -4358,7 +4366,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.", "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", "learn_more": "Learn more about actions",
"add": "Add action", "add": "Add action",
"search": "Search action", "empty_search": "No actions and blocks found for {term}",
"add_building_block": "Add building block", "add_building_block": "Add building block",
"invalid_action": "Invalid action", "invalid_action": "Invalid action",
"run": "Run action", "run": "Run action",
@@ -4382,7 +4390,11 @@
"action": "Action", "action": "Action",
"copied_to_clipboard": "Action copied to clipboard", "copied_to_clipboard": "Action copied to clipboard",
"cut_to_clipboard": "Action cut to clipboard", "cut_to_clipboard": "Action cut to clipboard",
"select": "Select an action",
"groups": { "groups": {
"device_id": {
"label": "Device"
},
"helpers": { "helpers": {
"label": "Helpers" "label": "Helpers"
}, },
@@ -4390,8 +4402,7 @@
"label": "Other actions" "label": "Other actions"
}, },
"building_blocks": { "building_blocks": {
"label": "Building blocks", "label": "Building blocks"
"description": "Build more complex sequences of actions."
} }
}, },
"type": { "type": {

View File

@@ -3,20 +3,7 @@ import { render } from "lit";
export const removeLaunchScreen = () => { export const removeLaunchScreen = () => {
const launchScreenElement = document.getElementById("ha-launch-screen"); const launchScreenElement = document.getElementById("ha-launch-screen");
if (!launchScreenElement) { if (launchScreenElement) {
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); launchScreenElement.parentElement!.removeChild(launchScreenElement);
} }
}; };

View File

@@ -1284,15 +1284,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@codemirror/view@npm:6.38.5, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0": "@codemirror/view@npm:6.38.6, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0":
version: 6.38.5 version: 6.38.6
resolution: "@codemirror/view@npm:6.38.5" resolution: "@codemirror/view@npm:6.38.6"
dependencies: dependencies:
"@codemirror/state": "npm:^6.5.0" "@codemirror/state": "npm:^6.5.0"
crelt: "npm:^1.0.6" crelt: "npm:^1.0.6"
style-mod: "npm:^4.1.0" style-mod: "npm:^4.1.0"
w3c-keyname: "npm:^2.2.4" w3c-keyname: "npm:^2.2.4"
checksum: 10/2335b593770042eb3adfe369073432b07cd2d15f1e230ae4dc7be7a7b8edd74e57c13e59b92a11e7e5d59ae030aabf7f55478dfec1cf2a2fe3a1ef3f091676a4 checksum: 10/5a047337a98de111817ce8c8d39e6429c90ca0b0a4d2678d6e161e9e5961b1d476a891f447ab7a05cac395d4a93530e7c68bedd93191285265f0742a308ad00b
languageName: node languageName: node
linkType: hard linkType: hard
@@ -1942,9 +1942,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@home-assistant/webawesome@npm:3.0.0-beta.6.ha.4": "@home-assistant/webawesome@npm:3.0.0-beta.6.ha.5":
version: 3.0.0-beta.6.ha.4 version: 3.0.0-beta.6.ha.5
resolution: "@home-assistant/webawesome@npm:3.0.0-beta.6.ha.4" resolution: "@home-assistant/webawesome@npm:3.0.0-beta.6.ha.5"
dependencies: dependencies:
"@ctrl/tinycolor": "npm:4.1.0" "@ctrl/tinycolor": "npm:4.1.0"
"@floating-ui/dom": "npm:^1.6.13" "@floating-ui/dom": "npm:^1.6.13"
@@ -1955,7 +1955,7 @@ __metadata:
lit: "npm:^3.2.1" lit: "npm:^3.2.1"
nanoid: "npm:^5.1.5" nanoid: "npm:^5.1.5"
qr-creator: "npm:^1.0.0" qr-creator: "npm:^1.0.0"
checksum: 10/d9072b321126ef458468ed2cf040e0b04cb2aff73336c6e742c0cfb25d9fb674b7672e7c9abcf5bcb0aa0b2fe953c20186f0910f485024c827bfe4cf399f10a4 checksum: 10/6bfa5e06b91df06402c348bc19ec59a7fe6ed70080989d60a3c6519f99f5dc72da8b42c5dc2cad9d1ab211c51c4c67a74c0e22f21368da3c9f2565cbf8646a90
languageName: node languageName: node
linkType: hard linkType: hard
@@ -9255,7 +9255,7 @@ __metadata:
"@codemirror/legacy-modes": "npm:6.5.2" "@codemirror/legacy-modes": "npm:6.5.2"
"@codemirror/search": "npm:6.5.11" "@codemirror/search": "npm:6.5.11"
"@codemirror/state": "npm:6.5.2" "@codemirror/state": "npm:6.5.2"
"@codemirror/view": "npm:6.38.5" "@codemirror/view": "npm:6.38.6"
"@date-fns/tz": "npm:1.4.1" "@date-fns/tz": "npm:1.4.1"
"@egjs/hammerjs": "npm:2.0.17" "@egjs/hammerjs": "npm:2.0.17"
"@formatjs/intl-datetimeformat": "npm:6.18.2" "@formatjs/intl-datetimeformat": "npm:6.18.2"
@@ -9273,7 +9273,7 @@ __metadata:
"@fullcalendar/list": "npm:6.1.19" "@fullcalendar/list": "npm:6.1.19"
"@fullcalendar/luxon3": "npm:6.1.19" "@fullcalendar/luxon3": "npm:6.1.19"
"@fullcalendar/timegrid": "npm:6.1.19" "@fullcalendar/timegrid": "npm:6.1.19"
"@home-assistant/webawesome": "npm:3.0.0-beta.6.ha.4" "@home-assistant/webawesome": "npm:3.0.0-beta.6.ha.5"
"@lezer/highlight": "npm:1.2.1" "@lezer/highlight": "npm:1.2.1"
"@lit-labs/motion": "npm:1.0.9" "@lit-labs/motion": "npm:1.0.9"
"@lit-labs/observers": "npm:2.0.6" "@lit-labs/observers": "npm:2.0.6"