Compare commits

..

1 Commits

Author SHA1 Message Date
Petar Petrov 128c5bddeb Show progress on the Zigbee backup button while the backup is created 2026-07-31 13:25:14 +03:00
85 changed files with 1683 additions and 2761 deletions
@@ -7,8 +7,6 @@ description: Home Assistant frontend component patterns. Use when implementing o
Use this skill when creating or reviewing Home Assistant UI components and common interaction patterns.
Cross-load `ha-frontend-events` when component work includes event listener typing, custom event dispatch, or event-map declarations.
## Dialogs
Open dialogs through the fire-event pattern:
-105
View File
@@ -1,105 +0,0 @@
---
name: ha-frontend-events
description: Home Assistant frontend event patterns. Use when typing event handlers, using HASSDomEvent types, dispatching with fireEvent, or declaring HASSDomEvents and event maps.
---
# HA Frontend Events
Use this skill when implementing or reviewing event listeners and custom event contracts. Cross-load `ha-frontend-components` when the work also involves dialogs, forms, alerts, shortcuts, tooltips, panels, Lovelace cards, or buttons.
## Event Handling
Use the event types from `src/common/dom/fire_event.ts` instead of plain `Event`, generic `CustomEvent`, or element casts when they express the handler contract:
- Use `HASSDomCurrentTargetEvent<T>` to read the element on which the listener was registered through `ev.currentTarget`.
- Use `HASSDomTargetEvent<T>` only to read the element that originated the event through `ev.target`.
- Use `HASSDomEvent<T>` to read a custom event payload through `ev.detail`.
- Use `ValueChangedEvent<T>` from `src/types.ts` for the standard `value-changed` event.
- Prefer an event type exported by the component being listened to, such as `HaSelectSelectEvent<T, Clearable>` or `HaDropdownSelectEvent<TValue, TData>`, over reconstructing its detail type.
- Use `ActionHandlerEvent` from `src/data/lovelace/action_handler.ts` for Lovelace tap, hold, and double-tap handlers.
Import event and element types with `import type`:
```ts
import type {
HASSDomCurrentTargetEvent,
HASSDomEvent,
HASSDomTargetEvent,
} from "../common/dom/fire_event";
import type { HaCheckbox } from "../components/ha-checkbox";
import type { HaEntityPicker } from "../components/entity/ha-entity-picker";
import type { HaRadioGroup } from "../components/radio/ha-radio-group";
import type { ValueChangedEvent } from "../types";
```
Type the handler so the selected property can be read directly. Do not cast `ev.currentTarget` or assign it to a single-use variable:
```ts
private _scopeChanged(ev: HASSDomCurrentTargetEvent<HaRadioGroup>): void {
this._scope = ev.currentTarget.value;
}
private _checkedChanged(ev: HASSDomTargetEvent<HaCheckbox>): void {
this._checked = ev.target.checked;
}
private _valueChanged(ev: ValueChangedEvent<string>): void {
this._value = ev.detail.value;
}
private _itemSelected(ev: HASSDomEvent<{ id: string }>): void {
this._selectedId = ev.detail.id;
}
```
Use intersections when a handler needs more than one facet of an event. Keep the native event type when the handler reads native fields such as `key`, modifier keys, `dataTransfer`, or focus relationships:
```ts
private _entityChanged(
ev: ValueChangedEvent<string> & HASSDomCurrentTargetEvent<HaEntityPicker>
): void {
ev.currentTarget.value = ev.detail.value;
}
private _keyDown(
ev: KeyboardEvent & HASSDomCurrentTargetEvent<HTMLInputElement>
): void {
if (ev.key === "Enter") {
this._submit(ev.currentTarget.value);
}
}
```
Dispatch Home Assistant component events with `fireEvent()` instead of constructing `Event` or `CustomEvent` directly. Register the event name and detail type by augmenting `HASSDomEvents`; use `undefined` when an event has no detail. `fireEvent()` constrains event names and supplied detail, and events bubble and cross shadow boundaries by default:
```ts
fireEvent(this, "item-selected", { id: item.id });
fireEvent(this, "refresh-requested");
```
When an event is already registered, derive handler and listener types from its registration rather than repeating the payload shape:
```ts
private _itemSelected(
ev: HASSDomEvent<HASSDomEvents["item-selected"]>
): void {
this._selectedId = ev.detail.id;
}
```
`HASSDomEvents` types `fireEvent()` calls. Augment `HTMLElementEventMap` for typed listeners on HTML elements, or `GlobalEventHandlersEventMap` when the event is handled on global event targets.
In component files, prefer placing global event declarations after the class at the bottom of the file. Preserve the existing placement when editing established files; foundational type, helper, and mixin files commonly keep declarations near the top before their consumers.
```ts
declare global {
interface HASSDomEvents {
"item-selected": { id: string };
"refresh-requested": undefined;
}
interface HTMLElementEventMap {
"item-selected": HASSDomEvent<HASSDomEvents["item-selected"]>;
}
}
```
+1 -1
View File
@@ -70,4 +70,4 @@ Configuration and props:
- Identify behavioral regressions, bugs, accessibility issues, and missing tests first.
- Keep style-only comments secondary unless they affect maintainability or user experience.
- Prefer small, direct fixes over large refactors during review follow-up.
- Cross-load `ha-frontend-contexts`, `ha-frontend-components`, `ha-frontend-events`, `ha-frontend-styling`, `ha-frontend-testing`, or `ha-frontend-user-facing-text` when a finding falls in that area.
- Cross-load `ha-frontend-contexts`, `ha-frontend-components`, `ha-frontend-styling`, `ha-frontend-testing`, or `ha-frontend-user-facing-text` when a finding falls in that area.
+1 -1
View File
@@ -62,7 +62,7 @@ Managed app, demo, gallery, and E2E app workflows share one lifetime lock, so on
`yarn dev:serve` also serves locally and supports `-c` for the core URL and `-p` for the port. The default is 8124, or 8123 in a devcontainer.
Dev server commands support `--background`, `--status`, `--stop`, and `--logs [--follow]`. `yarn dev`, `yarn dev:serve`, `yarn dev:demo`, and `yarn dev:gallery` also support `--fetch-translations`; this runs translation fetching, including first-time GitHub device authentication, under the workflow lock before starting the watcher. It works in foreground and background modes. Prefer managed background mode while iterating so the watcher stays available across test runs without occupying the terminal. `yarn dev` and `yarn dev:serve` share one managed process slot because both write the app output.
Dev server commands support `--background`, `--status`, `--stop`, and `--logs [--follow]`. Prefer managed background mode while iterating so the watcher stays available across test runs without occupying the terminal. `yarn dev` and `yarn dev:serve` share one managed process slot because both write the app output.
## Playwright E2E
+2 -2
View File
@@ -32,12 +32,12 @@ jobs:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
with:
languages: javascript-typescript
build-mode: none
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
uses: github/codeql-action/analyze@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
with:
category: "/language:javascript-typescript"
+4 -4
View File
@@ -38,7 +38,7 @@ jobs:
name: Prepare container dependencies
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.62.1-noble
image: mcr.microsoft.com/playwright:v1.62.0-noble
options: --user 1001
defaults:
run:
@@ -154,7 +154,7 @@ jobs:
- prepare-container-dependencies
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.62.1-noble
image: mcr.microsoft.com/playwright:v1.62.0-noble
options: --user 1001 --ipc=host
defaults:
run:
@@ -206,7 +206,7 @@ jobs:
- prepare-container-dependencies
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.62.1-noble
image: mcr.microsoft.com/playwright:v1.62.0-noble
options: --user 1001 --ipc=host
defaults:
run:
@@ -260,7 +260,7 @@ jobs:
- prepare-container-dependencies
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.62.1-noble
image: mcr.microsoft.com/playwright:v1.62.0-noble
options: --user 1001 --ipc=host
defaults:
run:
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Verify version
uses: home-assistant/actions/helpers/verify-version@ab22029681aa532bfe7de5774a9972d67bfbd2c0 # master
uses: home-assistant/actions/helpers/verify-version@e3fb68ebda13d88a0d695082f471ba2c83d025fb # master
- name: Setup Node and install
uses: ./.github/actions/setup
+944
View File
File diff suppressed because one or more lines are too long
-1000
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -13,4 +13,4 @@ nodeLinker: node-modules
npmMinimalAgeGate: 3d
yarnPath: .yarn/releases/yarn-4.18.0.cjs
yarnPath: .yarn/releases/yarn-4.17.1.cjs
-1
View File
@@ -40,7 +40,6 @@ Detailed guidance lives in project skills under `.agents/skills/`. Load the matc
- `ha-frontend-contexts`: Lit contexts, `hass` migration, and rerender-sensitive state access.
- `ha-frontend-components`: dialogs, forms, alerts, shortcuts, tooltips, panels, and Lovelace cards.
- `ha-frontend-events`: event handler typing, custom event dispatch, and event-map declarations.
- `ha-frontend-styling`: theme variables, spacing tokens, responsive layout, RTL, and view transitions.
- `ha-frontend-testing`: lint, typecheck, Vitest, Playwright e2e dev servers, and benchmarks.
- `ha-frontend-user-facing-text`: localization, terminology, sentence case, and Home Assistant text style.
+8 -26
View File
@@ -19,7 +19,6 @@ import {
acquireProcessRecord,
isProcessRecordAlive,
outputLog,
offerToStopProcessRecord,
processStartTime,
readProcessRecord,
releaseProcessRecord,
@@ -101,16 +100,9 @@ const readBuild = () => readProcessRecord(lockFile);
const releaseBuild = (token) => releaseProcessRecord(lockFile, token);
const stopCommandFor = (owner) =>
owner?.kind === "build"
? "yarn build --stop"
: owner?.kind === "dev"
? `yarn ${devCommand(owner.suite)} --stop`
: undefined;
const acquireBuild = async (modern, foreground) => {
const acquireBuild = (modern, foreground) => {
const token = `${process.pid}-${Date.now()}-${Math.random()}`;
const record = {
const result = acquireProcessRecord(lockFile, {
pid: process.pid,
startTime: processStartTime(process.pid),
processGroup: false,
@@ -119,20 +111,8 @@ const acquireBuild = async (modern, foreground) => {
modern,
starting: true,
token,
};
const result = acquireProcessRecord(lockFile, record);
if (result.acquired) {
return { token };
}
reportExisting(result.existing);
return (await offerToStopProcessRecord({
file: lockFile,
owner: result.existing,
ownerDescription: describeOutputOwner(result.existing),
stopCommand: stopCommandFor(result.existing),
}))
? acquireBuild(modern, foreground)
: { existing: result.existing };
});
return result.acquired ? { token } : { existing: result.existing };
};
const updateBuild = (token, child, processGroup) => {
@@ -177,8 +157,9 @@ const reportExisting = (existing) => {
};
const runForeground = async (modern) => {
const lock = await acquireBuild(modern, true);
const lock = acquireBuild(modern, true);
if (!lock.token) {
reportExisting(lock.existing);
return 1;
}
try {
@@ -196,8 +177,9 @@ const runForeground = async (modern) => {
};
const runBackground = async (modern) => {
const lock = await acquireBuild(modern, false);
const lock = acquireBuild(modern, false);
if (!lock.token) {
reportExisting(lock.existing);
return 1;
}
let child;
+43 -124
View File
@@ -8,9 +8,6 @@
// --status Report whether the suite's dev server is running.
// --stop Stop a running background dev server.
// --logs [--follow] Print (or follow) the background dev server log.
// --fetch-translations
// Fetch nightly translations before starting (app,
// app-serve, demo, and gallery only).
//
// Extra args (for example -p or -c on app-serve) are forwarded to the underlying
// script. Suites use one of two liveness models:
@@ -27,10 +24,8 @@ import { fileURLToPath } from "node:url";
import {
LIFECYCLE_MODE_FLAGS,
acquireProcessRecord,
detectCodingAgent,
isProcessRecordAlive,
outputLog,
offerToStopProcessRecord,
processStartTime,
readProcessRecord,
releaseProcessRecord,
@@ -78,7 +73,6 @@ const SUITES = new Map([
"demo",
{
alias: "dev:demo",
fetchTranslations: true,
liveness: "health",
port: 8090,
spawn: { cmd: gulpBin, args: ["develop-demo"] },
@@ -88,7 +82,6 @@ const SUITES = new Map([
"gallery",
{
alias: "dev:gallery",
fetchTranslations: true,
liveness: "health",
port: 8100,
spawn: { cmd: gulpBin, args: ["develop-gallery"] },
@@ -98,7 +91,6 @@ const SUITES = new Map([
"app",
{
alias: "dev",
fetchTranslations: true,
liveness: "process",
readyLog: /Build done @/,
spawn: { cmd: gulpBin, args: ["develop-app"] },
@@ -110,7 +102,6 @@ const SUITES = new Map([
alias: "dev:serve",
liveness: "process",
acceptsArgs: true,
fetchTranslations: true,
readyLog: /Build done @/,
spawn: { cmd: developAndServeScript, args: [] },
},
@@ -125,18 +116,40 @@ const READY_TIMEOUT_MS =
? readyTimeoutSeconds * 1000
: 180_000;
// Detect a coding agent from a small set of environment markers set by common
// agent CLIs (env-only; no process-ancestry detection).
const detectAgent = () => {
const env = process.env;
const has = (name) => Boolean(env[name]);
const eq = (name, value) => env[name] === value;
const signals = {
opencode: () =>
[
"OPENCODE",
"OPENCODE_BIN_PATH",
"OPENCODE_SERVER",
"OPENCODE_APP_INFO",
].some(has),
"claude-code": () => has("CLAUDECODE"),
cursor: () => has("CURSOR_TRACE_ID"),
"github-copilot": () =>
eq("TERM_PROGRAM", "vscode") && eq("GIT_PAGER", "cat"),
// Convention shared by several agents (Crush, Amp, ...).
generic: () => has("AGENT") || has("AI_AGENT"),
};
return Object.keys(signals).find((id) => signals[id]());
};
const usage = () => {
const suites = [...SUITES.keys()].join("|");
process.stderr.write(
`Usage: node build-scripts/dev-server.mjs --suite <${suites}> ` +
`[--background | --status | --stop | --logs [--follow]] ` +
`[--fetch-translations]\n`
`[--background | --status | --stop | --logs [--follow]]\n`
);
};
const parseArgs = (argv) => {
const args = {
fetchTranslations: false,
mode: "foreground",
follow: false,
modes: [],
@@ -152,9 +165,6 @@ const parseArgs = (argv) => {
case "--follow":
args.follow = true;
break;
case "--fetch-translations":
args.fetchTranslations = true;
break;
default:
if (LIFECYCLE_MODE_FLAGS.has(arg)) {
args.mode = LIFECYCLE_MODE_FLAGS.get(arg);
@@ -168,35 +178,12 @@ const parseArgs = (argv) => {
return args;
};
const translationPrebuildEnv = (token) => {
const env = workflowLockEnv(token);
delete env.SKIP_FETCH_NIGHTLY_TRANSLATIONS;
return env;
};
const suiteEnv = (token, fetchTranslations = false) => ({
...workflowLockEnv(token),
...(fetchTranslations && { SKIP_FETCH_NIGHTLY_TRANSLATIONS: "1" }),
});
const runPrebuild = (token, fetchTranslations = false) =>
fetchTranslations
? spawnForeground({
cmd: gulpBin,
args: ["setup-and-fetch-nightly-translations"],
cwd: repoRoot,
env: translationPrebuildEnv(token),
processGroup: true,
})
: Promise.resolve(0);
const logFileFor = (suite) => path.join(logDir, `${suite}.log`);
const acquireSuite = (suite) => {
const token = `${process.pid}-${Date.now()}-${Math.random()}`;
const record = {
pid: process.pid,
startTime: processStartTime(process.pid),
processGroup: false,
kind: "dev",
suite,
starting: true,
@@ -267,29 +254,12 @@ const reportProcessConflict = (suite, existing) => {
);
};
const stopCommandFor = (owner) =>
owner?.kind === "build"
? "yarn build --stop"
: owner?.kind === "dev"
? `yarn ${SUITES.get(owner.suite)?.alias ?? "dev"} --stop`
: undefined;
const acquireSuiteForStart = async (suite) => {
const acquireSuiteForStart = (suite) => {
const lock = acquireSuite(suite);
if (lock.token) {
return lock;
}
reportProcessConflict(suite, lock.existing);
if (
await offerToStopProcessRecord({
file: workflowLockFile,
owner: lock.existing,
ownerDescription: describeOutputOwner(lock.existing),
stopCommand: stopCommandFor(lock.existing),
})
) {
return acquireSuiteForStart(suite);
}
return {
code:
lock.existing?.kind === "dev" &&
@@ -435,9 +405,9 @@ const isHttpServing = async (port, timeoutMs = 1000) => {
return probeHost(0);
};
const runForegroundHealth = async (suite, cfg, fetchTranslations = false) => {
const runForegroundHealth = async (suite, cfg) => {
const { port } = cfg;
const lock = await acquireSuiteForStart(suite);
const lock = acquireSuiteForStart(suite);
if (!lock.token) {
return lock.code;
}
@@ -457,15 +427,11 @@ const runForegroundHealth = async (suite, cfg, fetchTranslations = false) => {
return 1;
}
try {
const prebuildCode = await runPrebuild(lock.token, fetchTranslations);
if (prebuildCode !== 0) {
return prebuildCode;
}
return await spawnForeground({
cmd: cfg.spawn.cmd,
args: cfg.spawn.args,
cwd: repoRoot,
env: suiteEnv(lock.token, fetchTranslations),
env: workflowLockEnv(lock.token),
processGroup: true,
onSpawn: (child) => updateSuite(suite, lock.token, child, port),
});
@@ -474,9 +440,9 @@ const runForegroundHealth = async (suite, cfg, fetchTranslations = false) => {
}
};
const runBackgroundHealth = async (suite, cfg, fetchTranslations = false) => {
const runBackgroundHealth = async (suite, cfg) => {
const { port } = cfg;
const lock = await acquireSuiteForStart(suite);
const lock = acquireSuiteForStart(suite);
if (!lock.token) {
return lock.code;
}
@@ -497,17 +463,12 @@ const runBackgroundHealth = async (suite, cfg, fetchTranslations = false) => {
}
let child;
try {
const prebuildCode = await runPrebuild(lock.token, fetchTranslations);
if (prebuildCode !== 0) {
releaseSuite(lock.token);
return prebuildCode;
}
const logFile = logFileFor(suite);
child = await spawnDetachedToLog({
cmd: cfg.spawn.cmd,
args: cfg.spawn.args,
cwd: repoRoot,
env: suiteEnv(lock.token, fetchTranslations),
env: workflowLockEnv(lock.token),
logFile,
});
updateSuite(suite, lock.token, child, port);
@@ -559,26 +520,17 @@ const spawnArgs = (cfg, passthrough) => [
...(cfg.acceptsArgs ? passthrough : []),
];
const runForegroundProcess = async (
suite,
cfg,
passthrough,
fetchTranslations = false
) => {
const lock = await acquireSuiteForStart(suite);
const runForegroundProcess = async (suite, cfg, passthrough) => {
const lock = acquireSuiteForStart(suite);
if (!lock.token) {
return lock.code;
}
try {
const prebuildCode = await runPrebuild(lock.token, fetchTranslations);
if (prebuildCode !== 0) {
return prebuildCode;
}
return await spawnForeground({
cmd: cfg.spawn.cmd,
args: spawnArgs(cfg, passthrough),
cwd: repoRoot,
env: suiteEnv(lock.token, fetchTranslations),
env: workflowLockEnv(lock.token),
processGroup: true,
onSpawn: (child) => updateSuite(suite, lock.token, child),
});
@@ -587,30 +539,20 @@ const runForegroundProcess = async (
}
};
const runBackgroundProcess = async (
suite,
cfg,
passthrough,
fetchTranslations = false
) => {
const lock = await acquireSuiteForStart(suite);
const runBackgroundProcess = async (suite, cfg, passthrough) => {
const lock = acquireSuiteForStart(suite);
if (!lock.token) {
return lock.code;
}
let child;
try {
const prebuildCode = await runPrebuild(lock.token, fetchTranslations);
if (prebuildCode !== 0) {
releaseSuite(lock.token);
return prebuildCode;
}
const logFile = logFileFor(suite);
child = await spawnDetachedToLog({
cmd: cfg.spawn.cmd,
args: spawnArgs(cfg, passthrough),
cwd: repoRoot,
env: suiteEnv(lock.token, fetchTranslations),
env: workflowLockEnv(lock.token),
logFile,
});
@@ -688,17 +630,6 @@ const main = async () => {
usage();
return 1;
}
if (
args.fetchTranslations &&
(!["foreground", "background"].includes(args.mode) ||
!cfg.fetchTranslations)
) {
process.stderr.write(
"--fetch-translations is only supported when starting app, app-serve, demo, or gallery.\n"
);
usage();
return 1;
}
if (args.passthrough.length && !cfg.acceptsArgs) {
process.stderr.write(
`Ignoring unexpected arguments: ${args.passthrough.join(" ")}\n`
@@ -712,7 +643,7 @@ const main = async () => {
mode === "foreground" &&
!["0", "false"].includes(process.env.HA_DEV_BACKGROUND)
) {
const agent = detectCodingAgent();
const agent = detectAgent();
if (agent) {
process.stdout.write(
`Detected coding agent (${agent}); starting in the background. ` +
@@ -734,26 +665,14 @@ const main = async () => {
const handlers =
cfg.liveness === "health"
? {
foreground: () =>
runForegroundHealth(args.suite, cfg, args.fetchTranslations),
background: () =>
runBackgroundHealth(args.suite, cfg, args.fetchTranslations),
foreground: () => runForegroundHealth(args.suite, cfg),
background: () => runBackgroundHealth(args.suite, cfg),
}
: {
foreground: () =>
runForegroundProcess(
args.suite,
cfg,
args.passthrough,
args.fetchTranslations
),
runForegroundProcess(args.suite, cfg, args.passthrough),
background: () =>
runBackgroundProcess(
args.suite,
cfg,
args.passthrough,
args.fetchTranslations
),
runBackgroundProcess(args.suite, cfg, args.passthrough),
};
return handlers[mode]();
};
-12
View File
@@ -58,12 +58,6 @@ const getCommonTemplateVars = () => {
return {
modernRegex: compileRegex(browserRegexes.concat(haMacOSRegex)).toString(),
hassUrl: process.env.HASS_URL || "",
// Single source for the stale-build recovery patterns, shared with the
// bundled src/util/recover-stale-build.ts and injected into the inline
// boot guard (_bootstrap_recovery.html.template).
staleBuildPatterns: fs.readJsonSync(
resolve(paths.root_dir, "src/util/stale-build-patterns.json")
),
};
};
@@ -113,9 +107,6 @@ const genPagesDevTask =
resolve(inputRoot, inputSub, `${page}.template`),
{
...commonVars,
// Dev entries are unhashed, so the stale-index recovery guard has
// nothing to key off and rebuild churn could cause spurious reloads.
useCacheRecovery: false,
latestEntryJS: entries.map(
(entry) => `${publicRoot}/frontend_latest/${entry}.js`
),
@@ -155,9 +146,6 @@ const genPagesProdTask =
resolve(inputRoot, inputSub, `${page}.template`),
{
...commonVars,
// Recover from a stale index.html that pins deleted hashed entry
// bundles (see _bootstrap_recovery.html.template).
useCacheRecovery: true,
latestEntryJS: entries.map((entry) => latestManifest[`${entry}.js`]),
es5EntryJS: outputES5
? entries.map((entry) => es5Manifest[`${entry}.js`])
-184
View File
@@ -1,7 +1,6 @@
import { spawn, execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { createInterface } from "node:readline/promises";
export const LIFECYCLE_MODE_FLAGS = new Map([
["--background", "background"],
@@ -10,134 +9,6 @@ export const LIFECYCLE_MODE_FLAGS = new Map([
["--logs", "logs"],
]);
const AGENT_PROVIDERS = [
{
id: "opencode",
env: [
"OPENCODE",
"OPENCODE_BIN_PATH",
"OPENCODE_SERVER",
"OPENCODE_APP_INFO",
"OPENCODE_MODES",
],
processes: ["opencode"],
},
{
id: "claude-code",
env: ["CLAUDECODE"],
processes: ["claude"],
},
{
id: "cursor",
env: ["CURSOR_TRACE_ID"],
processes: [],
},
{
id: "github-copilot",
matchesEnv: (env) =>
env.TERM_PROGRAM === "vscode" && env.GIT_PAGER === "cat",
processes: [],
},
{
id: "generic",
env: ["AGENT", "AI_AGENT"],
processes: [],
},
];
const MAX_ANCESTRY_HOPS = 24;
const readProcessParent = (pid) => {
try {
const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
const commandEnd = stat.lastIndexOf(")");
const commandStart = stat.indexOf("(");
if (commandStart === -1 || commandEnd === -1) {
return undefined;
}
const parentPid = Number.parseInt(
stat.slice(commandEnd + 2).split(" ")[1] ?? "",
10
);
return Number.isFinite(parentPid)
? {
command: stat.slice(commandStart + 1, commandEnd).toLowerCase(),
parentPid,
}
: undefined;
} catch {
return undefined;
}
};
export const detectCodingAgent = (env = process.env, pid = process.pid) => {
if (env.HA_CODING_AGENT === "0") {
return undefined;
}
const envMatch = AGENT_PROVIDERS.find(
(provider) =>
provider.matchesEnv?.(env) || provider.env?.some((name) => env[name])
);
if (envMatch) {
return envMatch.id;
}
if (env.HA_CODING_AGENT === "1") {
return "unknown";
}
let currentPid = pid;
for (let hop = 0; hop < MAX_ANCESTRY_HOPS; hop++) {
const processInfo = readProcessParent(currentPid);
if (!processInfo) {
return undefined;
}
const processMatch = AGENT_PROVIDERS.find((provider) =>
provider.processes.some((name) => processInfo.command.includes(name))
);
if (processMatch) {
return processMatch.id;
}
if (processInfo.parentPid <= 1) {
return undefined;
}
currentPid = processInfo.parentPid;
}
return undefined;
};
const canPromptForConflict = () =>
Boolean(process.stdin.isTTY && process.stderr.isTTY && !detectCodingAgent());
const formatCommand = (command) =>
!("NO_COLOR" in process.env)
? `\u001b[1;36m${command}\u001b[0m`
: `\`${command}\``;
const confirmStopConflict = async (ownerDescription, stopCommand) => {
const readline = createInterface({
input: process.stdin,
output: process.stderr,
});
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 30_000);
try {
process.stderr.write(
`${ownerDescription} can be stopped with ${formatCommand(stopCommand)}.\n`
);
const answer = await readline.question("Stop it and continue? [y/N] ", {
signal: controller.signal,
});
return ["y", "yes"].includes(answer.trim().toLowerCase());
} catch (err) {
if (err?.name !== "AbortError") {
throw err;
}
process.stderr.write("\n");
return false;
} finally {
clearTimeout(timeout);
readline.close();
}
};
export const sleep = (ms) =>
new Promise((resolve) => {
setTimeout(resolve, ms);
@@ -449,61 +320,6 @@ export const terminateProcess = async ({
return await isStopped();
};
const sameProcessRecord = (current, expected) =>
Boolean(expected?.token) && current?.token === expected.token;
const stopProcessRecord = async (file, owner) => {
let current = readProcessRecord(file);
if (!current) {
return true;
}
if (!sameProcessRecord(current, owner)) {
return false;
}
if (!isProcessRecordAlive(current)) {
releaseProcessRecord(file, current.token);
return true;
}
const stopped = await terminateProcess({
pid: current.pid,
processGroup: current.processGroup ?? false,
isStopped: () => {
const latest = readProcessRecord(file);
return (
!sameProcessRecord(latest, owner) ||
!latest ||
!isProcessRecordAlive(latest)
);
},
});
current = readProcessRecord(file);
if (current && !sameProcessRecord(current, owner)) {
return false;
}
if (!stopped && current) {
return false;
}
releaseProcessRecord(file, owner.token);
return true;
};
export const offerToStopProcessRecord = async ({
file,
owner,
ownerDescription,
stopCommand,
}) => {
if (
!owner ||
!stopCommand ||
!canPromptForConflict() ||
!(await confirmStopConflict(ownerDescription, stopCommand))
) {
return false;
}
return stopProcessRecord(file, owner);
};
export const terminateDetachedProcess = (child) =>
terminateProcess({
pid: child.pid,
@@ -23,7 +23,7 @@ export class DemoHaProgressButton extends LitElement {
<ha-progress-button @click=${this._clickedFail}>
Fail
</ha-progress-button>
<ha-progress-button size="small" @click=${this._clickedSuccess}>
<ha-progress-button size="s" @click=${this._clickedSuccess}>
small
</ha-progress-button>
<ha-progress-button
+7 -7
View File
@@ -52,13 +52,13 @@
"@codemirror/view": "6.43.7",
"@date-fns/tz": "1.5.0",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "7.6.0",
"@formatjs/intl-datetimeformat": "7.5.2",
"@formatjs/intl-displaynames": "7.3.13",
"@formatjs/intl-durationformat": "0.10.18",
"@formatjs/intl-getcanonicallocales": "3.2.11",
"@formatjs/intl-listformat": "8.3.13",
"@formatjs/intl-locale": "5.3.10",
"@formatjs/intl-numberformat": "9.4.0",
"@formatjs/intl-numberformat": "9.3.14",
"@formatjs/intl-pluralrules": "6.3.13",
"@formatjs/intl-relativetimeformat": "12.3.13",
"@fullcalendar/core": "6.1.21",
@@ -107,7 +107,7 @@
"hls.js": "1.6.16",
"home-assistant-js-websocket": "9.6.0",
"idb-keyval": "6.3.0",
"intl-messageformat": "11.2.13",
"intl-messageformat": "11.2.12",
"js-yaml": "5.2.2",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
@@ -150,9 +150,9 @@
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.1.0",
"@octokit/rest": "22.0.1",
"@playwright/test": "1.62.1",
"@playwright/test": "1.62.0",
"@rsdoctor/rspack-plugin": "1.6.1",
"@rspack/core": "2.1.7",
"@rspack/core": "2.1.6",
"@rspack/dev-server": "2.1.0",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.26",
@@ -193,7 +193,7 @@
"gulp-rename": "2.1.0",
"html-minifier-terser": "7.2.0",
"husky": "9.1.7",
"jsdom": "30.0.1",
"jsdom": "30.0.0",
"jszip": "3.10.1",
"license-checker-rseidelsohn": "5.0.1",
"lint-staged": "17.2.0",
@@ -228,7 +228,7 @@
"tslib": "2.8.1",
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch"
},
"packageManager": "yarn@4.18.0",
"packageManager": "yarn@4.17.1",
"volta": {
"node": "24.18.1"
}
+10 -6
View File
@@ -18,6 +18,8 @@ export class HaProgressButton extends LitElement {
@property() appearance: Appearance = "accent";
@property() size: "xs" | "s" | "m" | "l" | "xl" = "m";
@property({ attribute: false }) public iconPath?: string;
@property() variant: "brand" | "danger" | "neutral" | "warning" | "success" =
@@ -32,6 +34,7 @@ export class HaProgressButton extends LitElement {
return html`
<ha-button
.appearance=${appearance}
.size=${this.size}
.disabled=${this.disabled}
.loading=${this.progress}
.variant=${
@@ -118,15 +121,16 @@ export class HaProgressButton extends LitElement {
width: 100%;
}
/* Fade the content out rather than hiding it, so the button keeps its
accessible name while the result icon covers it. */
/* The icon lives in this shadow root, so callers cannot size it themselves. */
ha-button[size="xs"] ha-svg-icon[slot="start"],
ha-button[size="s"] ha-svg-icon[slot="start"] {
--mdc-icon-size: 16px;
}
ha-button.result::part(start),
ha-button.result::part(end),
ha-button.result::part(label),
ha-button.result::part(caret) {
opacity: 0;
}
ha-button.result::part(caret),
ha-button.result::part(spinner) {
visibility: hidden;
}
@@ -45,7 +45,6 @@ import {
} from "../../data/media_source";
import { isTTSMediaSource } from "../../data/tts";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { panelIsReady } from "../../layouts/panel-ready";
import { haStyle, haStyleScrollbar } from "../../resources/styles";
import { loadVirtualizer } from "../../resources/virtualizer";
import type { HomeAssistant } from "../../types";
@@ -162,8 +161,6 @@ export class HaMediaPlayerBrowse extends LitElement {
private _resizeObserver?: ResizeObserver;
private _initialReady = false;
public connectedCallback(): void {
super.connectedCallback();
this.updateComplete.then(() => this._attachResizeObserver());
@@ -277,7 +274,6 @@ export class HaMediaPlayerBrowse extends LitElement {
ids: navigateIds,
current: this._currentItem,
});
this._signalInitialReady();
} else {
if (!currentProm) {
currentProm = this._fetchData(
@@ -293,7 +289,6 @@ export class HaMediaPlayerBrowse extends LitElement {
ids: navigateIds,
current: item,
});
this._signalInitialReady();
},
(err) => {
// When we change entity ID, we will first try to see if the new entity is
@@ -327,10 +322,8 @@ export class HaMediaPlayerBrowse extends LitElement {
),
code: "entity_not_found",
});
this._signalInitialReady();
} else {
this._setError(err);
this._signalInitialReady();
}
}
);
@@ -1152,21 +1145,6 @@ export class HaMediaPlayerBrowse extends LitElement {
fireEvent(this, "close-dialog");
}
private _signalInitialReady(): void {
if (this._initialReady) {
return;
}
this._initialReady = true;
const root = this.getRootNode();
panelIsReady(
root instanceof ShadowRoot &&
root.host instanceof HTMLElement &&
root.host.tagName.startsWith("HA-PANEL-")
? root.host
: this
);
}
private _setError(error: any) {
if (!this.dialog) {
this._error = error;
@@ -1231,8 +1209,8 @@ export class HaMediaPlayerBrowse extends LitElement {
}
private _animateHeaderHeight() {
let start: number | undefined;
const animate = (time: number) => {
let start;
const animate = (time) => {
if (start === undefined) {
start = time;
}
-1
View File
@@ -23,7 +23,6 @@ export interface LovelaceViewElement extends HTMLElement {
badges?: HuiBadge[];
sections?: HuiSection[];
isStrategy: boolean;
initialRenderComplete?: Promise<void>;
setConfig(config: LovelaceViewConfig): void;
}
+1 -12
View File
@@ -37,26 +37,15 @@ declare global {
}
const clearUrlParams = () => {
const searchParams = new URLSearchParams(location.search);
let changed = false;
// Clear auth data from url if we have been able to establish a connection
if (location.search.includes("auth_callback=1")) {
const searchParams = new URLSearchParams(location.search);
// https://github.com/home-assistant/home-assistant-js-websocket/blob/master/lib/auth.ts
// Remove all data from QueryCallbackData type
searchParams.delete("auth_callback");
searchParams.delete("code");
searchParams.delete("state");
searchParams.delete("storeToken");
changed = true;
}
// Remove the cache-busting param added by the stale-index recovery guard in
// index.html once we have booted successfully, so it doesn't linger in the
// URL (and stops acting as the guard's one-shot loop marker).
if (searchParams.has("ha_cache_bust")) {
searchParams.delete("ha_cache_bust");
changed = true;
}
if (changed) {
const search = searchParams.toString();
history.replaceState(
null,
-130
View File
@@ -1,130 +0,0 @@
<script>
/*
* Boot-time recovery from a stale index.html.
*
* index.html pins the content-hashed entry bundles (core.<hash>.js /
* app.<hash>.js). After a frontend release those files are deleted from
* disk, so a cached (stale) index.html imports URLs that now 404 and the
* app never boots - the launch screen stays up forever. No bundled JS can
* recover this, because the bundle itself failed to load, so this guard has
* to live inline in the HTML document.
*
* On a failed entry load we navigate once to the same URL with a
* cache-busting query param (and, on https, after dropping the service
* worker + its caches) so the browser fetches a fresh index.html that points
* at the current hashes. The param doubles as a one-shot loop guard: if the
* freshly fetched index still fails we stop and show a message instead of
* reloading forever. core.ts strips the param again after a successful boot.
*/
(function () {
var BUST_PARAM = "ha_cache_bust";
// Only production entry bundles carry a content hash. Requiring the hash
// keeps the guard inert in development (unhashed core.js / app.js) and
// scoped to the entry chunks whose 404 is fatal to boot - not translations
// or lazily loaded panels, which fail non-fatally and are handled elsewhere.
// Injected from stale-build-patterns.json (the single source shared with
// the bundled util/recover-stale-build.ts) so the two never drift.
var HASHED_ENTRY = new RegExp(<%= JSON.stringify(staleBuildPatterns.hashedEntry) %>, "i");
var MODULE_ERROR = new RegExp(<%= JSON.stringify(staleBuildPatterns.moduleError) %>, "i");
function booted() {
// The launch screen is only removed once the app has taken over the
// page. If it is gone, any later chunk failure is post-boot (a lazy
// panel/dialog) and is not the stale-index boot failure this guard
// targets, so we leave it alone.
return !document.getElementById("ha-launch-screen");
}
function showFatal() {
var box = document.getElementById("ha-launch-screen-info-box");
if (box) {
box.textContent =
"Could not load Home Assistant. Please refresh the page, and clear your browser cache if the problem persists.";
}
}
function recover() {
if (location.search.indexOf(BUST_PARAM + "=") !== -1) {
// We already reloaded once with a fresh index and it still failed;
// stop to avoid a reload loop.
showFatal();
return;
}
var target =
location.pathname +
location.search +
(location.search ? "&" : "?") +
BUST_PARAM +
"=" +
Date.now() +
location.hash;
// On https the service worker "/" route ignores the query string
// (ignoreSearch), so a bust param alone would still be answered with the
// stale cached index. Drop the worker and its caches first so the reload
// is served fresh from the network.
if ("serviceWorker" in navigator && navigator.serviceWorker.controller) {
var go = function () {
location.replace(target);
};
Promise.all([
navigator.serviceWorker
.getRegistrations()
.then(function (regs) {
return Promise.all(
regs.map(function (r) {
return r.unregister();
})
);
})
.catch(function () {}),
self.caches
? caches
.keys()
.then(function (keys) {
return Promise.all(
keys.map(function (k) {
return caches.delete(k);
})
);
})
.catch(function () {})
: null,
]).then(go, go);
} else {
location.replace(target);
}
}
function maybeRecover(url, message) {
if (booted()) {
return;
}
if (
(url && HASHED_ENTRY.test(url)) ||
(message && (HASHED_ENTRY.test(message) || MODULE_ERROR.test(message)))
) {
recover();
}
}
// Resource-load errors (<script>, modulepreload <link>) do not bubble, so
// they are only observable in the capture phase at the window.
window.addEventListener(
"error",
function (ev) {
var el = ev.target;
if (el && (el.tagName === "SCRIPT" || el.tagName === "LINK")) {
maybeRecover(el.href || el.src, null);
}
},
true
);
// A failed dynamic import() rejects; the inline entry imports do not catch
// it, so the rejection surfaces here.
window.addEventListener("unhandledrejection", function (ev) {
var reason = ev && ev.reason;
maybeRecover(null, reason && (reason.message || String(reason)));
});
})();
</script>
-1
View File
@@ -2,7 +2,6 @@
<html>
<head>
<title>Home Assistant</title>
<% if (useCacheRecovery) { %><%= renderTemplate("_bootstrap_recovery.html.template") %><% } %>
<%= renderTemplate("_header.html.template") %>
<link rel="mask-icon" href="/static/icons/mask-icon.svg" color="#18bcf2" />
<link
+1 -4
View File
@@ -1,7 +1,6 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import "../components/animation/ha-fade-in";
import "../components/ha-top-app-bar-fixed";
import "../components/ha-spinner";
import type { HomeAssistant } from "../types";
@@ -37,9 +36,7 @@ class HassLoadingScreen extends LitElement {
private _renderContent(): TemplateResult {
return html`
<div class="content">
<ha-fade-in .delay=${500}>
<ha-spinner></ha-spinner>
</ha-fade-in>
<ha-spinner></ha-spinner>
${
this.message
? html`<div id="loading-text">${this.message}</div>`
-57
View File
@@ -1,5 +1,3 @@
import { ContextProvider, createContext } from "@lit/context";
import type { ReactiveController, ReactiveControllerHost } from "lit";
import { ReactiveElement } from "lit";
import { fireEvent } from "../common/dom/fire_event";
@@ -17,61 +15,6 @@ export const panelIsReady = async (element: HTMLElement) => {
fireEvent(element, "hass-panel-ready");
};
export type RegisterChildPanelReady = (ready: Promise<void>) => void;
export const childPanelReadyContext =
createContext<RegisterChildPanelReady>("child-panel-ready");
export class ChildPanelReady implements ReactiveController {
private _promises: Promise<void>[] = [];
private _host: ReactiveControllerHost & HTMLElement;
private _resolveReady?: () => void;
private _completing = false;
public ready = new Promise<void>((resolve) => {
this._resolveReady = resolve;
});
public constructor(host: ReactiveControllerHost & HTMLElement) {
this._host = host;
host.addController(this);
new ContextProvider(host, {
context: childPanelReadyContext,
initialValue: (ready) => this._promises.push(ready),
});
}
public hostUpdated() {
if (this._completing) {
return;
}
this._completing = true;
this._host.removeController(this);
void this._complete();
}
private async _complete() {
// Children created/updated during this render register after the host's
// update commits. Wait for that before snapshotting readiness promises.
await this._host.updateComplete;
await this._waitForPromises();
this._resolveReady?.();
await panelIsReady(this._host);
}
private _waitForPromises(): Promise<void> {
const count = this._promises.length;
return Promise.allSettled(this._promises).then(() =>
this._promises.length > count ? this._waitForPromises() : undefined
);
}
}
export class PanelReady {
public ready?: Promise<void>;
+28 -67
View File
@@ -19,66 +19,29 @@ import { HassRouterPage } from "./hass-router-page";
const CACHE_URL_PATHS = ["lovelace", "home", "config"];
const PANEL_READY_TIMEOUT = 2000;
const DASHBOARD_READY_TIMEOUT = 5000;
const COMPONENTS = {
app: { load: () => import("../panels/app/ha-panel-app") },
energy: {
load: () => import("../panels/energy/ha-panel-energy"),
waitForReady: true,
readyTimeout: DASHBOARD_READY_TIMEOUT,
},
calendar: {
load: () => import("../panels/calendar/ha-panel-calendar"),
waitForReady: true,
},
config: { load: () => import("../panels/config/ha-panel-config") },
custom: { load: () => import("../panels/custom/ha-panel-custom") },
lovelace: {
load: () => import("../panels/lovelace/ha-panel-lovelace"),
waitForReady: true,
readyTimeout: DASHBOARD_READY_TIMEOUT,
},
history: { load: () => import("../panels/history/ha-panel-history") },
iframe: { load: () => import("../panels/iframe/ha-panel-iframe") },
logbook: { load: () => import("../panels/logbook/ha-panel-logbook") },
map: { load: () => import("../panels/map/ha-panel-map") },
my: { load: () => import("../panels/my/ha-panel-my") },
profile: { load: () => import("../panels/profile/ha-panel-profile") },
todo: { load: () => import("../panels/todo/ha-panel-todo") },
"media-browser": {
load: () => import("../panels/media-browser/ha-panel-media-browser"),
waitForReady: true,
},
light: {
load: () => import("../panels/light/ha-panel-light"),
waitForReady: true,
readyTimeout: DASHBOARD_READY_TIMEOUT,
},
security: {
load: () => import("../panels/security/ha-panel-security"),
waitForReady: true,
readyTimeout: DASHBOARD_READY_TIMEOUT,
},
climate: {
load: () => import("../panels/climate/ha-panel-climate"),
waitForReady: true,
readyTimeout: DASHBOARD_READY_TIMEOUT,
},
maintenance: {
load: () => import("../panels/maintenance/ha-panel-maintenance"),
waitForReady: true,
readyTimeout: DASHBOARD_READY_TIMEOUT,
},
home: {
load: () => import("../panels/home/ha-panel-home"),
waitForReady: true,
readyTimeout: DASHBOARD_READY_TIMEOUT,
},
notfound: { load: () => import("../panels/notfound/ha-panel-notfound") },
} satisfies Record<
string,
Pick<RouteOptions, "load" | "waitForReady"> & { readyTimeout?: number }
>;
app: () => import("../panels/app/ha-panel-app"),
energy: () => import("../panels/energy/ha-panel-energy"),
calendar: () => import("../panels/calendar/ha-panel-calendar"),
config: () => import("../panels/config/ha-panel-config"),
custom: () => import("../panels/custom/ha-panel-custom"),
lovelace: () => import("../panels/lovelace/ha-panel-lovelace"),
history: () => import("../panels/history/ha-panel-history"),
iframe: () => import("../panels/iframe/ha-panel-iframe"),
logbook: () => import("../panels/logbook/ha-panel-logbook"),
map: () => import("../panels/map/ha-panel-map"),
my: () => import("../panels/my/ha-panel-my"),
profile: () => import("../panels/profile/ha-panel-profile"),
todo: () => import("../panels/todo/ha-panel-todo"),
"media-browser": () =>
import("../panels/media-browser/ha-panel-media-browser"),
light: () => import("../panels/light/ha-panel-light"),
security: () => import("../panels/security/ha-panel-security"),
climate: () => import("../panels/climate/ha-panel-climate"),
maintenance: () => import("../panels/maintenance/ha-panel-maintenance"),
home: () => import("../panels/home/ha-panel-home"),
notfound: () => import("../panels/notfound/ha-panel-notfound"),
};
@customElement("partial-panel-resolver")
class PartialPanelResolver extends HassRouterPage {
@@ -163,12 +126,13 @@ class PartialPanelResolver extends HassRouterPage {
private _getRoutes(panels: Panels): RouterOptions {
const routes: RouterOptions["routes"] = {};
Object.values(panels).forEach((panel) => {
const component = COMPONENTS[panel.component_name];
const data: RouteOptions = {
tag: `ha-panel-${panel.component_name}`,
cache: CACHE_URL_PATHS.includes(panel.url_path),
...(component ?? {}),
};
if (panel.component_name in COMPONENTS) {
data.load = COMPONENTS[panel.component_name];
}
routes[panel.url_path] = data;
});
@@ -257,12 +221,9 @@ class PartialPanelResolver extends HassRouterPage {
)
) {
await this.rebuild();
const component =
COMPONENTS[this.hass.panels[this._currentPage].component_name];
await promiseTimeout(
component?.readyTimeout ?? PANEL_READY_TIMEOUT,
this.pageRendered
).catch(() => undefined);
await promiseTimeout(PANEL_READY_TIMEOUT, this.pageRendered).catch(
() => undefined
);
// Only fire frontend/loaded when this call actually removed the launch
// screen, so later panel updates do not fire it again. Native apps remove
// it instantly because their own splash screen is still visible.
+48 -64
View File
@@ -1,23 +1,14 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
fireEvent,
type HASSDomCurrentTargetEvent,
} from "../../common/dom/fire_event";
import "../../components/ha-button";
import "../../components/ha-dialog";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-dialog-footer";
import "../../components/radio/ha-radio-group";
import type { HaRadioGroup } from "../../components/radio/ha-radio-group";
import "../../components/radio/ha-radio-option";
import "../../components/ha-svg-icon";
import "../../components/ha-switch";
import "../../components/ha-dialog";
import { RecurrenceRange } from "../../data/calendar";
import type { HomeAssistant } from "../../types";
import type { ConfirmEventDialogBoxParams } from "./show-confirm-event-dialog-box";
// `RecurrenceRange.THISEVENT` is "", which the radio group treats as unselected
// on its value-attribute and form-reset paths, so keep the options on their own
// literals and map them when confirming.
type RecurrenceScope = "this" | "future";
import "../../components/ha-button";
@customElement("confirm-event-dialog-box")
class ConfirmEventDialogBox extends LitElement {
@@ -27,14 +18,11 @@ class ConfirmEventDialogBox extends LitElement {
@state() private _open = false;
@state() private _closeState?: "canceled" | "confirmed";
@state() private _scope: RecurrenceScope = "this";
@state()
private _closeState?: "canceled" | "confirmed" | "confirmedFuture";
public async showDialog(params: ConfirmEventDialogBoxParams): Promise<void> {
this._params = params;
this._scope = "this";
this._closeState = undefined;
this._open = true;
}
@@ -51,27 +39,20 @@ class ConfirmEventDialogBox extends LitElement {
return nothing;
}
const { destructive, recurring } = this._params;
return html`
<ha-dialog
.open=${this._open}
header-title=${this._params.title}
width="small"
type="alert"
aria-describedby=${recurring ? nothing : "description"}
@closed=${this._dialogClosed}
>
${
recurring
? this._renderRecurrenceRange()
: html`<p id="description">${this._params.text}</p>`
}
<div>
<p>${this._params.text}</p>
</div>
<ha-dialog-footer slot="footer">
<ha-button
appearance="plain"
@click=${this._dismiss}
?autofocus=${!recurring && destructive}
slot="secondaryAction"
>
${this.hass.localize("ui.common.cancel")}
@@ -79,41 +60,29 @@ class ConfirmEventDialogBox extends LitElement {
<ha-button
slot="primaryAction"
@click=${this._confirm}
?autofocus=${!recurring && !destructive}
variant=${destructive ? "danger" : "brand"}
autofocus
variant="danger"
>
${this._params.confirmText}
</ha-button>
${
this._params.confirmFutureText
? html`
<ha-button
@click=${this._confirmFuture}
slot="primaryAction"
variant="danger"
>
${this._params.confirmFutureText}
</ha-button>
`
: ""
}
</ha-dialog-footer>
</ha-dialog>
`;
}
private _renderRecurrenceRange() {
const thisEvent = this.hass.localize(
"ui.components.calendar.event.recurrence_range.this_event"
);
const thisAndFuture = this.hass.localize(
"ui.components.calendar.event.recurrence_range.this_and_future"
);
return html`
<ha-radio-group
name="recurrence_range"
.label=${this._params!.text ?? this._params!.title}
.value=${this._scope}
@change=${this._scopeChanged}
>
<ha-radio-option value="this" autofocus>${thisEvent}</ha-radio-option>
<ha-radio-option value="future">${thisAndFuture}</ha-radio-option>
</ha-radio-group>
`;
}
private _scopeChanged(ev: HASSDomCurrentTargetEvent<HaRadioGroup>): void {
this._scope = ev.currentTarget.value as RecurrenceScope;
}
private _dismiss(): void {
this._closeState = "canceled";
this.closeDialog();
@@ -121,11 +90,17 @@ class ConfirmEventDialogBox extends LitElement {
private _confirm(): void {
this._closeState = "confirmed";
this._params!.confirm?.(
this._scope === "future"
? RecurrenceRange.THISANDFUTURE
: RecurrenceRange.THISEVENT
);
if (this._params!.confirm) {
this._params!.confirm(RecurrenceRange.THISEVENT);
}
this.closeDialog();
}
private _confirmFuture(): void {
this._closeState = "confirmedFuture";
if (this._params!.confirm) {
this._params!.confirm(RecurrenceRange.THISANDFUTURE);
}
this.closeDialog();
}
@@ -133,7 +108,10 @@ class ConfirmEventDialogBox extends LitElement {
if (!this._params) {
return;
}
if (this._closeState !== "confirmed") {
if (
this._closeState !== "confirmed" &&
this._closeState !== "confirmedFuture"
) {
this._params.cancel?.();
}
this._params = undefined;
@@ -147,12 +125,18 @@ class ConfirmEventDialogBox extends LitElement {
pointer-events: initial !important;
cursor: initial !important;
}
a {
color: var(--primary-color);
}
p {
margin: 0;
color: var(--primary-text-color);
}
ha-radio-group::part(form-control-label) {
font-weight: var(--ha-font-weight-medium);
.no-bottom-padding {
padding-bottom: 0;
}
.secondary {
color: var(--secondary-text-color);
}
ha-dialog {
/* Place above other dialogs */
@@ -224,9 +224,18 @@ class DialogCalendarEventDetail extends LitElement {
: this.hass.localize(
"ui.components.calendar.event.confirm_delete.prompt"
),
confirmText: this.hass.localize("ui.common.delete"),
recurring: !!entry.recurrence_id,
destructive: true,
confirmText: entry.recurrence_id
? this.hass.localize(
"ui.components.calendar.event.confirm_delete.delete_this"
)
: this.hass.localize(
"ui.components.calendar.event.confirm_delete.delete"
),
confirmFutureText: entry.recurrence_id
? this.hass.localize(
"ui.components.calendar.event.confirm_delete.delete_future"
)
: undefined,
});
if (range === undefined) {
// Cancel
@@ -569,8 +569,12 @@ class DialogCalendarEventEditor extends DirtyStateProviderMixin<CalendarEventFor
text: this.hass.localize(
"ui.components.calendar.event.confirm_update.recurring_prompt"
),
confirmText: this.hass.localize("ui.common.update"),
recurring: true,
confirmText: this.hass.localize(
"ui.components.calendar.event.confirm_update.update_this"
),
confirmFutureText: this.hass.localize(
"ui.components.calendar.event.confirm_update.update_future"
),
});
}
if (range === undefined) {
@@ -621,9 +625,18 @@ class DialogCalendarEventEditor extends DirtyStateProviderMixin<CalendarEventFor
: this.hass.localize(
"ui.components.calendar.event.confirm_delete.prompt"
),
confirmText: this.hass.localize("ui.common.delete"),
recurring: !!entry.recurrence_id,
destructive: true,
confirmText: entry.recurrence_id
? this.hass.localize(
"ui.components.calendar.event.confirm_delete.delete_this"
)
: this.hass.localize(
"ui.components.calendar.event.confirm_delete.delete"
),
confirmFutureText: entry.recurrence_id
? this.hass.localize(
"ui.components.calendar.event.confirm_delete.delete_future"
)
: undefined,
});
if (range === undefined) {
// Cancel
-7
View File
@@ -35,7 +35,6 @@ import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
import { subscribeEntityRegistry } from "../../data/entity/entity_registry";
import { fetchIntegrationManifest } from "../../data/integration";
import { showConfigFlowDialog } from "../../dialogs/config-flow/show-dialog-config-flow";
import { panelIsReady } from "../../layouts/panel-ready";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { haStyle } from "../../resources/styles";
import type { CalendarViewChanged, HomeAssistant } from "../../types";
@@ -78,8 +77,6 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
private _mql?: MediaQueryList;
private _initialReady = false;
public connectedCallback() {
super.connectedCallback();
this._mql = window.matchMedia(
@@ -106,10 +103,6 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
this._entityRegistry = entities;
// Refresh calendars when entity registry updates (includes color changes)
this._calendars = getCalendars(this.hass, this, this._entityRegistry);
if (!this._initialReady) {
this._initialReady = true;
panelIsReady(this);
}
// Resubscribe events if view dates are available (handles both initial load and color updates)
if (this._start && this._end) {
this._unsubscribeAll().then(() => {
@@ -1,16 +1,14 @@
import type { TemplateResult } from "lit";
import { fireEvent } from "../../common/dom/fire_event";
import type { RecurrenceRange } from "../../data/calendar";
export interface ConfirmEventDialogBoxParams {
title: string;
// Labels the recurrence range options when `recurring` is set
text?: string;
confirmText?: string;
// Let the user pick which occurrences of a recurring event are affected
recurring?: boolean;
destructive?: boolean;
confirmFutureText?: string; // Prompt for future recurring events
confirm?: (recurrenceRange: RecurrenceRange) => void;
cancel?: () => void;
text?: string | TemplateResult;
title: string;
}
export const loadGenericDialog = () => import("./confirm-event-dialog-box");
-4
View File
@@ -4,7 +4,6 @@ import { customElement, property, state } from "lit/decorators";
import { debounce } from "../../common/util/debounce";
import { deepEqual } from "../../common/util/deep-equal";
import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view";
import { ChildPanelReady } from "../../layouts/panel-ready";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strategy";
@@ -32,8 +31,6 @@ class PanelClimate extends LitElement {
@state() private _searchParams = new URLSearchParams(window.location.search);
private _childPanelReady?: ChildPanelReady;
public willUpdate(changedProps: PropertyValues<this>) {
super.willUpdate(changedProps);
// Initial setup
@@ -130,7 +127,6 @@ class PanelClimate extends LitElement {
return;
}
this._childPanelReady ??= new ChildPanelReady(this);
this._lovelace = {
config: config,
rawConfig: rawConfig,
@@ -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 === ""
@@ -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;
@@ -36,7 +36,6 @@ import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar";
import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart";
import { showShortcutsDialog } from "../../../dialogs/shortcuts/show-shortcuts-dialog";
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
import { ChildPanelReady } from "../../../layouts/panel-ready";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
@@ -158,11 +157,6 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
total: 0,
};
public constructor() {
super();
new ChildPanelReady(this);
}
private _pages = memoizeOne(
(
cloudStatus,
@@ -1,18 +1,12 @@
import { consume } from "@lit/context";
import type { CSSResultGroup, TemplateResult } from "lit";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { filterNavigationPages } from "../../../common/config/filter_navigation_pages";
import "../../../components/ha-card";
import "../../../components/ha-icon-next";
import type { CloudStatus } from "../../../data/cloud";
import { getConfigEntries } from "../../../data/config_entries";
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
import {
childPanelReadyContext,
type RegisterChildPanelReady,
} from "../../../layouts/panel-ready";
import type { HomeAssistant } from "../../../types";
import "../components/ha-config-navigation-list";
@@ -24,53 +18,21 @@ class HaConfigNavigation extends LitElement {
@property({ attribute: false }) public pages!: PageNavigation[];
@state() private _visiblePages?: PageNavigation[];
@state() private _hasBluetoothConfigEntries = false;
private _hasBluetoothConfigEntries = false;
private _bluetoothEntriesLoaded?: Promise<void>;
private _childReadyRegistered = false;
private _filterNavigationPages = memoizeOne(
(
hass: HomeAssistant,
pages: PageNavigation[],
hasBluetoothConfigEntries: boolean
) => filterNavigationPages(hass, pages, { hasBluetoothConfigEntries })
);
@consume({ context: childPanelReadyContext, subscribe: true })
private _registerChildPanelReady?: RegisterChildPanelReady;
protected override updated(changedProps: Map<PropertyKey, unknown>) {
super.updated(changedProps);
const pagesOrHassChanged =
changedProps.has("pages") || changedProps.has("hass");
if (pagesOrHassChanged) {
const ready = this._resolveVisiblePages();
if (!this._childReadyRegistered && this._registerChildPanelReady) {
this._registerChildPanelReady(ready);
this._childReadyRegistered = true;
}
return;
}
if (
!this._childReadyRegistered &&
this._registerChildPanelReady &&
this.pages &&
this.hass
) {
this._registerChildPanelReady(this._resolveVisiblePages());
this._childReadyRegistered = true;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
super.firstUpdated(changedProps);
getConfigEntries(this.hass, {
domain: "bluetooth",
}).then((bluetoothEntries) => {
this._hasBluetoothConfigEntries = bluetoothEntries.length > 0;
});
}
protected render(): TemplateResult {
const pages = (this._visiblePages ?? []).map((page) => ({
const pages = filterNavigationPages(this.hass, this.pages, {
hasBluetoothConfigEntries: this._hasBluetoothConfigEntries,
}).map((page) => ({
...page,
name:
page.name ||
@@ -113,42 +75,6 @@ class HaConfigNavigation extends LitElement {
`;
}
private _loadBluetoothEntries(): Promise<void> {
if (!this._bluetoothEntriesLoaded) {
this._bluetoothEntriesLoaded = getConfigEntries(this.hass, {
domain: "bluetooth",
})
.then((entries) => {
this._hasBluetoothConfigEntries = entries.length > 0;
})
.catch(() => {
this._hasBluetoothConfigEntries = false;
});
}
return this._bluetoothEntriesLoaded;
}
private async _resolveVisiblePages(): Promise<void> {
if (this.pages.some((page) => page.component === "bluetooth")) {
await this._loadBluetoothEntries();
}
const visiblePages = this._filterNavigationPages(
this.hass,
this.pages,
this._hasBluetoothConfigEntries
);
const currentVisiblePages = this._visiblePages;
if (
!currentVisiblePages ||
visiblePages.length !== currentVisiblePages.length ||
visiblePages.some((page, index) => page !== currentVisiblePages[index])
) {
this._visiblePages = visiblePages;
}
await this.updateComplete;
}
static styles: CSSResultGroup = css`
/* Accessibility */
.visually-hidden {
@@ -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,7 +630,7 @@ export class DialogEnergyGridSettings
this._updateFormDirtyState();
}
private _numberCostChanged(ev: HASSDomCurrentTargetEvent<HTMLInputElement>) {
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 };
@@ -661,9 +653,7 @@ export class DialogEnergyGridSettings
this._updateFormDirtyState();
}
private _numberCompensationChanged(
ev: HASSDomCurrentTargetEvent<HTMLInputElement>
) {
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 };
@@ -671,7 +661,7 @@ export class DialogEnergyGridSettings
}
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";
@@ -291,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
View File
@@ -89,7 +89,6 @@ class HaPanelConfig extends HassRouterPage {
dashboard: {
tag: "ha-config-dashboard",
load: () => import("./dashboard/ha-config-dashboard"),
waitForReady: true,
},
entities: {
tag: "ha-config-entities",
@@ -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 };
@@ -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;
@@ -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;
}
@@ -19,6 +19,8 @@ import { animationStyles } from "../../../../../resources/theme/animations.globa
import "../../../../../components/ha-alert";
import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
import "../../../../../components/buttons/ha-progress-button";
import type { HaProgressButton } from "../../../../../components/buttons/ha-progress-button";
import "../../../../../components/ha-icon-next";
import "../../../../../components/ha-md-list";
@@ -67,6 +69,8 @@ class ZHAConfigDashboard extends LitElement {
@state() private _error?: string;
@state() private _generatingBackup = false;
protected firstUpdated(changedProperties: PropertyValues<this>) {
super.firstUpdated(changedProperties);
if (!this.hass) {
@@ -355,17 +359,18 @@ class ZHAConfigDashboard extends LitElement {
"ui.panel.config.zha.configuration_page.download_backup_description"
)}
</span>
<ha-button
<ha-progress-button
appearance="plain"
slot="end"
size="s"
.iconPath=${mdiDownload}
.progress=${this._generatingBackup}
@click=${this._createAndDownloadBackup}
>
<ha-svg-icon .path=${mdiDownload} slot="start"></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.zha.configuration_page.download_backup_action"
)}
</ha-button>
</ha-progress-button>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline">
@@ -408,30 +413,29 @@ class ZHAConfigDashboard extends LitElement {
this._configuration = await fetchZHAConfiguration(this.hass!);
}
private async _createAndDownloadBackup(): Promise<void> {
private async _createAndDownloadBackup(ev: Event): Promise<void> {
const button = ev.currentTarget as HaProgressButton;
let backup_and_metadata: ZHANetworkBackupAndMetadata;
// Reading the backup from the coordinator can take 5-30 seconds.
this._generatingBackup = true;
try {
backup_and_metadata = await createZHANetworkBackup(this.hass!);
} catch (err: any) {
button.actionError();
showAlertDialog(this, {
title: "Failed to create backup",
title: this.hass.localize(
"ui.panel.config.zha.configuration_page.backup_failed"
),
text: err.message,
warning: true,
});
return;
} finally {
this._generatingBackup = false;
}
if (!backup_and_metadata.is_complete) {
await showAlertDialog(this, {
title: "Backup is incomplete",
text: "A backup has been created but it is incomplete and cannot be restored. This is a coordinator firmware limitation.",
});
}
const backupJSON: string =
"data:text/plain;charset=utf-8," +
encodeURIComponent(JSON.stringify(backup_and_metadata.backup, null, 4));
const backupTime: Date = new Date(
Date.parse(backup_and_metadata.backup.backup_time)
);
@@ -441,7 +445,23 @@ class ZHAConfigDashboard extends LitElement {
basename = `Incomplete ${basename}`;
}
fileDownload(backupJSON, `${basename}.json`);
const blob = new Blob(
[JSON.stringify(backup_and_metadata.backup, null, 4)],
{ type: "application/json" }
);
fileDownload(URL.createObjectURL(blob), `${basename}.json`);
button.actionSuccess();
if (!backup_and_metadata.is_complete) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.zha.configuration_page.backup_incomplete_title"
),
text: this.hass.localize(
"ui.panel.config.zha.configuration_page.backup_incomplete_text"
),
});
}
}
private _openOptionFlow() {
@@ -517,10 +537,6 @@ class ZHAConfigDashboard extends LitElement {
--md-item-overflow: visible;
}
ha-button[size="s"] ha-svg-icon {
--mdc-icon-size: 16px;
}
.network-status div.heading {
display: flex;
align-items: center;
@@ -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;
}
@@ -1,18 +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,
@@ -23,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 {
@@ -352,53 +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 {
private _customBatterySecondsChanged(ev: Event): void {
const seconds = Number((ev.target as HTMLInputElement).value);
this._configuration!.data.zha_options.consider_unavailable_battery =
Number(ev.currentTarget.value);
seconds;
this.requestUpdate();
}
@@ -426,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
@@ -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,16 +129,18 @@ class ZWaveJSCustomParam extends LitElement {
return parsed;
}
private _customParamNumberChanged(ev: HASSDomCurrentTargetEvent<HaInput>) {
this._customParamNumber = this._tryParseNumber(ev.currentTarget.value ?? "");
private _customParamNumberChanged(ev: Event) {
this._customParamNumber = this._tryParseNumber(
(ev.target as HTMLInputElement).value
);
}
private _customValueSizeChanged(ev: HaSelectSelectEvent) {
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 });
}
@@ -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";
@@ -364,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";
@@ -11,7 +11,6 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { HASSDomCurrentTargetEvent } from "../../../../common/dom/fire_event";
import { computeAreaName } from "../../../../common/entity/compute_area_name";
import { computeDeviceName } from "../../../../common/entity/compute_device_name";
import { computeEntityEntryName } from "../../../../common/entity/compute_entity_name";
@@ -261,9 +260,7 @@ class HaPanelDevStateRenderer extends LitElement {
return output;
}
private _copyEntity = async (
ev: HASSDomCurrentTargetEvent<HTMLElement & { entity: HassEntity }>
) => {
private _copyEntity = async (ev: Event) => {
ev.preventDefault();
const entity = (ev.currentTarget as HTMLElement & { entity: HassEntity })
.entity;
@@ -273,18 +270,14 @@ class HaPanelDevStateRenderer extends LitElement {
});
};
private _entityMoreInfo(
ev: HASSDomCurrentTargetEvent<HTMLElement & { entity: HassEntity }>
) {
private _entityMoreInfo(ev: Event) {
ev.preventDefault();
const entity = (ev.currentTarget as HTMLElement & { entity: HassEntity })
.entity;
fireEvent(this, "hass-more-info", { entityId: entity.entity_id });
}
private _entitySelected(
ev: HASSDomCurrentTargetEvent<HTMLElement & { entity: HassEntity }>
) {
private _entitySelected(ev: Event) {
ev.preventDefault();
const entity = (ev.currentTarget as HTMLElement & { entity: HassEntity })
.entity;
@@ -16,11 +16,7 @@ import { consume, type ContextType } from "@lit/context";
import { css, type CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import type {
HASSDomCurrentTargetEvent,
HASSDomEvent,
HASSDomTargetEvent,
} from "../../../../common/dom/fire_event";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeAreaName } from "../../../../common/entity/compute_area_name";
import {
@@ -600,9 +596,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
`;
}
private _handleSearchChange(
ev: InputEvent & HASSDomTargetEvent<HaInputSearch>
) {
private _handleSearchChange(ev: InputEvent) {
if (this.filter === (ev.target as HaInputSearch).value) {
return;
}
@@ -712,9 +706,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
);
}
private _showStatisticsAdjustSumDialog(
ev: HASSDomCurrentTargetEvent<HTMLElement & { statistic: StatisticData }>
) {
private _showStatisticsAdjustSumDialog(ev: Event) {
ev.stopPropagation();
showStatisticsAdjustSumDialog(this, {
statistic: (
@@ -790,11 +782,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
});
};
private _fixIssue = async (
ev: HASSDomCurrentTargetEvent<
HTMLElement & { data: StatisticsValidationResult[] }
>
) => {
private _fixIssue = async (ev: Event) => {
const issues = (
ev.currentTarget as HTMLElement & { data: StatisticsValidationResult[] }
).data.sort(
@@ -9,7 +9,6 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import type { HASSDomTargetEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import type { LocalizeKeys } from "../../../../common/translations/localize";
import { debounce } from "../../../../common/util/debounce";
@@ -420,7 +419,7 @@ ${
`;
}
private _splitRepositioned(ev: HASSDomTargetEvent<HaSplitPanel>) {
private _splitRepositioned(ev: Event) {
this._splitPosition = (ev.target as HaSplitPanel).position;
this._storeSplitPosition();
}
-4
View File
@@ -4,7 +4,6 @@ import { customElement, property, state } from "lit/decorators";
import { debounce } from "../../common/util/debounce";
import { deepEqual } from "../../common/util/deep-equal";
import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view";
import { ChildPanelReady } from "../../layouts/panel-ready";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strategy";
@@ -32,8 +31,6 @@ class PanelLight extends LitElement {
@state() private _searchParms = new URLSearchParams(window.location.search);
private _childPanelReady?: ChildPanelReady;
public willUpdate(changedProps: PropertyValues<this>) {
super.willUpdate(changedProps);
// Initial setup
@@ -130,7 +127,6 @@ class PanelLight extends LitElement {
return;
}
this._childPanelReady ??= new ChildPanelReady(this);
this._lovelace = {
config: config,
rawConfig: rawConfig,
@@ -4,7 +4,6 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { formatNumber } from "../../../../common/number/format_number";
import { round } from "../../../../common/number/round";
import "../../../../components/ha-card";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-tooltip";
@@ -92,8 +91,8 @@ class HuiEnergyGridBalanceCard
const { summedData } = getSummedData(this._data);
const imported = round(summedData.total.from_grid ?? 0, 3);
const exported = round(summedData.total.to_grid ?? 0, 3);
const imported = summedData.total.from_grid ?? 0;
const exported = summedData.total.to_grid ?? 0;
const net = imported - exported;
const fmt = (value: number) =>
@@ -3,7 +3,6 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/animation/ha-fade-in";
import "../../../components/ha-card";
import "../../../components/ha-spinner";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
@@ -39,9 +38,7 @@ export class HuiStartingCard extends LitElement implements LovelaceCard {
return html`
<div class="content">
<ha-fade-in .delay=${500}>
<ha-spinner></ha-spinner>
</ha-fade-in>
<ha-spinner></ha-spinner>
${this.hass.localize("ui.panel.lovelace.cards.starting.description")}
</div>
`;
@@ -1,6 +1,5 @@
import { migrateStateColorConfig } from "../common/entity-color-config";
import { migrateTimeFormatConfig } from "../common/entity-time-format-config";
import { migrateSecondaryInfoConfig } from "../common/entity-secondary-info-config";
import type {
ConditionalRowConfig,
LovelaceRowConfig,
@@ -21,13 +20,11 @@ const migrateEntitiesRowConfig = (
let newConf: LovelaceRowConfig = rowConf;
newConf = migrateTimeFormatConfig(newConf as EntitiesCardEntityConfig);
newConf = migrateStateColorConfig(newConf as EntitiesCardEntityConfig);
newConf = migrateSecondaryInfoConfig(newConf as EntitiesCardEntityConfig);
if (newConf.type === "conditional") {
const row = (newConf as ConditionalRowConfig).row;
if (row && typeof row === "object") {
let newRow = migrateTimeFormatConfig(row as EntitiesCardEntityConfig);
newRow = migrateStateColorConfig(newRow);
newRow = migrateSecondaryInfoConfig(newRow);
if (newRow !== row) {
newConf = { ...newConf, row: newRow } as ConditionalRowConfig;
}
+10 -1
View File
@@ -91,7 +91,16 @@ export interface EntityCardConfig extends LovelaceCardConfig {
export interface EntitiesCardEntityConfig extends EntityConfig {
type?: string;
secondary_info?: string | string[];
secondary_info?:
| "entity-id"
| "last-changed"
| "last-triggered"
| "last-updated"
| "area"
| "position"
| "state"
| "tilt-position"
| "brightness";
action_name?: string;
action?: string;
/** @deprecated use "action" instead */
@@ -1,40 +0,0 @@
import { isCustomType } from "../../../data/lovelace_custom_cards";
interface LegacySecondaryInfoConfig {
type?: string;
secondary_info?: string | string[];
}
const SUBSTITUTIONS = {
"last-changed": "last_changed",
"last-updated": "last_updated",
"last-triggered": "last_triggered",
position: "current_position",
"tilt-position": "current_tilt_position",
area: "area_name",
none: undefined,
};
export const migrateSecondaryInfoConfig = <T extends LegacySecondaryInfoConfig>(
config: T
): T => {
// Custom elements own their config schema and may use the same option with
// a different meaning, leave them untouched
if (config.type !== undefined && isCustomType(config.type)) {
return config;
}
if (
config.secondary_info === undefined ||
typeof config.secondary_info !== "string" ||
!(config.secondary_info in SUBSTITUTIONS)
) {
return config;
}
const { secondary_info, ...rest } = config;
return {
...rest,
...(SUBSTITUTIONS[secondary_info] && {
secondary_info: SUBSTITUTIONS[secondary_info],
}),
} as T;
};
@@ -4,9 +4,13 @@ import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { DOMAINS_INPUT_ROW } from "../../../common/const";
import { uid } from "../../../common/util/uid";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { toggleAttribute } from "../../../common/dom/toggle_attribute";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeAreaName } from "../../../common/entity/compute_area_name";
import { getEntityContext } from "../../../common/entity/context/get_entity_context";
import { formatDateTimeWithSeconds } from "../../../common/datetime/format_date_time";
import "../../../components/entity/state-badge";
import "../../../components/ha-relative-time";
import "../../../components/ha-tooltip";
@@ -17,7 +21,6 @@ import { actionHandler } from "../common/directives/action-handler-directive";
import { handleAction } from "../common/handle-action";
import { hasAction, hasAnyAction } from "../common/has-action";
import { createEntityNotFoundWarning } from "./hui-warning";
import "../../../state-display/state-display";
@customElement("hui-generic-entity-row")
export class HuiGenericEntityRow extends LitElement {
@@ -37,6 +40,8 @@ export class HuiGenericEntityRow extends LitElement {
@property({ attribute: "catch-interaction", type: Boolean })
public catchInteraction?;
private _secondaryInfoElementId = "-" + uid();
protected render() {
if (!this.hass || !this.config) {
return nothing;
@@ -96,15 +101,118 @@ export class HuiGenericEntityRow extends LitElement {
<div class="secondary">
${
this.secondaryText ||
html`<state-display
.stateObj=${stateObj}
.hass=${this.hass}
.content=${this.config.secondary_info}
.timeFormat=${this.config.time_format}
.name=${name}
timestamp-tooltip
>
</state-display>`
(this.config.secondary_info === "entity-id"
? stateObj.entity_id
: this.config.secondary_info === "last-changed"
? html`
<ha-tooltip
for="last-changed${
this._secondaryInfoElementId
}"
placement="right"
>
${formatDateTimeWithSeconds(
new Date(stateObj.last_changed),
this.hass.locale,
this.hass.config
)}
</ha-tooltip>
<ha-relative-time
id="last-changed${this._secondaryInfoElementId}"
.datetime=${stateObj.last_changed}
capitalize
></ha-relative-time>
`
: this.config.secondary_info === "last-updated"
? html`
<ha-tooltip
for="last-updated${
this._secondaryInfoElementId
}"
placement="right"
>
${formatDateTimeWithSeconds(
new Date(stateObj.last_updated),
this.hass.locale,
this.hass.config
)}
</ha-tooltip>
<ha-relative-time
id="last-updated${
this._secondaryInfoElementId
}"
.datetime=${stateObj.last_updated}
capitalize
></ha-relative-time>
`
: this.config.secondary_info ===
"last-triggered"
? stateObj.attributes.last_triggered
? html`
<ha-tooltip
for="last-triggered${
this._secondaryInfoElementId
}"
placement="right"
>
${formatDateTimeWithSeconds(
new Date(
stateObj.attributes
.last_triggered
),
this.hass.locale,
this.hass.config
)}
</ha-tooltip>
<ha-relative-time
id="last-triggered${
this._secondaryInfoElementId
}"
.datetime=${
stateObj.attributes.last_triggered
}
capitalize
></ha-relative-time>
`
: this.hass.localize(
"ui.panel.lovelace.cards.entities.never_triggered"
)
: this.config.secondary_info ===
"position" &&
stateObj.attributes.current_position !==
undefined
? `${this.hass.localize(
"ui.card.cover.position"
)}: ${stateObj.attributes.current_position}`
: this.config.secondary_info ===
"tilt-position" &&
stateObj.attributes
.current_tilt_position !== undefined
? `${this.hass.localize(
"ui.card.cover.tilt_position"
)}: ${
stateObj.attributes
.current_tilt_position
}`
: this.config.secondary_info ===
"brightness" &&
stateObj.attributes.brightness
? html`${Math.round(
(stateObj.attributes.brightness /
255) *
100
)}
%`
: this.config.secondary_info ===
"state"
? html`${this.hass.formatEntityState(
stateObj
)}`
: this.config.secondary_info ===
"area"
? (this._getArea(stateObj) ??
nothing)
: nothing)
}
</div>
`
@@ -145,6 +253,17 @@ export class HuiGenericEntityRow extends LitElement {
handleAction(this, this.hass!, this.config!, ev.detail.action!);
}
private _getArea(stateObj) {
const context = getEntityContext(
stateObj,
this.hass!.entities,
this.hass!.devices,
this.hass!.areas,
this.hass!.floors
);
return context.area ? computeAreaName(context.area) : undefined;
}
static styles = css`
:host {
display: flex;
@@ -10,7 +10,6 @@ import {
import {
formatDateTime,
formatDateTimeNumeric,
formatDateTimeWithSeconds,
} from "../../../common/datetime/format_date_time";
import {
formatTime,
@@ -30,8 +29,6 @@ import type {
} from "../../../types";
import type { LocalizeFunc } from "../../../common/translations/localize";
import type { TimestampRenderingFormat } from "./types";
import { uid } from "../../../common/util/uid";
import "../../../components/ha-tooltip";
const FORMATS: Record<
string,
@@ -63,8 +60,6 @@ class HuiTimestampDisplay extends LitElement {
@property({ type: Boolean }) public capitalize = false;
@property({ type: Boolean }) public tooltip = false;
@state() private _relative?: string;
@state()
@@ -92,8 +87,6 @@ class HuiTimestampDisplay extends LitElement {
private _interval?: number;
private _uid = "timestamp-display-" + uid();
public connectedCallback(): void {
super.connectedCallback();
this._connected = true;
@@ -121,14 +114,6 @@ class HuiTimestampDisplay extends LitElement {
const formatType = this._formatType;
if (INTERVAL_FORMAT.includes(formatType)) {
if (this.tooltip) {
return html`
<ha-tooltip for=${this._uid} placement="right">
${formatDateTimeWithSeconds(this.ts, this._locale, this._config)}
</ha-tooltip>
<span id=${this._uid}> ${this._relative} </span>
`;
}
return html` ${this._relative} `;
}
if (formatType in FORMATS) {
@@ -138,20 +123,9 @@ class HuiTimestampDisplay extends LitElement {
: format.style && format.style in FORMATS[formatType]
? format.style
: "default";
const formatted = FORMATS[formatType][style](
this.ts,
this._locale,
this._config
);
if (this.tooltip) {
return html`
<ha-tooltip for=${this._uid} placement="right">
${formatDateTimeWithSeconds(this.ts, this._locale, this._config)}
</ha-tooltip>
<span id=${this._uid}> ${formatted} </span>
`;
}
return html` ${formatted} `;
return html`
${FORMATS[formatType][style](this.ts, this._locale, this._config)}
`;
}
return html`${this._localize(
"ui.panel.lovelace.components.timestamp-display.invalid_format"
@@ -123,9 +123,8 @@ export class HuiCardPicker extends LitElement {
};
const fuse = new Fuse(cards, options);
cards = fuse.search(filter).map((result) => result.item);
return cardElements.filter(
(cardElement: CardElement) =>
cards.includes(cardElement.card) && cardElement.card.type !== "manual"
return cardElements.filter((cardElement: CardElement) =>
cards.includes(cardElement.card)
);
}
);
@@ -141,9 +140,7 @@ export class HuiCardPicker extends LitElement {
(cardElements: CardElement[]): CardElement[] =>
cardElements.filter(
(cardElement: CardElement) =>
!cardElement.card.isCustom &&
!cardElement.card.isEnergy &&
!(cardElement.card.type === "manual")
!cardElement.card.isCustom && !cardElement.card.isEnergy
)
);
@@ -154,12 +151,6 @@ export class HuiCardPicker extends LitElement {
)
);
private _manualCard = memoizeOne((cardElements: CardElement[]): CardElement =>
cardElements.find(
(cardElement: CardElement) => cardElement.card.type === "manual"
)!
);
private _favoriteCards(): CardElement[] {
return this._favorites
.map((key) => this._favoriteElement(key))
@@ -169,10 +160,9 @@ export class HuiCardPicker extends LitElement {
// A preview is a live DOM node, so every section a card shows up in needs
// its own element.
private _toCardElement(card: Card): CardElement {
const config = card.type === "manual" ? { type: "" } : undefined;
return {
card,
element: html`${until(this._renderCardElement(card, config), SPINNER)}`,
element: html`${until(this._renderCardElement(card), SPINNER)}`,
};
}
@@ -204,7 +194,6 @@ export class HuiCardPicker extends LitElement {
const othersCards = this._otherCards(this._cards);
const energyCardsItems = this._energyCards(this._cards);
const customCardsItems = this._customCards(this._cards);
const manualCard = this._manualCard(this._cards);
return html`
<ha-input-search
@@ -345,7 +334,25 @@ export class HuiCardPicker extends LitElement {
}
`
}
<div class="cards-container">${this._renderCard(manualCard, true)}</div>
<div class="cards-container">
<div
class="card manual"
@click=${this._cardPicked}
.config=${{ type: "" }}
>
<div class="card-header">
${this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.manual`
)}
</div>
<div class="preview description">
${this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.manual_description`
)}
</div>
<ha-ripple></ha-ripple>
</div>
</div>
</div>
`;
}
@@ -436,13 +443,6 @@ export class HuiCardPicker extends LitElement {
)
);
}
cards = cards.concat({
type: "manual",
name: this.hass!.localize("ui.panel.lovelace.editor.card.generic.manual"),
description: this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.manual_description"
),
});
this._cards = cards.map((card: Card) => this._toCardElement(card));
this._suggestedCards = cards
.filter((card: Card) => card.isSuggested)
@@ -481,12 +481,12 @@ export class HuiCardPicker extends LitElement {
}
);
private _renderCard(cardElement: CardElement, wide = false): TemplateResult {
private _renderCard(cardElement: CardElement): TemplateResult {
const key = cardKey(cardElement.card);
const favorite = this._favorites.includes(key);
return html`
<div class="card ${classMap({ "full-width": wide })}" tabindex="0">
<div class="card" tabindex="0">
${cardElement.element}
<ha-icon-button
class="favorite ${classMap({ selected: favorite })}"
@@ -832,7 +832,7 @@ export class HuiCardPicker extends LitElement {
);
}
.full-width {
.manual {
max-width: none;
grid-column: 1 / -1;
}
@@ -4,8 +4,12 @@ import memoizeOne from "memoize-one";
import { assert } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeDomain } from "../../../../common/entity/compute_domain";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import type {
HaFormSchema,
SchemaUnion,
} from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import { SENSOR_TIMESTAMP_DEVICE_CLASSES } from "../../../../data/sensor";
import type { EntitiesCardEntityConfig } from "../../cards/types";
@@ -13,7 +17,19 @@ import type { LovelaceRowEditor } from "../../types";
import { entitiesConfigStruct } from "../structs/entities-struct";
import { DOMAIN_TO_ELEMENT_TYPE } from "../../create-element/create-row-element";
import { TIMESTAMP_STATE_DOMAINS } from "../../../../common/const";
import { stateContentHasTimestamp } from "../../../../state-display/state-display";
const SECONDARY_INFO_VALUES = {
none: {},
"entity-id": {},
"last-changed": {},
"last-updated": {},
"last-triggered": { domains: ["automation", "script"] },
area: {},
position: { domains: ["cover"] },
state: {},
"tilt-position": { domains: ["cover"] },
brightness: { domains: ["light"] },
};
@customElement("hui-generic-entity-row-editor")
export class HuiGenericEntityRowEditor
@@ -31,87 +47,96 @@ export class HuiGenericEntityRowEditor
this._config = config;
}
private _schema = memoizeOne((showTimeFormat?: boolean) => {
return [
{ name: "entity", required: true, selector: { entity: {} } },
{
name: "name",
selector: { entity_name: {} },
context: { entity: "entity" },
},
{
name: "",
type: "grid",
schema: [
{
name: "icon",
selector: {
icon: {},
},
context: {
icon_entity: "entity",
},
},
{
name: "color",
selector: {
ui_color: {
include_state: true,
include_none: true,
private _schema = memoizeOne(
(entity: string, localize: LocalizeFunc, showTimeFormat?: boolean) => {
const domain = computeDomain(entity);
return [
{ name: "entity", required: true, selector: { entity: {} } },
{
name: "name",
selector: { entity_name: {} },
context: { entity: "entity" },
},
{
name: "",
type: "grid",
schema: [
{
name: "icon",
selector: {
icon: {},
},
context: {
icon_entity: "entity",
},
},
},
],
},
{
name: "secondary_info",
selector: {
ui_state_content: {
allow_context: true,
{
name: "color",
selector: {
ui_color: {
include_state: true,
include_none: true,
},
},
},
],
},
{
name: "secondary_info",
selector: {
select: {
options: (
Object.keys(SECONDARY_INFO_VALUES).filter(
(info) =>
!("domains" in SECONDARY_INFO_VALUES[info]) ||
("domains" in SECONDARY_INFO_VALUES[info] &&
SECONDARY_INFO_VALUES[info].domains!.includes(domain))
) as (keyof typeof SECONDARY_INFO_VALUES)[]
).map((info) => ({
value: info,
label: localize(
`ui.panel.lovelace.editor.card.entities.secondary_info_values.${info}`
),
})),
},
},
},
context: {
filter_entity: "entity",
},
},
{
name: "time_format",
visible: showTimeFormat,
selector: {
ui_time_format: {},
},
},
] as const;
});
...(showTimeFormat
? ([
{
name: "time_format",
selector: {
ui_time_format: {},
},
},
] as const satisfies readonly HaFormSchema[])
: []),
] as const;
}
);
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
let schema = this.schema;
if (!schema) {
const entity = this._config.entity;
const domain = entity ? computeDomain(entity) : "";
const simpleEntity =
(DOMAIN_TO_ELEMENT_TYPE[domain] ||
DOMAIN_TO_ELEMENT_TYPE["_domain_not_found"]) === "simple";
const showTimeFormat =
(this._config.secondary_info &&
stateContentHasTimestamp(
entity,
this.hass.states[entity],
this._config.secondary_info
)) ||
(simpleEntity
? TIMESTAMP_STATE_DOMAINS.has(domain)
: domain === "event" ||
(domain === "sensor" &&
SENSOR_TIMESTAMP_DEVICE_CLASSES.includes(
this.hass.states[entity]?.attributes.device_class
)));
schema = this._schema(showTimeFormat);
}
const entity = this._config.entity;
const domain = entity ? computeDomain(entity) : "";
const simpleEntity =
(DOMAIN_TO_ELEMENT_TYPE[domain] ||
DOMAIN_TO_ELEMENT_TYPE["_domain_not_found"]) === "simple";
const showTimeFormat = simpleEntity
? TIMESTAMP_STATE_DOMAINS.has(domain)
: domain === "event" ||
(domain === "sensor" &&
SENSOR_TIMESTAMP_DEVICE_CLASSES.includes(
this.hass.states[entity]?.attributes.device_class
));
const schema =
this.schema ||
this._schema(this._config.entity, this.hass.localize, showTimeFormat);
return html`
<ha-form
@@ -1,4 +1,4 @@
import { array, union, object, string, optional, boolean } from "superstruct";
import { union, object, string, optional, boolean } from "superstruct";
import { timeFormatConfigStruct } from "../../components/types";
import {
actionConfigStruct,
@@ -12,7 +12,7 @@ export const entitiesConfigStruct = union([
name: optional(entityNameStruct),
icon: optional(string()),
image: optional(string()),
secondary_info: optional(union([string(), array(string())])),
secondary_info: optional(string()),
time_format: optional(timeFormatConfigStruct),
color: optional(string()),
tap_action: optional(actionConfigStruct),
@@ -23,7 +23,6 @@ import { hasConfigOrEntityChanged } from "../common/has-changed";
import "../components/hui-generic-entity-row";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import type { LovelaceRow } from "./types";
import "../../../state-display/state-display";
@customElement("hui-weather-entity-row")
class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
@@ -163,15 +162,25 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
hasSecondary
? html`
<div class="secondary">
<state-display
.stateObj=${stateObj}
.hass=${this.hass}
.content=${this._config.secondary_info}
.timeFormat=${this._config.time_format}
.name=${name}
timestamp-tooltip
>
</state-display>
${
this._config.secondary_info === "entity-id"
? stateObj.entity_id
: this._config.secondary_info === "last-changed"
? html`
<ha-relative-time
.datetime=${stateObj.last_changed}
capitalize
></ha-relative-time>
`
: this._config.secondary_info === "last-updated"
? html`
<ha-relative-time
.datetime=${stateObj.last_updated}
capitalize
></ha-relative-time>
`
: ""
}
</div>
`
: ""
+3 -10
View File
@@ -76,7 +76,6 @@ import { showMoreInfoDialog } from "../../dialogs/more-info/show-ha-more-info-di
import { showQuickBar } from "../../dialogs/quick-bar/show-dialog-quick-bar";
import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
import { haStyle } from "../../resources/styles";
import { ChildPanelReady } from "../../layouts/panel-ready";
import type { HomeAssistant, PanelInfo } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
import { isMac } from "../../util/is_mac";
@@ -165,8 +164,6 @@ class HUIRoot extends LitElement {
private _restoreScroll = false;
private _childPanelReady?: ChildPanelReady;
private _undoRedoController = new UndoRedoController<UndoStackItem>(this, {
apply: (config) => this._applyUndoRedo(config),
currentConfig: () => ({
@@ -780,7 +777,7 @@ class HUIRoot extends LitElement {
huiView.narrow = this.narrow;
}
let newSelectView: HUIRoot["_curView"];
let newSelectView;
let viewPath: string | undefined = this.route!.path.split("/")[1];
viewPath = viewPath ? decodeURI(viewPath) : undefined;
@@ -1261,7 +1258,7 @@ class HUIRoot extends LitElement {
return;
}
let view: HUIView;
let view;
const viewConfig = this.config.views[viewIndex];
if (!viewConfig) {
@@ -1272,16 +1269,12 @@ class HUIRoot extends LitElement {
if (this._viewCache[viewIndex]) {
view = this._viewCache[viewIndex];
} else {
if (!this._childPanelReady) {
this._childPanelReady = new ChildPanelReady(this);
this.requestUpdate();
}
view = document.createElement("hui-view");
view.index = viewIndex;
this._viewCache[viewIndex] = view;
}
view.lovelace = this.lovelace!;
view.lovelace = this.lovelace;
view.hass = this.hass;
view.narrow = this.narrow;
+1 -17
View File
@@ -58,12 +58,6 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
private _mqlListenerRef?: () => void;
private _resolveInitialRender?: () => void;
public initialRenderComplete = new Promise<void>((resolve) => {
this._resolveInitialRender = resolve;
});
public connectedCallback() {
super.connectedCallback();
this._initMqls();
@@ -172,13 +166,7 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
root.removeChild(root.lastChild);
}
columns.forEach((column) => {
root.appendChild(column);
});
if (this.cards.length === 0 || columns.some((column) => column.lastChild)) {
this._resolveInitialRender?.();
this._resolveInitialRender = undefined;
}
columns.forEach((column) => root.appendChild(column));
}
private async _createColumns() {
@@ -246,10 +234,6 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
index,
this.lovelace!.editMode
);
if (columnElements.some((column) => column.isConnected)) {
this._resolveInitialRender?.();
this._resolveInitialRender = undefined;
}
}
// Remove empty columns
-26
View File
@@ -1,5 +1,4 @@
import deepClone from "deep-clone-simple";
import { consume } from "@lit/context";
import type { PropertyValues } from "lit";
import { ReactiveElement } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -20,10 +19,6 @@ import type {
} from "../../../data/lovelace/config/view";
import { isStrategyView } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
import {
childPanelReadyContext,
type RegisterChildPanelReady,
} from "../../../layouts/panel-ready";
import "../badges/hui-badge";
import type { HuiBadge } from "../badges/hui-badge";
import "../cards/hui-card";
@@ -100,15 +95,6 @@ export class HUIView extends ReactiveElement {
private _config?: LovelaceViewConfig;
private _resolveInitialRender?: () => void;
private _initialRenderComplete = new Promise<void>((resolve) => {
this._resolveInitialRender = resolve;
});
@consume({ context: childPanelReadyContext })
private _registerChildPanelReady?: RegisterChildPanelReady;
@storage({
key: "dashboardCardClipboard",
state: false,
@@ -166,11 +152,6 @@ export class HUIView extends ReactiveElement {
return this;
}
public connectedCallback(): void {
super.connectedCallback();
this._registerChildPanelReady?.(this._initialRenderComplete);
}
public willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
@@ -343,13 +324,6 @@ export class HUIView extends ReactiveElement {
const viewConfig = await this._generateConfig(rawConfig);
this._setConfig(viewConfig, isStrategy);
await customElements.whenDefined(this._layoutElement!.localName);
if (this._layoutElement instanceof ReactiveElement) {
await this._layoutElement.updateComplete;
}
await this._layoutElement!.initialRenderComplete;
this._resolveInitialRender?.();
this._resolveInitialRender = undefined;
}
private _createLayoutElement(config: LovelaceViewConfig): void {
@@ -5,7 +5,6 @@ import { debounce } from "../../common/util/debounce";
import { deepEqual } from "../../common/util/deep-equal";
import "../../components/ha-top-app-bar-fixed";
import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view";
import { ChildPanelReady } from "../../layouts/panel-ready";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strategy";
@@ -32,8 +31,6 @@ class PanelMaintenance extends LitElement {
@state() private _searchParams = new URLSearchParams(window.location.search);
private _childPanelReady?: ChildPanelReady;
public willUpdate(changedProps: PropertyValues<this>) {
super.willUpdate(changedProps);
// Initial setup
@@ -130,7 +127,6 @@ class PanelMaintenance extends LitElement {
return;
}
this._childPanelReady ??= new ChildPanelReady(this);
this._lovelace = {
config: config,
rawConfig: rawConfig,
-4
View File
@@ -5,7 +5,6 @@ import { debounce } from "../../common/util/debounce";
import { deepEqual } from "../../common/util/deep-equal";
import "../../components/ha-top-app-bar-fixed";
import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view";
import { ChildPanelReady } from "../../layouts/panel-ready";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strategy";
@@ -32,8 +31,6 @@ class PanelSecurity extends LitElement {
@state() private _searchParms = new URLSearchParams(window.location.search);
private _childPanelReady?: ChildPanelReady;
public willUpdate(changedProps: PropertyValues<this>) {
super.willUpdate(changedProps);
// Initial setup
@@ -130,7 +127,6 @@ class PanelSecurity extends LitElement {
return;
}
this._childPanelReady ??= new ChildPanelReady(this);
this._lovelace = {
config: config,
rawConfig: rawConfig,
+2 -7
View File
@@ -10,6 +10,7 @@ import {
STRINGS_SEPARATOR_DOT,
TIMESTAMP_STATE_DOMAINS,
} from "../common/const";
import "../components/ha-relative-time";
import { UNAVAILABLE, UNKNOWN } from "../data/entity/entity";
import {
SENSOR_TIMESTAMP_DEVICE_CLASSES,
@@ -117,9 +118,6 @@ class StateDisplay extends LitElement {
@property({ attribute: false }) public timeFormat?: string;
@property({ type: Boolean, attribute: "timestamp-tooltip" })
public timestampTooltip = false;
@property({ type: Boolean, attribute: "dash-unavailable" })
public dashUnavailable?: boolean;
@@ -187,9 +185,7 @@ class StateDisplay extends LitElement {
if (content === "name" && this.name) {
return html`${this.name}`;
}
if (content === "entity-id") {
return stateObj.entity_id;
}
if (
content === "device_name" ||
content === "area_name" ||
@@ -218,7 +214,6 @@ class StateDisplay extends LitElement {
.ts=${new Date(relativeDateTime)}
.format=${this.timeFormat}
capitalize
.tooltip=${this.timestampTooltip}
></hui-timestamp-display>`;
}
+9 -6
View File
@@ -1312,18 +1312,18 @@
"invalid_duration": "The duration of the event is not valid. Please check start and end date.",
"not_all_required_fields": "Not all required fields are filled in",
"end_auto_adjusted": "Event end was adjusted to prevent negative duration",
"recurrence_range": {
"this_event": "Only this event",
"this_and_future": "This and all future events"
},
"confirm_delete": {
"delete": "Delete event",
"delete_this": "Delete only this event",
"delete_future": "Delete all future events",
"prompt": "Do you want to delete this event?",
"recurring_prompt": "Which events do you want to delete?"
"recurring_prompt": "Do you want to delete only this event, or this and all future occurrences of the event?"
},
"confirm_update": {
"update": "Update event",
"recurring_prompt": "Which events do you want to update?"
"update_this": "Update only this event",
"update_future": "Update all future events",
"recurring_prompt": "Do you want to update only this event, or this and all future occurrences of the event?"
},
"repeat": {
"label": "Repeat",
@@ -7557,6 +7557,9 @@
"download_backup": "Download backup",
"download_backup_description": "Save your Zigbee network configuration to a file",
"download_backup_action": "Download",
"backup_failed": "Failed to create backup",
"backup_incomplete_title": "Backup is incomplete",
"backup_incomplete_text": "A backup has been created but it is incomplete and cannot be restored. This is a limitation of your adapter's firmware.",
"migrate_radio": "Migrate adapter",
"migrate_radio_description": "Move your Zigbee network to a different adapter",
"migrate_radio_action": "Migrate",
-4
View File
@@ -1,4 +0,0 @@
{
"hashedEntry": "/frontend_(?:latest|es5)/[^/?#]+\\.[0-9a-f]{8,}\\.js",
"moduleError": "dynamically imported module|Importing a module script failed|error loading dynamically imported|ChunkLoadError"
}
+1 -96
View File
@@ -4,7 +4,7 @@
* Run with:
* yarn test:e2e:app
*/
import { test, expect, type Page } from "@playwright/test";
import { test, expect } from "@playwright/test";
import {
appSidebar,
appSidebarConfig,
@@ -161,106 +161,11 @@ test.describe("Quick search", () => {
defineRouteSmokeTests(appRouteSmokeGroups);
const assertInitialReadiness = async (
page: Page,
options: {
path: string;
loadingSelector: string;
resolver:
"rejectMediaBrowse" | "resolveCalendarRegistry" | "resolveMediaBrowse";
readySelector: string;
}
) => {
await goToPanel(page, options.path);
const launchScreen = page.locator("#ha-launch-screen");
await expect(launchScreen).toBeAttached({ timeout: QUICK_TIMEOUT });
await expect(page.locator(options.loadingSelector)).toBeAttached({
timeout: QUICK_TIMEOUT,
});
await page.evaluate((resolver) => window[resolver]?.(), options.resolver);
await expect(page.locator(options.readySelector)).toBeAttached({
timeout: PANEL_TIMEOUT,
});
await expect(launchScreen).not.toBeAttached({ timeout: QUICK_TIMEOUT });
};
test.describe("Initial readiness", () => {
test("keeps the launch screen until calendar content renders", async ({
page,
}) => {
await assertInitialReadiness(page, {
path: "/?scenario=delayed-calendar#/calendar",
loadingSelector: "ha-panel-calendar ha-spinner",
resolver: "resolveCalendarRegistry",
readySelector: "ha-full-calendar",
});
});
test("keeps the launch screen until media content renders", async ({
page,
}) => {
await assertInitialReadiness(page, {
path: "/?scenario=delayed-media-browse#/media-browser/browser",
loadingSelector: "ha-media-player-browse > ha-spinner",
resolver: "resolveMediaBrowse",
readySelector: "ha-media-player-browse .no-items",
});
});
test("keeps the launch screen until a media error renders", async ({
page,
}) => {
await assertInitialReadiness(page, {
path: "/?scenario=delayed-media-browse-error#/media-browser/browser",
loadingSelector: "ha-media-player-browse > ha-spinner",
resolver: "rejectMediaBrowse",
readySelector: "ha-media-player-browse ha-alert",
});
});
});
// ---------------------------------------------------------------------------
// Lovelace
// ---------------------------------------------------------------------------
test.describe("Lovelace dashboard", () => {
test("keeps the launch screen until generated content renders", async ({
page,
}) => {
await goToPanel(page, "/?scenario=delayed-generated-dashboard#/climate");
const launchScreen = page.locator("#ha-launch-screen");
await expect(launchScreen).toBeAttached({ timeout: QUICK_TIMEOUT });
await expect(page.locator("hui-view")).not.toBeAttached();
await page.evaluate(() => window.resolveGeneratedDashboard?.());
await expect(page.locator("hui-view")).toBeAttached({
timeout: PANEL_TIMEOUT,
});
await expect(launchScreen).not.toBeAttached({ timeout: QUICK_TIMEOUT });
});
test("keeps the launch screen until initial content renders", async ({
page,
}) => {
await goToPanel(page, "/?scenario=delayed-lovelace#/lovelace");
const launchScreen = page.locator("#ha-launch-screen");
await expect(launchScreen).toBeAttached({ timeout: QUICK_TIMEOUT });
await expect(page.locator("hui-card")).not.toBeAttached();
await page.evaluate(() => window.resolveLovelaceConfig?.());
await expect(page.locator("hui-card").first()).toBeAttached({
timeout: PANEL_TIMEOUT,
});
await expect(launchScreen).not.toBeAttached({ timeout: QUICK_TIMEOUT });
});
test("renders cards", async ({ page }) => {
await goToPanel(page, "/lovelace");
// At least one card should appear
-5
View File
@@ -92,11 +92,6 @@ declare global {
interface Window {
__assistRun?: unknown;
__mockHass: MockHomeAssistant;
rejectMediaBrowse?: () => void;
resolveCalendarRegistry?: () => void;
resolveGeneratedDashboard?: () => void;
resolveLovelaceConfig?: () => void;
resolveMediaBrowse?: () => void;
}
}
+1 -103
View File
@@ -1,10 +1,5 @@
import type { ExtEntityRegistryEntry } from "../../../../../src/data/entity/entity_registry";
import type { AssistPipeline } from "../../../../../src/data/assist_pipeline";
import type {
EntityRegistryEntry,
ExtEntityRegistryEntry,
} from "../../../../../src/data/entity/entity_registry";
import type { LovelaceRawConfig } from "../../../../../src/data/lovelace/config/types";
import type { MediaPlayerItem } from "../../../../../src/data/media-player";
import type { MockHomeAssistant } from "../../../../../src/fake_data/provide_hass";
export type Scenario = (hass: MockHomeAssistant) => Promise<void> | void;
@@ -129,98 +124,6 @@ const quickSearchAssistScenario: Scenario = async (hass) => {
});
};
const addLaunchScreen = () => {
const launchScreen = document.createElement("div");
launchScreen.id = "ha-launch-screen";
document.body.prepend(launchScreen);
};
const delayedLovelaceScenario: Scenario = (hass) => {
addLaunchScreen();
const config: LovelaceRawConfig = {
views: [
{
title: "Home",
cards: [{ type: "markdown", content: "Dashboard ready" }],
},
],
};
let resolveConfig: ((config: LovelaceRawConfig) => void) | undefined;
const configPromise = new Promise<LovelaceRawConfig>((resolve) => {
resolveConfig = resolve;
});
window.resolveLovelaceConfig = () => resolveConfig?.(config);
hass.mockWS("lovelace/config", () => configPromise);
};
const delayedGeneratedDashboardScenario: Scenario = (hass) => {
addLaunchScreen();
const loadFragmentTranslation = hass.loadFragmentTranslation;
let resolveTranslation: (() => void) | undefined;
const translationReady = new Promise<void>((resolve) => {
resolveTranslation = resolve;
});
hass.loadFragmentTranslation = async (fragment) => {
if (fragment === "lovelace") {
await translationReady;
}
return loadFragmentTranslation(fragment);
};
window.resolveGeneratedDashboard = resolveTranslation;
};
const delayedCalendarScenario: Scenario = (hass) => {
addLaunchScreen();
let resolveRegistry: ((entries: EntityRegistryEntry[]) => void) | undefined;
const registryPromise = new Promise<EntityRegistryEntry[]>((resolve) => {
resolveRegistry = resolve;
});
window.resolveCalendarRegistry = () => resolveRegistry?.([]);
hass.mockWS("config/entity_registry/list", () => registryPromise);
};
const delayedMediaBrowseScenario: Scenario = (hass) => {
addLaunchScreen();
const root: MediaPlayerItem = {
title: "Media",
media_content_id: "media-source://media_source",
media_content_type: "app",
media_class: "directory",
can_play: false,
can_expand: true,
can_search: false,
children: [],
};
let resolveBrowse: ((item: MediaPlayerItem) => void) | undefined;
const browsePromise = new Promise<MediaPlayerItem>((resolve) => {
resolveBrowse = resolve;
});
window.resolveMediaBrowse = () => resolveBrowse?.(root);
hass.mockWS("media_source/browse_media", () => browsePromise);
};
const delayedMediaBrowseErrorScenario: Scenario = (hass) => {
addLaunchScreen();
let rejectBrowse:
((reason: { code: string; message: string }) => void) | undefined;
const browsePromise = new Promise<MediaPlayerItem>((_resolve, reject) => {
rejectBrowse = reject;
});
window.rejectMediaBrowse = () =>
rejectBrowse?.({ code: "unknown_error", message: "Browse failed" });
hass.mockWS("media_source/browse_media", () => browsePromise);
};
// ── Registry ──────────────────────────────────────────────────────────────
export const scenarios: Record<string, Scenario> = {
@@ -228,11 +131,6 @@ export const scenarios: Record<string, Scenario> = {
"non-admin": nonAdminScenario,
"dark-theme": darkThemeScenario,
"custom-theme": customThemeScenario,
"delayed-calendar": delayedCalendarScenario,
"delayed-generated-dashboard": delayedGeneratedDashboardScenario,
"delayed-media-browse": delayedMediaBrowseScenario,
"delayed-media-browse-error": delayedMediaBrowseErrorScenario,
"light-more-info": lightMoreInfoScenario,
"quick-search-assist": quickSearchAssistScenario,
"delayed-lovelace": delayedLovelaceScenario,
};
+104 -104
View File
@@ -40,7 +40,7 @@ __metadata:
languageName: node
linkType: hard
"@asamuzakjp/dom-selector@npm:^8.3.0":
"@asamuzakjp/dom-selector@npm:^8.2.5":
version: 8.3.0
resolution: "@asamuzakjp/dom-selector@npm:8.3.0"
dependencies:
@@ -2499,7 +2499,7 @@ __metadata:
languageName: node
linkType: hard
"@csstools/css-syntax-patches-for-csstree@npm:^1.1.7":
"@csstools/css-syntax-patches-for-csstree@npm:^1.1.6":
version: 1.1.7
resolution: "@csstools/css-syntax-patches-for-csstree@npm:1.1.7"
peerDependencies:
@@ -2763,12 +2763,12 @@ __metadata:
languageName: node
linkType: hard
"@formatjs/icu-messageformat-parser@npm:3.5.16":
version: 3.5.16
resolution: "@formatjs/icu-messageformat-parser@npm:3.5.16"
"@formatjs/icu-messageformat-parser@npm:3.5.15":
version: 3.5.15
resolution: "@formatjs/icu-messageformat-parser@npm:3.5.15"
dependencies:
"@formatjs/icu-skeleton-parser": "npm:2.1.11"
checksum: 10/406cf08cf01a68e244c7077b726b199dde6ba4c95ecb2d807bff9ec62a9acc77b7f47fad196f02dfbd7a7b8110cccf5d5392e9693069e2484cf96f00899a728f
checksum: 10/4337a0bc8df86c498509a3a4b51fc2b4bb6e36c444447d64b5d5ca7b60b3dac676e67863e160b5269f7d35a42f3ab17d2eeeb056123120181db31c7a50f831e9
languageName: node
linkType: hard
@@ -2779,13 +2779,13 @@ __metadata:
languageName: node
linkType: hard
"@formatjs/intl-datetimeformat@npm:7.6.0":
version: 7.6.0
resolution: "@formatjs/intl-datetimeformat@npm:7.6.0"
"@formatjs/intl-datetimeformat@npm:7.5.2":
version: 7.5.2
resolution: "@formatjs/intl-datetimeformat@npm:7.5.2"
dependencies:
"@formatjs/bigdecimal": "npm:0.2.7"
"@formatjs/intl-localematcher": "npm:0.8.13"
checksum: 10/8d172390d12c0d771721e5737d4af37ed3dd3bdbb59dfbc7ac2280ee17ff0ed56a737d0b8a3ca507e40c556140b4297a28d72b33b5b3b3e9becca55d8ef26549
checksum: 10/a9b18f890bd1fd747b6a4639a7fcc30456dfed2e78aebeca1dac245f1f2a5a20d295a9a125fc7037a7603ec257e0fadd7989ea180c68dd5a30c8ae2c429d689d
languageName: node
linkType: hard
@@ -2843,13 +2843,13 @@ __metadata:
languageName: node
linkType: hard
"@formatjs/intl-numberformat@npm:9.4.0":
version: 9.4.0
resolution: "@formatjs/intl-numberformat@npm:9.4.0"
"@formatjs/intl-numberformat@npm:9.3.14":
version: 9.3.14
resolution: "@formatjs/intl-numberformat@npm:9.3.14"
dependencies:
"@formatjs/bigdecimal": "npm:0.2.7"
"@formatjs/intl-localematcher": "npm:0.8.13"
checksum: 10/63476552f2cb9dd8530c8965bf891e9ead573813e5a42c1fc2dc7e254ff3fec15aba90ac44a4df1a875a2c2e04431e7eea643098651afaebf2f439d4a7cacea0
checksum: 10/7ba06936b41674cdcfa2aabc05a1dac5defbbb57a790bf8e3eb97d930750b0ec5ba715124cb3661d54015072f6877cbece5d92fa5e2ab98ba32d096c99432140
languageName: node
linkType: hard
@@ -4208,14 +4208,14 @@ __metadata:
languageName: node
linkType: hard
"@playwright/test@npm:1.62.1":
version: 1.62.1
resolution: "@playwright/test@npm:1.62.1"
"@playwright/test@npm:1.62.0":
version: 1.62.0
resolution: "@playwright/test@npm:1.62.0"
dependencies:
playwright: "npm:1.62.1"
playwright: "npm:1.62.0"
bin:
playwright: cli.js
checksum: 10/a84fdc2b0c0f01131fa4c0f0aaebf2698a618ca4a459e3b71ebec4b0b8a5be5b7b1a3cfc063e316d4d75f403a04078d214baf69c4bcbc9606ba12be696d4ff14
checksum: 10/90b504f3933b97687580d7e6ba42ce9e73a9cdd41da5ae59441104b3dfa1508142ed17e5742acc70fdc2aef79078365859c6a463cebf46a8f7db9506a3ece1d6
languageName: node
linkType: hard
@@ -4740,65 +4740,65 @@ __metadata:
languageName: node
linkType: hard
"@rspack/binding-darwin-arm64@npm:2.1.7":
version: 2.1.7
resolution: "@rspack/binding-darwin-arm64@npm:2.1.7"
"@rspack/binding-darwin-arm64@npm:2.1.6":
version: 2.1.6
resolution: "@rspack/binding-darwin-arm64@npm:2.1.6"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@rspack/binding-darwin-x64@npm:2.1.7":
version: 2.1.7
resolution: "@rspack/binding-darwin-x64@npm:2.1.7"
"@rspack/binding-darwin-x64@npm:2.1.6":
version: 2.1.6
resolution: "@rspack/binding-darwin-x64@npm:2.1.6"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@rspack/binding-linux-arm64-gnu@npm:2.1.7":
version: 2.1.7
resolution: "@rspack/binding-linux-arm64-gnu@npm:2.1.7"
"@rspack/binding-linux-arm64-gnu@npm:2.1.6":
version: 2.1.6
resolution: "@rspack/binding-linux-arm64-gnu@npm:2.1.6"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"@rspack/binding-linux-arm64-musl@npm:2.1.7":
version: 2.1.7
resolution: "@rspack/binding-linux-arm64-musl@npm:2.1.7"
"@rspack/binding-linux-arm64-musl@npm:2.1.6":
version: 2.1.6
resolution: "@rspack/binding-linux-arm64-musl@npm:2.1.6"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"@rspack/binding-linux-riscv64-gnu@npm:2.1.7":
version: 2.1.7
resolution: "@rspack/binding-linux-riscv64-gnu@npm:2.1.7"
"@rspack/binding-linux-riscv64-gnu@npm:2.1.6":
version: 2.1.6
resolution: "@rspack/binding-linux-riscv64-gnu@npm:2.1.6"
conditions: os=linux & cpu=riscv64 & libc=glibc
languageName: node
linkType: hard
"@rspack/binding-linux-riscv64-musl@npm:2.1.7":
version: 2.1.7
resolution: "@rspack/binding-linux-riscv64-musl@npm:2.1.7"
"@rspack/binding-linux-riscv64-musl@npm:2.1.6":
version: 2.1.6
resolution: "@rspack/binding-linux-riscv64-musl@npm:2.1.6"
conditions: os=linux & cpu=riscv64 & libc=musl
languageName: node
linkType: hard
"@rspack/binding-linux-x64-gnu@npm:2.1.7":
version: 2.1.7
resolution: "@rspack/binding-linux-x64-gnu@npm:2.1.7"
"@rspack/binding-linux-x64-gnu@npm:2.1.6":
version: 2.1.6
resolution: "@rspack/binding-linux-x64-gnu@npm:2.1.6"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"@rspack/binding-linux-x64-musl@npm:2.1.7":
version: 2.1.7
resolution: "@rspack/binding-linux-x64-musl@npm:2.1.7"
"@rspack/binding-linux-x64-musl@npm:2.1.6":
version: 2.1.6
resolution: "@rspack/binding-linux-x64-musl@npm:2.1.6"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"@rspack/binding-wasm32-wasi@npm:2.1.7":
version: 2.1.7
resolution: "@rspack/binding-wasm32-wasi@npm:2.1.7"
"@rspack/binding-wasm32-wasi@npm:2.1.6":
version: 2.1.6
resolution: "@rspack/binding-wasm32-wasi@npm:2.1.6"
dependencies:
"@emnapi/core": "npm:1.11.2"
"@emnapi/runtime": "npm:1.11.2"
@@ -4807,43 +4807,43 @@ __metadata:
languageName: node
linkType: hard
"@rspack/binding-win32-arm64-msvc@npm:2.1.7":
version: 2.1.7
resolution: "@rspack/binding-win32-arm64-msvc@npm:2.1.7"
"@rspack/binding-win32-arm64-msvc@npm:2.1.6":
version: 2.1.6
resolution: "@rspack/binding-win32-arm64-msvc@npm:2.1.6"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"@rspack/binding-win32-ia32-msvc@npm:2.1.7":
version: 2.1.7
resolution: "@rspack/binding-win32-ia32-msvc@npm:2.1.7"
"@rspack/binding-win32-ia32-msvc@npm:2.1.6":
version: 2.1.6
resolution: "@rspack/binding-win32-ia32-msvc@npm:2.1.6"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
"@rspack/binding-win32-x64-msvc@npm:2.1.7":
version: 2.1.7
resolution: "@rspack/binding-win32-x64-msvc@npm:2.1.7"
"@rspack/binding-win32-x64-msvc@npm:2.1.6":
version: 2.1.6
resolution: "@rspack/binding-win32-x64-msvc@npm:2.1.6"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"@rspack/binding@npm:2.1.7":
version: 2.1.7
resolution: "@rspack/binding@npm:2.1.7"
"@rspack/binding@npm:2.1.6":
version: 2.1.6
resolution: "@rspack/binding@npm:2.1.6"
dependencies:
"@rspack/binding-darwin-arm64": "npm:2.1.7"
"@rspack/binding-darwin-x64": "npm:2.1.7"
"@rspack/binding-linux-arm64-gnu": "npm:2.1.7"
"@rspack/binding-linux-arm64-musl": "npm:2.1.7"
"@rspack/binding-linux-riscv64-gnu": "npm:2.1.7"
"@rspack/binding-linux-riscv64-musl": "npm:2.1.7"
"@rspack/binding-linux-x64-gnu": "npm:2.1.7"
"@rspack/binding-linux-x64-musl": "npm:2.1.7"
"@rspack/binding-wasm32-wasi": "npm:2.1.7"
"@rspack/binding-win32-arm64-msvc": "npm:2.1.7"
"@rspack/binding-win32-ia32-msvc": "npm:2.1.7"
"@rspack/binding-win32-x64-msvc": "npm:2.1.7"
"@rspack/binding-darwin-arm64": "npm:2.1.6"
"@rspack/binding-darwin-x64": "npm:2.1.6"
"@rspack/binding-linux-arm64-gnu": "npm:2.1.6"
"@rspack/binding-linux-arm64-musl": "npm:2.1.6"
"@rspack/binding-linux-riscv64-gnu": "npm:2.1.6"
"@rspack/binding-linux-riscv64-musl": "npm:2.1.6"
"@rspack/binding-linux-x64-gnu": "npm:2.1.6"
"@rspack/binding-linux-x64-musl": "npm:2.1.6"
"@rspack/binding-wasm32-wasi": "npm:2.1.6"
"@rspack/binding-win32-arm64-msvc": "npm:2.1.6"
"@rspack/binding-win32-ia32-msvc": "npm:2.1.6"
"@rspack/binding-win32-x64-msvc": "npm:2.1.6"
dependenciesMeta:
"@rspack/binding-darwin-arm64":
optional: true
@@ -4869,15 +4869,15 @@ __metadata:
optional: true
"@rspack/binding-win32-x64-msvc":
optional: true
checksum: 10/1400d8553d2122850fc43a45b69828bcdcbccf7715381a627802e160da6f93e90ca41d0280a69d175894bc29a428a374c80157b874883363b998090dc0aff33c
checksum: 10/0d164b7425f448ed6b8b81e687f4a82377289476cee41553ced9532e211d216fa13429b9125f3e3f025e391709bf752983c16eb4549341a21cbd0c61510afbeb
languageName: node
linkType: hard
"@rspack/core@npm:2.1.7":
version: 2.1.7
resolution: "@rspack/core@npm:2.1.7"
"@rspack/core@npm:2.1.6":
version: 2.1.6
resolution: "@rspack/core@npm:2.1.6"
dependencies:
"@rspack/binding": "npm:2.1.7"
"@rspack/binding": "npm:2.1.6"
peerDependencies:
"@module-federation/runtime-tools": ^0.24.1 || ^2.0.0
"@swc/helpers": ^0.5.23
@@ -4886,7 +4886,7 @@ __metadata:
optional: true
"@swc/helpers":
optional: true
checksum: 10/84526df889fa2813cfa7dfbed71b713c47c2010b1f55da26db698a87ea11f3f433a931ec29616ff300cb63d0ea27cbe7d2b40df64abf45f9b1a484b59c94ca1a
checksum: 10/334f8e13aa3540b7320d3186250fc4e2f128711f765d2073274188f23c692d4262e7cbb88c10571626b55e480186c474bdbcb564e7fa97df803479554de855b4
languageName: node
linkType: hard
@@ -9904,13 +9904,13 @@ __metadata:
"@date-fns/tz": "npm:1.5.0"
"@egjs/hammerjs": "npm:2.0.17"
"@eslint/js": "npm:10.0.1"
"@formatjs/intl-datetimeformat": "npm:7.6.0"
"@formatjs/intl-datetimeformat": "npm:7.5.2"
"@formatjs/intl-displaynames": "npm:7.3.13"
"@formatjs/intl-durationformat": "npm:0.10.18"
"@formatjs/intl-getcanonicallocales": "npm:3.2.11"
"@formatjs/intl-listformat": "npm:8.3.13"
"@formatjs/intl-locale": "npm:5.3.10"
"@formatjs/intl-numberformat": "npm:9.4.0"
"@formatjs/intl-numberformat": "npm:9.3.14"
"@formatjs/intl-pluralrules": "npm:6.3.13"
"@formatjs/intl-relativetimeformat": "npm:12.3.13"
"@fullcalendar/core": "npm:6.1.21"
@@ -9937,10 +9937,10 @@ __metadata:
"@octokit/auth-oauth-device": "npm:8.0.3"
"@octokit/plugin-retry": "npm:8.1.0"
"@octokit/rest": "npm:22.0.1"
"@playwright/test": "npm:1.62.1"
"@playwright/test": "npm:1.62.0"
"@replit/codemirror-indentation-markers": "npm:6.5.3"
"@rsdoctor/rspack-plugin": "npm:1.6.1"
"@rspack/core": "npm:2.1.7"
"@rspack/core": "npm:2.1.6"
"@rspack/dev-server": "npm:2.1.0"
"@swc/helpers": "npm:0.5.23"
"@thomasloven/round-slider": "npm:0.6.0"
@@ -10007,9 +10007,9 @@ __metadata:
html-minifier-terser: "npm:7.2.0"
husky: "npm:9.1.7"
idb-keyval: "npm:6.3.0"
intl-messageformat: "npm:11.2.13"
intl-messageformat: "npm:11.2.12"
js-yaml: "npm:5.2.2"
jsdom: "npm:30.0.1"
jsdom: "npm:30.0.0"
jszip: "npm:3.10.1"
leaflet: "npm:1.9.4"
leaflet-draw: "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
@@ -10355,13 +10355,13 @@ __metadata:
languageName: node
linkType: hard
"intl-messageformat@npm:11.2.13":
version: 11.2.13
resolution: "intl-messageformat@npm:11.2.13"
"intl-messageformat@npm:11.2.12":
version: 11.2.12
resolution: "intl-messageformat@npm:11.2.12"
dependencies:
"@formatjs/fast-memoize": "npm:3.1.7"
"@formatjs/icu-messageformat-parser": "npm:3.5.16"
checksum: 10/7da1b2e01258ae310cd3aeabd6302497a70755ef860cc666405e89ede2927f5d5995d8a6f9be556d7dfc4f7b1eb0a13b26c9633234fdb4afea762013bb25af65
"@formatjs/icu-messageformat-parser": "npm:3.5.15"
checksum: 10/e9fb7dfe11f3fc34cb01e24f550f060c8c3e56a07d634b9e10a525a57df49300cf0721fc3a8b3771fb54e8a3171571815f36dd2ab9f78024bbc11b4a58f947a2
languageName: node
linkType: hard
@@ -10964,14 +10964,14 @@ __metadata:
languageName: node
linkType: hard
"jsdom@npm:30.0.1":
version: 30.0.1
resolution: "jsdom@npm:30.0.1"
"jsdom@npm:30.0.0":
version: 30.0.0
resolution: "jsdom@npm:30.0.0"
dependencies:
"@asamuzakjp/css-color": "npm:^6.0.5"
"@asamuzakjp/dom-selector": "npm:^8.3.0"
"@asamuzakjp/dom-selector": "npm:^8.2.5"
"@bramus/specificity": "npm:^2.4.2"
"@csstools/css-syntax-patches-for-csstree": "npm:^1.1.7"
"@csstools/css-syntax-patches-for-csstree": "npm:^1.1.6"
"@exodus/bytes": "npm:^1.15.1"
css-tree: "npm:^3.2.1"
data-urls: "npm:^7.0.0"
@@ -10983,7 +10983,7 @@ __metadata:
saxes: "npm:^6.0.0"
symbol-tree: "npm:^3.2.4"
tough-cookie: "npm:^6.0.2"
undici: "npm:^8.9.0"
undici: "npm:^8.7.0"
w3c-xmlserializer: "npm:^5.0.0"
webidl-conversions: "npm:^8.0.1"
whatwg-mimetype: "npm:^5.0.0"
@@ -10994,7 +10994,7 @@ __metadata:
peerDependenciesMeta:
canvas:
optional: true
checksum: 10/5089ceca7d340b3beec451b7a3286f2bf38d707482e0bdcf046e63b4de3ea2e679372fbb733c5c24c1ce1cc9e70272d10e49fd1b4b146beb77b12c17c069cb36
checksum: 10/dc73c071e67224465018733525047d05772667bd8e00a3703a6f6515238c027b6ca768bf86874a53d512fd0bb962d72cc6a6c31c1bc38e52318f9d412ad0bb90
languageName: node
linkType: hard
@@ -12748,27 +12748,27 @@ __metadata:
languageName: node
linkType: hard
"playwright-core@npm:1.62.1":
version: 1.62.1
resolution: "playwright-core@npm:1.62.1"
"playwright-core@npm:1.62.0":
version: 1.62.0
resolution: "playwright-core@npm:1.62.0"
bin:
playwright-core: cli.js
checksum: 10/2dab88793a6a5cbfa27641664548ded72a4f869d7eab8d3541e070081b5e09595253aa3dbd42eb4e6eca1b03d96a727c60704359b5b309e71ca58b12549a4c06
checksum: 10/1cf036e9ed51ea80d311ac25a871dddbc57d5e7e7a5ad23fc2741452aa8406c95e820901ec38aeecc6fca2ddc383ccc8acb0e2f29a4c12942dd0a1a63e7134a2
languageName: node
linkType: hard
"playwright@npm:1.62.1":
version: 1.62.1
resolution: "playwright@npm:1.62.1"
"playwright@npm:1.62.0":
version: 1.62.0
resolution: "playwright@npm:1.62.0"
dependencies:
fsevents: "npm:2.3.2"
playwright-core: "npm:1.62.1"
playwright-core: "npm:1.62.0"
dependenciesMeta:
fsevents:
optional: true
bin:
playwright: cli.js
checksum: 10/f99546873ab2545160ad5a0145fef8e2c1d805efd1ba77356f5b0ee3d8688192b3a2af08cd948bece36ca386fdfad889ef7e53b7aaad309940744c0bbccc0a19
checksum: 10/52401b5f2b67abcb555c1b1c229e5204a3ac72a604343a99e60b00bd0cd46ad661a9471ced4e6c3ff39de957a0b1f413810d191ce81cd40e455700d73a6a80a5
languageName: node
linkType: hard
@@ -15056,7 +15056,7 @@ __metadata:
languageName: node
linkType: hard
"undici@npm:^8.4.1, undici@npm:^8.9.0":
"undici@npm:^8.4.1, undici@npm:^8.7.0":
version: 8.9.0
resolution: "undici@npm:8.9.0"
checksum: 10/dfad3e233087eafdf1d361acd17c4d45a9590d5db9400458f1c03f47d8f1ef68647e2109ebb982c862e50c227093abd2d5f44b154904fc0b3d22576b6e95cfe7