20230926.0 (#18037)

This commit is contained in:
Bram Kragten 2023-09-26 23:56:14 +02:00 committed by GitHub
commit 60345f3fe8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
168 changed files with 4539 additions and 2842 deletions

View File

@ -2,9 +2,7 @@
You are amazing! Thanks for contributing to our project! You are amazing! Thanks for contributing to our project!
Please, DO NOT DELETE ANY TEXT from this template! (unless instructed). Please, DO NOT DELETE ANY TEXT from this template! (unless instructed).
--> -->
## Breaking change ## Breaking change
<!-- <!--
If your PR contains a breaking change for existing users, it is important If your PR contains a breaking change for existing users, it is important
to tell them what breaks, how to make it work again and why we did this. to tell them what breaks, how to make it work again and why we did this.
@ -13,8 +11,8 @@
Note: Remove this section if this PR is NOT a breaking change. Note: Remove this section if this PR is NOT a breaking change.
--> -->
## Proposed change
## Proposed change
<!-- <!--
Describe the big picture of your changes here to communicate to the Describe the big picture of your changes here to communicate to the
maintainers why we should accept this pull request. If it fixes a bug maintainers why we should accept this pull request. If it fixes a bug
@ -22,8 +20,8 @@
in the additional information section. in the additional information section.
--> -->
## Type of change
## Type of change
<!-- <!--
What type of change does your PR introduce to the Home Assistant frontend? What type of change does your PR introduce to the Home Assistant frontend?
NOTE: Please, check only 1! box! NOTE: Please, check only 1! box!
@ -38,7 +36,6 @@
- [ ] Code quality improvements to existing code or addition of tests - [ ] Code quality improvements to existing code or addition of tests
## Example configuration ## Example configuration
<!-- <!--
Supplying a configuration snippet, makes it easier for a maintainer to test Supplying a configuration snippet, makes it easier for a maintainer to test
your PR. your PR.
@ -49,7 +46,6 @@
``` ```
## Additional information ## Additional information
<!-- <!--
Details are important, and help maintainers processing your PR. Details are important, and help maintainers processing your PR.
Please be sure to fill out additional details, if applicable. Please be sure to fill out additional details, if applicable.
@ -60,7 +56,6 @@
- Link to documentation pull request: - Link to documentation pull request:
## Checklist ## Checklist
<!-- <!--
Put an `x` in the boxes that apply. You can also fill these out after Put an `x` in the boxes that apply. You can also fill these out after
creating the PR. If you're unsure about any of them, don't hesitate to ask. creating the PR. If you're unsure about any of them, don't hesitate to ask.

View File

@ -21,7 +21,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.6.0 uses: actions/checkout@v4.1.0
with: with:
ref: dev ref: dev
@ -57,7 +57,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.6.0 uses: actions/checkout@v4.1.0
with: with:
ref: master ref: master

View File

@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.6.0 uses: actions/checkout@v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3.8.1 uses: actions/setup-node@v3.8.1
with: with:
@ -55,7 +55,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.6.0 uses: actions/checkout@v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3.8.1 uses: actions/setup-node@v3.8.1
with: with:
@ -73,7 +73,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.6.0 uses: actions/checkout@v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3.8.1 uses: actions/setup-node@v3.8.1
with: with:
@ -91,7 +91,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.6.0 uses: actions/checkout@v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3.8.1 uses: actions/setup-node@v3.8.1
with: with:

View File

@ -23,7 +23,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3.6.0 uses: actions/checkout@v4.1.0
with: with:
# We must fetch at least the immediate parents so that if this is # We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head. # a pull request then we can checkout the head.

View File

@ -22,7 +22,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.6.0 uses: actions/checkout@v4.1.0
with: with:
ref: dev ref: dev
@ -58,7 +58,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.6.0 uses: actions/checkout@v4.1.0
with: with:
ref: master ref: master

View File

@ -16,7 +16,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.6.0 uses: actions/checkout@v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3.8.1 uses: actions/setup-node@v3.8.1

View File

@ -21,7 +21,7 @@ jobs:
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview') if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.6.0 uses: actions/checkout@v4.1.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3.8.1 uses: actions/setup-node@v3.8.1

View File

@ -20,7 +20,7 @@ jobs:
contents: write contents: write
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v3.6.0 uses: actions/checkout@v4.1.0
- name: Set up Python ${{ env.PYTHON_VERSION }} - name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v4 uses: actions/setup-python@v4

View File

@ -23,7 +23,7 @@ jobs:
contents: write # Required to upload release assets contents: write # Required to upload release assets
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v3.6.0 uses: actions/checkout@v4.1.0
- name: Verify version - name: Verify version
uses: home-assistant/actions/helpers/verify-version@master uses: home-assistant/actions/helpers/verify-version@master
@ -74,7 +74,7 @@ jobs:
echo "home-assistant-frontend==$version" > ./requirements.txt echo "home-assistant-frontend==$version" > ./requirements.txt
- name: Build wheels - name: Build wheels
uses: home-assistant/wheels@2023.04.0 uses: home-assistant/wheels@2023.09.1
with: with:
abi: cp311 abi: cp311
tag: musllinux_1_2 tag: musllinux_1_2

View File

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v3.6.0 uses: actions/checkout@v4.1.0
- name: Upload Translations - name: Upload Translations
run: | run: |

View File

@ -1,3 +1,4 @@
CLA.md CLA.md
CODE_OF_CONDUCT.md CODE_OF_CONDUCT.md
LICENSE.md LICENSE.md
PULL_REQUEST_TEMPLATE.md

View File

@ -100,6 +100,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
useBuiltIns: latestBuild ? false : "entry", useBuiltIns: latestBuild ? false : "entry",
corejs: latestBuild ? false : { version: "3.32", proposals: true }, corejs: latestBuild ? false : { version: "3.32", proposals: true },
bugfixes: true, bugfixes: true,
shippedProposals: true,
}, },
], ],
"@babel/preset-typescript", "@babel/preset-typescript",

View File

