mirror of
https://github.com/home-assistant/frontend.git
synced 2026-09-07 20:07:26 +00:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c01a140751 | ||
|
|
e50eaaa865 | ||
|
|
cd8b975a38 | ||
|
|
5dd3a4b662 | ||
|
|
e5fa96465c | ||
|
|
79467b51b1 | ||
|
|
c3cc2217bf | ||
|
|
6e6e4d4c4d | ||
|
|
f6cbe49d81 | ||
|
|
1e9592a0b8 | ||
|
|
b2b0491c99 | ||
|
|
9bffae3ee4 | ||
|
|
a287aacd98 |
@@ -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.
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,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,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 = (
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 }
|
||||
|
||||
@@ -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>
|
||||
${
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
${
|
||||
|
||||
+18
-21
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user