20231025.0 (#18395)

This commit is contained in:
Bram Kragten 2023-10-25 12:17:22 +02:00 committed by GitHub
commit fdaefadd18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
276 changed files with 9587 additions and 5309 deletions

View File

@ -21,7 +21,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.0
uses: actions/checkout@v4.1.1
with:
ref: dev
@ -57,7 +57,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.0
uses: actions/checkout@v4.1.1
with:
ref: master

View File

@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.0
uses: actions/checkout@v4.1.1
- name: Setup Node
uses: actions/setup-node@v3.8.1
with:
@ -55,7 +55,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.0
uses: actions/checkout@v4.1.1
- name: Setup Node
uses: actions/setup-node@v3.8.1
with:
@ -73,7 +73,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.0
uses: actions/checkout@v4.1.1
- name: Setup Node
uses: actions/setup-node@v3.8.1
with:
@ -85,13 +85,19 @@ jobs:
run: ./node_modules/.bin/gulp build-app
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@v3.1.3
with:
name: frontend-bundle-stats
path: build/stats/*.json
if-no-files-found: error
supervisor:
name: Build supervisor
needs: [lint, test]
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.0
uses: actions/checkout@v4.1.1
- name: Setup Node
uses: actions/setup-node@v3.8.1
with:
@ -103,3 +109,9 @@ jobs:
run: ./node_modules/.bin/gulp build-hassio
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@v3.1.3
with:
name: supervisor-bundle-stats
path: build/stats/*.json
if-no-files-found: error

View File

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

View File

@ -22,7 +22,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.0
uses: actions/checkout@v4.1.1
with:
ref: dev
@ -58,7 +58,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.0
uses: actions/checkout@v4.1.1
with:
ref: master

View File

@ -16,7 +16,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.0
uses: actions/checkout@v4.1.1
- name: Setup Node
uses: actions/setup-node@v3.8.1

View File

@ -21,7 +21,7 @@ jobs:
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.0
uses: actions/checkout@v4.1.1
- name: Setup Node
uses: actions/setup-node@v3.8.1

View File

@ -20,7 +20,7 @@ jobs:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.0
uses: actions/checkout@v4.1.1
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v4
@ -57,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v3.1.3
with:
name: wheels
path: dist/home_assistant_frontend*.whl
if-no-files-found: error
- name: Upload translations
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v3.1.3
with:
name: translations
path: translations.tar.gz

25
.github/workflows/relative-ci.yaml vendored Normal file
View File

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

View File

@ -23,7 +23,7 @@ jobs:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.0
uses: actions/checkout@v4.1.1
- name: Verify version
uses: home-assistant/actions/helpers/verify-version@master
@ -74,7 +74,7 @@ jobs:
echo "home-assistant-frontend==$version" > ./requirements.txt
- name: Build wheels
uses: home-assistant/wheels@2023.10.1
uses: home-assistant/wheels@2023.10.5
with:
abi: cp311
tag: musllinux_1_2

View File

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

3
.gitignore vendored
View File

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

File diff suppressed because one or more lines are too long

View File

@ -8,4 +8,4 @@ plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
yarnPath: .yarn/releases/yarn-3.6.3.cjs
yarnPath: .yarn/releases/yarn-3.6.4.cjs

View File

@ -98,7 +98,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
"@babel/preset-env",
{
useBuiltIns: latestBuild ? false : "entry",
corejs: latestBuild ? false : { version: "3.32", proposals: true },
corejs: latestBuild ? false : { version: "3.33", proposals: true },
bugfixes: true,
shippedProposals: true,
},
@ -149,7 +149,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
sourceMaps: !isTestBuild,
});
const nameSuffix = (latestBuild) => (latestBuild ? "-latest" : "-es5");
const nameSuffix = (latestBuild) => (latestBuild ? "-modern" : "-legacy");
const outputPath = (outputRoot, latestBuild) =>
path.resolve(outputRoot, latestBuild ? "frontend_latest" : "frontend_es5");
@ -183,7 +183,7 @@ const publicPath = (latestBuild, root = "") =>
module.exports.config = {
app({ isProdBuild, latestBuild, isStatsBuild, isTestBuild, isWDS }) {
return {
name: "app" + nameSuffix(latestBuild),
name: "frontend" + nameSuffix(latestBuild),
entry: {
service_worker: "./src/entrypoints/service_worker.ts",
app: "./src/entrypoints/app.ts",

View File

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

View File

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

View File

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

View File

@ -1,6 +1,8 @@
const { existsSync } = require("fs");
const path = require("path");
const webpack = require("webpack");
const { StatsWriterPlugin } = require("webpack-stats-plugin");
const filterStats = require("@bundle-stats/plugin-webpack-filter").default;
const TerserPlugin = require("terser-webpack-plugin");
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
const log = require("fancy-log");
@ -152,6 +154,15 @@ const createWebpackConfig = ({
)
),
!isProdBuild && new LogStartCompilePlugin(),
isProdBuild &&
new StatsWriterPlugin({
filename: path.relative(
outputPath,
path.join(paths.build_dir, "stats", `${name}.json`)
),
stats: { assets: true, chunks: true, modules: true },
transform: (stats) => JSON.stringify(filterStats(stats)),
}),
].filter(Boolean),
resolve: {
extensions: [".ts", ".js", ".json"],
@ -171,6 +182,8 @@ const createWebpackConfig = ({
"@lit-labs/virtualizer/layouts/grid.js",
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver":
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js",
"@lit-labs/observers/resize-controller":
"@lit-labs/observers/resize-controller.js",
},
},
output: {
@ -183,6 +196,7 @@ const createWebpackConfig = ({
isProdBuild && !isStatsBuild ? "[id]-[contenthash].js" : "[name].js",
assetModuleFilename:
isProdBuild && !isStatsBuild ? "[id]-[contenthash][ext]" : "[id][ext]",
crossOriginLoading: "use-credentials",
hashFunction: "xxhash64",
hashDigest: "base64url",
hashDigestLength: 11, // full length of 64 bit base64url

View File

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

View File

@ -100,7 +100,9 @@ export class HcMain extends HassElement {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
import("../second-load");
import("./hc-lovelace");
import("../../../../src/resources/ha-style");
window.addEventListener("location-changed", () => {
const panelPath = `/${this._urlPath || "lovelace"}/`;
if (location.pathname.startsWith(panelPath)) {
@ -260,7 +262,6 @@ export class HcMain extends HassElement {
{
strategy: {
type: "energy",
show_date_selection: true,
},
},
],
@ -308,7 +309,7 @@ export class HcMain extends HassElement {
? await fetchResources(this.hass!.connection)
: (this._lovelaceConfig as LegacyLovelaceConfig).resources;
if (resources) {
loadLovelaceResources(resources, this.hass!.auth.data.hassUrl);
loadLovelaceResources(resources, this.hass!);
}
}
@ -324,8 +325,7 @@ export class HcMain extends HassElement {
{
type: DEFAULT_STRATEGY,
},
this.hass!,
{ narrow: false }
this.hass!
)
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,7 +22,7 @@ import { mockLovelace } from "./stubs/lovelace";
import { mockMediaPlayer } from "./stubs/media_player";
import { mockPersistentNotification } from "./stubs/persistent_notification";
import { mockRecorder } from "./stubs/recorder";
import { mockShoppingList } from "./stubs/shopping_list";
import { mockTodo } from "./stubs/todo";
import { mockSystemLog } from "./stubs/system_log";
import { mockTemplate } from "./stubs/template";
import { mockTranslations } from "./stubs/translations";
@ -49,7 +49,7 @@ export class HaDemo extends HomeAssistantAppEl {
mockTranslations(hass);
mockHistory(hass);
mockRecorder(hass);
mockShoppingList(hass);
mockTodo(hass);
mockSystemLog(hass);
mockTemplate(hass);
mockEvents(hass);

View File

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

24
demo/src/stubs/todo.ts Normal file
View File

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

View File

@ -1,5 +1,5 @@
import "../../src/resources/ha-style";
import "../../src/resources/roboto";
import "./ha-gallery";
import("../../src/resources/ha-style");
document.body.appendChild(document.createElement("ha-gallery"));

View File

@ -49,11 +49,11 @@ export class DemoHaCircularSlider extends LitElement {
<div class="field">
<p>Current</p>
<ha-slider
labeled
min="10"
max="30"
.value=${this.current}
@change=${this._currentChanged}
pin
></ha-slider>
<p>${this.current} °C</p>
</div>

View File

@ -57,6 +57,7 @@ const DEVICES = [
sw_version: null,
hw_version: null,
via_device_id: null,
serial_number: null,
},
{
area_id: "backyard",
@ -74,6 +75,7 @@ const DEVICES = [
sw_version: null,
hw_version: null,
via_device_id: null,
serial_number: null,
},
{
area_id: null,
@ -91,6 +93,7 @@ const DEVICES = [
sw_version: null,
hw_version: null,
via_device_id: null,
serial_number: null,
},
];

View File

@ -57,8 +57,8 @@ export class DemoHaHsColorPicker extends LitElement {
></ha-hs-color-picker>
<p>Hue : ${this.value[0]}</p>
<ha-slider
labeled
step="1"
pin
min="0"
max="360"
.value=${this.value[0]}
@ -67,8 +67,8 @@ export class DemoHaHsColorPicker extends LitElement {
</ha-slider>
<p>Saturation : ${this.value[1]}</p>
<ha-slider
labeled
step="0.01"
pin
min="0"
max="1"
.value=${this.value[1]}
@ -77,8 +77,8 @@ export class DemoHaHsColorPicker extends LitElement {
</ha-slider>
<p>Color Brighness : ${this.brightness}</p>
<ha-slider
labeled
step="1"
pin
min="0"
max="255"
.value=${this.brightness}

View File

@ -53,6 +53,7 @@ const DEVICES = [
sw_version: null,
hw_version: null,
via_device_id: null,
serial_number: null,
},
{
area_id: "backyard",
@ -70,6 +71,7 @@ const DEVICES = [
sw_version: null,
hw_version: null,
via_device_id: null,
serial_number: null,
},
{
area_id: null,
@ -87,6 +89,7 @@ const DEVICES = [
sw_version: null,
hw_version: null,
via_device_id: null,
serial_number: null,
},
];

View File

@ -1,3 +0,0 @@
---
title: Shopping List Card
---

View File

@ -0,0 +1,3 @@
---
title: Todo List Card
---

View File

@ -2,25 +2,39 @@ import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, query } from "lit/decorators";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards";
import { getEntity } from "../../../../src/fake_data/entity";
import { mockTodo } from "../../../../demo/src/stubs/todo";
const ENTITIES = [
getEntity("todo", "shopping_list", "2", {
friendly_name: "Shopping List",
supported_features: 15,
}),
getEntity("todo", "read_only", "2", {
friendly_name: "Read only",
}),
];
const CONFIGS = [
{
heading: "List example",
config: `
- type: shopping-list
- type: todo-list
entity: todo.shopping_list
`,
},
{
heading: "List with title example",
config: `
- type: shopping-list
- type: todo-list
title: Shopping List
entity: todo.read_only
`,
},
];
@customElement("demo-lovelace-shopping-list-card")
class DemoShoppingListEntity extends LitElement {
@customElement("demo-lovelace-todo-list-card")
class DemoTodoListEntity extends LitElement {
@query("#demos") private _demoRoot!: HTMLElement;
protected render(): TemplateResult {
@ -32,18 +46,14 @@ class DemoShoppingListEntity extends LitElement {
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");
hass.updateTranslations("lovelace", "en");
hass.addEntities(ENTITIES);
hass.mockAPI("shopping_list", () => [
{ name: "list", id: 1, complete: false },
{ name: "all", id: 2, complete: false },
{ name: "the", id: 3, complete: false },
{ name: "things", id: 4, complete: true },
]);
mockTodo(hass);
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-lovelace-shopping-list-card": DemoShoppingListEntity;
"demo-lovelace-todo-list-card": DemoTodoListEntity;
}
}

View File

@ -213,6 +213,7 @@ const createDeviceRegistryEntries = (
name: "Tag Reader",
sw_version: null,
hw_version: "1.0.0",
serial_number: "00_12_4B_00_22_98_88_7F",
id: "mock-device-id",
identifiers: [],
via_device_id: null,

View File

@ -1,4 +1,5 @@
import { mdiClose } from "@mdi/js";
import { dump } from "js-yaml";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
@ -9,7 +10,6 @@ import "../../../../src/components/ha-expansion-panel";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/search-input";
import { HassioHardwareInfo } from "../../../../src/data/hassio/hardware";
import { dump } from "../../../../src/resources/js-yaml-dump";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { HassioHardwareDialogParams } from "./show-dialog-hassio-hardware";

View File

@ -1,15 +1,15 @@
// Compat needs to be first import
import "../../src/resources/compatibility";
import { setCancelSyntheticClickEvents } from "@polymer/polymer/lib/utils/settings";
import "../../src/resources/roboto";
import "../../src/resources/ha-style";
import "../../src/resources/safari-14-attachshadow-patch";
import "./hassio-main";
setCancelSyntheticClickEvents(false);
import("../../src/resources/ha-style");
import("@polymer/polymer/lib/utils/settings").then(
({ setCancelSyntheticClickEvents }) => setCancelSyntheticClickEvents(false)
);
const styleEl = document.createElement("style");
styleEl.innerHTML = `
styleEl.textContent = `
body {
font-family: Roboto, sans-serif;
-moz-osx-font-smoothing: grayscale;

View File

@ -25,24 +25,24 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "7.23.1",
"@babel/runtime": "7.23.2",
"@braintree/sanitize-url": "6.0.4",
"@codemirror/autocomplete": "6.9.1",
"@codemirror/autocomplete": "6.10.2",
"@codemirror/commands": "6.3.0",
"@codemirror/language": "6.9.1",
"@codemirror/legacy-modes": "6.3.3",
"@codemirror/search": "6.5.4",
"@codemirror/state": "6.2.1",
"@codemirror/view": "6.21.1",
"@codemirror/state": "6.3.1",
"@codemirror/view": "6.21.3",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.10.3",
"@formatjs/intl-displaynames": "6.5.2",
"@formatjs/intl-getcanonicallocales": "2.2.1",
"@formatjs/intl-listformat": "7.4.2",
"@formatjs/intl-locale": "3.3.4",
"@formatjs/intl-numberformat": "8.7.2",
"@formatjs/intl-pluralrules": "5.2.6",
"@formatjs/intl-relativetimeformat": "11.2.6",
"@formatjs/intl-datetimeformat": "6.11.0",
"@formatjs/intl-displaynames": "6.6.0",
"@formatjs/intl-getcanonicallocales": "2.3.0",
"@formatjs/intl-listformat": "7.5.0",
"@formatjs/intl-locale": "3.4.0",
"@formatjs/intl-numberformat": "8.8.0",
"@formatjs/intl-pluralrules": "5.2.7",
"@formatjs/intl-relativetimeformat": "11.2.7",
"@fullcalendar/core": "6.1.9",
"@fullcalendar/daygrid": "6.1.9",
"@fullcalendar/interaction": "6.1.9",
@ -52,10 +52,12 @@
"@lezer/highlight": "1.1.6",
"@lit-labs/context": "0.4.1",
"@lit-labs/motion": "1.0.4",
"@lit-labs/observers": "2.0.1",
"@lit-labs/virtualizer": "2.0.7",
"@lrnwebcomponents/simple-tooltip": "7.0.18",
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
"@material/mwc-base": "0.27.0",
"@material/mwc-button": "0.27.0",
"@material/mwc-checkbox": "0.27.0",
"@material/mwc-circular-progress": "0.27.0",
@ -71,7 +73,6 @@
"@material/mwc-radio": "0.27.0",
"@material/mwc-ripple": "0.27.0",
"@material/mwc-select": "0.27.0",
"@material/mwc-slider": "0.27.0",
"@material/mwc-switch": "0.27.0",
"@material/mwc-tab": "0.27.0",
"@material/mwc-tab-bar": "0.27.0",
@ -80,22 +81,21 @@
"@material/mwc-top-app-bar": "0.27.0",
"@material/mwc-top-app-bar-fixed": "0.27.0",
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
"@material/web": "=1.0.0",
"@mdi/js": "7.2.96",
"@mdi/svg": "7.2.96",
"@material/web": "=1.0.1",
"@mdi/js": "7.3.67",
"@mdi/svg": "7.3.67",
"@polymer/iron-flex-layout": "3.0.1",
"@polymer/iron-input": "3.0.1",
"@polymer/iron-resizable-behavior": "3.0.1",
"@polymer/paper-input": "3.2.1",
"@polymer/paper-item": "3.0.1",
"@polymer/paper-listbox": "3.0.1",
"@polymer/paper-slider": "3.0.1",
"@polymer/paper-tabs": "3.1.0",
"@polymer/paper-toast": "3.0.1",
"@polymer/polymer": "3.5.1",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.1.10",
"@vaadin/vaadin-themable-mixin": "24.1.10",
"@vaadin/combo-box": "24.2.0",
"@vaadin/vaadin-themable-mixin": "24.2.0",
"@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@ -105,7 +105,7 @@
"app-datepicker": "5.1.1",
"chart.js": "4.4.0",
"comlink": "4.4.1",
"core-js": "3.32.2",
"core-js": "3.33.1",
"cropperjs": "1.6.1",
"date-fns": "2.30.0",
"date-fns-tz": "2.0.0",
@ -114,15 +114,15 @@
"fuse.js": "6.6.2",
"google-timezones-json": "1.2.0",
"hls.js": "1.4.12",
"home-assistant-js-websocket": "8.2.0",
"home-assistant-js-websocket": "9.1.0",
"idb-keyval": "6.2.1",
"intl-messageformat": "10.5.3",
"intl-messageformat": "10.5.4",
"js-yaml": "4.1.0",
"leaflet": "1.9.4",
"leaflet-draw": "1.0.4",
"lit": "2.8.0",
"luxon": "3.4.3",
"marked": "9.0.3",
"marked": "9.1.2",
"memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "0.3.2",
@ -141,7 +141,7 @@
"ua-parser-js": "1.0.36",
"unfetch": "5.0.0",
"vis-data": "7.1.7",
"vis-network": "9.1.6",
"vis-network": "9.1.8",
"vue": "2.7.14",
"vue2-daterange-picker": "0.6.8",
"weekstart": "2.0.0",
@ -154,48 +154,49 @@
"xss": "1.0.14"
},
"devDependencies": {
"@babel/core": "7.23.0",
"@babel/plugin-proposal-decorators": "7.23.0",
"@babel/plugin-transform-runtime": "7.22.15",
"@babel/preset-env": "7.22.20",
"@babel/preset-typescript": "7.23.0",
"@babel/core": "7.23.2",
"@babel/plugin-proposal-decorators": "7.23.2",
"@babel/plugin-transform-runtime": "7.23.2",
"@babel/preset-env": "7.23.2",
"@babel/preset-typescript": "7.23.2",
"@bundle-stats/plugin-webpack-filter": "4.7.7",
"@koa/cors": "4.0.0",
"@lokalise/node-api": "12.0.0",
"@octokit/auth-oauth-device": "6.0.1",
"@octokit/plugin-retry": "6.0.1",
"@octokit/rest": "20.0.2",
"@open-wc/dev-server-hmr": "0.1.4",
"@rollup/plugin-babel": "6.0.3",
"@rollup/plugin-commonjs": "25.0.4",
"@rollup/plugin-json": "6.0.0",
"@rollup/plugin-node-resolve": "15.2.1",
"@rollup/plugin-replace": "5.0.2",
"@types/babel__plugin-transform-runtime": "7.9.3",
"@types/chromecast-caf-receiver": "6.0.10",
"@types/chromecast-caf-sender": "1.0.6",
"@types/esprima": "4.0.4",
"@rollup/plugin-babel": "6.0.4",
"@rollup/plugin-commonjs": "25.0.7",
"@rollup/plugin-json": "6.0.1",
"@rollup/plugin-node-resolve": "15.2.3",
"@rollup/plugin-replace": "5.0.4",
"@types/babel__plugin-transform-runtime": "7.9.4",
"@types/chromecast-caf-receiver": "6.0.11",
"@types/chromecast-caf-sender": "1.0.7",
"@types/esprima": "4.0.5",
"@types/glob": "8.1.0",
"@types/html-minifier-terser": "7.0.0",
"@types/js-yaml": "4.0.6",
"@types/leaflet": "1.9.6",
"@types/leaflet-draw": "1.0.8",
"@types/luxon": "3.3.2",
"@types/mocha": "10.0.2",
"@types/qrcode": "1.5.2",
"@types/serve-handler": "6.1.2",
"@types/sortablejs": "1.15.3",
"@types/tar": "6.1.6",
"@types/ua-parser-js": "0.7.37",
"@types/html-minifier-terser": "7.0.1",
"@types/js-yaml": "4.0.8",
"@types/leaflet": "1.9.7",
"@types/leaflet-draw": "1.0.9",
"@types/luxon": "3.3.3",
"@types/mocha": "10.0.3",
"@types/qrcode": "1.5.4",
"@types/serve-handler": "6.1.3",
"@types/sortablejs": "1.15.4",
"@types/tar": "6.1.7",
"@types/ua-parser-js": "0.7.38",
"@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "6.7.3",
"@typescript-eslint/parser": "6.7.3",
"@typescript-eslint/eslint-plugin": "6.8.0",
"@typescript-eslint/parser": "6.8.0",
"@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1",
"babel-loader": "9.1.3",
"babel-plugin-template-html-minifier": "4.1.0",
"chai": "4.3.10",
"del": "7.1.0",
"eslint": "8.50.0",
"eslint": "8.52.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "17.1.0",
"eslint-config-prettier": "9.0.0",
@ -220,10 +221,10 @@
"husky": "8.0.3",
"instant-mocha": "1.5.2",
"jszip": "3.10.1",
"lint-staged": "14.0.1",
"lint-staged": "15.0.2",
"lit-analyzer": "2.0.0-pre.3",
"lodash.template": "4.5.0",
"magic-string": "0.30.4",
"magic-string": "0.30.5",
"map-stream": "0.0.7",
"mocha": "10.2.0",
"object-hash": "3.0.0",
@ -235,7 +236,7 @@
"rollup-plugin-terser": "7.0.2",
"rollup-plugin-visualizer": "5.9.2",
"serve-handler": "6.1.5",
"sinon": "16.0.0",
"sinon": "17.0.0",
"source-map-url": "0.4.1",
"systemjs": "6.14.2",
"tar": "6.2.0",
@ -244,10 +245,11 @@
"typescript": "5.2.2",
"vinyl-buffer": "1.0.1",
"vinyl-source-stream": "2.0.0",
"webpack": "5.88.2",
"webpack": "5.89.0",
"webpack-cli": "5.1.4",
"webpack-dev-server": "4.15.1",
"webpack-manifest-plugin": "5.0.0",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "5.0.2",
"workbox-build": "7.0.0"
},
@ -255,8 +257,9 @@
"resolutions": {
"@polymer/polymer": "patch:@polymer/polymer@3.5.1#./.yarn/patches/@polymer/polymer/pr-5569.patch",
"@material/mwc-button@^0.25.3": "^0.27.0",
"lit@^2.7.4 || ^3.0.0": "^2.7.4",
"sortablejs@1.15.0": "patch:sortablejs@npm%3A1.15.0#./.yarn/patches/sortablejs-npm-1.15.0-f3a393abcc.patch",
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
},
"packageManager": "yarn@3.6.3"
"packageManager": "yarn@3.6.4"
}

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20231005.0"
version = "20231025.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"

View File

@ -33,7 +33,7 @@ fi
docker run \
-v ${LOCAL_FILE}:/opt/src/${LOCAL_FILE} \
lokalise/lokalise-cli-2@sha256:f1860b26be22fa73b8c93bc5f8690f2afc867610a42de6fc27adc790e5d4425d lokalise2 \
lokalise/lokalise-cli-2:v2.6.10 lokalise2 \
--token ${LOKALISE_TOKEN} \
--project-id ${PROJECT_ID} \
file upload \

View File

@ -26,14 +26,13 @@ export class HaAuthFormString extends HaFormString {
}
ha-auth-form-string ha-icon-button {
position: absolute;
top: 1em;
right: 12px;
--mdc-icon-button-size: 24px;
color: var(--secondary-text-color);
}
ha-auth-form-string ha-icon-button {
top: 8px;
right: 8px;
inset-inline-start: initial;
inset-inline-end: 12px;
inset-inline-end: 8px;
--mdc-icon-button-size: 40px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
direction: var(--direction);
}
</style>
@ -63,7 +62,7 @@ export class HaAuthFormString extends HaFormString {
.validationMessage=${this.schema.required ? "Required" : undefined}
@input=${this._valueChanged}
@change=${this._valueChanged}
></ha-auth-textfield>
></ha-auth-textfield>
${this.renderIcon()}
</ha-auth-textfield>
`;

View File

@ -0,0 +1,9 @@
export function getAllCombinations<T>(arr: T[]) {
return arr.reduce<T[][]>(
(combinations, element) =>
combinations.concat(
combinations.map((combination) => [...combination, element])
),
[[]]
);
}

View File

@ -16,6 +16,7 @@ import {
mdiCarCoolantLevel,
mdiCash,
mdiChatSleep,
mdiClipboardList,
mdiClock,
mdiCloudUpload,
mdiCog,
@ -120,6 +121,7 @@ export const FIXED_DOMAIN_ICONS = {
siren: mdiBullhorn,
stt: mdiMicrophoneMessage,
text: mdiFormTextbox,
todo: mdiClipboardList,
time: mdiClock,
timer: mdiTimerOutline,
tts: mdiSpeakerMessage,

View File

@ -5,12 +5,15 @@ import { FrontendLocaleData, TimeZone } from "../../data/translation";
const calcZonedDate = (
date: Date,
tz: string,
fn: (date: Date, options?: any) => Date,
fn: (date: Date, options?: any) => Date | number | boolean,
options?
) => {
const inputZoned = utcToZonedTime(date, tz);
const fnZoned = fn(inputZoned, options);
return zonedTimeToUtc(fnZoned, tz);
if (fnZoned instanceof Date) {
return zonedTimeToUtc(fnZoned, tz) as Date;
}
return fnZoned;
};
export const calcDate = (
@ -21,5 +24,16 @@ export const calcDate = (
options?
) =>
locale.time_zone === TimeZone.server
? calcZonedDate(date, config.time_zone, fn, options)
? (calcZonedDate(date, config.time_zone, fn, options) as Date)
: fn(date, options);
export const calcDateProperty = (
date: Date,
fn: (date: Date, options?: any) => boolean | number,
locale: FrontendLocaleData,
config: HassConfig,
options?
) =>
locale.time_zone === TimeZone.server
? (calcZonedDate(date, config.time_zone, fn, options) as number | boolean)
: fn(date, options);

View File

@ -37,6 +37,23 @@ const formatDateMem = memoizeOne(
})
);
// Aug 10, 2021
export const formatDateShort = (
dateObj: Date,
locale: FrontendLocaleData,
config: HassConfig
) => formatDateShortMem(locale, config.time_zone).format(dateObj);
const formatDateShortMem = memoizeOne(
(locale: FrontendLocaleData, serverTimeZone: string) =>
new Intl.DateTimeFormat(locale.language, {
year: "numeric",
month: "short",
day: "numeric",
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
})
);
// 10/08/2021
export const formatDateNumeric = (
dateObj: Date,
@ -102,13 +119,13 @@ const formatDateNumericMem = memoizeOne(
);
// Aug 10
export const formatDateShort = (
export const formatDateVeryShort = (
dateObj: Date,
locale: FrontendLocaleData,
config: HassConfig
) => formatDateShortMem(locale, config.time_zone).format(dateObj);
) => formatDateVeryShortMem(locale, config.time_zone).format(dateObj);
const formatDateShortMem = memoizeOne(
const formatDateVeryShortMem = memoizeOne(
(locale: FrontendLocaleData, serverTimeZone: string) =>
new Intl.DateTimeFormat(locale.language, {
day: "numeric",

View File

@ -1,8 +1,13 @@
import { HaDurationData } from "../../components/ha-duration-input";
import { FrontendLocaleData } from "../../data/translation";
import "../../resources/intl-polyfill";
const leftPad = (num: number) => (num < 10 ? `0${num}` : num);
export const formatDuration = (duration: HaDurationData) => {
export const formatDuration = (
locale: FrontendLocaleData,
duration: HaDurationData
) => {
const d = duration.days || 0;
const h = duration.hours || 0;
const m = duration.minutes || 0;
@ -10,7 +15,11 @@ export const formatDuration = (duration: HaDurationData) => {
const ms = duration.milliseconds || 0;
if (d > 0) {
return `${d} day${d === 1 ? "" : "s"} ${h}:${leftPad(m)}:${leftPad(s)}`;
return `${Intl.NumberFormat(locale.language, {
style: "unit",
unit: "day",
unitDisplay: "long",
}).format(d)} ${h}:${leftPad(m)}:${leftPad(s)}`;
}
if (h > 0) {
return `${h}:${leftPad(m)}:${leftPad(s)}`;
@ -19,10 +28,18 @@ export const formatDuration = (duration: HaDurationData) => {
return `${m}:${leftPad(s)}`;
}
if (s > 0) {
return `${s} second${s === 1 ? "" : "s"}`;
return Intl.NumberFormat(locale.language, {
style: "unit",
unit: "second",
unitDisplay: "long",
}).format(s);
}
if (ms > 0) {
return `${ms} millisecond${ms === 1 ? "" : "s"}`;
return Intl.NumberFormat(locale.language, {
style: "unit",
unit: "millisecond",
unitDisplay: "long",
}).format(ms);
}
return null;
};

View File

@ -41,9 +41,7 @@ export const applyThemesOnElement = (
// If there is no explicitly desired dark mode provided, we automatically
// use the active one from `themes`.
const darkMode =
themeSettings && themeSettings?.dark !== undefined
? themeSettings?.dark
: themes.darkMode;
themeSettings?.dark !== undefined ? themeSettings.dark : themes.darkMode;
let cacheKey = themeToApply;
let themeRules: Partial<ThemeVars> = {};
@ -135,10 +133,19 @@ export const applyThemesOnElement = (
// Set and/or reset styles
if (element.updateStyles) {
// Use updateStyles() method of Polymer elements
element.updateStyles(styles);
} else if (window.ShadyCSS) {
// Implement updateStyles() method of Polymer elements
// Use ShadyCSS if available
window.ShadyCSS.styleSubtree(/** @type {!HTMLElement} */ element, styles);
} else {
for (const s in styles) {
if (s === null) {
element.style.removeProperty(s);
} else {
element.style.setProperty(s, styles[s]);
}
}
}
};

