mirror of
https://github.com/home-assistant/frontend.git
synced 2026-05-07 18:03:29 +00:00
Compare commits
29 Commits
ha-list-sh
...
rc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4f4cbd323 | ||
|
|
2e0df00f0f | ||
|
|
ce02f8072d | ||
|
|
c973aa7516 | ||
|
|
1e2328707c | ||
|
|
56368b88cd | ||
|
|
fcd4f177c1 | ||
|
|
7423ae7316 | ||
|
|
4427c581f1 | ||
|
|
cf86bb9821 | ||
|
|
897802dc16 | ||
|
|
dd65173c5a | ||
|
|
cf26753f7d | ||
|
|
d6ab8ffb16 | ||
|
|
2dc4b16eac | ||
|
|
1eba765bc2 | ||
|
|
398479ddd7 | ||
|
|
c4fd7bb3e1 | ||
|
|
4cfc67a95e | ||
|
|
e38d1964ca | ||
|
|
ec8b5c77bd | ||
|
|
425f2775e2 | ||
|
|
3a3d8191a3 | ||
|
|
04fca68549 | ||
|
|
35601a0900 | ||
|
|
e7016c15af | ||
|
|
624521e30b | ||
|
|
4876bfa639 | ||
|
|
5dea0764b2 |
@@ -1,188 +0,0 @@
|
||||
---
|
||||
title: List
|
||||
---
|
||||
|
||||
# List
|
||||
|
||||
The list family provides accessible, keyboard-navigable list containers and
|
||||
item variants. Pick the container based on semantics, then the item based on
|
||||
interactivity.
|
||||
|
||||
## Containers
|
||||
|
||||
### `<ha-list-base>`
|
||||
|
||||
A styled container with roving-tabindex keyboard navigation. Host role is
|
||||
`list`. Children should be `<ha-list-item-*>`. Arrow keys rove focus;
|
||||
Home/End jump to the first/last enabled item; Enter/Space activates the
|
||||
focused item.
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| ------------ | ------- | ------- | ------------------------------ |
|
||||
| `wrap-focus` | Boolean | `false` | Arrow keys wrap past the ends. |
|
||||
| `aria-label` | String | — | Accessible name. |
|
||||
|
||||
**Events**
|
||||
|
||||
- `ha-list-activated` — Enter/Space on a focused item. Detail
|
||||
`{ index: number, item: HaListItemBase }`.
|
||||
|
||||
**Methods**
|
||||
|
||||
- `focus()` — focus the active item (or the first focusable one).
|
||||
- `focusItemAtIndex(index)` — make the item at `index` active and focus it.
|
||||
- `getActiveItemIndex()` — current active index, or `-1`.
|
||||
- `setActiveItemIndex(index, focusItem?)` — move the active index without
|
||||
necessarily focusing.
|
||||
- `updateListItems()` — re-discover slotted items (called automatically on
|
||||
slotchange).
|
||||
|
||||
**CSS parts**
|
||||
|
||||
- `base` — the outer `<div role="list">`.
|
||||
|
||||
**CSS custom properties**
|
||||
|
||||
- `--ha-list-gap` — spacing between items. Defaults to `0`.
|
||||
- `--ha-list-padding` — padding around the list. Defaults to `0`.
|
||||
|
||||
### `<ha-list-selectable>`
|
||||
|
||||
Selectable list. Extends `ha-list-base`. Host role is `listbox`; items must be
|
||||
`<ha-list-item-option>` (role `option`). Set `multi` for multi-select; the
|
||||
host reflects `aria-multiselectable`.
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| ------- | ------- | ------- | -------------------------------------- |
|
||||
| `multi` | Boolean | `false` | Allow multiple options to be selected. |
|
||||
|
||||
**Events**
|
||||
|
||||
- `ha-list-selected` — selection changed. Detail
|
||||
`{ index: number | Set<number>, diff: { added: Set<number>, removed: Set<number> } }`.
|
||||
`index` is a `number` in single mode (`-1` when nothing selected) and a
|
||||
`Set<number>` in multi mode.
|
||||
|
||||
**Methods / getters**
|
||||
|
||||
- `selected` (getter) — current selection (`number` or `Set<number>`).
|
||||
- `selectedItems` (getter) — selected `HaListItemOption` elements, in index
|
||||
order.
|
||||
- `setSelected(indices)` — replace the entire selection.
|
||||
- `select(index)` — add `index` to the selection (replaces in single mode).
|
||||
- `toggle(index, force?)` — toggle a single index, or force on/off.
|
||||
- `clearSelection()` — clear all.
|
||||
|
||||
### `<ha-list-nav>`
|
||||
|
||||
Same as `ha-list-base`, but wrapped in a `<nav>` landmark
|
||||
(`<nav><div role="list">…</div></nav>`). Use `aria-label` to name the
|
||||
landmark — the value is forwarded to the inner `<nav>`. Items should be
|
||||
`<ha-list-item-button>` with an `href`.
|
||||
|
||||
**CSS parts**
|
||||
|
||||
- `nav` — the `<nav>` wrapper.
|
||||
- `base` — the inner `<div role="list">`.
|
||||
|
||||
## Items
|
||||
|
||||
All items inherit from `ha-row-item`, which provides the row layout and the
|
||||
shared slots/attributes below.
|
||||
|
||||
### Shared row layout (`ha-row-item`)
|
||||
|
||||
**Slots**
|
||||
|
||||
- `start` — leading container (icon/avatar).
|
||||
- `end` — trailing container (meta/chevron).
|
||||
- `headline` — primary text (overrides the `headline` attribute).
|
||||
- `supporting-text` — secondary text (overrides the `supporting-text` attribute).
|
||||
- `content` — escape hatch: replaces the entire middle column.
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| ----------------- | ------- | ------- | --------------------------------------- |
|
||||
| `headline` | String | — | Primary text. Overridden by the slot. |
|
||||
| `supporting-text` | String | — | Secondary text. Overridden by the slot. |
|
||||
| `disabled` | Boolean | `false` | Dims the row and blocks pointer events. |
|
||||
|
||||
**CSS parts**
|
||||
|
||||
`base`, `start`, `content`, `headline`, `supporting-text`, `end`.
|
||||
|
||||
**CSS custom properties**
|
||||
|
||||
- `--ha-row-item-padding-block` — vertical padding.
|
||||
- `--ha-row-item-padding-inline` — horizontal padding.
|
||||
- `--ha-row-item-gap` — gap between `start`, `content`, and `end`.
|
||||
- `--ha-row-item-min-height` — minimum row height (default `48px`).
|
||||
|
||||
### `<ha-list-item-base>`
|
||||
|
||||
Non-interactive list row. Host role is `listitem`. Inherits everything from
|
||||
`ha-row-item`.
|
||||
|
||||
**Attributes**
|
||||
|
||||
- `interactive` (Boolean, default `false`) — opt this row into the parent
|
||||
list's roving tabindex. Useful for sortable rows that need keyboard focus
|
||||
but no click action. Interactive subclasses set this automatically.
|
||||
|
||||
**CSS custom properties**
|
||||
|
||||
- `--ha-list-item-focus-radius` — focus outline border-radius.
|
||||
- `--ha-list-item-focus-width` — focus outline width (steady state).
|
||||
- `--ha-list-item-focus-width-start` — focus outline width at the start of
|
||||
the focus-in animation.
|
||||
- `--ha-list-item-focus-offset` — focus outline offset.
|
||||
- `--ha-list-item-focus-background` — background color on keyboard focus.
|
||||
|
||||
### `<ha-list-item-button>`
|
||||
|
||||
Interactive row. Renders an inner `<a>` when `href` is set, otherwise a
|
||||
`<button>`. The full row is the hit target. When placed inside a list using
|
||||
roving tabindex, the host is the tab stop and the inner element carries
|
||||
`tabindex="-1"`.
|
||||
|
||||
**Attributes**
|
||||
|
||||
- `href` (String) — when set, renders an `<a>` instead of a `<button>`.
|
||||
- `target` (String) — anchor `target` (requires `href`).
|
||||
- `rel` (String) — anchor `rel` (requires `href`).
|
||||
- `download` (String) — anchor `download` (requires `href`).
|
||||
|
||||
**CSS parts**
|
||||
|
||||
- `ripple` — the ripple effect element.
|
||||
|
||||
### `<ha-list-item-option>`
|
||||
|
||||
Selectable row. Host role is `option`; reflects `aria-selected`. Designed to
|
||||
sit inside `<ha-list-selectable>`, which owns selection state and toggles
|
||||
`selected` on this item — the option itself does not fire selection events.
|
||||
|
||||
**Attributes**
|
||||
|
||||
- `selected` (Boolean, default `false`, reflected) — set by the parent
|
||||
`ha-list-selectable`.
|
||||
- `value` (String) — value identifying the option.
|
||||
- `appearance` (`"line"` | `"checkbox"`, default `"line"`) — `"line"`
|
||||
highlights the row; `"checkbox"` renders a decorative `<ha-checkbox>`.
|
||||
- `selection-position` (`"start"` | `"end"`, default `"start"`) — side the
|
||||
checkbox sits on when `appearance="checkbox"`.
|
||||
|
||||
**CSS parts**
|
||||
|
||||
- `checkbox` — wrapper around the `<ha-checkbox>` when `appearance="checkbox"`.
|
||||
- `ripple` — the ripple effect element.
|
||||
|
||||
**CSS custom properties**
|
||||
|
||||
- `--ha-list-item-selected-background` — background color when selected
|
||||
(`appearance="line"`).
|
||||
@@ -1,415 +0,0 @@
|
||||
import {
|
||||
mdiAccount,
|
||||
mdiChevronRight,
|
||||
mdiCog,
|
||||
mdiHome,
|
||||
mdiInformationOutline,
|
||||
mdiMapMarker,
|
||||
mdiOpenInNew,
|
||||
mdiViewDashboard,
|
||||
mdiWifi,
|
||||
} from "@mdi/js";
|
||||
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-svg-icon";
|
||||
import "../../../../src/components/item/ha-list-item-base";
|
||||
import "../../../../src/components/item/ha-list-item-button";
|
||||
import "../../../../src/components/item/ha-list-item-option";
|
||||
import "../../../../src/components/list/ha-list-base";
|
||||
import "../../../../src/components/list/ha-list-nav";
|
||||
import "../../../../src/components/list/ha-list-selectable";
|
||||
import type { HaListSelectedDetail } from "../../../../src/components/list/types";
|
||||
|
||||
type Appearance = "line" | "checkbox";
|
||||
type Position = "start" | "end";
|
||||
|
||||
const appearances: Appearance[] = ["line", "checkbox"];
|
||||
const positions: Position[] = ["start", "end"];
|
||||
const selectedStates = [false, true];
|
||||
const disabledStates = [false, true];
|
||||
|
||||
@customElement("demo-components-ha-list")
|
||||
export class DemoHaList extends LitElement {
|
||||
@state() private _buttonClicks = 0;
|
||||
|
||||
@state() private _single: number | Set<number> = -1;
|
||||
|
||||
@state() private _multiLine: number | Set<number> = new Set();
|
||||
|
||||
@state() private _multiCheckStart: number | Set<number> = new Set();
|
||||
|
||||
@state() private _multiCheckEnd: number | Set<number> = new Set();
|
||||
|
||||
private _options = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon"];
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<h2>ha-list-base</h2>
|
||||
<p>
|
||||
Styled container with keyboard focus navigation. Children should be
|
||||
<code>ha-list-item-*</code>.
|
||||
</p>
|
||||
|
||||
<ha-card header="Info list (non-interactive rows)">
|
||||
<ha-list-base aria-label="Device info">
|
||||
<ha-list-item-base
|
||||
headline="IP address"
|
||||
supporting-text="192.168.1.42"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiWifi}></ha-svg-icon>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base headline="Location" supporting-text="Living room">
|
||||
<ha-svg-icon slot="start" .path=${mdiMapMarker}></ha-svg-icon>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base headline="Firmware" supporting-text="2026.4.1">
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiInformationOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item-base>
|
||||
</ha-list-base>
|
||||
</ha-card>
|
||||
|
||||
<ha-card header="Vertical list (default)">
|
||||
<ha-list-base aria-label="Example list">
|
||||
<ha-list-item-button>
|
||||
<ha-svg-icon slot="start" .path=${mdiHome}></ha-svg-icon>
|
||||
<span slot="headline">First row</span>
|
||||
<span slot="supporting-text">Supporting text</span>
|
||||
<ha-svg-icon slot="end" .path=${mdiChevronRight}></ha-svg-icon>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button>
|
||||
<ha-svg-icon slot="start" .path=${mdiAccount}></ha-svg-icon>
|
||||
<span slot="headline">Second row</span>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button disabled>
|
||||
<span slot="headline">Disabled row</span>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button>
|
||||
<span slot="headline">Fourth row</span>
|
||||
</ha-list-item-button>
|
||||
</ha-list-base>
|
||||
</ha-card>
|
||||
|
||||
<ha-card header="Vertical list with wrap-focus">
|
||||
<ha-list-base wrap-focus aria-label="Wrap focus">
|
||||
<ha-list-item-button>
|
||||
<span slot="headline">A</span>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button>
|
||||
<span slot="headline">B</span>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button>
|
||||
<span slot="headline">C</span>
|
||||
</ha-list-item-button>
|
||||
</ha-list-base>
|
||||
</ha-card>
|
||||
|
||||
<h2>ha-list-item-base</h2>
|
||||
<p>Non-interactive base row with slot permutations.</p>
|
||||
|
||||
<ha-card header="Slot permutations">
|
||||
<ha-list-base aria-label="Slot permutations">
|
||||
<ha-list-item-base headline="Headline only"></ha-list-item-base>
|
||||
<ha-list-item-base
|
||||
headline="Headline"
|
||||
supporting-text="Supporting text"
|
||||
></ha-list-item-base>
|
||||
<ha-list-item-base headline="Start + headline">
|
||||
<ha-svg-icon slot="start" .path=${mdiHome}></ha-svg-icon>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base headline="Start + headline + end">
|
||||
<ha-svg-icon slot="start" .path=${mdiHome}></ha-svg-icon>
|
||||
<ha-svg-icon slot="end" .path=${mdiChevronRight}></ha-svg-icon>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base
|
||||
headline="Full row"
|
||||
supporting-text="All slots filled"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiHome}></ha-svg-icon>
|
||||
<ha-svg-icon slot="end" .path=${mdiChevronRight}></ha-svg-icon>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<div slot="content" class="custom-content">
|
||||
<strong>Custom content escape hatch</strong>
|
||||
<span>Replaces the whole middle column</span>
|
||||
</div>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base headline="Disabled row" disabled>
|
||||
<ha-svg-icon slot="start" .path=${mdiHome}></ha-svg-icon>
|
||||
</ha-list-item-base>
|
||||
</ha-list-base>
|
||||
</ha-card>
|
||||
|
||||
<h2>ha-list-item-button</h2>
|
||||
<p>
|
||||
Interactive row. Renders an inner <code><a></code> when
|
||||
<code>href</code> is set, otherwise a <code><button></code>.
|
||||
</p>
|
||||
|
||||
<ha-card header="Button (default) / link (with href)">
|
||||
<ha-list-base aria-label="Button items">
|
||||
<ha-list-item-button @click=${this._onButtonClick}>
|
||||
<ha-svg-icon slot="start" .path=${mdiHome}></ha-svg-icon>
|
||||
<span slot="headline">Button (clicks: ${this._buttonClicks})</span>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button
|
||||
href="https://www.home-assistant.io/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
<span slot="headline">Link (opens in new tab)</span>
|
||||
<span slot="supporting-text"
|
||||
>Cmd/Ctrl-click still opens in new tab</span
|
||||
>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button disabled>
|
||||
<span slot="headline">Disabled button</span>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button href="#nope" disabled>
|
||||
<span slot="headline">Disabled link</span>
|
||||
</ha-list-item-button>
|
||||
</ha-list-base>
|
||||
</ha-card>
|
||||
|
||||
<h2>ha-list-selectable + ha-list-item-option</h2>
|
||||
<p>
|
||||
Selectable list (<code>role="listbox"</code>). Items must be
|
||||
<code>ha-list-item-option</code>. Set <code>multi</code> for
|
||||
multi-select.
|
||||
</p>
|
||||
|
||||
<ha-card header="Single select, appearance=line">
|
||||
<ha-list-selectable
|
||||
aria-label="Single select"
|
||||
@ha-list-selected=${this._onSingle}
|
||||
>
|
||||
${this._options.map(
|
||||
(o, i) => html`
|
||||
<ha-list-item-option
|
||||
.value=${o}
|
||||
?selected=${this._isSel(this._single, i)}
|
||||
>
|
||||
<span slot="headline">${o}</span>
|
||||
</ha-list-item-option>
|
||||
`
|
||||
)}
|
||||
</ha-list-selectable>
|
||||
<pre>selected: ${JSON.stringify(this._toJson(this._single))}</pre>
|
||||
</ha-card>
|
||||
|
||||
<ha-card header="Multi select, appearance=line">
|
||||
<ha-list-selectable
|
||||
multi
|
||||
aria-label="Multi select line"
|
||||
@ha-list-selected=${this._onMultiLine}
|
||||
>
|
||||
${this._options.map(
|
||||
(o, i) => html`
|
||||
<ha-list-item-option
|
||||
.value=${o}
|
||||
?selected=${this._isSel(this._multiLine, i)}
|
||||
>
|
||||
<span slot="headline">${o}</span>
|
||||
</ha-list-item-option>
|
||||
`
|
||||
)}
|
||||
</ha-list-selectable>
|
||||
<pre>selected: ${JSON.stringify(this._toJson(this._multiLine))}</pre>
|
||||
</ha-card>
|
||||
|
||||
<ha-card
|
||||
header='Multi select, appearance=checkbox, selection-position="start"'
|
||||
>
|
||||
<ha-list-selectable
|
||||
multi
|
||||
aria-label="Multi checkbox start"
|
||||
@ha-list-selected=${this._onMultiCheckStart}
|
||||
>
|
||||
${this._options.map(
|
||||
(o, i) => html`
|
||||
<ha-list-item-option
|
||||
appearance="checkbox"
|
||||
selection-position="start"
|
||||
.value=${o}
|
||||
?selected=${this._isSel(this._multiCheckStart, i)}
|
||||
>
|
||||
<span slot="headline">${o}</span>
|
||||
</ha-list-item-option>
|
||||
`
|
||||
)}
|
||||
</ha-list-selectable>
|
||||
<pre>
|
||||
selected: ${JSON.stringify(this._toJson(this._multiCheckStart))}</pre
|
||||
>
|
||||
</ha-card>
|
||||
|
||||
<ha-card
|
||||
header='Multi select, appearance=checkbox, selection-position="end"'
|
||||
>
|
||||
<ha-list-selectable
|
||||
multi
|
||||
aria-label="Multi checkbox end"
|
||||
@ha-list-selected=${this._onMultiCheckEnd}
|
||||
>
|
||||
${this._options.map(
|
||||
(o, i) => html`
|
||||
<ha-list-item-option
|
||||
appearance="checkbox"
|
||||
selection-position="end"
|
||||
.value=${o}
|
||||
?selected=${this._isSel(this._multiCheckEnd, i)}
|
||||
>
|
||||
<span slot="headline">${o}</span>
|
||||
<span slot="supporting-text">${o.length} characters</span>
|
||||
</ha-list-item-option>
|
||||
`
|
||||
)}
|
||||
</ha-list-selectable>
|
||||
<pre>
|
||||
selected: ${JSON.stringify(this._toJson(this._multiCheckEnd))}</pre
|
||||
>
|
||||
</ha-card>
|
||||
|
||||
<ha-card header="Option: all combinations">
|
||||
<div class="grid">
|
||||
${appearances.map((appearance) =>
|
||||
positions.map((position) =>
|
||||
selectedStates.map((selected) =>
|
||||
disabledStates.map(
|
||||
(disabled) => html`
|
||||
<div role="listbox" class="wrap" aria-label="single option">
|
||||
<ha-list-item-option
|
||||
appearance=${appearance}
|
||||
selection-position=${position}
|
||||
?selected=${selected}
|
||||
?disabled=${disabled}
|
||||
>
|
||||
<span slot="headline"
|
||||
>${appearance} / pos=${position}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>selected=${String(selected)}
|
||||
disabled=${String(disabled)}</span
|
||||
>
|
||||
</ha-list-item-option>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
)
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</ha-card>
|
||||
|
||||
<h2>ha-list-nav</h2>
|
||||
<p>
|
||||
Same as <code>ha-list-base</code> but wrapped in a
|
||||
<code><nav></code> landmark.
|
||||
</p>
|
||||
|
||||
<ha-card header="Sidebar-style navigation">
|
||||
<ha-list-nav aria-label="Primary navigation">
|
||||
${[
|
||||
{ name: "Overview", path: "#overview", icon: mdiHome },
|
||||
{ name: "Dashboards", path: "#dashboards", icon: mdiViewDashboard },
|
||||
{ name: "Map", path: "#map", icon: mdiMapMarker },
|
||||
{ name: "Settings", path: "#settings", icon: mdiCog },
|
||||
].map(
|
||||
(p) => html`
|
||||
<ha-list-item-button .href=${p.path}>
|
||||
<ha-svg-icon slot="start" .path=${p.icon}></ha-svg-icon>
|
||||
<span slot="headline">${p.name}</span>
|
||||
<ha-svg-icon slot="end" .path=${mdiChevronRight}></ha-svg-icon>
|
||||
</ha-list-item-button>
|
||||
`
|
||||
)}
|
||||
</ha-list-nav>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _isSel(value: number | Set<number>, index: number): boolean {
|
||||
if (typeof value === "number") {
|
||||
return value === index;
|
||||
}
|
||||
return value.has(index);
|
||||
}
|
||||
|
||||
private _toJson(value: number | Set<number>): unknown {
|
||||
return value instanceof Set ? [...value] : value;
|
||||
}
|
||||
|
||||
private _onButtonClick = () => {
|
||||
this._buttonClicks++;
|
||||
};
|
||||
|
||||
private _onSingle = (ev: CustomEvent<HaListSelectedDetail>) => {
|
||||
this._single = ev.detail.index;
|
||||
};
|
||||
|
||||
private _onMultiLine = (ev: CustomEvent<HaListSelectedDetail>) => {
|
||||
this._multiLine = ev.detail.index;
|
||||
};
|
||||
|
||||
private _onMultiCheckStart = (ev: CustomEvent<HaListSelectedDetail>) => {
|
||||
this._multiCheckStart = ev.detail.index;
|
||||
};
|
||||
|
||||
private _onMultiCheckEnd = (ev: CustomEvent<HaListSelectedDetail>) => {
|
||||
this._multiCheckEnd = ev.detail.index;
|
||||
};
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-4);
|
||||
padding: var(--ha-space-6);
|
||||
}
|
||||
h2 {
|
||||
margin: var(--ha-space-4) 0 0;
|
||||
font-size: var(--ha-font-size-xl);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
p {
|
||||
margin: 0 0 var(--ha-space-2);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-card {
|
||||
max-width: 560px;
|
||||
}
|
||||
pre {
|
||||
padding: var(--ha-space-4);
|
||||
background: var(--secondary-background-color);
|
||||
margin: 0;
|
||||
}
|
||||
.custom-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-1);
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--ha-space-3);
|
||||
padding: var(--ha-space-3);
|
||||
}
|
||||
.wrap {
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
}
|
||||
.drag-handle {
|
||||
cursor: grab;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-components-ha-list": DemoHaList;
|
||||
}
|
||||
}
|
||||
@@ -43,22 +43,12 @@ const fullOptions: SelectBoxOption[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const manyOptions: SelectBoxOption[] = [
|
||||
{ value: "opt1", label: "Option 1" },
|
||||
{ value: "opt2", label: "Option 2" },
|
||||
{ value: "opt3", label: "Option 3" },
|
||||
{ value: "opt4", label: "Option 4" },
|
||||
{ value: "opt5", label: "Option 5" },
|
||||
{ value: "opt6", label: "Option 6" },
|
||||
];
|
||||
|
||||
const selects: {
|
||||
id: string;
|
||||
label: string;
|
||||
class?: string;
|
||||
options: SelectBoxOption[];
|
||||
disabled?: boolean;
|
||||
maxColumns?: number;
|
||||
}[] = [
|
||||
{
|
||||
id: "basic",
|
||||
@@ -70,12 +60,6 @@ const selects: {
|
||||
label: "With description and image",
|
||||
options: fullOptions,
|
||||
},
|
||||
{
|
||||
id: "two-columns",
|
||||
label: "2 columns (maxColumns=2)",
|
||||
options: manyOptions,
|
||||
maxColumns: 2,
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-components-ha-select-box")
|
||||
@@ -83,14 +67,13 @@ export class DemoHaSelectBox extends LitElement {
|
||||
@state() private value?: string = "off";
|
||||
|
||||
handleValueChanged(e: CustomEvent) {
|
||||
console.log(e.detail.value);
|
||||
this.value = e.detail.value as string;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${repeat(selects, (select) => {
|
||||
const { id, label, options, maxColumns } = select;
|
||||
const { id, label, options } = select;
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
@@ -98,7 +81,6 @@ export class DemoHaSelectBox extends LitElement {
|
||||
<ha-select-box
|
||||
.value=${this.value}
|
||||
.options=${options}
|
||||
.maxColumns=${maxColumns}
|
||||
@value-changed=${this.handleValueChanged}
|
||||
>
|
||||
</ha-select-box>
|
||||
|
||||
42
package.json
42
package.json
@@ -33,28 +33,27 @@
|
||||
"@codemirror/lang-jinja": "6.0.1",
|
||||
"@codemirror/lang-yaml": "6.1.3",
|
||||
"@codemirror/language": "6.12.3",
|
||||
"@codemirror/lint": "6.9.5",
|
||||
"@codemirror/search": "6.7.0",
|
||||
"@codemirror/state": "6.6.0",
|
||||
"@codemirror/view": "6.41.1",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.4.1",
|
||||
"@formatjs/intl-displaynames": "7.3.4",
|
||||
"@formatjs/intl-durationformat": "0.10.7",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.5",
|
||||
"@formatjs/intl-listformat": "8.3.4",
|
||||
"@formatjs/intl-locale": "5.3.4",
|
||||
"@formatjs/intl-numberformat": "9.3.4",
|
||||
"@formatjs/intl-pluralrules": "6.3.4",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.4",
|
||||
"@formatjs/intl-datetimeformat": "7.4.0",
|
||||
"@formatjs/intl-displaynames": "7.3.3",
|
||||
"@formatjs/intl-durationformat": "0.10.5",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.4",
|
||||
"@formatjs/intl-listformat": "8.3.3",
|
||||
"@formatjs/intl-locale": "5.3.3",
|
||||
"@formatjs/intl-numberformat": "9.3.3",
|
||||
"@formatjs/intl-pluralrules": "6.3.3",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.3",
|
||||
"@fullcalendar/core": "6.1.20",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"@fullcalendar/interaction": "6.1.20",
|
||||
"@fullcalendar/list": "6.1.20",
|
||||
"@fullcalendar/luxon3": "6.1.20",
|
||||
"@fullcalendar/timegrid": "6.1.20",
|
||||
"@home-assistant/webawesome": "3.3.1-ha.3",
|
||||
"@home-assistant/webawesome": "3.3.1-ha.1",
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"@lit-labs/motion": "1.1.0",
|
||||
"@lit-labs/observers": "2.1.0",
|
||||
@@ -66,6 +65,7 @@
|
||||
"@material/mwc-drawer": "0.27.0",
|
||||
"@material/mwc-formfield": "patch:@material/mwc-formfield@npm%3A0.27.0#~/.yarn/patches/@material-mwc-formfield-npm-0.27.0-9528cb60f6.patch",
|
||||
"@material/mwc-list": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"@material/mwc-radio": "0.27.0",
|
||||
"@material/mwc-top-app-bar": "0.27.0",
|
||||
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
||||
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
||||
@@ -99,7 +99,7 @@
|
||||
"hls.js": "1.6.16",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "11.2.3",
|
||||
"intl-messageformat": "11.2.2",
|
||||
"js-yaml": "4.1.1",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
@@ -107,7 +107,7 @@
|
||||
"lit": "3.3.2",
|
||||
"lit-html": "3.3.2",
|
||||
"luxon": "3.7.2",
|
||||
"marked": "18.0.3",
|
||||
"marked": "18.0.2",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.4",
|
||||
"object-hash": "3.0.0",
|
||||
@@ -133,16 +133,16 @@
|
||||
"@babel/core": "7.29.0",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.8",
|
||||
"@babel/plugin-transform-runtime": "7.29.0",
|
||||
"@babel/preset-env": "7.29.3",
|
||||
"@babel/preset-env": "7.29.2",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.22.1",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@html-eslint/eslint-plugin": "0.60.0",
|
||||
"@html-eslint/eslint-plugin": "0.59.0",
|
||||
"@lokalise/node-api": "15.7.1",
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.1.0",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.9",
|
||||
"@rspack/core": "2.0.1",
|
||||
"@rspack/core": "2.0.0",
|
||||
"@rspack/dev-server": "2.0.1",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.26",
|
||||
@@ -166,7 +166,7 @@
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.4",
|
||||
"del": "8.0.1",
|
||||
"eslint": "10.3.0",
|
||||
"eslint": "10.2.1",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-webpack": "0.13.11",
|
||||
"eslint-plugin-import-x": "4.16.2",
|
||||
@@ -177,14 +177,14 @@
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.4",
|
||||
"glob": "13.0.6",
|
||||
"globals": "17.6.0",
|
||||
"globals": "17.5.0",
|
||||
"gulp": "5.0.1",
|
||||
"gulp-brotli": "3.0.0",
|
||||
"gulp-json-transform": "0.5.0",
|
||||
"gulp-rename": "2.1.0",
|
||||
"html-minifier-terser": "7.2.0",
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "29.1.1",
|
||||
"jsdom": "29.0.2",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "16.4.0",
|
||||
"lit-analyzer": "2.0.3",
|
||||
@@ -200,7 +200,7 @@
|
||||
"terser-webpack-plugin": "5.5.0",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.59.1",
|
||||
"typescript-eslint": "8.59.0",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.1.5",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
@@ -213,7 +213,7 @@
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "2.1.2",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"globals": "17.6.0",
|
||||
"globals": "17.5.0",
|
||||
"tslib": "2.8.1",
|
||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"glob@^10.2.2": "^10.5.0"
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20260429.0"
|
||||
version = "20260429.3"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* Walks up the composed tree (jumping shadow roots → their hosts), returning
|
||||
* the ancestor chain top-down. Used to compare two nodes that may live in
|
||||
* different shadow trees — `Node.compareDocumentPosition` only works within a
|
||||
* single root and returns `DOCUMENT_POSITION_DISCONNECTED` otherwise.
|
||||
*/
|
||||
const composedAncestorPath = (node: Node): Node[] => {
|
||||
const path: Node[] = [];
|
||||
let cur: Node | null = node;
|
||||
while (cur) {
|
||||
path.push(cur);
|
||||
const parent = cur.parentNode;
|
||||
if (parent instanceof ShadowRoot) {
|
||||
cur = parent.host;
|
||||
} else if (parent) {
|
||||
cur = parent;
|
||||
} else {
|
||||
const root = cur.getRootNode();
|
||||
cur = root instanceof ShadowRoot ? root.host : null;
|
||||
}
|
||||
}
|
||||
return path.reverse();
|
||||
};
|
||||
|
||||
/**
|
||||
* Document-order comparator that works across shadow boundaries. Suitable as
|
||||
* the `Array.prototype.sort` callback for collections of nodes that may live
|
||||
* in different shadow trees.
|
||||
*/
|
||||
export const compareNodeOrder = (a: Node, b: Node): number => {
|
||||
if (a === b) {
|
||||
return 0;
|
||||
}
|
||||
const pa = composedAncestorPath(a);
|
||||
const pb = composedAncestorPath(b);
|
||||
let i = 0;
|
||||
while (i < pa.length && i < pb.length && pa[i] === pb[i]) {
|
||||
i++;
|
||||
}
|
||||
if (i === 0) {
|
||||
return 0;
|
||||
}
|
||||
if (i === pa.length) {
|
||||
return -1;
|
||||
}
|
||||
if (i === pb.length) {
|
||||
return 1;
|
||||
}
|
||||
// pa[i] and pb[i] are siblings under the LCA, guaranteed same root.
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return pa[i].compareDocumentPosition(pb[i]) & Node.DOCUMENT_POSITION_FOLLOWING
|
||||
? -1
|
||||
: 1;
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
// Generates an RFC 4122 v4 UUID. Falls back to crypto.getRandomValues when
|
||||
// crypto.randomUUID is unavailable (e.g. non-secure HTTP contexts on a LAN).
|
||||
export const generateUuidV4 = (): string => {
|
||||
if (typeof crypto.randomUUID === "function") {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
const bytes = new Uint8Array(16);
|
||||
crypto.getRandomValues(bytes);
|
||||
/* eslint-disable no-bitwise */
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
||||
/* eslint-enable no-bitwise */
|
||||
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(
|
||||
""
|
||||
);
|
||||
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
||||
};
|
||||
@@ -10,17 +10,6 @@ export const setViewTransitionDisabled = (disabled: boolean): void => {
|
||||
isViewTransitionDisabled = disabled;
|
||||
};
|
||||
|
||||
const isAbortError = (err: unknown): boolean =>
|
||||
err instanceof DOMException
|
||||
? err.name === "AbortError"
|
||||
: err instanceof Error && err.name === "AbortError";
|
||||
|
||||
const ignoreAbortError = (err: unknown): void => {
|
||||
if (!isAbortError(err)) {
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes a synchronous callback within a View Transition if supported, otherwise runs it directly.
|
||||
*
|
||||
@@ -51,8 +40,7 @@ export const withViewTransition = (
|
||||
callbackInvoked = true;
|
||||
callback(true);
|
||||
});
|
||||
transition.ready.catch(ignoreAbortError);
|
||||
return transition.finished.catch(ignoreAbortError);
|
||||
return transition.finished;
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
|
||||
@@ -11,8 +11,7 @@ import "../ha-icon-button";
|
||||
@customElement("ha-automation-row-event-chip")
|
||||
export class HaAutomationRowEventChip extends LitElement {
|
||||
@property({ reflect: true })
|
||||
public variant: "info" | "warning" | "success" | "danger" | "neutral" =
|
||||
"info";
|
||||
public variant: "info" | "warning" | "success" | "danger" = "info";
|
||||
|
||||
@property({ type: Boolean })
|
||||
public interactive = false;
|
||||
@@ -92,12 +91,6 @@ export class HaAutomationRowEventChip extends LitElement {
|
||||
--text-color: var(--ha-color-on-warning-normal);
|
||||
}
|
||||
|
||||
:host([variant="neutral"]) {
|
||||
--background-color: var(--ha-color-fill-neutral-normal-resting);
|
||||
--background-color-hover: var(--ha-color-fill-neutral-normal-hover);
|
||||
--text-color: var(--ha-color-on-neutral-normal);
|
||||
}
|
||||
|
||||
:host([variant="success"]) {
|
||||
--background-color: var(--ha-color-fill-success-normal-resting);
|
||||
--background-color-hover: var(--ha-color-fill-success-normal-hover);
|
||||
|
||||
@@ -1499,7 +1499,6 @@ export class HaChartBase extends LitElement {
|
||||
margin-inline-start: var(--ha-space-1);
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
line-height: 1;
|
||||
}
|
||||
.chart-legend .legend-toggle {
|
||||
background: none;
|
||||
|
||||
@@ -399,12 +399,12 @@ export class StateHistoryCharts extends LitElement {
|
||||
|
||||
.entry-container {
|
||||
width: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.entry-container.line {
|
||||
flex: 1;
|
||||
padding-top: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.entry-container:hover {
|
||||
|
||||
@@ -95,8 +95,6 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
|
||||
@property({ type: Boolean }) public error = false;
|
||||
|
||||
@property({ type: Boolean }) public lint = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "disable-fullscreen" })
|
||||
public disableFullscreen = false;
|
||||
|
||||
@@ -165,40 +163,6 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
return !!this.renderRoot.querySelector(`span.${className}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a YAML parse error (or null to clear) into the lint gutter as a
|
||||
* diagnostic. Avoids re-parsing the document — the caller (ha-yaml-editor)
|
||||
* already has the error from its own js-yaml load() call.
|
||||
*/
|
||||
public setYamlError(
|
||||
err: {
|
||||
mark?: { position: number; line: number; column: number };
|
||||
reason?: string;
|
||||
} | null
|
||||
): void {
|
||||
if (!this.codemirror || !this._loadedCodeMirror) return;
|
||||
let diagnostics: {
|
||||
from: number;
|
||||
to: number;
|
||||
severity: "error";
|
||||
message: string;
|
||||
}[] = [];
|
||||
if (err) {
|
||||
const doc = this.codemirror.state.doc;
|
||||
const pos = err.mark ? Math.min(err.mark.position, doc.length) : 0;
|
||||
const line = doc.lineAt(pos);
|
||||
const message = `${
|
||||
err.reason ||
|
||||
this.hass?.localize("ui.components.yaml-editor.error") ||
|
||||
"YAML syntax error"
|
||||
}${err.mark ? ` (${this.hass?.localize("ui.components.yaml-editor.error_location", { line: err.mark.line + 1, column: err.mark.column + 1 })})` : ""}`;
|
||||
diagnostics = [{ from: pos, to: line.to, severity: "error", message }];
|
||||
}
|
||||
this.codemirror.dispatch(
|
||||
this._loadedCodeMirror.setDiagnostics(this.codemirror.state, diagnostics)
|
||||
);
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.classList.toggle("in-dialog", this.inDialog);
|
||||
@@ -256,37 +220,16 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
transactions.push({
|
||||
effects: [
|
||||
this._loadedCodeMirror!.langCompartment!.reconfigure(this._mode),
|
||||
this._loadedCodeMirror!.yamlLintCompartment!.reconfigure(
|
||||
this.lint && !this.readOnly
|
||||
? [this._loadedCodeMirror!.lintGutter()]
|
||||
: []
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
if (changedProps.has("readOnly")) {
|
||||
transactions.push({
|
||||
effects: [
|
||||
this._loadedCodeMirror!.readonlyCompartment!.reconfigure(
|
||||
this._loadedCodeMirror!.EditorView!.editable.of(!this.readOnly)
|
||||
),
|
||||
this._loadedCodeMirror!.yamlLintCompartment!.reconfigure(
|
||||
this.lint && !this.readOnly
|
||||
? [this._loadedCodeMirror!.lintGutter()]
|
||||
: []
|
||||
),
|
||||
],
|
||||
});
|
||||
this._updateToolbarButtons();
|
||||
}
|
||||
if (changedProps.has("lint")) {
|
||||
transactions.push({
|
||||
effects: this._loadedCodeMirror!.yamlLintCompartment!.reconfigure(
|
||||
this.lint && !this.readOnly
|
||||
? [this._loadedCodeMirror!.lintGutter()]
|
||||
: []
|
||||
effects: this._loadedCodeMirror!.readonlyCompartment!.reconfigure(
|
||||
this._loadedCodeMirror!.EditorView!.editable.of(!this.readOnly)
|
||||
),
|
||||
});
|
||||
this._updateToolbarButtons();
|
||||
}
|
||||
if (changedProps.has("linewrap")) {
|
||||
transactions.push({
|
||||
@@ -369,7 +312,6 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
...this._loadedCodeMirror.searchKeymap,
|
||||
...this._loadedCodeMirror.historyKeymap,
|
||||
...this._loadedCodeMirror.tabKeyBindings,
|
||||
...this._loadedCodeMirror.lintKeymap,
|
||||
saveKeyBinding,
|
||||
]),
|
||||
this._loadedCodeMirror.search({ top: true }),
|
||||
@@ -384,9 +326,6 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
this._loadedCodeMirror.linewrapCompartment.of(
|
||||
this.linewrap ? this._loadedCodeMirror.EditorView.lineWrapping : []
|
||||
),
|
||||
this._loadedCodeMirror.yamlLintCompartment.of(
|
||||
this.lint && !this.readOnly ? [this._loadedCodeMirror.lintGutter()] : []
|
||||
),
|
||||
this._loadedCodeMirror.EditorView.updateListener.of(this._onUpdate),
|
||||
this._loadedCodeMirror.tooltips({
|
||||
position: "absolute",
|
||||
|
||||
@@ -13,17 +13,14 @@ import type { RelatedResult } from "../data/search";
|
||||
import { findRelated } from "../data/search";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-floor-icon";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-list";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-tree-indicator";
|
||||
import "./item/ha-list-item-option";
|
||||
import type { HaListItemOption } from "./item/ha-list-item-option";
|
||||
import "./list/ha-list-selectable";
|
||||
import type { HaListSelectable } from "./list/ha-list-selectable";
|
||||
import type { HaListSelectedDetail } from "./list/types";
|
||||
|
||||
@customElement("ha-filter-floor-areas")
|
||||
export class HaFilterFloorAreas extends LitElement {
|
||||
@@ -78,33 +75,27 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
</div>
|
||||
${this._shouldRender
|
||||
? html`
|
||||
<ha-list-selectable
|
||||
class="ha-scrollbar"
|
||||
multi
|
||||
@ha-list-selected=${this._handleListChanged}
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.panel.config.areas.caption"
|
||||
)}
|
||||
>
|
||||
<ha-list class="ha-scrollbar">
|
||||
${repeat(
|
||||
areas?.floors || [],
|
||||
(floor) => floor.floor_id,
|
||||
(floor) => html`
|
||||
<ha-list-item-option
|
||||
appearance="checkbox"
|
||||
selection-position="end"
|
||||
<ha-check-list-item
|
||||
.value=${floor.floor_id}
|
||||
.type=${"floors"}
|
||||
.selected=${this.value?.floors?.includes(
|
||||
floor.floor_id
|
||||
) || false}
|
||||
graphic="icon"
|
||||
@request-selected=${this._handleItemClick}
|
||||
@keydown=${this._handleItemKeydown}
|
||||
>
|
||||
<ha-floor-icon
|
||||
slot="start"
|
||||
slot="graphic"
|
||||
.floor=${floor}
|
||||
></ha-floor-icon>
|
||||
<span slot="headline">${floor.name} </span>
|
||||
</ha-list-item-option>
|
||||
${floor.name}
|
||||
</ha-check-list-item>
|
||||
${repeat(
|
||||
floor.areas,
|
||||
(area, index) =>
|
||||
@@ -119,7 +110,7 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
(area) => area.area_id,
|
||||
(area) => this._renderArea(area)
|
||||
)}
|
||||
</ha-list-selectable>
|
||||
</ha-list>
|
||||
`
|
||||
: nothing}
|
||||
</ha-expansion-panel>
|
||||
@@ -128,83 +119,79 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
|
||||
private _renderArea(area, last = false) {
|
||||
const hasFloor = !!area.floor_id;
|
||||
|
||||
return html`
|
||||
<ha-list-item-option
|
||||
appearance="checkbox"
|
||||
selection-position="end"
|
||||
<ha-check-list-item
|
||||
.value=${area.area_id}
|
||||
.selected=${this.value?.areas?.includes(area.area_id) || false}
|
||||
.type=${"areas"}
|
||||
graphic="icon"
|
||||
@request-selected=${this._handleItemClick}
|
||||
@keydown=${this._handleItemKeydown}
|
||||
class=${classMap({
|
||||
rtl: computeRTL(this.hass),
|
||||
floor: hasFloor,
|
||||
})}
|
||||
>
|
||||
${hasFloor
|
||||
? html`<ha-tree-indicator
|
||||
slot="start"
|
||||
.end=${last}
|
||||
></ha-tree-indicator>`
|
||||
? html`
|
||||
<ha-tree-indicator
|
||||
.end=${last}
|
||||
slot="graphic"
|
||||
></ha-tree-indicator>
|
||||
`
|
||||
: nothing}
|
||||
${area.icon
|
||||
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
|
||||
? html`<ha-icon slot="graphic" .icon=${area.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="start"
|
||||
slot="graphic"
|
||||
.path=${mdiTextureBox}
|
||||
></ha-svg-icon>`}
|
||||
<span slot="headline">${area.name}</span>
|
||||
</ha-list-item-option>
|
||||
${area.name}
|
||||
</ha-check-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleListChanged(ev: CustomEvent<HaListSelectedDetail>) {
|
||||
if (!ev.detail.diff?.added.size && !ev.detail.diff?.removed.size) {
|
||||
private _handleItemKeydown(ev) {
|
||||
if (ev.key === " " || ev.key === "Enter") {
|
||||
ev.preventDefault();
|
||||
this._handleItemClick(ev);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleItemClick(ev) {
|
||||
ev.stopPropagation();
|
||||
|
||||
const listItem = ev.currentTarget;
|
||||
const type = listItem?.type;
|
||||
const value = listItem?.value;
|
||||
|
||||
if (ev.detail.selected === listItem.selected || !value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.detail.diff?.added.size) {
|
||||
const addedIndex = ev.detail.diff.added.values().next().value;
|
||||
if (addedIndex === undefined) {
|
||||
return;
|
||||
}
|
||||
const addedItem = (ev.currentTarget as HaListSelectable).items[
|
||||
addedIndex
|
||||
] as HaListItemOption & { type: string; value: string };
|
||||
|
||||
if (this.value?.[type]?.includes(value)) {
|
||||
this.value = {
|
||||
...this.value,
|
||||
[type]: this.value[type].filter((val) => val !== value),
|
||||
};
|
||||
} else {
|
||||
if (!this.value) {
|
||||
this.value = {};
|
||||
}
|
||||
this.value = {
|
||||
...this.value,
|
||||
[addedItem.type]: [
|
||||
...(this.value[addedItem.type] || []),
|
||||
addedItem.value,
|
||||
],
|
||||
};
|
||||
} else {
|
||||
const removedIndex = ev.detail.diff?.removed.values().next().value;
|
||||
if (removedIndex === undefined) {
|
||||
return;
|
||||
}
|
||||
const removedItem = (ev.currentTarget as HaListSelectable).items[
|
||||
removedIndex
|
||||
] as HaListItemOption & { type: string; value: string };
|
||||
|
||||
this.value = {
|
||||
...this.value,
|
||||
[removedItem.type]: this.value![removedItem.type].filter(
|
||||
(val) => val !== removedItem.value
|
||||
),
|
||||
[type]: [...(this.value[type] || []), value],
|
||||
};
|
||||
}
|
||||
|
||||
listItem.selected = this.value[type]?.includes(value);
|
||||
}
|
||||
|
||||
protected updated(changed: PropertyValues<this>) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this.renderRoot.querySelector("ha-list-selectable")!.style.height =
|
||||
this.renderRoot.querySelector("ha-list")!.style.height =
|
||||
`${this.clientHeight - 49}px`;
|
||||
}, 300);
|
||||
}
|
||||
@@ -330,7 +317,11 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
.floor::part(base) {
|
||||
ha-check-list-item {
|
||||
--mdc-list-item-graphic-margin: 16px;
|
||||
}
|
||||
.floor {
|
||||
padding-left: 48px;
|
||||
padding-inline-start: 48px;
|
||||
padding-inline-end: 16px;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,10 @@ export class HaFormfield extends FormfieldBase {
|
||||
input.checked = !input.checked;
|
||||
fireEvent(input, "change");
|
||||
break;
|
||||
case "HA-RADIO":
|
||||
input.checked = true;
|
||||
fireEvent(input, "change");
|
||||
break;
|
||||
default:
|
||||
input.click();
|
||||
break;
|
||||
|
||||
@@ -469,8 +469,6 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
--ha-bottom-sheet-padding: 0;
|
||||
--ha-bottom-sheet-surface-background: var(--card-background-color);
|
||||
--ha-bottom-sheet-border-radius: var(--ha-border-radius-2xl);
|
||||
--ha-bottom-sheet-content-padding: 0 var(--safe-area-inset-right)
|
||||
var(--safe-area-inset-bottom) var(--safe-area-inset-left);
|
||||
}
|
||||
|
||||
ha-picker-field.opened {
|
||||
|
||||
@@ -53,10 +53,7 @@ export class HaIconButton extends LitElement {
|
||||
.download=${this.download}
|
||||
>
|
||||
${this.path
|
||||
? html`<ha-svg-icon
|
||||
aria-hidden="true"
|
||||
.path=${this.path}
|
||||
></ha-svg-icon>`
|
||||
? html`<ha-svg-icon .path=${this.path}></ha-svg-icon>`
|
||||
: html`<span><slot></slot></span>`}
|
||||
</ha-button>
|
||||
`;
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/item/ha-list-item-button";
|
||||
import "../../../components/list/ha-list-nav";
|
||||
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon-next";
|
||||
import "./ha-md-list";
|
||||
import "./ha-md-list-item";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-config-navigation-list")
|
||||
class HaConfigNavigationList extends LitElement {
|
||||
@customElement("ha-navigation-list")
|
||||
class HaNavigationList extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
@@ -23,11 +24,16 @@ class HaConfigNavigationList extends LitElement {
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<ha-list-nav .ariaLabel=${this.label}>
|
||||
<ha-md-list
|
||||
innerRole="menu"
|
||||
itemRoles="menuitem"
|
||||
innerAriaLabel=${ifDefined(this.label)}
|
||||
>
|
||||
${this.pages.map((page) => {
|
||||
const externalApp = page.path.endsWith("#external-app-configuration");
|
||||
return html`
|
||||
<ha-list-item-button
|
||||
<ha-md-list-item
|
||||
.type=${externalApp ? "button" : "link"}
|
||||
.href=${externalApp ? undefined : page.path}
|
||||
@click=${externalApp ? this._handleExternalApp : undefined}
|
||||
>
|
||||
@@ -49,10 +55,10 @@ class HaConfigNavigationList extends LitElement {
|
||||
${!this.narrow
|
||||
? html`<ha-icon-next slot="end"></ha-icon-next>`
|
||||
: ""}
|
||||
</ha-list-item-button>
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
})}
|
||||
</ha-list-nav>
|
||||
</ha-md-list>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -77,11 +83,14 @@ class HaConfigNavigationList extends LitElement {
|
||||
.icon-background ha-svg-icon {
|
||||
color: #fff;
|
||||
}
|
||||
ha-md-list-item {
|
||||
font-size: var(--navigation-list-item-title-font-size);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-navigation-list": HaConfigNavigationList;
|
||||
"ha-navigation-list": HaNavigationList;
|
||||
}
|
||||
}
|
||||
22
src/components/ha-radio.ts
Normal file
22
src/components/ha-radio.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { RadioBase } from "@material/mwc-radio/mwc-radio-base";
|
||||
import { styles } from "@material/mwc-radio/mwc-radio.css";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-radio")
|
||||
export class HaRadio extends RadioBase {
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--mdc-theme-secondary: var(--primary-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-radio": HaRadio;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./radio/ha-radio-group";
|
||||
import type { HaRadioGroup } from "./radio/ha-radio-group";
|
||||
import "./radio/ha-radio-option";
|
||||
import "./ha-radio";
|
||||
import type { HaRadio } from "./ha-radio";
|
||||
|
||||
interface SelectBoxOptionImage {
|
||||
src: string;
|
||||
@@ -45,14 +44,9 @@ export class HaSelectBox extends LitElement {
|
||||
const columns = Math.min(maxColumns, this.options.length);
|
||||
|
||||
return html`
|
||||
<ha-radio-group
|
||||
class="list"
|
||||
style=${styleMap({ "--columns": columns })}
|
||||
.value=${this.value}
|
||||
@change=${this._radioChanged}
|
||||
>
|
||||
<div class="list" style=${styleMap({ "--columns": columns })}>
|
||||
${this.options.map((option) => this._renderOption(option))}
|
||||
</ha-radio-group>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -80,24 +74,20 @@ export class HaSelectBox extends LitElement {
|
||||
selected: selected,
|
||||
})}"
|
||||
?disabled=${disabled}
|
||||
@click=${this._labelClick}
|
||||
>
|
||||
<div class="content">
|
||||
<ha-radio-option
|
||||
aria-describedby=${ifDefined(
|
||||
option.description ? `desc-${option.value}` : undefined
|
||||
)}
|
||||
aria-labelledby=${`label-${option.value}`}
|
||||
<ha-radio
|
||||
.checked=${option.value === this.value}
|
||||
.value=${option.value}
|
||||
.disabled=${disabled}
|
||||
></ha-radio-option>
|
||||
@change=${this._radioChanged}
|
||||
@click=${stopPropagation}
|
||||
></ha-radio>
|
||||
<div class="text">
|
||||
<span id=${`label-${option.value}`} class="label"
|
||||
>${option.label}</span
|
||||
>
|
||||
<span class="label">${option.label}</span>
|
||||
${option.description
|
||||
? html`<span class="description" id="desc-${option.value}"
|
||||
>${option.description}</span
|
||||
>`
|
||||
? html`<span class="description">${option.description}</span>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
@@ -110,9 +100,14 @@ export class HaSelectBox extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _labelClick(ev) {
|
||||
ev.stopPropagation();
|
||||
ev.currentTarget.querySelector("ha-radio")?.click();
|
||||
}
|
||||
|
||||
private _radioChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const radio = ev.currentTarget as HaRadioGroup;
|
||||
const radio = ev.currentTarget as HaRadio;
|
||||
const value = radio.value;
|
||||
if (this.disabled || value === undefined || value === (this.value ?? "")) {
|
||||
return;
|
||||
@@ -123,7 +118,7 @@ export class HaSelectBox extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.list::part(form-control-input) {
|
||||
.list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--columns, 1), minmax(0, 1fr));
|
||||
gap: var(--ha-space-3);
|
||||
@@ -151,9 +146,8 @@ export class HaSelectBox extends LitElement {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.option .content ha-radio-option {
|
||||
--ha-radio-option-control-margin: 0;
|
||||
margin: 0;
|
||||
.option .content ha-radio {
|
||||
margin: -12px;
|
||||
flex: none;
|
||||
}
|
||||
.option .content .text {
|
||||
@@ -162,7 +156,6 @@ export class HaSelectBox extends LitElement {
|
||||
gap: var(--ha-space-1);
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
.option .content .text .label {
|
||||
color: var(--primary-text-color);
|
||||
|
||||
@@ -78,28 +78,22 @@ export class HaObjectSelector extends LitElement {
|
||||
};
|
||||
|
||||
private _renderItem(item: any, index: number) {
|
||||
const fields = this.selector.object!.fields!;
|
||||
const preferredLabel = this.selector.object!.label_field;
|
||||
const hasValidLabelField = preferredLabel && preferredLabel in fields;
|
||||
const labelField =
|
||||
this.selector.object!.label_field ||
|
||||
Object.keys(this.selector.object!.fields!)[0];
|
||||
|
||||
const label = hasValidLabelField
|
||||
? formatSelectorValue(
|
||||
this.hass,
|
||||
item[preferredLabel!],
|
||||
fields[preferredLabel!]?.selector
|
||||
)
|
||||
: Object.entries(fields)
|
||||
.map(([key, field]) =>
|
||||
formatSelectorValue(this.hass, item[key], field.selector)
|
||||
)
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
const labelSelector = this.selector.object!.fields![labelField].selector;
|
||||
|
||||
const label = labelSelector
|
||||
? formatSelectorValue(this.hass, item[labelField], labelSelector)
|
||||
: "";
|
||||
|
||||
let description = "";
|
||||
|
||||
const descriptionField = this.selector.object!.description_field;
|
||||
if (descriptionField && descriptionField in fields) {
|
||||
const descriptionSelector = fields[descriptionField]?.selector;
|
||||
if (descriptionField) {
|
||||
const descriptionSelector =
|
||||
this.selector.object!.fields![descriptionField].selector;
|
||||
|
||||
description = descriptionSelector
|
||||
? formatSelectorValue(
|
||||
|
||||
@@ -15,11 +15,10 @@ import "../ha-dropdown-item";
|
||||
import "../ha-formfield";
|
||||
import "../ha-generic-picker";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-radio";
|
||||
import "../ha-select";
|
||||
import "../ha-select-box";
|
||||
import "../ha-sortable";
|
||||
import "../radio/ha-radio-group";
|
||||
import "../radio/ha-radio-option";
|
||||
|
||||
@customElement("ha-selector-select")
|
||||
export class HaSelectSelector extends LitElement {
|
||||
@@ -109,23 +108,24 @@ export class HaSelectSelector extends LitElement {
|
||||
) {
|
||||
if (!this.selector.select?.multiple) {
|
||||
return html`
|
||||
<ha-radio-group
|
||||
.label=${this.label}
|
||||
.disabled=${this.disabled}
|
||||
.value=${this.value}
|
||||
@change=${this._radioChanged}
|
||||
>
|
||||
<div>
|
||||
${this.label}
|
||||
${options.map(
|
||||
(item: SelectOption) => html`
|
||||
<ha-radio-option
|
||||
.value=${item.value}
|
||||
.disabled=${!!item.disabled}
|
||||
<ha-formfield
|
||||
.label=${item.label}
|
||||
.disabled=${item.disabled || this.disabled}
|
||||
>
|
||||
${item.label}
|
||||
</ha-radio-option>
|
||||
<ha-radio
|
||||
.checked=${item.value === this.value}
|
||||
.value=${item.value}
|
||||
.disabled=${item.disabled || this.disabled}
|
||||
@change=${this._radioChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
`
|
||||
)}
|
||||
</ha-radio-group>
|
||||
</div>
|
||||
${this._renderHelper()}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -40,11 +40,11 @@ import { isMobileClient } from "../util/is_mobile";
|
||||
import "./animation/ha-fade-in";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-md-list";
|
||||
import "./ha-md-list-item";
|
||||
import "./ha-spinner";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-tooltip";
|
||||
import "./item/ha-list-item-button";
|
||||
import "./list/ha-list-nav";
|
||||
import "./user/ha-user-badge";
|
||||
|
||||
const SORT_VALUE_URL_PATHS = {
|
||||
@@ -353,12 +353,12 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
|
||||
private _renderAllPanels(selectedPanel: string) {
|
||||
const renderList = (content, cls: string, scrollable: boolean) =>
|
||||
html`<ha-list-nav
|
||||
html`<ha-md-list
|
||||
class=${classMap({
|
||||
"ha-scrollbar": scrollable,
|
||||
[cls]: true,
|
||||
})}
|
||||
>${content}</ha-list-nav
|
||||
>${content}</ha-md-list
|
||||
>`;
|
||||
|
||||
if (!this._panelOrder || !this._hiddenPanels) {
|
||||
@@ -430,8 +430,9 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
const iconPath = getPanelIconPath(panel);
|
||||
|
||||
return html`
|
||||
<ha-list-item-button
|
||||
<ha-md-list-item
|
||||
.href=${`/${urlPath}`}
|
||||
type="link"
|
||||
id="sidebar-panel-${urlPath}"
|
||||
class=${classMap({ selected: isSelected })}
|
||||
>
|
||||
@@ -439,7 +440,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
? html`<ha-svg-icon slot="start" .path=${iconPath}></ha-svg-icon>`
|
||||
: html`<ha-icon slot="start" .icon=${icon}></ha-icon>`}
|
||||
<span class="item-text" slot="headline">${title}</span>
|
||||
</ha-list-item-button>
|
||||
</ha-md-list-item>
|
||||
${!this.alwaysExpand && title
|
||||
? this._renderToolTip(`sidebar-panel-${urlPath}`, title)
|
||||
: nothing}
|
||||
@@ -456,8 +457,9 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
}
|
||||
const isSelected = selectedPanel === "config";
|
||||
return html`
|
||||
<ha-list-item-button
|
||||
<ha-md-list-item
|
||||
class="configuration ${classMap({ selected: isSelected })}"
|
||||
type="button"
|
||||
href="/config"
|
||||
id="sidebar-config"
|
||||
>
|
||||
@@ -479,7 +481,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
>
|
||||
`
|
||||
: nothing}
|
||||
</ha-list-item-button>
|
||||
</ha-md-list-item>
|
||||
${!this.alwaysExpand
|
||||
? this._renderToolTip(
|
||||
"sidebar-config",
|
||||
@@ -495,9 +497,10 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
: 0;
|
||||
|
||||
return html`
|
||||
<ha-list-item-button
|
||||
<ha-md-list-item
|
||||
class="notifications"
|
||||
@click=${this._handleShowNotificationDrawer}
|
||||
type="button"
|
||||
id="sidebar-notifications"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiBell}></ha-svg-icon>
|
||||
@@ -512,7 +515,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
${notificationCount > 0
|
||||
? html`<span class="badge" slot="end">${notificationCount}</span>`
|
||||
: nothing}
|
||||
</ha-list-item-button>
|
||||
</ha-md-list-item>
|
||||
${!this.alwaysExpand
|
||||
? this._renderToolTip(
|
||||
"sidebar-notifications",
|
||||
@@ -527,8 +530,9 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
const isSelected = selectedPanel === "profile";
|
||||
|
||||
return html`
|
||||
<ha-list-item-button
|
||||
<ha-md-list-item
|
||||
href="/profile"
|
||||
type="link"
|
||||
id="sidebar-profile"
|
||||
class=${classMap({
|
||||
user: true,
|
||||
@@ -544,7 +548,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
<span class="item-text" slot="headline"
|
||||
>${this.hass.user ? this.hass.user.name : ""}</span
|
||||
>
|
||||
</ha-list-item-button>
|
||||
</ha-md-list-item>
|
||||
${!this.alwaysExpand && this.hass.user
|
||||
? this._renderToolTip("sidebar-profile", this.hass.user.name)
|
||||
: nothing}
|
||||
@@ -556,15 +560,16 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-list-item-button
|
||||
<ha-md-list-item
|
||||
@click=${this._handleExternalAppConfiguration}
|
||||
type="button"
|
||||
id="sidebar-external-config"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiCellphoneCog}></ha-svg-icon>
|
||||
<span class="item-text" slot="headline"
|
||||
>${this.hass.localize("ui.sidebar.external_app_configuration")}</span
|
||||
>
|
||||
</ha-list-item-button>
|
||||
</ha-md-list-item>
|
||||
${!this.alwaysExpand
|
||||
? this._renderToolTip(
|
||||
"sidebar-external-config",
|
||||
@@ -713,10 +718,10 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
ha-list-nav {
|
||||
ha-md-list {
|
||||
overflow-x: hidden;
|
||||
background: none;
|
||||
margin-left: var(--safe-area-inset-left, 0px);
|
||||
margin-block: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
@@ -726,38 +731,42 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
ha-list-nav.before-spacer {
|
||||
ha-md-list.before-spacer {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
ha-list-nav.after-spacer {
|
||||
ha-md-list.after-spacer {
|
||||
padding-top: 0;
|
||||
min-height: fit-content;
|
||||
}
|
||||
|
||||
ha-list-item-button {
|
||||
ha-md-list-item {
|
||||
flex-shrink: 0;
|
||||
margin: 0 var(--ha-space-1) var(--ha-space-1);
|
||||
box-sizing: border-box;
|
||||
margin: var(--ha-space-1);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
--ha-row-item-min-height: var(--ha-space-10);
|
||||
--ha-row-item-padding-block: 0;
|
||||
--md-list-item-one-line-container-height: var(--ha-space-10);
|
||||
--md-list-item-top-space: 0;
|
||||
--md-list-item-bottom-space: 0;
|
||||
width: var(--ha-space-12);
|
||||
position: relative;
|
||||
--md-list-item-label-text-color: var(--sidebar-text-color);
|
||||
--md-list-item-leading-space: var(--ha-space-3);
|
||||
--md-list-item-trailing-space: var(--ha-space-3);
|
||||
--md-list-item-leading-icon-size: var(--ha-space-6);
|
||||
transition: width var(--ha-animation-duration-normal) ease;
|
||||
}
|
||||
ha-list-item-button::part(headline) {
|
||||
color: var(--sidebar-text-color);
|
||||
}
|
||||
:host([expanded]) ha-list-item-button {
|
||||
:host([expanded]) ha-md-list-item {
|
||||
width: 248px;
|
||||
}
|
||||
:host([narrow][expanded]) ha-list-item-button {
|
||||
:host([narrow][expanded]) ha-md-list-item {
|
||||
width: calc(240px - var(--safe-area-inset-left, 0px));
|
||||
}
|
||||
|
||||
ha-list-item-button.selected::part(headline) {
|
||||
color: var(--sidebar-selected-icon-color);
|
||||
ha-md-list-item.selected {
|
||||
--md-list-item-label-text-color: var(--sidebar-selected-icon-color);
|
||||
--md-ripple-hover-color: var(--sidebar-selected-icon-color);
|
||||
}
|
||||
ha-list-item-button.selected::before {
|
||||
ha-md-list-item.selected::before {
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -779,12 +788,12 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
color: var(--sidebar-icon-color);
|
||||
}
|
||||
|
||||
ha-list-item-button.selected ha-svg-icon[slot="start"],
|
||||
ha-list-item-button.selected ha-icon[slot="start"] {
|
||||
ha-md-list-item.selected ha-svg-icon[slot="start"],
|
||||
ha-md-list-item.selected ha-icon[slot="start"] {
|
||||
color: var(--sidebar-selected-icon-color);
|
||||
}
|
||||
|
||||
ha-list-item-button .item-text {
|
||||
ha-md-list-item .item-text {
|
||||
display: block;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
@@ -797,7 +806,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
max-width var(--ha-animation-duration-normal) ease,
|
||||
opacity var(--ha-animation-duration-normal) ease;
|
||||
}
|
||||
:host([expanded]) ha-list-item-button .item-text {
|
||||
:host([expanded]) ha-md-list-item .item-text {
|
||||
max-width: 100%;
|
||||
opacity: 1;
|
||||
transition-delay: 0ms, 80ms;
|
||||
@@ -839,17 +848,13 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
ha-user-badge {
|
||||
width: var(--ha-space-10);
|
||||
height: var(--ha-space-10);
|
||||
ha-md-list-item.user {
|
||||
--md-list-item-leading-icon-size: var(--ha-space-10);
|
||||
--md-list-item-leading-space: var(--ha-space-1);
|
||||
}
|
||||
|
||||
ha-list-item-button.user {
|
||||
--ha-row-item-padding-inline: var(--ha-space-2) var(--ha-space-3);
|
||||
}
|
||||
|
||||
ha-list-item-button.user.rtl {
|
||||
--ha-row-item-padding-inline: var(--ha-space-4) var(--ha-space-3);
|
||||
ha-md-list-item.user.rtl {
|
||||
--md-list-item-leading-space: var(--ha-space-3);
|
||||
}
|
||||
|
||||
ha-user-badge {
|
||||
@@ -869,8 +874,8 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.menu,
|
||||
ha-list-item-button,
|
||||
ha-list-item-button .item-text,
|
||||
ha-md-list-item,
|
||||
ha-md-list-item .item-text,
|
||||
.title {
|
||||
transition: 1ms;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { copyToClipboard } from "../common/util/copy-clipboard";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { showToast } from "../util/toast";
|
||||
import "./ha-alert";
|
||||
import "./ha-button";
|
||||
import "./ha-code-editor";
|
||||
import type { HaCodeEditor } from "./ha-code-editor";
|
||||
@@ -57,8 +58,15 @@ export class HaYamlEditor extends LitElement {
|
||||
@property({ attribute: "has-extra-actions", type: Boolean })
|
||||
public hasExtraActions = false;
|
||||
|
||||
@property({ attribute: "show-errors", type: Boolean })
|
||||
public showErrors = true;
|
||||
|
||||
@state() private _yaml = "";
|
||||
|
||||
@state() private _error = "";
|
||||
|
||||
@state() private _showingError = false;
|
||||
|
||||
@query("ha-code-editor") _codeEditor?: HaCodeEditor;
|
||||
|
||||
public setValue(value): void {
|
||||
@@ -118,14 +126,16 @@ export class HaYamlEditor extends LitElement {
|
||||
.disableFullscreen=${this.disableFullscreen}
|
||||
.inDialog=${this.inDialog}
|
||||
mode="yaml"
|
||||
lint
|
||||
autocomplete-entities
|
||||
autocomplete-icons
|
||||
.error=${this.isValid === false}
|
||||
@value-changed=${this._onChange}
|
||||
@editor-save=${this._onEditorSave}
|
||||
@blur=${this._onBlur}
|
||||
dir="ltr"
|
||||
></ha-code-editor>
|
||||
${this._showingError
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
${this.copyClipboard || this.hasExtraActions
|
||||
? html`
|
||||
<div class="card-actions">
|
||||
@@ -148,13 +158,9 @@ export class HaYamlEditor extends LitElement {
|
||||
private _onChange(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
this._yaml = ev.detail.value;
|
||||
let parsed: unknown;
|
||||
let parsed;
|
||||
let isValid = true;
|
||||
let errorMsg: string | undefined;
|
||||
let yamlError: {
|
||||
mark?: { position: number; line: number; column: number };
|
||||
message?: string;
|
||||
} | null = null;
|
||||
let errorMsg;
|
||||
|
||||
if (this._yaml) {
|
||||
try {
|
||||
@@ -162,13 +168,15 @@ export class HaYamlEditor extends LitElement {
|
||||
} catch (err: any) {
|
||||
// Invalid YAML
|
||||
isValid = false;
|
||||
yamlError = err;
|
||||
errorMsg = `${this.hass.localize("ui.components.yaml-editor.error", { reason: err.reason })}${err.mark ? ` (${this.hass.localize("ui.components.yaml-editor.error_location", { line: err.mark.line + 1, column: err.mark.column + 1 })})` : ""}`;
|
||||
}
|
||||
} else {
|
||||
parsed = {};
|
||||
}
|
||||
this._codeEditor?.setYamlError(yamlError);
|
||||
this._error = errorMsg ?? "";
|
||||
if (isValid) {
|
||||
this._showingError = false;
|
||||
}
|
||||
|
||||
this.value = parsed;
|
||||
this.isValid = isValid;
|
||||
@@ -180,23 +188,16 @@ export class HaYamlEditor extends LitElement {
|
||||
} as any);
|
||||
}
|
||||
|
||||
private _onBlur(): void {
|
||||
if (this.showErrors && this._error) {
|
||||
this._showingError = true;
|
||||
}
|
||||
}
|
||||
|
||||
get yaml() {
|
||||
return this._yaml;
|
||||
}
|
||||
|
||||
get codemirror() {
|
||||
return this._codeEditor?.codemirror;
|
||||
}
|
||||
|
||||
get hasComments(): boolean {
|
||||
return this._codeEditor?.hasComments ?? false;
|
||||
}
|
||||
|
||||
private _onEditorSave(ev: CustomEvent): void {
|
||||
fireEvent(this, "editor-save");
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
private async _copyYaml(): Promise<void> {
|
||||
if (this.yaml) {
|
||||
await copyToClipboard(this.yaml);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiMagnify } from "@mdi/js";
|
||||
import { css, html, type PropertyValues } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { internationalizationContext } from "../../data/context";
|
||||
import { HaInput } from "./ha-input";
|
||||
|
||||
/**
|
||||
@@ -15,6 +17,10 @@ import { HaInput } from "./ha-input";
|
||||
*/
|
||||
@customElement("ha-input-search")
|
||||
export class HaInputSearch extends HaInput {
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.withClear = true;
|
||||
@@ -29,7 +35,7 @@ export class HaInputSearch extends HaInput {
|
||||
!this.placeholder &&
|
||||
(!this.hasUpdated || changedProps.has("_i18n"))
|
||||
) {
|
||||
this.placeholder = this.i18n?.localize?.("ui.common.search") || "Search";
|
||||
this.placeholder = this._i18n.localize("ui.common.search");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,21 +2,19 @@ import "@home-assistant/webawesome/dist/components/animation/animation";
|
||||
import "@home-assistant/webawesome/dist/components/input/input";
|
||||
import type WaInput from "@home-assistant/webawesome/dist/components/input/input";
|
||||
import { HasSlotController } from "@home-assistant/webawesome/dist/internal/slot";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiClose, mdiEye, mdiEyeOff } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
type PropertyValues,
|
||||
type TemplateResult,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import { internationalizationContext } from "../../data/context";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-svg-icon";
|
||||
import "../ha-tooltip";
|
||||
@@ -127,10 +125,6 @@ export class HaInput extends WaInputMixin(LitElement) {
|
||||
@query("wa-input")
|
||||
private _input?: WaInput;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
protected i18n?: ContextType<typeof internationalizationContext>;
|
||||
|
||||
private readonly _hasSlotController = new HasSlotController(
|
||||
this,
|
||||
"label",
|
||||
@@ -239,22 +233,19 @@ export class HaInput extends WaInputMixin(LitElement) {
|
||||
${this.renderStartDefault()}
|
||||
</slot>
|
||||
<slot name="end" slot="end"> ${this.renderEndDefault()} </slot>
|
||||
<slot name="clear-button" slot="clear-button">
|
||||
<ha-icon-button
|
||||
@click=${this._handleClearClick}
|
||||
.label=${this.i18n?.localize?.("ui.components.input.clear") ||
|
||||
"Clear"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
<slot name="clear-icon" slot="clear-icon">
|
||||
<ha-icon-button .path=${mdiClose}></ha-icon-button>
|
||||
</slot>
|
||||
<slot name="password-toggle-button" slot="password-toggle-button">
|
||||
<slot name="show-password-icon" slot="show-password-icon">
|
||||
<ha-icon-button
|
||||
@keydown=${stopPropagation}
|
||||
@click=${this._handlePasswordToggle}
|
||||
.label=${this.i18n?.localize?.(
|
||||
`ui.components.input.${this.passwordVisible ? "hide_password" : "show_password"}`
|
||||
) || (this.passwordVisible ? "Hide password" : "Show password")}
|
||||
.path=${this.passwordVisible ? mdiEyeOff : mdiEye}
|
||||
.path=${mdiEye}
|
||||
></ha-icon-button>
|
||||
</slot>
|
||||
<slot name="hide-password-icon" slot="hide-password-icon">
|
||||
<ha-icon-button
|
||||
@keydown=${stopPropagation}
|
||||
.path=${mdiEyeOff}
|
||||
></ha-icon-button>
|
||||
</slot>
|
||||
<div
|
||||
@@ -302,14 +293,6 @@ export class HaInput extends WaInputMixin(LitElement) {
|
||||
}
|
||||
};
|
||||
|
||||
private _handleClearClick() {
|
||||
this._input?.clear();
|
||||
}
|
||||
|
||||
private _handlePasswordToggle() {
|
||||
this.passwordVisible = !this.passwordVisible;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
waInputStyles,
|
||||
css`
|
||||
@@ -431,12 +414,6 @@ export class HaInput extends WaInputMixin(LitElement) {
|
||||
color: var(--ha-color-text-secondary);
|
||||
}
|
||||
|
||||
ha-icon-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--ha-color-text-secondary);
|
||||
}
|
||||
|
||||
:host([appearance="outlined"]) wa-input.no-label {
|
||||
--ha-icon-button-size: 24px;
|
||||
--mdc-icon-size: 18px;
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../list/types";
|
||||
import { HaRowItem } from "./ha-row-item";
|
||||
|
||||
/**
|
||||
* @element ha-list-item-base
|
||||
* @extends {HaRowItem}
|
||||
*
|
||||
* @summary
|
||||
* Non-interactive list row (role `listitem`). Base class for
|
||||
* `ha-list-item-button`, `ha-list-item-option`.
|
||||
*
|
||||
* @cssprop --ha-list-item-focus-radius - Focus outline border-radius.
|
||||
* @cssprop --ha-list-item-focus-width - Focus outline width (steady state).
|
||||
* @cssprop --ha-list-item-focus-width-start - Focus outline width at the start of the focus-in animation.
|
||||
* @cssprop --ha-list-item-focus-offset - Focus outline offset.
|
||||
* @cssprop --ha-list-item-focus-background - Background color applied on keyboard focus.
|
||||
*
|
||||
* @attr {boolean} interactive - Opts the row into the parent list's roving tabindex. Interactive subclasses set this automatically.
|
||||
*/
|
||||
@customElement("ha-list-item-base")
|
||||
export class HaListItemBase extends HaRowItem {
|
||||
/**
|
||||
* Whether the item takes keyboard focus. Read by the parent list to decide
|
||||
* if it should be part of the roving-tabindex ring. Interactive subclasses
|
||||
* (`ha-list-item-button`, `-option`, `-todo`) override the default to `true`.
|
||||
* For the plain base row, set the `interactive` attribute to opt into focus
|
||||
* (useful for sortable rows where you need keyboard reorder but no click
|
||||
* action).
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true }) public interactive = false;
|
||||
|
||||
/** Host `role` attribute. Subclasses override. */
|
||||
protected readonly defaultRole: string = "listitem";
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (!this.hasAttribute("role")) {
|
||||
this.setAttribute("role", this.defaultRole);
|
||||
}
|
||||
fireEvent(this, "ha-list-item-register", { item: this });
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
fireEvent(this, "ha-list-item-unregister", { item: this });
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate the item (Enter/Space from the parent list). Default dispatches
|
||||
* a click on the host. Subclasses that wrap a native element (e.g. `<a>`)
|
||||
* override this to click the inner element so browser default actions
|
||||
* (like anchor navigation) fire.
|
||||
*/
|
||||
public activate(): void {
|
||||
this.click();
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = [
|
||||
HaRowItem.styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-list-item-focus-radius: var(--ha-border-radius-sm);
|
||||
--ha-list-item-focus-width: 2px;
|
||||
--ha-list-item-focus-width-start: var(--ha-space-2);
|
||||
--ha-list-item-focus-offset: -2px;
|
||||
--ha-list-item-focus-background: var(
|
||||
--ha-color-fill-neutral-quiet-hover
|
||||
);
|
||||
}
|
||||
:host(:focus) {
|
||||
outline: none;
|
||||
}
|
||||
.base {
|
||||
border-radius: var(--ha-list-item-focus-radius);
|
||||
outline: var(--ha-list-item-focus-width) solid transparent;
|
||||
outline-offset: var(--ha-list-item-focus-offset);
|
||||
transition:
|
||||
outline-color var(--ha-animation-duration-fast) ease-out,
|
||||
background-color var(--ha-animation-duration-fast) ease-out;
|
||||
}
|
||||
@keyframes ha-list-item-focus-in {
|
||||
from {
|
||||
outline-width: var(--ha-list-item-focus-width-start);
|
||||
outline-offset: calc(-1 * var(--ha-list-item-focus-width-start));
|
||||
}
|
||||
to {
|
||||
outline-width: var(--ha-list-item-focus-width);
|
||||
outline-offset: var(--ha-list-item-focus-offset);
|
||||
}
|
||||
}
|
||||
:host(:focus-visible) .base {
|
||||
outline-color: var(--ha-color-focus);
|
||||
background-color: var(--ha-list-item-focus-background);
|
||||
animation: ha-list-item-focus-in var(--ha-animation-duration-normal)
|
||||
ease-in;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-list-item-base": HaListItemBase;
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import "../ha-ripple";
|
||||
import { HaListItemBase } from "./ha-list-item-base";
|
||||
|
||||
/**
|
||||
* @element ha-list-item-button
|
||||
* @extends {HaListItemBase}
|
||||
*
|
||||
* @summary
|
||||
* Interactive list row. Renders an inner `<a>` when `href` is set, otherwise
|
||||
* a `<button>`. The full row is the hit target. When placed in a list using
|
||||
* roving tabindex, the host is the tab stop and the inner element carries
|
||||
* `tabindex="-1"`. For a non-interactive row, use `ha-list-item-base`.
|
||||
*
|
||||
* @csspart ripple - The ripple effect element.
|
||||
*
|
||||
* @attr {string} href - URL. When set, renders an `<a>` instead of a `<button>`.
|
||||
* @attr {string} target - Anchor `target` attribute (requires `href`).
|
||||
* @attr {string} rel - Anchor `rel` attribute (requires `href`).
|
||||
* @attr {string} download - Anchor `download` attribute (requires `href`).
|
||||
*/
|
||||
@customElement("ha-list-item-button")
|
||||
export class HaListItemButton extends HaListItemBase {
|
||||
public override interactive = true;
|
||||
|
||||
@property({ type: String }) public href?: string;
|
||||
|
||||
@property({ type: String }) public target?: string;
|
||||
|
||||
@property({ type: String }) public rel?: string;
|
||||
|
||||
@property({ type: String }) public download?: string;
|
||||
|
||||
public override activate(): void {
|
||||
this.renderRoot.querySelector<HTMLElement>("#item")?.click();
|
||||
}
|
||||
|
||||
protected _renderBase(inner: TemplateResult): TemplateResult {
|
||||
if (this.href !== undefined) {
|
||||
return html`<a
|
||||
part="base"
|
||||
class="base interactive"
|
||||
id="item"
|
||||
href=${ifDefined(this.disabled ? undefined : this.href)}
|
||||
target=${ifDefined(this.target)}
|
||||
rel=${ifDefined(this.rel)}
|
||||
download=${ifDefined(this.download)}
|
||||
tabindex="-1"
|
||||
aria-disabled=${this.disabled ? "true" : "false"}
|
||||
>
|
||||
${this._renderRipple()}${inner}
|
||||
</a>`;
|
||||
}
|
||||
return html`<button
|
||||
part="base"
|
||||
class="base interactive"
|
||||
id="item"
|
||||
type="button"
|
||||
?disabled=${this.disabled}
|
||||
tabindex="-1"
|
||||
>
|
||||
${this._renderRipple()}${inner}
|
||||
</button>`;
|
||||
}
|
||||
|
||||
private _renderRipple() {
|
||||
return html`<ha-ripple
|
||||
part="ripple"
|
||||
for="item"
|
||||
?disabled=${this.disabled}
|
||||
></ha-ripple>`;
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = [
|
||||
HaListItemBase.styles,
|
||||
css`
|
||||
:host {
|
||||
cursor: pointer;
|
||||
--ha-ripple-color: var(--primary-text-color);
|
||||
}
|
||||
:host([disabled]) {
|
||||
cursor: default;
|
||||
}
|
||||
.base.interactive {
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
text-align: inherit;
|
||||
text-decoration: none;
|
||||
appearance: none;
|
||||
cursor: inherit;
|
||||
}
|
||||
:host([disabled]) .base.interactive {
|
||||
color: var(--disabled-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-list-item-button": HaListItemButton;
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../ha-checkbox";
|
||||
import "../ha-ripple";
|
||||
import { HaListItemBase } from "./ha-list-item-base";
|
||||
|
||||
export type HaListItemOptionAppearance = "line" | "checkbox";
|
||||
|
||||
export type HaListItemOptionSelectionPosition = "start" | "end";
|
||||
|
||||
/**
|
||||
* @element ha-list-item-option
|
||||
* @extends {HaListItemBase}
|
||||
*
|
||||
* @summary
|
||||
* Selectable list row (role `option`). Selection state is driven by the parent
|
||||
* `ha-list-selectable`; reflects `aria-selected`. When `appearance="checkbox"`, renders
|
||||
* a decorative `<ha-checkbox>` (clicks on the row are handled by the listbox).
|
||||
*
|
||||
* @csspart checkbox - Wrapper around the `<ha-checkbox>` when `appearance="checkbox"`.
|
||||
* @csspart ripple - The ripple effect element.
|
||||
*
|
||||
* @cssprop --ha-list-item-selected-background - Background color when selected (`appearance="line"`).
|
||||
*
|
||||
* @attr {boolean} selected - Whether the option is selected. Set by the parent `ha-list-selectable`.
|
||||
* @attr {string} value - Value identifying the option.
|
||||
* @attr {("line"|"checkbox")} appearance - Visual style. "line" highlights the row; "checkbox" renders an `ha-checkbox`.
|
||||
* @attr {("start"|"end")} selection-position - Side the checkbox sits on when `appearance="checkbox"`.
|
||||
*/
|
||||
@customElement("ha-list-item-option")
|
||||
export class HaListItemOption extends HaListItemBase {
|
||||
@property({ type: Boolean, reflect: true }) public selected = false;
|
||||
|
||||
@property({ type: String }) public value?: string;
|
||||
|
||||
@property({ type: String, reflect: true })
|
||||
public appearance: HaListItemOptionAppearance = "line";
|
||||
|
||||
@property({ type: String, reflect: true, attribute: "selection-position" })
|
||||
public selectionPosition: HaListItemOptionSelectionPosition = "start";
|
||||
|
||||
protected override readonly defaultRole = "option";
|
||||
|
||||
public override interactive = true;
|
||||
|
||||
public update(changed: Map<string, unknown>) {
|
||||
super.update(changed);
|
||||
if (changed.has("selected")) {
|
||||
this.setAttribute("aria-selected", this.selected ? "true" : "false");
|
||||
}
|
||||
if (changed.has("disabled")) {
|
||||
this.setAttribute("aria-disabled", this.disabled ? "true" : "false");
|
||||
}
|
||||
}
|
||||
|
||||
protected _renderBase(inner: TemplateResult): TemplateResult {
|
||||
return html`<div part="base" class="base" id="item">
|
||||
${this._renderRipple()}${this.selectionPosition === "start"
|
||||
? this._renderCheckbox()
|
||||
: nothing}${inner}${this.selectionPosition === "end"
|
||||
? this._renderCheckbox()
|
||||
: nothing}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _renderRipple() {
|
||||
return html`<ha-ripple
|
||||
part="ripple"
|
||||
for="item"
|
||||
?disabled=${this.disabled}
|
||||
></ha-ripple>`;
|
||||
}
|
||||
|
||||
private _renderCheckbox() {
|
||||
if (this.appearance !== "checkbox") {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<div part="checkbox" class="checkbox" inert>
|
||||
<ha-checkbox
|
||||
.checked=${this.selected}
|
||||
.disabled=${this.disabled}
|
||||
></ha-checkbox>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = [
|
||||
HaListItemBase.styles,
|
||||
css`
|
||||
:host {
|
||||
cursor: pointer;
|
||||
--ha-ripple-color: var(--primary-text-color);
|
||||
--ha-list-item-selected-background: var(
|
||||
--ha-color-fill-primary-quiet-resting,
|
||||
rgba(var(--rgb-primary-color), 0.08)
|
||||
);
|
||||
}
|
||||
:host([disabled]) {
|
||||
cursor: default;
|
||||
}
|
||||
.base {
|
||||
cursor: inherit;
|
||||
}
|
||||
:host([appearance="line"][selected]:not([disabled])) .base,
|
||||
:host([appearance="line"][active]:not([disabled])) .base {
|
||||
background-color: var(--ha-list-item-selected-background);
|
||||
}
|
||||
:host([appearance="line"][selected]:not([disabled])) {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
pointer-events: none;
|
||||
}
|
||||
.checkbox ha-checkbox {
|
||||
pointer-events: none;
|
||||
}
|
||||
ha-checkbox::part(base) {
|
||||
gap: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-list-item-option": HaListItemOption;
|
||||
}
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
import { HasSlotController } from "@home-assistant/webawesome/dist/internal/slot";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
/**
|
||||
* @element ha-row-item
|
||||
* @extends {LitElement}
|
||||
*
|
||||
* @summary
|
||||
* Generic row layout primitive. Renders a horizontal row with optional
|
||||
* leading/trailing slots and a stacked middle column (headline +
|
||||
* supporting text). Role-agnostic; use `ha-list-item-base` and its
|
||||
* subclasses for list semantics.
|
||||
*
|
||||
* @slot start - Leading container (usually icon/avatar).
|
||||
* @slot end - Trailing container (usually meta/chevron).
|
||||
* @slot headline - Primary text (overrides the `headline` attribute).
|
||||
* @slot supporting-text - Secondary text (overrides the `supporting-text` attribute).
|
||||
* @slot content - Escape hatch: replaces the entire middle column (headline + supporting-text).
|
||||
*
|
||||
* @csspart base - The outer container.
|
||||
* @csspart start - The leading slot wrapper.
|
||||
* @csspart content - The middle column wrapper.
|
||||
* @csspart headline - The headline wrapper.
|
||||
* @csspart supporting-text - The supporting-text wrapper.
|
||||
* @csspart end - The trailing slot wrapper.
|
||||
*
|
||||
* @cssprop --ha-row-item-padding-block - Vertical padding inside the row.
|
||||
* @cssprop --ha-row-item-padding-inline - Horizontal padding inside the row.
|
||||
* @cssprop --ha-row-item-gap - Gap between start, content, and end.
|
||||
* @cssprop --ha-row-item-min-height - Minimum row height.
|
||||
*
|
||||
* @attr {string} headline - Primary text. Overridden by the `headline` slot.
|
||||
* @attr {string} supporting-text - Secondary text. Overridden by the `supporting-text` slot.
|
||||
* @attr {boolean} disabled - Dims the row and blocks pointer events.
|
||||
*/
|
||||
@customElement("ha-row-item")
|
||||
export class HaRowItem extends LitElement {
|
||||
@property({ type: String }) public headline?: string;
|
||||
|
||||
@property({ type: String, attribute: "supporting-text" })
|
||||
public supportingText?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
protected readonly _slotController = new HasSlotController(
|
||||
this,
|
||||
"start",
|
||||
"end",
|
||||
"headline",
|
||||
"supporting-text",
|
||||
"content"
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return this._renderBase(this._renderInner());
|
||||
}
|
||||
|
||||
protected _renderBase(inner: TemplateResult): TemplateResult {
|
||||
return html`<div part="base" class="base" id="item">${inner}</div>`;
|
||||
}
|
||||
|
||||
protected _renderInner(): TemplateResult {
|
||||
const hasContent = this._slotController.test("content");
|
||||
|
||||
return html`
|
||||
<div part="start" class="start">
|
||||
<slot name="start"></slot>
|
||||
</div>
|
||||
<div part="content" class="content">
|
||||
${hasContent
|
||||
? html`<slot name="content"></slot>`
|
||||
: this._renderDefaultContent()}
|
||||
</div>
|
||||
<div part="end" class="end">
|
||||
<slot name="end"></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected _renderDefaultContent(): TemplateResult {
|
||||
const hasHeadlineSlot = this._slotController.test("headline");
|
||||
const hasSupportingSlot = this._slotController.test("supporting-text");
|
||||
|
||||
const showHeadline = hasHeadlineSlot || this.headline !== undefined;
|
||||
const showSupporting =
|
||||
hasSupportingSlot || this.supportingText !== undefined;
|
||||
|
||||
return html`
|
||||
${showHeadline
|
||||
? html`<div part="headline" class="headline">
|
||||
<slot name="headline">${this.headline ?? nothing}</slot>
|
||||
</div>`
|
||||
: nothing}
|
||||
${showSupporting
|
||||
? html`<div part="supporting-text" class="supporting">
|
||||
<slot name="supporting-text"
|
||||
>${this.supportingText ?? nothing}</slot
|
||||
>
|
||||
</div>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
:host {
|
||||
display: block;
|
||||
color: var(--primary-text-color);
|
||||
font-size: var(--ha-font-size-m);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
--ha-row-item-padding-block: var(--ha-space-3);
|
||||
--ha-row-item-padding-inline: var(--ha-space-4);
|
||||
--ha-row-item-gap: var(--ha-space-4);
|
||||
--ha-row-item-min-height: 48px;
|
||||
}
|
||||
:host([disabled]) {
|
||||
color: var(--disabled-text-color);
|
||||
pointer-events: none;
|
||||
}
|
||||
.base {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--ha-row-item-gap);
|
||||
padding-block: var(--ha-row-item-padding-block);
|
||||
padding-inline: var(--ha-row-item-padding-inline);
|
||||
min-height: var(--ha-row-item-min-height);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.content {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
.start,
|
||||
.end {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
:host(:not(:has([slot="start"]))) .start,
|
||||
:host(:not(:has([slot="end"]))) .end {
|
||||
display: none;
|
||||
}
|
||||
.headline {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.supporting {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: var(--ha-font-size-s);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-row-item": HaRowItem;
|
||||
}
|
||||
}
|
||||
@@ -1,313 +0,0 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { tinykeys } from "tinykeys";
|
||||
import { compareNodeOrder } from "../../common/dom/compare-node-order";
|
||||
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import type { HaListItemBase } from "../item/ha-list-item-base";
|
||||
import "./types";
|
||||
import type { HaListItemRegistrationDetail } from "./types";
|
||||
|
||||
/**
|
||||
* @element ha-list-base
|
||||
* @extends {LitElement}
|
||||
*
|
||||
* @summary
|
||||
* Base list container with roving-tabindex keyboard navigation (ArrowUp/Down,
|
||||
* Home/End, optional Enter/Space activation, optional wrap-focus). Tracks
|
||||
* `HaListItemBase` descendants via the `ha-list-item-register` /
|
||||
* `ha-list-item-unregister` events they fire on connect/disconnect — works
|
||||
* across any nesting depth and shadow boundaries. Subclasses override
|
||||
* `hostRole` and/or `render()` to specialize.
|
||||
*
|
||||
* @slot - List items (`<ha-list-item-*>`).
|
||||
*
|
||||
* @csspart base - The outer container (`<div role="list">`).
|
||||
*
|
||||
* @cssprop --ha-list-gap - Spacing between items. Defaults to `0`.
|
||||
* @cssprop --ha-list-padding - Padding around the list content. Defaults to `0`.
|
||||
*
|
||||
* @attr {boolean} wrap-focus - Whether ArrowUp/Down navigation wraps at the ends.
|
||||
* @attr {string} aria-label - Accessible label for the list.
|
||||
*
|
||||
* @fires ha-list-activated - Fired when an item is activated via Enter/Space. `detail: { index, item }`.
|
||||
*/
|
||||
@customElement("ha-list-base")
|
||||
export class HaListBase extends LitElement {
|
||||
@property({ type: Boolean, attribute: "wrap-focus" })
|
||||
public wrapFocus = false;
|
||||
|
||||
@property({ type: String, attribute: "aria-label", reflect: true })
|
||||
public ariaLabel: string | null = null;
|
||||
|
||||
public items: readonly HaListItemBase[] = [];
|
||||
|
||||
/** Host `role` attribute. Empty string means no role is set. */
|
||||
protected readonly hostRole: string = "list";
|
||||
|
||||
private _activeItemIndex = -1;
|
||||
|
||||
private _firstFocusableIndex = -1;
|
||||
|
||||
private _lastFocusableIndex = -1;
|
||||
|
||||
private _hasFocusableItem = false;
|
||||
|
||||
private _unbindKeys?: () => void;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (!this.hasAttribute("ha-list")) {
|
||||
this.setAttribute("ha-list", "");
|
||||
}
|
||||
if (!this.hasAttribute("role") && this.hostRole) {
|
||||
this.setAttribute("role", this.hostRole);
|
||||
}
|
||||
this._unbindKeys = tinykeys(this, {
|
||||
ArrowDown: this._onForward,
|
||||
ArrowUp: this._onBack,
|
||||
Home: this._onHome,
|
||||
End: this._onEnd,
|
||||
Enter: this._onActivate,
|
||||
Space: this._onActivate,
|
||||
});
|
||||
this.addEventListener("focusin", this._onFocusIn);
|
||||
this.addEventListener(
|
||||
"ha-list-item-register",
|
||||
this._onItemRegister as EventListener
|
||||
);
|
||||
this.addEventListener(
|
||||
"ha-list-item-unregister",
|
||||
this._onItemUnregister as EventListener
|
||||
);
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._unbindKeys?.();
|
||||
this._unbindKeys = undefined;
|
||||
this.removeEventListener("focusin", this._onFocusIn);
|
||||
this.removeEventListener(
|
||||
"ha-list-item-register",
|
||||
this._onItemRegister as EventListener
|
||||
);
|
||||
this.removeEventListener(
|
||||
"ha-list-item-unregister",
|
||||
this._onItemUnregister as EventListener
|
||||
);
|
||||
}
|
||||
|
||||
public focus(options?: FocusOptions) {
|
||||
if (!this.items.length) {
|
||||
super.focus(options);
|
||||
return;
|
||||
}
|
||||
this.focusItemAtIndex(
|
||||
this._activeItemIndex >= 0 ? this._activeItemIndex : 0
|
||||
);
|
||||
}
|
||||
|
||||
public focusItemAtIndex(index: number) {
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
this.setActiveItemIndex(index, true);
|
||||
}
|
||||
|
||||
public getActiveItemIndex(): number {
|
||||
return this._activeItemIndex;
|
||||
}
|
||||
|
||||
public setActiveItemIndex(index: number, focusItem = false) {
|
||||
if (!this._hasFocusableItem) {
|
||||
this._activeItemIndex = -1;
|
||||
return;
|
||||
}
|
||||
this._activeItemIndex = Math.max(0, Math.min(this.items.length - 1, index));
|
||||
if (!this._isFocusable(this._activeItemIndex)) {
|
||||
this._activeItemIndex = this._firstFocusableIndex;
|
||||
}
|
||||
this._applyActive(focusItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook called whenever the items array has changed. Subclasses can override
|
||||
* to layer in extra bookkeeping (e.g. selection state sync).
|
||||
*/
|
||||
public updateListItems() {
|
||||
this._recomputeFocusableIndexes();
|
||||
if (
|
||||
this._activeItemIndex >= this.items.length ||
|
||||
!this._hasFocusableItem ||
|
||||
this._activeItemIndex < 0
|
||||
) {
|
||||
this._activeItemIndex = this._firstFocusableIndex;
|
||||
}
|
||||
this._applyActive(false);
|
||||
}
|
||||
|
||||
private _onItemRegister = (
|
||||
ev: HASSDomEvent<HaListItemRegistrationDetail>
|
||||
) => {
|
||||
ev.stopPropagation();
|
||||
const item = ev.detail.item;
|
||||
if (this.items.includes(item)) {
|
||||
return;
|
||||
}
|
||||
const next = [...this.items, item];
|
||||
next.sort(compareNodeOrder);
|
||||
this.items = next;
|
||||
this.updateListItems();
|
||||
};
|
||||
|
||||
private _onItemUnregister = (
|
||||
ev: HASSDomEvent<HaListItemRegistrationDetail>
|
||||
) => {
|
||||
ev.stopPropagation();
|
||||
const item = ev.detail.item;
|
||||
if (!this.items.includes(item)) {
|
||||
return;
|
||||
}
|
||||
this.items = this.items.filter((it) => it !== item);
|
||||
this.updateListItems();
|
||||
};
|
||||
|
||||
private _recomputeFocusableIndexes() {
|
||||
let first = -1;
|
||||
let last = -1;
|
||||
for (let i = 0; i < this.items.length; i++) {
|
||||
if (this._isFocusable(i)) {
|
||||
if (first === -1) {
|
||||
first = i;
|
||||
}
|
||||
last = i;
|
||||
}
|
||||
}
|
||||
this._firstFocusableIndex = first;
|
||||
this._lastFocusableIndex = last;
|
||||
this._hasFocusableItem = first !== -1;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<div part="base" class="base">
|
||||
<slot></slot>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _isFocusable(index: number): boolean {
|
||||
const item = this.items[index];
|
||||
return !!item && item.interactive && !item.disabled;
|
||||
}
|
||||
|
||||
private _applyActive(focusItem: boolean) {
|
||||
this.items.forEach((item, i) => {
|
||||
if (!item.interactive || item.disabled) {
|
||||
item.removeAttribute("tabindex");
|
||||
return;
|
||||
}
|
||||
item.tabIndex = i === this._activeItemIndex ? 0 : -1;
|
||||
});
|
||||
if (focusItem && this._activeItemIndex >= 0) {
|
||||
this.items[this._activeItemIndex]?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private _onFocusIn = (ev: FocusEvent) => {
|
||||
const path = ev.composedPath();
|
||||
for (let i = 0; i < this.items.length; i++) {
|
||||
if (path.includes(this.items[i])) {
|
||||
if (i !== this._activeItemIndex) {
|
||||
this._activeItemIndex = i;
|
||||
this._applyActive(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private _onForward = (ev: KeyboardEvent) => {
|
||||
this._moveFocus(ev, this._stepIndex(this._activeItemIndex, 1));
|
||||
};
|
||||
|
||||
private _onBack = (ev: KeyboardEvent) => {
|
||||
this._moveFocus(ev, this._stepIndex(this._activeItemIndex, -1));
|
||||
};
|
||||
|
||||
private _onHome = (ev: KeyboardEvent) => {
|
||||
this._moveFocus(ev, this._firstFocusableIndex);
|
||||
};
|
||||
|
||||
private _onEnd = (ev: KeyboardEvent) => {
|
||||
this._moveFocus(ev, this._lastFocusableIndex);
|
||||
};
|
||||
|
||||
private _onActivate = (ev: KeyboardEvent) => {
|
||||
if (!this._isFocusable(this._activeItemIndex)) {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
const active = this.items[this._activeItemIndex];
|
||||
active.activate();
|
||||
fireEvent(this, "ha-list-activated", {
|
||||
index: this._activeItemIndex,
|
||||
item: active,
|
||||
});
|
||||
};
|
||||
|
||||
private _moveFocus(ev: KeyboardEvent, next: number) {
|
||||
if (!this._hasFocusableItem || next < 0 || next === this._activeItemIndex) {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
this._activeItemIndex = next;
|
||||
this._applyActive(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Step from `from` by `delta`, skipping non-interactive and disabled items.
|
||||
* Returns `from` when no other focusable item can be reached (honouring
|
||||
* `wrapFocus`).
|
||||
*/
|
||||
private _stepIndex(from: number, delta: 1 | -1): number {
|
||||
const n = this.items.length;
|
||||
if (!n || !this._hasFocusableItem) {
|
||||
return from;
|
||||
}
|
||||
let i = from;
|
||||
for (let step = 0; step < n; step++) {
|
||||
i += delta;
|
||||
if (i < 0 || i >= n) {
|
||||
if (!this.wrapFocus) {
|
||||
return from;
|
||||
}
|
||||
i = (i + n) % n;
|
||||
}
|
||||
if (this._isFocusable(i)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return from;
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
:host {
|
||||
display: block;
|
||||
--ha-list-gap: 0;
|
||||
--ha-list-padding: 0;
|
||||
}
|
||||
.base {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-list-gap);
|
||||
padding: var(--ha-list-padding);
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-list-base": HaListBase;
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { HaListBase } from "./ha-list-base";
|
||||
|
||||
/**
|
||||
* @element ha-list-nav
|
||||
* @extends {HaListBase}
|
||||
*
|
||||
* @summary
|
||||
* Navigation list. Wraps the list in a `<nav>` landmark. Items should be
|
||||
* `<ha-list-item-button>` with an `href`. Use `aria-label` to name the landmark.
|
||||
*
|
||||
* @csspart nav - The `<nav>` wrapper.
|
||||
* @csspart base - The inner `<div role="list">`.
|
||||
*/
|
||||
@customElement("ha-list-nav")
|
||||
export class HaListNav extends HaListBase {
|
||||
// No host role — the inner <nav> carries the landmark semantics, and the
|
||||
// inner <div role="list"> preserves the list semantics for screen readers.
|
||||
protected override readonly hostRole = "";
|
||||
|
||||
// The label is forwarded to the inner <nav>
|
||||
@property({ type: String, attribute: "aria-label", reflect: false })
|
||||
public override ariaLabel: string | null = null;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<nav
|
||||
part="nav"
|
||||
aria-label=${ifDefined(this.ariaLabel ?? undefined)}
|
||||
>
|
||||
<div part="base" class="base" role="list">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</nav>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-list-nav": HaListNav;
|
||||
}
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { HaListItemOption } from "../item/ha-list-item-option";
|
||||
import { HaListBase } from "./ha-list-base";
|
||||
import type { HaListSelectedDetail } from "./types";
|
||||
|
||||
/**
|
||||
* @element ha-list-selectable
|
||||
* @extends {HaListBase}
|
||||
*
|
||||
* @summary
|
||||
* Selection list (role `listbox`). Items must be `<ha-list-item-option>`.
|
||||
* Toggle single vs multi selection via the `multi` attribute.
|
||||
*
|
||||
* @attr {boolean} multi - Whether multiple options can be selected at once.
|
||||
*
|
||||
* @fires ha-list-selected - Fired when the selection changes. `detail: HaListSelectedDetail`.
|
||||
*/
|
||||
@customElement("ha-list-selectable")
|
||||
export class HaListSelectable extends HaListBase {
|
||||
@property({ type: Boolean, reflect: true }) public multi = false;
|
||||
|
||||
protected override readonly hostRole = "listbox";
|
||||
|
||||
private _selectedIndices?: Set<number>;
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("click", this._onOptionClick);
|
||||
this.setAttribute("aria-multiselectable", this.multi ? "true" : "false");
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("click", this._onOptionClick);
|
||||
}
|
||||
|
||||
public updated(changed: Map<string, unknown>) {
|
||||
super.updated(changed);
|
||||
if (changed.has("multi")) {
|
||||
this.setAttribute("aria-multiselectable", this.multi ? "true" : "false");
|
||||
if (!this.multi && (this._selectedIndices?.size ?? 0) > 1) {
|
||||
const first = Math.min(...this._selectedIndices!);
|
||||
this._setSelection(new Set([first]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current selection. `number` (or `-1` if nothing) when single,
|
||||
* `Set<number>` when multi.
|
||||
*/
|
||||
public get selected(): number | Set<number> {
|
||||
if (this.multi) {
|
||||
return new Set(this._selectedIndices);
|
||||
}
|
||||
return (this._selectedIndices?.size ?? 0) === 0
|
||||
? -1
|
||||
: this._selectedIndices!.values().next().value!;
|
||||
}
|
||||
|
||||
public get selectedItems(): HaListItemOption[] {
|
||||
return this._sortedSelectedIndices()
|
||||
.map((i) => this.items[i] as HaListItemOption | undefined)
|
||||
.filter((it): it is HaListItemOption => !!it);
|
||||
}
|
||||
|
||||
/** Replace the entire selection. */
|
||||
public setSelected(indices: number | number[] | Set<number>): void {
|
||||
const next =
|
||||
typeof indices === "number"
|
||||
? indices < 0
|
||||
? new Set<number>()
|
||||
: new Set([indices])
|
||||
: new Set(indices);
|
||||
if (!this.multi && next.size > 1) {
|
||||
const first = Math.min(...next);
|
||||
this._setSelection(new Set([first]));
|
||||
return;
|
||||
}
|
||||
this._setSelection(next);
|
||||
}
|
||||
|
||||
public select(index: number): void {
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
if (this.multi) {
|
||||
const next = new Set(this._selectedIndices);
|
||||
next.add(index);
|
||||
this._setSelection(next);
|
||||
} else {
|
||||
this._setSelection(new Set([index]));
|
||||
}
|
||||
}
|
||||
|
||||
public toggle(index: number, force?: boolean): void {
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
if (this.multi) {
|
||||
const next = new Set(this._selectedIndices);
|
||||
const isSelected = next.has(index);
|
||||
const shouldSelect = force !== undefined ? force : !isSelected;
|
||||
if (shouldSelect) {
|
||||
next.add(index);
|
||||
} else {
|
||||
next.delete(index);
|
||||
}
|
||||
this._setSelection(next);
|
||||
} else {
|
||||
const isSelected = this._selectedIndices!.has(index);
|
||||
const shouldSelect = force !== undefined ? force : !isSelected;
|
||||
this._setSelection(shouldSelect ? new Set([index]) : new Set());
|
||||
}
|
||||
}
|
||||
|
||||
public clearSelection(): void {
|
||||
this._setSelection(new Set());
|
||||
}
|
||||
|
||||
public updateListItems() {
|
||||
super.updateListItems();
|
||||
this._syncItemSelectedState();
|
||||
}
|
||||
|
||||
private _sortedSelectedIndices(): number[] {
|
||||
return [...this._selectedIndices!].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
private _syncItemSelectedState() {
|
||||
if (!this._selectedIndices) {
|
||||
this._selectedIndices = new Set<number>();
|
||||
this.items.forEach((item, i) => {
|
||||
const opt = item as HaListItemOption;
|
||||
if (opt.selected) {
|
||||
this._selectedIndices!.add(i);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.items.forEach((item, i) => {
|
||||
const opt = item as HaListItemOption;
|
||||
const shouldBe = this._selectedIndices!.has(i);
|
||||
if (opt.selected !== shouldBe) {
|
||||
opt.selected = shouldBe;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _setSelection(next: Set<number>): void {
|
||||
const prev = this._selectedIndices!;
|
||||
const added = new Set<number>();
|
||||
const removed = new Set<number>();
|
||||
next.forEach((i) => {
|
||||
if (!prev.has(i)) {
|
||||
added.add(i);
|
||||
}
|
||||
});
|
||||
prev.forEach((i) => {
|
||||
if (!next.has(i)) {
|
||||
removed.add(i);
|
||||
}
|
||||
});
|
||||
if (!added.size && !removed.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._selectedIndices = next;
|
||||
this._syncItemSelectedState();
|
||||
|
||||
const detail: HaListSelectedDetail = this.multi
|
||||
? { index: new Set(next), diff: { added, removed } }
|
||||
: {
|
||||
index: next.size === 0 ? -1 : next.values().next().value!,
|
||||
diff: { added, removed },
|
||||
};
|
||||
fireEvent(this, "ha-list-selected", detail);
|
||||
}
|
||||
|
||||
private _onOptionClick = (ev: Event) => {
|
||||
const path = ev.composedPath();
|
||||
for (const el of path) {
|
||||
if (el === this) {
|
||||
return;
|
||||
}
|
||||
if (el instanceof HaListItemOption) {
|
||||
const index = this.items.indexOf(el);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
const item = this.items[index];
|
||||
if (item.disabled) {
|
||||
return;
|
||||
}
|
||||
if (this.multi) {
|
||||
this.toggle(index);
|
||||
} else {
|
||||
this.select(index);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-list-selectable": HaListSelectable;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import type { HaListItemBase } from "../item/ha-list-item-base";
|
||||
|
||||
export interface HaListSelectedDetail {
|
||||
index: number | Set<number>;
|
||||
diff?: { added: Set<number>; removed: Set<number> };
|
||||
value?: string | string[];
|
||||
}
|
||||
|
||||
export interface HaListActivatedDetail {
|
||||
index: number;
|
||||
item: HaListItemBase;
|
||||
}
|
||||
|
||||
export interface HaListItemRegistrationDetail {
|
||||
item: HaListItemBase;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"ha-list-selected": HaListSelectedDetail;
|
||||
"ha-list-activated": HaListActivatedDetail;
|
||||
"ha-list-item-register": HaListItemRegistrationDetail;
|
||||
"ha-list-item-unregister": HaListItemRegistrationDetail;
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import RadioGroup from "@home-assistant/webawesome/dist/components/radio-group/radio-group";
|
||||
import { css, type CSSResultGroup } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
/**
|
||||
* Home Assistant radio group component
|
||||
*
|
||||
* @element ha-radio-group
|
||||
* @extends {RadioGroup}
|
||||
*
|
||||
* @summary
|
||||
* A Home Assistant themed radio group built on top of the Web Awesome radio group.
|
||||
* Groups `ha-radio-option` children so they behave as a single form control.
|
||||
*
|
||||
* @slot - The default slot where `ha-radio-option` elements are placed.
|
||||
* @slot label - The radio group's label. Required for accessibility. Alternatively, use the `label` attribute.
|
||||
* @slot hint - Text that describes how to use the radio group. Alternatively, use the `hint` attribute.
|
||||
*
|
||||
* @csspart form-control - The form control that wraps the label, input, and hint.
|
||||
* @csspart form-control-label - The label's wrapper.
|
||||
* @csspart form-control-input - The input's wrapper.
|
||||
* @csspart radios - The wrapper around the radio items, styled as a flex container by default.
|
||||
* @csspart hint - The hint's wrapper.
|
||||
*
|
||||
* @cssprop --ha-radio-group-required-marker - Marker shown after the label for required fields. Defaults to `--ha-input-required-marker`, then `"*"`.
|
||||
* @cssprop --ha-radio-group-required-marker-offset - Offset of the required marker. Defaults to `0.1rem`.
|
||||
*
|
||||
* @attr {string} label - The radio group's label.
|
||||
* @attr {string} hint - The radio group's hint text.
|
||||
* @attr {string} name - The name of the radio group, submitted as a name/value pair with form data.
|
||||
* @attr {("horizontal"|"vertical")} orientation - The orientation in which to show radio items.
|
||||
* @attr {boolean} disabled - Disables the radio group and all child radios.
|
||||
* @attr {boolean} required - Ensures a child radio is checked before allowing the containing form to submit.
|
||||
*
|
||||
* @fires change - Emitted when the radio group's selected value changes.
|
||||
* @fires input - Emitted when the radio group receives user input.
|
||||
* @fires wa-invalid - Emitted when the form control has been checked for validity and its constraints aren't satisfied.
|
||||
*/
|
||||
@customElement("ha-radio-group")
|
||||
export class HaRadioGroup extends RadioGroup {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.radioTag = "ha-radio-option";
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
RadioGroup.styles,
|
||||
css`
|
||||
:host {
|
||||
--wa-form-control-required-content: var(
|
||||
--ha-radio-group-required-marker,
|
||||
var(--ha-input-required-marker, "*")
|
||||
);
|
||||
--wa-form-control-required-content-offset: var(
|
||||
--ha-radio-group-required-marker-offset,
|
||||
0.1rem
|
||||
);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-radio-group": HaRadioGroup;
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
import Radio from "@home-assistant/webawesome/dist/components/radio/radio";
|
||||
import { css, type CSSResultGroup } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
/**
|
||||
* Home Assistant radio option component
|
||||
*
|
||||
* @element ha-radio-option
|
||||
* @extends {Radio}
|
||||
*
|
||||
* @summary
|
||||
* A Home Assistant themed radio built on top of the Web Awesome radio.
|
||||
* Intended to be used as a child of `ha-radio-group`.
|
||||
*
|
||||
* @slot - The radio option's label.
|
||||
*
|
||||
* @csspart control - The circular container that wraps the radio's checked state.
|
||||
* @csspart checked-icon - The checked icon.
|
||||
* @csspart label - The container that wraps the radio option's label.
|
||||
*
|
||||
* @cssprop --ha-radio-option-active-color - Accent color used for the checked indicator and border. Defaults to `--ha-color-fill-primary-loud-resting`.
|
||||
* @cssprop --ha-radio-option-height - Minimum height of the option in `button` appearance. Defaults to `40px`.
|
||||
* @cssprop --ha-radio-option-toggle-size - Size of the radio toggle circle in `default` appearance. Defaults to `20px`.
|
||||
* @cssprop --ha-radio-option-border-width - Border width of the radio control. Defaults to `--ha-border-width-md`.
|
||||
* @cssprop --ha-radio-option-border-color - Border color of the radio control. Defaults to `--ha-color-border-neutral-normal`.
|
||||
* @cssprop --ha-radio-option-border-color-hover - Border color of the radio control on hover. Defaults to `--ha-radio-option-border-color`, then `--ha-color-border-neutral-loud`.
|
||||
* @cssprop --ha-radio-option-background-color - Background color of the radio control. Defaults to `--wa-form-control-background-color`.
|
||||
* @cssprop --ha-radio-option-background-color-hover - Background color of the radio control on hover. Defaults to `--ha-color-fill-neutral-quiet-hover`.
|
||||
* @cssprop --ha-radio-option-checked-background-color - Background color of the radio control when checked. Defaults to `--ha-color-fill-primary-normal-resting`.
|
||||
* @cssprop --ha-radio-option-checked-icon-color - Color of the checked indicator dot. Defaults to `--ha-radio-option-active-color`.
|
||||
* @cssprop --ha-radio-option-checked-icon-scale - Size of the checked indicator relative to the toggle. Defaults to `0.7`.
|
||||
* @cssprop --ha-radio-option-control-margin - Margin around the radio toggle in `default` appearance. Defaults to `var(--ha-space-3) var(--ha-space-2) var(--ha-space-3) var(--ha-space-3)`.
|
||||
*
|
||||
* @attr {("default"|"button")} appearance - Sets the radio option's visual appearance.
|
||||
* @attr {("small"|"medium"|"large")} size - Sets the radio option's size. Overridden by the parent `ha-radio-group`.
|
||||
* @attr {boolean} checked - Draws the radio option in a checked state.
|
||||
* @attr {boolean} disabled - Disables the radio option.
|
||||
*/
|
||||
@customElement("ha-radio-option")
|
||||
export class HaRadioOption extends Radio {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
Radio.styles,
|
||||
css`
|
||||
:host {
|
||||
--wa-form-control-activated-color: var(
|
||||
--ha-radio-option-active-color,
|
||||
var(--ha-color-fill-primary-loud-resting)
|
||||
);
|
||||
--wa-form-control-height: var(--ha-radio-option-height, 40px);
|
||||
--wa-form-control-toggle-size: var(
|
||||
--ha-radio-option-toggle-size,
|
||||
20px
|
||||
);
|
||||
--wa-form-control-border-width: var(
|
||||
--ha-radio-option-border-width,
|
||||
var(--ha-border-width-md)
|
||||
);
|
||||
--wa-form-control-border-color: var(
|
||||
--ha-radio-option-border-color,
|
||||
var(--ha-color-border-neutral-normal)
|
||||
);
|
||||
--wa-form-control-background-color: var(
|
||||
--ha-radio-option-background-color,
|
||||
var(--wa-form-control-background-color)
|
||||
);
|
||||
--checked-icon-color: var(
|
||||
--ha-radio-option-checked-icon-color,
|
||||
var(--wa-form-control-activated-color)
|
||||
);
|
||||
--checked-icon-scale: var(--ha-radio-option-checked-icon-scale, 0.7);
|
||||
}
|
||||
|
||||
:host([appearance="default"]) .control {
|
||||
margin: var(
|
||||
--ha-radio-option-control-margin,
|
||||
var(--ha-space-3) var(--ha-space-2) var(--ha-space-3)
|
||||
var(--ha-space-3)
|
||||
);
|
||||
}
|
||||
|
||||
:host(:not([aria-checked="true"], [aria-disabled="true"]):hover)
|
||||
.control {
|
||||
border-color: var(
|
||||
--ha-radio-option-border-color-hover,
|
||||
var(
|
||||
--ha-radio-option-border-color,
|
||||
var(--ha-color-border-neutral-loud)
|
||||
)
|
||||
);
|
||||
background-color: var(
|
||||
--ha-radio-option-background-color-hover,
|
||||
var(--ha-color-fill-neutral-quiet-hover)
|
||||
);
|
||||
}
|
||||
|
||||
:host([aria-checked="true"]) .control {
|
||||
background-color: var(
|
||||
--ha-radio-option-checked-background-color,
|
||||
var(--ha-color-fill-primary-normal-resting)
|
||||
);
|
||||
}
|
||||
|
||||
[part~="label"] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:host([disabled]) [part~="label"] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-radio-option": HaRadioOption;
|
||||
}
|
||||
}
|
||||
@@ -189,20 +189,6 @@ export const updateBackupConfig = (
|
||||
config: BackupMutableConfig
|
||||
) => hass.callWS({ type: "backup/config/update", ...config });
|
||||
|
||||
export const saveBackupConfig = (hass: HomeAssistant, config: BackupConfig) =>
|
||||
updateBackupConfig(hass, {
|
||||
create_backup: {
|
||||
agent_ids: config.create_backup.agent_ids,
|
||||
include_folders: config.create_backup.include_folders ?? [],
|
||||
include_database: config.create_backup.include_database,
|
||||
include_addons: config.create_backup.include_addons ?? [],
|
||||
include_all_addons: config.create_backup.include_all_addons,
|
||||
password: config.create_backup.password,
|
||||
},
|
||||
retention: config.retention,
|
||||
schedule: config.schedule,
|
||||
});
|
||||
|
||||
export const getBackupDownloadUrl = (
|
||||
id: string,
|
||||
agentId: string,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export interface ValidConfig {
|
||||
interface ValidConfig {
|
||||
valid: true;
|
||||
error: null;
|
||||
}
|
||||
|
||||
export interface InvalidConfig {
|
||||
interface InvalidConfig {
|
||||
valid: false;
|
||||
error: string;
|
||||
}
|
||||
|
||||
@@ -98,30 +98,5 @@ export const formatSelectorValue = (
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
if ("object" in selector) {
|
||||
const { fields } = selector.object ?? {};
|
||||
const items = ensureArray(value);
|
||||
return items
|
||||
.map((item) => {
|
||||
if (item == null || typeof item !== "object") {
|
||||
return String(item);
|
||||
}
|
||||
if (fields) {
|
||||
return Object.entries(fields)
|
||||
.filter(([key]) => key in item && item[key] != null)
|
||||
.map(([key, field]) =>
|
||||
formatSelectorValue(hass, item[key], field.selector)
|
||||
)
|
||||
.join(" = ");
|
||||
}
|
||||
return JSON.stringify(item);
|
||||
})
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
return ensureArray(value)
|
||||
.map((v) =>
|
||||
v != null && typeof v === "object" ? JSON.stringify(v) : String(v)
|
||||
)
|
||||
.join(", ");
|
||||
return ensureArray(value).join(", ");
|
||||
};
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-form/ha-form";
|
||||
@@ -9,15 +7,9 @@ import "../../components/ha-dialog-footer";
|
||||
import "../../components/ha-dialog";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HassDialog, ShowDialogParams } from "../make-dialog-manager";
|
||||
import type { HassDialog } from "../make-dialog-manager";
|
||||
import type { FormDialogData, FormDialogParams } from "./show-form-dialog";
|
||||
|
||||
interface StackEntry {
|
||||
params: FormDialogParams;
|
||||
data: FormDialogData;
|
||||
nestedField?: string;
|
||||
}
|
||||
|
||||
@customElement("dialog-form")
|
||||
export class DialogForm
|
||||
extends LitElement
|
||||
@@ -33,8 +25,6 @@ export class DialogForm
|
||||
|
||||
@state() private _closeState?: "canceled" | "submitted";
|
||||
|
||||
@state() private _stack: StackEntry[] = [];
|
||||
|
||||
public async showDialog(params: FormDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._data = params.data || {};
|
||||
@@ -46,41 +36,11 @@ export class DialogForm
|
||||
return true;
|
||||
}
|
||||
|
||||
private _handleNestedShowDialog = (
|
||||
ev: HASSDomEvent<ShowDialogParams<unknown>>
|
||||
) => {
|
||||
if (ev.detail.dialogTag !== "dialog-form") {
|
||||
return;
|
||||
}
|
||||
ev.stopPropagation();
|
||||
|
||||
const origin = ev.composedPath()[0] as HTMLElement & { name?: string };
|
||||
this._stack = [
|
||||
...this._stack,
|
||||
{ params: this._params!, data: this._data, nestedField: origin?.name },
|
||||
];
|
||||
const nested = ev.detail.dialogParams as FormDialogParams;
|
||||
this._params = nested;
|
||||
this._data = nested?.data || {};
|
||||
};
|
||||
|
||||
private _popStack(): string | undefined {
|
||||
if (!this._stack.length) {
|
||||
return undefined;
|
||||
}
|
||||
const prev = this._stack[this._stack.length - 1];
|
||||
this._stack = this._stack.slice(0, -1);
|
||||
this._params = prev.params;
|
||||
this._data = prev.data;
|
||||
return prev.nestedField;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
if (!this._closeState) {
|
||||
this._params?.cancel?.();
|
||||
}
|
||||
this._closeState = undefined;
|
||||
this._stack = [];
|
||||
this._params = undefined;
|
||||
this._data = {};
|
||||
this._open = false;
|
||||
@@ -89,44 +49,14 @@ export class DialogForm
|
||||
|
||||
private _submit(): void {
|
||||
this._closeState = "submitted";
|
||||
const submit = this._params?.submit;
|
||||
const data = this._data;
|
||||
const nestedField = this._popStack();
|
||||
|
||||
submit?.(data);
|
||||
|
||||
if (!nestedField) {
|
||||
this.closeDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
const schemaField = this._params?.schema.find(
|
||||
(f) => "selector" in f && f.name === nestedField
|
||||
);
|
||||
const isMultiple =
|
||||
schemaField &&
|
||||
"selector" in schemaField &&
|
||||
"object" in schemaField.selector &&
|
||||
schemaField.selector.object?.multiple === true;
|
||||
|
||||
const current = this._data[nestedField];
|
||||
const newValue = isMultiple
|
||||
? [...(Array.isArray(current) ? current : []), data]
|
||||
: data;
|
||||
|
||||
this._data = deepClone({ ...this._data, [nestedField]: newValue });
|
||||
this._params?.submit?.(this._data);
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _cancel(): void {
|
||||
this._closeState = "canceled";
|
||||
const cancel = this._params?.cancel;
|
||||
const nestedField = this._popStack();
|
||||
|
||||
cancel?.();
|
||||
|
||||
if (!nestedField) {
|
||||
this.closeDialog();
|
||||
}
|
||||
this._params?.cancel?.();
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
@@ -154,7 +84,6 @@ export class DialogForm
|
||||
.data=${this._data}
|
||||
.schema=${this._params.schema}
|
||||
@value-changed=${this._valueChanged}
|
||||
@show-dialog=${this._handleNestedShowDialog}
|
||||
>
|
||||
</ha-form>
|
||||
<ha-dialog-footer slot="footer">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { mdiDevices } from "@mdi/js";
|
||||
import Fuse from "fuse.js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -149,14 +149,6 @@ export class QuickBar extends LitElement {
|
||||
this._loading = false;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("_loading") && !this._loading && this._opened) {
|
||||
requestAnimationFrame(() => {
|
||||
this._comboBox?.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _dialogOpened = async () => {
|
||||
this._opened = true;
|
||||
requestAnimationFrame(() => {
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import type { LitElement, PropertyValues } from "lit";
|
||||
import { state } from "lit/decorators";
|
||||
import type { StyleInfo } from "lit/directives/style-map";
|
||||
import type { Constructor } from "../types";
|
||||
|
||||
/**
|
||||
* Public interface added by {@link MatchMinHeightMixin}.
|
||||
*
|
||||
* Declared separately so consumers can reference the mixin's contributed
|
||||
* members in their own type annotations, per the Lit mixin authoring guide.
|
||||
*/
|
||||
export declare class MatchMinHeightMixinInterface {
|
||||
/** Most recently observed height of `matchMinHeightTarget`, in pixels. */
|
||||
protected _matchedMinHeight?: number;
|
||||
|
||||
/**
|
||||
* `StyleInfo` exposing the matched height as a `min-height` declaration.
|
||||
* Pass to `styleMap` to keep a layout at least as tall as the target
|
||||
* element. Empty until a height has been observed.
|
||||
*/
|
||||
protected get _matchMinHeightStyle(): StyleInfo;
|
||||
|
||||
/**
|
||||
* Element whose height should be matched as a `min-height` floor. Override
|
||||
* with a getter that returns a `@query` result. Return `null` to pause
|
||||
* observation (e.g. while the element is not rendered).
|
||||
*/
|
||||
protected get matchMinHeightTarget(): HTMLElement | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mixin that observes a target element's height and exposes it as a
|
||||
* `min-height` style. Useful for keeping a sibling layout (e.g. a YAML
|
||||
* editor) at least as tall as another (e.g. a UI form) to avoid content
|
||||
* shift when toggling between them.
|
||||
*
|
||||
* Subclasses override `matchMinHeightTarget` (typically returning a
|
||||
* `@query`-decorated element) to specify which element to observe.
|
||||
*/
|
||||
export const MatchMinHeightMixin = <T extends Constructor<LitElement>>(
|
||||
superClass: T
|
||||
) => {
|
||||
class MatchMinHeightMixinClass extends superClass {
|
||||
@state() protected _matchedMinHeight?: number;
|
||||
|
||||
private _matchTarget: HTMLElement | null = null;
|
||||
|
||||
private _matchResize = new ResizeController(this, {
|
||||
target: null,
|
||||
callback: (entries) => {
|
||||
const height = entries[0]?.contentRect.height;
|
||||
if (height) {
|
||||
this._matchedMinHeight = height;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
private static readonly DEFAULT_MATCH_TARGET: HTMLElement | null = null;
|
||||
|
||||
protected get matchMinHeightTarget(): HTMLElement | null {
|
||||
return MatchMinHeightMixinClass.DEFAULT_MATCH_TARGET;
|
||||
}
|
||||
|
||||
protected get _matchMinHeightStyle(): StyleInfo {
|
||||
return this._matchedMinHeight !== undefined
|
||||
? { "min-height": `${this._matchedMinHeight}px` }
|
||||
: {};
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>) {
|
||||
super.firstUpdated?.(changedProperties);
|
||||
this._attachMatchTarget();
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues<this>) {
|
||||
super.updated?.(changedProperties);
|
||||
this._attachMatchTarget();
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
this._detachMatchTarget();
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
private _attachMatchTarget() {
|
||||
const element = this.matchMinHeightTarget;
|
||||
if (element === this._matchTarget) {
|
||||
return;
|
||||
}
|
||||
this._detachMatchTarget();
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
this._matchTarget = element;
|
||||
this._matchResize.observe(element);
|
||||
}
|
||||
|
||||
private _detachMatchTarget() {
|
||||
if (!this._matchTarget) {
|
||||
return;
|
||||
}
|
||||
this._matchResize.unobserve?.(this._matchTarget);
|
||||
this._matchTarget = null;
|
||||
}
|
||||
}
|
||||
|
||||
return MatchMinHeightMixinClass as unknown as Constructor<MatchMinHeightMixinInterface> &
|
||||
T;
|
||||
};
|
||||
@@ -14,6 +14,7 @@ import "../components/ha-alert";
|
||||
import "../components/ha-button";
|
||||
import "../components/ha-list";
|
||||
import "../components/ha-list-item";
|
||||
import "../components/ha-radio";
|
||||
import "../components/ha-spinner";
|
||||
import "../components/input/ha-input";
|
||||
import type { HaInput } from "../components/input/ha-input";
|
||||
|
||||
@@ -39,7 +39,6 @@ import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
|
||||
import "../../../../components/automation/ha-automation-row";
|
||||
import type { HaAutomationRow } from "../../../../components/automation/ha-automation-row";
|
||||
import "../../../../components/automation/ha-automation-row-event-chip";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
|
||||
@@ -66,7 +65,6 @@ import {
|
||||
manifestsContext,
|
||||
} from "../../../../data/context";
|
||||
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
|
||||
import type { DomainManifestLookup } from "../../../../data/integration";
|
||||
import type {
|
||||
Action,
|
||||
DeviceAction,
|
||||
@@ -75,6 +73,7 @@ import type {
|
||||
ServiceAction,
|
||||
} from "../../../../data/script";
|
||||
import { getActionType, isAction } from "../../../../data/script";
|
||||
import type { DomainManifestLookup } from "../../../../data/integration";
|
||||
import { describeAction } from "../../../../data/script_i18n";
|
||||
import { callExecuteScript } from "../../../../data/service";
|
||||
import {
|
||||
@@ -204,7 +203,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
@state() private _running = false;
|
||||
|
||||
@state() private _runResult?: {
|
||||
variant: "success" | "danger" | "neutral";
|
||||
variant: "success" | "danger" | "info";
|
||||
title: string;
|
||||
details?: string;
|
||||
};
|
||||
@@ -756,8 +755,13 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
this.scrollIntoView();
|
||||
});
|
||||
|
||||
this._running = false;
|
||||
this._runResult = undefined;
|
||||
this._runResult = {
|
||||
variant: "info",
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.run"
|
||||
),
|
||||
};
|
||||
this._running = true;
|
||||
|
||||
const validated = await validateConfig(this.hass, {
|
||||
actions: this.action,
|
||||
@@ -772,22 +776,9 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
details: validated.actions.error,
|
||||
};
|
||||
} else {
|
||||
const runTimeout = setTimeout(() => {
|
||||
this._runResult = {
|
||||
variant: "neutral",
|
||||
title: `${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.running_action"
|
||||
)}...`,
|
||||
};
|
||||
|
||||
this._running = true;
|
||||
}, 500);
|
||||
|
||||
try {
|
||||
await callExecuteScript(this.hass, this.action);
|
||||
clearTimeout(runTimeout);
|
||||
} catch (err: any) {
|
||||
clearTimeout(runTimeout);
|
||||
this._runResult = {
|
||||
variant: "danger",
|
||||
title: this.hass.localize(
|
||||
@@ -798,7 +789,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
if (!this._runResult || this._runResult.variant === "neutral") {
|
||||
if (this._runResult.variant === "info") {
|
||||
this._runResult = {
|
||||
variant: "success",
|
||||
title: this.hass.localize(
|
||||
@@ -807,8 +798,6 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
this._running = true;
|
||||
|
||||
this._runResultTimeout = window.setTimeout(() => {
|
||||
this._running = false;
|
||||
}, 2500);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { mdiHelpCircleOutline } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
@@ -10,7 +9,7 @@ import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-select-box";
|
||||
import "../../../../components/ha-radio";
|
||||
import "../../../../components/input/ha-input";
|
||||
|
||||
import type { HaInput } from "../../../../components/input/ha-input";
|
||||
@@ -84,26 +83,56 @@ class DialogAutomationMode extends LitElement implements HassDialog {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
></ha-icon-button>
|
||||
<ha-select-box
|
||||
.options=${MODES.map((mode) => ({
|
||||
label: this.hass.localize(
|
||||
<ha-md-list
|
||||
role="listbox"
|
||||
tabindex="0"
|
||||
aria-activedescendant="option-${this._newMode}"
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.modes.label"
|
||||
)}
|
||||
>
|
||||
${MODES.map((mode) => {
|
||||
const label = this.hass.localize(
|
||||
`ui.panel.config.automation.editor.modes.${mode}`
|
||||
),
|
||||
description: this.hass.localize(
|
||||
`ui.panel.config.automation.editor.modes.${mode}_description`
|
||||
),
|
||||
value: mode,
|
||||
}))}
|
||||
.value=${this._newMode}
|
||||
@value-changed=${this._modeChanged}
|
||||
.maxColumns=${1}
|
||||
.hass=${this.hass}
|
||||
></ha-select-box>
|
||||
);
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
class="option"
|
||||
type="button"
|
||||
@click=${this._modeChanged}
|
||||
.value=${mode}
|
||||
id="option-${mode}"
|
||||
role="option"
|
||||
aria-label=${label}
|
||||
aria-selected=${this._newMode === mode}
|
||||
>
|
||||
<div slot="start">
|
||||
<ha-radio
|
||||
inert
|
||||
.checked=${this._newMode === mode}
|
||||
value=${mode}
|
||||
@change=${this._modeChanged}
|
||||
name="mode"
|
||||
></ha-radio>
|
||||
</div>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.modes.${mode}`
|
||||
)}
|
||||
</div>
|
||||
<div slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.modes.${mode}_description`
|
||||
)}
|
||||
</div>
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
})}
|
||||
</ha-md-list>
|
||||
|
||||
${isMaxMode(this._newMode)
|
||||
? html`
|
||||
<div class="max-value">
|
||||
<wa-divider></wa-divider>
|
||||
<div class="options">
|
||||
<ha-input
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.max.${this._newMode}`
|
||||
@@ -137,7 +166,7 @@ class DialogAutomationMode extends LitElement implements HassDialog {
|
||||
}
|
||||
|
||||
private _modeChanged(ev) {
|
||||
const mode = ev.detail.value;
|
||||
const mode = ev.currentTarget.value;
|
||||
this._newMode = mode;
|
||||
if (!isMaxMode(mode)) {
|
||||
this._newMax = undefined;
|
||||
@@ -171,8 +200,11 @@ class DialogAutomationMode extends LitElement implements HassDialog {
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
.max-value {
|
||||
margin-top: var(--ha-space-3);
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
.options {
|
||||
padding: 0 24px 24px 24px;
|
||||
}
|
||||
ha-wa-dialog ha-icon-button[slot="headerActionItems"] {
|
||||
color: var(--secondary-text-color);
|
||||
|
||||
@@ -52,11 +52,7 @@ import { isCondition, testCondition } from "../../../../data/automation";
|
||||
import { describeCondition } from "../../../../data/automation_i18n";
|
||||
import type { ConditionDescriptions } from "../../../../data/condition";
|
||||
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
|
||||
import {
|
||||
validateConfig,
|
||||
type InvalidConfig,
|
||||
type ValidConfig,
|
||||
} from "../../../../data/config";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
import { fullEntitiesContext } from "../../../../data/context";
|
||||
import type { DeviceCondition } from "../../../../data/device/device_automation";
|
||||
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
|
||||
@@ -599,6 +595,8 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
clearTimeout(this._testingTimeout);
|
||||
}
|
||||
|
||||
this._testingResult = undefined;
|
||||
this._testing = true;
|
||||
const condition = this.condition;
|
||||
requestAnimationFrame(() => {
|
||||
// @ts-ignore is supported in all browsers except firefox
|
||||
@@ -610,59 +608,53 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
this.scrollIntoView();
|
||||
});
|
||||
|
||||
let validateResult: Record<"conditions", InvalidConfig | ValidConfig>;
|
||||
try {
|
||||
validateResult = await validateConfig(this.hass, {
|
||||
const validateResult = await validateConfig(this.hass, {
|
||||
conditions: condition,
|
||||
});
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.validation_failed"
|
||||
),
|
||||
});
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error validating condition", err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Abort if condition changed.
|
||||
if (this.condition !== condition) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateResult.conditions.valid) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.invalid_condition"
|
||||
),
|
||||
text: validateResult.conditions.error,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let result: { result: boolean };
|
||||
try {
|
||||
result = await testCondition(this.hass, condition);
|
||||
} catch (err: any) {
|
||||
// Abort if condition changed.
|
||||
if (this.condition !== condition) {
|
||||
this._testing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.test_failed"
|
||||
),
|
||||
text: err.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!validateResult.conditions.valid) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.invalid_condition"
|
||||
),
|
||||
text: validateResult.conditions.error,
|
||||
});
|
||||
this._testing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this._testingResult = result.result;
|
||||
this._testing = true;
|
||||
this._testingTimeout = window.setTimeout(() => {
|
||||
this._testing = false;
|
||||
}, 2500);
|
||||
let result: { result: boolean };
|
||||
try {
|
||||
result = await testCondition(this.hass, condition);
|
||||
} catch (err: any) {
|
||||
if (this.condition !== condition) {
|
||||
this._testing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.test_failed"
|
||||
),
|
||||
text: err.message,
|
||||
});
|
||||
this._testing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this._testingResult = result.result;
|
||||
} finally {
|
||||
this._testingTimeout = window.setTimeout(() => {
|
||||
this._testing = false;
|
||||
}, 2500);
|
||||
}
|
||||
};
|
||||
|
||||
private _renameCondition = async (): Promise<void> => {
|
||||
|
||||
@@ -546,6 +546,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
.readOnly=${this.readOnly}
|
||||
@value-changed=${this._yamlChanged}
|
||||
@editor-save=${this._handleSaveAutomation}
|
||||
.showErrors=${false}
|
||||
disable-fullscreen
|
||||
></ha-yaml-editor>
|
||||
<ha-button
|
||||
|
||||
@@ -136,7 +136,6 @@ type AutomationItem = AutomationEntity & {
|
||||
formatted_state: string;
|
||||
category: string | undefined;
|
||||
label_entries: LabelRegistryEntry[];
|
||||
labels: string[]; // search only
|
||||
assistants: string[];
|
||||
assistants_sortable_key: string | undefined;
|
||||
};
|
||||
@@ -259,9 +258,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
const category = entityRegEntry?.categories.automation;
|
||||
const labels = labelReg && entityRegEntry?.labels;
|
||||
const label_entries = (labels || [])
|
||||
.map((lbl) => labelReg!.find((label) => label.label_id === lbl)!)
|
||||
.filter(Boolean);
|
||||
const assistants = getEntityVoiceAssistantsIds(
|
||||
entityReg,
|
||||
automation.entity_id
|
||||
@@ -277,8 +273,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
category: category
|
||||
? categoryReg?.find((cat) => cat.category_id === category)?.name
|
||||
: undefined,
|
||||
label_entries,
|
||||
labels: label_entries.map((lbl) => lbl.name),
|
||||
label_entries: (labels || [])
|
||||
.map((lbl) => labelReg!.find((label) => label.label_id === lbl)!)
|
||||
.filter(Boolean),
|
||||
assistants,
|
||||
assistants_sortable_key: getAssistantsSortableKey(assistants),
|
||||
selectable: entityRegEntry !== undefined,
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import "../../../../../components/entity/ha-entity-picker";
|
||||
import "../../../../../components/ha-formfield";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { computeStateDomain } from "../../../../../common/entity/compute_state_domain";
|
||||
import { hasLocation } from "../../../../../common/entity/has_location";
|
||||
import "../../../../../components/entity/ha-entity-picker";
|
||||
import "../../../../../components/radio/ha-radio-group";
|
||||
import type { HaRadioGroup } from "../../../../../components/radio/ha-radio-group";
|
||||
import "../../../../../components/radio/ha-radio-option";
|
||||
import type { ZoneTrigger } from "../../../../../data/automation";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../../types";
|
||||
import type { ValueChangedEvent, HomeAssistant } from "../../../../../types";
|
||||
import type { HaRadio } from "../../../../../components/ha-radio";
|
||||
|
||||
function zoneAndLocationFilter(stateObj) {
|
||||
return hasLocation(stateObj) && computeStateDomain(stateObj) !== "zone";
|
||||
@@ -57,27 +56,39 @@ export class HaZoneTrigger extends LitElement {
|
||||
.includeDomains=${includeDomains}
|
||||
></ha-entity-picker>
|
||||
|
||||
<ha-radio-group
|
||||
orientation="horizontal"
|
||||
.label=${this.hass.localize(
|
||||
<label>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.type.zone.event"
|
||||
)}
|
||||
.value=${event}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._radioGroupPicked}
|
||||
name="event"
|
||||
>
|
||||
<ha-radio-option value="enter">
|
||||
${this.hass.localize(
|
||||
<ha-formfield
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.type.zone.enter"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="leave">
|
||||
${this.hass.localize(
|
||||
>
|
||||
<ha-radio
|
||||
name="event"
|
||||
value="enter"
|
||||
.disabled=${this.disabled}
|
||||
.checked=${event === "enter"}
|
||||
@change=${this._radioGroupPicked}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.type.zone.leave"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
</ha-radio-group>
|
||||
>
|
||||
<ha-radio
|
||||
name="event"
|
||||
value="leave"
|
||||
.disabled=${this.disabled}
|
||||
.checked=${event === "leave"}
|
||||
@change=${this._radioGroupPicked}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
</label>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -95,17 +106,21 @@ export class HaZoneTrigger extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _radioGroupPicked(ev: Event) {
|
||||
private _radioGroupPicked(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.trigger,
|
||||
event: (ev.currentTarget as HaRadioGroup).value,
|
||||
event: (ev.target as HaRadio).value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
ha-entity-picker {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
import { mdiPuzzle } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-icon-next";
|
||||
import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import {
|
||||
getSupervisorUpdateConfig,
|
||||
type SupervisorUpdateConfig,
|
||||
} from "../../../../../data/supervisor/update";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
|
||||
@customElement("ha-backup-overview-app-update-backup")
|
||||
class HaBackupOverviewAppUpdateBackup extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _supervisorUpdateConfig?: SupervisorUpdateConfig;
|
||||
|
||||
protected firstUpdated() {
|
||||
this._fetchSupervisorUpdateConfig();
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.hasUpdated) {
|
||||
this._fetchSupervisorUpdateConfig();
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchSupervisorUpdateConfig() {
|
||||
try {
|
||||
this._supervisorUpdateConfig = await getSupervisorUpdateConfig(this.hass);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
private _appUpdateBackupDescription() {
|
||||
if (!this._supervisorUpdateConfig) {
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.backup.settings.app_update_backup.local_only"
|
||||
);
|
||||
}
|
||||
|
||||
if (!this._supervisorUpdateConfig.add_on_backup_before_update) {
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.backup.schedule.update_preference.skip_backups"
|
||||
);
|
||||
}
|
||||
|
||||
const copies =
|
||||
this._supervisorUpdateConfig.add_on_backup_retain_copies || 1;
|
||||
|
||||
return `${this.hass.localize(
|
||||
"ui.panel.config.backup.schedule.update_preference.backup_before_update"
|
||||
)} ${this.hass.localize(
|
||||
"ui.panel.config.backup.overview.settings.schedule_copies_backups",
|
||||
{ count: copies }
|
||||
)}`;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="card-header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.overview.app_update_backup.title"
|
||||
)}
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<ha-md-list>
|
||||
<ha-md-list-item
|
||||
type="link"
|
||||
href="/config/backup/app-update-backups"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPuzzle}></ha-svg-icon>
|
||||
<div slot="headline">${this._appUpdateBackupDescription()}</div>
|
||||
<div slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.overview.app_update_backup.description"
|
||||
)}
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.card-header {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
ha-md-list {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-backup-overview-app-update-backup": HaBackupOverviewAppUpdateBackup;
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-card";
|
||||
import {
|
||||
getSupervisorUpdateConfig,
|
||||
updateSupervisorUpdateConfig,
|
||||
type SupervisorUpdateConfig,
|
||||
} from "../../../data/supervisor/update";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "./components/config/ha-backup-config-addon";
|
||||
|
||||
@customElement("ha-config-backup-app-update-backups")
|
||||
class HaConfigBackupAppUpdateBackups extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _supervisorUpdateConfig?: SupervisorUpdateConfig;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues<this>): void {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (
|
||||
!this.hasUpdated &&
|
||||
this.hass &&
|
||||
isComponentLoaded(this.hass.config, "hassio")
|
||||
) {
|
||||
this._getSupervisorUpdateConfig();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<hass-subpage
|
||||
back-path="/config/backup/overview"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.backup.app_update_backups.header"
|
||||
)}
|
||||
>
|
||||
<div class="content">
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.settings.app_update_backup.description"
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.settings.app_update_backup.local_only"
|
||||
)}
|
||||
</p>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
<ha-backup-config-addon
|
||||
.hass=${this.hass}
|
||||
.supervisorUpdateConfig=${this._supervisorUpdateConfig}
|
||||
@update-config-changed=${this._supervisorUpdateConfigChanged}
|
||||
></ha-backup-config-addon>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _getSupervisorUpdateConfig() {
|
||||
try {
|
||||
this._supervisorUpdateConfig = await getSupervisorUpdateConfig(this.hass);
|
||||
this._error = undefined;
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
this._error = this.hass.localize(
|
||||
"ui.panel.config.backup.settings.app_update_backup.error_load",
|
||||
{
|
||||
error: err?.message || err,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async _supervisorUpdateConfigChanged(ev) {
|
||||
const config = ev.detail.value as SupervisorUpdateConfig;
|
||||
this._supervisorUpdateConfig = {
|
||||
...this._supervisorUpdateConfig,
|
||||
...config,
|
||||
} as SupervisorUpdateConfig;
|
||||
this._debounceSaveSupervisorUpdateConfig();
|
||||
}
|
||||
|
||||
private _debounceSaveSupervisorUpdateConfig = debounce(
|
||||
() => this._saveSupervisorUpdateConfig(),
|
||||
500
|
||||
);
|
||||
|
||||
private async _saveSupervisorUpdateConfig() {
|
||||
if (!this._supervisorUpdateConfig) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await updateSupervisorUpdateConfig(
|
||||
this.hass,
|
||||
this._supervisorUpdateConfig
|
||||
);
|
||||
this._error = undefined;
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
this._error = this.hass.localize(
|
||||
"ui.panel.config.backup.settings.app_update_backup.error_save",
|
||||
{
|
||||
error: err?.message || err?.toString(),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
p {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 28px 20px 0;
|
||||
max-width: 690px;
|
||||
margin: 0 auto;
|
||||
gap: var(--ha-space-6);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-backup-app-update-backups": HaConfigBackupAppUpdateBackups;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { mdiDotsVertical, mdiPlus, mdiUpload } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-dropdown";
|
||||
@@ -24,7 +23,6 @@ import {
|
||||
computeBackupAgentName,
|
||||
generateBackup,
|
||||
generateBackupWithAutomaticSettings,
|
||||
saveBackupConfig,
|
||||
} from "../../../data/backup";
|
||||
import type { ManagerStateEvent } from "../../../data/backup_manager";
|
||||
import type { CloudStatus } from "../../../data/cloud";
|
||||
@@ -34,12 +32,10 @@ import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import { showAlertDialog } from "../../lovelace/custom-card-helpers";
|
||||
import "./components/overview/ha-backup-overview-backups";
|
||||
import "./components/overview/ha-backup-overview-app-update-backup";
|
||||
import "./components/overview/ha-backup-overview-onboarding";
|
||||
import "./components/overview/ha-backup-overview-progress";
|
||||
import "./components/overview/ha-backup-overview-settings";
|
||||
import "./components/overview/ha-backup-overview-summary";
|
||||
import "./components/config/ha-backup-config-encryption-key";
|
||||
import { showBackupOnboardingDialog } from "./dialogs/show-dialog-backup_onboarding";
|
||||
import { showGenerateBackupDialog } from "./dialogs/show-dialog-generate-backup";
|
||||
import { showNewBackupDialog } from "./dialogs/show-dialog-new-backup";
|
||||
@@ -72,54 +68,10 @@ class HaConfigBackupOverview extends LitElement {
|
||||
{ uploaded_bytes: number; total_bytes: number }
|
||||
> = {};
|
||||
|
||||
@state() private _config?: BackupConfig;
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
super.willUpdate(changedProperties);
|
||||
if (changedProperties.has("config") && !this._config) {
|
||||
this._config = this.config;
|
||||
}
|
||||
}
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
// Update config when the page is displayed (e.g. when coming back from a settings page)
|
||||
this._config = this.config;
|
||||
}
|
||||
|
||||
private _uploadBackup = async () => {
|
||||
await showUploadBackupDialog(this, {});
|
||||
};
|
||||
|
||||
private _encryptionKeyChanged(ev) {
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
|
||||
const password = ev.detail.value as string;
|
||||
this._config = {
|
||||
...this._config,
|
||||
create_backup: {
|
||||
...this._config.create_backup,
|
||||
password,
|
||||
},
|
||||
};
|
||||
|
||||
this._debounceSaveConfig();
|
||||
}
|
||||
|
||||
private _debounceSaveConfig = debounce(() => this._saveConfig(), 500);
|
||||
|
||||
private async _saveConfig() {
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
|
||||
await saveBackupConfig(this.hass, this._config);
|
||||
|
||||
fireEvent(this, "ha-refresh-backup-config");
|
||||
}
|
||||
|
||||
private _handleOnboardingButtonClick(ev) {
|
||||
ev.stopPropagation();
|
||||
this._setupAutomaticBackup(true);
|
||||
@@ -282,41 +234,13 @@ class HaConfigBackupOverview extends LitElement {
|
||||
.backups=${this.backups}
|
||||
></ha-backup-overview-backups>
|
||||
|
||||
${!this._needsOnboarding && this._config
|
||||
${!this._needsOnboarding && this.config
|
||||
? html`
|
||||
<ha-card>
|
||||
<div class="card-header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.settings.encryption_key.title"
|
||||
)}
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.settings.encryption_key.description"
|
||||
)}
|
||||
</p>
|
||||
<ha-backup-config-encryption-key
|
||||
.hass=${this.hass}
|
||||
.value=${this._config.create_backup.password}
|
||||
@value-changed=${this._encryptionKeyChanged}
|
||||
></ha-backup-config-encryption-key>
|
||||
</div>
|
||||
</ha-card>
|
||||
|
||||
<ha-backup-overview-settings
|
||||
.hass=${this.hass}
|
||||
.config=${this._config}
|
||||
.config=${this.config!}
|
||||
.agents=${this.agents}
|
||||
></ha-backup-overview-settings>
|
||||
|
||||
${this.hass.config.components.includes("hassio")
|
||||
? html`
|
||||
<ha-backup-overview-app-update-backup
|
||||
.hass=${this.hass}
|
||||
></ha-backup-overview-app-update-backup>
|
||||
`
|
||||
: nothing}
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
@@ -346,10 +270,6 @@ class HaConfigBackupOverview extends LitElement {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
p {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 28px 20px 0;
|
||||
max-width: 690px;
|
||||
@@ -363,6 +283,10 @@ class HaConfigBackupOverview extends LitElement {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.card-content {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
.loading {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { BackupAgent, BackupConfig } from "../../../data/backup";
|
||||
import { saveBackupConfig } from "../../../data/backup";
|
||||
import { updateBackupConfig } from "../../../data/backup";
|
||||
import type { CloudStatus } from "../../../data/cloud";
|
||||
import {
|
||||
getSupervisorUpdateConfig,
|
||||
@@ -27,9 +27,11 @@ import "../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import "./components/config/ha-backup-config-addon";
|
||||
import "./components/config/ha-backup-config-agents";
|
||||
import "./components/config/ha-backup-config-data";
|
||||
import type { BackupConfigData } from "./components/config/ha-backup-config-data";
|
||||
import "./components/config/ha-backup-config-encryption-key";
|
||||
import "./components/config/ha-backup-config-schedule";
|
||||
import type { BackupConfigSchedule } from "./components/config/ha-backup-config-schedule";
|
||||
import { showLocalBackupLocationDialog } from "./dialogs/show-dialog-local-backup-location";
|
||||
@@ -77,7 +79,7 @@ class HaConfigBackupSettings extends LitElement {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
this._supervisorUpdateConfigError = this.hass.localize(
|
||||
"ui.panel.config.backup.settings.schedule.error_load",
|
||||
"ui.panel.config.backup.settings.app_update_backup.error_load",
|
||||
{
|
||||
error: err?.message || err,
|
||||
}
|
||||
@@ -313,6 +315,57 @@ class HaConfigBackupSettings extends LitElement {
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-card>
|
||||
${supervisor
|
||||
? html`<ha-card>
|
||||
<div class="card-header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.settings.app_update_backup.title"
|
||||
)}
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.settings.app_update_backup.description"
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.settings.app_update_backup.local_only"
|
||||
)}
|
||||
</p>
|
||||
${this._supervisorUpdateConfigError
|
||||
? html`<ha-alert alert-type="error">
|
||||
${this._supervisorUpdateConfigError}
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
<ha-backup-config-addon
|
||||
.hass=${this.hass}
|
||||
.supervisorUpdateConfig=${this._supervisorUpdateConfig}
|
||||
@update-config-changed=${this
|
||||
._supervisorUpdateConfigChanged}
|
||||
></ha-backup-config-addon>
|
||||
</div>
|
||||
</ha-card>`
|
||||
: nothing}
|
||||
<ha-card>
|
||||
<div class="card-header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.settings.encryption_key.title"
|
||||
)}
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.settings.encryption_key.description"
|
||||
)}
|
||||
</p>
|
||||
<ha-backup-config-encryption-key
|
||||
.hass=${this.hass}
|
||||
.value=${this._config.create_backup.password}
|
||||
@value-changed=${this._encryptionKeyChanged}
|
||||
></ha-backup-config-encryption-key>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
@@ -385,6 +438,18 @@ class HaConfigBackupSettings extends LitElement {
|
||||
this._debounceSave();
|
||||
}
|
||||
|
||||
private _encryptionKeyChanged(ev) {
|
||||
const password = ev.detail.value as string;
|
||||
this._config = {
|
||||
...this._config!,
|
||||
create_backup: {
|
||||
...this._config!.create_backup,
|
||||
password: password,
|
||||
},
|
||||
};
|
||||
this._debounceSave();
|
||||
}
|
||||
|
||||
private _debounceSaveSupervisorUpdateConfig = debounce(
|
||||
() => this._saveSupervisorUpdateConfig(),
|
||||
500
|
||||
@@ -403,7 +468,7 @@ class HaConfigBackupSettings extends LitElement {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
this._supervisorUpdateConfigError = this.hass.localize(
|
||||
"ui.panel.config.backup.settings.schedule.error_save",
|
||||
"ui.panel.config.backup.settings.app_update_backup.error_save",
|
||||
{
|
||||
error: err?.message || err?.toString(),
|
||||
}
|
||||
@@ -414,7 +479,18 @@ class HaConfigBackupSettings extends LitElement {
|
||||
private _debounceSave = debounce(() => this._save(), 500);
|
||||
|
||||
private async _save() {
|
||||
await saveBackupConfig(this.hass, this._config!);
|
||||
await updateBackupConfig(this.hass, {
|
||||
create_backup: {
|
||||
agent_ids: this._config!.create_backup.agent_ids,
|
||||
include_folders: this._config!.create_backup.include_folders ?? [],
|
||||
include_database: this._config!.create_backup.include_database,
|
||||
include_addons: this._config!.create_backup.include_addons ?? [],
|
||||
include_all_addons: this._config!.create_backup.include_all_addons,
|
||||
password: this._config!.create_backup.password,
|
||||
},
|
||||
retention: this._config!.retention,
|
||||
schedule: this._config!.schedule,
|
||||
});
|
||||
fireEvent(this, "ha-refresh-backup-config");
|
||||
}
|
||||
|
||||
|
||||
@@ -125,11 +125,6 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
|
||||
load: () => import("./ha-config-backup-settings"),
|
||||
cache: true,
|
||||
},
|
||||
"app-update-backups": {
|
||||
tag: "ha-config-backup-app-update-backups",
|
||||
load: () => import("./ha-config-backup-app-update-backups"),
|
||||
cache: true,
|
||||
},
|
||||
location: {
|
||||
tag: "ha-config-backup-location",
|
||||
load: () => import("./ha-config-backup-location"),
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import { mdiDownload } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { extractSearchParam } from "../../../common/url/search-params";
|
||||
import "../../../components/ha-analytics";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-md-list";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import "../../../components/ha-spinner";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-switch";
|
||||
import { getSignedPath } from "../../../data/auth";
|
||||
import type { HaSwitch } from "../../../components/ha-switch";
|
||||
import type { Analytics } from "../../../data/analytics";
|
||||
import {
|
||||
@@ -30,7 +26,6 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { fileDownload } from "../../../util/file_download";
|
||||
|
||||
@customElement("ha-config-analytics")
|
||||
class ConfigAnalytics extends SubscribeMixin(LitElement) {
|
||||
@@ -124,18 +119,6 @@ class ConfigAnalytics extends SubscribeMixin(LitElement) {
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button
|
||||
size="small"
|
||||
appearance="plain"
|
||||
@click=${this._downloadDeviceInfo}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiDownload}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.analytics.download_device_info"
|
||||
)}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-card>`
|
||||
: nothing}
|
||||
${this._zwaveEntryId !== undefined
|
||||
@@ -307,11 +290,6 @@ class ConfigAnalytics extends SubscribeMixin(LitElement) {
|
||||
this._save();
|
||||
}
|
||||
|
||||
private async _downloadDeviceInfo(): Promise<void> {
|
||||
const signedPath = await getSignedPath(this.hass, "/api/analytics/devices");
|
||||
fileDownload(signedPath.path);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import { mdiDotsVertical, mdiDownload } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { getSignedPath } from "../../../data/auth";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import { fileDownload } from "../../../util/file_download";
|
||||
import "./ha-config-analytics";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
|
||||
@customElement("ha-config-section-analytics")
|
||||
class HaConfigSectionAnalytics extends LitElement {
|
||||
@@ -21,6 +29,19 @@ class HaConfigSectionAnalytics extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.analytics.caption")}
|
||||
>
|
||||
<ha-dropdown
|
||||
@wa-select=${this._handleOverflowAction}
|
||||
slot="toolbar-icon"
|
||||
>
|
||||
<ha-icon-button slot="trigger" .path=${mdiDotsVertical}>
|
||||
</ha-icon-button>
|
||||
<ha-dropdown-item .value=${"download_device_info"}>
|
||||
<ha-svg-icon slot="icon" .path=${mdiDownload}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.analytics.download_device_info"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
<div class="content">
|
||||
<ha-config-analytics .hass=${this.hass}></ha-config-analytics>
|
||||
</div>
|
||||
@@ -28,6 +49,18 @@ class HaConfigSectionAnalytics extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _handleOverflowAction(
|
||||
ev: HaDropdownSelectEvent
|
||||
): Promise<void> {
|
||||
if (ev.detail.item.value === "download_device_info") {
|
||||
const signedPath = await getSignedPath(
|
||||
this.hass,
|
||||
"/api/analytics/devices"
|
||||
);
|
||||
fileDownload(signedPath.path);
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.content {
|
||||
padding: 28px 20px 0;
|
||||
|
||||
@@ -13,8 +13,10 @@ import "../../../components/ha-checkbox";
|
||||
import type { HaCheckbox } from "../../../components/ha-checkbox";
|
||||
import "../../../components/ha-country-picker";
|
||||
import "../../../components/ha-currency-picker";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-language-picker";
|
||||
import "../../../components/ha-select-box";
|
||||
import "../../../components/ha-radio";
|
||||
import type { HaRadio } from "../../../components/ha-radio";
|
||||
import "../../../components/ha-timezone-picker";
|
||||
import "../../../components/input/ha-input";
|
||||
import type { HaInput } from "../../../components/input/ha-input";
|
||||
@@ -208,61 +210,75 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
"ui.panel.config.core.section.core.core_config.unit_system"
|
||||
)}
|
||||
</div>
|
||||
<div class="unit-system-options">
|
||||
<ha-select-box
|
||||
<ha-formfield
|
||||
.label=${html`
|
||||
<span style="font-size: 14px">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.metric_example"
|
||||
)}
|
||||
</span>
|
||||
<div style="color: var(--secondary-text-color)">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system_metric"
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
>
|
||||
<ha-radio
|
||||
name="unit_system"
|
||||
.hass=${this.hass}
|
||||
.value=${this._unitSystem}
|
||||
value="metric"
|
||||
.checked=${this._unitSystem === "metric"}
|
||||
@change=${this._unitSystemChanged}
|
||||
.disabled=${disabled}
|
||||
@value-changed=${this._unitSystemChanged}
|
||||
.options=${[
|
||||
{
|
||||
value: "metric",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.metric_example"
|
||||
),
|
||||
description: this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system_metric"
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "us_customary",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.us_customary_example"
|
||||
),
|
||||
description: this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system_us_customary"
|
||||
),
|
||||
},
|
||||
]}
|
||||
>
|
||||
</ha-select-box>
|
||||
${this._unitSystem !== this._configuredUnitSystem()
|
||||
? html`
|
||||
<ha-checkbox
|
||||
.checked=${this._updateUnits}
|
||||
.disabled=${this._submittingRegional}
|
||||
@change=${this._updateUnitsChanged}
|
||||
>
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${html`
|
||||
<span style="font-size: 14px">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.us_customary_example"
|
||||
)}
|
||||
</span>
|
||||
<div style="color: var(--secondary-text-color)">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system_us_customary"
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
>
|
||||
<ha-radio
|
||||
name="unit_system"
|
||||
value="us_customary"
|
||||
.checked=${this._unitSystem === "us_customary"}
|
||||
@change=${this._unitSystemChanged}
|
||||
.disabled=${disabled}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
${this._unitSystem !== this._configuredUnitSystem()
|
||||
? html`
|
||||
<ha-checkbox
|
||||
.checked=${this._updateUnits}
|
||||
.disabled=${this._submittingRegional}
|
||||
@change=${this._updateUnitsChanged}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.update_units_label"
|
||||
)}
|
||||
<div slot="hint">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.update_units_label"
|
||||
"ui.panel.config.core.section.core.core_config.update_units_text_1"
|
||||
)}
|
||||
<div slot="hint">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.update_units_text_1"
|
||||
)}
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.update_units_text_2"
|
||||
)}
|
||||
<br /><br />
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.update_units_text_3"
|
||||
)}
|
||||
</div>
|
||||
</ha-checkbox>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.update_units_text_2"
|
||||
)}
|
||||
<br /><br />
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.update_units_text_3"
|
||||
)}
|
||||
</div>
|
||||
</ha-checkbox>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<div>
|
||||
<ha-currency-picker
|
||||
@@ -349,8 +365,10 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
this[`_${target.name}`] = target.value;
|
||||
}
|
||||
|
||||
private _unitSystemChanged(ev: ValueChangedEvent<string>) {
|
||||
this._unitSystem = ev.detail.value as "metric" | "us_customary";
|
||||
private _unitSystemChanged(ev: CustomEvent) {
|
||||
this._unitSystem = (ev.target as HaRadio).value as
|
||||
| "metric"
|
||||
| "us_customary";
|
||||
}
|
||||
|
||||
private _updateUnitsChanged(ev: CustomEvent) {
|
||||
@@ -487,15 +505,6 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
margin-top: var(--ha-space-2);
|
||||
margin-bottom: var(--ha-space-3);
|
||||
}
|
||||
.unit-system-options {
|
||||
padding-top: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.unit-system-options ha-checkbox {
|
||||
display: block;
|
||||
margin-top: var(--ha-space-3);
|
||||
margin-inline-start: var(--ha-space-3);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mdiPower } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import type { CSSResultGroup, TemplateResult, PropertyValues } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { canShowPage } from "../../../common/config/can_show_page";
|
||||
@@ -8,6 +8,7 @@ import { relativeTime } from "../../../common/datetime/relative_time";
|
||||
import { blankBeforePercent } from "../../../common/translations/blank_before_percent";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-navigation-list";
|
||||
import type { BackupContent } from "../../../data/backup";
|
||||
import { fetchBackupInfo } from "../../../data/backup";
|
||||
import type { CloudStatus } from "../../../data/cloud";
|
||||
@@ -28,7 +29,6 @@ import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart"
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../components/ha-config-navigation-list";
|
||||
import "../ha-config-section";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
|
||||
@@ -146,7 +146,7 @@ class HaConfigSystemNavigation extends LitElement {
|
||||
full-width
|
||||
>
|
||||
<ha-card outlined>
|
||||
<ha-config-navigation-list
|
||||
<ha-navigation-list
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.pages=${pages}
|
||||
@@ -154,7 +154,7 @@ class HaConfigSystemNavigation extends LitElement {
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.dashboard.system.main"
|
||||
)}
|
||||
></ha-config-navigation-list>
|
||||
></ha-navigation-list>
|
||||
</ha-card>
|
||||
</ha-config-section>
|
||||
</hass-subpage>
|
||||
@@ -286,6 +286,10 @@ class HaConfigSystemNavigation extends LitElement {
|
||||
margin-top: -42px;
|
||||
}
|
||||
}
|
||||
|
||||
ha-navigation-list {
|
||||
--navigation-list-item-title-font-size: var(--ha-font-size-l);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -434,7 +434,7 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
.dashboard-alert-title {
|
||||
padding: var(--ha-space-4) var(--ha-space-4) var(--ha-space-2);
|
||||
padding: var(--ha-space-4) var(--ha-space-4) 0;
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { filterNavigationPages } from "../../../common/config/filter_navigation_pages";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-navigation-list";
|
||||
import type { CloudStatus } from "../../../data/cloud";
|
||||
import { getConfigEntries } from "../../../data/config_entries";
|
||||
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../components/ha-config-navigation-list";
|
||||
|
||||
@customElement("ha-config-navigation")
|
||||
class HaConfigNavigation extends LitElement {
|
||||
@@ -65,17 +65,20 @@ class HaConfigNavigation extends LitElement {
|
||||
<div class="visually-hidden" role="heading" aria-level="2">
|
||||
${this.hass.localize("panel.config")}
|
||||
</div>
|
||||
<ha-config-navigation-list
|
||||
<ha-navigation-list
|
||||
has-secondary
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.pages=${pages}
|
||||
.label=${this.hass.localize("panel.config")}
|
||||
></ha-config-navigation-list>
|
||||
></ha-navigation-list>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
ha-navigation-list {
|
||||
--navigation-list-item-title-font-size: var(--ha-font-size-l);
|
||||
}
|
||||
/* Accessibility */
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
|
||||
@@ -10,9 +10,9 @@ import { getDeviceArea } from "../../../common/entity/context/get_device_context
|
||||
import "../../../components/entity/state-badge";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-md-list";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import "../../../components/ha-spinner";
|
||||
import "../../../components/item/ha-list-item-button";
|
||||
import "../../../components/list/ha-list-base";
|
||||
import "../../../components/progress/ha-progress-ring";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
import { subscribeDeviceRegistry } from "../../../data/device/device_registry";
|
||||
@@ -86,9 +86,7 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
|
||||
const updates = this.updateEntities;
|
||||
|
||||
return html`
|
||||
<ha-list-base
|
||||
aria-label=${this.hass.localize("ui.panel.config.updates.caption")}
|
||||
>
|
||||
<ha-md-list>
|
||||
${updates.map((entity) => {
|
||||
const entityEntry = this.getEntityEntry(entity.entity_id);
|
||||
const deviceEntry =
|
||||
@@ -103,12 +101,13 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<ha-list-item-button
|
||||
<ha-md-list-item
|
||||
class=${ifDefined(
|
||||
entity.attributes.skipped_version ? "skipped" : undefined
|
||||
)}
|
||||
.entity_id=${entity.entity_id}
|
||||
.hasMeta=${!this.narrow}
|
||||
type="button"
|
||||
@click=${this._openMoreInfo}
|
||||
>
|
||||
<div slot="start">
|
||||
@@ -150,10 +149,10 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
|
||||
${this._renderUpdateProgress(entity)}
|
||||
</div>`
|
||||
: nothing}
|
||||
</ha-list-item-button>
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
})}
|
||||
</ha-list-base>
|
||||
</ha-md-list>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -169,10 +168,10 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
|
||||
.skipped {
|
||||
background: var(--secondary-background-color);
|
||||
}
|
||||
ha-list-item-button {
|
||||
ha-md-list-item {
|
||||
--md-list-item-leading-icon-size: 40px;
|
||||
}
|
||||
ha-list-item-button ha-icon-next {
|
||||
ha-icon-next {
|
||||
color: var(--secondary-text-color);
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
|
||||
@@ -5,11 +5,9 @@ import { dump, JSON_SCHEMA, load } from "js-yaml";
|
||||
import type { CSSResultGroup, TemplateResult, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { until } from "lit/directives/until";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import { computeObjectId } from "../../../../common/entity/compute_object_id";
|
||||
import {
|
||||
@@ -25,7 +23,6 @@ import { showToast } from "../../../../util/toast";
|
||||
import "../../../../components/entity/ha-entity-picker";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-button-toggle-group";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/buttons/ha-progress-button";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
@@ -42,14 +39,12 @@ import {
|
||||
serviceCallWillDisconnect,
|
||||
} from "../../../../data/service";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant, ToggleButton } from "../../../../types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { documentationUrl } from "../../../../util/documentation-url";
|
||||
import { resolveMediaSource } from "../../../../data/media_source";
|
||||
import { MatchMinHeightMixin } from "../../../../mixins/match-min-height-mixin";
|
||||
import { withViewTransition } from "../../../../common/util/view-transition";
|
||||
|
||||
@customElement("developer-tools-action")
|
||||
class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
|
||||
class HaPanelDevAction extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
@@ -85,12 +80,6 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
|
||||
|
||||
@query("#yaml-editor") private _yamlEditor?: HaYamlEditor;
|
||||
|
||||
@query(".ui-mode-content") private _uiModeContent?: HTMLElement;
|
||||
|
||||
protected get matchMinHeightTarget(): HTMLElement | null {
|
||||
return this._yamlMode ? null : (this._uiModeContent ?? null);
|
||||
}
|
||||
|
||||
protected willUpdate() {
|
||||
if (
|
||||
!this.hasUpdated &&
|
||||
@@ -128,21 +117,6 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
|
||||
this._serviceData?.action
|
||||
);
|
||||
|
||||
const modeButtons: ToggleButton[] = [
|
||||
{
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.actions.ui_mode"
|
||||
),
|
||||
value: "ui",
|
||||
},
|
||||
{
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.actions.yaml_mode"
|
||||
),
|
||||
value: "yaml",
|
||||
},
|
||||
];
|
||||
|
||||
const domain = this._serviceData?.action
|
||||
? computeDomain(this._serviceData?.action)
|
||||
: undefined;
|
||||
@@ -158,34 +132,14 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
|
||||
|
||||
return html`
|
||||
<div class="content">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.actions.description"
|
||||
)}
|
||||
</p>
|
||||
<ha-card>
|
||||
<div class="card-header">
|
||||
<div class="header-row">
|
||||
<div class="header-title">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.actions.title"
|
||||
)}
|
||||
</div>
|
||||
<ha-button-toggle-group
|
||||
size="small"
|
||||
class="yaml-mode-toggle"
|
||||
.buttons=${modeButtons}
|
||||
.active=${this._yamlMode ? "yaml" : "ui"}
|
||||
.disabled=${!this._uiAvailable}
|
||||
@value-changed=${this._modeChanged}
|
||||
></ha-button-toggle-group>
|
||||
</div>
|
||||
<p class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.actions.description"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
${this._yamlMode
|
||||
? html`<div
|
||||
class="card-content"
|
||||
style=${styleMap(this._matchMinHeightStyle)}
|
||||
>
|
||||
? html`<div class="card-content">
|
||||
<ha-service-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._serviceData?.action}
|
||||
@@ -207,27 +161,44 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
|
||||
show-advanced
|
||||
show-service-id
|
||||
@value-changed=${this._serviceDataChanged}
|
||||
class="card-content ui-mode-content"
|
||||
class="card-content"
|
||||
></ha-service-control>
|
||||
`}
|
||||
${this._error !== undefined
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
<div class="card-actions">
|
||||
</ha-card>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<div class="buttons">
|
||||
<div class="switch-mode-container">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@click=${this._toggleYaml}
|
||||
.disabled=${!this._uiAvailable}
|
||||
>
|
||||
${this._yamlMode
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.actions.ui_mode"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.actions.yaml_mode"
|
||||
)}
|
||||
</ha-button>
|
||||
${!this._uiAvailable
|
||||
? html`<span class="error"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.actions.no_template_ui_support"
|
||||
)}</span
|
||||
>`
|
||||
: nothing}
|
||||
<ha-progress-button raised @click=${this._callService}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.actions.call_service"
|
||||
)}
|
||||
</ha-progress-button>
|
||||
: ""}
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-progress-button raised @click=${this._callService}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.actions.call_service"
|
||||
)}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</div>
|
||||
${this._response?.result
|
||||
? html`<div class="content response">
|
||||
@@ -468,7 +439,7 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
|
||||
}
|
||||
);
|
||||
|
||||
private async _callService(ev: Event) {
|
||||
private async _callService(ev) {
|
||||
const button = ev.currentTarget as HaProgressButton;
|
||||
|
||||
if (this._yamlMode && !this._yamlValid) {
|
||||
@@ -589,20 +560,13 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
|
||||
button.actionSuccess();
|
||||
}
|
||||
|
||||
private _modeChanged(ev: HASSDomEvent<{ value: string }>) {
|
||||
ev.stopPropagation();
|
||||
const yamlMode = ev.detail.value === "yaml";
|
||||
if (yamlMode === this._yamlMode) {
|
||||
return;
|
||||
}
|
||||
withViewTransition(() => {
|
||||
this._yamlMode = yamlMode;
|
||||
this._yamlValid = true;
|
||||
this._error = undefined;
|
||||
});
|
||||
private _toggleYaml() {
|
||||
this._yamlMode = !this._yamlMode;
|
||||
this._yamlValid = true;
|
||||
this._error = undefined;
|
||||
}
|
||||
|
||||
private _yamlChanged(ev: HASSDomEvent<{ value: any; isValid: boolean }>) {
|
||||
private _yamlChanged(ev) {
|
||||
if (!ev.detail.isValid) {
|
||||
this._yamlValid = false;
|
||||
return;
|
||||
@@ -638,7 +602,7 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private _serviceDataChanged(ev: HASSDomEvent<{ value: any }>) {
|
||||
private _serviceDataChanged(ev) {
|
||||
if (this._serviceData?.action !== ev.detail.value.action) {
|
||||
this._error = undefined;
|
||||
}
|
||||
@@ -646,7 +610,7 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
|
||||
this._checkUiSupported();
|
||||
}
|
||||
|
||||
private _serviceChanged(ev: HASSDomEvent<{ value: any }>) {
|
||||
private _serviceChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
if (ev.detail.value) {
|
||||
this._serviceData = { action: ev.detail.value, data: {} };
|
||||
@@ -703,55 +667,30 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
|
||||
max-width: 1200px;
|
||||
margin: auto;
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-1);
|
||||
.button-row {
|
||||
padding: var(--ha-space-2) var(--ha-space-4);
|
||||
border-top: 1px solid var(--divider-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
background: var(--card-background-color);
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
.header-row {
|
||||
.button-row .buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--ha-space-2);
|
||||
max-width: 1200px;
|
||||
margin: auto;
|
||||
}
|
||||
.header-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.secondary {
|
||||
margin: 0;
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
line-height: normal;
|
||||
letter-spacing: normal;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.card-content {
|
||||
.switch-mode-container {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: flex-start;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-4);
|
||||
margin: var(--ha-space-2);
|
||||
--service-control-padding: 0;
|
||||
}
|
||||
.card-content ha-yaml-editor {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.yaml-mode-toggle {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
.card-actions .error {
|
||||
flex: 1;
|
||||
color: var(--error-color);
|
||||
.switch-mode-container .error {
|
||||
margin-left: var(--ha-space-2);
|
||||
margin-inline-start: var(--ha-space-2);
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
.attributes {
|
||||
width: 100%;
|
||||
|
||||
@@ -244,13 +244,9 @@ class HaPanelDevAssist extends SubscribeMixin(LitElement) {
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-4);
|
||||
}
|
||||
.description {
|
||||
margin: 0;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
ha-textarea {
|
||||
width: 100%;
|
||||
|
||||
@@ -18,7 +18,7 @@ import "./events-list";
|
||||
class HaPanelDevEvent extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _eventType = "";
|
||||
|
||||
@@ -94,7 +94,6 @@ class HaPanelDevEvent extends LitElement {
|
||||
|
||||
<event-subscribe-card
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.selectedEventType=${this._selectedEventType}
|
||||
></event-subscribe-card>
|
||||
</div>
|
||||
@@ -159,8 +158,6 @@ class HaPanelDevEvent extends LitElement {
|
||||
padding: var(--ha-space-4);
|
||||
max-width: 1200px;
|
||||
margin: auto;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:host {
|
||||
@@ -168,26 +165,10 @@ class HaPanelDevEvent extends LitElement {
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:host([narrow]) {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
:host([narrow]) .content {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.flex {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:host([narrow]) .flex {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.inputs {
|
||||
@@ -199,19 +180,11 @@ class HaPanelDevEvent extends LitElement {
|
||||
}
|
||||
|
||||
event-subscribe-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
display: block;
|
||||
margin-top: var(--ha-space-4);
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
:host([narrow]) event-subscribe-card {
|
||||
flex: none;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
@@ -1,39 +1,21 @@
|
||||
import {
|
||||
mdiChevronDoubleLeft,
|
||||
mdiChevronDoubleRight,
|
||||
mdiChevronLeft,
|
||||
mdiChevronRight,
|
||||
mdiInformationOutline,
|
||||
} from "@mdi/js";
|
||||
import type { HassEvent } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { formatTimeWithSeconds } from "../../../../common/datetime/format_time";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { formatTime } from "../../../../common/datetime/format_time";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-tooltip";
|
||||
import "../../../../components/ha-yaml-editor";
|
||||
import "../../../../components/input/ha-input";
|
||||
import type { HaInput } from "../../../../components/input/ha-input";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
|
||||
const MAX_BUFFERED_EVENTS = 100;
|
||||
|
||||
interface SubscribedEvent {
|
||||
id: number;
|
||||
event: HassEvent;
|
||||
}
|
||||
|
||||
@customElement("event-subscribe-card")
|
||||
class EventSubscribeCard extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public selectedEventType = "";
|
||||
|
||||
@state() private _eventType = "";
|
||||
@@ -42,12 +24,13 @@ class EventSubscribeCard extends LitElement {
|
||||
|
||||
@state() private _eventFilter = "";
|
||||
|
||||
@state() private _events: SubscribedEvent[] = [];
|
||||
@state() private _events: {
|
||||
id: number;
|
||||
event: HassEvent;
|
||||
}[] = [];
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _viewedEventId?: number;
|
||||
|
||||
private _eventCount = 0;
|
||||
|
||||
@state() _ignoredEventsCount = 0;
|
||||
@@ -130,161 +113,43 @@ class EventSubscribeCard extends LitElement {
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
${this._renderEventsCard()}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderEventsCard(): TemplateResult {
|
||||
if (!this._events.length) {
|
||||
const message = this._subscribed
|
||||
? this.hass!.localize(
|
||||
"ui.panel.config.developer-tools.tabs.events.waiting_for_events"
|
||||
)
|
||||
: this.hass!.localize(
|
||||
"ui.panel.config.developer-tools.tabs.events.subscribe_prompt"
|
||||
);
|
||||
return html`
|
||||
<ha-card class="events-card">
|
||||
<div class="empty-state">${message}</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
const bufferTotal = this._events.length;
|
||||
const index = this._resolveViewedIndex();
|
||||
const event = this._events[index];
|
||||
const position = event.id + 1;
|
||||
|
||||
const bufferPosition = bufferTotal - index;
|
||||
const atNewest = index === 0;
|
||||
|
||||
const hasRolledOver = this._events[bufferTotal - 1].id > 0;
|
||||
|
||||
return html`
|
||||
<ha-card class="events-card">
|
||||
<div class="events-toolbar">
|
||||
<ha-icon-button
|
||||
.path=${mdiChevronDoubleLeft}
|
||||
.disabled=${index >= bufferTotal - 1}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.developer-tools.tabs.events.oldest_event"
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<div class="events">
|
||||
${repeat(
|
||||
this._events,
|
||||
(event) => event.id,
|
||||
(event) => html`
|
||||
<div class="event">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.developer-tools.tabs.events.event_fired",
|
||||
{ name: event.id }
|
||||
)}
|
||||
${formatTime(
|
||||
new Date(event.event.time_fired),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
)}:
|
||||
<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${event.event}
|
||||
read-only
|
||||
></ha-yaml-editor>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
@click=${this._showOldest}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
.path=${mdiChevronLeft}
|
||||
.disabled=${index >= bufferTotal - 1}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.developer-tools.tabs.events.older_event"
|
||||
)}
|
||||
@click=${this._showOlder}
|
||||
></ha-icon-button>
|
||||
<div class="event-info">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.developer-tools.tabs.events.event_fired",
|
||||
{
|
||||
name: position,
|
||||
time: formatTimeWithSeconds(
|
||||
new Date(event.event.time_fired),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
),
|
||||
}
|
||||
)}
|
||||
<span class="counter">(${bufferPosition} / ${bufferTotal})</span>
|
||||
${hasRolledOver
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
id="buffer-info"
|
||||
class="buffer-info"
|
||||
.path=${mdiInformationOutline}
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip for="buffer-info" placement="bottom">
|
||||
<span class="buffer-tooltip">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.developer-tools.tabs.events.buffer_disclaimer",
|
||||
{ count: MAX_BUFFERED_EVENTS }
|
||||
)}
|
||||
</span>
|
||||
</ha-tooltip>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-icon-button
|
||||
.path=${mdiChevronRight}
|
||||
.disabled=${atNewest}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.developer-tools.tabs.events.newer_event"
|
||||
)}
|
||||
@click=${this._showNewer}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
.path=${mdiChevronDoubleRight}
|
||||
.disabled=${atNewest}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.developer-tools.tabs.events.newest_event"
|
||||
)}
|
||||
@click=${this._showNewest}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.value=${event.event}
|
||||
auto-update
|
||||
read-only
|
||||
></ha-yaml-editor>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _resolveViewedIndex(): number {
|
||||
if (this._viewedEventId === undefined) {
|
||||
return 0;
|
||||
}
|
||||
const found = this._events.findIndex((e) => e.id === this._viewedEventId);
|
||||
|
||||
// Fall back to the oldest available event when the viewed one has aged out.
|
||||
return found === -1 ? this._events.length - 1 : found;
|
||||
}
|
||||
|
||||
private _showOldest() {
|
||||
if (!this._events.length) {
|
||||
return;
|
||||
}
|
||||
this._viewedEventId = this._events[this._events.length - 1].id;
|
||||
}
|
||||
|
||||
private _showOlder() {
|
||||
if (!this._events.length) {
|
||||
return;
|
||||
}
|
||||
const next = Math.min(
|
||||
this._resolveViewedIndex() + 1,
|
||||
this._events.length - 1
|
||||
);
|
||||
this._viewedEventId = this._events[next].id;
|
||||
}
|
||||
|
||||
private _showNewest() {
|
||||
if (!this._events.length) {
|
||||
return;
|
||||
}
|
||||
this._viewedEventId = this._events[0].id;
|
||||
}
|
||||
|
||||
private _showNewer() {
|
||||
if (!this._events.length) {
|
||||
return;
|
||||
}
|
||||
const next = Math.max(this._resolveViewedIndex() - 1, 0);
|
||||
this._viewedEventId = this._events[next].id;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: InputEvent) {
|
||||
private _valueChanged(ev: InputEvent): void {
|
||||
this._eventType = (ev.target as HaInput).value ?? "";
|
||||
this._error = undefined;
|
||||
}
|
||||
|
||||
private _filterChanged(ev: InputEvent) {
|
||||
private _filterChanged(ev: InputEvent): void {
|
||||
this._eventFilter = (ev.target as HaInput).value ?? "";
|
||||
}
|
||||
|
||||
@@ -295,7 +160,7 @@ class EventSubscribeCard extends LitElement {
|
||||
|
||||
const searchStr = this._eventFilter;
|
||||
|
||||
function visit(node: unknown) {
|
||||
function visit(node) {
|
||||
// Handle primitives directly
|
||||
if (node === null || typeof node !== "object") {
|
||||
return String(node).includes(searchStr);
|
||||
@@ -338,116 +203,55 @@ class EventSubscribeCard extends LitElement {
|
||||
return;
|
||||
}
|
||||
const tail =
|
||||
this._events.length >= MAX_BUFFERED_EVENTS
|
||||
? this._events.slice(0, MAX_BUFFERED_EVENTS - 1)
|
||||
this._events.length > 30
|
||||
? this._events.slice(0, 29)
|
||||
: this._events;
|
||||
const id = this._eventCount++;
|
||||
this._events = [
|
||||
{
|
||||
event,
|
||||
id,
|
||||
id: this._eventCount++,
|
||||
},
|
||||
...tail,
|
||||
];
|
||||
if (this._viewedEventId === undefined) {
|
||||
this._viewedEventId = id;
|
||||
}
|
||||
}, this._eventType);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
this._error = this.hass!.localize(
|
||||
"ui.panel.config.developer-tools.tabs.events.subscribe_failed",
|
||||
{
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: this.hass!.localize(
|
||||
"ui.panel.config.developer-tools.tabs.events.unknown_error"
|
||||
),
|
||||
}
|
||||
{ error: error.message || "Unknown error" }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _clearEvents() {
|
||||
private _clearEvents(): void {
|
||||
this._events = [];
|
||||
this._eventCount = 0;
|
||||
this._ignoredEventsCount = 0;
|
||||
this._error = undefined;
|
||||
this._viewedEventId = undefined;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
ha-input {
|
||||
margin-bottom: var(--ha-space-2);
|
||||
}
|
||||
.error-message {
|
||||
margin-top: var(--ha-space-2);
|
||||
}
|
||||
.event {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
padding-top: var(--ha-space-2);
|
||||
padding-bottom: var(--ha-space-2);
|
||||
margin: var(--ha-space-4) 0;
|
||||
}
|
||||
.event:last-child {
|
||||
border-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
pre {
|
||||
font-family: var(--ha-font-family-code);
|
||||
}
|
||||
ha-card {
|
||||
margin-bottom: var(--ha-space-2);
|
||||
}
|
||||
.events-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 620px;
|
||||
padding: var(--ha-space-2);
|
||||
}
|
||||
:host([narrow]) .events-card {
|
||||
height: auto;
|
||||
min-height: 360px;
|
||||
}
|
||||
.events-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--ha-space-8);
|
||||
color: var(--primary-text-color);
|
||||
text-align: center;
|
||||
font-size: var(--ha-font-size-xl);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
}
|
||||
.event-info {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: var(--ha-font-size-m);
|
||||
}
|
||||
.counter {
|
||||
color: var(--secondary-text-color);
|
||||
margin-left: var(--ha-space-2);
|
||||
}
|
||||
.buffer-info {
|
||||
color: var(--secondary-text-color);
|
||||
margin-left: var(--ha-space-1);
|
||||
vertical-align: middle;
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
.buffer-tooltip {
|
||||
white-space: pre-line;
|
||||
display: block;
|
||||
max-width: 320px;
|
||||
}
|
||||
ha-yaml-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
margin-top: var(--ha-space-2);
|
||||
--code-mirror-height: 100%;
|
||||
margin-bottom: var(--ha-space-1);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/radio/ha-radio-group";
|
||||
import type { HaRadioGroup } from "../../../../components/radio/ha-radio-group";
|
||||
import "../../../../components/radio/ha-radio-option";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-formfield";
|
||||
import "../../../../components/ha-radio";
|
||||
import {
|
||||
clearStatistics,
|
||||
getStatisticLabel,
|
||||
@@ -80,26 +79,37 @@ export class DialogStatisticsFixUnitsChanged extends LitElement {
|
||||
)}
|
||||
</p>
|
||||
|
||||
<ha-radio-group
|
||||
.label=${this.hass.localize(
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.units_changed.how_to_fix"
|
||||
)}
|
||||
.value=${this._action}
|
||||
name="action"
|
||||
@change=${this._handleActionChanged}
|
||||
</h3>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.units_changed.update",
|
||||
this._params.issue.data
|
||||
)}
|
||||
>
|
||||
<ha-radio-option value="update" autofocus>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.statistics.fix_issue.units_changed.update",
|
||||
this._params.issue.data
|
||||
)}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="clear">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.developer-tools.tabs.statistics.fix_issue.units_changed.clear`
|
||||
)}
|
||||
</ha-radio-option>
|
||||
</ha-radio-group>
|
||||
<ha-radio
|
||||
value="update"
|
||||
name="action"
|
||||
.checked=${this._action === "update"}
|
||||
@change=${this._handleActionChanged}
|
||||
autofocus
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.developer-tools.tabs.statistics.fix_issue.units_changed.clear`
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="clear"
|
||||
name="action"
|
||||
.checked=${this._action === "clear"}
|
||||
@change=${this._handleActionChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
@@ -119,10 +129,8 @@ export class DialogStatisticsFixUnitsChanged extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleActionChanged(ev: Event): void {
|
||||
this._action = (ev.currentTarget as HaRadioGroup).value as
|
||||
| "update"
|
||||
| "clear";
|
||||
private _handleActionChanged(ev): void {
|
||||
this._action = ev.target.value;
|
||||
}
|
||||
|
||||
private _cancel(): void {
|
||||
@@ -146,15 +154,7 @@ export class DialogStatisticsFixUnitsChanged extends LitElement {
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-radio-group::part(form-control-label) {
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
`,
|
||||
];
|
||||
return [haStyle, haStyleDialog];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,11 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { debounce } from "../../../../common/util/debounce";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-code-editor";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-spinner";
|
||||
import "../../../../components/ha-tip";
|
||||
import type { RenderTemplateResult } from "../../../../data/ws-templates";
|
||||
@@ -56,8 +54,6 @@ class HaPanelDevTemplate extends LitElement {
|
||||
|
||||
@state() private _unsubRenderTemplate?: Promise<UnsubscribeFunc>;
|
||||
|
||||
@state() private _descriptionExpanded = false;
|
||||
|
||||
private _template = "";
|
||||
|
||||
private _inited = false;
|
||||
@@ -92,58 +88,47 @@ class HaPanelDevTemplate extends LitElement {
|
||||
? "list"
|
||||
: "dict"
|
||||
: type;
|
||||
|
||||
return html`
|
||||
<div class="content">
|
||||
<ha-expansion-panel
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.templates.about"
|
||||
)}
|
||||
outlined
|
||||
.expanded=${this._descriptionExpanded}
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div class="description">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.templates.description"
|
||||
)}
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
href="https://jinja.palletsprojects.com/en/latest/templates/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.templates.jinja_documentation"
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
"/docs/configuration/templating/"
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.templates.template_extensions"
|
||||
)}</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
<div class="description">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.templates.description"
|
||||
)}
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
href="https://jinja.palletsprojects.com/en/latest/templates/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.templates.jinja_documentation"
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
"/docs/configuration/templating/"
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.templates.template_extensions"
|
||||
)}</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="content ${classMap({
|
||||
layout: !this.narrow,
|
||||
horizontal: !this.narrow,
|
||||
})}"
|
||||
style="--description-expanded: ${this._descriptionExpanded ? 1 : 0}"
|
||||
>
|
||||
<ha-card
|
||||
class="edit-pane"
|
||||
@@ -289,12 +274,6 @@ ${type === "object"
|
||||
`;
|
||||
}
|
||||
|
||||
private _expandedChanged(
|
||||
ev: HASSDomEvent<HASSDomEvents["expanded-changed"]>
|
||||
) {
|
||||
this._descriptionExpanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
@@ -309,41 +288,13 @@ ${type === "object"
|
||||
padding: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.content:has(ha-expansion-panel) {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.content.horizontal {
|
||||
--panel-header-height: calc(
|
||||
var(--header-height) + 1em * 2 + var(--ha-line-height-normal) *
|
||||
var(--ha-font-size-m) + 1px + 2px
|
||||
);
|
||||
--description-pane-height: calc(
|
||||
var(--ha-space-4) + 48px +
|
||||
(
|
||||
var(--ha-line-height-normal) * var(--ha-font-size-m) * 3 +
|
||||
var(--ha-space-1) * 2
|
||||
) *
|
||||
var(--description-expanded) + var(--ha-card-border-width, 1px) * 2
|
||||
);
|
||||
--card-header-height: calc(
|
||||
var(--ha-space-3) + var(--ha-space-4) +
|
||||
var(--ha-line-height-expanded) *
|
||||
var(--ha-card-header-font-size, var(--ha-font-size-2xl))
|
||||
);
|
||||
--card-actions-height: calc(1px + var(--ha-space-2) * 2 + 40px);
|
||||
--edit-pane-height: calc(
|
||||
100vh - var(--panel-header-height) - var(
|
||||
--description-pane-height
|
||||
) - var(--ha-space-4) *
|
||||
2
|
||||
);
|
||||
--code-mirror-max-height: calc(
|
||||
var(--edit-pane-height) - var(--card-header-height) +
|
||||
var(--ha-space-2) - var(--card-actions-height) - var(
|
||||
--ha-space-4
|
||||
) - var(--ha-card-border-width, 1px) *
|
||||
2
|
||||
100vh - var(--header-height) -
|
||||
(var(--ha-line-height-normal) * var(--ha-font-size-m) * 3) -
|
||||
(max(16px, var(--safe-area-inset-top)) * 2) -
|
||||
(max(16px, var(--safe-area-inset-bottom)) * 2) -
|
||||
(var(--ha-card-border-width, 1px) * 3) - (1em * 2) - 192px
|
||||
);
|
||||
}
|
||||
|
||||
@@ -394,13 +345,6 @@ ${type === "object"
|
||||
ul {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
.description > p {
|
||||
margin-block-start: 0;
|
||||
}
|
||||
.description > ul {
|
||||
margin-block-start: var(--ha-space-1);
|
||||
margin-block-end: var(--ha-space-1);
|
||||
}
|
||||
|
||||
.render-pane .card-content {
|
||||
user-select: text;
|
||||
|
||||
@@ -7,6 +7,7 @@ import "../../../../components/entity/ha-statistic-picker";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-radio";
|
||||
import "../../../../components/ha-select";
|
||||
import type { HaSelectSelectEvent } from "../../../../components/ha-select";
|
||||
import "../../../../components/input/ha-input";
|
||||
|
||||
@@ -7,6 +7,7 @@ import "../../../../components/entity/ha-statistic-picker";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-radio";
|
||||
import "../../../../components/ha-select";
|
||||
import type {
|
||||
HaSelectOption,
|
||||
|
||||
@@ -7,10 +7,10 @@ import "../../../../components/entity/ha-statistic-picker";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-formfield";
|
||||
import "../../../../components/ha-markdown";
|
||||
import "../../../../components/radio/ha-radio-group";
|
||||
import type { HaRadioGroup } from "../../../../components/radio/ha-radio-group";
|
||||
import "../../../../components/radio/ha-radio-option";
|
||||
import "../../../../components/ha-radio";
|
||||
import type { HaRadio } from "../../../../components/ha-radio";
|
||||
import "../../../../components/input/ha-input";
|
||||
import type { GasSourceTypeEnergyPreference } from "../../../../data/energy";
|
||||
import {
|
||||
@@ -197,31 +197,34 @@ export class DialogEnergyGasSettings
|
||||
)}
|
||||
></ha-statistic-picker>
|
||||
|
||||
<ha-radio-group
|
||||
<p>
|
||||
${this.hass.localize("ui.panel.config.energy.gas.dialog.cost_para")}
|
||||
</p>
|
||||
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.gas.dialog.cost_para"
|
||||
"ui.panel.config.energy.gas.dialog.no_cost"
|
||||
)}
|
||||
.value=${this._costs}
|
||||
name="costs"
|
||||
@change=${this._handleCostChanged}
|
||||
>
|
||||
<ha-radio-option value="no-costs">
|
||||
${this.hass.localize("ui.panel.config.energy.gas.dialog.no_cost")}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="statistic">
|
||||
${this.hass.localize("ui.panel.config.energy.gas.dialog.cost_stat")}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="entity" .disabled=${externalSource}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.gas.dialog.cost_entity"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="number" .disabled=${externalSource}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.gas.dialog.cost_number"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
</ha-radio-group>
|
||||
<ha-radio
|
||||
value="no-costs"
|
||||
name="costs"
|
||||
.checked=${this._costs === "no-costs"}
|
||||
@change=${this._handleCostChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.gas.dialog.cost_stat"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="statistic"
|
||||
name="costs"
|
||||
.checked=${this._costs === "statistic"}
|
||||
@change=${this._handleCostChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
${this._costs === "statistic"
|
||||
? html`<ha-statistic-picker
|
||||
class="price-options"
|
||||
@@ -233,59 +236,87 @@ export class DialogEnergyGasSettings
|
||||
)} (${this.hass.config.currency})`}
|
||||
@value-changed=${this._priceStatChanged}
|
||||
></ha-statistic-picker>`
|
||||
: this._costs === "entity"
|
||||
? html`<ha-entity-picker
|
||||
class="price-options"
|
||||
.hass=${this.hass}
|
||||
include-domains='["sensor", "input_number"]'
|
||||
.value=${this._source.entity_energy_price}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.gas.dialog.cost_entity_input"
|
||||
)}
|
||||
.helper=${pickedUnitClass
|
||||
? html`<ha-markdown
|
||||
.content=${this.hass.localize(
|
||||
"ui.panel.config.energy.gas.dialog.cost_entity_helper",
|
||||
pickedUnitClass === "energy"
|
||||
? {
|
||||
currency: this.hass.config.currency,
|
||||
class: this.hass.localize(
|
||||
"ui.panel.config.energy.gas.dialog.cost_entity_helper_energy"
|
||||
),
|
||||
unit1: "kWh",
|
||||
unit2: "Wh",
|
||||
}
|
||||
: {
|
||||
currency: this.hass.config.currency,
|
||||
class: this.hass.localize(
|
||||
"ui.panel.config.energy.gas.dialog.cost_entity_helper_volume"
|
||||
),
|
||||
unit1: "m³",
|
||||
unit2: "ft³",
|
||||
}
|
||||
)}
|
||||
></ha-markdown>`
|
||||
: nothing}
|
||||
@value-changed=${this._priceEntityChanged}
|
||||
></ha-entity-picker>`
|
||||
: this._costs === "number"
|
||||
? html`<ha-input
|
||||
.label=${`${this.hass.localize(
|
||||
"ui.panel.config.energy.gas.dialog.cost_number_input"
|
||||
)} ${unitPrice ? ` (${unitPrice})` : ""}`}
|
||||
class="price-options"
|
||||
step="any"
|
||||
type="number"
|
||||
.value=${this._source.number_energy_price !== null
|
||||
? String(this._source.number_energy_price)
|
||||
: ""}
|
||||
@change=${this._numberPriceChanged}
|
||||
>
|
||||
${unitPrice
|
||||
? html`<span slot="end">${unitPrice}</span>`
|
||||
: nothing}
|
||||
</ha-input>`
|
||||
: nothing}
|
||||
: ""}
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.gas.dialog.cost_entity"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="entity"
|
||||
name="costs"
|
||||
.checked=${this._costs === "entity"}
|
||||
.disabled=${externalSource}
|
||||
@change=${this._handleCostChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
${this._costs === "entity"
|
||||
? html`<ha-entity-picker
|
||||
class="price-options"
|
||||
.hass=${this.hass}
|
||||
include-domains='["sensor", "input_number"]'
|
||||
.value=${this._source.entity_energy_price}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.gas.dialog.cost_entity_input"
|
||||
)}
|
||||
.helper=${pickedUnitClass
|
||||
? html`<ha-markdown
|
||||
.content=${this.hass.localize(
|
||||
"ui.panel.config.energy.gas.dialog.cost_entity_helper",
|
||||
pickedUnitClass === "energy"
|
||||
? {
|
||||
currency: this.hass.config.currency,
|
||||
class: this.hass.localize(
|
||||
"ui.panel.config.energy.gas.dialog.cost_entity_helper_energy"
|
||||
),
|
||||
unit1: "kWh",
|
||||
unit2: "Wh",
|
||||
}
|
||||
: {
|
||||
currency: this.hass.config.currency,
|
||||
class: this.hass.localize(
|
||||
"ui.panel.config.energy.gas.dialog.cost_entity_helper_volume"
|
||||
),
|
||||
unit1: "m³",
|
||||
unit2: "ft³",
|
||||
}
|
||||
)}
|
||||
></ha-markdown>`
|
||||
: nothing}
|
||||
@value-changed=${this._priceEntityChanged}
|
||||
></ha-entity-picker>`
|
||||
: ""}
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.gas.dialog.cost_number"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="number"
|
||||
name="costs"
|
||||
.checked=${this._costs === "number"}
|
||||
.disabled=${externalSource}
|
||||
@change=${this._handleCostChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
${this._costs === "number"
|
||||
? html`<ha-input
|
||||
.label=${`${this.hass.localize(
|
||||
"ui.panel.config.energy.gas.dialog.cost_number_input"
|
||||
)} ${unitPrice ? ` (${unitPrice})` : ""}`}
|
||||
class="price-options"
|
||||
step="any"
|
||||
type="number"
|
||||
.value=${this._source.number_energy_price !== null
|
||||
? String(this._source.number_energy_price)
|
||||
: ""}
|
||||
@change=${this._numberPriceChanged}
|
||||
>
|
||||
${unitPrice
|
||||
? html`<span slot="end">${unitPrice}</span>`
|
||||
: nothing}
|
||||
</ha-input>`
|
||||
: ""}
|
||||
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
@@ -307,12 +338,9 @@ export class DialogEnergyGasSettings
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleCostChanged(ev: Event) {
|
||||
this._costs = (ev.currentTarget as HaRadioGroup).value as
|
||||
| "no-costs"
|
||||
| "number"
|
||||
| "entity"
|
||||
| "statistic";
|
||||
private _handleCostChanged(ev: CustomEvent) {
|
||||
const input = ev.currentTarget as HaRadio;
|
||||
this._costs = input.value as any;
|
||||
}
|
||||
|
||||
private _numberPriceChanged(ev: InputEvent) {
|
||||
@@ -392,12 +420,15 @@ export class DialogEnergyGasSettings
|
||||
display: block;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
ha-radio-group {
|
||||
margin-top: var(--ha-space-4);
|
||||
ha-formfield {
|
||||
display: block;
|
||||
}
|
||||
.price-options {
|
||||
display: block;
|
||||
margin-top: var(--ha-space-3);
|
||||
padding-left: 52px;
|
||||
padding-inline-start: 52px;
|
||||
padding-inline-end: initial;
|
||||
margin-top: -8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -7,9 +7,9 @@ import "../../../../components/entity/ha-statistic-picker";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/radio/ha-radio-group";
|
||||
import type { HaRadioGroup } from "../../../../components/radio/ha-radio-group";
|
||||
import "../../../../components/radio/ha-radio-option";
|
||||
import "../../../../components/ha-formfield";
|
||||
import "../../../../components/ha-radio";
|
||||
import type { HaRadio } from "../../../../components/ha-radio";
|
||||
import "../../../../components/input/ha-input";
|
||||
import type {
|
||||
GridSourceTypeEnergyPreference,
|
||||
@@ -234,32 +234,56 @@ export class DialogEnergyGridSettings
|
||||
)}
|
||||
</p>
|
||||
|
||||
<ha-radio-group
|
||||
.value=${this._importCostType}
|
||||
name="importCostType"
|
||||
@change=${this._handleImportCostTypeChanged}
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.no_cost_tracking"
|
||||
)}
|
||||
>
|
||||
<ha-radio-option value="no_cost">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.no_cost_tracking"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="stat">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.cost_stat"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="entity" .disabled=${externalImportSource}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.cost_entity"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="number" .disabled=${externalImportSource}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.cost_number"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
</ha-radio-group>
|
||||
<ha-radio
|
||||
value="no_cost"
|
||||
name="importCostType"
|
||||
.checked=${this._importCostType === "no_cost"}
|
||||
@change=${this._handleImportCostTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.cost_stat"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="stat"
|
||||
name="importCostType"
|
||||
.checked=${this._importCostType === "stat"}
|
||||
@change=${this._handleImportCostTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.cost_entity"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="entity"
|
||||
name="importCostType"
|
||||
.checked=${this._importCostType === "entity"}
|
||||
.disabled=${externalImportSource}
|
||||
@change=${this._handleImportCostTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.cost_number"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="number"
|
||||
name="importCostType"
|
||||
.checked=${this._importCostType === "number"}
|
||||
.disabled=${externalImportSource}
|
||||
@change=${this._handleImportCostTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
|
||||
${this._importCostType === "stat"
|
||||
? html`
|
||||
@@ -316,38 +340,56 @@ export class DialogEnergyGridSettings
|
||||
)}
|
||||
</p>
|
||||
|
||||
<ha-radio-group
|
||||
.value=${this._exportCostType}
|
||||
name="exportCostType"
|
||||
@change=${this._handleExportCostTypeChanged}
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.no_compensation_tracking"
|
||||
)}
|
||||
>
|
||||
<ha-radio-option value="no_cost">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.no_compensation_tracking"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="stat">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.compensation_stat"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option
|
||||
<ha-radio
|
||||
value="no_cost"
|
||||
name="exportCostType"
|
||||
.checked=${this._exportCostType === "no_cost"}
|
||||
@change=${this._handleExportCostTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.compensation_stat"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="stat"
|
||||
name="exportCostType"
|
||||
.checked=${this._exportCostType === "stat"}
|
||||
@change=${this._handleExportCostTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.compensation_entity"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="entity"
|
||||
name="exportCostType"
|
||||
.checked=${this._exportCostType === "entity"}
|
||||
.disabled=${externalExportSource}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.compensation_entity"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option
|
||||
@change=${this._handleExportCostTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.compensation_number"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="number"
|
||||
name="exportCostType"
|
||||
.checked=${this._exportCostType === "number"}
|
||||
.disabled=${externalExportSource}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.compensation_number"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
</ha-radio-group>
|
||||
@change=${this._handleExportCostTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
|
||||
${this._exportCostType === "stat"
|
||||
? html`
|
||||
@@ -492,7 +534,8 @@ export class DialogEnergyGridSettings
|
||||
}
|
||||
|
||||
private _handleImportCostTypeChanged(ev: Event) {
|
||||
this._importCostType = (ev.currentTarget as HaRadioGroup).value as CostType;
|
||||
const input = ev.currentTarget as HaRadio;
|
||||
this._importCostType = input.value as CostType;
|
||||
// Clear other cost fields when switching types
|
||||
this._source = {
|
||||
...this._source!,
|
||||
@@ -503,7 +546,8 @@ export class DialogEnergyGridSettings
|
||||
}
|
||||
|
||||
private _handleExportCostTypeChanged(ev: Event) {
|
||||
this._exportCostType = (ev.currentTarget as HaRadioGroup).value as CostType;
|
||||
const input = ev.currentTarget as HaRadio;
|
||||
this._exportCostType = input.value as CostType;
|
||||
// Clear other cost fields when switching types
|
||||
this._source = {
|
||||
...this._source!,
|
||||
@@ -603,8 +647,8 @@ export class DialogEnergyGridSettings
|
||||
ha-input:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
ha-radio-group {
|
||||
margin-bottom: var(--ha-space-4);
|
||||
ha-formfield {
|
||||
display: block;
|
||||
}
|
||||
.section-label {
|
||||
margin-top: var(--ha-space-4);
|
||||
|
||||
@@ -9,10 +9,10 @@ import "../../../../components/ha-checkbox";
|
||||
import type { HaCheckbox } from "../../../../components/ha-checkbox";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-formfield";
|
||||
import "../../../../components/ha-radio";
|
||||
import type { HaRadio } from "../../../../components/ha-radio";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/radio/ha-radio-group";
|
||||
import type { HaRadioGroup } from "../../../../components/radio/ha-radio-group";
|
||||
import "../../../../components/radio/ha-radio-option";
|
||||
import type { ConfigEntry } from "../../../../data/config_entries";
|
||||
import { getConfigEntries } from "../../../../data/config_entries";
|
||||
import type { SolarSourceTypeEnergyPreference } from "../../../../data/energy";
|
||||
@@ -156,22 +156,30 @@ export class DialogEnergySolarSettings
|
||||
)}
|
||||
</p>
|
||||
|
||||
<ha-radio-group
|
||||
.value=${this._forecast ? "true" : "false"}
|
||||
name="forecast"
|
||||
@change=${this._handleForecastChanged}
|
||||
<ha-formfield
|
||||
label=${this.hass.localize(
|
||||
"ui.panel.config.energy.solar.dialog.dont_forecast_production"
|
||||
)}
|
||||
>
|
||||
<ha-radio-option value="false">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.solar.dialog.dont_forecast_production"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="true">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.solar.dialog.forecast_production"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
</ha-radio-group>
|
||||
<ha-radio
|
||||
value="false"
|
||||
name="forecast"
|
||||
.checked=${!this._forecast}
|
||||
@change=${this._handleForecastChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
label=${this.hass.localize(
|
||||
"ui.panel.config.energy.solar.dialog.forecast_production"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="true"
|
||||
name="forecast"
|
||||
.checked=${this._forecast}
|
||||
@change=${this._handleForecastChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
${this._forecast
|
||||
? html`<div class="forecast-options">
|
||||
${this._configEntries?.map(
|
||||
@@ -249,8 +257,9 @@ export class DialogEnergySolarSettings
|
||||
);
|
||||
}
|
||||
|
||||
private _handleForecastChanged(ev: Event) {
|
||||
this._forecast = (ev.currentTarget as HaRadioGroup).value === "true";
|
||||
private _handleForecastChanged(ev: CustomEvent) {
|
||||
const input = ev.currentTarget as HaRadio;
|
||||
this._forecast = input.value === "true";
|
||||
}
|
||||
|
||||
private _forecastCheckChanged(ev) {
|
||||
@@ -320,18 +329,20 @@ export class DialogEnergySolarSettings
|
||||
margin-inline-end: 16px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
ha-formfield {
|
||||
display: block;
|
||||
}
|
||||
ha-statistic-picker {
|
||||
width: 100%;
|
||||
}
|
||||
ha-radio-group {
|
||||
margin-bottom: var(--ha-space-3);
|
||||
}
|
||||
.forecast-options {
|
||||
padding-left: 32px;
|
||||
padding-inline-start: 32px;
|
||||
padding-inline-end: initial;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
gap: var(--ha-space-2);
|
||||
margin-inline-start: var(--ha-space-3);
|
||||
}
|
||||
.forecast-options ha-button {
|
||||
margin-top: var(--ha-space-4);
|
||||
|
||||
@@ -6,11 +6,11 @@ import "../../../../components/entity/ha-entity-picker";
|
||||
import "../../../../components/entity/ha-statistic-picker";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-formfield";
|
||||
import "../../../../components/ha-radio";
|
||||
import "../../../../components/ha-markdown";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/radio/ha-radio-group";
|
||||
import type { HaRadioGroup } from "../../../../components/radio/ha-radio-group";
|
||||
import "../../../../components/radio/ha-radio-option";
|
||||
import type { HaRadio } from "../../../../components/ha-radio";
|
||||
import "../../../../components/input/ha-input";
|
||||
import type { WaterSourceTypeEnergyPreference } from "../../../../data/energy";
|
||||
import {
|
||||
@@ -155,33 +155,34 @@ export class DialogEnergyWaterSettings
|
||||
)}
|
||||
></ha-statistic-picker>
|
||||
|
||||
<ha-radio-group
|
||||
<p>
|
||||
${this.hass.localize("ui.panel.config.energy.water.dialog.cost_para")}
|
||||
</p>
|
||||
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.water.dialog.cost_para"
|
||||
"ui.panel.config.energy.water.dialog.no_cost"
|
||||
)}
|
||||
.value=${this._costs}
|
||||
name="costs"
|
||||
@change=${this._handleCostChanged}
|
||||
>
|
||||
<ha-radio-option value="no-costs">
|
||||
${this.hass.localize("ui.panel.config.energy.water.dialog.no_cost")}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="statistic">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.water.dialog.cost_stat"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="entity" .disabled=${externalSource}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.water.dialog.cost_entity"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="number" .disabled=${externalSource}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.energy.water.dialog.cost_number"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
</ha-radio-group>
|
||||
<ha-radio
|
||||
value="no-costs"
|
||||
name="costs"
|
||||
.checked=${this._costs === "no-costs"}
|
||||
@change=${this._handleCostChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.water.dialog.cost_stat"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="statistic"
|
||||
name="costs"
|
||||
.checked=${this._costs === "statistic"}
|
||||
@change=${this._handleCostChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
${this._costs === "statistic"
|
||||
? html`<ha-statistic-picker
|
||||
class="price-options"
|
||||
@@ -193,39 +194,67 @@ export class DialogEnergyWaterSettings
|
||||
)} (${this.hass.config.currency})`}
|
||||
@value-changed=${this._priceStatChanged}
|
||||
></ha-statistic-picker>`
|
||||
: this._costs === "entity"
|
||||
? html`<ha-entity-picker
|
||||
class="price-options"
|
||||
.hass=${this.hass}
|
||||
include-domains='["sensor", "input_number"]'
|
||||
.value=${this._source.entity_energy_price}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.water.dialog.cost_entity_input"
|
||||
: ""}
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.water.dialog.cost_entity"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="entity"
|
||||
name="costs"
|
||||
.checked=${this._costs === "entity"}
|
||||
.disabled=${externalSource}
|
||||
@change=${this._handleCostChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
${this._costs === "entity"
|
||||
? html`<ha-entity-picker
|
||||
class="price-options"
|
||||
.hass=${this.hass}
|
||||
include-domains='["sensor", "input_number"]'
|
||||
.value=${this._source.entity_energy_price}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.water.dialog.cost_entity_input"
|
||||
)}
|
||||
.helper=${html`<ha-markdown
|
||||
.content=${this.hass.localize(
|
||||
"ui.panel.config.energy.water.dialog.cost_entity_helper",
|
||||
{ currency: this.hass.config.currency }
|
||||
)}
|
||||
.helper=${html`<ha-markdown
|
||||
.content=${this.hass.localize(
|
||||
"ui.panel.config.energy.water.dialog.cost_entity_helper",
|
||||
{ currency: this.hass.config.currency }
|
||||
)}
|
||||
></ha-markdown>`}
|
||||
@value-changed=${this._priceEntityChanged}
|
||||
></ha-entity-picker>`
|
||||
: this._costs === "number"
|
||||
? html`<ha-input
|
||||
.label=${`${this.hass.localize(
|
||||
"ui.panel.config.energy.water.dialog.cost_number_input"
|
||||
)} (${unitPriceFixed})`}
|
||||
class="price-options"
|
||||
step="any"
|
||||
type="number"
|
||||
.value=${this._source.number_energy_price !== null
|
||||
? String(this._source.number_energy_price)
|
||||
: ""}
|
||||
@change=${this._numberPriceChanged}
|
||||
>
|
||||
<span slot="end">${unitPriceFixed}</span>
|
||||
</ha-input>`
|
||||
: nothing}
|
||||
></ha-markdown>`}
|
||||
@value-changed=${this._priceEntityChanged}
|
||||
></ha-entity-picker>`
|
||||
: ""}
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.water.dialog.cost_number"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="number"
|
||||
name="costs"
|
||||
.checked=${this._costs === "number"}
|
||||
.disabled=${externalSource}
|
||||
@change=${this._handleCostChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
${this._costs === "number"
|
||||
? html`<ha-input
|
||||
.label=${`${this.hass.localize(
|
||||
"ui.panel.config.energy.water.dialog.cost_number_input"
|
||||
)} (${unitPriceFixed})`}
|
||||
class="price-options"
|
||||
step="any"
|
||||
type="number"
|
||||
.value=${this._source.number_energy_price !== null
|
||||
? String(this._source.number_energy_price)
|
||||
: ""}
|
||||
@change=${this._numberPriceChanged}
|
||||
>
|
||||
<span slot="end">${unitPriceFixed}</span>
|
||||
</ha-input>`
|
||||
: ""}
|
||||
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
@@ -247,12 +276,9 @@ export class DialogEnergyWaterSettings
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleCostChanged(ev: Event) {
|
||||
this._costs = (ev.currentTarget as HaRadioGroup).value as
|
||||
| "no-costs"
|
||||
| "number"
|
||||
| "entity"
|
||||
| "statistic";
|
||||
private _handleCostChanged(ev: CustomEvent) {
|
||||
const input = ev.currentTarget as HaRadio;
|
||||
this._costs = input.value as any;
|
||||
}
|
||||
|
||||
private _numberPriceChanged(ev: InputEvent) {
|
||||
@@ -326,12 +352,15 @@ export class DialogEnergyWaterSettings
|
||||
display: block;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
ha-radio-group {
|
||||
margin-top: var(--ha-space-4);
|
||||
ha-formfield {
|
||||
display: block;
|
||||
}
|
||||
.price-options {
|
||||
display: block;
|
||||
margin-top: var(--ha-space-3);
|
||||
padding-left: 52px;
|
||||
padding-inline-start: 52px;
|
||||
padding-inline-end: initial;
|
||||
margin-top: -8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -4,9 +4,9 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LocalizeKeys } from "../../../../common/translations/localize";
|
||||
import "../../../../components/entity/ha-statistic-picker";
|
||||
import "../../../../components/radio/ha-radio-group";
|
||||
import type { HaRadioGroup } from "../../../../components/radio/ha-radio-group";
|
||||
import "../../../../components/radio/ha-radio-option";
|
||||
import "../../../../components/ha-formfield";
|
||||
import "../../../../components/ha-radio";
|
||||
import type { HaRadio } from "../../../../components/ha-radio";
|
||||
import type { PowerConfig } from "../../../../data/energy";
|
||||
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
|
||||
@@ -145,32 +145,54 @@ export class HaEnergyPowerConfig extends LitElement {
|
||||
)}
|
||||
</p>
|
||||
|
||||
<ha-radio-group
|
||||
.value=${this.powerType}
|
||||
name="powerType"
|
||||
@change=${this._handlePowerTypeChanged}
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
`${this.localizeBaseKey}.type_none` as LocalizeKeys
|
||||
)}
|
||||
>
|
||||
<ha-radio-option value="none">
|
||||
${this.hass.localize(
|
||||
`${this.localizeBaseKey}.type_none` as LocalizeKeys
|
||||
)}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="standard">
|
||||
${this.hass.localize(
|
||||
`${this.localizeBaseKey}.type_standard` as LocalizeKeys
|
||||
)}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="inverted">
|
||||
${this.hass.localize(
|
||||
`${this.localizeBaseKey}.type_inverted` as LocalizeKeys
|
||||
)}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="two_sensors">
|
||||
${this.hass.localize(
|
||||
`${this.localizeBaseKey}.type_two_sensors` as LocalizeKeys
|
||||
)}
|
||||
</ha-radio-option>
|
||||
</ha-radio-group>
|
||||
<ha-radio
|
||||
value="none"
|
||||
name="powerType"
|
||||
.checked=${this.powerType === "none"}
|
||||
@change=${this._handlePowerTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
`${this.localizeBaseKey}.type_standard` as LocalizeKeys
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="standard"
|
||||
name="powerType"
|
||||
.checked=${this.powerType === "standard"}
|
||||
@change=${this._handlePowerTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
`${this.localizeBaseKey}.type_inverted` as LocalizeKeys
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="inverted"
|
||||
name="powerType"
|
||||
.checked=${this.powerType === "inverted"}
|
||||
@change=${this._handlePowerTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
`${this.localizeBaseKey}.type_two_sensors` as LocalizeKeys
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
value="two_sensors"
|
||||
name="powerType"
|
||||
.checked=${this.powerType === "two_sensors"}
|
||||
@change=${this._handlePowerTypeChanged}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
|
||||
${this.powerType === "standard"
|
||||
? html`
|
||||
@@ -241,7 +263,8 @@ export class HaEnergyPowerConfig extends LitElement {
|
||||
}
|
||||
|
||||
private _handlePowerTypeChanged(ev: Event) {
|
||||
const newPowerType = (ev.currentTarget as HaRadioGroup).value as PowerType;
|
||||
const input = ev.currentTarget as HaRadio;
|
||||
const newPowerType = input.value as PowerType;
|
||||
// Clear power config when switching types
|
||||
fireEvent(this, "power-config-changed", {
|
||||
powerType: newPowerType,
|
||||
@@ -311,8 +334,8 @@ export class HaEnergyPowerConfig extends LitElement {
|
||||
ha-statistic-picker:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
ha-radio-group {
|
||||
margin-bottom: var(--ha-space-4);
|
||||
ha-formfield {
|
||||
display: block;
|
||||
}
|
||||
.power-section-label {
|
||||
margin-top: var(--ha-space-4);
|
||||
|
||||
@@ -29,6 +29,7 @@ import "../../../components/ha-icon-picker";
|
||||
import "../../../components/ha-labels-picker";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import "../../../components/ha-radio";
|
||||
import "../../../components/ha-select";
|
||||
import type { HaSelectSelectEvent } from "../../../components/ha-select";
|
||||
import "../../../components/ha-state-icon";
|
||||
|
||||
@@ -2,11 +2,11 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-formfield";
|
||||
import "../../../../components/ha-icon-picker";
|
||||
import "../../../../components/ha-radio";
|
||||
import type { HaRadio } from "../../../../components/ha-radio";
|
||||
import "../../../../components/input/ha-input";
|
||||
import "../../../../components/radio/ha-radio-group";
|
||||
import type { HaRadioGroup } from "../../../../components/radio/ha-radio-group";
|
||||
import "../../../../components/radio/ha-radio-option";
|
||||
import type { InputDateTime } from "../../../../data/input_datetime";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -87,37 +87,55 @@ class HaInputDateTimeForm extends LitElement {
|
||||
)}
|
||||
.disabled=${this.disabled}
|
||||
></ha-icon-picker>
|
||||
<ha-radio-group
|
||||
<br />
|
||||
${this.hass.localize("ui.dialogs.helper_settings.input_datetime.mode")}:
|
||||
<br />
|
||||
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.input_datetime.mode"
|
||||
"ui.dialogs.helper_settings.input_datetime.date"
|
||||
)}
|
||||
.value=${this._mode}
|
||||
.disabled=${this.disabled}
|
||||
name="mode"
|
||||
@change=${this._modeChanged}
|
||||
>
|
||||
<ha-radio-option value="date">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.input_datetime.date"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="time">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.input_datetime.time"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="datetime">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.input_datetime.datetime"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
</ha-radio-group>
|
||||
<ha-radio
|
||||
name="mode"
|
||||
value="date"
|
||||
.checked=${this._mode === "date"}
|
||||
@change=${this._modeChanged}
|
||||
.disabled=${this.disabled}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.input_datetime.time"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
name="mode"
|
||||
value="time"
|
||||
.checked=${this._mode === "time"}
|
||||
@change=${this._modeChanged}
|
||||
.disabled=${this.disabled}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.input_datetime.datetime"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
name="mode"
|
||||
value="datetime"
|
||||
.checked=${this._mode === "datetime"}
|
||||
@change=${this._modeChanged}
|
||||
.disabled=${this.disabled}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _modeChanged(ev: Event) {
|
||||
const mode = String((ev.currentTarget as HaRadioGroup).value);
|
||||
private _modeChanged(ev: CustomEvent) {
|
||||
const mode = (ev.target as HaRadio).value;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this._item,
|
||||
@@ -161,9 +179,6 @@ class HaInputDateTimeForm extends LitElement {
|
||||
ha-input {
|
||||
margin: var(--ha-space-2) 0;
|
||||
}
|
||||
ha-radio-group {
|
||||
margin-top: var(--ha-space-5);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-formfield";
|
||||
import "../../../../components/ha-icon-picker";
|
||||
import "../../../../components/radio/ha-radio-group";
|
||||
import type { HaRadioGroup } from "../../../../components/radio/ha-radio-group";
|
||||
import "../../../../components/radio/ha-radio-option";
|
||||
import "../../../../components/ha-radio";
|
||||
import type { HaRadio } from "../../../../components/ha-radio";
|
||||
import "../../../../components/ha-selector/ha-selector-select";
|
||||
import "../../../../components/input/ha-input";
|
||||
import type { InputNumber } from "../../../../data/input_number";
|
||||
@@ -137,26 +137,35 @@ class HaInputNumberForm extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
></ha-input>
|
||||
|
||||
<ha-radio-group
|
||||
orientation="horizontal"
|
||||
class="mode"
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.input_number.mode"
|
||||
)}
|
||||
.value=${this._mode}
|
||||
.disabled=${this.disabled}
|
||||
name="mode"
|
||||
@change=${this._modeChanged}
|
||||
>
|
||||
<ha-radio-option value="slider">
|
||||
${this.hass.localize(
|
||||
<div class="layout horizontal center justified mode">
|
||||
${this.hass.localize("ui.dialogs.helper_settings.input_number.mode")}
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.input_number.slider"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="box">
|
||||
${this.hass.localize("ui.dialogs.helper_settings.input_number.box")}
|
||||
</ha-radio-option>
|
||||
</ha-radio-group>
|
||||
>
|
||||
<ha-radio
|
||||
name="mode"
|
||||
value="slider"
|
||||
.checked=${this._mode === "slider"}
|
||||
@change=${this._modeChanged}
|
||||
.disabled=${this.disabled}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.input_number.box"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
name="mode"
|
||||
value="box"
|
||||
.checked=${this._mode === "box"}
|
||||
@change=${this._modeChanged}
|
||||
.disabled=${this.disabled}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
<ha-input
|
||||
.value=${this._step !== undefined ? String(this._step) : ""}
|
||||
.configValue=${"step"}
|
||||
@@ -185,12 +194,9 @@ class HaInputNumberForm extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _modeChanged(ev: Event) {
|
||||
private _modeChanged(ev: CustomEvent) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this._item,
|
||||
mode: (ev.currentTarget as HaRadioGroup).value,
|
||||
},
|
||||
value: { ...this._item, mode: (ev.target as HaRadio).value },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import "../../../../components/ha-formfield";
|
||||
import "../../../../components/ha-icon-picker";
|
||||
import "../../../../components/radio/ha-radio-group";
|
||||
import type { HaRadioGroup } from "../../../../components/radio/ha-radio-group";
|
||||
import "../../../../components/radio/ha-radio-option";
|
||||
import "../../../../components/ha-radio";
|
||||
import type { HaRadio } from "../../../../components/ha-radio";
|
||||
import "../../../../components/input/ha-input";
|
||||
import type { InputText } from "../../../../data/input_text";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
@@ -122,27 +122,35 @@ class HaInputTextForm extends LitElement {
|
||||
"ui.dialogs.helper_settings.input_text.max"
|
||||
)}
|
||||
></ha-input>
|
||||
<ha-radio-group
|
||||
orientation="horizontal"
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.input_text.mode"
|
||||
)}
|
||||
.value=${this._mode}
|
||||
.disabled=${this.disabled}
|
||||
name="mode"
|
||||
@change=${this._modeChanged}
|
||||
>
|
||||
<ha-radio-option value="text">
|
||||
${this.hass.localize(
|
||||
<div class="layout horizontal center justified">
|
||||
${this.hass.localize("ui.dialogs.helper_settings.input_text.mode")}
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.input_text.text"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="password">
|
||||
${this.hass.localize(
|
||||
>
|
||||
<ha-radio
|
||||
name="mode"
|
||||
value="text"
|
||||
.checked=${this._mode === "text"}
|
||||
@change=${this._modeChanged}
|
||||
.disabled=${this.disabled}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.input_text.password"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
</ha-radio-group>
|
||||
>
|
||||
<ha-radio
|
||||
name="mode"
|
||||
value="password"
|
||||
.checked=${this._mode === "password"}
|
||||
@change=${this._modeChanged}
|
||||
.disabled=${this.disabled}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
<ha-input
|
||||
.value=${this._pattern || ""}
|
||||
.configValue=${"pattern"}
|
||||
@@ -160,12 +168,9 @@ class HaInputTextForm extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _modeChanged(ev: Event) {
|
||||
private _modeChanged(ev: CustomEvent) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this._item,
|
||||
mode: (ev.currentTarget as HaRadioGroup).value,
|
||||
},
|
||||
value: { ...this._item, mode: (ev.target as HaRadio).value },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -146,7 +146,6 @@ interface HelperItem {
|
||||
category: string | undefined;
|
||||
area?: string;
|
||||
label_entries: LabelRegistryEntry[];
|
||||
labels: string[]; // search only
|
||||
assistants: string[];
|
||||
assistants_sortable_key: string | undefined;
|
||||
disabled?: boolean;
|
||||
@@ -553,9 +552,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
const entityRegEntry =
|
||||
entityRegistryByEntityId(entityReg)[item.entity_id];
|
||||
const labels = labelReg && entityRegEntry?.labels;
|
||||
const label_entries = (labels || []).map(
|
||||
(lbl) => labelReg!.find((label) => label.label_id === lbl)!
|
||||
);
|
||||
const category = entityRegEntry?.categories.helpers;
|
||||
const deviceId = entityRegEntry?.device_id;
|
||||
const areaId =
|
||||
@@ -576,8 +572,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
`ui.panel.config.helpers.types.${item.type}` as LocalizeKeys
|
||||
) ||
|
||||
item.type,
|
||||
label_entries,
|
||||
labels: label_entries.map((lbl) => lbl.name),
|
||||
label_entries: (labels || []).map(
|
||||
(lbl) => labelReg!.find((label) => label.label_id === lbl)!
|
||||
),
|
||||
category: category
|
||||
? categoryReg?.find((cat) => cat.category_id === category)?.name
|
||||
: undefined,
|
||||
|
||||
@@ -5,12 +5,12 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { computeDeviceNameDisplay } from "../../../../../common/entity/compute_device_name";
|
||||
import "../../../../../components/ha-dialog";
|
||||
import "../../../../../components/ha-expansion-panel";
|
||||
import "../../../../../components/ha-help-tooltip";
|
||||
import "../../../../../components/ha-list";
|
||||
import "../../../../../components/ha-list-item";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import "../../../../../components/item/ha-list-item-base";
|
||||
import "../../../../../components/list/ha-list-base";
|
||||
import "../../../../../components/ha-dialog";
|
||||
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
|
||||
import { subscribeDeviceRegistry } from "../../../../../data/device/device_registry";
|
||||
import type {
|
||||
@@ -91,103 +91,107 @@ class DialogZWaveJSNodeStatistics extends LitElement {
|
||||
)}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-list-base>
|
||||
<ha-list-item-base>
|
||||
<span slot="headline">
|
||||
<ha-list noninteractive>
|
||||
<ha-list-item twoline hasmeta>
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_statistics.commands_tx.label"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text">
|
||||
<span slot="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_statistics.commands_tx.tooltip"
|
||||
)}
|
||||
</span>
|
||||
<span slot="end">${this._nodeStatistics?.commands_tx}</span>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<span slot="headline">
|
||||
<span slot="meta">${this._nodeStatistics?.commands_tx}</span>
|
||||
</ha-list-item>
|
||||
<ha-list-item twoline hasmeta>
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_statistics.commands_rx.label"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text">
|
||||
<span slot="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_statistics.commands_rx.tooltip"
|
||||
)}
|
||||
</span>
|
||||
<span slot="end">${this._nodeStatistics?.commands_rx}</span>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<span slot="headline">
|
||||
<span slot="meta">${this._nodeStatistics?.commands_rx}</span>
|
||||
</ha-list-item>
|
||||
<ha-list-item twoline hasmeta>
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_statistics.commands_dropped_tx.label"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text">
|
||||
<span slot="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_statistics.commands_dropped_tx.tooltip"
|
||||
)}
|
||||
</span>
|
||||
<span slot="end">${this._nodeStatistics?.commands_dropped_tx}</span>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<span slot="headline">
|
||||
<span slot="meta"
|
||||
>${this._nodeStatistics?.commands_dropped_tx}</span
|
||||
>
|
||||
</ha-list-item>
|
||||
<ha-list-item twoline hasmeta>
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_statistics.commands_dropped_rx.label"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text">
|
||||
<span slot="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_statistics.commands_dropped_rx.tooltip"
|
||||
)}
|
||||
</span>
|
||||
<span slot="end">${this._nodeStatistics?.commands_dropped_rx}</span>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<span slot="headline">
|
||||
<span slot="meta"
|
||||
>${this._nodeStatistics?.commands_dropped_rx}</span
|
||||
>
|
||||
</ha-list-item>
|
||||
<ha-list-item twoline hasmeta>
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_statistics.timeout_response.label"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text">
|
||||
<span slot="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_statistics.timeout_response.tooltip"
|
||||
)}
|
||||
</span>
|
||||
<span slot="end">${this._nodeStatistics?.timeout_response}</span>
|
||||
</ha-list-item-base>
|
||||
<span slot="meta">${this._nodeStatistics?.timeout_response}</span>
|
||||
</ha-list-item>
|
||||
${this._nodeStatistics?.rtt
|
||||
? html`<ha-list-item-base>
|
||||
<span slot="headline">
|
||||
? html`<ha-list-item twoline hasmeta>
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_statistics.rtt.label"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text">
|
||||
<span slot="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_statistics.rtt.tooltip"
|
||||
)}
|
||||
</span>
|
||||
<span slot="end">${this._nodeStatistics.rtt}</span>
|
||||
</ha-list-item-base>`
|
||||
<span slot="meta">${this._nodeStatistics.rtt}</span>
|
||||
</ha-list-item>`
|
||||
: ``}
|
||||
${this._nodeStatistics?.rssi_translated
|
||||
? html`<ha-list-item-base>
|
||||
<span slot="headline">
|
||||
? html`<ha-list-item twoline hasmeta>
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_statistics.rssi.label"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text">
|
||||
<span slot="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_statistics.rssi.tooltip"
|
||||
)}
|
||||
</span>
|
||||
<span slot="end">${this._nodeStatistics.rssi_translated}</span>
|
||||
</ha-list-item-base>`
|
||||
<span slot="meta">${this._nodeStatistics.rssi_translated}</span>
|
||||
</ha-list-item>`
|
||||
: ``}
|
||||
</ha-list-base>
|
||||
</ha-list>
|
||||
${Object.entries(this._workingRoutes).map(([wrKey, wrValue]) =>
|
||||
wrValue
|
||||
? html`
|
||||
@@ -446,6 +450,10 @@ class DialogZWaveJSNodeStatistics extends LitElement {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-list-item {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -5,10 +5,8 @@ import {
|
||||
mdiDotsVertical,
|
||||
mdiHelpCircleOutline,
|
||||
mdiLabelOutline,
|
||||
mdiPalette,
|
||||
mdiPlus,
|
||||
mdiRobot,
|
||||
mdiScriptText,
|
||||
mdiShape,
|
||||
} from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
@@ -17,10 +15,7 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import type {
|
||||
FlattenObjectKeys,
|
||||
LocalizeFunc,
|
||||
} from "../../../common/translations/localize";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import type {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
@@ -52,7 +47,7 @@ import {
|
||||
showConfirmationDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import type { HomeAssistant, Route, TranslationDict } from "../../../types";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import {
|
||||
getCreatedAtTableColumn,
|
||||
getModifiedAtTableColumn,
|
||||
@@ -60,42 +55,6 @@ import {
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showLabelDetailDialog } from "./show-dialog-label-detail";
|
||||
|
||||
type ConfigTranslationKey = FlattenObjectKeys<
|
||||
TranslationDict["ui"]["panel"]["config"]
|
||||
>;
|
||||
|
||||
const NAVIGATION_ACTIONS: {
|
||||
value: string;
|
||||
icon: string;
|
||||
translationKey: ConfigTranslationKey;
|
||||
}[] = [
|
||||
{
|
||||
value: "navigate-entities",
|
||||
icon: mdiShape,
|
||||
translationKey: "entities.caption",
|
||||
},
|
||||
{
|
||||
value: "navigate-devices",
|
||||
icon: mdiDevices,
|
||||
translationKey: "devices.caption",
|
||||
},
|
||||
{
|
||||
value: "navigate-automations",
|
||||
icon: mdiRobot,
|
||||
translationKey: "automation.caption",
|
||||
},
|
||||
{
|
||||
value: "navigate-scenes",
|
||||
icon: mdiPalette,
|
||||
translationKey: "scene.caption",
|
||||
},
|
||||
{
|
||||
value: "navigate-scripts",
|
||||
icon: mdiScriptText,
|
||||
translationKey: "script.caption",
|
||||
},
|
||||
] as const;
|
||||
|
||||
@customElement("ha-config-labels")
|
||||
export class HaConfigLabels extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -286,14 +245,18 @@ export class HaConfigLabels extends LitElement {
|
||||
@wa-after-show=${this._overflowMenuOpened}
|
||||
@wa-after-hide=${this._overflowMenuClosed}
|
||||
>
|
||||
${NAVIGATION_ACTIONS.map(
|
||||
(action) => html`
|
||||
<ha-dropdown-item value=${action.value}>
|
||||
<ha-svg-icon slot="icon" .path=${action.icon}></ha-svg-icon>
|
||||
${this.hass.localize(`ui.panel.config.${action.translationKey}`)}
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
)}
|
||||
<ha-dropdown-item value="navigate-entities">
|
||||
<ha-svg-icon slot="icon" .path=${mdiShape}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.entities.caption")}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="navigate-devices">
|
||||
<ha-svg-icon slot="icon" .path=${mdiDevices}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.devices.caption")}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="navigate-automations">
|
||||
<ha-svg-icon slot="icon" .path=${mdiRobot}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.automation.caption")}
|
||||
</ha-dropdown-item>
|
||||
<wa-divider></wa-divider>
|
||||
<ha-dropdown-item variant="danger" value="remove">
|
||||
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
|
||||
@@ -399,19 +362,13 @@ export class HaConfigLabels extends LitElement {
|
||||
}
|
||||
switch (action) {
|
||||
case "navigate-entities":
|
||||
this._navigateConfig("/config/entities");
|
||||
this._navigateEntities();
|
||||
break;
|
||||
case "navigate-devices":
|
||||
this._navigateConfig("/config/devices/dashboard");
|
||||
this._navigateDevices();
|
||||
break;
|
||||
case "navigate-automations":
|
||||
this._navigateConfig("/config/automation/dashboard");
|
||||
break;
|
||||
case "navigate-scenes":
|
||||
this._navigateConfig("/config/scene/dashboard");
|
||||
break;
|
||||
case "navigate-scripts":
|
||||
this._navigateConfig("/config/script/dashboard");
|
||||
this._navigateAutomations();
|
||||
break;
|
||||
case "remove":
|
||||
this._handleRemoveLabelClick();
|
||||
@@ -419,8 +376,22 @@ export class HaConfigLabels extends LitElement {
|
||||
}
|
||||
};
|
||||
|
||||
private _navigateConfig = (path: string) => {
|
||||
navigate(`${path}?historyBack=1&label=${this._overflowLabel.label_id}`);
|
||||
private _navigateEntities = () => {
|
||||
navigate(
|
||||
`/config/entities?historyBack=1&label=${this._overflowLabel.label_id}`
|
||||
);
|
||||
};
|
||||
|
||||
private _navigateDevices = () => {
|
||||
navigate(
|
||||
`/config/devices/dashboard?historyBack=1&label=${this._overflowLabel.label_id}`
|
||||
);
|
||||
};
|
||||
|
||||
private _navigateAutomations = () => {
|
||||
navigate(
|
||||
`/config/automation/dashboard?historyBack=1&label=${this._overflowLabel.label_id}`
|
||||
);
|
||||
};
|
||||
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
|
||||
@@ -764,12 +764,19 @@ class ErrorLogCard extends LitElement {
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
overflow: auto;
|
||||
min-height: var(--error-log-card-height, calc(100vh - 255px));
|
||||
max-height: var(--error-log-card-height, calc(100vh - 255px));
|
||||
min-height: var(--error-log-card-height, calc(100vh - 244px));
|
||||
max-height: var(--error-log-card-height, calc(100vh - 244px));
|
||||
border-top: 1px solid var(--divider-color);
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
.error-log {
|
||||
min-height: var(--error-log-card-height, calc(100vh - 190px));
|
||||
max-height: var(--error-log-card-height, calc(100vh - 190px));
|
||||
}
|
||||
}
|
||||
|
||||
.error-log > div {
|
||||
overflow: auto;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
@@ -100,16 +100,28 @@ export class HaConfigLogs extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const search = html`
|
||||
<div class="search">
|
||||
<ha-input-search
|
||||
appearance="outlined"
|
||||
@input=${this._filterChanged}
|
||||
.value=${this._filter}
|
||||
.placeholder=${this.hass.localize("ui.panel.config.logs.search")}
|
||||
></ha-input-search>
|
||||
</div>
|
||||
`;
|
||||
const search = this.narrow
|
||||
? html`
|
||||
<div slot="header">
|
||||
<ha-input-search
|
||||
appearance="outlined"
|
||||
class="header"
|
||||
@input=${this._filterChanged}
|
||||
.value=${this._filter}
|
||||
.placeholder=${this.hass.localize("ui.panel.config.logs.search")}
|
||||
></ha-input-search>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="search">
|
||||
<ha-input-search
|
||||
appearance="outlined"
|
||||
@input=${this._filterChanged}
|
||||
.value=${this._filter}
|
||||
.placeholder=${this.hass.localize("ui.panel.config.logs.search")}
|
||||
></ha-input-search>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const selectedProvider = this._getActiveProvider(this._selectedLogProvider);
|
||||
|
||||
@@ -336,17 +348,22 @@ export class HaConfigLogs extends LitElement {
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
.search ha-input-search {
|
||||
ha-input-search {
|
||||
padding: var(--ha-space-3);
|
||||
background: var(--sidebar-background-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
ha-input-search.header {
|
||||
padding-inline-start: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
.content {
|
||||
direction: ltr;
|
||||
}
|
||||
ha-generic-picker {
|
||||
--md-list-item-leading-icon-color: var(--ha-color-primary-50);
|
||||
--mdc-icon-size: var(--ha-space-6);
|
||||
--mdc-icon-size: 32px;
|
||||
}
|
||||
|
||||
img {
|
||||
@@ -355,7 +372,7 @@ export class HaConfigLogs extends LitElement {
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
ha-generic-picker {
|
||||
max-width: max(30%, 180px);
|
||||
max-width: max(30%, 160px);
|
||||
}
|
||||
ha-button {
|
||||
max-width: 100%;
|
||||
|
||||
@@ -6,6 +6,7 @@ import "../../../components/ha-card";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-expansion-panel";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-radio";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/input/ha-input";
|
||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||
|
||||
@@ -9,12 +9,12 @@ import "../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-expansion-panel";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-list";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/radio/ha-radio-group";
|
||||
import type { HaRadioGroup } from "../../../components/radio/ha-radio-group";
|
||||
import "../../../components/radio/ha-radio-option";
|
||||
import "../../../components/ha-radio";
|
||||
import type { HaRadio } from "../../../components/ha-radio";
|
||||
import "../../../components/ha-spinner";
|
||||
import "../../../components/ha-tab-group";
|
||||
import "../../../components/ha-tab-group-tab";
|
||||
@@ -182,28 +182,53 @@ export class HassioNetwork extends LitElement {
|
||||
: nothing}
|
||||
${this._wifiConfiguration
|
||||
? html`
|
||||
<ha-radio-group
|
||||
orientation="horizontal"
|
||||
.value=${this._wifiConfiguration.auth || "open"}
|
||||
name="auth"
|
||||
@change=${this._handleRadioValueChangedAp}
|
||||
>
|
||||
<ha-radio-option value="open">
|
||||
${this.hass.localize(
|
||||
<div class="radio-row">
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.open"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="wep">
|
||||
${this.hass.localize(
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChangedAp}
|
||||
.ap=${this._wifiConfiguration}
|
||||
value="open"
|
||||
name="auth"
|
||||
.checked=${this._wifiConfiguration.auth ===
|
||||
undefined ||
|
||||
this._wifiConfiguration.auth === "open"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.wep"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="wpa-psk">
|
||||
${this.hass.localize(
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChangedAp}
|
||||
.ap=${this._wifiConfiguration}
|
||||
value="wep"
|
||||
name="auth"
|
||||
.checked=${this._wifiConfiguration.auth === "wep"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.wpa"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
</ha-radio-group>
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChangedAp}
|
||||
.ap=${this._wifiConfiguration}
|
||||
value="wpa-psk"
|
||||
name="auth"
|
||||
.checked=${this._wifiConfiguration.auth ===
|
||||
"wpa-psk"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
${this._wifiConfiguration.auth === "wpa-psk" ||
|
||||
this._wifiConfiguration.auth === "wep"
|
||||
? html`
|
||||
@@ -312,23 +337,51 @@ export class HassioNetwork extends LitElement {
|
||||
.header=${`IPv${version.charAt(version.length - 1)}`}
|
||||
outlined
|
||||
>
|
||||
<ha-radio-group
|
||||
orientation="horizontal"
|
||||
.version=${version}
|
||||
.value=${this._interface![version]?.method}
|
||||
name="${version}method"
|
||||
@change=${this._handleRadioValueChanged}
|
||||
>
|
||||
<ha-radio-option value="auto">
|
||||
${this.hass.localize("ui.panel.config.network.supervisor.auto")}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="static">
|
||||
${this.hass.localize("ui.panel.config.network.supervisor.static")}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="disabled">
|
||||
${this.hass.localize("ui.panel.config.network.supervisor.disabled")}
|
||||
</ha-radio-option>
|
||||
</ha-radio-group>
|
||||
<div class="radio-row">
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.auto"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
.version=${version}
|
||||
value="auto"
|
||||
name="${version}method"
|
||||
.checked=${this._interface![version]?.method === "auto"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.static"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
.version=${version}
|
||||
value="static"
|
||||
name="${version}method"
|
||||
.checked=${this._interface![version]?.method === "static"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.disabled"
|
||||
)}
|
||||
class="warning"
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
.version=${version}
|
||||
value="disabled"
|
||||
name="${version}method"
|
||||
.checked=${this._interface![version]?.method === "disabled"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
${["static", "auto"].includes(this._interface![version].method)
|
||||
? html`
|
||||
${this._interface![version].address.map(
|
||||
@@ -600,9 +653,9 @@ export class HassioNetwork extends LitElement {
|
||||
}
|
||||
|
||||
private _handleRadioValueChanged(ev: Event): void {
|
||||
const source = ev.currentTarget as HaRadioGroup;
|
||||
const source = ev.target as HaRadio;
|
||||
const value = source.value as "disabled" | "auto" | "static";
|
||||
const version = (source as any).version as "ipv4" | "ipv6";
|
||||
const version = (ev.target as any).version as "ipv4" | "ipv6";
|
||||
|
||||
if (
|
||||
!value ||
|
||||
@@ -618,8 +671,8 @@ export class HassioNetwork extends LitElement {
|
||||
}
|
||||
|
||||
private _handleRadioValueChangedAp(ev: Event): void {
|
||||
const source = ev.currentTarget as HaRadioGroup;
|
||||
const value = source.value as "open" | "wep" | "wpa-psk";
|
||||
const source = ev.target as HaRadio;
|
||||
const value = source.value as string as "open" | "wep" | "wpa-psk";
|
||||
this._wifiConfiguration!.auth = value;
|
||||
this._dirty = true;
|
||||
this.requestUpdate("_wifiConfiguration");
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { STRINGS_SEPARATOR_DOT } from "../../../common/const";
|
||||
import { relativeTime } from "../../../common/datetime/relative_time";
|
||||
import { capitalizeFirstLetter } from "../../../common/string/capitalize-first-letter";
|
||||
import "../../../components/item/ha-list-item-button";
|
||||
import "../../../components/list/ha-list-base";
|
||||
import { STRINGS_SEPARATOR_DOT } from "../../../common/const";
|
||||
import "../../../components/ha-md-list";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import type { StatisticsValidationResult } from "../../../data/recorder";
|
||||
import {
|
||||
@@ -19,9 +19,9 @@ import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-c
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
import { fixStatisticsIssue } from "../developer-tools/statistics/fix-statistics";
|
||||
import { showVacuumSegmentMappingDialog } from "../entities/dialogs/show-dialog-vacuum-segment-mapping";
|
||||
import { showRepairsFlowDialog } from "./show-dialog-repair-flow";
|
||||
import { showRepairsIssueDialog } from "./show-repair-issue-dialog";
|
||||
import { showVacuumSegmentMappingDialog } from "../entities/dialogs/show-dialog-vacuum-segment-mapping";
|
||||
|
||||
@customElement("ha-config-repairs")
|
||||
class HaConfigRepairs extends LitElement {
|
||||
@@ -40,9 +40,7 @@ class HaConfigRepairs extends LitElement {
|
||||
const issues = this.repairsIssues;
|
||||
|
||||
return html`
|
||||
<ha-list-base
|
||||
aria-label=${this.hass.localize("ui.panel.config.repairs.caption")}
|
||||
>
|
||||
<ha-md-list>
|
||||
${issues.map((issue) => {
|
||||
const domainName = domainToName(this.hass.localize, issue.domain);
|
||||
|
||||
@@ -57,11 +55,12 @@ class HaConfigRepairs extends LitElement {
|
||||
: "";
|
||||
|
||||
return html`
|
||||
<ha-list-item-button
|
||||
<ha-md-list-item
|
||||
.hasMeta=${!this.narrow}
|
||||
.issue=${issue}
|
||||
class=${issue.ignored ? "ignored" : ""}
|
||||
@click=${this._openShowMoreDialog}
|
||||
type="button"
|
||||
>
|
||||
<img
|
||||
slot="start"
|
||||
@@ -93,12 +92,12 @@ class HaConfigRepairs extends LitElement {
|
||||
`ui.panel.config.repairs.${issue.severity}`
|
||||
)}</span
|
||||
>`
|
||||
: nothing}
|
||||
: ""}
|
||||
${(issue.severity === "critical" ||
|
||||
issue.severity === "error") &&
|
||||
issue.created
|
||||
? STRINGS_SEPARATOR_DOT
|
||||
: nothing}
|
||||
: ""}
|
||||
${createdBy
|
||||
? html`<span .title=${createdBy}>${createdBy}</span>`
|
||||
: nothing}
|
||||
@@ -107,15 +106,15 @@ class HaConfigRepairs extends LitElement {
|
||||
"ui.panel.config.repairs.dialog.ignored_in_version_short",
|
||||
{ version: issue.dismissed_version }
|
||||
)}`
|
||||
: nothing}
|
||||
: ""}
|
||||
</span>
|
||||
${!this.narrow
|
||||
? html`<ha-icon-next slot="end"></ha-icon-next>`
|
||||
: nothing}
|
||||
</ha-list-item-button>
|
||||
: ""}
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
})}
|
||||
</ha-list-base>
|
||||
</ha-md-list>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -203,14 +202,11 @@ class HaConfigRepairs extends LitElement {
|
||||
outline: none;
|
||||
text-decoration: underline;
|
||||
}
|
||||
ha-list-item-button img[slot="start"] {
|
||||
ha-md-list-item img[slot="start"] {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
ha-list-item-button ha-icon-next[slot="end"] {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-list-item-button span[slot="supporting-text"] {
|
||||
ha-md-list-item span[slot="supporting-text"] {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.error {
|
||||
|
||||
@@ -122,7 +122,6 @@ type SceneItem = SceneEntity & {
|
||||
area: string | undefined;
|
||||
category: string | undefined;
|
||||
label_entries: LabelRegistryEntry[];
|
||||
labels: string[]; // search only
|
||||
assistants: string[];
|
||||
assistants_sortable_key: string | undefined;
|
||||
editable: boolean;
|
||||
@@ -240,9 +239,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
const category = entityRegEntry?.categories.scene;
|
||||
const labels = labelReg && entityRegEntry?.labels;
|
||||
const label_entries = (labels || []).map(
|
||||
(lbl) => labelReg!.find((label) => label.label_id === lbl)!
|
||||
);
|
||||
const assistants = getEntityVoiceAssistantsIds(
|
||||
entityReg,
|
||||
scene.entity_id
|
||||
@@ -256,8 +252,9 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
category: category
|
||||
? categoryReg?.find((cat) => cat.category_id === category)?.name
|
||||
: undefined,
|
||||
label_entries,
|
||||
labels: label_entries.map((lbl) => lbl.name),
|
||||
label_entries: (labels || []).map(
|
||||
(lbl) => labelReg!.find((label) => label.label_id === lbl)!
|
||||
),
|
||||
assistants,
|
||||
assistants_sortable_key: getAssistantsSortableKey(assistants),
|
||||
selectable: entityRegEntry !== undefined,
|
||||
|
||||
@@ -329,6 +329,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
.defaultValue=${this._config}
|
||||
@value-changed=${this._yamlChanged}
|
||||
@editor-save=${this._saveScene}
|
||||
.showErrors=${false}
|
||||
disable-fullscreen
|
||||
></ha-yaml-editor>`;
|
||||
}
|
||||
|
||||
@@ -464,6 +464,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
disable-fullscreen
|
||||
@value-changed=${this._yamlChanged}
|
||||
@editor-save=${this._handleSaveScript}
|
||||
.showErrors=${false}
|
||||
></ha-yaml-editor>
|
||||
<ha-button
|
||||
slot="fab"
|
||||
|
||||
@@ -127,7 +127,6 @@ type ScriptItem = ScriptEntity & {
|
||||
last_triggered: string | undefined;
|
||||
category: string | undefined;
|
||||
label_entries: LabelRegistryEntry[];
|
||||
labels: string[]; // search only
|
||||
assistants: string[];
|
||||
assistants_sortable_key: string | undefined;
|
||||
};
|
||||
@@ -246,9 +245,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
const category = entityRegEntry?.categories.script;
|
||||
const labels = labelReg && entityRegEntry?.labels;
|
||||
const label_entries = (labels || []).map(
|
||||
(lbl) => labelReg!.find((label) => label.label_id === lbl)!
|
||||
);
|
||||
const assistants = getEntityVoiceAssistantsIds(
|
||||
entityReg,
|
||||
script.entity_id
|
||||
@@ -263,8 +259,9 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
category: category
|
||||
? categoryReg?.find((cat) => cat.category_id === category)?.name
|
||||
: undefined,
|
||||
label_entries,
|
||||
labels: label_entries.map((lbl) => lbl.name),
|
||||
label_entries: (labels || []).map(
|
||||
(lbl) => labelReg!.find((label) => label.label_id === lbl)!
|
||||
),
|
||||
assistants,
|
||||
assistants_sortable_key: getAssistantsSortableKey(assistants),
|
||||
selectable: entityRegEntry !== undefined,
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import { mdiContentCopy } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { copyToClipboard } from "../../../common/util/copy-clipboard";
|
||||
import { generateUuidV4 } from "../../../common/util/uuid";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/ha-dialog-footer";
|
||||
import "../../../components/ha-expansion-panel";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-qr-code";
|
||||
import "../../../components/ha-switch";
|
||||
import "../../../components/input/ha-input";
|
||||
import type { HaInput } from "../../../components/input/ha-input";
|
||||
import type { Tag, UpdateTagParams } from "../../../data/tag";
|
||||
@@ -19,7 +15,6 @@ import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import type { TagDetailDialogParams } from "./show-dialog-tag-detail";
|
||||
|
||||
@customElement("dialog-tag-detail")
|
||||
@@ -33,8 +28,6 @@ class DialogTagDetail
|
||||
|
||||
@state() private _name!: string;
|
||||
|
||||
@state() private _useCustomId = false;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _params?: TagDetailDialogParams;
|
||||
@@ -49,7 +42,6 @@ class DialogTagDetail
|
||||
this._params = params;
|
||||
this._error = undefined;
|
||||
this._open = true;
|
||||
this._useCustomId = false;
|
||||
if (this._params.entry) {
|
||||
this._name = this._params.entry.name || "";
|
||||
} else {
|
||||
@@ -84,7 +76,7 @@ class DialogTagDetail
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this._params.entry
|
||||
? this.hass!.localize("ui.panel.config.tag.detail.tag_details")
|
||||
? this._params.entry.name || this._params.entry.id
|
||||
: this.hass!.localize("ui.panel.config.tag.detail.new_tag")}
|
||||
prevent-scrim-close
|
||||
@closed=${this._dialogClosed}
|
||||
@@ -105,35 +97,18 @@ class DialogTagDetail
|
||||
)}
|
||||
required
|
||||
></ha-input>
|
||||
${this._params.entry
|
||||
? nothing
|
||||
: html`
|
||||
<ha-expansion-panel
|
||||
outlined
|
||||
.header=${this.hass!.localize(
|
||||
"ui.panel.config.tag.detail.use_custom_id"
|
||||
)}
|
||||
.expanded=${this._useCustomId}
|
||||
@expanded-changed=${this._useCustomIdChanged}
|
||||
>
|
||||
<ha-input
|
||||
.value=${this._id || ""}
|
||||
.configValue=${"id"}
|
||||
@input=${this._valueChanged}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.tag.detail.tag_id"
|
||||
)}
|
||||
.placeholder=${this.hass!.localize(
|
||||
"ui.panel.config.tag.detail.tag_id_placeholder"
|
||||
)}
|
||||
></ha-input>
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.tag.detail.custom_id_warning"
|
||||
)}
|
||||
</ha-alert>
|
||||
</ha-expansion-panel>
|
||||
`}
|
||||
<ha-input
|
||||
.value=${this._params.entry
|
||||
? this._params.entry.id
|
||||
: this._id || ""}
|
||||
.readonly=${!!this._params.entry}
|
||||
.configValue=${"id"}
|
||||
@input=${this._valueChanged}
|
||||
.label=${this.hass!.localize("ui.panel.config.tag.detail.tag_id")}
|
||||
.placeholder=${this.hass!.localize(
|
||||
"ui.panel.config.tag.detail.tag_id_placeholder"
|
||||
)}
|
||||
></ha-input>
|
||||
</div>
|
||||
${this._params.entry
|
||||
? html`
|
||||
@@ -164,17 +139,6 @@ class DialogTagDetail
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="tag-id">
|
||||
<span class="tag-id-label">
|
||||
${this.hass!.localize("ui.panel.config.tag.detail.tag_id")}:
|
||||
</span>
|
||||
<span class="tag-id-value">${this._params.entry.id}</span>
|
||||
<ha-icon-button
|
||||
.path=${mdiContentCopy}
|
||||
.label=${this.hass!.localize("ui.common.copy")}
|
||||
@click=${this._copyId}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
`
|
||||
: ``}
|
||||
</div>
|
||||
@@ -225,27 +189,6 @@ class DialogTagDetail
|
||||
this[`_${configValue}`] = target.value;
|
||||
}
|
||||
|
||||
private _useCustomIdChanged(ev: CustomEvent) {
|
||||
this._useCustomId = ev.detail.expanded;
|
||||
if (this._useCustomId) {
|
||||
if (!this._id) {
|
||||
this._id = generateUuidV4();
|
||||
}
|
||||
} else {
|
||||
this._id = "";
|
||||
}
|
||||
}
|
||||
|
||||
private async _copyId() {
|
||||
if (!this._params?.entry) {
|
||||
return;
|
||||
}
|
||||
await copyToClipboard(this._params.entry.id);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||
});
|
||||
}
|
||||
|
||||
private async _updateEntry() {
|
||||
this._submitting = true;
|
||||
let newValue: Tag | undefined;
|
||||
@@ -256,10 +199,7 @@ class DialogTagDetail
|
||||
if (this._params!.entry) {
|
||||
newValue = await this._params!.updateEntry!(values);
|
||||
} else {
|
||||
newValue = await this._params!.createEntry(
|
||||
values,
|
||||
this._useCustomId ? this._id : ""
|
||||
);
|
||||
newValue = await this._params!.createEntry(values, this._id);
|
||||
}
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
@@ -306,29 +246,6 @@ class DialogTagDetail
|
||||
ha-input:not([required]) {
|
||||
margin-bottom: var(--ha-space-5);
|
||||
}
|
||||
ha-expansion-panel {
|
||||
display: block;
|
||||
margin-bottom: var(--ha-space-2);
|
||||
}
|
||||
ha-expansion-panel[expanded] {
|
||||
--expansion-panel-content-padding: var(--ha-space-3) var(--ha-space-2);
|
||||
}
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin-top: var(--ha-space-2);
|
||||
}
|
||||
.tag-id {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--ha-space-1);
|
||||
margin-top: var(--ha-space-3);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.tag-id-value {
|
||||
font-family: var(--ha-font-family-code);
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
::slotted(img) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -90,9 +90,9 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
|
||||
},
|
||||
id: {
|
||||
title: localize("ui.panel.config.tag.headers.tag_id"),
|
||||
main: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
defaultHidden: true,
|
||||
},
|
||||
last_scanned_datetime: {
|
||||
title: localize("ui.panel.config.tag.headers.last_scanned"),
|
||||
|
||||
@@ -402,12 +402,6 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
.content:has(state-history-charts) {
|
||||
overflow: visible;
|
||||
}
|
||||
.content.has-height:has(state-history-charts) {
|
||||
overflow: hidden;
|
||||
}
|
||||
.has-header {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction, hasAnyAction } from "../common/has-action";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import { hasConfigChanged } from "../common/has-changed";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import type { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
@@ -52,17 +52,10 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
|
||||
throw new Error("Image required");
|
||||
}
|
||||
|
||||
if (config.image_entity) {
|
||||
this._config = {
|
||||
tap_action: { action: "more-info" },
|
||||
...config,
|
||||
};
|
||||
} else {
|
||||
this._config = {
|
||||
tap_action: { action: "none" },
|
||||
...config,
|
||||
};
|
||||
}
|
||||
this._config = {
|
||||
tap_action: { action: "more-info" },
|
||||
...config,
|
||||
};
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
@@ -174,11 +167,6 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const clickable = Boolean(
|
||||
(this._config.image_entity && !this._config.tap_action) ||
|
||||
hasAnyAction(this._config)
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
@action=${this._handleAction}
|
||||
@@ -192,7 +180,15 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
|
||||
: undefined
|
||||
)}
|
||||
class=${classMap({
|
||||
clickable,
|
||||
clickable: Boolean(
|
||||
(this._config.image_entity && !this._config.tap_action) ||
|
||||
(this._config.tap_action &&
|
||||
this._config.tap_action.action !== "none") ||
|
||||
(this._config.hold_action &&
|
||||
this._config.hold_action.action !== "none") ||
|
||||
(this._config.double_tap_action &&
|
||||
this._config.double_tap_action.action !== "none")
|
||||
),
|
||||
})}
|
||||
>
|
||||
<img
|
||||
|
||||
@@ -470,8 +470,8 @@ export function addEntityToCondition(
|
||||
condition.condition === "numeric_state"
|
||||
) {
|
||||
return {
|
||||
entity: entityId,
|
||||
...condition,
|
||||
entity: entityId,
|
||||
};
|
||||
}
|
||||
return condition;
|
||||
|
||||
@@ -68,7 +68,9 @@ export class HuiPictureCardEditor
|
||||
{
|
||||
name: "tap_action",
|
||||
selector: {
|
||||
ui_action: {},
|
||||
ui_action: {
|
||||
default_action: "more-info",
|
||||
},
|
||||
},
|
||||
context: ACTION_RELATED_CONTEXT,
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@ import { cache } from "lit/directives/cache";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { handleStructError } from "../../../common/structs/handle-errors";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import { deepEqual } from "../../../common/util/deep-equal";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-spinner";
|
||||
@@ -70,6 +71,11 @@ export abstract class HuiElementEditor<
|
||||
// Error: Configuration broken - do not save
|
||||
@state() private _errors?: string[];
|
||||
|
||||
// Error from unparseable YAML, but don't show it immediately to prevent showing immediately on every keystroke
|
||||
@state() private _pendingYamlError?: string;
|
||||
|
||||
@state() private _yamlError = false;
|
||||
|
||||
// Warning: GUI editor can't handle configuration - ok to save
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
@@ -77,8 +83,6 @@ export abstract class HuiElementEditor<
|
||||
|
||||
@state() private _loading = false;
|
||||
|
||||
@state() private _yamlError = false;
|
||||
|
||||
@query("ha-yaml-editor") _yamlEditor?: HaYamlEditor;
|
||||
|
||||
private _loadCount = 0;
|
||||
@@ -101,7 +105,7 @@ export abstract class HuiElementEditor<
|
||||
}
|
||||
|
||||
private _setConfig(): void {
|
||||
if (!this._errors && !this._yamlError) {
|
||||
if (!this._errors) {
|
||||
try {
|
||||
this._updateConfigElement();
|
||||
} catch (err: any) {
|
||||
@@ -127,9 +131,7 @@ export abstract class HuiElementEditor<
|
||||
}
|
||||
|
||||
public get hasError(): boolean {
|
||||
return (
|
||||
this._yamlError || (this._errors !== undefined && this._errors.length > 0)
|
||||
);
|
||||
return this._errors !== undefined && this._errors.length > 0;
|
||||
}
|
||||
|
||||
public get GUImode(): boolean {
|
||||
@@ -249,8 +251,10 @@ export abstract class HuiElementEditor<
|
||||
.hass=${this.hass}
|
||||
.inDialog=${this.inDialog}
|
||||
@value-changed=${this._handleYAMLChanged}
|
||||
@blur=${this._onBlurYaml}
|
||||
@keydown=${this._ignoreKeydown}
|
||||
dir="ltr"
|
||||
.showErrors=${false}
|
||||
></ha-yaml-editor>
|
||||
</div>
|
||||
`}
|
||||
@@ -270,7 +274,7 @@ export abstract class HuiElementEditor<
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing}
|
||||
${this._errors?.length
|
||||
${this.hasError
|
||||
? html`
|
||||
<ha-alert
|
||||
alert-type="error"
|
||||
@@ -279,7 +283,7 @@ export abstract class HuiElementEditor<
|
||||
)}
|
||||
>
|
||||
<ul>
|
||||
${this._errors.map((error) => html`<li>${error}</li>`)}
|
||||
${this._errors!.map((error) => html`<li>${error}</li>`)}
|
||||
</ul>
|
||||
</ha-alert>
|
||||
`
|
||||
@@ -335,14 +339,40 @@ export abstract class HuiElementEditor<
|
||||
|
||||
private _handleYAMLChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const config = ev.detail.value;
|
||||
if (ev.detail.isValid) {
|
||||
this._config = ev.detail.value;
|
||||
this._config = config;
|
||||
this._errors = undefined;
|
||||
this._pendingYamlError = undefined;
|
||||
this._yamlError = false;
|
||||
this._debounceYamlError.cancel();
|
||||
this._setConfig();
|
||||
} else if (this._yamlError) {
|
||||
// If we're already showing a yaml error, don't bother to debounce, just update immediately.
|
||||
this._errors = [ev.detail.errorMsg];
|
||||
} else {
|
||||
this._yamlError = true;
|
||||
this._pendingYamlError = ev.detail.errorMsg;
|
||||
this._debounceYamlError();
|
||||
}
|
||||
}
|
||||
|
||||
private _debounceYamlError = debounce(() => {
|
||||
if (this._pendingYamlError) {
|
||||
this._yamlError = true;
|
||||
this._errors = [this._pendingYamlError];
|
||||
this._pendingYamlError = undefined;
|
||||
this._setConfig();
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
private _onBlurYaml() {
|
||||
this._debounceYamlError.cancel();
|
||||
if (this._pendingYamlError) {
|
||||
this._yamlError = true;
|
||||
this._errors = [this._pendingYamlError];
|
||||
this._pendingYamlError = undefined;
|
||||
this._setConfig();
|
||||
}
|
||||
this._setConfig();
|
||||
}
|
||||
|
||||
protected async unloadConfigElement(): Promise<void> {
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction, hasAnyAction } from "../common/has-action";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import type { LovelaceHeaderFooter } from "../types";
|
||||
import type { PictureHeaderFooterConfig } from "./types";
|
||||
|
||||
@@ -56,7 +56,9 @@ export class HuiPictureHeaderFooter
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const clickable = hasAnyAction(this._config);
|
||||
const clickable = Boolean(
|
||||
this._config.tap_action || this._config.hold_action
|
||||
);
|
||||
|
||||
return html`
|
||||
<img
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { undoDepth } from "@codemirror/commands";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { dump, load } from "js-yaml";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -7,8 +8,8 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { array, assert, object, optional, string, type } from "superstruct";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../components/ha-yaml-editor";
|
||||
import "../../components/ha-code-editor";
|
||||
import type { HaCodeEditor } from "../../components/ha-code-editor";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-top-app-bar-fixed";
|
||||
import type { LovelaceRawConfig } from "../../data/lovelace/config/types";
|
||||
@@ -46,10 +47,6 @@ class LovelaceFullConfigEditor extends LitElement {
|
||||
|
||||
@state() private _changed?: boolean;
|
||||
|
||||
private _config?: LovelaceRawConfig;
|
||||
|
||||
private _yamlError?: string;
|
||||
|
||||
protected render(): TemplateResult | undefined {
|
||||
return html`
|
||||
<ha-top-app-bar-fixed .narrow=${this.narrow}>
|
||||
@@ -84,14 +81,18 @@ class LovelaceFullConfigEditor extends LitElement {
|
||||
)}</ha-button
|
||||
>
|
||||
<div class="content">
|
||||
<ha-yaml-editor
|
||||
<ha-code-editor
|
||||
mode="yaml"
|
||||
autofocus
|
||||
autocomplete-entities
|
||||
autocomplete-icons
|
||||
.hass=${this.hass}
|
||||
@value-changed=${this._yamlChanged}
|
||||
@editor-save=${this._handleSave}
|
||||
disable-fullscreen
|
||||
dir="ltr"
|
||||
>
|
||||
</ha-yaml-editor>
|
||||
</ha-code-editor>
|
||||
</div>
|
||||
</ha-top-app-bar-fixed>
|
||||
`;
|
||||
@@ -99,7 +100,7 @@ class LovelaceFullConfigEditor extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
this.yamlEditor.setValue(this.lovelace!.rawConfig);
|
||||
this.yamlEditor.value = dump(this.lovelace!.rawConfig);
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues<this>) {
|
||||
@@ -111,7 +112,7 @@ class LovelaceFullConfigEditor extends LitElement {
|
||||
oldLovelace.rawConfig !== this.lovelace.rawConfig &&
|
||||
!deepEqual(oldLovelace.rawConfig, this.lovelace.rawConfig)
|
||||
) {
|
||||
this.yamlEditor.setValue(this.lovelace!.rawConfig);
|
||||
this.yamlEditor.value = dump(this.lovelace!.rawConfig);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +137,7 @@ class LovelaceFullConfigEditor extends LitElement {
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
|
||||
ha-yaml-editor {
|
||||
ha-code-editor {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -153,9 +154,7 @@ class LovelaceFullConfigEditor extends LitElement {
|
||||
];
|
||||
}
|
||||
|
||||
private _yamlChanged(ev: CustomEvent) {
|
||||
this._config = ev.detail.isValid ? ev.detail.value : undefined;
|
||||
this._yamlError = ev.detail.errorMsg;
|
||||
private _yamlChanged() {
|
||||
this._changed = undoDepth(this.yamlEditor.codemirror!.state) > 0;
|
||||
if (this._changed && !window.onbeforeunload) {
|
||||
window.onbeforeunload = () => true;
|
||||
@@ -205,7 +204,9 @@ class LovelaceFullConfigEditor extends LitElement {
|
||||
private async _handleSave() {
|
||||
this._saving = true;
|
||||
|
||||
if (!this.yamlEditor.yaml) {
|
||||
const value = this.yamlEditor.value;
|
||||
|
||||
if (!value) {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.lovelace.editor.raw_editor.confirm_reset_config_title"
|
||||
@@ -221,14 +222,6 @@ class LovelaceFullConfigEditor extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._yamlError) {
|
||||
showAlertDialog(this, {
|
||||
text: this._yamlError,
|
||||
});
|
||||
this._saving = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.yamlEditor.hasComments) {
|
||||
if (
|
||||
!confirm(
|
||||
@@ -241,8 +234,19 @@ class LovelaceFullConfigEditor extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
const config: LovelaceRawConfig = this._config!;
|
||||
|
||||
let config: LovelaceRawConfig;
|
||||
try {
|
||||
config = load(value) as LovelaceRawConfig;
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.lovelace.editor.raw_editor.error_parse_yaml",
|
||||
{ error: err }
|
||||
),
|
||||
});
|
||||
this._saving = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (isStrategyDashboard(config)) {
|
||||
assert(config, strategyStruct);
|
||||
@@ -281,8 +285,8 @@ class LovelaceFullConfigEditor extends LitElement {
|
||||
this._saving = false;
|
||||
}
|
||||
|
||||
private get yamlEditor(): HaYamlEditor {
|
||||
return this.shadowRoot!.querySelector("ha-yaml-editor")! as HaYamlEditor;
|
||||
private get yamlEditor(): HaCodeEditor {
|
||||
return this.shadowRoot!.querySelector("ha-code-editor")! as HaCodeEditor;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -409,8 +409,6 @@ class HUIRoot extends LitElement {
|
||||
slot="actionItems"
|
||||
.id="button-${index}"
|
||||
.path=${item.icon}
|
||||
.label=${label}
|
||||
hide-title
|
||||
@click=${item.buttonAction}
|
||||
></ha-icon-button>
|
||||
<ha-tooltip placement="bottom" .for="button-${index}">
|
||||
|
||||
@@ -4,13 +4,13 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { normalizeLuminance } from "../../common/color/palette";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-formfield";
|
||||
import "../../components/ha-radio";
|
||||
import type { HaRadio } from "../../components/ha-radio";
|
||||
import "../../components/ha-select";
|
||||
import type { HaSelectSelectEvent } from "../../components/ha-select";
|
||||
import "../../components/ha-settings-row";
|
||||
import "../../components/input/ha-input";
|
||||
import "../../components/radio/ha-radio-group";
|
||||
import type { HaRadioGroup } from "../../components/radio/ha-radio-group";
|
||||
import "../../components/radio/ha-radio-option";
|
||||
import {
|
||||
saveThemePreferences,
|
||||
subscribeThemePreferences,
|
||||
@@ -112,30 +112,45 @@ export class HaPickThemeRow extends SubscribeMixin(LitElement) {
|
||||
this.hass.themes.default_dark_theme &&
|
||||
this.hass.themes.default_theme) ||
|
||||
this._supportsModeSelection(curTheme)
|
||||
? html`<div class="inputs">
|
||||
<ha-radio-group
|
||||
@change=${this._handleDarkMode}
|
||||
name="dark_mode"
|
||||
.ariaLabel=${this.hass.localize(
|
||||
"ui.panel.profile.themes.theme_mode"
|
||||
? html` <div class="inputs">
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.profile.themes.dark_mode.auto"
|
||||
)}
|
||||
.value=${themeSettings?.dark === undefined
|
||||
? "auto"
|
||||
: themeSettings.dark
|
||||
? "dark"
|
||||
: "light"}
|
||||
orientation="horizontal"
|
||||
>
|
||||
<ha-radio-option value="auto">
|
||||
${this.hass.localize("ui.panel.profile.themes.dark_mode.auto")}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="light">
|
||||
${this.hass.localize("ui.panel.profile.themes.dark_mode.light")}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="dark">
|
||||
${this.hass.localize("ui.panel.profile.themes.dark_mode.dark")}
|
||||
</ha-radio-option>
|
||||
</ha-radio-group>
|
||||
<ha-radio
|
||||
@change=${this._handleDarkMode}
|
||||
name="dark_mode"
|
||||
value="auto"
|
||||
.checked=${themeSettings?.dark === undefined}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.profile.themes.dark_mode.light"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleDarkMode}
|
||||
name="dark_mode"
|
||||
value="light"
|
||||
.checked=${themeSettings?.dark === false}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.profile.themes.dark_mode.dark"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleDarkMode}
|
||||
name="dark_mode"
|
||||
value="dark"
|
||||
.checked=${themeSettings?.dark === true}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
${curTheme === HOME_ASSISTANT_THEME
|
||||
? html`<div class="color-pickers">
|
||||
<ha-input
|
||||
@@ -232,9 +247,9 @@ export class HaPickThemeRow extends SubscribeMixin(LitElement) {
|
||||
return !!(theme.modes && "light" in theme.modes && "dark" in theme.modes);
|
||||
}
|
||||
|
||||
private _handleDarkMode(ev: Event) {
|
||||
private _handleDarkMode(ev: CustomEvent) {
|
||||
let dark: boolean | undefined;
|
||||
switch ((ev.currentTarget as HaRadioGroup).value) {
|
||||
switch ((ev.target as HaRadio).value) {
|
||||
case "light":
|
||||
dark = false;
|
||||
break;
|
||||
@@ -303,10 +318,8 @@ export class HaPickThemeRow extends SubscribeMixin(LitElement) {
|
||||
justify-content: space-between;
|
||||
margin: 0 12px;
|
||||
}
|
||||
ha-radio-group {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-inline-end: var(--ha-space-3);
|
||||
ha-formfield {
|
||||
margin: 0 4px;
|
||||
}
|
||||
.color-pickers {
|
||||
display: flex;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user