Compare commits

..

54 Commits

Author SHA1 Message Date
Bram Kragten
a9b026d0ef Bumped version to 20260325.5 2026-04-01 11:15:10 +02:00
Petar Petrov
35339906ec Fix layout of compare card in water/gas views (#51329) 2026-04-01 11:14:50 +02:00
Wendelin
ce23f716cc Improve dialog open logic (#51328) 2026-04-01 11:14:49 +02:00
Petar Petrov
aaf8fa199f Await energy translation fragment before generating dashboard strategy (#51327) 2026-04-01 11:14:48 +02:00
Aidan Timson
fba430d507 Fix target item loading error (#51326) 2026-04-01 11:14:47 +02:00
Petar Petrov
59361cbd38 Fix ZHA device count not including devices without entities (#51322) 2026-04-01 11:14:46 +02:00
Petar Petrov
b558117d8c Use ha-card-border-color for integration cards instead of divider-color (#51321) 2026-04-01 11:14:45 +02:00
Petar Petrov
a7c8347751 Fix Fill example data inserting incorrect datetime format (#51320) 2026-04-01 11:14:44 +02:00
Wendelin
31ca9c849a Remove target description (#51315) 2026-04-01 11:14:43 +02:00
Bram Kragten
6252d7e8f5 Bumped version to 20260325.4 2026-03-31 15:36:46 +02:00
Bram Kragten
f42986adf6 Make translation downloading async (#51314) 2026-03-31 15:36:31 +02:00
Bram Kragten
9e70ea3723 Bumped version to 20260325.3 2026-03-31 14:58:38 +02:00
Bram Kragten
de3b7bf513 Fix has target check for actions (#51309) 2026-03-31 14:58:19 +02:00
Petar Petrov
2c5f491c9e Use boundaryFilter data zoom mode only for line charts (#51307) 2026-03-31 14:58:18 +02:00
Wendelin
1ef13c5100 Fix automation add TCA dialog sometimes not opening (#51306) 2026-03-31 14:58:17 +02:00
Aidan Timson
c166335aca Fix above/below numeric state entity formatting (#51298) 2026-03-31 14:51:11 +02:00
Petar Petrov
c64ec21eca Fix x-axis labels for statistics graph month/year periods (#51295) 2026-03-31 14:51:10 +02:00
Norbert Rittel
8d62056f4a Change picker descriptions of triggers to match new style (#51294) 2026-03-31 14:51:09 +02:00
Bram Kragten
62e73608b6 Triggers/conditions Add usage and grouping to new multi domains (#51287) 2026-03-31 14:51:08 +02:00
Wendelin
aa66d8891c Improve date-range-picker mobile presets (#51285) 2026-03-31 14:51:07 +02:00
Paul Bottein
494a96c635 Hide section when all cards are hidden (#51281) 2026-03-31 14:51:06 +02:00
Petar Petrov
36d77f54ce Disable physics by default for large networks (#51277) 2026-03-31 14:51:05 +02:00
Wendelin
12fec9f580 Fix ha-dropdown z-index for legacy browsers (#51276) 2026-03-31 14:51:04 +02:00
Bram Kragten
5f1f55448a Numeric threshold selector: remove duplicate uom from input (#51275) 2026-03-31 14:51:03 +02:00
Paul Bottein
837e345ecf Reduce heading button badge font size and fix alignement (#51274)
Title: Reduce heading button badge font size and fix alignement
2026-03-31 14:51:02 +02:00
Wendelin
0929d7d18a Remove mobile-specific styles for date-range-picker (#51273)
Remove mobile-specific styles for date-picker component
2026-03-31 14:51:01 +02:00
Aidan Timson
70991d3c1e Limit ha-toast width to window, refactor CSS (#51272)
* Limit `ha-toast` width to window and use safe width

* Query assigned slots to stop actions display

* Constrain max-width

* Increase start/end padding
2026-03-31 14:51:00 +02:00
Wendelin
82e5bd62a1 Fix time input background (#51270)
Fix input color tokens
2026-03-31 14:50:59 +02:00
Wendelin
b8adf4e866 Fix date-range-picker preset selection (#51269) 2026-03-31 14:50:58 +02:00
Tom Carpenter
111be984e0 Add date range picker time validation (#51267)
* Fix base time inputs reportValidity() function

The queryAll selector returns a NodeList not not an array. Need to spread it to an array before we can use every().

* Validate the date range picker time inputs

Enable auto validation to get the nice red underline on invalid values, and then check validity before accepting the input.

* Fix automatic 24hr value conversion in AM/PM format

When using AM/PM, entering a 24 hour value will automatically convert the first time. For example 15 will become 3. However if you then enter 15 again it will stay as 15 and not update.
To fix this, make sure we trigger an update of the input field once the current update cycle is complete.

* Validate time inputs on save not value update

In the value changed callback, the update 24->12hr input correction will not have been updated and therefore they will report invalid.
2026-03-31 14:50:57 +02:00
Tom Carpenter
78a2cbb532 Fix new date-range-picker rendering on small screens (#51257) 2026-03-31 14:50:56 +02:00
ildar170975
34b09b140b Map card editor: use context in attribute selector (#30393)
use context in attribute selector
2026-03-31 14:50:55 +02:00
Simon Lamon
f173f901c5 Gauge improvements (#30368)
* Gauge last improvements

* Change needle

* Fixup

* Feedback comments

* Update src/components/ha-gauge.ts

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-03-31 14:50:55 +02:00
Paul Bottein
ebb6ac8d8b Bumped version to 20260325.2 2026-03-27 22:09:10 +01:00
Wendelin
abe214a33a Fix picker field disabled background (#30385) 2026-03-27 22:08:51 +01:00
Paul Bottein
248332ae27 Revert entity naming change (#30384) 2026-03-27 22:08:50 +01:00
Wendelin
82fc2fccdc Automation add TCA: Fix classMap usage (#30380) 2026-03-27 22:08:49 +01:00
Marcin Bauer
c8f30a7ee4 Use dedicated tab copy in automation add dialogs (#30378)
Co-authored-by: Wendelin <w@pe8.at>
2026-03-27 22:08:48 +01:00
Norbert Rittel
77f48d91cd Shorten collection_key_description to fit available space (#30376) 2026-03-27 22:08:47 +01:00
Paul Bottein
caa707a7b1 Only display entity name instead of friendly name in state info (#30365) 2026-03-27 22:08:46 +01:00
Petar Petrov
0bed0fa37e Fix negative currency display on sensor card (#30359) 2026-03-27 22:08:46 +01:00
Bram Kragten
5b6309d984 Numeric threshold selector fixes (#30350)
* Update numeric threshold

* Update ha-selector-numeric-threshold.ts
2026-03-27 22:08:45 +01:00
Aidan Timson
264818bc70 Fix floating ha-toast (#30344) 2026-03-27 22:08:44 +01:00
Bram Kragten
d664ab6836 Bumped version to 20260325.1 2026-03-26 17:08:11 +01:00
Bram Kragten
a6c4184054 Replace ua-parser-js with simple regexs (#30355) 2026-03-26 17:07:45 +01:00
karwosts
cb6985eb7c Stabilize map colors (#30354) 2026-03-26 17:07:44 +01:00
Bram Kragten
d466ab63bd Add target error badge if target is missing (#30352)
* Add target error badge if target is missing

* Don't show for newly added items
2026-03-26 17:07:40 +01:00
Paul Bottein
1132cdb364 Replace computeLovelaceEntityName with hass.formatEntityName (#30351) 2026-03-26 17:07:39 +01:00
Paul Bottein
0f9d48a03d Use hardcoded label for temperature and humidity sensor in climate dashboard (#30348)
* Only use entity name for climate view sensors

* Use hardcoded text
2026-03-26 17:07:38 +01:00
Paul Bottein
7e085d9b08 Fix stack card scrollbar clipping box-shadows (#30346)
* Fix stack card scrollbar clipping box-shadows

* Remove grid options

* Remove scrollbar
2026-03-26 17:07:37 +01:00
Timothy
1a62c7296c Set tap highlight color to transparent for button (#30340) 2026-03-26 17:07:36 +01:00
Petar Petrov
be1921229c Fix energy pie chart legend showing raw data instead of formatted values (#30339) 2026-03-26 17:07:34 +01:00
Paul Bottein
640558ad35 Add composed/text mode toggle to entity name picker (#30337) 2026-03-26 17:07:33 +01:00
sir-Unknown
99636c9719 Fix calendar event description not preserving line breaks (#30329)
Add `white-space: pre-line` to the event description style so that
newlines in the calendar event description are rendered correctly
instead of being collapsed into a single line.
2026-03-26 17:07:32 +01:00
544 changed files with 8176 additions and 12377 deletions

View File

@@ -3,9 +3,6 @@ contact_links:
- name: Request a feature for the UI / Dashboards
url: https://github.com/orgs/home-assistant/discussions
about: Request a new feature for the Home Assistant frontend.
- name: Discuss UI or UX design
url: https://github.com/OpenHomeFoundation/ux-design/discussions
about: Share design feedback and discuss visual or UX changes with the design team.
- name: Report a bug that is NOT related to the UI / Dashboards
url: https://github.com/home-assistant/core/issues
about: This is the issue tracker for our frontend. Please report other issues in the backend ("core") repository.

View File

@@ -5,9 +5,6 @@ updates:
schedule:
interval: weekly
time: "06:00"
cooldown:
default-days-before-reopen: 30
default-days: 7
open-pull-requests-limit: 10
labels:
- Dependencies

View File

@@ -8,9 +8,6 @@ on:
branches:
- master
permissions:
contents: read
env:
NODE_OPTIONS: --max_old_space_size=6144
@@ -27,7 +24,6 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: dev
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
@@ -63,7 +59,6 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: master
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0

View File

@@ -18,9 +18,6 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
lint:
name: Lint and check format
@@ -28,8 +25,6 @@ jobs:
steps:
- name: Check out files from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
@@ -64,8 +59,6 @@ jobs:
steps:
- name: Check out files from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
@@ -84,8 +77,6 @@ jobs:
steps:
- name: Check out files from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
@@ -98,13 +89,13 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: frontend-bundle-stats
path: build/stats/*.json
if-no-files-found: error
- name: Upload frontend build
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: frontend-build
path: hass_frontend/

View File

@@ -7,10 +7,6 @@ on:
# The branches below must be a subset of the branches above
branches: [dev]
permissions:
contents: read
security-events: write
jobs:
analyze:
name: Analyze
@@ -32,7 +28,6 @@ jobs:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
persist-credentials: false
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
@@ -41,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
uses: github/codeql-action/init@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
with:
languages: ${{ matrix.language }}
# 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)
- name: Autobuild
uses: github/codeql-action/autobuild@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
uses: github/codeql-action/autobuild@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -62,4 +57,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
uses: github/codeql-action/analyze@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1

View File

@@ -9,9 +9,6 @@ on:
- dev
- master
permissions:
contents: read
env:
NODE_OPTIONS: --max_old_space_size=6144
@@ -28,7 +25,6 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: dev
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
@@ -64,7 +60,6 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: master
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0

View File

@@ -5,9 +5,6 @@ on:
schedule:
- cron: "0 0 * * *"
permissions:
contents: read
env:
NODE_OPTIONS: --max_old_space_size=6144
@@ -20,8 +17,6 @@ jobs:
steps:
- name: Check out files from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0

View File

@@ -10,9 +10,6 @@ on:
branches:
- dev
permissions:
contents: read
env:
NODE_OPTIONS: --max_old_space_size=6144
@@ -25,8 +22,6 @@ jobs:
steps:
- name: Check out files from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0

View File

@@ -1,6 +1,6 @@
name: "Pull Request Labeler"
on: pull_request_target # zizmor: ignore[dangerous-triggers] -- safe: only runs actions/labeler, no PR code checkout
on: pull_request_target
jobs:
triage:

View File

@@ -5,10 +5,6 @@ on:
schedule:
- cron: "0 * * * *"
permissions:
issues: write
pull-requests: write
jobs:
lock:
runs-on: ubuntu-latest

View File

@@ -21,8 +21,6 @@ jobs:
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
@@ -59,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: wheels
path: dist/home_assistant_frontend*.whl
if-no-files-found: error
- name: Upload translations
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: translations
path: translations.tar.gz

View File

@@ -1,39 +1,25 @@
name: RelativeCI
on:
# zizmor: ignore[dangerous-triggers] -- safe: only downloads artifacts, no PR code checkout
workflow_run:
workflows: [CI]
types:
- completed
permissions:
contents: read
actions: read
jobs:
upload-frontend-modern:
name: Upload stats (frontend/modern)
upload:
name: Upload stats
if: ${{ github.event.workflow_run.conclusion == 'success' }}
strategy:
matrix:
bundle: [frontend]
build: [modern, legacy]
runs-on: ubuntu-latest
steps:
- name: Send bundle stats and build information to RelativeCI
uses: relative-ci/agent-action@3c681926017930047fc03acaa35cd6a44efcbfc3 # v3.2.2
with:
key: ${{ secrets.RELATIVE_CI_KEY_frontend_modern }}
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
token: ${{ github.token }}
artifactName: frontend-bundle-stats
webpackStatsFile: frontend-modern.json
upload-frontend-legacy:
name: Upload stats (frontend/legacy)
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
- name: Send bundle stats and build information to RelativeCI
uses: relative-ci/agent-action@3c681926017930047fc03acaa35cd6a44efcbfc3 # v3.2.2
with:
key: ${{ secrets.RELATIVE_CI_KEY_frontend_legacy }}
token: ${{ github.token }}
artifactName: frontend-bundle-stats
webpackStatsFile: frontend-legacy.json
artifactName: ${{ format('{0}-bundle-stats', matrix.bundle) }}
webpackStatsFile: ${{ format('{0}-{1}.json', matrix.bundle, matrix.build) }}

View File

@@ -18,6 +18,6 @@ jobs:
pull-requests: read
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@5de93583980a40bd78603b6dfdcda5b4df377b32 # v7.2.0
- uses: release-drafter/release-drafter@139054aeaa9adc52ab36ddf67437541f039b88e2 # v7.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -27,8 +27,6 @@ jobs:
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
@@ -36,12 +34,13 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Verify version
uses: home-assistant/actions/helpers/verify-version@f6f29a7ee3fa0eccadf3620a7b9ee00ab54ec03b # master
uses: home-assistant/actions/helpers/verify-version@master
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies
run: yarn install
@@ -58,15 +57,16 @@ jobs:
script/release
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
skip-existing: true
- name: Upload release assets
env:
GH_TOKEN: ${{ github.token }}
TAG_NAME: ${{ github.event.release.tag_name }}
run: gh release upload "$TAG_NAME" dist/*.whl dist/*.tar.gz --clobber
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
with:
files: |
dist/*.whl
dist/*.tar.gz
wheels-init:
name: Init wheels build
@@ -74,17 +74,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Generate requirements.txt
env:
GITHUB_REF: ${{ github.ref }}
run: |
# Sleep to give pypi time to populate the new version across mirrors
sleep 240
version=$(echo "$GITHUB_REF" | awk -F"/" '{print $NF}' )
version=$(echo "${{ github.ref }}" | awk -F"/" '{print $NF}' )
echo "home-assistant-frontend==$version" > ./requirements.txt
# home-assistant/wheels doesn't support SHA pinning
- name: Build wheels
uses: home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0
uses: home-assistant/wheels@2025.12.0
with:
abi: cp314
tag: musllinux_1_2
@@ -101,12 +99,11 @@ jobs:
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies
run: yarn install
- name: Download Translations
@@ -116,11 +113,8 @@ jobs:
- name: Build landing-page
run: landing-page/script/build_landing_page
- name: Tar folder
env:
TAG_NAME: ${{ github.event.release.tag_name }}
run: tar -czf "landing-page/home_assistant_frontend_landingpage-${TAG_NAME}.tar.gz" -C landing-page/dist .
run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist .
- name: Upload release asset
env:
GH_TOKEN: ${{ github.token }}
TAG_NAME: ${{ github.event.release.tag_name }}
run: gh release upload "$TAG_NAME" "landing-page/home_assistant_frontend_landingpage-${TAG_NAME}.tar.gz" --clobber
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
with:
files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz

View File

@@ -5,10 +5,6 @@ on:
schedule:
- cron: "0 * * * *"
permissions:
issues: write
pull-requests: write
jobs:
stale:
runs-on: ubuntu-latest

View File

@@ -8,9 +8,6 @@ on:
paths:
- src/translations/en.json
permissions:
contents: read
jobs:
upload:
name: Upload
@@ -18,8 +15,6 @@ jobs:
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Upload Translations
run: |

2
.gitignore vendored
View File

@@ -57,4 +57,4 @@ test/coverage/
# AI tooling
.claude
.cursor
.opencode

View File

@@ -6,9 +6,9 @@ import rootConfig from "../eslint.config.mjs";
export default tseslint.config(...rootConfig, {
rules: {
"no-console": "off",
"import-x/no-extraneous-dependencies": "off",
"import-x/extensions": "off",
"import-x/no-dynamic-require": "off",
"import/no-extraneous-dependencies": "off",
"import/extensions": "off",
"import/no-dynamic-require": "off",
"global-require": "off",
"@typescript-eslint/no-require-imports": "off",
"prefer-arrow-callback": "off",

View File

@@ -124,10 +124,7 @@ async function pollProcess(lokaliseApi, projectId, processId) {
console.log(
`Lokalise export process for ${project} in progress...`,
process.status,
process.details?.items_to_process
? `${Math.round(((process.details.items_processed || 0) / process.details.items_to_process) * 100)}% (${process.details.items_processed}/${process.details.items_to_process})`
: ""
process.status
);
await new Promise((resolve) => {

View File

@@ -6,6 +6,7 @@ import presetEnv from "@babel/preset-env";
import compilationTargets from "@babel/helper-compilation-targets";
import coreJSCompat from "core-js-compat";
import { logPlugin } from "@babel/preset-env/lib/debug.js";
// eslint-disable-next-line import/no-relative-packages
import shippedPolyfills from "../node_modules/babel-plugin-polyfill-corejs3/lib/shipped-proposals.js";
import { babelOptions } from "./bundle.cjs";

View File

@@ -26,7 +26,7 @@ import "../../../../src/components/ha-svg-icon";
import "../../../../src/layouts/hass-loading-screen";
import { registerServiceWorker } from "../../../../src/util/register-service-worker";
import "./hc-layout";
import "../../../../src/components/input/ha-input";
import "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-button";
const seeFAQ = (qid) => html`
@@ -123,11 +123,11 @@ export class HcConnect extends LitElement {
To get started, enter your Home Assistant URL and click authorize.
If you want a preview instead, click the show demo button.
</p>
<ha-input
<ha-textfield
label="Home Assistant URL"
placeholder="https://abcdefghijklmnop.ui.nabu.casa"
@keydown=${this._handleInputKeyDown}
></ha-input>
></ha-textfield>
${this.error ? html` <p class="error">${this.error}</p> ` : ""}
</div>
<div class="card-actions">
@@ -204,7 +204,7 @@ export class HcConnect extends LitElement {
}
private async _handleConnect() {
const inputEl = this.shadowRoot!.querySelector("ha-input")!;
const inputEl = this.shadowRoot!.querySelector("ha-textfield")!;
const value = inputEl.value || "";
this.error = undefined;
@@ -319,7 +319,7 @@ export class HcConnect extends LitElement {
flex: 1;
}
ha-input {
ha-textfield {
width: 100%;
}
`;

View File

@@ -11,9 +11,9 @@ export const demoConfigs: (() => Promise<DemoConfig>)[] = [
() => import("./jimpower").then((mod) => mod.demoJimpower),
];
// eslint-disable-next-line import-x/no-mutable-exports
// eslint-disable-next-line import/no-mutable-exports
export let selectedDemoConfigIndex = 0;
// eslint-disable-next-line import-x/no-mutable-exports
// eslint-disable-next-line import/no-mutable-exports
export let selectedDemoConfig: Promise<DemoConfig> =
demoConfigs[selectedDemoConfigIndex]();

View File

@@ -1,4 +1,3 @@
/// <reference types="chromecast-caf-sender" />
import { mdiTelevision } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, state } from "lit/decorators";

View File

@@ -1,5 +1,6 @@
// @ts-check
/* eslint-disable import/no-extraneous-dependencies */
import unusedImports from "eslint-plugin-unused-imports";
import globals from "globals";
import path from "node:path";
@@ -12,7 +13,6 @@ import { configs as litConfigs } from "eslint-plugin-lit";
import { configs as wcConfigs } from "eslint-plugin-wc";
import { configs as a11yConfigs } from "eslint-plugin-lit-a11y";
import html from "@html-eslint/eslint-plugin";
import importX from "eslint-plugin-import-x";
const _filename = fileURLToPath(import.meta.url);
const _dirname = path.dirname(_filename);
@@ -22,27 +22,8 @@ const compat = new FlatCompat({
allConfig: js.configs.all,
});
// Load airbnb-base via FlatCompat for non-import rules only.
// eslint-plugin-import is incompatible with ESLint 10 (uses removed APIs),
// so we strip its plugin/rules/settings and use eslint-plugin-import-x instead.
const airbnbConfigs = compat.extends("airbnb-base").map((config) => {
const { plugins = {}, rules = {}, settings = {}, ...rest } = config;
return {
...rest,
plugins: Object.fromEntries(
Object.entries(plugins).filter(([key]) => key !== "import")
),
rules: Object.fromEntries(
Object.entries(rules).filter(([key]) => !key.startsWith("import/"))
),
settings: Object.fromEntries(
Object.entries(settings).filter(([key]) => !key.startsWith("import/"))
),
};
});
export default tseslint.config(
...airbnbConfigs,
...compat.extends("airbnb-base"),
eslintConfigPrettier,
litConfigs["flat/all"],
tseslint.configs.recommended,
@@ -50,7 +31,6 @@ export default tseslint.config(
tseslint.configs.stylistic,
wcConfigs["flat/recommended"],
a11yConfigs.recommended,
importX.flatConfigs.recommended,
{
plugins: {
"unused-imports": unusedImports,
@@ -78,7 +58,7 @@ export default tseslint.config(
},
settings: {
"import-x/resolver": {
"import/resolver": {
webpack: {
config: "./rspack.config.cjs",
},
@@ -107,20 +87,12 @@ export default tseslint.config(
"prefer-destructuring": "off",
"no-restricted-globals": [2, "event"],
"prefer-promise-reject-errors": "off",
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
"object-curly-newline": "off",
"default-case": "off",
"wc/no-self-class": "off",
"no-shadow": "off",
"no-use-before-define": "off",
"import/prefer-default-export": "off",
"import/no-default-export": "off",
"import/no-unresolved": "off",
"import/no-cycle": "off",
// import-x rules (migrated from eslint-plugin-import / airbnb-base)
"import-x/named": "off",
"import-x/prefer-default-export": "off",
"import-x/no-default-export": "off",
"import-x/no-unresolved": "off",
"import-x/no-cycle": "off",
"import-x/extensions": [
"import/extensions": [
"error",
"ignorePackages",
{
@@ -128,24 +100,12 @@ export default tseslint.config(
js: "never",
},
],
"import-x/no-mutable-exports": "error",
"import-x/no-amd": "error",
"import-x/first": "error",
"import-x/order": [
"error",
{ groups: [["builtin", "external", "internal"]] },
],
"import-x/newline-after-import": "error",
"import-x/no-absolute-path": "error",
"import-x/no-dynamic-require": "error",
"import-x/no-webpack-loader-syntax": "error",
"import-x/no-named-default": "error",
"import-x/no-self-import": "error",
"import-x/no-useless-path-segments": ["error", { commonjs: true }],
"import-x/no-import-module-exports": ["error", { exceptions: [] }],
"import-x/no-relative-packages": "error",
// TypeScript rules
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
"object-curly-newline": "off",
"default-case": "off",
"wc/no-self-class": "off",
"no-shadow": "off",
"@typescript-eslint/camelcase": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-use-before-define": "off",
@@ -225,6 +185,7 @@ export default tseslint.config(
allowObjectTypes: "always",
},
],
"no-use-before-define": "off",
},
},
{
@@ -233,12 +194,6 @@ export default tseslint.config(
globals: globals.audioWorklet,
},
},
{
files: ["src/entrypoints/service-worker.ts"],
languageOptions: {
globals: globals.serviceworker,
},
},
{
plugins: {
html,

View File

@@ -57,7 +57,7 @@ Check the [webawesome documentation](https://webawesome.com/docs/components/butt
| ---------- | ---------------------------------------------- | -------- | --------------------------------------------------------------------------------- |
| appearance | "accent"/"filled"/"plain" | "accent" | Sets the button appearance. |
| variants | "brand"/"danger"/"neutral"/"warning"/"success" | "brand" | Sets the button color variant. "brand" is default. |
| size | "small"/"medium"/"large" | "medium" | Sets the button size. |
| size | "small"/"medium" | "medium" | Sets the button size. |
| loading | Boolean | false | Shows a loading indicator instead of the buttons label and disable buttons click. |
| disabled | Boolean | false | Disables the button and prevents user interaction. |

View File

@@ -10,7 +10,7 @@ import "../../../../src/components/input/ha-input";
import "../../../../src/components/input/ha-input-copy";
import "../../../../src/components/input/ha-input-multi";
import "../../../../src/components/input/ha-input-search";
import { internationalizationContext } from "../../../../src/data/context";
import { localizeContext } from "../../../../src/data/context";
const LOCALIZE_KEYS: Record<string, string> = {
"ui.common.copy": "Copy",
@@ -26,19 +26,11 @@ const LOCALIZE_KEYS: Record<string, string> = {
export class DemoHaInput extends LitElement {
constructor() {
super();
// Provides internationalizationContext for ha-input-copy, ha-input-multi and ha-input-search
// Provides localizeContext for ha-input-copy, ha-input-multi and ha-input-search
// eslint-disable-next-line no-new
new ContextProvider(this, {
context: internationalizationContext,
initialValue: {
localize: ((key: string) => LOCALIZE_KEYS[key] ?? key) as any,
language: "en",
selectedLanguage: null,
locale: {} as any,
translationMetadata: {} as any,
loadBackendTranslation: (async () => (key: string) => key) as any,
loadFragmentTranslation: (async () => (key: string) => key) as any,
},
context: localizeContext,
initialValue: ((key: string) => LOCALIZE_KEYS[key] ?? key) as any,
});
}

View File

@@ -1,73 +0,0 @@
---
title: Textarea
---
# Textarea `<ha-textarea>`
A multiline text input component supporting Home Assistant theming and validation, based on webawesome textarea.
Supports autogrow, hints, validation, and both material and outlined appearances.
## Implementation
### Example usage
```html
<ha-textarea label="Description" value="Hello world"></ha-textarea>
<ha-textarea
label="Notes"
placeholder="Type here..."
resize="auto"
></ha-textarea>
<ha-textarea label="Required field" required></ha-textarea>
<ha-textarea label="Disabled" disabled value="Can't edit this"></ha-textarea>
```
### API
This component is based on the webawesome textarea component.
**Slots**
- `label`: Custom label content. Overrides the `label` property.
- `hint`: Custom hint content. Overrides the `hint` property.
**Properties/Attributes**
| Name | Type | Default | Description |
| ------------------ | -------------------------------------------------------------- | ------- | ------------------------------------------------------------------------ |
| value | String | - | The current value of the textarea. |
| label | String | "" | The textarea's label text. |
| hint | String | "" | The textarea's hint/helper text. |
| placeholder | String | "" | Placeholder text shown when the textarea is empty. |
| rows | Number | 4 | The number of visible text rows. |
| resize | "none"/"vertical"/"horizontal"/"both"/"auto" | "none" | Controls the textarea's resize behavior. |
| readonly | Boolean | false | Makes the textarea readonly. |
| disabled | Boolean | false | Disables the textarea and prevents user interaction. |
| required | Boolean | false | Makes the textarea a required field. |
| auto-validate | Boolean | false | Validates the textarea on blur instead of on form submit. |
| invalid | Boolean | false | Marks the textarea as invalid. |
| validation-message | String | "" | Custom validation message shown when the textarea is invalid. |
| minlength | Number | - | The minimum length of input that will be considered valid. |
| maxlength | Number | - | The maximum length of input that will be considered valid. |
| name | String | - | The name of the textarea, submitted as a name/value pair with form data. |
| autocapitalize | "off"/"none"/"on"/"sentences"/"words"/"characters" | "" | Controls whether and how text input is automatically capitalized. |
| autocomplete | String | - | Indicates whether the browser's autocomplete feature should be used. |
| autofocus | Boolean | false | Automatically focuses the textarea when the page loads. |
| spellcheck | Boolean | true | Enables or disables the browser's spellcheck feature. |
| inputmode | "none"/"text"/"decimal"/"numeric"/"tel"/"search"/"email"/"url" | "" | Hints at the type of data for showing an appropriate virtual keyboard. |
| enterkeyhint | "enter"/"done"/"go"/"next"/"previous"/"search"/"send" | "" | Customizes the label or icon of the Enter key on virtual keyboards. |
#### CSS Parts
- `wa-base` - The underlying wa-textarea base wrapper.
- `wa-hint` - The underlying wa-textarea hint container.
- `wa-textarea` - The underlying wa-textarea textarea element.
**CSS Custom Properties**
- `--ha-textarea-padding-bottom` - Padding below the textarea host.
- `--ha-textarea-max-height` - Maximum height of the textarea when using `resize="auto"`. Defaults to `200px`.
- `--ha-textarea-required-marker` - The marker shown after the label for required fields. Defaults to `"*"`.

View File

@@ -1,151 +0,0 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement } from "lit/decorators";
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-textarea";
@customElement("demo-components-ha-textarea")
export class DemoHaTextarea extends LitElement {
protected render(): TemplateResult {
return html`
${["light", "dark"].map(
(mode) => html`
<div class=${mode}>
<ha-card header="ha-textarea in ${mode}">
<div class="card-content">
<h3>Basic</h3>
<div class="row">
<ha-textarea label="Default"></ha-textarea>
<ha-textarea
label="With value"
value="Hello world"
></ha-textarea>
<ha-textarea
label="With placeholder"
placeholder="Type here..."
></ha-textarea>
</div>
<h3>Autogrow</h3>
<div class="row">
<ha-textarea
label="Autogrow empty"
resize="auto"
></ha-textarea>
<ha-textarea
label="Autogrow with value"
resize="auto"
value="This textarea will grow as you type more content into it. Try adding more lines to see the effect."
></ha-textarea>
</div>
<h3>States</h3>
<div class="row">
<ha-textarea
label="Disabled"
disabled
value="Disabled"
></ha-textarea>
<ha-textarea
label="Readonly"
readonly
value="Readonly"
></ha-textarea>
<ha-textarea label="Required" required></ha-textarea>
</div>
<div class="row">
<ha-textarea
label="Invalid"
invalid
validation-message="This field is required"
value=""
></ha-textarea>
<ha-textarea
label="With hint"
hint="Supports Markdown"
></ha-textarea>
<ha-textarea
label="With rows"
.rows=${6}
placeholder="6 rows"
></ha-textarea>
</div>
<h3>No label</h3>
<div class="row">
<ha-textarea
placeholder="No label, just placeholder"
></ha-textarea>
<ha-textarea
resize="auto"
placeholder="No label, autogrow"
></ha-textarea>
</div>
</div>
</ha-card>
</div>
`
)}
`;
}
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: true,
theme: "default",
},
undefined,
undefined,
true
);
}
static styles = css`
:host {
display: flex;
justify-content: center;
}
.dark,
.light {
display: block;
background-color: var(--primary-background-color);
padding: 0 50px;
}
ha-card {
margin: 24px auto;
}
.card-content {
display: flex;
flex-direction: column;
gap: var(--ha-space-2);
}
h3 {
margin: var(--ha-space-4) 0 var(--ha-space-1) 0;
font-size: var(--ha-font-size-l);
font-weight: var(--ha-font-weight-medium);
}
h3:first-child {
margin-top: 0;
}
.row {
display: flex;
gap: var(--ha-space-4);
}
.row > * {
flex: 1;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-textarea": DemoHaTextarea;
}
}

View File

@@ -19,7 +19,7 @@ The Home Assistant interface is based on Material Design. It's a design system c
We want to make it as easy for designers to contribute as it is for developers. Theres a lot a designer can contribute to:
- Meet us at <a href="https://www.home-assistant.io/join-chat-design" rel="noopener noreferrer" target="_blank">Discord #designers channel</a>. If you can't see the channel, make sure you set the correct role in Channels & Roles.
- Start designing with our <a href="https://www.figma.com/design/2WGI8IDGyxINjSV6NRvPur/Home-Assistant-Design-Kit" rel="noopener noreferrer" target="_blank">Figma DesignKit</a>.
- Start designing with our <a href="https://www.figma.com/community/file/967153512097289521/Home-Assistant-DesignKit" rel="noopener noreferrer" target="_blank">Figma DesignKit</a>.
- Find the latest UX <a href="https://github.com/home-assistant/frontend/discussions?discussions_q=label%3Aux" rel="noopener noreferrer" target="_blank">discussions</a> and <a href="https://github.com/home-assistant/frontend/labels/ux" rel="noopener noreferrer" target="_blank">issues</a> on GitHub. Everyone can start a new issue or discussion!
## Developers

View File

@@ -1,3 +0,0 @@
---
title: Box shadow
---

View File

@@ -1,98 +0,0 @@
import { css, html, LitElement } from "lit";
import { customElement } from "lit/decorators";
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
const SHADOWS = ["s", "m", "l"] as const;
@customElement("demo-misc-box-shadow")
export class DemoMiscBoxShadow extends LitElement {
protected render() {
return html`
${["light", "dark"].map(
(mode) => html`
<div class=${mode}>
<h2>${mode}</h2>
<div class="grid">
${SHADOWS.map(
(size) => html`
<div
class="box"
style="box-shadow: var(--ha-box-shadow-${size})"
>
${size}
</div>
`
)}
</div>
</div>
`
)}
`;
}
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: true,
theme: "default",
},
undefined,
undefined,
true
);
}
static styles = css`
:host {
display: flex;
flex-direction: row;
gap: 48px;
padding: 48px;
}
.light,
.dark {
flex: 1;
background-color: var(--primary-background-color);
border-radius: 16px;
padding: 32px;
}
h2 {
margin: 0 0 24px;
font-size: 18px;
font-weight: 500;
color: var(--primary-text-color);
text-transform: capitalize;
}
.grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 32px;
}
.box {
display: flex;
align-items: center;
justify-content: center;
height: 120px;
border-radius: 12px;
background-color: var(--card-background-color);
color: var(--primary-text-color);
font-size: 16px;
font-weight: 500;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"demo-misc-box-shadow": DemoMiscBoxShadow;
}
}

View File

@@ -422,6 +422,7 @@ export class DemoEntityState extends LitElement {
return html`
<ha-data-table
.hass=${this.hass}
.columns=${this._columns(this.hass)}
.data=${this._rows()}
auto-height

View File

@@ -1,5 +1,7 @@
import { IntersectionController } from "@lit-labs/observers/intersection-controller.js";
import "@material/mwc-linear-progress/mwc-linear-progress";
import { mdiArrowCollapseDown, mdiDownload } from "@mdi/js";
// eslint-disable-next-line import/extensions
import { IntersectionController } from "@lit-labs/observers/intersection-controller.js";
import { LitElement, type PropertyValues, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";

View File

@@ -1,3 +1,4 @@
import "@material/mwc-linear-progress";
import { mdiOpenInNew } from "@mdi/js";
import { css, html, nothing, type PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -7,7 +8,6 @@ import "../../src/components/ha-button";
import "../../src/components/ha-fade-in";
import "../../src/components/ha-spinner";
import "../../src/components/ha-svg-icon";
import "../../src/components/progress/ha-progress-bar";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import "../../src/onboarding/onboarding-welcome-links";
import { onBoardingStyles } from "../../src/onboarding/styles";
@@ -60,7 +60,7 @@ class HaLandingPage extends LandingPageBaseElement {
${!networkIssue && !this._supervisorError
? html`
<p>${this.localize("subheader")}</p>
<ha-progress-bar indeterminate></ha-progress-bar>
<mwc-linear-progress indeterminate></mwc-linear-progress>
`
: nothing}
${networkIssue || this._networkInfoError

View File

@@ -1,6 +1,6 @@
export default {
"*.?(c|m){js,ts}": [
"eslint --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --fix",
"eslint --flag v10_config_lookup_from_file --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --fix",
"prettier --cache --write",
"lit-analyzer --quiet",
],

View File

@@ -8,8 +8,8 @@
"version": "1.0.0",
"scripts": {
"build": "script/build_frontend",
"lint:eslint": "eslint \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --max-warnings=0",
"format:eslint": "eslint \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --fix",
"lint:eslint": "eslint --flag v10_config_lookup_from_file \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --max-warnings=0",
"format:eslint": "eslint --flag v10_config_lookup_from_file \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --fix",
"lint:prettier": "prettier . --cache --check",
"format:prettier": "prettier . --cache --write",
"lint:types": "tsc",
@@ -30,11 +30,11 @@
"@braintree/sanitize-url": "7.1.2",
"@codemirror/autocomplete": "6.20.1",
"@codemirror/commands": "6.10.3",
"@codemirror/language": "6.12.3",
"@codemirror/language": "6.12.2",
"@codemirror/legacy-modes": "6.5.2",
"@codemirror/search": "6.6.0",
"@codemirror/state": "6.6.0",
"@codemirror/view": "6.41.0",
"@codemirror/view": "6.40.0",
"@date-fns/tz": "1.4.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "7.3.1",
@@ -59,14 +59,22 @@
"@lit-labs/virtualizer": "2.1.1",
"@lit/context": "1.1.6",
"@lit/reactive-element": "2.1.2",
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
"@material/mwc-base": "0.27.0",
"@material/mwc-checkbox": "0.27.0",
"@material/mwc-dialog": "0.27.0",
"@material/mwc-drawer": "0.27.0",
"@material/mwc-fab": "0.27.0",
"@material/mwc-floating-label": "0.27.0",
"@material/mwc-formfield": "patch:@material/mwc-formfield@npm%3A0.27.0#~/.yarn/patches/@material-mwc-formfield-npm-0.27.0-9528cb60f6.patch",
"@material/mwc-linear-progress": "0.27.0",
"@material/mwc-list": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
"@material/mwc-radio": "0.27.0",
"@material/mwc-select": "0.27.0",
"@material/mwc-switch": "0.27.0",
"@material/mwc-textarea": "0.27.0",
"@material/mwc-textfield": "0.27.0",
"@material/mwc-top-app-bar": "0.27.0",
"@material/mwc-top-app-bar-fixed": "0.27.0",
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
@@ -74,14 +82,14 @@
"@mdi/js": "7.4.47",
"@mdi/svg": "7.4.47",
"@replit/codemirror-indentation-markers": "6.5.3",
"@swc/helpers": "0.5.21",
"@swc/helpers": "0.5.19",
"@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "3.9.1",
"@tsparticles/preset-links": "3.2.0",
"@vibrant/color": "4.0.4",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
"@webcomponents/webcomponentsjs": "2.8.0",
"barcode-detector": "3.1.2",
"barcode-detector": "3.1.1",
"cally": "0.9.2",
"color-name": "2.1.0",
"comlink": "4.4.2",
@@ -94,7 +102,7 @@
"dialog-polyfill": "0.5.6",
"echarts": "6.0.0",
"element-internals-polyfill": "3.0.2",
"fuse.js": "7.3.0",
"fuse.js": "7.1.0",
"google-timezones-json": "1.2.0",
"gulp-zopfli-green": "7.0.0",
"hls.js": "1.6.15",
@@ -108,7 +116,7 @@
"lit": "3.3.2",
"lit-html": "3.3.2",
"luxon": "3.7.2",
"marked": "18.0.0",
"marked": "17.0.5",
"memoize-one": "6.0.0",
"node-vibrant": "4.0.4",
"object-hash": "3.0.0",
@@ -136,18 +144,16 @@
"@babel/plugin-transform-runtime": "7.29.0",
"@babel/preset-env": "7.29.2",
"@bundle-stats/plugin-webpack-filter": "4.22.0",
"@eslint/eslintrc": "3.3.5",
"@eslint/js": "10.0.1",
"@html-eslint/eslint-plugin": "0.59.0",
"@lokalise/node-api": "15.7.1",
"@html-eslint/eslint-plugin": "0.58.1",
"@lokalise/node-api": "15.6.1",
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.1.0",
"@octokit/rest": "22.0.1",
"@rsdoctor/rspack-plugin": "1.5.7",
"@rspack/core": "1.7.11",
"@rsdoctor/rspack-plugin": "1.5.5",
"@rspack/core": "1.7.9",
"@rspack/dev-server": "1.2.1",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.26",
"@types/chromecast-caf-receiver": "6.0.25",
"@types/chromecast-caf-sender": "1.0.11",
"@types/color-name": "2.0.0",
"@types/culori": "4.0.1",
@@ -163,17 +169,16 @@
"@types/sortablejs": "1.15.9",
"@types/tar": "7.0.87",
"@types/webspeechapi": "0.0.29",
"@vitest/coverage-v8": "4.1.4",
"@vitest/coverage-v8": "4.1.0",
"babel-loader": "10.1.1",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.4",
"browserslist-useragent-regexp": "4.1.3",
"del": "8.0.1",
"eslint": "10.2.0",
"eslint": "9.39.4",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "10.1.8",
"eslint-import-resolver-webpack": "0.13.11",
"eslint-import-resolver-webpack": "0.13.10",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-import-x": "4.16.2",
"eslint-plugin-lit": "2.2.1",
"eslint-plugin-lit-a11y": "5.1.1",
"eslint-plugin-unused-imports": "4.4.1",
@@ -181,14 +186,13 @@
"fancy-log": "2.0.0",
"fs-extra": "11.3.4",
"glob": "13.0.6",
"globals": "17.4.0",
"gulp": "5.0.1",
"gulp-brotli": "3.0.0",
"gulp-json-transform": "0.5.0",
"gulp-rename": "2.1.0",
"html-minifier-terser": "7.2.0",
"husky": "9.1.7",
"jsdom": "29.0.2",
"jsdom": "29.0.1",
"jszip": "3.10.1",
"lint-staged": "16.4.0",
"lit-analyzer": "2.0.3",
@@ -196,17 +200,17 @@
"lodash.template": "4.5.0",
"map-stream": "0.0.7",
"pinst": "3.0.0",
"prettier": "3.8.2",
"prettier": "3.8.1",
"rspack-manifest-plugin": "5.2.1",
"serve": "14.2.6",
"sinon": "21.1.0",
"tar": "7.5.13",
"sinon": "21.0.3",
"tar": "7.5.12",
"terser-webpack-plugin": "5.4.0",
"ts-lit-plugin": "2.0.2",
"typescript": "6.0.2",
"typescript-eslint": "8.58.1",
"typescript": "5.9.3",
"typescript-eslint": "8.57.1",
"vite-tsconfig-paths": "6.1.1",
"vitest": "4.1.4",
"vitest": "4.1.0",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.4.0#~/.yarn/patches/workbox-build-npm-7.4.0-c84561662c.patch"

View File

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

View File

@@ -1,5 +1,4 @@
/* eslint-disable no-console */
/// <reference types="chromecast-caf-sender" />
import type { Auth } from "home-assistant-js-websocket";
import { castApiAvailable } from "./cast_framework";

View File

@@ -32,12 +32,6 @@ const YAML_ONLY_THEMES_COLORS = new Set([
"disabled",
]);
/**
* Compose a CSS variable out of a theme color
* @param color - Theme color (examples: `red`, `primary-text`)
* @returns CSS variable in `--xxx-color` format;
* initial color if not found in theme colors
*/
export function computeCssVariableName(color: string): string {
if (THEME_COLORS.has(color) || YAML_ONLY_THEMES_COLORS.has(color)) {
return `--${color}-color`;
@@ -45,12 +39,6 @@ export function computeCssVariableName(color: string): string {
return color;
}
/**
* Compose a CSS variable out of a theme color & then resolve it
* @param color - Theme color (examples: `red`, `primary-text`)
* @returns Resolved CSS variable in `var(--xxx-color)` format;
* initial color if not found in theme colors
*/
export function computeCssColor(color: string): string {
const cssVarName = computeCssVariableName(color);
if (cssVarName !== color) {
@@ -59,22 +47,6 @@ export function computeCssColor(color: string): string {
return color;
}
/**
* Get a color from document's styles
* @param color - Named theme color (examples: `red`, `primary-text`)
* @returns Resolved color; initial color if not found in document's styles
*/
export function resolveThemeColor(color: string): string {
const cssColor = computeCssVariableName(color);
if (cssColor.startsWith("--")) {
const resolved = getComputedStyle(document.body)
.getPropertyValue(cssColor)
.trim();
return resolved || color;
}
return cssColor;
}
/**
* Validates if a string is a valid color.
* Accepts: hex colors (#xxx, #xxxxxx), theme colors, and valid CSS color names.

View File

@@ -1,6 +1,5 @@
import colors from "color-name";
import { expandHex } from "./hex";
import { resolveThemeColor } from "./compute-color";
const rgb_hex = (component: number): string => {
const hex = Math.round(Math.min(Math.max(component, 0), 255)).toString(16);
@@ -131,43 +130,26 @@ export const rgb2hs = (rgb: [number, number, number]): [number, number] =>
export const hs2rgb = (hs: [number, number]): [number, number, number] =>
hsv2rgb([hs[0], hs[1], 255]);
/**
* Attempt to get a HEX color from a color defined in different formats:
* HEX, rgb/rgba, named color
* @param color - Color (HEX, rgb/rgba, named color) to be converted to HEX
* @returns HEX color
*/
export function theme2hex(color: string): string {
// Attempting to find a HEX pattern in the input string
if (color.startsWith("#")) {
if (color.length === 4 || color.length === 5) {
const c = color;
export function theme2hex(themeColor: string): string {
if (themeColor.startsWith("#")) {
if (themeColor.length === 4 || themeColor.length === 5) {
const c = themeColor;
// Convert short-form hex (#abc) to 6 digit (#aabbcc). Ignore alpha channel.
return `#${c[1]}${c[1]}${c[2]}${c[2]}${c[3]}${c[3]}`;
}
if (color.length === 9) {
if (themeColor.length === 9) {
// Ignore alpha channel.
return color.substring(0, 7);
return themeColor.substring(0, 7);
}
return color;
return themeColor;
}
// Attempting to find a match in a HA Frontend theme colors
const themeColor = resolveThemeColor(color.toLowerCase());
if (themeColor !== color.toLowerCase()) {
// theme color is recognized, now re-attempt
return theme2hex(themeColor);
const rgbFromColorName = colors[themeColor.toLowerCase()];
if (rgbFromColorName) {
return rgb2hex(rgbFromColorName);
}
// Attempting to find a match in a web colors array
const rgbFromWebColor = colors[color.toLowerCase()];
if (rgbFromWebColor) {
// HEX color is recognized for the input named color
return rgb2hex(rgbFromWebColor);
}
// Attempting to find an RGB pattern in the input string
const rgbMatch = color.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
const rgbMatch = themeColor.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
if (rgbMatch) {
const [, r, g, b] = rgbMatch.map(Number);
return rgb2hex([r, g, b]);
@@ -176,5 +158,5 @@ export function theme2hex(color: string): string {
// We have a named color, and there's nothing in the table,
// so nothing further we can do with it.
// Compare/border/background color will all be the same.
return color;
return themeColor;
}

View File

@@ -1,5 +1,4 @@
import { wcagLuminance, wcagContrast } from "culori";
import { theme2hex } from "./convert-color";
/**
* Calculates the luminosity of an RGB color.
@@ -49,13 +48,3 @@ export const getRGBContrastRatio = (
rgb1: [number, number, number],
rgb2: [number, number, number]
) => Math.round((rgbContrast(rgb1, rgb2) + Number.EPSILON) * 100) / 100;
/**
* Returns a contrasted color (black or white) based on the luminance of another color
* @param color - Color (HEX, rgb/rgba, named color) to calculate a contrasted color
* @returns HEX color ("#000000" for dark backgrounds, "#ffffff" for light backgrounds)
*/
export const getContrastedColorHex = (color: string): string => {
const lum = wcagLuminance(theme2hex(color));
return lum > 0.5 ? "#000000" : "#ffffff";
};

View File

@@ -1,9 +1,6 @@
import { listenMediaQuery } from "../dom/media_query";
import type { HomeAssistant } from "../../types";
import type {
Condition,
ConditionContext,
} from "../../panels/lovelace/common/validate-condition";
import type { Condition } from "../../panels/lovelace/common/validate-condition";
import { checkConditionsMet } from "../../panels/lovelace/common/validate-condition";
import { extractMediaQueries, extractTimeConditions } from "./extract";
import { calculateNextTimeUpdate } from "./time-calculator";
@@ -22,8 +19,7 @@ export function setupMediaQueryListeners(
conditions: Condition[],
hass: HomeAssistant,
addListener: (unsub: () => void) => void,
onUpdate: (conditionsMet: boolean) => void,
getContext?: () => ConditionContext
onUpdate: (conditionsMet: boolean) => void
): void {
const mediaQueries = extractMediaQueries(conditions);
@@ -40,8 +36,7 @@ export function setupMediaQueryListeners(
if (hasOnlyMediaQuery) {
onUpdate(matches);
} else {
const context = getContext?.() ?? {};
const conditionsMet = checkConditionsMet(conditions, hass, context);
const conditionsMet = checkConditionsMet(conditions, hass);
onUpdate(conditionsMet);
}
});
@@ -56,8 +51,7 @@ export function setupTimeListeners(
conditions: Condition[],
hass: HomeAssistant,
addListener: (unsub: () => void) => void,
onUpdate: (conditionsMet: boolean) => void,
getContext?: () => ConditionContext
onUpdate: (conditionsMet: boolean) => void
): void {
const timeConditions = extractTimeConditions(conditions);
@@ -76,8 +70,7 @@ export function setupTimeListeners(
timeoutId = setTimeout(() => {
if (delay <= MAX_TIMEOUT_DELAY) {
const context = getContext?.() ?? {};
const conditionsMet = checkConditionsMet(conditions, hass, context);
const conditionsMet = checkConditionsMet(conditions, hass);
onUpdate(conditionsMet);
}
scheduleUpdate();
@@ -94,17 +87,3 @@ export function setupTimeListeners(
scheduleUpdate();
});
}
/**
* Sets up all condition listeners (media query, time) for conditional visibility.
*/
export function setupConditionListeners(
conditions: Condition[],
hass: HomeAssistant,
addListener: (unsub: () => void) => void,
onUpdate: (conditionsMet: boolean) => void,
getContext?: () => ConditionContext
): void {
setupMediaQueryListeners(conditions, hass, addListener, onUpdate, getContext);
setupTimeListeners(conditions, hass, addListener, onUpdate, getContext);
}

View File

@@ -14,7 +14,7 @@ export const isLoadedIntegration = (
) =>
!page.component ||
ensureArray(page.component).some((integration) =>
isComponentLoaded(hass.config, integration)
isComponentLoaded(hass, integration)
);
export const isNotLoadedIntegration = (
@@ -23,7 +23,7 @@ export const isNotLoadedIntegration = (
) =>
!page.not_component ||
!ensureArray(page.not_component).some((integration) =>
isComponentLoaded(hass.config, integration)
isComponentLoaded(hass, integration)
);
export const isCore = (page: PageNavigation) => page.core;

View File

@@ -21,9 +21,6 @@ export const filterNavigationPages = (
if (page.path === "#external-app-configuration") {
return hass.auth.external?.config.hasSettingsScreen;
}
if (page.adminOnly && !hass.user?.is_admin) {
return false;
}
// Only show Bluetooth page if there are Bluetooth config entries
if (page.component === "bluetooth") {
return options.hasBluetoothConfigEntries ?? false;

View File

@@ -2,6 +2,6 @@ import type { HomeAssistant } from "../../types";
/** Return if a component is loaded. */
export const isComponentLoaded = (
hassConfig: HomeAssistant["config"],
hass: HomeAssistant,
component: string
): boolean => hassConfig && hassConfig.components.includes(component);
): boolean => hass && hass.config.components.includes(component);

View File

@@ -27,7 +27,6 @@ export type DateRange =
| "this_year"
| "now-7d"
| "now-30d"
| "now-365d"
| "now-12m"
| "now-1h"
| "now-12h"
@@ -103,11 +102,6 @@ export const calcDateRange = (
),
calcDate(today, endOfMonth, locale, hassConfig),
];
case "now-365d":
return [
calcDate(today, subDays, locale, hassConfig, 365),
calcDate(today, subDays, locale, hassConfig, 0),
];
case "now-1h":
return [
calcDate(today, subHours, locale, hassConfig, 1),

View File

@@ -38,14 +38,6 @@ export interface HASSDomEvent<T> extends Event {
detail: T;
}
export type HASSDomTargetEvent<T extends EventTarget> = Event & {
target: T;
};
export type HASSDomCurrentTargetEvent<T extends EventTarget> = Event & {
currentTarget: T;
};
/**
* Dispatches a custom event with an optional detail value.
*

View File

@@ -7,8 +7,7 @@ export type LeafletModuleType = typeof import("leaflet");
export type LeafletDrawModuleType = typeof import("leaflet-draw");
export const setupLeafletMap = async (
mapElement: HTMLElement,
initialView?: { latitude: number; longitude: number; zoom?: number }
mapElement: HTMLElement
): Promise<[Map, LeafletModuleType, TileLayer]> => {
if (!mapElement.parentNode) {
throw new Error("Cannot setup Leaflet map on disconnected element");
@@ -33,12 +32,7 @@ export const setupLeafletMap = async (
markerClusterStyle.setAttribute("rel", "stylesheet");
mapElement.parentNode.appendChild(markerClusterStyle);
if (initialView) {
map.setView(
[initialView.latitude, initialView.longitude],
initialView.zoom ?? 13
);
}
map.setView([52.3731339, 4.8903147], 13);
const tileLayer = createTileLayer(Leaflet).addTo(map);

View File

@@ -14,25 +14,24 @@ export const computeDeviceName = (
export const computeDeviceNameDisplay = (
device: DeviceRegistryEntry,
localize: HomeAssistant["localize"],
hassStates: HomeAssistant["states"],
hass: HomeAssistant,
entities?: EntityRegistryEntry[] | EntityRegistryDisplayEntry[] | string[]
) =>
computeDeviceName(device) ||
(entities && fallbackDeviceName(hassStates, entities)) ||
localize("ui.panel.config.devices.unnamed_device", {
type: localize(
(entities && fallbackDeviceName(hass, entities)) ||
hass.localize("ui.panel.config.devices.unnamed_device", {
type: hass.localize(
`ui.panel.config.devices.type.${device.entry_type || "device"}`
),
});
export const fallbackDeviceName = (
hassStates: HomeAssistant["states"],
hass: HomeAssistant,
entities: EntityRegistryEntry[] | EntityRegistryDisplayEntry[] | string[]
) => {
for (const entity of entities || []) {
const entityId = typeof entity === "string" ? entity : entity.entity_id;
const stateObj = hassStates[entityId];
const stateObj = hass.states[entityId];
if (stateObj) {
return computeStateName(stateObj);
}

View File

@@ -1,11 +1,26 @@
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
import type { FloorRegistryEntry } from "../../../data/floor_registry";
import type { HomeAssistant } from "../../../types";
export const getDeviceArea = (
interface DeviceContext {
device: DeviceRegistryEntry;
area: AreaRegistryEntry | null;
floor: FloorRegistryEntry | null;
}
export const getDeviceContext = (
device: DeviceRegistryEntry,
areas: HomeAssistant["areas"]
): AreaRegistryEntry | undefined => {
hass: HomeAssistant
): DeviceContext => {
const areaId = device.area_id;
return areaId ? areas[areaId] : undefined;
const area = areaId ? hass.areas[areaId] : undefined;
const floorId = area?.floor_id;
const floor = floorId ? hass.floors[floorId] : undefined;
return {
device: device,
area: area || null,
floor: floor || null,
};
};

View File

@@ -27,7 +27,7 @@ export const isDeletableEntity = (
const entityRegEntry = entityRegistry.find((e) => e.entity_id === entity_id);
if (isHelperDomain(domain)) {
return !!(
isComponentLoaded(hass.config, domain) &&
isComponentLoaded(hass, domain) &&
entityRegEntry &&
fetchedHelpers.some((e) => e.id === entityRegEntry.unique_id)
);
@@ -56,7 +56,7 @@ export const deleteEntity = (
const domain = computeDomain(entity_id);
const entityRegEntry = entityRegistry.find((e) => e.entity_id === entity_id);
if (isHelperDomain(domain)) {
if (isComponentLoaded(hass.config, domain)) {
if (isComponentLoaded(hass, domain)) {
if (
entityRegEntry &&
fetchedHelpers.some((e) => e.id === entityRegEntry.unique_id)

View File

@@ -242,18 +242,14 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
},
};
export const getStatesDomain = (
export const getStates = (
hass: HomeAssistant,
domain: string,
attribute?: string | undefined
state: HassEntity,
attribute: string | undefined = undefined
): string[] => {
const domain = computeStateDomain(state);
const result: string[] = [];
if (!attribute) {
// All entities can have unavailable states
result.push(...UNAVAILABLE_STATES);
}
if (!attribute && domain in FIXED_DOMAIN_STATES) {
result.push(...FIXED_DOMAIN_STATES[domain]);
} else if (
@@ -264,7 +260,21 @@ export const getStatesDomain = (
result.push(...FIXED_DOMAIN_ATTRIBUTE_STATES[domain][attribute]);
}
// Dynamic values based on the entities
switch (domain) {
case "climate":
if (!attribute) {
result.push(...state.attributes.hvac_modes);
} else if (attribute === "fan_mode") {
result.push(...state.attributes.fan_modes);
} else if (attribute === "preset_mode") {
result.push(...state.attributes.preset_modes);
} else if (attribute === "swing_mode") {
result.push(...state.attributes.swing_modes);
} else if (attribute === "swing_horizontal_mode") {
result.push(...state.attributes.swing_horizontal_modes);
}
break;
case "device_tracker":
case "person":
if (!attribute) {
@@ -283,37 +293,6 @@ export const getStatesDomain = (
);
}
break;
}
return result;
};
export const getStates = (
hass: HomeAssistant,
state: HassEntity,
attribute: string | undefined = undefined
): string[] => {
const domain = computeStateDomain(state);
const result: string[] = [];
// Fixed values based on a domain
result.push(...getStatesDomain(hass, domain, attribute));
// Dynamic values based on the entities
switch (domain) {
case "climate":
if (!attribute) {
result.push(...state.attributes.hvac_modes);
} else if (attribute === "fan_mode") {
result.push(...state.attributes.fan_modes);
} else if (attribute === "preset_mode") {
result.push(...state.attributes.preset_modes);
} else if (attribute === "swing_mode") {
result.push(...state.attributes.swing_modes);
} else if (attribute === "swing_horizontal_mode") {
result.push(...state.attributes.swing_horizontal_modes);
}
break;
case "event":
if (attribute === "event_type") {
result.push(...state.attributes.event_types);
@@ -374,5 +353,9 @@ export const getStates = (
break;
}
if (!attribute) {
// All entities can have unavailable states
result.push(...UNAVAILABLE_STATES);
}
return [...new Set(result)];
};

View File

@@ -1,8 +0,0 @@
/**
* Indicates whether the current browser has native ElementInternals support.
*/
export const nativeElementInternalsSupported =
Boolean(globalThis.ElementInternals) &&
globalThis.HTMLElement?.prototype.attachInternals
?.toString()
.includes("[native code]");

View File

@@ -41,7 +41,7 @@ export const protocolIntegrationPicked = async (
).filter((e) => !e.disabled_by);
if (
!isComponentLoaded(hass.config, "zwave_js") ||
!isComponentLoaded(hass, "zwave_js") ||
(!options?.config_entry && !entries?.length)
) {
// If the component isn't loaded, ask them to load the integration first
@@ -90,7 +90,7 @@ export const protocolIntegrationPicked = async (
).filter((e) => !e.disabled_by);
if (
!isComponentLoaded(hass.config, "zha") ||
!isComponentLoaded(hass, "zha") ||
(!options?.config_entry && !entries?.length)
) {
// If the component isn't loaded, ask them to load the integration first
@@ -139,7 +139,7 @@ export const protocolIntegrationPicked = async (
})
).filter((e) => !e.disabled_by);
if (
!isComponentLoaded(hass.config, domain) ||
!isComponentLoaded(hass, domain) ||
(!options?.config_entry && !entries?.length)
) {
// If the component isn't loaded, ask them to load the integration first

View File

@@ -10,10 +10,13 @@
*
* @see https://github.com/home-assistant/frontend/issues/28732
*/
// eslint-disable-next-line import/extensions
import { directive, Directive } from "lit-html/directive.js";
// eslint-disable-next-line import/extensions
import { setCommittedValue } from "lit-html/directive-helpers.js";
// eslint-disable-next-line lit/no-legacy-imports
import { nothing } from "lit-html";
// eslint-disable-next-line import/extensions
import type { Part } from "lit-html/directive.js";
class KeyedES5 extends Directive {

View File

@@ -71,6 +71,13 @@ export const formatNumberToParts = (
? numberFormatToLocale(localeOptions)
: undefined;
// Polyfill for Number.isNaN, which is more reliable than the global isNaN()
Number.isNaN =
Number.isNaN ||
function isNaN(input) {
return typeof input === "number" && isNaN(input);
};
if (
localeOptions?.number_format !== NumberFormat.none &&
!Number.isNaN(Number(num))

View File

@@ -18,16 +18,15 @@ import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { ensureArray } from "../../common/array/ensure-array";
import { getAllGraphColors } from "../../common/color/colors";
import { transform } from "../../common/decorators/transform";
import type { HASSDomEvent } from "../../common/dom/fire_event";
import { fireEvent } from "../../common/dom/fire_event";
import { listenMediaQuery } from "../../common/dom/media_query";
import { afterNextRender } from "../../common/util/render-status";
import { filterXSS } from "../../common/util/xss";
import { uiContext } from "../../data/context";
import { themesContext } from "../../data/context";
import type { Themes } from "../../data/ws-themes";
import type { ECOption } from "../../resources/echarts/echarts";
import type { HomeAssistant, HomeAssistantUI } from "../../types";
import type { HomeAssistant } from "../../types";
import { isMac } from "../../util/is_mac";
import "../chips/ha-assist-chip";
import "../ha-icon-button";
@@ -75,11 +74,8 @@ export class HaChartBase extends LitElement {
public extraComponents?: any[];
@state()
@consume({ context: uiContext, subscribe: true })
@transform<HomeAssistantUI, Themes>({
transformer: ({ themes }) => themes,
})
private _themes!: Themes;
@consume({ context: themesContext, subscribe: true })
_themes!: Themes;
@state() private _isZoomed = false;
@@ -95,10 +91,6 @@ export class HaChartBase extends LitElement {
private _lastTapTime?: number;
private _longPressTimer?: ReturnType<typeof setTimeout>;
private _longPressTriggered = false;
private _shouldResizeChart = false;
private _resizeAnimationDuration?: number;
@@ -136,7 +128,6 @@ export class HaChartBase extends LitElement {
public disconnectedCallback() {
super.disconnectedCallback();
this._legendPointerCancel();
this._pendingSetup = false;
while (this._listeners.length) {
this._listeners.pop()!();
@@ -178,7 +169,6 @@ export class HaChartBase extends LitElement {
if (!this.options?.dataZoom) {
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
}
this._updateSankeyRoam();
// drag to zoom
this.chart?.dispatchAction({
type: "takeGlobalCursor",
@@ -197,7 +187,6 @@ export class HaChartBase extends LitElement {
if (!this.options?.dataZoom) {
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
}
this._updateSankeyRoam();
this.chart?.dispatchAction({
type: "takeGlobalCursor",
key: "dataZoomSelect",
@@ -273,9 +262,6 @@ export class HaChartBase extends LitElement {
}
if (Object.keys(chartOptions).length > 0) {
this._setChartOptions(chartOptions);
if (chartOptions.series) {
this._updateSankeyRoam();
}
}
}
@@ -316,31 +302,22 @@ export class HaChartBase extends LitElement {
`;
}
private _getLegendItems() {
private _renderLegend() {
if (!this.options?.legend || !this.data) {
return undefined;
return nothing;
}
const legend = ensureArray(this.options.legend).find(
(l) => l.show && l.type === "custom"
) as CustomLegendOption | undefined;
if (!legend) {
return undefined;
return nothing;
}
const datasets = ensureArray(this.data);
return (
const items =
legend.data ||
datasets
.filter((d) => (d.data as any[])?.length && (d.id || d.name))
.map((d) => ({ id: d.id, name: d.name }))
);
}
private _renderLegend() {
const items = this._getLegendItems();
if (!items) {
return nothing;
}
const datasets = ensureArray(this.data!);
.map((d) => ({ id: d.id, name: d.name }));
const isMobile = window.matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
@@ -385,11 +362,6 @@ export class HaChartBase extends LitElement {
return html`<li
.id=${id}
@click=${this._legendClick}
@pointerdown=${this._legendPointerDown}
@pointerup=${this._legendPointerCancel}
@pointerleave=${this._legendPointerCancel}
@pointercancel=${this._legendPointerCancel}
@contextmenu=${this._legendContextMenu}
class=${classMap({ hidden: this._hiddenDatasets.has(id) })}
.title=${name}
>
@@ -460,22 +432,6 @@ export class HaChartBase extends LitElement {
this.chart.on("click", (e: ECElementEvent) => {
fireEvent(this, "chart-click", e);
});
this.chart.on("sankeyroam", () => {
const option = this.chart!.getOption();
const series = option.series as any[];
const sankeySeries = series?.find((s: any) => s.type === "sankey");
const zoomed = sankeySeries.zoom !== 1;
this._isZoomed = zoomed;
if (!zoomed) {
// Reset center when fully zoomed out
this.chart!.setOption({
series: [{ id: sankeySeries.id, center: null }],
});
}
fireEvent(this, "chart-sankeyroam", { zoom: sankeySeries.zoom });
// Clear cached emphasis states so labels don't revert to pre-zoom sizes
this.chart!.dispatchAction({ type: "downplay" });
});
if (!this.options?.dataZoom) {
this.chart.getZr().on("dblclick", this._handleClickZoom);
@@ -574,7 +530,6 @@ export class HaChartBase extends LitElement {
...this._createOptions(),
series: this._getSeries(),
});
this._updateSankeyRoam();
} finally {
this._loading = false;
}
@@ -632,7 +587,13 @@ export class HaChartBase extends LitElement {
id: "dataZoom",
type: "inside",
orient: "horizontal",
filterMode: this._getDataZoomFilterMode() as any,
// "boundaryFilter" is a custom mode added via axis-proxy-patch.ts.
// It rescales the Y-axis to the visible data while keeping one point
// just outside each boundary to avoid line gaps at the zoom edges.
// Only use it for line charts — it causes issues with bar charts.
filterMode: (ensureArray(this.data).every((s) => s.type === "line")
? "boundaryFilter"
: "filter") as any,
xAxisIndex: 0,
moveOnMouseMove: !this._isTouchDevice || this._isZoomed,
preventDefaultMouseMove: !this._isTouchDevice || this._isZoomed,
@@ -640,23 +601,6 @@ export class HaChartBase extends LitElement {
};
}
// "boundaryFilter" is a custom mode added via axis-proxy-patch.ts.
// It rescales the Y-axis to the visible data while keeping one point
// just outside each boundary to avoid line gaps at the zoom edges.
// Use "filter" for bar charts since boundaryFilter causes rendering issues.
// Use "weakFilter" for other types (e.g. custom/timeline) so bars
// spanning the visible range boundary are kept.
private _getDataZoomFilterMode(): string {
const series = ensureArray(this.data);
if (series.every((s) => s.type === "line")) {
return "boundaryFilter";
}
if (series.some((s) => s.type === "bar")) {
return "filter";
}
return "weakFilter";
}
private _createOptions(): ECOption {
let xAxis = this.options?.xAxis;
if (xAxis) {
@@ -1014,26 +958,6 @@ export class HaChartBase extends LitElement {
if (!this.chart) {
return;
}
// Handle sankey chart double-click zoom
const option = this.chart.getOption();
const allSeries = option.series as any[];
const sankeySeries = allSeries?.filter((s: any) => s.type === "sankey");
if (sankeySeries?.length) {
if (this._isZoomed) {
this._handleZoomReset();
} else {
this.chart.setOption({
series: sankeySeries.map((s: any) => ({
id: s.id,
zoom: 2,
})),
});
this._isZoomed = true;
}
if (sankeySeries.length === allSeries?.length) {
return;
}
}
const range = this._isZoomed
? [0, 100]
: [
@@ -1058,37 +982,6 @@ export class HaChartBase extends LitElement {
private _handleZoomReset() {
this.chart?.dispatchAction({ type: "dataZoom", start: 0, end: 100 });
// Reset sankey roam zoom
const option = this.chart?.getOption();
const sankeySeries = (option?.series as any[])?.filter(
(s: any) => s.type === "sankey"
);
if (sankeySeries?.length) {
this.chart?.setOption({
series: sankeySeries.map((s: any) => ({
id: s.id,
zoom: 1,
center: null,
})),
});
this._isZoomed = false;
fireEvent(this, "chart-sankeyroam", { zoom: 1 });
}
}
private _updateSankeyRoam() {
const option = this.chart?.getOption();
const sankeySeries = (option?.series as any[])?.filter(
(s: any) => s.type === "sankey"
);
if (sankeySeries?.length) {
this.chart?.setOption({
series: sankeySeries.map((s: any) => ({
id: s.id,
roam: this._modifierPressed || this._isTouchDevice ? true : "move",
})),
});
}
}
private _handleDataZoomEvent(e: any) {
@@ -1132,52 +1025,11 @@ export class HaChartBase extends LitElement {
fireEvent(this, "chart-zoom", { start, end });
}
// Long-press to solo on touch/pen devices (500ms, consistent with action-handler-directive)
private _legendPointerDown(ev: PointerEvent) {
// Mouse uses Ctrl/Cmd+click instead
if (ev.pointerType === "mouse") {
return;
}
const id = (ev.currentTarget as HTMLElement)?.id;
if (!id) {
return;
}
this._longPressTriggered = false;
this._longPressTimer = setTimeout(() => {
this._longPressTriggered = true;
this._longPressTimer = undefined;
this._soloLegend(id);
}, 500);
}
private _legendPointerCancel() {
if (this._longPressTimer) {
clearTimeout(this._longPressTimer);
this._longPressTimer = undefined;
}
}
private _legendContextMenu(ev: Event) {
if (this._longPressTimer || this._longPressTriggered) {
ev.preventDefault();
}
}
private _legendClick(ev: MouseEvent) {
private _legendClick(ev: any) {
if (!this.chart) {
return;
}
if (this._longPressTriggered) {
this._longPressTriggered = false;
return;
}
const id = (ev.currentTarget as HTMLElement)?.id;
// Cmd+click on Mac (Ctrl+click is right-click there), Ctrl+click elsewhere
const soloModifier = isMac ? ev.metaKey : ev.ctrlKey;
if (soloModifier) {
this._soloLegend(id);
return;
}
const id = ev.currentTarget?.id;
if (this._hiddenDatasets.has(id)) {
this._getAllIdsFromLegend(this.options, id).forEach((i) =>
this._hiddenDatasets.delete(i)
@@ -1192,60 +1044,6 @@ export class HaChartBase extends LitElement {
this.requestUpdate("_hiddenDatasets");
}
private _soloLegend(id: string) {
const allIds = this._getAllLegendIds();
const clickedIds = this._getAllIdsFromLegend(this.options, id);
const otherIds = allIds.filter((i) => !clickedIds.includes(i));
const clickedIsOnlyVisible =
clickedIds.every((i) => !this._hiddenDatasets.has(i)) &&
otherIds.every((i) => this._hiddenDatasets.has(i));
if (clickedIsOnlyVisible) {
// Already solo'd on this item — restore all series to visible
for (const hiddenId of [...this._hiddenDatasets]) {
this._hiddenDatasets.delete(hiddenId);
fireEvent(this, "dataset-unhidden", { id: hiddenId });
}
} else {
// Solo: hide every other series, unhide clicked if it was hidden
for (const otherId of otherIds) {
if (!this._hiddenDatasets.has(otherId)) {
this._hiddenDatasets.add(otherId);
fireEvent(this, "dataset-hidden", { id: otherId });
}
}
for (const clickedId of clickedIds) {
if (this._hiddenDatasets.has(clickedId)) {
this._hiddenDatasets.delete(clickedId);
fireEvent(this, "dataset-unhidden", { id: clickedId });
}
}
}
this.requestUpdate("_hiddenDatasets");
}
private _getAllLegendIds(): string[] {
const items = this._getLegendItems();
if (!items) {
return [];
}
const allIds = new Set<string>();
for (const item of items) {
const primaryId =
typeof item === "string"
? item
: ((item.id as string) ?? (item.name as string) ?? "");
for (const expandedId of this._getAllIdsFromLegend(
this.options,
primaryId
)) {
allIds.add(expandedId);
}
}
return [...allIds];
}
private _toggleExpandedLegend() {
this.expandLegend = !this.expandLegend;
setTimeout(() => {
@@ -1459,6 +1257,5 @@ declare global {
start: number;
end: number;
};
"chart-sankeyroam": { zoom: number };
}
}

View File

@@ -64,8 +64,6 @@ export class HaSankeyChart extends LitElement {
public chart?: EChartsType;
private _currentZoom = 1;
@state() private _sizeController = new ResizeController(this, {
callback: (entries) => entries[0]?.contentRect,
});
@@ -86,13 +84,11 @@ export class HaSankeyChart extends LitElement {
} as ECOption;
return html`<ha-chart-base
.hass=${this.hass}
.data=${this._createData(this.data, this._sizeController.value?.width)}
.options=${options}
height="100%"
.extraComponents=${[SankeyChart]}
@chart-click=${this._handleChartClick}
@chart-sankeyroam=${this._handleChartSankeyRoam}
></ha-chart-base>`;
}
@@ -113,10 +109,6 @@ export class HaSankeyChart extends LitElement {
return null;
};
private _handleChartSankeyRoam = (ev: CustomEvent) => {
this._currentZoom = ev.detail.zoom;
};
private _handleChartClick = (ev: CustomEvent<ECElementEvent>) => {
const detail = ev.detail;
// Only handle node clicks (not links)
@@ -188,7 +180,6 @@ export class HaSankeyChart extends LitElement {
})),
links,
draggable: false,
scaleLimit: { min: 1, max: 4 },
orient: this.vertical ? "vertical" : "horizontal",
nodeWidth: 15,
nodeGap: NODE_GAP,
@@ -219,7 +210,7 @@ export class HaSankeyChart extends LitElement {
""
);
const wordWidth = measureTextWidth(longestWord, FONT_SIZE);
const availableWidth = (params.rect.width + 6) * this._currentZoom;
const availableWidth = params.rect.width + 6;
const fontSize = Math.min(
FONT_SIZE,
(availableWidth / wordWidth) * FONT_SIZE
@@ -232,7 +223,7 @@ export class HaSankeyChart extends LitElement {
};
}
const availableHeight = (params.rect.height + 8) * this._currentZoom; // account for the margin
const availableHeight = params.rect.height + 8; // account for the margin
const fontSize = Math.min(
(availableHeight / params.labelRect.height) * FONT_SIZE,
FONT_SIZE

View File

@@ -28,13 +28,6 @@ const safeParseFloat = (value) => {
return isFinite(parsed) ? parsed : null;
};
const CLIMATE_MODE_CONFIGS = [
{ mode: "heat", action: "heating", cssVar: "--state-climate-heat-color" },
{ mode: "cool", action: "cooling", cssVar: "--state-climate-cool-color" },
{ mode: "dry", action: "drying", cssVar: "--state-climate-dry-color" },
{ mode: "fan_only", action: "fan", cssVar: "--state-climate-fan_only-color" },
] as const;
@customElement("state-history-chart-line")
export class StateHistoryChartLine extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -436,18 +429,23 @@ export class StateHistoryChartLine extends LitElement {
(entityState) => entityState.attributes?.hvac_action
);
const activeModes = CLIMATE_MODE_CONFIGS.map(
({ mode, action, cssVar }) => {
const isActive =
domain === "climate" && hasHvacAction
? (entityState: LineChartState) =>
CLIMATE_HVAC_ACTION_TO_MODE[
entityState.attributes?.hvac_action
] === mode
: (entityState: LineChartState) => entityState.state === mode;
return { action, cssVar, isActive };
}
).filter(({ isActive }) => states.states.some(isActive));
const isHeating =
domain === "climate" && hasHvacAction
? (entityState: LineChartState) =>
CLIMATE_HVAC_ACTION_TO_MODE[
entityState.attributes?.hvac_action
] === "heat"
: (entityState: LineChartState) => entityState.state === "heat";
const isCooling =
domain === "climate" && hasHvacAction
? (entityState: LineChartState) =>
CLIMATE_HVAC_ACTION_TO_MODE[
entityState.attributes?.hvac_action
] === "cool"
: (entityState: LineChartState) => entityState.state === "cool";
const hasHeat = states.states.some(isHeating);
const hasCool = states.states.some(isCooling);
// We differentiate between thermostats that have a target temperature
// range versus ones that have just a target temperature
@@ -468,19 +466,33 @@ export class StateHistoryChartLine extends LitElement {
"component.climate.entity_component._.state_attributes.current_temperature.name"
)
);
for (const { action, cssVar } of activeModes) {
if (hasHeat) {
addDataSet(
`${states.entity_id}-${action}`,
states.entity_id + "-heating",
this.showNames
? this.hass.localize(`ui.card.climate.${action}`, {
name: name,
})
? this.hass.localize("ui.card.climate.heating", { name: name })
: this.hass.localize(
`component.climate.entity_component._.state_attributes.hvac_action.state.${action}`
"component.climate.entity_component._.state_attributes.hvac_action.state.heating"
),
computedStyles.getPropertyValue(cssVar),
computedStyles.getPropertyValue("--state-climate-heat-color"),
true
);
// The "heating" series uses steppedArea to shade the area below the current
// temperature when the thermostat is calling for heat.
}
if (hasCool) {
addDataSet(
states.entity_id + "-cooling",
this.showNames
? this.hass.localize("ui.card.climate.cooling", { name: name })
: this.hass.localize(
"component.climate.entity_component._.state_attributes.hvac_action.state.cooling"
),
computedStyles.getPropertyValue("--state-climate-cool-color"),
true
);
// The "cooling" series uses steppedArea to shade the area below the current
// temperature when the thermostat is calling for heat.
}
if (hasTargetRange) {
@@ -528,8 +540,11 @@ export class StateHistoryChartLine extends LitElement {
entityState.attributes.current_temperature
);
const series = [curTemp];
for (const { isActive } of activeModes) {
series.push(isActive(entityState) ? curTemp : null);
if (hasHeat) {
series.push(isHeating(entityState) ? curTemp : null);
}
if (hasCool) {
series.push(isCooling(entityState) ? curTemp : null);
}
if (hasTargetRange) {
const targetHigh = safeParseFloat(

View File

@@ -1,8 +1,8 @@
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { mdiRestart } from "@mdi/js";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, eventOptions, property, state } from "lit/decorators";
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { mdiRestart } from "@mdi/js";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { restoreScroll } from "../../common/decorators/restore-scroll";
import type {
@@ -12,12 +12,12 @@ import type {
} from "../../data/history";
import { loadVirtualizer } from "../../resources/virtualizer";
import type { HomeAssistant } from "../../types";
import "../ha-button";
import type { StateHistoryChartLine } from "./state-history-chart-line";
import type { StateHistoryChartTimeline } from "./state-history-chart-timeline";
import "../ha-fab";
import "../ha-svg-icon";
import "./state-history-chart-line";
import type { StateHistoryChartLine } from "./state-history-chart-line";
import "./state-history-chart-timeline";
import type { StateHistoryChartTimeline } from "./state-history-chart-timeline";
const CANVAS_TIMELINE_ROWS_CHUNK = 10; // Split up the canvases to avoid hitting the render limit
@@ -105,7 +105,7 @@ export class StateHistoryCharts extends LitElement {
@restoreScroll(".container") private _savedScrollPos?: number;
protected render() {
if (!isComponentLoaded(this.hass.config, "history")) {
if (!isComponentLoaded(this.hass, "history")) {
return html`<div class="info">
${this.hass.localize("ui.components.history_charts.history_disabled")}
</div>`;
@@ -150,14 +150,16 @@ export class StateHistoryCharts extends LitElement {
this._renderHistoryItem(item, index)
)}`}
${this.syncCharts && this._hasZoomedCharts
? html`<ha-button
size="large"
? html`<ha-fab
slot="fab"
class="reset-button"
.label=${this.hass.localize(
"ui.components.history_charts.zoom_reset"
)}
@click=${this._handleGlobalZoomReset}
>
<ha-svg-icon slot="start" .path=${mdiRestart}></ha-svg-icon>
${this.hass.localize("ui.components.history_charts.zoom_reset")}
</ha-button>`
<ha-svg-icon slot="icon" .path=${mdiRestart}></ha-svg-icon>
</ha-fab>`
: nothing}
`;
}
@@ -446,7 +448,6 @@ export class StateHistoryCharts extends LitElement {
bottom: calc(24px + var(--safe-area-inset-bottom));
right: calc(24px + var(--safe-area-inset-bottom));
z-index: 1;
--ha-button-box-shadow: var(--ha-box-shadow-l);
}
`;
}

View File

@@ -149,7 +149,7 @@ export class StatisticsChart extends LitElement {
}
protected render(): TemplateResult {
if (!isComponentLoaded(this.hass.config, "history")) {
if (!isComponentLoaded(this.hass, "history")) {
return html`<div class="info">
${this.hass.localize("ui.components.history_charts.history_disabled")}
</div>`;

View File

@@ -2,6 +2,7 @@ import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { computeCssColor } from "../../common/color/compute-color";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { stringCompare } from "../../common/string/compare";
@@ -52,15 +53,16 @@ class HaDataTableLabels extends LitElement {
}
private _renderLabel(label: LabelRegistryEntry, clickAction: boolean) {
const color = label?.color ? computeCssColor(label.color) : undefined;
return html`
<ha-label
dense
role="button"
tabindex="0"
.color=${label.color}
.item=${label}
@click=${clickAction ? this._labelClicked : undefined}
@keydown=${clickAction ? this._labelClicked : undefined}
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
${label?.icon
@@ -100,6 +102,10 @@ class HaDataTableLabels extends LitElement {
position: fixed;
flex-wrap: nowrap;
}
ha-label {
--ha-label-background-color: var(--color, var(--grey-color));
--ha-label-background-opacity: 0.5;
}
.plus {
--ha-label-background-color: transparent;
border: 1px solid var(--divider-color);

View File

@@ -1,4 +1,3 @@
import { consume, type ContextType } from "@lit/context";
import { mdiArrowDown, mdiArrowUp, mdiChevronUp } from "@mdi/js";
import deepClone from "deep-clone-simple";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
@@ -16,21 +15,15 @@ import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { STRINGS_SEPARATOR_DOT } from "../../common/const";
import { restoreScroll } from "../../common/decorators/restore-scroll";
import { deepActiveElement } from "../../common/dom/deep-active-element";
import type {
HASSDomCurrentTargetEvent,
HASSDomTargetEvent,
} from "../../common/dom/fire_event";
import { fireEvent } from "../../common/dom/fire_event";
import { stringCompare } from "../../common/string/compare";
import type { LocalizeFunc } from "../../common/translations/localize";
import { debounce } from "../../common/util/debounce";
import { groupBy } from "../../common/util/group-by";
import { nextRender } from "../../common/util/render-status";
import { internationalizationContext } from "../../data/context";
import type { FrontendLocaleData } from "../../data/translation";
import { haStyleScrollbar } from "../../resources/styles";
import { loadVirtualizer } from "../../resources/virtualizer";
import type { HomeAssistant } from "../../types";
import "../ha-checkbox";
import type { HaCheckbox } from "../ha-checkbox";
import "../ha-svg-icon";
@@ -108,13 +101,12 @@ export interface DataTableRowData {
export type SortableColumnContainer = Record<string, ClonedDataTableColumnData>;
const UNDEFINED_GROUP_KEY = "zzzzz_undefined";
const AUTO_FOCUS_ALLOWED_ACTIVE_TAGS = ["BODY", "HTML", "HOME-ASSISTANT"];
@customElement("ha-data-table")
export class HaDataTable extends LitElement {
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n?: ContextType<typeof internationalizationContext>;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public localizeFunc?: LocalizeFunc;
@property({ type: Boolean }) public narrow = false;
@@ -168,10 +160,6 @@ export class HaDataTable extends LitElement {
@query("slot[name='header']") private _header!: HTMLSlotElement;
@query(".mdc-data-table__header-row") private _headerRow?: HTMLDivElement;
@query("lit-virtualizer") private _scroller?: HTMLElement;
@state() private _collapsedGroups: string[] = [];
@state() private _lastSelectedRowId: string | null = null;
@@ -248,30 +236,16 @@ export class HaDataTable extends LitElement {
this.updateComplete.then(() => this._calcTableHeight());
}
protected updated(changedProps: PropertyValues) {
if (!this._headerRow) {
protected updated() {
const header = this.renderRoot.querySelector(".mdc-data-table__header-row");
if (!header) {
return;
}
if (this._headerRow.scrollWidth > this._headerRow.clientWidth) {
this.style.setProperty(
"--table-row-width",
`${this._headerRow.scrollWidth}px`
);
if (header.scrollWidth > header.clientWidth) {
this.style.setProperty("--table-row-width", `${header.scrollWidth}px`);
} else {
this.style.removeProperty("--table-row-width");
}
const activeElement = deepActiveElement();
if (
changedProps.has("selectable") ||
(!this.autoHeight &&
activeElement &&
AUTO_FOCUS_ALLOWED_ACTIVE_TAGS.includes(activeElement.tagName))
) {
this._focusScroller();
}
}
public willUpdate(properties: PropertyValues) {
@@ -404,6 +378,8 @@ export class HaDataTable extends LitElement {
);
protected render() {
const localize = this.localizeFunc || this.hass.localize;
const columns = this._sortedColumns(this.columns, this.columnOrder);
const renderRow = (row: DataTableRowData, index: number) =>
@@ -527,10 +503,7 @@ export class HaDataTable extends LitElement {
<div class="mdc-data-table__row" role="row">
<div class="mdc-data-table__cell grows center" role="cell">
${this.noDataText ||
this._i18n?.localize?.(
"ui.components.data-table.no-data"
) ||
"No data"}
localize("ui.components.data-table.no-data")}
</div>
</div>
</div>
@@ -539,12 +512,10 @@ export class HaDataTable extends LitElement {
<lit-virtualizer
scroller
class="mdc-data-table__content scroller ha-scrollbar"
tabindex=${ifDefined(!this.autoHeight ? "0" : undefined)}
@scroll=${this._saveScrollPos}
.items=${this._groupData(
this._filteredData,
this._i18n?.localize,
this._i18n?.locale,
localize,
this.appendRow,
this.groupColumn,
this.groupOrder,
@@ -714,7 +685,7 @@ export class HaDataTable extends LitElement {
this._sortColumns[this.sortColumn],
this.sortDirection,
this.sortColumn,
this._i18n?.locale?.language
this.hass.locale.language
)
: filteredData;
@@ -740,8 +711,7 @@ export class HaDataTable extends LitElement {
private _groupData = memoizeOne(
(
data: DataTableRowData[],
localize: LocalizeFunc | undefined,
locale: FrontendLocaleData | undefined,
localize: LocalizeFunc,
appendRow,
groupColumn: string | undefined,
groupOrder: string[] | undefined,
@@ -765,7 +735,11 @@ export class HaDataTable extends LitElement {
)
.sort((a, b) => {
if (!groupOrder && isGroupSortColumn) {
const comparison = stringCompare(a, b, locale?.language);
const comparison = stringCompare(
a,
b,
this.hass.locale.language
);
if (sortDirection === "asc") {
return comparison;
}
@@ -786,7 +760,7 @@ export class HaDataTable extends LitElement {
return stringCompare(
["", "-", "—"].includes(a) ? "zzz" : a,
["", "-", "—"].includes(b) ? "zzz" : b,
locale?.language
this.hass.locale.language
);
})
.reduce(
@@ -813,15 +787,14 @@ export class HaDataTable extends LitElement {
>
<ha-icon-button
.path=${mdiChevronUp}
.label=${localize?.(
.label=${this.hass.localize(
`ui.components.data-table.${collapsed ? "expand" : "collapse"}`
) || (collapsed ? "Expand" : "Collapse")}
)}
class=${collapsed ? "collapsed" : ""}
>
</ha-icon-button>
${groupName === UNDEFINED_GROUP_KEY
? localize?.("ui.components.data-table.ungrouped") ||
"Ungrouped"
? localize("ui.components.data-table.ungrouped")
: groupName || ""}
</div>`,
});
@@ -852,10 +825,8 @@ export class HaDataTable extends LitElement {
): Promise<DataTableRowData[]> => filterData(data, columns, filter)
);
private _handleHeaderClick(
ev: HASSDomCurrentTargetEvent<HTMLElement & { columnId: string }>
) {
const columnId = ev.currentTarget.columnId;
private _handleHeaderClick(ev: Event) {
const columnId = (ev.currentTarget as any).columnId;
if (!this.columns[columnId].sortable) {
return;
}
@@ -873,12 +844,11 @@ export class HaDataTable extends LitElement {
column: columnId,
direction: this.sortDirection,
});
this._focusScroller();
}
private _handleHeaderRowCheckboxClick(ev: HASSDomTargetEvent<HaCheckbox>) {
if (ev.target.checked) {
private _handleHeaderRowCheckboxClick(ev: Event) {
const checkbox = ev.target as HaCheckbox;
if (checkbox.checked) {
this.selectAll();
} else {
this._checkedRows = [];
@@ -887,15 +857,13 @@ export class HaDataTable extends LitElement {
this._lastSelectedRowId = null;
}
private _handleRowCheckboxClicked = (
ev: HASSDomCurrentTargetEvent<HaCheckbox & { rowId: string }>
) => {
const rowId = ev.currentTarget.rowId;
private _handleRowCheckboxClicked = (ev: Event) => {
const checkbox = ev.currentTarget as HaCheckbox;
const rowId = (checkbox as any).rowId;
const groupedData = this._groupData(
this._filteredData,
this._i18n?.localize,
this._i18n?.locale,
this.localizeFunc || this.hass.localize,
this.appendRow,
this.groupColumn,
this.groupOrder,
@@ -927,7 +895,7 @@ export class HaDataTable extends LitElement {
...this._selectRange(groupedData, lastSelectedRowIndex, rowIndex),
];
}
} else if (!ev.currentTarget.checked) {
} else if (!checkbox.checked) {
if (!this._checkedRows.includes(rowId)) {
this._checkedRows = [...this._checkedRows, rowId];
}
@@ -965,9 +933,7 @@ export class HaDataTable extends LitElement {
return checkedRows;
}
private _handleRowClick = (
ev: HASSDomCurrentTargetEvent<HTMLElement & { rowId: string }>
) => {
private _handleRowClick = (ev: Event) => {
if (
ev
.composedPath()
@@ -983,13 +949,14 @@ export class HaDataTable extends LitElement {
) {
return;
}
const rowId = ev.currentTarget.rowId;
const rowId = (ev.currentTarget as any).rowId;
fireEvent(this, "row-click", { id: rowId }, { bubbles: false });
};
private _setTitle(ev: HASSDomCurrentTargetEvent<HTMLElement>) {
if (ev.currentTarget.scrollWidth > ev.currentTarget.offsetWidth) {
ev.currentTarget.setAttribute("title", ev.currentTarget.innerText);
private _setTitle(ev: Event) {
const target = ev.currentTarget as HTMLElement;
if (target.scrollWidth > target.offsetWidth) {
target.setAttribute("title", target.innerText);
}
}
@@ -1011,12 +978,6 @@ export class HaDataTable extends LitElement {
this._debounceSearch((ev.target as HTMLInputElement).value);
}
private _focusScroller(): void {
this._scroller?.focus({
preventScroll: true,
});
}
private async _calcTableHeight() {
if (this.autoHeight) {
return;
@@ -1026,27 +987,23 @@ export class HaDataTable extends LitElement {
}
@eventOptions({ passive: true })
private _saveScrollPos(e: HASSDomTargetEvent<HTMLDivElement>) {
this._savedScrollPos = e.target.scrollTop;
private _saveScrollPos(e: Event) {
this._savedScrollPos = (e.target as HTMLDivElement).scrollTop;
if (this._headerRow) {
this._headerRow.scrollLeft = e.target.scrollLeft;
}
this.renderRoot.querySelector(".mdc-data-table__header-row")!.scrollLeft = (
e.target as HTMLDivElement
).scrollLeft;
}
@eventOptions({ passive: true })
private _scrollContent(e: HASSDomTargetEvent<HTMLDivElement>) {
if (!this._scroller) {
return;
}
this._scroller.scrollLeft = e.target.scrollLeft;
private _scrollContent(e: Event) {
this.renderRoot.querySelector("lit-virtualizer")!.scrollLeft = (
e.target as HTMLDivElement
).scrollLeft;
}
private _collapseGroup = (
ev: HASSDomCurrentTargetEvent<HTMLElement & { group: string }>
) => {
const groupName = ev.currentTarget.group;
private _collapseGroup = (ev: Event) => {
const groupName = (ev.currentTarget as any).group;
if (this._collapsedGroups.includes(groupName)) {
this._collapsedGroups = this._collapsedGroups.filter(
(grp) => grp !== groupName
@@ -1469,11 +1426,6 @@ export class HaDataTable extends LitElement {
contain: size layout !important;
overscroll-behavior: contain;
}
lit-virtualizer:focus,
lit-virtualizer:focus-visible {
outline: none;
}
`,
];
}

View File

@@ -3,7 +3,6 @@ import { consume, type ContextType } from "@lit/context";
import type { ActionDetail } from "@material/mwc-list";
import { mdiCalendarToday } from "@mdi/js";
import "cally";
import type { HassConfig } from "home-assistant-js-websocket/dist/types";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, queryAll, state } from "lit/decorators";
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
@@ -13,13 +12,16 @@ import {
formatDateYear,
formatISODateOnly,
} from "../../common/datetime/format_date";
import { transform } from "../../common/decorators/transform";
import { fireEvent } from "../../common/dom/fire_event";
import { configContext, internationalizationContext } from "../../data/context";
import {
configContext,
localeContext,
localizeContext,
} from "../../data/context";
import { TimeZone } from "../../data/translation";
import { MobileAwareMixin } from "../../mixins/mobile-aware-mixin";
import { haStyleScrollbar } from "../../resources/styles";
import type { HomeAssistantConfig, ValueChangedEvent } from "../../types";
import type { ValueChangedEvent } from "../../types";
import "../chips/ha-chip-set";
import "../chips/ha-filter-chip";
import type { HaFilterChip } from "../chips/ha-filter-chip";
@@ -46,15 +48,16 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
public timePicker = false;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@consume({ context: localizeContext, subscribe: true })
private localize!: ContextType<typeof localizeContext>;
@state()
@consume({ context: localeContext, subscribe: true })
private locale!: ContextType<typeof localeContext>;
@state()
@consume({ context: configContext, subscribe: true })
@transform<HomeAssistantConfig, HassConfig>({
transformer: ({ config }) => config,
})
private _hassConfig!: HassConfig;
private hassConfig!: ContextType<typeof configContext>;
/** used to show month in calendar-range header */
@state() private _pickerMonth?: string;
@@ -84,20 +87,12 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
? formatCallyDateRange(
this.startDate,
this.endDate,
this._i18n?.locale,
this._hassConfig
this.locale,
this.hassConfig
)
: undefined;
this._pickerMonth = formatDateMonth(
date,
this._i18n.locale,
this._hassConfig
);
this._pickerYear = formatDateYear(
date,
this._i18n.locale,
this._hassConfig
);
this._pickerMonth = formatDateMonth(date, this.locale, this.hassConfig);
this._pickerYear = formatDateYear(date, this.locale, this.hassConfig);
if (this.timePicker && this.startDate && this.endDate) {
this._timeValue = {
@@ -149,12 +144,12 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
<div class="range">
<calendar-range
.value=${this._dateValue}
.locale=${this._i18n.locale.language}
.locale=${this.locale.language}
.focusedDate=${this._focusDate}
@focusday=${this._focusChanged}
@change=${this._handleChange}
show-outside-days
.firstDayOfWeek=${firstWeekdayIndex(this._i18n.locale)}
.firstDayOfWeek=${firstWeekdayIndex(this.locale)}
>
<ha-icon-button-prev
tabindex="-1"
@@ -167,7 +162,7 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
<ha-icon-button
@click=${this._focusToday}
.path=${mdiCalendarToday}
.label=${this._i18n.localize("ui.dialogs.date-picker.today")}
.label=${this.localize("ui.dialogs.date-picker.today")}
></ha-icon-button>
</div>
<ha-icon-button-next
@@ -181,9 +176,9 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
<div class="times">
<ha-time-input
.value=${`${this._timeValue.from.hours}:${this._timeValue.from.minutes}`}
.locale=${this._i18n.locale}
.locale=${this.locale}
@value-changed=${this._handleChangeTime}
.label=${this._i18n.localize(
.label=${this.localize(
"ui.components.date-range-picker.time_from"
)}
id="from"
@@ -192,9 +187,9 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
></ha-time-input>
<ha-time-input
.value=${`${this._timeValue.to.hours}:${this._timeValue.to.minutes}`}
.locale=${this._i18n.locale}
.locale=${this.locale}
@value-changed=${this._handleChangeTime}
.label=${this._i18n.localize(
.label=${this.localize(
"ui.components.date-range-picker.time_to"
)}
id="to"
@@ -208,33 +203,19 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
</div>
<div class="footer">
<ha-button appearance="plain" @click=${this._cancel}
>${this._i18n.localize("ui.common.cancel")}</ha-button
>${this.localize("ui.common.cancel")}</ha-button
>
<ha-button .disabled=${!this._dateValue} @click=${this._save}
>${this._i18n.localize(
"ui.components.date-range-picker.select"
)}</ha-button
>${this.localize("ui.components.date-range-picker.select")}</ha-button
>
</div>`;
}
private _focusToday() {
const date = new Date();
this._focusDate = formatISODateOnly(
date,
this._i18n.locale,
this._hassConfig
);
this._pickerMonth = formatDateMonth(
date,
this._i18n.locale,
this._hassConfig
);
this._pickerYear = formatDateYear(
date,
this._i18n.locale,
this._hassConfig
);
this._focusDate = formatISODateOnly(date, this.locale, this.hassConfig);
this._pickerMonth = formatDateMonth(date, this.locale, this.hassConfig);
this._pickerYear = formatDateYear(date, this.locale, this.hassConfig);
}
private _cancel() {
@@ -274,12 +255,12 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
}
}
if (this._i18n.locale.time_zone === TimeZone.server) {
if (this.locale.time_zone === TimeZone.server) {
startDate = new Date(
new TZDate(startDate, this._hassConfig.time_zone).getTime()
new TZDate(startDate, this.hassConfig.time_zone).getTime()
);
endDate = new Date(
new TZDate(endDate, this._hassConfig.time_zone).getTime()
new TZDate(endDate, this.hassConfig.time_zone).getTime()
);
}
@@ -305,16 +286,8 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
private _focusChanged(ev: CustomEvent<Date>) {
const date = ev.detail;
this._pickerMonth = formatDateMonth(
date,
this._i18n.locale,
this._hassConfig
);
this._pickerYear = formatDateYear(
date,
this._i18n.locale,
this._hassConfig
);
this._pickerMonth = formatDateMonth(date, this.locale, this.hassConfig);
this._pickerYear = formatDateYear(date, this.locale, this.hassConfig);
this._focusDate = undefined;
}

View File

@@ -3,7 +3,6 @@ import { consume, type ContextType } from "@lit/context";
import { mdiCalendar } from "@mdi/js";
import "cally";
import { isThisYear } from "date-fns";
import type { HassConfig } from "home-assistant-js-websocket/dist/types";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
@@ -15,10 +14,12 @@ import {
formatShortDateTime,
formatShortDateTimeWithYear,
} from "../../common/datetime/format_date_time";
import { transform } from "../../common/decorators/transform";
import { fireEvent } from "../../common/dom/fire_event";
import { configContext, internationalizationContext } from "../../data/context";
import type { HomeAssistantConfig } from "../../types";
import {
configContext,
localeContext,
localizeContext,
} from "../../data/context";
import "../ha-bottom-sheet";
import "../ha-icon-button";
import "../ha-icon-button-next";
@@ -42,15 +43,16 @@ const EXTENDED_RANGE_KEYS: DateRange[] = [
@customElement("ha-date-range-picker")
export class HaDateRangePicker extends LitElement {
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@consume({ context: localizeContext, subscribe: true })
private localize!: ContextType<typeof localizeContext>;
@state()
@consume({ context: localeContext, subscribe: true })
private locale!: ContextType<typeof localeContext>;
@state()
@consume({ context: configContext, subscribe: true })
@transform<HomeAssistantConfig, HassConfig>({
transformer: ({ config }) => config,
})
private _hassConfig!: HassConfig;
private hassConfig!: ContextType<typeof configContext>;
@property({ attribute: false }) public startDate!: Date;
@@ -115,8 +117,8 @@ export class HaDateRangePicker extends LitElement {
this._ranges = {};
rangeKeys.forEach((key) => {
this._ranges![
this._i18n.localize(`ui.components.date-range-picker.ranges.${key}`)
] = calcDateRange(this._i18n.locale, this._hassConfig, key);
this.localize(`ui.components.date-range-picker.ranges.${key}`)
] = calcDateRange(this.locale, this.hassConfig, key);
});
}
@@ -131,50 +133,47 @@ export class HaDateRangePicker extends LitElement {
${!this.minimal
? html`<ha-textarea
id="field"
rows="1"
resize="auto"
mobile-multiline
@click=${this._openPicker}
@keydown=${this._handleKeydown}
.value=${(isThisYear(this.startDate)
? formatShortDateTime(
this.startDate,
this._i18n.locale,
this._hassConfig
this.locale,
this.hassConfig
)
: formatShortDateTimeWithYear(
this.startDate,
this._i18n.locale,
this._hassConfig
this.locale,
this.hassConfig
)) +
(window.innerWidth >= 459 ? " - " : " - \n") +
(isThisYear(this.endDate)
? formatShortDateTime(
this.endDate,
this._i18n.locale,
this._hassConfig
this.locale,
this.hassConfig
)
: formatShortDateTimeWithYear(
this.endDate,
this._i18n.locale,
this._hassConfig
this.locale,
this.hassConfig
))}
.label=${this._i18n.localize(
.label=${this.localize(
"ui.components.date-range-picker.start_date"
) +
" - " +
this._i18n.localize(
"ui.components.date-range-picker.end_date"
)}
this.localize("ui.components.date-range-picker.end_date")}
.disabled=${this.disabled}
readonly
></ha-textarea>
<ha-icon-button-prev
.label=${this._i18n.localize("ui.common.previous")}
.label=${this.localize("ui.common.previous")}
@click=${this._handlePrev}
>
</ha-icon-button-prev>
<ha-icon-button-next
.label=${this._i18n.localize("ui.common.next")}
.label=${this.localize("ui.common.next")}
@click=${this._handleNext}
>
</ha-icon-button-next>`
@@ -182,7 +181,7 @@ export class HaDateRangePicker extends LitElement {
@click=${this._openPicker}
.disabled=${this.disabled}
id="field"
.label=${this._i18n.localize(
.label=${this.localize(
"ui.components.date-range-picker.select_date_range"
)}
.path=${mdiCalendar}
@@ -290,8 +289,8 @@ export class HaDateRangePicker extends LitElement {
this.startDate,
this.endDate,
forward,
this._i18n.locale,
this._hassConfig
this.locale,
this.hassConfig
);
this.startDate = start;
this.endDate = end;
@@ -337,7 +336,14 @@ export class HaDateRangePicker extends LitElement {
private _setTextareaFocusStyle(focused: boolean) {
const textarea = this.renderRoot.querySelector("ha-textarea");
if (textarea) {
textarea.setFocused(focused);
const foundation = (textarea as any).mdcFoundation;
if (foundation) {
if (focused) {
foundation.activateFocus();
} else {
foundation.deactivateFocus();
}
}
}
}

View File

@@ -2,7 +2,6 @@ import "@home-assistant/webawesome/dist/components/divider/divider";
import { consume, type ContextType } from "@lit/context";
import { mdiBackspace, mdiCalendarToday } from "@mdi/js";
import "cally";
import type { HassConfig } from "home-assistant-js-websocket/dist/types";
import { css, html, LitElement, nothing } from "lit";
import { customElement, state } from "lit/decorators";
import {
@@ -11,10 +10,12 @@ import {
formatDateYear,
formatISODateOnly,
} from "../../common/datetime/format_date";
import { transform } from "../../common/decorators/transform";
import { configContext, internationalizationContext } from "../../data/context";
import {
configContext,
localeContext,
localizeContext,
} from "../../data/context";
import { DialogMixin } from "../../dialogs/dialog-mixin";
import type { HomeAssistantConfig } from "../../types";
import "../ha-button";
import type { DatePickerDialogParams } from "../ha-date-input";
import "../ha-dialog";
@@ -39,15 +40,16 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
LitElement
) {
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@consume({ context: localizeContext, subscribe: true })
private localize!: ContextType<typeof localizeContext>;
@state()
@consume({ context: localeContext, subscribe: true })
private locale!: ContextType<typeof localeContext>;
@state()
@consume({ context: configContext, subscribe: true })
@transform<HomeAssistantConfig, HassConfig>({
transformer: ({ config }) => config,
})
private _hassConfig!: HassConfig;
private hassConfig!: ContextType<typeof configContext>;
@state() private _value?: {
year: string;
@@ -72,26 +74,14 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
? new Date(`${this.params.value.split("T")[0]}T00:00:00`)
: new Date();
this._pickerYear = formatDateYear(
date,
this._i18n.locale,
this._hassConfig
);
this._pickerMonth = formatDateMonth(
date,
this._i18n.locale,
this._hassConfig
);
this._pickerYear = formatDateYear(date, this.locale, this.hassConfig);
this._pickerMonth = formatDateMonth(date, this.locale, this.hassConfig);
this._value = this.params.value
? {
year: this._pickerYear,
title: formatDateShort(date, this._i18n.locale, this._hassConfig),
dateString: formatISODateOnly(
date,
this._i18n.locale,
this._hassConfig
),
title: formatDateShort(date, this.locale, this.hassConfig),
dateString: formatISODateOnly(date, this.locale, this.hassConfig),
}
: undefined;
}
@@ -105,7 +95,7 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
open
width="small"
.headerTitle=${this._value?.title ||
this._i18n.localize("ui.dialogs.date-picker.title")}
this.localize("ui.dialogs.date-picker.title")}
.headerSubtitle=${this._value?.year}
header-subtitle-position="above"
>
@@ -113,7 +103,7 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
? html`
<ha-icon-button
.path=${mdiBackspace}
.label=${this._i18n.localize("ui.dialogs.date-picker.clear")}
.label=${this.localize("ui.dialogs.date-picker.clear")}
slot="headerActionItems"
@click=${this._clear}
></ha-icon-button>
@@ -141,7 +131,7 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
<ha-icon-button
@click=${this._setToday}
.path=${mdiCalendarToday}
.label=${this._i18n.localize("ui.dialogs.date-picker.today")}
.label=${this.localize("ui.dialogs.date-picker.today")}
></ha-icon-button>
</div>
<ha-icon-button-next tabindex="-1" slot="next"></ha-icon-button-next>
@@ -153,10 +143,10 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
slot="secondaryAction"
@click=${this.closeDialog}
>
${this._i18n.localize("ui.common.cancel")}
${this.localize("ui.common.cancel")}
</ha-button>
<ha-button slot="primaryAction" @click=${this._setValue}>
${this._i18n.localize("ui.common.ok")}
${this.localize("ui.common.ok")}
</ha-button>
</ha-dialog-footer>
</ha-dialog>`;
@@ -174,39 +164,23 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
? new Date(`${value.split("T")[0]}T00:00:00`)
: new Date();
this._value = {
year: formatDateYear(date, this._i18n.locale, this._hassConfig),
title: formatDateShort(date, this._i18n.locale, this._hassConfig),
year: formatDateYear(date, this.locale, this.hassConfig),
title: formatDateShort(date, this.locale, this.hassConfig),
dateString:
value || formatISODateOnly(date, this._i18n.locale, this._hassConfig),
value || formatISODateOnly(date, this.locale, this.hassConfig),
};
if (setFocusDay) {
this._focusDate = this._value.dateString;
this._pickerMonth = formatDateMonth(
date,
this._i18n.locale,
this._hassConfig
);
this._pickerYear = formatDateYear(
date,
this._i18n.locale,
this._hassConfig
);
this._pickerMonth = formatDateMonth(date, this.locale, this.hassConfig);
this._pickerYear = formatDateYear(date, this.locale, this.hassConfig);
}
}
private _focusChanged(ev: CustomEvent<Date>) {
const date = ev.detail;
this._pickerMonth = formatDateMonth(
date,
this._i18n.locale,
this._hassConfig
);
this._pickerYear = formatDateYear(
date,
this._i18n.locale,
this._hassConfig
);
this._pickerMonth = formatDateMonth(date, this.locale, this.hassConfig);
this._pickerYear = formatDateYear(date, this.locale, this.hassConfig);
this._focusDate = undefined;
}

View File

@@ -6,7 +6,7 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeAreaName } from "../../common/entity/compute_area_name";
import { computeDeviceName } from "../../common/entity/compute_device_name";
import { getDeviceArea } 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 {
deviceComboBoxKeys,
@@ -14,11 +14,11 @@ import {
type DevicePickerItem,
} from "../../data/device/device_picker";
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
import type { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url";
import "../ha-generic-picker";
import type { HaGenericPicker } from "../ha-generic-picker";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
export type HaDevicePickerDeviceFilterFunc = (
device: DeviceRegistryEntry
@@ -154,7 +154,7 @@ export class HaDevicePicker extends LitElement {
return html`<span slot="headline">${deviceId}</span>`;
}
const area = getDeviceArea(device, this.hass.areas);
const { area } = getDeviceContext(device, this.hass);
const deviceName = device ? computeDeviceName(device) : undefined;
const areaName = area ? computeAreaName(area) : undefined;

View File

@@ -38,8 +38,6 @@ export class HaEntityStatePicker extends LitElement {
@property() public helper?: string;
@property({ attribute: "no-entity", type: Boolean }) public noEntity = false;
private _getItems = memoizeOne(
(
hass: HomeAssistant,
@@ -126,8 +124,7 @@ export class HaEntityStatePicker extends LitElement {
<ha-generic-picker
.hass=${this.hass}
.allowCustomValue=${this.allowCustomValue}
.disabled=${this.disabled ||
(!this.entityId && this.noEntity === false)}
.disabled=${this.disabled || !this.entityId}
.autofocus=${this.autofocus}
.required=${this.required}
.label=${this.label ??

View File

@@ -94,7 +94,7 @@ class HaAddonPicker extends LitElement {
private async _getApps() {
try {
if (isComponentLoaded(this.hass.config, "hassio")) {
if (isComponentLoaded(this.hass, "hassio")) {
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
this._addons = addonsInfo.addons
.filter((addon) => addon.version)

View File

@@ -8,7 +8,6 @@ import {
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import type { LocalizeFunc } from "../common/translations/localize";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-icon-button";
import "./ha-svg-icon";
@@ -39,8 +38,6 @@ class HaAlert extends LitElement {
@property({ type: Boolean }) public dismissable = false;
@property({ attribute: false }) public localize?: LocalizeFunc;
@property({ type: Boolean }) public narrow = false;
public render() {
@@ -68,7 +65,7 @@ class HaAlert extends LitElement {
${this.dismissable
? html`<ha-icon-button
@click=${this._dismissClicked}
.label=${this.localize!("ui.common.dismiss_alert")}
label="Dismiss alert"
.path=${mdiClose}
></ha-icon-button>`
: nothing}

View File

@@ -267,6 +267,7 @@ export class HaAreaControlsPicker extends LitElement {
: item.domain
? html`<ha-domain-icon
slot="start"
.hass=${this.hass}
.domain=${item.domain}
.deviceClass=${item.deviceClass}
></ha-domain-icon>`

View File

@@ -27,7 +27,7 @@ export type Appearance = "accent" | "filled" | "outlined" | "plain";
* @cssprop --ha-button-height - The height of the button.
* @cssprop --ha-button-border-radius - The border radius of the button. defaults to `var(--ha-border-radius-pill)`.
*
* @attr {("small"|"medium"|"large")} size - Sets the button size.
* @attr {("small"|"medium")} size - Sets the button size.
* @attr {("brand"|"neutral"|"danger"|"warning"|"success")} variant - Sets the button color variant. "primary" is default.
* @attr {("accent"|"filled"|"plain")} appearance - Sets the button appearance.
* @attr {boolean} loading - shows a loading indicator instead of the buttons label and disable buttons click.
@@ -62,7 +62,6 @@ export class HaButton extends Button {
transition: background-color var(--ha-animation-duration-fast)
ease-out;
text-wrap: wrap;
box-shadow: var(--ha-button-box-shadow);
}
:host([size="small"]) .button {
@@ -74,14 +73,6 @@ export class HaButton extends Button {
--wa-form-control-padding-inline: var(--ha-space-3);
}
:host([size="large"]) .button {
--wa-form-control-height: var(
--ha-button-height,
var(--button-height, 48px)
);
font-size: var(--ha-font-size-l);
}
:host([variant="brand"]) {
--button-color-fill-normal-active: var(
--ha-color-fill-primary-normal-active

View File

@@ -7,7 +7,7 @@ import memoizeOne from "memoize-one";
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeKeys } from "../common/translations/localize";
import { internationalizationContext } from "../data/context";
import { localizeContext } from "../data/context";
import type { UiColorExtraOption } from "../data/selector";
import type { ValueChangedEvent } from "../types";
import "./ha-combo-box-item";
@@ -55,8 +55,8 @@ export class HaColorPicker extends LitElement {
@property({ type: Boolean }) public required = false;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@consume({ context: localizeContext, subscribe: true })
private localize!: ContextType<typeof localizeContext>;
render() {
const effectiveValue = this.value ?? this.defaultColor ?? "";
@@ -73,7 +73,7 @@ export class HaColorPicker extends LitElement {
.rowRenderer=${this._rowRenderer}
.valueRenderer=${this._valueRenderer}
@value-changed=${this._valueChanged}
.notFoundLabel=${this._i18n?.localize?.(
.notFoundLabel=${this.localize?.(
"ui.components.color-picker.no_colors_found"
)}
.getAdditionalItems=${this._getAdditionalItems}
@@ -103,7 +103,7 @@ export class HaColorPicker extends LitElement {
{
id: searchString,
primary:
this._i18n?.localize?.("ui.components.color-picker.custom_color") ||
this.localize?.("ui.components.color-picker.custom_color") ||
"Custom color",
secondary: searchString,
},
@@ -130,15 +130,14 @@ export class HaColorPicker extends LitElement {
const items: PickerComboBoxItem[] = [];
const defaultSuffix =
this._i18n?.localize?.("ui.components.color-picker.default") ||
"Default";
this.localize?.("ui.components.color-picker.default") || "Default";
const addDefaultSuffix = (label: string, isDefault: boolean) =>
isDefault && defaultSuffix ? `${label} (${defaultSuffix})` : label;
if (includeNone) {
const noneLabel =
this._i18n?.localize?.("ui.components.color-picker.none") || "None";
this.localize?.("ui.components.color-picker.none") || "None";
items.push({
id: "none",
primary: addDefaultSuffix(noneLabel, defaultColor === "none"),
@@ -148,7 +147,7 @@ export class HaColorPicker extends LitElement {
if (includeState) {
const stateLabel =
this._i18n?.localize?.("ui.components.color-picker.state") || "State";
this.localize?.("ui.components.color-picker.state") || "State";
items.push({
id: "state",
primary: addDefaultSuffix(stateLabel, defaultColor === "state"),
@@ -171,7 +170,7 @@ export class HaColorPicker extends LitElement {
Array.from(THEME_COLORS).forEach((color) => {
const themeLabel =
this._i18n?.localize?.(
this.localize?.(
`ui.components.color-picker.colors.${color}` as LocalizeKeys
) || color;
items.push({
@@ -228,7 +227,7 @@ export class HaColorPicker extends LitElement {
return html`
<ha-svg-icon slot="start" .path=${mdiInvertColorsOff}></ha-svg-icon>
<span slot="headline">
${this._i18n?.localize?.("ui.components.color-picker.none") || "None"}
${this.localize?.("ui.components.color-picker.none") || "None"}
</span>
`;
}
@@ -236,8 +235,7 @@ export class HaColorPicker extends LitElement {
return html`
<ha-svg-icon slot="start" .path=${mdiPalette}></ha-svg-icon>
<span slot="headline">
${this._i18n?.localize?.("ui.components.color-picker.state") ||
"State"}
${this.localize?.("ui.components.color-picker.state") || "State"}
</span>
`;
}
@@ -245,7 +243,7 @@ export class HaColorPicker extends LitElement {
const extraOption = this.extraOptions?.find((o) => o.value === value);
const label =
extraOption?.label ||
this._i18n?.localize?.(
this.localize?.(
`ui.components.color-picker.colors.${value}` as LocalizeKeys
) ||
value;

View File

@@ -79,6 +79,7 @@ class HaConfigEntryPicker extends LitElement {
<span slot="supporting-text">${item.secondary}</span>
<ha-domain-icon
slot="start"
.hass=${this.hass}
.domain=${item.icon!}
brand-fallback
></ha-domain-icon>
@@ -114,6 +115,7 @@ class HaConfigEntryPicker extends LitElement {
slot="headline"
>${item?.icon
? html`<ha-domain-icon
.hass=${this.hass}
.domain=${item.icon!}
brand-fallback
></ha-domain-icon>`

View File

@@ -123,9 +123,6 @@ export class HaDateInput extends LitElement {
}
static styles = css`
:host {
min-width: 0px;
}
ha-svg-icon {
color: var(--secondary-text-color);
}

View File

@@ -14,7 +14,7 @@ import { ifDefined } from "lit/directives/if-defined";
import type { HASSDomEvent } from "../common/dom/fire_event";
import { fireEvent } from "../common/dom/fire_event";
import { withViewTransition } from "../common/util/view-transition";
import { internationalizationContext } from "../data/context";
import { localizeContext } from "../data/context";
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
import { haStyleScrollbar } from "../resources/styles";
import "./ha-dialog-header";
@@ -123,13 +123,13 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
@query(".body") public bodyContainer!: HTMLDivElement;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@consume({ context: localizeContext, subscribe: true })
private localize!: ContextType<typeof localizeContext>;
// disabled till iOS app fix the "focus_element" implementation
// @state()
// @consume({ context: configContext, subscribe: true })
// private _hassConfig?: ContextType<typeof configContext>;
// @consume({ context: authContext, subscribe: true })
// private auth?: ContextType<typeof authContext>;
@state()
private _bodyScrolled = false;
@@ -184,7 +184,7 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
<slot name="headerNavigationIcon" slot="navigationIcon">
<ha-icon-button
data-dialog="close"
.label=${this._i18n?.localize("ui.common.close") ?? "Close"}
.label=${this.localize?.("ui.common.close") ?? "Close"}
.path=${mdiClose}
></ha-icon-button>
</slot>
@@ -222,13 +222,13 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
requestAnimationFrame(() => {
// disabled till iOS app fix the "focus_element" implementation
// if (this._hassConfig?.auth.external && isIosApp(this._hassConfig.auth.external)) {
// if (this.auth?.external && isIosApp(this.auth.external)) {
// const element = this.querySelector("[autofocus]");
// if (element !== null) {
// if (!element.id) {
// element.id = "ha-dialog-autofocus";
// }
// this._hassConfig.auth.external.fireMessage({
// this.auth.external.fireMessage({
// type: "focus_element",
// payload: {
// element_id: element.id,

View File

@@ -1,18 +1,19 @@
import { consume, type ContextType } from "@lit/context";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { until } from "lit/directives/until";
import { configContext, connectionContext, uiContext } from "../data/context";
import {
DEFAULT_DOMAIN_ICON,
domainIcon,
FALLBACK_DOMAIN_ICONS,
} from "../data/icons";
import type { HomeAssistant } from "../types";
import { brandsUrl } from "../util/brands-url";
import "./ha-icon";
@customElement("ha-domain-icon")
export class HaDomainIcon extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public domain?: string;
@property({ attribute: false }) public deviceClass?: string;
@@ -24,18 +25,6 @@ export class HaDomainIcon extends LitElement {
@property({ attribute: "brand-fallback", type: Boolean })
public brandFallback?: boolean;
@state()
@consume({ context: configContext, subscribe: true })
private _hassConfig?: ContextType<typeof configContext>;
@state()
@consume({ context: connectionContext, subscribe: true })
private _connection?: ContextType<typeof connectionContext>;
@state()
@consume({ context: uiContext, subscribe: true })
private _hassUi?: ContextType<typeof uiContext>;
protected render() {
if (this.icon) {
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
@@ -45,13 +34,12 @@ export class HaDomainIcon extends LitElement {
return nothing;
}
if (!this._connection || !this._hassConfig) {
if (!this.hass) {
return this._renderFallback();
}
const icon = domainIcon(
this._connection.connection,
this._hassConfig.config,
this.hass,
this.domain,
this.deviceClass,
this.state
@@ -77,9 +65,9 @@ export class HaDomainIcon extends LitElement {
{
domain: this.domain!,
type: "icon",
darkOptimized: this._hassUi?.themes.darkMode,
darkOptimized: this.hass.themes?.darkMode,
},
this._hassConfig?.auth.data.hassUrl
this.hass.auth.data.hassUrl
);
return html`
<img

60
src/components/ha-fab.ts Normal file
View File

@@ -0,0 +1,60 @@
import { FabBase } from "@material/mwc-fab/mwc-fab-base";
import { styles } from "@material/mwc-fab/mwc-fab.css";
import { customElement } from "lit/decorators";
import { css } from "lit";
import { mainWindow } from "../common/dom/get_main_window";
@customElement("ha-fab")
export class HaFab extends FabBase {
protected firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)");
}
static override styles = [
styles,
css`
:host {
--mdc-typography-button-text-transform: none;
--mdc-typography-button-font-size: var(--ha-font-size-l);
--mdc-typography-button-font-family: var(--ha-font-family-body);
--mdc-typography-button-font-weight: var(--ha-font-weight-medium);
}
:host .mdc-fab--extended {
border-radius: var(
--ha-button-border-radius,
var(--ha-border-radius-pill)
);
}
:host .mdc-fab.mdc-fab--extended .ripple {
border-radius: var(
--ha-button-border-radius,
var(--ha-border-radius-pill)
);
}
:host .mdc-fab--extended .mdc-fab__icon {
margin-inline-start: -8px;
margin-inline-end: 12px;
direction: var(--direction);
}
:disabled {
--mdc-theme-secondary: var(--disabled-text-color);
cursor: not-allowed !important;
}
`,
// safari workaround - must be explicit
mainWindow.document.dir === "rtl"
? css`
:host .mdc-fab--extended .mdc-fab__icon {
direction: rtl;
}
`
: css``,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-fab": HaFab;
}
}

View File

@@ -1,3 +1,4 @@
import "@material/mwc-linear-progress/mwc-linear-progress";
import { mdiDelete, mdiFileUpload } from "@mdi/js";
import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
@@ -11,7 +12,6 @@ import type { HomeAssistant } from "../types";
import { bytesToString } from "../util/bytes-to-string";
import "./ha-button";
import "./ha-icon-button";
import "./progress/ha-progress-bar";
declare global {
interface HASSDomEvents {
@@ -100,11 +100,10 @@ export class HaFileUpload extends LitElement {
</div>`
: nothing}
</div>
<ha-progress-bar
<mwc-linear-progress
.indeterminate=${!this.progress}
.value=${this.progress}
loading
></ha-progress-bar>
.progress=${this.progress ? this.progress / 100 : undefined}
></mwc-linear-progress>
</div>`
: html`<label
for=${this.value ? "" : "input"}
@@ -320,7 +319,7 @@ export class HaFileUpload extends LitElement {
--mdc-button-outline-color: var(--primary-color);
--ha-icon-button-size: 24px;
}
ha-progress-bar {
mwc-linear-progress {
width: 100%;
padding: 8px 32px;
box-sizing: border-box;

View File

@@ -101,11 +101,7 @@ export class HaFilterDevices extends LitElement {
.value=${device.id}
.selected=${this.value?.includes(device.id) ?? false}
>
${computeDeviceNameDisplay(
device,
this.hass.localize,
this.hass.states
)}
${computeDeviceNameDisplay(device, this.hass)}
</ha-check-list-item>`;
private _handleItemClick(ev) {
@@ -155,18 +151,14 @@ export class HaFilterDevices extends LitElement {
.filter(
(device) =>
!filter ||
computeDeviceNameDisplay(
device,
this.hass.localize,
this.hass.states
)
computeDeviceNameDisplay(device, this.hass)
.toLowerCase()
.includes(filter)
)
.sort((a, b) =>
stringCompare(
computeDeviceNameDisplay(a, this.hass.localize, this.hass.states),
computeDeviceNameDisplay(b, this.hass.localize, this.hass.states),
computeDeviceNameDisplay(a, this.hass),
computeDeviceNameDisplay(b, this.hass),
this.hass.locale.language
)
);

View File

@@ -72,6 +72,7 @@ export class HaFilterDomains extends LitElement {
>
<ha-domain-icon
slot="graphic"
.hass=${this.hass}
.domain=${domain}
brand-fallback
></ha-domain-icon>

View File

@@ -82,6 +82,7 @@ export class HaFilterIntegrations extends LitElement {
>
<ha-domain-icon
slot="graphic"
.hass=${this.hass}
.domain=${integration.domain}
brand-fallback
></ha-domain-icon>

View File

@@ -6,6 +6,7 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../common/color/compute-color";
import { fireEvent } from "../common/dom/fire_event";
import { navigate } from "../common/navigate";
import { stringCompare } from "../common/string/compare";
@@ -97,14 +98,17 @@ export class HaFilterLabels extends LitElement {
this.value
),
(label) => label.label_id,
(label) =>
html`<ha-check-list-item
(label) => {
const color = label.color
? computeCssColor(label.color)
: undefined;
return html`<ha-check-list-item
.value=${label.label_id}
.selected=${(this.value || []).includes(label.label_id)}
hasMeta
>
<ha-label
.color=${label.color}
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
${label.icon
@@ -115,7 +119,8 @@ export class HaFilterLabels extends LitElement {
: nothing}
${label.name}
</ha-label>
</ha-check-list-item>`
</ha-check-list-item>`;
}
)}
</ha-list> `
: nothing}
@@ -248,6 +253,10 @@ export class HaFilterLabels extends LitElement {
.warning {
color: var(--error-color);
}
ha-label {
--ha-label-background-color: var(--color, var(--grey-color));
--ha-label-background-opacity: 0.5;
}
.add {
position: absolute;
bottom: 0;

View File

@@ -100,7 +100,7 @@ export class HaFormInteger extends LitElement implements HaFormElement {
inputMode="numeric"
.label=${this.label}
.hint=${this.helper}
.value=${this.data?.toString() ?? ""}
.value=${this.data !== undefined ? this.data.toString() : ""}
.disabled=${this.disabled}
.required=${this.schema.required}
.autoValidate=${this.schema.required}

View File

@@ -171,7 +171,7 @@ export class HaGauge extends LitElement {
? svg`
<path
class="needle"
d="M -34,-3 L -48,-1 A 1,1,0,0,0,-48,1 L -34,3 A 2,2,0,0,0,-34,-3 Z"
d="M -36,-2 L -44,-1 A 1,1,0,0,0,-44,1 L -36,2 A 2,2,0,0,0,-36,-2 Z"
style=${styleMap({ transform: `rotate(${this._angle}deg)` })}
/>
@@ -243,19 +243,19 @@ export class HaGauge extends LitElement {
.levels-base {
fill: none;
stroke: var(--primary-background-color);
stroke-width: 12;
stroke-width: 6;
stroke-linecap: butt;
}
.level {
fill: none;
stroke-width: 12;
stroke-width: 6;
stroke-linecap: butt;
}
.value {
fill: none;
stroke-width: 12;
stroke-width: 6;
stroke: var(--gauge-color);
stroke-linecap: butt;
transition: stroke-dashoffset 1s ease 0s;

View File

@@ -140,7 +140,7 @@ class HaHLSPlayer extends LitElement {
this._cleanUp();
this._resetError();
if (!isComponentLoaded(this.hass.config, "stream")) {
if (!isComponentLoaded(this.hass!, "stream")) {
this._setFatalError("Streaming component is not loaded.");
return;
}

View File

@@ -1,44 +1,18 @@
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { computeCssColor } from "../common/color/compute-color";
import { getContrastedColorHex } from "../common/color/rgb";
import { uid } from "../common/util/uid";
import "./ha-tooltip";
/**
* Returns CSS styles for a label's background & icon/text
* @param color Label color defined in HEX format
* @returns CSS styles
*/
export const getLabelColorStyle = (labelColor: string | undefined | null) => {
const color = labelColor ? computeCssColor(labelColor) : undefined;
return color
? `--ha-label-background-color: ${color};
--primary-text-color: ${getContrastedColorHex(labelColor!)};`
: `--ha-label-background-color: rgba(var(--rgb-primary-text-color), 0.15);`;
};
@customElement("ha-label")
class HaLabel extends LitElement {
@property({ type: Boolean, reflect: true }) dense = false;
@property()
public color?: string;
@property({ attribute: "description" })
public description?: string;
private _elementId = "label-" + uid();
public willUpdate(changedProps: PropertyValues<this>) {
super.willUpdate(changedProps);
if (!changedProps.has("color")) {
return;
}
this.style.cssText = getLabelColorStyle(this.color);
}
protected render(): TemplateResult {
return html`
<ha-tooltip
@@ -62,6 +36,10 @@ class HaLabel extends LitElement {
:host {
--ha-label-text-color: var(--primary-text-color);
--ha-label-icon-color: var(--primary-text-color);
--ha-label-background-color: rgba(
var(--rgb-primary-text-color),
0.15
);
--ha-label-background-opacity: 1;
border: 1px solid var(--outline-color);
position: relative;

View File

@@ -6,6 +6,7 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../common/color/compute-color";
import { fireEvent } from "../common/dom/fire_event";
import { stringCompare } from "../common/string/compare";
import { labelsContext } from "../data/context";
@@ -16,7 +17,6 @@ import type { HomeAssistant, ValueChangedEvent } from "../types";
import "./chips/ha-chip-set";
import "./chips/ha-input-chip";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import { getLabelColorStyle } from "./ha-label";
import "./ha-label-picker";
import type { HaLabelPicker } from "./ha-label-picker";
import "./ha-tooltip";
@@ -106,14 +106,9 @@ export class HaLabelsPicker extends LitElement {
labels?.find((label) => label.label_id === id) || {
label_id: id,
name: id,
color: "rgba(var(--rgb-primary-text-color), 0.15)",
}
)
.sort((a, b) => stringCompare(a?.name || "", b?.name || "", language))
.map((label) => ({
...label,
style: getLabelColorStyle(label.color),
}))
);
protected render(): TemplateResult {
@@ -140,6 +135,9 @@ export class HaLabelsPicker extends LitElement {
(label) => label?.label_id,
(label) => {
if (!label) return nothing;
const color = label.color
? computeCssColor(label.color)
: undefined;
const elementId = "label-" + label.label_id;
return html`
<ha-tooltip
@@ -156,7 +154,7 @@ export class HaLabelsPicker extends LitElement {
.disabled=${this.disabled}
.label=${label.name}
selected
style=${label.style}
style=${color ? `--color: ${color}` : ""}
>
${label.icon
? html`<ha-icon
@@ -241,10 +239,8 @@ export class HaLabelsPicker extends LitElement {
height: var(--ha-space-8);
}
ha-input-chip {
--md-input-chip-selected-container-color: var(
--ha-label-background-color,
var(--grey-color)
);
--md-input-chip-selected-container-color: var(--color, var(--grey-color));
--ha-input-chip-selected-container-opacity: 0.5;
--md-input-chip-selected-outline-width: 1px;
}
label {

View File

@@ -1,6 +1,7 @@
import type { PropertyValues } from "lit";
import { ReactiveElement, render, html } from "lit";
import { customElement, property } from "lit/decorators";
// eslint-disable-next-line import/extensions
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import hash from "object-hash";
import { fireEvent } from "../common/dom/fire_event";

View File

@@ -14,9 +14,9 @@ import {
} from "../data/supervisor/mounts";
import type { HomeAssistant } from "../types";
import "./ha-alert";
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
import "./ha-list-item";
import "./ha-select";
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
const _BACKUP_DATA_DISK_ = "/backup";
@@ -129,7 +129,7 @@ class HaMountPicker extends LitElement {
private async _getMounts() {
try {
if (isComponentLoaded(this.hass.config, "hassio")) {
if (isComponentLoaded(this.hass, "hassio")) {
this._mounts = await fetchSupervisorMounts(this.hass);
if (this.usage === SupervisorMountUsage.BACKUP && !this.value) {
this.value = this._mounts.default_backup_mount || _BACKUP_DATA_DISK_;

View File

@@ -1,5 +1,5 @@
import Fuse from "fuse.js";
import { mdiDevices, mdiTextureBox } from "@mdi/js";
import { mdiDevices, mdiPlus, mdiTextureBox } from "@mdi/js";
import { html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
@@ -8,7 +8,7 @@ import { caseInsensitiveStringCompare } from "../common/string/compare";
import { titleCase } from "../common/string/title-case";
import { getConfigEntries, type ConfigEntry } from "../data/config_entries";
import { fetchConfig } from "../data/lovelace/config/types";
import { getPanelIcon, getPanelTitle, SYSTEM_PANELS } from "../data/panel";
import { getPanelIcon, getPanelTitle } from "../data/panel";
import { findRelated, type RelatedResult } from "../data/search";
import { PANEL_DASHBOARDS } from "../panels/config/lovelace/dashboards/ha-config-lovelace-dashboards";
import { computeAreaPath } from "../panels/lovelace/strategies/areas/helpers/areas-strategy-helper";
@@ -132,6 +132,7 @@ export class HaNavigationPicker extends LitElement {
? html`
<ha-domain-icon
slot="start"
.hass=${this.hass}
.domain=${item.domain}
brand-fallback
></ha-domain-icon>
@@ -157,6 +158,7 @@ export class HaNavigationPicker extends LitElement {
? html`
<ha-domain-icon
slot="start"
.hass=${this.hass}
.domain=${item.domain}
brand-fallback
></ha-domain-icon>
@@ -235,6 +237,22 @@ export class HaNavigationPicker extends LitElement {
addGroup("views", views);
addGroup("other_routes", otherRoutes);
if (
searchString &&
!this._navigationItems.some((navItem) => navItem.id === searchString)
) {
items.push({
id: searchString,
primary: this.hass.localize(
"ui.components.navigation-picker.add_custom_path"
),
secondary: `"${searchString}"`,
icon_path: mdiPlus,
sorting_label: searchString,
group: "other_routes",
});
}
return items;
};
@@ -274,7 +292,6 @@ export class HaNavigationPicker extends LitElement {
const otherRoutes: NavigationItem[] = [];
for (const panel of panels) {
if (SYSTEM_PANELS.includes(panel.id)) continue;
const path = `/${panel.url_path}`;
const panelTitle = getPanelTitle(this.hass, panel);
const primary = panelTitle || path;

View File

@@ -15,7 +15,7 @@ import memoizeOne from "memoize-one";
import { tinykeys } from "tinykeys";
import { fireEvent } from "../common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import { internationalizationContext } from "../data/context";
import { localeContext, localizeContext } from "../data/context";
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
import {
multiTermSortedSearch,
@@ -162,8 +162,12 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
@query("ha-input-search") private _searchFieldElement?: HaInputSearch;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private i18n!: ContextType<typeof internationalizationContext>;
@consume({ context: localizeContext, subscribe: true })
private localize!: ContextType<typeof localizeContext>;
@state()
@consume({ context: localeContext, subscribe: true })
private locale!: ContextType<typeof localeContext>;
@state() private _items: PickerComboBoxItem[] = [];
@@ -218,9 +222,9 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
const searchLabel =
this.label ??
(this.allowCustomValue
? (this.i18n.localize?.("ui.components.combo-box.search_or_custom") ??
? (this.localize?.("ui.components.combo-box.search_or_custom") ??
"Search | Add custom value")
: (this.i18n.localize?.("ui.common.search") ?? "Search"));
: (this.localize?.("ui.common.search") ?? "Search"));
return html`<ha-input-search
appearance="outlined"
@@ -347,7 +351,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
return caseInsensitiveStringCompare(
sortLabelA,
sortLabelB,
this.i18n.locale?.language ?? navigator.language
this.locale?.language ?? navigator.language
);
});
}
@@ -364,7 +368,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
id: this._search,
primary:
this.customValueLabel ??
this.i18n.localize?.("ui.components.combo-box.add_custom_item") ??
this.localize?.("ui.components.combo-box.add_custom_item") ??
"Add custom item",
secondary: `"${this._search}"`,
icon_path: mdiPlus,
@@ -398,10 +402,10 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
? typeof this.notFoundLabel === "function"
? this.notFoundLabel(this._search)
: this.notFoundLabel ||
this.i18n.localize?.("ui.components.combo-box.no_match") ||
this.localize?.("ui.components.combo-box.no_match") ||
"No matching items found"
: this.emptyLabel ||
this.i18n.localize?.("ui.components.combo-box.no_items") ||
this.localize?.("ui.components.combo-box.no_items") ||
"No items available"}</span
>
</ha-combo-box-item>
@@ -493,7 +497,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
id: searchString,
primary:
this.customValueLabel ??
this.i18n.localize?.("ui.components.combo-box.add_custom_item") ??
this.localize?.("ui.components.combo-box.add_custom_item") ??
"Add custom item",
secondary: `"${searchString}"`,
icon_path: mdiPlus,
@@ -794,11 +798,11 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
}
ha-input-search {
padding: 0 var(--ha-space-3) var(--ha-space-3);
padding: 0 var(--ha-space-3);
}
:host([mode="dialog"]) ha-input-search {
padding: 0 var(--ha-space-4) var(--ha-space-3);
padding: 0 var(--ha-space-4);
}
ha-combo-box-item {
@@ -869,12 +873,12 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
display: flex;
flex-wrap: nowrap;
gap: var(--ha-space-2);
padding: 0 var(--ha-space-3) var(--ha-space-3);
padding: var(--ha-space-3) var(--ha-space-3);
overflow: auto;
}
:host([mode="dialog"]) .sections {
padding: 0 var(--ha-space-4) var(--ha-space-3);
padding: var(--ha-space-3) var(--ha-space-4);
}
.sections ha-filter-chip {
@@ -911,6 +915,10 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
padding: var(--ha-space-1) var(--ha-space-4);
}
:host([mode="dialog"]) ha-input-search {
padding: 0 var(--ha-space-4);
}
.section-title-wrapper {
height: 0;
position: relative;

View File

@@ -1,4 +1,4 @@
import { consume, type ContextType } from "@lit/context";
import { consume } from "@lit/context";
import { mdiClose, mdiMenuDown } from "@mdi/js";
import {
css,
@@ -11,8 +11,9 @@ import {
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event";
import { internationalizationContext } from "../data/context";
import { localizeContext } from "../data/context";
import { PickerMixin } from "../mixins/picker-mixin";
import type { HomeAssistant } from "../types";
import "./ha-combo-box-item";
import type { HaComboBoxItem } from "./ha-combo-box-item";
import "./ha-icon";
@@ -33,8 +34,8 @@ export class HaPickerField extends PickerMixin(LitElement) {
@query("ha-combo-box-item", true) public item!: HaComboBoxItem;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@consume({ context: localizeContext, subscribe: true })
private localize!: HomeAssistant["localize"];
public async focus() {
await this.updateComplete;
@@ -88,7 +89,7 @@ export class HaPickerField extends PickerMixin(LitElement) {
${this.unknown
? html`<div slot="supporting-text" class="unknown">
${this.unknownItemText ||
this._i18n?.localize("ui.components.combo-box.unknown_item")}
this.localize("ui.components.combo-box.unknown_item")}
</div>`
: nothing}
${showClearIcon

View File

@@ -1,144 +0,0 @@
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import type { PeriodKey, PeriodSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import { deepEqual } from "../../common/util/deep-equal";
import type { LocalizeFunc } from "../../common/translations/localize";
import "../ha-form/ha-form";
const PERIODS = {
none: undefined,
today: { calendar: { period: "day" } },
yesterday: { calendar: { period: "day", offset: -1 } },
tomorrow: { calendar: { period: "day", offset: 1 } },
this_week: { calendar: { period: "week" } },
last_week: { calendar: { period: "week", offset: -1 } },
next_week: { calendar: { period: "week", offset: 1 } },
this_month: { calendar: { period: "month" } },
last_month: { calendar: { period: "month", offset: -1 } },
next_month: { calendar: { period: "month", offset: 1 } },
this_year: { calendar: { period: "year" } },
last_year: { calendar: { period: "year", offset: -1 } },
next_7d: { calendar: { period: "day", offset: 7 } },
next_30d: { calendar: { period: "day", offset: 30 } },
} as const;
@customElement("ha-selector-period")
export class HaPeriodSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: PeriodSelector;
@property({ attribute: false }) public value?: unknown;
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
private _schema = memoizeOne(
(
selectedPeriodKey: PeriodKey | undefined,
selector: PeriodSelector,
localize: LocalizeFunc
) =>
[
{
name: "period",
required: this.required,
selector:
selectedPeriodKey && selectedPeriodKey in this._periods(selector)
? {
select: {
multiple: false,
options: Object.keys(this._periods(selector)).map(
(periodKey) => ({
value: periodKey,
label:
localize(
`ui.components.selectors.period.periods.${periodKey as PeriodKey}`
) || periodKey,
})
),
},
}
: { object: {} },
},
] as const
);
protected render() {
const data = this._data(this.value, this.selector);
const schema = this._schema(
typeof data.period === "string" ? (data.period as PeriodKey) : undefined,
this.selector,
this.hass.localize
);
return html`
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${schema}
.computeHelper=${this._computeHelperCallback}
.computeLabel=${this._computeLabelCallback}
.disabled=${this.disabled}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _periods = memoizeOne((selector: PeriodSelector) =>
Object.fromEntries(
Object.entries(PERIODS).filter(([key]) =>
selector.period?.options?.includes(key as any)
)
)
);
private _data = memoizeOne((value: unknown, selector: PeriodSelector) => {
for (const [periodKey, period] of Object.entries(this._periods(selector))) {
if (deepEqual(period, value)) {
return { period: periodKey };
}
}
return { period: value };
});
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
const newValue = ev.detail.value;
if (typeof newValue.period === "string") {
const periods = this._periods(this.selector);
if (newValue.period in periods) {
const period = this._periods(this.selector)[newValue.period];
fireEvent(this, "value-changed", { value: period });
}
} else {
fireEvent(this, "value-changed", { value: newValue.period });
}
}
private _computeHelperCallback = () => this.helper;
private _computeLabelCallback = () => this.label;
static styles = css`
:host {
position: relative;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-period": HaPeriodSelector;
}
}

View File

@@ -98,7 +98,6 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
.noEntity=${this.selector.state?.no_entity ?? false}
.disabled=${this.disabled}
.required=${this.required}
allow-custom-value

View File

@@ -65,14 +65,15 @@ export class HaTextSelector extends LitElement {
.label=${this.label}
.placeholder=${this.placeholder}
.value=${this.value || ""}
.hint=${this.helper}
.helper=${this.helper}
helperPersistent
.disabled=${this.disabled}
@input=${this._handleChange}
autocapitalize="none"
.autocomplete=${this.selector.text?.autocomplete}
spellcheck="false"
.required=${this.required}
resize="auto"
autogrow
></ha-textarea>`;
}
return html`<ha-input

View File

@@ -41,7 +41,6 @@ const LOAD_ELEMENTS = {
number: () => import("./ha-selector-number"),
numeric_threshold: () => import("./ha-selector-numeric-threshold"),
object: () => import("./ha-selector-object"),
period: () => import("./ha-selector-period"),
qr_code: () => import("./ha-selector-qr-code"),
select: () => import("./ha-selector-select"),
selector: () => import("./ha-selector-selector"),

View File

@@ -23,10 +23,7 @@ export class HaSlider extends Slider {
--marker-height: calc(var(--ha-slider-track-size, 4px) / 2);
--marker-width: calc(var(--ha-slider-track-size, 4px) / 2);
--wa-color-surface-default: var(--card-background-color);
--wa-color-neutral-fill-normal: var(
--ha-slider-track-color,
var(--disabled-color)
);
--wa-color-neutral-fill-normal: var(--disabled-color);
--wa-tooltip-background-color: var(
--ha-tooltip-background-color,
var(--secondary-background-color)

View File

@@ -1,19 +1,19 @@
import { mdiStarFourPoints } from "@mdi/js";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { html, css, LitElement, nothing } from "lit";
import { mdiStarFourPoints } from "@mdi/js";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event";
import { customElement, state, property } from "lit/decorators";
import type {
AITaskPreferences,
GenDataTask,
GenDataTaskResult,
} from "../data/ai_task";
import { fetchAITaskPreferences, generateDataAITask } from "../data/ai_task";
import type { HomeAssistant } from "../types";
import "./chips/ha-assist-chip";
import "./ha-svg-icon";
import type { HomeAssistant } from "../types";
import { fireEvent } from "../common/dom/fire_event";
import { isComponentLoaded } from "../common/config/is_component_loaded";
declare global {
interface HASSDomEvents {
@@ -56,7 +56,7 @@ export class HaSuggestWithAIButton extends LitElement {
protected firstUpdated(_changedProperties: PropertyValues): void {
super.firstUpdated(_changedProperties);
if (!this.hass || !isComponentLoaded(this.hass.config, "ai_task")) {
if (!this.hass || !isComponentLoaded(this.hass, "ai_task")) {
return;
}
fetchAITaskPreferences(this.hass).then((prefs) => {

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