Compare commits

..

2 Commits

Author SHA1 Message Date
Mike Degatano
8f679d12ff Removed unneessary casting 2022-02-20 17:25:48 -05:00
Mike Degatano
20793ecdba Add context data option to template tab 2022-02-20 16:48:20 -05:00
324 changed files with 13197 additions and 8945 deletions

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
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.
[![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/)
- [More information about Home Assistant](https://home-assistant.io)

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
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 { LovelaceConfig } from "../../../../src/data/lovelace";
import { Lovelace } from "../../../../src/panels/lovelace/types";
@@ -20,8 +20,6 @@ class HcLovelace extends LitElement {
@property() public urlPath: string | null = null;
@query("hui-view") private _huiView?: HTMLElement;
protected render(): TemplateResult {
const index = this._viewIndex;
if (index === undefined) {
@@ -80,12 +78,12 @@ class HcLovelace extends LitElement {
this.lovelaceConfig.background;
if (configBackground) {
this._huiView!.style.setProperty(
(this.shadowRoot!.querySelector(
"hui-view"
) as HTMLElement)!.style.setProperty(
"--lovelace-background",
configBackground
);
} else {
this._huiView!.style.removeProperty("--lovelace-background");
}
}
}
@@ -118,9 +116,6 @@ class HcLovelace extends LitElement {
:host > * {
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/roboto";
import "./layout/hc-lovelace";

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

@@ -36,10 +36,6 @@ module.exports = [
category: "misc",
header: "Miscelaneous",
},
{
category: "brand",
header: "Brand",
},
{
category: "user-test",
header: "User Tests",

View File

@@ -78,9 +78,6 @@ class DemoCards extends LitElement {
ha-formfield {
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) {
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(
PAGES[this.page]
.description()
@@ -32,22 +25,9 @@ class PageDescription extends HaMarkdown {
static styles = [
HaMarkdown.styles,
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 {
max-width: 800px;
margin: 16px auto;
margin: 0 auto;
}
.root > *:first-child {
margin-top: 0;

View File

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

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
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>`

View File

@@ -12,98 +12,6 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
import { provideHass } from "../../../../src/fake_data/provide_hass";
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: {
title: string;
@@ -130,8 +38,6 @@ const SCHEMAS: {
select: "Select",
icon: "Icon",
media: "Media",
location: "Location",
entities: "Entities",
},
schema: [
{ name: "addon", selector: { addon: {} } },
@@ -139,7 +45,6 @@ const SCHEMAS: {
{
name: "Attribute",
selector: { attribute: { entity_id: "" } },
context: { filter_entity: "entity" },
},
{ name: "Device", selector: { device: {} } },
{ name: "Duration", selector: { duration: {} } },
@@ -170,14 +75,6 @@ const SCHEMAS: {
media: {},
},
},
{
name: "location",
selector: { location: { radius: true, icon: "mdi:home" } },
},
{
name: "entities",
selector: { entity: { multiple: true } },
},
],
},
{
@@ -418,10 +315,9 @@ class DemoHaForm extends LitElement {
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en");
hass.addEntities(ENTITIES);
mockEntityRegistry(hass);
mockDeviceRegistry(hass, DEVICES);
mockAreaRegistry(hass, AREAS);
mockDeviceRegistry(hass);
mockAreaRegistry(hass);
mockHassioSupervisor(hass);
}

View File

@@ -146,8 +146,6 @@ const SCHEMAS: {
},
boolean: { name: "Boolean", selector: { boolean: {} } },
time: { name: "Time", selector: { time: {} } },
date: { name: "Date", selector: { date: {} } },
datetime: { name: "Date Time", selector: { datetime: {} } },
action: { name: "Action", selector: { action: {} } },
text: {
name: "Text",
@@ -164,44 +162,12 @@ const SCHEMAS: {
},
},
object: { name: "Object", selector: { object: {} } },
select_radio: {
name: "Select (Radio)",
selector: { select: { options: ["Option 1", "Option 2"] } },
},
select: {
name: "Select",
selector: {
select: {
options: [
"Option 1",
"Option 2",
"Option 3",
"Option 4",
"Option 5",
"Option 6",
],
},
},
selector: { select: { options: ["Option 1", "Option 2"] } },
},
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 } } },
},
},
];
@@ -313,7 +279,7 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
can_play: true,
can_expand: false,
children_media_class: null,
thumbnail: "https://brands.home-assistant.io/_/image/logo.png",
thumbnail: null,
},
{
title: "movie.mp4",

View File

@@ -2,8 +2,6 @@
title: Editing design.home-assistant.io
---
![Home Assistant Logo](/images/logo-with-text.png)
# 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.
@@ -43,12 +41,15 @@ import { html, css, LitElement } from "lit";
import { customElement } from "lit/decorators";
import "../../../../src/components/ha-card";
@customElement("demo-user-experience-usability")
export class DemoUserExperienceUsability extends LitElement {
protected render() {
return html`
<ha-card>
<div class="card-content">Hello world!</div>
<div class="card-content">
Hello world!
</div>
</ha-card>
`;
}

View File

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

View File

@@ -14,7 +14,7 @@ import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
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 "../../../src/components/ha-button-menu";
import "../../../src/components/ha-icon-button";
@@ -110,6 +110,8 @@ class HassioAddonStore extends LitElement {
<div class="search">
<search-input
.hass=${this.hass}
no-label-float
no-underline
.filter=${this._filter}
@value-changed=${this._filterChanged}
></search-input>

View File

@@ -1,4 +1,5 @@
import "@material/mwc-button";
import "@material/mwc-select";
import "@material/mwc-list/mwc-list-item";
import {
css,
@@ -13,7 +14,6 @@ import { stopPropagation } from "../../../../src/common/dom/stop_propagation";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-select";
import {
HassioAddonDetails,
HassioAddonSetOptionParams,
@@ -57,7 +57,7 @@ class HassioAddonAudio extends LitElement {
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
${this._inputDevices &&
html`<ha-select
html`<mwc-select
.label=${this.supervisor.localize(
"addon.configuration.audio.input"
)}
@@ -74,9 +74,9 @@ class HassioAddonAudio extends LitElement {
</mwc-list-item>
`
)}
</ha-select>`}
</mwc-select>`}
${this._outputDevices &&
html`<ha-select
html`<mwc-select
.label=${this.supervisor.localize(
"addon.configuration.audio.output"
)}
@@ -93,7 +93,7 @@ class HassioAddonAudio extends LitElement {
>
`
)}
</ha-select>`}
</mwc-select>`}
</div>
<div class="card-actions">
<ha-progress-button @click=${this._saveSettings}>
@@ -119,10 +119,10 @@ class HassioAddonAudio extends LitElement {
.card-actions {
text-align: right;
}
ha-select {
mwc-select {
width: 100%;
}
ha-select:last-child {
mwc-select:last-child {
margin-top: 8px;
}
`,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,11 @@
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-markdown";
import "../../../../src/components/ha-select";
import {
extractApiErrorMessage,
ignoreSupervisorError,
@@ -89,12 +89,11 @@ class HassioDatadiskDialog extends LitElement {
)}
<br /><br />
<ha-select
<mwc-select
.label=${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.select_device"
)}
@selected=${this._select_device}
dialogInitialFocus
>
${this.devices.map(
(device) =>
@@ -102,7 +101,7 @@ class HassioDatadiskDialog extends LitElement {
>${device}</mwc-list-item
>`
)}
</ha-select>
</mwc-select>
`
: this.devices === undefined
? this.dialogParams.supervisor.localize(
@@ -112,11 +111,7 @@ class HassioDatadiskDialog extends LitElement {
"dialog.datadisk_move.no_devices"
)}
<mwc-button
slot="secondaryAction"
@click=${this.closeDialog}
dialogInitialFocus
>
<mwc-button slot="secondaryAction" @click=${this.closeDialog}>
${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.cancel"
)}
@@ -161,7 +156,7 @@ class HassioDatadiskDialog extends LitElement {
haStyle,
haStyleDialog,
css`
ha-select {
mwc-select {
width: 100%;
}
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 memoizeOne from "memoize-one";
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 "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel";
@@ -80,6 +80,8 @@ class HassioHardwareDialog extends LitElement {
></ha-icon-button>
<search-input
.hass=${this.hass}
autofocus
no-label-float
.filter=${this._filter}
@value-changed=${this._handleSearchChange}
.label=${this._dialogParams.supervisor.localize(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,6 @@ import { customElement, property, state } from "lit/decorators";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-alert";
import "../../../src/components/ha-card";
import "../../../src/components/ha-select";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { fetchHassioLogs } from "../../../src/data/hassio/supervisor";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
@@ -71,7 +70,7 @@ class HassioSupervisorLog extends LitElement {
: ""}
${this.hass.userData?.showAdvanced
? html`
<ha-select
<mwc-select
.label=${this.supervisor.localize("system.log.log_provider")}
@selected=${this._setLogProvider}
.value=${this._selectedLogProvider}
@@ -83,7 +82,7 @@ class HassioSupervisorLog extends LitElement {
</mwc-list-item>
`
)}
</ha-select>
</mwc-select>
`
: ""}
@@ -146,7 +145,7 @@ class HassioSupervisorLog extends LitElement {
pre {
white-space: pre-wrap;
}
ha-select {
mwc-select {
width: 100%;
margin-bottom: 4px;
}

View File

@@ -1,8 +1,8 @@
{
"description": "A frontend for Home Assistant",
"description": "A frontend for Home Assistant using the Polymer framework",
"repository": {
"type": "git",
"url": "https://github.com/home-assistant/frontend"
"url": "https://github.com/home-assistant/home-assistant-polymer"
},
"name": "home-assistant-frontend",
"version": "1.0.0",
@@ -46,7 +46,6 @@
"@fullcalendar/daygrid": "5.9.0",
"@fullcalendar/interaction": "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",
"@material/chips": "14.0.0-canary.261f2db59.0",
"@material/data-table": "14.0.0-canary.261f2db59.0",
@@ -79,6 +78,7 @@
"@polymer/iron-icon": "^3.0.1",
"@polymer/iron-input": "^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-item": "^3.0.1",
"@polymer/paper-listbox": "^3.0.1",
@@ -95,7 +95,6 @@
"@vibrant/core": "^3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "^3.2.1-alpha.1",
"@vue/web-component-wrapper": "^1.2.0",
"@webcomponents/scoped-custom-element-registry": "^0.0.5",
"@webcomponents/webcomponentsjs": "^2.2.10",
"app-datepicker": "^5.0.1",
"chart.js": "^3.3.2",
@@ -108,7 +107,7 @@
"fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2",
"hls.js": "^1.1.5",
"home-assistant-js-websocket": "^7.0.1",
"home-assistant-js-websocket": "^6.0.1",
"idb-keyval": "^5.1.3",
"intl-messageformat": "^9.9.1",
"js-yaml": "^4.1.0",
@@ -116,7 +115,7 @@
"leaflet-draw": "^1.0.4",
"lit": "^2.1.2",
"lit-vaadin-helpers": "^0.3.0",
"marked": "^4.0.12",
"marked": "^3.0.2",
"memoize-one": "^5.2.1",
"node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "^0.3.2",
@@ -135,12 +134,13 @@
"vis-network": "^8.5.4",
"vue": "^2.6.12",
"vue2-daterange-picker": "^0.5.1",
"workbox-cacheable-response": "^6.4.2",
"workbox-core": "^6.4.2",
"workbox-expiration": "^6.4.2",
"workbox-precaching": "^6.4.2",
"workbox-routing": "^6.4.2",
"workbox-strategies": "^6.4.2",
"web-animations-js": "^2.3.2",
"workbox-cacheable-response": "^6.1.5",
"workbox-core": "^6.1.5",
"workbox-expiration": "^6.1.5",
"workbox-precaching": "^6.1.5",
"workbox-routing": "^6.1.5",
"workbox-strategies": "^6.1.5",
"xss": "^1.0.9"
},
"devDependencies": {
@@ -169,7 +169,7 @@
"@types/js-yaml": "^4",
"@types/leaflet": "^1",
"@types/leaflet-draw": "^1",
"@types/marked": "^4",
"@types/marked": "^2",
"@types/mocha": "^8",
"@types/qrcode": "^1.4.2",
"@types/sortablejs": "^1",
@@ -196,7 +196,7 @@
"fs-extra": "^7.0.1",
"glob": "^7.2.0",
"gulp": "^4.0.2",
"gulp-flatmap": "^1.0.2",
"gulp-foreach": "^0.1.0",
"gulp-json-transform": "^0.4.6",
"gulp-merge-json": "^1.3.1",
"gulp-rename": "^2.0.0",
@@ -233,7 +233,7 @@
"webpack-dev-server": "^4.3.0",
"webpack-manifest-plugin": "^4.0.2",
"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",
"resolutions": {
@@ -253,6 +253,5 @@
"prettier": {
"trailingComma": "es5",
"arrowParens": "always"
},
"packageManager": "yarn@3.2.0"
}
}

View File

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

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,10 +9,11 @@ import {
mdiCast,
mdiCastConnected,
mdiClock,
mdiEmoticonDead,
mdiFlash,
mdiGestureTapButton,
mdiLanConnect,
mdiLanDisconnect,
mdiLightSwitch,
mdiLock,
mdiLockAlert,
mdiLockClock,
@@ -21,11 +22,14 @@ import {
mdiPowerPlug,
mdiPowerPlugOff,
mdiRestart,
mdiSleep,
mdiTimerSand,
mdiToggleSwitch,
mdiToggleSwitchOff,
mdiCheckCircleOutline,
mdiCloseCircleOutline,
mdiWeatherNight,
mdiZWave,
} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
/**
@@ -108,7 +112,19 @@ export const domainIcon = (
case "switch":
return compareState === "on" ? mdiToggleSwitch : mdiToggleSwitchOff;
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": {

View File

@@ -1,12 +1,12 @@
import { mdiClose, mdiMagnify } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import "./ha-icon-button";
import "./ha-svg-icon";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
import { HomeAssistant } from "../types";
import { fireEvent } from "../common/dom/fire_event";
import "../../components/ha-icon-button";
import "../../components/ha-svg-icon";
import "../../components/ha-textfield";
import type { HaTextField } from "../../components/ha-textfield";
import { HomeAssistant } from "../../types";
import { fireEvent } from "../dom/fire_event";
@customElement("search-input")
class SearchInput extends LitElement {
@@ -35,7 +35,7 @@ class SearchInput extends LitElement {
.autofocus=${this.autofocus}
.label=${this.label || "Search"}
.value=${this.filter || ""}
icon
.icon=${true}
.iconTrailing=${this.filter || this.suffix}
@input=${this._filterInputChanged}
>

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

@@ -70,6 +70,9 @@ export const iconColorCSS = css`
}
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 */
ha-state-icon[data-state="unavailable"] {

View File

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

View File

@@ -1,9 +1,8 @@
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 { customElement, property, state } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import "../ha-circular-progress";
import "../ha-svg-icon";
@customElement("ha-progress-button")
export class HaProgressButton extends LitElement {
@@ -13,53 +12,38 @@ export class HaProgressButton extends LitElement {
@property({ type: Boolean }) public raised = false;
@state() private _result?: "success" | "error";
@query("mwc-button", true) private _button?: Button;
public render(): TemplateResult {
const overlay = this._result || this.progress;
return html`
<mwc-button
?raised=${this.raised}
.disabled=${this.disabled || this.progress}
@click=${this._buttonTapped}
class=${this._result || ""}
>
<slot></slot>
</mwc-button>
${!overlay
? ""
: html`
<div class="progress">
${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>
`}
${this.progress
? html`<div class="progress">
<ha-circular-progress size="small" active></ha-circular-progress>
</div>`
: ""}
`;
}
public actionSuccess(): void {
this._setResult("success");
this._tempClass("success");
}
public actionError(): void {
this._setResult("error");
this._tempClass("error");
}
private _setResult(result: "success" | "error"): void {
this._result = result;
private _tempClass(className: string): void {
this._button!.classList.add(className);
setTimeout(() => {
this._result = undefined;
}, 2000);
this._button!.classList.remove(className);
}, 1000);
}
private _buttonTapped(ev: Event): void {
@@ -85,7 +69,6 @@ export class HaProgressButton extends LitElement {
background-color: var(--success-color);
transition: none;
border-radius: 4px;
pointer-events: none;
}
mwc-button[raised].success {
@@ -98,7 +81,6 @@ export class HaProgressButton extends LitElement {
background-color: var(--error-color);
transition: none;
border-radius: 4px;
pointer-events: none;
}
mwc-button[raised].error {
@@ -107,21 +89,13 @@ export class HaProgressButton extends LitElement {
}
.progress {
bottom: 4px;
bottom: 0;
margin-top: 4px;
position: absolute;
text-align: center;
top: 4px;
top: 0;
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 { restoreScroll } from "../../common/decorators/restore-scroll";
import { fireEvent } from "../../common/dom/fire_event";
import "../search-input";
import "../../common/search/search-input";
import { debounce } from "../../common/util/debounce";
import { nextRender } from "../../common/util/render-status";
import { haStyleScrollbar } from "../../resources/styles";

View File

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

View File

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

View File

@@ -51,8 +51,6 @@ class HaEntitiesPickerLight extends LitElement {
@property({ attribute: "pick-entity-label" }) public pickEntityLabel?: string;
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
@@ -96,9 +94,7 @@ class HaEntitiesPickerLight extends LitElement {
private _entityFilter: HaEntityPickerEntityFilterFunc = (
stateObj: HassEntity
) =>
(!this.value || !this.value.includes(stateObj.entity_id)) &&
(!this.entityFilter || this.entityFilter(stateObj));
) => !this.value || !this.value.includes(stateObj.entity_id);
private get _currentEntities() {
return this.value || [];
@@ -118,7 +114,7 @@ class HaEntitiesPickerLight extends LitElement {
const newValue = event.detail.value;
if (
newValue === curValue ||
(newValue !== undefined && !isValidEntityId(newValue))
(newValue !== "" && !isValidEntityId(newValue))
) {
return;
}
@@ -151,7 +147,7 @@ class HaEntitiesPickerLight extends LitElement {
}
static override styles = css`
div {
ha-entity-picker {
margin-top: 8px;
}
`;

View File

@@ -15,21 +15,18 @@ import "../ha-icon-button";
import "../ha-svg-icon";
import "./state-badge";
interface HassEntityWithCachedName extends HassEntity {
friendly_name: string;
}
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
// eslint-disable-next-line lit/prefer-static-styles
const rowRenderer: ComboBoxLitRenderer<HassEntityWithCachedName> = (item) =>
html`<mwc-list-item graphic="avatar" .twoline=${!!item.entity_id}>
${item.state
? html`<state-badge slot="graphic" .stateObj=${item}></state-badge>`
: ""}
<span>${item.friendly_name}</span>
<span slot="secondary">${item.entity_id}</span>
</mwc-list-item>`;
const rowRenderer: ComboBoxLitRenderer<HassEntity & { friendly_name: string }> =
(item) =>
html`<mwc-list-item graphic="avatar" .twoline=${!!item.entity_id}>
${item.state
? html`<state-badge slot="graphic" .stateObj=${item}></state-badge>`
: ""}
<span>${item.friendly_name}</span>
<span slot="secondary">${item.entity_id}</span>
</mwc-list-item>`;
@customElement("ha-entity-picker")
export class HaEntityPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -99,7 +96,7 @@ export class HaEntityPicker extends LitElement {
private _initedStates = false;
private _states: HassEntityWithCachedName[] = [];
private _states: HassEntity[] = [];
private _getStates = memoizeOne(
(
@@ -110,8 +107,8 @@ export class HaEntityPicker extends LitElement {
entityFilter: this["entityFilter"],
includeDeviceClasses: this["includeDeviceClasses"],
includeUnitOfMeasurement: this["includeUnitOfMeasurement"]
): HassEntityWithCachedName[] => {
let states: HassEntityWithCachedName[] = [];
) => {
let states: HassEntity[] = [];
if (!hass) {
return [];
@@ -125,7 +122,7 @@ export class HaEntityPicker extends LitElement {
state: "",
last_changed: "",
last_updated: "",
context: { id: "", user_id: null, parent_id: null },
context: { id: "", user_id: null },
friendly_name: this.hass!.localize(
"ui.components.entity.entity-picker.no_entities"
),
@@ -193,7 +190,7 @@ export class HaEntityPicker extends LitElement {
state: "",
last_changed: "",
last_updated: "",
context: { id: "", user_id: null, parent_id: null },
context: { id: "", user_id: null },
friendly_name: this.hass!.localize(
"ui.components.entity.entity-picker.no_match"
),

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
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 { customElement, property, query } from "lit/decorators";
@@ -7,12 +7,6 @@ import { customElement, property, query } from "lit/decorators";
export class HaButtonMenu extends LitElement {
@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 activatable = false;
@@ -38,12 +32,9 @@ export class HaButtonMenu extends LitElement {
</div>
<mwc-menu
.corner=${this.corner}
.menuCorner=${this.menuCorner}
.fixed=${this.fixed}
.multi=${this.multi}
.activatable=${this.activatable}
.y=${this.y}
.x=${this.x}
>
<slot></slot>
</mwc-menu>

View File

@@ -41,7 +41,7 @@ export class HaDateInput extends LitElement {
return html`<ha-textfield
.label=${this.label}
.disabled=${this.disabled}
iconTrailing
iconTrailing="calendar"
@click=${this._openDialog}
.value=${this.value
? formatDateNumeric(new Date(this.value), this.locale)

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -1,18 +1,10 @@
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-alert";
import "./ha-form-boolean";
import "./ha-form-constant";
import "./ha-form-grid";
import "./ha-form-float";
import "./ha-form-integer";
import "./ha-form-multi_select";
@@ -22,20 +14,17 @@ import "./ha-form-string";
import { HaFormElement, HaFormDataContainer, HaFormSchema } from "./types";
import { HomeAssistant } from "../../types";
const getValue = (obj, item) =>
obj ? (!item.name ? obj : obj[item.name]) : null;
const getError = (obj, item) => (obj && item.name ? obj[item.name] : null);
const getValue = (obj, item) => (obj ? obj[item.name] : null);
let selectorImported = false;
@customElement("ha-form")
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>;
@@ -75,7 +64,7 @@ export class HaForm extends LitElement implements HaFormElement {
}
}
protected render(): TemplateResult {
protected render() {
return html`
<div class="root">
${this.error && this.error.base
@@ -86,7 +75,7 @@ export class HaForm extends LitElement implements HaFormElement {
`
: ""}
${this.schema.map((item) => {
const error = getError(this.error, item);
const error = getValue(this.error, item);
return html`
${error
@@ -106,17 +95,12 @@ export class HaForm extends LitElement implements HaFormElement {
.disabled=${this.disabled}
.helper=${this._computeHelper(item)}
.required=${item.required || false}
.context=${this._generateContext(item)}
></ha-selector>`
: dynamicElement(`ha-form-${item.type}`, {
schema: item,
data: getValue(this.data, item),
label: this._computeLabel(item, this.data),
disabled: this.disabled,
hass: this.hass,
computeLabel: this.computeLabel,
computeHelper: this.computeHelper,
context: this._generateContext(item),
})}
`;
})}
@@ -124,20 +108,6 @@ 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() {
const root = super.createRenderRoot();
// attach it as soon as possible to make sure we fetch all events.
@@ -145,12 +115,8 @@ export class HaForm extends LitElement implements HaFormElement {
ev.stopPropagation();
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", {
value: { ...this.data, ...newValue },
value: { ...this.data, [schema.name]: ev.detail.value },
});
});
return root;

View File

@@ -11,8 +11,7 @@ export type HaFormSchema =
| HaFormSelectSchema
| HaFormMultiSelectSchema
| HaFormTimeSchema
| HaFormSelector
| HaFormGridSchema;
| HaFormSelector;
export interface HaFormBaseSchema {
name: string;
@@ -24,14 +23,6 @@ export interface HaFormBaseSchema {
// This value will be set initially when form is loaded
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 {
@@ -41,7 +32,7 @@ export interface HaFormSelector extends HaFormBaseSchema {
export interface HaFormConstantSchema extends HaFormBaseSchema {
type: "constant";
value?: string;
value: string;
}
export interface HaFormIntegerSchema extends HaFormBaseSchema {

View File

@@ -1,5 +1,6 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select/mwc-select";
import { mdiCamera } from "@mdi/js";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";

View File

@@ -1,57 +0,0 @@
import { SelectBase } from "@material/mwc-select/mwc-select-base";
import { styles } from "@material/mwc-select/mwc-select.css";
import { css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { debounce } from "../common/util/debounce";
import { nextRender } from "../common/util/render-status";
@customElement("ha-select")
export class HaSelect extends SelectBase {
// @ts-ignore
@property({ type: Boolean }) public icon?: boolean;
protected override renderLeadingIcon() {
if (!this.icon) {
return nothing;
}
return html`<span class="mdc-select__icon"
><slot name="icon"></slot
></span>`;
}
connectedCallback() {
super.connectedCallback();
window.addEventListener("translations-updated", this._translationsUpdated);
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener(
"translations-updated",
this._translationsUpdated
);
}
private _translationsUpdated = debounce(async () => {
await nextRender();
this.layoutOptions();
}, 500);
static override styles = [
styles,
css`
.mdc-select:not(.mdc-select--disabled) .mdc-select__icon {
color: var(--secondary-text-color);
}
.mdc-select__anchor {
width: var(--ha-select-min-width, 200px);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-select": HaSelect;
}
}

View File

@@ -1,10 +1,9 @@
import "../entity/ha-entity-attribute-picker";
import { html, LitElement, PropertyValues } from "lit";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { AttributeSelector } from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event";
@customElement("ha-selector-attribute")
export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
@@ -18,16 +17,11 @@ export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
@property({ type: Boolean }) public disabled = false;
@property() public context?: {
filter_entity?: string;
};
protected render() {
return html`
<ha-entity-attribute-picker
.hass=${this.hass}
.entityId=${this.selector.attribute.entity_id ||
this.context?.filter_entity}
.entityId=${this.selector.attribute.entity_id}
.value=${this.value}
.label=${this.label}
.disabled=${this.disabled}
@@ -35,47 +29,6 @@ export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
></ha-entity-attribute-picker>
`;
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (
// No need to filter value if no value
!this.value ||
// Only adjust value if we used the context
this.selector.attribute.entity_id ||
// Only check if context has changed
!changedProps.has("context")
) {
return;
}
const oldContext = changedProps.get("context") as this["context"];
if (
!this.context ||
oldContext?.filter_entity === this.context.filter_entity
) {
return;
}
// Validate that that the attribute is still valid for this entity, else unselect.
let invalid = false;
if (this.context.filter_entity) {
const stateObj = this.hass.states[this.context.filter_entity];
if (!(stateObj && this.value in stateObj.attributes)) {
invalid = true;
}
} else {
invalid = this.value !== undefined;
}
if (invalid) {
fireEvent(this, "value-changed", {
value: undefined,
});
}
}
}
declare global {

View File

@@ -35,12 +35,9 @@ export class HaBooleanSelector extends LitElement {
static get styles(): CSSResultGroup {
return css`
:host {
height: 56px;
display: flex;
}
ha-formfield {
width: 100%;
margin: 16px 0;
--mdc-typography-body2-font-size: 1em;
}
`;

View File

@@ -1,58 +0,0 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import type { HomeAssistant } from "../../types";
import type { ColorRGBSelector } from "../../data/selector";
import { fireEvent } from "../../common/dom/fire_event";
import { hex2rgb, rgb2hex } from "../../common/color/convert-color";
import "../ha-textfield";
@customElement("ha-selector-color_rgb")
export class HaColorRGBSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: ColorRGBSelector;
@property() public value?: string;
@property() public label?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
protected render() {
return html`
<ha-textfield
type="color"
.value=${this.value ? rgb2hex(this.value as any) : ""}
.label=${this.label || ""}
@change=${this._valueChanged}
></ha-textfield>
`;
}
private _valueChanged(ev: CustomEvent) {
const value = (ev.target as any).value;
fireEvent(this, "value-changed", {
value: hex2rgb(value),
});
}
static styles = css`
:host {
display: flex;
justify-content: flex-end;
align-items: center;
}
ha-textfield {
--text-field-padding: 8px;
min-width: 75px;
flex-grow: 1;
margin: 0 4px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-color_rgb": HaColorRGBSelector;
}
}

View File

@@ -1,58 +0,0 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import type { HomeAssistant } from "../../types";
import type { ColorTempSelector } from "../../data/selector";
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-labeled-slider";
@customElement("ha-selector-color_temp")
export class HaColorTempSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: ColorTempSelector;
@property() public value?: string;
@property() public label?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
protected render() {
return html`
<ha-labeled-slider
pin
icon="hass:thermometer"
.caption=${this.label}
.min=${this.selector.color_temp.min_mireds ?? 153}
.max=${this.selector.color_temp.max_mireds ?? 500}
.value=${this.value}
@change=${this._valueChanged}
></ha-labeled-slider>
`;
}
private _valueChanged(ev: CustomEvent) {
fireEvent(this, "value-changed", {
value: Number((ev.target as any).value),
});
}
static styles = css`
ha-labeled-slider {
--ha-slider-background: -webkit-linear-gradient(
right,
rgb(255, 160, 0) 0%,
white 50%,
rgb(166, 209, 255) 100%
);
/* The color temp minimum value shouldn't be rendered differently. It's not "off". */
--paper-slider-knob-start-border-color: var(--primary-color);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-color_temp": HaColorTempSelector;
}
}

View File

@@ -1,36 +0,0 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import type { HomeAssistant } from "../../types";
import type { DateSelector } from "../../data/selector";
import "../ha-date-input";
@customElement("ha-selector-date")
export class HaDateSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: DateSelector;
@property() public value?: string;
@property() public label?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
protected render() {
return html`
<ha-date-input
.label=${this.label}
.locale=${this.hass.locale}
.disabled=${this.disabled}
.value=${this.value}
>
</ha-date-input>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-date": HaDateSelector;
}
}

View File

@@ -1,73 +0,0 @@
import { css, html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import type { HomeAssistant } from "../../types";
import type { DateTimeSelector } from "../../data/selector";
import type { HaDateInput } from "../ha-date-input";
import type { HaTimeInput } from "../ha-time-input";
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-date-input";
import "../ha-time-input";
@customElement("ha-selector-datetime")
export class HaDateTimeSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: DateTimeSelector;
@property() public value?: string;
@property() public label?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@query("ha-date-input") private _dateInput!: HaDateInput;
@query("ha-time-input") private _timeInput!: HaTimeInput;
protected render() {
const values = this.value?.split(" ");
return html`
<ha-date-input
.label=${this.label}
.locale=${this.hass.locale}
.disabled=${this.disabled}
.value=${values?.[0]}
@value-changed=${this._valueChanged}
>
</ha-date-input>
<ha-time-input
enable-second
.value=${values?.[1] || "00:00:00"}
.locale=${this.hass.locale}
.disabled=${this.disabled}
@value-changed=${this._valueChanged}
></ha-time-input>
`;
}
private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: `${this._dateInput.value} ${this._timeInput.value}`,
});
}
static styles = css`
:host {
display: flex;
align-items: center;
flex-direction: row;
}
ha-date-input {
min-width: 150px;
margin-right: 4px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-datetime": HaDateTimeSelector;
}
}

View File

@@ -1,11 +1,10 @@
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import type { DeviceSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import { DeviceRegistryEntry } from "../../data/device_registry";
import { DeviceSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../device/ha-device-picker";
import "../device/ha-devices-picker";
@customElement("ha-selector-device")
export class HaDeviceSelector extends LitElement {
@@ -31,36 +30,20 @@ export class HaDeviceSelector extends LitElement {
}
protected render() {
if (!this.selector.device.multiple) {
return html`<ha-device-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.deviceFilter=${this._filterDevices}
.includeDeviceClasses=${this.selector.device.entity?.device_class
? [this.selector.device.entity.device_class]
: undefined}
.includeDomains=${this.selector.device.entity?.domain
? [this.selector.device.entity.domain]
: undefined}
.disabled=${this.disabled}
allow-custom-entity
></ha-device-picker> `;
}
return html`
${this.label ? html`<label>${this.label}</label>` : ""}
<ha-devices-picker
.hass=${this.hass}
.value=${this.value}
.includeDeviceClasses=${this.selector.device.entity?.device_class
? [this.selector.device.entity.device_class]
: undefined}
.includeDomains=${this.selector.device.entity?.domain
? [this.selector.device.entity.domain]
: undefined}
></ha-devices-picker>
`;
return html`<ha-device-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.deviceFilter=${this._filterDevices}
.includeDeviceClasses=${this.selector.device.entity?.device_class
? [this.selector.device.entity.device_class]
: undefined}
.includeDomains=${this.selector.device.entity?.domain
? [this.selector.device.entity.domain]
: undefined}
.disabled=${this.disabled}
allow-custom-entity
></ha-device-picker>`;
}
private _filterDevices = (device: DeviceRegistryEntry): boolean => {

View File

@@ -7,7 +7,6 @@ import { EntitySelector } from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../types";
import "../entity/ha-entity-picker";
import "../entity/ha-entities-picker";
@customElement("ha-selector-entity")
export class HaEntitySelector extends SubscribeMixin(LitElement) {
@@ -24,25 +23,14 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
@property({ type: Boolean }) public disabled = false;
protected render() {
if (!this.selector.entity.multiple) {
return html`<ha-entity-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.entityFilter=${this._filterEntities}
.disabled=${this.disabled}
allow-custom-entity
></ha-entity-picker>`;
}
return html`
${this.label ? html`<label>${this.label}</label>` : ""}
<ha-entities-picker
.hass=${this.hass}
.value=${this.value}
.entityFilter=${this._filterEntities}
></ha-entities-picker>
`;
return html`<ha-entity-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.entityFilter=${this._filterEntities}
.disabled=${this.disabled}
allow-custom-entity
></ha-entity-picker>`;
}
public hassSubscribe(): UnsubscribeFunc[] {
@@ -62,13 +50,7 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
private _filterEntities = (entity: HassEntity): boolean => {
if (this.selector.entity?.domain) {
const filterDomain = this.selector.entity.domain;
const filterDomainIsArray = Array.isArray(filterDomain);
const entityDomain = computeStateDomain(entity);
if (
(filterDomainIsArray && !filterDomain.includes(entityDomain)) ||
(!filterDomainIsArray && entityDomain !== filterDomain)
) {
if (computeStateDomain(entity) !== this.selector.entity.domain) {
return false;
}
}

View File

@@ -22,8 +22,6 @@ export class HaIconSelector extends LitElement {
<ha-icon-picker
.label=${this.label}
.value=${this.value}
.fallbackPath=${this.selector.icon.fallbackPath}
.placeholder=${this.selector.icon.placeholder}
@value-changed=${this._valueChanged}
></ha-icon-picker>
`;

View File

@@ -1,80 +0,0 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import type {
LocationSelector,
LocationSelectorValue,
} from "../../data/selector";
import "../../panels/lovelace/components/hui-theme-select-editor";
import type { HomeAssistant } from "../../types";
import type { MarkerLocation } from "../map/ha-locations-editor";
import "../map/ha-locations-editor";
@customElement("ha-selector-location")
export class HaLocationSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: LocationSelector;
@property() public value?: LocationSelectorValue;
@property() public label?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
protected render() {
return html`
<ha-locations-editor
class="flex"
.hass=${this.hass}
.locations=${this._location(this.selector, this.value)}
@location-updated=${this._locationChanged}
@radius-updated=${this._radiusChanged}
></ha-locations-editor>
`;
}
private _location = memoizeOne(
(
selector: LocationSelector,
value?: LocationSelectorValue
): MarkerLocation[] => {
const computedStyles = getComputedStyle(this);
const zoneRadiusColor = selector.location.radius
? computedStyles.getPropertyValue("--zone-radius-color") ||
computedStyles.getPropertyValue("--accent-color")
: undefined;
return [
{
id: "location",
latitude: value?.latitude || this.hass.config.latitude,
longitude: value?.longitude || this.hass.config.longitude,
radius: selector.location.radius ? value?.radius || 1000 : undefined,
radius_color: zoneRadiusColor,
icon: selector.location.icon,
location_editable: true,
radius_editable: true,
},
];
}
);
private _locationChanged(ev: CustomEvent) {
const [latitude, longitude] = ev.detail.location;
fireEvent(this, "value-changed", {
value: { ...this.value, latitude, longitude },
});
}
private _radiusChanged(ev: CustomEvent) {
const radius = ev.detail.radius;
fireEvent(this, "value-changed", { value: { ...this.value, radius } });
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-location": HaLocationSelector;
}
}

View File

@@ -12,7 +12,6 @@ import {
} from "../../data/media-player";
import type { MediaSelector, MediaSelectorValue } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url";
import "../ha-alert";
import "../ha-form/ha-form";
import type { HaFormSchema } from "../ha-form/types";
@@ -51,18 +50,6 @@ export class HaMediaSelector extends LitElement {
getSignedPath(this.hass, thumbnail).then((signedPath) => {
this._thumbnailUrl = signedPath.path;
});
} else if (
thumbnail &&
thumbnail.startsWith("https://brands.home-assistant.io")
) {
// The backend is not aware of the theme used by the users,
// so we rewrite the URL to show a proper icon
this._thumbnailUrl = brandsUrl({
domain: extractDomainFromBrandUrl(thumbnail),
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
});
} else {
this._thumbnailUrl = thumbnail;
}

View File

@@ -19,24 +19,22 @@ export class HaNumberSelector extends LitElement {
@property() public label?: string;
@property({ type: Boolean }) public required = true;
@property({ type: Boolean }) public disabled = false;
protected render() {
return html`${this.selector.number.mode !== "box"
? html`${this.label}<ha-slider
.min=${this.selector.number.min}
.max=${this.selector.number.max}
.value=${this._value}
.step=${this.selector.number.step ?? 1}
.disabled=${this.disabled}
.required=${this.required}
pin
ignore-bar-touch
@change=${this._handleSliderChange}
>
</ha-slider>`
return html`${this.label}
${this.selector.number.mode !== "box"
? html`<ha-slider
.min=${this.selector.number.min}
.max=${this.selector.number.max}
.value=${this._value}
.step=${this.selector.number.step ?? 1}
.disabled=${this.disabled}
pin
ignore-bar-touch
@change=${this._handleSliderChange}
>
</ha-slider>`
: ""}
<ha-textfield
inputMode="numeric"
@@ -46,10 +44,9 @@ export class HaNumberSelector extends LitElement {
class=${classMap({ single: this.selector.number.mode === "box" })}
.min=${this.selector.number.min}
.max=${this.selector.number.max}
.value=${this.value || ""}
.value=${this.value}
.step=${this.selector.number.step ?? 1}
.disabled=${this.disabled}
.required=${this.required}
.suffix=${this.selector.number.unit_of_measurement}
type="number"
autoValidate
@@ -60,16 +57,14 @@ export class HaNumberSelector extends LitElement {
}
private get _value() {
return this.value ?? (this.selector.number.min || 0);
return this.value ?? 0;
}
private _handleInputChange(ev) {
ev.stopPropagation();
const value =
ev.target.value === "" || isNaN(ev.target.value)
? this.required
? this.selector.number.min || 0
: undefined
? undefined
: Number(ev.target.value);
if (this.value === value) {
return;

View File

@@ -1,19 +1,17 @@
import "@material/mwc-formfield/mwc-formfield";
import "@material/mwc-list/mwc-list-item";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import type { SelectOption, SelectSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-select";
import "../ha-radio";
import { SelectOption, SelectSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "@material/mwc-select/mwc-select";
import "@material/mwc-list/mwc-list-item";
@customElement("ha-selector-select")
export class HaSelectSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: SelectSelector;
@property() public selector!: SelectSelector;
@property() public value?: string;
@@ -23,51 +21,24 @@ export class HaSelectSelector extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
if (this.required && this.selector.select.options!.length < 6) {
return html`
<div>
${this.label}
${this.selector.select.options.map((item: string | SelectOption) => {
const value = typeof item === "object" ? item.value : item;
const label = typeof item === "object" ? item.label : item;
return html`<mwc-select
fixedMenuPosition
naturalMenuWidth
.label=${this.label}
.value=${this.value}
.helper=${this.helper}
.disabled=${this.disabled}
@closed=${stopPropagation}
@selected=${this._valueChanged}
>
${this.selector.select.options.map((item: string | SelectOption) => {
const value = typeof item === "object" ? item.value : item;
const label = typeof item === "object" ? item.label : item;
return html`
<mwc-formfield .label=${label}>
<ha-radio
.checked=${value === this.value}
.value=${value}
.disabled=${this.disabled}
@change=${this._valueChanged}
></ha-radio>
</mwc-formfield>
`;
})}
</div>
`;
}
return html`
<ha-select
fixedMenuPosition
naturalMenuWidth
.label=${this.label}
.value=${this.value}
.helper=${this.helper}
.disabled=${this.disabled}
@closed=${stopPropagation}
@selected=${this._valueChanged}
>
${this.selector.select.options.map((item: string | SelectOption) => {
const value = typeof item === "object" ? item.value : item;
const label = typeof item === "object" ? item.label : item;
return html`<mwc-list-item .value=${value}>${label}</mwc-list-item>`;
})}
</ha-select>
`;
return html`<mwc-list-item .value=${value}>${label}</mwc-list-item>`;
})}
</mwc-select>`;
}
private _valueChanged(ev) {
@@ -82,12 +53,9 @@ export class HaSelectSelector extends LitElement {
static get styles(): CSSResultGroup {
return css`
ha-select {
mwc-select {
width: 100%;
}
mwc-formfield {
display: block;
}
`;
}
}

View File

@@ -69,14 +69,10 @@ export class HaTextSelector extends LitElement {
}
private _handleChange(ev) {
let value = ev.target.value;
const value = ev.target.value;
if (this.value === value) {
return;
}
if (value === "" && !this.required) {
value = undefined;
}
fireEvent(this, "value-changed", { value });
}

View File

@@ -1,34 +0,0 @@
import "../../panels/lovelace/components/hui-theme-select-editor";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import type { HomeAssistant } from "../../types";
import type { ThemeSelector } from "../../data/selector";
@customElement("ha-selector-theme")
export class HaThemeSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: ThemeSelector;
@property() public value?: string;
@property() public label?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
protected render() {
return html`
<hui-theme-select-editor
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
></hui-theme-select-editor>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-theme": HaThemeSelector;
}
}

View File

@@ -1,16 +1,13 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import type { Selector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import { Selector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "./ha-selector-action";
import "./ha-selector-addon";
import "./ha-selector-area";
import "./ha-selector-attribute";
import "./ha-selector-boolean";
import "./ha-selector-color-rgb";
import "./ha-selector-date";
import "./ha-selector-datetime";
import "./ha-selector-device";
import "./ha-selector-duration";
import "./ha-selector-entity";
@@ -22,9 +19,6 @@ import "./ha-selector-text";
import "./ha-selector-time";
import "./ha-selector-icon";
import "./ha-selector-media";
import "./ha-selector-theme";
import "./ha-selector-location";
import "./ha-selector-color-temp";
@customElement("ha-selector")
export class HaSelector extends LitElement {
@@ -44,8 +38,6 @@ export class HaSelector extends LitElement {
@property({ type: Boolean }) public required = true;
@property() public context?: Record<string, any>;
public focus() {
this.shadowRoot?.getElementById("selector")?.focus();
}
@@ -65,7 +57,6 @@ export class HaSelector extends LitElement {
disabled: this.disabled,
required: this.required,
helper: this.helper,
context: this.context,
id: "selector",
})}
`;

View File

@@ -42,7 +42,9 @@ export class HaTab extends LitElement {
@keydown=${this._handleKeyDown}
>
${this.narrow ? html`<slot name="icon"></slot>` : ""}
<span class="name">${this.name}</span>
${!this.narrow || this.active
? html`<span class="name">${this.name}</span>`
: ""}
${this._shouldRenderRipple ? html`<mwc-ripple></mwc-ripple>` : ""}
</div>
`;

View File

@@ -9,12 +9,6 @@ export class HaTextField extends TextFieldBase {
@property({ attribute: "error-message" }) public errorMessage?: string;
// @ts-ignore
@property({ type: Boolean }) public icon?: boolean;
// @ts-ignore
@property({ type: Boolean }) public iconTrailing?: boolean;
override updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (
@@ -59,15 +53,6 @@ export class HaTextField extends TextFieldBase {
padding-right: var(--text-field-suffix-padding-right, 0px);
}
.mdc-text-field:not(.mdc-text-field--disabled)
.mdc-text-field__affix--suffix {
color: var(--secondary-text-color);
}
.mdc-text-field__icon {
color: var(--secondary-text-color);
}
input {
text-align: var(--text-field-text-align);
}

View File

@@ -43,7 +43,7 @@ class HaWebRtcPlayer extends LitElement {
private _remoteStream?: MediaStream;
protected override render(): TemplateResult {
protected render(): TemplateResult {
if (this._error) {
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
}
@@ -58,19 +58,12 @@ class HaWebRtcPlayer extends LitElement {
`;
}
public override connectedCallback() {
super.connectedCallback();
if (this.hasUpdated) {
this._startWebRtc();
}
}
public override disconnectedCallback() {
public disconnectedCallback() {
super.disconnectedCallback();
this._cleanUp();
}
protected override updated(changedProperties: PropertyValues<this>) {
protected updated(changedProperties: PropertyValues<this>) {
if (!changedProperties.has("entityid")) {
return;
}

View File

@@ -31,8 +31,6 @@ export class HaYamlEditor extends LitElement {
@property() public label?: string;
@property({ type: Boolean }) public readOnly = false;
@state() private _yaml = "";
public setValue(value): void {
@@ -63,7 +61,6 @@ export class HaYamlEditor extends LitElement {
<ha-code-editor
.hass=${this.hass}
.value=${this._yaml}
.readOnly=${this.readOnly}
mode="yaml"
autocomplete-entities
.error=${this.isValid === false}

View File

@@ -1,337 +0,0 @@
import { animate } from "@lit-labs/motion";
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiClose, mdiDelete } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { fireEvent } from "../../common/dom/fire_event";
import { computeRTLDirection } from "../../common/util/compute_rtl";
import {
MediaClassBrowserSettings,
MediaPlayerItem,
} from "../../data/media-player";
import {
browseLocalMediaPlayer,
removeLocalMedia,
} from "../../data/media_source";
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "../ha-circular-progress";
import "../ha-dialog";
import "../ha-header-bar";
import "../ha-svg-icon";
import "../ha-check-list-item";
import "./ha-media-player-browse";
import "./ha-media-upload-button";
import type { MediaManageDialogParams } from "./show-media-manage-dialog";
@customElement("dialog-media-manage")
class DialogMediaManage extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _currentItem?: MediaPlayerItem;
@state() private _params?: MediaManageDialogParams;
@state() private _uploading = false;
@state() private _deleting = false;
@state() private _selected = new Set<number>();
private _filesChanged = false;
public showDialog(params: MediaManageDialogParams): void {
this._params = params;
this._refreshMedia();
}
public closeDialog() {
if (this._filesChanged && this._params!.onClose) {
this._params!.onClose();
}
this._params = undefined;
this._currentItem = undefined;
this._uploading = false;
this._deleting = false;
this._filesChanged = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._params) {
return html``;
}
const children =
this._currentItem?.children?.filter((child) => !child.can_expand) || [];
let fileIndex = 0;
return html`
<ha-dialog
open
scrimClickAction
escapeKeyAction
hideActions
flexContent
.heading=${this._params.currentItem.title}
@closed=${this.closeDialog}
>
<ha-header-bar slot="heading">
${this._selected.size === 0
? html`
<span slot="title">
${this.hass.localize(
"ui.components.media-browser.file_management.title"
)}
</span>
<ha-media-upload-button
.disabled=${this._deleting}
.hass=${this.hass}
.currentItem=${this._params.currentItem}
@uploading=${this._startUploading}
@media-refresh=${this._doneUploading}
slot="actionItems"
></ha-media-upload-button>
${this._uploading
? ""
: html`
<ha-icon-button
.label=${this.hass.localize("ui.dialogs.generic.close")}
.path=${mdiClose}
dialogAction="close"
slot="actionItems"
class="header_button"
dir=${computeRTLDirection(this.hass)}
></ha-icon-button>
`}
`
: html`
<mwc-button
class="danger"
slot="title"
.disabled=${this._deleting}
.label=${this.hass.localize(
`ui.components.media-browser.file_management.${
this._deleting ? "deleting" : "delete"
}`,
{ count: this._selected.size }
)}
@click=${this._handleDelete}
>
<ha-svg-icon .path=${mdiDelete} slot="icon"></ha-svg-icon>
</mwc-button>
${this._deleting
? ""
: html`
<mwc-button
slot="actionItems"
.label=${`Deselect all`}
@click=${this._handleDeselectAll}
>
<ha-svg-icon
.path=${mdiClose}
slot="icon"
></ha-svg-icon>
</mwc-button>
`}
`}
</ha-header-bar>
${!this._currentItem
? html`
<div class="refresh">
<ha-circular-progress active></ha-circular-progress>
</div>
`
: !children.length
? html`<div class="no-items">
<p>
${this.hass.localize(
"ui.components.media-browser.file_management.no_items"
)}
</p>
${this._currentItem?.children?.length
? html`<span class="folders"
>${this.hass.localize(
"ui.components.media-browser.file_management.folders_not_supported"
)}</span
>`
: ""}
</div>`
: html`
<mwc-list multi @selected=${this._handleSelected}>
${repeat(
children,
(item) => item.media_content_id,
(item) => {
const icon = html`
<ha-svg-icon
slot="graphic"
.path=${MediaClassBrowserSettings[
item.media_class === "directory"
? item.children_media_class || item.media_class
: item.media_class
].icon}
></ha-svg-icon>
`;
return html`
<ha-check-list-item
${animate({
id: item.media_content_id,
skipInitial: true,
})}
graphic="icon"
.disabled=${this._uploading || this._deleting}
.selected=${this._selected.has(fileIndex++)}
.item=${item}
>
${icon} ${item.title}
</ha-check-list-item>
`;
}
)}
</mwc-list>
`}
</ha-dialog>
`;
}
private _handleSelected(ev) {
this._selected = ev.detail.index;
}
private _startUploading() {
this._uploading = true;
this._filesChanged = true;
}
private _doneUploading() {
this._uploading = false;
this._refreshMedia();
}
private _handleDeselectAll() {
if (this._selected.size) {
this._selected = new Set();
}
}
private async _handleDelete() {
if (
!(await showConfirmationDialog(this, {
text: this.hass.localize(
"ui.components.media-browser.file_management.confirm_delete",
{ count: this._selected.size }
),
warning: true,
}))
) {
return;
}
this._filesChanged = true;
this._deleting = true;
const toDelete: MediaPlayerItem[] = [];
let fileIndex = 0;
this._currentItem!.children!.forEach((item) => {
if (item.can_expand) {
return;
}
if (this._selected.has(fileIndex++)) {
toDelete.push(item);
}
});
try {
await Promise.all(
toDelete.map(async (item) => {
await removeLocalMedia(this.hass, item.media_content_id);
this._currentItem = {
...this._currentItem!,
children: this._currentItem!.children!.filter((i) => i !== item),
};
})
);
} finally {
this._deleting = false;
this._selected = new Set();
}
}
private async _refreshMedia() {
this._selected = new Set();
this._currentItem = undefined;
this._currentItem = await browseLocalMediaPlayer(
this.hass,
this._params!.currentItem.media_content_id
);
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-dialog {
--dialog-z-index: 8;
--dialog-content-padding: 0;
}
@media (min-width: 800px) {
ha-dialog {
--mdc-dialog-max-width: 800px;
--dialog-surface-position: fixed;
--dialog-surface-top: 40px;
--mdc-dialog-max-height: calc(100vh - 72px);
}
}
ha-header-bar {
--mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--mdc-theme-surface);
flex-shrink: 0;
border-bottom: 1px solid var(--divider-color, rgba(0, 0, 0, 0.12));
}
ha-media-upload-button,
mwc-button {
--mdc-theme-primary: var(--mdc-theme-on-primary);
}
.danger {
--mdc-theme-primary: var(--error-color);
}
ha-svg-icon[slot="icon"] {
vertical-align: middle;
}
.refresh {
display: flex;
height: 200px;
justify-content: center;
align-items: center;
}
.no-items {
text-align: center;
padding: 16px;
}
.folders {
color: var(--secondary-text-color);
font-style: italic;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-media-manage": DialogMediaManage;
}
}

View File

@@ -1,7 +1,7 @@
import "../ha-header-bar";
import { mdiArrowLeft, mdiClose } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event";
import { computeRTLDirection } from "../../common/util/compute_rtl";
import type {
@@ -13,11 +13,7 @@ import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "../ha-dialog";
import "./ha-media-player-browse";
import "./ha-media-manage-button";
import type {
HaMediaPlayerBrowse,
MediaPlayerItemId,
} from "./ha-media-player-browse";
import type { MediaPlayerItemId } from "./ha-media-player-browse";
import { MediaPlayerBrowseDialogParams } from "./show-media-browser-dialog";
@customElement("dialog-media-player-browse")
@@ -30,8 +26,6 @@ class DialogMediaPlayerBrowse extends LitElement {
@state() private _params?: MediaPlayerBrowseDialogParams;
@query("ha-media-player-browse") private _browser!: HaMediaPlayerBrowse;
public showDialog(params: MediaPlayerBrowseDialogParams): void {
this._params = params;
this._navigateIds = params.navigateIds || [
@@ -86,12 +80,6 @@ class DialogMediaPlayerBrowse extends LitElement {
: this._currentItem.title}
</span>
<ha-media-manage-button
slot="actionItems"
.hass=${this.hass}
.currentItem=${this._currentItem}
@media-refresh=${this._refreshMedia}
></ha-media-manage-button>
<ha-icon-button
.label=${this.hass.localize("ui.dialogs.generic.close")}
.path=${mdiClose}
@@ -136,10 +124,6 @@ class DialogMediaPlayerBrowse extends LitElement {
return this._params!.action || "play";
}
private _refreshMedia() {
this._browser.refresh();
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
@@ -173,10 +157,6 @@ class DialogMediaPlayerBrowse extends LitElement {
flex-shrink: 0;
border-bottom: 1px solid var(--divider-color, rgba(0, 0, 0, 0.12));
}
ha-media-manage-button {
--mdc-theme-primary: var(--mdc-theme-on-primary);
}
`,
];
}

View File

@@ -1,10 +1,9 @@
import "@material/mwc-select";
import "@material/mwc-list/mwc-list-item";
import { css, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { LocalStorage } from "../../common/decorators/local-storage";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { fetchCloudStatus, updateCloudPref } from "../../data/cloud";
import {
CloudTTSInfo,
@@ -16,11 +15,12 @@ import {
MediaPlayerBrowseAction,
MediaPlayerItem,
} from "../../data/media-player";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { buttonLinkStyle } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import "../ha-select";
import "../ha-textarea";
import { buttonLinkStyle } from "../../resources/styles";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { LocalStorage } from "../../common/decorators/local-storage";
import { stopPropagation } from "../../common/dom/stop_propagation";
export interface TtsMediaPickedEvent {
item: MediaPlayerItem;
@@ -103,7 +103,7 @@ class BrowseMediaTTS extends LitElement {
return html`
<div class="cloud-options">
<ha-select
<mwc-select
fixedMenuPosition
naturalMenuWidth
.label=${this.hass.localize(
@@ -117,9 +117,9 @@ class BrowseMediaTTS extends LitElement {
([key, label]) =>
html`<mwc-list-item .value=${key}>${label}</mwc-list-item>`
)}
</ha-select>
</mwc-select>
<ha-select
<mwc-select
fixedMenuPosition
naturalMenuWidth
.label=${this.hass.localize("ui.components.media-browser.tts.gender")}
@@ -131,7 +131,7 @@ class BrowseMediaTTS extends LitElement {
([key, label]) =>
html`<mwc-list-item .value=${key}>${label}</mwc-list-item>`
)}
</ha-select>
</mwc-select>
</div>
`;
}
@@ -256,7 +256,7 @@ class BrowseMediaTTS extends LitElement {
display: flex;
justify-content: space-between;
}
.cloud-options ha-select {
.cloud-options mwc-select {
width: 48%;
}
ha-textarea {

View File

@@ -1,69 +0,0 @@
import { mdiFolderEdit } from "@mdi/js";
import "@material/mwc-button";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { MediaPlayerItem } from "../../data/media-player";
import "../ha-svg-icon";
import { isLocalMediaSourceContentId } from "../../data/media_source";
import type { HomeAssistant } from "../../types";
import { showMediaManageDialog } from "./show-media-manage-dialog";
import { fireEvent } from "../../common/dom/fire_event";
declare global {
interface HASSDomEvents {
"media-refresh": unknown;
}
}
@customElement("ha-media-manage-button")
class MediaManageButton extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() currentItem?: MediaPlayerItem;
@state() _uploading = 0;
protected render(): TemplateResult {
if (
!this.currentItem ||
!isLocalMediaSourceContentId(this.currentItem.media_content_id || "")
) {
return html``;
}
return html`
<mwc-button
.label=${this.hass.localize(
"ui.components.media-browser.file_management.manage"
)}
@click=${this._manage}
>
<ha-svg-icon .path=${mdiFolderEdit} slot="icon"></ha-svg-icon>
</mwc-button>
`;
}
private _manage() {
showMediaManageDialog(this, {
currentItem: this.currentItem!,
onClose: () => fireEvent(this, "media-refresh"),
});
}
static styles = css`
mwc-button {
/* We use icon + text to show disabled state */
--mdc-button-disabled-ink-color: --mdc-theme-primary;
}
ha-svg-icon[slot="icon"],
ha-circular-progress[slot="icon"] {
vertical-align: middle;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-media-manage-button": MediaManageButton;
}
}

View File

@@ -34,24 +34,23 @@ import {
MediaPickedEvent,
MediaPlayerBrowseAction,
} from "../../data/media-player";
import { browseLocalMediaPlayer } from "../../data/media_source";
import { isTTSMediaSource } from "../../data/tts";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url";
import { documentationUrl } from "../../util/documentation-url";
import "../entity/ha-entity-picker";
import "../ha-button-menu";
import "../ha-card";
import type { HaCard } from "../ha-card";
import "../ha-circular-progress";
import "../ha-fab";
import "../ha-icon-button";
import "../ha-svg-icon";
import "./ha-browse-media-tts";
import "../ha-fab";
import { browseLocalMediaPlayer } from "../../data/media_source";
import { isTTSMediaSource } from "../../data/tts";
import type { TtsMediaPickedEvent } from "./ha-browse-media-tts";
import "./ha-browse-media-tts";
declare global {
interface HASSDomEvents {
@@ -132,11 +131,6 @@ export class HaMediaPlayerBrowse extends LitElement {
currentId.media_content_id,
currentId.media_content_type
);
// Update the parent with latest item.
fireEvent(this, "media-browsed", {
ids: this.navigateIds,
current: this._currentItem,
});
} catch (err) {
this._setError(err);
}
@@ -164,11 +158,10 @@ export class HaMediaPlayerBrowse extends LitElement {
const subtitle = this.hass.localize(
`ui.components.media-browser.class.${currentItem.media_class}`
);
const children = currentItem.children || [];
const mediaClass = MediaClassBrowserSettings[currentItem.media_class];
const childrenMediaClass = currentItem.children_media_class
? MediaClassBrowserSettings[currentItem.children_media_class]
: MediaClassBrowserSettings.directory;
const childrenMediaClass =
MediaClassBrowserSettings[currentItem.children_media_class];
return html`
${
@@ -271,7 +264,7 @@ export class HaMediaPlayerBrowse extends LitElement {
@tts-picked=${this._ttsPicked}
></ha-browse-media-tts>
`
: !children.length && !currentItem.not_shown
: !currentItem.children?.length
? html`
<div class="container no-items">
${currentItem.media_content_id ===
@@ -303,7 +296,7 @@ export class HaMediaPlayerBrowse extends LitElement {
childrenMediaClass.thumbnail_ratio === "portrait",
})}"
>
${children.map(
${currentItem.children.map(
(child) => html`
<div
class="child"
@@ -367,23 +360,11 @@ export class HaMediaPlayerBrowse extends LitElement {
</div>
`
)}
${currentItem.not_shown
? html`
<div class="grid not-shown">
<div class="title">
${this.hass.localize(
"ui.components.media-browser.not_shown",
{ count: currentItem.not_shown }
)}
</div>
</div>
`
: ""}
</div>
`
: html`
<mwc-list>
${children.map(
${currentItem.children.map(
(child) => html`
<mwc-list-item
@click=${this._childClicked}
@@ -427,25 +408,6 @@ export class HaMediaPlayerBrowse extends LitElement {
<li divider role="separator"></li>
`
)}
${currentItem.not_shown
? html`
<mwc-list-item
noninteractive
class="not-shown"
.graphic=${mediaClass.show_list_images
? "medium"
: "avatar"}
dir=${computeRTLDirection(this.hass)}
>
<span class="title">
${this.hass.localize(
"ui.components.media-browser.not_shown",
{ count: currentItem.not_shown }
)}
</span>
</mwc-list-item>
`
: ""}
</mwc-list>
`
}
@@ -682,17 +644,6 @@ export class HaMediaPlayerBrowse extends LitElement {
// Thumbnails served by local API require authentication
const signedPath = await getSignedPath(this.hass, thumbnailUrl);
thumbnailUrl = signedPath.path;
} else if (
thumbnailUrl.startsWith("https://brands.home-assistant.io")
) {
// The backend is not aware of the theme used by the users,
// so we rewrite the URL to show a proper icon
thumbnailUrl = brandsUrl({
domain: extractDomainFromBrandUrl(thumbnailUrl),
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
});
}
thumbnailCard.style.backgroundImage = `url(${thumbnailUrl})`;
observer.unobserve(thumbnailCard); // loaded, so no need to observe anymore
@@ -923,17 +874,6 @@ export class HaMediaPlayerBrowse extends LitElement {
transition: height 0.5s, margin 0.5s;
}
.not-shown {
font-style: italic;
color: var(--secondary-text-color);
}
.grid.not-shown {
display: flex;
align-items: center;
text-align: center;
}
/* ============= CHILDREN ============= */
mwc-list {

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