Compare commits

..

1 Commits

Author SHA1 Message Date
Simon Lamon
e93f105542 Update dropdown adjustments 2025-12-02 19:02:13 +00:00
443 changed files with 4881 additions and 7099 deletions

View File

@@ -21,12 +21,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with: with:
ref: dev ref: dev
- name: Setup Node - name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -56,12 +56,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with: with:
ref: master ref: master
- name: Setup Node - name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn

View File

@@ -24,9 +24,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -58,9 +58,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -76,9 +76,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -100,9 +100,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn

View File

@@ -23,7 +23,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with: with:
# We must fetch at least the immediate parents so that if this is # We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head. # a pull request then we can checkout the head.
@@ -36,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7 uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7 uses: github/codeql-action/autobuild@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@@ -57,4 +57,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7 uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5

View File

@@ -22,12 +22,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with: with:
ref: dev ref: dev
- name: Setup Node - name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -57,12 +57,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with: with:
ref: master ref: master
- name: Setup Node - name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn

View File

@@ -16,10 +16,10 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn 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') if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn

View File

@@ -20,7 +20,7 @@ jobs:
contents: write contents: write
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Python ${{ env.PYTHON_VERSION }} - name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
@@ -28,7 +28,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node - name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn

View File

@@ -23,7 +23,7 @@ jobs:
contents: write # Required to upload release assets contents: write # Required to upload release assets
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Python ${{ env.PYTHON_VERSION }} - name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
@@ -34,7 +34,7 @@ jobs:
uses: home-assistant/actions/helpers/verify-version@master uses: home-assistant/actions/helpers/verify-version@master
- name: Setup Node - name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -75,7 +75,7 @@ jobs:
# home-assistant/wheels doesn't support SHA pinning # home-assistant/wheels doesn't support SHA pinning
- name: Build wheels - name: Build wheels
uses: home-assistant/wheels@2025.12.0 uses: home-assistant/wheels@2025.11.0
with: with:
abi: cp313 abi: cp313
tag: musllinux_1_2 tag: musllinux_1_2
@@ -91,9 +91,9 @@ jobs:
contents: write # Required to upload release assets contents: write # Required to upload release assets
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -120,9 +120,9 @@ jobs:
contents: write # Required to upload release assets contents: write # Required to upload release assets
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn

View File

@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 90 days stale policy - name: 90 days stale policy
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 90 days-before-stale: 90

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Upload Translations - name: Upload Translations
run: | run: |

View File

@@ -156,9 +156,7 @@ const createTestTranslation = () =>
*/ */
const createMasterTranslation = () => const createMasterTranslation = () =>
gulp gulp
.src([EN_SRC, ...(mergeBackend ? [`${inBackendDir}/en.json`] : [])], { .src([EN_SRC, ...(mergeBackend ? [`${inBackendDir}/en.json`] : [])])
allowEmpty: true,
})
.pipe(new CustomJSON(lokaliseTransform)) .pipe(new CustomJSON(lokaliseTransform))
.pipe(new MergeJSON("en")) .pipe(new MergeJSON("en"))
.pipe(gulp.dest(workDir)); .pipe(gulp.dest(workDir));

View File

@@ -1,4 +1,4 @@
import type { DeviceRegistryEntry } from "../../../src/data/device/device_registry"; import type { DeviceRegistryEntry } from "../../../src/data/device_registry";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockDeviceRegistry = ( export const mockDeviceRegistry = (

View File

@@ -44,24 +44,18 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
number_energy_price: null, number_energy_price: null,
}, },
], ],
power: [
{ stat_rate: "sensor.power_grid" },
{ stat_rate: "sensor.power_grid_return" },
],
cost_adjustment_day: 0, cost_adjustment_day: 0,
}, },
{ {
type: "solar", type: "solar",
stat_energy_from: "sensor.solar_production", stat_energy_from: "sensor.solar_production",
stat_rate: "sensor.power_solar",
config_entry_solar_forecast: ["solar_forecast"], config_entry_solar_forecast: ["solar_forecast"],
}, },
{ /* {
type: "battery", type: "battery",
stat_energy_from: "sensor.battery_output", stat_energy_from: "sensor.battery_output",
stat_energy_to: "sensor.battery_input", stat_energy_to: "sensor.battery_input",
stat_rate: "sensor.power_battery", }, */
},
{ {
type: "gas", type: "gas",
stat_energy_from: "sensor.energy_gas", stat_energy_from: "sensor.energy_gas",
@@ -69,48 +63,28 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
entity_energy_price: null, entity_energy_price: null,
number_energy_price: null, number_energy_price: null,
}, },
{
type: "water",
stat_energy_from: "sensor.energy_water",
stat_cost: "sensor.energy_water_cost",
entity_energy_price: null,
number_energy_price: null,
},
], ],
device_consumption: [ device_consumption: [
{ {
stat_consumption: "sensor.energy_car", stat_consumption: "sensor.energy_car",
stat_rate: "sensor.power_car",
}, },
{ {
stat_consumption: "sensor.energy_ac", stat_consumption: "sensor.energy_ac",
stat_rate: "sensor.power_ac",
}, },
{ {
stat_consumption: "sensor.energy_washing_machine", stat_consumption: "sensor.energy_washing_machine",
stat_rate: "sensor.power_washing_machine",
}, },
{ {
stat_consumption: "sensor.energy_dryer", stat_consumption: "sensor.energy_dryer",
stat_rate: "sensor.power_dryer",
}, },
{ {
stat_consumption: "sensor.energy_heat_pump", stat_consumption: "sensor.energy_heat_pump",
stat_rate: "sensor.power_heat_pump",
}, },
{ {
stat_consumption: "sensor.energy_boiler", stat_consumption: "sensor.energy_boiler",
stat_rate: "sensor.power_boiler",
},
],
device_consumption_water: [
{
stat_consumption: "sensor.water_kitchen",
},
{
stat_consumption: "sensor.water_garden",
}, },
], ],
device_consumption_water: [],
}) })
); );
hass.mockWS( hass.mockWS(

View File

@@ -154,38 +154,6 @@ export const energyEntities = () =>
unit_of_measurement: "EUR", unit_of_measurement: "EUR",
}, },
}, },
"sensor.power_grid": {
entity_id: "sensor.power_grid",
state: "500",
attributes: {
state_class: "measurement",
unit_of_measurement: "W",
},
},
"sensor.power_grid_return": {
entity_id: "sensor.power_grid_return",
state: "-100",
attributes: {
state_class: "measurement",
unit_of_measurement: "W",
},
},
"sensor.power_solar": {
entity_id: "sensor.power_solar",
state: "200",
attributes: {
state_class: "measurement",
unit_of_measurement: "W",
},
},
"sensor.power_battery": {
entity_id: "sensor.power_battery",
state: "100",
attributes: {
state_class: "measurement",
unit_of_measurement: "W",
},
},
"sensor.energy_gas_cost": { "sensor.energy_gas_cost": {
entity_id: "sensor.energy_gas_cost", entity_id: "sensor.energy_gas_cost",
state: "2", state: "2",
@@ -203,15 +171,6 @@ export const energyEntities = () =>
unit_of_measurement: "m³", unit_of_measurement: "m³",
}, },
}, },
"sensor.energy_water": {
entity_id: "sensor.energy_water",
state: "4000",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
friendly_name: "Water",
unit_of_measurement: "L",
},
},
"sensor.energy_car": { "sensor.energy_car": {
entity_id: "sensor.energy_car", entity_id: "sensor.energy_car",
state: "4", state: "4",
@@ -266,58 +225,4 @@ export const energyEntities = () =>
unit_of_measurement: "kWh", unit_of_measurement: "kWh",
}, },
}, },
"sensor.power_car": {
entity_id: "sensor.power_car",
state: "40",
attributes: {
state_class: "measurement",
friendly_name: "Electric car",
unit_of_measurement: "W",
},
},
"sensor.power_ac": {
entity_id: "sensor.power_ac",
state: "30",
attributes: {
state_class: "measurement",
friendly_name: "Air conditioning",
unit_of_measurement: "W",
},
},
"sensor.power_washing_machine": {
entity_id: "sensor.power_washing_machine",
state: "60",
attributes: {
state_class: "measurement",
friendly_name: "Washing machine",
unit_of_measurement: "W",
},
},
"sensor.power_dryer": {
entity_id: "sensor.power_dryer",
state: "55",
attributes: {
state_class: "measurement",
friendly_name: "Dryer",
unit_of_measurement: "W",
},
},
"sensor.power_heat_pump": {
entity_id: "sensor.power_heat_pump",
state: "60",
attributes: {
state_class: "measurement",
friendly_name: "Heat pump",
unit_of_measurement: "W",
},
},
"sensor.power_boiler": {
entity_id: "sensor.power_boiler",
state: "70",
attributes: {
state_class: "measurement",
friendly_name: "Boiler",
unit_of_measurement: "W",
},
},
}); });

View File

