mirror of
https://github.com/home-assistant/frontend.git
synced 2026-03-29 22:33:53 +00:00
Compare commits
55 Commits
retro-east
...
clock-date
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5d61d4041 | ||
|
|
79780b111c | ||
|
|
a592a5f222 | ||
|
|
2ac8bf9179 | ||
|
|
fd21dd2fd4 | ||
|
|
370ccd95da | ||
|
|
ed2161effd | ||
|
|
19d902afc6 | ||
|
|
672d235c3e | ||
|
|
5246ce3f72 | ||
|
|
c83924efa7 | ||
|
|
bdbcec4d90 | ||
|
|
a074c80ec3 | ||
|
|
3795ad1253 | ||
|
|
7bb466a75b | ||
|
|
bff2514eed | ||
|
|
602d41b31d | ||
|
|
85d10cf982 | ||
|
|
a3ff3346db | ||
|
|
38a314ced4 | ||
|
|
2cf7452ed1 | ||
|
|
ae97cc1c8d | ||
|
|
65bba30266 | ||
|
|
8ee3544a32 | ||
|
|
fcddf8f548 | ||
|
|
c7824d4059 | ||
|
|
8c4f5206b1 | ||
|
|
cc2a7972fc | ||
|
|
33079bb12c | ||
|
|
34152e522e | ||
|
|
a0dc331056 | ||
|
|
4a56c1404f | ||
|
|
7e7845853d | ||
|
|
f8fe7a7d82 | ||
|
|
8b40b55324 | ||
|
|
ab55d1fdde | ||
|
|
597099f153 | ||
|
|
40ba2ade58 | ||
|
|
901fa4cdda | ||
|
|
edf007718a | ||
|
|
5abaeea1f9 | ||
|
|
1ce0a7eab2 | ||
|
|
c0c02eb548 | ||
|
|
18d5b84a02 | ||
|
|
ebc58f025a | ||
|
|
cb2758d868 | ||
|
|
5bbfa36228 | ||
|
|
a8070b322c | ||
|
|
9cbc44123e | ||
|
|
c8f4c892f9 | ||
|
|
40b9f9dccb | ||
|
|
823c222a55 | ||
|
|
02acd2996c | ||
|
|
c462fc0639 | ||
|
|
903553dab9 |
@@ -40,18 +40,24 @@ const convertToJSON = async (
|
||||
throw e;
|
||||
}
|
||||
// Convert to JSON
|
||||
const parts = localeData.split("} else {");
|
||||
const firstBlock = parts[0];
|
||||
const obj = INTL_POLYFILLS[pkg];
|
||||
const dataRegex = new RegExp(
|
||||
`Intl\\.${obj}\\.${addFunc}\\((?<data>.*)\\)`,
|
||||
"s"
|
||||
);
|
||||
localeData = localeData.match(dataRegex)?.groups?.data;
|
||||
localeData = firstBlock.match(dataRegex)?.groups?.data;
|
||||
if (!localeData) {
|
||||
throw Error(`Failed to extract data for language ${lang} from ${pkg}`);
|
||||
}
|
||||
// Parse to validate JSON, then stringify to minify
|
||||
localeData = JSON.stringify(JSON.parse(localeData));
|
||||
await writeFile(join(outDir, `${pkg}/${lang}.json`), localeData);
|
||||
try {
|
||||
localeData = JSON.stringify(JSON.parse(localeData));
|
||||
await writeFile(join(outDir, `${pkg}/${lang}.json`), localeData);
|
||||
} catch (e) {
|
||||
throw Error(`Failed to parse JSON for language ${lang} from ${pkg}: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
gulp.task("clean-locale-data", async () => deleteSync([outDir]));
|
||||
|
||||
20
package.json
20
package.json
@@ -37,15 +37,15 @@
|
||||
"@codemirror/view": "6.40.0",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.2.6",
|
||||
"@formatjs/intl-displaynames": "7.2.3",
|
||||
"@formatjs/intl-durationformat": "0.10.2",
|
||||
"@formatjs/intl-datetimeformat": "7.3.1",
|
||||
"@formatjs/intl-displaynames": "7.3.1",
|
||||
"@formatjs/intl-durationformat": "0.10.3",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.2",
|
||||
"@formatjs/intl-listformat": "8.2.3",
|
||||
"@formatjs/intl-locale": "5.2.2",
|
||||
"@formatjs/intl-numberformat": "9.2.4",
|
||||
"@formatjs/intl-pluralrules": "6.2.4",
|
||||
"@formatjs/intl-relativetimeformat": "12.2.4",
|
||||
"@formatjs/intl-listformat": "8.3.1",
|
||||
"@formatjs/intl-locale": "5.3.1",
|
||||
"@formatjs/intl-numberformat": "9.3.1",
|
||||
"@formatjs/intl-pluralrules": "6.3.1",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.1",
|
||||
"@fullcalendar/core": "6.1.20",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"@fullcalendar/interaction": "6.1.20",
|
||||
@@ -108,7 +108,7 @@
|
||||
"hls.js": "1.6.15",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "11.1.3",
|
||||
"intl-messageformat": "11.2.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
@@ -116,7 +116,7 @@
|
||||
"lit": "3.3.2",
|
||||
"lit-html": "3.3.2",
|
||||
"luxon": "3.7.2",
|
||||
"marked": "17.0.4",
|
||||
"marked": "17.0.5",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.4",
|
||||
"object-hash": "3.0.0",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { LitElement, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-multi-textfield";
|
||||
import "./input/ha-input-multi";
|
||||
|
||||
@customElement("ha-aliases-editor")
|
||||
class AliasesEditor extends LitElement {
|
||||
@@ -20,7 +20,7 @@ class AliasesEditor extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-multi-textfield
|
||||
<ha-input-multi
|
||||
.hass=${this.hass}
|
||||
.value=${this.aliases}
|
||||
.disabled=${this.disabled}
|
||||
@@ -32,7 +32,7 @@ class AliasesEditor extends LitElement {
|
||||
item-index
|
||||
@value-changed=${this._aliasesChanged}
|
||||
>
|
||||
</ha-multi-textfield>
|
||||
</ha-input-multi>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -76,10 +76,12 @@ export class HaGauge extends LitElement {
|
||||
const arcRadius = 40;
|
||||
const arcLength = Math.PI * arcRadius;
|
||||
const valueAngle = getAngle(this.value, this.min, this.max);
|
||||
const strokeOffset = arcLength * (1 - valueAngle / 180);
|
||||
const strokeOffset = this._updated
|
||||
? arcLength * (1 - valueAngle / 180)
|
||||
: arcLength;
|
||||
|
||||
return svg`
|
||||
<svg viewBox="-50 -50 100 60" class="gauge">
|
||||
<svg viewBox="-50 -50 100 55" class="gauge">
|
||||
<path
|
||||
class="levels-base"
|
||||
d="M -40 0 A 40 40 0 0 1 40 0"
|
||||
@@ -181,7 +183,7 @@ export class HaGauge extends LitElement {
|
||||
<text
|
||||
class="value-text"
|
||||
x="0"
|
||||
y="-10"
|
||||
y="-5"
|
||||
dominant-baseline="middle"
|
||||
text-anchor="middle"
|
||||
>
|
||||
@@ -222,22 +224,22 @@ export class HaGauge extends LitElement {
|
||||
.levels-base {
|
||||
fill: none;
|
||||
stroke: var(--primary-background-color);
|
||||
stroke-width: 10;
|
||||
stroke-width: 8;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.level {
|
||||
fill: none;
|
||||
stroke-width: 10;
|
||||
stroke-width: 8;
|
||||
stroke-linecap: butt;
|
||||
}
|
||||
|
||||
.value {
|
||||
fill: none;
|
||||
stroke-width: 10;
|
||||
stroke-width: 8;
|
||||
stroke: var(--gauge-color);
|
||||
stroke-linecap: round;
|
||||
transition: all 1s ease 0s;
|
||||
transition: stroke-dashoffset 1s ease 0s;
|
||||
}
|
||||
|
||||
.needle {
|
||||
@@ -249,6 +251,7 @@ export class HaGauge extends LitElement {
|
||||
}
|
||||
|
||||
.value-text {
|
||||
font-size: var(--ha-font-size-l);
|
||||
fill: var(--primary-text-color);
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
@@ -1,310 +0,0 @@
|
||||
export const RETRO_THEME = {
|
||||
// Sharp corners
|
||||
"ha-border-radius-sm": "0",
|
||||
"ha-border-radius-md": "0",
|
||||
"ha-border-radius-lg": "0",
|
||||
"ha-border-radius-xl": "0",
|
||||
"ha-border-radius-2xl": "0",
|
||||
"ha-border-radius-3xl": "0",
|
||||
"ha-border-radius-4xl": "0",
|
||||
"ha-border-radius-5xl": "0",
|
||||
"ha-border-radius-6xl": "0",
|
||||
"ha-border-radius-pill": "0",
|
||||
"ha-border-radius-circle": "0",
|
||||
|
||||
// Fonts
|
||||
"ha-font-family-body":
|
||||
"Tahoma, 'MS Sans Serif', 'Microsoft Sans Serif', Arial, sans-serif",
|
||||
"ha-font-family-heading":
|
||||
"Tahoma, 'MS Sans Serif', 'Microsoft Sans Serif', Arial, sans-serif",
|
||||
"ha-font-family-code": "'Courier New', Courier, monospace",
|
||||
"ha-font-family-longform":
|
||||
"Tahoma, 'MS Sans Serif', 'Microsoft Sans Serif', Arial, sans-serif",
|
||||
|
||||
// No transparency
|
||||
"ha-dialog-scrim-backdrop-filter": "none",
|
||||
|
||||
// Disable animations
|
||||
"ha-animation-duration-fast": "1ms",
|
||||
"ha-animation-duration-normal": "1ms",
|
||||
"ha-animation-duration-slow": "1ms",
|
||||
|
||||
modes: {
|
||||
light: {
|
||||
// Base colors
|
||||
"primary-color": "#000080",
|
||||
"dark-primary-color": "#00006B",
|
||||
"light-primary-color": "#4040C0",
|
||||
"accent-color": "#000080",
|
||||
"primary-text-color": "#000000",
|
||||
"secondary-text-color": "#404040",
|
||||
"text-primary-color": "#ffffff",
|
||||
"text-light-primary-color": "#000000",
|
||||
"disabled-text-color": "#808080",
|
||||
|
||||
// Backgrounds
|
||||
"primary-background-color": "#C0C0C0",
|
||||
"lovelace-background": "#008080",
|
||||
"secondary-background-color": "#C0C0C0",
|
||||
"card-background-color": "#C0C0C0",
|
||||
"clear-background-color": "#C0C0C0",
|
||||
|
||||
// RGB values
|
||||
"rgb-primary-color": "0, 0, 128",
|
||||
"rgb-accent-color": "0, 0, 128",
|
||||
"rgb-primary-text-color": "0, 0, 0",
|
||||
"rgb-secondary-text-color": "64, 64, 64",
|
||||
"rgb-text-primary-color": "255, 255, 255",
|
||||
"rgb-card-background-color": "192, 192, 192",
|
||||
|
||||
// UI chrome
|
||||
"divider-color": "#808080",
|
||||
"outline-color": "#808080",
|
||||
"outline-hover-color": "#404040",
|
||||
"shadow-color": "rgba(0, 0, 0, 0.5)",
|
||||
"scrollbar-thumb-color": "#808080",
|
||||
"disabled-color": "#808080",
|
||||
|
||||
// Cards - retro bevel effect
|
||||
"ha-card-border-width": "1px",
|
||||
"ha-card-border-color": "#808080",
|
||||
"ha-card-box-shadow": "1px 1px 0 #404040, -1px -1px 0 #ffffff",
|
||||
"ha-card-border-radius": "0",
|
||||
|
||||
// Dialogs
|
||||
"ha-dialog-border-radius": "0",
|
||||
"ha-dialog-surface-background": "#C0C0C0",
|
||||
"dialog-box-shadow": "1px 1px 0 #404040, -1px -1px 0 #ffffff",
|
||||
|
||||
// Box shadows - retro bevel
|
||||
"ha-box-shadow-s": "1px 1px 0 #404040, -1px -1px 0 #ffffff",
|
||||
"ha-box-shadow-m": "1px 1px 0 #404040, -1px -1px 0 #ffffff",
|
||||
"ha-box-shadow-l": "1px 1px 0 #404040, -1px -1px 0 #ffffff",
|
||||
|
||||
// Header
|
||||
"app-header-background-color": "#000080",
|
||||
"app-header-text-color": "#ffffff",
|
||||
"app-header-border-bottom": "2px outset #C0C0C0",
|
||||
|
||||
// Sidebar
|
||||
"sidebar-background-color": "#C0C0C0",
|
||||
"sidebar-text-color": "#000000",
|
||||
"sidebar-selected-text-color": "#ffffff",
|
||||
"sidebar-selected-icon-color": "#000080",
|
||||
"sidebar-icon-color": "#000000",
|
||||
|
||||
// Input
|
||||
"input-fill-color": "#C0C0C0",
|
||||
"input-disabled-fill-color": "#C0C0C0",
|
||||
"input-ink-color": "#000000",
|
||||
"input-label-ink-color": "#000000",
|
||||
"input-disabled-ink-color": "#808080",
|
||||
"input-idle-line-color": "#808080",
|
||||
"input-hover-line-color": "#000000",
|
||||
"input-disabled-line-color": "#808080",
|
||||
"input-outlined-idle-border-color": "#808080",
|
||||
"input-outlined-hover-border-color": "#000000",
|
||||
"input-outlined-disabled-border-color": "#C0C0C0",
|
||||
"input-dropdown-icon-color": "#000000",
|
||||
|
||||
// Status colors
|
||||
"error-color": "#FF0000",
|
||||
"warning-color": "#FF8000",
|
||||
"success-color": "#008000",
|
||||
"info-color": "#000080",
|
||||
|
||||
// State
|
||||
"state-icon-color": "#000080",
|
||||
"state-active-color": "#000080",
|
||||
"state-inactive-color": "#808080",
|
||||
|
||||
// Data table
|
||||
"data-table-border-width": "0",
|
||||
|
||||
// Primary scale
|
||||
"ha-color-primary-05": "#00003A",
|
||||
"ha-color-primary-10": "#000050",
|
||||
"ha-color-primary-20": "#000066",
|
||||
"ha-color-primary-30": "#00007A",
|
||||
"ha-color-primary-40": "#000080",
|
||||
"ha-color-primary-50": "#0000AA",
|
||||
"ha-color-primary-60": "#4040C0",
|
||||
"ha-color-primary-70": "#6060D0",
|
||||
"ha-color-primary-80": "#8080E0",
|
||||
"ha-color-primary-90": "#C8C8D8",
|
||||
"ha-color-primary-95": "#D8D8E0",
|
||||
|
||||
// Neutral scale
|
||||
"ha-color-neutral-05": "#000000",
|
||||
"ha-color-neutral-10": "#2A2A2A",
|
||||
"ha-color-neutral-20": "#404040",
|
||||
"ha-color-neutral-30": "#606060",
|
||||
"ha-color-neutral-40": "#707070",
|
||||
"ha-color-neutral-50": "#808080",
|
||||
"ha-color-neutral-60": "#909090",
|
||||
"ha-color-neutral-70": "#A0A0A0",
|
||||
"ha-color-neutral-80": "#B0B0B0",
|
||||
"ha-color-neutral-90": "#C8C8C8",
|
||||
"ha-color-neutral-95": "#D0D0D0",
|
||||
|
||||
// Codemirror
|
||||
"codemirror-keyword": "#000080",
|
||||
"codemirror-operator": "#000000",
|
||||
"codemirror-variable": "#008080",
|
||||
"codemirror-variable-2": "#000080",
|
||||
"codemirror-variable-3": "#808000",
|
||||
"codemirror-builtin": "#800080",
|
||||
"codemirror-atom": "#008080",
|
||||
"codemirror-number": "#FF0000",
|
||||
"codemirror-def": "#000080",
|
||||
"codemirror-string": "#008000",
|
||||
"codemirror-string-2": "#808000",
|
||||
"codemirror-comment": "#808080",
|
||||
"codemirror-tag": "#800000",
|
||||
"codemirror-meta": "#000080",
|
||||
"codemirror-attribute": "#FF0000",
|
||||
"codemirror-property": "#000080",
|
||||
"codemirror-qualifier": "#808000",
|
||||
"codemirror-type": "#000080",
|
||||
},
|
||||
dark: {
|
||||
// Base colors
|
||||
"primary-color": "#4040C0",
|
||||
"dark-primary-color": "#000080",
|
||||
"light-primary-color": "#6060D0",
|
||||
"accent-color": "#4040C0",
|
||||
"primary-text-color": "#C0C0C0",
|
||||
"secondary-text-color": "#A0A0A0",
|
||||
"text-primary-color": "#ffffff",
|
||||
"text-light-primary-color": "#C0C0C0",
|
||||
"disabled-text-color": "#606060",
|
||||
|
||||
// Backgrounds
|
||||
"primary-background-color": "#2A2A2A",
|
||||
"lovelace-background": "#003030",
|
||||
"secondary-background-color": "#2A2A2A",
|
||||
"card-background-color": "#3A3A3A",
|
||||
"clear-background-color": "#2A2A2A",
|
||||
|
||||
// RGB values
|
||||
"rgb-primary-color": "64, 64, 192",
|
||||
"rgb-accent-color": "64, 64, 192",
|
||||
"rgb-primary-text-color": "192, 192, 192",
|
||||
"rgb-secondary-text-color": "160, 160, 160",
|
||||
"rgb-text-primary-color": "255, 255, 255",
|
||||
"rgb-card-background-color": "58, 58, 58",
|
||||
|
||||
// UI chrome
|
||||
"divider-color": "#606060",
|
||||
"outline-color": "#606060",
|
||||
"outline-hover-color": "#808080",
|
||||
"shadow-color": "rgba(0, 0, 0, 0.7)",
|
||||
"scrollbar-thumb-color": "#606060",
|
||||
"disabled-color": "#606060",
|
||||
|
||||
// Cards - retro bevel effect
|
||||
"ha-card-border-width": "1px",
|
||||
"ha-card-border-color": "#606060",
|
||||
"ha-card-box-shadow": "1px 1px 0 #1A1A1A, -1px -1px 0 #5A5A5A",
|
||||
"ha-card-border-radius": "0",
|
||||
|
||||
// Dialogs
|
||||
"ha-dialog-border-radius": "0",
|
||||
"ha-dialog-surface-background": "#3A3A3A",
|
||||
"dialog-box-shadow": "1px 1px 0 #1A1A1A, -1px -1px 0 #5A5A5A",
|
||||
|
||||
// Box shadows - retro bevel
|
||||
"ha-box-shadow-s": "1px 1px 0 #1A1A1A, -1px -1px 0 #5A5A5A",
|
||||
"ha-box-shadow-m": "1px 1px 0 #1A1A1A, -1px -1px 0 #5A5A5A",
|
||||
"ha-box-shadow-l": "1px 1px 0 #1A1A1A, -1px -1px 0 #5A5A5A",
|
||||
|
||||
// Header
|
||||
"app-header-background-color": "#000060",
|
||||
"app-header-text-color": "#ffffff",
|
||||
"app-header-border-bottom": "2px outset #3A3A3A",
|
||||
|
||||
// Sidebar
|
||||
"sidebar-background-color": "#2A2A2A",
|
||||
"sidebar-text-color": "#C0C0C0",
|
||||
"sidebar-selected-text-color": "#ffffff",
|
||||
"sidebar-selected-icon-color": "#4040C0",
|
||||
"sidebar-icon-color": "#A0A0A0",
|
||||
|
||||
// Input
|
||||
"input-fill-color": "#3A3A3A",
|
||||
"input-disabled-fill-color": "#3A3A3A",
|
||||
"input-ink-color": "#C0C0C0",
|
||||
"input-label-ink-color": "#A0A0A0",
|
||||
"input-disabled-ink-color": "#606060",
|
||||
"input-idle-line-color": "#606060",
|
||||
"input-hover-line-color": "#808080",
|
||||
"input-disabled-line-color": "#404040",
|
||||
"input-outlined-idle-border-color": "#606060",
|
||||
"input-outlined-hover-border-color": "#808080",
|
||||
"input-outlined-disabled-border-color": "#404040",
|
||||
"input-dropdown-icon-color": "#A0A0A0",
|
||||
|
||||
// Status colors
|
||||
"error-color": "#FF4040",
|
||||
"warning-color": "#FFA040",
|
||||
"success-color": "#40C040",
|
||||
"info-color": "#4040C0",
|
||||
|
||||
// State
|
||||
"state-icon-color": "#4040C0",
|
||||
"state-active-color": "#4040C0",
|
||||
"state-inactive-color": "#606060",
|
||||
|
||||
// Data table
|
||||
"data-table-border-width": "0",
|
||||
|
||||
// Primary scale
|
||||
"ha-color-primary-05": "#00002A",
|
||||
"ha-color-primary-10": "#000040",
|
||||
"ha-color-primary-20": "#000060",
|
||||
"ha-color-primary-30": "#000080",
|
||||
"ha-color-primary-40": "#4040C0",
|
||||
"ha-color-primary-50": "#6060D0",
|
||||
"ha-color-primary-60": "#8080E0",
|
||||
"ha-color-primary-70": "#A0A0F0",
|
||||
"ha-color-primary-80": "#C0C0FF",
|
||||
"ha-color-primary-90": "#3A3A58",
|
||||
"ha-color-primary-95": "#303048",
|
||||
|
||||
// Neutral scale
|
||||
"ha-color-neutral-05": "#1A1A1A",
|
||||
"ha-color-neutral-10": "#2A2A2A",
|
||||
"ha-color-neutral-20": "#3A3A3A",
|
||||
"ha-color-neutral-30": "#4A4A4A",
|
||||
"ha-color-neutral-40": "#606060",
|
||||
"ha-color-neutral-50": "#707070",
|
||||
"ha-color-neutral-60": "#808080",
|
||||
"ha-color-neutral-70": "#909090",
|
||||
"ha-color-neutral-80": "#A0A0A0",
|
||||
"ha-color-neutral-90": "#C0C0C0",
|
||||
"ha-color-neutral-95": "#D0D0D0",
|
||||
|
||||
// Codemirror
|
||||
"codemirror-keyword": "#8080E0",
|
||||
"codemirror-operator": "#C0C0C0",
|
||||
"codemirror-variable": "#40C0C0",
|
||||
"codemirror-variable-2": "#8080E0",
|
||||
"codemirror-variable-3": "#C0C040",
|
||||
"codemirror-builtin": "#C040C0",
|
||||
"codemirror-atom": "#40C0C0",
|
||||
"codemirror-number": "#FF6060",
|
||||
"codemirror-def": "#8080E0",
|
||||
"codemirror-string": "#40C040",
|
||||
"codemirror-string-2": "#C0C040",
|
||||
"codemirror-comment": "#808080",
|
||||
"codemirror-tag": "#C04040",
|
||||
"codemirror-meta": "#8080E0",
|
||||
"codemirror-attribute": "#FF6060",
|
||||
"codemirror-property": "#8080E0",
|
||||
"codemirror-qualifier": "#C0C040",
|
||||
"codemirror-type": "#8080E0",
|
||||
"map-filter":
|
||||
"invert(0.9) hue-rotate(170deg) brightness(1.5) contrast(1.2) saturate(0.3)",
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,683 +0,0 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import {
|
||||
applyThemesOnElement,
|
||||
invalidateThemeCache,
|
||||
} from "../common/dom/apply_themes_on_element";
|
||||
import type { LocalizeKeys } from "../common/translations/localize";
|
||||
import { subscribeLabFeature } from "../data/labs";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { RETRO_THEME } from "./ha-retro-theme";
|
||||
|
||||
const TIP_COUNT = 25;
|
||||
|
||||
type CasitaExpression =
|
||||
| "hi"
|
||||
| "ok-nabu"
|
||||
| "heart"
|
||||
| "sleep"
|
||||
| "great-job"
|
||||
| "error";
|
||||
|
||||
const STORAGE_KEY = "retro-position";
|
||||
const DRAG_THRESHOLD = 5;
|
||||
const BUBBLE_TIMEOUT = 8000;
|
||||
const SLEEP_TIMEOUT = 30000;
|
||||
const BSOD_CLICK_COUNT = 5;
|
||||
const BSOD_CLICK_TIMEOUT = 3000;
|
||||
const BSOD_DISMISS_DELAY = 500;
|
||||
|
||||
@customElement("ha-retro")
|
||||
export class HaRetro extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _enabled = false;
|
||||
|
||||
public hassSubscribe() {
|
||||
return [
|
||||
subscribeLabFeature(
|
||||
this.hass!.connection,
|
||||
"frontend",
|
||||
"retro",
|
||||
(feature) => {
|
||||
this._enabled = feature.enabled;
|
||||
}
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@state() private _casitaVisible = true;
|
||||
|
||||
@state() private _showBubble = false;
|
||||
|
||||
@state() private _bubbleText = "";
|
||||
|
||||
@state() private _expression: CasitaExpression = "hi";
|
||||
|
||||
@state() private _position: { x: number; y: number } | null = null;
|
||||
|
||||
@state() private _showBsod = false;
|
||||
|
||||
private _clickCount = 0;
|
||||
|
||||
private _clickTimer?: ReturnType<typeof setTimeout>;
|
||||
|
||||
private _dragging = false;
|
||||
|
||||
private _dragStartX = 0;
|
||||
|
||||
private _dragStartY = 0;
|
||||
|
||||
private _dragOffsetX = 0;
|
||||
|
||||
private _dragOffsetY = 0;
|
||||
|
||||
private _dragMoved = false;
|
||||
|
||||
private _bubbleTimer?: ReturnType<typeof setTimeout>;
|
||||
|
||||
private _sleepTimer?: ReturnType<typeof setTimeout>;
|
||||
|
||||
private _boundPointerMove = this._onPointerMove.bind(this);
|
||||
|
||||
private _boundPointerUp = this._onPointerUp.bind(this);
|
||||
|
||||
private _themeApplied = false;
|
||||
|
||||
private _isApplyingTheme = false;
|
||||
|
||||
private _themeObserver?: MutationObserver;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._loadPosition();
|
||||
this._resetSleepTimer();
|
||||
this._applyRetroTheme();
|
||||
this._startThemeObserver();
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._clearTimers();
|
||||
this._stopThemeObserver();
|
||||
this._revertTheme();
|
||||
document.removeEventListener("pointermove", this._boundPointerMove);
|
||||
document.removeEventListener("pointerup", this._boundPointerUp);
|
||||
document.removeEventListener("keydown", this._boundDismissBsod);
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: Map<string, unknown>): void {
|
||||
if (changedProps.has("_enabled")) {
|
||||
if (this._enabled) {
|
||||
this.hass!.loadFragmentTranslation("retro");
|
||||
this._applyRetroTheme();
|
||||
this._startThemeObserver();
|
||||
} else {
|
||||
this._stopThemeObserver();
|
||||
this._revertTheme();
|
||||
}
|
||||
}
|
||||
if (changedProps.has("hass") && this._enabled) {
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
// Re-apply if darkMode changed
|
||||
if (oldHass && oldHass.themes.darkMode !== this.hass!.themes.darkMode) {
|
||||
this._themeApplied = false;
|
||||
this._applyRetroTheme();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _startThemeObserver(): void {
|
||||
if (this._themeObserver) return;
|
||||
this._themeObserver = new MutationObserver(() => {
|
||||
if (this._isApplyingTheme || !this._enabled || !this.hass) return;
|
||||
// Check if our theme was overwritten by the themes mixin
|
||||
const el = document.documentElement as HTMLElement & {
|
||||
__themes?: { cacheKey?: string };
|
||||
};
|
||||
if (!el.__themes?.cacheKey?.startsWith("Retro")) {
|
||||
this._themeApplied = false;
|
||||
this._applyRetroTheme();
|
||||
}
|
||||
});
|
||||
this._themeObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["style"],
|
||||
});
|
||||
}
|
||||
|
||||
private _stopThemeObserver(): void {
|
||||
this._themeObserver?.disconnect();
|
||||
this._themeObserver = undefined;
|
||||
}
|
||||
|
||||
private _applyRetroTheme(): void {
|
||||
if (!this.hass || this._themeApplied) return;
|
||||
|
||||
this._isApplyingTheme = true;
|
||||
|
||||
const themes = {
|
||||
...this.hass.themes,
|
||||
themes: {
|
||||
...this.hass.themes.themes,
|
||||
Retro: RETRO_THEME,
|
||||
},
|
||||
};
|
||||
|
||||
invalidateThemeCache();
|
||||
applyThemesOnElement(
|
||||
document.documentElement,
|
||||
themes,
|
||||
"Retro",
|
||||
{ dark: this.hass.themes.darkMode },
|
||||
true
|
||||
);
|
||||
this._themeApplied = true;
|
||||
this._isApplyingTheme = false;
|
||||
}
|
||||
|
||||
private _revertTheme(): void {
|
||||
if (!this.hass || !this._themeApplied) return;
|
||||
|
||||
this._isApplyingTheme = true;
|
||||
|
||||
invalidateThemeCache();
|
||||
applyThemesOnElement(
|
||||
document.documentElement,
|
||||
this.hass.themes,
|
||||
this.hass.selectedTheme?.theme || "default",
|
||||
{
|
||||
dark: this.hass.themes.darkMode,
|
||||
primaryColor: this.hass.selectedTheme?.primaryColor,
|
||||
accentColor: this.hass.selectedTheme?.accentColor,
|
||||
},
|
||||
true
|
||||
);
|
||||
this._themeApplied = false;
|
||||
this._isApplyingTheme = false;
|
||||
}
|
||||
|
||||
private _loadPosition(): void {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
const pos = JSON.parse(stored);
|
||||
if (typeof pos.x === "number" && typeof pos.y === "number") {
|
||||
this._position = this._clampPosition(pos.x, pos.y);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore invalid stored position
|
||||
}
|
||||
}
|
||||
|
||||
private _savePosition(): void {
|
||||
if (this._position) {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(this._position));
|
||||
} catch {
|
||||
// Ignore storage errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _clampPosition(x: number, y: number): { x: number; y: number } {
|
||||
const size = 80;
|
||||
return {
|
||||
x: Math.max(0, Math.min(window.innerWidth - size, x)),
|
||||
y: Math.max(0, Math.min(window.innerHeight - size, y)),
|
||||
};
|
||||
}
|
||||
|
||||
private _onPointerDown(ev: PointerEvent): void {
|
||||
if (ev.button !== 0 || this._showBsod) return;
|
||||
|
||||
this._dragging = true;
|
||||
this._dragMoved = false;
|
||||
this._dragStartX = ev.clientX;
|
||||
this._dragStartY = ev.clientY;
|
||||
|
||||
const rect = (ev.currentTarget as HTMLElement).getBoundingClientRect();
|
||||
this._dragOffsetX = ev.clientX - rect.left;
|
||||
this._dragOffsetY = ev.clientY - rect.top;
|
||||
|
||||
(ev.currentTarget as HTMLElement).setPointerCapture(ev.pointerId);
|
||||
document.addEventListener("pointermove", this._boundPointerMove);
|
||||
document.addEventListener("pointerup", this._boundPointerUp);
|
||||
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
private _onPointerMove(ev: PointerEvent): void {
|
||||
if (!this._dragging) return;
|
||||
|
||||
const dx = ev.clientX - this._dragStartX;
|
||||
const dy = ev.clientY - this._dragStartY;
|
||||
|
||||
if (!this._dragMoved && Math.hypot(dx, dy) < DRAG_THRESHOLD) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._dragMoved = true;
|
||||
|
||||
const x = ev.clientX - this._dragOffsetX;
|
||||
const y = ev.clientY - this._dragOffsetY;
|
||||
this._position = this._clampPosition(x, y);
|
||||
}
|
||||
|
||||
private _onPointerUp(ev: PointerEvent): void {
|
||||
document.removeEventListener("pointermove", this._boundPointerMove);
|
||||
document.removeEventListener("pointerup", this._boundPointerUp);
|
||||
|
||||
this._dragging = false;
|
||||
|
||||
if (this._dragMoved) {
|
||||
this._savePosition();
|
||||
} else {
|
||||
this._toggleBubble();
|
||||
}
|
||||
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
private _stopPropagation(ev: Event): void {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
private _dismiss(ev: Event): void {
|
||||
ev.stopPropagation();
|
||||
this._casitaVisible = false;
|
||||
this._clearTimers();
|
||||
}
|
||||
|
||||
private _toggleBubble(): void {
|
||||
this._clickCount++;
|
||||
if (this._clickTimer) {
|
||||
clearTimeout(this._clickTimer);
|
||||
}
|
||||
this._clickTimer = setTimeout(() => {
|
||||
this._clickCount = 0;
|
||||
}, BSOD_CLICK_TIMEOUT);
|
||||
|
||||
if (this._clickCount >= BSOD_CLICK_COUNT) {
|
||||
this._clickCount = 0;
|
||||
this._triggerBsod();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._showBubble) {
|
||||
this._hideBubble();
|
||||
} else {
|
||||
this._showTip();
|
||||
}
|
||||
}
|
||||
|
||||
private _boundDismissBsod = this._dismissBsodOnKey.bind(this);
|
||||
|
||||
private _bsodReadyToDismiss = false;
|
||||
|
||||
private _triggerBsod(): void {
|
||||
this._hideBubble();
|
||||
this._showBsod = true;
|
||||
this._bsodReadyToDismiss = false;
|
||||
this._expression = "error";
|
||||
// Delay enabling dismiss so the rapid clicks that triggered the BSOD don't immediately close it
|
||||
setTimeout(() => {
|
||||
this._bsodReadyToDismiss = true;
|
||||
document.addEventListener("keydown", this._boundDismissBsod);
|
||||
}, BSOD_DISMISS_DELAY);
|
||||
}
|
||||
|
||||
private _dismissBsod(): void {
|
||||
if (!this._bsodReadyToDismiss) return;
|
||||
this._showBsod = false;
|
||||
this._expression = "hi";
|
||||
this._resetSleepTimer();
|
||||
document.removeEventListener("keydown", this._boundDismissBsod);
|
||||
}
|
||||
|
||||
private _dismissBsodOnKey(): void {
|
||||
this._dismissBsod();
|
||||
}
|
||||
|
||||
private _showTip(): void {
|
||||
const tipIndex = Math.floor(Math.random() * TIP_COUNT) + 1;
|
||||
this._bubbleText = this.hass!.localize(
|
||||
`ui.panel.retro.tip_${tipIndex}` as LocalizeKeys
|
||||
);
|
||||
this._showBubble = true;
|
||||
this._expression = "ok-nabu";
|
||||
this._resetSleepTimer();
|
||||
|
||||
if (this._bubbleTimer) {
|
||||
clearTimeout(this._bubbleTimer);
|
||||
}
|
||||
this._bubbleTimer = setTimeout(() => {
|
||||
this._hideBubble();
|
||||
}, BUBBLE_TIMEOUT);
|
||||
}
|
||||
|
||||
private _hideBubble(): void {
|
||||
this._showBubble = false;
|
||||
this._expression = "hi";
|
||||
this._resetSleepTimer();
|
||||
|
||||
if (this._bubbleTimer) {
|
||||
clearTimeout(this._bubbleTimer);
|
||||
this._bubbleTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _closeBubble(ev: Event): void {
|
||||
ev.stopPropagation();
|
||||
this._hideBubble();
|
||||
}
|
||||
|
||||
private _resetSleepTimer(): void {
|
||||
if (this._sleepTimer) {
|
||||
clearTimeout(this._sleepTimer);
|
||||
}
|
||||
this._sleepTimer = setTimeout(() => {
|
||||
if (!this._showBubble) {
|
||||
this._expression = "sleep";
|
||||
}
|
||||
}, SLEEP_TIMEOUT);
|
||||
}
|
||||
|
||||
private _clearTimers(): void {
|
||||
if (this._bubbleTimer) {
|
||||
clearTimeout(this._bubbleTimer);
|
||||
this._bubbleTimer = undefined;
|
||||
}
|
||||
if (this._sleepTimer) {
|
||||
clearTimeout(this._sleepTimer);
|
||||
this._sleepTimer = undefined;
|
||||
}
|
||||
if (this._clickTimer) {
|
||||
clearTimeout(this._clickTimer);
|
||||
this._clickTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._enabled || !this._casitaVisible) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const size = 80;
|
||||
const posStyle = this._position
|
||||
? `left: ${this._position.x}px; top: ${this._position.y}px;`
|
||||
: `right: 16px; bottom: 16px;`;
|
||||
|
||||
return html`
|
||||
${this._showBsod
|
||||
? html`
|
||||
<div class="bsod" @click=${this._dismissBsod}>
|
||||
<div class="bsod-content">
|
||||
<h1 class="bsod-title">
|
||||
${this.hass!.localize("ui.panel.retro.bsod_title")}
|
||||
</h1>
|
||||
<p>${this.hass!.localize("ui.panel.retro.bsod_error")}</p>
|
||||
<p>
|
||||
* ${this.hass!.localize("ui.panel.retro.bsod_line_1")}<br />
|
||||
* ${this.hass!.localize("ui.panel.retro.bsod_line_2")}
|
||||
</p>
|
||||
<p class="bsod-prompt">
|
||||
${this.hass!.localize("ui.panel.retro.bsod_continue")}
|
||||
<span class="bsod-cursor">_</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<div
|
||||
class="casita-container ${this._dragging ? "dragging" : ""}"
|
||||
style="width: ${size}px; ${posStyle}"
|
||||
aria-hidden="true"
|
||||
@pointerdown=${this._onPointerDown}
|
||||
>
|
||||
${this._showBubble
|
||||
? html`
|
||||
<div class="speech-bubble">
|
||||
<span class="bubble-text">${this._bubbleText}</span>
|
||||
<button
|
||||
class="bubble-close"
|
||||
@pointerdown=${this._stopPropagation}
|
||||
@click=${this._closeBubble}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
<button
|
||||
class="bubble-dismiss"
|
||||
@pointerdown=${this._stopPropagation}
|
||||
@click=${this._dismiss}
|
||||
>
|
||||
${this.hass!.localize("ui.panel.retro.dismiss")}
|
||||
</button>
|
||||
<div class="bubble-arrow"></div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<img
|
||||
class="casita-image"
|
||||
src="/static/images/voice-assistant/${this._expression}.png"
|
||||
alt="Casita"
|
||||
draggable="false"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static readonly styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.casita-container {
|
||||
position: fixed;
|
||||
pointer-events: auto;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.3));
|
||||
}
|
||||
|
||||
.casita-container.dragging {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.casita-image {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
animation: bob 3s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dragging .casita-image {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.speech-bubble {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 8px);
|
||||
right: 0;
|
||||
background: #ffffe1;
|
||||
color: #000000;
|
||||
border-radius: 12px;
|
||||
border: 2px solid #000000;
|
||||
padding: 12px 28px 12px 12px;
|
||||
font-family: Tahoma, "MS Sans Serif", Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
width: 300px;
|
||||
box-sizing: border-box;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||
animation: bubble-in 200ms ease-out;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.bubble-close {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #000000;
|
||||
font-size: 14px;
|
||||
padding: 2px 6px;
|
||||
line-height: 1;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.bubble-close:hover {
|
||||
background: #e0e0c0;
|
||||
}
|
||||
|
||||
.bubble-dismiss {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #808080;
|
||||
font-family: Tahoma, "MS Sans Serif", Arial, sans-serif;
|
||||
font-size: 12px;
|
||||
padding: 0;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.bubble-dismiss:hover {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.bubble-arrow {
|
||||
position: absolute;
|
||||
bottom: -8px;
|
||||
right: 32px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 8px solid transparent;
|
||||
border-right: 8px solid transparent;
|
||||
border-top: 8px solid #ffffe1;
|
||||
}
|
||||
|
||||
@keyframes bob {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bubble-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.bsod {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #0000aa;
|
||||
color: #ffffff;
|
||||
font-family: "Lucida Console", "Courier New", monospace;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
animation: bsod-in 100ms ease-out;
|
||||
}
|
||||
|
||||
.bsod-content {
|
||||
max-width: 700px;
|
||||
padding: 32px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.bsod-title {
|
||||
display: inline-block;
|
||||
background: #aaaaaa;
|
||||
color: #0000aa;
|
||||
padding: 2px 12px;
|
||||
font-size: 18px;
|
||||
font-weight: normal;
|
||||
margin: 0 0 24px;
|
||||
}
|
||||
|
||||
.bsod-content p {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.bsod-prompt {
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.bsod-cursor {
|
||||
animation: blink 1s step-end infinite;
|
||||
}
|
||||
|
||||
@keyframes bsod-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.casita-image {
|
||||
animation: none;
|
||||
}
|
||||
.speech-bubble {
|
||||
animation: none;
|
||||
}
|
||||
.bsod {
|
||||
animation: none;
|
||||
}
|
||||
.bsod-cursor {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-retro": HaRetro;
|
||||
}
|
||||
}
|
||||
472
src/components/ha-selector/ha-selector-numeric-threshold.ts
Normal file
472
src/components/ha-selector/ha-selector-numeric-threshold.ts
Normal file
@@ -0,0 +1,472 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import {
|
||||
mdiArrowCollapseVertical,
|
||||
mdiArrowExpandVertical,
|
||||
mdiGreaterThan,
|
||||
mdiLessThan,
|
||||
} from "@mdi/js";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { NumericThresholdSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-button-toggle-group";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-select";
|
||||
import "./ha-selector";
|
||||
|
||||
type ThresholdType = "above" | "below" | "between" | "outside";
|
||||
|
||||
interface ThresholdValueEntry {
|
||||
active_choice?: string;
|
||||
number?: number;
|
||||
entity?: string;
|
||||
unit_of_measurement?: string;
|
||||
}
|
||||
|
||||
interface NumericThresholdValue {
|
||||
type: ThresholdType;
|
||||
value?: ThresholdValueEntry;
|
||||
value_min?: ThresholdValueEntry;
|
||||
value_max?: ThresholdValueEntry;
|
||||
}
|
||||
|
||||
@customElement("ha-selector-numeric_threshold")
|
||||
export class HaNumericThresholdSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: NumericThresholdSelector;
|
||||
|
||||
@property({ attribute: false }) public value?: NumericThresholdValue;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@state() private _type?: ThresholdType;
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
if (changedProperties.has("value")) {
|
||||
this._type = this.value?.type || "above";
|
||||
}
|
||||
}
|
||||
|
||||
private _getUnitOptions() {
|
||||
return this.selector.numeric_threshold?.unit_of_measurement;
|
||||
}
|
||||
|
||||
private _getEntityFilter() {
|
||||
const baseFilter = this.selector.numeric_threshold?.entity;
|
||||
const configuredUnits =
|
||||
this.selector.numeric_threshold?.unit_of_measurement;
|
||||
|
||||
if (!configuredUnits) {
|
||||
return baseFilter;
|
||||
}
|
||||
|
||||
if (Array.isArray(baseFilter)) {
|
||||
return baseFilter.map((f) => ({
|
||||
...f,
|
||||
unit_of_measurement: configuredUnits,
|
||||
}));
|
||||
}
|
||||
|
||||
if (baseFilter) {
|
||||
return { ...baseFilter, unit_of_measurement: configuredUnits };
|
||||
}
|
||||
|
||||
return { unit_of_measurement: configuredUnits };
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const type = this._type || "above";
|
||||
const showSingleValue = type === "above" || type === "below";
|
||||
const showRangeValues = type === "between" || type === "outside";
|
||||
const unitOptions = this._getUnitOptions();
|
||||
|
||||
const typeOptions = [
|
||||
{
|
||||
value: "above",
|
||||
label: this.hass.localize(
|
||||
"ui.components.selectors.numeric_threshold.above"
|
||||
),
|
||||
iconPath: mdiGreaterThan,
|
||||
},
|
||||
{
|
||||
value: "below",
|
||||
label: this.hass.localize(
|
||||
"ui.components.selectors.numeric_threshold.below"
|
||||
),
|
||||
iconPath: mdiLessThan,
|
||||
},
|
||||
{
|
||||
value: "between",
|
||||
label: this.hass.localize(
|
||||
"ui.components.selectors.numeric_threshold.in_range"
|
||||
),
|
||||
iconPath: mdiArrowCollapseVertical,
|
||||
},
|
||||
{
|
||||
value: "outside",
|
||||
label: this.hass.localize(
|
||||
"ui.components.selectors.numeric_threshold.outside_range"
|
||||
),
|
||||
iconPath: mdiArrowExpandVertical,
|
||||
},
|
||||
];
|
||||
|
||||
const choiceToggleButtons = [
|
||||
{
|
||||
label: this.hass.localize(
|
||||
"ui.components.selectors.numeric_threshold.number"
|
||||
),
|
||||
value: "number",
|
||||
},
|
||||
{
|
||||
label: this.hass.localize(
|
||||
"ui.components.selectors.numeric_threshold.entity"
|
||||
),
|
||||
value: "entity",
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<div class="container">
|
||||
${this.label
|
||||
? html`<label>${this.label}${this.required ? "*" : ""}</label>`
|
||||
: nothing}
|
||||
<div class="inputs">
|
||||
<ha-select
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.selectors.numeric_threshold.type"
|
||||
)}
|
||||
.value=${type}
|
||||
.options=${typeOptions}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._typeChanged}
|
||||
></ha-select>
|
||||
|
||||
${showSingleValue
|
||||
? this._renderValueRow(
|
||||
this.hass.localize(
|
||||
type === "above"
|
||||
? "ui.components.selectors.numeric_threshold.above"
|
||||
: "ui.components.selectors.numeric_threshold.below"
|
||||
),
|
||||
this.value?.value,
|
||||
this._valueChanged,
|
||||
this._valueChoiceChanged,
|
||||
this._unitChanged,
|
||||
unitOptions,
|
||||
choiceToggleButtons
|
||||
)
|
||||
: nothing}
|
||||
${showRangeValues
|
||||
? html`
|
||||
${this._renderValueRow(
|
||||
this.hass.localize(
|
||||
"ui.components.selectors.numeric_threshold.from"
|
||||
),
|
||||
this.value?.value_min,
|
||||
this._valueMinChanged,
|
||||
this._valueMinChoiceChanged,
|
||||
this._unitMinChanged,
|
||||
unitOptions,
|
||||
choiceToggleButtons
|
||||
)}
|
||||
${this._renderValueRow(
|
||||
this.hass.localize(
|
||||
"ui.components.selectors.numeric_threshold.to"
|
||||
),
|
||||
this.value?.value_max,
|
||||
this._valueMaxChanged,
|
||||
this._valueMaxChoiceChanged,
|
||||
this._unitMaxChanged,
|
||||
unitOptions,
|
||||
choiceToggleButtons
|
||||
)}
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
${this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderUnitSelect(
|
||||
entry: ThresholdValueEntry | undefined,
|
||||
handler: (ev: CustomEvent) => void,
|
||||
unitOptions: readonly string[]
|
||||
) {
|
||||
if (unitOptions.length <= 1) {
|
||||
return nothing;
|
||||
}
|
||||
const mappedUnitOptions = unitOptions.map((unit) => ({
|
||||
value: unit,
|
||||
label: unit,
|
||||
}));
|
||||
const unitLabel = this.hass.localize(
|
||||
"ui.components.selectors.numeric_threshold.unit"
|
||||
);
|
||||
return html`
|
||||
<ha-select
|
||||
class="unit-selector"
|
||||
.label=${unitLabel}
|
||||
.value=${entry?.unit_of_measurement || unitOptions[0]}
|
||||
.options=${mappedUnitOptions}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${handler}
|
||||
></ha-select>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderValueRow(
|
||||
rowLabel: string,
|
||||
entry: ThresholdValueEntry | undefined,
|
||||
onValueChanged: (ev: CustomEvent) => void,
|
||||
onChoiceChanged: (ev: CustomEvent) => void,
|
||||
onUnitChanged: (ev: CustomEvent) => void,
|
||||
unitOptions: readonly string[] | undefined,
|
||||
choiceToggleButtons: { label: string; value: string }[]
|
||||
) {
|
||||
const activeChoice = entry?.active_choice ?? "number";
|
||||
const isEntity = activeChoice === "entity";
|
||||
const showUnit = !isEntity && !!unitOptions && unitOptions.length > 1;
|
||||
const innerValue = isEntity ? entry?.entity : entry?.number;
|
||||
const effectiveUnit = entry?.unit_of_measurement || unitOptions?.[0];
|
||||
const numberSelector = {
|
||||
number: {
|
||||
...this.selector.numeric_threshold?.number,
|
||||
...(effectiveUnit ? { unit_of_measurement: effectiveUnit } : {}),
|
||||
},
|
||||
};
|
||||
const entitySelector = {
|
||||
entity: {
|
||||
filter: this._getEntityFilter(),
|
||||
},
|
||||
};
|
||||
const innerSelector = isEntity ? entitySelector : numberSelector;
|
||||
return html`
|
||||
<div class="value-row">
|
||||
<div class="value-header">
|
||||
<span class="value-label"
|
||||
>${rowLabel}${this.required ? "*" : ""}</span
|
||||
>
|
||||
<ha-button-toggle-group
|
||||
size="small"
|
||||
.buttons=${choiceToggleButtons}
|
||||
.active=${activeChoice}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${onChoiceChanged}
|
||||
></ha-button-toggle-group>
|
||||
</div>
|
||||
<div class="value-inputs">
|
||||
<ha-selector
|
||||
class="value-selector"
|
||||
.hass=${this.hass}
|
||||
.selector=${innerSelector}
|
||||
.value=${innerValue}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
@value-changed=${onValueChanged}
|
||||
></ha-selector>
|
||||
${showUnit
|
||||
? this._renderUnitSelect(entry, onUnitChanged, unitOptions!)
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _typeChanged(ev: CustomEvent) {
|
||||
const value = ev.detail?.value;
|
||||
if (!value || value === this._type) {
|
||||
return;
|
||||
}
|
||||
this._type = value as ThresholdType;
|
||||
|
||||
const newValue: NumericThresholdValue = {
|
||||
type: this._type,
|
||||
};
|
||||
|
||||
// Preserve values when switching between similar types
|
||||
if (this._type === "above" || this._type === "below") {
|
||||
newValue.value = this.value?.value ?? this.value?.value_min;
|
||||
} else if (this._type === "between" || this._type === "outside") {
|
||||
newValue.value_min = this.value?.value_min ?? this.value?.value;
|
||||
newValue.value_max = this.value?.value_max;
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
}
|
||||
|
||||
private _choiceChanged(
|
||||
field: "value" | "value_min" | "value_max",
|
||||
ev: CustomEvent
|
||||
) {
|
||||
ev.stopPropagation();
|
||||
const choice = ev.detail?.value as string;
|
||||
const defaultUnit = this._getUnitOptions()?.[0];
|
||||
const entry: ThresholdValueEntry = {
|
||||
...this.value?.[field],
|
||||
active_choice: choice,
|
||||
};
|
||||
if (choice !== "entity" && !entry.unit_of_measurement && defaultUnit) {
|
||||
entry.unit_of_measurement = defaultUnit;
|
||||
}
|
||||
const defaultType = field === "value" ? "above" : "between";
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.value,
|
||||
type: this._type || defaultType,
|
||||
[field]: entry,
|
||||
...(field === "value"
|
||||
? { value_min: undefined, value_max: undefined }
|
||||
: { value: undefined }),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _valueChoiceChanged = (ev: CustomEvent) =>
|
||||
this._choiceChanged("value", ev);
|
||||
|
||||
private _valueMinChoiceChanged = (ev: CustomEvent) =>
|
||||
this._choiceChanged("value_min", ev);
|
||||
|
||||
private _valueMaxChoiceChanged = (ev: CustomEvent) =>
|
||||
this._choiceChanged("value_max", ev);
|
||||
|
||||
// Called when the inner number/entity selector value changes
|
||||
private _entryChanged(
|
||||
field: "value" | "value_min" | "value_max",
|
||||
ev: CustomEvent
|
||||
) {
|
||||
ev.stopPropagation();
|
||||
const activeChoice = this.value?.[field]?.active_choice ?? "number";
|
||||
const defaultUnit = this._getUnitOptions()?.[0];
|
||||
const entry: ThresholdValueEntry = {
|
||||
...this.value?.[field],
|
||||
active_choice: activeChoice,
|
||||
[activeChoice]: ev.detail.value,
|
||||
};
|
||||
if (
|
||||
activeChoice !== "entity" &&
|
||||
!entry.unit_of_measurement &&
|
||||
defaultUnit
|
||||
) {
|
||||
entry.unit_of_measurement = defaultUnit;
|
||||
}
|
||||
const defaultType = field === "value" ? "above" : "between";
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.value,
|
||||
type: this._type || defaultType,
|
||||
[field]: entry,
|
||||
...(field === "value"
|
||||
? { value_min: undefined, value_max: undefined }
|
||||
: { value: undefined }),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _valueChanged = (ev: CustomEvent) => this._entryChanged("value", ev);
|
||||
|
||||
private _valueMinChanged = (ev: CustomEvent) =>
|
||||
this._entryChanged("value_min", ev);
|
||||
|
||||
private _valueMaxChanged = (ev: CustomEvent) =>
|
||||
this._entryChanged("value_max", ev);
|
||||
|
||||
private _unitFieldChanged(
|
||||
field: "value" | "value_min" | "value_max",
|
||||
ev: CustomEvent
|
||||
) {
|
||||
const unit = ev.detail?.value;
|
||||
if (unit === this.value?.[field]?.unit_of_measurement) return;
|
||||
const activeChoice = this.value?.[field]?.active_choice ?? "number";
|
||||
const defaultType = field === "value" ? "above" : "between";
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.value,
|
||||
type: this._type || defaultType,
|
||||
[field]: {
|
||||
...this.value?.[field],
|
||||
active_choice: activeChoice,
|
||||
unit_of_measurement: unit || undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _unitChanged = (ev: CustomEvent) =>
|
||||
this._unitFieldChanged("value", ev);
|
||||
|
||||
private _unitMinChanged = (ev: CustomEvent) =>
|
||||
this._unitFieldChanged("value_min", ev);
|
||||
|
||||
private _unitMaxChanged = (ev: CustomEvent) =>
|
||||
this._unitFieldChanged("value_max", ev);
|
||||
|
||||
static styles = css`
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
margin-bottom: var(--ha-space-1);
|
||||
}
|
||||
|
||||
.inputs,
|
||||
.value-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.value-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.value-label {
|
||||
font-size: var(--ha-font-size-s);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.value-inputs {
|
||||
display: flex;
|
||||
gap: var(--ha-space-2);
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.value-selector {
|
||||
flex: 1;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.unit-selector {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
ha-select {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-numeric_threshold": HaNumericThresholdSelector;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
import { mdiEye, mdiEyeOff } from "@mdi/js";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { StringSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-multi-textfield";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../ha-textarea";
|
||||
import "../ha-textfield";
|
||||
import "../input/ha-input";
|
||||
import "../input/ha-input-multi";
|
||||
|
||||
@customElement("ha-selector-text")
|
||||
export class HaTextSelector extends LitElement {
|
||||
@@ -30,9 +28,7 @@ export class HaTextSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@state() private _unmaskedPassword = false;
|
||||
|
||||
@query("ha-textfield, ha-textarea") private _input?: HTMLInputElement;
|
||||
@query("ha-input, ha-textarea") private _input?: HTMLInputElement;
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
@@ -49,8 +45,7 @@ export class HaTextSelector extends LitElement {
|
||||
protected render() {
|
||||
if (this.selector.text?.multiple) {
|
||||
return html`
|
||||
<ha-multi-textfield
|
||||
.hass=${this.hass}
|
||||
<ha-input-multi
|
||||
.value=${ensureArray(this.value ?? [])}
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.label}
|
||||
@@ -61,7 +56,7 @@ export class HaTextSelector extends LitElement {
|
||||
.autocomplete=${this.selector.text?.autocomplete}
|
||||
@value-changed=${this._handleChange}
|
||||
>
|
||||
</ha-multi-textfield>
|
||||
</ha-input-multi>
|
||||
`;
|
||||
}
|
||||
if (this.selector.text?.multiline) {
|
||||
@@ -81,45 +76,34 @@ export class HaTextSelector extends LitElement {
|
||||
autogrow
|
||||
></ha-textarea>`;
|
||||
}
|
||||
return html`<ha-textfield
|
||||
.name=${this.name}
|
||||
.value=${this.value || ""}
|
||||
.placeholder=${this.placeholder || ""}
|
||||
.helper=${this.helper}
|
||||
helperPersistent
|
||||
.disabled=${this.disabled}
|
||||
.type=${this._unmaskedPassword ? "text" : this.selector.text?.type}
|
||||
@input=${this._handleChange}
|
||||
@change=${this._handleChange}
|
||||
.label=${this.label || ""}
|
||||
.prefix=${this.selector.text?.prefix}
|
||||
.suffix=${this.selector.text?.type === "password"
|
||||
? // reserve some space for the icon.
|
||||
html`<div style="width: 24px"></div>`
|
||||
: this.selector.text?.suffix}
|
||||
.required=${this.required}
|
||||
.autocomplete=${this.selector.text?.autocomplete}
|
||||
></ha-textfield>
|
||||
${this.selector.text?.type === "password"
|
||||
? html`<ha-icon-button
|
||||
.label=${this.hass?.localize(
|
||||
this._unmaskedPassword
|
||||
? "ui.components.selectors.text.hide_password"
|
||||
: "ui.components.selectors.text.show_password"
|
||||
) || (this._unmaskedPassword ? "Hide password" : "Show password")}
|
||||
@click=${this._toggleUnmaskedPassword}
|
||||
.path=${this._unmaskedPassword ? mdiEyeOff : mdiEye}
|
||||
></ha-icon-button>`
|
||||
: ""}`;
|
||||
return html`<ha-input
|
||||
.name=${this.name}
|
||||
.value=${this.value || ""}
|
||||
.placeholder=${this.placeholder || ""}
|
||||
.hint=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.type=${this.selector.text?.type}
|
||||
@input=${this._handleChange}
|
||||
@change=${this._handleChange}
|
||||
.label=${this.label || ""}
|
||||
.required=${this.required}
|
||||
.autocomplete=${this.selector.text?.autocomplete}
|
||||
.passwordToggle=${this.selector.text?.type === "password"}
|
||||
>
|
||||
${this.selector.text?.prefix
|
||||
? html`<span slot="start">${this.selector.text.prefix}</span>`
|
||||
: nothing}
|
||||
${this.selector.text?.suffix
|
||||
? html`<span slot="end">${this.selector.text.suffix}</span>`
|
||||
: nothing}
|
||||
</ha-input>`;
|
||||
}
|
||||
|
||||
private _toggleUnmaskedPassword(): void {
|
||||
this._unmaskedPassword = !this._unmaskedPassword;
|
||||
}
|
||||
|
||||
private _handleChange(ev) {
|
||||
private _handleChange(ev: ValueChangedEvent<string> | InputEvent) {
|
||||
ev.stopPropagation();
|
||||
let value = ev.detail?.value ?? ev.target.value;
|
||||
let value: string | undefined =
|
||||
(ev as ValueChangedEvent<string>).detail?.value ??
|
||||
(ev.target as HTMLInputElement).value;
|
||||
if (this.value === value) {
|
||||
return;
|
||||
}
|
||||
@@ -139,20 +123,9 @@ export class HaTextSelector extends LitElement {
|
||||
position: relative;
|
||||
}
|
||||
ha-textarea,
|
||||
ha-textfield {
|
||||
ha-input {
|
||||
width: 100%;
|
||||
}
|
||||
ha-icon-button {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 8px;
|
||||
--ha-icon-button-size: 40px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ const LOAD_ELEMENTS = {
|
||||
language: () => import("./ha-selector-language"),
|
||||
navigation: () => import("./ha-selector-navigation"),
|
||||
number: () => import("./ha-selector-number"),
|
||||
numeric_threshold: () => import("./ha-selector-numeric-threshold"),
|
||||
object: () => import("./ha-selector-object"),
|
||||
qr_code: () => import("./ha-selector-qr-code"),
|
||||
select: () => import("./ha-selector-select"),
|
||||
@@ -61,6 +62,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"),
|
||||
};
|
||||
|
||||
@@ -14,6 +14,8 @@ export class HaSettingsRow extends LitElement {
|
||||
@property({ type: Boolean, attribute: "wrap-heading", reflect: true })
|
||||
public wrapHeading = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public empty = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="prefix-wrap">
|
||||
@@ -27,25 +29,30 @@ export class HaSettingsRow extends LitElement {
|
||||
<div class="secondary"><slot name="description"></slot></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content"><slot></slot></div>
|
||||
<div class="content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
padding: 0 16px;
|
||||
padding: 0 var(--ha-space-4);
|
||||
align-content: normal;
|
||||
align-self: auto;
|
||||
align-items: center;
|
||||
}
|
||||
.body {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
padding-top: var(--settings-row-body-padding-top, var(--ha-space-2));
|
||||
padding-bottom: var(
|
||||
--settings-row-body-padding-bottom,
|
||||
var(--ha-space-2)
|
||||
);
|
||||
padding-left: 0;
|
||||
padding-inline-start: 0;
|
||||
padding-right: 16px;
|
||||
padding-inline-end: 16px;
|
||||
padding-right: var(--ha-space-4);
|
||||
padding-inline-end: var(--ha-space-4);
|
||||
overflow: hidden;
|
||||
display: var(--layout-vertical_-_display, flex);
|
||||
flex-direction: var(--layout-vertical_-_flex-direction, column);
|
||||
@@ -63,7 +70,7 @@ export class HaSettingsRow extends LitElement {
|
||||
}
|
||||
.body > .secondary {
|
||||
display: block;
|
||||
padding-top: 4px;
|
||||
padding-top: var(--ha-space-1);
|
||||
font-family: var(
|
||||
--mdc-typography-body2-font-family,
|
||||
var(--mdc-typography-font-family, var(--ha-font-family-body))
|
||||
@@ -90,7 +97,10 @@ export class HaSettingsRow extends LitElement {
|
||||
justify-content: flex-end;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 16px 0;
|
||||
padding: var(--settings-row-content-padding-block, var(--ha-space-4)) 0;
|
||||
}
|
||||
:host([empty]) .content {
|
||||
display: none;
|
||||
}
|
||||
.content ::slotted(*) {
|
||||
width: var(--settings-row-content-width);
|
||||
@@ -99,16 +109,16 @@ export class HaSettingsRow extends LitElement {
|
||||
align-items: normal;
|
||||
flex-direction: column;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
padding-bottom: 8px;
|
||||
padding-bottom: var(--ha-space-2);
|
||||
}
|
||||
::slotted(ha-switch) {
|
||||
padding: 16px 0;
|
||||
padding: var(--settings-row-switch-padding-block, var(--ha-space-4)) 0;
|
||||
}
|
||||
.secondary {
|
||||
white-space: normal;
|
||||
}
|
||||
.prefix-wrap {
|
||||
flex: 1;
|
||||
flex: var(--settings-row-prefix-flex, 1);
|
||||
display: var(--settings-row-prefix-display);
|
||||
}
|
||||
:host([narrow]) .prefix-wrap {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiContentCopy, mdiEye, mdiEyeOff } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { copyToClipboard } from "../../common/util/copy-clipboard";
|
||||
import { localizeContext } from "../../data/context";
|
||||
import { showToast } from "../../util/toast";
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiDeleteOutline, mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-button";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-input-helper-text";
|
||||
import "./ha-sortable";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
@customElement("ha-multi-textfield")
|
||||
class HaMultiTextField extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { localizeContext } from "../../data/context";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import "../ha-button";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-sortable";
|
||||
import "./ha-input";
|
||||
import type { HaInput, InputType } from "./ha-input";
|
||||
|
||||
@customElement("ha-input-multi")
|
||||
class HaInputMulti extends LitElement {
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
@@ -25,7 +24,7 @@ class HaMultiTextField extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public helper?: string;
|
||||
|
||||
@property({ attribute: false }) public inputType?: string;
|
||||
@property({ attribute: false }) public inputType?: InputType;
|
||||
|
||||
@property({ attribute: false }) public inputSuffix?: string;
|
||||
|
||||
@@ -47,6 +46,10 @@ class HaMultiTextField extends LitElement {
|
||||
@property({ type: Boolean, attribute: "update-on-blur" })
|
||||
public updateOnBlur = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: localizeContext, subscribe: true })
|
||||
private localize?: ContextType<typeof localizeContext>;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-sortable
|
||||
@@ -63,9 +66,7 @@ class HaMultiTextField extends LitElement {
|
||||
const indexSuffix = `${this.itemIndex ? ` ${index + 1}` : ""}`;
|
||||
return html`
|
||||
<div class="layout horizontal center-center row">
|
||||
<ha-textfield
|
||||
.suffix=${this.inputSuffix}
|
||||
.prefix=${this.inputPrefix}
|
||||
<ha-input
|
||||
.type=${this.inputType}
|
||||
.autocomplete=${this.autocomplete}
|
||||
.disabled=${this.disabled}
|
||||
@@ -78,13 +79,20 @@ class HaMultiTextField extends LitElement {
|
||||
@input=${this._editItem}
|
||||
@change=${this._editItem}
|
||||
@keydown=${this._keyDown}
|
||||
></ha-textfield>
|
||||
>
|
||||
${this.inputPrefix
|
||||
? html`<span slot="start">${this.inputPrefix}</span>`
|
||||
: nothing}
|
||||
${this.inputSuffix
|
||||
? html`<span slot="end">${this.inputSuffix}</span>`
|
||||
: nothing}
|
||||
</ha-input>
|
||||
<ha-icon-button
|
||||
.disabled=${this.disabled}
|
||||
.index=${index}
|
||||
slot="navigationIcon"
|
||||
.label=${this.removeLabel ??
|
||||
this.hass?.localize("ui.common.remove") ??
|
||||
this.localize?.("ui.common.remove") ??
|
||||
"Remove"}
|
||||
@click=${this._removeItem}
|
||||
.path=${mdiDeleteOutline}
|
||||
@@ -112,10 +120,10 @@ class HaMultiTextField extends LitElement {
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.addLabel ??
|
||||
(this.label
|
||||
? this.hass?.localize("ui.components.multi-textfield.add_item", {
|
||||
? this.localize?.("ui.components.multi-textfield.add_item", {
|
||||
item: this.label,
|
||||
})
|
||||
: this.hass?.localize("ui.common.add")) ??
|
||||
: this.localize?.("ui.common.add")) ??
|
||||
"Add"}
|
||||
</ha-button>
|
||||
</div>
|
||||
@@ -138,8 +146,8 @@ class HaMultiTextField extends LitElement {
|
||||
const items = [...this._items, ""];
|
||||
this._fireChanged(items);
|
||||
await this.updateComplete;
|
||||
const field = this.shadowRoot?.querySelector(`ha-textfield[data-last]`) as
|
||||
| HaTextField
|
||||
const field = this.shadowRoot?.querySelector(`ha-input[data-last]`) as
|
||||
| HaInput
|
||||
| undefined;
|
||||
field?.focus();
|
||||
}
|
||||
@@ -191,9 +199,7 @@ class HaMultiTextField extends LitElement {
|
||||
css`
|
||||
.row {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
--ha-input-padding-bottom: 0;
|
||||
}
|
||||
ha-icon-button {
|
||||
display: block;
|
||||
@@ -210,6 +216,6 @@ class HaMultiTextField extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-multi-textfield": HaMultiTextField;
|
||||
"ha-input-multi": HaInputMulti;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ export interface LovelaceBaseSectionConfig {
|
||||
disabled?: boolean;
|
||||
column_span?: number;
|
||||
row_span?: number;
|
||||
background?: LovelaceSectionBackgroundConfig;
|
||||
background?: boolean | LovelaceSectionBackgroundConfig;
|
||||
/**
|
||||
* @deprecated Use heading card instead.
|
||||
*/
|
||||
@@ -34,6 +34,15 @@ export type LovelaceSectionRawConfig =
|
||||
| LovelaceSectionConfig
|
||||
| LovelaceStrategySectionConfig;
|
||||
|
||||
export function resolveSectionBackground(
|
||||
background: boolean | LovelaceSectionBackgroundConfig | undefined
|
||||
): LovelaceSectionBackgroundConfig | undefined {
|
||||
if (typeof background === "boolean") {
|
||||
return background ? {} : undefined;
|
||||
}
|
||||
return background;
|
||||
}
|
||||
|
||||
export function isStrategySection(
|
||||
section: LovelaceSectionRawConfig
|
||||
): section is LovelaceStrategySectionConfig {
|
||||
|
||||
@@ -56,6 +56,7 @@ export type Selector =
|
||||
| MediaSelector
|
||||
| NavigationSelector
|
||||
| NumberSelector
|
||||
| NumericThresholdSelector
|
||||
| ObjectSelector
|
||||
| AssistPipelineSelector
|
||||
| QRCodeSelector
|
||||
@@ -74,6 +75,7 @@ export type Selector =
|
||||
| TTSSelector
|
||||
| TTSVoiceSelector
|
||||
| UiActionSelector
|
||||
| UiClockDateFormatSelector
|
||||
| UiColorSelector
|
||||
| UiStateContentSelector
|
||||
| BackupLocationSelector;
|
||||
@@ -240,8 +242,8 @@ interface EntitySelectorFilter {
|
||||
integration?: string;
|
||||
domain?: string | readonly string[];
|
||||
device_class?: string | readonly string[];
|
||||
unit_of_measurement?: string | readonly string[];
|
||||
supported_features?: number | [number];
|
||||
unit_of_measurement?: string | readonly string[];
|
||||
}
|
||||
|
||||
export interface EntitySelector {
|
||||
@@ -363,6 +365,14 @@ export interface NumberSelector {
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface NumericThresholdSelector {
|
||||
numeric_threshold: {
|
||||
unit_of_measurement?: readonly string[];
|
||||
number?: NumberSelector["number"];
|
||||
entity?: EntitySelectorFilter | readonly EntitySelectorFilter[];
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface ObjectSelectorField {
|
||||
selector: Selector;
|
||||
label?: string;
|
||||
@@ -507,6 +517,10 @@ export interface UiActionSelector {
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface UiClockDateFormatSelector {
|
||||
ui_clock_date_format: {} | null;
|
||||
}
|
||||
|
||||
export interface UiColorExtraOption {
|
||||
value: string;
|
||||
label: string;
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import { createCollection } from "home-assistant-js-websocket";
|
||||
|
||||
export type ThemeVars = Record<string, string>;
|
||||
export interface ThemeVars {
|
||||
// Incomplete
|
||||
"primary-color": string;
|
||||
"text-primary-color": string;
|
||||
"accent-color": string;
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export type Theme = ThemeVars & {
|
||||
modes?: {
|
||||
|
||||
@@ -7,6 +7,22 @@ import { CONTINUOUS_DOMAINS } from "../../data/logbook";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { isNumericEntity } from "../../data/history";
|
||||
|
||||
export const MORE_INFO_VIEWS = [
|
||||
"info",
|
||||
"history",
|
||||
"settings",
|
||||
"related",
|
||||
"add_to",
|
||||
"details",
|
||||
] as const;
|
||||
|
||||
export type MoreInfoView = (typeof MORE_INFO_VIEWS)[number];
|
||||
|
||||
export const isMoreInfoView = (
|
||||
value: string | undefined
|
||||
): value is MoreInfoView =>
|
||||
value !== undefined && (MORE_INFO_VIEWS as readonly string[]).includes(value);
|
||||
|
||||
export const DOMAINS_NO_INFO = ["camera", "configurator"];
|
||||
/**
|
||||
* Entity domains that should be editable *if* they have an id present;
|
||||
|
||||
@@ -22,7 +22,7 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import { cache } from "lit/directives/cache";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { keyed } from "lit/directives/keyed";
|
||||
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||
@@ -72,6 +72,7 @@ import {
|
||||
DOMAINS_WITH_MORE_INFO,
|
||||
EDITABLE_DOMAINS_WITH_ID,
|
||||
EDITABLE_DOMAINS_WITH_UNIQUE_ID,
|
||||
type MoreInfoView,
|
||||
computeShowHistoryComponent,
|
||||
computeShowLogBookComponent,
|
||||
} from "./const";
|
||||
@@ -79,6 +80,7 @@ import "./controls/more-info-default";
|
||||
import type { FavoritesDialogContext } from "./favorites";
|
||||
import { getFavoritesDialogHandler } from "./favorites";
|
||||
import "./ha-more-info-add-to";
|
||||
import "./ha-more-info-details";
|
||||
import "./ha-more-info-history-and-logbook";
|
||||
import "./ha-more-info-info";
|
||||
import "./ha-more-info-settings";
|
||||
@@ -86,16 +88,14 @@ import "./more-info-content";
|
||||
|
||||
export interface MoreInfoDialogParams {
|
||||
entityId: string | null;
|
||||
view?: View;
|
||||
view?: MoreInfoView;
|
||||
/** @deprecated Use `view` instead */
|
||||
tab?: View;
|
||||
tab?: MoreInfoView;
|
||||
large?: boolean;
|
||||
data?: Record<string, any>;
|
||||
parentElement?: LitElement;
|
||||
}
|
||||
|
||||
type View = "info" | "history" | "settings" | "related" | "add_to";
|
||||
|
||||
interface ChildView {
|
||||
viewTag: string;
|
||||
viewTitle?: string;
|
||||
@@ -112,7 +112,7 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_VIEW: View = "info";
|
||||
const DEFAULT_VIEW: MoreInfoView = "info";
|
||||
|
||||
@customElement("ha-more-info-dialog")
|
||||
export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
@@ -134,9 +134,9 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
@state() private _data?: Record<string, any>;
|
||||
|
||||
@state() private _currView: View = DEFAULT_VIEW;
|
||||
@state() private _currView: MoreInfoView = DEFAULT_VIEW;
|
||||
|
||||
@state() private _initialView: View = DEFAULT_VIEW;
|
||||
@state() private _initialView: MoreInfoView = DEFAULT_VIEW;
|
||||
|
||||
@state() private _childView?: ChildView;
|
||||
|
||||
@@ -163,10 +163,15 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const view = params.view || params.tab || DEFAULT_VIEW;
|
||||
|
||||
this._data = params.data;
|
||||
this._currView = params.view || DEFAULT_VIEW;
|
||||
this._initialView = params.view || DEFAULT_VIEW;
|
||||
this._currView = view;
|
||||
this._initialView = view;
|
||||
this._childView = undefined;
|
||||
this._infoEditMode = false;
|
||||
this._detailsYamlMode = false;
|
||||
|
||||
this.large = params.large ?? false;
|
||||
this._fill = false;
|
||||
this._open = true;
|
||||
@@ -253,7 +258,7 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
return entity?.device_id ?? null;
|
||||
}
|
||||
|
||||
private _setView(view: View) {
|
||||
private _setView(view: MoreInfoView) {
|
||||
history.replaceState(
|
||||
{
|
||||
...history.state,
|
||||
@@ -278,6 +283,13 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
this._detailsYamlMode = false;
|
||||
return;
|
||||
}
|
||||
if (
|
||||
this._initialView !== DEFAULT_VIEW &&
|
||||
this._currView === this._initialView
|
||||
) {
|
||||
this._resetInitialView();
|
||||
return;
|
||||
}
|
||||
if (this._initialView !== this._currView) {
|
||||
this._setView(this._initialView);
|
||||
return;
|
||||
@@ -404,7 +416,9 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
this._resetInitialView();
|
||||
break;
|
||||
case "details":
|
||||
this._showDetails();
|
||||
this._setView("details");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -449,15 +463,6 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
this._entry = result.entity_entry;
|
||||
}
|
||||
|
||||
private _showDetails(): void {
|
||||
import("./ha-more-info-details");
|
||||
this._childView = {
|
||||
viewTag: "ha-more-info-details",
|
||||
viewTitle: this.hass.localize("ui.dialogs.more_info_control.details"),
|
||||
viewParams: { entityId: this._entityId },
|
||||
};
|
||||
}
|
||||
|
||||
private async _copyFavorites() {
|
||||
const favoritesContext = this._getFavoritesContext();
|
||||
|
||||
@@ -509,13 +514,8 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
(deviceId && this.hass.devices[deviceId].entry_type) || "device";
|
||||
|
||||
const isDefaultView = this._currView === DEFAULT_VIEW && !this._childView;
|
||||
const isSpecificInitialView =
|
||||
this._initialView !== DEFAULT_VIEW && !this._childView;
|
||||
const showCloseIcon =
|
||||
(isDefaultView &&
|
||||
this._parentEntityIds.length === 0 &&
|
||||
!this._childView) ||
|
||||
(isSpecificInitialView && !this._childView);
|
||||
isDefaultView && this._parentEntityIds.length === 0 && !this._childView;
|
||||
|
||||
const context = stateObj
|
||||
? getEntityContext(
|
||||
@@ -549,7 +549,11 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
const breadcrumb = [areaName, deviceName, entityName].filter(
|
||||
(v): v is string => Boolean(v)
|
||||
);
|
||||
const title = this._childView?.viewTitle || breadcrumb.pop() || entityId;
|
||||
const defaultTitle = breadcrumb.pop() || entityId;
|
||||
const title =
|
||||
this._currView === "details"
|
||||
? this.hass.localize("ui.dialogs.more_info_control.details")
|
||||
: this._childView?.viewTitle || defaultTitle;
|
||||
|
||||
const favoritesContext =
|
||||
this._entry && stateObj
|
||||
@@ -774,26 +778,16 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
`
|
||||
: nothing}
|
||||
`
|
||||
: isSpecificInitialView
|
||||
: this._currView === "details"
|
||||
? html`
|
||||
<ha-dropdown
|
||||
<ha-icon-button
|
||||
slot="headerActionItems"
|
||||
@closed=${stopPropagation}
|
||||
@wa-select=${this._handleMenuAction}
|
||||
placement="bottom-end"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-dropdown-item value="info">
|
||||
<ha-svg-icon slot="icon" .path=${mdiInformationOutline}>
|
||||
</ha-svg-icon>
|
||||
${this.hass.localize("ui.dialogs.more_info_control.info")}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.toggle_yaml_mode"
|
||||
)}
|
||||
.path=${mdiCodeBraces}
|
||||
@click=${this._toggleDetailsYamlMode}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: this._childView?.viewTag === "ha-more-info-details"
|
||||
? html`
|
||||
@@ -828,12 +822,24 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
this._childView
|
||||
? html`
|
||||
<div class="child-view">
|
||||
${dynamicElement(this._childView.viewTag, {
|
||||
hass: this.hass,
|
||||
entry: this._entry,
|
||||
params: this._childView.viewParams,
|
||||
yamlMode: this._detailsYamlMode,
|
||||
})}
|
||||
${this._childView.viewTag ===
|
||||
"ha-more-info-view-voice-assistants"
|
||||
? html`
|
||||
<ha-more-info-view-voice-assistants
|
||||
.hass=${this.hass}
|
||||
.entry=${this._entry!}
|
||||
.params=${this._childView.viewParams}
|
||||
></ha-more-info-view-voice-assistants>
|
||||
`
|
||||
: this._childView.viewTag ===
|
||||
"ha-more-info-view-vacuum-segment-mapping"
|
||||
? html`
|
||||
<ha-more-info-view-vacuum-segment-mapping
|
||||
.hass=${this.hass}
|
||||
.params=${this._childView.viewParams}
|
||||
></ha-more-info-view-vacuum-segment-mapping>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
: this._currView === "info"
|
||||
@@ -879,7 +885,16 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
@add-to-action-selected=${this._goBack}
|
||||
></ha-more-info-add-to>
|
||||
`
|
||||
: nothing
|
||||
: this._currView === "details"
|
||||
? html`
|
||||
<ha-more-info-details
|
||||
.hass=${this.hass}
|
||||
.entry=${this._entry}
|
||||
.params=${{ entityId }}
|
||||
.yamlMode=${this._detailsYamlMode}
|
||||
></ha-more-info-details>
|
||||
`
|
||||
: nothing
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
@@ -898,14 +913,11 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
const previousChildView = changedProps.get("_childView") as
|
||||
| ChildView
|
||||
const previousView = changedProps.get("_currView") as
|
||||
| MoreInfoView
|
||||
| undefined;
|
||||
|
||||
if (
|
||||
previousChildView?.viewTag === "ha-more-info-details" &&
|
||||
this._childView?.viewTag !== "ha-more-info-details"
|
||||
) {
|
||||
if (previousView === "details" && this._currView !== "details") {
|
||||
const dialog =
|
||||
this._dialogElement?.shadowRoot?.querySelector("ha-dialog");
|
||||
if (dialog) {
|
||||
@@ -914,7 +926,6 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
}
|
||||
|
||||
if (changedProps.has("_currView")) {
|
||||
this._childView = undefined;
|
||||
this._infoEditMode = false;
|
||||
this._detailsYamlMode = false;
|
||||
}
|
||||
@@ -935,15 +946,25 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
window.addEventListener("show-dialog", this._disableEscapeKeyClose);
|
||||
}
|
||||
|
||||
private _handleMoreInfoEvent(ev) {
|
||||
private _handleMoreInfoEvent(ev: HASSDomEvent<MoreInfoDialogParams>) {
|
||||
ev.stopPropagation();
|
||||
const entityId = ev.detail.entityId;
|
||||
if (!entityId) {
|
||||
return;
|
||||
}
|
||||
const view = ev.detail.view || ev.detail.tab || DEFAULT_VIEW;
|
||||
if (entityId === this._entityId) {
|
||||
this._infoEditMode = false;
|
||||
this._detailsYamlMode = false;
|
||||
this._setView(view);
|
||||
return;
|
||||
}
|
||||
this._parentEntityIds = [...this._parentEntityIds, this._entityId!];
|
||||
this._entityId = entityId;
|
||||
this._currView = DEFAULT_VIEW;
|
||||
this._currView = view === "details" ? view : DEFAULT_VIEW;
|
||||
this._initialView = view;
|
||||
this._infoEditMode = false;
|
||||
this._detailsYamlMode = false;
|
||||
this._childView = undefined;
|
||||
this._loadEntityRegistryEntry();
|
||||
}
|
||||
@@ -998,12 +1019,6 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
);
|
||||
}
|
||||
|
||||
.child-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
ha-more-info-history-and-logbook {
|
||||
padding: var(--ha-space-2) var(--ha-space-6) var(--ha-space-6)
|
||||
var(--ha-space-6);
|
||||
|
||||
@@ -52,7 +52,6 @@ export class HomeAssistantMain extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-snowflakes .hass=${this.hass} .narrow=${this.narrow}></ha-snowflakes>
|
||||
<ha-retro .hass=${this.hass} .narrow=${this.narrow}></ha-retro>
|
||||
<ha-drawer
|
||||
.type=${sidebarNarrow ? "modal" : ""}
|
||||
.open=${sidebarNarrow ? this._drawerOpen : false}
|
||||
@@ -80,7 +79,6 @@ export class HomeAssistantMain extends LitElement {
|
||||
protected firstUpdated() {
|
||||
import(/* webpackPreload: true */ "../components/ha-sidebar");
|
||||
import("../components/ha-snowflakes");
|
||||
import("../components/ha-retro");
|
||||
|
||||
if (this.hass.auth.external) {
|
||||
this._externalSidebar =
|
||||
|
||||
@@ -16,7 +16,7 @@ class SupervisorAppMetric extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const roundedValue = roundWithOneDecimal(this.value);
|
||||
return html`<ha-settings-row>
|
||||
return html`<ha-settings-row empty>
|
||||
<span slot="heading"> ${this.description} </span>
|
||||
<div slot="description" .title=${this.tooltip ?? ""}>
|
||||
<span class="value"> ${roundedValue} % </span>
|
||||
@@ -60,9 +60,9 @@ class SupervisorAppMetric extends LitElement {
|
||||
}
|
||||
.value {
|
||||
width: 48px;
|
||||
padding-right: 4px;
|
||||
padding-right: var(--ha-space-1);
|
||||
padding-inline-start: initial;
|
||||
padding-inline-end: 4px;
|
||||
padding-inline-end: var(--ha-space-1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -629,7 +629,7 @@ class SupervisorAppInfo extends LitElement {
|
||||
</div>
|
||||
<div>
|
||||
${this.addon.version && this.addon.state === "started"
|
||||
? html`<ha-settings-row ?three-line=${this.narrow}>
|
||||
? html`<ha-settings-row ?three-line=${this.narrow} empty>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.hostname"
|
||||
@@ -1286,7 +1286,7 @@ class SupervisorAppInfo extends LitElement {
|
||||
}
|
||||
ha-card {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
ha-card.warning {
|
||||
background-color: var(--error-color);
|
||||
@@ -1322,21 +1322,18 @@ class SupervisorAppInfo extends LitElement {
|
||||
}
|
||||
.errors {
|
||||
color: var(--error-color);
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
.description {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
img.logo {
|
||||
max-width: 100%;
|
||||
max-height: 60px;
|
||||
margin: 16px 0;
|
||||
margin: var(--ha-space-4) 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
ha-switch {
|
||||
display: flex;
|
||||
}
|
||||
ha-svg-icon.running {
|
||||
color: var(--success-color);
|
||||
}
|
||||
@@ -1383,7 +1380,7 @@ class SupervisorAppInfo extends LitElement {
|
||||
);
|
||||
}
|
||||
.capabilities {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
.card-actions {
|
||||
justify-content: space-between;
|
||||
@@ -1399,13 +1396,17 @@ class SupervisorAppInfo extends LitElement {
|
||||
cursor: pointer;
|
||||
}
|
||||
ha-markdown {
|
||||
padding: 16px;
|
||||
padding: var(--ha-space-4);
|
||||
--markdown-image-background-color: transparent;
|
||||
--markdown-image-border-radius: 0;
|
||||
--markdown-image-min-height: auto;
|
||||
--markdown-image-text-indent: 0;
|
||||
--markdown-image-transition: none;
|
||||
}
|
||||
ha-settings-row,
|
||||
supervisor-app-metric {
|
||||
--settings-row-prefix-flex: 2;
|
||||
}
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
height: 54px;
|
||||
@@ -1418,6 +1419,14 @@ class SupervisorAppInfo extends LitElement {
|
||||
ha-settings-row[three-line] {
|
||||
height: 74px;
|
||||
}
|
||||
.addon-options ha-settings-row {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
--settings-row-body-padding-top: 0;
|
||||
--settings-row-body-padding-bottom: 0;
|
||||
--settings-row-content-padding-block: var(--ha-space-2);
|
||||
--settings-row-switch-padding-block: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.addon-options {
|
||||
max-width: 90%;
|
||||
@@ -1439,7 +1448,7 @@ class SupervisorAppInfo extends LitElement {
|
||||
|
||||
:host > ha-alert {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
|
||||
a {
|
||||
@@ -1447,7 +1456,7 @@ class SupervisorAppInfo extends LitElement {
|
||||
}
|
||||
|
||||
supervisor-app-update-available-card {
|
||||
padding-bottom: 16px;
|
||||
padding-bottom: var(--ha-space-4);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { formatListWithAnds } from "../../../common/string/format-list";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
removeEntityRegistryEntry,
|
||||
updateEntityRegistryEntry,
|
||||
} from "../../../data/entity/entity_registry";
|
||||
import { findRelated } from "../../../data/search";
|
||||
import { fetchIntegrationManifest } from "../../../data/integration";
|
||||
import {
|
||||
showAlertDialog,
|
||||
@@ -27,6 +29,9 @@ import type { HomeAssistant } from "../../../types";
|
||||
import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail";
|
||||
import "./entity-registry-settings-editor";
|
||||
import type { EntityRegistrySettingsEditor } from "./entity-registry-settings-editor";
|
||||
import { computeEntityEntryName } from "../../../common/entity/compute_entity_name";
|
||||
|
||||
const RELATED_ENTITY_DOMAINS = ["automation", "script", "group", "scene"];
|
||||
|
||||
@customElement("entity-registry-settings")
|
||||
export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
@@ -209,11 +214,14 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private async _confirmDeleteEntry(): Promise<void> {
|
||||
const confirmationText = await this._getDeleteConfirmationText();
|
||||
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.confirm_delete"
|
||||
title: this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.confirm_delete_title"
|
||||
),
|
||||
text: confirmationText,
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
destructive: true,
|
||||
@@ -236,6 +244,46 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private async _getDeleteConfirmationText(): Promise<string | TemplateResult> {
|
||||
const mainText = this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.confirm_delete",
|
||||
{ entity_name: computeEntityEntryName(this.entry) }
|
||||
);
|
||||
|
||||
try {
|
||||
const related = await findRelated(
|
||||
this.hass,
|
||||
"entity",
|
||||
this.entry.entity_id
|
||||
);
|
||||
|
||||
const relatedItems = RELATED_ENTITY_DOMAINS.map((domain) => {
|
||||
const count = related[domain]?.length || 0;
|
||||
if (count === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return this.hass.localize(
|
||||
`ui.dialogs.entity_registry.editor.confirm_delete_count.${domain}`,
|
||||
{ count }
|
||||
);
|
||||
}).filter((item): item is string => Boolean(item));
|
||||
|
||||
if (relatedItems.length === 0) {
|
||||
return mainText;
|
||||
}
|
||||
|
||||
return html`${mainText} <br /><br />
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.confirm_delete_related",
|
||||
{
|
||||
items: formatListWithAnds(this.hass.locale, relatedItems),
|
||||
}
|
||||
)}`;
|
||||
} catch (_err) {
|
||||
return mainText;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
||||
@@ -46,14 +46,11 @@ class HaConfigLabs extends SubscribeMixin(LitElement) {
|
||||
const featuresToSort = [...features];
|
||||
|
||||
return featuresToSort.sort((a, b) => {
|
||||
// Place frontend fun features at the bottom
|
||||
const funFeatures = ["winter_mode", "retro"];
|
||||
const aIsFun =
|
||||
a.domain === "frontend" && funFeatures.includes(a.preview_feature);
|
||||
const bIsFun =
|
||||
b.domain === "frontend" && funFeatures.includes(b.preview_feature);
|
||||
if (aIsFun && !bIsFun) return 1;
|
||||
if (bIsFun && !aIsFun) return -1;
|
||||
// Place frontend.winter_mode at the bottom
|
||||
if (a.domain === "frontend" && a.preview_feature === "winter_mode")
|
||||
return 1;
|
||||
if (b.domain === "frontend" && b.preview_feature === "winter_mode")
|
||||
return -1;
|
||||
|
||||
// Sort everything else alphabetically
|
||||
return domainToName(localize, a.domain).localeCompare(
|
||||
|
||||
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);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
|
||||
.needle=${this._config!.needle}
|
||||
.levels=${this._config!.needle ? this._severityLevels() : undefined}
|
||||
></ha-gauge>
|
||||
<div class="name" .title=${name}>${name}</div>
|
||||
<p class="title" .title=${name}>${name}</p>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
@@ -282,10 +282,15 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
ha-card {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
padding: 16px;
|
||||
padding: var(--ha-space-3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -301,16 +306,23 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
ha-gauge {
|
||||
.title {
|
||||
width: 100%;
|
||||
font-size: var(--ha-font-size-l);
|
||||
line-height: var(--ha-line-height-expanded);
|
||||
padding: 0px 0px var(--ha-space-2) 0px;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: none;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.name {
|
||||
text-align: center;
|
||||
line-height: initial;
|
||||
color: var(--primary-text-color);
|
||||
ha-gauge {
|
||||
width: 100%;
|
||||
font-size: var(--ha-font-size-m);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -441,12 +441,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[];
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -3,17 +3,18 @@ import { LitElement, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import {
|
||||
DEFAULT_SECTION_BACKGROUND_OPACITY,
|
||||
resolveSectionBackground,
|
||||
type LovelaceSectionRawConfig,
|
||||
} from "../../../../data/lovelace/config/section";
|
||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
|
||||
interface SettingsData {
|
||||
@@ -95,13 +96,14 @@ export class HuiDialogEditSection extends LitElement {
|
||||
|
||||
render() {
|
||||
const backgroundEnabled = this.config.background !== undefined;
|
||||
const background = resolveSectionBackground(this.config.background);
|
||||
|
||||
const data: SettingsData = {
|
||||
column_span: this.config.column_span || 1,
|
||||
background_enabled: backgroundEnabled,
|
||||
background_color: this.config.background?.color ?? "default",
|
||||
background_color: background?.color ?? "default",
|
||||
background_opacity:
|
||||
this.config.background?.opacity ?? DEFAULT_SECTION_BACKGROUND_OPACITY,
|
||||
background?.opacity ?? DEFAULT_SECTION_BACKGROUND_OPACITY,
|
||||
};
|
||||
|
||||
const schema = this._schema(
|
||||
@@ -146,12 +148,13 @@ export class HuiDialogEditSection extends LitElement {
|
||||
};
|
||||
|
||||
if (newData.background_enabled) {
|
||||
const hasCustomColor =
|
||||
newData.background_color !== undefined &&
|
||||
newData.background_color !== "default";
|
||||
|
||||
newConfig.background = {
|
||||
...(newData.background_color && newData.background_color !== "default"
|
||||
? { color: newData.background_color }
|
||||
: {}),
|
||||
opacity:
|
||||
newData.background_opacity ?? DEFAULT_SECTION_BACKGROUND_OPACITY,
|
||||
...(hasCustomColor ? { color: newData.background_color } : {}),
|
||||
opacity: newData.background_opacity!,
|
||||
};
|
||||
} else {
|
||||
delete newConfig.background;
|
||||
|
||||
@@ -70,6 +70,7 @@ import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../dialogs/generic/show-dialog-box";
|
||||
import { isMoreInfoView } from "../../dialogs/more-info/const";
|
||||
import { showMoreInfoDialog } from "../../dialogs/more-info/show-ha-more-info-dialog";
|
||||
import { showQuickBar } from "../../dialogs/quick-bar/show-dialog-quick-bar";
|
||||
import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
|
||||
@@ -716,11 +717,18 @@ class HUIRoot extends LitElement {
|
||||
this._showVoiceCommandDialog();
|
||||
} else if (searchParams["more-info-entity-id"]) {
|
||||
const entityId = searchParams["more-info-entity-id"];
|
||||
const view = searchParams["more-info-view"];
|
||||
this._clearParam("more-info-entity-id");
|
||||
if (view) {
|
||||
this._clearParam("more-info-view");
|
||||
}
|
||||
// Wait for the next render to ensure the view is fully loaded
|
||||
// because the more info dialog is closed when the url changes
|
||||
afterNextRender(() => {
|
||||
this._showMoreInfoDialog(entityId);
|
||||
showMoreInfoDialog(this, {
|
||||
entityId,
|
||||
view: isMoreInfoView(view) ? view : undefined,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -975,10 +983,6 @@ class HUIRoot extends LitElement {
|
||||
showVoiceCommandDialog(this, this.hass, { pipeline_id: "last_used" });
|
||||
};
|
||||
|
||||
private _showMoreInfoDialog(entityId: string): void {
|
||||
showMoreInfoDialog(this, { entityId });
|
||||
}
|
||||
|
||||
private _enableEditMode = async () => {
|
||||
if (this._yamlMode) {
|
||||
showAlertDialog(this, {
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { css, LitElement, nothing } from "lit";
|
||||
import { css, LitElement, nothing, unsafeCSS } from "lit";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import {
|
||||
DEFAULT_SECTION_BACKGROUND_OPACITY,
|
||||
resolveSectionBackground,
|
||||
type LovelaceSectionBackgroundConfig,
|
||||
} from "../../../data/lovelace/config/section";
|
||||
|
||||
@customElement("hui-section-background")
|
||||
export class HuiSectionBackground extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public background?: LovelaceSectionBackgroundConfig;
|
||||
public background?: boolean | LovelaceSectionBackgroundConfig;
|
||||
|
||||
protected render() {
|
||||
return nothing;
|
||||
@@ -18,14 +19,15 @@ export class HuiSectionBackground extends LitElement {
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||
super.willUpdate(changedProperties);
|
||||
if (changedProperties.has("background") && this.background) {
|
||||
const color = this.background.color
|
||||
? computeCssColor(this.background.color)
|
||||
: "var(--ha-section-background-color, var(--secondary-background-color))";
|
||||
this.style.setProperty("--section-background", color);
|
||||
const opacity =
|
||||
this.background.opacity ?? DEFAULT_SECTION_BACKGROUND_OPACITY;
|
||||
this.style.setProperty("--section-background-opacity", `${opacity}%`);
|
||||
if (changedProperties.has("background")) {
|
||||
const resolved = resolveSectionBackground(this.background);
|
||||
if (resolved) {
|
||||
const color = resolved.color ? computeCssColor(resolved.color) : null;
|
||||
this.style.setProperty("--section-background-color", color);
|
||||
const opacity =
|
||||
resolved.opacity !== undefined ? `${resolved.opacity}%` : null;
|
||||
this.style.setProperty("--section-background-opacity", opacity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +36,14 @@ export class HuiSectionBackground extends LitElement {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
background-color: var(--section-background, none);
|
||||
opacity: var(--section-background-opacity, 100%);
|
||||
background-color: var(
|
||||
--section-background-color,
|
||||
var(--ha-section-background-color, var(--secondary-background-color))
|
||||
);
|
||||
opacity: var(
|
||||
--section-background-opacity,
|
||||
${unsafeCSS(DEFAULT_SECTION_BACKGROUND_OPACITY)}%
|
||||
);
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -30,11 +30,11 @@ import {
|
||||
import type { HuiSection } from "../sections/hui-section";
|
||||
import "../sections/hui-section-background";
|
||||
import type { Lovelace } from "../types";
|
||||
import { computeSectionsBackgroundAlignment } from "./sections-background-alignment";
|
||||
import { generateDefaultSection } from "./default-section";
|
||||
import "./hui-view-footer";
|
||||
import "./hui-view-header";
|
||||
import "./hui-view-sidebar";
|
||||
import { computeSectionsBackgroundAlignment } from "./sections-background-alignment";
|
||||
|
||||
export const DEFAULT_MAX_COLUMNS = 4;
|
||||
|
||||
|
||||
@@ -554,6 +554,18 @@
|
||||
"text": {
|
||||
"show_password": "Show password",
|
||||
"hide_password": "Hide password"
|
||||
},
|
||||
"numeric_threshold": {
|
||||
"type": "Threshold type",
|
||||
"above": "Above",
|
||||
"below": "Below",
|
||||
"in_range": "In range",
|
||||
"outside_range": "Outside range",
|
||||
"unit": "Unit",
|
||||
"number": "Number",
|
||||
"entity": "Entity",
|
||||
"from": "From",
|
||||
"to": "To"
|
||||
}
|
||||
},
|
||||
"logbook": {
|
||||
@@ -1825,7 +1837,15 @@
|
||||
"enabled_restart_confirm": "Restart Home Assistant to finish enabling the entities",
|
||||
"hidden_explanation": "Hidden entities will not be included in auto-populated dashboards or when their area, device or label is referenced. Their history is still tracked and you can still interact with them with actions.",
|
||||
"delete": "Delete",
|
||||
"confirm_delete": "Are you sure you want to delete this entity?",
|
||||
"confirm_delete_title": "Delete entity",
|
||||
"confirm_delete": "Deleting \"{entity_name}\" is an irreversible action. Are you sure you want to continue?",
|
||||
"confirm_delete_related": "This entity is part of {items}. If you delete it, you will need to update those manually.",
|
||||
"confirm_delete_count": {
|
||||
"automation": "{count} {count, plural,\n one {automation}\n other {automations}\n}",
|
||||
"script": "{count} {count, plural,\n one {script}\n other {scripts}\n}",
|
||||
"group": "{count} {count, plural,\n one {group}\n other {groups}\n}",
|
||||
"scene": "{count} {count, plural,\n one {scene}\n other {scenes}\n}"
|
||||
},
|
||||
"update": "Update",
|
||||
"note": "Note: This might not work yet with all integrations.",
|
||||
"use_device_area": "Use device area",
|
||||
@@ -9346,6 +9366,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",
|
||||
@@ -10653,39 +10700,6 @@
|
||||
"add_card": "Add current view as card",
|
||||
"add_card_error": "Unable to add card",
|
||||
"error_no_data": "You need to select some data sources first."
|
||||
},
|
||||
"retro": {
|
||||
"tip_1": "Try turning your house off and on again.",
|
||||
"tip_2": "If your automation doesn't work, just add more YAML.",
|
||||
"tip_3": "Talk to your devices. They won't answer, but it helps.",
|
||||
"tip_4": "The best way to secure your smart home is to go back to candles.",
|
||||
"tip_5": "Rebooting fixes everything. Everything.",
|
||||
"tip_6": "Naming your vacuum 'DJ Roomba' increases cleaning efficiency by 200%.",
|
||||
"tip_7": "Your automations run better when you're not looking.",
|
||||
"tip_8": "Every time you restart Home Assistant, a smart bulb loses its pairing.",
|
||||
"tip_9": "The cloud is just someone else's Raspberry Pi.",
|
||||
"tip_10": "You can automate your coffee machine, but you still have to drink it yourself.",
|
||||
"tip_11": "You can save energy by not having a home.",
|
||||
"tip_12": "Psst... you can drag me anywhere you want!",
|
||||
"tip_13": "Did you know? I never sleep. Well, sometimes I do. Zzz...",
|
||||
"tip_14": "Zigbee, Z-Wave, Wi-Fi, Thread... so many protocols, so little time.",
|
||||
"tip_15": "The sun can trigger your automations. Nature is the best sensor.",
|
||||
"tip_16": "It looks like you're trying to automate your home! Would you like help?",
|
||||
"tip_17": "My previous job was a paperclip. I got promoted.",
|
||||
"tip_18": "I run entirely on YAML and good vibes.",
|
||||
"tip_19": "Somewhere, a smart plug is blinking and nobody knows why.",
|
||||
"tip_20": "Home Assistant runs on a Raspberry Pi. I run on hopes and dreams.",
|
||||
"tip_21": "Behind every great home, there's someone staring at logs at 2am.",
|
||||
"tip_22": "404: Motivation not found. Try again after coffee.",
|
||||
"tip_23": "There are two types of people: those who back up, and those who will.",
|
||||
"tip_24": "My favorite color is #008080. Don't ask me why.",
|
||||
"tip_25": "Automations are just spicy if-then statements.",
|
||||
"dismiss": "Dismiss me",
|
||||
"bsod_title": "Home Assistant",
|
||||
"bsod_error": "A fatal exception 0E has occurred at C0FF:EE15G00D in VXD L1GHT5(01) + 0FF. The current automation will be terminated.",
|
||||
"bsod_line_1": "Don't worry, nothing is actually broken.",
|
||||
"bsod_line_2": "Your automations are still running. Probably.",
|
||||
"bsod_continue": "Press any key or click to continue"
|
||||
}
|
||||
},
|
||||
"tips": {
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import { computeSectionsBackgroundAlignment } from "../../../../src/panels/lovel
|
||||
|
||||
function mockSection(
|
||||
opts: {
|
||||
background?: {};
|
||||
background?: boolean;
|
||||
column_span?: number;
|
||||
hidden?: boolean;
|
||||
} = {}
|
||||
@@ -20,7 +20,7 @@ function mockSection(
|
||||
|
||||
describe("computeSectionsBackgroundAlignment", () => {
|
||||
it("returns empty set for single column layout", () => {
|
||||
const sections = [mockSection(), mockSection({ background: {} })];
|
||||
const sections = [mockSection(), mockSection({ background: true })];
|
||||
const result = computeSectionsBackgroundAlignment(sections, 1);
|
||||
expect(result.size).toBe(0);
|
||||
});
|
||||
@@ -33,15 +33,15 @@ describe("computeSectionsBackgroundAlignment", () => {
|
||||
|
||||
it("returns empty set when all sections have background", () => {
|
||||
const sections = [
|
||||
mockSection({ background: {} }),
|
||||
mockSection({ background: {} }),
|
||||
mockSection({ background: true }),
|
||||
mockSection({ background: true }),
|
||||
];
|
||||
const result = computeSectionsBackgroundAlignment(sections, 2);
|
||||
expect(result.size).toBe(0);
|
||||
});
|
||||
|
||||
it("marks section without background on same row as one with background", () => {
|
||||
const sections = [mockSection(), mockSection({ background: {} })];
|
||||
const sections = [mockSection(), mockSection({ background: true })];
|
||||
const result = computeSectionsBackgroundAlignment(sections, 2);
|
||||
expect(result.has(0)).toBe(true);
|
||||
expect(result.has(1)).toBe(false);
|
||||
@@ -49,7 +49,7 @@ describe("computeSectionsBackgroundAlignment", () => {
|
||||
|
||||
it("does not mark sections on different rows", () => {
|
||||
// Row 1: section 0 (no bg), Row 2: section 1 (bg)
|
||||
const sections = [mockSection(), mockSection({ background: {} })];
|
||||
const sections = [mockSection(), mockSection({ background: true })];
|
||||
const result = computeSectionsBackgroundAlignment(sections, 1);
|
||||
expect(result.size).toBe(0);
|
||||
});
|
||||
@@ -59,7 +59,7 @@ describe("computeSectionsBackgroundAlignment", () => {
|
||||
// section 2 (span 1, no bg) = row 2
|
||||
const sections = [
|
||||
mockSection({ column_span: 2 }),
|
||||
mockSection({ column_span: 2, background: {} }),
|
||||
mockSection({ column_span: 2, background: true }),
|
||||
mockSection(),
|
||||
];
|
||||
const result = computeSectionsBackgroundAlignment(sections, 4);
|
||||
@@ -71,7 +71,7 @@ describe("computeSectionsBackgroundAlignment", () => {
|
||||
it("wraps to new row when column_span exceeds remaining space", () => {
|
||||
// 2 columns: section 0 (span 1, bg) on row 1, section 1 (span 2, no bg) on row 2
|
||||
const sections = [
|
||||
mockSection({ background: {} }),
|
||||
mockSection({ background: true }),
|
||||
mockSection({ column_span: 2 }),
|
||||
];
|
||||
const result = computeSectionsBackgroundAlignment(sections, 2);
|
||||
@@ -81,7 +81,7 @@ describe("computeSectionsBackgroundAlignment", () => {
|
||||
it("skips hidden sections", () => {
|
||||
// section 0 (hidden, bg) should not cause section 1 to need margin
|
||||
const sections = [
|
||||
mockSection({ hidden: true, background: {} }),
|
||||
mockSection({ hidden: true, background: true }),
|
||||
mockSection(),
|
||||
];
|
||||
const result = computeSectionsBackgroundAlignment(sections, 2);
|
||||
@@ -94,7 +94,7 @@ describe("computeSectionsBackgroundAlignment", () => {
|
||||
// Row 2: section 2 (no bg) + section 3 (no bg) -> no margin needed
|
||||
const sections = [
|
||||
mockSection(),
|
||||
mockSection({ background: {} }),
|
||||
mockSection({ background: true }),
|
||||
mockSection(),
|
||||
mockSection(),
|
||||
];
|
||||
@@ -114,7 +114,7 @@ describe("computeSectionsBackgroundAlignment", () => {
|
||||
// section with span 10 in a 2-column layout should be treated as span 2
|
||||
// Row 1: section 0 (span clamped to 2, bg), Row 2: section 1 (no bg)
|
||||
const sections = [
|
||||
mockSection({ column_span: 10, background: {} }),
|
||||
mockSection({ column_span: 10, background: true }),
|
||||
mockSection(),
|
||||
];
|
||||
const result = computeSectionsBackgroundAlignment(sections, 2);
|
||||
@@ -125,7 +125,7 @@ describe("computeSectionsBackgroundAlignment", () => {
|
||||
// 3 columns: section 0 (no bg) + section 1 (bg) + section 2 (no bg)
|
||||
const sections = [
|
||||
mockSection(),
|
||||
mockSection({ background: {} }),
|
||||
mockSection({ background: true }),
|
||||
mockSection(),
|
||||
];
|
||||
const result = computeSectionsBackgroundAlignment(sections, 3);
|
||||
|
||||
177
yarn.lock
177
yarn.lock
@@ -1533,14 +1533,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/ecma402-abstract@npm:3.1.2":
|
||||
version: 3.1.2
|
||||
resolution: "@formatjs/ecma402-abstract@npm:3.1.2"
|
||||
"@formatjs/bigdecimal@npm:0.2.0":
|
||||
version: 0.2.0
|
||||
resolution: "@formatjs/bigdecimal@npm:0.2.0"
|
||||
checksum: 10/bf036a4414537e4292c8585a6b308b8e8fbb6efc876e6221124eb0d3b6c1d3cba4d4b9b9bf7dca0b52095e00af4c98fb147f563abffd743f5f9fbfd6655550ec
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/ecma402-abstract@npm:3.2.0":
|
||||
version: 3.2.0
|
||||
resolution: "@formatjs/ecma402-abstract@npm:3.2.0"
|
||||
dependencies:
|
||||
"@formatjs/bigdecimal": "npm:0.2.0"
|
||||
"@formatjs/fast-memoize": "npm:3.1.1"
|
||||
"@formatjs/intl-localematcher": "npm:0.8.2"
|
||||
decimal.js: "npm:^10.6.0"
|
||||
checksum: 10/0005707da31ac43dfd394659489055bf98a1a5719e838e2812b7efaeb2aebefd9e108e7e9f42c4dfc3a2912af61851f271d3eacfbfd56ee89214bc7bf1988e75
|
||||
checksum: 10/f28647ce8d44207e685dc619425e05aded48c24374c055b8a693adaf471bd9c09ef3f2b33cc8f9ffc875748b30fcc80486272edcb7b0ebaff6e82c3417563384
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1551,53 +1558,53 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/icu-messageformat-parser@npm:3.5.2":
|
||||
version: 3.5.2
|
||||
resolution: "@formatjs/icu-messageformat-parser@npm:3.5.2"
|
||||
"@formatjs/icu-messageformat-parser@npm:3.5.3":
|
||||
version: 3.5.3
|
||||
resolution: "@formatjs/icu-messageformat-parser@npm:3.5.3"
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract": "npm:3.1.2"
|
||||
"@formatjs/icu-skeleton-parser": "npm:2.1.2"
|
||||
checksum: 10/4d47d9793bc4d51583c15b61e5d2e540b8e9a4cda1f5b2c8b292738432ef0b59249ece6fa2d899c040469e85dd45da77fcaeed2d796ae8d7c2d06dadd418de52
|
||||
"@formatjs/ecma402-abstract": "npm:3.2.0"
|
||||
"@formatjs/icu-skeleton-parser": "npm:2.1.3"
|
||||
checksum: 10/a57118cd8cce392078d806bc6a4818006fc4d38c4a4eb18a2424a392094068128472ad2814bf6d670316f8d6adbbefea639fecdefcac67576cd19d9b7f56ece3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/icu-skeleton-parser@npm:2.1.2":
|
||||
version: 2.1.2
|
||||
resolution: "@formatjs/icu-skeleton-parser@npm:2.1.2"
|
||||
"@formatjs/icu-skeleton-parser@npm:2.1.3":
|
||||
version: 2.1.3
|
||||
resolution: "@formatjs/icu-skeleton-parser@npm:2.1.3"
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract": "npm:3.1.2"
|
||||
checksum: 10/a35dbe27ff87c691aa8ee32ed5474561923fe921e09d8d4ea959e85026bbb5c546227358d607cc8c30ce928574fd8c5f0e05772b43e5590324917e0e9bcd8ecb
|
||||
"@formatjs/ecma402-abstract": "npm:3.2.0"
|
||||
checksum: 10/9b0513d07b52a9c8783fe29a7f7beb6ec5a15ca1133c7186addd46eadc096eac2afec87e8d7c9714705f4f1498963807bbea4872295710f30136170ca5ea883e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-datetimeformat@npm:7.2.6":
|
||||
version: 7.2.6
|
||||
resolution: "@formatjs/intl-datetimeformat@npm:7.2.6"
|
||||
"@formatjs/intl-datetimeformat@npm:7.3.1":
|
||||
version: 7.3.1
|
||||
resolution: "@formatjs/intl-datetimeformat@npm:7.3.1"
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract": "npm:3.1.2"
|
||||
"@formatjs/bigdecimal": "npm:0.2.0"
|
||||
"@formatjs/ecma402-abstract": "npm:3.2.0"
|
||||
"@formatjs/intl-localematcher": "npm:0.8.2"
|
||||
decimal.js: "npm:^10.6.0"
|
||||
checksum: 10/df824f0e663f904a6ebd0e6e70faa8853c99219dc70a56c8da86aed346ef603c6325062ec5038a8ece781c7267ad9268267bc6930aecdfece7b715f26e8a7a5a
|
||||
checksum: 10/06a2775fc1a6a60defedbd3e9165286624e6a7b0e1c41b4f0a3771a32cac22e62e28a6a3a8185c7879b5553569b48359411061eb0342ca59810c7c0b7249e578
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-displaynames@npm:7.2.3":
|
||||
version: 7.2.3
|
||||
resolution: "@formatjs/intl-displaynames@npm:7.2.3"
|
||||
"@formatjs/intl-displaynames@npm:7.3.1":
|
||||
version: 7.3.1
|
||||
resolution: "@formatjs/intl-displaynames@npm:7.3.1"
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract": "npm:3.1.2"
|
||||
"@formatjs/ecma402-abstract": "npm:3.2.0"
|
||||
"@formatjs/intl-localematcher": "npm:0.8.2"
|
||||
checksum: 10/37e07754738b3abee6e3a3cc028c0c586932112dd9f6971fff622e133fde6020c0065042b0ad7d0c750d30e3e76e6f7d4e1ff41be8367325ca2cddb8198a3665
|
||||
checksum: 10/026f49c428cea693c8018668f7f867e03d881a52f1a6237dea6c1aff3361f06e7691555f421e2b0bd5bc718d5235f7d8fdefe31a9ec9a77b4d534aed9d206653
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-durationformat@npm:0.10.2":
|
||||
version: 0.10.2
|
||||
resolution: "@formatjs/intl-durationformat@npm:0.10.2"
|
||||
"@formatjs/intl-durationformat@npm:0.10.3":
|
||||
version: 0.10.3
|
||||
resolution: "@formatjs/intl-durationformat@npm:0.10.3"
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract": "npm:3.1.2"
|
||||
"@formatjs/ecma402-abstract": "npm:3.2.0"
|
||||
"@formatjs/intl-localematcher": "npm:0.8.2"
|
||||
checksum: 10/27aced459ed503f92ca3db54bb6f42967a1c8f085d5208a570b1c4acb31424f23f3c634bb35c562ac3a84f08c64cdbf7d4c9a44ba937bbb6a68ece7b084104be
|
||||
checksum: 10/33c945164ee8b6cebcff61e080592a707d364dab2191d755b992d109db3038d5acb39d577160de4ba51d97ad2868243ea2e18f96b7dc52f5222d933edaba5f7f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1608,24 +1615,24 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-listformat@npm:8.2.3":
|
||||
version: 8.2.3
|
||||
resolution: "@formatjs/intl-listformat@npm:8.2.3"
|
||||
"@formatjs/intl-listformat@npm:8.3.1":
|
||||
version: 8.3.1
|
||||
resolution: "@formatjs/intl-listformat@npm:8.3.1"
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract": "npm:3.1.2"
|
||||
"@formatjs/ecma402-abstract": "npm:3.2.0"
|
||||
"@formatjs/intl-localematcher": "npm:0.8.2"
|
||||
checksum: 10/1408a58632fef7c0817531657ecb345a997163ad955d96fa4c5b541a0b018e82e578c92ad53e316decd926027141174a9ed42ac406196235fddd83c2bdb0fc58
|
||||
checksum: 10/62d20411e8c96a8682a4eda336f9c8fdea858f6c181acd8034007b7a65a273b07efcdb3fb0c9a876ef5cd587f35bb1ddf18d36fc6615da8647d24f972bb260af
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-locale@npm:5.2.2":
|
||||
version: 5.2.2
|
||||
resolution: "@formatjs/intl-locale@npm:5.2.2"
|
||||
"@formatjs/intl-locale@npm:5.3.1":
|
||||
version: 5.3.1
|
||||
resolution: "@formatjs/intl-locale@npm:5.3.1"
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract": "npm:3.1.2"
|
||||
"@formatjs/ecma402-abstract": "npm:3.2.0"
|
||||
"@formatjs/intl-getcanonicallocales": "npm:3.2.2"
|
||||
"@formatjs/intl-supportedvaluesof": "npm:2.2.2"
|
||||
checksum: 10/f50228e5b2fc5808a200a236d992ca816f1a6b3c08f903b0bd20abc19d7b90e312719b1783de7f57a1e5fb99cac7b4f31e5e601b664964b16c65f673051033db
|
||||
"@formatjs/intl-supportedvaluesof": "npm:2.3.0"
|
||||
checksum: 10/48baf6a7c1df6b94989dc096638fdd02f204b656e44dd6917c5ecefbdd78f02394eeed53b6266ae1983ba6231efb9aa0fd138596d568abd8b026b49abb9c0c12
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1638,45 +1645,45 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-numberformat@npm:9.2.4":
|
||||
version: 9.2.4
|
||||
resolution: "@formatjs/intl-numberformat@npm:9.2.4"
|
||||
"@formatjs/intl-numberformat@npm:9.3.1":
|
||||
version: 9.3.1
|
||||
resolution: "@formatjs/intl-numberformat@npm:9.3.1"
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract": "npm:3.1.2"
|
||||
"@formatjs/bigdecimal": "npm:0.2.0"
|
||||
"@formatjs/ecma402-abstract": "npm:3.2.0"
|
||||
"@formatjs/intl-localematcher": "npm:0.8.2"
|
||||
decimal.js: "npm:^10.6.0"
|
||||
checksum: 10/bd8dbd2c3b87d0fac7303cf05b52916b57717a02161f8c5e0c8b87e6c6c8628fcd92b422cf33756d21a85f493ac7440c1ab1fecbe9c14b9f3e82876d46654837
|
||||
checksum: 10/331c18762e713b44eaa44ab215e98f1a8d16d698fcd94d9953b7904100ddec9863fbdae59cf54c65ce0c2212d9b9199cbae0dae0b08affa78995d66d3c426c0d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-pluralrules@npm:6.2.4":
|
||||
version: 6.2.4
|
||||
resolution: "@formatjs/intl-pluralrules@npm:6.2.4"
|
||||
"@formatjs/intl-pluralrules@npm:6.3.1":
|
||||
version: 6.3.1
|
||||
resolution: "@formatjs/intl-pluralrules@npm:6.3.1"
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract": "npm:3.1.2"
|
||||
"@formatjs/bigdecimal": "npm:0.2.0"
|
||||
"@formatjs/ecma402-abstract": "npm:3.2.0"
|
||||
"@formatjs/intl-localematcher": "npm:0.8.2"
|
||||
decimal.js: "npm:^10.6.0"
|
||||
checksum: 10/4af89c8846280eab313ada52d1a3c4a707b0fa1bca29311a80686cf315233d19a4a141b79ddc815620eb5a123e3991f2fd9f92299c468d084b1444b445747dbe
|
||||
checksum: 10/4b170e77e835c9df77968e3d8d7a64d84be299178b933d4e2a3817067d4a5d6e4c41624a091a3841d41226569544cb13c684b5727b59324e3a09b4f507c35408
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-relativetimeformat@npm:12.2.4":
|
||||
version: 12.2.4
|
||||
resolution: "@formatjs/intl-relativetimeformat@npm:12.2.4"
|
||||
"@formatjs/intl-relativetimeformat@npm:12.3.1":
|
||||
version: 12.3.1
|
||||
resolution: "@formatjs/intl-relativetimeformat@npm:12.3.1"
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract": "npm:3.1.2"
|
||||
"@formatjs/ecma402-abstract": "npm:3.2.0"
|
||||
"@formatjs/intl-localematcher": "npm:0.8.2"
|
||||
checksum: 10/cd6fd40216ce1f35679d707a7be54761397bac574f7f118fe6fccc46effedf7d035f332d16ea6a11c1cfaf409f8fcc0e5585c80a0a5cb766e10307e663dd072c
|
||||
checksum: 10/787988d9a8cdc40d51bc29a294aa7aedc8272e2e0537426876cd9a51a35dabc9db0ca5ec4731bc75513cf02a724d6ae0a7f35802b5cdcf1e5a3b1230f2577390
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-supportedvaluesof@npm:2.2.2":
|
||||
version: 2.2.2
|
||||
resolution: "@formatjs/intl-supportedvaluesof@npm:2.2.2"
|
||||
"@formatjs/intl-supportedvaluesof@npm:2.3.0":
|
||||
version: 2.3.0
|
||||
resolution: "@formatjs/intl-supportedvaluesof@npm:2.3.0"
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract": "npm:3.1.2"
|
||||
"@formatjs/ecma402-abstract": "npm:3.2.0"
|
||||
"@formatjs/fast-memoize": "npm:3.1.1"
|
||||
checksum: 10/4d833835c76f84d11347e133d638904ce0a8bf821a377545636e7e4f52aa4f153979265cfb90cd39ccb8d5e28c379f123dbc9e2011aa40a456c9a5e7dbcb6010
|
||||
checksum: 10/4d38a6049934b4009e030278a4155fa3c936492cc2b6170fc00f3254842ff9cd53a291b998801e726f4ba6ccc55e09cfa7c027e8de952bbb2ea9dd0e1965da93
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -8861,15 +8868,15 @@ __metadata:
|
||||
"@codemirror/view": "npm:6.40.0"
|
||||
"@date-fns/tz": "npm:1.4.1"
|
||||
"@egjs/hammerjs": "npm:2.0.17"
|
||||
"@formatjs/intl-datetimeformat": "npm:7.2.6"
|
||||
"@formatjs/intl-displaynames": "npm:7.2.3"
|
||||
"@formatjs/intl-durationformat": "npm:0.10.2"
|
||||
"@formatjs/intl-datetimeformat": "npm:7.3.1"
|
||||
"@formatjs/intl-displaynames": "npm:7.3.1"
|
||||
"@formatjs/intl-durationformat": "npm:0.10.3"
|
||||
"@formatjs/intl-getcanonicallocales": "npm:3.2.2"
|
||||
"@formatjs/intl-listformat": "npm:8.2.3"
|
||||
"@formatjs/intl-locale": "npm:5.2.2"
|
||||
"@formatjs/intl-numberformat": "npm:9.2.4"
|
||||
"@formatjs/intl-pluralrules": "npm:6.2.4"
|
||||
"@formatjs/intl-relativetimeformat": "npm:12.2.4"
|
||||
"@formatjs/intl-listformat": "npm:8.3.1"
|
||||
"@formatjs/intl-locale": "npm:5.3.1"
|
||||
"@formatjs/intl-numberformat": "npm:9.3.1"
|
||||
"@formatjs/intl-pluralrules": "npm:6.3.1"
|
||||
"@formatjs/intl-relativetimeformat": "npm:12.3.1"
|
||||
"@fullcalendar/core": "npm:6.1.20"
|
||||
"@fullcalendar/daygrid": "npm:6.1.20"
|
||||
"@fullcalendar/interaction": "npm:6.1.20"
|
||||
@@ -8981,7 +8988,7 @@ __metadata:
|
||||
html-minifier-terser: "npm:7.2.0"
|
||||
husky: "npm:9.1.7"
|
||||
idb-keyval: "npm:6.2.2"
|
||||
intl-messageformat: "npm:11.1.3"
|
||||
intl-messageformat: "npm:11.2.0"
|
||||
js-yaml: "npm:4.1.1"
|
||||
jsdom: "npm:29.0.1"
|
||||
jszip: "npm:3.10.1"
|
||||
@@ -8996,7 +9003,7 @@ __metadata:
|
||||
lodash.template: "npm:4.5.0"
|
||||
luxon: "npm:3.7.2"
|
||||
map-stream: "npm:0.0.7"
|
||||
marked: "npm:17.0.4"
|
||||
marked: "npm:17.0.5"
|
||||
memoize-one: "npm:6.0.0"
|
||||
node-vibrant: "npm:4.0.4"
|
||||
object-hash: "npm:3.0.0"
|
||||
@@ -9386,14 +9393,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"intl-messageformat@npm:11.1.3":
|
||||
version: 11.1.3
|
||||
resolution: "intl-messageformat@npm:11.1.3"
|
||||
"intl-messageformat@npm:11.2.0":
|
||||
version: 11.2.0
|
||||
resolution: "intl-messageformat@npm:11.2.0"
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract": "npm:3.1.2"
|
||||
"@formatjs/ecma402-abstract": "npm:3.2.0"
|
||||
"@formatjs/fast-memoize": "npm:3.1.1"
|
||||
"@formatjs/icu-messageformat-parser": "npm:3.5.2"
|
||||
checksum: 10/db8eca63d9f14c624202fbb579e5b28813430b03792bc6422fda30d7cb6a24ef8bcb60440ea6dce50d3f35d3e697928609c4b7822853fd25713f1c0ad9c55161
|
||||
"@formatjs/icu-messageformat-parser": "npm:3.5.3"
|
||||
checksum: 10/a2aa4929aacb92aa1ae0a21adf40e9c5774ea8d97a3617443212ae7075d45103d49afb85885bd5cf5e5da94c0a5ee38219b55dccca46f21f7fb671e4549c36f2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -10696,12 +10703,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"marked@npm:17.0.4":
|
||||
version: 17.0.4
|
||||
resolution: "marked@npm:17.0.4"
|
||||
"marked@npm:17.0.5":
|
||||
version: 17.0.5
|
||||
resolution: "marked@npm:17.0.5"
|
||||
bin:
|
||||
marked: bin/marked.js
|
||||
checksum: 10/a26c24db0882be6301d3947cee6e9fa70350ab9593cdf0c8c301ce64bb6125db158729fd5c7073106d0f176db6cb04749db19a0752637b071d5ec318d266f928
|
||||
checksum: 10/d8b364c9dc165a31d4809abdc00588ef256af6b52a01a1bd84d79383a8f235f24f71deff32459561ca7c9332f43fd139846d37230c609186b0ba6b0234ffbc0a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user