Compare commits

..

1 Commits

Author SHA1 Message Date
Paul Bottein
261cc6598d Add conditional form schema 2023-06-08 16:38:07 +02:00
1517 changed files with 43307 additions and 85085 deletions

View File

@@ -10,12 +10,6 @@ supports es6-module-dynamic-import
not Safari < 13
not iOS < 13
# Exclude KaiOS, QQ, and UC browsers due to lack of sufficient feature support data
# Babel ignores these automatically, but we need here for Webpack to output ESM with dynamic imports
not KaiOS > 0
not QQAndroid > 0
not UCAndroid > 0
# Exclude unsupported browsers
not dead

View File

@@ -1,5 +1,5 @@
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.148.1/containers/python-3/.devcontainer/base.Dockerfile
FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.11
FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.10
ENV \
DEBIAN_FRONTEND=noninteractive \

View File

@@ -5,7 +5,6 @@
"context": ".."
},
"appPort": "8124:8123",
"postCreateCommand": "sudo apt update && sudo apt upgrade -y && sudo apt install -y libpcap-dev",
"postStartCommand": "script/bootstrap",
"containerEnv": {
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"

View File

@@ -9,7 +9,7 @@ body:
If you have a feature or enhancement request for the frontend, please [start an discussion][fr] instead of creating an issue.
**Please do not report issues for custom cards.**
**Please not not report issues for custom cards.**
[fr]: https://github.com/home-assistant/frontend/discussions
[releases]: https://github.com/home-assistant/home-assistant/releases
@@ -24,7 +24,6 @@ body:
required: true
- label: I have tried a different browser to see if it is related to my browser.
required: true
- label: I have tried reproducing the issue in [safe mode](https://www.home-assistant.io/blog/2023/11/01/release-202311/#restarting-into-safe-mode) to rule out problems with unsupported custom resources.
- type: markdown
attributes:
value: |

View File

@@ -2,7 +2,9 @@
You are amazing! Thanks for contributing to our project!
Please, DO NOT DELETE ANY TEXT from this template! (unless instructed).
-->
## Breaking change
<!--
If your PR contains a breaking change for existing users, it is important
to tell them what breaks, how to make it work again and why we did this.
@@ -11,8 +13,8 @@
Note: Remove this section if this PR is NOT a breaking change.
-->
## Proposed change
<!--
Describe the big picture of your changes here to communicate to the
maintainers why we should accept this pull request. If it fixes a bug
@@ -20,8 +22,8 @@
in the additional information section.
-->
## Type of change
<!--
What type of change does your PR introduce to the Home Assistant frontend?
NOTE: Please, check only 1! box!
@@ -36,6 +38,7 @@
- [ ] Code quality improvements to existing code or addition of tests
## Example configuration
<!--
Supplying a configuration snippet, makes it easier for a maintainer to test
your PR.
@@ -46,6 +49,7 @@
```
## Additional information
<!--
Details are important, and help maintainers processing your PR.
Please be sure to fill out additional details, if applicable.
@@ -56,6 +60,7 @@
- Link to documentation pull request:
## Checklist
<!--
Put an `x` in the boxes that apply. You can also fill these out after
creating the PR. If you're unsure about any of them, don't hesitate to ask.

View File

@@ -6,6 +6,3 @@ updates:
interval: weekly
time: "06:00"
open-pull-requests-limit: 10
labels:
- Dependencies
- GitHub Actions

51
.github/labeler.yml vendored
View File

@@ -1,51 +0,0 @@
Build:
- changed-files:
- any-glob-to-any-file:
- build-scripts/**
- .browserslistrc
- gulpfile.js
Cast:
- changed-files:
- any-glob-to-any-file:
- cast/src/**
- src/cast/**
Demo:
- changed-files:
- any-glob-to-any-file:
- demo/src/**
- src/fake_data/**
Design:
- changed-files:
- any-glob-to-any-file:
- gallery/src/**
- src/fake_data/**
Dependencies:
- any:
- changed-files:
# Match when only these files are changed (i.e. don't match PRs that happen to add or remove packages)
- any-glob-to-all-files:
- package.json
- renovate.json
- yarn.lock
- .yarn/**
- .yarnrc.yml
- .nvmrc
# Dependabot and Renovate branches always match (i.e. compatibility tweaks by members considered minor)
- head-branch:
- "^renovate/"
- "^dependabot/"
GitHub Actions:
- changed-files:
- any-glob-to-any-file:
- .github/workflows/**
- .github/*.yml
Supervisor:
- changed-files:
- any-glob-to-any-file:
- hassio/src/**

View File

@@ -1,8 +1,3 @@
categories:
- title: "Dependency updates"
collapse-after: 3
labels:
- "Dependencies"
template: |
## What's Changed

View File

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

View File

@@ -24,9 +24,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.1
uses: actions/checkout@v3.5.2
- name: Setup Node
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v3.6.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -36,21 +36,10 @@ jobs:
run: yarn dedupe --check
- 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@v4.0.0
with:
path: |
node_modules/.cache/prettier
node_modules/.cache/eslint
node_modules/.cache/typescript
key: lint-${{ github.sha }}
restore-keys: lint-
- name: Run eslint
run: yarn run lint:eslint --quiet
- name: Run tsc
run: yarn run lint:types
- name: Run lit-analyzer
run: yarn run lint:lit --quiet
- name: Run prettier
run: yarn run lint:prettier
test:
@@ -58,16 +47,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.1
uses: actions/checkout@v3.5.2
- name: Setup Node
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v3.6.0
with:
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies
run: yarn install --immutable
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data
run: ./node_modules/.bin/gulp build-translations build-locale-data
- name: Run Tests
run: yarn run test
build:
@@ -76,9 +65,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.1
uses: actions/checkout@v3.5.2
- name: Setup Node
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v3.6.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -88,21 +77,15 @@ jobs:
run: ./node_modules/.bin/gulp build-app
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@v4.3.1
with:
name: frontend-bundle-stats
path: build/stats/*.json
if-no-files-found: error
supervisor:
name: Build supervisor
needs: [lint, test]
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.1
uses: actions/checkout@v3.5.2
- name: Setup Node
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v3.6.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -112,9 +95,3 @@ jobs:
run: ./node_modules/.bin/gulp build-hassio
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@v4.3.1
with:
name: supervisor-bundle-stats
path: build/stats/*.json
if-no-files-found: error

View File

@@ -17,44 +17,44 @@ jobs:
matrix:
# Override automatic language detection by changing the below list
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
language: ["javascript"]
language: ['javascript']
# Learn more...
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
steps:
- name: Checkout repository
uses: actions/checkout@v4.1.1
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
- name: Checkout repository
uses: actions/checkout@v3.5.2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
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@v3
# 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@v2
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

View File

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

View File

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

View File

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

View File

@@ -1,15 +0,0 @@
name: "Pull Request Labeler"
on: pull_request_target
jobs:
triage:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: Apply labels
uses: actions/labeler@v5.0.0
with:
sync-labels: true

View File

@@ -9,10 +9,9 @@ jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v5.0.1
- uses: dessant/lock-threads@v4.0.0
with:
github-token: ${{ github.token }}
process-only: "issues, prs"
issue-lock-inactive-days: "30"
issue-exclude-created-before: "2020-10-01T00:00:00Z"
issue-lock-reason: ""

View File

@@ -6,7 +6,7 @@ on:
- cron: "0 1 * * *"
env:
PYTHON_VERSION: "3.11"
PYTHON_VERSION: "3.10"
NODE_OPTIONS: --max_old_space_size=6144
permissions:
@@ -20,15 +20,15 @@ jobs:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.1
uses: actions/checkout@v3.5.2
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v3.6.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -42,7 +42,7 @@ jobs:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
- name: Bump version
run: script/version_bump.js nightly
run: script/version_bump.cjs nightly
- name: Build nightly Python wheels
run: |
@@ -57,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts
uses: actions/upload-artifact@v4.3.1
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist/home_assistant_frontend*.whl
if-no-files-found: error
- name: Upload translations
uses: actions/upload-artifact@v4.3.1
uses: actions/upload-artifact@v3
with:
name: translations
path: translations.tar.gz

View File

@@ -1,25 +0,0 @@
name: RelativeCI
on:
workflow_run:
workflows: [CI]
types:
- completed
jobs:
upload:
name: Upload stats
if: ${{ github.event.workflow_run.conclusion == 'success' }}
strategy:
matrix:
bundle: [frontend, supervisor]
build: [modern, legacy]
runs-on: ubuntu-latest
steps:
- name: Send bundle stats and build information to RelativeCI
uses: relative-ci/agent-action@v2.1.10
with:
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
token: ${{ github.token }}
artifactName: ${{ format('{0}-bundle-stats', matrix.bundle) }}
webpackStatsFile: ${{ format('{0}-{1}.json', matrix.bundle, matrix.build) }}

View File

@@ -5,19 +5,10 @@ on:
branches:
- dev
permissions:
contents: read
jobs:
update_release_draft:
permissions:
# write permission for contents is required to create a github release
contents: write
# write permission for pull-requests is required for autolabeler
# otherwise, read permission is required at least
pull-requests: read
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@v6.0.0
- uses: release-drafter/release-drafter@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -6,7 +6,7 @@ on:
- published
env:
PYTHON_VERSION: "3.11"
PYTHON_VERSION: "3.10"
NODE_OPTIONS: --max_old_space_size=6144
# Set default workflow permissions
@@ -23,18 +23,18 @@ jobs:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.1
uses: actions/checkout@v3.5.2
- name: Verify version
uses: home-assistant/actions/helpers/verify-version@master
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@v4.0.2
uses: actions/setup-node@v3.6.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -74,9 +74,9 @@ jobs:
echo "home-assistant-frontend==$version" > ./requirements.txt
- name: Build wheels
uses: home-assistant/wheels@2024.01.0
uses: home-assistant/wheels@2023.04.0
with:
abi: cp311
abi: cp310
tag: musllinux_1_2
arch: amd64
wheels-key: ${{ secrets.WHEELS_KEY }}

View File

@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 90 days stale policy
uses: actions/stale@v9.0.0
uses: actions/stale@v8.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 90

View File

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

3
.gitignore vendored
View File

@@ -47,6 +47,3 @@ src/cast/dev_const.ts
# Home Assistant config
/config/
# Jetbrains
/.idea/

2
.nvmrc
View File

@@ -1 +1 @@
lts/iron
18

View File

@@ -1,4 +1,9 @@
CLA.md
CODE_OF_CONDUCT.md
LICENSE.md
PULL_REQUEST_TEMPLATE.md
build
translations/*
node_modules/*
hass_frontend/*
pip-selfcheck.json
# vscode
.vscode/*
!.vscode/extensions.json

6
.vscode/launch.json vendored
View File

@@ -9,7 +9,9 @@
"webRoot": "${workspaceFolder}/hass_frontend",
"disableNetworkCache": true,
"preLaunchTask": "Develop Frontend",
"outFiles": ["${workspaceFolder}/hass_frontend/frontend_latest/*.js"]
"outFiles": [
"${workspaceFolder}/hass_frontend/frontend_latest/*.js"
]
},
{
"name": "Debug Gallery",
@@ -37,6 +39,6 @@
"webRoot": "${workspaceFolder}/cast/dist",
"disableNetworkCache": true,
"preLaunchTask": "Develop Cast"
}
},
]
}

2
.vscode/tasks.json vendored
View File

@@ -197,7 +197,7 @@
"type": "gulp",
"task": "setup-and-fetch-nightly-translations",
"problemMatcher": []
}
}
],
"inputs": [
{

View File

@@ -1,13 +0,0 @@
diff --git a/simple-tooltip.js b/simple-tooltip.js
index 78a87f6a223925f0e29fbedb268c85a142ec6985..3d686dd6a3d5a93342b4b01408089fc316b408ca 100644
--- a/simple-tooltip.js
+++ b/simple-tooltip.js
@@ -195,6 +195,8 @@ class SimpleTooltip extends LitElement {
.hidden {
position: absolute;
left: -10000px;
+ inset-inline-start: -10000px;
+ inset-inline-end: initial;
top: auto;
width: 1px;
height: 1px;

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,39 @@
diff --git a/modular/sortable.complete.esm.js b/modular/sortable.complete.esm.js
index 02e9f2d6bebeb430fe6e7c1cc3f9c3c9df051f14..bb8268b0844a1faa4108cc92c0be2a3dbaf23f83 100644
--- a/modular/sortable.complete.esm.js
+++ b/modular/sortable.complete.esm.js
@@ -1657,7 +1657,7 @@ Sortable.prototype =
target = parent; // store last element
}
/* jshint boss:true */
- while (parent = parent.parentNode);
+ while (parent = parent.parentNode || parent.getRootNode().host);
}
_unhideGhostForTarget();
diff --git a/modular/sortable.core.esm.js b/modular/sortable.core.esm.js
index b04c8b4634f7c6b4ef1aadbb48afe6564306dea9..39a107163c8c336ebd669b5ea8a936af87e1c1e7 100644
--- a/modular/sortable.core.esm.js
+++ b/modular/sortable.core.esm.js
@@ -1657,7 +1657,7 @@ Sortable.prototype =
target = parent; // store last element
}
/* jshint boss:true */
- while (parent = parent.parentNode);
+ while (parent = parent.parentNode || parent.getRootNode().host);
}
_unhideGhostForTarget();
diff --git a/modular/sortable.esm.js b/modular/sortable.esm.js
index 6ec7ed1bb557e21c2578200161e989c65d23150b..0a05475a22904472fac6c13f524c674da76584b0 100644
--- a/modular/sortable.esm.js
+++ b/modular/sortable.esm.js
@@ -1657,7 +1657,7 @@ Sortable.prototype =
target = parent; // store last element
}
/* jshint boss:true */
- while (parent = parent.parentNode);
+ while (parent = parent.parentNode || parent.getRootNode().host);
}
_unhideGhostForTarget();

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

874
.yarn/releases/yarn-3.6.0.cjs vendored Executable file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,9 +1,11 @@
compressionLevel: mixed
defaultSemverRangePrefix: ""
enableGlobalCache: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.1.0.cjs
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs
spec: "@yarnpkg/plugin-typescript"
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
yarnPath: .yarn/releases/yarn-3.6.0.cjs

View File

@@ -1,56 +0,0 @@
import defineProvider from "@babel/helper-define-polyfill-provider";
// List of polyfill keys with supported browser targets for the functionality
const PolyfillSupport = {
fetch: {
android: 42,
chrome: 42,
edge: 14,
firefox: 39,
ios: 10.3,
opera: 29,
opera_mobile: 29,
safari: 10.1,
samsung: 4.0,
},
proxy: {
android: 49,
chrome: 49,
edge: 12,
firefox: 18,
ios: 10.0,
opera: 36,
opera_mobile: 36,
safari: 10.0,
samsung: 5.0,
},
};
// Map of global variables and/or instance and static properties to the
// corresponding polyfill key and actual module to import
const polyfillMap = {
global: {
Proxy: { key: "proxy", module: "proxy-polyfill" },
fetch: { key: "fetch", module: "unfetch/polyfill" },
},
instance: {},
static: {},
};
// Create plugin using the same factory as for CoreJS
export default defineProvider(
({ createMetaResolver, debug, shouldInjectPolyfill }) => {
const resolvePolyfill = createMetaResolver(polyfillMap);
return {
name: "HA Custom",
polyfills: PolyfillSupport,
usageGlobal(meta, utils) {
const polyfill = resolvePolyfill(meta);
if (polyfill && shouldInjectPolyfill(polyfill.desc.key)) {
debug(polyfill.desc.key);
utils.injectGlobalImport(polyfill.desc.module);
}
},
};
}
);

View File

@@ -1,7 +1,6 @@
const path = require("path");
const env = require("./env.cjs");
const paths = require("./paths.cjs");
const { dependencies } = require("../package.json");
// GitHub base URL to use for production source maps
// Nightly builds use the commit SHA, otherwise assumes there is a tag that matches the version
@@ -9,11 +8,15 @@ module.exports.sourceMapURL = () => {
const ref = env.version().endsWith("dev")
? process.env.GITHUB_SHA || "dev"
: env.version();
return `https://raw.githubusercontent.com/home-assistant/frontend/${ref}/`;
return `https://raw.githubusercontent.com/home-assistant/frontend/${ref}`;
};
// Files from NPM Packages that should not be imported
module.exports.ignorePackages = () => [];
// eslint-disable-next-line unused-imports/no-unused-vars
module.exports.ignorePackages = ({ latestBuild }) => [
// Part of yaml.js and only used for !!js functions that we don't use
require.resolve("esprima"),
];
// Files from NPM packages that we should replace with empty file
module.exports.emptyPackages = ({ latestBuild, isHassioBuild }) =>
@@ -32,6 +35,8 @@ module.exports.emptyPackages = ({ latestBuild, isHassioBuild }) =>
require.resolve(
path.resolve(paths.polymer_dir, "src/resources/compatibility.ts")
),
// This polyfill is loaded in workers to support ES5, filter it out.
latestBuild && require.resolve("proxy-polyfill/src/index.js"),
// Icons in supervisor conflict with icons in HA so we don't load.
isHassioBuild &&
require.resolve(
@@ -72,7 +77,6 @@ module.exports.htmlMinifierOptions = {
module.exports.terserOptions = ({ latestBuild, isTestBuild }) => ({
safari10: !latestBuild,
ecma: latestBuild ? 2015 : 5,
module: latestBuild,
format: { comments: false },
sourceMap: !isTestBuild,
});
@@ -86,14 +90,15 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
setSpreadProperties: true,
},
browserslistEnv: latestBuild ? "modern" : "legacy",
// Must be unambiguous because some dependencies are CommonJS only
sourceType: "unambiguous",
presets: [
[
"@babel/preset-env",
{
useBuiltIns: latestBuild ? false : "usage",
corejs: latestBuild ? false : dependencies["core-js"],
useBuiltIns: latestBuild ? false : "entry",
corejs: latestBuild ? false : { version: "3.30", proposals: true },
bugfixes: true,
shippedProposals: true,
},
],
"@babel/preset-typescript",
@@ -109,39 +114,27 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
ignoreModuleNotFound: true,
},
],
[
path.resolve(
paths.polymer_dir,
"build-scripts/babel-plugins/custom-polyfill-plugin.js"
),
{ method: "usage-global" },
],
// Minify template literals for production
isProdBuild && [
"template-html-minifier",
{
modules: {
...Object.fromEntries(
["lit", "lit-element", "lit-html"].map((m) => [
m,
[
"html",
{ name: "svg", encapsulation: "svg" },
{ name: "css", encapsulation: "style" },
],
])
),
"@polymer/polymer/lib/utils/html-tag.js": ["html"],
lit: [
"html",
{ name: "svg", encapsulation: "svg" },
{ name: "css", encapsulation: "style" },
],
"@polymer/polymer/lib/utils/html-tag": ["html"],
},
strictCSS: true,
htmlMinifier: module.exports.htmlMinifierOptions,
failOnError: false, // we can turn this off in case of false positives
failOnError: true, // we can turn this off in case of false positives
},
],
// Import helpers and regenerator from runtime package
[
"@babel/plugin-transform-runtime",
{ version: dependencies["@babel/runtime"] },
{ version: require("../package.json").dependencies["@babel/runtime"] },
],
// Support some proposals still in TC39 process
["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }],
@@ -152,21 +145,9 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
/node_modules[\\/]webpack[\\/]buildin/,
],
sourceMaps: !isTestBuild,
overrides: [
{
// 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
sourceType: "unambiguous",
include: /\/node_modules\//,
exclude: [
"element-internals-polyfill",
"@?lit(?:-labs|-element|-html)?",
].map((p) => new RegExp(`/node_modules/${p}/`)),
},
],
});
const nameSuffix = (latestBuild) => (latestBuild ? "-modern" : "-legacy");
const nameSuffix = (latestBuild) => (latestBuild ? "-latest" : "-es5");
const outputPath = (outputRoot, latestBuild) =>
path.resolve(outputRoot, latestBuild ? "frontend_latest" : "frontend_es5");
@@ -200,7 +181,7 @@ const publicPath = (latestBuild, root = "") =>
module.exports.config = {
app({ isProdBuild, latestBuild, isStatsBuild, isTestBuild, isWDS }) {
return {
name: "frontend" + nameSuffix(latestBuild),
name: "app" + nameSuffix(latestBuild),
entry: {
service_worker: "./src/entrypoints/service_worker.ts",
app: "./src/entrypoints/app.ts",

View File

@@ -30,8 +30,8 @@ gulp.task(
env.useWDS()
? "wds-watch-app"
: env.useRollup()
? "rollup-watch-app"
: "webpack-watch-app"
? "rollup-watch-app"
: "webpack-watch-app"
)
);
@@ -45,8 +45,8 @@ gulp.task(
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
"copy-static-app",
env.useRollup() ? "rollup-prod-app" : "webpack-prod-app",
gulp.parallel("gen-pages-app-prod", "gen-service-worker-app-prod"),
// Don't compress running tests
...(env.isTestBuild() ? [] : ["compress-app"])
...(env.isTestBuild() ? [] : ["compress-app"]),
gulp.parallel("gen-pages-app-prod", "gen-service-worker-app-prod")
)
);

View File

@@ -1,26 +1,45 @@
// Tasks to compress
import { deleteAsync } from "del";
import gulp from "gulp";
import gulpIf from "gulp-if";
import vinylPaths from "vinyl-paths";
import zopfli from "gulp-zopfli-green";
import merge from "merge-stream";
import path from "path";
import paths from "../paths.cjs";
const zopfliOptions = { threshold: 150 };
const compressedExt = /\.gz$/;
const deleteUncompressed = (p) => deleteAsync(p.replace(compressedExt, ""));
const compressDist = (rootDir) =>
gulp
.src([
`${rootDir}/**/*.{js?(.map),json,css,svg,xml}`,
`${rootDir}/{authorize,onboarding}.html`,
])
gulp.task("compress-app", function compressApp() {
const jsLatest = gulp
.src(path.resolve(paths.app_output_latest, "**/*.js"))
.pipe(zopfli(zopfliOptions))
.pipe(gulp.dest(rootDir))
.pipe(gulpIf(compressedExt, vinylPaths(deleteUncompressed)));
.pipe(gulp.dest(paths.app_output_latest));
gulp.task("compress-app", () => compressDist(paths.app_output_root));
gulp.task("compress-hassio", () => compressDist(paths.hassio_output_root));
const jsEs5 = gulp
.src(path.resolve(paths.app_output_es5, "**/*.js"))
.pipe(zopfli(zopfliOptions))
.pipe(gulp.dest(paths.app_output_es5));
const polyfills = gulp
.src(path.resolve(paths.app_output_static, "polyfills/*.js"))
.pipe(zopfli(zopfliOptions))
.pipe(gulp.dest(path.resolve(paths.app_output_static, "polyfills")));
const translations = gulp
.src(path.resolve(paths.app_output_static, "translations/**/*.json"))
.pipe(zopfli(zopfliOptions))
.pipe(gulp.dest(path.resolve(paths.app_output_static, "translations")));
const icons = gulp
.src(path.resolve(paths.app_output_static, "mdi/*.json"))
.pipe(zopfli(zopfliOptions))
.pipe(gulp.dest(path.resolve(paths.app_output_static, "mdi")));
return merge(jsLatest, jsEs5, polyfills, translations, icons);
});
gulp.task("compress-hassio", function compressApp() {
return gulp
.src(path.resolve(paths.hassio_output_root, "**/*.js"))
.pipe(zopfli(zopfliOptions))
.pipe(gulp.dest(paths.hassio_output_root));
});

View File

@@ -1,14 +1,9 @@
import fs from "fs/promises";
import gulp from "gulp";
import path from "path";
import mapStream from "map-stream";
import transform from "gulp-json-transform";
import { LokaliseApi } from "@lokalise/node-api";
import JSZip from "jszip";
const inDir = "translations";
const inDirFrontend = `${inDir}/frontend`;
const inDirBackend = `${inDir}/backend`;
const inDirFrontend = "translations/frontend";
const inDirBackend = "translations/backend";
const srcMeta = "src/translations/translationMetadata.json";
const encoding = "utf8";
@@ -46,35 +41,9 @@ function checkHtml() {
});
}
function convertBackendTranslations(data, _file) {
const output = { component: {} };
if (!data.component) {
return output;
}
Object.keys(data.component).forEach((domain) => {
if (!("entity_component" in data.component[domain])) {
return;
}
output.component[domain] = { entity_component: {} };
Object.keys(data.component[domain].entity_component).forEach((key) => {
output.component[domain].entity_component[key] =
data.component[domain].entity_component[key];
});
});
return output;
}
gulp.task("convert-backend-translations", function () {
return gulp
.src([`${inDirBackend}/*.json`])
.pipe(transform((data, file) => convertBackendTranslations(data, file)))
.pipe(gulp.dest(inDirBackend));
});
// Backend translations do not currently pass HTML check so are excluded here for now
gulp.task("check-translations-html", function () {
return gulp
.src([`${inDirFrontend}/*.json`, `${inDirBackend}/*.json`])
.pipe(checkHtml());
return gulp.src([`${inDirFrontend}/*.json`]).pipe(checkHtml());
});
gulp.task("check-all-files-exist", async function () {
@@ -94,87 +63,7 @@ gulp.task("check-all-files-exist", async function () {
await Promise.allSettled(writings);
});
const lokaliseProjects = {
backend: "130246255a974bd3b5e8a1.51616605",
frontend: "3420425759f6d6d241f598.13594006",
};
gulp.task("fetch-lokalise", async function () {
let apiKey;
try {
apiKey =
process.env.LOKALISE_TOKEN ||
(await fs.readFile(".lokalise_token", { encoding }));
} catch {
throw new Error(
"An Administrator Lokalise API token is required to download the latest set of translations. Place your token in a new file `.lokalise_token` in the repo root directory."
);
}
const lokaliseApi = new LokaliseApi({ apiKey });
const mkdirPromise = Promise.all([
fs.mkdir(inDirFrontend, { recursive: true }),
fs.mkdir(inDirBackend, { recursive: true }),
]);
await Promise.all(
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",
})
.then((download) => fetch(download.bundle_url))
.then((response) => {
if (response.status === 200 || response.status === 0) {
return response.arrayBuffer();
}
throw new Error(response.statusText);
})
.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;
})
)
);
});
gulp.task(
"download-translations",
gulp.series(
"fetch-lokalise",
"convert-backend-translations",
"check-translations-html",
"check-all-files-exist"
)
"check-downloaded-translations",
gulp.series("check-translations-html", "check-all-files-exist")
);

