Compare commits

..

1 Commits

Author SHA1 Message Date
Bram Kragten
29a103e884 Prevent wrap of menu items 2024-04-02 15:09:08 +02:00
890 changed files with 19958 additions and 41162 deletions

View File

@@ -1,25 +1,28 @@
[modern] [modern]
# Modern builds target recent browsers supporting the latest features to minimize transpilation, polyfills, etc. # Support for dynamic import is the main litmus test for serving modern builds.
# It is served to browsers meeting the following requirements: # Although officially a ES2020 feature, browsers implemented it early, so this
# - released in the last year + current alpha/beta versions # enables all of ES2017 and some features in ES2018.
# - Firefox extended support release (ESR) supports es6-module-dynamic-import
# - with global utilization at or above 0.5%
# - must support dynamic import of ES modules # Exclude Safari 11-12 because of a bug in tagged template literals
# - exclude browsers no longer being maintained # https://bugs.webkit.org/show_bug.cgi?id=190756
# - exclude KaiOS, QQ, and UC browsers due to lack of sufficient feature support data # Note: Dropping version 11 also enables several more ES2018 features
unreleased versions not Safari < 13
last 1 year not iOS < 13
Firefox ESR
>= 0.5% and supports es6-module-dynamic-import # Exclude KaiOS, QQ, and UC browsers due to lack of sufficient feature support data
not dead # Babel ignores these automatically, but we need here for Webpack to output ESM with dynamic imports
not KaiOS > 0 not KaiOS > 0
not QQAndroid > 0 not QQAndroid > 0
not UCAndroid > 0 not UCAndroid > 0
# Exclude unsupported browsers
not dead
[legacy] [legacy]
# Legacy builds are served when modern requirements are not met and support browsers: # Legacy builds are served when modern requirements are not met and support browsers:
# - released in the last 7 years + current alpha/beta versionss # - released in the last 7 years + current alpha/beta versionss
# - with global utilization at or above 0.05% # - with global utilization above 0.05%
# The lattermost query ensures that support for popular old browsers is not dropped too early # The lattermost query ensures that support for popular old browsers is not dropped too early
# (e.g. IE 11, Android 4.4, or Samsung 4). # (e.g. IE 11, Android 4.4, or Samsung 4).
# #
@@ -33,10 +36,4 @@ not UCAndroid > 0
# As of May 2023, only web sockets must be added to the query. # As of May 2023, only web sockets must be added to the query.
unreleased versions unreleased versions
last 7 years last 7 years
>= 0.05% and supports websockets > 0.05% and supports websockets
[legacy-sw]
# Same as legacy plus supports service workers
unreleased versions
last 7 years
>= 0.05% and supports websockets and supports serviceworkers

View File

@@ -8,7 +8,6 @@
"postCreateCommand": "sudo apt update && sudo apt upgrade -y && sudo apt install -y libpcap-dev", "postCreateCommand": "sudo apt update && sudo apt upgrade -y && sudo apt install -y libpcap-dev",
"postStartCommand": "script/bootstrap", "postStartCommand": "script/bootstrap",
"containerEnv": { "containerEnv": {
"DEV_CONTAINER": "1",
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}" "WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
}, },
"customizations": { "customizations": {

View File

@@ -115,7 +115,6 @@
} }
], ],
"unused-imports/no-unused-imports": "error", "unused-imports/no-unused-imports": "error",
"lit/attribute-names": "warn",
"lit/attribute-value-entities": "off", "lit/attribute-value-entities": "off",
"lit/no-template-map": "off", "lit/no-template-map": "off",
"lit/no-native-attributes": "warn", "lit/no-native-attributes": "warn",
@@ -126,5 +125,6 @@
"lit-a11y/anchor-is-valid": "warn", "lit-a11y/anchor-is-valid": "warn",
"lit-a11y/role-has-required-aria-attrs": "warn" "lit-a11y/role-has-required-aria-attrs": "warn"
}, },
"plugins": ["unused-imports"] "plugins": ["disable", "unused-imports"],
"processor": "disable/disable"
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Send bundle stats and build information to RelativeCI - name: Send bundle stats and build information to RelativeCI
uses: relative-ci/agent-action@v2.1.12 uses: relative-ci/agent-action@v2.1.10
with: with:
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }} key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
token: ${{ github.token }} token: ${{ github.token }}

View File

@@ -23,7 +23,7 @@ jobs:
contents: write # Required to upload release assets contents: write # Required to upload release assets
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.2.0 uses: actions/checkout@v4.1.2
- name: Verify version - name: Verify version
uses: home-assistant/actions/helpers/verify-version@master uses: home-assistant/actions/helpers/verify-version@master
@@ -34,7 +34,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.4 uses: actions/setup-node@v4.0.2
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -55,7 +55,7 @@ jobs:
script/release script/release
- name: Upload release assets - name: Upload release assets
uses: softprops/action-gh-release@v2.0.8 uses: softprops/action-gh-release@v2.0.4
with: with:
files: | files: |
dist/*.whl dist/*.whl
@@ -74,9 +74,9 @@ jobs:
echo "home-assistant-frontend==$version" > ./requirements.txt echo "home-assistant-frontend==$version" > ./requirements.txt
- name: Build wheels - name: Build wheels
uses: home-assistant/wheels@2024.07.1 uses: home-assistant/wheels@2024.01.0
with: with:
abi: cp312 abi: cp311
tag: musllinux_1_2 tag: musllinux_1_2
arch: amd64 arch: amd64
wheels-key: ${{ secrets.WHEELS_KEY }} wheels-key: ${{ secrets.WHEELS_KEY }}

View File

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

View File

@@ -1 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
yarn run lint-staged --relative --shell "/bin/bash" yarn run lint-staged --relative --shell "/bin/bash"

View File

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

View File

@@ -1,55 +0,0 @@
diff --git a/build/inject-manifest.js b/build/inject-manifest.js
index 60e3d2bb51c11a19fbbedbad65e101082ec41c36..fed6026630f43f86e25446383982cf6fb694313b 100644
--- a/build/inject-manifest.js
+++ b/build/inject-manifest.js
@@ -104,7 +104,7 @@ async function injectManifest(config) {
replaceString: manifestString,
searchString: options.injectionPoint,
});
- filesToWrite[options.swDest] = source;
+ filesToWrite[options.swDest] = source.replace(url, encodeURI(upath_1.default.basename(destPath)));
filesToWrite[destPath] = map;
}
else {
diff --git a/build/lib/translate-url-to-sourcemap-paths.js b/build/lib/translate-url-to-sourcemap-paths.js
index 3220c5474eeac6e8a56ca9b2ac2bd9be48529e43..5f003879a904d4840529a42dd056d288fd213771 100644
--- a/build/lib/translate-url-to-sourcemap-paths.js
+++ b/build/lib/translate-url-to-sourcemap-paths.js
@@ -22,7 +22,7 @@ function translateURLToSourcemapPaths(url, swSrc, swDest) {
const possibleSrcPath = upath_1.default.resolve(upath_1.default.dirname(swSrc), url);
if (fs_extra_1.default.existsSync(possibleSrcPath)) {
srcPath = possibleSrcPath;
- destPath = upath_1.default.resolve(upath_1.default.dirname(swDest), url);
+ destPath = `${swDest}.map`;
}
else {
warning = `${errors_1.errors['cant-find-sourcemap']} ${possibleSrcPath}`;
diff --git a/src/inject-manifest.ts b/src/inject-manifest.ts
index 8795ddcaa77aea7b0356417e4bc4b19e2b3f860c..fcdc68342d9ac53936c9ed40a9ccfc2f5070cad3 100644
--- a/src/inject-manifest.ts
+++ b/src/inject-manifest.ts
@@ -129,7 +129,10 @@ export async function injectManifest(
searchString: options.injectionPoint!,
});
- filesToWrite[options.swDest] = source;
+ filesToWrite[options.swDest] = source.replace(
+ url!,
+ encodeURI(upath.basename(destPath)),
+ );
filesToWrite[destPath] = map;
} else {
// If there's no sourcemap associated with swSrc, a simple string
diff --git a/src/lib/translate-url-to-sourcemap-paths.ts b/src/lib/translate-url-to-sourcemap-paths.ts
index 072eac40d4ef5d095a01cb7f7e392a9e034853bd..f0bbe69e88ef3a415de18a7e9cb264daea273d71 100644
--- a/src/lib/translate-url-to-sourcemap-paths.ts
+++ b/src/lib/translate-url-to-sourcemap-paths.ts
@@ -28,7 +28,7 @@ export function translateURLToSourcemapPaths(
const possibleSrcPath = upath.resolve(upath.dirname(swSrc), url);
if (fse.existsSync(possibleSrcPath)) {
srcPath = possibleSrcPath;
- destPath = upath.resolve(upath.dirname(swDest), url);
+ destPath = `${swDest}.map`;
} else {
warning = `${errors['cant-find-sourcemap']} ${possibleSrcPath}`;
}

893
.yarn/releases/yarn-4.1.1.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

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

View File

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

View File

@@ -1,56 +1,7 @@
import defineProvider from "@babel/helper-define-polyfill-provider"; import defineProvider from "@babel/helper-define-polyfill-provider";
import { join } from "node:path";
import paths from "../paths.cjs";
const POLYFILL_DIR = join(paths.polymer_dir, "src/resources/polyfills");
// List of polyfill keys with supported browser targets for the functionality // List of polyfill keys with supported browser targets for the functionality
const PolyfillSupport = { const PolyfillSupport = {
// Note states and shadowRoot properties should be supported.
"element-internals": {
android: 90,
chrome: 90,
edge: 90,
firefox: 126,
ios: 17.4,
opera: 76,
opera_mobile: 64,
safari: 17.4,
samsung: 15.0,
},
"element-append": {
android: 54,
chrome: 54,
edge: 17,
firefox: 49,
ios: 10.0,
opera: 41,
opera_mobile: 41,
safari: 10.0,
samsung: 6.0,
},
"element-getattributenames": {
android: 61,
chrome: 61,
edge: 18,
firefox: 45,
ios: 10.3,
opera: 48,
opera_mobile: 45,
safari: 10.1,
samsung: 8.0,
},
"element-toggleattribute": {
android: 69,
chrome: 69,
edge: 18,
firefox: 63,
ios: 12.0,
opera: 56,
opera_mobile: 48,
safari: 12.0,
samsung: 10.0,
},
fetch: { fetch: {
android: 42, android: 42,
chrome: 42, chrome: 42,
@@ -62,31 +13,6 @@ const PolyfillSupport = {
safari: 10.1, safari: 10.1,
samsung: 4.0, samsung: 4.0,
}, },
"intl-getcanonicallocales": {
android: 54,
chrome: 54,
edge: 16,
firefox: 48,
ios: 10.3,
opera: 41,
opera_mobile: 41,
safari: 10.1,
samsung: 6.0,
},
"intl-locale": {
android: 74,
chrome: 74,
edge: 79,
firefox: 75,
ios: 14.0,
opera: 62,
opera_mobile: 53,
safari: 14.0,
samsung: 11.0,
},
"intl-other": {
// Not specified (i.e. always try polyfill) since compatibility depends on supported locales
},
proxy: { proxy: {
android: 49, android: 49,
chrome: 49, chrome: 49,
@@ -98,67 +24,17 @@ const PolyfillSupport = {
safari: 10.0, safari: 10.0,
samsung: 5.0, samsung: 5.0,
}, },
"resize-observer": {
android: 64,
chrome: 64,
edge: 79,
firefox: 69,
ios: 13.4,
opera: 51,
opera_mobile: 47,
safari: 13.1,
samsung: 9.0,
},
}; };
// Map of global variables and/or instance and static properties to the // Map of global variables and/or instance and static properties to the
// corresponding polyfill key and actual module to import // corresponding polyfill key and actual module to import
const polyfillMap = { const polyfillMap = {
global: { global: {
fetch: { key: "fetch", module: "unfetch/polyfill" },
Proxy: { key: "proxy", module: "proxy-polyfill" }, Proxy: { key: "proxy", module: "proxy-polyfill" },
ResizeObserver: { fetch: { key: "fetch", module: "unfetch/polyfill" },
key: "resize-observer",
module: join(POLYFILL_DIR, "resize-observer.ts"),
},
},
instance: {
attachInternals: {
key: "element-internals",
module: "element-internals-polyfill",
},
...Object.fromEntries(
["append", "getAttributeNames", "toggleAttribute"].map((prop) => {
const key = `element-${prop.toLowerCase()}`;
return [prop, { key, module: join(POLYFILL_DIR, `${key}.ts`) }];
})
),
},
static: {
Intl: {
getCanonicalLocales: {
key: "intl-getcanonicallocales",
module: join(POLYFILL_DIR, "intl-polyfill.ts"),
},
Locale: {
key: "intl-locale",
module: join(POLYFILL_DIR, "intl-polyfill.ts"),
},
...Object.fromEntries(
[
"DateTimeFormat",
"DisplayNames",
"ListFormat",
"NumberFormat",
"PluralRules",
"RelativeTimeFormat",
].map((obj) => [
obj,
{ key: "intl-other", module: join(POLYFILL_DIR, "intl-polyfill.ts") },
])
),
},
}, },
instance: {},
static: {},
}; };
// Create plugin using the same factory as for CoreJS // Create plugin using the same factory as for CoreJS
@@ -166,16 +42,14 @@ export default defineProvider(
({ createMetaResolver, debug, shouldInjectPolyfill }) => { ({ createMetaResolver, debug, shouldInjectPolyfill }) => {
const resolvePolyfill = createMetaResolver(polyfillMap); const resolvePolyfill = createMetaResolver(polyfillMap);
return { return {
name: "custom-polyfill", name: "HA Custom",
polyfills: PolyfillSupport, polyfills: PolyfillSupport,
usageGlobal(meta, utils) { usageGlobal(meta, utils) {
const polyfill = resolvePolyfill(meta); const polyfill = resolvePolyfill(meta);
if (polyfill && shouldInjectPolyfill(polyfill.desc.key)) { if (polyfill && shouldInjectPolyfill(polyfill.desc.key)) {
debug(polyfill.desc.key); debug(polyfill.desc.key);
utils.injectGlobalImport(polyfill.desc.module); utils.injectGlobalImport(polyfill.desc.module);
return true;
} }
return false;
}, },
}; };
} }

View File

@@ -3,8 +3,6 @@ const env = require("./env.cjs");
const paths = require("./paths.cjs"); const paths = require("./paths.cjs");
const { dependencies } = require("../package.json"); const { dependencies } = require("../package.json");
const BABEL_PLUGINS = path.join(__dirname, "babel-plugins");
// GitHub base URL to use for production source maps // 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 // Nightly builds use the commit SHA, otherwise assumes there is a tag that matches the version
module.exports.sourceMapURL = () => { module.exports.sourceMapURL = () => {
@@ -47,7 +45,7 @@ module.exports.emptyPackages = ({ latestBuild, isHassioBuild }) =>
module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({ module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
__DEV__: !isProdBuild, __DEV__: !isProdBuild,
__BUILD__: JSON.stringify(latestBuild ? "modern" : "legacy"), __BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
__VERSION__: JSON.stringify(env.version()), __VERSION__: JSON.stringify(env.version()),
__DEMO__: false, __DEMO__: false,
__SUPERVISOR__: false, __SUPERVISOR__: false,
@@ -79,12 +77,7 @@ module.exports.terserOptions = ({ latestBuild, isTestBuild }) => ({
sourceMap: !isTestBuild, sourceMap: !isTestBuild,
}); });
module.exports.babelOptions = ({ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
latestBuild,
isProdBuild,
isTestBuild,
sw,
}) => ({
babelrc: false, babelrc: false,
compact: false, compact: false,
assumptions: { assumptions: {
@@ -92,13 +85,13 @@ module.exports.babelOptions = ({
setPublicClassFields: true, setPublicClassFields: true,
setSpreadProperties: true, setSpreadProperties: true,
}, },
browserslistEnv: latestBuild ? "modern" : `legacy${sw ? "-sw" : ""}`, browserslistEnv: latestBuild ? "modern" : "legacy",
presets: [ presets: [
[ [
"@babel/preset-env", "@babel/preset-env",
{ {
useBuiltIns: "usage", useBuiltIns: latestBuild ? false : "usage",
corejs: dependencies["core-js"], corejs: latestBuild ? false : dependencies["core-js"],
bugfixes: true, bugfixes: true,
shippedProposals: true, shippedProposals: true,
}, },
@@ -107,12 +100,22 @@ module.exports.babelOptions = ({
], ],
plugins: [ plugins: [
[ [
path.join(BABEL_PLUGINS, "inline-constants-plugin.cjs"), path.resolve(
paths.polymer_dir,
"build-scripts/babel-plugins/inline-constants-plugin.cjs"
),
{ {
modules: ["@mdi/js"], modules: ["@mdi/js"],
ignoreModuleNotFound: true, ignoreModuleNotFound: true,
}, },
], ],
[
path.resolve(
paths.polymer_dir,
"build-scripts/babel-plugins/custom-polyfill-plugin.js"
),
{ method: "usage-global" },
],
// Minify template literals for production // Minify template literals for production
isProdBuild && [ isProdBuild && [
"template-html-minifier", "template-html-minifier",
@@ -140,14 +143,8 @@ module.exports.babelOptions = ({
"@babel/plugin-transform-runtime", "@babel/plugin-transform-runtime",
{ version: dependencies["@babel/runtime"] }, { version: dependencies["@babel/runtime"] },
], ],
// Transpile decorators (still in TC39 process) // Support some proposals still in TC39 process
// Modern browsers support class fields and private methods, but transform is required with the older decorator version dictated by Lit ["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }],
[
"@babel/plugin-proposal-decorators",
{ version: "2018-09", decoratorsBeforeExport: true },
],
"@babel/plugin-transform-class-properties",
"@babel/plugin-transform-private-methods",
].filter(Boolean), ].filter(Boolean),
exclude: [ exclude: [
// \\ for Windows, / for Mac OS and Linux // \\ for Windows, / for Mac OS and Linux
@@ -156,27 +153,6 @@ module.exports.babelOptions = ({
], ],
sourceMaps: !isTestBuild, sourceMaps: !isTestBuild,
overrides: [ overrides: [
{
// Add plugin to inject various polyfills, excluding the polyfills
// themselves to prevent self-injection.
plugins: [
[
path.join(BABEL_PLUGINS, "custom-polyfill-plugin.js"),
{ method: "usage-global" },
],
],
exclude: [
path.join(paths.polymer_dir, "src/resources/polyfills"),
...[
"@formatjs/(?:ecma402-abstract|intl-\\w+)",
"@lit-labs/virtualizer/polyfills",
"@webcomponents/scoped-custom-element-registry",
"element-internals-polyfill",
"proxy-polyfill",
"unfetch",
].map((p) => new RegExp(`/node_modules/${p}/`)),
],
},
{ {
// Use unambiguous for dependencies so that require() is correctly injected into CommonJS files // 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 // Exclusions are needed in some cases where ES modules have no static imports or exports, such as polyfills
@@ -226,13 +202,7 @@ module.exports.config = {
return { return {
name: "frontend" + nameSuffix(latestBuild), name: "frontend" + nameSuffix(latestBuild),
entry: { entry: {
"service-worker": service_worker: "./src/entrypoints/service_worker.ts",
!env.useRollup() && !latestBuild
? {
import: "./src/entrypoints/service-worker.ts",
layer: "sw",
}
: "./src/entrypoints/service-worker.ts",
app: "./src/entrypoints/app.ts", app: "./src/entrypoints/app.ts",
authorize: "./src/entrypoints/authorize.ts", authorize: "./src/entrypoints/authorize.ts",
onboarding: "./src/entrypoints/onboarding.ts", onboarding: "./src/entrypoints/onboarding.ts",

View File

@@ -32,7 +32,4 @@ module.exports = {
} }
return version[1]; return version[1];
}, },
isDevContainer() {
return process.env.DEV_CONTAINER === "1";
},
}; };

View File

@@ -1,68 +1,19 @@
// Tasks to compress // Tasks to compress
import { constants } from "node:zlib";
import gulp from "gulp"; import gulp from "gulp";
import brotli from "gulp-brotli";
import zopfli from "gulp-zopfli-green"; import zopfli from "gulp-zopfli-green";
import paths from "../paths.cjs"; import paths from "../paths.cjs";
const filesGlob = "*.{js,json,css,svg,xml}";
const brotliOptions = {
skipLarger: true,
params: {
[constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MAX_QUALITY,
},
};
const zopfliOptions = { threshold: 150 }; const zopfliOptions = { threshold: 150 };
const compressDistBrotli = (rootDir, modernDir, compressServiceWorker = true) => const compressDist = (rootDir) =>
gulp gulp
.src( .src([
[ `${rootDir}/**/*.{js,json,css,svg,xml}`,
`${modernDir}/**/${filesGlob}`, `${rootDir}/{authorize,onboarding}.html`,
compressServiceWorker ? `${rootDir}/sw-modern.js` : undefined, ])
].filter(Boolean),
{
base: rootDir,
}
)
.pipe(brotli(brotliOptions))
.pipe(gulp.dest(rootDir));
const compressDistZopfli = (rootDir, modernDir, compressModern = false) =>
gulp
.src(
[
`${rootDir}/**/${filesGlob}`,
compressModern ? undefined : `!${modernDir}/**/${filesGlob}`,
`!${rootDir}/{sw-modern,service_worker}.js`,
`${rootDir}/{authorize,onboarding}.html`,
].filter(Boolean),
{ base: rootDir }
)
.pipe(zopfli(zopfliOptions)) .pipe(zopfli(zopfliOptions))
.pipe(gulp.dest(rootDir)); .pipe(gulp.dest(rootDir));
const compressAppBrotli = () => gulp.task("compress-app", () => compressDist(paths.app_output_root));
compressDistBrotli(paths.app_output_root, paths.app_output_latest); gulp.task("compress-hassio", () => compressDist(paths.hassio_output_root));
const compressHassioBrotli = () =>
compressDistBrotli(
paths.hassio_output_root,
paths.hassio_output_latest,
false
);
const compressAppZopfli = () =>
compressDistZopfli(paths.app_output_root, paths.app_output_latest);
const compressHassioZopfli = () =>
compressDistZopfli(
paths.hassio_output_root,
paths.hassio_output_latest,
true
);
gulp.task("compress-app", gulp.parallel(compressAppBrotli, compressAppZopfli));
gulp.task(
"compress-hassio",
gulp.parallel(compressHassioBrotli, compressHassioZopfli)
);

View File

@@ -1,76 +1,28 @@
// Tasks to generate entry HTML // Tasks to generate entry HTML
import {
applyVersionsToRegexes,
compileRegex,
getPreUserAgentRegexes,
} from "browserslist-useragent-regexp";
import fs from "fs-extra"; import fs from "fs-extra";
import gulp from "gulp"; import gulp from "gulp";
import { minify } from "html-minifier-terser"; import { minify } from "html-minifier-terser";
import template from "lodash.template"; import template from "lodash.template";
import { dirname, extname, resolve } from "node:path"; import path from "path";
import { htmlMinifierOptions, terserOptions } from "../bundle.cjs"; import { htmlMinifierOptions, terserOptions } from "../bundle.cjs";
import env from "../env.cjs"; import env from "../env.cjs";
import paths from "../paths.cjs"; import paths from "../paths.cjs";
// macOS companion app has no way to obtain the Safari version used by WKWebView,
// and it is not in the default user agent string. So we add an additional regex
// to serve modern based on a minimum macOS version. We take the minimum Safari
// major version from browserslist and manually map that to a supported macOS
// version. Note this assumes the user has kept Safari updated.
const HA_MACOS_REGEX =
/Home Assistant\/[\d.]+ \(.+; macOS (\d+)\.(\d+)(?:\.(\d+))?\)/;
const SAFARI_TO_MACOS = {
15: [10, 15, 0],
16: [11, 0, 0],
17: [12, 0, 0],
18: [13, 0, 0],
};
const getCommonTemplateVars = () => {
const browserRegexes = getPreUserAgentRegexes({
env: "modern",
allowHigherVersions: true,
mobileToDesktop: true,
throwOnMissing: true,
});
const minSafariVersion = browserRegexes.find(
(regex) => regex.family === "safari"
)?.matchedVersions[0][0];
const minMacOSVersion = SAFARI_TO_MACOS[minSafariVersion];
if (!minMacOSVersion) {
throw Error(
`Could not find minimum MacOS version for Safari ${minSafariVersion}.`
);
}
const haMacOSRegex = applyVersionsToRegexes(
[
{
family: "ha_macos",
regex: HA_MACOS_REGEX,
matchedVersions: [minMacOSVersion],
requestVersions: [minMacOSVersion],
},
],
{ ignorePatch: true, allowHigherVersions: true }
);
return {
useRollup: env.useRollup(),
useWDS: env.useWDS(),
modernRegex: compileRegex(browserRegexes.concat(haMacOSRegex)).toString(),
};
};
const renderTemplate = (templateFile, data = {}) => { const renderTemplate = (templateFile, data = {}) => {
const compiled = template( const compiled = template(
fs.readFileSync(templateFile, { encoding: "utf-8" }) fs.readFileSync(templateFile, { encoding: "utf-8" })
); );
return compiled({ return compiled({
...data, ...data,
useRollup: env.useRollup(),
useWDS: env.useWDS(),
// Resolve any child/nested templates relative to the parent and pass the same data // Resolve any child/nested templates relative to the parent and pass the same data
renderTemplate: (childTemplate) => renderTemplate: (childTemplate) =>
renderTemplate(resolve(dirname(templateFile), childTemplate), data), renderTemplate(
path.resolve(path.dirname(templateFile), childTemplate),
data
),
}); });
}; };
@@ -104,12 +56,10 @@ const genPagesDevTask =
publicRoot = "" publicRoot = ""
) => ) =>
async () => { async () => {
const commonVars = getCommonTemplateVars();
for (const [page, entries] of Object.entries(pageEntries)) { for (const [page, entries] of Object.entries(pageEntries)) {
const content = renderTemplate( const content = renderTemplate(
resolve(inputRoot, inputSub, `${page}.template`), path.resolve(inputRoot, inputSub, `${page}.template`),
{ {
...commonVars,
latestEntryJS: entries.map((entry) => latestEntryJS: entries.map((entry) =>
useWDS useWDS
? `http://localhost:8000/src/entrypoints/${entry}.ts` ? `http://localhost:8000/src/entrypoints/${entry}.ts`
@@ -124,7 +74,7 @@ const genPagesDevTask =
es5CustomPanelJS: `${publicRoot}/frontend_es5/custom-panel.js`, es5CustomPanelJS: `${publicRoot}/frontend_es5/custom-panel.js`,
} }
); );
fs.outputFileSync(resolve(outputRoot, page), content); fs.outputFileSync(path.resolve(outputRoot, page), content);
} }
}; };
@@ -141,18 +91,16 @@ const genPagesProdTask =
) => ) =>
async () => { async () => {
const latestManifest = fs.readJsonSync( const latestManifest = fs.readJsonSync(
resolve(outputLatest, "manifest.json") path.resolve(outputLatest, "manifest.json")
); );
const es5Manifest = outputES5 const es5Manifest = outputES5
? fs.readJsonSync(resolve(outputES5, "manifest.json")) ? fs.readJsonSync(path.resolve(outputES5, "manifest.json"))
: {}; : {};
const commonVars = getCommonTemplateVars();
const minifiedHTML = []; const minifiedHTML = [];
for (const [page, entries] of Object.entries(pageEntries)) { for (const [page, entries] of Object.entries(pageEntries)) {
const content = renderTemplate( const content = renderTemplate(
resolve(inputRoot, inputSub, `${page}.template`), path.resolve(inputRoot, inputSub, `${page}.template`),
{ {
...commonVars,
latestEntryJS: entries.map((entry) => latestManifest[`${entry}.js`]), latestEntryJS: entries.map((entry) => latestManifest[`${entry}.js`]),
es5EntryJS: entries.map((entry) => es5Manifest[`${entry}.js`]), es5EntryJS: entries.map((entry) => es5Manifest[`${entry}.js`]),
latestCustomPanelJS: latestManifest["custom-panel.js"], latestCustomPanelJS: latestManifest["custom-panel.js"],
@@ -160,8 +108,8 @@ const genPagesProdTask =
} }
); );
minifiedHTML.push( minifiedHTML.push(
minifyHtml(content, extname(page)).then((minified) => minifyHtml(content, path.extname(page)).then((minified) =>
fs.outputFileSync(resolve(outputRoot, page), minified) fs.outputFileSync(path.resolve(outputRoot, page), minified)
) )
); );
} }

View File

@@ -9,7 +9,7 @@ import gulp from "gulp";
import jszip from "jszip"; import jszip from "jszip";
import path from "path"; import path from "path";
import process from "process"; import process from "process";
import { extract } from "tar"; import tar from "tar";
const MAX_AGE = 24; // hours const MAX_AGE = 24; // hours
const OWNER = "home-assistant"; const OWNER = "home-assistant";
@@ -156,7 +156,7 @@ gulp.task("fetch-nightly-translations", async function () {
console.log("Unpacking downloaded translations..."); console.log("Unpacking downloaded translations...");
const zip = await jszip.loadAsync(downloadResponse.data); const zip = await jszip.loadAsync(downloadResponse.data);
await deleteCurrent; await deleteCurrent;
const extractStream = zip.file(/.*/)[0].nodeStream().pipe(extract()); const extractStream = zip.file(/.*/)[0].nodeStream().pipe(tar.extract());
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
extractStream.on("close", resolve).on("error", reject); extractStream.on("close", resolve).on("error", reject);
}); });

