mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-13 13:00:24 +00:00
Compare commits
19 Commits
ha-wa-dial
...
copilot/al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84c8420286 | ||
|
|
1361fc36bf | ||
|
|
505ef2bd11 | ||
|
|
c0cc66c1ab | ||
|
|
7cfbc521c7 | ||
|
|
e064ce56cc | ||
|
|
8d688aa3a9 | ||
|
|
d122483449 | ||
|
|
f17bbc3f79 | ||
|
|
c88f8fcce0 | ||
|
|
8efabde916 | ||
|
|
e821e1ec83 | ||
|
|
dc7516da94 | ||
|
|
a545a377a7 | ||
|
|
3634dbcbbf | ||
|
|
75af4f939e | ||
|
|
453a2ac7f3 | ||
|
|
8fbd0226fc | ||
|
|
2a8d935601 |
@@ -5,14 +5,14 @@ subtitle: Dialogs provide important prompts in a user flow.
|
|||||||
|
|
||||||
# Material Design 3
|
# Material Design 3
|
||||||
|
|
||||||
Our dialogs are based on the latest version of Material Design. Please note that we have made some well-considered adjustments to these guideliness. Specs and guidelines can be found on its [website](https://m3.material.io/components/dialogs/overview).
|
Our dialogs are based on the latest version of Material Design. Please note that we have made some well-considered adjustments to these guidelines. Specs and guidelines can be found on its [website](https://m3.material.io/components/dialogs/overview).
|
||||||
|
|
||||||
# Guidelines
|
# Guidelines
|
||||||
|
|
||||||
## Design
|
## Design
|
||||||
|
|
||||||
- Dialogs have a max width of 560px. Alert and confirmation dialogs got a fixed width of 320px. If you need more width, consider a dedicated page instead.
|
- Dialogs have a max width of 560px. Alert and confirmation dialogs have a fixed width of 320px. If you need more width, consider a dedicated page instead.
|
||||||
- The close X-icon is on the top left, on all screen sizes. Except for alert and confirmation dialogs, they only have buttons and no X-icon. This is different compared to the Material guideliness.
|
- The close X-icon is on the top left, on all screen sizes. Except for alert and confirmation dialogs, they only have buttons and no X-icon. This is different compared to the Material guidelines.
|
||||||
- Dialogs can't be closed with ESC or clicked outside of the dialog when there is a form that the user needs to fill out. Instead it will animate "no" by a little shake.
|
- Dialogs can't be closed with ESC or clicked outside of the dialog when there is a form that the user needs to fill out. Instead it will animate "no" by a little shake.
|
||||||
- Extra icon buttons are on the top right, for example help, settings and expand dialog. More than 2 icon buttons, they will be in an overflow menu.
|
- Extra icon buttons are on the top right, for example help, settings and expand dialog. More than 2 icon buttons, they will be in an overflow menu.
|
||||||
- The submit button is grouped with a cancel button at the bottom right, on all screen sizes. Fullscreen mobile dialogs have them sticky at the bottom.
|
- The submit button is grouped with a cancel button at the bottom right, on all screen sizes. Fullscreen mobile dialogs have them sticky at the bottom.
|
||||||
@@ -26,7 +26,7 @@ Our dialogs are based on the latest version of Material Design. Please note that
|
|||||||
|
|
||||||
- A best practice is to always use a title, even if it is optional by Material guidelines.
|
- A best practice is to always use a title, even if it is optional by Material guidelines.
|
||||||
- People mainly read the title and a button. Put the most important information in those two.
|
- People mainly read the title and a button. Put the most important information in those two.
|
||||||
- Try to avoid user generated content in the title, this could make the title unreadable long.
|
- Try to avoid user generated content in the title, this could make the title unreadably long.
|
||||||
- If users become unsure, they read the description. Make sure this explains what will happen.
|
- If users become unsure, they read the description. Make sure this explains what will happen.
|
||||||
- Strive for minimalism.
|
- Strive for minimalism.
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
"@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.6",
|
"@home-assistant/webawesome": "3.0.0-beta.6.ha.6",
|
||||||
"@lezer/highlight": "1.2.2",
|
"@lezer/highlight": "1.2.3",
|
||||||
"@lit-labs/motion": "1.0.9",
|
"@lit-labs/motion": "1.0.9",
|
||||||
"@lit-labs/observers": "2.0.6",
|
"@lit-labs/observers": "2.0.6",
|
||||||
"@lit-labs/virtualizer": "2.1.1",
|
"@lit-labs/virtualizer": "2.1.1",
|
||||||
@@ -178,7 +178,7 @@
|
|||||||
"@types/tar": "6.1.13",
|
"@types/tar": "6.1.13",
|
||||||
"@types/ua-parser-js": "0.7.39",
|
"@types/ua-parser-js": "0.7.39",
|
||||||
"@types/webspeechapi": "0.0.29",
|
"@types/webspeechapi": "0.0.29",
|
||||||
"@vitest/coverage-v8": "4.0.2",
|
"@vitest/coverage-v8": "4.0.3",
|
||||||
"babel-loader": "10.0.0",
|
"babel-loader": "10.0.0",
|
||||||
"babel-plugin-template-html-minifier": "4.1.0",
|
"babel-plugin-template-html-minifier": "4.1.0",
|
||||||
"browserslist-useragent-regexp": "4.1.3",
|
"browserslist-useragent-regexp": "4.1.3",
|
||||||
@@ -219,7 +219,7 @@
|
|||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"typescript-eslint": "8.46.2",
|
"typescript-eslint": "8.46.2",
|
||||||
"vite-tsconfig-paths": "5.1.4",
|
"vite-tsconfig-paths": "5.1.4",
|
||||||
"vitest": "4.0.2",
|
"vitest": "4.0.3",
|
||||||
"webpack-stats-plugin": "1.1.3",
|
"webpack-stats-plugin": "1.1.3",
|
||||||
"webpackbar": "7.0.0",
|
"webpackbar": "7.0.0",
|
||||||
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
||||||
|
|||||||
116
src/common/util/swipe-gesture-recognizer.ts
Normal file
116
src/common/util/swipe-gesture-recognizer.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
export interface SwipeGestureResult {
|
||||||
|
velocity: number;
|
||||||
|
delta: number;
|
||||||
|
isSwipe: boolean;
|
||||||
|
isDownwardSwipe: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SwipeGestureConfig {
|
||||||
|
velocitySwipeThreshold?: number;
|
||||||
|
movementTimeThreshold?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VELOCITY_SWIPE_THRESHOLD = 0.5; // px/ms
|
||||||
|
const MOVEMENT_TIME_THRESHOLD = 100; // ms
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recognizes swipe gestures and calculates velocity for touch interactions.
|
||||||
|
* Tracks touch movement and provides velocity-based and position-based gesture detection.
|
||||||
|
*/
|
||||||
|
export class SwipeGestureRecognizer {
|
||||||
|
private _startY = 0;
|
||||||
|
|
||||||
|
private _delta = 0;
|
||||||
|
|
||||||
|
private _startTime = 0;
|
||||||
|
|
||||||
|
private _lastY = 0;
|
||||||
|
|
||||||
|
private _lastTime = 0;
|
||||||
|
|
||||||
|
private _velocityThreshold: number;
|
||||||
|
|
||||||
|
private _movementTimeThreshold: number;
|
||||||
|
|
||||||
|
constructor(config: SwipeGestureConfig = {}) {
|
||||||
|
this._velocityThreshold =
|
||||||
|
config.velocitySwipeThreshold ?? VELOCITY_SWIPE_THRESHOLD; // px/ms
|
||||||
|
this._movementTimeThreshold =
|
||||||
|
config.movementTimeThreshold ?? MOVEMENT_TIME_THRESHOLD; // ms
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize gesture tracking with starting touch position
|
||||||
|
*/
|
||||||
|
public start(clientY: number): void {
|
||||||
|
const now = Date.now();
|
||||||
|
this._startY = clientY;
|
||||||
|
this._startTime = now;
|
||||||
|
this._lastY = clientY;
|
||||||
|
this._lastTime = now;
|
||||||
|
this._delta = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update gesture state during movement
|
||||||
|
* Returns the current delta (negative when dragging down)
|
||||||
|
*/
|
||||||
|
public move(clientY: number): number {
|
||||||
|
const now = Date.now();
|
||||||
|
this._delta = this._startY - clientY;
|
||||||
|
this._lastY = clientY;
|
||||||
|
this._lastTime = now;
|
||||||
|
return this._delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate final gesture result when touch ends
|
||||||
|
*/
|
||||||
|
public end(): SwipeGestureResult {
|
||||||
|
const velocity = this.getVelocity();
|
||||||
|
const hasSignificantVelocity = Math.abs(velocity) > this._velocityThreshold;
|
||||||
|
|
||||||
|
return {
|
||||||
|
velocity,
|
||||||
|
delta: this._delta,
|
||||||
|
isSwipe: hasSignificantVelocity,
|
||||||
|
isDownwardSwipe: velocity > 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current drag delta (negative when dragging down)
|
||||||
|
*/
|
||||||
|
public getDelta(): number {
|
||||||
|
return this._delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate velocity based on recent movement
|
||||||
|
* Returns 0 if no recent movement detected
|
||||||
|
* Positive velocity means downward swipe
|
||||||
|
*/
|
||||||
|
public getVelocity(): number {
|
||||||
|
const now = Date.now();
|
||||||
|
const timeSinceLastMove = now - this._lastTime;
|
||||||
|
|
||||||
|
// Only consider velocity if the last movement was recent
|
||||||
|
if (timeSinceLastMove >= this._movementTimeThreshold) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeDelta = this._lastTime - this._startTime;
|
||||||
|
return timeDelta > 0 ? (this._lastY - this._startY) / timeDelta : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset all tracking state
|
||||||
|
*/
|
||||||
|
public reset(): void {
|
||||||
|
this._startY = 0;
|
||||||
|
this._delta = 0;
|
||||||
|
this._startTime = 0;
|
||||||
|
this._lastY = 0;
|
||||||
|
this._lastTime = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/components/entity/const.ts
Normal file
1
src/components/entity/const.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const ANY_STATE_VALUE = "__ANY_STATE_IGNORE_ATTRIBUTES__";
|
||||||
@@ -4,6 +4,7 @@ import { customElement, property } from "lit/decorators";
|
|||||||
import { keyed } from "lit/directives/keyed";
|
import { keyed } from "lit/directives/keyed";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import { ANY_STATE_VALUE } from "./const";
|
||||||
import { ensureArray } from "../../common/array/ensure-array";
|
import { ensureArray } from "../../common/array/ensure-array";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import "./ha-entity-state-picker";
|
import "./ha-entity-state-picker";
|
||||||
@@ -57,6 +58,7 @@ export class HaEntityStatesPicker extends LitElement {
|
|||||||
|
|
||||||
const value = this.value || [];
|
const value = this.value || [];
|
||||||
const hide = [...(this.hideStates || []), ...value];
|
const hide = [...(this.hideStates || []), ...value];
|
||||||
|
const hideValue = value.includes(ANY_STATE_VALUE);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${repeat(
|
${repeat(
|
||||||
@@ -84,7 +86,7 @@ export class HaEntityStatesPicker extends LitElement {
|
|||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
${this.disabled && value.length
|
${(this.disabled && value.length) || hideValue
|
||||||
? nothing
|
? nothing
|
||||||
: keyed(
|
: keyed(
|
||||||
value.length,
|
value.length,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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, property, query, state } from "lit/decorators";
|
||||||
|
import { SwipeGestureRecognizer } from "../common/util/swipe-gesture-recognizer";
|
||||||
import { haStyleScrollbar } from "../resources/styles";
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
|
|
||||||
export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300;
|
export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300;
|
||||||
@@ -14,6 +15,12 @@ export class HaBottomSheet extends LitElement {
|
|||||||
|
|
||||||
@state() private _drawerOpen = false;
|
@state() private _drawerOpen = false;
|
||||||
|
|
||||||
|
@query("#drawer") private _drawer!: HTMLElement;
|
||||||
|
|
||||||
|
private _gestureRecognizer = new SwipeGestureRecognizer();
|
||||||
|
|
||||||
|
private _isDragging = false;
|
||||||
|
|
||||||
private _handleAfterHide() {
|
private _handleAfterHide() {
|
||||||
this.open = false;
|
this.open = false;
|
||||||
const ev = new Event("closed", {
|
const ev = new Event("closed", {
|
||||||
@@ -33,19 +40,132 @@ export class HaBottomSheet extends LitElement {
|
|||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<wa-drawer
|
<wa-drawer
|
||||||
|
id="drawer"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
.open=${this._drawerOpen}
|
.open=${this._drawerOpen}
|
||||||
@wa-after-hide=${this._handleAfterHide}
|
@wa-after-hide=${this._handleAfterHide}
|
||||||
without-header
|
without-header
|
||||||
|
@touchstart=${this._handleTouchStart}
|
||||||
>
|
>
|
||||||
<slot name="header"></slot>
|
<slot name="header"></slot>
|
||||||
<div class="body ha-scrollbar">
|
<div id="body" class="body ha-scrollbar">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</wa-drawer>
|
</wa-drawer>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _handleTouchStart = (ev: TouchEvent) => {
|
||||||
|
// Check if any element inside drawer in the composed path has scrollTop > 0
|
||||||
|
for (const path of ev.composedPath()) {
|
||||||
|
const el = path as HTMLElement;
|
||||||
|
if (el === this._drawer) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (el.scrollTop > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._startResizing(ev.touches[0].clientY);
|
||||||
|
};
|
||||||
|
|
||||||
|
private _startResizing(clientY: number) {
|
||||||
|
// register event listeners for drag handling
|
||||||
|
document.addEventListener("touchmove", this._handleTouchMove, {
|
||||||
|
passive: false,
|
||||||
|
});
|
||||||
|
document.addEventListener("touchend", this._handleTouchEnd);
|
||||||
|
document.addEventListener("touchcancel", this._handleTouchEnd);
|
||||||
|
|
||||||
|
this._gestureRecognizer.start(clientY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleTouchMove = (ev: TouchEvent) => {
|
||||||
|
const currentY = ev.touches[0].clientY;
|
||||||
|
const delta = this._gestureRecognizer.move(currentY);
|
||||||
|
|
||||||
|
if (delta < 0) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this._isDragging = true;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (this._isDragging) {
|
||||||
|
this.style.setProperty(
|
||||||
|
"--dialog-transform",
|
||||||
|
`translateY(${delta * -1}px)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private _animateSnapBack() {
|
||||||
|
// Add transition for smooth animation
|
||||||
|
this.style.setProperty(
|
||||||
|
"--dialog-transition",
|
||||||
|
`transform ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms ease-out`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reset transform to snap back
|
||||||
|
this.style.removeProperty("--dialog-transform");
|
||||||
|
|
||||||
|
// Remove transition after animation completes
|
||||||
|
setTimeout(() => {
|
||||||
|
this.style.removeProperty("--dialog-transition");
|
||||||
|
}, BOTTOM_SHEET_ANIMATION_DURATION_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleTouchEnd = () => {
|
||||||
|
this._unregisterResizeHandlers();
|
||||||
|
|
||||||
|
this._isDragging = false;
|
||||||
|
|
||||||
|
const result = this._gestureRecognizer.end();
|
||||||
|
|
||||||
|
// If velocity exceeds threshold, use velocity direction to determine action
|
||||||
|
if (result.isSwipe) {
|
||||||
|
if (result.isDownwardSwipe) {
|
||||||
|
// Downward swipe - close the bottom sheet
|
||||||
|
this._drawerOpen = false;
|
||||||
|
} else {
|
||||||
|
// Upward swipe - keep open and animate back
|
||||||
|
this._animateSnapBack();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If velocity is below threshold, use position-based logic
|
||||||
|
// Get the drawer height to calculate 50% threshold
|
||||||
|
const drawerBody = this._drawer.shadowRoot?.querySelector(
|
||||||
|
'[part="body"]'
|
||||||
|
) as HTMLElement;
|
||||||
|
const drawerHeight = drawerBody?.offsetHeight || 0;
|
||||||
|
|
||||||
|
// delta is negative when dragging down
|
||||||
|
// Close if dragged down past 50% of the drawer height
|
||||||
|
if (
|
||||||
|
drawerHeight > 0 &&
|
||||||
|
result.delta < 0 &&
|
||||||
|
Math.abs(result.delta) > drawerHeight * 0.5
|
||||||
|
) {
|
||||||
|
this._drawerOpen = false;
|
||||||
|
} else {
|
||||||
|
this._animateSnapBack();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private _unregisterResizeHandlers = () => {
|
||||||
|
document.removeEventListener("touchmove", this._handleTouchMove);
|
||||||
|
document.removeEventListener("touchend", this._handleTouchEnd);
|
||||||
|
document.removeEventListener("touchcancel", this._handleTouchEnd);
|
||||||
|
};
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this._unregisterResizeHandlers();
|
||||||
|
this._isDragging = false;
|
||||||
|
}
|
||||||
|
|
||||||
static styles = [
|
static styles = [
|
||||||
haStyleScrollbar,
|
haStyleScrollbar,
|
||||||
css`
|
css`
|
||||||
@@ -59,6 +179,8 @@ export class HaBottomSheet extends LitElement {
|
|||||||
wa-drawer::part(dialog) {
|
wa-drawer::part(dialog) {
|
||||||
max-height: var(--ha-bottom-sheet-max-height, 90vh);
|
max-height: var(--ha-bottom-sheet-max-height, 90vh);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
transform: var(--dialog-transform);
|
||||||
|
transition: var(--dialog-transition);
|
||||||
}
|
}
|
||||||
wa-drawer::part(body) {
|
wa-drawer::part(body) {
|
||||||
max-width: var(--ha-bottom-sheet-max-width);
|
max-width: var(--ha-bottom-sheet-max-width);
|
||||||
@@ -90,6 +212,11 @@ export class HaBottomSheet extends LitElement {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import "./ha-svg-icon";
|
|||||||
|
|
||||||
@customElement("ha-generic-picker")
|
@customElement("ha-generic-picker")
|
||||||
export class HaGenericPicker extends LitElement {
|
export class HaGenericPicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
// eslint-disable-next-line lit/no-native-attributes
|
// eslint-disable-next-line lit/no-native-attributes
|
||||||
@property({ type: Boolean }) public autofocus = false;
|
@property({ type: Boolean }) public autofocus = false;
|
||||||
@@ -68,6 +68,21 @@ export class HaGenericPicker extends LitElement {
|
|||||||
@property({ attribute: "not-found-label", type: String })
|
@property({ attribute: "not-found-label", type: String })
|
||||||
public notFoundLabel?: string;
|
public notFoundLabel?: string;
|
||||||
|
|
||||||
|
@property({ attribute: "popover-placement" })
|
||||||
|
public popoverPlacement:
|
||||||
|
| "bottom"
|
||||||
|
| "top"
|
||||||
|
| "left"
|
||||||
|
| "right"
|
||||||
|
| "top-start"
|
||||||
|
| "top-end"
|
||||||
|
| "right-start"
|
||||||
|
| "right-end"
|
||||||
|
| "bottom-start"
|
||||||
|
| "bottom-end"
|
||||||
|
| "left-start"
|
||||||
|
| "left-end" = "bottom-start";
|
||||||
|
|
||||||
/** If set picker shows an add button instead of textbox when value isn't set */
|
/** If set picker shows an add button instead of textbox when value isn't set */
|
||||||
@property({ attribute: "add-button-label" }) public addButtonLabel?: string;
|
@property({ attribute: "add-button-label" }) public addButtonLabel?: string;
|
||||||
|
|
||||||
@@ -135,7 +150,7 @@ export class HaGenericPicker extends LitElement {
|
|||||||
style="--body-width: ${this._popoverWidth}px;"
|
style="--body-width: ${this._popoverWidth}px;"
|
||||||
without-arrow
|
without-arrow
|
||||||
distance="-4"
|
distance="-4"
|
||||||
placement="bottom-start"
|
.placement=${this.popoverPlacement}
|
||||||
for="picker"
|
for="picker"
|
||||||
auto-size="vertical"
|
auto-size="vertical"
|
||||||
auto-size-padding="16"
|
auto-size-padding="16"
|
||||||
@@ -144,9 +159,7 @@ export class HaGenericPicker extends LitElement {
|
|||||||
trap-focus
|
trap-focus
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label=${this.hass.localize(
|
aria-label=${this.label || "Select option"}
|
||||||
"ui.components.target-picker.add_target"
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
${this._renderComboBox()}
|
${this._renderComboBox()}
|
||||||
</wa-popover>
|
</wa-popover>
|
||||||
@@ -159,9 +172,7 @@ export class HaGenericPicker extends LitElement {
|
|||||||
@closed=${this._hidePicker}
|
@closed=${this._hidePicker}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label=${this.hass.localize(
|
aria-label=${this.label || "Select option"}
|
||||||
"ui.components.target-picker.add_target"
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
${this._renderComboBox(true)}
|
${this._renderComboBox(true)}
|
||||||
</ha-bottom-sheet>`
|
</ha-bottom-sheet>`
|
||||||
@@ -179,7 +190,8 @@ export class HaGenericPicker extends LitElement {
|
|||||||
<ha-picker-combo-box
|
<ha-picker-combo-box
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.allowCustomValue=${this.allowCustomValue}
|
.allowCustomValue=${this.allowCustomValue}
|
||||||
.label=${this.searchLabel ?? this.hass.localize("ui.common.search")}
|
.label=${this.searchLabel ??
|
||||||
|
(this.hass?.localize("ui.common.search") || "Search")}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
.rowRenderer=${this.rowRenderer}
|
.rowRenderer=${this.rowRenderer}
|
||||||
|
|||||||
@@ -1,56 +1,58 @@
|
|||||||
import type { PropertyValues } from "lit";
|
import type { PropertyValues } from "lit";
|
||||||
import { css, html, LitElement } from "lit";
|
import { css, html, LitElement } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, state } 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 { stopPropagation } from "../common/dom/stop_propagation";
|
|
||||||
import { formatLanguageCode } from "../common/language/format_language";
|
import { formatLanguageCode } from "../common/language/format_language";
|
||||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||||
import type { FrontendLocaleData } from "../data/translation";
|
import type { FrontendLocaleData } from "../data/translation";
|
||||||
import { translationMetadata } from "../resources/translations-metadata";
|
import { translationMetadata } from "../resources/translations-metadata";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||||
|
import "./ha-generic-picker";
|
||||||
import "./ha-list-item";
|
import "./ha-list-item";
|
||||||
|
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||||
import "./ha-select";
|
import "./ha-select";
|
||||||
import type { HaSelect } from "./ha-select";
|
|
||||||
|
|
||||||
export const getLanguageOptions = (
|
export const getLanguageOptions = (
|
||||||
languages: string[],
|
languages: string[],
|
||||||
nativeName: boolean,
|
nativeName: boolean,
|
||||||
noSort: boolean,
|
noSort: boolean,
|
||||||
locale?: FrontendLocaleData
|
locale?: FrontendLocaleData
|
||||||
) => {
|
): PickerComboBoxItem[] => {
|
||||||
let options: { label: string; value: string }[] = [];
|
let options: PickerComboBoxItem[] = [];
|
||||||
|
|
||||||
if (nativeName) {
|
if (nativeName) {
|
||||||
const translations = translationMetadata.translations;
|
const translations = translationMetadata.translations;
|
||||||
options = languages.map((lang) => {
|
options = languages.map((lang) => {
|
||||||
let label = translations[lang]?.nativeName;
|
let primary = translations[lang]?.nativeName;
|
||||||
if (!label) {
|
if (!primary) {
|
||||||
try {
|
try {
|
||||||
// this will not work if Intl.DisplayNames is polyfilled, it will return in the language of the user
|
// this will not work if Intl.DisplayNames is polyfilled, it will return in the language of the user
|
||||||
label = new Intl.DisplayNames(lang, {
|
primary = new Intl.DisplayNames(lang, {
|
||||||
type: "language",
|
type: "language",
|
||||||
fallback: "code",
|
fallback: "code",
|
||||||
}).of(lang)!;
|
}).of(lang)!;
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
label = lang;
|
primary = lang;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
value: lang,
|
id: lang,
|
||||||
label,
|
primary,
|
||||||
|
search_labels: [primary],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} else if (locale) {
|
} else if (locale) {
|
||||||
options = languages.map((lang) => ({
|
options = languages.map((lang) => ({
|
||||||
value: lang,
|
id: lang,
|
||||||
label: formatLanguageCode(lang, locale),
|
primary: formatLanguageCode(lang, locale),
|
||||||
|
search_labels: [formatLanguageCode(lang, locale)],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!noSort && locale) {
|
if (!noSort && locale) {
|
||||||
options.sort((a, b) =>
|
options.sort((a, b) =>
|
||||||
caseInsensitiveStringCompare(a.label, b.label, locale.language)
|
caseInsensitiveStringCompare(a.primary, b.primary, locale.language)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return options;
|
return options;
|
||||||
@@ -80,115 +82,69 @@ export class HaLanguagePicker extends LitElement {
|
|||||||
|
|
||||||
@state() _defaultLanguages: string[] = [];
|
@state() _defaultLanguages: string[] = [];
|
||||||
|
|
||||||
@query("ha-select") private _select!: HaSelect;
|
|
||||||
|
|
||||||
protected firstUpdated(changedProps: PropertyValues) {
|
protected firstUpdated(changedProps: PropertyValues) {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
this._computeDefaultLanguageOptions();
|
this._computeDefaultLanguageOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updated(changedProperties: PropertyValues) {
|
|
||||||
super.updated(changedProperties);
|
|
||||||
|
|
||||||
const localeChanged =
|
|
||||||
changedProperties.has("hass") &&
|
|
||||||
this.hass &&
|
|
||||||
changedProperties.get("hass") &&
|
|
||||||
changedProperties.get("hass").locale.language !==
|
|
||||||
this.hass.locale.language;
|
|
||||||
if (
|
|
||||||
changedProperties.has("languages") ||
|
|
||||||
changedProperties.has("value") ||
|
|
||||||
localeChanged
|
|
||||||
) {
|
|
||||||
this._select.layoutOptions();
|
|
||||||
if (!this.disabled && this._select.value !== this.value) {
|
|
||||||
fireEvent(this, "value-changed", { value: this._select.value });
|
|
||||||
}
|
|
||||||
if (!this.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const languageOptions = this._getLanguagesOptions(
|
|
||||||
this.languages ?? this._defaultLanguages,
|
|
||||||
this.nativeName,
|
|
||||||
this.noSort,
|
|
||||||
this.hass?.locale
|
|
||||||
);
|
|
||||||
const selectedItemIndex = languageOptions.findIndex(
|
|
||||||
(option) => option.value === this.value
|
|
||||||
);
|
|
||||||
if (selectedItemIndex === -1) {
|
|
||||||
this.value = undefined;
|
|
||||||
}
|
|
||||||
if (localeChanged) {
|
|
||||||
this._select.select(selectedItemIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getLanguagesOptions = memoizeOne(getLanguageOptions);
|
private _getLanguagesOptions = memoizeOne(getLanguageOptions);
|
||||||
|
|
||||||
private _computeDefaultLanguageOptions() {
|
private _computeDefaultLanguageOptions() {
|
||||||
this._defaultLanguages = Object.keys(translationMetadata.translations);
|
this._defaultLanguages = Object.keys(translationMetadata.translations);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
private _getItems = () =>
|
||||||
const languageOptions = this._getLanguagesOptions(
|
this._getLanguagesOptions(
|
||||||
this.languages ?? this._defaultLanguages,
|
this.languages ?? this._defaultLanguages,
|
||||||
this.nativeName,
|
this.nativeName,
|
||||||
this.noSort,
|
this.noSort,
|
||||||
this.hass?.locale
|
this.hass?.locale
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private _valueRenderer = (value) => {
|
||||||
|
const language = this._getItems().find(
|
||||||
|
(lang) => lang.id === value
|
||||||
|
)?.primary;
|
||||||
|
return html`<span slot="headline">${language ?? value}</span> `;
|
||||||
|
};
|
||||||
|
|
||||||
|
protected render() {
|
||||||
const value =
|
const value =
|
||||||
this.value ??
|
this.value ??
|
||||||
(this.required && !this.disabled
|
(this.required && !this.disabled ? this._getItems()[0].id : this.value);
|
||||||
? languageOptions[0]?.value
|
|
||||||
: this.value);
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-select
|
<ha-generic-picker
|
||||||
.label=${this.label ??
|
.hass=${this.hass}
|
||||||
|
.autofocus=${this.autofocus}
|
||||||
|
popover-placement="bottom-end"
|
||||||
|
.notFoundLabel=${this.hass?.localize(
|
||||||
|
"ui.components.language-picker.no_match"
|
||||||
|
)}
|
||||||
|
.placeholder=${this.label ??
|
||||||
(this.hass?.localize("ui.components.language-picker.language") ||
|
(this.hass?.localize("ui.components.language-picker.language") ||
|
||||||
"Language")}
|
"Language")}
|
||||||
.value=${value || ""}
|
.value=${value}
|
||||||
.required=${this.required}
|
.valueRenderer=${this._valueRenderer}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
@selected=${this._changed}
|
.getItems=${this._getItems}
|
||||||
@closed=${stopPropagation}
|
@value-changed=${this._changed}
|
||||||
fixedMenuPosition
|
hide-clear-icon
|
||||||
naturalMenuWidth
|
></ha-generic-picker>
|
||||||
.inlineArrow=${this.inlineArrow}
|
|
||||||
>
|
|
||||||
${languageOptions.length === 0
|
|
||||||
? html`<ha-list-item value=""
|
|
||||||
>${this.hass?.localize(
|
|
||||||
"ui.components.language-picker.no_languages"
|
|
||||||
) || "No languages"}</ha-list-item
|
|
||||||
>`
|
|
||||||
: languageOptions.map(
|
|
||||||
(option) => html`
|
|
||||||
<ha-list-item .value=${option.value}
|
|
||||||
>${option.label}</ha-list-item
|
|
||||||
>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</ha-select>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
ha-select {
|
ha-generic-picker {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
min-width: 200px;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
private _changed(ev): void {
|
private _changed(ev: ValueChangedEvent<string>): void {
|
||||||
const target = ev.target as HaSelect;
|
ev.stopPropagation();
|
||||||
if (this.disabled || target.value === "" || target.value === this.value) {
|
this.value = ev.detail.value;
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.value = target.value;
|
|
||||||
fireEvent(this, "value-changed", { value: this.value });
|
fireEvent(this, "value-changed", { value: this.value });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export type PickerComboBoxSearchFn<T extends PickerComboBoxItem> = (
|
|||||||
|
|
||||||
@customElement("ha-picker-combo-box")
|
@customElement("ha-picker-combo-box")
|
||||||
export class HaPickerComboBox extends LitElement {
|
export class HaPickerComboBox extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
// eslint-disable-next-line lit/no-native-attributes
|
// eslint-disable-next-line lit/no-native-attributes
|
||||||
@property({ type: Boolean }) public autofocus = false;
|
@property({ type: Boolean }) public autofocus = false;
|
||||||
@@ -140,7 +140,9 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`<ha-textfield
|
return html`<ha-textfield
|
||||||
.label=${this.label ?? this.hass.localize("ui.common.search")}
|
.label=${this.label ??
|
||||||
|
this.hass?.localize("ui.common.search") ??
|
||||||
|
"Search"}
|
||||||
@input=${this._filterChanged}
|
@input=${this._filterChanged}
|
||||||
></ha-textfield>
|
></ha-textfield>
|
||||||
<lit-virtualizer
|
<lit-virtualizer
|
||||||
@@ -159,12 +161,18 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
private _defaultNotFoundItem = memoizeOne(
|
private _defaultNotFoundItem = memoizeOne(
|
||||||
(
|
(
|
||||||
label: this["notFoundLabel"],
|
label: this["notFoundLabel"],
|
||||||
localize: LocalizeFunc
|
localize?: LocalizeFunc
|
||||||
): PickerComboBoxItemWithLabel => ({
|
): PickerComboBoxItemWithLabel => ({
|
||||||
id: NO_MATCHING_ITEMS_FOUND_ID,
|
id: NO_MATCHING_ITEMS_FOUND_ID,
|
||||||
primary: label || localize("ui.components.combo-box.no_match"),
|
primary:
|
||||||
|
label ||
|
||||||
|
(localize && localize("ui.components.combo-box.no_match")) ||
|
||||||
|
"No matching items found",
|
||||||
icon_path: mdiMagnify,
|
icon_path: mdiMagnify,
|
||||||
a11y_label: label || localize("ui.components.combo-box.no_match"),
|
a11y_label:
|
||||||
|
label ||
|
||||||
|
(localize && localize("ui.components.combo-box.no_match")) ||
|
||||||
|
"No matching items found",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -189,13 +197,13 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
caseInsensitiveStringCompare(
|
caseInsensitiveStringCompare(
|
||||||
entityA.sorting_label!,
|
entityA.sorting_label!,
|
||||||
entityB.sorting_label!,
|
entityB.sorting_label!,
|
||||||
this.hass.locale.language
|
this.hass?.locale.language ?? navigator.language
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!sortedItems.length) {
|
if (!sortedItems.length) {
|
||||||
sortedItems.push(
|
sortedItems.push(
|
||||||
this._defaultNotFoundItem(this.notFoundLabel, this.hass.localize)
|
this._defaultNotFoundItem(this.notFoundLabel, this.hass?.localize)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,8 +257,20 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
const textfield = ev.target as HaTextField;
|
const textfield = ev.target as HaTextField;
|
||||||
const searchString = textfield.value.trim();
|
const searchString = textfield.value.trim();
|
||||||
|
|
||||||
|
if (!searchString) {
|
||||||
|
this._items = this._allItems;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const index = this._fuseIndex(this._allItems);
|
const index = this._fuseIndex(this._allItems);
|
||||||
const fuse = new HaFuse(this._allItems, { shouldSort: false }, index);
|
const fuse = new HaFuse(
|
||||||
|
this._allItems,
|
||||||
|
{
|
||||||
|
shouldSort: false,
|
||||||
|
minMatchCharLength: Math.min(searchString.length, 2),
|
||||||
|
},
|
||||||
|
index
|
||||||
|
);
|
||||||
|
|
||||||
const results = fuse.multiTermsSearch(searchString);
|
const results = fuse.multiTermsSearch(searchString);
|
||||||
let filteredItems = this._allItems as PickerComboBoxItem[];
|
let filteredItems = this._allItems as PickerComboBoxItem[];
|
||||||
@@ -258,7 +278,7 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
const items = results.map((result) => result.item);
|
const items = results.map((result) => result.item);
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
items.push(
|
items.push(
|
||||||
this._defaultNotFoundItem(this.notFoundLabel, this.hass.localize)
|
this._defaultNotFoundItem(this.notFoundLabel, this.hass?.localize)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const additionalItems = this._getAdditionalItems(searchString);
|
const additionalItems = this._getAdditionalItems(searchString);
|
||||||
@@ -431,6 +451,17 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
|
|
||||||
private _pickSelectedItem = (ev: KeyboardEvent) => {
|
private _pickSelectedItem = (ev: KeyboardEvent) => {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
const firstItem = this._virtualizerElement?.items[0] as PickerComboBoxItem;
|
||||||
|
|
||||||
|
if (
|
||||||
|
this._virtualizerElement?.items.length === 1 &&
|
||||||
|
firstItem.id !== NO_MATCHING_ITEMS_FOUND_ID
|
||||||
|
) {
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: firstItem.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (this._selectedItemIndex === -1) {
|
if (this._selectedItemIndex === -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -438,7 +469,9 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
// if filter button is focused
|
// if filter button is focused
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
const item: any = this._virtualizerElement?.items[this._selectedItemIndex];
|
const item = this._virtualizerElement?.items[
|
||||||
|
this._selectedItemIndex
|
||||||
|
] as PickerComboBoxItem;
|
||||||
if (item && item.id !== NO_MATCHING_ITEMS_FOUND_ID) {
|
if (item && item.id !== NO_MATCHING_ITEMS_FOUND_ID) {
|
||||||
fireEvent(this, "value-changed", { value: item.id });
|
fireEvent(this, "value-changed", { value: item.id });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
import { css, html, LitElement } from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
|
||||||
import type { BackgroundSelector } from "../../data/selector";
|
|
||||||
import type { HomeAssistant } from "../../types";
|
|
||||||
import "../ha-picture-upload";
|
|
||||||
import "../ha-alert";
|
|
||||||
import type { HaPictureUpload } from "../ha-picture-upload";
|
|
||||||
import { URL_PREFIX } from "../../data/image_upload";
|
|
||||||
|
|
||||||
@customElement("ha-selector-background")
|
|
||||||
export class HaBackgroundSelector extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property() public value?: any;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public selector!: BackgroundSelector;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public required = true;
|
|
||||||
|
|
||||||
@state() private yamlBackground = false;
|
|
||||||
|
|
||||||
protected updated(changedProps) {
|
|
||||||
super.updated(changedProps);
|
|
||||||
|
|
||||||
if (changedProps.has("value")) {
|
|
||||||
this.yamlBackground = !!this.value && !this.value.startsWith(URL_PREFIX);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render() {
|
|
||||||
return html`
|
|
||||||
<div>
|
|
||||||
${this.yamlBackground
|
|
||||||
? html`
|
|
||||||
<div class="value">
|
|
||||||
<img
|
|
||||||
src=${this.value}
|
|
||||||
alt=${this.hass.localize(
|
|
||||||
"ui.components.picture-upload.current_image_alt"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<ha-alert alert-type="info">
|
|
||||||
${this.hass.localize(
|
|
||||||
`ui.components.selectors.background.yaml_info`
|
|
||||||
)}
|
|
||||||
<ha-button slot="action" @click=${this._clearValue}>
|
|
||||||
${this.hass.localize(
|
|
||||||
`ui.components.picture-upload.clear_picture`
|
|
||||||
)}
|
|
||||||
</ha-button>
|
|
||||||
</ha-alert>
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<ha-picture-upload
|
|
||||||
.hass=${this.hass}
|
|
||||||
.value=${this.value?.startsWith(URL_PREFIX) ? this.value : null}
|
|
||||||
.original=${!!this.selector.background?.original}
|
|
||||||
.cropOptions=${this.selector.background?.crop}
|
|
||||||
select-media
|
|
||||||
@change=${this._pictureChanged}
|
|
||||||
></ha-picture-upload>
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _pictureChanged(ev) {
|
|
||||||
const value = (ev.target as HaPictureUpload).value;
|
|
||||||
|
|
||||||
fireEvent(this, "value-changed", { value: value ?? undefined });
|
|
||||||
}
|
|
||||||
|
|
||||||
private _clearValue() {
|
|
||||||
fireEvent(this, "value-changed", { value: undefined });
|
|
||||||
}
|
|
||||||
|
|
||||||
static styles = css`
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
ha-picture-upload {
|
|
||||||
background-color: var(--primary-background-color);
|
|
||||||
border-radius: var(--file-upload-image-border-radius);
|
|
||||||
}
|
|
||||||
div {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
ha-button {
|
|
||||||
white-space: nowrap;
|
|
||||||
--mdc-theme-primary: var(--primary-color);
|
|
||||||
}
|
|
||||||
.value {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 200px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
border-radius: var(--file-upload-image-border-radius);
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
opacity: var(--picture-opacity, 1);
|
|
||||||
}
|
|
||||||
img:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"ha-selector-background": HaBackgroundSelector;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"),
|
||||||
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"),
|
||||||
number: () => import("./ha-selector-number"),
|
number: () => import("./ha-selector-number"),
|
||||||
|
|||||||
@@ -214,8 +214,6 @@ export class HaWaDialog extends LitElement {
|
|||||||
);
|
);
|
||||||
max-width: var(--ha-dialog-max-width, 100vw);
|
max-width: var(--ha-dialog-max-width, 100vw);
|
||||||
max-width: var(--ha-dialog-max-width, 100svw);
|
max-width: var(--ha-dialog-max-width, 100svw);
|
||||||
/* TODO: animate view transition between width changes.
|
|
||||||
Needs https://github.com/home-assistant/frontend/pull/27281 for mixin */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:host([width="small"]) wa-dialog {
|
:host([width="small"]) wa-dialog {
|
||||||
|
|||||||
@@ -435,9 +435,9 @@ export const convertStatisticsToHistory = (
|
|||||||
Object.entries(orderedStatistics).forEach(([key, value]) => {
|
Object.entries(orderedStatistics).forEach(([key, value]) => {
|
||||||
const entityHistoryStates: EntityHistoryState[] = value.map((e) => ({
|
const entityHistoryStates: EntityHistoryState[] = value.map((e) => ({
|
||||||
s: e.mean != null ? e.mean.toString() : e.state!.toString(),
|
s: e.mean != null ? e.mean.toString() : e.state!.toString(),
|
||||||
lc: e.start / 1000,
|
lc: e.end / 1000,
|
||||||
a: {},
|
a: {},
|
||||||
lu: e.start / 1000,
|
lu: e.end / 1000,
|
||||||
}));
|
}));
|
||||||
statsHistoryStates[key] = entityHistoryStates;
|
statsHistoryStates[key] = entityHistoryStates;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { MediaSelectorValue } from "../../selector";
|
||||||
import type { LovelaceBadgeConfig } from "./badge";
|
import type { LovelaceBadgeConfig } from "./badge";
|
||||||
import type { LovelaceCardConfig } from "./card";
|
import type { LovelaceCardConfig } from "./card";
|
||||||
import type { LovelaceSectionRawConfig } from "./section";
|
import type { LovelaceSectionRawConfig } from "./section";
|
||||||
@@ -8,7 +9,7 @@ export interface ShowViewConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface LovelaceViewBackgroundConfig {
|
export interface LovelaceViewBackgroundConfig {
|
||||||
image?: string;
|
image?: string | MediaSelectorValue;
|
||||||
opacity?: number;
|
opacity?: number;
|
||||||
size?: "auto" | "cover" | "contain";
|
size?: "auto" | "cover" | "contain";
|
||||||
alignment?:
|
alignment?:
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import type {
|
|||||||
import { ensureArray } from "../common/array/ensure-array";
|
import { ensureArray } from "../common/array/ensure-array";
|
||||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||||
import { supportsFeature } from "../common/entity/supports-feature";
|
import { supportsFeature } from "../common/entity/supports-feature";
|
||||||
import type { CropOptions } from "../dialogs/image-cropper-dialog/show-image-cropper-dialog";
|
|
||||||
import { isHelperDomain } from "../panels/config/helpers/const";
|
import { isHelperDomain } from "../panels/config/helpers/const";
|
||||||
import type { UiAction } from "../panels/lovelace/components/hui-action-editor";
|
import type { UiAction } from "../panels/lovelace/components/hui-action-editor";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
@@ -47,8 +46,6 @@ export type Selector =
|
|||||||
| FileSelector
|
| FileSelector
|
||||||
| IconSelector
|
| IconSelector
|
||||||
| LabelSelector
|
| LabelSelector
|
||||||
| ImageSelector
|
|
||||||
| BackgroundSelector
|
|
||||||
| LanguageSelector
|
| LanguageSelector
|
||||||
| LocationSelector
|
| LocationSelector
|
||||||
| MediaSelector
|
| MediaSelector
|
||||||
@@ -273,14 +270,6 @@ export interface IconSelector {
|
|||||||
} | null;
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImageSelector {
|
|
||||||
image: { original?: boolean; crop?: CropOptions } | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BackgroundSelector {
|
|
||||||
background: { original?: boolean; crop?: CropOptions } | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LabelSelector {
|
export interface LabelSelector {
|
||||||
label: {
|
label: {
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
|
|||||||
@@ -484,7 +484,7 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
this._unsubDataEntryFlowProgress = undefined;
|
this._unsubDataEntryFlowProgress = undefined;
|
||||||
}
|
}
|
||||||
if (_step.next_flow[0] === "config_flow") {
|
if (_step.next_flow[0] === "config_flow") {
|
||||||
showConfigFlowDialog(this._params!.dialogParentElement!, {
|
showConfigFlowDialog(this, {
|
||||||
continueFlowId: _step.next_flow[1],
|
continueFlowId: _step.next_flow[1],
|
||||||
carryOverDevices: this._devices(
|
carryOverDevices: this._devices(
|
||||||
this._params!.flowConfig.showDevices,
|
this._params!.flowConfig.showDevices,
|
||||||
@@ -496,32 +496,23 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
});
|
});
|
||||||
} else if (_step.next_flow[0] === "options_flow") {
|
} else if (_step.next_flow[0] === "options_flow") {
|
||||||
if (_step.type === "create_entry") {
|
if (_step.type === "create_entry") {
|
||||||
showOptionsFlowDialog(
|
showOptionsFlowDialog(this, _step.result!, {
|
||||||
this._params!.dialogParentElement!,
|
continueFlowId: _step.next_flow[1],
|
||||||
_step.result!,
|
navigateToResult: this._params!.navigateToResult,
|
||||||
{
|
dialogClosedCallback: this._params!.dialogClosedCallback,
|
||||||
continueFlowId: _step.next_flow[1],
|
});
|
||||||
navigateToResult: this._params!.navigateToResult,
|
|
||||||
dialogClosedCallback: this._params!.dialogClosedCallback,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else if (_step.next_flow[0] === "config_subentries_flow") {
|
} else if (_step.next_flow[0] === "config_subentries_flow") {
|
||||||
if (_step.type === "create_entry") {
|
if (_step.type === "create_entry") {
|
||||||
showSubConfigFlowDialog(
|
showSubConfigFlowDialog(this, _step.result!, _step.next_flow[0], {
|
||||||
this._params!.dialogParentElement!,
|
continueFlowId: _step.next_flow[1],
|
||||||
_step.result!,
|
navigateToResult: this._params!.navigateToResult,
|
||||||
_step.next_flow[0],
|
dialogClosedCallback: this._params!.dialogClosedCallback,
|
||||||
{
|
});
|
||||||
continueFlowId: _step.next_flow[1],
|
|
||||||
navigateToResult: this._params!.navigateToResult,
|
|
||||||
dialogClosedCallback: this._params!.dialogClosedCallback,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
showAlertDialog(this._params!.dialogParentElement!, {
|
showAlertDialog(this, {
|
||||||
text: this.hass.localize(
|
text: this.hass.localize(
|
||||||
"ui.panel.config.integrations.config_flow.error",
|
"ui.panel.config.integrations.config_flow.error",
|
||||||
{ error: `Unsupported next flow type: ${_step.next_flow[0]}` }
|
{ error: `Unsupported next flow type: ${_step.next_flow[0]}` }
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-reques
|
|||||||
import { navigate } from "../../common/navigate";
|
import { navigate } from "../../common/navigate";
|
||||||
import { computeRTL } from "../../common/util/compute_rtl";
|
import { computeRTL } from "../../common/util/compute_rtl";
|
||||||
import "../../components/ha-button-menu";
|
import "../../components/ha-button-menu";
|
||||||
import "../../components/ha-wa-dialog";
|
import "../../components/ha-dialog";
|
||||||
import "../../components/ha-dialog-header";
|
import "../../components/ha-dialog-header";
|
||||||
import "../../components/ha-icon-button";
|
import "../../components/ha-icon-button";
|
||||||
import "../../components/ha-icon-button-prev";
|
import "../../components/ha-icon-button-prev";
|
||||||
@@ -99,8 +99,6 @@ export class MoreInfoDialog extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean, reflect: true }) public large = false;
|
@property({ type: Boolean, reflect: true }) public large = false;
|
||||||
|
|
||||||
@state() private _open = false;
|
|
||||||
|
|
||||||
@state() private _parentEntityIds: string[] = [];
|
@state() private _parentEntityIds: string[] = [];
|
||||||
|
|
||||||
@state() private _entityId?: string | null;
|
@state() private _entityId?: string | null;
|
||||||
@@ -133,7 +131,6 @@ export class MoreInfoDialog extends LitElement {
|
|||||||
this._initialView = params.view || DEFAULT_VIEW;
|
this._initialView = params.view || DEFAULT_VIEW;
|
||||||
this._childView = undefined;
|
this._childView = undefined;
|
||||||
this.large = false;
|
this.large = false;
|
||||||
this._open = true;
|
|
||||||
this._loadEntityRegistryEntry();
|
this._loadEntityRegistryEntry();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,10 +149,6 @@ export class MoreInfoDialog extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog() {
|
public closeDialog() {
|
||||||
this._open = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _dialogClosed() {
|
|
||||||
this._entityId = undefined;
|
this._entityId = undefined;
|
||||||
this._parentEntityIds = [];
|
this._parentEntityIds = [];
|
||||||
this._entry = undefined;
|
this._entry = undefined;
|
||||||
@@ -371,21 +364,21 @@ export class MoreInfoDialog extends LitElement {
|
|||||||
const isRTL = computeRTL(this.hass);
|
const isRTL = computeRTL(this.hass);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-wa-dialog
|
<ha-dialog
|
||||||
.hass=${this.hass}
|
open
|
||||||
.open=${this._open}
|
@closed=${this.closeDialog}
|
||||||
.width=${this.large ? "full" : "medium"}
|
|
||||||
@closed=${this._dialogClosed}
|
|
||||||
@opened=${this._handleOpened}
|
@opened=${this._handleOpened}
|
||||||
?prevent-scrim-close=${!this._isEscapeEnabled}
|
.escapeKeyAction=${this._isEscapeEnabled ? undefined : ""}
|
||||||
flexcontent
|
.heading=${title}
|
||||||
|
hideActions
|
||||||
|
flexContent
|
||||||
>
|
>
|
||||||
<ha-dialog-header slot="header">
|
<ha-dialog-header slot="heading">
|
||||||
${showCloseIcon
|
${showCloseIcon
|
||||||
? html`
|
? html`
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
slot="navigationIcon"
|
slot="navigationIcon"
|
||||||
data-dialog="close"
|
dialogAction="cancel"
|
||||||
.label=${this.hass.localize("ui.common.close")}
|
.label=${this.hass.localize("ui.common.close")}
|
||||||
.path=${mdiClose}
|
.path=${mdiClose}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
@@ -567,7 +560,7 @@ export class MoreInfoDialog extends LitElement {
|
|||||||
<div
|
<div
|
||||||
class="content"
|
class="content"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
autofocus
|
dialogInitialFocus
|
||||||
@show-child-view=${this._showChildView}
|
@show-child-view=${this._showChildView}
|
||||||
@entity-entry-updated=${this._entryUpdated}
|
@entity-entry-updated=${this._entryUpdated}
|
||||||
@toggle-edit-mode=${this._handleToggleInfoEditModeEvent}
|
@toggle-edit-mode=${this._handleToggleInfoEditModeEvent}
|
||||||
@@ -587,6 +580,7 @@ export class MoreInfoDialog extends LitElement {
|
|||||||
: this._currView === "info"
|
: this._currView === "info"
|
||||||
? html`
|
? html`
|
||||||
<ha-more-info-info
|
<ha-more-info-info
|
||||||
|
dialogInitialFocus
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.entityId=${this._entityId}
|
.entityId=${this._entityId}
|
||||||
.entry=${this._entry}
|
.entry=${this._entry}
|
||||||
@@ -624,7 +618,7 @@ export class MoreInfoDialog extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
</ha-wa-dialog>
|
</ha-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -680,7 +674,13 @@ export class MoreInfoDialog extends LitElement {
|
|||||||
return [
|
return [
|
||||||
haStyleDialog,
|
haStyleDialog,
|
||||||
css`
|
css`
|
||||||
ha-wa-dialog {
|
ha-dialog {
|
||||||
|
/* Set the top top of the dialog to a fixed position, so it doesnt jump when the content changes size */
|
||||||
|
--vertical-align-dialog: flex-start;
|
||||||
|
--dialog-surface-margin-top: max(
|
||||||
|
var(--ha-space-10),
|
||||||
|
var(--safe-area-inset-top, var(--ha-space-0))
|
||||||
|
);
|
||||||
--dialog-content-padding: 0;
|
--dialog-content-padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -703,6 +703,30 @@ export class MoreInfoDialog extends LitElement {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||||
|
/* When in fullscreen dialog should be attached to top */
|
||||||
|
ha-dialog {
|
||||||
|
--dialog-surface-margin-top: var(--ha-space-0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (min-width: 600px) and (min-height: 501px) {
|
||||||
|
ha-dialog {
|
||||||
|
--mdc-dialog-min-width: 580px;
|
||||||
|
--mdc-dialog-max-width: 580px;
|
||||||
|
--mdc-dialog-max-height: calc(100% - 72px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-title {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([large]) ha-dialog {
|
||||||
|
--mdc-dialog-min-width: 90vw;
|
||||||
|
--mdc-dialog-max-width: 90vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -193,12 +193,12 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
|||||||
).map(
|
).map(
|
||||||
(lang) =>
|
(lang) =>
|
||||||
html`<ha-md-menu-item
|
html`<ha-md-menu-item
|
||||||
.value=${lang.value}
|
.value=${lang.id}
|
||||||
@click=${this._handlePickLanguage}
|
@click=${this._handlePickLanguage}
|
||||||
@keydown=${this._handlePickLanguage}
|
@keydown=${this._handlePickLanguage}
|
||||||
.selected=${this._language === lang.value}
|
.selected=${this._language === lang.id}
|
||||||
>
|
>
|
||||||
${lang.label}
|
${lang.primary}
|
||||||
</ha-md-menu-item>`
|
</ha-md-menu-item>`
|
||||||
)}
|
)}
|
||||||
</ha-md-button-menu>`
|
</ha-md-button-menu>`
|
||||||
|
|||||||
@@ -143,9 +143,14 @@ class DialogCalendarEventDetail extends LitElement {
|
|||||||
this.hass.locale.time_zone,
|
this.hass.locale.time_zone,
|
||||||
this.hass.config.time_zone
|
this.hass.config.time_zone
|
||||||
);
|
);
|
||||||
const start = new TZDate(this._data!.dtstart, timeZone);
|
// For all-day events (date-only strings), parse without timezone to avoid offset issues
|
||||||
const endValue = new TZDate(this._data!.dtend, timeZone);
|
const start = isDate(this._data!.dtstart)
|
||||||
// All day events should be displayed as a day earlier
|
? new Date(this._data!.dtstart + "T00:00:00")
|
||||||
|
: new TZDate(this._data!.dtstart, timeZone);
|
||||||
|
const endValue = isDate(this._data!.dtend)
|
||||||
|
? new Date(this._data!.dtend + "T00:00:00")
|
||||||
|
: new TZDate(this._data!.dtend, timeZone);
|
||||||
|
// All day event end dates are exclusive in iCalendar format, subtract one day for display
|
||||||
const end = isDate(this._data.dtend) ? addDays(endValue, -1) : endValue;
|
const end = isDate(this._data.dtend) ? addDays(endValue, -1) : endValue;
|
||||||
// The range can be shortened when the start and end are on the same day.
|
// The range can be shortened when the start and end are on the same day.
|
||||||
if (isSameDay(start, end)) {
|
if (isSameDay(start, end)) {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { ensureArray } from "../../../../../common/array/ensure-array";
|
|||||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||||
import { hasTemplate } from "../../../../../common/string/has-template";
|
import { hasTemplate } from "../../../../../common/string/has-template";
|
||||||
import type { StateTrigger } from "../../../../../data/automation";
|
import type { StateTrigger } from "../../../../../data/automation";
|
||||||
|
import { ANY_STATE_VALUE } from "../../../../../components/entity/const";
|
||||||
import type { HomeAssistant } from "../../../../../types";
|
import type { HomeAssistant } from "../../../../../types";
|
||||||
import { baseTriggerStruct, forDictStruct } from "../../structs";
|
import { baseTriggerStruct, forDictStruct } from "../../structs";
|
||||||
import type { TriggerElement } from "../ha-automation-trigger-row";
|
import type { TriggerElement } from "../ha-automation-trigger-row";
|
||||||
@@ -36,14 +37,12 @@ const stateTriggerStruct = assign(
|
|||||||
trigger: literal("state"),
|
trigger: literal("state"),
|
||||||
entity_id: optional(union([string(), array(string())])),
|
entity_id: optional(union([string(), array(string())])),
|
||||||
attribute: optional(string()),
|
attribute: optional(string()),
|
||||||
from: optional(nullable(string())),
|
from: optional(union([nullable(string()), array(string())])),
|
||||||
to: optional(nullable(string())),
|
to: optional(union([nullable(string()), array(string())])),
|
||||||
for: optional(union([number(), string(), forDictStruct])),
|
for: optional(union([number(), string(), forDictStruct])),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const ANY_STATE_VALUE = "__ANY_STATE_IGNORE_ATTRIBUTES__";
|
|
||||||
|
|
||||||
@customElement("ha-automation-trigger-state")
|
@customElement("ha-automation-trigger-state")
|
||||||
export class HaStateTrigger extends LitElement implements TriggerElement {
|
export class HaStateTrigger extends LitElement implements TriggerElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -57,7 +56,12 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _schema = memoizeOne(
|
private _schema = memoizeOne(
|
||||||
(localize: LocalizeFunc, attribute) =>
|
(
|
||||||
|
localize: LocalizeFunc,
|
||||||
|
attribute: string | undefined,
|
||||||
|
hideInFrom: string[],
|
||||||
|
hideInTo: string[]
|
||||||
|
) =>
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
name: "entity_id",
|
name: "entity_id",
|
||||||
@@ -131,6 +135,7 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
|
|||||||
},
|
},
|
||||||
selector: {
|
selector: {
|
||||||
state: {
|
state: {
|
||||||
|
multiple: true,
|
||||||
extra_options: (attribute
|
extra_options: (attribute
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
@@ -142,6 +147,7 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
|
|||||||
},
|
},
|
||||||
]) as any,
|
]) as any,
|
||||||
attribute: attribute,
|
attribute: attribute,
|
||||||
|
hide_states: hideInFrom,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -152,6 +158,7 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
|
|||||||
},
|
},
|
||||||
selector: {
|
selector: {
|
||||||
state: {
|
state: {
|
||||||
|
multiple: true,
|
||||||
extra_options: (attribute
|
extra_options: (attribute
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
@@ -163,6 +170,7 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
|
|||||||
},
|
},
|
||||||
]) as any,
|
]) as any,
|
||||||
attribute: attribute,
|
attribute: attribute,
|
||||||
|
hide_states: hideInTo,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -207,13 +215,15 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
|
|||||||
entity_id: ensureArray(this.trigger.entity_id),
|
entity_id: ensureArray(this.trigger.entity_id),
|
||||||
for: trgFor,
|
for: trgFor,
|
||||||
};
|
};
|
||||||
if (!data.attribute && data.to === null) {
|
|
||||||
data.to = ANY_STATE_VALUE;
|
data.to = this._normalizeStates(this.trigger.to, data.attribute);
|
||||||
}
|
data.from = this._normalizeStates(this.trigger.from, data.attribute);
|
||||||
if (!data.attribute && data.from === null) {
|
const schema = this._schema(
|
||||||
data.from = ANY_STATE_VALUE;
|
this.hass.localize,
|
||||||
}
|
this.trigger.attribute,
|
||||||
const schema = this._schema(this.hass.localize, this.trigger.attribute);
|
data.to,
|
||||||
|
data.from
|
||||||
|
);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-form
|
<ha-form
|
||||||
@@ -231,22 +241,58 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
|
|||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const newTrigger = ev.detail.value;
|
const newTrigger = ev.detail.value;
|
||||||
|
|
||||||
if (newTrigger.to === ANY_STATE_VALUE) {
|
newTrigger.to = this._applyAnyStateExclusive(
|
||||||
newTrigger.to = newTrigger.attribute ? undefined : null;
|
newTrigger.to,
|
||||||
}
|
newTrigger.attribute
|
||||||
if (newTrigger.from === ANY_STATE_VALUE) {
|
);
|
||||||
newTrigger.from = newTrigger.attribute ? undefined : null;
|
newTrigger.from = this._applyAnyStateExclusive(
|
||||||
}
|
newTrigger.from,
|
||||||
|
newTrigger.attribute
|
||||||
Object.keys(newTrigger).forEach((key) =>
|
|
||||||
newTrigger[key] === undefined || newTrigger[key] === ""
|
|
||||||
? delete newTrigger[key]
|
|
||||||
: {}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Object.keys(newTrigger).forEach((key) => {
|
||||||
|
const val = newTrigger[key];
|
||||||
|
if (
|
||||||
|
val === undefined ||
|
||||||
|
val === "" ||
|
||||||
|
(Array.isArray(val) && val.length === 0)
|
||||||
|
) {
|
||||||
|
delete newTrigger[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
fireEvent(this, "value-changed", { value: newTrigger });
|
fireEvent(this, "value-changed", { value: newTrigger });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _applyAnyStateExclusive(
|
||||||
|
val: string | string[] | null | undefined,
|
||||||
|
attribute?: string
|
||||||
|
): string | string[] | null | undefined {
|
||||||
|
const anyStateSelected = Array.isArray(val)
|
||||||
|
? val.includes(ANY_STATE_VALUE)
|
||||||
|
: val === ANY_STATE_VALUE;
|
||||||
|
if (anyStateSelected) {
|
||||||
|
// Any state is exclusive: null if no attribute, undefined if attribute
|
||||||
|
return attribute ? undefined : null;
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _normalizeStates(
|
||||||
|
value: string | string[] | null | undefined,
|
||||||
|
attribute?: string
|
||||||
|
): string[] {
|
||||||
|
// If no attribute is selected and backend value is null,
|
||||||
|
// expose it as the special ANY state option in the UI.
|
||||||
|
if (!attribute && value === null) {
|
||||||
|
return [ANY_STATE_VALUE];
|
||||||
|
}
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return ensureArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
private _computeLabelCallback = (
|
private _computeLabelCallback = (
|
||||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||||
): string =>
|
): string =>
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { fireEvent } from "../../../../common/dom/fire_event";
|
|||||||
import { computeDeviceNameDisplay } from "../../../../common/entity/compute_device_name";
|
import { computeDeviceNameDisplay } from "../../../../common/entity/compute_device_name";
|
||||||
import "../../../../components/ha-alert";
|
import "../../../../components/ha-alert";
|
||||||
import "../../../../components/ha-area-picker";
|
import "../../../../components/ha-area-picker";
|
||||||
import "../../../../components/ha-dialog";
|
import "../../../../components/ha-wa-dialog";
|
||||||
|
import "../../../../components/ha-dialog-footer";
|
||||||
import "../../../../components/ha-button";
|
import "../../../../components/ha-button";
|
||||||
import "../../../../components/ha-labels-picker";
|
import "../../../../components/ha-labels-picker";
|
||||||
import type { HaSwitch } from "../../../../components/ha-switch";
|
import type { HaSwitch } from "../../../../components/ha-switch";
|
||||||
@@ -19,6 +20,8 @@ import type { DeviceRegistryDetailDialogParams } from "./show-dialog-device-regi
|
|||||||
class DialogDeviceRegistryDetail extends LitElement {
|
class DialogDeviceRegistryDetail extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _open = false;
|
||||||
|
|
||||||
@state() private _nameByUser!: string;
|
@state() private _nameByUser!: string;
|
||||||
|
|
||||||
@state() private _error?: string;
|
@state() private _error?: string;
|
||||||
@@ -42,10 +45,15 @@ class DialogDeviceRegistryDetail extends LitElement {
|
|||||||
this._areaId = this._params.device.area_id || "";
|
this._areaId = this._params.device.area_id || "";
|
||||||
this._labels = this._params.device.labels || [];
|
this._labels = this._params.device.labels || [];
|
||||||
this._disabledBy = this._params.device.disabled_by;
|
this._disabledBy = this._params.device.disabled_by;
|
||||||
|
this._open = true;
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog(): void {
|
public closeDialog(): void {
|
||||||
|
this._open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _dialogClosed(): void {
|
||||||
this._error = "";
|
this._error = "";
|
||||||
this._params = undefined;
|
this._params = undefined;
|
||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
@@ -57,10 +65,12 @@ class DialogDeviceRegistryDetail extends LitElement {
|
|||||||
}
|
}
|
||||||
const device = this._params.device;
|
const device = this._params.device;
|
||||||
return html`
|
return html`
|
||||||
<ha-dialog
|
<ha-wa-dialog
|
||||||
open
|
.hass=${this.hass}
|
||||||
@closed=${this.closeDialog}
|
.open=${this._open}
|
||||||
.heading=${computeDeviceNameDisplay(device, this.hass)}
|
header-title=${computeDeviceNameDisplay(device, this.hass)}
|
||||||
|
prevent-scrim-close
|
||||||
|
@closed=${this._dialogClosed}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
${this._error
|
${this._error
|
||||||
@@ -68,6 +78,7 @@ class DialogDeviceRegistryDetail extends LitElement {
|
|||||||
: ""}
|
: ""}
|
||||||
<div class="form">
|
<div class="form">
|
||||||
<ha-textfield
|
<ha-textfield
|
||||||
|
autofocus
|
||||||
.value=${this._nameByUser}
|
.value=${this._nameByUser}
|
||||||
@input=${this._nameChanged}
|
@input=${this._nameChanged}
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
@@ -75,7 +86,6 @@ class DialogDeviceRegistryDetail extends LitElement {
|
|||||||
)}
|
)}
|
||||||
.placeholder=${device.name || ""}
|
.placeholder=${device.name || ""}
|
||||||
.disabled=${this._submitting}
|
.disabled=${this._submitting}
|
||||||
dialogInitialFocus
|
|
||||||
></ha-textfield>
|
></ha-textfield>
|
||||||
<ha-area-picker
|
<ha-area-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@@ -131,22 +141,25 @@ class DialogDeviceRegistryDetail extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ha-button
|
|
||||||
slot="secondaryAction"
|
<ha-dialog-footer slot="footer">
|
||||||
@click=${this.closeDialog}
|
<ha-button
|
||||||
.disabled=${this._submitting}
|
slot="secondaryAction"
|
||||||
appearance="plain"
|
@click=${this.closeDialog}
|
||||||
>
|
.disabled=${this._submitting}
|
||||||
${this.hass.localize("ui.common.cancel")}
|
appearance="plain"
|
||||||
</ha-button>
|
>
|
||||||
<ha-button
|
${this.hass.localize("ui.common.cancel")}
|
||||||
slot="primaryAction"
|
</ha-button>
|
||||||
@click=${this._updateEntry}
|
<ha-button
|
||||||
.disabled=${this._submitting}
|
slot="primaryAction"
|
||||||
>
|
@click=${this._updateEntry}
|
||||||
${this.hass.localize("ui.dialogs.device-registry-detail.update")}
|
.disabled=${this._submitting}
|
||||||
</ha-button>
|
>
|
||||||
</ha-dialog>
|
${this.hass.localize("ui.dialogs.device-registry-detail.update")}
|
||||||
|
</ha-button>
|
||||||
|
</ha-dialog-footer>
|
||||||
|
</ha-wa-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -280,10 +280,11 @@ ${type === "object"
|
|||||||
|
|
||||||
.content.horizontal {
|
.content.horizontal {
|
||||||
--code-mirror-max-height: calc(
|
--code-mirror-max-height: calc(
|
||||||
100vh - var(--header-height) - (var(--ha-line-height-normal) * 3) -
|
100vh - var(--header-height) -
|
||||||
(1em * 2) - (max(16px, var(--safe-area-inset-top)) * 2) -
|
(var(--ha-line-height-normal) * var(--ha-font-size-m) * 3) -
|
||||||
|
(max(16px, var(--safe-area-inset-top)) * 2) -
|
||||||
(max(16px, var(--safe-area-inset-bottom)) * 2) -
|
(max(16px, var(--safe-area-inset-bottom)) * 2) -
|
||||||
(var(--ha-card-border-width, 1px) * 2) - 179px
|
(var(--ha-card-border-width, 1px) * 3) - (1em * 2) - 192px
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -313,9 +313,14 @@ class HaPanelHistory extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const statsStartDate = new Date(this._startDate);
|
||||||
|
// History uses the end datapoint of the statistic, so if we want the
|
||||||
|
// graph to start at 7AM, need to fetch the statistic from 6AM.
|
||||||
|
statsStartDate.setHours(statsStartDate.getHours() - 1);
|
||||||
|
|
||||||
const statistics = await fetchStatistics(
|
const statistics = await fetchStatistics(
|
||||||
this.hass!,
|
this.hass!,
|
||||||
this._startDate,
|
statsStartDate,
|
||||||
this._endDate,
|
this._endDate,
|
||||||
statisticIds,
|
statisticIds,
|
||||||
"hour",
|
"hour",
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import { html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||||
|
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||||
|
import "../../../components/ha-control-number-buttons";
|
||||||
|
import { isUnavailableState } from "../../../data/entity";
|
||||||
|
import {
|
||||||
|
MediaPlayerEntityFeature,
|
||||||
|
type MediaPlayerEntity,
|
||||||
|
} from "../../../data/media-player";
|
||||||
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
|
||||||
|
import { cardFeatureStyles } from "./common/card-feature-styles";
|
||||||
|
import type {
|
||||||
|
LovelaceCardFeatureContext,
|
||||||
|
MediaPlayerVolumeButtonsCardFeatureConfig,
|
||||||
|
} from "./types";
|
||||||
|
import { clamp } from "../../../common/number/clamp";
|
||||||
|
|
||||||
|
export const supportsMediaPlayerVolumeButtonsCardFeature = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
context: LovelaceCardFeatureContext
|
||||||
|
) => {
|
||||||
|
const stateObj = context.entity_id
|
||||||
|
? hass.states[context.entity_id]
|
||||||
|
: undefined;
|
||||||
|
if (!stateObj) return false;
|
||||||
|
const domain = computeDomain(stateObj.entity_id);
|
||||||
|
return (
|
||||||
|
domain === "media_player" &&
|
||||||
|
supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_SET)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
@customElement("hui-media-player-volume-buttons-card-feature")
|
||||||
|
class HuiMediaPlayerVolumeButtonsCardFeature
|
||||||
|
extends LitElement
|
||||||
|
implements LovelaceCardFeature
|
||||||
|
{
|
||||||
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
||||||
|
|
||||||
|
@state() private _config?: MediaPlayerVolumeButtonsCardFeatureConfig;
|
||||||
|
|
||||||
|
private get _stateObj() {
|
||||||
|
if (!this.hass || !this.context || !this.context.entity_id) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return this.hass.states[this.context.entity_id] as
|
||||||
|
| MediaPlayerEntity
|
||||||
|
| undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getStubConfig(): MediaPlayerVolumeButtonsCardFeatureConfig {
|
||||||
|
return {
|
||||||
|
type: "media-player-volume-buttons",
|
||||||
|
step: 5,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
|
||||||
|
await import(
|
||||||
|
"../editor/config-elements/hui-media-player-volume-buttons-card-feature-editor"
|
||||||
|
);
|
||||||
|
return document.createElement(
|
||||||
|
"hui-media-player-volume-buttons-card-feature-editor"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setConfig(config: MediaPlayerVolumeButtonsCardFeatureConfig): void {
|
||||||
|
if (!config) {
|
||||||
|
throw new Error("Invalid configuration");
|
||||||
|
}
|
||||||
|
this._config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (
|
||||||
|
!this._config ||
|
||||||
|
!this.hass ||
|
||||||
|
!this.context ||
|
||||||
|
!this._stateObj ||
|
||||||
|
!supportsMediaPlayerVolumeButtonsCardFeature(this.hass, this.context)
|
||||||
|
) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const position =
|
||||||
|
this._stateObj.attributes.volume_level != null
|
||||||
|
? Math.round(this._stateObj.attributes.volume_level * 100)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-control-number-buttons
|
||||||
|
.disabled=${!this._stateObj || isUnavailableState(this._stateObj.state)}
|
||||||
|
.locale=${this.hass.locale}
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
.step=${this._config.step ?? 5}
|
||||||
|
.value=${position}
|
||||||
|
unit="%"
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
></ha-control-number-buttons>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _valueChanged(ev: CustomEvent) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
this.hass!.callService("media_player", "volume_set", {
|
||||||
|
entity_id: this._stateObj!.entity_id,
|
||||||
|
volume_level: clamp(ev.detail.value, 0, 100) / 100,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return cardFeatureStyles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-media-player-volume-buttons-card-feature": HuiMediaPlayerVolumeButtonsCardFeature;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,6 +50,11 @@ export interface MediaPlayerVolumeSliderCardFeatureConfig {
|
|||||||
type: "media-player-volume-slider";
|
type: "media-player-volume-slider";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MediaPlayerVolumeButtonsCardFeatureConfig {
|
||||||
|
type: "media-player-volume-buttons";
|
||||||
|
step?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface FanDirectionCardFeatureConfig {
|
export interface FanDirectionCardFeatureConfig {
|
||||||
type: "fan-direction";
|
type: "fan-direction";
|
||||||
}
|
}
|
||||||
@@ -252,6 +257,7 @@ export type LovelaceCardFeatureConfig =
|
|||||||
| LockCommandsCardFeatureConfig
|
| LockCommandsCardFeatureConfig
|
||||||
| LockOpenDoorCardFeatureConfig
|
| LockOpenDoorCardFeatureConfig
|
||||||
| MediaPlayerPlaybackCardFeatureConfig
|
| MediaPlayerPlaybackCardFeatureConfig
|
||||||
|
| MediaPlayerVolumeButtonsCardFeatureConfig
|
||||||
| MediaPlayerVolumeSliderCardFeatureConfig
|
| MediaPlayerVolumeSliderCardFeatureConfig
|
||||||
| NumericInputCardFeatureConfig
|
| NumericInputCardFeatureConfig
|
||||||
| SelectOptionsCardFeatureConfig
|
| SelectOptionsCardFeatureConfig
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { PropertyValues, TemplateResult } from "lit";
|
import type { PropertyValues, TemplateResult } from "lit";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { DOMAINS_TOGGLE } from "../../../common/const";
|
import { DOMAINS_TOGGLE } from "../../../common/const";
|
||||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||||
@@ -20,9 +20,11 @@ import type {
|
|||||||
import type {
|
import type {
|
||||||
LovelaceCard,
|
LovelaceCard,
|
||||||
LovelaceCardEditor,
|
LovelaceCardEditor,
|
||||||
|
LovelaceGridOptions,
|
||||||
LovelaceHeaderFooter,
|
LovelaceHeaderFooter,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import type { EntitiesCardConfig } from "./types";
|
import type { EntitiesCardConfig } from "./types";
|
||||||
|
import { haStyleScrollbar } from "../../../resources/styles";
|
||||||
|
|
||||||
export const computeShowHeaderToggle = <
|
export const computeShowHeaderToggle = <
|
||||||
T extends EntityConfig | LovelaceRowConfig,
|
T extends EntityConfig | LovelaceRowConfig,
|
||||||
@@ -75,6 +77,8 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
|||||||
|
|
||||||
private _hass?: HomeAssistant;
|
private _hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public layout?: string;
|
||||||
|
|
||||||
private _configEntities?: LovelaceRowConfig[];
|
private _configEntities?: LovelaceRowConfig[];
|
||||||
|
|
||||||
private _showHeaderToggle?: boolean;
|
private _showHeaderToggle?: boolean;
|
||||||
@@ -139,6 +143,14 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
|||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getGridOptions(): LovelaceGridOptions {
|
||||||
|
return {
|
||||||
|
columns: 12,
|
||||||
|
min_columns: 6,
|
||||||
|
min_rows: this._config?.title || this._showHeaderToggle ? 3 : 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public setConfig(config: EntitiesCardConfig): void {
|
public setConfig(config: EntitiesCardConfig): void {
|
||||||
if (!config.entities || !Array.isArray(config.entities)) {
|
if (!config.entities || !Array.isArray(config.entities)) {
|
||||||
throw new Error("Entities must be specified");
|
throw new Error("Entities must be specified");
|
||||||
@@ -233,7 +245,7 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
|||||||
`}
|
`}
|
||||||
</h1>
|
</h1>
|
||||||
`}
|
`}
|
||||||
<div id="states" class="card-content">
|
<div id="states" class="card-content ha-scrollbar">
|
||||||
${this._configEntities!.map((entityConf) =>
|
${this._configEntities!.map((entityConf) =>
|
||||||
this._renderEntity(entityConf)
|
this._renderEntity(entityConf)
|
||||||
)}
|
)}
|
||||||
@@ -246,69 +258,73 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = css`
|
static styles = [
|
||||||
ha-card {
|
haStyleScrollbar,
|
||||||
height: 100%;
|
css`
|
||||||
display: flex;
|
ha-card {
|
||||||
flex-direction: column;
|
height: 100%;
|
||||||
justify-content: space-between;
|
display: flex;
|
||||||
}
|
flex-direction: column;
|
||||||
.card-header {
|
justify-content: space-between;
|
||||||
display: flex;
|
}
|
||||||
justify-content: space-between;
|
.card-header {
|
||||||
}
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
.card-header .name {
|
.card-header .name {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
#states {
|
#states {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--entities-card-row-gap, var(--card-row-gap, 8px));
|
gap: var(--entities-card-row-gap, var(--card-row-gap, 8px));
|
||||||
}
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
#states > div > * {
|
#states > div > * {
|
||||||
overflow: clip visible;
|
overflow: clip visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
#states > div {
|
#states > div {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
padding: 0px 18px 0px 8px;
|
padding: 0px 18px 0px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
border-top-left-radius: var(
|
border-top-left-radius: var(
|
||||||
--ha-card-border-radius,
|
--ha-card-border-radius,
|
||||||
var(--ha-border-radius-lg)
|
var(--ha-border-radius-lg)
|
||||||
);
|
);
|
||||||
border-top-right-radius: var(
|
border-top-right-radius: var(
|
||||||
--ha-card-border-radius,
|
--ha-card-border-radius,
|
||||||
var(--ha-border-radius-lg)
|
var(--ha-border-radius-lg)
|
||||||
);
|
);
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
border-bottom-left-radius: var(
|
border-bottom-left-radius: var(
|
||||||
--ha-card-border-radius,
|
--ha-card-border-radius,
|
||||||
var(--ha-border-radius-lg)
|
var(--ha-border-radius-lg)
|
||||||
);
|
);
|
||||||
border-bottom-right-radius: var(
|
border-bottom-right-radius: var(
|
||||||
--ha-card-border-radius,
|
--ha-card-border-radius,
|
||||||
var(--ha-border-radius-lg)
|
var(--ha-border-radius-lg)
|
||||||
);
|
);
|
||||||
margin-top: -16px;
|
margin-top: -16px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
`;
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
private _renderEntity(entityConf: LovelaceRowConfig): TemplateResult {
|
private _renderEntity(entityConf: LovelaceRowConfig): TemplateResult {
|
||||||
const element = createRowElement(
|
const element = createRowElement(
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
|
|||||||
private async _fetchStatistics(sensorNumericDeviceClasses: string[]) {
|
private async _fetchStatistics(sensorNumericDeviceClasses: string[]) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const start = new Date();
|
const start = new Date();
|
||||||
start.setHours(start.getHours() - this._hoursToShow);
|
start.setHours(start.getHours() - this._hoursToShow - 1);
|
||||||
|
|
||||||
const statistics = await fetchStatistics(
|
const statistics = await fetchStatistics(
|
||||||
this.hass!,
|
this.hass!,
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ import { findEntities } from "../common/find-entities";
|
|||||||
import { processConfigEntities } from "../common/process-config-entities";
|
import { processConfigEntities } from "../common/process-config-entities";
|
||||||
import "../components/hui-warning";
|
import "../components/hui-warning";
|
||||||
import type { EntityConfig } from "../entity-rows/types";
|
import type { EntityConfig } from "../entity-rows/types";
|
||||||
import type { LovelaceCard, LovelaceCardEditor } from "../types";
|
import type {
|
||||||
|
LovelaceCard,
|
||||||
|
LovelaceCardEditor,
|
||||||
|
LovelaceGridOptions,
|
||||||
|
} from "../types";
|
||||||
import type { LogbookCardConfig } from "./types";
|
import type { LogbookCardConfig } from "./types";
|
||||||
import { resolveEntityIDs } from "../../../data/selector";
|
import { resolveEntityIDs } from "../../../data/selector";
|
||||||
import { ensureArray } from "../../../common/array/ensure-array";
|
import { ensureArray } from "../../../common/array/ensure-array";
|
||||||
@@ -64,6 +68,15 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
|
|||||||
return 9 + (this._config?.title ? 1 : 0);
|
return 9 + (this._config?.title ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getGridOptions(): LovelaceGridOptions {
|
||||||
|
return {
|
||||||
|
rows: 6,
|
||||||
|
columns: 12,
|
||||||
|
min_columns: 6,
|
||||||
|
min_rows: this._config?.title ? 4 : 3,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public validateTarget(
|
public validateTarget(
|
||||||
config: LogbookCardConfig
|
config: LogbookCardConfig
|
||||||
): HassServiceTarget | undefined {
|
): HassServiceTarget | undefined {
|
||||||
@@ -189,6 +202,10 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
|
|||||||
>
|
>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<ha-logbook
|
<ha-logbook
|
||||||
|
class=${classMap({
|
||||||
|
"is-grid": this.layout === "grid",
|
||||||
|
"is-panel": this.layout === "panel",
|
||||||
|
})}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.time=${this._time}
|
.time=${this._time}
|
||||||
.entityIds=${this._getEntityIds()}
|
.entityIds=${this._getEntityIds()}
|
||||||
@@ -212,6 +229,7 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
height: 100%;
|
||||||
padding: 0 16px 16px;
|
padding: 0 16px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,6 +242,11 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ha-logbook.is-grid,
|
||||||
|
ha-logbook.is-panel {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
:host([ispanel]) .content,
|
:host([ispanel]) .content,
|
||||||
:host([ispanel]) ha-logbook {
|
:host([ispanel]) ha-logbook {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import "../card-features/hui-light-color-temp-card-feature";
|
|||||||
import "../card-features/hui-lock-commands-card-feature";
|
import "../card-features/hui-lock-commands-card-feature";
|
||||||
import "../card-features/hui-lock-open-door-card-feature";
|
import "../card-features/hui-lock-open-door-card-feature";
|
||||||
import "../card-features/hui-media-player-playback-card-feature";
|
import "../card-features/hui-media-player-playback-card-feature";
|
||||||
|
import "../card-features/hui-media-player-volume-buttons-card-feature";
|
||||||
import "../card-features/hui-media-player-volume-slider-card-feature";
|
import "../card-features/hui-media-player-volume-slider-card-feature";
|
||||||
import "../card-features/hui-numeric-input-card-feature";
|
import "../card-features/hui-numeric-input-card-feature";
|
||||||
import "../card-features/hui-select-options-card-feature";
|
import "../card-features/hui-select-options-card-feature";
|
||||||
@@ -72,6 +73,7 @@ const TYPES = new Set<LovelaceCardFeatureConfig["type"]>([
|
|||||||
"lock-commands",
|
"lock-commands",
|
||||||
"lock-open-door",
|
"lock-open-door",
|
||||||
"media-player-playback",
|
"media-player-playback",
|
||||||
|
"media-player-volume-buttons",
|
||||||
"media-player-volume-slider",
|
"media-player-volume-slider",
|
||||||
"numeric-input",
|
"numeric-input",
|
||||||
"select-options",
|
"select-options",
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import { supportsLightColorTempCardFeature } from "../../card-features/hui-light
|
|||||||
import { supportsLockCommandsCardFeature } from "../../card-features/hui-lock-commands-card-feature";
|
import { supportsLockCommandsCardFeature } from "../../card-features/hui-lock-commands-card-feature";
|
||||||
import { supportsLockOpenDoorCardFeature } from "../../card-features/hui-lock-open-door-card-feature";
|
import { supportsLockOpenDoorCardFeature } from "../../card-features/hui-lock-open-door-card-feature";
|
||||||
import { supportsMediaPlayerPlaybackCardFeature } from "../../card-features/hui-media-player-playback-card-feature";
|
import { supportsMediaPlayerPlaybackCardFeature } from "../../card-features/hui-media-player-playback-card-feature";
|
||||||
|
import { supportsMediaPlayerVolumeButtonsCardFeature } from "../../card-features/hui-media-player-volume-buttons-card-feature";
|
||||||
import { supportsMediaPlayerVolumeSliderCardFeature } from "../../card-features/hui-media-player-volume-slider-card-feature";
|
import { supportsMediaPlayerVolumeSliderCardFeature } from "../../card-features/hui-media-player-volume-slider-card-feature";
|
||||||
import { supportsNumericInputCardFeature } from "../../card-features/hui-numeric-input-card-feature";
|
import { supportsNumericInputCardFeature } from "../../card-features/hui-numeric-input-card-feature";
|
||||||
import { supportsSelectOptionsCardFeature } from "../../card-features/hui-select-options-card-feature";
|
import { supportsSelectOptionsCardFeature } from "../../card-features/hui-select-options-card-feature";
|
||||||
@@ -102,6 +103,7 @@ const UI_FEATURE_TYPES = [
|
|||||||
"lock-commands",
|
"lock-commands",
|
||||||
"lock-open-door",
|
"lock-open-door",
|
||||||
"media-player-playback",
|
"media-player-playback",
|
||||||
|
"media-player-volume-buttons",
|
||||||
"media-player-volume-slider",
|
"media-player-volume-slider",
|
||||||
"numeric-input",
|
"numeric-input",
|
||||||
"select-options",
|
"select-options",
|
||||||
@@ -131,6 +133,7 @@ const EDITABLES_FEATURE_TYPES = new Set<UiFeatureTypes>([
|
|||||||
"fan-preset-modes",
|
"fan-preset-modes",
|
||||||
"humidifier-modes",
|
"humidifier-modes",
|
||||||
"lawn-mower-commands",
|
"lawn-mower-commands",
|
||||||
|
"media-player-volume-buttons",
|
||||||
"numeric-input",
|
"numeric-input",
|
||||||
"select-options",
|
"select-options",
|
||||||
"trend-graph",
|
"trend-graph",
|
||||||
@@ -171,6 +174,7 @@ const SUPPORTS_FEATURE_TYPES: Record<
|
|||||||
"lock-commands": supportsLockCommandsCardFeature,
|
"lock-commands": supportsLockCommandsCardFeature,
|
||||||
"lock-open-door": supportsLockOpenDoorCardFeature,
|
"lock-open-door": supportsLockOpenDoorCardFeature,
|
||||||
"media-player-playback": supportsMediaPlayerPlaybackCardFeature,
|
"media-player-playback": supportsMediaPlayerPlaybackCardFeature,
|
||||||
|
"media-player-volume-buttons": supportsMediaPlayerVolumeButtonsCardFeature,
|
||||||
"media-player-volume-slider": supportsMediaPlayerVolumeSliderCardFeature,
|
"media-player-volume-slider": supportsMediaPlayerVolumeSliderCardFeature,
|
||||||
"numeric-input": supportsNumericInputCardFeature,
|
"numeric-input": supportsNumericInputCardFeature,
|
||||||
"select-options": supportsSelectOptionsCardFeature,
|
"select-options": supportsSelectOptionsCardFeature,
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import "../../../../components/ha-form/ha-form";
|
||||||
|
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||||
|
import type { HomeAssistant } from "../../../../types";
|
||||||
|
import type {
|
||||||
|
LovelaceCardFeatureContext,
|
||||||
|
MediaPlayerVolumeButtonsCardFeatureConfig,
|
||||||
|
} from "../../card-features/types";
|
||||||
|
import type { LovelaceCardFeatureEditor } from "../../types";
|
||||||
|
|
||||||
|
@customElement("hui-media-player-volume-buttons-card-feature-editor")
|
||||||
|
export class HuiMediaPlayerVolumeButtonsCardFeatureEditor
|
||||||
|
extends LitElement
|
||||||
|
implements LovelaceCardFeatureEditor
|
||||||
|
{
|
||||||
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
||||||
|
|
||||||
|
@state() private _config?: MediaPlayerVolumeButtonsCardFeatureConfig;
|
||||||
|
|
||||||
|
public setConfig(config: MediaPlayerVolumeButtonsCardFeatureConfig): void {
|
||||||
|
this._config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _schema = memoizeOne(
|
||||||
|
() =>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: "step",
|
||||||
|
selector: {
|
||||||
|
number: {
|
||||||
|
mode: "slider",
|
||||||
|
step: 1,
|
||||||
|
min: 1,
|
||||||
|
max: 100,
|
||||||
|
unit_of_measurement: "%",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
);
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this.hass || !this._config) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: MediaPlayerVolumeButtonsCardFeatureConfig = {
|
||||||
|
type: "media-player-volume-buttons",
|
||||||
|
step: this._config.step ?? 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
const schema = this._schema();
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-form
|
||||||
|
.hass=${this.hass}
|
||||||
|
.data=${data}
|
||||||
|
.schema=${schema}
|
||||||
|
.computeLabel=${this._computeLabelCallback}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
></ha-form>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _valueChanged(ev: CustomEvent): void {
|
||||||
|
fireEvent(this, "config-changed", { config: ev.detail.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _computeLabelCallback = (
|
||||||
|
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||||
|
) =>
|
||||||
|
this.hass!.localize(
|
||||||
|
`ui.panel.lovelace.editor.features.types.media-player-volume-buttons.${schema.name}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-media-player-volume-buttons-card-feature-editor": HuiMediaPlayerVolumeButtonsCardFeatureEditor;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,18 @@
|
|||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { LitElement, css, html, nothing } from "lit";
|
import { LitElement, css, html, nothing } from "lit";
|
||||||
|
import type { PropertyValues } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import "../../../../components/ha-form/ha-form";
|
import "../../../../components/ha-form/ha-form";
|
||||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
|
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||||
|
|
||||||
|
import {
|
||||||
|
isMediaSourceContentId,
|
||||||
|
resolveMediaSource,
|
||||||
|
} from "../../../../data/media_source";
|
||||||
|
|
||||||
@customElement("hui-view-background-editor")
|
@customElement("hui-view-background-editor")
|
||||||
export class HuiViewBackgroundEditor extends LitElement {
|
export class HuiViewBackgroundEditor extends LitElement {
|
||||||
@@ -13,6 +20,8 @@ export class HuiViewBackgroundEditor extends LitElement {
|
|||||||
|
|
||||||
@state() private _config!: LovelaceViewConfig;
|
@state() private _config!: LovelaceViewConfig;
|
||||||
|
|
||||||
|
@state({ attribute: false }) private _resolvedImage?: string;
|
||||||
|
|
||||||
set config(config: LovelaceViewConfig) {
|
set config(config: LovelaceViewConfig) {
|
||||||
this._config = config;
|
this._config = config;
|
||||||
}
|
}
|
||||||
@@ -20,133 +29,195 @@ export class HuiViewBackgroundEditor extends LitElement {
|
|||||||
private _localizeValueCallback = (key: string) =>
|
private _localizeValueCallback = (key: string) =>
|
||||||
this.hass.localize(key as any);
|
this.hass.localize(key as any);
|
||||||
|
|
||||||
private _schema = memoizeOne((showSettings: boolean) => [
|
private _schema = memoizeOne(
|
||||||
{
|
(localize: LocalizeFunc, showSettings: boolean) =>
|
||||||
name: "image",
|
[
|
||||||
selector: { background: { original: true } },
|
{
|
||||||
},
|
name: "image",
|
||||||
...(showSettings
|
selector: {
|
||||||
? ([
|
media: {
|
||||||
{
|
accept: ["image/*"] as string[],
|
||||||
name: "settings",
|
clearable: true,
|
||||||
flatten: true,
|
image_upload: true,
|
||||||
expanded: true,
|
hide_content_type: true,
|
||||||
type: "expandable" as const,
|
content_id_helper: localize(
|
||||||
schema: [
|
"ui.panel.lovelace.editor.card.picture.content_id_helper"
|
||||||
{
|
),
|
||||||
name: "opacity",
|
},
|
||||||
selector: {
|
|
||||||
number: { min: 0, max: 100, mode: "slider", step: 10 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "attachment",
|
|
||||||
selector: {
|
|
||||||
button_toggle: {
|
|
||||||
translation_key:
|
|
||||||
"ui.panel.lovelace.editor.edit_view.background.attachment",
|
|
||||||
options: ["scroll", "fixed"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "size",
|
|
||||||
required: true,
|
|
||||||
selector: {
|
|
||||||
select: {
|
|
||||||
translation_key:
|
|
||||||
"ui.panel.lovelace.editor.edit_view.background.size",
|
|
||||||
options: ["auto", "cover", "contain"],
|
|
||||||
mode: "dropdown",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "alignment",
|
|
||||||
required: true,
|
|
||||||
selector: {
|
|
||||||
select: {
|
|
||||||
translation_key:
|
|
||||||
"ui.panel.lovelace.editor.edit_view.background.alignment",
|
|
||||||
options: [
|
|
||||||
"top left",
|
|
||||||
"top center",
|
|
||||||
"top right",
|
|
||||||
"center left",
|
|
||||||
"center",
|
|
||||||
"center right",
|
|
||||||
"bottom left",
|
|
||||||
"bottom center",
|
|
||||||
"bottom right",
|
|
||||||
],
|
|
||||||
mode: "dropdown",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "repeat",
|
|
||||||
required: true,
|
|
||||||
selector: {
|
|
||||||
select: {
|
|
||||||
translation_key:
|
|
||||||
"ui.panel.lovelace.editor.edit_view.background.repeat",
|
|
||||||
options: ["repeat", "no-repeat"],
|
|
||||||
mode: "dropdown",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
] as const)
|
},
|
||||||
: []),
|
...(showSettings
|
||||||
]);
|
? ([
|
||||||
|
{
|
||||||
|
name: "settings",
|
||||||
|
flatten: true,
|
||||||
|
expanded: true,
|
||||||
|
type: "expandable" as const,
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
name: "opacity",
|
||||||
|
selector: {
|
||||||
|
number: { min: 0, max: 100, mode: "slider", step: 10 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "attachment",
|
||||||
|
selector: {
|
||||||
|
button_toggle: {
|
||||||
|
translation_key:
|
||||||
|
"ui.panel.lovelace.editor.edit_view.background.attachment",
|
||||||
|
options: ["scroll", "fixed"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "size",
|
||||||
|
required: true,
|
||||||
|
selector: {
|
||||||
|
select: {
|
||||||
|
translation_key:
|
||||||
|
"ui.panel.lovelace.editor.edit_view.background.size",
|
||||||
|
options: ["auto", "cover", "contain"],
|
||||||
|
mode: "dropdown",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "alignment",
|
||||||
|
required: true,
|
||||||
|
selector: {
|
||||||
|
select: {
|
||||||
|
translation_key:
|
||||||
|
"ui.panel.lovelace.editor.edit_view.background.alignment",
|
||||||
|
options: [
|
||||||
|
"top left",
|
||||||
|
"top center",
|
||||||
|
"top right",
|
||||||
|
"center left",
|
||||||
|
"center",
|
||||||
|
"center right",
|
||||||
|
"bottom left",
|
||||||
|
"bottom center",
|
||||||
|
"bottom right",
|
||||||
|
],
|
||||||
|
mode: "dropdown",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "repeat",
|
||||||
|
required: true,
|
||||||
|
selector: {
|
||||||
|
select: {
|
||||||
|
translation_key:
|
||||||
|
"ui.panel.lovelace.editor.edit_view.background.repeat",
|
||||||
|
options: ["repeat", "no-repeat"],
|
||||||
|
mode: "dropdown",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
] as const)
|
||||||
|
: []),
|
||||||
|
] as const
|
||||||
|
);
|
||||||
|
|
||||||
|
protected updated(changedProps: PropertyValues) {
|
||||||
|
if (
|
||||||
|
this._config &&
|
||||||
|
this.hass &&
|
||||||
|
(changedProps.has("_config") ||
|
||||||
|
(changedProps.has("hass") && !changedProps.get("hass")))
|
||||||
|
) {
|
||||||
|
const background = this._backgroundData(this._config);
|
||||||
|
this.style.setProperty(
|
||||||
|
"--picture-opacity",
|
||||||
|
`${(background.opacity ?? 100) / 100}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const backgroundImage =
|
||||||
|
typeof background.image === "object"
|
||||||
|
? background.image.media_content_id
|
||||||
|
: background.image;
|
||||||
|
|
||||||
|
if (backgroundImage && isMediaSourceContentId(backgroundImage)) {
|
||||||
|
resolveMediaSource(this.hass, backgroundImage).then((result) => {
|
||||||
|
this._resolvedImage = result.url;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this._resolvedImage = backgroundImage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this.hass) {
|
if (!this.hass) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
let background = this._config?.background;
|
const background = this._backgroundData(this._config);
|
||||||
if (typeof background === "string") {
|
|
||||||
const backgroundUrl = background.match(/url\(['"]?([^'"]+)['"]?\)/)?.[1];
|
|
||||||
|
|
||||||
background = {
|
|
||||||
image: backgroundUrl,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!background) {
|
|
||||||
background = {
|
|
||||||
opacity: 33,
|
|
||||||
alignment: "center",
|
|
||||||
size: "cover",
|
|
||||||
repeat: "repeat",
|
|
||||||
attachment: "fixed",
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
background = {
|
|
||||||
opacity: 100,
|
|
||||||
alignment: "center",
|
|
||||||
size: "cover",
|
|
||||||
repeat: "no-repeat",
|
|
||||||
attachment: "scroll",
|
|
||||||
...background,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
|
${this._resolvedImage
|
||||||
|
? html`<div class="previewContainer">
|
||||||
|
<img
|
||||||
|
src=${this._resolvedImage}
|
||||||
|
alt=${this.hass.localize(
|
||||||
|
"ui.components.picture-upload.current_image_alt"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>`
|
||||||
|
: nothing}
|
||||||
<ha-form
|
<ha-form
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.data=${background}
|
.data=${background}
|
||||||
.schema=${this._schema(true)}
|
.schema=${this._schema(this.hass.localize, true)}
|
||||||
.computeLabel=${this._computeLabelCallback}
|
.computeLabel=${this._computeLabelCallback}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
.localizeValue=${this._localizeValueCallback}
|
.localizeValue=${this._localizeValueCallback}
|
||||||
style=${`--picture-opacity: ${(background.opacity ?? 100) / 100};`}
|
|
||||||
></ha-form>
|
></ha-form>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _backgroundData = memoizeOne(
|
||||||
|
(backgroundConfig?: LovelaceViewConfig) => {
|
||||||
|
let background = backgroundConfig?.background;
|
||||||
|
if (typeof background === "string") {
|
||||||
|
const backgroundUrl = background.match(
|
||||||
|
/url\(['"]?([^'"]+)['"]?\)/
|
||||||
|
)?.[1];
|
||||||
|
|
||||||
|
background = {
|
||||||
|
image: backgroundUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!background) {
|
||||||
|
background = {
|
||||||
|
opacity: 33,
|
||||||
|
alignment: "center",
|
||||||
|
size: "cover",
|
||||||
|
repeat: "repeat",
|
||||||
|
attachment: "fixed",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
background = {
|
||||||
|
opacity: 100,
|
||||||
|
alignment: "center",
|
||||||
|
size: "cover",
|
||||||
|
repeat: "no-repeat",
|
||||||
|
attachment: "scroll",
|
||||||
|
...background,
|
||||||
|
...(typeof background.image === "string"
|
||||||
|
? { image: { media_content_id: background.image } }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return background;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
private _valueChanged(ev: CustomEvent): void {
|
private _valueChanged(ev: CustomEvent): void {
|
||||||
const config = {
|
const config = {
|
||||||
...this._config,
|
...this._config,
|
||||||
@@ -195,6 +266,23 @@ export class HuiViewBackgroundEditor extends LitElement {
|
|||||||
display: block;
|
display: block;
|
||||||
--file-upload-image-border-radius: var(--ha-border-radius-sm);
|
--file-upload-image-border-radius: var(--ha-border-radius-sm);
|
||||||
}
|
}
|
||||||
|
.previewContainer {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 200px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
border-radius: var(--file-upload-image-border-radius);
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
opacity: var(--picture-opacity, 1);
|
||||||
|
}
|
||||||
|
img:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ export class HomeMainViewStrategy extends ReactiveElement {
|
|||||||
? floor.name
|
? floor.name
|
||||||
: hass.localize("ui.panel.lovelace.strategy.home.areas"),
|
: hass.localize("ui.panel.lovelace.strategy.home.areas"),
|
||||||
heading_style: "title",
|
heading_style: "title",
|
||||||
|
icon: floor.icon,
|
||||||
},
|
},
|
||||||
...cards,
|
...cards,
|
||||||
],
|
],
|
||||||
@@ -215,7 +216,9 @@ export class HomeMainViewStrategy extends ReactiveElement {
|
|||||||
column_span: maxColumns,
|
column_span: maxColumns,
|
||||||
cards: [],
|
cards: [],
|
||||||
};
|
};
|
||||||
const weatherEntity = Object.keys(hass.states).find(weatherFilter);
|
const weatherEntity = Object.keys(hass.states)
|
||||||
|
.filter(weatherFilter)
|
||||||
|
.sort()[0];
|
||||||
|
|
||||||
if (weatherEntity) {
|
if (weatherEntity) {
|
||||||
widgetSection.cards!.push(
|
widgetSection.cards!.push(
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ export class HUIViewBackground extends LitElement {
|
|||||||
const backgroundImage =
|
const backgroundImage =
|
||||||
typeof this.background === "string"
|
typeof this.background === "string"
|
||||||
? this.background
|
? this.background
|
||||||
: this.background?.image;
|
: typeof this.background?.image === "object"
|
||||||
|
? this.background.image.media_content_id
|
||||||
|
: this.background?.image;
|
||||||
|
|
||||||
if (backgroundImage && isMediaSourceContentId(backgroundImage)) {
|
if (backgroundImage && isMediaSourceContentId(backgroundImage)) {
|
||||||
resolveMediaSource(this.hass, backgroundImage).then((result) => {
|
resolveMediaSource(this.hass, backgroundImage).then((result) => {
|
||||||
@@ -73,13 +75,17 @@ 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) {
|
const image =
|
||||||
|
typeof background.image === "object"
|
||||||
|
? background.image.media_content_id || ""
|
||||||
|
: background.image;
|
||||||
|
if (isMediaSourceContentId(image) && !this.resolvedImage) {
|
||||||
return null;
|
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(this.resolvedImage || background.image)}')`;
|
return `${alignment} / ${size} ${repeat} url('${this.hass.hassUrl(this.resolvedImage || image)}')`;
|
||||||
}
|
}
|
||||||
if (typeof background === "string") {
|
if (typeof background === "string") {
|
||||||
if (isMediaSourceContentId(background) && !this.resolvedImage) {
|
if (isMediaSourceContentId(background) && !this.resolvedImage) {
|
||||||
|
|||||||
@@ -758,6 +758,7 @@
|
|||||||
},
|
},
|
||||||
"language-picker": {
|
"language-picker": {
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
|
"no_match": "No matching languages found",
|
||||||
"no_languages": "No languages available"
|
"no_languages": "No languages available"
|
||||||
},
|
},
|
||||||
"tts-picker": {
|
"tts-picker": {
|
||||||
@@ -8153,6 +8154,10 @@
|
|||||||
"media-player-playback": {
|
"media-player-playback": {
|
||||||
"label": "Media player playback controls"
|
"label": "Media player playback controls"
|
||||||
},
|
},
|
||||||
|
"media-player-volume-buttons": {
|
||||||
|
"label": "Media player volume buttons",
|
||||||
|
"step": "Step size"
|
||||||
|
},
|
||||||
"media-player-volume-slider": {
|
"media-player-volume-slider": {
|
||||||
"label": "Media player volume slider"
|
"label": "Media player volume slider"
|
||||||
},
|
},
|
||||||
|
|||||||
71
test/panels/calendar/dialog-calendar-event-detail.test.ts
Normal file
71
test/panels/calendar/dialog-calendar-event-detail.test.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { expect, test } from "vitest";
|
||||||
|
import { TZDate } from "@date-fns/tz";
|
||||||
|
import { isDate } from "../../../src/common/string/is_date";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These tests verify that all-day event dates are correctly identified
|
||||||
|
* and can be distinguished from datetime strings. This is critical for
|
||||||
|
* proper date display in the calendar event detail dialog.
|
||||||
|
*/
|
||||||
|
|
||||||
|
test("isDate correctly identifies date-only strings", () => {
|
||||||
|
// Valid date-only strings (all-day events)
|
||||||
|
expect(isDate("2025-10-10")).toBe(true);
|
||||||
|
expect(isDate("2007-06-28")).toBe(true);
|
||||||
|
expect(isDate("2025-12-31")).toBe(true);
|
||||||
|
|
||||||
|
// DateTime strings should not be identified as dates
|
||||||
|
expect(isDate("2025-10-10T00:00:00")).toBe(false);
|
||||||
|
expect(isDate("2025-10-10T14:30:00")).toBe(false);
|
||||||
|
expect(isDate("2025-10-10T14:30:00Z")).toBe(false);
|
||||||
|
expect(isDate("2025-10-10T14:30:00+00:00")).toBe(false);
|
||||||
|
expect(isDate("2025-10-10T14:30:00-08:00")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Date parsing for all-day events", () => {
|
||||||
|
// Verify that date-only strings can be parsed as local dates
|
||||||
|
const dateStr = "2025-10-10";
|
||||||
|
const parsed = new Date(dateStr + "T00:00:00");
|
||||||
|
|
||||||
|
expect(parsed.getFullYear()).toBe(2025);
|
||||||
|
expect(parsed.getMonth()).toBe(9); // October (0-indexed)
|
||||||
|
expect(parsed.getDate()).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Timed events respect timezone conversion", () => {
|
||||||
|
// Verify that datetime strings with timezone info are properly converted with TZDate
|
||||||
|
const datetimeStr = "2025-10-10T14:30:00-07:00"; // 2:30 PM Pacific time
|
||||||
|
const timeZone = "America/Los_Angeles"; // UTC-7 (PDT) in October
|
||||||
|
|
||||||
|
// This should NOT be identified as a date-only string
|
||||||
|
expect(isDate(datetimeStr)).toBe(false);
|
||||||
|
|
||||||
|
// Timed events should use TZDate which respects timezone
|
||||||
|
const tzDate = new TZDate(datetimeStr, timeZone);
|
||||||
|
|
||||||
|
// The date should be October 10, 2:30 PM in LA timezone
|
||||||
|
expect(tzDate.getFullYear()).toBe(2025);
|
||||||
|
expect(tzDate.getMonth()).toBe(9); // October (0-indexed)
|
||||||
|
expect(tzDate.getDate()).toBe(10);
|
||||||
|
expect(tzDate.getHours()).toBe(14);
|
||||||
|
expect(tzDate.getMinutes()).toBe(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Timed events display different day due to timezone offset", () => {
|
||||||
|
// An event at 1 AM UTC on October 10 should display as October 9 in Pacific time
|
||||||
|
const utcDatetimeStr = "2025-10-10T01:00:00Z";
|
||||||
|
const timeZone = "America/Los_Angeles"; // UTC-7 (PDT) in October
|
||||||
|
|
||||||
|
// This should NOT be identified as a date-only string
|
||||||
|
expect(isDate(utcDatetimeStr)).toBe(false);
|
||||||
|
|
||||||
|
// Parse the UTC datetime in Pacific timezone
|
||||||
|
const tzDate = new TZDate(utcDatetimeStr, timeZone);
|
||||||
|
|
||||||
|
// Due to the -7 hour offset, 1 AM UTC becomes 6 PM on the previous day in Pacific
|
||||||
|
expect(tzDate.getFullYear()).toBe(2025);
|
||||||
|
expect(tzDate.getMonth()).toBe(9); // October (0-indexed)
|
||||||
|
expect(tzDate.getDate()).toBe(9); // Previous day
|
||||||
|
expect(tzDate.getHours()).toBe(18); // 6 PM
|
||||||
|
expect(tzDate.getMinutes()).toBe(0);
|
||||||
|
});
|
||||||
126
yarn.lock
126
yarn.lock
@@ -2277,12 +2277,12 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@lezer/highlight@npm:1.2.2, @lezer/highlight@npm:^1.0.0":
|
"@lezer/highlight@npm:1.2.3, @lezer/highlight@npm:^1.0.0":
|
||||||
version: 1.2.2
|
version: 1.2.3
|
||||||
resolution: "@lezer/highlight@npm:1.2.2"
|
resolution: "@lezer/highlight@npm:1.2.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@lezer/common": "npm:^1.3.0"
|
"@lezer/common": "npm:^1.3.0"
|
||||||
checksum: 10/73cb339de042b354cbc0b9e83978a91d2448435edae865a192cfc50d536e0b7d2e3cd563aabeb59eb6c86b0c38b3edc6f2871da8482c5dd8dca4a0899e743f7f
|
checksum: 10/8f787d464f8a036f117a0b23e73ac034d224a57d72501c6559089098a28f127c9e495b90ac7d132acc86199e0b64d4c038f75f9293a37c7c61add52fa1acdb4e
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -5376,12 +5376,12 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@vitest/coverage-v8@npm:4.0.2":
|
"@vitest/coverage-v8@npm:4.0.3":
|
||||||
version: 4.0.2
|
version: 4.0.3
|
||||||
resolution: "@vitest/coverage-v8@npm:4.0.2"
|
resolution: "@vitest/coverage-v8@npm:4.0.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@bcoe/v8-coverage": "npm:^1.0.2"
|
"@bcoe/v8-coverage": "npm:^1.0.2"
|
||||||
"@vitest/utils": "npm:4.0.2"
|
"@vitest/utils": "npm:4.0.3"
|
||||||
ast-v8-to-istanbul: "npm:^0.3.5"
|
ast-v8-to-istanbul: "npm:^0.3.5"
|
||||||
debug: "npm:^4.4.3"
|
debug: "npm:^4.4.3"
|
||||||
istanbul-lib-coverage: "npm:^3.2.2"
|
istanbul-lib-coverage: "npm:^3.2.2"
|
||||||
@@ -5392,34 +5392,34 @@ __metadata:
|
|||||||
std-env: "npm:^3.9.0"
|
std-env: "npm:^3.9.0"
|
||||||
tinyrainbow: "npm:^3.0.3"
|
tinyrainbow: "npm:^3.0.3"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
"@vitest/browser": 4.0.2
|
"@vitest/browser": 4.0.3
|
||||||
vitest: 4.0.2
|
vitest: 4.0.3
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
"@vitest/browser":
|
"@vitest/browser":
|
||||||
optional: true
|
optional: true
|
||||||
checksum: 10/467279d5e2113ca8d9a47ff8576b24bfc890110451ece29fe1c539a1ca8e789e113ff6ac5a282bdfbc1d98f19f409f328c9ed38e5b8b1afd8dada1e97c235a30
|
checksum: 10/8051dc457f74f9dbd912be9805fc3c6c1a965e3c86d88a6f4b2d4b7e38511dcce689447adb92235c33bec8fddcd0ede8e81f6bdf33eedc9ff00070b28a474093
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@vitest/expect@npm:4.0.2":
|
"@vitest/expect@npm:4.0.3":
|
||||||
version: 4.0.2
|
version: 4.0.3
|
||||||
resolution: "@vitest/expect@npm:4.0.2"
|
resolution: "@vitest/expect@npm:4.0.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@standard-schema/spec": "npm:^1.0.0"
|
"@standard-schema/spec": "npm:^1.0.0"
|
||||||
"@types/chai": "npm:^5.2.2"
|
"@types/chai": "npm:^5.2.2"
|
||||||
"@vitest/spy": "npm:4.0.2"
|
"@vitest/spy": "npm:4.0.3"
|
||||||
"@vitest/utils": "npm:4.0.2"
|
"@vitest/utils": "npm:4.0.3"
|
||||||
chai: "npm:^6.0.1"
|
chai: "npm:^6.0.1"
|
||||||
tinyrainbow: "npm:^3.0.3"
|
tinyrainbow: "npm:^3.0.3"
|
||||||
checksum: 10/6661bf2154a5eda81385e93774546aca545a7b34fe1d20e2e9ffe3fddbde73f27ff3fc8e24e362a8ed43d8772f4dbcde389b0e951a917196709d874f197d17e4
|
checksum: 10/663936d8f3abd91cb9725196ec542d109d7c64ddcdb6a483d89c9d67aa78a8ddd4468348c54b69f9c801fc3add9a8ae35dd0491c9f2bd19ec17b9b7a9ebf0d82
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@vitest/mocker@npm:4.0.2":
|
"@vitest/mocker@npm:4.0.3":
|
||||||
version: 4.0.2
|
version: 4.0.3
|
||||||
resolution: "@vitest/mocker@npm:4.0.2"
|
resolution: "@vitest/mocker@npm:4.0.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@vitest/spy": "npm:4.0.2"
|
"@vitest/spy": "npm:4.0.3"
|
||||||
estree-walker: "npm:^3.0.3"
|
estree-walker: "npm:^3.0.3"
|
||||||
magic-string: "npm:^0.30.19"
|
magic-string: "npm:^0.30.19"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -5430,54 +5430,54 @@ __metadata:
|
|||||||
optional: true
|
optional: true
|
||||||
vite:
|
vite:
|
||||||
optional: true
|
optional: true
|
||||||
checksum: 10/b5b98b996896b2bf8af858ee34fb32d5e3d690e51071983e4b4e4a988d706c969dbeb0e0fc90e68bb52c759558e1417561348e7bfa1ee4de61f4e4fd5f38d2d6
|
checksum: 10/933cab25563f68335a9871a6deba8f886f6be155c4a2146ee2b3b625578a0b4e068a4a26cf1a8d4ba3b5eb34771276f0365e51320fd06ad3f3f19163c5521d77
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@vitest/pretty-format@npm:4.0.2":
|
"@vitest/pretty-format@npm:4.0.3":
|
||||||
version: 4.0.2
|
version: 4.0.3
|
||||||
resolution: "@vitest/pretty-format@npm:4.0.2"
|
resolution: "@vitest/pretty-format@npm:4.0.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
tinyrainbow: "npm:^3.0.3"
|
tinyrainbow: "npm:^3.0.3"
|
||||||
checksum: 10/73ccc8cf4d8edca0e3261a4ebb22ab0b29600efc9c13fe98e3d9ed54516bb130bde1a39463066bb25093d37c9b4eacb619b56560ff0ef29ee304b054a9613836
|
checksum: 10/1b1197e53e5bcf9f77c842005ff11068f754b87286c6a7669b78c08a05bdbaa5cf4c7326c3b13347b02341b084bf97992c3fe89ea98fb77019e28fd96bc4c5b4
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@vitest/runner@npm:4.0.2":
|
"@vitest/runner@npm:4.0.3":
|
||||||
version: 4.0.2
|
version: 4.0.3
|
||||||
resolution: "@vitest/runner@npm:4.0.2"
|
resolution: "@vitest/runner@npm:4.0.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@vitest/utils": "npm:4.0.2"
|
"@vitest/utils": "npm:4.0.3"
|
||||||
pathe: "npm:^2.0.3"
|
pathe: "npm:^2.0.3"
|
||||||
checksum: 10/9533f9c71fbe352076454822139719d31188e4b01baee14c524f079af0230498ecc898398057a4e40848bfc88ab0192ad034e92c520bd7bb5f736e7ccfaed247
|
checksum: 10/a028898045cedac1939cc1adeff8fe36cbba2714d08e8524c8028b6fef7d617440bf0dfd72f1e264e8bff876979c49923bf268cb5920305a0ca9562a8318a80c
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@vitest/snapshot@npm:4.0.2":
|
"@vitest/snapshot@npm:4.0.3":
|
||||||
version: 4.0.2
|
version: 4.0.3
|
||||||
resolution: "@vitest/snapshot@npm:4.0.2"
|
resolution: "@vitest/snapshot@npm:4.0.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@vitest/pretty-format": "npm:4.0.2"
|
"@vitest/pretty-format": "npm:4.0.3"
|
||||||
magic-string: "npm:^0.30.19"
|
magic-string: "npm:^0.30.19"
|
||||||
pathe: "npm:^2.0.3"
|
pathe: "npm:^2.0.3"
|
||||||
checksum: 10/040c993bd1e1bb97315d226e1926f5143f4324fea5fed21cd17131ea901d910e8931e8c3f25103707b26ba76307cc085ec75de179a6a88dfe7ddc20cce74d50f
|
checksum: 10/38d0707ad66b33987c4066ee713f22d4535712ca016cece007c84736fa543d3ad3a314e759632b63e369e6a5454b03b142f66f5d661ac81ce7c4f1d6f6d325f4
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@vitest/spy@npm:4.0.2":
|
"@vitest/spy@npm:4.0.3":
|
||||||
version: 4.0.2
|
version: 4.0.3
|
||||||
resolution: "@vitest/spy@npm:4.0.2"
|
resolution: "@vitest/spy@npm:4.0.3"
|
||||||
checksum: 10/abc986536e1e5ef0dc098b349c2a8f8d4d74fdb0147bb8db534bcc9f35519da79ba6382e0a9f9efc3131da61e4a8a1efbfd6ee97cc4eaf32cad9269bc0c8cbfd
|
checksum: 10/4fc8e3aae425fdbbe96126291079f67f1e6be9545ffbaab7a31de8d6e6825b115eb3fafbf24167eab91dbbb4ed6fcd120c387116f181de1e3369e8d5fdd75f17
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@vitest/utils@npm:4.0.2":
|
"@vitest/utils@npm:4.0.3":
|
||||||
version: 4.0.2
|
version: 4.0.3
|
||||||
resolution: "@vitest/utils@npm:4.0.2"
|
resolution: "@vitest/utils@npm:4.0.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@vitest/pretty-format": "npm:4.0.2"
|
"@vitest/pretty-format": "npm:4.0.3"
|
||||||
tinyrainbow: "npm:^3.0.3"
|
tinyrainbow: "npm:^3.0.3"
|
||||||
checksum: 10/8b452cf9d981cdc635d3e43cecb6b8de3453865634f7b49b0958d6c08c63cff7a82f369ffd7813264d8d48515a42972094394b87b4fb6d7ad131b5387c600664
|
checksum: 10/d4ddb293e908d43b954c5e41f351a61f719e30e9ea058e47af5b18389d74cb073ea1638ab28dbbd5b365ed31a21577bb090b8a3594292c59df18798fd292f002
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -9235,7 +9235,7 @@ __metadata:
|
|||||||
"@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.6"
|
"@home-assistant/webawesome": "npm:3.0.0-beta.6.ha.6"
|
||||||
"@lezer/highlight": "npm:1.2.2"
|
"@lezer/highlight": "npm:1.2.3"
|
||||||
"@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"
|
||||||
"@lit-labs/virtualizer": "npm:2.1.1"
|
"@lit-labs/virtualizer": "npm:2.1.1"
|
||||||
@@ -9299,7 +9299,7 @@ __metadata:
|
|||||||
"@vaadin/combo-box": "npm:24.9.2"
|
"@vaadin/combo-box": "npm:24.9.2"
|
||||||
"@vaadin/vaadin-themable-mixin": "npm:24.9.2"
|
"@vaadin/vaadin-themable-mixin": "npm:24.9.2"
|
||||||
"@vibrant/color": "npm:4.0.0"
|
"@vibrant/color": "npm:4.0.0"
|
||||||
"@vitest/coverage-v8": "npm:4.0.2"
|
"@vitest/coverage-v8": "npm:4.0.3"
|
||||||
"@vue/web-component-wrapper": "npm:1.3.0"
|
"@vue/web-component-wrapper": "npm:1.3.0"
|
||||||
"@webcomponents/scoped-custom-element-registry": "npm:0.0.10"
|
"@webcomponents/scoped-custom-element-registry": "npm:0.0.10"
|
||||||
"@webcomponents/webcomponentsjs": "npm:2.8.0"
|
"@webcomponents/webcomponentsjs": "npm:2.8.0"
|
||||||
@@ -9384,7 +9384,7 @@ __metadata:
|
|||||||
typescript-eslint: "npm:8.46.2"
|
typescript-eslint: "npm:8.46.2"
|
||||||
ua-parser-js: "npm:2.0.6"
|
ua-parser-js: "npm:2.0.6"
|
||||||
vite-tsconfig-paths: "npm:5.1.4"
|
vite-tsconfig-paths: "npm:5.1.4"
|
||||||
vitest: "npm:4.0.2"
|
vitest: "npm:4.0.3"
|
||||||
vue: "npm:2.7.16"
|
vue: "npm:2.7.16"
|
||||||
vue2-daterange-picker: "npm:0.6.8"
|
vue2-daterange-picker: "npm:0.6.8"
|
||||||
webpack-stats-plugin: "npm:1.1.3"
|
webpack-stats-plugin: "npm:1.1.3"
|
||||||
@@ -14766,17 +14766,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"vitest@npm:4.0.2":
|
"vitest@npm:4.0.3":
|
||||||
version: 4.0.2
|
version: 4.0.3
|
||||||
resolution: "vitest@npm:4.0.2"
|
resolution: "vitest@npm:4.0.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@vitest/expect": "npm:4.0.2"
|
"@vitest/expect": "npm:4.0.3"
|
||||||
"@vitest/mocker": "npm:4.0.2"
|
"@vitest/mocker": "npm:4.0.3"
|
||||||
"@vitest/pretty-format": "npm:4.0.2"
|
"@vitest/pretty-format": "npm:4.0.3"
|
||||||
"@vitest/runner": "npm:4.0.2"
|
"@vitest/runner": "npm:4.0.3"
|
||||||
"@vitest/snapshot": "npm:4.0.2"
|
"@vitest/snapshot": "npm:4.0.3"
|
||||||
"@vitest/spy": "npm:4.0.2"
|
"@vitest/spy": "npm:4.0.3"
|
||||||
"@vitest/utils": "npm:4.0.2"
|
"@vitest/utils": "npm:4.0.3"
|
||||||
debug: "npm:^4.4.3"
|
debug: "npm:^4.4.3"
|
||||||
es-module-lexer: "npm:^1.7.0"
|
es-module-lexer: "npm:^1.7.0"
|
||||||
expect-type: "npm:^1.2.2"
|
expect-type: "npm:^1.2.2"
|
||||||
@@ -14794,10 +14794,10 @@ __metadata:
|
|||||||
"@edge-runtime/vm": "*"
|
"@edge-runtime/vm": "*"
|
||||||
"@types/debug": ^4.1.12
|
"@types/debug": ^4.1.12
|
||||||
"@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0
|
"@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0
|
||||||
"@vitest/browser-playwright": 4.0.2
|
"@vitest/browser-playwright": 4.0.3
|
||||||
"@vitest/browser-preview": 4.0.2
|
"@vitest/browser-preview": 4.0.3
|
||||||
"@vitest/browser-webdriverio": 4.0.2
|
"@vitest/browser-webdriverio": 4.0.3
|
||||||
"@vitest/ui": 4.0.2
|
"@vitest/ui": 4.0.3
|
||||||
happy-dom: "*"
|
happy-dom: "*"
|
||||||
jsdom: "*"
|
jsdom: "*"
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
@@ -14821,7 +14821,7 @@ __metadata:
|
|||||||
optional: true
|
optional: true
|
||||||
bin:
|
bin:
|
||||||
vitest: vitest.mjs
|
vitest: vitest.mjs
|
||||||
checksum: 10/523ea3ff5b14a6fe886b530f66b6a450af885c2e9688740f6143d2885d7572518e78a0c3d7d1de972674856748ccd821c8ca677dfb96c9c773903ab783cb26d4
|
checksum: 10/535ef75a39d5d3233eeb1050a09cd9b3c9353daad610a442aec16ef657887c16d4a6264d37a4181d487cd07cbb4b2e763ce74b1df037b2850a184983545f3db6
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user