Compare commits

..

1 Commits

Author SHA1 Message Date
Paulus Schoutsen
9ecb41accd App info page no longer links to itself 2026-03-29 10:45:00 -04:00
1174 changed files with 17332 additions and 40778 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

@@ -69,6 +69,7 @@
- [ ] I understand the code I am submitting and can explain how it works.
- [ ] The code change is tested and works locally.
- [ ] There is no commented out code in this PR.
- [ ] I have followed the [development checklist][dev-checklist]
- [ ] I have followed the [perfect PR recommendations][perfect-pr]
- [ ] Any generated code has been carefully reviewed for correctness and compliance with project standards.
@@ -104,5 +105,6 @@ To help with the load of incoming pull requests:
Below, some useful links you could explore:
-->
[dev-checklist]: https://developers.home-assistant.io/docs/development_checklist/
[docs-repository]: https://github.com/home-assistant/home-assistant.io
[perfect-pr]: https://developers.home-assistant.io/docs/review-process/#creating-the-perfect-pr

View File

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

View File

@@ -30,7 +30,7 @@ jobs:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -66,7 +66,7 @@ jobs:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -31,7 +31,7 @@ jobs:
with:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -42,7 +42,7 @@ jobs:
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
- name: Setup lint cache
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
node_modules/.cache/prettier
@@ -67,7 +67,7 @@ jobs:
with:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -87,7 +87,7 @@ jobs:
with:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -98,13 +98,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

@@ -41,14 +41,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
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@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
uses: github/codeql-action/autobuild@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -62,4 +62,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
uses: github/codeql-action/analyze@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1

View File

@@ -31,7 +31,7 @@ jobs:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -67,7 +67,7 @@ jobs:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -24,7 +24,7 @@ jobs:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -29,7 +29,7 @@ jobs:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -1,296 +0,0 @@
name: E2E Tests
on:
push:
branches:
- dev
- master
pull_request:
branches:
- dev
- master
env:
NODE_OPTIONS: --max_old_space_size=6144
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
# ── Build the demo once and share it across test jobs via artifact ──────────
build-demo:
name: Build demo
runs-on: ubuntu-latest
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@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies
run: yarn install --immutable
- name: Build demo
run: ./node_modules/.bin/gulp build-demo
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload demo build
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: demo-dist
path: demo/dist/
if-no-files-found: error
retention-days: 3
# ── Build the e2e test app and share it via artifact ────────────────────────
build-e2e-test-app:
name: Build e2e test app
runs-on: ubuntu-latest
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@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies
run: yarn install --immutable
- name: Build e2e test app
run: ./node_modules/.bin/gulp build-e2e-test-app
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload e2e test app build
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: e2e-test-app-dist
path: test/e2e/app/dist/
if-no-files-found: error
retention-days: 3
# ── Build the gallery and share it via artifact ─────────────────────────────
build-gallery:
name: Build gallery
runs-on: ubuntu-latest
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@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies
run: yarn install --immutable
- name: Build gallery
run: ./node_modules/.bin/gulp build-gallery
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload gallery build
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: gallery-dist
path: gallery/dist/
if-no-files-found: error
retention-days: 3
# ── Run Playwright tests locally against Chromium ──────────────────────────
e2e-local:
name: E2E (local Chromium)
needs: [build-demo, build-e2e-test-app, build-gallery]
runs-on: ubuntu-latest
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@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies
run: yarn install --immutable
- name: Install Playwright browsers
run: npx playwright install chromium --with-deps
- name: Download demo build
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
with:
name: demo-dist
path: demo/dist/
- name: Download e2e test app build
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
with:
name: e2e-test-app-dist
path: test/e2e/app/dist/
- name: Download gallery build
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
with:
name: gallery-dist
path: gallery/dist/
- name: Run Playwright tests (local)
run: yarn test:e2e:local
- name: Upload blob report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: blob-report-local
path: test/e2e/reports/
retention-days: 3
# ── Run Playwright tests on BrowserStack (real devices + browsers) ─────────
e2e-browserstack:
name: E2E (BrowserStack)
needs: [build-demo, build-e2e-test-app, build-gallery]
runs-on: ubuntu-latest
environment: browserstack
env:
BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
# Unique identifier so BrowserStack Local tunnels from parallel CI runs
# don't collide with each other.
BROWSERSTACK_LOCAL_IDENTIFIER: ${{ github.run_id }}-${{ github.run_attempt }}
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@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies
run: yarn install --immutable
- name: Download demo build
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
with:
name: demo-dist
path: demo/dist/
- name: Download e2e test app build
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
with:
name: e2e-test-app-dist
path: test/e2e/app/dist/
- name: Download gallery build
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
with:
name: gallery-dist
path: gallery/dist/
# Start BrowserStack Local tunnel so BrowserStack's cloud browsers can
# reach the tests running on localhost.
- name: Start BrowserStack Local tunnel
uses: browserstack/github-actions/setup-local@93aebce225b754566349151c0676b26b005e591b # v1.0.4
with:
local-testing: start
local-identifier: ${{ env.BROWSERSTACK_LOCAL_IDENTIFIER }}
- name: Run Playwright tests (BrowserStack)
run: yarn test:e2e:browserstack
env:
BROWSERSTACK: "true"
BROWSERSTACK_LOCAL_IDENTIFIER: ${{ env.BROWSERSTACK_LOCAL_IDENTIFIER }}
- name: Stop BrowserStack Local tunnel
uses: browserstack/github-actions/setup-local@93aebce225b754566349151c0676b26b005e591b # v1.0.4
if: always()
with:
local-testing: stop
local-identifier: ${{ env.BROWSERSTACK_LOCAL_IDENTIFIER }}
- name: Upload blob report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: blob-report-browserstack
path: test/e2e/reports/
retention-days: 3
# ── Merge reports and upload ──────────────────────────────────────
report:
name: Report
needs: [e2e-local, e2e-browserstack]
runs-on: ubuntu-latest
if: always()
permissions:
contents: read
pull-requests: write
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@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies
run: yarn install --immutable
- name: Download blob report (local)
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
continue-on-error: true
with:
name: blob-report-local
path: test/e2e/reports/
- name: Download blob report (BrowserStack)
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
continue-on-error: true
with:
name: blob-report-browserstack
path: test/e2e/reports/
- name: Stage blobs for merge
run: node test/e2e/collect-blob-reports.mjs
- name: Merge blob reports
run: npx playwright merge-reports -c test/e2e/playwright.merge.config.ts test/e2e/reports/blob
- name: Upload merged HTML report
id: upload-report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: playwright-report
path: test/e2e/reports/combined/
retention-days: 14

View File

@@ -30,7 +30,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -59,14 +59,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

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

@@ -36,10 +36,10 @@ 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@d56d093b9ab8d2105bc0cb6ee9bcc0ef4ec8b96d # master
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: ".nvmrc"
@@ -58,7 +58,7 @@ 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
@@ -104,7 +104,7 @@ jobs:
with:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: ".nvmrc"
- name: Install dependencies

View File

@@ -22,7 +22,7 @@ jobs:
|| github.event.issue.type.name == 'Opportunity'
steps:
- name: Add no-stale label
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
await github.rest.issues.addLabels({
@@ -41,7 +41,7 @@ jobs:
if: github.event.issue.type.name == 'Task'
steps:
- name: Check if user is authorized
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const issueAuthor = context.payload.issue.user.login;

View File

@@ -6,7 +6,6 @@ on:
- cron: "0 * * * *"
permissions:
actions: write
issues: write
pull-requests: write

9
.gitignore vendored
View File

@@ -54,14 +54,7 @@ src/cast/dev_const.ts
# test coverage
test/coverage/
# Playwright e2e output
test/e2e/reports/
test/e2e/test-results/
# E2E test app build output
test/e2e/app/dist/
# AI tooling
.claude
.cursor
.opencode
.serena

2
.nvmrc
View File

@@ -1 +1 @@
24.15.0
24.14.1

File diff suppressed because one or more lines are too long

View File

@@ -8,4 +8,4 @@ enableGlobalCache: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.14.1.cjs
yarnPath: .yarn/releases/yarn-4.13.0.cjs

View File

@@ -176,14 +176,11 @@ module.exports.babelOptions = ({
{
// Use unambiguous for dependencies so that require() is correctly injected into CommonJS files
// Exclusions are needed in some cases where ES modules have no static imports or exports, such as polyfills
// (otherwise babel-plugin-polyfill-corejs3 injects bare require("core-js/modules/...") calls
// that rspack does not transform, causing ReferenceError in browsers like Safari 14).
sourceType: "unambiguous",
include: /\/node_modules\//,
exclude: [
"element-internals-polyfill",
"@?lit(?:-labs|-element|-html)?",
"@formatjs/(?:ecma402-abstract|intl-\\w+)",
].map((p) => new RegExp(`/node_modules/${p}/`)),
},
],
@@ -320,22 +317,4 @@ module.exports.config = {
isLandingPageBuild: true,
};
},
e2eTestApp({ isProdBuild, latestBuild, isStatsBuild }) {
return {
name: "e2e-test-app" + nameSuffix(latestBuild),
entry: {
main: path.resolve(paths.e2eTestApp_dir, "src/entrypoint.ts"),
},
outputPath: outputPath(paths.e2eTestApp_output_root, latestBuild),
publicPath: publicPath(latestBuild),
defineOverlay: {
__VERSION__: JSON.stringify(`E2E-TEST-${env.version()}`),
__DEMO__: true,
},
isProdBuild,
latestBuild,
isStatsBuild,
};
},
};

View File

@@ -1,18 +1,14 @@
// @ts-check
import globals from "globals";
import tseslint from "typescript-eslint";
import rootConfig from "../eslint.config.mjs";
export default tseslint.config(...rootConfig, {
languageOptions: {
globals: globals.node,
},
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

@@ -1,11 +0,0 @@
// Browser-only replacement for core-js/internals/get-built-in-node-module.
// The original helper evaluates `Function('return require("...")')()`
// when it detects a Node environment, which causes a runtime
// ReferenceError on browsers (notably Safari 14) if environment
// detection mis-classifies the page. Since browser bundles never need to
// access Node built-in modules, return undefined unconditionally.
//
// Wired up via rspack `NormalModuleReplacementPlugin` in build-scripts/rspack.cjs.
module.exports = function () {
return undefined;
};

View File

@@ -45,10 +45,3 @@ gulp.task(
])
)
);
gulp.task(
"clean-e2e-test-app",
gulp.parallel("clean-translations", async () =>
deleteSync([paths.e2eTestApp_output_root, paths.build_dir])
)
);

