Compare commits

..

1 Commits

Author SHA1 Message Date
Zack
2aeea10fc1 QUick Fix from a review 2022-02-10 14:15:45 -06:00
470 changed files with 16584 additions and 14970 deletions

View File

@@ -16,9 +16,6 @@
"runem.lit-plugin", "runem.lit-plugin",
"ms-python.vscode-pylance" "ms-python.vscode-pylance"
], ],
"containerEnv": {
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
},
"settings": { "settings": {
"terminal.integrated.shell.linux": "/bin/bash", "terminal.integrated.shell.linux": "/bin/bash",
"files.eol": "\n", "files.eol": "\n",

View File

@@ -10,18 +10,10 @@ env:
NODE_VERSION: 14 NODE_VERSION: 14
NODE_OPTIONS: --max_old_space_size=6144 NODE_OPTIONS: --max_old_space_size=6144
# Set default workflow permissions
# All scopes not mentioned here are set to no access
# https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token
permissions:
actions: none
jobs: jobs:
release: release:
name: Release name: Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: write # Required to upload release assets
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v2 uses: actions/checkout@v2
@@ -55,13 +47,6 @@ jobs:
script/release script/release
- name: Upload release assets
uses: softprops/action-gh-release@v0.1.14
with:
files: |
dist/*.whl
dist/*.tar.gz
wheels-init: wheels-init:
name: Init wheels build name: Init wheels build
needs: release needs: release

631
.yarn/releases/yarn-3.0.2.cjs vendored Executable file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -6,4 +6,4 @@ plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools" spec: "@yarnpkg/plugin-interactive-tools"
yarnPath: .yarn/releases/yarn-3.2.0.cjs yarnPath: .yarn/releases/yarn-3.0.2.cjs

View File

@@ -2,7 +2,7 @@
This is the repository for the official [Home Assistant](https://home-assistant.io) frontend. This is the repository for the official [Home Assistant](https://home-assistant.io) frontend.
[![Screenshot of the frontend](https://raw.githubusercontent.com/home-assistant/frontend/master/docs/screenshot.png)](https://demo.home-assistant.io/) [![Screenshot of the frontend](https://raw.githubusercontent.com/home-assistant/home-assistant-polymer/master/docs/screenshot.png)](https://demo.home-assistant.io/)
- [View demo of Home Assistant](https://demo.home-assistant.io/) - [View demo of Home Assistant](https://demo.home-assistant.io/)
- [More information about Home Assistant](https://home-assistant.io) - [More information about Home Assistant](https://home-assistant.io)

View File

@@ -33,10 +33,6 @@ module.exports.emptyPackages = ({ latestBuild, isHassioBuild }) =>
require.resolve( require.resolve(
path.resolve(paths.polymer_dir, "src/components/ha-icon.ts") path.resolve(paths.polymer_dir, "src/components/ha-icon.ts")
), ),
isHassioBuild &&
require.resolve(
path.resolve(paths.polymer_dir, "src/components/ha-icon-picker.ts")
),
].filter(Boolean); ].filter(Boolean);
module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({ module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({

View File

@@ -3,7 +3,7 @@
const gulp = require("gulp"); const gulp = require("gulp");
const fs = require("fs"); const fs = require("fs");
const path = require("path"); const path = require("path");
const { marked } = require("marked"); const marked = require("marked");
const glob = require("glob"); const glob = require("glob");
const yaml = require("js-yaml"); const yaml = require("js-yaml");

View File

@@ -7,7 +7,7 @@ const source = require("vinyl-source-stream");
const vinylBuffer = require("vinyl-buffer"); const vinylBuffer = require("vinyl-buffer");
const gulp = require("gulp"); const gulp = require("gulp");
const fs = require("fs"); const fs = require("fs");
const flatmap = require("gulp-flatmap"); const foreach = require("gulp-foreach");
const merge = require("gulp-merge-json"); const merge = require("gulp-merge-json");
const rename = require("gulp-rename"); const rename = require("gulp-rename");
const transform = require("gulp-json-transform"); const transform = require("gulp-json-transform");
@@ -183,7 +183,7 @@ gulp.task("build-merged-translations", () =>
}) })
.pipe(transform((data, file) => lokaliseTransform(data, data, file))) .pipe(transform((data, file) => lokaliseTransform(data, data, file)))
.pipe( .pipe(
flatmap((stream, file) => { foreach((stream, file) => {
// For each language generate a merged json file. It begins with the master // For each language generate a merged json file. It begins with the master
// translation as a failsafe for untranslated strings, and merges all parent // translation as a failsafe for untranslated strings, and merges all parent
// tags into one file for each specific subtag // tags into one file for each specific subtag

View File

@@ -1,5 +1,5 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import { LovelaceConfig } from "../../../../src/data/lovelace"; import { LovelaceConfig } from "../../../../src/data/lovelace";
import { Lovelace } from "../../../../src/panels/lovelace/types"; import { Lovelace } from "../../../../src/panels/lovelace/types";
@@ -20,8 +20,6 @@ class HcLovelace extends LitElement {
@property() public urlPath: string | null = null; @property() public urlPath: string | null = null;
@query("hui-view") private _huiView?: HTMLElement;
protected render(): TemplateResult { protected render(): TemplateResult {
const index = this._viewIndex; const index = this._viewIndex;
if (index === undefined) { if (index === undefined) {
@@ -80,12 +78,12 @@ class HcLovelace extends LitElement {
this.lovelaceConfig.background; this.lovelaceConfig.background;
if (configBackground) { if (configBackground) {
this._huiView!.style.setProperty( (this.shadowRoot!.querySelector(
"hui-view"
) as HTMLElement)!.style.setProperty(
"--lovelace-background", "--lovelace-background",
configBackground configBackground
); );
} else {
this._huiView!.style.removeProperty("--lovelace-background");
} }
} }
} }
@@ -118,9 +116,6 @@ class HcLovelace extends LitElement {
:host > * { :host > * {
flex: 1; flex: 1;
} }
hui-view {
background: var(--lovelace-background, var(--primary-background-color));
}
`; `;
} }
} }

View File

@@ -1,3 +1,4 @@
import "web-animations-js/web-animations-next-lite.min";
import "../../../src/resources/ha-style"; import "../../../src/resources/ha-style";
import "../../../src/resources/roboto"; import "../../../src/resources/roboto";
import "./layout/hc-lovelace"; import "./layout/hc-lovelace";

View File

@@ -2,3 +2,8 @@ import "../../src/resources/ha-style";
import "../../src/resources/roboto"; import "../../src/resources/roboto";
import "../../src/resources/safari-14-attachshadow-patch"; import "../../src/resources/safari-14-attachshadow-patch";
import "./ha-demo"; import "./ha-demo";
/* polyfill for paper-dropdown */
setTimeout(() => {
import("web-animations-js/web-animations-next-lite.min");
}, 1000);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -23,7 +23,7 @@ if [[ "${PULL_REQUEST}" == "true" ]]; then
createStatus "pending" "Building design preview" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID" createStatus "pending" "Building design preview" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID"
gulp build-gallery gulp build-gallery
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
createStatus "success" "Build complete" "$DEPLOY_PRIME_URL" createStatus "success" "Build complete" "$DEPLOY_URL"
else else
createStatus "error" "Build failed" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID" createStatus "error" "Build failed" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID"
fi fi

View File

@@ -20,6 +20,7 @@ module.exports = [
"editor-trigger", "editor-trigger",
"editor-condition", "editor-condition",
"editor-action", "editor-action",
"selectors",
"trace", "trace",
"trace-timeline", "trace-timeline",
], ],
@@ -36,17 +37,12 @@ module.exports = [
category: "misc", category: "misc",
header: "Miscelaneous", header: "Miscelaneous",
}, },
{
category: "brand",
header: "Brand",
},
{ {
category: "user-test", category: "user-test",
header: "Users", header: "User Tests",
pages: ["user-types", "configuration-menu"],
}, },
{ {
category: "design.home-assistant.io", category: "design.home-assistant.io",
header: "About", header: "Design Documentation",
}, },
]; ];

View File

@@ -78,9 +78,6 @@ class DemoCards extends LitElement {
ha-formfield { ha-formfield {
margin-right: 16px; margin-right: 16px;
} }
#container {
background-color: var(--primary-background-color);
}
`; `;
} }

View File

