Compare commits

..

4 Commits

Author SHA1 Message Date
Zack Barett
a8367f58a1 Update src/panels/lovelace/editor/hui-element-editor.ts
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
2021-10-04 17:43:02 -05:00
Zack Barett
bfd6b97f51 Update src/panels/lovelace/editor/hui-element-editor.ts
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
2021-10-04 17:43:01 -05:00
Zack Barett
af1e86fe82 Update src/panels/lovelace/editor/config-elements/hui-shopping-list-editor.ts
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
2021-10-04 17:43:01 -05:00
Zack Arnett
5b90c8a19c First pass 2021-10-04 17:43:00 -05:00
322 changed files with 3772 additions and 5634 deletions

View File

@@ -29,7 +29,6 @@
"__BUILD__": false,
"__VERSION__": false,
"__STATIC_PATH__": false,
"__SUPERVISOR__": false,
"Polymer": true
},
"env": {
@@ -112,7 +111,8 @@
],
"unused-imports/no-unused-imports": "error",
"lit/attribute-value-entities": "off",
"lit/no-template-map": "off"
"lit/no-template-map": "off",
"lit/no-template-arrow": "warn"
},
"plugins": ["disable", "unused-imports"],
"processor": "disable/disable"

View File

@@ -35,7 +35,6 @@ module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
__VERSION__: JSON.stringify(env.version()),
__DEMO__: false,
__SUPERVISOR__: false,
__BACKWARDS_COMPAT__: false,
__STATIC_PATH__: "/static/",
"process.env.NODE_ENV": JSON.stringify(
@@ -195,9 +194,6 @@ module.exports.config = {
publicPath: publicPath(latestBuild, paths.hassio_publicPath),
isProdBuild,
latestBuild,
defineOverlay: {
__SUPERVISOR__: true,
},
};
},
@@ -210,9 +206,6 @@ module.exports.config = {
publicPath: publicPath(latestBuild),
isProdBuild,
latestBuild,
defineOverlay: {
__DEMO__: true,
},
};
},
};

View File

