mirror of
https://github.com/home-assistant/frontend.git
synced 2026-03-10 17:27:49 +00:00
Compare commits
46 Commits
20260225.0
...
clock-date
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18fc990cf5 | ||
|
|
6747dc2d90 | ||
|
|
38c09c6b98 | ||
|
|
46c1309bf0 | ||
|
|
7ce54b3ab0 | ||
|
|
b95bbcd71b | ||
|
|
36c9f4ea57 | ||
|
|
77a043e18e | ||
|
|
361320ab60 | ||
|
|
8092546b70 | ||
|
|
cb5daa87df | ||
|
|
74042f4d17 | ||
|
|
b2eababfa7 | ||
|
|
41d27413f5 | ||
|
|
2e64f471c1 | ||
|
|
6561276983 | ||
|
|
0ce57a9dfe | ||
|
|
5dd7ed22bd | ||
|
|
831c290158 | ||
|
|
5691d11e3c | ||
|
|
1a15711422 | ||
|
|
f5e462b8e9 | ||
|
|
2e47a12051 | ||
|
|
7e5b3ef59b | ||
|
|
fe77991605 | ||
|
|
86108b08df | ||
|
|
01956c69ce | ||
|
|
6eadae63a0 | ||
|
|
c427e42c68 | ||
|
|
fb612be7ba | ||
|
|
60ada7c159 | ||
|
|
9a85a62548 | ||
|
|
cf4890bbd6 | ||
|
|
5cdbef2362 | ||
|
|
5115367d48 | ||
|
|
652c6e5a0b | ||
|
|
4615484fcd | ||
|
|
8009782759 | ||
|
|
0a904b2cd4 | ||
|
|
963f7086a9 | ||
|
|
c8a186d8d9 | ||
|
|
8271400193 | ||
|
|
721323a32c | ||
|
|
4c9c22130e | ||
|
|
135c913ebc | ||
|
|
4e800bd3a9 |
22
package.json
22
package.json
@@ -34,10 +34,10 @@
|
||||
"@codemirror/legacy-modes": "6.5.2",
|
||||
"@codemirror/search": "6.6.0",
|
||||
"@codemirror/state": "6.5.4",
|
||||
"@codemirror/view": "6.39.15",
|
||||
"@codemirror/view": "6.39.12",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.2.2",
|
||||
"@formatjs/intl-datetimeformat": "7.2.1",
|
||||
"@formatjs/intl-displaynames": "7.2.1",
|
||||
"@formatjs/intl-durationformat": "0.10.1",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.1",
|
||||
@@ -118,7 +118,7 @@
|
||||
"lit": "3.3.2",
|
||||
"lit-html": "3.3.2",
|
||||
"luxon": "3.7.2",
|
||||
"marked": "17.0.3",
|
||||
"marked": "17.0.1",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.4",
|
||||
"object-hash": "3.0.0",
|
||||
@@ -148,14 +148,14 @@
|
||||
"@babel/helper-define-polyfill-provider": "0.6.6",
|
||||
"@babel/plugin-transform-runtime": "7.29.0",
|
||||
"@babel/preset-env": "7.29.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.10",
|
||||
"@html-eslint/eslint-plugin": "0.56.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.9",
|
||||
"@html-eslint/eslint-plugin": "0.55.0",
|
||||
"@lokalise/node-api": "15.6.1",
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.1.0",
|
||||
"@octokit/plugin-retry": "8.0.3",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.2",
|
||||
"@rspack/core": "1.7.6",
|
||||
"@rspack/core": "1.7.5",
|
||||
"@rspack/dev-server": "1.2.1",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.25",
|
||||
@@ -180,18 +180,18 @@
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"del": "8.0.1",
|
||||
"eslint": "9.39.3",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-webpack": "0.13.10",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-lit": "2.2.1",
|
||||
"eslint-plugin-lit": "2.1.1",
|
||||
"eslint-plugin-lit-a11y": "5.1.1",
|
||||
"eslint-plugin-unused-imports": "4.4.1",
|
||||
"eslint-plugin-unused-imports": "4.3.0",
|
||||
"eslint-plugin-wc": "3.0.2",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.3",
|
||||
"glob": "13.0.6",
|
||||
"glob": "13.0.1",
|
||||
"gulp": "5.0.1",
|
||||
"gulp-brotli": "3.0.0",
|
||||
"gulp-json-transform": "0.5.0",
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20260225.0"
|
||||
version = "20260128.0"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
||||
@@ -210,39 +210,3 @@ const formatDateWeekdayShortMem = memoizeOne(
|
||||
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
|
||||
})
|
||||
);
|
||||
|
||||
// Mon, Aug 10
|
||||
export const formatDateWeekdayVeryShortDate = (
|
||||
dateObj: Date,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
) =>
|
||||
formatDateWeekdayVeryShortDateMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatDateWeekdayVeryShortDateMem = memoizeOne(
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
weekday: "short",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
|
||||
})
|
||||
);
|
||||
|
||||
// Mon, Aug 10, 2021
|
||||
export const formatDateWeekdayShortDate = (
|
||||
dateObj: Date,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
) => formatDateWeekdayShortDateMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatDateWeekdayShortDateMem = memoizeOne(
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
weekday: "short",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -136,6 +136,9 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public searchLabel?: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-label-float" })
|
||||
public noLabelFloat? = false;
|
||||
|
||||
@property({ type: String }) public filter = "";
|
||||
|
||||
@property({ attribute: false }) public groupColumn?: string;
|
||||
@@ -397,6 +400,7 @@ export class HaDataTable extends LitElement {
|
||||
.hass=${this.hass}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
.label=${this.searchLabel}
|
||||
.noLabelFloat=${this.noLabelFloat}
|
||||
></search-input>
|
||||
</div>
|
||||
`
|
||||
@@ -430,9 +434,9 @@ export class HaDataTable extends LitElement {
|
||||
<ha-checkbox
|
||||
class="mdc-data-table__row-checkbox"
|
||||
@change=${this._handleHeaderRowCheckboxClick}
|
||||
.indeterminate=${!!this._checkedRows.length &&
|
||||
.indeterminate=${this._checkedRows.length &&
|
||||
this._checkedRows.length !== this._checkableRowsCount}
|
||||
.checked=${!!this._checkedRows.length &&
|
||||
.checked=${this._checkedRows.length &&
|
||||
this._checkedRows.length === this._checkableRowsCount}
|
||||
>
|
||||
</ha-checkbox>
|
||||
|
||||
@@ -15,7 +15,6 @@ import { iconColorCSS } from "../../common/style/icon_color_css";
|
||||
import { cameraUrlWithWidthHeight } from "../../data/camera";
|
||||
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { addBrandsAuth } from "../../util/brands-url";
|
||||
import "../ha-state-icon";
|
||||
|
||||
@customElement("state-badge")
|
||||
@@ -138,7 +137,6 @@ export class StateBadge extends LitElement {
|
||||
let imageUrl =
|
||||
stateObj.attributes.entity_picture_local ||
|
||||
stateObj.attributes.entity_picture;
|
||||
imageUrl = addBrandsAuth(imageUrl);
|
||||
if (this.hass) {
|
||||
imageUrl = this.hass.hassUrl(imageUrl);
|
||||
}
|
||||
|
||||
@@ -84,9 +84,6 @@ export class HaButton extends Button {
|
||||
--button-color-fill-loud-hover: var(
|
||||
--ha-color-fill-primary-loud-hover
|
||||
);
|
||||
--button-color-fill-quiet-active: var(
|
||||
--ha-color-fill-primary-quiet-active
|
||||
);
|
||||
}
|
||||
|
||||
:host([variant="neutral"]) {
|
||||
@@ -102,9 +99,6 @@ export class HaButton extends Button {
|
||||
--button-color-fill-loud-hover: var(
|
||||
--ha-color-fill-neutral-loud-hover
|
||||
);
|
||||
--button-color-fill-quiet-active: var(
|
||||
--ha-color-fill-neutral-normal-active
|
||||
);
|
||||
}
|
||||
|
||||
:host([variant="success"]) {
|
||||
@@ -120,9 +114,6 @@ export class HaButton extends Button {
|
||||
--button-color-fill-loud-hover: var(
|
||||
--ha-color-fill-success-loud-hover
|
||||
);
|
||||
--button-color-fill-quiet-active: var(
|
||||
--ha-color-fill-success-quiet-active
|
||||
);
|
||||
}
|
||||
|
||||
:host([variant="warning"]) {
|
||||
@@ -138,9 +129,6 @@ export class HaButton extends Button {
|
||||
--button-color-fill-loud-hover: var(
|
||||
--ha-color-fill-warning-loud-hover
|
||||
);
|
||||
--button-color-fill-quiet-active: var(
|
||||
--ha-color-fill-warning-quiet-active
|
||||
);
|
||||
}
|
||||
|
||||
:host([variant="danger"]) {
|
||||
@@ -156,9 +144,6 @@ export class HaButton extends Button {
|
||||
--button-color-fill-loud-hover: var(
|
||||
--ha-color-fill-danger-loud-hover
|
||||
);
|
||||
--button-color-fill-quiet-active: var(
|
||||
--ha-color-fill-danger-quiet-active
|
||||
);
|
||||
}
|
||||
|
||||
:host([appearance~="plain"]) .button {
|
||||
@@ -202,10 +187,6 @@ export class HaButton extends Button {
|
||||
background-color: var(--ha-color-fill-disabled-normal-resting);
|
||||
color: var(--ha-color-on-disabled-normal);
|
||||
}
|
||||
:host([appearance~="plain"])
|
||||
.button:not(.disabled):not(.loading):active {
|
||||
background-color: var(--button-color-fill-quiet-active);
|
||||
}
|
||||
|
||||
:host([appearance~="accent"]) .button {
|
||||
background-color: var(
|
||||
|
||||
522
src/components/ha-clock-date-format-picker.ts
Normal file
522
src/components/ha-clock-date-format-picker.ts
Normal file
@@ -0,0 +1,522 @@
|
||||
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { resolveTimeZone } from "../common/datetime/resolve-time-zone";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import {
|
||||
CLOCK_CARD_DATE_PARTS,
|
||||
formatClockCardDate,
|
||||
} from "../panels/lovelace/cards/clock/clock-date-format";
|
||||
import type { ClockCardDatePart } from "../panels/lovelace/cards/types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import "./chips/ha-assist-chip";
|
||||
import "./chips/ha-chip-set";
|
||||
import "./chips/ha-input-chip";
|
||||
import "./ha-generic-picker";
|
||||
import type { HaGenericPicker } from "./ha-generic-picker";
|
||||
import "./ha-input-helper-text";
|
||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||
import "./ha-sortable";
|
||||
|
||||
type ClockDatePartSection = "weekday" | "day" | "month" | "year" | "separator";
|
||||
|
||||
type ClockDateSeparatorPart = Extract<
|
||||
ClockCardDatePart,
|
||||
"separator-dash" | "separator-slash" | "separator-dot" | "separator-new-line"
|
||||
>;
|
||||
|
||||
const CLOCK_DATE_PART_SECTION_ORDER: readonly ClockDatePartSection[] = [
|
||||
"day",
|
||||
"month",
|
||||
"year",
|
||||
"weekday",
|
||||
"separator",
|
||||
];
|
||||
|
||||
const CLOCK_DATE_SEPARATOR_VALUES: Record<ClockDateSeparatorPart, string> = {
|
||||
"separator-dash": "-",
|
||||
"separator-slash": "/",
|
||||
"separator-dot": ".",
|
||||
"separator-new-line": "",
|
||||
};
|
||||
|
||||
const getClockDatePartSection = (
|
||||
part: ClockCardDatePart
|
||||
): ClockDatePartSection => {
|
||||
if (part.startsWith("weekday-")) {
|
||||
return "weekday";
|
||||
}
|
||||
|
||||
if (part.startsWith("day-")) {
|
||||
return "day";
|
||||
}
|
||||
|
||||
if (part.startsWith("month-")) {
|
||||
return "month";
|
||||
}
|
||||
|
||||
if (part.startsWith("year-")) {
|
||||
return "year";
|
||||
}
|
||||
|
||||
return "separator";
|
||||
};
|
||||
|
||||
interface ClockDatePartSectionData {
|
||||
id: ClockDatePartSection;
|
||||
title: string;
|
||||
items: PickerComboBoxItem[];
|
||||
}
|
||||
|
||||
interface ClockDatePartValueItem {
|
||||
key: string;
|
||||
item: string;
|
||||
idx: number;
|
||||
}
|
||||
|
||||
@customElement("ha-clock-date-format-picker")
|
||||
export class HaClockDateFormatPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value?: string[] | string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@query("ha-generic-picker", true) private _picker?: HaGenericPicker;
|
||||
|
||||
private _editIndex?: number;
|
||||
|
||||
protected render() {
|
||||
const value = this._value;
|
||||
const valueItems = this._getValueItems(value);
|
||||
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required && !value.length}
|
||||
.value=${this._getPickerValue()}
|
||||
.sections=${this._getSections(this.hass.locale.language)}
|
||||
.getItems=${this._getItems}
|
||||
@value-changed=${this._pickerValueChanged}
|
||||
>
|
||||
<div slot="field" class="container">
|
||||
<ha-sortable
|
||||
no-style
|
||||
@item-moved=${this._moveItem}
|
||||
.disabled=${this.disabled}
|
||||
handle-selector="button.primary.action"
|
||||
filter=".add"
|
||||
>
|
||||
<ha-chip-set>
|
||||
${repeat(
|
||||
valueItems,
|
||||
(entry: ClockDatePartValueItem) => entry.key,
|
||||
({ item, idx }) => this._renderValueChip(item, idx)
|
||||
)}
|
||||
${this.disabled
|
||||
? nothing
|
||||
: html`
|
||||
<ha-assist-chip
|
||||
@click=${this._addItem}
|
||||
.disabled=${this.disabled}
|
||||
label=${this.hass.localize("ui.common.add")}
|
||||
class="add"
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
`}
|
||||
</ha-chip-set>
|
||||
</ha-sortable>
|
||||
</div>
|
||||
</ha-generic-picker>
|
||||
${this._renderHelper()}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderHelper() {
|
||||
return this.helper
|
||||
? html`
|
||||
<ha-input-helper-text .disabled=${this.disabled}>
|
||||
${this.helper}
|
||||
</ha-input-helper-text>
|
||||
`
|
||||
: nothing;
|
||||
}
|
||||
|
||||
private _getValueItems = memoizeOne(
|
||||
(value: string[]): ClockDatePartValueItem[] => {
|
||||
const occurrences = new Map<string, number>();
|
||||
|
||||
return value.map((item, idx) => {
|
||||
const occurrence = occurrences.get(item) ?? 0;
|
||||
occurrences.set(item, occurrence + 1);
|
||||
|
||||
return {
|
||||
key: `${item}:${occurrence}`,
|
||||
item,
|
||||
idx,
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
private _renderValueChip(item: string, idx: number) {
|
||||
const label = this._getItemLabel(item, this.hass.locale.language);
|
||||
const isValid = !!label;
|
||||
|
||||
return html`
|
||||
<ha-input-chip
|
||||
data-idx=${idx}
|
||||
@remove=${this._removeItem}
|
||||
@click=${this._editItem}
|
||||
.label=${label ?? item}
|
||||
.selected=${!this.disabled}
|
||||
.disabled=${this.disabled}
|
||||
class=${!isValid ? "invalid" : ""}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _addItem(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._editIndex = undefined;
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
}
|
||||
|
||||
private async _editItem(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const idx = parseInt(
|
||||
(ev.currentTarget as HTMLElement).dataset.idx ?? "",
|
||||
10
|
||||
);
|
||||
this._editIndex = idx;
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return !this.value ? [] : ensureArray(this.value);
|
||||
}
|
||||
|
||||
private _toValue = memoizeOne((value: string[]): string[] | undefined =>
|
||||
value.length === 0 ? undefined : value
|
||||
);
|
||||
|
||||
private _buildSections = memoizeOne(
|
||||
(language: string): ClockDatePartSectionData[] => {
|
||||
const itemsBySection: Record<ClockDatePartSection, PickerComboBoxItem[]> =
|
||||
{
|
||||
weekday: [],
|
||||
day: [],
|
||||
month: [],
|
||||
year: [],
|
||||
separator: [],
|
||||
};
|
||||
|
||||
const previewDate = new Date();
|
||||
const previewTimeZone = resolveTimeZone(
|
||||
this.hass.locale.time_zone,
|
||||
this.hass.config.time_zone
|
||||
);
|
||||
|
||||
CLOCK_CARD_DATE_PARTS.forEach((part) => {
|
||||
const section = getClockDatePartSection(part);
|
||||
const label =
|
||||
this.hass.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.date.parts.${part}`
|
||||
) ?? part;
|
||||
|
||||
const secondary =
|
||||
section === "separator"
|
||||
? CLOCK_DATE_SEPARATOR_VALUES[part as ClockDateSeparatorPart]
|
||||
: formatClockCardDate(
|
||||
previewDate,
|
||||
{ parts: [part] },
|
||||
language,
|
||||
previewTimeZone
|
||||
);
|
||||
|
||||
itemsBySection[section].push({
|
||||
id: part,
|
||||
primary: label,
|
||||
secondary,
|
||||
sorting_label: label,
|
||||
});
|
||||
});
|
||||
|
||||
return CLOCK_DATE_PART_SECTION_ORDER.map((section) => ({
|
||||
id: section,
|
||||
title:
|
||||
this.hass.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.date.sections.${section}`
|
||||
) ?? section,
|
||||
items: itemsBySection[section],
|
||||
})).filter((section) => section.items.length > 0);
|
||||
}
|
||||
);
|
||||
|
||||
private _getSections = memoizeOne(
|
||||
(_language: string): { id: string; label: string }[] =>
|
||||
this._buildSections(_language).map((section) => ({
|
||||
id: section.id,
|
||||
label: section.title,
|
||||
}))
|
||||
);
|
||||
|
||||
private _getItems = (
|
||||
searchString?: string,
|
||||
section?: string
|
||||
): (PickerComboBoxItem | string)[] => {
|
||||
const normalizedSearch = searchString?.trim().toLowerCase();
|
||||
|
||||
const sections = this._buildSections(this.hass.locale.language)
|
||||
.map((sectionData) => {
|
||||
if (!normalizedSearch) {
|
||||
return sectionData;
|
||||
}
|
||||
|
||||
return {
|
||||
...sectionData,
|
||||
items: sectionData.items.filter(
|
||||
(item) =>
|
||||
item.primary.toLowerCase().includes(normalizedSearch) ||
|
||||
item.secondary?.toLowerCase().includes(normalizedSearch) ||
|
||||
item.id.toLowerCase().includes(normalizedSearch)
|
||||
),
|
||||
};
|
||||
})
|
||||
.filter((sectionData) => sectionData.items.length > 0);
|
||||
|
||||
if (section) {
|
||||
return (
|
||||
sections.find((candidate) => candidate.id === section)?.items || []
|
||||
);
|
||||
}
|
||||
|
||||
const groupedItems: (PickerComboBoxItem | string)[] = [];
|
||||
|
||||
sections.forEach((sectionData) => {
|
||||
groupedItems.push(sectionData.title, ...sectionData.items);
|
||||
});
|
||||
|
||||
return groupedItems;
|
||||
};
|
||||
|
||||
private _getItemLabel = memoizeOne(
|
||||
(value: string, language: string): string | undefined => {
|
||||
const sections = this._buildSections(language);
|
||||
|
||||
for (const section of sections) {
|
||||
const item = section.items.find((candidate) => candidate.id === value);
|
||||
|
||||
if (item) {
|
||||
if (section.id === "separator") {
|
||||
if (value === "separator-new-line") {
|
||||
return item.primary;
|
||||
}
|
||||
|
||||
return item.secondary ?? item.primary;
|
||||
}
|
||||
|
||||
return `${item.secondary} [${item.primary} ${section.title}]`;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
);
|
||||
|
||||
private _getPickerValue(): string | undefined {
|
||||
if (this._editIndex != null) {
|
||||
return this._value[this._editIndex];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async _moveItem(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
|
||||
const value = this._value;
|
||||
const newValue = value.concat();
|
||||
const element = newValue.splice(oldIndex, 1)[0];
|
||||
newValue.splice(newIndex, 0, element);
|
||||
|
||||
this._setValue(newValue);
|
||||
}
|
||||
|
||||
private async _removeItem(ev: Event) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
const idx = parseInt(
|
||||
(ev.currentTarget as HTMLElement).dataset.idx ?? "",
|
||||
10
|
||||
);
|
||||
|
||||
if (Number.isNaN(idx)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = [...this._value];
|
||||
value.splice(idx, 1);
|
||||
|
||||
if (this._editIndex !== undefined) {
|
||||
if (this._editIndex === idx) {
|
||||
this._editIndex = undefined;
|
||||
} else if (this._editIndex > idx) {
|
||||
this._editIndex -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
this._setValue(value);
|
||||
}
|
||||
|
||||
private _pickerValueChanged(ev: ValueChangedEvent<string>): void {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
|
||||
if (this.disabled || !value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newValue = [...this._value];
|
||||
|
||||
if (this._editIndex != null) {
|
||||
newValue[this._editIndex] = value;
|
||||
this._editIndex = undefined;
|
||||
} else {
|
||||
newValue.push(value);
|
||||
}
|
||||
|
||||
this._setValue(newValue);
|
||||
|
||||
if (this._picker) {
|
||||
this._picker.value = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _setValue(value: string[]) {
|
||||
const newValue = this._toValue(value);
|
||||
this.value = newValue;
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: newValue,
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
background-color: var(--mdc-text-field-fill-color, whitesmoke);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
border-end-end-radius: var(--ha-border-radius-square);
|
||||
border-end-start-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
|
||||
.container:after {
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background-color: var(
|
||||
--mdc-text-field-idle-line-color,
|
||||
rgba(0, 0, 0, 0.42)
|
||||
);
|
||||
transition:
|
||||
height 180ms ease-in-out,
|
||||
background-color 180ms ease-in-out;
|
||||
}
|
||||
|
||||
:host([disabled]) .container:after {
|
||||
background-color: var(
|
||||
--mdc-text-field-disabled-line-color,
|
||||
rgba(0, 0, 0, 0.42)
|
||||
);
|
||||
}
|
||||
|
||||
.container:focus-within:after {
|
||||
height: 2px;
|
||||
background-color: var(--mdc-theme-primary);
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin: 0 0 var(--ha-space-2);
|
||||
}
|
||||
|
||||
.add {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
ha-chip-set {
|
||||
padding: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.invalid {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.sortable-fallback {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.sortable-ghost {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.sortable-drag {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
ha-input-helper-text {
|
||||
display: block;
|
||||
margin: var(--ha-space-2) 0 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-clock-date-format-picker": HaClockDateFormatPicker;
|
||||
}
|
||||
}
|
||||
@@ -13,11 +13,7 @@ import {
|
||||
} from "../../data/media-player";
|
||||
import type { MediaSelector, MediaSelectorValue } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import {
|
||||
brandsUrl,
|
||||
extractDomainFromBrandUrl,
|
||||
isBrandUrl,
|
||||
} from "../../util/brands-url";
|
||||
import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url";
|
||||
import "../ha-alert";
|
||||
import "../ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../ha-form/types";
|
||||
@@ -76,7 +72,16 @@ export class HaMediaSelector extends LitElement {
|
||||
if (thumbnail === oldThumbnail) {
|
||||
return;
|
||||
}
|
||||
if (thumbnail && isBrandUrl(thumbnail)) {
|
||||
if (thumbnail && thumbnail.startsWith("/")) {
|
||||
this._thumbnailUrl = undefined;
|
||||
// Thumbnails served by local API require authentication
|
||||
getSignedPath(this.hass, thumbnail).then((signedPath) => {
|
||||
this._thumbnailUrl = signedPath.path;
|
||||
});
|
||||
} else if (
|
||||
thumbnail &&
|
||||
thumbnail.startsWith("https://brands.home-assistant.io")
|
||||
) {
|
||||
// The backend is not aware of the theme used by the users,
|
||||
// so we rewrite the URL to show a proper icon
|
||||
this._thumbnailUrl = brandsUrl({
|
||||
@@ -84,12 +89,6 @@ export class HaMediaSelector extends LitElement {
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
});
|
||||
} else if (thumbnail && thumbnail.startsWith("/")) {
|
||||
this._thumbnailUrl = undefined;
|
||||
// Thumbnails served by local API require authentication
|
||||
getSignedPath(this.hass, thumbnail).then((signedPath) => {
|
||||
this._thumbnailUrl = signedPath.path;
|
||||
});
|
||||
} else {
|
||||
this._thumbnailUrl = thumbnail;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { UiClockDateFormatSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-clock-date-format-picker";
|
||||
|
||||
@customElement("ha-selector-ui_clock_date_format")
|
||||
export class HaSelectorUiClockDateFormat extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: UiClockDateFormatSelector;
|
||||
|
||||
@property() public value?: string | string[];
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-clock-date-format-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-clock-date-format-picker>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-ui_clock_date_format": HaSelectorUiClockDateFormat;
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,7 @@ const LOAD_ELEMENTS = {
|
||||
location: () => import("./ha-selector-location"),
|
||||
color_temp: () => import("./ha-selector-color-temp"),
|
||||
ui_action: () => import("./ha-selector-ui-action"),
|
||||
ui_clock_date_format: () => import("./ha-selector-ui-clock-date-format"),
|
||||
ui_color: () => import("./ha-selector-ui-color"),
|
||||
ui_state_content: () => import("./ha-selector-ui-state-content"),
|
||||
};
|
||||
|
||||
@@ -144,7 +144,6 @@ export const computePanels = memoizeOne(
|
||||
if (
|
||||
!isDefaultPanel &&
|
||||
(!panel.title ||
|
||||
panel.show_in_sidebar === false ||
|
||||
hiddenPanels.includes(panel.url_path) ||
|
||||
(panel.default_visible === false &&
|
||||
!panelsOrder.includes(panel.url_path)))
|
||||
|
||||
@@ -765,16 +765,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (isBrandUrl(thumbnailUrl)) {
|
||||
// The backend is not aware of the theme used by the users,
|
||||
// so we rewrite the URL to show a proper icon
|
||||
return brandsUrl({
|
||||
domain: extractDomainFromBrandUrl(thumbnailUrl),
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
});
|
||||
}
|
||||
|
||||
if (thumbnailUrl.startsWith("/")) {
|
||||
// Thumbnails served by local API require authentication
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -797,6 +787,16 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
if (isBrandUrl(thumbnailUrl)) {
|
||||
// The backend is not aware of the theme used by the users,
|
||||
// so we rewrite the URL to show a proper icon
|
||||
thumbnailUrl = brandsUrl({
|
||||
domain: extractDomainFromBrandUrl(thumbnailUrl),
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
});
|
||||
}
|
||||
|
||||
return thumbnailUrl;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { formatDurationDigital } from "../../common/datetime/format_duration";
|
||||
import type { FrontendLocaleData } from "../translation";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
|
||||
// These attributes are hidden from the more-info window for all entities.
|
||||
export const STATE_ATTRIBUTES = [
|
||||
"entity_id",
|
||||
"assumed_state",
|
||||
@@ -28,6 +29,8 @@ export const STATE_ATTRIBUTES = [
|
||||
"available_tones",
|
||||
];
|
||||
|
||||
// These attributes are hidden from the more-info window for entities of the
|
||||
// matching domain and device_class.
|
||||
export const STATE_ATTRIBUTES_DOMAIN_CLASS = {
|
||||
sensor: {
|
||||
enum: ["options"],
|
||||
|
||||
@@ -74,6 +74,7 @@ export type Selector =
|
||||
| TTSSelector
|
||||
| TTSVoiceSelector
|
||||
| UiActionSelector
|
||||
| UiClockDateFormatSelector
|
||||
| UiColorSelector
|
||||
| UiStateContentSelector
|
||||
| BackupLocationSelector;
|
||||
@@ -505,6 +506,10 @@ export interface UiActionSelector {
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface UiClockDateFormatSelector {
|
||||
ui_clock_date_format: {} | null;
|
||||
}
|
||||
|
||||
export interface UiColorSelector {
|
||||
ui_color: {
|
||||
default_color?: string;
|
||||
|
||||
136
src/dialogs/more-info/ha-more-info-attributes.ts
Normal file
136
src/dialogs/more-info/ha-more-info-attributes.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { computeAttributeNameDisplay } from "../../common/entity/compute_attribute_display";
|
||||
import "../../components/ha-attribute-value";
|
||||
import "../../components/ha-card";
|
||||
import { computeShownAttributes } from "../../data/entity/entity_attributes";
|
||||
import type { ExtEntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
interface AttributesViewParams {
|
||||
entityId: string;
|
||||
}
|
||||
|
||||
@customElement("ha-more-info-attributes")
|
||||
class HaMoreInfoAttributes extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public entry?: ExtEntityRegistryEntry | null;
|
||||
|
||||
@property({ attribute: false }) public params?: AttributesViewParams;
|
||||
|
||||
@state() private _stateObj?: HassEntity;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("params") || changedProps.has("hass")) {
|
||||
if (this.params?.entityId && this.hass) {
|
||||
this._stateObj = this.hass.states[this.params.entityId];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.params || !this._stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const attributes = computeShownAttributes(this._stateObj);
|
||||
|
||||
return html`
|
||||
<div class="content">
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
${attributes.map(
|
||||
(attribute) => html`
|
||||
<div class="data-entry">
|
||||
<div class="key">
|
||||
${computeAttributeNameDisplay(
|
||||
this.hass.localize,
|
||||
this._stateObj!,
|
||||
this.hass.entities,
|
||||
attribute
|
||||
)}
|
||||
</div>
|
||||
<div class="value">
|
||||
<ha-attribute-value
|
||||
.hass=${this.hass}
|
||||
.attribute=${attribute}
|
||||
.stateObj=${this._stateObj}
|
||||
></ha-attribute-value>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</ha-card>
|
||||
${this._stateObj.attributes.attribution
|
||||
? html`
|
||||
<div class="attribution">
|
||||
${this._stateObj.attributes.attribution}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: var(--ha-space-6);
|
||||
padding-bottom: max(var(--safe-area-inset-bottom), var(--ha-space-6));
|
||||
}
|
||||
|
||||
ha-card {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: var(--ha-space-2) var(--ha-space-4);
|
||||
}
|
||||
|
||||
.data-entry {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: var(--ha-space-2) 0;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.data-entry:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.data-entry .value {
|
||||
max-width: 60%;
|
||||
overflow-wrap: break-word;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.key {
|
||||
flex-grow: 1;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.attribution {
|
||||
color: var(--secondary-text-color);
|
||||
text-align: center;
|
||||
margin-top: var(--ha-space-4);
|
||||
font-size: var(--ha-font-size-s);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-more-info-attributes": HaMoreInfoAttributes;
|
||||
}
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { computeAttributeNameDisplay } from "../../common/entity/compute_attribute_display";
|
||||
import "../../components/ha-attribute-value";
|
||||
import "../../components/ha-card";
|
||||
import { computeShownAttributes } from "../../data/entity/entity_attributes";
|
||||
import type { ExtEntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
interface DetailsViewParams {
|
||||
entityId: string;
|
||||
}
|
||||
|
||||
@customElement("ha-more-info-details")
|
||||
class HaMoreInfoDetails extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public entry?: ExtEntityRegistryEntry | null;
|
||||
|
||||
@property({ attribute: false }) public params?: DetailsViewParams;
|
||||
|
||||
@state() private _stateObj?: HassEntity;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("params") || changedProps.has("hass")) {
|
||||
if (this.params?.entityId && this.hass) {
|
||||
this._stateObj = this.hass.states[this.params.entityId];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.params || !this._stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const translatedState = this.hass.formatEntityState(this._stateObj);
|
||||
const detailsAttributes = computeShownAttributes(this._stateObj);
|
||||
const detailsAttributeSet = new Set(detailsAttributes);
|
||||
const builtInAttributes = Object.keys(this._stateObj.attributes).filter(
|
||||
(attribute) => !detailsAttributeSet.has(attribute)
|
||||
);
|
||||
const allAttributes = [...detailsAttributes, ...builtInAttributes];
|
||||
|
||||
return html`
|
||||
<div class="content">
|
||||
<section class="section">
|
||||
<h2 class="section-title">
|
||||
${this.hass.localize(
|
||||
"ui.components.entity.entity-state-picker.state"
|
||||
)}
|
||||
</h2>
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<div class="attribute-group">
|
||||
<div class="data-entry">
|
||||
<div class="key">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.translated"
|
||||
)}
|
||||
</div>
|
||||
<div class="value">${translatedState}</div>
|
||||
</div>
|
||||
<div class="data-entry">
|
||||
<div class="key">
|
||||
${this.hass.localize("ui.dialogs.more_info_control.raw")}
|
||||
</div>
|
||||
<div class="value">${this._stateObj.state}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2 class="section-title">
|
||||
${this.hass.localize("ui.dialogs.more_info_control.attributes")}
|
||||
</h2>
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<div class="attribute-group">
|
||||
${this._renderAttributes(allAttributes)}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
</section>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderAttributes(attributes: string[]) {
|
||||
if (attributes.length === 0) {
|
||||
return html`<div class="empty">
|
||||
${this.hass.localize("ui.common.none")}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return attributes.map(
|
||||
(attribute) => html`
|
||||
<div class="data-entry">
|
||||
<div class="key">
|
||||
${computeAttributeNameDisplay(
|
||||
this.hass.localize,
|
||||
this._stateObj!,
|
||||
this.hass.entities,
|
||||
attribute
|
||||
)}
|
||||
</div>
|
||||
<div class="value">
|
||||
<ha-attribute-value
|
||||
.hass=${this.hass}
|
||||
.attribute=${attribute}
|
||||
.stateObj=${this._stateObj}
|
||||
></ha-attribute-value>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: var(--ha-space-6);
|
||||
padding-bottom: max(var(--safe-area-inset-bottom), var(--ha-space-6));
|
||||
}
|
||||
|
||||
.section + .section {
|
||||
margin-top: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0 0 var(--ha-space-2);
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
|
||||
ha-card {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: var(--ha-space-2) var(--ha-space-4);
|
||||
}
|
||||
|
||||
.data-entry {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: var(--ha-space-2) 0;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.attribute-group .data-entry:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.data-entry .value {
|
||||
max-width: 60%;
|
||||
overflow-wrap: break-word;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.key {
|
||||
flex-grow: 1;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: var(--secondary-text-color);
|
||||
text-align: center;
|
||||
padding: var(--ha-space-2) 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-more-info-details": HaMoreInfoDetails;
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ import "../../components/ha-dropdown-item";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-icon-button-prev";
|
||||
import "../../components/ha-related-items";
|
||||
import { computeShownAttributes } from "../../data/entity/entity_attributes";
|
||||
import type {
|
||||
EntityRegistryEntry,
|
||||
ExtEntityRegistryEntry,
|
||||
@@ -343,21 +344,31 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
case "info":
|
||||
this._resetInitialView();
|
||||
break;
|
||||
case "details":
|
||||
this._showDetails();
|
||||
case "attributes":
|
||||
this._showAttributes();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _showDetails(): void {
|
||||
import("./ha-more-info-details");
|
||||
private _showAttributes(): void {
|
||||
import("./ha-more-info-attributes");
|
||||
this._childView = {
|
||||
viewTag: "ha-more-info-details",
|
||||
viewTitle: this.hass.localize("ui.dialogs.more_info_control.details"),
|
||||
viewTag: "ha-more-info-attributes",
|
||||
viewParams: { entityId: this._entityId },
|
||||
};
|
||||
}
|
||||
|
||||
private _hasDisplayableAttributes(): boolean {
|
||||
if (!this._entityId) {
|
||||
return false;
|
||||
}
|
||||
const stateObj = this.hass.states[this._entityId];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return computeShownAttributes(stateObj).length > 0;
|
||||
}
|
||||
|
||||
private _goToAddEntityTo(ev) {
|
||||
// Only check for request-selected events (from menu items), not regular clicks (from icon button)
|
||||
if (ev.type === "request-selected" && !shouldHandleRequestSelectedEvent(ev))
|
||||
@@ -579,15 +590,19 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
"ui.dialogs.more_info_control.related"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="details">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiFormatListBulletedSquare}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.details"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
${this._hasDisplayableAttributes()
|
||||
? html`
|
||||
<ha-dropdown-item value="attributes">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiFormatListBulletedSquare}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.attributes"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
: nothing}
|
||||
${this._shouldShowAddEntityTo()
|
||||
? html`
|
||||
<ha-dropdown-item value="add_to">
|
||||
|
||||
@@ -32,28 +32,21 @@ const initRouting = () => {
|
||||
new CacheFirst({ matchOptions: { ignoreSearch: true } })
|
||||
);
|
||||
|
||||
// Cache any brand images used for 1 day
|
||||
// Brands are proxied via the local API with backend caching.
|
||||
// Strip the rotating access token from cache keys so token rotation
|
||||
// doesn't bust the cache, while preserving other params like "placeholder".
|
||||
// Cache any brand images used for 30 days
|
||||
// Use revalidation so cache is always available during an extended outage
|
||||
registerRoute(
|
||||
({ url, request }) =>
|
||||
url.pathname.startsWith("/api/brands/") &&
|
||||
url.origin === "https://brands.home-assistant.io" &&
|
||||
request.destination === "image",
|
||||
new StaleWhileRevalidate({
|
||||
cacheName: "brands",
|
||||
// CORS must be forced to work for CSS images
|
||||
fetchOptions: { mode: "cors", credentials: "omit" },
|
||||
plugins: [
|
||||
{
|
||||
cacheKeyWillBeUsed: async ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
url.searchParams.delete("token");
|
||||
return url.href;
|
||||
},
|
||||
},
|
||||
// Add 404 so we quickly respond to domains with missing images
|
||||
new CacheableResponsePlugin({ statuses: [0, 200, 404] }),
|
||||
new ExpirationPlugin({
|
||||
maxAgeSeconds: 60 * 60 * 24,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 30,
|
||||
purgeOnQuotaError: true,
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -51,6 +51,8 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean }) public supervisor = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "main-page" }) public mainPage = false;
|
||||
|
||||
@property({ attribute: false }) public initialCollapsedGroups: string[] = [];
|
||||
@@ -320,6 +322,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
? html`
|
||||
<ha-dropdown-item
|
||||
.value=${id}
|
||||
.clickAction=${this._handleGroupBy}
|
||||
.selected=${id === this._groupColumn}
|
||||
class=${classMap({ selected: id === this._groupColumn })}
|
||||
>
|
||||
@@ -380,6 +383,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
.route=${this.route}
|
||||
.tabs=${this.tabs}
|
||||
.mainPage=${this.mainPage}
|
||||
.supervisor=${this.supervisor}
|
||||
.pane=${showPane && this.showFilters}
|
||||
@sorting-changed=${this._sortingChanged}
|
||||
>
|
||||
@@ -485,6 +489,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
: ""}
|
||||
<ha-data-table
|
||||
.hass=${this.hass}
|
||||
.localize=${localize}
|
||||
.narrow=${this.narrow}
|
||||
.columns=${this.columns}
|
||||
.data=${this.data}
|
||||
|
||||
@@ -96,6 +96,7 @@ class HassTabsSubpage extends LitElement {
|
||||
(page) => html`
|
||||
<a href=${page.path} @click=${this._tabClicked}>
|
||||
<ha-tab
|
||||
.hass=${this.hass}
|
||||
.active=${page.path === activeTab?.path}
|
||||
.narrow=${this.narrow}
|
||||
.name=${page.translationKey
|
||||
|
||||
@@ -578,6 +578,7 @@ class AddIntegrationDialog extends LitElement {
|
||||
}
|
||||
return html`
|
||||
<ha-integration-list-item
|
||||
brand
|
||||
.hass=${this.hass}
|
||||
.integration=${integration}
|
||||
tabindex="0"
|
||||
|
||||
@@ -30,6 +30,8 @@ export class HaIntegrationListItem extends ListItemBase {
|
||||
// eslint-disable-next-line lit/attribute-names
|
||||
@property({ type: Boolean }) hasMeta = true;
|
||||
|
||||
@property({ type: Boolean }) brand = false;
|
||||
|
||||
// @ts-expect-error
|
||||
protected override renderSingleLine() {
|
||||
if (!this.integration) {
|
||||
@@ -66,6 +68,7 @@ export class HaIntegrationListItem extends ListItemBase {
|
||||
domain: this.integration.domain,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
brand: this.brand,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
|
||||
@@ -224,6 +224,7 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
|
||||
slot="graphic"
|
||||
.src=${brandsUrl({
|
||||
domain: router.brand,
|
||||
brand: true,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
|
||||
@@ -344,7 +344,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
const item: DataTableItem = {
|
||||
icon: getPanelIcon(panelInfo),
|
||||
title: getPanelTitle(this.hass, panelInfo) || panelInfo.url_path,
|
||||
show_in_sidebar: panelInfo.show_in_sidebar || false,
|
||||
show_in_sidebar: panelInfo.title != null,
|
||||
mode: "storage",
|
||||
url_path: panelInfo.url_path,
|
||||
filename: "",
|
||||
@@ -487,7 +487,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
title: getPanelTitle(this.hass, panelInfo) || panelInfo.url_path,
|
||||
icon: getPanelIcon(panelInfo),
|
||||
requireAdmin: panelInfo.require_admin || false,
|
||||
showInSidebar: panelInfo.show_in_sidebar || false,
|
||||
showInSidebar: panelInfo.title != null,
|
||||
isDefault: panelInfo.url_path === defaultPanel,
|
||||
updatePanel: async (values) => {
|
||||
await updatePanel(this.hass!, panelInfo.url_path, values);
|
||||
|
||||
@@ -39,9 +39,6 @@ export class PowerViewStrategy extends ReactiveElement {
|
||||
const hasPowerDevices = prefs?.device_consumption.some(
|
||||
(device) => device.stat_rate
|
||||
);
|
||||
const hasWaterDevices = prefs?.device_consumption_water.some(
|
||||
(device) => device.stat_rate
|
||||
);
|
||||
const hasWaterSources = prefs?.energy_sources.some(
|
||||
(source) => source.type === "water" && source.stat_rate
|
||||
);
|
||||
@@ -67,12 +64,11 @@ export class PowerViewStrategy extends ReactiveElement {
|
||||
max_columns: 2,
|
||||
};
|
||||
|
||||
// No sources configured
|
||||
// No power sources configured
|
||||
if (
|
||||
!prefs ||
|
||||
(!hasPowerSources &&
|
||||
!hasPowerDevices &&
|
||||
!hasWaterDevices &&
|
||||
!hasWaterSources &&
|
||||
!hasGasSources)
|
||||
) {
|
||||
@@ -130,24 +126,6 @@ export class PowerViewStrategy extends ReactiveElement {
|
||||
});
|
||||
}
|
||||
|
||||
if (hasWaterDevices) {
|
||||
const showFloorsAndAreas = shouldShowFloorsAndAreas(
|
||||
prefs.device_consumption_water,
|
||||
hass,
|
||||
(d) => d.stat_rate
|
||||
);
|
||||
chartsSection.cards!.push({
|
||||
title: hass.localize("ui.panel.energy.cards.water_flow_sankey_title"),
|
||||
type: "water-flow-sankey",
|
||||
collection_key: collectionKey,
|
||||
group_by_floor: showFloorsAndAreas,
|
||||
group_by_area: showFloorsAndAreas,
|
||||
grid_options: {
|
||||
columns: 36,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
tiles.forEach((card) => {
|
||||
tileSection.cards!.push({
|
||||
...card,
|
||||
|
||||
@@ -18,7 +18,6 @@ import "../../../components/ha-svg-icon";
|
||||
import { cameraUrlWithWidthHeight } from "../../../data/camera";
|
||||
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { addBrandsAuth } from "../../../util/brands-url";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
@@ -144,7 +143,7 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
|
||||
|
||||
if (!entityPicture) return undefined;
|
||||
|
||||
let imageUrl = this.hass!.hassUrl(addBrandsAuth(entityPicture));
|
||||
let imageUrl = this.hass!.hassUrl(entityPicture);
|
||||
if (computeStateDomain(stateObj) === "camera") {
|
||||
imageUrl = cameraUrlWithWidthHeight(imageUrl, 32, 32);
|
||||
}
|
||||
|
||||
221
src/panels/lovelace/cards/clock/clock-date-format.ts
Normal file
221
src/panels/lovelace/cards/clock/clock-date-format.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
import type { ClockCardConfig, ClockCardDatePart } from "../types";
|
||||
|
||||
type ClockCardSeparatorPart = Extract<
|
||||
ClockCardDatePart,
|
||||
"separator-dash" | "separator-slash" | "separator-dot" | "separator-new-line"
|
||||
>;
|
||||
|
||||
type ClockCardValuePart = Exclude<ClockCardDatePart, ClockCardSeparatorPart>;
|
||||
|
||||
/**
|
||||
* Normalized date configuration used by clock card renderers.
|
||||
*/
|
||||
export interface ClockCardDateConfig {
|
||||
parts: ClockCardDatePart[];
|
||||
}
|
||||
|
||||
/**
|
||||
* All selectable date tokens exposed by the clock card editor.
|
||||
*/
|
||||
export const CLOCK_CARD_DATE_PARTS: readonly ClockCardDatePart[] = [
|
||||
"weekday-short",
|
||||
"weekday-long",
|
||||
"day-numeric",
|
||||
"day-2-digit",
|
||||
"month-short",
|
||||
"month-long",
|
||||
"month-numeric",
|
||||
"month-2-digit",
|
||||
"year-2-digit",
|
||||
"year-numeric",
|
||||
"separator-dash",
|
||||
"separator-slash",
|
||||
"separator-dot",
|
||||
"separator-new-line",
|
||||
];
|
||||
|
||||
const DATE_PART_OPTIONS: Record<
|
||||
ClockCardValuePart,
|
||||
Pick<Intl.DateTimeFormatOptions, "weekday" | "day" | "month" | "year">
|
||||
> = {
|
||||
"weekday-short": { weekday: "short" },
|
||||
"weekday-long": { weekday: "long" },
|
||||
"day-numeric": { day: "numeric" },
|
||||
"day-2-digit": { day: "2-digit" },
|
||||
"month-short": { month: "short" },
|
||||
"month-long": { month: "long" },
|
||||
"month-numeric": { month: "numeric" },
|
||||
"month-2-digit": { month: "2-digit" },
|
||||
"year-2-digit": { year: "2-digit" },
|
||||
"year-numeric": { year: "numeric" },
|
||||
};
|
||||
|
||||
const DATE_SEPARATORS: Record<ClockCardSeparatorPart, string> = {
|
||||
"separator-dash": "-",
|
||||
"separator-slash": "/",
|
||||
"separator-dot": ".",
|
||||
"separator-new-line": "\n",
|
||||
};
|
||||
|
||||
const DATE_SEPARATOR_PARTS = new Set<ClockCardSeparatorPart>([
|
||||
"separator-dash",
|
||||
"separator-slash",
|
||||
"separator-dot",
|
||||
"separator-new-line",
|
||||
]);
|
||||
|
||||
const DATE_PART_FORMATTERS = new Map<string, Intl.DateTimeFormat>();
|
||||
|
||||
const isClockCardDatePart = (value: string): value is ClockCardDatePart =>
|
||||
CLOCK_CARD_DATE_PARTS.includes(value as ClockCardDatePart);
|
||||
|
||||
const isDateSeparatorPart = (
|
||||
part: ClockCardDatePart
|
||||
): part is ClockCardSeparatorPart =>
|
||||
DATE_SEPARATOR_PARTS.has(part as ClockCardSeparatorPart);
|
||||
|
||||
/**
|
||||
* Returns a reusable formatter for a specific date token.
|
||||
*/
|
||||
const getDatePartFormatter = (
|
||||
part: ClockCardValuePart,
|
||||
language: string,
|
||||
timeZone?: string
|
||||
): Intl.DateTimeFormat => {
|
||||
const cacheKey = `${language}|${timeZone || ""}|${part}`;
|
||||
const cached = DATE_PART_FORMATTERS.get(cacheKey);
|
||||
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const formatter = new Intl.DateTimeFormat(language, {
|
||||
...DATE_PART_OPTIONS[part],
|
||||
...(timeZone ? { timeZone } : {}),
|
||||
});
|
||||
|
||||
DATE_PART_FORMATTERS.set(cacheKey, formatter);
|
||||
|
||||
return formatter;
|
||||
};
|
||||
|
||||
const formatDatePart = (
|
||||
part: ClockCardValuePart,
|
||||
date: Date,
|
||||
language: string,
|
||||
timeZone?: string
|
||||
) => getDatePartFormatter(part, language, timeZone).format(date);
|
||||
|
||||
/**
|
||||
* Applies a single date token to Intl.DateTimeFormat options.
|
||||
*/
|
||||
const applyDatePartOption = (
|
||||
options: Intl.DateTimeFormatOptions,
|
||||
part: ClockCardDatePart
|
||||
) => {
|
||||
if (isDateSeparatorPart(part)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const partOptions = DATE_PART_OPTIONS[part];
|
||||
|
||||
if (partOptions.weekday) {
|
||||
options.weekday = partOptions.weekday;
|
||||
}
|
||||
|
||||
if (partOptions.day) {
|
||||
options.day = partOptions.day;
|
||||
}
|
||||
|
||||
if (partOptions.month) {
|
||||
options.month = partOptions.month;
|
||||
}
|
||||
|
||||
if (partOptions.year) {
|
||||
options.year = partOptions.year;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sanitizes configured date tokens while preserving their literal order.
|
||||
*/
|
||||
const normalizeDateParts = (
|
||||
parts: ClockCardConfig["date_format"]
|
||||
): ClockCardDatePart[] =>
|
||||
parts?.filter((part): part is ClockCardDatePart =>
|
||||
isClockCardDatePart(part)
|
||||
) || [];
|
||||
|
||||
/**
|
||||
* Returns a normalized date config from a card configuration object.
|
||||
*/
|
||||
export const getClockCardDateConfig = (
|
||||
config?: Pick<ClockCardConfig, "date_format">
|
||||
): ClockCardDateConfig => ({
|
||||
parts: normalizeDateParts(config?.date_format),
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks whether the clock configuration resolves to any visible date output.
|
||||
*/
|
||||
export const hasClockCardDate = (
|
||||
config?: Pick<ClockCardConfig, "date_format">
|
||||
): boolean => getClockCardDateConfig(config).parts.length > 0;
|
||||
|
||||
/**
|
||||
* Converts normalized date tokens into Intl.DateTimeFormat options.
|
||||
*
|
||||
* Separator tokens are ignored. If multiple tokens target the same Intl field,
|
||||
* the last one wins.
|
||||
*/
|
||||
export const getClockCardDateTimeFormatOptions = (
|
||||
dateConfig: ClockCardDateConfig
|
||||
): Intl.DateTimeFormatOptions => {
|
||||
const options: Intl.DateTimeFormatOptions = {};
|
||||
|
||||
dateConfig.parts.forEach((part) => {
|
||||
applyDatePartOption(options, part);
|
||||
});
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds the final date string from literal date tokens.
|
||||
*
|
||||
* Value tokens are localized through Intl.DateTimeFormat. Separator tokens are
|
||||
* always rendered literally. A default space is only inserted between adjacent
|
||||
* value tokens.
|
||||
*/
|
||||
export const formatClockCardDate = (
|
||||
date: Date,
|
||||
dateConfig: ClockCardDateConfig,
|
||||
language: string,
|
||||
timeZone?: string
|
||||
): string => {
|
||||
let result = "";
|
||||
let previousRenderedPartWasValue = false;
|
||||
|
||||
dateConfig.parts.forEach((part) => {
|
||||
if (isDateSeparatorPart(part)) {
|
||||
result += DATE_SEPARATORS[part];
|
||||
previousRenderedPartWasValue = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const value = formatDatePart(part, date, language, timeZone);
|
||||
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (previousRenderedPartWasValue) {
|
||||
result += " ";
|
||||
}
|
||||
|
||||
result += value;
|
||||
previousRenderedPartWasValue = true;
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
@@ -2,9 +2,16 @@ 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";
|
||||
import {
|
||||
formatClockCardDate,
|
||||
getClockCardDateConfig,
|
||||
hasClockCardDate,
|
||||
} from "./clock-date-format";
|
||||
|
||||
function romanize12HourClock(num: number) {
|
||||
const numerals = [
|
||||
@@ -26,6 +33,11 @@ function romanize12HourClock(num: number) {
|
||||
return numerals[num];
|
||||
}
|
||||
|
||||
const DATE_UPDATE_INTERVAL = 60_000;
|
||||
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,42 +52,18 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
|
||||
@state() private _secondOffsetSec?: number;
|
||||
|
||||
private _initDate() {
|
||||
if (!this.config || !this.hass) {
|
||||
return;
|
||||
}
|
||||
@state() private _date?: string;
|
||||
|
||||
let locale = this.hass.locale;
|
||||
if (this.config.time_format) {
|
||||
locale = { ...locale, time_format: this.config.time_format };
|
||||
}
|
||||
private _dateInterval?: number;
|
||||
|
||||
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),
|
||||
});
|
||||
private _timeZone?: string;
|
||||
|
||||
this._computeOffsets();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("hass")) {
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass || oldHass.locale !== this.hass?.locale) {
|
||||
this._initDate();
|
||||
}
|
||||
}
|
||||
}
|
||||
private _language?: string;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
document.addEventListener("visibilitychange", this._handleVisibilityChange);
|
||||
this._computeOffsets();
|
||||
this._initDate();
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
@@ -84,18 +72,87 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
"visibilitychange",
|
||||
this._handleVisibilityChange
|
||||
);
|
||||
this._stopDateTick();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("config") || changedProps.has("hass")) {
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (
|
||||
changedProps.has("config") ||
|
||||
!oldHass ||
|
||||
oldHass.locale !== this.hass?.locale
|
||||
) {
|
||||
this._initDate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _handleVisibilityChange = () => {
|
||||
if (!document.hidden) {
|
||||
this._computeOffsets();
|
||||
this._updateDate();
|
||||
}
|
||||
};
|
||||
|
||||
private _initDate() {
|
||||
if (!this.config || !this.hass) {
|
||||
this._stopDateTick();
|
||||
this._date = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
let locale = this.hass.locale;
|
||||
if (this.config.time_format) {
|
||||
locale = { ...locale, time_format: this.config.time_format };
|
||||
}
|
||||
|
||||
const timeZone =
|
||||
this.config.time_zone ||
|
||||
resolveTimeZone(locale.time_zone, this.hass.config?.time_zone);
|
||||
|
||||
this._language = this.hass.locale.language;
|
||||
this._timeZone = timeZone;
|
||||
|
||||
this._dateTimeFormat = new Intl.DateTimeFormat(this.hass.locale.language, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hourCycle: "h12",
|
||||
timeZone,
|
||||
});
|
||||
|
||||
this._computeOffsets();
|
||||
this._updateDate();
|
||||
|
||||
if (this.isConnected && hasClockCardDate(this.config)) {
|
||||
this._startDateTick();
|
||||
} else {
|
||||
this._stopDateTick();
|
||||
}
|
||||
}
|
||||
|
||||
private _startDateTick() {
|
||||
this._stopDateTick();
|
||||
this._dateInterval = window.setInterval(
|
||||
() => this._updateDate(),
|
||||
DATE_UPDATE_INTERVAL
|
||||
);
|
||||
}
|
||||
|
||||
private _stopDateTick() {
|
||||
if (this._dateInterval) {
|
||||
clearInterval(this._dateInterval);
|
||||
this._dateInterval = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _computeOffsets() {
|
||||
if (!this._dateTimeFormat) return;
|
||||
|
||||
const parts = this._dateTimeFormat.formatToParts();
|
||||
const date = new Date();
|
||||
|
||||
const parts = this._dateTimeFormat.formatToParts(date);
|
||||
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 +160,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 = date.getMilliseconds();
|
||||
const secondsWithMs = second + ms / 1000;
|
||||
|
||||
const hour12 = hour % 12;
|
||||
@@ -113,16 +170,44 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
this._hourOffsetSec = hour12 * 3600 + minute * 60 + secondsWithMs;
|
||||
}
|
||||
|
||||
private _updateDate() {
|
||||
if (!this.config || !hasClockCardDate(this.config) || !this._language) {
|
||||
this._date = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
const dateConfig = getClockCardDateConfig(this.config);
|
||||
this._date = formatClockCardDate(
|
||||
new Date(),
|
||||
dateConfig,
|
||||
this._language,
|
||||
this._timeZone
|
||||
);
|
||||
}
|
||||
|
||||
private _computeClock = memoizeOne((config: ClockCardConfig) => {
|
||||
const faceParts = config.face_style?.split("_");
|
||||
const dateConfig = getClockCardDateConfig(config);
|
||||
const showDate = hasClockCardDate(config);
|
||||
const isLongDate =
|
||||
dateConfig.parts.includes("month-long") ||
|
||||
dateConfig.parts.includes("weekday-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,
|
||||
showDate,
|
||||
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, showDate } =
|
||||
this._computeClock(this.config);
|
||||
|
||||
const indicator = (number?: number) => html`
|
||||
<div
|
||||
@@ -163,14 +248,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 +263,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 +295,34 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
`
|
||||
)
|
||||
: nothing}
|
||||
${showDate
|
||||
? html`<div
|
||||
class=${classMap({
|
||||
date: true,
|
||||
[sizeClass]: true,
|
||||
"long-date": isLongDate,
|
||||
})}
|
||||
>
|
||||
${this._date
|
||||
?.split("\n")
|
||||
.map((line, index) =>
|
||||
index > 0 ? html`<br />${line}` : line
|
||||
)}
|
||||
</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 +331,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>
|
||||
@@ -407,6 +516,36 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
transform: translate(-50%, 0) rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.date {
|
||||
position: absolute;
|
||||
top: 68%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: block;
|
||||
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;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.date.long-date:not(.size-medium):not(.size-large) {
|
||||
font-size: var(--ha-font-size-xs);
|
||||
}
|
||||
|
||||
.date.size-medium {
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
|
||||
.date.size-large {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,11 @@ import type { ClockCardConfig } from "../types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { useAmPm } from "../../../../common/datetime/use_am_pm";
|
||||
import { resolveTimeZone } from "../../../../common/datetime/resolve-time-zone";
|
||||
import {
|
||||
formatClockCardDate,
|
||||
getClockCardDateConfig,
|
||||
hasClockCardDate,
|
||||
} from "./clock-date-format";
|
||||
|
||||
const INTERVAL = 1000;
|
||||
|
||||
@@ -24,10 +29,20 @@ export class HuiClockCardDigital extends LitElement {
|
||||
|
||||
@state() private _timeAmPm?: string;
|
||||
|
||||
@state() private _date?: string;
|
||||
|
||||
private _tickInterval?: undefined | number;
|
||||
|
||||
private _lastDateMinute?: string;
|
||||
|
||||
private _timeZone?: string;
|
||||
|
||||
private _language?: string;
|
||||
|
||||
private _initDate() {
|
||||
if (!this.config || !this.hass) {
|
||||
this._date = undefined;
|
||||
this._lastDateMinute = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -37,24 +52,35 @@ export class HuiClockCardDigital extends LitElement {
|
||||
locale = { ...locale, time_format: this.config.time_format };
|
||||
}
|
||||
|
||||
const timeZone =
|
||||
this.config?.time_zone ||
|
||||
resolveTimeZone(locale.time_zone, this.hass.config?.time_zone);
|
||||
|
||||
const h12 = useAmPm(locale);
|
||||
this._language = this.hass.locale.language;
|
||||
this._timeZone = timeZone;
|
||||
|
||||
this._dateTimeFormat = new Intl.DateTimeFormat(this.hass.locale.language, {
|
||||
hour: h12 ? "numeric" : "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hourCycle: h12 ? "h12" : "h23",
|
||||
timeZone:
|
||||
this.config?.time_zone ||
|
||||
resolveTimeZone(locale.time_zone, this.hass.config?.time_zone),
|
||||
timeZone,
|
||||
});
|
||||
|
||||
this._lastDateMinute = undefined;
|
||||
|
||||
this._tick();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("hass")) {
|
||||
if (changedProps.has("config") || changedProps.has("hass")) {
|
||||
const oldHass = changedProps.get("hass");
|
||||
if (!oldHass || oldHass.locale !== this.hass?.locale) {
|
||||
if (
|
||||
changedProps.has("config") ||
|
||||
!oldHass ||
|
||||
oldHass.locale !== this.hass?.locale
|
||||
) {
|
||||
this._initDate();
|
||||
}
|
||||
}
|
||||
@@ -71,6 +97,7 @@ export class HuiClockCardDigital extends LitElement {
|
||||
}
|
||||
|
||||
private _startTick() {
|
||||
this._stopTick();
|
||||
this._tickInterval = window.setInterval(() => this._tick(), INTERVAL);
|
||||
this._tick();
|
||||
}
|
||||
@@ -85,7 +112,8 @@ export class HuiClockCardDigital extends LitElement {
|
||||
private _tick() {
|
||||
if (!this._dateTimeFormat) return;
|
||||
|
||||
const parts = this._dateTimeFormat.formatToParts();
|
||||
const date = new Date();
|
||||
const parts = this._dateTimeFormat.formatToParts(date);
|
||||
|
||||
this._timeHour = parts.find((part) => part.type === "hour")?.value;
|
||||
this._timeMinute = parts.find((part) => part.type === "minute")?.value;
|
||||
@@ -93,6 +121,33 @@ export class HuiClockCardDigital extends LitElement {
|
||||
? parts.find((part) => part.type === "second")?.value
|
||||
: undefined;
|
||||
this._timeAmPm = parts.find((part) => part.type === "dayPeriod")?.value;
|
||||
|
||||
this._updateDate(date);
|
||||
}
|
||||
|
||||
private _updateDate(date: Date) {
|
||||
if (!this.config || !hasClockCardDate(this.config) || !this._language) {
|
||||
this._date = undefined;
|
||||
this._lastDateMinute = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this._timeMinute !== undefined &&
|
||||
this._timeMinute === this._lastDateMinute &&
|
||||
this._date !== undefined
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dateConfig = getClockCardDateConfig(this.config);
|
||||
this._date = formatClockCardDate(
|
||||
date,
|
||||
dateConfig,
|
||||
this._language,
|
||||
this._timeZone
|
||||
);
|
||||
this._lastDateMinute = this._timeMinute;
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -101,18 +156,30 @@ export class HuiClockCardDigital extends LitElement {
|
||||
const sizeClass = this.config.clock_size
|
||||
? `size-${this.config.clock_size}`
|
||||
: "";
|
||||
const showDate = hasClockCardDate(this.config);
|
||||
|
||||
return html`
|
||||
<div class="time-parts ${sizeClass}">
|
||||
<div class="time-part hour">${this._timeHour}</div>
|
||||
<div class="time-part minute">${this._timeMinute}</div>
|
||||
${this._timeSecond !== undefined
|
||||
? html`<div class="time-part second">${this._timeSecond}</div>`
|
||||
: nothing}
|
||||
${this._timeAmPm !== undefined
|
||||
? html`<div class="time-part am-pm">${this._timeAmPm}</div>`
|
||||
: nothing}
|
||||
<div class="clock-container">
|
||||
<div class="time-parts ${sizeClass}">
|
||||
<div class="time-part hour">${this._timeHour}</div>
|
||||
<div class="time-part minute">${this._timeMinute}</div>
|
||||
${this._timeSecond !== undefined
|
||||
? html`<div class="time-part second">${this._timeSecond}</div>`
|
||||
: nothing}
|
||||
${this._timeAmPm !== undefined
|
||||
? html`<div class="time-part am-pm">${this._timeAmPm}</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
${showDate
|
||||
? html`<div class="date-container">
|
||||
<div class="date ${sizeClass}">
|
||||
${this._date
|
||||
?.split("\n")
|
||||
.map((line, index) => (index > 0 ? html`<br />${line}` : line))}
|
||||
</div>
|
||||
</div>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -121,6 +188,17 @@ export class HuiClockCardDigital extends LitElement {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.clock-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.date-container {
|
||||
width: 100%;
|
||||
margin-top: var(--ha-space-1);
|
||||
}
|
||||
|
||||
.time-parts {
|
||||
align-items: center;
|
||||
display: grid;
|
||||
@@ -188,6 +266,21 @@ export class HuiClockCardDigital extends LitElement {
|
||||
content: ":";
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.date {
|
||||
text-align: center;
|
||||
opacity: 0.8;
|
||||
font-size: var(--ha-font-size-s);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.date.size-medium {
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
|
||||
.date.size-large {
|
||||
font-size: var(--ha-font-size-2xl);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,8 +28,6 @@ import {
|
||||
formatDateMonthYear,
|
||||
formatDateShort,
|
||||
formatDateVeryShort,
|
||||
formatDateWeekdayShortDate,
|
||||
formatDateWeekdayVeryShortDate,
|
||||
} from "../../../../../common/datetime/format_date";
|
||||
import { formatTime } from "../../../../../common/datetime/format_time";
|
||||
import type { ECOption } from "../../../../../resources/echarts/echarts";
|
||||
@@ -224,9 +222,7 @@ function formatTooltip(
|
||||
if (suggestedPeriod === "month") {
|
||||
period = `${formatDateMonthYear(date, locale, config)}`;
|
||||
} else if (suggestedPeriod === "day") {
|
||||
period = showCompareYear
|
||||
? formatDateWeekdayShortDate(date, locale, config)
|
||||
: formatDateWeekdayVeryShortDate(date, locale, config);
|
||||
period = `${(showCompareYear ? formatDateShort : formatDateVeryShort)(date, locale, config)}`;
|
||||
} else {
|
||||
period = `${
|
||||
compare
|
||||
|
||||
@@ -21,7 +21,6 @@ import { cameraUrlWithWidthHeight } from "../../../data/camera";
|
||||
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||
import "../../../state-display/state-display";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { addBrandsAuth } from "../../../util/brands-url";
|
||||
import "../card-features/hui-card-features";
|
||||
import type { LovelaceCardFeatureContext } from "../card-features/types";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
@@ -159,7 +158,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
|
||||
if (!entityPicture) return undefined;
|
||||
|
||||
let imageUrl = this.hass!.hassUrl(addBrandsAuth(entityPicture));
|
||||
let imageUrl = this.hass!.hassUrl(entityPicture);
|
||||
if (computeDomain(entity.entity_id) === "camera") {
|
||||
imageUrl = cameraUrlWithWidthHeight(imageUrl, 80, 80);
|
||||
}
|
||||
|
||||
@@ -251,14 +251,6 @@ export interface WaterSankeyCardConfig extends EnergyCardBaseConfig {
|
||||
group_by_area?: boolean;
|
||||
}
|
||||
|
||||
export interface WaterFlowSankeyCardConfig extends EnergyCardBaseConfig {
|
||||
type: "water-flow-sankey";
|
||||
title?: string;
|
||||
layout?: "vertical" | "horizontal" | "auto";
|
||||
group_by_floor?: boolean;
|
||||
group_by_area?: boolean;
|
||||
}
|
||||
|
||||
export interface PowerSourcesGraphCardConfig extends EnergyCardBaseConfig {
|
||||
type: "power-sources-graph";
|
||||
title?: string;
|
||||
@@ -459,12 +451,29 @@ export interface ClockCardConfig extends LovelaceCardConfig {
|
||||
time_format?: TimeFormat;
|
||||
time_zone?: string;
|
||||
no_background?: boolean;
|
||||
date_format?: ClockCardDatePart[];
|
||||
// Analog clock options
|
||||
border?: boolean;
|
||||
ticks?: "none" | "quarter" | "hour" | "minute";
|
||||
face_style?: "markers" | "numbers_upright" | "roman";
|
||||
}
|
||||
|
||||
export type ClockCardDatePart =
|
||||
| "weekday-short"
|
||||
| "weekday-long"
|
||||
| "day-numeric"
|
||||
| "day-2-digit"
|
||||
| "month-short"
|
||||
| "month-long"
|
||||
| "month-numeric"
|
||||
| "month-2-digit"
|
||||
| "year-2-digit"
|
||||
| "year-numeric"
|
||||
| "separator-dash"
|
||||
| "separator-slash"
|
||||
| "separator-dot"
|
||||
| "separator-new-line";
|
||||
|
||||
export interface MediaControlCardConfig extends LovelaceCardConfig {
|
||||
entity: string;
|
||||
name?: string | EntityNameItem | EntityNameItem[];
|
||||
|
||||
@@ -1,628 +0,0 @@
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
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 "../../../../components/ha-card";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { EnergyData } from "../../../../data/energy";
|
||||
import {
|
||||
formatFlowRateShort,
|
||||
getEnergyDataCollection,
|
||||
getFlowRateFromState,
|
||||
} from "../../../../data/energy";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { LovelaceCard, LovelaceGridOptions } from "../../types";
|
||||
import type { WaterFlowSankeyCardConfig } from "../types";
|
||||
import "../../../../components/chart/ha-sankey-chart";
|
||||
import type { Link, Node } from "../../../../components/chart/ha-sankey-chart";
|
||||
import { getGraphColorByIndex } from "../../../../common/color/colors";
|
||||
import { getEntityContext } from "../../../../common/entity/context/get_entity_context";
|
||||
import { MobileAwareMixin } from "../../../../mixins/mobile-aware-mixin";
|
||||
|
||||
const DEFAULT_CONFIG: Partial<WaterFlowSankeyCardConfig> = {
|
||||
group_by_floor: true,
|
||||
group_by_area: true,
|
||||
};
|
||||
|
||||
// Minimum flow threshold as a fraction of total inflow to display a device node.
|
||||
// Devices below this threshold will be grouped into an "Other" node.
|
||||
const MIN_FLOW_THRESHOLD_FACTOR = 0.001; // 0.1% of total inflow
|
||||
|
||||
interface SmallConsumer {
|
||||
statRate: string;
|
||||
name: string | undefined;
|
||||
value: number;
|
||||
effectiveParent: string | undefined;
|
||||
idx: number;
|
||||
}
|
||||
|
||||
@customElement("hui-water-flow-sankey-card")
|
||||
class HuiWaterFlowSankeyCard
|
||||
extends SubscribeMixin(MobileAwareMixin(LitElement))
|
||||
implements LovelaceCard
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public layout?: string;
|
||||
|
||||
@state() private _config?: WaterFlowSankeyCardConfig;
|
||||
|
||||
@state() private _data?: EnergyData;
|
||||
|
||||
private _entities = new Set<string>();
|
||||
|
||||
protected hassSubscribeRequiredHostProps = ["_config"];
|
||||
|
||||
public setConfig(config: WaterFlowSankeyCardConfig): void {
|
||||
this._config = { ...DEFAULT_CONFIG, ...config };
|
||||
}
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
getEnergyDataCollection(this.hass, {
|
||||
key: this._config?.collection_key,
|
||||
}).subscribe((data) => {
|
||||
this._data = data;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public getCardSize(): Promise<number> | number {
|
||||
return 5;
|
||||
}
|
||||
|
||||
getGridOptions(): LovelaceGridOptions {
|
||||
return {
|
||||
columns: 12,
|
||||
min_columns: 6,
|
||||
rows: 6,
|
||||
min_rows: 2,
|
||||
};
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (
|
||||
changedProps.has("_config") ||
|
||||
changedProps.has("_data") ||
|
||||
changedProps.has("_isMobileSize")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (changedProps.has("hass")) {
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass || !this._entities.size) {
|
||||
return true;
|
||||
}
|
||||
for (const entityId of this._entities) {
|
||||
if (oldHass.states[entityId] !== this.hass.states[entityId]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!this._data) {
|
||||
return html`${this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.loading"
|
||||
)}`;
|
||||
}
|
||||
|
||||
const prefs = this._data.prefs;
|
||||
const computedStyle = getComputedStyle(this);
|
||||
|
||||
// Clear tracked entities and rebuild set
|
||||
this._entities.clear();
|
||||
|
||||
// Collect water sources with stat_rate
|
||||
const waterSources = prefs.energy_sources.filter(
|
||||
(source) => source.type === "water" && source.stat_rate
|
||||
);
|
||||
|
||||
let totalInflow = 0;
|
||||
waterSources.forEach((source) => {
|
||||
if (source.type === "water" && source.stat_rate) {
|
||||
const value = this._getCurrentFlowRate(source.stat_rate);
|
||||
if (value > 0) totalInflow += value;
|
||||
}
|
||||
});
|
||||
|
||||
// When there are no source meters, pre-compute total device flow so the
|
||||
// home node has the correct value (sum of all device consumption) rather
|
||||
// than 0. This avoids a broken sankey where the root node has value=0
|
||||
// while its children have positive values.
|
||||
let totalDeviceFlow = 0;
|
||||
if (waterSources.length === 0) {
|
||||
prefs.device_consumption_water.forEach((device) => {
|
||||
if (device.stat_rate) {
|
||||
totalDeviceFlow += this._getCurrentFlowRate(device.stat_rate);
|
||||
}
|
||||
});
|
||||
}
|
||||
const effectiveTotalInflow =
|
||||
waterSources.length === 0 ? totalDeviceFlow : totalInflow;
|
||||
|
||||
// Calculate dynamic threshold
|
||||
const minFlowThreshold = effectiveTotalInflow * MIN_FLOW_THRESHOLD_FACTOR;
|
||||
|
||||
const nodes: Node[] = [];
|
||||
const links: Link[] = [];
|
||||
const waterColor = computedStyle
|
||||
.getPropertyValue("--energy-water-color")
|
||||
.trim();
|
||||
const primaryColor = computedStyle
|
||||
.getPropertyValue("--primary-color")
|
||||
.trim();
|
||||
|
||||
// Determine the "root" node for device links.
|
||||
// - 0 sources: home node (value = sum of device values, computed later)
|
||||
// - 1 source: that source node is the root (no home node)
|
||||
// - >1 sources: home node aggregates all sources
|
||||
const showHomeNode = waterSources.length !== 1;
|
||||
let rootNodeId: string;
|
||||
|
||||
if (showHomeNode) {
|
||||
// Add source nodes and link to home
|
||||
waterSources.forEach((source) => {
|
||||
if (source.type !== "water" || !source.stat_rate) return;
|
||||
const value = this._getCurrentFlowRate(source.stat_rate);
|
||||
if (value <= 0) return;
|
||||
const sourceNodeId = `water_source_${source.stat_rate}`;
|
||||
nodes.push({
|
||||
id: sourceNodeId,
|
||||
label:
|
||||
this._getEntityLabel(source.stat_rate) ||
|
||||
this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.water"
|
||||
),
|
||||
value,
|
||||
color: waterColor,
|
||||
index: 0,
|
||||
entityId: source.stat_rate,
|
||||
});
|
||||
links.push({ source: sourceNodeId, target: "home" });
|
||||
});
|
||||
|
||||
const homeNode: Node = {
|
||||
id: "home",
|
||||
label: this.hass.config.location_name,
|
||||
value: Math.max(0, effectiveTotalInflow),
|
||||
color: primaryColor,
|
||||
index: 1,
|
||||
};
|
||||
nodes.push(homeNode);
|
||||
rootNodeId = "home";
|
||||
} else {
|
||||
// Single source: that source IS the root, no home node
|
||||
const source = waterSources[0];
|
||||
if (source.type === "water" && source.stat_rate) {
|
||||
const value = this._getCurrentFlowRate(source.stat_rate);
|
||||
nodes.push({
|
||||
id: source.stat_rate,
|
||||
label:
|
||||
this._getEntityLabel(source.stat_rate) ||
|
||||
this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.water"
|
||||
),
|
||||
value: Math.max(0, value),
|
||||
color: waterColor,
|
||||
index: 0,
|
||||
entityId: source.stat_rate,
|
||||
});
|
||||
rootNodeId = source.stat_rate;
|
||||
} else {
|
||||
// Fallback (shouldn't happen)
|
||||
rootNodeId = "home";
|
||||
}
|
||||
}
|
||||
|
||||
// Build a map of device relationships for hierarchy resolution
|
||||
const deviceMap = new Map<
|
||||
string,
|
||||
{ stat_rate?: string; included_in_stat?: string }
|
||||
>();
|
||||
prefs.device_consumption_water.forEach((device) => {
|
||||
deviceMap.set(device.stat_consumption, {
|
||||
stat_rate: device.stat_rate,
|
||||
included_in_stat: device.included_in_stat,
|
||||
});
|
||||
});
|
||||
|
||||
// Set of stat_rate entities that will be rendered as nodes
|
||||
const renderedStatRates = new Set<string>();
|
||||
prefs.device_consumption_water.forEach((device) => {
|
||||
if (device.stat_rate) {
|
||||
const value = this._getCurrentFlowRate(device.stat_rate);
|
||||
if (value >= minFlowThreshold) {
|
||||
renderedStatRates.add(device.stat_rate);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Find the effective parent for hierarchy
|
||||
const findEffectiveParent = (
|
||||
includedInStat: string | undefined
|
||||
): string | undefined => {
|
||||
let currentParent = includedInStat;
|
||||
while (currentParent) {
|
||||
const parentDevice = deviceMap.get(currentParent);
|
||||
if (!parentDevice) return undefined;
|
||||
if (
|
||||
parentDevice.stat_rate &&
|
||||
renderedStatRates.has(parentDevice.stat_rate)
|
||||
) {
|
||||
return parentDevice.stat_rate;
|
||||
}
|
||||
currentParent = parentDevice.included_in_stat;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
let untrackedConsumption = effectiveTotalInflow;
|
||||
const deviceNodes: Node[] = [];
|
||||
const parentLinks: Record<string, string> = {};
|
||||
const smallConsumersByParent = new Map<string, SmallConsumer[]>();
|
||||
|
||||
prefs.device_consumption_water.forEach((device, idx) => {
|
||||
if (!device.stat_rate) return;
|
||||
const value = this._getCurrentFlowRate(device.stat_rate);
|
||||
const effectiveParent = findEffectiveParent(device.included_in_stat);
|
||||
|
||||
if (value < minFlowThreshold) {
|
||||
const parentKey = effectiveParent ?? rootNodeId;
|
||||
if (!smallConsumersByParent.has(parentKey)) {
|
||||
smallConsumersByParent.set(parentKey, []);
|
||||
}
|
||||
smallConsumersByParent.get(parentKey)!.push({
|
||||
statRate: device.stat_rate,
|
||||
name: device.name,
|
||||
value,
|
||||
effectiveParent,
|
||||
idx,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const node = {
|
||||
id: device.stat_rate,
|
||||
label: device.name || this._getEntityLabel(device.stat_rate),
|
||||
value,
|
||||
color: getGraphColorByIndex(idx, computedStyle),
|
||||
index: 4,
|
||||
parent: effectiveParent,
|
||||
entityId: device.stat_rate,
|
||||
};
|
||||
|
||||
if (node.parent) {
|
||||
parentLinks[node.id] = node.parent;
|
||||
links.push({ source: node.parent, target: node.id });
|
||||
} else {
|
||||
untrackedConsumption -= value;
|
||||
}
|
||||
deviceNodes.push(node);
|
||||
});
|
||||
|
||||
// Process small consumers
|
||||
smallConsumersByParent.forEach((consumers, parentKey) => {
|
||||
const totalValue = consumers.reduce((sum, c) => sum + c.value, 0);
|
||||
if (totalValue <= 0) return;
|
||||
|
||||
if (consumers.length === 1) {
|
||||
const consumer = consumers[0];
|
||||
const node = {
|
||||
id: consumer.statRate,
|
||||
label: consumer.name || this._getEntityLabel(consumer.statRate),
|
||||
value: consumer.value,
|
||||
color: getGraphColorByIndex(consumer.idx, computedStyle),
|
||||
index: 4,
|
||||
parent: consumer.effectiveParent,
|
||||
entityId: consumer.statRate,
|
||||
};
|
||||
if (node.parent) {
|
||||
parentLinks[node.id] = node.parent;
|
||||
links.push({ source: node.parent, target: node.id });
|
||||
} else {
|
||||
untrackedConsumption -= consumer.value;
|
||||
}
|
||||
deviceNodes.push(node);
|
||||
} else {
|
||||
const otherNodeId = `other_${parentKey}`;
|
||||
const otherNode: Node = {
|
||||
id: otherNodeId,
|
||||
label: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_devices_detail_graph.other"
|
||||
),
|
||||
value: Math.ceil(totalValue),
|
||||
color: computedStyle
|
||||
.getPropertyValue("--state-unavailable-color")
|
||||
.trim(),
|
||||
index: 4,
|
||||
};
|
||||
|
||||
if (parentKey !== rootNodeId) {
|
||||
parentLinks[otherNodeId] = parentKey;
|
||||
links.push({ source: parentKey, target: otherNodeId });
|
||||
} else {
|
||||
untrackedConsumption -= totalValue;
|
||||
}
|
||||
deviceNodes.push(otherNode);
|
||||
}
|
||||
});
|
||||
|
||||
const devicesWithoutParent = deviceNodes.filter(
|
||||
(node) => !parentLinks[node.id]
|
||||
);
|
||||
|
||||
const { group_by_area, group_by_floor, layout, title } = this._config;
|
||||
if (group_by_area || group_by_floor) {
|
||||
const { areas, floors } = this._groupByFloorAndArea(devicesWithoutParent);
|
||||
|
||||
Object.keys(floors)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(this.hass.floors[b]?.level ?? -Infinity) -
|
||||
(this.hass.floors[a]?.level ?? -Infinity)
|
||||
)
|
||||
.forEach((floorId) => {
|
||||
let floorNodeId = `floor_${floorId}`;
|
||||
if (floorId === "no_floor" || !group_by_floor) {
|
||||
floorNodeId = rootNodeId;
|
||||
} else {
|
||||
nodes.push({
|
||||
id: floorNodeId,
|
||||
label: this.hass.floors[floorId].name,
|
||||
value: floors[floorId].value,
|
||||
index: 2,
|
||||
color: primaryColor,
|
||||
});
|
||||
links.push({ source: rootNodeId, target: floorNodeId });
|
||||
}
|
||||
|
||||
floors[floorId].areas.forEach((areaId) => {
|
||||
let targetNodeId: string;
|
||||
|
||||
if (areaId === "no_area" || !group_by_area) {
|
||||
targetNodeId = floorNodeId;
|
||||
} else {
|
||||
const areaNodeId = `area_${areaId}`;
|
||||
nodes.push({
|
||||
id: areaNodeId,
|
||||
label: this.hass.areas[areaId]?.name || areaId,
|
||||
value: areas[areaId].value,
|
||||
index: 3,
|
||||
color: primaryColor,
|
||||
});
|
||||
links.push({
|
||||
source: floorNodeId,
|
||||
target: areaNodeId,
|
||||
value: areas[areaId].value,
|
||||
});
|
||||
targetNodeId = areaNodeId;
|
||||
}
|
||||
|
||||
areas[areaId].devices.forEach((device) => {
|
||||
links.push({
|
||||
source: targetNodeId,
|
||||
target: device.id,
|
||||
value: device.value,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
devicesWithoutParent.forEach((deviceNode) => {
|
||||
links.push({
|
||||
source: rootNodeId,
|
||||
target: deviceNode.id,
|
||||
value: deviceNode.value,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const deviceSections = this._getDeviceSections(parentLinks, deviceNodes);
|
||||
deviceSections.forEach((section, index) => {
|
||||
section.forEach((node: Node) => {
|
||||
nodes.push({ ...node, index: 4 + index });
|
||||
});
|
||||
});
|
||||
|
||||
// Untracked consumption (only show if > 1 L/min threshold)
|
||||
if (untrackedConsumption > 1) {
|
||||
nodes.push({
|
||||
id: "untracked",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_devices_detail_graph.untracked_consumption"
|
||||
),
|
||||
value: untrackedConsumption,
|
||||
color: computedStyle
|
||||
.getPropertyValue("--state-unavailable-color")
|
||||
.trim(),
|
||||
index: 3 + deviceSections.length,
|
||||
});
|
||||
links.push({
|
||||
source: rootNodeId,
|
||||
target: "untracked",
|
||||
value: untrackedConsumption,
|
||||
});
|
||||
}
|
||||
|
||||
const hasData = nodes.some((node) => node.value > 0);
|
||||
|
||||
const vertical =
|
||||
layout === "vertical" || (layout !== "horizontal" && this._isMobileSize);
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
.header=${title}
|
||||
class=${classMap({
|
||||
"is-grid": this.layout === "grid",
|
||||
"is-panel": this.layout === "panel",
|
||||
"is-vertical": vertical,
|
||||
})}
|
||||
>
|
||||
<div class="card-content">
|
||||
${hasData
|
||||
? html`<ha-sankey-chart
|
||||
.data=${{ nodes, links }}
|
||||
.vertical=${vertical}
|
||||
.valueFormatter=${this._valueFormatter}
|
||||
@node-click=${this._handleNodeClick}
|
||||
></ha-sankey-chart>`
|
||||
: html`${this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.no_data"
|
||||
)}`}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueFormatter = (value: number) =>
|
||||
`<div style="direction:ltr; display: inline;">
|
||||
${formatFlowRateShort(this.hass.locale, this.hass.config.unit_system.length, value)}
|
||||
</div>`;
|
||||
|
||||
private _handleNodeClick(ev: CustomEvent<{ node: Node }>) {
|
||||
const { node } = ev.detail;
|
||||
if (node.entityId) {
|
||||
fireEvent(this, "hass-more-info", { entityId: node.entityId });
|
||||
}
|
||||
}
|
||||
|
||||
private _getCurrentFlowRate(entityId: string): number {
|
||||
this._entities.add(entityId);
|
||||
return getFlowRateFromState(this.hass.states[entityId]) ?? 0;
|
||||
}
|
||||
|
||||
private _getEntityLabel(entityId: string): string {
|
||||
const stateObj = this.hass.states[entityId];
|
||||
if (!stateObj) return entityId;
|
||||
return stateObj.attributes.friendly_name || entityId;
|
||||
}
|
||||
|
||||
protected _groupByFloorAndArea(deviceNodes: Node[]) {
|
||||
const areas: Record<string, { value: number; devices: Node[] }> = {
|
||||
no_area: { value: 0, devices: [] },
|
||||
};
|
||||
const floors: Record<string, { value: number; areas: string[] }> = {
|
||||
no_floor: { value: 0, areas: ["no_area"] },
|
||||
};
|
||||
|
||||
deviceNodes.forEach((deviceNode) => {
|
||||
const entity = this.hass.states[deviceNode.id];
|
||||
const { area, floor } = entity
|
||||
? getEntityContext(
|
||||
entity,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
)
|
||||
: { area: null, floor: null };
|
||||
|
||||
if (area) {
|
||||
if (area.area_id in areas) {
|
||||
areas[area.area_id].value += deviceNode.value;
|
||||
areas[area.area_id].devices.push(deviceNode);
|
||||
} else {
|
||||
areas[area.area_id] = {
|
||||
value: deviceNode.value,
|
||||
devices: [deviceNode],
|
||||
};
|
||||
}
|
||||
if (floor) {
|
||||
if (floor.floor_id in floors) {
|
||||
floors[floor.floor_id].value += deviceNode.value;
|
||||
if (!floors[floor.floor_id].areas.includes(area.area_id)) {
|
||||
floors[floor.floor_id].areas.push(area.area_id);
|
||||
}
|
||||
} else {
|
||||
floors[floor.floor_id] = {
|
||||
value: deviceNode.value,
|
||||
areas: [area.area_id],
|
||||
};
|
||||
}
|
||||
} else {
|
||||
floors.no_floor.value += deviceNode.value;
|
||||
if (!floors.no_floor.areas.includes(area.area_id)) {
|
||||
floors.no_floor.areas.unshift(area.area_id);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
areas.no_area.value += deviceNode.value;
|
||||
areas.no_area.devices.push(deviceNode);
|
||||
}
|
||||
});
|
||||
|
||||
return { areas, floors };
|
||||
}
|
||||
|
||||
protected _getDeviceSections(
|
||||
parentLinks: Record<string, string>,
|
||||
deviceNodes: Node[]
|
||||
): Node[][] {
|
||||
const parentSection: Node[] = [];
|
||||
const childSection: Node[] = [];
|
||||
const parentIds = Object.values(parentLinks);
|
||||
const remainingLinks: typeof parentLinks = {};
|
||||
|
||||
deviceNodes.forEach((deviceNode) => {
|
||||
const isChild = deviceNode.id in parentLinks;
|
||||
const isParent = parentIds.includes(deviceNode.id);
|
||||
if (isParent && !isChild) {
|
||||
parentSection.push(deviceNode);
|
||||
} else {
|
||||
childSection.push(deviceNode);
|
||||
}
|
||||
});
|
||||
|
||||
Object.entries(parentLinks).forEach(([child, parent]) => {
|
||||
if (!parentSection.some((node) => node.id === parent)) {
|
||||
remainingLinks[child] = parent;
|
||||
}
|
||||
});
|
||||
|
||||
if (parentSection.length > 0) {
|
||||
return [
|
||||
parentSection,
|
||||
...this._getDeviceSections(remainingLinks, childSection),
|
||||
];
|
||||
}
|
||||
|
||||
return [deviceNodes];
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-card {
|
||||
height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
--chart-max-height: none;
|
||||
}
|
||||
ha-card.is-vertical {
|
||||
height: 500px;
|
||||
}
|
||||
ha-card.is-grid,
|
||||
ha-card.is-panel {
|
||||
height: 100%;
|
||||
}
|
||||
.card-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-water-flow-sankey-card": HuiWaterFlowSankeyCard;
|
||||
}
|
||||
}
|
||||
@@ -67,8 +67,6 @@ const LAZY_LOAD_TYPES = {
|
||||
import("../cards/energy/hui-energy-usage-graph-card"),
|
||||
"energy-sankey": () => import("../cards/energy/hui-energy-sankey-card"),
|
||||
"water-sankey": () => import("../cards/water/hui-water-sankey-card"),
|
||||
"water-flow-sankey": () =>
|
||||
import("../cards/water/hui-water-flow-sankey-card"),
|
||||
"power-sources-graph": () =>
|
||||
import("../cards/energy/hui-power-sources-graph-card"),
|
||||
"power-total": () => import("../cards/energy/hui-power-total-card"),
|
||||
|
||||
@@ -142,6 +142,7 @@ export class HuiCreateDialogBadge
|
||||
`
|
||||
: html`
|
||||
<hui-entity-picker-table
|
||||
no-label-float
|
||||
.hass=${this.hass}
|
||||
.narrow=${true}
|
||||
@selected-changed=${this._handleSelectedChanged}
|
||||
|
||||
@@ -165,6 +165,7 @@ export class HuiCreateDialogCard
|
||||
`
|
||||
: html`
|
||||
<hui-entity-picker-table
|
||||
no-label-float
|
||||
.hass=${this.hass}
|
||||
narrow
|
||||
@selected-changed=${this._handleSelectedChanged}
|
||||
|
||||
@@ -43,6 +43,9 @@ export class HuiEntityPickerTable extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-label-float" })
|
||||
public noLabelFloat? = false;
|
||||
|
||||
@property({ type: Array }) public entities?: string[];
|
||||
|
||||
protected firstUpdated(_changedProperties: PropertyValues): void {
|
||||
@@ -112,6 +115,7 @@ export class HuiEntityPickerTable extends LitElement {
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.panel.lovelace.unused_entities.search"
|
||||
)}
|
||||
.noLabelFloat=${this.noLabelFloat}
|
||||
.noDataText=${this.hass.localize(
|
||||
"ui.panel.lovelace.unused_entities.no_data"
|
||||
)}
|
||||
|
||||
@@ -3,16 +3,15 @@ import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
array,
|
||||
assert,
|
||||
assign,
|
||||
boolean,
|
||||
defaulted,
|
||||
enums,
|
||||
literal,
|
||||
object,
|
||||
optional,
|
||||
string,
|
||||
union,
|
||||
} from "superstruct";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
@@ -20,57 +19,46 @@ import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import type { ClockCardConfig } from "../../cards/types";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
import { TimeFormat } from "../../../../data/translation";
|
||||
import {
|
||||
CLOCK_CARD_DATE_PARTS,
|
||||
getClockCardDateConfig,
|
||||
} from "../../cards/clock/clock-date-format";
|
||||
|
||||
const cardConfigStruct = assign(
|
||||
baseLovelaceCardConfig,
|
||||
object({
|
||||
title: optional(string()),
|
||||
clock_style: optional(union([literal("digital"), literal("analog")])),
|
||||
clock_size: optional(
|
||||
union([literal("small"), literal("medium"), literal("large")])
|
||||
),
|
||||
clock_style: optional(enums(["digital", "analog"])),
|
||||
clock_size: optional(enums(["small", "medium", "large"])),
|
||||
time_format: optional(enums(Object.values(TimeFormat))),
|
||||
time_zone: optional(enums(Object.keys(timezones))),
|
||||
show_seconds: optional(boolean()),
|
||||
no_background: optional(boolean()),
|
||||
date_format: optional(defaulted(array(enums(CLOCK_CARD_DATE_PARTS)), [])),
|
||||
// Analog clock options
|
||||
border: optional(defaulted(boolean(), false)),
|
||||
ticks: optional(
|
||||
defaulted(
|
||||
union([
|
||||
literal("none"),
|
||||
literal("quarter"),
|
||||
literal("hour"),
|
||||
literal("minute"),
|
||||
]),
|
||||
literal("hour")
|
||||
)
|
||||
defaulted(enums(["none", "quarter", "hour", "minute"]), "hour")
|
||||
),
|
||||
seconds_motion: optional(
|
||||
defaulted(
|
||||
union([literal("continuous"), literal("tick")]),
|
||||
literal("continuous")
|
||||
)
|
||||
defaulted(enums(["continuous", "tick"]), "continuous")
|
||||
),
|
||||
face_style: optional(
|
||||
defaulted(
|
||||
union([
|
||||
literal("markers"),
|
||||
literal("numbers_upright"),
|
||||
literal("roman"),
|
||||
]),
|
||||
literal("markers")
|
||||
)
|
||||
defaulted(enums(["markers", "numbers_upright", "roman"]), "markers")
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
type ClockCardFormData = Omit<ClockCardConfig, "time_format"> & {
|
||||
time_format?: ClockCardConfig["time_format"] | "auto";
|
||||
};
|
||||
|
||||
@customElement("hui-clock-card-editor")
|
||||
export class HuiClockCardEditor
|
||||
extends LitElement
|
||||
@@ -93,7 +81,7 @@ export class HuiClockCardEditor
|
||||
name: "clock_style",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
mode: "box",
|
||||
options: ["digital", "analog"].map((value) => ({
|
||||
value,
|
||||
label: localize(
|
||||
@@ -119,6 +107,13 @@ export class HuiClockCardEditor
|
||||
},
|
||||
{ name: "show_seconds", selector: { boolean: {} } },
|
||||
{ name: "no_background", selector: { boolean: {} } },
|
||||
{
|
||||
name: "date_format",
|
||||
required: false,
|
||||
selector: {
|
||||
ui_clock_date_format: {},
|
||||
},
|
||||
},
|
||||
...(clockStyle === "digital"
|
||||
? ([
|
||||
{
|
||||
@@ -241,18 +236,28 @@ export class HuiClockCardEditor
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
private _data = memoizeOne((config) => ({
|
||||
clock_style: "digital",
|
||||
clock_size: "small",
|
||||
time_format: "auto",
|
||||
show_seconds: false,
|
||||
no_background: false,
|
||||
// Analog clock options
|
||||
border: false,
|
||||
ticks: "hour",
|
||||
face_style: "markers",
|
||||
...config,
|
||||
}));
|
||||
private _data = memoizeOne((config: ClockCardConfig): ClockCardFormData => {
|
||||
const dateConfig = getClockCardDateConfig(config);
|
||||
|
||||
const data: ClockCardFormData = {
|
||||
...config,
|
||||
clock_style: config.clock_style ?? "digital",
|
||||
clock_size: config.clock_size ?? "small",
|
||||
time_format: config.time_format ?? "auto",
|
||||
show_seconds: config.show_seconds ?? false,
|
||||
no_background: config.no_background ?? false,
|
||||
// Analog clock options
|
||||
border: config.border ?? false,
|
||||
ticks: config.ticks ?? "hour",
|
||||
face_style: config.face_style ?? "markers",
|
||||
};
|
||||
|
||||
if (config.date_format === undefined) {
|
||||
data.date_format = dateConfig.parts;
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
|
||||
public setConfig(config: ClockCardConfig): void {
|
||||
assert(config, cardConfigStruct);
|
||||
@@ -270,8 +275,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}
|
||||
@@ -281,35 +287,40 @@ export class HuiClockCardEditor
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
if (ev.detail.value.time_format === "auto") {
|
||||
delete ev.detail.value.time_format;
|
||||
private _valueChanged(ev: ValueChangedEvent<ClockCardFormData>): void {
|
||||
const config = ev.detail.value;
|
||||
|
||||
if (!config.date_format || config.date_format.length === 0) {
|
||||
delete config.date_format;
|
||||
}
|
||||
|
||||
if (ev.detail.value.clock_style === "analog") {
|
||||
ev.detail.value.border = ev.detail.value.border ?? false;
|
||||
ev.detail.value.ticks = ev.detail.value.ticks ?? "hour";
|
||||
ev.detail.value.face_style = ev.detail.value.face_style ?? "markers";
|
||||
if (ev.detail.value.show_seconds) {
|
||||
ev.detail.value.seconds_motion =
|
||||
ev.detail.value.seconds_motion ?? "continuous";
|
||||
if (config.time_format === "auto") {
|
||||
delete config.time_format;
|
||||
}
|
||||
|
||||
if (config.clock_style === "analog") {
|
||||
config.border = config.border ?? false;
|
||||
config.ticks = config.ticks ?? "hour";
|
||||
config.face_style = config.face_style ?? "markers";
|
||||
if (config.show_seconds) {
|
||||
config.seconds_motion = config.seconds_motion ?? "continuous";
|
||||
} else {
|
||||
delete ev.detail.value.seconds_motion;
|
||||
delete config.seconds_motion;
|
||||
}
|
||||
} else {
|
||||
delete ev.detail.value.border;
|
||||
delete ev.detail.value.ticks;
|
||||
delete ev.detail.value.face_style;
|
||||
delete ev.detail.value.seconds_motion;
|
||||
delete config.border;
|
||||
delete config.ticks;
|
||||
delete config.face_style;
|
||||
delete config.seconds_motion;
|
||||
}
|
||||
|
||||
if (ev.detail.value.ticks !== "none") {
|
||||
ev.detail.value.face_style = ev.detail.value.face_style ?? "markers";
|
||||
if (config.ticks !== "none") {
|
||||
config.face_style = config.face_style ?? "markers";
|
||||
} else {
|
||||
delete ev.detail.value.face_style;
|
||||
delete config.face_style;
|
||||
}
|
||||
|
||||
fireEvent(this, "config-changed", { config: ev.detail.value });
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
@@ -344,6 +355,10 @@ export class HuiClockCardEditor
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.no_background`
|
||||
);
|
||||
case "date_format":
|
||||
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`
|
||||
@@ -369,6 +384,10 @@ export class HuiClockCardEditor
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "date_format":
|
||||
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`
|
||||
|
||||
@@ -32,7 +32,7 @@ export const securityEntityFilters: EntityFilter[] = [
|
||||
},
|
||||
{
|
||||
domain: "cover",
|
||||
device_class: ["door", "garage", "gate", "window"],
|
||||
device_class: ["door", "garage", "gate"],
|
||||
entity_category: "none",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ export const waColorStyles = css`
|
||||
html {
|
||||
--wa-color-brand-fill-loud: var(--ha-color-fill-primary-loud-resting);
|
||||
--wa-color-brand-fill-normal: var(--ha-color-fill-primary-normal-resting);
|
||||
--wa-color-brand-fill-quiet: var(--ha-color-fill-primary-quiet-hover);
|
||||
--wa-color-brand-fill-quiet: var(--ha-color-fill-primary-quiet-resting);
|
||||
--wa-color-brand-border-loud: var(--ha-color-border-loud);
|
||||
--wa-color-brand-border-normal: var(--ha-color-primary-50);
|
||||
--wa-color-brand-border-quiet: var(--ha-color-border-quiet);
|
||||
@@ -14,7 +14,7 @@ export const waColorStyles = css`
|
||||
|
||||
--wa-color-neutral-fill-loud: var(--ha-color-fill-neutral-loud-resting);
|
||||
--wa-color-neutral-fill-normal: var(--ha-color-fill-neutral-normal-resting);
|
||||
--wa-color-neutral-fill-quiet: var(--ha-color-fill-neutral-quiet-hover);
|
||||
--wa-color-neutral-fill-quiet: var(--ha-color-fill-neutral-quiet-resting);
|
||||
--wa-color-neutral-border-loud: var(--ha-color-border-neutral-loud);
|
||||
--wa-color-neutral-border-normal: var(--ha-color-border-neutral-normal);
|
||||
--wa-color-neutral-border-quiet: var(--ha-color-border-neutral-quiet);
|
||||
@@ -24,7 +24,7 @@ export const waColorStyles = css`
|
||||
|
||||
--wa-color-success-fill-loud: var(--ha-color-fill-success-loud-resting);
|
||||
--wa-color-success-fill-normal: var(--ha-color-fill-success-normal-resting);
|
||||
--wa-color-success-fill-quiet: var(--ha-color-fill-success-quiet-hover);
|
||||
--wa-color-success-fill-quiet: var(--ha-color-fill-success-quiet-resting);
|
||||
--wa-color-success-border-loud: var(--ha-color-border-success-loud);
|
||||
--wa-color-success-border-normal: var(--ha-color-border-success-normal);
|
||||
--wa-color-success-border-quiet: var(--ha-color-border-success-quiet);
|
||||
@@ -34,7 +34,7 @@ export const waColorStyles = css`
|
||||
|
||||
--wa-color-warning-fill-loud: var(--ha-color-fill-warning-loud-resting);
|
||||
--wa-color-warning-fill-normal: var(--ha-color-fill-warning-normal-resting);
|
||||
--wa-color-warning-fill-quiet: var(--ha-color-fill-warning-quiet-hover);
|
||||
--wa-color-warning-fill-quiet: var(--ha-color-fill-warning-quiet-resting);
|
||||
--wa-color-warning-border-loud: var(--ha-color-border-warning-loud);
|
||||
--wa-color-warning-border-normal: var(--ha-color-border-warning-normal);
|
||||
--wa-color-warning-border-quiet: var(--ha-color-border-warning-quiet);
|
||||
@@ -44,7 +44,7 @@ export const waColorStyles = css`
|
||||
|
||||
--wa-color-danger-fill-loud: var(--ha-color-fill-danger-loud-resting);
|
||||
--wa-color-danger-fill-normal: var(--ha-color-fill-danger-normal-resting);
|
||||
--wa-color-danger-fill-quiet: var(--ha-color-fill-danger-quiet-hover);
|
||||
--wa-color-danger-fill-quiet: var(--ha-color-fill-danger-quiet-resting);
|
||||
--wa-color-danger-border-loud: var(--ha-color-border-danger-loud);
|
||||
--wa-color-danger-border-normal: var(--ha-color-border-danger-normal);
|
||||
--wa-color-danger-border-quiet: var(--ha-color-border-danger-quiet);
|
||||
|
||||
@@ -30,10 +30,6 @@ import { subscribeEntityRegistryDisplay } from "../data/ws-entity_registry_displ
|
||||
import { subscribeFloorRegistry } from "../data/ws-floor_registry";
|
||||
import { subscribePanels } from "../data/ws-panels";
|
||||
import { translationMetadata } from "../resources/translations-metadata";
|
||||
import {
|
||||
clearBrandsTokenRefresh,
|
||||
fetchAndScheduleBrandsAccessToken,
|
||||
} from "../util/brands-url";
|
||||
import type { Constructor, HomeAssistant, ServiceCallResponse } from "../types";
|
||||
import { getLocalLanguage } from "../util/common-translation";
|
||||
import { fetchWithAuth } from "../util/fetch-with-auth";
|
||||
@@ -323,10 +319,6 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
this._updateHass({ systemData: {} });
|
||||
});
|
||||
clearInterval(this.__backendPingInterval);
|
||||
|
||||
// Fetch the brands access token on initial connect and schedule refresh
|
||||
fetchAndScheduleBrandsAccessToken(this.hass!);
|
||||
|
||||
this.__backendPingInterval = setInterval(() => {
|
||||
if (this.hass?.connected) {
|
||||
// If the backend is busy, or the connection is latent,
|
||||
@@ -351,9 +343,6 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
this._updateHass({ connected: true });
|
||||
broadcastConnectionStatus("connected");
|
||||
|
||||
// Refresh the brands access token on reconnect and restart refresh schedule
|
||||
fetchAndScheduleBrandsAccessToken(this.hass!);
|
||||
|
||||
// on reconnect always fetch config as we might miss an update while we were disconnected
|
||||
// @ts-ignore
|
||||
this.hass!.callWS({ type: "get_config" }).then((config: HassConfig) => {
|
||||
@@ -371,6 +360,5 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
this._updateHass({ connected: false });
|
||||
broadcastConnectionStatus("disconnected");
|
||||
clearInterval(this.__backendPingInterval);
|
||||
clearBrandsTokenRefresh();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1524,8 +1524,6 @@
|
||||
"settings": "Settings",
|
||||
"edit": "Edit entity",
|
||||
"details": "Details",
|
||||
"translated": "Translated",
|
||||
"raw": "Raw",
|
||||
"back_to_info": "Back to info",
|
||||
"info": "Information",
|
||||
"related": "Related",
|
||||
@@ -9092,6 +9090,33 @@
|
||||
"large": "Large"
|
||||
},
|
||||
"show_seconds": "Display seconds",
|
||||
"date": {
|
||||
"label": "Date",
|
||||
"description": "Select and order the date parts. Add a separator to control punctuation.",
|
||||
"sections": {
|
||||
"weekday": "Weekday",
|
||||
"day": "Day",
|
||||
"month": "Month",
|
||||
"year": "Year",
|
||||
"separator": "Separator"
|
||||
},
|
||||
"parts": {
|
||||
"weekday-short": "Short",
|
||||
"weekday-long": "Long",
|
||||
"day-numeric": "Numeric",
|
||||
"day-2-digit": "2-digit",
|
||||
"month-short": "Short",
|
||||
"month-long": "Long",
|
||||
"month-numeric": "Numeric",
|
||||
"month-2-digit": "2-digit",
|
||||
"year-2-digit": "2-digit",
|
||||
"year-numeric": "Full",
|
||||
"separator-dash": "Dash",
|
||||
"separator-slash": "Slash",
|
||||
"separator-dot": "Dot",
|
||||
"separator-new-line": "New line"
|
||||
}
|
||||
},
|
||||
"time_format": "[%key:ui::panel::profile::time_format::dropdown_label%]",
|
||||
"time_formats": {
|
||||
"auto": "Use user settings",
|
||||
@@ -10370,7 +10395,6 @@
|
||||
"water_sankey_title": "Water flow",
|
||||
"energy_top_consumers_title": "Top consumers",
|
||||
"power_sankey_title": "Current power flow",
|
||||
"water_flow_sankey_title": "Current water flow",
|
||||
"power_sources_graph_title": "Power sources"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -142,7 +142,6 @@ export interface PanelInfo<T = Record<string, any> | null> {
|
||||
config_panel_domain?: string;
|
||||
default_visible?: boolean;
|
||||
require_admin?: boolean;
|
||||
show_in_sidebar?: boolean;
|
||||
}
|
||||
|
||||
export type Panels = Record<string, PanelInfo>;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export interface BrandsOptions {
|
||||
domain: string;
|
||||
type: "icon" | "logo" | "icon@2x" | "logo@2x";
|
||||
darkOptimized?: boolean;
|
||||
brand?: boolean;
|
||||
}
|
||||
|
||||
export interface HardwareBrandsOptions {
|
||||
@@ -13,94 +12,17 @@ export interface HardwareBrandsOptions {
|
||||
darkOptimized?: boolean;
|
||||
}
|
||||
|
||||
let _brandsAccessToken: string | undefined;
|
||||
let _brandsRefreshInterval: ReturnType<typeof setInterval> | undefined;
|
||||
|
||||
// Token refreshes every 30 minutes and is valid for 1 hour.
|
||||
// Re-fetch every 30 minutes to always have a valid token.
|
||||
const TOKEN_REFRESH_MS = 30 * 60 * 1000;
|
||||
|
||||
export const fetchAndScheduleBrandsAccessToken = (
|
||||
hass: HomeAssistant
|
||||
): Promise<void> =>
|
||||
fetchBrandsAccessToken(hass).then(
|
||||
() => scheduleBrandsTokenRefresh(hass),
|
||||
() => {
|
||||
// Ignore failures; older backends may not support this command
|
||||
}
|
||||
);
|
||||
|
||||
export const fetchBrandsAccessToken = async (
|
||||
hass: HomeAssistant
|
||||
): Promise<void> => {
|
||||
const result = await hass.callWS<{ token: string }>({
|
||||
type: "brands/access_token",
|
||||
});
|
||||
_brandsAccessToken = result.token;
|
||||
};
|
||||
|
||||
export const scheduleBrandsTokenRefresh = (hass: HomeAssistant): void => {
|
||||
clearBrandsTokenRefresh();
|
||||
_brandsRefreshInterval = setInterval(() => {
|
||||
fetchBrandsAccessToken(hass).catch(() => {
|
||||
// Ignore failures; older backends may not support this command
|
||||
});
|
||||
}, TOKEN_REFRESH_MS);
|
||||
};
|
||||
|
||||
export const clearBrandsTokenRefresh = (): void => {
|
||||
if (_brandsRefreshInterval) {
|
||||
clearInterval(_brandsRefreshInterval);
|
||||
_brandsRefreshInterval = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const brandsUrl = (options: BrandsOptions): string => {
|
||||
const base = `/api/brands/integration/${options.domain}/${
|
||||
export const brandsUrl = (options: BrandsOptions): string =>
|
||||
`https://brands.home-assistant.io/${options.brand ? "brands/" : ""}_/${options.domain}/${
|
||||
options.darkOptimized ? "dark_" : ""
|
||||
}${options.type}.png`;
|
||||
if (_brandsAccessToken) {
|
||||
return `${base}?token=${_brandsAccessToken}`;
|
||||
}
|
||||
return base;
|
||||
};
|
||||
|
||||
export const hardwareBrandsUrl = (options: HardwareBrandsOptions): string => {
|
||||
const base = `/api/brands/hardware/${options.category}/${
|
||||
export const hardwareBrandsUrl = (options: HardwareBrandsOptions): string =>
|
||||
`https://brands.home-assistant.io/hardware/${options.category}/${
|
||||
options.darkOptimized ? "dark_" : ""
|
||||
}${options.manufacturer}${options.model ? `_${options.model}` : ""}.png`;
|
||||
if (_brandsAccessToken) {
|
||||
return `${base}?token=${_brandsAccessToken}`;
|
||||
}
|
||||
return base;
|
||||
};
|
||||
|
||||
export const addBrandsAuth = (url: string): string => {
|
||||
if (!_brandsAccessToken || !url.startsWith("/api/brands/")) {
|
||||
return url;
|
||||
}
|
||||
const fullUrl = new URL(url, location.origin);
|
||||
fullUrl.searchParams.set("token", _brandsAccessToken);
|
||||
return `${fullUrl.pathname}${fullUrl.search}`;
|
||||
};
|
||||
|
||||
export const extractDomainFromBrandUrl = (url: string): string => {
|
||||
// Handle both new local API paths (/api/brands/integration/{domain}/...)
|
||||
// and legacy CDN URLs (https://brands.home-assistant.io/_/{domain}/...)
|
||||
if (url.startsWith("/api/brands/")) {
|
||||
// /api/brands/integration/{domain}/... -> ["" ,"api", "brands", "integration", "{domain}", ...]
|
||||
return url.split("/")[4];
|
||||
}
|
||||
// https://brands.home-assistant.io/_/{domain}/... -> ["", "_", "{domain}", ...]
|
||||
const parsed = new URL(url);
|
||||
const segments = parsed.pathname.split("/").filter((s) => s.length > 0);
|
||||
const underscoreIdx = segments.indexOf("_");
|
||||
if (underscoreIdx !== -1 && underscoreIdx + 1 < segments.length) {
|
||||
return segments[underscoreIdx + 1];
|
||||
}
|
||||
return segments[1] ?? "";
|
||||
};
|
||||
export const extractDomainFromBrandUrl = (url: string) => url.split("/")[4];
|
||||
|
||||
export const isBrandUrl = (thumbnail: string | ""): boolean =>
|
||||
thumbnail.startsWith("/api/brands/") ||
|
||||
thumbnail.startsWith("https://brands.home-assistant.io/");
|
||||
|
||||
261
test/panels/lovelace/cards/clock-date-format.test.ts
Normal file
261
test/panels/lovelace/cards/clock-date-format.test.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
import { assert, describe, it } from "vitest";
|
||||
import type { ClockCardDatePart } from "../../../../src/panels/lovelace/cards/types";
|
||||
import {
|
||||
formatClockCardDate,
|
||||
getClockCardDateConfig,
|
||||
getClockCardDateTimeFormatOptions,
|
||||
hasClockCardDate,
|
||||
} from "../../../../src/panels/lovelace/cards/clock/clock-date-format";
|
||||
|
||||
describe("clock-date-format", () => {
|
||||
const date = new Date("2024-11-08T10:20:30.000Z");
|
||||
|
||||
it("returns an empty config when date format is missing", () => {
|
||||
assert.deepEqual(getClockCardDateConfig(), { parts: [] });
|
||||
});
|
||||
|
||||
it("preserves literal token order", () => {
|
||||
const config = getClockCardDateConfig({
|
||||
date_format: [
|
||||
"day-numeric",
|
||||
"separator-dot",
|
||||
"month-short",
|
||||
"month-long",
|
||||
"separator-slash",
|
||||
"year-2-digit",
|
||||
"year-numeric",
|
||||
],
|
||||
});
|
||||
|
||||
assert.deepEqual(config.parts, [
|
||||
"day-numeric",
|
||||
"separator-dot",
|
||||
"month-short",
|
||||
"month-long",
|
||||
"separator-slash",
|
||||
"year-2-digit",
|
||||
"year-numeric",
|
||||
]);
|
||||
});
|
||||
|
||||
it("filters invalid date tokens", () => {
|
||||
const config = getClockCardDateConfig({
|
||||
date_format: [
|
||||
"month-short",
|
||||
"invalid",
|
||||
"year-2-digit",
|
||||
] as unknown as ClockCardDatePart[],
|
||||
});
|
||||
|
||||
assert.deepEqual(config.parts, ["month-short", "year-2-digit"]);
|
||||
});
|
||||
|
||||
it("builds Intl options from selected date tokens", () => {
|
||||
const options = getClockCardDateTimeFormatOptions({
|
||||
parts: [
|
||||
"weekday-short",
|
||||
"separator-slash",
|
||||
"day-2-digit",
|
||||
"month-long",
|
||||
"month-numeric",
|
||||
"year-2-digit",
|
||||
],
|
||||
});
|
||||
|
||||
assert.deepEqual(options, {
|
||||
weekday: "short",
|
||||
day: "2-digit",
|
||||
month: "numeric",
|
||||
year: "2-digit",
|
||||
});
|
||||
});
|
||||
|
||||
it("reports whether any date part is configured", () => {
|
||||
assert.equal(hasClockCardDate(), false);
|
||||
assert.equal(hasClockCardDate({ date_format: [] }), false);
|
||||
assert.equal(hasClockCardDate({ date_format: ["separator-dot"] }), true);
|
||||
assert.equal(
|
||||
hasClockCardDate({ date_format: ["separator-new-line"] }),
|
||||
true
|
||||
);
|
||||
assert.equal(hasClockCardDate({ date_format: ["weekday-short"] }), true);
|
||||
});
|
||||
|
||||
it("formats output in configured part order with literal separators", () => {
|
||||
const result = formatClockCardDate(
|
||||
date,
|
||||
{
|
||||
parts: [
|
||||
"month-numeric",
|
||||
"separator-slash",
|
||||
"day-2-digit",
|
||||
"separator-dash",
|
||||
"year-2-digit",
|
||||
],
|
||||
},
|
||||
"en",
|
||||
"UTC"
|
||||
);
|
||||
|
||||
assert.equal(result, "11/08-24");
|
||||
});
|
||||
|
||||
it("uses separator only for the next gap", () => {
|
||||
const result = formatClockCardDate(
|
||||
date,
|
||||
{
|
||||
parts: [
|
||||
"day-numeric",
|
||||
"separator-dot",
|
||||
"month-numeric",
|
||||
"year-numeric",
|
||||
],
|
||||
},
|
||||
"en",
|
||||
"UTC"
|
||||
);
|
||||
|
||||
assert.equal(result, "8.11 2024");
|
||||
});
|
||||
|
||||
it("supports using the same separator style multiple times", () => {
|
||||
const result = formatClockCardDate(
|
||||
date,
|
||||
{
|
||||
parts: [
|
||||
"month-numeric",
|
||||
"separator-slash",
|
||||
"day-2-digit",
|
||||
"separator-slash",
|
||||
"year-2-digit",
|
||||
],
|
||||
},
|
||||
"en",
|
||||
"UTC"
|
||||
);
|
||||
|
||||
assert.equal(result, "11/08/24");
|
||||
});
|
||||
|
||||
it("renders separators even when no value follows", () => {
|
||||
const result = formatClockCardDate(
|
||||
date,
|
||||
{
|
||||
parts: ["day-numeric", "separator-dash", "separator-dot"],
|
||||
},
|
||||
"en",
|
||||
"UTC"
|
||||
);
|
||||
|
||||
assert.equal(result, "8-.");
|
||||
});
|
||||
|
||||
it("renders separators even when no value precedes", () => {
|
||||
const result = formatClockCardDate(
|
||||
date,
|
||||
{
|
||||
parts: ["separator-slash", "separator-dot", "day-numeric"],
|
||||
},
|
||||
"en",
|
||||
"UTC"
|
||||
);
|
||||
|
||||
assert.equal(result, "/.8");
|
||||
});
|
||||
|
||||
it("renders all consecutive separators between values", () => {
|
||||
const result = formatClockCardDate(
|
||||
date,
|
||||
{
|
||||
parts: [
|
||||
"day-numeric",
|
||||
"separator-dash",
|
||||
"separator-slash",
|
||||
"separator-dot",
|
||||
"month-numeric",
|
||||
],
|
||||
},
|
||||
"en",
|
||||
"UTC"
|
||||
);
|
||||
|
||||
assert.equal(result, "8-/.11");
|
||||
});
|
||||
|
||||
it("renders repeated separators without deduplication", () => {
|
||||
const result = formatClockCardDate(
|
||||
date,
|
||||
{
|
||||
parts: [
|
||||
"day-numeric",
|
||||
"separator-dash",
|
||||
"separator-dash",
|
||||
"separator-dash",
|
||||
"month-numeric",
|
||||
],
|
||||
},
|
||||
"en",
|
||||
"UTC"
|
||||
);
|
||||
|
||||
assert.equal(result, "8---11");
|
||||
});
|
||||
|
||||
it("renders separator-only configurations", () => {
|
||||
const result = formatClockCardDate(
|
||||
date,
|
||||
{
|
||||
parts: ["separator-dash", "separator-slash", "separator-dot"],
|
||||
},
|
||||
"en",
|
||||
"UTC"
|
||||
);
|
||||
|
||||
assert.equal(result, "-/.");
|
||||
});
|
||||
|
||||
it("supports inserting a new line between date values", () => {
|
||||
const result = formatClockCardDate(
|
||||
date,
|
||||
{
|
||||
parts: [
|
||||
"month-numeric",
|
||||
"separator-new-line",
|
||||
"day-2-digit",
|
||||
"year-numeric",
|
||||
],
|
||||
},
|
||||
"en",
|
||||
"UTC"
|
||||
);
|
||||
|
||||
assert.equal(result, "11\n08 2024");
|
||||
});
|
||||
|
||||
it("allows multiple variants for the same date part", () => {
|
||||
const result = formatClockCardDate(
|
||||
date,
|
||||
{
|
||||
parts: ["month-short", "month-long", "year-numeric"],
|
||||
},
|
||||
"en",
|
||||
"UTC"
|
||||
);
|
||||
|
||||
assert.equal(result, "Nov November 2024");
|
||||
});
|
||||
|
||||
it("filters invalid tokens when formatting", () => {
|
||||
const config = getClockCardDateConfig({
|
||||
date_format: [
|
||||
"month-numeric",
|
||||
"invalid",
|
||||
"year-numeric",
|
||||
] as unknown as ClockCardDatePart[],
|
||||
});
|
||||
|
||||
const result = formatClockCardDate(date, config, "en", "UTC");
|
||||
|
||||
assert.equal(result, "11 2024");
|
||||
});
|
||||
});
|
||||
@@ -1,144 +1,27 @@
|
||||
import { assert, describe, it, vi, afterEach } from "vitest";
|
||||
import type { HomeAssistant } from "../../src/types";
|
||||
import {
|
||||
addBrandsAuth,
|
||||
brandsUrl,
|
||||
clearBrandsTokenRefresh,
|
||||
fetchBrandsAccessToken,
|
||||
scheduleBrandsTokenRefresh,
|
||||
} from "../../src/util/brands-url";
|
||||
import { assert, describe, it } from "vitest";
|
||||
import { brandsUrl } from "../../src/util/brands-url";
|
||||
|
||||
describe("Generate brands Url", () => {
|
||||
it("Generate logo brands url for cloud component", () => {
|
||||
assert.strictEqual(
|
||||
// @ts-ignore
|
||||
brandsUrl({ domain: "cloud", type: "logo" }),
|
||||
"/api/brands/integration/cloud/logo.png"
|
||||
"https://brands.home-assistant.io/_/cloud/logo.png"
|
||||
);
|
||||
});
|
||||
it("Generate icon brands url for cloud component", () => {
|
||||
assert.strictEqual(
|
||||
// @ts-ignore
|
||||
brandsUrl({ domain: "cloud", type: "icon" }),
|
||||
"/api/brands/integration/cloud/icon.png"
|
||||
"https://brands.home-assistant.io/_/cloud/icon.png"
|
||||
);
|
||||
});
|
||||
|
||||
it("Generate dark theme optimized logo brands url for cloud component", () => {
|
||||
assert.strictEqual(
|
||||
// @ts-ignore
|
||||
brandsUrl({ domain: "cloud", type: "logo", darkOptimized: true }),
|
||||
"/api/brands/integration/cloud/dark_logo.png"
|
||||
"https://brands.home-assistant.io/_/cloud/dark_logo.png"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("addBrandsAuth", () => {
|
||||
it("Returns non-brands URLs unchanged", () => {
|
||||
assert.strictEqual(
|
||||
addBrandsAuth("/api/camera_proxy/camera.foo?token=abc"),
|
||||
"/api/camera_proxy/camera.foo?token=abc"
|
||||
);
|
||||
});
|
||||
|
||||
it("Returns brands URL unchanged when no token is available", () => {
|
||||
assert.strictEqual(
|
||||
addBrandsAuth("/api/brands/integration/demo/icon.png"),
|
||||
"/api/brands/integration/demo/icon.png"
|
||||
);
|
||||
});
|
||||
|
||||
it("Appends token to brands URL when token is available", async () => {
|
||||
const mockHass = {
|
||||
callWS: async () => ({ token: "test-token-123" }),
|
||||
} as unknown as HomeAssistant;
|
||||
await fetchBrandsAccessToken(mockHass);
|
||||
|
||||
assert.strictEqual(
|
||||
addBrandsAuth("/api/brands/integration/demo/icon.png"),
|
||||
"/api/brands/integration/demo/icon.png?token=test-token-123"
|
||||
);
|
||||
});
|
||||
|
||||
it("Replaces existing token param instead of duplicating", async () => {
|
||||
const mockHass = {
|
||||
callWS: async () => ({ token: "new-token" }),
|
||||
} as unknown as HomeAssistant;
|
||||
await fetchBrandsAccessToken(mockHass);
|
||||
|
||||
assert.strictEqual(
|
||||
addBrandsAuth("/api/brands/integration/demo/icon.png?token=old-token"),
|
||||
"/api/brands/integration/demo/icon.png?token=new-token"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("scheduleBrandsTokenRefresh", () => {
|
||||
afterEach(() => {
|
||||
clearBrandsTokenRefresh();
|
||||
vi.restoreAllMocks();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("Refreshes the token after 30 minutes", async () => {
|
||||
vi.useFakeTimers();
|
||||
let callCount = 0;
|
||||
const mockHass = {
|
||||
callWS: async () => {
|
||||
callCount++;
|
||||
return { token: `token-${callCount}` };
|
||||
},
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
await fetchBrandsAccessToken(mockHass);
|
||||
assert.strictEqual(callCount, 1);
|
||||
assert.strictEqual(
|
||||
brandsUrl({ domain: "test", type: "icon" }),
|
||||
"/api/brands/integration/test/icon.png?token=token-1"
|
||||
);
|
||||
|
||||
scheduleBrandsTokenRefresh(mockHass);
|
||||
|
||||
// Advance 30 minutes
|
||||
await vi.advanceTimersByTimeAsync(30 * 60 * 1000);
|
||||
assert.strictEqual(callCount, 2);
|
||||
assert.strictEqual(
|
||||
brandsUrl({ domain: "test", type: "icon" }),
|
||||
"/api/brands/integration/test/icon.png?token=token-2"
|
||||
);
|
||||
});
|
||||
|
||||
it("Does not refresh before 30 minutes", async () => {
|
||||
vi.useFakeTimers();
|
||||
let callCount = 0;
|
||||
const mockHass = {
|
||||
callWS: async () => {
|
||||
callCount++;
|
||||
return { token: `token-${callCount}` };
|
||||
},
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
await fetchBrandsAccessToken(mockHass);
|
||||
scheduleBrandsTokenRefresh(mockHass);
|
||||
|
||||
// Advance 29 minutes — should not have refreshed
|
||||
await vi.advanceTimersByTimeAsync(29 * 60 * 1000);
|
||||
assert.strictEqual(callCount, 1);
|
||||
});
|
||||
|
||||
it("clearBrandsTokenRefresh stops the interval", async () => {
|
||||
vi.useFakeTimers();
|
||||
let callCount = 0;
|
||||
const mockHass = {
|
||||
callWS: async () => {
|
||||
callCount++;
|
||||
return { token: `token-${callCount}` };
|
||||
},
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
await fetchBrandsAccessToken(mockHass);
|
||||
scheduleBrandsTokenRefresh(mockHass);
|
||||
clearBrandsTokenRefresh();
|
||||
|
||||
// Advance 30 minutes — should not have refreshed because we cleared
|
||||
await vi.advanceTimersByTimeAsync(30 * 60 * 1000);
|
||||
assert.strictEqual(callCount, 1);
|
||||
});
|
||||
});
|
||||
|
||||
356
yarn.lock
356
yarn.lock
@@ -1204,14 +1204,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@bundle-stats/plugin-webpack-filter@npm:4.21.10":
|
||||
version: 4.21.10
|
||||
resolution: "@bundle-stats/plugin-webpack-filter@npm:4.21.10"
|
||||
"@bundle-stats/plugin-webpack-filter@npm:4.21.9":
|
||||
version: 4.21.9
|
||||
resolution: "@bundle-stats/plugin-webpack-filter@npm:4.21.9"
|
||||
dependencies:
|
||||
tslib: "npm:2.8.1"
|
||||
peerDependencies:
|
||||
core-js: ^3.0.0
|
||||
checksum: 10/25655718152d351429d5a2fb78652d51039d6b444da596c24f576840d1dc9cb8070204748f3f48a010401d0da21b42e5b73c840794dbd504f2fd62bf7bdda691
|
||||
checksum: 10/927bdcc8b4822d2f167f3bf53023a51579268c6aaf5cdafbe4243c30da742e776d955c8fd7a4bad866ff3c6761cdfc5e2beb5f4c76b5080d8b9509cc0e3b159e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1282,15 +1282,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@codemirror/view@npm:6.39.15, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0, @codemirror/view@npm:^6.37.0":
|
||||
version: 6.39.15
|
||||
resolution: "@codemirror/view@npm:6.39.15"
|
||||
"@codemirror/view@npm:6.39.12, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0, @codemirror/view@npm:^6.37.0":
|
||||
version: 6.39.12
|
||||
resolution: "@codemirror/view@npm:6.39.12"
|
||||
dependencies:
|
||||
"@codemirror/state": "npm:^6.5.0"
|
||||
crelt: "npm:^1.0.6"
|
||||
style-mod: "npm:^4.1.0"
|
||||
w3c-keyname: "npm:^2.2.4"
|
||||
checksum: 10/098889e36997cfca0699fff637f87c698ee324786d231a13973bd3ccd15c8d4efdb9b85acc509ea76aaba2f0e1f67868fabb40625ca86684e9d8c8f1abe65d9b
|
||||
checksum: 10/acd476d485914095fe38009bb29c7a6ac4cf3de2d3921172e27fc40639eb5bce0534766fd6e093c94aa60f076f263ce90a670a8052e220244bf4065f95c064bb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1627,13 +1627,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint/css-tree@npm:^3.6.9":
|
||||
version: 3.6.9
|
||||
resolution: "@eslint/css-tree@npm:3.6.9"
|
||||
"@eslint/css-tree@npm:^3.6.8":
|
||||
version: 3.6.8
|
||||
resolution: "@eslint/css-tree@npm:3.6.8"
|
||||
dependencies:
|
||||
mdn-data: "npm:2.23.0"
|
||||
source-map-js: "npm:^1.0.1"
|
||||
checksum: 10/f3ce66f5c8ba1957b82e1c1df79b91092cde79d3c6d3abeb9facaccb4fecc21018bf352f75e39d614b7bb2e33dd4bebaa60f2d98468167237080b9e31b35e19d
|
||||
checksum: 10/dce5da0aef43b82375906b1760b1cbea29a424ec458564f3274295dd1625610809603f96dc5a7cccab85e4d943996b917347c9a3e84f2328c24912685f400053
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1654,10 +1654,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint/js@npm:9.39.3":
|
||||
version: 9.39.3
|
||||
resolution: "@eslint/js@npm:9.39.3"
|
||||
checksum: 10/91a1a1822cfeb2eb8a89aae86be5dfabad0b66b0915946516690a8485ddd80b91f43eee346789313fea1acbb7390a4958119ca7dc9a684a5c4014f12fcb3aaf3
|
||||
"@eslint/js@npm:9.39.2":
|
||||
version: 9.39.2
|
||||
resolution: "@eslint/js@npm:9.39.2"
|
||||
checksum: 10/6b7f676746f3111b5d1b23715319212ab9297868a0fa9980d483c3da8965d5841673aada2d5653e85a3f7156edee0893a7ae7035211b4efdcb2848154bb947f2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1758,15 +1758,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-datetimeformat@npm:7.2.2":
|
||||
version: 7.2.2
|
||||
resolution: "@formatjs/intl-datetimeformat@npm:7.2.2"
|
||||
"@formatjs/intl-datetimeformat@npm:7.2.1":
|
||||
version: 7.2.1
|
||||
resolution: "@formatjs/intl-datetimeformat@npm:7.2.1"
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract": "npm:3.1.1"
|
||||
"@formatjs/intl-localematcher": "npm:0.8.1"
|
||||
decimal.js: "npm:^10.6.0"
|
||||
tslib: "npm:^2.8.1"
|
||||
checksum: 10/7ab7bc95687cdb6e6e22d5b717d6770b8b692e7feb630d0b206825e12ed82b0eb6692d4b9e7b067b2e63deaf9d08305ecb9ae43bcd5b6ad426c13d32659be62b
|
||||
checksum: 10/ed6664a70084780d13db121debb9f337d93b9f17b8690158fcf37684aaccbc9f74366bc3a01a34a3032302b0fc2189fd924492f049869c623d3c94f08c60675e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1979,74 +1979,63 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@html-eslint/core@npm:^0.56.0":
|
||||
version: 0.56.0
|
||||
resolution: "@html-eslint/core@npm:0.56.0"
|
||||
dependencies:
|
||||
"@html-eslint/types": "npm:^0.56.0"
|
||||
eslint: "npm:^9.39.1"
|
||||
html-standard: "npm:^0.0.13"
|
||||
checksum: 10/0722e7405e56b256c471229dbd0ccb49cf0edd3e5224a087240f1954c74af0a6f289e5be0b83957480f1fa5abf6f7cbad7212134a6872a3fd66e1dc1ad879d66
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@html-eslint/eslint-plugin@npm:0.56.0":
|
||||
version: 0.56.0
|
||||
resolution: "@html-eslint/eslint-plugin@npm:0.56.0"
|
||||
"@html-eslint/eslint-plugin@npm:0.55.0":
|
||||
version: 0.55.0
|
||||
resolution: "@html-eslint/eslint-plugin@npm:0.55.0"
|
||||
dependencies:
|
||||
"@eslint/plugin-kit": "npm:^0.4.1"
|
||||
"@html-eslint/core": "npm:^0.56.0"
|
||||
"@html-eslint/parser": "npm:^0.56.0"
|
||||
"@html-eslint/template-parser": "npm:^0.56.0"
|
||||
"@html-eslint/template-syntax-parser": "npm:^0.56.0"
|
||||
"@html-eslint/types": "npm:^0.56.0"
|
||||
"@html-eslint/parser": "npm:^0.55.0"
|
||||
"@html-eslint/template-parser": "npm:^0.55.0"
|
||||
"@html-eslint/template-syntax-parser": "npm:^0.55.0"
|
||||
"@html-eslint/types": "npm:^0.55.0"
|
||||
html-standard: "npm:^0.0.11"
|
||||
peerDependencies:
|
||||
eslint: ">=8.0.0 || ^10.0.0-0"
|
||||
checksum: 10/16c5d638045266ca201b5dc154b3706262801668b2edde4f0905bdba9ba2e14b7175eacf9ca7d2ad5a5a03e3369843822d34ee607b6b7fbbeb020ac7f87629fb
|
||||
checksum: 10/945a0f0b6f4007beade9d22b889984498d10aa7855d3087fd9fb71dc20b1da7b452a3877b55f7683ef12736b356279356d66e8a201b25ee4a332e139315f16ae
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@html-eslint/parser@npm:^0.56.0":
|
||||
version: 0.56.0
|
||||
resolution: "@html-eslint/parser@npm:0.56.0"
|
||||
"@html-eslint/parser@npm:^0.55.0":
|
||||
version: 0.55.0
|
||||
resolution: "@html-eslint/parser@npm:0.55.0"
|
||||
dependencies:
|
||||
"@eslint/css-tree": "npm:^3.6.9"
|
||||
"@html-eslint/template-syntax-parser": "npm:^0.56.0"
|
||||
"@html-eslint/types": "npm:^0.56.0"
|
||||
"@eslint/css-tree": "npm:^3.6.8"
|
||||
"@html-eslint/template-syntax-parser": "npm:^0.55.0"
|
||||
"@html-eslint/types": "npm:^0.55.0"
|
||||
css-tree: "npm:^3.1.0"
|
||||
es-html-parser: "npm:0.3.1"
|
||||
checksum: 10/1fead2be97481f461ee7030f4ea9274e5f969540118b4429e3eea361f62138660c1b7538668c7f8d55a8ae6985deff3056968d71bae62b91fb001d4209a24b50
|
||||
checksum: 10/f763ffb6d33f7ae7b8873772fabcb93650cd0e160b8b855cb24c41e19d9b9d423781ba2eff5cbe25b3ffc628c592a4350e91c6c56af475d21429595d944a359d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@html-eslint/template-parser@npm:^0.56.0":
|
||||
version: 0.56.0
|
||||
resolution: "@html-eslint/template-parser@npm:0.56.0"
|
||||
"@html-eslint/template-parser@npm:^0.55.0":
|
||||
version: 0.55.0
|
||||
resolution: "@html-eslint/template-parser@npm:0.55.0"
|
||||
dependencies:
|
||||
"@html-eslint/types": "npm:^0.56.0"
|
||||
"@html-eslint/types": "npm:^0.55.0"
|
||||
es-html-parser: "npm:0.3.1"
|
||||
checksum: 10/45e41dc8fb46336f40d1b78ef55ee3150d7a1034d393529eb40ce37a7d0a8fa04f6ed8f9a63478ff884d9588b1359b97a799681a37cd485d237dce867b7f9d20
|
||||
checksum: 10/a8243200a347319d14614496790b1954b55dca148406c3af58d62c158b28934c4aa832e62e546961a99cfced551d0468dc5bae4455b005ea500fad7435427934
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@html-eslint/template-syntax-parser@npm:^0.56.0":
|
||||
version: 0.56.0
|
||||
resolution: "@html-eslint/template-syntax-parser@npm:0.56.0"
|
||||
"@html-eslint/template-syntax-parser@npm:^0.55.0":
|
||||
version: 0.55.0
|
||||
resolution: "@html-eslint/template-syntax-parser@npm:0.55.0"
|
||||
dependencies:
|
||||
"@html-eslint/types": "npm:^0.56.0"
|
||||
checksum: 10/2cc4f2cbfa58c6381b2542ca86e424a97429326aa0b8b6b09e0a51d27b0e26fd723a4550e4c17536e00cfd506cbd0c61d8fd3dff1c829714c4432406096b4a01
|
||||
"@html-eslint/types": "npm:^0.55.0"
|
||||
checksum: 10/d2207b86abb86014aefb24e10e9d1b40719b4e884c146079704fb2dd95dcd5be909df9e71e18b0bbc365d7ae7b787a9d48a4d8126999bc5d109f8ae4a0a130bb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@html-eslint/types@npm:^0.56.0":
|
||||
version: 0.56.0
|
||||
resolution: "@html-eslint/types@npm:0.56.0"
|
||||
"@html-eslint/types@npm:^0.55.0":
|
||||
version: 0.55.0
|
||||
resolution: "@html-eslint/types@npm:0.55.0"
|
||||
dependencies:
|
||||
"@types/css-tree": "npm:^2.3.11"
|
||||
"@types/estree": "npm:^1.0.6"
|
||||
es-html-parser: "npm:0.3.1"
|
||||
eslint: "npm:^9.39.1"
|
||||
checksum: 10/5359ccfb8f431ccedfe7d9c0b20d883d19bbb6a24eee925c036c7774d6f2177eda021826abbc6f947b59c2364d7cf25a596591e852c1141000a3fab6ea70acc9
|
||||
checksum: 10/10d6b53e0cbb3f529a20949040bc363722f6c91bd224d78c8df2d8e6ff8ac157186e23db7bff61e7c1927f110237fd669c9d910a72399aa8b9929445c288a3cb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -2081,6 +2070,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@isaacs/balanced-match@npm:^4.0.1":
|
||||
version: 4.0.1
|
||||
resolution: "@isaacs/balanced-match@npm:4.0.1"
|
||||
checksum: 10/102fbc6d2c0d5edf8f6dbf2b3feb21695a21bc850f11bc47c4f06aa83bd8884fde3fe9d6d797d619901d96865fdcb4569ac2a54c937992c48885c5e3d9967fe8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@isaacs/brace-expansion@npm:^5.0.1":
|
||||
version: 5.0.1
|
||||
resolution: "@isaacs/brace-expansion@npm:5.0.1"
|
||||
dependencies:
|
||||
"@isaacs/balanced-match": "npm:^4.0.1"
|
||||
checksum: 10/aec226065bc4285436a27379e08cc35bf94ef59f5098ac1c026495c9ba4ab33d851964082d3648d56d63eb90f2642867bd15a3e1b810b98beb1a8c14efce6a94
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@isaacs/fs-minipass@npm:^4.0.0":
|
||||
version: 4.0.1
|
||||
resolution: "@isaacs/fs-minipass@npm:4.0.1"
|
||||
@@ -3592,16 +3597,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/plugin-retry@npm:8.1.0":
|
||||
version: 8.1.0
|
||||
resolution: "@octokit/plugin-retry@npm:8.1.0"
|
||||
"@octokit/plugin-retry@npm:8.0.3":
|
||||
version: 8.0.3
|
||||
resolution: "@octokit/plugin-retry@npm:8.0.3"
|
||||
dependencies:
|
||||
"@octokit/request-error": "npm:^7.0.2"
|
||||
"@octokit/types": "npm:^16.0.0"
|
||||
bottleneck: "npm:^2.15.3"
|
||||
peerDependencies:
|
||||
"@octokit/core": ">=7"
|
||||
checksum: 10/0bccaa14ef295ac5dc3e6fa96bb7c555b8b188dfe0bf1db5ea83acb29af375bf08a43e0d44c42941608afc6ab414b6dcdfb44881a8e8346b963d7501e0aea766
|
||||
checksum: 10/fbd459a1a73b286006fd09d1779c7e6fd2687ce945f4a66d87685c50466dab07de8f70a8ea0e87dbfe85924c5f56539e0c3631ea640b0b1b4dd23699298e1aa9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -4067,92 +4072,92 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-darwin-arm64@npm:1.7.6":
|
||||
version: 1.7.6
|
||||
resolution: "@rspack/binding-darwin-arm64@npm:1.7.6"
|
||||
"@rspack/binding-darwin-arm64@npm:1.7.5":
|
||||
version: 1.7.5
|
||||
resolution: "@rspack/binding-darwin-arm64@npm:1.7.5"
|
||||
conditions: os=darwin & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-darwin-x64@npm:1.7.6":
|
||||
version: 1.7.6
|
||||
resolution: "@rspack/binding-darwin-x64@npm:1.7.6"
|
||||
"@rspack/binding-darwin-x64@npm:1.7.5":
|
||||
version: 1.7.5
|
||||
resolution: "@rspack/binding-darwin-x64@npm:1.7.5"
|
||||
conditions: os=darwin & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-arm64-gnu@npm:1.7.6":
|
||||
version: 1.7.6
|
||||
resolution: "@rspack/binding-linux-arm64-gnu@npm:1.7.6"
|
||||
"@rspack/binding-linux-arm64-gnu@npm:1.7.5":
|
||||
version: 1.7.5
|
||||
resolution: "@rspack/binding-linux-arm64-gnu@npm:1.7.5"
|
||||
conditions: os=linux & cpu=arm64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-arm64-musl@npm:1.7.6":
|
||||
version: 1.7.6
|
||||
resolution: "@rspack/binding-linux-arm64-musl@npm:1.7.6"
|
||||
"@rspack/binding-linux-arm64-musl@npm:1.7.5":
|
||||
version: 1.7.5
|
||||
resolution: "@rspack/binding-linux-arm64-musl@npm:1.7.5"
|
||||
conditions: os=linux & cpu=arm64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-x64-gnu@npm:1.7.6":
|
||||
version: 1.7.6
|
||||
resolution: "@rspack/binding-linux-x64-gnu@npm:1.7.6"
|
||||
"@rspack/binding-linux-x64-gnu@npm:1.7.5":
|
||||
version: 1.7.5
|
||||
resolution: "@rspack/binding-linux-x64-gnu@npm:1.7.5"
|
||||
conditions: os=linux & cpu=x64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-x64-musl@npm:1.7.6":
|
||||
version: 1.7.6
|
||||
resolution: "@rspack/binding-linux-x64-musl@npm:1.7.6"
|
||||
"@rspack/binding-linux-x64-musl@npm:1.7.5":
|
||||
version: 1.7.5
|
||||
resolution: "@rspack/binding-linux-x64-musl@npm:1.7.5"
|
||||
conditions: os=linux & cpu=x64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-wasm32-wasi@npm:1.7.6":
|
||||
version: 1.7.6
|
||||
resolution: "@rspack/binding-wasm32-wasi@npm:1.7.6"
|
||||
"@rspack/binding-wasm32-wasi@npm:1.7.5":
|
||||
version: 1.7.5
|
||||
resolution: "@rspack/binding-wasm32-wasi@npm:1.7.5"
|
||||
dependencies:
|
||||
"@napi-rs/wasm-runtime": "npm:1.0.7"
|
||||
conditions: cpu=wasm32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-win32-arm64-msvc@npm:1.7.6":
|
||||
version: 1.7.6
|
||||
resolution: "@rspack/binding-win32-arm64-msvc@npm:1.7.6"
|
||||
"@rspack/binding-win32-arm64-msvc@npm:1.7.5":
|
||||
version: 1.7.5
|
||||
resolution: "@rspack/binding-win32-arm64-msvc@npm:1.7.5"
|
||||
conditions: os=win32 & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-win32-ia32-msvc@npm:1.7.6":
|
||||
version: 1.7.6
|
||||
resolution: "@rspack/binding-win32-ia32-msvc@npm:1.7.6"
|
||||
"@rspack/binding-win32-ia32-msvc@npm:1.7.5":
|
||||
version: 1.7.5
|
||||
resolution: "@rspack/binding-win32-ia32-msvc@npm:1.7.5"
|
||||
conditions: os=win32 & cpu=ia32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-win32-x64-msvc@npm:1.7.6":
|
||||
version: 1.7.6
|
||||
resolution: "@rspack/binding-win32-x64-msvc@npm:1.7.6"
|
||||
"@rspack/binding-win32-x64-msvc@npm:1.7.5":
|
||||
version: 1.7.5
|
||||
resolution: "@rspack/binding-win32-x64-msvc@npm:1.7.5"
|
||||
conditions: os=win32 & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding@npm:1.7.6":
|
||||
version: 1.7.6
|
||||
resolution: "@rspack/binding@npm:1.7.6"
|
||||
"@rspack/binding@npm:1.7.5":
|
||||
version: 1.7.5
|
||||
resolution: "@rspack/binding@npm:1.7.5"
|
||||
dependencies:
|
||||
"@rspack/binding-darwin-arm64": "npm:1.7.6"
|
||||
"@rspack/binding-darwin-x64": "npm:1.7.6"
|
||||
"@rspack/binding-linux-arm64-gnu": "npm:1.7.6"
|
||||
"@rspack/binding-linux-arm64-musl": "npm:1.7.6"
|
||||
"@rspack/binding-linux-x64-gnu": "npm:1.7.6"
|
||||
"@rspack/binding-linux-x64-musl": "npm:1.7.6"
|
||||
"@rspack/binding-wasm32-wasi": "npm:1.7.6"
|
||||
"@rspack/binding-win32-arm64-msvc": "npm:1.7.6"
|
||||
"@rspack/binding-win32-ia32-msvc": "npm:1.7.6"
|
||||
"@rspack/binding-win32-x64-msvc": "npm:1.7.6"
|
||||
"@rspack/binding-darwin-arm64": "npm:1.7.5"
|
||||
"@rspack/binding-darwin-x64": "npm:1.7.5"
|
||||
"@rspack/binding-linux-arm64-gnu": "npm:1.7.5"
|
||||
"@rspack/binding-linux-arm64-musl": "npm:1.7.5"
|
||||
"@rspack/binding-linux-x64-gnu": "npm:1.7.5"
|
||||
"@rspack/binding-linux-x64-musl": "npm:1.7.5"
|
||||
"@rspack/binding-wasm32-wasi": "npm:1.7.5"
|
||||
"@rspack/binding-win32-arm64-msvc": "npm:1.7.5"
|
||||
"@rspack/binding-win32-ia32-msvc": "npm:1.7.5"
|
||||
"@rspack/binding-win32-x64-msvc": "npm:1.7.5"
|
||||
dependenciesMeta:
|
||||
"@rspack/binding-darwin-arm64":
|
||||
optional: true
|
||||
@@ -4174,23 +4179,23 @@ __metadata:
|
||||
optional: true
|
||||
"@rspack/binding-win32-x64-msvc":
|
||||
optional: true
|
||||
checksum: 10/fec6c978e51f20471e278a07018b414125cf3bccf9c6bd7032ca65603cfe5bf0fdd7f58c156c0640b5dfab05e82a1e1170ac6d1aacaf4f46b61564be77dbe41b
|
||||
checksum: 10/3e66805d4dae5f2051f10c5a9126e5bf25926d2a6ccb1c794af2aa49c15e4fdcb9e362bd015a9afef1e788a3272dfe7a28a3c866713badda34579896d736ed4f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/core@npm:1.7.6":
|
||||
version: 1.7.6
|
||||
resolution: "@rspack/core@npm:1.7.6"
|
||||
"@rspack/core@npm:1.7.5":
|
||||
version: 1.7.5
|
||||
resolution: "@rspack/core@npm:1.7.5"
|
||||
dependencies:
|
||||
"@module-federation/runtime-tools": "npm:0.22.0"
|
||||
"@rspack/binding": "npm:1.7.6"
|
||||
"@rspack/binding": "npm:1.7.5"
|
||||
"@rspack/lite-tapable": "npm:1.1.0"
|
||||
peerDependencies:
|
||||
"@swc/helpers": ">=0.5.1"
|
||||
peerDependenciesMeta:
|
||||
"@swc/helpers":
|
||||
optional: true
|
||||
checksum: 10/9f23c4849926d9ddff34f703ab2be41878bca9e877c130d16d20d911ba4b13f15dfe96d7e86225d7f5a1e48034ab92cccec89f3765f84ff518538f6bb07f1f06
|
||||
checksum: 10/c17d93ef1e7e0728b74bf527150642d9bf072cabb63964ebd32c82da94d6d2f9eac7ff2cc13031bbd376c0c03710899f549e61d2bcfc0a6638a9f3bc8620b7ce
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -6061,13 +6066,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"balanced-match@npm:^4.0.2":
|
||||
version: 4.0.4
|
||||
resolution: "balanced-match@npm:4.0.4"
|
||||
checksum: 10/fb07bb66a0959c2843fc055838047e2a95ccebb837c519614afb067ebfdf2fa967ca8d712c35ced07f2cd26fc6f07964230b094891315ad74f11eba3d53178a0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"barcode-detector@npm:3.0.8":
|
||||
version: 3.0.8
|
||||
resolution: "barcode-detector@npm:3.0.8"
|
||||
@@ -6239,15 +6237,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"brace-expansion@npm:^5.0.2":
|
||||
version: 5.0.3
|
||||
resolution: "brace-expansion@npm:5.0.3"
|
||||
dependencies:
|
||||
balanced-match: "npm:^4.0.2"
|
||||
checksum: 10/8ba7deae4ca333d52418d2cde3287ac23f44f7330d92c3ecd96a8941597bea8aab02227bd990944d6711dd549bcc6e550fe70be5d94aa02e2fdc88942f480c9b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"braces@npm:^3.0.3, braces@npm:~3.0.2":
|
||||
version: 3.0.3
|
||||
resolution: "braces@npm:3.0.3"
|
||||
@@ -7951,28 +7940,28 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint-plugin-lit@npm:2.2.1, eslint-plugin-lit@npm:^2.0.0":
|
||||
version: 2.2.1
|
||||
resolution: "eslint-plugin-lit@npm:2.2.1"
|
||||
"eslint-plugin-lit@npm:2.1.1, eslint-plugin-lit@npm:^2.0.0":
|
||||
version: 2.1.1
|
||||
resolution: "eslint-plugin-lit@npm:2.1.1"
|
||||
dependencies:
|
||||
parse5: "npm:^6.0.1"
|
||||
parse5-htmlparser2-tree-adapter: "npm:^6.0.1"
|
||||
peerDependencies:
|
||||
eslint: ">= 8"
|
||||
checksum: 10/fc0f3c2bec52c2c1c2467b373a71e68f2bb101b3872b30be9f439660e735684bec016c6b83cea1ed71e6d94f9ec9621a805f3a188b618b84c2e25ae1adbde192
|
||||
checksum: 10/4eebb8612f5383e495bea277eda3bb86ed43d6532ec6c9443ca5048a848f96bf9b7e7f55d34dd43550d5e02cd815f19bd2e5e56c11b91d418b7948b379c78d08
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint-plugin-unused-imports@npm:4.4.1":
|
||||
version: 4.4.1
|
||||
resolution: "eslint-plugin-unused-imports@npm:4.4.1"
|
||||
"eslint-plugin-unused-imports@npm:4.3.0":
|
||||
version: 4.3.0
|
||||
resolution: "eslint-plugin-unused-imports@npm:4.3.0"
|
||||
peerDependencies:
|
||||
"@typescript-eslint/eslint-plugin": ^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0
|
||||
eslint: ^10.0.0 || ^9.0.0 || ^8.0.0
|
||||
eslint: ^9.0.0 || ^8.0.0
|
||||
peerDependenciesMeta:
|
||||
"@typescript-eslint/eslint-plugin":
|
||||
optional: true
|
||||
checksum: 10/b420fd55c393a6fdacfdbd0d1adf4cd44bed9a6584f05245091a6716272c57f38154d04b76f253619d8bf22823c0b9d630ef6b5b09edad6e51b8a1f7aec56c22
|
||||
checksum: 10/64ec1f686d90f18a27c27273a0338bad6964a611f497b81c5e7eace9d9a4f38d96070d7c389c2626095a9e5e7dbcafccc6444fa2933c9a7649996a7ca875738f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -8019,9 +8008,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint@npm:9.39.3, eslint@npm:^9.39.1":
|
||||
version: 9.39.3
|
||||
resolution: "eslint@npm:9.39.3"
|
||||
"eslint@npm:9.39.2, eslint@npm:^9.39.1":
|
||||
version: 9.39.2
|
||||
resolution: "eslint@npm:9.39.2"
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils": "npm:^4.8.0"
|
||||
"@eslint-community/regexpp": "npm:^4.12.1"
|
||||
@@ -8029,7 +8018,7 @@ __metadata:
|
||||
"@eslint/config-helpers": "npm:^0.4.2"
|
||||
"@eslint/core": "npm:^0.17.0"
|
||||
"@eslint/eslintrc": "npm:^3.3.1"
|
||||
"@eslint/js": "npm:9.39.3"
|
||||
"@eslint/js": "npm:9.39.2"
|
||||
"@eslint/plugin-kit": "npm:^0.4.1"
|
||||
"@humanfs/node": "npm:^0.16.6"
|
||||
"@humanwhocodes/module-importer": "npm:^1.0.1"
|
||||
@@ -8064,7 +8053,7 @@ __metadata:
|
||||
optional: true
|
||||
bin:
|
||||
eslint: bin/eslint.js
|
||||
checksum: 10/1c95c92983ddf435e7f7d54edd06d703a15773a7d189583d3388e5b5ac714f0a2450b91c0b3bb9b9ccec9bd20994fd8e48d231ed6dabca0be56ef314b32820ff
|
||||
checksum: 10/53ff0e9c8264e7e8d40d50fdc0c0df0b701cfc5289beedfb686c214e3e7b199702f894bbd1bb48653727bb1ecbd1147cf5f555a4ae71e1daf35020cdc9072d9f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -8845,14 +8834,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"glob@npm:13.0.6, glob@npm:^13.0.0":
|
||||
version: 13.0.6
|
||||
resolution: "glob@npm:13.0.6"
|
||||
"glob@npm:13.0.1, glob@npm:^13.0.0":
|
||||
version: 13.0.1
|
||||
resolution: "glob@npm:13.0.1"
|
||||
dependencies:
|
||||
minimatch: "npm:^10.2.2"
|
||||
minipass: "npm:^7.1.3"
|
||||
path-scurry: "npm:^2.0.2"
|
||||
checksum: 10/201ad69e5f0aa74e1d8c00a481581f8b8c804b6a4fbfabeeb8541f5d756932800331daeba99b58fb9e4cd67e12ba5a7eba5b82fb476691588418060b84353214
|
||||
minimatch: "npm:^10.1.2"
|
||||
minipass: "npm:^7.1.2"
|
||||
path-scurry: "npm:^2.0.0"
|
||||
checksum: 10/465e8cc269ab88d7415a3906cdc0f4543a2ae54df99207204af5bc28a944396d8d893822f546a8056a78ec714e608ab4f3502532c4d6b9cc5e113adf0fe5109e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9149,17 +9138,17 @@ __metadata:
|
||||
"@babel/preset-env": "npm:7.29.0"
|
||||
"@babel/runtime": "npm:7.28.6"
|
||||
"@braintree/sanitize-url": "npm:7.1.2"
|
||||
"@bundle-stats/plugin-webpack-filter": "npm:4.21.10"
|
||||
"@bundle-stats/plugin-webpack-filter": "npm:4.21.9"
|
||||
"@codemirror/autocomplete": "npm:6.20.0"
|
||||
"@codemirror/commands": "npm:6.10.2"
|
||||
"@codemirror/language": "npm:6.12.1"
|
||||
"@codemirror/legacy-modes": "npm:6.5.2"
|
||||
"@codemirror/search": "npm:6.6.0"
|
||||
"@codemirror/state": "npm:6.5.4"
|
||||
"@codemirror/view": "npm:6.39.15"
|
||||
"@codemirror/view": "npm:6.39.12"
|
||||
"@date-fns/tz": "npm:1.4.1"
|
||||
"@egjs/hammerjs": "npm:2.0.17"
|
||||
"@formatjs/intl-datetimeformat": "npm:7.2.2"
|
||||
"@formatjs/intl-datetimeformat": "npm:7.2.1"
|
||||
"@formatjs/intl-displaynames": "npm:7.2.1"
|
||||
"@formatjs/intl-durationformat": "npm:0.10.1"
|
||||
"@formatjs/intl-getcanonicallocales": "npm:3.2.1"
|
||||
@@ -9175,7 +9164,7 @@ __metadata:
|
||||
"@fullcalendar/luxon3": "npm:6.1.20"
|
||||
"@fullcalendar/timegrid": "npm:6.1.20"
|
||||
"@home-assistant/webawesome": "npm:3.2.1-ha.2"
|
||||
"@html-eslint/eslint-plugin": "npm:0.56.0"
|
||||
"@html-eslint/eslint-plugin": "npm:0.55.0"
|
||||
"@lezer/highlight": "npm:1.2.3"
|
||||
"@lit-labs/motion": "npm:1.1.0"
|
||||
"@lit-labs/observers": "npm:2.1.0"
|
||||
@@ -9207,11 +9196,11 @@ __metadata:
|
||||
"@mdi/js": "npm:7.4.47"
|
||||
"@mdi/svg": "npm:7.4.47"
|
||||
"@octokit/auth-oauth-device": "npm:8.0.3"
|
||||
"@octokit/plugin-retry": "npm:8.1.0"
|
||||
"@octokit/plugin-retry": "npm:8.0.3"
|
||||
"@octokit/rest": "npm:22.0.1"
|
||||
"@replit/codemirror-indentation-markers": "npm:6.5.3"
|
||||
"@rsdoctor/rspack-plugin": "npm:1.5.2"
|
||||
"@rspack/core": "npm:1.7.6"
|
||||
"@rspack/core": "npm:1.7.5"
|
||||
"@rspack/dev-server": "npm:1.2.1"
|
||||
"@swc/helpers": "npm:0.5.18"
|
||||
"@thomasloven/round-slider": "npm:0.6.0"
|
||||
@@ -9257,19 +9246,19 @@ __metadata:
|
||||
dialog-polyfill: "npm:0.5.6"
|
||||
echarts: "npm:6.0.0"
|
||||
element-internals-polyfill: "npm:3.0.2"
|
||||
eslint: "npm:9.39.3"
|
||||
eslint: "npm:9.39.2"
|
||||
eslint-config-airbnb-base: "npm:15.0.0"
|
||||
eslint-config-prettier: "npm:10.1.8"
|
||||
eslint-import-resolver-webpack: "npm:0.13.10"
|
||||
eslint-plugin-import: "npm:2.32.0"
|
||||
eslint-plugin-lit: "npm:2.2.1"
|
||||
eslint-plugin-lit: "npm:2.1.1"
|
||||
eslint-plugin-lit-a11y: "npm:5.1.1"
|
||||
eslint-plugin-unused-imports: "npm:4.4.1"
|
||||
eslint-plugin-unused-imports: "npm:4.3.0"
|
||||
eslint-plugin-wc: "npm:3.0.2"
|
||||
fancy-log: "npm:2.0.0"
|
||||
fs-extra: "npm:11.3.3"
|
||||
fuse.js: "npm:7.1.0"
|
||||
glob: "npm:13.0.6"
|
||||
glob: "npm:13.0.1"
|
||||
google-timezones-json: "npm:1.2.0"
|
||||
gulp: "npm:5.0.1"
|
||||
gulp-brotli: "npm:3.0.0"
|
||||
@@ -9296,7 +9285,7 @@ __metadata:
|
||||
lodash.template: "npm:4.5.0"
|
||||
luxon: "npm:3.7.2"
|
||||
map-stream: "npm:0.0.7"
|
||||
marked: "npm:17.0.3"
|
||||
marked: "npm:17.0.1"
|
||||
memoize-one: "npm:6.0.0"
|
||||
node-vibrant: "npm:4.0.4"
|
||||
object-hash: "npm:3.0.0"
|
||||
@@ -9416,13 +9405,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"html-standard@npm:^0.0.13":
|
||||
version: 0.0.13
|
||||
resolution: "html-standard@npm:0.0.13"
|
||||
"html-standard@npm:^0.0.11":
|
||||
version: 0.0.11
|
||||
resolution: "html-standard@npm:0.0.11"
|
||||
dependencies:
|
||||
vscode-css-languageservice: "npm:^6.3.9"
|
||||
vscode-languageserver-textdocument: "npm:^1.0.12"
|
||||
checksum: 10/a069a58cae3367c97ed82bd31bf166a6c93d746ae406df1428832a4187f93ff1b1b06317ffe178b6bc6b79a705c175ce977c7ba2471f5fb26a1c1b65baac5de5
|
||||
checksum: 10/724cf7e7234edfda8bad776f2deaf430fa986fdb97fd41d491229aa1e5a1f01dbf6603efb1651b997487fc2c67a788bd6346266830ac4d860938dc3dc11e99b4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -10891,12 +10879,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"marked@npm:17.0.3":
|
||||
version: 17.0.3
|
||||
resolution: "marked@npm:17.0.3"
|
||||
"marked@npm:17.0.1":
|
||||
version: 17.0.1
|
||||
resolution: "marked@npm:17.0.1"
|
||||
bin:
|
||||
marked: bin/marked.js
|
||||
checksum: 10/2e3bce6bb012b0d39fba75689b4dea672156d372ae8f089de99e73932bf71f0bb3a5bffa43e58474d6476950b462b7580cb181d68134df3d222b12d190bfa4b2
|
||||
checksum: 10/8d39cf5a1eb97288d09d47dba57a613b8c45a212287b27aa16b4bc41baaebb147340c359d379da1bc6d351474d169aa3a63f981770b67f7663ed0a479dd950c2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -11081,12 +11069,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minimatch@npm:^10.2.2":
|
||||
version: 10.2.2
|
||||
resolution: "minimatch@npm:10.2.2"
|
||||
"minimatch@npm:^10.1.2":
|
||||
version: 10.1.2
|
||||
resolution: "minimatch@npm:10.1.2"
|
||||
dependencies:
|
||||
brace-expansion: "npm:^5.0.2"
|
||||
checksum: 10/e135be7b502ac97c02bcee42ccc1c55dc26dbac036c0f4acde69e42fe339d7fb53fae711e57b3546cb533426382ea492c73a073c7f78832e0453d120d48dd015
|
||||
"@isaacs/brace-expansion": "npm:^5.0.1"
|
||||
checksum: 10/6f0ef975463739207144e411bdd54f7205ce38770b162fa3bc4c9be4987a16cb20d0962a82f26c2372598cfba90faa97b327239d303b529b774f17681c163b46
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -11182,10 +11170,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2, minipass@npm:^7.1.3":
|
||||
version: 7.1.3
|
||||
resolution: "minipass@npm:7.1.3"
|
||||
checksum: 10/175e4d5e20980c3cd316ae82d2c031c42f6c746467d8b1905b51060a0ba4461441a0c25bb67c025fd9617f9a3873e152c7b543c6b5ac83a1846be8ade80dffd6
|
||||
"minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2":
|
||||
version: 7.1.2
|
||||
resolution: "minipass@npm:7.1.2"
|
||||
checksum: 10/c25f0ee8196d8e6036661104bacd743785b2599a21de5c516b32b3fa2b83113ac89a2358465bc04956baab37ffb956ae43be679b2262bf7be15fce467ccd7950
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -11861,13 +11849,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"path-scurry@npm:^2.0.2":
|
||||
version: 2.0.2
|
||||
resolution: "path-scurry@npm:2.0.2"
|
||||
"path-scurry@npm:^2.0.0":
|
||||
version: 2.0.1
|
||||
resolution: "path-scurry@npm:2.0.1"
|
||||
dependencies:
|
||||
lru-cache: "npm:^11.0.0"
|
||||
minipass: "npm:^7.1.2"
|
||||
checksum: 10/2b4257422bcb870a4c2d205b3acdbb213a72f5e2250f61c80f79c9d014d010f82bdf8584441612c8e1fa4eb098678f5704a66fa8377d72646bad4be38e57a2c3
|
||||
checksum: 10/1e9c74e9ccf94d7c16056a5cb2dba9fa23eec1bc221ab15c44765486b9b9975b4cd9a4d55da15b96eadf67d5202e9a2f1cec9023fbb35fe7d9ccd0ff1891f88b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user