Compare commits

...
13 Commits
Author SHA1 Message Date
chaim-shwartzandGitHub c01a140751 Fix crash when hass.auth is undefined in partial-panel-resolver (#53962) 2026-09-07 18:33:04 +00:00
e50eaaa865 Add more automation row details (#54018)
* Add for and offset to trigger and condition rows

* Add actions with timeout.

* fix separator

* Fix templated automation row durations

Co-authored-by: wendevlin <[email protected]>

* review

* Enhance duration validation with finite check

Co-authored-by: Copilot Autofix powered by AI <[email protected]>

* Add behavior inrow

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
2026-09-07 20:31:23 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
cd8b975a38 Update dependency @codemirror/state to v6.7.4 (#54045)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-07 18:26:07 +00:00
5dd3a4b662 Add UI/UX readiness skill and strengthen review guidance (#54032)
* Create UX readyness skill

* Refine UI/UX review skill responsibilities

* Account for existing PR review feedback

* Clarify UI/UX readiness evidence

* Simplify UI/UX readiness routes

* Update review guidelines for pull requests

Co-authored-by: Copilot Autofix powered by AI <[email protected]>

* Handle optional gallery artifacts

---------

Co-authored-by: Copilot Autofix powered by AI <[email protected]>
2026-09-07 20:21:40 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
e5fa96465c Update dependency eslint to v10.10.0 (#54044)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-07 20:17:55 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
79467b51b1 Update dependency browserslist to v4.28.9 (#54043)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-07 20:17:49 +02:00
Maarten LakerveldandGitHub c3cc2217bf Don't zoom the zone map when opening the home zone editor (#54033) 2026-09-07 14:34:17 +01:00
6e6e4d4c4d Point at the sources pane from history and activity empty states (#54019)
Co-authored-by: Claude Opus 5 <[email protected]>
2026-09-07 14:09:13 +02:00
Aidan TimsonandGitHub f6cbe49d81 Resolve Supervisor browser URLs against Home Assistant (#53944)
* Resolve browser URLs against Home Assistant

* Resolve Supervisor log download URLs against Home Assistant
2026-09-07 13:12:49 +02:00
Aidan TimsonandGitHub 1e9592a0b8 Resolve Supervisor URLs against HA context for icons (#53942)
Resolve Supervisor URLs against Home Assistant
2026-09-07 12:07:28 +01:00
Aidan TimsonandGitHub b2b0491c99 Centralize Supervisor app icons (#53941)
* Centralize Supervisor app icons

* Constrain app icons in combo boxes

* Preserve quick bar app icon styles

* Style colored app icons in combo box items

* Use fixed dimensions for colored app icons
2026-09-07 12:01:55 +01:00
9bffae3ee4 Exit scene live mode when leaving the editor (#53837)
* Exit scene live mode when leaving the editor

Leaving the page previously only unsubscribed, so live-activated
device states stayed behind. Restore stored states unless the tab
is hidden for panel suspend.

* Drop extra unit tests for scene live-mode disconnect.

* Restore scene live states only on editor disconnect.

Back and delete already unmount the editor, which now exits live
mode, so a second applyScene was redundant.

* Resubscribe to scene live edits after hidden-tab suspend.

Existing scenes stayed in live mode when the panel was reattached,
but the state_changed subscription was not restored.

* Keep the scene live subscription across hidden-tab suspend.

Unsubscribing and resubscribing on reattach races the closed
websocket; leaving the subscription in place lets the library
restore it after reconnect.

* Tear down scene live mode on hidden-tab route changes.

document.hidden is not enough: saving a new scene remounts a
new editor. Skip teardown only when an ancestor was detached
(panel suspend). Reuse an existing live subscription on
reattach so new scenes do not get duplicate listeners.

Co-authored-by: Petar Petrov <[email protected]>

---------

Co-authored-by: Cursor Agent <[email protected]>
2026-09-07 12:08:46 +02:00
renovate[bot]andGitHub a287aacd98 Update dependency @codemirror/state to v6.7.3 (#54020) 2026-09-07 11:05:14 +01:00
40 changed files with 1001 additions and 266 deletions
+20 -1
View File
@@ -1,6 +1,6 @@
---
name: ha-frontend-review
description: Home Assistant frontend PR and review guidance. Use when reviewing frontend changes, preparing a PR, checking recurring review issues, or applying the PR template.
description: Home Assistant frontend PR and review guidance, including implementation quality, existing review feedback, established design patterns, and the authority of UI/UX evidence. Use when reviewing frontend changes, preparing a PR, checking recurring review issues, or applying the PR template.
---
# HA Frontend Review
@@ -92,9 +92,28 @@ Configuration and props:
- Keep APIs extensible without adding speculative abstractions.
- Validate configuration before applying changes.
## UI/UX Evidence
For user-facing changes, establish the existing design context as part of frontend review:
- Treat an applicable gallery specification as the authoritative repository source for the documented component or interaction. Inspect its written guidance and demos when present.
- Note relevant production designs, approved designs, and shared `ha-*` components so the implementation can also be compared with established frontend behaviour.
- Require the applicable gallery documentation or demo to change when the implementation intentionally changes behaviour documented there.
- Prefer an established input layout or appropriate shared component over a raw input or ad hoc control.
- Treat explicit UI/UX approval, current repository guidance, and direct human or workflow instructions as stronger evidence than inference.
- Treat linked tasks as evidence of the problem and stated requirements, not automatic UI/UX approval.
- When the direction remains uncertain, search frontend pull requests that carry or previously carried **Needs UX**. Inspect their label history, comments, and reviews rather than relying on the current label alone.
- Judge relevant feedback by its content and surrounding discussion. When GitHub provides `author_association`, prioritise feedback marked `MEMBER`.
- Prefer recent feedback about similar interactions or components. Label removal can show that a workflow gate moved on, but does not prove approval without the surrounding discussion.
- Summarise the applicable guidance instead of maintaining a reviewer list or treating one historical decision as a permanent rule.
## Review Flow
- Before reviewing a pull request, read its existing comments, reviews, and threads, including their status, resolver, and Copilot resolution reason when available.
- Prioritise substantive human feedback, especially from authors marked `MEMBER`, and validate agent-generated feedback against the code and repository guidance.
- Do not duplicate unresolved findings as new inline comments; reference any that still need action in the review summary. Treat resolved feedback as closed only when the resolution reason or surrounding discussion supports that outcome; otherwise validate it against the current code before suppressing it. Respect **Won't fix** and **Incorrect** reasons.
- Identify behavioral regressions, bugs, accessibility issues, and missing tests first.
- Record the applicable UI/UX evidence for user-facing changes, whether or not further input is needed.
- Keep style-only comments secondary unless they affect maintainability or user experience.
- Prefer small, direct fixes over large refactors during review follow-up.
- Load the matching `ha-frontend-*` skill when a finding falls within its area.
@@ -0,0 +1,44 @@
---
name: ha-frontend-ux-readiness
description: Home Assistant frontend UI/UX readiness routing for new cards, badges, screens, and interaction patterns. Use it with `ha-frontend-review` to decide whether frontend review is enough or further UI/UX input is needed. Skip it for PRs already carrying Needs UX and for fixes or technical details that preserve an established experience.
---
# HA Frontend UI/UX Readiness
Use the design context and evidence gathered through `ha-frontend-review` to decide whether a change's UI/UX direction is settled and whether further UI/UX review should be recommended or is explicitly required. This skill routes the result; it does not repeat or replace implementation, accessibility, or frontend quality review.
## Early Exit
If the PR already carries the **Needs UX** label, reviewers have already decided to ask designers or UX reviewers for guidance. Do not use this skill to make that recommendation again.
## Triage
1. Apply `ha-frontend-review` and record its UI/UX evidence.
- Include the applicable gallery specification, production or approved designs, shared `ha-*` components, task requirements, historical feedback, and direct instructions.
- Keep the implementation review separate from this readiness decision.
2. Classify the user-facing decision.
- Existing-feature fixes and technical details normally remain with frontend review when they preserve the established experience.
- New cards, badges, screens, and interaction patterns normally need this readiness check.
3. Decide whether the evidence gathered during frontend review settles the direction.
- Frontend maintainer guidance, explicit UI/UX approval, current guidance, an established pattern, or a task that settles the material visual and interaction choices can settle the direction.
- Insufficient or conflicting evidence leaves the direction unresolved.
4. Select exactly one route below and state which frontend-review evidence supports it.
## Routes
- **Frontend review sufficient:** The change preserves the established experience, or the evidence settles the material visual and interaction choices. Cite that evidence and continue with `ha-frontend-review`.
- **UI/UX review recommended:** A new user experience has material design choices that remain unresolved. Identify the decision that needs input and recommend UI/UX review.
- **UI/UX review required:** A workflow rule or direct human instruction explicitly requires UI/UX review. Cite that evidence. A new design, task link, or lack of evidence is not enough on its own.
Frontend maintainer guidance, explicit UI/UX approval, established patterns, and task requirements inform the route. A task can settle requirements without settling the UI/UX. Recommend UI/UX review when important design choices remain unresolved.
## Reporting
Keep the result concise and use this structure:
- **Route:** Frontend review sufficient, UI/UX review recommended, or UI/UX review required
- **Established patterns:** relevant gallery specifications, production designs, and shared `ha-*` components for the next reviewer to compare
- **Supporting evidence:** approval, guidance, established patterns, task requirements, direct instructions, or the evidence gap or conflict
- **Next action:** the review action implied by the route
Separate whether the UI/UX direction is settled from whether the implementation is correct.
+2 -1
View File
@@ -47,7 +47,8 @@ Detailed guidance lives in project skills under `.agents/skills/`. Load the matc
- `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.
- `ha-frontend-review`: PR template use, review checklist, and recurring review issues.
- `ha-frontend-review`: PR template use, existing review feedback, review checklist, recurring issues, and UI/UX evidence including gallery specifications.
- `ha-frontend-ux-readiness`: UI/UX routing that complements frontend review for new user experiences.
- `ha-frontend-gallery`: gallery pages, demos, sidebar structure, content, and verification.
- `ha-frontend-demo`: standalone demo structure, configurations, navigation, shared stubs, and verification.
+3 -3
View File
@@ -48,7 +48,7 @@
"@codemirror/language": "6.12.4",
"@codemirror/lint": "6.9.7",
"@codemirror/search": "6.7.2",
"@codemirror/state": "6.7.2",
"@codemirror/state": "6.7.4",
"@codemirror/view": "6.43.11",
"@date-fns/tz": "1.5.0",
"@egjs/hammerjs": "2.0.17",
@@ -175,10 +175,10 @@
"@vitest/coverage-v8": "4.1.11",
"babel-loader": "10.1.1",
"babel-plugin-polyfill-corejs3": "1.0.0",
"browserslist": "4.28.8",
"browserslist": "4.28.9",
"browserslist-useragent-regexp": "4.1.4",
"del": "8.0.1",
"eslint": "10.9.1",
"eslint": "10.10.0",
"eslint-config-prettier": "10.1.8",
"eslint-import-resolver-webpack": "0.13.11",
"eslint-plugin-import-x": "4.17.1",
+12
View File
@@ -57,6 +57,18 @@ export const formatDurationLong = (
duration: HaDurationData
) => formatDurationLongMem(locale).format(duration);
const formatDurationNarrowMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DurationFormat(locale.language, {
style: "narrow",
})
);
export const formatDurationNarrow = (
locale: FrontendLocaleData,
duration: HaDurationData
) => formatDurationNarrowMem(locale).format(duration);
const formatDigitalDurationMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DurationFormat(locale.language, {
+5
View File
@@ -0,0 +1,5 @@
export const isExternalHassUrl = (): boolean =>
Boolean(__HASS_URL__) && __HASS_URL__ !== location.origin;
export const resolveHassUrl = (path: string): string =>
isExternalHassUrl() ? new URL(path, __HASS_URL__).toString() : path;
+29 -13
View File
@@ -1,11 +1,12 @@
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { html, LitElement, nothing } 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 { fireEvent } from "../common/dom/fire_event";
import { fetchHassioAddonsInfo } from "../data/hassio/addon";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import "./ha-alert";
import "./ha-app-icon";
import "./ha-combo-box-item";
import "./ha-generic-picker";
import type { HaGenericPicker } from "./ha-generic-picker";
@@ -18,13 +19,22 @@ const SEARCH_KEYS = [
{ name: "search_labels.repository", weight: 5 },
];
const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
interface AddonPickerItem extends PickerComboBoxItem {
slug: string;
hasIcon: boolean;
}
const rowRenderer: RenderItemFunction<AddonPickerItem> = (item) => html`
<ha-combo-box-item type="button">
<span slot="headline">${item.primary}</span>
<span slot="supporting-text">${item.secondary}</span>
${
item.icon
? html` <img alt="" slot="start" .src=${item.icon} /> `
item.hasIcon
? html`<ha-app-icon
slot="start"
.slug=${item.slug}
.hasIcon=${item.hasIcon}
></ha-app-icon>`
: nothing
}
</ha-combo-box-item>
@@ -40,7 +50,7 @@ class HaAddonPicker extends LitElement {
@property() public helper?: string;
@state() private _addons?: PickerComboBoxItem[];
@state() private _addons?: AddonPickerItem[];
@property({ type: Boolean }) public disabled = false;
@@ -102,11 +112,10 @@ class HaAddonPicker extends LitElement {
.filter((addon) => addon.version)
.map((addon) => ({
id: addon.slug,
slug: addon.slug,
hasIcon: addon.icon,
primary: addon.name,
secondary: addon.slug,
icon: addon.icon
? `/api/hassio/addons/${addon.slug}/icon`
: undefined,
search_labels: {
description: addon.description || null,
repository: addon.repository || null,
@@ -151,15 +160,22 @@ class HaAddonPicker extends LitElement {
private _valueRenderer = (itemId: string) => {
const item = this._addons!.find((addon) => addon.id === itemId);
return html`${
item?.icon
? html`<img
item?.hasIcon
? html`<ha-app-icon
slot="start"
alt=${item.primary ?? "Unknown"}
.src=${item.icon}
/>`
.slug=${item.slug}
.hasIcon=${item.hasIcon}
.alt=${item.primary ?? "Unknown"}
></ha-app-icon>`
: nothing
}<span slot="headline">${item?.primary || "Unknown"}</span>`;
};
static styles = css`
ha-app-icon {
--ha-app-icon-size: var(--ha-space-8);
}
`;
}
declare global {
+77
View File
@@ -0,0 +1,77 @@
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { keyed } from "lit/directives/keyed";
import type { HASSDomCurrentTargetEvent } from "../common/dom/fire_event";
import { isExternalHassUrl } from "../common/url/hass-url";
import { supervisorUrl } from "../data/hassio/common";
@customElement("ha-app-icon")
export class HaAppIcon extends LitElement {
@property() public slug = "";
@property({ attribute: "has-icon", type: Boolean })
public hasIcon?: boolean;
@property() public alt = "";
@property() public loading: "eager" | "lazy" = "eager";
@state() private _failedSrc?: string;
@query("img") private _image?: HTMLImageElement;
protected render() {
const src = supervisorUrl(`addons/${this.slug}/icon`);
if (!this.slug || this.hasIcon === false || this._failedSrc === src) {
return html`<slot></slot>`;
}
return keyed(
src,
html`
<img
src=${src}
alt=${this.alt}
loading=${this.loading}
crossorigin=${ifDefined(
isExternalHassUrl() ? undefined : "anonymous"
)}
referrerpolicy="no-referrer"
@error=${this._handleError}
/>
`
);
}
private _handleError(ev: HASSDomCurrentTargetEvent<HTMLImageElement>) {
if (ev.currentTarget !== this._image) {
return;
}
this._failedSrc = ev.currentTarget.getAttribute("src") ?? undefined;
}
static styles = css`
:host {
display: inline-flex;
align-items: center;
justify-content: center;
width: var(--ha-app-icon-size, var(--mdc-icon-size));
height: var(--ha-app-icon-size, var(--mdc-icon-size));
flex-shrink: 0;
}
img {
width: 100%;
height: 100%;
object-fit: contain;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-app-icon": HaAppIcon;
}
}
+10 -1
View File
@@ -51,10 +51,19 @@ export class HaComboBoxItem extends HaMdListItem {
white-space: normal;
}
::slotted(state-badge),
::slotted(img) {
::slotted(img),
::slotted(ha-app-icon) {
width: 32px;
height: 32px;
}
::slotted(ha-app-icon.colored) {
width: 24px;
height: 24px;
padding: var(--ha-space-1);
border-radius: var(--ha-border-radius-circle);
background-color: var(--app-icon-background-color);
color: var(--white-color);
}
::slotted(.code) {
font-family: var(--ha-font-family-code);
font-size: var(--ha-font-size-xs);
+13
View File
@@ -13,6 +13,8 @@ import "./ha-dialog-footer";
import "./ha-filter-pane-chip";
import "./ha-icon-button";
const HIGHLIGHT_DURATION = 1500;
/**
* Filter pane for a filtered page: a column next to the content on wide
* screens, a bottom sheet on narrow ones. Mirrors the filter pane of
@@ -113,6 +115,15 @@ export class HaFilterPane extends LitElement {
`;
}
public highlight() {
const color = getComputedStyle(this).getPropertyValue("--primary-color");
this.getAnimations().forEach((animation) => animation.cancel());
this.animate([{ outlineColor: color }, { outlineColor: "transparent" }], {
duration: HIGHLIGHT_DURATION,
easing: "ease-out",
});
}
private _close() {
fireEvent(this, "close-filter-pane");
}
@@ -133,6 +144,8 @@ export class HaFilterPane extends LitElement {
box-sizing: border-box;
overflow: hidden;
border-inline-end: 1px solid var(--divider-color);
outline: var(--ha-border-width-md) solid transparent;
outline-offset: calc(-1 * var(--ha-border-width-md));
}
/* The bottom sheet positions itself, so the pane takes no space. */
+2 -1
View File
@@ -2,6 +2,7 @@ import { isComponentLoaded } from "../common/config/is_component_loaded";
import { atLeastVersion } from "../common/config/version";
import type { HomeAssistant, LogFileDisabledReason } from "../types";
import type { HassioAddonInfo } from "./hassio/addon";
import { supervisorUrl } from "./hassio/common";
export interface LogProvider {
key: string;
@@ -18,7 +19,7 @@ export const fetchErrorLog = (hass: HomeAssistant) =>
export const getErrorLogDownloadUrl = (hass: HomeAssistant) =>
hasSupervisorCoreLogDownload(hass)
? "/api/hassio/core/logs/latest"
? supervisorUrl("core/logs/latest")
: "/api/error_log";
export const getCoreLogFileDownloadUnavailableReason = (
+4
View File
@@ -1,5 +1,9 @@
import { resolveHassUrl } from "../../common/url/hass-url";
import type { CallWS } from "../../types";
export const supervisorUrl = (path: string): string =>
resolveHassUrl(`/api/hassio/${path}`);
export interface HassioResponse<T> {
data: T;
message?: string;
+10 -8
View File
@@ -1,6 +1,6 @@
import type { HomeAssistant, PanelInfo } from "../../types";
import type { SupervisorArch } from "../supervisor/supervisor";
import type { HassioResponse } from "./common";
import { supervisorUrl, type HassioResponse } from "./common";
export interface HassioHomeAssistantInfo {
arch: SupervisorArch;
@@ -198,18 +198,20 @@ export const fetchHassioLogsFollowSkip = async (
);
export const getHassioLogDownloadUrl = (provider: string) =>
`/api/hassio/${
provider.includes("_") ? `addons/${provider}` : provider
}/logs`;
supervisorUrl(
`${provider.includes("_") ? `addons/${provider}` : provider}/logs`
);
export const getHassioLogDownloadLinesUrl = (
provider: string,
lines: number,
boot = 0
) =>
`/api/hassio/${
provider.includes("_") ? `addons/${provider}` : provider
}/logs${boot !== 0 ? `/boots/${boot}` : ""}?lines=${lines}`;
supervisorUrl(
`${
provider.includes("_") ? `addons/${provider}` : provider
}/logs${boot !== 0 ? `/boots/${boot}` : ""}?lines=${lines}`
);
export const setSupervisorOption = async (
hass: HomeAssistant,
@@ -223,4 +225,4 @@ export const setSupervisorOption = async (
});
};
export const coreLatestLogsUrl = "/api/hassio/core/logs/latest";
export const coreLatestLogsUrl = supervisorUrl("core/logs/latest");
+7 -10
View File
@@ -28,6 +28,7 @@ export interface NavigationComboBoxItem extends PickerComboBoxItem {
path: string;
image?: string;
iconColor?: string;
app?: Pick<HassioAddonInfo, "slug" | "icon">;
}
export interface BaseNavigationCommand {
@@ -38,6 +39,7 @@ export interface BaseNavigationCommand {
iconPath?: string;
iconColor?: string;
image?: string;
app?: Pick<HassioAddonInfo, "slug" | "icon">;
}
export interface ActionCommandComboBoxItem extends PickerComboBoxItem {
@@ -62,19 +64,14 @@ const generateNavigationPanelCommands = (
const primary = localize(translationKey) || panel.title || panel.url_path;
let image: string | undefined;
if (apps) {
const app = apps.find(({ slug }) => slug === panel.url_path);
if (app) {
image = app.icon ? `/api/hassio/addons/${app.slug}/icon` : undefined;
}
}
const panelApp = apps?.find(({ slug }) => slug === panel.url_path);
return {
primary,
icon,
image,
app: panelApp
? { slug: panelApp.slug, icon: panelApp.icon }
: undefined,
path: `/${panel.url_path}`,
};
});
@@ -171,7 +168,7 @@ export const generateNavigationCommands = (
for (const app of apps.filter((a) => a.version)) {
appItems.push({
path: `/config/app/${app.slug}`,
image: app.icon ? `/api/hassio/addons/${app.slug}/icon` : undefined,
app: { slug: app.slug, icon: app.icon },
primary: hass.localize(
"ui.dialogs.quick-bar.commands.navigation.app_info",
{ app: app.name }
+53 -30
View File
@@ -12,6 +12,7 @@ import { navigate } from "../../common/navigate";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import "../../components/entity/state-badge";
import "../../components/ha-adaptive-dialog";
import "../../components/ha-app-icon";
import "../../components/ha-combo-box-item";
import "../../components/ha-domain-icon";
import "../../components/ha-icon";
@@ -310,6 +311,7 @@ export class QuickBar extends LitElement {
}
const iconPath = item.icon_path || mdiDevices;
const iconColor = "iconColor" in item ? item.iconColor : undefined;
return html`
<ha-combo-box-item
@@ -334,44 +336,65 @@ export class QuickBar extends LitElement {
brand-fallback
></ha-domain-icon>
`
: "image" in item && item.image
: "app" in item && item.app
? html`
<img
<ha-app-icon
slot="start"
alt=${item.primary ?? "Unknown"}
.src=${item.image}
.slug=${item.app.slug}
.hasIcon=${item.app.icon}
.alt=${item.primary ?? "Unknown"}
class=${iconColor ? "colored" : nothing}
style=${
"iconColor" in item && item.iconColor
? `background-color: ${item.iconColor}; padding: 4px; border-radius: var(--ha-border-radius-circle); width: 24px; height: 24px`
: ""
iconColor
? `--app-icon-background-color: ${iconColor}`
: nothing
}
/>
>
${
item.icon
? html`<ha-icon .icon=${item.icon}></ha-icon>`
: html`<ha-svg-icon .path=${iconPath}></ha-svg-icon>`
}
</ha-app-icon>
`
: item.icon
? html`<ha-icon
style="margin: var(--ha-space-1);"
slot="start"
.icon=${item.icon}
></ha-icon>`
: "iconColor" in item && item.iconColor
? html`
<div
slot="start"
style=${`padding: 4px; border-radius: var(--ha-border-radius-circle); background-color: ${item.iconColor};`}
>
: "image" in item && item.image
? html`
<img
slot="start"
alt=${item.primary ?? "Unknown"}
.src=${item.image}
style=${
"iconColor" in item && item.iconColor
? `background-color: ${item.iconColor}; padding: 4px; border-radius: var(--ha-border-radius-circle); width: 24px; height: 24px`
: ""
}
/>
`
: item.icon
? html`<ha-icon
style="margin: var(--ha-space-1);"
slot="start"
.icon=${item.icon}
></ha-icon>`
: "iconColor" in item && item.iconColor
? html`
<div
slot="start"
style=${`padding: 4px; border-radius: var(--ha-border-radius-circle); background-color: ${item.iconColor};`}
>
<ha-svg-icon
style="color: var(--white-color); --mdc-icon-size: 24px;"
.path=${iconPath}
></ha-svg-icon>
</div>
`
: html`
<ha-svg-icon
style="color: var(--white-color); --mdc-icon-size: 24px;"
style="margin: var(--ha-space-1);"
slot="start"
.path=${iconPath}
></ha-svg-icon>
</div>
`
: html`
<ha-svg-icon
style="margin: var(--ha-space-1);"
slot="start"
.path=${iconPath}
></ha-svg-icon>
`
`
}
<span slot="headline">${item.primary}</span>
${
+2 -2
View File
@@ -267,9 +267,9 @@ class PartialPanelResolver extends HassRouterPage {
// screen, so later panel updates do not fire it again. Native apps remove
// it instantly because their own splash screen is still visible.
if (
removeLaunchScreen(!!this.hass.auth.external?.config.hasSplashscreen)
removeLaunchScreen(!!this.hass.auth?.external?.config.hasSplashscreen)
) {
this.hass.auth.external?.fireMessage({ type: "frontend/loaded" });
this.hass.auth?.external?.fireMessage({ type: "frontend/loaded" });
}
}
}
@@ -86,6 +86,7 @@ import type { HassioStats } from "../../../../../data/hassio/common";
import {
extractApiErrorMessage,
fetchHassioStats,
supervisorUrl,
} from "../../../../../data/hassio/common";
import type { StoreAddonDetails } from "../../../../../data/supervisor/store";
import {
@@ -205,7 +206,9 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
<img
class="logo"
alt=""
src="/api/hassio/addons/${this._currentAddon.slug}/logo"
src=${supervisorUrl(
`addons/${this._currentAddon.slug}/logo`
)}
/>
`
: nothing
@@ -3,6 +3,7 @@ import { mdiCheckCircle, mdiHelpCircleOutline } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../components/ha-app-icon";
import "../../../../components/ha-svg-icon";
import type { AddonStage, AddonState } from "../../../../data/hassio/addon";
import type { HomeAssistant } from "../../../../types";
@@ -41,21 +42,28 @@ class SupervisorAppsCardContent extends LitElement {
@property() public icon = mdiHelpCircleOutline;
@property({ attribute: false }) public iconImage?: string;
@property({ attribute: false }) public appSlug?: string;
@property({ attribute: false }) public hasAppIcon?: boolean;
protected render(): TemplateResult {
return html`
<div class="app">
<div class="icon-wrapper">
${
this.iconImage
this.appSlug
? html`
<img
class="icon-image"
src=${this.iconImage}
.title=${this.iconTitle}
alt=${this.iconTitle ?? ""}
/>
<ha-app-icon
.slug=${this.appSlug}
.hasIcon=${this.hasAppIcon}
.alt=${this.iconTitle ?? ""}
.title=${this.iconTitle ?? ""}
>
<ha-svg-icon
class="app-icon"
.path=${this.icon}
></ha-svg-icon>
</ha-app-icon>
`
: html`
<ha-svg-icon
@@ -134,15 +142,15 @@ class SupervisorAppsCardContent extends LitElement {
width: 40px;
height: 40px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.app-icon {
margin-left: var(--ha-space-2);
margin-top: var(--ha-space-2);
color: var(--secondary-text-color);
}
.icon-image {
max-height: 40px;
max-width: 40px;
ha-app-icon {
--ha-app-icon-size: 40px;
}
.title {
flex: 1;
@@ -151,11 +151,8 @@ export class HaConfigAppsInstalled extends LitElement {
"ui.panel.config.apps.installed.app_running"
)
}
.iconImage=${
addon.icon
? `/api/hassio/addons/${addon.slug}/icon`
: undefined
}
.appSlug=${addon.slug}
.hasAppIcon=${addon.icon}
></supervisor-apps-card-content>
</div>
</ha-card>
@@ -116,11 +116,8 @@ export class SupervisorAppsRepositoryEl extends LitElement {
? "not_available"
: ""
}
.iconImage=${
addon.icon
? `/api/hassio/addons/${addon.slug}/icon`
: undefined
}
.appSlug=${addon.slug}
.hasAppIcon=${addon.icon}
.showTopbar=${addon.installed || !addon.available}
.topbarClass=${
addon.installed
@@ -39,9 +39,9 @@ import { capitalizeFirstLetter } from "../../../../common/string/capitalize-firs
import { truncateWithEllipsis } from "../../../../common/string/truncate-with-ellipsis";
import { handleStructError } from "../../../../common/structs/handle-errors";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import "../../../../components/automation/ha-automation-condition-live-test";
import "../../../../components/automation/ha-automation-row";
import type { HaAutomationRow } from "../../../../components/automation/ha-automation-row";
import "../../../../components/automation/ha-automation-condition-live-test";
import "../../../../components/automation/ha-automation-row-event-chip";
import "../../../../components/ha-card";
import "../../../../components/ha-dropdown";
@@ -91,6 +91,7 @@ import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
import { showEditorToast } from "../editor-toast";
import "../ha-automation-editor-warning";
import "../ha-automation-row-options";
import { overflowStyles, rowStyles } from "../styles";
import { getDeviceTarget } from "../target/get_device_target";
import { getEntityTarget } from "../target/get_entity_target";
@@ -373,6 +374,13 @@ export default class HaAutomationActionRow extends LitElement {
)
: nothing
}
${
type === "wait_template" || type === "wait_for_trigger"
? html`<ha-automation-row-options
.config=${this.action}
></ha-automation-row-options>`
: nothing
}
${
noteTooltipText
? html`
@@ -35,9 +35,9 @@ import { capitalizeFirstLetter } from "../../../../common/string/capitalize-firs
import { truncateWithEllipsis } from "../../../../common/string/truncate-with-ellipsis";
import { handleStructError } from "../../../../common/structs/handle-errors";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import "../../../../components/automation/ha-automation-condition-live-test";
import "../../../../components/automation/ha-automation-row";
import type { HaAutomationRow } from "../../../../components/automation/ha-automation-row";
import "../../../../components/automation/ha-automation-condition-live-test";
import "../../../../components/automation/ha-automation-row-event-chip";
import "../../../../components/ha-alert";
import "../../../../components/ha-card";
@@ -73,6 +73,8 @@ import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
import { showEditorToast } from "../editor-toast";
import "../ha-automation-editor-warning";
import "../ha-automation-row-behavior";
import "../ha-automation-row-options";
import { overflowStyles, rowStyles } from "../styles";
import { getDeviceTarget } from "../target/get_device_target";
import { getEntityTarget } from "../target/get_entity_target";
@@ -228,6 +230,15 @@ export default class HaAutomationConditionRow extends LitElement {
${capitalizeFirstLetter(
describeCondition(this.condition, this.hass, this._entityReg)
)}
${
this._getType(this.condition, this.conditionDescriptions) ===
"platform"
? html`<ha-automation-row-behavior
mode="condition"
.config=${this.condition}
></ha-automation-row-behavior>`
: nothing
}
${
target !== undefined || targetRequired
? this._renderTargets(
@@ -238,6 +249,14 @@ export default class HaAutomationConditionRow extends LitElement {
)
: nothing
}
${
this._getType(this.condition, this.conditionDescriptions) ===
"platform"
? html`<ha-automation-row-options
.config=${this.condition}
></ha-automation-row-options>`
: nothing
}
${
this.condition.note?.trim()
? html`
@@ -0,0 +1,66 @@
import { consume, type ContextType } from "@lit/context";
import { html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { internationalizationContext } from "../../../data/context";
import type {
AutomationBehaviorConditionMode,
AutomationBehaviorTriggerMode,
} from "../../../data/selector";
import { rowSummaryStyles } from "./styles";
interface HaAutomationRowBehaviorConfig {
options?: {
behavior?: AutomationBehaviorTriggerMode | AutomationBehaviorConditionMode;
};
}
@customElement("ha-automation-row-behavior")
export class HaAutomationRowBehavior extends LitElement {
@property({ attribute: false }) public config!: HaAutomationRowBehaviorConfig;
@property() public mode: "trigger" | "condition" = "trigger";
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
private get _label(): string | undefined {
const behavior = this.config?.options?.behavior;
if (!behavior) {
return undefined;
}
return (
(this.mode === "condition"
? this._i18n?.localize(
`ui.components.selectors.automation_behavior.condition.options.${behavior as AutomationBehaviorConditionMode}.row_label`
)
: this._i18n?.localize(
`ui.components.selectors.automation_behavior.trigger.options.${behavior as AutomationBehaviorTriggerMode}.row_label`
)) || undefined
);
}
protected render() {
const label = this._label;
if (!label) {
return nothing;
}
return html`<span class="dot-separator"></span>${label}`;
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
// Collapse the host when empty so the parent flex gap is not reserved.
this.toggleAttribute("hidden", !this._label);
}
static styles = rowSummaryStyles;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-row-behavior": HaAutomationRowBehavior;
}
}
@@ -0,0 +1,125 @@
import { consume, type ContextType } from "@lit/context";
import { html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { createDurationData } from "../../../common/datetime/create_duration_data";
import { formatDurationNarrow } from "../../../common/datetime/format_duration";
import { hasTemplate } from "../../../common/string/has-template";
import type { ForDict } from "../../../data/automation";
import { internationalizationContext } from "../../../data/context";
import { rowSummaryStyles } from "./styles";
interface HaAutomationRowOptionsConfig {
options?: {
for?: string | number | ForDict;
offset?: string | number | ForDict;
offset_type?: "before" | "after";
};
timeout?: string | number | ForDict;
}
@customElement("ha-automation-row-options")
export class HaAutomationRowOptions extends LitElement {
@property({ attribute: false }) public config!: HaAutomationRowOptionsConfig;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
protected render() {
const supportedOptions = this._formatOptions(this.config, this._i18n);
if (!supportedOptions.length) {
return nothing;
}
return html`<span class="dot-separator"></span
>${supportedOptions.join(", ")}`;
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
// Collapse the host when empty so the parent flex gap is not reserved.
this.toggleAttribute(
"hidden",
!this._formatOptions(this.config, this._i18n).length
);
}
private _formatOptions = memoizeOne(
(
config: HaAutomationRowOptionsConfig,
_i18n: typeof this._i18n
): string[] => {
const parts: string[] = [];
if ("options" in config && config.options) {
const options = config.options;
const forDuration = this._duration(options.for);
if (forDuration) {
parts.push(
this._i18n.localize(
"ui.panel.config.automation.editor.row_options.for",
{
duration: forDuration,
}
)
);
}
const offsetDuration = this._duration(options.offset);
if (offsetDuration) {
const offsetType =
options.offset_type === "before" ? "before" : "after";
parts.push(
this._i18n.localize(
`ui.panel.config.automation.editor.row_options.offset_${offsetType}`,
{ offset: offsetDuration }
)
);
}
}
if ("timeout" in config) {
const timeoutDuration = this._duration(config.timeout);
if (timeoutDuration) {
parts.push(
this._i18n.localize(
"ui.panel.config.automation.editor.row_options.timeout",
{ duration: timeoutDuration }
)
);
}
}
return parts;
}
);
private _duration(value: unknown): string | undefined {
if (value === undefined || hasTemplate(value)) {
return undefined;
}
const duration = createDurationData(
value as string | number | ForDict | undefined
);
if (
!duration ||
Object.values(duration).some(
(part) => part !== undefined && !Number.isFinite(part)
)
) {
return undefined;
}
return formatDurationNarrow(this._i18n.locale, duration);
}
static styles = rowSummaryStyles;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-row-options": HaAutomationRowOptions;
}
}
+19
View File
@@ -3,6 +3,25 @@ import { css } from "lit";
export const SIDEBAR_MIN_WIDTH = 375;
export const CONTENT_MIN_WIDTH = 350;
export const rowSummaryStyles = css`
:host {
display: inline-flex;
align-items: center;
gap: var(--ha-space-2);
vertical-align: middle;
color: var(--ha-color-text-secondary);
}
:host([hidden]) {
display: none;
}
.dot-separator {
width: 2px;
height: 2px;
border-radius: var(--ha-border-radius-circle);
background-color: currentColor;
}
`;
export const rowStyles = css`
ha-icon-button {
--mdc-theme-text-primary-on-background: var(--primary-text-color);
@@ -72,6 +72,8 @@ import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
import { showEditorToast } from "../editor-toast";
import "../ha-automation-editor-warning";
import "../ha-automation-row-behavior";
import "../ha-automation-row-options";
import { overflowStyles, rowStyles } from "../styles";
import { getDeviceTarget } from "../target/get_device_target";
import { getEntityTarget } from "../target/get_entity_target";
@@ -253,6 +255,13 @@ export default class HaAutomationTriggerRow extends LitElement {
${capitalizeFirstLetter(
describeTrigger(this.trigger, this.hass, this._entityReg)
)}
${
type === "platform"
? html`<ha-automation-row-behavior
.config=${this.trigger}
></ha-automation-row-behavior>`
: nothing
}
${
target !== undefined || targetRequired
? this._renderTargets(
@@ -263,6 +272,13 @@ export default class HaAutomationTriggerRow extends LitElement {
)
: nothing
}
${
type === "platform"
? html`<ha-automation-row-options
.config=${this.trigger}
></ha-automation-row-options>`
: nothing
}
${
type !== "list" &&
(this.trigger as Exclude<Trigger, TriggerList>).note?.trim()
@@ -4,8 +4,10 @@ import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stringCompare } from "../../../../common/string/compare";
import "../../../../components/ha-app-icon";
import "../../../../components/ha-checkbox";
import type { HaCheckbox } from "../../../../components/ha-checkbox";
import "../../../../components/ha-svg-icon";
import type { HomeAssistant } from "../../../../types";
import "./ha-backup-formfield-label";
@@ -50,13 +52,17 @@ export class HaBackupAddonsPicker extends LitElement {
<ha-backup-formfield-label
.label=${item.name}
.version=${this.hideVersion ? undefined : item.version}
.iconPath=${item.iconPath || mdiPuzzle}
.imageUrl=${
this.addons?.find((a) => a.slug === item.slug)?.icon
? `/api/hassio/addons/${item.slug}/icon`
: undefined
}
>
<ha-app-icon
slot="icon"
.slug=${item.slug}
.hasIcon=${item.icon}
loading="lazy"
>
<ha-svg-icon
.path=${item.iconPath || mdiPuzzle}
></ha-svg-icon>
</ha-app-icon>
</ha-backup-formfield-label>
</ha-checkbox>
`
@@ -86,6 +92,10 @@ export class HaBackupAddonsPicker extends LitElement {
padding-inline-start: var(--ha-space-2);
padding-bottom: var(--ha-space-3);
}
ha-app-icon {
--ha-app-icon-size: var(--ha-space-6);
}
`;
}
@@ -15,20 +15,25 @@ class SupervisorFormfieldLabel extends LitElement {
protected render(): TemplateResult {
return html`
${
this.imageUrl
? html`<img
loading="lazy"
alt=""
src=${this.imageUrl}
class="icon"
/>`
: this.iconPath
? html`
<ha-svg-icon .path=${this.iconPath} class="icon"></ha-svg-icon>
`
: nothing
}
<slot name="icon">
${
this.imageUrl
? html`<img
loading="lazy"
alt=""
src=${this.imageUrl}
class="icon"
/>`
: this.iconPath
? html`
<ha-svg-icon
.path=${this.iconPath}
class="icon"
></ha-svg-icon>
`
: nothing
}
</slot>
<span class="label">
${this.label}
${
@@ -17,6 +17,7 @@ import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
import { caseInsensitiveStringCompare } from "../../../../../common/string/compare";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-app-icon";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-icon-next";
@@ -204,23 +205,12 @@ export class SerialConfigDashboard extends LitElement {
});
}
private _renderConsumerIcon(src: string, alt: string): TemplateResult {
return html`<img
slot="start"
.src=${src}
crossorigin="anonymous"
referrerpolicy="no-referrer"
alt=${alt}
/>`;
}
// The panel the integration behind this consumer is configured in, if it has
// one. A stopped consumer has no panel loaded to send the user to.
private _consumerPanel(consumer: SerialPortConsumer): string | undefined {
if (!consumer.active) {
return undefined;
}
const domain =
consumer.kind === "config_entry"
? consumer.domain
@@ -253,22 +243,28 @@ export class SerialConfigDashboard extends LitElement {
<ha-md-list-item type="link" href=${href} class="consumer">
${
consumer.kind === "config_entry"
? this._renderConsumerIcon(
brandsUrl(
? html`<img
slot="start"
.src=${brandsUrl(
{
domain: consumer.domain!,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
),
consumer.domain!
)
)}
crossorigin="anonymous"
referrerpolicy="no-referrer"
alt=${consumer.domain!}
/>`
: consumer.kind === "app"
? this._renderConsumerIcon(
`/api/hassio/addons/${consumer.slug}/icon`,
consumer.slug!
)
? html`<ha-app-icon
slot="start"
.slug=${consumer.slug!}
.alt=${consumer.title || consumer.slug!}
>
<ha-svg-icon .path=${mdiPuzzle}></ha-svg-icon>
</ha-app-icon>`
: html`<ha-svg-icon
slot="start"
.path=${mdiPuzzle}
@@ -673,7 +669,8 @@ export class SerialConfigDashboard extends LitElement {
--md-list-item-leading-space: var(--ha-space-14);
}
ha-md-list-item.consumer img[slot="start"] {
ha-md-list-item.consumer img[slot="start"],
ha-md-list-item.consumer ha-app-icon[slot="start"] {
width: 24px;
height: 24px;
}
+39 -33
View File
@@ -17,6 +17,7 @@ import type { HASSDomTargetEvent } from "../../../common/dom/fire_event";
import { navigate } from "../../../common/navigate";
import { stringCompare } from "../../../common/string/compare";
import { extractSearchParam } from "../../../common/url/search-params";
import "../../../components/ha-app-icon";
import "../../../components/ha-button";
import "../../../components/ha-generic-picker";
import type { HaGenericPicker } from "../../../components/ha-generic-picker";
@@ -24,7 +25,10 @@ import type { PickerComboBoxItem } from "../../../components/ha-picker-combo-box
import "../../../components/input/ha-input-search";
import type { HaInputSearch } from "../../../components/input/ha-input-search";
import type { LogProvider } from "../../../data/error_log";
import { fetchHassioAddonsInfo } from "../../../data/hassio/addon";
import {
fetchHassioAddonsInfo,
type HassioAddonInfo,
} from "../../../data/hassio/addon";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-subpage";
import { mdiHomeAssistant } from "../../../resources/home-assistant-logo-svg";
@@ -61,6 +65,11 @@ const logProviders: LogProvider[] = [
},
];
interface LogProviderPickerItem extends PickerComboBoxItem {
addon?: HassioAddonInfo;
hasAppIcon?: boolean;
}
@customElement("ha-config-logs")
export class HaConfigLogs extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -138,18 +147,9 @@ export class HaConfigLogs extends LitElement {
@click=${this._openPicker}
>
${
selectedProvider?.icon
? html`<img
src=${selectedProvider.icon}
alt=${selectedProvider.primary}
slot="start"
/>`
: selectedProvider?.icon_path
? html`<ha-svg-icon
slot="start"
.path=${selectedProvider.icon_path}
></ha-svg-icon>`
: nothing
selectedProvider
? this._renderProviderIcon(selectedProvider)
: nothing
}
${selectedProvider?.primary}
<ha-svg-icon
@@ -264,33 +264,40 @@ export class HaConfigLogs extends LitElement {
}
}
private _getLogProviderItems = (): PickerComboBoxItem[] =>
private _getLogProviderItems = (): LogProviderPickerItem[] =>
this._logProviders.map((provider) => ({
id: provider.key,
primary: provider.name,
icon: provider.addon
addon: provider.addon,
hasAppIcon: provider.addon
? atLeastVersion(this.hass.config.version, 0, 105) &&
provider.addon.icon
? `/api/hassio/addons/${provider.addon.slug}/icon`
: undefined
: undefined,
icon_path: provider.addon
? mdiPuzzle
: this._getProviderIconPath(provider.key),
}));
private _providerRenderer = (item: PickerComboBoxItem) => html`
private _renderProviderIcon(item: LogProviderPickerItem) {
if (item.addon) {
return html`<ha-app-icon
slot="start"
.alt=${item.primary}
.hasIcon=${item.hasAppIcon}
.slug=${item.addon.slug}
>
<ha-svg-icon .path=${item.icon_path}></ha-svg-icon>
</ha-app-icon>`;
}
return item.icon_path
? html`<ha-svg-icon slot="start" .path=${item.icon_path}></ha-svg-icon>`
: nothing;
}
private _providerRenderer = (item: LogProviderPickerItem) => html`
<ha-combo-box-item type="button" compact>
${
item.icon
? html`<img src=${item.icon} alt=${item.primary} slot="start" />`
: item.icon_path
? html`<ha-svg-icon
slot="start"
.path=${item.icon_path}
></ha-svg-icon>`
: nothing
}
${this._renderProviderIcon(item)}
<span slot="headline">${item.primary}</span>
${
item.secondary
@@ -308,11 +315,10 @@ export class HaConfigLogs extends LitElement {
return {
id: provider.key,
primary: provider.name,
icon: provider.addon
addon: provider.addon,
hasAppIcon: provider.addon
? atLeastVersion(this.hass.config.version, 0, 105) &&
provider.addon.icon
? `/api/hassio/addons/${provider.addon.slug}/icon`
: undefined
: undefined,
icon_path: provider.addon
? mdiPuzzle
@@ -365,8 +371,8 @@ export class HaConfigLogs extends LitElement {
--mdc-icon-size: var(--ha-space-6);
}
img {
height: 32px;
ha-app-icon {
--ha-app-icon-size: 32px;
}
@media all and (max-width: 870px) {
+17 -8
View File
@@ -221,7 +221,19 @@ export class HaSceneEditor extends DirtyStateProviderMixin<number>()(
public disconnectedCallback() {
super.disconnectedCallback();
if (this._unsubscribeEvents) {
// Hidden-tab panel suspend detaches an ancestor and later reattaches
// this same instance. Direct removal (route change) has no parentNode,
// so we still tear down even if the tab is hidden — e.g. saving a new
// scene remounts the itemId editor.
// Leave the live subscription in place on suspend so the websocket
// library can restore it after reconnect; a fresh subscribe here races
// the closed socket.
if (document.hidden && this.parentNode) {
return;
}
if (this._mode === "live" && this.hass) {
this._exitLiveMode();
} else if (this._unsubscribeEvents) {
this._unsubscribeEvents();
this._unsubscribeEvents = undefined;
}
@@ -911,11 +923,14 @@ export class HaSceneEditor extends DirtyStateProviderMixin<number>()(
}
private async _subscribeEvents() {
if (this._unsubscribeEvents) {
return;
}
const unsubscribe = await this.hass!.connection.subscribeEvents<HassEvent>(
(event) => this._stateChanged(event),
"state_changed"
);
if (!this.isConnected || this._mode !== "live") {
if (!this.isConnected || this._mode !== "live" || this._unsubscribeEvents) {
unsubscribe();
return;
}
@@ -1092,9 +1107,6 @@ export class HaSceneEditor extends DirtyStateProviderMixin<number>()(
};
private _goBack(): void {
if (this._mode === "live") {
applyScene(this.hass, this._storedStates);
}
afterNextRender(() => goBack("/config/scene/dashboard"));
}
@@ -1119,9 +1131,6 @@ export class HaSceneEditor extends DirtyStateProviderMixin<number>()(
return;
}
await deleteScene(this.hass, this.sceneId);
if (this._mode === "live") {
applyScene(this.hass, this._storedStates);
}
goBack("/config/scene/dashboard");
}
+2
View File
@@ -455,6 +455,8 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
}
private async _editHomeZone(ev) {
// Keep the click from selecting the list item, which zooms the map
ev.stopPropagation();
if (ev.currentTarget.noEdit) {
showAlertDialog(this, {
title: this.hass.localize("ui.panel.config.zone.can_not_edit"),
+10 -13
View File
@@ -41,6 +41,7 @@ import type { HaDropdownSelectEvent } from "../../components/ha-dropdown";
import "../../components/ha-dropdown-item";
import "../../components/ha-empty-state";
import "../../components/ha-filter-pane";
import type { HaFilterPane } from "../../components/ha-filter-pane";
import "../../components/ha-filter-pane-chip";
import "../../components/ha-icon-button";
import type { SourceFilters } from "../../components/ha-sources-picker";
@@ -108,6 +109,8 @@ class HaPanelHistory extends LitElement {
@state() private _showSources?: boolean;
@query("ha-filter-pane") private _filterPane?: HaFilterPane;
@state() private _entitySources?: EntitySources;
@state() private _stateHistory?: HistoryResult;
@@ -294,19 +297,9 @@ class HaPanelHistory extends LitElement {
: "ui.panel.history.start_search"
)}
>
${
!this._sourcesShown()
? html`
<ha-button appearance="plain" @click=${this._openSources}>
${this.hass.localize(
hasTargets
? "ui.panel.history.change_sources"
: "ui.panel.history.add_targets"
)}
</ha-button>
`
: nothing
}
<ha-button appearance="plain" @click=${this._openSources}>
${this.hass.localize("ui.panel.history.change_sources")}
</ha-button>
</ha-empty-state>
`;
}
@@ -397,6 +390,10 @@ class HaPanelHistory extends LitElement {
}
private _openSources() {
if (this._sourcesShown()) {
this._filterPane?.highlight();
return;
}
this._showSources = true;
}
+8 -1
View File
@@ -9,7 +9,7 @@ import {
import type { HassServiceTarget } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fromUnixTime } from "date-fns";
import { ensureArray } from "../../common/array/ensure-array";
@@ -37,6 +37,7 @@ import "../../components/ha-dropdown-item";
import "../../components/ha-empty-state";
import "../../components/ha-filter-pane-chip";
import "../../components/ha-filter-pane";
import type { HaFilterPane } from "../../components/ha-filter-pane";
import "../../components/ha-icon-button";
import {
applySourceFilters,
@@ -81,6 +82,8 @@ export class HaPanelLogbook extends LitElement {
@state() private _showSources?: boolean;
@query("ha-filter-pane") private _filterPane?: HaFilterPane;
@state() private _entitySources?: EntitySources;
@state() private _targetPickerValue: HassServiceTarget = {};
@@ -244,6 +247,10 @@ export class HaPanelLogbook extends LitElement {
}
private _openSources() {
if (this._sourcesShown()) {
this._filterPane?.highlight();
return;
}
this._showSources = true;
}
+11 -1
View File
@@ -665,14 +665,17 @@
"options": {
"each": {
"label": "Each",
"row_label": "each of",
"description": "Triggers once for every target that changes"
},
"first": {
"label": "First",
"row_label": "first of",
"description": "Triggers the first time any target changes"
},
"all": {
"label": "All",
"row_label": "all of",
"description": "Triggers once all targets have changed"
}
}
@@ -681,10 +684,12 @@
"options": {
"any": {
"label": "Any",
"row_label": "any of",
"description": "Is true if any target matches the desired state"
},
"all": {
"label": "All",
"row_label": "all of",
"description": "Is true if all targets match the desired state"
}
}
@@ -5252,6 +5257,12 @@
"placeholder": "Optional description",
"add": "Add description"
},
"row_options": {
"for": "for {duration}",
"offset_before": "{offset} before",
"offset_after": "{offset} after",
"timeout": "timeout {duration}"
},
"note": {
"label": "Note",
"edit": "Edit note",
@@ -11940,7 +11951,6 @@
"history": {
"sources": "Sources",
"sources_count": "Sources: {count}",
"add_targets": "Add targets",
"change_sources": "Change sources",
"no_targets": "No targets selected",
"start_search_title": "Nothing to show yet",
+2 -1
View File
@@ -1,3 +1,4 @@
import { resolveHassUrl } from "../common/url/hass-url";
import { isExternalAndroid } from "../data/external";
// 10 seconds gives the Android WebView download listener enough time
@@ -9,7 +10,7 @@ const BLOB_REVOKE_DELAY_MS = 10_000;
export const fileDownload = (href: string, filename = ""): void => {
const element = document.createElement("a");
element.target = "_blank";
element.href = href;
element.href = resolveHassUrl(href);
element.download = filename;
element.style.display = "none";
document.body.appendChild(element);
+17 -2
View File
@@ -1,9 +1,10 @@
import "@formatjs/intl-durationformat/polyfill-force";
import { expect, describe, it } from "vitest";
import { describe, expect, it } from "vitest";
import {
formatDuration,
formatDurationLong,
formatDurationDigital,
formatDurationLong,
formatDurationNarrow,
formatNumericDuration,
} from "../../../src/common/datetime/format_duration";
import type { FrontendLocaleData } from "../../../src/data/translation";
@@ -63,6 +64,20 @@ describe("formatDurationLong", () => {
});
});
describe("formatDurationNarrow", () => {
it("formats narrow duration", () => {
const duration = { days: 1, hours: 2, minutes: 3, seconds: 4 };
expect(formatDurationNarrow(LOCALE, duration)).toBe("1d 2h 3m 4s");
});
it("only formats units that are present", () => {
expect(formatDurationNarrow(LOCALE, { hours: 1, minutes: 30 })).toBe(
"1h 30m"
);
expect(formatDurationNarrow(LOCALE, { seconds: 45 })).toBe("45s");
});
});
describe("formatDurationDigital", () => {
it("formats digital duration", () => {
const duration = { hours: 1, minutes: 2, seconds: 3 };
+104
View File
@@ -0,0 +1,104 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import type { HaAppIcon } from "../../src/components/ha-app-icon";
import "../../src/components/ha-app-icon";
let appIcon: HaAppIcon | undefined;
const mountAppIcon = async (properties: Partial<HaAppIcon> = {}) => {
appIcon = document.createElement("ha-app-icon");
Object.assign(appIcon, properties);
appIcon.append(document.createElement("ha-svg-icon"));
document.body.append(appIcon);
await appIcon.updateComplete;
return appIcon;
};
afterEach(() => {
appIcon?.remove();
appIcon = undefined;
vi.unstubAllGlobals();
});
describe("ha-app-icon", () => {
it("does not request an unavailable icon", async () => {
const element = await mountAppIcon({ slug: "example", hasIcon: false });
expect(element.shadowRoot!.querySelector("img")).toBeNull();
expect(element.shadowRoot!.querySelector("slot")).not.toBeNull();
});
it.each([true, undefined])(
"requests the icon when availability is %s",
async (hasIcon) => {
const element = await mountAppIcon({ slug: "example", hasIcon });
expect(
element.shadowRoot!.querySelector("img")!.getAttribute("src")
).toBe("/api/hassio/addons/example/icon");
}
);
it("requests the icon from the configured Home Assistant URL", async () => {
vi.stubGlobal("__HASS_URL__", "http://homeassistant.local:8123");
const element = await mountAppIcon({ slug: "example", hasIcon: true });
const image = element.shadowRoot!.querySelector("img")!;
expect(image.getAttribute("src")).toBe(
"http://homeassistant.local:8123/api/hassio/addons/example/icon"
);
expect(image.hasAttribute("crossorigin")).toBe(false);
});
it("renders the fallback after an image error", async () => {
const element = await mountAppIcon({ slug: "example", hasIcon: true });
element.shadowRoot!.querySelector("img")!.dispatchEvent(new Event("error"));
await element.updateComplete;
expect(element.shadowRoot!.querySelector("img")).toBeNull();
expect(element.shadowRoot!.querySelector("slot")).not.toBeNull();
});
it("tries a new source after the slug changes", async () => {
const element = await mountAppIcon({ slug: "first", hasIcon: true });
element.shadowRoot!.querySelector("img")!.dispatchEvent(new Event("error"));
await element.updateComplete;
element.slug = "second";
await element.updateComplete;
expect(element.shadowRoot!.querySelector("img")!.getAttribute("src")).toBe(
"/api/hassio/addons/second/icon"
);
});
it("ignores an error from a previous source", async () => {
const element = await mountAppIcon({ slug: "first", hasIcon: true });
const firstImage = element.shadowRoot!.querySelector("img")!;
element.slug = "second";
await element.updateComplete;
firstImage.dispatchEvent(new Event("error"));
await element.updateComplete;
expect(element.shadowRoot!.querySelector("img")!.getAttribute("src")).toBe(
"/api/hassio/addons/second/icon"
);
});
it("ignores an old error after returning to the same source", async () => {
const element = await mountAppIcon({ slug: "first", hasIcon: true });
const firstImage = element.shadowRoot!.querySelector("img")!;
element.slug = "second";
await element.updateComplete;
element.slug = "first";
await element.updateComplete;
firstImage.dispatchEvent(new Event("error"));
await element.updateComplete;
expect(element.shadowRoot!.querySelector("img")!.getAttribute("src")).toBe(
"/api/hassio/addons/first/icon"
);
});
});
+11
View File
@@ -36,6 +36,7 @@ describe("fileDownload", () => {
afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
delete (window as any).externalApp;
delete (window as any).externalAppV2;
});
@@ -58,6 +59,16 @@ describe("fileDownload", () => {
expect(URL.revokeObjectURL).not.toHaveBeenCalled();
});
it("resolves relative URLs against the configured Home Assistant URL", async () => {
vi.stubGlobal("__HASS_URL__", "http://homeassistant.local:8123");
await loadFileDownload();
fileDownload("/api/hassio/core/logs/follow");
expect(createdElement.href).toBe(
"http://homeassistant.local:8123/api/hassio/core/logs/follow"
);
});
it("revokes blob URLs immediately outside Android", async () => {
await loadFileDownload();
fileDownload("blob:http://localhost/abc-123", "file.json");
+150 -70
View File
@@ -2294,6 +2294,28 @@ __metadata:
languageName: node
linkType: hard
"@cacheable/memory@npm:^2.2.0":
version: 2.2.0
resolution: "@cacheable/memory@npm:2.2.0"
dependencies:
"@cacheable/utils": "npm:^2.5.0"
"@keyv/bigmap": "npm:^1.3.1"
hookified: "npm:^1.15.1"
keyv: "npm:^5.6.0"
checksum: 10/97db83d7a74979accca9de56cf278e42c22b4273e752d7e23c44fbec4a47450716c4920903424fd19184a7bee6325064b6c453afc6f96bf92b39eecfe0b40648
languageName: node
linkType: hard
"@cacheable/utils@npm:^2.5.0":
version: 2.5.0
resolution: "@cacheable/utils@npm:2.5.0"
dependencies:
hashery: "npm:^1.5.1"
keyv: "npm:^5.6.0"
checksum: 10/a77a7e682926b984cb28b6993f4595638ce8799de3223dbdcdb796a077a02de763687bdc5669779019aba44d27693260ef92d860a19330d7eaa4dfcca3433c3b
languageName: node
linkType: hard
"@codemirror/autocomplete@npm:6.20.3, @codemirror/autocomplete@npm:^6.0.0":
version: 6.20.3
resolution: "@codemirror/autocomplete@npm:6.20.3"
@@ -2430,12 +2452,12 @@ __metadata:
languageName: node
linkType: hard
"@codemirror/state@npm:6.7.2, @codemirror/state@npm:^6.0.0, @codemirror/state@npm:^6.7.0":
version: 6.7.2
resolution: "@codemirror/state@npm:6.7.2"
"@codemirror/state@npm:6.7.4, @codemirror/state@npm:^6.0.0, @codemirror/state@npm:^6.7.0":
version: 6.7.4
resolution: "@codemirror/state@npm:6.7.4"
dependencies:
"@marijn/find-cluster-break": "npm:^1.0.0"
checksum: 10/363a6217e6d34ce406abca93ae7b87181e93e308be5ee037bec911b9d8c7bf6559de7e85620460bc1b7c1a590c05d75bc64e3b09d76fd82110ab829237e3fde2
checksum: 10/429b7ff6b617d3934eea001165228c3ed77df1943c0b2fdaf019ef3a6b616ada482c344ec46edf5c83121afe6973e9ce4931446922cc2b35f1dad2525eff5300
languageName: node
linkType: hard
@@ -2682,13 +2704,13 @@ __metadata:
languageName: node
linkType: hard
"@eslint/plugin-kit@npm:^0.7.2":
version: 0.7.2
resolution: "@eslint/plugin-kit@npm:0.7.2"
"@eslint/plugin-kit@npm:^0.7.3":
version: 0.7.3
resolution: "@eslint/plugin-kit@npm:0.7.3"
dependencies:
"@eslint/core": "npm:^1.2.1"
levn: "npm:^0.4.1"
checksum: 10/ef9fc6f8ca28e132d4c81cfbaa92274800d1d73bb9d6ef2124613dd39b7f09e3592deb64bad10b183bff78db5465d4b100f522d994c8550424526b9ac4a072b0
checksum: 10/bd603f0438d6829a5b0c8cbb90383e3b0fda3aac3a87faa22c80124ce4121f5dcfe8ef9b933c3308cbed70a27c4f51036403e3f0cb64ca2743e595dd865ccef6
languageName: node
linkType: hard
@@ -3347,6 +3369,25 @@ __metadata:
languageName: node
linkType: hard
"@keyv/bigmap@npm:^1.3.1":
version: 1.3.1
resolution: "@keyv/bigmap@npm:1.3.1"
dependencies:
hashery: "npm:^1.4.0"
hookified: "npm:^1.15.0"
peerDependencies:
keyv: ^5.6.0
checksum: 10/9745786a94729117460b5399a964ec151e94a89db0e5ed573419c4c620f0595f05d7ad87d596bb7e212a4f3ad2335271694fddcfaa8400013f7b64e11669a884
languageName: node
linkType: hard
"@keyv/serialize@npm:^1.1.1":
version: 1.1.1
resolution: "@keyv/serialize@npm:1.1.1"
checksum: 10/e3b2cb1377863342acedd5ff785af3e69269bee9b44707c617d1c8bc14eeb5ac763159d6455903ffe92f143c2238e1e783c4f113f9c8910eacccf172894472da
languageName: node
linkType: hard
"@lezer/common@npm:^1.0.0, @lezer/common@npm:^1.0.2, @lezer/common@npm:^1.1.0, @lezer/common@npm:^1.2.0, @lezer/common@npm:^1.3.0, @lezer/common@npm:^1.5.0":
version: 1.5.2
resolution: "@lezer/common@npm:1.5.2"
@@ -7528,12 +7569,12 @@ __metadata:
languageName: node
linkType: hard
"baseline-browser-mapping@npm:^2.11.12":
version: 2.11.14
resolution: "baseline-browser-mapping@npm:2.11.14"
"baseline-browser-mapping@npm:^2.11.20":
version: 2.11.21
resolution: "baseline-browser-mapping@npm:2.11.21"
bin:
baseline-browser-mapping: dist/cli.cjs
checksum: 10/b1745ec32246d3d1182bfeab2c2efeaf29925e6f617da59f28fde032fce6fa4eaa4632bcf2f9edeca83ceeb7cdfd72d6361ebd7c10e1d4fcb117259ec7059165
checksum: 10/b4d223df8dbb8deeaa3dae3b9b60ad48b24c57a5473827b5a6ee1c21917e7c37af2185224c5366f2932654ec5fb1a9fee4f95fe7213cc29f4347550f25e4234b
languageName: node
linkType: hard
@@ -7718,18 +7759,18 @@ __metadata:
languageName: node
linkType: hard
"browserslist@npm:4.28.8, browserslist@npm:^4.24.0, browserslist@npm:^4.28.2, browserslist@npm:^4.28.7":
version: 4.28.8
resolution: "browserslist@npm:4.28.8"
"browserslist@npm:4.28.9, browserslist@npm:^4.24.0, browserslist@npm:^4.28.2, browserslist@npm:^4.28.7":
version: 4.28.9
resolution: "browserslist@npm:4.28.9"
dependencies:
baseline-browser-mapping: "npm:^2.11.12"
caniuse-lite: "npm:^1.0.30001809"
electron-to-chromium: "npm:^1.5.402"
node-releases: "npm:^2.0.53"
update-browserslist-db: "npm:^1.3.0"
baseline-browser-mapping: "npm:^2.11.20"
caniuse-lite: "npm:^1.0.30001810"
electron-to-chromium: "npm:^1.5.420"
node-releases: "npm:^2.0.54"
update-browserslist-db: "npm:^1.3.2"
bin:
browserslist: cli.js
checksum: 10/9df1d87f915639ebe8f85caf8d58bc0627863dff5e921b6e0394a92f82d279f1b2b324594267d025c80482397150cb4bf5a3ab55fde5d2c1c0f13c064f48b5e9
checksum: 10/493e5e3c11d4bdd34ce8cb7ed5edc98bc5948457e6a3cc1268b253d0ff9a616033b10980515e0e6faeddae745fb9c330cd4510189d681ad86f9ce0b2da3c840e
languageName: node
linkType: hard
@@ -7810,6 +7851,19 @@ __metadata:
languageName: node
linkType: hard
"cacheable@npm:^2.5.0":
version: 2.5.0
resolution: "cacheable@npm:2.5.0"
dependencies:
"@cacheable/memory": "npm:^2.2.0"
"@cacheable/utils": "npm:^2.5.0"
hookified: "npm:^1.15.0"
keyv: "npm:^5.6.0"
qified: "npm:^0.10.1"
checksum: 10/216a896e17dfa9c598a69e2fff31216126e94286f6c0ceb1bfab1d34e6c1a8791e8dc34dbd28660ec72a84f1d370c3733a9fc481e00a5512fd4e0cfc35775db6
languageName: node
linkType: hard
"call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2":
version: 1.0.2
resolution: "call-bind-apply-helpers@npm:1.0.2"
@@ -7875,10 +7929,10 @@ __metadata:
languageName: node
linkType: hard
"caniuse-lite@npm:^1.0.30001809":
version: 1.0.30001809
resolution: "caniuse-lite@npm:1.0.30001809"
checksum: 10/1034b9673e2393bf0c12f5188e0957817d423f71909809eafd69e511c91dedd880d36dd7ee1041605d73fcd3f89a42ba4e235eb942e6bb9f23acdca2fc98f927
"caniuse-lite@npm:^1.0.30001810":
version: 1.0.30001810
resolution: "caniuse-lite@npm:1.0.30001810"
checksum: 10/47d7db627c3f3d1a10e06eef9efbb84295c6ef9d959b585b64032cc293637ca7b9b6af7e5d5752972b3e690d9561f14b9561db03890fbc6e38dc1aaa54e51d97
languageName: node
linkType: hard
@@ -8844,10 +8898,10 @@ __metadata:
languageName: node
linkType: hard
"electron-to-chromium@npm:^1.5.402":
version: 1.5.407
resolution: "electron-to-chromium@npm:1.5.407"
checksum: 10/9c4507ef13786d3e4fe16d04929b662b500ac814b2f09f8bda94858ae0bc9962f9a26bfb00bf519b94724eb97bcdc6409cf40cf1b9b540c3443d8f2850fa066a
"electron-to-chromium@npm:^1.5.420":
version: 1.5.422
resolution: "electron-to-chromium@npm:1.5.422"
checksum: 10/a4cc1bd7204bdb3da063f1ad8103f9990e03da3d0dae0aaf75e1669f2c36a9ae807498c5b27eed61fa98d6b10baf7fe05ff6038a99a4a85a0285e623825253d6
languageName: node
linkType: hard
@@ -9331,16 +9385,16 @@ __metadata:
languageName: node
linkType: hard
"eslint@npm:10.9.1":
version: 10.9.1
resolution: "eslint@npm:10.9.1"
"eslint@npm:10.10.0":
version: 10.10.0
resolution: "eslint@npm:10.10.0"
dependencies:
"@eslint-community/eslint-utils": "npm:^4.8.0"
"@eslint-community/regexpp": "npm:^4.12.2"
"@eslint/config-array": "npm:^0.23.5"
"@eslint/config-helpers": "npm:^0.7.0"
"@eslint/core": "npm:^1.2.1"
"@eslint/plugin-kit": "npm:^0.7.2"
"@eslint/plugin-kit": "npm:^0.7.3"
"@humanfs/node": "npm:^0.16.6"
"@humanwhocodes/module-importer": "npm:^1.0.1"
"@humanwhocodes/retry": "npm:^0.4.2"
@@ -9355,7 +9409,7 @@ __metadata:
esquery: "npm:^1.7.0"
esutils: "npm:^2.0.2"
fast-deep-equal: "npm:^3.1.3"
file-entry-cache: "npm:^8.0.0"
file-entry-cache: "npm:11.1.5 || >11.1.6 <12"
find-up: "npm:^5.0.0"
glob-parent: "npm:^6.0.2"
ignore: "npm:^5.2.0"
@@ -9372,7 +9426,7 @@ __metadata:
optional: true
bin:
eslint: bin/eslint.js
checksum: 10/4cee237c7d2c445a05330e595b18521d7452fd3f419241bf1ccf81c769a6761c10ca5a06988197482d4842ed1a26b1dcb51b9526408990cee84559e6c08cb3ab
checksum: 10/02fe7e79bf9f81d939ef3ad0c3151c0588f3441eef00bdc468797f2a9f8b047bea97c57a793fc97cf1e4886400b50f049b8e66ec1371820e9df33b54a8bc90d8
languageName: node
linkType: hard
@@ -9635,12 +9689,12 @@ __metadata:
languageName: node
linkType: hard
"file-entry-cache@npm:^8.0.0":
version: 8.0.0
resolution: "file-entry-cache@npm:8.0.0"
"file-entry-cache@npm:11.1.5 || >11.1.6 <12":
version: 11.1.5
resolution: "file-entry-cache@npm:11.1.5"
dependencies:
flat-cache: "npm:^4.0.0"
checksum: 10/afe55c4de4e0d226a23c1eae62a7219aafb390859122608a89fa4df6addf55c7fd3f1a2da6f5b41e7cdff496e4cf28bbd215d53eab5c817afa96d2b40c81bfb0
flat-cache: "npm:^6.1.23"
checksum: 10/a4a6c45c9561fbceb6511b3483e299e636e5c67e225ea4445c8cbdbe6c571da8505b29b29fc4c62fe9cf28ffe33314f401fad3da09e4b62321c92f0d3a113135
languageName: node
linkType: hard
@@ -9739,17 +9793,18 @@ __metadata:
languageName: node
linkType: hard
"flat-cache@npm:^4.0.0":
version: 4.0.1
resolution: "flat-cache@npm:4.0.1"
"flat-cache@npm:^6.1.23":
version: 6.1.23
resolution: "flat-cache@npm:6.1.23"
dependencies:
flatted: "npm:^3.2.9"
keyv: "npm:^4.5.4"
checksum: 10/58ce851d9045fffc7871ce2bd718bc485ad7e777bf748c054904b87c351ff1080c2c11da00788d78738bfb51b71e4d5ea12d13b98eb36e3358851ffe495b62dc
cacheable: "npm:^2.5.0"
flatted: "npm:^3.4.2"
hookified: "npm:^1.15.0"
checksum: 10/63ad3682f399e3e7b7255493b07469f80c418034a10e12e1c7043b41d666ff69b20be924984a0b91cd767a4f71540e4fb4421651d15adc736fca1c04b8505758
languageName: node
linkType: hard
"flatted@npm:^3.2.9":
"flatted@npm:^3.4.2":
version: 3.4.4
resolution: "flatted@npm:3.4.4"
checksum: 10/5a6682ab87ed15e125419c61352c4bef26f310adceece03eae5f22c119d64ed47bfc0ee1948a77d0122559eaa86395ea06ef45b59bfe3db4fb13fb4cdff876de
@@ -10315,6 +10370,15 @@ __metadata:
languageName: node
linkType: hard
"hashery@npm:^1.4.0, hashery@npm:^1.5.1":
version: 1.5.1
resolution: "hashery@npm:1.5.1"
dependencies:
hookified: "npm:^1.15.0"
checksum: 10/34eebad3e4d4041ae5c2457f129169b7be8d067fce4717ec6b903017e82b420263808a4d89f15cc77fa21822ae36d16841dc06d945392b6833b49082d3653e82
languageName: node
linkType: hard
"hasown@npm:^2.0.2, hasown@npm:^2.0.3, hasown@npm:^2.0.4":
version: 2.0.4
resolution: "hasown@npm:2.0.4"
@@ -10350,7 +10414,7 @@ __metadata:
"@codemirror/language": "npm:6.12.4"
"@codemirror/lint": "npm:6.9.7"
"@codemirror/search": "npm:6.7.2"
"@codemirror/state": "npm:6.7.2"
"@codemirror/state": "npm:6.7.4"
"@codemirror/view": "npm:6.43.11"
"@date-fns/tz": "npm:1.5.0"
"@egjs/hammerjs": "npm:2.0.17"
@@ -10423,7 +10487,7 @@ __metadata:
babel-loader: "npm:10.1.1"
babel-plugin-polyfill-corejs3: "npm:1.0.0"
barcode-detector: "npm:3.2.2"
browserslist: "npm:4.28.8"
browserslist: "npm:4.28.9"
browserslist-useragent-regexp: "npm:4.1.4"
cally: "npm:0.9.2"
color-name: "npm:2.1.1"
@@ -10438,7 +10502,7 @@ __metadata:
echarts: "npm:6.1.0"
echarts-extension-chart2music: "npm:0.1.1"
element-internals-polyfill: "npm:3.0.2"
eslint: "npm:10.9.1"
eslint: "npm:10.10.0"
eslint-config-prettier: "npm:10.1.8"
eslint-import-resolver-webpack: "npm:0.13.11"
eslint-plugin-import-x: "npm:4.17.1"
@@ -10534,6 +10598,20 @@ __metadata:
languageName: node
linkType: hard
"hookified@npm:^1.15.0, hookified@npm:^1.15.1":
version: 1.15.1
resolution: "hookified@npm:1.15.1"
checksum: 10/ecaee63506d9a213e8b78dfdb92346227f951d3bcd8ef60d14105c5590f61d466f130a908274e1ec87ee3b3668bd87d9250cad64547cf15cfa69932bcdbbea8f
languageName: node
linkType: hard
"hookified@npm:^2.1.1":
version: 2.2.0
resolution: "hookified@npm:2.2.0"
checksum: 10/36be80a9645161c9747c90a0a26f75f074470984cf221fb73cf0e2f367667e0d4680797f7b4f27b83d1f4cd4a3b5913f685c91132b5d6eeb05522674081657a8
languageName: node
linkType: hard
"hosted-git-info@npm:^10.1.0, hosted-git-info@npm:^10.1.1":
version: 10.1.1
resolution: "hosted-git-info@npm:10.1.1"
@@ -11504,13 +11582,6 @@ __metadata:
languageName: node
linkType: hard
"json-buffer@npm:3.0.1":
version: 3.0.1
resolution: "json-buffer@npm:3.0.1"
checksum: 10/82876154521b7b68ba71c4f969b91572d1beabadd87bd3a6b236f85fbc7dc4695089191ed60bb59f9340993c51b33d479f45b6ba9f3548beb519705281c32c3c
languageName: node
linkType: hard
"json-parse-even-better-errors@npm:^5.0.0":
version: 5.0.0
resolution: "json-parse-even-better-errors@npm:5.0.0"
@@ -11643,12 +11714,12 @@ __metadata:
languageName: node
linkType: hard
"keyv@npm:^4.5.4":
version: 4.5.4
resolution: "keyv@npm:4.5.4"
"keyv@npm:^5.6.0":
version: 5.6.0
resolution: "keyv@npm:5.6.0"
dependencies:
json-buffer: "npm:3.0.1"
checksum: 10/167eb6ef64cc84b6fa0780ee50c9de456b422a1e18802209234f7c2cf7eae648c7741f32e50d7e24ccb22b24c13154070b01563d642755b156c357431a191e75
"@keyv/serialize": "npm:^1.1.1"
checksum: 10/f1de999fdf635d703d091981a0a3844fb20081e01d45c6743ada7d01f3a6570dc47d1882e10d0a98d31075b342db8e62b4ebe4f5bf473dcd0903064d718709c1
languageName: node
linkType: hard
@@ -12648,10 +12719,10 @@ __metadata:
languageName: node
linkType: hard
"node-releases@npm:^2.0.53":
version: 2.0.53
resolution: "node-releases@npm:2.0.53"
checksum: 10/24cb89ef08ec15b53fe50dad852dd0444e30abae425c6b9c457dcd3892405cbe8cf7b041a6eeefb4d6be8ce461fe03778501b25916bcc1539a6bc7debc1954e5
"node-releases@npm:^2.0.54":
version: 2.0.54
resolution: "node-releases@npm:2.0.54"
checksum: 10/36380abbe4ce6f5bfec7dab13fa8174e67d2c316a9cbedcd01196bd37dbec99ca7214caf7a91f05d27ae17657d5cb55f39c53db216a7992fd9a2e29966ebd2ba
languageName: node
linkType: hard
@@ -13650,6 +13721,15 @@ __metadata:
languageName: node
linkType: hard
"qified@npm:^0.10.1":
version: 0.10.1
resolution: "qified@npm:0.10.1"
dependencies:
hookified: "npm:^2.1.1"
checksum: 10/65eda075defad24a3c53a0bcb0056fdaffb8355ca5dbe33bffaf3143e767dd1f0ddb403a4d9402e3bd0bc54b8efdec934055fb13eb5b6e7070835a80bf517f79
languageName: node
linkType: hard
"qr-creator@npm:^1.0.0":
version: 1.0.0
resolution: "qr-creator@npm:1.0.0"
@@ -15944,9 +16024,9 @@ __metadata:
languageName: node
linkType: hard
"update-browserslist-db@npm:^1.3.0":
version: 1.3.1
resolution: "update-browserslist-db@npm:1.3.1"
"update-browserslist-db@npm:^1.3.2":
version: 1.3.2
resolution: "update-browserslist-db@npm:1.3.2"
dependencies:
escalade: "npm:^3.2.0"
picocolors: "npm:^1.1.1"
@@ -15954,7 +16034,7 @@ __metadata:
browserslist: ">= 4.21.0"
bin:
update-browserslist-db: cli.js
checksum: 10/24ba3c17529c4319c6c29e59fb2cb5dadb101ea2170bc6cfca4dd3e0a2fa6686e00d0f5cadb5ba22da3ec3f1a71e36a1785b71717b6f42237cfff1fb160b8bd5
checksum: 10/ca883e9d0fca1b5bfb9d5e7d9468c5ddd1d66ea827566f474875171381ad33b84aa555c138094fb2c6297c680e06482ad8b4c1ad931e8bfab9a4cf36ea9cf006
languageName: node
linkType: hard