View File

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

View File

@@ -1,19 +1,20 @@
// Generate service workers // Generate service worker.
// Based on manifest, create a file with the content as service_worker.js
import { deleteAsync } from "del"; import fs from "fs-extra";
import gulp from "gulp"; import gulp from "gulp";
import { mkdir, readFile, symlink, writeFile } from "node:fs/promises"; import path from "path";
import { basename, join, relative } from "node:path"; import sourceMapUrl from "source-map-url";
import { injectManifest } from "workbox-build"; import workboxBuild from "workbox-build";
import paths from "../paths.cjs"; import paths from "../paths.cjs";
const SW_MAP = { const swDest = path.resolve(paths.app_output_root, "service_worker.js");
[paths.app_output_latest]: "modern",
[paths.app_output_es5]: "legacy",
};
const SW_DEV = const writeSW = (content) => fs.outputFileSync(swDest, content.trim() + "\n");
`
gulp.task("gen-service-worker-app-dev", (done) => {
writeSW(
`
console.debug('Service worker disabled in development'); console.debug('Service worker disabled in development');
self.addEventListener('install', (event) => { self.addEventListener('install', (event) => {
@@ -21,67 +22,72 @@ self.addEventListener('install', (event) => {
// removing any prod service worker the dev might have running // removing any prod service worker the dev might have running
self.skipWaiting(); self.skipWaiting();
}); });
`.trim() + "\n"; `
gulp.task("gen-service-worker-app-dev", async () => {
await mkdir(paths.app_output_root, { recursive: true });
await Promise.all(
Object.values(SW_MAP).map((build) =>
writeFile(join(paths.app_output_root, `sw-${build}.js`), SW_DEV, {
encoding: "utf-8",
})
)
); );
done();
}); });
gulp.task("gen-service-worker-app-prod", () => gulp.task("gen-service-worker-app-prod", async () => {
Promise.all( // Read bundled source file
Object.entries(SW_MAP).map(async ([outPath, build]) => { const bundleManifestLatest = fs.readJsonSync(
const manifest = JSON.parse( path.resolve(paths.app_output_latest, "manifest.json")
await readFile(join(outPath, "manifest.json"), "utf-8") );
); let serviceWorkerContent = fs.readFileSync(
const swSrc = join(paths.app_output_root, manifest["service-worker.js"]); paths.app_output_root + bundleManifestLatest["service_worker.js"],
const swDest = join(paths.app_output_root, `sw-${build}.js`); "utf-8"
const buildDir = relative(paths.app_output_root, outPath); );
const { warnings } = await injectManifest({
swSrc, // Delete old file from frontend_latest so manifest won't pick it up
swDest, fs.removeSync(
injectionPoint: "__WB_MANIFEST__", paths.app_output_root + bundleManifestLatest["service_worker.js"]
// Files that mach this pattern will be considered unique and skip revision check );
// ignore JS files + translation files fs.removeSync(
dontCacheBustURLsMatching: new RegExp( paths.app_output_root + bundleManifestLatest["service_worker.js.map"]
`(?:${buildDir}/.+|static/translations/.+)` );
),
globDirectory: paths.app_output_root, // Remove ES5
globPatterns: [ const bundleManifestES5 = fs.readJsonSync(
`${buildDir}/*.js`, path.resolve(paths.app_output_es5, "manifest.json")
// Cache all English translations because we catch them as fallback );
// Using pattern to match hash instead of * to avoid caching en-GB fs.removeSync(paths.app_output_root + bundleManifestES5["service_worker.js"]);
// 'v' added as valid hash letter because in dev we hash with 'dev' fs.removeSync(
"static/translations/**/en-+([a-fv0-9]).json", paths.app_output_root + bundleManifestES5["service_worker.js.map"]
// Icon shown on splash screen );
"static/icons/favicon-192x192.png",
"static/icons/favicon.ico", const workboxManifest = await workboxBuild.getManifest({
// Common fonts // Files that mach this pattern will be considered unique and skip revision check
"static/fonts/roboto/Roboto-Light.woff2", // ignore JS files + translation files
"static/fonts/roboto/Roboto-Medium.woff2", dontCacheBustURLsMatching: /(frontend_latest\/.+|static\/translations\/.+)/,
"static/fonts/roboto/Roboto-Regular.woff2",
"static/fonts/roboto/Roboto-Bold.woff2", globDirectory: paths.app_output_root,
], globPatterns: [
globIgnores: [`${buildDir}/service-worker*`], "frontend_latest/*.js",
}); // Cache all English translations because we catch them as fallback
if (warnings.length > 0) { // Using pattern to match hash instead of * to avoid caching en-GB
console.warn( // 'v' added as valid hash letter because in dev we hash with 'dev'
`Problems while injecting ${build} service worker:\n`, "static/translations/**/en-+([a-fv0-9]).json",
warnings.join("\n") // Icon shown on splash screen
); "static/icons/favicon-192x192.png",
} "static/icons/favicon.ico",
await deleteAsync(`${swSrc}?(.map)`); // Common fonts
// Needed to install new SW from a cached HTML "static/fonts/roboto/Roboto-Light.woff2",
if (build === "modern") { "static/fonts/roboto/Roboto-Medium.woff2",
const swOld = join(paths.app_output_root, "service_worker.js"); "static/fonts/roboto/Roboto-Regular.woff2",
await symlink(basename(swDest), swOld); "static/fonts/roboto/Roboto-Bold.woff2",
} ],
}) });
)
); for (const warning of workboxManifest.warnings) {
console.warn(warning);
}
// remove source map and add WB manifest
serviceWorkerContent = sourceMapUrl.removeFrom(serviceWorkerContent);
serviceWorkerContent = serviceWorkerContent.replace(
"WB_MANIFEST",
JSON.stringify(workboxManifest.manifestEntries)
);
// Write new file to root
fs.writeFileSync(swDest, serviceWorkerContent);
});

View File

@@ -1,112 +1,92 @@
/* eslint-disable max-classes-per-file */ import { createHash } from "crypto";
import { deleteSync } from "del";
import { deleteAsync } from "del"; import { mkdirSync, readdirSync, readFileSync, renameSync } from "fs";
import { glob } from "glob"; import { writeFile } from "node:fs/promises";
import gulp from "gulp"; import gulp from "gulp";
import flatmap from "gulp-flatmap";
import transform from "gulp-json-transform";
import merge from "gulp-merge-json";
import rename from "gulp-rename"; import rename from "gulp-rename";
import merge from "lodash.merge"; import path from "path";
import { createHash } from "node:crypto"; import vinylBuffer from "vinyl-buffer";
import { mkdir, readFile } from "node:fs/promises"; import source from "vinyl-source-stream";
import { basename, join } from "node:path";
import { PassThrough, Transform } from "node:stream";
import { finished } from "node:stream/promises";
import env from "../env.cjs"; import env from "../env.cjs";
import paths from "../paths.cjs"; import paths from "../paths.cjs";
import { mapFiles } from "../util.cjs";
import "./fetch-nightly-translations.js"; import "./fetch-nightly-translations.js";
const inFrontendDir = "translations/frontend"; const inFrontendDir = "translations/frontend";
const inBackendDir = "translations/backend"; const inBackendDir = "translations/backend";
const workDir = "build/translations"; const workDir = "build/translations";
const outDir = join(workDir, "output"); const fullDir = workDir + "/full";
const EN_SRC = join(paths.translations_src, "en.json"); const coreDir = workDir + "/core";
const TEST_LOCALE = "en-x-test"; const outDir = workDir + "/output";
let mergeBackend = false; let mergeBackend = false;
gulp.task( gulp.task(
"translations-enable-merge-backend", "translations-enable-merge-backend",
gulp.parallel(async () => { gulp.parallel((done) => {
mergeBackend = true; mergeBackend = true;
done();
}, "allow-setup-fetch-nightly-translations") }, "allow-setup-fetch-nightly-translations")
); );
// Transform stream to apply a function on Vinyl JSON files (buffer mode only). // Panel translations which should be split from the core translations.
// The provided function can either return a new object, or an array of const TRANSLATION_FRAGMENTS = Object.keys(
// [object, subdirectory] pairs for fragmentizing the JSON. JSON.parse(
class CustomJSON extends Transform { readFileSync(
constructor(func, reviver = null) { path.resolve(paths.polymer_dir, "src/translations/en.json"),
super({ objectMode: true }); "utf-8"
this._func = func; )
this._reviver = reviver; ).ui.panel
} );
async _transform(file, _, callback) { function recursiveFlatten(prefix, data) {
try { let output = {};
let obj = JSON.parse(file.contents.toString(), this._reviver); Object.keys(data).forEach((key) => {
if (this._func) obj = this._func(obj, file.path); if (typeof data[key] === "object") {
for (const [outObj, dir] of Array.isArray(obj) ? obj : [[obj, ""]]) { output = {
const outFile = file.clone({ contents: false }); ...output,
outFile.contents = Buffer.from(JSON.stringify(outObj)); ...recursiveFlatten(prefix + key + ".", data[key]),
outFile.dirname += `/${dir}`; };
this.push(outFile);
}
callback(null);
} catch (err) {
callback(err);
}
}
}
// Transform stream to merge Vinyl JSON files (buffer mode only).
class MergeJSON extends Transform {
_objects = [];
constructor(stem, startObj = {}, reviver = null) {
super({ objectMode: true, allowHalfOpen: false });
this._stem = stem;
this._startObj = structuredClone(startObj);
this._reviver = reviver;
}
async _transform(file, _, callback) {
try {
this._objects.push(JSON.parse(file.contents.toString(), this._reviver));
if (!this._outFile) this._outFile = file.clone({ contents: false });
callback(null);
} catch (err) {
callback(err);
}
}
async _flush(callback) {
try {
const mergedObj = merge(this._startObj, ...this._objects);
this._outFile.contents = Buffer.from(JSON.stringify(mergedObj));
this._outFile.stem = this._stem;
callback(null, this._outFile);
} catch (err) {
callback(err);
}
}
}
// Utility to flatten object keys to single level using separator
const flatten = (data, prefix = "", sep = ".") => {
const output = {};
for (const [key, value] of Object.entries(data)) {
if (typeof value === "object") {
Object.assign(output, flatten(value, prefix + key + sep, sep));
} else { } else {
output[prefix + key] = value; output[prefix + key] = data[key];
} }
} });
return output; return output;
}; }
// Filter functions that can be passed directly to JSON.parse() function flatten(data) {
const emptyReviver = (_key, value) => value || undefined; return recursiveFlatten("", data);
const testReviver = (_key, value) => }
value && typeof value === "string" ? "TRANSLATED" : value;
function emptyFilter(data) {
const newData = {};
Object.keys(data).forEach((key) => {
if (data[key]) {
if (typeof data[key] === "object") {
newData[key] = emptyFilter(data[key]);
} else {
newData[key] = data[key];
}
}
});
return newData;
}
function recursiveEmpty(data) {
const newData = {};
Object.keys(data).forEach((key) => {
if (data[key]) {
if (typeof data[key] === "object") {
newData[key] = recursiveEmpty(data[key]);
} else {
newData[key] = "TRANSLATED";
}
}
});
return newData;
}
/** /**
* Replace Lokalise key placeholders with their actual values. * Replace Lokalise key placeholders with their actual values.
@@ -115,44 +95,60 @@ const testReviver = (_key, value) =>
* be included in src/translations/en.json, but still be usable while * be included in src/translations/en.json, but still be usable while
* developing locally. * developing locally.
* *
* @link https://docs.lokalise.com/en/articles/1400528-key-referencing * @link https://docs.lokalise.co/article/KO5SZWLLsy-key-referencing
*/ */
const KEY_REFERENCE = /\[%key:([^%]+)%\]/; const re_key_reference = /\[%key:([^%]+)%\]/;
const lokaliseTransform = (data, path, original = data) => { function lokaliseTransform(data, original, file) {
const output = {}; const output = {};
for (const [key, value] of Object.entries(data)) { Object.entries(data).forEach(([key, value]) => {
if (typeof value === "object") { if (value instanceof Object) {
output[key] = lokaliseTransform(value, path, original); output[key] = lokaliseTransform(value, original, file);
} else { } else {
output[key] = value.replace(KEY_REFERENCE, (_match, lokalise_key) => { output[key] = value.replace(re_key_reference, (_match, lokalise_key) => {
const replace = lokalise_key.split("::").reduce((tr, k) => { const replace = lokalise_key.split("::").reduce((tr, k) => {
if (!tr) { if (!tr) {
throw Error(`Invalid key placeholder ${lokalise_key} in ${path}`); throw Error(
`Invalid key placeholder ${lokalise_key} in ${file.path}`
);
} }
return tr[k]; return tr[k];
}, original); }, original);
if (typeof replace !== "string") { if (typeof replace !== "string") {
throw Error(`Invalid key placeholder ${lokalise_key} in ${path}`); throw Error(
`Invalid key placeholder ${lokalise_key} in ${file.path}`
);
} }
return replace; return replace;
}); });
} }
} });
return output; return output;
}; }
gulp.task("clean-translations", () => deleteAsync([workDir])); gulp.task("clean-translations", async () => deleteSync([workDir]));
const makeWorkDir = () => mkdir(workDir, { recursive: true }); gulp.task("ensure-translations-build-dir", async () => {
mkdirSync(workDir, { recursive: true });
});
const createTestTranslation = () => gulp.task("create-test-metadata", () =>
env.isProdBuild()
? Promise.resolve()
: writeFile(
workDir + "/testMetadata.json",
JSON.stringify({ test: { nativeName: "Test" } })
)
);
gulp.task("create-test-translation", () =>
env.isProdBuild() env.isProdBuild()
? Promise.resolve() ? Promise.resolve()
: gulp : gulp
.src(EN_SRC) .src(path.join(paths.translations_src, "en.json"))
.pipe(new CustomJSON(null, testReviver)) .pipe(transform((data, _file) => recursiveEmpty(data)))
.pipe(rename(`${TEST_LOCALE}.json`)) .pipe(rename("test.json"))
.pipe(gulp.dest(workDir)); .pipe(gulp.dest(workDir))
);
/** /**
* This task will build a master translation file, to be used as the base for * This task will build a master translation file, to be used as the base for
@@ -163,164 +159,279 @@ const createTestTranslation = () =>
* project is buildable immediately after merging new translation keys, since * project is buildable immediately after merging new translation keys, since
* the Lokalise update to translations/en.json will not happen immediately. * the Lokalise update to translations/en.json will not happen immediately.
*/ */
const createMasterTranslation = () => gulp.task("build-master-translation", () => {
gulp const src = [path.join(paths.translations_src, "en.json")];
.src([EN_SRC, ...(mergeBackend ? [`${inBackendDir}/en.json`] : [])])
.pipe(new CustomJSON(lokaliseTransform))
.pipe(new MergeJSON("en"))
.pipe(gulp.dest(workDir));
const FRAGMENTS = ["base"]; if (mergeBackend) {
src.push(path.join(inBackendDir, "en.json"));
const toggleSupervisorFragment = async () => {
FRAGMENTS[0] = "supervisor";
};
const panelFragment = (fragment) =>
fragment !== "base" && fragment !== "supervisor";
const HASHES = new Map();
const createTranslations = async () => {
// Parse and store the master to avoid repeating this for each locale, then
// add the panel fragments when processing the app.
const enMaster = JSON.parse(await readFile(`${workDir}/en.json`, "utf-8"));
if (FRAGMENTS[0] === "base") {
FRAGMENTS.push(...Object.keys(enMaster.ui.panel));
} }
// The downstream pipeline is setup first. It hashes the merged data for return gulp
// each locale, then fragmentizes and flattens the data for final output. .src(src)
const translationFiles = await glob([ .pipe(transform((data, file) => lokaliseTransform(data, data, file)))
`${inFrontendDir}/!(en).json`,
...(env.isProdBuild() ? [] : [`${workDir}/${TEST_LOCALE}.json`]),
]);
const hashStream = new Transform({
objectMode: true,
transform: async (file, _, callback) => {
const hash = env.isProdBuild()
? createHash("md5").update(file.contents).digest("hex")
: "dev";
HASHES.set(file.stem, hash);
file.stem += `-${hash}`;
callback(null, file);
},
}).setMaxListeners(translationFiles.length + 1);
const fragmentsStream = hashStream
.pipe( .pipe(
new CustomJSON((data) => merge({
FRAGMENTS.map((fragment) => { fileName: "en.json",
switch (fragment) {
case "base":
// Remove the panels and supervisor to create the base translations
return [
flatten({
...data,
ui: { ...data.ui, panel: undefined },
supervisor: undefined,
}),
"",
];
case "supervisor":
// Supervisor key is at the top level
return [flatten(data.supervisor), ""];
default:
// Create a fragment with only the given panel
return [
flatten(data.ui.panel[fragment], `ui.panel.${fragment}.`),
fragment,
];
}
})
)
)
.pipe(gulp.dest(outDir));
// Send the English master downstream first, then for each other locale
// generate merged JSON data to continue piping. It begins with the master
// translation as a failsafe for untranslated strings, and merges all parent
// tags into one file for each specific subtag
//
// TODO: This is a naive interpretation of BCP47 that should be improved.
// Will be OK for now as long as we don't have anything more complicated
// than a base translation + region.
const masterStream = gulp
.src(`${workDir}/en.json`)
.pipe(new PassThrough({ objectMode: true }));
masterStream.pipe(hashStream, { end: false });
const mergesFinished = [finished(masterStream)];
for (const translationFile of translationFiles) {
const locale = basename(translationFile, ".json");
const subtags = locale.split("-");
const mergeFiles = [];
for (let i = 1; i <= subtags.length; i++) {
const lang = subtags.slice(0, i).join("-");
if (lang === TEST_LOCALE) {
mergeFiles.push(`${workDir}/${TEST_LOCALE}.json`);
} else if (lang !== "en") {
mergeFiles.push(`${inFrontendDir}/${lang}.json`);
if (mergeBackend) {
mergeFiles.push(`${inBackendDir}/${lang}.json`);
}
}
}
const mergeStream = gulp
.src(mergeFiles, { allowEmpty: true })
.pipe(new MergeJSON(locale, enMaster, emptyReviver));
mergesFinished.push(finished(mergeStream));
mergeStream.pipe(hashStream, { end: false });
}
// Wait for all merges to finish, then it's safe to end writing to the
// downstream pipeline and wait for all fragments to finish writing.
await Promise.all(mergesFinished);
hashStream.end();
await finished(fragmentsStream);
};
const writeTranslationMetaData = () =>
gulp
.src([`${paths.translations_src}/translationMetadata.json`])
.pipe(
new CustomJSON((meta) => {
// Add the test translation in development.
if (!env.isProdBuild()) {
meta[TEST_LOCALE] = { nativeName: "Translation Test" };
}
// Filter out locales without a native name, and add the hashes.
for (const locale of Object.keys(meta)) {
if (!meta[locale].nativeName) {
meta[locale] = undefined;
console.warn(
`Skipping locale ${locale} because native name is not translated.`
);
} else {
meta[locale].hash = HASHES.get(locale);
}
}
return {
fragments: FRAGMENTS.filter(panelFragment),
translations: meta,
};
}) })
) )
.pipe(gulp.dest(workDir)); .pipe(gulp.dest(fullDir));
});
gulp.task("build-merged-translations", () =>
gulp
.src([
inFrontendDir + "/*.json",
"!" + inFrontendDir + "/en.json",
...(env.isProdBuild() ? [] : [workDir + "/test.json"]),
])
.pipe(transform((data, file) => lokaliseTransform(data, data, file)))
.pipe(
flatmap((stream, file) => {
// For each language generate a merged json file. It begins with the master
// translation as a failsafe for untranslated strings, and merges all parent
// tags into one file for each specific subtag
//
// TODO: This is a naive interpretation of BCP47 that should be improved.
// Will be OK for now as long as we don't have anything more complicated
// than a base translation + region.
const tr = path.basename(file.history[0], ".json");
const subtags = tr.split("-");
const src = [fullDir + "/en.json"];
for (let i = 1; i <= subtags.length; i++) {
const lang = subtags.slice(0, i).join("-");
if (lang === "test") {
src.push(workDir + "/test.json");
} else if (lang !== "en") {
src.push(inFrontendDir + "/" + lang + ".json");
if (mergeBackend) {
src.push(inBackendDir + "/" + lang + ".json");
}
}
}
return gulp
.src(src, { allowEmpty: true })
.pipe(transform((data) => emptyFilter(data)))
.pipe(
merge({
fileName: tr + ".json",
})
)
.pipe(gulp.dest(fullDir));
})
)
);
let taskName;
const splitTasks = [];
TRANSLATION_FRAGMENTS.forEach((fragment) => {
taskName = "build-translation-fragment-" + fragment;
gulp.task(taskName, () =>
// Return only the translations for this fragment.
gulp
.src(fullDir + "/*.json")
.pipe(
transform((data) => ({
ui: {
panel: {
[fragment]: data.ui.panel[fragment],
},
},
}))
)
.pipe(gulp.dest(workDir + "/" + fragment))
);
splitTasks.push(taskName);
});
taskName = "build-translation-core";
gulp.task(taskName, () =>
// Remove the fragment translations from the core translation.
gulp
.src(fullDir + "/*.json")
.pipe(
transform((data, _file) => {
TRANSLATION_FRAGMENTS.forEach((fragment) => {
delete data.ui.panel[fragment];
});
delete data.supervisor;
return data;
})
)
.pipe(gulp.dest(coreDir))
);
splitTasks.push(taskName);
gulp.task("build-flattened-translations", () =>
// Flatten the split versions of our translations, and move them into outDir
gulp
.src(
TRANSLATION_FRAGMENTS.map(
(fragment) => workDir + "/" + fragment + "/*.json"
).concat(coreDir + "/*.json"),
{ base: workDir }
)
.pipe(
transform((data) =>
// Polymer.AppLocalizeBehavior requires flattened json
flatten(data)
)
)
.pipe(
rename((filePath) => {
if (filePath.dirname === "core") {
filePath.dirname = "";
}
// In dev we create the file with the fake hash in the filename
if (!env.isProdBuild()) {
filePath.basename += "-dev";
}
})
)
.pipe(gulp.dest(outDir))
);
const fingerprints = {};
gulp.task("build-translation-fingerprints", () => {
// Fingerprint full file of each language
const files = readdirSync(fullDir);
for (let i = 0; i < files.length; i++) {
fingerprints[files[i].split(".")[0]] = {
// In dev we create fake hashes
hash: env.isProdBuild()
? createHash("md5")
.update(readFileSync(path.join(fullDir, files[i]), "utf-8"))
.digest("hex")
: "dev",
};
}
// In dev we create the file with the fake hash in the filename
if (env.isProdBuild()) {
mapFiles(outDir, ".json", (filename) => {
const parsed = path.parse(filename);
// nl.json -> nl-<hash>.json
if (!(parsed.name in fingerprints)) {
throw new Error(`Unable to find hash for ${filename}`);
}
renameSync(
filename,
`${parsed.dir}/${parsed.name}-${fingerprints[parsed.name].hash}${
parsed.ext
}`
);
});
}
const stream = source("translationFingerprints.json");
stream.write(JSON.stringify(fingerprints));
process.nextTick(() => stream.end());
return stream.pipe(vinylBuffer()).pipe(gulp.dest(workDir));
});
gulp.task("build-translation-fragment-supervisor", () =>
gulp
.src(fullDir + "/*.json")
.pipe(transform((data) => data.supervisor))
.pipe(
rename((filePath) => {
// In dev we create the file with the fake hash in the filename
if (!env.isProdBuild()) {
filePath.basename += "-dev";
}
})
)
.pipe(gulp.dest(workDir + "/supervisor"))
);
gulp.task("build-translation-flatten-supervisor", () =>
gulp
.src(workDir + "/supervisor/*.json")
.pipe(
transform((data) =>
// Polymer.AppLocalizeBehavior requires flattened json
flatten(data)
)
)
.pipe(gulp.dest(outDir))
);
gulp.task("build-translation-write-metadata", () =>
gulp
.src([
path.join(paths.translations_src, "translationMetadata.json"),
...(env.isProdBuild() ? [] : [workDir + "/testMetadata.json"]),
workDir + "/translationFingerprints.json",
])
.pipe(merge({}))
.pipe(
transform((data) => {
const newData = {};
Object.entries(data).forEach(([key, value]) => {
// Filter out translations without native name.
if (value.nativeName) {
newData[key] = value;
} else {
console.warn(
`Skipping language ${key}. Native name was not translated.`
);
}
});
return newData;
})
)
.pipe(
transform((data) => ({
fragments: TRANSLATION_FRAGMENTS,
translations: data,
}))
)
.pipe(rename("translationMetadata.json"))
.pipe(gulp.dest(workDir))
);
gulp.task(
"create-translations",
gulp.series(
gulp.parallel("create-test-metadata", "create-test-translation"),
"build-master-translation",
"build-merged-translations",
gulp.parallel(...splitTasks),
"build-flattened-translations"
)
);
gulp.task( gulp.task(
"build-translations", "build-translations",
gulp.series( gulp.series(
gulp.parallel( gulp.parallel(
"fetch-nightly-translations", "fetch-nightly-translations",
gulp.series("clean-translations", makeWorkDir) gulp.series("clean-translations", "ensure-translations-build-dir")
), ),
createTestTranslation, "create-translations",
createMasterTranslation, "build-translation-fingerprints",
createTranslations, "build-translation-write-metadata"
writeTranslationMetaData
) )
); );
gulp.task( gulp.task(
"build-supervisor-translations", "build-supervisor-translations",
gulp.series(toggleSupervisorFragment, "build-translations") gulp.series(
gulp.parallel(
"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",
"build-translation-flatten-supervisor",
"build-translation-fingerprints",
"build-translation-write-metadata"
)
); );

View File

@@ -40,12 +40,8 @@ const runDevServer = async ({
compiler, compiler,
contentBase, contentBase,
port, port,
listenHost = undefined, listenHost = "localhost",
}) => { }) => {
if (listenHost === undefined) {
// For dev container, we need to listen on all hosts
listenHost = env.isDevContainer() ? "0.0.0.0" : "localhost";
}
const server = new WebpackDevServer( const server = new WebpackDevServer(
{ {
hot: false, hot: false,
@@ -103,7 +99,7 @@ gulp.task("webpack-watch-app", () => {
).watch({ poll: isWsl }, doneHandler()); ).watch({ poll: isWsl }, doneHandler());
gulp.watch( gulp.watch(
path.join(paths.translations_src, "en.json"), path.join(paths.translations_src, "en.json"),
gulp.series("build-translations", "copy-translations-app") gulp.series("create-translations", "copy-translations-app")
); );
}); });

16
build-scripts/util.cjs Normal file
View File

@@ -0,0 +1,16 @@
const path = require("path");
const fs = require("fs");
// Helper function to map recursively over files in a folder and it's subfolders
module.exports.mapFiles = function mapFiles(startPath, filter, mapFunc) {
const files = fs.readdirSync(startPath);
for (let i = 0; i < files.length; i++) {
const filename = path.join(startPath, files[i]);
const stat = fs.lstatSync(filename);
if (stat.isDirectory()) {
mapFiles(filename, filter, mapFunc);
} else if (filename.indexOf(filter) >= 0) {
mapFunc(filename);
}
}
};

View File

@@ -10,7 +10,6 @@ const WebpackBar = require("webpackbar");
const { const {
TransformAsyncModulesPlugin, TransformAsyncModulesPlugin,
} = require("transform-async-modules-webpack-plugin"); } = require("transform-async-modules-webpack-plugin");
const { dependencies } = require("../package.json");
const paths = require("./paths.cjs"); const paths = require("./paths.cjs");
const bundle = require("./bundle.cjs"); const bundle = require("./bundle.cjs");
@@ -63,25 +62,17 @@ const createWebpackConfig = ({
rules: [ rules: [
{ {
test: /\.m?js$|\.ts$/, test: /\.m?js$|\.ts$/,
use: (info) => ({ use: {
loader: "babel-loader", loader: "babel-loader",
options: { options: {
...bundle.babelOptions({ ...bundle.babelOptions({ latestBuild, isProdBuild, isTestBuild }),
latestBuild,
isProdBuild,
isTestBuild,
sw: info.issuerLayer === "sw",
}),
cacheDirectory: !isProdBuild, cacheDirectory: !isProdBuild,
cacheCompression: false, cacheCompression: false,
}, },
}), },
resolve: { resolve: {
fullySpecified: false, fullySpecified: false,
}, },
parser: {
worker: ["*context.audioWorklet.addModule()", "..."],
},
}, },
{ {
test: /\.css$/, test: /\.css$/,
@@ -100,15 +91,11 @@ const createWebpackConfig = ({
moduleIds: isProdBuild && !isStatsBuild ? "deterministic" : "named", moduleIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
chunkIds: isProdBuild && !isStatsBuild ? "deterministic" : "named", chunkIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
splitChunks: { splitChunks: {
// Disable splitting for web workers and worklets because imports of // Disable splitting for web workers with ESM output
// external chunks are broken for: // Imports of external chunks are broken
// - ESM output: https://github.com/webpack/webpack/issues/17014 chunks: latestBuild
// - Worklets use `importScripts`: https://github.com/webpack/webpack/issues/11543 ? (chunk) => !chunk.canBeInitial() && !/^.+-worker$/.test(chunk.name)
chunks: (chunk) => : undefined,
!chunk.canBeInitial() &&
!new RegExp(`^.+-work${latestBuild ? "(?:let|er)" : "let"}$`).test(
chunk.name
),
}, },
}, },
plugins: [ plugins: [
@@ -169,15 +156,11 @@ const createWebpackConfig = ({
transform: (stats) => JSON.stringify(filterStats(stats)), transform: (stats) => JSON.stringify(filterStats(stats)),
}), }),
!latestBuild && !latestBuild &&
new TransformAsyncModulesPlugin({ new TransformAsyncModulesPlugin({ browserslistEnv: "legacy" }),
browserslistEnv: "legacy",
runtime: { version: dependencies["@babel/runtime"] },
}),
].filter(Boolean), ].filter(Boolean),
resolve: { resolve: {
extensions: [".ts", ".js", ".json"], extensions: [".ts", ".js", ".json"],
alias: { alias: {
"lit/static-html$": "lit/static-html.js",
"lit/decorators$": "lit/decorators.js", "lit/decorators$": "lit/decorators.js",
"lit/directive$": "lit/directive.js", "lit/directive$": "lit/directive.js",
"lit/directives/until$": "lit/directives/until.js", "lit/directives/until$": "lit/directives/until.js",
@@ -240,7 +223,6 @@ const createWebpackConfig = ({
), ),
}, },
experiments: { experiments: {
layers: true,
outputModule: true, outputModule: true,
}, },
}; };

View File

@@ -1,5 +0,0 @@
"use strict";
self.addEventListener("fetch", (event) => {
event.respondWith(fetch(event.request));
});

View File

@@ -36,7 +36,13 @@
</head> </head>
<body> <body>
<%= renderTemplate("../../../src/html/_js_base.html.template") %> <%= renderTemplate("../../../src/html/_js_base.html.template") %>
<%= renderTemplate("../../../src/html/_script_loader.html.template") %> <script>
<% for (const entry of latestEntryJS) { %>
import("<%= entry %>");
<% } %>
window.latestJS = true;
</script>
<%= renderTemplate("../../../src/html/_script_load_es5.html.template") %>
<hc-layout subtitle="FAQ"> <hc-layout subtitle="FAQ">
<style> <style>
a { a {
@@ -139,7 +145,7 @@
</p> </p>
</div> </div>
<div class="section-header">What does Home Assistant Cast do?</div> <div class="section-header">Wat does Home Assistant Cast do?</div>
<div class="card-content"> <div class="card-content">
<p> <p>
Home Assistant Cast is a receiver application for the Chromecast. When Home Assistant Cast is a receiver application for the Chromecast. When
@@ -226,5 +232,17 @@ http:
</p> </p>
</div> </div>
</hc-layout> </hc-layout>
<script>
var _gaq = [["_setAccount", "UA-57927901-9"], ["_trackPageview"]];
(function (d, t) {
var g = d.createElement(t),
s = d.getElementsByTagName(t)[0];
g.src =
("https:" == location.protocol ? "//ssl" : "//www") +
".google-analytics.com/ga.js";
s.parentNode.insertBefore(g, s);
})(document, "script");
</script>
</body> </body>
</html> </html>

View File

@@ -13,9 +13,15 @@
<%= renderTemplate("_social_meta.html.template") %> <%= renderTemplate("_social_meta.html.template") %>
</head> </head>
<body> <body>
<hc-connect></hc-connect>
<%= renderTemplate("../../../src/html/_js_base.html.template") %> <%= renderTemplate("../../../src/html/_js_base.html.template") %>
<%= renderTemplate("../../../src/html/_script_loader.html.template") %> <hc-connect></hc-connect>
<script>
<% for (const entry of latestEntryJS) { %>
import("<%= entry %>");
<% } %>
window.latestJS = true;
</script>
<%= renderTemplate("../../../src/html/_script_load_es5.html.template") %>
<script> <script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),

View File

@@ -14,10 +14,22 @@
--background-color: #41bdf5; --background-color: #41bdf5;
} }
</style> </style>
<script>
var _gaq=[['_setAccount','UA-57927901-10'],['_trackPageview']];
(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
g.src=('https:'==location.protocol?'//ssl':'//www')+'.google-analytics.com/ga.js';
s.parentNode.insertBefore(g,s)}(document,'script'));
</script>
</head> </head>
<body> <body>
<cast-media-player></cast-media-player>
<%= renderTemplate("../../../src/html/_js_base.html.template") %> <%= renderTemplate("../../../src/html/_js_base.html.template") %>
<%= renderTemplate("../../../src/html/_script_loader.html.template") %> <cast-media-player></cast-media-player>
<script>
<% for (const entry of latestEntryJS) { %>
import("<%= entry %>");
<% } %>
window.latestJS = true;
</script>
<%= renderTemplate("../../../src/html/_script_load_es5.html.template") %>
</body> </body>
</html> </html>

View File

@@ -11,4 +11,10 @@
font-size: initial; font-size: initial;
} }
</style> </style>
<script>
var _gaq=[['_setAccount','UA-57927901-10'],['_trackPageview']];
(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
g.src=('https:'==location.protocol?'//ssl':'//www')+'.google-analytics.com/ga.js';
s.parentNode.insertBefore(g,s)}(document,'script'));
</script>
</html> </html>

