Compare commits

..

1 Commits

Author SHA1 Message Date
renovate[bot]
6a46a637e4 Update dependency typescript-eslint to v8.47.0 2025-11-20 17:37:15 +00:00
60 changed files with 364 additions and 2120 deletions

View File

@@ -2,8 +2,6 @@
You are an assistant helping with development of the Home Assistant frontend. The frontend is built using Lit-based Web Components and TypeScript, providing a responsive and performant interface for home automation control.
**Note**: This file contains high-level guidelines and references to implementation patterns. For detailed component documentation, API references, and usage examples, refer to the `gallery/` directory.
## Table of Contents
- [Quick Reference](#quick-reference)
@@ -153,10 +151,6 @@ try {
### Styling Guidelines
- **Use CSS custom properties**: Leverage the theme system
- **Use spacing tokens**: Prefer `--ha-space-*` tokens over hardcoded values for consistent spacing
- Spacing scale: `--ha-space-0` (0px) through `--ha-space-20` (80px) in 4px increments
- Defined in `src/resources/theme/core.globals.ts`
- Common values: `--ha-space-2` (8px), `--ha-space-4` (16px), `--ha-space-8` (32px)
- **Mobile-first responsive**: Design for mobile, enhance for desktop
- **Follow Material Design**: Use Material Web Components where appropriate
- **Support RTL**: Ensure all layouts work in RTL languages
@@ -165,68 +159,21 @@ try {
static get styles() {
return css`
:host {
padding: var(--ha-space-4);
--spacing: 16px;
padding: var(--spacing);
color: var(--primary-text-color);
background-color: var(--card-background-color);
}
.content {
gap: var(--ha-space-2);
}
@media (max-width: 600px) {
:host {
padding: var(--ha-space-2);
--spacing: 8px;
}
}
`;
}
```
### View Transitions
The View Transitions API creates smooth animations between DOM state changes. When implementing view transitions:
**Core Resources:**
- **Utility wrapper**: `src/common/util/view-transition.ts` - `withViewTransition()` function with graceful fallback
- **Real-world example**: `src/util/launch-screen.ts` - Launch screen fade pattern with browser support detection
- **Animation keyframes**: `src/resources/theme/animations.globals.ts` - Global `fade-in`, `fade-out`, `scale` animations
- **Animation duration**: `src/resources/theme/core.globals.ts` - `--ha-animation-base-duration` (350ms, respects `prefers-reduced-motion`)
**Implementation Guidelines:**
1. Always use `withViewTransition()` wrapper for automatic fallback
2. Keep transitions simple (subtle crossfades and fades work best)
3. Use `--ha-animation-base-duration` CSS variable for consistent timing
4. Assign unique `view-transition-name` to elements (must be unique at any given time)
5. For Lit components: Override `performUpdate()` or use `::part()` for internal elements
**Default Root Transition:**
By default, `:root` receives `view-transition-name: root`, creating a full-page crossfade. Target with [`::view-transition-group(root)`](https://developer.mozilla.org/en-US/docs/Web/CSS/::view-transition-group) to customize the default page transition.
**Important Constraints:**
- Each `view-transition-name` must be unique at any given time
- Only one view transition can run at a time
- **Shadow DOM incompatibility**: View transitions operate at document level and do not work within Shadow DOM due to style isolation ([spec discussion](https://github.com/w3c/csswg-drafts/issues/10303)). For web components, set `view-transition-name` on the `:host` element or use document-level transitions
**Current Usage & Planned Applications:**
- Launch screen fade out (implemented)
- Automation sidebar transitions (planned - #27238)
- More info dialog content changes (planned - #27672)
- Toolbar navigation, ha-spinner transitions (planned)
**Specification & Documentation:**
For browser support, API details, and current specifications, refer to these authoritative sources (note: check publication dates as specs evolve):
- [MDN: View Transition API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API) - Comprehensive API reference
- [Chrome for Developers: View Transitions](https://developer.chrome.com/docs/web-platform/view-transitions) - Implementation guide and examples
- [W3C Draft Specification](https://drafts.csswg.org/css-view-transitions/) - Official specification (evolving)
### Performance Best Practices
- **Code split**: Split code at the panel/dialog level
@@ -248,9 +195,8 @@ For browser support, API details, and current specifications, refer to these aut
**Available Dialog Types:**
- `ha-wa-dialog` - Preferred for new dialogs (Web Awesome based)
- `ha-md-dialog` - Material Design 3 dialog component
- `ha-dialog` - Legacy component (still widely used)
- `ha-md-dialog` - Preferred for new code (Material Design 3)
- `ha-dialog` - Legacy component still widely used
**Opening Dialogs (Fire Event Pattern - Recommended):**
@@ -265,45 +211,15 @@ fireEvent(this, "show-dialog", {
**Dialog Implementation Requirements:**
- Implement `HassDialog<T>` interface
- Use `@state() private _open = false` to control dialog visibility
- Set `_open = true` in `showDialog()`, `_open = false` in `closeDialog()`
- Use `createCloseHeading()` for standard headers
- Import `haStyleDialog` for consistent styling
- Return `nothing` when no params (loading state)
- Fire `dialog-closed` event in `_dialogClosed()` handler
- Use `header-title` attribute for simple titles
- Use `header-subtitle` attribute for simple subtitles
- Use slots for custom content where the standard attributes are not enough
- Use `ha-dialog-footer` with `primaryAction`/`secondaryAction` slots for footer content
- Add `autofocus` to first focusable element (e.g., `<ha-form autofocus>`). The component may need to forward this attribute internally.
- Fire `dialog-closed` event when closing
- Add `dialogInitialFocus` for accessibility
**Dialog Sizing:**
- Use `width` attribute with predefined sizes: `"small"` (320px), `"medium"` (560px - default), `"large"` (720px), or `"full"`
- Custom sizing is NOT recommended - use the standard width presets
- Example: `<ha-wa-dialog width="small">` for alert/confirmation dialogs
**Button Appearance Guidelines:**
- **Primary action buttons**: Default appearance (no appearance attribute) or omit for standard styling
- **Secondary action buttons**: Use `appearance="plain"` for cancel/dismiss actions
- **Destructive actions**: Use `appearance="filled"` for delete/remove operations (combined with appropriate semantic styling)
- **Button sizes**: Use `size="small"` (32px height) or default/medium (40px height)
- Always place primary action in `slot="primaryAction"` and secondary in `slot="secondaryAction"` within `ha-dialog-footer`
**Recent Examples:**
See these files for current patterns:
- `src/panels/config/repairs/dialog-repairs-issue.ts`
- `src/dialogs/restart/dialog-restart.ts`
- `src/panels/config/lovelace/resources/dialog-lovelace-resource-detail.ts`
**Gallery Documentation:**
- `gallery/src/pages/components/ha-wa-dialog.markdown`
- `gallery/src/pages/components/ha-dialogs.markdown`
````
### Form Component (ha-form)
- Schema-driven using `HaFormSchema[]`
- Supports entity, device, area, target, number, boolean, time, action, text, object, select, icon, media, location selectors
- Built-in validation with error display
@@ -319,11 +235,7 @@ See these files for current patterns:
.computeLabel=${(schema) => this.hass.localize(`ui.panel.${schema.name}`)}
@value-changed=${this._valueChanged}
></ha-form>
```
**Gallery Documentation:**
- `gallery/src/pages/components/ha-form.markdown`
````
### Alert Component (ha-alert)
@@ -337,35 +249,6 @@ See these files for current patterns:
<ha-alert alert-type="success" dismissable>Success message</ha-alert>
```
**Gallery Documentation:**
- `gallery/src/pages/components/ha-alert.markdown`
### Keyboard Shortcuts (ShortcutManager)
The `ShortcutManager` class provides a unified way to register keyboard shortcuts with automatic input field protection.
**Key Features:**
- Automatically blocks shortcuts when input fields are focused
- Prevents shortcuts during text selection (configurable via `allowWhenTextSelected`)
- Supports both character-based and KeyCode-based shortcuts (for non-latin keyboards)
**Implementation:**
- **Class definition**: `src/common/keyboard/shortcuts.ts`
- **Real-world example**: `src/state/quick-bar-mixin.ts` - Global shortcuts (e, c, d, m, a, Shift+?) with non-latin keyboard fallbacks
### Tooltip Component (ha-tooltip)
The `ha-tooltip` component wraps Web Awesome tooltip with Home Assistant theming. Use for providing contextual help text on hover.
**Implementation:**
- **Component definition**: `src/components/ha-tooltip.ts`
- **Usage example**: `src/components/ha-label.ts`
- **Gallery documentation**: `gallery/src/pages/components/ha-tooltip.markdown`
## Common Patterns
### Creating a Panel
@@ -406,19 +289,11 @@ export class DialogMyFeature
@state()
private _params?: MyDialogParams;
@state()
private _open = false;
public async showDialog(params: MyDialogParams): Promise<void> {
this._params = params;
this._open = true;
}
public closeDialog(): void {
this._open = false;
}
private _dialogClosed(): void {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -429,27 +304,23 @@ export class DialogMyFeature
}
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${this._params.title}
header-subtitle=${this._params.subtitle}
@closed=${this._dialogClosed}
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(this.hass, this._params.title)}
>
<p>Dialog content</p>
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button slot="primaryAction" @click=${this._submit}>
${this.hass.localize("ui.common.save")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
<!-- Dialog content -->
<ha-button
appearance="plain"
@click=${this.closeDialog}
slot="secondaryAction"
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button @click=${this._submit} slot="primaryAction">
${this.hass.localize("ui.common.save")}
</ha-button>
</ha-dialog>
`;
}

View File

@@ -1 +0,0 @@
.github/copilot-instructions.md

View File

@@ -217,7 +217,7 @@
"terser-webpack-plugin": "5.3.14",
"ts-lit-plugin": "2.0.2",
"typescript": "5.9.3",
"typescript-eslint": "8.46.3",
"typescript-eslint": "8.47.0",
"vite-tsconfig-paths": "5.1.4",
"vitest": "4.0.8",
"webpack-stats-plugin": "1.1.3",

View File

@@ -59,8 +59,7 @@ export class HaAuthFlow extends LitElement {
willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!this.hasUpdated && this.clientId === genClientId()) {
// Preselect store token when logging in to own instance
if (!this.hasUpdated) {
this._storeToken = this.initStoreToken;
}

View File

@@ -597,15 +597,10 @@ export class HaChartBase extends LitElement {
aria: { show: true },
dataZoom: this._getDataZoomConfig(),
toolbox: {
top: Number.MAX_SAFE_INTEGER,
left: Number.MAX_SAFE_INTEGER,
top: Infinity,
left: Infinity,
feature: {
dataZoom: {
show: true,
yAxisIndex: false,
filterMode: "none",
showTitle: false,
},
dataZoom: { show: true, yAxisIndex: false, filterMode: "none" },
},
iconStyle: { opacity: 0 },
},

View File

@@ -188,7 +188,6 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
layout: physicsEnabled ? "force" : "none",
draggable: true,
roam: true,
roamTrigger: "global",
selectedMode: "single",
label: {
show: showLabels,

View File

@@ -17,7 +17,6 @@ import type { HomeAssistant } from "../types";
import { AudioRecorder } from "../util/audio-recorder";
import { documentationUrl } from "../util/documentation-url";
import "./ha-alert";
import "./ha-markdown";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
@@ -41,11 +40,7 @@ export class HaAssistChat extends LitElement {
@query("#message-input") private _messageInput!: HaTextField;
@query(".message:last-child")
private _lastChatMessage!: LitElement;
@query(".message:last-child img:last-of-type")
private _lastChatMessageImage: HTMLImageElement | undefined;
@query("#scroll-container") private _scrollContainer!: HTMLDivElement;
@state() private _conversation: AssistMessage[] = [];
@@ -97,7 +92,10 @@ export class HaAssistChat extends LitElement {
public disconnectedCallback() {
super.disconnectedCallback();
this._audioRecorder?.close();
this._audioRecorder = undefined;
this._unloadAudio();
this._conversation = [];
this._conversationId = null;
}
protected render(): TemplateResult {
@@ -114,7 +112,7 @@ export class HaAssistChat extends LitElement {
const supportsSTT = this.pipeline?.stt_engine && !this.disableSpeech;
return html`
<div class="messages">
<div class="messages" id="scroll-container">
${controlHA
? nothing
: html`
@@ -126,18 +124,11 @@ export class HaAssistChat extends LitElement {
`}
<div class="spacer"></div>
${this._conversation!.map(
// New lines matter for messages
// prettier-ignore
(message) => html`
<ha-markdown
class="message ${classMap({
error: !!message.error,
[message.who]: true,
})}"
breaks
cache
.content=${message.text}
>
</ha-markdown>
`
<div class="message ${classMap({ error: !!message.error, [message.who]: true })}">${message.text}</div>
`
)}
</div>
<div class="input" slot="primaryAction">
@@ -198,28 +189,12 @@ export class HaAssistChat extends LitElement {
`;
}
private async _scrollMessagesBottom() {
const lastChatMessage = this._lastChatMessage;
if (!lastChatMessage.hasUpdated) {
await lastChatMessage.updateComplete;
}
if (
this._lastChatMessageImage &&
!this._lastChatMessageImage.naturalHeight
) {
try {
await this._lastChatMessageImage.decode();
} catch (err: any) {
// eslint-disable-next-line no-console
console.warn("Failed to decode image:", err);
}
}
const isLastMessageFullyVisible =
lastChatMessage.getBoundingClientRect().y <
this.getBoundingClientRect().top + 24;
if (!isLastMessageFullyVisible) {
lastChatMessage.scrollIntoView({ behavior: "smooth", block: "start" });
private _scrollMessagesBottom() {
const scrollContainer = this._scrollContainer;
if (!scrollContainer) {
return;
}
scrollContainer.scrollTo(0, scrollContainer.scrollHeight);
}
private _handleKeyUp(ev: KeyboardEvent) {
@@ -611,31 +586,42 @@ export class HaAssistChat extends LitElement {
flex: 1;
}
.message {
white-space: pre-line;
font-size: var(--ha-font-size-l);
clear: both;
max-width: -webkit-fill-available;
overflow-wrap: break-word;
scroll-margin-top: 24px;
margin: 8px 0;
padding: 8px;
border-radius: var(--ha-border-radius-xl);
}
.message:last-child {
margin-bottom: 0;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
.message {
font-size: var(--ha-font-size-l);
}
}
.message p {
margin: 0;
}
.message p:not(:last-child) {
margin-bottom: 8px;
}
.message.user {
margin-left: 24px;
margin-inline-start: 24px;
margin-inline-end: initial;
align-self: flex-end;
text-align: right;
border-bottom-right-radius: 0px;
--markdown-link-color: var(--text-primary-color);
background-color: var(--chat-background-color-user, var(--primary-color));
color: var(--text-primary-color);
direction: var(--direction);
}
.message.hass {
margin-right: 24px;
margin-inline-end: 24px;
@@ -650,21 +636,20 @@ export class HaAssistChat extends LitElement {
color: var(--primary-text-color);
direction: var(--direction);
}
.message.user a {
color: var(--text-primary-color);
}
.message.hass a {
color: var(--primary-text-color);
}
.message.error {
background-color: var(--error-color);
color: var(--text-primary-color);
}
ha-markdown {
--markdown-image-border-radius: calc(var(--ha-border-radius-xl) / 2);
--markdown-table-border-color: var(--divider-color);
--markdown-code-background-color: var(--primary-background-color);
--markdown-code-text-color: var(--primary-text-color);
&:not(:has(ha-markdown-element)) {
min-height: 1lh;
min-width: 1lh;
flex-shrink: 0;
}
}
.bouncer {
width: 48px;
height: 48px;

View File

@@ -101,7 +101,6 @@ export class HaDialog extends DialogBase {
}
.mdc-dialog__container {
align-items: var(--vertical-align-dialog, center);
padding: var(--dialog-container-padding, var(--ha-space-0));
}
.mdc-dialog__title {
padding: var(--ha-space-4) var(--ha-space-4) var(--ha-space-0)
@@ -136,7 +135,7 @@ export class HaDialog extends DialogBase {
--ha-dialog-surface-background,
var(--mdc-theme-surface, #fff)
);
padding: var(--dialog-surface-padding, var(--ha-space-0));
padding: var(--dialog-surface-padding);
}
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
display: flex;

View File

@@ -1,15 +1,11 @@
import type { PropertyValues } from "lit";
import { ReactiveElement, render, html } from "lit";
import { ReactiveElement } from "lit";
import { customElement, property } from "lit/decorators";
// eslint-disable-next-line import/extensions
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import hash from "object-hash";
import { fireEvent } from "../common/dom/fire_event";
import { renderMarkdown } from "../resources/render-markdown";
import { CacheManager } from "../util/cache-manager";
const h = (template: ReturnType<typeof unsafeHTML>) => html`${template}`;
const markdownCache = new CacheManager<string>(1000);
const _gitHubMarkdownAlerts = {
@@ -52,26 +48,18 @@ class HaMarkdownElement extends ReactiveElement {
return this;
}
private _renderPromise: ReturnType<typeof this._render> = Promise.resolve();
protected update(changedProps) {
super.update(changedProps);
if (this.content !== undefined) {
this._renderPromise = this._render();
this._render();
}
}
protected async getUpdateComplete(): Promise<boolean> {
await super.getUpdateComplete();
await this._renderPromise;
return true;
}
protected willUpdate(_changedProperties: PropertyValues): void {
if (!this.innerHTML && this.cache) {
const key = this._computeCacheKey();
if (markdownCache.has(key)) {
render(markdownCache.get(key)!, this.renderRoot);
this.innerHTML = markdownCache.get(key)!;
this._resize();
}
}
@@ -87,7 +75,7 @@ class HaMarkdownElement extends ReactiveElement {
}
private async _render() {
const elements = await renderMarkdown(
this.innerHTML = await renderMarkdown(
String(this.content),
{
breaks: this.breaks,
@@ -99,11 +87,6 @@ class HaMarkdownElement extends ReactiveElement {
}
);
render(
elements.map((e) => h(unsafeHTML(e))),
this.renderRoot
);
this._resize();
const walker = document.createTreeWalker(

View File

@@ -1,12 +1,5 @@
import {
css,
html,
LitElement,
nothing,
type ReactiveElement,
type CSSResultGroup,
} from "lit";
import { customElement, property, query } from "lit/decorators";
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
import { customElement, property } from "lit/decorators";
import "./ha-markdown-element";
@customElement("ha-markdown")
@@ -25,14 +18,6 @@ export class HaMarkdown extends LitElement {
@property({ type: Boolean }) public cache = false;
@query("ha-markdown-element") private _markdownElement!: ReactiveElement;
protected async getUpdateComplete() {
const result = await super.getUpdateComplete();
await this._markdownElement.updateComplete;
return result;
}
protected render() {
if (!this.content) {
return nothing;
@@ -68,46 +53,19 @@ export class HaMarkdown extends LitElement {
margin: var(--ha-space-1) 0;
}
a {
color: var(--markdown-link-color, var(--primary-color));
color: var(--primary-color);
}
img {
background-color: rgba(10, 10, 10, 0.15);
border-radius: var(--markdown-image-border-radius);
max-width: 100%;
min-height: 2lh;
height: auto;
width: auto;
text-indent: 4px;
transition: height 0.2s ease-in-out;
}
p:first-child > img:first-child {
vertical-align: top;
}
p:first-child > img:last-child {
vertical-align: top;
}
ol,
ul {
list-style-position: inside;
padding-inline-start: 0;
}
li {
&:has(input[type="checkbox"]) {
list-style: none;
& > input[type="checkbox"] {
margin-left: 0;
}
}
}
svg {
background-color: var(--markdown-svg-background-color, none);
color: var(--markdown-svg-color, none);
}
code,
pre {
background-color: var(--markdown-code-background-color, none);
border-radius: var(--ha-border-radius-sm);
color: var(--markdown-code-text-color, inherit);
}
svg {
background-color: var(--markdown-svg-background-color, none);
color: var(--markdown-svg-color, none);
}
code {
font-size: var(--ha-font-size-s);
@@ -139,24 +97,6 @@ export class HaMarkdown extends LitElement {
border-bottom: none;
margin: var(--ha-space-4) 0;
}
table {
border-collapse: collapse;
display: block;
overflow-x: auto;
}
th {
text-align: start;
}
td,
th {
border: 1px solid var(--markdown-table-border-color, transparent);
padding: 0.25em 0.5em;
}
blockquote {
border-left: 4px solid var(--divider-color);
margin-inline: 0;
padding-inline: 1em;
}
` as CSSResultGroup;
}

View File

@@ -18,7 +18,7 @@ import {
removeLocalMedia,
} from "../../data/media_source";
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
import { haStyleDialog, haStyleDialogFixedTop } from "../../resources/styles";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "../ha-button";
import "../ha-check-list-item";
@@ -305,7 +305,6 @@ class DialogMediaManage extends LitElement {
static get styles(): CSSResultGroup {
return [
haStyleDialog,
haStyleDialogFixedTop,
css`
ha-dialog {
--dialog-z-index: 9;
@@ -315,9 +314,9 @@ class DialogMediaManage extends LitElement {
@media (min-width: 800px) {
ha-dialog {
--mdc-dialog-max-width: 800px;
--mdc-dialog-max-height: calc(
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
);
--dialog-surface-position: fixed;
--dialog-surface-top: 40px;
--mdc-dialog-max-height: calc(100vh - 72px);
}
}

View File

@@ -19,7 +19,7 @@ import type {
MediaPlayerItem,
MediaPlayerLayoutType,
} from "../../data/media-player";
import { haStyleDialog, haStyleDialogFixedTop } from "../../resources/styles";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "../ha-dialog";
import "../ha-dialog-header";
@@ -223,7 +223,6 @@ class DialogMediaPlayerBrowse extends LitElement {
static get styles(): CSSResultGroup {
return [
haStyleDialog,
haStyleDialogFixedTop,
css`
ha-dialog {
--dialog-z-index: 9;
@@ -231,27 +230,23 @@ class DialogMediaPlayerBrowse extends LitElement {
}
ha-media-player-browse {
--media-browser-max-height: calc(
100vh - 65px - var(--safe-area-inset-y)
);
--media-browser-max-height: calc(100vh - 65px);
}
:host(.opened) ha-media-player-browse {
height: calc(100vh - 65px - var(--safe-area-inset-y));
height: calc(100vh - 65px);
}
@media (min-width: 800px) {
ha-dialog {
--mdc-dialog-max-width: 800px;
--mdc-dialog-max-height: calc(
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
);
--dialog-surface-position: fixed;
--dialog-surface-top: 40px;
--mdc-dialog-max-height: calc(100vh - 72px);
}
ha-media-player-browse {
position: initial;
--media-browser-max-height: calc(
100vh - 145px - var(--safe-area-inset-y)
);
--media-browser-max-height: calc(100vh - 145px);
width: 700px;
}
}

View File

@@ -10,7 +10,6 @@ import {
import { formatTime } from "../common/datetime/format_time";
import type { LocalizeFunc } from "../common/translations/localize";
import type { HomeAssistant } from "../types";
import { documentationUrl } from "../util/documentation-url";
import { fileDownload } from "../util/file_download";
import { handleFetchPromise } from "../util/hass-call-api";
import type { BackupManagerState, ManagerStateEvent } from "./backup_manager";
@@ -415,7 +414,7 @@ ${hass.auth.data.hassUrl}
${hass.localize("ui.panel.config.backup.emergency_kit_file.encryption_key")}
${encryptionKey}
${hass.localize("ui.panel.config.backup.emergency_kit_file.more_info", { link: documentationUrl(hass, "/more-info/backup-emergency-kit") })}`);
${hass.localize("ui.panel.config.backup.emergency_kit_file.more_info", { link: "https://www.home-assistant.io/more-info/backup-emergency-kit" })}`);
export const geneateEmergencyKitFileName = (
hass: HomeAssistant,

View File

@@ -3,7 +3,7 @@ import type { Connection } from "home-assistant-js-websocket";
export interface CoreFrontendUserData {
showAdvanced?: boolean;
showEntityIdPicker?: boolean;
default_panel?: string;
defaultPanel?: string;
}
export interface SidebarFrontendUserData {
@@ -12,11 +12,7 @@ export interface SidebarFrontendUserData {
}
export interface CoreFrontendSystemData {
default_panel?: string;
}
export interface HomeFrontendSystemData {
favorite_entities?: string[];
defaultPanel?: string;
}
declare global {
@@ -26,7 +22,6 @@ declare global {
}
interface FrontendSystemData {
core: CoreFrontendSystemData;
home: HomeFrontendSystemData;
}
}

View File

@@ -1,78 +0,0 @@
import type { Connection } from "home-assistant-js-websocket";
import { createCollection } from "home-assistant-js-websocket";
import type { Store } from "home-assistant-js-websocket/dist/store";
import { debounce } from "../common/util/debounce";
import type { HomeAssistant } from "../types";
export interface LabPreviewFeature {
preview_feature: string;
domain: string;
enabled: boolean;
is_built_in: boolean;
feedback_url?: string;
learn_more_url?: string;
report_issue_url?: string;
}
export interface LabPreviewFeaturesResponse {
features: LabPreviewFeature[];
}
export const fetchLabFeatures = async (
hass: HomeAssistant
): Promise<LabPreviewFeature[]> => {
const response = await hass.callWS<LabPreviewFeaturesResponse>({
type: "labs/list",
});
return response.features;
};
export const labsUpdatePreviewFeature = (
hass: HomeAssistant,
domain: string,
preview_feature: string,
enabled: boolean,
create_backup?: boolean
): Promise<void> =>
hass.callWS({
type: "labs/update",
domain,
preview_feature,
enabled,
...(create_backup !== undefined && { create_backup }),
});
const fetchLabFeaturesCollection = (conn: Connection) =>
conn
.sendMessagePromise<LabPreviewFeaturesResponse>({
type: "labs/list",
})
.then((response) => response.features);
const subscribeLabUpdates = (
conn: Connection,
store: Store<LabPreviewFeature[]>
) =>
conn.subscribeEvents(
debounce(
() =>
fetchLabFeaturesCollection(conn).then((features: LabPreviewFeature[]) =>
store.setState(features, true)
),
500,
true
),
"labs_updated"
);
export const subscribeLabFeatures = (
conn: Connection,
onChange: (features: LabPreviewFeature[]) => void
) =>
createCollection<LabPreviewFeature[]>(
"_labFeatures",
fetchLabFeaturesCollection,
subscribeLabUpdates,
conn,
onChange
);

View File

@@ -9,8 +9,8 @@ export const getLegacyDefaultPanelUrlPath = (): string | null => {
};
export const getDefaultPanelUrlPath = (hass: HomeAssistant): string =>
hass.userData?.default_panel ||
hass.systemData?.default_panel ||
hass.userData?.defaultPanel ||
hass.systemData?.defaultPanel ||
getLegacyDefaultPanelUrlPath() ||
DEFAULT_PANEL;

View File

@@ -72,7 +72,6 @@ export type TranslationCategory =
| "system_health"
| "application_credentials"
| "issues"
| "preview_features"
| "selector"
| "services"
| "triggers";

View File

@@ -50,7 +50,7 @@ import { lightSupportsFavoriteColors } from "../../data/light";
import type { ItemType } from "../../data/search";
import { SearchableDomains } from "../../data/search";
import { getSensorNumericDeviceClasses } from "../../data/sensor";
import { haStyleDialog, haStyleDialogFixedTop } from "../../resources/styles";
import { haStyleDialog } from "../../resources/styles";
import "../../state-summary/state-card-content";
import type { HomeAssistant } from "../../types";
import {
@@ -707,9 +707,14 @@ export class MoreInfoDialog extends LitElement {
static get styles() {
return [
haStyleDialog,
haStyleDialogFixedTop,
css`
ha-dialog {
/* Set the top top of the dialog to a fixed position, so it doesnt jump when the content changes size */
--vertical-align-dialog: flex-start;
--dialog-surface-margin-top: max(
var(--ha-space-10),
var(--safe-area-inset-top, var(--ha-space-0))
);
--dialog-content-padding: 0;
}
@@ -732,6 +737,13 @@ export class MoreInfoDialog extends LitElement {
display: block;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
/* When in fullscreen dialog should be attached to top */
ha-dialog {
--dialog-surface-margin-top: var(--ha-space-0);
}
}
@media all and (min-width: 600px) and (min-height: 501px) {
ha-dialog {
--mdc-dialog-min-width: 580px;

View File

@@ -46,11 +46,7 @@ import { getPanelNameTranslationKey } from "../../data/panel";
import type { PageNavigation } from "../../layouts/hass-tabs-subpage";
import { configSections } from "../../panels/config/ha-panel-config";
import { HaFuse } from "../../resources/fuse";
import {
haStyleDialog,
haStyleDialogFixedTop,
haStyleScrollbar,
} from "../../resources/styles";
import { haStyleDialog, haStyleScrollbar } from "../../resources/styles";
import { loadVirtualizer } from "../../resources/virtualizer";
import type { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url";
@@ -990,7 +986,6 @@ export class QuickBar extends LitElement {
return [
haStyleScrollbar,
haStyleDialog,
haStyleDialogFixedTop,
css`
ha-list {
position: relative;
@@ -1015,9 +1010,9 @@ export class QuickBar extends LitElement {
ha-dialog {
--mdc-dialog-max-width: 800px;
--mdc-dialog-min-width: 500px;
--mdc-dialog-max-height: calc(
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
);
--dialog-surface-position: fixed;
--dialog-surface-top: var(--ha-space-10);
--mdc-dialog-max-height: calc(100% - var(--ha-space-18));
}
}

View File

@@ -5,7 +5,6 @@ import { atLeastVersion } from "../common/config/version";
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
import "../components/ha-card";
import { haStyle } from "../resources/styles";
import { documentationUrl } from "../util/documentation-url";
import type { HomeAssistant } from "../types";
import "./hass-subpage";
@@ -58,7 +57,7 @@ class SupervisorErrorScreen extends LitElement {
</li>
<li>
<a
href=${documentationUrl(this.hass, "/help/")}
href="https://www.home-assistant.io/help/"
target="_blank"
rel="noreferrer"
>

View File

@@ -4,7 +4,6 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-card";
import { documentationUrl } from "../util/documentation-url";
import type { HomeAssistant } from "../types";
import { showAppDialog } from "./dialogs/show-app-dialog";
import { showCommunityDialog } from "./dialogs/show-community-dialog";
@@ -23,10 +22,7 @@ class OnboardingWelcomeLinks extends LitElement {
return html`<a
target="_blank"
rel="noreferrer noopener"
href=${documentationUrl(
this.hass,
"/blog/2016/01/19/perfect-home-automation/"
)}
href="https://www.home-assistant.io/blog/2016/01/19/perfect-home-automation/"
>
<onboarding-welcome-link
noninteractive

View File

@@ -2,7 +2,6 @@ import { mdiClose, mdiOpenInNew } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { documentationUrl } from "../../../util/documentation-url";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-code-editor";
@@ -141,7 +140,7 @@ class DialogImportBlueprint extends LitElement {
<ha-button
size="small"
appearance="plain"
href=${documentationUrl(this.hass, "/get-blueprints")}
href="https://www.home-assistant.io/get-blueprints"
target="_blank"
rel="noreferrer noopener"
>

View File

@@ -299,7 +299,7 @@ class HaBlueprintOverview extends LitElement {
>
<ha-button
appearance="plain"
href=${documentationUrl(this.hass, "/get-blueprints")}
href="https://www.home-assistant.io/get-blueprints"
target="_blank"
rel="noreferrer noopener"
size="small"

View File

@@ -1,3 +1,4 @@
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical, mdiRefresh } from "@mdi/js";
import type { HassEntities } from "home-assistant-js-websocket";
import type { TemplateResult } from "lit";
@@ -5,6 +6,7 @@ import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
import "../../../components/ha-alert";
import "../../../components/ha-bar";
import "../../../components/ha-button-menu";
@@ -31,9 +33,6 @@ import "../../../layouts/hass-subpage";
import type { HomeAssistant } from "../../../types";
import "../dashboard/ha-config-updates";
import { showJoinBetaDialog } from "./updates/show-dialog-join-beta";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "@home-assistant/webawesome/dist/components/divider/divider";
@customElement("ha-config-section-updates")
class HaConfigSectionUpdates extends LitElement {
@@ -74,25 +73,24 @@ class HaConfigSectionUpdates extends LitElement {
.path=${mdiRefresh}
@click=${this._checkUpdates}
></ha-icon-button>
<ha-dropdown @wa-select=${this._handleOverflowAction}>
<ha-button-menu multi>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-dropdown-item
type="checkbox"
value="show_skipped"
.checked=${this._showSkipped}
<ha-check-list-item
left
@request-selected=${this._toggleSkipped}
.selected=${this._showSkipped}
>
${this.hass.localize("ui.panel.config.updates.show_skipped")}
</ha-dropdown-item>
</ha-check-list-item>
${this._supervisorInfo
? html`
<wa-divider></wa-divider>
<ha-dropdown-item
value="toggle_beta"
<li divider role="separator"></li>
<ha-list-item
@request-selected=${this._toggleBeta}
.disabled=${this._supervisorInfo.channel === "dev"}
>
${this._supervisorInfo.channel === "stable"
@@ -100,10 +98,10 @@ class HaConfigSectionUpdates extends LitElement {
: this.hass.localize(
"ui.panel.config.updates.leave_beta"
)}
</ha-dropdown-item>
</ha-list-item>
`
: ""}
</ha-dropdown>
</ha-button-menu>
</div>
<div class="content">
<ha-card outlined>
@@ -135,19 +133,27 @@ class HaConfigSectionUpdates extends LitElement {
this._supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
}
private async _handleOverflowAction(
ev: CustomEvent<{ item: { value: string } }>
private _toggleSkipped(ev: CustomEvent<RequestSelectedDetail>): void {
if (ev.detail.source !== "property") {
return;
}
this._showSkipped = !this._showSkipped;
}
private async _toggleBeta(
ev: CustomEvent<RequestSelectedDetail>
): Promise<void> {
if (ev.detail.item.value === "toggle_beta") {
if (this._supervisorInfo!.channel === "stable") {
showJoinBetaDialog(this, {
join: async () => this._setChannel("beta"),
});
} else {
this._setChannel("stable");
}
} else if (ev.detail.item.value === "show_skipped") {
this._showSkipped = !this._showSkipped;
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
if (this._supervisorInfo!.channel === "stable") {
showJoinBetaDialog(this, {
join: async () => this._setChannel("beta"),
});
} else {
this._setChannel("stable");
}
}

View File

@@ -23,8 +23,6 @@ import {
fetchHassioHassOsInfo,
fetchHassioHostInfo,
} from "../../../data/hassio/host";
import type { LabPreviewFeature } from "../../../data/labs";
import { fetchLabFeatures } from "../../../data/labs";
import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart";
import "../../../layouts/hass-subpage";
import { haStyle } from "../../../resources/styles";
@@ -52,8 +50,6 @@ class HaConfigSystemNavigation extends LitElement {
@state() private _externalAccess = false;
@state() private _labFeatures?: LabPreviewFeature[];
protected render(): TemplateResult {
const pages = configSections.general
.filter((page) => canShowPage(this.hass, page))
@@ -98,12 +94,6 @@ class HaConfigSystemNavigation extends LitElement {
this._boardName ||
this.hass.localize("ui.panel.config.hardware.description");
break;
case "labs":
description =
this._labFeatures && this._labFeatures.some((f) => f.enabled)
? this.hass.localize("ui.panel.config.labs.description_enabled")
: this.hass.localize("ui.panel.config.labs.description");
break;
default:
description = this.hass.localize(
@@ -166,7 +156,6 @@ class HaConfigSystemNavigation extends LitElement {
const isHassioLoaded = isComponentLoaded(this.hass, "hassio");
this._fetchBackupInfo();
this._fetchHardwareInfo(isHassioLoaded);
this._fetchLabFeatures();
if (isHassioLoaded) {
this._fetchStorageInfo();
}
@@ -222,12 +211,6 @@ class HaConfigSystemNavigation extends LitElement {
this._externalAccess = this.hass.config.external_url !== null;
}
private async _fetchLabFeatures() {
if (isComponentLoaded(this.hass, "labs")) {
this._labFeatures = await fetchLabFeatures(this.hass);
}
}
private async _showRestartDialog() {
showRestartDialog(this);
}

View File

@@ -281,12 +281,8 @@ class DialogNewDashboard extends LitElement implements HassDialog {
@media all and (min-width: 850px) {
ha-dialog {
--mdc-dialog-min-width: 845px;
--mdc-dialog-min-height: calc(
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
);
--mdc-dialog-max-height: calc(
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
);
--mdc-dialog-min-height: calc(100vh - 72px);
--mdc-dialog-max-height: calc(100vh - 72px);
}
}

View File

@@ -2,7 +2,6 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../components/ha-alert";
import type { EnergyValidationIssue } from "../../../../data/energy";
import { documentationUrl } from "../../../../util/documentation-url";
import type { HomeAssistant } from "../../../../types";
@customElement("ha-energy-validation-result")
@@ -30,10 +29,7 @@ class EnergyValidationMessage extends LitElement {
)}
${issue.type === "recorder_untracked"
? html`(<a
href=${documentationUrl(
this.hass,
"/integrations/recorder#configure-filter"
)}
href="https://www.home-assistant.io/integrations/recorder#configure-filter"
target="_blank"
rel="noopener noreferrer"
>${this.hass.localize("ui.panel.config.common.learn_more")}</a

View File

@@ -7,7 +7,6 @@ import {
mdiCog,
mdiDatabase,
mdiDevices,
mdiFlask,
mdiInformation,
mdiInformationOutline,
mdiLabel,
@@ -329,13 +328,6 @@ export const configSections: Record<string, PageNavigation[]> = {
iconPath: mdiShape,
iconColor: "#f1c447",
},
{
path: "/config/labs",
translationKey: "labs",
iconPath: mdiFlask,
iconColor: "#b1b134",
core: true,
},
{
path: "/config/network",
translationKey: "network",
@@ -523,10 +515,6 @@ class HaPanelConfig extends SubscribeMixin(HassRouterPage) {
tag: "ha-config-section-general",
load: () => import("./core/ha-config-section-general"),
},
labs: {
tag: "ha-config-labs",
load: () => import("./labs/ha-config-labs"),
},
zha: {
tag: "zha-config-dashboard-router",
load: () =>

View File

@@ -462,7 +462,7 @@ class AddIntegrationDialog extends LitElement {
style=${styleMap({
width: `${this._width}px`,
height: this._narrow
? "calc(100vh - 184px - var(--safe-area-inset-top, var(--ha-space-0)) - var(--safe-area-inset-bottom, var(--ha-space-0)))"
? "calc(100vh - 184px - var(--safe-area-inset-top, 0px) - var(--safe-area-inset-bottom, 0px))"
: "500px",
})}
@click=${this._integrationPicked}

View File

@@ -1,10 +1,4 @@
import {
mdiBookshelf,
mdiCog,
mdiDelete,
mdiDotsVertical,
mdiOpenInNew,
} from "@mdi/js";
import { mdiBookshelf, mdiCog, mdiDotsVertical, mdiOpenInNew } from "@mdi/js";
import type { TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
@@ -13,11 +7,6 @@ import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-button";
import "../../../components/ha-button-menu";
import "../../../components/ha-list-item";
import {
deleteApplicationCredential,
fetchApplicationCredentialsConfigEntry,
} from "../../../data/application_credential";
import { deleteConfigEntry } from "../../../data/config_entries";
import {
ATTENTION_SOURCES,
DISCOVERY_SOURCES,
@@ -26,10 +15,7 @@ import {
} from "../../../data/config_flow";
import type { IntegrationManifest } from "../../../data/integration";
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import type { DataEntryFlowProgressExtended } from "./ha-config-integrations";
@@ -74,7 +60,7 @@ export class HaConfigFlowCard extends LitElement {
: "ui.common.add"
)}
</ha-button>
${this.flow.context.configuration_url || this.manifest || attention
${this.flow.context.configuration_url || this.manifest
? html`<ha-button-menu slot="header-button">
<ha-icon-button
slot="trigger"
@@ -132,22 +118,6 @@ export class HaConfigFlowCard extends LitElement {
</ha-list-item>
</a>`
: ""}
${attention
? html`<ha-list-item
class="warning"
graphic="icon"
@click=${this._handleDelete}
>
<ha-svg-icon
class="warning"
slot="graphic"
.path=${mdiDelete}
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.delete"
)}
</ha-list-item>`
: ""}
</ha-button-menu>`
: ""}
</ha-integration-action-card>
@@ -205,109 +175,6 @@ export class HaConfigFlowCard extends LitElement {
});
}
// Return an application credentials id for this config entry to prompt the
// user for removal. This is best effort so we don't stop overall removal
// if the integration isn't loaded or there is some other error.
private async _fetchApplicationCredentials(entryId: string) {
try {
return (await fetchApplicationCredentialsConfigEntry(this.hass, entryId))
.application_credentials_id;
} catch (_err: any) {
// We won't prompt the user to remove credentials
return null;
}
}
private async _removeApplicationCredential(applicationCredentialsId: string) {
const confirmed = await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_entry.application_credentials.delete_title"
),
text: html`${this.hass.localize(
"ui.panel.config.integrations.config_entry.application_credentials.delete_prompt"
)},
<br />
<br />
${this.hass.localize(
"ui.panel.config.integrations.config_entry.application_credentials.delete_detail"
)}
<br />
<br />
<a
href="https://www.home-assistant.io/integrations/application_credentials"
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.application_credentials.learn_more"
)}
</a>`,
confirmText: this.hass.localize("ui.common.delete"),
dismissText: this.hass.localize("ui.common.cancel"),
destructive: true,
});
if (!confirmed) {
return;
}
try {
await deleteApplicationCredential(this.hass, applicationCredentialsId);
} catch (err: any) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_entry.application_credentials.delete_error_title"
),
text: err.message,
});
}
}
private async _handleDelete() {
const entryId = this.flow.context.entry_id;
if (!entryId) {
// This shouldn't happen for reauth flows, but handle gracefully
return;
}
const applicationCredentialsId =
await this._fetchApplicationCredentials(entryId);
const confirmed = await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_entry.delete_confirm_title",
{ title: localizeConfigFlowTitle(this.hass.localize, this.flow) }
),
text: this.hass.localize(
"ui.panel.config.integrations.config_entry.delete_confirm_text"
),
confirmText: this.hass!.localize("ui.common.delete"),
dismissText: this.hass!.localize("ui.common.cancel"),
destructive: true,
});
if (!confirmed) {
return;
}
const result = await deleteConfigEntry(this.hass, entryId);
if (result.require_restart) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_entry.restart_confirm"
),
});
}
if (applicationCredentialsId) {
this._removeApplicationCredential(applicationCredentialsId);
}
this._handleFlowUpdated();
}
static styles = css`
a {
text-decoration: none;
@@ -324,9 +191,6 @@ export class HaConfigFlowCard extends LitElement {
--mdc-theme-primary: var(--error-color);
--ha-card-border-color: var(--error-color);
}
.warning {
--mdc-theme-text-primary-on-background: var(--error-color);
}
`;
}

View File

@@ -12,10 +12,7 @@ import "../../../../../components/ha-tab-group";
import "../../../../../components/ha-tab-group-tab";
import type { ZHADevice, ZHAGroup } from "../../../../../data/zha";
import { fetchBindableDevices, fetchGroups } from "../../../../../data/zha";
import {
haStyleDialog,
haStyleDialogFixedTop,
} from "../../../../../resources/styles";
import { haStyleDialog } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import { sortZHADevices, sortZHAGroups } from "./functions";
import type {
@@ -214,11 +211,11 @@ class DialogZHAManageZigbeeDevice extends LitElement {
static get styles(): CSSResultGroup {
return [
haStyleDialog,
haStyleDialogFixedTop,
css`
ha-dialog {
--dialog-surface-position: static;
--dialog-content-position: static;
--vertical-align-dialog: flex-start;
}
.content {
@@ -232,9 +229,8 @@ class DialogZHAManageZigbeeDevice extends LitElement {
ha-dialog {
--mdc-dialog-min-width: 560px;
--mdc-dialog-max-width: 560px;
--mdc-dialog-max-height: calc(
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
);
--dialog-surface-margin-top: 40px;
--mdc-dialog-max-height: calc(100% - 72px);
}
}

View File

@@ -295,7 +295,7 @@ export class ZHANetworkVisualizationPage extends LitElement {
color:
route.route_status === "Active"
? primaryColor
: style.getPropertyValue("--dark-primary-color"),
: style.getPropertyValue("--disabled-color"),
type: ["Child", "Parent"].includes(neighbor.relationship)
? "solid"
: "dotted",
@@ -335,7 +335,7 @@ export class ZHANetworkVisualizationPage extends LitElement {
symbolSize: 5,
lineStyle: {
width: 1,
color: style.getPropertyValue("--dark-primary-color"),
color: style.getPropertyValue("--disabled-color"),
type: "dotted",
},
ignoreForceLayout: true,

View File

@@ -1,223 +0,0 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { relativeTime } from "../../../common/datetime/relative_time";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-button";
import type { HaMdDialog } from "../../../components/ha-md-dialog";
import "../../../components/ha-md-dialog";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import type { HaSwitch } from "../../../components/ha-switch";
import "../../../components/ha-switch";
import type { BackupConfig } from "../../../data/backup";
import { fetchBackupConfig } from "../../../data/backup";
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
import type { HomeAssistant } from "../../../types";
import type { LabsPreviewFeatureEnableDialogParams } from "./show-dialog-labs-preview-feature-enable";
@customElement("dialog-labs-preview-feature-enable")
export class DialogLabsPreviewFeatureEnable
extends LitElement
implements HassDialog<LabsPreviewFeatureEnableDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: LabsPreviewFeatureEnableDialogParams;
@state() private _backupConfig?: BackupConfig;
@state() private _createBackup = false;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public async showDialog(
params: LabsPreviewFeatureEnableDialogParams
): Promise<void> {
this._params = params;
this._createBackup = false;
await this._fetchBackupConfig();
}
public closeDialog(): boolean {
this._dialog?.close();
return true;
}
private _dialogClosed(): void {
this._params = undefined;
this._backupConfig = undefined;
this._createBackup = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
private async _fetchBackupConfig() {
try {
const { config } = await fetchBackupConfig(this.hass);
this._backupConfig = config;
// Default to enabled if automatic backups are configured, disabled otherwise
this._createBackup =
config.automatic_backups_configured &&
!!config.create_backup.password &&
config.create_backup.agent_ids.length > 0;
} catch {
// User will get manual backup option if fetch fails
this._createBackup = false;
}
}
private _computeCreateBackupTexts():
| { title: string; description?: string }
| undefined {
if (
!this._backupConfig ||
!this._backupConfig.automatic_backups_configured ||
!this._backupConfig.create_backup.password ||
this._backupConfig.create_backup.agent_ids.length === 0
) {
return {
title: this.hass.localize("ui.panel.config.labs.create_backup.manual"),
description: this.hass.localize(
"ui.panel.config.labs.create_backup.manual_description"
),
};
}
const lastAutomaticBackupDate = this._backupConfig
.last_completed_automatic_backup
? new Date(this._backupConfig.last_completed_automatic_backup)
: null;
const now = new Date();
return {
title: this.hass.localize("ui.panel.config.labs.create_backup.automatic"),
description: lastAutomaticBackupDate
? this.hass.localize(
"ui.panel.config.labs.create_backup.automatic_description_last",
{
relative_time: relativeTime(
lastAutomaticBackupDate,
this.hass.locale,
now,
true
),
}
)
: this.hass.localize(
"ui.panel.config.labs.create_backup.automatic_description_none"
),
};
}
private _createBackupChanged(ev: Event): void {
this._createBackup = (ev.target as HaSwitch).checked;
}
private _handleCancel(): void {
this.closeDialog();
}
private _handleConfirm(): void {
if (this._params) {
this._params.onConfirm(this._createBackup);
}
this.closeDialog();
}
protected render() {
if (!this._params) {
return nothing;
}
const createBackupTexts = this._computeCreateBackupTexts();
return html`
<ha-md-dialog open @closed=${this._dialogClosed}>
<span slot="headline">
${this.hass.localize("ui.panel.config.labs.enable_title")}
</span>
<div slot="content">
<p>
${this.hass.localize(
`component.${this._params.preview_feature.domain}.preview_features.${this._params.preview_feature.preview_feature}.enable_confirmation`
) || this.hass.localize("ui.panel.config.labs.enable_confirmation")}
</p>
</div>
<div slot="actions">
${createBackupTexts
? html`
<ha-md-list>
<ha-md-list-item>
<span slot="headline">${createBackupTexts.title}</span>
${createBackupTexts.description
? html`
<span slot="supporting-text">
${createBackupTexts.description}
</span>
`
: nothing}
<ha-switch
slot="end"
.checked=${this._createBackup}
@change=${this._createBackupChanged}
></ha-switch>
</ha-md-list-item>
</ha-md-list>
`
: nothing}
<div>
<ha-button appearance="plain" @click=${this._handleCancel}>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
appearance="filled"
variant="brand"
@click=${this._handleConfirm}
>
${this.hass.localize("ui.panel.config.labs.enable")}
</ha-button>
</div>
</div>
</ha-md-dialog>
`;
}
static readonly styles = css`
ha-md-dialog {
--dialog-content-padding: var(--ha-space-6);
}
p {
margin: 0;
color: var(--secondary-text-color);
}
div[slot="actions"] {
display: flex;
flex-direction: column;
padding: 0;
}
ha-md-list {
background: none;
--md-list-item-leading-space: var(--ha-space-6);
--md-list-item-trailing-space: var(--ha-space-6);
margin: 0;
padding: 0;
border-top: var(--ha-border-width-sm) solid var(--divider-color);
}
div[slot="actions"] > div {
display: flex;
justify-content: flex-end;
gap: var(--ha-space-2);
padding: var(--ha-space-4) var(--ha-space-6);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"dialog-labs-preview-feature-enable": DialogLabsPreviewFeatureEnable;
}
}

View File

@@ -1,113 +0,0 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-md-dialog";
import "../../../components/ha-spinner";
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
import type { HomeAssistant } from "../../../types";
import type { LabsProgressDialogParams } from "./show-dialog-labs-progress";
@customElement("dialog-labs-progress")
export class DialogLabsProgress
extends LitElement
implements HassDialog<LabsProgressDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: LabsProgressDialogParams;
@state() private _open = false;
public async showDialog(params: LabsProgressDialogParams): Promise<void> {
this._params = params;
this._open = true;
}
public closeDialog(): boolean {
this._open = false;
return true;
}
private _handleClosed(): void {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._params) {
return nothing;
}
return html`
<ha-md-dialog
.open=${this._open}
hideActions
scrimClickAction=""
escapeKeyAction=""
@closed=${this._handleClosed}
>
<div slot="content">
<div class="summary">
<ha-spinner></ha-spinner>
<div class="content">
<p class="heading">
${this.hass.localize(
"ui.panel.config.labs.progress.creating_backup"
)}
</p>
<p class="description">
${this.hass.localize(
this._params.enabled
? "ui.panel.config.labs.progress.backing_up_before_enabling"
: "ui.panel.config.labs.progress.backing_up_before_disabling"
)}
</p>
</div>
</div>
</div>
</ha-md-dialog>
`;
}
static readonly styles = css`
ha-md-dialog {
--dialog-content-padding: var(--ha-space-6);
}
.summary {
display: flex;
flex-direction: row;
column-gap: var(--ha-space-4);
align-items: center;
justify-content: center;
padding: var(--ha-space-4) 0;
}
ha-spinner {
--ha-spinner-size: 60px;
flex-shrink: 0;
}
.content {
flex: 1;
min-width: 0;
}
.heading {
font-size: var(--ha-font-size-xl);
line-height: var(--ha-line-height-condensed);
color: var(--primary-text-color);
margin: 0 0 var(--ha-space-1);
}
.description {
font-size: var(--ha-font-size-m);
line-height: var(--ha-line-height-condensed);
letter-spacing: 0.25px;
color: var(--secondary-text-color);
margin: 0;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"dialog-labs-progress": DialogLabsProgress;
}
}

View File

@@ -1,550 +0,0 @@
import { mdiFlask, mdiHelpCircle, mdiOpenInNew } from "@mdi/js";
import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import type { LocalizeFunc } from "../../../common/translations/localize";
import { extractSearchParam } from "../../../common/url/search-params";
import { domainToName } from "../../../data/integration";
import {
labsUpdatePreviewFeature,
subscribeLabFeatures,
} from "../../../data/labs";
import type { LabPreviewFeature } from "../../../data/labs";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../types";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { brandsUrl } from "../../../util/brands-url";
import { showToast } from "../../../util/toast";
import { documentationUrl } from "../../../util/documentation-url";
import { haStyle } from "../../../resources/styles";
import { showLabsPreviewFeatureEnableDialog } from "./show-dialog-labs-preview-feature-enable";
import {
showLabsProgressDialog,
closeLabsProgressDialog,
} from "./show-dialog-labs-progress";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
import "../../../components/ha-markdown";
import "../../../components/ha-switch";
import "../../../layouts/hass-subpage";
@customElement("ha-config-labs")
class HaConfigLabs extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@state() private _preview_features: LabPreviewFeature[] = [];
@state() private _highlightedPreviewFeature?: string;
private _sortedPreviewFeatures = memoizeOne(
(localize: LocalizeFunc, features: LabPreviewFeature[]) =>
// Sort by localized integration name alphabetically
[...features].sort((a, b) =>
domainToName(localize, a.domain).localeCompare(
domainToName(localize, b.domain)
)
)
);
public hassSubscribe() {
return [
subscribeLabFeatures(this.hass.connection, (features) => {
// Load title translations for integrations with preview features
const domains = [...new Set(features.map((f) => f.domain))];
this.hass.loadBackendTranslation("title", domains);
this._preview_features = features;
}),
];
}
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
// Load preview_features translations
this.hass.loadBackendTranslation("preview_features");
this._handleUrlParams();
}
private _handleUrlParams(): void {
// Check for feature parameters in URL
const domain = extractSearchParam("domain");
const previewFeature = extractSearchParam("preview_feature");
if (domain && previewFeature) {
const previewFeatureId = `${domain}.${previewFeature}`;
this._highlightedPreviewFeature = previewFeatureId;
// Wait for next render to ensure cards are in DOM
this.updateComplete.then(() => {
this._scrollToPreviewFeature(previewFeatureId);
});
}
}
protected render() {
const sortedFeatures = this._sortedPreviewFeatures(
this.hass.localize,
this._preview_features
);
return html`
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
.header=${this.hass.localize("ui.panel.config.labs.caption")}
>
${sortedFeatures.length
? html`
<a
slot="toolbar-icon"
href=${documentationUrl(this.hass, "/integrations/labs/")}
target="_blank"
rel="noopener noreferrer"
.title=${this.hass.localize("ui.common.help")}
>
<ha-icon-button
.label=${this.hass.localize("ui.common.help")}
.path=${mdiHelpCircle}
></ha-icon-button>
</a>
`
: nothing}
<div class="content">
${!sortedFeatures.length
? html`
<div class="empty">
<ha-svg-icon .path=${mdiFlask}></ha-svg-icon>
<h1>
${this.hass.localize("ui.panel.config.labs.empty.title")}
</h1>
${this.hass.localize(
"ui.panel.config.labs.empty.description"
)}
<a
href=${documentationUrl(this.hass, "/integrations/labs/")}
target="_blank"
rel="noopener noreferrer"
>
${this.hass.localize("ui.panel.config.labs.learn_more")}
<ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon>
</a>
</div>
`
: html`
<ha-card outlined>
<div class="card-content intro-card">
<h1>
${this.hass.localize("ui.panel.config.labs.intro_title")}
</h1>
<p class="intro-text">
${this.hass.localize(
"ui.panel.config.labs.intro_description"
)}
</p>
<ha-alert alert-type="warning">
${this.hass.localize(
"ui.panel.config.labs.intro_warning"
)}
</ha-alert>
</div>
</ha-card>
${sortedFeatures.map((preview_feature) =>
this._renderPreviewFeature(preview_feature)
)}
`}
</div>
</hass-subpage>
`;
}
private _renderPreviewFeature(
preview_feature: LabPreviewFeature
): TemplateResult {
const featureName = this.hass.localize(
`component.${preview_feature.domain}.preview_features.${preview_feature.preview_feature}.name`
);
const description = this.hass.localize(
`component.${preview_feature.domain}.preview_features.${preview_feature.preview_feature}.description`
);
const integrationName = domainToName(
this.hass.localize,
preview_feature.domain
);
const integrationNameWithCustomLabel = !preview_feature.is_built_in
? `${integrationName}${this.hass.localize("ui.panel.config.labs.custom_integration")}`
: integrationName;
const previewFeatureId = `${preview_feature.domain}.${preview_feature.preview_feature}`;
const isHighlighted = this._highlightedPreviewFeature === previewFeatureId;
// Build description with learn more link if available
const descriptionWithLink = preview_feature.learn_more_url
? `${description}\n\n[${this.hass.localize("ui.panel.config.labs.learn_more")}](${preview_feature.learn_more_url})`
: description;
return html`
<ha-card
outlined
data-feature-id=${previewFeatureId}
class=${isHighlighted ? "highlighted" : ""}
>
<div class="card-content">
<div class="card-header">
<img
alt=""
src=${brandsUrl({
domain: preview_feature.domain,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<div class="feature-title">
<span class="integration-name"
>${integrationNameWithCustomLabel}</span
>
<h2>${featureName}</h2>
</div>
</div>
<ha-markdown .content=${descriptionWithLink} breaks></ha-markdown>
</div>
<div class="card-actions">
<div>
${preview_feature.feedback_url
? html`
<ha-button
appearance="plain"
href=${preview_feature.feedback_url}
target="_blank"
rel="noopener noreferrer"
>
${this.hass.localize(
"ui.panel.config.labs.provide_feedback"
)}
</ha-button>
`
: nothing}
${preview_feature.report_issue_url
? html`
<ha-button
appearance="plain"
href=${preview_feature.report_issue_url}
target="_blank"
rel="noopener noreferrer"
>
${this.hass.localize("ui.panel.config.labs.report_issue")}
</ha-button>
`
: nothing}
</div>
<ha-button
appearance="filled"
.variant=${preview_feature.enabled ? "danger" : "brand"}
@click=${this._handleToggle}
.preview_feature=${preview_feature}
>
${this.hass.localize(
preview_feature.enabled
? "ui.panel.config.labs.disable"
: "ui.panel.config.labs.enable"
)}
</ha-button>
</div>
</ha-card>
`;
}
private _scrollToPreviewFeature(previewFeatureId: string): void {
const card = this.shadowRoot?.querySelector(
`[data-feature-id="${previewFeatureId}"]`
) as HTMLElement;
if (card) {
card.scrollIntoView({ behavior: "smooth", block: "center" });
// Clear highlight after animation
setTimeout(() => {
this._highlightedPreviewFeature = undefined;
}, 3000);
}
}
private async _handleToggle(ev: Event): Promise<void> {
const buttonEl = ev.currentTarget as HTMLElement & {
preview_feature: LabPreviewFeature;
};
const preview_feature = buttonEl.preview_feature;
const enabled = !preview_feature.enabled;
const previewFeatureId = `${preview_feature.domain}.${preview_feature.preview_feature}`;
if (enabled) {
// Show custom enable dialog with backup option
showLabsPreviewFeatureEnableDialog(this, {
preview_feature,
previewFeatureId,
onConfirm: async (shouldCreateBackup) => {
await this._performToggle(
previewFeatureId,
enabled,
shouldCreateBackup
);
},
});
return;
}
// Show simple confirmation dialog for disable
const confirmed = await showConfirmationDialog(this, {
title: this.hass.localize("ui.panel.config.labs.disable_title"),
text:
this.hass.localize(
`component.${preview_feature.domain}.preview_features.${preview_feature.preview_feature}.disable_confirmation`
) || this.hass.localize("ui.panel.config.labs.disable_confirmation"),
confirmText: this.hass.localize("ui.panel.config.labs.disable"),
dismissText: this.hass.localize("ui.common.cancel"),
destructive: true,
});
if (!confirmed) {
return;
}
await this._performToggle(previewFeatureId, enabled, false);
}
private async _performToggle(
previewFeatureId: string,
enabled: boolean,
createBackup: boolean
): Promise<void> {
if (createBackup) {
showLabsProgressDialog(this, { enabled });
}
const parts = previewFeatureId.split(".", 2);
if (parts.length !== 2) {
showToast(this, {
message: this.hass.localize("ui.common.unknown_error"),
});
return;
}
const [domain, preview_feature] = parts;
try {
await labsUpdatePreviewFeature(
this.hass,
domain,
preview_feature,
enabled,
createBackup
);
} catch (err: any) {
if (createBackup) {
closeLabsProgressDialog();
}
const errorMessage =
err?.message || this.hass.localize("ui.common.unknown_error");
showToast(this, {
message: this.hass.localize(
enabled
? "ui.panel.config.labs.enable_failed"
: "ui.panel.config.labs.disable_failed",
{ error: errorMessage }
),
});
return;
}
// Close dialog before showing success toast
if (createBackup) {
closeLabsProgressDialog();
}
// Show success toast - collection will auto-update via labs_updated event
showToast(this, {
message: this.hass.localize(
enabled
? "ui.panel.config.labs.enabled_success"
: "ui.panel.config.labs.disabled_success"
),
});
}
static styles = [
haStyle,
css`
:host {
display: block;
}
.content {
max-width: 800px;
margin: 0 auto;
padding: var(--ha-space-4);
min-height: calc(100vh - 64px);
display: flex;
flex-direction: column;
}
.content:has(.empty) {
justify-content: center;
}
ha-card {
margin-bottom: var(--ha-space-4);
position: relative;
transition: box-shadow 0.3s ease;
}
ha-card.highlighted {
animation: highlight-fade 2.5s ease-out forwards;
}
@keyframes highlight-fade {
0% {
box-shadow:
0 0 0 var(--ha-border-width-md) var(--primary-color),
0 0 var(--ha-shadow-blur-lg) rgba(var(--rgb-primary-color), 0.4);
}
100% {
box-shadow:
0 0 0 var(--ha-border-width-md) transparent,
0 0 0 transparent;
}
}
/* Intro card */
.intro-card {
display: flex;
flex-direction: column;
gap: var(--ha-space-4);
}
.intro-card h1 {
margin: 0;
}
.intro-text {
margin: 0 0 var(--ha-space-3);
}
/* Feature cards */
.card-content {
padding: var(--ha-space-4);
}
.card-header {
display: flex;
gap: var(--ha-space-3);
margin-bottom: var(--ha-space-4);
align-items: flex-start;
}
.card-header img {
width: 38px;
height: 38px;
flex-shrink: 0;
margin-top: 2px;
}
.feature-title {
flex: 1;
min-width: 0;
}
.feature-title h2 {
margin: 0;
line-height: 1.3;
}
.integration-name {
display: block;
margin-bottom: 2px;
font-size: 14px;
color: var(--secondary-text-color);
}
/* Empty state */
.empty {
max-width: 500px;
margin: 0 auto;
padding: var(--ha-space-12) var(--ha-space-4);
text-align: center;
}
.empty ha-svg-icon {
width: 120px;
height: 120px;
color: var(--secondary-text-color);
opacity: 0.3;
}
.empty h1 {
margin: var(--ha-space-6) 0 var(--ha-space-4);
}
.empty p {
margin: 0 0 var(--ha-space-6);
font-size: 16px;
line-height: 24px;
color: var(--secondary-text-color);
}
.empty a {
display: inline-flex;
align-items: center;
gap: var(--ha-space-1);
color: var(--primary-color);
text-decoration: none;
font-weight: 500;
}
.empty a:hover {
text-decoration: underline;
}
.empty a:focus-visible {
outline: var(--ha-border-width-md) solid var(--primary-color);
outline-offset: 2px;
border-radius: var(--ha-border-radius-sm);
}
.empty a ha-svg-icon {
width: 16px;
height: 16px;
opacity: 1;
}
/* Card actions */
.card-actions {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: var(--ha-space-2);
padding: var(--ha-space-2);
border-top: var(--ha-border-width-sm) solid var(--divider-color);
}
.card-actions > div {
display: flex;
flex-wrap: wrap;
gap: var(--ha-space-2);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-labs": HaConfigLabs;
}
}

View File

@@ -1,22 +0,0 @@
import { fireEvent } from "../../../common/dom/fire_event";
import type { LabPreviewFeature } from "../../../data/labs";
export interface LabsPreviewFeatureEnableDialogParams {
preview_feature: LabPreviewFeature;
previewFeatureId: string;
onConfirm: (createBackup: boolean) => void;
}
export const loadLabsPreviewFeatureEnableDialog = () =>
import("./dialog-labs-preview-feature-enable");
export const showLabsPreviewFeatureEnableDialog = (
element: HTMLElement,
params: LabsPreviewFeatureEnableDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-labs-preview-feature-enable",
dialogImport: loadLabsPreviewFeatureEnableDialog,
dialogParams: params,
});
};

View File

@@ -1,22 +0,0 @@
import { fireEvent } from "../../../common/dom/fire_event";
import { closeDialog } from "../../../dialogs/make-dialog-manager";
export interface LabsProgressDialogParams {
enabled: boolean;
}
export const loadLabsProgressDialog = () => import("./dialog-labs-progress");
export const showLabsProgressDialog = (
element: HTMLElement,
dialogParams: LabsProgressDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-labs-progress",
dialogImport: loadLabsProgressDialog,
dialogParams,
});
};
export const closeLabsProgressDialog = () =>
closeDialog("dialog-labs-progress");

View File

@@ -62,7 +62,7 @@ export class DialogLovelaceDashboardDetail extends LitElement {
return nothing;
}
const defaultPanelUrlPath =
this.hass.systemData?.default_panel || DEFAULT_PANEL;
this.hass.systemData?.defaultPanel || DEFAULT_PANEL;
const titleInvalid = !this._data.title || !this._data.title.trim();
return html`
@@ -260,7 +260,7 @@ export class DialogLovelaceDashboardDetail extends LitElement {
return;
}
const defaultPanel = this.hass.systemData?.default_panel || DEFAULT_PANEL;
const defaultPanel = this.hass.systemData?.defaultPanel || DEFAULT_PANEL;
// Add warning dialog to saying that this will change the default dashboard for all users
const confirm = await showConfirmationDialog(this, {
title: this.hass.localize(
@@ -284,7 +284,7 @@ export class DialogLovelaceDashboardDetail extends LitElement {
saveFrontendSystemData(this.hass.connection, "core", {
...this.hass.systemData,
default_panel: urlPath === defaultPanel ? undefined : urlPath,
defaultPanel: urlPath === defaultPanel ? undefined : urlPath,
});
}

View File

@@ -404,7 +404,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
return html` <hass-loading-screen></hass-loading-screen> `;
}
const defaultPanel = this.hass.systemData?.default_panel || DEFAULT_PANEL;
const defaultPanel = this.hass.systemData?.defaultPanel || DEFAULT_PANEL;
return html`
<hass-tabs-subpage-data-table

View File

@@ -1265,10 +1265,6 @@ export class HaSceneEditor extends PreventUnsavedMixin(
display: block;
margin-bottom: 24px;
}
ha-alert ha-button[slot="action"] {
width: max-content;
white-space: nowrap;
}
ha-fab.dirty {
bottom: 0;
}

View File

@@ -2,7 +2,6 @@ import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { documentationUrl } from "../../../util/documentation-url";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import { createCloseHeading } from "../../../components/ha-dialog";
@@ -15,6 +14,8 @@ import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import type { TagDetailDialogParams } from "./show-dialog-tag-detail";
const TAG_BASE = "https://www.home-assistant.io/tag/";
@customElement("dialog-tag-detail")
class DialogTagDetail
extends LitElement
@@ -121,7 +122,7 @@ class DialogTagDetail
</div>
<div id="qr">
<ha-qr-code
.data=${`${documentationUrl(this.hass, "/tag/")}${this._params!.entry!.id}`}
.data=${`${TAG_BASE}${this._params!.entry!.id}`}
center-image="/static/icons/favicon-192x192.png"
error-correction-level="quartile"
scale="5"

View File

@@ -46,39 +46,12 @@ export class EnergyElectricityViewStrategy extends ReactiveElement {
const hasBattery = prefs.energy_sources.some(
(source) => source.type === "battery"
);
const hasPowerSources = prefs.energy_sources.find(
(source) =>
(source.type === "solar" && source.stat_rate) ||
(source.type === "battery" && source.stat_rate) ||
(source.type === "grid" && source.power?.length)
);
const hasPowerDevices = prefs.device_consumption.find(
(device) => device.stat_rate
);
view.cards!.push({
type: "energy-compare",
collection_key: "energy_dashboard",
});
if (hasPowerSources) {
if (hasPowerDevices) {
view.cards!.push({
title: hass.localize("ui.panel.energy.cards.power_sankey_title"),
type: "power-sankey",
collection_key: collectionKey,
grid_options: {
columns: 24,
},
});
}
view.cards!.push({
title: hass.localize("ui.panel.energy.cards.power_sources_graph_title"),
type: "power-sources-graph",
collection_key: collectionKey,
});
}
// Only include if we have a grid or battery.
if (hasGrid || hasBattery) {
view.cards!.push({

View File

@@ -1,151 +0,0 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/entity/ha-entities-picker";
import "../../../components/ha-button";
import "../../../components/ha-dialog-footer";
import "../../../components/ha-wa-dialog";
import type { HomeFrontendSystemData } from "../../../data/frontend";
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import type { EditHomeDialogParams } from "./show-dialog-edit-home";
@customElement("dialog-edit-home")
export class DialogEditHome
extends LitElement
implements HassDialog<EditHomeDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: EditHomeDialogParams;
@state() private _config?: HomeFrontendSystemData;
@state() private _open = false;
@state() private _submitting = false;
public showDialog(params: EditHomeDialogParams): void {
this._params = params;
this._config = { ...params.config };
this._open = true;
}
public closeDialog(): boolean {
this._open = false;
return true;
}
private _dialogClosed(): void {
this._params = undefined;
this._config = undefined;
this._submitting = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._params) {
return nothing;
}
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
.headerTitle=${this.hass.localize("ui.panel.home.editor.title")}
@closed=${this._dialogClosed}
>
<p class="description">
${this.hass.localize("ui.panel.home.editor.description")}
</p>
<ha-entities-picker
autofocus
.hass=${this.hass}
.value=${this._config?.favorite_entities || []}
.label=${this.hass.localize(
"ui.panel.lovelace.editor.strategy.home.favorite_entities"
)}
.placeholder=${this.hass.localize(
"ui.panel.lovelace.editor.strategy.home.add_favorite_entity"
)}
.helper=${this.hass.localize(
"ui.panel.home.editor.favorite_entities_helper"
)}
reorder
allow-custom-entity
@value-changed=${this._favoriteEntitiesChanged}
></ha-entities-picker>
<ha-dialog-footer slot="footer">
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this.closeDialog}
.disabled=${this._submitting}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._save}
.disabled=${this._submitting}
>
${this.hass.localize("ui.common.save")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
`;
}
private _favoriteEntitiesChanged(ev: CustomEvent): void {
const entities = ev.detail.value as string[];
this._config = {
...this._config,
favorite_entities: entities.length > 0 ? entities : undefined,
};
}
private async _save(): Promise<void> {
if (!this._params || !this._config) {
return;
}
this._submitting = true;
try {
await this._params.saveConfig(this._config);
this.closeDialog();
} catch (err: any) {
// eslint-disable-next-line no-console
console.error("Failed to save home configuration:", err);
} finally {
this._submitting = false;
}
}
static styles = [
haStyleDialog,
css`
ha-wa-dialog {
--dialog-content-padding: var(--ha-space-6);
}
.description {
margin: 0 0 var(--ha-space-4) 0;
color: var(--secondary-text-color);
}
ha-entities-picker {
display: block;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"dialog-edit-home": DialogEditHome;
}
}

View File

@@ -1,20 +0,0 @@
import { fireEvent } from "../../../common/dom/fire_event";
import type { HomeFrontendSystemData } from "../../../data/frontend";
export interface EditHomeDialogParams {
config: HomeFrontendSystemData;
saveConfig: (config: HomeFrontendSystemData) => Promise<void>;
}
export const loadEditHomeDialog = () => import("./dialog-edit-home");
export const showEditHomeDialog = (
element: HTMLElement,
params: EditHomeDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-edit-home",
dialogImport: loadEditHomeDialog,
dialogParams: params,
});
};

View File

@@ -3,18 +3,18 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { debounce } from "../../common/util/debounce";
import { deepEqual } from "../../common/util/deep-equal";
import {
fetchFrontendSystemData,
saveFrontendSystemData,
type HomeFrontendSystemData,
} from "../../data/frontend";
import type { LovelaceDashboardStrategyConfig } from "../../data/lovelace/config/types";
import type { HomeAssistant, PanelInfo, Route } from "../../types";
import { showToast } from "../../util/toast";
import "../lovelace/hui-root";
import { generateLovelaceDashboardStrategy } from "../lovelace/strategies/get-strategy";
import type { Lovelace } from "../lovelace/types";
import { showEditHomeDialog } from "./dialogs/show-dialog-edit-home";
import { showAlertDialog } from "../lovelace/custom-card-helpers";
const HOME_LOVELACE_CONFIG: LovelaceDashboardStrategyConfig = {
strategy: {
type: "home",
},
};
@customElement("ha-panel-home")
class PanelHome extends LitElement {
@@ -28,14 +28,12 @@ class PanelHome extends LitElement {
@state() private _lovelace?: Lovelace;
@state() private _config: FrontendSystemData["home"] = {};
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
// Initial setup
if (!this.hasUpdated) {
this.hass.loadFragmentTranslation("lovelace");
this._loadConfig();
this._setLovelace();
return;
}
@@ -97,28 +95,9 @@ class PanelHome extends LitElement {
`;
}
private async _loadConfig() {
try {
const data = await fetchFrontendSystemData(this.hass.connection, "home");
this._config = data || {};
} catch (err) {
// eslint-disable-next-line no-console
console.error("Failed to load favorites:", err);
this._config = {};
}
this._setLovelace();
}
private async _setLovelace() {
const strategyConfig: LovelaceDashboardStrategyConfig = {
strategy: {
type: "home",
favorite_entities: this._config.favorite_entities,
},
};
const config = await generateLovelaceDashboardStrategy(
strategyConfig,
HOME_LOVELACE_CONFIG,
this.hass
);
@@ -142,34 +121,15 @@ class PanelHome extends LitElement {
}
private _setEditMode = () => {
showEditHomeDialog(this, {
config: this._config,
saveConfig: async (config) => {
await this._saveConfig(config);
},
// For now, we just show an alert that edit mode is not supported.
// This will be expanded in the future.
showAlertDialog(this, {
title: "Edit mode not available",
text: "The Home panel does not support edit mode.",
confirmText: this.hass.localize("ui.common.ok"),
});
};
private async _saveConfig(config: HomeFrontendSystemData): Promise<void> {
try {
await saveFrontendSystemData(this.hass.connection, "home", config);
this._config = config || {};
} catch (err: any) {
// eslint-disable-next-line no-console
console.error("Failed to save home configuration:", err);
showToast(this, {
message: this.hass.localize("ui.panel.home.editor.save_failed"),
duration: 0,
dismissable: true,
});
return;
}
showToast(this, {
message: this.hass.localize("ui.common.successfully_saved"),
});
this._setLovelace();
}
static readonly styles: CSSResultGroup = css`
:host {
display: block;

View File

@@ -21,10 +21,7 @@ import {
} from "../../../../data/lovelace_custom_cards";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import {
haStyleDialog,
haStyleDialogFixedTop,
} from "../../../../resources/styles";
import { haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
import "../../badges/hui-badge";
@@ -398,7 +395,6 @@ export class HuiDialogEditBadge
static get styles(): CSSResultGroup {
return [
haStyleDialog,
haStyleDialogFixedTop,
css`
:host {
--code-mirror-max-height: calc(100vh - 176px);
@@ -407,6 +403,8 @@ export class HuiDialogEditBadge
ha-dialog {
--mdc-dialog-max-width: 100px;
--dialog-z-index: 6;
--dialog-surface-position: fixed;
--dialog-surface-top: 40px;
--mdc-dialog-max-width: 90vw;
--dialog-content-padding: 24px 12px;
}

View File

@@ -184,15 +184,19 @@ export class HuiCreateDialogCard
return [
haStyleDialog,
css`
@media all and (max-width: 450px), all and (max-height: 500px) {
/* overrule the ha-style-dialog max-height on small screens */
ha-dialog {
--mdc-dialog-max-height: 100%;
height: 100%;
}
}
@media all and (min-width: 850px) {
ha-dialog {
--mdc-dialog-min-width: 845px;
--mdc-dialog-min-height: calc(
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
);
--mdc-dialog-max-height: calc(
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
);
--mdc-dialog-min-height: calc(100vh - 72px);
--mdc-dialog-max-height: calc(100vh - 72px);
}
}

View File

@@ -21,10 +21,7 @@ import {
} from "../../../../data/lovelace_custom_cards";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import {
haStyleDialog,
haStyleDialogFixedTop,
} from "../../../../resources/styles";
import { haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { showToast } from "../../../../util/toast";
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
@@ -374,7 +371,6 @@ export class HuiDialogEditCard
static get styles(): CSSResultGroup {
return [
haStyleDialog,
haStyleDialogFixedTop,
css`
:host {
--code-mirror-max-height: calc(100vh - 176px);
@@ -383,6 +379,8 @@ export class HuiDialogEditCard
ha-dialog {
--mdc-dialog-max-width: 100px;
--dialog-z-index: 6;
--dialog-surface-position: fixed;
--dialog-surface-top: 40px;
--mdc-dialog-max-width: 90vw;
--dialog-content-padding: 24px 12px;
}

View File

@@ -17,10 +17,7 @@ import "../../../../../components/ha-dialog-header";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-list-item";
import type { LovelaceStrategyConfig } from "../../../../../data/lovelace/config/strategy";
import {
haStyleDialog,
haStyleDialogFixedTop,
} from "../../../../../resources/styles";
import { haStyleDialog } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import { showSaveSuccessToast } from "../../../../../util/toast-saved-success";
import { cleanLegacyStrategyConfig } from "../../../strategies/legacy-strategy";
@@ -222,21 +219,14 @@ class DialogDashboardStrategyEditor extends LitElement {
static get styles(): CSSResultGroup {
return [
haStyleDialog,
haStyleDialogFixedTop,
css`
ha-dialog {
--dialog-content-padding: 0 24px;
--mdc-dialog-min-width: min(
640px,
calc(100vw - var(--safe-area-inset-x))
);
--mdc-dialog-max-width: min(
640px,
calc(100vw - var(--safe-area-inset-x))
);
--mdc-dialog-max-height: calc(
100vh - var(--ha-space-20) - var(--safe-area-inset-y)
);
--dialog-surface-position: fixed;
--dialog-surface-top: 40px;
--mdc-dialog-min-width: min(640px, calc(100% - 32px));
--mdc-dialog-max-width: min(640px, calc(100% - 32px));
--mdc-dialog-max-height: calc(100% - 80px);
}
@media all and (max-width: 450px), all and (max-height: 500px) {
@@ -244,12 +234,9 @@ class DialogDashboardStrategyEditor extends LitElement {
ha-dialog {
height: 100%;
--dialog-surface-top: 0px;
--mdc-dialog-min-width: 100vw;
--mdc-dialog-max-width: 100vw;
--mdc-dialog-min-height: 100vh;
--mdc-dialog-min-height: 100svh;
--mdc-dialog-max-height: 100vh;
--mdc-dialog-max-height: 100svh;
--mdc-dialog-min-width: 100%;
--mdc-dialog-max-width: 100%;
--mdc-dialog-max-height: 100%;
--dialog-content-padding: 8px;
}
}

View File

@@ -30,10 +30,7 @@ import {
} from "../../../../data/lovelace/config/view";
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import {
haStyleDialog,
haStyleDialogFixedTop,
} from "../../../../resources/styles";
import { haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import type { Lovelace } from "../../types";
import { addSection, deleteSection, moveSection } from "../config-util";
@@ -421,8 +418,19 @@ export class HuiDialogEditSection
static get styles(): CSSResultGroup {
return [
haStyleDialog,
haStyleDialogFixedTop,
css`
ha-dialog {
/* Set the top top of the dialog to a fixed position, so it doesnt jump when the content changes size */
--vertical-align-dialog: flex-start;
--dialog-surface-margin-top: 40px;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
/* When in fullscreen dialog should be attached to top */
ha-dialog {
--dialog-surface-margin-top: 0px;
}
}
ha-dialog.yaml-mode {
--dialog-content-padding: 0;
}

View File

@@ -36,10 +36,7 @@ import {
showAlertDialog,
showConfirmationDialog,
} from "../../../../dialogs/generic/show-dialog-box";
import {
haStyleDialog,
haStyleDialogFixedTop,
} from "../../../../resources/styles";
import { haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import "../../components/hui-entity-editor";
import type { Lovelace } from "../../types";
@@ -634,8 +631,19 @@ export class HuiDialogEditView extends LitElement {
static get styles(): CSSResultGroup {
return [
haStyleDialog,
haStyleDialogFixedTop,
css`
ha-dialog {
/* Set the top top of the dialog to a fixed position, so it doesnt jump when the content changes size */
--vertical-align-dialog: flex-start;
--dialog-surface-margin-top: 40px;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
/* When in fullscreen dialog should be attached to top */
ha-dialog {
--dialog-surface-margin-top: 0px;
}
}
ha-dialog.yaml-mode {
--dialog-content-padding: 0;
}

View File

@@ -16,10 +16,7 @@ import "../../../../components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import type { LovelaceViewHeaderConfig } from "../../../../data/lovelace/config/view";
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
import {
haStyleDialog,
haStyleDialogFixedTop,
} from "../../../../resources/styles";
import { haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import "./hui-view-header-settings-editor";
import type { EditViewHeaderDialogParams } from "./show-edit-view-header-dialog";
@@ -204,8 +201,19 @@ export class HuiDialogEditViewHeader extends LitElement {
static get styles(): CSSResultGroup {
return [
haStyleDialog,
haStyleDialogFixedTop,
css`
ha-dialog {
/* Set the top top of the dialog to a fixed position, so it doesnt jump when the content changes size */
--vertical-align-dialog: flex-start;
--dialog-surface-margin-top: 40px;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
/* When in fullscreen dialog should be attached to top */
ha-dialog {
--dialog-surface-margin-top: 0px;
}
}
ha-dialog.yaml-mode {
--dialog-content-padding: 0;
}

View File

@@ -188,13 +188,6 @@ export const getMyRedirects = (): Redirects => ({
helpers: {
redirect: "/config/helpers",
},
labs: {
redirect: "/config/labs",
params: {
domain: "string?",
preview_feature: "string?",
},
},
tags: {
component: "tag",
redirect: "/config/tags",

View File

@@ -7,7 +7,6 @@ import "../../components/ha-settings-row";
import "../../components/ha-switch";
import type { CoreFrontendUserData } from "../../data/frontend";
import { saveFrontendUserData } from "../../data/frontend";
import { documentationUrl } from "../../util/documentation-url";
import type { HomeAssistant } from "../../types";
@customElement("ha-advanced-mode-row")
@@ -32,10 +31,7 @@ class AdvancedModeRow extends LitElement {
<span slot="description">
${this.hass.localize("ui.panel.profile.advanced_mode.description")}
<a
href=${documentationUrl(
this.hass,
"/blog/2019/07/17/release-96/#advanced-mode"
)}
href="https://www.home-assistant.io/blog/2019/07/17/release-96/#advanced-mode"
target="_blank"
rel="noreferrer"
>${this.hass.localize("ui.panel.profile.advanced_mode.link_promo")}

View File

@@ -25,7 +25,7 @@ class HaPickDashboardRow extends LitElement {
}
protected render(): TemplateResult {
const value = this.hass.userData?.default_panel || USE_SYSTEM_VALUE;
const value = this.hass.userData?.defaultPanel || USE_SYSTEM_VALUE;
return html`
<ha-settings-row .narrow=${this.narrow}>
<span slot="heading">
@@ -84,12 +84,12 @@ class HaPickDashboardRow extends LitElement {
return;
}
const urlPath = value === USE_SYSTEM_VALUE ? undefined : value;
if (urlPath === this.hass.userData?.default_panel) {
if (urlPath === this.hass.userData?.defaultPanel) {
return;
}
saveFrontendUserData(this.hass.connection, "core", {
...this.hass.userData,
default_panel: urlPath,
defaultPanel: urlPath,
});
}
}

View File

@@ -15,7 +15,7 @@ const renderMarkdown = async (
allowSvg?: boolean;
allowDataUrl?: boolean;
} = {}
): Promise<string[]> => {
): Promise<string> => {
if (!whiteListNormal) {
whiteListNormal = {
...getDefaultWhiteList(),
@@ -53,43 +53,38 @@ const renderMarkdown = async (
whiteList.a.push("download");
}
marked.setOptions(markedOptions);
const tokens = marked.lexer(content);
return tokens.map((token) =>
filterXSS(marked.parser([token]), {
whiteList,
onTagAttr: (
tag: string,
name: string,
value: string
): string | undefined => {
// Override the default `onTagAttr` behavior to only render
// our markdown checkboxes.
// Returning undefined causes the default measure to be taken
// in the xss library.
if (tag === "input") {
if (
(name === "type" && value === "checkbox") ||
name === "checked" ||
name === "disabled"
) {
return undefined;
}
return "";
}
return filterXSS(await marked(content, markedOptions), {
whiteList,
onTagAttr: (
tag: string,
name: string,
value: string
): string | undefined => {
// Override the default `onTagAttr` behavior to only render
// our markdown checkboxes.
// Returning undefined causes the default measure to be taken
// in the xss library.
if (tag === "input") {
if (
hassOptions.allowDataUrl &&
tag === "a" &&
name === "href" &&
value.startsWith("data:")
(name === "type" && value === "checkbox") ||
name === "checked" ||
name === "disabled"
) {
return `href="${value}"`;
return undefined;
}
return undefined;
},
})
);
return "";
}
if (
hassOptions.allowDataUrl &&
tag === "a" &&
name === "href" &&
value.startsWith("data:")
) {
return `href="${value}"`;
}
return undefined;
},
});
};
const api = {

View File

@@ -142,11 +142,6 @@ export const haStyleDialog = css`
--mdc-dialog-max-width: 600px;
--mdc-dialog-max-width: min(600px, 95vw);
--justify-action-buttons: space-between;
--dialog-container-padding: var(--safe-area-inset-top, var(--ha-space-0))
var(--safe-area-inset-right, var(--ha-space-0))
var(--safe-area-inset-bottom, var(--ha-space-0))
var(--safe-area-inset-left, var(--ha-space-0));
--dialog-surface-padding: var(--ha-space-0);
}
ha-dialog .form {
@@ -166,11 +161,9 @@ export const haStyleDialog = css`
--mdc-dialog-min-height: 100svh;
--mdc-dialog-max-height: 100vh;
--mdc-dialog-max-height: 100svh;
--dialog-container-padding: var(--ha-space-0);
--dialog-surface-padding: var(--safe-area-inset-top, var(--ha-space-0))
var(--safe-area-inset-right, var(--ha-space-0))
var(--safe-area-inset-bottom, var(--ha-space-0))
var(--safe-area-inset-left, var(--ha-space-0));
--dialog-surface-padding: var(--safe-area-inset-top)
var(--safe-area-inset-right) var(--safe-area-inset-bottom)
var(--safe-area-inset-left);
--vertical-align-dialog: flex-end;
--ha-dialog-border-radius: var(--ha-border-radius-square);
}
@@ -180,37 +173,6 @@ export const haStyleDialog = css`
}
`;
export const haStyleDialogFixedTop = css`
ha-dialog {
/* Pin dialog to top so it doesn't jump when content changes size */
--vertical-align-dialog: flex-start;
--dialog-surface-margin-top: var(--ha-space-10);
--mdc-dialog-max-height: calc(
100vh - var(--dialog-surface-margin-top) - var(--ha-space-2) - var(
--safe-area-inset-y,
var(--ha-space-0)
)
);
--mdc-dialog-max-height: calc(
100svh - var(--dialog-surface-margin-top) - var(--ha-space-2) - var(
--safe-area-inset-y,
var(--ha-space-0)
)
);
}
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-dialog {
/* When in fullscreen, dialog should be attached to top */
--dialog-surface-margin-top: var(--ha-space-0);
--mdc-dialog-min-height: 100vh;
--mdc-dialog-min-height: 100svh;
--mdc-dialog-max-height: 100vh;
--mdc-dialog-max-height: 100svh;
}
}
`;
export const haStyleScrollbar = css`
.ha-scrollbar::-webkit-scrollbar {
width: 0.4rem;

View File

@@ -32,9 +32,6 @@ export const mainStyles = css`
--safe-area-inset-bottom: var(--app-safe-area-inset-bottom, env(safe-area-inset-bottom, 0));
--safe-area-inset-left: var(--app-safe-area-inset-left, env(safe-area-inset-left, 0));
--safe-area-inset-right: var(--app-safe-area-inset-right, env(safe-area-inset-right, 0));
--safe-area-inset-y: calc(var(--safe-area-inset-top, 0px) + var(--safe-area-inset-bottom, 0px));
--safe-area-inset-x: calc(var(--safe-area-inset-left, 0px) + var(--safe-area-inset-right, 0px));
}
`;

View File

@@ -2220,14 +2220,6 @@
"migrate_to_user_data": "This will change the sidebar on all the devices you are logged in to. To create a sidebar per device, you should use a different user for that device."
},
"panel": {
"home": {
"editor": {
"title": "Edit home page",
"description": "Configure your home page display preferences.",
"favorite_entities_helper": "Display your favorite entities. Home Assistant will still suggest based on commonly used up to 8 slots.",
"save_failed": "Failed to save home page configuration"
}
},
"my": {
"not_supported": "This redirect is not supported by your Home Assistant instance. Check the {link} for the supported redirects and the version they where introduced.",
"component_not_loaded": "This redirect is not supported by your Home Assistant instance. You need the integration {integration} to use this redirect.",
@@ -6800,45 +6792,6 @@
"intro": "Share anonymized information from your installation to help make Home Assistant better and help us convince manufacturers to add local control and privacy-focused features.",
"download_device_info": "Preview device analytics"
},
"labs": {
"caption": "Labs",
"custom_integration": "Custom integration",
"description": "Preview new features",
"description_enabled": "Preview features are enabled",
"intro_title": "Home Assistant Labs",
"intro_subtitle": "Preview upcoming features",
"intro_description": "Home Assistant Labs lets you preview new features we're actively working on. These features are stable and fully functional, but may not yet include the complete feature set we envision. We're still refining the design and approach based on your feedback.",
"intro_warning": "Preview features may change or be replaced with different solutions in future releases.",
"empty": {
"title": "No preview features available",
"description": "There are currently no preview features available to try. Check back in future releases for new features!"
},
"learn_more": "Learn more",
"provide_feedback": "Provide feedback",
"report_issue": "Report issue",
"enable": "Enable",
"disable": "Disable",
"enable_title": "Enable preview feature?",
"enable_confirmation": "This preview feature is stable but may evolve based on feedback. Enabling it may affect your setup, and changes may persist after disabling.",
"disable_title": "Disable preview feature?",
"disable_confirmation": "This will disable the preview feature, but changes made while it was active may still affect your setup.",
"enabled_success": "Preview feature enabled",
"disabled_success": "Preview feature disabled",
"enable_failed": "Enabling preview feature failed: {error}",
"disable_failed": "Disabling preview feature failed: {error}",
"progress": {
"creating_backup": "Creating backup",
"backing_up_before_enabling": "Home Assistant is being backed up before enabling the Home Assistant Labs preview feature",
"backing_up_before_disabling": "Home Assistant is being backed up before disabling the Home Assistant Labs preview feature"
},
"create_backup": {
"automatic": "Automatic backup before enabling",
"automatic_description_last": "Last automatic backup {relative_time}.",
"automatic_description_none": "No automatic backup yet.",
"manual": "Create manual backup before enabling",
"manual_description": "Includes Home Assistant settings and history."
}
},
"network": {
"caption": "Network",
"description": "External access {state}",
@@ -9517,8 +9470,7 @@
"energy_devices_detail_graph_title": "Individual devices detail usage",
"energy_sankey_title": "Energy flow",
"energy_top_consumers_title": "Top consumers",
"power_sankey_title": "Current power flow",
"power_sources_graph_title": "Power sources"
"power_sankey_title": "Current power flow"
}
},
"history": {

146
yarn.lock
View File

@@ -4945,106 +4945,106 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/eslint-plugin@npm:8.46.3":
version: 8.46.3
resolution: "@typescript-eslint/eslint-plugin@npm:8.46.3"
"@typescript-eslint/eslint-plugin@npm:8.47.0":
version: 8.47.0
resolution: "@typescript-eslint/eslint-plugin@npm:8.47.0"
dependencies:
"@eslint-community/regexpp": "npm:^4.10.0"
"@typescript-eslint/scope-manager": "npm:8.46.3"
"@typescript-eslint/type-utils": "npm:8.46.3"
"@typescript-eslint/utils": "npm:8.46.3"
"@typescript-eslint/visitor-keys": "npm:8.46.3"
"@typescript-eslint/scope-manager": "npm:8.47.0"
"@typescript-eslint/type-utils": "npm:8.47.0"
"@typescript-eslint/utils": "npm:8.47.0"
"@typescript-eslint/visitor-keys": "npm:8.47.0"
graphemer: "npm:^1.4.0"
ignore: "npm:^7.0.0"
natural-compare: "npm:^1.4.0"
ts-api-utils: "npm:^2.1.0"
peerDependencies:
"@typescript-eslint/parser": ^8.46.3
"@typescript-eslint/parser": ^8.47.0
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/0c1eb81a43f1d04fdd79c4e59f9f0687b86735ae6c98d94fe5eb021da2f83e0e2426a2922fe94296fb0a9ab131d53fe4cde8b54d0948d7b23e01e648a318bd1c
checksum: 10/53d86116a39429c0cdde5969f9ea2cf712f7c7cb2ed023088a876686a4771df131dbefda7645ba0724e2a5a0532e14bdffcb92c708060a46a8607dc5243083d1
languageName: node
linkType: hard
"@typescript-eslint/parser@npm:8.46.3":
version: 8.46.3
resolution: "@typescript-eslint/parser@npm:8.46.3"
"@typescript-eslint/parser@npm:8.47.0":
version: 8.47.0
resolution: "@typescript-eslint/parser@npm:8.47.0"
dependencies:
"@typescript-eslint/scope-manager": "npm:8.46.3"
"@typescript-eslint/types": "npm:8.46.3"
"@typescript-eslint/typescript-estree": "npm:8.46.3"
"@typescript-eslint/visitor-keys": "npm:8.46.3"
"@typescript-eslint/scope-manager": "npm:8.47.0"
"@typescript-eslint/types": "npm:8.47.0"
"@typescript-eslint/typescript-estree": "npm:8.47.0"
"@typescript-eslint/visitor-keys": "npm:8.47.0"
debug: "npm:^4.3.4"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/d36edeba9ce37d219115fb101a4496bca2685969b217d0f64c0c255867a8793a8b41a95b86e26775a09b3abbb7c5b93ef712ea9a0fba3d055dcf385b17825075
checksum: 10/e7213296a27b78511b8c9b2627ff6530d0eb31e6b076eef6f34f11ca7fbcb7e998a2fa079bfc1563a53f9d88326aa4af995241bcdf08a15c3e5be0d13fdff2d7
languageName: node
linkType: hard
"@typescript-eslint/project-service@npm:8.46.3":
version: 8.46.3
resolution: "@typescript-eslint/project-service@npm:8.46.3"
"@typescript-eslint/project-service@npm:8.47.0":
version: 8.47.0
resolution: "@typescript-eslint/project-service@npm:8.47.0"
dependencies:
"@typescript-eslint/tsconfig-utils": "npm:^8.46.3"
"@typescript-eslint/types": "npm:^8.46.3"
"@typescript-eslint/tsconfig-utils": "npm:^8.47.0"
"@typescript-eslint/types": "npm:^8.47.0"
debug: "npm:^4.3.4"
peerDependencies:
typescript: ">=4.8.4 <6.0.0"
checksum: 10/2f041dfc664209b6a213cf585df28d0913ddf81916b83119c897a10dd9ad20dcd0ee3c523ee95440f498da6ba9d6e50cf08852418c0a2ebddd92c7a7cd295736
checksum: 10/e2f935dae66ce27e6c0cce8b750da0e8fe84b6e0fa248bf8210b84eec3c4d2e2679a878185f445ce507d132215a676dcf8a21d47ab70c547da47ede000a128e1
languageName: node
linkType: hard
"@typescript-eslint/scope-manager@npm:8.46.3":
version: 8.46.3
resolution: "@typescript-eslint/scope-manager@npm:8.46.3"
"@typescript-eslint/scope-manager@npm:8.47.0":
version: 8.47.0
resolution: "@typescript-eslint/scope-manager@npm:8.47.0"
dependencies:
"@typescript-eslint/types": "npm:8.46.3"
"@typescript-eslint/visitor-keys": "npm:8.46.3"
checksum: 10/6bb6c3210bfcca59cf60860b51bfae8d28b01d074a8608b6f24b3290952ff74103e08d390d11cbf613812fca04aa55ad14ad9da04c3041e23acdca235ab1ff78
"@typescript-eslint/types": "npm:8.47.0"
"@typescript-eslint/visitor-keys": "npm:8.47.0"
checksum: 10/e97ae0f746f6bb5706181a973bcc0c1268706ef7e8c18594b37168bb0b41b1673d3f0ba1a2575ee3bd121066500fdc75af313f6ad283198942a5cdb65ade7621
languageName: node
linkType: hard
"@typescript-eslint/tsconfig-utils@npm:8.46.3, @typescript-eslint/tsconfig-utils@npm:^8.46.3":
version: 8.46.3
resolution: "@typescript-eslint/tsconfig-utils@npm:8.46.3"
"@typescript-eslint/tsconfig-utils@npm:8.47.0, @typescript-eslint/tsconfig-utils@npm:^8.47.0":
version: 8.47.0
resolution: "@typescript-eslint/tsconfig-utils@npm:8.47.0"
peerDependencies:
typescript: ">=4.8.4 <6.0.0"
checksum: 10/e7a16eadf79483d4b61dee56a08d032bafe26d44d634e7863a5875dbb44393570896641272a4e9810f4eac76a4109f59ad667b036d7627ef1647dc672ea19c5e
checksum: 10/7f44441da3778928937419f8ebc62939538cf30087e56c0ca56f599ce98111b82f496902a9e15d713822b9cd14b17937d57b722468450a48748f8e50fd7161af
languageName: node
linkType: hard
"@typescript-eslint/type-utils@npm:8.46.3":
version: 8.46.3
resolution: "@typescript-eslint/type-utils@npm:8.46.3"
"@typescript-eslint/type-utils@npm:8.47.0":
version: 8.47.0
resolution: "@typescript-eslint/type-utils@npm:8.47.0"
dependencies:
"@typescript-eslint/types": "npm:8.46.3"
"@typescript-eslint/typescript-estree": "npm:8.46.3"
"@typescript-eslint/utils": "npm:8.46.3"
"@typescript-eslint/types": "npm:8.47.0"
"@typescript-eslint/typescript-estree": "npm:8.47.0"
"@typescript-eslint/utils": "npm:8.47.0"
debug: "npm:^4.3.4"
ts-api-utils: "npm:^2.1.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/b29cd001c715033ec9cd5fdf2723915f1b4c6c9342283ed00d20e4b942117625facba9a2cf3914b06633c2af9a167430f8f134323627adb0be85f73da4e89d72
checksum: 10/07dcdd1ac071bbaf87b6b320d107129787a62cc403ce78e081cbe5e2ed0c576d660654e4117e6224c4c23d46919d7130b70801835d2fc41d9344c47ff946ce81
languageName: node
linkType: hard
"@typescript-eslint/types@npm:8.46.3, @typescript-eslint/types@npm:^8.46.3":
version: 8.46.3
resolution: "@typescript-eslint/types@npm:8.46.3"
checksum: 10/3de35df2ec2f2937c8f6eb262cd49f34500a18d01e0d8da6f348afd621f6c222c41d4ea15203ebbf0bd59814aa2b4c83fde7eb6d4aad1fa1514ee7a742887c6a
"@typescript-eslint/types@npm:8.47.0, @typescript-eslint/types@npm:^8.47.0":
version: 8.47.0
resolution: "@typescript-eslint/types@npm:8.47.0"
checksum: 10/fc42416c01c512cfe1533bdf521925bca999adc68ffefa246e48552783f1fe9d22487d912611c5cb35fca481604aae3cab88279a53ce76c7cd7510b76775c078
languageName: node
linkType: hard
"@typescript-eslint/typescript-estree@npm:8.46.3":
version: 8.46.3
resolution: "@typescript-eslint/typescript-estree@npm:8.46.3"
"@typescript-eslint/typescript-estree@npm:8.47.0":
version: 8.47.0
resolution: "@typescript-eslint/typescript-estree@npm:8.47.0"
dependencies:
"@typescript-eslint/project-service": "npm:8.46.3"
"@typescript-eslint/tsconfig-utils": "npm:8.46.3"
"@typescript-eslint/types": "npm:8.46.3"
"@typescript-eslint/visitor-keys": "npm:8.46.3"
"@typescript-eslint/project-service": "npm:8.47.0"
"@typescript-eslint/tsconfig-utils": "npm:8.47.0"
"@typescript-eslint/types": "npm:8.47.0"
"@typescript-eslint/visitor-keys": "npm:8.47.0"
debug: "npm:^4.3.4"
fast-glob: "npm:^3.3.2"
is-glob: "npm:^4.0.3"
@@ -5053,32 +5053,32 @@ __metadata:
ts-api-utils: "npm:^2.1.0"
peerDependencies:
typescript: ">=4.8.4 <6.0.0"
checksum: 10/b55cf72fe3dff0b9bdf9b1793e43fdb2789fa6d706ba7d69fb94801bea82041056a95659bd8fe1e6f026787b2e8d0f8d060149841095a0a82044e3469b8d82cd
checksum: 10/a480e83f1fca8a389642cbb18855ef25214c4765694b1d4a74051d2653a4fbbbf3a3cc4e544d1ecb79d49958fbf819246043c0d823d4384aa1c7b5ff79d02fcc
languageName: node
linkType: hard
"@typescript-eslint/utils@npm:8.46.3":
version: 8.46.3
resolution: "@typescript-eslint/utils@npm:8.46.3"
"@typescript-eslint/utils@npm:8.47.0":
version: 8.47.0
resolution: "@typescript-eslint/utils@npm:8.47.0"
dependencies:
"@eslint-community/eslint-utils": "npm:^4.7.0"
"@typescript-eslint/scope-manager": "npm:8.46.3"
"@typescript-eslint/types": "npm:8.46.3"
"@typescript-eslint/typescript-estree": "npm:8.46.3"
"@typescript-eslint/scope-manager": "npm:8.47.0"
"@typescript-eslint/types": "npm:8.47.0"
"@typescript-eslint/typescript-estree": "npm:8.47.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/369c962bc20a2a6022ef4533ad55ab4e3d2403e7e200505b29fae6f0b8fc99be8fe149d929781f5ead0d3f88f2c74904f60aaa3771e6773e2b7dd8f61f07a534
checksum: 10/e165bbcaaafb88761f12272bc4b3be1631d8a8ea319765c80cfe5bf7a5858f437486eeae177643baa213570a664f0254b41bf0541e9238b57080bb30d1a2c8ab
languageName: node
linkType: hard
"@typescript-eslint/visitor-keys@npm:8.46.3":
version: 8.46.3
resolution: "@typescript-eslint/visitor-keys@npm:8.46.3"
"@typescript-eslint/visitor-keys@npm:8.47.0":
version: 8.47.0
resolution: "@typescript-eslint/visitor-keys@npm:8.47.0"
dependencies:
"@typescript-eslint/types": "npm:8.46.3"
"@typescript-eslint/types": "npm:8.47.0"
eslint-visitor-keys: "npm:^4.2.1"
checksum: 10/02659a4cc4780d677907ed7e356e18b941e0ed18883acfda0d74d3e388144f90aa098b8fcdc2f4c01e9e6b60ac6154d1afb009feb6169c483260a5c8b4891171
checksum: 10/1e184cdebc4ab15da8a46ae2624ba4543c6bea83ced80a1602da99b72c00b5f6ea913ae021823c555a35a65bb9a9df09d119713998c44b00eba25e1407844294
languageName: node
linkType: hard
@@ -9373,7 +9373,7 @@ __metadata:
tinykeys: "npm:3.0.0"
ts-lit-plugin: "npm:2.0.2"
typescript: "npm:5.9.3"
typescript-eslint: "npm:8.46.3"
typescript-eslint: "npm:8.47.0"
ua-parser-js: "npm:2.0.6"
vite-tsconfig-paths: "npm:5.1.4"
vitest: "npm:4.0.8"
@@ -14295,18 +14295,18 @@ __metadata:
languageName: node
linkType: hard
"typescript-eslint@npm:8.46.3":
version: 8.46.3
resolution: "typescript-eslint@npm:8.46.3"
"typescript-eslint@npm:8.47.0":
version: 8.47.0
resolution: "typescript-eslint@npm:8.47.0"
dependencies:
"@typescript-eslint/eslint-plugin": "npm:8.46.3"
"@typescript-eslint/parser": "npm:8.46.3"
"@typescript-eslint/typescript-estree": "npm:8.46.3"
"@typescript-eslint/utils": "npm:8.46.3"
"@typescript-eslint/eslint-plugin": "npm:8.47.0"
"@typescript-eslint/parser": "npm:8.47.0"
"@typescript-eslint/typescript-estree": "npm:8.47.0"
"@typescript-eslint/utils": "npm:8.47.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/2f77eb70c8fd6ec4920d5abf828ef28007df8ff94605246a4ca918fadb996a83f7fb82510a1de69fad7f0159ee8f15246d467ebc42df20a4585919cb6b401715
checksum: 10/159dad98535dafd68c6228fae4aaf9e02d65d9ac3b02ddf0356b56ce72651dd9860e8bf9e0ee22532d77dd382d7f810e1bf1dd41c3fab381f627a08580c5117e
languageName: node
linkType: hard