mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-19 12:19:32 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91549f554e | ||
|
|
247d560aa0 | ||
|
|
e15861a8fb | ||
|
|
67376887b9 |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -59,6 +59,7 @@ export default [
|
||||
"ha-control-select",
|
||||
"ha-control-select-menu",
|
||||
"ha-hs-color-picker",
|
||||
"ha-color-temp-brightness-picker",
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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><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><ha-adaptive-dialog open>
|
||||
<pre><code><ha-adaptive-dialog .hass=\${this.hass} open>
|
||||
<ha-form autofocus .schema=\${schema}></ha-form>
|
||||
</ha-adaptive-dialog></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
@@ -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"
|
||||
}
|
||||
|
||||
Generated
+116
-116
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
>(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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%);
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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}`;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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}`;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 "";
|
||||
|
||||
@@ -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 }) =>
|
||||
|
||||
@@ -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,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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}`;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
) ||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
}`;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
Reference in new issue
Block a user