View File

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

View File

@@ -1,6 +1,7 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import { ActionDetail } from "@material/mwc-list/mwc-list"; import { mdiCast, mdiCastConnected } from "@mdi/js";
import { mdiCast, mdiCastConnected, mdiViewDashboard } 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 { Auth, Connection } from "home-assistant-js-websocket";
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit"; import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
@@ -27,7 +28,6 @@ import { LovelaceViewConfig } from "../../../../src/data/lovelace/config/view";
import "../../../../src/layouts/hass-loading-screen"; import "../../../../src/layouts/hass-loading-screen";
import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config"; import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config";
import "./hc-layout"; import "./hc-layout";
import "../../../../src/components/ha-list-item";
@customElement("hc-cast") @customElement("hc-cast")
class HcCast extends LitElement { class HcCast extends LitElement {
@@ -83,37 +83,34 @@ class HcCast extends LitElement {
` `
: html` : html`
<div class="section-header">PICK A VIEW</div> <div class="section-header">PICK A VIEW</div>
<mwc-list @action=${this._handlePickView} activatable> <paper-listbox
attr-for-selected="data-path"
.selected=${this.castManager.status.lovelacePath || ""}
>
${( ${(
this.lovelaceViews ?? [ this.lovelaceViews ?? [
generateDefaultViewConfig({}, {}, {}, {}, () => ""), generateDefaultViewConfig({}, {}, {}, {}, () => ""),
] ]
).map( ).map(
(view, idx) => (view, idx) => html`
html`<ha-list-item <paper-icon-item
graphic="avatar" @click=${this._handlePickView}
.activated=${this.castManager.status?.lovelacePath === data-path=${view.path || idx}
(view.path ?? idx)}
.selected=${this.castManager.status?.lovelacePath ===
(view.path ?? idx)}
> >
${view.title || view.path || "Unnamed view"}
${view.icon ${view.icon
? html` ? html`
<ha-icon <ha-icon
.icon=${view.icon} .icon=${view.icon}
slot="graphic" slot="item-icon"
></ha-icon> ></ha-icon>
` `
: html`<ha-svg-icon : ""}
slot="item-icon" ${view.title || view.path}
.path=${mdiViewDashboard} </paper-icon-item>
></ha-svg-icon>`}</ha-list-item `
> ` )}
)}</mwc-list </paper-listbox>
>
`} `}
<div class="card-actions"> <div class="card-actions">
${this.castManager.status ${this.castManager.status
? html` ? html`
@@ -185,8 +182,8 @@ class HcCast extends LitElement {
this.castManager.requestSession(); this.castManager.requestSession();
} }
private async _handlePickView(ev: CustomEvent<ActionDetail>) { private async _handlePickView(ev: Event) {
const path = this.lovelaceViews![ev.detail.index].path ?? ev.detail.index; const path = (ev.currentTarget as any).getAttribute("data-path");
await ensureConnectedCastSession(this.castManager!, this.auth!); await ensureConnectedCastSession(this.castManager!, this.auth!);
castSendShowLovelaceView(this.castManager, this.auth.data.hassUrl, path); castSendShowLovelaceView(this.castManager, this.auth.data.hassUrl, path);
} }
@@ -249,14 +246,25 @@ class HcCast extends LitElement {
height: 18px; height: 18px;
} }
ha-list-item ha-icon, paper-listbox {
ha-list-item ha-svg-icon { padding-top: 0;
}
paper-listbox ha-icon {
padding: 12px; padding: 12px;
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
:host([hide-icons]) ha-icon { paper-icon-item {
display: none; cursor: pointer;
}
paper-icon-item[disabled] {
cursor: initial;
}
:host([hide-icons]) paper-icon-item {
--paper-item-icon-width: 0px;
} }
.spacer { .spacer {

View File

@@ -2,7 +2,6 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import { LovelaceConfig } from "../../../../src/data/lovelace/config/types"; import { LovelaceConfig } from "../../../../src/data/lovelace/config/types";
import { getPanelTitleFromUrlPath } from "../../../../src/data/panel";
import { Lovelace } from "../../../../src/panels/lovelace/types"; import { Lovelace } from "../../../../src/panels/lovelace/types";
import "../../../../src/panels/lovelace/views/hui-view"; import "../../../../src/panels/lovelace/views/hui-view";
import { HomeAssistant } from "../../../../src/types"; import { HomeAssistant } from "../../../../src/types";
@@ -62,12 +61,7 @@ class HcLovelace extends LitElement {
const index = this._viewIndex; const index = this._viewIndex;
if (index !== undefined) { if (index !== undefined) {
const title = getPanelTitleFromUrlPath( const dashboardTitle = this.lovelaceConfig.title || this.urlPath;
this.hass,
this.urlPath || "lovelace"
);
const dashboardTitle = title || this.urlPath;
const viewTitle = const viewTitle =
this.lovelaceConfig.views[index].title || this.lovelaceConfig.views[index].title ||
@@ -86,17 +80,10 @@ class HcLovelace extends LitElement {
this.lovelaceConfig.views[index].background || this.lovelaceConfig.views[index].background ||
this.lovelaceConfig.background; this.lovelaceConfig.background;
const backgroundStyle = if (configBackground) {
typeof configBackground === "string"
? configBackground
: configBackground?.image
? `center / cover no-repeat url('${configBackground.image}')`
: undefined;
if (backgroundStyle) {
this._huiView!.style.setProperty( this._huiView!.style.setProperty(
"--lovelace-background", "--lovelace-background",
backgroundStyle configBackground
); );
} else { } else {
this._huiView!.style.removeProperty("--lovelace-background"); this._huiView!.style.removeProperty("--lovelace-background");

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -1,5 +0,0 @@
"use strict";
self.addEventListener("fetch", (event) => {
event.respondWith(fetch(event.request));
});

View File

@@ -1,7 +1,7 @@
import { convertEntities } from "../../../../src/fake_data/entity"; import { convertEntities } from "../../../../src/fake_data/entity";
import { DemoConfig } from "../types"; import { DemoConfig } from "../types";
export const demoEntitiesSections: DemoConfig["entities"] = (localize) => export const demoEntitiesSections: DemoConfig["entities"] = () =>
convertEntities({ convertEntities({
"cover.living_room_garden_shutter": { "cover.living_room_garden_shutter": {
entity_id: "cover.living_room_garden_shutter", entity_id: "cover.living_room_garden_shutter",
@@ -111,60 +111,13 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
friendly_name: "Living room Temperature", friendly_name: "Living room Temperature",
}, },
}, },
"sensor.outdoor_temperature": {
entity_id: "sensor.outdoor_temperature",
state: "10.5",
attributes: {
state_class: "measurement",
unit_of_measurement: "°C",
device_class: "temperature",
friendly_name: "Outdoor temperature",
},
},
"sensor.outdoor_humidity": {
entity_id: "sensor.outdoor_humidity",
state: "70.4",
attributes: {
state_class: "measurement",
unit_of_measurement: "%",
device_class: "humidity",
friendly_name: "Outdoor humidity",
},
},
"device_tracker.car": {
entity_id: "sensor.outdoor_humidity",
state: "not_home",
attributes: {
friendly_name: "Car",
icon: "mdi:car",
},
},
"media_player.living_room_nest_mini": { "media_player.living_room_nest_mini": {
entity_id: "media_player.living_room_nest_mini", entity_id: "media_player.living_room_nest_mini",
state: "playing", state: "off",
attributes: { attributes: {
device_class: "speaker", device_class: "speaker",
volume_level: 0.18, friendly_name: "Living room Nest Mini",
is_volume_muted: false, supported_features: 152461,
media_content_type: "music",
media_duration: 300,
media_position: 0,
media_position_updated_at: new Date(
// 23 seconds in
new Date().getTime() - 23000
).toISOString(),
media_title: "I Wasn't Born To Follow",
media_artist: "The Byrds",
media_album_name: "The Notorious Byrd Brothers",
source_list: ["It's A Party", "Radio HSL", "Retro 70s and 80s"],
shuffle: false,
night_sound: false,
speech_enhance: false,
friendly_name: localize(
"ui.panel.page-demo.config.sections.entities.media_player.living_room_nest_mini"
),
entity_picture: "/assets/sections/images/media_player_family_room.jpg",
supported_features: 64063,
}, },
}, },
"cover.kitchen_shutter": { "cover.kitchen_shutter": {
@@ -215,27 +168,8 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
state: "on", state: "on",
attributes: { attributes: {
device_class: "speaker", device_class: "speaker",
volume_level: 0.18, friendly_name: "Kitchen Nest Audio",
is_volume_muted: false, supported_features: 152461,
media_content_type: "music",
media_duration: 300,
media_position: 0,
media_position_updated_at: new Date(
// 23 seconds in
new Date().getTime() - 23000
).toISOString(),
media_title: "I Wasn't Born To Follow",
media_artist: "The Byrds",
media_album_name: "The Notorious Byrd Brothers",
source_list: ["It's A Party", "Radio HSL", "Retro 70s and 80s"],
shuffle: false,
night_sound: false,
speech_enhance: false,
friendly_name: localize(
"ui.panel.page-demo.config.sections.entities.media_player.kitchen_nest_audio"
),
entity_picture: "/assets/sections/images/media_player_family_room.jpg",
supported_features: 64063,
}, },
}, },
"binary_sensor.tesla_wall_connector_vehicle_connected": { "binary_sensor.tesla_wall_connector_vehicle_connected": {
@@ -399,28 +333,8 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
entity_id: "media_player.study_nest_hub", entity_id: "media_player.study_nest_hub",
state: "off", state: "off",
attributes: { attributes: {
device_class: "speaker", friendly_name: "Study Nest Hub",
volume_level: 0.18, supported_features: 152461,
is_volume_muted: false,
media_content_type: "music",
media_duration: 300,
media_position: 0,
media_position_updated_at: new Date(
// 23 seconds in
new Date().getTime() - 23000
).toISOString(),
media_title: "I Wasn't Born To Follow",
media_artist: "The Byrds",
media_album_name: "The Notorious Byrd Brothers",
source_list: ["It's A Party", "Radio HSL", "Retro 70s and 80s"],
shuffle: false,
night_sound: false,
speech_enhance: false,
friendly_name: localize(
"ui.panel.page-demo.config.sections.entities.media_player.study_nest_hub"
),
entity_picture: "/assets/sections/images/media_player_family_room.jpg",
supported_features: 64063,
}, },
}, },
"sensor.standing_desk_height": { "sensor.standing_desk_height": {

View File

@@ -1,41 +1,40 @@
import { isFrontpageEmbed } from "../../util/is_frontpage";
import { DemoConfig } from "../types"; import { DemoConfig } from "../types";
export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({ export const demoLovelaceSections: DemoConfig["lovelace"] = () => ({
title: "Home Assistant Demo", title: "Home Assistant Demo",
views: [ views: [
{ {
type: "sections", type: "sections",
title: isFrontpageEmbed ? "Home Assistant" : "Demo", title: "Demo",
path: "home", path: "home",
icon: "mdi:home-assistant", icon: "mdi:home-assistant",
badges: [
{
type: "entity",
entity: "sensor.outdoor_temperature",
color: "red",
},
{
type: "entity",
entity: "sensor.outdoor_humidity",
color: "indigo",
},
{
type: "entity",
entity: "device_tracker.car",
},
],
sections: [ sections: [
...(isFrontpageEmbed {
? [] title: "Welcome 👋",
: [ cards: [{ type: "custom:ha-demo-card" }],
{ },
title: `${localize("ui.panel.page-demo.config.sections.titles.welcome")} 👋`,
cards: [{ type: "custom:ha-demo-card" }],
},
]),
{ {
cards: [ cards: [
{
type: "tile",
entity: "cover.living_room_garden_shutter",
name: "Garden",
},
{
type: "tile",
entity: "cover.living_room_graveyard_shutter",
name: "Rear",
},
{
type: "tile",
entity: "cover.living_room_left_shutter",
name: "Left",
},
{
type: "tile",
entity: "cover.living_room_right_shutter",
name: "Right",
},
{ {
type: "tile", type: "tile",
entity: "light.floor_lamp", entity: "light.floor_lamp",
@@ -61,17 +60,13 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
detail: 1, detail: 1,
name: "Temperature", name: "Temperature",
}, },
{
type: "tile",
entity: "cover.living_room_garden_shutter",
name: "Blinds",
},
{ {
type: "tile", type: "tile",
entity: "media_player.living_room_nest_mini", entity: "media_player.living_room_nest_mini",
name: "Nest Mini",
}, },
], ],
title: `🛋️ ${localize("ui.panel.page-demo.config.sections.titles.living_room")} `, title: "🛋️ Living room ",
}, },
{ {
type: "grid", type: "grid",
@@ -104,9 +99,10 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
{ {
type: "tile", type: "tile",
entity: "media_player.kitchen_nest_audio", entity: "media_player.kitchen_nest_audio",
name: "Nest Audio",
}, },
], ],
title: `👩‍🍳 ${localize("ui.panel.page-demo.config.sections.titles.kitchen")}`, title: "👩‍🍳 Kitchen",
}, },
{ {
type: "grid", type: "grid",
@@ -148,7 +144,7 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
color: "dark-grey", color: "dark-grey",
}, },
], ],
title: `⚡️ ${localize("ui.panel.page-demo.config.sections.titles.energy")}`, title: "⚡️ Energy",
}, },
{ {
type: "grid", type: "grid",
@@ -185,7 +181,7 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
state_content: ["preset_mode", "current_temperature"], state_content: ["preset_mode", "current_temperature"],
}, },
], ],
title: `🌤️ ${localize("ui.panel.page-demo.config.sections.titles.climate")}`, title: "🌤️ Climate",
}, },
{ {
type: "grid", type: "grid",
@@ -203,6 +199,7 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
{ {
type: "tile", type: "tile",
entity: "media_player.study_nest_hub", entity: "media_player.study_nest_hub",
name: "Nest Hub",
}, },
{ {
type: "tile", type: "tile",
@@ -212,7 +209,7 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
icon: "mdi:desk", icon: "mdi:desk",
}, },
], ],
title: `🧑‍💻 ${localize("ui.panel.page-demo.config.sections.titles.study")}`, title: "🧑‍💻 Study",
}, },
{ {
type: "grid", type: "grid",
@@ -246,7 +243,7 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
name: "Illuminance", name: "Illuminance",
}, },
], ],
title: `🌳 ${localize("ui.panel.page-demo.config.sections.titles.outdoor")}`, title: "🌳 Outdoor",
}, },
{ {
type: "grid", type: "grid",
@@ -276,7 +273,7 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
icon: "mdi:home-assistant", icon: "mdi:home-assistant",
}, },
], ],
title: `🎉 ${localize("ui.panel.page-demo.config.sections.titles.updates")}`, title: "🎉 Updates",
}, },
], ],
}, },

View File

@@ -1,9 +1,8 @@
import "@material/mwc-button";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { until } from "lit/directives/until"; import { until } from "lit/directives/until";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import "../../../src/components/ha-button";
import "../../../src/components/ha-circular-progress"; import "../../../src/components/ha-circular-progress";
import { LovelaceCardConfig } from "../../../src/data/lovelace/config/card"; import { LovelaceCardConfig } from "../../../src/data/lovelace/config/card";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
@@ -12,6 +11,7 @@ import {
demoConfigs, demoConfigs,
selectedDemoConfig, selectedDemoConfig,
selectedDemoConfigIndex, selectedDemoConfigIndex,
setDemoConfig,
} from "../configs/demo-configs"; } from "../configs/demo-configs";
@customElement("ha-demo-card") @customElement("ha-demo-card")
@@ -64,9 +64,9 @@ export class HADemoCard extends LitElement implements LovelaceCard {
)} )}
</div> </div>
<ha-button @click=${this._nextConfig} .disabled=${this._switching}> <mwc-button @click=${this._nextConfig} .disabled=${this._switching}>
${this.hass.localize("ui.panel.page-demo.cards.demo.next_demo")} ${this.hass.localize("ui.panel.page-demo.cards.demo.next_demo")}
</ha-button> </mwc-button>
</div> </div>
<div class="content"> <div class="content">
<p class="small-hidden"> <p class="small-hidden">
@@ -87,9 +87,9 @@ export class HADemoCard extends LitElement implements LovelaceCard {
</div> </div>
<div class="actions small-hidden"> <div class="actions small-hidden">
<a href="https://www.home-assistant.io" target="_blank"> <a href="https://www.home-assistant.io" target="_blank">
<ha-button> <mwc-button>
${this.hass.localize("ui.panel.page-demo.cards.demo.learn_more")} ${this.hass.localize("ui.panel.page-demo.cards.demo.learn_more")}
</ha-button> </mwc-button>
</a> </a>
</div> </div>
</ha-card> </ha-card>
@@ -113,7 +113,13 @@ export class HADemoCard extends LitElement implements LovelaceCard {
private async _updateConfig(index: number) { private async _updateConfig(index: number) {
this._switching = true; this._switching = true;
fireEvent(this, "set-demo-config" as any, { index }); try {
await setDemoConfig(this.hass, this.lovelace!, index);
} catch (err: any) {
alert("Failed to switch config :-(");
} finally {
this._switching = false;
}
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
@@ -143,7 +149,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {
height: 60px; height: 60px;
} }
.picker ha-button { .picker mwc-button {
margin-right: 8px; margin-right: 8px;
} }

View File

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

View File

@@ -10,7 +10,6 @@ import {
import { HomeAssistantAppEl } from "../../src/layouts/home-assistant"; import { HomeAssistantAppEl } from "../../src/layouts/home-assistant";
import { HomeAssistant } from "../../src/types"; import { HomeAssistant } from "../../src/types";
import { selectedDemoConfig } from "./configs/demo-configs"; import { selectedDemoConfig } from "./configs/demo-configs";
import { mockAreaRegistry } from "./stubs/area_registry";
import { mockAuth } from "./stubs/auth"; import { mockAuth } from "./stubs/auth";
import { mockConfigEntries } from "./stubs/config_entries"; import { mockConfigEntries } from "./stubs/config_entries";
import { mockEnergy } from "./stubs/energy"; import { mockEnergy } from "./stubs/energy";
@@ -24,10 +23,10 @@ import { mockLovelace } from "./stubs/lovelace";
import { mockMediaPlayer } from "./stubs/media_player"; import { mockMediaPlayer } from "./stubs/media_player";
import { mockPersistentNotification } from "./stubs/persistent_notification"; import { mockPersistentNotification } from "./stubs/persistent_notification";
import { mockRecorder } from "./stubs/recorder"; import { mockRecorder } from "./stubs/recorder";
import { mockTodo } from "./stubs/todo";
import { mockSensor } from "./stubs/sensor"; import { mockSensor } from "./stubs/sensor";
import { mockSystemLog } from "./stubs/system_log"; import { mockSystemLog } from "./stubs/system_log";
import { mockTemplate } from "./stubs/template"; import { mockTemplate } from "./stubs/template";
import { mockTodo } from "./stubs/todo";
import { mockTranslations } from "./stubs/translations"; import { mockTranslations } from "./stubs/translations";
@customElement("ha-demo") @customElement("ha-demo")
@@ -63,7 +62,6 @@ export class HaDemo extends HomeAssistantAppEl {
mockEnergy(hass); mockEnergy(hass);
mockPersistentNotification(hass); mockPersistentNotification(hass);
mockConfigEntries(hass); mockConfigEntries(hass);
mockAreaRegistry(hass);
mockEntityRegistry(hass, [ mockEntityRegistry(hass, [
{ {
config_entry_id: "co2signal", config_entry_id: "co2signal",
@@ -82,8 +80,6 @@ export class HaDemo extends HomeAssistantAppEl {
has_entity_name: false, has_entity_name: false,
unique_id: "co2_intensity", unique_id: "co2_intensity",
options: null, options: null,
created_at: 0,
modified_at: 0,
}, },
{ {
config_entry_id: "co2signal", config_entry_id: "co2signal",
@@ -102,8 +98,6 @@ export class HaDemo extends HomeAssistantAppEl {
has_entity_name: false, has_entity_name: false,
unique_id: "grid_fossil_fuel_percentage", unique_id: "grid_fossil_fuel_percentage",
options: null, options: null,
created_at: 0,
modified_at: 0,
}, },
]); ]);

View File

@@ -63,47 +63,46 @@
align-items: center; align-items: center;
} }
#ha-launch-screen svg { #ha-launch-screen svg {
width: 112px; width: 170px;
flex-shrink: 0; flex-shrink: 0;
} }
#ha-launch-screen .ha-launch-screen-spacer-top { #ha-launch-screen .ha-launch-screen-spacer {
flex: 1; flex: 1;
margin-top: calc( 2 * max(env(safe-area-inset-bottom), 48px) + 46px );
padding-top: 48px;
}
#ha-launch-screen .ha-launch-screen-spacer-bottom {
flex: 1;
padding-top: 48px;
}
.ohf-logo {
margin: max(env(safe-area-inset-bottom), 48px) 0;
display: flex;
flex-direction: column;
align-items: center;
opacity: .66;
}
@media (prefers-color-scheme: dark) {
.ohf-logo {
filter: invert(1);
}
} }
</style> </style>
</head> </head>
<body> <body>
<div id="ha-launch-screen"> <div id="ha-launch-screen">
<div class="ha-launch-screen-spacer-top"></div> <div class="ha-launch-screen-spacer"></div>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240">
<path fill="#18BCF2" d="M240 224.762a15 15 0 0 1-15 15H15a15 15 0 0 1-15-15v-90c0-8.25 4.77-19.769 10.61-25.609l98.78-98.7805c5.83-5.83 15.38-5.83 21.21 0l98.79 98.7895c5.83 5.83 10.61 17.36 10.61 25.61v90-.01Z"/> <path fill="#18BCF2" d="M240 224.762a15 15 0 0 1-15 15H15a15 15 0 0 1-15-15v-90c0-8.25 4.77-19.769 10.61-25.609l98.78-98.7805c5.83-5.83 15.38-5.83 21.21 0l98.79 98.7895c5.83 5.83 10.61 17.36 10.61 25.61v90-.01Z"/>
<path fill="#F2F4F9" d="m107.27 239.762-40.63-40.63c-2.09.72-4.32 1.13-6.64 1.13-11.3 0-20.5-9.2-20.5-20.5s9.2-20.5 20.5-20.5 20.5 9.2 20.5 20.5c0 2.33-.41 4.56-1.13 6.65l31.63 31.63v-115.88c-6.8-3.3395-11.5-10.3195-11.5-18.3895 0-11.3 9.2-20.5 20.5-20.5s20.5 9.2 20.5 20.5c0 8.07-4.7 15.05-11.5 18.3895v81.27l31.46-31.46c-.62-1.96-.96-4.04-.96-6.2 0-11.3 9.2-20.5 20.5-20.5s20.5 9.2 20.5 20.5-9.2 20.5-20.5 20.5c-2.5 0-4.88-.47-7.09-1.29L129 208.892v30.88z"/> <path fill="#F2F4F9" d="m107.27 239.762-40.63-40.63c-2.09.72-4.32 1.13-6.64 1.13-11.3 0-20.5-9.2-20.5-20.5s9.2-20.5 20.5-20.5 20.5 9.2 20.5 20.5c0 2.33-.41 4.56-1.13 6.65l31.63 31.63v-115.88c-6.8-3.3395-11.5-10.3195-11.5-18.3895 0-11.3 9.2-20.5 20.5-20.5s20.5 9.2 20.5 20.5c0 8.07-4.7 15.05-11.5 18.3895v81.27l31.46-31.46c-.62-1.96-.96-4.04-.96-6.2 0-11.3 9.2-20.5 20.5-20.5s20.5 9.2 20.5 20.5-9.2 20.5-20.5 20.5c-2.5 0-4.88-.47-7.09-1.29L129 208.892v30.88z"/>
</svg> </svg>
<div id="ha-launch-screen-info-box" class="ha-launch-screen-spacer-bottom"></div> <div id="ha-launch-screen-info-box" class="ha-launch-screen-spacer"></div>
<div class="ohf-logo">
<img src="/static/images/ohf-badge.svg" alt="Home Assistant is a project by the Open Home Foundation" height="46">
</div>
</div> </div>
<ha-demo></ha-demo> <ha-demo></ha-demo>
<%= renderTemplate("../../../src/html/_js_base.html.template") %> <%= renderTemplate("../../../src/html/_js_base.html.template") %>
<%= renderTemplate("../../../src/html/_preload_roboto.html.template") %> <%= renderTemplate("../../../src/html/_preload_roboto.html.template") %>
<%= renderTemplate("../../../src/html/_script_loader.html.template") %> <script>
// Safari 12 and below does not have a compliant ES2015 implementation of template literals, so we ship ES5
if (!isS11_12) {
<% for (const entry of latestEntryJS) { %>
import("<%= entry %>");
<% } %>
window.latestJS = true;
}
</script>
<%= renderTemplate("../../../src/html/_script_load_es5.html.template") %>
<script>
var _gaq = [["_setAccount", "UA-57927901-5"], ["_trackPageview"]];
(function (d, t) {
var g = d.createElement(t),
s = d.getElementsByTagName(t)[0];
g.src =
("https:" == location.protocol ? "//ssl" : "//www") +
".google-analytics.com/ga.js";
s.parentNode.insertBefore(g, s);
})(document, "script");
</script>
</body> </body>
</html> </html>

