mirror of
https://github.com/home-assistant/frontend.git
synced 2026-05-01 15:03:05 +00:00
Compare commits
17 Commits
add-contro
...
devtools-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
446c2e0ac3 | ||
|
|
cf6a1d46d0 | ||
|
|
fcaa42705f | ||
|
|
41515f642b | ||
|
|
4fbb9e7a02 | ||
|
|
7aa9805ef0 | ||
|
|
3ca9cd93a5 | ||
|
|
c90a29f082 | ||
|
|
d4fea44844 | ||
|
|
54a5e480c0 | ||
|
|
2f04ca9647 | ||
|
|
b3f334add4 | ||
|
|
cc759df646 | ||
|
|
d1b2d4e9f3 | ||
|
|
bc4437b3b5 | ||
|
|
c99b43dcf3 | ||
|
|
8945b917b3 |
@@ -54,7 +54,7 @@
|
||||
"@fullcalendar/list": "6.1.20",
|
||||
"@fullcalendar/luxon3": "6.1.20",
|
||||
"@fullcalendar/timegrid": "6.1.20",
|
||||
"@home-assistant/webawesome": "3.3.1-ha.1",
|
||||
"@home-assistant/webawesome": "3.3.1-ha.2",
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"@lit-labs/motion": "1.1.0",
|
||||
"@lit-labs/observers": "2.1.0",
|
||||
|
||||
42
src/components/ha-code-editor-jinja-arg-hover.ts
Normal file
42
src/components/ha-code-editor-jinja-arg-hover.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { CompletionItem } from "./ha-code-editor-completion-items";
|
||||
import "./ha-code-editor-completion-items";
|
||||
|
||||
@customElement("ha-code-editor-jinja-arg-hover")
|
||||
export class HaCodeEditorJinjaArgHover extends LitElement {
|
||||
/** Bold heading shown above the items grid (e.g. entity/device/area name). */
|
||||
@property({ attribute: false }) public heading?: string;
|
||||
|
||||
@property({ attribute: false }) public items: CompletionItem[] = [];
|
||||
|
||||
render() {
|
||||
return html`
|
||||
${this.heading
|
||||
? html`<div class="heading">${this.heading}</div>`
|
||||
: nothing}
|
||||
<ha-code-editor-completion-items
|
||||
.items=${this.items}
|
||||
></ha-code-editor-completion-items>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: 6px 10px;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-code-editor-jinja-arg-hover": HaCodeEditorJinjaArgHover;
|
||||
}
|
||||
}
|
||||
101
src/components/ha-code-editor-jinja-hover.ts
Normal file
101
src/components/ha-code-editor-jinja-hover.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import type { Completion } from "@codemirror/autocomplete";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { mdiHelpCircleOutline } from "@mdi/js";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-code-editor-jinja-hover")
|
||||
export class HaCodeEditorJinjaHover extends LitElement {
|
||||
@property({ attribute: false }) public completion!: Completion;
|
||||
|
||||
@property({ attribute: false }) public docUrl?: string;
|
||||
|
||||
@property({ attribute: false }) public openDocumentation =
|
||||
"Open documentation";
|
||||
|
||||
render() {
|
||||
const info =
|
||||
typeof this.completion.info === "string"
|
||||
? this.completion.info
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<div class="header">
|
||||
<div class="sig">
|
||||
<strong>${this.completion.label}</strong>
|
||||
${this.completion.detail
|
||||
? html`<span class="detail">(${this.completion.detail})</span>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this.docUrl
|
||||
? html`<a
|
||||
class="doc-link"
|
||||
href=${this.docUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title=${this.openDocumentation}
|
||||
><ha-svg-icon .path=${mdiHelpCircleOutline}></ha-svg-icon
|
||||
></a>`
|
||||
: nothing}
|
||||
</div>
|
||||
${info ? html`<div class="desc">${info}</div>` : nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: 6px 10px;
|
||||
max-width: 360px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.sig {
|
||||
font-family: var(--ha-font-family-code);
|
||||
font-size: 0.9em;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.detail {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.doc-link {
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: var(--secondary-text-color);
|
||||
opacity: 0.7;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.doc-link:hover {
|
||||
opacity: 1;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.doc-link ha-svg-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 0.9em;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-code-editor-jinja-hover": HaCodeEditorJinjaHover;
|
||||
}
|
||||
}
|
||||
@@ -36,9 +36,13 @@ import { computeAreaName } from "../common/entity/compute_area_name";
|
||||
import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||
import { copyToClipboard } from "../common/util/copy-clipboard";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { JinjaArgType } from "../resources/jinja_ha_completions";
|
||||
import type {
|
||||
JinjaArgType,
|
||||
HassArgHoverContext,
|
||||
} from "../resources/jinja_ha_completions";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { showToast } from "../util/toast";
|
||||
import { documentationUrl } from "../util/documentation-url";
|
||||
import { labelsContext } from "../data/context";
|
||||
import type { LabelRegistryEntry } from "../data/label/label_registry";
|
||||
import "./ha-code-editor-completion-items";
|
||||
@@ -387,6 +391,16 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
this._loadedCodeMirror.tooltips({
|
||||
position: "absolute",
|
||||
}),
|
||||
this._loadedCodeMirror.hoverTooltip(
|
||||
(view, pos) =>
|
||||
this._loadedCodeMirror!.haJinjaHoverSource(
|
||||
view,
|
||||
pos,
|
||||
this.hass ? documentationUrl(this.hass, "") : undefined,
|
||||
this.hass ? this._hassArgHoverContext() : undefined
|
||||
),
|
||||
{ hoverTime: 300 }
|
||||
),
|
||||
...(this.placeholder ? [placeholder(this.placeholder)] : []),
|
||||
];
|
||||
|
||||
@@ -636,6 +650,48 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds a HassArgHoverContext from the current hass object so that
|
||||
* haJinjaHoverSource can resolve entity / device / area friendly names
|
||||
* without importing the full HomeAssistant type into the resource file.
|
||||
*/
|
||||
private _hassArgHoverContext(): HassArgHoverContext {
|
||||
const hass = this.hass!;
|
||||
const labelMap: Record<
|
||||
string,
|
||||
{ name: string; description?: string | null }
|
||||
> = {};
|
||||
for (const label of this._labels ?? []) {
|
||||
labelMap[label.label_id] = {
|
||||
name: label.name,
|
||||
description: label.description,
|
||||
};
|
||||
}
|
||||
return {
|
||||
states: hass.states as HassArgHoverContext["states"],
|
||||
devices: hass.devices as HassArgHoverContext["devices"],
|
||||
areas: hass.areas as HassArgHoverContext["areas"],
|
||||
floors: hass.floors as HassArgHoverContext["floors"],
|
||||
entities: hass.entities as HassArgHoverContext["entities"],
|
||||
labels: labelMap,
|
||||
formatEntityState: (entityId) =>
|
||||
hass.formatEntityState(hass.states[entityId]),
|
||||
formatEntityName: (entityId) => {
|
||||
const stateObj = hass.states[entityId];
|
||||
return (
|
||||
(stateObj?.attributes.friendly_name as string | undefined) ??
|
||||
hass.entities[entityId]?.name ??
|
||||
undefined
|
||||
);
|
||||
},
|
||||
formatAttributeName: (entityId, attribute) =>
|
||||
hass.formatEntityAttributeName(hass.states[entityId], attribute),
|
||||
formatAttributeValue: (entityId, attribute) =>
|
||||
hass.formatEntityAttributeValue(hass.states[entityId], attribute),
|
||||
localize: (key) => hass.localize(key as never),
|
||||
};
|
||||
}
|
||||
|
||||
private _renderInfo = (completion: Completion): CompletionInfo => {
|
||||
const key =
|
||||
typeof completion.apply === "string"
|
||||
|
||||
@@ -53,7 +53,10 @@ export class HaIconButton extends LitElement {
|
||||
.download=${this.download}
|
||||
>
|
||||
${this.path
|
||||
? html`<ha-svg-icon .path=${this.path}></ha-svg-icon>`
|
||||
? html`<ha-svg-icon
|
||||
aria-hidden="true"
|
||||
.path=${this.path}
|
||||
></ha-svg-icon>`
|
||||
: html`<span><slot></slot></span>`}
|
||||
</ha-button>
|
||||
`;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiMagnify } from "@mdi/js";
|
||||
import { css, html, type PropertyValues } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { internationalizationContext } from "../../data/context";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { HaInput } from "./ha-input";
|
||||
|
||||
/**
|
||||
@@ -17,10 +15,6 @@ 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;
|
||||
@@ -35,7 +29,7 @@ export class HaInputSearch extends HaInput {
|
||||
!this.placeholder &&
|
||||
(!this.hasUpdated || changedProps.has("_i18n"))
|
||||
) {
|
||||
this.placeholder = this._i18n.localize("ui.common.search");
|
||||
this.placeholder = this.i18n?.localize?.("ui.common.search") || "Search";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,19 +2,21 @@ 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 {
|
||||
LitElement,
|
||||
type PropertyValues,
|
||||
type TemplateResult,
|
||||
css,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
type PropertyValues,
|
||||
type TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property, query, state } 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";
|
||||
@@ -125,6 +127,10 @@ 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",
|
||||
@@ -233,19 +239,22 @@ export class HaInput extends WaInputMixin(LitElement) {
|
||||
${this.renderStartDefault()}
|
||||
</slot>
|
||||
<slot name="end" slot="end"> ${this.renderEndDefault()} </slot>
|
||||
<slot name="clear-icon" slot="clear-icon">
|
||||
<ha-icon-button .path=${mdiClose}></ha-icon-button>
|
||||
</slot>
|
||||
<slot name="show-password-icon" slot="show-password-icon">
|
||||
<slot name="clear-button" slot="clear-button">
|
||||
<ha-icon-button
|
||||
@keydown=${stopPropagation}
|
||||
.path=${mdiEye}
|
||||
@click=${this._handleClearClick}
|
||||
.label=${this.i18n?.localize?.("ui.components.input.clear") ||
|
||||
"Clear"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</slot>
|
||||
<slot name="hide-password-icon" slot="hide-password-icon">
|
||||
<slot name="password-toggle-button" slot="password-toggle-button">
|
||||
<ha-icon-button
|
||||
@keydown=${stopPropagation}
|
||||
.path=${mdiEyeOff}
|
||||
@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}
|
||||
></ha-icon-button>
|
||||
</slot>
|
||||
<div
|
||||
@@ -293,6 +302,14 @@ export class HaInput extends WaInputMixin(LitElement) {
|
||||
}
|
||||
};
|
||||
|
||||
private _handleClearClick() {
|
||||
this._input?.clear();
|
||||
}
|
||||
|
||||
private _handlePasswordToggle() {
|
||||
this.passwordVisible = !this.passwordVisible;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
waInputStyles,
|
||||
css`
|
||||
@@ -414,6 +431,12 @@ 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;
|
||||
|
||||
110
src/mixins/match-min-height-mixin.ts
Normal file
110
src/mixins/match-min-height-mixin.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
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;
|
||||
};
|
||||
@@ -5,9 +5,11 @@ 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 {
|
||||
@@ -23,6 +25,7 @@ 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";
|
||||
@@ -39,12 +42,14 @@ import {
|
||||
serviceCallWillDisconnect,
|
||||
} from "../../../../data/service";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { HomeAssistant, ToggleButton } 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 LitElement {
|
||||
class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
@@ -80,6 +85,12 @@ class HaPanelDevAction extends 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 &&
|
||||
@@ -117,6 +128,21 @@ class HaPanelDevAction extends 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;
|
||||
@@ -132,14 +158,34 @@ class HaPanelDevAction extends 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">
|
||||
? html`<div
|
||||
class="card-content"
|
||||
style=${styleMap(this._matchMinHeightStyle)}
|
||||
>
|
||||
<ha-service-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._serviceData?.action}
|
||||
@@ -161,44 +207,27 @@ class HaPanelDevAction extends LitElement {
|
||||
show-advanced
|
||||
show-service-id
|
||||
@value-changed=${this._serviceDataChanged}
|
||||
class="card-content"
|
||||
class="card-content ui-mode-content"
|
||||
></ha-service-control>
|
||||
`}
|
||||
${this._error !== undefined
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
</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>
|
||||
<div class="card-actions">
|
||||
${!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-progress-button raised @click=${this._callService}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.actions.call_service"
|
||||
)}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
${this._response?.result
|
||||
? html`<div class="content response">
|
||||
@@ -439,7 +468,7 @@ class HaPanelDevAction extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private async _callService(ev) {
|
||||
private async _callService(ev: Event) {
|
||||
const button = ev.currentTarget as HaProgressButton;
|
||||
|
||||
if (this._yamlMode && !this._yamlValid) {
|
||||
@@ -560,13 +589,20 @@ class HaPanelDevAction extends LitElement {
|
||||
button.actionSuccess();
|
||||
}
|
||||
|
||||
private _toggleYaml() {
|
||||
this._yamlMode = !this._yamlMode;
|
||||
this._yamlValid = true;
|
||||
this._error = undefined;
|
||||
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 _yamlChanged(ev) {
|
||||
private _yamlChanged(ev: HASSDomEvent<{ value: any; isValid: boolean }>) {
|
||||
if (!ev.detail.isValid) {
|
||||
this._yamlValid = false;
|
||||
return;
|
||||
@@ -602,7 +638,7 @@ class HaPanelDevAction extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _serviceDataChanged(ev) {
|
||||
private _serviceDataChanged(ev: HASSDomEvent<{ value: any }>) {
|
||||
if (this._serviceData?.action !== ev.detail.value.action) {
|
||||
this._error = undefined;
|
||||
}
|
||||
@@ -610,7 +646,7 @@ class HaPanelDevAction extends LitElement {
|
||||
this._checkUiSupported();
|
||||
}
|
||||
|
||||
private _serviceChanged(ev) {
|
||||
private _serviceChanged(ev: HASSDomEvent<{ value: any }>) {
|
||||
ev.stopPropagation();
|
||||
if (ev.detail.value) {
|
||||
this._serviceData = { action: ev.detail.value, data: {} };
|
||||
@@ -667,30 +703,55 @@ class HaPanelDevAction extends LitElement {
|
||||
max-width: 1200px;
|
||||
margin: auto;
|
||||
}
|
||||
.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%;
|
||||
}
|
||||
.button-row .buttons {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
max-width: 1200px;
|
||||
margin: auto;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-1);
|
||||
}
|
||||
.switch-mode-container {
|
||||
.header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
.switch-mode-container .error {
|
||||
margin-left: var(--ha-space-2);
|
||||
margin-inline-start: var(--ha-space-2);
|
||||
margin-inline-end: initial;
|
||||
.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 {
|
||||
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);
|
||||
}
|
||||
.attributes {
|
||||
width: 100%;
|
||||
|
||||
@@ -9,6 +9,7 @@ import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-code-editor";
|
||||
import "../../../../components/ha-spinner";
|
||||
import "../../../../components/ha-tip";
|
||||
import type { RenderTemplateResult } from "../../../../data/ws-templates";
|
||||
import { subscribeRenderTemplate } from "../../../../data/ws-templates";
|
||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
@@ -158,6 +159,14 @@ class HaPanelDevTemplate extends LitElement {
|
||||
${this.hass.localize("ui.common.clear")}
|
||||
</ha-button>
|
||||
</div>
|
||||
<ha-tip .hass=${this.hass}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.templates.keyboard_tip",
|
||||
{
|
||||
autocomplete: html`<kbd>Ctrl</kbd>+<kbd>Space</kbd>`,
|
||||
}
|
||||
)}
|
||||
</ha-tip>
|
||||
</ha-card>
|
||||
|
||||
<ha-card
|
||||
@@ -361,6 +370,22 @@ ${type === "object"
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
ha-tip {
|
||||
padding: 0 var(--ha-space-4) var(--ha-space-4);
|
||||
display: block;
|
||||
}
|
||||
|
||||
kbd {
|
||||
display: inline-block;
|
||||
font-family: var(--ha-font-family-code);
|
||||
font-size: 0.85em;
|
||||
padding: 1px 5px;
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: var(--ha-border-radius-xs);
|
||||
background-color: var(--secondary-background-color);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
.content ha-card {
|
||||
max-width: 100%;
|
||||
|
||||
@@ -409,6 +409,8 @@ 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}">
|
||||
|
||||
@@ -44,6 +44,7 @@ export {
|
||||
drawSelection,
|
||||
EditorView,
|
||||
highlightActiveLine,
|
||||
hoverTooltip,
|
||||
keymap,
|
||||
lineNumbers,
|
||||
rectangularSelection,
|
||||
@@ -71,9 +72,10 @@ export const closeBracketsOverride = Prec.highest(
|
||||
|
||||
export {
|
||||
haJinjaCompletionSource,
|
||||
haJinjaHoverSource,
|
||||
JINJA_FUNCTION_ARG_TYPES,
|
||||
} from "./jinja_ha_completions";
|
||||
export type { JinjaArgType } from "./jinja_ha_completions";
|
||||
export type { HassArgHoverContext, JinjaArgType } from "./jinja_ha_completions";
|
||||
export { closePercentBrace };
|
||||
|
||||
export const langCompartment = new Compartment();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -491,6 +491,11 @@
|
||||
"markdown": "Markdown"
|
||||
},
|
||||
"components": {
|
||||
"input": {
|
||||
"clear": "Clear",
|
||||
"show_password": "Show password",
|
||||
"hide_password": "Hide password"
|
||||
},
|
||||
"selectors": {
|
||||
"serial_port": {
|
||||
"enter_manually": "Enter manually",
|
||||
@@ -1498,6 +1503,9 @@
|
||||
"area_label": "Area",
|
||||
"description": "Configure which areas correspond to each vacuum segment"
|
||||
},
|
||||
"codemirror": {
|
||||
"open_documentation": "Open documentation"
|
||||
},
|
||||
"safe_mode": {
|
||||
"title": "Safe mode",
|
||||
"text": "Home Assistant is running in safe mode, custom integrations and community frontend modules are not available. Restart Home Assistant to exit safe mode."
|
||||
@@ -3815,8 +3823,8 @@
|
||||
"column_description": "Description",
|
||||
"column_example": "Example",
|
||||
"fill_example_data": "Fill example data",
|
||||
"yaml_mode": "Go to YAML mode",
|
||||
"ui_mode": "Go to UI mode",
|
||||
"yaml_mode": "YAML mode",
|
||||
"ui_mode": "UI mode",
|
||||
"yaml_parameters": "Parameters only available in YAML mode",
|
||||
"all_parameters": "All available parameters",
|
||||
"accepts_target": "This action accepts a target, for example: `entity_id: light.bed_light`",
|
||||
@@ -3876,7 +3884,8 @@
|
||||
"no_listeners": "This template does not listen for any events and will not update automatically.",
|
||||
"listeners": "This template listens for the following state changed events:",
|
||||
"entity": "Entity",
|
||||
"domain": "Domain"
|
||||
"domain": "Domain",
|
||||
"keyboard_tip": "Press {autocomplete} to trigger autocomplete, when your cursor is inside a function that supports it."
|
||||
},
|
||||
"statistics": {
|
||||
"title": "Statistics",
|
||||
|
||||
10
yarn.lock
10
yarn.lock
@@ -1857,9 +1857,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@home-assistant/webawesome@npm:3.3.1-ha.1":
|
||||
version: 3.3.1-ha.1
|
||||
resolution: "@home-assistant/webawesome@npm:3.3.1-ha.1"
|
||||
"@home-assistant/webawesome@npm:3.3.1-ha.2":
|
||||
version: 3.3.1-ha.2
|
||||
resolution: "@home-assistant/webawesome@npm:3.3.1-ha.2"
|
||||
dependencies:
|
||||
"@ctrl/tinycolor": "npm:4.1.0"
|
||||
"@floating-ui/dom": "npm:^1.6.13"
|
||||
@@ -1870,7 +1870,7 @@ __metadata:
|
||||
lit: "npm:^3.2.1"
|
||||
nanoid: "npm:^5.1.5"
|
||||
qr-creator: "npm:^1.0.0"
|
||||
checksum: 10/903b7e5e44a9c4afa5120b5fbbf139880889798a82262b10b5c5fa51cd56cd344d8027d69658b15c85efc579688476bbd8e5eff73535f903959bbd9ffd00051f
|
||||
checksum: 10/c66965544bb34eb9d6d131579c31e4c6d21c8c231bdf565db61195da03a995d17e25d6d997875c902d7d2f3020c70434088efb7299b2626d4a7d1343cb33d726
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -8144,7 +8144,7 @@ __metadata:
|
||||
"@fullcalendar/list": "npm:6.1.20"
|
||||
"@fullcalendar/luxon3": "npm:6.1.20"
|
||||
"@fullcalendar/timegrid": "npm:6.1.20"
|
||||
"@home-assistant/webawesome": "npm:3.3.1-ha.1"
|
||||
"@home-assistant/webawesome": "npm:3.3.1-ha.2"
|
||||
"@html-eslint/eslint-plugin": "npm:0.59.0"
|
||||
"@lezer/highlight": "npm:1.2.3"
|
||||
"@lit-labs/motion": "npm:1.1.0"
|
||||
|
||||
Reference in New Issue
Block a user