View File

@ -1,19 +1,29 @@
// https://gist.github.com/hagemann/382adfc57adbd5af078dc93feef01fe1
export const slugify = (value: string, delimiter = "_") => {
const a =
"àáäâãåăæąçćčđďèéěėëêęğǵḧìíïîįłḿǹńňñòóöôœøṕŕřßşśšșťțùúüûǘůűūųẃẍÿýźžż·/_,:;";
const b = `aaaaaaaaacccddeeeeeeegghiiiiilmnnnnooooooprrsssssttuuuuuuuuuwxyyzzz${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}`;
"àáâäæãåāăąçćčđďèéêëēėęěğǵḧîïíīįìıİłḿñńǹňôöòóœøōõőṕŕřßśšşșťțûüùúūǘůűųẃẍÿýžźż·";
const b = `aaaaaaaaaacccddeeeeeeeegghiiiiiiiilmnnnnoooooooooprrsssssttuuuuuuuuuwxyyzzz${delimiter}`;
const p = new RegExp(a.split("").join("|"), "g");
return value
.toString()
.toLowerCase()
.replace(/\s+/g, delimiter) // Replace spaces with delimiter
.replace(p, (c) => b.charAt(a.indexOf(c))) // Replace special characters
.replace(/&/g, `${delimiter}and${delimiter}`) // Replace & with 'and'
.replace(/[^\w-]+/g, "") // Remove all non-word characters
.replace(/-/g, delimiter) // Replace - with delimiter
.replace(new RegExp(`(${delimiter})\\1+`, "g"), "$1") // Replace multiple delimiters with single delimiter
.replace(new RegExp(`^${delimiter}+`), "") // Trim delimiter from start of text
.replace(new RegExp(`${delimiter}+$`), ""); // Trim delimiter from end of text
let slugified;
if (value === "") {
slugified = "";
} else {
slugified = value
.toString()
.toLowerCase()
.replace(p, (c) => b.charAt(a.indexOf(c))) // Replace special characters
.replace(/(?<=\d),(?=\d)/g, "") // Remove Commas between numbers
.replace(/[^a-z0-9]+/g, delimiter) // Replace all non-word characters
.replace(new RegExp(`(${delimiter})\\1+`, "g"), "$1") // Replace multiple delimiters with single delimiter
.replace(new RegExp(`^${delimiter}+`), "") // Trim delimiter from start of text
.replace(new RegExp(`${delimiter}+$`), ""); // Trim delimiter from end of text
if (slugified === "") {
slugified = "unknown";
}
}
return slugified;
};