View File

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

View File

@@ -1,4 +1,4 @@
import { format, startOfToday, startOfTomorrow } from "date-fns"; import { format, startOfToday, startOfTomorrow } from "date-fns/esm";
import { import {
EnergyInfo, EnergyInfo,
EnergyPreferences, EnergyPreferences,

View File

@@ -1,55 +1,5 @@
import { convertEntities } from "../../../src/fake_data/entity"; import { convertEntities } from "../../../src/fake_data/entity";
export const mapEntities = () =>
convertEntities({
"zone.home": {
entity_id: "zone.home",
state: "zoning",
attributes: {
hidden: true,
latitude: 52.3631339,
longitude: 4.8903147,
radius: 200,
friendly_name: "Home",
icon: "hademo:home",
},
},
"zone.uva": {
entity_id: "zone.buckhead",
state: "zoning",
attributes: {
hidden: true,
radius: 400,
friendly_name: "UvA",
icon: "hademo:school",
latitude: 52.3558182,
longitude: 4.9535376,
},
},
"person.arsaboo": {
entity_id: "person.arsaboo",
state: "not_home",
attributes: {
radius: 50,
friendly_name: "Arsaboo",
latitude: 52.3579946,
longitude: 4.8664597,
entity_picture: "/assets/arsaboo/images/arsaboo.jpg",
},
},
"person.melody": {
entity_id: "person.melody",
state: "not_home",
attributes: {
radius: 50,
friendly_name: "Melody",
latitude: 52.3408927,
longitude: 4.8711073,
entity_picture: "/assets/arsaboo/images/melody.jpg",
},
},
});
export const energyEntities = () => export const energyEntities = () =>
convertEntities({ convertEntities({
"sensor.grid_fossil_fuel_percentage": { "sensor.grid_fossil_fuel_percentage": {

View File

@@ -1,52 +1,35 @@
import type { LocalizeFunc } from "../../../src/common/translations/localize"; import type { LocalizeFunc } from "../../../src/common/translations/localize";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
import { import { selectedDemoConfig } from "../configs/demo-configs";
selectedDemoConfig,
selectedDemoConfigIndex,
setDemoConfig,
} from "../configs/demo-configs";
import "../custom-cards/cast-demo-row"; import "../custom-cards/cast-demo-row";
import "../custom-cards/ha-demo-card"; import "../custom-cards/ha-demo-card";
import { mapEntities } from "./entities"; import type { HADemoCard } from "../custom-cards/ha-demo-card";
export const mockLovelace = ( export const mockLovelace = (
hass: MockHomeAssistant, hass: MockHomeAssistant,
localizePromise: Promise<LocalizeFunc> localizePromise: Promise<LocalizeFunc>
) => { ) => {
hass.mockWS("lovelace/config", ({ url_path }) => { hass.mockWS("lovelace/config", () =>
if (url_path === "map") { Promise.all([selectedDemoConfig, localizePromise]).then(
hass.addEntities(mapEntities());
return {
strategy: {
type: "map",
},
};
}
return Promise.all([selectedDemoConfig, localizePromise]).then(
([config, localize]) => config.lovelace(localize) ([config, localize]) => config.lovelace(localize)
); )
}); );
hass.mockWS("lovelace/config/save", () => Promise.resolve()); hass.mockWS("lovelace/config/save", () => Promise.resolve());
hass.mockWS("lovelace/resources", () => Promise.resolve([])); hass.mockWS("lovelace/resources", () => Promise.resolve([]));
}; };
customElements.whenDefined("hui-root").then(() => { customElements.whenDefined("hui-view").then(() => {
// eslint-disable-next-line // eslint-disable-next-line
const HUIRoot = customElements.get("hui-root")!; const HUIView = customElements.get("hui-view");
// Patch HUI-VIEW to make the lovelace object available to the demo card
const oldCreateCard = HUIView!.prototype.createCardElement;
const oldFirstUpdated = HUIRoot.prototype.firstUpdated; HUIView!.prototype.createCardElement = function (config) {
const el = oldCreateCard.call(this, config);
HUIRoot.prototype.firstUpdated = function (changedProperties) { if (el.tagName === "HA-DEMO-CARD") {
oldFirstUpdated.call(this, changedProperties); (el as HADemoCard).lovelace = this.lovelace;
this.addEventListener("set-demo-config", async (ev) => { }
const index = (ev as CustomEvent).detail.index; return el;
try {
await setDemoConfig(this.hass, this.lovelace!, index);
} catch (err: any) {
setDemoConfig(this.hass, this.lovelace!, selectedDemoConfigIndex);
alert("Failed to switch config :-(");
}
});
}; };
}); });

View File

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

View File

@@ -1 +0,0 @@
export const isFrontpageEmbed = document.location.search === "?frontpage";

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

View File

@@ -1,9 +1,7 @@
import { load } from "js-yaml"; import { load } from "js-yaml";
import { LitElement, PropertyValueMap, css, html, nothing } from "lit"; import { html, css, LitElement, PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import { createCardElement } from "../../../src/panels/lovelace/create-element/create-card-element";
import "../../../src/panels/lovelace/cards/hui-card";
import type { HuiCard } from "../../../src/panels/lovelace/cards/hui-card";
import { HomeAssistant } from "../../../src/types"; import { HomeAssistant } from "../../../src/types";
export interface DemoCardConfig { export interface DemoCardConfig {
@@ -21,12 +19,7 @@ class DemoCard extends LitElement {
@state() private _size?: number; @state() private _size?: number;
@query("hui-card", false) private _card?: HuiCard; @query("#card") private _card!: HTMLElement;
private _config = memoizeOne((config: string) => {
const c = (load(config) as any)[0];
return c;
});
render() { render() {
return html` return html`
@@ -37,32 +30,63 @@ class DemoCard extends LitElement {
: ""} : ""}
</h2> </h2>
<div class="root"> <div class="root">
<hui-card <div id="card"></div>
.config=${this._config(this.config.config)} ${this.showConfig ? html`<pre>${this.config.config.trim()}</pre>` : ""}
.hass=${this.hass}
@card-updated=${this._cardUpdated}
></hui-card>
${this.showConfig
? html`<pre>${this.config.config.trim()}</pre>`
: nothing}
</div> </div>
`; `;
} }
private async _cardUpdated(ev) { updated(changedProps: PropertyValues) {
ev.stopPropagation(); super.updated(changedProps);
this._updateSize();
if (changedProps.has("config")) {
const card = this._card;
while (card.lastChild) {
card.removeChild(card.lastChild);
}
const el = this._createCardElement((load(this.config.config) as any)[0]);
card.appendChild(el);
this._getSize(el);
}
if (changedProps.has("hass")) {
const card = this._card.lastChild;
if (card) {
(card as any).hass = this.hass;
}
}
} }
private async _updateSize() { async _getSize(el) {
this._size = await this._card?.getCardSize(); await customElements.whenDefined(el.localName);
if (!("getCardSize" in el)) {
this._size = undefined;
return;
}
this._size = await el.getCardSize();
} }
protected update( _createCardElement(cardConfig) {
_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown> const element = createCardElement(cardConfig);
): void { if (this.hass) {
super.update(_changedProperties); element.hass = this.hass;
this._updateSize(); }
element.addEventListener(
"ll-rebuild",
(ev) => {
ev.stopPropagation();
this._rebuildCard(element, cardConfig);
},
{ once: true }
);
return element;
}
_rebuildCard(cardElToReplace, config) {
const newCardEl = this._createCardElement(config);
cardElToReplace.parentElement.replaceChild(newCardEl, cardElToReplace);
} }
static styles = css` static styles = css`
@@ -77,7 +101,7 @@ class DemoCard extends LitElement {
font-size: 0.5em; font-size: 0.5em;
color: var(--primary-text-color); color: var(--primary-text-color);
} }
hui-card { #card {
max-width: 400px; max-width: 400px;
width: 100vw; width: 100vw;
} }

View File

@@ -532,6 +532,15 @@ export default {
last_changed: "2018-07-19T10:44:46.200946+00:00", last_changed: "2018-07-19T10:44:46.200946+00:00",
last_updated: "2018-07-19T10:44:46.200946+00:00", last_updated: "2018-07-19T10:44:46.200946+00:00",
}, },
"mailbox.demomailbox": {
entity_id: "mailbox.demomailbox",
state: "10",
attributes: {
friendly_name: "DemoMailbox",
},
last_changed: "2018-07-19T10:45:16.555210+00:00",
last_updated: "2018-07-19T10:45:16.555210+00:00",
},
"input_select.living_room_preset": { "input_select.living_room_preset": {
entity_id: "input_select.living_room_preset", entity_id: "input_select.living_room_preset",
state: "Visitors", state: "Visitors",

View File

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

View File

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

View File

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

View File

@@ -3,16 +3,13 @@ title: When to use remove, delete, add and create
subtitle: The difference between remove/delete and add/create. subtitle: The difference between remove/delete and add/create.
--- ---
# Removing or deleting content # Remove vs Delete
_Remove_ and _Delete_ are quite similar, but can be frustrating if used inconsistently. Remove and Delete are quite similar, but can be frustrating if used inconsistently.
- Remove refers to an action that can be restored or reapplied.
- Delete refers to a permanent, non-recoverable action.
## Remove ## Remove
The term _Remove_ should always be used when an item/setting or content is to be removed or disassociated, but the action can be reversed or reapplied. Take away and set aside, but kept in existence.
For example: For example:
@@ -25,7 +22,7 @@ For example:
## Delete ## Delete
The term _Delete_ should always be used to refer to any action that will cause the permanent deletion of an item/setting or content. Erase, rendered nonexistent or nonrecoverable.
For example: For example:

View File

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

View File

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

View File

@@ -20,7 +20,6 @@ import { HaWaitForTriggerAction } from "../../../../src/panels/config/automation
import { HaWaitAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_template"; import { HaWaitAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_template";
import { Action } from "../../../../src/data/script"; import { Action } from "../../../../src/data/script";
import { HaConditionAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-condition"; import { HaConditionAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-condition";
import { HaSequenceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-sequence";
import { HaParallelAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-parallel"; import { HaParallelAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-parallel";
import { HaIfAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-if"; import { HaIfAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-if";
import { HaStopAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-stop"; import { HaStopAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-stop";
@@ -40,7 +39,6 @@ const SCHEMAS: { name: string; actions: Action[] }[] = [
{ name: "If-Then", actions: [HaIfAction.defaultConfig] }, { name: "If-Then", actions: [HaIfAction.defaultConfig] },
{ name: "Choose", actions: [HaChooseAction.defaultConfig] }, { name: "Choose", actions: [HaChooseAction.defaultConfig] },
{ name: "Variables", actions: [{ variables: { hello: "1" } }] }, { name: "Variables", actions: [{ variables: { hello: "1" } }] },
{ name: "Sequence", actions: [HaSequenceAction.defaultConfig] },
{ name: "Parallel", actions: [HaParallelAction.defaultConfig] }, { name: "Parallel", actions: [HaParallelAction.defaultConfig] },
{ name: "Stop", actions: [HaStopAction.defaultConfig] }, { name: "Stop", actions: [HaStopAction.defaultConfig] },
]; ];

View File

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

View File

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

View File

@@ -187,7 +187,7 @@ export class DemoHaControlSelect extends LitElement {
--mdc-icon-size: 24px; --mdc-icon-size: 24px;
--control-select-color: var(--state-fan-active-color); --control-select-color: var(--state-fan-active-color);
--control-select-thickness: 130px; --control-select-thickness: 130px;
--control-select-border-radius: 36px; --control-select-border-radius: 48px;
} }
.vertical-selects { .vertical-selects {
height: 300px; height: 300px;

View File

@@ -151,7 +151,7 @@ export class DemoHaBarSlider extends LitElement {
--control-slider-background: #ffcf4c; --control-slider-background: #ffcf4c;
--control-slider-background-opacity: 0.2; --control-slider-background-opacity: 0.2;
--control-slider-thickness: 130px; --control-slider-thickness: 130px;
--control-slider-border-radius: 36px; --control-slider-border-radius: 48px;
} }
.vertical-sliders { .vertical-sliders {
height: 300px; height: 300px;

View File

@@ -118,7 +118,7 @@ export class DemoHaControlSwitch extends LitElement {
--control-switch-on-color: var(--green-color); --control-switch-on-color: var(--green-color);
--control-switch-off-color: var(--red-color); --control-switch-off-color: var(--red-color);
--control-switch-thickness: 130px; --control-switch-thickness: 130px;
--control-switch-border-radius: 36px; --control-switch-border-radius: 48px;
--control-switch-padding: 6px; --control-switch-padding: 6px;
--mdc-icon-size: 24px; --mdc-icon-size: 24px;
} }

View File

@@ -15,7 +15,6 @@ import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass"; import { provideHass } from "../../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../../src/types"; import { HomeAssistant } from "../../../../src/types";
import "../../components/demo-black-white-row"; import "../../components/demo-black-white-row";
import { DeviceRegistryEntry } from "../../../../src/data/device_registry";
const ENTITIES = [ const ENTITIES = [
getEntity("alarm_control_panel", "alarm", "disarmed", { getEntity("alarm_control_panel", "alarm", "disarmed", {
@@ -42,7 +41,7 @@ const ENTITIES = [
}), }),
]; ];
const DEVICES: DeviceRegistryEntry[] = [ const DEVICES = [
{ {
area_id: "bedroom", area_id: "bedroom",
configuration_url: null, configuration_url: null,
@@ -54,7 +53,6 @@ const DEVICES: DeviceRegistryEntry[] = [
identifiers: [["demo", "volume1"] as [string, string]], identifiers: [["demo", "volume1"] as [string, string]],
manufacturer: null, manufacturer: null,
model: null, model: null,
model_id: null,
name_by_user: null, name_by_user: null,
name: "Dishwasher", name: "Dishwasher",
sw_version: null, sw_version: null,
@@ -62,9 +60,6 @@ const DEVICES: DeviceRegistryEntry[] = [
via_device_id: null, via_device_id: null,
serial_number: null, serial_number: null,
labels: [], labels: [],
created_at: 0,
modified_at: 0,
primary_config_entry: null,
}, },
{ {
area_id: "backyard", area_id: "backyard",
@@ -77,7 +72,6 @@ const DEVICES: DeviceRegistryEntry[] = [
identifiers: [["demo", "pwm1"] as [string, string]], identifiers: [["demo", "pwm1"] as [string, string]],
manufacturer: null, manufacturer: null,
model: null, model: null,
model_id: null,
name_by_user: null, name_by_user: null,
name: "Lamp", name: "Lamp",
sw_version: null, sw_version: null,
@@ -85,9 +79,6 @@ const DEVICES: DeviceRegistryEntry[] = [
via_device_id: null, via_device_id: null,
serial_number: null, serial_number: null,
labels: [], labels: [],
created_at: 0,
modified_at: 0,
primary_config_entry: null,
}, },
{ {
area_id: null, area_id: null,
@@ -100,7 +91,6 @@ const DEVICES: DeviceRegistryEntry[] = [
identifiers: [["demo", "pwm1"] as [string, string]], identifiers: [["demo", "pwm1"] as [string, string]],
manufacturer: null, manufacturer: null,
model: null, model: null,
model_id: null,
name_by_user: "User name", name_by_user: "User name",
name: "Technical name", name: "Technical name",
sw_version: null, sw_version: null,
@@ -108,9 +98,6 @@ const DEVICES: DeviceRegistryEntry[] = [
via_device_id: null, via_device_id: null,
serial_number: null, serial_number: null,
labels: [], labels: [],
created_at: 0,
modified_at: 0,
primary_config_entry: null,
}, },
]; ];
@@ -123,8 +110,6 @@ const AREAS: AreaRegistryEntry[] = [
picture: null, picture: null,
aliases: [], aliases: [],
labels: [], labels: [],
created_at: 0,
modified_at: 0,
}, },
{ {
area_id: "bedroom", area_id: "bedroom",
@@ -134,8 +119,6 @@ const AREAS: AreaRegistryEntry[] = [
picture: null, picture: null,
aliases: [], aliases: [],
labels: [], labels: [],
created_at: 0,
modified_at: 0,
}, },
{ {
area_id: "livingroom", area_id: "livingroom",
@@ -145,8 +128,6 @@ const AREAS: AreaRegistryEntry[] = [
picture: null, picture: null,
aliases: [], aliases: [],
labels: [], labels: [],
created_at: 0,
modified_at: 0,
}, },
]; ];

View File

@@ -21,7 +21,6 @@ import { FloorRegistryEntry } from "../../../../src/data/floor_registry";
import { LabelRegistryEntry } from "../../../../src/data/label_registry"; import { LabelRegistryEntry } from "../../../../src/data/label_registry";
import { mockFloorRegistry } from "../../../../demo/src/stubs/floor_registry"; import { mockFloorRegistry } from "../../../../demo/src/stubs/floor_registry";
import { mockLabelRegistry } from "../../../../demo/src/stubs/label_registry"; import { mockLabelRegistry } from "../../../../demo/src/stubs/label_registry";
import { DeviceRegistryEntry } from "../../../../src/data/device_registry";
const ENTITIES = [ const ENTITIES = [
getEntity("alarm_control_panel", "alarm", "disarmed", { getEntity("alarm_control_panel", "alarm", "disarmed", {
@@ -42,7 +41,7 @@ const ENTITIES = [
}), }),
]; ];
const DEVICES: DeviceRegistryEntry[] = [ const DEVICES = [
{ {
area_id: "bedroom", area_id: "bedroom",
configuration_url: null, configuration_url: null,
@@ -54,7 +53,6 @@ const DEVICES: DeviceRegistryEntry[] = [
identifiers: [["demo", "volume1"] as [string, string]], identifiers: [["demo", "volume1"] as [string, string]],
manufacturer: null, manufacturer: null,
model: null, model: null,
model_id: null,
name_by_user: null, name_by_user: null,
name: "Dishwasher", name: "Dishwasher",
sw_version: null, sw_version: null,
@@ -62,9 +60,6 @@ const DEVICES: DeviceRegistryEntry[] = [
via_device_id: null, via_device_id: null,
serial_number: null, serial_number: null,
labels: [], labels: [],
created_at: 0,
modified_at: 0,
primary_config_entry: null,
}, },
{ {
area_id: "backyard", area_id: "backyard",
@@ -77,7 +72,6 @@ const DEVICES: DeviceRegistryEntry[] = [
identifiers: [["demo", "pwm1"] as [string, string]], identifiers: [["demo", "pwm1"] as [string, string]],
manufacturer: null, manufacturer: null,
model: null, model: null,
model_id: null,
name_by_user: null, name_by_user: null,
name: "Lamp", name: "Lamp",
sw_version: null, sw_version: null,
@@ -85,9 +79,6 @@ const DEVICES: DeviceRegistryEntry[] = [
via_device_id: null, via_device_id: null,
serial_number: null, serial_number: null,
labels: [], labels: [],
created_at: 0,
modified_at: 0,
primary_config_entry: null,
}, },
{ {
area_id: null, area_id: null,
@@ -100,7 +91,6 @@ const DEVICES: DeviceRegistryEntry[] = [
identifiers: [["demo", "pwm1"] as [string, string]], identifiers: [["demo", "pwm1"] as [string, string]],
manufacturer: null, manufacturer: null,
model: null, model: null,
model_id: null,
name_by_user: "User name", name_by_user: "User name",
name: "Technical name", name: "Technical name",
sw_version: null, sw_version: null,
@@ -108,9 +98,6 @@ const DEVICES: DeviceRegistryEntry[] = [
via_device_id: null, via_device_id: null,
serial_number: null, serial_number: null,
labels: [], labels: [],
created_at: 0,
modified_at: 0,
primary_config_entry: null,
}, },
]; ];
@@ -123,8 +110,6 @@ const AREAS: AreaRegistryEntry[] = [
picture: null, picture: null,
aliases: [], aliases: [],
labels: [], labels: [],
created_at: 0,
modified_at: 0,
}, },
{ {
area_id: "bedroom", area_id: "bedroom",
@@ -134,8 +119,6 @@ const AREAS: AreaRegistryEntry[] = [
picture: null, picture: null,
aliases: [], aliases: [],
labels: [], labels: [],
created_at: 0,
modified_at: 0,
}, },
{ {
area_id: "livingroom", area_id: "livingroom",
@@ -145,8 +128,6 @@ const AREAS: AreaRegistryEntry[] = [
picture: null, picture: null,
aliases: [], aliases: [],
labels: [], labels: [],
created_at: 0,
modified_at: 0,
}, },
]; ];
@@ -157,8 +138,6 @@ const FLOORS: FloorRegistryEntry[] = [
level: 0, level: 0,
icon: null, icon: null,
aliases: [], aliases: [],
created_at: 0,
modified_at: 0,
}, },
{ {
floor_id: "first", floor_id: "first",
@@ -166,8 +145,6 @@ const FLOORS: FloorRegistryEntry[] = [
level: 1, level: 1,
icon: "mdi:numeric-1", icon: "mdi:numeric-1",
aliases: [], aliases: [],
created_at: 0,
modified_at: 0,
}, },
{ {
floor_id: "second", floor_id: "second",
@@ -175,8 +152,6 @@ const FLOORS: FloorRegistryEntry[] = [
level: 2, level: 2,
icon: "mdi:numeric-2", icon: "mdi:numeric-2",
aliases: [], aliases: [],
created_at: 0,
modified_at: 0,
}, },
]; ];
@@ -186,18 +161,12 @@ const LABELS: LabelRegistryEntry[] = [
name: "Energy", name: "Energy",
icon: null, icon: null,
color: "yellow", color: "yellow",
description: null,
created_at: 0,
modified_at: 0,
}, },
{ {
label_id: "entertainment", label_id: "entertainment",
name: "Entertainment", name: "Entertainment",
icon: "mdi:popcorn", icon: "mdi:popcorn",
color: "blue", color: "blue",
description: null,
created_at: 0,
modified_at: 0,
}, },
]; ];

View File

@@ -56,46 +56,48 @@ export class DemoDateTimeDateTimeNumeric extends LitElement {
<div class="center">12 Hours</div> <div class="center">12 Hours</div>
<div class="center">24 Hours</div> <div class="center">24 Hours</div>
</div> </div>
${Object.entries(translationMetadata.translations).map( ${Object.entries(translationMetadata.translations)
([key, value]) => html` .filter(([key, _]) => key !== "test")
<div class="container"> .map(
<div>${value.nativeName}</div> ([key, value]) => html`
<div class="center"> <div class="container">
${formatDateTimeNumeric( <div>${value.nativeName}</div>
this.date, <div class="center">
{ ${formatDateTimeNumeric(
...defaultLocale, this.date,
language: key, {
time_format: TimeFormat.language, ...defaultLocale,
}, language: key,
demoConfig time_format: TimeFormat.language,
)} },
demoConfig
)}
</div>
<div class="center">
${formatDateTimeNumeric(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatDateTimeNumeric(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div> </div>
<div class="center"> `
${formatDateTimeNumeric( )}
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatDateTimeNumeric(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
</mwc-list> </mwc-list>
`; `;
} }

View File

@@ -56,46 +56,48 @@ export class DemoDateTimeDateTimeSeconds extends LitElement {
<div class="center">12 Hours</div> <div class="center">12 Hours</div>
<div class="center">24 Hours</div> <div class="center">24 Hours</div>
</div> </div>
${Object.entries(translationMetadata.translations).map( ${Object.entries(translationMetadata.translations)
([key, value]) => html` .filter(([key, _]) => key !== "test")
<div class="container"> .map(
<div>${value.nativeName}</div> ([key, value]) => html`
<div class="center"> <div class="container">
${formatDateTimeWithSeconds( <div>${value.nativeName}</div>
this.date, <div class="center">
{ ${formatDateTimeWithSeconds(
...defaultLocale, this.date,
language: key, {
time_format: TimeFormat.language, ...defaultLocale,
}, language: key,
demoConfig time_format: TimeFormat.language,
)} },
demoConfig
)}
</div>
<div class="center">
${formatDateTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatDateTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div> </div>
<div class="center"> `
${formatDateTimeWithSeconds( )}
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatDateTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
</mwc-list> </mwc-list>
`; `;
} }

View File

@@ -56,46 +56,48 @@ export class DemoDateTimeDateTimeShortYear extends LitElement {
<div class="center">12 Hours</div> <div class="center">12 Hours</div>
<div class="center">24 Hours</div> <div class="center">24 Hours</div>
</div> </div>
${Object.entries(translationMetadata.translations).map( ${Object.entries(translationMetadata.translations)
([key, value]) => html` .filter(([key, _]) => key !== "test")
<div class="container"> .map(
<div>${value.nativeName}</div> ([key, value]) => html`
<div class="center"> <div class="container">
${formatShortDateTimeWithYear( <div>${value.nativeName}</div>
this.date, <div class="center">
{ ${formatShortDateTimeWithYear(
...defaultLocale, this.date,
language: key, {
time_format: TimeFormat.language, ...defaultLocale,
}, language: key,
demoConfig time_format: TimeFormat.language,
)} },
demoConfig
)}
</div>
<div class="center">
${formatShortDateTimeWithYear(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatShortDateTimeWithYear(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div> </div>
<div class="center"> `
${formatShortDateTimeWithYear( )}
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatShortDateTimeWithYear(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
</mwc-list> </mwc-list>
`; `;
} }

View File

@@ -56,46 +56,48 @@ export class DemoDateTimeDateTimeShort extends LitElement {
<div class="center">12 Hours</div> <div class="center">12 Hours</div>
<div class="center">24 Hours</div> <div class="center">24 Hours</div>
</div> </div>
${Object.entries(translationMetadata.translations).map( ${Object.entries(translationMetadata.translations)
([key, value]) => html` .filter(([key, _]) => key !== "test")
<div class="container"> .map(
<div>${value.nativeName}</div> ([key, value]) => html`
<div class="center"> <div class="container">
${formatShortDateTime( <div>${value.nativeName}</div>
this.date, <div class="center">
{ ${formatShortDateTime(
...defaultLocale, this.date,
language: key, {
time_format: TimeFormat.language, ...defaultLocale,
}, language: key,
demoConfig time_format: TimeFormat.language,
)} },
demoConfig
)}
</div>
<div class="center">
${formatShortDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatShortDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div> </div>
<div class="center"> `
${formatShortDateTime( )}
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatShortDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
</mwc-list> </mwc-list>
`; `;
} }

View File

@@ -56,46 +56,48 @@ export class DemoDateTimeDateTime extends LitElement {
<div class="center">12 Hours</div> <div class="center">12 Hours</div>
<div class="center">24 Hours</div> <div class="center">24 Hours</div>
</div> </div>
${Object.entries(translationMetadata.translations).map( ${Object.entries(translationMetadata.translations)
([key, value]) => html` .filter(([key, _]) => key !== "test")
<div class="container"> .map(
<div>${value.nativeName}</div> ([key, value]) => html`
<div class="center"> <div class="container">
${formatDateTime( <div>${value.nativeName}</div>
this.date, <div class="center">
{ ${formatDateTime(
...defaultLocale, this.date,
language: key, {
time_format: TimeFormat.language, ...defaultLocale,
}, language: key,
demoConfig time_format: TimeFormat.language,
)} },
demoConfig
)}
</div>
<div class="center">
${formatDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div> </div>
<div class="center"> `
${formatDateTime( )}
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
</mwc-list> </mwc-list>
`; `;
} }

View File

@@ -35,57 +35,59 @@ export class DemoDateTimeDate extends LitElement {
<div class="center">Month-Day-Year</div> <div class="center">Month-Day-Year</div>
<div class="center">Year-Month-Day</div> <div class="center">Year-Month-Day</div>
</div> </div>
${Object.entries(translationMetadata.translations).map( ${Object.entries(translationMetadata.translations)
([key, value]) => html` .filter(([key, _]) => key !== "test")
<div class="container"> .map(
<div>${value.nativeName}</div> ([key, value]) => html`
<div class="center"> <div class="container">
${formatDateNumeric( <div>${value.nativeName}</div>
date, <div class="center">
{ ${formatDateNumeric(
...defaultLocale, date,
language: key, {
date_format: DateFormat.language, ...defaultLocale,
}, language: key,
demoConfig date_format: DateFormat.language,
)} },
demoConfig
)}
</div>
<div class="center">
${formatDateNumeric(
date,
{
...defaultLocale,
language: key,
date_format: DateFormat.DMY,
},
demoConfig
)}
</div>
<div class="center">
${formatDateNumeric(
date,
{
...defaultLocale,
language: key,
date_format: DateFormat.MDY,
},
demoConfig
)}
</div>
<div class="center">
${formatDateNumeric(
date,
{
...defaultLocale,
language: key,
date_format: DateFormat.YMD,
},
demoConfig
)}
</div>
</div> </div>
<div class="center"> `
${formatDateNumeric( )}
date,
{
...defaultLocale,
language: key,
date_format: DateFormat.DMY,
},
demoConfig
)}
</div>
<div class="center">
${formatDateNumeric(
date,
{
...defaultLocale,
language: key,
date_format: DateFormat.MDY,
},
demoConfig
)}
</div>
<div class="center">
${formatDateNumeric(
date,
{
...defaultLocale,
language: key,
date_format: DateFormat.YMD,
},
demoConfig
)}
</div>
</div>
`
)}
</mwc-list> </mwc-list>
`; `;
} }

View File

@@ -56,46 +56,48 @@ export class DemoDateTimeTimeSeconds extends LitElement {
<div class="center">12 Hours</div> <div class="center">12 Hours</div>
<div class="center">24 Hours</div> <div class="center">24 Hours</div>
</div> </div>
${Object.entries(translationMetadata.translations).map( ${Object.entries(translationMetadata.translations)
([key, value]) => html` .filter(([key, _]) => key !== "test")
<div class="container"> .map(
<div>${value.nativeName}</div> ([key, value]) => html`
<div class="center"> <div class="container">
${formatTimeWithSeconds( <div>${value.nativeName}</div>
this.date, <div class="center">
{ ${formatTimeWithSeconds(
...defaultLocale, this.date,
language: key, {
time_format: TimeFormat.language, ...defaultLocale,
}, language: key,
demoConfig time_format: TimeFormat.language,
)} },
demoConfig
)}
</div>
<div class="center">
${formatTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div> </div>
<div class="center"> `
${formatTimeWithSeconds( )}
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
</mwc-list> </mwc-list>
`; `;
} }

View File

@@ -56,46 +56,48 @@ export class DemoDateTimeTimeWeekday extends LitElement {
<div class="center">12 Hours</div> <div class="center">12 Hours</div>
<div class="center">24 Hours</div> <div class="center">24 Hours</div>
</div> </div>
${Object.entries(translationMetadata.translations).map( ${Object.entries(translationMetadata.translations)
([key, value]) => html` .filter(([key, _]) => key !== "test")
<div class="container"> .map(
<div>${value.nativeName}</div> ([key, value]) => html`
<div class="center"> <div class="container">
${formatTimeWeekday( <div>${value.nativeName}</div>
this.date, <div class="center">
{ ${formatTimeWeekday(
...defaultLocale, this.date,
language: key, {
time_format: TimeFormat.language, ...defaultLocale,
}, language: key,
demoConfig time_format: TimeFormat.language,
)} },
demoConfig
)}
</div>
<div class="center">
${formatTimeWeekday(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatTimeWeekday(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div> </div>
<div class="center"> `
${formatTimeWeekday( )}
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatTimeWeekday(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
</mwc-list> </mwc-list>
`; `;
} }

View File

@@ -56,46 +56,48 @@ export class DemoDateTimeTime extends LitElement {
<div class="center">12 Hours</div> <div class="center">12 Hours</div>
<div class="center">24 Hours</div> <div class="center">24 Hours</div>
</div> </div>
${Object.entries(translationMetadata.translations).map( ${Object.entries(translationMetadata.translations)
([key, value]) => html` .filter(([key, _]) => key !== "test")
<div class="container"> .map(
<div>${value.nativeName}</div> ([key, value]) => html`
<div class="center"> <div class="container">
${formatTime( <div>${value.nativeName}</div>
this.date, <div class="center">
{ ${formatTime(
...defaultLocale, this.date,
language: key, {
time_format: TimeFormat.language, ...defaultLocale,
}, language: key,
demoConfig time_format: TimeFormat.language,
)} },
demoConfig
)}
</div>
<div class="center">
${formatTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div> </div>
<div class="center"> `
${formatTime( )}
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
</mwc-list> </mwc-list>
`; `;
} }

View File

@@ -287,11 +287,11 @@ const CONFIGS = [
config: ` config: `
- type: entities - type: entities
entities: entities:
- type: perform-action - type: call-service
icon: mdi:power icon: mdi:power
name: Bed light name: Bed light
action_name: Toggle light action_name: Toggle light
action: light.toggle service: light.toggle
data: data:
entity_id: light.bed_light entity_id: light.bed_light
- type: section - type: section

View File

@@ -1,3 +0,0 @@
---
title: Picture Card
---

View File

@@ -1,61 +0,0 @@
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, query } from "lit/decorators";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
const ENTITIES = [
getEntity("person", "paulus", "home", {
friendly_name: "Paulus",
entity_picture: "/images/paulus.jpg",
}),
];
const CONFIGS = [
{
heading: "Image URL",
config: `
- type: picture
image: /images/living_room.png
`,
},
{
heading: "Person entity",
config: `
- type: picture
image_entity: person.paulus
`,
},
{
heading: "Error: Image required",
config: `
- type: picture
entity: person.paulus
`,
},
];
@customElement("demo-lovelace-picture-card")
class DemoPicture extends LitElement {
@query("#demos") private _demoRoot!: HTMLElement;
protected render(): TemplateResult {
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
}
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");
hass.updateTranslations("lovelace", "en");
hass.addEntities(ENTITIES);
mockIcons(hass);
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-lovelace-picture-card": DemoPicture;
}
}

View File

@@ -25,15 +25,6 @@ const ENTITIES = [
friendly_name: "Movement Backyard", friendly_name: "Movement Backyard",
device_class: "motion", device_class: "motion",
}), }),
getEntity("person", "paulus", "home", {
friendly_name: "Paulus",
entity_picture: "/images/paulus.jpg",
}),
getEntity("sensor", "battery", 35, {
device_class: "battery",
friendly_name: "Battery",
unit_of_measurement: "%",
}),
]; ];
const CONFIGS = [ const CONFIGS = [
@@ -132,19 +123,6 @@ const CONFIGS = [
left: 35% left: 35%
`, `,
}, },
{
heading: "Person entity",
config: `
- type: picture-elements
image_entity: person.paulus
elements:
- type: state-icon
entity: sensor.battery
style:
top: 8%
left: 8%
`,
},
]; ];
@customElement("demo-lovelace-picture-elements-card") @customElement("demo-lovelace-picture-elements-card")

View File

@@ -12,10 +12,6 @@ const ENTITIES = [
getEntity("light", "bed_light", "off", { getEntity("light", "bed_light", "off", {
friendly_name: "Bed Light", friendly_name: "Bed Light",
}), }),
getEntity("person", "paulus", "home", {
friendly_name: "Paulus",
entity_picture: "/images/paulus.jpg",
}),
]; ];
const CONFIGS = [ const CONFIGS = [
@@ -54,13 +50,6 @@ const CONFIGS = [
entity: camera.demo_camera entity: camera.demo_camera
`, `,
}, },
{
heading: "Person entity",
config: `
- type: picture-entity
entity: person.paulus
`,
},
{ {
heading: "Hidden name", heading: "Hidden name",
config: ` config: `

View File

@@ -20,15 +20,6 @@ const ENTITIES = [
friendly_name: "Basement Floor Wet", friendly_name: "Basement Floor Wet",
device_class: "moisture", device_class: "moisture",
}), }),
getEntity("person", "paulus", "home", {
friendly_name: "Paulus",
entity_picture: "/images/paulus.jpg",
}),
getEntity("sensor", "battery", 35, {
device_class: "battery",
friendly_name: "Battery",
unit_of_measurement: "%",
}),
]; ];
const CONFIGS = [ const CONFIGS = [
@@ -99,15 +90,6 @@ const CONFIGS = [
- light.ceiling_lights - light.ceiling_lights
`, `,
}, },
{
heading: "Person entity",
config: `
- type: picture-glance
image_entity: person.paulus
entities:
- sensor.battery
`,
},
{ {
heading: "Custom icon", heading: "Custom icon",
config: ` config: `

View File

@@ -2,13 +2,11 @@ import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, query } from "lit/decorators"; import { customElement, query } from "lit/decorators";
import { CoverEntityFeature } from "../../../../src/data/cover"; import { CoverEntityFeature } from "../../../../src/data/cover";
import { LightColorMode } from "../../../../src/data/light"; import { LightColorMode } from "../../../../src/data/light";
import { LockEntityFeature } from "../../../../src/data/lock";
import { VacuumEntityFeature } from "../../../../src/data/vacuum"; import { VacuumEntityFeature } from "../../../../src/data/vacuum";
import { getEntity } from "../../../../src/fake_data/entity"; import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass"; import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards"; import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons"; import { mockIcons } from "../../../../demo/src/stubs/icons";
import { ClimateEntityFeature } from "../../../../src/data/climate";
const ENTITIES = [ const ENTITIES = [
getEntity("switch", "tv_outlet", "on", { getEntity("switch", "tv_outlet", "on", {
@@ -22,11 +20,6 @@ const ENTITIES = [
getEntity("light", "unavailable", "unavailable", { getEntity("light", "unavailable", "unavailable", {
friendly_name: "Unavailable entity", friendly_name: "Unavailable entity",
}), }),
getEntity("lock", "front_door", "locked", {
friendly_name: "Front Door Lock",
device_class: "lock",
supported_features: LockEntityFeature.OPEN,
}),
getEntity("climate", "thermostat", "heat", { getEntity("climate", "thermostat", "heat", {
current_temperature: 73, current_temperature: 73,
min_temp: 45, min_temp: 45,
@@ -61,36 +54,6 @@ const ENTITIES = [
CoverEntityFeature.OPEN_TILT + CoverEntityFeature.OPEN_TILT +
CoverEntityFeature.STOP_TILT, CoverEntityFeature.STOP_TILT,
}), }),
getEntity("input_number", "counter", "1.0", {
friendly_name: "Counter",
initial: 0,
min: 0,
max: 100,
step: 1,
mode: "slider",
}),
getEntity("climate", "dual_thermostat", "heat/cool", {
friendly_name: "Dual thermostat",
hvac_modes: ["off", "cool", "heat_cool", "auto", "dry", "fan_only"],
min_temp: 7,
max_temp: 35,
fan_modes: ["on_low", "on_high", "auto_low", "auto_high", "off"],
preset_modes: ["home", "eco", "away"],
swing_modes: ["auto", "1", "2", "3", "off"],
current_temperature: 23,
target_temp_high: 24,
target_temp_low: 21,
fan_mode: "auto_low",
preset_mode: "home",
swing_mode: "auto",
supported_features:
ClimateEntityFeature.TURN_ON +
ClimateEntityFeature.TURN_OFF +
ClimateEntityFeature.SWING_MODE +
ClimateEntityFeature.PRESET_MODE +
ClimateEntityFeature.FAN_MODE +
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
}),
]; ];
const CONFIGS = [ const CONFIGS = [
@@ -175,24 +138,6 @@ const CONFIGS = [
- type: "color-temp" - type: "color-temp"
`, `,
}, },
{
heading: "Lock commands feature",
config: `
- type: tile
entity: lock.front_door
features:
- type: "lock-commands"
`,
},
{
heading: "Lock open door feature",
config: `
- type: tile
entity: lock.front_door
features:
- type: "lock-open-door"
`,
},
{ {
heading: "Vacuum commands feature", heading: "Vacuum commands feature",
config: ` config: `
@@ -224,25 +169,6 @@ const CONFIGS = [
- type: "cover-tilt" - type: "cover-tilt"
`, `,
}, },
{
heading: "Number buttons feature",
config: `
- type: tile
entity: input_number.counter
features:
- type: numeric-input
style: buttons
`,
},
{
heading: "Dual thermostat feature",
config: `
- type: tile
entity: climate.dual_thermostat
features:
- type: target-temperature
`,
},
]; ];
@customElement("demo-lovelace-tile-card") @customElement("demo-lovelace-tile-card")

View File

@@ -140,9 +140,6 @@ const ENTITIES: HassEntity[] = [
createEntity("climate.auto_preheating", "auto", undefined, { createEntity("climate.auto_preheating", "auto", undefined, {
hvac_action: "preheating", hvac_action: "preheating",
}), }),
createEntity("climate.auto_defrosting", "auto", undefined, {
hvac_action: "defrosting",
}),
createEntity("climate.auto_heating", "auto", undefined, { createEntity("climate.auto_heating", "auto", undefined, {
hvac_action: "heating", hvac_action: "heating",
}), }),
@@ -358,18 +355,19 @@ export class DemoEntityState extends LitElement {
}, },
entity_id: { entity_id: {
title: "Entity ID", title: "Entity ID",
width: "30%",
filterable: true, filterable: true,
sortable: true, sortable: true,
}, },
state: { state: {
title: "State", title: "State",
width: "20%",
sortable: true, sortable: true,
template: (entry) => template: (entry) =>
html`${computeStateDisplay( html`${computeStateDisplay(
hass.localize, hass.localize,
entry.stateObj, entry.stateObj,
hass.locale, hass.locale,
[], // numericDeviceClasses
hass.config, hass.config,
hass.entities hass.entities
)}`, )}`,
@@ -377,12 +375,14 @@ export class DemoEntityState extends LitElement {
device_class: { device_class: {
title: "Device class", title: "Device class",
template: (entry) => html`${entry.device_class ?? "-"}`, template: (entry) => html`${entry.device_class ?? "-"}`,
width: "20%",
filterable: true, filterable: true,
sortable: true, sortable: true,
}, },
domain: { domain: {
title: "Domain", title: "Domain",
template: (entry) => html`${computeDomain(entry.entity_id)}`, template: (entry) => html`${computeDomain(entry.entity_id)}`,
width: "20%",
filterable: true, filterable: true,
sortable: true, sortable: true,
}, },

View File

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

View File

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

View File

@@ -36,8 +36,6 @@ const createConfigEntry = (
pref_disable_new_entities: false, pref_disable_new_entities: false,
pref_disable_polling: false, pref_disable_polling: false,
reason: null, reason: null,
error_reason_translation_key: null,
error_reason_translation_placeholders: null,
...override, ...override,
}); });
@@ -203,8 +201,6 @@ const createEntityRegistryEntries = (
options: null, options: null,
labels: [], labels: [],
categories: {}, categories: {},
created_at: 0,
modified_at: 0,
}, },
]; ];
@@ -217,7 +213,6 @@ const createDeviceRegistryEntries = (
connections: [], connections: [],
manufacturer: "ESPHome", manufacturer: "ESPHome",
model: "Mock Device", model: "Mock Device",
model_id: "ABC-001",
name: "Tag Reader", name: "Tag Reader",
sw_version: null, sw_version: null,
hw_version: "1.0.0", hw_version: "1.0.0",
@@ -230,9 +225,6 @@ const createDeviceRegistryEntries = (
disabled_by: null, disabled_by: null,
configuration_url: null, configuration_url: null,
labels: [], labels: [],
created_at: 0,
modified_at: 0,
primary_config_entry: null,
}, },
]; ];

View File

@@ -1,7 +1,4 @@
import { globIterate } from "glob"; import { globIterate } from "glob";
import { availableParallelism } from "node:os";
process.env.UV_THREADPOOL_SIZE = availableParallelism();
const gulpImports = []; const gulpImports = [];

View File

@@ -127,13 +127,14 @@ export class HassioBackups extends LitElement {
main: true, main: true,
sortable: true, sortable: true,
filterable: true, filterable: true,
flex: 2, grows: true,
template: (backup) => template: (backup) =>
html`${backup.name || backup.slug} html`${backup.name || backup.slug}
<div class="secondary">${backup.secondary}</div>`, <div class="secondary">${backup.secondary}</div>`,
}, },
size: { size: {
title: this.supervisor.localize("backup.size"), title: this.supervisor.localize("backup.size"),
width: "15%",
hidden: narrow, hidden: narrow,
filterable: true, filterable: true,
sortable: true, sortable: true,
@@ -141,6 +142,7 @@ export class HassioBackups extends LitElement {
}, },
location: { location: {
title: this.supervisor.localize("backup.location"), title: this.supervisor.localize("backup.location"),
width: "15%",
hidden: narrow, hidden: narrow,
filterable: true, filterable: true,
sortable: true, sortable: true,
@@ -149,6 +151,7 @@ export class HassioBackups extends LitElement {
}, },
date: { date: {
title: this.supervisor.localize("backup.created"), title: this.supervisor.localize("backup.created"),
width: "15%",
direction: "desc", direction: "desc",
hidden: narrow, hidden: narrow,
filterable: true, filterable: true,

View File

@@ -1,8 +1,6 @@
import type { IFuseOptions } from "fuse.js";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import { stripDiacritics } from "../../../src/common/string/strip-diacritics"; import type { IFuseOptions } from "fuse.js";
import { StoreAddon } from "../../../src/data/supervisor/store"; import { StoreAddon } from "../../../src/data/supervisor/store";
import { getStripDiacriticsFn } from "../../../src/util/fuse";
export function filterAndSort(addons: StoreAddon[], filter: string) { export function filterAndSort(addons: StoreAddon[], filter: string) {
const options: IFuseOptions<StoreAddon> = { const options: IFuseOptions<StoreAddon> = {
@@ -10,8 +8,7 @@ export function filterAndSort(addons: StoreAddon[], filter: string) {
isCaseSensitive: false, isCaseSensitive: false,
minMatchCharLength: Math.min(filter.length, 2), minMatchCharLength: Math.min(filter.length, 2),
threshold: 0.2, threshold: 0.2,
getFn: getStripDiacriticsFn,
}; };
const fuse = new Fuse(addons, options); const fuse = new Fuse(addons, options);
return fuse.search(stripDiacritics(filter)).map((result) => result.item); return fuse.search(filter).map((result) => result.item);
} }

View File

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

View File

@@ -1,19 +1,19 @@
import { mdiRefresh, mdiStorePlus } from "@mdi/js"; import { mdiStorePlus, mdiUpdate } from "@mdi/js";
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version"; import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/ha-fab"; import "../../../src/components/ha-fab";
import { reloadHassioAddons } from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-subpage";
import "../../../src/layouts/hass-tabs-subpage"; import "../../../src/layouts/hass-tabs-subpage";
import { haStyle } from "../../../src/resources/styles"; import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../src/types"; import { HomeAssistant, Route } from "../../../src/types";
import { supervisorTabs } from "../hassio-tabs"; import { supervisorTabs } from "../hassio-tabs";
import "./hassio-addons"; import "./hassio-addons";
import "../../../src/layouts/hass-subpage";
import { reloadHassioAddons } from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import { fireEvent } from "../../../src/common/dom/fire_event";
@customElement("hassio-dashboard") @customElement("hassio-dashboard")
class HassioDashboard extends LitElement { class HassioDashboard extends LitElement {
@@ -43,7 +43,7 @@ class HassioDashboard extends LitElement {
<ha-icon-button <ha-icon-button
slot="toolbar-icon" slot="toolbar-icon"
@click=${this._handleCheckUpdates} @click=${this._handleCheckUpdates}
.path=${mdiRefresh} .path=${mdiUpdate}
.label=${this.supervisor.localize("store.check_updates")} .label=${this.supervisor.localize("store.check_updates")}
></ha-icon-button> ></ha-icon-button>
<hassio-addons <hassio-addons

View File

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

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