mirror of
https://github.com/home-assistant/frontend.git
synced 2026-09-07 20:07:26 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1717d07049 | ||
|
|
33a31ff345 |
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: ha-frontend-review
|
||||
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.
|
||||
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.
|
||||
---
|
||||
|
||||
# HA Frontend Review
|
||||
@@ -92,28 +92,9 @@ 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.
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
---
|
||||
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,8 +47,7 @@ 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, 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-review`: PR template use, review checklist, and recurring review issues.
|
||||
- `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.4",
|
||||
"@codemirror/state": "6.7.2",
|
||||
"@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.9",
|
||||
"browserslist": "4.28.8",
|
||||
"browserslist-useragent-regexp": "4.1.4",
|
||||
"del": "8.0.1",
|
||||
"eslint": "10.10.0",
|
||||
"eslint": "10.9.1",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-webpack": "0.13.11",
|
||||
"eslint-plugin-import-x": "4.17.1",
|
||||
|
||||
@@ -57,18 +57,6 @@ 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, {
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
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,12 +1,11 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { 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";
|
||||
@@ -19,22 +18,13 @@ const SEARCH_KEYS = [
|
||||
{ name: "search_labels.repository", weight: 5 },
|
||||
];
|
||||
|
||||
interface AddonPickerItem extends PickerComboBoxItem {
|
||||
slug: string;
|
||||
hasIcon: boolean;
|
||||
}
|
||||
|
||||
const rowRenderer: RenderItemFunction<AddonPickerItem> = (item) => html`
|
||||
const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
|
||||
<ha-combo-box-item type="button">
|
||||
<span slot="headline">${item.primary}</span>
|
||||
<span slot="supporting-text">${item.secondary}</span>
|
||||
${
|
||||
item.hasIcon
|
||||
? html`<ha-app-icon
|
||||
slot="start"
|
||||
.slug=${item.slug}
|
||||
.hasIcon=${item.hasIcon}
|
||||
></ha-app-icon>`
|
||||
item.icon
|
||||
? html` <img alt="" slot="start" .src=${item.icon} /> `
|
||||
: nothing
|
||||
}
|
||||
</ha-combo-box-item>
|
||||
@@ -50,7 +40,7 @@ class HaAddonPicker extends LitElement {
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@state() private _addons?: AddonPickerItem[];
|
||||
@state() private _addons?: PickerComboBoxItem[];
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@@ -112,10 +102,11 @@ 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,
|
||||
@@ -160,22 +151,15 @@ class HaAddonPicker extends LitElement {
|
||||
private _valueRenderer = (itemId: string) => {
|
||||
const item = this._addons!.find((addon) => addon.id === itemId);
|
||||
return html`${
|
||||
item?.hasIcon
|
||||
? html`<ha-app-icon
|
||||
item?.icon
|
||||
? html`<img
|
||||
slot="start"
|
||||
.slug=${item.slug}
|
||||
.hasIcon=${item.hasIcon}
|
||||
.alt=${item.primary ?? "Unknown"}
|
||||
></ha-app-icon>`
|
||||
alt=${item.primary ?? "Unknown"}
|
||||
.src=${item.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 {
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
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,19 +51,10 @@ export class HaComboBoxItem extends HaMdListItem {
|
||||
white-space: normal;
|
||||
}
|
||||
::slotted(state-badge),
|
||||
::slotted(img),
|
||||
::slotted(ha-app-icon) {
|
||||
::slotted(img) {
|
||||
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,8 +13,6 @@ 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
|
||||
@@ -115,15 +113,6 @@ 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");
|
||||
}
|
||||
@@ -144,8 +133,6 @@ 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,7 +2,6 @@ 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;
|
||||
@@ -19,7 +18,7 @@ export const fetchErrorLog = (hass: HomeAssistant) =>
|
||||
|
||||
export const getErrorLogDownloadUrl = (hass: HomeAssistant) =>
|
||||
hasSupervisorCoreLogDownload(hass)
|
||||
? supervisorUrl("core/logs/latest")
|
||||
? "/api/hassio/core/logs/latest"
|
||||
: "/api/error_log";
|
||||
|
||||
export const getCoreLogFileDownloadUnavailableReason = (
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
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 { supervisorUrl, type HassioResponse } from "./common";
|
||||
import type { HassioResponse } from "./common";
|
||||
|
||||
export interface HassioHomeAssistantInfo {
|
||||
arch: SupervisorArch;
|
||||
@@ -198,20 +198,18 @@ export const fetchHassioLogsFollowSkip = async (
|
||||
);
|
||||
|
||||
export const getHassioLogDownloadUrl = (provider: string) =>
|
||||
supervisorUrl(
|
||||
`${provider.includes("_") ? `addons/${provider}` : provider}/logs`
|
||||
);
|
||||
`/api/hassio/${
|
||||
provider.includes("_") ? `addons/${provider}` : provider
|
||||
}/logs`;
|
||||
|
||||
export const getHassioLogDownloadLinesUrl = (
|
||||
provider: string,
|
||||
lines: number,
|
||||
boot = 0
|
||||
) =>
|
||||
supervisorUrl(
|
||||
`${
|
||||
provider.includes("_") ? `addons/${provider}` : provider
|
||||
}/logs${boot !== 0 ? `/boots/${boot}` : ""}?lines=${lines}`
|
||||
);
|
||||
`/api/hassio/${
|
||||
provider.includes("_") ? `addons/${provider}` : provider
|
||||
}/logs${boot !== 0 ? `/boots/${boot}` : ""}?lines=${lines}`;
|
||||
|
||||
export const setSupervisorOption = async (
|
||||
hass: HomeAssistant,
|
||||
@@ -225,4 +223,4 @@ export const setSupervisorOption = async (
|
||||
});
|
||||
};
|
||||
|
||||
export const coreLatestLogsUrl = supervisorUrl("core/logs/latest");
|
||||
export const coreLatestLogsUrl = "/api/hassio/core/logs/latest";
|
||||
|
||||
+10
-7
@@ -28,7 +28,6 @@ export interface NavigationComboBoxItem extends PickerComboBoxItem {
|
||||
path: string;
|
||||
image?: string;
|
||||
iconColor?: string;
|
||||
app?: Pick<HassioAddonInfo, "slug" | "icon">;
|
||||
}
|
||||
|
||||
export interface BaseNavigationCommand {
|
||||
@@ -39,7 +38,6 @@ export interface BaseNavigationCommand {
|
||||
iconPath?: string;
|
||||
iconColor?: string;
|
||||
image?: string;
|
||||
app?: Pick<HassioAddonInfo, "slug" | "icon">;
|
||||
}
|
||||
|
||||
export interface ActionCommandComboBoxItem extends PickerComboBoxItem {
|
||||
@@ -64,14 +62,19 @@ const generateNavigationPanelCommands = (
|
||||
|
||||
const primary = localize(translationKey) || panel.title || panel.url_path;
|
||||
|
||||
const panelApp = apps?.find(({ slug }) => slug === 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;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
primary,
|
||||
icon,
|
||||
app: panelApp
|
||||
? { slug: panelApp.slug, icon: panelApp.icon }
|
||||
: undefined,
|
||||
image,
|
||||
path: `/${panel.url_path}`,
|
||||
};
|
||||
});
|
||||
@@ -168,7 +171,7 @@ export const generateNavigationCommands = (
|
||||
for (const app of apps.filter((a) => a.version)) {
|
||||
appItems.push({
|
||||
path: `/config/app/${app.slug}`,
|
||||
app: { slug: app.slug, icon: app.icon },
|
||||
image: app.icon ? `/api/hassio/addons/${app.slug}/icon` : undefined,
|
||||
primary: hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.navigation.app_info",
|
||||
{ app: app.name }
|
||||
|
||||
@@ -12,7 +12,6 @@ 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";
|
||||
@@ -311,7 +310,6 @@ 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
|
||||
@@ -336,65 +334,44 @@ export class QuickBar extends LitElement {
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
`
|
||||
: "app" in item && item.app
|
||||
: "image" in item && item.image
|
||||
? html`
|
||||
<ha-app-icon
|
||||
<img
|
||||
slot="start"
|
||||
.slug=${item.app.slug}
|
||||
.hasIcon=${item.app.icon}
|
||||
.alt=${item.primary ?? "Unknown"}
|
||||
class=${iconColor ? "colored" : nothing}
|
||||
alt=${item.primary ?? "Unknown"}
|
||||
.src=${item.image}
|
||||
style=${
|
||||
iconColor
|
||||
? `--app-icon-background-color: ${iconColor}`
|
||||
: nothing
|
||||
"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 .icon=${item.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon .path=${iconPath}></ha-svg-icon>`
|
||||
}
|
||||
</ha-app-icon>
|
||||
/>
|
||||
`
|
||||
: "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`
|
||||
: 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="margin: var(--ha-space-1);"
|
||||
slot="start"
|
||||
style="color: var(--white-color); --mdc-icon-size: 24px;"
|
||||
.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,7 +86,6 @@ import type { HassioStats } from "../../../../../data/hassio/common";
|
||||
import {
|
||||
extractApiErrorMessage,
|
||||
fetchHassioStats,
|
||||
supervisorUrl,
|
||||
} from "../../../../../data/hassio/common";
|
||||
import type { StoreAddonDetails } from "../../../../../data/supervisor/store";
|
||||
import {
|
||||
@@ -206,9 +205,7 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
|
||||
<img
|
||||
class="logo"
|
||||
alt=""
|
||||
src=${supervisorUrl(
|
||||
`addons/${this._currentAddon.slug}/logo`
|
||||
)}
|
||||
src="/api/hassio/addons/${this._currentAddon.slug}/logo"
|
||||
/>
|
||||
`
|
||||
: nothing
|
||||
|
||||
@@ -3,7 +3,6 @@ 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";
|
||||
@@ -42,28 +41,21 @@ class SupervisorAppsCardContent extends LitElement {
|
||||
|
||||
@property() public icon = mdiHelpCircleOutline;
|
||||
|
||||
@property({ attribute: false }) public appSlug?: string;
|
||||
|
||||
@property({ attribute: false }) public hasAppIcon?: boolean;
|
||||
@property({ attribute: false }) public iconImage?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="app">
|
||||
<div class="icon-wrapper">
|
||||
${
|
||||
this.appSlug
|
||||
this.iconImage
|
||||
? html`
|
||||
<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>
|
||||
<img
|
||||
class="icon-image"
|
||||
src=${this.iconImage}
|
||||
.title=${this.iconTitle}
|
||||
alt=${this.iconTitle ?? ""}
|
||||
/>
|
||||
`
|
||||
: html`
|
||||
<ha-svg-icon
|
||||
@@ -142,15 +134,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);
|
||||
}
|
||||
ha-app-icon {
|
||||
--ha-app-icon-size: 40px;
|
||||
.icon-image {
|
||||
max-height: 40px;
|
||||
max-width: 40px;
|
||||
}
|
||||
.title {
|
||||
flex: 1;
|
||||
|
||||
@@ -151,8 +151,11 @@ export class HaConfigAppsInstalled extends LitElement {
|
||||
"ui.panel.config.apps.installed.app_running"
|
||||
)
|
||||
}
|
||||
.appSlug=${addon.slug}
|
||||
.hasAppIcon=${addon.icon}
|
||||
.iconImage=${
|
||||
addon.icon
|
||||
? `/api/hassio/addons/${addon.slug}/icon`
|
||||
: undefined
|
||||
}
|
||||
></supervisor-apps-card-content>
|
||||
</div>
|
||||
</ha-card>
|
||||
|
||||
@@ -116,8 +116,11 @@ export class SupervisorAppsRepositoryEl extends LitElement {
|
||||
? "not_available"
|
||||
: ""
|
||||
}
|
||||
.appSlug=${addon.slug}
|
||||
.hasAppIcon=${addon.icon}
|
||||
.iconImage=${
|
||||
addon.icon
|
||||
? `/api/hassio/addons/${addon.slug}/icon`
|
||||
: undefined
|
||||
}
|
||||
.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,7 +91,6 @@ 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";
|
||||
@@ -374,13 +373,6 @@ 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,8 +73,6 @@ 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";
|
||||
@@ -230,15 +228,6 @@ 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(
|
||||
@@ -249,14 +238,6 @@ 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`
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
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,25 +3,6 @@ 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,8 +72,6 @@ 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";
|
||||
@@ -255,13 +253,6 @@ 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(
|
||||
@@ -272,13 +263,6 @@ 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,10 +4,8 @@ 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";
|
||||
|
||||
@@ -52,17 +50,13 @@ 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>
|
||||
`
|
||||
@@ -92,10 +86,6 @@ 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,25 +15,20 @@ class SupervisorFormfieldLabel extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<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>
|
||||
${
|
||||
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
|
||||
}
|
||||
<span class="label">
|
||||
${this.label}
|
||||
${
|
||||
|
||||
+21
-18
@@ -17,7 +17,6 @@ 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";
|
||||
@@ -205,12 +204,23 @@ 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
|
||||
@@ -243,28 +253,22 @@ export class SerialConfigDashboard extends LitElement {
|
||||
<ha-md-list-item type="link" href=${href} class="consumer">
|
||||
${
|
||||
consumer.kind === "config_entry"
|
||||
? html`<img
|
||||
slot="start"
|
||||
.src=${brandsUrl(
|
||||
? this._renderConsumerIcon(
|
||||
brandsUrl(
|
||||
{
|
||||
domain: consumer.domain!,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
)}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
alt=${consumer.domain!}
|
||||
/>`
|
||||
),
|
||||
consumer.domain!
|
||||
)
|
||||
: consumer.kind === "app"
|
||||
? 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>`
|
||||
? this._renderConsumerIcon(
|
||||
`/api/hassio/addons/${consumer.slug}/icon`,
|
||||
consumer.slug!
|
||||
)
|
||||
: html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiPuzzle}
|
||||
@@ -669,8 +673,7 @@ 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 ha-app-icon[slot="start"] {
|
||||
ha-md-list-item.consumer img[slot="start"] {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ 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";
|
||||
@@ -25,10 +24,7 @@ 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,
|
||||
type HassioAddonInfo,
|
||||
} from "../../../data/hassio/addon";
|
||||
import { fetchHassioAddonsInfo } from "../../../data/hassio/addon";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { mdiHomeAssistant } from "../../../resources/home-assistant-logo-svg";
|
||||
@@ -65,11 +61,6 @@ 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;
|
||||
@@ -147,9 +138,18 @@ export class HaConfigLogs extends LitElement {
|
||||
@click=${this._openPicker}
|
||||
>
|
||||
${
|
||||
selectedProvider
|
||||
? this._renderProviderIcon(selectedProvider)
|
||||
: nothing
|
||||
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?.primary}
|
||||
<ha-svg-icon
|
||||
@@ -264,40 +264,33 @@ export class HaConfigLogs extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _getLogProviderItems = (): LogProviderPickerItem[] =>
|
||||
private _getLogProviderItems = (): PickerComboBoxItem[] =>
|
||||
this._logProviders.map((provider) => ({
|
||||
id: provider.key,
|
||||
primary: provider.name,
|
||||
addon: provider.addon,
|
||||
hasAppIcon: provider.addon
|
||||
icon: 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 _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`
|
||||
private _providerRenderer = (item: PickerComboBoxItem) => html`
|
||||
<ha-combo-box-item type="button" compact>
|
||||
${this._renderProviderIcon(item)}
|
||||
${
|
||||
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
|
||||
}
|
||||
<span slot="headline">${item.primary}</span>
|
||||
${
|
||||
item.secondary
|
||||
@@ -315,10 +308,11 @@ export class HaConfigLogs extends LitElement {
|
||||
return {
|
||||
id: provider.key,
|
||||
primary: provider.name,
|
||||
addon: provider.addon,
|
||||
hasAppIcon: provider.addon
|
||||
icon: 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
|
||||
@@ -371,8 +365,8 @@ export class HaConfigLogs extends LitElement {
|
||||
--mdc-icon-size: var(--ha-space-6);
|
||||
}
|
||||
|
||||
ha-app-icon {
|
||||
--ha-app-icon-size: 32px;
|
||||
img {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
|
||||
@@ -221,19 +221,7 @@ export class HaSceneEditor extends DirtyStateProviderMixin<number>()(
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
// 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) {
|
||||
if (this._unsubscribeEvents) {
|
||||
this._unsubscribeEvents();
|
||||
this._unsubscribeEvents = undefined;
|
||||
}
|
||||
@@ -923,14 +911,11 @@ 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" || this._unsubscribeEvents) {
|
||||
if (!this.isConnected || this._mode !== "live") {
|
||||
unsubscribe();
|
||||
return;
|
||||
}
|
||||
@@ -1107,6 +1092,9 @@ export class HaSceneEditor extends DirtyStateProviderMixin<number>()(
|
||||
};
|
||||
|
||||
private _goBack(): void {
|
||||
if (this._mode === "live") {
|
||||
applyScene(this.hass, this._storedStates);
|
||||
}
|
||||
afterNextRender(() => goBack("/config/scene/dashboard"));
|
||||
}
|
||||
|
||||
@@ -1131,6 +1119,9 @@ 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,8 +455,6 @@ 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,7 +41,6 @@ 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";
|
||||
@@ -109,8 +108,6 @@ class HaPanelHistory extends LitElement {
|
||||
|
||||
@state() private _showSources?: boolean;
|
||||
|
||||
@query("ha-filter-pane") private _filterPane?: HaFilterPane;
|
||||
|
||||
@state() private _entitySources?: EntitySources;
|
||||
|
||||
@state() private _stateHistory?: HistoryResult;
|
||||
@@ -297,9 +294,19 @@ class HaPanelHistory extends LitElement {
|
||||
: "ui.panel.history.start_search"
|
||||
)}
|
||||
>
|
||||
<ha-button appearance="plain" @click=${this._openSources}>
|
||||
${this.hass.localize("ui.panel.history.change_sources")}
|
||||
</ha-button>
|
||||
${
|
||||
!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-empty-state>
|
||||
`;
|
||||
}
|
||||
@@ -390,10 +397,6 @@ 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, query, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fromUnixTime } from "date-fns";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
@@ -37,7 +37,6 @@ 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,
|
||||
@@ -82,8 +81,6 @@ export class HaPanelLogbook extends LitElement {
|
||||
|
||||
@state() private _showSources?: boolean;
|
||||
|
||||
@query("ha-filter-pane") private _filterPane?: HaFilterPane;
|
||||
|
||||
@state() private _entitySources?: EntitySources;
|
||||
|
||||
@state() private _targetPickerValue: HassServiceTarget = {};
|
||||
@@ -247,10 +244,6 @@ export class HaPanelLogbook extends LitElement {
|
||||
}
|
||||
|
||||
private _openSources() {
|
||||
if (this._sourcesShown()) {
|
||||
this._filterPane?.highlight();
|
||||
return;
|
||||
}
|
||||
this._showSources = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import {
|
||||
mdiAutoFix,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiDragHorizontalVariant,
|
||||
@@ -7,13 +8,14 @@ import {
|
||||
mdiPlusCircleMultipleOutline,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
@@ -32,10 +34,32 @@ export class HuiSectionEditMode extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public viewIndex!: number;
|
||||
|
||||
@property({ type: Boolean, attribute: "is-strategy", reflect: true })
|
||||
public isStrategy = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="section-header">
|
||||
<div class="section-actions">
|
||||
${
|
||||
this.isStrategy
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
id="strategy-icon"
|
||||
.path=${mdiAutoFix}
|
||||
role="img"
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.section.automatic"
|
||||
)}
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip for="strategy-icon">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.section.automatic"
|
||||
)}
|
||||
</ha-tooltip>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<ha-svg-icon
|
||||
aria-hidden="true"
|
||||
class="handle"
|
||||
@@ -70,7 +94,25 @@ export class HuiSectionEditMode extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-wrapper">
|
||||
<slot></slot>
|
||||
<div class="section-content" ?inert=${this.isStrategy}>
|
||||
<slot></slot>
|
||||
</div>
|
||||
${
|
||||
this.isStrategy
|
||||
? html`
|
||||
<button
|
||||
class="edit-overlay"
|
||||
type="button"
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.section.edit_automatic"
|
||||
)}
|
||||
@click=${this._editSection}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPencil}></ha-svg-icon>
|
||||
</button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -183,7 +225,12 @@ export class HuiSectionEditMode extends LitElement {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
#strategy-icon {
|
||||
padding: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.section-wrapper {
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
border-radius: var(
|
||||
--ha-section-border-radius,
|
||||
@@ -193,6 +240,67 @@ export class HuiSectionEditMode extends LitElement {
|
||||
border: 2px dashed var(--divider-color);
|
||||
min-height: var(--row-height);
|
||||
}
|
||||
|
||||
:host([is-strategy]) .section-wrapper {
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.edit-overlay {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: inherit;
|
||||
background: none;
|
||||
color: var(--primary-text-color);
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity var(--ha-animation-duration-fast) ease-in-out;
|
||||
}
|
||||
|
||||
.edit-overlay::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: inherit;
|
||||
background: var(--primary-background-color);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.edit-overlay:hover,
|
||||
.edit-overlay:focus-visible,
|
||||
.edit-overlay:active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.edit-overlay:focus-visible {
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.edit-overlay ha-svg-icon {
|
||||
position: relative;
|
||||
padding: var(--ha-space-2);
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
background: var(--secondary-background-color);
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.edit-overlay {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import {
|
||||
mdiClose,
|
||||
mdiDotsVertical,
|
||||
mdiFileMoveOutline,
|
||||
mdiPlaylistEdit,
|
||||
} from "@mdi/js";
|
||||
import { mdiClose, mdiDotsVertical, mdiFileMoveOutline } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { cache } from "lit/directives/cache";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { withViewTransition } from "../../../../common/util/view-transition";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
@@ -22,6 +20,8 @@ import "../../../../components/ha-tab-group-tab";
|
||||
import "../../../../components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
||||
import { isStrategySection } from "../../../../data/lovelace/config/section";
|
||||
import type { LovelaceStrategyConfig } from "../../../../data/lovelace/config/strategy";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import { saveConfig } from "../../../../data/lovelace/config/types";
|
||||
import {
|
||||
@@ -35,7 +35,8 @@ import {
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
} from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
|
||||
import { cleanLegacyStrategyConfig } from "../../strategies/legacy-strategy";
|
||||
import type { Lovelace } from "../../types";
|
||||
import { addSection, deleteSection, moveSection } from "../config-util";
|
||||
import {
|
||||
@@ -45,12 +46,15 @@ import {
|
||||
import { showSelectViewDialog } from "../select-view/show-select-view-dialog";
|
||||
import "./hui-section-settings-editor";
|
||||
import "./hui-section-visibility-editor";
|
||||
import "./hui-section-strategy-element-editor";
|
||||
import type { HuiSectionStrategyElementEditor } from "./hui-section-strategy-element-editor";
|
||||
import type { ConfigChangedEvent } from "../hui-element-editor";
|
||||
import type { EditSectionDialogParams } from "./show-edit-section-dialog";
|
||||
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
|
||||
import { getViewType } from "../../views/get-view-type";
|
||||
import { SECTIONS_VIEW_LAYOUT } from "../../views/const";
|
||||
|
||||
const TABS = ["tab-settings", "tab-visibility"] as const;
|
||||
const TABS = ["tab-strategy", "tab-appearance", "tab-visibility"] as const;
|
||||
|
||||
@customElement("hui-dialog-edit-section")
|
||||
export class HuiDialogEditSection
|
||||
@@ -73,8 +77,15 @@ export class HuiDialogEditSection
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _strategyError = false;
|
||||
|
||||
@state() private _yamlError = false;
|
||||
|
||||
@query("ha-yaml-editor") private _editor?: HaYamlEditor;
|
||||
|
||||
@query("hui-section-strategy-element-editor")
|
||||
private _strategyEditor?: HuiSectionStrategyElementEditor;
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
super.updated(changedProperties);
|
||||
if (this._yamlMode && changedProperties.has("_yamlMode")) {
|
||||
@@ -95,6 +106,9 @@ export class HuiDialogEditSection
|
||||
this._params.viewIndex,
|
||||
this._params.sectionIndex,
|
||||
]);
|
||||
this._currTab = isStrategySection(this._config)
|
||||
? "tab-strategy"
|
||||
: "tab-appearance";
|
||||
this._viewConfig = findLovelaceContainer(this._params.lovelaceConfig, [
|
||||
this._params.viewIndex,
|
||||
]);
|
||||
@@ -111,6 +125,8 @@ export class HuiDialogEditSection
|
||||
this._yamlMode = false;
|
||||
this._config = undefined;
|
||||
this._currTab = TABS[0];
|
||||
this._strategyError = false;
|
||||
this._yamlError = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
@@ -123,6 +139,15 @@ export class HuiDialogEditSection
|
||||
"ui.panel.lovelace.editor.edit_section.header"
|
||||
);
|
||||
|
||||
const strategy = isStrategySection(this._config)
|
||||
? cleanLegacyStrategyConfig(this._config.strategy)
|
||||
: undefined;
|
||||
const tabs = strategy ? TABS : TABS.filter((tab) => tab !== "tab-strategy");
|
||||
const currentTab =
|
||||
!strategy && this._currTab === "tab-strategy"
|
||||
? "tab-appearance"
|
||||
: this._currTab;
|
||||
|
||||
let content: TemplateResult<1> | typeof nothing = nothing;
|
||||
|
||||
if (this._yamlMode) {
|
||||
@@ -134,8 +159,8 @@ export class HuiDialogEditSection
|
||||
></ha-yaml-editor>
|
||||
`;
|
||||
} else {
|
||||
switch (this._currTab) {
|
||||
case "tab-settings":
|
||||
switch (currentTab) {
|
||||
case "tab-appearance":
|
||||
content = html`
|
||||
<hui-section-settings-editor
|
||||
.config=${this._config}
|
||||
@@ -155,6 +180,17 @@ export class HuiDialogEditSection
|
||||
</hui-section-visibility-editor>
|
||||
`;
|
||||
break;
|
||||
case "tab-strategy":
|
||||
content = html`
|
||||
<hui-section-strategy-element-editor
|
||||
.hass=${this.hass}
|
||||
.lovelace=${this._params.lovelaceConfig}
|
||||
.value=${strategy}
|
||||
in-dialog
|
||||
@config-changed=${this._strategyConfigChanged}
|
||||
></hui-section-strategy-element-editor>
|
||||
`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,12 +223,6 @@ export class HuiDialogEditSection
|
||||
.label=${this.hass!.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown-item value="toggle-yaml">
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_view.edit_${!this._yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="move-to-view">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
@@ -207,12 +237,12 @@ export class HuiDialogEditSection
|
||||
!this._yamlMode
|
||||
? html`
|
||||
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
|
||||
${TABS.map(
|
||||
${tabs.map(
|
||||
(tab) => html`
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.panel=${tab}
|
||||
.active=${this._currTab === tab}
|
||||
.active=${currentTab === tab}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.edit_section.${tab.replace("-", "_")}`
|
||||
@@ -225,8 +255,21 @@ export class HuiDialogEditSection
|
||||
: nothing
|
||||
}
|
||||
</ha-dialog-header>
|
||||
${content}
|
||||
${this._yamlMode ? content : cache(content)}
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
class="gui-mode-button"
|
||||
appearance="plain"
|
||||
@click=${this._toggleMode}
|
||||
.disabled=${this._yamlMode && this._yamlError}
|
||||
>
|
||||
${this.hass.localize(
|
||||
this._yamlMode
|
||||
? "ui.panel.lovelace.editor.edit_card.show_visual_editor"
|
||||
: "ui.panel.lovelace.editor.edit_card.show_code_editor"
|
||||
)}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@@ -238,7 +281,7 @@ export class HuiDialogEditSection
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._save}
|
||||
?disabled=${!this.isDirtyState}
|
||||
?disabled=${!this.isDirtyState || this._yamlError || this._strategyError}
|
||||
>
|
||||
${this.hass!.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
@@ -247,12 +290,30 @@ export class HuiDialogEditSection
|
||||
`;
|
||||
}
|
||||
|
||||
private _configChanged(ev: CustomEvent): void {
|
||||
private _configChanged(
|
||||
ev: ValueChangedEvent<LovelaceSectionRawConfig>
|
||||
): void {
|
||||
ev.stopPropagation();
|
||||
this._config = ev.detail.value;
|
||||
this._updateDirtyState(this._config!);
|
||||
}
|
||||
|
||||
private _strategyConfigChanged(
|
||||
ev: HASSDomEvent<ConfigChangedEvent<LovelaceStrategyConfig>>
|
||||
): void {
|
||||
ev.stopPropagation();
|
||||
if (!this._config || !isStrategySection(this._config)) return;
|
||||
this._strategyError = Boolean(
|
||||
this._strategyEditor?.hasError ||
|
||||
ev.detail.error ||
|
||||
typeof ev.detail.config?.type !== "string" ||
|
||||
!ev.detail.config.type
|
||||
);
|
||||
if (this._strategyError) return;
|
||||
this._config = { ...this._config, strategy: ev.detail.config };
|
||||
this._updateDirtyState(this._config);
|
||||
}
|
||||
|
||||
private _handleTabChanged(ev: CustomEvent): void {
|
||||
const newTab = ev.detail.name;
|
||||
if (newTab === this._currTab) {
|
||||
@@ -264,15 +325,19 @@ export class HuiDialogEditSection
|
||||
private async _handleAction(ev: HaDropdownSelectEvent) {
|
||||
const value = ev.detail.item.value;
|
||||
switch (value) {
|
||||
case "toggle-yaml":
|
||||
this._yamlMode = !this._yamlMode;
|
||||
break;
|
||||
case "move-to-view":
|
||||
this._openSelectView();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleMode(): void {
|
||||
if (this._yamlMode && this._yamlError) return;
|
||||
withViewTransition(() => {
|
||||
this._yamlMode = !this._yamlMode;
|
||||
});
|
||||
}
|
||||
|
||||
private _openSelectView(): void {
|
||||
if (!this._params || !this.lovelace) {
|
||||
return;
|
||||
@@ -402,12 +467,21 @@ export class HuiDialogEditSection
|
||||
}
|
||||
};
|
||||
|
||||
private _viewYamlChanged(ev: CustomEvent) {
|
||||
private _viewYamlChanged(
|
||||
ev: ValueChangedEvent<LovelaceSectionRawConfig> &
|
||||
HASSDomEvent<{ isValid: boolean }>
|
||||
) {
|
||||
ev.stopPropagation();
|
||||
if (!ev.detail.isValid) {
|
||||
this._yamlError =
|
||||
!ev.detail.isValid ||
|
||||
!ev.detail.value ||
|
||||
typeof ev.detail.value !== "object" ||
|
||||
Array.isArray(ev.detail.value);
|
||||
if (this._yamlError) {
|
||||
return;
|
||||
}
|
||||
this._config = ev.detail.value;
|
||||
this._strategyError = false;
|
||||
this._updateDirtyState(this._config!);
|
||||
}
|
||||
|
||||
@@ -423,7 +497,12 @@ export class HuiDialogEditSection
|
||||
}
|
||||
|
||||
private async _save(): Promise<void> {
|
||||
if (!this._params || !this._config) {
|
||||
if (
|
||||
!this._params ||
|
||||
!this._config ||
|
||||
this._yamlError ||
|
||||
this._strategyError
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const newConfig = updateLovelaceContainer(
|
||||
@@ -448,6 +527,9 @@ export class HuiDialogEditSection
|
||||
ha-dialog.yaml-mode {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
.gui-mode-button {
|
||||
margin-inline-end: auto;
|
||||
}
|
||||
ha-tab-group-tab {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import type { LovelaceStrategyConfig } from "../../../../data/lovelace/config/strategy";
|
||||
import { getLovelaceStrategy } from "../../strategies/get-strategy";
|
||||
import type { LovelaceStrategyEditor } from "../../strategies/types";
|
||||
import { HuiTypedElementEditor } from "../hui-typed-element-editor";
|
||||
|
||||
@customElement("hui-section-strategy-element-editor")
|
||||
export class HuiSectionStrategyElementEditor extends HuiTypedElementEditor<LovelaceStrategyConfig> {
|
||||
static styles = [
|
||||
HuiTypedElementEditor.styles,
|
||||
css`
|
||||
.gui-editor,
|
||||
.yaml-editor {
|
||||
padding: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
protected async getConfigElement(): Promise<
|
||||
LovelaceStrategyEditor | undefined
|
||||
> {
|
||||
const strategy = await getLovelaceStrategy(
|
||||
"section",
|
||||
this.configElementType!
|
||||
);
|
||||
return strategy.getConfigElement?.();
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-section-strategy-element-editor": HuiSectionStrategyElementEditor;
|
||||
}
|
||||
}
|
||||
@@ -324,7 +324,7 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
|
||||
this._layoutElementType = config.type;
|
||||
this._layoutElement.addEventListener("ll-create-card", (ev) => {
|
||||
ev.stopPropagation();
|
||||
if (!this.lovelace) return;
|
||||
if (!this.lovelace || isStrategySection(this.config)) return;
|
||||
showCreateCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveConfig: this.lovelace.saveConfig,
|
||||
@@ -357,7 +357,7 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
|
||||
});
|
||||
this._layoutElement.addEventListener("ll-delete-card", (ev) => {
|
||||
ev.stopPropagation();
|
||||
if (!this.lovelace) return;
|
||||
if (!this.lovelace || isStrategySection(this.config)) return;
|
||||
performDeleteCard(this.hass, this.lovelace, ev.detail);
|
||||
});
|
||||
this._layoutElement.addEventListener("ll-duplicate-card", (ev) => {
|
||||
|
||||
@@ -17,7 +17,8 @@ export interface LovelaceStrategy<T = any> {
|
||||
newHass: HomeAssistant
|
||||
): boolean;
|
||||
registryDependencies?: readonly LovelaceStrategyDependency[];
|
||||
getConfigElement?: () => LovelaceStrategyEditor;
|
||||
getConfigElement?: () =>
|
||||
LovelaceStrategyEditor | Promise<LovelaceStrategyEditor>;
|
||||
noEditor?: boolean;
|
||||
configRequired?: boolean;
|
||||
}
|
||||
|
||||
+11
-1
@@ -7,7 +7,10 @@ import type { HomeAssistant } from "../../../../types";
|
||||
import type { HeadingCardConfig } from "../../cards/types";
|
||||
import type { Condition } from "../../common/validate-condition";
|
||||
import { computeFavoriteCardConfig } from "../helpers/favorite-cards";
|
||||
import type { LovelaceStrategyDependency } from "../types";
|
||||
import type {
|
||||
LovelaceStrategyDependency,
|
||||
LovelaceStrategyEditor,
|
||||
} from "../types";
|
||||
|
||||
const DEFAULT_LIMIT = 8;
|
||||
|
||||
@@ -30,6 +33,13 @@ export interface CommonControlsSectionStrategyConfig {
|
||||
export class CommonControlsSectionStrategy extends ReactiveElement {
|
||||
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
|
||||
|
||||
public static async getConfigElement(): Promise<LovelaceStrategyEditor> {
|
||||
await import("./hui-common-controls-section-strategy-editor");
|
||||
return document.createElement(
|
||||
"hui-common-controls-section-strategy-editor"
|
||||
);
|
||||
}
|
||||
|
||||
static async generate(
|
||||
config: CommonControlsSectionStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
import { mdiFormatListBulleted } from "@mdi/js";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import type { ValueChangedEvent } from "../../../../types";
|
||||
import type { LovelaceStrategyEditor } from "../types";
|
||||
import type { CommonControlsSectionStrategyConfig } from "./common-controls-section-strategy";
|
||||
|
||||
const SCHEMA = [
|
||||
{
|
||||
name: "limit",
|
||||
selector: { number: { min: 1, mode: "box" } },
|
||||
},
|
||||
{
|
||||
name: "hide_empty",
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
{
|
||||
name: "entities",
|
||||
type: "expandable",
|
||||
flatten: true,
|
||||
iconPath: mdiFormatListBulleted,
|
||||
schema: [
|
||||
{
|
||||
name: "include_entities",
|
||||
selector: { entity: { multiple: true, reorder: true } },
|
||||
},
|
||||
{
|
||||
name: "exclude_entities",
|
||||
selector: { entity: { multiple: true } },
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[];
|
||||
|
||||
@customElement("hui-common-controls-section-strategy-editor")
|
||||
export class HuiCommonControlsSectionStrategyEditor
|
||||
extends LitElement
|
||||
implements LovelaceStrategyEditor
|
||||
{
|
||||
@state() @consumeLocalize() private _localize!: LocalizeFunc;
|
||||
|
||||
@state() private _config?: CommonControlsSectionStrategyConfig;
|
||||
|
||||
public setConfig(config: CommonControlsSectionStrategyConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._config) return nothing;
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.data=${this._config}
|
||||
.schema=${SCHEMA}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
}
|
||||
|
||||
private _computeLabel = (schema: SchemaUnion<typeof SCHEMA>) =>
|
||||
this._localize(
|
||||
`ui.panel.lovelace.editor.strategy.common_controls.${schema.name}`
|
||||
);
|
||||
|
||||
private _computeHelper = (schema: SchemaUnion<typeof SCHEMA>) =>
|
||||
schema.name === "entities" || schema.name === "hide_empty"
|
||||
? ""
|
||||
: this._localize(
|
||||
`ui.panel.lovelace.editor.strategy.common_controls.${schema.name}_helper`
|
||||
);
|
||||
|
||||
private _valueChanged(
|
||||
ev: ValueChangedEvent<CommonControlsSectionStrategyConfig>
|
||||
): void {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "config-changed", {
|
||||
config: { ...this._config, ...ev.detail.value },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-common-controls-section-strategy-editor": HuiCommonControlsSectionStrategyEditor;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import "../../../components/ha-svg-icon";
|
||||
import { maxColumnsContext } from "../common/context";
|
||||
import type { LovelaceViewElement } from "../../../data/lovelace";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import { isStrategySection } from "../../../data/lovelace/config/section";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { HuiBadge } from "../badges/hui-badge";
|
||||
@@ -290,6 +291,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
.lovelace=${this.lovelace}
|
||||
.index=${idx}
|
||||
.viewIndex=${this.index}
|
||||
.isStrategy=${isStrategySection(section.config)}
|
||||
>
|
||||
${this._renderSection(
|
||||
section,
|
||||
|
||||
+15
-12
@@ -665,17 +665,14 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -684,12 +681,10 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -5257,12 +5252,6 @@
|
||||
"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",
|
||||
@@ -9780,6 +9769,8 @@
|
||||
"success": "View moved successfully"
|
||||
},
|
||||
"section": {
|
||||
"automatic": "Automatic section",
|
||||
"edit_automatic": "Edit automatic section",
|
||||
"add_badge": "Add badge",
|
||||
"add_card": "[%key:ui::panel::lovelace::editor::edit_card::add%]",
|
||||
"drop_card_create_section": "Drop card here to create a new section",
|
||||
@@ -9797,8 +9788,9 @@
|
||||
},
|
||||
"edit_section": {
|
||||
"header": "Edit section",
|
||||
"tab_strategy": "Configuration",
|
||||
"tab_visibility": "[%key:ui::panel::lovelace::editor::edit_view::tab_visibility%]",
|
||||
"tab_settings": "[%key:ui::panel::lovelace::editor::edit_view::tab_settings%]",
|
||||
"tab_appearance": "Appearance",
|
||||
"edit_ui": "[%key:ui::panel::lovelace::editor::edit_view::edit_ui%]",
|
||||
"edit_yaml": "[%key:ui::panel::lovelace::editor::edit_view::edit_yaml%]",
|
||||
"settings": {
|
||||
@@ -11136,6 +11128,16 @@
|
||||
}
|
||||
},
|
||||
"strategy": {
|
||||
"common_controls": {
|
||||
"limit": "Maximum number of cards",
|
||||
"limit_helper": "Defaults to 8 cards",
|
||||
"entities": "Entities",
|
||||
"include_entities": "Always include",
|
||||
"include_entities_helper": "Shown first, in the order you choose, within the card limit",
|
||||
"exclude_entities": "Exclude from suggestions",
|
||||
"exclude_entities_helper": "These entities will not be suggested based on your activity",
|
||||
"hide_empty": "Hide when empty"
|
||||
},
|
||||
"original-states": {
|
||||
"areas": "Areas to display",
|
||||
"hide_entities_without_area": "Hide entities without area",
|
||||
@@ -11951,6 +11953,7 @@
|
||||
"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,4 +1,3 @@
|
||||
import { resolveHassUrl } from "../common/url/hass-url";
|
||||
import { isExternalAndroid } from "../data/external";
|
||||
|
||||
// 10 seconds gives the Android WebView download listener enough time
|
||||
@@ -10,7 +9,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 = resolveHassUrl(href);
|
||||
element.href = href;
|
||||
element.download = filename;
|
||||
element.style.display = "none";
|
||||
document.body.appendChild(element);
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import "@formatjs/intl-durationformat/polyfill-force";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { expect, describe, it } from "vitest";
|
||||
import {
|
||||
formatDuration,
|
||||
formatDurationDigital,
|
||||
formatDurationLong,
|
||||
formatDurationNarrow,
|
||||
formatDurationDigital,
|
||||
formatNumericDuration,
|
||||
} from "../../../src/common/datetime/format_duration";
|
||||
import type { FrontendLocaleData } from "../../../src/data/translation";
|
||||
@@ -64,20 +63,6 @@ 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 };
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
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,7 +36,6 @@ describe("fileDownload", () => {
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
delete (window as any).externalApp;
|
||||
delete (window as any).externalAppV2;
|
||||
});
|
||||
@@ -59,16 +58,6 @@ 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,28 +2294,6 @@ __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"
|
||||
@@ -2452,12 +2430,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@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"
|
||||
"@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"
|
||||
dependencies:
|
||||
"@marijn/find-cluster-break": "npm:^1.0.0"
|
||||
checksum: 10/429b7ff6b617d3934eea001165228c3ed77df1943c0b2fdaf019ef3a6b616ada482c344ec46edf5c83121afe6973e9ce4931446922cc2b35f1dad2525eff5300
|
||||
checksum: 10/363a6217e6d34ce406abca93ae7b87181e93e308be5ee037bec911b9d8c7bf6559de7e85620460bc1b7c1a590c05d75bc64e3b09d76fd82110ab829237e3fde2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -2704,13 +2682,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint/plugin-kit@npm:^0.7.3":
|
||||
version: 0.7.3
|
||||
resolution: "@eslint/plugin-kit@npm:0.7.3"
|
||||
"@eslint/plugin-kit@npm:^0.7.2":
|
||||
version: 0.7.2
|
||||
resolution: "@eslint/plugin-kit@npm:0.7.2"
|
||||
dependencies:
|
||||
"@eslint/core": "npm:^1.2.1"
|
||||
levn: "npm:^0.4.1"
|
||||
checksum: 10/bd603f0438d6829a5b0c8cbb90383e3b0fda3aac3a87faa22c80124ce4121f5dcfe8ef9b933c3308cbed70a27c4f51036403e3f0cb64ca2743e595dd865ccef6
|
||||
checksum: 10/ef9fc6f8ca28e132d4c81cfbaa92274800d1d73bb9d6ef2124613dd39b7f09e3592deb64bad10b183bff78db5465d4b100f522d994c8550424526b9ac4a072b0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -3369,25 +3347,6 @@ __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"
|
||||
@@ -7569,12 +7528,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"baseline-browser-mapping@npm:^2.11.20":
|
||||
version: 2.11.21
|
||||
resolution: "baseline-browser-mapping@npm:2.11.21"
|
||||
"baseline-browser-mapping@npm:^2.11.12":
|
||||
version: 2.11.14
|
||||
resolution: "baseline-browser-mapping@npm:2.11.14"
|
||||
bin:
|
||||
baseline-browser-mapping: dist/cli.cjs
|
||||
checksum: 10/b4d223df8dbb8deeaa3dae3b9b60ad48b24c57a5473827b5a6ee1c21917e7c37af2185224c5366f2932654ec5fb1a9fee4f95fe7213cc29f4347550f25e4234b
|
||||
checksum: 10/b1745ec32246d3d1182bfeab2c2efeaf29925e6f617da59f28fde032fce6fa4eaa4632bcf2f9edeca83ceeb7cdfd72d6361ebd7c10e1d4fcb117259ec7059165
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -7759,18 +7718,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"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"
|
||||
"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"
|
||||
dependencies:
|
||||
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"
|
||||
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"
|
||||
bin:
|
||||
browserslist: cli.js
|
||||
checksum: 10/493e5e3c11d4bdd34ce8cb7ed5edc98bc5948457e6a3cc1268b253d0ff9a616033b10980515e0e6faeddae745fb9c330cd4510189d681ad86f9ce0b2da3c840e
|
||||
checksum: 10/9df1d87f915639ebe8f85caf8d58bc0627863dff5e921b6e0394a92f82d279f1b2b324594267d025c80482397150cb4bf5a3ab55fde5d2c1c0f13c064f48b5e9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -7851,19 +7810,6 @@ __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"
|
||||
@@ -7929,10 +7875,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"caniuse-lite@npm:^1.0.30001810":
|
||||
version: 1.0.30001810
|
||||
resolution: "caniuse-lite@npm:1.0.30001810"
|
||||
checksum: 10/47d7db627c3f3d1a10e06eef9efbb84295c6ef9d959b585b64032cc293637ca7b9b6af7e5d5752972b3e690d9561f14b9561db03890fbc6e38dc1aaa54e51d97
|
||||
"caniuse-lite@npm:^1.0.30001809":
|
||||
version: 1.0.30001809
|
||||
resolution: "caniuse-lite@npm:1.0.30001809"
|
||||
checksum: 10/1034b9673e2393bf0c12f5188e0957817d423f71909809eafd69e511c91dedd880d36dd7ee1041605d73fcd3f89a42ba4e235eb942e6bb9f23acdca2fc98f927
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -8898,10 +8844,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"electron-to-chromium@npm:^1.5.420":
|
||||
version: 1.5.422
|
||||
resolution: "electron-to-chromium@npm:1.5.422"
|
||||
checksum: 10/a4cc1bd7204bdb3da063f1ad8103f9990e03da3d0dae0aaf75e1669f2c36a9ae807498c5b27eed61fa98d6b10baf7fe05ff6038a99a4a85a0285e623825253d6
|
||||
"electron-to-chromium@npm:^1.5.402":
|
||||
version: 1.5.407
|
||||
resolution: "electron-to-chromium@npm:1.5.407"
|
||||
checksum: 10/9c4507ef13786d3e4fe16d04929b662b500ac814b2f09f8bda94858ae0bc9962f9a26bfb00bf519b94724eb97bcdc6409cf40cf1b9b540c3443d8f2850fa066a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9385,16 +9331,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint@npm:10.10.0":
|
||||
version: 10.10.0
|
||||
resolution: "eslint@npm:10.10.0"
|
||||
"eslint@npm:10.9.1":
|
||||
version: 10.9.1
|
||||
resolution: "eslint@npm:10.9.1"
|
||||
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.3"
|
||||
"@eslint/plugin-kit": "npm:^0.7.2"
|
||||
"@humanfs/node": "npm:^0.16.6"
|
||||
"@humanwhocodes/module-importer": "npm:^1.0.1"
|
||||
"@humanwhocodes/retry": "npm:^0.4.2"
|
||||
@@ -9409,7 +9355,7 @@ __metadata:
|
||||
esquery: "npm:^1.7.0"
|
||||
esutils: "npm:^2.0.2"
|
||||
fast-deep-equal: "npm:^3.1.3"
|
||||
file-entry-cache: "npm:11.1.5 || >11.1.6 <12"
|
||||
file-entry-cache: "npm:^8.0.0"
|
||||
find-up: "npm:^5.0.0"
|
||||
glob-parent: "npm:^6.0.2"
|
||||
ignore: "npm:^5.2.0"
|
||||
@@ -9426,7 +9372,7 @@ __metadata:
|
||||
optional: true
|
||||
bin:
|
||||
eslint: bin/eslint.js
|
||||
checksum: 10/02fe7e79bf9f81d939ef3ad0c3151c0588f3441eef00bdc468797f2a9f8b047bea97c57a793fc97cf1e4886400b50f049b8e66ec1371820e9df33b54a8bc90d8
|
||||
checksum: 10/4cee237c7d2c445a05330e595b18521d7452fd3f419241bf1ccf81c769a6761c10ca5a06988197482d4842ed1a26b1dcb51b9526408990cee84559e6c08cb3ab
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9689,12 +9635,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"file-entry-cache@npm:11.1.5 || >11.1.6 <12":
|
||||
version: 11.1.5
|
||||
resolution: "file-entry-cache@npm:11.1.5"
|
||||
"file-entry-cache@npm:^8.0.0":
|
||||
version: 8.0.0
|
||||
resolution: "file-entry-cache@npm:8.0.0"
|
||||
dependencies:
|
||||
flat-cache: "npm:^6.1.23"
|
||||
checksum: 10/a4a6c45c9561fbceb6511b3483e299e636e5c67e225ea4445c8cbdbe6c571da8505b29b29fc4c62fe9cf28ffe33314f401fad3da09e4b62321c92f0d3a113135
|
||||
flat-cache: "npm:^4.0.0"
|
||||
checksum: 10/afe55c4de4e0d226a23c1eae62a7219aafb390859122608a89fa4df6addf55c7fd3f1a2da6f5b41e7cdff496e4cf28bbd215d53eab5c817afa96d2b40c81bfb0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9793,18 +9739,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"flat-cache@npm:^6.1.23":
|
||||
version: 6.1.23
|
||||
resolution: "flat-cache@npm:6.1.23"
|
||||
"flat-cache@npm:^4.0.0":
|
||||
version: 4.0.1
|
||||
resolution: "flat-cache@npm:4.0.1"
|
||||
dependencies:
|
||||
cacheable: "npm:^2.5.0"
|
||||
flatted: "npm:^3.4.2"
|
||||
hookified: "npm:^1.15.0"
|
||||
checksum: 10/63ad3682f399e3e7b7255493b07469f80c418034a10e12e1c7043b41d666ff69b20be924984a0b91cd767a4f71540e4fb4421651d15adc736fca1c04b8505758
|
||||
flatted: "npm:^3.2.9"
|
||||
keyv: "npm:^4.5.4"
|
||||
checksum: 10/58ce851d9045fffc7871ce2bd718bc485ad7e777bf748c054904b87c351ff1080c2c11da00788d78738bfb51b71e4d5ea12d13b98eb36e3358851ffe495b62dc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"flatted@npm:^3.4.2":
|
||||
"flatted@npm:^3.2.9":
|
||||
version: 3.4.4
|
||||
resolution: "flatted@npm:3.4.4"
|
||||
checksum: 10/5a6682ab87ed15e125419c61352c4bef26f310adceece03eae5f22c119d64ed47bfc0ee1948a77d0122559eaa86395ea06ef45b59bfe3db4fb13fb4cdff876de
|
||||
@@ -10370,15 +10315,6 @@ __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"
|
||||
@@ -10414,7 +10350,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.4"
|
||||
"@codemirror/state": "npm:6.7.2"
|
||||
"@codemirror/view": "npm:6.43.11"
|
||||
"@date-fns/tz": "npm:1.5.0"
|
||||
"@egjs/hammerjs": "npm:2.0.17"
|
||||
@@ -10487,7 +10423,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.9"
|
||||
browserslist: "npm:4.28.8"
|
||||
browserslist-useragent-regexp: "npm:4.1.4"
|
||||
cally: "npm:0.9.2"
|
||||
color-name: "npm:2.1.1"
|
||||
@@ -10502,7 +10438,7 @@ __metadata:
|
||||
echarts: "npm:6.1.0"
|
||||
echarts-extension-chart2music: "npm:0.1.1"
|
||||
element-internals-polyfill: "npm:3.0.2"
|
||||
eslint: "npm:10.10.0"
|
||||
eslint: "npm:10.9.1"
|
||||
eslint-config-prettier: "npm:10.1.8"
|
||||
eslint-import-resolver-webpack: "npm:0.13.11"
|
||||
eslint-plugin-import-x: "npm:4.17.1"
|
||||
@@ -10598,20 +10534,6 @@ __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"
|
||||
@@ -11582,6 +11504,13 @@ __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"
|
||||
@@ -11714,12 +11643,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"keyv@npm:^5.6.0":
|
||||
version: 5.6.0
|
||||
resolution: "keyv@npm:5.6.0"
|
||||
"keyv@npm:^4.5.4":
|
||||
version: 4.5.4
|
||||
resolution: "keyv@npm:4.5.4"
|
||||
dependencies:
|
||||
"@keyv/serialize": "npm:^1.1.1"
|
||||
checksum: 10/f1de999fdf635d703d091981a0a3844fb20081e01d45c6743ada7d01f3a6570dc47d1882e10d0a98d31075b342db8e62b4ebe4f5bf473dcd0903064d718709c1
|
||||
json-buffer: "npm:3.0.1"
|
||||
checksum: 10/167eb6ef64cc84b6fa0780ee50c9de456b422a1e18802209234f7c2cf7eae648c7741f32e50d7e24ccb22b24c13154070b01563d642755b156c357431a191e75
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -12719,10 +12648,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-releases@npm:^2.0.54":
|
||||
version: 2.0.54
|
||||
resolution: "node-releases@npm:2.0.54"
|
||||
checksum: 10/36380abbe4ce6f5bfec7dab13fa8174e67d2c316a9cbedcd01196bd37dbec99ca7214caf7a91f05d27ae17657d5cb55f39c53db216a7992fd9a2e29966ebd2ba
|
||||
"node-releases@npm:^2.0.53":
|
||||
version: 2.0.53
|
||||
resolution: "node-releases@npm:2.0.53"
|
||||
checksum: 10/24cb89ef08ec15b53fe50dad852dd0444e30abae425c6b9c457dcd3892405cbe8cf7b041a6eeefb4d6be8ce461fe03778501b25916bcc1539a6bc7debc1954e5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -13721,15 +13650,6 @@ __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"
|
||||
@@ -16024,9 +15944,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"update-browserslist-db@npm:^1.3.2":
|
||||
version: 1.3.2
|
||||
resolution: "update-browserslist-db@npm:1.3.2"
|
||||
"update-browserslist-db@npm:^1.3.0":
|
||||
version: 1.3.1
|
||||
resolution: "update-browserslist-db@npm:1.3.1"
|
||||
dependencies:
|
||||
escalade: "npm:^3.2.0"
|
||||
picocolors: "npm:^1.1.1"
|
||||
@@ -16034,7 +15954,7 @@ __metadata:
|
||||
browserslist: ">= 4.21.0"
|
||||
bin:
|
||||
update-browserslist-db: cli.js
|
||||
checksum: 10/ca883e9d0fca1b5bfb9d5e7d9468c5ddd1d66ea827566f474875171381ad33b84aa555c138094fb2c6297c680e06482ad8b4c1ad931e8bfab9a4cf36ea9cf006
|
||||
checksum: 10/24ba3c17529c4319c6c29e59fb2cb5dadb101ea2170bc6cfca4dd3e0a2fa6686e00d0f5cadb5ba22da3ec3f1a71e36a1785b71717b6f42237cfff1fb160b8bd5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user