@ -6,6 +6,8 @@ import presetEnv from "@babel/preset-env";
import compilationTargets from "@babel/helper-compilation-targets"; import compilationTargets from "@babel/helper-compilation-targets";
import coreJSCompat from "core-js-compat"; import coreJSCompat from "core-js-compat";
import { logPlugin } from "@babel/preset-env/lib/debug.js"; import { logPlugin } from "@babel/preset-env/lib/debug.js";
// eslint-disable-next-line import/no-relative-packages
import shippedPolyfills from "../node_modules/babel-plugin-polyfill-corejs3/lib/shipped-proposals.js";
import { babelOptions } from "./bundle.cjs"; import { babelOptions } from "./bundle.cjs";
const detailsOpen = (heading) => const detailsOpen = (heading) =>
@ -26,6 +28,22 @@ const dummyAPI = {
targets: () => ({}), targets: () => ({}),
}; };
// Generate filter function based on proposal/method inputs
// Copied and adapted from babel-plugin-polyfill-corejs3/esm/index.mjs
const polyfillFilter = (method, proposals, shippedProposals) => (name) => {
if (proposals || method === "entry-global") return true;
if (shippedProposals && shippedPolyfills.default.has(name)) {
return true;
}
if (name.startsWith("esnext.")) {
const esName = `es.${name.slice(7)}`;
// If its imaginative esName is not in latest compat data, it means the proposal is not stage 4
return esName in coreJSCompat.data;
}
return true;
};
// Log the plugins and polyfills for each build environment
for (const buildType of ["Modern", "Legacy"]) { for (const buildType of ["Modern", "Legacy"]) {
const browserslistEnv = buildType.toLowerCase(); const browserslistEnv = buildType.toLowerCase();
const babelOpts = babelOptions({ latestBuild: browserslistEnv === "modern" }); const babelOpts = babelOptions({ latestBuild: browserslistEnv === "modern" });
@ -46,7 +64,13 @@ for (const buildType of ["Modern", "Legacy"]) {
const targets = compilationTargets.default(babelOpts?.targets, { const targets = compilationTargets.default(babelOpts?.targets, {
browserslistEnv, browserslistEnv,
}); });
const polyfillList = coreJSCompat({ targets }).list; const polyfillList = coreJSCompat({ targets }).list.filter(
polyfillFilter(
`${presetEnvOpts.useBuiltIns}-global`,
presetEnvOpts?.corejs?.proposals,
presetEnvOpts?.shippedProposals
)
);
console.log( console.log(
"The following %i polyfills may be injected by Babel:\n", "The following %i polyfills may be injected by Babel:\n",
polyfillList.length polyfillList.length

View File

@ -165,6 +165,7 @@ const createWebpackConfig = ({
"lit/directives/guard$": "lit/directives/guard.js", "lit/directives/guard$": "lit/directives/guard.js",
"lit/directives/cache$": "lit/directives/cache.js", "lit/directives/cache$": "lit/directives/cache.js",
"lit/directives/repeat$": "lit/directives/repeat.js", "lit/directives/repeat$": "lit/directives/repeat.js",
"lit/directives/live$": "lit/directives/live.js",
"lit/polyfill-support$": "lit/polyfill-support.js", "lit/polyfill-support$": "lit/polyfill-support.js",
"@lit-labs/virtualizer/layouts/grid": "@lit-labs/virtualizer/layouts/grid":
"@lit-labs/virtualizer/layouts/grid.js", "@lit-labs/virtualizer/layouts/grid.js",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -32,6 +32,8 @@ import { HassElement } from "../../../../src/state/hass-element";
import { castContext } from "../cast_context"; import { castContext } from "../cast_context";
import "./hc-launch-screen"; import "./hc-launch-screen";
const DEFAULT_STRATEGY = "original-states";
let resourcesLoaded = false; let resourcesLoaded = false;
@customElement("hc-main") @customElement("hc-main")
export class HcMain extends HassElement { export class HcMain extends HassElement {
@ -258,7 +260,7 @@ export class HcMain extends HassElement {
{ {
strategy: { strategy: {
type: "energy", type: "energy",
options: { show_date_selection: true }, show_date_selection: true,
}, },
}, },
], ],
@ -320,10 +322,10 @@ export class HcMain extends HassElement {
this._handleNewLovelaceConfig( this._handleNewLovelaceConfig(
await generateLovelaceDashboardStrategy( await generateLovelaceDashboardStrategy(
{ {
hass: this.hass!, type: DEFAULT_STRATEGY,
narrow: false,
}, },
"original-states" this.hass!,
{ narrow: false }
) )
); );
} }

View File

@ -8,25 +8,67 @@
"src": "/static/icons/favicon-192x192.png", "src": "/static/icons/favicon-192x192.png",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png", "type": "image/png",
"purpose": "maskable any" "purpose": "any"
}, },
{ {
"src": "/static/icons/favicon-384x384.png", "src": "/static/icons/favicon-384x384.png",
"sizes": "384x384", "sizes": "384x384",
"type": "image/png", "type": "image/png",
"purpose": "maskable any" "purpose": "any"
}, },
{ {
"src": "/static/icons/favicon-512x512.png", "src": "/static/icons/favicon-512x512.png",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png", "type": "image/png",
"purpose": "maskable any" "purpose": "any"
}, },
{ {
"src": "/static/icons/favicon-1024x1024.png", "src": "/static/icons/favicon-1024x1024.png",
"sizes": "1024x1024", "sizes": "1024x1024",
"type": "image/png", "type": "image/png",
"purpose": "maskable any" "purpose": "any"
},
{
"src": "/static/icons/maskable_icon-48x48.png",
"sizes": "48x48",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/static/icons/maskable_icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/static/icons/maskable_icon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/static/icons/maskable_icon-128x128.png",
"sizes": "128x128",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/static/icons/maskable_icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/static/icons/maskable_icon-384x384.png",
"sizes": "384x384",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/static/icons/maskable_icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
} }
], ],
"lang": "en-US", "lang": "en-US",

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,4 @@
# Note!
Note, the assets in this folder, are not part of the CC license this repository is shipped in.
All rights reserved.

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View File

@ -2,30 +2,86 @@
title: "Logo" title: "Logo"
--- ---
# Using our logo # Our logo
As a community, we are proud of our logo. Follow these guidelines to ensure it always looks its best. Our logo follows Google's material design spec and uses the blue interface color. As a community, we are proud of our logo. Follow these guidelines to ensure it always represents the identity of the Home Assistant project and community the best way possible.
[Download Logo](https://github.com/home-assistant/assets/tree/master/logo) [Download Logo](https://github.com/home-assistant/assets/tree/master/logo)
![Logo](/images/logo.png) ![Logo](/images/brand/logo.png)
## Using the icon Please note that this logo is not released under the CC license. All rights reserved.
Our icon is a shorter and most used version of our logo. The icon can exist without the wordmark, the wordmark should never exist without the icon. # Design
![Logo variants](/images/logo-variants.png) At the core of the Home Assistant logomark is the Blue House with Antenna, the three most recognizable and distinct features of the previous logo throughout the past decade.
## Using the right variant ### Blue
The pretty blue logo with a background shadow, pictured top left, is our primary logo. It should only be used with black, white, and non-duotone photography. Blue feels stable and essential. A bright sky blue is joyful, clear, and free of clouds.
When needed you can use our logo without a shadow, as seen as the second variant. ### House
The outlined logo should only be used on packaging. Of all possible combinations of shapes, a home is best abstracted in the shape of a structure with a pitched roof. With the vast amount of logos based on this shape, the best we can do is to make it more iconic. The house is further simplified - there is no gable and there is no chimney - to an orthogonal shape with an elegant and deliberate proportion.
## Exclusion zone ### Antenna
The logo needs some personal space. It's exclusion zone is equal to a quarter the height of the icon. Call it a tree, a set of nodes, a PCB, or an antenna. The antenna is the most recognizable and memorable part of the previous Home Assistant logo, and is an easily understandable symbol that conveys technologies that are smart, connected, and growing evergreen.
![Clearspace](/images/clearspace.png) # Usage
The default variation is the static colored wordmark in horizontal layout and dark text on a light background.
## Layout variations
![Logo layout variants](/images/brand/logo-layout-variants.png)
The default layout is the wordmark in horizontal layout. It provides the clearest context to the brand identity of Home Assistant.
Use the logomark variant when the context is clear that the logo is about Home Assistant. For example, inside the Home Assistant app where users are already aware of where they are at, the logomark variant without the wordmark can be used. The logomark can exist without the wordmark, however, the wordmark should never exist without the icon.
Use the wordmark in vertical layout when the space available has an aspect ratio less than 4:3. For example, in a square space on a t-shirt where a logo is needed, since there is no established context of Home Assistant, the wordmark in vertical layout should be used.
Lastly, use the wordmark in vertical layout with small logomark when Home Assistant is displayed in context of other Home Assistant-related projects. For example, in a flowchart showing the voice pipeline, use this layout for Home Assistant and its other related projects.
## Color variations, backgrounds, and placement
The default color is the colored version on light background with dark text.
For backgrounds that are dark, for example, when it is used on a page in a dark theme, use the colored version on dark background with light text.
In printed materials where color is unavailable, use the monochrome color variations.
On background that are dark or photographic, use the light monochrome color on dark background variation.
On backgrounds that are light or photographic, use the colored version. Do not use the monochrome variations.
Do not enclose the logmark in a square or color or any confined backgrounds, except in specific situations enforced by another company's marketplace guidelines, for example, an iOS app icon.
Do not add drop shadow to the logomark or the wordmark. If legibility is compromised due to the background, change the background to provide more contrast, or in last resort, add a heavily blurred drop shadaow.
It should only be used with black, white, and non-duotone photography.
Unlike the previous version of our logo, no outlined variants are available. Use the monochrome variants in those spaces.
### Exclusion zone
The logo needs some personal space. Its exclusion zone is equal to a quarter the height of the icon.
![Space clearance for the wordmark](/images/brand/logo-exclusion-zone.png)
## Animation
The default is the static variant.
Use the animated variant only for introductory purposes, for example, in the beginning of a video or on a loading screen.
Use the animated with sound variant only when sound is warranted in the user's context. For example, use it in the beginning of a video since sounds are expected in a video, but do not use it on a loading screen since sounds are not expected in a user interface.
Do not repeat the logo animation.
## Sizes and app icon variants
Special variants are created for specific contexts.
Use the tiny variants when the logomark is used in a very small space (16x16 dp), for example, the favicon of the Home Assistant website, a notification on Android, or the menubar of macOS.

View File

@ -1,10 +1,10 @@
import { mdiHomeAssistant } from "@mdi/js";
import { css, html, LitElement, TemplateResult } from "lit"; import { css, html, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import "../../../../src/components/ha-card"; import "../../../../src/components/ha-card";
import "../../../../src/components/ha-chip"; import "../../../../src/components/ha-chip";
import "../../../../src/components/ha-chip-set"; import "../../../../src/components/ha-chip-set";
import "../../../../src/components/ha-svg-icon"; import "../../../../src/components/ha-svg-icon";
import { mdiHomeAssistant } from "../../../../src/resources/home-assistant-logo-svg";
const chips: { const chips: {
icon?: string; icon?: string;

View File

@ -343,7 +343,7 @@ export class DemoEntityState extends LitElement {
const columns: DataTableColumnContainer<EntityRowData> = { const columns: DataTableColumnContainer<EntityRowData> = {
icon: { icon: {
title: "Icon", title: "Icon",
template: (_, entry) => html` template: (entry) => html`
<state-badge <state-badge
.stateObj=${entry.stateObj} .stateObj=${entry.stateObj}
.stateColor=${true} .stateColor=${true}
@ -360,7 +360,7 @@ export class DemoEntityState extends LitElement {
title: "State", title: "State",
width: "20%", width: "20%",
sortable: true, sortable: true,
template: (_, entry) => template: (entry) =>
html`${computeStateDisplay( html`${computeStateDisplay(
hass.localize, hass.localize,
entry.stateObj, entry.stateObj,
@ -371,14 +371,14 @@ export class DemoEntityState extends LitElement {
}, },
device_class: { device_class: {
title: "Device class", title: "Device class",
template: (dc) => html`${dc ?? "-"}`, template: (entry) => html`${entry.device_class ?? "-"}`,
width: "20%", width: "20%",
filterable: true, filterable: true,
sortable: true, sortable: true,
}, },
domain: { domain: {
title: "Domain", title: "Domain",
template: (_, entry) => html`${computeDomain(entry.entity_id)}`, template: (entry) => html`${computeDomain(entry.entity_id)}`,
width: "20%", width: "20%",
filterable: true, filterable: true,
sortable: true, sortable: true,

View File

@ -7,7 +7,6 @@ import {
mdiDocker, mdiDocker,
mdiExclamationThick, mdiExclamationThick,
mdiFlask, mdiFlask,
mdiHomeAssistant,
mdiKey, mdiKey,
mdiLinkLock, mdiLinkLock,
mdiNetwork, mdiNetwork,
@ -22,7 +21,7 @@ import {
mdiPound, mdiPound,
mdiShield, mdiShield,
} from "@mdi/js"; } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
@ -40,11 +39,11 @@ import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-switch"; import "../../../../src/components/ha-switch";
import { import {
AddonCapability, AddonCapability,
fetchHassioAddonChangelog,
fetchHassioAddonInfo,
HassioAddonDetails, HassioAddonDetails,
HassioAddonSetOptionParams, HassioAddonSetOptionParams,
HassioAddonSetSecurityParams, HassioAddonSetSecurityParams,
fetchHassioAddonChangelog,
fetchHassioAddonInfo,
installHassioAddon, installHassioAddon,
rebuildLocalAddon, rebuildLocalAddon,
restartHassioAddon, restartHassioAddon,
@ -56,9 +55,9 @@ import {
validateHassioAddonOption, validateHassioAddonOption,
} from "../../../../src/data/hassio/addon"; } from "../../../../src/data/hassio/addon";
import { import {
HassioStats,
extractApiErrorMessage, extractApiErrorMessage,
fetchHassioStats, fetchHassioStats,
HassioStats,
} from "../../../../src/data/hassio/common"; } from "../../../../src/data/hassio/common";
import { import {
StoreAddon, StoreAddon,
@ -69,6 +68,7 @@ import {
showAlertDialog, showAlertDialog,
showConfirmationDialog, showConfirmationDialog,
} from "../../../../src/dialogs/generic/show-dialog-box"; } from "../../../../src/dialogs/generic/show-dialog-box";
import { mdiHomeAssistant } from "../../../../src/resources/home-assistant-logo-svg";
import { haStyle } from "../../../../src/resources/styles"; import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../../src/types"; import { HomeAssistant, Route } from "../../../../src/types";
import { bytesToString } from "../../../../src/util/bytes-to-string"; import { bytesToString } from "../../../../src/util/bytes-to-string";

View File

@ -49,6 +49,10 @@ import { showHassioCreateBackupDialog } from "../dialogs/backup/show-dialog-hass
import { supervisorTabs } from "../hassio-tabs"; import { supervisorTabs } from "../hassio-tabs";
import { hassioStyle } from "../resources/hassio-style"; import { hassioStyle } from "../resources/hassio-style";
type BackupItem = HassioBackup & {
secondary: string;
};
@customElement("hassio-backups") @customElement("hassio-backups")
export class HassioBackups extends LitElement { export class HassioBackups extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@ -117,15 +121,15 @@ export class HassioBackups extends LitElement {
} }
private _columns = memoizeOne( private _columns = memoizeOne(
(narrow: boolean): DataTableColumnContainer => ({ (narrow: boolean): DataTableColumnContainer<BackupItem> => ({
name: { name: {
title: this.supervisor.localize("backup.name"), title: this.supervisor.localize("backup.name"),
main: true, main: true,
sortable: true, sortable: true,
filterable: true, filterable: true,
grows: true, grows: true,
template: (entry: string, backup: any) => template: (backup) =>
html`${entry || backup.slug} html`${backup.name || backup.slug}
<div class="secondary">${backup.secondary}</div>`, <div class="secondary">${backup.secondary}</div>`,
}, },
size: { size: {
@ -134,7 +138,7 @@ export class HassioBackups extends LitElement {
hidden: narrow, hidden: narrow,
filterable: true, filterable: true,
sortable: true, sortable: true,
template: (entry: number) => Math.ceil(entry * 10) / 10 + " MB", template: (backup) => Math.ceil(backup.size * 10) / 10 + " MB",
}, },
location: { location: {
title: this.supervisor.localize("backup.location"), title: this.supervisor.localize("backup.location"),
@ -142,8 +146,8 @@ export class HassioBackups extends LitElement {
hidden: narrow, hidden: narrow,
filterable: true, filterable: true,
sortable: true, sortable: true,
template: (entry: string | null) => template: (backup) =>
entry || this.supervisor.localize("backup.data_disk"), backup.location || this.supervisor.localize("backup.data_disk"),
}, },
date: { date: {
title: this.supervisor.localize("backup.created"), title: this.supervisor.localize("backup.created"),
@ -152,8 +156,8 @@ export class HassioBackups extends LitElement {
hidden: narrow, hidden: narrow,
filterable: true, filterable: true,
sortable: true, sortable: true,
template: (entry: string) => template: (backup) =>
relativeTime(new Date(entry), this.hass.locale), relativeTime(new Date(backup.date), this.hass.locale),
}, },
secondary: { secondary: {
title: "", title: "",
@ -163,7 +167,7 @@ export class HassioBackups extends LitElement {
}) })
); );
private _backupData = memoizeOne((backups: HassioBackup[]) => private _backupData = memoizeOne((backups: HassioBackup[]): BackupItem[] =>
backups.map((backup) => ({ backups.map((backup) => ({
...backup, ...backup,
secondary: this._computeBackupContent(backup), secondary: this._computeBackupContent(backup),

View File

@ -1,13 +1,13 @@
import { mdiFolder, mdiHomeAssistant, mdiPuzzle } from "@mdi/js"; import { mdiFolder, mdiPuzzle } from "@mdi/js";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input"; import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import { import {
css,
CSSResultGroup, CSSResultGroup,
html,
LitElement, LitElement,
nothing,
TemplateResult, TemplateResult,
css,
html,
nothing,
} from "lit"; } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version"; import { atLeastVersion } from "../../../src/common/config/version";
@ -24,6 +24,7 @@ import {
HassioPartialBackupCreateParams, HassioPartialBackupCreateParams,
} from "../../../src/data/hassio/backup"; } from "../../../src/data/hassio/backup";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { mdiHomeAssistant } from "../../../src/resources/home-assistant-logo-svg";
import { import {
HomeAssistant, HomeAssistant,
TranslationDict, TranslationDict,

View File

@ -1,5 +1,4 @@
import "@material/mwc-button"; import "@material/mwc-button";
import { mdiHomeAssistant } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
@ -13,6 +12,7 @@ import {
HassioSupervisorInfo, HassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor"; } from "../../../src/data/hassio/supervisor";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { mdiHomeAssistant } from "../../../src/resources/home-assistant-logo-svg";
import { haStyle } from "../../../src/resources/styles"; import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types"; import { HomeAssistant } from "../../../src/types";
import { hassioStyle } from "../resources/hassio-style"; import { hassioStyle } from "../resources/hassio-style";

View File

@ -25,35 +25,35 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@babel/runtime": "7.22.15", "@babel/runtime": "7.23.1",
"@braintree/sanitize-url": "6.0.4", "@braintree/sanitize-url": "6.0.4",
"@codemirror/autocomplete": "6.9.0", "@codemirror/autocomplete": "6.9.1",
"@codemirror/commands": "6.2.5", "@codemirror/commands": "6.2.5",
"@codemirror/language": "6.9.0", "@codemirror/language": "6.9.1",
"@codemirror/legacy-modes": "6.3.3", "@codemirror/legacy-modes": "6.3.3",
"@codemirror/search": "6.5.2", "@codemirror/search": "6.5.4",
"@codemirror/state": "6.2.1", "@codemirror/state": "6.2.1",
"@codemirror/view": "6.17.1", "@codemirror/view": "6.20.2",
"@egjs/hammerjs": "2.0.17", "@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.10.0", "@formatjs/intl-datetimeformat": "6.10.3",
"@formatjs/intl-displaynames": "6.5.0", "@formatjs/intl-displaynames": "6.5.2",
"@formatjs/intl-getcanonicallocales": "2.2.1", "@formatjs/intl-getcanonicallocales": "2.2.1",
"@formatjs/intl-listformat": "7.4.0", "@formatjs/intl-listformat": "7.4.2",
"@formatjs/intl-locale": "3.3.2", "@formatjs/intl-locale": "3.3.4",
"@formatjs/intl-numberformat": "8.7.0", "@formatjs/intl-numberformat": "8.7.2",
"@formatjs/intl-pluralrules": "5.2.4", "@formatjs/intl-pluralrules": "5.2.6",
"@formatjs/intl-relativetimeformat": "11.2.4", "@formatjs/intl-relativetimeformat": "11.2.6",
"@fullcalendar/core": "6.1.8", "@fullcalendar/core": "6.1.9",
"@fullcalendar/daygrid": "6.1.8", "@fullcalendar/daygrid": "6.1.9",
"@fullcalendar/interaction": "6.1.8", "@fullcalendar/interaction": "6.1.9",
"@fullcalendar/list": "6.1.8", "@fullcalendar/list": "6.1.9",
"@fullcalendar/luxon3": "6.1.8", "@fullcalendar/luxon3": "6.1.9",
"@fullcalendar/timegrid": "6.1.8", "@fullcalendar/timegrid": "6.1.9",
"@lezer/highlight": "1.1.6", "@lezer/highlight": "1.1.6",
"@lit-labs/context": "0.4.0", "@lit-labs/context": "0.4.1",
"@lit-labs/motion": "1.0.4", "@lit-labs/motion": "1.0.4",
"@lit-labs/virtualizer": "2.0.7", "@lit-labs/virtualizer": "2.0.7",
"@lrnwebcomponents/simple-tooltip": "7.0.16", "@lrnwebcomponents/simple-tooltip": "7.0.18",
"@material/chips": "=14.0.0-canary.53b3cad2f.0", "@material/chips": "=14.0.0-canary.53b3cad2f.0",
"@material/data-table": "=14.0.0-canary.53b3cad2f.0", "@material/data-table": "=14.0.0-canary.53b3cad2f.0",
"@material/mwc-button": "0.27.0", "@material/mwc-button": "0.27.0",
@ -62,6 +62,7 @@
"@material/mwc-dialog": "0.27.0", "@material/mwc-dialog": "0.27.0",
"@material/mwc-drawer": "0.27.0", "@material/mwc-drawer": "0.27.0",
"@material/mwc-fab": "0.27.0", "@material/mwc-fab": "0.27.0",
"@material/mwc-floating-label": "0.27.0",
"@material/mwc-formfield": "0.27.0", "@material/mwc-formfield": "0.27.0",
"@material/mwc-icon-button": "0.27.0", "@material/mwc-icon-button": "0.27.0",
"@material/mwc-linear-progress": "0.27.0", "@material/mwc-linear-progress": "0.27.0",
@ -79,7 +80,7 @@
"@material/mwc-top-app-bar": "0.27.0", "@material/mwc-top-app-bar": "0.27.0",
"@material/mwc-top-app-bar-fixed": "0.27.0", "@material/mwc-top-app-bar-fixed": "0.27.0",
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0", "@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
"@material/web": "=1.0.0-pre.16", "@material/web": "=1.0.0-pre.17",
"@mdi/js": "7.2.96", "@mdi/js": "7.2.96",
"@mdi/svg": "7.2.96", "@mdi/svg": "7.2.96",
"@polymer/iron-flex-layout": "3.0.1", "@polymer/iron-flex-layout": "3.0.1",
@ -93,8 +94,8 @@
"@polymer/paper-toast": "3.0.1", "@polymer/paper-toast": "3.0.1",
"@polymer/polymer": "3.5.1", "@polymer/polymer": "3.5.1",
"@thomasloven/round-slider": "0.6.0", "@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.1.6", "@vaadin/combo-box": "24.1.9",
"@vaadin/vaadin-themable-mixin": "24.1.6", "@vaadin/vaadin-themable-mixin": "24.1.9",
"@vibrant/color": "3.2.1-alpha.1", "@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1", "@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1", "@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@ -102,10 +103,10 @@
"@webcomponents/scoped-custom-element-registry": "0.0.9", "@webcomponents/scoped-custom-element-registry": "0.0.9",
"@webcomponents/webcomponentsjs": "2.8.0", "@webcomponents/webcomponentsjs": "2.8.0",
"app-datepicker": "5.1.1", "app-datepicker": "5.1.1",
"chart.js": "4.3.3", "chart.js": "4.4.0",
"comlink": "4.4.1", "comlink": "4.4.1",
"core-js": "3.32.2", "core-js": "3.32.2",
"cropperjs": "1.6.0", "cropperjs": "1.6.1",
"date-fns": "2.30.0", "date-fns": "2.30.0",
"date-fns-tz": "2.0.0", "date-fns-tz": "2.0.0",
"deep-clone-simple": "1.1.1", "deep-clone-simple": "1.1.1",
@ -115,13 +116,13 @@
"hls.js": "1.4.12", "hls.js": "1.4.12",
"home-assistant-js-websocket": "8.2.0", "home-assistant-js-websocket": "8.2.0",
"idb-keyval": "6.2.1", "idb-keyval": "6.2.1",
"intl-messageformat": "10.5.0", "intl-messageformat": "10.5.3",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"leaflet": "1.9.4", "leaflet": "1.9.4",
"leaflet-draw": "1.0.4", "leaflet-draw": "1.0.4",
"lit": "2.8.0", "lit": "2.8.0",
"luxon": "3.4.3", "luxon": "3.4.3",
"marked": "7.0.5", "marked": "9.0.3",
"memoize-one": "6.0.0", "memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1", "node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "0.3.2", "proxy-polyfill": "0.3.2",
@ -137,9 +138,9 @@
"tinykeys": "2.1.0", "tinykeys": "2.1.0",
"tsparticles-engine": "2.12.0", "tsparticles-engine": "2.12.0",
"tsparticles-preset-links": "2.12.0", "tsparticles-preset-links": "2.12.0",
"ua-parser-js": "1.0.35", "ua-parser-js": "1.0.36",
"unfetch": "5.0.0", "unfetch": "5.0.0",
"vis-data": "7.1.6", "vis-data": "7.1.7",
"vis-network": "9.1.6", "vis-network": "9.1.6",
"vue": "2.7.14", "vue": "2.7.14",
"vue2-daterange-picker": "0.6.8", "vue2-daterange-picker": "0.6.8",
@ -153,15 +154,15 @@
"xss": "1.0.14" "xss": "1.0.14"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.22.15", "@babel/core": "7.23.0",
"@babel/plugin-proposal-decorators": "7.22.15", "@babel/plugin-proposal-decorators": "7.23.0",
"@babel/plugin-transform-runtime": "7.22.15", "@babel/plugin-transform-runtime": "7.22.15",
"@babel/preset-env": "7.22.15", "@babel/preset-env": "7.22.20",
"@babel/preset-typescript": "7.22.15", "@babel/preset-typescript": "7.23.0",
"@koa/cors": "4.0.0", "@koa/cors": "4.0.0",
"@lokalise/node-api": "11.0.1", "@lokalise/node-api": "12.0.0",
"@octokit/auth-oauth-device": "6.0.0", "@octokit/auth-oauth-device": "6.0.1",
"@octokit/plugin-retry": "6.0.0", "@octokit/plugin-retry": "6.0.1",
"@octokit/rest": "20.0.1", "@octokit/rest": "20.0.1",
"@open-wc/dev-server-hmr": "0.1.4", "@open-wc/dev-server-hmr": "0.1.4",
"@rollup/plugin-babel": "6.0.3", "@rollup/plugin-babel": "6.0.3",
@ -169,32 +170,32 @@
"@rollup/plugin-json": "6.0.0", "@rollup/plugin-json": "6.0.0",
"@rollup/plugin-node-resolve": "15.2.1", "@rollup/plugin-node-resolve": "15.2.1",
"@rollup/plugin-replace": "5.0.2", "@rollup/plugin-replace": "5.0.2",
"@types/babel__plugin-transform-runtime": "7.9.2", "@types/babel__plugin-transform-runtime": "7.9.3",
"@types/chromecast-caf-receiver": "6.0.10", "@types/chromecast-caf-receiver": "6.0.10",
"@types/chromecast-caf-sender": "1.0.6", "@types/chromecast-caf-sender": "1.0.6",
"@types/esprima": "4.0.3", "@types/esprima": "4.0.4",
"@types/glob": "8.1.0", "@types/glob": "8.1.0",
"@types/html-minifier-terser": "7.0.0", "@types/html-minifier-terser": "7.0.0",
"@types/js-yaml": "4.0.5", "@types/js-yaml": "4.0.6",
"@types/leaflet": "1.9.4", "@types/leaflet": "1.9.6",
"@types/leaflet-draw": "1.0.8", "@types/leaflet-draw": "1.0.8",
"@types/luxon": "3.3.2", "@types/luxon": "3.3.2",
"@types/mocha": "10.0.1", "@types/mocha": "10.0.1",
"@types/qrcode": "1.5.2", "@types/qrcode": "1.5.2",
"@types/serve-handler": "6.1.1", "@types/serve-handler": "6.1.1",
"@types/sortablejs": "1.15.2", "@types/sortablejs": "1.15.2",
"@types/tar": "6.1.5", "@types/tar": "6.1.6",
"@types/ua-parser-js": "0.7.37", "@types/ua-parser-js": "0.7.37",
"@types/webspeechapi": "0.0.29", "@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "6.6.0", "@typescript-eslint/eslint-plugin": "6.7.2",
"@typescript-eslint/parser": "6.6.0", "@typescript-eslint/parser": "6.7.2",
"@web/dev-server": "0.1.38", "@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1", "@web/dev-server-rollup": "0.4.1",
"babel-loader": "9.1.3", "babel-loader": "9.1.3",
"babel-plugin-template-html-minifier": "4.1.0", "babel-plugin-template-html-minifier": "4.1.0",
"chai": "4.3.8", "chai": "4.3.8",
"del": "7.1.0", "del": "7.1.0",
"eslint": "8.48.0", "eslint": "8.50.0",
"eslint-config-airbnb-base": "15.0.0", "eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "17.1.0", "eslint-config-airbnb-typescript": "17.1.0",
"eslint-config-prettier": "9.0.0", "eslint-config-prettier": "9.0.0",
@ -202,13 +203,13 @@
"eslint-plugin-disable": "2.0.3", "eslint-plugin-disable": "2.0.3",
"eslint-plugin-import": "2.28.1", "eslint-plugin-import": "2.28.1",
"eslint-plugin-lit": "1.9.1", "eslint-plugin-lit": "1.9.1",
"eslint-plugin-lit-a11y": "3.0.0", "eslint-plugin-lit-a11y": "4.1.0",
"eslint-plugin-unused-imports": "3.0.0", "eslint-plugin-unused-imports": "3.0.0",
"eslint-plugin-wc": "1.5.0", "eslint-plugin-wc": "2.0.3",
"esprima": "4.0.1", "esprima": "4.0.1",
"fancy-log": "2.0.0", "fancy-log": "2.0.0",
"fs-extra": "11.1.1", "fs-extra": "11.1.1",
"glob": "10.3.4", "glob": "10.3.7",
"gulp": "4.0.2", "gulp": "4.0.2",
"gulp-flatmap": "1.0.2", "gulp-flatmap": "1.0.2",
"gulp-json-transform": "0.4.8", "gulp-json-transform": "0.4.8",
@ -234,7 +235,7 @@
"rollup-plugin-terser": "7.0.2", "rollup-plugin-terser": "7.0.2",
"rollup-plugin-visualizer": "5.9.2", "rollup-plugin-visualizer": "5.9.2",
"serve-handler": "6.1.5", "serve-handler": "6.1.5",
"sinon": "15.2.0", "sinon": "16.0.0",
"source-map-url": "0.4.1", "source-map-url": "0.4.1",
"systemjs": "6.14.2", "systemjs": "6.14.2",
"tar": "6.2.0", "tar": "6.2.0",

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square70x70logo src="/static/icons/tile-win-70x70.png"/>
<square150x150logo src="/static/icons/tile-win-150x150.png"/>
<wide310x150logo src="/static/icons/tile-win-310x150.png"/>
<square310x310logo src="/static/icons/tile-win-310x310.png"/>
<TileColor>#18bcf2</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1 +1,23 @@
<svg width="16" height="16" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path fill-rule="nonzero" fill="#000" d="M9,16 L9,17 L7,17 L7,16 L1,16 L1,9 L-1,9 L8.00163907,0 L13,4.785368 L13,3 L15,3 L15,7.035368 L17,9 L15,9 L15,16 L9,16 Z M9,16 L9,13.5 C9.49775077,13.0022492 10.1813086,12.3186914 11.0506735,11.4493265 C11.1951058,11.4824829 11.3455072,11.5 11.5,11.5 C12.6045695,11.5 13.5,10.6045695 13.5,9.5 C13.5,8.3954305 12.6045695,7.5 11.5,7.5 C10.3954305,7.5 9.5,8.3954305 9.5,9.5 C9.5,9.65449279 9.5175171,9.80489423 9.55067348,9.94932652 L9,10.5 L9,7.73243561 C9.59780137,7.38662619 10,6.74028236 10,6 C10,4.8954305 9.1045695,4 8,4 C6.8954305,4 6,4.8954305 6,6 C6,6.74028236 6.40219863,7.38662619 7,7.73243561 L7,10.5 L6.44932652,9.94932652 C6.4824829,9.80489423 6.5,9.65449279 6.5,9.5 C6.5,8.3954305 5.6045695,7.5 4.5,7.5 C3.3954305,7.5 2.5,8.3954305 2.5,9.5 C2.5,10.6045695 3.3954305,11.5 4.5,11.5 C4.65352068,11.5 4.80300134,11.4827027 4.9465994,11.4499505 C5.81726201,12.3268973 6.50172888,13.0147433 7,13.5134884 L7,16 L9,16 Z M11.5,10 C11.2238576,10 11,9.77614237 11,9.5 C11,9.22385763 11.2238576,9 11.5,9 C11.7761424,9 12,9.22385763 12,9.5 C12,9.77614237 11.7761424,10 11.5,10 Z M4.5,10 C4.22385763,10 4,9.77614237 4,9.5 C4,9.22385763 4.22385763,9 4.5,9 C4.77614237,9 5,9.22385763 5,9.5 C5,9.77614237 4.77614237,10 4.5,10 Z M8,6.5 C7.72385763,6.5 7.5,6.27614237 7.5,6 C7.5,5.72385763 7.72385763,5.5 8,5.5 C8.27614237,5.5 8.5,5.72385763 8.5,6 C8.5,6.27614237 8.27614237,6.5 8,6.5 Z" id="house-small-tree"/></svg> <?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="480.000000pt" height="480.000000pt" viewBox="0 0 480.000000 480.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,480.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M2313 4666 c-23 -7 -56 -23 -75 -34 -47 -30 -2059 -2048 -2095 -2102
-45 -67 -77 -135 -109 -230 l-29 -85 0 -995 0 -995 27 -51 c31 -59 93 -118
152 -145 39 -18 83 -19 1001 -19 l960 0 -406 405 c-395 395 -406 406 -433 395
-15 -5 -63 -10 -107 -10 -429 0 -566 577 -181 767 67 34 86 38 164 42 105 4
165 -13 246 -67 113 -74 175 -190 176 -327 1 -44 -3 -96 -7 -115 l-8 -35 316
-315 315 -315 0 1160 -1 1160 -51 35 c-260 177 -226 567 62 704 82 39 209 48
293 21 239 -78 354 -352 242 -575 -32 -63 -89 -125 -141 -156 l-44 -26 0 -811
0 -812 315 315 c218 217 313 320 309 330 -14 35 -16 134 -4 190 26 122 111
227 230 284 82 39 209 48 293 21 115 -38 214 -130 258 -242 19 -46 23 -78 24
-153 0 -86 -3 -101 -32 -163 -40 -84 -118 -163 -198 -202 -49 -23 -77 -29
-150 -33 -50 -2 -108 1 -130 7 l-40 11 -437 -438 -438 -437 0 -307 0 -308 998
0 c981 0 998 1 1042 21 58 26 115 81 148 144 l27 50 0 995 0 995 -33 95 c-72
209 -6 135 -1147 1278 -840 843 -1040 1037 -1082 1059 -64 31 -159 39 -220 19z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 561 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 850 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 824 B

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 852 B

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20230911.0" version = "20230926.0"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "The Home Assistant frontend" description = "The Home Assistant frontend"
readme = "README.md" readme = "README.md"

View File

@ -1,4 +1,4 @@
#!/bin/sh #!/bin/bash
# Helper to start Home Assistant Core inside the devcontainer # Helper to start Home Assistant Core inside the devcontainer
# Stop on errors # Stop on errors
@ -11,11 +11,35 @@ if [ -z "${DEVCONTAINER}" ]; then
exit 1 exit 1
fi fi
if [ -z $(which hass) ]; then # Default to installing (or upgrading to) dev branch
echo "Installing Home Asstant core from dev." coreURL="https://github.com/home-assistant/core.git"
python3 -m pip install --upgrade \ ref="dev"
colorlog \
git+https://github.com/home-assistant/home-assistant.git@dev while getopts "hr:s" opt; do
case $opt in
h) # Help
echo "Usage: $0 [-h|-r <ref>|-s]"
echo -n "Install and run core at the given ref, i.e. branch, tag, or commit. The dev branch is used if no option is specified."
echo "The -s flag skips the install/upgrade, using whatever version is currently installed."
exit 0
;;
r) # Git ref
ref="${OPTARG}"
;;
s) # Skip (use current install)
ref=""
;;
*)
echo "Try $0 -h for help" >&2
exit 1
;;
esac
done
if [ -n "$ref" ]; then
echo "Installing Home Assistant core at ${ref}..."
python3 -m pip install --user --upgrade --src "$HOME/src" \
--editable "git+${coreURL}@${ref}#egg=homeassistant"
fi fi
if [ ! -d "${WD}/config" ]; then if [ ! -d "${WD}/config" ]; then
@ -30,7 +54,7 @@ logger:
homeassistant.components.frontend: debug homeassistant.components.frontend: debug
" >> "${WD}/config/configuration.yaml" " >> "${WD}/config/configuration.yaml"
if [ ! -z "${HASSIO}" ]; then if [ -n "${HASSIO}" ]; then
echo " echo "
# frontend: # frontend:
# development_repo: ${WD} # development_repo: ${WD}
@ -46,7 +70,7 @@ frontend:
# development_repo: ${WD}" >> "${WD}/config/configuration.yaml" # development_repo: ${WD}" >> "${WD}/config/configuration.yaml"
fi fi
if [ ! -z "${CODESPACES}" ]; then if [ -n "${CODESPACES}" ]; then
echo " echo "
http: http:
use_x_forwarded_for: true use_x_forwarded_for: true

View File

@ -1,19 +1,12 @@
/* eslint-disable lit/prefer-static-styles */
import "@material/mwc-button"; import "@material/mwc-button";
import { genClientId } from "home-assistant-js-websocket"; import { genClientId } from "home-assistant-js-websocket";
import { import { html, LitElement, nothing, PropertyValues } from "lit";
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
} from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { LocalizeFunc } from "../common/translations/localize"; import { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-alert"; import "../components/ha-alert";
import "../components/ha-checkbox"; import "../components/ha-checkbox";
import { computeInitialHaFormData } from "../components/ha-form/compute-initial-ha-form-data"; import { computeInitialHaFormData } from "../components/ha-form/compute-initial-ha-form-data";
import "../components/ha-form/ha-form";
import "../components/ha-formfield"; import "../components/ha-formfield";
import "../components/ha-markdown"; import "../components/ha-markdown";
import { AuthProvider, autocompleteLoginFields } from "../data/auth"; import { AuthProvider, autocompleteLoginFields } from "../data/auth";
@ -21,7 +14,7 @@ import {
DataEntryFlowStep, DataEntryFlowStep,
DataEntryFlowStepForm, DataEntryFlowStepForm,
} from "../data/data_entry_flow"; } from "../data/data_entry_flow";
import "./ha-password-manager-polyfill"; import "./ha-auth-form";
type State = "loading" | "error" | "step"; type State = "loading" | "error" | "step";
@ -49,6 +42,10 @@ export class HaAuthFlow extends LitElement {
@state() private _storeToken = false; @state() private _storeToken = false;
createRenderRoot() {
return this;
}
willUpdate(changedProps: PropertyValues) { willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps); super.willUpdate(changedProps);
@ -79,13 +76,17 @@ export class HaAuthFlow extends LitElement {
protected render() { protected render() {
return html` return html`
<style>
ha-auth-flow .action {
margin: 24px 0 8px;
text-align: center;
}
ha-auth-flow .store-token {
margin-top: 10px;
margin-left: -16px;
}
</style>
<form>${this._renderForm()}</form> <form>${this._renderForm()}</form>
<ha-password-manager-polyfill
.step=${this._step}
.stepData=${this._stepData}
@form-submitted=${this._handleSubmit}
@value-changed=${this._stepDataChanged}
></ha-password-manager-polyfill>
`; `;
} }
@ -128,12 +129,6 @@ export class HaAuthFlow extends LitElement {
(form as any).focus(); (form as any).focus();
} }
}, 100); }, 100);
setTimeout(() => {
this.renderRoot.querySelector(
"ha-password-manager-polyfill"
)!.boundingRect = this.getBoundingClientRect();
}, 500);
} }
private _renderForm() { private _renderForm() {
@ -205,7 +200,7 @@ export class HaAuthFlow extends LitElement {
></ha-markdown> ></ha-markdown>
` `
: nothing} : nothing}
<ha-form <ha-auth-form
.data=${this._stepData} .data=${this._stepData}
.schema=${autocompleteLoginFields(step.data_schema)} .schema=${autocompleteLoginFields(step.data_schema)}
.error=${step.errors} .error=${step.errors}
@ -213,7 +208,7 @@ export class HaAuthFlow extends LitElement {
.computeLabel=${this._computeLabelCallback(step)} .computeLabel=${this._computeLabelCallback(step)}
.computeError=${this._computeErrorCallback(step)} .computeError=${this._computeErrorCallback(step)}
@value-changed=${this._stepDataChanged} @value-changed=${this._stepDataChanged}
></ha-form> ></ha-auth-form>
${this.clientId === genClientId() && ${this.clientId === genClientId() &&
!["select_mfa_module", "mfa"].includes(step.step_id) !["select_mfa_module", "mfa"].includes(step.step_id)
? html` ? html`
@ -395,20 +390,6 @@ export class HaAuthFlow extends LitElement {
this._submitting = false; this._submitting = false;
} }
} }
static get styles(): CSSResultGroup {
return css`
.action {
margin: 24px 0 8px;
text-align: center;
}
/* Align with the rest of the form. */
.store-token {
margin-top: 10px;
margin-left: -16px;
}
`;
}
} }
declare global { declare global {

View File

@ -0,0 +1,77 @@
/* eslint-disable lit/prefer-static-styles */
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators";
import { HaFormString } from "../components/ha-form/ha-form-string";
import "../components/ha-icon-button";
import "./ha-auth-textfield";
@customElement("ha-auth-form-string")
export class HaAuthFormString extends HaFormString {
protected createRenderRoot() {
return this;
}
protected render(): TemplateResult {
return html`
<style>
ha-auth-form-string {
display: block;
position: relative;
}
ha-auth-form-string[own-margin] {
margin-bottom: 5px;
}
ha-auth-form-string ha-auth-textfield {
display: block !important;
}
ha-auth-form-string ha-icon-button {
position: absolute;
top: 1em;
right: 12px;
--mdc-icon-button-size: 24px;
color: var(--secondary-text-color);
}
ha-auth-form-string ha-icon-button {
inset-inline-start: initial;
inset-inline-end: 12px;
direction: var(--direction);
}
</style>
<ha-auth-textfield
.type=${
!this.isPassword
? this.stringType
: this.unmaskedPassword
? "text"
: "password"
}
.label=${this.label}
.value=${this.data || ""}
.helper=${this.helper}
helperPersistent
.disabled=${this.disabled}
.required=${this.schema.required}
.autoValidate=${this.schema.required}
.name=${this.schema.name}
.autocomplete=${this.schema.autocomplete}
.suffix=${
this.isPassword
? // reserve some space for the icon.
html`<div style="width: 24px"></div>`
: this.schema.description?.suffix
}
.validationMessage=${this.schema.required ? "Required" : undefined}
@input=${this._valueChanged}
@change=${this._valueChanged}
></ha-auth-textfield>
${this.renderIcon()}
</ha-auth-textfield>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-auth-form-string": HaAuthFormString;
}
}

44
src/auth/ha-auth-form.ts Normal file
View File

@ -0,0 +1,44 @@
/* eslint-disable lit/prefer-static-styles */
import { html } from "lit";
import { customElement } from "lit/decorators";
import { HaForm } from "../components/ha-form/ha-form";
import "./ha-auth-form-string";
@customElement("ha-auth-form")
export class HaAuthForm extends HaForm {
protected fieldElementName(type: string): string {
if (type === "string") {
return `ha-auth-form-${type}`;
}
return super.fieldElementName(type);
}
protected createRenderRoot() {
// attach it as soon as possible to make sure we fetch all events.
this.addValueChangedListener(this);
return this;
}
protected render() {
return html`
<style>
ha-auth-form .root > * {
display: block;
}
ha-auth-form .root > *:not([own-margin]):not(:last-child) {
margin-bottom: 24px;
}
ha-auth-form ha-alert[own-margin] {
margin-bottom: 4px;
}
</style>
${super.render()}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-auth-form": HaAuthForm;
}
}

View File

@ -0,0 +1,254 @@
/* eslint-disable lit/value-after-constraints */
/* eslint-disable lit/prefer-static-styles */
import { floatingLabel } from "@material/mwc-floating-label/mwc-floating-label-directive";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { live } from "lit/directives/live";
import { HaTextField } from "../components/ha-textfield";
@customElement("ha-auth-textfield")
export class HaAuthTextField extends HaTextField {
protected renderLabel(): TemplateResult | string {
return !this.label
? ""
: html`
<span
.floatingLabelFoundation=${floatingLabel(
this.label
) as unknown as any}
.id=${this.name}
>${this.label}</span
>
`;
}
protected renderInput(shouldRenderHelperText: boolean): TemplateResult {
const minOrUndef = this.minLength === -1 ? undefined : this.minLength;
const maxOrUndef = this.maxLength === -1 ? undefined : this.maxLength;
const autocapitalizeOrUndef = this.autocapitalize
? (this.autocapitalize as
| "off"
| "none"
| "on"
| "sentences"
| "words"
| "characters")
: undefined;
const showValidationMessage = this.validationMessage && !this.isUiValid;
const ariaLabelledbyOrUndef = this.label ? this.name : undefined;
const ariaControlsOrUndef = shouldRenderHelperText
? "helper-text"
: undefined;
const ariaDescribedbyOrUndef =
this.focused || this.helperPersistent || showValidationMessage
? "helper-text"
: undefined;
// TODO: live() directive needs casting for lit-analyzer
// https://github.com/runem/lit-analyzer/pull/91/files
// TODO: lit-analyzer labels min/max as (number|string) instead of string
return html` <input
aria-labelledby=${ifDefined(ariaLabelledbyOrUndef)}
aria-controls=${ifDefined(ariaControlsOrUndef)}
aria-describedby=${ifDefined(ariaDescribedbyOrUndef)}
class="mdc-text-field__input"
type=${this.type}
.value=${live(this.value) as unknown as string}
?disabled=${this.disabled}
placeholder=${this.placeholder}
?required=${this.required}
?readonly=${this.readOnly}
minlength=${ifDefined(minOrUndef)}
maxlength=${ifDefined(maxOrUndef)}
pattern=${ifDefined(this.pattern ? this.pattern : undefined)}
min=${ifDefined(this.min === "" ? undefined : (this.min as number))}
max=${ifDefined(this.max === "" ? undefined : (this.max as number))}
step=${ifDefined(this.step === null ? undefined : (this.step as number))}
size=${ifDefined(this.size === null ? undefined : this.size)}
name=${ifDefined(this.name === "" ? undefined : this.name)}
inputmode=${ifDefined(this.inputMode)}
autocapitalize=${ifDefined(autocapitalizeOrUndef)}
@input=${this.handleInputChange}
@focus=${this.onInputFocus}
@blur=${this.onInputBlur}
/>`;
}
public render() {
return html`
<style>
ha-auth-textfield {
display: inline-flex;
flex-direction: column;
outline: none;
}
ha-auth-textfield:not([disabled]):hover
:not(.mdc-text-field--invalid):not(.mdc-text-field--focused)
mwc-notched-outline {
--mdc-notched-outline-border-color: var(
--mdc-text-field-outlined-hover-border-color,
rgba(0, 0, 0, 0.87)
);
}
ha-auth-textfield:not([disabled])
.mdc-text-field:not(.mdc-text-field--outlined) {
background-color: var(--mdc-text-field-fill-color, whitesmoke);
}
ha-auth-textfield:not([disabled])
.mdc-text-field.mdc-text-field--invalid
mwc-notched-outline {
--mdc-notched-outline-border-color: var(
--mdc-text-field-error-color,
var(--mdc-theme-error, #b00020)
);
}
ha-auth-textfield:not([disabled])
.mdc-text-field.mdc-text-field--invalid
+ .mdc-text-field-helper-line
.mdc-text-field-character-counter,
ha-auth-textfield:not([disabled])
.mdc-text-field.mdc-text-field--invalid
.mdc-text-field__icon {
color: var(
--mdc-text-field-error-color,
var(--mdc-theme-error, #b00020)
);
}
ha-auth-textfield:not([disabled])
.mdc-text-field:not(.mdc-text-field--invalid):not(
.mdc-text-field--focused
)
.mdc-floating-label,
ha-auth-textfield:not([disabled])
.mdc-text-field:not(.mdc-text-field--invalid):not(
.mdc-text-field--focused
)
.mdc-floating-label::after {
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
}
ha-auth-textfield:not([disabled])
.mdc-text-field.mdc-text-field--focused
mwc-notched-outline {
--mdc-notched-outline-stroke-width: 2px;
}
ha-auth-textfield:not([disabled])
.mdc-text-field.mdc-text-field--focused:not(.mdc-text-field--invalid)
mwc-notched-outline {
--mdc-notched-outline-border-color: var(
--mdc-text-field-focused-label-color,
var(--mdc-theme-primary, rgba(98, 0, 238, 0.87))
);
}
ha-auth-textfield:not([disabled])
.mdc-text-field.mdc-text-field--focused:not(.mdc-text-field--invalid)
.mdc-floating-label {
color: #6200ee;
color: var(--mdc-theme-primary, #6200ee);
}
ha-auth-textfield:not([disabled])
.mdc-text-field
.mdc-text-field__input {
color: var(--mdc-text-field-ink-color, rgba(0, 0, 0, 0.87));
}
ha-auth-textfield:not([disabled])
.mdc-text-field
.mdc-text-field__input::placeholder {
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
}
ha-auth-textfield:not([disabled])
.mdc-text-field-helper-line
.mdc-text-field-helper-text:not(
.mdc-text-field-helper-text--validation-msg
),
ha-auth-textfield:not([disabled])
.mdc-text-field-helper-line:not(.mdc-text-field--invalid)
.mdc-text-field-character-counter {
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
}
ha-auth-textfield[disabled]
.mdc-text-field:not(.mdc-text-field--outlined) {
background-color: var(--mdc-text-field-disabled-fill-color, #fafafa);
}
ha-auth-textfield[disabled]
.mdc-text-field.mdc-text-field--outlined
mwc-notched-outline {
--mdc-notched-outline-border-color: var(
--mdc-text-field-outlined-disabled-border-color,
rgba(0, 0, 0, 0.06)
);
}
ha-auth-textfield[disabled]
.mdc-text-field:not(.mdc-text-field--invalid):not(
.mdc-text-field--focused
)
.mdc-floating-label,
ha-auth-textfield[disabled]
.mdc-text-field:not(.mdc-text-field--invalid):not(
.mdc-text-field--focused
)
.mdc-floating-label::after {
color: var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.38));
}
ha-auth-textfield[disabled] .mdc-text-field .mdc-text-field__input,
ha-auth-textfield[disabled]
.mdc-text-field
.mdc-text-field__input::placeholder {
color: var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.38));
}
ha-auth-textfield[disabled]
.mdc-text-field-helper-line
.mdc-text-field-helper-text,
ha-auth-textfield[disabled]
.mdc-text-field-helper-line
.mdc-text-field-character-counter {
color: var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.38));
}
ha-auth-textfield:not([disabled])
.mdc-text-field.mdc-text-field--focused:not(.mdc-text-field--invalid)
.mdc-floating-label {
color: var(--mdc-theme-primary, #6200ee);
}
ha-auth-textfield[no-spinner] input::-webkit-outer-spin-button,
ha-auth-textfield[no-spinner] input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
ha-auth-textfield[no-spinner] input[type="number"] {
-moz-appearance: textfield;
}
</style>
${super.render()}
`;
}
protected createRenderRoot() {
// add parent style to light dom
const style = document.createElement("style");
style.textContent = HaTextField.elementStyles as unknown as string;
this.append(style);
return this;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-auth-textfield": HaAuthTextField;
}
}

View File

@ -1,13 +1,7 @@
import punycode from "punycode"; /* eslint-disable lit/prefer-static-styles */
import { import { html, LitElement, nothing, PropertyValues } from "lit";
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
} from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import punycode from "punycode";
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element"; import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
import { extractSearchParamsObject } from "../common/url/search-params"; import { extractSearchParamsObject } from "../common/url/search-params";
import "../components/ha-alert"; import "../components/ha-alert";
@ -61,13 +55,27 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
protected render() { protected render() {
if (this._error) { if (this._error) {
return html`<ha-alert alert-type="error" return html`
>${this._error} ${this.redirectUri}</ha-alert <style>
>`; ha-authorize ha-alert {
display: block;
margin: 16px 0;
}
</style>
<ha-alert alert-type="error"
>${this._error} ${this.redirectUri}</ha-alert
>
`;
} }
if (!this._authProviders) { if (!this._authProviders) {
return html` return html`
<style>
ha-authorize p {
font-size: 14px;
line-height: 20px;
}
</style>
<p>${this.localize("ui.panel.page-authorize.initializing")}</p> <p>${this.localize("ui.panel.page-authorize.initializing")}</p>
`; `;
} }
@ -79,6 +87,25 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
const app = this.clientId && this.clientId in appNames; const app = this.clientId && this.clientId in appNames;
return html` return html`
<style>
ha-pick-auth-provider {
display: block;
margin-top: 48px;
}
ha-auth-flow {
display: block;
margin-top: 24px;
}
ha-alert {
display: block;
margin: 16px 0;
}
p {
font-size: 14px;
line-height: 20px;
}
</style>
${!this._ownInstance ${!this._ownInstance
? html`<ha-alert .alertType=${app ? "info" : "warning"}> ? html`<ha-alert .alertType=${app ? "info" : "warning"}>
${app ${app
@ -123,6 +150,10 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
`; `;
} }
createRenderRoot() {
return this;
}
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
@ -217,25 +248,4 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
private async _handleAuthProviderPick(ev) { private async _handleAuthProviderPick(ev) {
this._authProvider = ev.detail; this._authProvider = ev.detail;
} }
static get styles(): CSSResultGroup {
return css`
ha-pick-auth-provider {
display: block;
margin-top: 48px;
}
ha-auth-flow {
display: block;
margin-top: 24px;
}
ha-alert {
display: block;
margin: 16px 0;
}
p {
font-size: 14px;
line-height: 20px;
}
`;
}
} }

View File

@ -1,127 +0,0 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../common/dom/fire_event";
import type { HaFormSchema } from "../components/ha-form/types";
import { autocompleteLoginFields } from "../data/auth";
import type { DataEntryFlowStep } from "../data/data_entry_flow";
declare global {
interface HTMLElementTagNameMap {
"ha-password-manager-polyfill": HaPasswordManagerPolyfill;
}
interface HASSDomEvents {
"form-submitted": undefined;
}
}
const ENABLED_HANDLERS = [
"homeassistant",
"legacy_api_password",
"command_line",
];
@customElement("ha-password-manager-polyfill")
export class HaPasswordManagerPolyfill extends LitElement {
@property({ attribute: false }) public step?: DataEntryFlowStep;
@property({ attribute: false }) public stepData: any;
@property({ attribute: false }) public boundingRect?: DOMRect;
private _styleElement?: HTMLStyleElement;
public connectedCallback() {
super.connectedCallback();
this._styleElement = document.createElement("style");
this._styleElement.textContent = css`
.password-manager-polyfill {
position: absolute;
opacity: 0;
z-index: -1;
}
.password-manager-polyfill input {
width: 100%;
height: 62px;
padding: 0;
border: 0;
}
.password-manager-polyfill input[type="submit"] {
width: 0;
height: 0;
}
`.toString();
document.head.append(this._styleElement);
}
public disconnectedCallback() {
super.disconnectedCallback();
this._styleElement?.remove();
delete this._styleElement;
}
protected createRenderRoot() {
// Add under document body so the element isn't placed inside any shadow roots
return document.body;
}
protected render() {
if (
this.step &&
this.step.type === "form" &&
this.step.step_id === "init" &&
ENABLED_HANDLERS.includes(this.step.handler[0])
) {
return html`
<form
class="password-manager-polyfill"
style=${styleMap({
top: `${this.boundingRect?.y || 148}px`,
left: `calc(50% - ${(this.boundingRect?.width || 360) / 2}px)`,
width: `${this.boundingRect?.width || 360}px`,
})}
aria-hidden="true"
@submit=${this._handleSubmit}
>
${autocompleteLoginFields(this.step.data_schema).map((input) =>
this.render_input(input)
)}
<input type="submit" />
</form>
`;
}
return nothing;
}
private render_input(schema: HaFormSchema) {
const inputType = schema.name.includes("password") ? "password" : "text";
if (schema.type !== "string") {
return "";
}
return html`
<input
tabindex="-1"
.id=${schema.name}
.name=${schema.name}
.type=${inputType}
.value=${this.stepData[schema.name] || ""}
.autocomplete=${schema.autocomplete}
@input=${this._valueChanged}
@change=${this._valueChanged}
/>
`;
}
private _handleSubmit(ev: Event) {
ev.preventDefault();
fireEvent(this, "form-submitted");
}
private _valueChanged(ev: Event) {
const target = ev.target! as HTMLInputElement;
this.stepData = { ...this.stepData, [target.id]: target.value };
fireEvent(this, "value-changed", {
value: this.stepData,
});
}
}

View File

@ -15,6 +15,7 @@ import {
mdiCalendarClock, mdiCalendarClock,
mdiCarCoolantLevel, mdiCarCoolantLevel,
mdiCash, mdiCash,
mdiChatSleep,
mdiClock, mdiClock,
mdiCloudUpload, mdiCloudUpload,
mdiCog, mdiCog,
@ -31,7 +32,6 @@ import {
mdiGauge, mdiGauge,
mdiGoogleAssistant, mdiGoogleAssistant,
mdiGoogleCirclesCommunities, mdiGoogleCirclesCommunities,
mdiHomeAssistant,
mdiHomeAutomation, mdiHomeAutomation,
mdiImage, mdiImage,
mdiImageFilterFrames, mdiImageFilterFrames,
@ -70,6 +70,8 @@ import {
mdiWifi, mdiWifi,
} from "@mdi/js"; } from "@mdi/js";
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
// Constants should be alphabetically sorted by name. // Constants should be alphabetically sorted by name.
// Arrays with values should be alphabetically sorted if order doesn't matter. // Arrays with values should be alphabetically sorted if order doesn't matter.
// Each constant should have a description what it is supposed to be used for. // Each constant should have a description what it is supposed to be used for.
@ -123,6 +125,7 @@ export const FIXED_DOMAIN_ICONS = {
tts: mdiSpeakerMessage, tts: mdiSpeakerMessage,
updater: mdiCloudUpload, updater: mdiCloudUpload,
vacuum: mdiRobotVacuum, vacuum: mdiRobotVacuum,
wake_word: mdiChatSleep,
zone: mdiMapMarkerRadius, zone: mdiMapMarkerRadius,
}; };

View File

@ -193,6 +193,7 @@ export const computeStateDisplayFromEntityAttributes = (
"scene", "scene",
"stt", "stt",
"tts", "tts",
"wake_word",
].includes(domain) || ].includes(domain) ||
(domain === "sensor" && attributes.device_class === "timestamp") (domain === "sensor" && attributes.device_class === "timestamp")
) { ) {

View File

@ -7,7 +7,7 @@ export const computeStateNameFromEntityAttributes = (
): string => ): string =>
attributes.friendly_name === undefined attributes.friendly_name === undefined
? computeObjectId(entityId).replace(/_/g, " ") ? computeObjectId(entityId).replace(/_/g, " ")
: attributes.friendly_name || ""; : (attributes.friendly_name ?? "").toString();
export const computeStateName = (stateObj: HassEntity): string => export const computeStateName = (stateObj: HassEntity): string =>
computeStateNameFromEntityAttributes(stateObj.entity_id, stateObj.attributes); computeStateNameFromEntityAttributes(stateObj.entity_id, stateObj.attributes);

View File

@ -53,13 +53,14 @@ export class HaChartBase extends LitElement {
@state() private _hiddenDatasets: Set<number> = new Set(); @state() private _hiddenDatasets: Set<number> = new Set();
public disconnectedCallback() { public disconnectedCallback() {
this._releaseCanvas();
super.disconnectedCallback(); super.disconnectedCallback();
this._releaseCanvas();
} }
public connectedCallback() { public connectedCallback() {
super.connectedCallback(); super.connectedCallback();
if (this.hasUpdated) { if (this.hasUpdated) {
this._releaseCanvas();
this._setupChart(); this._setupChart();
} }
} }
@ -110,7 +111,7 @@ export class HaChartBase extends LitElement {
return; return;
} }
if (changedProps.has("plugins") || changedProps.has("chartType")) { if (changedProps.has("plugins") || changedProps.has("chartType")) {
this.chart.destroy(); this._releaseCanvas();
this._setupChart(); this._setupChart();
return; return;
} }

View File

@ -35,6 +35,8 @@ export class StateHistoryChartLine extends LitElement {
@property({ type: Boolean }) public showNames = true; @property({ type: Boolean }) public showNames = true;
@property({ type: Boolean }) public clickForMoreInfo = true;
@property({ attribute: false }) public startTime!: Date; @property({ attribute: false }) public startTime!: Date;
@property({ attribute: false }) public endTime!: Date; @property({ attribute: false }) public endTime!: Date;
@ -45,6 +47,8 @@ export class StateHistoryChartLine extends LitElement {
@state() private _chartData?: ChartData<"line">; @state() private _chartData?: ChartData<"line">;
@state() private _entityIds: string[] = [];
@state() private _chartOptions?: ChartOptions; @state() private _chartOptions?: ChartOptions;
@state() private _yWidth = 0; @state() private _yWidth = 0;
@ -171,6 +175,25 @@ export class StateHistoryChartLine extends LitElement {
}, },
// @ts-expect-error // @ts-expect-error
locale: numberFormatToLocale(this.hass.locale), locale: numberFormatToLocale(this.hass.locale),
onClick: (e: any) => {
if (!this.clickForMoreInfo) {
return;
}
const points = e.chart.getElementsAtEventForMode(
e,
"nearest",
{ intersect: true },
true
);
if (points.length) {
const firstPoint = points[0];
fireEvent(this, "hass-more-info", {
entityId: this._entityIds[firstPoint.datasetIndex],
});
}
},
}; };
} }
if ( if (
@ -191,6 +214,7 @@ export class StateHistoryChartLine extends LitElement {
const computedStyles = getComputedStyle(this); const computedStyles = getComputedStyle(this);
const entityStates = this.data; const entityStates = this.data;
const datasets: ChartDataset<"line">[] = []; const datasets: ChartDataset<"line">[] = [];
const entityIds: string[] = [];
if (entityStates.length === 0) { if (entityStates.length === 0) {
return; return;
} }
@ -242,6 +266,7 @@ export class StateHistoryChartLine extends LitElement {
pointRadius: 0, pointRadius: 0,
data: [], data: [],
}); });
entityIds.push(states.entity_id);
}; };
if ( if (
@ -493,6 +518,7 @@ export class StateHistoryChartLine extends LitElement {
this._chartData = { this._chartData = {
datasets, datasets,
}; };
this._entityIds = entityIds;
} }
} }
customElements.define("state-history-chart-line", StateHistoryChartLine); customElements.define("state-history-chart-line", StateHistoryChartLine);

View File

@ -1,4 +1,5 @@
import type { ChartData, ChartDataset, ChartOptions } from "chart.js"; import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
import { getRelativePosition } from "chart.js/helpers";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time"; import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
@ -32,6 +33,8 @@ export class StateHistoryChartTimeline extends LitElement {
@property({ type: Boolean }) public showNames = true; @property({ type: Boolean }) public showNames = true;
@property({ type: Boolean }) public clickForMoreInfo = true;
@property({ type: Boolean }) public chunked = false; @property({ type: Boolean }) public chunked = false;
@property({ attribute: false }) public startTime!: Date; @property({ attribute: false }) public startTime!: Date;
@ -220,6 +223,22 @@ export class StateHistoryChartTimeline extends LitElement {
}, },
// @ts-expect-error // @ts-expect-error
locale: numberFormatToLocale(this.hass.locale), locale: numberFormatToLocale(this.hass.locale),
onClick: (e: any) => {
if (!this.clickForMoreInfo) {
return;
}
const chart = e.chart;
const canvasPosition = getRelativePosition(e, chart);
const index = Math.abs(
chart.scales.y.getValueForPixel(canvasPosition.y)
);
fireEvent(this, "hass-more-info", {
// @ts-ignore
entityId: this._chartData?.datasets[index]?.label,
});
},
}; };
} }

View File

@ -69,6 +69,8 @@ export class StateHistoryCharts extends LitElement {
@property({ type: Boolean }) public showNames = true; @property({ type: Boolean }) public showNames = true;
@property({ type: Boolean }) public clickForMoreInfo = true;
@property({ type: Boolean }) public isLoadingData = false; @property({ type: Boolean }) public isLoadingData = false;
@state() private _computedStartTime!: Date; @state() private _computedStartTime!: Date;
@ -181,6 +183,7 @@ export class StateHistoryCharts extends LitElement {
.paddingYAxis=${this._maxYWidth} .paddingYAxis=${this._maxYWidth}
.names=${this.names} .names=${this.names}
.chartIndex=${index} .chartIndex=${index}
.clickForMoreInfo=${this.clickForMoreInfo}
@y-width-changed=${this._yWidthChanged} @y-width-changed=${this._yWidthChanged}
></state-history-chart-line> ></state-history-chart-line>
</div> `; </div> `;
@ -197,6 +200,7 @@ export class StateHistoryCharts extends LitElement {
.chunked=${this.virtualize} .chunked=${this.virtualize}
.paddingYAxis=${this._maxYWidth} .paddingYAxis=${this._maxYWidth}
.chartIndex=${index} .chartIndex=${index}
.clickForMoreInfo=${this.clickForMoreInfo}
@y-width-changed=${this._yWidthChanged} @y-width-changed=${this._yWidthChanged}
></state-history-chart-timeline> ></state-history-chart-timeline>
</div> `; </div> `;

View File

@ -74,7 +74,7 @@ export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
title: TemplateResult | string; title: TemplateResult | string;
label?: TemplateResult | string; label?: TemplateResult | string;
type?: "numeric" | "icon" | "icon-button" | "overflow-menu" | "flex"; type?: "numeric" | "icon" | "icon-button" | "overflow-menu" | "flex";
template?: (data: any, row: T) => TemplateResult | string | typeof nothing; template?: (row: T) => TemplateResult | string | typeof nothing;
width?: string; width?: string;
maxWidth?: string; maxWidth?: string;
grows?: boolean; grows?: boolean;
@ -431,7 +431,7 @@ export class HaDataTable extends LitElement {
}) })
: ""} : ""}
> >
${column.template ? column.template(row[key], row) : row[key]} ${column.template ? column.template(row) : row[key]}
</div> </div>
`; `;
})} })}

View File

@ -131,11 +131,15 @@ export class StateBadge extends LitElement {
if (this.hass) { if (this.hass) {
imageUrl = this.hass.hassUrl(imageUrl); imageUrl = this.hass.hassUrl(imageUrl);
} }
if (computeDomain(stateObj.entity_id) === "camera") { const domain = computeDomain(stateObj.entity_id);
if (domain === "camera") {
imageUrl = cameraUrlWithWidthHeight(imageUrl, 80, 80); imageUrl = cameraUrlWithWidthHeight(imageUrl, 80, 80);
} }
hostStyle.backgroundImage = `url(${imageUrl})`; hostStyle.backgroundImage = `url(${imageUrl})`;
this._showIcon = false; this._showIcon = false;
if (domain === "update") {
hostStyle.borderRadius = "0";
}
} else if (this.color) { } else if (this.color) {
// Externally provided overriding color wins over state color // Externally provided overriding color wins over state color
iconStyle.color = this.color; iconStyle.color = this.color;

View File

@ -41,8 +41,6 @@ export class HaClickableListItem extends HaListItem {
height: 100%; height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
padding-left: var(--mdc-list-side-padding, 20px);
padding-right: var(--mdc-list-side-padding, 20px);
overflow: hidden; overflow: hidden;
} }
`, `,

View File

@ -1,10 +1,12 @@
import { Ripple } from "@material/mwc-ripple"; import { Ripple } from "@material/mwc-ripple";
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers"; import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
import { SelectBase } from "@material/mwc-select/mwc-select-base"; import { SelectBase } from "@material/mwc-select/mwc-select-base";
import { mdiMenuDown } from "@mdi/js";
import { css, html, nothing } from "lit"; import { css, html, nothing } from "lit";
import { import {
customElement, customElement,
eventOptions, eventOptions,
property,
query, query,
queryAsync, queryAsync,
state, state,
@ -24,6 +26,12 @@ export class HaControlSelectMenu extends SelectBase {
@query(".select-anchor") protected anchorElement!: HTMLDivElement | null; @query(".select-anchor") protected anchorElement!: HTMLDivElement | null;
@property({ type: Boolean, attribute: "show-arrow" })
public showArrow?: boolean;
@property({ type: Boolean, attribute: "hide-label" })
public hideLabel?: boolean;
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>; @queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
@state() private _shouldRenderRipple = false; @state() private _shouldRenderRipple = false;
@ -36,7 +44,9 @@ export class HaControlSelectMenu extends SelectBase {
"select-no-value": !this.selectedText, "select-no-value": !this.selectedText,
}; };
const labelledby = this.label ? "label" : undefined; const labelledby = this.label && !this.hideLabel ? "label" : undefined;
const labelAttribute =
this.label && this.hideLabel ? this.label : undefined;
return html` return html`
<div class="select ${classMap(classes)}"> <div class="select ${classMap(classes)}">
@ -57,6 +67,7 @@ export class HaControlSelectMenu extends SelectBase {
aria-invalid=${!this.isUiValid} aria-invalid=${!this.isUiValid}
aria-haspopup="listbox" aria-haspopup="listbox"
aria-labelledby=${ifDefined(labelledby)} aria-labelledby=${ifDefined(labelledby)}
aria-label=${ifDefined(labelAttribute)}
aria-required=${this.required} aria-required=${this.required}
@click=${this.onClick} @click=${this.onClick}
@focus=${this.onFocus} @focus=${this.onFocus}
@ -72,11 +83,14 @@ export class HaControlSelectMenu extends SelectBase {
> >
${this.renderIcon()} ${this.renderIcon()}
<div class="content"> <div class="content">
<p id="label" class="label">${this.label}</p> ${this.hideLabel
? nothing
: html`<p id="label" class="label">${this.label}</p>`}
${this.selectedText ${this.selectedText
? html`<p class="value">${this.selectedText}</p>` ? html`<p class="value">${this.selectedText}</p>`
: nothing} : nothing}
</div> </div>
${this.renderArrow()}
${this._shouldRenderRipple && !this.disabled ${this._shouldRenderRipple && !this.disabled
? html` <mwc-ripple></mwc-ripple> ` ? html` <mwc-ripple></mwc-ripple> `
: nothing} : nothing}
@ -86,13 +100,29 @@ export class HaControlSelectMenu extends SelectBase {
`; `;
} }
private renderArrow() {
if (!this.showArrow) return nothing;
return html`
<div class="icon">
<ha-svg-icon .path=${mdiMenuDown}></ha-svg-icon>
</div>
`;
}
private renderIcon() { private renderIcon() {
const index = this.mdcFoundation?.getSelectedIndex(); const index = this.mdcFoundation?.getSelectedIndex();
const items = this.menuElement?.items ?? []; const items = this.menuElement?.items ?? [];
const item = index != null ? items[index] : undefined; const item = index != null ? items[index] : undefined;
const icon = const defaultIcon = this.querySelector("[slot='icon']");
item?.querySelector("[slot='graphic']") ?? const icon = (item?.querySelector("[slot='graphic']") ?? null) as
(null as HaSvgIcon | HaIcon | null); | HaSvgIcon
| HaIcon
| null;
if (!defaultIcon && !icon) {
return null;
}
return html` return html`
<div class="icon"> <div class="icon">
@ -171,14 +201,18 @@ export class HaControlSelectMenu extends SelectBase {
--control-select-menu-background-color: var(--disabled-color); --control-select-menu-background-color: var(--disabled-color);
--control-select-menu-background-opacity: 0.2; --control-select-menu-background-opacity: 0.2;
--control-select-menu-border-radius: 14px; --control-select-menu-border-radius: 14px;
--control-select-menu-height: 48px;
--control-select-menu-padding: 6px 10px;
--mdc-icon-size: 20px; --mdc-icon-size: 20px;
font-size: 14px;
line-height: 1.4;
width: auto; width: auto;
color: var(--primary-text-color); color: var(--primary-text-color);
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
} }
.select-anchor { .select-anchor {
height: 48px; height: var(--control-select-menu-height);
padding: 6px 10px; padding: var(--control-select-menu-padding);
overflow: hidden; overflow: hidden;
position: relative; position: relative;
cursor: pointer; cursor: pointer;
@ -193,15 +227,12 @@ export class HaControlSelectMenu extends SelectBase {
--mdc-ripple-color: var(--control-select-menu-background-color); --mdc-ripple-color: var(--control-select-menu-background-color);
/* For safari border-radius overflow */ /* For safari border-radius overflow */
z-index: 0; z-index: 0;
font-size: inherit;
transition: color 180ms ease-in-out; transition: color 180ms ease-in-out;
gap: 10px; gap: 10px;
width: 100%; width: 100%;
user-select: none; user-select: none;
font-size: 14px;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
line-height: 20px;
letter-spacing: 0.25px; letter-spacing: 0.25px;
} }
.content { .content {
@ -223,8 +254,7 @@ export class HaControlSelectMenu extends SelectBase {
} }
.label { .label {
font-size: 12px; font-size: 0.85em;
line-height: 16px;
letter-spacing: 0.4px; letter-spacing: 0.4px;
} }

View File

@ -1,11 +1,13 @@
/* eslint-disable lit/prefer-static-styles */
import { mdiEye, mdiEyeOff } from "@mdi/js"; import { mdiEye, mdiEyeOff } from "@mdi/js";
import { import {
css,
CSSResultGroup, CSSResultGroup,
html,
LitElement, LitElement,
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
css,
html,
nothing,
} from "lit"; } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
@ -32,7 +34,7 @@ export class HaFormString extends LitElement implements HaFormElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@state() private _unmaskedPassword = false; @state() protected unmaskedPassword = false;
@query("ha-textfield") private _input?: HaTextField; @query("ha-textfield") private _input?: HaTextField;
@ -43,14 +45,11 @@ export class HaFormString extends LitElement implements HaFormElement {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
const isPassword = MASKED_FIELDS.some((field) =>
this.schema.name.includes(field)
);
return html` return html`
<ha-textfield <ha-textfield
.type=${!isPassword .type=${!this.isPassword
? this._stringType ? this.stringType
: this._unmaskedPassword : this.unmaskedPassword
? "text" ? "text"
: "password"} : "password"}
.label=${this.label} .label=${this.label}
@ -62,7 +61,7 @@ export class HaFormString extends LitElement implements HaFormElement {
.autoValidate=${this.schema.required} .autoValidate=${this.schema.required}
.name=${this.schema.name} .name=${this.schema.name}
.autocomplete=${this.schema.autocomplete} .autocomplete=${this.schema.autocomplete}
.suffix=${isPassword .suffix=${this.isPassword
? // reserve some space for the icon. ? // reserve some space for the icon.
html`<div style="width: 24px"></div>` html`<div style="width: 24px"></div>`
: this.schema.description?.suffix} : this.schema.description?.suffix}
@ -70,14 +69,19 @@ export class HaFormString extends LitElement implements HaFormElement {
@input=${this._valueChanged} @input=${this._valueChanged}
@change=${this._valueChanged} @change=${this._valueChanged}
></ha-textfield> ></ha-textfield>
${isPassword ${this.renderIcon()}
? html`<ha-icon-button `;
toggles }
.label=${`${this._unmaskedPassword ? "Hide" : "Show"} password`}
@click=${this._toggleUnmaskedPassword} protected renderIcon() {
.path=${this._unmaskedPassword ? mdiEyeOff : mdiEye} if (!this.isPassword) return nothing;
></ha-icon-button>` return html`
: ""} <ha-icon-button
toggles
.label=${`${this.unmaskedPassword ? "Hide" : "Show"} password`}
@click=${this.toggleUnmaskedPassword}
.path=${this.unmaskedPassword ? mdiEyeOff : mdiEye}
></ha-icon-button>
`; `;
} }
@ -87,11 +91,11 @@ export class HaFormString extends LitElement implements HaFormElement {
} }
} }
private _toggleUnmaskedPassword(): void { protected toggleUnmaskedPassword(): void {
this._unmaskedPassword = !this._unmaskedPassword; this.unmaskedPassword = !this.unmaskedPassword;
} }
private _valueChanged(ev: Event): void { protected _valueChanged(ev: Event): void {
let value: string | undefined = (ev.target as HaTextField).value; let value: string | undefined = (ev.target as HaTextField).value;
if (this.data === value) { if (this.data === value) {
return; return;
@ -104,7 +108,7 @@ export class HaFormString extends LitElement implements HaFormElement {
}); });
} }
private get _stringType(): string { protected get stringType(): string {
if (this.schema.format) { if (this.schema.format) {
if (["email", "url"].includes(this.schema.format)) { if (["email", "url"].includes(this.schema.format)) {
return this.schema.format; return this.schema.format;
@ -116,6 +120,10 @@ export class HaFormString extends LitElement implements HaFormElement {
return "text"; return "text";
} }
protected get isPassword(): boolean {
return MASKED_FIELDS.some((field) => this.schema.name.includes(field));
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host { :host {

View File

@ -1,3 +1,4 @@
/* eslint-disable lit/prefer-static-styles */
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@ -135,7 +136,7 @@ export class HaForm extends LitElement implements HaFormElement {
.required=${item.required || false} .required=${item.required || false}
.context=${this._generateContext(item)} .context=${this._generateContext(item)}
></ha-selector>` ></ha-selector>`
: dynamicElement(`ha-form-${item.type}`, { : dynamicElement(this.fieldElementName(item.type), {
schema: item, schema: item,
data: getValue(this.data, item), data: getValue(this.data, item),
label: this._computeLabel(item, this.data), label: this._computeLabel(item, this.data),
@ -152,6 +153,10 @@ export class HaForm extends LitElement implements HaFormElement {
`; `;
} }
protected fieldElementName(type: string): string {
return `ha-form-${type}`;
}
private _generateContext( private _generateContext(
schema: HaFormSchema schema: HaFormSchema
): Record<string, any> | undefined { ): Record<string, any> | undefined {
@ -169,10 +174,17 @@ export class HaForm extends LitElement implements HaFormElement {
protected createRenderRoot() { protected createRenderRoot() {
const root = super.createRenderRoot(); const root = super.createRenderRoot();
// attach it as soon as possible to make sure we fetch all events. // attach it as soon as possible to make sure we fetch all events.
root.addEventListener("value-changed", (ev) => { this.addValueChangedListener(root);
return root;
}
protected addValueChangedListener(element: Element | ShadowRoot) {
element.addEventListener("value-changed", (ev) => {
ev.stopPropagation(); ev.stopPropagation();
const schema = (ev.target as HaFormElement).schema as HaFormSchema; const schema = (ev.target as HaFormElement).schema as HaFormSchema;
if (ev.target === this) return;
const newValue = !schema.name const newValue = !schema.name
? ev.detail.value ? ev.detail.value
: { [schema.name]: ev.detail.value }; : { [schema.name]: ev.detail.value };
@ -181,7 +193,6 @@ export class HaForm extends LitElement implements HaFormElement {
value: { ...this.data, ...newValue }, value: { ...this.data, ...newValue },
}); });
}); });
return root;
} }
private _computeLabel(schema: HaFormSchema, data: HaFormDataContainer) { private _computeLabel(schema: HaFormSchema, data: HaFormDataContainer) {

View File

@ -124,6 +124,17 @@ export class HaIcon extends LitElement {
return; return;
} }
if (iconName === "home-assistant") {
const icon = (await import("../resources/home-assistant-logo-svg"))
.mdiHomeAssistant;
if (this.icon === requestedIcon) {
this._path = icon;
}
cachedIcons[iconName] = icon;
return;
}
let databaseIcon: string | undefined; let databaseIcon: string | undefined;
try { try {
databaseIcon = await getIcon(iconName); databaseIcon = await getIcon(iconName);

File diff suppressed because one or more lines are too long

View File

@ -26,6 +26,7 @@ export class HaLocationSelector extends LitElement {
protected render() { protected render() {
return html` return html`
<p>${this.label ? this.label : ""}</p>
<ha-locations-editor <ha-locations-editor
class="flex" class="flex"
.hass=${this.hass} .hass=${this.hass}
@ -78,10 +79,13 @@ export class HaLocationSelector extends LitElement {
} }
static styles = css` static styles = css`
:host { ha-locations-editor {
display: block; display: block;
height: 400px; height: 400px;
} }
p {
margin-top: 0;
}
`; `;
} }

View File

@ -237,8 +237,6 @@ export class HaTargetPicker extends LitElement {
: html`<span role="gridcell"> : html`<span role="gridcell">
<ha-icon-button <ha-icon-button
class="expand-btn mdc-chip__icon mdc-chip__icon--trailing" class="expand-btn mdc-chip__icon mdc-chip__icon--trailing"
tabindex="-1"
role="button"
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.components.target-picker.expand" "ui.components.target-picker.expand"
)} )}
@ -257,8 +255,6 @@ export class HaTargetPicker extends LitElement {
<span role="gridcell"> <span role="gridcell">
<ha-icon-button <ha-icon-button
class="mdc-chip__icon mdc-chip__icon--trailing" class="mdc-chip__icon mdc-chip__icon--trailing"
tabindex="-1"
role="button"
.label=${this.hass.localize("ui.components.target-picker.remove")} .label=${this.hass.localize("ui.components.target-picker.remove")}
.path=${mdiClose} .path=${mdiClose}
hideTooltip hideTooltip

View File

@ -15,7 +15,7 @@ class HaEntityMarker extends LitElement {
protected render() { protected render() {
return html` return html`
<div <div
class="marker" class="marker ${this.entityPicture ? "picture" : ""}"
style=${styleMap({ "border-color": this.entityColor })} style=${styleMap({ "border-color": this.entityColor })}
@click=${this._badgeTap} @click=${this._badgeTap}
> >
@ -45,7 +45,6 @@ class HaEntityMarker extends LitElement {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
box-sizing: border-box; box-sizing: border-box;
overflow: hidden;
width: 48px; width: 48px;
height: 48px; height: 48px;
font-size: var(--ha-marker-font-size, 1.5em); font-size: var(--ha-marker-font-size, 1.5em);
@ -54,6 +53,9 @@ class HaEntityMarker extends LitElement {
color: var(--primary-text-color); color: var(--primary-text-color);
background-color: var(--card-background-color); background-color: var(--card-background-color);
} }
.marker.picture {
overflow: hidden;
}
.entity-picture { .entity-picture {
background-size: cover; background-size: cover;
height: 100%; height: 100%;

View File

@ -37,6 +37,9 @@ export interface HaMapPaths {
export interface HaMapEntity { export interface HaMapEntity {
entity_id: string; entity_id: string;
color: string; color: string;
label_mode?: "name" | "state";
name?: string;
focus?: boolean;
} }
@customElement("ha-map") @customElement("ha-map")
@ -71,6 +74,8 @@ export class HaMap extends ReactiveElement {
private _mapItems: Array<Marker | Circle> = []; private _mapItems: Array<Marker | Circle> = [];
private _mapFocusItems: Array<Marker | Circle> = [];
private _mapZones: Array<Marker | Circle> = []; private _mapZones: Array<Marker | Circle> = [];
private _mapPaths: Array<Polyline | CircleMarker> = []; private _mapPaths: Array<Polyline | CircleMarker> = [];
@ -168,7 +173,7 @@ export class HaMap extends ReactiveElement {
return; return;
} }
if (!this._mapItems.length && !this.layers?.length) { if (!this._mapFocusItems.length && !this.layers?.length) {
this.leafletMap.setView( this.leafletMap.setView(
new this.Leaflet.LatLng( new this.Leaflet.LatLng(
this.hass.config.latitude, this.hass.config.latitude,
@ -180,7 +185,9 @@ export class HaMap extends ReactiveElement {
} }
let bounds = this.Leaflet.latLngBounds( let bounds = this.Leaflet.latLngBounds(
this._mapItems ? this._mapItems.map((item) => item.getLatLng()) : [] this._mapFocusItems
? this._mapFocusItems.map((item) => item.getLatLng())
: []
); );
if (this.fitZones) { if (this.fitZones) {
@ -324,6 +331,7 @@ export class HaMap extends ReactiveElement {
if (this._mapItems.length) { if (this._mapItems.length) {
this._mapItems.forEach((marker) => marker.remove()); this._mapItems.forEach((marker) => marker.remove());
this._mapItems = []; this._mapItems = [];
this._mapFocusItems = [];
} }
if (this._mapZones.length) { if (this._mapZones.length) {
@ -353,7 +361,8 @@ export class HaMap extends ReactiveElement {
if (!stateObj) { if (!stateObj) {
continue; continue;
} }
const title = computeStateName(stateObj); const customTitle = typeof entity !== "string" ? entity.name : undefined;
const title = customTitle ?? computeStateName(stateObj);
const { const {
latitude, latitude,
longitude, longitude,
@ -413,17 +422,20 @@ export class HaMap extends ReactiveElement {
// DRAW ENTITY // DRAW ENTITY
// create icon // create icon
const entityName = title const entityName =
.split(" ") typeof entity !== "string" && entity.label_mode === "state"
.map((part) => part[0]) ? this.hass.formatEntityState(stateObj)
.join("") : customTitle ??
.substr(0, 3); title
.split(" ")
.map((part) => part[0])
.join("")
.substr(0, 3);
// create marker with the icon // create marker with the icon
this._mapItems.push( const marker = Leaflet.marker([latitude, longitude], {
Leaflet.marker([latitude, longitude], { icon: Leaflet.divIcon({
icon: Leaflet.divIcon({ html: `
html: `
<ha-entity-marker <ha-entity-marker
entity-id="${getEntityId(entity)}" entity-id="${getEntityId(entity)}"
entity-name="${entityName}" entity-name="${entityName}"
@ -437,12 +449,15 @@ export class HaMap extends ReactiveElement {
} }
></ha-entity-marker> ></ha-entity-marker>
`, `,
iconSize: [48, 48], iconSize: [48, 48],
className: "", className: "",
}), }),
title: computeStateName(stateObj), title: title,
}) });
); this._mapItems.push(marker);
if (typeof entity === "string" || entity.focus !== false) {
this._mapFocusItems.push(marker);
}
// create circle around if entity has accuracy // create circle around if entity has accuracy
if (gpsAccuracy) { if (gpsAccuracy) {

View File

@ -14,6 +14,8 @@ export interface AssistPipeline {
tts_engine: string | null; tts_engine: string | null;
tts_language: string | null; tts_language: string | null;
tts_voice: string | null; tts_voice: string | null;
wake_word_entity: string | null;
wake_word_id: string | null;
} }
export interface AssistPipelineMutableParams { export interface AssistPipelineMutableParams {
@ -26,6 +28,8 @@ export interface AssistPipelineMutableParams {
tts_engine: string | null; tts_engine: string | null;
tts_language: string | null; tts_language: string | null;
tts_voice: string | null; tts_voice: string | null;
wake_word_entity: string | null;
wake_word_id: string | null;
} }
export interface assistRunListing { export interface assistRunListing {
@ -61,6 +65,19 @@ interface PipelineErrorEvent extends PipelineEventBase {
}; };
} }
interface PipelineWakeWordStartEvent extends PipelineEventBase {
type: "wake_word-start";
data: {
engine: string;
metadata: SpeechMetadata;
};
}
interface PipelineWakeWordEndEvent extends PipelineEventBase {
type: "wake_word-end";
data: { wake_word_output: { ww_id: string; timestamp: number } };
}
interface PipelineSTTStartEvent extends PipelineEventBase { interface PipelineSTTStartEvent extends PipelineEventBase {
type: "stt-start"; type: "stt-start";
data: { data: {
@ -110,6 +127,8 @@ export type PipelineRunEvent =
| PipelineRunStartEvent | PipelineRunStartEvent
| PipelineRunEndEvent | PipelineRunEndEvent
| PipelineErrorEvent | PipelineErrorEvent
| PipelineWakeWordStartEvent
| PipelineWakeWordEndEvent
| PipelineSTTStartEvent | PipelineSTTStartEvent
| PipelineSTTEndEvent | PipelineSTTEndEvent
| PipelineIntentStartEvent | PipelineIntentStartEvent
@ -126,6 +145,14 @@ export type PipelineRunOptions = (
start_stage: "stt"; start_stage: "stt";
input: { sample_rate: number }; input: { sample_rate: number };
} }
| {
start_stage: "wake_word";
input: {
sample_rate: number;
timeout?: number;
audio_seconds_to_buffer?: number;
};
}
) & { ) & {
end_stage: "stt" | "intent" | "tts"; end_stage: "stt" | "intent" | "tts";
pipeline?: string; pipeline?: string;
@ -135,9 +162,11 @@ export type PipelineRunOptions = (
export interface PipelineRun { export interface PipelineRun {
init_options?: PipelineRunOptions; init_options?: PipelineRunOptions;
events: PipelineRunEvent[]; events: PipelineRunEvent[];
stage: "ready" | "stt" | "intent" | "tts" | "done" | "error"; stage: "ready" | "wake_word" | "stt" | "intent" | "tts" | "done" | "error";
run: PipelineRunStartEvent["data"]; run: PipelineRunStartEvent["data"];
error?: PipelineErrorEvent["data"]; error?: PipelineErrorEvent["data"];
wake_word?: PipelineWakeWordStartEvent["data"] &
Partial<PipelineWakeWordEndEvent["data"]> & { done: boolean };
stt?: PipelineSTTStartEvent["data"] & stt?: PipelineSTTStartEvent["data"] &
Partial<PipelineSTTEndEvent["data"]> & { done: boolean }; Partial<PipelineSTTEndEvent["data"]> & { done: boolean };
intent?: PipelineIntentStartEvent["data"] & intent?: PipelineIntentStartEvent["data"] &
@ -167,7 +196,18 @@ export const processEvent = (
return undefined; return undefined;
} }
if (event.type === "stt-start") { if (event.type === "wake_word-start") {
run = {
...run,
stage: "wake_word",
wake_word: { ...event.data, done: false },
};
} else if (event.type === "wake_word-end") {
run = {
...run,
wake_word: { ...run.wake_word!, ...event.data, done: true },
};
} else if (event.type === "stt-start") {
run = { run = {
...run, ...run,
stage: "stt", stage: "stt",

View File

@ -19,6 +19,10 @@ import {
} from "./device_automation"; } from "./device_automation";
import { EntityRegistryEntry } from "./entity_registry"; import { EntityRegistryEntry } from "./entity_registry";
import { FrontendLocaleData } from "./translation"; import { FrontendLocaleData } from "./translation";
import {
formatListWithAnds,
formatListWithOrs,
} from "../common/string/format-list";
const triggerTranslationBaseKey = const triggerTranslationBaseKey =
"ui.panel.config.automation.editor.triggers.type"; "ui.panel.config.automation.editor.triggers.type";
@ -104,11 +108,6 @@ const tryDescribeTrigger = (
return trigger.alias; return trigger.alias;
} }
const disjunctionFormatter = new Intl.ListFormat("en", {
style: "long",
type: "disjunction",
});
// Event Trigger // Event Trigger
if (trigger.platform === "event" && trigger.event_type) { if (trigger.platform === "event" && trigger.event_type) {
const eventTypes: string[] = []; const eventTypes: string[] = [];
@ -121,7 +120,7 @@ const tryDescribeTrigger = (
eventTypes.push(trigger.event_type); eventTypes.push(trigger.event_type);
} }
const eventTypesString = disjunctionFormatter.format(eventTypes); const eventTypesString = formatListWithOrs(hass.locale, eventTypes);
return hass.localize( return hass.localize(
`${triggerTranslationBaseKey}.event.description.full`, `${triggerTranslationBaseKey}.event.description.full`,
{ eventTypes: eventTypesString } { eventTypes: eventTypesString }
@ -242,7 +241,7 @@ const tryDescribeTrigger = (
); );
} }
if (from.length !== 0) { if (from.length !== 0) {
const fromString = disjunctionFormatter.format(from); const fromString = formatListWithOrs(hass.locale, from);
base += ` from ${fromString}`; base += ` from ${fromString}`;
} }
} else { } else {
@ -283,7 +282,7 @@ const tryDescribeTrigger = (
); );
} }
if (to.length !== 0) { if (to.length !== 0) {
const toString = disjunctionFormatter.format(to); const toString = formatListWithOrs(hass.locale, to);
base += ` to ${toString}`; base += ` to ${toString}`;
} }
} else { } else {
@ -356,7 +355,7 @@ const tryDescribeTrigger = (
); );
return hass.localize(`${triggerTranslationBaseKey}.time.description.full`, { return hass.localize(`${triggerTranslationBaseKey}.time.description.full`, {
time: disjunctionFormatter.format(result), time: formatListWithOrs(hass.locale, result),
}); });
} }
@ -505,11 +504,12 @@ const tryDescribeTrigger = (
); );
} }
const entitiesString = disjunctionFormatter.format(entities); return hass.localize(`${triggerTranslationBaseKey}.zone.description.full`, {
const zonesString = disjunctionFormatter.format(zones); entity: formatListWithOrs(hass.locale, entities),
return `When ${entitiesString} ${trigger.event}s ${zonesString} ${ event: trigger.event.toString(),
zones.length > 1 ? "zones" : "zone" zone: formatListWithOrs(hass.locale, zones),
}`; numberOfZones: zones.length,
});
} }
// Geo Location Trigger // Geo Location Trigger
@ -540,11 +540,15 @@ const tryDescribeTrigger = (
); );
} }
const sourcesString = disjunctionFormatter.format(sources); return hass.localize(
const zonesString = disjunctionFormatter.format(zones); `${triggerTranslationBaseKey}.geo_location.description.full`,
return `When ${sourcesString} ${trigger.event}s ${zonesString} ${ {
zones.length > 1 ? "zones" : "zone" source: formatListWithOrs(hass.locale, sources),
}`; event: trigger.event.toString(),
zone: formatListWithOrs(hass.locale, zones),
numberOfZones: zones.length,
}
);
} }
// MQTT Trigger // MQTT Trigger
@ -583,7 +587,8 @@ const tryDescribeTrigger = (
return hass.localize( return hass.localize(
`${triggerTranslationBaseKey}.conversation.description.full`, `${triggerTranslationBaseKey}.conversation.description.full`,
{ {
sentence: disjunctionFormatter.format( sentence: formatListWithOrs(
hass.locale,
ensureArray(trigger.command).map((cmd) => `'${cmd}'`) ensureArray(trigger.command).map((cmd) => `'${cmd}'`)
), ),
} }
@ -592,7 +597,9 @@ const tryDescribeTrigger = (
// Persistent Notification Trigger // Persistent Notification Trigger
if (trigger.platform === "persistent_notification") { if (trigger.platform === "persistent_notification") {
return "When a persistent notification is updated"; return hass.localize(
`${triggerTranslationBaseKey}.persistent_notification.description.full`
);
} }
// Device Trigger // Device Trigger
@ -650,15 +657,6 @@ const tryDescribeCondition = (
return condition.alias; return condition.alias;
} }
const conjunctionFormatter = new Intl.ListFormat("en", {
style: "long",
type: "conjunction",
});
const disjunctionFormatter = new Intl.ListFormat("en", {
style: "long",
type: "disjunction",
});
if (!condition.condition) { if (!condition.condition) {
const shorthands: Array<"and" | "or" | "not"> = ["and", "or", "not"]; const shorthands: Array<"and" | "or" | "not"> = ["and", "or", "not"];
for (const key of shorthands) { for (const key of shorthands) {
@ -756,8 +754,8 @@ const tryDescribeCondition = (
if (entities.length !== 0) { if (entities.length !== 0) {
const entitiesString = const entitiesString =
condition.match === "any" condition.match === "any"
? disjunctionFormatter.format(entities) ? formatListWithOrs(hass.locale, entities)
: conjunctionFormatter.format(entities); : formatListWithAnds(hass.locale, entities);
base += ` ${entitiesString} ${ base += ` ${entitiesString} ${
condition.entity_id.length > 1 ? "are" : "is" condition.entity_id.length > 1 ? "are" : "is"
}`; }`;
@ -812,7 +810,7 @@ const tryDescribeCondition = (
states.push("a state"); states.push("a state");
} }
const statesString = disjunctionFormatter.format(states); const statesString = formatListWithOrs(hass.locale, states);
base += ` ${statesString}`; base += ` ${statesString}`;
if (condition.for) { if (condition.for) {
@ -902,7 +900,7 @@ const tryDescribeCondition = (
`ui.panel.config.automation.editor.conditions.type.time.weekdays.${d}` `ui.panel.config.automation.editor.conditions.type.time.weekdays.${d}`
) )
); );
result += " day is " + disjunctionFormatter.format(localizedDays); result += " day is " + formatListWithOrs(hass.locale, localizedDays);
} }
return result; return result;
@ -981,8 +979,8 @@ const tryDescribeCondition = (
); );
} }
const entitiesString = disjunctionFormatter.format(entities); const entitiesString = formatListWithOrs(hass.locale, entities);
const zonesString = disjunctionFormatter.format(zones); const zonesString = formatListWithOrs(hass.locale, zones);
return hass.localize( return hass.localize(
`${conditionsTranslationBaseKey}.zone.description.full`, `${conditionsTranslationBaseKey}.zone.description.full`,
{ {

View File

@ -17,12 +17,14 @@ export interface LovelacePanelConfig {
mode: "yaml" | "storage"; mode: "yaml" | "storage";
} }
export type LovelaceStrategyConfig = {
type: string;
[key: string]: any;
};
export interface LovelaceConfig { export interface LovelaceConfig {
title?: string; title?: string;
strategy?: { strategy?: LovelaceStrategyConfig;
type: string;
options?: Record<string, unknown>;
};
views: LovelaceViewConfig[]; views: LovelaceViewConfig[];
background?: string; background?: string;
} }
@ -81,10 +83,7 @@ export interface LovelaceViewConfig {
index?: number; index?: number;
title?: string; title?: string;
type?: string; type?: string;
strategy?: { strategy?: LovelaceStrategyConfig;
type: string;
options?: Record<string, unknown>;
};
badges?: Array<string | LovelaceBadgeConfig>; badges?: Array<string | LovelaceBadgeConfig>;
cards?: LovelaceCardConfig[]; cards?: LovelaceCardConfig[];
path?: string; path?: string;

View File

@ -5,7 +5,6 @@ import {
mdiCodeBraces, mdiCodeBraces,
mdiDevices, mdiDevices,
mdiGestureDoubleTap, mdiGestureDoubleTap,
mdiHomeAssistant,
mdiMapMarker, mdiMapMarker,
mdiMapMarkerRadius, mdiMapMarkerRadius,
mdiMessageAlert, mdiMessageAlert,
@ -18,6 +17,8 @@ import {
mdiWebhook, mdiWebhook,
} from "@mdi/js"; } from "@mdi/js";
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
export const TRIGGER_TYPES = { export const TRIGGER_TYPES = {
calendar: mdiCalendar, calendar: mdiCalendar,
device: mdiDevices, device: mdiDevices,

12
src/data/wake_word.ts Normal file
View File

@ -0,0 +1,12 @@
import type { HomeAssistant } from "../types";
export interface WakeWord {
id: string;
name: string;
}
export const fetchWakeWordInfo = (hass: HomeAssistant, entity_id: string) =>
hass.callWS<{ wake_words: WakeWord[] }>({
type: "wake_word/info",
entity_id,
});

View File

@ -1,137 +0,0 @@
import "@material/mwc-button/mwc-button";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-alert";
import "../../components/ha-dialog";
import { haStyle, haStyleDialog } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import { AliasesDialogParams } from "./show-dialog-aliases";
import "../../components/ha-aliases-editor";
@customElement("dialog-aliases")
class DialogAliases extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _error?: string;
@state() private _params?: AliasesDialogParams;
@state() private _aliases!: string[];
@state() private _submitting = false;
public async showDialog(params: AliasesDialogParams): Promise<void> {
this._params = params;
this._error = undefined;
this._aliases =
this._params.aliases?.length > 0
? [...this._params.aliases].sort()
: [""];
await this.updateComplete;
}
public closeDialog(): void {
this._error = "";
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._params) {
return nothing;
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${this.hass.localize("ui.dialogs.aliases.heading", {
name: this._params.name,
})}
>
<div>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<ha-aliases-editor
.hass=${this.hass}
.aliases=${this._aliases}
@value-changed=${this._aliasesChanged}
></ha-aliases-editor>
</div>
<mwc-button
slot="secondaryAction"
@click=${this.closeDialog}
.disabled=${this._submitting}
>
${this.hass.localize("ui.common.cancel")}
</mwc-button>
<mwc-button
slot="primaryAction"
@click=${this._updateAliases}
.disabled=${this._submitting}
>
${this.hass.localize("ui.dialogs.aliases.save")}
</mwc-button>
</ha-dialog>
`;
}
private _aliasesChanged(ev: CustomEvent): void {
this._aliases = ev.detail.value;
}
private async _updateAliases(): Promise<void> {
this._submitting = true;
const noEmptyAliases = this._aliases
.map((alias) => alias.trim())
.filter((alias) => alias);
try {
await this._params!.updateAliases(noEmptyAliases);
this.closeDialog();
} catch (err: any) {
this._error =
err.message || this.hass.localize("ui.dialogs.aliases.unknown_error");
} finally {
this._submitting = false;
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
.row {
margin-bottom: 8px;
}
ha-textfield {
display: block;
}
ha-icon-button {
display: block;
}
mwc-button {
margin-left: 8px;
}
#alias_input {
margin-top: 8px;
}
.alias {
border: 1px solid var(--divider-color);
border-radius: 4px;
margin-top: 4px;
--mdc-icon-button-size: 24px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-aliases": DialogAliases;
}
}

View File

@ -1,20 +0,0 @@
import { fireEvent } from "../../common/dom/fire_event";
export interface AliasesDialogParams {
name: string;
aliases: string[];
updateAliases: (aliases: string[]) => Promise<unknown>;
}
export const loadAliasesDialog = () => import("./dialog-aliases");
export const showAliasesDialog = (
element: HTMLElement,
aliasesParams: AliasesDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-aliases",
dialogImport: loadAliasesDialog,
dialogParams: aliasesParams,
});
};

View File

@ -53,7 +53,8 @@ class DialogLightColorFavorite extends LitElement {
): Promise<void> { ): Promise<void> {
this._entry = dialogParams.entry; this._entry = dialogParams.entry;
this._dialogParams = dialogParams; this._dialogParams = dialogParams;
this._updateModes(dialogParams.defaultMode); this._updateModes();
this._loadCurrentColorAndMode(dialogParams.add, dialogParams.defaultMode);
await this.updateComplete; await this.updateComplete;
} }
@ -64,7 +65,7 @@ class DialogLightColorFavorite extends LitElement {
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
private _updateModes(defaultMode?: LightPickerMode) { private _updateModes() {
const supportsTemp = lightSupportsColorMode( const supportsTemp = lightSupportsColorMode(
this.stateObj!, this.stateObj!,
LightColorMode.COLOR_TEMP LightColorMode.COLOR_TEMP
@ -81,13 +82,44 @@ class DialogLightColorFavorite extends LitElement {
} }
this._modes = modes; this._modes = modes;
this._mode = }
defaultMode ??
(this.stateObj!.attributes.color_mode private _loadCurrentColorAndMode(
? this.stateObj!.attributes.color_mode === LightColorMode.COLOR_TEMP add?: boolean,
? LightColorMode.COLOR_TEMP defaultMode?: LightPickerMode
: "color" ) {
: this._modes[0]); const attributes = this.stateObj!.attributes;
const color_mode = attributes.color_mode;
let currentColor: LightColor | undefined;
let currentMode: LightPickerMode | undefined;
if (color_mode === LightColorMode.XY) {
currentMode = "color";
// XY color not supported for favorites. Try to grab the hs or rgb instead.
if (attributes.hs_color) {
currentColor = { hs_color: attributes.hs_color };
} else if (attributes.rgb_color) {
currentColor = { rgb_color: attributes.rgb_color };
}
} else if (
color_mode === LightColorMode.COLOR_TEMP &&
attributes.color_temp_kelvin
) {
currentMode = LightColorMode.COLOR_TEMP;
currentColor = {
color_temp_kelvin: attributes.color_temp_kelvin,
};
} else if (attributes[color_mode + "_color"]) {
currentMode = "color";
currentColor = {
[color_mode + "_color"]: attributes[color_mode + "_color"],
} as LightColor;
}
if (add) {
this._color = currentColor;
}
this._mode = defaultMode ?? currentMode ?? this._modes[0];
} }
private _colorChanged(ev: CustomEvent) { private _colorChanged(ev: CustomEvent) {

View File

@ -141,6 +141,7 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
private _add = async () => { private _add = async () => {
const color = await showLightColorFavoriteDialog(this, { const color = await showLightColorFavoriteDialog(this, {
entry: this.entry!, entry: this.entry!,
add: true,
title: this.hass.localize( title: this.hass.localize(
"ui.dialogs.more_info_control.light.favorite_color.add_title" "ui.dialogs.more_info_control.light.favorite_color.add_title"
), ),

View File

@ -7,6 +7,7 @@ export interface LightColorFavoriteDialogParams {
entry: ExtEntityRegistryEntry; entry: ExtEntityRegistryEntry;
title: string; title: string;
defaultMode?: LightPickerMode; defaultMode?: LightPickerMode;
add?: boolean;
submit?: (color?: LightColor) => void; submit?: (color?: LightColor) => void;
cancel?: () => void; cancel?: () => void;
} }

View File

@ -99,6 +99,7 @@ export class MoreInfoHistory extends LitElement {
.historyData=${this._stateHistory} .historyData=${this._stateHistory}
.isLoadingData=${!this._stateHistory} .isLoadingData=${!this._stateHistory}
.showNames=${false} .showNames=${false}
.clickForMoreInfo=${false}
></state-history-charts>`}` ></state-history-charts>`}`
: ""}`; : ""}`;
} }

View File

@ -119,6 +119,8 @@ export class QuickBar extends LitElement {
this._focusSet = false; this._focusSet = false;
this._filter = ""; this._filter = "";
this._search = ""; this._search = "";
this._entityItems = undefined;
this._commandItems = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }

View File

@ -10,33 +10,50 @@
max-width: 360px; max-width: 360px;
margin: 0 auto; margin: 0 auto;
} }
.header { .header {
font-size: 1.96em;
display: flex; display: flex;
align-items: center;
justify-content: center; justify-content: center;
font-weight: 300; height: 73px;
} }
.logomark {
.header img { fill: #F2F4F9;
margin-right: 16px; }
.wordmark {
fill: #1D2126;
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
html { html {
background-color: #111111; background-color: #111111;
color: #e1e1e1; color: #e1e1e1;
} }
.wordmark {
fill: #F2F4F9;
}
} }
</style> </style>
</head> </head>
<body> <body>
<div class="content"> <div class="content">
<div class="header"> <div class="header">
<img src="/static/icons/favicon-192x192.png" height="52" alt="" /> <svg viewBox="0 0 1945 401" xmlns="http://www.w3.org/2000/svg">
Home Assistant <path d="M360 304.813C360 313.063 353.25 319.813 345 319.813H135C126.75 319.813 120 313.063 120 304.813V214.813C120 206.563 124.77 195.043 130.61 189.203L229.39 90.423C235.22 84.593 244.77 84.593 250.6 90.423L349.39 189.213C355.22 195.043 360 206.573 360 214.823V304.823V304.813Z" class="logomark"/>
<path d="M349.39 189.203L250.61 90.423C244.78 84.593 235.23 84.593 229.4 90.423L130.61 189.203C124.78 195.033 120 206.563 120 214.813V304.813C120 313.063 126.75 319.813 135 319.813H227.27L186.64 279.183C184.55 279.903 182.32 280.313 180 280.313C168.7 280.313 159.5 271.113 159.5 259.813C159.5 248.513 168.7 239.313 180 239.313C191.3 239.313 200.5 248.513 200.5 259.813C200.5 262.143 200.09 264.373 199.37 266.463L231 298.093V182.213C224.2 178.873 219.5 171.893 219.5 163.823C219.5 152.523 228.7 143.323 240 143.323C251.3 143.323 260.5 152.523 260.5 163.823C260.5 171.893 255.8 178.873 249 182.213V263.483L280.46 232.023C279.84 230.063 279.5 227.983 279.5 225.823C279.5 214.523 288.7 205.323 300 205.323C311.3 205.323 320.5 214.523 320.5 225.823C320.5 237.123 311.3 246.323 300 246.323C297.5 246.323 295.12 245.853 292.91 245.033L249 288.943V319.823H345C353.25 319.823 360 313.073 360 304.823V214.823C360 206.573 355.23 195.053 349.39 189.213V189.203Z" fill="#18BCF2"/>
<path d="M440.04 126.606H464.8V187.606L529.16 187.806V126.606H554.16V272.606H529.16V209.826L464.8 209.626V272.626H440L440.04 126.606Z" class="wordmark"/>
<path d="M626.8 171.236C641.9 171.236 654.203 176.086 663.71 185.786C673.217 195.486 677.97 207.956 677.97 223.196C677.97 238.363 673.217 250.796 663.71 260.496C654.203 270.196 641.9 275.046 626.8 275.046C611.56 275.046 599.19 270.196 589.69 260.496C580.19 250.796 575.437 238.363 575.43 223.196C575.43 207.863 580.183 195.38 589.69 185.746C599.197 176.113 611.567 171.276 626.8 171.236ZM626.8 253.756C634.873 253.756 641.433 250.91 646.48 245.216C651.527 239.523 654.047 232.116 654.04 222.996C654.04 213.89 651.52 206.516 646.48 200.876C641.44 195.236 634.88 192.423 626.8 192.436C618.533 192.436 611.867 195.25 606.8 200.876C601.733 206.503 599.193 213.876 599.18 222.996C599.18 232.116 601.72 239.523 606.8 245.216C611.88 250.91 618.547 253.756 626.8 253.756Z" class="wordmark"/>
<path d="M846.68 209.826V272.616H823.68V213.426C823.68 206.6 821.923 201.266 818.41 197.426C814.897 193.586 810.11 191.663 804.05 191.656C797.61 191.656 792.467 193.756 788.62 197.956C784.773 202.156 782.853 208.033 782.86 215.586V272.586H759.67V213.426C759.67 206.6 757.96 201.266 754.54 197.426C751.12 193.586 746.383 191.663 740.33 191.656C733.89 191.656 728.717 193.756 724.81 197.956C720.903 202.156 718.95 208.033 718.95 215.586V272.586H695.32V173.976H717.32L718.1 183.446C723.833 175.046 733.167 170.846 746.1 170.846C753.653 170.846 760.197 172.41 765.73 175.536C771.258 178.659 775.662 183.441 778.32 189.206C780.708 183.505 784.932 178.765 790.32 175.736C795.88 172.476 802.503 170.846 810.19 170.846C821.39 170.846 830.277 174.296 836.85 181.196C843.423 188.096 846.7 197.64 846.68 209.826Z" class="wordmark"/>
<path d="M961.28 231.986H885.7C886.48 239.406 889.28 245.073 894.1 248.986C898.92 252.9 905.04 254.846 912.46 254.826C924.5 254.826 932.93 249.826 937.75 239.826L957.48 247.636C953.937 256.093 947.799 263.206 939.95 267.946C931.95 272.84 922.787 275.283 912.46 275.276C897.873 275.276 886.04 270.506 876.96 260.966C867.88 251.426 863.337 238.91 863.33 223.416C863.33 207.923 867.907 195.326 877.06 185.626C886.213 175.926 898.173 171.076 912.94 171.076C927.46 171.076 939.147 175.86 948 185.426C956.853 194.993 961.28 207.593 961.28 223.226V231.986ZM886.09 215.376H937.26C936.8 207.376 934.377 201.273 929.99 197.066C925.603 192.86 919.693 190.78 912.26 190.826C904.927 190.826 898.927 192.956 894.26 197.216C889.593 201.476 886.87 207.53 886.09 215.376Z" class="wordmark"/>
<path d="M1113.53 238.626H1057.97L1045.97 272.626H1020.38L1073.11 126.626H1098.7L1151.53 272.626H1125.36L1113.53 238.626ZM1106.31 217.626L1085.9 159.226L1065.29 217.626H1106.31Z" class="wordmark"/>
<path d="M1197.86 256.196C1202.74 256.196 1206.6 255.236 1209.43 253.316C1210.82 252.385 1211.94 251.108 1212.69 249.61C1213.43 248.113 1213.78 246.447 1213.68 244.776C1213.68 239.51 1210.13 235.993 1203.04 234.226L1189.46 230.616C1170.79 225.536 1161.46 215.933 1161.46 201.806C1161.46 192.76 1164.75 185.37 1171.32 179.636C1177.89 173.903 1186.75 171.04 1197.88 171.046C1207.84 171.046 1216.17 173.323 1222.88 177.876C1226.12 179.984 1228.89 182.737 1231.01 185.963C1233.14 189.19 1234.57 192.82 1235.23 196.626L1214.04 201.626C1213.86 199.944 1213.33 198.317 1212.48 196.854C1211.63 195.391 1210.48 194.124 1209.11 193.136C1206.04 190.98 1202.36 189.881 1198.61 190.006C1194.38 190.006 1190.96 191.066 1188.36 193.186C1187.11 194.159 1186.1 195.412 1185.42 196.845C1184.74 198.278 1184.41 199.851 1184.45 201.436C1184.42 202.719 1184.67 203.994 1185.17 205.173C1185.68 206.353 1186.43 207.411 1187.38 208.276C1189.34 210.096 1192.37 211.56 1196.46 212.666L1209.84 215.986C1218.76 218.4 1225.56 222.046 1230.25 226.926C1232.56 229.322 1234.36 232.152 1235.57 235.253C1236.77 238.353 1237.34 241.662 1237.25 244.986C1237.25 254.24 1233.77 261.596 1226.8 267.056C1219.83 272.516 1210.23 275.253 1197.99 275.266C1186.66 275.266 1177.57 272.776 1170.74 267.796C1167.44 265.455 1164.65 262.454 1162.57 258.982C1160.48 255.51 1159.14 251.643 1158.63 247.626L1180.21 243.816C1180.34 245.633 1180.88 247.398 1181.78 248.978C1182.69 250.559 1183.94 251.915 1185.44 252.946C1188.6 255.116 1192.72 256.196 1197.86 256.196Z" class="wordmark"/>
<path d="M1287.46 256.196C1292.35 256.196 1296.2 255.196 1299.04 253.316C1300.43 252.383 1301.55 251.104 1302.29 249.607C1303.03 248.11 1303.37 246.445 1303.28 244.776C1303.28 239.51 1299.73 235.993 1292.64 234.226L1279.07 230.616C1260.4 225.536 1251.07 215.933 1251.07 201.806C1251.07 192.76 1254.36 185.37 1260.93 179.636C1267.5 173.903 1276.36 171.04 1287.49 171.046C1297.45 171.046 1305.78 173.323 1312.49 177.876C1315.73 179.985 1318.5 182.739 1320.62 185.964C1322.75 189.19 1324.19 192.82 1324.85 196.626L1303.65 201.626C1303.47 199.944 1302.94 198.317 1302.09 196.854C1301.24 195.391 1300.09 194.124 1298.72 193.136C1295.65 190.98 1291.97 189.881 1288.22 190.006C1283.99 190.006 1280.57 191.066 1277.97 193.186C1276.72 194.16 1275.71 195.414 1275.03 196.846C1274.36 198.279 1274.02 199.851 1274.06 201.436C1274.03 202.719 1274.28 203.994 1274.79 205.173C1275.29 206.353 1276.04 207.411 1276.99 208.276C1278.99 210.096 1281.99 211.556 1286.08 212.666L1299.46 215.986C1308.37 218.4 1315.17 222.046 1319.87 226.926C1322.17 229.323 1323.98 232.154 1325.18 235.254C1326.38 238.354 1326.96 241.662 1326.87 244.986C1326.87 254.24 1323.39 261.596 1316.42 267.056C1309.45 272.516 1299.85 275.253 1287.61 275.266C1276.28 275.266 1267.19 272.776 1260.36 267.796C1257.06 265.455 1254.27 262.454 1252.19 258.982C1250.1 255.51 1248.76 251.643 1248.25 247.626L1269.84 243.816C1269.97 245.633 1270.5 247.397 1271.41 248.978C1272.31 250.558 1273.56 251.915 1275.06 252.946C1278.2 255.116 1282.32 256.196 1287.46 256.196Z" class="wordmark"/>
<path d="M1341.91 139.826C1341.88 137.852 1342.24 135.891 1342.97 134.059C1343.71 132.227 1344.81 130.562 1346.2 129.163C1347.6 127.764 1349.26 126.659 1351.09 125.914C1352.92 125.169 1354.88 124.799 1356.85 124.826C1358.81 124.796 1360.75 125.171 1362.56 125.926C1364.36 126.681 1366 127.802 1367.35 129.216C1370.14 132.026 1371.71 135.826 1371.71 139.786C1371.71 143.747 1370.14 147.546 1367.35 150.356C1366 151.786 1364.38 152.921 1362.57 153.687C1360.76 154.453 1358.81 154.834 1356.85 154.806C1354.88 154.832 1352.93 154.45 1351.11 153.684C1349.29 152.918 1347.66 151.785 1346.3 150.356C1344.9 148.988 1343.78 147.35 1343.03 145.54C1342.27 143.731 1341.89 141.787 1341.91 139.826ZM1368.67 174.006V272.636H1345.03V173.976L1368.67 174.006Z" class="wordmark"/>
<path d="M1425.85 256.196C1430.73 256.196 1434.59 255.236 1437.42 253.316C1438.81 252.384 1439.93 251.106 1440.68 249.609C1441.42 248.112 1441.76 246.446 1441.67 244.776C1441.67 239.51 1438.12 235.993 1431.02 234.226L1417.45 230.616C1398.78 225.536 1389.45 215.933 1389.45 201.806C1389.45 192.76 1392.74 185.37 1399.31 179.636C1405.88 173.903 1414.74 171.04 1425.88 171.046C1435.84 171.046 1444.17 173.323 1450.88 177.876C1454.11 179.987 1456.88 182.741 1459.01 185.967C1461.13 189.193 1462.57 192.821 1463.23 196.626L1442.04 201.626C1441.86 199.944 1441.33 198.317 1440.48 196.854C1439.63 195.391 1438.48 194.124 1437.11 193.136C1434.04 190.982 1430.36 189.884 1426.61 190.006C1422.38 190.006 1418.96 191.066 1416.35 193.186C1415.1 194.161 1414.1 195.415 1413.42 196.848C1412.74 198.281 1412.41 199.852 1412.45 201.436C1412.42 202.719 1412.67 203.994 1413.17 205.173C1413.68 206.353 1414.43 207.411 1415.38 208.276C1417.33 210.096 1420.36 211.56 1424.46 212.666L1437.84 215.986C1446.76 218.4 1453.56 222.046 1458.25 226.926C1460.56 229.322 1462.36 232.152 1463.57 235.253C1464.77 238.353 1465.34 241.662 1465.25 244.986C1465.25 254.24 1461.77 261.596 1454.8 267.056C1447.83 272.516 1438.23 275.253 1425.99 275.266C1414.66 275.266 1405.58 272.776 1398.75 267.796C1395.44 265.458 1392.66 262.458 1390.57 258.985C1388.49 255.513 1387.15 251.645 1386.64 247.626L1408.22 243.816C1408.35 245.633 1408.88 247.397 1409.79 248.978C1410.69 250.558 1411.94 251.915 1413.44 252.946C1416.58 255.116 1420.7 256.196 1425.85 256.196Z" class="wordmark"/>
<path d="M1535.86 272.606C1530.23 273.964 1524.46 274.718 1518.67 274.856C1508.71 274.856 1500.95 272.12 1495.38 266.646C1489.81 261.173 1487.03 253.036 1487.03 242.236V193.996H1471.89V173.996H1487.03V143.216H1510.47V173.976H1533.22V193.976H1510.47V237.976C1510.47 248.643 1514.6 253.976 1522.87 253.976C1526.29 253.833 1529.67 253.14 1532.87 251.926L1535.86 272.606Z" class="wordmark"/>
<path d="M1615.01 272.606C1614.16 268.914 1613.6 265.158 1613.35 261.376C1610.28 265.78 1606.08 269.273 1601.19 271.486C1595.48 274.09 1589.26 275.375 1582.98 275.246C1572.17 275.246 1563.5 272.366 1556.98 266.606C1550.46 260.846 1547.21 253.05 1547.22 243.216C1547.22 233.576 1550.43 226.04 1556.84 220.606C1563.25 215.173 1572.02 212.453 1583.15 212.446H1612.8V208.646C1612.8 202.46 1610.99 197.836 1607.38 194.776C1603.77 191.716 1598.71 190.186 1592.2 190.186C1580.68 190.186 1572.41 195.003 1567.39 204.636L1550.21 194.576C1558.54 178.89 1572.96 171.046 1593.47 171.046C1605.9 171.046 1616.08 174.236 1624.03 180.616C1631.98 186.996 1635.98 197.38 1636.03 211.766V251.026C1636.03 262.153 1636.61 269.346 1637.78 272.606H1615.01ZM1613.01 228.756H1588.69C1583.16 228.756 1578.87 230.026 1575.8 232.566C1574.28 233.837 1573.08 235.441 1572.28 237.254C1571.48 239.066 1571.12 241.039 1571.21 243.016C1571.21 247.636 1572.77 251.153 1575.9 253.566C1579.03 255.976 1583.48 257.176 1589.28 257.176C1596.24 257.176 1601.94 254.98 1606.37 250.586C1610.8 246.193 1613.02 239.666 1613.01 231.006V228.756Z" class="wordmark"/>
<path d="M1749.53 211.766V272.606H1726V216.826C1726 207.446 1723.79 201.033 1719.36 197.586C1715.18 194.155 1709.92 192.323 1704.51 192.416C1701.37 192.323 1698.24 192.853 1695.31 193.975C1692.38 195.098 1689.69 196.791 1687.42 198.956C1682.87 203.316 1680.6 209.533 1680.59 217.606V272.606H1656.96V173.976H1679.12L1679.9 184.616C1682.96 180.236 1687.19 176.803 1692.11 174.706C1697.6 172.234 1703.57 170.986 1709.59 171.046C1722.35 171.046 1732.2 174.576 1739.13 181.636C1746.06 188.696 1749.53 198.74 1749.53 211.766Z" class="wordmark"/>
<path d="M1824.98 272.606C1819.35 273.952 1813.59 274.696 1807.8 274.826C1797.84 274.826 1790.08 272.09 1784.51 266.616C1778.94 261.143 1776.16 253.006 1776.16 242.206V193.996H1761.02V173.996H1776.16V143.216H1799.59V173.976H1822.35V193.976H1799.59V237.976C1799.59 248.643 1803.73 253.976 1812 253.976C1815.42 253.833 1818.8 253.14 1822 251.926L1824.98 272.606Z" class="wordmark"/>
</svg>
</div> </div>
<ha-authorize><p>Initializing</p></ha-authorize> <ha-authorize></ha-authorize>
</div> </div>
<%= renderTemplate("_js_base.html.template") %> <%= renderTemplate("_js_base.html.template") %>
<%= renderTemplate("_preload_roboto.html.template") %> <%= renderTemplate("_preload_roboto.html.template") %>

File diff suppressed because one or more lines are too long

View File

@ -45,13 +45,12 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
public hassSubscribe(): Array<UnsubscribeFunc | Promise<UnsubscribeFunc>> { public hassSubscribe(): Array<UnsubscribeFunc | Promise<UnsubscribeFunc>> {
return [ return [
subscribeConfigFlowInProgress(this.hass, (flows) => { subscribeConfigFlowInProgress(this.hass, (flows) => {
this._discovered = flows; this._discovered = flows.filter(
(flow) => !HIDDEN_DOMAINS.has(flow.handler)
);
const integrations: Set<string> = new Set(); const integrations: Set<string> = new Set();
for (const flow of flows) { for (const flow of this._discovered) {
// To render title placeholders integrations.add(flow.handler);
if (flow.context.title_placeholders) {
integrations.add(flow.handler);
}
} }
this.hass.loadBackendTranslation("title", Array.from(integrations)); this.hass.loadBackendTranslation("title", Array.from(integrations));
}), }),
@ -60,12 +59,14 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
(messages) => { (messages) => {
let fullUpdate = false; let fullUpdate = false;
const newEntries: ConfigEntry[] = []; const newEntries: ConfigEntry[] = [];
const integrations: Set<string> = new Set();
messages.forEach((message) => { messages.forEach((message) => {
if (message.type === null || message.type === "added") { if (message.type === null || message.type === "added") {
if (HIDDEN_DOMAINS.has(message.entry.domain)) { if (HIDDEN_DOMAINS.has(message.entry.domain)) {
return; return;
} }
newEntries.push(message.entry); newEntries.push(message.entry);
integrations.add(message.entry.domain);
if (message.type === null) { if (message.type === null) {
fullUpdate = true; fullUpdate = true;
} }
@ -86,6 +87,7 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
if (!newEntries.length && !fullUpdate) { if (!newEntries.length && !fullUpdate) {
return; return;
} }
this.hass.loadBackendTranslation("title", Array.from(integrations));
const existingEntries = fullUpdate ? [] : this._entries; const existingEntries = fullUpdate ? [] : this._entries;
this._entries = [...existingEntries!, ...newEntries]; this._entries = [...existingEntries!, ...newEntries];
}, },

View File

@ -62,17 +62,16 @@ export class HaConfigApplicationCredentials extends LitElement {
), ),
direction: "asc", direction: "asc",
grows: true, grows: true,
template: (_, entry: ApplicationCredential) => html`${entry.name}`, template: (entry) => html`${entry.name}`,
}, },
clientId: { client_id: {
title: localize( title: localize(
"ui.panel.config.application_credentials.picker.headers.client_id" "ui.panel.config.application_credentials.picker.headers.client_id"
), ),
width: "30%", width: "30%",
direction: "asc", direction: "asc",
hidden: narrow, hidden: narrow,
template: (_, entry: ApplicationCredential) => template: (entry) => html`${entry.client_id}`,
html`${entry.client_id}`,
}, },
application: { application: {
title: localize( title: localize(
@ -81,7 +80,7 @@ export class HaConfigApplicationCredentials extends LitElement {
sortable: true, sortable: true,
width: "30%", width: "30%",
direction: "asc", direction: "asc",
template: (_, entry) => html`${domainToName(localize, entry.domain)}`, template: (entry) => html`${domainToName(localize, entry.domain)}`,
}, },
}; };

View File

@ -1,21 +1,19 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "@material/mwc-list/mwc-list"; import "@material/mwc-list/mwc-list";
import { mdiPencil } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators"; import { property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { stringCompare } from "../../../common/string/compare";
import "../../../components/ha-alert"; import "../../../components/ha-alert";
import { createCloseHeading } from "../../../components/ha-dialog"; import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-picture-upload"; import "../../../components/ha-picture-upload";
import type { HaPictureUpload } from "../../../components/ha-picture-upload"; import type { HaPictureUpload } from "../../../components/ha-picture-upload";
import "../../../components/ha-textfield"; import "../../../components/ha-textfield";
import { AreaRegistryEntryMutableParams } from "../../../data/area_registry"; import { AreaRegistryEntryMutableParams } from "../../../data/area_registry";
import { showAliasesDialog } from "../../../dialogs/aliases/show-dialog-aliases";
import { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog"; import { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog";
import { ValueChangedEvent, HomeAssistant } from "../../../types"; import { ValueChangedEvent, HomeAssistant } from "../../../types";
import { haStyleDialog } from "../../../resources/styles"; import { haStyleDialog } from "../../../resources/styles";
import { AreaRegistryDetailDialogParams } from "./show-dialog-area-registry-detail"; import { AreaRegistryDetailDialogParams } from "./show-dialog-area-registry-detail";
import "../../../components/ha-aliases-editor";
const cropOptions: CropOptions = { const cropOptions: CropOptions = {
round: false, round: false,
@ -69,8 +67,8 @@ class DialogAreaDetail extends LitElement {
.heading=${createCloseHeading( .heading=${createCloseHeading(
this.hass, this.hass,
entry entry
? entry.name ? this.hass.localize("ui.panel.config.areas.editor.update_area")
: this.hass.localize("ui.panel.config.areas.editor.default_name") : this.hass.localize("ui.panel.config.areas.editor.create_area")
)} )}
> >
<div> <div>
@ -80,14 +78,16 @@ class DialogAreaDetail extends LitElement {
<div class="form"> <div class="form">
${entry ${entry
? html` ? html`
<div> <ha-settings-row>
${this.hass.localize( <span slot="heading">
"ui.panel.config.areas.editor.area_id" ${this.hass.localize(
)}: "ui.panel.config.areas.editor.area_id"
${entry.area_id} )}
</div> </span>
<span slot="description"> ${entry.area_id} </span>
</ha-settings-row>
` `
: ""} : nothing}
<ha-textfield <ha-textfield
.value=${this._name} .value=${this._name}
@ -108,75 +108,40 @@ class DialogAreaDetail extends LitElement {
@change=${this._pictureChanged} @change=${this._pictureChanged}
></ha-picture-upload> ></ha-picture-upload>
<div class="label"> <h3 class="header">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.areas.editor.aliases_section" "ui.panel.config.areas.editor.aliases_section"
)} )}
</div> </h3>
<mwc-list class="aliases" @action=${this._handleAliasesClicked}>
<mwc-list-item .twoline=${this._aliases.length > 0} hasMeta> <p class="description">
<span>
${this._aliases.length > 0
? this.hass.localize(
"ui.panel.config.areas.editor.configured_aliases",
{ count: this._aliases.length }
)
: this.hass.localize(
"ui.panel.config.areas.editor.no_aliases"
)}
</span>
<span slot="secondary">
${[...this._aliases]
.sort((a, b) =>
stringCompare(a, b, this.hass.locale.language)
)
.join(", ")}
</span>
<ha-svg-icon slot="meta" .path=${mdiPencil}></ha-svg-icon>
</mwc-list-item>
</mwc-list>
<div class="secondary">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.areas.editor.aliases_description" "ui.panel.config.areas.editor.aliases_description"
)} )}
</div> </p>
<ha-aliases-editor
.hass=${this.hass}
.aliases=${this._aliases}
@value-changed=${this._aliasesChanged}
></ha-aliases-editor>
</div> </div>
</div> </div>
${entry <mwc-button slot="primaryAction" @click=${this.closeDialog}>
? html` ${this.hass.localize("ui.common.cancel")}
<mwc-button </mwc-button>
slot="secondaryAction"
class="warning"
@click=${this._deleteEntry}
.disabled=${this._submitting}
>
${this.hass.localize("ui.panel.config.areas.editor.delete")}
</mwc-button>
`
: nothing}
<mwc-button <mwc-button
slot="primaryAction" slot="primaryAction"
@click=${this._updateEntry} @click=${this._updateEntry}
.disabled=${nameInvalid || this._submitting} .disabled=${nameInvalid || this._submitting}
> >
${entry ${entry
? this.hass.localize("ui.panel.config.areas.editor.update") ? this.hass.localize("ui.common.save")
: this.hass.localize("ui.panel.config.areas.editor.create")} : this.hass.localize("ui.common.add")}
</mwc-button> </mwc-button>
</ha-dialog> </ha-dialog>
`; `;
} }
private _handleAliasesClicked() {
showAliasesDialog(this, {
name: this._name,
aliases: this._aliases,
updateAliases: async (aliases: string[]) => {
this._aliases = aliases;
},
});
}
private _isNameValid() { private _isNameValid() {
return this._name.trim() !== ""; return this._name.trim() !== "";
} }
@ -214,15 +179,8 @@ class DialogAreaDetail extends LitElement {
} }
} }
private async _deleteEntry() { private _aliasesChanged(ev: CustomEvent): void {
this._submitting = true; this._aliases = ev.detail.value;
try {
if (await this._params!.removeEntry!()) {
this.closeDialog();
}
} finally {
this._submitting = false;
}
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@ -1,6 +1,6 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "@material/mwc-list"; import "@material/mwc-list";
import { mdiImagePlus, mdiPencil } from "@mdi/js"; import { mdiDelete, mdiDotsVertical, mdiImagePlus, mdiPencil } from "@mdi/js";
import { import {
HassEntity, HassEntity,
UnsubscribeFunc, UnsubscribeFunc,
@ -246,13 +246,32 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
.narrow=${this.narrow} .narrow=${this.narrow}
.header=${area.name} .header=${area.name}
> >
<ha-icon-button <ha-button-menu slot="toolbar-icon">
.path=${mdiPencil} <ha-icon-button
.entry=${area} slot="trigger"
@click=${this._showSettings} .label=${this.hass.localize("ui.common.menu")}
slot="toolbar-icon" .path=${mdiDotsVertical}
.label=${this.hass.localize("ui.panel.config.areas.edit_settings")} ></ha-icon-button>
></ha-icon-button>
<mwc-list-item
graphic="icon"
.entry=${area}
@click=${this._showSettings}
>
${this.hass.localize("ui.panel.config.areas.edit_settings")}
<ha-svg-icon slot="graphic" .path=${mdiPencil}> </ha-svg-icon>
</mwc-list-item>
<mwc-list-item
class="warning"
graphic="icon"
@click=${this._deleteConfirm}
>
${this.hass.localize("ui.panel.config.areas.editor.delete")}
<ha-svg-icon class="warning" slot="graphic" .path=${mdiDelete}>
</ha-svg-icon>
</mwc-list-item>
</ha-button-menu>
<div class="container"> <div class="container">
<div class="column"> <div class="column">
@ -634,31 +653,25 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
entry, entry,
updateEntry: async (values) => updateEntry: async (values) =>
updateAreaRegistryEntry(this.hass!, entry!.area_id, values), updateAreaRegistryEntry(this.hass!, entry!.area_id, values),
removeEntry: async () => { });
if ( }
!(await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.areas.delete.confirmation_title",
{ name: entry!.name }
),
text: this.hass.localize(
"ui.panel.config.areas.delete.confirmation_text"
),
dismissText: this.hass.localize("ui.common.cancel"),
confirmText: this.hass.localize("ui.common.delete"),
destructive: true,
}))
) {
return false;
}
try { private async _deleteConfirm() {
await deleteAreaRegistryEntry(this.hass!, entry!.area_id); const area = this._area(this.areaId, this._areas);
afterNextRender(() => history.back()); showConfirmationDialog(this, {
return true; title: this.hass.localize(
} catch (err: any) { "ui.panel.config.areas.delete.confirmation_title",
return false; { name: area!.name }
} ),
text: this.hass.localize(
"ui.panel.config.areas.delete.confirmation_text"
),
dismissText: this.hass.localize("ui.common.cancel"),
confirmText: this.hass.localize("ui.common.delete"),
destructive: true,
confirm: async () => {
await deleteAreaRegistryEntry(this.hass!, area!.area_id);
afterNextRender(() => history.back());
}, },
}); });
} }

View File

@ -10,7 +10,6 @@ export interface AreaRegistryDetailDialogParams {
updateEntry?: ( updateEntry?: (
updates: Partial<AreaRegistryEntryMutableParams> updates: Partial<AreaRegistryEntryMutableParams>
) => Promise<unknown>; ) => Promise<unknown>;
removeEntry?: () => Promise<boolean>;
} }
export const loadAreaRegistryDetailDialog = () => export const loadAreaRegistryDetailDialog = () =>

View File

@ -1,7 +1,13 @@
import { consume } from "@lit-labs/context"; import { consume } from "@lit-labs/context";
import { mdiDelete, mdiPlus } from "@mdi/js"; import type { SortableEvent } from "sortablejs";
import { mdiDelete, mdiPlus, mdiArrowUp, mdiArrowDown, mdiDrag } from "@mdi/js";
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit"; import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import {
loadSortable,
SortableInstance,
} from "../../../../../resources/sortable.ondemand";
import { ensureArray } from "../../../../../common/array/ensure-array"; import { ensureArray } from "../../../../../common/array/ensure-array";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-button"; import "../../../../../components/ha-button";
@ -14,6 +20,7 @@ import { ActionElement } from "../ha-automation-action-row";
import { describeCondition } from "../../../../../data/automation_i18n"; import { describeCondition } from "../../../../../data/automation_i18n";
import { fullEntitiesContext } from "../../../../../data/context"; import { fullEntitiesContext } from "../../../../../data/context";
import { EntityRegistryEntry } from "../../../../../data/entity_registry"; import { EntityRegistryEntry } from "../../../../../data/entity_registry";
import { sortableStyles } from "../../../../../resources/ha-sortable-style";
@customElement("ha-automation-action-choose") @customElement("ha-automation-action-choose")
export class HaChooseAction extends LitElement implements ActionElement { export class HaChooseAction extends LitElement implements ActionElement {
@ -27,81 +34,49 @@ export class HaChooseAction extends LitElement implements ActionElement {
@state() private _showDefault = false; @state() private _showDefault = false;
@state() private expandedUpdateFlag = false; @state() private _expandedStates: boolean[] = [];
@state() @state()
@consume({ context: fullEntitiesContext, subscribe: true }) @consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[]; _entityReg!: EntityRegistryEntry[];
private _expandLast = false;
private _sortable?: SortableInstance;
public static get defaultConfig() { public static get defaultConfig() {
return { choose: [{ conditions: [], sequence: [] }] }; return { choose: [{ conditions: [], sequence: [] }] };
} }
protected willUpdate(changedProperties: PropertyValues) { private _expandedChanged(ev) {
if (!changedProperties.has("action")) { this._expandedStates = this._expandedStates.concat();
return; this._expandedStates[ev.target!.index] = ev.detail.expanded;
}
const oldCnt =
changedProperties.get("action") === undefined ||
changedProperties.get("action").choose === undefined
? 0
: ensureArray(changedProperties.get("action").choose).length;
const newCnt = this.action.choose
? ensureArray(this.action.choose).length
: 0;
if (newCnt === oldCnt + 1) {
this.expand(newCnt - 1);
}
}
private expand(i: number) {
this.updateComplete.then(() => {
this.shadowRoot!.querySelectorAll("ha-expansion-panel")[i].expanded =
true;
this.expandedUpdateFlag = !this.expandedUpdateFlag;
});
}
private isExpanded(i: number) {
const nodes = this.shadowRoot!.querySelectorAll("ha-expansion-panel");
if (nodes[i]) {
return nodes[i].expanded;
}
return false;
}
private _expandedChanged() {
this.expandedUpdateFlag = !this.expandedUpdateFlag;
} }
private _getDescription(option, idx: number) { private _getDescription(option, idx: number) {
if (option.alias) { if (option.alias) {
return option.alias; return option.alias;
} }
if (this.isExpanded(idx)) { if (this._expandedStates[idx]) {
return ""; return "";
} }
if (!option.conditions || option.conditions.length === 0) { const conditions = ensureArray(option.conditions);
if (!conditions || conditions.length === 0) {
return this.hass.localize( return this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.no_conditions" "ui.panel.config.automation.editor.actions.type.choose.no_conditions"
); );
} }
let str = ""; let str = "";
if (typeof option.conditions[0] === "string") { if (typeof conditions[0] === "string") {
str += option.conditions[0]; str += conditions[0];
} else { } else {
str += describeCondition( str += describeCondition(conditions[0], this.hass, this._entityReg);
option.conditions[0],
this.hass,
this._entityReg
);
} }
if (option.conditions.length > 1) { if (conditions.length > 1) {
str += this.hass.localize( str += this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.option_description_additional", "ui.panel.config.automation.editor.actions.type.choose.option_description_additional",
"numberOfAdditionalConditions", "numberOfAdditionalConditions",
option.conditions.length - 1 conditions.length - 1
); );
} }
return str; return str;
@ -111,67 +86,100 @@ export class HaChooseAction extends LitElement implements ActionElement {
const action = this.action; const action = this.action;
return html` return html`
${(action.choose ? ensureArray(action.choose) : []).map( <div class="options">
(option, idx) => ${repeat(
html`<ha-card> action.choose ? ensureArray(action.choose) : [],
<ha-expansion-panel (option) => option,
leftChevron (option, idx) =>
@expanded-changed=${this._expandedChanged} html`<ha-card>
> <ha-expansion-panel
<h3 slot="header"> .index=${idx}
${this.hass.localize( leftChevron
"ui.panel.config.automation.editor.actions.type.choose.option", @expanded-changed=${this._expandedChanged}
"number", >
idx + 1 <h3 slot="header">
)}:
${this._getDescription(option, idx)}
</h3>
<ha-icon-button
slot="icons"
.idx=${idx}
.disabled=${this.disabled}
@click=${this._removeOption}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.remove_option"
)}
.path=${mdiDelete}
></ha-icon-button>
<div class="card-content">
<h4>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.conditions" "ui.panel.config.automation.editor.actions.type.choose.option",
"number",
idx + 1
)}: )}:
</h4> ${this._getDescription(option, idx)}
<ha-automation-condition </h3>
nested ${this.reOrderMode
.conditions=${ensureArray<string | Condition>( ? html`
option.conditions <ha-icon-button
)} .index=${idx}
.reOrderMode=${this.reOrderMode} slot="icons"
.disabled=${this.disabled} .label=${this.hass.localize(
.hass=${this.hass} "ui.panel.config.automation.editor.move_up"
.idx=${idx} )}
@value-changed=${this._conditionChanged} .path=${mdiArrowUp}
></ha-automation-condition> @click=${this._moveUp}
<h4> .disabled=${idx === 0}
${this.hass.localize( ></ha-icon-button>
"ui.panel.config.automation.editor.actions.type.choose.sequence" <ha-icon-button
)}: .index=${idx}
</h4> slot="icons"
<ha-automation-action .label=${this.hass.localize(
nested "ui.panel.config.automation.editor.move_down"
.actions=${ensureArray(option.sequence) || []} )}
.reOrderMode=${this.reOrderMode} .path=${mdiArrowDown}
.disabled=${this.disabled} @click=${this._moveDown}
.hass=${this.hass} .disabled=${idx ===
.idx=${idx} ensureArray(this.action.choose).length - 1}
@value-changed=${this._actionChanged} ></ha-icon-button>
></ha-automation-action> <div class="handle" slot="icons">
</div> <ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
</ha-expansion-panel> </div>
</ha-card>` `
)} : html`
<ha-icon-button
slot="icons"
.idx=${idx}
.disabled=${this.disabled}
@click=${this._removeOption}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.remove_option"
)}
.path=${mdiDelete}
></ha-icon-button>
`}
<div class="card-content">
<h4>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.conditions"
)}:
</h4>
<ha-automation-condition
nested
.conditions=${ensureArray<string | Condition>(
option.conditions
)}
.reOrderMode=${this.reOrderMode}
.disabled=${this.disabled}
.hass=${this.hass}
.idx=${idx}
@value-changed=${this._conditionChanged}
></ha-automation-condition>
<h4>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.sequence"
)}:
</h4>
<ha-automation-action
nested
.actions=${ensureArray(option.sequence) || []}
.reOrderMode=${this.reOrderMode}
.disabled=${this.disabled}
.hass=${this.hass}
.idx=${idx}
@value-changed=${this._actionChanged}
></ha-automation-action>
</div>
</ha-expansion-panel>
</ha-card>`
)}
</div>
<ha-button <ha-button
outlined outlined
.label=${this.hass.localize( .label=${this.hass.localize(
@ -212,6 +220,30 @@ export class HaChooseAction extends LitElement implements ActionElement {
`; `;
} }
protected firstUpdated() {
ensureArray(this.action.choose).forEach(() =>
this._expandedStates.push(false)
);
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("reOrderMode")) {
if (this.reOrderMode) {
this._createSortable();
} else {
this._destroySortable();
}
}
if (this._expandLast) {
const nodes = this.shadowRoot!.querySelectorAll("ha-expansion-panel");
nodes[nodes.length - 1].expanded = true;
this._expandLast = false;
}
}
private _addDefault() { private _addDefault() {
this._showDefault = true; this._showDefault = true;
} }
@ -250,6 +282,38 @@ export class HaChooseAction extends LitElement implements ActionElement {
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: { ...this.action, choose }, value: { ...this.action, choose },
}); });
this._expandLast = true;
this._expandedStates[choose.length - 1] = true;
}
private _moveUp(ev) {
const index = (ev.target as any).index;
const newIndex = index - 1;
this._move(index, newIndex);
}
private _moveDown(ev) {
const index = (ev.target as any).index;
const newIndex = index + 1;
this._move(index, newIndex);
}
private _dragged(ev: SortableEvent): void {
if (ev.oldIndex === ev.newIndex) return;
this._move(ev.oldIndex!, ev.newIndex!);
}
private _move(index: number, newIndex: number) {
const options = ensureArray(this.action.choose)!.concat();
const item = options.splice(index, 1)[0];
options.splice(newIndex, 0, item);
const expanded = this._expandedStates.splice(index, 1)[0];
this._expandedStates.splice(newIndex, 0, expanded);
fireEvent(this, "value-changed", {
value: { ...this.action, choose: options },
});
} }
private _removeOption(ev: CustomEvent) { private _removeOption(ev: CustomEvent) {
@ -258,6 +322,7 @@ export class HaChooseAction extends LitElement implements ActionElement {
? [...ensureArray(this.action.choose)] ? [...ensureArray(this.action.choose)]
: []; : [];
choose.splice(index, 1); choose.splice(index, 1);
this._expandedStates.splice(index, 1);
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: { ...this.action, choose }, value: { ...this.action, choose },
}); });
@ -274,9 +339,37 @@ export class HaChooseAction extends LitElement implements ActionElement {
}); });
} }
private async _createSortable() {
const Sortable = await loadSortable();
this._sortable = new Sortable(this.shadowRoot!.querySelector(".options")!, {
animation: 150,
fallbackClass: "sortable-fallback",
handle: ".handle",
onChoose: (evt: SortableEvent) => {
(evt.item as any).placeholder =
document.createComment("sort-placeholder");
evt.item.after((evt.item as any).placeholder);
},
onEnd: (evt: SortableEvent) => {
// put back in original location
if ((evt.item as any).placeholder) {
(evt.item as any).placeholder.replaceWith(evt.item);
delete (evt.item as any).placeholder;
}
this._dragged(evt);
},
});
}
private _destroySortable() {
this._sortable?.destroy();
this._sortable = undefined;
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,
sortableStyles,
css` css`
ha-card { ha-card {
margin: 0 0 16px 0; margin: 0 0 16px 0;
@ -295,8 +388,6 @@ export class HaChooseAction extends LitElement implements ActionElement {
font-weight: inherit; font-weight: inherit;
} }
ha-icon-button { ha-icon-button {
position: absolute;
right: 0;
inset-inline-start: initial; inset-inline-start: initial;
inset-inline-end: 0; inset-inline-end: 0;
direction: var(--direction); direction: var(--direction);
@ -310,6 +401,14 @@ export class HaChooseAction extends LitElement implements ActionElement {
.card-content { .card-content {
padding: 0 16px 16px 16px; padding: 0 16px 16px 16px;
} }
.handle {
cursor: move;
padding: 12px;
}
.handle ha-svg-icon {
pointer-events: none;
height: 24px;
}
`, `,
]; ];
} }

Some files were not shown because too many files have changed in this diff Show More