@@ -12,14 +12,7 @@ class PageDescription extends HaMarkdown {
if (!PAGES[this.page].description) { if (!PAGES[this.page].description) {
return html``; return html``;
} }
return html` return html`
<div class="heading">
<div class="title">
${PAGES[this.page].metadata.title || this.page.split("/")[1]}
</div>
<div class="subtitle">${PAGES[this.page].metadata.subtitle}</div>
</div>
${until( ${until(
PAGES[this.page] PAGES[this.page]
.description() .description()
@@ -32,22 +25,9 @@ class PageDescription extends HaMarkdown {
static styles = [ static styles = [
HaMarkdown.styles, HaMarkdown.styles,
css` css`
.heading {
padding: 16px;
border-bottom: 1px solid var(--secondary-background-color);
}
.title {
font-size: 42px;
line-height: 56px;
padding-bottom: 8px;
}
.subtitle {
font-size: 18px;
line-height: 24px;
}
.root { .root {
max-width: 800px; max-width: 800px;
margin: 16px auto; margin: 0 auto;
} }
.root > *:first-child { .root > *:first-child {
margin-top: 0; margin-top: 0;

View File

@@ -5,7 +5,6 @@ import { html, css, LitElement, PropertyValues } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import "../../src/components/ha-icon-button"; import "../../src/components/ha-icon-button";
import "../../src/managers/notification-manager"; import "../../src/managers/notification-manager";
import "../../src/components/ha-expansion-panel";
import { haStyle } from "../../src/resources/styles"; import { haStyle } from "../../src/resources/styles";
import { PAGES, SIDEBAR } from "../build/import-pages"; import { PAGES, SIDEBAR } from "../build/import-pages";
import { dynamicElement } from "../../src/common/dom/dynamic-element-directive"; import { dynamicElement } from "../../src/common/dom/dynamic-element-directive";
@@ -45,10 +44,6 @@ class HaGallery extends LitElement {
for (const page of group.pages!) { for (const page of group.pages!) {
const key = `${group.category}/${page}`; const key = `${group.category}/${page}`;
const active = this._page === key; const active = this._page === key;
if (!(key in PAGES)) {
console.error("Undefined page referenced in sidebar.js:", key);
continue;
}
const title = PAGES[key].metadata.title || page; const title = PAGES[key].metadata.title || page;
links.push(html` links.push(html`
<a ?active=${active} href=${`#${group.category}/${page}`}>${title}</a> <a ?active=${active} href=${`#${group.category}/${page}`}>${title}</a>
@@ -58,9 +53,10 @@ class HaGallery extends LitElement {
sidebar.push( sidebar.push(
group.header group.header
? html` ? html`
<ha-expansion-panel .header=${group.header}> <details>
<summary class="section">${group.header}</summary>
${links} ${links}
</ha-expansion-panel> </details>
` `
: links : links
); );
@@ -96,34 +92,27 @@ class HaGallery extends LitElement {
${dynamicElement(`demo-${this._page.replace("/", "-")}`)} ${dynamicElement(`demo-${this._page.replace("/", "-")}`)}
</div> </div>
<div class="page-footer"> <div class="page-footer">
<div class="header">Help us to improve our documentation</div> ${PAGES[this._page].description ||
<div class="secondary"> Object.keys(PAGES[this._page].metadata).length > 0
Suggest an edit to this page, or provide/view feedback for this ? html`
page. <a
</div> href=${`${GITHUB_DEMO_URL}${this._page}.markdown`}
<div> target="_blank"
${PAGES[this._page].description || >
Object.keys(PAGES[this._page].metadata).length > 0 Edit text
? html` </a>
<a `
href=${`${GITHUB_DEMO_URL}${this._page}.markdown`} : ""}
target="_blank" ${PAGES[this._page].demo
> ? html`
Edit text <a
</a> href=${`${GITHUB_DEMO_URL}${this._page}.ts`}
` target="_blank"
: ""} >
${PAGES[this._page].demo Edit demo
? html` </a>
<a `
href=${`${GITHUB_DEMO_URL}${this._page}.ts`} : ""}
target="_blank"
>
Edit demo
</a>
`
: ""}
</div>
</div> </div>
</div> </div>
</mwc-drawer> </mwc-drawer>
@@ -197,16 +186,27 @@ class HaGallery extends LitElement {
padding: 4px; padding: 4px;
} }
.sidebar details {
margin-top: 1em;
margin-left: 1em;
}
.sidebar summary {
cursor: pointer;
font-weight: bold;
margin-bottom: 8px;
}
.sidebar a { .sidebar a {
color: var(--primary-text-color); color: var(--primary-text-color);
display: block; display: block;
padding: 12px; padding: 4px 12px;
text-decoration: none; text-decoration: none;
position: relative; position: relative;
} }
.sidebar a[active]::before { .sidebar a[active]::before {
border-radius: 12px; border-radius: 4px;
position: absolute; position: absolute;
top: 0; top: 0;
right: 2px; right: 2px;
@@ -237,32 +237,14 @@ class HaGallery extends LitElement {
.page-footer { .page-footer {
text-align: center; text-align: center;
margin: 16px; margin: 16px 0;
padding: 16px; padding-top: 16px;
border-radius: 12px; border-top: 1px solid rgba(0, 0, 0, 0.12);
background-color: var(--primary-background-color);
}
.page-footer div {
margin-top: 4px;
}
.page-footer .header {
font-size: 16px;
font-weight: 500;
line-height: 28px;
text-align: center;
}
.page-footer .secondary {
line-height: 23px;
text-align: center;
} }
.page-footer a { .page-footer a {
display: inline-block; display: inline-block;
margin: 0 8px; margin: 0 8px;
text-decoration: none;
} }
`, `,
]; ];

View File

@@ -3,20 +3,10 @@ import { html, css, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "../../../../src/components/ha-card"; import "../../../../src/components/ha-card";
import { describeAction } from "../../../../src/data/script_i18n"; import { describeAction } from "../../../../src/data/script_i18n";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass"; import { provideHass } from "../../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../../src/types"; import { HomeAssistant } from "../../../../src/types";
const ENTITIES = [ const actions = [
getEntity("scene", "kitchen_morning", "scening", {
friendly_name: "Kitchen Morning",
}),
getEntity("media_player", "kitchen", "playing", {
friendly_name: "Sonos Kitchen",
}),
];
const ACTIONS = [
{ wait_template: "{{ true }}", alias: "Something with an alias" }, { wait_template: "{{ true }}", alias: "Something with an alias" },
{ delay: "0:05" }, { delay: "0:05" },
{ wait_template: "{{ true }}" }, { wait_template: "{{ true }}" },
@@ -29,20 +19,8 @@ const ACTIONS = [
device_id: "abcdefgh", device_id: "abcdefgh",
domain: "plex", domain: "plex",
entity_id: "media_player.kitchen", entity_id: "media_player.kitchen",
type: "turn_on",
}, },
{ scene: "scene.kitchen_morning" }, { scene: "scene.kitchen_morning" },
{
service: "scene.turn_on",
target: { entity_id: "scene.kitchen_morning" },
metadata: {},
},
{
service: "media_player.play_media",
target: { entity_id: "media_player.kitchen" },
data: { media_content_id: "", media_content_type: "" },
metadata: { title: "Happy Song" },
},
{ {
wait_for_trigger: [ wait_for_trigger: [
{ {
@@ -74,7 +52,7 @@ export class DemoAutomationDescribeAction extends LitElement {
} }
return html` return html`
<ha-card header="Actions"> <ha-card header="Actions">
${ACTIONS.map( ${actions.map(
(conf) => html` (conf) => html`
<div class="action"> <div class="action">
<span>${describeAction(this.hass, conf as any)}</span> <span>${describeAction(this.hass, conf as any)}</span>
@@ -90,7 +68,6 @@ export class DemoAutomationDescribeAction extends LitElement {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
const hass = provideHass(this); const hass = provideHass(this);
hass.updateTranslations(null, "en"); hass.updateTranslations(null, "en");
hass.addEntities(ENTITIES);
} }
static get styles() { static get styles() {

View File

@@ -14,7 +14,7 @@ import { HaDelayAction } from "../../../../src/panels/config/automation/action/t
import { HaDeviceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-device_id"; import { HaDeviceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-device_id";
import { HaEventAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-event"; import { HaEventAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-event";
import { HaRepeatAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-repeat"; import { HaRepeatAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-repeat";
import { HaSceneAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-activate_scene"; import { HaSceneAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-scene";
import { HaServiceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-service"; import { HaServiceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-service";
import { HaWaitForTriggerAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_for_trigger"; import { HaWaitForTriggerAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_for_trigger";
import { HaWaitAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_template"; import { HaWaitAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_template";

View File

@@ -0,0 +1,3 @@
---
title: Selectors
---

View File

@@ -0,0 +1,102 @@
/* eslint-disable lit/no-template-arrow */
import { LitElement, TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../../src/types";
import "../../components/demo-black-white-row";
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
import "../../../../src/panels/config/automation/trigger/ha-automation-trigger";
import { Selector } from "../../../../src/data/selector";
import "../../../../src/components/ha-selector/ha-selector";
const SCHEMAS: { name: string; selector: Selector }[] = [
{ name: "Addon", selector: { addon: {} } },
{ name: "Entity", selector: { entity: {} } },
{ name: "Device", selector: { device: {} } },
{ name: "Area", selector: { area: {} } },
{ name: "Target", selector: { target: {} } },
{
name: "Number",
selector: {
number: {
min: 0,
max: 10,
},
},
},
{ name: "Boolean", selector: { boolean: {} } },
{ name: "Time", selector: { time: {} } },
{ name: "Action", selector: { action: {} } },
{ name: "Text", selector: { text: { multiline: false } } },
{ name: "Text Multiline", selector: { text: { multiline: true } } },
{ name: "Object", selector: { object: {} } },
{
name: "Select",
selector: {
select: {
options: ["Everyone Home", "Some Home", "All gone"],
},
},
},
];
@customElement("demo-automation-selectors")
class DemoHaSelector extends LitElement {
@state() private hass!: HomeAssistant;
private data: any = SCHEMAS.map(() => undefined);
constructor() {
super();
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en");
mockEntityRegistry(hass);
mockDeviceRegistry(hass);
mockAreaRegistry(hass);
mockHassioSupervisor(hass);
}
protected render(): TemplateResult {
const valueChanged = (ev) => {
const sampleIdx = ev.target.sampleIdx;
this.data[sampleIdx] = ev.detail.value;
this.requestUpdate();
};
return html`
${SCHEMAS.map(
(info, sampleIdx) => html`
<demo-black-white-row
.title=${info.name}
.value=${{ selector: info.selector, data: this.data[sampleIdx] }}
>
${["light", "dark"].map(
(slot) =>
html`
<ha-selector
slot=${slot}
.hass=${this.hass}
.selector=${info.selector}
.label=${info.name}
.value=${this.data[sampleIdx]}
.sampleIdx=${sampleIdx}
@value-changed=${valueChanged}
></ha-selector>
`
)}
</demo-black-white-row>
`
)}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-automation-selectors": DemoHaSelector;
}
}

View File

@@ -1,34 +0,0 @@
---
title: "Logo"
---
![Using our logo](/images/using-our-logo.png)
# Using our logo
As a community, we are proud of our logo. Follow these guidelines to ensure it always looks its best. Our logo follows Google's material design spec and uses the blue interface color.
[Download Logo](https://github.com/home-assistant/assets/tree/master/logo)
![Logo](/images/logo.png)
## Using the icon
Our icon is a shorter and most used version of our logo. The icon can exist without the wordmark, the wordmark should never exist without the icon.
![Logo variants](/images/logo-variants.png)
## Using the right variant
The pretty blue logo with a background shadow, pictured top left, is our primary logo. It should only be used with black, white, and non-duotone photography.
When needed you can use our logo without a shadow, as seen as the second variant.
The outlined logo should only be used on packaging.
## Exclusion zone
The logo needs some personal space. It's exclusion zone is equal to a quarter the height of the icon.
![Clearspace](/images/clearspace.png)

View File

@@ -1,41 +0,0 @@
---
title: "Our story"
---
## Open source home automation that puts local control and privacy first
Home Assistant is a free and open-source software for home automation that is designed to be the central control system for smart home devices with a focus on local control and privacy. It can be accessed via a web-based user interface, via apps for Android and iOS, or using voice commands via a supported virtual assistant like Google Assistant and Amazon Alexa.
IoT devices and services are supported by modular support for controlling proprietary ecosystems if they provide public access via an Open API for third-party integrations and protocols like Bluetooth, MQTT, Zigbee, and Z-Wave, After the Home Assistant software application is installed as a computer appliance it will act as a central control system for home automation. Information from all entities it sees can be used and controlled from within scripts trigger automations using scheduling and "blueprint" subroutines, e.g. for controlling lighting, climate, entertainment systems, and appliances.
# Open Home
The Open Home is our vision for the smart home. It defines the values that we put at the heart of every decision we make at Home Assistant. Its woven into our architecture, licensing, community, and everything else.
The Open Home is about privacy, choice, and durability.
## Privacy
Your home should be your safe space. A place where you can be your true self without having to bother about what the world thinks of you. A place where you dont need to act differently to avoid an algorithm categorizing your behavior. Privacy for the Open Home means that devices need to work locally. No one else needs to know if you turn on a light bulb or change the thermostat.
It is okay for a product to offer a cloud connection, but it should be extra and opt-in.
## Choice
Devices in your home gather data about themselves and their surroundings. Your data. Vendors shouldnt be able to limit your access to your data or limit the interoperability of your devices with the rest of your smart home.
Choice for the Open Home means that devices need to make the gathered data available through local APIs. This avoids vendor lock-in and allows users to create their own smart home with devices from different manufacturers.
## Durability
If there is one thing that technology firms are very good at, it is launching new products. However, maintaining the products and making sure they keep working is an afterthought for most. The result is that vendors can decide to no longer support your device, crippling its features or even preventing it from working at all. As we install more and more devices in our home, durability is becoming more and more important. We shouldnt have to buy everything new every couple of years because the manufacturer decided to move on.
Durability for the Open Home means that devices are designed and built to keep working. Not just this year, but for the next decade.
# Our history
The project was started as a Python application by Paulus Schoutsen in September 2013 and first published publicly on GitHub in November 2013. In July 2017, a managed operating system called Hass.io was initially introduced to make it easier use to use Home Assistant on single-board computers like the Raspberry Pi series. Its bundled "supervisor" management system allowed users to manage, backup, and update the local installation and introduced the option to extend the functionality of the software with add-ons.
An optional subscription service was introduced in December 2017 for $5/month to solve the complexities associated with secured remote access, as well as linking to Amazon Alexa and Google Assistant. Nabu Casa, Inc. was formed in September 2018 to take over the subscription service. The company's funding is based solely on revenue from the subscription service. It is used to finance the project's infrastructure and to pay for full-time employees contributing to the project.
In January 2020, branding was adjusted to make it easier to refer to different parts of the project. The main piece of software was renamed to Home Assistant Core, while the full suite of software with the embedded operating system and bundled "supervisor" management system was renamed to Home Assistant.

View File

@@ -1,6 +1,5 @@
--- ---
title: Alerts title: Alerts
subtitle: An alert displays a short, important message in a way that attracts the user's attention without interrupting the user's task.
--- ---
# Alert `<ha-alert>` # Alert `<ha-alert>`

View File

@@ -12,98 +12,6 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor"; import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
import { provideHass } from "../../../../src/fake_data/provide_hass"; import { provideHass } from "../../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../../src/types"; import { HomeAssistant } from "../../../../src/types";
import { getEntity } from "../../../../src/fake_data/entity";
const ENTITIES = [
getEntity("alarm_control_panel", "alarm", "disarmed", {
friendly_name: "Alarm",
}),
getEntity("media_player", "livingroom", "playing", {
friendly_name: "Livingroom",
}),
getEntity("media_player", "lounge", "idle", {
friendly_name: "Lounge",
supported_features: 444983,
}),
getEntity("light", "bedroom", "on", {
friendly_name: "Bedroom",
}),
getEntity("switch", "coffee", "off", {
friendly_name: "Coffee",
}),
];
const DEVICES = [
{
area_id: "bedroom",
configuration_url: null,
config_entries: ["config_entry_1"],
connections: [],
disabled_by: null,
entry_type: null,
id: "device_1",
identifiers: [["demo", "volume1"] as [string, string]],
manufacturer: null,
model: null,
name_by_user: null,
name: "Dishwasher",
sw_version: null,
hw_version: null,
via_device_id: null,
},
{
area_id: "backyard",
configuration_url: null,
config_entries: ["config_entry_2"],
connections: [],
disabled_by: null,
entry_type: null,
id: "device_2",
identifiers: [["demo", "pwm1"] as [string, string]],
manufacturer: null,
model: null,
name_by_user: null,
name: "Lamp",
sw_version: null,
hw_version: null,
via_device_id: null,
},
{
area_id: null,
configuration_url: null,
config_entries: ["config_entry_3"],
connections: [],
disabled_by: null,
entry_type: null,
id: "device_3",
identifiers: [["demo", "pwm1"] as [string, string]],
manufacturer: null,
model: null,
name_by_user: "User name",
name: "Technical name",
sw_version: null,
hw_version: null,
via_device_id: null,
},
];
const AREAS = [
{
area_id: "backyard",
name: "Backyard",
picture: null,
},
{
area_id: "bedroom",
name: "Bedroom",
picture: null,
},
{
area_id: "livingroom",
name: "Livingroom",
picture: null,
},
];
const SCHEMAS: { const SCHEMAS: {
title: string; title: string;
@@ -128,10 +36,6 @@ const SCHEMAS: {
text_multiline: "Text Multiline", text_multiline: "Text Multiline",
object: "Object", object: "Object",
select: "Select", select: "Select",
icon: "Icon",
media: "Media",
location: "Location",
entities: "Entities",
}, },
schema: [ schema: [
{ name: "addon", selector: { addon: {} } }, { name: "addon", selector: { addon: {} } },
@@ -139,7 +43,6 @@ const SCHEMAS: {
{ {
name: "Attribute", name: "Attribute",
selector: { attribute: { entity_id: "" } }, selector: { attribute: { entity_id: "" } },
context: { filter_entity: "entity" },
}, },
{ name: "Device", selector: { device: {} } }, { name: "Device", selector: { device: {} } },
{ name: "Duration", selector: { duration: {} } }, { name: "Duration", selector: { duration: {} } },
@@ -158,26 +61,6 @@ const SCHEMAS: {
select: { options: ["Everyone Home", "Some Home", "All gone"] }, select: { options: ["Everyone Home", "Some Home", "All gone"] },
}, },
}, },
{
name: "icon",
selector: {
icon: {},
},
},
{
name: "media",
selector: {
media: {},
},
},
{
name: "location",
selector: { location: { radius: true, icon: "mdi:home" } },
},
{
name: "entities",
selector: { entity: { multiple: true } },
},
], ],
}, },
{ {
@@ -418,10 +301,9 @@ class DemoHaForm extends LitElement {
const hass = provideHass(this); const hass = provideHass(this);
hass.updateTranslations(null, "en"); hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en"); hass.updateTranslations("config", "en");
hass.addEntities(ENTITIES);
mockEntityRegistry(hass); mockEntityRegistry(hass);
mockDeviceRegistry(hass, DEVICES); mockDeviceRegistry(hass);
mockAreaRegistry(hass, AREAS); mockAreaRegistry(hass);
mockHassioSupervisor(hass); mockHassioSupervisor(hass);
} }

View File

@@ -1,5 +1,3 @@
--- ---
title: Selectors title: Target Selectors
--- ---
See the website for [list of available selectors](https://www.home-assistant.io/docs/blueprint/selectors/).

View File

@@ -12,100 +12,6 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry"; import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry"; import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor"; import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
import { getEntity } from "../../../../src/fake_data/entity";
import { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin";
import { showDialog } from "../../../../src/dialogs/make-dialog-manager";
const ENTITIES = [
getEntity("alarm_control_panel", "alarm", "disarmed", {
friendly_name: "Alarm",
}),
getEntity("media_player", "livingroom", "playing", {
friendly_name: "Livingroom",
}),
getEntity("media_player", "lounge", "idle", {
friendly_name: "Lounge",
supported_features: 444983,
}),
getEntity("light", "bedroom", "on", {
friendly_name: "Bedroom",
}),
getEntity("switch", "coffee", "off", {
friendly_name: "Coffee",
}),
];
const DEVICES = [
{
area_id: "bedroom",
configuration_url: null,
config_entries: ["config_entry_1"],
connections: [],
disabled_by: null,
entry_type: null,
id: "device_1",
identifiers: [["demo", "volume1"] as [string, string]],
manufacturer: null,
model: null,
name_by_user: null,
name: "Dishwasher",
sw_version: null,
hw_version: null,
via_device_id: null,
},
{
area_id: "backyard",
configuration_url: null,
config_entries: ["config_entry_2"],
connections: [],
disabled_by: null,
entry_type: null,
id: "device_2",
identifiers: [["demo", "pwm1"] as [string, string]],
manufacturer: null,
model: null,
name_by_user: null,
name: "Lamp",
sw_version: null,
hw_version: null,
via_device_id: null,
},
{
area_id: null,
configuration_url: null,
config_entries: ["config_entry_3"],
connections: [],
disabled_by: null,
entry_type: null,
id: "device_3",
identifiers: [["demo", "pwm1"] as [string, string]],
manufacturer: null,
model: null,
name_by_user: "User name",
name: "Technical name",
sw_version: null,
hw_version: null,
via_device_id: null,
},
];
const AREAS = [
{
area_id: "backyard",
name: "Backyard",
picture: null,
},
{
area_id: "bedroom",
name: "Bedroom",
picture: null,
},
{
area_id: "livingroom",
name: "Livingroom",
picture: null,
},
];
const SCHEMAS: { const SCHEMAS: {
name: string; name: string;
@@ -146,8 +52,6 @@ const SCHEMAS: {
}, },
boolean: { name: "Boolean", selector: { boolean: {} } }, boolean: { name: "Boolean", selector: { boolean: {} } },
time: { name: "Time", selector: { time: {} } }, time: { name: "Time", selector: { time: {} } },
date: { name: "Date", selector: { date: {} } },
datetime: { name: "Date Time", selector: { datetime: {} } },
action: { name: "Action", selector: { action: {} } }, action: { name: "Action", selector: { action: {} } },
text: { text: {
name: "Text", name: "Text",
@@ -164,51 +68,17 @@ const SCHEMAS: {
}, },
}, },
object: { name: "Object", selector: { object: {} } }, object: { name: "Object", selector: { object: {} } },
select_radio: {
name: "Select (Radio)",
selector: { select: { options: ["Option 1", "Option 2"] } },
},
select: { select: {
name: "Select", name: "Select",
selector: { selector: { select: { options: ["Option 1", "Option 2"] } },
select: {
options: [
"Option 1",
"Option 2",
"Option 3",
"Option 4",
"Option 5",
"Option 6",
],
},
},
}, },
icon: { name: "Icon", selector: { icon: {} } },
media: { name: "Media", selector: { media: {} } },
location: { name: "Location", selector: { location: {} } },
location_radius: {
name: "Location with radius",
selector: { location: { radius: true, icon: "mdi:home" } },
},
color_temp: {
name: "Color Temperature",
selector: { color_temp: {} },
},
color_rgb: { name: "Color", selector: { color_rgb: {} } },
},
},
{
name: "Multiples",
input: {
entity: { name: "Entity", selector: { entity: { multiple: true } } },
device: { name: "Device", selector: { device: { multiple: true } } },
}, },
}, },
]; ];
@customElement("demo-components-ha-selector") @customElement("demo-components-ha-selector")
class DemoHaSelector extends LitElement implements ProvideHassElement { class DemoHaSelector extends LitElement {
@state() public hass!: HomeAssistant; @state() private hass!: HomeAssistant;
private data = SCHEMAS.map(() => ({})); private data = SCHEMAS.map(() => ({}));
@@ -217,130 +87,12 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
const hass = provideHass(this); const hass = provideHass(this);
hass.updateTranslations(null, "en"); hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en"); hass.updateTranslations("config", "en");
hass.addEntities(ENTITIES);
mockEntityRegistry(hass); mockEntityRegistry(hass);
mockDeviceRegistry(hass, DEVICES); mockDeviceRegistry(hass);
mockAreaRegistry(hass, AREAS); mockAreaRegistry(hass);
mockHassioSupervisor(hass); mockHassioSupervisor(hass);
hass.mockWS("auth/sign_path", (params) => params);
hass.mockWS("media_player/browse_media", this._browseMedia);
} }
public provideHass(el) {
el.hass = this.hass;
}
public connectedCallback() {
super.connectedCallback();
this.addEventListener("show-dialog", this._dialogManager);
}
public disconnectedCallback() {
super.disconnectedCallback();
this.removeEventListener("show-dialog", this._dialogManager);
}
private _browseMedia = ({ media_content_id }) => {
if (media_content_id === undefined) {
return {
title: "Media",
media_class: "directory",
media_content_type: "",
media_content_id: "media-source://media_source/local/.",
can_play: false,
can_expand: true,
children_media_class: "directory",
thumbnail: null,
children: [
{
title: "Misc",
media_class: "directory",
media_content_type: "",
media_content_id: "media-source://media_source/local/misc",
can_play: false,
can_expand: true,
children_media_class: null,
thumbnail: null,
},
{
title: "Movies",
media_class: "directory",
media_content_type: "",
media_content_id: "media-source://media_source/local/movies",
can_play: true,
can_expand: true,
children_media_class: "movie",
thumbnail: null,
},
{
title: "Music",
media_class: "album",
media_content_type: "",
media_content_id: "media-source://media_source/local/music",
can_play: false,
can_expand: true,
children_media_class: "music",
thumbnail: "/images/album_cover_2.jpg",
},
],
};
}
return {
title: "Subfolder",
media_class: "directory",
media_content_type: "",
media_content_id: "media-source://media_source/local/sub",
can_play: false,
can_expand: true,
children_media_class: "directory",
thumbnail: null,
children: [
{
title: "audio.mp3",
media_class: "music",
media_content_type: "audio/mpeg",
media_content_id: "media-source://media_source/local/audio.mp3",
can_play: true,
can_expand: false,
children_media_class: null,
thumbnail: "/images/album_cover.jpg",
},
{
title: "image.jpg",
media_class: "image",
media_content_type: "image/jpeg",
media_content_id: "media-source://media_source/local/image.jpg",
can_play: true,
can_expand: false,
children_media_class: null,
thumbnail: "https://brands.home-assistant.io/_/image/logo.png",
},
{
title: "movie.mp4",
media_class: "movie",
media_content_type: "image/jpeg",
media_content_id: "media-source://media_source/local/movie.mp4",
can_play: true,
can_expand: false,
children_media_class: null,
thumbnail: null,
},
],
};
};
private _dialogManager = (e) => {
const { dialogTag, dialogImport, dialogParams, addHistory } = e.detail;
showDialog(
this,
this.shadowRoot!,
dialogTag,
dialogParams,
dialogImport,
addHistory
);
};
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
${SCHEMAS.map((info, idx) => { ${SCHEMAS.map((info, idx) => {
@@ -379,6 +131,7 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
} }
static styles = css` static styles = css`
paper-input,
ha-selector { ha-selector {
width: 60; width: 60;
} }

View File

@@ -2,8 +2,6 @@
title: Editing design.home-assistant.io title: Editing design.home-assistant.io
--- ---
![Home Assistant Logo](/images/logo-with-text.png)
# How to edit design.home-assistant.io # How to edit design.home-assistant.io
All pages are stored in [the pages folder][pages-folder] on GitHub. Pages are grouped in a folder per sidebar section. Each page can contain a `<page name>.markdown` description file, a `<page name>.ts` demo file or both. If both are defined the description is rendered first. The description can contain metadata to specify the title of the page. All pages are stored in [the pages folder][pages-folder] on GitHub. Pages are grouped in a folder per sidebar section. Each page can contain a `<page name>.markdown` description file, a `<page name>.ts` demo file or both. If both are defined the description is rendered first. The description can contain metadata to specify the title of the page.
@@ -43,12 +41,15 @@ import { html, css, LitElement } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import "../../../../src/components/ha-card"; import "../../../../src/components/ha-card";
@customElement("demo-user-experience-usability") @customElement("demo-user-experience-usability")
export class DemoUserExperienceUsability extends LitElement { export class DemoUserExperienceUsability extends LitElement {
protected render() { protected render() {
return html` return html`
<ha-card> <ha-card>
<div class="card-content">Hello world!</div> <div class="card-content">
Hello world!
</div>
</ha-card> </ha-card>
`; `;
} }

View File

@@ -29,7 +29,6 @@ const createConfigEntry = (
source: "zeroconf", source: "zeroconf",
state: "loaded", state: "loaded",
supports_options: false, supports_options: false,
supports_remove_device: false,
supports_unload: true, supports_unload: true,
disabled_by: null, disabled_by: null,
pref_disable_new_entities: false, pref_disable_new_entities: false,
@@ -188,7 +187,6 @@ const createEntityRegistryEntries = (
device_id: "mock-device-id", device_id: "mock-device-id",
area_id: null, area_id: null,
disabled_by: null, disabled_by: null,
hidden_by: null,
entity_category: null, entity_category: null,
entity_id: "binary_sensor.updater", entity_id: "binary_sensor.updater",
name: null, name: null,

View File

@@ -1,3 +0,0 @@
---
title: Update
---

View File

@@ -1,140 +0,0 @@
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import "../../../../src/components/ha-card";
import {
UPDATE_SUPPORT_BACKUP,
UPDATE_SUPPORT_PROGRESS,
UPDATE_SUPPORT_INSTALL,
} from "../../../../src/data/update";
import "../../../../src/dialogs/more-info/more-info-content";
import { getEntity } from "../../../../src/fake_data/entity";
import {
MockHomeAssistant,
provideHass,
} from "../../../../src/fake_data/provide_hass";
import "../../components/demo-more-infos";
const base_attributes = {
title: "Awesome",
current_version: "1.2.2",
latest_version: "1.2.3",
release_url: "https://home-assistant.io",
supported_features: UPDATE_SUPPORT_INSTALL,
skipped_version: null,
in_progress: false,
release_summary:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. In nec metus aliquet, porta mi ut, ultrices odio. Etiam egestas orci tellus, non semper metus blandit tincidunt. Praesent elementum turpis vel tempor pharetra. Sed quis cursus diam. Proin sem justo.",
};
const ENTITIES = [
getEntity("update", "update1", "on", {
...base_attributes,
friendly_name: "Update",
}),
getEntity("update", "update2", "on", {
...base_attributes,
title: null,
friendly_name: "Update without title",
}),
getEntity("update", "update3", "on", {
...base_attributes,
release_url: null,
friendly_name: "Update without release_url",
}),
getEntity("update", "update4", "on", {
...base_attributes,
release_summary: null,
friendly_name: "Update without release_summary",
}),
getEntity("update", "update5", "off", {
...base_attributes,
current_version: "1.2.3",
friendly_name: "No update",
}),
getEntity("update", "update6", "off", {
...base_attributes,
skipped_version: "1.2.3",
friendly_name: "Skipped version",
}),
getEntity("update", "update7", "on", {
...base_attributes,
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_BACKUP,
friendly_name: "With backup support",
}),
getEntity("update", "update8", "on", {
...base_attributes,
in_progress: true,
friendly_name: "With true in_progress",
}),
getEntity("update", "update9", "on", {
...base_attributes,
in_progress: 25,
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS,
friendly_name: "With 25 in_progress",
}),
getEntity("update", "update10", "on", {
...base_attributes,
in_progress: 50,
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS,
friendly_name: "With 50 in_progress",
}),
getEntity("update", "update11", "on", {
...base_attributes,
in_progress: 75,
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS,
friendly_name: "With 75 in_progress",
}),
getEntity("update", "update12", "unavailable", {
...base_attributes,
in_progress: 50,
friendly_name: "Unavailable",
}),
getEntity("update", "update13", "on", {
...base_attributes,
supported_features: 0,
friendly_name: "No install support",
}),
getEntity("update", "update14", "off", {
...base_attributes,
current_version: null,
friendly_name: "Update without current_version",
}),
getEntity("update", "update15", "off", {
...base_attributes,
latest_version: null,
friendly_name: "Update without latest_version",
}),
];
@customElement("demo-more-info-update")
class DemoMoreInfoUpdate extends LitElement {
@property() public hass!: MockHomeAssistant;
@query("demo-more-infos") private _demoRoot!: HTMLElement;
protected render(): TemplateResult {
return html`
<demo-more-infos
.hass=${this.hass}
.entities=${ENTITIES.map((ent) => ent.entityId)}
></demo-more-infos>
`;
}
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");
hass.addEntities(ENTITIES);
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-more-info-update": DemoMoreInfoUpdate;
}
}

View File

@@ -1,17 +0,0 @@
---
title: "User types"
---
We have defined three user types for Home Assistant. They are a lean segmentation of users that helps us make decisions throughout the product. User types differ from traditional personas in that the segmentation criteria arent demographic and dont personify a group into a single character with a fictitious background story.
# Outgrowers
Users that outgrow big tech smart home solutions. It just needs to work with easy setup via an app.
# Tinkerers
Technoid users in home networking and development that know how to code.
# Questioner
Users who want more advanced home automation, but need support to make it work.

View File

@@ -14,7 +14,7 @@ import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version"; import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event"; import { fireEvent } from "../../../src/common/dom/fire_event";
import { navigate } from "../../../src/common/navigate"; import { navigate } from "../../../src/common/navigate";
import "../../../src/components/search-input"; import "../../../src/common/search/search-input";
import { extractSearchParam } from "../../../src/common/url/search-params"; import { extractSearchParam } from "../../../src/common/url/search-params";
import "../../../src/components/ha-button-menu"; import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-icon-button"; import "../../../src/components/ha-icon-button";
@@ -110,6 +110,8 @@ class HassioAddonStore extends LitElement {
<div class="search"> <div class="search">
<search-input <search-input
.hass=${this.hass} .hass=${this.hass}
no-label-float
no-underline
.filter=${this._filter} .filter=${this._filter}
@value-changed=${this._filterChanged} @value-changed=${this._filterChanged}
></search-input> ></search-input>
@@ -219,14 +221,13 @@ class HassioAddonStore extends LitElement {
margin-top: 24px; margin-top: 24px;
} }
.search { .search {
position: sticky; padding: 0 16px;
top: 0; background: var(--sidebar-background-color);
z-index: 2; border-bottom: 1px solid var(--divider-color);
} }
search-input { .search search-input {
display: block; position: relative;
--mdc-text-field-fill-color: var(--sidebar-background-color); top: 2px;
--mdc-text-field-idle-line-color: var(--divider-color);
} }
.advanced { .advanced {
padding: 12px; padding: 12px;

View File

@@ -1,5 +1,7 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "@material/mwc-list/mwc-list-item"; import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@@ -9,11 +11,10 @@ import {
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { stopPropagation } from "../../../../src/common/dom/stop_propagation"; import "web-animations-js/web-animations-next-lite.min";
import "../../../../src/components/buttons/ha-progress-button"; import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert"; import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-card"; import "../../../../src/components/ha-card";
import "../../../../src/components/ha-select";
import { import {
HassioAddonDetails, HassioAddonDetails,
HassioAddonSetOptionParams, HassioAddonSetOptionParams,
@@ -56,44 +57,49 @@ class HassioAddonAudio extends LitElement {
${this._error ${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>` ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""} : ""}
${this._inputDevices &&
html`<ha-select <paper-dropdown-menu
.label=${this.supervisor.localize( .label=${this.supervisor.localize(
"addon.configuration.audio.input" "addon.configuration.audio.input"
)} )}
@selected=${this._setInputDevice} @iron-select=${this._setInputDevice}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${this._selectedInput!}
> >
${this._inputDevices.map( <paper-listbox
(item) => html` slot="dropdown-content"
<mwc-list-item .value=${item.device || ""}> attr-for-selected="device"
${item.name} .selected=${this._selectedInput!}
</mwc-list-item> >
` ${this._inputDevices &&
)} this._inputDevices.map(
</ha-select>`} (item) => html`
${this._outputDevices && <paper-item device=${item.device || ""}>
html`<ha-select ${item.name}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu>
<paper-dropdown-menu
.label=${this.supervisor.localize( .label=${this.supervisor.localize(
"addon.configuration.audio.output" "addon.configuration.audio.output"
)} )}
@selected=${this._setOutputDevice} @iron-select=${this._setOutputDevice}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${this._selectedOutput!}
> >
${this._outputDevices.map( <paper-listbox
(item) => html` slot="dropdown-content"
<mwc-list-item .value=${item.device || ""} attr-for-selected="device"
>${item.name}</mwc-list-item .selected=${this._selectedOutput!}
> >
` ${this._outputDevices &&
)} this._outputDevices.map(
</ha-select>`} (item) => html`
<paper-item device=${item.device || ""}
>${item.name}</paper-item
>
`
)}
</paper-listbox>
</paper-dropdown-menu>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<ha-progress-button @click=${this._saveSettings}> <ha-progress-button @click=${this._saveSettings}>
@@ -110,7 +116,8 @@ class HassioAddonAudio extends LitElement {
hassioStyle, hassioStyle,
css` css`
:host, :host,
ha-card { ha-card,
paper-dropdown-menu {
display: block; display: block;
} }
paper-item { paper-item {
@@ -119,30 +126,24 @@ class HassioAddonAudio extends LitElement {
.card-actions { .card-actions {
text-align: right; text-align: right;
} }
ha-select {
width: 100%;
}
ha-select:last-child {
margin-top: 8px;
}
`, `,
]; ];
} }
protected willUpdate(changedProperties: PropertyValues): void { protected update(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties); super.update(changedProperties);
if (changedProperties.has("addon")) { if (changedProperties.has("addon")) {
this._addonChanged(); this._addonChanged();
} }
} }
private _setInputDevice(ev): void { private _setInputDevice(ev): void {
const device = ev.target.value; const device = ev.detail.item.getAttribute("device");
this._selectedInput = device; this._selectedInput = device;
} }
private _setOutputDevice(ev): void { private _setOutputDevice(ev): void {
const device = ev.target.value; const device = ev.detail.item.getAttribute("device");
this._selectedOutput = device; this._selectedOutput = device;
} }

View File

@@ -9,7 +9,6 @@ import {
mdiFlask, mdiFlask,
mdiHomeAssistant, mdiHomeAssistant,
mdiKey, mdiKey,
mdiLinkLock,
mdiNetwork, mdiNetwork,
mdiNumeric1, mdiNumeric1,
mdiNumeric2, mdiNumeric2,
@@ -17,8 +16,6 @@ import {
mdiNumeric4, mdiNumeric4,
mdiNumeric5, mdiNumeric5,
mdiNumeric6, mdiNumeric6,
mdiNumeric7,
mdiNumeric8,
mdiPound, mdiPound,
mdiShield, mdiShield,
} from "@mdi/js"; } from "@mdi/js";
@@ -34,7 +31,6 @@ import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert"; import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-card"; import "../../../../src/components/ha-card";
import "../../../../src/components/ha-chip"; import "../../../../src/components/ha-chip";
import "../../../../src/components/ha-chip-set";
import "../../../../src/components/ha-markdown"; import "../../../../src/components/ha-markdown";
import "../../../../src/components/ha-settings-row"; import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon"; import "../../../../src/components/ha-svg-icon";
@@ -88,8 +84,6 @@ const RATING_ICON = {
4: mdiNumeric4, 4: mdiNumeric4,
5: mdiNumeric5, 5: mdiNumeric5,
6: mdiNumeric6, 6: mdiNumeric6,
7: mdiNumeric7,
8: mdiNumeric8,
}; };
@customElement("hassio-addon-info") @customElement("hassio-addon-info")
@@ -215,7 +209,7 @@ class HassioAddonInfo extends LitElement {
>`} >`}
</div> </div>
<ha-chip-set class="capabilities"> <div class="capabilities">
${this.addon.stage !== "stable" ${this.addon.stage !== "stable"
? html` <ha-chip ? html` <ha-chip
hasIcon hasIcon
@@ -240,9 +234,9 @@ class HassioAddonInfo extends LitElement {
<ha-chip <ha-chip
hasIcon hasIcon
class=${classMap({ class=${classMap({
green: Number(this.addon.rating) >= 6, green: [5, 6].includes(Number(this.addon.rating)),
yellow: [3, 4, 5].includes(Number(this.addon.rating)), yellow: [3, 4].includes(Number(this.addon.rating)),
red: Number(this.addon.rating) >= 2, red: [1, 2].includes(Number(this.addon.rating)),
})} })}
@click=${this._showMoreInfo} @click=${this._showMoreInfo}
id="rating" id="rating"
@@ -370,17 +364,7 @@ class HassioAddonInfo extends LitElement {
</ha-chip> </ha-chip>
` `
: ""} : ""}
${this.addon.signed </div>
? html`
<ha-chip hasIcon @click=${this._showMoreInfo} id="signed">
<ha-svg-icon slot="icon" .path=${mdiLinkLock}></ha-svg-icon>
${this.supervisor.localize(
"addon.dashboard.capability.label.signed"
)}
</ha-chip>
`
: ""}
</ha-chip-set>
<div class="description light-color"> <div class="description light-color">
${this.addon.description}.<br /> ${this.addon.description}.<br />

View File

@@ -1,4 +1,5 @@
import { mdiFolderUpload } from "@mdi/js"; import { mdiFolderUpload } from "@mdi/js";
import "@polymer/paper-input/paper-input-container";
import { html, LitElement, TemplateResult } from "lit"; import { html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators"; import { customElement, state } from "lit/decorators";
import { fireEvent } from "../../../src/common/dom/fire_event"; import { fireEvent } from "../../../src/common/dom/fire_event";

View File

@@ -1,7 +1,7 @@
import { mdiFolder, mdiHomeAssistant, mdiPuzzle } from "@mdi/js"; import { mdiFolder, mdiHomeAssistant, mdiPuzzle } from "@mdi/js";
import { PaperInputElement } from "@polymer/paper-input/paper-input"; import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version"; import { atLeastVersion } from "../../../src/common/config/version";
import { formatDate } from "../../../src/common/datetime/format_date"; import { formatDate } from "../../../src/common/datetime/format_date";
import { formatDateTime } from "../../../src/common/datetime/format_date_time"; import { formatDateTime } from "../../../src/common/datetime/format_date_time";
@@ -92,8 +92,6 @@ export class SupervisorBackupContent extends LitElement {
@property() public confirmBackupPassword = ""; @property() public confirmBackupPassword = "";
@query("paper-input, ha-radio, ha-checkbox", true) private _focusTarget;
public willUpdate(changedProps) { public willUpdate(changedProps) {
super.willUpdate(changedProps); super.willUpdate(changedProps);
if (!this.hasUpdated) { if (!this.hasUpdated) {
@@ -111,10 +109,6 @@ export class SupervisorBackupContent extends LitElement {
} }
} }
public override focus() {
this._focusTarget?.focus();
}
private _localize = (string: string) => private _localize = (string: string) =>
this.supervisor?.localize(`backup.${string}`) || this.supervisor?.localize(`backup.${string}`) ||
this.localize!(`ui.panel.page-onboarding.restore.${string}`); this.localize!(`ui.panel.page-onboarding.restore.${string}`);
@@ -175,23 +169,24 @@ export class SupervisorBackupContent extends LitElement {
: ""} : ""}
${this.backupType === "partial" ${this.backupType === "partial"
? html`<div class="partial-picker"> ? html`<div class="partial-picker">
<ha-formfield ${this.backup && this.backup.homeassistant
.label=${html`<supervisor-formfield-label ? html`
label="Home Assistant" <ha-formfield
.iconPath=${mdiHomeAssistant} .label=${html`<supervisor-formfield-label
.version=${this.backup label="Home Assistant"
? this.backup.homeassistant .iconPath=${mdiHomeAssistant}
: this.hass.config.version} .version=${this.backup.homeassistant}
> >
</supervisor-formfield-label>`} </supervisor-formfield-label>`}
> >
<ha-checkbox <ha-checkbox
.checked=${this.homeAssistant} .checked=${this.homeAssistant}
@click=${this.toggleHomeAssistant} @click=${this.toggleHomeAssistant}
> >
</ha-checkbox> </ha-checkbox>
</ha-formfield> </ha-formfield>
`
: ""}
${foldersSection?.templates.length ${foldersSection?.templates.length
? html` ? html`
<ha-formfield <ha-formfield

View File

@@ -148,6 +148,7 @@ export class HassioUpdate extends LitElement {
} }
ha-settings-row { ha-settings-row {
padding: 0; padding: 0;
--paper-item-body-two-line-min-height: 32px;
} }
`, `,
]; ];

View File

@@ -64,7 +64,6 @@ export class DialogHassioBackupUpload
.path=${mdiClose} .path=${mdiClose}
slot="actionItems" slot="actionItems"
dialogAction="cancel" dialogAction="cancel"
dialogInitialFocus
></ha-icon-button> ></ha-icon-button>
</ha-header-bar> </ha-header-bar>
</div> </div>

View File

@@ -92,7 +92,6 @@ class HassioBackupDialog
.backup=${this._backup} .backup=${this._backup}
.onboarding=${this._dialogParams.onboarding || false} .onboarding=${this._dialogParams.onboarding || false}
.localize=${this._dialogParams.localize} .localize=${this._dialogParams.localize}
dialogInitialFocus
> >
</supervisor-backup-content>`} </supervisor-backup-content>`}
${this._error ${this._error

View File

@@ -61,7 +61,6 @@ class HassioCreateBackupDialog extends LitElement {
: html`<supervisor-backup-content : html`<supervisor-backup-content
.hass=${this.hass} .hass=${this.hass}
.supervisor=${this._dialogParams.supervisor} .supervisor=${this._dialogParams.supervisor}
dialogInitialFocus
> >
</supervisor-backup-content>`} </supervisor-backup-content>`}
${this._error ${this._error

View File

@@ -1,11 +1,12 @@
import "@material/mwc-list/mwc-list-item"; import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-circular-progress"; import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-markdown"; import "../../../../src/components/ha-markdown";
import "../../../../src/components/ha-select";
import { import {
extractApiErrorMessage, extractApiErrorMessage,
ignoreSupervisorError, ignoreSupervisorError,
@@ -89,20 +90,18 @@ class HassioDatadiskDialog extends LitElement {
)} )}
<br /><br /> <br /><br />
<ha-select <paper-dropdown-menu
.label=${this.dialogParams.supervisor.localize( .label=${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.select_device" "dialog.datadisk_move.select_device"
)} )}
@selected=${this._select_device} @value-changed=${this._select_device}
dialogInitialFocus
> >
${this.devices.map( <paper-listbox slot="dropdown-content">
(device) => ${this.devices.map(
html`<mwc-list-item .value=${device} (device) => html`<paper-item>${device}</paper-item>`
>${device}</mwc-list-item )}
>` </paper-listbox>
)} </paper-dropdown-menu>
</ha-select>
` `
: this.devices === undefined : this.devices === undefined
? this.dialogParams.supervisor.localize( ? this.dialogParams.supervisor.localize(
@@ -112,11 +111,7 @@ class HassioDatadiskDialog extends LitElement {
"dialog.datadisk_move.no_devices" "dialog.datadisk_move.no_devices"
)} )}
<mwc-button <mwc-button slot="secondaryAction" @click=${this.closeDialog}>
slot="secondaryAction"
@click=${this.closeDialog}
dialogInitialFocus
>
${this.dialogParams.supervisor.localize( ${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.cancel" "dialog.datadisk_move.cancel"
)} )}
@@ -135,8 +130,8 @@ class HassioDatadiskDialog extends LitElement {
`; `;
} }
private _select_device(ev) { private _select_device(event) {
this.selectedDevice = ev.target.value; this.selectedDevice = event.detail.value;
} }
private async _moveDatadisk() { private async _moveDatadisk() {
@@ -161,7 +156,7 @@ class HassioDatadiskDialog extends LitElement {
haStyle, haStyle,
haStyleDialog, haStyleDialog,
css` css`
ha-select { paper-dropdown-menu {
width: 100%; width: 100%;
} }
ha-circular-progress { ha-circular-progress {

View File

@@ -3,7 +3,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/search-input"; import "../../../../src/common/search/search-input";
import { stringCompare } from "../../../../src/common/string/compare"; import { stringCompare } from "../../../../src/common/string/compare";
import "../../../../src/components/ha-dialog"; import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel"; import "../../../../src/components/ha-expansion-panel";
@@ -80,6 +80,8 @@ class HassioHardwareDialog extends LitElement {
></ha-icon-button> ></ha-icon-button>
<search-input <search-input
.hass=${this.hass} .hass=${this.hass}
autofocus
no-label-float
.filter=${this._filter} .filter=${this._filter}
@value-changed=${this._handleSearchChange} @value-changed=${this._handleSearchChange}
.label=${this._dialogParams.supervisor.localize( .label=${this._dialogParams.supervisor.localize(
@@ -176,7 +178,7 @@ class HassioHardwareDialog extends LitElement {
padding: 0.2em 0.4em; padding: 0.2em 0.4em;
} }
search-input { search-input {
margin: 8px 16px 0; margin: 0 16px;
display: block; display: block;
} }
.device-property { .device-property {

View File

@@ -37,10 +37,7 @@ class HassioMarkdownDialog extends LitElement {
@closed=${this.closeDialog} @closed=${this.closeDialog}
.heading=${createCloseHeading(this.hass, this.title)} .heading=${createCloseHeading(this.hass, this.title)}
> >
<ha-markdown <ha-markdown .content=${this.content || ""}></ha-markdown>
.content=${this.content || ""}
dialogInitialFocus
></ha-markdown>
</ha-dialog> </ha-dialog>
`; `;
} }

View File

@@ -119,7 +119,6 @@ export class DialogHassioNetwork
html`<mwc-tab html`<mwc-tab
.id=${device.interface} .id=${device.interface}
.label=${device.interface} .label=${device.interface}
dialogInitialFocus
> >
</mwc-tab>` </mwc-tab>`
)} )}
@@ -316,7 +315,6 @@ export class DialogHassioNetwork
value="auto" value="auto"
name="${version}method" name="${version}method"
.checked=${this._interface![version]?.method === "auto"} .checked=${this._interface![version]?.method === "auto"}
dialogInitialFocus
> >
</ha-radio> </ha-radio>
</ha-formfield> </ha-formfield>

View File

@@ -80,7 +80,6 @@ class HassioRegistriesDialog extends LitElement {
.schema=${SCHEMA} .schema=${SCHEMA}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
.computeLabel=${this._computeLabel} .computeLabel=${this._computeLabel}
dialogInitialFocus
></ha-form> ></ha-form>
<div class="action"> <div class="action">
<mwc-button <mwc-button
@@ -125,7 +124,7 @@ class HassioRegistriesDialog extends LitElement {
</ha-alert> </ha-alert>
`} `}
<div class="action"> <div class="action">
<mwc-button @click=${this._addRegistry} dialogInitialFocus> <mwc-button @click=${this._addRegistry}>
${this.supervisor.localize( ${this.supervisor.localize(
"dialog.registries.add_new_registry" "dialog.registries.add_new_registry"
)} )}

View File

@@ -106,9 +106,6 @@ class HassioRepositoriesDialog extends LitElement {
</paper-item-body> </paper-item-body>
<div class="delete"> <div class="delete">
<ha-icon-button <ha-icon-button
.label=${this._dialogParams!.supervisor.localize(
"dialog.repositories.remove"
)}
.disabled=${usedRepositories.includes(repo.slug)} .disabled=${usedRepositories.includes(repo.slug)}
.slug=${repo.slug} .slug=${repo.slug}
.path=${usedRepositories.includes(repo.slug) .path=${usedRepositories.includes(repo.slug)
@@ -142,7 +139,6 @@ class HassioRepositoriesDialog extends LitElement {
"dialog.repositories.add" "dialog.repositories.add"
)} )}
@keydown=${this._handleKeyAdd} @keydown=${this._handleKeyAdd}
dialogInitialFocus
></paper-input> ></paper-input>
<mwc-button @click=${this._addRepository}> <mwc-button @click=${this._addRepository}>
${this._processing ${this._processing

View File

@@ -1,12 +1,9 @@
// Compat needs to be first import // Compat needs to be first import
import "../../src/resources/compatibility"; import "../../src/resources/compatibility";
import { setCancelSyntheticClickEvents } from "@polymer/polymer/lib/utils/settings";
import "../../src/resources/roboto"; import "../../src/resources/roboto";
import "../../src/resources/safari-14-attachshadow-patch"; import "../../src/resources/safari-14-attachshadow-patch";
import "./hassio-main"; import "./hassio-main";
setCancelSyntheticClickEvents(false);
const styleEl = document.createElement("style"); const styleEl = document.createElement("style");
styleEl.innerHTML = ` styleEl.innerHTML = `
body { body {

View File

@@ -121,8 +121,7 @@ export class HassioMain extends SupervisorBaseElement {
this.parentElement, this.parentElement,
this.hass.themes, this.hass.themes,
themeName, themeName,
themeSettings, themeSettings
true
); );
} }
} }

View File

@@ -1,10 +1,12 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-alert"; import "../../../src/components/ha-alert";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import "../../../src/components/ha-select";
import { extractApiErrorMessage } from "../../../src/data/hassio/common"; import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { fetchHassioLogs } from "../../../src/data/hassio/supervisor"; import { fetchHassioLogs } from "../../../src/data/hassio/supervisor";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
@@ -71,19 +73,24 @@ class HassioSupervisorLog extends LitElement {
: ""} : ""}
${this.hass.userData?.showAdvanced ${this.hass.userData?.showAdvanced
? html` ? html`
<ha-select <paper-dropdown-menu
.label=${this.supervisor.localize("system.log.log_provider")} .label=${this.supervisor.localize("system.log.log_provider")}
@selected=${this._setLogProvider} @iron-select=${this._setLogProvider}
.value=${this._selectedLogProvider}
> >
${logProviders.map( <paper-listbox
(provider) => html` slot="dropdown-content"
<mwc-list-item .value=${provider.key}> attr-for-selected="provider"
${provider.name} .selected=${this._selectedLogProvider}
</mwc-list-item> >
` ${logProviders.map(
)} (provider) => html`
</ha-select> <paper-item provider=${provider.key}>
${provider.name}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu>
` `
: ""} : ""}
@@ -103,7 +110,7 @@ class HassioSupervisorLog extends LitElement {
} }
private async _setLogProvider(ev): Promise<void> { private async _setLogProvider(ev): Promise<void> {
const provider = ev.target.value; const provider = ev.detail.item.getAttribute("provider");
this._selectedLogProvider = provider; this._selectedLogProvider = provider;
this._loadData(); this._loadData();
} }
@@ -146,9 +153,9 @@ class HassioSupervisorLog extends LitElement {
pre { pre {
white-space: pre-wrap; white-space: pre-wrap;
} }
ha-select { paper-dropdown-menu {
width: 100%; padding: 0 2%;
margin-bottom: 4px; width: 96%;
} }
`, `,
]; ];

View File

@@ -10,6 +10,7 @@ import {
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../src/common/dom/fire_event"; import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/common/search/search-input";
import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-alert"; import "../../../src/components/ha-alert";
import "../../../src/components/ha-button-menu"; import "../../../src/components/ha-button-menu";
@@ -45,6 +46,7 @@ import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-loading-screen"; import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-subpage"; import "../../../src/layouts/hass-subpage";
import "../../../src/layouts/hass-tabs-subpage"; import "../../../src/layouts/hass-tabs-subpage";
import { SUPERVISOR_UPDATE_NAMES } from "../../../src/panels/config/dashboard/ha-config-updates";
import { HomeAssistant, Route } from "../../../src/types"; import { HomeAssistant, Route } from "../../../src/types";
import { addonArchIsSupported, extractChangelog } from "../util/addon"; import { addonArchIsSupported, extractChangelog } from "../util/addon";
@@ -54,12 +56,6 @@ declare global {
} }
} }
const SUPERVISOR_UPDATE_NAMES = {
core: "Home Assistant Core",
os: "Home Assistant Operating System",
supervisor: "Home Assistant Supervisor",
};
type updateType = "os" | "supervisor" | "core" | "addon"; type updateType = "os" | "supervisor" | "core" | "addon";
const changelogUrl = ( const changelogUrl = (

View File

@@ -1,8 +1,8 @@
{ {
"description": "A frontend for Home Assistant", "description": "A frontend for Home Assistant using the Polymer framework",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/home-assistant/frontend" "url": "https://github.com/home-assistant/home-assistant-polymer"
}, },
"name": "home-assistant-frontend", "name": "home-assistant-frontend",
"version": "1.0.0", "version": "1.0.0",
@@ -46,7 +46,6 @@
"@fullcalendar/daygrid": "5.9.0", "@fullcalendar/daygrid": "5.9.0",
"@fullcalendar/interaction": "5.9.0", "@fullcalendar/interaction": "5.9.0",
"@fullcalendar/list": "5.9.0", "@fullcalendar/list": "5.9.0",
"@lit-labs/motion": "^1.0.2",
"@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.7.0-pre.2#./.yarn/patches/@lit-labs/virtualizer/event-target-shim.patch", "@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.7.0-pre.2#./.yarn/patches/@lit-labs/virtualizer/event-target-shim.patch",
"@material/chips": "14.0.0-canary.261f2db59.0", "@material/chips": "14.0.0-canary.261f2db59.0",
"@material/data-table": "14.0.0-canary.261f2db59.0", "@material/data-table": "14.0.0-canary.261f2db59.0",
@@ -72,13 +71,14 @@
"@material/mwc-textfield": "0.25.3", "@material/mwc-textfield": "0.25.3",
"@material/mwc-top-app-bar-fixed": "^0.25.3", "@material/mwc-top-app-bar-fixed": "^0.25.3",
"@material/top-app-bar": "14.0.0-canary.261f2db59.0", "@material/top-app-bar": "14.0.0-canary.261f2db59.0",
"@mdi/js": "6.6.95", "@mdi/js": "6.5.95",
"@mdi/svg": "6.6.95", "@mdi/svg": "6.5.95",
"@polymer/app-layout": "^3.1.0", "@polymer/app-layout": "^3.1.0",
"@polymer/iron-flex-layout": "^3.0.1", "@polymer/iron-flex-layout": "^3.0.1",
"@polymer/iron-icon": "^3.0.1", "@polymer/iron-icon": "^3.0.1",
"@polymer/iron-input": "^3.0.1", "@polymer/iron-input": "^3.0.1",
"@polymer/iron-resizable-behavior": "^3.0.1", "@polymer/iron-resizable-behavior": "^3.0.1",
"@polymer/paper-dropdown-menu": "^3.2.0",
"@polymer/paper-input": "^3.2.1", "@polymer/paper-input": "^3.2.1",
"@polymer/paper-item": "^3.0.1", "@polymer/paper-item": "^3.0.1",
"@polymer/paper-listbox": "^3.0.1", "@polymer/paper-listbox": "^3.0.1",
@@ -95,7 +95,6 @@
"@vibrant/core": "^3.2.1-alpha.1", "@vibrant/core": "^3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "^3.2.1-alpha.1", "@vibrant/quantizer-mmcq": "^3.2.1-alpha.1",
"@vue/web-component-wrapper": "^1.2.0", "@vue/web-component-wrapper": "^1.2.0",
"@webcomponents/scoped-custom-element-registry": "^0.0.5",
"@webcomponents/webcomponentsjs": "^2.2.10", "@webcomponents/webcomponentsjs": "^2.2.10",
"app-datepicker": "^5.0.1", "app-datepicker": "^5.0.1",
"chart.js": "^3.3.2", "chart.js": "^3.3.2",
@@ -107,8 +106,8 @@
"deep-freeze": "^0.0.1", "deep-freeze": "^0.0.1",
"fuse.js": "^6.0.0", "fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2", "google-timezones-json": "^1.0.2",
"hls.js": "^1.1.5", "hls.js": "^1.0.11",
"home-assistant-js-websocket": "^7.0.1", "home-assistant-js-websocket": "^6.0.1",
"idb-keyval": "^5.1.3", "idb-keyval": "^5.1.3",
"intl-messageformat": "^9.9.1", "intl-messageformat": "^9.9.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
@@ -116,7 +115,7 @@
"leaflet-draw": "^1.0.4", "leaflet-draw": "^1.0.4",
"lit": "^2.1.2", "lit": "^2.1.2",
"lit-vaadin-helpers": "^0.3.0", "lit-vaadin-helpers": "^0.3.0",
"marked": "^4.0.12", "marked": "^3.0.2",
"memoize-one": "^5.2.1", "memoize-one": "^5.2.1",
"node-vibrant": "3.2.1-alpha.1", "node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "^0.3.2", "proxy-polyfill": "^0.3.2",
@@ -135,12 +134,13 @@
"vis-network": "^8.5.4", "vis-network": "^8.5.4",
"vue": "^2.6.12", "vue": "^2.6.12",
"vue2-daterange-picker": "^0.5.1", "vue2-daterange-picker": "^0.5.1",
"workbox-cacheable-response": "^6.4.2", "web-animations-js": "^2.3.2",
"workbox-core": "^6.4.2", "workbox-cacheable-response": "^6.1.5",
"workbox-expiration": "^6.4.2", "workbox-core": "^6.1.5",
"workbox-precaching": "^6.4.2", "workbox-expiration": "^6.1.5",
"workbox-routing": "^6.4.2", "workbox-precaching": "^6.1.5",
"workbox-strategies": "^6.4.2", "workbox-routing": "^6.1.5",
"workbox-strategies": "^6.1.5",
"xss": "^1.0.9" "xss": "^1.0.9"
}, },
"devDependencies": { "devDependencies": {
@@ -169,7 +169,7 @@
"@types/js-yaml": "^4", "@types/js-yaml": "^4",
"@types/leaflet": "^1", "@types/leaflet": "^1",
"@types/leaflet-draw": "^1", "@types/leaflet-draw": "^1",
"@types/marked": "^4", "@types/marked": "^2",
"@types/mocha": "^8", "@types/mocha": "^8",
"@types/qrcode": "^1.4.2", "@types/qrcode": "^1.4.2",
"@types/sortablejs": "^1", "@types/sortablejs": "^1",
@@ -196,7 +196,7 @@
"fs-extra": "^7.0.1", "fs-extra": "^7.0.1",
"glob": "^7.2.0", "glob": "^7.2.0",
"gulp": "^4.0.2", "gulp": "^4.0.2",
"gulp-flatmap": "^1.0.2", "gulp-foreach": "^0.1.0",
"gulp-json-transform": "^0.4.6", "gulp-json-transform": "^0.4.6",
"gulp-merge-json": "^1.3.1", "gulp-merge-json": "^1.3.1",
"gulp-rename": "^2.0.0", "gulp-rename": "^2.0.0",
@@ -233,7 +233,7 @@
"webpack-dev-server": "^4.3.0", "webpack-dev-server": "^4.3.0",
"webpack-manifest-plugin": "^4.0.2", "webpack-manifest-plugin": "^4.0.2",
"webpackbar": "^5.0.0-3", "webpackbar": "^5.0.0-3",
"workbox-build": "^6.4.2" "workbox-build": "^6.1.5"
}, },
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch", "_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
"resolutions": { "resolutions": {
@@ -253,6 +253,5 @@
"prettier": { "prettier": {
"trailingComma": "es5", "trailingComma": "es5",
"arrowParens": "always" "arrowParens": "always"
}, }
"packageManager": "yarn@3.2.0"
} }

View File

@@ -2,6 +2,6 @@
from pathlib import Path from pathlib import Path
def where() -> Path: def where():
"""Return path to the frontend.""" """Return path to the frontend."""
return Path(__file__).parent return Path(__file__).parent

View File

View File

@@ -4,8 +4,6 @@
# Stop on errors # Stop on errors
set -e set -e
WD="${WORKSPACE_DIRECTORY:=/workspaces/frontend}"
if [ -z "${DEVCONTAINER}" ]; then if [ -z "${DEVCONTAINER}" ]; then
echo "This task should only run inside a devcontainer, for local install HA Core in a venv." echo "This task should only run inside a devcontainer, for local install HA Core in a venv."
exit 1 exit 1
@@ -18,9 +16,9 @@ if [ -z $(which hass) ]; then
git+git://github.com/home-assistant/home-assistant.git@dev git+git://github.com/home-assistant/home-assistant.git@dev
fi fi
if [ ! -d "${WD}/config" ]; then if [ ! -d "/workspaces/frontend/config" ]; then
echo "Creating default configuration." echo "Creating default configuration."
mkdir -p "${WD}/config"; mkdir -p "/workspaces/frontend/config";
hass --script ensure_config -c config hass --script ensure_config -c config
echo "demo: echo "demo:
@@ -28,24 +26,24 @@ logger:
default: info default: info
logs: logs:
homeassistant.components.frontend: debug homeassistant.components.frontend: debug
" >> "${WD}/config/configuration.yaml" " >> /workspaces/frontend/config/configuration.yaml
if [ ! -z "${HASSIO}" ]; then if [ ! -z "${HASSIO}" ]; then
echo " echo "
# frontend: # frontend:
# development_repo: ${WD} # development_repo: /workspaces/frontend
hassio: hassio:
development_repo: ${WD}" >> "${WD}/config/configuration.yaml" development_repo: /workspaces/frontend" >> /workspaces/frontend/config/configuration.yaml
else else
echo " echo "
frontend: frontend:
development_repo: ${WD} development_repo: /workspaces/frontend
# hassio: # hassio:
# development_repo: ${WD}" >> "${WD}/config/configuration.yaml" # development_repo: /workspaces/frontend" >> /workspaces/frontend/config/configuration.yaml
fi fi
fi fi
hass -c "${WD}/config" hass -c /workspaces/frontend/config

View File

@@ -1,6 +1,6 @@
[metadata] [metadata]
name = home-assistant-frontend name = home-assistant-frontend
version = 20220322.0 version = 20220203.0
author = The Home Assistant Authors author = The Home Assistant Authors
author_email = hello@home-assistant.io author_email = hello@home-assistant.io
license = Apache-2.0 license = Apache-2.0
@@ -19,8 +19,3 @@ python_requires = >= 3.4.0
[options.packages.find] [options.packages.find]
include = include =
hass_frontend* hass_frontend*
[mypy]
python_version = 3.4
show_error_codes = True
strict = True

7
setup.py Normal file
View File

@@ -0,0 +1,7 @@
"""
Entry point for setuptools. Required for editable installs.
TODO: Remove file after updating to pip 21.3
"""
from setuptools import setup
setup()

View File

@@ -101,19 +101,13 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
this._fetchAuthProviders(); this._fetchAuthProviders();
if (matchMedia("(prefers-color-scheme: dark)").matches) { if (matchMedia("(prefers-color-scheme: dark)").matches) {
applyThemesOnElement( applyThemesOnElement(document.documentElement, {
document.documentElement, default_theme: "default",
{ default_dark_theme: null,
default_theme: "default", themes: {},
default_dark_theme: null, darkMode: true,
themes: {}, theme: "default",
darkMode: true, });
theme: "default",
},
undefined,
undefined,
true
);
} }
if (!this.redirectUri) { if (!this.redirectUri) {

View File

@@ -187,7 +187,6 @@ export const DOMAINS_WITH_MORE_INFO = [
"scene", "scene",
"sun", "sun",
"timer", "timer",
"update",
"vacuum", "vacuum",
"water_heater", "water_heater",
"weather", "weather",
@@ -201,7 +200,6 @@ export const DOMAINS_HIDE_DEFAULT_MORE_INFO = [
"input_text", "input_text",
"number", "number",
"scene", "scene",
"update",
"select", "select",
]; ];

View File

@@ -3,9 +3,9 @@ import type { ForDict } from "../../data/automation";
export const createDurationData = ( export const createDurationData = (
duration: string | number | ForDict | undefined duration: string | number | ForDict | undefined
): HaDurationData | undefined => { ): HaDurationData => {
if (duration === undefined) { if (duration === undefined) {
return undefined; return {};
} }
if (typeof duration !== "object") { if (typeof duration !== "object") {
if (typeof duration === "string" || isNaN(duration)) { if (typeof duration === "string" || isNaN(duration)) {

View File

@@ -31,12 +31,11 @@ export const applyThemesOnElement = (
element, element,
themes: HomeAssistant["themes"], themes: HomeAssistant["themes"],
selectedTheme?: string, selectedTheme?: string,
themeSettings?: Partial<HomeAssistant["selectedTheme"]>, themeSettings?: Partial<HomeAssistant["selectedTheme"]>
main?: boolean
) => { ) => {
// If there is no explicitly desired theme provided, and the element is the main element we automatically // If there is no explicitly desired theme provided, we automatically
// use the active one from `themes`. // use the active one from `themes`.
const themeToApply = selectedTheme || (main ? themes.theme : undefined); const themeToApply = selectedTheme || themes.theme;
// If there is no explicitly desired dark mode provided, we automatically // If there is no explicitly desired dark mode provided, we automatically
// use the active one from `themes`. // use the active one from `themes`.
@@ -48,7 +47,7 @@ export const applyThemesOnElement = (
let cacheKey = themeToApply; let cacheKey = themeToApply;
let themeRules: Partial<ThemeVars> = {}; let themeRules: Partial<ThemeVars> = {};
if (themeToApply && darkMode) { if (darkMode) {
cacheKey = `${cacheKey}__dark`; cacheKey = `${cacheKey}__dark`;
themeRules = { ...darkStyles }; themeRules = { ...darkStyles };
} }

View File

@@ -1,4 +1,4 @@
import type { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
export const canToggleDomain = (hass: HomeAssistant, domain: string) => { export const canToggleDomain = (hass: HomeAssistant, domain: string) => {
const services = hass.services[domain]; const services = hass.services[domain];

View File

@@ -1,30 +1,14 @@
import type { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import type { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { canToggleDomain } from "./can_toggle_domain"; import { canToggleDomain } from "./can_toggle_domain";
import { computeStateDomain } from "./compute_state_domain"; import { computeStateDomain } from "./compute_state_domain";
import { supportsFeature } from "./supports-feature"; import { supportsFeature } from "./supports-feature";
export const canToggleState = (hass: HomeAssistant, stateObj: HassEntity) => { export const canToggleState = (hass: HomeAssistant, stateObj: HassEntity) => {
const domain = computeStateDomain(stateObj); const domain = computeStateDomain(stateObj);
if (domain === "group") { if (domain === "group") {
if ( return stateObj.state === "on" || stateObj.state === "off";
stateObj.attributes?.entity_id?.some((entity) => {
const entityStateObj = hass.states[entity];
if (!entityStateObj) {
return false;
}
const entityDomain = computeStateDomain(entityStateObj);
return canToggleDomain(hass, entityDomain);
})
) {
return stateObj.state === "on" || stateObj.state === "off";
}
return false;
} }
if (domain === "climate") { if (domain === "climate") {
return supportsFeature(stateObj, 4096); return supportsFeature(stateObj, 4096);
} }

View File

@@ -1,18 +1,12 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity"; import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { FrontendLocaleData } from "../../data/translation"; import { FrontendLocaleData } from "../../data/translation";
import {
updateIsInstalling,
UpdateEntity,
UPDATE_SUPPORT_PROGRESS,
} from "../../data/update";
import { formatDate } from "../datetime/format_date"; import { formatDate } from "../datetime/format_date";
import { formatDateTime } from "../datetime/format_date_time"; import { formatDateTime } from "../datetime/format_date_time";
import { formatTime } from "../datetime/format_time"; import { formatTime } from "../datetime/format_time";
import { formatNumber, isNumericState } from "../number/format_number"; import { formatNumber, isNumericState } from "../number/format_number";
import { LocalizeFunc } from "../translations/localize"; import { LocalizeFunc } from "../translations/localize";
import { computeStateDomain } from "./compute_state_domain"; import { computeStateDomain } from "./compute_state_domain";
import { supportsFeature } from "./supports-feature";
export const computeStateDisplay = ( export const computeStateDisplay = (
localize: LocalizeFunc, localize: LocalizeFunc,
@@ -129,33 +123,7 @@ export const computeStateDisplay = (
domain === "scene" || domain === "scene" ||
(domain === "sensor" && stateObj.attributes.device_class === "timestamp") (domain === "sensor" && stateObj.attributes.device_class === "timestamp")
) { ) {
try { return formatDateTime(new Date(compareState), locale);
return formatDateTime(new Date(compareState), locale);
} catch (_err) {
return compareState;
}
}
if (domain === "update") {
// When updating, and entity does not support % show "Installing"
// When updating, and entity does support % show "Installing (xx%)"
// When update available, show the version
// When the latest version is skipped, show the latest version
// When update is not available, show "Up-to-date"
// When update is not available and there is no latest_version show "Unavailable"
return compareState === "on"
? updateIsInstalling(stateObj as UpdateEntity)
? supportsFeature(stateObj, UPDATE_SUPPORT_PROGRESS)
? localize("ui.card.update.installing_with_progress", {
progress: stateObj.attributes.in_progress,
})
: localize("ui.card.update.installing")
: stateObj.attributes.latest_version
: stateObj.attributes.skipped_version ===
stateObj.attributes.latest_version
? stateObj.attributes.latest_version ??
localize("state.default.unavailable")
: localize("ui.card.update.up_to_date");
} }
return ( return (

View File

@@ -1,4 +1,4 @@
import type { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { computeDomain } from "./compute_domain"; import { computeDomain } from "./compute_domain";
export const computeStateDomain = (stateObj: HassEntity) => export const computeStateDomain = (stateObj: HassEntity) =>

View File

@@ -120,7 +120,6 @@ export const computeOpenIcon = (stateObj: HassEntity): string => {
case "awning": case "awning":
case "door": case "door":
case "gate": case "gate":
case "curtain":
return mdiArrowExpandHorizontal; return mdiArrowExpandHorizontal;
default: default:
return mdiArrowUp; return mdiArrowUp;
@@ -132,7 +131,6 @@ export const computeCloseIcon = (stateObj: HassEntity): string => {
case "awning": case "awning":
case "door": case "door":
case "gate": case "gate":
case "curtain":
return mdiArrowCollapseHorizontal; return mdiArrowCollapseHorizontal;
default: default:
return mdiArrowDown; return mdiArrowDown;

View File

@@ -9,10 +9,11 @@ import {
mdiCast, mdiCast,
mdiCastConnected, mdiCastConnected,
mdiClock, mdiClock,
mdiEmoticonDead,
mdiFlash,
mdiGestureTapButton, mdiGestureTapButton,
mdiLanConnect, mdiLanConnect,
mdiLanDisconnect, mdiLanDisconnect,
mdiLightSwitch,
mdiLock, mdiLock,
mdiLockAlert, mdiLockAlert,
mdiLockClock, mdiLockClock,
@@ -21,16 +22,16 @@ import {
mdiPowerPlug, mdiPowerPlug,
mdiPowerPlugOff, mdiPowerPlugOff,
mdiRestart, mdiRestart,
mdiSleep,
mdiTimerSand,
mdiToggleSwitch, mdiToggleSwitch,
mdiToggleSwitchOff, mdiToggleSwitchOff,
mdiCheckCircleOutline, mdiCheckCircleOutline,
mdiCloseCircleOutline, mdiCloseCircleOutline,
mdiWeatherNight, mdiWeatherNight,
mdiPackage, mdiZWave,
mdiPackageDown,
} from "@mdi/js"; } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { updateIsInstalling, UpdateEntity } from "../../data/update";
/** /**
* Return the icon to be used for a domain. * Return the icon to be used for a domain.
* *
@@ -111,7 +112,19 @@ export const domainIcon = (
case "switch": case "switch":
return compareState === "on" ? mdiToggleSwitch : mdiToggleSwitchOff; return compareState === "on" ? mdiToggleSwitch : mdiToggleSwitchOff;
default: default:
return mdiLightSwitch; return mdiFlash;
}
case "zwave":
switch (compareState) {
case "dead":
return mdiEmoticonDead;
case "sleeping":
return mdiSleep;
case "initializing":
return mdiTimerSand;
default:
return mdiZWave;
} }
case "sensor": { case "sensor": {
@@ -136,13 +149,6 @@ export const domainIcon = (
return stateObj?.state === "above_horizon" return stateObj?.state === "above_horizon"
? FIXED_DOMAIN_ICONS[domain] ? FIXED_DOMAIN_ICONS[domain]
: mdiWeatherNight; : mdiWeatherNight;
case "update":
return compareState === "on"
? updateIsInstalling(stateObj as UpdateEntity)
? mdiPackageDown
: mdiPackageUp
: mdiPackage;
} }
if (domain in FIXED_DOMAIN_ICONS) { if (domain in FIXED_DOMAIN_ICONS) {

View File

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

View File

@@ -1,12 +1,12 @@
import { mdiClose, mdiMagnify } from "@mdi/js"; import { mdiClose, mdiMagnify } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import "./ha-icon-button"; import "../../components/ha-icon-button";
import "./ha-svg-icon"; import "../../components/ha-svg-icon";
import "./ha-textfield"; import "../../components/ha-textfield";
import type { HaTextField } from "./ha-textfield"; import type { HaTextField } from "../../components/ha-textfield";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../../types";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../dom/fire_event";
@customElement("search-input") @customElement("search-input")
class SearchInput extends LitElement { class SearchInput extends LitElement {
@@ -14,9 +14,6 @@ class SearchInput extends LitElement {
@property() public filter?: string; @property() public filter?: string;
@property({ type: Boolean })
public suffix = false;
@property({ type: Boolean }) @property({ type: Boolean })
public autofocus = false; public autofocus = false;
@@ -35,8 +32,8 @@ class SearchInput extends LitElement {
.autofocus=${this.autofocus} .autofocus=${this.autofocus}
.label=${this.label || "Search"} .label=${this.label || "Search"}
.value=${this.filter || ""} .value=${this.filter || ""}
icon .icon=${true}
.iconTrailing=${this.filter || this.suffix} .iconTrailing=${this.filter}
@input=${this._filterInputChanged} @input=${this._filterInputChanged}
> >
<slot name="prefix" slot="leadingIcon"> <slot name="prefix" slot="leadingIcon">
@@ -46,18 +43,16 @@ class SearchInput extends LitElement {
.path=${mdiMagnify} .path=${mdiMagnify}
></ha-svg-icon> ></ha-svg-icon>
</slot> </slot>
<div class="trailing" slot="trailingIcon"> ${this.filter &&
${this.filter && html`
html` <ha-icon-button
<ha-icon-button slot="trailingIcon"
@click=${this._clearSearch} @click=${this._clearSearch}
.label=${this.hass.localize("ui.common.clear")} .label=${this.hass.localize("ui.common.clear")}
.path=${mdiClose} .path=${mdiClose}
class="clear-button" class="clear-button"
></ha-icon-button> ></ha-icon-button>
`} `}
<slot name="suffix"></slot>
</div>
</ha-textfield> </ha-textfield>
`; `;
} }
@@ -86,16 +81,15 @@ class SearchInput extends LitElement {
ha-svg-icon { ha-svg-icon {
outline: none; outline: none;
} }
ha-icon-button {
--mdc-icon-button-size: 24px;
}
.clear-button { .clear-button {
--mdc-icon-size: 20px; --mdc-icon-size: 20px;
} }
ha-textfield { ha-textfield {
display: inherit; display: inherit;
} }
.trailing {
display: flex;
align-items: center;
}
`; `;
} }
} }

View File

@@ -1,4 +0,0 @@
const regexp =
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
export const isIPAddress = (input: string): boolean => regexp.test(input);

View File

@@ -15,7 +15,6 @@ export const iconColorCSS = css`
ha-state-icon[data-domain="media_player"][data-state="on"], ha-state-icon[data-domain="media_player"][data-state="on"],
ha-state-icon[data-domain="media_player"][data-state="paused"], ha-state-icon[data-domain="media_player"][data-state="paused"],
ha-state-icon[data-domain="media_player"][data-state="playing"], ha-state-icon[data-domain="media_player"][data-state="playing"],
ha-state-icon[data-domain="remote"][data-state="on"],
ha-state-icon[data-domain="script"][data-state="on"], ha-state-icon[data-domain="script"][data-state="on"],
ha-state-icon[data-domain="sun"][data-state="above_horizon"], ha-state-icon[data-domain="sun"][data-state="above_horizon"],
ha-state-icon[data-domain="switch"][data-state="on"], ha-state-icon[data-domain="switch"][data-state="on"],
@@ -70,6 +69,9 @@ export const iconColorCSS = css`
} }
ha-state-icon[data-domain="plant"][data-state="problem"], ha-state-icon[data-domain="plant"][data-state="problem"],
ha-state-icon[data-domain="zwave"][data-state="dead"] {
color: var(--state-icon-error-color);
}
/* Color the icon if unavailable */ /* Color the icon if unavailable */
ha-state-icon[data-state="unavailable"] { ha-state-icon[data-state="unavailable"] {

View File

@@ -11,7 +11,7 @@ export const debounce = <T extends any[]>(
immediate = false immediate = false
) => { ) => {
let timeout: number | undefined; let timeout: number | undefined;
const debouncedFunc = (...args: T): void => { return (...args: T): void => {
const later = () => { const later = () => {
timeout = undefined; timeout = undefined;
if (!immediate) { if (!immediate) {
@@ -25,8 +25,4 @@ export const debounce = <T extends any[]>(
func(...args); func(...args);
} }
}; };
debouncedFunc.cancel = () => {
clearTimeout(timeout);
};
return debouncedFunc;
}; };

View File

@@ -1,9 +1,8 @@
import "@material/mwc-button"; import "@material/mwc-button";
import { mdiAlertOctagram, mdiCheckBold } from "@mdi/js"; import type { Button } from "@material/mwc-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import "../ha-circular-progress"; import "../ha-circular-progress";
import "../ha-svg-icon";
@customElement("ha-progress-button") @customElement("ha-progress-button")
export class HaProgressButton extends LitElement { export class HaProgressButton extends LitElement {
@@ -13,53 +12,38 @@ export class HaProgressButton extends LitElement {
@property({ type: Boolean }) public raised = false; @property({ type: Boolean }) public raised = false;
@state() private _result?: "success" | "error"; @query("mwc-button", true) private _button?: Button;
public render(): TemplateResult { public render(): TemplateResult {
const overlay = this._result || this.progress;
return html` return html`
<mwc-button <mwc-button
?raised=${this.raised} ?raised=${this.raised}
.disabled=${this.disabled || this.progress} .disabled=${this.disabled || this.progress}
@click=${this._buttonTapped} @click=${this._buttonTapped}
class=${this._result || ""}
> >
<slot></slot> <slot></slot>
</mwc-button> </mwc-button>
${!overlay ${this.progress
? "" ? html`<div class="progress">
: html` <ha-circular-progress size="small" active></ha-circular-progress>
<div class="progress"> </div>`
${this._result === "success" : ""}
? html`<ha-svg-icon .path=${mdiCheckBold}></ha-svg-icon>`
: this._result === "error"
? html`<ha-svg-icon .path=${mdiAlertOctagram}></ha-svg-icon>`
: this.progress
? html`
<ha-circular-progress
size="small"
active
></ha-circular-progress>
`
: ""}
</div>
`}
`; `;
} }
public actionSuccess(): void { public actionSuccess(): void {
this._setResult("success"); this._tempClass("success");
} }
public actionError(): void { public actionError(): void {
this._setResult("error"); this._tempClass("error");
} }
private _setResult(result: "success" | "error"): void { private _tempClass(className: string): void {
this._result = result; this._button!.classList.add(className);
setTimeout(() => { setTimeout(() => {
this._result = undefined; this._button!.classList.remove(className);
}, 2000); }, 1000);
} }
private _buttonTapped(ev: Event): void { private _buttonTapped(ev: Event): void {
@@ -85,7 +69,6 @@ export class HaProgressButton extends LitElement {
background-color: var(--success-color); background-color: var(--success-color);
transition: none; transition: none;
border-radius: 4px; border-radius: 4px;
pointer-events: none;
} }
mwc-button[raised].success { mwc-button[raised].success {
@@ -98,7 +81,6 @@ export class HaProgressButton extends LitElement {
background-color: var(--error-color); background-color: var(--error-color);
transition: none; transition: none;
border-radius: 4px; border-radius: 4px;
pointer-events: none;
} }
mwc-button[raised].error { mwc-button[raised].error {
@@ -107,21 +89,13 @@ export class HaProgressButton extends LitElement {
} }
.progress { .progress {
bottom: 4px; bottom: 0;
margin-top: 4px;
position: absolute; position: absolute;
text-align: center; text-align: center;
top: 4px; top: 0;
width: 100%; width: 100%;
} }
ha-svg-icon {
color: white;
}
mwc-button.success slot,
mwc-button.error slot {
visibility: hidden;
}
`; `;
} }
} }

View File

@@ -21,7 +21,7 @@ import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { restoreScroll } from "../../common/decorators/restore-scroll"; import { restoreScroll } from "../../common/decorators/restore-scroll";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import "../search-input"; import "../../common/search/search-input";
import { debounce } from "../../common/util/debounce"; import { debounce } from "../../common/util/debounce";
import { nextRender } from "../../common/util/render-status"; import { nextRender } from "../../common/util/render-status";
import { haStyleScrollbar } from "../../resources/styles"; import { haStyleScrollbar } from "../../resources/styles";

View File

@@ -115,9 +115,6 @@ class DateRangePickerElement extends WrappedElement {
color: var(--primary-text-color); color: var(--primary-text-color);
min-width: initial !important; min-width: initial !important;
} }
.daterangepicker:before {
display: none;
}
.daterangepicker:after { .daterangepicker:after {
border-bottom: 6px solid var(--card-background-color); border-bottom: 6px solid var(--card-background-color);
} }

View File

@@ -1,4 +1,5 @@
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select/mwc-select";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { property, state } from "lit/decorators"; import { property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
@@ -7,7 +8,6 @@ import {
deviceAutomationsEqual, deviceAutomationsEqual,
} from "../../data/device_automation"; } from "../../data/device_automation";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-select";
const NO_AUTOMATION_KEY = "NO_AUTOMATION"; const NO_AUTOMATION_KEY = "NO_AUTOMATION";
const UNKNOWN_AUTOMATION_KEY = "UNKNOWN_AUTOMATION"; const UNKNOWN_AUTOMATION_KEY = "UNKNOWN_AUTOMATION";
@@ -90,7 +90,7 @@ export abstract class HaDeviceAutomationPicker<
} }
const value = this._value; const value = this._value;
return html` return html`
<ha-select <mwc-select
.label=${this.label} .label=${this.label}
.value=${value} .value=${value}
@selected=${this._automationChanged} @selected=${this._automationChanged}
@@ -113,7 +113,7 @@ export abstract class HaDeviceAutomationPicker<
</mwc-list-item> </mwc-list-item>
` `
)} )}
</ha-select> </mwc-select>
`; `;
} }
@@ -167,7 +167,7 @@ export abstract class HaDeviceAutomationPicker<
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
ha-select { mwc-select {
width: 100%; width: 100%;
margin-top: 4px; margin-top: 4px;
} }

View File

@@ -1,4 +1,4 @@
import { css, html, LitElement, TemplateResult } from "lit"; import { html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { PolymerChangedEvent } from "../../polymer-types"; import { PolymerChangedEvent } from "../../polymer-types";
@@ -116,12 +116,6 @@ class HaDevicesPicker extends LitElement {
this._updateDevices([...currentDevices, toAdd]); this._updateDevices([...currentDevices, toAdd]);
} }
static override styles = css`
div {
margin-top: 8px;
}
`;
} }
declare global { declare global {

View File

@@ -46,29 +46,11 @@ class HaEntitiesPickerLight extends LitElement {
@property({ type: Array, attribute: "include-unit-of-measurement" }) @property({ type: Array, attribute: "include-unit-of-measurement" })
public includeUnitOfMeasurement?: string[]; public includeUnitOfMeasurement?: string[];
/**
* List of allowed entities to show. Will ignore all other filters.
* @type {Array}
* @attr include-entities
*/
@property({ type: Array, attribute: "include-entities" })
public includeEntities?: string[];
/**
* List of entities to be excluded.
* @type {Array}
* @attr exclude-entities
*/
@property({ type: Array, attribute: "exclude-entities" })
public excludeEntities?: string[];
@property({ attribute: "picked-entity-label" }) @property({ attribute: "picked-entity-label" })
public pickedEntityLabel?: string; public pickedEntityLabel?: string;
@property({ attribute: "pick-entity-label" }) public pickEntityLabel?: string; @property({ attribute: "pick-entity-label" }) public pickEntityLabel?: string;
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.hass) { if (!this.hass) {
return html``; return html``;
@@ -85,8 +67,6 @@ class HaEntitiesPickerLight extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.includeDomains=${this.includeDomains} .includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains} .excludeDomains=${this.excludeDomains}
.includeEntities=${this.includeEntities}
.excludeEntities=${this.excludeEntities}
.includeDeviceClasses=${this.includeDeviceClasses} .includeDeviceClasses=${this.includeDeviceClasses}
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement} .includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
.entityFilter=${this._entityFilter} .entityFilter=${this._entityFilter}
@@ -102,8 +82,6 @@ class HaEntitiesPickerLight extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.includeDomains=${this.includeDomains} .includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains} .excludeDomains=${this.excludeDomains}
.includeEntities=${this.includeEntities}
.excludeEntities=${this.excludeEntities}
.includeDeviceClasses=${this.includeDeviceClasses} .includeDeviceClasses=${this.includeDeviceClasses}
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement} .includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
.entityFilter=${this._entityFilter} .entityFilter=${this._entityFilter}
@@ -116,9 +94,7 @@ class HaEntitiesPickerLight extends LitElement {
private _entityFilter: HaEntityPickerEntityFilterFunc = ( private _entityFilter: HaEntityPickerEntityFilterFunc = (
stateObj: HassEntity stateObj: HassEntity
) => ) => !this.value || !this.value.includes(stateObj.entity_id);
(!this.value || !this.value.includes(stateObj.entity_id)) &&
(!this.entityFilter || this.entityFilter(stateObj));
private get _currentEntities() { private get _currentEntities() {
return this.value || []; return this.value || [];
@@ -138,7 +114,7 @@ class HaEntitiesPickerLight extends LitElement {
const newValue = event.detail.value; const newValue = event.detail.value;
if ( if (
newValue === curValue || newValue === curValue ||
(newValue !== undefined && !isValidEntityId(newValue)) (newValue !== "" && !isValidEntityId(newValue))
) { ) {
return; return;
} }
@@ -171,7 +147,7 @@ class HaEntitiesPickerLight extends LitElement {
} }
static override styles = css` static override styles = css`
div { ha-entity-picker {
margin-top: 8px; margin-top: 8px;
} }
`; `;

View File

@@ -7,7 +7,6 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain"; import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name"; import { computeStateName } from "../../common/entity/compute_state_name";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { PolymerChangedEvent } from "../../polymer-types"; import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-combo-box"; import "../ha-combo-box";
@@ -16,21 +15,18 @@ import "../ha-icon-button";
import "../ha-svg-icon"; import "../ha-svg-icon";
import "./state-badge"; import "./state-badge";
interface HassEntityWithCachedName extends HassEntity {
friendly_name: string;
}
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
// eslint-disable-next-line lit/prefer-static-styles // eslint-disable-next-line lit/prefer-static-styles
const rowRenderer: ComboBoxLitRenderer<HassEntityWithCachedName> = (item) => const rowRenderer: ComboBoxLitRenderer<HassEntity & { friendly_name: string }> =
html`<mwc-list-item graphic="avatar" .twoline=${!!item.entity_id}> (item) =>
${item.state html`<mwc-list-item graphic="avatar" .twoline=${!!item.entity_id}>
? html`<state-badge slot="graphic" .stateObj=${item}></state-badge>` ${item.state
: ""} ? html`<state-badge slot="graphic" .stateObj=${item}></state-badge>`
<span>${item.friendly_name}</span> : ""}
<span slot="secondary">${item.entity_id}</span> <span>${item.friendly_name}</span>
</mwc-list-item>`; <span slot="secondary">${item.entity_id}</span>
</mwc-list-item>`;
@customElement("ha-entity-picker") @customElement("ha-entity-picker")
export class HaEntityPicker extends LitElement { export class HaEntityPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -78,22 +74,6 @@ export class HaEntityPicker extends LitElement {
@property({ type: Array, attribute: "include-unit-of-measurement" }) @property({ type: Array, attribute: "include-unit-of-measurement" })
public includeUnitOfMeasurement?: string[]; public includeUnitOfMeasurement?: string[];
/**
* List of allowed entities to show. Will ignore all other filters.
* @type {Array}
* @attr include-entities
*/
@property({ type: Array, attribute: "include-entities" })
public includeEntities?: string[];
/**
* List of entities to be excluded.
* @type {Array}
* @attr exclude-entities
*/
@property({ type: Array, attribute: "exclude-entities" })
public excludeEntities?: string[];
@property() public entityFilter?: HaEntityPickerEntityFilterFunc; @property() public entityFilter?: HaEntityPickerEntityFilterFunc;
@property({ type: Boolean }) public hideClearIcon = false; @property({ type: Boolean }) public hideClearIcon = false;
@@ -116,7 +96,7 @@ export class HaEntityPicker extends LitElement {
private _initedStates = false; private _initedStates = false;
private _states: HassEntityWithCachedName[] = []; private _states: HassEntity[] = [];
private _getStates = memoizeOne( private _getStates = memoizeOne(
( (
@@ -126,11 +106,9 @@ export class HaEntityPicker extends LitElement {
excludeDomains: this["excludeDomains"], excludeDomains: this["excludeDomains"],
entityFilter: this["entityFilter"], entityFilter: this["entityFilter"],
includeDeviceClasses: this["includeDeviceClasses"], includeDeviceClasses: this["includeDeviceClasses"],
includeUnitOfMeasurement: this["includeUnitOfMeasurement"], includeUnitOfMeasurement: this["includeUnitOfMeasurement"]
includeEntities: this["includeEntities"], ) => {
excludeEntities: this["excludeEntities"] let states: HassEntity[] = [];
): HassEntityWithCachedName[] => {
let states: HassEntityWithCachedName[] = [];
if (!hass) { if (!hass) {
return []; return [];
@@ -144,7 +122,7 @@ export class HaEntityPicker extends LitElement {
state: "", state: "",
last_changed: "", last_changed: "",
last_updated: "", last_updated: "",
context: { id: "", user_id: null, parent_id: null }, context: { id: "", user_id: null },
friendly_name: this.hass!.localize( friendly_name: this.hass!.localize(
"ui.components.entity.entity-picker.no_entities" "ui.components.entity.entity-picker.no_entities"
), ),
@@ -158,30 +136,6 @@ export class HaEntityPicker extends LitElement {
]; ];
} }
if (includeEntities) {
entityIds = entityIds.filter((entityId) =>
this.includeEntities!.includes(entityId)
);
return entityIds
.map((key) => ({
...hass!.states[key],
friendly_name: computeStateName(hass!.states[key]) || key,
}))
.sort((entityA, entityB) =>
caseInsensitiveStringCompare(
entityA.friendly_name,
entityB.friendly_name
)
);
}
if (excludeEntities) {
entityIds = entityIds.filter(
(entityId) => !excludeEntities!.includes(entityId)
);
}
if (includeDomains) { if (includeDomains) {
entityIds = entityIds.filter((eid) => entityIds = entityIds.filter((eid) =>
includeDomains.includes(computeDomain(eid)) includeDomains.includes(computeDomain(eid))
@@ -194,17 +148,10 @@ export class HaEntityPicker extends LitElement {
); );
} }
states = entityIds states = entityIds.sort().map((key) => ({
.map((key) => ({ ...hass!.states[key],
...hass!.states[key], friendly_name: computeStateName(hass!.states[key]) || key,
friendly_name: computeStateName(hass!.states[key]) || key, }));
}))
.sort((entityA, entityB) =>
caseInsensitiveStringCompare(
entityA.friendly_name,
entityB.friendly_name
)
);
if (includeDeviceClasses) { if (includeDeviceClasses) {
states = states.filter( states = states.filter(
@@ -243,7 +190,7 @@ export class HaEntityPicker extends LitElement {
state: "", state: "",
last_changed: "", last_changed: "",
last_updated: "", last_updated: "",
context: { id: "", user_id: null, parent_id: null }, context: { id: "", user_id: null },
friendly_name: this.hass!.localize( friendly_name: this.hass!.localize(
"ui.components.entity.entity-picker.no_match" "ui.components.entity.entity-picker.no_match"
), ),
@@ -281,9 +228,7 @@ export class HaEntityPicker extends LitElement {
this.excludeDomains, this.excludeDomains,
this.entityFilter, this.entityFilter,
this.includeDeviceClasses, this.includeDeviceClasses,
this.includeUnitOfMeasurement, this.includeUnitOfMeasurement
this.includeEntities,
this.excludeEntities
); );
if (this._initedStates) { if (this._initedStates) {
(this.comboBox as any).filteredItems = this._states; (this.comboBox as any).filteredItems = this._states;

View File

@@ -1,3 +1,6 @@
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers"; import { ComboBoxLitRenderer } from "lit-vaadin-helpers";

View File

@@ -1,6 +1,6 @@
import { LitElement, html, TemplateResult, css } from "lit"; import { LitElement, html, TemplateResult, css } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "./ha-select"; import "@material/mwc-select/mwc-select";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import "./ha-textfield"; import "./ha-textfield";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
@@ -193,7 +193,7 @@ export class HaBaseTimeInput extends LitElement {
: ""} : ""}
${this.format === 24 ${this.format === 24
? "" ? ""
: html`<ha-select : html`<mwc-select
.required=${this.required} .required=${this.required}
.value=${this.amPm} .value=${this.amPm}
.disabled=${this.disabled} .disabled=${this.disabled}
@@ -205,7 +205,7 @@ export class HaBaseTimeInput extends LitElement {
> >
<mwc-list-item value="AM">AM</mwc-list-item> <mwc-list-item value="AM">AM</mwc-list-item>
<mwc-list-item value="PM">PM</mwc-list-item> <mwc-list-item value="PM">PM</mwc-list-item>
</ha-select>`} </mwc-select>`}
</div> </div>
`; `;
} }
@@ -280,7 +280,7 @@ export class HaBaseTimeInput extends LitElement {
ha-textfield:last-child { ha-textfield:last-child {
--text-field-border-top-right-radius: var(--mdc-shape-medium); --text-field-border-top-right-radius: var(--mdc-shape-medium);
} }
ha-select { mwc-select {
--mdc-shape-small: 0; --mdc-shape-small: 0;
width: 85px; width: 85px;
} }

View File

@@ -1,5 +1,5 @@
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import "./ha-select"; import "@material/mwc-select/mwc-select";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
@@ -24,7 +24,7 @@ class HaBluePrintPicker extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
public open() { public open() {
const select = this.shadowRoot?.querySelector("ha-select"); const select = this.shadowRoot?.querySelector("mwc-select");
if (select) { if (select) {
// @ts-expect-error // @ts-expect-error
select.menuOpen = true; select.menuOpen = true;
@@ -49,7 +49,7 @@ class HaBluePrintPicker extends LitElement {
return html``; return html``;
} }
return html` return html`
<ha-select <mwc-select
.label=${this.label || .label=${this.label ||
this.hass.localize("ui.components.blueprint-picker.label")} this.hass.localize("ui.components.blueprint-picker.label")}
fixedMenuPosition fixedMenuPosition
@@ -71,7 +71,7 @@ class HaBluePrintPicker extends LitElement {
</mwc-list-item> </mwc-list-item>
` `
)} )}
</ha-select> </mwc-select>
`; `;
} }
@@ -101,7 +101,7 @@ class HaBluePrintPicker extends LitElement {
:host { :host {
display: inline-block; display: inline-block;
} }
ha-select { mwc-select {
width: 100%; width: 100%;
min-width: 200px; min-width: 200px;
display: block; display: block;

View File

@@ -1,5 +1,5 @@
import "@material/mwc-menu"; import "@material/mwc-menu";
import type { Corner, Menu, MenuCorner } from "@material/mwc-menu"; import type { Corner, Menu } from "@material/mwc-menu";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
@@ -7,12 +7,6 @@ import { customElement, property, query } from "lit/decorators";
export class HaButtonMenu extends LitElement { export class HaButtonMenu extends LitElement {
@property() public corner: Corner = "TOP_START"; @property() public corner: Corner = "TOP_START";
@property() public menuCorner: MenuCorner = "START";
@property({ type: Number }) public x?: number;
@property({ type: Number }) public y?: number;
@property({ type: Boolean }) public multi = false; @property({ type: Boolean }) public multi = false;
@property({ type: Boolean }) public activatable = false; @property({ type: Boolean }) public activatable = false;
@@ -38,12 +32,9 @@ export class HaButtonMenu extends LitElement {
</div> </div>
<mwc-menu <mwc-menu
.corner=${this.corner} .corner=${this.corner}
.menuCorner=${this.menuCorner}
.fixed=${this.fixed} .fixed=${this.fixed}
.multi=${this.multi} .multi=${this.multi}
.activatable=${this.activatable} .activatable=${this.activatable}
.y=${this.y}
.x=${this.x}
> >
<slot></slot> <slot></slot>
</mwc-menu> </mwc-menu>

View File

@@ -4,7 +4,6 @@ import { mdiFilterVariant } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { computeStateName } from "../common/entity/compute_state_name"; import { computeStateName } from "../common/entity/compute_state_name";
import { computeDeviceName } from "../data/device_registry"; import { computeDeviceName } from "../data/device_registry";
import { findRelated, RelatedResult } from "../data/search"; import { findRelated, RelatedResult } from "../data/search";
@@ -66,7 +65,6 @@ export class HaRelatedFilterButtonMenu extends LitElement {
.fullwidth=${this.narrow} .fullwidth=${this.narrow}
.corner=${this.corner} .corner=${this.corner}
@closed=${this._onClosed} @closed=${this._onClosed}
@input=${stopPropagation}
> >
<ha-area-picker <ha-area-picker
.label=${this.hass.localize( .label=${this.hass.localize(
@@ -76,7 +74,6 @@ export class HaRelatedFilterButtonMenu extends LitElement {
.value=${this.value?.area} .value=${this.value?.area}
no-add no-add
@value-changed=${this._areaPicked} @value-changed=${this._areaPicked}
@click=${this._preventDefault}
></ha-area-picker> ></ha-area-picker>
<ha-device-picker <ha-device-picker
.label=${this.hass.localize( .label=${this.hass.localize(
@@ -85,7 +82,6 @@ export class HaRelatedFilterButtonMenu extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.value=${this.value?.device} .value=${this.value?.device}
@value-changed=${this._devicePicked} @value-changed=${this._devicePicked}
@click=${this._preventDefault}
></ha-device-picker> ></ha-device-picker>
<ha-entity-picker <ha-entity-picker
.label=${this.hass.localize( .label=${this.hass.localize(
@@ -95,7 +91,6 @@ export class HaRelatedFilterButtonMenu extends LitElement {
.value=${this.value?.entity} .value=${this.value?.entity}
.excludeDomains=${this.excludeDomains} .excludeDomains=${this.excludeDomains}
@value-changed=${this._entityPicked} @value-changed=${this._entityPicked}
@click=${this._preventDefault}
></ha-entity-picker> ></ha-entity-picker>
</mwc-menu-surface> </mwc-menu-surface>
`; `;
@@ -108,17 +103,11 @@ export class HaRelatedFilterButtonMenu extends LitElement {
this._open = true; this._open = true;
} }
private _onClosed(ev): void { private _onClosed(): void {
ev.stopPropagation();
this._open = false; this._open = false;
} }
private _preventDefault(ev) {
ev.preventDefault();
}
private async _entityPicked(ev: CustomEvent) { private async _entityPicked(ev: CustomEvent) {
ev.stopPropagation();
const entityId = ev.detail.value; const entityId = ev.detail.value;
if (!entityId) { if (!entityId) {
fireEvent(this, "related-changed", { value: undefined }); fireEvent(this, "related-changed", { value: undefined });
@@ -138,7 +127,6 @@ export class HaRelatedFilterButtonMenu extends LitElement {
} }
private async _devicePicked(ev: CustomEvent) { private async _devicePicked(ev: CustomEvent) {
ev.stopPropagation();
const deviceId = ev.detail.value; const deviceId = ev.detail.value;
if (!deviceId) { if (!deviceId) {
fireEvent(this, "related-changed", { value: undefined }); fireEvent(this, "related-changed", { value: undefined });
@@ -162,7 +150,6 @@ export class HaRelatedFilterButtonMenu extends LitElement {
} }
private async _areaPicked(ev: CustomEvent) { private async _areaPicked(ev: CustomEvent) {
ev.stopPropagation();
const areaId = ev.detail.value; const areaId = ev.detail.value;
if (!areaId) { if (!areaId) {
fireEvent(this, "related-changed", { value: undefined }); fireEvent(this, "related-changed", { value: undefined });
@@ -186,7 +173,9 @@ export class HaRelatedFilterButtonMenu extends LitElement {
:host { :host {
display: inline-block; display: inline-block;
position: relative; position: relative;
--mdc-menu-min-width: 250px; }
:host([narrow]) {
position: static;
} }
ha-area-picker, ha-area-picker,
ha-device-picker, ha-device-picker,
@@ -196,15 +185,8 @@ export class HaRelatedFilterButtonMenu extends LitElement {
padding: 4px 16px; padding: 4px 16px;
box-sizing: border-box; box-sizing: border-box;
} }
ha-area-picker {
padding-top: 16px;
}
ha-entity-picker {
padding-bottom: 16px;
}
:host([narrow]) ha-area-picker, :host([narrow]) ha-area-picker,
:host([narrow]) ha-device-picker, :host([narrow]) ha-device-picker {
:host([narrow]) ha-entity-picker {
width: 100%; width: 100%;
} }
`; `;

View File

@@ -1,11 +1,11 @@
import { mdiCalendar } from "@mdi/js"; import { mdiCalendar } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement } from "lit"; import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { formatDateNumeric } from "../common/datetime/format_date"; import { formatDateNumeric } from "../common/datetime/format_date";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./ha-svg-icon"; import "./ha-svg-icon";
import "./ha-textfield";
const loadDatePickerDialog = () => import("./ha-dialog-date-picker"); const loadDatePickerDialog = () => import("./ha-dialog-date-picker");
@@ -38,17 +38,17 @@ export class HaDateInput extends LitElement {
@property() public label?: string; @property() public label?: string;
render() { render() {
return html`<ha-textfield return html`<paper-input
.label=${this.label} .label=${this.label}
.disabled=${this.disabled} .disabled=${this.disabled}
iconTrailing no-label-float
@click=${this._openDialog} @click=${this._openDialog}
.value=${this.value .value=${this.value
? formatDateNumeric(new Date(this.value), this.locale) ? formatDateNumeric(new Date(this.value), this.locale)
: ""} : ""}
> >
<ha-svg-icon slot="trailingIcon" .path=${mdiCalendar}></ha-svg-icon> <ha-svg-icon slot="suffix" .path=${mdiCalendar}></ha-svg-icon>
</ha-textfield>`; </paper-input>`;
} }
private _openDialog() { private _openDialog() {
@@ -73,6 +73,9 @@ export class HaDateInput extends LitElement {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
paper-input {
width: 110px;
}
ha-svg-icon { ha-svg-icon {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }

View File

@@ -3,6 +3,7 @@ import "@material/mwc-list/mwc-list";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiCalendar } from "@mdi/js"; import { mdiCalendar } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@@ -18,7 +19,6 @@ import { computeRTLDirection } from "../common/util/compute_rtl";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./date-range-picker"; import "./date-range-picker";
import "./ha-svg-icon"; import "./ha-svg-icon";
import "./ha-textfield";
export interface DateRangePickerRanges { export interface DateRangePickerRanges {
[key: string]: [Date, Date]; [key: string]: [Date, Date];
@@ -61,7 +61,7 @@ export class HaDateRangePicker extends LitElement {
> >
<div slot="input" class="date-range-inputs"> <div slot="input" class="date-range-inputs">
<ha-svg-icon .path=${mdiCalendar}></ha-svg-icon> <ha-svg-icon .path=${mdiCalendar}></ha-svg-icon>
<ha-textfield <paper-input
.value=${formatDateTime(this.startDate, this.hass.locale)} .value=${formatDateTime(this.startDate, this.hass.locale)}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.components.date-range-picker.start_date" "ui.components.date-range-picker.start_date"
@@ -69,16 +69,16 @@ export class HaDateRangePicker extends LitElement {
.disabled=${this.disabled} .disabled=${this.disabled}
@click=${this._handleInputClick} @click=${this._handleInputClick}
readonly readonly
></ha-textfield> ></paper-input>
<ha-textfield <paper-input
.value=${formatDateTime(this.endDate, this.hass.locale)} .value=${formatDateTime(this.endDate, this.hass.locale)}
.label=${this.hass.localize( label=${this.hass.localize(
"ui.components.date-range-picker.end_date" "ui.components.date-range-picker.end_date"
)} )}
.disabled=${this.disabled} .disabled=${this.disabled}
@click=${this._handleInputClick} @click=${this._handleInputClick}
readonly readonly
></ha-textfield> ></paper-input>
</div> </div>
${this.ranges ${this.ranges
? html`<div ? html`<div
@@ -158,13 +158,13 @@ export class HaDateRangePicker extends LitElement {
border-top: 1px solid var(--divider-color); border-top: 1px solid var(--divider-color);
} }
ha-textfield { paper-input {
display: inline-block; display: inline-block;
max-width: 250px; max-width: 250px;
min-width: 200px; min-width: 200px;
} }
ha-textfield:last-child { paper-input:last-child {
margin-left: 8px; margin-left: 8px;
} }
@@ -176,7 +176,7 @@ export class HaDateRangePicker extends LitElement {
} }
@media only screen and (max-width: 500px) { @media only screen and (max-width: 500px) {
ha-textfield { paper-input {
min-width: inherit; min-width: inherit;
} }

View File

@@ -1,13 +1,6 @@
import { mdiChevronDown } from "@mdi/js"; import { mdiChevronDown } from "@mdi/js";
import { import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
css, import { customElement, property, query } from "lit/decorators";
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { nextRender } from "../common/util/render-status"; import { nextRender } from "../common/util/render-status";
@@ -23,21 +16,11 @@ class HaExpansionPanel extends LitElement {
@property() secondary?: string; @property() secondary?: string;
@state() _showContent = this.expanded;
@query(".container") private _container!: HTMLDivElement; @query(".container") private _container!: HTMLDivElement;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div <div class="summary" @click=${this._toggleContainer}>
id="summary"
@click=${this._toggleContainer}
@keydown=${this._toggleContainer}
role="button"
tabindex="0"
aria-expanded=${this.expanded}
aria-controls="sect1"
>
<slot class="header" name="header"> <slot class="header" name="header">
${this.header} ${this.header}
<slot class="secondary" name="secondary">${this.secondary}</slot> <slot class="secondary" name="secondary">${this.secondary}</slot>
@@ -50,37 +33,21 @@ class HaExpansionPanel extends LitElement {
<div <div
class="container ${classMap({ expanded: this.expanded })}" class="container ${classMap({ expanded: this.expanded })}"
@transitionend=${this._handleTransitionEnd} @transitionend=${this._handleTransitionEnd}
role="region"
aria-labelledby="summary"
aria-hidden=${!this.expanded}
tabindex="-1"
> >
${this._showContent ? html`<slot></slot>` : ""} <slot></slot>
</div> </div>
`; `;
} }
protected willUpdate(changedProps: PropertyValues) {
if (changedProps.has("expanded") && this.expanded) {
this._showContent = this.expanded;
}
}
private _handleTransitionEnd() { private _handleTransitionEnd() {
this._container.style.removeProperty("height"); this._container.style.removeProperty("height");
this._showContent = this.expanded;
} }
private async _toggleContainer(ev): Promise<void> { private async _toggleContainer(): Promise<void> {
if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") {
return;
}
ev.preventDefault();
const newExpanded = !this.expanded; const newExpanded = !this.expanded;
fireEvent(this, "expanded-will-change", { expanded: newExpanded }); fireEvent(this, "expanded-will-change", { expanded: newExpanded });
if (newExpanded) { if (newExpanded) {
this._showContent = true;
// allow for dynamic content to be rendered // allow for dynamic content to be rendered
await nextRender(); await nextRender();
} }
@@ -113,21 +80,17 @@ class HaExpansionPanel extends LitElement {
var(--divider-color, #e0e0e0) var(--divider-color, #e0e0e0)
); );
border-radius: var(--ha-card-border-radius, 4px); border-radius: var(--ha-card-border-radius, 4px);
padding: 0 8px;
} }
#summary { .summary {
display: flex; display: flex;
padding: var(--expansion-panel-summary-padding, 0 8px); padding: var(--expansion-panel-summary-padding, 0);
min-height: 48px; min-height: 48px;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
overflow: hidden; overflow: hidden;
font-weight: 500; font-weight: 500;
outline: none;
}
#summary:focus {
background: var(--input-fill-color);
} }
.summary-icon { .summary-icon {
@@ -140,7 +103,6 @@ class HaExpansionPanel extends LitElement {
} }
.container { .container {
padding: var(--expansion-panel-content-padding, 0 8px);
overflow: hidden; overflow: hidden;
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1); transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
height: 0px; height: 0px;

View File

@@ -1,5 +1,6 @@
import { styles } from "@material/mwc-textfield/mwc-textfield.css";
import { mdiClose } from "@mdi/js"; 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 { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
@@ -20,7 +21,7 @@ export class HaFileUpload extends LitElement {
@property() public accept!: string; @property() public accept!: string;
@property() public icon?: string; @property() public icon!: string;
@property() public label!: string; @property() public label!: string;
@@ -38,7 +39,15 @@ export class HaFileUpload extends LitElement {
protected firstUpdated(changedProperties: PropertyValues) { protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties); super.firstUpdated(changedProperties);
if (this.autoOpenFileDialog) { if (this.autoOpenFileDialog) {
this._openFilePicker(); this._input?.click();
}
}
protected updated(changedProperties: PropertyValues) {
if (changedProperties.has("_drag") && !this.uploading) {
(
this.shadowRoot!.querySelector("paper-input-container") as any
)._setFocused(this._drag);
} }
} }
@@ -51,75 +60,51 @@ export class HaFileUpload extends LitElement {
active active
></ha-circular-progress>` ></ha-circular-progress>`
: html` : html`
<label <label for="input">
for="input" <paper-input-container
class="mdc-text-field mdc-text-field--filled ${classMap({ .alwaysFloatLabel=${Boolean(this.value)}
"mdc-text-field--focused": this._drag, @drop=${this._handleDrop}
"mdc-text-field--with-leading-icon": Boolean(this.icon), @dragenter=${this._handleDragStart}
"mdc-text-field--with-trailing-icon": Boolean(this.value), @dragover=${this._handleDragStart}
})}" @dragleave=${this._handleDragEnd}
@drop=${this._handleDrop} @dragend=${this._handleDragEnd}
@dragenter=${this._handleDragStart} class=${classMap({
@dragover=${this._handleDragStart} dragged: this._drag,
@dragleave=${this._handleDragEnd} })}
@dragend=${this._handleDragEnd}
>
<span class="mdc-text-field__ripple"></span>
<span
class="mdc-floating-label ${this.value || this._drag
? "mdc-floating-label--float-above"
: ""}"
id="label"
>${this.label}</span
> >
${this.icon <label for="input" slot="label"> ${this.label} </label>
? html`<span <iron-input slot="input">
class="mdc-text-field__icon mdc-text-field__icon--leading" <input
tabindex="-1" id="input"
> type="file"
<ha-icon-button class="file"
@click=${this._openFilePicker} accept=${this.accept}
.path=${this.icon} @change=${this._handleFilePicked}
></ha-icon-button> />
</span>` ${this.value}
: ""} </iron-input>
<div class="value">${this.value}</div> ${this.value
<input ? html`
id="input" <ha-icon-button
type="file" slot="suffix"
class="mdc-text-field__input file" @click=${this._clearValue}
accept=${this.accept} .label=${this.hass?.localize("ui.common.close") ||
@change=${this._handleFilePicked} "close"}
aria-labelledby="label" .path=${mdiClose}
/> ></ha-icon-button>
${this.value `
? html`<span : html`
class="mdc-text-field__icon mdc-text-field__icon--trailing" <ha-icon-button
tabindex="1" slot="suffix"
> .path=${this.icon}
<ha-icon-button ></ha-icon-button>
slot="suffix" `}
@click=${this._clearValue} </paper-input-container>
.label=${this.hass?.localize("ui.common.close") ||
"close"}
.path=${mdiClose}
></ha-icon-button>
</span>`
: ""}
<span
class="mdc-line-ripple ${this._drag
? "mdc-line-ripple--active"
: ""}"
></span>
</label> </label>
`} `}
`; `;
} }
private _openFilePicker() {
this._input?.click();
}
private _handleDrop(ev: DragEvent) { private _handleDrop(ev: DragEvent) {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
@@ -152,66 +137,40 @@ export class HaFileUpload extends LitElement {
} }
static get styles() { static get styles() {
return [ return css`
styles, paper-input-container {
css` position: relative;
:host { padding: 8px;
display: block; margin: 0 -8px;
} }
.mdc-text-field--filled { paper-input-container.dragged:before {
height: auto; position: var(--layout-fit_-_position);
padding-top: 16px; top: var(--layout-fit_-_top);
cursor: pointer; right: var(--layout-fit_-_right);
} bottom: var(--layout-fit_-_bottom);
.mdc-text-field--filled.mdc-text-field--with-trailing-icon { left: var(--layout-fit_-_left);
padding-top: 28px; background: currentColor;
} content: "";
.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon { opacity: var(--dark-divider-opacity);
color: var(--secondary-text-color); pointer-events: none;
} border-radius: 4px;
.mdc-text-field--filled.mdc-text-field--with-trailing-icon }
.mdc-text-field__icon { input.file {
align-self: flex-end; display: none;
} }
.mdc-text-field__icon--leading { img {
margin-bottom: 12px; max-width: 125px;
} max-height: 125px;
.mdc-text-field--filled .mdc-floating-label--float-above { }
transform: scale(0.75); ha-icon-button {
top: 8px; --mdc-icon-button-size: 24px;
} --mdc-icon-size: 20px;
.dragged:before { }
position: var(--layout-fit_-_position); ha-circular-progress {
top: var(--layout-fit_-_top); display: block;
right: var(--layout-fit_-_right); text-align-last: center;
bottom: var(--layout-fit_-_bottom); }
left: var(--layout-fit_-_left); `;
background: currentColor;
content: "";
opacity: var(--dark-divider-opacity);
pointer-events: none;
border-radius: 4px;
}
.value {
width: 100%;
}
input.file {
display: none;
}
img {
max-width: 100%;
max-height: 125px;
}
ha-icon-button {
--mdc-icon-button-size: 24px;
--mdc-icon-size: 20px;
}
ha-circular-progress {
display: block;
text-align-last: center;
}
`,
];
} }
} }

View File

@@ -1,5 +1,4 @@
import type { Selector } from "../../data/selector"; import { HaFormSchema } from "./types";
import type { HaFormSchema } from "./types";
export const computeInitialHaFormData = ( export const computeInitialHaFormData = (
schema: HaFormSchema[] schema: HaFormSchema[]
@@ -32,25 +31,6 @@ export const computeInitialHaFormData = (
minutes: 0, minutes: 0,
seconds: 0, seconds: 0,
}; };
} else if ("selector" in field) {
const selector: Selector = field.selector;
if ("boolean" in selector) {
data[field.name] = false;
} else if ("text" in selector) {
data[field.name] = "";
} else if ("number" in selector) {
data[field.name] = "min" in selector.number ? selector.number.min : 0;
} else if ("select" in selector) {
if (selector.select.options.length) {
data[field.name] = selector.select.options[0][0];
}
} else if ("duration" in selector) {
data[field.name] = {
hours: 0,
minutes: 0,
seconds: 0,
};
}
} }
}); });
return data; return data;

View File

@@ -9,9 +9,7 @@ export class HaFormConstant extends LitElement implements HaFormElement {
@property() public label!: string; @property() public label!: string;
protected render(): TemplateResult { protected render(): TemplateResult {
return html`<span class="label">${this.label}</span>${this.schema.value return html`<span class="label">${this.label}</span>: ${this.schema.value}`;
? `: ${this.schema.value}`
: ""}`;
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@@ -1,95 +0,0 @@
import "./ha-form";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property } from "lit/decorators";
import type {
HaFormGridSchema,
HaFormDataContainer,
HaFormElement,
HaFormSchema,
} from "./types";
import type { HomeAssistant } from "../../types";
@customElement("ha-form-grid")
export class HaFormGrid extends LitElement implements HaFormElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public data!: HaFormDataContainer;
@property({ attribute: false }) public schema!: HaFormGridSchema;
@property({ type: Boolean }) public disabled = false;
@property() public computeLabel?: (
schema: HaFormSchema,
data?: HaFormDataContainer
) => string;
@property() public computeHelper?: (schema: HaFormSchema) => string;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this.setAttribute("own-margin", "");
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (changedProps.has("schema")) {
if (this.schema.column_min_width) {
this.style.setProperty(
"--form-grid-min-width",
this.schema.column_min_width
);
} else {
this.style.setProperty("--form-grid-min-width", "");
}
}
}
protected render(): TemplateResult {
return html`
${this.schema.schema.map(
(item) =>
html`
<ha-form
.hass=${this.hass}
.data=${this.data}
.schema=${[item]}
.disabled=${this.disabled}
.computeLabel=${this.computeLabel}
.computeHelper=${this.computeHelper}
></ha-form>
`
)}
`;
}
static get styles(): CSSResultGroup {
return css`
:host {
display: grid !important;
grid-template-columns: repeat(
var(--form-grid-column-count, auto-fit),
minmax(var(--form-grid-min-width, 200px), 1fr)
);
grid-gap: 8px;
}
:host > ha-form {
display: block;
margin-bottom: 24px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-form-grid": HaFormGrid;
}
}

View File

@@ -1,3 +1,4 @@
import "@material/mwc-select/mwc-select";
import { mdiMenuDown, mdiMenuUp } from "@mdi/js"; import { mdiMenuDown, mdiMenuUp } from "@mdi/js";
import { import {
css, css,
@@ -10,8 +11,7 @@ import {
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import "../ha-button-menu"; import "../ha-button-menu";
import "../ha-check-list-item"; import { HaCheckListItem } from "../ha-check-list-item";
import type { HaCheckListItem } from "../ha-check-list-item";
import "../ha-checkbox"; import "../ha-checkbox";
import type { HaCheckbox } from "../ha-checkbox"; import type { HaCheckbox } from "../ha-checkbox";
import "../ha-formfield"; import "../ha-formfield";

View File

@@ -1,20 +1,17 @@
import memoizeOne from "memoize-one"; import "@material/mwc-select";
import { html, LitElement, TemplateResult } from "lit"; import type { Select } from "@material/mwc-select";
import { customElement, property } from "lit/decorators"; import "@material/mwc-list/mwc-list-item";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import type { HomeAssistant } from "../../types"; import "../ha-radio";
import type { import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./types";
HaFormElement,
HaFormSelectData, import { stopPropagation } from "../../common/dom/stop_propagation";
HaFormSelectSchema, import type { HaRadio } from "../ha-radio";
} from "./types";
import type { SelectSelector } from "../../data/selector";
import "../ha-selector/ha-selector-select";
@customElement("ha-form-select") @customElement("ha-form-select")
export class HaFormSelect extends LitElement implements HaFormElement { export class HaFormSelect extends LitElement implements HaFormElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public schema!: HaFormSelectSchema; @property({ attribute: false }) public schema!: HaFormSelectSchema;
@property() public data!: HaFormSelectData; @property() public data!: HaFormSelectData;
@@ -23,35 +20,60 @@ export class HaFormSelect extends LitElement implements HaFormElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
private _selectSchema = memoizeOne( @query("mwc-select", true) private _input?: HTMLElement;
(options): SelectSelector => ({
select: { public focus() {
options: options.map((option) => ({ if (this._input) {
value: option[0], this._input.focus();
label: option[1], }
})), }
},
})
);
protected render(): TemplateResult { protected render(): TemplateResult {
if (this.schema.required && 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` return html`
<ha-selector-select <mwc-select
.hass=${this.hass} fixedMenuPosition
.schema=${this.schema} naturalMenuWidth
.value=${this.data}
.label=${this.label} .label=${this.label}
.value=${this.data}
.disabled=${this.disabled} .disabled=${this.disabled}
.required=${this.schema.required} @closed=${stopPropagation}
.selector=${this._selectSchema(this.schema.options)} @selected=${this._valueChanged}
@value-changed=${this._valueChanged} >
></ha-selector-select> ${!this.schema.required
? 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>
`; `;
} }
private _valueChanged(ev: CustomEvent) { private _valueChanged(ev: CustomEvent) {
ev.stopPropagation(); ev.stopPropagation();
let value: string | undefined = ev.detail.value; let value: string | undefined = (ev.target as Select | HaRadio).value;
if (value === this.data) { if (value === this.data) {
return; return;
@@ -65,6 +87,15 @@ export class HaFormSelect extends LitElement implements HaFormElement {
value, value,
}); });
} }
static get styles(): CSSResultGroup {
return css`
mwc-select,
mwc-formfield {
display: block;
}
`;
}
} }
declare global { declare global {

View File

@@ -1,18 +1,10 @@
import { import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { dynamicElement } from "../../common/dom/dynamic-element-directive"; import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import "../ha-alert"; import "../ha-alert";
import "./ha-form-boolean"; import "./ha-form-boolean";
import "./ha-form-constant"; import "./ha-form-constant";
import "./ha-form-grid";
import "./ha-form-float"; import "./ha-form-float";
import "./ha-form-integer"; import "./ha-form-integer";
import "./ha-form-multi_select"; import "./ha-form-multi_select";
@@ -22,20 +14,17 @@ import "./ha-form-string";
import { HaFormElement, HaFormDataContainer, HaFormSchema } from "./types"; import { HaFormElement, HaFormDataContainer, HaFormSchema } from "./types";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
const getValue = (obj, item) => const getValue = (obj, item) => (obj ? obj[item.name] : null);
obj ? (!item.name ? obj : obj[item.name]) : null;
const getError = (obj, item) => (obj && item.name ? obj[item.name] : null);
let selectorImported = false; let selectorImported = false;
@customElement("ha-form") @customElement("ha-form")
export class HaForm extends LitElement implements HaFormElement { export class HaForm extends LitElement implements HaFormElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property() public hass!: HomeAssistant;
@property({ attribute: false }) public data!: HaFormDataContainer; @property() public data!: HaFormDataContainer;
@property({ attribute: false }) public schema!: HaFormSchema[]; @property() public schema!: HaFormSchema[];
@property() public error?: Record<string, string>; @property() public error?: Record<string, string>;
@@ -43,12 +32,7 @@ export class HaForm extends LitElement implements HaFormElement {
@property() public computeError?: (schema: HaFormSchema, error) => string; @property() public computeError?: (schema: HaFormSchema, error) => string;
@property() public computeLabel?: ( @property() public computeLabel?: (schema: HaFormSchema) => string;
schema: HaFormSchema,
data?: HaFormDataContainer
) => string;
@property() public computeHelper?: (schema: HaFormSchema) => string;
public focus() { public focus() {
const root = this.shadowRoot?.querySelector(".root"); const root = this.shadowRoot?.querySelector(".root");
@@ -75,7 +59,7 @@ export class HaForm extends LitElement implements HaFormElement {
} }
} }
protected render(): TemplateResult { protected render() {
return html` return html`
<div class="root"> <div class="root">
${this.error && this.error.base ${this.error && this.error.base
@@ -86,8 +70,7 @@ export class HaForm extends LitElement implements HaFormElement {
` `
: ""} : ""}
${this.schema.map((item) => { ${this.schema.map((item) => {
const error = getError(this.error, item); const error = getValue(this.error, item);
return html` return html`
${error ${error
? html` ? html`
@@ -102,21 +85,15 @@ export class HaForm extends LitElement implements HaFormElement {
.hass=${this.hass} .hass=${this.hass}
.selector=${item.selector} .selector=${item.selector}
.value=${getValue(this.data, item)} .value=${getValue(this.data, item)}
.label=${this._computeLabel(item, this.data)} .label=${this._computeLabel(item)}
.disabled=${this.disabled} .disabled=${this.disabled}
.helper=${this._computeHelper(item)} .required=${item.required}
.required=${item.required || false}
.context=${this._generateContext(item)}
></ha-selector>` ></ha-selector>`
: dynamicElement(`ha-form-${item.type}`, { : dynamicElement(`ha-form-${item.type}`, {
schema: item, schema: item,
data: getValue(this.data, item), data: getValue(this.data, item),
label: this._computeLabel(item, this.data), label: this._computeLabel(item),
disabled: this.disabled, disabled: this.disabled,
hass: this.hass,
computeLabel: this.computeLabel,
computeHelper: this.computeHelper,
context: this._generateContext(item),
})} })}
`; `;
})} })}
@@ -124,50 +101,27 @@ export class HaForm extends LitElement implements HaFormElement {
`; `;
} }
private _generateContext(
schema: HaFormSchema
): Record<string, any> | undefined {
if (!schema.context) {
return undefined;
}
const context = {};
for (const [context_key, data_key] of Object.entries(schema.context)) {
context[context_key] = this.data[data_key];
}
return context;
}
protected createRenderRoot() { protected createRenderRoot() {
const root = super.createRenderRoot(); const root = super.createRenderRoot();
// attach it as soon as possible to make sure we fetch all events. // attach it as soon as possible to make sure we fetch all events.
root.addEventListener("value-changed", (ev) => { root.addEventListener("value-changed", (ev) => {
ev.stopPropagation(); ev.stopPropagation();
const schema = (ev.target as HaFormElement).schema as HaFormSchema; const schema = (ev.target as HaFormElement).schema as HaFormSchema;
const newValue = !schema.name
? ev.detail.value
: { [schema.name]: ev.detail.value };
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: { ...this.data, ...newValue }, value: { ...this.data, [schema.name]: ev.detail.value },
}); });
}); });
return root; return root;
} }
private _computeLabel(schema: HaFormSchema, data: HaFormDataContainer) { private _computeLabel(schema: HaFormSchema) {
return this.computeLabel return this.computeLabel
? this.computeLabel(schema, data) ? this.computeLabel(schema)
: schema : schema
? schema.name ? schema.name
: ""; : "";
} }
private _computeHelper(schema: HaFormSchema) {
return this.computeHelper ? this.computeHelper(schema) : "";
}
private _computeError(error, schema: HaFormSchema | HaFormSchema[]) { private _computeError(error, schema: HaFormSchema | HaFormSchema[]) {
return this.computeError ? this.computeError(error, schema) : error; return this.computeError ? this.computeError(error, schema) : error;
} }

View File

@@ -11,8 +11,7 @@ export type HaFormSchema =
| HaFormSelectSchema | HaFormSelectSchema
| HaFormMultiSelectSchema | HaFormMultiSelectSchema
| HaFormTimeSchema | HaFormTimeSchema
| HaFormSelector | HaFormSelector;
| HaFormGridSchema;
export interface HaFormBaseSchema { export interface HaFormBaseSchema {
name: string; name: string;
@@ -24,14 +23,6 @@ export interface HaFormBaseSchema {
// This value will be set initially when form is loaded // This value will be set initially when form is loaded
suggested_value?: HaFormData; suggested_value?: HaFormData;
}; };
context?: Record<string, string>;
}
export interface HaFormGridSchema extends HaFormBaseSchema {
type: "grid";
name: "";
column_min_width?: string;
schema: HaFormSchema[];
} }
export interface HaFormSelector extends HaFormBaseSchema { export interface HaFormSelector extends HaFormBaseSchema {
@@ -41,7 +32,7 @@ export interface HaFormSelector extends HaFormBaseSchema {
export interface HaFormConstantSchema extends HaFormBaseSchema { export interface HaFormConstantSchema extends HaFormBaseSchema {
type: "constant"; type: "constant";
value?: string; value: string;
} }
export interface HaFormIntegerSchema extends HaFormBaseSchema { export interface HaFormIntegerSchema extends HaFormBaseSchema {
@@ -58,7 +49,7 @@ export interface HaFormSelectSchema extends HaFormBaseSchema {
export interface HaFormMultiSelectSchema extends HaFormBaseSchema { export interface HaFormMultiSelectSchema extends HaFormBaseSchema {
type: "multi_select"; type: "multi_select";
options: Record<string, string> | string[] | Array<[string, string]>; options: Record<string, string> | string[];
} }
export interface HaFormFloatSchema extends HaFormBaseSchema { export interface HaFormFloatSchema extends HaFormBaseSchema {

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