View File

@ -13,45 +13,33 @@ export const handleStructError = (
for (const failure of err.failures()) {
if (failure.value === undefined) {
errors.push(
hass.localize(
"ui.errors.config.key_missing",
"key",
failure.path.join(".")
)
hass.localize("ui.errors.config.key_missing", {
key: failure.path.join("."),
})
);
} else if (failure.type === "never") {
warnings.push(
hass.localize(
"ui.errors.config.key_not_expected",
"key",
failure.path.join(".")
)
hass.localize("ui.errors.config.key_not_expected", {
key: failure.path.join("."),
})
);
} else if (failure.type === "union") {
continue;
} else if (failure.type === "enums") {
warnings.push(
hass.localize(
"ui.errors.config.key_wrong_type",
"key",
failure.path.join("."),
"type_correct",
failure.message.replace("Expected ", "").split(", ")[0],
"type_wrong",
JSON.stringify(failure.value)
)
hass.localize("ui.errors.config.key_wrong_type", {
key: failure.path.join("."),
type_correct: failure.message.replace("Expected ", "").split(", ")[0],
type_wrong: JSON.stringify(failure.value),
})
);
} else {
warnings.push(
hass.localize(
"ui.errors.config.key_wrong_type",
"key",
failure.path.join("."),
"type_correct",
failure.refinement || failure.type,
"type_wrong",
JSON.stringify(failure.value)
)
hass.localize("ui.errors.config.key_wrong_type", {
key: failure.path.join("."),
type_correct: failure.refinement || failure.type,
type_wrong: JSON.stringify(failure.value),
})
);
}
}

View File

@ -1,6 +1,13 @@
import "@material/mwc-button";
import { mdiAlertOctagram, mdiCheckBold } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import "../ha-circular-progress";
import "../ha-svg-icon";
@ -27,7 +34,7 @@ export class HaProgressButton extends LitElement {
<slot></slot>
</mwc-button>
${!overlay
? ""
? nothing
: html`
<div class="progress">
${this._result === "success"

View File

@ -39,7 +39,7 @@ import {
formatDate,
formatDateMonth,
formatDateMonthYear,
formatDateShort,
formatDateVeryShort,
formatDateWeekdayDay,
formatDateYear,
} from "../../common/datetime/format_date";
@ -128,7 +128,7 @@ _adapters._date.override({
this.options.config
);
case "day":
return formatDateShort(
return formatDateVeryShort(
new Date(time),
this.options.locale,
this.options.config

View File

@ -12,6 +12,7 @@ import { styleMap } from "lit/directives/style-map";
import { clamp } from "../../common/number/clamp";
import { computeRTL } from "../../common/util/compute_rtl";
import { HomeAssistant } from "../../types";
import { debounce } from "../../common/util/debounce";
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
@ -52,6 +53,12 @@ export class HaChartBase extends LitElement {
@state() private _hiddenDatasets: Set<number> = new Set();
private _paddingUpdateCount = 0;
private _paddingUpdateLock = false;
private _paddingYAxisInternal = 0;
public disconnectedCallback() {
super.disconnectedCallback();
this._releaseCanvas();
@ -104,9 +111,44 @@ export class HaChartBase extends LitElement {
});
}
public shouldUpdate(changedProps: PropertyValues): boolean {
if (
this._paddingUpdateLock &&
changedProps.size === 1 &&
changedProps.has("paddingYAxis")
) {
return false;
}
return true;
}
private _debouncedClearUpdates = debounce(
() => {
this._paddingUpdateCount = 0;
},
2000,
false
);
public willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (!this._paddingUpdateLock) {
this._paddingYAxisInternal = this.paddingYAxis;
if (changedProps.size === 1 && changedProps.has("paddingYAxis")) {
this._paddingUpdateCount++;
if (this._paddingUpdateCount > 300) {
this._paddingUpdateLock = true;
// eslint-disable-next-line
console.error(
"Detected excessive chart padding updates, possibly an infinite loop. Disabling axis padding."
);
} else {
this._debouncedClearUpdates();
}
}
}
if (!this.hasUpdated || !this.chart) {
return;
}
@ -171,10 +213,10 @@ export class HaChartBase extends LitElement {
this.height ?? this._chartHeight ?? this.clientWidth / 2
}px`,
"padding-left": `${
computeRTL(this.hass) ? 0 : this.paddingYAxis
computeRTL(this.hass) ? 0 : this._paddingYAxisInternal
}px`,
"padding-right": `${
computeRTL(this.hass) ? this.paddingYAxis : 0
computeRTL(this.hass) ? this._paddingYAxisInternal : 0
}px`,
})}
>
@ -324,7 +366,7 @@ export class HaChartBase extends LitElement {
clamp(
context.tooltip.caretX,
100,
this.clientWidth - 100 - this.paddingYAxis
this.clientWidth - 100 - this._paddingYAxisInternal
) -
100 +
"px",

View File

@ -141,16 +141,10 @@ export class StateHistoryChartLine extends LitElement {
`${context.dataset.label}: ${formatNumber(
context.parsed.y,
this.hass.locale,
this.data[context.datasetIndex]?.entity_id
? getNumberFormatOptions(
this.hass.states[
this.data[context.datasetIndex].entity_id
],
this.hass.entities[
this.data[context.datasetIndex].entity_id
]
)
: undefined
getNumberFormatOptions(
undefined,
this.hass.entities[this._entityIds[context.datasetIndex]]
)
)} ${this.unit}`,
},
},

