mirror of
https://github.com/home-assistant/frontend.git
synced 2026-01-15 03:37:28 +00:00
Compare commits
12 Commits
entity-nam
...
clock-date
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e6fb3c103 | ||
|
|
4b8cf9a056 | ||
|
|
bcac688538 | ||
|
|
616209c0f0 | ||
|
|
21ba3952d9 | ||
|
|
7392e05230 | ||
|
|
74de8365eb | ||
|
|
999d54147d | ||
|
|
721d0237ac | ||
|
|
a3ad223230 | ||
|
|
69290a2ffb | ||
|
|
c840af63f5 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -15,7 +15,7 @@ dist/
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
.pnp.*
|
||||
node_modules/
|
||||
/node_modules/
|
||||
yarn-error.log
|
||||
npm-debug.log
|
||||
|
||||
|
||||
@@ -236,6 +236,6 @@
|
||||
},
|
||||
"packageManager": "yarn@4.12.0",
|
||||
"volta": {
|
||||
"node": "24.13.0"
|
||||
"node": "24.12.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
// From https://github.com/epoberezkin/fast-deep-equal
|
||||
// MIT License - Copyright (c) 2017 Evgeny Poberezkin
|
||||
|
||||
interface DeepEqualOptions {
|
||||
/** Compare Symbol properties in addition to string keys */
|
||||
compareSymbols?: boolean;
|
||||
}
|
||||
|
||||
export const deepEqual = (
|
||||
a: any,
|
||||
b: any,
|
||||
options?: DeepEqualOptions
|
||||
): boolean => {
|
||||
export const deepEqual = (a: any, b: any): boolean => {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
@@ -28,7 +18,7 @@ export const deepEqual = (
|
||||
return false;
|
||||
}
|
||||
for (i = length; i-- !== 0; ) {
|
||||
if (!deepEqual(a[i], b[i], options)) {
|
||||
if (!deepEqual(a[i], b[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -45,7 +35,7 @@ export const deepEqual = (
|
||||
}
|
||||
}
|
||||
for (i of a.entries()) {
|
||||
if (!deepEqual(i[1], b.get(i[0]), options)) {
|
||||
if (!deepEqual(i[1], b.get(i[0]))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -103,28 +93,11 @@ export const deepEqual = (
|
||||
for (i = length; i-- !== 0; ) {
|
||||
const key = keys[i];
|
||||
|
||||
if (!deepEqual(a[key], b[key], options)) {
|
||||
if (!deepEqual(a[key], b[key])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Compare Symbol properties if requested
|
||||
if (options?.compareSymbols) {
|
||||
const symbolsA = Object.getOwnPropertySymbols(a);
|
||||
const symbolsB = Object.getOwnPropertySymbols(b);
|
||||
if (symbolsA.length !== symbolsB.length) {
|
||||
return false;
|
||||
}
|
||||
for (const sym of symbolsA) {
|
||||
if (!Object.prototype.hasOwnProperty.call(b, sym)) {
|
||||
return false;
|
||||
}
|
||||
if (!deepEqual(a[sym], b[sym], options)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import { fullEntitiesContext } from "../../data/context";
|
||||
import type { DeviceAutomation } from "../../data/device/device_automation";
|
||||
import {
|
||||
@@ -12,12 +11,11 @@ import {
|
||||
} from "../../data/device/device_automation";
|
||||
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-generic-picker";
|
||||
import "../ha-md-select";
|
||||
import "../ha-md-select-option";
|
||||
import type { PickerValueRenderer } from "../ha-picker-field";
|
||||
|
||||
const NO_AUTOMATION_KEY = "NO_AUTOMATION";
|
||||
const UNKNOWN_AUTOMATION_KEY = "UNKNOWN_AUTOMATION";
|
||||
|
||||
export abstract class HaDeviceAutomationPicker<
|
||||
T extends DeviceAutomation,
|
||||
@@ -30,7 +28,7 @@ export abstract class HaDeviceAutomationPicker<
|
||||
|
||||
@property({ type: Object }) public value?: T;
|
||||
|
||||
@state() private _automations?: T[];
|
||||
@state() private _automations: T[] = [];
|
||||
|
||||
// Trigger an empty render so we start with a clean DOM.
|
||||
// paper-listbox does not like changing things around.
|
||||
@@ -46,6 +44,12 @@ export abstract class HaDeviceAutomationPicker<
|
||||
);
|
||||
}
|
||||
|
||||
protected get UNKNOWN_AUTOMATION_TEXT() {
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.devices.automation.actions.unknown_action"
|
||||
);
|
||||
}
|
||||
|
||||
private _localizeDeviceAutomation: (
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
@@ -71,7 +75,7 @@ export abstract class HaDeviceAutomationPicker<
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
if (!this.value || !this._automations) {
|
||||
if (!this.value) {
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -84,7 +88,7 @@ export abstract class HaDeviceAutomationPicker<
|
||||
);
|
||||
|
||||
if (idx === -1) {
|
||||
return this.value.alias || this.value.type || "unknown";
|
||||
return UNKNOWN_AUTOMATION_KEY;
|
||||
}
|
||||
|
||||
return `${this._automations[idx].device_id}_${idx}`;
|
||||
@@ -95,21 +99,37 @@ export abstract class HaDeviceAutomationPicker<
|
||||
return nothing;
|
||||
}
|
||||
const value = this._value;
|
||||
|
||||
return html`<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.label}
|
||||
.value=${value}
|
||||
.disabled=${!this._automations || this._automations.length === 0}
|
||||
.getItems=${this._getItems(value, this._automations)}
|
||||
@value-changed=${this._automationChanged}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.unknownItemText=${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.actions.unknown_action"
|
||||
)}
|
||||
hide-clear-icon
|
||||
>
|
||||
</ha-generic-picker>`;
|
||||
return html`
|
||||
<ha-md-select
|
||||
.label=${this.label}
|
||||
.value=${value}
|
||||
@change=${this._automationChanged}
|
||||
@closed=${stopPropagation}
|
||||
.disabled=${this._automations.length === 0}
|
||||
>
|
||||
${value === NO_AUTOMATION_KEY
|
||||
? html`<ha-md-select-option .value=${NO_AUTOMATION_KEY}>
|
||||
${this.NO_AUTOMATION_TEXT}
|
||||
</ha-md-select-option>`
|
||||
: nothing}
|
||||
${value === UNKNOWN_AUTOMATION_KEY
|
||||
? html`<ha-md-select-option .value=${UNKNOWN_AUTOMATION_KEY}>
|
||||
${this.UNKNOWN_AUTOMATION_TEXT}
|
||||
</ha-md-select-option>`
|
||||
: nothing}
|
||||
${this._automations.map(
|
||||
(automation, idx) => html`
|
||||
<ha-md-select-option .value=${`${automation.device_id}_${idx}`}>
|
||||
${this._localizeDeviceAutomation(
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
automation
|
||||
)}
|
||||
</ha-md-select-option>
|
||||
`
|
||||
)}
|
||||
</ha-md-select>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps) {
|
||||
@@ -120,57 +140,6 @@ export abstract class HaDeviceAutomationPicker<
|
||||
}
|
||||
}
|
||||
|
||||
private _getItems = memoizeOne(
|
||||
(value: string, automations: T[] | undefined) => {
|
||||
if (!automations) {
|
||||
return () => undefined;
|
||||
}
|
||||
|
||||
const automationListItems = automations.map((automation, idx) => {
|
||||
const primary = this._localizeDeviceAutomation(
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
automation
|
||||
);
|
||||
return {
|
||||
id: `${automation.device_id}_${idx}`,
|
||||
primary,
|
||||
};
|
||||
});
|
||||
|
||||
automationListItems.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(
|
||||
a.primary,
|
||||
b.primary,
|
||||
this.hass.locale.language
|
||||
)
|
||||
);
|
||||
|
||||
if (value === NO_AUTOMATION_KEY) {
|
||||
automationListItems.unshift({
|
||||
id: NO_AUTOMATION_KEY,
|
||||
primary: this.NO_AUTOMATION_TEXT,
|
||||
});
|
||||
}
|
||||
|
||||
return () => automationListItems;
|
||||
}
|
||||
);
|
||||
|
||||
private _valueRenderer: PickerValueRenderer = (value: string) => {
|
||||
const automation = this._automations?.find(
|
||||
(a, idx) => value === `${a.device_id}_${idx}`
|
||||
);
|
||||
|
||||
const text = automation
|
||||
? this._localizeDeviceAutomation(this.hass, this._entityReg, automation)
|
||||
: value === NO_AUTOMATION_KEY
|
||||
? this.NO_AUTOMATION_TEXT
|
||||
: value;
|
||||
|
||||
return html`<span slot="headline">${text}</span>`;
|
||||
};
|
||||
|
||||
private async _updateDeviceInfo() {
|
||||
this._automations = this.deviceId
|
||||
? (await this._fetchDeviceAutomations(this.hass, this.deviceId)).sort(
|
||||
@@ -192,14 +161,13 @@ export abstract class HaDeviceAutomationPicker<
|
||||
this._renderEmpty = false;
|
||||
}
|
||||
|
||||
private _automationChanged(ev: CustomEvent<{ value: string }>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
if (!value || NO_AUTOMATION_KEY === value) {
|
||||
private _automationChanged(ev) {
|
||||
const value = ev.target.value;
|
||||
if (!value || [UNKNOWN_AUTOMATION_KEY, NO_AUTOMATION_KEY].includes(value)) {
|
||||
return;
|
||||
}
|
||||
const [deviceId, idx] = value.split("_");
|
||||
const automation = this._automations![idx];
|
||||
const automation = this._automations[idx];
|
||||
if (automation.device_id !== deviceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -255,7 +255,6 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
...this._loadedCodeMirror.tabKeyBindings,
|
||||
saveKeyBinding,
|
||||
]),
|
||||
this._loadedCodeMirror.search({ top: true }),
|
||||
this._loadedCodeMirror.langCompartment.of(this._mode),
|
||||
this._loadedCodeMirror.haTheme,
|
||||
this._loadedCodeMirror.haSyntaxHighlighting,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mdiPlus } from "@mdi/js";
|
||||
import { mdiLabel, mdiPlus } from "@mdi/js";
|
||||
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
@@ -25,9 +25,11 @@ import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-generic-picker";
|
||||
import type { HaGenericPicker } from "./ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||
import type { PickerValueRenderer } from "./ha-picker-field";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
const ADD_NEW_ID = "___ADD_NEW___";
|
||||
const NO_LABELS = "___NO_LABELS___";
|
||||
|
||||
@customElement("ha-label-picker")
|
||||
export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
@@ -106,10 +108,52 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
];
|
||||
}
|
||||
|
||||
private _labelMap = memoizeOne(
|
||||
(
|
||||
labels: LabelRegistryEntry[] | undefined
|
||||
): Map<string, LabelRegistryEntry> => {
|
||||
if (!labels) {
|
||||
return new Map();
|
||||
}
|
||||
return new Map(labels.map((label) => [label.label_id, label]));
|
||||
}
|
||||
);
|
||||
|
||||
private _computeValueRenderer = memoizeOne(
|
||||
(labels: LabelRegistryEntry[] | undefined): PickerValueRenderer =>
|
||||
(value) => {
|
||||
const label = this._labelMap(labels).get(value);
|
||||
|
||||
if (!label) {
|
||||
return html`
|
||||
<ha-svg-icon slot="start" .path=${mdiLabel}></ha-svg-icon>
|
||||
<span slot="headline">${value}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
${label.icon
|
||||
? html`<ha-icon slot="start" .icon=${label.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon slot="start" .path=${mdiLabel}></ha-svg-icon>`}
|
||||
<span slot="headline">${label.name}</span>
|
||||
`;
|
||||
}
|
||||
);
|
||||
|
||||
private _getLabelsMemoized = memoizeOne(getLabels);
|
||||
|
||||
private _getItems = () =>
|
||||
this._getLabelsMemoized(
|
||||
private _getItems = () => {
|
||||
if (!this._labels || this._labels.length === 0) {
|
||||
return [
|
||||
{
|
||||
id: NO_LABELS,
|
||||
primary: this.hass.localize("ui.components.label-picker.no_labels"),
|
||||
icon_path: mdiLabel,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return this._getLabelsMemoized(
|
||||
this.hass.states,
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
@@ -122,6 +166,7 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
this.entityFilter,
|
||||
this.excludeLabels
|
||||
);
|
||||
};
|
||||
|
||||
private _allLabelNames = memoizeOne((labels?: LabelRegistryEntry[]) => {
|
||||
if (!labels) {
|
||||
@@ -174,6 +219,8 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
this.placeholder ??
|
||||
this.hass.localize("ui.components.label-picker.label");
|
||||
|
||||
const valueRenderer = this._computeValueRenderer(this._labels);
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.disabled=${this.disabled}
|
||||
@@ -190,6 +237,7 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
.value=${this.value}
|
||||
.getItems=${this._getItems}
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
.valueRenderer=${valueRenderer}
|
||||
.searchKeys=${labelComboBoxKeys}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
@@ -203,6 +251,10 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
const value = ev.detail.value;
|
||||
|
||||
if (value === NO_LABELS) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
this._setValue(undefined);
|
||||
return;
|
||||
|
||||
@@ -377,7 +377,7 @@ interface SelectBoxOptionImage {
|
||||
}
|
||||
|
||||
export interface SelectOption {
|
||||
value: string;
|
||||
value: any;
|
||||
label: string;
|
||||
description?: string;
|
||||
image?: string | SelectBoxOptionImage;
|
||||
|
||||
@@ -58,7 +58,6 @@ import { fullEntitiesContext } from "../../../../data/context";
|
||||
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
|
||||
import type {
|
||||
Action,
|
||||
DeviceAction,
|
||||
NonConditionAction,
|
||||
RepeatAction,
|
||||
ServiceAction,
|
||||
@@ -234,13 +233,6 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
private _renderRow() {
|
||||
const type = getAutomationActionType(this.action);
|
||||
|
||||
const target =
|
||||
type === "service" && "target" in this.action
|
||||
? (this.action as ServiceAction).target
|
||||
: type === "device_id" && (this.action as DeviceAction).device_id
|
||||
? { device_id: (this.action as DeviceAction).device_id }
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
${type === "service" && "action" in this.action && this.action.action
|
||||
? html`
|
||||
@@ -262,7 +254,9 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
${capitalizeFirstLetter(
|
||||
describeAction(this.hass, this._entityReg, this.action)
|
||||
)}
|
||||
${target ? this._renderTargets(target) : nothing}
|
||||
${type === "service" && "target" in this.action
|
||||
? this._renderTargets((this.action as ServiceAction).target)
|
||||
: nothing}
|
||||
</h3>
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
|
||||
@@ -1504,7 +1504,14 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
box-shadow: inset var(--ha-shadow-offset-x-lg)
|
||||
calc(var(--ha-shadow-offset-y-lg) * -1) var(--ha-shadow-blur-lg)
|
||||
var(--ha-shadow-spread-lg) var(--ha-color-shadow-light);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.targets-show-more {
|
||||
box-shadow: inset var(--ha-shadow-offset-x-lg)
|
||||
calc(var(--ha-shadow-offset-y-lg) * -1) var(--ha-shadow-blur-lg)
|
||||
var(--ha-shadow-spread-lg) var(--ha-color-shadow-dark);
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px), all and (max-height: 500px) {
|
||||
|
||||
@@ -76,7 +76,6 @@ import "./types/ha-automation-condition-template";
|
||||
import "./types/ha-automation-condition-time";
|
||||
import "./types/ha-automation-condition-trigger";
|
||||
import "./types/ha-automation-condition-zone";
|
||||
import type { DeviceCondition } from "../../../../data/device/device_automation";
|
||||
|
||||
export interface ConditionElement extends LitElement {
|
||||
condition: Condition;
|
||||
@@ -185,14 +184,6 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
}
|
||||
|
||||
private _renderRow() {
|
||||
const target =
|
||||
"target" in (this.conditionDescriptions[this.condition.condition] || {})
|
||||
? (this.condition as PlatformCondition).target
|
||||
: "device_id" in this.condition &&
|
||||
(this.condition as DeviceCondition).device_id
|
||||
? { device_id: [(this.condition as DeviceCondition).device_id] }
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<ha-condition-icon
|
||||
slot="leading-icon"
|
||||
@@ -203,7 +194,10 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
${capitalizeFirstLetter(
|
||||
describeCondition(this.condition, this.hass, this._entityReg)
|
||||
)}
|
||||
${target ? this._renderTargets(target) : nothing}
|
||||
${"target" in
|
||||
(this.conditionDescriptions[this.condition.condition] || {})
|
||||
? this._renderTargets((this.condition as PlatformCondition).target)
|
||||
: nothing}
|
||||
</h3>
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
|
||||
@@ -56,7 +56,6 @@ import { isTrigger, subscribeTrigger } from "../../../../data/automation";
|
||||
import { describeTrigger } from "../../../../data/automation_i18n";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
import { fullEntitiesContext } from "../../../../data/context";
|
||||
import type { DeviceTrigger } from "../../../../data/device/device_automation";
|
||||
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
|
||||
import type { TriggerDescriptions } from "../../../../data/trigger";
|
||||
import { isTriggerList } from "../../../../data/trigger";
|
||||
@@ -197,15 +196,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
|
||||
const yamlMode = this._yamlMode || !supported;
|
||||
|
||||
const target =
|
||||
type === "platform" &&
|
||||
"target" in
|
||||
this.triggerDescriptions[(this.trigger as PlatformTrigger).trigger]
|
||||
? (this.trigger as PlatformTrigger).target
|
||||
: type === "device" && (this.trigger as DeviceTrigger).device_id
|
||||
? { device_id: (this.trigger as DeviceTrigger).device_id }
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
${type === "list"
|
||||
? html`<ha-svg-icon
|
||||
@@ -220,7 +210,11 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
></ha-trigger-icon>`}
|
||||
<h3 slot="header">
|
||||
${describeTrigger(this.trigger, this.hass, this._entityReg)}
|
||||
${target ? this._renderTargets(target) : nothing}
|
||||
${type === "platform" &&
|
||||
"target" in
|
||||
this.triggerDescriptions[(this.trigger as PlatformTrigger).trigger]
|
||||
? this._renderTargets((this.trigger as PlatformTrigger).target)
|
||||
: nothing}
|
||||
</h3>
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
|
||||
@@ -20,6 +20,7 @@ import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
import { showCategoryRegistryDetailDialog } from "./show-dialog-category-registry-detail";
|
||||
|
||||
const ADD_NEW_ID = "___ADD_NEW___";
|
||||
const NO_CATEGORIES_ID = "___NO_CATEGORIES___";
|
||||
|
||||
@customElement("ha-category-picker")
|
||||
export class HaCategoryPicker extends SubscribeMixin(LitElement) {
|
||||
@@ -100,11 +101,17 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
|
||||
private _getCategories = memoizeOne(
|
||||
(
|
||||
categories: CategoryRegistryEntry[] | undefined
|
||||
): PickerComboBoxItem[] | undefined => {
|
||||
if (!categories) {
|
||||
return undefined;
|
||||
(categories: CategoryRegistryEntry[] | undefined): PickerComboBoxItem[] => {
|
||||
if (!categories || categories.length === 0) {
|
||||
return [
|
||||
{
|
||||
id: NO_CATEGORIES_ID,
|
||||
primary: this.hass.localize(
|
||||
"ui.components.category-picker.no_categories"
|
||||
),
|
||||
icon_path: mdiTag,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const items = categories.map<PickerComboBoxItem>((category) => ({
|
||||
@@ -203,6 +210,10 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
const value = ev.detail.value;
|
||||
|
||||
if (value === NO_CATEGORIES_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
this._setValue(undefined);
|
||||
return;
|
||||
|
||||
@@ -8,8 +8,6 @@ import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { computeEntityNameList } from "../../../common/entity/compute_entity_name_display";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import "../../../components/ha-check-list-item";
|
||||
import "../../../components/search-input";
|
||||
import "../../../components/ha-dialog";
|
||||
@@ -20,7 +18,6 @@ import "../../../components/ha-list";
|
||||
import type { ExposeEntitySettings } from "../../../data/expose";
|
||||
import { voiceAssistants } from "../../../data/expose";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { loadVirtualizer } from "../../../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "./entity-voice-settings";
|
||||
import type { ExposeEntityDialogParams } from "./show-dialog-expose-entity";
|
||||
@@ -35,12 +32,6 @@ class DialogExposeEntity extends LitElement {
|
||||
|
||||
@state() private _selected: string[] = [];
|
||||
|
||||
public willUpdate(): void {
|
||||
if (!this.hasUpdated) {
|
||||
loadVirtualizer();
|
||||
}
|
||||
}
|
||||
|
||||
public async showDialog(params: ExposeEntityDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
}
|
||||
@@ -152,96 +143,36 @@ class DialogExposeEntity extends LitElement {
|
||||
filter?: string
|
||||
) => {
|
||||
const lowerFilter = filter?.toLowerCase();
|
||||
return Object.values(this.hass.states).filter((entity) => {
|
||||
if (
|
||||
!this._params!.filterAssistants.some(
|
||||
return Object.values(this.hass.states).filter(
|
||||
(entity) =>
|
||||
this._params!.filterAssistants.some(
|
||||
(ass) => !exposedEntities[entity.entity_id]?.[ass]
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!lowerFilter) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (entity.entity_id.toLowerCase().includes(lowerFilter)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const entityName = computeStateName(entity);
|
||||
if (entityName?.toLowerCase().includes(lowerFilter)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const [, deviceName, areaName] = computeEntityNameList(
|
||||
entity,
|
||||
[{ type: "entity" }, { type: "device" }, { type: "area" }],
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
|
||||
if (deviceName?.toLowerCase().includes(lowerFilter)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (areaName?.toLowerCase().includes(lowerFilter)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
) &&
|
||||
(!lowerFilter ||
|
||||
entity.entity_id.toLowerCase().includes(lowerFilter) ||
|
||||
computeStateName(entity)?.toLowerCase().includes(lowerFilter))
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
private _renderItem = (entityState: HassEntity) => {
|
||||
const [entityName, deviceName, areaName] = computeEntityNameList(
|
||||
entityState,
|
||||
[{ type: "entity" }, { type: "device" }, { type: "area" }],
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
const primary = entityName || deviceName || entityState.entity_id;
|
||||
const context = [areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
const showEntityId = this.hass.userData?.showEntityIdPicker;
|
||||
|
||||
return html`
|
||||
<ha-check-list-item
|
||||
graphic="icon"
|
||||
?twoLine=${context}
|
||||
?threeLine=${showEntityId}
|
||||
.value=${entityState.entity_id}
|
||||
.selected=${this._selected.includes(entityState.entity_id)}
|
||||
@request-selected=${this._handleSelected}
|
||||
>
|
||||
<ha-state-icon
|
||||
title=${ifDefined(entityState?.state)}
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${entityState}
|
||||
></ha-state-icon>
|
||||
${primary}
|
||||
${context || showEntityId
|
||||
? html`<span slot="secondary">
|
||||
${context}
|
||||
${showEntityId
|
||||
? html`<br /><span class="entity-id"
|
||||
>${entityState.entity_id}</span
|
||||
>`
|
||||
: nothing}
|
||||
</span>`
|
||||
: nothing}
|
||||
</ha-check-list-item>
|
||||
`;
|
||||
};
|
||||
private _renderItem = (entityState: HassEntity) => html`
|
||||
<ha-check-list-item
|
||||
graphic="icon"
|
||||
twoLine
|
||||
.value=${entityState.entity_id}
|
||||
.selected=${this._selected.includes(entityState.entity_id)}
|
||||
@request-selected=${this._handleSelected}
|
||||
>
|
||||
<ha-state-icon
|
||||
title=${ifDefined(entityState?.state)}
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${entityState}
|
||||
></ha-state-icon>
|
||||
${computeStateName(entityState)}
|
||||
<span slot="secondary">${entityState.entity_id}</span>
|
||||
</ha-check-list-item>
|
||||
`;
|
||||
|
||||
private _expose() {
|
||||
this._params!.exposeEntities(this._selected);
|
||||
@@ -267,7 +198,6 @@ class DialogExposeEntity extends LitElement {
|
||||
width: 100%;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
margin-top: var(--ha-space-2);
|
||||
--text-field-suffix-padding-left: 8px;
|
||||
}
|
||||
.header {
|
||||
@@ -280,7 +210,7 @@ class DialogExposeEntity extends LitElement {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: calc(var(--ha-space-1) * -1) 0;
|
||||
margin: -4px 0;
|
||||
}
|
||||
.subtitle {
|
||||
color: var(--secondary-text-color);
|
||||
@@ -295,17 +225,9 @@ class DialogExposeEntity extends LitElement {
|
||||
width: 100%;
|
||||
height: 72px;
|
||||
}
|
||||
ha-check-list-item[threeLine] {
|
||||
height: 88px;
|
||||
}
|
||||
ha-check-list-item .entity-id {
|
||||
line-height: var(--ha-line-height-normal);
|
||||
padding-left: var(--ha-space-1);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
}
|
||||
ha-check-list-item ha-state-icon {
|
||||
margin-left: var(--ha-space-6);
|
||||
margin-inline-start: var(--ha-space-6);
|
||||
margin-left: 24px;
|
||||
margin-inline-start: 24px;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
@media all and (max-height: 800px) {
|
||||
@@ -340,8 +262,8 @@ class DialogExposeEntity extends LitElement {
|
||||
--text-field-suffix-padding-left: unset;
|
||||
}
|
||||
ha-check-list-item ha-state-icon {
|
||||
margin-left: var(--ha-space-2);
|
||||
margin-inline-start: var(--ha-space-2);
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,21 +167,20 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
flex: 2,
|
||||
template: narrow
|
||||
? undefined
|
||||
: (entry) => html`
|
||||
${entry.name}<br />
|
||||
<div class="secondary">${entry.entity_id}</div>
|
||||
`,
|
||||
},
|
||||
area: {
|
||||
title: localize("ui.panel.config.voice_assistants.expose.headers.area"),
|
||||
sortable: true,
|
||||
groupable: true,
|
||||
filterable: true,
|
||||
template: (entry) => entry.area || "—",
|
||||
},
|
||||
// For search & narrow
|
||||
entity_id: {
|
||||
title: localize(
|
||||
"ui.panel.config.voice_assistants.expose.headers.entity_id"
|
||||
),
|
||||
sortable: true,
|
||||
hidden: !narrow,
|
||||
filterable: true,
|
||||
defaultHidden: true,
|
||||
},
|
||||
domain: {
|
||||
title: localize(
|
||||
@@ -192,6 +191,13 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
},
|
||||
area: {
|
||||
title: localize("ui.panel.config.voice_assistants.expose.headers.area"),
|
||||
sortable: true,
|
||||
groupable: true,
|
||||
filterable: true,
|
||||
template: (entry) => entry.area || "—",
|
||||
},
|
||||
assistants: {
|
||||
title: localize(
|
||||
"ui.panel.config.voice_assistants.expose.headers.assistants"
|
||||
@@ -813,8 +819,8 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
}
|
||||
.selected-txt {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
padding-left: var(--ha-space-4);
|
||||
padding-inline-start: var(--ha-space-4);
|
||||
padding-left: 16px;
|
||||
padding-inline-start: 16px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.table-header .selected-txt {
|
||||
@@ -824,8 +830,8 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
.header-toolbar .header-btns {
|
||||
margin-right: calc(var(--ha-space-3) * -1);
|
||||
margin-inline-end: calc(var(--ha-space-3) * -1);
|
||||
margin-right: -12px;
|
||||
margin-inline-end: -12px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.header-btns {
|
||||
@@ -833,17 +839,17 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
}
|
||||
.header-btns > ha-button,
|
||||
.header-btns > ha-icon-button {
|
||||
margin: var(--ha-space-2);
|
||||
margin: 8px;
|
||||
}
|
||||
ha-button-menu {
|
||||
margin-left: var(--ha-space-2);
|
||||
margin-inline-start: var(--ha-space-2);
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
.clear {
|
||||
color: var(--primary-color);
|
||||
padding-left: var(--ha-space-2);
|
||||
padding-inline-start: var(--ha-space-2);
|
||||
padding-left: 8px;
|
||||
padding-inline-start: 8px;
|
||||
text-transform: uppercase;
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { resolveTimeZone } from "../../../../common/datetime/resolve-time-zone";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { ClockCardConfig } from "../types";
|
||||
@@ -26,6 +28,11 @@ function romanize12HourClock(num: number) {
|
||||
return numerals[num];
|
||||
}
|
||||
|
||||
const INTERVAL = 1000;
|
||||
const QUARTER_TICKS = Array.from({ length: 4 }, (_, i) => i);
|
||||
const HOUR_TICKS = Array.from({ length: 12 }, (_, i) => i);
|
||||
const MINUTE_TICKS = Array.from({ length: 60 }, (_, i) => i);
|
||||
|
||||
@customElement("hui-clock-card-analog")
|
||||
export class HuiClockCardAnalog extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@@ -40,27 +47,32 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
|
||||
@state() private _secondOffsetSec?: number;
|
||||
|
||||
private _initDate() {
|
||||
if (!this.config || !this.hass) {
|
||||
return;
|
||||
@state() private _year = "";
|
||||
|
||||
@state() private _month = "";
|
||||
|
||||
@state() private _day = "";
|
||||
|
||||
private _tickInterval?: undefined | number;
|
||||
|
||||
private _currentDate = new Date();
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
document.addEventListener("visibilitychange", this._handleVisibilityChange);
|
||||
this._computeDateTime();
|
||||
if (this.config?.date && this.config.date !== "none") {
|
||||
this._startTick();
|
||||
}
|
||||
}
|
||||
|
||||
let locale = this.hass.locale;
|
||||
if (this.config.time_format) {
|
||||
locale = { ...locale, time_format: this.config.time_format };
|
||||
}
|
||||
|
||||
this._dateTimeFormat = new Intl.DateTimeFormat(this.hass.locale.language, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hourCycle: "h12",
|
||||
timeZone:
|
||||
this.config.time_zone ||
|
||||
resolveTimeZone(locale.time_zone, this.hass.config?.time_zone),
|
||||
});
|
||||
|
||||
this._computeOffsets();
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
document.removeEventListener(
|
||||
"visibilitychange",
|
||||
this._handleVisibilityChange
|
||||
);
|
||||
this._stopTick();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
@@ -72,30 +84,78 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
document.addEventListener("visibilitychange", this._handleVisibilityChange);
|
||||
this._computeOffsets();
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
document.removeEventListener(
|
||||
"visibilitychange",
|
||||
this._handleVisibilityChange
|
||||
);
|
||||
}
|
||||
|
||||
private _handleVisibilityChange = () => {
|
||||
if (!document.hidden) {
|
||||
this._computeOffsets();
|
||||
this._computeDateTime();
|
||||
}
|
||||
};
|
||||
|
||||
private _computeOffsets() {
|
||||
private _initDate() {
|
||||
if (!this.config || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
let locale = this.hass.locale;
|
||||
if (this.config.time_format) {
|
||||
locale = { ...locale, time_format: this.config.time_format };
|
||||
}
|
||||
|
||||
this._dateTimeFormat = new Intl.DateTimeFormat(this.hass.locale.language, {
|
||||
...(this.config.date && this.config.date !== "none"
|
||||
? this.config.date === "day"
|
||||
? {
|
||||
day: "numeric",
|
||||
}
|
||||
: this.config.date === "day-month"
|
||||
? {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
}
|
||||
: this.config.date === "day-month-long"
|
||||
? {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
}
|
||||
: {
|
||||
year: "numeric",
|
||||
month: this.config.date === "long" ? "long" : "short",
|
||||
day: "numeric",
|
||||
}
|
||||
: {}),
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hourCycle: "h12",
|
||||
timeZone:
|
||||
this.config.time_zone ||
|
||||
resolveTimeZone(locale.time_zone, this.hass.config?.time_zone),
|
||||
});
|
||||
|
||||
this._computeDateTime();
|
||||
}
|
||||
|
||||
private _startTick() {
|
||||
this._tick();
|
||||
this._tickInterval = window.setInterval(() => this._tick(), INTERVAL);
|
||||
}
|
||||
|
||||
private _stopTick() {
|
||||
if (this._tickInterval) {
|
||||
clearInterval(this._tickInterval);
|
||||
this._tickInterval = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _updateDate() {
|
||||
this._currentDate = new Date();
|
||||
}
|
||||
|
||||
private _computeDateTime() {
|
||||
if (!this._dateTimeFormat) return;
|
||||
|
||||
const parts = this._dateTimeFormat.formatToParts();
|
||||
this._updateDate();
|
||||
|
||||
const parts = this._dateTimeFormat.formatToParts(this._currentDate);
|
||||
const hourStr = parts.find((p) => p.type === "hour")?.value;
|
||||
const minuteStr = parts.find((p) => p.type === "minute")?.value;
|
||||
const secondStr = parts.find((p) => p.type === "second")?.value;
|
||||
@@ -103,7 +163,7 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
const hour = hourStr ? parseInt(hourStr, 10) : 0;
|
||||
const minute = minuteStr ? parseInt(minuteStr, 10) : 0;
|
||||
const second = secondStr ? parseInt(secondStr, 10) : 0;
|
||||
const ms = new Date().getMilliseconds();
|
||||
const ms = this._currentDate.getMilliseconds();
|
||||
const secondsWithMs = second + ms / 1000;
|
||||
|
||||
const hour12 = hour % 12;
|
||||
@@ -111,18 +171,38 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
this._secondOffsetSec = secondsWithMs;
|
||||
this._minuteOffsetSec = minute * 60 + secondsWithMs;
|
||||
this._hourOffsetSec = hour12 * 3600 + minute * 60 + secondsWithMs;
|
||||
|
||||
// Also update date parts if date is shown
|
||||
if (this.config?.date && this.config.date !== "none") {
|
||||
this._year = parts.find((p) => p.type === "year")?.value ?? "";
|
||||
this._month = parts.find((p) => p.type === "month")?.value ?? "";
|
||||
this._day = parts.find((p) => p.type === "day")?.value ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
private _tick() {
|
||||
this._computeDateTime();
|
||||
}
|
||||
|
||||
private _computeClock = memoizeOne((config: ClockCardConfig) => {
|
||||
const faceParts = config.face_style?.split("_");
|
||||
const isLongDate =
|
||||
config.date === "day-month-long" || config.date === "long";
|
||||
|
||||
return {
|
||||
sizeClass: config.clock_size ? `size-${config.clock_size}` : "",
|
||||
isNumbers: faceParts?.includes("numbers") ?? false,
|
||||
isRoman: faceParts?.includes("roman") ?? false,
|
||||
isUpright: faceParts?.includes("upright") ?? false,
|
||||
isLongDate,
|
||||
};
|
||||
});
|
||||
|
||||
render() {
|
||||
if (!this.config) return nothing;
|
||||
|
||||
const sizeClass = this.config.clock_size
|
||||
? `size-${this.config.clock_size}`
|
||||
: "";
|
||||
|
||||
const isNumbers = this.config?.face_style?.startsWith("numbers");
|
||||
const isRoman = this.config?.face_style?.startsWith("roman");
|
||||
const isUpright = this.config?.face_style?.endsWith("upright");
|
||||
const { sizeClass, isNumbers, isRoman, isUpright, isLongDate } =
|
||||
this._computeClock(this.config);
|
||||
|
||||
const indicator = (number?: number) => html`
|
||||
<div
|
||||
@@ -163,14 +243,14 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
})}
|
||||
>
|
||||
${this.config.ticks === "quarter"
|
||||
? Array.from({ length: 4 }, (_, i) => i).map(
|
||||
? QUARTER_TICKS.map(
|
||||
(i) =>
|
||||
// 4 ticks (12, 3, 6, 9) at 0°, 90°, 180°, 270°
|
||||
html`
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="tick hour"
|
||||
style=${`--tick-rotation: ${i * 90}deg;`}
|
||||
style=${styleMap({ "--tick-rotation": `${i * 90}deg` })}
|
||||
>
|
||||
${indicator([12, 3, 6, 9][i])}
|
||||
</div>
|
||||
@@ -178,28 +258,30 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
)
|
||||
: !this.config.ticks || // Default to hour ticks
|
||||
this.config.ticks === "hour"
|
||||
? Array.from({ length: 12 }, (_, i) => i).map(
|
||||
? HOUR_TICKS.map(
|
||||
(i) =>
|
||||
// 12 ticks (1-12)
|
||||
html`
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="tick hour"
|
||||
style=${`--tick-rotation: ${i * 30}deg;`}
|
||||
style=${styleMap({ "--tick-rotation": `${i * 30}deg` })}
|
||||
>
|
||||
${indicator(((i + 11) % 12) + 1)}
|
||||
</div>
|
||||
`
|
||||
)
|
||||
: this.config.ticks === "minute"
|
||||
? Array.from({ length: 60 }, (_, i) => i).map(
|
||||
? MINUTE_TICKS.map(
|
||||
(i) =>
|
||||
// 60 ticks (1-60)
|
||||
html`
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="tick ${i % 5 === 0 ? "hour" : "minute"}"
|
||||
style=${`--tick-rotation: ${i * 6}deg;`}
|
||||
style=${styleMap({
|
||||
"--tick-rotation": `${i * 6}deg`,
|
||||
})}
|
||||
>
|
||||
${i % 5 === 0
|
||||
? indicator(((i / 5 + 11) % 12) + 1)
|
||||
@@ -208,14 +290,33 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
`
|
||||
)
|
||||
: nothing}
|
||||
${this.config?.date && this.config.date !== "none"
|
||||
? html`<div
|
||||
class=${classMap({
|
||||
"date-parts": true,
|
||||
[sizeClass]: true,
|
||||
"long-date": isLongDate,
|
||||
})}
|
||||
>
|
||||
<span class="date-part day-month"
|
||||
>${this._day} ${this._month}</span
|
||||
>
|
||||
<span class="date-part year">${this._year}</span>
|
||||
</div>`
|
||||
: nothing}
|
||||
|
||||
<div class="center-dot"></div>
|
||||
<div
|
||||
class="hand hour"
|
||||
style=${`animation-delay: -${this._hourOffsetSec ?? 0}s;`}
|
||||
style=${styleMap({
|
||||
"animation-delay": `-${this._hourOffsetSec ?? 0}s`,
|
||||
})}
|
||||
></div>
|
||||
<div
|
||||
class="hand minute"
|
||||
style=${`animation-delay: -${this._minuteOffsetSec ?? 0}s;`}
|
||||
style=${styleMap({
|
||||
"animation-delay": `-${this._minuteOffsetSec ?? 0}s`,
|
||||
})}
|
||||
></div>
|
||||
${this.config.show_seconds
|
||||
? html`<div
|
||||
@@ -224,11 +325,13 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
second: true,
|
||||
step: this.config.seconds_motion === "tick",
|
||||
})}
|
||||
style=${`animation-delay: -${
|
||||
(this.config.seconds_motion === "tick"
|
||||
? Math.floor(this._secondOffsetSec ?? 0)
|
||||
: (this._secondOffsetSec ?? 0)) as number
|
||||
}s;`}
|
||||
style=${styleMap({
|
||||
"animation-delay": `-${
|
||||
(this.config.seconds_motion === "tick"
|
||||
? Math.floor(this._secondOffsetSec ?? 0)
|
||||
: (this._secondOffsetSec ?? 0)) as number
|
||||
}s`,
|
||||
})}
|
||||
></div>`
|
||||
: nothing}
|
||||
</div>
|
||||
@@ -270,6 +373,14 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Modern browsers: Use container queries for responsive font sizing */
|
||||
@supports (container-type: inline-size) {
|
||||
.dial {
|
||||
container-type: inline-size;
|
||||
container-name: clock;
|
||||
}
|
||||
}
|
||||
|
||||
.dial-border {
|
||||
border: 2px solid var(--divider-color);
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
@@ -407,6 +518,78 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
transform: translate(-50%, 0) rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.date-parts {
|
||||
position: absolute;
|
||||
top: 68%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: grid;
|
||||
align-items: center;
|
||||
grid-template-areas:
|
||||
"day-month"
|
||||
"year";
|
||||
direction: ltr;
|
||||
color: var(--primary-text-color);
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
text-align: center;
|
||||
opacity: 0.8;
|
||||
max-width: 87%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Modern browsers: Use container queries for responsive font sizing */
|
||||
@supports (container-type: inline-size) {
|
||||
/* Small clock with long date: reduce to xs */
|
||||
@container clock (max-width: 139px) {
|
||||
.date-parts.long-date {
|
||||
font-size: var(--ha-font-size-xs);
|
||||
}
|
||||
}
|
||||
|
||||
/* Medium clock: scale up */
|
||||
@container clock (min-width: 140px) {
|
||||
.date-parts {
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
}
|
||||
|
||||
/* Large clock: scale up more */
|
||||
@container clock (min-width: 200px) {
|
||||
.date-parts {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Legacy browsers: Use existing size classes */
|
||||
@supports not (container-type: inline-size) {
|
||||
/* Small clock (no size class) with long date */
|
||||
.date-parts.long-date:not(.size-medium):not(.size-large) {
|
||||
font-size: var(--ha-font-size-xs);
|
||||
}
|
||||
|
||||
.date-parts.size-medium {
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
|
||||
.date-parts.size-large {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
}
|
||||
}
|
||||
|
||||
.date-part.day-month {
|
||||
grid-area: day-month;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.date-part.year {
|
||||
grid-area: year;
|
||||
overflow: hidden;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ export class HuiClockCardDigital extends LitElement {
|
||||
|
||||
@state() private _timeAmPm?: string;
|
||||
|
||||
@state() private _date?: string;
|
||||
|
||||
private _tickInterval?: undefined | number;
|
||||
|
||||
private _initDate() {
|
||||
@@ -39,6 +41,27 @@ export class HuiClockCardDigital extends LitElement {
|
||||
|
||||
const h12 = useAmPm(locale);
|
||||
this._dateTimeFormat = new Intl.DateTimeFormat(this.hass.locale.language, {
|
||||
...(this.config.date && this.config.date !== "none"
|
||||
? this.config.date === "day"
|
||||
? {
|
||||
day: "numeric",
|
||||
}
|
||||
: this.config.date === "day-month"
|
||||
? {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
}
|
||||
: this.config.date === "day-month-long"
|
||||
? {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
}
|
||||
: {
|
||||
year: "numeric",
|
||||
month: this.config.date === "long" ? "long" : "short",
|
||||
day: "numeric",
|
||||
}
|
||||
: {}),
|
||||
hour: h12 ? "numeric" : "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
@@ -93,6 +116,16 @@ export class HuiClockCardDigital extends LitElement {
|
||||
? parts.find((part) => part.type === "second")?.value
|
||||
: undefined;
|
||||
this._timeAmPm = parts.find((part) => part.type === "dayPeriod")?.value;
|
||||
|
||||
this._date = this.config?.date
|
||||
? [
|
||||
parts.find((part) => part.type === "day")?.value,
|
||||
parts.find((part) => part.type === "month")?.value,
|
||||
parts.find((part) => part.type === "year")?.value,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
: undefined;
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -113,6 +146,9 @@ export class HuiClockCardDigital extends LitElement {
|
||||
? html`<div class="time-part am-pm">${this._timeAmPm}</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this.config.date && this.config.date !== "none"
|
||||
? html`<div class="date ${sizeClass}">${this._date}</div>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -188,6 +224,20 @@ export class HuiClockCardDigital extends LitElement {
|
||||
content: ":";
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.date {
|
||||
text-align: center;
|
||||
opacity: 0.8;
|
||||
font-size: var(--ha-font-size-s);
|
||||
}
|
||||
|
||||
.date.size-medium {
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
|
||||
.date.size-large {
|
||||
font-size: var(--ha-font-size-2xl);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -73,18 +73,13 @@ export class HuiCard extends ConditionalListenerMixin<LovelaceCardConfig>(
|
||||
};
|
||||
|
||||
// If the element has fixed rows or columns, we use the values from the element
|
||||
// unless the user has already configured their own
|
||||
if (elementOptions.fixed_rows) {
|
||||
if (configOptions.rows === undefined) {
|
||||
mergedConfig.rows = elementOptions.rows;
|
||||
}
|
||||
mergedConfig.rows = elementOptions.rows;
|
||||
delete mergedConfig.min_rows;
|
||||
delete mergedConfig.max_rows;
|
||||
}
|
||||
if (elementOptions.fixed_columns) {
|
||||
if (configOptions.columns === undefined) {
|
||||
mergedConfig.columns = elementOptions.columns;
|
||||
}
|
||||
mergedConfig.columns = elementOptions.columns;
|
||||
delete mergedConfig.min_columns;
|
||||
delete mergedConfig.max_columns;
|
||||
}
|
||||
|
||||
@@ -11,10 +11,7 @@ import { findEntities } from "../common/find-entities";
|
||||
import type { LovelaceElement, LovelaceElementConfig } from "../elements/types";
|
||||
import type { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { createStyledHuiElement } from "./picture-elements/create-styled-hui-element";
|
||||
import {
|
||||
PREVIEW_CLICK_CALLBACK,
|
||||
type PictureElementsCardConfig,
|
||||
} from "./types";
|
||||
import type { PictureElementsCardConfig } from "./types";
|
||||
import type { PersonEntity } from "../../../data/person";
|
||||
|
||||
@customElement("hui-picture-elements-card")
|
||||
@@ -169,7 +166,6 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
|
||||
.aspectRatio=${this._config.aspect_ratio}
|
||||
.darkModeFilter=${this._config.dark_mode_filter}
|
||||
.darkModeImage=${darkModeImage}
|
||||
@click=${this._handleImageClick}
|
||||
></hui-image>
|
||||
${this._elements}
|
||||
</div>
|
||||
@@ -225,19 +221,6 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
|
||||
curCardEl === elToReplace ? newCardEl : curCardEl
|
||||
);
|
||||
}
|
||||
|
||||
private _handleImageClick(ev: MouseEvent): void {
|
||||
if (!this.preview || !this._config?.[PREVIEW_CLICK_CALLBACK]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = (ev.currentTarget as HTMLElement).getBoundingClientRect();
|
||||
const x = ((ev.clientX - rect.left) / rect.width) * 100;
|
||||
const y = ((ev.clientY - rect.top) / rect.height) * 100;
|
||||
|
||||
// only the edited card has this callback
|
||||
this._config[PREVIEW_CLICK_CALLBACK](x, y);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -421,6 +421,7 @@ export interface ClockCardConfig extends LovelaceCardConfig {
|
||||
time_format?: TimeFormat;
|
||||
time_zone?: string;
|
||||
no_background?: boolean;
|
||||
date?: "none" | "short" | "long" | "day" | "day-month" | "day-month-long";
|
||||
// Analog clock options
|
||||
border?: boolean;
|
||||
ticks?: "none" | "quarter" | "hour" | "minute";
|
||||
@@ -487,10 +488,6 @@ export interface PictureCardConfig extends LovelaceCardConfig {
|
||||
alt_text?: string;
|
||||
}
|
||||
|
||||
// Symbol for preview click callback - preserved through spreads, not serialized
|
||||
// This allows the editor to attach a callback that only exists on the edited card's config
|
||||
export const PREVIEW_CLICK_CALLBACK = Symbol("previewClickCallback");
|
||||
|
||||
export interface PictureElementsCardConfig extends LovelaceCardConfig {
|
||||
title?: string;
|
||||
image?: string | MediaSelectorValue;
|
||||
@@ -505,7 +502,6 @@ export interface PictureElementsCardConfig extends LovelaceCardConfig {
|
||||
theme?: string;
|
||||
dark_mode_image?: string | MediaSelectorValue;
|
||||
dark_mode_filter?: string;
|
||||
[PREVIEW_CLICK_CALLBACK]?: (x: number, y: number) => void;
|
||||
}
|
||||
|
||||
export interface PictureEntityCardConfig extends LovelaceCardConfig {
|
||||
|
||||
@@ -39,6 +39,19 @@ const cardConfigStruct = assign(
|
||||
time_zone: optional(enums(Object.keys(timezones))),
|
||||
show_seconds: optional(boolean()),
|
||||
no_background: optional(boolean()),
|
||||
date: optional(
|
||||
defaulted(
|
||||
union([
|
||||
literal("none"),
|
||||
literal("short"),
|
||||
literal("long"),
|
||||
literal("day"),
|
||||
literal("day-month"),
|
||||
literal("day-month-long"),
|
||||
]),
|
||||
literal("none")
|
||||
)
|
||||
),
|
||||
// Analog clock options
|
||||
border: optional(defaulted(boolean(), false)),
|
||||
ticks: optional(
|
||||
@@ -93,7 +106,7 @@ export class HuiClockCardEditor
|
||||
name: "clock_style",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
mode: "box",
|
||||
options: ["digital", "analog"].map((value) => ({
|
||||
value,
|
||||
label: localize(
|
||||
@@ -119,6 +132,27 @@ export class HuiClockCardEditor
|
||||
},
|
||||
{ name: "show_seconds", selector: { boolean: {} } },
|
||||
{ name: "no_background", selector: { boolean: {} } },
|
||||
{
|
||||
name: "date",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
options: [
|
||||
"none",
|
||||
"short",
|
||||
"long",
|
||||
"day",
|
||||
"day-month",
|
||||
"day-month-long",
|
||||
].map((value) => ({
|
||||
value,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.clock.dates.${value}`
|
||||
),
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
...(clockStyle === "digital"
|
||||
? ([
|
||||
{
|
||||
@@ -260,13 +294,14 @@ export class HuiClockCardEditor
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
private _data = memoizeOne((config) => ({
|
||||
private _data = memoizeOne((config: ClockCardConfig) => ({
|
||||
clock_style: "digital",
|
||||
clock_size: "small",
|
||||
time_zone: "auto",
|
||||
time_format: "auto",
|
||||
show_seconds: false,
|
||||
no_background: false,
|
||||
date: "none",
|
||||
// Analog clock options
|
||||
border: false,
|
||||
ticks: "hour",
|
||||
@@ -290,8 +325,9 @@ export class HuiClockCardEditor
|
||||
.data=${this._data(this._config)}
|
||||
.schema=${this._schema(
|
||||
this.hass.localize,
|
||||
this._data(this._config).clock_style,
|
||||
this._data(this._config).ticks,
|
||||
this._data(this._config)
|
||||
.clock_style as ClockCardConfig["clock_style"],
|
||||
this._data(this._config).ticks as ClockCardConfig["ticks"],
|
||||
this._data(this._config).show_seconds
|
||||
)}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@@ -367,6 +403,10 @@ export class HuiClockCardEditor
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.no_background`
|
||||
);
|
||||
case "date":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.date.label`
|
||||
);
|
||||
case "border":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.border.label`
|
||||
@@ -392,6 +432,10 @@ export class HuiClockCardEditor
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "date":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.date.description`
|
||||
);
|
||||
case "border":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.border.description`
|
||||
|
||||
@@ -15,16 +15,12 @@ import {
|
||||
} from "superstruct";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import "../../../../components/ha-icon";
|
||||
import "../../../../components/ha-switch";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
PREVIEW_CLICK_CALLBACK,
|
||||
type PictureElementsCardConfig,
|
||||
} from "../../cards/types";
|
||||
import type { PictureElementsCardConfig } from "../../cards/types";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import "../hui-sub-element-editor";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
@@ -32,6 +28,7 @@ import type { EditDetailElementEvent, SubElementEditorConfig } from "../types";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
import "../hui-picture-elements-card-row-editor";
|
||||
import type { LovelaceElementConfig } from "../../elements/types";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
|
||||
const genericElementConfigStruct = type({
|
||||
@@ -69,44 +66,6 @@ export class HuiPictureElementsCardEditor
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _onPreviewClick = (x: number, y: number): void => {
|
||||
if (this._subElementEditorConfig?.type === "element") {
|
||||
this._handlePositionClick(x, y);
|
||||
}
|
||||
};
|
||||
|
||||
private _handlePositionClick(x: number, y: number): void {
|
||||
if (
|
||||
!this._subElementEditorConfig?.elementConfig ||
|
||||
this._subElementEditorConfig.type !== "element" ||
|
||||
this._subElementEditorConfig.elementConfig.type === "conditional"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elementConfig = this._subElementEditorConfig
|
||||
.elementConfig as LovelaceElementConfig;
|
||||
const currentPosition = (elementConfig.style as Record<string, string>)
|
||||
?.position;
|
||||
if (currentPosition && currentPosition !== "absolute") {
|
||||
return;
|
||||
}
|
||||
|
||||
const newElement = {
|
||||
...elementConfig,
|
||||
style: {
|
||||
...((elementConfig.style as Record<string, string>) || {}),
|
||||
left: `${Math.round(x)}%`,
|
||||
top: `${Math.round(y)}%`,
|
||||
},
|
||||
};
|
||||
|
||||
const updateEvent = new CustomEvent("config-changed", {
|
||||
detail: { config: newElement },
|
||||
});
|
||||
this._handleSubElementChanged(updateEvent);
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc) =>
|
||||
[
|
||||
@@ -179,16 +138,6 @@ export class HuiPictureElementsCardEditor
|
||||
|
||||
if (this._subElementEditorConfig) {
|
||||
return html`
|
||||
${this._subElementEditorConfig.type === "element" &&
|
||||
this._subElementEditorConfig.elementConfig?.type !== "conditional"
|
||||
? html`
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.picture-elements.position_hint"
|
||||
)}
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing}
|
||||
<hui-sub-element-editor
|
||||
.hass=${this.hass}
|
||||
.config=${this._subElementEditorConfig}
|
||||
@@ -232,7 +181,6 @@ export class HuiPictureElementsCardEditor
|
||||
return;
|
||||
}
|
||||
|
||||
// no need to attach the preview click callback here, no element is being edited
|
||||
fireEvent(this, "config-changed", { config: ev.detail.value });
|
||||
}
|
||||
|
||||
@@ -243,8 +191,7 @@ export class HuiPictureElementsCardEditor
|
||||
const config = {
|
||||
...this._config,
|
||||
elements: ev.detail.elements as LovelaceElementConfig[],
|
||||
[PREVIEW_CLICK_CALLBACK]: this._onPreviewClick,
|
||||
} as PictureElementsCardConfig;
|
||||
} as LovelaceCardConfig;
|
||||
|
||||
fireEvent(this, "config-changed", { config });
|
||||
|
||||
@@ -285,12 +232,7 @@ export class HuiPictureElementsCardEditor
|
||||
elementConfig: value,
|
||||
};
|
||||
|
||||
fireEvent(this, "config-changed", {
|
||||
config: {
|
||||
...this._config,
|
||||
[PREVIEW_CLICK_CALLBACK]: this._onPreviewClick,
|
||||
},
|
||||
});
|
||||
fireEvent(this, "config-changed", { config: this._config });
|
||||
}
|
||||
|
||||
private _editDetailElement(ev: HASSDomEvent<EditDetailElementEvent>): void {
|
||||
|
||||
@@ -10,9 +10,7 @@ export const getElementStubConfig = async (
|
||||
): Promise<LovelaceElementConfig> => {
|
||||
let elementConfig: LovelaceElementConfig = { type };
|
||||
|
||||
if (type === "conditional") {
|
||||
elementConfig = { type, conditions: [], elements: [] };
|
||||
} else {
|
||||
if (type !== "conditional") {
|
||||
elementConfig.style = { left: "50%", top: "50%" };
|
||||
}
|
||||
|
||||
|
||||
@@ -89,11 +89,7 @@ export abstract class HuiElementEditor<
|
||||
}
|
||||
|
||||
public set value(config: T | undefined) {
|
||||
// Compare symbols to detect callback changes (e.g., preview click handlers)
|
||||
if (
|
||||
this._config &&
|
||||
deepEqual(config, this._config, { compareSymbols: true })
|
||||
) {
|
||||
if (this._config && deepEqual(config, this._config)) {
|
||||
return;
|
||||
}
|
||||
this._config = config;
|
||||
|
||||
@@ -15,11 +15,7 @@ import { tags } from "@lezer/highlight";
|
||||
export { autocompletion } from "@codemirror/autocomplete";
|
||||
export { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
|
||||
export { highlightingFor, foldGutter } from "@codemirror/language";
|
||||
export {
|
||||
highlightSelectionMatches,
|
||||
search,
|
||||
searchKeymap,
|
||||
} from "@codemirror/search";
|
||||
export { highlightSelectionMatches, searchKeymap } from "@codemirror/search";
|
||||
export { EditorState } from "@codemirror/state";
|
||||
export {
|
||||
crosshairCursor,
|
||||
|
||||
@@ -15,27 +15,27 @@ export const semanticColorStyles = css`
|
||||
--ha-color-text-disabled: var(--ha-color-neutral-60);
|
||||
--ha-color-text-link: var(--ha-color-primary-40);
|
||||
/* border primary */
|
||||
--ha-color-border-primary-quiet: var(--ha-color-primary-90);
|
||||
--ha-color-border-primary-quiet: var(--ha-color-primary-80);
|
||||
--ha-color-border-primary-normal: var(--ha-color-primary-70);
|
||||
--ha-color-border-primary-loud: var(--ha-color-primary-40);
|
||||
|
||||
/* border neutral */
|
||||
--ha-color-border-neutral-quiet: var(--ha-color-neutral-90);
|
||||
--ha-color-border-neutral-quiet: var(--ha-color-neutral-80);
|
||||
--ha-color-border-neutral-normal: var(--ha-color-neutral-60);
|
||||
--ha-color-border-neutral-loud: var(--ha-color-neutral-40);
|
||||
|
||||
/* border danger */
|
||||
--ha-color-border-danger-quiet: var(--ha-color-red-90);
|
||||
--ha-color-border-danger-quiet: var(--ha-color-red-80);
|
||||
--ha-color-border-danger-normal: var(--ha-color-red-70);
|
||||
--ha-color-border-danger-loud: var(--ha-color-red-40);
|
||||
|
||||
/* border warning */
|
||||
--ha-color-border-warning-quiet: var(--ha-color-orange-90);
|
||||
--ha-color-border-warning-quiet: var(--ha-color-orange-80);
|
||||
--ha-color-border-warning-normal: var(--ha-color-orange-70);
|
||||
--ha-color-border-warning-loud: var(--ha-color-orange-40);
|
||||
|
||||
/* border success */
|
||||
--ha-color-border-success-quiet: var(--ha-color-green-90);
|
||||
--ha-color-border-success-quiet: var(--ha-color-green-80);
|
||||
--ha-color-border-success-normal: var(--ha-color-green-70);
|
||||
--ha-color-border-success-loud: var(--ha-color-green-40);
|
||||
|
||||
|
||||
@@ -8241,6 +8241,18 @@
|
||||
"large": "Large"
|
||||
},
|
||||
"show_seconds": "Display seconds",
|
||||
"date": {
|
||||
"label": "Date",
|
||||
"description": "Whether to show the date on the clock. Can also be a custom date format."
|
||||
},
|
||||
"dates": {
|
||||
"none": "No date",
|
||||
"short": "Short",
|
||||
"long": "Long",
|
||||
"day": "Day only",
|
||||
"day-month": "Day and month",
|
||||
"day-month-long": "Day and month (long)"
|
||||
},
|
||||
"time_format": "[%key:ui::panel::profile::time_format::dropdown_label%]",
|
||||
"time_formats": {
|
||||
"auto": "Use user settings",
|
||||
@@ -8326,7 +8338,6 @@
|
||||
"dark_mode_image": "Dark mode image path",
|
||||
"state_filter": "State filter",
|
||||
"dark_mode_filter": "Dark mode state filter",
|
||||
"position_hint": "Click on the image preview to position this element",
|
||||
"element_types": {
|
||||
"state-badge": "State badge",
|
||||
"state-icon": "State icon",
|
||||
|
||||
Reference in New Issue
Block a user