Compare commits

...

17 Commits

Author SHA1 Message Date
Aidan Timson
446c2e0ac3 Update src/panels/config/developer-tools/action/developer-tools-action.ts
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-04-30 13:36:51 +01:00
Aidan Timson
cf6a1d46d0 Move toggle group to right of title 2026-04-30 12:31:22 +01:00
Aidan Timson
fcaa42705f Types 2026-04-30 12:29:03 +01:00
Aidan Timson
41515f642b Rename mixin 2026-04-30 12:08:18 +01:00
Aidan Timson
4fbb9e7a02 Remove unneeded spread 2026-04-30 12:00:39 +01:00
Aidan Timson
7aa9805ef0 View transition between modes 2026-04-30 11:41:00 +01:00
Aidan Timson
3ca9cd93a5 Use min height mixin to preserve height when switching to yaml mode 2026-04-30 11:38:48 +01:00
Aidan Timson
c90a29f082 Improve content layout 2026-04-30 11:23:15 +01:00
Aidan Timson
d4fea44844 Remove memo 2026-04-30 11:13:55 +01:00
Aidan Timson
54a5e480c0 Toggle buttons 2026-04-30 11:10:55 +01:00
Aidan Timson
2f04ca9647 Center 2026-04-30 10:54:31 +01:00
Aidan Timson
b3f334add4 More standard 2026-04-30 10:52:37 +01:00
Aidan Timson
cc759df646 Move items into card 2026-04-30 10:46:46 +01:00
Aidan Timson
d1b2d4e9f3 Remove background (and borders) of devtools actions button row 2026-04-30 10:29:08 +01:00
Aidan Timson
bc4437b3b5 Ally: Add aria labels to ha-icon-button and hui-root (#51784)
* Ally: Add aria labels to ha-icon-button and hui-root

* use aria-hidden

* Add hidden content for label to satisfy ally review

* Make fix in button instead (probably should update upstream)

* Aria label (pending wa update)
2026-04-30 09:20:56 +00:00
Wendelin
c99b43dcf3 Use input button slots for a11y (#51801)
Co-authored-by: Copilot <copilot@github.com>
2026-04-30 09:12:23 +00:00
Bram Kragten
8945b917b3 Add tooltips for Jinja editors (#51792)
* Add descriptions to Jinja2 tags, filters, expressions, tests and variables

All standard Jinja2 tags, filters, and expression completions now carry
info and detail strings so the autocomplete info popover shows meaningful
documentation when users browse them — not just HA-specific functions.

* Add keyboard shortcut tip to the template developer tool

A ha-tip below the editor card now shows users that Ctrl+Space triggers
autocomplete, Ctrl+F opens the search panel, and F11 toggles fullscreen,
making the editor's built-in features more discoverable.

* Add hover tooltips for Jinja2 functions, filters and expressions

Hovering over a function, filter, tag, test, or variable name inside a
Jinja2 template shows a tooltip with its signature and description.
Non-tag completions also get a help-circle icon linking to the
corresponding Home Assistant template-functions documentation page.

The tooltip is rendered as a custom Lit element (ha-code-editor-jinja-hover)
that takes the Completion object and an optional docUrl as properties.

The tooltip source (haJinjaHoverSource) is wired into ha-code-editor
via CodeMirror's hoverTooltip extension. The documentationUrl() helper
is used so the link points to the correct subdomain (www / rc / next)
based on the running HA version.

* Add hover tooltips for Jinja2 hover + arg value tooltips for entity/device/area

Wire haJinjaHoverSource into ha-code-editor via CodeMirror hoverTooltip.
Two types of hover are now shown in jinja2/yaml mode:

- Hovering a function/filter/tag/expression name shows its signature,
  description, and a doc-link icon (non-tags only).
- Hovering a string-literal argument of a known HA Jinja function (e.g.
  states(), device_name(), area_entities()) shows the friendly name,
  current state, device, and area for entity_id arguments; the device
  name and area for device_id arguments; and the area name for area_id
  arguments. The same applies to states["entity_id"] subscripts.

The arg-value tooltip reuses CompletionItem / ha-code-editor-completion-items
(the same component used for autocomplete info popovers) via a new
ha-code-editor-jinja-arg-hover element. HA registry data is passed from
ha-code-editor via a HassArgHoverContext interface to keep jinja_ha_completions.ts
free of HomeAssistant type imports.

* only add tip for autocomplete

* review
2026-04-30 12:07:50 +03:00
15 changed files with 2061 additions and 291 deletions

View File

@@ -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",

View 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;
}
}

View 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;
}
}

View File

@@ -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"

View File

@@ -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>
`;

View File

@@ -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";
}
}

View File

@@ -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;

View 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;
};

View File

@@ -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%;

View File

@@ -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%;

View File

@@ -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}">

View File

@@ -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

View File

@@ -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",

View File

@@ -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"