View File

@ -73,9 +73,9 @@ export class StateHistoryCharts extends LitElement {
@property({ type: Boolean }) public isLoadingData = false;
@state() private _computedStartTime!: Date;
private _computedStartTime!: Date;
@state() private _computedEndTime!: Date;
private _computedEndTime!: Date;
@state() private _maxYWidth = 0;
@ -114,31 +114,6 @@ export class StateHistoryCharts extends LitElement {
${this.hass.localize("ui.components.history_charts.no_history_found")}
</div>`;
}
const now = new Date();
this._computedEndTime =
this.upToNow || !this.endTime || this.endTime > now ? now : this.endTime;
if (this.startTime) {
this._computedStartTime = this.startTime;
} else if (this.hoursToShow) {
this._computedStartTime = new Date(
new Date().getTime() - 60 * 60 * this.hoursToShow * 1000
);
} else {
this._computedStartTime = new Date(
this.historyData.timeline.reduce(
(minTime, stateInfo) =>
Math.min(
minTime,
new Date(stateInfo.data[0].last_changed).getTime()
),
new Date().getTime()
)
);
}
const combinedItems = this.historyData.timeline.length
? (this.virtualize
? chunkData(this.historyData.timeline, CANVAS_TIMELINE_ROWS_CHUNK)
@ -220,10 +195,45 @@ export class StateHistoryCharts extends LitElement {
return true;
}
protected willUpdate() {
protected willUpdate(changedProps: PropertyValues) {
if (!this.hasUpdated) {
loadVirtualizer();
}
if (
[...changedProps.keys()].some(
(prop) =>
!(
["_maxYWidth", "_childYWidths", "_chartCount"] as PropertyKey[]
).includes(prop)
)
) {
// Don't recompute times when we just want to update layout
const now = new Date();
this._computedEndTime =
this.upToNow || !this.endTime || this.endTime > now
? now
: this.endTime;
if (this.startTime) {
this._computedStartTime = this.startTime;
} else if (this.hoursToShow) {
this._computedStartTime = new Date(
new Date().getTime() - 60 * 60 * this.hoursToShow * 1000
);
} else {
this._computedStartTime = new Date(
this.historyData.timeline.reduce(
(minTime, stateInfo) =>
Math.min(
minTime,
new Date(stateInfo.data[0].last_changed).getTime()
),
new Date().getTime()
)
);
}
}
}
protected updated(changedProps: PropertyValues) {

View File

@ -19,6 +19,7 @@ import { isComponentLoaded } from "../../common/config/is_component_loaded";
import {
formatNumber,
numberFormatToLocale,
getNumberFormatOptions,
} from "../../common/number/format_number";
import {
getDisplayUnit,
@ -74,6 +75,8 @@ export class StatisticsChart extends LitElement {
@state() private _chartData: ChartData = { datasets: [] };
@state() private _statisticIds: string[] = [];
@state() private _chartOptions?: ChartOptions;
@query("ha-chart-base") private _chart?: HaChartBase;
@ -189,7 +192,11 @@ export class StatisticsChart extends LitElement {
label: (context) =>
`${context.dataset.label}: ${formatNumber(
context.parsed.y,
this.hass.locale
this.hass.locale,
getNumberFormatOptions(
undefined,
this.hass.entities[this._statisticIds[context.datasetIndex]]
)
)} ${
// @ts-ignore
context.dataset.unit || ""
@ -248,6 +255,7 @@ export class StatisticsChart extends LitElement {
let colorIndex = 0;
const statisticsData = Object.entries(this.statisticsData);
const totalDataSets: ChartDataset<"line">[] = [];
const statisticIds: string[] = [];
let endTime: Date;
if (statisticsData.length === 0) {
@ -386,6 +394,7 @@ export class StatisticsChart extends LitElement {
unit: meta?.unit_of_measurement,
band,
});
statisticIds.push(statistic_id);
}
});
@ -411,11 +420,7 @@ export class StatisticsChart extends LitElement {
} else {
val = stat[type];
}
dataValues.push(
val !== null && val !== undefined
? Math.round(val * 100) / 100
: null
);
dataValues.push(val ?? null);
});
pushData(startDate, new Date(stat.end), dataValues);
});
@ -431,6 +436,7 @@ export class StatisticsChart extends LitElement {
this._chartData = {
datasets: totalDataSets,
};
this._statisticIds = statisticIds;
}
static get styles(): CSSResultGroup {

View File

@ -31,6 +31,10 @@ const Component = Vue.extend({
type: Boolean,
default: true,
},
openingDirection: {
type: String,
default: "right",
},
disabled: {
type: Boolean,
default: false,
@ -66,7 +70,7 @@ const Component = Vue.extend({
props: {
"time-picker": this.timePicker,
"auto-apply": this.autoApply,
opens: "right",
opens: this.openingDirection,
"show-dropdowns": false,
"time-picker24-hour": this.twentyfourHours,
disabled: this.disabled,
@ -126,9 +130,9 @@ class DateRangePickerElement extends WrappedElement {
${dateRangePickerStyles}
.calendars {
display: flex;
flex-wrap: nowrap !important;
}
.daterangepicker {
left: 0px !important;
top: auto;
box-shadow: var(--ha-card-box-shadow, none);
background-color: var(--card-background-color);
@ -252,6 +256,10 @@ class DateRangePickerElement extends WrappedElement {
direction: ltr;
text-align: left;
}
.vue-daterange-picker{
min-width: unset !important;
display: block !important;
}
`;
const shadowRoot = this.shadowRoot!;
shadowRoot.appendChild(style);

View File

@ -87,6 +87,8 @@ export class HaStatisticPicker extends LitElement {
@property({ type: Array, attribute: "exclude-statistics" })
public excludeStatistics?: string[];
@property() public helpMissingEntityUrl = "/more-info/statistics/";
@state() private _opened?: boolean;
@query("ha-combo-box", true) public comboBox!: HaComboBox;
@ -111,7 +113,7 @@ export class HaStatisticPicker extends LitElement {
? html`<a
target="_blank"
rel="noopener noreferrer"
href=${documentationUrl(this.hass, "/more-info/statistics/")}
href=${documentationUrl(this.hass, this.helpMissingEntityUrl)}
>${this.hass.localize(
"ui.components.statistic-picker.learn_more"
)}</a

View File

@ -112,9 +112,7 @@ export class StateBadge extends LitElement {
const stateObj = this.stateObj;
const iconStyle: { [name: string]: string } = {};
const hostStyle: Partial<CSSStyleDeclaration> = {
backgroundImage: "",
};
let backgroundImage = "";
this._showIcon = true;
@ -135,10 +133,12 @@ export class StateBadge extends LitElement {
if (domain === "camera") {
imageUrl = cameraUrlWithWidthHeight(imageUrl, 80, 80);
}
hostStyle.backgroundImage = `url(${imageUrl})`;
backgroundImage = `url(${imageUrl})`;
this._showIcon = false;
if (domain === "update") {
hostStyle.borderRadius = "0";
this.style.borderRadius = "0";
} else if (domain === "media_player") {
this.style.borderRadius = "8%";
}
} else if (this.color) {
// Externally provided overriding color wins over state color
@ -179,12 +179,12 @@ export class StateBadge extends LitElement {
if (this.hass) {
imageUrl = this.hass.hassUrl(imageUrl);
}
hostStyle.backgroundImage = `url(${imageUrl})`;
backgroundImage = `url(${imageUrl})`;
this._showIcon = false;
}
this._iconStyle = iconStyle;
Object.assign(this.style, hostStyle);
this.style.backgroundImage = backgroundImage;
}
static get styles(): CSSResultGroup {

View File

@ -95,25 +95,25 @@ class StateInfo extends LitElement {
:host {
min-width: 120px;
white-space: nowrap;
display: flex;
align-items: center;
}
state-badge {
float: left;
}
:host([rtl]) state-badge {
float: right;
flex: none;
}
.info {
margin-left: 56px;
margin-left: 8px;
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
min-width: 0;
}
:host([rtl]) .info {
margin-right: 56px;
margin-right: 8px;
margin-left: 0;
text-align: right;
}

View File

@ -4,7 +4,6 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { haStyle } from "../resources/styles";
import { HomeAssistant } from "../types";
import "./ha-area-picker";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
import { fireEvent } from "../common/dom/fire_event";

View File

@ -1,11 +1,9 @@
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, nothing } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { until } from "lit/directives/until";
import { HomeAssistant } from "../types";
import { formatNumber } from "../common/number/format_number";
let jsYamlPromise: Promise<typeof import("../resources/js-yaml-dump")>;
import { HomeAssistant } from "../types";
@customElement("ha-attribute-value")
class HaAttributeValue extends LitElement {
@ -44,7 +42,7 @@ class HaAttributeValue extends LitElement {
${attributeValue}
</a>
`;
} catch (_) {
} catch {
// Nothing to do here
}
}
@ -55,15 +53,19 @@ class HaAttributeValue extends LitElement {
attributeValue.some((val) => val instanceof Object)) ||
(!Array.isArray(attributeValue) && attributeValue instanceof Object)
) {
if (!jsYamlPromise) {
jsYamlPromise = import("../resources/js-yaml-dump");
}
const yaml = jsYamlPromise.then((jsYaml) => jsYaml.dump(attributeValue));
const yaml = import("js-yaml").then(({ dump }) => dump(attributeValue));
return html`<pre>${until(yaml, "")}</pre>`;
}
return this.hass.formatEntityAttributeValue(this.stateObj!, this.attribute);
}
static styles = css`
pre {
margin: 0;
white-space: pre-wrap;
}
`;
}
declare global {

View File

@ -26,6 +26,8 @@ export class HaButtonMenu extends LitElement {
@property({ type: Boolean }) public fixed = false;
@property({ type: Boolean, attribute: "no-anchor" }) public noAnchor = false;
@query("mwc-menu", true) private _menu?: Menu;
public get items() {
@ -82,7 +84,7 @@ export class HaButtonMenu extends LitElement {
if (this.disabled) {
return;
}
this._menu!.anchor = this;
this._menu!.anchor = this.noAnchor ? null : this;
this._menu!.show();
}

View File

@ -17,6 +17,9 @@ export class HaButton extends Button {
.mdc-button {
height: var(--button-height, 36px);
}
.trailing-icon {
display: flex;
}
`,
];
}

View File

@ -12,7 +12,6 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { CodeMirror, loadCodeMirror } from "../resources/codemirror.ondemand";
import { HomeAssistant } from "../types";
import "./ha-icon";
@ -58,7 +57,7 @@ export class HaCodeEditor extends ReactiveElement {
@state() private _value = "";
private _loadedCodeMirror?: CodeMirror;
private _loadedCodeMirror?: typeof import("../resources/codemirror");
private _iconList?: Completion[];
@ -110,7 +109,7 @@ export class HaCodeEditor extends ReactiveElement {
// Ensure CodeMirror module is loaded before any update
protected override async scheduleUpdate() {
this._loadedCodeMirror ??= await loadCodeMirror();
this._loadedCodeMirror ??= await import("../resources/codemirror");
super.scheduleUpdate();
}

View File

@ -64,6 +64,7 @@ class HaConfigEntryPicker extends LitElement {
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"
referrerpolicy="no-referrer"
@error=${this._onImageError}
@load=${this._onImageLoad}

View File

@ -269,37 +269,61 @@ export class HaCountryPicker extends LitElement {
@property() public label?: string;
@property() public countries?: string[];
@property() public helper?: string;
@property({ type: Boolean }) public required = false;
@property({ type: Boolean, reflect: true }) public disabled = false;
private _getOptions = memoizeOne((language?: string) => {
const countryDisplayNames =
Intl && "DisplayNames" in Intl
? new Intl.DisplayNames(language, {
type: "region",
fallback: "code",
})
: undefined;
@property({ type: Boolean }) public noSort = false;
const options = COUNTRIES.map((country) => ({
value: country,
label: countryDisplayNames ? countryDisplayNames.of(country)! : country,
}));
options.sort((a, b) =>
caseInsensitiveStringCompare(a.label, b.label, language)
);
return options;
});
private _getOptions = memoizeOne(
(language?: string, countries?: string[]) => {
let options: { label: string; value: string }[] = [];
const countryDisplayNames =
Intl && "DisplayNames" in Intl
? new Intl.DisplayNames(language, {
type: "region",
fallback: "code",
})
: undefined;
if (countries) {
options = countries.map((country) => ({
value: country,
label: countryDisplayNames
? countryDisplayNames.of(country)!
: country,
}));
} else {
options = COUNTRIES.map((country) => ({
value: country,
label: countryDisplayNames
? countryDisplayNames.of(country)!
: country,
}));
}
if (!this.noSort) {
options.sort((a, b) =>
caseInsensitiveStringCompare(a.label, b.label, language)
);
}
return options;
}
);
protected render() {
const options = this._getOptions(this.language);
const options = this._getOptions(this.language, this.countries);
return html`
<ha-select
.label=${this.label}
.value=${this.value}
.required=${this.required}
.helper=${this.helper}
.disabled=${this.disabled}
@selected=${this._changed}
@closed=${stopPropagation}

View File

@ -15,10 +15,11 @@ import {
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { calcDate } from "../common/datetime/calc_date";
import { firstWeekdayIndex } from "../common/datetime/first_weekday";
import { formatDate } from "../common/datetime/format_date";
@ -29,6 +30,7 @@ import { HomeAssistant } from "../types";
import "./date-range-picker";
import "./ha-svg-icon";
import "./ha-textfield";
import "./ha-icon-button";
export interface DateRangePickerRanges {
[key: string]: [Date, Date];
@ -54,8 +56,22 @@ export class HaDateRangePicker extends LitElement {
@property({ type: String }) private _rtlDirection = "ltr";
protected willUpdate() {
if (!this.hasUpdated && this.ranges === undefined) {
@property({ type: Boolean }) private minimal = false;
@property() public openingDirection?: "right" | "left" | "center" | "inline";
@state() private _calcedOpeningDirection?:
| "right"
| "left"
| "center"
| "inline";
protected willUpdate(changedProps: PropertyValues) {
if (
(!this.hasUpdated && this.ranges === undefined) ||
(changedProps.has("hass") &&
this.hass?.localize !== changedProps.get("hass")?.localize)
) {
const today = new Date();
const weekStartsOn = firstWeekdayIndex(this.hass.locale);
const weekStart = calcDate(
@ -133,41 +149,62 @@ export class HaDateRangePicker extends LitElement {
<date-range-picker
?disabled=${this.disabled}
?auto-apply=${this.autoApply}
?time-picker=${this.timePicker}
time-picker=${this.timePicker}
twentyfour-hours=${this._hour24format}
start-date=${this.startDate}
end-date=${this.endDate}
?ranges=${this.ranges !== false}
opening-direction=${this.openingDirection ||
this._calcedOpeningDirection}
first-day=${firstWeekdayIndex(this.hass.locale)}
>
<div slot="input" class="date-range-inputs">
<ha-svg-icon .path=${mdiCalendar}></ha-svg-icon>
<ha-textfield
.value=${this.timePicker
? formatDateTime(
this.startDate,
this.hass.locale,
this.hass.config
)
: formatDate(this.startDate, this.hass.locale, this.hass.config)}
.label=${this.hass.localize(
"ui.components.date-range-picker.start_date"
)}
.disabled=${this.disabled}
@click=${this._handleInputClick}
readonly
></ha-textfield>
<ha-textfield
.value=${this.timePicker
? formatDateTime(this.endDate, this.hass.locale, this.hass.config)
: formatDate(this.endDate, this.hass.locale, this.hass.config)}
.label=${this.hass.localize(
"ui.components.date-range-picker.end_date"
)}
.disabled=${this.disabled}
@click=${this._handleInputClick}
readonly
></ha-textfield>
<div slot="input" class="date-range-inputs" @click=${this._handleClick}>
${!this.minimal
? html`<ha-svg-icon .path=${mdiCalendar}></ha-svg-icon>
<ha-textfield
.value=${this.timePicker
? formatDateTime(
this.startDate,
this.hass.locale,
this.hass.config
)
: formatDate(
this.startDate,
this.hass.locale,
this.hass.config
)}
.label=${this.hass.localize(
"ui.components.date-range-picker.start_date"
)}
.disabled=${this.disabled}
@click=${this._handleInputClick}
readonly
></ha-textfield>
<ha-textfield
.value=${this.timePicker
? formatDateTime(
this.endDate,
this.hass.locale,
this.hass.config
)
: formatDate(
this.endDate,
this.hass.locale,
this.hass.config
)}
.label=${this.hass.localize(
"ui.components.date-range-picker.end_date"
)}
.disabled=${this.disabled}
@click=${this._handleInputClick}
readonly
></ha-textfield>`
: html`<ha-icon-button
.label=${this.hass.localize(
"ui.components.date-range-picker.select_date_range"
)}
.path=${mdiCalendar}
></ha-icon-button>`}
</div>
${this.ranges
? html`<div
@ -181,7 +218,7 @@ export class HaDateRangePicker extends LitElement {
)}
</mwc-list>
</div>`
: ""}
: nothing}
<div slot="footer" class="date-range-footer">
<mwc-button @click=${this._cancelDateRange}
>${this.hass.localize("ui.common.cancel")}</mwc-button
@ -225,6 +262,22 @@ export class HaDateRangePicker extends LitElement {
}
}
private _handleClick() {
// calculate opening direction if not set
if (!this._dateRangePicker.open && !this.openingDirection) {
const datePickerPosition = this.getBoundingClientRect().x;
let opens: "right" | "left" | "center" | "inline";
if (datePickerPosition > (2 * window.innerWidth) / 3) {
opens = "left";
} else if (datePickerPosition < window.innerWidth / 3) {
opens = "right";
} else {
opens = "center";
}
this._calcedOpeningDirection = opens;
}
}
static get styles(): CSSResultGroup {
return css`
ha-svg-icon {
@ -234,6 +287,10 @@ export class HaDateRangePicker extends LitElement {
direction: var(--direction);
}
ha-icon-button {
direction: var(--direction);
}
.date-range-inputs {
display: flex;
align-items: center;

View File

@ -163,10 +163,7 @@ export class HaExpansionPanel extends LitElement {
box-shadow: none;
border-width: 1px;
border-style: solid;
border-color: var(
--ha-card-border-color,
var(--divider-color, #e0e0e0)
);
border-color: var(--outline-color);
border-radius: var(--ha-card-border-radius, 12px);
}

View File

@ -66,6 +66,10 @@ export const computeInitialHaFormData = (
typeof firstOption === "string" ? firstOption : firstOption.value;
data[field.name] = selector.select.multiple ? [val] : val;
}
} else if ("country" in selector) {
if (selector.country?.countries?.length) {
data[field.name] = selector.country.countries[0];
}
} else if ("duration" in selector) {
data[field.name] = {
hours: 0,

View File

@ -57,8 +57,7 @@ export class HaFormInteger extends LitElement implements HaFormElement {
`
: ""}
<ha-slider
pin
ignore-bar-touch
labeled
.value=${this._value}
.min=${this.schema.valueMin}
.max=${this.schema.valueMax}

View File

@ -138,15 +138,13 @@ export class HaFormString extends LitElement implements HaFormElement {
}
ha-icon-button {
position: absolute;
top: 1em;
right: 12px;
--mdc-icon-button-size: 24px;
color: var(--secondary-text-color);
}
ha-icon-button {
top: 8px;
right: 8px;
inset-inline-start: initial;
inset-inline-end: 12px;
inset-inline-end: 8px;
--mdc-icon-button-size: 40px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
direction: var(--direction);
}
`;

View File

@ -1,9 +1,9 @@
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
css,
html,
nothing,
} from "lit";
import { customElement, property, state } from "lit/decorators";
@ -11,12 +11,12 @@ import { fireEvent } from "../common/dom/fire_event";
import { debounce } from "../common/util/debounce";
import { CustomIcon, customIcons } from "../data/custom_icons";
import {
checkCacheVersion,
Chunks,
findIconChunk,
getIcon,
Icons,
MDI_PREFIXES,
checkCacheVersion,
findIconChunk,
getIcon,
writeCache,
} from "../data/iconsets";
import "./ha-svg-icon";
@ -47,6 +47,8 @@ export class HaIcon extends LitElement {
@state() private _path?: string;
@state() private _secondaryPath?: string;
@state() private _viewBox?: string;
@state() private _legacy = false;
@ -55,6 +57,7 @@ export class HaIcon extends LitElement {
super.willUpdate(changedProps);
if (changedProps.has("icon")) {
this._path = undefined;
this._secondaryPath = undefined;
this._viewBox = undefined;
this._loadIcon();
}
@ -70,6 +73,7 @@ export class HaIcon extends LitElement {
}
return html`<ha-svg-icon
.path=${this._path}
.secondaryPath=${this._secondaryPath}
.viewBox=${this._viewBox}
></ha-svg-icon>`;
}
@ -175,6 +179,7 @@ export class HaIcon extends LitElement {
return;
}
this._path = icon.path;
this._secondaryPath = icon.secondaryPath;
this._viewBox = icon.viewBox;
}

View File

@ -1,90 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "./ha-icon";
import "./ha-slider";
class HaLabeledSlider extends PolymerElement {
static get template() {
return html`
<style>
:host {
display: block;
}
.title {
margin: 5px 0 8px;
color: var(--primary-text-color);
}
.slider-container {
display: flex;
}
ha-icon {
margin-top: 4px;
color: var(--secondary-text-color);
}
ha-slider {
flex-grow: 1;
background-image: var(--ha-slider-background);
border-radius: 4px;
}
</style>
<div class="title">[[_getTitle()]]</div>
<div class="extra-container"><slot name="extra"></slot></div>
<div class="slider-container">
<ha-icon icon="[[icon]]" hidden$="[[!icon]]"></ha-icon>
<ha-slider
min="[[min]]"
max="[[max]]"
step="[[step]]"
pin="[[pin]]"
disabled="[[disabled]]"
value="{{value}}"
></ha-slider>
</div>
<template is="dom-if" if="[[helper]]">
<ha-input-helper-text>[[helper]]</ha-input-helper-text>
</template>
`;
}
_getTitle() {
return `${this.caption}${this.caption && this.required ? " *" : ""}`;
}
static get properties() {
return {
caption: String,
disabled: Boolean,
required: Boolean,
min: Number,
max: Number,
pin: Boolean,
step: Number,
helper: String,
extra: {
type: Boolean,
value: false,
},
ignoreBarTouch: {
type: Boolean,
value: true,
},
icon: {
type: String,
value: "",
},
value: {
type: Number,
notify: true,
},
};
}
}
customElements.define("ha-labeled-slider", HaLabeledSlider);

View File

@ -0,0 +1,96 @@
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import "./ha-icon";
import "./ha-slider";
import { fireEvent } from "../common/dom/fire_event";
@customElement("ha-labeled-slider")
class HaLabeledSlider extends LitElement {
@property() public labeled? = false;
@property() public caption?: string;
@property() public disabled?: boolean;
@property() public required?: boolean;
@property() public min: number = 0;
@property() public max: number = 100;
@property() public step: number = 1;
@property() public helper?: string;
@property() public extra = false;
@property() public icon?: string;
@property() public value?: number;
protected render() {
return html`
<div class="title">${this._getTitle()}</div>
<div class="extra-container"><slot name="extra"></slot></div>
<div class="slider-container">
${this.icon ? html`<ha-icon icon=${this.icon}></ha-icon>` : nothing}
<ha-slider
.min=${this.min}
.max=${this.max}
.step=${this.step}
labeled=${this.labeled}
.disabled=${this.disabled}
.value=${this.value}
@change=${this._inputChanged}
></ha-slider>
</div>
${this.helper
? html`<ha-input-helper-text> ${this.helper} </ha-input-helper-text>`
: nothing}
`;
}
private _getTitle(): string {
return `${this.caption}${this.caption && this.required ? " *" : ""}`;
}
private _inputChanged(ev) {
fireEvent(this, "value-changed", {
value: Number((ev.target as any).value),
});
}
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
}
.title {
margin: 5px 0 8px;
color: var(--primary-text-color);
}
.slider-container {
display: flex;
}
ha-icon {
margin-top: 8px;
color: var(--secondary-text-color);
}
ha-slider {
flex-grow: 1;
background-image: var(--ha-slider-background);
border-radius: 4px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-labeled-slider": HaLabeledSlider;
}
}

View File

@ -1,19 +1,16 @@
import { css } from "lit";
import { customElement } from "lit/decorators";
import { OutlinedButton } from "@material/web/button/internal/outlined-button";
import { styles as outlinedStyles } from "@material/web/button/internal/outlined-styles.css";
import { styles as sharedStyles } from "@material/web/button/internal/shared-styles.css";
import { MdOutlinedButton } from "@material/web/button/outlined-button";
@customElement("ha-outlined-button")
export class HaOutlinedButton extends OutlinedButton {
export class HaOutlinedButton extends MdOutlinedButton {
static override styles = [
sharedStyles,
outlinedStyles,
...super.styles,
css`
:host {
--ha-icon-display: block;
--md-sys-color-primary: var(--primary-text-color);
--md-sys-color-outline: var(--divider-color);
--md-sys-color-outline: var(--outline-color);
}
`,
];

View File

@ -1,21 +1,11 @@
import { css } from "lit";
import { customElement } from "lit/decorators";
import { IconButton } from "@material/web/iconbutton/internal/icon-button";
import { styles as outlinedStyles } from "@material/web/iconbutton/internal/outlined-styles.css";
import { styles as sharedStyles } from "@material/web/iconbutton/internal/shared-styles.css";
import { MdOutlinedIconButton } from "@material/web/iconbutton/outlined-icon-button";
@customElement("ha-outlined-icon-button")
export class HaOutlinedIconButton extends IconButton {
protected override getRenderClasses() {
return {
...super.getRenderClasses(),
outlined: true,
};
}
export class HaOutlinedIconButton extends MdOutlinedIconButton {
static override styles = [
sharedStyles,
outlinedStyles,
...super.styles,
css`
:host {
--ha-icon-display: block;

View File

@ -154,6 +154,8 @@ export class HaRelatedItems extends LitElement {
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"
referrerpolicy="no-referrer"
alt=${entry.domain}
slot="graphic"
/>

View File

@ -1,15 +1,32 @@
import { SelectBase } from "@material/mwc-select/mwc-select-base";
import { styles } from "@material/mwc-select/mwc-select.css";
import { mdiClose } from "@mdi/js";
import { css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { debounce } from "../common/util/debounce";
import { nextRender } from "../common/util/render-status";
import "./ha-icon-button";
@customElement("ha-select")
export class HaSelect extends SelectBase {
// @ts-ignore
@property({ type: Boolean }) public icon?: boolean;
@property({ type: Boolean, reflect: true }) public clearable?: boolean;
protected override render() {
return html`
${super.render()}
${this.clearable && !this.required && !this.disabled && this.value
? html`<ha-icon-button
label="clear"
@click=${this._clearValue}
.path=${mdiClose}
></ha-icon-button>`
: nothing}
`;
}
protected override renderLeadingIcon() {
if (!this.icon) {
return nothing;
@ -33,6 +50,15 @@ export class HaSelect extends SelectBase {
);
}
private _clearValue(): void {
if (this.disabled || !this.value) {
return;
}
this.valueSetDirectly = true;
this.select(-1);
this.mdcFoundation.handleChange();
}
private _translationsUpdated = debounce(async () => {
await nextRender();
this.layoutOptions();
@ -41,6 +67,9 @@ export class HaSelect extends SelectBase {
static override styles = [
styles,
css`
:host([clearable]) {
position: relative;
}
.mdc-select:not(.mdc-select--disabled) .mdc-select__icon {
color: var(--secondary-text-color);
}
@ -68,6 +97,23 @@ export class HaSelect extends SelectBase {
.mdc-select__anchor .mdc-floating-label--float-above {
transform-origin: var(--float-start);
}
.mdc-select__selected-text-container {
padding-inline-end: var(--select-selected-text-padding-end, 0px);
}
:host([clearable]) .mdc-select__selected-text-container {
padding-inline-end: var(--select-selected-text-padding-end, 12px);
}
ha-icon-button {
position: absolute;
top: 10px;
right: 28px;
--mdc-icon-button-size: 36px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
inset-inline-start: initial;
inset-inline-end: 28px;
direction: var(--direction);
}
`,
];
}

View File

@ -24,7 +24,7 @@ export class HaColorTempSelector extends LitElement {
protected render() {
return html`
<ha-labeled-slider
pin
labeled
icon="hass:thermometer"
.caption=${this.label || ""}
.min=${this.selector.color_temp?.min_mireds ?? 153}
@ -33,27 +33,25 @@ export class HaColorTempSelector extends LitElement {
.disabled=${this.disabled}
.helper=${this.helper}
.required=${this.required}
@change=${this._valueChanged}
@value-changed=${this._valueChanged}
></ha-labeled-slider>
`;
}
private _valueChanged(ev: CustomEvent) {
fireEvent(this, "value-changed", {
value: Number((ev.target as any).value),
value: Number((ev.detail as any).value),
});
}
static styles = css`
ha-labeled-slider {
--ha-slider-background: -webkit-linear-gradient(
var(--float-end),
--ha-slider-background: linear-gradient(
to var(--float-end),
rgb(255, 160, 0) 0%,
white 50%,
rgb(166, 209, 255) 100%
);
/* The color temp minimum value shouldn't be rendered differently. It's not "off". */
--paper-slider-knob-start-border-color: var(--primary-color);
}
`;
}

View File

@ -0,0 +1,49 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { CountrySelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../ha-country-picker";
@customElement("ha-selector-country")
export class HaCountrySelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: CountrySelector;
@property() public value?: any;
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
return html`
<ha-country-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
.countries=${this.selector.country?.countries}
.noSort=${this.selector.country?.no_sort}
.disabled=${this.disabled}
.required=${this.required}
></ha-country-picker>
`;
}
static styles = css`
ha-country-picker {
width: 100%;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-country": HaCountrySelector;
}
}

View File

@ -26,7 +26,7 @@ export class HaDateSelector extends LitElement {
.label=${this.label}
.locale=${this.hass.locale}
.disabled=${this.disabled}
.value=${this.value}
.value=${typeof this.value === "string" ? this.value : undefined}
.required=${this.required}
.helper=${this.helper}
>

View File

@ -30,7 +30,8 @@ export class HaDateTimeSelector extends LitElement {
@query("ha-time-input") private _timeInput!: HaTimeInput;
protected render() {
const values = this.value?.split(" ");
const values =
typeof this.value === "string" ? this.value.split(" ") : undefined;
return html`
<div class="input">

View File

@ -1,4 +1,4 @@
import { css, CSSResultGroup, html, LitElement } from "lit";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../common/dom/fire_event";
@ -26,8 +26,22 @@ export class HaNumberSelector extends LitElement {
@property({ type: Boolean }) public disabled = false;
private _valueStr = "";
protected willUpdate(changedProps: PropertyValues) {
if (changedProps.has("value")) {
if (this.value !== Number(this._valueStr)) {
this._valueStr =
!this.value || isNaN(this.value) ? "" : this.value.toString();
}
}
}
protected render() {
const isBox = this.selector.number?.mode === "box";
const isBox =
this.selector.number?.mode === "box" ||
this.selector.number?.min === undefined ||
this.selector.number?.max === undefined;
return html`
<div class="input">
@ -37,6 +51,7 @@ export class HaNumberSelector extends LitElement {
? html`${this.label}${this.required ? "*" : ""}`
: ""}
<ha-slider
labeled
.min=${this.selector.number?.min}
.max=${this.selector.number?.max}
.value=${this.value ?? ""}
@ -45,8 +60,6 @@ export class HaNumberSelector extends LitElement {
: this.selector.number?.step ?? 1}
.disabled=${this.disabled}
.required=${this.required}
pin
ignore-bar-touch
@change=${this._handleSliderChange}
>
</ha-slider>
@ -57,14 +70,12 @@ export class HaNumberSelector extends LitElement {
(this.selector.number?.step ?? 1) % 1 !== 0
? "decimal"
: "numeric"}
.label=${this.selector.number?.mode !== "box"
? undefined
: this.label}
.label=${!isBox ? undefined : this.label}
.placeholder=${this.placeholder}
class=${classMap({ single: this.selector.number?.mode === "box" })}
class=${classMap({ single: isBox })}
.min=${this.selector.number?.min}
.max=${this.selector.number?.max}
.value=${this.value ?? ""}
.value=${this._valueStr ?? ""}
.step=${this.selector.number?.step ?? 1}
helperPersistent
.helper=${isBox ? this.helper : undefined}
@ -73,7 +84,7 @@ export class HaNumberSelector extends LitElement {
.suffix=${this.selector.number?.unit_of_measurement}
type="number"
autoValidate
?no-spinner=${this.selector.number?.mode !== "box"}
?no-spinner=${!isBox}
@input=${this._handleInputChange}
>
</ha-textfield>
@ -86,6 +97,7 @@ export class HaNumberSelector extends LitElement {
private _handleInputChange(ev) {
ev.stopPropagation();
this._valueStr = ev.target.value;
const value =
ev.target.value === "" || isNaN(ev.target.value)
? undefined

View File

@ -1,11 +1,16 @@
import "@material/mwc-list/mwc-list-item";
import { mdiClose } from "@mdi/js";
import { css, html, LitElement } from "lit";
import { mdiClose, mdiDrag } from "@mdi/js";
import { LitElement, PropertyValues, css, html, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { SortableEvent } from "sortablejs";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { ensureArray } from "../../common/array/ensure-array";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import type { SelectOption, SelectSelector } from "../../data/selector";
import { sortableStyles } from "../../resources/ha-sortable-style";
import { SortableInstance } from "../../resources/sortable";
import type { HomeAssistant } from "../../types";
import "../ha-checkbox";
import "../ha-chip";
@ -13,10 +18,9 @@ import "../ha-chip-set";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-formfield";
import "../ha-input-helper-text";
import "../ha-radio";
import "../ha-select";
import "../ha-input-helper-text";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
@customElement("ha-selector-select")
export class HaSelectSelector extends LitElement {
@ -38,6 +42,68 @@ export class HaSelectSelector extends LitElement {
@query("ha-combo-box", true) private comboBox!: HaComboBox;
private _sortable?: SortableInstance;
protected updated(changedProps: PropertyValues): void {
if (changedProps.has("value") || changedProps.has("selector")) {
const sortableNeeded =
this.selector.select?.multiple &&
this.selector.select.reorder &&
this.value?.length;
if (!this._sortable && sortableNeeded) {
this._createSortable();
} else if (this._sortable && !sortableNeeded) {
this._destroySortable();
}
}
}
private async _createSortable() {
const Sortable = (await import("../../resources/sortable")).default;
this._sortable = new Sortable(
this.shadowRoot!.querySelector("ha-chip-set")!,
{
animation: 150,
fallbackClass: "sortable-fallback",
draggable: "ha-chip",
onChoose: (evt: SortableEvent) => {
(evt.item as any).placeholder =
document.createComment("sort-placeholder");
evt.item.after((evt.item as any).placeholder);
},
onEnd: (evt: SortableEvent) => {
// put back in original location
if ((evt.item as any).placeholder) {
(evt.item as any).placeholder.replaceWith(evt.item);
delete (evt.item as any).placeholder;
}
this._dragged(evt);
},
}
);
}
private _dragged(ev: SortableEvent): void {
if (ev.oldIndex === ev.newIndex) return;
this._move(ev.oldIndex!, ev.newIndex!);
}
private _move(index: number, newIndex: number) {
const value = this.value as string[];
const newValue = value.concat();
const element = newValue.splice(index, 1)[0];
newValue.splice(newIndex, 0, element);
this.value = newValue;
fireEvent(this, "value-changed", {
value: newValue,
});
}
private _destroySortable() {
this._sortable?.destroy();
this._sortable = undefined;
}
private _filter = "";
protected render() {
@ -71,7 +137,11 @@ export class HaSelectSelector extends LitElement {
);
}
if (!this.selector.select?.custom_value && this._mode === "list") {
if (
!this.selector.select?.custom_value &&
!this.selector.select?.reorder &&
this._mode === "list"
) {
if (!this.selector.select?.multiple) {
return html`
<div>
@ -124,23 +194,39 @@ export class HaSelectSelector extends LitElement {
return html`
${value?.length
? html`<ha-chip-set>
${value.map(
(item, idx) => html`
<ha-chip hasTrailingIcon>
${options.find((option) => option.value === item)?.label ||
item}
<ha-svg-icon
slot="trailing-icon"
.path=${mdiClose}
.idx=${idx}
@click=${this._removeItem}
></ha-svg-icon>
</ha-chip>
`
)}
</ha-chip-set>`
: ""}
? html`
<ha-chip-set>
${repeat(
value,
(item) => item,
(item, idx) => html`
<ha-chip
hasTrailingIcon
.hasIcon=${this.selector.select?.reorder}
>
${this.selector.select?.reorder
? html`
<ha-svg-icon
slot="icon"
.path=${mdiDrag}
data-handle
></ha-svg-icon>
`
: nothing}
${options.find((option) => option.value === item)
?.label || item}
<ha-svg-icon
slot="trailing-icon"
.path=${mdiClose}
.idx=${idx}
@click=${this._removeItem}
></ha-svg-icon>
</ha-chip>
`
)}
</ha-chip-set>
`
: nothing}
<ha-combo-box
item-value-path="value"
@ -198,6 +284,7 @@ export class HaSelectSelector extends LitElement {
.helper=${this.helper ?? ""}
.disabled=${this.disabled}
.required=${this.required}
clearable
@closed=${stopPropagation}
@selected=${this._valueChanged}
>
@ -228,7 +315,7 @@ export class HaSelectSelector extends LitElement {
private _valueChanged(ev) {
ev.stopPropagation();
const value = ev.detail?.value || ev.target.value;
if (this.disabled || value === undefined || value === this.value) {
if (this.disabled || value === undefined || value === (this.value ?? "")) {
return;
}
fireEvent(this, "value-changed", {
@ -330,16 +417,22 @@ export class HaSelectSelector extends LitElement {
this.comboBox.filteredItems = filteredItems;
}
static styles = css`
ha-select,
mwc-formfield,
ha-formfield {
display: block;
}
mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
`;
static styles = [
sortableStyles,
css`
:host {
position: relative;
}
ha-select,
mwc-formfield,
ha-formfield {
display: block;
}
mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
`,
];
}
declare global {

View File

@ -111,13 +111,13 @@ export class HaTextSelector extends LitElement {
}
ha-icon-button {
position: absolute;
top: 10px;
right: 10px;
--mdc-icon-button-size: 36px;
top: 8px;
right: 8px;
inset-inline-start: initial;
inset-inline-end: 8px;
--mdc-icon-button-size: 40px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
inset-inline-start: initial;
inset-inline-end: 10px;
direction: var(--direction);
}
`;

View File

@ -23,7 +23,7 @@ export class HaTimeSelector extends LitElement {
protected render() {
return html`
<ha-time-input
.value=${this.value}
.value=${typeof this.value === "string" ? this.value : undefined}
.locale=${this.hass.locale}
.disabled=${this.disabled}
.required=${this.required}

View File

@ -21,6 +21,7 @@ const LOAD_ELEMENTS = {
config_entry: () => import("./ha-selector-config-entry"),
conversation_agent: () => import("./ha-selector-conversation-agent"),
constant: () => import("./ha-selector-constant"),
country: () => import("./ha-selector-country"),
date: () => import("./ha-selector-date"),
datetime: () => import("./ha-selector-datetime"),
device: () => import("./ha-selector-device"),

View File

@ -2,9 +2,9 @@ import "@material/mwc-button/mwc-button";
import {
mdiBell,
mdiCalendar,
mdiCart,
mdiCellphoneCog,
mdiChartBox,
mdiClipboardList,
mdiClose,
mdiCog,
mdiFormatListBulletedType,
@ -50,7 +50,7 @@ import { subscribeRepairsIssueRegistry } from "../data/repairs";
import { UpdateEntity, updateCanInstall } from "../data/update";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
import { SortableInstance, loadSortable } from "../resources/sortable.ondemand";
import type { SortableInstance } from "../resources/sortable";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant, PanelInfo, Route } from "../types";
import "./ha-icon";
@ -81,7 +81,7 @@ const PANEL_ICONS = {
lovelace: mdiViewDashboard,
map: mdiTooltipAccount,
"media-browser": mdiPlayBoxMultiple,
"shopping-list": mdiCart,
todo: mdiClipboardList,
};
const panelSorter = (
@ -689,7 +689,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
}
private async _createSortable() {
const Sortable = await loadSortable();
const Sortable = (await import("../resources/sortable")).default;
this._sortable = new Sortable(
this.shadowRoot!.getElementById("sortable")!,
{

View File

@ -1,117 +0,0 @@
import "@polymer/paper-slider";
const PaperSliderClass = customElements.get("paper-slider");
let subTemplate;
export class HaSlider extends PaperSliderClass {
static get template() {
if (!subTemplate) {
subTemplate = PaperSliderClass.template.cloneNode(true);
const superStyle = subTemplate.content.querySelector("style");
// append style to add mirroring of pin in RTL
superStyle.appendChild(
document.createTextNode(`
:host([dir="rtl"]) #sliderContainer.pin.expand > .slider-knob > .slider-knob-inner::after {
-webkit-transform: scale(1) translate(0, -17px) scaleX(-1) !important;
transform: scale(1) translate(0, -17px) scaleX(-1) !important;
}
.pin > .slider-knob > .slider-knob-inner {
font-size: var(--ha-slider-pin-font-size, 15px);
line-height: normal;
cursor: pointer;
}
.disabled.ring > .slider-knob > .slider-knob-inner {
background-color: var(--paper-slider-disabled-knob-color, var(--disabled-text-color));
border: 2px solid var(--paper-slider-disabled-knob-color, var(--disabled-text-color));
}
.pin > .slider-knob > .slider-knob-inner::before {
top: unset;
margin-left: unset;
bottom: calc(15px + var(--calculated-paper-slider-height)/2);
left: 50%;
width: 2.6em;
height: 2.6em;
-webkit-transform-origin: left bottom;
transform-origin: left bottom;
-webkit-transform: rotate(-45deg) scale(0) translate(0);
transform: rotate(-45deg) scale(0) translate(0);
}
.pin.expand > .slider-knob > .slider-knob-inner::before {
-webkit-transform: rotate(-45deg) scale(1) translate(7px, -7px);
transform: rotate(-45deg) scale(1) translate(7px, -7px);
}
.pin > .slider-knob > .slider-knob-inner::after {
top: unset;
font-size: unset;
bottom: calc(15px + var(--calculated-paper-slider-height)/2);
left: 50%;
margin-left: -1.3em;
width: 2.6em;
height: 2.5em;
-webkit-transform-origin: center bottom;
transform-origin: center bottom;
-webkit-transform: scale(0) translate(0);
transform: scale(0) translate(0);
}
.pin.expand > .slider-knob > .slider-knob-inner::after {
-webkit-transform: scale(1) translate(0, -10px);
transform: scale(1) translate(0, -10px);
}
.slider-input {
width: 54px;
}
`)
);
}
return subTemplate;
}
_setImmediateValue(newImmediateValue) {
super._setImmediateValue(
this.step >= 1
? Math.round(newImmediateValue)
: Math.round(newImmediateValue * 100) / 100
);
}
_calcStep(value) {
if (!this.step) {
return parseFloat(value);
}
const numSteps = Math.round((value - this.min) / this.step);
const stepStr = this.step.toString();
const stepPointAt = stepStr.indexOf(".");
if (stepPointAt !== -1) {
/**
* For small values of this.step, if we calculate the step using
* For non-integer values of this.step, if we calculate the step using
* `Math.round(value / step) * step` we may hit a precision point issue
* eg. 0.1 * 0.2 = 0.020000000000000004
* http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
*
* as a work around we can round with the decimal precision of `step`
*/
const precision = 10 ** (stepStr.length - stepPointAt - 1);
return (
Math.round((numSteps * this.step + this.min) * precision) / precision
);
}
return numSteps * this.step + this.min;
}
}
customElements.define("ha-slider", HaSlider);

View File

@ -0,0 +1,26 @@
import { customElement } from "lit/decorators";
import { MdSlider } from "@material/web/slider/slider";
import { CSSResult, css } from "lit";
@customElement("ha-slider")
export class HaSlider extends MdSlider {
static override styles: CSSResult[] = [
...MdSlider.styles,
css`
:host {
--md-sys-color-primary: var(--primary-color);
--md-sys-color-outline: var(--outline-color);
min-width: 100px;
min-inline-size: 100px;
width: 200px;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-slider": HaSlider;
}
}

View File

@ -1,10 +1,19 @@
import { css, CSSResultGroup, LitElement, svg, SVGTemplateResult } from "lit";
import {
css,
CSSResultGroup,
LitElement,
nothing,
svg,
SVGTemplateResult,
} from "lit";
import { customElement, property } from "lit/decorators";
@customElement("ha-svg-icon")
export class HaSvgIcon extends LitElement {
@property() public path?: string;
@property() public secondaryPath?: string;
@property() public viewBox?: string;
protected render(): SVGTemplateResult {
@ -13,11 +22,20 @@ export class HaSvgIcon extends LitElement {
viewBox=${this.viewBox || "0 0 24 24"}
preserveAspectRatio="xMidYMid meet"
focusable="false"
role="img"
role="img"
aria-hidden="true"
>
<g>
${this.path ? svg`<path d=${this.path}></path>` : ""}
${
this.path
? svg`<path class="primary-path" d=${this.path}></path>`
: nothing
}
${
this.secondaryPath
? svg`<path class="secondary-path" d=${this.secondaryPath}></path>`
: nothing
}
</g>
</svg>`;
}
@ -30,7 +48,7 @@ export class HaSvgIcon extends LitElement {
justify-content: center;
position: relative;
vertical-align: middle;
fill: currentcolor;
fill: var(--icon-primary-color, currentcolor);
width: var(--mdc-icon-size, 24px);
height: var(--mdc-icon-size, 24px);
}
@ -40,6 +58,13 @@ export class HaSvgIcon extends LitElement {
pointer-events: none;
display: block;
}
path.primary-path {
opacity: var(--icon-primary-opactity, 1);
}
path.secondary-path {
fill: var(--icon-secondary-color, currentcolor);
opacity: var(--icon-secondary-opactity, 0.5);
}
`;
}
}

View File

@ -0,0 +1,320 @@
import {
addHasRemoveClass,
BaseElement,
} from "@material/mwc-base/base-element";
import { supportsPassiveEventListener } from "@material/mwc-base/utils";
import { MDCTopAppBarAdapter } from "@material/top-app-bar/adapter";
import { strings } from "@material/top-app-bar/constants";
import MDCFixedTopAppBarFoundation from "@material/top-app-bar/fixed/foundation";
import { html, css, nothing } from "lit";
import { property, query, customElement } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styles } from "@material/mwc-top-app-bar/mwc-top-app-bar.css";
import { haStyleScrollbar } from "../resources/styles";
export const passiveEventOptionsIfSupported = supportsPassiveEventListener
? { passive: true }
: undefined;
@customElement("ha-two-pane-top-app-bar-fixed")
export abstract class TopAppBarBaseBase extends BaseElement {
protected override mdcFoundation!: MDCFixedTopAppBarFoundation;
protected override mdcFoundationClass = MDCFixedTopAppBarFoundation;
@query(".mdc-top-app-bar") protected mdcRoot!: HTMLElement;
// _actionItemsSlot should have type HTMLSlotElement, but when TypeScript's
// emitDecoratorMetadata is enabled, the HTMLSlotElement constructor will
// be emitted into the runtime, which will cause an "HTMLSlotElement is
// undefined" error in browsers that don't define it (e.g. IE11).
@query('slot[name="actionItems"]') protected _actionItemsSlot!: HTMLElement;
protected _scrollTarget!: HTMLElement | Window;
@property({ type: Boolean }) centerTitle = false;
@property({ type: Boolean, reflect: true }) prominent = false;
@property({ type: Boolean, reflect: true }) dense = false;
@property({ type: Boolean }) pane = false;
@property({ type: Boolean }) footer = false;
@query(".content") private _contentElement!: HTMLElement;
@query(".pane .ha-scrollbar") private _paneElement?: HTMLElement;
@property({ type: Object })
get scrollTarget() {
return this._scrollTarget || window;
}
set scrollTarget(value) {
this.unregisterListeners();
const old = this.scrollTarget;
this._scrollTarget = value;
this.updateRootPosition();
this.requestUpdate("scrollTarget", old);
this.registerListeners();
}
protected updateRootPosition() {
if (this.mdcRoot) {
const windowScroller = this.scrollTarget === window;
// we add support for top-app-bar's tied to an element scroller.
this.mdcRoot.style.position = windowScroller ? "" : "absolute";
}
}
protected barClasses() {
return {
"mdc-top-app-bar--dense": this.dense,
"mdc-top-app-bar--prominent": this.prominent,
"center-title": this.centerTitle,
"mdc-top-app-bar--fixed": true,
"mdc-top-app-bar--pane": this.pane,
};
}
protected contentClasses() {
return {
"mdc-top-app-bar--fixed-adjust": !this.dense && !this.prominent,
"mdc-top-app-bar--dense-fixed-adjust": this.dense && !this.prominent,
"mdc-top-app-bar--prominent-fixed-adjust": !this.dense && this.prominent,
"mdc-top-app-bar--dense-prominent-fixed-adjust":
this.dense && this.prominent,
"mdc-top-app-bar--pane": this.pane,
};
}
protected override render() {
const title = html`<span class="mdc-top-app-bar__title"
><slot name="title"></slot
></span>`;
return html`
<header class="mdc-top-app-bar ${classMap(this.barClasses())}">
<div class="mdc-top-app-bar__row">
${this.pane
? html`<section
class="mdc-top-app-bar__section mdc-top-app-bar__section--align-start"
id="title"
>
<slot
name="navigationIcon"
@click=${this.handleNavigationClick}
></slot>
${title}
</section>`
: nothing}
<section class="mdc-top-app-bar__section" id="navigation">
${this.pane
? nothing
: html`<slot
name="navigationIcon"
@click=${this.handleNavigationClick}
></slot
>${title}`}
</section>
<section
class="mdc-top-app-bar__section mdc-top-app-bar__section--align-end"
id="actions"
role="toolbar"
>
<slot name="actionItems"></slot>
</section>
</div>
</header>
<div class=${classMap(this.contentClasses())}>
${this.pane
? html`<div class="pane">
<div class="shadow-container"></div>
<div class="ha-scrollbar">
<slot name="pane"></slot>
</div>
${this.footer
? html`<div class="footer">
<slot name="pane-footer"></slot>
</div>`
: nothing}
</div>`
: nothing}
<div class="main">
${this.pane ? html`<div class="shadow-container"></div>` : nothing}
<div class="content">
<slot></slot>
</div>
</div>
</div>
`;
}
protected updated(changedProperties) {
super.updated(changedProperties);
if (
changedProperties.has("pane") &&
changedProperties.get("pane") !== undefined
) {
this.unregisterListeners();
this.registerListeners();
}
}
protected createAdapter(): MDCTopAppBarAdapter {
return {
...addHasRemoveClass(this.mdcRoot),
setStyle: (prprty: string, value: string) =>
this.mdcRoot.style.setProperty(prprty, value),
getTopAppBarHeight: () => this.mdcRoot.clientHeight,
notifyNavigationIconClicked: () => {
this.dispatchEvent(
new Event(strings.NAVIGATION_EVENT, {
bubbles: true,
cancelable: true,
})
);
},
getViewportScrollY: () =>
this.scrollTarget instanceof Window
? this.scrollTarget.pageYOffset
: this.scrollTarget.scrollTop,
getTotalActionItems: () =>
(this._actionItemsSlot as HTMLSlotElement).assignedNodes({
flatten: true,
}).length,
};
}
protected handleTargetScroll = () => {
this.mdcFoundation.handleTargetScroll();
};
protected handlePaneScroll = (ev) => {
if (ev.target.scrollTop > 0) {
ev.target.parentElement.classList.add("scrolled");
} else {
ev.target.parentElement.classList.remove("scrolled");
}
};
protected handleNavigationClick = () => {
this.mdcFoundation.handleNavigationClick();
};
protected registerListeners() {
if (this.pane) {
this._paneElement!.addEventListener(
"scroll",
this.handlePaneScroll,
passiveEventOptionsIfSupported
);
this._contentElement.addEventListener(
"scroll",
this.handlePaneScroll,
passiveEventOptionsIfSupported
);
return;
}
this.scrollTarget.addEventListener(
"scroll",
this.handleTargetScroll,
passiveEventOptionsIfSupported
);
}
protected unregisterListeners() {
this._paneElement?.removeEventListener("scroll", this.handlePaneScroll);
this._contentElement.removeEventListener("scroll", this.handlePaneScroll);
this.scrollTarget.removeEventListener("scroll", this.handleTargetScroll);
}
protected override firstUpdated() {
super.firstUpdated();
this.updateRootPosition();
this.registerListeners();
}
override disconnectedCallback() {
super.disconnectedCallback();
this.unregisterListeners();
}
static override styles = [
styles,
haStyleScrollbar,
css`
.mdc-top-app-bar__row {
height: var(--header-height);
border-bottom: var(--app-header-border-bottom);
}
.mdc-top-app-bar--fixed-adjust {
padding-top: var(--header-height);
}
.shadow-container {
position: absolute;
top: calc(-1 * var(--header-height));
width: 100%;
height: var(--header-height);
z-index: 1;
transition: box-shadow 200ms linear;
}
.scrolled .shadow-container {
box-shadow: var(
--mdc-top-app-bar-fixed-box-shadow,
0px 2px 4px -1px rgba(0, 0, 0, 0.2),
0px 4px 5px 0px rgba(0, 0, 0, 0.14),
0px 1px 10px 0px rgba(0, 0, 0, 0.12)
);
}
.mdc-top-app-bar {
--mdc-typography-headline6-font-weight: 400;
color: var(--app-header-text-color, var(--mdc-theme-on-primary, #fff));
background-color: var(
--app-header-background-color,
var(--mdc-theme-primary)
);
}
.mdc-top-app-bar--pane.mdc-top-app-bar--fixed-scrolled {
box-shadow: none;
}
#title {
border-right: 1px solid rgba(255, 255, 255, 0.12);
box-sizing: border-box;
flex: 0 0 var(--sidepane-width, 250px);
width: var(--sidepane-width, 250px);
}
div.mdc-top-app-bar--pane {
display: flex;
height: calc(100vh - var(--header-height));
}
.pane {
border-right: 1px solid var(--divider-color);
box-sizing: border-box;
display: flex;
flex: 0 0 var(--sidepane-width, 250px);
width: var(--sidepane-width, 250px);
flex-direction: column;
position: relative;
}
.pane .ha-scrollbar {
flex: 1;
}
.pane .footer {
border-top: 1px solid var(--divider-color);
}
.main {
min-height: 100%;
}
.mdc-top-app-bar--pane .main {
position: relative;
flex: 1;
height: 100%;
}
.mdc-top-app-bar--pane .content {
height: 100%;
overflow: auto;
}
`,
];
}

View File

@ -298,7 +298,7 @@ export class HatScriptGraph extends LitElement {
.notEnabled=${disabled || config.enabled === false}
nofocus
></hat-graph-node>
${ensureArray(config.then).map((action, j) =>
${ensureArray(config.then ?? []).map((action, j) =>
this.render_action_node(
action,
`${path}/then/${j}`,

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