View File

@@ -99,44 +99,6 @@ const lokaliseProjects = {
frontend: "3420425759f6d6d241f598.13594006",
};
const POLL_INTERVAL_MS = 1000;
/* eslint-disable no-await-in-loop */
async function pollProcess(lokaliseApi, projectId, processId) {
while (true) {
const process = await lokaliseApi
.queuedProcesses()
.get(processId, { project_id: projectId });
const project =
projectId === lokaliseProjects.backend ? "backend" : "frontend";
if (process.status === "finished") {
console.log(`Lokalise export process for ${project} finished`);
return process;
}
if (process.status === "failed" || process.status === "cancelled") {
throw new Error(
`Lokalise export process for ${project} ${process.status}: ${process.message}`
);
}
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})`
: ""
);
await new Promise((resolve) => {
setTimeout(resolve, POLL_INTERVAL_MS);
});
}
}
/* eslint-enable no-await-in-loop */
gulp.task("fetch-lokalise", async function () {
let apiKey;
try {
@@ -156,60 +118,55 @@ gulp.task("fetch-lokalise", async function () {
]);
await Promise.all(
Object.entries(lokaliseProjects).map(async ([project, projectId]) => {
try {
const exportProcess = await lokaliseApi
.files()
.async_download(projectId, {
format: "json",
original_filenames: false,
replace_breaks: false,
json_unescaped_slashes: true,
export_empty_as: "skip",
filter_data: ["verified"],
});
const finishedProcess = await pollProcess(
lokaliseApi,
projectId,
exportProcess.process_id
);
const bundleUrl = finishedProcess.details.download_url;
console.log(`Downloading translations from: ${bundleUrl}`);
const response = await fetch(bundleUrl);
if (response.status !== 200 && response.status !== 0) {
Object.entries(lokaliseProjects).map(([project, projectId]) =>
lokaliseApi
.files()
.download(projectId, {
format: "json",
original_filenames: false,
replace_breaks: false,
json_unescaped_slashes: true,
export_empty_as: "skip",
filter_data: ["verified"],
})
.then((download) => fetch(download.bundle_url))
.then((response) => {
if (response.status === 200 || response.status === 0) {
return response.arrayBuffer();
}
throw new Error(response.statusText);
}
console.log(`Extracting translations...`);
const contents = await JSZip.loadAsync(await response.arrayBuffer());
await mkdirPromise;
await Promise.all(
Object.keys(contents.files).map(async (filename) => {
const file = contents.file(filename);
if (!file) {
// no file, probably a directory
return;
}
const content = await file.async("nodebuffer");
await fs.writeFile(
path.join(inDir, project, filename.split("/").splice(-1)[0]),
content,
{ flag: "w", encoding }
);
})
);
} catch (err) {
console.error(err);
throw err;
}
})
})
.then(JSZip.loadAsync)
.then(async (contents) => {
await mkdirPromise;
return Promise.all(
Object.keys(contents.files).map(async (filename) => {
const file = contents.file(filename);
if (!file) {
// no file, probably a directory
return Promise.resolve();
}
return file
.async("nodebuffer")
.then((content) =>
fs.writeFile(
path.join(
inDir,
project,
filename.split("/").splice(-1)[0]
),
content,
{ flag: "w", encoding }
)
);
})
);
})
.catch((err) => {
console.error(err);
throw err;
})
)
);
});

View File

@@ -1,41 +0,0 @@
import gulp from "gulp";
import "./clean.js";
import "./entry-html.js";
import "./gather-static.js";
import "./gen-icons-json.js";
import "./translations.js";
import "./rspack.js";
gulp.task(
"develop-e2e-test-app",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "development";
},
"clean-e2e-test-app",
"translations-enable-merge-backend",
gulp.parallel(
"gen-icons-json",
"gen-pages-e2e-test-app-dev",
"build-translations",
"build-locale-data"
),
"copy-static-e2e-test-app",
"rspack-dev-server-e2e-test-app"
)
);
gulp.task(
"build-e2e-test-app",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "production";
},
"clean-e2e-test-app",
"translations-enable-merge-backend",
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
"copy-static-e2e-test-app",
"rspack-prod-e2e-test-app",
"gen-pages-e2e-test-app-prod"
)
);

View File

@@ -266,24 +266,3 @@ gulp.task(
paths.landingPage_output_es5
)
);
const E2E_TEST_APP_PAGE_ENTRIES = { "index.html": ["main"] };
gulp.task(
"gen-pages-e2e-test-app-dev",
genPagesDevTask(
E2E_TEST_APP_PAGE_ENTRIES,
paths.e2eTestApp_dir,
paths.e2eTestApp_output_root
)
);
gulp.task(
"gen-pages-e2e-test-app-prod",
genPagesProdTask(
E2E_TEST_APP_PAGE_ENTRIES,
paths.e2eTestApp_dir,
paths.e2eTestApp_output_root,
paths.e2eTestApp_output_latest
)
);

View File

@@ -201,23 +201,3 @@ gulp.task("copy-static-landing-page", async () => {
copyFonts(paths.landingPage_output_static);
copyTranslations(paths.landingPage_output_static);
});
gulp.task("copy-static-e2e-test-app", async () => {
// Copy app static files (icons, polyfills, etc.)
fs.copySync(
polyPath("public/static"),
path.resolve(paths.e2eTestApp_output_root, "static")
);
// Copy e2e test app public files (manifest, sw stubs)
const e2ePublic = path.resolve(paths.e2eTestApp_dir, "public");
if (fs.existsSync(e2ePublic)) {
fs.copySync(e2ePublic, paths.e2eTestApp_output_root);
}
copyPolyfills(paths.e2eTestApp_output_static);
copyMapPanel(paths.e2eTestApp_output_static);
copyFonts(paths.e2eTestApp_output_static);
copyTranslations(paths.e2eTestApp_output_static);
copyLocaleData(paths.e2eTestApp_output_static);
copyMdiIcons(paths.e2eTestApp_output_static);
});

View File

@@ -4,7 +4,6 @@ import "./clean.js";
import "./compress.js";
import "./demo.js";
import "./download-translations.js";
import "./e2e-test-app.js";
import "./entry-html.js";
import "./fetch-nightly-translations.js";
import "./gallery.js";

View File

@@ -14,7 +14,6 @@ import {
createDemoConfig,
createGalleryConfig,
createLandingPageConfig,
createE2eTestAppConfig,
} from "../rspack.cjs";
const bothBuilds = (createConfigFunc, params) => [
@@ -211,22 +210,3 @@ gulp.task("rspack-prod-landing-page", () =>
})
)
);
gulp.task("rspack-dev-server-e2e-test-app", () =>
runDevServer({
compiler: rspack(
createE2eTestAppConfig({ isProdBuild: false, latestBuild: true })
),
contentBase: paths.e2eTestApp_output_root,
port: 8095,
})
);
gulp.task("rspack-prod-e2e-test-app", () =>
prodBuild(
bothBuilds(createE2eTestAppConfig, {
isProdBuild: true,
isStatsBuild: env.isStatsBuild(),
})
)
);

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

@@ -50,15 +50,4 @@ module.exports = {
),
translations_src: path.resolve(__dirname, "../src/translations"),
e2eTestApp_dir: path.resolve(__dirname, "../test/e2e/app"),
e2eTestApp_output_root: path.resolve(__dirname, "../test/e2e/app/dist"),
e2eTestApp_output_static: path.resolve(
__dirname,
"../test/e2e/app/dist/static"
),
e2eTestApp_output_latest: path.resolve(
__dirname,
"../test/e2e/app/dist/frontend_latest"
),
};

View File

@@ -12,7 +12,7 @@ const TerserPlugin = require("terser-webpack-plugin");
const { WebpackManifestPlugin } = require("rspack-manifest-plugin");
const log = require("fancy-log");
// eslint-disable-next-line @typescript-eslint/naming-convention
const SafeWebpackBar = require("./safe-webpackbar.cjs");
const WebpackBar = require("webpackbar/rspack");
const paths = require("./paths.cjs");
const bundle = require("./bundle.cjs");
@@ -126,7 +126,7 @@ const createRspackConfig = ({
},
},
plugins: [
!isStatsBuild && new SafeWebpackBar({ fancy: !isProdBuild }),
!isStatsBuild && new WebpackBar({ fancy: !isProdBuild }),
new WebpackManifestPlugin({
// Only include the JS of entrypoints
filter: (file) => file.isInitial && !file.name.endsWith(".map"),
@@ -173,16 +173,6 @@ const createRspackConfig = ({
path.resolve(paths.root_dir, "src/util/empty.js")
)
: false,
// core-js ships a Node-only helper that evaluates
// `Function('return require("...")')()` when its runtime environment
// detection mis-classifies the page as Node. That produces a
// ReferenceError on browsers (observed on Safari 14). Since browser
// bundles never need to access Node built-in modules, replace it with
// a CommonJS no-op stub matching the helper's API (returns undefined).
new rspack.NormalModuleReplacementPlugin(
/core-js[\\/]internals[\\/]get-built-in-node-module(?:\.js)?$/,
path.resolve(__dirname, "get-built-in-node-module-shim.cjs")
),
!isProdBuild && new LogStartCompilePlugin(),
isProdBuild &&
new StatsWriterPlugin({
@@ -337,11 +327,6 @@ const createGalleryConfig = ({ isProdBuild, latestBuild }) =>
const createLandingPageConfig = ({ isProdBuild, latestBuild }) =>
createRspackConfig(bundle.config.landingPage({ isProdBuild, latestBuild }));
const createE2eTestAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) =>
createRspackConfig(
bundle.config.e2eTestApp({ isProdBuild, latestBuild, isStatsBuild })
);
module.exports = {
createAppConfig,
createDemoConfig,
@@ -349,5 +334,4 @@ module.exports = {
createGalleryConfig,
createRspackConfig,
createLandingPageConfig,
createE2eTestAppConfig,
};

View File

@@ -1,25 +0,0 @@
// eslint-disable-next-line @typescript-eslint/naming-convention
const WebpackBar = require("webpackbar/rspack");
// Rspack 2's ProgressPlugin passes the third `info` arg as
// `{ builtModules, moduleIdentifier? }` instead of the v1 string. webpackbar@7's
// parseRequest still expects a string and crashes on `split`. Extract
// moduleIdentifier (the v1 equivalent) so progress still shows the active module.
class SafeWebpackBar extends WebpackBar {
constructor(options) {
super(options);
const inner = this.webpackbar;
const originalUpdate = inner.updateProgress.bind(inner);
inner.updateProgress = (percent, message, details = []) =>
originalUpdate(
percent,
message,
details.map((d) => {
if (typeof d === "string") return d;
return d?.moduleIdentifier ?? "";
})
);
}
}
module.exports = SafeWebpackBar;

View File

@@ -1,7 +1,7 @@
import type { ActionDetail } from "@material/mwc-list/mwc-list";
import { mdiCast, mdiCastConnected, mdiViewDashboard } from "@mdi/js";
import type { Auth, Connection } from "home-assistant-js-websocket";
import type { TemplateResult, PropertyValues } from "lit";
import type { TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import type { CastManager } from "../../../../src/cast/cast_manager";
@@ -150,7 +150,7 @@ class HcCast extends LitElement {
`;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
const llColl = atLeastVersion(this.connection.haVersion, 0, 107)
@@ -183,7 +183,7 @@ class HcCast extends LitElement {
});
}
protected updated(changedProps: PropertyValues<this>) {
protected updated(changedProps) {
super.updated(changedProps);
toggleAttribute(
this,

View File

@@ -12,7 +12,7 @@ import {
ERR_INVALID_HTTPS_TO_HTTP,
getAuth,
} from "home-assistant-js-websocket";
import type { TemplateResult, PropertyValues } from "lit";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, state } from "lit/decorators";
import type { CastManager } from "../../../../src/cast/cast_manager";
@@ -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">
@@ -158,7 +158,7 @@ export class HcConnect extends LitElement {
`;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
import("./hc-cast");
@@ -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

@@ -1,6 +1,6 @@
import type { Auth, Connection, HassUser } from "home-assistant-js-websocket";
import { getUser } from "home-assistant-js-websocket";
import type { TemplateResult, PropertyValues } from "lit";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../src/components/ha-card";
@@ -53,7 +53,7 @@ class HcLayout extends LitElement {
`;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
if (this.connection) {

View File

@@ -1,4 +1,3 @@
import type { PropertyValues } from "lit";
import { html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { mockHistory } from "../../../../demo/src/stubs/history";
@@ -30,7 +29,7 @@ class HcDemo extends HassElement {
`;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._initializeHass();
}
@@ -42,7 +41,7 @@ class HcDemo extends HassElement {
this._updateHass(hassUpdate),
};
const hass = provideHass(this, initial, true);
const hass = (this.hass = provideHass(this, initial));
mockHistory(hass);

View File

@@ -1,5 +1,4 @@
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { css, html, LitElement, type TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import type { LovelaceConfig } from "../../../../src/data/lovelace/config/types";
@@ -65,7 +64,7 @@ class HcLovelace extends LitElement {
`;
}
protected updated(changedProps: PropertyValues<this>) {
protected updated(changedProps) {
super.updated(changedProps);
if (changedProps.has("viewPath") || changedProps.has("lovelaceConfig")) {

View File

@@ -1,6 +1,6 @@
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { createConnection, getAuth } from "home-assistant-js-websocket";
import type { TemplateResult, PropertyValues } from "lit";
import type { TemplateResult } from "lit";
import { html } from "lit";
import { customElement, state } from "lit/decorators";
import { CAST_NS } from "../../../../src/cast/const";
@@ -106,7 +106,7 @@ export class HcMain extends HassElement {
`;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
import("./hc-lovelace");
import("../../../../src/resources/append-ha-style");

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,6 +1,4 @@
/// <reference types="chromecast-caf-sender" />
import { mdiTelevision } from "@mdi/js";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, state } from "lit/decorators";
import type { CastManager } from "../../../src/cast/cast_manager";
@@ -38,7 +36,7 @@ class CastDemoRow extends LitElement implements LovelaceRow {
`;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
import("../../../src/cast/cast_manager").then(({ getCastManager }) =>
getCastManager().then((mgr) => {
@@ -63,7 +61,7 @@ class CastDemoRow extends LitElement implements LovelaceRow {
);
}
protected updated(changedProps: PropertyValues<this>) {
protected updated(changedProps) {
super.updated(changedProps);
this.style.display = this._castManager ? "" : "none";
}

View File

@@ -1,4 +1,4 @@
import type { CSSResultGroup, PropertyValues } from "lit";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { until } from "lit/directives/until";
@@ -102,7 +102,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {
`;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
if (this._hidden) {
this.style.display = "none";

View File

@@ -39,7 +39,7 @@ export class HaDemo extends HomeAssistantAppEl {
this._updateHass(hassUpdate),
};
const hass = provideHass(this, initial, true);
const hass = (this.hass = provideHass(this, initial));
const localizePromise =
// @ts-ignore
this._loadFragmentTranslations(hass.language, "page-demo").then(

View File

@@ -1,21 +0,0 @@
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockAssist = (hass: MockHomeAssistant) => {
// Stub for assist pipeline list — returns empty so developer tools assist
// tab loads without errors.
hass.mockWS("assist_pipeline/pipeline/list", () => ({
pipelines: [],
preferred_pipeline: null,
}));
// Stub for assist pipeline run — immediately sends run-end event so
// the UI does not hang waiting for a response.
hass.mockWS("assist_pipeline/run", (_msg, _hass, onChange) => {
if (onChange) {
onChange({
type: "run-end",
});
}
return null;
});
};

View File

@@ -1,21 +0,0 @@
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockCloud = (hass: MockHomeAssistant) => {
// REST mock for cloud status — returns disconnected so config panel loads
// without errors but without requiring cloud integration.
hass.mockAPI("cloud/status", () => ({
logged_in: false,
cloud: "disconnected",
prefs: {
google_enabled: false,
alexa_enabled: false,
cloudhooks: {},
remote_enabled: false,
},
google_registered: false,
alexa_registered: false,
remote_domain: null,
remote_connected: false,
remote_certificate: null,
}));
};

View File

@@ -1,5 +1,4 @@
import type { LocalizeFunc } from "../../../src/common/translations/localize";
import type { LovelaceInfo } from "../../../src/data/lovelace/resource";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
import {
selectedDemoConfig,
@@ -28,9 +27,6 @@ export const mockLovelace = (
);
});
hass.mockWS("lovelace/info", () =>
Promise.resolve({ resource_mode: "storage" } as LovelaceInfo)
);
hass.mockWS("lovelace/config/save", () => Promise.resolve());
hass.mockWS("lovelace/resources", () => Promise.resolve([]));
hass.mockWS("lovelace/dashboards/list", () => Promise.resolve([]));

View File

@@ -15,6 +15,7 @@ import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
const generateMeanStatistics = (
start: Date,
end: Date,
// eslint-disable-next-line default-param-last
period: "5minute" | "hour" | "day" | "month" = "hour",
maxDiff: number
): StatisticValue[] => {
@@ -48,6 +49,7 @@ const generateMeanStatistics = (
const generateSumStatistics = (
start: Date,
end: Date,
// eslint-disable-next-line default-param-last
period: "5minute" | "hour" | "day" | "month" = "hour",
initValue: number,
maxDiff: number
@@ -84,6 +86,7 @@ const generateSumStatistics = (
const generateCurvedStatistics = (
start: Date,
end: Date,
// eslint-disable-next-line default-param-last
_period: "5minute" | "hour" | "day" | "month" = "hour",
initValue: number,
maxDiff: number,

View File

@@ -1,56 +1,58 @@
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockSensor = (hass: MockHomeAssistant) => {
hass.mockWS("sensor/numeric_device_classes", () => ({
numeric_device_classes: [
"volume_storage",
"gas",
"data_size",
"irradiance",
"wind_speed",
"volatile_organic_compounds",
"volatile_organic_compounds_parts",
"voltage",
"frequency",
"precipitation_intensity",
"volume",
"precipitation",
"battery",
"nitrogen_dioxide",
"speed",
"signal_strength",
"pm1",
"nitrous_oxide",
"atmospheric_pressure",
"data_rate",
"temperature",
"power_factor",
"aqi",
"current",
"volume_flow_rate",
"humidity",
"duration",
"ozone",
"distance",
"pressure",
"pm25",
"weight",
"energy",
"carbon_monoxide",
"apparent_power",
"illuminance",
"energy_storage",
"moisture",
"power",
"water",
"carbon_dioxide",
"ph",
"reactive_power",
"monetary",
"nitrogen_monoxide",
"pm10",
"sound_pressure",
"sulphur_dioxide",
],
}));
hass.mockWS("sensor/numeric_device_classes", () => [
{
numeric_device_classes: [
"volume_storage",
"gas",
"data_size",
"irradiance",
"wind_speed",
"volatile_organic_compounds",
"volatile_organic_compounds_parts",
"voltage",
"frequency",
"precipitation_intensity",
"volume",
"precipitation",
"battery",
"nitrogen_dioxide",
"speed",
"signal_strength",
"pm1",
"nitrous_oxide",
"atmospheric_pressure",
"data_rate",
"temperature",
"power_factor",
"aqi",
"current",
"volume_flow_rate",
"humidity",
"duration",
"ozone",
"distance",
"pressure",
"pm25",
"weight",
"energy",
"carbon_monoxide",
"apparent_power",
"illuminance",
"energy_storage",
"moisture",
"power",
"water",
"carbon_dioxide",
"ph",
"reactive_power",
"monetary",
"nitrogen_monoxide",
"pm10",
"sound_pressure",
"sulphur_dioxide",
],
},
]);
};

View File

@@ -1,5 +0,0 @@
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockUpdate = (hass: MockHomeAssistant) => {
hass.mockWS("update/list", () => []);
};

View File

@@ -1,18 +1,29 @@
// @ts-check
/* eslint-disable import/no-extraneous-dependencies */
import unusedImports from "eslint-plugin-unused-imports";
import globals from "globals";
import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";
import tseslint from "typescript-eslint";
import eslintConfigPrettier from "eslint-config-prettier";
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);
const compat = new FlatCompat({
baseDirectory: _dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});
export default tseslint.config(
js.configs.recommended,
...compat.extends("airbnb-base"),
eslintConfigPrettier,
litConfigs["flat/all"],
tseslint.configs.recommended,
@@ -20,7 +31,6 @@ export default tseslint.config(
tseslint.configs.stylistic,
wcConfigs["flat/recommended"],
a11yConfigs.recommended,
importX.flatConfigs.recommended,
{
plugins: {
"unused-imports": unusedImports,
@@ -48,7 +58,7 @@ export default tseslint.config(
},
settings: {
"import-x/resolver": {
"import/resolver": {
webpack: {
config: "./rspack.config.cjs",
},
@@ -56,65 +66,33 @@ export default tseslint.config(
},
rules: {
"array-callback-return": ["error", { allowImplicit: true }],
"block-scoped-var": "error",
"consistent-return": "error",
curly: ["error", "multi-line"],
"default-case-last": "error",
eqeqeq: ["error", "always", { null: "ignore" }],
"guard-for-in": "error",
"no-await-in-loop": "error",
"no-caller": "error",
"no-constructor-return": "error",
"no-eval": "error",
"no-extend-native": "error",
"no-implied-eval": "error",
"no-iterator": "error",
"no-new-func": "error",
"no-new-wrappers": "error",
"no-octal-escape": "error",
"no-promise-executor-return": "error",
"no-return-assign": ["error", "always"],
"no-script-url": "error",
"no-self-compare": "error",
"no-sequences": "error",
"no-template-curly-in-string": "error",
"no-unreachable-loop": "error",
"no-else-return": ["error", { allowElseIf: false }],
"no-lonely-if": "error",
"no-unneeded-ternary": ["error", { defaultAssignment: false }],
"no-useless-computed-key": "error",
"no-useless-concat": "error",
"no-useless-rename": "error",
"no-useless-return": "error",
"one-var": ["error", "never"],
"operator-assignment": ["error", "always"],
"prefer-arrow-callback": "error",
"prefer-exponentiation-operator": "error",
"prefer-object-spread": "error",
"prefer-regex-literals": ["error", { disallowRedundantWrapping: true }],
"symbol-description": "error",
yoda: "error",
// TODO: Enable once violations are fixed (43 instances as of 2026-04)
// "no-useless-assignment": "error",
"no-useless-assignment": "error",
// Project rules
"class-methods-use-this": "off",
"new-cap": "off",
"prefer-template": "off",
"object-shorthand": "off",
"func-names": "off",
"no-underscore-dangle": "off",
strict: "off",
"no-plusplus": "off",
"no-bitwise": "error",
"comma-dangle": "off",
"vars-on-top": "off",
"no-continue": "off",
"no-param-reassign": "off",
"no-multi-assign": "off",
"no-console": "error",
radix: "off",
"no-alert": "off",
"no-nested-ternary": "off",
"prefer-destructuring": "off",
"no-restricted-globals": [2, "event"],
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
"wc/no-self-class": "off",
"prefer-promise-reject-errors": "off",
"import/prefer-default-export": "off",
"import/no-default-export": "off",
"import/no-unresolved": "off",
"import/no-cycle": "off",
// import-x rules
"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",
{
@@ -122,27 +100,19 @@ 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",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-shadow": ["error"],
"@typescript-eslint/naming-convention": [
@@ -206,6 +176,7 @@ export default tseslint.config(
"lit-a11y/role-has-required-aria-attrs": "error",
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/no-import-type-side-effects": "error",
camelcase: "off",
"@typescript-eslint/no-dynamic-delete": "off",
"@typescript-eslint/no-empty-object-type": [
"error",
@@ -214,6 +185,7 @@ export default tseslint.config(
allowObjectTypes: "always",
},
],
"no-use-before-define": "off",
},
},
{
@@ -222,18 +194,6 @@ export default tseslint.config(
globals: globals.audioWorklet,
},
},
{
files: ["src/entrypoints/service-worker.ts"],
languageOptions: {
globals: globals.serviceworker,
},
},
{
files: ["test/e2e/*.mjs"],
languageOptions: {
globals: globals.node,
},
},
{
plugins: {
html,

View File

@@ -1,4 +1,4 @@
import type { TemplateResult, PropertyValues } from "lit";
import type { TemplateResult } from "lit";
import { html, LitElement, css, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
@@ -50,7 +50,7 @@ class DemoBlackWhiteRow extends LitElement {
`;
}
firstUpdated(changedProps: PropertyValues<this>) {
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),

View File

@@ -1,5 +1,5 @@
import { load } from "js-yaml";
import type { PropertyValues } from "lit";
import type { PropertyValueMap } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
@@ -60,7 +60,9 @@ class DemoCard extends LitElement {
this._size = await this._card?.getCardSize();
}
protected update(_changedProperties: PropertyValues<this>): void {
protected update(
_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>
): void {
super.update(_changedProperties);
this._updateSize();
}

View File

@@ -1,22 +1,17 @@
import "@material/mwc-drawer";
import "@material/mwc-top-app-bar-fixed";
import { mdiMenu, mdiSwapHorizontal } from "@mdi/js";
import { mdiMenu } from "@mdi/js";
import type { PropertyValues } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, query, state } from "lit/decorators";
import { dynamicElement } from "../../src/common/dom/dynamic-element-directive";
import { setDirectionStyles } from "../../src/common/util/compute_rtl";
import "../../src/components/ha-button";
import { HaExpansionPanel } from "../../src/components/ha-expansion-panel";
import "../../src/components/ha-icon-button";
import "../../src/components/ha-svg-icon";
import "../../src/managers/notification-manager";
import { haStyle } from "../../src/resources/styles";
import { PAGES, SIDEBAR } from "../build/import-pages";
import "./components/page-description";
const RTL_STORAGE_KEY = "gallery-rtl";
const GITHUB_DEMO_URL =
"https://github.com/home-assistant/frontend/blob/dev/gallery/src/pages/";
@@ -34,8 +29,6 @@ class HaGallery extends LitElement {
document.location.hash.substring(1) ||
`${SIDEBAR[0].category}/${SIDEBAR[0].pages![0]}`;
@state() private _rtl = localStorage.getItem(RTL_STORAGE_KEY) === "true";
@query("notification-manager")
private _notifications!: HTMLElementTagNameMap["notification-manager"];
@@ -104,43 +97,33 @@ class HaGallery extends LitElement {
${dynamicElement(`demo-${this._page.replace("/", "-")}`)}
</div>
<div class="page-footer">
<div class="edit-docs">
<div class="header">Help us to improve our documentation</div>
<div class="secondary">
Suggest an edit to this page, or provide/view feedback for this
page.
</div>
<div>
${PAGES[this._page].description ||
Object.keys(PAGES[this._page].metadata).length > 0
? html`
<a
href=${`${GITHUB_DEMO_URL}${this._page}.markdown`}
target="_blank"
>
Edit text
</a>
`
: ""}
${PAGES[this._page].demo
? html`
<a
href=${`${GITHUB_DEMO_URL}${this._page}.ts`}
target="_blank"
>
Edit demo
</a>
`
: ""}
</div>
<div class="header">Help us to improve our documentation</div>
<div class="secondary">
Suggest an edit to this page, or provide/view feedback for this
page.
</div>
<div class="rtl-toggle">
<ha-icon-button
@click=${this._toggleRtl}
.label=${this._rtl ? "Switch to LTR" : "Switch to RTL"}
>
<ha-svg-icon .path=${mdiSwapHorizontal}></ha-svg-icon>
</ha-icon-button>
<div>
${PAGES[this._page].description ||
Object.keys(PAGES[this._page].metadata).length > 0
? html`
<a
href=${`${GITHUB_DEMO_URL}${this._page}.markdown`}
target="_blank"
>
Edit text
</a>
`
: ""}
${PAGES[this._page].demo
? html`
<a
href=${`${GITHUB_DEMO_URL}${this._page}.ts`}
target="_blank"
>
Edit demo
</a>
`
: ""}
</div>
</div>
</div>
@@ -152,11 +135,9 @@ class HaGallery extends LitElement {
`;
}
firstUpdated(changedProps: PropertyValues<this>) {
firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._applyDirection();
this.addEventListener("show-notification", (ev) =>
this._notifications.showDialog({ message: ev.detail.message })
);
@@ -183,11 +164,6 @@ class HaGallery extends LitElement {
updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("_rtl")) {
this._applyDirection();
}
if (!changedProps.has("_page")) {
return;
}
@@ -210,15 +186,6 @@ class HaGallery extends LitElement {
this._drawer.open = !this._drawer.open;
}
private _toggleRtl() {
this._rtl = !this._rtl;
localStorage.setItem(RTL_STORAGE_KEY, String(this._rtl));
}
private _applyDirection() {
setDirectionStyles(this._rtl ? "rtl" : "ltr", this);
}
static styles = [
haStyle,
css`
@@ -271,16 +238,11 @@ class HaGallery extends LitElement {
}
.page-footer {
display: flex;
border-radius: var(--ha-border-radius-lg);
background-color: var(--primary-background-color);
}
.edit-docs {
flex: 1;
text-align: center;
margin: 16px;
padding: 16px;
border-radius: var(--ha-border-radius-lg);
background-color: var(--primary-background-color);
}
.page-footer div {
@@ -304,18 +266,6 @@ class HaGallery extends LitElement {
margin: 0 8px;
text-decoration: none;
}
.rtl-toggle {
padding: var(--ha-space-4);
display: inline-flex;
align-items: flex-end;
margin-top: 12px !important;
}
.rtl-toggle ha-icon-button {
border: 1px solid var(--divider-color);
border-radius: var(--ha-border-radius-pill);
}
`,
];
}

View File

@@ -1,5 +1,4 @@
import { dump } from "js-yaml";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../src/components/ha-card";
@@ -172,7 +171,7 @@ export class DemoAutomationDescribeAction extends LitElement {
`;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");

View File

@@ -1,5 +1,4 @@
import { dump } from "js-yaml";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../src/components/ha-card";
@@ -97,7 +96,7 @@ export class DemoAutomationDescribeCondition extends LitElement {
`;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");

View File

@@ -1,5 +1,4 @@
import { dump } from "js-yaml";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../src/components/ha-card";
@@ -120,7 +119,7 @@ export class DemoAutomationDescribeTrigger extends LitElement {
`;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");

View File

@@ -1,6 +1,5 @@
/* eslint-disable lit/no-template-arrow */
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../src/components/ha-card";
@@ -52,7 +51,7 @@ export class DemoAutomationTraceTimeline extends LitElement {
`;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");

View File

@@ -1,6 +1,5 @@
/* eslint-disable lit/no-template-arrow */
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../src/components/ha-card";
@@ -52,7 +51,7 @@ export class DemoAutomationTrace extends LitElement {
`;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");

View File

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

View File

@@ -1,217 +0,0 @@
import { css, html, LitElement } from "lit";
import { customElement, state } from "lit/decorators";
import { mdiCog, mdiHelp } from "@mdi/js";
import "../../../../src/components/ha-adaptive-popover";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-dialog-footer";
import "../../../../src/components/ha-form/ha-form";
import "../../../../src/components/ha-icon-button";
import type { HASSDomCurrentTargetEvent } from "../../../../src/common/dom/fire_event";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../../src/types";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
const SCHEMA: HaFormSchema[] = [
{ type: "string", name: "Name", default: "", autofocus: true },
{ type: "string", name: "Email", default: "" },
];
type PopoverType = false | "basic" | "small" | "large" | "form" | "actions";
@customElement("demo-components-ha-adaptive-popover")
export class DemoHaAdaptivePopover extends LitElement {
@state() private _openPopover: PopoverType = false;
@state() private _hass?: HomeAssistant;
@state() private _dialogAnchor?: HTMLElement;
protected firstUpdated() {
this._hass = provideHass(this);
}
protected render() {
return html`
<div class="content">
<h1>Adaptive popover <code>&lt;ha-adaptive-popover&gt;</code></h1>
<p class="subtitle">
Responsive popover component that opens as an anchored popover on
desktop and falls back to adaptive dialog behavior otherwise.
</p>
<h2>Demos</h2>
<div class="buttons">
<ha-button @click=${this._handleOpenPopover("basic")}
>Basic adaptive popover</ha-button
>
<ha-button @click=${this._handleOpenPopover("small")}
>Small adaptive popover</ha-button
>
<ha-button @click=${this._handleOpenPopover("large")}
>Large adaptive popover</ha-button
>
<ha-button @click=${this._handleOpenPopover("form")}
>Adaptive popover with form</ha-button
>
<ha-button @click=${this._handleOpenPopover("actions")}
>Adaptive popover with actions</ha-button
>
</div>
<ha-card>
<div class="card-content">
<p>
<strong>Tip:</strong> On desktop, this uses the opener as the
popover anchor. On narrow screens, it falls back to adaptive
dialog behavior.
</p>
</div>
</ha-card>
<ha-adaptive-popover
.hass=${this._hass}
.open=${this._openPopover === "basic"}
.dialogAnchor=${this._dialogAnchor}
header-title="Basic adaptive popover"
header-subtitle="Anchored to the opener on desktop"
@closed=${this._handleClosed}
>
<div>
This component uses a desktop popover when an anchor is available,
and adaptive dialog behavior otherwise.
</div>
</ha-adaptive-popover>
<ha-adaptive-popover
.hass=${this._hass}
.open=${this._openPopover === "small"}
.dialogAnchor=${this._dialogAnchor}
width="small"
header-title="Small adaptive popover"
@closed=${this._handleClosed}
>
<div>This popover uses the small width preset (320px).</div>
</ha-adaptive-popover>
<ha-adaptive-popover
.hass=${this._hass}
.open=${this._openPopover === "large"}
.dialogAnchor=${this._dialogAnchor}
width="large"
header-title="Large adaptive popover"
@closed=${this._handleClosed}
>
<div>This popover uses the large width preset (1024px).</div>
</ha-adaptive-popover>
<ha-adaptive-popover
.hass=${this._hass}
.open=${this._openPopover === "form"}
.dialogAnchor=${this._dialogAnchor}
header-title="Adaptive popover with form"
header-subtitle="This is an adaptive popover with a form"
@closed=${this._handleClosed}
>
<ha-form autofocus .schema=${SCHEMA}></ha-form>
<ha-dialog-footer slot="footer">
<ha-button
@click=${this._handleClosed}
slot="secondaryAction"
variant="plain"
>Cancel</ha-button
>
<ha-button
@click=${this._handleClosed}
slot="primaryAction"
variant="accent"
>Submit</ha-button
>
</ha-dialog-footer>
</ha-adaptive-popover>
<ha-adaptive-popover
.hass=${this._hass}
.open=${this._openPopover === "actions"}
.dialogAnchor=${this._dialogAnchor}
header-title="Adaptive popover with actions"
header-subtitle="This is an adaptive popover with header actions"
@closed=${this._handleClosed}
>
<div slot="headerActionItems">
<ha-icon-button label="Settings" path=${mdiCog}></ha-icon-button>
<ha-icon-button label="Help" path=${mdiHelp}></ha-icon-button>
</div>
<div>Adaptive popover content</div>
</ha-adaptive-popover>
</div>
`;
}
private _handleOpenPopover =
(popover: PopoverType) => (ev?: HASSDomCurrentTargetEvent<HTMLElement>) => {
this._dialogAnchor = ev?.currentTarget;
this._openPopover = popover;
};
private _handleClosed = () => {
this._dialogAnchor = undefined;
this._openPopover = false;
};
static styles = [
css`
:host {
display: block;
padding: var(--ha-space-4);
}
.content {
max-width: 1000px;
margin: 0 auto;
}
h1 {
margin-top: 0;
margin-bottom: var(--ha-space-2);
}
h2 {
margin-top: var(--ha-space-6);
margin-bottom: var(--ha-space-3);
}
p {
margin: var(--ha-space-2) 0;
line-height: 1.6;
}
.subtitle {
color: var(--secondary-text-color);
font-size: 1.1em;
margin-bottom: var(--ha-space-4);
}
.buttons {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: var(--ha-space-2);
margin: var(--ha-space-4) 0;
}
.card-content {
padding: var(--ha-space-3);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-adaptive-popover": DemoHaAdaptivePopover;
}
}

View File

@@ -1,4 +1,4 @@
import type { TemplateResult, PropertyValues } from "lit";
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";
@@ -160,7 +160,7 @@ export class DemoHaAlert extends LitElement {
`;
}
firstUpdated(changedProps: PropertyValues<this>) {
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),

View File

@@ -1,5 +1,5 @@
import { mdiButtonCursor, mdiHome } from "@mdi/js";
import type { TemplateResult, PropertyValues } from "lit";
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";
@@ -84,7 +84,7 @@ export class DemoHaBadge extends LitElement {
`;
}
firstUpdated(changedProps: PropertyValues<this>) {
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),

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

@@ -1,5 +1,5 @@
import { mdiHome } from "@mdi/js";
import type { TemplateResult, PropertyValues } from "lit";
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";
@@ -118,7 +118,7 @@ export class DemoHaButton extends LitElement {
`;
}
firstUpdated(changedProps: PropertyValues<this>) {
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),

View File

@@ -9,7 +9,6 @@ import { css, html, LitElement } from "lit";
import { customElement, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { repeat } from "lit/directives/repeat";
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-control-switch";
@@ -51,100 +50,59 @@ export class DemoHaControlSwitch extends LitElement {
protected render(): TemplateResult {
return html`
<div class="themes">
${["light", "dark"].map(
(mode) => html`
<div class=${mode}>
<ha-card header="ha-control-switch ${mode}">
${repeat(switches, (sw) => {
const { id, label, ...config } = sw;
return html`
<div class="card-content">
<label id="${mode}-${id}">${label}</label>
<pre>Config: ${JSON.stringify(config)}</pre>
<ha-control-switch
.checked=${this.checked}
class=${ifDefined(config.class)}
@change=${this.handleValueChanged}
.pathOn=${mdiLightbulb}
.pathOff=${mdiLightbulbOff}
.label=${label}
?disabled=${config.disabled}
?reversed=${config.reversed}
>
</ha-control-switch>
</div>
`;
})}
<div class="card-content">
<p class="title"><b>Vertical</b></p>
<div class="vertical-switches">
${repeat(switches, (sw) => {
const { label, ...config } = sw;
return html`
<ha-control-switch
.checked=${this.checked}
vertical
class=${ifDefined(config.class)}
@change=${this.handleValueChanged}
.label=${label}
.pathOn=${mdiGarageOpen}
.pathOff=${mdiGarage}
?disabled=${config.disabled}
?reversed=${config.reversed}
>
</ha-control-switch>
`;
})}
</div>
</div>
</ha-card>
${repeat(switches, (sw) => {
const { id, label, ...config } = sw;
return html`
<ha-card>
<div class="card-content">
<label id=${id}>${label}</label>
<pre>Config: ${JSON.stringify(config)}</pre>
<ha-control-switch
.checked=${this.checked}
class=${ifDefined(config.class)}
@change=${this.handleValueChanged}
.pathOn=${mdiLightbulb}
.pathOff=${mdiLightbulbOff}
.label=${label}
?disabled=${config.disabled}
?reversed=${config.reversed}
>
</ha-control-switch>
</div>
`
)}
</div>
</ha-card>
`;
})}
<ha-card>
<div class="card-content">
<p class="title"><b>Vertical</b></p>
<div class="vertical-switches">
${repeat(switches, (sw) => {
const { id, label, ...config } = sw;
return html`
<ha-control-switch
.checked=${this.checked}
vertical
class=${ifDefined(config.class)}
@change=${this.handleValueChanged}
.label=${label}
.pathOn=${mdiGarageOpen}
.pathOff=${mdiGarage}
?disabled=${config.disabled}
?reversed=${config.reversed}
>
</ha-control-switch>
`;
})}
</div>
</div>
</ha-card>
`;
}
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: block;
}
.themes {
display: flex;
flex-direction: row;
justify-content: center;
flex-wrap: wrap;
gap: 16px;
padding: 16px;
}
.dark,
.light {
display: block;
background-color: var(--primary-background-color);
padding: 16px;
border-radius: var(--ha-border-radius-md);
}
ha-card {
max-width: 600px;
margin: 0 auto;
margin: 24px auto;
}
pre {
margin-top: 0;

View File

@@ -8,7 +8,7 @@ import {
mdiContentPaste,
mdiDelete,
} from "@mdi/js";
import type { TemplateResult, PropertyValues } from "lit";
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";
@@ -80,7 +80,7 @@ export class DemoHaDropdown extends LitElement {
`;
}
firstUpdated(changedProps: PropertyValues<this>) {
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),

View File

@@ -1,6 +1,6 @@
import { ContextProvider } from "@lit/context";
import { mdiMagnify } from "@mdi/js";
import type { TemplateResult, PropertyValues } from "lit";
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";
@@ -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,18 +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,
});
}
@@ -178,7 +171,7 @@ export class DemoHaInput extends LitElement {
`;
}
firstUpdated(changedProps: PropertyValues<this>) {
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),

View File

@@ -1,188 +0,0 @@
---
title: List
---
# List
The list family provides accessible, keyboard-navigable list containers and
item variants. Pick the container based on semantics, then the item based on
interactivity.
## Containers
### `<ha-list-base>`
A styled container with roving-tabindex keyboard navigation. Host role is
`list`. Children should be `<ha-list-item-*>`. Arrow keys rove focus;
Home/End jump to the first/last enabled item; Enter/Space activates the
focused item.
**Attributes**
| Name | Type | Default | Description |
| ------------ | ------- | ------- | ------------------------------ |
| `wrap-focus` | Boolean | `false` | Arrow keys wrap past the ends. |
| `aria-label` | String | — | Accessible name. |
**Events**
- `ha-list-activated` — Enter/Space on a focused item. Detail
`{ index: number, item: HaListItemBase }`.
**Methods**
- `focus()` — focus the active item (or the first focusable one).
- `focusItemAtIndex(index)` — make the item at `index` active and focus it.
- `getActiveItemIndex()` — current active index, or `-1`.
- `setActiveItemIndex(index, focusItem?)` — move the active index without
necessarily focusing.
- `updateListItems()` — re-discover slotted items (called automatically on
slotchange).
**CSS parts**
- `base` — the outer `<div role="list">`.
**CSS custom properties**
- `--ha-list-gap` — spacing between items. Defaults to `0`.
- `--ha-list-padding` — padding around the list. Defaults to `0`.
### `<ha-list-selectable>`
Selectable list. Extends `ha-list-base`. Host role is `listbox`; items must be
`<ha-list-item-option>` (role `option`). Set `multi` for multi-select; the
host reflects `aria-multiselectable`.
**Attributes**
| Name | Type | Default | Description |
| ------- | ------- | ------- | -------------------------------------- |
| `multi` | Boolean | `false` | Allow multiple options to be selected. |
**Events**
- `ha-list-selected` — selection changed. Detail
`{ index: number | Set<number>, diff: { added: Set<number>, removed: Set<number> } }`.
`index` is a `number` in single mode (`-1` when nothing selected) and a
`Set<number>` in multi mode.
**Methods / getters**
- `selected` (getter) — current selection (`number` or `Set<number>`).
- `selectedItems` (getter) — selected `HaListItemOption` elements, in index
order.
- `setSelected(indices)` — replace the entire selection.
- `select(index)` — add `index` to the selection (replaces in single mode).
- `toggle(index, force?)` — toggle a single index, or force on/off.
- `clearSelection()` — clear all.
### `<ha-list-nav>`
Same as `ha-list-base`, but wrapped in a `<nav>` landmark
(`<nav><div role="list">…</div></nav>`). Use `aria-label` to name the
landmark — the value is forwarded to the inner `<nav>`. Items should be
`<ha-list-item-button>` with an `href`.
**CSS parts**
- `nav` — the `<nav>` wrapper.
- `base` — the inner `<div role="list">`.
## Items
All items inherit from `ha-row-item`, which provides the row layout and the
shared slots/attributes below.
### Shared row layout (`ha-row-item`)
**Slots**
- `start` — leading container (icon/avatar).
- `end` — trailing container (meta/chevron).
- `headline` — primary text (overrides the `headline` attribute).
- `supporting-text` — secondary text (overrides the `supporting-text` attribute).
- `content` — escape hatch: replaces the entire middle column.
**Attributes**
| Name | Type | Default | Description |
| ----------------- | ------- | ------- | --------------------------------------- |
| `headline` | String | — | Primary text. Overridden by the slot. |
| `supporting-text` | String | — | Secondary text. Overridden by the slot. |
| `disabled` | Boolean | `false` | Dims the row and blocks pointer events. |
**CSS parts**
`base`, `start`, `content`, `headline`, `supporting-text`, `end`.
**CSS custom properties**
- `--ha-row-item-padding-block` — vertical padding.
- `--ha-row-item-padding-inline` — horizontal padding.
- `--ha-row-item-gap` — gap between `start`, `content`, and `end`.
- `--ha-row-item-min-height` — minimum row height (default `48px`).
### `<ha-list-item-base>`
Non-interactive list row. Host role is `listitem`. Inherits everything from
`ha-row-item`.
**Attributes**
- `interactive` (Boolean, default `false`) — opt this row into the parent
list's roving tabindex. Useful for sortable rows that need keyboard focus
but no click action. Interactive subclasses set this automatically.
**CSS custom properties**
- `--ha-list-item-focus-radius` — focus outline border-radius.
- `--ha-list-item-focus-width` — focus outline width (steady state).
- `--ha-list-item-focus-width-start` — focus outline width at the start of
the focus-in animation.
- `--ha-list-item-focus-offset` — focus outline offset.
- `--ha-list-item-focus-background` — background color on keyboard focus.
### `<ha-list-item-button>`
Interactive row. Renders an inner `<a>` when `href` is set, otherwise a
`<button>`. The full row is the hit target. When placed inside a list using
roving tabindex, the host is the tab stop and the inner element carries
`tabindex="-1"`.
**Attributes**
- `href` (String) — when set, renders an `<a>` instead of a `<button>`.
- `target` (String) — anchor `target` (requires `href`).
- `rel` (String) — anchor `rel` (requires `href`).
- `download` (String) — anchor `download` (requires `href`).
**CSS parts**
- `ripple` — the ripple effect element.
### `<ha-list-item-option>`
Selectable row. Host role is `option`; reflects `aria-selected`. Designed to
sit inside `<ha-list-selectable>`, which owns selection state and toggles
`selected` on this item — the option itself does not fire selection events.
**Attributes**
- `selected` (Boolean, default `false`, reflected) — set by the parent
`ha-list-selectable`.
- `value` (String) — value identifying the option.
- `appearance` (`"line"` | `"checkbox"`, default `"line"`) — `"line"`
highlights the row; `"checkbox"` renders a decorative `<ha-checkbox>`.
- `selection-position` (`"start"` | `"end"`, default `"start"`) — side the
checkbox sits on when `appearance="checkbox"`.
**CSS parts**
- `checkbox` — wrapper around the `<ha-checkbox>` when `appearance="checkbox"`.
- `ripple` — the ripple effect element.
**CSS custom properties**
- `--ha-list-item-selected-background` — background color when selected
(`appearance="line"`).

View File

@@ -1,415 +0,0 @@
import {
mdiAccount,
mdiChevronRight,
mdiCog,
mdiHome,
mdiInformationOutline,
mdiMapMarker,
mdiOpenInNew,
mdiViewDashboard,
mdiWifi,
} from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, state } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/item/ha-list-item-base";
import "../../../../src/components/item/ha-list-item-button";
import "../../../../src/components/item/ha-list-item-option";
import "../../../../src/components/list/ha-list-base";
import "../../../../src/components/list/ha-list-nav";
import "../../../../src/components/list/ha-list-selectable";
import type { HaListSelectedDetail } from "../../../../src/components/list/types";
type Appearance = "line" | "checkbox";
type Position = "start" | "end";
const appearances: Appearance[] = ["line", "checkbox"];
const positions: Position[] = ["start", "end"];
const selectedStates = [false, true];
const disabledStates = [false, true];
@customElement("demo-components-ha-list")
export class DemoHaList extends LitElement {
@state() private _buttonClicks = 0;
@state() private _single: number | Set<number> = -1;
@state() private _multiLine: number | Set<number> = new Set();
@state() private _multiCheckStart: number | Set<number> = new Set();
@state() private _multiCheckEnd: number | Set<number> = new Set();
private _options = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon"];
protected render(): TemplateResult {
return html`
<h2>ha-list-base</h2>
<p>
Styled container with keyboard focus navigation. Children should be
<code>ha-list-item-*</code>.
</p>
<ha-card header="Info list (non-interactive rows)">
<ha-list-base aria-label="Device info">
<ha-list-item-base
headline="IP address"
supporting-text="192.168.1.42"
>
<ha-svg-icon slot="start" .path=${mdiWifi}></ha-svg-icon>
</ha-list-item-base>
<ha-list-item-base headline="Location" supporting-text="Living room">
<ha-svg-icon slot="start" .path=${mdiMapMarker}></ha-svg-icon>
</ha-list-item-base>
<ha-list-item-base headline="Firmware" supporting-text="2026.4.1">
<ha-svg-icon
slot="start"
.path=${mdiInformationOutline}
></ha-svg-icon>
</ha-list-item-base>
</ha-list-base>
</ha-card>
<ha-card header="Vertical list (default)">
<ha-list-base aria-label="Example list">
<ha-list-item-button>
<ha-svg-icon slot="start" .path=${mdiHome}></ha-svg-icon>
<span slot="headline">First row</span>
<span slot="supporting-text">Supporting text</span>
<ha-svg-icon slot="end" .path=${mdiChevronRight}></ha-svg-icon>
</ha-list-item-button>
<ha-list-item-button>
<ha-svg-icon slot="start" .path=${mdiAccount}></ha-svg-icon>
<span slot="headline">Second row</span>
</ha-list-item-button>
<ha-list-item-button disabled>
<span slot="headline">Disabled row</span>
</ha-list-item-button>
<ha-list-item-button>
<span slot="headline">Fourth row</span>
</ha-list-item-button>
</ha-list-base>
</ha-card>
<ha-card header="Vertical list with wrap-focus">
<ha-list-base wrap-focus aria-label="Wrap focus">
<ha-list-item-button>
<span slot="headline">A</span>
</ha-list-item-button>
<ha-list-item-button>
<span slot="headline">B</span>
</ha-list-item-button>
<ha-list-item-button>
<span slot="headline">C</span>
</ha-list-item-button>
</ha-list-base>
</ha-card>
<h2>ha-list-item-base</h2>
<p>Non-interactive base row with slot permutations.</p>
<ha-card header="Slot permutations">
<ha-list-base aria-label="Slot permutations">
<ha-list-item-base headline="Headline only"></ha-list-item-base>
<ha-list-item-base
headline="Headline"
supporting-text="Supporting text"
></ha-list-item-base>
<ha-list-item-base headline="Start + headline">
<ha-svg-icon slot="start" .path=${mdiHome}></ha-svg-icon>
</ha-list-item-base>
<ha-list-item-base headline="Start + headline + end">
<ha-svg-icon slot="start" .path=${mdiHome}></ha-svg-icon>
<ha-svg-icon slot="end" .path=${mdiChevronRight}></ha-svg-icon>
</ha-list-item-base>
<ha-list-item-base
headline="Full row"
supporting-text="All slots filled"
>
<ha-svg-icon slot="start" .path=${mdiHome}></ha-svg-icon>
<ha-svg-icon slot="end" .path=${mdiChevronRight}></ha-svg-icon>
</ha-list-item-base>
<ha-list-item-base>
<div slot="content" class="custom-content">
<strong>Custom content escape hatch</strong>
<span>Replaces the whole middle column</span>
</div>
</ha-list-item-base>
<ha-list-item-base headline="Disabled row" disabled>
<ha-svg-icon slot="start" .path=${mdiHome}></ha-svg-icon>
</ha-list-item-base>
</ha-list-base>
</ha-card>
<h2>ha-list-item-button</h2>
<p>
Interactive row. Renders an inner <code>&lt;a&gt;</code> when
<code>href</code> is set, otherwise a <code>&lt;button&gt;</code>.
</p>
<ha-card header="Button (default) / link (with href)">
<ha-list-base aria-label="Button items">
<ha-list-item-button @click=${this._onButtonClick}>
<ha-svg-icon slot="start" .path=${mdiHome}></ha-svg-icon>
<span slot="headline">Button (clicks: ${this._buttonClicks})</span>
</ha-list-item-button>
<ha-list-item-button
href="https://www.home-assistant.io/"
target="_blank"
rel="noopener noreferrer"
>
<ha-svg-icon slot="start" .path=${mdiOpenInNew}></ha-svg-icon>
<span slot="headline">Link (opens in new tab)</span>
<span slot="supporting-text"
>Cmd/Ctrl-click still opens in new tab</span
>
</ha-list-item-button>
<ha-list-item-button disabled>
<span slot="headline">Disabled button</span>
</ha-list-item-button>
<ha-list-item-button href="#nope" disabled>
<span slot="headline">Disabled link</span>
</ha-list-item-button>
</ha-list-base>
</ha-card>
<h2>ha-list-selectable + ha-list-item-option</h2>
<p>
Selectable list (<code>role="listbox"</code>). Items must be
<code>ha-list-item-option</code>. Set <code>multi</code> for
multi-select.
</p>
<ha-card header="Single select, appearance=line">
<ha-list-selectable
aria-label="Single select"
@ha-list-selected=${this._onSingle}
>
${this._options.map(
(o, i) => html`
<ha-list-item-option
.value=${o}
?selected=${this._isSel(this._single, i)}
>
<span slot="headline">${o}</span>
</ha-list-item-option>
`
)}
</ha-list-selectable>
<pre>selected: ${JSON.stringify(this._toJson(this._single))}</pre>
</ha-card>
<ha-card header="Multi select, appearance=line">
<ha-list-selectable
multi
aria-label="Multi select line"
@ha-list-selected=${this._onMultiLine}
>
${this._options.map(
(o, i) => html`
<ha-list-item-option
.value=${o}
?selected=${this._isSel(this._multiLine, i)}
>
<span slot="headline">${o}</span>
</ha-list-item-option>
`
)}
</ha-list-selectable>
<pre>selected: ${JSON.stringify(this._toJson(this._multiLine))}</pre>
</ha-card>
<ha-card
header='Multi select, appearance=checkbox, selection-position="start"'
>
<ha-list-selectable
multi
aria-label="Multi checkbox start"
@ha-list-selected=${this._onMultiCheckStart}
>
${this._options.map(
(o, i) => html`
<ha-list-item-option
appearance="checkbox"
selection-position="start"
.value=${o}
?selected=${this._isSel(this._multiCheckStart, i)}
>
<span slot="headline">${o}</span>
</ha-list-item-option>
`
)}
</ha-list-selectable>
<pre>
selected: ${JSON.stringify(this._toJson(this._multiCheckStart))}</pre
>
</ha-card>
<ha-card
header='Multi select, appearance=checkbox, selection-position="end"'
>
<ha-list-selectable
multi
aria-label="Multi checkbox end"
@ha-list-selected=${this._onMultiCheckEnd}
>
${this._options.map(
(o, i) => html`
<ha-list-item-option
appearance="checkbox"
selection-position="end"
.value=${o}
?selected=${this._isSel(this._multiCheckEnd, i)}
>
<span slot="headline">${o}</span>
<span slot="supporting-text">${o.length} characters</span>
</ha-list-item-option>
`
)}
</ha-list-selectable>
<pre>
selected: ${JSON.stringify(this._toJson(this._multiCheckEnd))}</pre
>
</ha-card>
<ha-card header="Option: all combinations">
<div class="grid">
${appearances.map((appearance) =>
positions.map((position) =>
selectedStates.map((selected) =>
disabledStates.map(
(disabled) => html`
<div role="listbox" class="wrap" aria-label="single option">
<ha-list-item-option
appearance=${appearance}
selection-position=${position}
?selected=${selected}
?disabled=${disabled}
>
<span slot="headline"
>${appearance} / pos=${position}</span
>
<span slot="supporting-text"
>selected=${String(selected)}
disabled=${String(disabled)}</span
>
</ha-list-item-option>
</div>
`
)
)
)
)}
</div>
</ha-card>
<h2>ha-list-nav</h2>
<p>
Same as <code>ha-list-base</code> but wrapped in a
<code>&lt;nav&gt;</code> landmark.
</p>
<ha-card header="Sidebar-style navigation">
<ha-list-nav aria-label="Primary navigation">
${[
{ name: "Overview", path: "#overview", icon: mdiHome },
{ name: "Dashboards", path: "#dashboards", icon: mdiViewDashboard },
{ name: "Map", path: "#map", icon: mdiMapMarker },
{ name: "Settings", path: "#settings", icon: mdiCog },
].map(
(p) => html`
<ha-list-item-button .href=${p.path}>
<ha-svg-icon slot="start" .path=${p.icon}></ha-svg-icon>
<span slot="headline">${p.name}</span>
<ha-svg-icon slot="end" .path=${mdiChevronRight}></ha-svg-icon>
</ha-list-item-button>
`
)}
</ha-list-nav>
</ha-card>
`;
}
private _isSel(value: number | Set<number>, index: number): boolean {
if (typeof value === "number") {
return value === index;
}
return value.has(index);
}
private _toJson(value: number | Set<number>): unknown {
return value instanceof Set ? [...value] : value;
}
private _onButtonClick = () => {
this._buttonClicks++;
};
private _onSingle = (ev: CustomEvent<HaListSelectedDetail>) => {
this._single = ev.detail.index;
};
private _onMultiLine = (ev: CustomEvent<HaListSelectedDetail>) => {
this._multiLine = ev.detail.index;
};
private _onMultiCheckStart = (ev: CustomEvent<HaListSelectedDetail>) => {
this._multiCheckStart = ev.detail.index;
};
private _onMultiCheckEnd = (ev: CustomEvent<HaListSelectedDetail>) => {
this._multiCheckEnd = ev.detail.index;
};
static styles = css`
:host {
display: flex;
flex-direction: column;
gap: var(--ha-space-4);
padding: var(--ha-space-6);
}
h2 {
margin: var(--ha-space-4) 0 0;
font-size: var(--ha-font-size-xl);
font-weight: var(--ha-font-weight-medium);
}
p {
margin: 0 0 var(--ha-space-2);
color: var(--secondary-text-color);
}
ha-card {
max-width: 560px;
}
pre {
padding: var(--ha-space-4);
background: var(--secondary-background-color);
margin: 0;
}
.custom-content {
display: flex;
flex-direction: column;
gap: var(--ha-space-1);
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--ha-space-3);
padding: var(--ha-space-3);
}
.wrap {
border: 1px solid var(--divider-color);
border-radius: var(--ha-border-radius-sm);
}
.drag-handle {
cursor: grab;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-list": DemoHaList;
}
}

View File

@@ -1,4 +1,4 @@
import type { TemplateResult, PropertyValues } from "lit";
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";
@@ -65,7 +65,7 @@ export class DemoHaProgressButton extends LitElement {
`;
}
firstUpdated(changedProps: PropertyValues<this>) {
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),

View File

@@ -43,22 +43,12 @@ const fullOptions: SelectBoxOption[] = [
},
];
const manyOptions: SelectBoxOption[] = [
{ value: "opt1", label: "Option 1" },
{ value: "opt2", label: "Option 2" },
{ value: "opt3", label: "Option 3" },
{ value: "opt4", label: "Option 4" },
{ value: "opt5", label: "Option 5" },
{ value: "opt6", label: "Option 6" },
];
const selects: {
id: string;
label: string;
class?: string;
options: SelectBoxOption[];
disabled?: boolean;
maxColumns?: number;
}[] = [
{
id: "basic",
@@ -70,12 +60,6 @@ const selects: {
label: "With description and image",
options: fullOptions,
},
{
id: "two-columns",
label: "2 columns (maxColumns=2)",
options: manyOptions,
maxColumns: 2,
},
];
@customElement("demo-components-ha-select-box")
@@ -83,14 +67,13 @@ export class DemoHaSelectBox extends LitElement {
@state() private value?: string = "off";
handleValueChanged(e: CustomEvent) {
console.log(e.detail.value);
this.value = e.detail.value as string;
}
protected render(): TemplateResult {
return html`
${repeat(selects, (select) => {
const { id, label, options, maxColumns } = select;
const { id, label, options } = select;
return html`
<ha-card>
<div class="card-content">
@@ -98,7 +81,6 @@ export class DemoHaSelectBox extends LitElement {
<ha-select-box
.value=${this.value}
.options=${options}
.maxColumns=${maxColumns}
@value-changed=${this.handleValueChanged}
>
</ha-select-box>

View File

@@ -1,5 +1,5 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, state } from "lit/decorators";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockConfigEntries } from "../../../../demo/src/stubs/config_entries";
@@ -692,11 +692,7 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
([key, value]) => html`
<ha-settings-row narrow slot=${slot}>
<span slot="heading">${value?.name || key}</span>
${value?.description
? html`<span slot="description"
>${value?.description}</span
>`
: nothing}
<span slot="description">${value?.description}</span>
<ha-selector
.hass=${this.hass}
.selector=${value!.selector}

View File

@@ -1,4 +1,4 @@
import type { TemplateResult, PropertyValues } from "lit";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
@@ -51,7 +51,7 @@ export class DemoHaSlider extends LitElement {
`;
}
firstUpdated(changedProps: PropertyValues<this>) {
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),

View File

@@ -1,4 +1,4 @@
import type { TemplateResult, PropertyValues } from "lit";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
@@ -33,7 +33,7 @@ export class DemoHaSpinner extends LitElement {
`;
}
firstUpdated(changedProps: PropertyValues<this>) {
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),

View File

@@ -3,73 +3,37 @@ title: Switch / Toggle
---
<style>
.wrapper {
display: flex;
gap: 24px;
align-items: center;
ha-switch {
display: block;
}
</style>
# Switch `<ha-switch>`
A toggle switch representing two states: on and off.
A toggle switch can represent two states: on and off.
## Implementation
## Examples
### Example usage
<div class="wrapper">
<ha-switch checked></ha-switch>
<ha-switch></ha-switch>
<ha-switch disabled></ha-switch>
<ha-switch disabled checked></ha-switch>
</div>
```html
Switch in on state
<ha-switch checked></ha-switch>
Switch in off state
<ha-switch></ha-switch>
Disabled switch
<ha-switch disabled></ha-switch>
<ha-switch disabled checked></ha-switch>
```
## CSS variables
### API
For the switch / toggle there are always two variables, one for the on / checked state and one for the off / unchecked state.
This component is based on the webawesome switch component.
Check the [webawesome documentation](https://webawesome.com/docs/components/switch/) for more details.
The track element (background rounded rectangle that the round circular handle travels on) is set to being half transparent, so the final color will also be impacted by the color behind the track.
**Properties/Attributes**
`switch-checked-color` / `switch-unchecked-color`
Set both the color of the round handle and the track behind it. If you want to control them separately, use the variables below instead.
| Name | Type | Default | Description |
| -------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------------- |
| checked | Boolean | false | The checked state of the switch. |
| disabled | Boolean | false | Disables the switch and prevents user interaction. |
| required | Boolean | false | Makes the switch a required field. |
| haptic | Boolean | false | Enables haptic vibration on toggle. Only use when the new state is applied immediately (not when save is required). |
`switch-checked-button-color` / `switch-unchecked-button-color`
Color of the round handle
**CSS Custom Properties**
- `--ha-switch-size` - The size of the switch track height. Defaults to `24px`.
- `--ha-switch-thumb-size` - The size of the thumb. Defaults to `18px`.
- `--ha-switch-width` - The width of the switch track. Defaults to `48px`.
- `--ha-switch-thumb-box-shadow` - The box shadow of the thumb. Defaults to `var(--ha-box-shadow-s)`.
- `--ha-switch-background-color` - Background color of the unchecked track.
- `--ha-switch-thumb-background-color` - Background color of the unchecked thumb.
- `--ha-switch-background-color-hover` - Background color of the unchecked track on hover.
- `--ha-switch-thumb-background-color-hover` - Background color of the unchecked thumb on hover.
- `--ha-switch-border-color` - Border color of the unchecked track.
- `--ha-switch-thumb-border-color` - Border color of the unchecked thumb.
- `--ha-switch-thumb-border-color-hover` - Border color of the unchecked thumb on hover.
- `--ha-switch-checked-background-color` - Background color of the checked track.
- `--ha-switch-checked-thumb-background-color` - Background color of the checked thumb.
- `--ha-switch-checked-background-color-hover` - Background color of the checked track on hover.
- `--ha-switch-checked-thumb-background-color-hover` - Background color of the checked thumb on hover.
- `--ha-switch-checked-border-color` - Border color of the checked track.
- `--ha-switch-checked-thumb-border-color` - Border color of the checked thumb.
- `--ha-switch-checked-border-color-hover` - Border color of the checked track on hover.
- `--ha-switch-checked-thumb-border-color-hover` - Border color of the checked thumb on hover.
- `--ha-switch-disabled-opacity` - Opacity of the switch when disabled. Defaults to `0.2`.
- `--ha-switch-required-marker` - The marker shown after the label for required fields. Defaults to `"*"`.
- `--ha-switch-required-marker-offset` - Offset of the required marker. Defaults to `0.1rem`.
`switch-checked-track-color` / `switch-unchecked-track-color`
Color of the track behind the round handle

View File

@@ -1,95 +1 @@
import type { TemplateResult, PropertyValues } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-switch";
import type { HomeAssistant } from "../../../../src/types";
@customElement("demo-components-ha-switch")
export class DemoHaSwitch extends LitElement {
@property({ attribute: false }) hass!: HomeAssistant;
protected render(): TemplateResult {
return html`
${["light", "dark"].map(
(mode) => html`
<div class=${mode}>
<ha-card header="ha-switch ${mode}">
<div class="card-content">
<div class="row">
<span>Unchecked</span>
<ha-switch></ha-switch>
</div>
<div class="row">
<span>Checked</span>
<ha-switch checked></ha-switch>
</div>
<div class="row">
<span>Disabled</span>
<ha-switch disabled></ha-switch>
</div>
<div class="row">
<span>Disabled checked</span>
<ha-switch disabled checked></ha-switch>
</div>
</div>
</ha-card>
</div>
`
)}
`;
}
firstUpdated(changedProps: PropertyValues<this>) {
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;
margin: 16px;
border-radius: var(--ha-border-radius-md);
}
ha-card {
margin: 24px auto;
}
.card-content {
display: flex;
flex-direction: column;
gap: var(--ha-space-4);
}
.row {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--ha-space-4);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-switch": DemoHaSwitch;
}
}

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, PropertyValues } 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: PropertyValues<this>) {
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

@@ -1,4 +1,4 @@
import type { TemplateResult, PropertyValues } from "lit";
import type { TemplateResult } from "lit";
import { html, css, LitElement } from "lit";
import { customElement } from "lit/decorators";
import "../../../../src/components/ha-tip";
@@ -31,7 +31,7 @@ export class DemoHaTip extends LitElement {
)}`;
}
firstUpdated(changedProps: PropertyValues<this>) {
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),

View File

@@ -95,7 +95,7 @@ class DemoAlarmPanelEntity extends LitElement {
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
}
protected firstUpdated(changedProperties: PropertyValues<this>) {
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");

View File

@@ -116,7 +116,7 @@ class DemoArea extends LitElement {
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
}
protected firstUpdated(changedProperties: PropertyValues<this>) {
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");

View File

@@ -77,7 +77,7 @@ class DemoConditional extends LitElement {
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
}
protected firstUpdated(changedProperties: PropertyValues<this>) {
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");

View File

@@ -426,7 +426,7 @@ class DemoEntities extends LitElement {
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
}
protected firstUpdated(changedProperties: PropertyValues<this>) {
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");

View File

@@ -81,7 +81,7 @@ class DemoButtonEntity extends LitElement {
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
}
protected firstUpdated(changedProperties: PropertyValues<this>) {
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");

View File

@@ -301,7 +301,7 @@ class DemoEntityFilter extends LitElement {
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
}
protected firstUpdated(changedProperties: PropertyValues<this>) {
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");

View File

@@ -134,53 +134,6 @@ const CONFIGS = [
entity: sensor.not_working
`,
},
{
heading: "Lower minimum",
config: `
- type: gauge
entity: sensor.brightness_high
needle: true
severity:
green: 0
yellow: 0.45
red: 0.9
min: -0.05
name: " "
max: 1.9
unit: GBP/h`,
},
{
heading: "A lot of segments",
config: `
- type: gauge
needle: true
name: Percent gauge
entity: sensor.brightness_high
unit: "%"
min: 0
max: 100
segments:
- from: 0
color: "#db4437"
- from: 10
color: "#cc4d39"
- from: 20
color: "#bd563a"
- from: 30
color: "#ad603c"
- from: 40
color: "#9e693d"
- from: 50
color: "#8f723f"
- from: 60
color: "#807b41"
- from: 70
color: "#718442"
- from: 80
color: "#618e44"
- from: 90
color: "#43a047"`,
},
];
@customElement("demo-lovelace-gauge-card")
@@ -191,7 +144,7 @@ class DemoGaugeEntity extends LitElement {
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
}
protected firstUpdated(changedProperties: PropertyValues<this>) {
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");

View File

@@ -261,7 +261,7 @@ class DemoGlanceEntity extends LitElement {
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
}
protected firstUpdated(changedProperties: PropertyValues<this>) {
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");

View File

@@ -232,7 +232,7 @@ class DemoStack extends LitElement {
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
}
protected firstUpdated(changedProperties: PropertyValues<this>) {
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");

View File

@@ -46,7 +46,7 @@ class DemoIframe extends LitElement {
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
}
protected firstUpdated(changedProperties: PropertyValues<this>) {
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
provideHass(this._demos);
}

View File

@@ -87,7 +87,7 @@ class DemoLightEntity extends LitElement {
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
}
protected firstUpdated(changedProperties: PropertyValues<this>) {
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");

View File

@@ -196,7 +196,7 @@ class DemoMap extends LitElement {
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
}
protected firstUpdated(changedProperties: PropertyValues<this>) {
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");

View File

@@ -290,7 +290,7 @@ class DemoMarkdown extends LitElement {
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
}
protected firstUpdated(changedProperties: PropertyValues<this>) {
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");

View File

@@ -166,7 +166,7 @@ class DemoHuiMediaControlCard extends LitElement {
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
}
protected firstUpdated(changedProperties: PropertyValues<this>) {
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");

View File

@@ -63,7 +63,7 @@ export class DemoLovelaceMediaPlayerRow extends LitElement {
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
}
protected firstUpdated(changedProperties: PropertyValues<this>) {
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");

View File

@@ -48,7 +48,7 @@ class DemoPicture extends LitElement {
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
}
protected firstUpdated(changedProperties: PropertyValues<this>) {
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");

View File

@@ -179,7 +179,7 @@ class DemoPictureElements extends LitElement {
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
}
protected firstUpdated(changedProperties: PropertyValues<this>) {
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");

View File

@@ -111,7 +111,7 @@ class DemoPictureEntity extends LitElement {
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
}
protected firstUpdated(changedProperties: PropertyValues<this>) {
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");

View File

@@ -171,7 +171,7 @@ class DemoPictureGlance extends LitElement {
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
}
protected firstUpdated(changedProperties: PropertyValues<this>) {
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");

View File

@@ -39,7 +39,7 @@ export class DemoPlantEntity extends LitElement {
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
}
protected firstUpdated(changedProperties: PropertyValues<this>) {
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");

View File

@@ -246,7 +246,7 @@ class DemoThermostatEntity extends LitElement {
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
}
protected firstUpdated(changedProperties: PropertyValues<this>) {
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");

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