@@ -1,4 +1,4 @@
import type { EntityRegistryEntry } from "../../../src/data/entity/entity_registry"; import type { EntityRegistryEntry } from "../../../src/data/entity_registry";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockEntityRegistry = ( export const mockEntityRegistry = (

View File

@@ -1,4 +1,4 @@
import type { LabelRegistryEntry } from "../../../src/data/label/label_registry"; import type { LabelRegistryEntry } from "../../../src/data/label_registry";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockLabelRegistry = ( export const mockLabelRegistry = (

View File

@@ -17,15 +17,17 @@ const generateMeanStatistics = (
end: Date, end: Date,
// eslint-disable-next-line default-param-last // eslint-disable-next-line default-param-last
period: "5minute" | "hour" | "day" | "month" = "hour", period: "5minute" | "hour" | "day" | "month" = "hour",
initValue: number,
maxDiff: number maxDiff: number
): StatisticValue[] => { ): StatisticValue[] => {
const statistics: StatisticValue[] = []; const statistics: StatisticValue[] = [];
let currentDate = new Date(start); let currentDate = new Date(start);
currentDate.setMinutes(0, 0, 0); currentDate.setMinutes(0, 0, 0);
let lastVal = initValue;
const now = new Date(); const now = new Date();
while (end > currentDate && currentDate < now) { while (end > currentDate && currentDate < now) {
const delta = Math.random() * maxDiff; const delta = Math.random() * maxDiff;
const mean = delta; const mean = lastVal + delta;
statistics.push({ statistics.push({
start: currentDate.getTime(), start: currentDate.getTime(),
end: currentDate.getTime(), end: currentDate.getTime(),
@@ -36,6 +38,7 @@ const generateMeanStatistics = (
state: mean, state: mean,
sum: null, sum: null,
}); });
lastVal = mean;
currentDate = currentDate =
period === "day" period === "day"
? addDays(currentDate, 1) ? addDays(currentDate, 1)
@@ -333,6 +336,7 @@ export const mockRecorder = (mockHass: MockHomeAssistant) => {
start, start,
end, end,
period, period,
state,
state * (state > 80 ? 0.05 : 0.1) state * (state > 80 ? 0.05 : 0.1)
); );
} }

View File

@@ -142,7 +142,7 @@ export class DemoAutomationDescribeAction extends LitElement {
<div class="action"> <div class="action">
<span> <span>
${this._action ${this._action
? describeAction(this.hass, [], this._action) ? describeAction(this.hass, [], [], {}, this._action)
: "<invalid YAML>"} : "<invalid YAML>"}
</span> </span>
<ha-yaml-editor <ha-yaml-editor
@@ -155,7 +155,7 @@ export class DemoAutomationDescribeAction extends LitElement {
${ACTIONS.map( ${ACTIONS.map(
(conf) => html` (conf) => html`
<div class="action"> <div class="action">
<span>${describeAction(this.hass, [], conf as any)}</span> <span>${describeAction(this.hass, [], [], {}, conf as any)}</span>
<pre>${dump(conf)}</pre> <pre>${dump(conf)}</pre>
</div> </div>
` `

View File

@@ -1,3 +0,0 @@
---
title: Adaptive dialog (ha-adaptive-dialog)
---

View File

@@ -1,732 +0,0 @@
import { css, html, LitElement } from "lit";
import { customElement, state } from "lit/decorators";
import { mdiCog, mdiHelp } from "@mdi/js";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-dialog-footer";
import "../../../../src/components/ha-adaptive-dialog";
import "../../../../src/components/ha-form/ha-form";
import "../../../../src/components/ha-icon-button";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../../src/types";
const SCHEMA: HaFormSchema[] = [
{ type: "string", name: "Name", default: "", autofocus: true },
{ type: "string", name: "Email", default: "" },
];
type DialogType =
| false
| "basic"
| "basic-subtitle-below"
| "basic-subtitle-above"
| "form"
| "form-block-mode"
| "actions"
| "large"
| "small";
@customElement("demo-components-ha-adaptive-dialog")
export class DemoHaAdaptiveDialog extends LitElement {
@state() private _openDialog: DialogType = false;
@state() private _hass?: HomeAssistant;
protected firstUpdated() {
const hass = provideHass(this);
this._hass = hass;
}
protected render() {
return html`
<div class="content">
<h1>Adaptive dialog <code>&lt;ha-adaptive-dialog&gt;</code></h1>
<p class="subtitle">
Responsive dialog component that automatically switches between a full
dialog and bottom sheet based on screen size.
</p>
<h2>Demos</h2>
<div class="buttons">
<ha-button @click=${this._handleOpenDialog("basic")}
>Basic adaptive dialog</ha-button
>
<ha-button @click=${this._handleOpenDialog("basic-subtitle-below")}
>Adaptive dialog with subtitle below</ha-button
>
<ha-button @click=${this._handleOpenDialog("basic-subtitle-above")}
>Adaptive dialog with subtitle above</ha-button
>
<ha-button @click=${this._handleOpenDialog("small")}
>Small width adaptive dialog</ha-button
>
<ha-button @click=${this._handleOpenDialog("large")}
>Large width adaptive dialog</ha-button
>
<ha-button @click=${this._handleOpenDialog("form")}
>Adaptive dialog with form</ha-button
>
<ha-button @click=${this._handleOpenDialog("form-block-mode")}
>Adaptive dialog with form (block mode change)</ha-button
>
<ha-button @click=${this._handleOpenDialog("actions")}
>Adaptive dialog with actions</ha-button
>
</div>
<ha-card>
<div class="card-content">
<p>
<strong>Tip:</strong> Resize your browser window to see the
responsive behavior. The dialog automatically switches to a bottom
sheet on narrow screens (&lt;870px width) or short screens
(&lt;500px height).
</p>
</div>
</ha-card>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "basic"}
header-title="Basic adaptive dialog"
@closed=${this._handleClosed}
>
<div>Adaptive dialog content</div>
</ha-adaptive-dialog>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "basic-subtitle-below"}
header-title="Adaptive dialog with subtitle"
header-subtitle="This is an adaptive dialog with a subtitle below"
@closed=${this._handleClosed}
>
<div>Adaptive dialog content</div>
</ha-adaptive-dialog>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "basic-subtitle-above"}
header-title="Adaptive dialog with subtitle above"
header-subtitle="This is an adaptive dialog with a subtitle above"
header-subtitle-position="above"
@closed=${this._handleClosed}
>
<div>Adaptive dialog content</div>
</ha-adaptive-dialog>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "small"}
width="small"
header-title="Small adaptive dialog"
@closed=${this._handleClosed}
>
<div>This dialog uses the small width preset (320px).</div>
</ha-adaptive-dialog>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "large"}
width="large"
header-title="Large adaptive dialog"
@closed=${this._handleClosed}
>
<div>This dialog uses the large width preset (1024px).</div>
</ha-adaptive-dialog>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "form"}
header-title="Adaptive dialog with form"
header-subtitle="This is an adaptive dialog with a form"
@closed=${this._handleClosed}
>
<ha-form autofocus .schema=${SCHEMA}></ha-form>
<ha-dialog-footer slot="footer">
<ha-button
@click=${this._handleClosed}
slot="secondaryAction"
variant="plain"
>Cancel</ha-button
>
<ha-button
@click=${this._handleClosed}
slot="primaryAction"
variant="accent"
>Submit</ha-button
>
</ha-dialog-footer>
</ha-adaptive-dialog>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "form-block-mode"}
header-title="Adaptive dialog with form (block mode change)"
header-subtitle="This form will not reset when the viewport size changes"
block-mode-change
@closed=${this._handleClosed}
>
<ha-form autofocus .schema=${SCHEMA}></ha-form>
<ha-dialog-footer slot="footer">
<ha-button
@click=${this._handleClosed}
slot="secondaryAction"
variant="plain"
>Cancel</ha-button
>
<ha-button
@click=${this._handleClosed}
slot="primaryAction"
variant="accent"
>Submit</ha-button
>
</ha-dialog-footer>
</ha-adaptive-dialog>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "actions"}
header-title="Adaptive dialog with actions"
header-subtitle="This is an adaptive dialog with header actions"
@closed=${this._handleClosed}
>
<div slot="headerActionItems">
<ha-icon-button label="Settings" path=${mdiCog}></ha-icon-button>
<ha-icon-button label="Help" path=${mdiHelp}></ha-icon-button>
</div>
<div>Adaptive dialog content</div>
</ha-adaptive-dialog>
<h2>Design</h2>
<h3>Responsive behavior</h3>
<p>
The <code>ha-adaptive-dialog</code> component automatically switches
between two modes based on screen size:
</p>
<ul>
<li>
<strong>Dialog mode:</strong> Used on larger screens (width &gt;
870px and height &gt; 500px). Renders as a centered dialog using
<code>ha-wa-dialog</code>.
</li>
<li>
<strong>Bottom sheet mode:</strong> Used on mobile devices and
smaller screens (width ≤ 870px or height ≤ 500px). Renders as a
drawer from the bottom using <code>ha-bottom-sheet</code>.
</li>
</ul>
<p>
The mode is determined automatically and updates when the window is
resized. To prevent mode changes after the initial mount (useful for
preventing form resets), use the <code>block-mode-change</code>
attribute.
</p>
<h3>Width</h3>
<p>
In dialog mode, there are multiple width presets available. These are
ignored in bottom sheet mode.
</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>small</code></td>
<td><code>min(320px, var(--full-width))</code></td>
</tr>
<tr>
<td><code>medium</code></td>
<td><code>min(580px, var(--full-width))</code></td>
</tr>
<tr>
<td><code>large</code></td>
<td><code>min(1024px, var(--full-width))</code></td>
</tr>
<tr>
<td><code>full</code></td>
<td><code>var(--full-width)</code></td>
</tr>
</tbody>
</table>
<p>Adaptive dialogs have a default width of <code>medium</code>.</p>
<h3>Header</h3>
<p>
The header contains a navigation icon, title, subtitle, and action
items.
</p>
<table>
<thead>
<tr>
<th>Slot</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>headerNavigationIcon</code></td>
<td>
Leading header action (e.g., close/back button). In bottom sheet
mode, defaults to a close button if not provided.
</td>
</tr>
<tr>
<td><code>headerTitle</code></td>
<td>The header title content.</td>
</tr>
<tr>
<td><code>headerSubtitle</code></td>
<td>The header subtitle content.</td>
</tr>
<tr>
<td><code>headerActionItems</code></td>
<td>Trailing header actions (e.g., icon buttons, menus).</td>
</tr>
</tbody>
</table>
<h4>Header title</h4>
<p>
The header title can be set using the <code>header-title</code>
attribute or by providing custom content in the
<code>headerTitle</code> slot.
</p>
<h4>Header subtitle</h4>
<p>
The header subtitle can be set using the
<code>header-subtitle</code> attribute or by providing custom content
in the <code>headerSubtitle</code> slot. The subtitle position
relative to the title can be controlled with the
<code>header-subtitle-position</code> attribute.
</p>
<h4>Header navigation icon</h4>
<p>
In bottom sheet mode, a close button is automatically provided if no
custom navigation icon is specified. In dialog mode, the dialog can be
closed via the standard dialog close button.
</p>
<h4>Header action items</h4>
<p>
The header action items usually contain icon buttons and/or menu
buttons.
</p>
<h3>Body</h3>
<p>The body is the content of the adaptive dialog.</p>
<h3>Footer</h3>
<p>The footer is the footer of the adaptive dialog.</p>
<p>
It is recommended to use the <code>ha-dialog-footer</code> component
for the footer and to style the buttons inside the footer as follows:
</p>
<table>
<thead>
<tr>
<th>Slot</th>
<th>Description</th>
<th>Variant to use</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>secondaryAction</code></td>
<td>The secondary action button(s).</td>
<td><code>plain</code></td>
</tr>
<tr>
<td><code>primaryAction</code></td>
<td>The primary action button(s).</td>
<td><code>accent</code></td>
</tr>
</tbody>
</table>
<h2>Implementation</h2>
<h3>When to use</h3>
<p>
Use <code>ha-adaptive-dialog</code> when you need a dialog that should
adapt to different screen sizes automatically. This is particularly
useful for:
</p>
<ul>
<li>Forms and data entry that need to work well on mobile devices</li>
<li>
Content that benefits from full-screen presentation on small devices
</li>
<li>
Interfaces that need consistent behavior across desktop and mobile
</li>
</ul>
<p>
If you don't need responsive behavior, use
<code>ha-wa-dialog</code> directly for desktop-only dialogs or
<code>ha-bottom-sheet</code> for mobile-only sheets.
</p>
<p>
Use the <code>block-mode-change</code> attribute when you want to
prevent the dialog from switching modes after it's opened. This is
especially useful for forms, as it prevents form data from being lost
when users resize their browser window.
</p>
<h3>Example usage</h3>
<pre><code>&lt;ha-adaptive-dialog
.hass=\${this.hass}
open
width="medium"
header-title="Dialog title"
header-subtitle="Dialog subtitle"
&gt;
&lt;div slot="headerActionItems"&gt;
&lt;ha-icon-button label="Settings" path="mdiCog"&gt;&lt;/ha-icon-button&gt;
&lt;ha-icon-button label="Help" path="mdiHelp"&gt;&lt;/ha-icon-button&gt;
&lt;/div&gt;
&lt;div&gt;Dialog content&lt;/div&gt;
&lt;ha-dialog-footer slot="footer"&gt;
&lt;ha-button slot="secondaryAction" variant="plain"
&gt;Cancel&lt;/ha-button
&gt;
&lt;ha-button slot="primaryAction" variant="accent"&gt;Submit&lt;/ha-button&gt;
&lt;/ha-dialog-footer&gt;
&lt;/ha-adaptive-dialog&gt;</code></pre>
<p>Example with <code>block-mode-change</code> for forms:</p>
<pre><code>&lt;ha-adaptive-dialog
.hass=\${this.hass}
open
header-title="Edit configuration"
block-mode-change
&gt;
&lt;ha-form .schema=\${schema} .data=\${data}&gt;&lt;/ha-form&gt;
&lt;ha-dialog-footer slot="footer"&gt;
&lt;ha-button slot="secondaryAction" variant="plain"
&gt;Cancel&lt;/ha-button
&gt;
&lt;ha-button slot="primaryAction" variant="accent"&gt;Save&lt;/ha-button&gt;
&lt;/ha-dialog-footer&gt;
&lt;/ha-adaptive-dialog&gt;</code></pre>
<h3>API</h3>
<p>
This component combines <code>ha-wa-dialog</code> and
<code>ha-bottom-sheet</code> with automatic mode switching based on
screen size.
</p>
<h4>Attributes</h4>
<table>
<thead>
<tr>
<th>Attribute</th>
<th>Description</th>
<th>Default</th>
<th>Options</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>open</code></td>
<td>Controls the adaptive dialog open state.</td>
<td><code>false</code></td>
<td><code>false</code>, <code>true</code></td>
</tr>
<tr>
<td><code>width</code></td>
<td>
Preferred dialog width preset (dialog mode only, ignored in
bottom sheet mode).
</td>
<td><code>medium</code></td>
<td>
<code>small</code>, <code>medium</code>, <code>large</code>,
<code>full</code>
</td>
</tr>
<tr>
<td><code>header-title</code></td>
<td>Header title text when no custom title slot is provided.</td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>header-subtitle</code></td>
<td>
Header subtitle text when no custom subtitle slot is provided.
</td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>header-subtitle-position</code></td>
<td>Position of the subtitle relative to the title.</td>
<td><code>below</code></td>
<td><code>above</code>, <code>below</code></td>
</tr>
<tr>
<td><code>aria-labelledby</code></td>
<td>
The ID of the element that labels the dialog (for
accessibility).
</td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>aria-describedby</code></td>
<td>
The ID of the element that describes the dialog (for
accessibility).
</td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>block-mode-change</code></td>
<td>
When set, the mode is determined at mount time based on the
current screen size, but subsequent mode changes are blocked.
Useful for preventing forms from resetting when the viewport
size changes.
</td>
<td><code>false</code></td>
<td><code>false</code>, <code>true</code></td>
</tr>
</tbody>
</table>
<h4>CSS custom properties</h4>
<table>
<thead>
<tr>
<th>CSS Property</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>--ha-dialog-surface-background</code></td>
<td>Dialog/sheet background color.</td>
</tr>
<tr>
<td><code>--ha-dialog-border-radius</code></td>
<td>Border radius of the dialog surface (dialog mode only).</td>
</tr>
<tr>
<td><code>--ha-dialog-show-duration</code></td>
<td>Show animation duration (dialog mode only).</td>
</tr>
<tr>
<td><code>--ha-dialog-hide-duration</code></td>
<td>Hide animation duration (dialog mode only).</td>
</tr>
</tbody>
</table>
<h4>Events</h4>
<table>
<thead>
<tr>
<th>Event</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>opened</code></td>
<td>
Fired when the adaptive dialog is shown (dialog mode only).
</td>
</tr>
<tr>
<td><code>closed</code></td>
<td>
Fired after the adaptive dialog is hidden (dialog mode only).
</td>
</tr>
<tr>
<td><code>after-show</code></td>
<td>Fired after show animation completes (dialog mode only).</td>
</tr>
</tbody>
</table>
<h3>Focus management</h3>
<p>
To automatically focus an element when the adaptive dialog opens, add
the
<code>autofocus</code> attribute to it. Components with
<code>delegatesFocus: true</code> (like <code>ha-form</code>) will
forward focus to their first focusable child.
</p>
<p>Example:</p>
<pre><code>&lt;ha-adaptive-dialog .hass=\${this.hass} open&gt;
&lt;ha-form autofocus .schema=\${schema}&gt;&lt;/ha-form&gt;
&lt;/ha-adaptive-dialog&gt;</code></pre>
</div>
`;
}
private _handleOpenDialog = (dialog: DialogType) => () => {
this._openDialog = dialog;
};
private _handleClosed = () => {
this._openDialog = false;
};
static styles = [
css`
:host {
display: block;
padding: var(--ha-space-4);
}
.content {
max-width: 1000px;
margin: 0 auto;
}
h1 {
margin-top: 0;
margin-bottom: var(--ha-space-2);
}
h2 {
margin-top: var(--ha-space-6);
margin-bottom: var(--ha-space-3);
}
h3,
h4 {
margin-top: var(--ha-space-4);
margin-bottom: var(--ha-space-2);
}
p {
margin: var(--ha-space-2) 0;
line-height: 1.6;
}
ul {
margin: var(--ha-space-2) 0;
padding-left: var(--ha-space-5);
}
li {
margin: var(--ha-space-1) 0;
line-height: 1.6;
}
.subtitle {
color: var(--secondary-text-color);
font-size: 1.1em;
margin-bottom: var(--ha-space-4);
}
table {
width: 100%;
border-collapse: collapse;
margin: var(--ha-space-3) 0;
}
th,
td {
text-align: left;
padding: var(--ha-space-2);
border-bottom: 1px solid var(--divider-color);
}
th {
font-weight: 500;
}
code {
background-color: var(--secondary-background-color);
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
font-size: 0.9em;
}
pre {
background-color: var(--secondary-background-color);
padding: var(--ha-space-3);
border-radius: 8px;
overflow-x: auto;
margin: var(--ha-space-3) 0;
}
pre code {
background-color: transparent;
padding: 0;
}
.buttons {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: var(--ha-space-2);
margin: var(--ha-space-4) 0;
}
.card-content {
padding: var(--ha-space-3);
}
a {
color: var(--primary-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-adaptive-dialog": DemoHaAdaptiveDialog;
}
}

View File

@@ -11,11 +11,11 @@ import { computeInitialHaFormData } from "../../../../src/components/ha-form/com
import "../../../../src/components/ha-form/ha-form"; import "../../../../src/components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../src/components/ha-form/types"; import type { HaFormSchema } from "../../../../src/components/ha-form/types";
import type { AreaRegistryEntry } from "../../../../src/data/area_registry"; import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
import { getEntity } from "../../../../src/fake_data/entity"; import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass"; import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../../src/types"; import type { HomeAssistant } from "../../../../src/types";
import "../../components/demo-black-white-row"; import "../../components/demo-black-white-row";
import type { DeviceRegistryEntry } from "../../../../src/data/device_registry";
const ENTITIES = [ const ENTITIES = [
getEntity("alarm_control_panel", "alarm", "disarmed", { getEntity("alarm_control_panel", "alarm", "disarmed", {

View File

@@ -13,9 +13,9 @@ import "../../../../src/components/ha-selector/ha-selector";
import "../../../../src/components/ha-settings-row"; import "../../../../src/components/ha-settings-row";
import type { AreaRegistryEntry } from "../../../../src/data/area_registry"; import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
import type { BlueprintInput } from "../../../../src/data/blueprint"; import type { BlueprintInput } from "../../../../src/data/blueprint";
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry"; import type { DeviceRegistryEntry } from "../../../../src/data/device_registry";
import type { FloorRegistryEntry } from "../../../../src/data/floor_registry"; import type { FloorRegistryEntry } from "../../../../src/data/floor_registry";
import type { LabelRegistryEntry } from "../../../../src/data/label/label_registry"; import type { LabelRegistryEntry } from "../../../../src/data/label_registry";
import { showDialog } from "../../../../src/dialogs/make-dialog-manager"; import { showDialog } from "../../../../src/dialogs/make-dialog-manager";
import { getEntity } from "../../../../src/fake_data/entity"; import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass"; import { provideHass } from "../../../../src/fake_data/provide_hass";

View File

@@ -139,7 +139,7 @@ export class DemoHaWaDialog extends LitElement {
</tr> </tr>
<tr> <tr>
<td><code>large</code></td> <td><code>large</code></td>
<td><code>min(1024px, var(--full-width))</code></td> <td><code>min(720px, var(--full-width))</code></td>
</tr> </tr>
<tr> <tr>
<td><code>full</code></td> <td><code>full</code></td>
@@ -381,6 +381,10 @@ export class DemoHaWaDialog extends LitElement {
<td><code>--dialog-z-index</code></td> <td><code>--dialog-z-index</code></td>
<td>Z-index for the dialog.</td> <td>Z-index for the dialog.</td>
</tr> </tr>
<tr>
<td><code>--dialog-surface-position</code></td>
<td>CSS position of the dialog surface.</td>
</tr>
<tr> <tr>
<td><code>--dialog-surface-margin-top</code></td> <td><code>--dialog-surface-margin-top</code></td>
<td>Top margin for the dialog surface.</td> <td>Top margin for the dialog surface.</td>

View File

@@ -6,8 +6,8 @@ import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import type { IntegrationManifest } from "../../../../src/data/integration"; import type { IntegrationManifest } from "../../../../src/data/integration";
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry"; import type { DeviceRegistryEntry } from "../../../../src/data/device_registry";
import type { EntityRegistryEntry } from "../../../../src/data/entity/entity_registry"; import type { EntityRegistryEntry } from "../../../../src/data/entity_registry";
import { provideHass } from "../../../../src/fake_data/provide_hass"; import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../../../src/panels/config/integrations/ha-config-flow-card"; import "../../../../src/panels/config/integrations/ha-config-flow-card";
import type { import type {

View File

@@ -52,7 +52,7 @@
"@fullcalendar/list": "6.1.19", "@fullcalendar/list": "6.1.19",
"@fullcalendar/luxon3": "6.1.19", "@fullcalendar/luxon3": "6.1.19",
"@fullcalendar/timegrid": "6.1.19", "@fullcalendar/timegrid": "6.1.19",
"@home-assistant/webawesome": "3.0.0-ha.2", "@home-assistant/webawesome": "3.0.0-ha.0",
"@lezer/highlight": "1.2.3", "@lezer/highlight": "1.2.3",
"@lit-labs/motion": "1.0.9", "@lit-labs/motion": "1.0.9",
"@lit-labs/observers": "2.0.6", "@lit-labs/observers": "2.0.6",
@@ -157,8 +157,8 @@
"@octokit/auth-oauth-device": "8.0.3", "@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.0.3", "@octokit/plugin-retry": "8.0.3",
"@octokit/rest": "22.0.1", "@octokit/rest": "22.0.1",
"@rsdoctor/rspack-plugin": "1.3.12", "@rsdoctor/rspack-plugin": "1.3.11",
"@rspack/core": "1.6.6", "@rspack/core": "1.6.5",
"@rspack/dev-server": "1.1.4", "@rspack/dev-server": "1.1.4",
"@types/babel__plugin-transform-runtime": "7.9.5", "@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.22", "@types/chromecast-caf-receiver": "6.0.22",
@@ -178,7 +178,7 @@
"@types/tar": "6.1.13", "@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39", "@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29", "@types/webspeechapi": "0.0.29",
"@vitest/coverage-v8": "4.0.15", "@vitest/coverage-v8": "4.0.14",
"babel-loader": "10.0.0", "babel-loader": "10.0.0",
"babel-plugin-template-html-minifier": "4.1.0", "babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3", "browserslist-useragent-regexp": "4.1.3",
@@ -209,17 +209,17 @@
"lodash.template": "4.5.0", "lodash.template": "4.5.0",
"map-stream": "0.0.7", "map-stream": "0.0.7",
"pinst": "3.0.0", "pinst": "3.0.0",
"prettier": "3.7.4", "prettier": "3.7.2",
"rspack-manifest-plugin": "5.2.0", "rspack-manifest-plugin": "5.2.0",
"serve": "14.2.5", "serve": "14.2.5",
"sinon": "21.0.0", "sinon": "21.0.0",
"tar": "7.5.2", "tar": "7.5.2",
"terser-webpack-plugin": "5.3.15", "terser-webpack-plugin": "5.3.14",
"ts-lit-plugin": "2.0.2", "ts-lit-plugin": "2.0.2",
"typescript": "5.9.3", "typescript": "5.9.3",
"typescript-eslint": "8.48.1", "typescript-eslint": "8.48.0",
"vite-tsconfig-paths": "5.1.4", "vite-tsconfig-paths": "5.1.4",
"vitest": "4.0.15", "vitest": "4.0.14",
"webpack-stats-plugin": "1.1.3", "webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0", "webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch" "workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20251203.0" version = "20251029.0"
license = "Apache-2.0" license = "Apache-2.0"
license-files = ["LICENSE*"] license-files = ["LICENSE*"]
description = "The Home Assistant frontend" description = "The Home Assistant frontend"

View File

@@ -3,8 +3,8 @@ import {
DOMAIN_ATTRIBUTES_FORMATERS, DOMAIN_ATTRIBUTES_FORMATERS,
DOMAIN_ATTRIBUTES_UNITS, DOMAIN_ATTRIBUTES_UNITS,
TEMPERATURE_ATTRIBUTES, TEMPERATURE_ATTRIBUTES,
} from "../../data/entity/entity_attributes"; } from "../../data/entity_attributes";
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry"; import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import type { FrontendLocaleData } from "../../data/translation"; import type { FrontendLocaleData } from "../../data/translation";
import type { WeatherEntity } from "../../data/weather"; import type { WeatherEntity } from "../../data/weather";
import { getWeatherUnit } from "../../data/weather"; import { getWeatherUnit } from "../../data/weather";

View File

@@ -1,12 +1,12 @@
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import type { DeviceRegistryEntry } from "../../data/device/device_registry"; import type { DeviceRegistryEntry } from "../../data/device_registry";
import type { import type {
EntityRegistryDisplayEntry, EntityRegistryDisplayEntry,
EntityRegistryEntry, EntityRegistryEntry,
} from "../../data/entity/entity_registry"; } from "../../data/entity_registry";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { getDuplicates } from "../string/get_duplicates";
import { computeStateName } from "./compute_state_name"; import { computeStateName } from "./compute_state_name";
import { getDuplicates } from "../string/get_duplicates";
export const computeDeviceName = ( export const computeDeviceName = (
device: DeviceRegistryEntry device: DeviceRegistryEntry

View File

@@ -2,7 +2,7 @@ import type { HassEntity } from "home-assistant-js-websocket";
import type { import type {
EntityRegistryDisplayEntry, EntityRegistryDisplayEntry,
EntityRegistryEntry, EntityRegistryEntry,
} from "../../data/entity/entity_registry"; } from "../../data/entity_registry";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { computeDeviceName } from "./compute_device_name"; import { computeDeviceName } from "./compute_device_name";
import { computeStateName } from "./compute_state_name"; import { computeStateName } from "./compute_state_name";

View File

@@ -1,12 +1,12 @@
import type { HassConfig, HassEntity } from "home-assistant-js-websocket"; import type { HassConfig, HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity"; import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry"; import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import type { FrontendLocaleData } from "../../data/translation"; import type { FrontendLocaleData } from "../../data/translation";
import { TimeZone } from "../../data/translation"; import { TimeZone } from "../../data/translation";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { DURATION_UNITS, formatDuration } from "../datetime/format_duration";
import { formatDate } from "../datetime/format_date"; import { formatDate } from "../datetime/format_date";
import { formatDateTime } from "../datetime/format_date_time"; import { formatDateTime } from "../datetime/format_date_time";
import { DURATION_UNITS, formatDuration } from "../datetime/format_duration";
import { formatTime } from "../datetime/format_time"; import { formatTime } from "../datetime/format_time";
import { import {
formatNumber, formatNumber,

View File

@@ -1,5 +1,5 @@
import type { AreaRegistryEntry } from "../../../data/area_registry"; import type { AreaRegistryEntry } from "../../../data/area_registry";
import type { DeviceRegistryEntry } from "../../../data/device/device_registry"; import type { DeviceRegistryEntry } from "../../../data/device_registry";
import type { FloorRegistryEntry } from "../../../data/floor_registry"; import type { FloorRegistryEntry } from "../../../data/floor_registry";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";

View File

@@ -1,11 +1,11 @@
import type { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import type { AreaRegistryEntry } from "../../../data/area_registry"; import type { AreaRegistryEntry } from "../../../data/area_registry";
import type { DeviceRegistryEntry } from "../../../data/device/device_registry"; import type { DeviceRegistryEntry } from "../../../data/device_registry";
import type { import type {
EntityRegistryDisplayEntry, EntityRegistryDisplayEntry,
EntityRegistryEntry, EntityRegistryEntry,
ExtEntityRegistryEntry, ExtEntityRegistryEntry,
} from "../../../data/entity/entity_registry"; } from "../../../data/entity_registry";
import type { FloorRegistryEntry } from "../../../data/floor_registry"; import type { FloorRegistryEntry } from "../../../data/floor_registry";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";

View File

@@ -1,14 +1,14 @@
import type { ConfigEntry } from "../../data/config_entries"; import type { HomeAssistant } from "../../types";
import { deleteConfigEntry } from "../../data/config_entries";
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
import { removeEntityRegistryEntry } from "../../data/entity/entity_registry";
import { HELPERS_CRUD } from "../../data/helpers_crud";
import type { IntegrationManifest } from "../../data/integration"; import type { IntegrationManifest } from "../../data/integration";
import { computeDomain } from "./compute_domain";
import { HELPERS_CRUD } from "../../data/helpers_crud";
import type { Helper } from "../../panels/config/helpers/const"; import type { Helper } from "../../panels/config/helpers/const";
import { isHelperDomain } from "../../panels/config/helpers/const"; import { isHelperDomain } from "../../panels/config/helpers/const";
import type { HomeAssistant } from "../../types";
import { isComponentLoaded } from "../config/is_component_loaded"; import { isComponentLoaded } from "../config/is_component_loaded";
import { computeDomain } from "./compute_domain"; import type { EntityRegistryEntry } from "../../data/entity_registry";
import { removeEntityRegistryEntry } from "../../data/entity_registry";
import type { ConfigEntry } from "../../data/config_entries";
import { deleteConfigEntry } from "../../data/config_entries";
export const isDeletableEntity = ( export const isDeletableEntity = (
hass: HomeAssistant, hass: HomeAssistant,

View File

@@ -1,9 +1,9 @@
import type { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE_STATES } from "../../data/entity/entity";
import type { HomeAssistant } from "../../types";
import { stringCompare } from "../string/compare";
import { computeDomain } from "./compute_domain";
import { computeStateDomain } from "./compute_state_domain"; import { computeStateDomain } from "./compute_state_domain";
import { UNAVAILABLE_STATES } from "../../data/entity";
import type { HomeAssistant } from "../../types";
import { computeDomain } from "./compute_domain";
import { stringCompare } from "../string/compare";
export const FIXED_DOMAIN_STATES = { export const FIXED_DOMAIN_STATES = {
alarm_control_panel: [ alarm_control_panel: [

View File

@@ -1,7 +1,7 @@
import type { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import { isUnavailableState, UNAVAILABLE } from "../../data/entity/entity";
import type { HomeAssistant } from "../../types";
import { computeStateDomain } from "./compute_state_domain"; import { computeStateDomain } from "./compute_state_domain";
import { isUnavailableState, UNAVAILABLE } from "../../data/entity";
import type { HomeAssistant } from "../../types";
export const computeGroupEntitiesState = (states: HassEntity[]): string => { export const computeGroupEntitiesState = (states: HassEntity[]): string => {
if (!states.length) { if (!states.length) {

View File

@@ -1,5 +1,5 @@
import type { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import { isUnavailableState, OFF, UNAVAILABLE } from "../../data/entity/entity"; import { isUnavailableState, OFF, UNAVAILABLE } from "../../data/entity";
import { computeDomain } from "./compute_domain"; import { computeDomain } from "./compute_domain";
export function stateActive(stateObj: HassEntity, state?: string): boolean { export function stateActive(stateObj: HassEntity, state?: string): boolean {

View File

@@ -1,5 +1,5 @@
import type { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE } from "../../data/entity/entity"; import { UNAVAILABLE } from "../../data/entity";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { DOMAINS_WITH_CARD } from "../const"; import { DOMAINS_WITH_CARD } from "../const";
import { canToggleState } from "./can_toggle_state"; import { canToggleState } from "./can_toggle_state";

View File

@@ -1,6 +1,6 @@
/** Return a color representing a state. */ /** Return a color representing a state. */
import type { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE } from "../../data/entity/entity"; import { UNAVAILABLE } from "../../data/entity";
import type { GroupEntity } from "../../data/group"; import type { GroupEntity } from "../../data/group";
import { computeGroupDomain } from "../../data/group"; import { computeGroupDomain } from "../../data/group";
import { computeCssVariable } from "../../resources/css-variables"; import { computeCssVariable } from "../../resources/css-variables";

View File

@@ -2,7 +2,7 @@ import type {
HassEntity, HassEntity,
HassEntityAttributeBase, HassEntityAttributeBase,
} from "home-assistant-js-websocket"; } from "home-assistant-js-websocket";
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry"; import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import type { FrontendLocaleData } from "../../data/translation"; import type { FrontendLocaleData } from "../../data/translation";
import { NumberFormat } from "../../data/translation"; import { NumberFormat } from "../../data/translation";
import { round } from "./round"; import { round } from "./round";

View File

@@ -373,7 +373,6 @@ export class StateHistoryChartTimeline extends LitElement {
itemName: 3, itemName: 3,
}, },
renderItem: this._renderItem, renderItem: this._renderItem,
progressive: 0,
}); });
}); });

View File

@@ -3,11 +3,11 @@ import { getGraphColorByIndex } from "../../common/color/colors";
import { hex2rgb, lab2hex, rgb2lab } from "../../common/color/convert-color"; import { hex2rgb, lab2hex, rgb2lab } from "../../common/color/convert-color";
import { labBrighten } from "../../common/color/lab"; import { labBrighten } from "../../common/color/lab";
import { computeDomain } from "../../common/entity/compute_domain"; import { computeDomain } from "../../common/entity/compute_domain";
import { stateColorProperties } from "../../common/entity/state_color";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { computeCssValue } from "../../resources/css-variables";
import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states"; import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
import { stateColorProperties } from "../../common/entity/state_color";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
import { computeCssValue } from "../../resources/css-variables";
const DOMAIN_STATE_SHADES: Record<string, Record<string, number>> = { const DOMAIN_STATE_SHADES: Record<string, Record<string, number>> = {
media_player: { media_player: {

View File

@@ -2,17 +2,15 @@ import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
import type { LabelRegistryEntry } from "../../data/label_registry";
import { computeCssColor } from "../../common/color/compute-color"; import { computeCssColor } from "../../common/color/compute-color";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { stringCompare } from "../../common/string/compare";
import type { LabelRegistryEntry } from "../../data/label/label_registry";
import "../chips/ha-chip-set";
import "../ha-dropdown";
import "../ha-dropdown-item";
import type { HaDropdownItem } from "../ha-dropdown-item";
import "../ha-icon";
import "../ha-label"; import "../ha-label";
import { stringCompare } from "../../common/string/compare";
import "../chips/ha-chip-set";
import "../ha-button-menu";
import "../ha-icon";
import "../ha-list-item";
@customElement("ha-data-table-labels") @customElement("ha-data-table-labels")
class HaDataTableLabels extends LitElement { class HaDataTableLabels extends LitElement {
@@ -28,11 +26,12 @@ class HaDataTableLabels extends LitElement {
(label) => this._renderLabel(label, true) (label) => this._renderLabel(label, true)
)} )}
${labels.length > 2 ${labels.length > 2
? html`<ha-dropdown ? html`<ha-button-menu
absolute
role="button" role="button"
tabindex="0" tabindex="0"
@click=${stopPropagation} @click=${this._handleIconOverflowMenuOpened}
@wa-select=${this._handleDropdownSelect} @closed=${this._handleIconOverflowMenuClosed}
> >
<ha-label slot="trigger" class="plus" dense> <ha-label slot="trigger" class="plus" dense>
+${labels.length - 2} +${labels.length - 2}
@@ -41,12 +40,12 @@ class HaDataTableLabels extends LitElement {
labels.slice(2), labels.slice(2),
(label) => label.label_id, (label) => label.label_id,
(label) => html` (label) => html`
<ha-dropdown-item .value=${label.label_id} .item=${label}> <ha-list-item @click=${this._labelClicked} .item=${label}>
${this._renderLabel(label, false)} ${this._renderLabel(label, false)}
</ha-dropdown-item> </ha-list-item>
` `
)} )}
</ha-dropdown>` </ha-button-menu>`
: nothing} : nothing}
</ha-chip-set> </ha-chip-set>
`; `;
@@ -82,12 +81,21 @@ class HaDataTableLabels extends LitElement {
fireEvent(this, "label-clicked", { label }); fireEvent(this, "label-clicked", { label });
} }
private _handleDropdownSelect( protected _handleIconOverflowMenuOpened(e) {
ev: CustomEvent<{ item: HaDropdownItem & { item?: LabelRegistryEntry } }> e.stopPropagation();
) { // If this component is used inside a data table, the z-index of the row
const label = ev.detail?.item?.item; // needs to be increased. Otherwise the ha-button-menu would be displayed
if (label) { // underneath the next row in the table.
fireEvent(this, "label-clicked", { label }); const row = this.closest(".mdc-data-table__row") as HTMLDivElement | null;
if (row) {
row.style.zIndex = "1";
}
}
protected _handleIconOverflowMenuClosed() {
const row = this.closest(".mdc-data-table__row") as HTMLDivElement | null;
if (row) {
row.style.zIndex = "";
} }
} }
@@ -106,6 +114,9 @@ class HaDataTableLabels extends LitElement {
--ha-label-background-color: var(--color, var(--grey-color)); --ha-label-background-color: var(--color, var(--grey-color));
--ha-label-background-opacity: 0.5; --ha-label-background-opacity: 0.5;
} }
ha-button-menu {
border-radius: 10px;
}
.plus { .plus {
--ha-label-background-color: transparent; --ha-label-background-color: transparent;
border: 1px solid var(--divider-color); border: 1px solid var(--divider-color);

View File

@@ -16,10 +16,8 @@ import memoizeOne from "memoize-one";
import { restoreScroll } from "../../common/decorators/restore-scroll"; import { restoreScroll } from "../../common/decorators/restore-scroll";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { stringCompare } from "../../common/string/compare"; import { stringCompare } from "../../common/string/compare";
import type { LocalizeFunc } from "../../common/translations/localize";
import { debounce } from "../../common/util/debounce"; import { debounce } from "../../common/util/debounce";
import { groupBy } from "../../common/util/group-by"; import { groupBy } from "../../common/util/group-by";
import { nextRender } from "../../common/util/render-status";
import { haStyleScrollbar } from "../../resources/styles"; import { haStyleScrollbar } from "../../resources/styles";
import { loadVirtualizer } from "../../resources/virtualizer"; import { loadVirtualizer } from "../../resources/virtualizer";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
@@ -28,6 +26,8 @@ import type { HaCheckbox } from "../ha-checkbox";
import "../ha-svg-icon"; import "../ha-svg-icon";
import "../search-input"; import "../search-input";
import { filterData, sortData } from "./sort-filter"; import { filterData, sortData } from "./sort-filter";
import type { LocalizeFunc } from "../../common/translations/localize";
import { nextRender } from "../../common/util/render-status";
export interface RowClickedEvent { export interface RowClickedEvent {
id: string; id: string;

View File

@@ -1,9 +1,9 @@
import { expose } from "comlink"; import { expose } from "comlink";
import Fuse, { type FuseOptionKey } from "fuse.js"; import Fuse from "fuse.js";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { ipCompare, stringCompare } from "../../common/string/compare"; import { ipCompare, stringCompare } from "../../common/string/compare";
import { stripDiacritics } from "../../common/string/strip-diacritics"; import { stripDiacritics } from "../../common/string/strip-diacritics";
import { multiTermSearch } from "../../resources/fuseMultiTerm"; import { HaFuse } from "../../resources/fuse";
import type { import type {
ClonedDataTableColumnData, ClonedDataTableColumnData,
DataTableRowData, DataTableRowData,
@@ -11,10 +11,9 @@ import type {
SortingDirection, SortingDirection,
} from "./ha-data-table"; } from "./ha-data-table";
const getSearchKeys = memoizeOne( const fuseIndex = memoizeOne(
(columns: SortableColumnContainer): FuseOptionKey<DataTableRowData>[] => { (data: DataTableRowData[], columns: SortableColumnContainer) => {
const searchKeys = new Set<string>(); const searchKeys = new Set<string>();
Object.entries(columns).forEach(([key, column]) => { Object.entries(columns).forEach(([key, column]) => {
if (column.filterable) { if (column.filterable) {
searchKeys.add( searchKeys.add(
@@ -24,15 +23,10 @@ const getSearchKeys = memoizeOne(
); );
} }
}); });
return Array.from(searchKeys); return Fuse.createIndex([...searchKeys], data);
} }
); );
const fuseIndex = memoizeOne(
(data: DataTableRowData[], keys: FuseOptionKey<DataTableRowData>[]) =>
Fuse.createIndex(keys, data)
);
const filterData = ( const filterData = (
data: DataTableRowData[], data: DataTableRowData[],
columns: SortableColumnContainer, columns: SortableColumnContainer,
@@ -44,13 +38,21 @@ const filterData = (
return data; return data;
} }
const keys = getSearchKeys(columns); const index = fuseIndex(data, columns);
const index = fuseIndex(data, keys); const fuse = new HaFuse(
data,
{ shouldSort: false, minMatchCharLength: 1 },
index
);
return multiTermSearch<DataTableRowData>(data, filter, keys, index, { const searchResults = fuse.multiTermsSearch(filter);
threshold: 0.2, // reduce fuzzy matches in data tables
}); if (searchResults) {
return searchResults.map((result) => result.item);
}
return data;
}; };
const sortData = ( const sortData = (

View File

@@ -1,9 +1,9 @@
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import type { DeviceAction } from "../../data/device/device_automation"; import type { DeviceAction } from "../../data/device_automation";
import { import {
fetchDeviceActions, fetchDeviceActions,
localizeDeviceAutomationAction, localizeDeviceAutomationAction,
} from "../../data/device/device_automation"; } from "../../data/device_automation";
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker"; import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
@customElement("ha-device-action-picker") @customElement("ha-device-action-picker")

View File

@@ -2,17 +2,17 @@ import { consume } from "@lit/context";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators"; import { property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { fullEntitiesContext } from "../../data/context"; import { fullEntitiesContext } from "../../data/context";
import type { DeviceAutomation } from "../../data/device/device_automation"; import type { DeviceAutomation } from "../../data/device_automation";
import { import {
deviceAutomationsEqual, deviceAutomationsEqual,
sortDeviceAutomations, sortDeviceAutomations,
} from "../../data/device/device_automation"; } from "../../data/device_automation";
import type { EntityRegistryEntry } from "../../data/entity/entity_registry"; import type { EntityRegistryEntry } from "../../data/entity_registry";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import "../ha-md-select";
import "../ha-md-select-option"; import "../ha-md-select-option";
import "../ha-md-select";
import { stopPropagation } from "../../common/dom/stop_propagation";
const NO_AUTOMATION_KEY = "NO_AUTOMATION"; const NO_AUTOMATION_KEY = "NO_AUTOMATION";
const UNKNOWN_AUTOMATION_KEY = "UNKNOWN_AUTOMATION"; const UNKNOWN_AUTOMATION_KEY = "UNKNOWN_AUTOMATION";

View File

@@ -1,9 +1,9 @@
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import type { DeviceCondition } from "../../data/device/device_automation"; import type { DeviceCondition } from "../../data/device_automation";
import { import {
fetchDeviceConditions, fetchDeviceConditions,
localizeDeviceAutomationCondition, localizeDeviceAutomationCondition,
} from "../../data/device/device_automation"; } from "../../data/device_automation";
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker"; import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
@customElement("ha-device-condition-picker") @customElement("ha-device-condition-picker")

View File

@@ -9,11 +9,10 @@ import { computeDeviceName } from "../../common/entity/compute_device_name";
import { getDeviceContext } from "../../common/entity/context/get_device_context"; import { getDeviceContext } from "../../common/entity/context/get_device_context";
import { getConfigEntries, type ConfigEntry } from "../../data/config_entries"; import { getConfigEntries, type ConfigEntry } from "../../data/config_entries";
import { import {
deviceComboBoxKeys,
getDevices, getDevices,
type DevicePickerItem, type DevicePickerItem,
} from "../../data/device/device_picker"; type DeviceRegistryEntry,
import type { DeviceRegistryEntry } from "../../data/device/device_registry"; } from "../../data/device_registry";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url"; import { brandsUrl } from "../../util/brands-url";
import "../ha-generic-picker"; import "../ha-generic-picker";
@@ -217,10 +216,6 @@ export class HaDevicePicker extends LitElement {
.getItems=${this._getItems} .getItems=${this._getItems}
.hideClearIcon=${this.hideClearIcon} .hideClearIcon=${this.hideClearIcon}
.valueRenderer=${valueRenderer} .valueRenderer=${valueRenderer}
.searchKeys=${deviceComboBoxKeys}
.unknownItemText=${this.hass.localize(
"ui.components.device-picker.unknown"
)}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
> >
</ha-generic-picker> </ha-generic-picker>

View File

@@ -1,9 +1,9 @@
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import type { DeviceTrigger } from "../../data/device/device_automation"; import type { DeviceTrigger } from "../../data/device_automation";
import { import {
fetchDeviceTriggers, fetchDeviceTriggers,
localizeDeviceAutomationTrigger, localizeDeviceAutomationTrigger,
} from "../../data/device/device_automation"; } from "../../data/device_automation";
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker"; import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
@customElement("ha-device-trigger-picker") @customElement("ha-device-trigger-picker")

View File

@@ -4,7 +4,7 @@ import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { isValidEntityId } from "../../common/entity/valid_entity_id"; import { isValidEntityId } from "../../common/entity/valid_entity_id";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity"; import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
import type { HomeAssistant, ValueChangedEvent } from "../../types"; import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-sortable"; import "../ha-sortable";
import "./ha-entity-picker"; import "./ha-entity-picker";

View File

@@ -7,12 +7,11 @@ import { fireEvent } from "../../common/dom/fire_event";
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display"; import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
import { isValidEntityId } from "../../common/entity/valid_entity_id"; import { isValidEntityId } from "../../common/entity/valid_entity_id";
import { computeRTL } from "../../common/util/compute_rtl"; import { computeRTL } from "../../common/util/compute_rtl";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity"; import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
import { import {
entityComboBoxKeys,
getEntities, getEntities,
type EntityComboBoxItem, type EntityComboBoxItem,
} from "../../data/entity/entity_picker"; } from "../../data/entity_registry";
import { domainToName } from "../../data/integration"; import { domainToName } from "../../data/integration";
import { import {
isHelperDomain, isHelperDomain,
@@ -289,14 +288,10 @@ export class HaEntityPicker extends LitElement {
.hideClearIcon=${this.hideClearIcon} .hideClearIcon=${this.hideClearIcon}
.searchFn=${this._searchFn} .searchFn=${this._searchFn}
.valueRenderer=${this._valueRenderer} .valueRenderer=${this._valueRenderer}
.searchKeys=${entityComboBoxKeys} @value-changed=${this._valueChanged}
.addButtonLabel=${this.addButton .addButtonLabel=${this.addButton
? this.hass.localize("ui.components.entity.entity-picker.add") ? this.hass.localize("ui.components.entity.entity-picker.add")
: undefined} : undefined}
.unknownItemText=${this.hass.localize(
"ui.components.entity.entity-picker.unknown"
)}
@value-changed=${this._valueChanged}
> >
</ha-generic-picker> </ha-generic-picker>
`; `;

View File

@@ -6,11 +6,7 @@ import { customElement, property, state } from "lit/decorators";
import { STATES_OFF } from "../../common/const"; import { STATES_OFF } from "../../common/const";
import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name"; import { computeStateName } from "../../common/entity/compute_state_name";
import { import { UNAVAILABLE, UNKNOWN, isUnavailableState } from "../../data/entity";
UNAVAILABLE,
UNKNOWN,
isUnavailableState,
} from "../../data/entity/entity";
import { forwardHaptic } from "../../data/haptics"; import { forwardHaptic } from "../../data/haptics";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import "../ha-formfield"; import "../ha-formfield";

View File

@@ -14,12 +14,8 @@ import {
getNumberFormatOptions, getNumberFormatOptions,
isNumericState, isNumericState,
} from "../../common/number/format_number"; } from "../../common/number/format_number";
import { import { isUnavailableState, UNAVAILABLE, UNKNOWN } from "../../data/entity";
isUnavailableState, import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
UNAVAILABLE,
UNKNOWN,
} from "../../data/entity/entity";
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
import { timerTimeRemaining } from "../../data/timer"; import { timerTimeRemaining } from "../../data/timer";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import "../ha-label-badge"; import "../ha-label-badge";

View File

@@ -38,21 +38,9 @@ type StatisticItemType = "entity" | "external" | "no_state";
interface StatisticComboBoxItem extends PickerComboBoxItem { interface StatisticComboBoxItem extends PickerComboBoxItem {
statistic_id?: string; statistic_id?: string;
stateObj?: HassEntity; stateObj?: HassEntity;
domainName?: string;
type?: StatisticItemType; type?: StatisticItemType;
} }
const SEARCH_KEYS = [
{ name: "label", weight: 10 },
{ name: "search_labels.entityName", weight: 10 },
{ name: "search_labels.friendlyName", weight: 9 },
{ name: "search_labels.deviceName", weight: 8 },
{ name: "search_labels.areaName", weight: 6 },
{ name: "search_labels.domainName", weight: 4 },
{ name: "statisticId", weight: 3 },
{ name: "id", weight: 2 },
];
@customElement("ha-statistic-picker") @customElement("ha-statistic-picker")
export class HaStatisticPicker extends LitElement { export class HaStatisticPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -245,6 +233,7 @@ export class HaStatisticPicker extends LitElement {
), ),
type, type,
sorting_label: [sortingPrefix, label].join("_"), sorting_label: [sortingPrefix, label].join("_"),
search_labels: [label, id],
icon_path: mdiShape, icon_path: mdiShape,
}); });
} else if (type === "external") { } else if (type === "external") {
@@ -257,7 +246,7 @@ export class HaStatisticPicker extends LitElement {
secondary: domainName, secondary: domainName,
type, type,
sorting_label: [sortingPrefix, label].join("_"), sorting_label: [sortingPrefix, label].join("_"),
search_labels: { label, domainName }, search_labels: [label, domainName, id],
icon_path: mdiChartLine, icon_path: mdiChartLine,
}); });
} }
@@ -291,12 +280,13 @@ export class HaStatisticPicker extends LitElement {
stateObj: stateObj, stateObj: stateObj,
type: "entity", type: "entity",
sorting_label: [sortingPrefix, deviceName, entityName].join("_"), sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
search_labels: { search_labels: [
entityName: entityName || null, entityName,
deviceName: deviceName || null, deviceName,
areaName: areaName || null, areaName,
friendlyName, friendlyName,
}, id,
].filter(Boolean) as string[],
}); });
}); });
@@ -371,13 +361,13 @@ export class HaStatisticPicker extends LitElement {
stateObj: stateObj, stateObj: stateObj,
type: "entity", type: "entity",
sorting_label: [sortingPrefix, deviceName, entityName].join("_"), sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
search_labels: { search_labels: [
entityName: entityName || null, entityName,
deviceName: deviceName || null, deviceName,
areaName: areaName || null, areaName,
friendlyName, friendlyName,
statisticId, statisticId,
}, ].filter(Boolean) as string[],
}; };
} }
@@ -404,7 +394,7 @@ export class HaStatisticPicker extends LitElement {
secondary: domainName, secondary: domainName,
type: "external", type: "external",
sorting_label: [sortingPrefix, label].join("_"), sorting_label: [sortingPrefix, label].join("_"),
search_labels: { label, domainName, statisticId }, search_labels: [label, domainName, statisticId],
icon_path: mdiChartLine, icon_path: mdiChartLine,
}; };
} }
@@ -419,7 +409,7 @@ export class HaStatisticPicker extends LitElement {
secondary: this.hass.localize("ui.components.statistic-picker.no_state"), secondary: this.hass.localize("ui.components.statistic-picker.no_state"),
type: "no_state", type: "no_state",
sorting_label: [sortingPrefix, label].join("_"), sorting_label: [sortingPrefix, label].join("_"),
search_labels: { label, statisticId }, search_labels: [label, statisticId],
icon_path: mdiShape, icon_path: mdiShape,
}; };
} }
@@ -485,10 +475,6 @@ export class HaStatisticPicker extends LitElement {
.searchFn=${this._searchFn} .searchFn=${this._searchFn}
.valueRenderer=${this._valueRenderer} .valueRenderer=${this._valueRenderer}
.helper=${this.helper} .helper=${this.helper}
.searchKeys=${SEARCH_KEYS}
.unknownItemText=${this.hass.localize(
"ui.components.statistic-picker.unknown"
)}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
> >
</ha-generic-picker> </ha-generic-picker>

View File

@@ -1,188 +0,0 @@
import { mdiClose } from "@mdi/js";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import type { HomeAssistant } from "../types";
import { listenMediaQuery } from "../common/dom/media_query";
import "./ha-bottom-sheet";
import "./ha-dialog-header";
import "./ha-icon-button";
import "./ha-wa-dialog";
import type { DialogWidth } from "./ha-wa-dialog";
type DialogSheetMode = "dialog" | "bottom-sheet";
/**
* Home Assistant adaptive dialog component
*
* @element ha-adaptive-dialog
* @extends {LitElement}
*
* @summary
* A responsive dialog component that automatically switches between a full dialog (ha-wa-dialog)
* and a bottom sheet (ha-bottom-sheet) based on screen size. Uses dialog mode on larger screens
* (>870px width and >500px height) and bottom sheet mode on smaller screens or mobile devices.
*
* @slot headerNavigationIcon - Leading header action (e.g. close/back button).
* @slot headerTitle - Custom title content (used when header-title is not set).
* @slot headerSubtitle - Custom subtitle content (used when header-subtitle is not set).
* @slot headerActionItems - Trailing header actions (e.g. buttons, menus).
* @slot - Dialog/sheet content body.
* @slot footer - Dialog/sheet footer content.
*
* @cssprop --ha-dialog-surface-background - Dialog/sheet background color.
* @cssprop --ha-dialog-border-radius - Border radius of the dialog surface (dialog mode only).
* @cssprop --ha-dialog-show-duration - Show animation duration (dialog mode only).
* @cssprop --ha-dialog-hide-duration - Hide animation duration (dialog mode only).
*
* @attr {boolean} open - Controls the dialog/sheet open state.
* @attr {("small"|"medium"|"large"|"full")} width - Preferred dialog width preset (dialog mode only). Defaults to "medium".
* @attr {string} header-title - Header title text. If not set, the headerTitle slot is used.
* @attr {string} header-subtitle - Header subtitle text. If not set, the headerSubtitle slot is used.
* @attr {("above"|"below")} header-subtitle-position - Position of the subtitle relative to the title. Defaults to "below".
* @attr {boolean} block-mode-change - When set, the mode is determined at mount time based on the current screen size, but subsequent mode changes are blocked. Useful for preventing forms from resetting when the viewport size changes.
*
* @event opened - Fired when the dialog/sheet is shown (dialog mode only).
* @event closed - Fired after the dialog/sheet is hidden.
* @event after-show - Fired after show animation completes (dialog mode only).
*
* @remarks
* **Responsive Behavior:**
* The component automatically switches between dialog and bottom sheet modes based on viewport size.
* Dialog mode is used for screens wider than 870px and taller than 500px.
* Bottom sheet mode is used for mobile devices and smaller screens.
*
* When `block-mode-change` is set, the mode is determined once at mount time based on the initial
* screen size. Subsequent viewport size changes will not trigger mode switches, which is useful
* for preventing form resets or other state loss when users resize their browser window.
*
* **Focus Management:**
* To automatically focus an element when opened, add the `autofocus` attribute to it.
* Components with `delegatesFocus: true` (like `ha-form`) will forward focus to their first focusable child.
* Example: `<ha-form autofocus .schema=${schema}></ha-form>`
*/
@customElement("ha-adaptive-dialog")
export class HaAdaptiveDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "aria-labelledby" })
public ariaLabelledBy?: string;
@property({ attribute: "aria-describedby" })
public ariaDescribedBy?: string;
@property({ type: Boolean, reflect: true })
public open = false;
@property({ type: String, reflect: true, attribute: "width" })
public width: DialogWidth = "medium";
@property({ attribute: "header-title" })
public headerTitle?: string;
@property({ attribute: "header-subtitle" })
public headerSubtitle?: string;
@property({ type: String, attribute: "header-subtitle-position" })
public headerSubtitlePosition: "above" | "below" = "below";
@property({ type: Boolean, attribute: "block-mode-change" })
public blockModeChange = false;
@state() private _mode: DialogSheetMode = "dialog";
private _unsubMediaQuery?: () => void;
private _modeSet = false;
connectedCallback() {
super.connectedCallback();
this._unsubMediaQuery = listenMediaQuery(
"(max-width: 870px), (max-height: 500px)",
(matches) => {
if (!this._modeSet || !this.blockModeChange) {
this._mode = matches ? "bottom-sheet" : "dialog";
this._modeSet = true;
}
}
);
}
disconnectedCallback() {
super.disconnectedCallback();
this._unsubMediaQuery?.();
this._unsubMediaQuery = undefined;
this._modeSet = false;
}
render() {
if (this._mode === "bottom-sheet") {
return html`
<ha-bottom-sheet .open=${this.open} flexcontent>
<ha-dialog-header
slot="header"
.subtitlePosition=${this.headerSubtitlePosition}
>
<slot name="headerNavigationIcon" slot="navigationIcon">
<ha-icon-button
data-drawer="close"
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
.path=${mdiClose}
></ha-icon-button>
</slot>
${this.headerTitle !== undefined
? html`<span slot="title" class="title" id="ha-wa-dialog-title">
${this.headerTitle}
</span>`
: html`<slot name="headerTitle" slot="title"></slot>`}
${this.headerSubtitle !== undefined
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
<slot name="headerActionItems" slot="actionItems"></slot>
</ha-dialog-header>
<slot></slot>
<slot name="footer" slot="footer"></slot>
</ha-bottom-sheet>
`;
}
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this.open}
.width=${this.width}
.ariaLabelledBy=${this.ariaLabelledBy}
.ariaDescribedBy=${this.ariaDescribedBy}
.headerTitle=${this.headerTitle}
.headerSubtitle=${this.headerSubtitle}
.headerSubtitlePosition=${this.headerSubtitlePosition}
flexcontent
>
<slot name="headerNavigationIcon" slot="headerNavigationIcon"></slot>
<slot name="headerTitle" slot="headerTitle"></slot>
<slot name="headerSubtitle" slot="headerSubtitle"></slot>
<slot name="headerActionItems" slot="headerActionItems"></slot>
<slot></slot>
<slot name="footer" slot="footer"></slot>
</ha-wa-dialog>
`;
}
static get styles() {
return [
css`
ha-bottom-sheet {
--ha-bottom-sheet-surface-background: var(
--ha-dialog-surface-background,
var(--card-background-color, var(--ha-color-surface-default))
);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-adaptive-dialog": HaAdaptiveDialog;
}
}

View File

@@ -0,0 +1,270 @@
import { mdiTextureBox } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { HassEntity } from "home-assistant-js-websocket";
import type { TemplateResult } from "lit";
import { LitElement, html, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { computeAreaName } from "../common/entity/compute_area_name";
import { computeFloorName } from "../common/entity/compute_floor_name";
import { computeRTL } from "../common/util/compute_rtl";
import {
getAreasAndFloors,
type AreaFloorValue,
type FloorComboBoxItem,
} from "../data/area_floor";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-combo-box-item";
import "./ha-floor-icon";
import "./ha-generic-picker";
import type { HaGenericPicker } from "./ha-generic-picker";
import "./ha-icon-button";
import type { PickerValueRenderer } from "./ha-picker-field";
import "./ha-svg-icon";
import "./ha-tree-indicator";
const SEPARATOR = "________";
@customElement("ha-area-floor-picker")
export class HaAreaFloorPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@property({ attribute: false }) public value?: AreaFloorValue;
@property() public helper?: string;
@property() public placeholder?: string;
@property({ type: String, attribute: "search-label" })
public searchLabel?: string;
/**
* Show only areas with entities from specific domains.
* @type {Array}
* @attr include-domains
*/
@property({ type: Array, attribute: "include-domains" })
public includeDomains?: string[];
/**
* Show no areas with entities of these domains.
* @type {Array}
* @attr exclude-domains
*/
@property({ type: Array, attribute: "exclude-domains" })
public excludeDomains?: string[];
/**
* Show only areas with entities of these device classes.
* @type {Array}
* @attr include-device-classes
*/
@property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
/**
* List of areas to be excluded.
* @type {Array}
* @attr exclude-areas
*/
@property({ type: Array, attribute: "exclude-areas" })
public excludeAreas?: string[];
/**
* List of floors to be excluded.
* @type {Array}
* @attr exclude-floors
*/
@property({ type: Array, attribute: "exclude-floors" })
public excludeFloors?: string[];
@property({ attribute: false })
public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property({ attribute: false })
public entityFilter?: (entity: HassEntity) => boolean;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@query("ha-generic-picker") private _picker?: HaGenericPicker;
public async open() {
await this.updateComplete;
await this._picker?.open();
}
private _valueRenderer: PickerValueRenderer = (value: string) => {
const item = this._parseValue(value);
const area = item.type === "area" && this.hass.areas[value];
if (area) {
const areaName = computeAreaName(area);
return html`
${area.icon
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon
slot="start"
.path=${mdiTextureBox}
></ha-svg-icon>`}
<slot name="headline">${areaName}</slot>
`;
}
const floor = item.type === "floor" && this.hass.floors[value];
if (floor) {
const floorName = computeFloorName(floor);
return html`
<ha-floor-icon slot="start" .floor=${floor}></ha-floor-icon>
<span slot="headline">${floorName}</span>
`;
}
return html`
<ha-svg-icon slot="start" .path=${mdiTextureBox}></ha-svg-icon>
<span slot="headline">${value}</span>
`;
};
private _rowRenderer: ComboBoxLitRenderer<FloorComboBoxItem> = (
item,
{ index },
combobox
) => {
const nextItem = combobox.filteredItems?.[index + 1];
const isLastArea =
!nextItem ||
nextItem.type === "floor" ||
(nextItem.type === "area" && !nextItem.area?.floor_id);
const rtl = computeRTL(this.hass);
const hasFloor = item.type === "area" && item.area?.floor_id;
return html`
<ha-combo-box-item
type="button"
style=${item.type === "area" && hasFloor
? "--md-list-item-leading-space: 48px;"
: ""}
>
${item.type === "area" && hasFloor
? html`
<ha-tree-indicator
style=${styleMap({
width: "48px",
position: "absolute",
top: "0px",
left: rtl ? undefined : "4px",
right: rtl ? "4px" : undefined,
transform: rtl ? "scaleX(-1)" : "",
})}
.end=${isLastArea}
slot="start"
></ha-tree-indicator>
`
: nothing}
${item.type === "floor" && item.floor
? html`<ha-floor-icon
slot="start"
.floor=${item.floor}
></ha-floor-icon>`
: item.icon
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
: html`<ha-svg-icon
slot="start"
.path=${item.icon_path || mdiTextureBox}
></ha-svg-icon>`}
${item.primary}
</ha-combo-box-item>
`;
};
private _getAreasAndFloorsMemoized = memoizeOne(getAreasAndFloors);
private _getItems = () =>
this._getAreasAndFloorsMemoized(
this.hass.states,
this.hass.floors,
this.hass.areas,
this.hass.devices,
this.hass.entities,
this._formatValue,
this.includeDomains,
this.excludeDomains,
this.includeDeviceClasses,
this.deviceFilter,
this.entityFilter,
this.excludeAreas,
this.excludeFloors
);
private _formatValue = memoizeOne((value: AreaFloorValue): string =>
[value.type, value.id].join(SEPARATOR)
);
private _parseValue = memoizeOne((value: string): AreaFloorValue => {
const [type, id] = value.split(SEPARATOR);
return { id, type: type as "floor" | "area" };
});
protected render(): TemplateResult {
const placeholder =
this.placeholder ?? this.hass.localize("ui.components.area-picker.area");
const value = this.value ? this._formatValue(this.value) : undefined;
return html`
<ha-generic-picker
.hass=${this.hass}
.autofocus=${this.autofocus}
.label=${this.label}
.searchLabel=${this.searchLabel}
.notFoundLabel=${this.hass.localize(
"ui.components.area-picker.no_match"
)}
.placeholder=${placeholder}
.value=${value}
.getItems=${this._getItems}
.valueRenderer=${this._valueRenderer}
.rowRenderer=${this._rowRenderer}
@value-changed=${this._valueChanged}
>
</ha-generic-picker>
`;
}
private _valueChanged(ev: ValueChangedEvent<string>) {
ev.stopPropagation();
const value = ev.detail.value;
if (!value) {
this._setValue(undefined);
return;
}
const selected = this._parseValue(value);
this._setValue(selected);
}
private _setValue(value?: AreaFloorValue) {
this.value = value;
fireEvent(this, "value-changed", { value });
fireEvent(this, "change");
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-area-floor-picker": HaAreaFloorPicker;
}
}

View File

@@ -13,9 +13,9 @@ import { createAreaRegistryEntry } from "../data/area_registry";
import type { import type {
DeviceEntityDisplayLookup, DeviceEntityDisplayLookup,
DeviceRegistryEntry, DeviceRegistryEntry,
} from "../data/device/device_registry"; } from "../data/device_registry";
import { getDeviceEntityDisplayLookup } from "../data/device/device_registry"; import { getDeviceEntityDisplayLookup } from "../data/device_registry";
import type { EntityRegistryDisplayEntry } from "../data/entity/entity_registry"; import type { EntityRegistryDisplayEntry } from "../data/entity_registry";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box"; import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import { showAreaRegistryDetailDialog } from "../panels/config/areas/show-dialog-area-registry-detail"; import { showAreaRegistryDetailDialog } from "../panels/config/areas/show-dialog-area-registry-detail";
import type { HomeAssistant, ValueChangedEvent } from "../types"; import type { HomeAssistant, ValueChangedEvent } from "../types";
@@ -30,12 +30,6 @@ import "./ha-svg-icon";
const ADD_NEW_ID = "___ADD_NEW___"; const ADD_NEW_ID = "___ADD_NEW___";
const SEARCH_KEYS = [
{ name: "areaName", weight: 10 },
{ name: "aliases", weight: 8 },
{ name: "floorName", weight: 6 },
{ name: "id", weight: 3 },
];
@customElement("ha-area-picker") @customElement("ha-area-picker")
export class HaAreaPicker extends LitElement { export class HaAreaPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -296,12 +290,13 @@ export class HaAreaPicker extends LitElement {
secondary: floorName, secondary: floorName,
icon: area.icon || undefined, icon: area.icon || undefined,
icon_path: area.icon ? undefined : mdiTextureBox, icon_path: area.icon ? undefined : mdiTextureBox,
search_labels: { sorting_label: areaName,
areaName: areaName || null, search_labels: [
floorName: floorName || null, areaName,
id: area.area_id, floorName,
aliases: area.aliases.join(" "), area.area_id,
}, ...area.aliases,
].filter((v): v is string => Boolean(v)),
}; };
}); });
@@ -384,10 +379,6 @@ export class HaAreaPicker extends LitElement {
.getAdditionalItems=${this._getAdditionalItems} .getAdditionalItems=${this._getAdditionalItems}
.valueRenderer=${valueRenderer} .valueRenderer=${valueRenderer}
.addButtonLabel=${this.addButtonLabel} .addButtonLabel=${this.addButtonLabel}
.searchKeys=${SEARCH_KEYS}
.unknownItemText=${this.hass.localize(
"ui.components.area-picker.unknown"
)}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
> >
</ha-generic-picker> </ha-generic-picker>

View File

@@ -4,6 +4,7 @@ import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { getAreaContext } from "../common/entity/context/get_area_context"; import { getAreaContext } from "../common/entity/context/get_area_context";
import { areaCompare } from "../data/area_registry";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "./ha-expansion-panel"; import "./ha-expansion-panel";
import "./ha-items-display-editor"; import "./ha-items-display-editor";
@@ -36,7 +37,11 @@ export class HaAreasDisplayEditor extends LitElement {
public showNavigationButton = false; public showNavigationButton = false;
protected render(): TemplateResult { protected render(): TemplateResult {
const areas = Object.values(this.hass.areas); const compare = areaCompare(this.hass.areas);
const areas = Object.values(this.hass.areas).sort((areaA, areaB) =>
compare(areaA.area_id, areaB.area_id)
);
const items: DisplayItem[] = areas.map((area) => { const items: DisplayItem[] = areas.map((area) => {
const { floor } = getAreaContext(area, this.hass.floors); const { floor } = getAreaContext(area, this.hass.floors);

View File

@@ -7,6 +7,7 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { computeFloorName } from "../common/entity/compute_floor_name"; import { computeFloorName } from "../common/entity/compute_floor_name";
import { getAreaContext } from "../common/entity/context/get_area_context"; import { getAreaContext } from "../common/entity/context/get_area_context";
import { areaCompare } from "../data/area_registry";
import type { FloorRegistryEntry } from "../data/floor_registry"; import type { FloorRegistryEntry } from "../data/floor_registry";
import { getFloors } from "../panels/lovelace/strategies/areas/helpers/areas-strategy-helper"; import { getFloors } from "../panels/lovelace/strategies/areas/helpers/areas-strategy-helper";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
@@ -130,8 +131,11 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
// update items if floors change // update items if floors change
_hassFloors: HomeAssistant["floors"] _hassFloors: HomeAssistant["floors"]
): Record<string, DisplayItem[]> => { ): Record<string, DisplayItem[]> => {
const areas = Object.values(hassAreas); const compare = areaCompare(hassAreas);
const areas = Object.values(hassAreas).sort((areaA, areaB) =>
compare(areaA.area_id, areaB.area_id)
);
const groupedItems: Record<string, DisplayItem[]> = areas.reduce( const groupedItems: Record<string, DisplayItem[]> = areas.reduce(
(acc, area) => { (acc, area) => {
const { floor } = getAreaContext(area, this.hass.floors); const { floor } = getAreaContext(area, this.hass.floors);

View File

@@ -134,7 +134,6 @@ export class HaAssistChat extends LitElement {
})}" })}"
breaks breaks
cache cache
assist
.content=${message.text} .content=${message.text}
> >
</ha-markdown> </ha-markdown>

View File

@@ -3,15 +3,15 @@ import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display"; import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { import {
STATE_ATTRIBUTES, STATE_ATTRIBUTES,
STATE_ATTRIBUTES_DOMAIN_CLASS, STATE_ATTRIBUTES_DOMAIN_CLASS,
} from "../data/entity/entity_attributes"; } from "../data/entity_attributes";
import { haStyle } from "../resources/styles"; import { haStyle } from "../resources/styles";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "./ha-attribute-value"; import "./ha-attribute-value";
import "./ha-expansion-panel"; import "./ha-expansion-panel";
import { computeStateDomain } from "../common/entity/compute_state_domain";
@customElement("ha-attributes") @customElement("ha-attributes")
class HaAttributes extends LitElement { class HaAttributes extends LitElement {

View File

@@ -52,10 +52,8 @@ export class HaAutomationRow extends LitElement {
<slot name="leading-icon"></slot> <slot name="leading-icon"></slot>
</div> </div>
<slot class="header" name="header"></slot> <slot class="header" name="header"></slot>
<div class="icons">
<slot name="icons"></slot> <slot name="icons"></slot>
</div> </div>
</div>
`; `;
} }
@@ -120,9 +118,9 @@ export class HaAutomationRow extends LitElement {
} }
.row { .row {
display: flex; display: flex;
padding: 0 var(--ha-space-3); padding: var(--ha-space-0) var(--ha-space-2);
min-height: 48px; min-height: 48px;
align-items: flex-start; align-items: center;
cursor: pointer; cursor: pointer;
overflow: hidden; overflow: hidden;
font-weight: var(--ha-font-weight-medium); font-weight: var(--ha-font-weight-medium);
@@ -147,9 +145,6 @@ export class HaAutomationRow extends LitElement {
align-items: center; align-items: center;
transform: rotate(45deg); transform: rotate(45deg);
} }
.leading-icon-wrapper {
padding-top: var(--ha-space-3);
}
::slotted([slot="leading-icon"]) { ::slotted([slot="leading-icon"]) {
color: var(--ha-color-on-neutral-quiet); color: var(--ha-color-on-neutral-quiet);
} }
@@ -177,10 +172,6 @@ export class HaAutomationRow extends LitElement {
overflow-wrap: anywhere; overflow-wrap: anywhere;
margin: var(--ha-space-0) var(--ha-space-3); margin: var(--ha-space-0) var(--ha-space-3);
} }
.icons {
display: flex;
align-items: center;
}
:host([sort-selected]) .row { :host([sort-selected]) .row {
outline: solid; outline: solid;
outline-color: rgba(var(--rgb-accent-color), 0.6); outline-color: rgba(var(--rgb-accent-color), 0.6);

View File

@@ -2,13 +2,12 @@ import "@home-assistant/webawesome/dist/components/drawer/drawer";
import { css, html, LitElement, type PropertyValues } from "lit"; import { css, html, LitElement, type PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { SwipeGestureRecognizer } from "../common/util/swipe-gesture-recognizer"; import { SwipeGestureRecognizer } from "../common/util/swipe-gesture-recognizer";
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300; export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300;
@customElement("ha-bottom-sheet") @customElement("ha-bottom-sheet")
export class HaBottomSheet extends ScrollableFadeMixin(LitElement) { export class HaBottomSheet extends LitElement {
@property({ type: Boolean }) public open = false; @property({ type: Boolean }) public open = false;
@property({ type: Boolean, reflect: true, attribute: "flexcontent" }) @property({ type: Boolean, reflect: true, attribute: "flexcontent" })
@@ -18,12 +17,6 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
@query("#drawer") private _drawer!: HTMLElement; @query("#drawer") private _drawer!: HTMLElement;
@query("#body") private _bodyElement!: HTMLDivElement;
protected get scrollableElement(): HTMLElement | null {
return this._bodyElement;
}
private _gestureRecognizer = new SwipeGestureRecognizer(); private _gestureRecognizer = new SwipeGestureRecognizer();
private _isDragging = false; private _isDragging = false;
@@ -56,13 +49,9 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
@touchstart=${this._handleTouchStart} @touchstart=${this._handleTouchStart}
> >
<slot name="header"></slot> <slot name="header"></slot>
<div class="content-wrapper">
<div id="body" class="body ha-scrollbar"> <div id="body" class="body ha-scrollbar">
<slot></slot> <slot></slot>
</div> </div>
${this.renderScrollableFades()}
</div>
<slot name="footer"></slot>
</wa-drawer> </wa-drawer>
`; `;
} }
@@ -178,9 +167,7 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
this._isDragging = false; this._isDragging = false;
} }
static get styles() { static styles = [
return [
...super.styles,
haStyleScrollbar, haStyleScrollbar,
css` css`
wa-drawer { wa-drawer {
@@ -221,13 +208,6 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.content-wrapper {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
:host([flexcontent]) .body { :host([flexcontent]) .body {
flex: 1; flex: 1;
max-width: 100%; max-width: 100%;
@@ -239,27 +219,9 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
var(--safe-area-inset-left) var(--safe-area-inset-left)
); );
} }
slot[name="footer"] {
display: block;
padding: var(--ha-space-0);
}
::slotted([slot="footer"]) {
display: flex;
padding: var(--ha-space-3) var(--ha-space-4) var(--ha-space-4)
var(--ha-space-4);
gap: var(--ha-space-3);
justify-content: flex-end;
align-items: center;
width: 100%;
box-sizing: border-box;
}
:host([flexcontent]) slot[name="footer"] {
flex-shrink: 0;
}
`, `,
]; ];
} }
}
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {

View File

@@ -3,7 +3,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import type { ClimateEntity } from "../data/climate"; import type { ClimateEntity } from "../data/climate";
import { CLIMATE_PRESET_NONE } from "../data/climate"; import { CLIMATE_PRESET_NONE } from "../data/climate";
import { isUnavailableState, OFF } from "../data/entity/entity"; import { isUnavailableState, OFF } from "../data/entity";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
@customElement("ha-climate-state") @customElement("ha-climate-state")

View File

@@ -9,14 +9,14 @@ import type { ConfigEntry, SubEntry } from "../data/config_entries";
import { getConfigEntry, getSubEntries } from "../data/config_entries"; import { getConfigEntry, getSubEntries } from "../data/config_entries";
import type { Agent } from "../data/conversation"; import type { Agent } from "../data/conversation";
import { listAgents } from "../data/conversation"; import { listAgents } from "../data/conversation";
import { getExtendedEntityRegistryEntry } from "../data/entity/entity_registry";
import { fetchIntegrationManifest } from "../data/integration"; import { fetchIntegrationManifest } from "../data/integration";
import { showOptionsFlowDialog } from "../dialogs/config-flow/show-dialog-options-flow"; import { showOptionsFlowDialog } from "../dialogs/config-flow/show-dialog-options-flow";
import { showSubConfigFlowDialog } from "../dialogs/config-flow/show-dialog-sub-config-flow";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "./ha-list-item"; import "./ha-list-item";
import "./ha-select"; import "./ha-select";
import type { HaSelect } from "./ha-select"; import type { HaSelect } from "./ha-select";
import { getExtendedEntityRegistryEntry } from "../data/entity_registry";
import { showSubConfigFlowDialog } from "../dialogs/config-flow/show-dialog-sub-config-flow";
const NONE = "__NONE_OPTION__"; const NONE = "__NONE_OPTION__";

View File

@@ -47,7 +47,6 @@ export class HaDomainIcon extends LitElement {
if (icn) { if (icn) {
return html`<ha-icon .icon=${icn}></ha-icon>`; return html`<ha-icon .icon=${icn}></ha-icon>`;
} }
return this._renderFallback(); return this._renderFallback();
}); });

View File

@@ -1,9 +1,6 @@
import DropdownItem from "@home-assistant/webawesome/dist/components/dropdown-item/dropdown-item"; import DropdownItem from "@home-assistant/webawesome/dist/components/dropdown-item/dropdown-item";
import "@home-assistant/webawesome/dist/components/icon/icon"; import { css, type CSSResultGroup } from "lit";
import { css, type CSSResultGroup, html } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import "./ha-svg-icon";
import { mdiCheckboxBlankOutline, mdiCheckboxMarked } from "@mdi/js";
/** /**
* Home Assistant dropdown item component * Home Assistant dropdown item component
@@ -17,16 +14,6 @@ import { mdiCheckboxBlankOutline, mdiCheckboxMarked } from "@mdi/js";
*/ */
@customElement("ha-dropdown-item") @customElement("ha-dropdown-item")
export class HaDropdownItem extends DropdownItem { export class HaDropdownItem extends DropdownItem {
protected renderCheckboxIcon() {
return html`
<ha-svg-icon
id="check"
part="checkmark"
.path=${this.checked ? mdiCheckboxMarked : mdiCheckboxBlankOutline}
></ha-svg-icon>
`;
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
DropdownItem.styles, DropdownItem.styles,
@@ -35,10 +22,6 @@ export class HaDropdownItem extends DropdownItem {
min-height: var(--ha-space-10); min-height: var(--ha-space-10);
} }
#check {
visibility: visible;
}
#icon ::slotted(*) { #icon ::slotted(*) {
color: var(--ha-color-on-neutral-normal); color: var(--ha-color-on-neutral-normal);
} }

View File

@@ -10,8 +10,8 @@ import { computeCssColor } from "../common/color/compute-color";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { navigate } from "../common/navigate"; import { navigate } from "../common/navigate";
import { stringCompare } from "../common/string/compare"; import { stringCompare } from "../common/string/compare";
import type { LabelRegistryEntry } from "../data/label/label_registry"; import type { LabelRegistryEntry } from "../data/label_registry";
import { subscribeLabelRegistry } from "../data/label/label_registry"; import { subscribeLabelRegistry } from "../data/label_registry";
import { SubscribeMixin } from "../mixins/subscribe-mixin"; import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
@@ -167,33 +167,30 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
} }
private async _labelSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) { private async _labelSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
if (!ev.detail.index.size) {
fireEvent(this, "data-table-filter-changed", {
value: [],
items: undefined,
});
this.value = [];
return;
}
const value: string[] = [];
const filteredLabels = this._filteredLabels( const filteredLabels = this._filteredLabels(
this._labels, this._labels,
this._filter, this._filter,
this.value this.value
); );
const filteredLabelIds = new Set(filteredLabels.map((l) => l.label_id));
// Keep previously selected labels that are not in the current filtered view
const preservedLabels = (this.value || []).filter(
(id) => !filteredLabelIds.has(id)
);
// Build the new selection from the filtered labels based on selected indices
const newlySelectedLabels: string[] = [];
for (const index of ev.detail.index) { for (const index of ev.detail.index) {
const labelId = filteredLabels[index]?.label_id; const labelId = filteredLabels[index].label_id;
if (labelId) { value.push(labelId);
newlySelectedLabels.push(labelId);
} }
} this.value = value;
const value = [...preservedLabels, ...newlySelectedLabels];
this.value = value.length ? value : [];
fireEvent(this, "data-table-filter-changed", { fireEvent(this, "data-table-filter-changed", {
value: value.length ? value : undefined, value,
items: undefined, items: undefined,
}); });
} }

View File

@@ -12,9 +12,9 @@ import { updateAreaRegistryEntry } from "../data/area_registry";
import type { import type {
DeviceEntityDisplayLookup, DeviceEntityDisplayLookup,
DeviceRegistryEntry, DeviceRegistryEntry,
} from "../data/device/device_registry"; } from "../data/device_registry";
import { getDeviceEntityDisplayLookup } from "../data/device/device_registry"; import { getDeviceEntityDisplayLookup } from "../data/device_registry";
import type { EntityRegistryDisplayEntry } from "../data/entity/entity_registry"; import type { EntityRegistryDisplayEntry } from "../data/entity_registry";
import { import {
createFloorRegistryEntry, createFloorRegistryEntry,
getFloorAreaLookup, getFloorAreaLookup,
@@ -35,12 +35,6 @@ import "./ha-svg-icon";
const ADD_NEW_ID = "___ADD_NEW___"; const ADD_NEW_ID = "___ADD_NEW___";
const SEARCH_KEYS = [
{ name: "floorName", weight: 10 },
{ name: "aliases", weight: 8 },
{ name: "floor_id", weight: 3 },
];
interface FloorComboBoxItem extends PickerComboBoxItem { interface FloorComboBoxItem extends PickerComboBoxItem {
floor?: FloorRegistryEntry; floor?: FloorRegistryEntry;
} }
@@ -291,11 +285,10 @@ export class HaFloorPicker extends LitElement {
id: floor.floor_id, id: floor.floor_id,
primary: floorName, primary: floorName,
floor: floor, floor: floor,
search_labels: { sorting_label: floor.level?.toString() || "zzzzz",
floorName, search_labels: [floorName, floor.floor_id, ...floor.aliases].filter(
floor_id: floor.floor_id, (v): v is string => Boolean(v)
aliases: floor.aliases.join(" "), ),
},
}; };
}); });
@@ -400,10 +393,6 @@ export class HaFloorPicker extends LitElement {
.getAdditionalItems=${this._getAdditionalItems} .getAdditionalItems=${this._getAdditionalItems}
.valueRenderer=${valueRenderer} .valueRenderer=${valueRenderer}
.rowRenderer=${this._rowRenderer} .rowRenderer=${this._rowRenderer}
.searchKeys=${SEARCH_KEYS}
.unknownItemText=${this.hass.localize(
"ui.components.floor-picker.unknown"
)}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
> >
</ha-generic-picker> </ha-generic-picker>

View File

@@ -4,10 +4,8 @@ import { mdiPlaylistPlus } from "@mdi/js";
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit"; import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { tinykeys } from "tinykeys"; import { tinykeys } from "tinykeys";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "./ha-bottom-sheet"; import "./ha-bottom-sheet";
import "./ha-button"; import "./ha-button";
@@ -48,9 +46,8 @@ export class HaGenericPicker extends LitElement {
@property({ attribute: "hide-clear-icon", type: Boolean }) @property({ attribute: "hide-clear-icon", type: Boolean })
public hideClearIcon = false; public hideClearIcon = false;
/** To prevent lags, getItems needs to be memoized */
@property({ attribute: false }) @property({ attribute: false })
public getItems!: ( public getItems?: (
searchString?: string, searchString?: string,
section?: string section?: string
) => (PickerComboBoxItem | string)[]; ) => (PickerComboBoxItem | string)[];
@@ -67,9 +64,6 @@ export class HaGenericPicker extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>; public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
@property({ attribute: false })
public searchKeys?: FuseWeightedKey[];
@property({ attribute: false }) @property({ attribute: false })
public notFoundLabel?: string | ((search: string) => string); public notFoundLabel?: string | ((search: string) => string);
@@ -113,8 +107,6 @@ export class HaGenericPicker extends LitElement {
@property({ attribute: "selected-section" }) public selectedSection?: string; @property({ attribute: "selected-section" }) public selectedSection?: string;
@property({ attribute: "unknown-item-text" }) public unknownItemText?: string;
@query(".container") private _containerElement?: HTMLDivElement; @query(".container") private _containerElement?: HTMLDivElement;
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox; @query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
@@ -164,8 +156,6 @@ export class HaGenericPicker extends LitElement {
type="button" type="button"
class=${this._opened ? "opened" : ""} class=${this._opened ? "opened" : ""}
compact compact
.unknown=${this._unknownValue(this.value, this.getItems())}
.unknownItemText=${this.unknownItemText}
aria-label=${ifDefined(this.label)} aria-label=${ifDefined(this.label)}
@click=${this.open} @click=${this.open}
@clear=${this._clear} @clear=${this._clear}
@@ -239,23 +229,10 @@ export class HaGenericPicker extends LitElement {
.sections=${this.sections} .sections=${this.sections}
.sectionTitleFunction=${this.sectionTitleFunction} .sectionTitleFunction=${this.sectionTitleFunction}
.selectedSection=${this.selectedSection} .selectedSection=${this.selectedSection}
.searchKeys=${this.searchKeys}
></ha-picker-combo-box> ></ha-picker-combo-box>
`; `;
} }
private _unknownValue = memoizeOne(
(value?: string, items?: (PickerComboBoxItem | string)[]) => {
if (value === undefined || value === null || value === "" || !items) {
return false;
}
return !items.some(
(item) => typeof item !== "string" && item.id === value
);
}
);
private _renderHelper() { private _renderHelper() {
return this.helper return this.helper
? html`<ha-input-helper-text .disabled=${this.disabled} ? html`<ha-input-helper-text .disabled=${this.disabled}
@@ -367,10 +344,7 @@ export class HaGenericPicker extends LitElement {
wa-popover::part(body) { wa-popover::part(body) {
width: max(var(--body-width), 250px); width: max(var(--body-width), 250px);
max-width: var( max-width: max(var(--body-width), 250px);
--ha-generic-picker-max-width,
max(var(--body-width), 250px)
);
max-height: 500px; max-height: 500px;
height: 70vh; height: 70vh;
overflow: hidden; overflow: hidden;

View File

@@ -32,12 +32,6 @@ export class HaGridSizeEditor extends LitElement {
@property({ attribute: false }) public step = 1; @property({ attribute: false }) public step = 1;
@property({ type: Boolean, attribute: "rows-disabled" })
public rowsDisabled?: boolean;
@property({ type: Boolean, attribute: "columns-disabled" })
public columnsDisabled?: boolean;
@state() public _localValue?: CardGridSize = { rows: 1, columns: 1 }; @state() public _localValue?: CardGridSize = { rows: 1, columns: 1 };
protected willUpdate(changedProperties) { protected willUpdate(changedProperties) {
@@ -48,11 +42,9 @@ export class HaGridSizeEditor extends LitElement {
protected render() { protected render() {
const disabledColumns = const disabledColumns =
this.columnsDisabled || this.columnMin !== undefined && this.columnMin === this.columnMax;
(this.columnMin !== undefined && this.columnMin === this.columnMax);
const disabledRows = const disabledRows =
this.rowsDisabled || this.rowMin !== undefined && this.rowMin === this.rowMax;
(this.rowMin !== undefined && this.rowMin === this.rowMax);
const autoHeight = this._localValue?.rows === "auto"; const autoHeight = this._localValue?.rows === "auto";
const fullWidth = this._localValue?.columns === "full"; const fullWidth = this._localValue?.columns === "full";
@@ -80,7 +72,7 @@ export class HaGridSizeEditor extends LitElement {
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
@slider-moved=${this._sliderMoved} @slider-moved=${this._sliderMoved}
.disabled=${disabledColumns} .disabled=${disabledColumns}
tooltip-mode=${disabledColumns ? "never" : "always"} tooltip-mode="always"
></ha-grid-layout-slider> ></ha-grid-layout-slider>
<ha-grid-layout-slider <ha-grid-layout-slider
@@ -96,7 +88,7 @@ export class HaGridSizeEditor extends LitElement {
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
@slider-moved=${this._sliderMoved} @slider-moved=${this._sliderMoved}
.disabled=${disabledRows} .disabled=${disabledRows}
tooltip-mode=${disabledRows ? "never" : "always"} tooltip-mode="always"
></ha-grid-layout-slider> ></ha-grid-layout-slider>
${!this.isDefault ${!this.isDefault
? html` ? html`

View File

@@ -1,7 +1,7 @@
import type { TemplateResult } from "lit"; import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit"; import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { isUnavailableState, OFF } from "../data/entity/entity"; import { isUnavailableState, OFF } from "../data/entity";
import type { HumidifierEntity } from "../data/humidifier"; import type { HumidifierEntity } from "../data/humidifier";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";

View File

@@ -11,12 +11,12 @@ import {
} from "lit/decorators"; } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { getLabels, labelComboBoxKeys } from "../data/label/label_picker"; import type { LabelRegistryEntry } from "../data/label_registry";
import { import {
createLabelRegistryEntry, createLabelRegistryEntry,
getLabels,
subscribeLabelRegistry, subscribeLabelRegistry,
type LabelRegistryEntry, } from "../data/label_registry";
} from "../data/label/label_registry";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box"; import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import { SubscribeMixin } from "../mixins/subscribe-mixin"; import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { showLabelDetailDialog } from "../panels/config/labels/show-dialog-label-detail"; import { showLabelDetailDialog } from "../panels/config/labels/show-dialog-label-detail";
@@ -237,7 +237,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
.getItems=${this._getItems} .getItems=${this._getItems}
.getAdditionalItems=${this._getAdditionalItems} .getAdditionalItems=${this._getAdditionalItems}
.valueRenderer=${valueRenderer} .valueRenderer=${valueRenderer}
.searchKeys=${labelComboBoxKeys}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
> >
<slot .slot=${this._slotNodes?.length ? "field" : undefined}></slot> <slot .slot=${this._slotNodes?.length ? "field" : undefined}></slot>

View File

@@ -8,11 +8,11 @@ import memoizeOne from "memoize-one";
import { computeCssColor } from "../common/color/compute-color"; import { computeCssColor } from "../common/color/compute-color";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { stringCompare } from "../common/string/compare"; import { stringCompare } from "../common/string/compare";
import type { LabelRegistryEntry } from "../data/label/label_registry"; import type { LabelRegistryEntry } from "../data/label_registry";
import { import {
subscribeLabelRegistry, subscribeLabelRegistry,
updateLabelRegistryEntry, updateLabelRegistryEntry,
} from "../data/label/label_registry"; } from "../data/label_registry";
import { SubscribeMixin } from "../mixins/subscribe-mixin"; import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { showLabelDetailDialog } from "../panels/config/labels/show-dialog-label-detail"; import { showLabelDetailDialog } from "../panels/config/labels/show-dialog-label-detail";
import type { HomeAssistant, ValueChangedEvent } from "../types"; import type { HomeAssistant, ValueChangedEvent } from "../types";

View File

@@ -40,12 +40,14 @@ export const getLanguageOptions = (
return { return {
id: lang, id: lang,
primary, primary,
search_labels: [primary],
}; };
}); });
} else if (locale) { } else if (locale) {
options = languages.map((lang) => ({ options = languages.map((lang) => ({
id: lang, id: lang,
primary: formatLanguageCode(lang, locale), primary: formatLanguageCode(lang, locale),
search_labels: [formatLanguageCode(lang, locale)],
})); }));
} }

View File

@@ -99,7 +99,10 @@ class HaMarkdownElement extends ReactiveElement {
} }
); );
render(h(unsafeHTML(elements.join(""))), this.renderRoot); render(
elements.map((e) => h(unsafeHTML(e))),
this.renderRoot
);
this._resize(); this._resize();

View File

@@ -25,11 +25,11 @@ export class HaMarkdown extends LitElement {
@property({ type: Boolean }) public cache = false; @property({ type: Boolean }) public cache = false;
@query("ha-markdown-element") private _markdownElement?: ReactiveElement; @query("ha-markdown-element") private _markdownElement!: ReactiveElement;
protected async getUpdateComplete() { protected async getUpdateComplete() {
const result = await super.getUpdateComplete(); const result = await super.getUpdateComplete();
await this._markdownElement?.updateComplete; await this._markdownElement.updateComplete;
return result; return result;
} }
@@ -70,15 +70,13 @@ export class HaMarkdown extends LitElement {
a { a {
color: var(--markdown-link-color, var(--primary-color)); color: var(--markdown-link-color, var(--primary-color));
} }
:host([assist]) img {
height: auto;
width: auto;
transition: height 0.2s ease-in-out;
}
img { img {
background-color: var(--markdown-image-background-color); background-color: var(--markdown-image-background-color);
border-radius: var(--markdown-image-border-radius); border-radius: var(--markdown-image-border-radius);
max-width: 100%; max-width: 100%;
height: auto;
width: auto;
transition: height 0.2s ease-in-out;
} }
p:first-child > img:first-child { p:first-child > img:first-child {
vertical-align: top; vertical-align: top;

View File

@@ -14,11 +14,7 @@ import memoizeOne from "memoize-one";
import { tinykeys } from "tinykeys"; import { tinykeys } from "tinykeys";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../common/string/compare"; import { caseInsensitiveStringCompare } from "../common/string/compare";
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin"; import { HaFuse } from "../resources/fuse";
import {
multiTermSortedSearch,
type FuseWeightedKey,
} from "../resources/fuseMultiTerm";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import { loadVirtualizer } from "../resources/virtualizer"; import { loadVirtualizer } from "../resources/virtualizer";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
@@ -29,26 +25,11 @@ import "./ha-icon";
import "./ha-textfield"; import "./ha-textfield";
import type { HaTextField } from "./ha-textfield"; import type { HaTextField } from "./ha-textfield";
export const DEFAULT_SEARCH_KEYS: FuseWeightedKey[] = [
{
name: "primary",
weight: 10,
},
{
name: "secondary",
weight: 7,
},
{
name: "id",
weight: 3,
},
];
export interface PickerComboBoxItem { export interface PickerComboBoxItem {
id: string; id: string;
primary: string; primary: string;
secondary?: string; secondary?: string;
search_labels?: Record<string, string | null>; search_labels?: string[];
sorting_label?: string; sorting_label?: string;
icon_path?: string; icon_path?: string;
icon?: string; icon?: string;
@@ -78,7 +59,7 @@ export type PickerComboBoxSearchFn<T extends PickerComboBoxItem> = (
) => T[]; ) => T[];
@customElement("ha-picker-combo-box") @customElement("ha-picker-combo-box")
export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) { export class HaPickerComboBox extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
// eslint-disable-next-line lit/no-native-attributes // eslint-disable-next-line lit/no-native-attributes
@@ -95,13 +76,10 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
@property() public value?: string; @property() public value?: string;
@property({ attribute: false })
public searchKeys?: FuseWeightedKey[];
@state() private _listScrolled = false; @state() private _listScrolled = false;
@property({ attribute: false }) @property({ attribute: false })
public getItems!: ( public getItems?: (
searchString?: string, searchString?: string,
section?: string section?: string
) => (PickerComboBoxItem | string)[]; ) => (PickerComboBoxItem | string)[];
@@ -148,14 +126,8 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
@state() private _items: (PickerComboBoxItem | string)[] = []; @state() private _items: (PickerComboBoxItem | string)[] = [];
protected get scrollableElement(): HTMLElement | null {
return this._virtualizerElement as HTMLElement | null;
}
@state() private _sectionTitle?: string; @state() private _sectionTitle?: string;
@state() private _valuePinned = true;
private _allItems: (PickerComboBoxItem | string)[] = []; private _allItems: (PickerComboBoxItem | string)[] = [];
private _selectedItemIndex = -1; private _selectedItemIndex = -1;
@@ -208,7 +180,6 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
</div> </div>
` `
: nothing} : nothing}
<div class="virtualizer-wrapper">
<lit-virtualizer <lit-virtualizer
.keyFunction=${this._keyFunction} .keyFunction=${this._keyFunction}
tabindex="0" tabindex="0"
@@ -217,22 +188,11 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
.renderItem=${this._renderItem} .renderItem=${this._renderItem}
style="min-height: 36px;" style="min-height: 36px;"
class=${this._listScrolled ? "scrolled" : ""} class=${this._listScrolled ? "scrolled" : ""}
.layout=${this.value && this._valuePinned
? {
pin: {
index: this._getInitialSelectedIndex(),
block: "center",
},
}
: undefined}
@unpinned=${this._handleUnpinned}
@scroll=${this._onScrollList} @scroll=${this._onScrollList}
@focus=${this._focusList} @focus=${this._focusList}
@visibilityChanged=${this._visibilityChanged} @visibilityChanged=${this._visibilityChanged}
> >
</lit-virtualizer> </lit-virtualizer>`;
${this.renderScrollableFades()}
</div>`;
} }
private _renderSectionButtons() { private _renderSectionButtons() {
@@ -276,42 +236,24 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
} }
} }
@eventOptions({ passive: true })
private _handleUnpinned() {
this._valuePinned = false;
}
private _getAdditionalItems = (searchString?: string) => private _getAdditionalItems = (searchString?: string) =>
this.getAdditionalItems?.(searchString) || []; this.getAdditionalItems?.(searchString) || [];
private _getItems = () => { private _getItems = () => {
let items = [...this.getItems(this._search, this.selectedSection)]; let items = [
...(this.getItems
? this.getItems(this._search, this.selectedSection)
: []),
];
if (!this.sections?.length) { if (!this.sections?.length) {
items = items.sort((entityA, entityB) => { items = items.sort((entityA, entityB) =>
const sortLabelA = caseInsensitiveStringCompare(
typeof entityA === "string" ? entityA : entityA.sorting_label; (entityA as PickerComboBoxItem).sorting_label!,
const sortLabelB = (entityB as PickerComboBoxItem).sorting_label!,
typeof entityB === "string" ? entityB : entityB.sorting_label;
if (!sortLabelA || !sortLabelB) {
return 0;
}
if (!sortLabelB) {
return -1;
}
if (!sortLabelA) {
return 1;
}
return caseInsensitiveStringCompare(
sortLabelA,
sortLabelB,
this.hass?.locale.language ?? navigator.language this.hass?.locale.language ?? navigator.language
)
); );
});
} }
if (!items.length) { if (!items.length) {
@@ -329,9 +271,6 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
}; };
private _renderItem = (item: PickerComboBoxItem | string, index: number) => { private _renderItem = (item: PickerComboBoxItem | string, index: number) => {
if (!item) {
return nothing;
}
if (item === "padding") { if (item === "padding") {
return html`<div class="bottom-padding"></div>`; return html`<div class="bottom-padding"></div>`;
} }
@@ -392,9 +331,8 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
fireEvent(this, "value-changed", { value: newValue }); fireEvent(this, "value-changed", { value: newValue });
}; };
private _fuseIndex = memoizeOne( private _fuseIndex = memoizeOne((states: PickerComboBoxItem[]) =>
(states: PickerComboBoxItem[], searchKeys?: FuseWeightedKey[]) => Fuse.createIndex(["search_labels"], states)
Fuse.createIndex(searchKeys || DEFAULT_SEARCH_KEYS, states)
); );
private _filterChanged = (ev: Event) => { private _filterChanged = (ev: Event) => {
@@ -410,25 +348,33 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
return; return;
} }
const index = this._fuseIndex( const index = this._fuseIndex(this._allItems as PickerComboBoxItem[]);
const fuse = new HaFuse(
this._allItems as PickerComboBoxItem[], this._allItems as PickerComboBoxItem[],
this.searchKeys {
shouldSort: false,
minMatchCharLength: Math.min(searchString.length, 2),
},
index
); );
let filteredItems = multiTermSortedSearch<PickerComboBoxItem>( const results = fuse.multiTermsSearch(searchString);
this._allItems as PickerComboBoxItem[], let filteredItems = [...this._allItems];
searchString,
this.searchKeys || DEFAULT_SEARCH_KEYS,
(item) => item.id,
index
) as (PickerComboBoxItem | string)[];
if (!filteredItems.length) { if (results) {
const items: (PickerComboBoxItem | string)[] = results.map(
(result) => result.item
);
if (!items.length) {
filteredItems.push(NO_ITEMS_AVAILABLE_ID); filteredItems.push(NO_ITEMS_AVAILABLE_ID);
} }
const additionalItems = this._getAdditionalItems(); const additionalItems = this._getAdditionalItems();
filteredItems.push(...additionalItems); items.push(...additionalItems);
filteredItems = items;
}
if (this.searchFn) { if (this.searchFn) {
filteredItems = this.searchFn( filteredItems = this.searchFn(
@@ -636,29 +582,9 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
} }
private _keyFunction = (item: PickerComboBoxItem | string) => private _keyFunction = (item: PickerComboBoxItem | string) =>
typeof item === "string" ? item : item?.id; typeof item === "string" ? item : item.id;
private _getInitialSelectedIndex() { static styles = [
if (!this._virtualizerElement || !this.value) {
return 0;
}
const index = this._virtualizerElement.items.findIndex(
(item) =>
typeof item !== "string" &&
(item as PickerComboBoxItem).id === this.value
);
if (index === -1) {
return 0;
}
return index;
}
static get styles() {
return [
...super.styles,
haStyleScrollbar, haStyleScrollbar,
css` css`
:host { :host {
@@ -691,14 +617,6 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
} }
} }
.virtualizer-wrapper {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
lit-virtualizer { lit-virtualizer {
flex: 1; flex: 1;
} }
@@ -817,7 +735,6 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
`, `,
]; ];
} }
}
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {

View File

@@ -1,4 +1,3 @@
import { consume } from "@lit/context";
import { mdiClose, mdiMenuDown } from "@mdi/js"; import { mdiClose, mdiMenuDown } from "@mdi/js";
import { import {
css, css,
@@ -8,10 +7,8 @@ import {
type CSSResultGroup, type CSSResultGroup,
type TemplateResult, type TemplateResult,
} from "lit"; } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { localizeContext } from "../data/context";
import type { HomeAssistant } from "../types";
import "./ha-combo-box-item"; import "./ha-combo-box-item";
import type { HaComboBoxItem } from "./ha-combo-box-item"; import type { HaComboBoxItem } from "./ha-combo-box-item";
import "./ha-icon-button"; import "./ha-icon-button";
@@ -36,10 +33,6 @@ export class HaPickerField extends LitElement {
@property() public placeholder?: string; @property() public placeholder?: string;
@property({ type: Boolean, reflect: true }) public unknown = false;
@property({ attribute: "unknown-item-text" }) public unknownItemText?: string;
@property({ attribute: "hide-clear-icon", type: Boolean }) @property({ attribute: "hide-clear-icon", type: Boolean })
public hideClearIcon = false; public hideClearIcon = false;
@@ -48,10 +41,6 @@ export class HaPickerField extends LitElement {
@query("ha-combo-box-item", true) public item!: HaComboBoxItem; @query("ha-combo-box-item", true) public item!: HaComboBoxItem;
@state()
@consume({ context: localizeContext, subscribe: true })
private localize!: HomeAssistant["localize"];
public async focus() { public async focus() {
await this.updateComplete; await this.updateComplete;
await this.item?.focus(); await this.item?.focus();
@@ -72,12 +61,6 @@ export class HaPickerField extends LitElement {
${this.placeholder} ${this.placeholder}
</span> </span>
`} `}
${this.unknown
? html`<div slot="supporting-text" class="unknown">
${this.unknownItemText ||
this.localize("ui.components.combo-box.unknown_item")}
</div>`
: nothing}
${showClearIcon ${showClearIcon
? html` ? html`
<ha-icon-button <ha-icon-button
@@ -159,10 +142,6 @@ export class HaPickerField extends LitElement {
background-color: var(--mdc-theme-primary); background-color: var(--mdc-theme-primary);
} }
:host([unknown]) ha-combo-box-item {
background-color: var(--ha-color-fill-warning-quiet-resting);
}
.clear { .clear {
margin: 0 -8px; margin: 0 -8px;
--mdc-icon-button-size: 32px; --mdc-icon-button-size: 32px;
@@ -177,10 +156,6 @@ export class HaPickerField extends LitElement {
color: var(--secondary-text-color); color: var(--secondary-text-color);
padding: 0 8px; padding: 0 8px;
} }
.unknown {
color: var(--ha-color-on-warning-normal);
}
`, `,
]; ];
} }

View File

@@ -9,13 +9,13 @@ import { customElement, property, query, state } from "lit/decorators";
import { prepareZXingModule } from "barcode-detector"; import { prepareZXingModule } from "barcode-detector";
import type QrScanner from "qr-scanner"; import type QrScanner from "qr-scanner";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { addExternalBarCodeListener } from "../external_app/external_app_entrypoint"; import { addExternalBarCodeListener } from "../external_app/external_app_entrypoint";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "./ha-alert"; import "./ha-alert";
import "./ha-button"; import "./ha-button";
import "./ha-dropdown"; import "./ha-button-menu";
import "./ha-dropdown-item"; import "./ha-list-item";
import type { HaDropdownItem } from "./ha-dropdown-item";
import "./ha-spinner"; import "./ha-spinner";
import "./ha-textfield"; import "./ha-textfield";
import type { HaTextField } from "./ha-textfield"; import type { HaTextField } from "./ha-textfield";
@@ -52,8 +52,6 @@ class HaQrScanner extends LitElement {
@state() private _warning?: string; @state() private _warning?: string;
@state() private _selectedCamera?: string;
private _qrScanner?: QrScanner; private _qrScanner?: QrScanner;
private _qrNotFoundCount = 0; private _qrNotFoundCount = 0;
@@ -123,7 +121,7 @@ class HaQrScanner extends LitElement {
!this._error && !this._error &&
this._cameras && this._cameras &&
this._cameras.length > 1 this._cameras.length > 1
? html`<ha-dropdown @wa-select=${this._handleDropdownSelect}> ? html`<ha-button-menu fixed @closed=${stopPropagation}>
<ha-icon-button <ha-icon-button
slot="trigger" slot="trigger"
.label=${this.hass.localize( .label=${this.hass.localize(
@@ -133,17 +131,15 @@ class HaQrScanner extends LitElement {
></ha-icon-button> ></ha-icon-button>
${this._cameras!.map( ${this._cameras!.map(
(camera) => html` (camera) => html`
<ha-dropdown-item <ha-list-item
.value=${camera.id} .value=${camera.id}
class=${this._selectedCamera === camera.id @click=${this._cameraChanged}
? "selected"
: ""}
> >
${camera.label} ${camera.label}
</ha-dropdown-item> </ha-list-item>
` `
)} )}
</ha-dropdown>` </ha-button-menu>`
: nothing} : nothing}
</div>` </div>`
: html`<ha-alert alert-type="warning"> : html`<ha-alert alert-type="warning">
@@ -209,9 +205,6 @@ class HaQrScanner extends LitElement {
private async _listCameras(qrScanner: typeof QrScanner): Promise<void> { private async _listCameras(qrScanner: typeof QrScanner): Promise<void> {
this._cameras = await qrScanner.listCameras(true); this._cameras = await qrScanner.listCameras(true);
if (this._cameras.length > 0) {
this._selectedCamera = this._cameras[0].id;
}
} }
private _qrCodeError = (err: any) => { private _qrCodeError = (err: any) => {
@@ -259,12 +252,8 @@ class HaQrScanner extends LitElement {
this._qrCodeScanned(this._manualInput!.value); this._qrCodeScanned(this._manualInput!.value);
} }
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) { private _cameraChanged(ev: CustomEvent): void {
const cameraId = ev.detail?.item?.value; this._qrScanner?.setCamera((ev.target as any).value);
if (cameraId) {
this._selectedCamera = cameraId;
this._qrScanner?.setCamera(cameraId);
}
} }
private _openExternalScanner() { private _openExternalScanner() {
@@ -370,7 +359,7 @@ class HaQrScanner extends LitElement {
#canvas-container { #canvas-container {
position: relative; position: relative;
} }
ha-icon-button { ha-button-menu {
position: absolute; position: absolute;
bottom: 8px; bottom: 8px;
right: 8px; right: 8px;
@@ -380,9 +369,6 @@ class HaQrScanner extends LitElement {
color: white; color: white;
border-radius: var(--ha-border-radius-circle); border-radius: var(--ha-border-radius-circle);
} }
ha-dropdown-item.selected {
font-weight: var(--ha-font-weight-bold);
}
.row { .row {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -7,7 +7,7 @@ import { fullEntitiesContext } from "../../data/context";
import { import {
subscribeEntityRegistry, subscribeEntityRegistry,
type EntityRegistryEntry, type EntityRegistryEntry,
} from "../../data/entity/entity_registry"; } from "../../data/entity_registry";
import type { Action } from "../../data/script"; import type { Action } from "../../data/script";
import { migrateAutomationAction } from "../../data/script"; import { migrateAutomationAction } from "../../data/script";
import type { ActionSelector } from "../../data/selector"; import type { ActionSelector } from "../../data/selector";

View File

@@ -4,14 +4,14 @@ import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array"; import { ensureArray } from "../../common/array/ensure-array";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import { getDeviceIntegrationLookup } from "../../data/device_registry";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import type { EntitySources } from "../../data/entity_sources";
import { fetchEntitySourcesWithCache } from "../../data/entity_sources";
import type { AreaSelector } from "../../data/selector";
import type { ConfigEntry } from "../../data/config_entries"; import type { ConfigEntry } from "../../data/config_entries";
import { getConfigEntries } from "../../data/config_entries"; import { getConfigEntries } from "../../data/config_entries";
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
import { getDeviceIntegrationLookup } from "../../data/device/device_registry";
import type { EntitySources } from "../../data/entity/entity_sources";
import { fetchEntitySourcesWithCache } from "../../data/entity/entity_sources";
import type { AreaSelector } from "../../data/selector";
import { import {
filterSelectorDevices, filterSelectorDevices,
filterSelectorEntities, filterSelectorEntities,

View File

@@ -5,13 +5,13 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array"; import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import { getDeviceIntegrationLookup } from "../../data/device_registry";
import type { EntitySources } from "../../data/entity_sources";
import { fetchEntitySourcesWithCache } from "../../data/entity_sources";
import type { DeviceSelector } from "../../data/selector";
import type { ConfigEntry } from "../../data/config_entries"; import type { ConfigEntry } from "../../data/config_entries";
import { getConfigEntries } from "../../data/config_entries"; import { getConfigEntries } from "../../data/config_entries";
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
import { getDeviceIntegrationLookup } from "../../data/device/device_registry";
import type { EntitySources } from "../../data/entity/entity_sources";
import { fetchEntitySourcesWithCache } from "../../data/entity/entity_sources";
import type { DeviceSelector } from "../../data/selector";
import { import {
filterSelectorDevices, filterSelectorDevices,
filterSelectorEntities, filterSelectorEntities,

View File

@@ -4,12 +4,12 @@ import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { ensureArray } from "../../common/array/ensure-array"; import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import type { EntitySources } from "../../data/entity/entity_sources"; import type { EntitySources } from "../../data/entity_sources";
import { fetchEntitySourcesWithCache } from "../../data/entity/entity_sources"; import { fetchEntitySourcesWithCache } from "../../data/entity_sources";
import type { EntitySelector } from "../../data/selector"; import type { EntitySelector } from "../../data/selector";
import { import {
computeCreateDomains,
filterSelectorEntities, filterSelectorEntities,
computeCreateDomains,
} from "../../data/selector"; } from "../../data/selector";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import "../entity/ha-entities-picker"; import "../entity/ha-entities-picker";

View File

@@ -4,14 +4,14 @@ import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array"; import { ensureArray } from "../../common/array/ensure-array";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import { getDeviceIntegrationLookup } from "../../data/device_registry";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import type { EntitySources } from "../../data/entity_sources";
import { fetchEntitySourcesWithCache } from "../../data/entity_sources";
import type { FloorSelector } from "../../data/selector";
import type { ConfigEntry } from "../../data/config_entries"; import type { ConfigEntry } from "../../data/config_entries";
import { getConfigEntries } from "../../data/config_entries"; import { getConfigEntries } from "../../data/config_entries";
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
import { getDeviceIntegrationLookup } from "../../data/device/device_registry";
import type { EntitySources } from "../../data/entity/entity_sources";
import { fetchEntitySourcesWithCache } from "../../data/entity/entity_sources";
import type { FloorSelector } from "../../data/selector";
import { import {
filterSelectorDevices, filterSelectorDevices,
filterSelectorEntities, filterSelectorEntities,

View File

@@ -7,15 +7,15 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array"; import { ensureArray } from "../../common/array/ensure-array";
import type { DeviceRegistryEntry } from "../../data/device/device_registry"; import type { DeviceRegistryEntry } from "../../data/device_registry";
import { getDeviceIntegrationLookup } from "../../data/device/device_registry"; import { getDeviceIntegrationLookup } from "../../data/device_registry";
import type { EntitySources } from "../../data/entity/entity_sources"; import type { EntitySources } from "../../data/entity_sources";
import { fetchEntitySourcesWithCache } from "../../data/entity/entity_sources"; import { fetchEntitySourcesWithCache } from "../../data/entity_sources";
import type { TargetSelector } from "../../data/selector"; import type { TargetSelector } from "../../data/selector";
import { import {
computeCreateDomains,
filterSelectorDevices, filterSelectorDevices,
filterSelectorEntities, filterSelectorEntities,
computeCreateDomains,
} from "../../data/selector"; } from "../../data/selector";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import "../ha-target-picker"; import "../ha-target-picker";

View File

@@ -21,13 +21,6 @@ interface ServiceComboBoxItem extends PickerComboBoxItem {
service_id?: string; service_id?: string;
} }
const SEARCH_KEYS = [
{ name: "search_labels.name", weight: 10 },
{ name: "search_labels.description", weight: 8 },
{ name: "search_labels.domainName", weight: 6 },
{ name: "search_labels.serviceId", weight: 3 },
];
@customElement("ha-service-picker") @customElement("ha-service-picker")
class HaServicePicker extends LitElement { class HaServicePicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -148,10 +141,6 @@ class HaServicePicker extends LitElement {
this.hass.localize, this.hass.localize,
this.hass.services this.hass.services
)} )}
.searchKeys=${SEARCH_KEYS}
.unknownItemText=${this.hass.localize(
"ui.components.service-picker.unknown"
)}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
> >
</ha-generic-picker> </ha-generic-picker>
@@ -205,7 +194,9 @@ class HaServicePicker extends LitElement {
secondary: description, secondary: description,
domain_name: domainName, domain_name: domainName,
service_id: serviceId, service_id: serviceId,
search_labels: { serviceId, domainName, name, description }, search_labels: [serviceId, domainName, name, description].filter(
Boolean
),
sorting_label: serviceId, sorting_label: serviceId,
}); });
} }

View File

@@ -1,7 +1,7 @@
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import { subscribeLabFeature } from "../data/labs"; import { subscribeLabFeatures } from "../data/labs";
import { SubscribeMixin } from "../mixins/subscribe-mixin"; import { SubscribeMixin } from "../mixins/subscribe-mixin";
interface Snowflake { interface Snowflake {
@@ -10,7 +10,7 @@ interface Snowflake {
size: number; size: number;
duration: number; duration: number;
delay: number; delay: number;
rotation: number; blur: number;
} }
@customElement("ha-snowflakes") @customElement("ha-snowflakes")
@@ -27,14 +27,13 @@ export class HaSnowflakes extends SubscribeMixin(LitElement) {
public hassSubscribe() { public hassSubscribe() {
return [ return [
subscribeLabFeature( subscribeLabFeatures(this.hass!.connection, (features) => {
this.hass!.connection, this._enabled =
"frontend", features.find(
"winter_mode", (f) =>
(feature) => { f.domain === "frontend" && f.preview_feature === "winter_mode"
this._enabled = feature.enabled; )?.enabled ?? false;
} }),
),
]; ];
} }
@@ -52,7 +51,7 @@ export class HaSnowflakes extends SubscribeMixin(LitElement) {
size: Math.random() * 12 + 8, // Random size between 8-20px size: Math.random() * 12 + 8, // Random size between 8-20px
duration: Math.random() * 8 + 8, // Random duration between 8-16s duration: Math.random() * 8 + 8, // Random duration between 8-16s
delay: Math.random() * 8, // Random delay between 0-8s delay: Math.random() * 8, // Random delay between 0-8s
rotation: Math.random() * 720 - 360, // Random starting rotation -360 to 360deg blur: Math.random() * 1, // Random blur between 0-1px
}); });
} }
this._snowflakes = snowflakes; this._snowflakes = snowflakes;
@@ -76,27 +75,20 @@ export class HaSnowflakes extends SubscribeMixin(LitElement) {
<div class="snowflakes ${isDark ? "dark" : "light"}" aria-hidden="true"> <div class="snowflakes ${isDark ? "dark" : "light"}" aria-hidden="true">
${this._snowflakes.map( ${this._snowflakes.map(
(flake) => html` (flake) => html`
<svg <div
class="snowflake ${this.narrow && flake.id >= 30 class="snowflake ${this.narrow && flake.id >= 30
? "hide-narrow" ? "hide-narrow"
: ""}" : ""}"
style=" style="
left: ${flake.left}%; left: ${flake.left}%;
width: ${flake.size}px; font-size: ${flake.size}px;
height: ${flake.size}px;
animation-duration: ${flake.duration}s; animation-duration: ${flake.duration}s;
animation-delay: ${flake.delay}s; animation-delay: ${flake.delay}s;
--rotation: ${flake.rotation}deg; filter: blur(${flake.blur}px);
" "
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
> >
<path
d="M7.991 0a.644.644 0 0 1 .283 1.221v2.553l.986-.988a.645.645 0 0 1 .612-.839.644.644 0 1 1-.222 1.247l-1.376 1.38V7.52l1.65-.954.466-1.879a.645.645 0 0 1 .1-1.042.643.643 0 1 1 .445 1.189l-.363 1.356 3.145-1.82a.643.643 0 1 1 .282.49l-2.205 1.277 1.347.361a.643.643 0 1 1-.158.543l-1.88-.505L8.573 8l1.632.945 1.858-.535a.64.64 0 0 1 .95-.434.643.643 0 1 1-.805.98l-1.354.364L14 11.14a.641.641 0 0 1 .914.855.643.643 0 0 1-1.197-.366l-2.205-1.276.36 1.35a.642.642 0 0 1 .419.95.643.643 0 1 1-.967-.816l-.503-1.884L8.273 8.48v1.909l1.39 1.344a.644.644 0 1 1 .208 1.252.644.644 0 0 1-.606-.852l-.991-.994v3.64A.644.644 0 0 1 7.99 16a.644.644 0 0 1-.282-1.221v-2.553l-.986.988a.645.645 0 0 1-.612.839.644.644 0 1 1 .222-1.247l1.376-1.38V8.5l-1.632.945-.467 1.879q.079.068.134.163a.643.643 0 1 1-.68-.31l.364-1.357-3.145 1.82A.643.643 0 1 1 2 11.15l2.205-1.276-1.347-.361a.643.643 0 1 1 .158-.543l1.88.505L7.444 8l-1.65-.954-1.857.534a.64.64 0 0 1-.95.434.643.643 0 1 1 .805-.98l1.354-.364L2 4.85a.641.641 0 0 1-.914-.855.643.643 0 0 1 1.197.366l2.205 1.276-.36-1.35a.642.642 0 0 1-.419-.95.643.643 0 1 1 .967.816l.503 1.884L7.71 7.5V5.611L6.32 4.267a.644.644 0 1 1-.208-1.252.644.644 0 0 1 .607.852l.991.994V1.22A.644.644 0 0 1 7.991 0" </div>
fill="currentColor"
/>
</svg>
` `
)} )}
</div> </div>
@@ -136,10 +128,16 @@ export class HaSnowflakes extends SubscribeMixin(LitElement) {
.light .snowflake { .light .snowflake {
color: #00bcd4; color: #00bcd4;
text-shadow:
0 0 5px #00bcd4,
0 0 10px #00e5ff;
} }
.dark .snowflake { .dark .snowflake {
color: #fff; color: #fff;
text-shadow:
0 0 5px rgba(255, 255, 255, 0.8),
0 0 10px rgba(255, 255, 255, 0.5);
} }
.snowflake.hide-narrow { .snowflake.hide-narrow {
@@ -148,23 +146,19 @@ export class HaSnowflakes extends SubscribeMixin(LitElement) {
@keyframes fall { @keyframes fall {
0% { 0% {
transform: translateY(-10vh) translateX(0) rotate(var(--rotation)); transform: translateY(-10vh) translateX(0);
} }
25% { 25% {
transform: translateY(30vh) translateX(10px) transform: translateY(30vh) translateX(10px);
rotate(calc(var(--rotation) + 25deg));
} }
50% { 50% {
transform: translateY(60vh) translateX(-10px) transform: translateY(60vh) translateX(-10px);
rotate(calc(var(--rotation) + 50deg));
} }
75% { 75% {
transform: translateY(85vh) translateX(10px) transform: translateY(85vh) translateX(10px);
rotate(calc(var(--rotation) + 75deg));
} }
100% { 100% {
transform: translateY(120vh) translateX(0) transform: translateY(120vh) translateX(0);
rotate(calc(var(--rotation) + 100deg));
} }
} }

View File

@@ -13,30 +13,19 @@ import memoizeOne from "memoize-one";
import { ensureArray } from "../common/array/ensure-array"; import { ensureArray } from "../common/array/ensure-array";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { isValidEntityId } from "../common/entity/valid_entity_id"; import { isValidEntityId } from "../common/entity/valid_entity_id";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import { computeRTL } from "../common/util/compute_rtl"; import { computeRTL } from "../common/util/compute_rtl";
import { import {
areaFloorComboBoxKeys,
getAreasAndFloors, getAreasAndFloors,
type AreaFloorValue, type AreaFloorValue,
type FloorComboBoxItem, type FloorComboBoxItem,
} from "../data/area_floor_picker"; } from "../data/area_floor";
import { getConfigEntries, type ConfigEntry } from "../data/config_entries"; import { getConfigEntries, type ConfigEntry } from "../data/config_entries";
import { labelsContext } from "../data/context"; import { labelsContext } from "../data/context";
import { import { getDevices, type DevicePickerItem } from "../data/device_registry";
deviceComboBoxKeys, import type { HaEntityPickerEntityFilterFunc } from "../data/entity";
getDevices, import { getEntities, type EntityComboBoxItem } from "../data/entity_registry";
type DevicePickerItem,
} from "../data/device/device_picker";
import type { HaEntityPickerEntityFilterFunc } from "../data/entity/entity";
import {
entityComboBoxKeys,
getEntities,
type EntityComboBoxItem,
} from "../data/entity/entity_picker";
import { domainToName } from "../data/integration"; import { domainToName } from "../data/integration";
import { getLabels, labelComboBoxKeys } from "../data/label/label_picker"; import { getLabels, type LabelRegistryEntry } from "../data/label_registry";
import type { LabelRegistryEntry } from "../data/label/label_registry";
import { import {
areaMeetsFilter, areaMeetsFilter,
deviceMeetsFilter, deviceMeetsFilter,
@@ -48,11 +37,7 @@ import {
import { SubscribeMixin } from "../mixins/subscribe-mixin"; import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { isHelperDomain } from "../panels/config/helpers/const"; import { isHelperDomain } from "../panels/config/helpers/const";
import { showHelperDetailDialog } from "../panels/config/helpers/show-dialog-helper-detail"; import { showHelperDetailDialog } from "../panels/config/helpers/show-dialog-helper-detail";
import { import { HaFuse } from "../resources/fuse";
multiTermSearch,
multiTermSortedSearch,
type FuseWeightedKey,
} from "../resources/fuseMultiTerm";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import { brandsUrl } from "../util/brands-url"; import { brandsUrl } from "../util/brands-url";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
@@ -128,16 +113,16 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
private _fuseIndexes = { private _fuseIndexes = {
area: memoizeOne((states: FloorComboBoxItem[]) => area: memoizeOne((states: FloorComboBoxItem[]) =>
this._createFuseIndex(states, areaFloorComboBoxKeys) this._createFuseIndex(states)
), ),
entity: memoizeOne((states: EntityComboBoxItem[]) => entity: memoizeOne((states: EntityComboBoxItem[]) =>
this._createFuseIndex(states, entityComboBoxKeys) this._createFuseIndex(states)
), ),
device: memoizeOne((states: DevicePickerItem[]) => device: memoizeOne((states: DevicePickerItem[]) =>
this._createFuseIndex(states, deviceComboBoxKeys) this._createFuseIndex(states)
), ),
label: memoizeOne((states: PickerComboBoxItem[]) => label: memoizeOne((states: PickerComboBoxItem[]) =>
this._createFuseIndex(states, labelComboBoxKeys) this._createFuseIndex(states)
), ),
}; };
@@ -149,8 +134,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
} }
} }
private _createFuseIndex = (states, keys: FuseWeightedKey[]) => private _createFuseIndex = (states) =>
Fuse.createIndex(keys, states); Fuse.createIndex(["search_labels"], states);
protected render() { protected render() {
if (this.addOnTop) { if (this.addOnTop) {
@@ -743,14 +728,15 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
: undefined, : undefined,
undefined, undefined,
`entity${SEPARATOR}` `entity${SEPARATOR}`
).sort(this._sortBySortingLabel); );
if (searchTerm) { if (searchTerm) {
entityItems = this._filterGroup( entityItems = this._filterGroup(
"entity", "entity",
entityItems, entityItems,
searchTerm, searchTerm,
entityComboBoxKeys (item: EntityComboBoxItem) =>
item.stateObj?.entity_id === searchTerm
) as EntityComboBoxItem[]; ) as EntityComboBoxItem[];
} }
@@ -776,15 +762,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
: undefined, : undefined,
undefined, undefined,
`device${SEPARATOR}` `device${SEPARATOR}`
).sort(this._sortBySortingLabel); );
if (searchTerm) { if (searchTerm) {
deviceItems = this._filterGroup( deviceItems = this._filterGroup("device", deviceItems, searchTerm);
"device",
deviceItems,
searchTerm,
deviceComboBoxKeys
);
} }
if (!filterType && deviceItems.length) { if (!filterType && deviceItems.length) {
@@ -818,9 +799,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
areasAndFloors = this._filterGroup( areasAndFloors = this._filterGroup(
"area", "area",
areasAndFloors, areasAndFloors,
searchTerm, searchTerm
areaFloorComboBoxKeys,
false
) as FloorComboBoxItem[]; ) as FloorComboBoxItem[];
} }
@@ -862,15 +841,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
entityFilter, entityFilter,
targetValue?.label_id ? ensureArray(targetValue.label_id) : undefined, targetValue?.label_id ? ensureArray(targetValue.label_id) : undefined,
`label${SEPARATOR}` `label${SEPARATOR}`
).sort(this._sortBySortingLabel); );
if (searchTerm) { if (searchTerm) {
labels = this._filterGroup( labels = this._filterGroup("label", labels, searchTerm);
"label",
labels,
searchTerm,
labelComboBoxKeys
);
} }
if (!filterType && labels.length) { if (!filterType && labels.length) {
@@ -889,24 +863,40 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
type: TargetType, type: TargetType,
items: (FloorComboBoxItem | PickerComboBoxItem | EntityComboBoxItem)[], items: (FloorComboBoxItem | PickerComboBoxItem | EntityComboBoxItem)[],
searchTerm: string, searchTerm: string,
weightedKeys: FuseWeightedKey[], checkExact?: (
sort = true item: FloorComboBoxItem | PickerComboBoxItem | EntityComboBoxItem
) => boolean
) { ) {
const fuseIndex = this._fuseIndexes[type](items); const fuseIndex = this._fuseIndexes[type](items);
const fuse = new HaFuse(
if (sort) {
return multiTermSortedSearch(
items, items,
searchTerm, {
weightedKeys, shouldSort: false,
(item) => item.id, minMatchCharLength: Math.min(searchTerm.length, 2),
},
fuseIndex fuseIndex
); );
const results = fuse.multiTermsSearch(searchTerm);
let filteredItems = items;
if (results) {
filteredItems = results.map((result) => result.item);
} }
return multiTermSearch(items, searchTerm, weightedKeys, fuseIndex, { if (!checkExact) {
ignoreLocation: true, return filteredItems;
}); }
// If there is exact match for entity id, put it first
const index = filteredItems.findIndex((item) => checkExact(item));
if (index === -1) {
return filteredItems;
}
const [exactMatch] = filteredItems.splice(index, 1);
filteredItems.unshift(exactMatch);
return filteredItems;
} }
private _getAdditionalItems = () => this._getCreateItems(this.createDomains); private _getAdditionalItems = () => this._getCreateItems(this.createDomains);
@@ -962,7 +952,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
let hasFloor = false; let hasFloor = false;
let rtl = false; let rtl = false;
let showEntityId = false; let showEntityId = false;
if (type === "area" || type === "floor") { if (type === "area" || type === "floor") {
item.id = item[type]?.[`${type}_id`];
rtl = computeRTL(this.hass); rtl = computeRTL(this.hass);
hasFloor = hasFloor =
type === "area" && !!(item as FloorComboBoxItem).area?.floor_id; type === "area" && !!(item as FloorComboBoxItem).area?.floor_id;
@@ -1068,13 +1061,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
term: html`<b>${search}</b>`, term: html`<b>${search}</b>`,
}); });
private _sortBySortingLabel = (entityA, entityB) =>
caseInsensitiveStringCompare(
(entityA as PickerComboBoxItem).sorting_label!,
(entityB as PickerComboBoxItem).sorting_label!,
this.hass?.locale.language ?? navigator.language
);
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
.add-target-wrapper { .add-target-wrapper {

View File

@@ -13,7 +13,6 @@ export class HaToast extends Snackbar {
} }
.mdc-snackbar { .mdc-snackbar {
z-index: 10;
margin: 8px; margin: 8px;
right: calc(8px + var(--safe-area-inset-right)); right: calc(8px + var(--safe-area-inset-right));
bottom: calc(8px + var(--safe-area-inset-bottom)); bottom: calc(8px + var(--safe-area-inset-bottom));

View File

@@ -10,7 +10,6 @@ import {
} from "lit/decorators"; } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "./ha-dialog-header"; import "./ha-dialog-header";
@@ -50,6 +49,7 @@ export type DialogWidth = "small" | "medium" | "large" | "full";
* @cssprop --ha-dialog-surface-background - Dialog background color. * @cssprop --ha-dialog-surface-background - Dialog background color.
* @cssprop --ha-dialog-border-radius - Border radius of the dialog surface. * @cssprop --ha-dialog-border-radius - Border radius of the dialog surface.
* @cssprop --dialog-z-index - Z-index for the dialog. * @cssprop --dialog-z-index - Z-index for the dialog.
* @cssprop --dialog-surface-position - CSS position of the dialog surface.
* @cssprop --dialog-surface-margin-top - Top margin for the dialog surface. * @cssprop --dialog-surface-margin-top - Top margin for the dialog surface.
* *
* @attr {boolean} open - Controls the dialog open state. * @attr {boolean} open - Controls the dialog open state.
@@ -73,7 +73,7 @@ export type DialogWidth = "small" | "medium" | "large" | "full";
* @see https://github.com/home-assistant/frontend/issues/27143 * @see https://github.com/home-assistant/frontend/issues/27143
*/ */
@customElement("ha-wa-dialog") @customElement("ha-wa-dialog")
export class HaWaDialog extends ScrollableFadeMixin(LitElement) { export class HaWaDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "aria-labelledby" }) @property({ attribute: "aria-labelledby" })
@@ -114,10 +114,6 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
@state() @state()
private _bodyScrolled = false; private _bodyScrolled = false;
protected get scrollableElement(): HTMLElement | null {
return this.bodyContainer;
}
protected updated( protected updated(
changedProperties: Map<string | number | symbol, unknown> changedProperties: Map<string | number | symbol, unknown>
): void { ): void {
@@ -166,12 +162,9 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
<slot name="headerActionItems" slot="actionItems"></slot> <slot name="headerActionItems" slot="actionItems"></slot>
</ha-dialog-header> </ha-dialog-header>
</slot> </slot>
<div class="content-wrapper">
<div class="body ha-scrollbar" @scroll=${this._handleBodyScroll}> <div class="body ha-scrollbar" @scroll=${this._handleBodyScroll}>
<slot></slot> <slot></slot>
</div> </div>
${this.renderScrollableFades()}
</div>
<slot name="footer" slot="footer"></slot> <slot name="footer" slot="footer"></slot>
</wa-dialog> </wa-dialog>
`; `;
@@ -207,16 +200,11 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
this._bodyScrolled = (ev.target as HTMLDivElement).scrollTop > 0; this._bodyScrolled = (ev.target as HTMLDivElement).scrollTop > 0;
} }
static get styles() { static styles = [
return [
...super.styles,
haStyleScrollbar, haStyleScrollbar,
css` css`
wa-dialog { wa-dialog {
--full-width: var( --full-width: var(--ha-dialog-width-full, min(95vw, var(--safe-width)));
--ha-dialog-width-full,
min(95vw, var(--safe-width))
);
--width: min(var(--ha-dialog-width-md, 580px), var(--full-width)); --width: min(var(--ha-dialog-width-md, 580px), var(--full-width));
--spacing: var(--dialog-content-padding, var(--ha-space-6)); --spacing: var(--dialog-content-padding, var(--ha-space-6));
--show-duration: var(--ha-dialog-show-duration, 200ms); --show-duration: var(--ha-dialog-show-duration, 200ms);
@@ -256,6 +244,7 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
calc(var(--safe-height) - var(--ha-space-20)) calc(var(--safe-height) - var(--ha-space-20))
); );
min-height: var(--ha-dialog-min-height); min-height: var(--ha-dialog-min-height);
position: var(--dialog-surface-position, relative);
margin-top: var(--dialog-surface-margin-top, auto); margin-top: var(--dialog-surface-margin-top, auto);
/* Used to offset the dialog from the safe areas when space is limited */ /* Used to offset the dialog from the safe areas when space is limited */
transform: translate( transform: translate(
@@ -341,20 +330,11 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
overflow: hidden; overflow: hidden;
} }
.content-wrapper {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.body { .body {
position: var(--dialog-content-position, relative); position: var(--dialog-content-position, relative);
padding: var( padding: 0 var(--dialog-content-padding, var(--ha-space-6))
--dialog-content-padding, var(--dialog-content-padding, var(--ha-space-6))
0 var(--ha-space-6) var(--ha-space-6) var(--ha-space-6) var(--dialog-content-padding, var(--ha-space-6));
);
overflow: auto; overflow: auto;
flex-grow: 1; flex-grow: 1;
} }
@@ -381,7 +361,6 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
`, `,
]; ];
} }
}
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {

View File

@@ -7,7 +7,7 @@ import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain"; import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name"; import { computeStateName } from "../../common/entity/compute_state_name";
import { supportsFeature } from "../../common/entity/supports-feature"; import { supportsFeature } from "../../common/entity/supports-feature";
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry"; import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import { extractApiErrorMessage } from "../../data/hassio/common"; import { extractApiErrorMessage } from "../../data/hassio/common";
import { import {
type MediaPlayerEntity, type MediaPlayerEntity,

View File

@@ -17,7 +17,7 @@ import { until } from "lit/directives/until";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { slugify } from "../../common/string/slugify"; import { slugify } from "../../common/string/slugify";
import { debounce } from "../../common/util/debounce"; import { debounce } from "../../common/util/debounce";
import { isUnavailableState } from "../../data/entity/entity"; import { isUnavailableState } from "../../data/entity";
import type { import type {
MediaPickedEvent, MediaPickedEvent,
MediaPlayerBrowseAction, MediaPlayerBrowseAction,

View File

@@ -1,5 +1,5 @@
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import type { HaEntityPickerEntityFilterFunc } from "../../../data/entity/entity"; import type { HaEntityPickerEntityFilterFunc } from "../../../data/entity";
import type { TargetType } from "../../../data/target"; import type { TargetType } from "../../../data/target";
import type { HaDevicePickerDeviceFilterFunc } from "../../device/ha-device-picker"; import type { HaDevicePickerDeviceFilterFunc } from "../../device/ha-device-picker";

View File

@@ -1,6 +1,6 @@
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity"; import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
import type { TargetType, TargetTypeFloorless } from "../../data/target"; import type { TargetType, TargetTypeFloorless } from "../../data/target";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import type { HaDevicePickerDeviceFilterFunc } from "../device/ha-device-picker"; import type { HaDevicePickerDeviceFilterFunc } from "../device/ha-device-picker";

View File

@@ -23,11 +23,11 @@ import { computeRTL } from "../../common/util/compute_rtl";
import type { AreaRegistryEntry } from "../../data/area_registry"; import type { AreaRegistryEntry } from "../../data/area_registry";
import { getConfigEntry } from "../../data/config_entries"; import { getConfigEntry } from "../../data/config_entries";
import { labelsContext } from "../../data/context"; import { labelsContext } from "../../data/context";
import type { DeviceRegistryEntry } from "../../data/device/device_registry"; import type { DeviceRegistryEntry } from "../../data/device_registry";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity"; import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
import type { FloorRegistryEntry } from "../../data/floor_registry"; import type { FloorRegistryEntry } from "../../data/floor_registry";
import { domainToName } from "../../data/integration"; import { domainToName } from "../../data/integration";
import type { LabelRegistryEntry } from "../../data/label/label_registry"; import type { LabelRegistryEntry } from "../../data/label_registry";
import { import {
areaMeetsFilter, areaMeetsFilter,
deviceMeetsFilter, deviceMeetsFilter,

View File

@@ -23,7 +23,7 @@ import { slugify } from "../../common/string/slugify";
import { getConfigEntry } from "../../data/config_entries"; import { getConfigEntry } from "../../data/config_entries";
import { labelsContext } from "../../data/context"; import { labelsContext } from "../../data/context";
import { domainToName } from "../../data/integration"; import { domainToName } from "../../data/integration";
import type { LabelRegistryEntry } from "../../data/label/label_registry"; import type { LabelRegistryEntry } from "../../data/label_registry";
import type { TargetType } from "../../data/target"; import type { TargetType } from "../../data/target";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url"; import { brandsUrl } from "../../util/brands-url";

View File

@@ -17,7 +17,6 @@ export class HaTraceBlueprintConfig extends LitElement {
return html` return html`
<ha-code-editor <ha-code-editor
.value=${dump(this.trace.blueprint_inputs || "").trimRight()} .value=${dump(this.trace.blueprint_inputs || "").trimRight()}
.hass=${this.hass}
read-only read-only
dir="ltr" dir="ltr"
></ha-code-editor> ></ha-code-editor>

View File

@@ -17,7 +17,6 @@ export class HaTraceConfig extends LitElement {
return html` return html`
<ha-code-editor <ha-code-editor
.value=${dump(this.trace.config).trimRight()} .value=${dump(this.trace.config).trimRight()}
.hass=${this.hass}
read-only read-only
dir="ltr" dir="ltr"
></ha-code-editor> ></ha-code-editor>

View File

@@ -1,16 +1,14 @@
import { consume } from "@lit/context";
import { dump } from "js-yaml"; import { dump } from "js-yaml";
import { consume } from "@lit/context";
import type { CSSResultGroup, TemplateResult } from "lit"; import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time"; import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import { describeCondition, describeTrigger } from "../../data/automation_i18n"; import "../ha-code-editor";
import { fullEntitiesContext, labelsContext } from "../../data/context"; import "../ha-icon-button";
import type { EntityRegistryEntry } from "../../data/entity/entity_registry"; import "./hat-logbook-note";
import type { LabelRegistryEntry } from "../../data/label/label_registry";
import type { LogbookEntry } from "../../data/logbook"; import type { LogbookEntry } from "../../data/logbook";
import { describeAction } from "../../data/script_i18n";
import type { import type {
ActionTraceStep, ActionTraceStep,
ChooseActionTraceStep, ChooseActionTraceStep,
@@ -18,12 +16,19 @@ import type {
} from "../../data/trace"; } from "../../data/trace";
import { getDataFromPath } from "../../data/trace"; import { getDataFromPath } from "../../data/trace";
import "../../panels/logbook/ha-logbook-renderer"; import "../../panels/logbook/ha-logbook-renderer";
import type { HomeAssistant } from "../../types";
import "../ha-code-editor";
import "../ha-icon-button";
import "./hat-logbook-note";
import type { NodeInfo } from "./hat-script-graph";
import { traceTabStyles } from "./trace-tab-styles"; import { traceTabStyles } from "./trace-tab-styles";
import type { HomeAssistant } from "../../types";
import type { NodeInfo } from "./hat-script-graph";
import { describeCondition, describeTrigger } from "../../data/automation_i18n";
import type { EntityRegistryEntry } from "../../data/entity_registry";
import type { LabelRegistryEntry } from "../../data/label_registry";
import type { FloorRegistryEntry } from "../../data/floor_registry";
import {
floorsContext,
fullEntitiesContext,
labelsContext,
} from "../../data/context";
import { describeAction } from "../../data/script_i18n";
const TRACE_PATH_TABS = [ const TRACE_PATH_TABS = [
"step_config", "step_config",
@@ -58,6 +63,10 @@ export class HaTracePathDetails extends LitElement {
@consume({ context: labelsContext, subscribe: true }) @consume({ context: labelsContext, subscribe: true })
_labelReg!: LabelRegistryEntry[]; _labelReg!: LabelRegistryEntry[];
@state()
@consume({ context: floorsContext, subscribe: true })
_floorReg!: Record<string, FloorRegistryEntry>;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div class="padded-box trace-info"> <div class="padded-box trace-info">
@@ -184,6 +193,8 @@ export class HaTracePathDetails extends LitElement {
${describeAction( ${describeAction(
this.hass, this.hass,
this._entityReg, this._entityReg,
this._labelReg,
this._floorReg,
currentDetail currentDetail
)} )}
</h2>` </h2>`
@@ -267,7 +278,6 @@ export class HaTracePathDetails extends LitElement {
return config return config
? html`<ha-code-editor ? html`<ha-code-editor
.value=${dump(config).trimEnd()} .value=${dump(config).trimEnd()}
.hass=${this.hass}
read-only read-only
dir="ltr" dir="ltr"
></ha-code-editor>` ></ha-code-editor>`

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