@@ -22,38 +22,17 @@ const getMeta = () => {
const svg = fs.readFileSync(`${ICON_PATH}/${icon.name}.svg`, {
encoding,
});
return {
path: svg.match(/ d="([^"]+)"/)[1],
name: icon.name,
tags: icon.tags,
};
return { path: svg.match(/ d="([^"]+)"/)[1], name: icon.name };
});
};
const addRemovedMeta = (meta) => {
const file = fs.readFileSync(REMOVED_ICONS_PATH, { encoding });
const removed = JSON.parse(file);
const removedMeta = removed.map((removeIcon) => ({
path: removeIcon.path,
name: removeIcon.name,
tags: [],
}));
const combinedMeta = [...meta, ...removedMeta];
const combinedMeta = [...meta, ...removed];
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 chunks = [];
const CHUNK_SIZE = 50000;
@@ -98,9 +77,8 @@ const findDifferentiator = (curString, prevString) => {
};
gulp.task("gen-icons-json", (done) => {
const meta = getMeta();
const metaAndRemoved = addRemovedMeta(meta);
const split = splitBySize(metaAndRemoved);
const meta = addRemovedMeta(getMeta());
const split = splitBySize(meta);
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
@@ -138,12 +116,5 @@ gulp.task("gen-icons-json", (done) => {
JSON.stringify({ version: package.version, parts })
);
const orderedMeta = orderMeta(meta);
fs.writeFileSync(
path.resolve(OUTPUT_DIR, "iconList.json"),
JSON.stringify(orderedMeta.map((icon) => icon.name))
);
done();
});

View File

@@ -1,6 +1,9 @@
const gulp = require("gulp");
const fs = require("fs");
const path = require("path");
const env = require("../env");
const paths = require("../paths");
require("./clean.js");
require("./gen-icons-json.js");
@@ -17,6 +20,7 @@ gulp.task(
process.env.NODE_ENV = "development";
},
"clean-hassio",
"gen-icons-json",
"gen-index-hassio-dev",
"build-supervisor-translations",
"copy-translations-supervisor",
@@ -33,6 +37,7 @@ gulp.task(
process.env.NODE_ENV = "production";
},
"clean-hassio",
"gen-icons-json",
"build-supervisor-translations",
"copy-translations-supervisor",
"build-locale-data",

View File

@@ -4,6 +4,9 @@ const del = require("del");
const path = require("path");
const gulp = require("gulp");
const fs = require("fs");
const merge = require("gulp-merge-json");
const rename = require("gulp-rename");
const transform = require("gulp-json-transform");
const paths = require("../paths");
const outDir = "build/locale-data";

View File

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

View File

@@ -1,7 +0,0 @@
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

@@ -1,7 +0,0 @@
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

@@ -1,7 +0,0 @@
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

@@ -1,59 +0,0 @@
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

@@ -1,35 +0,0 @@
#!/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

@@ -1,143 +0,0 @@
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

@@ -1,85 +0,0 @@
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

@@ -1,61 +0,0 @@
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,25 +1,23 @@
/* eslint-disable lit/no-template-arrow */
import "@material/mwc-button";
import { LitElement, TemplateResult, html } from "lit";
import { LitElement, TemplateResult, css, html } from "lit";
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 "../components/demo-black-white-row";
import "../../../src/components/ha-card";
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
import type { HaFormSchema } from "../../../src/components/ha-form/ha-form";
const SCHEMAS: {
title: string;
translations?: Record<string, string>;
error?: Record<string, string>;
schema: HaFormSchema[];
data?: Record<string, any>;
}[] = [
{
title: "Authentication",
translations: {
username: "Username",
password: "Password",
invalid_login: "Invalid username or password",
invalid_login: "Invalid login",
},
error: {
base: "invalid_login",
@@ -59,11 +57,6 @@ const SCHEMAS: {
optional: true,
default: 10,
},
{
type: "float",
name: "float",
required: true,
},
{
type: "string",
name: "string",
@@ -90,80 +83,6 @@ const SCHEMAS: {
optional: true,
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",
},
],
},
{
@@ -176,7 +95,7 @@ const SCHEMAS: {
other: "Other",
},
name: "multi",
required: true,
optional: true,
default: ["default"],
},
{
@@ -189,90 +108,101 @@ const SCHEMAS: {
and: "another_one",
option: "1000",
},
name: "multi many otions",
name: "multi",
optional: true,
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")
class DemoHaForm extends LitElement {
private data = SCHEMAS.map(
({ schema, data }) => data || computeInitialHaFormData(schema)
);
private lightModeData: any = [];
private disabled = SCHEMAS.map(() => false);
private darkModeData: any = [];
protected render(): TemplateResult {
return html`
${SCHEMAS.map((info, idx) => {
const translations = info.translations || {};
return html`
<demo-black-white-row
.title=${info.title}
.value=${this.data[idx]}
.disabled=${this.disabled[idx]}
@submitted=${() => {
this.disabled[idx] = true;
this.requestUpdate();
setTimeout(() => {
this.disabled[idx] = false;
this.requestUpdate();
}, 2000);
}}
>
${["light", "dark"].map(
(slot) => html`
<ha-form
slot=${slot}
.data=${this.data[idx]}
.schema=${info.schema}
.error=${info.error}
.disabled=${this.disabled[idx]}
.computeError=${(error) => translations[error] || error}
.computeLabel=${(schema) =>
translations[schema.name] || schema.name}
@value-changed=${(e) => {
this.data[idx] = e.detail.value;
this.requestUpdate();
}}
></ha-form>
`
)}
</demo-black-white-row>
`;
const computeLabel = (schema) =>
translations[schema.name] || schema.name;
const computeError = (error) => translations[error] || error;
return [
[this.lightModeData, "light"],
[this.darkModeData, "dark"],
].map(
([data, type]) => html`
<div class="row" data-type=${type}>
<ha-card .header=${info.title}>
<div class="card-content">
<ha-form
.data=${data[idx]}
.schema=${info.schema}
.error=${info.error}
.computeError=${computeError}
.computeLabel=${computeLabel}
@value-changed=${(e) => {
data[idx] = e.detail.value;
this.requestUpdate();
}}
></ha-form>
</div>
</ha-card>
<pre>${JSON.stringify(data[idx], undefined, 2)}</pre>
</div>
`
);
})}
`;
}
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 {

View File

@@ -1,122 +0,0 @@
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

@@ -1,131 +0,0 @@
/* 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,7 +187,6 @@ const createEntityRegistryEntries = (
device_id: "mock-device-id",
area_id: null,
disabled_by: null,
entity_category: null,
entity_id: "binary_sensor.updater",
name: null,
icon: null,
@@ -212,7 +211,6 @@ const createDeviceRegistryEntries = (
area_id: null,
name_by_user: null,
disabled_by: null,
configuration_url: null,
},
];

View File

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

View File

@@ -4,7 +4,6 @@ import { property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { navigate } from "../../../src/common/navigate";
import { caseInsensitiveStringCompare } from "../../../src/common/string/compare";
import "../../../src/components/ha-card";
import {
HassioAddonInfo,
@@ -33,7 +32,7 @@ class HassioAddonRepositoryEl extends LitElement {
return filterAndSort(addons, filter);
}
return addons.sort((a, b) =>
caseInsensitiveStringCompare(a.name, b.name)
a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1
);
}
);

View File

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

View File

@@ -15,13 +15,12 @@ import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-button-menu";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
import type { HaFormSchema } from "../../../../src/components/ha-form/ha-form";
import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-switch";
import "../../../../src/components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../src/components/ha-yaml-editor";
@@ -101,11 +100,9 @@ class HassioAddonConfig extends LitElement {
</h2>
<div class="card-menu">
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
<ha-icon-button
.label=${this.hass.localize("common.menu")}
.path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>
<mwc-icon-button slot="trigger">
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item .disabled=${!this._canShowSchema}>
${this._yamlMode
? this.supervisor.localize(

View File

@@ -180,21 +180,24 @@ class HassioAddonInfo extends LitElement {
: ""}
${!this.addon.protected
? html`
<ha-alert
alert-type="warning"
.title=${this.supervisor.localize(
"addon.dashboard.protection_mode.title"
)}
.actionText=${this.supervisor.localize(
<ha-card class="warning">
<h1 class="card-header">${this.supervisor.localize(
"addon.dashboard.protection_mode.title"
)}
</h1>
<div class="card-content">
${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"
)}
@alert-action-clicked=${this._protectionToggled}
>
${this.supervisor.localize(
"addon.dashboard.protection_mode.content"
)}
</ha-alert>
`
</mwc-button>
</div>
</div>
</ha-card>
`
: ""}
<ha-card>
@@ -294,11 +297,10 @@ class HassioAddonInfo extends LitElement {
})}
@click=${this._showMoreInfo}
id="rating"
.value=${this.addon.rating}
label="rating"
description=""
>
${this.addon.rating}
</ha-label-badge>
></ha-label-badge>
${this.addon.host_network
? html`
<ha-label-badge
@@ -362,9 +364,9 @@ class HassioAddonInfo extends LitElement {
<ha-label-badge
@click=${this._showMoreInfo}
id="docker_api"
.label=${this.supervisor.localize(
.label=".${this.supervisor.localize(
"addon.dashboard.capability.label.docker"
)}
)}"
description=""
>
<ha-svg-icon .path=${mdiDocker}></ha-svg-icon>

View File

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

View File

@@ -1,6 +1,7 @@
import { mdiHelpCircle } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../src/components/ha-relative-time";
import "../../../src/components/ha-svg-icon";
import { HomeAssistant } from "../../../src/types";
@@ -18,6 +19,8 @@ class HassioCardContent extends LitElement {
@property() public topbarClass?: string;
@property() public datetime?: string;
@property() public iconTitle?: string;
@property() public iconClass?: string;
@@ -53,6 +56,15 @@ class HassioCardContent extends LitElement {
/* treat as available when undefined */
this.available === false ? " (Not available)" : ""
}
${this.datetime
? html`
<ha-relative-time
.hass=${this.hass}
class="addition"
.datetime=${this.datetime}
></ha-relative-time>
`
: undefined}
</div>
</div>
`;
@@ -94,6 +106,9 @@ class HassioCardContent extends LitElement {
height: 2.4em;
line-height: 1.2em;
}
ha-relative-time {
display: block;
}
.icon_image img {
max-height: 40px;
max-width: 40px;

View File

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

View File

@@ -3,7 +3,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version";
import { navigate } from "../../../src/common/navigate";
import { caseInsensitiveStringCompare } from "../../../src/common/string/compare";
import { stringCompare } from "../../../src/common/string/compare";
import "../../../src/components/ha-card";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../src/resources/styles";
@@ -33,7 +33,7 @@ class HassioAddons extends LitElement {
</ha-card>
`
: this.supervisor.supervisor.addons
.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name))
.sort((a, b) => stringCompare(a.name, b.name))
.map(
(addon) => html`
<ha-card .addon=${addon} @click=${this._addonTapped}>

View File

@@ -3,7 +3,6 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-icon-button";
import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
@@ -53,12 +52,9 @@ export class DialogHassioBackupUpload
<div slot="heading">
<ha-header-bar>
<span slot="title"> Upload backup </span>
<ha-icon-button
.label=${this.hass.localize("common.close")}
.path=${mdiClose}
slot="actionItems"
dialogAction="cancel"
></ha-icon-button>
<mwc-icon-button slot="actionItems" dialogAction="cancel">
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
</ha-header-bar>
</div>
<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-button-menu";
import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-svg-icon";
import { getSignedPath } from "../../../../src/data/auth";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import {
@@ -76,12 +76,9 @@ class HassioBackupDialog
<div slot="heading">
<ha-header-bar>
<span slot="title">${this._backup.name}</span>
<ha-icon-button
.label=${this.hass.localize("common.close")}
.path=${mdiClose}
slot="actionItems"
dialogAction="cancel"
></ha-icon-button>
<mwc-icon-button slot="actionItems" dialogAction="cancel">
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
</ha-header-bar>
</div>
${this._restoringBackup
@@ -113,11 +110,9 @@ class HassioBackupDialog
@action=${this._handleMenuAction}
@closed=${stopPropagation}
>
<ha-icon-button
.label=${this.hass.localize("common.menu")}
.path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>
<mwc-icon-button slot="trigger" alt="menu">
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item>Download Backup</mwc-list-item>
<mwc-list-item class="error">Delete Backup</mwc-list-item>
</ha-button-menu>`
@@ -131,6 +126,9 @@ class HassioBackupDialog
haStyle,
haStyleDialog,
css`
ha-svg-icon {
color: var(--primary-text-color);
}
ha-circular-progress {
display: block;
text-align: center;

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiDelete } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
@@ -8,11 +9,10 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../../../../src/common/string/compare";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-circular-progress";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-svg-icon";
import {
fetchHassioAddonsInfo,
HassioAddonRepository,
@@ -57,7 +57,7 @@ class HassioRepositoriesDialog extends LitElement {
private _filteredRepositories = memoizeOne((repos: HassioAddonRepository[]) =>
repos
.filter((repo) => repo.slug !== "core" && repo.slug !== "local")
.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name))
.sort((a, b) => (a.name < b.name ? -1 : 1))
);
protected render(): TemplateResult {
@@ -89,14 +89,15 @@ class HassioRepositoriesDialog extends LitElement {
<div secondary>${repo.maintainer}</div>
<div secondary>${repo.url}</div>
</paper-item-body>
<ha-icon-button
<mwc-icon-button
.slug=${repo.slug}
.label=${this._dialogParams!.supervisor.localize(
.title=${this._dialogParams!.supervisor.localize(
"dialog.repositories.remove"
)}
.path=${mdiDelete}
@click=${this._removeRepository}
></ha-icon-button>
>
<ha-svg-icon .path=${mdiDelete}></ha-svg-icon>
</mwc-icon-button>
</paper-item>
`
)

View File

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

View File

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

View File

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

View File

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

View File

@@ -46,29 +46,26 @@
"@fullcalendar/interaction": "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",
"@material/chips": "14.0.0-canary.353ca7e9f.0",
"@material/data-table": "14.0.0-canary.353ca7e9f.0",
"@material/mwc-button": "0.25.2",
"@material/mwc-checkbox": "0.25.2",
"@material/mwc-circular-progress": "0.25.2",
"@material/mwc-dialog": "0.25.2",
"@material/mwc-fab": "0.25.2",
"@material/mwc-formfield": "0.25.2",
"@material/mwc-icon-button": "0.25.2",
"@material/mwc-linear-progress": "0.25.2",
"@material/mwc-list": "0.25.2",
"@material/mwc-menu": "0.25.2",
"@material/mwc-radio": "0.25.2",
"@material/mwc-ripple": "0.25.2",
"@material/mwc-select": "0.25.2",
"@material/mwc-slider": "0.25.2",
"@material/mwc-switch": "0.25.2",
"@material/mwc-tab": "0.25.2",
"@material/mwc-tab-bar": "0.25.2",
"@material/mwc-textfield": "0.25.2",
"@material/top-app-bar": "14.0.0-canary.353ca7e9f.0",
"@mdi/js": "6.3.95",
"@mdi/svg": "6.3.95",
"@material/chips": "13.0.0-canary.65125b3a6.0",
"@material/data-table": "13.0.0-canary.65125b3a6.0",
"@material/mwc-button": "0.25.1",
"@material/mwc-checkbox": "0.25.1",
"@material/mwc-circular-progress": "0.25.1",
"@material/mwc-dialog": "0.25.1",
"@material/mwc-fab": "0.25.1",
"@material/mwc-formfield": "0.25.1",
"@material/mwc-icon-button": "0.25.1",
"@material/mwc-linear-progress": "0.25.1",
"@material/mwc-list": "0.25.1",
"@material/mwc-menu": "0.25.1",
"@material/mwc-radio": "0.25.1",
"@material/mwc-ripple": "0.25.1",
"@material/mwc-switch": "0.25.1",
"@material/mwc-tab": "0.25.1",
"@material/mwc-tab-bar": "0.25.1",
"@material/top-app-bar": "13.0.0-canary.65125b3a6.0",
"@mdi/js": "6.2.95",
"@mdi/svg": "6.2.95",
"@polymer/app-layout": "^3.1.0",
"@polymer/iron-flex-layout": "^3.0.1",
"@polymer/iron-icon": "^3.0.1",

View File

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

View File

@@ -11,14 +11,12 @@ import "./ha-password-manager-polyfill";
import { property, state } from "lit/decorators";
import "../components/ha-form/ha-form";
import "../components/ha-markdown";
import "../components/ha-alert";
import { AuthProvider } from "../data/auth";
import {
DataEntryFlowStep,
DataEntryFlowStepForm,
} from "../data/data_entry_flow";
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
import { computeInitialHaFormData } from "../components/ha-form/compute-initial-ha-form-data";
type State = "loading" | "error" | "step";
@@ -33,42 +31,12 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
@state() private _state: State = "loading";
@state() private _stepData?: Record<string, any>;
@state() private _stepData: any = {};
@state() private _step?: DataEntryFlowStep;
@state() private _errorMessage?: string;
@state() private _submitting = 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() {
return html`
<form>${this._renderForm()}</form>
@@ -108,24 +76,6 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
if (changedProps.has("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 {
@@ -137,33 +87,27 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
return html`
${this._renderStep(this._step)}
<div class="action">
<mwc-button
raised
@click=${this._handleSubmit}
.disabled=${this._submitting}
>
${this._step.type === "form"
<mwc-button raised @click=${this._handleSubmit}
>${this._step.type === "form"
? this.localize("ui.panel.page-authorize.form.next")
: this.localize("ui.panel.page-authorize.form.start_over")}
</mwc-button>
: this.localize(
"ui.panel.page-authorize.form.start_over"
)}</mwc-button
>
</div>
`;
case "error":
return html`
<ha-alert alert-type="error">
<div class="error">
${this.localize(
"ui.panel.page-authorize.form.error",
"error",
this._errorMessage
)}
</ha-alert>
</div>
`;
case "loading":
return html`
<ha-alert alert-type="info">
${this.localize("ui.panel.page-authorize.form.working")}
</ha-alert>
`;
return html` ${this.localize("ui.panel.page-authorize.form.working")} `;
default:
return html``;
}
@@ -196,7 +140,6 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
.data=${this._stepData}
.schema=${step.data_schema}
.error=${step.errors}
.disabled=${this._submitting}
.computeLabel=${this._computeLabelCallback(step)}
.computeError=${this._computeErrorCallback(step)}
@value-changed=${this._stepDataChanged}
@@ -246,8 +189,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
return;
}
this._step = data;
this._state = "step";
await this._updateStep(data);
} else {
this._state = "error";
this._errorMessage = data.message;
@@ -278,6 +220,39 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
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) {
this._stepData = ev.detail.value;
}
@@ -322,7 +297,9 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
this._providerChanged(this.authProvider);
return;
}
this._submitting = true;
this._state = "loading";
// To avoid a jumping UI.
this.style.setProperty("min-height", `${this.offsetHeight}px`);
const postData = { ...this._stepData, client_id: this.clientId };
@@ -339,24 +316,30 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
this._redirect(newStep.result);
return;
}
this._step = newStep;
this._state = "step";
await this._updateStep(newStep);
} catch (err: any) {
// eslint-disable-next-line no-console
console.error("Error submitting step", err);
this._state = "error";
this._errorMessage = this._unknownError();
} finally {
this._submitting = false;
this.style.setProperty("min-height", "");
}
}
static get styles(): CSSResultGroup {
return css`
:host {
/* So we can set min-height to avoid jumping during loading */
display: block;
}
.action {
margin: 24px 0 8px;
text-align: center;
}
.error {
color: red;
}
`;
}
}

View File

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

View File

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

View File

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

View File

@@ -57,29 +57,28 @@ export const FIXED_DOMAIN_ICONS = {
export const FIXED_DEVICE_CLASS_ICONS = {
aqi: "hass:air-filter",
// battery: "hass:battery", => not included by design since `sensorIcon()` will dynamically determine the icon
current: "hass:current-ac",
carbon_dioxide: "mdi:molecule-co2",
carbon_monoxide: "mdi:molecule-co",
current: "hass:current-ac",
date: "hass:calendar",
energy: "hass:lightning-bolt",
gas: "hass:gas-cylinder",
humidity: "hass:water-percent",
illuminance: "hass:brightness-5",
monetary: "mdi:cash",
nitrogen_dioxide: "mdi:molecule",
nitrogen_monoxide: "mdi:molecule",
nitrous_oxide: "mdi:molecule",
ozone: "mdi:molecule",
temperature: "hass:thermometer",
monetary: "mdi:cash",
pm25: "mdi:molecule",
pm1: "mdi:molecule",
pm10: "mdi:molecule",
pm25: "mdi:molecule",
pressure: "hass:gauge",
power: "hass:flash",
power_factor: "hass:angle-acute",
pressure: "hass:gauge",
signal_strength: "hass:wifi",
sulphur_dioxide: "mdi:molecule",
temperature: "hass:thermometer",
timestamp: "hass:clock",
volatile_organic_compounds: "mdi:molecule",
voltage: "hass:sine-wave",

View File

@@ -36,62 +36,55 @@ export const applyThemesOnElement = (
let cacheKey = selectedTheme;
let themeRules: Partial<ThemeVars> = {};
// If there is no explicitly desired dark mode provided, we automatically
// use the active one from hass.themes.
if (!themeSettings || themeSettings?.dark === undefined) {
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 (themeSettings) {
if (themeSettings.dark) {
cacheKey = `${cacheKey}__dark`;
themeRules = { ...darkStyles };
}
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";
}
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;
// Nothing was changed
if (element._themes?.cacheKey === cacheKey) {
return;
if (themeSettings.dark && primaryColor) {
themeRules["app-header-background-color"] = hexBlend(
primaryColor,
"#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;
}
}
}
@@ -122,7 +115,7 @@ export const applyThemesOnElement = (
}
const newTheme =
Object.keys(themeRules).length && cacheKey
themeRules && cacheKey
? PROCESSED_THEMES[cacheKey] || processTheme(cacheKey, themeRules)
: undefined;

View File

@@ -22,7 +22,6 @@ export const binarySensorIcon = (state?: string, stateObj?: HassEntity) => {
case "gas":
case "problem":
case "safety":
case "tamper":
return is_off ? "hass:check-circle" : "hass:alert-circle";
case "smoke":
return is_off ? "hass:check-circle" : "hass:smoke";

View File

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

View File

@@ -1,2 +0,0 @@
export const capitalizeFirstLetter = (str: string) =>
str.charAt(0).toUpperCase() + str.slice(1);

View File

@@ -180,10 +180,10 @@ export function fuzzyScore(
wordLow
);
let row: number;
let row = 1;
let column = 1;
let patternPos: number;
let wordPos: number;
let patternPos = patternStart;
let wordPos = wordStart;
const hasStrongFirstMatch = [false];

View File

@@ -4,7 +4,7 @@ import { shouldPolyfill as shouldPolyfillRelativeTime } from "@formatjs/intl-rel
import { shouldPolyfill as shouldPolyfillDateTime } from "@formatjs/intl-datetimeformat/lib/should-polyfill";
import IntlMessageFormat from "intl-messageformat";
import { Resources } from "../../types";
import { getLocalLanguage } from "../../util/common-translation";
import { getLocalLanguage } from "../../util/hass-translation";
export type LocalizeFunc = (key: string, ...args: any[]) => string;
interface FormatType {

View File

@@ -86,7 +86,6 @@ export default class HaChartBase extends LitElement {
class=${classMap({
hidden: this._hiddenDatasets.has(index),
})}
.title=${dataset.label}
>
<div
class="bullet"

View File

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

View File

@@ -1,5 +1,4 @@
import { Layout1d, scroll } from "@lit-labs/virtualizer";
import { mdiArrowDown, mdiArrowUp } from "@mdi/js";
import deepClone from "deep-clone-simple";
import {
css,
@@ -28,9 +27,8 @@ import { nextRender } from "../../common/util/render-status";
import { haStyleScrollbar } from "../../resources/styles";
import "../ha-checkbox";
import type { HaCheckbox } from "../ha-checkbox";
import "../ha-svg-icon";
import "../ha-icon";
import { filterData, sortData } from "./sort-filter";
import { HomeAssistant } from "../../types";
declare global {
// for fire event
@@ -94,8 +92,6 @@ export interface SortableColumnContainer {
@customElement("ha-data-table")
export class HaDataTable extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Object }) public columns: DataTableColumnContainer = {};
@property({ type: Array }) public data: DataTableRowData[] = [];
@@ -235,7 +231,6 @@ export class HaDataTable extends LitElement {
? html`
<div class="table-header">
<search-input
.hass=${this.hass}
@value-changed=${this._handleSearchChange}
.label=${this.searchLabel}
.noLabelFloat=${this.noLabelFloat}
@@ -316,11 +311,11 @@ export class HaDataTable extends LitElement {
>
${column.sortable
? html`
<ha-svg-icon
.path=${sorted && this._sortDirection === "desc"
? mdiArrowDown
: mdiArrowUp}
></ha-svg-icon>
<ha-icon
.icon=${sorted && this._sortDirection === "desc"
? "hass:arrow-down"
: "hass:arrow-up"}
></ha-icon>
`
: ""}
<span>${column.title}</span>
@@ -868,14 +863,14 @@ export class HaDataTable extends LitElement {
:host([dir="rtl"]) .mdc-data-table__header-cell > * {
transition: right 0.2s ease;
}
.mdc-data-table__header-cell ha-svg-icon {
.mdc-data-table__header-cell ha-icon {
top: -3px;
position: absolute;
}
.mdc-data-table__header-cell.not-sorted ha-svg-icon {
.mdc-data-table__header-cell.not-sorted ha-icon {
left: -20px;
}
:host([dir="rtl"]) .mdc-data-table__header-cell.not-sorted ha-svg-icon {
:host([dir="rtl"]) .mdc-data-table__header-cell.not-sorted ha-icon {
right: -20px;
}
.mdc-data-table__header-cell.sortable:not(.not-sorted) span,
@@ -891,16 +886,16 @@ export class HaDataTable extends LitElement {
left: auto;
right: 24px;
}
.mdc-data-table__header-cell.sortable:not(.not-sorted) ha-svg-icon,
.mdc-data-table__header-cell.sortable:hover.not-sorted ha-svg-icon {
.mdc-data-table__header-cell.sortable:not(.not-sorted) ha-icon,
.mdc-data-table__header-cell.sortable:hover.not-sorted ha-icon {
left: 12px;
}
:host([dir="rtl"])
.mdc-data-table__header-cell.sortable:not(.not-sorted)
ha-svg-icon,
ha-icon,
:host([dir="rtl"])
.mdc-data-table__header-cell.sortable:hover.not-sorted
ha-svg-icon {
ha-icon {
left: auto;
right: 12px;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
import { mdiAlert } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import {
css,
@@ -15,12 +14,11 @@ import { computeStateDisplay } from "../../common/entity/compute_state_display";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { stateIcon } from "../../common/entity/state_icon";
import { timerTimeRemaining } from "../../data/timer";
import { formatNumber } from "../../common/number/format_number";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { timerTimeRemaining } from "../../data/timer";
import { HomeAssistant } from "../../types";
import "../ha-label-badge";
import "../ha-icon";
@customElement("ha-state-label-badge")
export class HaStateLabelBadge extends LitElement {
@@ -60,31 +58,15 @@ export class HaStateLabelBadge extends LitElement {
<ha-label-badge
class="warning"
label=${this.hass!.localize("state_badge.default.error")}
icon="hass:alert"
description=${this.hass!.localize(
"state_badge.default.entity_not_found"
)}
>
<ha-svg-icon .path=${mdiAlert}></ha-svg-icon>
</ha-label-badge>
></ha-label-badge>
`;
}
// 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 icon = this.icon ? this.icon : this._computeIcon(domain, entityState);
const image = this.icon
? ""
: this.image
? this.image
: entityState.attributes.entity_picture_local ||
entityState.attributes.entity_picture;
const value =
!image && !icon ? this._computeValue(domain, entityState) : undefined;
return html`
<ha-label-badge
@@ -93,21 +75,21 @@ export class HaStateLabelBadge extends LitElement {
"has-unit_of_measurement":
"unit_of_measurement" in entityState.attributes,
})}
.image=${image}
.value=${this._computeValue(domain, entityState)}
.icon=${this.icon ? this.icon : this._computeIcon(domain, entityState)}
.image=${this.icon
? ""
: this.image
? this.image
: entityState.attributes.entity_picture_local ||
entityState.attributes.entity_picture}
.label=${this._computeLabel(
domain,
entityState,
this._timerTimeRemaining
)}
.description=${this.name ?? computeStateName(entityState)}
>
${!image && icon ? html`<ha-icon .icon=${icon}></ha-icon>` : ""}
${value && !icon && !image
? html`<span class=${value && value.length > 4 ? "big" : ""}
>${value}</span
>`
: ""}
</ha-label-badge>
.description=${this.name ? this.name : computeStateName(entityState)}
></ha-label-badge>
`;
}
@@ -226,9 +208,7 @@ export class HaStateLabelBadge extends LitElement {
:host {
cursor: pointer;
}
.big {
font-size: 70%;
}
ha-label-badge {
--ha-label-badge-color: var(--label-badge-red, #df4c1e);
}

View File

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

View File

@@ -24,15 +24,13 @@ class StateInfo extends LitElement {
return html``;
}
const name = computeStateName(this.stateObj);
return html`<state-badge
.stateObj=${this.stateObj}
.stateColor=${true}
></state-badge>
<div class="info">
<div class="name" .title=${name} .inDialog=${this.inDialog}>
${name}
<div class="name" .inDialog=${this.inDialog}>
${computeStateName(this.stateObj)}
</div>
${this.inDialog
? html`<div class="time-ago">
@@ -40,7 +38,6 @@ class StateInfo extends LitElement {
id="last_changed"
.hass=${this.hass}
.datetime=${this.stateObj.last_changed}
capitalize
></ha-relative-time>
<paper-tooltip animation-delay="0" for="last_changed">
<div>
@@ -53,7 +50,6 @@ class StateInfo extends LitElement {
<ha-relative-time
.hass=${this.hass}
.datetime=${this.stateObj.last_changed}
capitalize
></ha-relative-time>
</div>
<div class="row">
@@ -65,7 +61,6 @@ class StateInfo extends LitElement {
<ha-relative-time
.hass=${this.hass}
.datetime=${this.stateObj.last_updated}
capitalize
></ha-relative-time>
</div>
</div>
@@ -97,6 +92,7 @@ class StateInfo extends LitElement {
state-badge {
float: left;
}
:host([rtl]) state-badge {
float: right;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ import {
unsafeCSS,
} from "lit";
import { customElement, property } from "lit/decorators";
import "./ha-icon";
declare global {
// for fire event

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
import { mdiStop } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import {
css,
@@ -14,7 +13,6 @@ import { computeCloseIcon, computeOpenIcon } from "../common/entity/cover_icon";
import { UNAVAILABLE } from "../data/entity";
import type { HomeAssistant } from "../types";
import CoverEntity from "../util/cover-model";
import "./ha-icon";
import "./ha-icon-button";
@customElement("ha-cover-controls")
@@ -47,11 +45,10 @@ class HaCoverControls extends LitElement {
.label=${this.hass.localize(
"ui.dialogs.more_info_control.open_cover"
)}
.icon=${computeOpenIcon(this.stateObj)}
@click=${this._onOpenTap}
.disabled=${this._computeOpenDisabled()}
>
<ha-icon .icon=${computeOpenIcon(this.stateObj)}></ha-icon>
</ha-icon-button>
></ha-icon-button>
<ha-icon-button
class=${classMap({
hidden: !this._entityObj.supportsStop,
@@ -59,7 +56,7 @@ class HaCoverControls extends LitElement {
.label=${this.hass.localize(
"ui.dialogs.more_info_control.stop_cover"
)}
.path=${mdiStop}
icon="hass:stop"
@click=${this._onStopTap}
.disabled=${this.stateObj.state === UNAVAILABLE}
></ha-icon-button>
@@ -70,11 +67,10 @@ class HaCoverControls extends LitElement {
.label=${this.hass.localize(
"ui.dialogs.more_info_control.close_cover"
)}
.icon=${computeCloseIcon(this.stateObj)}
@click=${this._onCloseTap}
.disabled=${this._computeClosedDisabled()}
>
<ha-icon .icon=${computeCloseIcon(this.stateObj)}></ha-icon>
</ha-icon-button>
></ha-icon-button>
</div>
`;
}

View File

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

View File

@@ -11,13 +11,14 @@ export const createCloseHeading = (
title: string | TemplateResult
) => html`
<span class="header_title">${title}</span>
<ha-icon-button
.label=${hass.localize("ui.dialogs.generic.close")}
.path=${mdiClose}
<mwc-icon-button
aria-label=${hass.localize("ui.dialogs.generic.close")}
dialogAction="close"
class="header_button"
dir=${computeRTLDirection(hass)}
></ha-icon-button>
>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
`;
@customElement("ha-dialog")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,19 +1,16 @@
import "@material/mwc-textfield";
import type { TextField } from "@material/mwc-textfield";
import "@material/mwc-slider";
import type { Slider } from "@material/mwc-slider";
import {
css,
CSSResultGroup,
html,
LitElement,
TemplateResult,
PropertyValues,
} from "lit";
import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { HaCheckbox } from "../ha-checkbox";
import { HaFormElement, HaFormIntegerData, HaFormIntegerSchema } from "./types";
import "../ha-slider";
import type { HaSlider } from "../ha-slider";
import {
HaFormElement,
HaFormIntegerData,
HaFormIntegerSchema,
} from "./ha-form";
@customElement("ha-form-integer")
export class HaFormInteger extends LitElement implements HaFormElement {
@@ -23,12 +20,10 @@ export class HaFormInteger extends LitElement implements HaFormElement {
@property() public label?: string;
@property({ type: Boolean }) public disabled = false;
@property() public suffix?: string;
@query("paper-input ha-slider") private _input?: HTMLElement;
private _lastValue?: HaFormIntegerData;
public focus() {
if (this._input) {
this._input.focus();
@@ -36,116 +31,66 @@ export class HaFormInteger extends LitElement implements HaFormElement {
}
protected render(): TemplateResult {
if ("valueMin" in this.schema && "valueMax" in this.schema) {
return html`
<div>
${this.label}
<div class="flex">
${this.schema.optional
? html`
<ha-checkbox
@change=${this._handleCheckboxChange}
.checked=${this.data !== undefined}
.disabled=${this.disabled}
></ha-checkbox>
`
: ""}
<mwc-slider
discrete
.value=${this._value}
.min=${this.schema.valueMin}
.max=${this.schema.valueMax}
.disabled=${this.disabled ||
(this.data === undefined && this.schema.optional)}
@change=${this._valueChanged}
></mwc-slider>
return "valueMin" in this.schema && "valueMax" in this.schema
? html`
<div>
${this.label}
<div class="flex">
${this.schema.optional && this.schema.default === undefined
? html`
<ha-checkbox
@change=${this._handleCheckboxChange}
.checked=${this.data !== undefined}
></ha-checkbox>
`
: ""}
<ha-slider
pin
editable
.value=${this._value}
.min=${this.schema.valueMin}
.max=${this.schema.valueMax}
.disabled=${this.data === undefined &&
this.schema.optional &&
this.schema.default === undefined}
@value-changed=${this._valueChanged}
></ha-slider>
</div>
</div>
</div>
`;
}
return html`
<mwc-textfield
type="number"
inputMode="numeric"
.label=${this.label}
.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
);
}
`
: html`
<paper-input
type="number"
.label=${this.label}
.value=${this._value}
.required=${this.schema.required}
.autoValidate=${this.schema.required}
@value-changed=${this._valueChanged}
></paper-input>
`;
}
private get _value() {
if (this.data !== undefined) {
return this.data;
}
if (this.schema.optional) {
return 0;
}
return this.schema.description?.suggested_value || this.schema.default || 0;
return (
this.data ||
this.schema.description?.suggested_value ||
this.schema.default ||
0
);
}
private _handleCheckboxChange(ev: Event) {
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", {
value,
value: checked ? this._value : undefined,
});
}
private _valueChanged(ev: Event) {
const source = ev.target as TextField | Slider;
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;
}
const value = Number((ev.target as PaperInputElement | HaSlider).value);
if (this._value === value) {
return;
}
fireEvent(this, "value-changed", {
value,
});
@@ -153,17 +98,12 @@ export class HaFormInteger extends LitElement implements HaFormElement {
static get styles(): CSSResultGroup {
return css`
:host([own-margin]) {
margin-bottom: 5px;
}
.flex {
display: flex;
}
mwc-slider {
flex: 1;
}
mwc-textfield {
display: block;
ha-slider {
width: 100%;
margin-right: 16px;
}
`;
}

View File

@@ -1,35 +1,18 @@
import { mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@material/mwc-textfield";
import "@material/mwc-formfield";
import {
css,
CSSResultGroup,
html,
LitElement,
TemplateResult,
PropertyValues,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-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 { customElement, property, state, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-button-menu";
import "../ha-svg-icon";
import "../ha-icon";
import {
HaFormElement,
HaFormMultiSelectData,
HaFormMultiSelectSchema,
} 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;
} from "./ha-form";
@customElement("ha-form-multi_select")
export class HaFormMultiSelect extends LitElement implements HaFormElement {
@@ -39,9 +22,9 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
@property() public label!: string;
@property({ type: Boolean }) public disabled = false;
@property() public suffix!: string;
@state() private _opened = false;
@state() private _init = false;
@query("paper-menu-button", true) private _input?: HTMLElement;
@@ -52,144 +35,118 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
}
protected render(): TemplateResult {
const options = Object.entries(this.schema.options);
const options = Array.isArray(this.schema.options)
? this.schema.options
: Object.entries(this.schema.options!);
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`
<ha-button-menu
.disabled=${this.disabled}
fixed
corner="BOTTOM_START"
@opened=${this._handleOpen}
@closed=${this._handleClose}
>
<mwc-textfield
slot="trigger"
.label=${this.label}
.value=${data
.map((value) => this.schema.options![value] || value)
.join(", ")}
.disabled=${this.disabled}
tabindex="-1"
></mwc-textfield>
<ha-svg-icon
slot="trigger"
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
></ha-svg-icon>
${renderedOptions}
</ha-button-menu>
<paper-menu-button horizontal-align="right" vertical-offset="8">
<div class="dropdown-trigger" slot="dropdown-trigger">
<paper-ripple></paper-ripple>
<paper-input
id="input"
type="text"
readonly
value=${data
.map((value) => this.schema.options![value] || value)
.join(", ")}
label=${this.label}
input-role="button"
input-aria-haspopup="listbox"
autocomplete="off"
>
<ha-icon
icon="paper-dropdown-menu:arrow-drop-down"
suffix
slot="suffix"
></ha-icon>
</paper-input>
</div>
<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() {
this.updateComplete.then(() => {
const { formElement, mdcRoot } =
this.shadowRoot?.querySelector("mwc-textfield") || ({} as any);
if (formElement) {
formElement.style.textOverflow = "ellipsis";
}
if (mdcRoot) {
mdcRoot.style.cursor = "pointer";
const input = (
this.shadowRoot?.querySelector("paper-input")?.inputElement as any
)?.inputElement;
if (input) {
input.style.textOverflow = "ellipsis";
}
});
}
protected updated(changedProps: PropertyValues): void {
if (changedProps.has("schema")) {
this.toggleAttribute(
"own-margin",
Object.keys(this.schema.options).length >= SHOW_ALL_ENTRIES_LIMIT &&
!!this.schema.required
);
}
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 _onSelect(ev: Event) {
ev.stopPropagation();
}
private _valueChanged(ev: CustomEvent): void {
const { value, checked } = ev.target as HaCheckbox;
let newValue: string[];
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);
if (!ev.detail.value || !this._init) {
// ignore first call because that is the init of the component
this._init = true;
return;
}
fireEvent(this, "value-changed", {
value: newValue,
});
}
private _handleOpen(ev: Event): void {
ev.stopPropagation();
this._opened = true;
this.toggleAttribute("opened", true);
}
private _handleClose(ev: Event): void {
ev.stopPropagation();
this._opened = false;
this.toggleAttribute("opened", false);
fireEvent(
this,
"value-changed",
{
value: ev.detail.value.map((element) => element.itemValue),
},
{ bubbles: false }
);
}
static get styles(): CSSResultGroup {
return css`
:host([own-margin]) {
margin-bottom: 5px;
}
ha-button-menu {
paper-menu-button {
display: block;
cursor: pointer;
padding: 0;
--paper-item-icon-width: 34px;
}
mwc-formfield {
display: block;
padding-right: 16px;
paper-ripple {
top: 12px;
left: 0px;
bottom: 8px;
right: 0px;
}
mwc-textfield {
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);
paper-input {
text-overflow: ellipsis;
}
`;
}

View File

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

View File

@@ -1,14 +1,15 @@
import "@material/mwc-select";
import type { Select } from "@material/mwc-select";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiClose, mdiMenuDown } from "@mdi/js";
import "@polymer/paper-input/paper-input";
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 { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-radio";
import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./types";
import { stopPropagation } from "../../common/dom/stop_propagation";
import type { HaRadio } from "../ha-radio";
import "../ha-svg-icon";
import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./ha-form";
@customElement("ha-form-select")
export class HaFormSelect extends LitElement implements HaFormElement {
@@ -18,9 +19,9 @@ export class HaFormSelect extends LitElement implements HaFormElement {
@property() public label!: string;
@property({ type: Boolean }) public disabled = false;
@property() public suffix!: string;
@query("mwc-select", true) private _input?: HTMLElement;
@query("ha-paper-dropdown-menu", true) private _input?: HTMLElement;
public focus() {
if (this._input) {
@@ -29,69 +30,90 @@ export class HaFormSelect extends LitElement implements HaFormElement {
}
protected render(): TemplateResult {
if (!this.schema.optional && this.schema.options!.length < 6) {
return html`
<div>
${this.label}
${this.schema.options.map(
([value, label]) => html`
<mwc-formfield .label=${label}>
<ha-radio
.checked=${value === this.data}
.value=${value}
.disabled=${this.disabled}
@change=${this._valueChanged}
></ha-radio>
</mwc-formfield>
`
)}
</div>
`;
}
return html`
<mwc-select
fixedMenuPosition
.label=${this.label}
.value=${this.data}
.disabled=${this.disabled}
@closed=${stopPropagation}
@selected=${this._valueChanged}
>
${this.schema.optional
? html`<mwc-list-item value=""></mwc-list-item>`
: ""}
${this.schema.options!.map(
([value, label]) => html`
<mwc-list-item .value=${value}>${label}</mwc-list-item>
`
)}
</mwc-select>
<paper-menu-button horizontal-align="right" vertical-offset="8">
<div class="dropdown-trigger" slot="dropdown-trigger">
<paper-ripple></paper-ripple>
<paper-input
id="input"
type="text"
readonly
value=${this.data}
label=${this.label}
input-role="button"
input-aria-haspopup="listbox"
autocomplete="off"
>
${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>
<paper-listbox
slot="dropdown-content"
attr-for-selected="item-value"
.selected=${this.data}
@selected-item-changed=${this._valueChanged}
>
${
// TS doesn't work with union array types https://github.com/microsoft/TypeScript/issues/36390
// @ts-ignore
this.schema.options!.map(
(item: string | [string, string]) => html`
<paper-item .itemValue=${this._optionValue(item)}>
${this._optionLabel(item)}
</paper-item>
`
)
}
</paper-listbox>
</paper-menu-button>
`;
}
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
let value: string | undefined = (ev.target as Select | HaRadio).value;
private _optionValue(item: string | [string, string]) {
return Array.isArray(item) ? item[0] : item;
}
if (value === this.data) {
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) {
if (!ev.detail.value) {
return;
}
if (value === "") {
value = undefined;
}
fireEvent(this, "value-changed", {
value,
value: ev.detail.value.itemValue,
});
}
static get styles(): CSSResultGroup {
return css`
mwc-select,
mwc-formfield {
paper-menu-button {
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,22 +1,16 @@
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiEye, mdiEyeOff } from "@mdi/js";
import "@material/mwc-textfield";
import type { TextField } from "@material/mwc-textfield";
import {
css,
CSSResultGroup,
html,
LitElement,
TemplateResult,
PropertyValues,
} from "lit";
import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-icon-button";
import "../ha-svg-icon";
import type {
HaFormElement,
HaFormStringData,
HaFormStringSchema,
} from "./types";
} from "./ha-form";
const MASKED_FIELDS = ["password", "secret", "token"];
@@ -28,11 +22,11 @@ export class HaFormString extends LitElement implements HaFormElement {
@property() public label!: string;
@property({ type: Boolean }) public disabled = false;
@property() public suffix!: string;
@state() private _unmaskedPassword = false;
@query("mwc-textfield") private _input?: HTMLElement;
@query("paper-input") private _input?: HTMLElement;
public focus(): void {
if (this._input) {
@@ -41,44 +35,40 @@ export class HaFormString extends LitElement implements HaFormElement {
}
protected render(): TemplateResult {
const isPassword = MASKED_FIELDS.some((field) =>
this.schema.name.includes(field)
);
return html`
<mwc-textfield
.type=${!isPassword
? this._stringType
: this._unmaskedPassword
? "text"
: "password"}
.label=${this.label}
.value=${this.data || ""}
.disabled=${this.disabled}
.required=${this.schema.required}
.autoValidate=${this.schema.required}
.suffix=${isPassword
? // reserve some space for the icon.
html`<div style="width: 24px"></div>`
: this.schema.description?.suffix}
.validationMessage=${this.schema.required ? "Required" : undefined}
@input=${this._valueChanged}
></mwc-textfield>
${isPassword
? html`<ha-icon-button
toggles
.label="Click to toggle between masked and clear password"
@click=${this._toggleUnmaskedPassword}
tabindex="-1"
.path=${this._unmaskedPassword ? mdiEyeOff : mdiEye}
></ha-icon-button>`
: ""}
`;
}
protected updated(changedProps: PropertyValues): void {
if (changedProps.has("schema")) {
this.toggleAttribute("own-margin", !!this.schema.required);
}
return MASKED_FIELDS.some((field) => this.schema.name.includes(field))
? html`
<paper-input
.type=${this._unmaskedPassword ? "text" : "password"}
.label=${this.label}
.value=${this.data}
.required=${this.schema.required}
.autoValidate=${this.schema.required}
@value-changed=${this._valueChanged}
>
<mwc-icon-button
toggles
slot="suffix"
id="iconButton"
title="Click to toggle between masked and clear password"
@click=${this._toggleUnmaskedPassword}
tabindex="-1"
><ha-svg-icon
.path=${this._unmaskedPassword ? mdiEyeOff : mdiEye}
></ha-svg-icon>
</mwc-icon-button>
</paper-input>
`
: html`
<paper-input
.type=${this._stringType}
.label=${this.label}
.value=${this.data}
.required=${this.schema.required}
.autoValidate=${this.schema.required}
error-message="Required"
@value-changed=${this._valueChanged}
></paper-input>
`;
}
private _toggleUnmaskedPassword(): void {
@@ -86,13 +76,10 @@ export class HaFormString extends LitElement implements HaFormElement {
}
private _valueChanged(ev: Event): void {
let value: string | undefined = (ev.target as TextField).value;
const value = (ev.target as PaperInputElement).value;
if (this.data === value) {
return;
}
if (value === "" && this.schema.optional) {
value = undefined;
}
fireEvent(this, "value-changed", {
value,
});
@@ -112,20 +99,7 @@ export class HaFormString extends LitElement implements HaFormElement {
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
position: relative;
}
:host([own-margin]) {
margin-bottom: 5px;
}
mwc-textfield {
display: block;
}
ha-icon-button {
position: absolute;
top: 1em;
right: 12px;
mwc-icon-button {
--mdc-icon-button-size: 24px;
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 { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-alert";
import { HaDurationData } from "../ha-duration-input";
import "./ha-form-boolean";
import "./ha-form-constant";
import "./ha-form-float";
@@ -11,80 +11,158 @@ import "./ha-form-multi_select";
import "./ha-form-positive_time_period_dict";
import "./ha-form-select";
import "./ha-form-string";
import { HaFormElement, HaFormDataContainer, HaFormSchema } from "./types";
const getValue = (obj, item) => (obj ? obj[item.name] : null);
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?: 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")
export class HaForm extends LitElement implements HaFormElement {
@property() public data!: HaFormDataContainer;
@property() public data!: HaFormDataContainer | HaFormData;
@property() public schema!: HaFormSchema[];
@property() public schema!: HaFormSchema | HaFormSchema[];
@property() public error?: Record<string, string>;
@property({ type: Boolean }) public disabled = false;
@property() public error;
@property() public computeError?: (schema: HaFormSchema, error) => string;
@property() public computeLabel?: (schema: HaFormSchema) => string;
@property() public computeSuffix?: (schema: HaFormSchema) => string;
public focus() {
const root = this.shadowRoot?.querySelector(".root");
if (!root) {
const input =
this.shadowRoot!.getElementById("child-form") ||
this.shadowRoot!.querySelector("ha-form");
if (!input) {
return;
}
for (const child of root.children) {
if (child.tagName !== "HA-ALERT") {
(child as HTMLElement).focus();
break;
}
}
(input as HTMLElement).focus();
}
protected render() {
return html`
<div class="root">
if (Array.isArray(this.schema)) {
return html`
${this.error && this.error.base
? html`
<ha-alert alert-type="error">
<div class="error">
${this._computeError(this.error.base, this.schema)}
</ha-alert>
</div>
`
: ""}
${this.schema.map((item) => {
const error = getValue(this.error, item);
return html`
${error
? html`
<ha-alert own-margin alert-type="error">
${this._computeError(error, item)}
</ha-alert>
`
: ""}
${dynamicElement(`ha-form-${item.type}`, {
schema: item,
data: getValue(this.data, item),
label: this._computeLabel(item),
disabled: this.disabled,
})}
`;
})}
</div>
`;
}
${this.schema.map(
(item) => html`
<ha-form
.data=${this._getValue(this.data, item)}
.schema=${item}
.error=${this._getValue(this.error, item)}
@value-changed=${this._valueChanged}
.computeError=${this.computeError}
.computeLabel=${this.computeLabel}
.computeSuffix=${this.computeSuffix}
></ha-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;
return html`
${this.error
? html`
<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",
})}
`;
}
private _computeLabel(schema: HaFormSchema) {
@@ -95,25 +173,38 @@ 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[]) {
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 {
// .root has overflow: auto to avoid margin collapse
return css`
.root {
margin-bottom: -24px;
overflow: auto;
}
.root > * {
display: block;
}
.root > *:not([own-margin]) {
margin-bottom: 24px;
}
ha-alert[own-margin] {
margin-bottom: 4px;
.error {
color: var(--error-color);
}
`;
}

View File

@@ -1,86 +0,0 @@
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,6 +8,7 @@ import {
TemplateResult,
} from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { nextRender } from "../common/util/render-status";
import { getExternalConfig } from "../external_app/external_config";
import type { HomeAssistant } from "../types";
@@ -64,6 +65,7 @@ class HaHLSPlayer extends LitElement {
.muted=${this.muted}
?playsinline=${this.playsInline}
?controls=${this.controls}
@loadeddata=${this._elementResized}
></video>
`;
}
@@ -204,6 +206,10 @@ class HaHLSPlayer extends LitElement {
});
}
private _elementResized() {
fireEvent(this, "iron-resize");
}
private _cleanUp() {
if (this._hlsPolyfillInstance) {
this._hlsPolyfillInstance.destroy();

View File

@@ -1,8 +1,9 @@
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiArrowLeft, mdiArrowRight } from "@mdi/js";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { HomeAssistant } from "../types";
import "./ha-icon-button";
import "./ha-svg-icon";
@customElement("ha-icon-button-arrow-next")
export class HaIconButtonArrowNext extends LitElement {
@@ -27,13 +28,12 @@ export class HaIconButtonArrowNext extends LitElement {
}
protected render(): TemplateResult {
return html`
<ha-icon-button
.disabled=${this.disabled}
.label=${this.label || this.hass?.localize("ui.common.next") || "Next"}
.path=${this._icon}
></ha-icon-button>
`;
return html`<mwc-icon-button
.disabled=${this.disabled}
.label=${this.label || this.hass?.localize("ui.common.next") || "Next"}
>
<ha-svg-icon .path=${this._icon}></ha-svg-icon>
</mwc-icon-button> `;
}
}

View File

@@ -1,8 +1,9 @@
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiArrowLeft, mdiArrowRight } from "@mdi/js";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { HomeAssistant } from "../types";
import "./ha-icon-button";
import "./ha-svg-icon";
@customElement("ha-icon-button-arrow-prev")
export class HaIconButtonArrowPrev extends LitElement {
@@ -28,11 +29,12 @@ export class HaIconButtonArrowPrev extends LitElement {
protected render(): TemplateResult {
return html`
<ha-icon-button
<mwc-icon-button
.disabled=${this.disabled}
.label=${this.label || this.hass?.localize("ui.common.back") || "Back"}
.path=${this._icon}
></ha-icon-button>
>
<ha-svg-icon .path=${this._icon}></ha-svg-icon>
</mwc-icon-button>
`;
}
}

View File

@@ -1,8 +1,9 @@
import "@material/mwc-icon-button";
import { mdiChevronLeft, mdiChevronRight } from "@mdi/js";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { HomeAssistant } from "../types";
import "./ha-icon-button";
import "./ha-svg-icon";
@customElement("ha-icon-button-next")
export class HaIconButtonNext extends LitElement {
@@ -28,11 +29,12 @@ export class HaIconButtonNext extends LitElement {
protected render(): TemplateResult {
return html`
<ha-icon-button
<mwc-icon-button
.disabled=${this.disabled}
.label=${this.label || this.hass?.localize("ui.common.next") || "Next"}
.path=${this._icon}
></ha-icon-button>
>
<ha-svg-icon .path=${this._icon}></ha-svg-icon>
</mwc-icon-button>
`;
}
}

View File

@@ -1,8 +1,9 @@
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiChevronLeft, mdiChevronRight } from "@mdi/js";
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { HomeAssistant } from "../types";
import "./ha-icon-button";
import "./ha-svg-icon";
@customElement("ha-icon-button-prev")
export class HaIconButtonPrev extends LitElement {
@@ -28,11 +29,12 @@ export class HaIconButtonPrev extends LitElement {
protected render(): TemplateResult {
return html`
<ha-icon-button
<mwc-icon-button
.disabled=${this.disabled}
.label=${this.label || this.hass?.localize("ui.common.back") || "Back"}
.path=${this._icon}
></ha-icon-button>
>
<ha-svg-icon .path=${this._icon}></ha-svg-icon>
</mwc-icon-button>
`;
}
}

View File

@@ -1,36 +1,25 @@
import "@material/mwc-icon-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import "./ha-svg-icon";
import "./ha-icon";
@customElement("ha-icon-button")
export class HaIconButton extends LitElement {
@property({ type: Boolean, reflect: true }) disabled = false;
// SVG icon path (if you need a non SVG icon instead, use the provided slot to pass an <ha-icon> in)
@property({ type: String }) path?: string;
@property({ type: String }) icon = "";
// Label that is used for ARIA support and as tooltip
@property({ type: String }) label = "";
@property({ type: Boolean }) hideTitle = false;
static shadowRootOptions: ShadowRootInit = {
mode: "open",
delegatesFocus: true,
};
protected render(): TemplateResult {
// Note: `ariaLabel` required despite the `mwc-icon-button` docs saying `label` should be enough
return html`
<mwc-icon-button
.ariaLabel=${this.label}
.title=${this.hideTitle ? "" : this.label}
.disabled=${this.disabled}
>
${this.path
? html`<ha-svg-icon .path=${this.path}></ha-svg-icon>`
: html`<slot></slot>`}
<mwc-icon-button .label=${this.label} .disabled=${this.disabled}>
<ha-icon .icon=${this.icon}></ha-icon>
</mwc-icon-button>
`;
}
@@ -48,6 +37,9 @@ export class HaIconButton extends LitElement {
--mdc-theme-on-primary: currentColor;
--mdc-theme-text-disabled-on-light: var(--disabled-text-color);
}
ha-icon {
--ha-icon-display: inline;
}
`;
}
}

View File

@@ -1,142 +0,0 @@
import "@polymer/paper-input/paper-input";
import { mdiCheck } from "@mdi/js";
import { css, html, LitElement, TemplateResult } from "lit";
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { PolymerChangedEvent } from "../polymer-types";
import "./ha-icon";
import iconList from "../../build/mdi/iconList.json";
const mdiIconList = iconList.map((icon) => `mdi:${icon}`);
// eslint-disable-next-line lit/prefer-static-styles
const rowRenderer: ComboBoxLitRenderer<string> = (item) => html`<style>
paper-icon-item {
padding: 0;
margin: -8px;
}
#content {
display: flex;
align-items: center;
}
ha-svg-icon {
padding-left: 2px;
color: var(--secondary-text-color);
}
:host(:not([selected])) ha-svg-icon {
display: none;
}
:host([selected]) paper-icon-item {
margin-left: 0;
}
</style>
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
<paper-icon-item>
<ha-icon .icon=${item} slot="item-icon"></ha-icon>
<paper-item-body> ${item} </paper-item-body>
</paper-icon-item>`;
@customElement("ha-icon-picker")
export class HaIconPicker extends LitElement {
@property() public value?: string;
@property() public label?: string;
@property() public placeholder?: string;
@property({ attribute: "error-message" }) public errorMessage?: string;
@property({ type: Boolean }) public disabled = false;
@query("vaadin-combo-box-light", true) private comboBox!: HTMLElement;
@property({ type: Boolean }) private _opened = false;
protected render(): TemplateResult {
return html`
<vaadin-combo-box-light
item-value-path="icon"
item-label-path="icon"
.value=${this._value}
.allowCustomValue=${true}
.filteredItems=${[]}
${comboBoxRenderer(rowRenderer)}
@opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged}
@filter-changed=${this._filterChanged}
>
<paper-input
.label=${this.label}
.placeholder=${this.placeholder}
.disabled=${this.disabled}
class="input"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
>
${!this._opened && (this._value || this.placeholder)
? html`
<ha-icon .icon=${this._value || this.placeholder} slot="suffix">
</ha-icon>
`
: ""}
</paper-input>
</vaadin-combo-box-light>
`;
}
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private _valueChanged(ev: PolymerChangedEvent<string>) {
this._setValue(ev.detail.value);
}
private _setValue(value: string) {
this.value = value;
fireEvent(
this,
"value-changed",
{ value },
{
bubbles: false,
composed: false,
}
);
}
private _filterChanged(ev: CustomEvent): void {
const filterString = ev.detail.value.toLowerCase();
const characterCount = filterString.length;
if (characterCount >= 2) {
const filteredItems = mdiIconList.filter((icon) =>
icon.toLowerCase().includes(filterString)
);
if (filteredItems.length > 0) {
(this.comboBox as any).filteredItems = filteredItems;
} else {
(this.comboBox as any).filteredItems = [filterString];
}
} else {
(this.comboBox as any).filteredItems = [];
}
}
private get _value() {
return this.value || "";
}
static get styles() {
return css`
ha-icon {
position: absolute;
bottom: 2px;
right: 0;
}
`;
}
}

View File

@@ -361,10 +361,7 @@ const mdiDeprecatedIcons: DeprecatedIcon = {
const chunks: Chunks = {};
// Supervisor doesn't use icons, and should not update/downgrade the icon DB.
if (!__SUPERVISOR__) {
checkCacheVersion();
}
checkCacheVersion();
const debouncedWriteCache = debounce(() => writeCache(chunks), 2000);

View File

@@ -8,8 +8,13 @@ import {
} from "lit";
import { property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import "./ha-icon";
class HaLabelBadge extends LitElement {
@property() public value?: string;
@property() public icon?: string;
@property() public label?: string;
@property() public description?: string;
@@ -20,8 +25,20 @@ class HaLabelBadge extends LitElement {
return html`
<div class="badge-container">
<div class="label-badge" id="badge">
<div class="value">
<slot></slot>
<div
class=${classMap({
value: true,
big: Boolean(this.value && this.value.length > 4),
})}
>
<slot>
${this.icon && !this.value && !this.image
? html`<ha-icon .icon=${this.icon}></ha-icon>`
: ""}
${this.value && !this.image
? html`<span>${this.value}</span>`
: ""}
</slot>
</div>
${this.label
? html`
@@ -37,7 +54,7 @@ class HaLabelBadge extends LitElement {
: ""}
</div>
${this.description
? html`<div class="title">${this.description}</div>`
? html` <div class="title">${this.description}</div> `
: ""}
</div>
`;
@@ -70,15 +87,14 @@ class HaLabelBadge extends LitElement {
background-size: cover;
transition: border 0.3s ease-in-out;
}
.label-badge .label.big span {
font-size: 90%;
padding: 10% 12% 7% 12%; /* push smaller text a bit down to center vertically */
}
.label-badge .value {
font-size: 90%;
overflow: hidden;
text-overflow: ellipsis;
}
.label-badge .value.big {
font-size: 70%;
}
.label-badge .label {
position: absolute;
bottom: -1em;
@@ -103,6 +119,10 @@ class HaLabelBadge extends LitElement {
transition: background-color 0.3s ease-in-out;
text-transform: var(--ha-label-badge-label-text-transform, uppercase);
}
.label-badge .label.big span {
font-size: 90%;
padding: 10% 12% 7% 12%; /* push smaller text a bit down to center vertically */
}
.badge-container .title {
margin-top: 1em;
font-size: var(--ha-label-badge-title-font-size, 0.9em);

View File

@@ -1,3 +1,4 @@
import "@material/mwc-icon-button";
import { mdiMenu } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
@@ -6,7 +7,7 @@ import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain";
import { subscribeNotifications } from "../data/persistent_notification";
import { HomeAssistant } from "../types";
import "./ha-icon-button";
import "./ha-svg-icon";
@customElement("ha-menu-button")
class HaMenuButton extends LitElement {
@@ -49,11 +50,12 @@ class HaMenuButton extends LitElement {
(entityId) => computeDomain(entityId) === "configurator"
));
return html`
<ha-icon-button
.label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
.path=${mdiMenu}
<mwc-icon-button
aria-label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
@click=${this._toggleMenu}
></ha-icon-button>
>
<ha-svg-icon .path=${mdiMenu}></ha-svg-icon>
</mwc-icon-button>
${hasNotifications ? html` <div class="dot"></div> ` : ""}
`;
}

View File

@@ -1,3 +1,4 @@
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiImagePlus } from "@mdi/js";
import "@polymer/paper-input/paper-input-container";
import { html, LitElement, TemplateResult } from "lit";
@@ -12,6 +13,7 @@ import {
import { HomeAssistant } from "../types";
import "./ha-circular-progress";
import "./ha-file-upload";
import "./ha-svg-icon";
@customElement("ha-picture-upload")
export class HaPictureUpload extends LitElement {
@@ -32,7 +34,6 @@ export class HaPictureUpload extends LitElement {
public render(): TemplateResult {
return html`
<ha-file-upload
.hass=${this.hass}
.icon=${mdiImagePlus}
.label=${this.label ||
this.hass.localize("ui.components.picture-upload.label")}

View File

@@ -1,7 +1,6 @@
import { PropertyValues, ReactiveElement } from "lit";
import { customElement, property } from "lit/decorators";
import { relativeTime } from "../common/datetime/relative_time";
import { capitalizeFirstLetter } from "../common/string/capitalize-first-letter";
import type { HomeAssistant } from "../types";
@customElement("ha-relative-time")
@@ -10,8 +9,6 @@ class HaRelativeTime extends ReactiveElement {
@property({ attribute: false }) public datetime?: string | Date;
@property({ type: Boolean }) public capitalize = false;
private _interval?: number;
public disconnectedCallback(): void {
@@ -58,10 +55,7 @@ class HaRelativeTime extends ReactiveElement {
if (!this.datetime) {
this.innerHTML = this.hass.localize("ui.components.relative_time.never");
} else {
const relTime = relativeTime(new Date(this.datetime), this.hass.locale);
this.innerHTML = this.capitalize
? capitalizeFirstLetter(relTime)
: relTime;
this.innerHTML = relativeTime(new Date(this.datetime), this.hass.locale);
}
}
}

View File

@@ -34,7 +34,7 @@ export class HaDeviceSelector extends LitElement {
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.deviceFilter=${this._filterDevices}
.deviceFilter=${(device) => this._filterDevices(device)}
.includeDeviceClasses=${this.selector.device.entity?.device_class
? [this.selector.device.entity.device_class]
: undefined}
@@ -46,7 +46,7 @@ export class HaDeviceSelector extends LitElement {
></ha-device-picker>`;
}
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
private _filterDevices(device: DeviceRegistryEntry): boolean {
if (
this.selector.device?.manufacturer &&
device.manufacturer !== this.selector.device.manufacturer
@@ -70,7 +70,7 @@ export class HaDeviceSelector extends LitElement {
}
}
return true;
};
}
private async _loadConfigEntries() {
this._configEntries = (await getConfigEntries(this.hass)).filter(

View File

@@ -27,7 +27,7 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.entityFilter=${this._filterEntities}
.entityFilter=${(entity) => this._filterEntities(entity)}
.disabled=${this.disabled}
allow-custom-entity
></ha-entity-picker>`;
@@ -48,7 +48,7 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
];
}
private _filterEntities = (entity: HassEntity): boolean => {
private _filterEntities(entity: HassEntity): boolean {
if (this.selector.entity?.domain) {
if (computeStateDomain(entity) !== this.selector.entity.domain) {
return false;
@@ -72,7 +72,7 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
}
}
return true;
};
}
}
declare global {

View File

@@ -69,9 +69,10 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
return html`<ha-target-picker
.hass=${this.hass}
.value=${this.value}
.deviceFilter=${this._filterDevices}
.entityRegFilter=${this._filterRegEntities}
.entityFilter=${this._filterEntities}
.deviceFilter=${(device) => this._filterDevices(device)}
.entityRegFilter=${(entity: EntityRegistryEntry) =>
this._filterRegEntities(entity)}
.entityFilter=${(entity: HassEntity) => this._filterEntities(entity)}
.includeDeviceClasses=${this.selector.target.entity?.device_class
? [this.selector.target.entity.device_class]
: undefined}
@@ -82,7 +83,7 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
></ha-target-picker>`;
}
private _filterEntities = (entity: HassEntity): boolean => {
private _filterEntities(entity: HassEntity): boolean {
if (
this.selector.target.entity?.integration ||
this.selector.target.device?.integration
@@ -97,18 +98,18 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
}
}
return true;
};
}
private _filterRegEntities = (entity: EntityRegistryEntry): boolean => {
private _filterRegEntities(entity: EntityRegistryEntry): boolean {
if (this.selector.target.entity?.integration) {
if (entity.platform !== this.selector.target.entity.integration) {
return false;
}
}
return true;
};
}
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
private _filterDevices(device: DeviceRegistryEntry): boolean {
if (
this.selector.target.device?.manufacturer &&
device.manufacturer !== this.selector.target.device.manufacturer
@@ -134,7 +135,7 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
}
}
return true;
};
}
private async _loadConfigEntries() {
this._configEntries = (await getConfigEntries(this.hass)).filter(

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