Compare commits

..

1 Commits

Author SHA1 Message Date
Paul Bottein
932120869b Add checkbox mode to boolean selector 2024-08-26 19:14:31 +02:00
345 changed files with 4795 additions and 11314 deletions

View File

@@ -21,12 +21,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4.1.7
with:
ref: dev
- name: Setup Node
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.0.3
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -57,12 +57,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4.1.7
with:
ref: master
- name: Setup Node
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.0.3
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -24,9 +24,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4.1.7
- name: Setup Node
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.0.3
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -58,9 +58,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4.1.7
- name: Setup Node
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.0.3
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -76,9 +76,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4.1.7
- name: Setup Node
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.0.3
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -89,7 +89,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@v4.4.0
uses: actions/upload-artifact@v4.3.6
with:
name: frontend-bundle-stats
path: build/stats/*.json
@@ -100,9 +100,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4.1.7
- name: Setup Node
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.0.3
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -113,7 +113,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@v4.4.0
uses: actions/upload-artifact@v4.3.6
with:
name: supervisor-bundle-stats
path: build/stats/*.json

View File

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

View File

@@ -22,12 +22,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4.1.7
with:
ref: dev
- name: Setup Node
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.0.3
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -58,12 +58,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4.1.7
with:
ref: master
- name: Setup Node
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.0.3
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -16,10 +16,10 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4.1.7
- name: Setup Node
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.0.3
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -21,10 +21,10 @@ jobs:
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4.1.7
- name: Setup Node
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.0.3
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -20,7 +20,7 @@ jobs:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5
@@ -28,7 +28,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.0.3
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -57,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts
uses: actions/upload-artifact@v4.4.0
uses: actions/upload-artifact@v4.3.6
with:
name: wheels
path: dist/home_assistant_frontend*.whl
if-no-files-found: error
- name: Upload translations
uses: actions/upload-artifact@v4.4.0
uses: actions/upload-artifact@v4.3.6
with:
name: translations
path: translations.tar.gz

View File

@@ -23,7 +23,7 @@ jobs:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@v4.2.0
uses: actions/checkout@v4.1.7
- name: Verify version
uses: home-assistant/actions/helpers/verify-version@master
@@ -34,7 +34,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@v4.0.4
uses: actions/setup-node@v4.0.3
with:
node-version-file: ".nvmrc"
cache: yarn

View File

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

View File

@@ -1,7 +1,16 @@
diff --git a/modular/sortable.core.esm.js b/modular/sortable.core.esm.js
index 8b5e49b011713c8859c669069fbe85ce53974e1d..6a0afc92787157b8a31c38cc5f67dfa526090a00 100644
index 93ba17509e2e8583ab241fea6845fbe714c584a2..de0651ddb5dced30d36f7d764da0dd0b441f523f 100644
--- a/modular/sortable.core.esm.js
+++ b/modular/sortable.core.esm.js
@@ -1461,7 +1461,7 @@ Sortable.prototype = /** @lends Sortable.prototype */{
}
target = parent; // store last element
}
- /* jshint boss:true */ while (parent = parent.parentNode);
+ /* jshint boss:true */ while (parent = parent.parentNode || parent.getRootNode().host);
}
_unhideGhostForTarget();
}
@@ -1781,11 +1781,16 @@ Sortable.prototype = /** @lends Sortable.prototype */{
}
if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, !!target) !== false) {
@@ -24,7 +33,7 @@ index 8b5e49b011713c8859c669069fbe85ce53974e1d..6a0afc92787157b8a31c38cc5f67dfa5
}
parentEl = el; // actualization
@@ -1802,7 +1807,12 @@ Sortable.prototype = /** @lends Sortable.prototype */{
@@ -1802,7 +1807,13 @@ Sortable.prototype = /** @lends Sortable.prototype */{
targetRect = getRect(target);
if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, false) !== false) {
capture();
@@ -35,10 +44,11 @@ index 8b5e49b011713c8859c669069fbe85ce53974e1d..6a0afc92787157b8a31c38cc5f67dfa5
+ catch(err) {
+ return completed(false);
+ }
+
parentEl = el; // actualization
changed();
@@ -1849,10 +1859,15 @@ Sortable.prototype = /** @lends Sortable.prototype */{
@@ -1849,12 +1860,17 @@ Sortable.prototype = /** @lends Sortable.prototype */{
_silent = true;
setTimeout(_unsilent, 30);
capture();
@@ -46,6 +56,8 @@ index 8b5e49b011713c8859c669069fbe85ce53974e1d..6a0afc92787157b8a31c38cc5f67dfa5
- el.appendChild(dragEl);
- } else {
- target.parentNode.insertBefore(dragEl, after ? nextSibling : target);
- }
+ try {
+ if (after && !nextSibling) {
+ el.appendChild(dragEl);
@@ -55,6 +67,7 @@ index 8b5e49b011713c8859c669069fbe85ce53974e1d..6a0afc92787157b8a31c38cc5f67dfa5
+ }
+ catch(err) {
+ return completed(false);
}
+ }
// Undo chrome's scroll adjustment (has no effect on other browsers)
if (scrolledPastTop) {
scrollBy(scrolledPastTop, 0, scrollBefore - scrolledPastTop.scrollTop);

File diff suppressed because one or more lines are too long

View File

@@ -6,4 +6,4 @@ enableGlobalCache: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.5.0.cjs
yarnPath: .yarn/releases/yarn-4.4.0.cjs

View File

@@ -27,5 +27,3 @@ A complete guide can be found at the following [link](https://www.home-assistant
Home Assistant is open-source and Apache 2 licensed. Feel free to browse the repository, learn and reuse parts in your own projects.
We use [BrowserStack](https://www.browserstack.com) to test Home Assistant on a large variety of devices.
[![Home Assistant - A project from the Open Home Foundation](https://www.openhomefoundation.org/badges/home-assistant.png)](https://www.openhomefoundation.org/)

View File

@@ -15,29 +15,23 @@ const brotliOptions = {
};
const zopfliOptions = { threshold: 150 };
const compressDistBrotli = (rootDir, modernDir, compressServiceWorker = true) =>
const compressDistBrotli = (rootDir, modernDir) =>
gulp
.src(
[
`${modernDir}/**/${filesGlob}`,
compressServiceWorker ? `${rootDir}/sw-modern.js` : undefined,
].filter(Boolean),
{
base: rootDir,
}
)
.src([`${modernDir}/**/${filesGlob}`, `${rootDir}/sw-modern.js`], {
base: rootDir,
})
.pipe(brotli(brotliOptions))
.pipe(gulp.dest(rootDir));
const compressDistZopfli = (rootDir, modernDir, compressModern = false) =>
const compressDistZopfli = (rootDir, modernDir) =>
gulp
.src(
[
`${rootDir}/**/${filesGlob}`,
compressModern ? undefined : `!${modernDir}/**/${filesGlob}`,
`!${modernDir}/**/${filesGlob}`,
`!${rootDir}/{sw-modern,service_worker}.js`,
`${rootDir}/{authorize,onboarding}.html`,
].filter(Boolean),
],
{ base: rootDir }
)
.pipe(zopfli(zopfliOptions))
@@ -46,20 +40,12 @@ const compressDistZopfli = (rootDir, modernDir, compressModern = false) =>
const compressAppBrotli = () =>
compressDistBrotli(paths.app_output_root, paths.app_output_latest);
const compressHassioBrotli = () =>
compressDistBrotli(
paths.hassio_output_root,
paths.hassio_output_latest,
false
);
compressDistBrotli(paths.hassio_output_root, paths.hassio_output_latest);
const compressAppZopfli = () =>
compressDistZopfli(paths.app_output_root, paths.app_output_latest);
const compressHassioZopfli = () =>
compressDistZopfli(
paths.hassio_output_root,
paths.hassio_output_latest,
true
);
compressDistZopfli(paths.hassio_output_root, paths.hassio_output_latest);
gulp.task("compress-app", gulp.parallel(compressAppBrotli, compressAppZopfli));
gulp.task(

View File

@@ -60,12 +60,6 @@ function copyPolyfills(staticDir) {
npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js.map"),
staticPath("polyfills/")
);
// dialog-polyfill css
copyFileDir(
npmPath("dialog-polyfill/dialog-polyfill.css"),
staticPath("polyfills/")
);
}
function copyLoaderJS(staticDir) {

View File

@@ -139,7 +139,7 @@
</p>
</div>
<div class="section-header">What does Home Assistant Cast do?</div>
<div class="section-header">Wat does Home Assistant Cast do?</div>
<div class="card-content">
<p>
Home Assistant Cast is a receiver application for the Chromecast. When

View File

@@ -36,7 +36,6 @@ import { HassElement } from "../../../../src/state/hass-element";
import { castContext } from "../cast_context";
import "./hc-launch-screen";
import { getPanelTitleFromUrlPath } from "../../../../src/data/panel";
import { checkLovelaceConfig } from "../../../../src/panels/lovelace/common/check-lovelace-config";
const DEFAULT_CONFIG: LovelaceDashboardStrategyConfig = {
strategy: {
@@ -366,9 +365,7 @@ export class HcMain extends HassElement {
this._urlPath || "lovelace"
);
castContext.setApplicationState(title || "");
this._lovelaceConfig = checkLovelaceConfig(
lovelaceConfig
) as LovelaceConfig;
this._lovelaceConfig = lovelaceConfig;
}
private _handleShowDemo(_msg: ShowDemoMessage) {

View File

@@ -111,37 +111,9 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
friendly_name: "Living room Temperature",
},
},
"sensor.outdoor_temperature": {
entity_id: "sensor.outdoor_temperature",
state: "10.5",
attributes: {
state_class: "measurement",
unit_of_measurement: "°C",
device_class: "temperature",
friendly_name: "Outdoor temperature",
},
},
"sensor.outdoor_humidity": {
entity_id: "sensor.outdoor_humidity",
state: "70.4",
attributes: {
state_class: "measurement",
unit_of_measurement: "%",
device_class: "humidity",
friendly_name: "Outdoor humidity",
},
},
"device_tracker.car": {
entity_id: "sensor.outdoor_humidity",
state: "not_home",
attributes: {
friendly_name: "Car",
icon: "mdi:car",
},
},
"media_player.living_room_nest_mini": {
entity_id: "media_player.living_room_nest_mini",
state: "playing",
state: "on",
attributes: {
device_class: "speaker",
volume_level: 0.18,

View File

@@ -9,22 +9,6 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
title: isFrontpageEmbed ? "Home Assistant" : "Demo",
path: "home",
icon: "mdi:home-assistant",
badges: [
{
type: "entity",
entity: "sensor.outdoor_temperature",
color: "red",
},
{
type: "entity",
entity: "sensor.outdoor_humidity",
color: "indigo",
},
{
type: "entity",
entity: "device_tracker.car",
},
],
sections: [
...(isFrontpageEmbed
? []

View File

@@ -1,9 +0,0 @@
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockConfig = (hass: MockHomeAssistant) => {
hass.mockWS("validate_config", () => ({
actions: { valid: true },
conditions: { valid: true },
triggers: { valid: true },
}));
};

View File

@@ -1,6 +0,0 @@
import { Tag } from "../../../src/data/tag";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockTags = (hass: MockHomeAssistant) => {
hass.mockWS("tag/list", () => [{ id: "my-tag", name: "My Tag" }] as Tag[]);
};

View File

@@ -217,22 +217,22 @@ export const basicTrace: DemoTrace = {
id: "1615419646544",
alias: "Ensure Party mode",
description: "",
triggers: [
trigger: [
{
trigger: "state",
platform: "state",
entity_id: "input_boolean.toggle_1",
},
],
conditions: [
condition: [
{
condition: "template",
alias: "Test if Paulus is home",
value_template: "{{ true }}",
},
],
actions: [
action: [
{
action: "input_boolean.toggle",
service: "input_boolean.toggle",
target: {
entity_id: "input_boolean.toggle_4",
},
@@ -268,7 +268,7 @@ export const basicTrace: DemoTrace = {
],
default: [
{
action: "input_boolean.toggle",
service: "input_boolean.toggle",
alias: "Toggle 2",
target: {
entity_id: "input_boolean.toggle_2",
@@ -277,7 +277,7 @@ export const basicTrace: DemoTrace = {
],
},
{
action: "input_boolean.toggle",
service: "input_boolean.toggle",
target: {
entity_id: "input_boolean.toggle_4",
},

View File

@@ -31,8 +31,8 @@ export const mockDemoTrace = (
],
},
config: {
triggers: [],
actions: [],
trigger: [],
action: [],
},
context: {
id: "abcd",

View File

@@ -133,17 +133,17 @@ export const motionLightTrace: DemoTrace = {
config: {
mode: "restart",
max_exceeded: "silent",
triggers: [
trigger: [
{
trigger: "state",
platform: "state",
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
from: "off",
to: "on",
},
],
actions: [
action: [
{
action: "light.turn_on",
service: "light.turn_on",
target: {
entity_id: "light.elgato_key_light_air",
},
@@ -162,7 +162,7 @@ export const motionLightTrace: DemoTrace = {
delay: 0,
},
{
action: "light.turn_off",
service: "light.turn_off",
target: {
entity_id: "light.elgato_key_light_air",
},

View File

@@ -48,7 +48,7 @@ const ACTIONS = [
{
wait_for_trigger: [
{
trigger: "state",
platform: "state",
entity_id: "input_boolean.toggle_1",
},
],
@@ -121,7 +121,7 @@ const ACTIONS = [
];
const initialAction: Action = {
action: "light.turn_on",
service: "light.turn_on",
target: {
entity_id: "light.kitchen",
},
@@ -142,7 +142,7 @@ export class DemoAutomationDescribeAction extends LitElement {
<div class="action">
<span>
${this._action
? describeAction(this.hass, [], [], this._action)
? describeAction(this.hass, [], [], [], this._action)
: "<invalid YAML>"}
</span>
<ha-yaml-editor
@@ -155,7 +155,7 @@ export class DemoAutomationDescribeAction extends LitElement {
${ACTIONS.map(
(conf) => html`
<div class="action">
<span>${describeAction(this.hass, [], [], conf as any)}</span>
<span>${describeAction(this.hass, [], [], [], conf as any)}</span>
<pre>${dump(conf)}</pre>
</div>
`

View File

@@ -22,52 +22,46 @@ const ENTITIES = [
];
const triggers = [
{ trigger: "state", entity_id: "light.kitchen", from: "off", to: "on" },
{ trigger: "mqtt" },
{ platform: "state", entity_id: "light.kitchen", from: "off", to: "on" },
{ platform: "mqtt" },
{
trigger: "geo_location",
platform: "geo_location",
source: "test_source",
zone: "zone.home",
event: "enter",
},
{ trigger: "homeassistant", event: "start" },
{ platform: "homeassistant", event: "start" },
{
trigger: "numeric_state",
platform: "numeric_state",
entity_id: "light.kitchen",
attribute: "brightness",
below: 80,
above: 20,
},
{ trigger: "sun", event: "sunset" },
{ trigger: "time_pattern" },
{ trigger: "time_pattern", hours: "*", minutes: "/5", seconds: "10" },
{ trigger: "webhook" },
{ trigger: "persistent_notification" },
{ platform: "sun", event: "sunset" },
{ platform: "time_pattern" },
{ platform: "time_pattern", hours: "*", minutes: "/5", seconds: "10" },
{ platform: "webhook" },
{ platform: "persistent_notification" },
{
trigger: "zone",
platform: "zone",
entity_id: "person.person",
zone: "zone.home",
event: "enter",
},
{ trigger: "tag" },
{ trigger: "time", at: "15:32" },
{ trigger: "template" },
{ trigger: "conversation", command: "Turn on the lights" },
{ platform: "tag" },
{ platform: "time", at: "15:32" },
{ platform: "template" },
{ platform: "conversation", command: "Turn on the lights" },
{
trigger: "conversation",
platform: "conversation",
command: ["Turn on the lights", "Turn the lights on"],
},
{ trigger: "event", event_type: "homeassistant_started" },
{
triggers: [
{ trigger: "state", entity_id: "light.kitchen", to: "on" },
{ trigger: "state", entity_id: "light.kitchen", to: "off" },
],
},
{ platform: "event", event_type: "homeassistant_started" },
];
const initialTrigger: Trigger = {
trigger: "state",
platform: "state",
entity_id: "light.kitchen",
};

View File

@@ -11,6 +11,7 @@ import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervis
import type { ConditionWithShorthand } from "../../../../src/data/automation";
import "../../../../src/panels/config/automation/condition/ha-automation-condition";
import { HaDeviceCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-device";
import { HaLogicalCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-logical";
import HaNumericStateCondition from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-numeric_state";
import { HaStateCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-state";
import { HaSunCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-sun";
@@ -18,67 +19,62 @@ import { HaTemplateCondition } from "../../../../src/panels/config/automation/co
import { HaTimeCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-time";
import { HaTriggerCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-trigger";
import { HaZoneCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-zone";
import { HaAndCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-and";
import { HaOrCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-or";
import { HaNotCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-not";
const SCHEMAS: { name: string; conditions: ConditionWithShorthand[] }[] = [
{
name: "State",
conditions: [{ ...HaStateCondition.defaultConfig }],
conditions: [{ condition: "state", ...HaStateCondition.defaultConfig }],
},
{
name: "Numeric State",
conditions: [{ ...HaNumericStateCondition.defaultConfig }],
conditions: [
{ condition: "numeric_state", ...HaNumericStateCondition.defaultConfig },
],
},
{
name: "Sun",
conditions: [{ ...HaSunCondition.defaultConfig }],
conditions: [{ condition: "sun", ...HaSunCondition.defaultConfig }],
},
{
name: "Zone",
conditions: [{ ...HaZoneCondition.defaultConfig }],
conditions: [{ condition: "zone", ...HaZoneCondition.defaultConfig }],
},
{
name: "Time",
conditions: [{ ...HaTimeCondition.defaultConfig }],
conditions: [{ condition: "time", ...HaTimeCondition.defaultConfig }],
},
{
name: "Template",
conditions: [{ ...HaTemplateCondition.defaultConfig }],
conditions: [
{ condition: "template", ...HaTemplateCondition.defaultConfig },
],
},
{
name: "Device",
conditions: [{ ...HaDeviceCondition.defaultConfig }],
conditions: [{ condition: "device", ...HaDeviceCondition.defaultConfig }],
},
{
name: "And",
conditions: [{ ...HaAndCondition.defaultConfig }],
conditions: [{ condition: "and", ...HaLogicalCondition.defaultConfig }],
},
{
name: "Or",
conditions: [{ ...HaOrCondition.defaultConfig }],
conditions: [{ condition: "or", ...HaLogicalCondition.defaultConfig }],
},
{
name: "Not",
conditions: [{ ...HaNotCondition.defaultConfig }],
conditions: [{ condition: "not", ...HaLogicalCondition.defaultConfig }],
},
{
name: "Trigger",
conditions: [{ ...HaTriggerCondition.defaultConfig }],
conditions: [{ condition: "trigger", ...HaTriggerCondition.defaultConfig }],
},
{
name: "Shorthand",
conditions: [
{
...HaAndCondition.defaultConfig,
},
{
...HaOrCondition.defaultConfig,
},
{
...HaNotCondition.defaultConfig,
},
{ and: HaLogicalCondition.defaultConfig.conditions },
{ or: HaLogicalCondition.defaultConfig.conditions },
{ not: HaLogicalCondition.defaultConfig.conditions },
],
},
];

View File

@@ -8,9 +8,6 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
import { mockConfig } from "../../../../demo/src/stubs/config";
import { mockTags } from "../../../../demo/src/stubs/tags";
import { mockAuth } from "../../../../demo/src/stubs/auth";
import type { Trigger } from "../../../../src/data/automation";
import { HaGeolocationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-geo_location";
import { HaEventTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-event";
@@ -29,53 +26,59 @@ import { HaStateTrigger } from "../../../../src/panels/config/automation/trigger
import { HaMQTTTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt";
import "../../../../src/panels/config/automation/trigger/ha-automation-trigger";
import { HaConversationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-conversation";
import { HaTriggerList } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-list";
const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
{
name: "State",
triggers: [{ ...HaStateTrigger.defaultConfig }],
triggers: [{ platform: "state", ...HaStateTrigger.defaultConfig }],
},
{
name: "MQTT",
triggers: [{ ...HaMQTTTrigger.defaultConfig }],
triggers: [{ platform: "mqtt", ...HaMQTTTrigger.defaultConfig }],
},
{
name: "GeoLocation",
triggers: [{ ...HaGeolocationTrigger.defaultConfig }],
triggers: [
{ platform: "geo_location", ...HaGeolocationTrigger.defaultConfig },
],
},
{
name: "Home Assistant",
triggers: [{ ...HaHassTrigger.defaultConfig }],
triggers: [{ platform: "homeassistant", ...HaHassTrigger.defaultConfig }],
},
{
name: "Numeric State",
triggers: [{ ...HaNumericStateTrigger.defaultConfig }],
triggers: [
{ platform: "numeric_state", ...HaNumericStateTrigger.defaultConfig },
],
},
{
name: "Sun",
triggers: [{ ...HaSunTrigger.defaultConfig }],
triggers: [{ platform: "sun", ...HaSunTrigger.defaultConfig }],
},
{
name: "Time Pattern",
triggers: [{ ...HaTimePatternTrigger.defaultConfig }],
triggers: [
{ platform: "time_pattern", ...HaTimePatternTrigger.defaultConfig },
],
},
{
name: "Webhook",
triggers: [{ ...HaWebhookTrigger.defaultConfig }],
triggers: [{ platform: "webhook", ...HaWebhookTrigger.defaultConfig }],
},
{
name: "Persistent Notification",
triggers: [
{
platform: "persistent_notification",
...HaPersistentNotificationTrigger.defaultConfig,
},
],
@@ -83,47 +86,43 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
{
name: "Zone",
triggers: [{ ...HaZoneTrigger.defaultConfig }],
triggers: [{ platform: "zone", ...HaZoneTrigger.defaultConfig }],
},
{
name: "Tag",
triggers: [{ ...HaTagTrigger.defaultConfig }],
triggers: [{ platform: "tag", ...HaTagTrigger.defaultConfig }],
},
{
name: "Time",
triggers: [{ ...HaTimeTrigger.defaultConfig }],
triggers: [{ platform: "time", ...HaTimeTrigger.defaultConfig }],
},
{
name: "Template",
triggers: [{ ...HaTemplateTrigger.defaultConfig }],
triggers: [{ platform: "template", ...HaTemplateTrigger.defaultConfig }],
},
{
name: "Event",
triggers: [{ ...HaEventTrigger.defaultConfig }],
triggers: [{ platform: "event", ...HaEventTrigger.defaultConfig }],
},
{
name: "Device Trigger",
triggers: [{ ...HaDeviceTrigger.defaultConfig }],
triggers: [{ platform: "device", ...HaDeviceTrigger.defaultConfig }],
},
{
name: "Sentence",
triggers: [
{ ...HaConversationTrigger.defaultConfig },
{ platform: "conversation", ...HaConversationTrigger.defaultConfig },
{
trigger: "conversation",
platform: "conversation",
command: ["Turn on the lights", "Turn the lights on"],
},
],
},
{
name: "Trigger list",
triggers: [{ ...HaTriggerList.defaultConfig }],
},
];
@customElement("demo-automation-editor-trigger")
@@ -143,9 +142,6 @@ export class DemoAutomationEditorTrigger extends LitElement {
mockDeviceRegistry(hass);
mockAreaRegistry(hass);
mockHassioSupervisor(hass);
mockConfig(hass);
mockTags(hass);
mockAuth(hass);
}
protected render(): TemplateResult {

View File

@@ -64,7 +64,6 @@ const DEVICES: DeviceRegistryEntry[] = [
labels: [],
created_at: 0,
modified_at: 0,
primary_config_entry: null,
},
{
area_id: "backyard",
@@ -87,7 +86,6 @@ const DEVICES: DeviceRegistryEntry[] = [
labels: [],
created_at: 0,
modified_at: 0,
primary_config_entry: null,
},
{
area_id: null,
@@ -110,7 +108,6 @@ const DEVICES: DeviceRegistryEntry[] = [
labels: [],
created_at: 0,
modified_at: 0,
primary_config_entry: null,
},
];

View File

@@ -64,7 +64,6 @@ const DEVICES: DeviceRegistryEntry[] = [
labels: [],
created_at: 0,
modified_at: 0,
primary_config_entry: null,
},
{
area_id: "backyard",
@@ -87,7 +86,6 @@ const DEVICES: DeviceRegistryEntry[] = [
labels: [],
created_at: 0,
modified_at: 0,
primary_config_entry: null,
},
{
area_id: null,
@@ -110,7 +108,6 @@ const DEVICES: DeviceRegistryEntry[] = [
labels: [],
created_at: 0,
modified_at: 0,
primary_config_entry: null,
},
];

View File

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

View File

@@ -1,93 +0,0 @@
import { css, html, LitElement } from "lit";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-markdown";
import { customElement } from "lit/decorators";
interface MarkdownContent {
content: string;
breaks: boolean;
allowSvg: boolean;
lazyImages: boolean;
}
const mdContentwithDefaults = (md: Partial<MarkdownContent>) =>
({
breaks: false,
allowSvg: false,
lazyImages: false,
...md,
}) as MarkdownContent;
const generateContent = (md) => `
\`\`\`json
${JSON.stringify({ ...md, content: undefined })}
\`\`\`
---
${md.content}
`;
const markdownContents: MarkdownContent[] = [
mdContentwithDefaults({
content: "_Hello_ **there** 👋, ~~nice~~ of you ||to|| show up.",
}),
...[true, false].map((breaks) =>
mdContentwithDefaults({
breaks,
content: `
![image](https://img.shields.io/badge/markdown-rendering-brightgreen)
![image](https://img.shields.io/badge/markdown-rendering-blue)
> [!TIP]
> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer dictum quis ante eu eleifend. Integer sed [consectetur est, nec elementum magna](#). Fusce lobortis lectus ac rutrum tincidunt. Quisque suscipit gravida ante, in convallis risus vulputate non.
key | description
-- | --
lorem | ipsum
- list item 1
- list item 2
`,
})
),
];
@customElement("demo-misc-ha-markdown")
export class DemoMiscMarkdown extends LitElement {
protected render() {
return html`
<div class="container">
${markdownContents.map(
(md) =>
html`<ha-card>
<ha-markdown
.content=${generateContent(md)}
.breaks=${md.breaks}
.allowSvg=${md.allowSvg}
.lazyImages=${md.lazyImages}
></ha-markdown>
</ha-card>`
)}
</div>
`;
}
static get styles() {
return css`
ha-card {
margin: 12px;
padding: 12px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-misc-ha-markdown": DemoMiscMarkdown;
}
}

View File

@@ -232,7 +232,6 @@ const createDeviceRegistryEntries = (
labels: [],
created_at: 0,
modified_at: 0,
primary_config_entry: null,
},
];

View File

@@ -15,7 +15,6 @@ import { LocalizeFunc } from "../../../src/common/translations/localize";
import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-textfield";
import "../../../src/components/ha-password-field";
import "../../../src/components/ha-radio";
import type { HaRadio } from "../../../src/components/ha-radio";
import {
@@ -262,21 +261,23 @@ export class SupervisorBackupContent extends LitElement {
: ""}
${this.backupHasPassword
? html`
<ha-password-field
<ha-textfield
.label=${this._localize("password")}
type="password"
name="backupPassword"
.value=${this.backupPassword}
@change=${this._handleTextValueChanged}
>
</ha-password-field>
</ha-textfield>
${!this.backup
? html`<ha-password-field
? html`<ha-textfield
.label=${this._localize("confirm_password")}
type="password"
name="confirmBackupPassword"
.value=${this.confirmBackupPassword}
@change=${this._handleTextValueChanged}
>
</ha-password-field>`
</ha-textfield>`
: ""}
`
: ""}

View File

@@ -13,12 +13,10 @@ import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel";
import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-password-field";
import "../../../../src/components/ha-radio";
import "../../../../src/components/ha-textfield";
import type { HaTextField } from "../../../../src/components/ha-textfield";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import {
AccessPoints,
@@ -36,6 +34,7 @@ import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { HassioNetworkDialogParams } from "./show-dialog-network";
import type { HaTextField } from "../../../../src/components/ha-textfield";
const IP_VERSIONS = ["ipv4", "ipv6"];
@@ -247,8 +246,9 @@ export class DialogHassioNetwork
${this._wifiConfiguration.auth === "wpa-psk" ||
this._wifiConfiguration.auth === "wep"
? html`
<ha-password-field
<ha-textfield
class="flex-auto"
type="password"
id="psk"
.label=${this.supervisor.localize(
"dialog.network.wifi_password"
@@ -256,7 +256,7 @@ export class DialogHassioNetwork
version="wifi"
@change=${this._handleInputValueChangedWifi}
>
</ha-password-field>
</ha-textfield>
`
: ""}
`

View File

@@ -25,8 +25,8 @@ import type { HomeAssistant } from "../../../../src/types";
import { HassioRepositoryDialogParams } from "./show-dialog-repositories";
import type { HaTextField } from "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-md-list";
import "../../../../src/components/ha-md-list-item";
import "../../../../src/components/ha-list-new";
import "../../../../src/components/ha-list-item-new";
@customElement("dialog-hassio-repositories")
class HassioRepositoriesDialog extends LitElement {
@@ -107,11 +107,11 @@ class HassioRepositoriesDialog extends LitElement {
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<div class="form">
<ha-md-list>
<ha-list-new>
${repositories.length
? repositories.map(
(repo) => html`
<ha-md-list-item class="option">
<ha-list-item-new class="option">
${repo.name}
<div slot="supporting-text">
<div>${repo.maintainer}</div>
@@ -142,11 +142,11 @@ class HassioRepositoriesDialog extends LitElement {
)}
</simple-tooltip>
</div>
</ha-md-list-item>
</ha-list-item-new>
`
)
: html`<ha-md-list-item> No repositories </ha-md-list-item>`}
</ha-md-list>
: html`<ha-list-item-new> No repositories </ha-list-item-new>`}
</ha-list-new>
<div class="layout horizontal bottom">
<ha-textfield
class="flex-auto"
@@ -209,7 +209,7 @@ class HassioRepositoriesDialog extends LitElement {
div.delete ha-icon-button {
color: var(--error-color);
}
ha-md-list-item {
ha-list-item-new {
position: relative;
}
`,

View File

@@ -25,15 +25,15 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "7.25.6",
"@babel/runtime": "7.25.4",
"@braintree/sanitize-url": "7.1.0",
"@codemirror/autocomplete": "6.18.1",
"@codemirror/commands": "6.6.2",
"@codemirror/language": "6.10.3",
"@codemirror/autocomplete": "6.18.0",
"@codemirror/commands": "6.6.0",
"@codemirror/language": "6.10.2",
"@codemirror/legacy-modes": "6.4.1",
"@codemirror/search": "6.5.6",
"@codemirror/state": "6.4.1",
"@codemirror/view": "6.34.1",
"@codemirror/view": "6.32.0",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.12.5",
"@formatjs/intl-displaynames": "6.6.8",
@@ -80,17 +80,16 @@
"@material/mwc-top-app-bar": "0.27.0",
"@material/mwc-top-app-bar-fixed": "0.27.0",
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
"@material/web": "2.2.0",
"@material/web": "2.1.0",
"@mdi/js": "7.4.47",
"@mdi/svg": "7.4.47",
"@polymer/paper-item": "3.0.1",
"@polymer/paper-listbox": "3.0.1",
"@polymer/paper-tabs": "3.1.0",
"@polymer/polymer": "3.5.1",
"@replit/codemirror-indentation-markers": "6.5.3",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.4.10",
"@vaadin/vaadin-themable-mixin": "24.4.10",
"@vaadin/combo-box": "24.4.6",
"@vaadin/vaadin-themable-mixin": "24.4.6",
"@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@@ -103,11 +102,10 @@
"comlink": "4.4.1",
"core-js": "3.38.1",
"cropperjs": "1.6.2",
"date-fns": "4.1.0",
"date-fns": "3.6.0",
"date-fns-tz": "3.1.3",
"deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1",
"dialog-polyfill": "0.5.6",
"element-internals-polyfill": "1.3.11",
"fuse.js": "7.0.0",
"google-timezones-json": "1.2.0",
@@ -117,10 +115,10 @@
"intl-messageformat": "10.5.14",
"js-yaml": "4.1.0",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
"leaflet-draw": "1.0.4",
"lit": "2.8.0",
"luxon": "3.5.0",
"marked": "14.1.2",
"marked": "14.0.0",
"memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "0.3.2",
@@ -129,13 +127,13 @@
"qrcode": "1.5.4",
"roboto-fontface": "0.10.0",
"rrule": "2.8.1",
"sortablejs": "patch:sortablejs@npm%3A1.15.3#~/.yarn/patches/sortablejs-npm-1.15.3-3235a8f83b.patch",
"sortablejs": "1.15.2",
"stacktrace-js": "2.0.2",
"superstruct": "2.0.2",
"tinykeys": "3.0.0",
"tsparticles-engine": "2.12.0",
"tsparticles-preset-links": "2.12.0",
"ua-parser-js": "1.0.39",
"ua-parser-js": "1.0.38",
"unfetch": "5.0.0",
"vis-data": "7.1.9",
"vis-network": "9.1.9",
@@ -157,22 +155,22 @@
"@babel/plugin-transform-runtime": "7.25.4",
"@babel/preset-env": "7.25.4",
"@babel/preset-typescript": "7.24.7",
"@bundle-stats/plugin-webpack-filter": "4.15.1",
"@bundle-stats/plugin-webpack-filter": "4.14.2",
"@koa/cors": "5.0.0",
"@lokalise/node-api": "12.7.0",
"@octokit/auth-oauth-device": "7.1.1",
"@octokit/plugin-retry": "7.1.2",
"@octokit/plugin-retry": "7.1.1",
"@octokit/rest": "21.0.2",
"@open-wc/dev-server-hmr": "0.1.4",
"@rollup/plugin-babel": "6.0.4",
"@rollup/plugin-commonjs": "26.0.1",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-node-resolve": "15.2.4",
"@rollup/plugin-node-resolve": "15.2.3",
"@rollup/plugin-replace": "5.0.7",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.17",
"@types/chromecast-caf-sender": "1.0.10",
"@types/color-name": "2.0.0",
"@types/color-name": "1.1.4",
"@types/glob": "8.1.0",
"@types/html-minifier-terser": "7.0.2",
"@types/js-yaml": "4.0.9",
@@ -191,20 +189,20 @@
"@typescript-eslint/parser": "7.18.0",
"@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1",
"babel-loader": "9.2.1",
"babel-loader": "9.1.3",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
"chai": "5.1.1",
"del": "7.1.0",
"eslint": "8.57.1",
"eslint": "8.57.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "18.0.0",
"eslint-config-prettier": "9.1.0",
"eslint-import-resolver-webpack": "0.13.9",
"eslint-plugin-import": "2.30.0",
"eslint-plugin-lit": "1.15.0",
"eslint-import-resolver-webpack": "0.13.8",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-lit": "1.14.0",
"eslint-plugin-lit-a11y": "4.1.4",
"eslint-plugin-unused-imports": "4.1.4",
"eslint-plugin-unused-imports": "4.1.3",
"eslint-plugin-wc": "2.1.1",
"fancy-log": "2.0.0",
"fs-extra": "11.2.0",
@@ -215,10 +213,10 @@
"gulp-rename": "2.0.0",
"gulp-zopfli-green": "6.0.2",
"html-minifier-terser": "7.2.0",
"husky": "9.1.6",
"husky": "9.1.5",
"instant-mocha": "1.5.2",
"jszip": "3.10.1",
"lint-staged": "15.2.10",
"lint-staged": "15.2.9",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0",
@@ -229,21 +227,21 @@
"open": "10.1.0",
"pinst": "3.0.0",
"prettier": "3.3.3",
"rollup": "2.79.2",
"rollup": "2.79.1",
"rollup-plugin-string": "3.0.0",
"rollup-plugin-terser": "7.0.2",
"rollup-plugin-visualizer": "5.12.0",
"serve-handler": "6.1.5",
"sinon": "19.0.2",
"sinon": "18.0.0",
"systemjs": "6.15.1",
"tar": "7.4.3",
"terser-webpack-plugin": "5.3.10",
"transform-async-modules-webpack-plugin": "1.1.1",
"ts-lit-plugin": "2.0.2",
"typescript": "5.6.2",
"webpack": "5.95.0",
"typescript": "5.5.4",
"webpack": "5.94.0",
"webpack-cli": "5.1.4",
"webpack-dev-server": "5.1.0",
"webpack-dev-server": "5.0.4",
"webpack-manifest-plugin": "5.0.0",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "6.0.1",
@@ -256,7 +254,9 @@
"lit": "2.8.0",
"clean-css": "5.3.3",
"@lit/reactive-element": "1.6.3",
"@fullcalendar/daygrid": "6.1.15"
"@fullcalendar/daygrid": "6.1.15",
"sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch",
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
},
"packageManager": "yarn@4.5.0"
"packageManager": "yarn@4.4.0"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

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

View File

@@ -1,36 +1,36 @@
import { theme2hex } from "./convert-color";
export const COLORS = [
"#4269d0",
"#f4bd4a",
"#ff725c",
"#6cc5b0",
"#a463f2",
"#ff8ab7",
"#9c6b4e",
"#97bbf5",
"#01ab63",
"#9498a0",
"#094bad",
"#c99000",
"#d84f3e",
"#49a28f",
"#048732",
"#d96895",
"#8043ce",
"#7599d1",
"#7a4c31",
"#74787f",
"#6989f4",
"#ffd444",
"#ff957c",
"#8fe9d3",
"#62cc71",
"#ffadda",
"#c884ff",
"#badeff",
"#bf8b6d",
"#b6bac2",
"#44739e",
"#984ea3",
"#00d2d5",
"#ff7f00",
"#af8d00",
"#7f80cd",
"#b3e900",
"#c42e60",
"#a65628",
"#f781bf",
"#8dd3c7",
"#bebada",
"#fb8072",
"#80b1d3",
"#fdb462",
"#fccde5",
"#bc80bd",
"#ffed6f",
"#c4eaff",
"#cf8c00",
"#1b9e77",
"#d95f02",
"#e7298a",
"#e6ab02",
"#a6761d",
"#0097ff",
"#00d067",
"#f43600",
"#4ba93b",
"#5779bb",
"#927acc",
"#97ee3f",
"#bf3947",

View File

@@ -234,12 +234,7 @@ export const SENSOR_ENTITIES = [
"weather",
];
export const ASSIST_ENTITIES = [
"assist_satellite",
"conversation",
"stt",
"tts",
];
export const ASSIST_ENTITIES = ["conversation", "stt", "tts"];
/** Domains that render an input element instead of a text value when displayed in a row.
* Those rows should then not show a cursor pointer when hovered (which would normally

View File

@@ -71,7 +71,8 @@ export const computeStateDisplayFromEntityAttributes = (
if (
attributes.device_class === "duration" &&
attributes.unit_of_measurement &&
UNIT_TO_MILLISECOND_CONVERT[attributes.unit_of_measurement]
UNIT_TO_MILLISECOND_CONVERT[attributes.unit_of_measurement] &&
entity?.display_precision === undefined
) {
try {
return formatDuration(state, attributes.unit_of_measurement);

View File

@@ -1,6 +0,0 @@
import type { ChartEvent } from "chart.js";
export const clickIsTouch = (event: ChartEvent): boolean =>
!(event.native instanceof MouseEvent) ||
(event.native instanceof PointerEvent &&
event.native.pointerType !== "mouse");

View File

@@ -16,7 +16,6 @@ import {
HaChartBase,
MIN_TIME_BETWEEN_UPDATES,
} from "./ha-chart-base";
import { clickIsTouch } from "./click_is_touch";
const safeParseFloat = (value) => {
const parsed = parseFloat(value);
@@ -221,7 +220,12 @@ export class StateHistoryChartLine extends LitElement {
// @ts-expect-error
locale: numberFormatToLocale(this.hass.locale),
onClick: (e: any) => {
if (!this.clickForMoreInfo || clickIsTouch(e)) {
if (
!this.clickForMoreInfo ||
!(e.native instanceof MouseEvent) ||
(e.native instanceof PointerEvent &&
e.native.pointerType !== "mouse")
) {
return;
}

View File

@@ -16,7 +16,6 @@ import {
} from "./ha-chart-base";
import type { TimeLineData } from "./timeline-chart/const";
import { computeTimelineColor } from "./timeline-chart/timeline-color";
import { clickIsTouch } from "./click_is_touch";
@customElement("state-history-chart-timeline")
export class StateHistoryChartTimeline extends LitElement {
@@ -225,7 +224,11 @@ export class StateHistoryChartTimeline extends LitElement {
// @ts-expect-error
locale: numberFormatToLocale(this.hass.locale),
onClick: (e: any) => {
if (!this.clickForMoreInfo || clickIsTouch(e)) {
if (
!this.clickForMoreInfo ||
!(e.native instanceof MouseEvent) ||
(e.native instanceof PointerEvent && e.native.pointerType !== "mouse")
) {
return;
}

View File

@@ -39,7 +39,6 @@ import type {
ChartDatasetExtra,
HaChartBase,
} from "./ha-chart-base";
import { clickIsTouch } from "./click_is_touch";
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
mean: "mean",
@@ -279,7 +278,11 @@ export class StatisticsChart extends LitElement {
// @ts-expect-error
locale: numberFormatToLocale(this.hass.locale),
onClick: (e: any) => {
if (!this.clickForMoreInfo || clickIsTouch(e)) {
if (
!this.clickForMoreInfo ||
!(e.native instanceof MouseEvent) ||
(e.native instanceof PointerEvent && e.native.pointerType !== "mouse")
) {
return;
}

View File

@@ -25,6 +25,7 @@ import { fireEvent } from "../../common/dom/fire_event";
import { stringCompare } from "../../common/string/compare";
import { debounce } from "../../common/util/debounce";
import { groupBy } from "../../common/util/group-by";
import { nextRender } from "../../common/util/render-status";
import { haStyleScrollbar } from "../../resources/styles";
import { loadVirtualizer } from "../../resources/virtualizer";
import { HomeAssistant } from "../../types";
@@ -34,7 +35,6 @@ import "../ha-svg-icon";
import "../search-input";
import { filterData, sortData } from "./sort-filter";
import { LocalizeFunc } from "../../common/translations/localize";
import { nextRender } from "../../common/util/render-status";
export interface RowClickedEvent {
id: string;
@@ -169,6 +169,8 @@ export class HaDataTable extends LitElement {
@query("slot[name='header']") private _header!: HTMLSlotElement;
@state() private _items: DataTableRowData[] = [];
@state() private _collapsedGroups: string[] = [];
private _checkableRowsCount?: number;
@@ -177,9 +179,7 @@ export class HaDataTable extends LitElement {
private _sortColumns: SortableColumnContainer = {};
private _curRequest = 0;
private _lastUpdate = 0;
private curRequest = 0;
// @ts-ignore
@restoreScroll(".scroller") private _savedScrollPos?: number;
@@ -206,9 +206,9 @@ export class HaDataTable extends LitElement {
public connectedCallback() {
super.connectedCallback();
if (this._filteredData.length) {
if (this._items.length) {
// Force update of location of rows
this._filteredData = [...this._filteredData];
this._items = [...this._items];
}
}
@@ -291,13 +291,16 @@ export class HaDataTable extends LitElement {
properties.has("columns") ||
properties.has("_filter") ||
properties.has("sortColumn") ||
properties.has("sortDirection")
properties.has("sortDirection") ||
properties.has("groupColumn") ||
properties.has("groupOrder") ||
properties.has("_collapsedGroups")
) {
this._sortFilterData();
}
if (properties.has("selectable") || properties.has("hiddenColumns")) {
this._filteredData = [...this._filteredData];
this._items = [...this._items];
}
}
@@ -464,15 +467,7 @@ export class HaDataTable extends LitElement {
scroller
class="mdc-data-table__content scroller ha-scrollbar"
@scroll=${this._saveScrollPos}
.items=${this._groupData(
this._filteredData,
localize,
this.appendRow,
this.hasFab,
this.groupColumn,
this.groupOrder,
this._collapsedGroups
)}
.items=${this._items}
.keyFunction=${this._keyFunction}
.renderItem=${renderRow}
></lit-virtualizer>
@@ -607,13 +602,8 @@ export class HaDataTable extends LitElement {
private async _sortFilterData() {
const startTime = new Date().getTime();
const timeBetweenUpdate = startTime - this._lastUpdate;
const timeBetweenRequest = startTime - this._curRequest;
this._curRequest = startTime;
const forceUpdate =
!this._lastUpdate ||
(timeBetweenUpdate > 500 && timeBetweenRequest < 500);
this.curRequest++;
const curRequest = this.curRequest;
let filteredData = this.data;
if (this._filter) {
@@ -624,10 +614,6 @@ export class HaDataTable extends LitElement {
);
}
if (!forceUpdate && this._curRequest !== startTime) {
return;
}
const prom = this.sortColumn
? sortData(
filteredData,
@@ -648,103 +634,91 @@ export class HaDataTable extends LitElement {
setTimeout(resolve, 100 - elapsed);
});
}
if (!forceUpdate && this._curRequest !== startTime) {
if (this.curRequest !== curRequest) {
return;
}
this._lastUpdate = startTime;
const localize = this.localizeFunc || this.hass.localize;
if (this.appendRow || this.hasFab || this.groupColumn) {
let items = [...data];
if (this.groupColumn) {
const grouped = groupBy(items, (item) => item[this.groupColumn!]);
if (grouped.undefined) {
// make sure ungrouped items are at the bottom
grouped[UNDEFINED_GROUP_KEY] = grouped.undefined;
delete grouped.undefined;
}
const sorted: {
[key: string]: DataTableRowData[];
} = Object.keys(grouped)
.sort((a, b) => {
const orderA = this.groupOrder?.indexOf(a) ?? -1;
const orderB = this.groupOrder?.indexOf(b) ?? -1;
if (orderA !== orderB) {
if (orderA === -1) {
return 1;
}
if (orderB === -1) {
return -1;
}
return orderA - orderB;
}
return stringCompare(
["", "-", "—"].includes(a) ? "zzz" : a,
["", "-", "—"].includes(b) ? "zzz" : b,
this.hass.locale.language
);
})
.reduce((obj, key) => {
obj[key] = grouped[key];
return obj;
}, {});
const groupedItems: DataTableRowData[] = [];
Object.entries(sorted).forEach(([groupName, rows]) => {
groupedItems.push({
append: true,
content: html`<div
class="mdc-data-table__cell group-header"
role="cell"
.group=${groupName}
@click=${this._collapseGroup}
>
<ha-icon-button
.path=${mdiChevronUp}
class=${this._collapsedGroups.includes(groupName)
? "collapsed"
: ""}
>
</ha-icon-button>
${groupName === UNDEFINED_GROUP_KEY
? localize("ui.components.data-table.ungrouped")
: groupName || ""}
</div>`,
});
if (!this._collapsedGroups.includes(groupName)) {
groupedItems.push(...rows);
}
});
items = groupedItems;
}
if (this.appendRow) {
items.push({ append: true, content: this.appendRow });
}
if (this.hasFab) {
items.push({ empty: true });
}
this._items = items;
} else {
this._items = data;
}
this._filteredData = data;
}
private _groupData = memoizeOne(
(
data: DataTableRowData[],
localize: LocalizeFunc,
appendRow,
hasFab: boolean,
groupColumn: string | undefined,
groupOrder: string[] | undefined,
collapsedGroups: string[]
) => {
if (appendRow || hasFab || groupColumn) {
let items = [...data];
if (groupColumn) {
const grouped = groupBy(items, (item) => item[groupColumn]);
if (grouped.undefined) {
// make sure ungrouped items are at the bottom
grouped[UNDEFINED_GROUP_KEY] = grouped.undefined;
delete grouped.undefined;
}
const sorted: {
[key: string]: DataTableRowData[];
} = Object.keys(grouped)
.sort((a, b) => {
const orderA = groupOrder?.indexOf(a) ?? -1;
const orderB = groupOrder?.indexOf(b) ?? -1;
if (orderA !== orderB) {
if (orderA === -1) {
return 1;
}
if (orderB === -1) {
return -1;
}
return orderA - orderB;
}
return stringCompare(
["", "-", "—"].includes(a) ? "zzz" : a,
["", "-", "—"].includes(b) ? "zzz" : b,
this.hass.locale.language
);
})
.reduce((obj, key) => {
obj[key] = grouped[key];
return obj;
}, {});
const groupedItems: DataTableRowData[] = [];
Object.entries(sorted).forEach(([groupName, rows]) => {
groupedItems.push({
append: true,
content: html`<div
class="mdc-data-table__cell group-header"
role="cell"
.group=${groupName}
@click=${this._collapseGroup}
>
<ha-icon-button
.path=${mdiChevronUp}
class=${collapsedGroups.includes(groupName)
? "collapsed"
: ""}
>
</ha-icon-button>
${groupName === UNDEFINED_GROUP_KEY
? localize("ui.components.data-table.ungrouped")
: groupName || ""}
</div>`,
});
if (!collapsedGroups.includes(groupName)) {
groupedItems.push(...rows);
}
});
items = groupedItems;
}
if (appendRow) {
items.push({ append: true, content: appendRow });
}
if (hasFab) {
items.push({ empty: true });
}
return items;
}
return data;
}
);
private _memFilterData = memoizeOne(
(
data: DataTableRowData[],
@@ -828,8 +802,8 @@ export class HaDataTable extends LitElement {
private _checkedRowsChanged() {
// force scroller to update, change it's items
if (this._filteredData.length) {
this._filteredData = [...this._filteredData];
if (this._items.length) {
this._items = [...this._items];
}
fireEvent(this, "selection-changed", {
value: this._checkedRows,
@@ -1011,7 +985,6 @@ export class HaDataTable extends LitElement {
/* @noflip */
padding-inline-end: initial;
width: 60px;
min-width: 60px;
}
.mdc-data-table__table {

View File

@@ -26,7 +26,7 @@ class HaDeviceTriggerPicker extends HaDeviceAutomationPicker<DeviceTrigger> {
fetchDeviceTriggers,
(deviceId?: string) => ({
device_id: deviceId || "",
trigger: "device",
platform: "device",
domain: "",
entity_id: "",
})

View File

@@ -1,9 +1,10 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { isValidEntityId } from "../../common/entity/valid_entity_id";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import type { ValueChangedEvent, HomeAssistant } from "../../types";
import "./ha-entity-picker";
import type { HaEntityPickerEntityFilterFunc } from "./ha-entity-picker";
@@ -97,7 +98,10 @@ class HaEntitiesPickerLight extends LitElement {
.excludeEntities=${this.excludeEntities}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
.entityFilter=${this.entityFilter}
.entityFilter=${this._getEntityFilter(
this.value,
this.entityFilter
)}
.value=${entityId}
.label=${this.pickedEntityLabel}
.disabled=${this.disabled}
@@ -114,13 +118,10 @@ class HaEntitiesPickerLight extends LitElement {
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.includeEntities=${this.includeEntities}
.excludeEntities=${this._excludeEntities(
this.value,
this.excludeEntities
)}
.excludeEntities=${this.excludeEntities}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
.entityFilter=${this.entityFilter}
.entityFilter=${this._getEntityFilter(this.value, this.entityFilter)}
.label=${this.pickEntityLabel}
.helper=${this.helper}
.disabled=${this.disabled}
@@ -132,16 +133,14 @@ class HaEntitiesPickerLight extends LitElement {
`;
}
private _excludeEntities = memoizeOne(
private _getEntityFilter = memoizeOne(
(
value: string[] | undefined,
excludeEntities: string[] | undefined
): string[] | undefined => {
if (value === undefined) {
return excludeEntities;
}
return [...(excludeEntities || []), ...value];
}
entityFilter: HaEntityPickerEntityFilterFunc | undefined
): HaEntityPickerEntityFilterFunc =>
(stateObj: HassEntity) =>
(!value || !value.includes(stateObj.entity_id)) &&
(!entityFilter || entityFilter(stateObj))
);
private get _currentEntities() {

View File

@@ -87,7 +87,7 @@ export class HaEntityPicker extends LitElement {
public includeUnitOfMeasurement?: string[];
/**
* List of allowed entities to show.
* List of allowed entities to show. Will ignore all other filters.
* @type {Array}
* @attr include-entities
*/
@@ -220,13 +220,30 @@ export class HaEntityPicker extends LitElement {
if (includeEntities) {
entityIds = entityIds.filter((entityId) =>
includeEntities.includes(entityId)
this.includeEntities!.includes(entityId)
);
return entityIds
.map((key) => {
const friendly_name = computeStateName(hass!.states[key]) || key;
return {
...hass!.states[key],
friendly_name,
strings: [key, friendly_name],
};
})
.sort((entityA, entityB) =>
caseInsensitiveStringCompare(
entityA.friendly_name,
entityB.friendly_name,
this.hass.locale.language
)
);
}
if (excludeEntities) {
entityIds = entityIds.filter(
(entityId) => !excludeEntities.includes(entityId)
(entityId) => !excludeEntities!.includes(entityId)
);
}

View File

@@ -173,7 +173,6 @@ class HaEntityStatePicker extends LitElement {
no-style
@item-moved=${this._moveItem}
.disabled=${this.disabled}
filter="button.trailing.action"
>
<ha-chip-set>
${repeat(

View File

@@ -1,6 +1,6 @@
import { mdiTextureBox } from "@mdi/js";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { HassEntity } from "home-assistant-js-websocket";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { LitElement, PropertyValues, TemplateResult, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
@@ -20,7 +20,12 @@ import {
getDeviceEntityDisplayLookup,
} from "../data/device_registry";
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
import { FloorRegistryEntry, getFloorAreaLookup } from "../data/floor_registry";
import {
FloorRegistryEntry,
getFloorAreaLookup,
subscribeFloorRegistry,
} from "../data/floor_registry";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { HomeAssistant, ValueChangedEvent } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-combo-box";
@@ -45,7 +50,7 @@ interface FloorAreaEntry {
}
@customElement("ha-area-floor-picker")
export class HaAreaFloorPicker extends LitElement {
export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@@ -106,12 +111,22 @@ export class HaAreaFloorPicker extends LitElement {
@property({ type: Boolean }) public required = false;
@state() private _floors?: FloorRegistryEntry[];
@state() private _opened?: boolean;
@query("ha-combo-box", true) public comboBox!: HaComboBox;
private _init = false;
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
return [
subscribeFloorRegistry(this.hass.connection, (floors) => {
this._floors = floors;
}),
];
}
public async open() {
await this.updateComplete;
await this.comboBox?.open();
@@ -416,12 +431,12 @@ export class HaAreaFloorPicker extends LitElement {
protected updated(changedProps: PropertyValues) {
if (
(!this._init && this.hass) ||
(!this._init && this.hass && this._floors) ||
(this._init && changedProps.has("_opened") && this._opened)
) {
this._init = true;
const areas = this._getAreas(
Object.values(this.hass.floors),
this._floors!,
Object.values(this.hass.areas),
Object.values(this.hass.devices),
Object.values(this.hass.entities),

View File

@@ -1,155 +0,0 @@
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import "./ha-ripple";
type BadgeType = "badge" | "button";
@customElement("ha-badge")
export class HaBadge extends LitElement {
@property() public type: BadgeType = "badge";
@property() public label?: string;
@property({ type: Boolean, attribute: "icon-only" }) iconOnly = false;
protected render() {
const label = this.label;
return html`
<div
class="badge ${classMap({
"icon-only": this.iconOnly,
})}"
role=${ifDefined(this.type === "button" ? "button" : undefined)}
tabindex=${ifDefined(this.type === "button" ? "0" : undefined)}
>
<ha-ripple .disabled=${this.type !== "button"}></ha-ripple>
<slot name="icon"></slot>
${this.iconOnly
? nothing
: html`<span class="info">
${label ? html`<span class="label">${label}</span>` : nothing}
<span class="content"><slot></slot></span>
</span>`}
</div>
`;
}
static get styles(): CSSResultGroup {
return css`
:host {
--badge-color: var(--secondary-text-color);
-webkit-tap-highlight-color: transparent;
}
.badge {
position: relative;
--ha-ripple-color: var(--badge-color);
--ha-ripple-hover-opacity: 0.04;
--ha-ripple-pressed-opacity: 0.12;
transition:
box-shadow 180ms ease-in-out,
border-color 180ms ease-in-out;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 8px;
height: var(--ha-badge-size, 36px);
min-width: var(--ha-badge-size, 36px);
padding: 0px 12px;
box-sizing: border-box;
width: auto;
border-radius: var(
--ha-badge-border-radius,
calc(var(--ha-badge-size, 36px) / 2)
);
background: var(
--ha-card-background,
var(--card-background-color, white)
);
-webkit-backdrop-filter: var(--ha-card-backdrop-filter, none);
backdrop-filter: var(--ha-card-backdrop-filter, none);
border-width: var(--ha-card-border-width, 1px);
box-shadow: var(--ha-card-box-shadow, none);
border-style: solid;
border-color: var(
--ha-card-border-color,
var(--divider-color, #e0e0e0)
);
}
.badge:focus-visible {
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
--shadow-focus: 0 0 0 1px var(--badge-color);
border-color: var(--badge-color);
box-shadow: var(--shadow-default), var(--shadow-focus);
}
[role="button"] {
cursor: pointer;
}
[role="button"]:focus {
outline: none;
}
.info {
display: flex;
flex-direction: column;
align-items: flex-start;
padding-inline-start: initial;
text-align: center;
font-family: Roboto;
}
.label {
font-size: 10px;
font-style: normal;
font-weight: 500;
line-height: 10px;
letter-spacing: 0.1px;
color: var(--secondary-text-color);
}
.content {
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 16px;
letter-spacing: 0.1px;
color: var(--primary-text-color);
}
::slotted([slot="icon"]) {
--mdc-icon-size: 18px;
color: var(--badge-color);
line-height: 0;
margin-left: -4px;
margin-right: 0;
margin-inline-start: -4px;
margin-inline-end: 0;
}
::slotted(img[slot="icon"]) {
width: 30px;
height: 30px;
border-radius: 50%;
object-fit: cover;
overflow: hidden;
margin-left: -10px;
margin-right: 0;
margin-inline-start: -10px;
margin-inline-end: 0;
}
.badge.icon-only {
padding: 0;
}
.badge.icon-only ::slotted([slot="icon"]) {
margin-left: 0;
margin-right: 0;
margin-inline-start: 0;
margin-inline-end: 0;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-badge": HaBadge;
}
}

View File

@@ -6,8 +6,8 @@ import type { HaIconButton } from "./ha-icon-button";
import "./ha-menu";
import type { HaMenu } from "./ha-menu";
@customElement("ha-md-button-menu")
export class HaMdButtonMenu extends LitElement {
@customElement("ha-button-menu-new")
export class HaButtonMenuNew extends LitElement {
protected readonly [FOCUS_TARGET];
@property({ type: Boolean }) public disabled = false;
@@ -84,6 +84,6 @@ export class HaMdButtonMenu extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"ha-md-button-menu": HaMdButtonMenu;
"ha-button-menu-new": HaButtonMenuNew;
}
}

View File

@@ -124,12 +124,9 @@ export class HaCodeEditor extends ReactiveElement {
const transactions: TransactionSpec[] = [];
if (changedProps.has("mode")) {
transactions.push({
effects: [
this._loadedCodeMirror!.langCompartment!.reconfigure(this._mode),
this._loadedCodeMirror!.foldingCompartment.reconfigure(
this._getFoldingExtensions()
),
],
effects: this._loadedCodeMirror!.langCompartment!.reconfigure(
this._mode
),
});
}
if (changedProps.has("readOnly")) {
@@ -180,14 +177,6 @@ export class HaCodeEditor extends ReactiveElement {
this._loadedCodeMirror.crosshairCursor(),
this._loadedCodeMirror.highlightSelectionMatches(),
this._loadedCodeMirror.highlightActiveLine(),
this._loadedCodeMirror.indentationMarkers({
thickness: 0,
activeThickness: 1,
colors: {
activeLight: "var(--secondary-text-color)",
activeDark: "var(--secondary-text-color)",
},
}),
this._loadedCodeMirror.keymap.of([
...this._loadedCodeMirror.defaultKeymap,
...this._loadedCodeMirror.searchKeymap,
@@ -205,9 +194,6 @@ export class HaCodeEditor extends ReactiveElement {
this.linewrap ? this._loadedCodeMirror.EditorView.lineWrapping : []
),
this._loadedCodeMirror.EditorView.updateListener.of(this._onUpdate),
this._loadedCodeMirror.foldingCompartment.of(
this._getFoldingExtensions()
),
];
if (!this.readOnly) {
@@ -325,17 +311,6 @@ export class HaCodeEditor extends ReactiveElement {
fireEvent(this, "value-changed", { value: this._value });
};
private _getFoldingExtensions = (): Extension => {
if (this.mode === "yaml") {
return [
this._loadedCodeMirror!.foldGutter(),
this._loadedCodeMirror!.foldingOnIndent,
];
}
return [];
};
static get styles(): CSSResultGroup {
return css`
:host(.error-state) .cm-gutters {

View File

@@ -1,16 +1,14 @@
import { mdiInvertColorsOff, mdiPalette } from "@mdi/js";
import "@material/mwc-list/mwc-list-item";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { LocalizeKeys } from "../common/translations/localize";
import { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-md-divider";
import "./ha-select";
import type { HaSelect } from "./ha-select";
import "./ha-list-item";
import { HomeAssistant } from "../types";
import { LocalizeKeys } from "../common/translations/localize";
@customElement("ha-color-picker")
export class HaColorPicker extends LitElement {
@@ -22,97 +20,43 @@ export class HaColorPicker extends LitElement {
@property() public value?: string;
@property({ type: String, attribute: "default_color" })
public defaultColor?: string;
@property({ type: Boolean, attribute: "include_state" })
public includeState = false;
@property({ type: Boolean, attribute: "include_none" })
public includeNone = false;
@property({ type: Boolean }) public defaultColor = false;
@property({ type: Boolean }) public disabled = false;
@query("ha-select") private _select!: HaSelect;
connectedCallback(): void {
super.connectedCallback();
// Refresh layout options when the field is connected to the DOM to ensure current value displayed
this._select.layoutOptions();
}
private _valueSelected(ev) {
ev.stopPropagation();
if (!this.isConnected) return;
_valueSelected(ev) {
const value = ev.target.value;
this.value = value === this.defaultColor ? undefined : value;
fireEvent(this, "value-changed", {
value: this.value,
});
if (value) {
fireEvent(this, "value-changed", {
value: value !== "default" ? value : undefined,
});
}
}
render() {
const value = this.value || this.defaultColor || "";
const isCustom = !(
THEME_COLORS.has(value) ||
value === "none" ||
value === "state"
);
return html`
<ha-select
.icon=${Boolean(value)}
.icon=${Boolean(this.value)}
.label=${this.label}
.value=${value}
.value=${this.value || "default"}
.helper=${this.helper}
.disabled=${this.disabled}
@closed=${stopPropagation}
@selected=${this._valueSelected}
fixedMenuPosition
naturalMenuWidth
.clearable=${!this.defaultColor}
>
${value
${this.value
? html`
<span slot="icon">
${value === "none"
? html`
<ha-svg-icon path=${mdiInvertColorsOff}></ha-svg-icon>
`
: value === "state"
? html`<ha-svg-icon path=${mdiPalette}></ha-svg-icon>`
: this.renderColorCircle(value || "grey")}
${this.renderColorCircle(this.value || "grey")}
</span>
`
: nothing}
${this.includeNone
? html`
<ha-list-item value="none" graphic="icon">
${this.hass.localize("ui.components.color-picker.none")}
${this.defaultColor === "none"
? ` (${this.hass.localize("ui.components.color-picker.default")})`
: nothing}
<ha-svg-icon
slot="graphic"
path=${mdiInvertColorsOff}
></ha-svg-icon>
</ha-list-item>
`
: nothing}
${this.includeState
? html`
<ha-list-item value="state" graphic="icon">
${this.hass.localize("ui.components.color-picker.state")}
${this.defaultColor === "state"
? ` (${this.hass.localize("ui.components.color-picker.default")})`
: nothing}
<ha-svg-icon slot="graphic" path=${mdiPalette}></ha-svg-icon>
</ha-list-item>
`
: nothing}
${this.includeState || this.includeNone
? html`<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
${this.defaultColor
? html` <ha-list-item value="default">
${this.hass.localize(`ui.components.color-picker.default_color`)}
</ha-list-item>`
: nothing}
${Array.from(THEME_COLORS).map(
(color) => html`
@@ -120,21 +64,10 @@ export class HaColorPicker extends LitElement {
${this.hass.localize(
`ui.components.color-picker.colors.${color}` as LocalizeKeys
) || color}
${this.defaultColor === color
? ` (${this.hass.localize("ui.components.color-picker.default")})`
: nothing}
<span slot="graphic">${this.renderColorCircle(color)}</span>
</ha-list-item>
`
)}
${isCustom
? html`
<ha-list-item .value=${value} graphic="icon">
${value}
<span slot="graphic">${this.renderColorCircle(value)}</span>
</ha-list-item>
`
: nothing}
</ha-select>
`;
}
@@ -154,11 +87,10 @@ export class HaColorPicker extends LitElement {
return css`
.circle-color {
display: block;
background-color: var(--circle-color, var(--divider-color));
background-color: var(--circle-color);
border-radius: 10px;
width: 20px;
height: 20px;
box-sizing: border-box;
}
ha-select {
width: 100%;

View File

@@ -45,35 +45,15 @@ export class HaConversationAgentPicker extends LitElement {
if (!this._agents) {
return nothing;
}
let value = this.value;
if (!value && this.required) {
// Select Home Assistant conversation agent if it supports the language
for (const agent of this._agents) {
if (
agent.id === "conversation.home_assistant" &&
agent.supported_languages.includes(this.language!)
) {
value = agent.id;
break;
}
}
if (!value) {
// Select the first agent that supports the language
for (const agent of this._agents) {
if (
agent.supported_languages === "*" &&
agent.supported_languages.includes(this.language!)
) {
value = agent.id;
break;
}
}
}
}
if (!value) {
value = NONE;
}
const value =
this.value ??
(this.required &&
(!this.language ||
this._agents
.find((agent) => agent.id === "homeassistant")
?.supported_languages.includes(this.language))
? "homeassistant"
: NONE);
return html`
<ha-select
.label=${this.label ||

View File

@@ -10,13 +10,8 @@ export class HaDialogHeader extends LitElement {
<section class="header-navigation-icon">
<slot name="navigationIcon"></slot>
</section>
<section class="header-content">
<div class="header-title">
<slot name="title"></slot>
</div>
<div class="header-subtitle">
<slot name="subtitle"></slot>
</div>
<section class="header-title">
<slot name="title"></slot>
</section>
<section class="header-action-items">
<slot name="actionItems"></slot>
@@ -44,24 +39,17 @@ export class HaDialogHeader extends LitElement {
padding: 4px;
box-sizing: border-box;
}
.header-content {
.header-title {
flex: 1;
font-size: 22px;
line-height: 28px;
font-weight: 400;
padding: 10px 4px;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.header-title {
font-size: 22px;
line-height: 28px;
font-weight: 400;
}
.header-subtitle {
font-size: 14px;
line-height: 20px;
color: var(--secondary-text-color);
}
@media all and (min-width: 450px) and (min-height: 500px) {
.header-bar {
padding: 12px;

View File

@@ -68,8 +68,8 @@ export class HaExpansionPanel extends LitElement {
></ha-svg-icon>
`
: ""}
<slot name="icons"></slot>
</div>
<slot name="icons"></slot>
</div>
<div
class="container ${classMap({ expanded: this.expanded })}"

View File

@@ -1,5 +1,6 @@
import "@material/mwc-menu/mwc-menu-surface";
import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
CSSResultGroup,
LitElement,
@@ -14,8 +15,13 @@ import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { computeRTL } from "../common/util/compute_rtl";
import { getFloorAreaLookup } from "../data/floor_registry";
import {
FloorRegistryEntry,
getFloorAreaLookup,
subscribeFloorRegistry,
} from "../data/floor_registry";
import { RelatedResult, findRelated } from "../data/search";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-check-list-item";
@@ -25,7 +31,7 @@ import "./ha-svg-icon";
import "./ha-tree-indicator";
@customElement("ha-filter-floor-areas")
export class HaFilterFloorAreas extends LitElement {
export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public value?: {
@@ -41,6 +47,8 @@ export class HaFilterFloorAreas extends LitElement {
@state() private _shouldRender = false;
@state() private _floors?: FloorRegistryEntry[];
public willUpdate(properties: PropertyValues) {
super.willUpdate(properties);
@@ -52,7 +60,7 @@ export class HaFilterFloorAreas extends LitElement {
}
protected render() {
const areas = this._areas(this.hass.areas, this.hass.floors);
const areas = this._areas(this.hass.areas, this._floors);
return html`
<ha-expansion-panel
@@ -181,6 +189,14 @@ export class HaFilterFloorAreas extends LitElement {
this._findRelated();
}
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
return [
subscribeFloorRegistry(this.hass.connection, (floors) => {
this._floors = floors;
}),
];
}
protected updated(changed) {
if (changed.has("expanded") && this.expanded) {
setTimeout(() => {
@@ -204,9 +220,9 @@ export class HaFilterFloorAreas extends LitElement {
}
private _areas = memoizeOne(
(areaReg: HomeAssistant["areas"], floorReg: HomeAssistant["floors"]) => {
(areaReg: HomeAssistant["areas"], floors?: FloorRegistryEntry[]) => {
const areas = Object.values(areaReg);
const floors = Object.values(floorReg);
const floorAreaLookup = getFloorAreaLookup(areas);
const unassisgnedAreas = areas.filter(

View File

@@ -1,5 +1,5 @@
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { HassEntity } from "home-assistant-js-websocket";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { LitElement, PropertyValues, TemplateResult, html } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@@ -24,8 +24,10 @@ import {
FloorRegistryEntry,
createFloorRegistryEntry,
getFloorAreaLookup,
subscribeFloorRegistry,
} from "../data/floor_registry";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { showFloorRegistryDetailDialog } from "../panels/config/areas/show-dialog-floor-registry-detail";
import { HomeAssistant, ValueChangedEvent } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
@@ -51,7 +53,7 @@ const rowRenderer: ComboBoxLitRenderer<FloorRegistryEntry> = (item) =>
</ha-list-item>`;
@customElement("ha-floor-picker")
export class HaFloorPicker extends LitElement {
export class HaFloorPicker extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@@ -109,6 +111,8 @@ export class HaFloorPicker extends LitElement {
@state() private _opened?: boolean;
@state() private _floors?: FloorRegistryEntry[];
@query("ha-combo-box", true) public comboBox!: HaComboBox;
private _suggestion?: string;
@@ -125,6 +129,14 @@ export class HaFloorPicker extends LitElement {
await this.comboBox?.focus();
}
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
return [
subscribeFloorRegistry(this.hass.connection, (floors) => {
this._floors = floors;
}),
];
}
private _getFloors = memoizeOne(
(
floors: FloorRegistryEntry[],
@@ -308,12 +320,12 @@ export class HaFloorPicker extends LitElement {
protected updated(changedProps: PropertyValues) {
if (
(!this._init && this.hass) ||
(!this._init && this.hass && this._floors) ||
(this._init && changedProps.has("_opened") && this._opened)
) {
this._init = true;
const floors = this._getFloors(
Object.values(this.hass.floors),
this._floors!,
Object.values(this.hass.areas),
Object.values(this.hass.devices),
Object.values(this.hass.entities),
@@ -348,7 +360,8 @@ export class HaFloorPicker extends LitElement {
? this.hass.localize("ui.components.floor-picker.floor")
: this.label}
.placeholder=${this.placeholder
? this.hass.floors[this.placeholder]?.name
? this._floors?.find((floor) => floor.floor_id === this.placeholder)
?.name
: undefined}
.renderer=${rowRenderer}
@filter-changed=${this._filterChanged}
@@ -447,7 +460,7 @@ export class HaFloorPicker extends LitElement {
floor_id: floor.floor_id,
});
});
const floors = [...Object.values(this.hass.floors), floor];
const floors = [...this._floors!, floor];
this.comboBox.filteredItems = this._getFloors(
floors,
Object.values(this.hass.areas)!,

View File

@@ -95,10 +95,10 @@ export const computeInitialHaFormData = (
} else if (
"action" in selector ||
"trigger" in selector ||
"condition" in selector
"condition" in selector ||
"media" in selector ||
"target" in selector
) {
data[field.name] = [];
} else if ("media" in selector || "target" in selector) {
data[field.name] = {};
} else {
throw new Error(

View File

@@ -21,49 +21,13 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
@property({ attribute: false }) public computeLabel?: (
schema: HaFormSchema,
data?: HaFormDataContainer,
options?: { path?: string[] }
data?: HaFormDataContainer
) => string;
@property({ attribute: false }) public computeHelper?: (
schema: HaFormSchema,
options?: { path?: string[] }
schema: HaFormSchema
) => string;
@property({ attribute: false }) public localizeValue?: (
key: string
) => string;
private _renderDescription() {
const description = this.computeHelper?.(this.schema);
return description ? html`<p>${description}</p>` : nothing;
}
private _computeLabel = (
schema: HaFormSchema,
data?: HaFormDataContainer,
options?: { path?: string[] }
) => {
if (!this.computeLabel) return this.computeLabel;
return this.computeLabel(schema, data, {
...options,
path: [...(options?.path || []), this.schema.name],
});
};
private _computeHelper = (
schema: HaFormSchema,
options?: { path?: string[] }
) => {
if (!this.computeHelper) return this.computeHelper;
return this.computeHelper(schema, {
...options,
path: [...(options?.path || []), this.schema.name],
});
};
protected render() {
return html`
<ha-expansion-panel outlined .expanded=${Boolean(this.schema.expanded)}>
@@ -79,18 +43,16 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
<ha-svg-icon .path=${this.schema.iconPath}></ha-svg-icon>
`
: nothing}
${this.schema.title || this.computeLabel?.(this.schema)}
${this.schema.title}
</div>
<div class="content">
${this._renderDescription()}
<ha-form
.hass=${this.hass}
.data=${this.data}
.schema=${this.schema.schema}
.disabled=${this.disabled}
.computeLabel=${this._computeLabel}
.computeHelper=${this._computeHelper}
.localizeValue=${this.localizeValue}
.computeLabel=${this.computeLabel}
.computeHelper=${this.computeHelper}
></ha-form>
</div>
</ha-expansion-panel>
@@ -109,9 +71,6 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
.content {
padding: 12px;
}
.content p {
margin: 0 0 24px;
}
ha-expansion-panel {
display: block;
--expansion-panel-content-padding: 0;

View File

@@ -35,10 +35,6 @@ export class HaFormGrid extends LitElement implements HaFormElement {
schema: HaFormSchema
) => string;
@property({ attribute: false }) public localizeValue?: (
key: string
) => string;
public async focus() {
await this.updateComplete;
this.renderRoot.querySelector("ha-form")?.focus();
@@ -69,7 +65,6 @@ export class HaFormGrid extends LitElement implements HaFormElement {
.disabled=${this.disabled}
.computeLabel=${this.computeLabel}
.computeHelper=${this.computeHelper}
.localizeValue=${this.localizeValue}
></ha-form>
`
)}

View File

@@ -31,7 +31,7 @@ const LOAD_ELEMENTS = {
};
const getValue = (obj, item) =>
obj ? (!item.name || item.flatten ? obj : obj[item.name]) : null;
obj ? (!item.name ? obj : obj[item.name]) : null;
const getError = (obj, item) => (obj && item.name ? obj[item.name] : null);
@@ -163,7 +163,6 @@ export class HaForm extends LitElement implements HaFormElement {
localize: this.hass?.localize,
computeLabel: this.computeLabel,
computeHelper: this.computeHelper,
localizeValue: this.localizeValue,
context: this._generateContext(item),
...this.getFormProperties(),
})}
@@ -205,10 +204,9 @@ export class HaForm extends LitElement implements HaFormElement {
if (ev.target === this) return;
const newValue =
!schema.name || ("flatten" in schema && schema.flatten)
? ev.detail.value
: { [schema.name]: ev.detail.value };
const newValue = !schema.name
? ev.detail.value
: { [schema.name]: ev.detail.value };
this.data = {
...this.data,

View File

@@ -31,15 +31,15 @@ export interface HaFormBaseSchema {
export interface HaFormGridSchema extends HaFormBaseSchema {
type: "grid";
flatten?: boolean;
name: string;
column_min_width?: string;
schema: readonly HaFormSchema[];
}
export interface HaFormExpandableSchema extends HaFormBaseSchema {
type: "expandable";
flatten?: boolean;
title?: string;
name: "";
title: string;
icon?: string;
iconPath?: string;
expanded?: boolean;
@@ -100,7 +100,7 @@ export type SchemaUnion<
SchemaArray extends readonly HaFormSchema[],
Schema = SchemaArray[number],
> = Schema extends HaFormGridSchema | HaFormExpandableSchema
? SchemaUnion<Schema["schema"]> | Schema
? SchemaUnion<Schema["schema"]>
: Schema;
export interface HaFormDataContainer {

View File

@@ -18,9 +18,9 @@ export class HaFormfield extends FormfieldBase {
return html` <div class="mdc-form-field ${classMap(classes)}">
<slot></slot>
<label class="mdc-label" @click=${this._labelClick}>
<slot name="label">${this.label}</slot>
</label>
<label class="mdc-label" @click=${this._labelClick}
><slot name="label">${this.label}</slot></label
>
</div>`;
}
@@ -57,13 +57,13 @@ export class HaFormfield extends FormfieldBase {
}
.mdc-form-field {
align-items: var(--ha-formfield-align-items, center);
gap: 4px;
}
.mdc-form-field > label {
direction: var(--direction);
margin-inline-start: 0;
margin-inline-end: auto;
padding: 0;
padding-inline-start: 4px;
padding-inline-end: 0;
}
:host([disabled]) label {
color: var(--disabled-text-color);

View File

@@ -1,24 +1,24 @@
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider";
import "./ha-icon-button";
import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider";
import { mdiRestore } from "@mdi/js";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../common/dom/fire_event";
import { conditionalClamp } from "../common/number/clamp";
import {
CardGridSize,
DEFAULT_GRID_SIZE,
} from "../panels/lovelace/common/compute-card-grid-size";
import { HomeAssistant } from "../types";
import { conditionalClamp } from "../common/number/clamp";
type GridSizeValue = {
rows?: number | "auto";
columns?: number;
};
@customElement("ha-grid-size-picker")
export class HaGridSizeEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public value?: CardGridSize;
@property({ attribute: false }) public value?: GridSizeValue;
@property({ attribute: false }) public rows = 8;
@@ -34,7 +34,7 @@ export class HaGridSizeEditor extends LitElement {
@property({ attribute: false }) public isDefault?: boolean;
@state() public _localValue?: CardGridSize = { rows: 1, columns: 1 };
@state() public _localValue?: GridSizeValue = undefined;
protected willUpdate(changedProperties) {
if (changedProperties.has("value")) {
@@ -49,7 +49,6 @@ export class HaGridSizeEditor extends LitElement {
this.rowMin !== undefined && this.rowMin === this.rowMax;
const autoHeight = this._localValue?.rows === "auto";
const fullWidth = this._localValue?.columns === "full";
const rowMin = this.rowMin ?? 1;
const rowMax = this.rowMax ?? this.rows;
@@ -68,7 +67,7 @@ export class HaGridSizeEditor extends LitElement {
.min=${columnMin}
.max=${columnMax}
.range=${this.columns}
.value=${fullWidth ? this.columns : columnValue}
.value=${columnValue}
@value-changed=${this._valueChanged}
@slider-moved=${this._sliderMoved}
.disabled=${disabledColumns}
@@ -105,12 +104,12 @@ export class HaGridSizeEditor extends LitElement {
`
: nothing}
<div
class="preview ${classMap({ "full-width": fullWidth })}"
class="preview"
style=${styleMap({
"--total-rows": this.rows,
"--total-columns": this.columns,
"--rows": rowValue,
"--columns": fullWidth ? this.columns : columnValue,
"--columns": columnValue,
})}
>
<div>
@@ -141,21 +140,12 @@ export class HaGridSizeEditor extends LitElement {
const cell = ev.currentTarget as HTMLElement;
const rows = Number(cell.getAttribute("data-row"));
const columns = Number(cell.getAttribute("data-column"));
const clampedRow: CardGridSize["rows"] = conditionalClamp(
rows,
this.rowMin,
this.rowMax
);
let clampedColumn: CardGridSize["columns"] = conditionalClamp(
const clampedRow = conditionalClamp(rows, this.rowMin, this.rowMax);
const clampedColumn = conditionalClamp(
columns,
this.columnMin,
this.columnMax
);
const currentSize = this.value ?? DEFAULT_GRID_SIZE;
if (currentSize.columns === "full" && clampedColumn === this.columns) {
clampedColumn = "full";
}
fireEvent(this, "value-changed", {
value: { rows: clampedRow, columns: clampedColumn },
});
@@ -163,23 +153,12 @@ export class HaGridSizeEditor extends LitElement {
private _valueChanged(ev) {
ev.stopPropagation();
const key = ev.currentTarget.id as "rows" | "columns";
const currentSize = this.value ?? DEFAULT_GRID_SIZE;
let value = ev.detail.value as CardGridSize[typeof key];
if (
key === "columns" &&
currentSize.columns === "full" &&
value === this.columns
) {
value = "full";
}
const newSize = {
...currentSize,
[key]: value,
const key = ev.currentTarget.id;
const newValue = {
...this.value,
[key]: ev.detail.value,
};
fireEvent(this, "value-changed", { value: newSize });
fireEvent(this, "value-changed", { value: newValue });
}
private _reset(ev) {
@@ -194,14 +173,11 @@ export class HaGridSizeEditor extends LitElement {
private _sliderMoved(ev) {
ev.stopPropagation();
const key = ev.currentTarget.id as "rows" | "columns";
const currentSize = this.value ?? DEFAULT_GRID_SIZE;
const value = ev.detail.value as CardGridSize[typeof key] | undefined;
const key = ev.currentTarget.id;
const value = ev.detail.value;
if (value === undefined) return;
this._localValue = {
...currentSize,
...this.value,
[key]: ev.detail.value,
};
}
@@ -213,7 +189,7 @@ export class HaGridSizeEditor extends LitElement {
grid-template-areas:
"reset column-slider"
"row-slider preview";
grid-template-rows: auto auto;
grid-template-rows: auto 1fr;
grid-template-columns: auto 1fr;
gap: 8px;
}
@@ -229,12 +205,17 @@ export class HaGridSizeEditor extends LitElement {
.preview {
position: relative;
grid-area: preview;
aspect-ratio: 1 / 1.2;
}
.preview > div {
position: relative;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
display: grid;
grid-template-columns: repeat(var(--total-columns), 1fr);
grid-template-rows: repeat(var(--total-rows), 25px);
grid-template-rows: repeat(var(--total-rows), 1fr);
gap: 4px;
}
.preview .cell {
@@ -245,23 +226,15 @@ export class HaGridSizeEditor extends LitElement {
opacity: 0.2;
cursor: pointer;
}
.preview .selected {
position: absolute;
.selected {
pointer-events: none;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
.selected .cell {
background-color: var(--primary-color);
grid-column: 1 / span min(var(--columns, 0), var(--total-columns));
grid-row: 1 / span min(var(--rows, 0), var(--total-rows));
grid-column: 1 / span var(--columns, 0);
grid-row: 1 / span var(--rows, 0);
opacity: 0.5;
}
.preview.full-width .selected .cell {
grid-column: 1 / -1;
}
`,
];
}

View File

@@ -1,58 +0,0 @@
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
type HeadingBadgeType = "text" | "button";
@customElement("ha-heading-badge")
export class HaBadge extends LitElement {
@property() public type: HeadingBadgeType = "text";
protected render() {
return html`
<div
class="heading-badge"
role=${ifDefined(this.type === "button" ? "button" : undefined)}
tabindex=${ifDefined(this.type === "button" ? "0" : undefined)}
>
<slot name="icon"></slot>
<slot></slot>
</div>
`;
}
static get styles(): CSSResultGroup {
return css`
:host {
color: var(--secondary-text-color);
}
[role="button"] {
cursor: pointer;
}
.heading-badge {
display: flex;
flex-direction: row;
white-space: nowrap;
align-items: center;
gap: 3px;
font-family: Roboto;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: 0.1px;
--mdc-icon-size: 14px;
}
::slotted([slot="icon"]) {
--ha-icon-display: block;
color: var(--icon-color, inherit);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-heading-badge": HaBadge;
}
}

View File

@@ -2,8 +2,8 @@ import { MdListItem } from "@material/web/list/list-item";
import { css } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-md-list-item")
export class HaMdListItem extends MdListItem {
@customElement("ha-list-item-new")
export class HaListItemNew extends MdListItem {
static override styles = [
...super.styles,
css`
@@ -21,6 +21,6 @@ export class HaMdListItem extends MdListItem {
declare global {
interface HTMLElementTagNameMap {
"ha-md-list-item": HaMdListItem;
"ha-list-item-new": HaListItemNew;
}
}

View File

@@ -2,8 +2,8 @@ import { MdList } from "@material/web/list/list";
import { css } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-md-list")
export class HaMdList extends MdList {
@customElement("ha-list-new")
export class HaListNew extends MdList {
static override styles = [
...super.styles,
css`
@@ -16,6 +16,6 @@ export class HaMdList extends MdList {
declare global {
interface HTMLElementTagNameMap {
"ha-md-list": HaMdList;
"ha-list-new": HaListNew;
}
}

View File

@@ -96,25 +96,7 @@ class HaMarkdownElement extends ReactiveElement {
haAlertNode.append(
...Array.from(node.childNodes)
.map((child) => {
const arr = Array.from(child.childNodes);
if (!this.breaks && arr.length) {
// When we are not breaking, the first line of the blockquote is not considered,
// so we need to adjust the first child text content
const firstChild = arr[0];
if (
firstChild.nodeType === Node.TEXT_NODE &&
firstChild.textContent === gitHubAlertMatch.input &&
firstChild.textContent?.includes("\n")
) {
firstChild.textContent = firstChild.textContent
.split("\n")
.slice(1)
.join("\n");
}
}
return arr;
})
.map((child) => Array.from(child.childNodes))
.reduce((acc, val) => acc.concat(val), [])
.filter(
(childNode) =>

View File

@@ -1,250 +0,0 @@
import { MdDialog } from "@material/web/dialog/dialog";
import {
type DialogAnimation,
DIALOG_DEFAULT_CLOSE_ANIMATION,
DIALOG_DEFAULT_OPEN_ANIMATION,
} from "@material/web/dialog/internal/animations";
import { css } from "lit";
import { customElement, property } from "lit/decorators";
// workaround to be able to overlay an dialog with another dialog
MdDialog.addInitializer(async (instance) => {
await instance.updateComplete;
const dialogInstance = instance as MdDialog;
// @ts-expect-error dialog is private
dialogInstance.dialog.prepend(dialogInstance.scrim);
// @ts-expect-error scrim is private
dialogInstance.scrim.style.inset = 0;
// @ts-expect-error scrim is private
dialogInstance.scrim.style.zIndex = 0;
const { getOpenAnimation, getCloseAnimation } = dialogInstance;
dialogInstance.getOpenAnimation = () => {
const animations = getOpenAnimation.call(this);
animations.container = [
...(animations.container ?? []),
...(animations.dialog ?? []),
];
animations.dialog = [];
return animations;
};
dialogInstance.getCloseAnimation = () => {
const animations = getCloseAnimation.call(this);
animations.container = [
...(animations.container ?? []),
...(animations.dialog ?? []),
];
animations.dialog = [];
return animations;
};
});
let DIALOG_POLYFILL: Promise<typeof import("dialog-polyfill")>;
/**
* Based on the home assistant design: https://design.home-assistant.io/#components/ha-dialogs
*
*/
@customElement("ha-md-dialog")
export class HaMdDialog extends MdDialog {
/**
* When true the dialog will not close when the user presses the esc key or press out of the dialog.
*/
@property({ attribute: "disable-cancel-action", type: Boolean })
public disableCancelAction = false;
private _polyfillDialogRegistered = false;
constructor() {
super();
this.addEventListener("cancel", this._handleCancel);
if (typeof HTMLDialogElement !== "function") {
this.addEventListener("open", this._handleOpen);
if (!DIALOG_POLYFILL) {
DIALOG_POLYFILL = import("dialog-polyfill");
}
}
// if browser doesn't support animate API disable open/close animations
if (this.animate === undefined) {
this.quick = true;
}
// if browser doesn't support animate API disable open/close animations
if (this.animate === undefined) {
this.quick = true;
}
}
// prevent open in older browsers and wait for polyfill to load
private async _handleOpen(openEvent: Event) {
openEvent.preventDefault();
if (this._polyfillDialogRegistered) {
return;
}
this._polyfillDialogRegistered = true;
this._loadPolyfillStylesheet("/static/polyfills/dialog-polyfill.css");
const dialog = this.shadowRoot?.querySelector(
"dialog"
) as HTMLDialogElement;
const dialogPolyfill = await DIALOG_POLYFILL;
dialogPolyfill.default.registerDialog(dialog);
this.removeEventListener("open", this._handleOpen);
this.show();
}
private async _loadPolyfillStylesheet(href) {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = href;
return new Promise<void>((resolve, reject) => {
link.onload = () => resolve();
link.onerror = () =>
reject(new Error(`Stylesheet failed to load: ${href}`));
this.shadowRoot?.appendChild(link);
});
}
_handleCancel(closeEvent: Event) {
if (this.disableCancelAction) {
closeEvent.preventDefault();
const dialogElement = this.shadowRoot?.querySelector("dialog .container");
if (this.animate !== undefined) {
dialogElement?.animate(
[
{
transform: "rotate(-1deg)",
"animation-timing-function": "ease-in",
},
{
transform: "rotate(1.5deg)",
"animation-timing-function": "ease-out",
},
{
transform: "rotate(0deg)",
"animation-timing-function": "ease-in",
},
],
{
duration: 200,
iterations: 2,
}
);
}
}
}
static override styles = [
...super.styles,
css`
:host {
--md-dialog-container-color: var(--card-background-color);
--md-dialog-headline-color: var(--primary-text-color);
--md-dialog-supporting-text-color: var(--primary-text-color);
--md-sys-color-scrim: #000000;
--md-dialog-headline-weight: 400;
--md-dialog-headline-size: 1.574rem;
--md-dialog-supporting-text-size: 1rem;
--md-dialog-supporting-text-line-height: 1.5rem;
}
:host([type="alert"]) {
min-width: 320px;
}
:host(:not([type="alert"])) {
@media all and (max-width: 450px), all and (max-height: 500px) {
min-width: calc(
100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
);
max-width: calc(
100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
);
min-height: 100%;
max-height: 100%;
--md-dialog-container-shape: 0;
}
}
:host ::slotted(ha-dialog-header) {
display: contents;
}
slot[name="content"]::slotted(*) {
padding: var(--dialog-content-padding, 24px);
}
.scrim {
z-index: 10; // overlay navigation
}
`,
];
}
// by default the dialog open/close animation will be from/to the top
// but if we have a special mobile dialog which is at the bottom of the screen, an from bottom animation can be used:
const OPEN_FROM_BOTTOM_ANIMATION: DialogAnimation = {
...DIALOG_DEFAULT_OPEN_ANIMATION,
dialog: [
[
// Dialog slide up
[{ transform: "translateY(50px)" }, { transform: "translateY(0)" }],
{ duration: 500, easing: "cubic-bezier(.3,0,0,1)" },
],
],
container: [
[
// Container fade in
[{ opacity: 0 }, { opacity: 1 }],
{ duration: 50, easing: "linear", pseudoElement: "::before" },
],
],
};
const CLOSE_TO_BOTTOM_ANIMATION: DialogAnimation = {
...DIALOG_DEFAULT_CLOSE_ANIMATION,
dialog: [
[
// Dialog slide down
[{ transform: "translateY(0)" }, { transform: "translateY(50px)" }],
{ duration: 150, easing: "cubic-bezier(.3,0,0,1)" },
],
],
container: [
[
// Container fade out
[{ opacity: "1" }, { opacity: "0" }],
{ delay: 100, duration: 50, easing: "linear", pseudoElement: "::before" },
],
],
};
export const getMobileOpenFromBottomAnimation = () => {
const matches = window.matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
return matches ? OPEN_FROM_BOTTOM_ANIMATION : DIALOG_DEFAULT_OPEN_ANIMATION;
};
export const getMobileCloseToBottomAnimation = () => {
const matches = window.matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
return matches ? CLOSE_TO_BOTTOM_ANIMATION : DIALOG_DEFAULT_CLOSE_ANIMATION;
};
declare global {
interface HTMLElementTagNameMap {
"ha-md-dialog": HaMdDialog;
}
}

View File

@@ -1,21 +0,0 @@
import { MdDivider } from "@material/web/divider/divider";
import { css } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-md-divider")
export class HaMdDivider extends MdDivider {
static override styles = [
...super.styles,
css`
:host {
--md-divider-color: var(--divider-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-md-divider": HaMdDivider;
}
}

View File

@@ -2,8 +2,8 @@ import { MdMenuItem } from "@material/web/menu/menu-item";
import { css } from "lit";
import { customElement, property } from "lit/decorators";
@customElement("ha-md-menu-item")
export class HaMdMenuItem extends MdMenuItem {
@customElement("ha-menu-item")
export class HaMenuItem extends MdMenuItem {
@property({ attribute: false }) clickAction?: (item?: HTMLElement) => void;
static override styles = [
@@ -41,6 +41,6 @@ export class HaMdMenuItem extends MdMenuItem {
declare global {
interface HTMLElementTagNameMap {
"ha-md-menu-item": HaMdMenuItem;
"ha-menu-item": HaMenuItem;
}
}

View File

@@ -6,7 +6,7 @@ import {
} from "@material/web/menu/internal/controllers/shared";
import { css } from "lit";
import { customElement } from "lit/decorators";
import type { HaMdMenuItem } from "./ha-md-menu-item";
import type { HaMenuItem } from "./ha-menu-item";
@customElement("ha-menu")
export class HaMenu extends MdMenu {
@@ -22,7 +22,7 @@ export class HaMenu extends MdMenu {
) {
return;
}
(ev.detail.initiator as HaMdMenuItem).clickAction?.(ev.detail.initiator);
(ev.detail.initiator as HaMenuItem).clickAction?.(ev.detail.initiator);
}
static override styles = [

View File

@@ -24,11 +24,9 @@ export class HaOutlinedField extends MdOutlinedField {
}
.with-start .start {
margin-inline-end: var(--ha-outlined-field-start-margin, 4px);
margin-inline-start: initial;
}
.with-end .end {
margin-inline-start: var(--ha-outlined-field-end-margin, 4px);
margin-inline-end: initial;
}
`,
];

View File

@@ -1,185 +0,0 @@
import { TextAreaCharCounter } from "@material/mwc-textfield/mwc-textfield-base";
import { mdiEye, mdiEyeOff } from "@mdi/js";
import { LitElement, css, html } from "lit";
import {
customElement,
eventOptions,
property,
query,
state,
} from "lit/decorators";
import { HomeAssistant } from "../types";
import "./ha-icon-button";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
@customElement("ha-password-field")
export class HaPasswordField extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean }) public invalid?: boolean;
@property({ attribute: "error-message" }) public errorMessage?: string;
@property({ type: Boolean }) public icon = false;
@property({ type: Boolean }) public iconTrailing = false;
@property() public autocomplete?: string;
@property() public autocorrect?: string;
@property({ attribute: "input-spellcheck" })
public inputSpellcheck?: string;
@property({ type: String }) value = "";
@property({ type: String }) placeholder = "";
@property({ type: String }) label = "";
@property({ type: Boolean, reflect: true }) disabled = false;
@property({ type: Boolean }) required = false;
@property({ type: Number }) minLength = -1;
@property({ type: Number }) maxLength = -1;
@property({ type: Boolean, reflect: true }) outlined = false;
@property({ type: String }) helper = "";
@property({ type: Boolean }) validateOnInitialRender = false;
@property({ type: String }) validationMessage = "";
@property({ type: Boolean }) autoValidate = false;
@property({ type: String }) pattern = "";
@property({ type: Number }) size: number | null = null;
@property({ type: Boolean }) helperPersistent = false;
@property({ type: Boolean }) charCounter: boolean | TextAreaCharCounter =
false;
@property({ type: Boolean }) endAligned = false;
@property({ type: String }) prefix = "";
@property({ type: String }) suffix = "";
@property({ type: String }) name = "";
@property({ type: String, attribute: "input-mode" })
inputMode!: string;
@property({ type: Boolean }) readOnly = false;
@property({ type: String }) autocapitalize = "";
@state() private _unmaskedPassword = false;
@query("ha-textfield") private _textField!: HaTextField;
protected render() {
return html`<ha-textfield
.invalid=${this.invalid}
.errorMessage=${this.errorMessage}
.icon=${this.icon}
.iconTrailing=${this.iconTrailing}
.autocomplete=${this.autocomplete}
.autocorrect=${this.autocorrect}
.inputSpellcheck=${this.inputSpellcheck}
.value=${this.value}
.placeholder=${this.placeholder}
.label=${this.label}
.disabled=${this.disabled}
.required=${this.required}
.minLength=${this.minLength}
.maxLength=${this.maxLength}
.outlined=${this.outlined}
.helper=${this.helper}
.validateOnInitialRender=${this.validateOnInitialRender}
.validationMessage=${this.validationMessage}
.autoValidate=${this.autoValidate}
.pattern=${this.pattern}
.size=${this.size}
.helperPersistent=${this.helperPersistent}
.charCounter=${this.charCounter}
.endAligned=${this.endAligned}
.prefix=${this.prefix}
.name=${this.name}
.inputMode=${this.inputMode}
.readOnly=${this.readOnly}
.autocapitalize=${this.autocapitalize}
.type=${this._unmaskedPassword ? "text" : "password"}
.suffix=${html`<div style="width: 24px"></div>`}
@input=${this._handleInputChange}
></ha-textfield>
<ha-icon-button
toggles
.label=${this.hass?.localize(
this._unmaskedPassword
? "ui.components.selectors.text.hide_password"
: "ui.components.selectors.text.show_password"
) || (this._unmaskedPassword ? "Hide password" : "Show password")}
@click=${this._toggleUnmaskedPassword}
.path=${this._unmaskedPassword ? mdiEyeOff : mdiEye}
></ha-icon-button>`;
}
public checkValidity(): boolean {
return this._textField.checkValidity();
}
public reportValidity(): boolean {
return this._textField.reportValidity();
}
public setCustomValidity(message: string): void {
return this._textField.setCustomValidity(message);
}
public layout(): Promise<void> {
return this._textField.layout();
}
private _toggleUnmaskedPassword(): void {
this._unmaskedPassword = !this._unmaskedPassword;
}
@eventOptions({ passive: true })
private _handleInputChange(ev) {
this.value = ev.target.value;
}
static styles = css`
:host {
display: block;
position: relative;
}
ha-textfield {
width: 100%;
}
ha-icon-button {
position: absolute;
top: 8px;
right: 8px;
inset-inline-start: initial;
inset-inline-end: 8px;
--mdc-icon-button-size: 40px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
direction: var(--direction);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-password-field": HaPasswordField;
}
}

View File

@@ -1,7 +1,6 @@
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { Action, migrateAutomationAction } from "../../data/script";
import { Action } from "../../data/script";
import { ActionSelector } from "../../data/selector";
import "../../panels/config/automation/action/ha-automation-action";
import { HomeAssistant } from "../../types";
@@ -18,19 +17,12 @@ export class HaActionSelector extends LitElement {
@property({ type: Boolean, reflect: true }) public disabled = false;
private _actions = memoizeOne((action: Action | undefined) => {
if (!action) {
return [];
}
return migrateAutomationAction(action);
});
protected render() {
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
<ha-automation-action
.disabled=${this.disabled}
.actions=${this._actions(this.value)}
.actions=${this.value || []}
.hass=${this.hass}
.path=${this.selector.action?.path}
></ha-automation-action>

View File

@@ -1,15 +1,19 @@
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { BooleanSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../ha-checkbox";
import "../ha-formfield";
import "../ha-switch";
import "../ha-input-helper-text";
import "../ha-switch";
@customElement("ha-selector-boolean")
export class HaBooleanSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: BooleanSelector;
@property({ type: Boolean }) public value = false;
@property() public placeholder?: any;
@@ -21,20 +25,28 @@ export class HaBooleanSelector extends LitElement {
@property({ type: Boolean }) public disabled = false;
protected render() {
const checkbox = this.selector.boolean?.mode === "checkbox";
return html`
<ha-formfield alignEnd spaceBetween .label=${this.label}>
<ha-switch
.checked=${this.value ?? this.placeholder === true}
@change=${this._handleChange}
.disabled=${this.disabled}
></ha-switch>
<span slot="label">
<p class="primary">${this.label}</p>
${this.helper
? html`<p class="secondary">${this.helper}</p>`
: nothing}
</span>
<ha-formfield .alignEnd=${!checkbox} spaceBetween .label=${this.label}>
${checkbox
? html`
<ha-checkbox
.checked=${this.value ?? this.placeholder === true}
@change=${this._handleChange}
.disabled=${this.disabled}
></ha-checkbox>
`
: html`
<ha-switch
.checked=${this.value ?? this.placeholder === true}
@change=${this._handleChange}
.disabled=${this.disabled}
></ha-switch>
`}
</ha-formfield>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""}
`;
}
@@ -50,21 +62,10 @@ export class HaBooleanSelector extends LitElement {
return css`
ha-formfield {
display: flex;
min-height: 56px;
height: 56px;
align-items: center;
--mdc-typography-body2-font-size: 1em;
}
p {
margin: 0;
}
.secondary {
direction: var(--direction);
padding-top: 4px;
box-sizing: border-box;
color: var(--secondary-text-color);
font-size: 0.875rem;
font-weight: var(--mdc-typography-body2-font-weight, 400);
}
`;
}
}

View File

@@ -31,7 +31,7 @@ export class HaColorRGBSelector extends LitElement {
.label=${this.label || ""}
.required=${this.required}
.helper=${this.helper}
.disabled=${this.disabled}
.disalbled=${this.disabled}
@change=${this._valueChanged}
></ha-textfield>
`;

View File

@@ -162,14 +162,8 @@ export class HaLocationSelector extends LitElement {
private _computeLabel = (
entry: SchemaUnion<ReturnType<typeof this._schema>>
): string => {
if (entry.name) {
return this.hass.localize(
`ui.components.selectors.location.${entry.name}`
);
}
return "";
};
): string =>
this.hass.localize(`ui.components.selectors.location.${entry.name}`);
static styles = css`
ha-locations-editor {

View File

@@ -1,11 +1,4 @@
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
} from "lit";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../common/dom/fire_event";
@@ -67,12 +60,12 @@ export class HaNumberSelector extends LitElement {
}
return html`
${this.label && !isBox
? html`${this.label}${this.required ? "*" : ""}`
: nothing}
<div class="input">
${!isBox
? html`
${this.label
? html`${this.label}${this.required ? "*" : ""}`
: ""}
<ha-slider
labeled
.min=${this.selector.number!.min}
@@ -82,11 +75,10 @@ export class HaNumberSelector extends LitElement {
.disabled=${this.disabled}
.required=${this.required}
@change=${this._handleSliderChange}
.ticks=${this.selector.number?.slider_ticks}
>
</ha-slider>
`
: nothing}
: ""}
<ha-textfield
.inputMode=${this.selector.number?.step === "any" ||
(this.selector.number?.step ?? 1) % 1 !== 0
@@ -113,7 +105,7 @@ export class HaNumberSelector extends LitElement {
</div>
${!isBox && this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: nothing}
: ""}
`;
}
@@ -149,9 +141,6 @@ export class HaNumberSelector extends LitElement {
}
ha-slider {
flex: 1;
margin-right: 16px;
margin-inline-end: 16px;
margin-inline-start: 0;
}
ha-textfield {
--ha-textfield-input-width: 40px;

View File

@@ -7,7 +7,12 @@ import "../ha-code-editor";
import "../ha-input-helper-text";
import "../ha-alert";
const WARNING_STRINGS = ["template:", "sensor:", "state:", "trigger: template"];
const WARNING_STRINGS = [
"template:",
"sensor:",
"state:",
"platform: template",
];
@customElement("ha-selector-template")
export class HaTemplateSelector extends LitElement {

View File

@@ -82,7 +82,6 @@ export class HaTextSelector extends LitElement {
.disabled=${this.disabled}
.type=${this._unmaskedPassword ? "text" : this.selector.text?.type}
@input=${this._handleChange}
@change=${this._handleChange}
.label=${this.label || ""}
.prefix=${this.selector.text?.prefix}
.suffix=${this.selector.text?.type === "password"

View File

@@ -30,7 +30,7 @@ export class HaTimeSelector extends LitElement {
clearable
.helper=${this.helper}
.label=${this.label}
.enableSecond=${!this.selector.time?.no_second}
enable-second
></ha-time-input>
`;
}

View File

@@ -1,7 +1,6 @@
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { migrateAutomationTrigger, Trigger } from "../../data/automation";
import { Trigger } from "../../data/automation";
import { TriggerSelector } from "../../data/selector";
import "../../panels/config/automation/trigger/ha-automation-trigger";
import { HomeAssistant } from "../../types";
@@ -18,19 +17,12 @@ export class HaTriggerSelector extends LitElement {
@property({ type: Boolean, reflect: true }) public disabled = false;
private _triggers = memoizeOne((trigger: Trigger | undefined) => {
if (!trigger) {
return [];
}
return migrateAutomationTrigger(trigger);
});
protected render() {
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
<ha-automation-trigger
.disabled=${this.disabled}
.triggers=${this._triggers(this.value)}
.triggers=${this.value || []}
.hass=${this.hass}
.path=${this.selector.trigger?.path}
></ha-automation-trigger>

View File

@@ -24,8 +24,6 @@ export class HaSelectorUiColor extends LitElement {
.hass=${this.hass}
.value=${this.value}
.helper=${this.helper}
.includeNone=${this.selector.ui_color?.include_none}
.includeState=${this.selector.ui_color?.include_state}
.defaultColor=${this.selector.ui_color?.default_color}
@value-changed=${this._valueChanged}
></ha-color-picker>

View File

@@ -44,7 +44,6 @@ import "./ha-service-picker";
import "./ha-settings-row";
import "./ha-yaml-editor";
import type { HaYamlEditor } from "./ha-yaml-editor";
import "./ha-service-section-icon";
const attributeFilter = (values: any[], attribute: any) => {
if (typeof attribute === "object") {
@@ -240,24 +239,12 @@ export class HaServiceControl extends LitElement {
...value,
selector: value.selector as Selector | undefined,
}));
const hasSelector: string[] = [];
fields.forEach((field) => {
if ((field as any).fields) {
Object.entries((field as any).fields).forEach(([key, subField]) => {
if ((subField as any).selector) {
hasSelector.push(key);
}
});
} else if (field.selector) {
hasSelector.push(field.key);
}
});
return {
...serviceDomains[domain][serviceName],
fields,
hasSelector,
hasSelector: fields.length
? fields.filter((field) => field.selector).map((field) => field.key)
: [],
};
}
);
@@ -509,18 +496,12 @@ export class HaServiceControl extends LitElement {
) ||
dataField.name ||
dataField.key}
.secondary=${this._getSectionDescription(
>
${this._renderSectionDescription(
dataField,
domain,
serviceName
)}
>
<ha-service-section-icon
slot="icons"
.hass=${this.hass}
.service=${this._value!.action}
.section=${dataField.key}
></ha-service-section-icon>
${Object.entries(dataField.fields).map(([key, field]) =>
this._renderField(
{ key, ...field },
@@ -541,14 +522,20 @@ export class HaServiceControl extends LitElement {
)} `;
}
private _getSectionDescription(
private _renderSectionDescription(
dataField: ExtHassService["fields"][number],
domain: string | undefined,
serviceName: string | undefined
) {
return this.hass!.localize(
const description = this.hass!.localize(
`component.${domain}.services.${serviceName}.sections.${dataField.key}.description`
);
if (!description) {
return nothing;
}
return html`<p>${description}</p>`;
}
private _renderField = (
@@ -805,8 +792,7 @@ export class HaServiceControl extends LitElement {
const value = ev.detail.value;
if (
this._value?.data?.[key] === value ||
((!this._value?.data || !(key in this._value.data)) &&
(value === "" || value === undefined))
(!this._value?.data?.[key] && (value === "" || value === undefined))
) {
return;
}

View File

@@ -1,53 +0,0 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { until } from "lit/directives/until";
import { HomeAssistant } from "../types";
import "./ha-icon";
import "./ha-svg-icon";
import { serviceSectionIcon } from "../data/icons";
@customElement("ha-service-section-icon")
export class HaServiceSectionIcon extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public service?: string;
@property() public section?: string;
@property() public icon?: string;
protected render() {
if (this.icon) {
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
}
if (!this.service || !this.section) {
return nothing;
}
if (!this.hass) {
return this._renderFallback();
}
const icon = serviceSectionIcon(this.hass, this.service, this.section).then(
(icn) => {
if (icn) {
return html`<ha-icon .icon=${icn}></ha-icon>`;
}
return this._renderFallback();
}
);
return html`${until(icon)}`;
}
private _renderFallback() {
return nothing;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-service-section-icon": HaServiceSectionIcon;
}
}

View File

@@ -20,7 +20,6 @@ export class HaSlider extends MdSlider {
--md-sys-color-on-surface: var(--primary-text-color);
--md-slider-handle-width: 14px;
--md-slider-handle-height: 14px;
--md-slider-state-layer-size: 24px;
min-width: 100px;
min-inline-size: 100px;
width: 200px;

View File

@@ -43,13 +43,6 @@ export class HaSortable extends LitElement {
@property({ type: String, attribute: "handle-selector" })
public handleSelector?: string;
/**
* Selectors that do not lead to dragging (String or Function)
* https://github.com/SortableJS/Sortable?tab=readme-ov-file#filter-option
* */
@property({ type: String, attribute: "filter" })
public filter?: string;
@property({ type: String })
public group?: string | SortableInstance.GroupOptions;
@@ -152,9 +145,6 @@ export class HaSortable extends LitElement {
if (this.group) {
options.group = this.group;
}
if (this.filter) {
options.filter = this.filter;
}
this._sortable = new Sortable(container, options);
}

View File

@@ -16,10 +16,11 @@ import { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
import type { HaSelect } from "./ha-select";
import { computeDomain } from "../common/entity/compute_domain";
const NONE = "__NONE_OPTION__";
const NAME_MAP = { cloud: "Home Assistant Cloud" };
@customElement("ha-stt-picker")
export class HaSTTPicker extends LitElement {
@property() public value?: string;
@@ -40,32 +41,13 @@ export class HaSTTPicker extends LitElement {
if (!this._engines) {
return nothing;
}
let value = this.value;
if (!value && this.required) {
for (const entity of Object.values(this.hass.entities)) {
if (
entity.platform === "cloud" &&
computeDomain(entity.entity_id) === "stt"
) {
value = entity.entity_id;
break;
}
}
if (!value) {
for (const sttEngine of this._engines) {
if (sttEngine?.supported_languages?.length !== 0) {
value = sttEngine.engine_id;
break;
}
}
}
}
if (!value) {
value = NONE;
}
const value =
this.value ??
(this.required
? this._engines.find(
(engine) => engine.supported_languages?.length !== 0
)
: NONE);
return html`
<ha-select
.label=${this.label ||
@@ -84,15 +66,12 @@ export class HaSTTPicker extends LitElement {
</ha-list-item>`
: nothing}
${this._engines.map((engine) => {
if (engine.deprecated && engine.engine_id !== value) {
return nothing;
}
let label: string;
let label = engine.engine_id;
if (engine.engine_id.includes(".")) {
const stateObj = this.hass!.states[engine.engine_id];
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
} else {
label = engine.name || engine.engine_id;
} else if (engine.engine_id in NAME_MAP) {
label = NAME_MAP[engine.engine_id];
}
return html`<ha-list-item
.value=${engine.engine_id}

View File

@@ -35,6 +35,10 @@ import {
computeDeviceName,
} from "../data/device_registry";
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
import {
FloorRegistryEntry,
subscribeFloorRegistry,
} from "../data/floor_registry";
import {
LabelRegistryEntry,
subscribeLabelRegistry,
@@ -99,12 +103,17 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
@query(".add-container", true) private _addContainer?: HTMLDivElement;
@state() private _floors?: FloorRegistryEntry[];
@state() private _labels?: LabelRegistryEntry[];
private _opened = false;
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
return [
subscribeFloorRegistry(this.hass.connection, (floors) => {
this._floors = floors;
}),
subscribeLabelRegistry(this.hass.connection, (labels) => {
this._labels = labels;
}),
@@ -123,7 +132,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
<div class="mdc-chip-set items">
${this.value?.floor_id
? ensureArray(this.value.floor_id).map((floor_id) => {
const floor = this.hass.floors[floor_id];
const floor = this._floors?.find(
(flr) => flr.floor_id === floor_id
);
return this._renderChip(
"floor_id",
floor_id,

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