Compare commits

..
Author SHA1 Message Date
Maarten Lakerveld 91549f554e Add the color temp brightness picker gallery page to the e2e smoke list
The gallery smoke test only covers pages registered here, so the new
picker page was not exercised in CI.
2026-10-12 21:36:32 +02:00
Maarten Lakerveld 247d560aa0 Show dual-axis view for color lights that support color temperature 2026-10-12 21:36:32 +02:00
Maarten Lakerveld e15861a8fb Add dual-axis temperature/brightness view to light more-info dialog 2026-10-12 21:36:32 +02:00
Maarten Lakerveld 67376887b9 Add ha-color-temp-brightness-picker 2D pad component with gallery page 2026-10-12 14:17:17 +02:00
341 changed files with 3458 additions and 8977 deletions

No files matched your search

+2 -3
View File
@@ -15,12 +15,11 @@ jobs:
if: github.repository == 'home-assistant/frontend'
runs-on: ubuntu-latest
steps:
- name: Stale policy
- name: 90 days stale policy
uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-issue-stale: 90
days-before-pr-stale: 60
days-before-stale: 90
days-before-close: 7
operations-per-run: 25
remove-stale-when-updated: true
+1 -4
View File
@@ -115,10 +115,7 @@ const createRspackConfig = ({
dontHash = new Set();
}
const ignorePackages = bundle.ignorePackages({ latestBuild });
// pnpm links node_modules/lit-html into its store; rspack compares real paths
const litHtmlRoot = fs.realpathSync(
path.resolve(__dirname, "../node_modules/lit-html")
);
const litHtmlRoot = path.resolve(__dirname, "../node_modules/lit-html");
const litHtmlDevelopmentRoot = path.join(litHtmlRoot, "development");
const litDisableDevModeLoader = path.join(
__dirname,
+2 -2
View File
@@ -27,7 +27,6 @@ import type {
LovelaceConfig,
LovelaceDashboardStrategyConfig,
} from "../../../../src/data/lovelace/config/types";
import { normalizeLovelaceConfig } from "../../../../src/data/lovelace/config/normalize";
import { isStrategyDashboard } from "../../../../src/data/lovelace/config/types";
import { fetchResources } from "../../../../src/data/lovelace/resource";
import { loadLovelaceResources } from "../../../../src/panels/lovelace/common/load-resources";
@@ -35,6 +34,7 @@ import { HassElement } from "../../../../src/state/hass-element";
import { castContext } from "../cast_context";
import "./hc-launch-screen";
import { getPanelTitleFromUrlPath } from "../../../../src/data/panel";
import { checkLovelaceConfig } from "../../../../src/panels/lovelace/common/check-lovelace-config";
const DEFAULT_CONFIG: LovelaceDashboardStrategyConfig = {
strategy: {
@@ -359,7 +359,7 @@ export class HcMain extends HassElement {
this._urlPath || "lovelace"
);
castContext.setApplicationState(title || "");
this._lovelaceConfig = normalizeLovelaceConfig(
this._lovelaceConfig = checkLovelaceConfig(
lovelaceConfig
) as LovelaceConfig;
}
-3
View File
@@ -39,9 +39,6 @@
html {
background-color: var(--primary-background-color, #fafafa);
color: var(--primary-text-color, #212121);
/* Prevent iOS Safari from inflating text when rotating to landscape */
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}
@media (prefers-color-scheme: dark) {
html {
-1
View File
@@ -39,7 +39,6 @@ const backupInfo: BackupInfo = {
},
],
agent_errors: {},
agent_error_translations: {},
last_attempted_automatic_backup: recent,
last_completed_automatic_backup: recent,
last_action_event: { manager_state: "idle" },
+1
View File
@@ -59,6 +59,7 @@ export default [
"ha-control-select",
"ha-control-select-menu",
"ha-hs-color-picker",
"ha-color-temp-brightness-picker",
],
},
{
-10
View File
@@ -151,16 +151,6 @@ export const createMediaPlayerEntities = () => [
supported_features: 289,
},
},
{
entity_id: "media_player.theater_off_assumed",
state: "off",
attributes: {
friendly_name: "TV Off (assumed state, cannot be switched on)",
assumed_state: true,
// Off + Next + Pause
supported_features: 289,
},
},
{
entity_id: "media_player.theater_on_static",
state: "on",
-5
View File
@@ -12,11 +12,6 @@
<script type="module" src="<%= entry %>"></script>
<% } %>
<style>
html {
/* Prevent iOS Safari from inflating text when rotating to landscape */
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}
body {
font-family: Roboto, Noto, sans-serif;
margin: 0;
@@ -9,6 +9,7 @@ import "../../../../src/components/ha-form/ha-form";
import "../../../../src/components/ha-icon-button";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../../src/types";
const SCHEMA: HaFormSchema[] = [
{ type: "string", name: "Name", default: "", autofocus: true },
@@ -30,8 +31,11 @@ type DialogType =
export class DemoHaAdaptiveDialog extends LitElement {
@state() private _openDialog: DialogType = false;
@state() private _hass?: HomeAssistant;
protected firstUpdated() {
provideHass(this);
const hass = provideHass(this);
this._hass = hass;
}
protected render() {
@@ -85,6 +89,7 @@ export class DemoHaAdaptiveDialog extends LitElement {
</ha-card>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "basic"}
header-title="Basic adaptive dialog"
@closed=${this._handleClosed}
@@ -93,6 +98,7 @@ export class DemoHaAdaptiveDialog extends LitElement {
</ha-adaptive-dialog>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "basic-subtitle-below"}
header-title="Adaptive dialog with subtitle"
header-subtitle="This is an adaptive dialog with a subtitle below"
@@ -102,6 +108,7 @@ export class DemoHaAdaptiveDialog extends LitElement {
</ha-adaptive-dialog>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "basic-subtitle-above"}
header-title="Adaptive dialog with subtitle above"
header-subtitle="This is an adaptive dialog with a subtitle above"
@@ -112,6 +119,7 @@ export class DemoHaAdaptiveDialog extends LitElement {
</ha-adaptive-dialog>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "small"}
width="small"
header-title="Small adaptive dialog"
@@ -121,6 +129,7 @@ export class DemoHaAdaptiveDialog extends LitElement {
</ha-adaptive-dialog>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "large"}
width="large"
header-title="Large adaptive dialog"
@@ -130,6 +139,7 @@ export class DemoHaAdaptiveDialog extends LitElement {
</ha-adaptive-dialog>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "form"}
header-title="Adaptive dialog with form"
header-subtitle="This is an adaptive dialog with a form"
@@ -153,6 +163,7 @@ export class DemoHaAdaptiveDialog extends LitElement {
</ha-adaptive-dialog>
<ha-adaptive-dialog
.hass=${this._hass}
.allowModeChange=${this._openDialog === "allow-mode-change"}
header-title="Adaptive dialog with allow mode change"
header-subtitle="Resize the window while this dialog is open"
@@ -165,6 +176,7 @@ export class DemoHaAdaptiveDialog extends LitElement {
</ha-adaptive-dialog>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "actions"}
header-title="Adaptive dialog with actions"
header-subtitle="This is an adaptive dialog with header actions"
@@ -383,6 +395,7 @@ export class DemoHaAdaptiveDialog extends LitElement {
<h3>Example usage</h3>
<pre><code>&lt;ha-adaptive-dialog
.hass=\${this.hass}
open
header-title="Dialog title"
header-subtitle="Dialog subtitle"
@@ -594,7 +607,7 @@ export class DemoHaAdaptiveDialog extends LitElement {
<p>Example:</p>
<pre><code>&lt;ha-adaptive-dialog open&gt;
<pre><code>&lt;ha-adaptive-dialog .hass=\${this.hass} open&gt;
&lt;ha-form autofocus .schema=\${schema}&gt;&lt;/ha-form&gt;
&lt;/ha-adaptive-dialog&gt;</code></pre>
</div>
@@ -9,6 +9,7 @@ import "../../../../src/components/ha-form/ha-form";
import "../../../../src/components/ha-icon-button";
import type { HASSDomCurrentTargetEvent } from "../../../../src/common/dom/fire_event";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../../src/types";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
const SCHEMA: HaFormSchema[] = [
@@ -22,10 +23,12 @@ type PopoverType = false | "basic" | "small" | "large" | "form" | "actions";
export class DemoHaAdaptivePopover extends LitElement {
@state() private _openPopover: PopoverType = false;
@state() private _hass?: HomeAssistant;
@state() private _dialogAnchor?: HTMLElement;
protected firstUpdated() {
provideHass(this);
this._hass = provideHass(this);
}
protected render() {
@@ -69,6 +72,7 @@ export class DemoHaAdaptivePopover extends LitElement {
</ha-card>
<ha-adaptive-popover
.hass=${this._hass}
.open=${this._openPopover === "basic"}
.dialogAnchor=${this._dialogAnchor}
header-title="Basic adaptive popover"
@@ -82,6 +86,7 @@ export class DemoHaAdaptivePopover extends LitElement {
</ha-adaptive-popover>
<ha-adaptive-popover
.hass=${this._hass}
.open=${this._openPopover === "small"}
.dialogAnchor=${this._dialogAnchor}
width="small"
@@ -92,6 +97,7 @@ export class DemoHaAdaptivePopover extends LitElement {
</ha-adaptive-popover>
<ha-adaptive-popover
.hass=${this._hass}
.open=${this._openPopover === "large"}
.dialogAnchor=${this._dialogAnchor}
width="large"
@@ -102,6 +108,7 @@ export class DemoHaAdaptivePopover extends LitElement {
</ha-adaptive-popover>
<ha-adaptive-popover
.hass=${this._hass}
.open=${this._openPopover === "form"}
.dialogAnchor=${this._dialogAnchor}
header-title="Adaptive popover with form"
@@ -126,6 +133,7 @@ export class DemoHaAdaptivePopover extends LitElement {
</ha-adaptive-popover>
<ha-adaptive-popover
.hass=${this._hass}
.open=${this._openPopover === "actions"}
.dialogAnchor=${this._dialogAnchor}
header-title="Adaptive popover with actions"
@@ -51,12 +51,11 @@ The badge component is a small component that displays a number or status inform
**Properties/Attributes**
| Name | Type | Default | Description |
| --------------- | ----------------------- | ----------- | ------------------------------------------------------------- |
| type | `"badge"` or `"button"` | `"badge"` | If it's button it shows a ripple effect |
| label | string | `undefined` | Text label for the badge, only visible if `iconOnly = false` |
| iconOnly | boolean | `false` | Only show label |
| accessibleLabel | string | `undefined` | Accessible name of the button, only used if `iconOnly = true` |
| Name | Type | Default | Description |
| -------- | ----------------------- | ----------- | ------------------------------------------------------------ |
| type | `"badge"` or `"button"` | `"badge"` | If it's button it shows a ripple effect |
| label | string | `undefined` | Text label for the badge, only visible if `iconOnly = false` |
| iconOnly | boolean | `false` | Only show label |
**CSS Custom Properties**
@@ -0,0 +1,4 @@
---
title: Color temperature and brightness picker
subtitle: Dual-axis pad controlling color temperature and brightness together
---
@@ -0,0 +1,102 @@
import "../../../../src/components/ha-color-temp-brightness-picker";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, state } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-slider";
const MIN_KELVIN = 2000;
const MAX_KELVIN = 6500;
@customElement("demo-components-ha-color-temp-brightness-picker")
export class DemoHaColorTempBrightnessPicker extends LitElement {
@state()
value: [number, number] = [3000, 50];
@state()
liveValue?: [number, number];
private _valueCursor(ev) {
this.liveValue = ev.detail.value;
}
private _valueChanged(ev) {
this.value = ev.detail.value;
}
private _kelvinChanged(ev) {
this.value = [Number(ev.target.value), this.value[1]];
}
private _brightnessChanged(ev) {
this.value = [this.value[0], Number(ev.target.value)];
}
protected render(): TemplateResult {
const [kelvin, brightness] = this.liveValue ?? this.value;
return html`
<ha-card>
<div class="card-content">
<p class="value">${kelvin} K - ${brightness}%</p>
<ha-color-temp-brightness-picker
.value=${this.value}
.minKelvin=${MIN_KELVIN}
.maxKelvin=${MAX_KELVIN}
@value-changed=${this._valueChanged}
@cursor-moved=${this._valueCursor}
></ha-color-temp-brightness-picker>
<p>Color temperature: ${this.value[0]} K</p>
<ha-slider
labeled
step="1"
.min=${MIN_KELVIN}
.max=${MAX_KELVIN}
.value=${this.value[0]}
@change=${this._kelvinChanged}
>
</ha-slider>
<p>Brightness: ${this.value[1]}%</p>
<ha-slider
labeled
step="1"
min="1"
max="100"
.value=${this.value[1]}
@change=${this._brightnessChanged}
>
</ha-slider>
</div>
</ha-card>
`;
}
static styles = css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
.card-content {
display: flex;
align-items: center;
flex-direction: column;
}
ha-color-temp-brightness-picker {
width: 320px;
height: 320px;
}
.value {
font-size: var(--ha-font-size-xl);
font-weight: var(--ha-font-weight-bold);
margin: 0 0 12px 0;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-color-temp-brightness-picker": DemoHaColorTempBrightnessPicker;
}
}
@@ -63,25 +63,26 @@ means supplying one.
### Properties/Attributes
| Name | Type | Default | Description |
| ------------------ | ------------------ | --------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| getItems | Function | - | Required. Returns the items, optionally filtered by search string and section. |
| getAdditionalItems | Function | - | Extra items appended to search results, for example "add new". |
| value | String | - | Id of the current item. Marks its row `selected`, and the virtualized list opens with that row in view. |
| label | String | - | Placeholder for the search field. Falls back to a localized "Search". |
| mode | "popover"/"dialog" | "popover" | Adjusts padding for the surface the list sits in. |
| shown | Boolean | true | Whether the surface finished animating. `ha-generic-picker` sets it so the virtualizer does not measure rows mid-animation. |
| sections | Array | - | Filter chips. Section headers are plain strings returned by `getItems`, and the sticky title shows the one above the first visible row. |
| selectedSection | String | - | Section filter selected when the list is initialized. |
| searchKeys | Array | - | Fuse weighted keys for fuzzy search. |
| searchFn | Function | - | Post-processes filtered results. |
| rowRenderer | Function | - | Replaces the default row template. |
| allowCustomValue | Boolean | false | Offers the typed string as an item. |
| customValueLabel | String | - | Label for that custom item. |
| notFoundLabel | String/Function | - | Shown when a search matches nothing. |
| emptyLabel | String | - | Shown when there are no items at all. |
| noSort | Boolean | false | Keeps the order `getItems` returned. |
| clearable | Boolean | false | Adjusts search field padding for a clear affordance. |
| Name | Type | Default | Description |
| -------------------- | ------------------ | --------- | --------------------------------------------------------------------------------------------------------------------------- |
| getItems | Function | - | Required. Returns the items, optionally filtered by search string and section. |
| getAdditionalItems | Function | - | Extra items appended to search results, for example "add new". |
| value | String | - | Id of the current item. Marks its row `selected`, and the virtualized list opens with that row in view. |
| label | String | - | Placeholder for the search field. Falls back to a localized "Search". |
| mode | "popover"/"dialog" | "popover" | Adjusts padding for the surface the list sits in. |
| shown | Boolean | true | Whether the surface finished animating. `ha-generic-picker` sets it so the virtualizer does not measure rows mid-animation. |
| sections | Array | - | Filter chips. Section headers are plain strings returned by `getItems`. |
| selectedSection | String | - | Section filter selected when the list is initialized. |
| sectionTitleFunction | Function | - | Builds the sticky section title from the visible range. |
| searchKeys | Array | - | Fuse weighted keys for fuzzy search. |
| searchFn | Function | - | Post-processes filtered results. |
| rowRenderer | Function | - | Replaces the default row template. |
| allowCustomValue | Boolean | false | Offers the typed string as an item. |
| customValueLabel | String | - | Label for that custom item. |
| notFoundLabel | String/Function | - | Shown when a search matches nothing. |
| emptyLabel | String | - | Shown when there are no items at all. |
| noSort | Boolean | false | Keeps the order `getItems` returned. |
| clearable | Boolean | false | Adjusts search field padding for a clear affordance. |
### Events
@@ -102,13 +102,6 @@ const CONFIGS = [
entity: "media_player.theater_off_static",
},
},
{
heading: "Player Off (assumed state, cannot be switched on)",
config: {
type: "media-control",
entity: "media_player.theater_off_assumed",
},
},
{
heading: "Player On (cannot be switched off)",
config: {
@@ -290,6 +290,7 @@ export class DemoIntegrationCard extends LitElement {
class=${classMap({
highlight: info.highlight !== undefined,
})}
.hass=${this.hass}
domain="esphome"
.items=${info.items}
.manifest=${createManifest(
@@ -312,6 +313,7 @@ export class DemoIntegrationCard extends LitElement {
<!-- One that is standalone to see how it increases height if height
not defined by other cards. -->
<ha-integration-card
.hass=${this.hass}
domain="esphome"
.items=${[
loadedEntry,
+6 -6
View File
@@ -85,9 +85,9 @@
"@swc/helpers": "0.5.23",
"@tsparticles/engine": "4.4.0",
"@tsparticles/preset-links": "4.4.0",
"@versatiles/style": "6.4.0",
"@versatiles/style": "6.3.1",
"@vibrant/color": "4.0.4",
"@vvo/tzdb": "6.201.1",
"@vvo/tzdb": "6.198.0",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
"barcode-detector": "3.2.2",
"cally": "0.9.2",
@@ -113,7 +113,7 @@
"lit": "3.3.3",
"lit-html": "3.3.3",
"luxon": "3.7.2",
"maplibre-gl": "6.14.0",
"maplibre-gl": "6.13.0",
"marked": "18.1.0",
"memoize-one": "6.0.0",
"nanoid": "6.0.2",
@@ -168,7 +168,7 @@
"@types/leaflet.markercluster": "1.5.6",
"@types/lodash.merge": "4.6.9",
"@types/luxon": "3.7.6",
"@types/node": "24.19.2",
"@types/node": "24.19.1",
"@types/qrcode": "1.5.6",
"@types/sortablejs": "1.15.9",
"@types/tar": "7.0.87",
@@ -209,7 +209,7 @@
"map-stream": "0.0.7",
"minify-literals": "2.2.0",
"pinst": "3.0.0",
"prettier": "3.9.10",
"prettier": "3.9.9",
"rspack-manifest-plugin": "5.2.2",
"serve": "14.2.6",
"sinon": "22.1.0",
@@ -225,7 +225,7 @@
"webpackbar": "7.0.0",
"workbox-build": "7.4.1"
},
"packageManager": "[email protected].2",
"packageManager": "[email protected].0",
"volta": {
"node": "24.21.0"
}
+116 -116
View File
@@ -7,153 +7,153 @@ importers:
configDependencies: {}
packageManagerDependencies:
pnpm:
specifier: 12.11.2
version: 12.11.2
specifier: 12.11.0
version: 12.11.0
packages:
'@pnpm/[email protected].2':
resolution: {integrity: sha512-owLw13OfPkEoY+hLBNeeG+dCFhywr7q2d/EHPz4mNL2RhKnv8gNwMARkjkiDQu234aItVwFNDeGnmQXm+ikkAg==}
'@pnpm/[email protected].0':
resolution: {integrity: sha512-iaRous2TG9l3evU4PlfpyHEQYk7FgePfIUd/Duc1Ev2V+xAcN5IgsDRstvMSxa4zo04xtiGKTG06X1RW+knSVw==}
cpu: [arm64]
os: [android]
'@pnpm/[email protected].2':
resolution: {integrity: sha512-GYPkVebl4iokOA/4MPSpGoUVfFlsth7uqOCe1XiaiM23LAdvUOMJBObqHJijr89ZqjwHm9pO6vO45GRwJ2U2qA==}
'@pnpm/[email protected].0':
resolution: {integrity: sha512-3SFkB+pF5cdV0KdTUrNvH6Xjp1CJ81w0u4HD5aI5pLWsqOHCi90s78Ja2v9aSnNsONMzJ+TSSEStPRyMQLNh4A==}
cpu: [x64]
os: [android]
'@pnpm/[email protected].2':
resolution: {integrity: sha512-n9M4On66y8FDxUAKgMO1FXt0qihyG4e+56p3/F0uo8Bx9KplW8BwKVnXigzCJ8aNM6sVVPkOE6M9XXbeVIxnZA==}
'@pnpm/[email protected].0':
resolution: {integrity: sha512-xlh3Cuv2s0aTjStWpmO0VVifQSUEn0UPLn/KilQv893OdJ6kjfWReB/hI55OV1ppjO/xPiJ4CoS9KIGz3TdjSw==}
cpu: [arm64]
os: [darwin]
'@pnpm/[email protected].2':
resolution: {integrity: sha512-oSW7n3sBT0c0cyk4kj10T2Y6Mpm+OFEiCAWozgX039tdoI6yqJF1uaFD3sw7ugLdu6VV3+uY0pa1TUZIPdxDWA==}
'@pnpm/[email protected].0':
resolution: {integrity: sha512-/nDHVuVK+3gc9MtBi3fKmqWuukp8I6cfi0hLxtBDqJVCWPEhKx7auoH59RLg1WsAKsrULgs8FDHFtJKzscqSGg==}
cpu: [x64]
os: [darwin]
'@pnpm/[email protected].2':
resolution: {integrity: sha512-qbzc3KoDpa/Lkf8vs3jnDe6dUUbso+6eLIyssYFMiliLpYNxn8dirk11YoNnOQcdHVB50v5RFls1Yid6ttT5pg==}
'@pnpm/[email protected].0':
resolution: {integrity: sha512-XVkee+Ltk+WujwIhoOYTJrxxjiVvnjJDrjrhkZ4fcB4N6Klw129xEmUWGc5GCqQ5fIP0lQKgi/gERqtTDF76lg==}
cpu: [x64]
os: [freebsd]
'@pnpm/[email protected].2':
resolution: {integrity: sha512-uMEYDwKRktGwANubQ+kYCwmhNwg3Ik6qK89ahsbQ5cP30R/5FVJb/T99vr9XRUBC/A7mZI6eeT92oyVas9ACyg==}
'@pnpm/[email protected].0':
resolution: {integrity: sha512-P/dDOnqFG5t3oR6zBvxZ/19gpGCRTqTzDxY9XYhP56csybJrQ96WTCLh+E5/2VlOCps/Ote7dh127h5Z6fJEOQ==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@pnpm/[email protected].2':
resolution: {integrity: sha512-6e8ZAQUwFXAWHHHv5FesWqf/kEK04wuitBv5f/gxzkC0DiBfaixr+f+6JQede8SF4Ji5LcMwVFJbzAtffpWxEg==}
'@pnpm/[email protected].0':
resolution: {integrity: sha512-i8pwzaDfiyE6Aq6zOSUSWsUdSUP9aJw648q8HvAAhDCvd2ywYNkop2BvUfC3tcmg7gYt3XuEHNDKbp5FcIKAPg==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@pnpm/[email protected].2':
resolution: {integrity: sha512-Ui8A4nepkVupx2VpuFfysEluMvWK6gTB5rgVts7Ss/+8dj0RD28x3F/seNeH/zSypgC6yKdV6qsKVQh/uffTuQ==}
'@pnpm/[email protected].0':
resolution: {integrity: sha512-J0y8s/0d43yqYs69mu3v4mGS9UczmI99jNOo2XrrFAJw6XUWBSsjwiN6xd6OwiRa/1QP+joooGMUMOMgmSj5/A==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@pnpm/[email protected].2':
resolution: {integrity: sha512-i1yLdQ5bu+wjQ+srtFoJAaY2gN/+oXHFdjul4llCeoDxHE0rTIG3R41NhHfCjyFPW4+CpbBT58933CL9sMOCnA==}
'@pnpm/[email protected].0':
resolution: {integrity: sha512-XRJMy9cpSa/WqeQRwJJENBvZNg4o6iOjSlG/Nj3cR8p19367YP0WGDFsMn5QYSwUsQivFvZtQ7AHasGYoKOunA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@pnpm/[email protected].2':
resolution: {integrity: sha512-YS1/lhPjjTsN4QL50VaFZKRa3Uc/bDnhE8eKbJMFvuX06UuFS5YaiDg04APoZdfVSGqTfX27QAg7rfLMSWaqxA==}
'@pnpm/[email protected].0':
resolution: {integrity: sha512-oN2T6oMjPdav6WOsF0Zr3jh9KvLLUEDnwVy4kgOpWb2decAUhZKxO0bcEG1ozEJVQqe99Y96L8URLo9AYAiopA==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@pnpm/[email protected].2':
resolution: {integrity: sha512-7eIxQR05fvJxCujUSKp5KTtehJ7QqrgqP/HD1+BcB/SaV2Be4XI1PEHiOgA1zxsiMIeMvTF6E4G4dYHrFjeXRg==}
'@pnpm/[email protected].0':
resolution: {integrity: sha512-ThGTDSSTlBqsn5LOXrioEoNQXq7UBmiMmGWgThwRaZ4u9q8dEipeqYgXGOuYWuNkHOFr9QpDjgT4Ty2xmnU5ng==}
cpu: [x64]
os: [linux]
libc: [musl]
'@pnpm/[email protected].2':
resolution: {integrity: sha512-FK7QKe24AYgppJwO3mpooYptduuvIGqKJPi5ac7MdnWkjUy46JBfYhCW0grnkWKC8YOf0x+vGviuQqJEj3SpQQ==}
'@pnpm/[email protected].0':
resolution: {integrity: sha512-H78vY3QwTjPjj45Jke9XC4zcwDTBVVJHjvYbvh36xsG9Lz/UDtzUaHgIc6V3xZd5N4eWVldAETjHWPKe0lWpnQ==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@pnpm/[email protected].2':
resolution: {integrity: sha512-PCTjVDdJb6VvraZxrEeFv6EqEA0hhIsAySx28e95J2Tqpi88hLpgOXgNqV0vpxq9A+b8GLQfyk/D+1UlJPv/YQ==}
'@pnpm/[email protected].0':
resolution: {integrity: sha512-VKMsGYtmRtrqCbwKIJRwebR1qF4ODPqiOMCb5N2+qUHfjp3qdZ7IomSrt8QdwbJYcGe+ZMVcRL80zSbLgSNDLg==}
cpu: [arm64]
os: [win32]
'@pnpm/[email protected].2':
resolution: {integrity: sha512-8sgX86OvRjJ3yZs/TvGR2L31wH9ScPZ+f+geKI3ikuB0RRiK3i4LlTRO8OXN6pdRqj8epMde4312MCjj7JsJdA==}
'@pnpm/[email protected].0':
resolution: {integrity: sha512-tCIMq219fSc5xr+sd2ZTFr31sE5Shi9E6kka4VqlxwkwVSarKrhzMiArqaofcSDuUyPwHmjFfXmgE7cNee46kw==}
cpu: [x64]
os: [win32]
[email protected].2:
resolution: {integrity: sha512-JaUWUqGWeFr18ovabf6NxYLr76SsHRgCIc4TkCMcX7dmN4IupLbkYzznSs3LCnf3Ja8RutOBd/3t2u6BipVcuQ==}
[email protected].0:
resolution: {integrity: sha512-Kcy01B+twbOJBgyn4FuzABLKBLHRUCx1vAkwQh6ibYz7lKZ04YwA3E6BzAZhqxDTDQJZvr0SK8wEz/ahL2gs+w==}
engines: {node: '>=18.*'}
hasBin: true
snapshots:
'@pnpm/[email protected].2':
'@pnpm/[email protected].0':
optional: true
'@pnpm/[email protected].2':
'@pnpm/[email protected].0':
optional: true
'@pnpm/[email protected].2':
'@pnpm/[email protected].0':
optional: true
'@pnpm/[email protected].2':
'@pnpm/[email protected].0':
optional: true
'@pnpm/[email protected].2':
'@pnpm/[email protected].0':
optional: true
'@pnpm/[email protected].2':
'@pnpm/[email protected].0':
optional: true
'@pnpm/[email protected].2':
'@pnpm/[email protected].0':
optional: true
'@pnpm/[email protected].2':
'@pnpm/[email protected].0':
optional: true
'@pnpm/[email protected].2':
'@pnpm/[email protected].0':
optional: true
'@pnpm/[email protected].2':
'@pnpm/[email protected].0':
optional: true
'@pnpm/[email protected].2':
'@pnpm/[email protected].0':
optional: true
'@pnpm/[email protected].2':
'@pnpm/[email protected].0':
optional: true
'@pnpm/[email protected].2':
'@pnpm/[email protected].0':
optional: true
'@pnpm/[email protected].2':
'@pnpm/[email protected].0':
optional: true
[email protected].2:
[email protected].0:
optionalDependencies:
'@pnpm/exe.android-arm64': 12.11.2
'@pnpm/exe.android-x64': 12.11.2
'@pnpm/exe.darwin-arm64': 12.11.2
'@pnpm/exe.darwin-x64': 12.11.2
'@pnpm/exe.freebsd-x64': 12.11.2
'@pnpm/exe.linux-arm64': 12.11.2
'@pnpm/exe.linux-arm64-musl': 12.11.2
'@pnpm/exe.linux-ppc64': 12.11.2
'@pnpm/exe.linux-riscv64': 12.11.2
'@pnpm/exe.linux-s390x': 12.11.2
'@pnpm/exe.linux-x64': 12.11.2
'@pnpm/exe.linux-x64-musl': 12.11.2
'@pnpm/exe.win32-arm64': 12.11.2
'@pnpm/exe.win32-x64': 12.11.2
'@pnpm/exe.android-arm64': 12.11.0
'@pnpm/exe.android-x64': 12.11.0
'@pnpm/exe.darwin-arm64': 12.11.0
'@pnpm/exe.darwin-x64': 12.11.0
'@pnpm/exe.freebsd-x64': 12.11.0
'@pnpm/exe.linux-arm64': 12.11.0
'@pnpm/exe.linux-arm64-musl': 12.11.0
'@pnpm/exe.linux-ppc64': 12.11.0
'@pnpm/exe.linux-riscv64': 12.11.0
'@pnpm/exe.linux-s390x': 12.11.0
'@pnpm/exe.linux-x64': 12.11.0
'@pnpm/exe.linux-x64-musl': 12.11.0
'@pnpm/exe.win32-arm64': 12.11.0
'@pnpm/exe.win32-x64': 12.11.0
---
lockfileVersion: '9.0'
@@ -267,7 +267,7 @@ importers:
version: 6.1.21(@fullcalendar/[email protected])
'@home-assistant/webawesome':
specifier: 3.14.0-ha.0
version: 3.14.0-ha.0(@floating-ui/[email protected])(@types/[email protected].2)
version: 3.14.0-ha.0(@floating-ui/[email protected])(@types/[email protected].1)
'@lezer/common':
specifier: 1.5.3
version: 1.5.3
@@ -294,7 +294,7 @@ importers:
version: 1.0.3
'@maplibre/maplibre-gl-leaflet':
specifier: 0.1.4
version: 0.1.4(@types/[email protected])([email protected])([email protected]4.0)
version: 0.1.4(@types/[email protected])([email protected])([email protected]3.0)
'@material/mwc-list':
specifier: 0.27.0
version: 0.27.0(patch_hash=9450e509a86887d8f68ca9cf3f81e6ed70e94e49de13c5f6f20f94519ad4a987)
@@ -317,14 +317,14 @@ importers:
specifier: 4.4.0
version: 4.4.0
'@versatiles/style':
specifier: 6.4.0
version: 6.4.0([email protected]4.0)
specifier: 6.3.1
version: 6.3.1([email protected]3.0)
'@vibrant/color':
specifier: 4.0.4
version: 4.0.4
'@vvo/tzdb':
specifier: 6.201.1
version: 6.201.1
specifier: 6.198.0
version: 6.198.0
'@webcomponents/scoped-custom-element-registry':
specifier: 0.0.10
version: 0.0.10
@@ -401,8 +401,8 @@ importers:
specifier: 3.7.2
version: 3.7.2
maplibre-gl:
specifier: 6.14.0
version: 6.14.0
specifier: 6.13.0
version: 6.13.0
marked:
specifier: 18.1.0
version: 18.1.0
@@ -561,8 +561,8 @@ importers:
specifier: 3.7.6
version: 3.7.6
'@types/node':
specifier: 24.19.2
version: 24.19.2
specifier: 24.19.1
version: 24.19.1
'@types/qrcode':
specifier: 1.5.6
version: 1.5.6
@@ -684,8 +684,8 @@ importers:
specifier: 3.0.0
version: 3.0.0
prettier:
specifier: 3.9.10
version: 3.9.10
specifier: 3.9.9
version: 3.9.9
rspack-manifest-plugin:
specifier: 5.2.2
version: 5.2.2(@rspack/[email protected](@swc/[email protected]))
@@ -712,13 +712,13 @@ importers:
version: 8.71.1([email protected]([email protected]))([email protected])([email protected])
vite:
specifier: 8.3.4
version: 8.3.4(@types/[email protected].2)([email protected])([email protected])
version: 8.3.4(@types/[email protected].1)([email protected])([email protected])
vite-tsconfig-paths:
specifier: 6.1.1
version: 6.1.1([email protected])([email protected])([email protected](@types/[email protected].2)([email protected])([email protected]))
version: 6.1.1([email protected])([email protected])([email protected](@types/[email protected].1)([email protected])([email protected]))
vitest:
specifier: 5.0.3
version: 5.0.3(@types/[email protected].2)(@vitest/[email protected])([email protected])([email protected](@types/[email protected].2)([email protected])([email protected]))
version: 5.0.3(@types/[email protected].1)(@vitest/[email protected])([email protected])([email protected](@types/[email protected].1)([email protected])([email protected]))
webpack-stats-plugin:
specifier: 1.1.3
version: 1.1.3
@@ -3366,8 +3366,8 @@ packages:
'@types/[email protected]':
resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
'@types/[email protected].2':
resolution: {integrity: sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==}
'@types/[email protected].1':
resolution: {integrity: sha512-aS3/DG0oM05K0RIXXP+hKjinGG5IgSSVGzswZxW3O0sS3pH4/fycXundUC9XsszgKCk4gHXylTEK6hyFxVxnoQ==}
'@types/[email protected]':
resolution: {integrity: sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==}
@@ -3696,8 +3696,8 @@ packages:
cpu: [x64]
os: [win32]
'@versatiles/style@6.4.0':
resolution: {integrity: sha512-xBPqkEzxivOFRIt+722aygTngNZOw5UL4Qg4UaF1Ps3m+69pgkh16l86iFO7STKUj8c/1oAYF0wkP90r7sxwXQ==}
'@versatiles/style@6.3.1':
resolution: {integrity: sha512-Aa5WifgCvS6ytMv0NkJdQAHbP3rQg4o929N3VrXnwiKxqU8pG7Yg3+i+WbKx82oYZ+3cbPyUURLvSzC4tiFQWQ==}
engines: {node: '>=20'}
peerDependencies:
maplibre-gl: '>=5.0.0'
@@ -3775,8 +3775,8 @@ packages:
'@vscode/[email protected]':
resolution: {integrity: sha512-2ZUIRfhofZ/npLlf872EBnPmn27Kt4M2UssmQIfnJvgGgMYZJ5fvtHEDnttBBf2hnVtBgNCqZMVHJA+wsFVqTA==}
'@vvo/tzdb@6.201.1':
resolution: {integrity: sha512-ib7ymHHyiGD9N/WWGMX5951hmxaFnOP3W8l0ATUxaH0Y/NczPHWS4cQgJUd1S9zyjCq0od8WXxRHlnjlHNPdNg==}
'@vvo/tzdb@6.198.0':
resolution: {integrity: sha512-bNRWBhWYl0edVgyX6AYbhoCM2tk2lXJjGCyO2VDc2xn6Dw8dLd7WGj2DDXkVOkmOIQTNjEAcxrEpIzz5pWVwFg==}
'@webcomponents/[email protected]':
resolution: {integrity: sha512-wP4LF28aysE2Pq3NQRNQxko7Q0vOOwcoOSMg8FFI4S6z76UuXkYIc5ndC31dJMwso1/vSteL75LW2CEKedAJbA==}
@@ -5914,8 +5914,8 @@ packages:
[email protected]:
resolution: {integrity: sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==}
[email protected]4.0:
resolution: {integrity: sha512-L7U9y9TyPJ3P/JJbW+eH+A3FzdPq+3fnZFqqeCwf/S6cRGganHwVI5b/C8/zOYzreUzsloDPZVVaMQA94wOt4A==}
[email protected]3.0:
resolution: {integrity: sha512-ELXk4h+xl0URFQEnVbAN6MGn61P6jq4amUWHiskLVsvHCaKdf1Zq6G8UNOD0AQ3UaXoL5ECc8ovz8yqYEBGPKw==}
engines: {node: '>=16.14.0', npm: '>=8.1.0'}
[email protected]:
@@ -6479,8 +6479,8 @@ packages:
resolution: {integrity: sha512-E6rsNU1QNJgB3sjj7OANinGncFKuK+164sLXw1/CqBjj/EkXSoSdHCtWQGBNlREIGLnL7IEUEGa08YFVUbrhVg==}
engines: {node: '>=16'}
[email protected].10:
resolution: {integrity: sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==}
[email protected].9:
resolution: {integrity: sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==}
engines: {node: '>=14'}
hasBin: true
@@ -9499,12 +9499,12 @@ snapshots:
dependencies:
is-negated-glob: 1.0.0
'@home-assistant/[email protected](@floating-ui/[email protected])(@types/[email protected].2)':
'@home-assistant/[email protected](@floating-ui/[email protected])(@types/[email protected].1)':
dependencies:
'@ctrl/tinycolor': 4.1.0
'@floating-ui/dom': 1.8.0
'@konnorr/qr-creator': 1.1.0
'@lit-labs/ssr': 4.1.0(@types/[email protected].2)
'@lit-labs/ssr': 4.1.0(@types/[email protected].1)
'@lit-labs/ssr-client': 1.1.8
'@lit/context': 1.1.6
'@lit/react': 1.0.8
@@ -9740,7 +9740,7 @@ snapshots:
'@lit-labs/[email protected]': {}
'@lit-labs/[email protected](@types/[email protected].2)':
'@lit-labs/[email protected](@types/[email protected].1)':
dependencies:
'@lit-labs/ssr-client': 1.1.8
'@lit-labs/ssr-dom-shim': 1.6.0
@@ -9753,7 +9753,7 @@ snapshots:
node-fetch: 3.3.2
parse5: 7.3.0
optionalDependencies:
'@types/node': 24.19.2
'@types/node': 24.19.1
'@lit-labs/[email protected]':
dependencies:
@@ -9794,11 +9794,11 @@ snapshots:
dependencies:
kdbush: 4.1.0
'@maplibre/[email protected](@types/[email protected])([email protected])([email protected]4.0)':
'@maplibre/[email protected](@types/[email protected])([email protected])([email protected]3.0)':
dependencies:
'@types/leaflet': 1.9.22
leaflet: 1.9.4
maplibre-gl: 6.14.0
maplibre-gl: 6.13.0
'@maplibre/[email protected]':
dependencies:
@@ -10976,11 +10976,11 @@ snapshots:
'@types/[email protected]':
dependencies:
'@types/node': 24.19.2
'@types/node': 24.19.1
'@types/[email protected]':
dependencies:
'@types/node': 24.19.2
'@types/node': 24.19.1
'@types/[email protected]': {}
@@ -11038,7 +11038,7 @@ snapshots:
dependencies:
undici-types: 5.26.5
'@types/[email protected].2':
'@types/[email protected].1':
dependencies:
undici-types: 7.24.6
@@ -11046,11 +11046,11 @@ snapshots:
'@types/[email protected]':
dependencies:
'@types/node': 24.19.2
'@types/node': 24.19.1
'@types/[email protected]':
dependencies:
'@types/node': 24.19.2
'@types/node': 24.19.1
'@types/[email protected]': {}
@@ -11068,7 +11068,7 @@ snapshots:
'@types/[email protected]':
dependencies:
'@types/node': 24.19.2
'@types/node': 24.19.1
'@typescript-eslint/[email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected])':
dependencies:
@@ -11291,11 +11291,11 @@ snapshots:
'@unrs/[email protected]':
optional: true
'@versatiles/style@6.4.0([email protected]4.0)':
'@versatiles/style@6.3.1([email protected]3.0)':
dependencies:
'@maplibre/maplibre-gl-style-spec': 26.4.4
optionalDependencies:
maplibre-gl: 6.14.0
maplibre-gl: 6.13.0
'@vibrant/[email protected]': {}
@@ -11362,7 +11362,7 @@ snapshots:
obug: 2.1.4
std-env: 4.2.0
tinyrainbow: 3.1.1
vitest: 5.0.3(@types/[email protected].2)(@vitest/[email protected])([email protected])([email protected](@types/[email protected].2)([email protected])([email protected]))
vitest: 5.0.3(@types/[email protected].1)(@vitest/[email protected])([email protected])([email protected](@types/[email protected].1)([email protected])([email protected]))
'@vitest/[email protected]': {}
@@ -11370,14 +11370,14 @@ snapshots:
dependencies:
'@vitest/istanbul-lib-coverage': 1.0.1
'@vitest/[email protected]([email protected](@types/[email protected].2)([email protected])([email protected]))':
'@vitest/[email protected]([email protected](@types/[email protected].1)([email protected])([email protected]))':
dependencies:
'@jridgewell/trace-mapping': 0.3.31
'@vitest/spy': 5.0.3
estree-walker: 3.0.3
magic-string: 1.2.3
optionalDependencies:
vite: 8.3.4(@types/[email protected].2)([email protected])([email protected])
vite: 8.3.4(@types/[email protected].1)([email protected])([email protected])
'@vitest/[email protected]': {}
@@ -11385,7 +11385,7 @@ snapshots:
'@vscode/[email protected]': {}
'@vvo/tzdb@6.201.1': {}
'@vvo/tzdb@6.198.0': {}
'@webcomponents/[email protected]': {}
@@ -12237,7 +12237,7 @@ snapshots:
[email protected]([email protected]):
dependencies:
'@types/cors': 2.8.19
'@types/node': 24.19.2
'@types/node': 24.19.1
'@types/ws': 8.18.1
accepts: 1.3.8
base64id: 2.0.0
@@ -13330,7 +13330,7 @@ snapshots:
[email protected]:
dependencies:
'@types/node': 24.19.2
'@types/node': 24.19.1
merge-stream: 2.0.0
supports-color: 8.1.1
@@ -13669,7 +13669,7 @@ snapshots:
[email protected]: {}
[email protected]4.0:
[email protected]3.0:
dependencies:
'@mapbox/point-geometry': 1.1.0
'@mapbox/tiny-sdf': 2.2.0
@@ -14280,7 +14280,7 @@ snapshots:
[email protected]: {}
[email protected].10: {}
[email protected].9: {}
[email protected]: {}
@@ -15416,17 +15416,17 @@ snapshots:
- bare-abort-controller
- react-native-b4a
[email protected]([email protected])([email protected])([email protected](@types/[email protected].2)([email protected])([email protected])):
[email protected]([email protected])([email protected])([email protected](@types/[email protected].1)([email protected])([email protected])):
dependencies:
debug: 4.4.3([email protected])
globrex: 0.1.2
tsconfck: 3.1.6([email protected])
vite: 8.3.4(@types/[email protected].2)([email protected])([email protected])
vite: 8.3.4(@types/[email protected].1)([email protected])([email protected])
transitivePeerDependencies:
- supports-color
- typescript
[email protected](@types/[email protected].2)([email protected])([email protected]):
[email protected](@types/[email protected].1)([email protected])([email protected]):
dependencies:
lightningcss: 1.33.0
picomatch: 4.0.7
@@ -15434,15 +15434,15 @@ snapshots:
rolldown: 1.2.13
tinyglobby: 0.2.17
optionalDependencies:
'@types/node': 24.19.2
'@types/node': 24.19.1
fsevents: 2.3.3
terser: 5.50.0
yaml: 2.9.1
[email protected](@types/[email protected].2)(@vitest/[email protected])([email protected])([email protected](@types/[email protected].2)([email protected])([email protected])):
[email protected](@types/[email protected].1)(@vitest/[email protected])([email protected])([email protected](@types/[email protected].1)([email protected])([email protected])):
dependencies:
'@types/chai': 5.2.3
'@vitest/mocker': 5.0.3([email protected](@types/[email protected].2)([email protected])([email protected]))
'@vitest/mocker': 5.0.3([email protected](@types/[email protected].1)([email protected])([email protected]))
chai: 6.2.2
es-module-lexer: 2.3.2
expect-type: 1.4.0
@@ -15453,10 +15453,10 @@ snapshots:
tinybench: 6.2.0
tinyexec: 1.3.1
tinyglobby: 0.2.17
vite: 8.3.4(@types/[email protected].2)([email protected])([email protected])
vite: 8.3.4(@types/[email protected].1)([email protected])([email protected])
why-is-node-running: 3.2.1
optionalDependencies:
'@types/node': 24.19.2
'@types/node': 24.19.1
'@vitest/coverage-v8': 5.0.3([email protected])
jsdom: 30.1.2
transitivePeerDependencies:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 223 KiB

After

Width:  |  Height:  |  Size: 223 KiB

@@ -1,141 +0,0 @@
import type {
ReactiveController,
ReactiveControllerHost,
} from "@lit/reactive-element/reactive-controller";
import { mainWindow } from "../dom/get_main_window";
import { replaceCurrentUrl } from "../navigate";
import { constructUrlCurrentPath } from "../url/construct-url";
import { currentPath } from "../url/current-path";
import {
updateQueryString,
type QueryParamConfig,
type QueryParamValues,
} from "../url/query-params";
const DEBOUNCE_WAIT = 300;
export interface QueryParamsControllerOptions {
/** Whether the host keeps its state in the URL. */
enabled: () => boolean;
/**
* Called when the page of the host is navigated to while the host is
* connected, like a second link to an open page or coming back to it. Read
* the URL with `searchParams()` and apply it, then `write()` the state back.
*/
navigated: () => void;
}
/**
* Keeps part of a page's state in the query string of the URL, so it can be
* shared, bookmarked and reloaded. Writes replace the current history entry,
* so Back leaves the page instead of undoing each change, and only touch the
* config's params. The controller only acts while the URL is still the
* page the host was connected on.
*/
export class QueryParamsController<
C extends QueryParamConfig,
> implements ReactiveController {
private _path?: string;
private _pending?: QueryParamValues<C>;
private _pendingTimeout?: number;
private _hostUpdated = false;
constructor(
private _host: ReactiveControllerHost,
private _config: C,
private _options: QueryParamsControllerOptions
) {
this._host.addController(this);
}
public hostConnected() {
this._path = currentPath();
mainWindow.addEventListener("location-changed", this._navigated);
mainWindow.addEventListener("popstate", this._navigated);
// Before the first update the host reads the URL itself, after its
// initial state is set
if (this._hostUpdated) {
this._navigated();
}
}
public hostUpdated() {
this._hostUpdated = true;
}
public hostDisconnected() {
mainWindow.removeEventListener("location-changed", this._navigated);
mainWindow.removeEventListener("popstate", this._navigated);
this._cancelPending();
}
/** The current search params, or undefined when not on the host's page. */
public searchParams(): URLSearchParams | undefined {
if (!this._isActive()) {
return undefined;
}
return new URLSearchParams(mainWindow.location.search);
}
/**
* Write values to the URL. Pass `debounced` for values that change while
* typing; any later write replaces a pending one.
*/
public write(values: QueryParamValues<NoInfer<C>>, debounced = false) {
this._cancelPending();
if (debounced) {
this._pending = values;
this._pendingTimeout = window.setTimeout(
() => this.flush(),
DEBOUNCE_WAIT
);
return;
}
this._write(values);
}
/**
* Write a pending debounced write now, like when typing ends, before
* something like a dialog adds a history entry it would land on.
*/
public flush() {
const values = this._pending;
this._cancelPending();
if (values) {
this._write(values);
}
}
private _cancelPending() {
clearTimeout(this._pendingTimeout);
this._pendingTimeout = undefined;
this._pending = undefined;
}
private _isActive(): boolean {
return this._options.enabled() && currentPath() === this._path;
}
private _navigated = () => {
this._cancelPending();
// Leaving the page gets here too, before the router swaps the page
if (this._isActive()) {
this._options.navigated();
}
};
private _write(values: QueryParamValues<C>) {
if (!this._isActive()) {
return;
}
const current = mainWindow.location.search;
const queryString = updateQueryString(current, values, this._config);
if (current !== (queryString ? `?${queryString}` : "")) {
replaceCurrentUrl(constructUrlCurrentPath(queryString));
}
}
}
-1
View File
@@ -13,7 +13,6 @@ export const canOverrideAlphanumericInput = (composedPath: EventTarget[]) => {
if (
el.tagName === "TEXTAREA" ||
(el as HTMLElement).isContentEditable ||
el.parentElement?.tagName === "HA-SELECT" ||
el.parentElement?.tagName === "HA-DROPDOWN"
) {
+23 -1
View File
@@ -1,7 +1,12 @@
import memoizeOne from "memoize-one";
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
import type {
EntityRegistryDisplayEntry,
EntityRegistryEntry,
} from "../../data/entity/entity_registry";
import type { HomeAssistant } from "../../types";
import { getDuplicates } from "../string/get_duplicates";
import { computeStateName } from "./compute_state_name";
export const computeDeviceName = (
device: DeviceRegistryEntry
@@ -9,15 +14,32 @@ export const computeDeviceName = (
export const computeDeviceNameDisplay = (
device: DeviceRegistryEntry,
localize: HomeAssistant["localize"]
localize: HomeAssistant["localize"],
hassStates: HomeAssistant["states"],
entities?: EntityRegistryEntry[] | EntityRegistryDisplayEntry[] | string[]
) =>
computeDeviceName(device) ||
(entities && fallbackDeviceName(hassStates, entities)) ||
localize("ui.panel.config.devices.unnamed_device", {
type: localize(
`ui.panel.config.devices.type.${device.entry_type || "device"}`
),
});
export const fallbackDeviceName = (
hassStates: HomeAssistant["states"],
entities: EntityRegistryEntry[] | EntityRegistryDisplayEntry[] | string[]
) => {
for (const entity of entities || []) {
const entityId = typeof entity === "string" ? entity : entity.entity_id;
const stateObj = hassStates[entityId];
if (stateObj) {
return computeStateName(stateObj);
}
}
return undefined;
};
export const getDuplicatedDeviceNames = memoizeOne(
(devices: HomeAssistant["devices"]): Set<string> => {
const names = Object.values(devices)
+5 -4
View File
@@ -4,7 +4,6 @@ import type { HomeAssistant } from "../../types";
import { stringCompare } from "../string/compare";
import { computeDomain } from "./compute_domain";
import { computeStateDomain } from "./compute_state_domain";
import { computeZoneTrackerState } from "../../data/zone";
export const FIXED_DOMAIN_STATES = {
alarm_control_panel: [
@@ -276,10 +275,12 @@ export const getStatesDomain = (
result.push(
...Object.entries(hass.states)
.filter(
([entityId]) =>
computeDomain(entityId) === "zone" && entityId !== "zone.home"
([entityId, stateObj]) =>
computeDomain(entityId) === "zone" &&
entityId !== "zone.home" &&
stateObj.attributes.friendly_name
)
.map(([_entityId, stateObj]) => computeZoneTrackerState(stateObj))
.map(([_entityId, stateObj]) => stateObj.attributes.friendly_name!)
.sort((zone1, zone2) =>
stringCompare(zone1, zone2, hass.locale.language)
)
+21
View File
@@ -0,0 +1,21 @@
/**
* Sort function to help sort states by name
*
* Usage:
* const states = [state1, state2]
* states.sort(statessortStatesByName);
*/
import type { HassEntity } from "home-assistant-js-websocket";
import { computeStateName } from "./compute_state_name";
export const sortStatesByName = (entityA: HassEntity, entityB: HassEntity) => {
const nameA = computeStateName(entityA);
const nameB = computeStateName(entityB);
if (nameA < nameB) {
return -1;
}
if (nameA > nameB) {
return 1;
}
return 0;
};
+1 -3
View File
@@ -4,8 +4,6 @@ import { theme2hex } from "../color/convert-color";
/** The zone marker: a colored circle with the zone's icon or initials, shared by the map and the zone editor */
export const ZONE_CIRCLE_SIZE = 36;
// Same as an entity marker's icon
const ZONE_ICON_SIZE = 24;
// WCAG minimum contrast for icons and other graphics
const MIN_CONTENT_CONTRAST = 3;
@@ -70,6 +68,6 @@ export const zoneMarkerStyles = `
overflow: hidden;
font-size: var(--ha-font-size-s);
font-weight: var(--ha-font-weight-medium);
--mdc-icon-size: ${ZONE_ICON_SIZE}px;
--mdc-icon-size: ${ZONE_CIRCLE_SIZE / 2}px;
}
`;
-106
View File
@@ -1,106 +0,0 @@
import type {
DataTableColumnContainer,
SortingDirection,
} from "../../components/data-table/ha-data-table";
import {
decodeQueryParams,
hasQueryParams,
type QueryParamConfig,
type QueryParamValues,
type SearchParamsSource,
} from "./query-params";
export const dataTableQueryParamConfig = {
string: ["search", "sort", "direction", "group"],
} as const satisfies QueryParamConfig;
export type DataTableQueryParams = QueryParamValues<
typeof dataTableQueryParamConfig
>;
// How a data table is searched, sorted and grouped
export interface DataTableQueryState {
search: string;
sorting?: { column: string; direction: "asc" | "desc" };
groupColumn?: string;
}
// Sorting uses the column id unless the column has a name for the URL
const sortName = (columns: DataTableColumnContainer, columnId: string) =>
columns[columnId].urlName ?? columnId;
/**
* Params for the table's state. Empty and default values are left out, and
* grouping is only written for columns that have a name for the URL, so
* internal column ids don't end up in links.
*/
export const dataTableQueryParamsFromState = (
columns: DataTableColumnContainer,
search: string,
sortColumn: string | undefined,
sortDirection: SortingDirection,
groupColumn: string | undefined
): DataTableQueryParams => {
const params: DataTableQueryParams = {};
if (search) {
params.search = search;
}
if (sortColumn && sortDirection && columns[sortColumn]?.sortable) {
params.sort = sortName(columns, sortColumn);
if (sortDirection === "desc") {
params.direction = "desc";
}
}
if (groupColumn && columns[groupColumn]?.groupable) {
const group = columns[groupColumn].urlName;
if (group) {
params.group = group;
}
}
return params;
};
/**
* The table's state a link asks for, undefined when it has no table params so
* what was picked before stays. A link replaces the search, a search left
* behind would hide part of what it shows. Sorting and grouping it leaves out,
* or that don't match a column, keep what was picked before.
*/
export const dataTableQueryStateFromUrl = (
searchParams: SearchParamsSource,
columns: DataTableColumnContainer
): DataTableQueryState | undefined => {
if (!hasQueryParams(searchParams, dataTableQueryParamConfig)) {
return undefined;
}
const { search, sort, direction, group } = decodeQueryParams(
searchParams,
dataTableQueryParamConfig
);
const state: DataTableQueryState = { search: search ?? "" };
const sortColumn = sort
? Object.keys(columns).find(
(id) => columns[id].sortable && sortName(columns, id) === sort
)
: undefined;
if (sortColumn) {
state.sorting = {
column: sortColumn,
direction: direction === "desc" ? "desc" : "asc",
};
}
const groupColumn = group
? Object.keys(columns).find(
(id) => columns[id].groupable && columns[id].urlName === group
)
: undefined;
if (groupColumn) {
state.groupColumn = groupColumn;
}
return state;
};
@@ -1,26 +0,0 @@
import {
createQueryString,
decodeQueryParams,
type QueryParamConfig,
type QueryParamValues,
type SearchParamsSource,
} from "./query-params";
// The filters of the Marketplace tables. Search, sorting and grouping are kept
// by the data table, see data-table-query-params.
export const marketplaceFilterQueryParamConfig = {
list: ["status", "type", "source"],
} as const satisfies QueryParamConfig;
export type MarketplaceFilterQueryParams = QueryParamValues<
typeof marketplaceFilterQueryParamConfig
>;
export const decodeMarketplaceFilterQueryParams = (
searchParams: SearchParamsSource
): MarketplaceFilterQueryParams =>
decodeQueryParams(searchParams, marketplaceFilterQueryParamConfig);
export const createMarketplaceFilterQueryString = (
values: MarketplaceFilterQueryParams
): string => createQueryString(values, marketplaceFilterQueryParamConfig);
-45
View File
@@ -134,51 +134,6 @@ export function createQueryString(
return searchParams.toString();
}
const queryParamKeys = (config: QueryParamConfig): string[] => [
...(config.list ?? []),
...(config.date ?? []),
...(config.boolean ?? []).map(({ key }) => key),
...(config.string ?? []),
];
/** Whether the search params hold any of the config's params. */
export const hasQueryParams = (
searchParams: SearchParamsSource,
config: QueryParamConfig
): boolean =>
queryParamKeys(config).some(
(key) => getSearchParam(searchParams, key) !== null
);
/**
* Replace the config's params in a query string with the given values,
* keeping every other param exactly as it is written.
*/
export function updateQueryString<C extends QueryParamConfig>(
current: string,
values: QueryParamValues<NoInfer<C>>,
config: C
): string;
export function updateQueryString(
current: string,
values: Record<string, QueryParamValue | undefined>,
config: QueryParamConfig
): string {
const keys = queryParamKeys(config);
const kept = current
.replace(/^\?/, "")
.split("&")
.filter((part) => {
if (!part) {
return false;
}
const [key] = new URLSearchParams(part).keys();
return !keys.includes(key);
});
const updated = createQueryString(values, config);
return [...kept, ...(updated ? [updated] : [])].join("&");
}
export const serviceTargetFromQueryParams = <
Key extends keyof HassServiceTarget & string,
>(
-68
View File
@@ -1,68 +0,0 @@
/**
* Keeps a locally edited value from being overwritten by entity state updates
* while the change is waiting to be sent or the service call is in flight.
*
* Call `schedule()` when the user changes the value and a (debounced) service
* call is queued, wrap the service call in `run()`, and only copy entity state
* into the local value when `shouldSync()` returns true. After a successful
* call the local value is kept until the next state update syncs it. When the
* call fails, the `resync` callback restores the entity state.
*
* Only the latest call is tracked, as it supersedes any earlier ones.
*/
export class PendingValueSync {
private _scheduled = false;
private _latestCall?: symbol;
public get pending(): boolean {
return this._scheduled || this._latestCall !== undefined;
}
/** Mark that a service call has been scheduled for a local change. */
public schedule(): void {
this._scheduled = true;
}
/**
* Start over, e.g. when switching entities, so a pending change is not
* shown for (or sent to) a different entity. Calls already in flight are
* not aborted, but no longer block state updates or trigger `resync`.
*/
public reset(): void {
this._scheduled = false;
this._latestCall = undefined;
}
/** Whether an entity state update may be copied into the local value. */
public shouldSync(): boolean {
return !this.pending;
}
/**
* Run the service call for the scheduled change. If the latest call fails
* and no new change is scheduled, `resync` is called. Errors are not
* rethrown, as `callService` already notifies the user when the call fails.
*/
public async run(
call: () => Promise<unknown>,
resync: () => void
): Promise<void> {
const thisCall = Symbol("call");
this._scheduled = false;
this._latestCall = thisCall;
let failed = false;
try {
await call();
} catch (_err) {
failed = true;
}
if (this._latestCall !== thisCall) {
return;
}
this._latestCall = undefined;
if (failed && !this._scheduled) {
resync();
}
}
}
@@ -7,7 +7,6 @@ import memoizeOne from "memoize-one";
import { consume } from "../../common/decorators/consume";
import { truncateWithEllipsis } from "../../common/string/truncate-with-ellipsis";
import type { Condition } from "../../data/automation";
import { getRowNote } from "../../data/automation";
import type { ConditionDescription } from "../../data/condition";
import { internationalizationContext } from "../../data/context";
import "../../panels/config/automation/ha-automation-row-options";
@@ -43,7 +42,7 @@ export class HaAutomationConditionSummary extends LitElement {
this.condition?.condition === "numeric_state";
const targetRequired =
(descriptionHasTarget || hasEntityTarget) && !this.isNew;
const note = getRowNote(this.condition?.note);
const note = this.condition?.note?.trim();
let target: HassServiceTarget | undefined;
if (this.condition) {
@@ -66,15 +66,15 @@ export class DialogDataTableSettings extends LitElement {
}
private _preserveLastFixed(columnOrder) {
let strippedColumnOrder;
const lastFixedCount = this._lastFixedCount();
if (!lastFixedCount || !columnOrder) {
return columnOrder;
if (lastFixedCount && columnOrder) {
strippedColumnOrder = [...columnOrder];
strippedColumnOrder.splice(
columnOrder.length - lastFixedCount,
lastFixedCount
);
}
const strippedColumnOrder = [...columnOrder];
strippedColumnOrder.splice(
columnOrder.length - lastFixedCount,
lastFixedCount
);
return strippedColumnOrder;
}
+30 -281
View File
@@ -1,7 +1,3 @@
import type {
LitVirtualizer,
VisibilityChangedEvent,
} from "@lit-labs/virtualizer";
import type { ContextType } from "@lit/context";
import { mdiArrowDown, mdiArrowUp, mdiChevronUp } from "@mdi/js";
import deepClone from "deep-clone-simple";
@@ -79,8 +75,6 @@ export interface DataTableSortColumnData {
valueColumn?: string;
direction?: SortingDirection;
groupable?: boolean;
// Name in the `sort` and `group` URL params, grouping is only kept with one
urlName?: string;
moveable?: boolean;
hideable?: boolean;
defaultHidden?: boolean;
@@ -197,29 +191,7 @@ export class HaDataTable extends LitElement {
@query(".mdc-data-table__header-row") private _headerRow?: HTMLDivElement;
@query("lit-virtualizer")
private _scroller?: LitVirtualizer<DataTableRowData>;
@query(".sticky-group-header") private _stickyGroupHeader?: HTMLElement;
@query(".sticky-group-header .group-header")
private _stickyGroupHeaderCell?: HTMLElement;
// Group pinned to the top of the scroller while its own header is scrolled out of view
@state() private _stickyGroup?: string;
// Position and width of the sticky group header, applied directly on scroll
// to avoid re-rendering the table on every frame
private _stickyGroupPosition = { top: 0, width: 0, scrollLeft: 0 };
private _stickyGroupFrame?: number;
// Resizing doesn't scroll, but changes the width of the sticky group header
private _resizeObserver = new ResizeObserver(() =>
this._scheduleStickyGroupUpdate()
);
private _firstVisibleIndex = 0;
@query("lit-virtualizer") private _scroller?: HTMLElement;
@state() private _collapsedGroups: string[] = [];
@@ -265,7 +237,6 @@ export class HaDataTable extends LitElement {
public connectedCallback() {
super.connectedCallback();
this._resizeObserver.observe(this);
if (this._filteredData?.length) {
// Force update of location of rows
this._filteredData = [...this._filteredData];
@@ -276,21 +247,7 @@ export class HaDataTable extends LitElement {
this.updateComplete.then(() => this._calcTableHeight());
}
public disconnectedCallback() {
super.disconnectedCallback();
this._resizeObserver.disconnect();
if (this._stickyGroupFrame !== undefined) {
cancelAnimationFrame(this._stickyGroupFrame);
this._stickyGroupFrame = undefined;
}
}
protected updated(changedProps: PropertyValues<this>) {
// The sticky group header may have just been rendered, and the items may
// have changed (filter, sort, collapse) without the visible range changing
this._positionStickyGroupHeader();
this._scheduleStickyGroupUpdate();
if (!this._headerRow) {
return;
}
@@ -386,7 +343,6 @@ export class HaDataTable extends LitElement {
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
} else if (properties.has("groupColumn")) {
this._collapsedGroups = [];
this._stickyGroup = undefined;
this._lastSelectedRowId = null;
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
}
@@ -417,12 +373,6 @@ export class HaDataTable extends LitElement {
}
}
private _memRenderRow = memoizeOne(
(columns: DataTableColumnContainer, narrow: boolean) =>
(row: DataTableRowData, index: number) =>
this._renderRow(columns, narrow, row, index)
);
private _sortedColumns = memoizeOne(
(columns: DataTableColumnContainer, columnOrder?: string[]) => {
if (!columnOrder || !columnOrder.length) {
@@ -458,7 +408,8 @@ export class HaDataTable extends LitElement {
protected render() {
const columns = this._sortedColumns(this.columns, this.columnOrder);
const renderRow = this._memRenderRow(columns, this.narrow);
const renderRow = (row: DataTableRowData, index: number) =>
this._renderRow(columns, this.narrow, row, index);
const filteredDataLength = this._filteredData?.length || 0;
@@ -704,12 +655,9 @@ export class HaDataTable extends LitElement {
: html`
<lit-virtualizer
scroller
class="mdc-data-table__content scroller ha-scrollbar ${classMap(
{ grouped: Boolean(this.groupColumn) && !this.autoHeight }
)}"
class="mdc-data-table__content scroller ha-scrollbar"
tabindex=${ifDefined(!this.autoHeight ? "0" : undefined)}
@scroll=${this._saveScrollPos}
@visibilityChanged=${this._handleVisibilityChanged}
.items=${this._groupData(
this._filteredData,
this._i18n?.localize,
@@ -724,25 +672,6 @@ export class HaDataTable extends LitElement {
.keyFunction=${this._keyFunction}
.renderItem=${renderRow}
></lit-virtualizer>
${
this.groupColumn &&
!this.autoHeight &&
this._stickyGroup !== undefined
? html`<div
class="mdc-data-table__row sticky-group-header"
aria-hidden="true"
.group=${this._stickyGroup}
@click=${this._collapseStickyGroup}
>
${this._renderGroupHeader(
this._stickyGroup,
this._collapsedGroups.includes(this._stickyGroup),
this._i18n?.localize,
true
)}
</div>`
: nothing
}
`
}
</div>
@@ -763,14 +692,7 @@ export class HaDataTable extends LitElement {
return nothing;
}
if (row.append) {
return html`<div
class="mdc-data-table__row ${classMap({
"group-header-row": row.groupHeader !== undefined,
})}"
.group=${row.groupHeader}
>
${row.content}
</div>`;
return html`<div class="mdc-data-table__row">${row.content}</div>`;
}
if (row.empty) {
return html`<div class="mdc-data-table__row empty-row"></div>`;
@@ -878,13 +800,7 @@ export class HaDataTable extends LitElement {
key: string,
column: DataTableColumnData
): boolean {
// The grouped value is already shown in the sticky group header, which
// isn't available for auto-height tables
if (
column.main ||
column.showNarrow ||
(!this.autoHeight && key === this.groupColumn)
) {
if (column.main || column.showNarrow) {
return false;
}
return this._isColumnVisible(key, column);
@@ -1032,15 +948,33 @@ export class HaDataTable extends LitElement {
const groupedItems: DataTableRowData[] = [];
sortedEntries.forEach(([groupName, rows]) => {
const collapsed = collapsedGroups.includes(groupName);
groupedItems.push({
append: true,
selectable: false,
groupHeader: groupName,
content: this._renderGroupHeader(
groupName,
collapsedGroups.includes(groupName),
localize
),
content: html`<div
class="mdc-data-table__cell group-header"
role="cell"
.group=${groupName}
@click=${this._collapseGroup}
>
<ha-icon-button
.path=${mdiChevronUp}
.label=${
localize?.(
`ui.components.data-table.${collapsed ? "expand" : "collapse"}`
) || (collapsed ? "Expand" : "Collapse")
}
class=${collapsed ? "collapsed" : ""}
>
</ha-icon-button>
${
groupName === UNDEFINED_GROUP_KEY
? localize?.("ui.components.data-table.ungrouped") ||
"Ungrouped"
: groupName || ""
}
</div>`,
});
if (!collapsedGroups.includes(groupName)) {
groupedItems.push(...rows);
@@ -1061,40 +995,6 @@ export class HaDataTable extends LitElement {
}
);
private _renderGroupHeader(
groupName: string,
collapsed: boolean,
localize: LocalizeFunc | undefined,
sticky = false
) {
// The sticky copy is hidden from assistive technology, so its button is
// made inert. It handles clicks on its whole row instead, as its cell
// shifts with horizontal scrolling.
return html`<div
class="mdc-data-table__cell group-header"
role="cell"
.group=${groupName}
@click=${sticky ? nothing : this._collapseGroup}
>
<ha-icon-button
.path=${mdiChevronUp}
.label=${
localize?.(
`ui.components.data-table.${collapsed ? "expand" : "collapse"}`
) || (collapsed ? "Expand" : "Collapse")
}
class=${collapsed ? "collapsed" : ""}
?inert=${sticky}
>
</ha-icon-button>
${
groupName === UNDEFINED_GROUP_KEY
? localize?.("ui.components.data-table.ungrouped") || "Ungrouped"
: groupName || ""
}
</div>`;
}
private _memFilterData = memoizeOne(
(
data: DataTableRowData[],
@@ -1297,101 +1197,6 @@ export class HaDataTable extends LitElement {
if (this._headerRow) {
this._headerRow.scrollLeft = e.target.scrollLeft;
}
this._scheduleStickyGroupUpdate();
}
private _handleVisibilityChanged(ev: VisibilityChangedEvent) {
this._firstVisibleIndex = ev.first;
// Fired before the virtualizer positions its children, so measure later
this._scheduleStickyGroupUpdate();
}
private _scheduleStickyGroupUpdate() {
if (!this.groupColumn) {
this._stickyGroup = undefined;
return;
}
if (this._stickyGroupFrame !== undefined) {
return;
}
this._stickyGroupFrame = requestAnimationFrame(() => {
this._stickyGroupFrame = undefined;
this._updateStickyGroup();
});
}
/**
* The virtualizer positions rows absolutely, so `position: sticky` can't be
* used. Instead, an overlay copy of the current group header is shown at the
* top of the scroller, and the next group header slides under it. The
* overlay doesn't move with the scroll, as browsers scroll off the main
* thread and it would lag behind the rows.
*/
private _updateStickyGroup() {
const scroller = this._scroller;
if (
!scroller ||
!this.groupColumn ||
this.autoHeight ||
scroller.scrollTop <= 0
) {
this._stickyGroup = undefined;
return;
}
const scrollerTop = scroller.getBoundingClientRect().top;
let group: string | undefined;
let currentTop = -Infinity;
scroller
.querySelectorAll<HTMLElement & { group: string }>(".group-header-row")
.forEach((row) => {
const top = row.getBoundingClientRect().top - scrollerTop;
// Rows can sit at fractional positions, so a header less than a
// pixel below the top counts as reached
if (top < 1 && top > currentTop) {
currentTop = top;
group = row.group;
}
});
if (group === undefined) {
// The current group header is not rendered, find it from the first visible row
const items = scroller.items;
for (
let i = Math.min(this._firstVisibleIndex, items.length - 1);
i >= 0;
i--
) {
if (items[i]?.append && items[i].groupHeader !== undefined) {
group = items[i].groupHeader;
break;
}
}
}
this._stickyGroup = group;
this._stickyGroupPosition = {
top: scroller.offsetTop,
width: scroller.clientWidth,
scrollLeft: scroller.scrollLeft,
};
this._positionStickyGroupHeader();
}
private _positionStickyGroupHeader() {
const header = this._stickyGroupHeader;
if (!header) {
return;
}
const { top, width, scrollLeft } = this._stickyGroupPosition;
header.style.width = `${width}px`;
header.style.transform = `translateY(${top}px)`;
// Follow the horizontal scroll of the group header rows it covers
if (this._stickyGroupHeaderCell) {
this._stickyGroupHeaderCell.style.transform = `translateX(${-scrollLeft}px)`;
}
}
@eventOptions({ passive: true })
@@ -1403,40 +1208,6 @@ export class HaDataTable extends LitElement {
this._scroller.scrollLeft = e.target.scrollLeft;
}
private _collapseStickyGroup = async (
ev: HASSDomCurrentTargetEvent<HTMLElement & { group: string }>
) => {
const groupName = ev.currentTarget.group;
this._collapseGroup(ev);
await this.updateComplete;
const scroller = this._scroller;
if (!scroller) {
return;
}
// Keep the toggled group in view, its rows may have been above the viewport.
// Scroll a rendered header to the very top ourselves, as scrollIntoView
// would leave room for the sticky header (scroll-padding-top).
const row = Array.from(
scroller.querySelectorAll<HTMLElement & { group: string }>(
".group-header-row"
)
).find((headerRow) => headerRow.group === groupName);
if (row) {
scroller.scrollTop +=
row.getBoundingClientRect().top - scroller.getBoundingClientRect().top;
return;
}
const index = scroller.items.findIndex(
(item) => item?.append && item.groupHeader === groupName
);
if (index !== -1) {
// The header isn't rendered, but element() still returns a proxy for
// any index in the items: its scrollIntoView() pins that index in the
// virtualizer's layout, which renders it and aligns it to the top
scroller.element(index)?.scrollIntoView({ block: "start" });
}
};
private _collapseGroup = (
ev: HASSDomCurrentTargetEvent<HTMLElement & { group: string }>
) => {
@@ -1791,28 +1562,6 @@ export class HaDataTable extends LitElement {
transform: rotate(180deg);
}
.sticky-group-header {
position: absolute;
top: 0;
inset-inline-start: 0;
z-index: 1;
/* Match the top border of the group header rows it covers */
border-top: 1px solid transparent;
border-bottom: 1px solid var(--divider-color);
background-color: var(--primary-background-color);
will-change: transform;
}
.sticky-group-header .group-header {
/* Don't paint over the bottom divider */
background-color: transparent;
}
/* Keep focused rows clear of the sticky group header */
lit-virtualizer.grouped {
scroll-padding-top: var(--data-table-row-height, 52px);
}
:host {
display: block;
}
@@ -56,7 +56,6 @@ export abstract class HaDeviceAutomationPicker<
private _localizeDeviceAutomation: (
localize: LocalizeFunc,
states: HassEntities,
formatEntityName: HomeAssistant["formatEntityName"],
entityRegistry: EntityRegistryEntry[],
automation: T
) => string;
@@ -106,6 +105,7 @@ export abstract class HaDeviceAutomationPicker<
const value = this._value;
return html`<ha-generic-picker
.hass=${this.hass}
.label=${this.label}
.value=${value}
.disabled=${!this._automations || this._automations.length === 0}
@@ -139,7 +139,6 @@ export abstract class HaDeviceAutomationPicker<
const primary = this._localizeDeviceAutomation(
this.hass.localize,
this.hass.states,
this.hass.formatEntityName,
this._entityReg,
automation
);
@@ -187,7 +186,6 @@ export abstract class HaDeviceAutomationPicker<
? this._localizeDeviceAutomation(
this.hass.localize,
this.hass.states,
this.hass.formatEntityName,
this._entityReg,
described
)
@@ -429,6 +429,7 @@ export class HaDevicePicker extends LitElement {
return html`
<ha-generic-picker
.noUnknownState=${canReplace}
.hass=${this.hass}
.autofocus=${this.autofocus}
.disabled=${this.disabled}
.helper=${this.helper}
@@ -84,6 +84,7 @@ class HaEntityAttributePicker extends LitElement {
return html`
<ha-generic-picker
.hass=${this.hass}
.value=${this.value}
.label=${
this.label ??
@@ -205,6 +205,7 @@ export class HaEntityNamePicker extends LitElement {
return html`
<ha-generic-picker
.hass=${this.hass}
.disabled=${this.disabled}
.required=${this.required && !value.length}
.getItems=${this._getFilteredItems}
@@ -77,9 +77,6 @@ export class HaEntityPicker extends LitElement {
@property() public label?: string;
@property({ attribute: "aria-label" })
public ariaLabel: string | null = null;
@property() public value?: string;
@property() public helper?: string;
@@ -452,7 +449,6 @@ export class HaEntityPicker extends LitElement {
.allowCustomValue=${this.allowCustomEntity}
.required=${this.required}
.label=${this.label}
.ariaLabel=${this.ariaLabel}
.placeholder=${placeholder}
.helper=${this.helper}
.value=${this.addButton ? undefined : this.value}
@@ -261,6 +261,7 @@ export class HaStateContentPicker extends LitElement {
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
<ha-generic-picker
.hass=${this.hass}
.disabled=${this.disabled}
.required=${this.required && !value.length}
.value=${this._getPickerValue()}
@@ -138,6 +138,7 @@ export class HaEntityStatePicker extends LitElement {
return html`
<ha-generic-picker
.hass=${this.hass}
.allowCustomValue=${this.allowCustomValue}
.disabled=${this.disabled || noEntity}
.autofocus=${this.autofocus}
@@ -511,6 +511,7 @@ export class HaStatisticPicker extends LitElement {
return html`
<ha-generic-picker
.hass=${this.hass}
.autofocus=${this.autofocus}
.allowCustomValue=${this.allowCustomEntity}
.disabled=${this.disabled}
+2 -5
View File
@@ -230,7 +230,7 @@ export class StateBadge extends LitElement {
const domain = computeDomain(this.stateObj.entity_id);
if (domain === "update") {
cls.set("has-no-radius", true);
} else if (domain === "media_player") {
} else if (domain === "media_player" || domain === "camera") {
cls.set("has-media-image", true);
} else if (this.style.backgroundImage !== "") {
cls.set("has-image", true);
@@ -257,10 +257,7 @@ export class StateBadge extends LitElement {
justify-content: center;
}
:host(.has-image) {
border-radius: var(
--state-badge-with-image-border-radius,
var(--ha-picture-border-radius)
);
border-radius: var(--state-badge-with-image-border-radius, 50%);
}
:host(.has-media-image) {
border-radius: var(--state-badge-with-media-image-border-radius, 8%);
+13 -18
View File
@@ -1,15 +1,10 @@
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import type { ContextType } from "@lit/context";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { consume } from "../common/decorators/consume";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeFunc } from "../common/translations/localize";
import { apiContext, configContext } from "../data/context";
import { fetchHassioAddonsInfo } from "../data/hassio/addon";
import type { ValueChangedEvent } from "../types";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import "./ha-alert";
import "./ha-app-icon";
import "./ha-combo-box-item";
@@ -47,13 +42,7 @@ const rowRenderer: RenderItemFunction<AddonPickerItem> = (item) => html`
@customElement("ha-addon-picker")
class HaAddonPicker extends LitElement {
@state() @consumeLocalize() private _localize!: LocalizeFunc;
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
@consume({ context: configContext, subscribe: true })
private _config!: ContextType<typeof configContext>;
public hass!: HomeAssistant;
@property() public label?: string;
@@ -84,7 +73,10 @@ class HaAddonPicker extends LitElement {
}
protected render() {
const label = this.label ?? this._localize("ui.components.app-picker.app");
const label =
this.label === undefined && this.hass
? this.hass.localize("ui.components.app-picker.app")
: this.label;
if (this._error) {
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
@@ -95,6 +87,7 @@ class HaAddonPicker extends LitElement {
return html`
<ha-generic-picker
.hass=${this.hass}
.autofocus=${this.autofocus}
.label=${label}
.valueRenderer=${this._valueRenderer}
@@ -113,8 +106,8 @@ class HaAddonPicker extends LitElement {
private async _getApps() {
try {
if (isComponentLoaded(this._config.config, "hassio")) {
const addonsInfo = await fetchHassioAddonsInfo(this._api);
if (isComponentLoaded(this.hass.config, "hassio")) {
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
this._addons = addonsInfo.addons
.filter((addon) => addon.version)
.map((addon) => ({
@@ -130,12 +123,14 @@ class HaAddonPicker extends LitElement {
sorting_label: [addon.name, addon.slug].filter(Boolean).join("_"),
}));
} else {
this._error = this._localize(
this._error = this.hass.localize(
"ui.components.app-picker.error.no_supervisor"
);
}
} catch (_err: any) {
this._error = this._localize("ui.components.app-picker.error.fetch_apps");
this._error = this.hass.localize(
"ui.components.app-picker.error.fetch_apps"
);
}
}
-10
View File
@@ -372,16 +372,6 @@ export class HaAnsiToHtml extends LitElement {
return !!numberOfFoundLines;
}
public trimLines(maxLines: number) {
const pre = this._pre;
if (!pre) {
return;
}
while (pre.childElementCount > maxLines && pre.firstElementChild) {
pre.firstElementChild.remove();
}
}
public clear() {
if (this._pre) {
this._pre.innerHTML = "";
@@ -281,6 +281,7 @@ export class HaAreaControlsPicker extends LitElement {
return html`
<ha-generic-picker
.hass=${this.hass}
.value=${this.value || ""}
.disabled=${this.disabled}
.required=${this.required}
+9 -1
View File
@@ -3,12 +3,16 @@ import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import type { HomeAssistant } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-area-picker";
import "./ha-sortable";
@customElement("ha-areas-picker")
export class HaAreasPicker extends LitElement {
export class HaAreasPicker extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@property({ type: Array }) public value?: string[];
@@ -63,6 +67,10 @@ export class HaAreasPicker extends LitElement {
@property({ type: Boolean }) public reorder = false;
protected render() {
if (!this.hass) {
return nothing;
}
const currentAreas = this._currentAreas;
return html`
<ha-sortable
+3 -12
View File
@@ -14,25 +14,16 @@ export class HaBadge extends LitElement {
@property({ type: Boolean, attribute: "icon-only" }) iconOnly = false;
/**
* Accessible name for the badge button when it shows only its icon.
*/
@property({ attribute: "accessible-label" }) public accessibleLabel?: string;
protected render() {
const label = this.label;
const isButton = this.type === "button";
return html`
<div
class="badge ${classMap({
"icon-only": this.iconOnly,
})}"
role=${ifDefined(isButton ? "button" : undefined)}
tabindex=${ifDefined(isButton ? "0" : undefined)}
aria-label=${ifDefined(
isButton && this.iconOnly ? this.accessibleLabel : undefined
)}
role=${ifDefined(this.type === "button" ? "button" : undefined)}
tabindex=${ifDefined(this.type === "button" ? "0" : undefined)}
>
<ha-ripple .disabled=${this.type !== "button"}></ha-ripple>
<slot name="icon"></slot>
@@ -132,7 +123,7 @@ export class HaBadge extends LitElement {
::slotted(img[slot="icon"]) {
width: 30px;
height: 30px;
border-radius: var(--ha-picture-border-radius);
border-radius: var(--ha-border-radius-circle);
object-fit: cover;
overflow: hidden;
margin-left: -10px;
@@ -14,7 +14,7 @@ import {
formatClockCardDate,
} from "../panels/lovelace/cards/clock/clock-date-format";
import type { ClockCardDatePart } from "../panels/lovelace/cards/types";
import type { ValueChangedEvent } from "../types";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import "./chips/ha-assist-chip";
import "./chips/ha-chip-set";
import "./chips/ha-input-chip";
@@ -116,6 +116,8 @@ interface ClockDatePartValueItem {
@customElement("ha-clock-date-format-picker")
export class HaClockDateFormatPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = false;
@@ -151,6 +153,7 @@ export class HaClockDateFormatPicker extends LitElement {
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
<ha-generic-picker
.hass=${this.hass}
.disabled=${this.disabled}
.required=${this.required && !value.length}
.value=${this._getPickerValue()}
+30 -54
View File
@@ -31,12 +31,9 @@ import type { ContextType } from "@lit/context";
import { consume } from "../common/decorators/consume";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import {
computeEntitySearchLabels,
computeEntityNameParts,
} from "../common/entity/compute_entity_name_display";
import { getEntityContext } from "../common/entity/context/get_entity_context";
import { computeDeviceName } from "../common/entity/compute_device_name";
import { computeEntityNameParts } from "../common/entity/compute_entity_name_display";
import { computeAreaName } from "../common/entity/compute_area_name";
import { computeFloorName } from "../common/entity/compute_floor_name";
import { copyToClipboard } from "../common/util/copy-clipboard";
@@ -45,11 +42,7 @@ import type {
JinjaArgType,
HassArgHoverContext,
} from "../resources/jinja_ha_completions";
import type {
HomeAssistant,
HomeAssistantFormatters,
HomeAssistantRegistries,
} from "../types";
import type { HomeAssistant } from "../types";
import { showToast } from "../util/toast";
import { documentationUrl } from "../util/documentation-url";
import {
@@ -720,9 +713,11 @@ export class HaCodeEditor extends ReactiveElement {
this._formatters!.formatEntityState(this._states![entityId]),
formatEntityName: (entityId) => {
const stateObj = this._states?.[entityId];
return stateObj
? this._formatters!.formatEntityName(stateObj)
: (this._registries?.entities?.[entityId]?.name ?? undefined);
return (
(stateObj?.attributes.friendly_name as string | undefined) ??
this._registries?.entities?.[entityId]?.name ??
undefined
);
},
formatAttributeName: (entityId, attribute) =>
this._formatters!.formatEntityAttributeName(
@@ -1045,42 +1040,23 @@ export class HaCodeEditor extends ReactiveElement {
});
};
private _getStates = memoizeOne(
(
states: HassEntities,
formatEntityName: HomeAssistantFormatters["formatEntityName"],
registries: HomeAssistantRegistries
): Completion[] => {
const options = Object.keys(states).map((key) => {
const name = formatEntityName(states[key]);
const { friendlyName } = computeEntitySearchLabels(
states[key],
registries
);
return {
type: "variable",
// label is used for searching, so include the names and entity_id here
label: [name, friendlyName !== name && friendlyName, key]
.filter(Boolean)
.join(" "),
displayLabel: key,
detail: name || undefined,
apply: key,
};
});
return options;
private _getStates = memoizeOne((states: HassEntities): Completion[] => {
if (!states) {
return [];
}
);
private _getEntityCompletions(): Completion[] {
if (!this._states || !this._formatters || !this._registries) return [];
return this._getStates(
this._states,
this._formatters.formatEntityName,
this._registries
);
}
const options = Object.keys(states).map((key) => ({
type: "variable",
label: states[key].attributes.friendly_name
? `${states[key].attributes.friendly_name} ${key}` // label is used for searching, so include both name and entity_id here
: key,
displayLabel: key,
detail: states[key].attributes.friendly_name,
apply: key,
}));
return options;
});
// Map of HA Jinja function name → (arg index → JinjaArgType).
// Derived from the snippet definitions in jinja_ha_completions.ts.
@@ -1460,8 +1436,8 @@ export class HaCodeEditor extends ReactiveElement {
from: number;
to: number;
}): CompletionResult | null {
const states = this._getEntityCompletions();
if (!states.length) return null;
const states = this._getStates(this._states!);
if (!states?.length) return null;
// from is stringNode.from + 1 to skip the opening quote character.
const from = stringNode.from + 1;
// Always offer completions inside a known entity-string context, including
@@ -1653,9 +1629,9 @@ export class HaCodeEditor extends ReactiveElement {
// If cursor is after the entity field, show all entities
if (context.pos >= afterField) {
const states = this._getEntityCompletions();
const states = this._getStates(this._states!);
if (!states.length) {
if (!states || !states.length) {
return null;
}
@@ -1708,9 +1684,9 @@ export class HaCodeEditor extends ReactiveElement {
const afterListMarker = currentLine.from + listItemMatch[0].length;
if (context.pos >= afterListMarker) {
const states = this._getEntityCompletions();
const states = this._getStates(this._states!);
if (!states.length) {
if (!states || !states.length) {
return null;
}
@@ -1768,9 +1744,9 @@ export class HaCodeEditor extends ReactiveElement {
return null;
}
const states = this._getEntityCompletions();
const states = this._getStates(this._states!);
if (!states.length) {
if (!states || !states.length) {
return null;
}
@@ -0,0 +1,315 @@
import { DIRECTION_ALL, Manager, Pan, Tap } from "@egjs/hammerjs";
import type { PropertyValues } from "lit";
import { css, html, LitElement } from "lit";
import memoizeOne from "memoize-one";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { rgb2hex } from "../common/color/convert-color";
import {
DEFAULT_MAX_KELVIN,
DEFAULT_MIN_KELVIN,
temperature2rgb,
} from "../common/color/convert-light-color";
import { fireEvent } from "../common/dom/fire_event";
import { mainWindow } from "../common/dom/get_main_window";
import { generateColorTemperatureGradient } from "../dialogs/more-info/components/lights/light-color-temp-picker";
const clamp = (value: number, min: number, max: number) =>
Math.min(max, Math.max(min, value));
// Dark end of the vertical brightness gradient; not fully black so the
// temperature hue stays visible at the bottom of the pad
const PAD_DARK_COLOR = "rgb(26, 26, 26)";
// The marker color's luminance scales with brightness down to this floor,
// so the marker never renders fully black at minimum brightness
const MARKER_LUMINANCE_FLOOR = 0.2;
@customElement("ha-color-temp-brightness-picker")
export class HaColorTempBrightnessPicker extends LitElement {
@property({ type: Boolean, reflect: true })
public disabled = false;
/** [color temperature in kelvin, brightness percentage] */
@property({ attribute: false })
public value?: [number, number];
@property({ attribute: false })
public minKelvin?: number;
@property({ attribute: false })
public maxKelvin?: number;
@query("#pad") private _pad?: HTMLElement;
private _mc?: HammerManager;
@state()
private _pressed?: string;
// Normalized physical position on the pad, [0, 1] left-to-right/top-to-bottom
@state()
private _cursorPosition?: [number, number];
@state()
private _localValue?: [number, number];
private get _minKelvin() {
return this.minKelvin ?? DEFAULT_MIN_KELVIN;
}
private get _maxKelvin() {
return this.maxKelvin ?? DEFAULT_MAX_KELVIN;
}
protected firstUpdated(changedProps: PropertyValues<this>): void {
super.firstUpdated(changedProps);
this._setupListeners();
}
connectedCallback(): void {
super.connectedCallback();
this._setupListeners();
}
disconnectedCallback(): void {
super.disconnectedCallback();
this._destroyListeners();
}
protected willUpdate(changedProps: PropertyValues<this>): void {
super.willUpdate(changedProps);
const rangeChanged =
changedProps.has("minKelvin") || changedProps.has("maxKelvin");
if (
rangeChanged ||
(changedProps.has("value") &&
(this._localValue?.[0] !== this.value?.[0] ||
this._localValue?.[1] !== this.value?.[1]))
) {
this._resetPosition();
}
}
private _setupListeners() {
if (this._pad && !this._mc) {
this._mc = new Manager(this._pad);
this._mc.add(
new Pan({
direction: DIRECTION_ALL,
enable: true,
})
);
this._mc.add(new Tap({ event: "singletap" }));
let savedPosition;
this._mc.on("panstart", (e) => {
if (this.disabled) return;
this._pressed = e.pointerType;
savedPosition = this._cursorPosition;
});
this._mc.on("pancancel", () => {
if (this.disabled) return;
this._pressed = undefined;
this._cursorPosition = savedPosition;
});
this._mc.on("panmove", (e) => {
if (this.disabled) return;
this._cursorPosition = this._getPositionFromEvent(e);
this._localValue = this._getValueFromCoord(...this._cursorPosition);
fireEvent(this, "cursor-moved", { value: this._localValue });
});
this._mc.on("panend", (e) => {
if (this.disabled) return;
this._pressed = undefined;
this._cursorPosition = this._getPositionFromEvent(e);
this._localValue = this._getValueFromCoord(...this._cursorPosition);
fireEvent(this, "cursor-moved", { value: undefined });
fireEvent(this, "value-changed", { value: this._localValue });
});
this._mc.on("singletap", (e) => {
if (this.disabled) return;
this._cursorPosition = this._getPositionFromEvent(e);
this._localValue = this._getValueFromCoord(...this._cursorPosition);
fireEvent(this, "value-changed", { value: this._localValue });
});
}
}
private _destroyListeners() {
if (this._mc) {
this._mc.destroy();
this._mc = undefined;
}
}
private _resetPosition() {
if (this.value === undefined) {
this._cursorPosition = undefined;
this._localValue = undefined;
return;
}
this._cursorPosition = this._getCoordsFromValue(this.value);
this._localValue = this.value;
}
private get _rtl() {
return mainWindow.document.dir === "rtl";
}
private _getCoordsFromValue = (value: [number, number]): [number, number] => {
const xValue =
(clamp(value[0], this._minKelvin, this._maxKelvin) - this._minKelvin) /
(this._maxKelvin - this._minKelvin);
const x = this._rtl ? 1 - xValue : xValue;
const y = 1 - clamp(value[1], 1, 100) / 100;
return [x, y];
};
private _getValueFromCoord = (x: number, y: number): [number, number] => {
const xValue = this._rtl ? 1 - x : x;
const kelvin = Math.round(
this._minKelvin + xValue * (this._maxKelvin - this._minKelvin)
);
const brightness = clamp(Math.round((1 - y) * 100), 1, 100);
return [kelvin, brightness];
};
private _getPositionFromEvent = (e: HammerInput): [number, number] => {
const boundingRect = this._pad!.getBoundingClientRect();
const x = clamp(
(e.center.x - boundingRect.left) / boundingRect.width,
0,
1
);
const y = clamp(
(e.center.y - boundingRect.top) / boundingRect.height,
0,
1
);
return [x, y];
};
private _generateTemperatureGradient = memoizeOne(
(min: number, max: number) => generateColorTemperatureGradient(min, max)
);
render() {
const gradient = this._generateTemperatureGradient(
this._minKelvin,
this._maxKelvin
);
const [x, y] = this._cursorPosition ?? [0, 0];
const markerScale = this._pressed
? this._pressed === "touch"
? "2.5"
: "1.5"
: "1";
const markerOffset = this._pressed === "touch" ? "0px, -24px" : "0px, 0px";
let markerColor = "#ffffff";
if (this._localValue) {
const rgb = temperature2rgb(this._localValue[0]);
const factor =
MARKER_LUMINANCE_FLOOR +
(1 - MARKER_LUMINANCE_FLOOR) * (this._localValue[1] / 100);
markerColor = rgb2hex([
Math.round(rgb[0] * factor),
Math.round(rgb[1] * factor),
Math.round(rgb[2] * factor),
]);
}
return html`
<div class="container ${classMap({ pressed: Boolean(this._pressed) })}">
<div
id="pad"
style=${styleMap({
"background-image": `linear-gradient(to top, ${PAD_DARK_COLOR}, white), linear-gradient(to ${this._rtl ? "left" : "right"}, ${gradient})`,
})}
></div>
<div
class="cursor"
style=${styleMap({
left: `${x * 100}%`,
top: `${y * 100}%`,
visibility: this._cursorPosition ? undefined : "hidden",
})}
>
<div
class="marker"
style=${styleMap({
"background-color": markerColor,
transform: `translate(${markerOffset}) scale(${markerScale})`,
})}
></div>
</div>
</div>
`;
}
static styles = css`
:host {
display: block;
outline: none;
}
.container {
position: relative;
width: 100%;
height: 100%;
display: flex;
}
#pad {
width: 100%;
height: 100%;
clip-path: inset(0 round var(--ha-border-radius-6xl));
background-blend-mode: multiply;
background-repeat: no-repeat;
cursor: pointer;
touch-action: none;
}
:host([disabled]) #pad {
cursor: initial;
filter: grayscale(1) opacity(0.5);
}
.cursor {
position: absolute;
width: 0;
height: 0;
pointer-events: none;
}
.marker {
position: absolute;
top: -16px;
left: -16px;
width: 32px;
height: 32px;
border-radius: var(--ha-border-radius-circle);
border: 2px solid white;
box-shadow:
0 1px 4px rgba(0, 0, 0, 0.3),
0 1px 6px rgba(0, 0, 0, 0.15);
}
.container:not(.pressed) .marker {
transition:
transform var(--ha-animation-duration-instant) ease-in-out,
background-color var(--ha-animation-duration-instant) ease-in-out;
}
.container:not(.pressed) .cursor {
transition:
left var(--ha-animation-duration-fast) ease-in-out,
top var(--ha-animation-duration-fast) ease-in-out;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-color-temp-brightness-picker": HaColorTempBrightnessPicker;
}
}
+11 -15
View File
@@ -1,16 +1,11 @@
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import type { ContextType } from "@lit/context";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { consume } from "../common/decorators/consume";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeFunc } from "../common/translations/localize";
import type { ConfigEntry } from "../data/config_entries";
import { getConfigEntries } from "../data/config_entries";
import { apiContext } from "../data/context";
import { domainToName } from "../data/integration";
import type { ValueChangedEvent } from "../types";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import "./ha-combo-box-item";
import "./ha-domain-icon";
import "./ha-generic-picker";
@@ -25,10 +20,7 @@ const SEARCH_KEYS = [
@customElement("ha-config-entry-picker")
class HaConfigEntryPicker extends LitElement {
@state() @consumeLocalize() private _localize!: LocalizeFunc;
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
public hass!: HomeAssistant;
@property() public integration?: string;
@@ -64,9 +56,13 @@ class HaConfigEntryPicker extends LitElement {
}
return html`
<ha-generic-picker
.hass=${this.hass}
.label=${
this.label ??
this._localize("ui.components.config-entry-picker.config_entry")
this.label === undefined && this.hass
? this.hass.localize(
"ui.components.config-entry-picker.config_entry"
)
: this.label
}
.value=${this.value}
.required=${this.required}
@@ -94,18 +90,18 @@ class HaConfigEntryPicker extends LitElement {
`;
private async _getConfigEntries() {
getConfigEntries(this._api, {
getConfigEntries(this.hass, {
type: ["device", "hub", "service"],
domain: this.integration,
}).then((configEntries) => {
this._configEntries = configEntries.map((entry: ConfigEntry) => {
const domainName = domainToName(this._localize, entry.domain);
const domainName = domainToName(this.hass.localize, entry.domain);
return {
id: entry.entry_id,
icon: entry.domain,
primary:
entry.title ||
this._localize(
this.hass.localize(
"ui.panel.config.integrations.config_entry.unnamed_entry"
),
secondary: domainName,
-2
View File
@@ -337,7 +337,6 @@ export class HaControlScrubber extends LitElement {
display: block;
--control-scrubber-color: var(--primary-color);
--control-scrubber-background: var(--disabled-color);
--control-scrubber-background-opacity: 1;
--control-scrubber-rail-color: var(--disabled-color);
--control-scrubber-rail-opacity: 0.2;
--control-scrubber-thickness: 40px;
@@ -395,7 +394,6 @@ export class HaControlScrubber extends LitElement {
left: 50%;
width: var(--track-width);
background: var(--control-scrubber-background);
opacity: var(--control-scrubber-background-opacity);
transform: translate3d(calc(var(--value, 0) * -100%), 0, 0);
transition: transform 180ms ease-in-out;
}
+74 -28
View File
@@ -4,13 +4,14 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { debounce } from "../common/util/debounce";
import type { ConfigEntry, SubEntry } from "../data/config_entries";
import { getConfigEntry, getSubEntries } from "../data/config_entries";
import type { Agent } from "../data/conversation";
import { listAgents } from "../data/conversation";
import type { EntityConfigFlow } from "../dialogs/config-flow/entity-config-flow";
import {
fetchEntityConfigFlow,
showEntityConfigFlow,
} from "../dialogs/config-flow/entity-config-flow";
import { getExtendedEntityRegistryEntry } from "../data/entity/entity_registry";
import { fetchIntegrationManifest } from "../data/integration";
import { showOptionsFlowDialog } from "../dialogs/config-flow/show-dialog-options-flow";
import { showSubConfigFlowDialog } from "../dialogs/config-flow/show-dialog-sub-config-flow";
import type { HomeAssistant } from "../types";
import "./ha-select";
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
@@ -33,7 +34,9 @@ export class HaConversationAgentPicker extends LitElement {
@state() _agents?: Agent[];
@state() private _configFlow?: EntityConfigFlow;
@state() private _configEntry?: ConfigEntry;
@state() private _subConfigEntry?: SubEntry;
protected render() {
if (!this._agents) {
@@ -76,15 +79,16 @@ export class HaConversationAgentPicker extends LitElement {
.options=${options}
></ha-select
>${
this._configFlow
(this._subConfigEntry &&
this._configEntry?.supported_subentry_types[
this._subConfigEntry.subentry_type
]?.supports_reconfigure) ||
this._configEntry?.supports_options
? html`<ha-icon-button
.label=${this.hass.localize(
"ui.components.conversation-agent-picker.settings"
)}
.path=${mdiCog}
@click=${this._openConfigFlow}
@click=${this._openOptionsFlow}
></ha-icon-button>`
: nothing
: ""
}
`;
}
@@ -98,23 +102,40 @@ export class HaConversationAgentPicker extends LitElement {
}
if (changedProperties.has("value")) {
this._fetchConfigFlow();
this._maybeFetchConfigEntry();
}
}
private async _fetchConfigFlow() {
const entityId = this.value;
this._configFlow = undefined;
let configFlow: EntityConfigFlow | undefined;
try {
configFlow = entityId
? await fetchEntityConfigFlow(this.hass, entityId)
: undefined;
} catch (_err) {
configFlow = undefined;
private async _maybeFetchConfigEntry() {
if (!this.value || !(this.value in this.hass.entities)) {
this._configEntry = undefined;
return;
}
if (entityId === this.value) {
this._configFlow = configFlow;
try {
const regEntry = await getExtendedEntityRegistryEntry(
this.hass,
this.value
);
if (!regEntry.config_entry_id) {
this._configEntry = undefined;
return;
}
this._configEntry = (
await getConfigEntry(this.hass, regEntry.config_entry_id)
).config_entry;
if (!regEntry.config_subentry_id) {
this._subConfigEntry = undefined;
} else {
this._subConfigEntry = (
await getSubEntries(this.hass, regEntry.config_entry_id)
).find((entry) => entry.subentry_id === regEntry.config_subentry_id);
}
} catch (_err) {
this._configEntry = undefined;
this._subConfigEntry = undefined;
}
}
@@ -182,10 +203,35 @@ export class HaConversationAgentPicker extends LitElement {
}
}
private _openConfigFlow() {
if (this._configFlow) {
showEntityConfigFlow(this, this.hass, this._configFlow);
private async _openOptionsFlow() {
if (!this._configEntry) {
return;
}
if (
this._subConfigEntry &&
this._configEntry.supported_subentry_types[
this._subConfigEntry.subentry_type
]?.supports_reconfigure
) {
showSubConfigFlowDialog(
this,
this._configEntry,
this._subConfigEntry.subentry_type,
{
startFlowHandler: this._configEntry.entry_id,
subEntryId: this._subConfigEntry.subentry_id,
}
);
return;
}
showOptionsFlowDialog(this, this._configEntry, {
manifest: await fetchIntegrationManifest(
this.hass,
this._configEntry.domain
),
});
}
static styles = css`
+10 -15
View File
@@ -1,13 +1,10 @@
import type { ContextType } from "@lit/context";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { consume } from "../common/decorators/consume";
import { fireEvent } from "../common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import { internationalizationContext } from "../data/context";
import type { FrontendLocaleData } from "../data/translation";
import type { ValueChangedEvent } from "../types";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import "./ha-generic-picker";
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
@@ -307,9 +304,7 @@ export const getCountryOptions = (
@customElement("ha-country-picker")
export class HaCountryPicker extends LitElement {
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n?: ContextType<typeof internationalizationContext>;
@property({ attribute: false }) public hass?: HomeAssistant;
@property() public value?: string;
@@ -331,7 +326,7 @@ export class HaCountryPicker extends LitElement {
this._getCountryOptions(
this.countries ?? COUNTRIES,
this.noSort,
this._i18n?.locale
this.hass?.locale
);
private _getCountryName = (country?: string) =>
@@ -343,7 +338,7 @@ export class HaCountryPicker extends LitElement {
protected render() {
const label =
this.label ??
(this._i18n?.localize("ui.components.country-picker.country") ||
(this.hass?.localize("ui.components.country-picker.country") ||
"Country");
const value =
@@ -352,9 +347,10 @@ export class HaCountryPicker extends LitElement {
return html`
<ha-generic-picker
.hass=${this.hass}
.notFoundLabel=${this._notFoundLabel}
.emptyLabel=${
this._i18n?.localize("ui.components.country-picker.no_countries") ||
this.hass?.localize("ui.components.country-picker.no_countries") ||
"No countries available"
}
.label=${label}
@@ -387,10 +383,9 @@ export class HaCountryPicker extends LitElement {
private _notFoundLabel = (search: string) => {
const term = html`<b>'${search}'</b>`;
return (
this._i18n?.localize("ui.components.country-picker.no_match", { term }) ||
html`No countries found for ${term}`
);
return this.hass
? this.hass.localize("ui.components.country-picker.no_match", { term })
: html`No countries found for ${term}`;
};
}
+10 -16
View File
@@ -1,13 +1,10 @@
import type { ContextType } from "@lit/context";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { consume } from "../common/decorators/consume";
import { fireEvent } from "../common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import { internationalizationContext } from "../data/context";
import type { FrontendLocaleData } from "../data/translation";
import type { ValueChangedEvent } from "../types";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import "./ha-generic-picker";
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
@@ -204,9 +201,7 @@ export const getCurrencyOptions = (
@customElement("ha-currency-picker")
export class HaCurrencyPicker extends LitElement {
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n?: ContextType<typeof internationalizationContext>;
@property({ attribute: false }) public hass?: HomeAssistant;
@property() public value?: string;
@@ -218,7 +213,7 @@ export class HaCurrencyPicker extends LitElement {
private _getCurrencyOptions = memoizeOne(getCurrencyOptions);
private _getItems = () => this._getCurrencyOptions(this._i18n?.locale);
private _getItems = () => this._getCurrencyOptions(this.hass?.locale);
private _getCurrencyName = (currency?: string) =>
this._getItems().find((c) => c.id === currency)?.primary;
@@ -229,14 +224,15 @@ export class HaCurrencyPicker extends LitElement {
protected render() {
const label =
this.label ??
(this._i18n?.localize("ui.components.currency-picker.currency") ||
(this.hass?.localize("ui.components.currency-picker.currency") ||
"Currency");
return html`
<ha-generic-picker
.hass=${this.hass}
.notFoundLabel=${this._notFoundLabel}
.emptyLabel=${
this._i18n?.localize("ui.components.currency-picker.no_currencies") ||
this.hass?.localize("ui.components.currency-picker.no_currencies") ||
"No currencies available"
}
.label=${label}
@@ -268,11 +264,9 @@ export class HaCurrencyPicker extends LitElement {
private _notFoundLabel = (search: string) => {
const term = html`<b>'${search}'</b>`;
return (
this._i18n?.localize("ui.components.currency-picker.no_match", {
term,
}) || html`No currencies found for ${term}`
);
return this.hass
? this.hass.localize("ui.components.currency-picker.no_match", { term })
: html`No currencies found for ${term}`;
};
}
-9
View File
@@ -76,15 +76,6 @@ export class HaFileUpload extends LitElement {
}
}
protected updated(changedProperties: PropertyValues<this>) {
super.updated(changedProperties);
// Reset the native input when the value is cleared from outside, so
// picking the same file again still fires a change event.
if (changedProperties.has("value") && !this.value && this._input) {
this._input.value = "";
}
}
private get _name() {
if (this.value === undefined) {
return "";
+10 -1
View File
@@ -20,6 +20,7 @@ import {
apiContext,
devicesContext,
internationalizationContext,
statesContext,
} from "../data/context";
import type { RelatedResult } from "../data/search";
import { findRelated } from "../data/search";
@@ -41,6 +42,10 @@ export class HaFilterDevices extends LitElement {
@consumeLocalize()
private _localize!: LocalizeFunc;
@consume({ context: statesContext, subscribe: true })
@state()
private _states!: ContextType<typeof statesContext>;
@consume({ context: devicesContext, subscribe: true })
@state()
private _devicesReg!: ContextType<typeof devicesContext>;
@@ -120,6 +125,7 @@ export class HaFilterDevices extends LitElement {
this._devicesReg,
this._filter || "",
this._localize,
this._states,
this._i18n.locale.language
)}
.rowRenderer=${this._renderItem}
@@ -152,6 +158,7 @@ export class HaFilterDevices extends LitElement {
this._devicesReg,
this._filter || "",
this._localize,
this._states,
this._i18n.locale.language
)[ev.detail].id,
];
@@ -162,6 +169,7 @@ export class HaFilterDevices extends LitElement {
this._devicesReg,
this._filter || "",
this._localize,
this._states,
this._i18n.locale.language
)[ev.detail].id;
this.value = (this.value ?? []).filter((deviceId) => deviceId !== id);
@@ -188,6 +196,7 @@ export class HaFilterDevices extends LitElement {
devices: ContextType<typeof devicesContext>,
filter: string,
localize: LocalizeFunc,
states: ContextType<typeof statesContext>,
language: string | undefined
): HaFilterDevicesItem[] => {
const values = Object.values(devices);
@@ -195,7 +204,7 @@ export class HaFilterDevices extends LitElement {
.map((device) => ({
id: device.id,
interactive: true,
name: computeDeviceNameDisplay(device, localize),
name: computeDeviceNameDisplay(device, localize, states),
}))
.filter(
({ name }) =>
+1
View File
@@ -403,6 +403,7 @@ export class HaFloorPicker extends LitElement {
return html`
<ha-generic-picker
.hass=${this.hass}
.autofocus=${this.autofocus}
.disabled=${this.disabled}
.label=${this.label}
+4 -1
View File
@@ -4,7 +4,7 @@ import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import type { SelectSelector } from "../../data/selector";
import type { ValueChangedEvent } from "../../types";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-selector/ha-selector-select";
import type {
HaFormElement,
@@ -28,6 +28,8 @@ export const matchSelectOptionValue = (
@customElement("ha-form-select")
export class HaFormSelect extends LitElement implements HaFormElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public schema!: HaFormSelectSchema;
@property({ attribute: false }) public data!: HaFormSelectData;
@@ -63,6 +65,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
protected render(): TemplateResult {
return html`
<ha-selector-select
.hass=${this.hass}
.value=${this.data}
.label=${this.label}
.helper=${this.helper}
+12 -6
View File
@@ -36,9 +36,6 @@ import "./ha-svg-icon";
@customElement("ha-generic-picker")
export class HaGenericPicker extends PickerMixin(LitElement) {
@property({ attribute: "aria-label" })
public ariaLabel: string | null = null;
@property({ type: Boolean, attribute: "allow-custom-value" })
public allowCustomValue;
@@ -97,6 +94,14 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
| "separator"
)[];
@property({ attribute: false }) public sectionTitleFunction?: (listInfo: {
firstIndex: number;
lastIndex: number;
firstItem: PickerComboBoxItem | string;
secondItem: PickerComboBoxItem | string;
itemsCount: number;
}) => string | undefined;
@property({ attribute: "selected-section" }) public selectedSection?: string;
@property({ attribute: false }) public popoverAnchor?: Element | null;
@@ -204,7 +209,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
compact
.unknown=${this._unknownValue}
.unknownItemText=${this.unknownItemText}
aria-label=${ifDefined(this.ariaLabel || this.label)}
aria-label=${ifDefined(this.label)}
@click=${this.open}
@clear=${this._clear}
.icon=${this.icon}
@@ -234,7 +239,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
@closed=${this._hidePicker}
role="dialog"
aria-modal="true"
aria-label=${this.ariaLabel || this.label || "Select option"}
aria-label=${this.label || "Select option"}
>
${this._renderComboBox(true)}
</ha-bottom-sheet>
@@ -258,7 +263,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
trap-focus
role="dialog"
aria-modal="true"
aria-label=${this.ariaLabel || this.label || "Select option"}
aria-label=${this.label || "Select option"}
>
${this._renderComboBox()}
</wa-popover>
@@ -290,6 +295,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
.searchFn=${this.searchFn}
.mode=${dialogMode ? "dialog" : "popover"}
.sections=${this.sections}
.sectionTitleFunction=${this.sectionTitleFunction}
.selectedSection=${this.selectedSection}
.searchKeys=${this.searchKeys}
.customValueLabel=${this.customValueLabel}
+1
View File
@@ -218,6 +218,7 @@ export class HaLabelPicker extends LitElement {
return html`
<ha-generic-picker
.disabled=${this.disabled}
.hass=${this.hass}
.autofocus=${this.autofocus}
.label=${this.label}
.helper=${this.helper}
+12 -16
View File
@@ -1,17 +1,14 @@
import type { ContextType } from "@lit/context";
import { mdiMenuDown } from "@mdi/js";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { consume } from "../common/decorators/consume";
import { fireEvent } from "../common/dom/fire_event";
import { formatLanguageCode } from "../common/language/format_language";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import { internationalizationContext } from "../data/context";
import type { FrontendLocaleData } from "../data/translation";
import { translationMetadata } from "../resources/translations-metadata";
import type { ValueChangedEvent } from "../types";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import "./ha-button";
import "./ha-generic-picker";
import type { HaGenericPicker } from "./ha-generic-picker";
@@ -94,6 +91,8 @@ export class HaLanguagePicker extends LitElement {
@property({ type: Array }) public languages?: string[];
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = false;
@@ -108,10 +107,6 @@ export class HaLanguagePicker extends LitElement {
@property({ attribute: "no-sort", type: Boolean }) public noSort = false;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n?: ContextType<typeof internationalizationContext>;
@state() _defaultLanguages: string[] = [];
@query("ha-generic-picker", true) public genericPicker!: HaGenericPicker;
@@ -132,7 +127,7 @@ export class HaLanguagePicker extends LitElement {
this.languages ?? this._defaultLanguages,
this.nativeName,
this.noSort,
this._i18n?.locale
this.hass?.locale
);
private _getLanguageName = (lang?: string) =>
@@ -146,7 +141,7 @@ export class HaLanguagePicker extends LitElement {
protected render() {
const label =
this.label ??
(this._i18n?.localize("ui.components.language-picker.language") ||
(this.hass?.localize("ui.components.language-picker.language") ||
"Language");
const value =
@@ -155,10 +150,11 @@ export class HaLanguagePicker extends LitElement {
return html`
<ha-generic-picker
.hass=${this.hass}
.autofocus=${this.autofocus}
.notFoundLabel=${this._notFoundLabel}
.emptyLabel=${
this._i18n?.localize("ui.components.language-picker.no_languages") ||
this.hass?.localize("ui.components.language-picker.no_languages") ||
"No languages available"
}
.label=${label}
@@ -209,11 +205,11 @@ export class HaLanguagePicker extends LitElement {
private _notFoundLabel = (search: string) => {
const term = html`<b>‘${search}’</b>`;
return (
this._i18n?.localize("ui.components.language-picker.no_match", {
term,
}) || html`No languages found for ${term}`
);
return this.hass
? this.hass.localize("ui.components.language-picker.no_match", {
term,
})
: html`No languages found for ${term}`;
};
}
+1
View File
@@ -130,6 +130,7 @@ export class HaNavigationPicker extends LitElement {
return html`
<ha-generic-picker
.hass=${this.hass}
.value=${this._loading ? undefined : this.value}
allow-custom-value
.placeholder=${this.label}
+34 -17
View File
@@ -14,7 +14,6 @@ import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { tinykeys } from "tinykeys";
import { findSectionTitle } from "./list/find-section-title";
import { consume } from "../common/decorators/consume";
import {
fireEvent,
@@ -189,6 +188,14 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
| "separator"
)[];
@property({ attribute: false }) public sectionTitleFunction?: (listInfo: {
firstIndex: number;
lastIndex: number;
firstItem: PickerComboBoxItem | string;
secondItem: PickerComboBoxItem | string;
itemsCount: number;
}) => string | undefined;
@property({ attribute: "selected-section" }) public selectedSection?: string;
@property({ type: Boolean, reflect: true }) public clearable = false;
@@ -226,10 +233,6 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
private _allItems: PickerComboBoxItem[] = [];
// Rows from this index on are added by the combo box itself, like "Add custom
// item", and belong to no section.
private _sectionedItemsCount = 0;
// The virtualized list renders its scroller a moment after it connects, so
// the scroll fades attach once it reports its first visible rows.
private _virtualScrollElement?: HTMLElement;
@@ -316,14 +319,14 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
this.sections?.length
? html`
<div class="section-title-wrapper">
<ha-section-title
<div
class=${classMap({
"section-title": true,
show: !this._selectedSection && !!this._sectionTitle,
})}
>
${this._sectionTitle}
</ha-section-title>
</div>
</div>
`
: nothing
@@ -419,12 +422,15 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
this._virtualScrollElement = scrollElement;
this.requestUpdate();
}
if (this.sections?.length) {
const { first } = ev.detail;
this._sectionTitle =
!this._selectedSection && first < this._sectionedItemsCount
? findSectionTitle(this._items, first)
: undefined;
if (this.sectionTitleFunction && this.sections?.length) {
const { first, last } = ev.detail;
this._sectionTitle = this.sectionTitleFunction({
firstIndex: first,
lastIndex: last,
firstItem: this._items[first],
secondItem: this._items[first + 1],
itemsCount: this._items.length,
});
}
}
@@ -461,8 +467,6 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
});
}
this._sectionedItemsCount = items.length;
if (!items.length && (!this.allowCustomValue || this.emptyLabel)) {
items.push({ id: NO_ITEMS_AVAILABLE_ID, primary: "" });
}
@@ -531,7 +535,10 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
private _renderRow(item: PickerComboBoxItem | string, index: number) {
if (typeof item === "string") {
return html`<ha-section-title>${item}</ha-section-title>`;
return html`<ha-section-title
style="padding: var(--ha-space-1) var(--ha-space-4);"
>${item}</ha-section-title
>`;
}
if (item.id === PADDING_ID) {
return html`<div
@@ -860,7 +867,6 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
padding-top: var(--ha-space-4);
flex: 1;
min-height: 0;
--ha-section-title-padding: var(--ha-space-1) var(--ha-space-4);
}
:host([clearable]) {
@@ -968,6 +974,17 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
border: 1px solid var(--ha-color-border-neutral-quiet);
}
.section-title {
box-sizing: border-box;
background-color: var(--ha-color-fill-neutral-quiet-resting);
padding: var(--ha-space-1) var(--ha-space-4);
font-weight: var(--ha-font-weight-bold);
color: var(--secondary-text-color);
min-height: var(--ha-space-6);
display: flex;
align-items: center;
}
.section-title-wrapper {
height: 0;
position: relative;
-11
View File
@@ -70,8 +70,6 @@ export class HaPictureUpload extends LitElement {
@state() private _uploading = false;
@state() private _pickedFile?: File;
public render(): TemplateResult {
if (!this.value) {
const secondary =
@@ -107,7 +105,6 @@ export class HaPictureUpload extends LitElement {
)
}
.uploading=${this._uploading}
.value=${this._pickedFile}
@file-picked=${this._handleFilePicked}
@change=${this._handleFileCleared}
accept="image/png, image/jpeg, image/gif"
@@ -146,7 +143,6 @@ export class HaPictureUpload extends LitElement {
private async _handleFilePicked(ev) {
const file = ev.detail.files[0];
this._pickedFile = file;
if (this.crop) {
this._cropFile(file);
} else {
@@ -156,12 +152,10 @@ export class HaPictureUpload extends LitElement {
private async _handleFileCleared() {
this.value = null;
this._pickedFile = undefined;
}
private async _cropFile(file: File, mediaId?: string) {
if (!["image/png", "image/jpeg", "image/gif"].includes(file.type)) {
this._pickedFile = undefined;
showAlertDialog(this, {
text: this._i18n.localize(
"ui.components.picture-upload.unsupported_format"
@@ -187,15 +181,11 @@ export class HaPictureUpload extends LitElement {
this._uploadFile(croppedFile);
}
},
cancelCallback: () => {
this._pickedFile = undefined;
},
});
}
private async _uploadFile(file: File) {
if (!["image/png", "image/jpeg", "image/gif"].includes(file.type)) {
this._pickedFile = undefined;
showAlertDialog(this, {
text: this._i18n.localize(
"ui.components.picture-upload.unsupported_format"
@@ -244,7 +234,6 @@ export class HaPictureUpload extends LitElement {
});
} finally {
this._uploading = false;
this._pickedFile = undefined;
fireEvent(this, "uploading-changed", { uploading: false });
}
}
+1 -1
View File
@@ -487,7 +487,7 @@ export class HaRelatedItems extends LitElement {
}
.avatar {
border-radius: var(--ha-picture-border-radius-sm);
border-radius: var(--ha-border-radius-circle);
background-position: center center;
background-size: cover;
}
+393
View File
@@ -0,0 +1,393 @@
import type { PropertyValues } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { BOTTOM_SHEET_ANIMATION_DURATION_MS } from "./ha-bottom-sheet";
/**
* A bottom sheet component that slides up from the bottom of the screen.
*
* The bottom sheet provides a draggable interface that allows users to resize
* the sheet by dragging the handle at the top. It supports both mouse and touch
* interactions and automatically closes when dragged below a 20% of screen height.
*
* A persistent sheet never closes by dragging; it stops at its minimum height
* instead, so a host can keep a collapsed strip of content reachable.
*
* @fires bottom-sheet-closed - Fired when the bottom sheet is closed
* @fires bottom-sheet-resized - Fired with the sheet's height in pixels once
* it has opened and whenever a drag ends
*
* @cssprop --ha-bottom-sheet-border-width - Border width for the sheet
* @cssprop --ha-bottom-sheet-border-style - Border style for the sheet
* @cssprop --ha-bottom-sheet-border-color - Border color for the sheet
* @cssprop --ha-bottom-sheet-handle-padding - How far below the handle the
* grab area reaches; shrink it when content sits right under the handle
*/
@customElement("ha-resizable-bottom-sheet")
export class HaResizableBottomSheet extends LitElement {
@query("dialog") private _dialog!: HTMLDialogElement;
/** Dragging down stops at the minimum height instead of closing the sheet */
@property({ type: Boolean }) public persistent = false;
/**
* The height in pixels the sheet cannot be dragged below, e.g. what a host
* measures for the strip it wants to keep visible. Without it the sheet
* stops at 20% of the viewport.
*/
@property({ type: Number, attribute: "min-height" })
public minHeight?: number;
/**
* The largest share of the viewport the sheet opens at, in percent. It
* opens at its content height up to this, and can be dragged to 90 after.
*/
@property({ type: Number, attribute: "open-max-viewport-height" })
public openMaxViewportHeight = 70;
/**
* Whether the sheet may open smaller than 55% of the viewport, down to its
* content height and the minimum it can be dragged to.
*/
@property({ type: Boolean, attribute: "open-at-content-height" })
public openAtContentHeight = false;
private _dragging = false;
private _dragStartY = 0;
private _initialSize = 0;
private _opened = false;
@state() private _dialogMaxViewpointHeight?: number;
@state() private _dialogMinViewpointHeight?: number;
@state() private _dialogViewportHeight?: number;
render() {
// Until it has opened, the sheet sizes to its content within the opening
// bounds; afterwards it keeps the height it settled on or was dragged to
const maxHeight =
this._dialogMaxViewpointHeight ?? this.openMaxViewportHeight;
const minHeight =
this._dialogMinViewpointHeight ??
(this.openAtContentHeight ? this._minViewportHeight() : 55);
return html`<dialog
open
@transitionend=${this._handleTransitionEnd}
style=${`
--height: ${this._dialogViewportHeight}vh;
--height: ${this._dialogViewportHeight}dvh;
--max-height: ${maxHeight}vh;
--max-height: ${maxHeight}dvh;
--min-height: ${minHeight}vh;
--min-height: ${minHeight}dvh;
`}
>
<div class="handle-wrapper">
<div
@mousedown=${this._handleMouseDown}
@touchstart=${this._handleTouchStart}
class="handle"
></div>
</div>
<slot></slot>
</dialog>`;
}
protected firstUpdated(changedProperties: PropertyValues<this>) {
super.firstUpdated(changedProperties);
this._openSheet();
}
private _openSheet() {
requestAnimationFrame(() => {
// trigger opening animation
this._dialog.classList.add("show");
});
}
public closeSheet() {
requestAnimationFrame(() => {
this._dialog.classList.remove("show");
});
}
private _handleTransitionEnd() {
if (this._dialog.classList.contains("show")) {
// after show animation is done
// - set the height to the natural height, to prevent content shift when switch content
// - set max height to 90vh, so it opens at max 70vh but can be resized to 90vh
this._dialogViewportHeight =
(this._dialog.offsetHeight / window.innerHeight) * 100;
this._dialogMaxViewpointHeight = 90;
this._dialogMinViewpointHeight = this._minViewportHeight();
this._opened = true;
this._fireResized();
} else {
// after close animation is done close dialog element and fire closed event
this._dialog.close();
fireEvent(this, "bottom-sheet-closed");
}
}
connectedCallback() {
super.connectedCallback();
// register event listeners for drag handling
document.addEventListener("mousemove", this._handleMouseMove);
document.addEventListener("mouseup", this._handleMouseUp);
document.addEventListener("touchmove", this._handleTouchMove, {
passive: false,
});
document.addEventListener("touchend", this._handleTouchEnd);
document.addEventListener("touchcancel", this._handleTouchEnd);
window.addEventListener("resize", this._handleViewportResize);
}
disconnectedCallback() {
super.disconnectedCallback();
// unregister event listeners for drag handling
document.removeEventListener("mousemove", this._handleMouseMove);
document.removeEventListener("mouseup", this._handleMouseUp);
document.removeEventListener("touchmove", this._handleTouchMove);
document.removeEventListener("touchend", this._handleTouchEnd);
document.removeEventListener("touchcancel", this._handleTouchEnd);
window.removeEventListener("resize", this._handleViewportResize);
}
// minHeight is a pixel value, so its viewport-relative minimum depends on the
// window height; recompute it when the viewport changes (for example on
// rotation) so the cached percentage cannot clip the persistent peek content.
private _handleViewportResize = () => {
this._applyMinViewportHeight();
};
private _handleMouseDown = (ev: MouseEvent) => {
this._startDrag(ev.clientY);
};
private _handleTouchStart = (ev: TouchEvent) => {
// Prevent the browser from interpreting this as a scroll/PTR gesture.
ev.preventDefault();
this._startDrag(ev.touches[0].clientY);
};
private _startDrag(clientY: number) {
this._dragging = true;
this._dragStartY = clientY;
this._initialSize = (this._dialog.offsetHeight / window.innerHeight) * 100;
document.body.style.setProperty("cursor", "grabbing");
}
private _handleMouseMove = (ev: MouseEvent) => {
if (!this._dragging) {
return;
}
this._updateSize(ev.clientY);
};
private _handleTouchMove = (ev: TouchEvent) => {
if (!this._dragging) {
return;
}
ev.preventDefault(); // Prevent scrolling
this._updateSize(ev.touches[0].clientY);
};
private _updateSize(clientY: number) {
const deltaY = this._dragStartY - clientY;
const viewportHeight = window.innerHeight;
const deltaVh = (deltaY / viewportHeight) * 100;
// Calculate new size and clamp between 10vh and 90vh
let newSize = this._initialSize + deltaVh;
newSize = Math.max(10, Math.min(90, newSize));
if (this.persistent) {
newSize = Math.max(this._minViewportHeight(), newSize);
} else if (newSize < 20 && deltaY < 0) {
// on drag down and below 20vh
this._endDrag();
this.closeSheet();
return;
}
this._dialogViewportHeight = newSize;
}
private _minViewportHeight(): number {
return this.minHeight === undefined
? 20
: (this.minHeight / window.innerHeight) * 100;
}
protected updated(changedProperties: PropertyValues<this>) {
super.updated(changedProperties);
// A minimum set after opening applies right away
if (changedProperties.has("minHeight") && this._opened) {
this._applyMinViewportHeight();
}
}
// Applies the current pixel minimum as a viewport percentage and, once it has
// rendered, reports the size if it changed: raising the minimum can grow a
// sheet resting at the old one, and the host needs the new height to keep its
// overlay padding in step.
private _applyMinViewportHeight() {
if (!this._opened) {
return;
}
this._dialogMinViewpointHeight = this._minViewportHeight();
this.updateComplete.then(() => {
if (this._dialog.offsetHeight !== this._lastReportedHeight) {
this._fireResized();
}
});
}
private _lastReportedHeight?: number;
private _fireResized() {
this._lastReportedHeight = this._dialog.offsetHeight;
fireEvent(this, "bottom-sheet-resized", {
height: this._lastReportedHeight,
});
}
private _handleMouseUp = () => {
this._endDrag();
};
private _handleTouchEnd = () => {
this._endDrag();
};
private _endDrag() {
if (!this._dragging) {
return;
}
this._dragging = false;
document.body.style.removeProperty("cursor");
// Hosts lay out around the sheet once, not on every move
this.updateComplete.then(() => this._fireResized());
}
static styles = css`
.handle-wrapper {
position: absolute;
top: 0;
width: 100%;
padding-bottom: 2px;
display: flex;
justify-content: center;
align-items: center;
cursor: grab;
touch-action: none;
}
.handle-wrapper .handle {
height: 20px;
width: 200px;
display: flex;
justify-content: center;
align-items: center;
z-index: 7;
/* Extends the grab area over the content below the handle */
padding-bottom: var(--ha-bottom-sheet-handle-padding, 76px);
}
.handle-wrapper .handle::after {
content: "";
border-radius: var(--ha-border-radius-md);
height: 4px;
background: var(--divider-color, #e0e0e0);
width: 80px;
}
.handle-wrapper .handle:active::after {
cursor: grabbing;
}
dialog {
height: var(--height, auto);
max-height: min(
var(--max-height, 70vh),
calc(100vh - var(--safe-area-inset-top))
);
max-height: min(
var(--max-height, 70dvh),
calc(100dvh - var(--safe-area-inset-top))
);
min-height: var(--min-height, 30vh);
min-height: var(--min-height, 30dvh);
background-color: var(
--ha-bottom-sheet-surface-background,
var(--ha-color-surface-default)
);
display: flex;
flex-direction: column;
top: 0;
inset-inline-start: 0;
position: fixed;
--sheet-inset-left: var(
--ha-bottom-sheet-inset-left,
var(--safe-area-inset-left)
);
--sheet-inset-right: var(
--ha-bottom-sheet-inset-right,
var(--safe-area-inset-right)
);
--sheet-border-width: var(--ha-bottom-sheet-border-width, 0px);
--sheet-side-borders: calc(2 * var(--sheet-border-width));
width: calc(
100% - var(--sheet-side-borders) - var(--sheet-inset-left) - var(
--sheet-inset-right
)
);
max-width: 100%;
border: none;
box-shadow: var(--wa-shadow-l);
padding: 0;
margin: 0;
top: auto;
inset-inline-end: auto;
bottom: 0;
inset-inline-start: 0;
box-shadow: 0px -8px 16px rgba(0, 0, 0, 0.2);
border-top-left-radius: var(
--ha-bottom-sheet-border-radius,
var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
);
border-top-right-radius: var(
--ha-bottom-sheet-border-radius,
var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
);
transform: translateY(100%);
transition: transform ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms ease;
border-top-width: var(--sheet-border-width);
border-right-width: var(--sheet-border-width);
border-left-width: var(--sheet-border-width);
border-bottom-width: 0;
border-style: var(--ha-bottom-sheet-border-style);
border-color: var(--ha-bottom-sheet-border-color);
margin-left: var(--sheet-inset-left);
margin-right: var(--sheet-inset-right);
}
dialog.show {
transform: translateY(0);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-resizable-bottom-sheet": HaResizableBottomSheet;
}
interface HASSDomEvents {
"bottom-sheet-closed": undefined;
"bottom-sheet-resized": { height: number };
}
}
+1 -10
View File
@@ -1,12 +1,6 @@
import { css, html, LitElement } from "lit";
import { customElement } from "lit/decorators";
/**
* @element ha-section-title
* @summary Bold, tinted heading that labels a group of items in a list.
* @slot - The title text.
* @cssprop --ha-section-title-padding - Padding around the title. Defaults to `var(--ha-space-2) var(--ha-space-3)`.
*/
@customElement("ha-section-title")
class HaSectionTitle extends LitElement {
protected render() {
@@ -16,10 +10,7 @@ class HaSectionTitle extends LitElement {
static styles = css`
:host {
background-color: var(--ha-color-fill-neutral-quiet-resting);
padding: var(
--ha-section-title-padding,
var(--ha-space-2) var(--ha-space-3)
);
padding: var(--ha-space-2) var(--ha-space-3);
font-weight: var(--ha-font-weight-bold);
color: var(--secondary-text-color);
min-height: var(--ha-space-6);
@@ -1,10 +1,13 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import type { AppSelector, AddonSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-addon-picker";
@customElement("ha-selector-app")
export class HaAppSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: AppSelector | AddonSelector;
@property() public value?: any;
@@ -19,6 +22,7 @@ export class HaAppSelector extends LitElement {
protected render() {
return html`<ha-addon-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
@@ -113,6 +113,7 @@ export class HaAreaSelector extends LitElement {
return html`
<ha-areas-picker
.hass=${this.hass}
.value=${this.value}
.helper=${this.helper}
.pickAreaLabel=${this.label}
@@ -1,10 +1,13 @@
import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators";
import type { AreasDisplaySelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-areas-display-editor";
@customElement("ha-selector-areas_display")
export class HaAreasDisplaySelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: AreasDisplaySelector;
@property() public value?: any;
@@ -1,10 +1,13 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import type { ConfigEntrySelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-config-entry-picker";
@customElement("ha-selector-config_entry")
export class HaConfigEntrySelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: ConfigEntrySelector;
@property() public value?: any;
@@ -19,6 +22,7 @@ export class HaConfigEntrySelector extends LitElement {
protected render() {
return html`<ha-config-entry-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
@@ -1,10 +1,13 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import type { CountrySelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-country-picker";
@customElement("ha-selector-country")
export class HaCountrySelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: CountrySelector;
@property() public value?: any;
@@ -20,6 +23,7 @@ export class HaCountrySelector extends LitElement {
protected render() {
return html`
<ha-country-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
@@ -91,8 +91,6 @@ export class HaDateTimeSelector extends LitElement {
below for its hint while ha-time-input does not. */
align-items: flex-start;
flex-direction: row;
/* Narrow screens have no room for both fields on one row. */
flex-wrap: wrap;
}
ha-date-input {
@@ -1,10 +1,13 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import type { LanguageSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-language-picker";
@customElement("ha-selector-language")
export class HaLanguageSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: LanguageSelector;
@property() public value?: any;
@@ -20,6 +23,7 @@ export class HaLanguageSelector extends LitElement {
protected render() {
return html`
<ha-language-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
@@ -11,7 +11,10 @@ import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { internationalizationContext } from "../../data/context";
import type { SelectOption, SelectSelector } from "../../data/selector";
import type { FrontendLocaleData } from "../../data/translation";
import type { HomeAssistantInternationalization } from "../../types";
import type {
HomeAssistant,
HomeAssistantInternationalization,
} from "../../types";
import "../chips/ha-chip-set";
import "../chips/ha-input-chip";
import "../ha-checkbox";
@@ -26,6 +29,8 @@ import "../radio/ha-radio-option";
@customElement("ha-selector-select")
export class HaSelectSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consume({ context: internationalizationContext, subscribe: true })
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
@@ -211,6 +216,7 @@ export class HaSelectSelector extends LitElement {
<ha-generic-picker
no-sort
.hass=${this.hass}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required && !value.length}
@@ -227,6 +233,7 @@ export class HaSelectSelector extends LitElement {
return html`
<ha-generic-picker
no-sort
.hass=${this.hass}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
@@ -515,6 +515,7 @@ export class HaSerialPortSelector extends LitElement {
return html`
<ha-generic-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
@@ -1,10 +1,13 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import type { ThemeSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-theme-picker";
@customElement("ha-selector-theme")
export class HaThemeSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: ThemeSelector;
@property() public value?: string;
@@ -1,10 +1,13 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import type { UiClockDateFormatSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-clock-date-format-picker";
@customElement("ha-selector-ui_clock_date_format")
export class HaSelectorUiClockDateFormat extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: UiClockDateFormatSelector;
@property() public value?: string | string[];
@@ -20,6 +23,7 @@ export class HaSelectorUiClockDateFormat extends LitElement {
protected render() {
return html`
<ha-clock-date-format-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
@@ -3,9 +3,12 @@ import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { UiColorSelector } from "../../data/selector";
import "../ha-color-picker";
import type { HomeAssistant } from "../../types";
@customElement("ha-selector-ui_color")
export class HaSelectorUiColor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: UiColorSelector;
@property() public value?: string;
@@ -20,6 +23,7 @@ export class HaSelectorUiColor extends LitElement {
return html`
<ha-color-picker
.label=${this.label}
.hass=${this.hass}
.value=${this.value}
.helper=${this.helper}
.disabled=${this.disabled}
+2
View File
@@ -510,6 +510,7 @@ export class HaServiceControl extends LitElement {
this.hidePicker
? nothing
: html`<ha-service-picker
.hass=${this.hass}
.value=${this._value?.action}
.disabled=${this.disabled}
@value-changed=${this._serviceChanged}
@@ -614,6 +615,7 @@ export class HaServiceControl extends LitElement {
>
<ha-service-section-icon
slot="icons"
.hass=${this.hass}
.service=${this._value!.action}
.section=${dataField.key}
></ha-service-section-icon>
+26 -37
View File
@@ -1,20 +1,12 @@
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import type { ContextType } from "@lit/context";
import { mdiRoomService } from "@mdi/js";
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import { consume } from "../common/decorators/consume";
import { fireEvent } from "../common/dom/fire_event";
import { isValidServiceId } from "../common/entity/valid_service_id";
import type { LocalizeFunc } from "../common/translations/localize";
import {
configContext,
connectionContext,
internationalizationContext,
servicesContext,
} from "../data/context";
import { getServiceIcons } from "../data/icons";
import { domainToName } from "../data/integration";
import type { HomeAssistant, ValueChangedEvent } from "../types";
@@ -39,19 +31,7 @@ const SEARCH_KEYS = [
@customElement("ha-service-picker")
class HaServicePicker extends LitElement {
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@state()
@consume({ context: servicesContext, subscribe: true })
private _services!: ContextType<typeof servicesContext>;
@consume({ context: connectionContext, subscribe: true })
private _connection!: ContextType<typeof connectionContext>;
@consume({ context: configContext, subscribe: true })
private _config!: ContextType<typeof configContext>;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public disabled = false;
@@ -73,8 +53,8 @@ class HaServicePicker extends LitElement {
protected firstUpdated(props: PropertyValues<this>) {
super.firstUpdated(props);
this._i18n.loadBackendTranslation("services");
getServiceIcons(this._connection.connection, this._config.config);
this.hass.loadBackendTranslation("services");
getServiceIcons(this.hass.connection, this.hass.config);
}
private _rowRenderer: RenderItemFunction<ServiceComboBoxItem> = (
@@ -82,7 +62,11 @@ class HaServicePicker extends LitElement {
index
) => html`
<ha-combo-box-item .borderTop=${index !== 0}>
<ha-service-icon slot="start" .service=${item.id}></ha-service-icon>
<ha-service-icon
slot="start"
.hass=${this.hass}
.service=${item.id}
></ha-service-icon>
<span slot="headline">${item.primary}</span>
<span slot="supporting-text">${item.secondary}</span>
${
@@ -121,7 +105,7 @@ class HaServicePicker extends LitElement {
}
const descriptionPlaceholders =
services[domain][service].description_placeholders;
this.hass.services[domain][service].description_placeholders;
const serviceName =
localize(
@@ -132,7 +116,11 @@ class HaServicePicker extends LitElement {
service;
return html`
<ha-service-icon slot="start" .service=${serviceId}></ha-service-icon>
<ha-service-icon
slot="start"
.hass=${this.hass}
.service=${serviceId}
></ha-service-icon>
<span slot="headline">${serviceName}</span>
${
this.showServiceId
@@ -148,12 +136,13 @@ class HaServicePicker extends LitElement {
protected render(): TemplateResult {
const placeholder =
this.placeholder ??
this._i18n.localize("ui.components.service-picker.action");
this.hass.localize("ui.components.service-picker.action");
return html`
<ha-generic-picker
.hass=${this.hass}
.autofocus=${this.autofocus}
.notFoundLabel=${this._i18n.localize(
.notFoundLabel=${this.hass.localize(
"ui.components.service-picker.no_match"
)}
.label=${this.label}
@@ -162,11 +151,11 @@ class HaServicePicker extends LitElement {
.getItems=${this._getItems}
.rowRenderer=${this._rowRenderer}
.valueRenderer=${this._valueRenderer(
this._i18n.localize,
this._services
this.hass.localize,
this.hass.services
)}
.searchKeys=${SEARCH_KEYS}
.unknownItemText=${this._i18n.localize(
.unknownItemText=${this.hass.localize(
"ui.components.service-picker.unknown"
)}
@value-changed=${this._valueChanged}
@@ -176,9 +165,9 @@ class HaServicePicker extends LitElement {
}
private _getItems = () =>
this._serviceItems(this._i18n.localize, this._services);
this._services(this.hass.localize, this.hass.services);
private _serviceItems = memoizeOne(
private _services = memoizeOne(
(
localize: LocalizeFunc,
services: HomeAssistant["services"]
@@ -198,10 +187,10 @@ class HaServicePicker extends LitElement {
const domainName = domainToName(localize, domain);
const descriptionPlaceholders =
services[domain][service].description_placeholders;
this.hass.services[domain][service].description_placeholders;
const name =
localize(
this.hass.localize(
`component.${domain}.services.${service}.name`,
descriptionPlaceholders
) ||
@@ -209,7 +198,7 @@ class HaServicePicker extends LitElement {
service;
const description =
localize(
this.hass.localize(
`component.${domain}.services.${service}.description`,
descriptionPlaceholders
) ||
+3 -6
View File
@@ -879,16 +879,13 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
pointer-events: none;
}
/* Keep the rounded-square avatar clear of the row edges and
centered in the collapsed item: 8px + 32px + 8px = 48px */
ha-user-badge {
width: var(--ha-space-8);
height: var(--ha-space-8);
flex-shrink: 0;
width: 40px;
height: 40px;
}
ha-list-item-button.user {
--ha-row-item-padding-inline: var(--ha-space-2) 0;
--ha-row-item-padding-inline: var(--ha-space-1) 0;
}
.spacer {
+8 -48
View File
@@ -16,9 +16,6 @@ const HALF_VIEWPORT_SHARE = 0.5;
// How far a touch moves before its direction decides what it drags
const TOUCH_SLOP_PX = 4;
// How much of the lowest stop a drag hides before it closes the sheet
const DISMISS_DRAG_SHARE = 0.75;
interface Stop {
position: BottomSheetPosition;
height: number;
@@ -34,11 +31,9 @@ interface Stop {
* them when a drag ends, or to the next one in the direction of a quick swipe.
* The handle drags with a mouse or a finger. On touch the whole sheet drags
* too, until it is at full height: then its content scrolls, and pulling down
* from the top brings the sheet down again. Dragging down collapses it to
* `minHeight` so a host keeps a strip of content reachable. Only `closeSheet`
* closes it, unless it is `dismissable`: then a drag that pulls it below its
* lowest stop closes it with a downward swipe, or once it is almost out of the
* screen. A quick swipe that ends above the lowest stop still stops there.
* from the top brings the sheet down again. Dragging down never closes it; it
* collapses to `minHeight` so a host keeps a strip of content reachable, and
* only `closeSheet` closes it.
*
* @fires bottom-sheet-closed - Fired when the bottom sheet is closed
* @fires bottom-sheet-resized - Fired with the sheet's height in pixels
@@ -66,17 +61,12 @@ export class HaSnapBottomSheet extends LitElement {
/** Names the sheet for assistive technologies */
@property() public label?: string;
@property({ type: Boolean }) public dismissable = false;
@state() private _shown = false;
@state() private _position: BottomSheetPosition = "half";
@state() private _dragHeight?: number;
// How far below its lowest stop a drag has pulled the sheet
@state() private _dismissOffset?: number;
private _gesture = new SwipeGestureRecognizer();
private _dragging = false;
@@ -128,21 +118,13 @@ export class HaSnapBottomSheet extends LitElement {
@click=${this._handleBackdropClick}
></div>
<div
class="sheet ${classMap({
show: this._shown,
full,
dragging,
dismissing: Boolean(this._dismissOffset),
})}"
class="sheet ${classMap({ show: this._shown, full, dragging })}"
popover=${popover}
role="region"
aria-label=${ifDefined(this.label)}
style=${styleMap({
height: this._height(),
"min-height": this._collapsedHeight(),
transform: this._dismissOffset
? `translateY(${this._dismissOffset}px)`
: undefined,
})}
@touchstart=${this._handleTouchStart}
>
@@ -201,7 +183,6 @@ export class HaSnapBottomSheet extends LitElement {
this._dragging = false;
this._pendingTouch = undefined;
this._dragHeight = undefined;
this._dismissOffset = undefined;
document.removeEventListener("mousemove", this._handleMouseMove);
document.removeEventListener("mouseup", this._handleMouseUp);
document.removeEventListener("touchmove", this._handleTouchMove);
@@ -336,20 +317,15 @@ export class HaSnapBottomSheet extends LitElement {
private _startDrag(clientY: number) {
this._dragging = true;
this._dragStartHeight = this._sheet.offsetHeight;
if (this.dismissable) {
this._dismissOffset = 0;
}
this._gesture.start(clientY);
}
private _moveDrag(clientY: number) {
const delta = this._gesture.move(clientY);
const lowestHeight = this._stops()[0].height;
const height = this._dragStartHeight + delta;
this._dragHeight = Math.max(lowestHeight, height);
if (this._dismissOffset !== undefined) {
this._dismissOffset = Math.max(0, lowestHeight - height);
}
this._dragHeight = Math.max(
this._stops()[0].height,
this._dragStartHeight + delta
);
}
private _endDrag() {
@@ -359,18 +335,6 @@ export class HaSnapBottomSheet extends LitElement {
this._dragging = false;
document.body.style.removeProperty("cursor");
const { isSwipe, isDownwardSwipe } = this._gesture.end();
const dismissOffset = this._dismissOffset;
this._dismissOffset = undefined;
if (
dismissOffset &&
((isSwipe && isDownwardSwipe) ||
dismissOffset > this._stops()[0].height * DISMISS_DRAG_SHARE)
) {
this._position = this._stops()[0].position;
this._dragHeight = undefined;
this.closeSheet();
return;
}
this._position = this._stopAfterDrag(
this._sheet.offsetHeight,
isSwipe ? (isDownwardSwipe ? "down" : "up") : undefined
@@ -592,10 +556,6 @@ export class HaSnapBottomSheet extends LitElement {
.sheet.show {
transform: translateY(0);
}
.sheet.dismissing {
transition: none;
}
`;
}
+2 -3
View File
@@ -3,6 +3,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateName } from "../common/entity/compute_state_name";
import { debounce } from "../common/util/debounce";
import type { STTEngine } from "../data/stt";
import { listSTTEngines } from "../data/stt";
@@ -62,9 +63,7 @@ export class HaSTTPicker extends LitElement {
let label: string;
if (engine.engine_id.includes(".")) {
const stateObj = this.hass.states[engine.engine_id];
label = stateObj
? this.hass.formatEntityName(stateObj)
: engine.engine_id;
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
} else {
label = engine.name || engine.engine_id;
}
+3 -3
View File
@@ -95,8 +95,8 @@ export class HaTab extends LitElement {
font-size: var(--ha-font-size-s);
line-height: var(--ha-line-height-normal);
white-space: nowrap;
color: var(--ha-color-on-primary-normal);
background-color: var(--ha-color-fill-primary-normal-resting);
color: var(--text-primary-color);
background-color: var(--warning-color);
}
.icon {
@@ -112,7 +112,7 @@ export class HaTab extends LitElement {
width: var(--ha-space-2);
height: var(--ha-space-2);
border-radius: var(--ha-border-radius-pill);
background-color: var(--ha-color-fill-primary-loud-resting);
background-color: var(--warning-color);
}
/* Hidden from view but still read out through aria-describedby */
+43
View File
@@ -399,6 +399,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
return html`
<div class="add-target-wrapper">
<ha-generic-picker
.hass=${this.hass}
popover-placement="bottom-start"
.disabled=${this.disabled}
.autofocus=${this.autofocus}
@@ -408,6 +409,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
.emptyLabel=${this.hass.localize(
"ui.components.target-picker.no_targets"
)}
.sectionTitleFunction=${this._sectionTitleFunction}
.selectedSection=${this._selectedSection}
.popoverAnchor=${this._replaceTargetAnchor}
.rowRenderer=${this._renderRow}
@@ -608,6 +610,47 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
return undefined;
}
private _sectionTitleFunction = ({
firstIndex,
lastIndex,
firstItem,
secondItem,
itemsCount,
}: {
firstIndex: number;
lastIndex: number;
firstItem: PickerComboBoxItem | string;
secondItem: PickerComboBoxItem | string;
itemsCount: number;
}) => {
if (
firstItem === undefined ||
secondItem === undefined ||
typeof firstItem === "string" ||
(typeof secondItem === "string" && secondItem !== "padding") ||
(firstIndex === 0 && lastIndex === itemsCount - 1)
) {
return undefined;
}
const type = getTargetComboBoxItemType(firstItem as PickerComboBoxItem);
const translationType:
"areas" | "entities" | "devices" | "labels" | undefined =
type === "area" || type === "floor"
? "areas"
: type === "entity"
? "entities"
: type && type !== "empty"
? `${type}s`
: undefined;
return translationType
? this.hass.localize(
`ui.components.target-picker.type.${translationType}`
)
: undefined;
};
private _getItems = (searchString: string, section: string) => {
this._selectedSection = section as TargetTypeFloorless | undefined;
+17 -30
View File
@@ -8,34 +8,20 @@ import {
mdiProgressWrench,
mdiStopCircleOutline,
} from "@mdi/js";
import type { ContextType } from "@lit/context";
import type { HassConfig } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { formatDateTimeWithSeconds } from "../common/datetime/format_date_time";
import { consume } from "../common/decorators/consume";
import { transform } from "../common/decorators/transform";
import { configContext, internationalizationContext } from "../data/context";
import { customElement, property, query } from "lit/decorators";
import type { HomeAssistant } from "../types";
import type { Trace } from "../data/trace";
import type { HomeAssistantConfig } from "../types";
import "./ha-button";
import "./ha-generic-picker";
import type { HaGenericPicker } from "./ha-generic-picker";
import { formatDateTimeWithSeconds } from "../common/datetime/format_date_time";
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
@customElement("ha-trace-picker")
class HaTracePicker extends LitElement {
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@state()
@consume({ context: configContext, subscribe: true })
@transform<HomeAssistantConfig, HassConfig>({
transformer: ({ config }) => config,
})
private _hassConfig!: HassConfig;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public traces!: Trace[];
@@ -46,7 +32,8 @@ class HaTracePicker extends LitElement {
protected render() {
return html` <ha-generic-picker
name="trace"
.label=${this._i18n.localize(
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.automation.trace.select_trace"
)}
.value=${this.value}
@@ -84,27 +71,27 @@ class HaTracePicker extends LitElement {
id: trace.run_id,
primary: formatDateTimeWithSeconds(
new Date(trace.timestamp.start),
this._i18n.locale,
this._hassConfig
this.hass.locale,
this.hass.config
),
};
if (trace.state === "running") {
item.secondary = this._i18n.localize(
item.secondary = this.hass.localize(
"ui.panel.config.automation.trace.picker.still_running"
);
item.icon_path = mdiProgressClock;
} else if (trace.state === "debugged") {
item.secondary = this._i18n.localize(
item.secondary = this.hass.localize(
"ui.panel.config.automation.trace.picker.debugged"
);
item.icon_path = mdiProgressWrench;
} else if (trace.not_triggered) {
item.secondary = this._i18n.localize(
item.secondary = this.hass.localize(
"ui.panel.config.automation.trace.picker.not_triggered"
);
item.icon_path = mdiCircleOffOutline;
} else if (trace.script_execution === "finished") {
item.secondary = this._i18n.localize(
item.secondary = this.hass.localize(
"ui.panel.config.automation.trace.picker.finished",
{
executiontime: renderRuntime(),
@@ -112,7 +99,7 @@ class HaTracePicker extends LitElement {
);
item.icon_path = mdiCheckCircleOutline;
} else if (trace.script_execution === "aborted") {
item.secondary = this._i18n.localize(
item.secondary = this.hass.localize(
"ui.panel.config.automation.trace.picker.aborted",
{
executiontime: renderRuntime(),
@@ -120,7 +107,7 @@ class HaTracePicker extends LitElement {
);
item.icon_path = mdiAlertCircleOutline;
} else if (trace.script_execution === "cancelled") {
item.secondary = this._i18n.localize(
item.secondary = this.hass.localize(
"ui.panel.config.automation.trace.picker.cancelled",
{
executiontime: renderRuntime(),
@@ -160,7 +147,7 @@ class HaTracePicker extends LitElement {
icon = mdiHelpCircleOutline;
}
item.secondary = this._i18n.localize(
item.secondary = this.hass.localize(
`ui.panel.config.automation.trace.picker.${message}`,
{
error,
@@ -178,8 +165,8 @@ class HaTracePicker extends LitElement {
trace
? formatDateTimeWithSeconds(
new Date(trace.timestamp.start),
this._i18n.locale,
this._hassConfig
this.hass.locale,
this.hass.config
)
: runId
}`;
+2 -3
View File
@@ -3,6 +3,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateName } from "../common/entity/compute_state_name";
import { debounce } from "../common/util/debounce";
import type { TTSEngine } from "../data/tts";
import { listTTSEngines } from "../data/tts";
@@ -64,9 +65,7 @@ export class HaTTSPicker extends LitElement {
let label: string;
if (engine.engine_id.includes(".")) {
const stateObj = this.hass.states[engine.engine_id];
label = stateObj
? this.hass.formatEntityName(stateObj)
: engine.engine_id;
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
} else {
label = engine.name || engine.engine_id;
}
-28
View File
@@ -1,28 +0,0 @@
/**
* Find the title of the section the first visible row belongs to, in a list
* where section headers are plain strings followed by their items.
*
* Returns undefined while a header is the first visible row, or the next one
* is about to scroll into its place, so a sticky title does not overlap it.
* @param items - The list rows, with headers as strings
* @param firstVisible - Index of the first visible row
*/
export const findSectionTitle = (
items: readonly unknown[],
firstVisible: number
): string | undefined => {
if (
items[firstVisible] === undefined ||
typeof items[firstVisible] === "string" ||
typeof items[firstVisible + 1] === "string"
) {
return undefined;
}
for (let i = firstVisible - 1; i >= 0; i--) {
const item = items[i];
if (typeof item === "string") {
return item;
}
}
return undefined;
};
+1 -1
View File
@@ -153,7 +153,7 @@ class HaEntityMarker extends LitElement {
width: var(--ha-marker-size, 48px);
height: var(--ha-marker-size, 48px);
font-size: var(--ha-marker-font-size, var(--ha-font-size-xl));
border-radius: var(--ha-picture-border-radius);
border-radius: var(--ha-marker-border-radius, 50%);
border: var(--ha-marker-border-width, 3px) solid
var(--ha-marker-color, var(--card-background-color, #fff));
color: var(--primary-text-color);
+2 -7
View File
@@ -43,8 +43,6 @@ export interface MarkerLocation {
icon?: string;
iconPath?: string;
radius_color?: string;
/** Draw as a zone marker, without a radius */
zone_marker?: boolean;
location_editable?: boolean;
radius_editable?: boolean;
/** Clicking or activating the marker fires marker-clicked */
@@ -55,9 +53,6 @@ export interface MarkerLocation {
const ICON_SIZE = 24;
const isZoneMarker = (location: MarkerLocation): boolean =>
!!location.radius || !!location.zone_marker;
/**
* A map with draggable markers and zone circles, drawn by ha-map. Without
* editing support (the Leaflet fallback) they are static, with a notice.
@@ -195,7 +190,7 @@ export class HaLocationsEditor extends LitElement {
location: [location.latitude, location.longitude],
radius: location.radius,
element: this._elementFor(location),
elementSize: isZoneMarker(location)
elementSize: location.radius
? [ZONE_CIRCLE_SIZE, ZONE_CIRCLE_SIZE]
: [ICON_SIZE, ICON_SIZE],
title: location.name,
@@ -214,7 +209,7 @@ export class HaLocationsEditor extends LitElement {
private _elementsThemes?: Themes;
private _elementFor(location: MarkerLocation): HTMLElement | undefined {
const isZone = isZoneMarker(location);
const isZone = !!location.radius;
const key = JSON.stringify([
isZone,
location.icon,
+15 -2
View File
@@ -17,6 +17,7 @@ import { UNIT_KM } from "../../common/const";
import { transform } from "../../common/decorators/transform";
import { fireEvent } from "../../common/dom/fire_event";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { getEntityLocation } from "../../common/entity/get_entity_location";
import { supportsVectorMaps } from "../../common/map/base-layer";
import type {
@@ -78,7 +79,6 @@ import type {
ThemeMode,
} from "../../types";
import { FLOATING_LIFT, floatingMarkerFootprint } from "./ha-entity-marker";
import { computeZoneTrackerState } from "../../data/zone";
declare global {
// for fire event
@@ -1359,7 +1359,11 @@ export class HaMap extends ReactiveElement {
computeStateDomain(stateObj) === "zone" &&
(this.renderPassive || !stateObj.attributes.passive)
) {
zoneByState[computeZoneTrackerState(stateObj)] = stateObj.entity_id;
zoneByState[
stateObj.entity_id === "zone.home"
? "home"
: computeStateName(stateObj)
] = stateObj.entity_id;
if (
typeof stateObj.attributes.latitude === "number" &&
typeof stateObj.attributes.longitude === "number"
@@ -1987,8 +1991,13 @@ export class HaMap extends ReactiveElement {
font-size: var(--ha-font-size-s);
font-weight: var(--ha-font-weight-medium);
}
/* Markers are rounded squares to match the cluster bubble avatars */
ha-entity-marker {
--ha-marker-border-radius: var(--ha-border-radius-lg);
}
.cluster-bubble ha-entity-marker {
flex: none;
--ha-marker-border-radius: 10px;
}
${unsafeCSS(zoneMarkerStyles)}
.leaflet-bottom {
@@ -2047,6 +2056,10 @@ export class HaMap extends ReactiveElement {
box-shadow: none !important;
text-align: center;
}
ha-icon {
--mdc-icon-size: calc(var(--ha-marker-size, 48px) / 2);
}
`;
}
@@ -77,6 +77,7 @@ class BrowseMediaTTS extends LitElement {
this._provider?.supported_languages?.length
? html` <div class="options">
<ha-language-picker
.hass=${this.hass}
.languages=${this._provider.supported_languages}
.value=${this._language}
required
@@ -662,7 +662,11 @@ export class HaTargetPickerItemRow extends LitElement {
return {
name: device
? computeDeviceNameDisplay(device, this.hass.localize)
? computeDeviceNameDisplay(
device,
this.hass.localize,
this.hass.states
)
: item,
context,
fallbackIconPath: mdiDevices,
-1
View File
@@ -180,7 +180,6 @@ export class HaTileContainer extends LitElement {
.container.inline ::slotted([slot="features-inline"]) {
/* size the feature on the 6 column grid track, so it lines up with neighbouring tiles */
width: calc(50% - var(--column-gap, 0px) / 2 - var(--ha-space-3));
--feature-columns: 2;
flex: none;
padding: 0 var(--ha-space-3);
padding-inline-start: 0;
-7
View File
@@ -21,7 +21,6 @@ import "../ha-svg-icon";
* @cssprop --ha-tile-icon-border-radius - The border radius of the tile icon. defaults to `var(--ha-border-radius-pill)`.
*
* @attr {string} image-url - The URL of the image to display instead of an icon.
* @attr {string} accessible-label - The accessible name of the icon button when it is interactive.
*/
@customElement("ha-tile-icon")
export class HaTileIcon extends LitElement {
@@ -37,9 +36,6 @@ export class HaTileIcon extends LitElement {
@property({ type: String, attribute: "icon-path" })
public iconPath?: string;
@property({ attribute: "accessible-label" })
public accessibleLabel?: string;
@property({ attribute: false })
public actionHandlerOptions?: ActionHandlerOptions;
@@ -64,9 +60,6 @@ export class HaTileIcon extends LitElement {
class="container ${this.interactive && !hasImage ? "background" : ""}"
role=${ifDefined(this.interactive ? "button" : undefined)}
tabindex=${ifDefined(this.interactive ? "0" : undefined)}
aria-label=${ifDefined(
this.interactive ? this.accessibleLabel : undefined
)}
.actionHandler=${actionHandler(this.actionHandlerOptions)}
>
<slot name="icon">${this._renderIcon()}</slot>
+2 -1
View File
@@ -1,4 +1,5 @@
import { css, html } from "lit";
import { computeStateName } from "../../common/entity/compute_state_name";
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
import type { TraceId } from "../../data/trace";
import { getTraceUrl } from "../../data/trace";
@@ -17,7 +18,7 @@ export const computeChildTraceName = (
entry.platform === childId.domain && entry.unique_id === childId.item_id
)?.entity_id;
const stateObj = entityId ? hass.states[entityId] : undefined;
return stateObj ? hass.formatEntityName(stateObj) : entityId;
return stateObj ? computeStateName(stateObj) : entityId;
};
// A link from a step to the run of the script or automation it started.
+2 -2
View File
@@ -49,7 +49,7 @@ class PersonBadge extends LitElement {
width: 100%;
height: 100%;
background-size: cover;
border-radius: var(--ha-picture-border-radius);
border-radius: var(--ha-border-radius-circle);
}
.initials {
display: inline-flex;
@@ -58,7 +58,7 @@ class PersonBadge extends LitElement {
box-sizing: border-box;
width: 100%;
height: 100%;
border-radius: var(--ha-picture-border-radius);
border-radius: var(--ha-border-radius-circle);
background-color: var(--light-primary-color);
text-decoration: none;
color: var(--text-light-primary-color, var(--primary-text-color));
+2 -2
View File
@@ -96,7 +96,7 @@ class UserBadge extends LitElement {
width: 100%;
height: 100%;
background-size: cover;
border-radius: var(--ha-picture-border-radius);
border-radius: var(--ha-border-radius-circle);
}
.initials {
display: inline-flex;
@@ -105,7 +105,7 @@ class UserBadge extends LitElement {
box-sizing: border-box;
width: 100%;
height: 100%;
border-radius: var(--ha-picture-border-radius);
border-radius: var(--ha-border-radius-circle);
background-color: var(--light-primary-color);
text-decoration: none;
color: var(--text-light-primary-color, var(--primary-text-color));
Loaded 100 of 341 files, more files were not shown because too many files have changed in this diff. Show more