Compare commits

..

2 Commits

Author SHA1 Message Date
Paul Bottein ac1c192c8b Address back navigation review findings 2026-08-05 11:03:50 +02:00
Paul Bottein 2599f97aae Unify back navigation across subpages 2026-08-04 19:06:55 +02:00
157 changed files with 2368 additions and 4058 deletions
-46
View File
@@ -1,46 +0,0 @@
// Gulp transform that brotli-compresses files, several at a time.
//
// Drop-in replacement for gulp-brotli. zlib already does the work off the main
// thread, but that plugin wraps it in through2, which waits for each file
// before starting the next, so only one compression is ever in flight. The
// compressed bytes are unchanged; only how many run at once differs.
//
// The real ceiling is libuv's threadpool, which zlib runs on. It sizes itself
// from UV_THREADPOOL_SIZE before any JavaScript runs, so it can only be raised
// from the environment, never from inside the build.
import { availableParallelism } from "node:os";
import { buffer as readStream } from "node:stream/consumers";
import { promisify } from "node:util";
import { brotliCompress } from "node:zlib";
import { ParallelTransform } from "./parallel-transform.mjs";
const EXTENSION = ".br";
const compress = promisify(brotliCompress);
/**
* @param {object} [options]
* @param {boolean} [options.skipLarger] Drop files that compression grows.
* @param {object} [options.params] Brotli parameters, passed to zlib as-is.
*/
export default ({ skipLarger = false, params } = {}) =>
new ParallelTransform(availableParallelism(), async (file) => {
if (file.isNull()) {
return file;
}
if (file.isStream()) {
file.contents = await readStream(file.contents);
}
const compressed = await compress(file.contents, { params });
if (skipLarger && compressed.length >= file.contents.length) {
// Dropped rather than passed through, as gulp-brotli did: the
// uncompressed file is already in the output directory.
return undefined;
}
file.contents = compressed;
file.path += EXTENSION;
return file;
});
-8
View File
@@ -311,15 +311,7 @@ module.exports.config = {
return {
name: "e2e-test-app" + nameSuffix(latestBuild),
entry: {
dashboard: path.resolve(
paths.e2eTestApp_dir,
"src/dashboard-entrypoint.ts"
),
main: path.resolve(paths.e2eTestApp_dir, "src/entrypoint.ts"),
onboarding: path.resolve(
paths.e2eTestApp_dir,
"src/onboarding-entrypoint.ts"
),
},
outputPath: outputPath(paths.e2eTestApp_output_root, latestBuild),
publicPath: publicPath(latestBuild),
+1 -1
View File
@@ -2,7 +2,7 @@
import { constants } from "node:zlib";
import gulp from "gulp";
import brotli from "../brotli.mjs";
import brotli from "gulp-brotli";
import paths from "../paths.cjs";
import zopfli from "../zopfli.mjs";
+1 -5
View File
@@ -303,11 +303,7 @@ gulp.task(
)
);
const E2E_TEST_APP_PAGE_ENTRIES = {
"index.html": ["main"],
"dashboard.html": ["dashboard"],
"onboarding.html": ["onboarding"],
};
const E2E_TEST_APP_PAGE_ENTRIES = { "index.html": ["main"] };
gulp.task(
"gen-pages-e2e-test-app-dev",
-64
View File
@@ -1,64 +0,0 @@
// Object-mode transform that keeps several files in flight at once.
//
// through2 and node's Transform both wait for the previous callback before
// handling the next file, which serialises asynchronous work down to one file
// at a time. This keeps `limit` files in flight and applies backpressure beyond
// that. Files are emitted in completion order rather than input order.
import { Transform } from "node:stream";
export class ParallelTransform extends Transform {
#limit;
#handle;
#inFlight = 0;
#resume;
#finish;
constructor(limit, handle) {
super({ objectMode: true, highWaterMark: limit });
this.#limit = limit;
this.#handle = handle;
}
// eslint-disable-next-line @typescript-eslint/naming-convention -- node's Transform API
_transform(file, _encoding, callback) {
this.#inFlight += 1;
this.#handle(file)
.then((result) => {
if (result) {
this.push(result);
}
})
.catch((error) => this.destroy(error))
.finally(() => {
this.#inFlight -= 1;
const resume = this.#resume;
this.#resume = undefined;
resume?.();
if (this.#inFlight === 0) {
const finish = this.#finish;
this.#finish = undefined;
finish?.();
}
});
if (this.#inFlight < this.#limit) {
callback();
} else {
this.#resume = callback;
}
}
// eslint-disable-next-line @typescript-eslint/naming-convention -- node's Transform API
_flush(callback) {
if (this.#inFlight === 0) {
callback();
} else {
this.#finish = callback;
}
}
}
+60 -1
View File
@@ -6,9 +6,9 @@
// instance per worker parallelises it; the output bytes are unchanged.
import { availableParallelism } from "node:os";
import { Transform } from "node:stream";
import { buffer as readStream } from "node:stream/consumers";
import { Worker } from "node:worker_threads";
import { ParallelTransform } from "./parallel-transform.mjs";
const WORKER_URL = new URL("./zopfli-worker.mjs", import.meta.url);
const EXTENSION = ".gz";
@@ -112,6 +112,65 @@ const sharedPool = () => {
return pool;
};
// through2 and node's Transform both wait for the previous callback before
// handling the next file, which would serialise the pool down to one worker.
// This keeps `limit` files in flight and applies backpressure beyond that.
class ParallelTransform extends Transform {
#limit;
#handle;
#inFlight = 0;
#resume;
#finish;
constructor(limit, handle) {
super({ objectMode: true, highWaterMark: limit });
this.#limit = limit;
this.#handle = handle;
}
// eslint-disable-next-line @typescript-eslint/naming-convention -- node's Transform API
_transform(file, _encoding, callback) {
this.#inFlight += 1;
this.#handle(file)
.then((result) => {
if (result) {
this.push(result);
}
})
.catch((error) => this.destroy(error))
.finally(() => {
this.#inFlight -= 1;
const resume = this.#resume;
this.#resume = undefined;
resume?.();
if (this.#inFlight === 0) {
const finish = this.#finish;
this.#finish = undefined;
finish?.();
}
});
if (this.#inFlight < this.#limit) {
callback();
} else {
this.#resume = callback;
}
}
// eslint-disable-next-line @typescript-eslint/naming-convention -- node's Transform API
_flush(callback) {
if (this.#inFlight === 0) {
callback();
} else {
this.#finish = callback;
}
}
}
/**
* @param {object} [options]
* @param {number} [options.threshold] Skip files smaller than this many bytes.
+31 -1
View File
@@ -17,6 +17,9 @@ const rspackConfigPath = fileURLToPath(
new URL("./rspack.config.cjs", import.meta.url)
);
// Applies everywhere, including the files exempted from the history rule below.
const restrictedSyntax = ["LabeledStatement", "WithStatement"];
export default tseslint.config(
js.configs.recommended,
eslintConfigPrettier,
@@ -111,7 +114,16 @@ export default tseslint.config(
"no-bitwise": "error",
"no-console": "error",
"no-restricted-globals": [2, "event"],
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
"no-restricted-syntax": [
"error",
...restrictedSyntax,
{
selector:
"CallExpression[callee.property.name=/^(push|replace)State$/]",
message:
"Use navigate(), updateHistoryState() or replaceCurrentUrl() from common/navigate. History entries carry the app's own bookkeeping, which a raw pushState/replaceState drops.",
},
],
"wc/no-self-class": "off",
// import-x rules
@@ -222,6 +234,24 @@ export default tseslint.config(
],
},
},
{
// These own history entries themselves: the navigation helpers, the dialog
// stack, the boot paths that run before the app has any state to keep, and
// the tests that fabricate entries to simulate a document load.
files: [
"src/common/navigate.ts",
"src/dialogs/make-dialog-manager.ts",
"src/state/url-sync-mixin.ts",
"src/panels/config/automation/add-automation-element-dialog.ts",
"src/entrypoints/core.ts",
"src/onboarding/**/*.ts",
"cast/**/*.ts",
"test/**/*.ts",
],
rules: {
"no-restricted-syntax": ["error", ...restrictedSyntax],
},
},
{
files: ["src/util/recorder-worklet.js"],
languageOptions: {
@@ -1,7 +0,0 @@
import type { LovelaceCardConfig } from "../../../src/data/lovelace/config/card";
import { getCardElementClass } from "../../../src/panels/lovelace/create-element/create-card-element";
export const validateCardConfig = async (config: LovelaceCardConfig) => {
const cardClass = await getCardElementClass(config.type);
new cardClass().setConfig(config);
};
+9 -40
View File
@@ -1,21 +1,15 @@
import { dump } from "js-yaml";
import { load } from "js-yaml";
import type { PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import "../../../src/components/ha-alert";
import type { LovelaceCardConfig } from "../../../src/data/lovelace/config/card";
import "../../../src/panels/lovelace/cards/hui-card";
import type { HuiCard } from "../../../src/panels/lovelace/cards/hui-card";
import type { HomeAssistant } from "../../../src/types";
import { validateCardConfig } from "../common/validate-card-config";
export interface DemoCardConfig<
T extends LovelaceCardConfig = LovelaceCardConfig,
> {
export interface DemoCardConfig {
heading: string;
config: T;
expectConfigError?: boolean;
config: string;
}
@customElement("demo-card")
@@ -29,29 +23,12 @@ class DemoCard extends LitElement {
@state() private _size?: number;
@state() private _configError?: string;
@query("hui-card", false) private _card?: HuiCard;
private _yamlConfig = memoizeOne((config: LovelaceCardConfig) =>
dump([config]).trim()
);
protected async firstUpdated() {
try {
await validateCardConfig(this.config.config);
} catch (err) {
if (this.config.expectConfigError) {
return;
}
this._configError = err instanceof Error ? err.message : String(err);
return;
}
if (this.config.expectConfigError) {
this._configError = `Expected config error for ${this.config.heading}`;
}
}
private _config = memoizeOne((config: string) => {
const c = (load(config) as any)[0];
return c;
});
render() {
return html`
@@ -63,20 +40,15 @@ class DemoCard extends LitElement {
: ""
}
</h2>
${
this._configError
? html`<ha-alert alert-type="error">${this._configError}</ha-alert>`
: nothing
}
<div class="root">
<hui-card
.config=${this.config.config}
.config=${this._config(this.config.config)}
.hass=${this.hass}
@card-updated=${this._cardUpdated}
></hui-card>
${
this.showConfig
? html`<pre>${this._yamlConfig(this.config.config)}</pre>`
? html`<pre>${this.config.config.trim()}</pre>`
: nothing
}
</div>
@@ -109,9 +81,6 @@ class DemoCard extends LitElement {
font-size: 0.5em;
color: var(--primary-text-color);
}
ha-alert {
margin-bottom: 16px;
}
hui-card {
max-width: 400px;
width: 100vw;
+30 -30
View File
@@ -1,8 +1,6 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { AlarmPanelCardConfig } from "../../../../src/panels/lovelace/cards/types";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
@@ -42,50 +40,52 @@ const ENTITIES = [
const CONFIGS = [
{
heading: "Basic Example",
config: {
type: "alarm-panel",
entity: "alarm_control_panel.alarm",
},
config: `
- type: alarm-panel
entity: alarm_control_panel.alarm
`,
},
{
heading: "With Title",
config: {
type: "alarm-panel",
entity: "alarm_control_panel.alarm_armed",
name: "My Alarm",
},
config: `
- type: alarm-panel
entity: alarm_control_panel.alarm_armed
name: My Alarm
`,
},
{
heading: "Code Example",
config: {
type: "alarm-panel",
entity: "alarm_control_panel.alarm_code",
},
config: `
- type: alarm-panel
entity: alarm_control_panel.alarm_code
`,
},
{
heading: "Using only Arm_Home State",
config: {
type: "alarm-panel",
entity: "alarm_control_panel.alarm",
states: ["arm_home"],
},
config: `
- type: alarm-panel
entity: alarm_control_panel.alarm
states:
- arm_home
`,
},
{
heading: "Unavailable",
config: {
type: "alarm-panel",
entity: "alarm_control_panel.unavailable",
states: ["arm_home"],
},
config: `
- type: alarm-panel
entity: alarm_control_panel.unavailable
states:
- arm_home
`,
},
{
heading: "Invalid Entity",
config: {
type: "alarm-panel",
entity: "alarm_control_panel.alarm1",
},
config: `
- type: alarm-panel
entity: alarm_control_panel.alarm1
`,
},
] satisfies DemoCardConfig<AlarmPanelCardConfig>[];
];
@customElement("demo-lovelace-alarm-panel-card")
class DemoAlarmPanelEntity extends LitElement {
+17 -19
View File
@@ -1,8 +1,6 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { AreaCardConfig } from "../../../../src/panels/lovelace/cards/types";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
@@ -82,33 +80,33 @@ const ENTITIES = [
const CONFIGS = [
{
heading: "Bedroom",
config: {
type: "area",
area: "bedroom",
},
config: `
- type: area
area: bedroom
`,
},
{
heading: "Living Room",
config: {
type: "area",
area: "living_room",
},
config: `
- type: area
area: living_room
`,
},
{
heading: "Office",
config: {
type: "area",
area: "office",
},
config: `
- type: area
area: office
`,
},
{
heading: "Kitchen",
config: {
type: "area",
area: "kitchen",
},
config: `
- type: area
area: kitchen
`,
},
] satisfies DemoCardConfig<AreaCardConfig>[];
];
@customElement("demo-lovelace-area-card")
class DemoArea extends LitElement {
+25 -32
View File
@@ -1,12 +1,7 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type {
ConditionalCardConfig,
EntitiesCardConfig,
} from "../../../../src/panels/lovelace/cards/types";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
@@ -44,37 +39,35 @@ const ENTITIES = [
const CONFIGS = [
{
heading: "Controller",
config: {
type: "entities",
entities: [
"light.controller_1",
"light.controller_2",
{ type: "divider" },
"light.floor",
"light.kitchen",
],
},
config: `
- type: entities
entities:
- light.controller_1
- light.controller_2
- type: divider
- light.floor
- light.kitchen
`,
},
{
heading: "Demo",
config: {
type: "conditional",
conditions: [
{ entity: "light.controller_1", state: "on" },
{ entity: "light.controller_2", state_not: "off" },
],
card: {
type: "entities",
entities: [
"light.controller_1",
"light.controller_2",
"light.floor",
"light.kitchen",
],
},
},
config: `
- type: conditional
conditions:
- entity: light.controller_1
state: "on"
- entity: light.controller_2
state_not: "off"
card:
type: entities
entities:
- light.controller_1
- light.controller_2
- light.floor
- light.kitchen
`,
},
] satisfies DemoCardConfig<EntitiesCardConfig | ConditionalCardConfig>[];
];
@customElement("demo-lovelace-conditional-card")
class DemoConditional extends LitElement {
+135 -166
View File
@@ -1,13 +1,7 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type {
EntitiesCardConfig,
EntitiesCardEntityConfig,
} from "../../../../src/panels/lovelace/cards/types";
import type { CallServiceConfig } from "../../../../src/panels/lovelace/entity-rows/types";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
@@ -260,194 +254,169 @@ const ENTITIES = [
},
];
type GalleryEntitiesCardConfig = Omit<EntitiesCardConfig, "entities"> & {
type: EntitiesCardConfig["type"];
entities: (
| EntitiesCardConfig["entities"][number]
| Pick<EntitiesCardEntityConfig, "entity" | "secondary_info">
| Omit<CallServiceConfig, "entity">
)[];
};
const CONFIGS = [
{
heading: "Basic",
config: {
type: "entities",
entities: [
"scene.romantic_lights",
"device_tracker.demo_paulus",
"cover.kitchen_window",
"group.kitchen",
"lock.kitchen_door",
"light.bed_light",
"light.non_existing",
"climate.ecobee",
"input_number.number",
"sensor.humidity",
"text.message",
"event.doorbell",
],
},
config: `
- type: entities
entities:
- scene.romantic_lights
- device_tracker.demo_paulus
- cover.kitchen_window
- group.kitchen
- lock.kitchen_door
- light.bed_light
- light.non_existing
- climate.ecobee
- input_number.number
- sensor.humidity
- text.message
- event.doorbell
`,
},
{
heading: "With enabled state color",
config: {
type: "entities",
state_color: true,
entities: [
"scene.romantic_lights",
"device_tracker.demo_paulus",
"cover.kitchen_window",
"group.kitchen",
"lock.kitchen_door",
"light.bed_light",
"light.non_existing",
"climate.ecobee",
"input_number.number",
"sensor.humidity",
"text.message",
],
},
config: `
- type: entities
state_color: true
entities:
- scene.romantic_lights
- device_tracker.demo_paulus
- cover.kitchen_window
- group.kitchen
- lock.kitchen_door
- light.bed_light
- light.non_existing
- climate.ecobee
- input_number.number
- sensor.humidity
- text.message
`,
},
{
heading: "Helpers",
config: {
type: "entities",
title: "Helpers",
entities: [
{ entity: "input_boolean.toggle" },
{ entity: "input_datetime.date_and_time" },
{ entity: "input_number.number" },
{ entity: "input_select.dropdown" },
{ entity: "input_text.text" },
{ entity: "timer.timer" },
{ entity: "counter.counter" },
],
},
config: `
- type: entities
title: Helpers
entities:
- entity: input_boolean.toggle
- entity: input_datetime.date_and_time
- entity: input_number.number
- entity: input_select.dropdown
- entity: input_text.text
- entity: timer.timer
- entity: counter.counter
`,
},
{
heading: "With title, toggle-able",
config: {
type: "entities",
entities: [
"scene.romantic_lights",
"device_tracker.demo_paulus",
"cover.kitchen_window",
"group.kitchen",
"lock.kitchen_door",
"light.bed_light",
"climate.ecobee",
"input_number.number",
],
title: "Random group",
},
config: `
- type: entities
entities:
- scene.romantic_lights
- device_tracker.demo_paulus
- cover.kitchen_window
- group.kitchen
- lock.kitchen_door
- light.bed_light
- climate.ecobee
- input_number.number
title: Random group
`,
},
{
heading: "With title, toggle = false",
config: {
type: "entities",
entities: [
"scene.romantic_lights",
"device_tracker.demo_paulus",
"cover.kitchen_window",
"group.kitchen",
"lock.kitchen_door",
"light.bed_light",
"climate.ecobee",
"input_number.number",
],
title: "Random group",
show_header_toggle: false,
},
config: `
- type: entities
entities:
- scene.romantic_lights
- device_tracker.demo_paulus
- cover.kitchen_window
- group.kitchen
- lock.kitchen_door
- light.bed_light
- climate.ecobee
- input_number.number
title: Random group
show_header_toggle: false
`,
},
{
heading: "With title, can't toggle",
config: {
type: "entities",
entities: ["device_tracker.demo_paulus"],
title: "Random group",
},
config: `
- type: entities
entities:
- device_tracker.demo_paulus
title: Random group
`,
},
{
heading: "Unavailable",
config: {
type: "entities",
entities: [
"scene.unavailable",
"device_tracker.unavailable",
"cover.unavailable",
"lock.unavailable",
"light.unavailable",
"climate.unavailable",
"input_number.unavailable",
"input_select.unavailable",
"text.unavailable",
"event.unavailable",
],
},
config: `
- type: entities
entities:
- scene.unavailable
- device_tracker.unavailable
- cover.unavailable
- lock.unavailable
- light.unavailable
- climate.unavailable
- input_number.unavailable
- input_select.unavailable
- text.unavailable
- event.unavailable
`,
},
{
heading: "Custom name, secondary info, custom icon",
config: {
type: "entities",
entities: [
{ entity: "scene.romantic_lights", name: "¯\\_(ツ)_/¯" },
{
entity: "device_tracker.demo_paulus",
secondary_info: "entity-id",
},
{
entity: "cover.kitchen_window",
secondary_info: "last-changed",
},
{ entity: "group.kitchen", icon: "mdi:home-assistant" },
"lock.kitchen_door",
{
entity: "light.bed_light",
icon: "mdi:alarm-light",
name: "Bed Light Custom Icon",
},
"climate.ecobee",
"input_number.number",
],
title: "Random group",
show_header_toggle: false,
},
config: `
- type: entities
entities:
- entity: scene.romantic_lights
name: ¯\\_(ツ)_/¯
- entity: device_tracker.demo_paulus
secondary_info: entity-id
- entity: cover.kitchen_window
secondary_info: last-changed
- entity: group.kitchen
icon: mdi:home-assistant
- lock.kitchen_door
- entity: light.bed_light
icon: mdi:alarm-light
name: Bed Light Custom Icon
- climate.ecobee
- input_number.number
title: Random group
show_header_toggle: false
`,
},
{
heading: "Special rows",
config: {
type: "entities",
entities: [
{
type: "perform-action",
icon: "mdi:power",
name: "Bed light",
action_name: "Toggle light",
action: "light.toggle",
data: { entity_id: "light.bed_light" },
},
{ type: "section", label: "Links" },
{
type: "weblink",
url: "http://google.com/",
icon: "mdi:google",
name: "Google",
},
{ type: "divider" },
{
type: "divider",
style: {
height: "30px",
margin: "4px 0",
background: 'center / contain url("/images/divider.png") no-repeat',
},
},
],
},
config: `
- type: entities
entities:
- type: perform-action
icon: mdi:power
name: Bed light
action_name: Toggle light
action: light.toggle
data:
entity_id: light.bed_light
- type: section
label: Links
- type: weblink
url: http://google.com/
icon: mdi:google
name: Google
- type: divider
- type: divider
style:
height: 30px
margin: 4px 0
background: center / contain url("/images/divider.png") no-repeat
`,
},
] satisfies DemoCardConfig<GalleryEntitiesCardConfig>[];
];
@customElement("demo-lovelace-entities-card")
class DemoEntities extends LitElement {
@@ -1,8 +1,6 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { ButtonCardConfig } from "../../../../src/panels/lovelace/cards/types";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
@@ -20,64 +18,60 @@ const ENTITIES = [
const CONFIGS = [
{
heading: "Basic example",
config: {
type: "button",
entity: "light.bed_light",
},
config: `
- type: button
entity: light.bed_light
`,
},
{
heading: "With Name (defined in card)",
config: {
type: "button",
name: "Custom Name",
entity: "light.bed_light",
},
config: `
- type: button
name: Custom Name
entity: light.bed_light
`,
},
{
heading: "With Icon",
config: {
type: "button",
entity: "light.bed_light",
icon: "mdi:tools",
},
config: `
- type: button
entity: light.bed_light
icon: mdi:tools
`,
},
{
heading: "With State",
config: {
type: "button",
entity: "light.bed_light",
show_state: true,
},
config: `
- type: button
entity: light.bed_light
show_state: true
`,
},
{
heading: "Custom Tap Action (toggle)",
config: {
type: "button",
entity: "light.bed_light",
tap_action: {
action: "toggle",
},
},
config: `
- type: button
entity: light.bed_light
tap_action:
action: toggle
`,
},
{
heading: "Running Service",
config: {
type: "button",
entity: "light.bed_light",
tap_action: {
action: "perform-action",
perform_action: "light.toggle",
},
},
config: `
- type: button
entity: light.bed_light
service: light.toggle
`,
},
{
heading: "Invalid Entity",
config: {
type: "button",
entity: "sensor.invalid_entity",
},
config: `
- type: button
entity: sensor.invalid_entity
`,
},
] satisfies DemoCardConfig<ButtonCardConfig>[];
];
@customElement("demo-lovelace-entity-button-card")
class DemoButtonEntity extends LitElement {
+141 -160
View File
@@ -1,12 +1,7 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type {
EntitiesCardConfig,
EntityFilterCardConfig,
} from "../../../../src/panels/lovelace/cards/types";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
@@ -119,198 +114,184 @@ const ENTITIES = [
},
];
type StateFilterEntityFilterCardConfig = Pick<
EntityFilterCardConfig,
"type" | "entities" | "card" | "show_empty"
> & {
conditions?: never;
state_filter: NonNullable<EntityFilterCardConfig["state_filter"]>;
};
const VALID_CONFIGS = [
const CONFIGS = [
{
heading: "Unfiltered entities",
config: {
type: "entities",
entities: [
"device_tracker.demo_anne_therese",
"device_tracker.demo_home_boy",
"device_tracker.demo_paulus",
"light.bed_light",
"light.ceiling_lights",
"light.kitchen_lights",
],
},
config: `
- type: entities
entities:
- device_tracker.demo_anne_therese
- device_tracker.demo_home_boy
- device_tracker.demo_paulus
- light.bed_light
- light.ceiling_lights
- light.kitchen_lights
`,
},
{
heading: "On and home entities",
config: {
type: "entity-filter",
entities: [
"device_tracker.demo_anne_therese",
"device_tracker.demo_home_boy",
"device_tracker.demo_paulus",
"light.bed_light",
"light.ceiling_lights",
"light.kitchen_lights",
],
conditions: [{ condition: "state", state: ["on", "home"] }],
},
config: `
- type: entity-filter
entities:
- device_tracker.demo_anne_therese
- device_tracker.demo_home_boy
- device_tracker.demo_paulus
- light.bed_light
- light.ceiling_lights
- light.kitchen_lights
conditions:
- condition: state
state:
- "on"
- home
`,
},
{
heading: "Same state as Bed Light",
config: {
type: "entity-filter",
entities: [
"device_tracker.demo_anne_therese",
"device_tracker.demo_home_boy",
"device_tracker.demo_paulus",
"light.bed_light",
"light.ceiling_lights",
"light.kitchen_lights",
],
conditions: [{ condition: "state", state: ["light.bed_light"] }],
},
config: `
- type: entity-filter
entities:
- device_tracker.demo_anne_therese
- device_tracker.demo_home_boy
- device_tracker.demo_paulus
- light.bed_light
- light.ceiling_lights
- light.kitchen_lights
conditions:
- condition: state
state:
- light.bed_light
`,
},
{
heading: 'With "entities" card config',
config: {
type: "entity-filter",
entities: [
"device_tracker.demo_anne_therese",
"device_tracker.demo_home_boy",
"device_tracker.demo_paulus",
"light.bed_light",
"light.ceiling_lights",
"light.kitchen_lights",
],
conditions: [{ condition: "state", state: ["on", "home"] }],
card: {
type: "entities",
title: "Custom Title",
show_header_toggle: false,
},
},
config: `
- type: entity-filter
entities:
- device_tracker.demo_anne_therese
- device_tracker.demo_home_boy
- device_tracker.demo_paulus
- light.bed_light
- light.ceiling_lights
- light.kitchen_lights
conditions:
- condition: state
state:
- "on"
- home
card:
type: entities
title: Custom Title
show_header_toggle: false
`,
},
{
heading: 'With "glance" card config',
config: {
type: "entity-filter",
entities: [
"device_tracker.demo_anne_therese",
"device_tracker.demo_home_boy",
"device_tracker.demo_paulus",
"light.bed_light",
"light.ceiling_lights",
"light.kitchen_lights",
],
conditions: [{ condition: "state", state: ["on", "home"] }],
card: {
type: "glance",
show_state: true,
title: "Custom Title",
},
},
config: `
- type: entity-filter
entities:
- device_tracker.demo_anne_therese
- device_tracker.demo_home_boy
- device_tracker.demo_paulus
- light.bed_light
- light.ceiling_lights
- light.kitchen_lights
conditions:
- condition: state
state:
- "on"
- home
card:
type: glance
show_state: true
title: Custom Title
`,
},
{
heading:
"Filtered entities by battery attribute (< '30') using state filter",
config: {
type: "entity-filter",
entities: [
"device_tracker.demo_anne_therese",
"device_tracker.demo_home_boy",
"device_tracker.demo_paulus",
],
state_filter: [{ operator: "<", attribute: "battery", value: "30" }],
},
config: `
- type: entity-filter
entities:
- device_tracker.demo_anne_therese
- device_tracker.demo_home_boy
- device_tracker.demo_paulus
state_filter:
- operator: <
attribute: battery
value: "30"
`,
},
{
heading: "Unfiltered number entities",
config: {
type: "entities",
entities: [
"input_number.min_battery_level",
"sensor.battery_1",
"sensor.battery_3",
"sensor.battery_2",
"sensor.battery_4",
],
},
config: `
- type: entities
entities:
- input_number.min_battery_level
- sensor.battery_1
- sensor.battery_3
- sensor.battery_2
- sensor.battery_4
`,
},
{
heading: "Battery lower than 50%",
config: {
type: "entity-filter",
entities: [
"sensor.battery_1",
"sensor.battery_3",
"sensor.battery_2",
"sensor.battery_4",
],
conditions: [{ condition: "numeric_state", below: 50 }],
},
config: `
- type: entity-filter
entities:
- sensor.battery_1
- sensor.battery_3
- sensor.battery_2
- sensor.battery_4
conditions:
- condition: numeric_state
below: 50
`,
},
{
heading: "Battery lower than min battery level",
config: {
type: "entity-filter",
entities: [
"sensor.battery_1",
"sensor.battery_3",
"sensor.battery_2",
"sensor.battery_4",
],
conditions: [
{ condition: "numeric_state", below: "input_number.min_battery_level" },
],
},
config: `
- type: entity-filter
entities:
- sensor.battery_1
- sensor.battery_3
- sensor.battery_2
- sensor.battery_4
conditions:
- condition: numeric_state
below: input_number.min_battery_level
`,
},
{
heading: "Battery between min battery level and 70%",
config: {
type: "entity-filter",
entities: [
"sensor.battery_1",
"sensor.battery_3",
"sensor.battery_2",
"sensor.battery_4",
],
conditions: [
{
condition: "numeric_state",
above: "input_number.min_battery_level",
below: 70,
},
],
},
config: `
- type: entity-filter
entities:
- sensor.battery_1
- sensor.battery_3
- sensor.battery_2
- sensor.battery_4
conditions:
- condition: numeric_state
above: input_number.min_battery_level
below: 70
`,
},
] satisfies DemoCardConfig<
| EntitiesCardConfig
| EntityFilterCardConfig
| StateFilterEntityFilterCardConfig
>[];
const INVALID_CONFIGS = [
{
heading: "Error: Entities must be specified",
config: { type: "entity-filter" },
expectConfigError: true,
config: `
- type: entity-filter
`,
},
{
heading: "Error: Incorrect filter config",
config: {
type: "entity-filter",
entities: ["sensor.gas_station_lowest_price"],
},
expectConfigError: true,
config: `
- type: entity-filter
entities:
- sensor.gas_station_lowest_price
`,
},
] satisfies DemoCardConfig<
| Pick<EntityFilterCardConfig, "type">
| Pick<EntityFilterCardConfig, "type" | "entities">
>[];
const CONFIGS = [...VALID_CONFIGS, ...INVALID_CONFIGS];
];
@customElement("demo-lovelace-entity-filter-card")
class DemoEntityFilter extends LitElement {
+115 -115
View File
@@ -1,9 +1,7 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { GaugeCardConfig } from "../../../../src/panels/lovelace/cards/types";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
@@ -32,156 +30,158 @@ const ENTITIES = [
const CONFIGS = [
{
heading: "Basic example",
config: {
type: "gauge",
entity: "sensor.outside_humidity",
name: "Outside Humidity",
},
config: `
- type: gauge
entity: sensor.outside_humidity
name: Outside Humidity
`,
},
{
heading: "Custom unit of measurement",
config: {
type: "gauge",
entity: "sensor.outside_temperature",
unit: "C",
name: "Outside Temperature",
},
config: `
- type: gauge
entity: sensor.outside_temperature
unit_of_measurement: C
name: Outside Temperature
`,
},
{
heading: "Rendering needle",
config: {
type: "gauge",
entity: "sensor.outside_humidity",
name: "Outside Humidity",
needle: true,
},
config: `
- type: gauge
entity: sensor.outside_humidity
name: Outside Humidity
needle: true
`,
},
{
heading: "Rendering needle and severity levels",
config: {
type: "gauge",
entity: "sensor.brightness_high",
name: "Brightness High",
needle: true,
severity: {
red: 75,
green: 0,
yellow: 50,
},
},
config: `
- type: gauge
entity: sensor.brightness_high
name: Brightness High
needle: true
severity:
red: 75
green: 0
yellow: 50
`,
},
{
heading: "Setting severity levels",
config: {
type: "gauge",
entity: "sensor.brightness",
name: "Brightness Low",
severity: {
red: 75,
green: 0,
yellow: 50,
},
},
config: `
- type: gauge
entity: sensor.brightness
name: Brightness Low
severity:
red: 75
green: 0
yellow: 50
`,
},
{
heading: "Setting severity levels",
config: {
type: "gauge",
entity: "sensor.brightness_medium",
name: "Brightness Medium",
severity: {
red: 75,
green: 0,
yellow: 50,
},
},
config: `
- type: gauge
entity: sensor.brightness_medium
name: Brightness Medium
severity:
red: 75
green: 0
yellow: 50
`,
},
{
heading: "Setting severity levels",
config: {
type: "gauge",
entity: "sensor.brightness_high",
name: "Brightness High",
severity: {
red: 75,
green: 0,
yellow: 50,
},
},
config: `
- type: gauge
entity: sensor.brightness_high
name: Brightness High
severity:
red: 75
green: 0
yellow: 50
`,
},
{
heading: "Setting min (0) and mx (15) values",
config: {
type: "gauge",
entity: "sensor.brightness",
name: "Brightness",
min: 0,
max: 15,
},
config: `
- type: gauge
entity: sensor.brightness
name: Brightness
min: 0
max: 15
`,
},
{
heading: "Invalid entity",
config: {
type: "gauge",
entity: "sensor.invalid_entity",
},
config: `
- type: gauge
entity: sensor.invalid_entity
`,
},
{
heading: "Non-numeric value",
config: {
type: "gauge",
entity: "plant.bonsai",
},
config: `
- type: gauge
entity: plant.bonsai
`,
},
{
heading: "Unavailable entity",
config: {
type: "gauge",
entity: "sensor.not_working",
},
config: `
- type: gauge
entity: sensor.not_working
`,
},
{
heading: "Lower minimum",
config: {
type: "gauge",
entity: "sensor.brightness_high",
needle: true,
severity: {
green: 0,
yellow: 0.45,
red: 0.9,
},
min: -0.05,
name: " ",
max: 1.9,
unit: "GBP/h",
},
config: `
- type: gauge
entity: sensor.brightness_high
needle: true
severity:
green: 0
yellow: 0.45
red: 0.9
min: -0.05
name: " "
max: 1.9
unit: GBP/h`,
},
{
heading: "A lot of segments",
config: {
type: "gauge",
needle: true,
name: "Percent gauge",
entity: "sensor.brightness_high",
unit: "%",
min: 0,
max: 100,
segments: [
{ from: 0, color: "#db4437" },
{ from: 10, color: "#cc4d39" },
{ from: 20, color: "#bd563a" },
{ from: 30, color: "#ad603c" },
{ from: 40, color: "#9e693d" },
{ from: 50, color: "#8f723f" },
{ from: 60, color: "#807b41" },
{ from: 70, color: "#718442" },
{ from: 80, color: "#618e44" },
{ from: 90, color: "#43a047" },
],
},
config: `
- type: gauge
needle: true
name: Percent gauge
entity: sensor.brightness_high
unit: "%"
min: 0
max: 100
segments:
- from: 0
color: "#db4437"
- from: 10
color: "#cc4d39"
- from: 20
color: "#bd563a"
- from: 30
color: "#ad603c"
- from: 40
color: "#9e693d"
- from: 50
color: "#8f723f"
- from: 60
color: "#807b41"
- from: 70
color: "#718442"
- from: 80
color: "#618e44"
- from: 90
color: "#43a047"`,
},
] satisfies DemoCardConfig<GaugeCardConfig>[];
];
@customElement("demo-lovelace-gauge-card")
class DemoGaugeEntity extends LitElement {
+135 -161
View File
@@ -1,12 +1,7 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type {
GlanceCardConfig,
GlanceConfigEntity,
} from "../../../../src/panels/lovelace/cards/types";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
@@ -91,193 +86,172 @@ const ENTITIES = [
},
];
type LegacyNullNameGlanceCardConfig = Omit<GlanceCardConfig, "entities"> & {
type: GlanceCardConfig["type"];
entities: (
| string
| GlanceConfigEntity
| (Omit<GlanceConfigEntity, "name"> & { name: null })
)[];
};
const CONFIGS = [
{
heading: "Basic example",
config: {
type: "glance",
entities: [
"device_tracker.demo_paulus",
"media_player.living_room",
"sun.sun",
"cover.kitchen_window",
"light.kitchen_lights",
"lock.kitchen_door",
"light.ceiling_lights",
],
},
config: `
- type: glance
entities:
- device_tracker.demo_paulus
- media_player.living_room
- sun.sun
- cover.kitchen_window
- light.kitchen_lights
- lock.kitchen_door
- light.ceiling_lights
`,
},
{
heading: "No state colors",
config: {
type: "glance",
state_color: false,
entities: [
"device_tracker.demo_paulus",
"media_player.living_room",
"sun.sun",
"cover.kitchen_window",
"light.kitchen_lights",
"lock.kitchen_door",
"light.ceiling_lights",
],
},
config: `
- type: glance
state_color: false
entities:
- device_tracker.demo_paulus
- media_player.living_room
- sun.sun
- cover.kitchen_window
- light.kitchen_lights
- lock.kitchen_door
- light.ceiling_lights
`,
},
{
heading: "With title",
config: {
type: "glance",
title: "Custom title",
columns: 4,
entities: [
"device_tracker.demo_paulus",
"media_player.living_room",
"sun.sun",
"cover.kitchen_window",
"light.kitchen_lights",
"lock.kitchen_door",
"light.ceiling_lights",
],
},
config: `
- type: glance
title: Custom title
columns: 4
entities:
- device_tracker.demo_paulus
- media_player.living_room
- sun.sun
- cover.kitchen_window
- light.kitchen_lights
- lock.kitchen_door
- light.ceiling_lights
`,
},
{
heading: "Custom number of columns",
config: {
type: "glance",
columns: 7,
entities: [
"device_tracker.demo_paulus",
"media_player.living_room",
"sun.sun",
"cover.kitchen_window",
"light.kitchen_lights",
"lock.kitchen_door",
"light.ceiling_lights",
],
},
config: `
- type: glance
columns: 7
entities:
- device_tracker.demo_paulus
- media_player.living_room
- sun.sun
- cover.kitchen_window
- light.kitchen_lights
- lock.kitchen_door
- light.ceiling_lights
`,
},
{
heading: "No entity names",
config: {
type: "glance",
columns: 4,
show_name: false,
entities: [
"device_tracker.demo_paulus",
"media_player.living_room",
"sun.sun",
"cover.kitchen_window",
"light.kitchen_lights",
"lock.kitchen_door",
"light.ceiling_lights",
],
},
config: `
- type: glance
columns: 4
show_name: false
entities:
- device_tracker.demo_paulus
- media_player.living_room
- sun.sun
- cover.kitchen_window
- light.kitchen_lights
- lock.kitchen_door
- light.ceiling_lights
`,
},
{
heading: "No state labels",
config: {
type: "glance",
columns: 4,
show_state: false,
entities: [
"device_tracker.demo_paulus",
"media_player.living_room",
"sun.sun",
"cover.kitchen_window",
"light.kitchen_lights",
"lock.kitchen_door",
"light.ceiling_lights",
],
},
config: `
- type: glance
columns: 4
show_state: false
entities:
- device_tracker.demo_paulus
- media_player.living_room
- sun.sun
- cover.kitchen_window
- light.kitchen_lights
- lock.kitchen_door
- light.ceiling_lights
`,
},
{
heading: "No names and no state labels",
config: {
type: "glance",
columns: 4,
show_name: false,
show_state: false,
entities: [
"device_tracker.demo_paulus",
"media_player.living_room",
"sun.sun",
"cover.kitchen_window",
"light.kitchen_lights",
"lock.kitchen_door",
"light.ceiling_lights",
],
},
config: `
- type: glance
columns: 4
show_name: false
show_state: false
entities:
- device_tracker.demo_paulus
- media_player.living_room
- sun.sun
- cover.kitchen_window
- light.kitchen_lights
- lock.kitchen_door
- light.ceiling_lights
`,
},
{
heading: "Custom name + custom icon",
config: {
type: "glance",
columns: 4,
entities: [
{
entity: "device_tracker.demo_paulus",
name: "¯\\_(ツ)_/¯",
icon: "mdi:home-assistant",
},
{
entity: "media_player.living_room",
name: "¯\\_(ツ)_/¯",
icon: "mdi:home-assistant",
},
],
},
config: `
- type: glance
columns: 4
entities:
- entity: device_tracker.demo_paulus
name: ¯\\_(ツ)_/¯
icon: mdi:home-assistant
- entity: media_player.living_room
name: ¯\\_(ツ)_/¯
icon: mdi:home-assistant
`,
},
{
heading: "Selectively hidden name",
config: {
type: "glance",
columns: 4,
entities: [
"device_tracker.demo_paulus",
{ entity: "media_player.living_room", name: null },
"sun.sun",
{ entity: "cover.kitchen_window", name: null },
"light.kitchen_lights",
{ entity: "lock.kitchen_door", name: null },
"light.ceiling_lights",
],
},
config: `
- type: glance
columns: 4
entities:
- device_tracker.demo_paulus
- entity: media_player.living_room
name:
- sun.sun
- entity: cover.kitchen_window
name:
- light.kitchen_lights
- entity: lock.kitchen_door
name:
- light.ceiling_lights
`,
},
{
heading: "Custom tap action",
config: {
type: "glance",
columns: 4,
entities: [
{
entity: "lock.kitchen_door",
name: "Custom",
tap_action: { action: "toggle" },
},
{
entity: "light.ceiling_lights",
name: "Custom",
tap_action: {
action: "perform-action",
perform_action: "light.turn_on",
data: { entity_id: "light.ceiling_lights" },
},
},
{ entity: "sun.sun", name: "Regular" },
{ entity: "light.kitchen_lights", name: "Regular" },
],
},
config: `
- type: glance
columns: 4
entities:
- entity: lock.kitchen_door
name: Custom
tap_action:
type: toggle
- entity: light.ceiling_lights
name: Custom
tap_action:
action: call-service
service: light.turn_on
data:
entity_id: light.ceiling_lights
- entity: sun.sun
name: Regular
- entity: light.kitchen_lights
name: Regular
`,
},
] satisfies DemoCardConfig<GlanceCardConfig | LegacyNullNameGlanceCardConfig>[];
];
@customElement("demo-lovelace-glance-card")
class DemoGlanceEntity extends LitElement {
+124 -129
View File
@@ -1,13 +1,8 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { DemoCardConfig } from "../../components/demo-card";
import { mockHistory } from "../../../../demo/src/stubs/history";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type {
GridCardConfig,
StackCardConfig,
} from "../../../../src/panels/lovelace/cards/types";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
@@ -75,159 +70,159 @@ const ENTITIES = [
const CONFIGS = [
{
heading: "Default Grid",
config: {
type: "grid",
cards: [
{ type: "entity", entity: "light.kitchen_lights" },
{ type: "entity", entity: "light.bed_light" },
{ type: "entity", entity: "device_tracker.demo_paulus" },
{ type: "sensor", entity: "sensor.illumination", graph: "line" },
{ type: "entity", entity: "device_tracker.demo_anne_therese" },
],
},
config: `
- type: grid
cards:
- type: entity
entity: light.kitchen_lights
- type: entity
entity: light.bed_light
- type: entity
entity: device_tracker.demo_paulus
- type: sensor
entity: sensor.illumination
graph: line
- type: entity
entity: device_tracker.demo_anne_therese
`,
},
{
heading: "Non-square Grid with 2 columns",
config: {
type: "grid",
columns: 2,
square: false,
cards: [
{ type: "entity", entity: "light.kitchen_lights" },
{ type: "entity", entity: "light.bed_light" },
{ type: "entity", entity: "device_tracker.demo_paulus" },
{ type: "sensor", entity: "sensor.illumination", graph: "line" },
],
},
config: `
- type: grid
columns: 2
square: false
cards:
- type: entity
entity: light.kitchen_lights
- type: entity
entity: light.bed_light
- type: entity
entity: device_tracker.demo_paulus
- type: sensor
entity: sensor.illumination
graph: line
`,
},
{
heading: "Default Grid with title",
config: {
type: "grid",
title: "Kitchen",
cards: [
{ type: "entity", entity: "light.kitchen_lights" },
{ type: "entity", entity: "light.bed_light" },
{ type: "entity", entity: "device_tracker.demo_paulus" },
{ type: "sensor", entity: "sensor.illumination", graph: "line" },
{ type: "entity", entity: "device_tracker.demo_anne_therese" },
],
},
config: `
- type: grid
title: Kitchen
cards:
- type: entity
entity: light.kitchen_lights
- type: entity
entity: light.bed_light
- type: entity
entity: device_tracker.demo_paulus
- type: sensor
entity: sensor.illumination
graph: line
- type: entity
entity: device_tracker.demo_anne_therese
`,
},
{
heading: "Columns 4",
config: {
type: "grid",
columns: 4,
cards: [
{ type: "entity", entity: "light.kitchen_lights" },
{ type: "entity", entity: "light.bed_light" },
{ type: "entity", entity: "device_tracker.demo_paulus" },
{ type: "sensor", entity: "sensor.illumination", graph: "line" },
],
},
config: `
- type: grid
columns: 4
cards:
- type: entity
entity: light.kitchen_lights
- type: entity
entity: light.bed_light
- type: entity
entity: device_tracker.demo_paulus
- type: sensor
entity: sensor.illumination
graph: line
`,
},
{
heading: "Columns 2",
config: {
type: "grid",
columns: 2,
cards: [
{ type: "entity", entity: "light.kitchen_lights" },
{ type: "entity", entity: "light.bed_light" },
],
},
config: `
- type: grid
columns: 2
cards:
- type: entity
entity: light.kitchen_lights
- type: entity
entity: light.bed_light
`,
},
{
heading: "Columns 1",
config: {
type: "grid",
columns: 1,
cards: [{ type: "entity", entity: "light.kitchen_lights" }],
},
config: `
- type: grid
columns: 1
cards:
- type: entity
entity: light.kitchen_lights
`,
},
{
heading: "Size for single card",
config: {
type: "grid",
cards: [{ type: "entity", entity: "light.kitchen_lights" }],
},
config: `
- type: grid
cards:
- type: entity
entity: light.kitchen_lights
`,
},
{
heading: "Vertical Stack",
config: {
type: "vertical-stack",
cards: [
{
type: "picture-entity",
image: "/images/kitchen.png",
entity: "light.kitchen_lights",
},
{
type: "glance",
entities: [
"device_tracker.demo_anne_therese",
"device_tracker.demo_home_boy",
"device_tracker.demo_paulus",
],
},
],
},
config: `
- type: vertical-stack
cards:
- type: picture-entity
image: /images/kitchen.png
entity: light.kitchen_lights
- type: glance
entities:
- device_tracker.demo_anne_therese
- device_tracker.demo_home_boy
- device_tracker.demo_paulus
`,
},
{
heading: "Horizontal Stack",
config: {
type: "horizontal-stack",
cards: [
{
type: "picture-entity",
image: "/images/kitchen.png",
entity: "light.kitchen_lights",
},
{
type: "glance",
entities: [
"device_tracker.demo_anne_therese",
"device_tracker.demo_home_boy",
"device_tracker.demo_paulus",
],
},
],
},
config: `
- type: horizontal-stack
cards:
- type: picture-entity
image: /images/kitchen.png
entity: light.kitchen_lights
- type: glance
entities:
- device_tracker.demo_anne_therese
- device_tracker.demo_home_boy
- device_tracker.demo_paulus
`,
},
{
heading: "Combination of both",
config: {
type: "vertical-stack",
cards: [
{
type: "horizontal-stack",
cards: [
{
type: "picture-entity",
image: "/images/kitchen.png",
entity: "light.kitchen_lights",
},
{
type: "glance",
entities: [
"device_tracker.demo_anne_therese",
"device_tracker.demo_home_boy",
"device_tracker.demo_paulus",
],
},
],
},
{
type: "picture-entity",
image: "/images/bed.png",
entity: "light.bed_light",
},
],
},
config: `
- type: vertical-stack
cards:
- type: horizontal-stack
cards:
- type: picture-entity
image: /images/kitchen.png
entity: light.kitchen_lights
- type: glance
entities:
- device_tracker.demo_anne_therese
- device_tracker.demo_home_boy
- device_tracker.demo_paulus
- type: picture-entity
image: /images/bed.png
entity: light.bed_light
`,
},
] satisfies DemoCardConfig<GridCardConfig | StackCardConfig>[];
];
@customElement("demo-lovelace-grid-and-stack-card")
class DemoStack extends LitElement {
+20 -22
View File
@@ -1,44 +1,42 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { IframeCardConfig } from "../../../../src/panels/lovelace/cards/types";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards";
const CONFIGS = [
{
heading: "Without title",
config: {
type: "iframe",
url: "https://embed.windy.com/embed2.html",
},
config: `
- type: iframe
url: https://embed.windy.com/embed2.html
`,
},
{
heading: "With title",
config: {
type: "iframe",
url: "https://embed.windy.com/embed2.html",
title: "Weather radar",
},
config: `
- type: iframe
url: https://embed.windy.com/embed2.html
title: Weather radar
`,
},
{
heading: "Height-Width 3:4",
config: {
type: "iframe",
url: "https://embed.windy.com/embed2.html",
aspect_ratio: "75%",
},
config: `
- type: iframe
url: https://embed.windy.com/embed2.html
aspect_ratio: 75%
`,
},
{
heading: "Height-Width 1:1",
config: {
type: "iframe",
url: "https://embed.windy.com/embed2.html",
aspect_ratio: "100%",
},
config: `
- type: iframe
url: https://embed.windy.com/embed2.html
aspect_ratio: 100%
`,
},
] satisfies DemoCardConfig<IframeCardConfig>[];
];
@customElement("demo-lovelace-iframe-card")
class DemoIframe extends LitElement {
+21 -23
View File
@@ -1,8 +1,6 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { LightCardConfig } from "../../../../src/panels/lovelace/cards/types";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
@@ -46,40 +44,40 @@ const ENTITIES = [
const CONFIGS = [
{
heading: "Switchable Light",
config: {
type: "light",
entity: "light.bed_light",
},
config: `
- type: light
entity: light.bed_light
`,
},
{
heading: "Dimmable Light On",
config: {
type: "light",
entity: "light.dim_on",
},
config: `
- type: light
entity: light.dim_on
`,
},
{
heading: "Dimmable Light Off",
config: {
type: "light",
entity: "light.dim_off",
},
config: `
- type: light
entity: light.dim_off
`,
},
{
heading: "Unavailable",
config: {
type: "light",
entity: "light.unavailable",
},
config: `
- type: light
entity: light.unavailable
`,
},
{
heading: "Non existing",
config: {
type: "light",
entity: "light.nonexisting",
},
config: `
- type: light
entity: light.nonexisting
`,
},
] satisfies DemoCardConfig<LightCardConfig>[];
];
@customElement("demo-lovelace-light-card")
class DemoLightEntity extends LitElement {
+70 -58
View File
@@ -1,9 +1,7 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { MapCardConfig } from "../../../../src/panels/lovelace/cards/types";
import "../../components/demo-cards";
const ENTITIES = [
@@ -88,93 +86,107 @@ const ENTITIES = [
const CONFIGS = [
{
heading: "Without title",
config: {
type: "map",
entities: [
{ entity: "device_tracker.demo_paulus" },
"device_tracker.demo_home_boy",
"zone.home",
],
},
config: `
- type: map
entities:
- entity: device_tracker.demo_paulus
- device_tracker.demo_home_boy
- zone.home
`,
},
{
heading: "With title",
config: {
type: "map",
entities: [{ entity: "device_tracker.demo_paulus" }, "zone.home"],
title: "Where is Paulus?",
},
config: `
- type: map
entities:
- entity: device_tracker.demo_paulus
- zone.home
title: Where is Paulus?
`,
},
{
heading: "Height-Width 1:2",
config: {
type: "map",
entities: [{ entity: "device_tracker.demo_paulus" }, "zone.home"],
aspect_ratio: "50%",
},
config: `
- type: map
entities:
- entity: device_tracker.demo_paulus
- zone.home
aspect_ratio: 50%
`,
},
{
heading: "Default Zoom",
config: {
type: "map",
default_zoom: 12,
entities: [{ entity: "device_tracker.demo_paulus" }, "zone.home"],
},
config: `
- type: map
default_zoom: 12
entities:
- entity: device_tracker.demo_paulus
- zone.home
`,
},
{
heading: "Default Zoom too High",
config: {
type: "map",
default_zoom: 20,
entities: [{ entity: "device_tracker.demo_paulus" }, "zone.home"],
},
config: `
- type: map
default_zoom: 20
entities:
- entity: device_tracker.demo_paulus
- zone.home
`,
},
{
heading: "Single Marker",
config: {
type: "map",
entities: ["device_tracker.demo_paulus"],
},
config: `
- type: map
entities:
- device_tracker.demo_paulus
`,
},
{
heading: "Single Marker Default Zoom",
config: {
type: "map",
default_zoom: 8,
entities: ["device_tracker.demo_paulus"],
},
config: `
- type: map
default_zoom: 8
entities:
- device_tracker.demo_paulus
`,
},
{
heading: "No Entities",
config: {
type: "map",
entities: ["light.bed_light"],
},
config: `
- type: map
entities:
- light.bed_light
`,
},
{
heading: "No Entities, Default Zoom",
config: {
type: "map",
default_zoom: 8,
entities: ["light.bed_light"],
},
config: `
- type: map
default_zoom: 8
entities:
- light.bed_light
`,
},
{
heading: "Geo Location Entities",
config: {
type: "map",
geo_location_sources: ["bushfire_demo"],
},
config: `
- type: map
geo_location_sources:
- bushfire_demo
`,
},
{
heading: "Geo Location Entities with Home Zone",
config: {
type: "map",
geo_location_sources: ["bushfire_demo"],
entities: ["zone.bushfire"],
},
config: `
- type: map
geo_location_sources:
- bushfire_demo
entities:
- zone.bushfire
`,
},
] satisfies DemoCardConfig<MapCardConfig>[];
];
@customElement("demo-lovelace-map-card")
class DemoMap extends LitElement {
+6 -10
View File
@@ -1,18 +1,17 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { DemoCardConfig } from "../../components/demo-card";
import { mockTemplate } from "../../../../demo/src/stubs/template";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { MarkdownCardConfig } from "../../../../src/panels/lovelace/cards/types";
import "../../components/demo-cards";
const CONFIGS = [
{
heading: "markdown-it demo",
config: {
type: "markdown",
content: `# h1 Heading 8-)
config: `
- type: markdown
content: |
# h1 Heading 8-)
## h2 Heading
@@ -279,12 +278,9 @@ const CONFIGS = [
<ha-alert alert-type="success">This is a success alert — check it out!</ha-alert>
<ha-alert title="Test alert">This is an alert with a title</ha-alert>
`
.replace(/^ {4}/gm, "")
.replace(/\n+$/, "\n"),
},
`,
},
] satisfies DemoCardConfig<MarkdownCardConfig>[];
];
@customElement("demo-lovelace-markdown-card")
class DemoMarkdown extends LitElement {
@@ -1,165 +1,162 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type {
GridCardConfig,
MediaControlCardConfig,
} from "../../../../src/panels/lovelace/cards/types";
import "../../components/demo-cards";
import { createMediaPlayerEntities } from "../../data/media_players";
const CONFIGS = [
{
heading: "Paused Music",
config: {
type: "media-control",
entity: "media_player.music_paused",
},
config: `
- type: media-control
entity: media_player.music_paused
`,
},
{
heading: "Playing Music",
config: {
type: "media-control",
entity: "media_player.music_playing",
},
config: `
- type: media-control
entity: media_player.music_playing
`,
},
{
heading: "Playing Stream",
config: {
type: "media-control",
entity: "media_player.stream_playing",
},
config: `
- type: media-control
entity: media_player.stream_playing
`,
},
{
heading: "Paused Stream",
config: {
type: "media-control",
entity: "media_player.stream_paused",
},
config: `
- type: media-control
entity: media_player.stream_paused
`,
},
{
heading: 'Playing Stream (with "previous" support)',
config: {
type: "media-control",
entity: "media_player.stream_playing_previous",
},
config: `
- type: media-control
entity: media_player.stream_playing_previous
`,
},
{
heading: "Playing non-skip TV Show",
config: {
type: "media-control",
entity: "media_player.tv_playing",
},
config: `
- type: media-control
entity: media_player.tv_playing
`,
},
{
heading: "Screen Casting",
config: {
type: "media-control",
entity: "media_player.android_cast",
},
config: `
- type: media-control
entity: media_player.android_cast
`,
},
{
heading: "Digital Picture Frame",
config: {
type: "media-control",
entity: "media_player.image_display",
},
config: `
- type: media-control
entity: media_player.image_display
`,
},
{
heading: "Sonos Idle",
config: {
type: "media-control",
entity: "media_player.sonos_idle",
},
config: `
- type: media-control
entity: media_player.sonos_idle
`,
},
{
heading: "Idle waiting for Browse Media",
config: {
type: "media-control",
entity: "media_player.idle_browse_media",
},
config: `
- type: media-control
entity: media_player.idle_browse_media
`,
},
{
heading: "Player Off",
config: {
type: "media-control",
entity: "media_player.theater_off",
},
config: `
- type: media-control
entity: media_player.theater_off
`,
},
{
heading: "Player On",
config: {
type: "media-control",
entity: "media_player.theater_on",
},
config: `
- type: media-control
entity: media_player.theater_on
`,
},
{
heading: "Player Off (cannot be switched on)",
config: {
type: "media-control",
entity: "media_player.theater_off_static",
},
config: `
- type: media-control
entity: media_player.theater_off_static
`,
},
{
heading: "Player On (cannot be switched off)",
config: {
type: "media-control",
entity: "media_player.theater_on_static",
},
config: `
- type: media-control
entity: media_player.theater_on_static
`,
},
{
heading: "Player Idle",
config: {
type: "media-control",
entity: "media_player.idle",
},
config: `
- type: media-control
entity: media_player.idle
`,
},
{
heading: "Player Playing",
config: {
type: "media-control",
entity: "media_player.playing",
},
config: `
- type: media-control
entity: media_player.playing
`,
},
{
heading: "Player Unavailable",
config: {
type: "media-control",
entity: "media_player.unavailable",
},
config: `
- type: media-control
entity: media_player.unavailable
`,
},
{
heading: "Player Unknown",
config: {
type: "media-control",
entity: "media_player.unknown",
},
config: `
- type: media-control
entity: media_player.unknown
`,
},
{
heading: "Receiver On (selectable sources)",
config: {
type: "media-control",
entity: "media_player.receiver_on",
},
config: `
- type: media-control
entity: media_player.receiver_on
`,
},
{
heading: "Receiver Off (selectable sources)",
config: {
type: "media-control",
entity: "media_player.receiver_off",
},
config: `
- type: media-control
entity: media_player.receiver_off
`,
},
{
heading: "Grid Full Size",
config: {
type: "grid",
columns: 1,
cards: [{ type: "media-control", entity: "media_player.music_paused" }],
},
config: `
- type: grid
columns: 1
cards:
- type: media-control
entity: media_player.music_paused
`,
},
] satisfies DemoCardConfig<MediaControlCardConfig | GridCardConfig>[];
];
@customElement("demo-lovelace-media-control-card")
class DemoHuiMediaControlCard extends LitElement {
+45 -46
View File
@@ -1,60 +1,59 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { EntitiesCardConfig } from "../../../../src/panels/lovelace/cards/types";
import "../../components/demo-cards";
import { createMediaPlayerEntities } from "../../data/media_players";
const CONFIGS = [
{
heading: "Media Players",
config: {
type: "entities",
entities: [
{ entity: "media_player.music_paused", name: "Paused Music" },
{ entity: "media_player.music_playing", name: "Playing Music" },
{ entity: "media_player.stream_playing", name: "Playing Stream" },
{ entity: "media_player.stream_paused", name: "Paused Stream" },
{
entity: "media_player.stream_playing_previous",
name: 'Playing Stream (with "previous" support)',
},
{ entity: "media_player.tv_playing", name: "Playing non-skip TV Show" },
{ entity: "media_player.android_cast", name: "Screen casting" },
{ entity: "media_player.image_display", name: "Digital Picture Frame" },
{ entity: "media_player.sonos_idle", name: "Sonos Idle" },
{
entity: "media_player.idle_browse_media",
name: "Idle waiting for Browse Media",
},
{ entity: "media_player.theater_off", name: "Player Off" },
{ entity: "media_player.theater_on", name: "Player On" },
{
entity: "media_player.theater_off_static",
name: "Player Off (cannot be switched on)",
},
{
entity: "media_player.theater_on_static",
name: "Player On (cannot be switched off)",
},
{ entity: "media_player.idle", name: "Player Idle" },
{ entity: "media_player.playing", name: "Player Playing" },
{ entity: "media_player.unavailable", name: "Player Unavailable" },
{ entity: "media_player.unknown", name: "Player Unknown" },
{
entity: "media_player.receiver_on",
name: "Receiver On (selectable sources)",
},
{
entity: "media_player.receiver_off",
name: "Receiver Off (selectable sources)",
},
],
},
config: `
- type: entities
entities:
- entity: media_player.music_paused
name: Paused Music
- entity: media_player.music_playing
name: Playing Music
- entity: media_player.stream_playing
name: Playing Stream
- entity: media_player.stream_paused
name: Paused Stream
- entity: media_player.stream_playing_previous
name: Playing Stream (with "previous" support)
- entity: media_player.tv_playing
name: Playing non-skip TV Show
- entity: media_player.android_cast
name: Screen casting
- entity: media_player.image_display
name: Digital Picture Frame
- entity: media_player.sonos_idle
name: Sonos Idle
- entity: media_player.idle_browse_media
name: Idle waiting for Browse Media
- entity: media_player.theater_off
name: Player Off
- entity: media_player.theater_on
name: Player On
- entity: media_player.theater_off_static
name: Player Off (cannot be switched on)
- entity: media_player.theater_on_static
name: Player On (cannot be switched off)
- entity: media_player.idle
name: Player Idle
- entity: media_player.playing
name: Player Playing
- entity: media_player.unavailable
name: Player Unavailable
- entity: media_player.unknown
name: Player Unknown
- entity: media_player.receiver_on
name: Receiver On (selectable sources)
- entity: media_player.receiver_off
name: Receiver Off (selectable sources)
`,
},
] satisfies DemoCardConfig<EntitiesCardConfig>[];
];
@customElement("demo-lovelace-media-player-row")
export class DemoLovelaceMediaPlayerRow extends LitElement {
+13 -15
View File
@@ -1,8 +1,6 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { PictureCardConfig } from "../../../../src/panels/lovelace/cards/types";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
@@ -21,26 +19,26 @@ const ENTITIES = [
const CONFIGS = [
{
heading: "Image URL",
config: {
type: "picture",
image: "/images/living_room.png",
},
config: `
- type: picture
image: /images/living_room.png
`,
},
{
heading: "Person entity",
config: {
type: "picture",
image_entity: "person.paulus",
},
config: `
- type: picture
image_entity: person.paulus
`,
},
{
heading: "Error: Image required",
config: {
type: "picture",
},
expectConfigError: true,
config: `
- type: picture
entity: person.paulus
`,
},
] satisfies DemoCardConfig<PictureCardConfig>[];
];
@customElement("demo-lovelace-picture-card")
class DemoPicture extends LitElement {
@@ -1,13 +1,7 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { PictureElementsCardConfig } from "../../../../src/panels/lovelace/cards/types";
import type {
ImageElementConfig,
LovelaceElementConfig,
} from "../../../../src/panels/lovelace/elements/types";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
@@ -66,141 +60,116 @@ const ENTITIES = [
},
];
type LegacyImageElementConfig = Omit<
ImageElementConfig,
"state_filter" | "state_image"
> & {
state_filter?: Record<string, string>;
state_image?: Record<string, string>;
};
type GalleryPictureElementsCardConfig = Omit<
PictureElementsCardConfig,
"elements"
> & {
type: PictureElementsCardConfig["type"];
elements: (LovelaceElementConfig | LegacyImageElementConfig)[];
};
const CONFIGS = [
{
heading: "Card with few elements",
config: {
type: "picture-elements",
image: "/images/floorplan.png",
elements: [
{
type: "service-button",
title: "Lights Off",
style: { top: "97%", left: "90%", padding: "0px" },
service: "light.turn_off",
data: { entity_id: "group.all_lights" },
},
{
type: "icon",
icon: "mdi:cctv",
entity: "camera.demo_camera",
style: {
top: "12%",
left: "6%",
transform: "rotate(-60deg) scaleX(-1)",
"--mdc-icon-size": "30px",
"--mdc-icon-stroke-color": "black",
"--mdc-icon-fill-color": "rgba(50, 50, 50, .75)",
},
},
{
type: "image",
entity: "light.bed_light",
tap_action: { action: "toggle" },
image: "/images/light_bulb_off.png",
state_image: { on: "/images/light_bulb_on.png" },
state_filter: {
on: "brightness(130%) saturate(1.5) drop-shadow(0px 0px 10px gold)",
off: "brightness(80%) saturate(0.8)",
},
style: {
top: "35%",
left: "65%",
width: "7%",
padding: "50px 50px 100px 50px",
},
},
{
type: "state-icon",
entity: "binary_sensor.movement_backyard",
style: { top: "8%", left: "35%" },
},
],
},
config: `
- type: picture-elements
image: /images/floorplan.png
elements:
- type: service-button
title: Lights Off
style:
top: 97%
left: 90%
padding: 0px
service: light.turn_off
data:
entity_id: group.all_lights
- type: icon
icon: mdi:cctv
entity: camera.demo_camera
style:
top: 12%
left: 6%
transform: rotate(-60deg) scaleX(-1)
--mdc-icon-size: 30px
--mdc-icon-stroke-color: black
--mdc-icon-fill-color: rgba(50, 50, 50, .75)
- type: image
entity: light.bed_light
tap_action:
action: toggle
image: /images/light_bulb_off.png
state_image:
'on': /images/light_bulb_on.png
state_filter:
'on': brightness(130%) saturate(1.5) drop-shadow(0px 0px 10px gold)
'off': brightness(80%) saturate(0.8)
style:
top: 35%
left: 65%
width: 7%
padding: 50px 50px 100px 50px
- type: state-icon
entity: binary_sensor.movement_backyard
style:
top: 8%
left: 35%
`,
},
{
heading: "Card with header",
config: {
type: "picture-elements",
image: "/images/floorplan.png",
title: "My House",
elements: [
{
type: "service-button",
title: "Lights Off",
style: { top: "97%", left: "90%", padding: "0px" },
service: "light.turn_off",
data: { entity_id: "group.all_lights" },
},
{
type: "icon",
icon: "mdi:cctv",
entity: "camera.demo_camera",
style: {
top: "12%",
left: "6%",
transform: "rotate(-60deg) scaleX(-1)",
"--mdc-icon-size": "30px",
"--mdc-icon-stroke-color": "black",
"--mdc-icon-fill-color": "rgba(50, 50, 50, .75)",
},
},
{
type: "image",
entity: "light.bed_light",
tap_action: { action: "toggle" },
image: "/images/light_bulb_off.png",
state_image: { on: "/images/light_bulb_on.png" },
state_filter: {
on: "brightness(130%) saturate(1.5) drop-shadow(0px 0px 10px gold)",
off: "brightness(80%) saturate(0.8)",
},
style: {
top: "35%",
left: "65%",
width: "7%",
padding: "50px 50px 100px 50px",
},
},
{
type: "state-icon",
entity: "binary_sensor.movement_backyard",
style: { top: "8%", left: "35%" },
},
],
},
config: `
- type: picture-elements
image: /images/floorplan.png
title: My House
elements:
- type: service-button
title: Lights Off
style:
top: 97%
left: 90%
padding: 0px
service: light.turn_off
data:
entity_id: group.all_lights
- type: icon
icon: mdi:cctv
entity: camera.demo_camera
style:
top: 12%
left: 6%
transform: rotate(-60deg) scaleX(-1)
--mdc-icon-size: 30px
--mdc-icon-stroke-color: black
--mdc-icon-fill-color: rgba(50, 50, 50, .75)
- type: image
entity: light.bed_light
tap_action:
action: toggle
image: /images/light_bulb_off.png
state_image:
'on': /images/light_bulb_on.png
state_filter:
'on': brightness(130%) saturate(1.5) drop-shadow(0px 0px 10px gold)
'off': brightness(80%) saturate(0.8)
style:
top: 35%
left: 65%
width: 7%
padding: 50px 50px 100px 50px
- type: state-icon
entity: binary_sensor.movement_backyard
style:
top: 8%
left: 35%
`,
},
{
heading: "Person entity",
config: {
type: "picture-elements",
image_entity: "person.paulus",
elements: [
{
type: "state-icon",
entity: "sensor.battery",
style: { top: "8%", left: "8%" },
},
],
},
config: `
- type: picture-elements
image_entity: person.paulus
elements:
- type: state-icon
entity: sensor.battery
style:
top: 8%
left: 8%
`,
},
] satisfies DemoCardConfig<GalleryPictureElementsCardConfig>[];
];
@customElement("demo-lovelace-picture-elements-card")
class DemoPictureElements extends LitElement {
@@ -1,9 +1,7 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { PictureEntityCardConfig } from "../../../../src/panels/lovelace/cards/types";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
@@ -35,73 +33,75 @@ const ENTITIES = [
const CONFIGS = [
{
heading: "State on",
config: {
type: "picture-entity",
image: "/images/kitchen.png",
entity: "light.kitchen_lights",
tap_action: { action: "toggle" },
},
config: `
- type: picture-entity
image: /images/kitchen.png
entity: light.kitchen_lights
tap_action:
action: toggle
`,
},
{
heading: "State off",
config: {
type: "picture-entity",
image: "/images/bed.png",
entity: "light.bed_light",
tap_action: { action: "toggle" },
},
config: `
- type: picture-entity
image: /images/bed.png
entity: light.bed_light
tap_action:
action: toggle
`,
},
{
heading: "Entity unavailable",
config: {
type: "picture-entity",
image: "/images/living_room.png",
entity: "light.non_existing",
},
config: `
- type: picture-entity
image: /images/living_room.png
entity: light.non_existing
`,
},
{
heading: "Camera entity",
config: {
type: "picture-entity",
entity: "camera.demo_camera",
},
config: `
- type: picture-entity
entity: camera.demo_camera
`,
},
{
heading: "Person entity",
config: {
type: "picture-entity",
entity: "person.paulus",
},
config: `
- type: picture-entity
entity: person.paulus
`,
},
{
heading: "Hidden name",
config: {
type: "picture-entity",
image: "/images/kitchen.png",
entity: "light.kitchen_lights",
show_name: false,
},
config: `
- type: picture-entity
image: /images/kitchen.png
entity: light.kitchen_lights
show_name: false
`,
},
{
heading: "Hidden state",
config: {
type: "picture-entity",
image: "/images/kitchen.png",
entity: "light.kitchen_lights",
show_state: false,
},
config: `
- type: picture-entity
image: /images/kitchen.png
entity: light.kitchen_lights
show_state: false
`,
},
{
heading: "Both hidden",
config: {
type: "picture-entity",
image: "/images/kitchen.png",
entity: "light.kitchen_lights",
show_name: false,
show_state: false,
},
config: `
- type: picture-entity
image: /images/kitchen.png
entity: light.kitchen_lights
show_name: false
show_state: false
`,
},
] satisfies DemoCardConfig<PictureEntityCardConfig>[];
];
@customElement("demo-lovelace-picture-entity-card")
class DemoPictureEntity extends LitElement {
@@ -1,9 +1,7 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { PictureGlanceCardConfig } from "../../../../src/panels/lovelace/cards/types";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
@@ -60,110 +58,110 @@ const ENTITIES = [
const CONFIGS = [
{
heading: "Title, dialog, toggle",
config: {
type: "picture-glance",
image: "/images/living_room.png",
title: "Living room",
entities: [
"switch.decorative_lights",
"light.ceiling_lights",
"binary_sensor.movement_backyard",
"binary_sensor.basement_floor_wet",
],
},
config: `
- type: picture-glance
image: /images/living_room.png
title: Living room
entities:
- switch.decorative_lights
- light.ceiling_lights
- binary_sensor.movement_backyard
- binary_sensor.basement_floor_wet
`,
},
{
heading: "Title, dialog, no toggle",
config: {
type: "picture-glance",
image: "/images/living_room.png",
title: "Living room",
entities: [
"binary_sensor.movement_backyard",
"binary_sensor.basement_floor_wet",
],
},
config: `
- type: picture-glance
image: /images/living_room.png
title: Living room
entities:
- binary_sensor.movement_backyard
- binary_sensor.basement_floor_wet
`,
},
{
heading: "Title, no dialog, toggle",
config: {
type: "picture-glance",
image: "/images/living_room.png",
title: "Living room",
entities: ["switch.decorative_lights", "light.ceiling_lights"],
},
config: `
- type: picture-glance
image: /images/living_room.png
title: Living room
entities:
- switch.decorative_lights
- light.ceiling_lights
`,
},
{
heading: "No title, dialog, toggle",
config: {
type: "picture-glance",
image: "/images/living_room.png",
entities: [
"switch.decorative_lights",
"light.ceiling_lights",
"binary_sensor.movement_backyard",
"binary_sensor.basement_floor_wet",
],
},
config: `
- type: picture-glance
image: /images/living_room.png
entities:
- switch.decorative_lights
- light.ceiling_lights
- binary_sensor.movement_backyard
- binary_sensor.basement_floor_wet
`,
},
{
heading: "No title, dialog, no toggle",
config: {
type: "picture-glance",
image: "/images/living_room.png",
entities: [
"binary_sensor.movement_backyard",
"binary_sensor.basement_floor_wet",
],
},
config: `
- type: picture-glance
image: /images/living_room.png
entities:
- binary_sensor.movement_backyard
- binary_sensor.basement_floor_wet
`,
},
{
heading: "No title, no dialog, toggle",
config: {
type: "picture-glance",
image: "/images/living_room.png",
entities: ["switch.decorative_lights", "light.ceiling_lights"],
},
config: `
- type: picture-glance
image: /images/living_room.png
entities:
- switch.decorative_lights
- light.ceiling_lights
`,
},
{
heading: "Person entity",
config: {
type: "picture-glance",
image_entity: "person.paulus",
entities: ["sensor.battery"],
},
config: `
- type: picture-glance
image_entity: person.paulus
entities:
- sensor.battery
`,
},
{
heading: "Custom icon",
config: {
type: "picture-glance",
image: "/images/living_room.png",
title: "Living room",
entities: [
{ entity: "switch.decorative_lights", icon: "mdi:power" },
"binary_sensor.basement_floor_wet",
],
},
config: `
- type: picture-glance
image: /images/living_room.png
title: Living room
entities:
- entity: switch.decorative_lights
icon: mdi:power
- binary_sensor.basement_floor_wet
`,
},
{
heading: "Custom tap action",
config: {
type: "picture-glance",
image: "/images/living_room.png",
title: "Living room",
entity: "light.ceiling_lights",
tap_action: { action: "toggle" },
entities: [
{
entity: "switch.decorative_lights",
icon: "mdi:power",
tap_action: { action: "toggle" },
},
"binary_sensor.basement_floor_wet",
],
},
config: `
- type: picture-glance
image: /images/living_room.png
title: Living room
entity: light.ceiling_lights
tap_action:
action: toggle
entities:
- entity: switch.decorative_lights
icon: mdi:power
tap_action:
action: toggle
- binary_sensor.basement_floor_wet
`,
},
] satisfies DemoCardConfig<PictureGlanceCardConfig>[];
];
@customElement("demo-lovelace-picture-glance-card")
class DemoPictureGlance extends LitElement {
+14 -16
View File
@@ -1,8 +1,6 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { PlantStatusCardConfig } from "../../../../src/panels/lovelace/cards/types";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards";
import { createPlantEntities } from "../../data/plants";
@@ -11,27 +9,27 @@ import { mockIcons } from "../../../../demo/src/stubs/icons";
const CONFIGS = [
{
heading: "Basic example",
config: {
type: "plant-status",
entity: "plant.lemon_tree",
},
config: `
- type: plant-status
entity: plant.lemon_tree
`,
},
{
heading: "Problem (too bright) + low battery",
config: {
type: "plant-status",
entity: "plant.apple_tree",
},
config: `
- type: plant-status
entity: plant.apple_tree
`,
},
{
heading: "With picture + multiple problems",
config: {
type: "plant-status",
entity: "plant.sunflowers",
name: "Sunflowers Name Overwrite",
},
config: `
- type: plant-status
entity: plant.sunflowers
name: Sunflowers Name Overwrite
`,
},
] satisfies DemoCardConfig<PlantStatusCardConfig>[];
];
@customElement("demo-lovelace-plant-card")
export class DemoPlantEntity extends LitElement {
+95 -108
View File
@@ -1,9 +1,7 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { DemoCardConfig } from "../../components/demo-card";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { ThermostatCardConfig } from "../../../../src/panels/lovelace/cards/types";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
@@ -125,131 +123,120 @@ const ENTITIES = [
const CONFIGS = [
{
heading: "Range example",
config: {
type: "thermostat",
entity: "climate.ecobee",
},
config: `
- type: thermostat
entity: climate.ecobee
`,
},
{
heading: "Single temp example",
config: {
type: "thermostat",
entity: "climate.nest",
},
config: `
- type: thermostat
entity: climate.nest
`,
},
{
heading: "Feature example",
config: {
type: "thermostat",
entity: "climate.overkiz_radiator",
features: [
{
type: "climate-hvac-modes",
hvac_modes: ["heat", "off", "auto"],
},
{
type: "climate-preset-modes",
style: "icons",
preset_modes: [
"none",
"frost_protection",
"eco",
"comfort",
"comfort-1",
"comfort-2",
"auto",
"boost",
"external",
"prog",
],
},
{
type: "climate-preset-modes",
style: "dropdown",
preset_modes: [
"none",
"frost_protection",
"eco",
"comfort",
"comfort-1",
"comfort-2",
"auto",
"boost",
"external",
"prog",
],
},
],
},
config: `
- type: thermostat
entity: climate.overkiz_radiator
features:
- type: climate-hvac-modes
hvac_modes:
- heat
- 'off'
- auto
- type: climate-preset-modes
style: icons
preset_modes:
- none
- frost_protection
- eco
- comfort
- comfort-1
- comfort-2
- auto
- boost
- external
- prog
- type: climate-preset-modes
style: dropdown
preset_modes:
- none
- frost_protection
- eco
- comfort
- comfort-1
- comfort-2
- auto
- boost
- external
- prog
`,
},
{
heading: "Preset only example",
config: {
type: "thermostat",
entity: "climate.overkiz_towel_dryer",
features: [
{
type: "climate-hvac-modes",
hvac_modes: ["heat", "off"],
},
{
type: "climate-preset-modes",
style: "icons",
preset_modes: [
"none",
"frost_protection",
"eco",
"comfort",
"comfort-1",
"comfort-2",
],
},
],
},
config: `
- type: thermostat
entity: climate.overkiz_towel_dryer
features:
- type: climate-hvac-modes
hvac_modes:
- heat
- 'off'
- type: climate-preset-modes
style: icons
preset_modes:
- none
- frost_protection
- eco
- comfort
- comfort-1
- comfort-2
`,
},
{
heading: "Fan only example",
config: {
type: "thermostat",
entity: "climate.sensibo",
features: [
{
type: "climate-hvac-modes",
hvac_modes: ["fan_only", "off"],
},
{
type: "climate-fan-modes",
style: "icons",
fan_modes: ["low", "high"],
},
{
type: "climate-swing-modes",
style: "icons",
swing_modes: ["both", "rangefull", "off"],
},
{
type: "climate-swing-horizontal-modes",
style: "icons",
swing_horizontal_modes: ["both", "rangefull", "off"],
},
],
},
config: `
- type: thermostat
entity: climate.sensibo
features:
- type: climate-hvac-modes
hvac_modes:
- fan_only
- 'off'
- type: climate-fan-modes
style: icons
fan_modes:
- low
- high
- type: climate-swing-modes
style: icons
swing_modes:
- 'both'
- 'rangefull'
- 'off'
swing_horizontal_modes:
- 'both'
- 'rangefull'
- 'off'
`,
},
{
heading: "Unavailable",
config: {
type: "thermostat",
entity: "climate.unavailable",
},
config: `
- type: thermostat
entity: climate.unavailable
`,
},
{
heading: "Non existing",
config: {
type: "thermostat",
entity: "climate.nonexisting",
},
config: `
- type: thermostat
entity: climate.nonexisting
`,
},
] satisfies DemoCardConfig<ThermostatCardConfig>[];
];
@customElement("demo-lovelace-thermostat-card")
class DemoThermostatEntity extends LitElement {
+123 -114
View File
@@ -1,7 +1,6 @@
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, query } from "lit/decorators";
import type { DemoCardConfig } from "../../components/demo-card";
import { CoverEntityFeature } from "../../../../src/data/cover";
import { LightColorMode } from "../../../../src/data/light";
import { LockEntityFeature } from "../../../../src/data/lock";
@@ -12,7 +11,6 @@ import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
import { ClimateEntityFeature } from "../../../../src/data/climate";
import { FanEntityFeature } from "../../../../src/data/fan";
import type { TileCardConfig } from "../../../../src/panels/lovelace/cards/types";
const ENTITIES = [
{
@@ -168,179 +166,190 @@ const ENTITIES = [
const CONFIGS = [
{
heading: "Basic example",
config: {
type: "tile",
entity: "switch.tv_outlet",
},
config: `
- type: tile
entity: switch.tv_outlet
`,
},
{
heading: "Vertical example",
config: {
type: "tile",
entity: "switch.tv_outlet",
vertical: true,
},
config: `
- type: tile
entity: switch.tv_outlet
vertical: true
`,
},
{
heading: "Custom color",
config: {
type: "tile",
entity: "switch.tv_outlet",
color: "pink",
},
config: `
- type: tile
entity: switch.tv_outlet
color: pink
`,
},
{
heading: "Whole tile tap action",
config: {
type: "tile",
entity: "switch.tv_outlet",
color: "pink",
tap_action: {
action: "toggle",
},
icon_tap_action: {
action: "none",
},
},
config: `
- type: tile
entity: switch.tv_outlet
color: pink
tap_action:
action: toggle
icon_tap_action:
action: none
`,
},
{
heading: "Unknown entity",
config: {
type: "tile",
entity: "light.unknown",
},
config: `
- type: tile
entity: light.unknown
`,
},
{
heading: "Unavailable entity",
config: {
type: "tile",
entity: "light.unavailable",
},
config: `
- type: tile
entity: light.unavailable
`,
},
{
heading: "Climate",
config: {
type: "tile",
entity: "climate.thermostat",
},
config: `
- type: tile
entity: climate.thermostat
`,
},
{
heading: "Person",
config: {
type: "tile",
entity: "person.paulus",
},
config: `
- type: tile
entity: person.paulus
`,
},
{
heading: "Light brightness feature",
config: {
type: "tile",
entity: "light.bed_light",
features: [{ type: "light-brightness" }],
},
config: `
- type: tile
entity: light.bed_light
features:
- type: "light-brightness"
`,
},
{
heading: "Light color temperature feature",
config: {
type: "tile",
entity: "light.bed_light",
features: [{ type: "light-color-temp" }],
},
config: `
- type: tile
entity: light.bed_light
features:
- type: "color-temp"
`,
},
{
heading: "Lock commands feature",
config: {
type: "tile",
entity: "lock.front_door",
features: [{ type: "lock-commands" }],
},
config: `
- type: tile
entity: lock.front_door
features:
- type: "lock-commands"
`,
},
{
heading: "Lock open door feature",
config: {
type: "tile",
entity: "lock.front_door",
features: [{ type: "lock-open-door" }],
},
config: `
- type: tile
entity: lock.front_door
features:
- type: "lock-open-door"
`,
},
{
heading: "Media player volume slider feature",
config: {
type: "tile",
entity: "media_player.living_room",
features: [{ type: "media-player-volume-slider" }],
},
config: `
- type: tile
entity: media_player.living_room
features:
- type: "media-player-volume-slider"
`,
},
{
heading: "Vacuum commands feature",
config: {
type: "tile",
entity: "vacuum.first_floor_vacuum",
features: [
{
type: "vacuum-commands",
commands: ["start_pause", "stop", "return_home"],
},
],
},
config: `
- type: tile
entity: vacuum.first_floor_vacuum
features:
- type: "vacuum-commands"
commands:
- start_pause
- stop
- return_home
`,
},
{
heading: "Cover open close feature",
config: {
type: "tile",
entity: "cover.kitchen_shutter",
features: [{ type: "cover-open-close" }],
},
config: `
- type: tile
entity: cover.kitchen_shutter
features:
- type: "cover-open-close"
`,
},
{
heading: "Cover tilt feature",
config: {
type: "tile",
entity: "cover.pergola_roof",
features: [{ type: "cover-tilt" }],
},
config: `
- type: tile
entity: cover.pergola_roof
features:
- type: "cover-tilt"
`,
},
{
heading: "Number buttons feature",
config: {
type: "tile",
entity: "input_number.counter",
features: [{ type: "numeric-input", style: "buttons" }],
},
config: `
- type: tile
entity: input_number.counter
features:
- type: numeric-input
style: buttons
`,
},
{
heading: "Dual thermostat feature",
config: {
type: "tile",
entity: "climate.dual_thermostat",
features: [{ type: "target-temperature" }],
},
config: `
- type: tile
entity: climate.dual_thermostat
features:
- type: target-temperature
`,
},
{
heading: "Fan direction feature",
config: {
type: "tile",
entity: "fan.fan_demo",
features: [{ type: "fan-direction" }],
},
config: `
- type: tile
entity: fan.fan_demo
features:
- type: fan-direction
`,
},
{
heading: "Fan speed feature",
config: {
type: "tile",
entity: "fan.fan_demo",
features: [{ type: "fan-speed" }],
},
config: `
- type: tile
entity: fan.fan_demo
features:
- type: fan-speed
`,
},
{
heading: "Fan oscillate feature",
config: {
type: "tile",
entity: "fan.fan_demo",
features: [{ type: "fan-oscillate" }],
},
config: `
- type: tile
entity: fan.fan_demo
features:
- type: fan-oscillate
`,
},
] satisfies DemoCardConfig<TileCardConfig>[];
];
@customElement("demo-lovelace-tile-card")
class DemoTile extends LitElement {
+10 -12
View File
@@ -4,8 +4,6 @@ import { customElement, query } from "lit/decorators";
import { mockIcons } from "../../../../demo/src/stubs/icons";
import { mockTodo } from "../../../../demo/src/stubs/todo";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { TodoListCardConfig } from "../../../../src/panels/lovelace/cards/types";
import type { DemoCardConfig } from "../../components/demo-card";
import "../../components/demo-cards";
const ENTITIES = [
@@ -29,20 +27,20 @@ const ENTITIES = [
const CONFIGS = [
{
heading: "List example",
config: {
type: "todo-list",
entity: "todo.shopping_list",
},
config: `
- type: todo-list
entity: todo.shopping_list
`,
},
{
heading: "List with title example",
config: {
type: "todo-list",
title: "Shopping List",
entity: "todo.read_only",
},
config: `
- type: todo-list
title: Shopping List
entity: todo.read_only
`,
},
] satisfies DemoCardConfig<TodoListCardConfig>[];
];
@customElement("demo-lovelace-todo-list-card")
class DemoTodoListEntity extends LitElement {
+3 -2
View File
@@ -147,8 +147,8 @@
"@gfx/zopfli": "1.0.15",
"@html-eslint/eslint-plugin": "0.64.0",
"@lokalise/node-api": "16.3.0",
"@octokit/auth-oauth-device": "8.0.4",
"@octokit/plugin-retry": "8.1.1",
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.1.0",
"@octokit/rest": "22.0.1",
"@playwright/test": "1.62.1",
"@rsdoctor/rspack-plugin": "1.6.1",
@@ -188,6 +188,7 @@
"glob": "13.0.6",
"globals": "17.8.0",
"gulp": "5.0.1",
"gulp-brotli": "3.0.0",
"gulp-json-transform": "0.5.0",
"gulp-rename": "2.1.0",
"html-minifier-terser": "7.2.0",
+4 -2
View File
@@ -1,8 +1,9 @@
import type { ReactiveElement } from "lit";
import { getHistoryState, updateHistoryState } from "../navigate";
import { throttle } from "../util/throttle";
const throttleReplaceState = throttle((value) => {
history.replaceState({ scrollPosition: value }, "");
updateHistoryState({ scrollPosition: value });
}, 300);
export function restoreScroll(selector: string) {
@@ -39,7 +40,8 @@ export function restoreScroll(selector: string) {
newDescriptor = {
get(this: ReactiveElement) {
return (
this[`__${String(propertyKey)}`] || history.state?.scrollPosition
this[`__${String(propertyKey)}`] ||
getHistoryState()?.scrollPosition
);
},
set(this: ReactiveElement, value) {
+77 -39
View File
@@ -1,6 +1,7 @@
import { closeAllDialogs } from "../dialogs/make-dialog-manager";
import { fireEvent } from "./dom/fire_event";
import { mainWindow } from "./dom/get_main_window";
import { currentPath } from "./url/current-path";
declare global {
// for fire event
@@ -17,6 +18,32 @@ export interface NavigateOptions {
// max time to wait for dialogs to close before navigating
const DIALOG_WAIT_TIMEOUT = 500;
/**
* State of the current history entry. Always read through this, the app writes
* to the main window and a panel running in an iframe has its own history.
*/
export const getHistoryState = (): any => mainWindow.history.state;
/**
* Merge into the current history entry's state, keeping what is already there.
* Entries carry the app's own bookkeeping (`from`, `root`, dialog state), so
* they must never be replaced wholesale.
*/
export const updateHistoryState = (patch: Record<string, unknown>) => {
mainWindow.history.replaceState(
{ ...mainWindow.history.state, ...patch },
""
);
};
/**
* Rewrite the URL of the current history entry without navigating and without
* touching its state. For query parameter cleanup.
*/
export const replaceCurrentUrl = (url: string) => {
mainWindow.history.replaceState(mainWindow.history.state, "", url);
};
/**
* Stash a destination URL in the current history entry's state. If the page
* is refreshed while a dialog is open, urlSyncMixin will navigate to this URL
@@ -24,10 +51,7 @@ const DIALOG_WAIT_TIMEOUT = 500;
* The current URL is not changed.
*/
export const setRefreshUrl = (path: string) => {
mainWindow.history.replaceState(
{ ...mainWindow.history.state, refreshUrl: path },
""
);
updateHistoryState({ refreshUrl: path });
};
/**
@@ -63,37 +87,44 @@ export const navigate = async (path: string, options?: NavigateOptions) => {
}
const replace = options?.replace || false;
if (__DEMO__) {
if (!path.includes("#")) {
// The demo routes with the hash instead of the pathname. Resolve the
// path like the browser would do for pushState, and keep the query
// parameters in the URL query instead of inside the hash.
const url = new URL(
path,
`${mainWindow.location.origin}${mainWindow.location.hash.substring(1)}`
);
path = `${mainWindow.location.pathname}${url.search}#${url.pathname}`;
}
if (replace) {
mainWindow.history.replaceState(
mainWindow.history.state?.root
? { root: true }
: (options?.data ?? null),
"",
path
);
} else {
mainWindow.history.pushState(options?.data ?? null, "", path);
}
} else if (replace) {
mainWindow.history.replaceState(
mainWindow.history.state?.root ? { root: true } : (options?.data ?? null),
if (__DEMO__ && !path.includes("#")) {
// The demo routes with the hash instead of the pathname. Resolve the
// path like the browser would do for pushState, and keep the query
// parameters in the URL query instead of inside the hash.
const url = new URL(
path,
`${mainWindow.location.origin}${mainWindow.location.hash.substring(1)}`
);
path = `${mainWindow.location.pathname}${url.search}#${url.pathname}`;
}
const { history } = mainWindow;
if (replace) {
// A replaced entry keeps its predecessor, so it keeps `from`.
const { root, from } = history.state ?? {};
const state = root ? { root: true } : (options?.data ?? null);
history.replaceState(
from === undefined ? state : { ...state, from },
"",
path
);
} else {
mainWindow.history.pushState(options?.data ?? null, "", path);
const { data } = options ?? {};
// Only objects survive being merged with `from`. Callers outside the app
// pass whatever they want, so keep anything else under a key rather than
// letting the spread mangle it.
const mergeable =
data === undefined || (typeof data === "object" && data !== null);
history.pushState(
mergeable
? { ...data, from: currentPath() }
: { data, from: currentPath() },
"",
path
);
}
fireEvent(mainWindow, "location-changed", {
replace,
});
@@ -101,8 +132,17 @@ export const navigate = async (path: string, options?: NavigateOptions) => {
};
/**
* Navigate back in history, with fallback to a default path if no history exists.
* This prevents a user from getting stuck when they navigate directly to a page with no history.
* Whether the previous history entry is a page this app navigated away from.
* `history.length` cannot answer this: a login redirect goes through
* `location.assign`, which leaves /auth/authorize right behind the requested
* page, and going back there would bounce the user out of the app.
*/
export const canGoBack = (): boolean =>
mainWindow.history.state?.from !== undefined;
/**
* Navigate back to the page we came from, falling back to a path when the
* previous entry is not ours (deep link, login redirect, fresh tab).
*/
export const goBack = async (fallbackPath?: string): Promise<void> => {
const canProceed = await ensureDialogsClosed(Date.now());
@@ -110,14 +150,12 @@ export const goBack = async (fallbackPath?: string): Promise<void> => {
return;
}
// Check if we have history to go back to
const { history } = mainWindow;
if (history.length > 1) {
history.back();
// Read after closing dialogs: their history entries are popped by then, so
// this is the state of the page entry.
if (canGoBack()) {
mainWindow.history.back();
return;
}
// No history available, navigate to fallback path
const fallback = fallbackPath || "/";
navigate(fallback, { replace: true });
await navigate(fallbackPath || "/", { replace: true });
};
-13
View File
@@ -1,13 +0,0 @@
// RFC 1123 hostname label: 1-63 chars, alphanumeric, with hyphens allowed
// only between the first and last character. The hyphen is escaped because a
// `pattern` attribute is compiled with the `v` flag, under which a trailing
// unescaped hyphen is an invalid character class — and a pattern that fails to
// compile is silently ignored, disabling validation altogether.
const LABEL = "[a-zA-Z0-9](?:[a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?";
// Hostname such as "localhost" or "homeassistant.lan", as dot-separated
// labels. Unanchored, for use as an HTML `pattern` attribute (the browser
// anchors it as `^(?:…)$`). The final label may not be all digits, so a
// mistyped IP address like "300.1.1.1" is rejected rather than accepted as a
// hostname. Deliberately excludes underscores and a trailing dot.
export const HOSTNAME_PATTERN = `(?:${LABEL}\\.)*(?!\\d+$)${LABEL}`;
+10
View File
@@ -0,0 +1,10 @@
import { mainWindow } from "../dom/get_main_window";
/**
* The path of the page currently shown by the app. The demo routes with the
* hash instead of the pathname, see navigate().
*/
export const currentPath = (): string =>
__DEMO__
? mainWindow.location.hash.substring(1)
: mainWindow.location.pathname;
+6 -5
View File
@@ -53,10 +53,8 @@ export class HaControlNumberButton extends LitElement {
});
private _boundedValue(value: number) {
// Clamp after snapping: when the step does not divide the range evenly,
// snapping alone rounds past the bounds (min 1, max 99, step 10 → 0 / 100).
const stepped = Math.round(value / this._step) * this._step;
return conditionalClamp(stepped, this.min, this.max);
const clamped = conditionalClamp(value, this.min, this.max);
return Math.round(clamped / this._step) * this._step;
}
private get _step() {
@@ -69,7 +67,10 @@ export class HaControlNumberButton extends LitElement {
private get _tenPercentStep() {
if (this.max == null || this.min == null) return this._step;
return Math.max((this.max - this.min) / 10, this._step);
const range = this.max - this.min / 10;
if (range <= this._step) return this._step;
return Math.max(range / 10);
}
private _handlePlusButton() {
+7 -5
View File
@@ -115,9 +115,7 @@ export class HaControlSlider extends LitElement {
}
steppedValue(value: number) {
// Clamp after snapping: when the step does not divide the range evenly,
// snapping alone rounds past the bounds (min 1, max 99, step 10 → 0 / 100).
return this.boundedValue(Math.round(value / this.step) * this.step);
return Math.round(value / this.step) * this.step;
}
private _displayedValue(value: number) {
@@ -256,9 +254,13 @@ export class HaControlSlider extends LitElement {
} else if (e.code === "End") {
this.value = this.max;
} else if (e.code === "PageUp") {
this.value = this.steppedValue((this.value ?? 0) + this._tenPercentStep);
this.value = this.steppedValue(
this.boundedValue((this.value ?? 0) + this._tenPercentStep)
);
} else if (e.code === "PageDown") {
this.value = this.steppedValue((this.value ?? 0) - this._tenPercentStep);
this.value = this.steppedValue(
this.boundedValue((this.value ?? 0) - this._tenPercentStep)
);
} else {
const isRtl = mainWindow.document.dir === "rtl";
let multiplier = 1;
@@ -131,7 +131,6 @@ export class HaNumberSelector extends LitElement {
.hint=${isBox ? this.helper : undefined}
.disabled=${this.disabled}
.required=${this.required}
.validationMessage=${this.selector.number?.validation_message}
type="number"
autoValidate
.withoutSpinButtons=${!isBox}
+3 -27
View File
@@ -31,8 +31,6 @@ export class HaToast extends LitElement {
@property({ type: Number, attribute: "bottom-offset" }) public bottomOffset =
0;
@property({ type: Boolean }) public stacked = false;
@query(".toast")
private _toast?: HTMLDivElement;
@@ -150,12 +148,7 @@ export class HaToast extends LitElement {
}
private _showToastPopover(): void {
if (
!this._toast ||
this.stacked ||
!popoverSupported ||
this._isPopoverOpen()
) {
if (!this._toast || !popoverSupported || this._isPopoverOpen()) {
return;
}
@@ -163,12 +156,7 @@ export class HaToast extends LitElement {
}
private _hideToastPopover(): void {
if (
!this._toast ||
this.stacked ||
!popoverSupported ||
!this._isPopoverOpen()
) {
if (!this._toast || !popoverSupported || !this._isPopoverOpen()) {
return;
}
@@ -199,15 +187,12 @@ export class HaToast extends LitElement {
class=${classMap({
toast: true,
active: this._active,
stacked: this.stacked,
visible: this._visible,
})}
style=${styleMap({
"--ha-toast-bottom-offset": `${this.bottomOffset}px`,
})}
popover=${ifDefined(
popoverSupported && !this.stacked ? "manual" : undefined
)}
popover=${ifDefined(popoverSupported ? "manual" : undefined)}
>
<span class="message">${this.labelText}</span>
<div class=${classMap({ actions: true, "has-action": hasAction })}>
@@ -268,15 +253,6 @@ export class HaToast extends LitElement {
transform: translate(calc(-50% * var(--scale-direction)), 0);
}
.toast.stacked {
position: static;
transform: translateY(var(--ha-space-2));
}
.toast.stacked.visible {
transform: translateY(0);
}
.toast:not(.active) {
display: none;
}
-3
View File
@@ -397,9 +397,6 @@ export interface NumberSelector {
unit_of_measurement?: string;
slider_ticks?: boolean;
translation_key?: string;
// Shown instead of the browser's native message when the value fails
// min/max/step constraint validation.
validation_message?: string;
} | null;
}
+10 -10
View File
@@ -40,7 +40,11 @@ import {
getEntityEntryContext,
} from "../../common/entity/context/get_entity_context";
import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event";
import { navigate } from "../../common/navigate";
import {
getHistoryState,
navigate,
updateHistoryState,
} from "../../common/navigate";
import type { LocalizeKeys } from "../../common/translations/localize";
import { computeRTL } from "../../common/util/compute_rtl";
import { withViewTransition } from "../../common/util/view-transition";
@@ -268,16 +272,12 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
}
private _setView(view: MoreInfoView) {
history.replaceState(
{
...history.state,
dialogParams: {
...history.state?.dialogParams,
view,
},
updateHistoryState({
dialogParams: {
...getHistoryState()?.dialogParams,
view,
},
""
);
});
this._currView = view;
}
+29
View File
@@ -0,0 +1,29 @@
import { isNavigationClick } from "../common/dom/is-navigation-click";
import { goBack } from "../common/navigate";
import { sanitizeNavigationPath } from "../common/url/sanitize-navigation-path";
/**
* Shared behavior of the toolbar back arrow. The arrow is a link to the
* declared parent page so it can be opened in a new tab, but a plain click
* returns to the page the user came from instead.
*/
export const handleBackClick = (
ev: MouseEvent,
backPath?: string,
backCallback?: () => void
): void => {
const path = sanitizeNavigationPath(backPath);
// Middle click, ctrl and cmd open the parent in a new tab, let the anchor
// handle those.
if (path && !isNavigationClick(ev)) {
return;
}
if (backCallback) {
backCallback();
return;
}
goBack(path);
};
+10 -19
View File
@@ -4,8 +4,8 @@ import { customElement, eventOptions, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { restoreScroll } from "../common/decorators/restore-scroll";
import type { HASSDomTargetEvent } from "../common/dom/fire_event";
import { goBack } from "../common/navigate";
import { sanitizeNavigationPath } from "../common/url/sanitize-navigation-path";
import { handleBackClick } from "./back-navigation";
import "../components/ha-icon-button-arrow-prev";
import "../components/ha-menu-button";
import { haStyleScrollbar } from "../resources/styles";
@@ -37,19 +37,14 @@ class HassSubpage extends LitElement {
<div class="toolbar ${classMap({ narrow: this.narrow })}">
<div class="toolbar-content">
${
this.mainPage || history.state?.root
this.mainPage || (!backPath && history.state?.root)
? html`<ha-menu-button></ha-menu-button>`
: backPath
? html`
<ha-icon-button-arrow-prev
href=${backPath}
></ha-icon-button-arrow-prev>
`
: html`
<ha-icon-button-arrow-prev
@click=${this._backTapped}
></ha-icon-button-arrow-prev>
`
: html`
<ha-icon-button-arrow-prev
.href=${backPath}
@click=${this._backTapped}
></ha-icon-button-arrow-prev>
`
}
<div class="main-title">
@@ -79,12 +74,8 @@ class HassSubpage extends LitElement {
this._savedScrollPos = (e.target as HTMLDivElement).scrollTop;
}
private _backTapped(): void {
if (this.backCallback) {
this.backCallback();
return;
}
goBack();
private _backTapped(ev: MouseEvent): void {
handleBackClick(ev, this.backPath, this.backCallback);
}
static get styles(): CSSResultGroup {
+10 -18
View File
@@ -14,9 +14,10 @@ import { canShowPage } from "../common/config/can_show_page";
import { restoreScroll } from "../common/decorators/restore-scroll";
import type { HASSDomTargetEvent } from "../common/dom/fire_event";
import { isNavigationClick } from "../common/dom/is-navigation-click";
import { goBack, navigate } from "../common/navigate";
import { navigate } from "../common/navigate";
import type { LocalizeFunc } from "../common/translations/localize";
import { sanitizeNavigationPath } from "../common/url/sanitize-navigation-path";
import { handleBackClick } from "./back-navigation";
import "../components/ha-icon-button-arrow-prev";
import "../components/ha-menu-button";
import "../components/ha-svg-icon";
@@ -175,17 +176,12 @@ export class HassTabsSubpage extends LitElement {
${
this.mainPage || (!backPath && history.state?.root)
? html`<ha-menu-button></ha-menu-button>`
: backPath
? html`
<ha-icon-button-arrow-prev
.href=${backPath}
></ha-icon-button-arrow-prev>
`
: html`
<ha-icon-button-arrow-prev
@click=${this._backTapped}
></ha-icon-button-arrow-prev>
`
: html`
<ha-icon-button-arrow-prev
.href=${backPath}
@click=${this._backTapped}
></ha-icon-button-arrow-prev>
`
}
${
this._narrow || !this.showTabs
@@ -246,12 +242,8 @@ export class HassTabsSubpage extends LitElement {
this._content.focus({ preventScroll: true });
}
private _backTapped(): void {
if (this.backCallback) {
this.backCallback();
return;
}
goBack();
private _backTapped(ev: MouseEvent): void {
handleBackClick(ev, this.backPath, this.backCallback);
}
private _isActiveTabPath(tabPath: string, currentPath: string): boolean {
+82 -221
View File
@@ -1,20 +1,7 @@
import { mdiClose } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { ifDefined } from "lit/directives/if-defined";
import { repeat } from "lit/directives/repeat";
import { styleMap } from "lit/directives/style-map";
import {
customElement,
property,
query,
queryAll,
state,
} from "lit/decorators";
import type {
HASSDomCurrentTargetEvent,
HASSDomEvent,
} from "../common/dom/fire_event";
import { popoverSupported } from "../common/feature-detect/support-popover";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import type { HASSDomEvent } from "../common/dom/fire_event";
import type { LocalizeKeys } from "../common/translations/localize";
import "../components/ha-button";
import "../components/ha-icon-button";
@@ -23,7 +10,7 @@ import type { ToastClosedEventDetail } from "../components/ha-toast";
import type { HomeAssistant } from "../types";
export interface ShowToastParams {
// Unique ID for updating or closing a specific toast without flickering.
// Unique ID for the toast. If a new toast is shown with the same ID as the previous toast, it will be replaced to avoid flickering.
id?: string;
message:
| string
@@ -47,150 +34,105 @@ export interface ToastActionParams {
| { translationKey: LocalizeKeys; args?: Record<string, string | number> };
}
interface Notification {
key: string;
parameters: ShowToastParams;
}
@customElement("notification-manager")
class NotificationManager extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _notifications: Notification[] = [];
@state() private _parameters?: ShowToastParams;
@query(".stack") private _stack?: HTMLDivElement;
@query("ha-toast")
private _toast!: HTMLElementTagNameMap["ha-toast"] | undefined;
@queryAll("ha-toast")
private _toasts!: NodeListOf<HTMLElementTagNameMap["ha-toast"]>;
private _anonymousId = 0;
private _showDialogId = 0;
public async showDialog(parameters: ShowToastParams) {
if (parameters.duration === 0) {
if (parameters.id) {
await this._closeNotification(`identified-${parameters.id}`);
} else {
const notification = [...this._notifications]
.reverse()
.find(({ parameters: { id } }) => !id);
if (notification) {
await this._closeNotification(notification.key);
}
}
const showId = ++this._showDialogId;
if (!parameters.id || this._parameters?.id !== parameters.id) {
await this._toast?.hide();
}
if (showId !== this._showDialogId) {
return;
}
const normalizedParameters = {
...parameters,
duration:
parameters.duration === undefined ||
(parameters.duration > 0 && parameters.duration <= 4000)
? 4000
: parameters.duration,
};
const key = parameters.id
? `identified-${parameters.id}`
: `anonymous-${++this._anonymousId}`;
const existingIndex = parameters.id
? this._notifications.findIndex(
(notification) => notification.key === key
)
: -1;
if (parameters.duration === 0) {
this._parameters = undefined;
return;
}
this._notifications =
existingIndex === -1
? [...this._notifications, { key, parameters: normalizedParameters }]
: this._notifications.map((notification, index) =>
index === existingIndex
? { key, parameters: normalizedParameters }
: notification
);
this._parameters = parameters;
if (
this._parameters.duration === undefined ||
(this._parameters.duration > 0 && this._parameters.duration <= 4000)
) {
this._parameters.duration = 4000;
}
await this.updateComplete;
this._showStack();
this._getToast(key)?.show();
if (showId !== this._showDialogId) {
return;
}
this._toast?.show();
}
private _toastClosed(
ev: HASSDomEvent<ToastClosedEventDetail> &
HASSDomCurrentTargetEvent<HTMLElementTagNameMap["ha-toast"]>
) {
const key = ev.currentTarget.dataset.notificationKey!;
private _toastClosed(ev: HASSDomEvent<ToastClosedEventDetail>) {
if (ev.detail.reason === "dismiss") {
this._getNotification(key)?.parameters.dismiss?.();
this._parameters?.dismiss?.();
}
this._removeNotification(key);
this._parameters = undefined;
}
protected render() {
if (!this._notifications.length) {
if (!this._parameters) {
return nothing;
}
const bottomOffset = Math.max(
...this._notifications.map(
({ parameters }) => parameters.bottomOffset ?? 0
)
);
return html`
<div
class="stack"
style=${styleMap({
"--notification-stack-bottom-offset": `${bottomOffset}px`,
})}
popover=${ifDefined(popoverSupported ? "manual" : undefined)}
<ha-toast
.labelText=${
typeof this._parameters.message !== "string"
? this.hass.localize(
this._parameters.message.translationKey,
this._parameters.message.args
)
: this._parameters.message
}
.announceText=${
this._parameters.announceMessage
? typeof this._parameters.announceMessage !== "string"
? this.hass.localize(
this._parameters.announceMessage.translationKey,
this._parameters.announceMessage.args
)
: this._parameters.announceMessage
: undefined
}
.timeoutMs=${this._parameters.duration!}
.bottomOffset=${this._parameters.bottomOffset ?? 0}
@toast-closed=${this._toastClosed}
>
${repeat(
this._notifications,
(notification) => notification.key,
({ key, parameters }) => html`
<ha-toast
data-notification-key=${key}
.labelText=${
typeof parameters.message !== "string"
? this.hass.localize(
parameters.message.translationKey,
parameters.message.args
)
: parameters.message
}
.announceText=${
parameters.announceMessage
? typeof parameters.announceMessage !== "string"
? this.hass.localize(
parameters.announceMessage.translationKey,
parameters.announceMessage.args
)
: parameters.announceMessage
: undefined
}
.timeoutMs=${parameters.duration!}
.stacked=${true}
@toast-closed=${this._toastClosed}
>
${this._renderAction(key, parameters.secondaryAction, true)}
${this._renderAction(key, parameters.action, false)}
${
parameters.dismissable
? html`
<ha-icon-button
data-notification-key=${key}
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
slot="dismiss"
@click=${this._dismissClicked}
></ha-icon-button>
`
: nothing
}
</ha-toast>
`
)}
</div>
${this._renderAction(this._parameters.secondaryAction, true)}
${this._renderAction(this._parameters.action, false)}
${
this._parameters?.dismissable
? html`
<ha-icon-button
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
slot="dismiss"
@click=${this._dismissClicked}
></ha-icon-button>
`
: nothing
}
</ha-toast>
`;
}
private _renderAction(
key: string,
action: ToastActionParams | undefined,
secondary: boolean
) {
@@ -199,7 +141,6 @@ class NotificationManager extends LitElement {
}
return html`
<ha-button
data-notification-key=${key}
appearance=${action.primary ? "filled" : "plain"}
size="s"
slot="action"
@@ -214,99 +155,19 @@ class NotificationManager extends LitElement {
`;
}
private _buttonClicked(
ev: HASSDomCurrentTargetEvent<HTMLElementTagNameMap["ha-button"]>
) {
const key = ev.currentTarget.dataset.notificationKey!;
this._getToast(key)?.hide("action");
this._getNotification(key)?.parameters.action?.action();
private _buttonClicked() {
this._toast?.hide("action");
this._parameters?.action?.action();
}
private _secondaryButtonClicked(
ev: HASSDomCurrentTargetEvent<HTMLElementTagNameMap["ha-button"]>
) {
const key = ev.currentTarget.dataset.notificationKey!;
this._getToast(key)?.hide("action");
this._getNotification(key)?.parameters.secondaryAction?.action();
private _secondaryButtonClicked() {
this._toast?.hide("action");
this._parameters?.secondaryAction?.action();
}
private _dismissClicked(
ev: HASSDomCurrentTargetEvent<HTMLElementTagNameMap["ha-icon-button"]>
) {
this._getToast(ev.currentTarget.dataset.notificationKey!)?.hide("dismiss");
private _dismissClicked() {
this._toast?.hide("dismiss");
}
private _getNotification(key: string) {
return this._notifications.find((notification) => notification.key === key);
}
private _getToast(key: string) {
return [...this._toasts].find(
(toast) => toast.dataset.notificationKey === key
);
}
private async _closeNotification(key: string) {
const notification = this._getNotification(key);
if (!notification) {
return;
}
await this._getToast(key)?.hide();
if (this._getNotification(key) === notification) {
this._removeNotification(key);
}
}
private _removeNotification(key: string) {
this._notifications = this._notifications.filter(
(notification) => notification.key !== key
);
if (!this._notifications.length) {
this._hideStack();
}
}
private _showStack() {
if (!popoverSupported || !this._stack) {
return;
}
// Top-layer order is order of entry — re-enter so we paint above any
// dialog backdrop opened since the stack was first shown.
if (this._stack.matches(":popover-open")) {
this._stack.hidePopover();
}
this._stack.showPopover();
}
private _hideStack() {
if (!popoverSupported || !this._stack?.matches(":popover-open")) {
return;
}
this._stack.hidePopover();
}
static override styles = css`
.stack {
position: fixed;
inset-block-start: auto;
inset-inline-end: auto;
inset-block-end: calc(
var(--safe-area-inset-bottom, 0px) + var(--ha-space-4) +
var(--notification-stack-bottom-offset, 0px)
);
inset-inline-start: 50%;
margin: 0;
padding: 0;
border: none;
overflow: visible;
background: transparent;
transform: translateX(calc(-50% * var(--scale-direction)));
display: flex;
flex-direction: column;
align-items: center;
gap: var(--ha-space-2);
}
`;
}
declare global {
@@ -3,7 +3,6 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import type { HASSDomTargetEvent } from "../../../common/dom/fire_event";
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
import "../../../components/entity/ha-entity-picker";
import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker";
@@ -456,7 +455,7 @@ class DialogAreaDetail
return deviceReg && deviceReg.area_id === areaId;
};
private _nameChanged(ev: InputEvent & HASSDomTargetEvent<HaInput>) {
private _nameChanged(ev: InputEvent) {
this._error = undefined;
this._name = (ev.target as HaInput).value ?? "";
this._updateDirtyState(this._currentState());
@@ -480,9 +479,7 @@ class DialogAreaDetail
this._updateDirtyState(this._currentState());
}
private _pictureChanged(
ev: ValueChangedEvent<string | null> & HASSDomTargetEvent<HaPictureUpload>
) {
private _pictureChanged(ev: ValueChangedEvent<string | null>) {
this._error = undefined;
this._picture = (ev.target as HaPictureUpload).value;
this._updateDirtyState(this._currentState());
@@ -493,9 +490,7 @@ class DialogAreaDetail
this._updateDirtyState(this._currentState());
}
private _sensorChanged(
ev: ValueChangedEvent<string> & HASSDomTargetEvent<HaEntityPicker>
): void {
private _sensorChanged(ev: CustomEvent): void {
const deviceClass = (ev.target as HaEntityPicker).includeDeviceClasses![0];
const key = `_${deviceClass}Entity`;
this[key] = ev.detail.value || null;
@@ -5,7 +5,6 @@ import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import type { HASSDomTargetEvent } from "../../../common/dom/fire_event";
import "../../../components/chips/ha-chip-set";
import "../../../components/chips/ha-input-chip";
import "../../../components/ha-alert";
@@ -337,13 +336,13 @@ class DialogFloorDetail extends DirtyStateProviderMixin<FloorFormState>()(
this._updateDirtyState(this._currentState());
}
private _nameChanged(ev: InputEvent & HASSDomTargetEvent<HaInput>) {
private _nameChanged(ev: InputEvent) {
this._error = undefined;
this._name = (ev.target as HaInput).value ?? "";
this._updateDirtyState(this._currentState());
}
private _levelChanged(ev: InputEvent & HASSDomTargetEvent<HaInput>) {
private _levelChanged(ev: InputEvent) {
this._error = undefined;
this._level =
(ev.target as HaInput).value === ""
@@ -644,6 +644,7 @@ class HaConfigAreaPage extends LitElement {
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config/areas/dashboard"
.header=${html`${
area.icon
? html`<ha-icon
@@ -86,8 +86,6 @@ export class HaConfigAreasDashboard extends LitElement {
@state() private _hierarchy?: AreasFloorHierarchy;
private _searchParms = new URLSearchParams(window.location.search);
private _blockHierarchyUpdate = false;
private _blockHierarchyUpdateTimeout?: number;
@@ -168,9 +166,7 @@ export class HaConfigAreasDashboard extends LitElement {
<hass-tabs-subpage
.hass=${this.hass}
.isWide=${this.isWide}
.backPath=${
this._searchParms.has("historyBack") ? undefined : "/config"
}
back-path="/config"
.tabs=${configSections.areas}
.route=${this.route}
has-fab
@@ -443,9 +443,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.backPath=${
this._searchParms.has("historyBack") ? undefined : "/config"
}
back-path="/config"
id="entity_id"
.route=${this.route}
.tabs=${configSections.automations}
@@ -14,7 +14,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { fireEvent } from "../../../common/dom/fire_event";
import { navigate } from "../../../common/navigate";
import { navigate, replaceCurrentUrl } from "../../../common/navigate";
import { computeRTL } from "../../../common/util/compute_rtl";
import "../../../components/ha-button";
import "../../../components/ha-dropdown";
@@ -110,6 +110,7 @@ export class HaAutomationTrace extends LitElement {
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config/automation/dashboard"
.header=${title}
.scrollable=${this.narrow}
>
@@ -452,11 +453,7 @@ export class HaAutomationTrace extends LitElement {
if (runId) {
const params = new URLSearchParams(location.search);
params.delete("run_id");
history.replaceState(
null,
"",
`${location.pathname}?${params.toString()}`
);
replaceCurrentUrl(`${location.pathname}?${params.toString()}`);
}
await showAlertDialog(this, {
@@ -11,6 +11,7 @@ import { property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { storage } from "../../../common/decorators/storage";
import { fireEvent } from "../../../common/dom/fire_event";
import { replaceCurrentUrl } from "../../../common/navigate";
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
import {
extractSearchParam,
@@ -35,8 +36,6 @@ import type HaAutomationSidebar from "./ha-automation-sidebar";
export const SIDEBAR_DEFAULT_WIDTH = 500;
export const PASTED_CONFIG_TOAST_ID = "pasted-config";
export const ManualEditorMixin = <TConfig>(
superClass: Constructor<LitElement>
) => {
@@ -173,11 +172,7 @@ export const ManualEditorMixin = <TConfig>(
}
protected clearParam(param: string) {
window.history.replaceState(
null,
"",
constructUrlCurrentPath(removeSearchParam(param))
);
replaceCurrentUrl(constructUrlCurrentPath(removeSearchParam(param)));
}
protected async openSidebar(ev: CustomEvent<SidebarConfig>) {
@@ -239,7 +234,6 @@ export const ManualEditorMixin = <TConfig>(
this.pastedConfig = undefined;
showToast(this, {
id: PASTED_CONFIG_TOAST_ID,
message: "",
duration: 0,
});
@@ -37,10 +37,7 @@ import "./action/ha-automation-action";
import type HaAutomationAction from "./action/ha-automation-action";
import "./condition/ha-automation-condition";
import type HaAutomationCondition from "./condition/ha-automation-condition";
import {
ManualEditorMixin,
PASTED_CONFIG_TOAST_ID,
} from "./ha-manual-editor-mixin";
import { ManualEditorMixin } from "./ha-manual-editor-mixin";
import { showPasteReplaceDialog } from "./paste-replace-dialog/show-dialog-paste-replace";
import { manualEditorStyles, saveFabStyles } from "./styles";
import "./trigger/ha-automation-trigger";
@@ -434,7 +431,6 @@ export class HaManualAutomationEditor extends ManualEditorMixin<ManualAutomation
protected showPastedToastWithUndo() {
showEditorToast(this, {
id: PASTED_CONFIG_TOAST_ID,
message: this.hass.localize(
"ui.panel.config.automation.editor.paste_toast_message"
),
@@ -74,8 +74,6 @@ class HaConfigBackupOverview extends LitElement {
@state() private _config?: BackupConfig;
private _searchParms = new URLSearchParams(window.location.search);
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
if (changedProperties.has("config") && !this._config) {
@@ -206,9 +204,7 @@ class HaConfigBackupOverview extends LitElement {
return html`
<hass-subpage
.backPath=${
this._searchParms.has("historyBack") ? undefined : "/config/system"
}
back-path="/config/system"
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.backup.overview.header")}
@@ -4,6 +4,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { fireEvent } from "../../../common/dom/fire_event";
import { replaceCurrentUrl } from "../../../common/navigate";
import { debounce } from "../../../common/util/debounce";
import { nextRender } from "../../../common/util/render-status";
import "../../../components/ha-alert";
@@ -118,7 +119,7 @@ class HaConfigBackupSettings extends LitElement {
}
private _clearHash() {
history.replaceState(null, "", window.location.pathname);
replaceCurrentUrl(window.location.pathname);
}
protected render() {
@@ -2,7 +2,6 @@ import { mdiClose, mdiOpenInNew } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import type { HASSDomTargetEvent } from "../../../common/dom/fire_event";
import { withViewTransition } from "../../../common/util/view-transition";
import "../../../components/ha-alert";
import "../../../components/ha-button";
@@ -280,7 +279,7 @@ class DialogImportBlueprint extends DirtyStateProviderMixin<BlueprintImportState
});
}
private _inputChanged(ev: HASSDomTargetEvent<HaInput>) {
private _inputChanged(ev: Event) {
this._updateDirtyState({
value: (ev.target as HaInput).value ?? "",
hasResult: !!this._result,
@@ -13,10 +13,7 @@ import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { storage } from "../../../common/decorators/storage";
import type {
HASSDomCurrentTargetEvent,
HASSDomEvent,
} from "../../../common/dom/fire_event";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { navigate } from "../../../common/navigate";
@@ -486,7 +483,7 @@ class HaBlueprintOverview extends LitElement {
this._createNew(blueprint);
}
private _handleUsageClick = (ev: HASSDomCurrentTargetEvent<HTMLElement>) => {
private _handleUsageClick = (ev: Event) => {
ev.stopPropagation();
ev.preventDefault();
const target = ev.currentTarget as HTMLElement | null;
@@ -68,7 +68,6 @@ export class CloudAccount extends SubscribeMixin(LitElement) {
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
header="Home Assistant Cloud"
>
<ha-dropdown slot="toolbar-icon" @wa-select=${this._handleMenuAction}>
@@ -21,6 +21,7 @@ export class CloudForgotPassword extends LitElement {
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
.header=${this.hass.localize(
"ui.panel.config.cloud.forgot_password.title"
)}
@@ -45,6 +45,7 @@ export class CloudLoginPanel extends LitElement {
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
header="Home Assistant Cloud"
>
<ha-dropdown slot="toolbar-icon" @wa-select=${this._handleMenuAction}>
@@ -38,6 +38,7 @@ export class CloudRegister extends LitElement {
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
.header=${this.hass.localize("ui.panel.config.cloud.register.title")}
>
<div class="content">
@@ -66,8 +66,6 @@ class HaConfigSectionUpdates extends LitElement {
@property({ type: Boolean }) public narrow = false;
@state() private _searchParms = new URLSearchParams(window.location.search);
@state() private _showSkipped = false;
@state() private _supervisorInfo?: HassioSupervisorInfo;
@@ -155,9 +153,7 @@ class HaConfigSectionUpdates extends LitElement {
return html`
<hass-subpage
.backPath=${
this._searchParms.has("historyBack") ? undefined : "/config/system"
}
back-path="/config/system"
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.updates.caption")}
@@ -986,6 +986,7 @@ export class HaConfigDevicePage extends LitElement {
return html`<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config/devices/dashboard"
.header=${deviceName}
>
<ha-tooltip for="edit-settings-button" slot="toolbar-icon">
@@ -23,7 +23,7 @@ import {
PROTOCOL_INTEGRATIONS,
protocolIntegrationPicked,
} from "../../../common/integrations/protocolIntegrationPicked";
import { navigate } from "../../../common/navigate";
import { navigate, updateHistoryState } from "../../../common/navigate";
import type { LocalizeFunc } from "../../../common/translations/localize";
import {
hasRejectedItems,
@@ -778,9 +778,7 @@ export class HaConfigDeviceDashboard extends LitElement {
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.backPath=${
this._searchParms.has("historyBack") ? undefined : "/config"
}
back-path="/config"
.tabs=${configSections.devices}
.route=${this.route}
.searchLabel=${this.hass.localize(
@@ -1043,7 +1041,7 @@ export class HaConfigDeviceDashboard extends LitElement {
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
history.replaceState({ filter: this._filter }, "");
updateHistoryState({ filter: this._filter });
}
private _addDevice() {
@@ -2,7 +2,6 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import "../../../../components/entity/ha-statistic-picker";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog";
@@ -341,7 +340,7 @@ export class DialogEnergyBatterySettings
}
private _handlePowerConfigChanged(
ev: HASSDomEvent<HASSDomEvents["power-config-changed"]>
ev: CustomEvent<{ powerType: PowerType; powerConfig: PowerConfig }>
) {
this._powerType = ev.detail.powerType;
this._powerConfig = ev.detail.powerConfig;
@@ -3,7 +3,6 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { HASSDomCurrentTargetEvent } from "../../../../common/dom/fire_event";
import type { LocalizeKeys } from "../../../../common/translations/localize";
import "../../../../components/ha-alert";
import "../../../../components/ha-button";
@@ -215,7 +214,7 @@ export class DialogEnergyCustomise
`;
}
private _toggleCard = (ev: HASSDomCurrentTargetEvent<HaSwitch>): void => {
private _toggleCard = (ev: Event): void => {
const target = ev.currentTarget as HaSwitch;
const cardKey = target.dataset.cardKey;
if (!cardKey) {
@@ -2,7 +2,6 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { HASSDomCurrentTargetEvent } from "../../../../common/dom/fire_event";
import "../../../../components/entity/ha-entity-picker";
import "../../../../components/entity/ha-statistic-picker";
import "../../../../components/ha-button";
@@ -354,7 +353,7 @@ export class DialogEnergyGasSettings
`;
}
private _handleCostChanged(ev: HASSDomCurrentTargetEvent<HaRadioGroup>) {
private _handleCostChanged(ev: Event) {
this._costs = (ev.currentTarget as HaRadioGroup).value as CostType;
this._updateFormDirtyState();
}
@@ -2,10 +2,6 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import type {
HASSDomCurrentTargetEvent,
HASSDomEvent,
} from "../../../../common/dom/fire_event";
import "../../../../components/entity/ha-entity-picker";
import "../../../../components/entity/ha-statistic-picker";
import "../../../../components/ha-button";
@@ -597,9 +593,7 @@ export class DialogEnergyGridSettings
this._updateFormDirtyState();
}
private _handleImportCostTypeChanged(
ev: HASSDomCurrentTargetEvent<HaRadioGroup>
) {
private _handleImportCostTypeChanged(ev: Event) {
this._importCostType = (ev.currentTarget as HaRadioGroup).value as CostType;
// Clear other cost fields when switching types
this._source = {
@@ -611,9 +605,7 @@ export class DialogEnergyGridSettings
this._updateFormDirtyState();
}
private _handleExportCostTypeChanged(
ev: HASSDomCurrentTargetEvent<HaRadioGroup>
) {
private _handleExportCostTypeChanged(ev: Event) {
this._exportCostType = (ev.currentTarget as HaRadioGroup).value as CostType;
// Clear other cost fields when switching types
this._source = {
@@ -638,10 +630,9 @@ export class DialogEnergyGridSettings
this._updateFormDirtyState();
}
private _numberCostChanged(ev: HASSDomCurrentTargetEvent<HaInput>) {
const value = ev.currentTarget.value
? parseFloat(ev.currentTarget.value)
: null;
private _numberCostChanged(ev: Event) {
const input = ev.currentTarget as HTMLInputElement;
const value = input.value ? parseFloat(input.value) : null;
this._source = { ...this._source!, number_energy_price: value };
this._updateFormDirtyState();
}
@@ -662,16 +653,15 @@ export class DialogEnergyGridSettings
this._updateFormDirtyState();
}
private _numberCompensationChanged(ev: HASSDomCurrentTargetEvent<HaInput>) {
const value = ev.currentTarget.value
? parseFloat(ev.currentTarget.value)
: null;
private _numberCompensationChanged(ev: Event) {
const input = ev.currentTarget as HTMLInputElement;
const value = input.value ? parseFloat(input.value) : null;
this._source = { ...this._source!, number_energy_price_export: value };
this._updateFormDirtyState();
}
private _handlePowerConfigChanged(
ev: HASSDomEvent<HASSDomEvents["power-config-changed"]>
ev: CustomEvent<{ powerType: PowerType; powerConfig: PowerConfig }>
) {
this._powerType = ev.detail.powerType;
this._powerConfig = ev.detail.powerConfig;
@@ -3,7 +3,6 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { HASSDomCurrentTargetEvent } from "../../../../common/dom/fire_event";
import "../../../../components/entity/ha-statistic-picker";
import "../../../../components/ha-button";
import "../../../../components/ha-checkbox";
@@ -13,7 +12,6 @@ import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-svg-icon";
import "../../../../components/radio/ha-radio-group";
import "../../../../components/input/ha-input";
import { domainToName } from "../../../../data/integration";
import type { HaRadioGroup } from "../../../../components/radio/ha-radio-group";
import "../../../../components/radio/ha-radio-option";
import type { ConfigEntry } from "../../../../data/config_entries";
@@ -236,7 +234,7 @@ export class DialogEnergySolarSettings
},
this.hass.auth.data.hassUrl
)}
/>${entry.title || domainToName(this.hass.localize, entry.domain)}
/>${entry.title}
</div>
</ha-checkbox>`
)}
@@ -292,7 +290,7 @@ export class DialogEnergySolarSettings
);
}
private _handleForecastChanged(ev: HASSDomCurrentTargetEvent<HaRadioGroup>) {
private _handleForecastChanged(ev: Event) {
this._forecast = (ev.currentTarget as HaRadioGroup).value === "true";
this._updateFormDirtyState();
}
@@ -2,7 +2,6 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { HASSDomCurrentTargetEvent } from "../../../../common/dom/fire_event";
import "../../../../components/entity/ha-entity-picker";
import "../../../../components/entity/ha-statistic-picker";
import "../../../../components/ha-button";
@@ -290,7 +289,7 @@ export class DialogEnergyWaterSettings
`;
}
private _handleCostChanged(ev: HASSDomCurrentTargetEvent<HaRadioGroup>) {
private _handleCostChanged(ev: Event) {
this._costs = (ev.currentTarget as HaRadioGroup).value as CostType;
this._updateFormDirtyState();
}
@@ -3,7 +3,6 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { HASSDomCurrentTargetEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import type { LocalizeKeys } from "../../../../common/translations/localize";
import "../../../../components/entity/ha-statistic-picker";
@@ -235,7 +234,7 @@ export class HaEnergyPowerConfig extends LitElement {
fireEvent(this, "hass-more-info", { entityId: this.helperEntityId! });
}
private _handlePowerTypeChanged(ev: HASSDomCurrentTargetEvent<HaRadioGroup>) {
private _handlePowerTypeChanged(ev: Event) {
const newPowerType = (ev.currentTarget as HaRadioGroup).value as PowerType;
// Clear power config when switching types
fireEvent(this, "power-config-changed", {
+1 -7
View File
@@ -78,8 +78,6 @@ class HaConfigEnergy extends LitElement {
@property({ attribute: false }) public route!: Route;
@state() private _searchParms = new URLSearchParams(window.location.search);
@state() private _info?: EnergyInfo;
@state() private _preferences?: EnergyPreferences;
@@ -126,11 +124,7 @@ class HaConfigEnergy extends LitElement {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.backPath=${
this._searchParms.has("historyBack")
? undefined
: "/config/lovelace/dashboards"
}
back-path="/config/lovelace/dashboards"
.route=${this.route}
.tabs=${TABS}
>
@@ -39,6 +39,7 @@ import {
PROTOCOL_INTEGRATIONS,
protocolIntegrationPicked,
} from "../../../common/integrations/protocolIntegrationPicked";
import { updateHistoryState } from "../../../common/navigate";
import { slugify } from "../../../common/string/slugify";
import type { LocalizeFunc } from "../../../common/translations/localize";
import {
@@ -810,9 +811,7 @@ export class HaConfigEntities extends LitElement {
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.backPath=${
this._searchParms.has("historyBack") ? undefined : "/config"
}
back-path="/config"
.route=${this.route}
.tabs=${configSections.devices}
.columns=${this._columns(this.hass.localize, filteredEntities)}
@@ -1246,7 +1245,7 @@ export class HaConfigEntities extends LitElement {
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
history.replaceState({ filter: this._filter }, "");
updateHistoryState({ filter: this._filter });
}
private _handleSelectionChanged(
-1
View File
@@ -106,7 +106,6 @@ class HaPanelConfig extends HassRouterPage {
integrations: {
tag: "ha-config-integrations",
load: () => import("./integrations/ha-config-integrations"),
waitForReady: true,
},
labels: {
tag: "ha-config-labels",
@@ -644,9 +644,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.backPath=${
this._searchParms.has("historyBack") ? undefined : "/config"
}
back-path="/config"
.route=${this.route}
.tabs=${configSections.devices}
.searchLabel=${this.hass.localize(
@@ -373,7 +373,11 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
: this._manifest?.documentation;
return html`
<hass-subpage .hass=${this.hass} .narrow=${this.narrow}>
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config/integrations/dashboard"
>
${
documentationLink
? html`
@@ -1,5 +1,4 @@
import { mdiFilterVariant, mdiPlus } from "@mdi/js";
import { consume } from "@lit/context";
import type { IFuseOptions } from "fuse.js";
import Fuse from "fuse.js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
@@ -14,7 +13,7 @@ import {
PROTOCOL_INTEGRATIONS,
protocolIntegrationPicked,
} from "../../../common/integrations/protocolIntegrationPicked";
import { navigate } from "../../../common/navigate";
import { navigate, updateHistoryState } from "../../../common/navigate";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import { extractSearchParam } from "../../../common/url/search-params";
import { nextRender } from "../../../common/util/render-status";
@@ -55,10 +54,6 @@ import type { ImprovDiscoveredDevice } from "../../../external_app/external_mess
import "../../../layouts/hass-loading-screen";
import "../../../layouts/hass-tabs-subpage";
import type { HassTabsSubpage } from "../../../layouts/hass-tabs-subpage";
import {
childPanelReadyContext,
type RegisterChildPanelReady,
} from "../../../layouts/panel-ready";
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
@@ -163,8 +158,6 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
window.location.hash.substring(1)
);
@state() private _searchParams = new URLSearchParams(window.location.search);
@state() private _filter: string = history.state?.filter || "";
@state() private _logInfos?: Record<string, IntegrationLogInfo>;
@@ -173,17 +166,6 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
@query("hass-tabs-subpage") private _tabsSubpage?: HassTabsSubpage;
private _resolveInitialRender?: () => void;
private _initialRenderComplete = new Promise<void>((resolve) => {
this._resolveInitialRender = resolve;
});
private _childReadyRegistered = false;
@consume({ context: childPanelReadyContext, subscribe: true })
private _registerChildPanelReady?: RegisterChildPanelReady;
public disconnectedCallback(): void {
super.disconnectedCallback();
window.removeEventListener(
@@ -404,10 +386,6 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
protected updated(changed: PropertyValues<this>) {
super.updated(changed);
if (!this._childReadyRegistered && this._registerChildPanelReady) {
this._registerChildPanelReady(this._initialRenderComplete);
this._childReadyRegistered = true;
}
if (changed.has("route")) {
this._handleRouteChanged();
}
@@ -435,9 +413,6 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
}
if (this.configEntries && this.configEntriesInProgress) {
this._resolveInitialRender?.();
this._resolveInitialRender = undefined;
const activeElement = deepActiveElement();
if (
@@ -526,9 +501,7 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
return html`
<hass-tabs-subpage
.hass=${this.hass}
.backPath=${
this._searchParams.has("historyBack") ? undefined : "/config"
}
back-path="/config"
.route=${this.route}
.tabs=${configSections.devices}
has-fab
@@ -885,7 +858,7 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
private _handleSearchChange(ev: InputEvent) {
this._filter = (ev.target as HaInputSearch).value ?? "";
history.replaceState({ filter: this._filter }, "");
updateHistoryState({ filter: this._filter });
}
private async _highlightEntry() {
@@ -11,7 +11,6 @@ import {
import type { DataEntryFlowProgress } from "../../../data/data_entry_flow";
import { domainToName } from "../../../data/integration";
import "../../../layouts/hass-loading-screen";
import { ChildPanelReady } from "../../../layouts/panel-ready";
import type { RouterOptions } from "../../../layouts/hass-router-page";
import { HassRouterPage } from "../../../layouts/hass-router-page";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
@@ -71,11 +70,6 @@ class HaConfigIntegrations extends SubscribeMixin(HassRouterPage) {
private _loadTranslationsPromise?: Promise<LocalizeFunc>;
public constructor() {
super();
new ChildPanelReady(this);
}
public hassSubscribe() {
return [
subscribeConfigEntries(
@@ -3,7 +3,6 @@ import type { CSSResultGroup, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import type { HASSDomCurrentTargetEvent } from "../../../../../common/dom/fire_event";
import { computeDeviceName } from "../../../../../common/entity/compute_device_name";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-button";
@@ -452,9 +451,7 @@ export class BluetoothAdapterInfoPage extends LitElement {
return this._formatModeLabel(scannerState.current_mode);
}
private async _handleEnable(
ev: HASSDomCurrentTargetEvent<HTMLElement & { entry: ConfigEntry }>
) {
private async _handleEnable(ev: Event) {
const button = ev.currentTarget as HTMLElement & { entry: ConfigEntry };
const entryId = button.entry.entry_id;
try {
@@ -477,9 +474,7 @@ export class BluetoothAdapterInfoPage extends LitElement {
}
}
private _openOptionFlow(
ev: HASSDomCurrentTargetEvent<HTMLElement & { entry: ConfigEntry }>
) {
private _openOptionFlow(ev: Event) {
ev.preventDefault();
ev.stopPropagation();
const button = ev.currentTarget as HTMLElement & { entry: ConfigEntry };
@@ -95,6 +95,7 @@ export class DHCPConfigPanel extends SubscribeMixin(LitElement) {
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
back-path="/config/integrations/integration/dhcp"
.columns=${this._columns(this.hass.localize)}
.data=${this._dataWithIds(this._data)}
.noDataText=${this.hass.localize(
@@ -3,7 +3,6 @@ import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import type { HASSDomCurrentTargetEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-button";
import "../../../../../components/ha-dialog-footer";
@@ -212,9 +211,7 @@ class DialogMatterLockManage extends LitElement {
`;
}
private _handleUserClick(
ev: HASSDomCurrentTargetEvent<HTMLElement & { user: MatterLockUser }>
): void {
private _handleUserClick(ev: Event): void {
// Ignore clicks that originated from the delete button
const path = ev.composedPath();
if (path.some((el) => (el as HTMLElement).tagName === "HA-ICON-BUTTON")) {
@@ -224,9 +221,7 @@ class DialogMatterLockManage extends LitElement {
this._editUser(user);
}
private _handleDeleteUserClick(
ev: HASSDomCurrentTargetEvent<HTMLElement & { user: MatterLockUser }>
): void {
private _handleDeleteUserClick(ev: Event): void {
ev.preventDefault();
ev.stopPropagation();
const user = (ev.currentTarget as any).user as MatterLockUser;
@@ -61,7 +61,11 @@ export class MQTTConfigPanel extends LitElement {
protected render(): TemplateResult {
return html`
<hass-subpage .narrow=${this.narrow} .hass=${this.hass}>
<hass-subpage
.narrow=${this.narrow}
.hass=${this.hass}
back-path="/config/integrations/integration/mqtt"
>
<div class="content">
<ha-card
.header=${this.hass.localize("ui.panel.config.mqtt.settings_title")}
@@ -99,6 +99,7 @@ export class SSDPConfigPanel extends SubscribeMixin(LitElement) {
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
back-path="/config/integrations/integration/ssdp"
.columns=${this._columns(this.hass.localize)}
.initialGroupColumn=${this._activeGrouping}
.initialCollapsedGroups=${this._activeCollapsed}
@@ -106,6 +106,7 @@ export class ZeroconfConfigPanel extends SubscribeMixin(LitElement) {
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
back-path="/config/integrations/integration/zeroconf"
.columns=${this._columns(this.hass.localize)}
.initialGroupColumn=${this._activeGrouping}
.initialCollapsedGroups=${this._activeCollapsed}
@@ -76,6 +76,7 @@ class ZHAAddDevicesPage extends LitElement {
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config/zha/dashboard"
.header=${this.hass.localize("ui.panel.config.zha.add_device")}
>
<ha-button
@@ -11,7 +11,10 @@ import { addGroup, fetchGroupableDevices } from "../../../../../data/zha";
import "../../../../../layouts/hass-subpage";
import type { HomeAssistant } from "../../../../../types";
import "./zha-device-endpoint-list";
import type { ZHADeviceEndpointList } from "./zha-device-endpoint-list";
import type {
DeviceEndpointSelectionChangedEvent,
ZHADeviceEndpointList,
} from "./zha-device-endpoint-list";
@customElement("zha-add-group-page")
export class ZHAAddGroupPage extends LitElement {
@@ -128,7 +131,7 @@ export class ZHAAddGroupPage extends LitElement {
}
private _handleAddSelectionChanged(
ev: HASSDomEvent<HASSDomEvents["selection-changed"]>
ev: HASSDomEvent<DeviceEndpointSelectionChangedEvent>
): void {
this._selectedDevicesToAdd = ev.detail.value;
}
@@ -4,15 +4,10 @@ import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import type {
HASSDomCurrentTargetEvent,
HASSDomEvent,
} from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-list";
import "../../../../../components/input/ha-input-search";
import type { HaInputSearch } from "../../../../../components/input/ha-input-search";
import "../../../../../components/item/ha-list-item-base";
import "../../../../../components/item/ha-list-item-option";
import type { HaListItemOption } from "../../../../../components/item/ha-list-item-option";
@@ -274,16 +269,11 @@ export class ZHADeviceEndpointList extends LitElement {
.join(" · ");
}
private _handleFilterChanged(
ev: HASSDomCurrentTargetEvent<HaInputSearch>
): void {
this._filter = ev.currentTarget.value ?? "";
private _handleFilterChanged(ev: Event): void {
this._filter = (ev.currentTarget as HTMLInputElement).value;
}
private _handleItemSelected(
ev: HASSDomEvent<HASSDomEvents["ha-list-item-selected"]> &
HASSDomCurrentTargetEvent<HaListSelectable>
): void {
private _handleItemSelected(ev: CustomEvent<number>): void {
const list = ev.currentTarget as HaListSelectable;
let selectedDeviceIds = this._selectedDeviceIds;
@@ -300,10 +290,7 @@ export class ZHADeviceEndpointList extends LitElement {
}
}
private _handleItemDeselected(
ev: HASSDomEvent<HASSDomEvents["ha-list-item-deselected"]> &
HASSDomCurrentTargetEvent<HaListSelectable>
): void {
private _handleItemDeselected(ev: CustomEvent<number>): void {
const list = ev.currentTarget as HaListSelectable;
let selectedDeviceIds = this._selectedDeviceIds;
@@ -19,7 +19,10 @@ import "../../../../../layouts/hass-subpage";
import type { HomeAssistant } from "../../../../../types";
import { formatAsPaddedHex } from "./functions";
import "./zha-device-endpoint-list";
import type { ZHADeviceEndpointList } from "./zha-device-endpoint-list";
import type {
DeviceEndpointSelectionChangedEvent,
ZHADeviceEndpointList,
} from "./zha-device-endpoint-list";
import { showZHAAddGroupMembersDialog } from "./show-dialog-zha-add-group-members";
@customElement("zha-group-page")
@@ -199,7 +202,7 @@ export class ZHAGroupPage extends LitElement {
}
private _handleRemoveSelectionChanged(
ev: HASSDomEvent<HASSDomEvents["selection-changed"]>
ev: HASSDomEvent<DeviceEndpointSelectionChangedEvent>
): void {
this._selectedDevicesToRemove = ev.detail.value;
}
@@ -55,6 +55,7 @@ export class ZHANetworkVisualizationPage extends LitElement {
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config/zha/dashboard"
.header=${this.hass.localize(
"ui.panel.config.zha.visualization.header"
)}
@@ -1,16 +1,13 @@
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import type { HASSDomCurrentTargetEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/buttons/ha-progress-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-select";
import "../../../../../components/input/ha-input";
import type { HaInput } from "../../../../../components/input/ha-input";
import "../../../../../components/ha-switch";
import type { HaSwitch } from "../../../../../components/ha-switch";
import type { ZHAConfiguration } from "../../../../../data/zha";
import {
fetchZHAConfiguration,
@@ -21,8 +18,6 @@ import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
const PREDEFINED_TIMEOUTS = [1800, 3600, 7200, 21600, 43200, 86400];
type ZHASwitchChangeEvent = HASSDomCurrentTargetEvent<HaSwitch>;
type ZHAInputChangeEvent = HASSDomCurrentTargetEvent<HaInput>;
@customElement("zha-options-page")
class ZHAOptionsPage extends LitElement {
@@ -350,54 +345,53 @@ class ZHAOptionsPage extends LitElement {
`;
}
private _enableIdentifyOnJoinChanged(ev: ZHASwitchChangeEvent): void {
this._configuration!.data.zha_options.enable_identify_on_join =
ev.currentTarget.checked;
private _enableIdentifyOnJoinChanged(ev: Event): void {
const checked = (ev.target as HTMLInputElement).checked;
this._configuration!.data.zha_options.enable_identify_on_join = checked;
this.requestUpdate();
}
private _enhancedLightTransitionChanged(ev: ZHASwitchChangeEvent): void {
this._configuration!.data.zha_options.enhanced_light_transition =
ev.currentTarget.checked;
private _enhancedLightTransitionChanged(ev: Event): void {
const checked = (ev.target as HTMLInputElement).checked;
this._configuration!.data.zha_options.enhanced_light_transition = checked;
this.requestUpdate();
}
private _lightTransitioningFlagChanged(ev: ZHASwitchChangeEvent): void {
this._configuration!.data.zha_options.light_transitioning_flag =
ev.currentTarget.checked;
private _lightTransitioningFlagChanged(ev: Event): void {
const checked = (ev.target as HTMLInputElement).checked;
this._configuration!.data.zha_options.light_transitioning_flag = checked;
this.requestUpdate();
}
private _groupMembersAssumeStateChanged(ev: ZHASwitchChangeEvent): void {
this._configuration!.data.zha_options.group_members_assume_state =
ev.currentTarget.checked;
private _groupMembersAssumeStateChanged(ev: Event): void {
const checked = (ev.target as HTMLInputElement).checked;
this._configuration!.data.zha_options.group_members_assume_state = checked;
this.requestUpdate();
}
private _enableMainsStartupPollingChanged(ev: ZHASwitchChangeEvent): void {
private _enableMainsStartupPollingChanged(ev: Event): void {
const checked = (ev.target as HTMLInputElement).checked;
this._configuration!.data.zha_options.enable_mains_startup_polling =
ev.currentTarget.checked;
checked;
this.requestUpdate();
}
private _defaultLightTransitionChanged(ev: ZHAInputChangeEvent): void {
this._configuration!.data.zha_options.default_light_transition = Number(
ev.currentTarget.value
);
private _defaultLightTransitionChanged(ev: Event): void {
const value = Number((ev.target as HTMLInputElement).value);
this._configuration!.data.zha_options.default_light_transition = value;
this.requestUpdate();
}
private _customMainsSecondsChanged(ev: ZHAInputChangeEvent): void {
this._configuration!.data.zha_options.consider_unavailable_mains = Number(
ev.currentTarget.value
);
private _customMainsSecondsChanged(ev: Event): void {
const seconds = Number((ev.target as HTMLInputElement).value);
this._configuration!.data.zha_options.consider_unavailable_mains = seconds;
this.requestUpdate();
}
private _customBatterySecondsChanged(ev: ZHAInputChangeEvent): void {
this._configuration!.data.zha_options.consider_unavailable_battery = Number(
ev.currentTarget.value
);
private _customBatterySecondsChanged(ev: Event): void {
const seconds = Number((ev.target as HTMLInputElement).value);
this._configuration!.data.zha_options.consider_unavailable_battery =
seconds;
this.requestUpdate();
}
@@ -425,15 +419,7 @@ class ZHAOptionsPage extends LitElement {
this.requestUpdate();
}
private async _updateConfiguration(
ev: HASSDomCurrentTargetEvent<
HTMLElement & {
progress: boolean;
actionSuccess: () => void;
actionError: () => void;
}
>
): Promise<void> {
private async _updateConfiguration(ev: Event): Promise<void> {
const button = ev.currentTarget as HTMLElement & {
progress: boolean;
actionSuccess: () => void;
@@ -2,7 +2,6 @@ import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import type { HASSDomCurrentTargetEvent } from "../../../../../common/dom/fire_event";
import type { LocalizeKeys } from "../../../../../common/translations/localize";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-button";
@@ -479,9 +478,7 @@ class DialogZwaveCredentialUserEdit extends DirtyStateProviderMixin<CredentialFo
}
}
private _handleCredentialTypeChanged(
ev: HASSDomCurrentTargetEvent<HaRadioGroup>
): void {
private _handleCredentialTypeChanged(ev: Event): void {
this._credentialType = (ev.currentTarget as HaRadioGroup)
.value as ZwaveCredentialType;
// Switching types invalidates any data tied to the previous type's
@@ -34,7 +34,12 @@ class ZWaveJSConfigEntryPicker extends LitElement {
if (this._configEntries.length === 0) {
return html`
<hass-subpage header="Z-Wave" .narrow=${this.narrow} .hass=${this.hass}>
<hass-subpage
header="Z-Wave"
.narrow=${this.narrow}
.hass=${this.hass}
back-path="/config/integrations/integration/zwave_js"
>
<div class="content">
<ha-card>
<div class="card-content">
@@ -51,7 +56,12 @@ class ZWaveJSConfigEntryPicker extends LitElement {
}
return html`
<hass-subpage header="Z-Wave" .narrow=${this.narrow} .hass=${this.hass}>
<hass-subpage
header="Z-Wave"
.narrow=${this.narrow}
.hass=${this.hass}
back-path="/config/integrations/integration/zwave_js"
>
<div class="content">
<ha-card
.header=${this.hass.localize(
@@ -2,13 +2,11 @@ import { mdiCloseCircle } from "@mdi/js";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import type { HASSDomCurrentTargetEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-button";
import "../../../../../components/ha-select";
import type { HaSelectSelectEvent } from "../../../../../components/ha-select";
import "../../../../../components/ha-spinner";
import "../../../../../components/input/ha-input";
import type { HaInput } from "../../../../../components/input/ha-input";
import {
getZwaveNodeRawConfigParameter,
setZwaveNodeRawConfigParameter,
@@ -131,9 +129,9 @@ class ZWaveJSCustomParam extends LitElement {
return parsed;
}
private _customParamNumberChanged(ev: HASSDomCurrentTargetEvent<HaInput>) {
private _customParamNumberChanged(ev: Event) {
this._customParamNumber = this._tryParseNumber(
ev.currentTarget.value ?? ""
(ev.target as HTMLInputElement).value
);
}
@@ -141,8 +139,8 @@ class ZWaveJSCustomParam extends LitElement {
this._valueSize = this._tryParseNumber(ev.detail.value) ?? 1;
}
private _customValueChanged(ev: HASSDomCurrentTargetEvent<HaInput>) {
this._value = this._tryParseNumber(ev.currentTarget.value ?? "");
private _customValueChanged(ev: Event) {
this._value = this._tryParseNumber((ev.target as HTMLInputElement).value);
}
private _customValueFormatChanged(ev: HaSelectSelectEvent) {
+1 -4
View File
@@ -4,7 +4,6 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import type { HASSDomCurrentTargetEvent } from "../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-alert";
@@ -284,9 +283,7 @@ export class SystemLogCard extends LitElement {
fileDownload(signedUrl.path, logFileName);
}
private _openLog(
ev: HASSDomCurrentTargetEvent<HTMLElement & { logItem: LoggedError }>
): void {
private _openLog(ev: Event): void {
const item = (ev.currentTarget as any).logItem;
showSystemLogDetailDialog(this, { item });
}
@@ -175,6 +175,7 @@ export class HaConfigLovelaceResources extends LitElement {
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
back-path="/config/lovelace/dashboards"
.tabs=${lovelaceResourcesTabs}
.columns=${this._columns(this.hass.language, this.hass.localize)}
.data=${this._resources}
@@ -4,7 +4,6 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { mainWindow } from "../../../common/dom/get_main_window";
import { HOSTNAME_PATTERN } from "../../../common/string/is_hostname";
import {
IP_ADDRESS_OR_NETWORK_PATTERN,
IP_ADDRESS_PATTERN,
@@ -39,16 +38,7 @@ const SCHEMA = memoizeOne(
{
name: "server_port",
required: true,
selector: {
number: {
min: 1,
max: 65535,
mode: "box",
validation_message: localize(
"ui.panel.config.network.http.invalid_port"
),
},
},
selector: { number: { min: 1, max: 65535, mode: "box" } },
},
{
name: "ssl",
@@ -128,16 +118,7 @@ const SCHEMA = memoizeOne(
{
name: "login_attempts_threshold",
required: true,
selector: {
number: {
min: -1,
max: 1000,
mode: "box",
validation_message: localize(
"ui.panel.config.network.http.invalid_login_attempts_threshold"
),
},
},
selector: { number: { min: -1, max: 1000, mode: "box" } },
},
],
},
@@ -152,7 +133,7 @@ const SCHEMA = memoizeOne(
selector: {
text: {
multiple: true,
pattern: `${IP_ADDRESS_PATTERN}|${HOSTNAME_PATTERN}`,
pattern: IP_ADDRESS_PATTERN,
validation_message: localize(
"ui.panel.config.network.http.invalid_host"
),
@@ -2,7 +2,6 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import type { HASSDomCurrentTargetEvent } from "../../../common/dom/fire_event";
import { isIPAddress } from "../../../common/string/is_ip_address";
import "../../../components/ha-alert";
import "../../../components/ha-button";
@@ -155,12 +154,14 @@ class ConfigUrlForm extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.panel.config.url.description")}
</div>
<h4>
${this.hass.localize("ui.panel.config.url.external_url_label")}
</h4>
${
hasCloud
? html`
<h4>
${this.hass.localize(
"ui.panel.config.url.external_url_label"
)}
</h4>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
@@ -362,12 +363,12 @@ class ConfigUrlForm extends SubscribeMixin(LitElement) {
);
}
private _toggleCloud(ev: HASSDomCurrentTargetEvent<HaSwitch>) {
private _toggleCloud(ev: Event) {
this._cloudChecked = (ev.currentTarget as HaSwitch).checked;
this._showCustomExternalUrl = !this._cloudChecked;
}
private _toggleInternalAutomatic(ev: HASSDomCurrentTargetEvent<HaSwitch>) {
private _toggleInternalAutomatic(ev: Event) {
this._showCustomInternalUrl = !(ev.currentTarget as HaSwitch).checked;
}
@@ -2,10 +2,6 @@ import { mdiDeleteOutline, mdiMenuDown, mdiPlus, mdiWifi } from "@mdi/js";
import { css, type CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { cache } from "lit/directives/cache";
import type {
HASSDomCurrentTargetEvent,
HASSDomTargetEvent,
} from "../../../common/dom/fire_event";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-card";
@@ -634,9 +630,7 @@ export class HassioNetwork extends LitElement {
this._interface = { ...this._interfaces[this._curTabIndex] };
}
private _handleRadioValueChanged(
ev: HASSDomCurrentTargetEvent<HaRadioGroup & { version: "ipv4" | "ipv6" }>
): void {
private _handleRadioValueChanged(ev: Event): void {
const source = ev.currentTarget as HaRadioGroup;
const value = source.value as "disabled" | "auto" | "static";
const version = (source as any).version as "ipv4" | "ipv6";
@@ -651,9 +645,7 @@ export class HassioNetwork extends LitElement {
this.requestUpdate("_interface");
}
private _handleRadioValueChangedAp(
ev: HASSDomCurrentTargetEvent<HaRadioGroup>
): void {
private _handleRadioValueChangedAp(ev: Event): void {
const source = ev.currentTarget as HaRadioGroup;
const value = source.value as "open" | "wep" | "wpa-psk";
this._wifiConfiguration!.auth = value;
@@ -661,11 +653,7 @@ export class HassioNetwork extends LitElement {
this.requestUpdate("_wifiConfiguration");
}
private _handleInputValueChanged(
ev: HASSDomTargetEvent<
HaInput & { version: "ipv4" | "ipv6"; index: number }
>
): void {
private _handleInputValueChanged(ev: Event): void {
const source = ev.target as HaInput;
const value = source.value;
const version = (ev.target as any).version as "ipv4" | "ipv6";
@@ -703,7 +691,7 @@ export class HassioNetwork extends LitElement {
}
}
private _handleInputValueChangedWifi(ev: HASSDomTargetEvent<HaInput>): void {
private _handleInputValueChangedWifi(ev: Event): void {
const source = ev.target as HaInput;
const value = source.value;
const id = source.id;
@@ -720,9 +708,7 @@ export class HassioNetwork extends LitElement {
this._wifiConfiguration![id] = value;
}
private _addAddress(
ev: HASSDomTargetEvent<HTMLElement & { version: "ipv4" | "ipv6" }>
): void {
private _addAddress(ev: Event): void {
const version = (ev.target as any).version as "ipv4" | "ipv6";
this._interface![version]!.address!.push(
version === "ipv4" ? "0.0.0.0/24" : "::/64"
@@ -731,11 +717,7 @@ export class HassioNetwork extends LitElement {
this.requestUpdate("_interface");
}
private _removeAddress(
ev: HASSDomTargetEvent<
HTMLElement & { index: number; version: "ipv4" | "ipv6" }
>
): void {
private _removeAddress(ev: Event): void {
const source = ev.target as any;
const index = source.index as number;
const version = source.version as "ipv4" | "ipv6";
@@ -770,11 +752,7 @@ export class HassioNetwork extends LitElement {
this.requestUpdate("_interface");
}
private _removeNameserver(
ev: HASSDomTargetEvent<
HTMLElement & { index: number; version: "ipv4" | "ipv6" }
>
): void {
private _removeNameserver(ev: Event): void {
const source = ev.target as any;
const index = source.index as number;
const version = source.version as "ipv4" | "ipv6";

Some files were not shown because too many files have changed in this diff Show More