View File

@@ -4,7 +4,6 @@ import fs from "fs-extra";
import gulp from "gulp";
import path from "path";
import paths from "../paths.cjs";
import env from "../env.cjs";
const npmPath = (...parts) =>
path.resolve(paths.polymer_dir, "node_modules", ...parts);
@@ -63,9 +62,6 @@ function copyPolyfills(staticDir) {
}
function copyLoaderJS(staticDir) {
if (!env.useRollup()) {
return;
}
const staticPath = genStaticPath(staticDir);
copyFileDir(npmPath("systemjs/dist/s.min.js"), staticPath("js"));
copyFileDir(npmPath("systemjs/dist/s.min.js.map"), staticPath("js"));

View File

@@ -1,86 +1,71 @@
import { deleteSync } from "del";
import { mkdir, readFile, writeFile } from "fs/promises";
import fs from "fs";
import gulp from "gulp";
import { join, resolve } from "node:path";
import path from "path";
import paths from "../paths.cjs";
const formatjsDir = join(paths.polymer_dir, "node_modules", "@formatjs");
const outDir = join(paths.build_dir, "locale-data");
const INTL_POLYFILLS = {
"intl-datetimeformat": "DateTimeFormat",
"intl-displaynames": "DisplayNames",
"intl-listformat": "ListFormat",
"intl-numberformat": "NumberFormat",
"intl-relativetimeformat": "RelativeTimeFormat",
};
const convertToJSON = async (
pkg,
lang,
subDir = "locale-data",
addFunc = "__addLocaleData",
skipMissing = true
) => {
let localeData;
try {
localeData = await readFile(
join(formatjsDir, pkg, subDir, `${lang}.js`),
"utf-8"
);
} catch (e) {
// Ignore if language is missing (i.e. not supported by @formatjs)
if (e.code === "ENOENT" && skipMissing) {
console.warn(`Skipped missing data for language ${lang} from ${pkg}`);
return;
}
throw e;
}
// Convert to JSON
const obj = INTL_POLYFILLS[pkg];
const dataRegex = new RegExp(
`Intl\\.${obj}\\.${addFunc}\\((?<data>.*)\\)`,
"s"
);
localeData = localeData.match(dataRegex)?.groups?.data;
if (!localeData) {
throw Error(`Failed to extract data for language ${lang} from ${pkg}`);
}
// Parse to validate JSON, then stringify to minify
localeData = JSON.stringify(JSON.parse(localeData));
await writeFile(join(outDir, `${pkg}/${lang}.json`), localeData);
};
const outDir = "build/locale-data";
gulp.task("clean-locale-data", async () => deleteSync([outDir]));
gulp.task("create-locale-data", async () => {
gulp.task("ensure-locale-data-build-dir", async () => {
fs.mkdirSync(outDir, { recursive: true });
});
const modules = {
"intl-relativetimeformat": "RelativeTimeFormat",
"intl-datetimeformat": "DateTimeFormat",
"intl-numberformat": "NumberFormat",
"intl-displaynames": "DisplayNames",
};
gulp.task("create-locale-data", (done) => {
const translationMeta = JSON.parse(
await readFile(
resolve(paths.translations_src, "translationMetadata.json"),
"utf-8"
fs.readFileSync(
path.join(paths.translations_src, "translationMetadata.json")
)
);
const conversions = [];
for (const pkg of Object.keys(INTL_POLYFILLS)) {
// eslint-disable-next-line no-await-in-loop
await mkdir(join(outDir, pkg), { recursive: true });
for (const lang of Object.keys(translationMeta)) {
conversions.push(convertToJSON(pkg, lang));
}
}
conversions.push(
convertToJSON(
"intl-datetimeformat",
"add-all-tz",
".",
"__addTZData",
false
)
);
await Promise.all(conversions);
Object.entries(modules).forEach(([module, className]) => {
Object.keys(translationMeta).forEach((lang) => {
try {
const localeData = fs
.readFileSync(
path.resolve(
paths.polymer_dir,
`node_modules/@formatjs/${module}/locale-data/${lang}.js`
),
"utf-8"
)
.replace(
new RegExp(
`\\/\\*\\s*@generated\\s*\\*\\/\\s*\\/\\/\\s*prettier-ignore\\s*if\\s*\\(Intl\\.${className}\\s*&&\\s*typeof\\s*Intl\\.${className}\\.__addLocaleData\\s*===\\s*'function'\\)\\s*{\\s*Intl\\.${className}\\.__addLocaleData\\(`,
"im"
),
""
)
.replace(/\)\s*}/im, "");
// make sure we have valid JSON
JSON.parse(localeData);
fs.mkdirSync(path.join(outDir, module), { recursive: true });
fs.writeFileSync(
path.join(outDir, `${module}/${lang}.json`),
localeData
);
} catch (e) {
if (e.code !== "ENOENT") {
throw e;
}
}
});
done();
});
});
gulp.task(
"build-locale-data",
gulp.series("clean-locale-data", "create-locale-data")
gulp.series(
"clean-locale-data",
"ensure-locale-data-build-dir",
"create-locale-data"
)
);

View File

@@ -1,7 +1,12 @@
import { createHash } from "crypto";
import { deleteSync } from "del";
import { mkdirSync, readdirSync, readFileSync, renameSync } from "fs";
import { writeFile } from "node:fs/promises";
import {
mkdirSync,
readdirSync,
readFileSync,
renameSync,
writeFile,
} from "fs";
import gulp from "gulp";
import flatmap from "gulp-flatmap";
import transform from "gulp-json-transform";
@@ -131,23 +136,27 @@ gulp.task("ensure-translations-build-dir", async () => {
mkdirSync(workDir, { recursive: true });
});
gulp.task("create-test-metadata", () =>
env.isProdBuild()
? Promise.resolve()
: writeFile(
workDir + "/testMetadata.json",
JSON.stringify({ test: { nativeName: "Test" } })
)
);
gulp.task("create-test-metadata", (cb) => {
writeFile(
workDir + "/testMetadata.json",
JSON.stringify({
test: {
nativeName: "Test",
},
}),
cb
);
});
gulp.task("create-test-translation", () =>
env.isProdBuild()
? Promise.resolve()
: gulp
.src(path.join(paths.translations_src, "en.json"))
.pipe(transform((data, _file) => recursiveEmpty(data)))
.pipe(rename("test.json"))
.pipe(gulp.dest(workDir))
gulp.task(
"create-test-translation",
gulp.series("create-test-metadata", () =>
gulp
.src(path.join(paths.translations_src, "en.json"))
.pipe(transform((data, _file) => recursiveEmpty(data)))
.pipe(rename("test.json"))
.pipe(gulp.dest(workDir))
)
);
/**
@@ -179,11 +188,16 @@ gulp.task("build-master-translation", () => {
gulp.task("build-merged-translations", () =>
gulp
.src([
inFrontendDir + "/*.json",
"!" + inFrontendDir + "/en.json",
...(env.isProdBuild() ? [] : [workDir + "/test.json"]),
])
.src(
[
inFrontendDir + "/*.json",
"!" + inFrontendDir + "/en.json",
workDir + "/test.json",
],
{
allowEmpty: true,
}
)
.pipe(transform((data, file) => lokaliseTransform(data, data, file)))
.pipe(
flatmap((stream, file) => {
@@ -363,11 +377,14 @@ gulp.task("build-translation-flatten-supervisor", () =>
gulp.task("build-translation-write-metadata", () =>
gulp
.src([
path.join(paths.translations_src, "translationMetadata.json"),
...(env.isProdBuild() ? [] : [workDir + "/testMetadata.json"]),
workDir + "/translationFingerprints.json",
])
.src(
[
path.join(paths.translations_src, "translationMetadata.json"),
workDir + "/testMetadata.json",
workDir + "/translationFingerprints.json",
],
{ allowEmpty: true }
)
.pipe(merge({}))
.pipe(
transform((data) => {
@@ -398,7 +415,7 @@ gulp.task("build-translation-write-metadata", () =>
gulp.task(
"create-translations",
gulp.series(
gulp.parallel("create-test-metadata", "create-test-translation"),
env.isProdBuild() ? (done) => done() : "create-test-translation",
"build-master-translation",
"build-merged-translations",
gulp.parallel(...splitTasks),
@@ -426,7 +443,6 @@ gulp.task(
"fetch-nightly-translations",
gulp.series("clean-translations", "ensure-translations-build-dir")
),
gulp.parallel("create-test-metadata", "create-test-translation"),
"build-master-translation",
"build-merged-translations",
"build-translation-fragment-supervisor",

View File

@@ -1,9 +1,9 @@
// Tasks to run webpack.
import fs from "fs";
import path from "path";
import log from "fancy-log";
import fs from "fs";
import gulp from "gulp";
import path from "path";
import webpack from "webpack";
import WebpackDevServer from "webpack-dev-server";
import env from "../env.cjs";
@@ -44,7 +44,6 @@ const runDevServer = async ({
}) => {
const server = new WebpackDevServer(
{
hot: false,
open: true,
host: listenHost,
port,
@@ -115,9 +114,7 @@ gulp.task("webpack-prod-app", () =>
gulp.task("webpack-dev-server-demo", () =>
runDevServer({
compiler: webpack(
createDemoConfig({ isProdBuild: false, latestBuild: true })
),
compiler: webpack(bothBuilds(createDemoConfig, { isProdBuild: false })),
contentBase: paths.demo_output_root,
port: 8090,
})
@@ -133,9 +130,7 @@ gulp.task("webpack-prod-demo", () =>
gulp.task("webpack-dev-server-cast", () =>
runDevServer({
compiler: webpack(
createCastConfig({ isProdBuild: false, latestBuild: true })
),
compiler: webpack(bothBuilds(createCastConfig, { isProdBuild: false })),
contentBase: paths.cast_output_root,
port: 8080,
// Accessible from the network, because that's how Cast hits it.
@@ -178,9 +173,8 @@ gulp.task("webpack-prod-hassio", () =>
gulp.task("webpack-dev-server-gallery", () =>
runDevServer({
compiler: webpack(
createGalleryConfig({ isProdBuild: false, latestBuild: true })
),
// We don't use the es5 build, but the dev server will fuck up the publicPath if we don't
compiler: webpack(bothBuilds(createGalleryConfig, { isProdBuild: false })),
contentBase: paths.gallery_output_root,
port: 8100,
listenHost: "0.0.0.0",

View File

@@ -6,8 +6,6 @@ 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";
const detailsOpen = (heading) =>
@@ -28,22 +26,6 @@ const dummyAPI = {
targets: () => ({}),
};
// Generate filter function based on proposal/method inputs
// Copied and adapted from babel-plugin-polyfill-corejs3/esm/index.mjs
const polyfillFilter = (method, proposals, shippedProposals) => (name) => {
if (proposals || method === "entry-global") return true;
if (shippedProposals && shippedPolyfills.default.has(name)) {
return true;
}
if (name.startsWith("esnext.")) {
const esName = `es.${name.slice(7)}`;
// If its imaginative esName is not in latest compat data, it means the proposal is not stage 4
return esName in coreJSCompat.data;
}
return true;
};
// Log the plugins and polyfills for each build environment
for (const buildType of ["Modern", "Legacy"]) {
const browserslistEnv = buildType.toLowerCase();
const babelOpts = babelOptions({ latestBuild: browserslistEnv === "modern" });
@@ -64,13 +46,7 @@ for (const buildType of ["Modern", "Legacy"]) {
const targets = compilationTargets.default(babelOpts?.targets, {
browserslistEnv,
});
const polyfillList = coreJSCompat({ targets }).list.filter(
polyfillFilter(
`${presetEnvOpts.useBuiltIns}-global`,
presetEnvOpts?.corejs?.proposals,
presetEnvOpts?.shippedProposals
)
);
const polyfillList = coreJSCompat({ targets }).list;
console.log(
"The following %i polyfills may be injected by Babel:\n",
polyfillList.length

View File

@@ -142,5 +142,4 @@ module.exports = {
createCastConfig,
createHassioConfig,
createGalleryConfig,
createRollupConfig,
};

View File

@@ -1,15 +1,9 @@
const { existsSync } = require("fs");
const path = require("path");
const webpack = require("webpack");
const { StatsWriterPlugin } = require("webpack-stats-plugin");
const filterStats = require("@bundle-stats/plugin-webpack-filter").default;
const path = require("path");
const TerserPlugin = require("terser-webpack-plugin");
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
const log = require("fancy-log");
const WebpackBar = require("webpackbar");
const {
TransformAsyncModulesPlugin,
} = require("transform-async-modules-webpack-plugin");
const paths = require("./paths.cjs");
const bundle = require("./bundle.cjs");
@@ -47,15 +41,15 @@ const createWebpackConfig = ({
return {
name,
mode: isProdBuild ? "production" : "development",
target: `browserslist:${latestBuild ? "modern" : "legacy"}`,
target: ["web", latestBuild ? "es2017" : "es5"],
// For tests/CI, source maps are skipped to gain build speed
// For production, generate source maps for accurate stack traces without source code
// For development, generate "cheap" versions that can map to original line numbers
devtool: isTestBuild
? false
: isProdBuild
? "nosources-source-map"
: "eval-cheap-module-source-map",
? "nosources-source-map"
: "eval-cheap-module-source-map",
entry,
node: false,
module: {
@@ -90,13 +84,6 @@ const createWebpackConfig = ({
],
moduleIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
chunkIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
splitChunks: {
// Disable splitting for web workers with ESM output
// Imports of external chunks are broken
chunks: latestBuild
? (chunk) => !chunk.canBeInitial() && !/^.+-worker$/.test(chunk.name)
: undefined,
},
},
plugins: [
!isStatsBuild && new WebpackBar({ fancy: !isProdBuild }),
@@ -145,18 +132,18 @@ const createWebpackConfig = ({
),
path.resolve(paths.polymer_dir, "src/util/empty.js")
),
!isProdBuild && new LogStartCompilePlugin(),
isProdBuild &&
new StatsWriterPlugin({
filename: path.relative(
outputPath,
path.join(paths.build_dir, "stats", `${name}.json`)
),
stats: { assets: true, chunks: true, modules: true },
transform: (stats) => JSON.stringify(filterStats(stats)),
}),
// See `src/resources/intl-polyfill-legacy.ts` for explanation
!latestBuild &&
new TransformAsyncModulesPlugin({ browserslistEnv: "legacy" }),
new webpack.NormalModuleReplacementPlugin(
new RegExp(
path.resolve(paths.polymer_dir, "src/resources/intl-polyfill.ts")
),
path.resolve(
paths.polymer_dir,
"src/resources/intl-polyfill-legacy.ts"
)
),
!isProdBuild && new LogStartCompilePlugin(),
].filter(Boolean),
resolve: {
extensions: [".ts", ".js", ".json"],
@@ -170,27 +157,20 @@ const createWebpackConfig = ({
"lit/directives/guard$": "lit/directives/guard.js",
"lit/directives/cache$": "lit/directives/cache.js",
"lit/directives/repeat$": "lit/directives/repeat.js",
"lit/directives/live$": "lit/directives/live.js",
"lit/polyfill-support$": "lit/polyfill-support.js",
"@lit-labs/virtualizer/layouts/grid":
"@lit-labs/virtualizer/layouts/grid.js",
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver":
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js",
"@lit-labs/observers/resize-controller":
"@lit-labs/observers/resize-controller.js",
},
},
output: {
module: latestBuild,
filename: ({ chunk }) =>
!isProdBuild || isStatsBuild || dontHash.has(chunk.name)
? "[name].js"
: "[name].[contenthash].js",
: "[name]-[contenthash].js",
chunkFilename:
isProdBuild && !isStatsBuild ? "[name].[contenthash].js" : "[name].js",
isProdBuild && !isStatsBuild ? "[id]-[contenthash].js" : "[name].js",
assetModuleFilename:
isProdBuild && !isStatsBuild ? "[id].[contenthash][ext]" : "[id][ext]",
crossOriginLoading: "use-credentials",
isProdBuild && !isStatsBuild ? "[id]-[contenthash][ext]" : "[id][ext]",
hashFunction: "xxhash64",
hashDigest: "base64url",
hashDigestLength: 11, // full length of 64 bit base64url
@@ -201,29 +181,22 @@ const createWebpackConfig = ({
// Since production source maps don't include sources, we need to point to them elsewhere
// For dependencies, just provide the path (no source in browser)
// Otherwise, point to the raw code on GitHub for browser to load
...Object.fromEntries(
["", "Fallback"].map((v) => [
`devtool${v}ModuleFilenameTemplate`,
!isTestBuild && isProdBuild
? (info) => {
if (
!path.isAbsolute(info.absoluteResourcePath) ||
!existsSync(info.resourcePath) ||
info.resourcePath.startsWith("./node_modules")
) {
// Source URLs are unknown for dependencies, so we use a relative URL with a
// non - existent top directory. This results in a clean source tree in browser
// dev tools, and they stay happy getting 404s with valid requests.
return `/unknown${path.resolve("/", info.resourcePath)}`;
}
return new URL(info.resourcePath, bundle.sourceMapURL()).href;
devtoolModuleFilenameTemplate:
!isTestBuild && isProdBuild
? (info) => {
const sourcePath = info.resourcePath.replace(/^\.\//, "");
if (
sourcePath.startsWith("node_modules") ||
sourcePath.startsWith("webpack")
) {
return `no-source/${sourcePath}`;
}
: undefined,
])
),
return `${bundle.sourceMapURL()}/${sourcePath}`;
}
: undefined,
},
experiments: {
outputModule: true,
topLevelAwait: true,
},
};
};
@@ -270,5 +243,4 @@ module.exports = {
createCastConfig,
createHassioConfig,
createGalleryConfig,
createWebpackConfig,
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

View File

@@ -1,3 +1,3 @@
self.addEventListener("fetch", (event) => {
self.addEventListener("fetch", function(event) {
event.respondWith(fetch(event.request));
});

View File

@@ -1,4 +1,4 @@
import "../../../src/resources/safari-14-attachshadow-patch";
import "../../../src/resources/ha-style";
import "../../../src/resources/roboto";
import "./layout/hc-connect";
import("../../../src/resources/ha-style");

View File

@@ -3,7 +3,7 @@ import { mdiCast, mdiCastConnected } from "@mdi/js";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-listbox/paper-listbox";
import { Auth, Connection } from "home-assistant-js-websocket";
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { CastManager } from "../../../../src/cast/cast_manager";
import {
@@ -22,27 +22,26 @@ import "../../../../src/components/ha-svg-icon";
import {
getLegacyLovelaceCollection,
getLovelaceCollection,
LovelaceConfig,
} from "../../../../src/data/lovelace";
import { isStrategyDashboard } from "../../../../src/data/lovelace/config/types";
import { LovelaceViewConfig } from "../../../../src/data/lovelace/config/view";
import "../../../../src/layouts/hass-loading-screen";
import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config";
import "./hc-layout";
@customElement("hc-cast")
class HcCast extends LitElement {
@property({ attribute: false }) public auth!: Auth;
@property() public auth!: Auth;
@property({ attribute: false }) public connection!: Connection;
@property() public connection!: Connection;
@property({ attribute: false }) public castManager!: CastManager;
@property() public castManager!: CastManager;
@state() private askWrite = false;
@state() private lovelaceViews?: LovelaceViewConfig[] | null;
@state() private lovelaceConfig?: LovelaceConfig | null;
protected render(): TemplateResult {
if (this.lovelaceViews === undefined) {
if (this.lovelaceConfig === undefined) {
return html`<hass-loading-screen no-toolbar></hass-loading-screen>`;
}
@@ -73,44 +72,43 @@ class HcCast extends LitElement {
${error
? html` <div class="card-content">${error}</div> `
: !this.castManager.status
? html`
<p class="center-item">
<mwc-button raised @click=${this._handleLaunch}>
<ha-svg-icon .path=${mdiCast}></ha-svg-icon>
Start Casting
</mwc-button>
</p>
`
: html`
<div class="section-header">PICK A VIEW</div>
<paper-listbox
attr-for-selected="data-path"
.selected=${this.castManager.status.lovelacePath || ""}
>
${(
this.lovelaceViews ?? [
generateDefaultViewConfig({}, {}, {}, {}, () => ""),
]
).map(
(view, idx) => html`
<paper-icon-item
@click=${this._handlePickView}
data-path=${view.path || idx}
>
${view.icon
? html`
<ha-icon
.icon=${view.icon}
slot="item-icon"
></ha-icon>
`
: ""}
${view.title || view.path}
</paper-icon-item>
`
)}
</paper-listbox>
`}
? html`
<p class="center-item">
<mwc-button raised @click=${this._handleLaunch}>
<ha-svg-icon .path=${mdiCast}></ha-svg-icon>
Start Casting
</mwc-button>
</p>
`
: html`
<div class="section-header">PICK A VIEW</div>
<paper-listbox
attr-for-selected="data-path"
.selected=${this.castManager.status.lovelacePath || ""}
>
${(this.lovelaceConfig
? this.lovelaceConfig.views
: [generateDefaultViewConfig({}, {}, {}, {}, () => "")]
).map(
(view, idx) => html`
<paper-icon-item
@click=${this._handlePickView}
data-path=${view.path || idx}
>
${view.icon
? html`
<ha-icon
.icon=${view.icon}
slot="item-icon"
></ha-icon>
`
: ""}
${view.title || view.path}
</paper-icon-item>
`
)}
</paper-listbox>
`}
<div class="card-actions">
${this.castManager.status
? html`
@@ -138,15 +136,11 @@ class HcCast extends LitElement {
llColl.refresh().then(
() => {
llColl.subscribe((config) => {
if (isStrategyDashboard(config)) {
this.lovelaceViews = null;
} else {
this.lovelaceViews = config.views;
}
this.lovelaceConfig = config;
});
},
async () => {
this.lovelaceViews = null;
this.lovelaceConfig = null;
}
);
@@ -165,7 +159,9 @@ class HcCast extends LitElement {
toggleAttribute(
this,
"hide-icons",
this.lovelaceViews ? !this.lovelaceViews.some((view) => view.icon) : true
this.lovelaceConfig
? !this.lovelaceConfig.views.some((view) => view.icon)
: true
);
}
@@ -241,8 +237,6 @@ class HcCast extends LitElement {
mwc-button ha-svg-icon {
margin-right: 8px;
margin-inline-end: 8px;
margin-inline-start: initial;
height: 18px;
}

View File

@@ -1,5 +1,6 @@
import "@material/mwc-button";
import { mdiCastConnected, mdiCast } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import {
Auth,
Connection,
@@ -23,7 +24,6 @@ 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/ha-textfield";
const seeFAQ = (qid) => html`
See <a href="./faq.html${qid ? `#${qid}` : ""}">the FAQ</a> for more
@@ -33,13 +33,13 @@ const translateErr = (err) =>
err === ERR_CANNOT_CONNECT
? "Unable to connect"
: err === ERR_HASS_HOST_REQUIRED
? "Please enter a Home Assistant URL."
: err === ERR_INVALID_HTTPS_TO_HTTP
? html`
Cannot connect to Home Assistant instances over "http://".
${seeFAQ("https")}
`
: `Unknown error (${err}).`;
? "Please enter a Home Assistant URL."
: err === ERR_INVALID_HTTPS_TO_HTTP
? html`
Cannot connect to Home Assistant instances over "http://".
${seeFAQ("https")}
`
: `Unknown error (${err}).`;
const INTRO = html`
<p>
@@ -116,11 +116,13 @@ 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-textfield
label="Home Assistant URL"
placeholder="https://abcdefghijklmnop.ui.nabu.casa"
@keydown=${this._handleInputKeyDown}
></ha-textfield>
<p>
<paper-input
label="Home Assistant URL"
placeholder="https://abcdefghijklmnop.ui.nabu.casa"
@keydown=${this._handleInputKeyDown}
></paper-input>
</p>
${this.error ? html` <p class="error">${this.error}</p> ` : ""}
</div>
<div class="card-actions">
@@ -194,7 +196,7 @@ export class HcConnect extends LitElement {
}
private async _handleConnect() {
const inputEl = this.shadowRoot!.querySelector("ha-textfield")!;
const inputEl = this.shadowRoot!.querySelector("paper-input")!;
const value = inputEl.value || "";
this.error = undefined;
@@ -313,10 +315,6 @@ export class HcConnect extends LitElement {
.spacer {
flex: 1;
}
ha-textfield {
width: 100%;
}
`;
}
}

View File

@@ -10,13 +10,13 @@ import "../../../../src/components/ha-card";
@customElement("hc-layout")
class HcLayout extends LitElement {
@property() public subtitle?: string;
@property() public subtitle?: string | undefined;
@property({ attribute: false }) public auth?: Auth;
@property() public auth?: Auth;
@property({ attribute: false }) public connection?: Connection;
@property() public connection?: Connection;
@property({ attribute: false }) public user?: HassUser;
@property() public user?: HassUser;
protected render(): TemplateResult {
return html`

View File

@@ -1,21 +1,21 @@
import { framework } from "../receiver/cast_framework";
import { cast } from "chromecast-caf-receiver";
const castContext = framework.CastReceiverContext.getInstance();
const castContext = cast.framework.CastReceiverContext.getInstance();
const playerManager = castContext.getPlayerManager();
playerManager.setMessageInterceptor(
framework.messages.MessageType.LOAD,
cast.framework.messages.MessageType.LOAD,
(loadRequestData) => {
const media = loadRequestData.media;
// Special handling if it came from Google Assistant
if (media.entity) {
media.contentId = media.entity;
media.streamType = framework.messages.StreamType.LIVE;
media.streamType = cast.framework.messages.StreamType.LIVE;
media.contentType = "application/vnd.apple.mpegurl";
// @ts-ignore
media.hlsVideoSegmentFormat =
framework.messages.HlsVideoSegmentFormat.FMP4;
cast.framework.messages.HlsVideoSegmentFormat.FMP4;
}
return loadRequestData;
}

View File

@@ -1,3 +1,3 @@
import { framework } from "./cast_framework";
import { cast } from "chromecast-caf-receiver";
export const castContext = framework.CastReceiverContext.getInstance();
export const castContext = cast.framework.CastReceiverContext.getInstance();

View File

@@ -1,3 +0,0 @@
import type { cast as ReceiverCast } from "chromecast-caf-receiver";
export const framework = (cast as unknown as typeof ReceiverCast).framework;

View File

@@ -1,5 +1,7 @@
import { LovelaceCardConfig } from "../../../../src/data/lovelace/config/card";
import { LovelaceConfig } from "../../../../src/data/lovelace/config/types";
import {
LovelaceCardConfig,
LovelaceConfig,
} from "../../../../src/data/lovelace";
import { castContext } from "../cast_context";
export const castDemoLovelace: () => LovelaceConfig = () => {

View File

@@ -1,4 +1,4 @@
import { framework } from "./cast_framework";
import { cast } from "chromecast-caf-receiver";
import { CAST_NS } from "../../../src/cast/const";
import { HassMessage } from "../../../src/cast/receiver_messages";
import "../../../src/resources/custom-card-support";
@@ -34,14 +34,14 @@ const setTouchControlsVisibility = (visible: boolean) => {
let timeOut: number | undefined;
const playDummyMedia = (viewTitle?: string) => {
const loadRequestData = new framework.messages.LoadRequestData();
const loadRequestData = new cast.framework.messages.LoadRequestData();
loadRequestData.autoplay = true;
loadRequestData.media = new framework.messages.MediaInformation();
loadRequestData.media = new cast.framework.messages.MediaInformation();
loadRequestData.media.contentId =
"https://cast.home-assistant.io/images/google-nest-hub.png";
loadRequestData.media.contentType = "image/jpeg";
loadRequestData.media.streamType = framework.messages.StreamType.NONE;
const metadata = new framework.messages.GenericMediaMetadata();
loadRequestData.media.streamType = cast.framework.messages.StreamType.NONE;
const metadata = new cast.framework.messages.GenericMediaMetadata();
metadata.title = viewTitle;
loadRequestData.media.metadata = metadata;
@@ -86,10 +86,10 @@ const showMediaPlayer = () => {
}
};
const options = new framework.CastReceiverOptions();
const options = new cast.framework.CastReceiverOptions();
options.disableIdleTimeout = true;
options.customNamespaces = {
[CAST_NS]: framework.system.MessageType.JSON,
[CAST_NS]: cast.framework.system.MessageType.JSON,
};
castContext.addCustomMessageListener(
@@ -98,7 +98,8 @@ castContext.addCustomMessageListener(
(ev: ReceivedMessage<HassMessage>) => {
// We received a show Lovelace command, stop media from playing, hide media player and show Lovelace controller
if (
playerManager.getPlayerState() !== framework.messages.PlayerState.IDLE
playerManager.getPlayerState() !==
cast.framework.messages.PlayerState.IDLE
) {
playerManager.stop();
} else {
@@ -113,7 +114,7 @@ castContext.addCustomMessageListener(
const playerManager = castContext.getPlayerManager();
playerManager.setMessageInterceptor(
framework.messages.MessageType.LOAD,
cast.framework.messages.MessageType.LOAD,
(loadRequestData) => {
if (
loadRequestData.media.contentId ===
@@ -127,24 +128,25 @@ playerManager.setMessageInterceptor(
// Special handling if it came from Google Assistant
if (media.entity) {
media.contentId = media.entity;
media.streamType = framework.messages.StreamType.LIVE;
media.streamType = cast.framework.messages.StreamType.LIVE;
media.contentType = "application/vnd.apple.mpegurl";
// @ts-ignore
media.hlsVideoSegmentFormat =
framework.messages.HlsVideoSegmentFormat.FMP4;
cast.framework.messages.HlsVideoSegmentFormat.FMP4;
}
return loadRequestData;
}
);
playerManager.addEventListener(
framework.events.EventType.MEDIA_STATUS,
cast.framework.events.EventType.MEDIA_STATUS,
(event) => {
if (
event.mediaStatus?.playerState === framework.messages.PlayerState.IDLE &&
event.mediaStatus?.playerState ===
cast.framework.messages.PlayerState.IDLE &&
event.mediaStatus?.idleReason &&
event.mediaStatus?.idleReason !==
framework.messages.IdleReason.INTERRUPTED
cast.framework.messages.IdleReason.INTERRUPTED
) {
// media finished or stopped, return to default Lovelace
showLovelaceController();

View File

@@ -1,7 +1,7 @@
import { html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { mockHistory } from "../../../../demo/src/stubs/history";
import { LovelaceConfig } from "../../../../src/data/lovelace/config/types";
import { LovelaceConfig } from "../../../../src/data/lovelace";
import {
MockHomeAssistant,
provideHass,

View File

@@ -12,8 +12,8 @@ class HcLaunchScreen extends LitElement {
return html`
<div class="container">
<img
alt="Nabu Casa logo on left, Home Assistant logo on right, and red heart in center"
src="https://cast.home-assistant.io/images/nabu-loves-hass.png"
alt="Home Assistant logo on left, Nabu Casa logo on right, and red heart in center"
src="https://www.home-assistant.io/images/blog/2018-09-thinking-big/social.png"
/>
<div class="status">
${this.hass ? "Connected" : "Not Connected"}
@@ -28,6 +28,7 @@ class HcLaunchScreen extends LitElement {
:host {
display: block;
height: 100vh;
padding-top: 64px;
background-color: white;
font-size: 24px;
}
@@ -35,13 +36,15 @@ class HcLaunchScreen extends LitElement {
display: flex;
flex-direction: column;
text-align: center;
align-items: center;
height: 100%;
justify-content: space-evenly;
}
img {
max-width: 80%;
object-fit: cover;
width: 717px;
height: 376px;
display: block;
margin: 0 auto;
}
.status {
padding-right: 54px;
}
`;
}

View File

@@ -1,7 +1,7 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { LovelaceConfig } from "../../../../src/data/lovelace/config/types";
import { LovelaceConfig } from "../../../../src/data/lovelace";
import { Lovelace } from "../../../../src/panels/lovelace/types";
import "../../../../src/panels/lovelace/views/hui-view";
import { HomeAssistant } from "../../../../src/types";
@@ -14,10 +14,9 @@ import "./hc-launch-screen";
class HcLovelace extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false })
public lovelaceConfig!: LovelaceConfig;
@property({ attribute: false }) public lovelaceConfig!: LovelaceConfig;
@property() public viewPath?: string | number | null;
@property() public viewPath?: string | number;
@property() public urlPath: string | null = null;
@@ -93,9 +92,6 @@ class HcLovelace extends LitElement {
}
private get _viewIndex() {
if (this.viewPath === null) {
return 0;
}
const selectedView = this.viewPath;
const selectedViewInt = parseInt(selectedView as string, 10);
for (let i = 0; i < this.lovelaceConfig.views.length; i++) {

View File

@@ -21,27 +21,17 @@ import {
import { atLeastVersion } from "../../../../src/common/config/version";
import { isNavigationClick } from "../../../../src/common/dom/is-navigation-click";
import {
fetchResources,
getLegacyLovelaceCollection,
getLovelaceCollection,
} from "../../../../src/data/lovelace";
import {
isStrategyDashboard,
LegacyLovelaceConfig,
LovelaceConfig,
LovelaceDashboardStrategyConfig,
} from "../../../../src/data/lovelace/config/types";
import { fetchResources } from "../../../../src/data/lovelace/resource";
} from "../../../../src/data/lovelace";
import { loadLovelaceResources } from "../../../../src/panels/lovelace/common/load-resources";
import { HassElement } from "../../../../src/state/hass-element";
import { castContext } from "../cast_context";
import "./hc-launch-screen";
const DEFAULT_CONFIG: LovelaceDashboardStrategyConfig = {
strategy: {
type: "original-states",
},
};
let resourcesLoaded = false;
@customElement("hc-main")
export class HcMain extends HassElement {
@@ -51,10 +41,10 @@ export class HcMain extends HassElement {
@state() private _lovelacePath: string | number | null = null;
@state() private _urlPath?: string | null;
@state() private _error?: string;
@state() private _urlPath?: string | null;
private _hassUUID?: string;
private _unsubLovelace?: UnsubscribeFunc;
@@ -81,7 +71,7 @@ export class HcMain extends HassElement {
if (
!this._lovelaceConfig ||
this._urlPath === undefined ||
this._lovelacePath === null ||
// Guard against part of HA not being loaded yet.
!this.hass ||
!this.hass.states ||
@@ -99,18 +89,16 @@ export class HcMain extends HassElement {
<hc-lovelace
.hass=${this.hass}
.lovelaceConfig=${this._lovelaceConfig}
.urlPath=${this._urlPath}
.viewPath=${this._lovelacePath}
@config-refresh=${this._generateDefaultLovelaceConfig}
.urlPath=${this._urlPath}
@config-refresh=${this._generateLovelaceConfig}
></hc-lovelace>
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
import("./hc-lovelace");
import("../../../../src/resources/ha-style");
import("../second-load");
window.addEventListener("location-changed", () => {
const panelPath = `/${this._urlPath || "lovelace"}/`;
if (location.pathname.startsWith(panelPath)) {
@@ -205,6 +193,7 @@ export class HcMain extends HassElement {
expires_in: 0,
}),
});
this._hassUUID = msg.hassUUID;
} catch (err: any) {
const errorMessage = this._getErrorMessage(err);
this._error = errorMessage;
@@ -224,17 +213,6 @@ export class HcMain extends HassElement {
this.hass.connection.close();
}
this.initializeHass(auth, connection);
if (this._hassUUID !== msg.hassUUID) {
this._hassUUID = msg.hassUUID;
this._lovelaceConfig = undefined;
this._urlPath = undefined;
this._lovelacePath = null;
if (this._unsubLovelace) {
this._unsubLovelace();
this._unsubLovelace = undefined;
}
resourcesLoaded = false;
}
this._error = undefined;
this._sendStatus();
}
@@ -243,7 +221,7 @@ export class HcMain extends HassElement {
this._showDemo = false;
// We should not get this command before we are connected.
// Means a client got out of sync. Let's send status to them.
if (!this.hass?.connected) {
if (!this.hass) {
this._sendStatus(msg.senderId!);
this._error = "Cannot show Lovelace because we're not connected.";
this._sendError(
@@ -280,12 +258,13 @@ export class HcMain extends HassElement {
{
strategy: {
type: "energy",
options: { show_date_selection: true },
},
},
],
};
this._urlPath = "energy";
this._lovelacePath = null;
this._lovelacePath = 0;
this._sendStatus();
return;
}
@@ -294,7 +273,6 @@ export class HcMain extends HassElement {
this._lovelaceConfig = undefined;
if (this._unsubLovelace) {
this._unsubLovelace();
this._unsubLovelace = undefined;
}
const llColl = atLeastVersion(this.hass.connection.haVersion, 0, 107)
? getLovelaceCollection(this.hass.connection, msg.urlPath)
@@ -303,20 +281,9 @@ export class HcMain extends HassElement {
// configuration.
try {
await llColl.refresh();
this._unsubLovelace = llColl.subscribe(async (rawConfig) => {
if (isStrategyDashboard(rawConfig)) {
const { generateLovelaceDashboardStrategy } = await import(
"../../../../src/panels/lovelace/strategies/get-strategy"
);
const config = await generateLovelaceDashboardStrategy(
rawConfig.strategy,
this.hass!
);
this._handleNewLovelaceConfig(config);
} else {
this._handleNewLovelaceConfig(rawConfig);
}
});
this._unsubLovelace = llColl.subscribe((lovelaceConfig) =>
this._handleNewLovelaceConfig(lovelaceConfig)
);
} catch (err: any) {
if (
atLeastVersion(this.hass.connection.haVersion, 0, 107) &&
@@ -330,7 +297,7 @@ export class HcMain extends HassElement {
}
// Generate a Lovelace config.
this._unsubLovelace = () => undefined;
await this._generateDefaultLovelaceConfig();
await this._generateLovelaceConfig();
}
}
if (!resourcesLoaded) {
@@ -339,21 +306,24 @@ export class HcMain extends HassElement {
? await fetchResources(this.hass!.connection)
: (this._lovelaceConfig as LegacyLovelaceConfig).resources;
if (resources) {
loadLovelaceResources(resources, this.hass!);
loadLovelaceResources(resources, this.hass!.auth.data.hassUrl);
}
}
this._sendStatus();
}
private async _generateDefaultLovelaceConfig() {
private async _generateLovelaceConfig() {
const { generateLovelaceDashboardStrategy } = await import(
"../../../../src/panels/lovelace/strategies/get-strategy"
);
this._handleNewLovelaceConfig(
await generateLovelaceDashboardStrategy(
DEFAULT_CONFIG.strategy,
this.hass!
{
hass: this.hass!,
narrow: false,
},
"original-states"
)
);
}

View File

@@ -0,0 +1,3 @@
import "../../../src/resources/ha-style";
import "../../../src/resources/roboto";
import "./layout/hc-lovelace";

View File

@@ -8,67 +8,25 @@
"src": "/static/icons/favicon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
"purpose": "maskable any"
},
{
"src": "/static/icons/favicon-384x384.png",
"sizes": "384x384",
"type": "image/png",
"purpose": "any"
"purpose": "maskable any"
},
{
"src": "/static/icons/favicon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
"purpose": "maskable any"
},
{
"src": "/static/icons/favicon-1024x1024.png",
"sizes": "1024x1024",
"type": "image/png",
"purpose": "any"
},
{
"src": "/static/icons/maskable_icon-48x48.png",
"sizes": "48x48",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/static/icons/maskable_icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/static/icons/maskable_icon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/static/icons/maskable_icon-128x128.png",
"sizes": "128x128",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/static/icons/maskable_icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/static/icons/maskable_icon-384x384.png",
"sizes": "384x384",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/static/icons/maskable_icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
"purpose": "maskable any"
}
],
"lang": "en-US",

View File

@@ -1,3 +1,3 @@
self.addEventListener("fetch", (event) => {
self.addEventListener("fetch", function(event) {
event.respondWith(fetch(event.request));
});

View File

@@ -3,15 +3,6 @@ import { DemoConfig } from "../types";
export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
convertEntities({
"todo.shopping_list": {
entity_id: "todo.shopping_list",
state: "2",
attributes: {
supported_features: 15,
friendly_name: "Shopping List",
icon: "mdi:cart",
},
},
"zone.home": {
entity_id: "zone.home",
state: "zoning",

View File

@@ -3,15 +3,6 @@ import { DemoConfig } from "../types";
export const demoEntitiesJimpower: DemoConfig["entities"] = () =>
convertEntities({
"todo.shopping_list": {
entity_id: "todo.shopping_list",
state: "2",
attributes: {
supported_features: 15,
friendly_name: "Shopping List",
icon: "mdi:cart",
},
},
"zone.powertec": {
entity_id: "zone.powertec",
state: "zoning",

View File

@@ -4,11 +4,16 @@ export const demoThemeJimpower = () => ({
"primary-color": "#5294E2",
"label-badge-red": "var(--accent-color)",
"paper-tabs-selection-bar-color": "green",
"paper-slider-knob-color": "var(--accent-color)",
"light-primary-color": "var(--accent-color)",
"primary-background-color": "#383C45",
"primary-text-color": "#FFFFFF",
"paper-item-selected_-_background-color": "#434954",
"paper-slider-active-color": "var(--accent-color)",
"secondary-background-color": "#383C45",
"paper-slider-container-color":
"linear-gradient(var(--primary-background-color), var(--secondary-background-color)) no-repeat",
"paper-slider-disabled-active-color": "var(--disabled-text-color)",
"disabled-text-color": "#7F848E",
"paper-item-icon_-_color": "green",
"paper-grey-200": "#414A59",
@@ -27,10 +32,14 @@ export const demoThemeJimpower = () => ({
"switch-unchecked-button-color": "var(--disabled-text-color)",
"label-badge-border-color": "green",
"paper-listbox-color": "var(--primary-color)",
"paper-slider-disabled-secondary-color": "var(--disabled-text-color)",
"card-background-color": "#434954",
"label-badge-text-color": "var(--primary-text-color)",
"paper-slider-knob-start-color": "var(--accent-color)",
"switch-unchecked-track-color": "var(--disabled-text-color)",
"dark-primary-color": "var(--accent-color)",
"paper-slider-secondary-color": "var(--secondary-background-color)",
"paper-slider-pin-color": "var(--accent-color)",
"paper-item-icon-active-color": "#F9C536",
"accent-color": "#E45E65",
"table-row-alternative-background-color": "#3E424B",

View File

@@ -3,15 +3,6 @@ import { DemoConfig } from "../types";
export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
convertEntities({
"todo.shopping_list": {
entity_id: "todo.shopping_list",
state: "2",
attributes: {
supported_features: 15,
friendly_name: "Shopping List",
icon: "mdi:cart",
},
},
"zone.anna": {
entity_id: "zone.anna",
state: "zoning",

View File

@@ -5,12 +5,17 @@ export const demoThemeKernehed = () => ({
"primary-color": "#2980b9",
"label-badge-red": "var(--accent-color)",
"paper-tabs-selection-bar-color": "green",
"paper-slider-knob-color": "var(--accent-color)",
"primary-text-color": "#FFFFFF",
"light-primary-color": "var(--accent-color)",
"primary-background-color": "#222222",
"sidebar-icon-color": "#777777",
"paper-item-selected_-_background-color": "#292929",
"paper-slider-active-color": "var(--accent-color)",
"secondary-background-color": "#222222",
"paper-slider-container-color":
"linear-gradient(var(--primary-background-color), var(--secondary-background-color)) no-repeat",
"paper-slider-disabled-active-color": "var(--disabled-text-color)",
"disabled-text-color": "#777777",
"paper-item-icon_-_color": "green",
"paper-grey-200": "#222222",
@@ -28,10 +33,14 @@ export const demoThemeKernehed = () => ({
"switch-unchecked-button-color": "var(--disabled-text-color)",
"label-badge-border-color": "green",
"paper-listbox-color": "#777777",
"paper-slider-disabled-secondary-color": "var(--disabled-text-color)",
"card-background-color": "#292929",
"label-badge-text-color": "var(--primary-text-color)",
"paper-slider-knob-start-color": "var(--accent-color)",
"switch-unchecked-track-color": "var(--disabled-text-color)",
"dark-primary-color": "var(--accent-color)",
"paper-slider-secondary-color": "var(--secondary-background-color)",
"paper-slider-pin-color": "var(--accent-color)",
"paper-item-icon-active-color": "#b58e31",
"accent-color": "#2980b9",
"table-row-alternative-background-color": "#292929",

View File

@@ -3,15 +3,6 @@ import { DemoConfig } from "../types";
export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
convertEntities({
"todo.shopping_list": {
entity_id: "todo.shopping_list",
state: "2",
attributes: {
supported_features: 15,
friendly_name: "Shopping List",
icon: "mdi:cart",
},
},
"sensor.pollen_grabo": {
entity_id: "sensor.pollen_grabo",
state: "",

View File

@@ -220,8 +220,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
state_filter: ["on"],
},
{
type: "todo-list",
entity: "todo.shopping_list",
type: "shopping-list",
},
{
entities: [

View File

@@ -1,5 +1,6 @@
export const demoThemeTeachingbirds = () => ({
"paper-card-header-color": "var(--paper-item-icon-color)",
"paper-slider-pin-color": "var(--primary-color)",
"paper-listbox-background-color": "#202020",
"paper-grey-50": "var(--primary-text-color)",
"paper-item-icon-color": "#d3d3d3",
@@ -7,6 +8,8 @@ export const demoThemeTeachingbirds = () => ({
"primary-color": "#389638",
"light-primary-color": "#6f956f",
"label-badge-red": "var(--primary-color)",
"paper-slider-secondary-color": "var(--light-primary-color)",
"paper-slider-knob-color": "var(--primary-color)",
"paper-listbox-color": "#FFFFFF",
"paper-toggle-button-checked-bar-color": "var(--light-primary-color)",
"switch-unchecked-track-color": "var(--primary-text-color)",
@@ -14,7 +17,9 @@ export const demoThemeTeachingbirds = () => ({
"label-badge-text-color": "var(--text-primary-color)",
"primary-background-color": "#303030",
"sidebar-icon-color": "var(--paper-item-icon-color)",
"paper-slider-active-color": "#d8bf50",
"secondary-background-color": "#2b2b2b",
"paper-slider-knob-start-color": "var(--primary-color)",
"paper-item-icon-active-color": "#d8bf50",
"switch-checked-color": "var(--primary-color)",
"secondary-text-color": "#389638",

View File

@@ -1,5 +1,5 @@
import { LocalizeFunc } from "../../../src/common/translations/localize";
import { LovelaceConfig } from "../../../src/data/lovelace/config/types";
import { LovelaceConfig } from "../../../src/data/lovelace";
import { Entity } from "../../../src/fake_data/entity";
export interface DemoConfig {

View File

@@ -4,7 +4,7 @@ import { customElement, property, state } from "lit/decorators";
import { until } from "lit/directives/until";
import "../../../src/components/ha-card";
import "../../../src/components/ha-circular-progress";
import { LovelaceCardConfig } from "../../../src/data/lovelace/config/card";
import { LovelaceCardConfig } from "../../../src/data/lovelace";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
import { Lovelace, LovelaceCard } from "../../../src/panels/lovelace/types";
import {
@@ -39,9 +39,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {
<div class="picker">
<div class="label">
${this._switching
? html`<ha-circular-progress
indeterminate
></ha-circular-progress>`
? html`<ha-circular-progress active></ha-circular-progress>`
: until(
selectedDemoConfig.then(
(conf) => html`
@@ -50,7 +48,8 @@ export class HADemoCard extends LitElement implements LovelaceCard {
<a target="_blank" href=${conf.authorUrl}>
${this.hass.localize(
"ui.panel.page-demo.cards.demo.demo_by",
{ name: conf.authorName }
"name",
conf.authorName
)}
</a>
</small>

View File

@@ -1,4 +1,4 @@
import "../../src/resources/ha-style";
import "../../src/resources/roboto";
import "../../src/resources/safari-14-attachshadow-patch";
import "./ha-demo";
import("../../src/resources/ha-style");

View File

@@ -17,14 +17,12 @@ import { energyEntities } from "./stubs/entities";
import { mockEntityRegistry } from "./stubs/entity_registry";
import { mockEvents } from "./stubs/events";
import { mockFrontend } from "./stubs/frontend";
import { mockIcons } from "./stubs/icons";
import { mockHistory } from "./stubs/history";
import { mockLovelace } from "./stubs/lovelace";
import { mockMediaPlayer } from "./stubs/media_player";
import { mockPersistentNotification } from "./stubs/persistent_notification";
import { mockRecorder } from "./stubs/recorder";
import { mockTodo } from "./stubs/todo";
import { mockSensor } from "./stubs/sensor";
import { mockShoppingList } from "./stubs/shopping_list";
import { mockSystemLog } from "./stubs/system_log";
import { mockTemplate } from "./stubs/template";
import { mockTranslations } from "./stubs/translations";
@@ -51,14 +49,12 @@ export class HaDemo extends HomeAssistantAppEl {
mockTranslations(hass);
mockHistory(hass);
mockRecorder(hass);
mockTodo(hass);
mockSensor(hass);
mockShoppingList(hass);
mockSystemLog(hass);
mockTemplate(hass);
mockEvents(hass);
mockMediaPlayer(hass);
mockFrontend(hass);
mockIcons(hass);
mockEnergy(hass);
mockPersistentNotification(hass);
mockConfigEntries(hass);

File diff suppressed because one or more lines are too long

View File

@@ -1,18 +1,20 @@
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockConfigEntries = (hass: MockHomeAssistant) => {
hass.mockWS("config_entries/get", () => ({
entry_id: "co2signal",
domain: "co2signal",
title: "Electricity Maps",
source: "user",
state: "loaded",
supports_options: false,
supports_remove_device: false,
supports_unload: true,
pref_disable_new_entities: false,
pref_disable_polling: false,
disabled_by: null,
reason: null,
}));
hass.mockWS("config_entries/get_matching", () => [
{
entry_id: "co2signal",
domain: "co2signal",
title: "CO2 Signal",
source: "user",
state: "loaded",
supports_options: false,
supports_remove_device: false,
supports_unload: true,
pref_disable_new_entities: false,
pref_disable_polling: false,
disabled_by: null,
reason: null,
},
]);
};

View File

@@ -1,33 +0,0 @@
import { IconCategory } from "../../../src/data/icons";
import { ENTITY_COMPONENT_ICONS } from "../../../src/fake_data/entity_component_icons";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockIcons = (hass: MockHomeAssistant) => {
hass.mockWS(
"frontend/get_icons",
async ({
category,
integration,
}: {
category: IconCategory;
integration?: string;
}) => {
if (integration) {
try {
const response = await fetch(
`https://raw.githubusercontent.com/home-assistant/core/dev/homeassistant/components/${integration}/icons.json`
).then((resp) => resp.json());
return { resources: { [integration]: response[category] || {} } };
} catch {
return { resources: {} };
}
}
if (category === "entity_component") {
return {
resources: ENTITY_COMPONENT_ICONS,
};
}
return { resources: {} };
}
);
};

View File

@@ -1,20 +1,16 @@
import { PersistentNotificationMessage } from "../../../src/data/persistent_notification";
import { PersistentNotification } from "../../../src/data/persistent_notification";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockPersistentNotification = (hass: MockHomeAssistant) => {
hass.mockWS("persistent_notification/subscribe", (_msg, _hass, onChange) => {
onChange!({
type: "added",
notifications: {
"demo-1": {
created_at: new Date().toISOString(),
message: "There was motion detected in the backyard.",
notification_id: "demo-1",
title: "Motion Detected!",
status: "unread",
},
hass.mockWS("persistent_notification/get", () =>
Promise.resolve([
{
created_at: new Date().toISOString(),
message: "There was motion detected in the backyard.",
notification_id: "demo-1",
title: "Motion Detected!",
status: "unread",
},
} as PersistentNotificationMessage);
return () => {};
});
] as PersistentNotification[])
);
};

View File

@@ -43,8 +43,8 @@ const generateMeanStatistics = (
period === "day"
? addDays(currentDate, 1)
: period === "month"
? addMonths(currentDate, 1)
: addHours(currentDate, 1);
? addMonths(currentDate, 1)
: addHours(currentDate, 1);
}
return statistics;
};
@@ -72,7 +72,6 @@ const generateSumStatistics = (
min: null,
max: null,
last_reset: 0,
change: add,
state: initValue + sum,
sum,
});
@@ -80,8 +79,8 @@ const generateSumStatistics = (
period === "day"
? addDays(currentDate, 1)
: period === "month"
? addMonths(currentDate, 1)
: addHours(currentDate, 1);
? addMonths(currentDate, 1)
: addHours(currentDate, 1);
}
return statistics;
};
@@ -104,8 +103,8 @@ const generateCurvedStatistics = (
let half = false;
const now = new Date();
while (end > currentDate && currentDate < now) {
const add = i * (Math.random() * maxDiff);
sum += add;
const add = Math.random() * maxDiff;
sum += i * add;
statistics.push({
start: currentDate.getTime(),
end: currentDate.getTime(),
@@ -113,7 +112,6 @@ const generateCurvedStatistics = (
min: null,
max: null,
last_reset: 0,
change: add,
state: initValue + sum,
sum: metered ? sum : null,
});

View File

@@ -1,58 +0,0 @@
import { 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",
],
},
]);
};

View File

@@ -0,0 +1,44 @@
import { ShoppingListItem } from "../../../src/data/shopping-list";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
let items: ShoppingListItem[] = [
{
id: 12,
name: "Milk",
complete: false,
},
{
id: 13,
name: "Eggs",
complete: false,
},
{
id: 14,
name: "Oranges",
complete: true,
},
];
export const mockShoppingList = (hass: MockHomeAssistant) => {
hass.mockWS("shopping_list/items", () => items);
hass.mockWS("shopping_list/items/add", (msg) => {
const item: ShoppingListItem = {
id: new Date().getTime(),
complete: false,
name: msg.name,
};
items.push(item);
hass.mockEvent("shopping_list_updated");
return item;
});
hass.mockWS("shopping_list/items/update", ({ type, item_id, ...updates }) => {
items = items.map((item) =>
item.id === item_id ? { ...item, ...updates } : item
);
hass.mockEvent("shopping_list_updated");
});
hass.mockWS("shopping_list/items/clear", () => {
items = items.filter((item) => !item.complete);
hass.mockEvent("shopping_list_updated");
});
};

View File

@@ -1,25 +0,0 @@
import { TodoItem, TodoItemStatus } from "../../../src/data/todo";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockTodo = (hass: MockHomeAssistant) => {
hass.mockWS("todo/item/list", () => ({
items: [
{
uid: "12",
summary: "Milk",
status: TodoItemStatus.NeedsAction,
},
{
uid: "13",
summary: "Eggs",
status: TodoItemStatus.NeedsAction,
},
{
uid: "14",
summary: "Oranges",
status: TodoItemStatus.Completed,
},
] as TodoItem[],
}));
hass.mockWS("todo/item/subscribe", (_msg, _hass) => () => {});
};

View File

@@ -1,4 +0,0 @@
# Note!
Note, the assets in this folder, are not part of the CC license this repository is shipped in.
All rights reserved.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -1,5 +1,5 @@
import { Button } from "@material/mwc-button";
import { html, LitElement, css, TemplateResult, nothing } from "lit";
import { html, LitElement, css, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
import { fireEvent } from "../../../src/common/dom/fire_event";
@@ -9,9 +9,9 @@ import "../../../src/components/ha-card";
class DemoBlackWhiteRow extends LitElement {
@property() title!: string;
@property() value?: any;
@property() value!: any;
@property({ type: Boolean }) public disabled = false;
@property() disabled = false;
protected render(): TemplateResult {
return html`
@@ -45,9 +45,7 @@ class DemoBlackWhiteRow extends LitElement {
</mwc-button>
</div>
</ha-card>
${this.value
? html`<pre>${JSON.stringify(this.value, undefined, 2)}</pre>`
: nothing}
<pre>${JSON.stringify(this.value, undefined, 2)}</pre>
</div>
</div>
`;

View File

@@ -11,11 +11,11 @@ export interface DemoCardConfig {
@customElement("demo-card")
class DemoCard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public hass!: HomeAssistant;
@property({ attribute: false }) public config!: DemoCardConfig;
@property() public config!: DemoCardConfig;
@property({ type: Boolean }) public showConfig = false;
@property() public showConfig = false;
@state() private _size?: number;

View File

@@ -1,3 +1,4 @@
import "@polymer/app-layout/app-toolbar/app-toolbar";
import { html, css, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
@@ -6,13 +7,12 @@ import "../../../src/components/ha-switch";
import { HomeAssistant } from "../../../src/types";
import "./demo-card";
import type { DemoCardConfig } from "./demo-card";
import "../ha-demo-options";
@customElement("demo-cards")
class DemoCards extends LitElement {
@property({ attribute: false }) public configs!: DemoCardConfig[];
@property() public configs!: DemoCardConfig[];
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public hass!: HomeAssistant;
@state() private _showConfig = false;
@@ -20,14 +20,20 @@ class DemoCards extends LitElement {
render() {
return html`
<ha-demo-options>
<ha-formfield label="Show config">
<ha-switch @change=${this._showConfigToggled}> </ha-switch>
</ha-formfield>
<ha-formfield label="Dark theme">
<ha-switch @change=${this._darkThemeToggled}> </ha-switch>
</ha-formfield>
</ha-demo-options>
<app-toolbar>
<div class="filters">
<ha-formfield label="Show config">
<ha-switch
.checked=${this._showConfig}
@change=${this._showConfigToggled}
>
</ha-switch>
</ha-formfield>
<ha-formfield label="Dark theme">
<ha-switch @change=${this._darkThemeToggled}> </ha-switch>
</ha-formfield>
</div>
</app-toolbar>
<div id="container">
<div class="cards">
${this.configs.map(
@@ -63,6 +69,12 @@ class DemoCards extends LitElement {
demo-card {
margin: 16px 16px 32px;
}
app-toolbar {
background-color: var(--light-primary-color);
}
.filters {
margin-left: 60px;
}
ha-formfield {
margin-right: 16px;
}

View File

@@ -0,0 +1,93 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/ha-card";
import "../../../src/dialogs/more-info/more-info-content";
import "../../../src/state-summary/state-card-content";
class DemoMoreInfo extends PolymerElement {
static get template() {
return html`
<style>
.root {
display: flex;
}
#card {
max-width: 400px;
width: 100vw;
}
ha-card {
width: 352px;
padding: 20px 24px;
}
state-card-content {
display: block;
margin-bottom: 16px;
}
pre {
width: 400px;
margin: 0 16px;
overflow: auto;
color: var(--primary-text-color);
}
@media only screen and (max-width: 800px) {
.root {
flex-direction: column;
}
pre {
margin: 16px 0;
}
}
</style>
<div class="root">
<div id="card">
<ha-card>
<state-card-content
state-obj="[[_stateObj]]"
hass="[[hass]]"
in-dialog
></state-card-content>
<more-info-content
hass="[[hass]]"
state-obj="[[_stateObj]]"
></more-info-content>
</ha-card>
</div>
<template is="dom-if" if="[[showConfig]]">
<pre>[[_jsonEntity(_stateObj)]]</pre>
</template>
</div>
`;
}
static get properties() {
return {
hass: Object,
entityId: String,
showConfig: Boolean,
_stateObj: {
type: Object,
computed: "_getState(entityId, hass.states)",
},
};
}
_getState(entityId, states) {
return states[entityId];
}
_jsonEntity(stateObj) {
// We are caching some things on stateObj
// (it sucks, we will remove in the future)
const tmp = {};
Object.keys(stateObj).forEach((key) => {
if (key[0] !== "_") {
tmp[key] = stateObj[key];
}
});
return JSON.stringify(tmp, null, 2);
}
}
customElements.define("demo-more-info", DemoMoreInfo);

View File

@@ -1,93 +0,0 @@
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../src/components/ha-card";
import "../../../src/dialogs/more-info/more-info-content";
import "../../../src/state-summary/state-card-content";
import "../ha-demo-options";
import { HomeAssistant } from "../../../src/types";
@customElement("demo-more-info")
class DemoMoreInfo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public entityId!: string;
@property({ type: Boolean }) public showConfig = false;
render() {
const state = this._getState(this.entityId, this.hass.states);
return html`
<div class="root">
<div id="card">
<ha-card>
<state-card-content
.stateObj=${state}
.hass=${this.hass}
inDialog
></state-card-content>
<more-info-content
.hass=${this.hass}
.stateObj=${state}
></more-info-content>
</ha-card>
</div>
${this.showConfig ? html`<pre>${this._jsonEntity(state)}</pre>` : ""}
</div>
`;
}
private _getState(entityId, states) {
return states[entityId];
}
private _jsonEntity(stateObj) {
// We are caching some things on stateObj
// (it sucks, we will remove in the future)
const tmp = {};
Object.keys(stateObj).forEach((key) => {
if (key[0] !== "_") {
tmp[key] = stateObj[key];
}
});
return JSON.stringify(tmp, null, 2);
}
static styles = css`
.root {
display: flex;
}
#card {
max-width: 400px;
width: 100vw;
}
ha-card {
width: 352px;
padding: 20px 24px;
}
state-card-content {
display: block;
margin-bottom: 16px;
}
pre {
width: 400px;
margin: 0 16px;
overflow: auto;
color: var(--primary-text-color);
}
@media only screen and (max-width: 800px) {
.root {
flex-direction: column;
}
pre {
margin: 16px 0;
}
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"demo-more-info": DemoMoreInfo;
}
}

View File

@@ -0,0 +1,83 @@
import "@polymer/app-layout/app-toolbar/app-toolbar";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-switch";
import "./demo-more-info";
class DemoMoreInfos extends PolymerElement {
static get template() {
return html`
<style>
#container {
min-height: calc(100vh - 128px);
background: var(--primary-background-color);
}
.cards {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
demo-more-info {
margin: 16px 16px 32px;
}
app-toolbar {
background-color: var(--light-primary-color);
}
.filters {
margin-left: 60px;
}
ha-formfield {
margin-right: 16px;
}
</style>
<app-toolbar>
<div class="filters">
<ha-formfield label="Show entities">
<ha-switch checked="[[_showConfig]]" on-change="_showConfigToggled">
</ha-switch>
</ha-formfield>
<ha-formfield label="Dark theme">
<ha-switch on-change="_darkThemeToggled"> </ha-switch>
</ha-formfield>
</div>
</app-toolbar>
<div id="container">
<div class="cards">
<template is="dom-repeat" items="[[entities]]">
<demo-more-info
entity-id="[[item]]"
show-config="[[_showConfig]]"
hass="[[hass]]"
></demo-more-info>
</template>
</div>
</div>
`;
}
static get properties() {
return {
entities: Array,
hass: Object,
_showConfig: {
type: Boolean,
value: false,
},
};
}
_showConfigToggled(ev) {
this._showConfig = ev.target.checked;
}
_darkThemeToggled(ev) {
applyThemesOnElement(this.$.container, { themes: {} }, "default", {
dark: ev.target.checked,
});
}
}
customElements.define("demo-more-infos", DemoMoreInfos);

View File

@@ -1,87 +0,0 @@
import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-switch";
import { HomeAssistant } from "../../../src/types";
import "../ha-demo-options";
import "./demo-more-info";
@customElement("demo-more-infos")
class DemoMoreInfos extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Array }) public entities!: string[];
@state() private _showConfig = false;
render() {
return html`
<ha-demo-options>
<ha-formfield label="Show config">
<ha-switch @change=${this._showConfigToggled}> </ha-switch>
</ha-formfield>
<ha-formfield label="Dark theme">
<ha-switch @change=${this._darkThemeToggled}> </ha-switch>
</ha-formfield>
</ha-demo-options>
<div id="container">
<div class="cards">
${this.entities.map(
(item) =>
html`<demo-more-info
.entityId=${item}
.showConfig=${this._showConfig}
.hass=${this.hass}
></demo-more-info>`
)}
</div>
</div>
`;
}
static styles = css`
#container {
min-height: calc(100vh - 128px);
background: var(--primary-background-color);
}
.cards {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
demo-more-info {
margin: 16px 16px 32px;
}
ha-formfield {
margin-right: 16px;
}
`;
_showConfigToggled(ev) {
this._showConfig = ev.target.checked;
}
_darkThemeToggled(ev) {
applyThemesOnElement(
this.shadowRoot!.querySelector("#container"),
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: false,
theme: "default",
},
"default",
{
dark: ev.target.checked,
}
);
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-more-infos": DemoMoreInfos;
}
}

View File

@@ -1,24 +0,0 @@
import type { ControlSelectOption } from "../../../src/components/ha-control-select";
export const timeOptions: ControlSelectOption[] = [
{
value: "now",
label: "Now",
},
{
value: "00:15:30",
label: "12:15:30 AM",
},
{
value: "06:15:30",
label: "06:15:30 AM",
},
{
value: "12:15:30",
label: "12:15:30 PM",
},
{
value: "18:15:30",
label: "06:15:30 PM",
},
];

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