Merge pull request #10415 from home-assistant/dev (#10415)

* Use MWC components for ha-form (#10120)

* Dont create icon for supervisor (#10191)

* Fix import (#10206)

* Add "gas" device_class to customize (and sort existing ones) (#10196)

* Make zone names readable on map in dark mode (#10195)

* Tweak ha-form (#10194)

* Extract black/white row into component (#10212)

* Extract black/white row into component

* Remove unused import

* Fix dirty check/leaving automation editor (#10211)

* Add selector demo to gallery (#10213)

* Fix icon overlay for person badges (#10201)

* Convert iframe panel to Lit (#10216)

* Allow disabling an ha-form (#10218)

* Fix alarm panel badge (#10221)

* Add missing validation text (#10225)

* Apply flat polyfill globally (#10222)

* Add ha-bar to gallery (#10242)

* Handle text overflow for tabs (#10239)

* Remove "battery" device class from fixed icon list (#10246)

* Add ha-chip to gallery (#10252)

* Add netlify build script for gallery (#10253)

* Add ha-label-badge to gallery (#10248)

* Use correct build url (#10258)

* Remove "Hass.io" from translation (#10257)

* Update demo template (#10256)

* Add WebRTC stream player (#10193)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Add tamper device class for binary sensor (#10268)

* Fix missing translatable energy texts (#10230)

* Consolidate all icon button logic into `<ha-icon-button>` + ensure tooltip (#9230)

* Fix sizing / positioning error for trace graph node with subsequent branches (#10049)

* Initial support for entity category (#10266)

* Add support for device configuration URL (#10251)

* Add support for device configuration URL

* Lint

* Tweak text

* Bump mdc/mwc to 0.25.2 (#10271)

* Bumped version to 20211014.0

* Warn if iframe won't be able to load the website (#10217)

* Disable ha-form while submitting entry flow (#10290)

* Convert all warning classes to ha-alert (#10289)

* ABC automation types + use MWC (#10287)

* Add "capitalize" option to `hui-timestamp-display` (#10280)

* Add additional binary device classes to inversion list (#10152)

* Fix energy onboarding `add_solar_production` button (#10275) (#10286)

* Unify default dashboard name (#10254)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Fix icon buttons in Safari (#10293)

* Only render badge value if there is no icon and no image (#10310)

* Update MDI to v6.3.95 (#10313)

* Rename `stream_type` to `frontend_stream_type` (#10298)

* Fix translation key energy distribution solar (#10316)

* Prevent mwc-list-item from opening up quick-bar (#10317)

* Remove element resize hook (#10300)

* Improve WebRTC stream error handling and cleanup (#10302)

* Fix formatting of weather extrema temperatures (#10306)

* Ensure current active dark modes gets used for manually set themes (#10307)

* Add views dropdown and footer actions to the "move to view" dialog (#10172)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Icon Picker (#10161)

* Use maxLiveSyncPlaybackRate in ha-hls-player (#10323)

* Revise grid neutrality energy dashboard card, modify energy dashboard presentation to match (#10054)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Fix `ha-icon-button` in `ha-file-upload` (#10328)

* Use error for protection mode alert (#10315)

* Change unsupported reason container to software (#10325)

* Migrate all paper checkbox elements to mwc (#10329)

* Migrate all paper-radio elements to mwc-radio (#10327)

* Correct grid neutrality card tooltip, make consistent with new colors (#10326)

* Fix select options for add-on config (#10330)

* Migrate all paper dialogs to mwc (#10333)

* Stack gas and solar sources (#10244)

* Set default value when enabling optional value (#10247)

* Fix overflow icon color in backup dialog (#10331)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Convert default state icons (#10223)

* Convert default state icons

* update

* Update cast/src/launcher/layout/hc-cast.ts

Co-authored-by: Philip Allgaier <mail@spacegaier.de>

* Update ha-config-core.js

* Update

* Finish

* Add siren icon

* FIx

* Add curtain icons

Co-authored-by: Philip Allgaier <mail@spacegaier.de>

* Use secondary-text-color for trailing icon (#10340)

* Use svg icons for default panels (#10342)

* Tweak icon picker a bit (#10319)

* Add support for `no-state` and `entity-no-longer-available` statistic… (#10345)

* Change dark mode input fill color (#10341)

* Replace paper progress with mwc-linear-progess (#10339)

* Bumped version to 20211020.0

* Add auto slider/box mode to number entity (#10272)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Correct automation editor event action translation (#10355)

* Convert cloud account config to Lit (#10350)

* Restore proper state badge image behavior (#10369)

* Add to do list support to markdown (#10129)

* Catch error if input_datetime state is incorrect (#10237)

* Update MDI to v6.4.95 (#10389)

* Remove deprecated icons that where replaced (#10371)

* Make all automation type pickers use natural width to be able to show… (#10391)

* Trim device name from entities on device page (#10285)

* Update markdown card to allow word to be broken (#10387)

* Fix Full Calendar Background color (#10373)

* Add additional properties to zwave_js device info panel (#10132)

* Fix various `slugify()` issues + add tests (#10383)

* Add stopPropagation to move click handlers (#10379)

* Use ha-chip for alarm control panel card (#10393)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Fix timezone issues with date formatting for ES5 (#10370)

* Add automation editor to gallery (#10392)

* Use ha-chip instead of ha-label-badge for add-on capabilities (#10398)

* Do not close edit dialog when more info is escaped (#10249)

* Ensure Sortable is recreated when card editors are reopened (#10382)

* Ensure explicit `false` values from customize form get stored (#10381)

* Add running device class to binary sensor (#10400)

* Ensure consistent card look on device config page (#10386)

* Add "Keep me logged in" checkbox within login flow (#10226)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Update delay label (#10284)

* Introduced ha-icon-overflow-menu component (#10352)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Use ha-alert to warn about logs from custom integrations (#10396)

* Add support for hiding current weather in forecast card (#10267)

* Allow configuration_url to point to an internal panel (#10395)

* Bump Lit (#10409)

* Bump format js (#10405)

* Bump codemirror (#10404)

* Bump and patch material elements (#10406)

* Add blueprint scripts (#9504)

* Make device classes in logbook translatable (#10376)

* Improve device info add to Lovelace (#10413)

* Add navigation option from more-info to history (#9717)

* Move entities to center column on device page (#10412)

* Bumped version to 20211026.0

* Shrink new section titles in more-info dialog a bit (#10414)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
Co-authored-by: Philip Allgaier <mail@spacegaier.de>
Co-authored-by: Jack Wilsdon <jack.wilsdon@gmail.com>
Co-authored-by: Josh McCarty <josh@joshmccarty.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Allen Porter <allen.porter@gmail.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: chriss158 <edgi@arcor.de>
Co-authored-by: Kyle Niewiada <aav7fl@users.noreply.github.com>
Co-authored-by: MartinT <44962077+MartinTuroci@users.noreply.github.com>
Co-authored-by: Michael Irigoyen <michael@irigoyen.dev>
Co-authored-by: Allen Porter <allen@thebends.org>
Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
Co-authored-by: uvjustin <46082645+uvjustin@users.noreply.github.com>
Co-authored-by: Will Adler <will@wtadler.com>
Co-authored-by: Rogério Ribeiro <zroger499@gmail.com>
Co-authored-by: Zack Barett <arnett.zackary@gmail.com>
Co-authored-by: Raman Gupta <7243222+raman325@users.noreply.github.com>
Co-authored-by: Nathan Orick <cnathanorick@gmail.com>
Co-authored-by: Tobias Kündig <tobias@offline.ch>
Co-authored-by: Marc Hörsken <mback2k@users.noreply.github.com>
This commit is contained in:
Paulus Schoutsen 2021-10-26 13:35:46 -07:00 committed by GitHub
commit 2c9223ed80
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
388 changed files with 10121 additions and 6704 deletions

View File

@ -0,0 +1,12 @@
diff --git a/mwc-icon-button-base.js b/mwc-icon-button-base.js
index 45cdaab93ccc0a6daaaaabc01266dcdc32e46bfd..b3ea5b541597308d85f86ce6c23fd00785fda835 100644
--- a/mwc-icon-button-base.js
+++ b/mwc-icon-button-base.js
@@ -63,7 +63,6 @@ export class IconButtonBase extends LitElement {
@touchend="${this.handleRippleDeactivate}"
@touchcancel="${this.handleRippleDeactivate}"
>${this.renderRipple()}
- <i class="material-icons">${this.icon}</i>
<span
><slot></slot
></span>

View File

@ -210,6 +210,9 @@ module.exports.config = {
publicPath: publicPath(latestBuild), publicPath: publicPath(latestBuild),
isProdBuild, isProdBuild,
latestBuild, latestBuild,
defineOverlay: {
__DEMO__: true,
},
}; };
}, },
}; };

View File

@ -22,17 +22,38 @@ const getMeta = () => {
const svg = fs.readFileSync(`${ICON_PATH}/${icon.name}.svg`, { const svg = fs.readFileSync(`${ICON_PATH}/${icon.name}.svg`, {
encoding, encoding,
}); });
return { path: svg.match(/ d="([^"]+)"/)[1], name: icon.name }; return {
path: svg.match(/ d="([^"]+)"/)[1],
name: icon.name,
tags: icon.tags,
};
}); });
}; };
const addRemovedMeta = (meta) => { const addRemovedMeta = (meta) => {
const file = fs.readFileSync(REMOVED_ICONS_PATH, { encoding }); const file = fs.readFileSync(REMOVED_ICONS_PATH, { encoding });
const removed = JSON.parse(file); const removed = JSON.parse(file);
const combinedMeta = [...meta, ...removed]; const removedMeta = removed.map((removeIcon) => ({
path: removeIcon.path,
name: removeIcon.name,
tags: [],
}));
const combinedMeta = [...meta, ...removedMeta];
return combinedMeta.sort((a, b) => a.name.localeCompare(b.name)); return combinedMeta.sort((a, b) => a.name.localeCompare(b.name));
}; };
const homeAutomationTag = "Home Automation";
const orderMeta = (meta) => {
const homeAutomationMeta = meta.filter((icon) =>
icon.tags.includes(homeAutomationTag)
);
const otherMeta = meta.filter(
(icon) => !icon.tags.includes(homeAutomationTag)
);
return [...homeAutomationMeta, ...otherMeta];
};
const splitBySize = (meta) => { const splitBySize = (meta) => {
const chunks = []; const chunks = [];
const CHUNK_SIZE = 50000; const CHUNK_SIZE = 50000;
@ -77,8 +98,10 @@ const findDifferentiator = (curString, prevString) => {
}; };
gulp.task("gen-icons-json", (done) => { gulp.task("gen-icons-json", (done) => {
const meta = addRemovedMeta(getMeta()); const meta = getMeta();
const split = splitBySize(meta);
const metaAndRemoved = addRemovedMeta(meta);
const split = splitBySize(metaAndRemoved);
if (!fs.existsSync(OUTPUT_DIR)) { if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true }); fs.mkdirSync(OUTPUT_DIR, { recursive: true });
@ -116,5 +139,10 @@ gulp.task("gen-icons-json", (done) => {
JSON.stringify({ version: package.version, parts }) JSON.stringify({ version: package.version, parts })
); );
fs.writeFileSync(
path.resolve(OUTPUT_DIR, "iconList.json"),
JSON.stringify(orderMeta(meta).map((icon) => icon.name))
);
done(); done();
}); });

View File

@ -17,7 +17,6 @@ gulp.task(
process.env.NODE_ENV = "development"; process.env.NODE_ENV = "development";
}, },
"clean-hassio", "clean-hassio",
"gen-icons-json",
"gen-index-hassio-dev", "gen-index-hassio-dev",
"build-supervisor-translations", "build-supervisor-translations",
"copy-translations-supervisor", "copy-translations-supervisor",
@ -34,7 +33,6 @@ gulp.task(
process.env.NODE_ENV = "production"; process.env.NODE_ENV = "production";
}, },
"clean-hassio", "clean-hassio",
"gen-icons-json",
"build-supervisor-translations", "build-supervisor-translations",
"copy-translations-supervisor", "copy-translations-supervisor",
"build-locale-data", "build-locale-data",

View File

@ -173,6 +173,7 @@ gulp.task("webpack-dev-server-gallery", () =>
compiler: webpack(bothBuilds(createGalleryConfig, { isProdBuild: false })), compiler: webpack(bothBuilds(createGalleryConfig, { isProdBuild: false })),
contentBase: paths.gallery_output_root, contentBase: paths.gallery_output_root,
port: 8100, port: 8100,
listenHost: "0.0.0.0",
}) })
); );

View File

@ -1,4 +1,5 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import { mdiCast, mdiCastConnected } from "@mdi/js";
import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-listbox/paper-listbox"; import "@polymer/paper-listbox/paper-listbox";
import { Auth, Connection } from "home-assistant-js-websocket"; import { Auth, Connection } from "home-assistant-js-websocket";
@ -17,6 +18,7 @@ import {
import { atLeastVersion } from "../../../../src/common/config/version"; import { atLeastVersion } from "../../../../src/common/config/version";
import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute"; import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute";
import "../../../../src/components/ha-icon"; import "../../../../src/components/ha-icon";
import "../../../../src/components/ha-svg-icon";
import { import {
getLegacyLovelaceCollection, getLegacyLovelaceCollection,
getLovelaceCollection, getLovelaceCollection,
@ -73,7 +75,7 @@ class HcCast extends LitElement {
? html` ? html`
<p class="center-item"> <p class="center-item">
<mwc-button raised @click=${this._handleLaunch}> <mwc-button raised @click=${this._handleLaunch}>
<ha-icon icon="hass:cast"></ha-icon> <ha-svg-icon .path=${mdiCast}></ha-svg-icon>
Start Casting Start Casting
</mwc-button> </mwc-button>
</p> </p>
@ -111,7 +113,7 @@ class HcCast extends LitElement {
${this.castManager.status ${this.castManager.status
? html` ? html`
<mwc-button @click=${this._handleLaunch}> <mwc-button @click=${this._handleLaunch}>
<ha-icon icon="hass:cast-connected"></ha-icon> <ha-svg-icon .path=${mdiCastConnected}></ha-svg-icon>
Manage Manage
</mwc-button> </mwc-button>
` `
@ -233,7 +235,7 @@ class HcCast extends LitElement {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
mwc-button ha-icon { mwc-button ha-svg-icon {
margin-right: 8px; margin-right: 8px;
height: 18px; height: 18px;
} }

View File

@ -1,4 +1,5 @@
import "@material/mwc-button"; import "@material/mwc-button";
import { mdiCastConnected, mdiCast } from "@mdi/js";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import { import {
Auth, Auth,
@ -19,7 +20,7 @@ import {
loadTokens, loadTokens,
saveTokens, saveTokens,
} from "../../../../src/common/auth/token_storage"; } from "../../../../src/common/auth/token_storage";
import "../../../../src/components/ha-icon"; import "../../../../src/components/ha-svg-icon";
import "../../../../src/layouts/hass-loading-screen"; import "../../../../src/layouts/hass-loading-screen";
import { registerServiceWorker } from "../../../../src/util/register-service-worker"; import { registerServiceWorker } from "../../../../src/util/register-service-worker";
import "./hc-layout"; import "./hc-layout";
@ -127,11 +128,11 @@ export class HcConnect extends LitElement {
<div class="card-actions"> <div class="card-actions">
<mwc-button @click=${this._handleDemo}> <mwc-button @click=${this._handleDemo}>
Show Demo Show Demo
<ha-icon <ha-svg-icon
.icon=${this.castManager.castState === "CONNECTED" .path=${this.castManager.castState === "CONNECTED"
? "hass:cast-connected" ? mdiCastConnected
: "hass:cast"} : mdiCast}
></ha-icon> ></ha-svg-icon>
</mwc-button> </mwc-button>
<div class="spacer"></div> <div class="spacer"></div>
<mwc-button @click=${this._handleConnect}>Authorize</mwc-button> <mwc-button @click=${this._handleConnect}>Authorize</mwc-button>
@ -307,7 +308,7 @@ export class HcConnect extends LitElement {
color: darkred; color: darkred;
} }
mwc-button ha-icon { mwc-button ha-svg-icon {
margin-left: 8px; margin-left: 8px;
} }

View File

@ -1,3 +1,4 @@
import { mdiTelevision } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators"; import { customElement, state } from "lit/decorators";
import { CastManager } from "../../../src/cast/cast_manager"; import { CastManager } from "../../../src/cast/cast_manager";
@ -27,7 +28,7 @@ class CastDemoRow extends LitElement implements LovelaceRow {
return html``; return html``;
} }
return html` return html`
<ha-icon icon="hademo:television"></ha-icon> <ha-svg-icon .path=${mdiTelevision}></ha-svg-icon>
<div class="flex"> <div class="flex">
<div class="name">Show Chromecast interface</div> <div class="name">Show Chromecast interface</div>
<google-cast-launcher></google-cast-launcher> <google-cast-launcher></google-cast-launcher>
@ -72,7 +73,7 @@ class CastDemoRow extends LitElement implements LovelaceRow {
display: flex; display: flex;
align-items: center; align-items: center;
} }
ha-icon { ha-svg-icon {
padding: 8px; padding: 8px;
color: var(--paper-item-icon-color); color: var(--paper-item-icon-color);
} }

View File

@ -0,0 +1,7 @@
import { AreaRegistryEntry } from "../../../src/data/area_registry";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockAreaRegistry = (
hass: MockHomeAssistant,
data: AreaRegistryEntry[] = []
) => hass.mockWS("config/area_registry/list", () => data);

View File

@ -0,0 +1,7 @@
import { DeviceRegistryEntry } from "../../../src/data/device_registry";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockDeviceRegistry = (
hass: MockHomeAssistant,
data: DeviceRegistryEntry[] = []
) => hass.mockWS("config/device_registry/list", () => data);

View File

@ -0,0 +1,7 @@
import { EntityRegistryEntry } from "../../../src/data/entity_registry";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockEntityRegistry = (
hass: MockHomeAssistant,
data: EntityRegistryEntry[] = []
) => hass.mockWS("config/entity_registry/list", () => data);

View File

@ -0,0 +1,59 @@
import { HassioSupervisorInfo } from "../../../src/data/hassio/supervisor";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockHassioSupervisor = (hass: MockHomeAssistant) => {
hass.config.components.push("hassio");
hass.mockWS("supervisor/api", (msg) => {
if (msg.endpoint === "/supervisor/info") {
const data: HassioSupervisorInfo = {
version: "2021.10.dev0805",
version_latest: "2021.10.dev0806",
update_available: true,
channel: "dev",
arch: "aarch64",
supported: true,
healthy: true,
ip_address: "172.30.32.2",
wait_boot: 5,
timezone: "America/Los_Angeles",
logging: "info",
debug: false,
debug_block: false,
diagnostics: true,
addons: [
{
name: "Visual Studio Code",
slug: "a0d7b954_vscode",
description:
"Fully featured VSCode experience, to edit your HA config in the browser, including auto-completion!",
state: "started",
version: "3.6.2",
version_latest: "3.6.2",
update_available: false,
repository: "a0d7b954",
icon: true,
logo: true,
},
{
name: "Z-Wave JS",
slug: "core_zwave_js",
description:
"Control a ZWave network with Home Assistant Z-Wave JS",
state: "started",
version: "0.1.45",
version_latest: "0.1.45",
update_available: false,
repository: "core",
icon: true,
logo: true,
},
] as any,
addons_repositories: [
"https://github.com/hassio-addons/repository",
] as any,
};
return data;
}
return Promise.reject(`${msg.method} ${msg.endpoint} is not implemented`);
});
};

View File

@ -0,0 +1,35 @@
#!/bin/bash
TARGET_LABEL="Needs gallery preview"
if [[ "$NETLIFY" != "true" ]]; then
echo "This script can only be run on Netlify"
exit 1
fi
function createStatus() {
state="$1"
description="$2"
target_url="$3"
curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/home-assistant/frontend/statuses/$COMMIT_REF" \
-d '{"state": "'"${state}"'", "context": "Netlify/Gallery Preview Build", "description": "'"$description"'", "target_url": "'"$target_url"'"}'
}
if [[ "${PULL_REQUEST}" == "false" ]]; then
gulp build-gallery
else
if [[ "$(curl -sSLf -H "Accept: application/vnd.github.v3+json" -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/home-assistant/frontend/pulls/${REVIEW_ID}" | jq '.labels[].name' -r)" =~ "$TARGET_LABEL" ]]; then
createStatus "pending" "Building gallery preview" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID"
gulp build-gallery
if [ $? -eq 0 ]; then
createStatus "success" "Build complete" "$DEPLOY_URL"
else
createStatus "error" "Build failed" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID"
fi
else
createStatus "success" "Build was not requested by PR label"
fi
fi

View File

@ -0,0 +1,143 @@
import { Button } from "@material/mwc-button";
import { html, LitElement, css, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
import { fireEvent } from "../../../src/common/dom/fire_event";
@customElement("demo-black-white-row")
class DemoBlackWhiteRow extends LitElement {
@property() title!: string;
@property() value!: any;
@property() disabled = false;
protected render(): TemplateResult {
return html`
<div class="row">
<div class="content light">
<ha-card .header=${this.title}>
<div class="card-content">
<slot name="light"></slot>
</div>
<div class="card-actions">
<mwc-button
.disabled=${this.disabled}
@click=${this.handleSubmit}
>
Submit
</mwc-button>
</div>
</ha-card>
</div>
<div class="content dark">
<ha-card .header=${this.title}>
<div class="card-content">
<slot name="dark"></slot>
</div>
<div class="card-actions">
<mwc-button
.disabled=${this.disabled}
@click=${this.handleSubmit}
>
Submit
</mwc-button>
</div>
</ha-card>
<pre>${JSON.stringify(this.value, undefined, 2)}</pre>
</div>
</div>
`;
}
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: false,
},
"default",
{ dark: true }
);
}
handleSubmit(ev) {
const content = (ev.target as Button).closest(".content")!;
fireEvent(this, "submitted" as any, {
slot: content.classList.contains("light") ? "light" : "dark",
});
}
static styles = css`
.row {
display: flex;
}
.content {
padding: 50px 0;
background-color: var(--primary-background-color);
}
.light {
flex: 1;
padding-left: 50px;
padding-right: 50px;
box-sizing: border-box;
}
.light ha-card {
margin-left: auto;
}
.dark {
display: flex;
flex: 1;
padding-left: 50px;
box-sizing: border-box;
flex-wrap: wrap;
}
ha-card {
width: 400px;
}
pre {
width: 300px;
margin: 0 16px 0;
overflow: auto;
color: var(--primary-text-color);
}
.card-actions {
display: flex;
flex-direction: row-reverse;
border-top: none;
}
@media only screen and (max-width: 1500px) {
.light {
flex: initial;
}
}
@media only screen and (max-width: 1000px) {
.light,
.dark {
padding: 16px;
}
.row,
.dark {
flex-direction: column;
}
ha-card {
margin: 0 auto;
width: 100%;
max-width: 400px;
}
pre {
margin: 16px auto;
}
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"demo-black-white-row": DemoBlackWhiteRow;
}
}

View File

@ -0,0 +1,91 @@
/* eslint-disable lit/no-template-arrow */
import { LitElement, TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators";
import { provideHass } from "../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../src/types";
import "../components/demo-black-white-row";
import { mockEntityRegistry } from "../../../demo/src/stubs/entity_registry";
import { mockDeviceRegistry } from "../../../demo/src/stubs/device_registry";
import { mockAreaRegistry } from "../../../demo/src/stubs/area_registry";
import { mockHassioSupervisor } from "../../../demo/src/stubs/hassio_supervisor";
import "../../../src/panels/config/automation/action/ha-automation-action";
import { HaChooseAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-choose";
import { HaDelayAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-delay";
import { HaDeviceAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-device_id";
import { HaEventAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-event";
import { HaRepeatAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-repeat";
import { HaSceneAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-scene";
import { HaServiceAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-service";
import { HaWaitForTriggerAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-wait_for_trigger";
import { HaWaitAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-wait_template";
import { Action } from "../../../src/data/script";
import { HaConditionAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-condition";
const SCHEMAS: { name: string; actions: Action[] }[] = [
{ name: "Event", actions: [HaEventAction.defaultConfig] },
{ name: "Device", actions: [HaDeviceAction.defaultConfig] },
{ name: "Service", actions: [HaServiceAction.defaultConfig] },
{ name: "Condition", actions: [HaConditionAction.defaultConfig] },
{ name: "Delay", actions: [HaDelayAction.defaultConfig] },
{ name: "Scene", actions: [HaSceneAction.defaultConfig] },
{ name: "Wait", actions: [HaWaitAction.defaultConfig] },
{ name: "WaitForTrigger", actions: [HaWaitForTriggerAction.defaultConfig] },
{ name: "Repeat", actions: [HaRepeatAction.defaultConfig] },
{ name: "Choose", actions: [HaChooseAction.defaultConfig] },
{ name: "Variables", actions: [{ variables: { hello: "1" } }] },
];
@customElement("demo-automation-editor-action")
class DemoHaAutomationEditorAction extends LitElement {
@state() private hass!: HomeAssistant;
private data: any = SCHEMAS.map((info) => info.actions);
constructor() {
super();
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en");
mockEntityRegistry(hass);
mockDeviceRegistry(hass);
mockAreaRegistry(hass);
mockHassioSupervisor(hass);
}
protected render(): TemplateResult {
const valueChanged = (ev) => {
const sampleIdx = ev.target.sampleIdx;
this.data[sampleIdx] = ev.detail.value;
this.requestUpdate();
};
return html`
${SCHEMAS.map(
(info, sampleIdx) => html`
<demo-black-white-row
.title=${info.name}
.value=${this.data[sampleIdx]}
>
${["light", "dark"].map(
(slot) =>
html`
<ha-automation-action
slot=${slot}
.hass=${this.hass}
.actions=${this.data[sampleIdx]}
.sampleIdx=${sampleIdx}
@value-changed=${valueChanged}
></ha-automation-action>
`
)}
</demo-black-white-row>
`
)}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-ha-automation-editor-action": DemoHaAutomationEditorAction;
}
}

View File

@ -0,0 +1,127 @@
/* eslint-disable lit/no-template-arrow */
import { LitElement, TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators";
import { provideHass } from "../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../src/types";
import "../components/demo-black-white-row";
import { mockEntityRegistry } from "../../../demo/src/stubs/entity_registry";
import { mockDeviceRegistry } from "../../../demo/src/stubs/device_registry";
import { mockAreaRegistry } from "../../../demo/src/stubs/area_registry";
import { mockHassioSupervisor } from "../../../demo/src/stubs/hassio_supervisor";
import type { Condition } from "../../../src/data/automation";
import "../../../src/panels/config/automation/condition/ha-automation-condition";
import { HaDeviceCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-device";
import { HaLogicalCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-logical";
import HaNumericStateCondition from "../../../src/panels/config/automation/condition/types/ha-automation-condition-numeric_state";
import { HaStateCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-state";
import { HaSunCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-sun";
import { HaTemplateCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-template";
import { HaTimeCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-time";
import { HaTriggerCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-trigger";
import { HaZoneCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-zone";
const SCHEMAS: { name: string; conditions: Condition[] }[] = [
{
name: "State",
conditions: [{ condition: "state", ...HaStateCondition.defaultConfig }],
},
{
name: "Numeric State",
conditions: [
{ condition: "numeric_state", ...HaNumericStateCondition.defaultConfig },
],
},
{
name: "Sun",
conditions: [{ condition: "sun", ...HaSunCondition.defaultConfig }],
},
{
name: "Zone",
conditions: [{ condition: "zone", ...HaZoneCondition.defaultConfig }],
},
{
name: "Time",
conditions: [{ condition: "time", ...HaTimeCondition.defaultConfig }],
},
{
name: "Template",
conditions: [
{ condition: "template", ...HaTemplateCondition.defaultConfig },
],
},
{
name: "Device",
conditions: [{ condition: "device", ...HaDeviceCondition.defaultConfig }],
},
{
name: "And",
conditions: [{ condition: "and", ...HaLogicalCondition.defaultConfig }],
},
{
name: "Or",
conditions: [{ condition: "or", ...HaLogicalCondition.defaultConfig }],
},
{
name: "Not",
conditions: [{ condition: "not", ...HaLogicalCondition.defaultConfig }],
},
{
name: "Trigger",
conditions: [{ condition: "trigger", ...HaTriggerCondition.defaultConfig }],
},
];
@customElement("demo-automation-editor-condition")
class DemoHaAutomationEditorCondition extends LitElement {
@state() private hass!: HomeAssistant;
private data: any = SCHEMAS.map((info) => info.conditions);
constructor() {
super();
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en");
mockEntityRegistry(hass);
mockDeviceRegistry(hass);
mockAreaRegistry(hass);
mockHassioSupervisor(hass);
}
protected render(): TemplateResult {
const valueChanged = (ev) => {
const sampleIdx = ev.target.sampleIdx;
this.data[sampleIdx] = ev.detail.value;
this.requestUpdate();
};
return html`
${SCHEMAS.map(
(info, sampleIdx) => html`
<demo-black-white-row
.title=${info.name}
.value=${this.data[sampleIdx]}
>
${["light", "dark"].map(
(slot) =>
html`
<ha-automation-condition
slot=${slot}
.hass=${this.hass}
.conditions=${this.data[sampleIdx]}
.sampleIdx=${sampleIdx}
@value-changed=${valueChanged}
></ha-automation-condition>
`
)}
</demo-black-white-row>
`
)}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-ha-automation-editor-condition": DemoHaAutomationEditorCondition;
}
}

View File

@ -0,0 +1,159 @@
/* eslint-disable lit/no-template-arrow */
import { LitElement, TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators";
import { provideHass } from "../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../src/types";
import "../components/demo-black-white-row";
import { mockEntityRegistry } from "../../../demo/src/stubs/entity_registry";
import { mockDeviceRegistry } from "../../../demo/src/stubs/device_registry";
import { mockAreaRegistry } from "../../../demo/src/stubs/area_registry";
import { mockHassioSupervisor } from "../../../demo/src/stubs/hassio_supervisor";
import type { Trigger } from "../../../src/data/automation";
import { HaGeolocationTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-geo_location";
import { HaEventTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-event";
import { HaHassTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-homeassistant";
import { HaNumericStateTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state";
import { HaSunTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-sun";
import { HaTagTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-tag";
import { HaTemplateTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-template";
import { HaTimeTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-time";
import { HaTimePatternTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-time_pattern";
import { HaWebhookTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-webhook";
import { HaZoneTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-zone";
import { HaDeviceTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-device";
import { HaStateTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-state";
import { HaMQTTTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt";
import "../../../src/panels/config/automation/trigger/ha-automation-trigger";
const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
{
name: "State",
triggers: [{ platform: "state", ...HaStateTrigger.defaultConfig }],
},
{
name: "MQTT",
triggers: [{ platform: "mqtt", ...HaMQTTTrigger.defaultConfig }],
},
{
name: "GeoLocation",
triggers: [
{ platform: "geo_location", ...HaGeolocationTrigger.defaultConfig },
],
},
{
name: "Home Assistant",
triggers: [{ platform: "homeassistant", ...HaHassTrigger.defaultConfig }],
},
{
name: "Numeric State",
triggers: [
{ platform: "numeric_state", ...HaNumericStateTrigger.defaultConfig },
],
},
{
name: "Sun",
triggers: [{ platform: "sun", ...HaSunTrigger.defaultConfig }],
},
{
name: "Time Pattern",
triggers: [
{ platform: "time_pattern", ...HaTimePatternTrigger.defaultConfig },
],
},
{
name: "Webhook",
triggers: [{ platform: "webhook", ...HaWebhookTrigger.defaultConfig }],
},
{
name: "Zone",
triggers: [{ platform: "zone", ...HaZoneTrigger.defaultConfig }],
},
{
name: "Tag",
triggers: [{ platform: "tag", ...HaTagTrigger.defaultConfig }],
},
{
name: "Time",
triggers: [{ platform: "time", ...HaTimeTrigger.defaultConfig }],
},
{
name: "Template",
triggers: [{ platform: "template", ...HaTemplateTrigger.defaultConfig }],
},
{
name: "Event",
triggers: [{ platform: "event", ...HaEventTrigger.defaultConfig }],
},
{
name: "Device Trigger",
triggers: [{ platform: "device", ...HaDeviceTrigger.defaultConfig }],
},
];
@customElement("demo-automation-editor-trigger")
class DemoHaAutomationEditorTrigger extends LitElement {
@state() private hass!: HomeAssistant;
private data: any = SCHEMAS.map((info) => info.triggers);
constructor() {
super();
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en");
mockEntityRegistry(hass);
mockDeviceRegistry(hass);
mockAreaRegistry(hass);
mockHassioSupervisor(hass);
}
protected render(): TemplateResult {
const valueChanged = (ev) => {
const sampleIdx = ev.target.sampleIdx;
this.data[sampleIdx] = ev.detail.value;
this.requestUpdate();
};
return html`
${SCHEMAS.map(
(info, sampleIdx) => html`
<demo-black-white-row
.title=${info.name}
.value=${this.data[sampleIdx]}
>
${["light", "dark"].map(
(slot) =>
html`
<ha-automation-trigger
slot=${slot}
.hass=${this.hass}
.triggers=${this.data[sampleIdx]}
.sampleIdx=${sampleIdx}
@value-changed=${valueChanged}
></ha-automation-trigger>
`
)}
</demo-black-white-row>
`
)}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-ha-automation-editor-trigger": DemoHaAutomationEditorTrigger;
}
}

View File

@ -0,0 +1,85 @@
import { html, css, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import "../../../src/components/ha-bar";
import "../../../src/components/ha-card";
const bars: {
min?: number;
max?: number;
value: number;
warning?: number;
error?: number;
}[] = [
{
value: 33,
},
{
value: 150,
},
{
min: -10,
value: 0,
},
{
value: 80,
},
{
value: 200,
max: 13,
},
{
value: 4,
min: 13,
},
];
@customElement("demo-ha-bar")
export class DemoHaBar extends LitElement {
protected render(): TemplateResult {
return html`
${bars
.map((bar) => ({ min: 0, max: 100, warning: 70, error: 90, ...bar }))
.map(
(bar) => html`
<ha-card>
<div class="card-content">
<pre>Config: ${JSON.stringify(bar)}</pre>
<ha-bar
class=${classMap({
warning: bar.value > bar.warning,
error: bar.value > bar.error,
})}
.min=${bar.min}
.max=${bar.max}
.value=${bar.value}
>
</ha-bar>
</div>
</ha-card>
`
)}
`;
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
.warning {
--ha-bar-primary-color: var(--warning-color);
}
.error {
--ha-bar-primary-color: var(--error-color);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-ha-bar": DemoHaBar;
}
}

View File

@ -0,0 +1,61 @@
import { mdiHomeAssistant } from "@mdi/js";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import "../../../src/components/ha-card";
import "../../../src/components/ha-chip";
import "../../../src/components/ha-svg-icon";
const chips: {
icon?: string;
content?: string;
}[] = [
{},
{
icon: mdiHomeAssistant,
},
{
content: "Content",
},
{
icon: mdiHomeAssistant,
content: "Content",
},
];
@customElement("demo-ha-chip")
export class DemoHaChip extends LitElement {
protected render(): TemplateResult {
return html`
<ha-card header="ha-chip demo">
<div class="card-content">
${chips.map(
(chip) => html`
<ha-chip .hasIcon=${chip.icon !== undefined}>
${chip.icon
? html`<ha-svg-icon slot="icon" .path=${chip.icon}>
</ha-svg-icon>`
: ""}
${chip.content}
</ha-chip>
`
)}
</div>
</ha-card>
`;
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-ha-chip": DemoHaChip;
}
}

View File

@ -1,23 +1,25 @@
/* eslint-disable lit/no-template-arrow */ /* eslint-disable lit/no-template-arrow */
import { LitElement, TemplateResult, css, html } from "lit"; import "@material/mwc-button";
import { LitElement, TemplateResult, html } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import { computeInitialHaFormData } from "../../../src/components/ha-form/compute-initial-ha-form-data";
import type { HaFormSchema } from "../../../src/components/ha-form/types";
import "../../../src/components/ha-form/ha-form"; import "../../../src/components/ha-form/ha-form";
import "../../../src/components/ha-card"; import "../components/demo-black-white-row";
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
import type { HaFormSchema } from "../../../src/components/ha-form/ha-form";
const SCHEMAS: { const SCHEMAS: {
title: string; title: string;
translations?: Record<string, string>; translations?: Record<string, string>;
error?: Record<string, string>; error?: Record<string, string>;
schema: HaFormSchema[]; schema: HaFormSchema[];
data?: Record<string, any>;
}[] = [ }[] = [
{ {
title: "Authentication", title: "Authentication",
translations: { translations: {
username: "Username", username: "Username",
password: "Password", password: "Password",
invalid_login: "Invalid login", invalid_login: "Invalid username or password",
}, },
error: { error: {
base: "invalid_login", base: "invalid_login",
@ -57,6 +59,11 @@ const SCHEMAS: {
optional: true, optional: true,
default: 10, default: 10,
}, },
{
type: "float",
name: "float",
required: true,
},
{ {
type: "string", type: "string",
name: "string", name: "string",
@ -83,6 +90,80 @@ const SCHEMAS: {
optional: true, optional: true,
default: ["default"], default: ["default"],
}, },
{
type: "positive_time_period_dict",
name: "time",
required: true,
},
],
},
{
title: "Numbers",
schema: [
{
type: "integer",
name: "int",
required: true,
},
{
type: "integer",
name: "int with default",
optional: true,
default: 10,
},
{
type: "integer",
name: "int range required",
required: true,
default: 5,
valueMin: 0,
valueMax: 10,
},
{
type: "integer",
name: "int range optional",
optional: true,
valueMin: 0,
valueMax: 10,
},
],
},
{
title: "select",
schema: [
{
type: "select",
options: [
["default", "Default"],
["other", "Other"],
],
name: "select",
required: true,
default: "default",
},
{
type: "select",
options: [
["default", "Default"],
["other", "Other"],
],
name: "select optional",
optional: true,
},
{
type: "select",
options: [
["default", "Default"],
["other", "Other"],
["uno", "mas"],
["one", "more"],
["and", "another_one"],
["option", "1000"],
],
name: "select many otions",
optional: true,
default: "default",
},
], ],
}, },
{ {
@ -95,7 +176,7 @@ const SCHEMAS: {
other: "Other", other: "Other",
}, },
name: "multi", name: "multi",
optional: true, required: true,
default: ["default"], default: ["default"],
}, },
{ {
@ -108,101 +189,90 @@ const SCHEMAS: {
and: "another_one", and: "another_one",
option: "1000", option: "1000",
}, },
name: "multi", name: "multi many otions",
optional: true, optional: true,
default: ["default"], default: ["default"],
}, },
], ],
}, },
{
title: "Field specific error",
data: {
new_password: "hello",
new_password_2: "bye",
},
translations: {
new_password: "New Password",
new_password_2: "Re-type Password",
not_match: "The passwords do not match",
},
error: {
new_password_2: "not_match",
},
schema: [
{
type: "string",
name: "new_password",
required: true,
},
{
type: "string",
name: "new_password_2",
required: true,
},
],
},
]; ];
@customElement("demo-ha-form") @customElement("demo-ha-form")
class DemoHaForm extends LitElement { class DemoHaForm extends LitElement {
private lightModeData: any = []; private data = SCHEMAS.map(
({ schema, data }) => data || computeInitialHaFormData(schema)
);
private darkModeData: any = []; private disabled = SCHEMAS.map(() => false);
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
${SCHEMAS.map((info, idx) => { ${SCHEMAS.map((info, idx) => {
const translations = info.translations || {}; const translations = info.translations || {};
const computeLabel = (schema) => return html`
translations[schema.name] || schema.name; <demo-black-white-row
const computeError = (error) => translations[error] || error; .title=${info.title}
.value=${this.data[idx]}
return [ .disabled=${this.disabled[idx]}
[this.lightModeData, "light"], @submitted=${() => {
[this.darkModeData, "dark"], this.disabled[idx] = true;
].map( this.requestUpdate();
([data, type]) => html` setTimeout(() => {
<div class="row" data-type=${type}> this.disabled[idx] = false;
<ha-card .header=${info.title}> this.requestUpdate();
<div class="card-content"> }, 2000);
<ha-form }}
.data=${data[idx]} >
.schema=${info.schema} ${["light", "dark"].map(
.error=${info.error} (slot) => html`
.computeError=${computeError} <ha-form
.computeLabel=${computeLabel} slot=${slot}
@value-changed=${(e) => { .data=${this.data[idx]}
data[idx] = e.detail.value; .schema=${info.schema}
this.requestUpdate(); .error=${info.error}
}} .disabled=${this.disabled[idx]}
></ha-form> .computeError=${(error) => translations[error] || error}
</div> .computeLabel=${(schema) =>
</ha-card> translations[schema.name] || schema.name}
<pre>${JSON.stringify(data[idx], undefined, 2)}</pre> @value-changed=${(e) => {
</div> this.data[idx] = e.detail.value;
` this.requestUpdate();
); }}
></ha-form>
`
)}
</demo-black-white-row>
`;
})} })}
`; `;
} }
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this.shadowRoot!.querySelectorAll("[data-type=dark]").forEach((el) => {
applyThemesOnElement(
el,
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: false,
},
"default",
{ dark: true }
);
});
}
static styles = css`
.row {
margin: 0 auto;
max-width: 800px;
display: flex;
padding: 50px;
background-color: var(--primary-background-color);
}
ha-card {
width: 100%;
max-width: 384px;
}
pre {
width: 400px;
margin: 0 16px;
overflow: auto;
color: var(--primary-text-color);
}
@media only screen and (max-width: 800px) {
.row {
flex-direction: column;
}
pre {
margin: 16px 0;
}
}
`;
} }
declare global { declare global {

View File

@ -0,0 +1,122 @@
import { html, css, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import "../../../src/components/ha-label-badge";
import "../../../src/components/ha-card";
const colors = ["#03a9f4", "#ffa600", "#43a047"];
const badges: {
label?: string;
description?: string;
image?: string;
}[] = [
{
label: "label",
},
{
label: "label",
description: "Description",
},
{
description: "Description",
},
{
label: "label",
description: "Description",
image: "/images/living_room.png",
},
{
description: "Description",
image: "/images/living_room.png",
},
{
label: "label",
image: "/images/living_room.png",
},
{
image: "/images/living_room.png",
},
{
label: "big label",
},
{
label: "big label",
description: "Description",
},
{
label: "big label",
description: "Description",
image: "/images/living_room.png",
},
];
@customElement("demo-ha-label-badge")
export class DemoHaLabelBadge extends LitElement {
protected render(): TemplateResult {
return html`
<ha-card>
<div class="card-content">
${badges.map(
(badge) => html`
<ha-label-badge
style="--ha-label-badge-color: ${colors[
Math.floor(Math.random() * colors.length)
]}"
.label=${badge.label}
.description=${badge.description}
.image=${badge.image}
>
</ha-label-badge>
`
)}
</div>
</ha-card>
<ha-card>
<div class="card-content">
${badges.map(
(badge) => html`
<div class="badge">
<ha-label-badge
style="--ha-label-badge-color: ${colors[
Math.floor(Math.random() * colors.length)
]}"
.label=${badge.label}
.description=${badge.description}
.image=${badge.image}
>
</ha-label-badge>
<pre>${JSON.stringify(badge, null, 2)}</pre>
</div>
`
)}
</div>
</ha-card>
`;
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
pre {
margin-left: 16px;
background-color: var(--markdown-code-background-color);
padding: 8px;
}
.badge {
display: flex;
flex-direction: row;
margin-bottom: 16px;
align-items: center;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-ha-label-badge": DemoHaLabelBadge;
}
}

View File

@ -0,0 +1,131 @@
/* eslint-disable lit/no-template-arrow */
import "@material/mwc-button";
import { LitElement, TemplateResult, css, html } from "lit";
import { customElement, state } from "lit/decorators";
import "../../../src/components/ha-selector/ha-selector";
import "../../../src/components/ha-settings-row";
import { provideHass } from "../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../src/types";
import "../components/demo-black-white-row";
import { BlueprintInput } from "../../../src/data/blueprint";
import { mockEntityRegistry } from "../../../demo/src/stubs/entity_registry";
import { mockDeviceRegistry } from "../../../demo/src/stubs/device_registry";
import { mockAreaRegistry } from "../../../demo/src/stubs/area_registry";
import { mockHassioSupervisor } from "../../../demo/src/stubs/hassio_supervisor";
const SCHEMAS: {
name: string;
input: Record<string, BlueprintInput | null>;
}[] = [
{
name: "One of each",
input: {
entity: { name: "Entity", selector: { entity: {} } },
device: { name: "Device", selector: { device: {} } },
addon: { name: "Addon", selector: { addon: {} } },
area: { name: "Area", selector: { area: {} } },
target: { name: "Target", selector: { target: {} } },
number_box: {
name: "Number Box",
selector: {
number: {
min: 0,
max: 10,
mode: "box",
},
},
},
number_slider: {
name: "Number Slider",
selector: {
number: {
min: 0,
max: 10,
mode: "slider",
},
},
},
boolean: { name: "Boolean", selector: { boolean: {} } },
time: { name: "Time", selector: { time: {} } },
action: { name: "Action", selector: { action: {} } },
text: { name: "Text", selector: { text: { multiline: false } } },
text_multiline: {
name: "Text multiline",
selector: { text: { multiline: true } },
},
object: { name: "Object", selector: { object: {} } },
select: {
name: "Select",
selector: { select: { options: ["Option 1", "Option 2"] } },
},
},
},
];
@customElement("demo-ha-selector")
class DemoHaSelector extends LitElement {
@state() private hass!: HomeAssistant;
private data = SCHEMAS.map(() => ({}));
constructor() {
super();
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en");
mockEntityRegistry(hass);
mockDeviceRegistry(hass);
mockAreaRegistry(hass);
mockHassioSupervisor(hass);
}
protected render(): TemplateResult {
return html`
${SCHEMAS.map((info, idx) => {
const data = this.data[idx];
const valueChanged = (ev) => {
this.data[idx] = {
...data,
[ev.target.key]: ev.detail.value,
};
this.requestUpdate();
};
return html`
<demo-black-white-row .title=${info.name} .value=${this.data[idx]}>
${["light", "dark"].map((slot) =>
Object.entries(info.input).map(
([key, value]) =>
html`
<ha-settings-row narrow slot=${slot}>
<span slot="heading">${value?.name || key}</span>
<span slot="description">${value?.description}</span>
<ha-selector
.hass=${this.hass}
.selector=${value!.selector}
.key=${key}
.value=${data[key] ?? value!.default}
@value-changed=${valueChanged}
></ha-selector>
</ha-settings-row>
`
)
)}
</demo-black-white-row>
`;
})}
`;
}
static styles = css`
paper-input,
ha-selector {
width: 60;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"demo-ha-selector": DemoHaSelector;
}
}

View File

@ -187,6 +187,7 @@ const createEntityRegistryEntries = (
device_id: "mock-device-id", device_id: "mock-device-id",
area_id: null, area_id: null,
disabled_by: null, disabled_by: null,
entity_category: null,
entity_id: "binary_sensor.updater", entity_id: "binary_sensor.updater",
name: null, name: null,
icon: null, icon: null,
@ -211,6 +212,7 @@ const createDeviceRegistryEntries = (
area_id: null, area_id: null,
name_by_user: null, name_by_user: null,
disabled_by: null, disabled_by: null,
configuration_url: null,
}, },
]; ];

View File

@ -65,10 +65,11 @@ class HaGallery extends PolymerElement {
<app-header slot="header" fixed> <app-header slot="header" fixed>
<app-toolbar> <app-toolbar>
<ha-icon-button <ha-icon-button
icon="hass:arrow-left"
on-click="_backTapped" on-click="_backTapped"
class$="[[_computeHeaderButtonClass(_demo)]]" class$="[[_computeHeaderButtonClass(_demo)]]"
></ha-icon-button> >
<ha-icon icon="hass:arrow-left"></ha-icon>
</ha-icon-button>
<div main-title> <div main-title>
[[_withDefault(_demo, "Home Assistant Gallery")]] [[_withDefault(_demo, "Home Assistant Gallery")]]
</div> </div>

View File

@ -1,4 +1,3 @@
import "@material/mwc-icon-button/mwc-icon-button";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js"; import { mdiDotsVertical } from "@mdi/js";
@ -18,7 +17,7 @@ import { navigate } from "../../../src/common/navigate";
import "../../../src/common/search/search-input"; import "../../../src/common/search/search-input";
import { extractSearchParam } from "../../../src/common/url/search-params"; import { extractSearchParam } from "../../../src/common/url/search-params";
import "../../../src/components/ha-button-menu"; import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-svg-icon"; import "../../../src/components/ha-icon-button";
import { import {
HassioAddonInfo, HassioAddonInfo,
HassioAddonRepository, HassioAddonRepository,
@ -92,9 +91,11 @@ class HassioAddonStore extends LitElement {
slot="toolbar-icon" slot="toolbar-icon"
@action=${this._handleAction} @action=${this._handleAction}
> >
<mwc-icon-button slot="trigger" alt="menu"> <ha-icon-button
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon> .label=${this.supervisor.localize("common.menu")}
</mwc-icon-button> .path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>
<mwc-list-item> <mwc-list-item>
${this.supervisor.localize("store.repositories")} ${this.supervisor.localize("store.repositories")}
</mwc-list-item> </mwc-list-item>
@ -113,6 +114,7 @@ class HassioAddonStore extends LitElement {
: html` : html`
<div class="search"> <div class="search">
<search-input <search-input
.hass=${this.hass}
no-label-float no-label-float
no-underline no-underline
.filter=${this._filter} .filter=${this._filter}

View File

@ -15,12 +15,13 @@ import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/buttons/ha-progress-button"; import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-button-menu"; import "../../../../src/components/ha-button-menu";
import "../../../../src/components/ha-card"; import "../../../../src/components/ha-card";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-form/ha-form"; import "../../../../src/components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../src/components/ha-form/ha-form"; import type { HaFormSchema } from "../../../../src/components/ha-form/types";
import "../../../../src/components/ha-formfield"; import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-switch"; import "../../../../src/components/ha-switch";
import "../../../../src/components/ha-yaml-editor"; import "../../../../src/components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../src/components/ha-yaml-editor"; import type { HaYamlEditor } from "../../../../src/components/ha-yaml-editor";
@ -77,6 +78,18 @@ class HassioAddonConfig extends LitElement {
this.addon.translations.en?.configuration?.[entry.name].name || this.addon.translations.en?.configuration?.[entry.name].name ||
entry.name; entry.name;
private _schema = memoizeOne((schema: HaFormSchema[]): HaFormSchema[] =>
// @ts-expect-error supervisor does not implement [string, string] for select.options[]
schema.map((entry) =>
entry.type === "select"
? {
...entry,
options: entry.options.map((option) => [option, option]),
}
: entry
)
);
private _filteredShchema = memoizeOne( private _filteredShchema = memoizeOne(
(options: Record<string, unknown>, schema: HaFormSchema[]) => (options: Record<string, unknown>, schema: HaFormSchema[]) =>
schema.filter((entry) => entry.name in options || entry.required) schema.filter((entry) => entry.name in options || entry.required)
@ -100,9 +113,11 @@ class HassioAddonConfig extends LitElement {
</h2> </h2>
<div class="card-menu"> <div class="card-menu">
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}> <ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
<mwc-icon-button slot="trigger"> <ha-icon-button
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon> .label=${this.hass.localize("common.menu")}
</mwc-icon-button> .path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>
<mwc-list-item .disabled=${!this._canShowSchema}> <mwc-list-item .disabled=${!this._canShowSchema}>
${this._yamlMode ${this._yamlMode
? this.supervisor.localize( ? this.supervisor.localize(
@ -125,12 +140,14 @@ class HassioAddonConfig extends LitElement {
.data=${this._options!} .data=${this._options!}
@value-changed=${this._configChanged} @value-changed=${this._configChanged}
.computeLabel=${this.computeLabel} .computeLabel=${this.computeLabel}
.schema=${this._showOptional .schema=${this._schema(
? this.addon.schema! this._showOptional
: this._filteredShchema( ? this.addon.schema!
this.addon.options, : this._filteredShchema(
this.addon.schema! this.addon.options,
)} this.addon.schema!
)
)}
></ha-form>` ></ha-form>`
: html` <ha-yaml-editor : html` <ha-yaml-editor
@value-changed=${this._configChanged} @value-changed=${this._configChanged}

View File

@ -11,6 +11,12 @@ import {
mdiHomeAssistant, mdiHomeAssistant,
mdiKey, mdiKey,
mdiNetwork, mdiNetwork,
mdiNumeric1,
mdiNumeric2,
mdiNumeric3,
mdiNumeric4,
mdiNumeric5,
mdiNumeric6,
mdiPound, mdiPound,
mdiShield, mdiShield,
} from "@mdi/js"; } from "@mdi/js";
@ -25,7 +31,7 @@ import "../../../../src/components/buttons/ha-call-api-button";
import "../../../../src/components/buttons/ha-progress-button"; import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert"; import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-card"; import "../../../../src/components/ha-card";
import "../../../../src/components/ha-label-badge"; import "../../../../src/components/ha-chip";
import "../../../../src/components/ha-markdown"; import "../../../../src/components/ha-markdown";
import "../../../../src/components/ha-settings-row"; import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon"; import "../../../../src/components/ha-svg-icon";
@ -73,6 +79,15 @@ const STAGE_ICON = {
deprecated: mdiExclamationThick, deprecated: mdiExclamationThick,
}; };
const RATING_ICON = {
1: mdiNumeric1,
2: mdiNumeric2,
3: mdiNumeric3,
4: mdiNumeric4,
5: mdiNumeric5,
6: mdiNumeric6,
};
@customElement("hassio-addon-info") @customElement("hassio-addon-info")
class HassioAddonInfo extends LitElement { class HassioAddonInfo extends LitElement {
@property({ type: Boolean }) public narrow!: boolean; @property({ type: Boolean }) public narrow!: boolean;
@ -180,24 +195,21 @@ class HassioAddonInfo extends LitElement {
: ""} : ""}
${!this.addon.protected ${!this.addon.protected
? html` ? html`
<ha-card class="warning"> <ha-alert
<h1 class="card-header">${this.supervisor.localize( alert-type="error"
"addon.dashboard.protection_mode.title" .title=${this.supervisor.localize(
)} "addon.dashboard.protection_mode.title"
</h1> )}
<div class="card-content"> .actionText=${this.supervisor.localize(
${this.supervisor.localize("addon.dashboard.protection_mode.content")}
</div>
<div class="card-actions protection-enable">
<mwc-button @click=${this._protectionToggled}>
${this.supervisor.localize(
"addon.dashboard.protection_mode.enable" "addon.dashboard.protection_mode.enable"
)} )}
</mwc-button> @alert-action-clicked=${this._protectionToggled}
</div> >
</div> ${this.supervisor.localize(
</ha-card> "addon.dashboard.protection_mode.content"
` )}
</ha-alert>
`
: ""} : ""}
<ha-card> <ha-card>
@ -249,6 +261,163 @@ class HassioAddonInfo extends LitElement {
>`} >`}
</div> </div>
<div class="capabilities">
${this.addon.stage !== "stable"
? html` <ha-chip
hasIcon
class=${classMap({
yellow: this.addon.stage === "experimental",
red: this.addon.stage === "deprecated",
})}
@click=${this._showMoreInfo}
id="stage"
>
<ha-svg-icon
slot="icon"
.path=${STAGE_ICON[this.addon.stage]}
>
</ha-svg-icon>
${this.supervisor.localize(
`addon.dashboard.capability.stages.${this.addon.stage}`
)}
</ha-chip>`
: ""}
<ha-chip
hasIcon
class=${classMap({
green: [5, 6].includes(Number(this.addon.rating)),
yellow: [3, 4].includes(Number(this.addon.rating)),
red: [1, 2].includes(Number(this.addon.rating)),
})}
@click=${this._showMoreInfo}
id="rating"
>
<ha-svg-icon slot="icon" .path=${RATING_ICON[this.addon.rating]}>
</ha-svg-icon>
${this.supervisor.localize(
"addon.dashboard.capability.label.rating"
)}
</ha-chip>
${this.addon.host_network
? html`
<ha-chip
hasIcon
@click=${this._showMoreInfo}
id="host_network"
>
<ha-svg-icon slot="icon" .path=${mdiNetwork}> </ha-svg-icon>
${this.supervisor.localize(
"addon.dashboard.capability.label.host"
)}
</ha-chip>
`
: ""}
${this.addon.full_access
? html`
<ha-chip
hasIcon
@click=${this._showMoreInfo}
id="full_access"
>
<ha-svg-icon slot="icon" .path=${mdiChip}></ha-svg-icon>
${this.supervisor.localize(
"addon.dashboard.capability.label.hardware"
)}
</ha-chip>
`
: ""}
${this.addon.homeassistant_api
? html`
<ha-chip
hasIcon
@click=${this._showMoreInfo}
id="homeassistant_api"
>
<ha-svg-icon
slot="icon"
.path=${mdiHomeAssistant}
></ha-svg-icon>
${this.supervisor.localize(
"addon.dashboard.capability.label.core"
)}
</ha-chip>
`
: ""}
${this._computeHassioApi
? html`
<ha-chip hasIcon @click=${this._showMoreInfo} id="hassio_api">
<ha-svg-icon
slot="icon"
.path=${mdiHomeAssistant}
></ha-svg-icon>
${this.supervisor.localize(
`addon.dashboard.capability.role.${this.addon.hassio_role}`
) || this.addon.hassio_role}
</ha-chip>
`
: ""}
${this.addon.docker_api
? html`
<ha-chip hasIcon @click=${this._showMoreInfo} id="docker_api">
<ha-svg-icon slot="icon" .path=${mdiDocker}></ha-svg-icon>
${this.supervisor.localize(
"addon.dashboard.capability.label.docker"
)}
</ha-chip>
`
: ""}
${this.addon.host_pid
? html`
<ha-chip hasIcon @click=${this._showMoreInfo} id="host_pid">
<ha-svg-icon slot="icon" .path=${mdiPound}></ha-svg-icon>
${this.supervisor.localize(
"addon.dashboard.capability.label.host_pid"
)}
</ha-chip>
`
: ""}
${this.addon.apparmor !== "default"
? html`
<ha-chip
hasIcon
@click=${this._showMoreInfo}
class=${this._computeApparmorClassName}
id="apparmor"
>
<ha-svg-icon slot="icon" .path=${mdiShield}></ha-svg-icon>
${this.supervisor.localize(
"addon.dashboard.capability.label.apparmor"
)}
</ha-chip>
`
: ""}
${this.addon.auth_api
? html`
<ha-chip hasIcon @click=${this._showMoreInfo} id="auth_api">
<ha-svg-icon slot="icon" .path=${mdiKey}></ha-svg-icon>
${this.supervisor.localize(
"addon.dashboard.capability.label.auth"
)}
</ha-chip>
`
: ""}
${this.addon.ingress
? html`
<ha-chip hasIcon @click=${this._showMoreInfo} id="ingress">
<ha-svg-icon
slot="icon"
.path=${mdiCursorDefaultClickOutline}
></ha-svg-icon>
${this.supervisor.localize(
"addon.dashboard.capability.label.ingress"
)}
</ha-chip>
`
: ""}
</div>
<div class="description light-color"> <div class="description light-color">
${this.addon.description}.<br /> ${this.addon.description}.<br />
${this.supervisor.localize( ${this.supervisor.localize(
@ -269,172 +438,6 @@ class HassioAddonInfo extends LitElement {
/> />
` `
: ""} : ""}
<div class="security">
${this.addon.stage !== "stable"
? html` <ha-label-badge
class=${classMap({
yellow: this.addon.stage === "experimental",
red: this.addon.stage === "deprecated",
})}
@click=${this._showMoreInfo}
id="stage"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.stage"
)}
description=""
>
<ha-svg-icon
.path=${STAGE_ICON[this.addon.stage]}
></ha-svg-icon>
</ha-label-badge>`
: ""}
<ha-label-badge
class=${classMap({
green: [5, 6].includes(Number(this.addon.rating)),
yellow: [3, 4].includes(Number(this.addon.rating)),
red: [1, 2].includes(Number(this.addon.rating)),
})}
@click=${this._showMoreInfo}
id="rating"
label="rating"
description=""
>
${this.addon.rating}
</ha-label-badge>
${this.addon.host_network
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="host_network"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.host"
)}
description=""
>
<ha-svg-icon .path=${mdiNetwork}></ha-svg-icon>
</ha-label-badge>
`
: ""}
${this.addon.full_access
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="full_access"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.hardware"
)}
description=""
>
<ha-svg-icon .path=${mdiChip}></ha-svg-icon>
</ha-label-badge>
`
: ""}
${this.addon.homeassistant_api
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="homeassistant_api"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.hass"
)}
description=""
>
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
</ha-label-badge>
`
: ""}
${this._computeHassioApi
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="hassio_api"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.hassio"
)}
.description=${this.supervisor.localize(
`addon.dashboard.capability.role.${this.addon.hassio_role}`
) || this.addon.hassio_role}
>
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
</ha-label-badge>
`
: ""}
${this.addon.docker_api
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="docker_api"
.label=".${this.supervisor.localize(
"addon.dashboard.capability.label.docker"
)}"
description=""
>
<ha-svg-icon .path=${mdiDocker}></ha-svg-icon>
</ha-label-badge>
`
: ""}
${this.addon.host_pid
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="host_pid"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.host_pid"
)}
description=""
>
<ha-svg-icon .path=${mdiPound}></ha-svg-icon>
</ha-label-badge>
`
: ""}
${this.addon.apparmor
? html`
<ha-label-badge
@click=${this._showMoreInfo}
class=${this._computeApparmorClassName}
id="apparmor"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.apparmor"
)}
description=""
>
<ha-svg-icon .path=${mdiShield}></ha-svg-icon>
</ha-label-badge>
`
: ""}
${this.addon.auth_api
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="auth_api"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.auth"
)}
description=""
>
<ha-svg-icon .path=${mdiKey}></ha-svg-icon>
</ha-label-badge>
`
: ""}
${this.addon.ingress
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="ingress"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.ingress"
)}
description=""
>
<ha-svg-icon
.path=${mdiCursorDefaultClickOutline}
></ha-svg-icon>
</ha-label-badge>
`
: ""}
</div>
${this.addon.version ${this.addon.version
? html` ? html`
<div <div
@ -1178,34 +1181,31 @@ class HassioAddonInfo extends LitElement {
.description a { .description a {
color: var(--primary-color); color: var(--primary-color);
} }
ha-chip {
text-transform: capitalize;
--ha-chip-text-color: var(--text-primary-color);
--ha-chip-background-color: var(--primary-color);
}
.red { .red {
--ha-label-badge-color: var(--label-badge-red, #df4c1e); --ha-chip-background-color: var(--label-badge-red, #df4c1e);
} }
.blue { .blue {
--ha-label-badge-color: var(--label-badge-blue, #039be5); --ha-chip-background-color: var(--label-badge-blue, #039be5);
} }
.green { .green {
--ha-label-badge-color: var(--label-badge-green, #0da035); --ha-chip-background-color: var(--label-badge-green, #0da035);
} }
.yellow { .yellow {
--ha-label-badge-color: var(--label-badge-yellow, #f4b400); --ha-chip-background-color: var(--label-badge-yellow, #f4b400);
} }
.security { .capabilities {
margin-bottom: 16px; margin-bottom: 16px;
} }
.card-actions { .card-actions {
justify-content: space-between; justify-content: space-between;
display: flex; display: flex;
} }
.security h3 {
margin-bottom: 8px;
font-weight: normal;
}
.security ha-label-badge {
cursor: pointer;
margin-right: 4px;
--ha-label-badge-padding: 8px 0 0 0;
}
.changelog { .changelog {
display: contents; display: contents;
} }
@ -1245,6 +1245,9 @@ class HassioAddonInfo extends LitElement {
} }
@media (max-width: 720px) { @media (max-width: 720px) {
ha-chip {
line-height: 36px;
}
.addon-options { .addon-options {
max-width: 100%; max-width: 100%;
} }

View File

@ -23,7 +23,8 @@ import {
} from "../../../src/components/data-table/ha-data-table"; } from "../../../src/components/data-table/ha-data-table";
import "../../../src/components/ha-button-menu"; import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-fab"; import "../../../src/components/ha-fab";
import { extractApiErrorMessage } from "../../../src/data/hassio/common"; import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-svg-icon";
import { import {
fetchHassioBackups, fetchHassioBackups,
friendlyFolderName, friendlyFolderName,
@ -31,6 +32,7 @@ import {
reloadHassioBackups, reloadHassioBackups,
removeBackup, removeBackup,
} from "../../../src/data/hassio/backup"; } from "../../../src/data/hassio/backup";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { import {
showAlertDialog, showAlertDialog,
@ -40,9 +42,9 @@ import "../../../src/layouts/hass-tabs-subpage-data-table";
import type { HaTabsSubpageDataTable } from "../../../src/layouts/hass-tabs-subpage-data-table"; import type { HaTabsSubpageDataTable } from "../../../src/layouts/hass-tabs-subpage-data-table";
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 { showHassioCreateBackupDialog } from "../dialogs/backup/show-dialog-hassio-create-backup";
import { showHassioBackupDialog } from "../dialogs/backup/show-dialog-hassio-backup";
import { showBackupUploadDialog } from "../dialogs/backup/show-dialog-backup-upload"; import { showBackupUploadDialog } from "../dialogs/backup/show-dialog-backup-upload";
import { showHassioBackupDialog } from "../dialogs/backup/show-dialog-hassio-backup";
import { showHassioCreateBackupDialog } from "../dialogs/backup/show-dialog-hassio-create-backup";
import { supervisorTabs } from "../hassio-tabs"; import { supervisorTabs } from "../hassio-tabs";
import { hassioStyle } from "../resources/hassio-style"; import { hassioStyle } from "../resources/hassio-style";
@ -179,9 +181,11 @@ export class HassioBackups extends LitElement {
slot="toolbar-icon" slot="toolbar-icon"
@action=${this._handleAction} @action=${this._handleAction}
> >
<mwc-icon-button slot="trigger" alt="menu"> <ha-icon-button
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon> .label=${this.hass.localize("common.menu")}
</mwc-icon-button> .path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>
<mwc-list-item> <mwc-list-item>
${this.supervisor?.localize("common.reload")} ${this.supervisor?.localize("common.reload")}
</mwc-list-item> </mwc-list-item>
@ -216,13 +220,15 @@ export class HassioBackups extends LitElement {
</mwc-button> </mwc-button>
` `
: html` : html`
<mwc-icon-button <ha-icon-button
.label=${this.supervisor.localize(
"snapshot.delete_selected"
)}
.path=${mdiDelete}
id="delete-btn" id="delete-btn"
class="warning" class="warning"
@click=${this._deleteSelected} @click=${this._deleteSelected}
> ></ha-icon-button>
<ha-svg-icon .path=${mdiDelete}></ha-svg-icon>
</mwc-icon-button>
<paper-tooltip animation-delay="0" for="delete-btn"> <paper-tooltip animation-delay="0" for="delete-btn">
${this.supervisor.localize("backup.delete_selected")} ${this.supervisor.localize("backup.delete_selected")}
</paper-tooltip> </paper-tooltip>
@ -368,7 +374,7 @@ export class HassioBackups extends LitElement {
margin-right: -12px; margin-right: -12px;
} }
.header-btns > mwc-button, .header-btns > mwc-button,
.header-btns > mwc-icon-button { .header-btns > ha-icon-button {
margin: 8px; margin: 8px;
} }
`, `,

View File

@ -1,4 +1,3 @@
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiFolderUpload } from "@mdi/js"; import { mdiFolderUpload } from "@mdi/js";
import "@polymer/paper-input/paper-input-container"; import "@polymer/paper-input/paper-input-container";
import { html, LitElement, TemplateResult } from "lit"; import { html, LitElement, TemplateResult } from "lit";
@ -6,9 +5,8 @@ import { customElement, state } from "lit/decorators";
import { fireEvent } from "../../../src/common/dom/fire_event"; import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/ha-circular-progress"; import "../../../src/components/ha-circular-progress";
import "../../../src/components/ha-file-upload"; import "../../../src/components/ha-file-upload";
import "../../../src/components/ha-svg-icon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { HassioBackup, uploadBackup } from "../../../src/data/hassio/backup"; import { HassioBackup, uploadBackup } from "../../../src/data/hassio/backup";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box"; import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import { HomeAssistant } from "../../../src/types"; import { HomeAssistant } from "../../../src/types";
@ -31,6 +29,7 @@ export class HassioUploadBackup extends LitElement {
public render(): TemplateResult { public render(): TemplateResult {
return html` return html`
<ha-file-upload <ha-file-upload
.hass=${this.hass}
.uploading=${this._uploading} .uploading=${this._uploading}
.icon=${mdiFolderUpload} .icon=${mdiFolderUpload}
accept="application/x-tar" accept="application/x-tar"

View File

@ -3,6 +3,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-header-bar"; import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-icon-button";
import { HassDialog } from "../../../../src/dialogs/make-dialog-manager"; import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../src/resources/styles"; import { haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types"; import type { HomeAssistant } from "../../../../src/types";
@ -52,9 +53,12 @@ export class DialogHassioBackupUpload
<div slot="heading"> <div slot="heading">
<ha-header-bar> <ha-header-bar>
<span slot="title"> Upload backup </span> <span slot="title"> Upload backup </span>
<mwc-icon-button slot="actionItems" dialogAction="cancel"> <ha-icon-button
<ha-svg-icon .path=${mdiClose}></ha-svg-icon> .label=${this.hass.localize("common.close")}
</mwc-icon-button> .path=${mdiClose}
slot="actionItems"
dialogAction="cancel"
></ha-icon-button>
</ha-header-bar> </ha-header-bar>
</div> </div>
<hassio-upload-backup <hassio-upload-backup

View File

@ -9,7 +9,7 @@ import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert"; import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-button-menu"; import "../../../../src/components/ha-button-menu";
import "../../../../src/components/ha-header-bar"; import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-svg-icon"; import "../../../../src/components/ha-icon-button";
import { getSignedPath } from "../../../../src/data/auth"; import { getSignedPath } from "../../../../src/data/auth";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { import {
@ -76,9 +76,12 @@ class HassioBackupDialog
<div slot="heading"> <div slot="heading">
<ha-header-bar> <ha-header-bar>
<span slot="title">${this._backup.name}</span> <span slot="title">${this._backup.name}</span>
<mwc-icon-button slot="actionItems" dialogAction="cancel"> <ha-icon-button
<ha-svg-icon .path=${mdiClose}></ha-svg-icon> .label=${this.hass.localize("common.close")}
</mwc-icon-button> .path=${mdiClose}
slot="actionItems"
dialogAction="cancel"
></ha-icon-button>
</ha-header-bar> </ha-header-bar>
</div> </div>
${this._restoringBackup ${this._restoringBackup
@ -110,9 +113,11 @@ class HassioBackupDialog
@action=${this._handleMenuAction} @action=${this._handleMenuAction}
@closed=${stopPropagation} @closed=${stopPropagation}
> >
<mwc-icon-button slot="trigger" alt="menu"> <ha-icon-button
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon> .label=${this.hass.localize("common.menu")}
</mwc-icon-button> .path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>
<mwc-list-item>Download Backup</mwc-list-item> <mwc-list-item>Download Backup</mwc-list-item>
<mwc-list-item class="error">Delete Backup</mwc-list-item> <mwc-list-item class="error">Delete Backup</mwc-list-item>
</ha-button-menu>` </ha-button-menu>`
@ -126,9 +131,6 @@ class HassioBackupDialog
haStyle, haStyle,
haStyleDialog, haStyleDialog,
css` css`
ha-svg-icon {
color: var(--primary-text-color);
}
ha-circular-progress { ha-circular-progress {
display: block; display: block;
text-align: center; text-align: center;
@ -139,6 +141,9 @@ class HassioBackupDialog
flex-shrink: 0; flex-shrink: 0;
display: block; display: block;
} }
ha-icon-button {
color: var(--secondary-text-color);
}
`, `,
]; ];
} }

View File

@ -7,6 +7,7 @@ import "../../../../src/common/search/search-input";
import { stringCompare } from "../../../../src/common/string/compare"; import { stringCompare } from "../../../../src/common/string/compare";
import "../../../../src/components/ha-dialog"; import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel"; import "../../../../src/components/ha-expansion-panel";
import "../../../../src/components/ha-icon-button";
import { HassioHardwareInfo } from "../../../../src/data/hassio/hardware"; import { HassioHardwareInfo } from "../../../../src/data/hassio/hardware";
import { dump } from "../../../../src/resources/js-yaml-dump"; import { dump } from "../../../../src/resources/js-yaml-dump";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
@ -70,10 +71,13 @@ class HassioHardwareDialog extends LitElement {
<h2> <h2>
${this._dialogParams.supervisor.localize("dialog.hardware.title")} ${this._dialogParams.supervisor.localize("dialog.hardware.title")}
</h2> </h2>
<mwc-icon-button dialogAction="close"> <ha-icon-button
<ha-svg-icon .path=${mdiClose}></ha-svg-icon> .label=${this.hass.localize("common.close")}
</mwc-icon-button> .path=${mdiClose}
dialogAction="close"
></ha-icon-button>
<search-input <search-input
.hass=${this.hass}
autofocus autofocus
no-label-float no-label-float
.filter=${this._filter} .filter=${this._filter}
@ -141,7 +145,7 @@ class HassioHardwareDialog extends LitElement {
haStyle, haStyle,
haStyleDialog, haStyleDialog,
css` css`
mwc-icon-button { ha-icon-button {
position: absolute; position: absolute;
right: 16px; right: 16px;
top: 10px; top: 10px;

View File

@ -47,11 +47,6 @@ class HassioMarkdownDialog extends LitElement {
haStyleDialog, haStyleDialog,
hassioStyle, hassioStyle,
css` css`
ha-paper-dialog {
min-width: 350px;
font-size: 14px;
border-radius: 2px;
}
app-toolbar { app-toolbar {
margin: 0; margin: 0;
padding: 0 16px; padding: 0 16px;
@ -62,19 +57,6 @@ class HassioMarkdownDialog extends LitElement {
margin-left: 16px; margin-left: 16px;
} }
@media all and (max-width: 450px), all and (max-height: 500px) { @media all and (max-width: 450px), all and (max-height: 500px) {
ha-paper-dialog {
max-height: 100%;
}
ha-paper-dialog::before {
content: "";
position: fixed;
z-index: -1;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
background-color: inherit;
}
app-toolbar { app-toolbar {
color: var(--text-primary-color); color: var(--text-primary-color);
background-color: var(--primary-color); background-color: var(--primary-color);

View File

@ -1,5 +1,4 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@material/mwc-icon-button";
import "@material/mwc-list/mwc-list"; import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import "@material/mwc-tab"; import "@material/mwc-tab";
@ -16,9 +15,9 @@ import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel"; import "../../../../src/components/ha-expansion-panel";
import "../../../../src/components/ha-formfield"; import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-header-bar"; import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-radio"; import "../../../../src/components/ha-radio";
import "../../../../src/components/ha-related-items"; import "../../../../src/components/ha-related-items";
import "../../../../src/components/ha-svg-icon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { import {
AccessPoints, AccessPoints,
@ -104,9 +103,12 @@ export class DialogHassioNetwork
<span slot="title"> <span slot="title">
${this.supervisor.localize("dialog.network.title")} ${this.supervisor.localize("dialog.network.title")}
</span> </span>
<mwc-icon-button slot="actionItems" dialogAction="cancel"> <ha-icon-button
<ha-svg-icon .path=${mdiClose}></ha-svg-icon> .label=${this.hass.localize("common.close")}
</mwc-icon-button> .path=${mdiClose}
slot="actionItems"
dialogAction="cancel"
></ha-icon-button>
</ha-header-bar> </ha-header-bar>
${this._interfaces.length > 1 ${this._interfaces.length > 1
? html`<mwc-tab-bar ? html`<mwc-tab-bar

View File

@ -1,5 +1,4 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@material/mwc-icon-button/mwc-icon-button";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiDelete } from "@mdi/js"; import { mdiDelete } from "@mdi/js";
import { PaperInputElement } from "@polymer/paper-input/paper-input"; import { PaperInputElement } from "@polymer/paper-input/paper-input";
@ -7,7 +6,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import "../../../../src/components/ha-circular-progress"; import "../../../../src/components/ha-circular-progress";
import { createCloseHeading } from "../../../../src/components/ha-dialog"; import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-svg-icon"; import "../../../../src/components/ha-icon-button";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { import {
addHassioDockerRegistry, addHassioDockerRegistry,
@ -110,16 +109,15 @@ class HassioRegistriesDialog extends LitElement {
)}: )}:
${entry.username}</span ${entry.username}</span
> >
<mwc-icon-button <ha-icon-button
.entry=${entry} .entry=${entry}
.title=${this.supervisor.localize( .label=${this.supervisor.localize(
"dialog.registries.remove" "dialog.registries.remove"
)} )}
.path=${mdiDelete}
slot="meta" slot="meta"
@click=${this._removeRegistry} @click=${this._removeRegistry}
> ></ha-icon-button>
<ha-svg-icon .path=${mdiDelete}></ha-svg-icon>
</mwc-icon-button>
</mwc-list-item> </mwc-list-item>
` `
) )
@ -234,7 +232,7 @@ class HassioRegistriesDialog extends LitElement {
mwc-button { mwc-button {
margin-left: 8px; margin-left: 8px;
} }
mwc-icon-button { ha-icon-button {
color: var(--error-color); color: var(--error-color);
margin: -10px; margin: -10px;
} }

View File

@ -1,5 +1,4 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiDelete } from "@mdi/js"; import { mdiDelete } 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";
@ -13,7 +12,7 @@ import { caseInsensitiveStringCompare } from "../../../../src/common/string/comp
import "../../../../src/components/ha-alert"; import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-circular-progress"; import "../../../../src/components/ha-circular-progress";
import { createCloseHeading } from "../../../../src/components/ha-dialog"; import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-svg-icon"; import "../../../../src/components/ha-icon-button";
import { import {
fetchHassioAddonsInfo, fetchHassioAddonsInfo,
HassioAddonRepository, HassioAddonRepository,
@ -90,15 +89,14 @@ class HassioRepositoriesDialog extends LitElement {
<div secondary>${repo.maintainer}</div> <div secondary>${repo.maintainer}</div>
<div secondary>${repo.url}</div> <div secondary>${repo.url}</div>
</paper-item-body> </paper-item-body>
<mwc-icon-button <ha-icon-button
.slug=${repo.slug} .slug=${repo.slug}
.title=${this._dialogParams!.supervisor.localize( .label=${this._dialogParams!.supervisor.localize(
"dialog.repositories.remove" "dialog.repositories.remove"
)} )}
.path=${mdiDelete}
@click=${this._removeRepository} @click=${this._removeRepository}
> ></ha-icon-button>
<ha-svg-icon .path=${mdiDelete}></ha-svg-icon>
</mwc-icon-button>
</paper-item> </paper-item>
` `
) )

View File

@ -6,7 +6,6 @@ import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-circular-progress"; import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog"; import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-settings-row"; import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-switch"; import "../../../../src/components/ha-switch";
import { import {
extractApiErrorMessage, extractApiErrorMessage,

View File

@ -113,12 +113,6 @@ export class HassioMain extends SupervisorBaseElement {
: this.hass.themes.default_theme); : this.hass.themes.default_theme);
themeSettings = this.hass.selectedTheme; themeSettings = this.hass.selectedTheme;
if (themeSettings?.dark === undefined) {
themeSettings = {
...this.hass.selectedTheme,
dark: this.hass.themes.darkMode,
};
}
} else { } else {
themeName = themeName =
(this.hass.selectedTheme as unknown as string) || (this.hass.selectedTheme as unknown as string) ||

View File

@ -12,6 +12,7 @@ import { fireEvent } from "../../../src/common/dom/fire_event";
import { navigate } from "../../../src/common/navigate"; import { navigate } from "../../../src/common/navigate";
import { extractSearchParam } from "../../../src/common/url/search-params"; import { extractSearchParam } from "../../../src/common/url/search-params";
import { nextRender } from "../../../src/common/util/render-status"; import { nextRender } from "../../../src/common/util/render-status";
import "../../../src/components/ha-icon-button";
import { import {
fetchHassioAddonInfo, fetchHassioAddonInfo,
HassioAddonDetails, HassioAddonDetails,
@ -72,12 +73,11 @@ class HassioIngressView extends LitElement {
return html`${this.narrow || this.hass.dockedSidebar === "always_hidden" return html`${this.narrow || this.hass.dockedSidebar === "always_hidden"
? html`<div class="header"> ? html`<div class="header">
<mwc-icon-button <ha-icon-button
aria-label=${this.hass.localize("ui.sidebar.sidebar_toggle")} .label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
.path=${mdiMenu}
@click=${this._toggleMenu} @click=${this._toggleMenu}
> ></ha-icon-button>
<ha-svg-icon .path=${mdiMenu}></ha-svg-icon>
</mwc-icon-button>
<div class="main-title">${this._addon.name}</div> <div class="main-title">${this._addon.name}</div>
</div> </div>
${iframe}` ${iframe}`
@ -241,7 +241,7 @@ class HassioIngressView extends LitElement {
flex-grow: 1; flex-grow: 1;
} }
mwc-icon-button { ha-icon-button {
pointer-events: auto; pointer-events: auto;
} }

View File

@ -9,6 +9,7 @@ import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-button-menu"; import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-settings-row"; import "../../../src/components/ha-settings-row";
import { import {
extractApiErrorMessage, extractApiErrorMessage,
@ -181,9 +182,11 @@ class HassioHostInfo extends LitElement {
: ""} : ""}
<ha-button-menu corner="BOTTOM_START"> <ha-button-menu corner="BOTTOM_START">
<mwc-icon-button slot="trigger"> <ha-icon-button
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon> .label=${this.hass.localize("common.menu")}
</mwc-icon-button> .path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>
<mwc-list-item <mwc-list-item
.action=${"hardware"} .action=${"hardware"}
@click=${this._handleMenuAction} @click=${this._handleMenuAction}

View File

@ -31,29 +31,9 @@ import { documentationUrl } from "../../../src/util/documentation-url";
import "../components/supervisor-metric"; import "../components/supervisor-metric";
import { hassioStyle } from "../resources/hassio-style"; import { hassioStyle } from "../resources/hassio-style";
const UNSUPPORTED_REASON_URL = { const UNSUPPORTED_REASON_URL = {};
apparmor: "/more-info/unsupported/apparmor",
container: "/more-info/unsupported/container",
content_trust: "/more-info/unsupported/content_trust",
dbus: "/more-info/unsupported/dbus",
docker_configuration: "/more-info/unsupported/docker_configuration",
docker_version: "/more-info/unsupported/docker_version",
job_conditions: "/more-info/unsupported/job_conditions",
lxc: "/more-info/unsupported/lxc",
network_manager: "/more-info/unsupported/network_manager",
os_agent: "/more-info/unsupported/os_agent",
os: "/more-info/unsupported/os",
privileged: "/more-info/unsupported/privileged",
source_mods: "/more-info/unsupported/source_mods",
systemd: "/more-info/unsupported/systemd",
};
const UNHEALTHY_REASON_URL = { const UNHEALTHY_REASON_URL = {
privileged: "/more-info/unsupported/privileged", privileged: "/more-info/unsupported/privileged",
supervisor: "/more-info/unhealthy/supervisor",
setup: "/more-info/unhealthy/setup",
docker: "/more-info/unhealthy/docker",
untrusted: "/more-info/unhealthy/untrusted",
}; };
@customElement("hassio-supervisor-info") @customElement("hassio-supervisor-info")
@ -425,20 +405,19 @@ class HassioSupervisorInfo extends LitElement {
${this.supervisor.resolution.unsupported.map( ${this.supervisor.resolution.unsupported.map(
(reason) => html` (reason) => html`
<li> <li>
${UNSUPPORTED_REASON_URL[reason] <a
? html`<a href=${documentationUrl(
href=${documentationUrl( this.hass,
this.hass, UNSUPPORTED_REASON_URL[reason] ||
UNSUPPORTED_REASON_URL[reason] `/more-info/unsupported/${reason}`
)} )}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
${this.supervisor.localize( ${this.supervisor.localize(
`system.supervisor.unsupported_reason.${reason}` `system.supervisor.unsupported_reason.${reason}`
) || reason} ) || reason}
</a>` </a>
: reason}
</li> </li>
` `
)} )}
@ -456,20 +435,19 @@ class HassioSupervisorInfo extends LitElement {
${this.supervisor.resolution.unhealthy.map( ${this.supervisor.resolution.unhealthy.map(
(reason) => html` (reason) => html`
<li> <li>
${UNHEALTHY_REASON_URL[reason] <a
? html`<a href=${documentationUrl(
href=${documentationUrl( this.hass,
this.hass, UNHEALTHY_REASON_URL[reason] ||
UNHEALTHY_REASON_URL[reason] `/more-info/unhealthy/${reason}`
)} )}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
${this.supervisor.localize( ${this.supervisor.localize(
`system.supervisor.unhealthy_reason.${reason}` `system.supervisor.unhealthy_reason.${reason}`
) || reason} ) || reason}
</a>` </a>
: reason}
</li> </li>
` `
)} )}

View File

@ -22,23 +22,23 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@braintree/sanitize-url": "^5.0.2", "@braintree/sanitize-url": "^5.0.2",
"@codemirror/commands": "^0.19.2", "@codemirror/commands": "^0.19.5",
"@codemirror/gutter": "^0.19.1", "@codemirror/gutter": "^0.19.3",
"@codemirror/highlight": "^0.19.2", "@codemirror/highlight": "^0.19.6",
"@codemirror/history": "^0.19.0", "@codemirror/history": "^0.19.0",
"@codemirror/legacy-modes": "^0.19.0", "@codemirror/legacy-modes": "^0.19.0",
"@codemirror/rectangular-selection": "^0.19.0", "@codemirror/rectangular-selection": "^0.19.1",
"@codemirror/search": "^0.19.0", "@codemirror/search": "^0.19.2",
"@codemirror/state": "^0.19.1", "@codemirror/state": "^0.19.2",
"@codemirror/stream-parser": "^0.19.1", "@codemirror/stream-parser": "^0.19.2",
"@codemirror/text": "^0.19.2", "@codemirror/text": "^0.19.4",
"@codemirror/view": "^0.19.4", "@codemirror/view": "^0.19.9",
"@formatjs/intl-datetimeformat": "^4.2.4", "@formatjs/intl-datetimeformat": "^4.2.5",
"@formatjs/intl-getcanonicallocales": "^1.7.3", "@formatjs/intl-getcanonicallocales": "^1.8.0",
"@formatjs/intl-locale": "^2.4.38", "@formatjs/intl-locale": "^2.4.40",
"@formatjs/intl-numberformat": "^7.2.4", "@formatjs/intl-numberformat": "^7.2.5",
"@formatjs/intl-pluralrules": "^4.1.4", "@formatjs/intl-pluralrules": "^4.1.5",
"@formatjs/intl-relativetimeformat": "^9.3.1", "@formatjs/intl-relativetimeformat": "^9.3.2",
"@formatjs/intl-utils": "^3.8.4", "@formatjs/intl-utils": "^3.8.4",
"@fullcalendar/common": "5.9.0", "@fullcalendar/common": "5.9.0",
"@fullcalendar/core": "5.9.0", "@fullcalendar/core": "5.9.0",
@ -46,45 +46,38 @@
"@fullcalendar/interaction": "5.9.0", "@fullcalendar/interaction": "5.9.0",
"@fullcalendar/list": "5.9.0", "@fullcalendar/list": "5.9.0",
"@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.6.0#./.yarn/patches/@lit-labs/virtualizer/0.7.0.patch", "@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.6.0#./.yarn/patches/@lit-labs/virtualizer/0.7.0.patch",
"@material/chips": "13.0.0-canary.65125b3a6.0", "@material/chips": "14.0.0-canary.261f2db59.0",
"@material/data-table": "13.0.0-canary.65125b3a6.0", "@material/data-table": "14.0.0-canary.261f2db59.0",
"@material/mwc-button": "0.25.1", "@material/mwc-button": "0.25.3",
"@material/mwc-checkbox": "0.25.1", "@material/mwc-checkbox": "0.25.3",
"@material/mwc-circular-progress": "0.25.1", "@material/mwc-circular-progress": "0.25.3",
"@material/mwc-dialog": "0.25.1", "@material/mwc-dialog": "0.25.3",
"@material/mwc-fab": "0.25.1", "@material/mwc-fab": "0.25.3",
"@material/mwc-formfield": "0.25.1", "@material/mwc-formfield": "0.25.3",
"@material/mwc-icon-button": "0.25.1", "@material/mwc-icon-button": "patch:@material/mwc-icon-button@0.25.3#./.yarn/patches/@material/mwc-icon-button/remove-icon.patch",
"@material/mwc-linear-progress": "0.25.1", "@material/mwc-linear-progress": "0.25.3",
"@material/mwc-list": "0.25.1", "@material/mwc-list": "0.25.3",
"@material/mwc-menu": "0.25.1", "@material/mwc-menu": "0.25.3",
"@material/mwc-radio": "0.25.1", "@material/mwc-radio": "0.25.3",
"@material/mwc-ripple": "0.25.1", "@material/mwc-ripple": "0.25.3",
"@material/mwc-switch": "0.25.1", "@material/mwc-select": "0.25.3",
"@material/mwc-tab": "0.25.1", "@material/mwc-slider": "0.25.3",
"@material/mwc-tab-bar": "0.25.1", "@material/mwc-switch": "0.25.3",
"@material/top-app-bar": "13.0.0-canary.65125b3a6.0", "@material/mwc-tab": "0.25.3",
"@mdi/js": "6.2.95", "@material/mwc-tab-bar": "0.25.3",
"@mdi/svg": "6.2.95", "@material/mwc-textfield": "0.25.3",
"@material/top-app-bar": "14.0.0-canary.261f2db59.0",
"@mdi/js": "6.4.95",
"@mdi/svg": "6.4.95",
"@polymer/app-layout": "^3.1.0", "@polymer/app-layout": "^3.1.0",
"@polymer/iron-flex-layout": "^3.0.1", "@polymer/iron-flex-layout": "^3.0.1",
"@polymer/iron-icon": "^3.0.1", "@polymer/iron-icon": "^3.0.1",
"@polymer/iron-input": "^3.0.1", "@polymer/iron-input": "^3.0.1",
"@polymer/iron-overlay-behavior": "^3.0.3",
"@polymer/iron-resizable-behavior": "^3.0.1", "@polymer/iron-resizable-behavior": "^3.0.1",
"@polymer/paper-checkbox": "^3.1.0",
"@polymer/paper-dialog": "^3.0.1",
"@polymer/paper-dialog-behavior": "^3.0.1",
"@polymer/paper-dialog-scrollable": "^3.0.1",
"@polymer/paper-dropdown-menu": "^3.2.0", "@polymer/paper-dropdown-menu": "^3.2.0",
"@polymer/paper-input": "^3.2.1", "@polymer/paper-input": "^3.2.1",
"@polymer/paper-item": "^3.0.1", "@polymer/paper-item": "^3.0.1",
"@polymer/paper-listbox": "^3.0.1", "@polymer/paper-listbox": "^3.0.1",
"@polymer/paper-menu-button": "^3.1.0",
"@polymer/paper-progress": "^3.0.1",
"@polymer/paper-radio-button": "^3.0.1",
"@polymer/paper-radio-group": "^3.0.1",
"@polymer/paper-ripple": "^3.0.2",
"@polymer/paper-slider": "^3.0.1", "@polymer/paper-slider": "^3.0.1",
"@polymer/paper-styles": "^3.0.1", "@polymer/paper-styles": "^3.0.1",
"@polymer/paper-tabs": "^3.1.0", "@polymer/paper-tabs": "^3.1.0",
@ -115,7 +108,7 @@
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"leaflet": "^1.7.1", "leaflet": "^1.7.1",
"leaflet-draw": "^1.0.4", "leaflet-draw": "^1.0.4",
"lit": "^2.0.0", "lit": "^2.0.2",
"lit-vaadin-helpers": "^0.2.1", "lit-vaadin-helpers": "^0.2.1",
"marked": "^3.0.2", "marked": "^3.0.2",
"memoize-one": "^5.2.1", "memoize-one": "^5.2.1",
@ -187,7 +180,7 @@
"eslint-import-resolver-webpack": "^0.13.1", "eslint-import-resolver-webpack": "^0.13.1",
"eslint-plugin-disable": "^2.0.1", "eslint-plugin-disable": "^2.0.1",
"eslint-plugin-import": "^2.24.2", "eslint-plugin-import": "^2.24.2",
"eslint-plugin-lit": "^1.5.1", "eslint-plugin-lit": "^1.6.1",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-unused-imports": "^1.1.5", "eslint-plugin-unused-imports": "^1.1.5",
"eslint-plugin-wc": "^1.3.2", "eslint-plugin-wc": "^1.3.2",
@ -237,10 +230,10 @@
"resolutions": { "resolutions": {
"@polymer/polymer": "patch:@polymer/polymer@3.4.1#./.yarn/patches/@polymer/polymer/pr-5569.patch", "@polymer/polymer": "patch:@polymer/polymer@3.4.1#./.yarn/patches/@polymer/polymer/pr-5569.patch",
"@webcomponents/webcomponentsjs": "^2.2.10", "@webcomponents/webcomponentsjs": "^2.2.10",
"lit": "^2.0.0", "lit": "^2.0.2",
"lit-html": "2.0.0", "lit-html": "2.0.1",
"lit-element": "3.0.0", "lit-element": "3.0.1",
"@lit/reactive-element": "1.0.0" "@lit/reactive-element": "1.0.1"
}, },
"main": "src/home-assistant.js", "main": "src/home-assistant.js",
"husky": { "husky": {

View File

@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup( setup(
name="home-assistant-frontend", name="home-assistant-frontend",
version="20211007.1", version="20211026.0",
description="The Home Assistant frontend", description="The Home Assistant frontend",
url="https://github.com/home-assistant/frontend", url="https://github.com/home-assistant/frontend",
author="The Home Assistant Authors", author="The Home Assistant Authors",

View File

@ -7,16 +7,20 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import "./ha-password-manager-polyfill";
import { property, state } from "lit/decorators"; import { property, state } from "lit/decorators";
import "../components/ha-checkbox";
import "../components/ha-form/ha-form"; import "../components/ha-form/ha-form";
import "../components/ha-formfield";
import "../components/ha-markdown"; import "../components/ha-markdown";
import "../components/ha-alert";
import { AuthProvider } from "../data/auth"; import { AuthProvider } from "../data/auth";
import { import {
DataEntryFlowStep, DataEntryFlowStep,
DataEntryFlowStepForm, DataEntryFlowStepForm,
} from "../data/data_entry_flow"; } from "../data/data_entry_flow";
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin"; import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
import { computeInitialHaFormData } from "../components/ha-form/compute-initial-ha-form-data";
import "./ha-password-manager-polyfill";
type State = "loading" | "error" | "step"; type State = "loading" | "error" | "step";
@ -31,12 +35,44 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
@state() private _state: State = "loading"; @state() private _state: State = "loading";
@state() private _stepData: any = {}; @state() private _stepData?: Record<string, any>;
@state() private _step?: DataEntryFlowStep; @state() private _step?: DataEntryFlowStep;
@state() private _errorMessage?: string; @state() private _errorMessage?: string;
@state() private _submitting = false;
@state() private _storeToken = false;
willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!changedProps.has("_step")) {
return;
}
if (!this._step) {
this._stepData = undefined;
return;
}
const oldStep = changedProps.get("_step") as HaAuthFlow["_step"];
if (
!oldStep ||
this._step.flow_id !== oldStep.flow_id ||
(this._step.type === "form" &&
oldStep.type === "form" &&
this._step.step_id !== oldStep.step_id)
) {
this._stepData =
this._step.type === "form"
? computeInitialHaFormData(this._step.data_schema)
: undefined;
}
}
protected render() { protected render() {
return html` return html`
<form>${this._renderForm()}</form> <form>${this._renderForm()}</form>
@ -76,6 +112,24 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
if (changedProps.has("authProvider")) { if (changedProps.has("authProvider")) {
this._providerChanged(this.authProvider); this._providerChanged(this.authProvider);
} }
if (!changedProps.has("_step") || this._step?.type !== "form") {
return;
}
// 100ms to give all the form elements time to initialize.
setTimeout(() => {
const form = this.renderRoot.querySelector("ha-form");
if (form) {
(form as any).focus();
}
}, 100);
setTimeout(() => {
this.renderRoot.querySelector(
"ha-password-manager-polyfill"
)!.boundingRect = this.getBoundingClientRect();
}, 500);
} }
private _renderForm(): TemplateResult { private _renderForm(): TemplateResult {
@ -87,27 +141,33 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
return html` return html`
${this._renderStep(this._step)} ${this._renderStep(this._step)}
<div class="action"> <div class="action">
<mwc-button raised @click=${this._handleSubmit} <mwc-button
>${this._step.type === "form" raised
? this.localize("ui.panel.page-authorize.form.next") @click=${this._handleSubmit}
: this.localize( .disabled=${this._submitting}
"ui.panel.page-authorize.form.start_over"
)}</mwc-button
> >
${this._step.type === "form"
? this.localize("ui.panel.page-authorize.form.next")
: this.localize("ui.panel.page-authorize.form.start_over")}
</mwc-button>
</div> </div>
`; `;
case "error": case "error":
return html` return html`
<div class="error"> <ha-alert alert-type="error">
${this.localize( ${this.localize(
"ui.panel.page-authorize.form.error", "ui.panel.page-authorize.form.error",
"error", "error",
this._errorMessage this._errorMessage
)} )}
</div> </ha-alert>
`; `;
case "loading": case "loading":
return html` ${this.localize("ui.panel.page-authorize.form.working")} `; return html`
<ha-alert alert-type="info">
${this.localize("ui.panel.page-authorize.form.working")}
</ha-alert>
`;
default: default:
return html``; return html``;
} }
@ -140,16 +200,34 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
.data=${this._stepData} .data=${this._stepData}
.schema=${step.data_schema} .schema=${step.data_schema}
.error=${step.errors} .error=${step.errors}
.disabled=${this._submitting}
.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-form>
${this.clientId === window.location.origin && step.step_id !== "mfa"
? html`
<ha-formfield
class="store-token"
.label=${this.localize("ui.panel.page-authorize.store_token")}
>
<ha-checkbox
.checked=${this._storeToken}
@change=${this._storeTokenChanged}
></ha-checkbox>
</ha-formfield>
`
: ""}
`; `;
default: default:
return html``; return html``;
} }
} }
private _storeTokenChanged(e: CustomEvent<HTMLInputElement>) {
this._storeToken = (e.currentTarget as HTMLInputElement).checked;
}
private async _providerChanged(newProvider?: AuthProvider) { private async _providerChanged(newProvider?: AuthProvider) {
if (this._step && this._step.type === "form") { if (this._step && this._step.type === "form") {
fetch(`/auth/login_flow/${this._step.flow_id}`, { fetch(`/auth/login_flow/${this._step.flow_id}`, {
@ -189,7 +267,8 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
return; return;
} }
await this._updateStep(data); this._step = data;
this._state = "step";
} else { } else {
this._state = "error"; this._state = "error";
this._errorMessage = data.message; this._errorMessage = data.message;
@ -216,43 +295,13 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
if (this.oauth2State) { if (this.oauth2State) {
url += `&state=${encodeURIComponent(this.oauth2State)}`; url += `&state=${encodeURIComponent(this.oauth2State)}`;
} }
if (this._storeToken) {
url += `&storeToken=true`;
}
document.location.assign(url); document.location.assign(url);
} }
private async _updateStep(step: DataEntryFlowStep) {
let stepData: any = null;
if (
this._step &&
(step.flow_id !== this._step.flow_id ||
(step.type === "form" &&
this._step.type === "form" &&
step.step_id !== this._step.step_id))
) {
stepData = {};
}
this._step = step;
this._state = "step";
if (stepData != null) {
this._stepData = stepData;
}
await this.updateComplete;
// 100ms to give all the form elements time to initialize.
setTimeout(() => {
const form = this.renderRoot.querySelector("ha-form");
if (form) {
(form as any).focus();
}
}, 100);
setTimeout(() => {
this.renderRoot.querySelector(
"ha-password-manager-polyfill"
)!.boundingRect = this.getBoundingClientRect();
}, 500);
}
private _stepDataChanged(ev: CustomEvent) { private _stepDataChanged(ev: CustomEvent) {
this._stepData = ev.detail.value; this._stepData = ev.detail.value;
} }
@ -297,9 +346,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
this._providerChanged(this.authProvider); this._providerChanged(this.authProvider);
return; return;
} }
this._state = "loading"; this._submitting = true;
// To avoid a jumping UI.
this.style.setProperty("min-height", `${this.offsetHeight}px`);
const postData = { ...this._stepData, client_id: this.clientId }; const postData = { ...this._stepData, client_id: this.clientId };
@ -316,29 +363,28 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
this._redirect(newStep.result); this._redirect(newStep.result);
return; return;
} }
await this._updateStep(newStep); this._step = newStep;
this._state = "step";
} catch (err: any) { } catch (err: any) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error("Error submitting step", err); console.error("Error submitting step", err);
this._state = "error"; this._state = "error";
this._errorMessage = this._unknownError(); this._errorMessage = this._unknownError();
} finally { } finally {
this.style.setProperty("min-height", ""); this._submitting = false;
} }
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host {
/* So we can set min-height to avoid jumping during loading */
display: block;
}
.action { .action {
margin: 24px 0 8px; margin: 24px 0 8px;
text-align: center; text-align: center;
} }
.error { /* Align with the rest of the form. */
color: red; .store-token {
margin-top: 10px;
margin-left: -16px;
} }
`; `;
} }

View File

@ -174,6 +174,10 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
display: block; display: block;
margin-top: 48px; margin-top: 48px;
} }
ha-auth-flow {
display: block;
margin-top: 24px;
}
`; `;
} }
} }

View File

@ -2,8 +2,8 @@
import { html, LitElement, TemplateResult } from "lit"; import { html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { HaFormSchema } from "../components/ha-form/ha-form"; import type { HaFormSchema } from "../components/ha-form/types";
import { DataEntryFlowStep } from "../data/data_entry_flow"; import type { DataEntryFlowStep } from "../data/data_entry_flow";
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {

View File

@ -30,6 +30,14 @@ export function askWrite() {
export function saveTokens(tokens: AuthData | null) { export function saveTokens(tokens: AuthData | null) {
tokenCache.tokens = tokens; tokenCache.tokens = tokens;
if (
!tokenCache.writeEnabled &&
new URLSearchParams(window.location.search).get("storeToken") === "true"
) {
tokenCache.writeEnabled = true;
}
if (tokenCache.writeEnabled) { if (tokenCache.writeEnabled) {
try { try {
storage.hassTokens = JSON.stringify(tokens); storage.hassTokens = JSON.stringify(tokens);
@ -45,7 +53,6 @@ export function enableWrite() {
saveTokens(tokenCache.tokens); saveTokens(tokenCache.tokens);
} }
} }
export function loadTokens() { export function loadTokens() {
if (tokenCache.tokens === undefined) { if (tokenCache.tokens === undefined) {
try { try {

View File

@ -4,6 +4,10 @@ export const atLeastVersion = (
minor: number, minor: number,
patch?: number patch?: number
): boolean => { ): boolean => {
if (__DEMO__) {
return true;
}
const [haMajor, haMinor, haPatch] = version.split(".", 3); const [haMajor, haMinor, haPatch] = version.split(".", 3);
return ( return (

View File

@ -1,88 +1,146 @@
/** Constants to be used in the frontend. */ /** Constants to be used in the frontend. */
import {
mdiAccount,
mdiAirFilter,
mdiAlert,
mdiAngleAcute,
mdiAppleSafari,
mdiBell,
mdiBookmark,
mdiBrightness5,
mdiBullhorn,
mdiCalendar,
mdiCalendarClock,
mdiCash,
mdiClock,
mdiCloudUpload,
mdiCog,
mdiCommentAlert,
mdiCounter,
mdiCurrentAc,
mdiEye,
mdiFan,
mdiFlash,
mdiFlower,
mdiFormatListBulleted,
mdiFormTextbox,
mdiGasCylinder,
mdiGauge,
mdiGoogleAssistant,
mdiGoogleCirclesCommunities,
mdiHomeAssistant,
mdiHomeAutomation,
mdiImageFilterFrames,
mdiLightbulb,
mdiLightningBolt,
mdiMailbox,
mdiMapMarkerRadius,
mdiMolecule,
mdiMoleculeCo,
mdiMoleculeCo2,
mdiPalette,
mdiRayVertex,
mdiRemote,
mdiRobot,
mdiRobotVacuum,
mdiScriptText,
mdiSineWave,
mdiTextToSpeech,
mdiThermometer,
mdiThermostat,
mdiTimerOutline,
mdiToggleSwitchOutline,
mdiVideo,
mdiWaterPercent,
mdiWeatherCloudy,
mdiWhiteBalanceSunny,
mdiWifi,
} from "@mdi/js";
// 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.
/** Icon to use when no icon specified for domain. */ /** Icon to use when no icon specified for domain. */
export const DEFAULT_DOMAIN_ICON = "hass:bookmark"; export const DEFAULT_DOMAIN_ICON = mdiBookmark;
/** Icons for each domain */ /** Icons for each domain */
export const FIXED_DOMAIN_ICONS = { export const FIXED_DOMAIN_ICONS = {
alert: "hass:alert", alert: mdiAlert,
alexa: "hass:amazon-alexa", air_quality: mdiAirFilter,
air_quality: "hass:air-filter", automation: mdiRobot,
automation: "hass:robot", calendar: mdiCalendar,
calendar: "hass:calendar", camera: mdiVideo,
camera: "hass:video", climate: mdiThermostat,
climate: "hass:thermostat", configurator: mdiCog,
configurator: "hass:cog", conversation: mdiTextToSpeech,
conversation: "hass:text-to-speech", counter: mdiCounter,
counter: "hass:counter", device_tracker: mdiAccount,
device_tracker: "hass:account", fan: mdiFan,
fan: "hass:fan", google_assistant: mdiGoogleAssistant,
google_assistant: "hass:google-assistant", group: mdiGoogleCirclesCommunities,
group: "hass:google-circles-communities", homeassistant: mdiHomeAssistant,
homeassistant: "hass:home-assistant", homekit: mdiHomeAutomation,
homekit: "hass:home-automation", image_processing: mdiImageFilterFrames,
image_processing: "hass:image-filter-frames", input_boolean: mdiToggleSwitchOutline,
input_boolean: "hass:toggle-switch-outline", input_datetime: mdiCalendarClock,
input_datetime: "hass:calendar-clock", input_number: mdiRayVertex,
input_number: "hass:ray-vertex", input_select: mdiFormatListBulleted,
input_select: "hass:format-list-bulleted", input_text: mdiFormTextbox,
input_text: "hass:form-textbox", light: mdiLightbulb,
light: "hass:lightbulb", mailbox: mdiMailbox,
mailbox: "hass:mailbox", notify: mdiCommentAlert,
notify: "hass:comment-alert", number: mdiRayVertex,
number: "hass:ray-vertex", persistent_notification: mdiBell,
persistent_notification: "hass:bell", person: mdiAccount,
person: "hass:account", plant: mdiFlower,
plant: "hass:flower", proximity: mdiAppleSafari,
proximity: "hass:apple-safari", remote: mdiRemote,
remote: "hass:remote", scene: mdiPalette,
scene: "hass:palette", script: mdiScriptText,
script: "hass:script-text", select: mdiFormatListBulleted,
select: "hass:format-list-bulleted", sensor: mdiEye,
sensor: "hass:eye", siren: mdiBullhorn,
simple_alarm: "hass:bell", simple_alarm: mdiBell,
sun: "hass:white-balance-sunny", sun: mdiWhiteBalanceSunny,
switch: "hass:flash", switch: mdiFlash,
timer: "hass:timer-outline", timer: mdiTimerOutline,
updater: "hass:cloud-upload", updater: mdiCloudUpload,
vacuum: "hass:robot-vacuum", vacuum: mdiRobotVacuum,
water_heater: "hass:thermometer", water_heater: mdiThermometer,
weather: "hass:weather-cloudy", weather: mdiWeatherCloudy,
zone: "hass:map-marker-radius", zone: mdiMapMarkerRadius,
}; };
export const FIXED_DEVICE_CLASS_ICONS = { export const FIXED_DEVICE_CLASS_ICONS = {
aqi: "hass:air-filter", aqi: mdiAirFilter,
battery: "hass:battery", // battery: mdiBattery, => not included by design since `sensorIcon()` will dynamically determine the icon
carbon_dioxide: "mdi:molecule-co2", carbon_dioxide: mdiMoleculeCo2,
carbon_monoxide: "mdi:molecule-co", carbon_monoxide: mdiMoleculeCo,
current: "hass:current-ac", current: mdiCurrentAc,
date: "hass:calendar", date: mdiCalendar,
energy: "hass:lightning-bolt", energy: mdiLightningBolt,
gas: "hass:gas-cylinder", gas: mdiGasCylinder,
humidity: "hass:water-percent", humidity: mdiWaterPercent,
illuminance: "hass:brightness-5", illuminance: mdiBrightness5,
monetary: "mdi:cash", monetary: mdiCash,
nitrogen_dioxide: "mdi:molecule", nitrogen_dioxide: mdiMolecule,
nitrogen_monoxide: "mdi:molecule", nitrogen_monoxide: mdiMolecule,
nitrous_oxide: "mdi:molecule", nitrous_oxide: mdiMolecule,
ozone: "mdi:molecule", ozone: mdiMolecule,
pm1: "mdi:molecule", pm1: mdiMolecule,
pm10: "mdi:molecule", pm10: mdiMolecule,
pm25: "mdi:molecule", pm25: mdiMolecule,
power: "hass:flash", power: mdiFlash,
power_factor: "hass:angle-acute", power_factor: mdiAngleAcute,
pressure: "hass:gauge", pressure: mdiGauge,
signal_strength: "hass:wifi", signal_strength: mdiWifi,
sulphur_dioxide: "mdi:molecule", sulphur_dioxide: mdiMolecule,
temperature: "hass:thermometer", temperature: mdiThermometer,
timestamp: "hass:clock", timestamp: mdiClock,
volatile_organic_compounds: "mdi:molecule", volatile_organic_compounds: mdiMolecule,
voltage: "hass:sine-wave", voltage: mdiSineWave,
}; };
/** Domains that have a state card. */ /** Domains that have a state card. */

View File

@ -36,55 +36,62 @@ export const applyThemesOnElement = (
let cacheKey = selectedTheme; let cacheKey = selectedTheme;
let themeRules: Partial<ThemeVars> = {}; let themeRules: Partial<ThemeVars> = {};
if (themeSettings) { // If there is no explicitly desired dark mode provided, we automatically
if (themeSettings.dark) { // use the active one from hass.themes.
cacheKey = `${cacheKey}__dark`; if (!themeSettings || themeSettings?.dark === undefined) {
themeRules = { ...darkStyles }; themeSettings = {
...themeSettings,
dark: themes.darkMode,
};
}
if (themeSettings.dark) {
cacheKey = `${cacheKey}__dark`;
themeRules = { ...darkStyles };
}
if (selectedTheme === "default") {
// Determine the primary and accent colors from the current settings.
// Fallbacks are implicitly the HA default blue and orange or the
// derived "darkStyles" values, depending on the light vs dark mode.
const primaryColor = themeSettings.primaryColor;
const accentColor = themeSettings.accentColor;
if (themeSettings.dark && primaryColor) {
themeRules["app-header-background-color"] = hexBlend(
primaryColor,
"#121212",
8
);
} }
if (selectedTheme === "default") { if (primaryColor) {
// Determine the primary and accent colors from the current settings. cacheKey = `${cacheKey}__primary_${primaryColor}`;
// Fallbacks are implicitly the HA default blue and orange or the const rgbPrimaryColor = hex2rgb(primaryColor);
// derived "darkStyles" values, depending on the light vs dark mode. const labPrimaryColor = rgb2lab(rgbPrimaryColor);
const primaryColor = themeSettings.primaryColor; themeRules["primary-color"] = primaryColor;
const accentColor = themeSettings.accentColor; const rgbLightPrimaryColor = lab2rgb(labBrighten(labPrimaryColor));
themeRules["light-primary-color"] = rgb2hex(rgbLightPrimaryColor);
themeRules["dark-primary-color"] = lab2hex(labDarken(labPrimaryColor));
themeRules["text-primary-color"] =
rgbContrast(rgbPrimaryColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
themeRules["text-light-primary-color"] =
rgbContrast(rgbLightPrimaryColor, [33, 33, 33]) < 6
? "#fff"
: "#212121";
themeRules["state-icon-color"] = themeRules["dark-primary-color"];
}
if (accentColor) {
cacheKey = `${cacheKey}__accent_${accentColor}`;
themeRules["accent-color"] = accentColor;
const rgbAccentColor = hex2rgb(accentColor);
themeRules["text-accent-color"] =
rgbContrast(rgbAccentColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
}
if (themeSettings.dark && primaryColor) { // Nothing was changed
themeRules["app-header-background-color"] = hexBlend( if (element._themes?.cacheKey === cacheKey) {
primaryColor, return;
"#121212",
8
);
}
if (primaryColor) {
cacheKey = `${cacheKey}__primary_${primaryColor}`;
const rgbPrimaryColor = hex2rgb(primaryColor);
const labPrimaryColor = rgb2lab(rgbPrimaryColor);
themeRules["primary-color"] = primaryColor;
const rgbLightPrimaryColor = lab2rgb(labBrighten(labPrimaryColor));
themeRules["light-primary-color"] = rgb2hex(rgbLightPrimaryColor);
themeRules["dark-primary-color"] = lab2hex(labDarken(labPrimaryColor));
themeRules["text-primary-color"] =
rgbContrast(rgbPrimaryColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
themeRules["text-light-primary-color"] =
rgbContrast(rgbLightPrimaryColor, [33, 33, 33]) < 6
? "#fff"
: "#212121";
themeRules["state-icon-color"] = themeRules["dark-primary-color"];
}
if (accentColor) {
cacheKey = `${cacheKey}__accent_${accentColor}`;
themeRules["accent-color"] = accentColor;
const rgbAccentColor = hex2rgb(accentColor);
themeRules["text-accent-color"] =
rgbContrast(rgbAccentColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
}
// Nothing was changed
if (element._themes?.cacheKey === cacheKey) {
return;
}
} }
} }

View File

@ -1,24 +1,36 @@
/** Return an icon representing a alarm panel state. */ /** Return an icon representing a alarm panel state. */
import {
mdiShieldLock,
mdiShieldAirplane,
mdiShieldHome,
mdiShieldMoon,
mdiSecurity,
mdiShieldOutline,
mdiBellRing,
mdiShieldOff,
mdiShield,
} from "@mdi/js";
export const alarmPanelIcon = (state?: string) => { export const alarmPanelIcon = (state?: string) => {
switch (state) { switch (state) {
case "armed_away": case "armed_away":
return "hass:shield-lock"; return mdiShieldLock;
case "armed_vacation": case "armed_vacation":
return "hass:shield-airplane"; return mdiShieldAirplane;
case "armed_home": case "armed_home":
return "hass:shield-home"; return mdiShieldHome;
case "armed_night": case "armed_night":
return "hass:shield-moon"; return mdiShieldMoon;
case "armed_custom_bypass": case "armed_custom_bypass":
return "hass:security"; return mdiSecurity;
case "pending": case "pending":
return "hass:shield-outline"; return mdiShieldOutline;
case "triggered": case "triggered":
return "hass:bell-ring"; return mdiBellRing;
case "disarmed": case "disarmed":
return "hass:shield-off"; return mdiShieldOff;
default: default:
return "hass:shield"; return mdiShield;
} }
}; };

View File

@ -1,35 +1,92 @@
/** Return an icon representing a battery state. */ /** Return an icon representing a battery state. */
import {
mdiBattery,
mdiBattery10,
mdiBattery20,
mdiBattery30,
mdiBattery40,
mdiBattery50,
mdiBattery60,
mdiBattery70,
mdiBattery80,
mdiBattery90,
mdiBatteryAlert,
mdiBatteryAlertVariantOutline,
mdiBatteryCharging,
mdiBatteryCharging10,
mdiBatteryCharging20,
mdiBatteryCharging30,
mdiBatteryCharging40,
mdiBatteryCharging50,
mdiBatteryCharging60,
mdiBatteryCharging70,
mdiBatteryCharging80,
mdiBatteryCharging90,
mdiBatteryChargingOutline,
mdiBatteryUnknown,
} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
export const batteryIcon = ( const BATTERY_ICONS = {
10: mdiBattery10,
20: mdiBattery20,
30: mdiBattery30,
40: mdiBattery40,
50: mdiBattery50,
60: mdiBattery60,
70: mdiBattery70,
80: mdiBattery80,
90: mdiBattery90,
100: mdiBattery,
};
const BATTERY_CHARGING_ICONS = {
10: mdiBatteryCharging10,
20: mdiBatteryCharging20,
30: mdiBatteryCharging30,
40: mdiBatteryCharging40,
50: mdiBatteryCharging50,
60: mdiBatteryCharging60,
70: mdiBatteryCharging70,
80: mdiBatteryCharging80,
90: mdiBatteryCharging90,
100: mdiBatteryCharging,
};
export const batteryStateIcon = (
batteryState: HassEntity, batteryState: HassEntity,
batteryChargingState?: HassEntity batteryChargingState?: HassEntity
) => { ) => {
const battery = Number(batteryState.state); const battery = batteryState.state;
const battery_charging = const batteryCharging =
batteryChargingState && batteryChargingState.state === "on"; batteryChargingState && batteryChargingState.state === "on";
let icon = "hass:battery";
if (isNaN(battery)) { return batteryIcon(battery, batteryCharging);
if (batteryState.state === "off") { };
icon += "-full";
} else if (batteryState.state === "on") { export const batteryIcon = (
icon += "-alert"; batteryState: number | string,
} else { batteryCharging?: boolean
icon += "-unknown"; ) => {
} const batteryValue = Number(batteryState);
return icon; if (isNaN(batteryValue)) {
} if (batteryState === "off") {
return mdiBattery;
const batteryRound = Math.round(battery / 10) * 10; }
if (battery_charging && battery > 10) { if (batteryState === "on") {
icon += `-charging-${batteryRound}`; return mdiBatteryAlert;
} else if (battery_charging) { }
icon += "-outline"; return mdiBatteryUnknown;
} else if (battery <= 5) { }
icon += "-alert";
} else if (battery > 5 && battery < 95) { const batteryRound = Math.round(batteryValue / 10) * 10;
icon += `-${batteryRound}`; if (batteryCharging && batteryValue >= 10) {
} return BATTERY_CHARGING_ICONS[batteryRound];
return icon; }
if (batteryCharging) {
return mdiBatteryChargingOutline;
}
if (batteryValue <= 5) {
return mdiBatteryAlertVariantOutline;
}
return BATTERY_ICONS[batteryRound];
}; };

View File

@ -1,3 +1,46 @@
import {
mdiAlertCircle,
mdiBattery,
mdiBatteryCharging,
mdiBatteryOutline,
mdiBrightness5,
mdiBrightness7,
mdiCheckboxMarkedCircle,
mdiCheckCircle,
mdiCropPortrait,
mdiDoorClosed,
mdiDoorOpen,
mdiFire,
mdiGarage,
mdiGarageOpen,
mdiHome,
mdiHomeOutline,
mdiLock,
mdiLockOpen,
mdiMusicNote,
mdiMusicNoteOff,
mdiPackage,
mdiPackageUp,
mdiPlay,
mdiPowerPlug,
mdiPowerPlugOff,
mdiRadioboxBlank,
mdiRun,
mdiServerNetwork,
mdiServerNetworkOff,
mdiSmoke,
mdiSnowflake,
mdiSquare,
mdiSquareOutline,
mdiStop,
mdiThermometer,
mdiVibrate,
mdiWalk,
mdiWater,
mdiWaterOff,
mdiWindowClosed,
mdiWindowOpen,
} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
/** Return an icon representing a binary sensor state. */ /** Return an icon representing a binary sensor state. */
@ -6,52 +49,55 @@ export const binarySensorIcon = (state?: string, stateObj?: HassEntity) => {
const is_off = state === "off"; const is_off = state === "off";
switch (stateObj?.attributes.device_class) { switch (stateObj?.attributes.device_class) {
case "battery": case "battery":
return is_off ? "hass:battery" : "hass:battery-outline"; return is_off ? mdiBattery : mdiBatteryOutline;
case "battery_charging": case "battery_charging":
return is_off ? "hass:battery" : "hass:battery-charging"; return is_off ? mdiBattery : mdiBatteryCharging;
case "cold": case "cold":
return is_off ? "hass:thermometer" : "hass:snowflake"; return is_off ? mdiThermometer : mdiSnowflake;
case "connectivity": case "connectivity":
return is_off ? "hass:server-network-off" : "hass:server-network"; return is_off ? mdiServerNetworkOff : mdiServerNetwork;
case "door": case "door":
return is_off ? "hass:door-closed" : "hass:door-open"; return is_off ? mdiDoorClosed : mdiDoorOpen;
case "garage_door": case "garage_door":
return is_off ? "hass:garage" : "hass:garage-open"; return is_off ? mdiGarage : mdiGarageOpen;
case "power": case "power":
return is_off ? "hass:power-plug-off" : "hass:power-plug"; return is_off ? mdiPowerPlugOff : mdiPowerPlug;
case "gas": case "gas":
case "problem": case "problem":
case "safety": case "safety":
return is_off ? "hass:check-circle" : "hass:alert-circle"; case "tamper":
return is_off ? mdiCheckCircle : mdiAlertCircle;
case "smoke": case "smoke":
return is_off ? "hass:check-circle" : "hass:smoke"; return is_off ? mdiCheckCircle : mdiSmoke;
case "heat": case "heat":
return is_off ? "hass:thermometer" : "hass:fire"; return is_off ? mdiThermometer : mdiFire;
case "light": case "light":
return is_off ? "hass:brightness-5" : "hass:brightness-7"; return is_off ? mdiBrightness5 : mdiBrightness7;
case "lock": case "lock":
return is_off ? "hass:lock" : "hass:lock-open"; return is_off ? mdiLock : mdiLockOpen;
case "moisture": case "moisture":
return is_off ? "hass:water-off" : "hass:water"; return is_off ? mdiWaterOff : mdiWater;
case "motion": case "motion":
return is_off ? "hass:walk" : "hass:run"; return is_off ? mdiWalk : mdiRun;
case "occupancy": case "occupancy":
return is_off ? "hass:home-outline" : "hass:home"; return is_off ? mdiHomeOutline : mdiHome;
case "opening": case "opening":
return is_off ? "hass:square" : "hass:square-outline"; return is_off ? mdiSquare : mdiSquareOutline;
case "plug": case "plug":
return is_off ? "hass:power-plug-off" : "hass:power-plug"; return is_off ? mdiPowerPlugOff : mdiPowerPlug;
case "presence": case "presence":
return is_off ? "hass:home-outline" : "hass:home"; return is_off ? mdiHomeOutline : mdiHome;
case "running":
return is_off ? mdiStop : mdiPlay;
case "sound": case "sound":
return is_off ? "hass:music-note-off" : "hass:music-note"; return is_off ? mdiMusicNoteOff : mdiMusicNote;
case "update": case "update":
return is_off ? "mdi:package" : "mdi:package-up"; return is_off ? mdiPackage : mdiPackageUp;
case "vibration": case "vibration":
return is_off ? "hass:crop-portrait" : "hass:vibrate"; return is_off ? mdiCropPortrait : mdiVibrate;
case "window": case "window":
return is_off ? "hass:window-closed" : "hass:window-open"; return is_off ? mdiWindowClosed : mdiWindowOpen;
default: default:
return is_off ? "hass:radiobox-blank" : "hass:checkbox-marked-circle"; return is_off ? mdiRadioboxBlank : mdiCheckboxMarkedCircle;
} }
}; };

View File

@ -39,7 +39,7 @@ export const computeStateDisplay = (
const domain = computeStateDomain(stateObj); const domain = computeStateDomain(stateObj);
if (domain === "input_datetime") { if (domain === "input_datetime") {
if (state) { if (state !== undefined) {
// If trying to display an explicit state, need to parse the explict state to `Date` then format. // If trying to display an explicit state, need to parse the explict state to `Date` then format.
// Attributes aren't available, we have to use `state`. // Attributes aren't available, we have to use `state`.
try { try {
@ -63,7 +63,7 @@ export const computeStateDisplay = (
} }
} }
return state; return state;
} catch { } catch (_e) {
// Formatting methods may throw error if date parsing doesn't go well, // Formatting methods may throw error if date parsing doesn't go well,
// just return the state string in that case. // just return the state string in that case.
return state; return state;
@ -71,7 +71,17 @@ export const computeStateDisplay = (
} else { } else {
// If not trying to display an explicit state, create `Date` object from `stateObj`'s attributes then format. // If not trying to display an explicit state, create `Date` object from `stateObj`'s attributes then format.
let date: Date; let date: Date;
if (!stateObj.attributes.has_time) { if (stateObj.attributes.has_date && stateObj.attributes.has_time) {
date = new Date(
stateObj.attributes.year,
stateObj.attributes.month - 1,
stateObj.attributes.day,
stateObj.attributes.hour,
stateObj.attributes.minute
);
return formatDateTime(date, locale);
}
if (stateObj.attributes.has_date) {
date = new Date( date = new Date(
stateObj.attributes.year, stateObj.attributes.year,
stateObj.attributes.month - 1, stateObj.attributes.month - 1,
@ -79,20 +89,12 @@ export const computeStateDisplay = (
); );
return formatDate(date, locale); return formatDate(date, locale);
} }
if (!stateObj.attributes.has_date) { if (stateObj.attributes.has_time) {
date = new Date(); date = new Date();
date.setHours(stateObj.attributes.hour, stateObj.attributes.minute); date.setHours(stateObj.attributes.hour, stateObj.attributes.minute);
return formatTime(date, locale); return formatTime(date, locale);
} }
return stateObj.state;
date = new Date(
stateObj.attributes.year,
stateObj.attributes.month - 1,
stateObj.attributes.day,
stateObj.attributes.hour,
stateObj.attributes.minute
);
return formatDateTime(date, locale);
} }
} }

View File

@ -1,4 +1,30 @@
/** Return an icon representing a cover state. */ /** Return an icon representing a cover state. */
import {
mdiArrowUpBox,
mdiArrowDownBox,
mdiGarage,
mdiGarageOpen,
mdiGateArrowRight,
mdiGate,
mdiGateOpen,
mdiDoorOpen,
mdiDoorClosed,
mdiCircle,
mdiWindowShutter,
mdiWindowShutterOpen,
mdiBlinds,
mdiBlindsOpen,
mdiWindowClosed,
mdiWindowOpen,
mdiArrowExpandHorizontal,
mdiArrowUp,
mdiArrowCollapseHorizontal,
mdiArrowDown,
mdiCircleSlice8,
mdiArrowSplitVertical,
mdiCurtains,
mdiCurtainsClosed,
} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
export const coverIcon = (state?: string, stateObj?: HassEntity): string => { export const coverIcon = (state?: string, stateObj?: HassEntity): string => {
@ -8,74 +34,84 @@ export const coverIcon = (state?: string, stateObj?: HassEntity): string => {
case "garage": case "garage":
switch (state) { switch (state) {
case "opening": case "opening":
return "hass:arrow-up-box"; return mdiArrowUpBox;
case "closing": case "closing":
return "hass:arrow-down-box"; return mdiArrowDownBox;
case "closed": case "closed":
return "hass:garage"; return mdiGarage;
default: default:
return "hass:garage-open"; return mdiGarageOpen;
} }
case "gate": case "gate":
switch (state) { switch (state) {
case "opening": case "opening":
case "closing": case "closing":
return "hass:gate-arrow-right"; return mdiGateArrowRight;
case "closed": case "closed":
return "hass:gate"; return mdiGate;
default: default:
return "hass:gate-open"; return mdiGateOpen;
} }
case "door": case "door":
return open ? "hass:door-open" : "hass:door-closed"; return open ? mdiDoorOpen : mdiDoorClosed;
case "damper": case "damper":
return open ? "hass:circle" : "hass:circle-slice-8"; return open ? mdiCircle : mdiCircleSlice8;
case "shutter": case "shutter":
switch (state) { switch (state) {
case "opening": case "opening":
return "hass:arrow-up-box"; return mdiArrowUpBox;
case "closing": case "closing":
return "hass:arrow-down-box"; return mdiArrowDownBox;
case "closed": case "closed":
return "hass:window-shutter"; return mdiWindowShutter;
default: default:
return "hass:window-shutter-open"; return mdiWindowShutterOpen;
}
case "curtain":
switch (state) {
case "opening":
return mdiArrowSplitVertical;
case "closing":
return mdiArrowCollapseHorizontal;
case "closed":
return mdiCurtainsClosed;
default:
return mdiCurtains;
} }
case "blind": case "blind":
case "curtain":
case "shade": case "shade":
switch (state) { switch (state) {
case "opening": case "opening":
return "hass:arrow-up-box"; return mdiArrowUpBox;
case "closing": case "closing":
return "hass:arrow-down-box"; return mdiArrowDownBox;
case "closed": case "closed":
return "hass:blinds"; return mdiBlinds;
default: default:
return "hass:blinds-open"; return mdiBlindsOpen;
} }
case "window": case "window":
switch (state) { switch (state) {
case "opening": case "opening":
return "hass:arrow-up-box"; return mdiArrowUpBox;
case "closing": case "closing":
return "hass:arrow-down-box"; return mdiArrowDownBox;
case "closed": case "closed":
return "hass:window-closed"; return mdiWindowClosed;
default: default:
return "hass:window-open"; return mdiWindowOpen;
} }
} }
switch (state) { switch (state) {
case "opening": case "opening":
return "hass:arrow-up-box"; return mdiArrowUpBox;
case "closing": case "closing":
return "hass:arrow-down-box"; return mdiArrowDownBox;
case "closed": case "closed":
return "hass:window-closed"; return mdiWindowClosed;
default: default:
return "hass:window-open"; return mdiWindowOpen;
} }
}; };
@ -84,9 +120,9 @@ export const computeOpenIcon = (stateObj: HassEntity): string => {
case "awning": case "awning":
case "door": case "door":
case "gate": case "gate":
return "hass:arrow-expand-horizontal"; return mdiArrowExpandHorizontal;
default: default:
return "hass:arrow-up"; return mdiArrowUp;
} }
}; };
@ -95,8 +131,8 @@ export const computeCloseIcon = (stateObj: HassEntity): string => {
case "awning": case "awning":
case "door": case "door":
case "gate": case "gate":
return "hass:arrow-collapse-horizontal"; return mdiArrowCollapseHorizontal;
default: default:
return "hass:arrow-down"; return mdiArrowDown;
} }
}; };

View File

@ -1,3 +1,20 @@
import {
mdiAirHumidifierOff,
mdiAirHumidifier,
mdiLockOpen,
mdiLockAlert,
mdiLockClock,
mdiLock,
mdiCastConnected,
mdiCast,
mdiEmoticonDead,
mdiSleep,
mdiTimerSand,
mdiZWave,
mdiClock,
mdiCalendar,
mdiWeatherNight,
} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
/** /**
* Return the icon to be used for a domain. * Return the icon to be used for a domain.
@ -28,36 +45,34 @@ export const domainIcon = (
return coverIcon(compareState, stateObj); return coverIcon(compareState, stateObj);
case "humidifier": case "humidifier":
return state && state === "off" return state && state === "off" ? mdiAirHumidifierOff : mdiAirHumidifier;
? "hass:air-humidifier-off"
: "hass:air-humidifier";
case "lock": case "lock":
switch (compareState) { switch (compareState) {
case "unlocked": case "unlocked":
return "hass:lock-open"; return mdiLockOpen;
case "jammed": case "jammed":
return "hass:lock-alert"; return mdiLockAlert;
case "locking": case "locking":
case "unlocking": case "unlocking":
return "hass:lock-clock"; return mdiLockClock;
default: default:
return "hass:lock"; return mdiLock;
} }
case "media_player": case "media_player":
return compareState === "playing" ? "hass:cast-connected" : "hass:cast"; return compareState === "playing" ? mdiCastConnected : mdiCast;
case "zwave": case "zwave":
switch (compareState) { switch (compareState) {
case "dead": case "dead":
return "hass:emoticon-dead"; return mdiEmoticonDead;
case "sleeping": case "sleeping":
return "hass:sleep"; return mdiSleep;
case "initializing": case "initializing":
return "hass:timer-sand"; return mdiTimerSand;
default: default:
return "hass:z-wave"; return mdiZWave;
} }
case "sensor": { case "sensor": {
@ -71,17 +86,17 @@ export const domainIcon = (
case "input_datetime": case "input_datetime":
if (!stateObj?.attributes.has_date) { if (!stateObj?.attributes.has_date) {
return "hass:clock"; return mdiClock;
} }
if (!stateObj.attributes.has_time) { if (!stateObj.attributes.has_time) {
return "hass:calendar"; return mdiCalendar;
} }
break; break;
case "sun": case "sun":
return stateObj?.state === "above_horizon" return stateObj?.state === "above_horizon"
? FIXED_DOMAIN_ICONS[domain] ? FIXED_DOMAIN_ICONS[domain]
: "hass:weather-night"; : mdiWeatherNight;
} }
if (domain in FIXED_DOMAIN_ICONS) { if (domain in FIXED_DOMAIN_ICONS) {

View File

@ -1,8 +1,9 @@
/** Return an icon representing a sensor state. */ /** Return an icon representing a sensor state. */
import { mdiBattery, mdiThermometer } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { FIXED_DEVICE_CLASS_ICONS, UNIT_C, UNIT_F } from "../const";
import { batteryIcon } from "./battery_icon";
import { SENSOR_DEVICE_CLASS_BATTERY } from "../../data/sensor"; import { SENSOR_DEVICE_CLASS_BATTERY } from "../../data/sensor";
import { FIXED_DEVICE_CLASS_ICONS, UNIT_C, UNIT_F } from "../const";
import { batteryStateIcon } from "./battery_icon";
export const sensorIcon = (stateObj?: HassEntity): string | undefined => { export const sensorIcon = (stateObj?: HassEntity): string | undefined => {
const dclass = stateObj?.attributes.device_class; const dclass = stateObj?.attributes.device_class;
@ -12,12 +13,12 @@ export const sensorIcon = (stateObj?: HassEntity): string | undefined => {
} }
if (dclass === SENSOR_DEVICE_CLASS_BATTERY) { if (dclass === SENSOR_DEVICE_CLASS_BATTERY) {
return stateObj ? batteryIcon(stateObj) : "hass:battery"; return stateObj ? batteryStateIcon(stateObj) : mdiBattery;
} }
const unit = stateObj?.attributes.unit_of_measurement; const unit = stateObj?.attributes.unit_of_measurement;
if (unit === UNIT_C || unit === UNIT_F) { if (unit === UNIT_C || unit === UNIT_F) {
return "hass:thermometer"; return mdiThermometer;
} }
return undefined; return undefined;

View File

@ -4,13 +4,9 @@ import { DEFAULT_DOMAIN_ICON } from "../const";
import { computeDomain } from "./compute_domain"; import { computeDomain } from "./compute_domain";
import { domainIcon } from "./domain_icon"; import { domainIcon } from "./domain_icon";
export const stateIcon = (state?: HassEntity) => { export const stateIconPath = (state?: HassEntity) => {
if (!state) { if (!state) {
return DEFAULT_DOMAIN_ICON; return DEFAULT_DOMAIN_ICON;
} }
if (state.attributes.icon) {
return state.attributes.icon;
}
return domainIcon(computeDomain(state.entity_id), state); return domainIcon(computeDomain(state.entity_id), state);
}; };

View File

@ -0,0 +1,24 @@
/**
* Strips a device name from an entity name.
* @param entityName the entity name
* @param lowerCasedPrefixWithSpaceSuffix the prefix to strip, lower cased with a space suffix
* @returns
*/
export const stripPrefixFromEntityName = (
entityName: string,
lowerCasedPrefixWithSpaceSuffix: string
) => {
if (!entityName.toLowerCase().startsWith(lowerCasedPrefixWithSpaceSuffix)) {
return undefined;
}
const newName = entityName.substring(lowerCasedPrefixWithSpaceSuffix.length);
// If first word already has an upper case letter (e.g. from brand name)
// leave as-is, otherwise capitalize the first word.
return hasUpperCase(newName.substr(0, newName.indexOf(" ")))
? newName
: newName[0].toUpperCase() + newName.slice(1);
};
const hasUpperCase = (str: string): boolean => str.toLowerCase() !== str;

View File

@ -1,4 +1,3 @@
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiClose, mdiMagnify } from "@mdi/js"; import { mdiClose, mdiMagnify } 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";
@ -11,11 +10,15 @@ import {
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import "../../components/ha-icon-button";
import "../../components/ha-svg-icon"; import "../../components/ha-svg-icon";
import { HomeAssistant } from "../../types";
import { fireEvent } from "../dom/fire_event"; import { fireEvent } from "../dom/fire_event";
@customElement("search-input") @customElement("search-input")
class SearchInput extends LitElement { class SearchInput extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public filter?: string; @property() public filter?: string;
@property({ type: Boolean, attribute: "no-label-float" }) @property({ type: Boolean, attribute: "no-label-float" })
@ -50,13 +53,12 @@ class SearchInput extends LitElement {
</slot> </slot>
${this.filter && ${this.filter &&
html` html`
<mwc-icon-button <ha-icon-button
slot="suffix" slot="suffix"
@click=${this._clearSearch} @click=${this._clearSearch}
title="Clear" .label=${this.hass.localize("ui.common.clear")}
> .path=${mdiClose}
<ha-svg-icon .path=${mdiClose}></ha-svg-icon> ></ha-icon-button>
</mwc-icon-button>
`} `}
</paper-input> </paper-input>
`; `;
@ -90,10 +92,10 @@ class SearchInput extends LitElement {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
ha-svg-icon, ha-svg-icon,
mwc-icon-button { ha-icon-button {
color: var(--primary-text-color); color: var(--primary-text-color);
} }
mwc-icon-button { ha-icon-button {
--mdc-icon-button-size: 24px; --mdc-icon-button-size: 24px;
} }
ha-svg-icon.prefix { ha-svg-icon.prefix {

View File

@ -12,8 +12,8 @@ export const slugify = (value: string, delimiter = "_") => {
.replace(p, (c) => b.charAt(a.indexOf(c))) // Replace special characters .replace(p, (c) => b.charAt(a.indexOf(c))) // Replace special characters
.replace(/&/g, `${delimiter}and${delimiter}`) // Replace & with 'and' .replace(/&/g, `${delimiter}and${delimiter}`) // Replace & with 'and'
.replace(/[^\w-]+/g, "") // Remove all non-word characters .replace(/[^\w-]+/g, "") // Remove all non-word characters
.replace(/-/, delimiter) // Replace - with delimiter .replace(/-/g, delimiter) // Replace - with delimiter
.replace(new RegExp(`/${delimiter}${delimiter}+/`, "g"), delimiter) // Replace multiple delimiters with single delimiter .replace(new RegExp(`(${delimiter})\\1+`, "g"), "$1") // Replace multiple delimiters with single delimiter
.replace(new RegExp(`/^${delimiter}+/`), "") // Trim delimiter from start of text .replace(new RegExp(`^${delimiter}+`), "") // Trim delimiter from start of text
.replace(new RegExp(`/-+$/`), ""); // Trim delimiter from end of text .replace(new RegExp(`${delimiter}+$`), ""); // Trim delimiter from end of text
}; };

View File

@ -1,57 +1,57 @@
import { css } from "lit"; import { css } from "lit";
export const iconColorCSS = css` export const iconColorCSS = css`
ha-icon[data-domain="alert"][data-state="on"], ha-state-icon[data-domain="alert"][data-state="on"],
ha-icon[data-domain="automation"][data-state="on"], ha-state-icon[data-domain="automation"][data-state="on"],
ha-icon[data-domain="binary_sensor"][data-state="on"], ha-state-icon[data-domain="binary_sensor"][data-state="on"],
ha-icon[data-domain="calendar"][data-state="on"], ha-state-icon[data-domain="calendar"][data-state="on"],
ha-icon[data-domain="camera"][data-state="streaming"], ha-state-icon[data-domain="camera"][data-state="streaming"],
ha-icon[data-domain="cover"][data-state="open"], ha-state-icon[data-domain="cover"][data-state="open"],
ha-icon[data-domain="fan"][data-state="on"], ha-state-icon[data-domain="fan"][data-state="on"],
ha-icon[data-domain="humidifier"][data-state="on"], ha-state-icon[data-domain="humidifier"][data-state="on"],
ha-icon[data-domain="light"][data-state="on"], ha-state-icon[data-domain="light"][data-state="on"],
ha-icon[data-domain="input_boolean"][data-state="on"], ha-state-icon[data-domain="input_boolean"][data-state="on"],
ha-icon[data-domain="lock"][data-state="unlocked"], ha-state-icon[data-domain="lock"][data-state="unlocked"],
ha-icon[data-domain="media_player"][data-state="on"], ha-state-icon[data-domain="media_player"][data-state="on"],
ha-icon[data-domain="media_player"][data-state="paused"], ha-state-icon[data-domain="media_player"][data-state="paused"],
ha-icon[data-domain="media_player"][data-state="playing"], ha-state-icon[data-domain="media_player"][data-state="playing"],
ha-icon[data-domain="script"][data-state="on"], ha-state-icon[data-domain="script"][data-state="on"],
ha-icon[data-domain="sun"][data-state="above_horizon"], ha-state-icon[data-domain="sun"][data-state="above_horizon"],
ha-icon[data-domain="switch"][data-state="on"], ha-state-icon[data-domain="switch"][data-state="on"],
ha-icon[data-domain="timer"][data-state="active"], ha-state-icon[data-domain="timer"][data-state="active"],
ha-icon[data-domain="vacuum"][data-state="cleaning"], ha-state-icon[data-domain="vacuum"][data-state="cleaning"],
ha-icon[data-domain="group"][data-state="on"], ha-state-icon[data-domain="group"][data-state="on"],
ha-icon[data-domain="group"][data-state="home"], ha-state-icon[data-domain="group"][data-state="home"],
ha-icon[data-domain="group"][data-state="open"], ha-state-icon[data-domain="group"][data-state="open"],
ha-icon[data-domain="group"][data-state="locked"], ha-state-icon[data-domain="group"][data-state="locked"],
ha-icon[data-domain="group"][data-state="problem"] { ha-state-icon[data-domain="group"][data-state="problem"] {
color: var(--paper-item-icon-active-color, #fdd835); color: var(--paper-item-icon-active-color, #fdd835);
} }
ha-icon[data-domain="climate"][data-state="cooling"] { ha-state-icon[data-domain="climate"][data-state="cooling"] {
color: var(--cool-color, var(--state-climate-cool-color)); color: var(--cool-color, var(--state-climate-cool-color));
} }
ha-icon[data-domain="climate"][data-state="heating"] { ha-state-icon[data-domain="climate"][data-state="heating"] {
color: var(--heat-color, var(--state-climate-heat-color)); color: var(--heat-color, var(--state-climate-heat-color));
} }
ha-icon[data-domain="climate"][data-state="drying"] { ha-state-icon[data-domain="climate"][data-state="drying"] {
color: var(--dry-color, var(--state-climate-dry-color)); color: var(--dry-color, var(--state-climate-dry-color));
} }
ha-icon[data-domain="alarm_control_panel"] { ha-state-icon[data-domain="alarm_control_panel"] {
color: var(--alarm-color-armed, var(--label-badge-red)); color: var(--alarm-color-armed, var(--label-badge-red));
} }
ha-icon[data-domain="alarm_control_panel"][data-state="disarmed"] { ha-state-icon[data-domain="alarm_control_panel"][data-state="disarmed"] {
color: var(--alarm-color-disarmed, var(--label-badge-green)); color: var(--alarm-color-disarmed, var(--label-badge-green));
} }
ha-icon[data-domain="alarm_control_panel"][data-state="pending"], ha-state-icon[data-domain="alarm_control_panel"][data-state="pending"],
ha-icon[data-domain="alarm_control_panel"][data-state="arming"] { ha-state-icon[data-domain="alarm_control_panel"][data-state="arming"] {
color: var(--alarm-color-pending, var(--label-badge-yellow)); color: var(--alarm-color-pending, var(--label-badge-yellow));
animation: pulse 1s infinite; animation: pulse 1s infinite;
} }
ha-icon[data-domain="alarm_control_panel"][data-state="triggered"] { ha-state-icon[data-domain="alarm_control_panel"][data-state="triggered"] {
color: var(--alarm-color-triggered, var(--label-badge-red)); color: var(--alarm-color-triggered, var(--label-badge-red));
animation: pulse 1s infinite; animation: pulse 1s infinite;
} }
@ -68,13 +68,13 @@ export const iconColorCSS = css`
} }
} }
ha-icon[data-domain="plant"][data-state="problem"], ha-state-icon[data-domain="plant"][data-state="problem"],
ha-icon[data-domain="zwave"][data-state="dead"] { ha-state-icon[data-domain="zwave"][data-state="dead"] {
color: var(--state-icon-error-color); color: var(--state-icon-error-color);
} }
/* Color the icon if unavailable */ /* Color the icon if unavailable */
ha-icon[data-state="unavailable"] { ha-state-icon[data-state="unavailable"] {
color: var(--state-unavailable-color); color: var(--state-unavailable-color);
} }
`; `;

View File

@ -12,21 +12,19 @@ import { HomeAssistant } from "../../types";
import "./ha-chart-base"; import "./ha-chart-base";
import type { TimeLineData } from "./timeline-chart/const"; import type { TimeLineData } from "./timeline-chart/const";
/** Binary sensor device classes for which the static colors for on/off need to be inverted. /** Binary sensor device classes for which the static colors for on/off are NOT inverted.
* List the ones were "off" = good or normal state = should be rendered "green". * List the ones were "on" = good or normal state => should be rendered "green".
* Note: It is now a "not inverted" list (compared to the past) since we now have more inverted ones.
*/ */
const BINARY_SENSOR_DEVICE_CLASS_COLOR_INVERTED = new Set([ const BINARY_SENSOR_DEVICE_CLASS_COLOR_NOT_INVERTED = new Set([
"battery", "battery_charging",
"door", "connectivity",
"garage_door", "light",
"gas", "moving",
"lock", "plug",
"motion", "power",
"opening", "presence",
"problem", "update",
"safety",
"smoke",
"window",
]); ]);
const STATIC_STATE_COLORS = new Set([ const STATIC_STATE_COLORS = new Set([
@ -47,7 +45,7 @@ const invertOnOff = (entityState?: HassEntity) =>
entityState && entityState &&
computeDomain(entityState.entity_id) === "binary_sensor" && computeDomain(entityState.entity_id) === "binary_sensor" &&
"device_class" in entityState.attributes && "device_class" in entityState.attributes &&
BINARY_SENSOR_DEVICE_CLASS_COLOR_INVERTED.has( !BINARY_SENSOR_DEVICE_CLASS_COLOR_NOT_INVERTED.has(
entityState.attributes.device_class! entityState.attributes.device_class!
); );

View File

@ -30,6 +30,7 @@ import "../ha-checkbox";
import type { HaCheckbox } from "../ha-checkbox"; import type { HaCheckbox } from "../ha-checkbox";
import "../ha-svg-icon"; import "../ha-svg-icon";
import { filterData, sortData } from "./sort-filter"; import { filterData, sortData } from "./sort-filter";
import { HomeAssistant } from "../../types";
declare global { declare global {
// for fire event // for fire event
@ -69,7 +70,7 @@ export interface DataTableSortColumnData {
export interface DataTableColumnData extends DataTableSortColumnData { export interface DataTableColumnData extends DataTableSortColumnData {
title: TemplateResult | string; title: TemplateResult | string;
type?: "numeric" | "icon" | "icon-button"; type?: "numeric" | "icon" | "icon-button" | "overflow-menu";
template?: <T>(data: any, row: T) => TemplateResult | string; template?: <T>(data: any, row: T) => TemplateResult | string;
width?: string; width?: string;
maxWidth?: string; maxWidth?: string;
@ -93,6 +94,8 @@ export interface SortableColumnContainer {
@customElement("ha-data-table") @customElement("ha-data-table")
export class HaDataTable extends LitElement { export class HaDataTable extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Object }) public columns: DataTableColumnContainer = {}; @property({ type: Object }) public columns: DataTableColumnContainer = {};
@property({ type: Array }) public data: DataTableRowData[] = []; @property({ type: Array }) public data: DataTableRowData[] = [];
@ -232,6 +235,7 @@ export class HaDataTable extends LitElement {
? html` ? html`
<div class="table-header"> <div class="table-header">
<search-input <search-input
.hass=${this.hass}
@value-changed=${this._handleSearchChange} @value-changed=${this._handleSearchChange}
.label=${this.searchLabel} .label=${this.searchLabel}
.noLabelFloat=${this.noLabelFloat} .noLabelFloat=${this.noLabelFloat}
@ -277,15 +281,13 @@ export class HaDataTable extends LitElement {
} }
const sorted = key === this._sortColumn; const sorted = key === this._sortColumn;
const classes = { const classes = {
"mdc-data-table__header-cell--numeric": Boolean( "mdc-data-table__header-cell--numeric":
column.type === "numeric" column.type === "numeric",
), "mdc-data-table__header-cell--icon": column.type === "icon",
"mdc-data-table__header-cell--icon": Boolean( "mdc-data-table__header-cell--icon-button":
column.type === "icon" column.type === "icon-button",
), "mdc-data-table__header-cell--overflow-menu":
"mdc-data-table__header-cell--icon-button": Boolean( column.type === "overflow-menu",
column.type === "icon-button"
),
sortable: Boolean(column.sortable), sortable: Boolean(column.sortable),
"not-sorted": Boolean(column.sortable && !sorted), "not-sorted": Boolean(column.sortable && !sorted),
grows: Boolean(column.grows), grows: Boolean(column.grows),
@ -401,14 +403,14 @@ export class HaDataTable extends LitElement {
<div <div
role="cell" role="cell"
class="mdc-data-table__cell ${classMap({ class="mdc-data-table__cell ${classMap({
"mdc-data-table__cell--numeric": Boolean( "mdc-data-table__cell--numeric":
column.type === "numeric" column.type === "numeric",
), "mdc-data-table__cell--icon":
"mdc-data-table__cell--icon": Boolean( column.type === "icon",
column.type === "icon"
),
"mdc-data-table__cell--icon-button": "mdc-data-table__cell--icon-button":
Boolean(column.type === "icon-button"), column.type === "icon-button",
"mdc-data-table__cell--overflow-menu":
column.type === "overflow-menu",
grows: Boolean(column.grows), grows: Boolean(column.grows),
forceLTR: Boolean(column.forceLTR), forceLTR: Boolean(column.forceLTR),
})}" })}"
@ -743,10 +745,16 @@ export class HaDataTable extends LitElement {
text-align: right; text-align: right;
} }
.mdc-data-table__cell--icon:first-child ha-icon { .mdc-data-table__cell--icon:first-child ha-icon,
.mdc-data-table__cell--icon:first-child ha-state-icon,
.mdc-data-table__cell--icon:first-child ha-svg-icon {
margin-left: 8px; margin-left: 8px;
} }
:host([dir="rtl"]) .mdc-data-table__cell--icon:first-child ha-icon { :host([dir="rtl"]) .mdc-data-table__cell--icon:first-child ha-icon,
:host([dir="rtl"])
.mdc-data-table__cell--icon:first-child
ha-state-icon,
:host([dir="rtl"]) .mdc-data-table__cell--icon:first-child ha-svg-icon {
margin-left: auto; margin-left: auto;
margin-right: 8px; margin-right: 8px;
} }
@ -759,40 +767,65 @@ export class HaDataTable extends LitElement {
margin-left: -8px; margin-left: -8px;
} }
.mdc-data-table__cell--overflow-menu,
.mdc-data-table__header-cell--overflow-menu,
.mdc-data-table__header-cell--icon-button, .mdc-data-table__header-cell--icon-button,
.mdc-data-table__cell--icon-button { .mdc-data-table__cell--icon-button {
width: 56px;
padding: 8px; padding: 8px;
} }
.mdc-data-table__header-cell--icon-button,
.mdc-data-table__cell--icon-button {
width: 56px;
}
.mdc-data-table__cell--overflow-menu,
.mdc-data-table__cell--icon-button { .mdc-data-table__cell--icon-button {
color: var(--secondary-text-color); color: var(--secondary-text-color);
text-overflow: clip; text-overflow: clip;
} }
.mdc-data-table__header-cell--icon-button:first-child, .mdc-data-table__header-cell--icon-button:first-child,
.mdc-data-table__cell--icon-button:first-child { .mdc-data-table__cell--icon-button:first-child,
width: 64px;
padding-left: 16px;
}
:host([dir="rtl"])
.mdc-data-table__header-cell--icon-button:first-child,
:host([dir="rtl"]) .mdc-data-table__cell--icon-button:first-child {
padding-left: auto;
padding-right: 16px;
}
.mdc-data-table__header-cell--icon-button:last-child, .mdc-data-table__header-cell--icon-button:last-child,
.mdc-data-table__cell--icon-button:last-child { .mdc-data-table__cell--icon-button:last-child {
width: 64px; width: 64px;
padding-right: 16px;
}
:host([dir="rtl"]) .mdc-data-table__header-cell--icon-button:last-child,
:host([dir="rtl"]) .mdc-data-table__cell--icon-button:last-child {
padding-right: auto;
padding-left: 16px;
} }
.mdc-data-table__cell--overflow-menu:first-child,
.mdc-data-table__header-cell--overflow-menu:first-child,
.mdc-data-table__header-cell--icon-button:first-child,
.mdc-data-table__cell--icon-button:first-child {
padding-left: 16px;
}
:host([dir="rtl"])
.mdc-data-table__header-cell--overflow-menu:first-child,
:host([dir="rtl"]) .mdc-data-table__cell--overflow-menu:first-child,
:host([dir="rtl"])
.mdc-data-table__header-cell--overflow-menu:first-child,
:host([dir="rtl"]) .mdc-data-table__cell--overflow-menu:first-child {
padding-left: 8px;
padding-right: 16px;
}
.mdc-data-table__cell--overflow-menu:last-child,
.mdc-data-table__header-cell--overflow-menu:last-child,
.mdc-data-table__header-cell--icon-button:last-child,
.mdc-data-table__cell--icon-button:last-child {
padding-right: 16px;
}
:host([dir="rtl"])
.mdc-data-table__header-cell--overflow-menu:last-child,
:host([dir="rtl"]) .mdc-data-table__cell--overflow-menu:last-child,
:host([dir="rtl"]) .mdc-data-table__header-cell--icon-button:last-child,
:host([dir="rtl"]) .mdc-data-table__cell--icon-button:last-child {
padding-right: 8px;
padding-left: 16px;
}
.mdc-data-table__cell--overflow-menu,
.mdc-data-table__header-cell--overflow-menu {
overflow: initial;
}
.mdc-data-table__cell--icon-button a { .mdc-data-table__cell--icon-button a {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }

View File

@ -1,5 +1,4 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
@ -37,6 +36,7 @@ import {
import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { PolymerChangedEvent } from "../../polymer-types"; import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-icon-button";
import "../ha-svg-icon"; import "../ha-svg-icon";
import "./ha-devices-picker"; import "./ha-devices-picker";
@ -324,29 +324,25 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
> >
<div class="suffix" slot="suffix"> <div class="suffix" slot="suffix">
${this.value ${this.value
? html`<mwc-icon-button ? html`<ha-icon-button
class="clear-button" class="clear-button"
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.components.device-picker.clear" "ui.components.device-picker.clear"
)} )}
.path=${mdiClose}
@click=${this._clearValue} @click=${this._clearValue}
no-ripple no-ripple
> ></ha-icon-button> `
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button> `
: ""} : ""}
${areas.length > 0 ${areas.length > 0
? html` ? html`
<mwc-icon-button <ha-icon-button
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.components.device-picker.show_devices" "ui.components.device-picker.show_devices"
)} )}
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
class="toggle-button" class="toggle-button"
> ></ha-icon-button>
<ha-svg-icon
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
></ha-svg-icon>
</mwc-icon-button>
` `
: ""} : ""}
</div> </div>
@ -408,7 +404,7 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
.suffix { .suffix {
display: flex; display: flex;
} }
mwc-icon-button { ha-icon-button {
--mdc-icon-button-size: 24px; --mdc-icon-button-size: 24px;
padding: 0px 2px; padding: 0px 2px;
color: var(--secondary-text-color); color: var(--secondary-text-color);

View File

@ -338,7 +338,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
paper-input > mwc-icon-button { paper-input > ha-icon-button {
--mdc-icon-button-size: 24px; --mdc-icon-button-size: 24px;
padding: 2px; padding: 2px;
color: var(--secondary-text-color); color: var(--secondary-text-color);

View File

@ -1,89 +0,0 @@
/**
@license
Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/*
Fixes issue with not using shadow dom properly in iron-overlay-behavior/icon-focusables-helper.js
*/
import { IronFocusablesHelper } from "@polymer/iron-overlay-behavior/iron-focusables-helper";
import { dom } from "@polymer/polymer/lib/legacy/polymer.dom";
export const HaIronFocusablesHelper = {
/**
* Returns a sorted array of tabbable nodes, including the root node.
* It searches the tabbable nodes in the light and shadow dom of the chidren,
* sorting the result by tabindex.
* @param {!Node} node
* @return {!Array<!HTMLElement>}
*/
getTabbableNodes: function (node) {
const result = [];
// If there is at least one element with tabindex > 0, we need to sort
// the final array by tabindex.
const needsSortByTabIndex = this._collectTabbableNodes(node, result);
if (needsSortByTabIndex) {
return IronFocusablesHelper._sortByTabIndex(result);
}
return result;
},
/**
* Searches for nodes that are tabbable and adds them to the `result` array.
* Returns if the `result` array needs to be sorted by tabindex.
* @param {!Node} node The starting point for the search; added to `result`
* if tabbable.
* @param {!Array<!HTMLElement>} result
* @return {boolean}
* @private
*/
_collectTabbableNodes: function (node, result) {
// If not an element or not visible, no need to explore children.
if (
node.nodeType !== Node.ELEMENT_NODE ||
!IronFocusablesHelper._isVisible(node)
) {
return false;
}
const element = /** @type {!HTMLElement} */ (node);
const tabIndex = IronFocusablesHelper._normalizedTabIndex(element);
let needsSort = tabIndex > 0;
if (tabIndex >= 0) {
result.push(element);
}
// In ShadowDOM v1, tab order is affected by the order of distrubution.
// E.g. getTabbableNodes(#root) in ShadowDOM v1 should return [#A, #B];
// in ShadowDOM v0 tab order is not affected by the distrubution order,
// in fact getTabbableNodes(#root) returns [#B, #A].
// <div id="root">
// <!-- shadow -->
// <slot name="a">
// <slot name="b">
// <!-- /shadow -->
// <input id="A" slot="a">
// <input id="B" slot="b" tabindex="1">
// </div>
// TODO(valdrin) support ShadowDOM v1 when upgrading to Polymer v2.0.
let children;
if (element.localName === "content" || element.localName === "slot") {
children = dom(element).getDistributedNodes();
} else {
// /////////////////////////
// Use shadow root if possible, will check for distributed nodes.
// THIS IS THE CHANGED LINE
children = dom(element.shadowRoot || element.root || element).children;
// /////////////////////////
}
for (let i = 0; i < children.length; i++) {
// Ensure method is always invoked to collect tabbable children.
needsSort = this._collectTabbableNodes(children[i], result) || needsSort;
}
return needsSort;
},
};

View File

@ -1,31 +0,0 @@
import "@polymer/paper-dialog/paper-dialog";
import type { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog";
import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class";
import type { Constructor } from "../../types";
import { HaIronFocusablesHelper } from "./ha-iron-focusables-helper";
const paperDialogClass = customElements.get(
"paper-dialog"
) as Constructor<PaperDialogElement>;
// behavior that will override existing iron-overlay-behavior and call the fixed implementation
const haTabFixBehaviorImpl = {
get _focusableNodes() {
return HaIronFocusablesHelper.getTabbableNodes(this);
},
};
// paper-dialog that uses the haTabFixBehaviorImpl behavior
// export class HaPaperDialog extends paperDialogClass {}
// @ts-ignore
export class HaPaperDialog
extends mixinBehaviors([haTabFixBehaviorImpl], paperDialogClass)
implements PaperDialogElement {}
declare global {
interface HTMLElementTagNameMap {
"ha-paper-dialog": HaPaperDialog;
}
}
// @ts-ignore
customElements.define("ha-paper-dialog", HaPaperDialog);

View File

@ -1,7 +1,7 @@
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { batteryIcon } from "../../common/entity/battery_icon"; import { batteryStateIcon } from "../../common/entity/battery_icon";
import "../ha-icon"; import "../ha-svg-icon";
@customElement("ha-battery-icon") @customElement("ha-battery-icon")
export class HaBatteryIcon extends LitElement { export class HaBatteryIcon extends LitElement {
@ -11,9 +11,18 @@ export class HaBatteryIcon extends LitElement {
protected render() { protected render() {
return html` return html`
<ha-icon <ha-svg-icon
.icon=${batteryIcon(this.batteryStateObj, this.batteryChargingStateObj)} .path=${batteryStateIcon(
></ha-icon> this.batteryStateObj,
this.batteryChargingStateObj
)}
></ha-svg-icon>
`; `;
} }
} }
declare global {
interface HTMLElementTagNameMap {
"ha-battery-icon": HaBatteryIcon;
}
}

View File

@ -1,4 +1,3 @@
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
@ -18,6 +17,7 @@ import { fireEvent } from "../../common/dom/fire_event";
import { PolymerChangedEvent } from "../../polymer-types"; import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { formatAttributeName } from "../../util/hass-attributes-util"; import { formatAttributeName } from "../../util/hass-attributes-util";
import "../ha-icon-button";
import "../ha-svg-icon"; import "../ha-svg-icon";
import "./state-badge"; import "./state-badge";
@ -114,31 +114,27 @@ class HaEntityAttributePicker extends LitElement {
<div class="suffix" slot="suffix"> <div class="suffix" slot="suffix">
${this.value ${this.value
? html` ? html`
<mwc-icon-button <ha-icon-button
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.components.entity.entity-picker.clear" "ui.components.entity.entity-picker.clear"
)} )}
.path=${mdiClose}
class="clear-button" class="clear-button"
tabindex="-1" tabindex="-1"
@click=${this._clearValue} @click=${this._clearValue}
no-ripple no-ripple
> ></ha-icon-button>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
` `
: ""} : ""}
<mwc-icon-button <ha-icon-button
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.components.entity.entity-attribute-picker.show_attributes" "ui.components.entity.entity-attribute-picker.show_attributes"
)} )}
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
class="toggle-button" class="toggle-button"
tabindex="-1" tabindex="-1"
> ></ha-icon-button>
<ha-svg-icon
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
></ha-svg-icon>
</mwc-icon-button>
</div> </div>
</paper-input> </paper-input>
</vaadin-combo-box-light> </vaadin-combo-box-light>
@ -178,7 +174,7 @@ class HaEntityAttributePicker extends LitElement {
.suffix { .suffix {
display: flex; display: flex;
} }
mwc-icon-button { ha-icon-button {
--mdc-icon-button-size: 24px; --mdc-icon-button-size: 24px;
padding: 0px 2px; padding: 0px 2px;
color: var(--secondary-text-color); color: var(--secondary-text-color);

View File

@ -1,4 +1,3 @@
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-icon-item";
@ -21,6 +20,7 @@ import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name"; import { computeStateName } from "../../common/entity/compute_state_name";
import { PolymerChangedEvent } from "../../polymer-types"; import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-icon-button";
import "../ha-svg-icon"; import "../ha-svg-icon";
import "./state-badge"; import "./state-badge";
@ -267,31 +267,27 @@ export class HaEntityPicker extends LitElement {
<div class="suffix" slot="suffix"> <div class="suffix" slot="suffix">
${this.value && !this.hideClearIcon ${this.value && !this.hideClearIcon
? html` ? html`
<mwc-icon-button <ha-icon-button
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.components.entity.entity-picker.clear" "ui.components.entity.entity-picker.clear"
)} )}
.path=${mdiClose}
class="clear-button" class="clear-button"
tabindex="-1" tabindex="-1"
@click=${this._clearValue} @click=${this._clearValue}
no-ripple no-ripple
> ></ha-icon-button>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
` `
: ""} : ""}
<mwc-icon-button <ha-icon-button
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.components.entity.entity-picker.show_entities" "ui.components.entity.entity-picker.show_entities"
)} )}
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
class="toggle-button" class="toggle-button"
tabindex="-1" tabindex="-1"
> ></ha-icon-button>
<ha-svg-icon
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
></ha-svg-icon>
</mwc-icon-button>
</div> </div>
</paper-input> </paper-input>
</vaadin-combo-box-light> </vaadin-combo-box-light>
@ -340,7 +336,7 @@ export class HaEntityPicker extends LitElement {
.suffix { .suffix {
display: flex; display: flex;
} }
mwc-icon-button { ha-icon-button {
--mdc-icon-button-size: 24px; --mdc-icon-button-size: 24px;
padding: 0px 2px; padding: 0px 2px;
color: var(--secondary-text-color); color: var(--secondary-text-color);

View File

@ -1,3 +1,4 @@
import { mdiFlash, mdiFlashOff } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { import {
css, css,
@ -41,15 +42,15 @@ export class HaEntityToggle extends LitElement {
if (this.stateObj.attributes.assumed_state) { if (this.stateObj.attributes.assumed_state) {
return html` return html`
<ha-icon-button <ha-icon-button
aria-label=${`Turn ${computeStateName(this.stateObj)} off`} .label=${`Turn ${computeStateName(this.stateObj)} off`}
icon="hass:flash-off" .path=${mdiFlashOff}
.disabled=${this.stateObj.state === UNAVAILABLE} .disabled=${this.stateObj.state === UNAVAILABLE}
@click=${this._turnOff} @click=${this._turnOff}
?state-active=${!this._isOn} ?state-active=${!this._isOn}
></ha-icon-button> ></ha-icon-button>
<ha-icon-button <ha-icon-button
aria-label=${`Turn ${computeStateName(this.stateObj)} on`} .label=${`Turn ${computeStateName(this.stateObj)} on`}
icon="hass:flash" .path=${mdiFlash}
.disabled=${this.stateObj.state === UNAVAILABLE} .disabled=${this.stateObj.state === UNAVAILABLE}
@click=${this._turnOn} @click=${this._turnOn}
?state-active=${this._isOn} ?state-active=${this._isOn}

View File

@ -14,19 +14,18 @@ import secondsToDuration from "../../common/datetime/seconds_to_duration";
import { computeStateDisplay } from "../../common/entity/compute_state_display"; import { computeStateDisplay } from "../../common/entity/compute_state_display";
import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name"; import { computeStateName } from "../../common/entity/compute_state_name";
import { stateIcon } from "../../common/entity/state_icon";
import { formatNumber } from "../../common/number/format_number"; import { formatNumber } from "../../common/number/format_number";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity"; import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { timerTimeRemaining } from "../../data/timer"; import { timerTimeRemaining } from "../../data/timer";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-label-badge"; import "../ha-label-badge";
import "../ha-icon"; import "../ha-state-icon";
@customElement("ha-state-label-badge") @customElement("ha-state-label-badge")
export class HaStateLabelBadge extends LitElement { export class HaStateLabelBadge extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property() public state?: HassEntity; @property({ attribute: false }) public state?: HassEntity;
@property() public name?: string; @property() public name?: string;
@ -69,16 +68,23 @@ export class HaStateLabelBadge extends LitElement {
`; `;
} }
// Rendering priority inside badge:
// 1. Icon directly defined in badge config
// 2. Image directly defined in badge config
// 3. Image taken from entity picture
// 4. Icon determined via entity state
// 5. Value string as fallback
const domain = computeStateDomain(entityState); const domain = computeStateDomain(entityState);
const value = this._computeValue(domain, entityState); const showIcon = this.icon || this._computeShowIcon(domain, entityState);
const icon = this.icon ? this.icon : this._computeIcon(domain, entityState);
const image = this.icon const image = this.icon
? "" ? ""
: this.image : this.image
? this.image ? this.image
: entityState.attributes.entity_picture_local || : entityState.attributes.entity_picture_local ||
entityState.attributes.entity_picture; entityState.attributes.entity_picture;
const value =
!image && !showIcon ? this._computeValue(domain, entityState) : undefined;
return html` return html`
<ha-label-badge <ha-label-badge
@ -95,8 +101,13 @@ export class HaStateLabelBadge extends LitElement {
)} )}
.description=${this.name ?? computeStateName(entityState)} .description=${this.name ?? computeStateName(entityState)}
> >
${!image && icon ? html`<ha-icon .icon=${icon}></ha-icon>` : ""} ${!image && showIcon
${value && (this.icon || !this.image) ? html`<ha-state-icon
.icon=${this.icon}
.state=${entityState}
></ha-state-icon>`
: ""}
${value && !image && !showIcon
? html`<span class=${value && value.length > 4 ? "big" : ""} ? html`<span class=${value && value.length > 4 ? "big" : ""}
>${value}</span >${value}</span
>` >`
@ -144,9 +155,9 @@ export class HaStateLabelBadge extends LitElement {
} }
} }
private _computeIcon(domain: string, entityState: HassEntity) { private _computeShowIcon(domain: string, entityState: HassEntity): boolean {
if (entityState.state === UNAVAILABLE) { if (entityState.state === UNAVAILABLE) {
return null; return false;
} }
switch (domain) { switch (domain) {
case "alarm_control_panel": case "alarm_control_panel":
@ -156,17 +167,13 @@ export class HaStateLabelBadge extends LitElement {
case "person": case "person":
case "scene": case "scene":
case "sun": case "sun":
return stateIcon(entityState); return true;
case "timer": case "timer":
return entityState.state === "active" return true;
? "hass:timer-outline"
: "hass:timer-off-outline";
case "sensor": case "sensor":
return entityState.attributes.device_class === "moon__phase" return entityState.attributes.device_class === "moon__phase";
? stateIcon(entityState)
: null;
default: default:
return null; return false;
} }
} }

View File

@ -1,4 +1,3 @@
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiCheck } from "@mdi/js"; import { mdiCheck } from "@mdi/js";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-icon-item";
@ -289,7 +288,7 @@ export class HaStatisticPicker extends LitElement {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
paper-input > mwc-icon-button { paper-input > ha-icon-button {
--mdc-icon-button-size: 24px; --mdc-icon-button-size: 24px;
padding: 2px; padding: 2px;
color: var(--secondary-text-color); color: var(--secondary-text-color);

View File

@ -1,3 +1,4 @@
import { mdiAlert } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import { import {
css, css,
@ -12,10 +13,9 @@ import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { computeActiveState } from "../../common/entity/compute_active_state"; import { computeActiveState } from "../../common/entity/compute_active_state";
import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { stateIcon } from "../../common/entity/state_icon";
import { iconColorCSS } from "../../common/style/icon_color_css"; import { iconColorCSS } from "../../common/style/icon_color_css";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import "../ha-icon"; import "../ha-state-icon";
export class StateBadge extends LitElement { export class StateBadge extends LitElement {
public hass?: HomeAssistant; public hass?: HomeAssistant;
@ -39,7 +39,7 @@ export class StateBadge extends LitElement {
// We either need a `stateObj` or one override // We either need a `stateObj` or one override
if (!stateObj && !this.overrideIcon && !this.overrideImage) { if (!stateObj && !this.overrideIcon && !this.overrideImage) {
return html`<div class="missing"> return html`<div class="missing">
<ha-icon icon="hass:alert"></ha-icon> <ha-svg-icon .path=${mdiAlert}></ha-svg-icon>
</div>`; </div>`;
} }
@ -49,18 +49,17 @@ export class StateBadge extends LitElement {
const domain = stateObj ? computeStateDomain(stateObj) : undefined; const domain = stateObj ? computeStateDomain(stateObj) : undefined;
return html` return html`<ha-state-icon
<ha-icon style=${styleMap(this._iconStyle)}
style=${styleMap(this._iconStyle)} data-domain=${ifDefined(
data-domain=${ifDefined( this.stateColor || (domain === "light" && this.stateColor !== false)
this.stateColor || (domain === "light" && this.stateColor !== false) ? domain
? domain : undefined
: undefined )}
)} data-state=${stateObj ? computeActiveState(stateObj) : ""}
data-state=${stateObj ? computeActiveState(stateObj) : ""} .icon=${this.overrideIcon}
.icon=${this.overrideIcon || (stateObj ? stateIcon(stateObj) : "")} .state=${stateObj}
></ha-icon> ></ha-state-icon>`;
`;
} }
public willUpdate(changedProps: PropertyValues) { public willUpdate(changedProps: PropertyValues) {
@ -154,7 +153,7 @@ export class StateBadge extends LitElement {
:host([icon]:focus) { :host([icon]:focus) {
background: var(--divider-color); background: var(--divider-color);
} }
ha-icon { ha-state-icon {
transition: color 0.3s ease-in-out, filter 0.3s ease-in-out; transition: color 0.3s ease-in-out, filter 0.3s ease-in-out;
} }
.missing { .missing {

View File

@ -53,6 +53,7 @@ class StateInfo extends LitElement {
<ha-relative-time <ha-relative-time
.hass=${this.hass} .hass=${this.hass}
.datetime=${this.stateObj.last_changed} .datetime=${this.stateObj.last_changed}
capitalize
></ha-relative-time> ></ha-relative-time>
</div> </div>
<div class="row"> <div class="row">
@ -64,6 +65,7 @@ class StateInfo extends LitElement {
<ha-relative-time <ha-relative-time
.hass=${this.hass} .hass=${this.hass}
.datetime=${this.stateObj.last_updated} .datetime=${this.stateObj.last_updated}
capitalize
></ha-relative-time> ></ha-relative-time>
</div> </div>
</div> </div>

View File

@ -1,5 +1,4 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@material/mwc-icon-button/mwc-icon-button";
import { import {
mdiAlertCircleOutline, mdiAlertCircleOutline,
mdiAlertOutline, mdiAlertOutline,
@ -11,6 +10,7 @@ import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import "./ha-icon-button";
import "./ha-svg-icon"; import "./ha-svg-icon";
const ALERT_ICONS = { const ALERT_ICONS = {
@ -66,12 +66,11 @@ class HaAlert extends LitElement {
.label=${this.actionText} .label=${this.actionText}
></mwc-button>` ></mwc-button>`
: this.dismissable : this.dismissable
? html`<mwc-icon-button ? html`<ha-icon-button
@click=${this._dismiss_clicked} @click=${this._dismiss_clicked}
aria-label="Dismiss alert" label="Dismiss alert"
> .path=${mdiClose}
<ha-svg-icon .path=${mdiClose}> </ha-svg-icon> ></ha-icon-button>`
</mwc-icon-button> `
: ""} : ""}
</div> </div>
</div> </div>
@ -140,7 +139,7 @@ class HaAlert extends LitElement {
mwc-button { mwc-button {
--mdc-theme-primary: var(--primary-text-color); --mdc-theme-primary: var(--primary-text-color);
} }
mwc-icon-button { ha-icon-button {
--mdc-icon-button-size: 36px; --mdc-icon-button-size: 36px;
} }
.issue-type.info > .icon { .issue-type.info > .icon {

View File

@ -1,4 +1,3 @@
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; import { mdiCheck, mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
@ -42,6 +41,7 @@ import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { PolymerChangedEvent } from "../polymer-types"; import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-icon-button";
import "./ha-svg-icon"; import "./ha-svg-icon";
const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = ( const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (
@ -362,28 +362,24 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
> >
${this.value ${this.value
? html` ? html`
<mwc-icon-button <ha-icon-button
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.components.area-picker.clear" "ui.components.area-picker.clear"
)} )}
.path=${mdiClose}
slot="suffix" slot="suffix"
class="clear-button" class="clear-button"
@click=${this._clearValue} @click=${this._clearValue}
> ></ha-icon-button>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
` `
: ""} : ""}
<mwc-icon-button <ha-icon-button
.label=${this.hass.localize("ui.components.area-picker.toggle")} .label=${this.hass.localize("ui.components.area-picker.toggle")}
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
slot="suffix" slot="suffix"
class="toggle-button" class="toggle-button"
> ></ha-icon-button>
<ha-svg-icon
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
></ha-svg-icon>
</mwc-icon-button>
</paper-input> </paper-input>
</vaadin-combo-box-light> </vaadin-combo-box-light>
`; `;
@ -457,7 +453,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
paper-input > mwc-icon-button { paper-input > ha-icon-button {
--mdc-icon-button-size: 24px; --mdc-icon-button-size: 24px;
padding: 2px; padding: 2px;
color: var(--secondary-text-color); color: var(--secondary-text-color);

View File

@ -1,4 +1,3 @@
import "@material/mwc-icon-button";
import type { Corner } from "@material/mwc-menu"; import type { Corner } from "@material/mwc-menu";
import "@material/mwc-menu/mwc-menu-surface"; import "@material/mwc-menu/mwc-menu-surface";
import { mdiFilterVariant } from "@mdi/js"; import { mdiFilterVariant } from "@mdi/js";
@ -12,7 +11,7 @@ import type { HomeAssistant } from "../types";
import "./device/ha-device-picker"; import "./device/ha-device-picker";
import "./entity/ha-entity-picker"; import "./entity/ha-entity-picker";
import "./ha-area-picker"; import "./ha-area-picker";
import "./ha-svg-icon"; import "./ha-icon-button";
declare global { declare global {
// for fire event // for fire event
@ -55,9 +54,10 @@ export class HaRelatedFilterButtonMenu extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<mwc-icon-button @click=${this._handleClick}> <ha-icon-button
<ha-svg-icon .path=${mdiFilterVariant}></ha-svg-icon> @click=${this._handleClick}
</mwc-icon-button> .path=${mdiFilterVariant}
></ha-icon-button>
<mwc-menu-surface <mwc-menu-surface
.open=${this._open} .open=${this._open}
.anchor=${this} .anchor=${this}

View File

@ -1,12 +1,10 @@
import "@material/mwc-button/mwc-button"; import { Button } from "@material/mwc-button/mwc-button";
import type { Button } from "@material/mwc-button/mwc-button";
import "@material/mwc-icon-button/mwc-icon-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, queryAll } from "lit/decorators"; import { customElement, property, queryAll } from "lit/decorators";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import type { ToggleButton } from "../types"; import type { ToggleButton } from "../types";
import "./ha-svg-icon"; import "./ha-icon-button";
@customElement("ha-button-toggle-group") @customElement("ha-button-toggle-group")
export class HaButtonToggleGroup extends LitElement { export class HaButtonToggleGroup extends LitElement {
@ -25,14 +23,13 @@ export class HaButtonToggleGroup extends LitElement {
<div> <div>
${this.buttons.map((button) => ${this.buttons.map((button) =>
button.iconPath button.iconPath
? html`<mwc-icon-button ? html`<ha-icon-button
.label=${button.label} .label=${button.label}
.path=${button.iconPath}
.value=${button.value} .value=${button.value}
?active=${this.active === button.value} ?active=${this.active === button.value}
@click=${this._handleClick} @click=${this._handleClick}
> ></ha-icon-button>`
<ha-svg-icon .path=${button.iconPath}></ha-svg-icon>
</mwc-icon-button>`
: html`<mwc-button : html`<mwc-button
style=${styleMap({ style=${styleMap({
width: this.fullWidth width: this.fullWidth
@ -77,16 +74,16 @@ export class HaButtonToggleGroup extends LitElement {
--mdc-shape-small: 0; --mdc-shape-small: 0;
--mdc-button-outline-width: 1px 0 1px 1px; --mdc-button-outline-width: 1px 0 1px 1px;
} }
mwc-icon-button { ha-icon-button {
border: 1px solid var(--primary-color); border: 1px solid var(--primary-color);
border-right-width: 0px; border-right-width: 0px;
} }
mwc-icon-button, ha-icon-button,
mwc-button { mwc-button {
position: relative; position: relative;
cursor: pointer; cursor: pointer;
} }
mwc-icon-button::before, ha-icon-button::before,
mwc-button::before { mwc-button::before {
top: 0; top: 0;
left: 0; left: 0;
@ -99,23 +96,23 @@ export class HaButtonToggleGroup extends LitElement {
content: ""; content: "";
transition: opacity 15ms linear, background-color 15ms linear; transition: opacity 15ms linear, background-color 15ms linear;
} }
mwc-icon-button[active]::before, ha-icon-button[active]::before,
mwc-button[active]::before { mwc-button[active]::before {
opacity: var(--mdc-icon-button-ripple-opacity, 0.12); opacity: var(--mdc-icon-button-ripple-opacity, 0.12);
} }
mwc-icon-button:first-child, ha-icon-button:first-child,
mwc-button:first-child { mwc-button:first-child {
--mdc-shape-small: 4px 0 0 4px; --mdc-shape-small: 4px 0 0 4px;
border-radius: 4px 0 0 4px; border-radius: 4px 0 0 4px;
} }
mwc-icon-button:last-child, ha-icon-button:last-child,
mwc-button:last-child { mwc-button:last-child {
border-radius: 0 4px 4px 0; border-radius: 0 4px 4px 0;
border-right-width: 1px; border-right-width: 1px;
--mdc-shape-small: 0 4px 4px 0; --mdc-shape-small: 0 4px 4px 0;
--mdc-button-outline-width: 1px; --mdc-button-outline-width: 1px;
} }
mwc-icon-button:only-child, ha-icon-button:only-child,
mwc-button:only-child { mwc-button:only-child {
--mdc-shape-small: 4px; --mdc-shape-small: 4px;
border-right-width: 1px; border-right-width: 1px;

View File

@ -15,9 +15,12 @@ import {
CAMERA_SUPPORT_STREAM, CAMERA_SUPPORT_STREAM,
computeMJPEGStreamUrl, computeMJPEGStreamUrl,
fetchStreamUrl, fetchStreamUrl,
STREAM_TYPE_HLS,
STREAM_TYPE_WEB_RTC,
} from "../data/camera"; } from "../data/camera";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./ha-hls-player"; import "./ha-hls-player";
import "./ha-web-rtc-player";
@customElement("ha-camera-stream") @customElement("ha-camera-stream")
class HaCameraStream extends LitElement { class HaCameraStream extends LitElement {
@ -34,8 +37,8 @@ class HaCameraStream extends LitElement {
@property({ type: Boolean, attribute: "allow-exoplayer" }) @property({ type: Boolean, attribute: "allow-exoplayer" })
public allowExoPlayer = false; public allowExoPlayer = false;
// We keep track if we should force MJPEG with a string // We keep track if we should force MJPEG if there was a failure
// that way it automatically resets if we change entity. // to get the HLS stream url. This is reset if we change entities.
@state() private _forceMJPEG?: string; @state() private _forceMJPEG?: string;
@state() private _url?: string; @state() private _url?: string;
@ -48,7 +51,8 @@ class HaCameraStream extends LitElement {
!this._shouldRenderMJPEG && !this._shouldRenderMJPEG &&
this.stateObj && this.stateObj &&
(changedProps.get("stateObj") as CameraEntity | undefined)?.entity_id !== (changedProps.get("stateObj") as CameraEntity | undefined)?.entity_id !==
this.stateObj.entity_id this.stateObj.entity_id &&
this.stateObj!.attributes.frontend_stream_type === STREAM_TYPE_HLS
) { ) {
this._forceMJPEG = undefined; this._forceMJPEG = undefined;
this._url = undefined; this._url = undefined;
@ -70,43 +74,64 @@ class HaCameraStream extends LitElement {
if (!this.stateObj) { if (!this.stateObj) {
return html``; return html``;
} }
if (__DEMO__ || this._shouldRenderMJPEG) {
return html` return html` <img
${__DEMO__ || this._shouldRenderMJPEG .src=${__DEMO__
? html` ? this.stateObj.attributes.entity_picture!
<img : this._connected
.src=${__DEMO__ ? computeMJPEGStreamUrl(this.stateObj)
? this.stateObj!.attributes.entity_picture! : ""}
: this._connected .alt=${`Preview of the ${computeStateName(this.stateObj)} camera.`}
? computeMJPEGStreamUrl(this.stateObj) />`;
: ""} }
.alt=${`Preview of the ${computeStateName( if (this.stateObj.attributes.frontend_stream_type === STREAM_TYPE_HLS) {
this.stateObj return this._url
)} camera.`} ? html` <ha-hls-player
/> autoplay
` playsinline
: this._url .allowExoPlayer=${this.allowExoPlayer}
? html` .muted=${this.muted}
<ha-hls-player .controls=${this.controls}
autoplay .hass=${this.hass}
playsinline .url=${this._url}
.allowExoPlayer=${this.allowExoPlayer} ></ha-hls-player>`
.muted=${this.muted} : html``;
.controls=${this.controls} }
.hass=${this.hass} if (this.stateObj.attributes.frontend_stream_type === STREAM_TYPE_WEB_RTC) {
.url=${this._url} return html` <ha-web-rtc-player
></ha-hls-player> autoplay
` playsinline
: ""} .muted=${this.muted}
`; .controls=${this.controls}
.hass=${this.hass}
.entityid=${this.stateObj.entity_id}
></ha-web-rtc-player>`;
}
return html``;
} }
private get _shouldRenderMJPEG() { private get _shouldRenderMJPEG() {
return ( if (this._forceMJPEG === this.stateObj!.entity_id) {
this._forceMJPEG === this.stateObj!.entity_id || // Fallback when unable to fetch stream url
return true;
}
if (
!isComponentLoaded(this.hass!, "stream") || !isComponentLoaded(this.hass!, "stream") ||
!supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM) !supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)
); ) {
// Steaming is not supported by the camera so fallback to MJPEG stream
return true;
}
if (
this.stateObj!.attributes.frontend_stream_type === STREAM_TYPE_WEB_RTC &&
typeof RTCPeerConnection === "undefined"
) {
// Stream requires WebRTC but browser does not support, so fallback to
// MJPEG stream.
return true;
}
// Render stream
return false;
} }
private async _getStreamUrl(): Promise<void> { private async _getStreamUrl(): Promise<void> {

View File

@ -3,6 +3,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */ /* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import { EventsMixin } from "../mixins/events-mixin"; import { EventsMixin } from "../mixins/events-mixin";
import "./ha-icon";
import "./ha-icon-button"; import "./ha-icon-button";
/* /*
@ -40,16 +41,14 @@ class HaClimateControl extends EventsMixin(PolymerElement) {
<div id="target_temperature">[[value]] [[units]]</div> <div id="target_temperature">[[value]] [[units]]</div>
<div class="control-buttons"> <div class="control-buttons">
<div> <div>
<ha-icon-button <ha-icon-button on-click="incrementValue">
icon="hass:chevron-up" <ha-icon icon="hass:chevron-up"></ha-icon>
on-click="incrementValue" </ha-icon-button>
></ha-icon-button>
</div> </div>
<div> <div>
<ha-icon-button <ha-icon-button on-click="decrementValue">
icon="hass:chevron-down" <ha-icon icon="hass:chevron-down"></ha-icon>
on-click="decrementValue" </ha-icon-button>
></ha-icon-button>
</div> </div>
</div> </div>
`; `;

View File

@ -1,4 +1,3 @@
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
@ -11,7 +10,7 @@ import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { PolymerChangedEvent } from "../polymer-types"; import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./ha-svg-icon"; import "./ha-icon-button";
// eslint-disable-next-line lit/prefer-static-styles // eslint-disable-next-line lit/prefer-static-styles
const defaultRowRenderer: ComboBoxLitRenderer<string> = (item) => html`<style> const defaultRowRenderer: ComboBoxLitRenderer<string> = (item) => html`<style>
@ -94,26 +93,22 @@ export class HaComboBox extends LitElement {
> >
${this.value ${this.value
? html` ? html`
<mwc-icon-button <ha-icon-button
.label=${this.hass.localize("ui.components.combo-box.clear")} .label=${this.hass.localize("ui.components.combo-box.clear")}
.path=${mdiClose}
slot="suffix" slot="suffix"
class="clear-button" class="clear-button"
@click=${this._clearValue} @click=${this._clearValue}
> ></ha-icon-button>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
` `
: ""} : ""}
<mwc-icon-button <ha-icon-button
.label=${this.hass.localize("ui.components.combo-box.show")} .label=${this.hass.localize("ui.components.combo-box.show")}
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
slot="suffix" slot="suffix"
class="toggle-button" class="toggle-button"
> ></ha-icon-button>
<ha-svg-icon
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
></ha-svg-icon>
</mwc-icon-button>
</paper-input> </paper-input>
</vaadin-combo-box-light> </vaadin-combo-box-light>
`; `;
@ -146,7 +141,7 @@ export class HaComboBox extends LitElement {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
paper-input > mwc-icon-button { paper-input > ha-icon-button {
--mdc-icon-button-size: 24px; --mdc-icon-button-size: 24px;
padding: 2px; padding: 2px;
color: var(--secondary-text-color); color: var(--secondary-text-color);

View File

@ -1,3 +1,4 @@
import { mdiStop } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import { import {
css, css,
@ -45,10 +46,11 @@ class HaCoverControls extends LitElement {
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.dialogs.more_info_control.open_cover" "ui.dialogs.more_info_control.open_cover"
)} )}
.icon=${computeOpenIcon(this.stateObj)}
@click=${this._onOpenTap} @click=${this._onOpenTap}
.disabled=${this._computeOpenDisabled()} .disabled=${this._computeOpenDisabled()}
></ha-icon-button> .path=${computeOpenIcon(this.stateObj)}
>
</ha-icon-button>
<ha-icon-button <ha-icon-button
class=${classMap({ class=${classMap({
hidden: !this._entityObj.supportsStop, hidden: !this._entityObj.supportsStop,
@ -56,7 +58,7 @@ class HaCoverControls extends LitElement {
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.dialogs.more_info_control.stop_cover" "ui.dialogs.more_info_control.stop_cover"
)} )}
icon="hass:stop" .path=${mdiStop}
@click=${this._onStopTap} @click=${this._onStopTap}
.disabled=${this.stateObj.state === UNAVAILABLE} .disabled=${this.stateObj.state === UNAVAILABLE}
></ha-icon-button> ></ha-icon-button>
@ -67,10 +69,11 @@ class HaCoverControls extends LitElement {
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.dialogs.more_info_control.close_cover" "ui.dialogs.more_info_control.close_cover"
)} )}
.icon=${computeCloseIcon(this.stateObj)}
@click=${this._onCloseTap} @click=${this._onCloseTap}
.disabled=${this._computeClosedDisabled()} .disabled=${this._computeClosedDisabled()}
></ha-icon-button> .path=${computeCloseIcon(this.stateObj)}
>
</ha-icon-button>
</div> </div>
`; `;
} }

View File

@ -1,3 +1,4 @@
import { mdiArrowBottomLeft, mdiArrowTopRight, mdiStop } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { import {
css, css,
@ -39,10 +40,10 @@ class HaCoverTiltControls extends LitElement {
class=${classMap({ class=${classMap({
invisible: !this._entityObj.supportsOpenTilt, invisible: !this._entityObj.supportsOpenTilt,
})} })}
label=${this.hass.localize( .label=${this.hass.localize(
"ui.dialogs.more_info_control.open_tilt_cover" "ui.dialogs.more_info_control.open_tilt_cover"
)} )}
icon="hass:arrow-top-right" .path=${mdiArrowTopRight}
@click=${this._onOpenTiltTap} @click=${this._onOpenTiltTap}
.disabled=${this._computeOpenDisabled()} .disabled=${this._computeOpenDisabled()}
></ha-icon-button> ></ha-icon-button>
@ -50,8 +51,8 @@ class HaCoverTiltControls extends LitElement {
class=${classMap({ class=${classMap({
invisible: !this._entityObj.supportsStopTilt, invisible: !this._entityObj.supportsStopTilt,
})} })}
label=${this.hass.localize("ui.dialogs.more_info_control.stop_cover")} .label=${this.hass.localize("ui.dialogs.more_info_control.stop_cover")}
icon="hass:stop" .path=${mdiStop}
@click=${this._onStopTiltTap} @click=${this._onStopTiltTap}
.disabled=${this.stateObj.state === UNAVAILABLE} .disabled=${this.stateObj.state === UNAVAILABLE}
></ha-icon-button> ></ha-icon-button>
@ -59,10 +60,10 @@ class HaCoverTiltControls extends LitElement {
class=${classMap({ class=${classMap({
invisible: !this._entityObj.supportsCloseTilt, invisible: !this._entityObj.supportsCloseTilt,
})} })}
label=${this.hass.localize( .label=${this.hass.localize(
"ui.dialogs.more_info_control.close_tilt_cover" "ui.dialogs.more_info_control.close_tilt_cover"
)} )}
icon="hass:arrow-bottom-left" .path=${mdiArrowBottomLeft}
@click=${this._onCloseTiltTap} @click=${this._onCloseTiltTap}
.disabled=${this._computeClosedDisabled()} .disabled=${this._computeClosedDisabled()}
></ha-icon-button>`; ></ha-icon-button>`;

View File

@ -4,29 +4,27 @@ import { css, CSSResultGroup, html, TemplateResult } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import { computeRTLDirection } from "../common/util/compute_rtl"; import { computeRTLDirection } from "../common/util/compute_rtl";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "@material/mwc-icon-button/mwc-icon-button"; import "./ha-icon-button";
import "./ha-svg-icon";
export const createCloseHeading = ( export const createCloseHeading = (
hass: HomeAssistant, hass: HomeAssistant,
title: string | TemplateResult title: string | TemplateResult
) => html` ) => html`
<span class="header_title">${title}</span> <span class="header_title">${title}</span>
<mwc-icon-button <ha-icon-button
aria-label=${hass.localize("ui.dialogs.generic.close")} .label=${hass.localize("ui.dialogs.generic.close")}
.path=${mdiClose}
dialogAction="close" dialogAction="close"
class="header_button" class="header_button"
dir=${computeRTLDirection(hass)} dir=${computeRTLDirection(hass)}
> ></ha-icon-button>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
`; `;
@customElement("ha-dialog") @customElement("ha-dialog")
// @ts-expect-error // @ts-expect-error
export class HaDialog extends Dialog { export class HaDialog extends Dialog {
public scrollToPos(x: number, y: number) { public scrollToPos(x: number, y: number) {
this.contentElement.scrollTo(x, y); this.contentElement?.scrollTo(x, y);
} }
protected renderHeading() { protected renderHeading() {
@ -46,6 +44,12 @@ export class HaDialog extends Dialog {
justify-content: var(--justify-action-buttons, flex-end); justify-content: var(--justify-action-buttons, flex-end);
padding-bottom: max(env(safe-area-inset-bottom), 8px); padding-bottom: max(env(safe-area-inset-bottom), 8px);
} }
.mdc-dialog__actions span:nth-child(1) {
flex: var(--secondary-action-button-flex, unset);
}
.mdc-dialog__actions span:nth-child(2) {
flex: var(--primary-action-button-flex, unset);
}
.mdc-dialog__container { .mdc-dialog__container {
align-items: var(--vertial-align-dialog, center); align-items: var(--vertial-align-dialog, center);
} }

View File

@ -16,12 +16,12 @@ class HaDurationInput extends LitElement {
@property() public label?: string; @property() public label?: string;
@property() public suffix?: string;
@property({ type: Boolean }) public required?: boolean; @property({ type: Boolean }) public required?: boolean;
@property({ type: Boolean }) public enableMillisecond?: boolean; @property({ type: Boolean }) public enableMillisecond?: boolean;
@property({ type: Boolean }) public disabled = false;
@query("paper-time-input", true) private _input?: HTMLElement; @query("paper-time-input", true) private _input?: HTMLElement;
public focus() { public focus() {
@ -36,6 +36,7 @@ class HaDurationInput extends LitElement {
.label=${this.label} .label=${this.label}
.required=${this.required} .required=${this.required}
.autoValidate=${this.required} .autoValidate=${this.required}
.disabled=${this.disabled}
error-message="Required" error-message="Required"
enable-second enable-second
.enableMillisecond=${this.enableMillisecond} .enableMillisecond=${this.enableMillisecond}

View File

@ -1,13 +1,13 @@
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiClose } from "@mdi/js"; import { mdiClose } from "@mdi/js";
import "@polymer/iron-input/iron-input"; import "@polymer/iron-input/iron-input";
import "@polymer/paper-input/paper-input-container"; import "@polymer/paper-input/paper-input-container";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state, query } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { HomeAssistant } from "../types";
import "./ha-circular-progress"; import "./ha-circular-progress";
import "./ha-svg-icon"; import "./ha-icon-button";
declare global { declare global {
interface HASSDomEvents { interface HASSDomEvents {
@ -17,6 +17,8 @@ declare global {
@customElement("ha-file-upload") @customElement("ha-file-upload")
export class HaFileUpload extends LitElement { export class HaFileUpload extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public accept!: string; @property() public accept!: string;
@property() public icon!: string; @property() public icon!: string;
@ -82,15 +84,20 @@ export class HaFileUpload extends LitElement {
${this.value} ${this.value}
</iron-input> </iron-input>
${this.value ${this.value
? html`<mwc-icon-button ? html`
slot="suffix" <ha-icon-button
@click=${this._clearValue} slot="suffix"
> @click=${this._clearValue}
<ha-svg-icon .path=${mdiClose}></ha-svg-icon> .label=${this.hass.localize("ui.common.close")}
</mwc-icon-button>` .path=${mdiClose}
: html`<mwc-icon-button slot="suffix"> ></ha-icon-button>
<ha-svg-icon .path=${this.icon}></ha-svg-icon> `
</mwc-icon-button>`} : html`
<ha-icon-button
slot="suffix"
.path=${this.icon}
></ha-icon-button>
`}
</paper-input-container> </paper-input-container>
</label> </label>
`} `}
@ -154,7 +161,7 @@ export class HaFileUpload extends LitElement {
max-width: 125px; max-width: 125px;
max-height: 125px; max-height: 125px;
} }
mwc-icon-button { ha-icon-button {
--mdc-icon-button-size: 24px; --mdc-icon-button-size: 24px;
--mdc-icon-size: 20px; --mdc-icon-size: 20px;
} }

View File

@ -0,0 +1,37 @@
import { HaFormSchema } from "./types";
export const computeInitialHaFormData = (
schema: HaFormSchema[]
): Record<string, any> => {
const data = {};
schema.forEach((field) => {
if (field.description?.suggested_value) {
data[field.name] = field.description.suggested_value;
} else if ("default" in field) {
data[field.name] = field.default;
} else if (!field.required) {
// Do nothing.
} else if (field.type === "boolean") {
data[field.name] = false;
} else if (field.type === "string") {
data[field.name] = "";
} else if (field.type === "integer") {
data[field.name] = "valueMin" in field ? field.valueMin : 0;
} else if (field.type === "constant") {
data[field.name] = field.value;
} else if (field.type === "float") {
data[field.name] = 0.0;
} else if (field.type === "select") {
if (field.options.length) {
data[field.name] = field.options[0][0];
}
} else if (field.type === "positive_time_period_dict") {
data[field.name] = {
hours: 0,
minutes: 0,
seconds: 0,
};
}
});
return data;
};

View File

@ -1,13 +1,14 @@
import "@polymer/paper-checkbox/paper-checkbox"; import "@material/mwc-formfield";
import type { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox"; import { html, LitElement, TemplateResult } from "lit";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import type { import type {
HaFormBooleanData, HaFormBooleanData,
HaFormBooleanSchema, HaFormBooleanSchema,
HaFormElement, HaFormElement,
} from "./ha-form"; } from "./types";
import type { HaCheckbox } from "../ha-checkbox";
import "../ha-checkbox";
@customElement("ha-form-boolean") @customElement("ha-form-boolean")
export class HaFormBoolean extends LitElement implements HaFormElement { export class HaFormBoolean extends LitElement implements HaFormElement {
@ -17,9 +18,9 @@ export class HaFormBoolean extends LitElement implements HaFormElement {
@property() public label!: string; @property() public label!: string;
@property() public suffix!: string; @property({ type: Boolean }) public disabled = false;
@query("paper-checkbox", true) private _input?: HTMLElement; @query("ha-checkbox", true) private _input?: HTMLElement;
public focus() { public focus() {
if (this._input) { if (this._input) {
@ -29,26 +30,21 @@ export class HaFormBoolean extends LitElement implements HaFormElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<paper-checkbox .checked=${this.data} @change=${this._valueChanged}> <mwc-formfield .label=${this.label}>
${this.label} <ha-checkbox
</paper-checkbox> .checked=${this.data}
.disabled=${this.disabled}
@change=${this._valueChanged}
></ha-checkbox>
</mwc-formfield>
`; `;
} }
private _valueChanged(ev: Event) { private _valueChanged(ev: Event) {
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: (ev.target as PaperCheckboxElement).checked, value: (ev.target as HaCheckbox).checked,
}); });
} }
static get styles(): CSSResultGroup {
return css`
paper-checkbox {
display: block;
padding: 22px 0;
}
`;
}
} }
declare global { declare global {

View File

@ -1,14 +1,6 @@
import { import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { HaFormConstantSchema, HaFormElement } from "./types";
import { HaFormConstantSchema, HaFormElement } from "./ha-form";
@customElement("ha-form-constant") @customElement("ha-form-constant")
export class HaFormConstant extends LitElement implements HaFormElement { export class HaFormConstant extends LitElement implements HaFormElement {
@ -16,13 +8,6 @@ export class HaFormConstant extends LitElement implements HaFormElement {
@property() public label!: string; @property() public label!: string;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
fireEvent(this, "value-changed", {
value: this.schema.value,
});
}
protected render(): TemplateResult { protected render(): TemplateResult {
return html`<span class="label">${this.label}</span>: ${this.schema.value}`; return html`<span class="label">${this.label}</span>: ${this.schema.value}`;
} }

View File

@ -1,9 +1,9 @@
import "@polymer/paper-input/paper-input"; import "@material/mwc-textfield";
import type { PaperInputElement } from "@polymer/paper-input/paper-input"; import type { TextField } from "@material/mwc-textfield";
import { html, LitElement, TemplateResult } from "lit"; import { css, html, LitElement, TemplateResult, PropertyValues } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { HaFormElement, HaFormFloatData, HaFormFloatSchema } from "./ha-form"; import { HaFormElement, HaFormFloatData, HaFormFloatSchema } from "./types";
@customElement("ha-form-float") @customElement("ha-form-float")
export class HaFormFloat extends LitElement implements HaFormElement { export class HaFormFloat extends LitElement implements HaFormElement {
@ -13,9 +13,9 @@ export class HaFormFloat extends LitElement implements HaFormElement {
@property() public label!: string; @property() public label!: string;
@property() public suffix!: string; @property({ type: Boolean }) public disabled = false;
@query("paper-input", true) private _input?: HTMLElement; @query("mwc-textfield") private _input?: HTMLElement;
public focus() { public focus() {
if (this._input) { if (this._input) {
@ -25,33 +25,60 @@ export class HaFormFloat extends LitElement implements HaFormElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<paper-input <mwc-textfield
inputMode="decimal"
.label=${this.label} .label=${this.label}
.value=${this._value} .value=${this.data !== undefined ? this.data : ""}
.disabled=${this.disabled}
.required=${this.schema.required} .required=${this.schema.required}
.autoValidate=${this.schema.required} .autoValidate=${this.schema.required}
@value-changed=${this._valueChanged} .suffix=${this.schema.description?.suffix}
> .validationMessage=${this.schema.required ? "Required" : undefined}
<span suffix slot="suffix">${this.suffix}</span> @input=${this._valueChanged}
</paper-input> ></mwc-textfield>
`; `;
} }
private get _value() { protected updated(changedProps: PropertyValues): void {
return this.data; if (changedProps.has("schema")) {
this.toggleAttribute("own-margin", !!this.schema.required);
}
} }
private _valueChanged(ev: Event) { private _valueChanged(ev: Event) {
const value: number | undefined = (ev.target as PaperInputElement).value const source = ev.target as TextField;
? Number((ev.target as PaperInputElement).value) const rawValue = source.value;
: undefined;
if (this._value === value) { let value: number | undefined;
if (rawValue !== "") {
value = parseFloat(rawValue);
}
// Detect anything changed
if (this.data === value) {
// parseFloat will drop invalid text at the end, in that case update textfield
const newRawValue = value === undefined ? "" : String(value);
if (source.value !== newRawValue) {
source.value = newRawValue;
return;
}
return; return;
} }
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value, value,
}); });
} }
static styles = css`
:host([own-margin]) {
margin-bottom: 5px;
}
mwc-textfield {
display: block;
}
`;
} }
declare global { declare global {

View File

@ -1,16 +1,19 @@
import "@polymer/paper-input/paper-input"; import "@material/mwc-textfield";
import type { PaperInputElement } from "@polymer/paper-input/paper-input"; import type { TextField } from "@material/mwc-textfield";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import "@material/mwc-slider";
import type { Slider } from "@material/mwc-slider";
import {
css,
CSSResultGroup,
html,
LitElement,
TemplateResult,
PropertyValues,
} from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { HaCheckbox } from "../ha-checkbox"; import { HaCheckbox } from "../ha-checkbox";
import "../ha-slider"; import { HaFormElement, HaFormIntegerData, HaFormIntegerSchema } from "./types";
import type { HaSlider } from "../ha-slider";
import {
HaFormElement,
HaFormIntegerData,
HaFormIntegerSchema,
} from "./ha-form";
@customElement("ha-form-integer") @customElement("ha-form-integer")
export class HaFormInteger extends LitElement implements HaFormElement { export class HaFormInteger extends LitElement implements HaFormElement {
@ -20,10 +23,12 @@ export class HaFormInteger extends LitElement implements HaFormElement {
@property() public label?: string; @property() public label?: string;
@property() public suffix?: string; @property({ type: Boolean }) public disabled = false;
@query("paper-input ha-slider") private _input?: HTMLElement; @query("paper-input ha-slider") private _input?: HTMLElement;
private _lastValue?: HaFormIntegerData;
public focus() { public focus() {
if (this._input) { if (this._input) {
this._input.focus(); this._input.focus();
@ -31,66 +36,116 @@ export class HaFormInteger extends LitElement implements HaFormElement {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
return "valueMin" in this.schema && "valueMax" in this.schema if ("valueMin" in this.schema && "valueMax" in this.schema) {
? html` return html`
<div> <div>
${this.label} ${this.label}
<div class="flex"> <div class="flex">
${this.schema.optional && this.schema.default === undefined ${this.schema.optional
? html` ? html`
<ha-checkbox <ha-checkbox
@change=${this._handleCheckboxChange} @change=${this._handleCheckboxChange}
.checked=${this.data !== undefined} .checked=${this.data !== undefined}
></ha-checkbox> .disabled=${this.disabled}
` ></ha-checkbox>
: ""} `
<ha-slider : ""}
pin <mwc-slider
editable discrete
.value=${this._value} .value=${this._value}
.min=${this.schema.valueMin} .min=${this.schema.valueMin}
.max=${this.schema.valueMax} .max=${this.schema.valueMax}
.disabled=${this.data === undefined && .disabled=${this.disabled ||
this.schema.optional && (this.data === undefined && this.schema.optional)}
this.schema.default === undefined} @change=${this._valueChanged}
@value-changed=${this._valueChanged} ></mwc-slider>
></ha-slider>
</div>
</div> </div>
` </div>
: html` `;
<paper-input }
type="number"
.label=${this.label} return html`
.value=${this._value} <mwc-textfield
.required=${this.schema.required} type="number"
.autoValidate=${this.schema.required} inputMode="numeric"
@value-changed=${this._valueChanged} .label=${this.label}
></paper-input> .value=${this.data !== undefined ? this.data : ""}
`; .disabled=${this.disabled}
.required=${this.schema.required}
.autoValidate=${this.schema.required}
.suffix=${this.schema.description?.suffix}
.validationMessage=${this.schema.required ? "Required" : undefined}
@input=${this._valueChanged}
></mwc-textfield>
`;
}
protected updated(changedProps: PropertyValues): void {
if (changedProps.has("schema")) {
this.toggleAttribute(
"own-margin",
!("valueMin" in this.schema && "valueMax" in this.schema) &&
!!this.schema.required
);
}
} }
private get _value() { private get _value() {
return ( if (this.data !== undefined) {
this.data || return this.data;
this.schema.description?.suggested_value || }
this.schema.default ||
0 if (this.schema.optional) {
); return 0;
}
return this.schema.description?.suggested_value || this.schema.default || 0;
} }
private _handleCheckboxChange(ev: Event) { private _handleCheckboxChange(ev: Event) {
const checked = (ev.target as HaCheckbox).checked; const checked = (ev.target as HaCheckbox).checked;
let value: HaFormIntegerData | undefined;
if (checked) {
for (const candidate of [
this._lastValue,
this.schema.description?.suggested_value as HaFormIntegerData,
this.schema.default,
0,
]) {
if (candidate !== undefined) {
value = candidate;
break;
}
}
} else {
// We track last value so user can disable and enable a field without losing
// their value.
this._lastValue = this.data;
}
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: checked ? this._value : undefined, value,
}); });
} }
private _valueChanged(ev: Event) { private _valueChanged(ev: Event) {
const value = Number((ev.target as PaperInputElement | HaSlider).value); const source = ev.target as TextField | Slider;
if (this._value === value) { const rawValue = source.value;
let value: number | undefined;
if (rawValue !== "") {
value = parseInt(String(rawValue));
}
if (this.data === value) {
// parseInt will drop invalid text at the end, in that case update textfield
const newRawValue = value === undefined ? "" : String(value);
if (source.value !== newRawValue) {
source.value = newRawValue;
}
return; return;
} }
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value, value,
}); });
@ -98,12 +153,17 @@ export class HaFormInteger extends LitElement implements HaFormElement {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host([own-margin]) {
margin-bottom: 5px;
}
.flex { .flex {
display: flex; display: flex;
} }
ha-slider { mwc-slider {
width: 100%; flex: 1;
margin-right: 16px; }
mwc-textfield {
display: block;
} }
`; `;
} }

View File

@ -1,19 +1,35 @@
import { mdiMenuDown } from "@mdi/js"; import { mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-checkbox/paper-checkbox"; import "@material/mwc-textfield";
import "@polymer/paper-input/paper-input"; import "@material/mwc-formfield";
import "@polymer/paper-item/paper-icon-item"; import {
import "@polymer/paper-listbox/paper-listbox"; css,
import "@polymer/paper-menu-button/paper-menu-button"; CSSResultGroup,
import "@polymer/paper-ripple/paper-ripple"; html,
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; LitElement,
import { customElement, property, state, query } from "lit/decorators"; TemplateResult,
PropertyValues,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import "../ha-button-menu";
import "../ha-svg-icon"; import "../ha-svg-icon";
import { import {
HaFormElement, HaFormElement,
HaFormMultiSelectData, HaFormMultiSelectData,
HaFormMultiSelectSchema, HaFormMultiSelectSchema,
} from "./ha-form"; } from "./types";
import "../ha-checkbox";
import type { HaCheckbox } from "../ha-checkbox";
function optionValue(item: string | string[]): string {
return Array.isArray(item) ? item[0] : item;
}
function optionLabel(item: string | string[]): string {
return Array.isArray(item) ? item[1] || item[0] : item;
}
const SHOW_ALL_ENTRIES_LIMIT = 6;
@customElement("ha-form-multi_select") @customElement("ha-form-multi_select")
export class HaFormMultiSelect extends LitElement implements HaFormElement { export class HaFormMultiSelect extends LitElement implements HaFormElement {
@ -23,11 +39,11 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
@property() public label!: string; @property() public label!: string;
@property() public suffix!: string; @property({ type: Boolean }) public disabled = false;
@state() private _init = false; @state() private _opened = false;
@query("paper-menu-button", true) private _input?: HTMLElement; @query("ha-button-menu") private _input?: HTMLElement;
public focus(): void { public focus(): void {
if (this._input) { if (this._input) {
@ -36,118 +52,144 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
const options = Array.isArray(this.schema.options) const options = Object.entries(this.schema.options);
? this.schema.options
: Object.entries(this.schema.options!);
const data = this.data || []; const data = this.data || [];
const renderedOptions = options.map((item: string | [string, string]) => {
const value = optionValue(item);
return html`
<mwc-formfield .label=${optionLabel(item)}>
<ha-checkbox
.checked=${data.includes(value)}
.value=${value}
.disabled=${this.disabled}
@change=${this._valueChanged}
></ha-checkbox>
</mwc-formfield>
`;
});
// We will just render all checkboxes.
if (options.length < SHOW_ALL_ENTRIES_LIMIT) {
return html`<div>${this.label}${renderedOptions}</div> `;
}
return html` return html`
<paper-menu-button horizontal-align="right" vertical-offset="8"> <ha-button-menu
<div class="dropdown-trigger" slot="dropdown-trigger"> .disabled=${this.disabled}
<paper-ripple></paper-ripple> fixed
<paper-input corner="BOTTOM_START"
id="input" @opened=${this._handleOpen}
type="text" @closed=${this._handleClose}
readonly >
value=${data <mwc-textfield
.map((value) => this.schema.options![value] || value) slot="trigger"
.join(", ")} .label=${this.label}
label=${this.label} .value=${data
input-role="button" .map((value) => this.schema.options![value] || value)
input-aria-haspopup="listbox" .join(", ")}
autocomplete="off" .disabled=${this.disabled}
> tabindex="-1"
<ha-svg-icon ></mwc-textfield>
.path=${mdiMenuDown} <ha-svg-icon
suffix slot="trigger"
slot="suffix" .path=${this._opened ? mdiMenuUp : mdiMenuDown}
></ha-svg-icon> ></ha-svg-icon>
</paper-input> ${renderedOptions}
</div> </ha-button-menu>
<paper-listbox
multi
slot="dropdown-content"
attr-for-selected="item-value"
.selectedValues=${data}
@selected-items-changed=${this._valueChanged}
@iron-select=${this._onSelect}
>
${
// TS doesn't work with union array types https://github.com/microsoft/TypeScript/issues/36390
// @ts-ignore
options.map((item: string | [string, string]) => {
const value = this._optionValue(item);
return html`
<paper-icon-item .itemValue=${value}>
<paper-checkbox
.checked=${data.includes(value)}
slot="item-icon"
></paper-checkbox>
${this._optionLabel(item)}
</paper-icon-item>
`;
})
}
</paper-listbox>
</paper-menu-button>
`; `;
} }
protected firstUpdated() { protected firstUpdated() {
this.updateComplete.then(() => { this.updateComplete.then(() => {
const input = ( const { formElement, mdcRoot } =
this.shadowRoot?.querySelector("paper-input")?.inputElement as any this.shadowRoot?.querySelector("mwc-textfield") || ({} as any);
)?.inputElement; if (formElement) {
if (input) { formElement.style.textOverflow = "ellipsis";
input.style.textOverflow = "ellipsis"; }
if (mdcRoot) {
mdcRoot.style.cursor = "pointer";
} }
}); });
} }
private _optionValue(item: string | string[]): string { protected updated(changedProps: PropertyValues): void {
return Array.isArray(item) ? item[0] : item; if (changedProps.has("schema")) {
} this.toggleAttribute(
"own-margin",
private _optionLabel(item: string | string[]): string { Object.keys(this.schema.options).length >= SHOW_ALL_ENTRIES_LIMIT &&
return Array.isArray(item) ? item[1] || item[0] : item; !!this.schema.required
} );
}
private _onSelect(ev: Event) {
ev.stopPropagation();
} }
private _valueChanged(ev: CustomEvent): void { private _valueChanged(ev: CustomEvent): void {
if (!ev.detail.value || !this._init) { const { value, checked } = ev.target as HaCheckbox;
// ignore first call because that is the init of the component
this._init = true; let newValue: string[];
return;
if (checked) {
if (!this.data) {
newValue = [value];
} else if (this.data.includes(value)) {
return;
} else {
newValue = [...this.data, value];
}
} else {
if (!this.data.includes(value)) {
return;
}
newValue = this.data.filter((v) => v !== value);
} }
fireEvent( fireEvent(this, "value-changed", {
this, value: newValue,
"value-changed", });
{ }
value: ev.detail.value.map((element) => element.itemValue),
}, private _handleOpen(ev: Event): void {
{ bubbles: false } ev.stopPropagation();
); this._opened = true;
this.toggleAttribute("opened", true);
}
private _handleClose(ev: Event): void {
ev.stopPropagation();
this._opened = false;
this.toggleAttribute("opened", false);
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
paper-menu-button { :host([own-margin]) {
margin-bottom: 5px;
}
ha-button-menu {
display: block; display: block;
padding: 0; cursor: pointer;
--paper-item-icon-width: 34px;
} }
paper-ripple { mwc-formfield {
top: 12px; display: block;
left: 0px; padding-right: 16px;
bottom: 8px;
right: 0px;
} }
paper-input { mwc-textfield {
text-overflow: ellipsis; display: block;
pointer-events: none;
}
ha-svg-icon {
color: var(--input-dropdown-icon-color);
position: absolute;
right: 1em;
top: 1em;
cursor: pointer;
}
:host([opened]) ha-svg-icon {
color: var(--primary-color);
}
:host([opened]) ha-button-menu {
--mdc-text-field-idle-line-color: var(--input-hover-line-color);
--mdc-text-field-label-ink-color: var(--primary-color);
} }
`; `;
} }

View File

@ -1,7 +1,7 @@
import { html, LitElement, TemplateResult } from "lit"; import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import "../ha-duration-input"; import "../ha-duration-input";
import { HaFormElement, HaFormTimeData, HaFormTimeSchema } from "./ha-form"; import { HaFormElement, HaFormTimeData, HaFormTimeSchema } from "./types";
@customElement("ha-form-positive_time_period_dict") @customElement("ha-form-positive_time_period_dict")
export class HaFormTimePeriod extends LitElement implements HaFormElement { export class HaFormTimePeriod extends LitElement implements HaFormElement {
@ -11,7 +11,7 @@ export class HaFormTimePeriod extends LitElement implements HaFormElement {
@property() public label!: string; @property() public label!: string;
@property() public suffix!: string; @property({ type: Boolean }) public disabled = false;
@query("ha-time-input", true) private _input?: HTMLElement; @query("ha-time-input", true) private _input?: HTMLElement;
@ -27,6 +27,7 @@ export class HaFormTimePeriod extends LitElement implements HaFormElement {
.label=${this.label} .label=${this.label}
.required=${this.schema.required} .required=${this.schema.required}
.data=${this.data} .data=${this.data}
.disabled=${this.disabled}
></ha-duration-input> ></ha-duration-input>
`; `;
} }

View File

@ -1,15 +1,14 @@
import "@material/mwc-icon-button/mwc-icon-button"; import "@material/mwc-select";
import { mdiClose, mdiMenuDown } from "@mdi/js"; import type { Select } from "@material/mwc-select";
import "@polymer/paper-input/paper-input"; import "@material/mwc-list/mwc-list-item";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-menu-button/paper-menu-button";
import "@polymer/paper-ripple/paper-ripple";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import "../ha-svg-icon"; import "../ha-radio";
import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./ha-form"; import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./types";
import { stopPropagation } from "../../common/dom/stop_propagation";
import type { HaRadio } from "../ha-radio";
@customElement("ha-form-select") @customElement("ha-form-select")
export class HaFormSelect extends LitElement implements HaFormElement { export class HaFormSelect extends LitElement implements HaFormElement {
@ -19,9 +18,9 @@ export class HaFormSelect extends LitElement implements HaFormElement {
@property() public label!: string; @property() public label!: string;
@property() public suffix!: string; @property({ type: Boolean }) public disabled = false;
@query("ha-paper-dropdown-menu", true) private _input?: HTMLElement; @query("mwc-select", true) private _input?: HTMLElement;
public focus() { public focus() {
if (this._input) { if (this._input) {
@ -30,90 +29,70 @@ export class HaFormSelect extends LitElement implements HaFormElement {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
return html` if (!this.schema.optional && this.schema.options!.length < 6) {
<paper-menu-button horizontal-align="right" vertical-offset="8"> return html`
<div class="dropdown-trigger" slot="dropdown-trigger"> <div>
<paper-ripple></paper-ripple> ${this.label}
<paper-input ${this.schema.options.map(
id="input" ([value, label]) => html`
type="text" <mwc-formfield .label=${label}>
readonly <ha-radio
value=${this.data} .checked=${value === this.data}
label=${this.label} .value=${value}
input-role="button" .disabled=${this.disabled}
input-aria-haspopup="listbox" @change=${this._valueChanged}
autocomplete="off" ></ha-radio>
> </mwc-formfield>
${this.data && this.schema.optional `
? html`<mwc-icon-button )}
slot="suffix"
class="clear-button"
@click=${this._clearValue}
>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>`
: ""}
<mwc-icon-button slot="suffix">
<ha-svg-icon .path=${mdiMenuDown}></ha-svg-icon>
</mwc-icon-button>
</paper-input>
</div> </div>
<paper-listbox `;
slot="dropdown-content" }
attr-for-selected="item-value"
.selected=${this.data} return html`
@selected-item-changed=${this._valueChanged} <mwc-select
> fixedMenuPosition
${ naturalMenuWidth
// TS doesn't work with union array types https://github.com/microsoft/TypeScript/issues/36390 .label=${this.label}
// @ts-ignore .value=${this.data}
this.schema.options!.map( .disabled=${this.disabled}
(item: string | [string, string]) => html` @closed=${stopPropagation}
<paper-item .itemValue=${this._optionValue(item)}> @selected=${this._valueChanged}
${this._optionLabel(item)} >
</paper-item> ${this.schema.optional
` ? html`<mwc-list-item value=""></mwc-list-item>`
) : ""}
} ${this.schema.options!.map(
</paper-listbox> ([value, label]) => html`
</paper-menu-button> <mwc-list-item .value=${value}>${label}</mwc-list-item>
`
)}
</mwc-select>
`; `;
} }
private _optionValue(item: string | [string, string]) {
return Array.isArray(item) ? item[0] : item;
}
private _optionLabel(item: string | [string, string]) {
return Array.isArray(item) ? item[1] || item[0] : item;
}
private _clearValue(ev: CustomEvent) {
ev.stopPropagation();
fireEvent(this, "value-changed", { value: undefined });
}
private _valueChanged(ev: CustomEvent) { private _valueChanged(ev: CustomEvent) {
if (!ev.detail.value) { ev.stopPropagation();
let value: string | undefined = (ev.target as Select | HaRadio).value;
if (value === this.data) {
return; return;
} }
if (value === "") {
value = undefined;
}
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: ev.detail.value.itemValue, value,
}); });
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
paper-menu-button { mwc-select,
mwc-formfield {
display: block; display: block;
padding: 0;
}
paper-input > mwc-icon-button {
--mdc-icon-button-size: 24px;
padding: 2px;
}
.clear-button {
color: var(--secondary-text-color);
} }
`; `;
} }

View File

@ -1,16 +1,22 @@
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiEye, mdiEyeOff } from "@mdi/js"; import { mdiEye, mdiEyeOff } from "@mdi/js";
import "@polymer/paper-input/paper-input"; import "@material/mwc-textfield";
import type { PaperInputElement } from "@polymer/paper-input/paper-input"; import type { TextField } from "@material/mwc-textfield";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import {
css,
CSSResultGroup,
html,
LitElement,
TemplateResult,
PropertyValues,
} from "lit";
import { customElement, property, state, query } from "lit/decorators"; import { customElement, property, state, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import "../ha-svg-icon"; import "../ha-icon-button";
import type { import type {
HaFormElement, HaFormElement,
HaFormStringData, HaFormStringData,
HaFormStringSchema, HaFormStringSchema,
} from "./ha-form"; } from "./types";
const MASKED_FIELDS = ["password", "secret", "token"]; const MASKED_FIELDS = ["password", "secret", "token"];
@ -22,11 +28,11 @@ export class HaFormString extends LitElement implements HaFormElement {
@property() public label!: string; @property() public label!: string;
@property() public suffix!: string; @property({ type: Boolean }) public disabled = false;
@state() private _unmaskedPassword = false; @state() private _unmaskedPassword = false;
@query("paper-input") private _input?: HTMLElement; @query("mwc-textfield") private _input?: HTMLElement;
public focus(): void { public focus(): void {
if (this._input) { if (this._input) {
@ -35,40 +41,44 @@ export class HaFormString extends LitElement implements HaFormElement {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
return MASKED_FIELDS.some((field) => this.schema.name.includes(field)) const isPassword = MASKED_FIELDS.some((field) =>
? html` this.schema.name.includes(field)
<paper-input );
.type=${this._unmaskedPassword ? "text" : "password"} return html`
.label=${this.label} <mwc-textfield
.value=${this.data} .type=${!isPassword
.required=${this.schema.required} ? this._stringType
.autoValidate=${this.schema.required} : this._unmaskedPassword
@value-changed=${this._valueChanged} ? "text"
> : "password"}
<mwc-icon-button .label=${this.label}
toggles .value=${this.data || ""}
slot="suffix" .disabled=${this.disabled}
id="iconButton" .required=${this.schema.required}
title="Click to toggle between masked and clear password" .autoValidate=${this.schema.required}
@click=${this._toggleUnmaskedPassword} .suffix=${isPassword
tabindex="-1" ? // reserve some space for the icon.
><ha-svg-icon html`<div style="width: 24px"></div>`
.path=${this._unmaskedPassword ? mdiEyeOff : mdiEye} : this.schema.description?.suffix}
></ha-svg-icon> .validationMessage=${this.schema.required ? "Required" : undefined}
</mwc-icon-button> @input=${this._valueChanged}
</paper-input> ></mwc-textfield>
` ${isPassword
: html` ? html`<ha-icon-button
<paper-input toggles
.type=${this._stringType} .label="Click to toggle between masked and clear password"
.label=${this.label} @click=${this._toggleUnmaskedPassword}
.value=${this.data} tabindex="-1"
.required=${this.schema.required} .path=${this._unmaskedPassword ? mdiEyeOff : mdiEye}
.autoValidate=${this.schema.required} ></ha-icon-button>`
error-message="Required" : ""}
@value-changed=${this._valueChanged} `;
></paper-input> }
`;
protected updated(changedProps: PropertyValues): void {
if (changedProps.has("schema")) {
this.toggleAttribute("own-margin", !!this.schema.required);
}
} }
private _toggleUnmaskedPassword(): void { private _toggleUnmaskedPassword(): void {
@ -76,10 +86,13 @@ export class HaFormString extends LitElement implements HaFormElement {
} }
private _valueChanged(ev: Event): void { private _valueChanged(ev: Event): void {
const value = (ev.target as PaperInputElement).value; let value: string | undefined = (ev.target as TextField).value;
if (this.data === value) { if (this.data === value) {
return; return;
} }
if (value === "" && this.schema.optional) {
value = undefined;
}
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value, value,
}); });
@ -99,7 +112,20 @@ export class HaFormString extends LitElement implements HaFormElement {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
mwc-icon-button { :host {
display: block;
position: relative;
}
:host([own-margin]) {
margin-bottom: 5px;
}
mwc-textfield {
display: block;
}
ha-icon-button {
position: absolute;
top: 1em;
right: 12px;
--mdc-icon-button-size: 24px; --mdc-icon-button-size: 24px;
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }

View File

@ -2,7 +2,7 @@ import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { dynamicElement } from "../../common/dom/dynamic-element-directive"; import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { HaDurationData } from "../ha-duration-input"; import "../ha-alert";
import "./ha-form-boolean"; import "./ha-form-boolean";
import "./ha-form-constant"; import "./ha-form-constant";
import "./ha-form-float"; import "./ha-form-float";
@ -11,160 +11,82 @@ import "./ha-form-multi_select";
import "./ha-form-positive_time_period_dict"; import "./ha-form-positive_time_period_dict";
import "./ha-form-select"; import "./ha-form-select";
import "./ha-form-string"; import "./ha-form-string";
import { HaFormElement, HaFormDataContainer, HaFormSchema } from "./types";
export type HaFormSchema = const getValue = (obj, item) => (obj ? obj[item.name] : null);
| HaFormConstantSchema
| HaFormStringSchema
| HaFormIntegerSchema
| HaFormFloatSchema
| HaFormBooleanSchema
| HaFormSelectSchema
| HaFormMultiSelectSchema
| HaFormTimeSchema;
export interface HaFormBaseSchema {
name: string;
default?: HaFormData;
required?: boolean;
optional?: boolean;
description?: { suffix?: string; suggested_value?: HaFormData };
}
export interface HaFormConstantSchema extends HaFormBaseSchema {
type: "constant";
value: string;
}
export interface HaFormIntegerSchema extends HaFormBaseSchema {
type: "integer";
default?: HaFormIntegerData;
valueMin?: number;
valueMax?: number;
}
export interface HaFormSelectSchema extends HaFormBaseSchema {
type: "select";
options?: string[] | Array<[string, string]>;
}
export interface HaFormMultiSelectSchema extends HaFormBaseSchema {
type: "multi_select";
options?: Record<string, string> | string[] | Array<[string, string]>;
}
export interface HaFormFloatSchema extends HaFormBaseSchema {
type: "float";
}
export interface HaFormStringSchema extends HaFormBaseSchema {
type: "string";
format?: string;
}
export interface HaFormBooleanSchema extends HaFormBaseSchema {
type: "boolean";
}
export interface HaFormTimeSchema extends HaFormBaseSchema {
type: "positive_time_period_dict";
}
export interface HaFormDataContainer {
[key: string]: HaFormData;
}
export type HaFormData =
| HaFormStringData
| HaFormIntegerData
| HaFormFloatData
| HaFormBooleanData
| HaFormSelectData
| HaFormMultiSelectData
| HaFormTimeData;
export type HaFormStringData = string;
export type HaFormIntegerData = number;
export type HaFormFloatData = number;
export type HaFormBooleanData = boolean;
export type HaFormSelectData = string;
export type HaFormMultiSelectData = string[];
export type HaFormTimeData = HaDurationData;
export interface HaFormElement extends LitElement {
schema: HaFormSchema | HaFormSchema[];
data?: HaFormDataContainer | HaFormData;
label?: string;
suffix?: string;
}
@customElement("ha-form") @customElement("ha-form")
export class HaForm extends LitElement implements HaFormElement { export class HaForm extends LitElement implements HaFormElement {
@property() public data!: HaFormDataContainer | HaFormData; @property() public data!: HaFormDataContainer;
@property() public schema!: HaFormSchema | HaFormSchema[]; @property() public schema!: HaFormSchema[];
@property() public error; @property() public error?: Record<string, string>;
@property({ type: Boolean }) public disabled = false;
@property() public computeError?: (schema: HaFormSchema, error) => string; @property() public computeError?: (schema: HaFormSchema, error) => string;
@property() public computeLabel?: (schema: HaFormSchema) => string; @property() public computeLabel?: (schema: HaFormSchema) => string;
@property() public computeSuffix?: (schema: HaFormSchema) => string;
public focus() { public focus() {
const input = const root = this.shadowRoot?.querySelector(".root");
this.shadowRoot!.getElementById("child-form") || if (!root) {
this.shadowRoot!.querySelector("ha-form");
if (!input) {
return; return;
} }
(input as HTMLElement).focus(); for (const child of root.children) {
if (child.tagName !== "HA-ALERT") {
(child as HTMLElement).focus();
break;
}
}
} }
protected render() { protected render() {
if (Array.isArray(this.schema)) { return html`
return html` <div class="root">
${this.error && this.error.base ${this.error && this.error.base
? html` ? html`
<div class="error"> <ha-alert alert-type="error">
${this._computeError(this.error.base, this.schema)} ${this._computeError(this.error.base, this.schema)}
</div> </ha-alert>
` `
: ""} : ""}
${this.schema.map( ${this.schema.map((item) => {
(item) => html` const error = getValue(this.error, item);
<ha-form return html`
.data=${this._getValue(this.data, item)} ${error
.schema=${item} ? html`
.error=${this._getValue(this.error, item)} <ha-alert own-margin alert-type="error">
@value-changed=${this._valueChanged} ${this._computeError(error, item)}
.computeError=${this.computeError} </ha-alert>
.computeLabel=${this.computeLabel} `
.computeSuffix=${this.computeSuffix} : ""}
></ha-form> ${dynamicElement(`ha-form-${item.type}`, {
` schema: item,
)} data: getValue(this.data, item),
`; label: this._computeLabel(item),
} disabled: this.disabled,
})}
return html` `;
${this.error })}
? html` </div>
<div class="error">
${this._computeError(this.error, this.schema)}
</div>
`
: ""}
${dynamicElement(`ha-form-${this.schema.type}`, {
schema: this.schema,
data: this.data,
label: this._computeLabel(this.schema),
suffix: this._computeSuffix(this.schema),
id: "child-form",
})}
`; `;
} }
protected createRenderRoot() {
const root = super.createRenderRoot();
// attach it as soon as possible to make sure we fetch all events.
root.addEventListener("value-changed", (ev) => {
ev.stopPropagation();
const schema = (ev.target as HaFormElement).schema as HaFormSchema;
fireEvent(this, "value-changed", {
value: { ...this.data, [schema.name]: ev.detail.value },
});
});
return root;
}
private _computeLabel(schema: HaFormSchema) { private _computeLabel(schema: HaFormSchema) {
return this.computeLabel return this.computeLabel
? this.computeLabel(schema) ? this.computeLabel(schema)
@ -173,38 +95,25 @@ export class HaForm extends LitElement implements HaFormElement {
: ""; : "";
} }
private _computeSuffix(schema: HaFormSchema) {
return this.computeSuffix
? this.computeSuffix(schema)
: schema && schema.description
? schema.description.suffix
: "";
}
private _computeError(error, schema: HaFormSchema | HaFormSchema[]) { private _computeError(error, schema: HaFormSchema | HaFormSchema[]) {
return this.computeError ? this.computeError(error, schema) : error; return this.computeError ? this.computeError(error, schema) : error;
} }
private _getValue(obj, item) {
if (obj) {
return obj[item.name];
}
return null;
}
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
const schema = (ev.target as HaFormElement).schema as HaFormSchema;
const data = this.data as HaFormDataContainer;
fireEvent(this, "value-changed", {
value: { ...data, [schema.name]: ev.detail.value },
});
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
// .root has overflow: auto to avoid margin collapse
return css` return css`
.error { .root {
color: var(--error-color); margin-bottom: -24px;
overflow: auto;
}
.root > * {
display: block;
}
.root > *:not([own-margin]) {
margin-bottom: 24px;
}
ha-alert[own-margin] {
margin-bottom: 4px;
} }
`; `;
} }

View File

@ -0,0 +1,86 @@
import type { LitElement } from "lit";
import type { HaDurationData } from "../ha-duration-input";
export type HaFormSchema =
| HaFormConstantSchema
| HaFormStringSchema
| HaFormIntegerSchema
| HaFormFloatSchema
| HaFormBooleanSchema
| HaFormSelectSchema
| HaFormMultiSelectSchema
| HaFormTimeSchema;
export interface HaFormBaseSchema {
name: string;
default?: HaFormData;
required?: boolean;
optional?: boolean;
description?: { suffix?: string; suggested_value?: HaFormData };
}
export interface HaFormConstantSchema extends HaFormBaseSchema {
type: "constant";
value: string;
}
export interface HaFormIntegerSchema extends HaFormBaseSchema {
type: "integer";
default?: HaFormIntegerData;
valueMin?: number;
valueMax?: number;
}
export interface HaFormSelectSchema extends HaFormBaseSchema {
type: "select";
options: Array<[string, string]>;
}
export interface HaFormMultiSelectSchema extends HaFormBaseSchema {
type: "multi_select";
options: Record<string, string>;
}
export interface HaFormFloatSchema extends HaFormBaseSchema {
type: "float";
}
export interface HaFormStringSchema extends HaFormBaseSchema {
type: "string";
format?: string;
}
export interface HaFormBooleanSchema extends HaFormBaseSchema {
type: "boolean";
}
export interface HaFormTimeSchema extends HaFormBaseSchema {
type: "positive_time_period_dict";
}
export interface HaFormDataContainer {
[key: string]: HaFormData;
}
export type HaFormData =
| HaFormStringData
| HaFormIntegerData
| HaFormFloatData
| HaFormBooleanData
| HaFormSelectData
| HaFormMultiSelectData
| HaFormTimeData;
export type HaFormStringData = string;
export type HaFormIntegerData = number;
export type HaFormFloatData = number;
export type HaFormBooleanData = boolean;
export type HaFormSelectData = string;
export type HaFormMultiSelectData = string[];
export type HaFormTimeData = HaDurationData;
export interface HaFormElement extends LitElement {
schema: HaFormSchema | HaFormSchema[];
data?: HaFormDataContainer | HaFormData;
label?: string;
}

View File

@ -8,7 +8,6 @@ import {
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { nextRender } from "../common/util/render-status"; import { nextRender } from "../common/util/render-status";
import { getExternalConfig } from "../external_app/external_config"; import { getExternalConfig } from "../external_app/external_config";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
@ -65,7 +64,6 @@ class HaHLSPlayer extends LitElement {
.muted=${this.muted} .muted=${this.muted}
?playsinline=${this.playsInline} ?playsinline=${this.playsInline}
?controls=${this.controls} ?controls=${this.controls}
@loadeddata=${this._elementResized}
></video> ></video>
`; `;
} }
@ -191,6 +189,7 @@ class HaHLSPlayer extends LitElement {
fragLoadingTimeOut: 30000, fragLoadingTimeOut: 30000,
manifestLoadingTimeOut: 30000, manifestLoadingTimeOut: 30000,
levelLoadingTimeOut: 30000, levelLoadingTimeOut: 30000,
maxLiveSyncPlaybackRate: 2,
}); });
this._hlsPolyfillInstance = hls; this._hlsPolyfillInstance = hls;
hls.attachMedia(videoEl); hls.attachMedia(videoEl);
@ -206,10 +205,6 @@ class HaHLSPlayer extends LitElement {
}); });
} }
private _elementResized() {
fireEvent(this, "iron-resize");
}
private _cleanUp() { private _cleanUp() {
if (this._hlsPolyfillInstance) { if (this._hlsPolyfillInstance) {
this._hlsPolyfillInstance.destroy(); this._hlsPolyfillInstance.destroy();

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