mirror of
https://github.com/home-assistant/frontend.git
synced 2026-03-07 15:57:43 +00:00
Compare commits
20 Commits
log-classi
...
ha-input-w
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33a6b393d7 | ||
|
|
bebfae94c9 | ||
|
|
2a4ed177f2 | ||
|
|
11900ffcbb | ||
|
|
7230998afa | ||
|
|
419af46a87 | ||
|
|
12e5d1741e | ||
|
|
a7d8cd92e8 | ||
|
|
ef9cb98514 | ||
|
|
146c0fe01b | ||
|
|
be19dff1f8 | ||
|
|
9f87aca155 | ||
|
|
0be9846b11 | ||
|
|
fb307e76a8 | ||
|
|
9b720c8a9f | ||
|
|
643942f350 | ||
|
|
544a0c2971 | ||
|
|
1b367e85da | ||
|
|
820c8d7975 | ||
|
|
ab966d039a |
@@ -255,6 +255,10 @@ const createRspackConfig = ({
|
||||
"@formatjs/intl-relativetimeformat/should-polyfill.js",
|
||||
"@formatjs/intl-relativetimeformat/polyfill-force":
|
||||
"@formatjs/intl-relativetimeformat/polyfill-force.js",
|
||||
"@home-assistant/webawesome/dist/internal/slot": path.resolve(
|
||||
__dirname,
|
||||
"../node_modules/@home-assistant/webawesome/dist/internal/slot.js"
|
||||
),
|
||||
},
|
||||
},
|
||||
output: {
|
||||
|
||||
@@ -200,7 +200,7 @@
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "28.1.0",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "16.3.0",
|
||||
"lint-staged": "16.2.7",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.5.0",
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
/* eslint-disable lit/prefer-static-styles */
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { HaFormString } from "../components/ha-form/ha-form-string";
|
||||
import "../components/ha-icon-button";
|
||||
import "./ha-auth-textfield";
|
||||
import "../components/input/ha-input";
|
||||
|
||||
@customElement("ha-auth-form-string")
|
||||
export class HaAuthFormString extends HaFormString {
|
||||
@@ -12,63 +9,13 @@ export class HaAuthFormString extends HaFormString {
|
||||
return this;
|
||||
}
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this.querySelector("ha-auth-textfield")?.reportValidity() ?? true;
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.style.position = "relative";
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
ha-auth-form-string {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
ha-auth-form-string[own-margin] {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
ha-auth-form-string ha-auth-textfield {
|
||||
display: block !important;
|
||||
}
|
||||
ha-auth-form-string ha-icon-button {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 8px;
|
||||
--ha-icon-button-size: 40px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
</style>
|
||||
<ha-auth-textfield
|
||||
.type=${!this.isPassword
|
||||
? this.stringType
|
||||
: this.unmaskedPassword
|
||||
? "text"
|
||||
: "password"}
|
||||
.label=${this.label}
|
||||
.value=${this.data || ""}
|
||||
.helper=${this.helper}
|
||||
helperPersistent
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.schema.required}
|
||||
.autoValidate=${this.schema.required}
|
||||
.name=${this.schema.name}
|
||||
.autocomplete=${this.schema.autocomplete}
|
||||
?autofocus=${this.schema.autofocus}
|
||||
.suffix=${this.isPassword
|
||||
? // reserve some space for the icon.
|
||||
html`<div style="width: 24px"></div>`
|
||||
: this.schema.description?.suffix}
|
||||
.validationMessage=${this.schema.required
|
||||
? this.localize?.("ui.panel.page-authorize.form.error_required")
|
||||
: undefined}
|
||||
@input=${this._valueChanged}
|
||||
@change=${this._valueChanged}
|
||||
></ha-auth-textfield>
|
||||
${this.renderIcon()}
|
||||
`;
|
||||
public reportValidity(): boolean {
|
||||
return this.querySelector("ha-input")?.reportValidity() ?? true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/* eslint-disable lit/prefer-static-styles */
|
||||
import { html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { HaForm } from "../components/ha-form/ha-form";
|
||||
import "./ha-auth-form-string";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
|
||||
const localizeBaseKey = "ui.panel.page-authorize.form";
|
||||
|
||||
@@ -34,6 +34,9 @@ export class HaAuthForm extends HaForm {
|
||||
protected render() {
|
||||
return html`
|
||||
<style>
|
||||
ha-auth-form {
|
||||
--ha-input-required-marker: "";
|
||||
}
|
||||
ha-auth-form .root > * {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -1,264 +0,0 @@
|
||||
/* eslint-disable lit/value-after-constraints */
|
||||
/* eslint-disable lit/prefer-static-styles */
|
||||
import { floatingLabel } from "@material/mwc-floating-label/mwc-floating-label-directive";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { live } from "lit/directives/live";
|
||||
import { HaTextField } from "../components/ha-textfield";
|
||||
|
||||
@customElement("ha-auth-textfield")
|
||||
export class HaAuthTextField extends HaTextField {
|
||||
protected renderLabel(): TemplateResult | string {
|
||||
return !this.label
|
||||
? ""
|
||||
: html`
|
||||
<span
|
||||
.floatingLabelFoundation=${floatingLabel(
|
||||
this.label
|
||||
) as unknown as any}
|
||||
.id=${this.name}
|
||||
>${this.label}</span
|
||||
>
|
||||
`;
|
||||
}
|
||||
|
||||
protected renderInput(shouldRenderHelperText: boolean): TemplateResult {
|
||||
const minOrUndef = this.minLength === -1 ? undefined : this.minLength;
|
||||
const maxOrUndef = this.maxLength === -1 ? undefined : this.maxLength;
|
||||
const autocapitalizeOrUndef = this.autocapitalize
|
||||
? (this.autocapitalize as
|
||||
| "off"
|
||||
| "none"
|
||||
| "on"
|
||||
| "sentences"
|
||||
| "words"
|
||||
| "characters")
|
||||
: undefined;
|
||||
const showValidationMessage = this.validationMessage && !this.isUiValid;
|
||||
const ariaLabelledbyOrUndef = this.label ? this.name : undefined;
|
||||
const ariaControlsOrUndef = shouldRenderHelperText
|
||||
? "helper-text"
|
||||
: undefined;
|
||||
const ariaDescribedbyOrUndef =
|
||||
this.focused || this.helperPersistent || showValidationMessage
|
||||
? "helper-text"
|
||||
: undefined;
|
||||
// TODO: live() directive needs casting for lit-analyzer
|
||||
// https://github.com/runem/lit-analyzer/pull/91/files
|
||||
// TODO: lit-analyzer labels min/max as (number|string) instead of string
|
||||
return html`<input
|
||||
aria-labelledby=${ifDefined(ariaLabelledbyOrUndef)}
|
||||
aria-controls=${ifDefined(ariaControlsOrUndef)}
|
||||
aria-describedby=${ifDefined(ariaDescribedbyOrUndef)}
|
||||
class="mdc-text-field__input"
|
||||
type=${this.type}
|
||||
.value=${live(this.value) as unknown as string}
|
||||
?disabled=${this.disabled}
|
||||
placeholder=${this.placeholder}
|
||||
?required=${this.required}
|
||||
?readonly=${this.readOnly}
|
||||
minlength=${ifDefined(minOrUndef)}
|
||||
maxlength=${ifDefined(maxOrUndef)}
|
||||
pattern=${ifDefined(this.pattern ? this.pattern : undefined)}
|
||||
min=${ifDefined(this.min === "" ? undefined : (this.min as number))}
|
||||
max=${ifDefined(this.max === "" ? undefined : (this.max as number))}
|
||||
step=${ifDefined(this.step === null ? undefined : (this.step as number))}
|
||||
size=${ifDefined(this.size === null ? undefined : this.size)}
|
||||
name=${ifDefined(this.name === "" ? undefined : this.name)}
|
||||
inputmode=${ifDefined(this.inputMode)}
|
||||
autocapitalize=${ifDefined(autocapitalizeOrUndef)}
|
||||
?autofocus=${this.autofocus}
|
||||
@input=${this.handleInputChange}
|
||||
@focus=${this.onInputFocus}
|
||||
@blur=${this.onInputBlur}
|
||||
/>`;
|
||||
}
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<style>
|
||||
ha-auth-textfield {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
outline: none;
|
||||
}
|
||||
ha-auth-textfield:not([disabled]):hover
|
||||
:not(.mdc-text-field--invalid):not(.mdc-text-field--focused)
|
||||
mwc-notched-outline {
|
||||
--mdc-notched-outline-border-color: var(
|
||||
--mdc-text-field-outlined-hover-border-color,
|
||||
rgba(0, 0, 0, 0.87)
|
||||
);
|
||||
}
|
||||
|
||||
ha-auth-textfield:not([disabled])
|
||||
.mdc-text-field:not(.mdc-text-field--outlined) {
|
||||
background-color: var(--mdc-text-field-fill-color, whitesmoke);
|
||||
}
|
||||
|
||||
ha-auth-textfield:not([disabled])
|
||||
.mdc-text-field.mdc-text-field--invalid
|
||||
mwc-notched-outline {
|
||||
--mdc-notched-outline-border-color: var(
|
||||
--mdc-text-field-error-color,
|
||||
var(--mdc-theme-error, #b00020)
|
||||
);
|
||||
}
|
||||
|
||||
ha-auth-textfield:not([disabled])
|
||||
.mdc-text-field.mdc-text-field--invalid
|
||||
+ .mdc-text-field-helper-line
|
||||
.mdc-text-field-character-counter,
|
||||
ha-auth-textfield:not([disabled])
|
||||
.mdc-text-field.mdc-text-field--invalid
|
||||
.mdc-text-field__icon {
|
||||
color: var(
|
||||
--mdc-text-field-error-color,
|
||||
var(--mdc-theme-error, #b00020)
|
||||
);
|
||||
}
|
||||
|
||||
ha-auth-textfield:not([disabled])
|
||||
.mdc-text-field:not(.mdc-text-field--invalid):not(
|
||||
.mdc-text-field--focused
|
||||
)
|
||||
.mdc-floating-label,
|
||||
ha-auth-textfield:not([disabled])
|
||||
.mdc-text-field:not(.mdc-text-field--invalid):not(
|
||||
.mdc-text-field--focused
|
||||
)
|
||||
.mdc-floating-label::after {
|
||||
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
|
||||
}
|
||||
|
||||
ha-auth-textfield:not([disabled])
|
||||
.mdc-text-field.mdc-text-field--focused
|
||||
mwc-notched-outline {
|
||||
--mdc-notched-outline-stroke-width: 2px;
|
||||
}
|
||||
|
||||
ha-auth-textfield:not([disabled])
|
||||
.mdc-text-field.mdc-text-field--focused:not(.mdc-text-field--invalid)
|
||||
mwc-notched-outline {
|
||||
--mdc-notched-outline-border-color: var(
|
||||
--mdc-text-field-focused-label-color,
|
||||
var(--mdc-theme-primary, rgba(98, 0, 238, 0.87))
|
||||
);
|
||||
}
|
||||
|
||||
ha-auth-textfield:not([disabled])
|
||||
.mdc-text-field.mdc-text-field--focused:not(.mdc-text-field--invalid)
|
||||
.mdc-floating-label {
|
||||
color: #6200ee;
|
||||
color: var(--mdc-theme-primary, #6200ee);
|
||||
}
|
||||
|
||||
ha-auth-textfield:not([disabled])
|
||||
.mdc-text-field
|
||||
.mdc-text-field__input {
|
||||
color: var(--mdc-text-field-ink-color, rgba(0, 0, 0, 0.87));
|
||||
}
|
||||
|
||||
ha-auth-textfield:not([disabled])
|
||||
.mdc-text-field
|
||||
.mdc-text-field__input::placeholder {
|
||||
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
|
||||
}
|
||||
|
||||
ha-auth-textfield:not([disabled])
|
||||
.mdc-text-field-helper-line
|
||||
.mdc-text-field-helper-text:not(
|
||||
.mdc-text-field-helper-text--validation-msg
|
||||
),
|
||||
ha-auth-textfield:not([disabled])
|
||||
.mdc-text-field-helper-line:not(.mdc-text-field--invalid)
|
||||
.mdc-text-field-character-counter {
|
||||
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
|
||||
}
|
||||
|
||||
ha-auth-textfield[disabled]
|
||||
.mdc-text-field:not(.mdc-text-field--outlined) {
|
||||
background-color: var(--mdc-text-field-disabled-fill-color, #fafafa);
|
||||
}
|
||||
|
||||
ha-auth-textfield[disabled]
|
||||
.mdc-text-field.mdc-text-field--outlined
|
||||
mwc-notched-outline {
|
||||
--mdc-notched-outline-border-color: var(
|
||||
--mdc-text-field-outlined-disabled-border-color,
|
||||
rgba(0, 0, 0, 0.06)
|
||||
);
|
||||
}
|
||||
|
||||
ha-auth-textfield[disabled]
|
||||
.mdc-text-field:not(.mdc-text-field--invalid):not(
|
||||
.mdc-text-field--focused
|
||||
)
|
||||
.mdc-floating-label,
|
||||
ha-auth-textfield[disabled]
|
||||
.mdc-text-field:not(.mdc-text-field--invalid):not(
|
||||
.mdc-text-field--focused
|
||||
)
|
||||
.mdc-floating-label::after {
|
||||
color: var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.38));
|
||||
}
|
||||
|
||||
ha-auth-textfield[disabled] .mdc-text-field .mdc-text-field__input,
|
||||
ha-auth-textfield[disabled]
|
||||
.mdc-text-field
|
||||
.mdc-text-field__input::placeholder {
|
||||
color: var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.38));
|
||||
}
|
||||
|
||||
ha-auth-textfield[disabled]
|
||||
.mdc-text-field-helper-line
|
||||
.mdc-text-field-helper-text,
|
||||
ha-auth-textfield[disabled]
|
||||
.mdc-text-field-helper-line
|
||||
.mdc-text-field-character-counter {
|
||||
color: var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.38));
|
||||
}
|
||||
ha-auth-textfield:not([disabled])
|
||||
.mdc-text-field.mdc-text-field--focused:not(.mdc-text-field--invalid)
|
||||
.mdc-floating-label {
|
||||
color: var(--mdc-theme-primary, #6200ee);
|
||||
}
|
||||
ha-auth-textfield[no-spinner] input::-webkit-outer-spin-button,
|
||||
ha-auth-textfield[no-spinner] input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
ha-auth-textfield[no-spinner] input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
</style>
|
||||
${super.render()}
|
||||
`;
|
||||
}
|
||||
|
||||
protected createRenderRoot() {
|
||||
// add parent style to light dom
|
||||
const style = document.createElement("style");
|
||||
style.textContent = HaTextField.elementStyles as unknown as string;
|
||||
this.append(style);
|
||||
return this;
|
||||
}
|
||||
|
||||
public firstUpdated() {
|
||||
super.firstUpdated();
|
||||
|
||||
if (this.autofocus) {
|
||||
this.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-auth-textfield": HaAuthTextField;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../ha-sortable";
|
||||
import "../input/ha-input-label";
|
||||
import "./ha-entity-picker";
|
||||
|
||||
@customElement("ha-entities-picker")
|
||||
@@ -88,45 +89,55 @@ class HaEntitiesPicker extends LitElement {
|
||||
|
||||
const currentEntities = this._currentEntities;
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<ha-sortable
|
||||
.disabled=${!this.reorder || this.disabled}
|
||||
handle-selector=".entity-handle"
|
||||
@item-moved=${this._entityMoved}
|
||||
>
|
||||
<div class="list">
|
||||
${currentEntities.map(
|
||||
(entityId) => html`
|
||||
<div class="entity">
|
||||
<ha-entity-picker
|
||||
.curValue=${entityId}
|
||||
.hass=${this.hass}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeEntities=${this.includeEntities}
|
||||
.excludeEntities=${this.excludeEntities}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.value=${entityId}
|
||||
.disabled=${this.disabled}
|
||||
.createDomains=${this.createDomains}
|
||||
@value-changed=${this._entityChanged}
|
||||
></ha-entity-picker>
|
||||
${this.reorder
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
class="entity-handle"
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</ha-sortable>
|
||||
<div>
|
||||
${this.label
|
||||
? html`<ha-input-label .label=${this.label}></ha-input-label>`
|
||||
: nothing}
|
||||
${currentEntities.length === 0
|
||||
? html`
|
||||
<div class="empty">
|
||||
${this.hass.localize(
|
||||
"ui.components.entity.entities-picker.no_entities"
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<ha-sortable
|
||||
.disabled=${!this.reorder || this.disabled}
|
||||
handle-selector=".entity-handle"
|
||||
@item-moved=${this._entityMoved}
|
||||
>
|
||||
${currentEntities.map(
|
||||
(entityId) => html`
|
||||
<div class="entity">
|
||||
<ha-entity-picker
|
||||
.curValue=${entityId}
|
||||
.hass=${this.hass}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeEntities=${this.includeEntities}
|
||||
.excludeEntities=${this.excludeEntities}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.value=${entityId}
|
||||
.disabled=${this.disabled}
|
||||
.createDomains=${this.createDomains}
|
||||
@value-changed=${this._entityChanged}
|
||||
></ha-entity-picker>
|
||||
${this.reorder
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
class="entity-handle"
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</ha-sortable>
|
||||
`}
|
||||
<div class="add">
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.includeDomains=${this.includeDomains}
|
||||
@@ -145,7 +156,7 @@ class HaEntitiesPicker extends LitElement {
|
||||
.createDomains=${this.createDomains}
|
||||
.required=${this.required && !currentEntities.length}
|
||||
@value-changed=${this._addEntity}
|
||||
.addButton=${currentEntities.length > 0}
|
||||
add-button
|
||||
></ha-entity-picker>
|
||||
</div>
|
||||
`;
|
||||
@@ -225,8 +236,26 @@ class HaEntitiesPicker extends LitElement {
|
||||
}
|
||||
|
||||
static override styles = css`
|
||||
div {
|
||||
margin-top: 8px;
|
||||
ha-sortable,
|
||||
.empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--ha-space-2) var(--ha-space-3);
|
||||
gap: var(--ha-space-2);
|
||||
margin-bottom: var(--ha-space-2);
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: var(--wa-form-control-border-color);
|
||||
border-radius: var(--ha-border-radius-lg);
|
||||
background-color: var(--wa-form-control-background-color);
|
||||
position: relative;
|
||||
min-height: 48px;
|
||||
}
|
||||
.empty {
|
||||
color: var(--ha-color-text-disabled);
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
@@ -239,6 +268,7 @@ class HaEntitiesPicker extends LitElement {
|
||||
}
|
||||
.entity ha-entity-picker {
|
||||
flex: 1;
|
||||
max-width: 100%;
|
||||
}
|
||||
.entity-handle {
|
||||
padding: 8px;
|
||||
|
||||
@@ -183,27 +183,16 @@ export class HaAreaPicker extends LitElement {
|
||||
};
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const baseLabel =
|
||||
const label =
|
||||
this.label ?? this.hass.localize("ui.components.area-picker.area");
|
||||
const valueRenderer = this._computeValueRenderer(this.hass.areas);
|
||||
|
||||
// Only show label if there's no floor
|
||||
let label: string | undefined = baseLabel;
|
||||
if (this.value && baseLabel) {
|
||||
const area = this.hass.areas[this.value];
|
||||
if (area) {
|
||||
const { floor } = getAreaContext(area, this.hass.floors);
|
||||
if (floor) {
|
||||
label = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.autofocus=${this.autofocus}
|
||||
.label=${label}
|
||||
.placeholder=${this.placeholder || label}
|
||||
.helper=${this.helper}
|
||||
.notFoundLabel=${this._notFoundLabel}
|
||||
.emptyLabel=${this.hass.localize("ui.components.area-picker.no_areas")}
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import { mdiEye, mdiEyeOff } from "@mdi/js";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
LocalizeFunc,
|
||||
LocalizeKeys,
|
||||
} from "../../common/translations/localize";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-textfield";
|
||||
import type { HaTextField } from "../ha-textfield";
|
||||
import "../input/ha-input";
|
||||
import type { HaInput } from "../input/ha-input";
|
||||
import type {
|
||||
HaFormElement,
|
||||
HaFormStringData,
|
||||
@@ -37,7 +33,7 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
|
||||
@state() protected unmaskedPassword = false;
|
||||
|
||||
@query("ha-textfield", true) private _input?: HaTextField;
|
||||
@query("ha-input", true) private _input?: HaInput;
|
||||
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
@@ -50,48 +46,29 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-textfield
|
||||
.type=${!this.isPassword
|
||||
? this.stringType
|
||||
: this.unmaskedPassword
|
||||
? "text"
|
||||
: "password"}
|
||||
<ha-input
|
||||
.passwordToggle=${this.isPassword}
|
||||
.passwordVisible=${this.unmaskedPassword}
|
||||
.type=${!this.isPassword ? this.stringType : "password"}
|
||||
.label=${this.label}
|
||||
.value=${this.data || ""}
|
||||
.helper=${this.helper}
|
||||
helperPersistent
|
||||
.hint=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.schema.required}
|
||||
.autoValidate=${this.schema.required}
|
||||
.required=${!!this.schema.required}
|
||||
.autoValidate=${!!this.schema.required}
|
||||
.name=${this.schema.name}
|
||||
.autofocus=${this.schema.autofocus}
|
||||
.autofocus=${!!this.schema.autofocus}
|
||||
.autocomplete=${this.schema.autocomplete}
|
||||
.suffix=${this.isPassword
|
||||
? // reserve some space for the icon.
|
||||
html`<div style="width: 24px"></div>`
|
||||
: this.schema.description?.suffix}
|
||||
.validationMessage=${this.schema.required
|
||||
? this.localize?.("ui.common.error_required")
|
||||
: undefined}
|
||||
@input=${this._valueChanged}
|
||||
@change=${this._valueChanged}
|
||||
></ha-textfield>
|
||||
${this.renderIcon()}
|
||||
`;
|
||||
}
|
||||
|
||||
protected renderIcon() {
|
||||
if (!this.isPassword) return nothing;
|
||||
return html`
|
||||
<ha-icon-button
|
||||
.label=${this.localize?.(
|
||||
`${this.localizeBaseKey}.${
|
||||
this.unmaskedPassword ? "hide_password" : "show_password"
|
||||
}` as LocalizeKeys
|
||||
)}
|
||||
@click=${this.toggleUnmaskedPassword}
|
||||
.path=${this.unmaskedPassword ? mdiEyeOff : mdiEye}
|
||||
></ha-icon-button>
|
||||
>
|
||||
${this.schema.description?.suffix
|
||||
? html`<span slot="end">${this.schema.description.suffix}</span>`
|
||||
: nothing}
|
||||
</ha-input>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -101,12 +78,8 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected toggleUnmaskedPassword(): void {
|
||||
this.unmaskedPassword = !this.unmaskedPassword;
|
||||
}
|
||||
|
||||
protected _valueChanged(ev: Event): void {
|
||||
let value: string | undefined = (ev.target as HaTextField).value;
|
||||
let value: string | undefined = (ev.target as HaInput).value;
|
||||
if (this.data === value) {
|
||||
return;
|
||||
}
|
||||
@@ -118,10 +91,10 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
});
|
||||
}
|
||||
|
||||
protected get stringType(): string {
|
||||
protected get stringType(): "email" | "url" | "text" {
|
||||
if (this.schema.format) {
|
||||
if (["email", "url"].includes(this.schema.format)) {
|
||||
return this.schema.format;
|
||||
return this.schema.format as "email" | "url";
|
||||
}
|
||||
if (this.schema.format === "fqdnurl") {
|
||||
return "url";
|
||||
@@ -142,20 +115,6 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
:host([own-margin]) {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
ha-icon-button {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 8px;
|
||||
--ha-icon-button-size: 40px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import type { HomeAssistant } from "../types";
|
||||
import { isIosApp } from "../util/is_ios";
|
||||
import "./ha-bottom-sheet";
|
||||
import "./ha-button";
|
||||
import type { HaButton } from "./ha-button";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-input-helper-text";
|
||||
import "./ha-picker-combo-box";
|
||||
@@ -29,7 +30,10 @@ import type {
|
||||
PickerComboBoxSearchFn,
|
||||
} from "./ha-picker-combo-box";
|
||||
import "./ha-picker-field";
|
||||
import type { HaPickerField } from "./ha-picker-field";
|
||||
import "./ha-svg-icon";
|
||||
import "./input/ha-input-label";
|
||||
import { inputWrapperStyles } from "./input/styles";
|
||||
|
||||
@customElement("ha-generic-picker")
|
||||
export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
@@ -113,6 +117,12 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
|
||||
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
|
||||
|
||||
@query("ha-picker-field")
|
||||
private _pickerField?: HaPickerField;
|
||||
|
||||
@query("#picker")
|
||||
private _picker?: HaButton;
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@state() private _pickerWrapperOpen = false;
|
||||
@@ -158,32 +168,43 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
this._initialFieldValue = value;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
// Only show label if it's not a top label and there is a value.
|
||||
const label = this.useTopLabel && this.value ? undefined : this.label;
|
||||
private _renderAddButton() {
|
||||
const button = html`
|
||||
<ha-button
|
||||
id="add-button"
|
||||
size="small"
|
||||
appearance="filled"
|
||||
@click=${this.open}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPlaylistPlus} slot="start"></ha-svg-icon>
|
||||
${this.addButtonLabel ||
|
||||
this.placeholder ||
|
||||
this.hass?.localize("ui.common.add")}
|
||||
</ha-button>
|
||||
`;
|
||||
|
||||
if (this.addButtonLabel) {
|
||||
return button;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.label
|
||||
? html`<ha-input-label .label=${this.label}></ha-input-label>`
|
||||
: nothing}
|
||||
<div class="input-wrapper">${button}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`<div class="container">
|
||||
${this.useTopLabel && this.label
|
||||
? html`<label ?disabled=${this.disabled}>${this.label}</label>`
|
||||
: nothing}
|
||||
<div id="picker">
|
||||
<slot name="field">
|
||||
${this.addButtonLabel && !this.value
|
||||
? html`<ha-button
|
||||
size="small"
|
||||
appearance="filled"
|
||||
@click=${this.open}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${mdiPlaylistPlus}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
${this.addButtonLabel}
|
||||
</ha-button>`
|
||||
${(this.addButtonLabel || !this.icon) && !this.value
|
||||
? this._renderAddButton()
|
||||
: html`<ha-picker-field
|
||||
type="button"
|
||||
class=${this._opened ? "opened" : ""}
|
||||
.open=${this._opened}
|
||||
compact
|
||||
.unknown=${this._unknownValue}
|
||||
.unknownItemText=${this.unknownItemText}
|
||||
@@ -192,7 +213,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
@clear=${this._clear}
|
||||
.icon=${this.icon}
|
||||
.image=${this.image}
|
||||
.label=${label}
|
||||
.label=${this.label}
|
||||
.placeholder=${this.placeholder}
|
||||
.value=${this.value}
|
||||
.valueRenderer=${this.valueRenderer}
|
||||
@@ -227,7 +248,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
without-arrow
|
||||
distance="-4"
|
||||
.placement=${this.popoverPlacement}
|
||||
for="picker"
|
||||
.anchor=${this._pickerField?.item || this._picker}
|
||||
auto-size="vertical"
|
||||
auto-size-padding="16"
|
||||
@wa-after-show=${this._dialogOpened}
|
||||
@@ -408,6 +429,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
inputWrapperStyles,
|
||||
css`
|
||||
.container {
|
||||
position: relative;
|
||||
@@ -463,10 +485,6 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
--ha-bottom-sheet-surface-background: var(--card-background-color);
|
||||
--ha-bottom-sheet-border-radius: var(--ha-border-radius-2xl);
|
||||
}
|
||||
|
||||
ha-picker-field.opened {
|
||||
--mdc-text-field-idle-line-color: var(--primary-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-label-picker";
|
||||
import type { HaLabelPicker } from "./ha-label-picker";
|
||||
import "./ha-tooltip";
|
||||
import { inputWrapperStyles } from "./input/styles";
|
||||
|
||||
@customElement("ha-labels-picker")
|
||||
export class HaLabelsPicker extends SubscribeMixin(LitElement) {
|
||||
@@ -134,7 +135,7 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) {
|
||||
.excludeLabels=${this.value}
|
||||
@value-changed=${this._labelChanged}
|
||||
>
|
||||
<ha-chip-set>
|
||||
<ha-chip-set class="input-wrapper">
|
||||
${labels?.length
|
||||
? repeat(
|
||||
labels,
|
||||
@@ -230,30 +231,29 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) {
|
||||
this.labelPicker.open();
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-chip-set {
|
||||
background-color: var(--mdc-text-field-fill-color);
|
||||
border-bottom: 1px solid var(--ha-color-border-neutral-normal);
|
||||
border-top-right-radius: var(--ha-border-radius-sm);
|
||||
border-top-left-radius: var(--ha-border-radius-sm);
|
||||
padding: var(--ha-space-3);
|
||||
}
|
||||
.placeholder {
|
||||
color: var(--mdc-text-field-label-ink-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--ha-space-8);
|
||||
}
|
||||
ha-input-chip {
|
||||
--md-input-chip-selected-container-color: var(--color, var(--grey-color));
|
||||
--ha-input-chip-selected-container-opacity: 0.5;
|
||||
--md-input-chip-selected-outline-width: 1px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
`;
|
||||
static styles = [
|
||||
inputWrapperStyles,
|
||||
css`
|
||||
.placeholder {
|
||||
color: var(--mdc-text-field-label-ink-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--ha-space-8);
|
||||
}
|
||||
ha-input-chip {
|
||||
--md-input-chip-selected-container-color: var(
|
||||
--color,
|
||||
var(--grey-color)
|
||||
);
|
||||
--ha-input-chip-selected-container-opacity: 0.5;
|
||||
--md-input-chip-selected-outline-width: 1px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { styles } from "@material/web/textfield/internal/filled-styles";
|
||||
import { FilledTextField } from "@material/web/textfield/internal/filled-text-field";
|
||||
import { styles as sharedStyles } from "@material/web/textfield/internal/shared-styles";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-md-textfield")
|
||||
export class HaMdTextfield extends FilledTextField {
|
||||
static override styles = [
|
||||
sharedStyles,
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-icon-display: block;
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-sys-color-secondary: var(--secondary-text-color);
|
||||
--md-sys-color-surface: var(--card-background-color);
|
||||
--md-sys-color-on-surface-variant: var(--secondary-text-color);
|
||||
|
||||
--md-sys-color-surface-container-highest: var(--input-fill-color);
|
||||
--md-sys-color-on-surface: var(--input-ink-color);
|
||||
|
||||
--md-sys-color-surface-container: var(--input-fill-color);
|
||||
--md-sys-color-secondary-container: var(--input-fill-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-textfield": HaMdTextfield;
|
||||
}
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
import type { TextAreaCharCounter } from "@material/mwc-textfield/mwc-textfield-base";
|
||||
import { mdiEye, mdiEyeOff } from "@mdi/js";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
property,
|
||||
query,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
@customElement("ha-password-field")
|
||||
export class HaPasswordField extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public invalid?: boolean;
|
||||
|
||||
@property({ attribute: "error-message" }) public errorMessage?: string;
|
||||
|
||||
@property({ type: Boolean }) public icon = false;
|
||||
|
||||
// eslint-disable-next-line lit/attribute-names
|
||||
@property({ type: Boolean }) public iconTrailing = false;
|
||||
|
||||
@property() public autocomplete?: string;
|
||||
|
||||
@property({ type: Boolean }) public autocorrect = true;
|
||||
|
||||
@property({ attribute: "input-spellcheck" })
|
||||
public inputSpellcheck?: string;
|
||||
|
||||
@property({ type: String }) value = "";
|
||||
|
||||
@property({ type: String }) placeholder = "";
|
||||
|
||||
@property({ type: String }) label = "";
|
||||
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
@property({ type: Boolean }) required = false;
|
||||
|
||||
// eslint-disable-next-line lit/attribute-names
|
||||
@property({ type: Number }) minLength = -1;
|
||||
|
||||
// eslint-disable-next-line lit/attribute-names
|
||||
@property({ type: Number }) maxLength = -1;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) outlined = false;
|
||||
|
||||
@property({ type: String }) helper = "";
|
||||
|
||||
// eslint-disable-next-line lit/attribute-names
|
||||
@property({ type: Boolean }) validateOnInitialRender = false;
|
||||
|
||||
// eslint-disable-next-line lit/attribute-names
|
||||
@property({ type: String }) validationMessage = "";
|
||||
|
||||
// eslint-disable-next-line lit/attribute-names
|
||||
@property({ type: Boolean }) autoValidate = false;
|
||||
|
||||
@property({ type: String }) pattern = "";
|
||||
|
||||
@property({ type: Number }) size: number | null = null;
|
||||
|
||||
// eslint-disable-next-line lit/attribute-names
|
||||
@property({ type: Boolean }) helperPersistent = false;
|
||||
|
||||
// eslint-disable-next-line lit/attribute-names
|
||||
@property({ type: Boolean }) charCounter: boolean | TextAreaCharCounter =
|
||||
false;
|
||||
|
||||
// eslint-disable-next-line lit/attribute-names
|
||||
@property({ type: Boolean }) endAligned = false;
|
||||
|
||||
@property({ type: String }) prefix = "";
|
||||
|
||||
@property({ type: String }) suffix = "";
|
||||
|
||||
@property({ type: String }) name = "";
|
||||
|
||||
@property({ type: String, attribute: "input-mode" })
|
||||
inputMode!: string;
|
||||
|
||||
// eslint-disable-next-line lit/attribute-names
|
||||
@property({ type: Boolean }) readOnly = false;
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property({ attribute: false }) autocapitalize = "";
|
||||
|
||||
@state() private _unmaskedPassword = false;
|
||||
|
||||
@query("ha-textfield") private _textField!: HaTextField;
|
||||
|
||||
protected render() {
|
||||
return html`<ha-textfield
|
||||
.invalid=${this.invalid}
|
||||
.errorMessage=${this.errorMessage}
|
||||
.icon=${this.icon}
|
||||
.iconTrailing=${this.iconTrailing}
|
||||
.autocomplete=${this.autocomplete}
|
||||
.autocorrect=${this.autocorrect}
|
||||
.inputSpellcheck=${this.inputSpellcheck}
|
||||
.value=${this.value}
|
||||
.placeholder=${this.placeholder}
|
||||
.label=${this.label}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.minLength=${this.minLength}
|
||||
.maxLength=${this.maxLength}
|
||||
.outlined=${this.outlined}
|
||||
.helper=${this.helper}
|
||||
.validateOnInitialRender=${this.validateOnInitialRender}
|
||||
.validationMessage=${this.validationMessage}
|
||||
.autoValidate=${this.autoValidate}
|
||||
.pattern=${this.pattern}
|
||||
.size=${this.size}
|
||||
.helperPersistent=${this.helperPersistent}
|
||||
.charCounter=${this.charCounter}
|
||||
.endAligned=${this.endAligned}
|
||||
.prefix=${this.prefix}
|
||||
.name=${this.name}
|
||||
.inputMode=${this.inputMode}
|
||||
.readOnly=${this.readOnly}
|
||||
.autocapitalize=${this.autocapitalize}
|
||||
.type=${this._unmaskedPassword ? "text" : "password"}
|
||||
.suffix=${html`<div style="width: 24px"></div>`}
|
||||
@input=${this._handleInputEvent}
|
||||
@change=${this._handleChangeEvent}
|
||||
></ha-textfield>
|
||||
<ha-icon-button
|
||||
.label=${this.hass?.localize(
|
||||
this._unmaskedPassword
|
||||
? "ui.components.selectors.text.hide_password"
|
||||
: "ui.components.selectors.text.show_password"
|
||||
) || (this._unmaskedPassword ? "Hide password" : "Show password")}
|
||||
@click=${this._toggleUnmaskedPassword}
|
||||
.path=${this._unmaskedPassword ? mdiEyeOff : mdiEye}
|
||||
></ha-icon-button>`;
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
this._textField.focus();
|
||||
}
|
||||
|
||||
public checkValidity(): boolean {
|
||||
return this._textField.checkValidity();
|
||||
}
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._textField.reportValidity();
|
||||
}
|
||||
|
||||
public setCustomValidity(message: string): void {
|
||||
return this._textField.setCustomValidity(message);
|
||||
}
|
||||
|
||||
public layout(): Promise<void> {
|
||||
return this._textField.layout();
|
||||
}
|
||||
|
||||
private _toggleUnmaskedPassword(): void {
|
||||
this._unmaskedPassword = !this._unmaskedPassword;
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _handleInputEvent(ev) {
|
||||
this.value = ev.target.value;
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _handleChangeEvent(ev) {
|
||||
this.value = ev.target.value;
|
||||
this._reDispatchEvent(ev);
|
||||
}
|
||||
|
||||
private _reDispatchEvent(oldEvent: Event) {
|
||||
const newEvent = new Event(oldEvent.type, oldEvent);
|
||||
this.dispatchEvent(newEvent);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
ha-textfield {
|
||||
width: 100%;
|
||||
}
|
||||
ha-icon-button {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 8px;
|
||||
--ha-icon-button-size: 40px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-password-field": HaPasswordField;
|
||||
}
|
||||
}
|
||||
@@ -220,7 +220,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
: (this.hass?.localize("ui.common.search") ?? "Search"));
|
||||
|
||||
return html`<ha-textfield
|
||||
.label=${searchLabel}
|
||||
.placeholder=${searchLabel}
|
||||
@blur=${this._resetSelectedItem}
|
||||
@input=${this._filterChanged}
|
||||
.iconTrailing=${this.clearable && !!this._search}
|
||||
@@ -798,6 +798,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
flex-direction: column;
|
||||
padding-top: var(--ha-space-4);
|
||||
flex: 1;
|
||||
--ha-input-padding-top: 0;
|
||||
}
|
||||
|
||||
:host([clearable]) {
|
||||
|
||||
@@ -18,6 +18,7 @@ import "./ha-combo-box-item";
|
||||
import type { HaComboBoxItem } from "./ha-combo-box-item";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
import "./input/ha-input-label";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -31,6 +32,8 @@ export type PickerValueRenderer = (value: string) => TemplateResult<1>;
|
||||
export class HaPickerField extends PickerMixin(LitElement) {
|
||||
@property({ type: Boolean, reflect: true }) public invalid = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public open = false;
|
||||
|
||||
@query("ha-combo-box-item", true) public item!: HaComboBoxItem;
|
||||
|
||||
@state()
|
||||
@@ -43,31 +46,25 @@ export class HaPickerField extends PickerMixin(LitElement) {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const hasValue = !!this.value;
|
||||
|
||||
const showClearIcon =
|
||||
!!this.value && !this.required && !this.disabled && !this.hideClearIcon;
|
||||
|
||||
const placeholderText = this.placeholder ?? this.label;
|
||||
const placeholder = this.placeholder || this.label;
|
||||
|
||||
const overlineLabel =
|
||||
this.label && hasValue
|
||||
? html`<span slot="overline"
|
||||
>${this.label}${this.required ? " *" : ""}</span
|
||||
>`
|
||||
: nothing;
|
||||
|
||||
const headlineContent = hasValue
|
||||
const headlineContent = this.value
|
||||
? this.valueRenderer
|
||||
? this.valueRenderer(this.value ?? "")
|
||||
: html`<span slot="headline">${this.value}</span>`
|
||||
: placeholderText
|
||||
: placeholder
|
||||
? html`<span slot="headline" class="placeholder">
|
||||
${placeholderText}${this.required ? " *" : ""}
|
||||
${placeholder}${this.required ? " *" : ""}
|
||||
</span>`
|
||||
: nothing;
|
||||
|
||||
return html`
|
||||
${this.label
|
||||
? html` <ha-input-label .label=${this.label}></ha-input-label> `
|
||||
: nothing}
|
||||
<ha-combo-box-item
|
||||
aria-label=${ifDefined(this.label || this.placeholder)}
|
||||
.disabled=${this.disabled}
|
||||
@@ -85,7 +82,7 @@ export class HaPickerField extends PickerMixin(LitElement) {
|
||||
: this.icon
|
||||
? html`<ha-icon slot="start" .icon=${this.icon}></ha-icon>`
|
||||
: html`<slot name="start"></slot>`}
|
||||
${overlineLabel}${headlineContent}
|
||||
${headlineContent}
|
||||
${this.unknown
|
||||
? html`<div slot="supporting-text" class="unknown">
|
||||
${this.unknownItemText ||
|
||||
@@ -119,20 +116,17 @@ export class HaPickerField extends PickerMixin(LitElement) {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
css`
|
||||
ha-combo-box-item[disabled] {
|
||||
background-color: var(
|
||||
--mdc-text-field-disabled-fill-color,
|
||||
whitesmoke
|
||||
);
|
||||
}
|
||||
ha-combo-box-item {
|
||||
position: relative;
|
||||
background-color: var(--mdc-text-field-fill-color, whitesmoke);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
border-end-end-radius: 0;
|
||||
border-end-start-radius: 0;
|
||||
--md-list-item-one-line-container-height: 56px;
|
||||
--md-list-item-two-line-container-height: 56px;
|
||||
background-color: var(--wa-form-control-background-color);
|
||||
border-radius: var(--ha-border-radius-lg);
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: var(--wa-form-control-border-color);
|
||||
--ha-ripple-hover-color: var(--wa-form-control-border-color);
|
||||
--ha-ripple-hover-opacity: 0;
|
||||
--md-list-item-one-line-container-height: 48px;
|
||||
--md-list-item-two-line-container-height: 48px;
|
||||
--md-list-item-top-space: 0px;
|
||||
--md-list-item-bottom-space: 0px;
|
||||
--md-list-item-leading-space: var(--ha-space-4);
|
||||
@@ -143,44 +137,26 @@ export class HaPickerField extends PickerMixin(LitElement) {
|
||||
--md-focus-ring-duration: 0s;
|
||||
}
|
||||
|
||||
/* Add Similar focus style as the text field */
|
||||
ha-combo-box-item[disabled]:after {
|
||||
background-color: var(
|
||||
--mdc-text-field-disabled-line-color,
|
||||
rgba(0, 0, 0, 0.42)
|
||||
);
|
||||
}
|
||||
ha-combo-box-item:after {
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background-color: var(
|
||||
--mdc-text-field-idle-line-color,
|
||||
rgba(0, 0, 0, 0.42)
|
||||
);
|
||||
transform:
|
||||
height 180ms ease-in-out,
|
||||
background-color 180ms ease-in-out;
|
||||
ha-combo-box-item[disabled] {
|
||||
background-color: var(--ha-color-fill-disabled-quiet-resting);
|
||||
}
|
||||
|
||||
ha-combo-box-item:focus:after {
|
||||
height: 2px;
|
||||
background-color: var(--mdc-theme-primary);
|
||||
ha-combo-box-item:not([disabled]):hover {
|
||||
border-color: var(--ha-color-border-neutral-normal);
|
||||
}
|
||||
|
||||
:host([open]) ha-combo-box-item {
|
||||
border-color: var(--ha-color-border-primary-normal);
|
||||
}
|
||||
|
||||
:host([unknown]) ha-combo-box-item {
|
||||
border-color: var(--ha-color-border-warning-normal);
|
||||
background-color: var(--ha-color-fill-warning-quiet-resting);
|
||||
}
|
||||
|
||||
:host([invalid]) ha-combo-box-item:after {
|
||||
height: 2px;
|
||||
background-color: var(--mdc-theme-error, var(--error-color, #b00020));
|
||||
:host([invalid]) ha-combo-box-item {
|
||||
border-color: var(--ha-color-border-danger-normal);
|
||||
background-color: var(--ha-color-fill-danger-quiet-resting);
|
||||
}
|
||||
|
||||
.clear {
|
||||
@@ -197,12 +173,18 @@ export class HaPickerField extends PickerMixin(LitElement) {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
:host([invalid]) .placeholder {
|
||||
color: var(--mdc-theme-error, var(--error-color, #b00020));
|
||||
:host([open]) {
|
||||
--ha-input-label-background: var(--ha-color-fill-primary-quiet-hover);
|
||||
}
|
||||
|
||||
.unknown {
|
||||
color: var(--ha-color-on-warning-normal);
|
||||
:host([invalid]) {
|
||||
--ha-input-label-background: var(
|
||||
--ha-color-fill-danger-quiet-resting
|
||||
);
|
||||
}
|
||||
|
||||
:host([open][invalid]) {
|
||||
--ha-input-label-background: var(--ha-color-fill-danger-quiet-hover);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -127,6 +127,7 @@ export class HaSelect extends LitElement {
|
||||
return html`
|
||||
<ha-picker-field
|
||||
slot="trigger"
|
||||
.open=${this._opened}
|
||||
type="button"
|
||||
class=${this._opened ? "opened" : ""}
|
||||
compact
|
||||
|
||||
@@ -1,242 +1,351 @@
|
||||
import { TextFieldBase } from "@material/mwc-textfield/mwc-textfield-base";
|
||||
import { styles } from "@material/mwc-textfield/mwc-textfield.css";
|
||||
import type { TemplateResult, PropertyValues } from "lit";
|
||||
import { html, css } from "lit";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
import "./input/ha-input";
|
||||
import type { HaInput } from "./input/ha-input";
|
||||
|
||||
/**
|
||||
* Legacy wrapper around ha-input that preserves the mwc-textfield API.
|
||||
* New code should use ha-input directly.
|
||||
* @deprecated Use ha-input instead.
|
||||
*/
|
||||
@customElement("ha-textfield")
|
||||
export class HaTextField extends TextFieldBase {
|
||||
@property({ type: Boolean }) public invalid?: boolean;
|
||||
export class HaTextField extends LitElement {
|
||||
@property({ type: String })
|
||||
public value = "";
|
||||
|
||||
@property({ attribute: "error-message" }) public errorMessage?: string;
|
||||
@property({ type: String })
|
||||
public type:
|
||||
| "text"
|
||||
| "search"
|
||||
| "tel"
|
||||
| "url"
|
||||
| "email"
|
||||
| "password"
|
||||
| "date"
|
||||
| "month"
|
||||
| "week"
|
||||
| "time"
|
||||
| "datetime-local"
|
||||
| "number"
|
||||
| "color" = "text";
|
||||
|
||||
@property({ type: String })
|
||||
public label = "";
|
||||
|
||||
@property({ type: String })
|
||||
public placeholder = "";
|
||||
|
||||
@property({ type: String })
|
||||
public prefix = "";
|
||||
|
||||
@property({ type: String })
|
||||
public suffix = "";
|
||||
|
||||
@property({ type: Boolean })
|
||||
// @ts-ignore
|
||||
@property({ type: Boolean }) public icon = false;
|
||||
public icon = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line lit/attribute-names
|
||||
@property({ type: Boolean }) public iconTrailing = false;
|
||||
public iconTrailing = false;
|
||||
|
||||
@property() public autocomplete?: string;
|
||||
@property({ type: Boolean })
|
||||
public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public autocorrect = true;
|
||||
@property({ type: Boolean })
|
||||
public required = false;
|
||||
|
||||
@property({ type: Number, attribute: "minlength" })
|
||||
public minLength = -1;
|
||||
|
||||
@property({ type: Number, attribute: "maxlength" })
|
||||
public maxLength = -1;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public outlined = false;
|
||||
|
||||
@property({ type: String })
|
||||
public helper = "";
|
||||
|
||||
@property({ type: Boolean, attribute: "validateoninitialrender" })
|
||||
public validateOnInitialRender = false;
|
||||
|
||||
@property({ type: String, attribute: "validationmessage" })
|
||||
public validationMessage = "";
|
||||
|
||||
@property({ type: Boolean, attribute: "autovalidate" })
|
||||
public autoValidate = false;
|
||||
|
||||
@property({ type: String })
|
||||
public pattern = "";
|
||||
|
||||
@property()
|
||||
public min: number | string = "";
|
||||
|
||||
@property()
|
||||
public max: number | string = "";
|
||||
|
||||
@property()
|
||||
public step: number | "any" | null = null;
|
||||
|
||||
@property({ type: Number })
|
||||
public size: number | null = null;
|
||||
|
||||
@property({ type: Boolean, attribute: "helperpersistent" })
|
||||
public helperPersistent = false;
|
||||
|
||||
@property({ attribute: "charcounter" })
|
||||
public charCounter: boolean | "external" | "internal" = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "endaligned" })
|
||||
public endAligned = false;
|
||||
|
||||
@property({ type: String, attribute: "inputmode" })
|
||||
public inputMode = "";
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "readonly" })
|
||||
public readOnly = false;
|
||||
|
||||
@property({ type: String })
|
||||
public name = "";
|
||||
|
||||
@property({ type: String })
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public autocapitalize = "";
|
||||
|
||||
// --- ha-textfield-specific properties ---
|
||||
|
||||
@property({ type: Boolean })
|
||||
public invalid = false;
|
||||
|
||||
@property({ attribute: "error-message" })
|
||||
public errorMessage?: string;
|
||||
|
||||
@property()
|
||||
public autocomplete?: string;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public autocorrect = true;
|
||||
|
||||
@property({ attribute: "input-spellcheck" })
|
||||
public inputSpellcheck?: string;
|
||||
|
||||
@query("input") public formElement!: HTMLInputElement;
|
||||
@query("ha-input")
|
||||
private _haInput?: HaInput;
|
||||
|
||||
override updated(changedProperties: PropertyValues) {
|
||||
static shadowRootOptions: ShadowRootInit = {
|
||||
mode: "open",
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
public get formElement(): HTMLInputElement | undefined {
|
||||
return (this._haInput as any)?._input?.input;
|
||||
}
|
||||
|
||||
public select(): void {
|
||||
this._haInput?.select();
|
||||
}
|
||||
|
||||
public setSelectionRange(
|
||||
selectionStart: number,
|
||||
selectionEnd: number,
|
||||
selectionDirection?: "forward" | "backward" | "none"
|
||||
): void {
|
||||
this._haInput?.setSelectionRange(
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
selectionDirection
|
||||
);
|
||||
}
|
||||
|
||||
public setRangeText(
|
||||
replacement: string,
|
||||
start?: number,
|
||||
end?: number,
|
||||
selectMode?: "select" | "start" | "end" | "preserve"
|
||||
): void {
|
||||
this._haInput?.setRangeText(replacement, start, end, selectMode);
|
||||
}
|
||||
|
||||
public checkValidity(): boolean {
|
||||
return this._haInput?.checkValidity() ?? true;
|
||||
}
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._haInput?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
public setCustomValidity(message: string): void {
|
||||
this.validationMessage = message;
|
||||
this.invalid = !!message;
|
||||
}
|
||||
|
||||
/** No-op. Preserved for backward compatibility. */
|
||||
public layout(): void {
|
||||
// no-op — mwc-textfield needed this for notched outline recalculation
|
||||
}
|
||||
|
||||
protected override firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
if (this.validateOnInitialRender) {
|
||||
this.reportValidity();
|
||||
}
|
||||
}
|
||||
|
||||
protected override updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
changedProperties.has("invalid") ||
|
||||
changedProperties.has("errorMessage")
|
||||
) {
|
||||
this.setCustomValidity(
|
||||
this.invalid
|
||||
? this.errorMessage || this.validationMessage || "Invalid"
|
||||
: ""
|
||||
);
|
||||
|
||||
if (changedProperties.has("invalid") && this._haInput) {
|
||||
if (
|
||||
this.invalid ||
|
||||
this.validateOnInitialRender ||
|
||||
(changedProperties.has("invalid") &&
|
||||
changedProperties.get("invalid") !== undefined)
|
||||
(changedProperties.get("invalid") !== undefined && !this.invalid)
|
||||
) {
|
||||
// Only report validity if the field is invalid or the invalid state has changed from
|
||||
// true to false to prevent setting empty required fields to invalid on first render
|
||||
this.reportValidity();
|
||||
}
|
||||
}
|
||||
if (changedProperties.has("autocomplete")) {
|
||||
if (this.autocomplete) {
|
||||
this.formElement.setAttribute("autocomplete", this.autocomplete);
|
||||
} else {
|
||||
this.formElement.removeAttribute("autocomplete");
|
||||
}
|
||||
}
|
||||
if (changedProperties.has("autocorrect")) {
|
||||
if (this.autocorrect === false) {
|
||||
this.formElement.setAttribute("autocorrect", "off");
|
||||
} else {
|
||||
this.formElement.removeAttribute("autocorrect");
|
||||
}
|
||||
}
|
||||
if (changedProperties.has("inputSpellcheck")) {
|
||||
if (this.inputSpellcheck) {
|
||||
this.formElement.setAttribute("spellcheck", this.inputSpellcheck);
|
||||
} else {
|
||||
this.formElement.removeAttribute("spellcheck");
|
||||
}
|
||||
}
|
||||
|
||||
private _mapType(
|
||||
type: string
|
||||
):
|
||||
| "text"
|
||||
| "search"
|
||||
| "tel"
|
||||
| "url"
|
||||
| "email"
|
||||
| "password"
|
||||
| "date"
|
||||
| "datetime-local"
|
||||
| "number"
|
||||
| "time" {
|
||||
// mwc-textfield supports "color", "month", "week" which ha-input doesn't
|
||||
switch (type) {
|
||||
case "text":
|
||||
case "search":
|
||||
case "tel":
|
||||
case "url":
|
||||
case "email":
|
||||
case "password":
|
||||
case "date":
|
||||
case "datetime-local":
|
||||
case "number":
|
||||
case "time":
|
||||
return type;
|
||||
default:
|
||||
return "text";
|
||||
}
|
||||
}
|
||||
|
||||
protected override renderIcon(
|
||||
_icon: string,
|
||||
isTrailingIcon = false
|
||||
): TemplateResult {
|
||||
const type = isTrailingIcon ? "trailing" : "leading";
|
||||
|
||||
protected override render(): TemplateResult {
|
||||
const errorMsg = this.errorMessage || this.validationMessage;
|
||||
return html`
|
||||
<span
|
||||
class="mdc-text-field__icon mdc-text-field__icon--${type}"
|
||||
tabindex=${isTrailingIcon ? 1 : -1}
|
||||
<ha-input
|
||||
.type=${this._mapType(this.type)}
|
||||
.value=${this.value || undefined}
|
||||
.label=${this.label}
|
||||
.placeholder=${this.placeholder}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.readonly=${this.readOnly}
|
||||
.pattern=${this.pattern || undefined}
|
||||
.minlength=${this.minLength > 0 ? this.minLength : undefined}
|
||||
.maxlength=${this.maxLength > 0 ? this.maxLength : undefined}
|
||||
.min=${this.min !== "" ? this.min : undefined}
|
||||
.max=${this.max !== "" ? this.max : undefined}
|
||||
.step=${this.step ?? undefined}
|
||||
.name=${this.name || undefined}
|
||||
.autocomplete=${this.autocomplete}
|
||||
.autocorrect=${this.autocorrect}
|
||||
.spellcheck=${this.inputSpellcheck === "true"}
|
||||
.inputmode=${this._mapInputMode(this.inputMode)}
|
||||
.autocapitalize=${this.autocapitalize || ""}
|
||||
.invalid=${this.invalid}
|
||||
.validationMessage=${errorMsg || ""}
|
||||
.autoValidate=${this.autoValidate}
|
||||
.hint=${this.helper}
|
||||
.withoutSpinButtons=${this.type === "number"}
|
||||
@input=${this._onInput}
|
||||
@change=${this._onChange}
|
||||
>
|
||||
<slot name="${type}Icon"></slot>
|
||||
</span>
|
||||
${this.icon
|
||||
? html`<slot name="leadingIcon" slot="start"></slot>`
|
||||
: nothing}
|
||||
${this.prefix
|
||||
? html`<span class="prefix" slot="start">${this.prefix}</span>`
|
||||
: nothing}
|
||||
${this.suffix
|
||||
? html`<span class="suffix" slot="end">${this.suffix}</span>`
|
||||
: nothing}
|
||||
${this.iconTrailing
|
||||
? html`<slot name="trailingIcon" slot="end"></slot>`
|
||||
: nothing}
|
||||
</ha-input>
|
||||
`;
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
.mdc-text-field__input {
|
||||
width: var(--ha-textfield-input-width, 100%);
|
||||
}
|
||||
.mdc-text-field:not(.mdc-text-field--with-leading-icon) {
|
||||
padding: var(--text-field-padding, 0px 16px);
|
||||
}
|
||||
.mdc-text-field__affix--suffix {
|
||||
padding-left: var(--text-field-suffix-padding-left, 12px);
|
||||
padding-right: var(--text-field-suffix-padding-right, 0px);
|
||||
padding-inline-start: var(--text-field-suffix-padding-left, 12px);
|
||||
padding-inline-end: var(--text-field-suffix-padding-right, 0px);
|
||||
direction: ltr;
|
||||
}
|
||||
.mdc-text-field--with-leading-icon {
|
||||
padding-inline-start: var(--text-field-suffix-padding-left, 0px);
|
||||
padding-inline-end: var(--text-field-suffix-padding-right, 16px);
|
||||
direction: var(--direction);
|
||||
}
|
||||
private _mapInputMode(
|
||||
mode: string
|
||||
):
|
||||
| "none"
|
||||
| "text"
|
||||
| "decimal"
|
||||
| "numeric"
|
||||
| "tel"
|
||||
| "search"
|
||||
| "email"
|
||||
| "url"
|
||||
| "" {
|
||||
switch (mode) {
|
||||
case "none":
|
||||
case "text":
|
||||
case "decimal":
|
||||
case "numeric":
|
||||
case "tel":
|
||||
case "search":
|
||||
case "email":
|
||||
case "url":
|
||||
return mode;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
.mdc-text-field--with-leading-icon.mdc-text-field--with-trailing-icon {
|
||||
padding-left: var(--text-field-suffix-padding-left, 0px);
|
||||
padding-right: var(--text-field-suffix-padding-right, 0px);
|
||||
padding-inline-start: var(--text-field-suffix-padding-left, 0px);
|
||||
padding-inline-end: var(--text-field-suffix-padding-right, 0px);
|
||||
}
|
||||
.mdc-text-field:not(.mdc-text-field--disabled)
|
||||
.mdc-text-field__affix--suffix {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
private _onInput(): void {
|
||||
this.value = this._haInput?.value ?? "";
|
||||
}
|
||||
|
||||
.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
private _onChange(): void {
|
||||
this.value = this._haInput?.value ?? "";
|
||||
}
|
||||
|
||||
.mdc-text-field__icon--leading {
|
||||
margin-inline-start: 16px;
|
||||
margin-inline-end: 8px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.mdc-text-field__icon--trailing {
|
||||
padding: var(--textfield-icon-trailing-padding, 12px);
|
||||
}
|
||||
ha-input {
|
||||
--ha-input-padding-bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mdc-floating-label:not(.mdc-floating-label--float-above) {
|
||||
max-width: calc(100% - 16px);
|
||||
}
|
||||
.prefix,
|
||||
.suffix {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.mdc-floating-label--float-above {
|
||||
max-width: calc((100% - 16px) / 0.75);
|
||||
transition: none;
|
||||
}
|
||||
.prefix {
|
||||
margin-inline-end: var(--text-field-prefix-padding-right);
|
||||
}
|
||||
|
||||
input {
|
||||
text-align: var(--text-field-text-align, start);
|
||||
}
|
||||
|
||||
input[type="color"] {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* Edge, hide reveal password icon */
|
||||
::-ms-reveal {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Chrome, Safari, Edge, Opera */
|
||||
:host([no-spinner]) input::-webkit-outer-spin-button,
|
||||
:host([no-spinner]) input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input[type="color"]::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
:host([no-spinner]) input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
.mdc-text-field__ripple {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mdc-text-field {
|
||||
overflow: var(--text-field-overflow);
|
||||
}
|
||||
|
||||
.mdc-floating-label {
|
||||
padding-inline-end: 16px;
|
||||
padding-inline-start: initial;
|
||||
inset-inline-start: 16px !important;
|
||||
inset-inline-end: initial !important;
|
||||
transform-origin: var(--float-start);
|
||||
direction: var(--direction);
|
||||
text-align: var(--float-start);
|
||||
box-sizing: border-box;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.mdc-text-field--with-leading-icon.mdc-text-field--filled
|
||||
.mdc-floating-label {
|
||||
max-width: calc(
|
||||
100% - 48px - var(--text-field-suffix-padding-left, 0px)
|
||||
);
|
||||
inset-inline-start: calc(
|
||||
48px + var(--text-field-suffix-padding-left, 0px)
|
||||
) !important;
|
||||
inset-inline-end: initial !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.mdc-text-field__input[type="number"] {
|
||||
direction: var(--direction);
|
||||
}
|
||||
.mdc-text-field__affix--prefix {
|
||||
padding-right: var(--text-field-prefix-padding-right, 2px);
|
||||
padding-inline-end: var(--text-field-prefix-padding-right, 2px);
|
||||
padding-inline-start: initial;
|
||||
}
|
||||
|
||||
.mdc-text-field:not(.mdc-text-field--disabled)
|
||||
.mdc-text-field__affix--prefix {
|
||||
color: var(--mdc-text-field-label-ink-color);
|
||||
}
|
||||
#helper-text ha-markdown {
|
||||
display: inline-block;
|
||||
}
|
||||
`,
|
||||
// safari workaround - must be explicit
|
||||
mainWindow.document.dir === "rtl"
|
||||
? css`
|
||||
.mdc-text-field--with-leading-icon,
|
||||
.mdc-text-field__icon--leading,
|
||||
.mdc-floating-label,
|
||||
.mdc-text-field--with-leading-icon.mdc-text-field--filled
|
||||
.mdc-floating-label,
|
||||
.mdc-text-field__input[type="number"] {
|
||||
direction: rtl;
|
||||
--direction: rtl;
|
||||
}
|
||||
`
|
||||
: css``,
|
||||
];
|
||||
/* Edge, hide reveal password icon */
|
||||
::-ms-reveal {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
157
src/components/input/ha-input-label.ts
Normal file
157
src/components/input/ha-input-label.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { preventDefault } from "@fullcalendar/core/internal";
|
||||
import { HasSlotController } from "@home-assistant/webawesome/dist/internal/slot";
|
||||
import { mdiInformationOutline } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-tooltip";
|
||||
|
||||
@customElement("ha-input-label")
|
||||
export class HaInputLabel extends LitElement {
|
||||
@property() label?: string;
|
||||
|
||||
@property() hint?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) inline = false;
|
||||
|
||||
private readonly _hasSlotController = new HasSlotController(this, "label");
|
||||
|
||||
render() {
|
||||
const hasLabel = this.label || this._hasSlotController.test("label");
|
||||
|
||||
return html`
|
||||
<div class="text">
|
||||
${hasLabel
|
||||
? html`<div class="label-content">
|
||||
<span>
|
||||
<slot name="label">${this.label}</slot>
|
||||
</span>
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
<slot name="end">
|
||||
${this.hint
|
||||
? html`<ha-icon-button
|
||||
@click=${preventDefault}
|
||||
.path=${mdiInformationOutline}
|
||||
.label=${"Hint"}
|
||||
hide-title
|
||||
id="hint"
|
||||
></ha-icon-button>
|
||||
<ha-tooltip for="hint">${this.hint}</ha-tooltip> `
|
||||
: nothing}
|
||||
</slot>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
height: 24px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
color: var(--ha-input-label-text-color, var(--ha-color-text-secondary));
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
gap: var(--ha-space-1);
|
||||
}
|
||||
|
||||
.text {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
padding-inline-end: calc(var(--ha-border-radius-xl) / 2);
|
||||
max-width: calc(100% - var(--ha-border-radius-xl));
|
||||
}
|
||||
|
||||
.label-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
--input-label-background: var(
|
||||
--ha-input-label-background,
|
||||
var(--ha-color-fill-neutral-quiet-resting)
|
||||
);
|
||||
background-color: var(--input-label-background);
|
||||
border-top-left-radius: var(--ha-border-radius-xl);
|
||||
border-top-right-radius: var(--ha-border-radius-xl);
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.label-content span {
|
||||
padding: 0 var(--ha-space-2);
|
||||
min-width: 0;
|
||||
overflow-x: clip;
|
||||
overflow-y: visible;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
:host([required]) .label-content span::after {
|
||||
margin-inline-start: 1px;
|
||||
content: var(--ha-input-required-marker, "*");
|
||||
}
|
||||
|
||||
.label-content::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset-inline-start: 0;
|
||||
top: 100%;
|
||||
background-color: var(--input-label-background);
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
height: calc(var(--ha-border-radius-xl) / 2 + 4px);
|
||||
width: calc(var(--ha-border-radius-xl) / 2 + 4px);
|
||||
mask: radial-gradient(
|
||||
calc(var(--ha-border-radius-lg) / 2) at 100% 100%,
|
||||
transparent 98%,
|
||||
black 100%
|
||||
);
|
||||
}
|
||||
|
||||
:dir(rtl) .label-content::before {
|
||||
mask: radial-gradient(
|
||||
calc(var(--ha-border-radius-lg) / 2) at 0 100%,
|
||||
transparent 98%,
|
||||
black 100%
|
||||
);
|
||||
}
|
||||
|
||||
.label-content::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset-inline-end: calc(-1 * var(--ha-border-radius-xl) / 2);
|
||||
top: var(--ha-border-radius-xl);
|
||||
height: calc(var(--ha-border-radius-xl) / 2);
|
||||
width: calc(var(--ha-border-radius-xl) / 2);
|
||||
background-color: var(--input-label-background);
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
mask: radial-gradient(
|
||||
calc(var(--ha-border-radius-xl) / 2) at 100% 0,
|
||||
transparent 98%,
|
||||
black 100%
|
||||
);
|
||||
}
|
||||
|
||||
:dir(rtl) .label-content::after {
|
||||
mask: radial-gradient(
|
||||
calc(var(--ha-border-radius-xl) / 2) at 0 0,
|
||||
transparent 98%,
|
||||
black 100%
|
||||
);
|
||||
}
|
||||
|
||||
#hint {
|
||||
--ha-icon-button-size: 16px;
|
||||
--mdc-icon-size: 16px;
|
||||
color: var(--ha-color-on-disabled-normal);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-input-label": HaInputLabel;
|
||||
}
|
||||
}
|
||||
501
src/components/input/ha-input.ts
Normal file
501
src/components/input/ha-input.ts
Normal file
@@ -0,0 +1,501 @@
|
||||
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 { mdiClose, mdiEye, mdiEyeOff } from "@mdi/js";
|
||||
import { LitElement, type PropertyValues, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-tooltip";
|
||||
import "./ha-input-label";
|
||||
|
||||
@customElement("ha-input")
|
||||
export class HaInput extends LitElement {
|
||||
/** The type of input. */
|
||||
@property()
|
||||
public type:
|
||||
| "date"
|
||||
| "datetime-local"
|
||||
| "email"
|
||||
| "number"
|
||||
| "password"
|
||||
| "search"
|
||||
| "tel"
|
||||
| "text"
|
||||
| "time"
|
||||
| "url" = "text";
|
||||
|
||||
/** The current value of the input. */
|
||||
@property()
|
||||
public value?: string;
|
||||
|
||||
/** The input's size. */
|
||||
@property()
|
||||
public size: "small" | "medium" | "large" = "medium";
|
||||
|
||||
/** The input's visual appearance. */
|
||||
@property()
|
||||
public appearance: "filled" | "outlined" | "filled-outlined" = "outlined";
|
||||
|
||||
/** Draws a pill-style input with rounded edges. */
|
||||
@property({ type: Boolean })
|
||||
public pill = false;
|
||||
|
||||
/** The input's label. */
|
||||
@property()
|
||||
public label = "";
|
||||
|
||||
/** The input's hint. */
|
||||
@property()
|
||||
public hint? = "";
|
||||
|
||||
/** Adds a clear button when the input is not empty. */
|
||||
@property({ type: Boolean, attribute: "with-clear" })
|
||||
public withClear = false;
|
||||
|
||||
/** Placeholder text to show as a hint when the input is empty. */
|
||||
@property()
|
||||
public placeholder = "";
|
||||
|
||||
/** Makes the input readonly. */
|
||||
@property({ type: Boolean })
|
||||
public readonly = false;
|
||||
|
||||
/** Adds a button to toggle the password's visibility. */
|
||||
@property({ type: Boolean, attribute: "password-toggle" })
|
||||
public passwordToggle = false;
|
||||
|
||||
/** Determines whether or not the password is currently visible. */
|
||||
@property({ type: Boolean, attribute: "password-visible" })
|
||||
public passwordVisible = false;
|
||||
|
||||
/** Hides the browser's built-in increment/decrement spin buttons for number inputs. */
|
||||
@property({ type: Boolean, attribute: "without-spin-buttons" })
|
||||
public withoutSpinButtons = false;
|
||||
|
||||
/** Makes the input a required field. */
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public required = false;
|
||||
|
||||
/** A regular expression pattern to validate input against. */
|
||||
@property()
|
||||
public pattern?: string;
|
||||
|
||||
/** The minimum length of input that will be considered valid. */
|
||||
@property({ type: Number })
|
||||
public minlength?: number;
|
||||
|
||||
/** The maximum length of input that will be considered valid. */
|
||||
@property({ type: Number })
|
||||
public maxlength?: number;
|
||||
|
||||
/** The input's minimum value. Only applies to date and number input types. */
|
||||
@property()
|
||||
public min?: number | string;
|
||||
|
||||
/** The input's maximum value. Only applies to date and number input types. */
|
||||
@property()
|
||||
public max?: number | string;
|
||||
|
||||
/** Specifies the granularity that the value must adhere to. */
|
||||
@property()
|
||||
public step?: number | "any";
|
||||
|
||||
/** Controls whether and how text input is automatically capitalized. */
|
||||
@property()
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public autocapitalize:
|
||||
| "off"
|
||||
| "none"
|
||||
| "on"
|
||||
| "sentences"
|
||||
| "words"
|
||||
| "characters"
|
||||
| "" = "";
|
||||
|
||||
/** Indicates whether the browser's autocorrect feature is on or off. */
|
||||
@property({ type: Boolean })
|
||||
public autocorrect = false;
|
||||
|
||||
/** Specifies what permission the browser has to provide assistance in filling out form field values. */
|
||||
@property()
|
||||
public autocomplete?: string;
|
||||
|
||||
/** Indicates that the input should receive focus on page load. */
|
||||
@property({ type: Boolean })
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public autofocus = false;
|
||||
|
||||
/** Used to customize the label or icon of the Enter key on virtual keyboards. */
|
||||
@property()
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public enterkeyhint:
|
||||
| "enter"
|
||||
| "done"
|
||||
| "go"
|
||||
| "next"
|
||||
| "previous"
|
||||
| "search"
|
||||
| "send"
|
||||
| "" = "";
|
||||
|
||||
/** Enables spell checking on the input. */
|
||||
@property({ type: Boolean })
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public spellcheck = true;
|
||||
|
||||
/** Tells the browser what type of data will be entered by the user. */
|
||||
@property()
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public inputmode:
|
||||
| "none"
|
||||
| "text"
|
||||
| "decimal"
|
||||
| "numeric"
|
||||
| "tel"
|
||||
| "search"
|
||||
| "email"
|
||||
| "url"
|
||||
| "" = "";
|
||||
|
||||
/** The name of the input, submitted as a name/value pair with form data. */
|
||||
@property()
|
||||
public name?: string;
|
||||
|
||||
/** Disables the form control. */
|
||||
@property({ type: Boolean })
|
||||
public disabled = false;
|
||||
|
||||
/** Custom validation message to show when the input is invalid. */
|
||||
@property({ attribute: "validation-message" })
|
||||
public validationMessage? = "";
|
||||
|
||||
/** When true, validates the input on blur instead of on form submit. */
|
||||
@property({ type: Boolean, attribute: "auto-validate" })
|
||||
public autoValidate = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public invalid = false;
|
||||
|
||||
@state()
|
||||
private _invalid = false;
|
||||
|
||||
@query("wa-input")
|
||||
private _input?: WaInput;
|
||||
|
||||
private readonly _hasSlotController = new HasSlotController(
|
||||
this,
|
||||
"label",
|
||||
"label-end"
|
||||
);
|
||||
|
||||
static shadowRootOptions: ShadowRootInit = {
|
||||
mode: "open",
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
/** Selects all the text in the input. */
|
||||
public select(): void {
|
||||
this._input?.select();
|
||||
}
|
||||
|
||||
/** Sets the start and end positions of the text selection (0-based). */
|
||||
public setSelectionRange(
|
||||
selectionStart: number,
|
||||
selectionEnd: number,
|
||||
selectionDirection?: "forward" | "backward" | "none"
|
||||
): void {
|
||||
this._input?.setSelectionRange(
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
selectionDirection
|
||||
);
|
||||
}
|
||||
|
||||
/** Replaces a range of text with a new string. */
|
||||
public setRangeText(
|
||||
replacement: string,
|
||||
start?: number,
|
||||
end?: number,
|
||||
selectMode?: "select" | "start" | "end" | "preserve"
|
||||
): void {
|
||||
this._input?.setRangeText(replacement, start, end, selectMode);
|
||||
}
|
||||
|
||||
/** Displays the browser picker for an input element. */
|
||||
public showPicker(): void {
|
||||
this._input?.showPicker();
|
||||
}
|
||||
|
||||
/** Increments the value of a numeric input type by the value of the step attribute. */
|
||||
public stepUp(): void {
|
||||
this._input?.stepUp();
|
||||
}
|
||||
|
||||
/** Decrements the value of a numeric input type by the value of the step attribute. */
|
||||
public stepDown(): void {
|
||||
this._input?.stepDown();
|
||||
}
|
||||
|
||||
public checkValidity(): boolean {
|
||||
return this._input?.checkValidity() ?? true;
|
||||
}
|
||||
|
||||
public reportValidity(): boolean {
|
||||
const valid = this.checkValidity();
|
||||
|
||||
this._invalid = !valid;
|
||||
return valid;
|
||||
}
|
||||
|
||||
protected override updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
const nativeInput = this._input?.input;
|
||||
if (!nativeInput) return;
|
||||
|
||||
// wa-input hardcodes aria-describedby="hint" pointing to its internal hint slot wrapper.
|
||||
// We remove it and use aria-description instead to properly convey our hint or error text.
|
||||
// TODO: fix upstream in wa-input
|
||||
nativeInput.removeAttribute("aria-describedby");
|
||||
|
||||
// wa-input doesn't set aria-invalid on its internal <input>, so we do it manually
|
||||
// TODO: fix upstream in wa-input
|
||||
if (changedProperties.has("invalid") || changedProperties.has("_invalid")) {
|
||||
const isInvalid = this.invalid || this._invalid;
|
||||
nativeInput.setAttribute("aria-invalid", String(isInvalid));
|
||||
}
|
||||
|
||||
// Expose hint or validation error to screen readers on the input itself
|
||||
const description =
|
||||
this.invalid || this._invalid
|
||||
? this.validationMessage || this._input?.validationMessage
|
||||
: this.hint;
|
||||
|
||||
if (description) {
|
||||
nativeInput.setAttribute("aria-description", description);
|
||||
} else {
|
||||
nativeInput.removeAttribute("aria-description");
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const hasLabelSlot = this.label
|
||||
? false
|
||||
: this._hasSlotController.test("label");
|
||||
return html`
|
||||
<wa-input
|
||||
.type=${this.type}
|
||||
.value=${this.value ?? null}
|
||||
.size=${this.size}
|
||||
.appearance=${this.appearance}
|
||||
.withClear=${this.withClear}
|
||||
.placeholder=${!this.placeholder || this.label || !this.required
|
||||
? this.placeholder
|
||||
: this._placeholderWithRequiredMarker(this.placeholder)}
|
||||
.readonly=${this.readonly}
|
||||
.passwordToggle=${this.passwordToggle}
|
||||
.passwordVisible=${this.passwordVisible}
|
||||
.withoutSpinButtons=${this.withoutSpinButtons}
|
||||
.required=${this.required}
|
||||
.pattern=${this.pattern}
|
||||
.minlength=${this.minlength}
|
||||
.maxlength=${this.maxlength}
|
||||
.min=${this.min}
|
||||
.max=${this.max}
|
||||
.step=${this.step}
|
||||
.autocapitalize=${this.autocapitalize || undefined}
|
||||
.autocorrect=${this.autocorrect ? "on" : "off"}
|
||||
.autocomplete=${this.autocomplete}
|
||||
.autofocus=${this.autofocus}
|
||||
.enterkeyhint=${this.enterkeyhint || undefined}
|
||||
.spellcheck=${this.spellcheck}
|
||||
.inputmode=${this.inputmode || undefined}
|
||||
.name=${this.name}
|
||||
.disabled=${this.disabled}
|
||||
class=${this.invalid || this._invalid ? "invalid" : ""}
|
||||
@input=${this._handleInput}
|
||||
@change=${this._handleChange}
|
||||
@blur=${this._handleBlur}
|
||||
@wa-invalid=${this._handleInvalid}
|
||||
>
|
||||
${this.label || this.hint || hasLabelSlot
|
||||
? html`
|
||||
<ha-input-label
|
||||
slot="label"
|
||||
.label=${this.label}
|
||||
.hint=${this.hint}
|
||||
>
|
||||
${hasLabelSlot
|
||||
? html`<slot name="label" slot="label"></slot>`
|
||||
: nothing}
|
||||
${!this.hint && this._hasSlotController.test("label-end")
|
||||
? html`<slot name="label-end" slot="label"></slot>`
|
||||
: nothing}
|
||||
</ha-input-label>
|
||||
`
|
||||
: nothing}
|
||||
<slot name="start" slot="start"></slot>
|
||||
<slot name="end" slot="end"></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">
|
||||
<ha-icon-button
|
||||
@keydown=${stopPropagation}
|
||||
.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
|
||||
slot="hint"
|
||||
class="error ${this.invalid || this._invalid ? "visible" : ""}"
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
>
|
||||
<span
|
||||
>${this.validationMessage || this._input?.validationMessage}</span
|
||||
>
|
||||
</div>
|
||||
</wa-input>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleInput() {
|
||||
this.value = this._input?.value ?? undefined;
|
||||
if (this._invalid && this._input?.checkValidity()) {
|
||||
this._invalid = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleChange() {
|
||||
this.value = this._input?.value ?? undefined;
|
||||
}
|
||||
|
||||
private _handleBlur() {
|
||||
if (this.autoValidate) {
|
||||
this._invalid = !this._input?.checkValidity();
|
||||
}
|
||||
}
|
||||
|
||||
private _handleInvalid() {
|
||||
this._invalid = true;
|
||||
}
|
||||
|
||||
private _placeholderWithRequiredMarker = memoizeOne((placeholder: string) => {
|
||||
let marker = getComputedStyle(this).getPropertyValue(
|
||||
"--ha-input-required-marker"
|
||||
);
|
||||
|
||||
if (!marker) {
|
||||
marker = "*";
|
||||
}
|
||||
|
||||
if (marker.startsWith('"') && marker.endsWith('"')) {
|
||||
marker = marker.slice(1, -1);
|
||||
}
|
||||
|
||||
if (!marker) {
|
||||
return placeholder;
|
||||
}
|
||||
|
||||
return `${placeholder}${marker}`;
|
||||
});
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding-top: var(--ha-input-padding-top, var(--ha-space-2));
|
||||
padding-bottom: var(--ha-input-padding-bottom, var(--ha-space-2));
|
||||
text-align: var(--ha-input-text-align, start);
|
||||
}
|
||||
wa-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
--wa-transition-fast: 0.15s;
|
||||
}
|
||||
|
||||
wa-input:not([disabled])::part(base):hover {
|
||||
--wa-form-control-border-color: var(--ha-color-border-neutral-normal);
|
||||
}
|
||||
|
||||
wa-input:not([disabled])::part(base):focus-within {
|
||||
outline: none;
|
||||
--wa-form-control-border-color: var(--ha-color-border-primary-normal);
|
||||
}
|
||||
|
||||
wa-input.invalid:not([disabled])::part(base) {
|
||||
--wa-form-control-border-color: var(--ha-color-border-danger-quiet);
|
||||
}
|
||||
wa-input.invalid:not([disabled])::part(base):hover,
|
||||
wa-input.invalid:not([disabled])::part(base):focus-within {
|
||||
--wa-form-control-border-color: var(--ha-color-border-danger-normal);
|
||||
}
|
||||
|
||||
wa-input:disabled::part(base) {
|
||||
background-color: var(--ha-color-fill-disabled-quiet-resting);
|
||||
}
|
||||
|
||||
wa-input::part(label) {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
wa-input::part(hint) {
|
||||
margin-block-start: 0;
|
||||
color: var(--ha-color-on-danger-quiet);
|
||||
font-size: var(--ha-font-size-s);
|
||||
margin-inline-start: var(--ha-space-3);
|
||||
}
|
||||
|
||||
.error {
|
||||
transition:
|
||||
opacity 0.3s ease-out,
|
||||
height 0.3s ease-out;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.error span {
|
||||
transition: transform 0.3s ease-out;
|
||||
display: inline-block;
|
||||
transform: translateY(
|
||||
calc(-1 * (var(--ha-font-size-s) + var(--ha-space-1)))
|
||||
);
|
||||
}
|
||||
|
||||
.error.visible {
|
||||
padding-top: var(--ha-space-1);
|
||||
height: calc(var(--ha-font-size-s) + var(--ha-space-2));
|
||||
}
|
||||
|
||||
.error.visible span {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
wa-input::part(end) {
|
||||
color: var(--ha-color-text-secondary);
|
||||
}
|
||||
|
||||
:host(:focus-within) {
|
||||
--ha-input-label-background: var(--ha-color-fill-primary-quiet-hover);
|
||||
}
|
||||
|
||||
wa-input.invalid {
|
||||
--ha-input-label-background: var(--ha-color-fill-danger-quiet-active);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-input": HaInput;
|
||||
}
|
||||
}
|
||||
13
src/components/input/styles.ts
Normal file
13
src/components/input/styles.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { css } from "lit";
|
||||
|
||||
export const inputWrapperStyles = css`
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: var(--wa-form-control-border-color);
|
||||
border-radius: var(--ha-border-radius-lg);
|
||||
background-color: var(--wa-form-control-background-color);
|
||||
padding: var(--ha-space-2);
|
||||
}
|
||||
`;
|
||||
@@ -37,11 +37,9 @@ export interface LovelaceViewHeaderConfig {
|
||||
badges_wrap?: "wrap" | "scroll";
|
||||
}
|
||||
|
||||
export const DEFAULT_FOOTER_MAX_WIDTH_PX = 600;
|
||||
|
||||
export interface LovelaceViewFooterConfig {
|
||||
card?: LovelaceCardConfig;
|
||||
max_width?: number;
|
||||
column_span?: number;
|
||||
}
|
||||
|
||||
export interface LovelaceViewSidebarConfig {
|
||||
|
||||
@@ -7,23 +7,10 @@ export type SystemLogLevel =
|
||||
| "info"
|
||||
| "debug";
|
||||
|
||||
export type SystemLogErrorType =
|
||||
| "auth"
|
||||
| "connection"
|
||||
| "invalid_response"
|
||||
| "rate_limit"
|
||||
| "server"
|
||||
| "slow_setup"
|
||||
| "timeout"
|
||||
| "ssl"
|
||||
| "statistics"
|
||||
| "dns";
|
||||
|
||||
export interface LoggedError {
|
||||
name: string;
|
||||
message: [string];
|
||||
level: SystemLogLevel;
|
||||
error_type?: SystemLogErrorType;
|
||||
source: [string, number];
|
||||
exception: string;
|
||||
count: number;
|
||||
|
||||
@@ -4,11 +4,9 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-password-field";
|
||||
import type { HaPasswordField } from "../../../components/ha-password-field";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../../components/ha-textfield";
|
||||
import "../../../components/input/ha-input";
|
||||
import type { HaInput } from "../../../components/input/ha-input";
|
||||
import { cloudLogin } from "../../../data/cloud";
|
||||
import { showCloudAlreadyConnectedDialog } from "../../../panels/config/cloud/dialog-cloud-already-connected/show-dialog-cloud-already-connected";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
@@ -28,9 +26,9 @@ export class CloudStepSignin extends LitElement {
|
||||
|
||||
@state() private _checkConnection = true;
|
||||
|
||||
@query("#email", true) private _emailField!: HaTextField;
|
||||
@query("#email", true) private _emailField!: HaInput;
|
||||
|
||||
@query("#password", true) private _passwordField!: HaPasswordField;
|
||||
@query("#password", true) private _passwordField!: HaInput;
|
||||
|
||||
render() {
|
||||
return html`<div class="content">
|
||||
@@ -42,7 +40,7 @@ export class CloudStepSignin extends LitElement {
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
autofocus
|
||||
id="email"
|
||||
name="email"
|
||||
@@ -54,12 +52,14 @@ export class CloudStepSignin extends LitElement {
|
||||
autocomplete="email"
|
||||
required
|
||||
@keydown=${this._keyDown}
|
||||
validationMessage=${this.hass.localize(
|
||||
.validationMessage=${this.hass.localize(
|
||||
"ui.panel.config.cloud.register.email_error_msg"
|
||||
)}
|
||||
></ha-textfield>
|
||||
<ha-password-field
|
||||
></ha-input>
|
||||
<ha-input
|
||||
id="password"
|
||||
type="password"
|
||||
password-toggle
|
||||
name="password"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.cloud.register.password"
|
||||
@@ -69,10 +69,10 @@ export class CloudStepSignin extends LitElement {
|
||||
minlength="8"
|
||||
required
|
||||
@keydown=${this._keyDown}
|
||||
validationMessage=${this.hass.localize(
|
||||
.validationMessage=${this.hass.localize(
|
||||
"ui.panel.config.cloud.register.password_error_msg"
|
||||
)}
|
||||
></ha-password-field>
|
||||
></ha-input>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<ha-button
|
||||
@@ -95,8 +95,8 @@ export class CloudStepSignin extends LitElement {
|
||||
const emailField = this._emailField;
|
||||
const passwordField = this._passwordField;
|
||||
|
||||
const email = emailField.value;
|
||||
const password = passwordField.value;
|
||||
const email = emailField.value as string;
|
||||
const password = passwordField.value as string;
|
||||
|
||||
if (!emailField.reportValidity()) {
|
||||
passwordField.reportValidity();
|
||||
@@ -216,8 +216,7 @@ export class CloudStepSignin extends LitElement {
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
ha-textfield,
|
||||
ha-password-field {
|
||||
ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -3,11 +3,9 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-password-field";
|
||||
import type { HaPasswordField } from "../../../components/ha-password-field";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../../components/ha-textfield";
|
||||
import "../../../components/input/ha-input";
|
||||
import type { HaInput } from "../../../components/input/ha-input";
|
||||
import {
|
||||
cloudLogin,
|
||||
cloudRegister,
|
||||
@@ -30,9 +28,9 @@ export class CloudStepSignup extends LitElement {
|
||||
|
||||
@state() private _state?: "VERIFY";
|
||||
|
||||
@query("#email", true) private _emailField!: HaTextField;
|
||||
@query("#email", true) private _emailField!: HaInput;
|
||||
|
||||
@query("#password", true) private _passwordField!: HaPasswordField;
|
||||
@query("#password", true) private _passwordField!: HaInput;
|
||||
|
||||
render() {
|
||||
return html`<div class="content">
|
||||
@@ -53,7 +51,7 @@ export class CloudStepSignup extends LitElement {
|
||||
{ email: this._email }
|
||||
)}
|
||||
</p>`
|
||||
: html`<ha-textfield
|
||||
: html`<ha-input
|
||||
autofocus
|
||||
id="email"
|
||||
name="email"
|
||||
@@ -65,12 +63,14 @@ export class CloudStepSignup extends LitElement {
|
||||
autocomplete="email"
|
||||
required
|
||||
@keydown=${this._keyDown}
|
||||
validationMessage=${this.hass.localize(
|
||||
.validationMessage=${this.hass.localize(
|
||||
"ui.panel.config.cloud.register.email_error_msg"
|
||||
)}
|
||||
></ha-textfield>
|
||||
<ha-password-field
|
||||
></ha-input>
|
||||
<ha-input
|
||||
id="password"
|
||||
type="password"
|
||||
password-toggle
|
||||
name="password"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.cloud.register.password"
|
||||
@@ -80,10 +80,10 @@ export class CloudStepSignup extends LitElement {
|
||||
minlength="8"
|
||||
required
|
||||
@keydown=${this._keyDown}
|
||||
validationMessage=${this.hass.localize(
|
||||
.validationMessage=${this.hass.localize(
|
||||
"ui.panel.config.cloud.register.password_error_msg"
|
||||
)}
|
||||
></ha-password-field>`}
|
||||
></ha-input>`}
|
||||
</div>
|
||||
<div class="footer side-by-side">
|
||||
${this._state === "VERIFY"
|
||||
@@ -131,19 +131,26 @@ export class CloudStepSignup extends LitElement {
|
||||
const emailField = this._emailField;
|
||||
const passwordField = this._passwordField;
|
||||
|
||||
let invalid = false;
|
||||
|
||||
if (!emailField.reportValidity()) {
|
||||
passwordField.reportValidity();
|
||||
invalid = true;
|
||||
emailField.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!passwordField.reportValidity()) {
|
||||
passwordField.focus();
|
||||
if (!invalid) {
|
||||
passwordField.focus();
|
||||
}
|
||||
invalid = true;
|
||||
}
|
||||
|
||||
if (invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const email = emailField.value.toLowerCase();
|
||||
const password = passwordField.value;
|
||||
const email = emailField.value!.toLowerCase();
|
||||
const password = passwordField.value!;
|
||||
|
||||
this._requestInProgress = true;
|
||||
|
||||
@@ -211,10 +218,6 @@ export class CloudStepSignup extends LitElement {
|
||||
.content {
|
||||
width: 100%;
|
||||
}
|
||||
ha-textfield,
|
||||
ha-password-field {
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
|
||||
import { customElement, property, state, query } from "lit/decorators";
|
||||
import "../../components/ha-button";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { formatDateTimeWithBrowserDefaults } from "../../common/datetime/format_date_time";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import "../../components/buttons/ha-progress-button";
|
||||
import type { HaProgressButton } from "../../components/buttons/ha-progress-button";
|
||||
import "../../components/ha-alert";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-icon-button-arrow-prev";
|
||||
import "../../components/ha-md-list";
|
||||
import "../../components/ha-md-list-item";
|
||||
import "../../components/buttons/ha-progress-button";
|
||||
import "../../components/ha-icon-button-arrow-prev";
|
||||
import "../../components/ha-password-field";
|
||||
import "../../panels/config/backup/components/ha-backup-data-picker";
|
||||
import "../../panels/config/backup/components/ha-backup-formfield-label";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import "../../components/input/ha-input";
|
||||
import {
|
||||
getPreferredAgentForDownload,
|
||||
type BackupContentExtended,
|
||||
type BackupData,
|
||||
} from "../../data/backup";
|
||||
import { restoreOnboardingBackup } from "../../data/backup_onboarding";
|
||||
import type { HaProgressButton } from "../../components/buttons/ha-progress-button";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../panels/config/backup/components/ha-backup-data-picker";
|
||||
import "../../panels/config/backup/components/ha-backup-formfield-label";
|
||||
import { onBoardingStyles } from "../styles";
|
||||
import { formatDateTimeWithBrowserDefaults } from "../../common/datetime/format_date_time";
|
||||
|
||||
@customElement("onboarding-restore-backup-restore")
|
||||
class OnboardingRestoreBackupRestore extends LitElement {
|
||||
@@ -170,7 +170,7 @@ class OnboardingRestoreBackupRestore extends LitElement {
|
||||
`ui.panel.page-onboarding.restore.details.restore.encryption.description${this.mode === "cloud" ? "_cloud" : ""}`
|
||||
)}
|
||||
</span>
|
||||
<ha-password-field
|
||||
<ha-input
|
||||
.disabled=${this._loading}
|
||||
@input=${this._encryptionKeyChanged}
|
||||
.label=${this.localize(
|
||||
@@ -178,13 +178,11 @@ class OnboardingRestoreBackupRestore extends LitElement {
|
||||
)}
|
||||
.value=${this._encryptionKey}
|
||||
@keydown=${this._keyDown}
|
||||
.errorMessage=${this._encryptionKeyWrong
|
||||
? this.localize(
|
||||
"ui.panel.page-onboarding.restore.details.restore.encryption.incorrect_key"
|
||||
)
|
||||
: ""}
|
||||
.validationMessage=${this.localize(
|
||||
"ui.panel.page-onboarding.restore.details.restore.encryption.incorrect_key"
|
||||
)}
|
||||
.invalid=${this._encryptionKeyWrong}
|
||||
></ha-password-field>
|
||||
></ha-input>
|
||||
</div>`
|
||||
: nothing}
|
||||
|
||||
@@ -353,7 +351,7 @@ class OnboardingRestoreBackupRestore extends LitElement {
|
||||
.encryption {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.encryption ha-password-field {
|
||||
.encryption ha-input {
|
||||
margin-top: 24px;
|
||||
}
|
||||
.actions {
|
||||
|
||||
@@ -5,15 +5,14 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/ha-dialog-footer";
|
||||
import "../../../components/ha-fade-in";
|
||||
import "../../../components/ha-generic-picker";
|
||||
import "../../../components/ha-markdown";
|
||||
import "../../../components/ha-password-field";
|
||||
import type { PickerComboBoxItem } from "../../../components/ha-picker-combo-box";
|
||||
import "../../../components/ha-spinner";
|
||||
import "../../../components/ha-textfield";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/input/ha-input";
|
||||
import type {
|
||||
ApplicationCredential,
|
||||
ApplicationCredentialsConfig,
|
||||
@@ -69,6 +68,7 @@ export class DialogAddApplicationCredential extends LitElement {
|
||||
this._params = params;
|
||||
this._domain = params.selectedDomain;
|
||||
this._manifest = params.manifest;
|
||||
this._invalid = false;
|
||||
this._name = "";
|
||||
this._description = "";
|
||||
this._clientId = "";
|
||||
@@ -195,7 +195,7 @@ export class DialogAddApplicationCredential extends LitElement {
|
||||
.content=${this._description}
|
||||
></ha-markdown>`
|
||||
: nothing}
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
class="name"
|
||||
name="name"
|
||||
.label=${this.hass.localize(
|
||||
@@ -205,12 +205,12 @@ export class DialogAddApplicationCredential extends LitElement {
|
||||
.invalid=${this._invalid && !this._name}
|
||||
required
|
||||
@input=${this._handleValueChanged}
|
||||
.errorMessage=${this.hass.localize(
|
||||
.validationMessage=${this.hass.localize(
|
||||
"ui.common.error_required"
|
||||
)}
|
||||
dialogInitialFocus
|
||||
></ha-textfield>
|
||||
<ha-textfield
|
||||
></ha-input>
|
||||
<ha-input
|
||||
class="clientId"
|
||||
name="clientId"
|
||||
.label=${this.hass.localize(
|
||||
@@ -220,16 +220,17 @@ export class DialogAddApplicationCredential extends LitElement {
|
||||
.invalid=${this._invalid && !this._clientId}
|
||||
required
|
||||
@input=${this._handleValueChanged}
|
||||
.errorMessage=${this.hass.localize(
|
||||
.validationMessage=${this.hass.localize(
|
||||
"ui.common.error_required"
|
||||
)}
|
||||
dialogInitialFocus
|
||||
.helper=${this.hass.localize(
|
||||
.hint=${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.editor.client_id_helper"
|
||||
)}
|
||||
helperPersistent
|
||||
></ha-textfield>
|
||||
<ha-password-field
|
||||
></ha-input>
|
||||
<ha-input
|
||||
type="password"
|
||||
password-toggle
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.editor.client_secret"
|
||||
)}
|
||||
@@ -238,14 +239,13 @@ export class DialogAddApplicationCredential extends LitElement {
|
||||
.invalid=${this._invalid && !this._clientSecret}
|
||||
required
|
||||
@input=${this._handleValueChanged}
|
||||
.errorMessage=${this.hass.localize(
|
||||
.validationMessage=${this.hass.localize(
|
||||
"ui.common.error_required"
|
||||
)}
|
||||
.helper=${this.hass.localize(
|
||||
.hint=${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.editor.client_secret_helper"
|
||||
)}
|
||||
helperPersistent
|
||||
></ha-password-field>
|
||||
></ha-input>
|
||||
</div>
|
||||
|
||||
<ha-dialog-footer slot="footer">
|
||||
@@ -377,11 +377,6 @@ export class DialogAddApplicationCredential extends LitElement {
|
||||
display: flex;
|
||||
padding: var(--ha-space-2) 0;
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
margin-top: var(--ha-space-4);
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/ha-md-textfield";
|
||||
import type { HaMdTextfield } from "../../../../../components/ha-md-textfield";
|
||||
import "../../../../../components/ha-select";
|
||||
import "../../../../../components/input/ha-input";
|
||||
import type { HaInput } from "../../../../../components/input/ha-input";
|
||||
import type { SupervisorUpdateConfig } from "../../../../../data/supervisor/update";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../../types";
|
||||
|
||||
@@ -62,7 +62,7 @@ class HaBackupConfigAddon extends LitElement {
|
||||
`ui.panel.config.backup.settings.app_update_backup.retention_description`
|
||||
)}
|
||||
</span>
|
||||
<ha-md-textfield
|
||||
<ha-input
|
||||
slot="end"
|
||||
@change=${this._backupRetentionChanged}
|
||||
.value=${this.supervisorUpdateConfig?.add_on_backup_retain_copies?.toString() ||
|
||||
@@ -70,11 +70,13 @@ class HaBackupConfigAddon extends LitElement {
|
||||
type="number"
|
||||
min=${MIN_RETENTION_VALUE.toString()}
|
||||
step="1"
|
||||
.suffixText=${this.hass.localize(
|
||||
"ui.panel.config.backup.schedule.retention_units.copies"
|
||||
)}
|
||||
>
|
||||
</ha-md-textfield>
|
||||
<span slot="end">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.schedule.retention_units.copies"
|
||||
)}
|
||||
</span>
|
||||
</ha-input>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
`;
|
||||
@@ -92,7 +94,7 @@ class HaBackupConfigAddon extends LitElement {
|
||||
}
|
||||
|
||||
private _backupRetentionChanged(ev) {
|
||||
const target = ev.currentTarget as HaMdTextfield;
|
||||
const target = ev.currentTarget as HaInput;
|
||||
const add_on_backup_retain_copies = Number(target.value);
|
||||
if (add_on_backup_retain_copies >= MIN_RETENTION_VALUE) {
|
||||
fireEvent(this, "update-config-changed", {
|
||||
@@ -115,7 +117,7 @@ class HaBackupConfigAddon extends LitElement {
|
||||
ha-select {
|
||||
min-width: 210px;
|
||||
}
|
||||
ha-md-textfield {
|
||||
ha-input {
|
||||
width: 210px;
|
||||
}
|
||||
@media all and (max-width: 450px) {
|
||||
@@ -123,7 +125,7 @@ class HaBackupConfigAddon extends LitElement {
|
||||
min-width: 160px;
|
||||
width: 160px;
|
||||
}
|
||||
ha-md-textfield {
|
||||
ha-input {
|
||||
width: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { clamp } from "../../../../../common/number/clamp";
|
||||
import "../../../../../components/ha-expansion-panel";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/ha-md-textfield";
|
||||
import type { HaMdTextfield } from "../../../../../components/ha-md-textfield";
|
||||
import "../../../../../components/ha-select";
|
||||
import type { HaSelect } from "../../../../../components/ha-select";
|
||||
import "../../../../../components/input/ha-input";
|
||||
import type { HaInput } from "../../../../../components/input/ha-input";
|
||||
import type { BackupConfig, Retention } from "../../../../../data/backup";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../../types";
|
||||
|
||||
@@ -54,7 +54,7 @@ class HaBackupConfigRetention extends LitElement {
|
||||
|
||||
@state() private _value = 3;
|
||||
|
||||
@query("#value") private _customValueField?: HaMdTextfield;
|
||||
@query("#value") private _customValueField?: HaInput;
|
||||
|
||||
@query("#type") private _customTypeField?: HaSelect;
|
||||
|
||||
@@ -141,7 +141,7 @@ class HaBackupConfigRetention extends LitElement {
|
||||
"ui.panel.config.backup.schedule.custom_retention_label"
|
||||
)}
|
||||
</span>
|
||||
<ha-md-textfield
|
||||
<ha-input
|
||||
slot="end"
|
||||
@change=${this._retentionValueChanged}
|
||||
.value=${this._value.toString()}
|
||||
@@ -151,7 +151,7 @@ class HaBackupConfigRetention extends LitElement {
|
||||
.max=${MAX_VALUE.toString()}
|
||||
step="1"
|
||||
>
|
||||
</ha-md-textfield>
|
||||
</ha-input>
|
||||
<ha-select
|
||||
slot="end"
|
||||
@selected=${this._retentionTypeChanged}
|
||||
@@ -208,8 +208,8 @@ class HaBackupConfigRetention extends LitElement {
|
||||
|
||||
private _retentionValueChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
const target = ev.currentTarget as HaMdTextfield;
|
||||
const value = parseInt(target.value);
|
||||
const target = ev.currentTarget as HaInput;
|
||||
const value = parseInt(target.value ?? "");
|
||||
const clamped = clamp(value, MIN_VALUE, MAX_VALUE);
|
||||
target.value = clamped.toString();
|
||||
|
||||
@@ -258,14 +258,14 @@ class HaBackupConfigRetention extends LitElement {
|
||||
width: 160px;
|
||||
}
|
||||
}
|
||||
ha-md-textfield#value {
|
||||
ha-input#value {
|
||||
min-width: 70px;
|
||||
}
|
||||
ha-select#type {
|
||||
min-width: 100px;
|
||||
}
|
||||
@media all and (max-width: 450px) {
|
||||
ha-md-textfield#value {
|
||||
ha-input#value {
|
||||
min-width: 60px;
|
||||
margin: 0 -8px;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import "../../../../../components/ha-expansion-panel";
|
||||
import "../../../../../components/ha-formfield";
|
||||
import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/ha-md-textfield";
|
||||
import "../../../../../components/ha-select";
|
||||
import "../../../../../components/ha-time-input";
|
||||
import "../../../../../components/ha-tip";
|
||||
|
||||
@@ -13,7 +13,6 @@ import "../../../../components/ha-icon-next";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-password-field";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type {
|
||||
BackupConfig,
|
||||
|
||||
@@ -11,7 +11,6 @@ import "../../../../components/ha-icon-button-prev";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-password-field";
|
||||
import {
|
||||
downloadEmergencyKit,
|
||||
generateEncryptionKey,
|
||||
|
||||
@@ -4,9 +4,9 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-password-field";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/input/ha-input";
|
||||
import {
|
||||
canDecryptBackupOnDownload,
|
||||
getPreferredAgentForDownload,
|
||||
@@ -85,12 +85,14 @@ class DialogDownloadDecryptedBackup extends LitElement implements HassDialog {
|
||||
)}
|
||||
</p>
|
||||
|
||||
<ha-password-field
|
||||
<ha-input
|
||||
type="password"
|
||||
password-toggle
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.download.encryption_key"
|
||||
)}
|
||||
@input=${this._keyChanged}
|
||||
></ha-password-field>
|
||||
></ha-input>
|
||||
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
|
||||
@@ -6,12 +6,12 @@ import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-spinner";
|
||||
import "../../../../components/ha-password-field";
|
||||
import "../../../../components/input/ha-input";
|
||||
|
||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { RestoreBackupParams } from "../../../../data/backup";
|
||||
import {
|
||||
fetchBackupConfig,
|
||||
@@ -23,11 +23,11 @@ import type {
|
||||
RestoreBackupState,
|
||||
} from "../../../../data/backup_manager";
|
||||
import { subscribeBackupEvents } from "../../../../data/backup_manager";
|
||||
import { waitForIntegrationSetup } from "../../../../data/integration";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { RestoreBackupDialogParams } from "./show-dialog-restore-backup";
|
||||
import { waitForIntegrationSetup } from "../../../../data/integration";
|
||||
|
||||
interface FormData {
|
||||
encryption_key_type: "config" | "custom";
|
||||
@@ -211,14 +211,16 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
|
||||
return html`
|
||||
${this._renderEncryptionIntro()}
|
||||
|
||||
<ha-password-field
|
||||
<ha-input
|
||||
type="password"
|
||||
password-toggle
|
||||
autofocus
|
||||
@input=${this._passwordChanged}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.restore.encryption.input_label"
|
||||
)}
|
||||
.value=${this._userPassword || ""}
|
||||
></ha-password-field>
|
||||
></ha-input>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -387,10 +389,6 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
}
|
||||
ha-password-field {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5,12 +5,11 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-password-field";
|
||||
import {
|
||||
downloadEmergencyKit,
|
||||
generateEncryptionKey,
|
||||
|
||||
@@ -11,7 +11,6 @@ import "../../../../components/ha-icon-button-prev";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-password-field";
|
||||
import { downloadEmergencyKit } from "../../../../data/backup";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
|
||||
@@ -13,7 +13,6 @@ import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-password-field";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { BackupAgent, BackupConfig } from "../../../data/backup";
|
||||
import { updateBackupConfig } from "../../../data/backup";
|
||||
|
||||
@@ -7,6 +7,7 @@ import { navigate } from "../../../../common/navigate";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-icon-next";
|
||||
import "../../../../components/ha-list";
|
||||
@@ -24,7 +25,6 @@ import "../../ha-config-section";
|
||||
import { showSupportPackageDialog } from "../account/show-dialog-cloud-support-package";
|
||||
import "./cloud-login";
|
||||
import type { CloudLogin } from "./cloud-login";
|
||||
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
|
||||
|
||||
@customElement("cloud-login-panel")
|
||||
export class CloudLoginPanel extends LitElement {
|
||||
@@ -149,7 +149,7 @@ export class CloudLoginPanel extends LitElement {
|
||||
private _handleForgotPassword() {
|
||||
this._dismissFlash();
|
||||
fireEvent(this, "cloud-email-changed", {
|
||||
value: this._cloudLoginElement.emailField.value,
|
||||
value: this._cloudLoginElement.emailField.value ?? "",
|
||||
});
|
||||
navigate("/config/cloud/forgot-password");
|
||||
}
|
||||
@@ -158,7 +158,7 @@ export class CloudLoginPanel extends LitElement {
|
||||
this._dismissFlash();
|
||||
|
||||
fireEvent(this, "cloud-email-changed", {
|
||||
value: this._cloudLoginElement.emailField.value,
|
||||
value: this._cloudLoginElement.emailField.value ?? "",
|
||||
});
|
||||
navigate("/config/cloud/register");
|
||||
}
|
||||
|
||||
@@ -2,26 +2,24 @@ import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/buttons/ha-progress-button";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-password-field";
|
||||
import type { HaPasswordField } from "../../../../components/ha-password-field";
|
||||
import "../../../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../../../components/ha-textfield";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/input/ha-input";
|
||||
import type { HaInput } from "../../../../components/input/ha-input";
|
||||
import { setAssistPipelinePreferred } from "../../../../data/assist_pipeline";
|
||||
import { cloudLogin } from "../../../../data/cloud";
|
||||
import { loginHaCloud } from "../../../../data/onboarding";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
showPromptDialog,
|
||||
} from "../../../lovelace/custom-card-helpers";
|
||||
import { setAssistPipelinePreferred } from "../../../../data/assist_pipeline";
|
||||
import { showCloudAlreadyConnectedDialog } from "../dialog-cloud-already-connected/show-dialog-cloud-already-connected";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { loginHaCloud } from "../../../../data/onboarding";
|
||||
|
||||
@customElement("cloud-login")
|
||||
export class CloudLogin extends LitElement {
|
||||
@@ -40,9 +38,9 @@ export class CloudLogin extends LitElement {
|
||||
|
||||
@property({ type: Boolean, attribute: "card-less" }) public cardLess = false;
|
||||
|
||||
@query("#email", true) public emailField!: HaTextField;
|
||||
@query("#email", true) public emailField!: HaInput;
|
||||
|
||||
@query("#password", true) private _passwordField!: HaPasswordField;
|
||||
@query("#password", true) private _passwordField!: HaInput;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@@ -71,13 +69,14 @@ export class CloudLogin extends LitElement {
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
.label=${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.login.email`
|
||||
)}
|
||||
id="email"
|
||||
name="username"
|
||||
type="email"
|
||||
hint="This should be a real email address, not an alias. If you used an alias to register, use the email address that the alias forwards to."
|
||||
autocomplete="username"
|
||||
required
|
||||
.value=${this.email ?? ""}
|
||||
@@ -86,10 +85,13 @@ export class CloudLogin extends LitElement {
|
||||
.validationMessage=${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.login.email_error_msg`
|
||||
)}
|
||||
></ha-textfield>
|
||||
<ha-password-field
|
||||
></ha-input>
|
||||
<ha-input
|
||||
id="password"
|
||||
type="password"
|
||||
password-toggle
|
||||
name="password"
|
||||
hint="Use your nabu casa password, not your Home Assistant password. If you don't remember it, use the forgot password link below."
|
||||
.label=${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.login.password`
|
||||
)}
|
||||
@@ -101,7 +103,7 @@ export class CloudLogin extends LitElement {
|
||||
.validationMessage=${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.login.password_error_msg`
|
||||
)}
|
||||
></ha-password-field>
|
||||
></ha-input>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button
|
||||
@@ -277,21 +279,29 @@ export class CloudLogin extends LitElement {
|
||||
|
||||
private async _handleLogin() {
|
||||
if (!this._inProgress) {
|
||||
let valid = true;
|
||||
|
||||
if (!this.emailField.reportValidity()) {
|
||||
this.emailField.focus();
|
||||
return;
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (!this._passwordField.reportValidity()) {
|
||||
this._passwordField.focus();
|
||||
if (valid) {
|
||||
this._passwordField.focus();
|
||||
}
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._inProgress = true;
|
||||
|
||||
this._login(
|
||||
this.emailField.value,
|
||||
this._passwordField.value,
|
||||
this.emailField.value as string,
|
||||
this._passwordField.value as string,
|
||||
this.checkConnection
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,14 +5,13 @@ import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/buttons/ha-progress-button";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../../../components/ha-textfield";
|
||||
import "../../../../components/input/ha-input";
|
||||
import type { HaInput } from "../../../../components/input/ha-input";
|
||||
import { cloudRegister, cloudResendVerification } from "../../../../data/cloud";
|
||||
import "../../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../../ha-config-section";
|
||||
import "../../../../components/ha-password-field";
|
||||
|
||||
@customElement("cloud-register")
|
||||
export class CloudRegister extends LitElement {
|
||||
@@ -30,9 +29,9 @@ export class CloudRegister extends LitElement {
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@query("#email", true) private _emailField!: HaTextField;
|
||||
@query("#email", true) private _emailField!: HaInput;
|
||||
|
||||
@query("#password", true) private _passwordField!: HaTextField;
|
||||
@query("#password", true) private _passwordField!: HaInput;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
@@ -131,7 +130,7 @@ export class CloudRegister extends LitElement {
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
autofocus
|
||||
id="email"
|
||||
name="email"
|
||||
@@ -143,12 +142,14 @@ export class CloudRegister extends LitElement {
|
||||
required
|
||||
.value=${this.email ?? ""}
|
||||
@keydown=${this._keyDown}
|
||||
validationMessage=${this.hass.localize(
|
||||
.validationMessage=${this.hass.localize(
|
||||
"ui.panel.config.cloud.register.email_error_msg"
|
||||
)}
|
||||
></ha-textfield>
|
||||
<ha-password-field
|
||||
></ha-input>
|
||||
<ha-input
|
||||
id="password"
|
||||
type="password"
|
||||
password-toggle
|
||||
name="password"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.cloud.register.password"
|
||||
@@ -158,10 +159,10 @@ export class CloudRegister extends LitElement {
|
||||
minlength="8"
|
||||
required
|
||||
@keydown=${this._keyDown}
|
||||
validationMessage=${this.hass.localize(
|
||||
.validationMessage=${this.hass.localize(
|
||||
"ui.panel.config.cloud.register.password_error_msg"
|
||||
)}
|
||||
></ha-password-field>
|
||||
></ha-input>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<button
|
||||
@@ -209,8 +210,8 @@ export class CloudRegister extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const email = emailField.value.toLowerCase();
|
||||
const password = passwordField.value;
|
||||
const email = emailField.value?.toLowerCase() || "";
|
||||
const password = passwordField.value || "";
|
||||
|
||||
this._requestInProgress = true;
|
||||
|
||||
@@ -235,7 +236,7 @@ export class CloudRegister extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const email = emailField.value;
|
||||
const email = emailField.value || "";
|
||||
|
||||
const doResend = async (username: string) => {
|
||||
try {
|
||||
|
||||
@@ -8,7 +8,7 @@ import "../../../../components/ha-duration-input";
|
||||
import type { HaDurationData } from "../../../../components/ha-duration-input";
|
||||
import "../../../../components/ha-formfield";
|
||||
import "../../../../components/ha-icon-picker";
|
||||
import "../../../../components/ha-textfield";
|
||||
import "../../../../components/input/ha-input";
|
||||
import type { ForDict } from "../../../../data/automation";
|
||||
import type { DurationDict, Timer } from "../../../../data/timer";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
@@ -66,21 +66,21 @@ class HaTimerForm extends LitElement {
|
||||
|
||||
return html`
|
||||
<div class="form">
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
.value=${this._name}
|
||||
.configValue=${"name"}
|
||||
@input=${this._valueChanged}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.dialogs.helper_settings.generic.name"
|
||||
)}
|
||||
autoValidate
|
||||
auto-validate
|
||||
required
|
||||
.validationMessage=${this.hass!.localize(
|
||||
"ui.dialogs.helper_settings.required_error_msg"
|
||||
)}
|
||||
dialogInitialFocus
|
||||
.disabled=${this.disabled}
|
||||
></ha-textfield>
|
||||
></ha-input>
|
||||
<ha-icon-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._icon}
|
||||
|
||||
@@ -110,13 +110,6 @@ class DialogSystemLogDetail extends LitElement {
|
||||
${item.name}<br />
|
||||
${this.hass.localize("ui.panel.config.logs.detail.source")}:
|
||||
${item.source.join(":")}
|
||||
<br />
|
||||
${this.hass.localize("ui.panel.config.logs.classification")}:
|
||||
${item.error_type
|
||||
? this.hass.localize(
|
||||
`ui.panel.config.logs.error_type.${item.error_type}`
|
||||
)
|
||||
: this.hass.localize("ui.panel.config.logs.other")}
|
||||
${integration
|
||||
? html`
|
||||
<br />
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import {
|
||||
mdiDotsVertical,
|
||||
mdiChevronDown,
|
||||
mdiChip,
|
||||
mdiDns,
|
||||
mdiDownload,
|
||||
mdiFilterVariant,
|
||||
mdiFilterVariantRemove,
|
||||
mdiPackageVariant,
|
||||
mdiPuzzle,
|
||||
mdiRadar,
|
||||
mdiRefresh,
|
||||
mdiText,
|
||||
mdiVolumeHigh,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
@@ -23,14 +17,10 @@ import { navigate } from "../../../common/navigate";
|
||||
import { stringCompare } from "../../../common/string/compare";
|
||||
import { extractSearchParam } from "../../../common/url/search-params";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/chips/ha-assist-chip";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-generic-picker";
|
||||
import "../../../components/ha-icon-button";
|
||||
import type { HaGenericPicker } from "../../../components/ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "../../../components/ha-picker-combo-box";
|
||||
import "../../../components/search-input-outlined";
|
||||
import "../../../components/search-input";
|
||||
import type { LogProvider } from "../../../data/error_log";
|
||||
import { fetchHassioAddonsInfo } from "../../../data/hassio/addon";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
@@ -38,7 +28,6 @@ import "../../../layouts/hass-subpage";
|
||||
import { mdiHomeAssistant } from "../../../resources/home-assistant-logo-svg";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant, Route, ValueChangedEvent } from "../../../types";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "./error-log-card";
|
||||
import "./system-log-card";
|
||||
import type { SystemLogCard } from "./system-log-card";
|
||||
@@ -92,9 +81,13 @@ export class HaConfigLogs extends LitElement {
|
||||
|
||||
@state() private _logProviders = logProviders;
|
||||
|
||||
@state() private _showSystemLogFilters = false;
|
||||
|
||||
@state() private _systemLogFiltersCount = 0;
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
const systemLog = this.systemLog;
|
||||
if (systemLog && systemLog.loaded) {
|
||||
systemLog.fetchData();
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps): void {
|
||||
super.firstUpdated(changedProps);
|
||||
@@ -105,140 +98,37 @@ export class HaConfigLogs extends LitElement {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
private _toggleSystemLogFilters = () => {
|
||||
this._showSystemLogFilters = !this._showSystemLogFilters;
|
||||
};
|
||||
|
||||
private _handleSystemLogFiltersChanged(ev: CustomEvent) {
|
||||
this._showSystemLogFilters = ev.detail.open;
|
||||
this._systemLogFiltersCount = ev.detail.count;
|
||||
}
|
||||
|
||||
private _downloadSystemLog = () => {
|
||||
this.systemLog?.downloadLogs();
|
||||
};
|
||||
|
||||
private _refreshSystemLog = () => {
|
||||
this.systemLog?.fetchData();
|
||||
};
|
||||
|
||||
private _clearSystemLog = () => {
|
||||
this.systemLog?.clearLogs();
|
||||
};
|
||||
|
||||
private _clearSystemLogFilters = () => {
|
||||
this.systemLog?.clearFilters();
|
||||
};
|
||||
|
||||
private _handleSystemLogOverflowAction(ev: HaDropdownSelectEvent): void {
|
||||
if (ev.detail.item.value === "show-full-logs") {
|
||||
this._showDetail();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const showSystemLog = this._selectedLogProvider === "core" && !this._detail;
|
||||
const selectedProvider = this._getActiveProvider(this._selectedLogProvider);
|
||||
const header =
|
||||
selectedProvider?.primary ||
|
||||
this.hass.localize("ui.panel.config.logs.caption");
|
||||
|
||||
const searchRow = html`
|
||||
<div
|
||||
class="search-row ${showSystemLog
|
||||
? "with-filters"
|
||||
: ""} ${showSystemLog && this._showSystemLogFilters && !this.narrow
|
||||
? "with-pane"
|
||||
: ""}"
|
||||
>
|
||||
${showSystemLog
|
||||
? this._showSystemLogFilters && !this.narrow
|
||||
? html`
|
||||
<div class="filter-controls">
|
||||
<div class="relative filter-button">
|
||||
<ha-assist-chip
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.subpage-data-table.filters"
|
||||
)}
|
||||
active
|
||||
@click=${this._toggleSystemLogFilters}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiFilterVariant}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${this._systemLogFiltersCount
|
||||
? html`<div class="badge">
|
||||
${this._systemLogFiltersCount}
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-icon-button
|
||||
.path=${mdiFilterVariantRemove}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.subpage-data-table.clear_filter"
|
||||
)}
|
||||
.disabled=${!this._systemLogFiltersCount}
|
||||
@click=${this._clearSystemLogFilters}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="relative filter-button">
|
||||
<ha-assist-chip
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.subpage-data-table.filters"
|
||||
)}
|
||||
.active=${this._showSystemLogFilters ||
|
||||
Boolean(this._systemLogFiltersCount)}
|
||||
@click=${this._toggleSystemLogFilters}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiFilterVariant}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${this._systemLogFiltersCount
|
||||
? html`<div class="badge">
|
||||
${this._systemLogFiltersCount}
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<search-input-outlined
|
||||
class="search-input"
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
.label=${this.hass.localize("ui.panel.config.logs.search")}
|
||||
.placeholder=${this.hass.localize("ui.panel.config.logs.search")}
|
||||
@value-changed=${this._filterChanged}
|
||||
></search-input-outlined>
|
||||
|
||||
${showSystemLog
|
||||
? html`
|
||||
<ha-assist-chip
|
||||
class="clear-chip"
|
||||
.label=${this.hass.localize("ui.panel.config.logs.clear")}
|
||||
.disabled=${!this.systemLog?.hasItems}
|
||||
@click=${this._clearSystemLog}
|
||||
></ha-assist-chip>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
|
||||
const search = this.narrow
|
||||
? html`<div slot="header">${searchRow}</div>`
|
||||
: searchRow;
|
||||
? html`
|
||||
<div slot="header">
|
||||
<search-input
|
||||
class="header"
|
||||
@value-changed=${this._filterChanged}
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
.label=${this.hass.localize("ui.panel.config.logs.search")}
|
||||
></search-input>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="search">
|
||||
<search-input
|
||||
@value-changed=${this._filterChanged}
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
.label=${this.hass.localize("ui.panel.config.logs.search")}
|
||||
></search-input>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const selectedProvider = this._getActiveProvider(this._selectedLogProvider);
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${header}
|
||||
.header=${this.hass.localize("ui.panel.config.logs.caption")}
|
||||
back-path="/config/system"
|
||||
>
|
||||
${isComponentLoaded(this.hass, "hassio") && this._logProviders
|
||||
@@ -274,48 +164,17 @@ export class HaConfigLogs extends LitElement {
|
||||
</ha-generic-picker>
|
||||
`
|
||||
: nothing}
|
||||
${showSystemLog
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="toolbar-icon"
|
||||
.path=${mdiDownload}
|
||||
@click=${this._downloadSystemLog}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.logs.download_logs"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
slot="toolbar-icon"
|
||||
.path=${mdiRefresh}
|
||||
@click=${this._refreshSystemLog}
|
||||
.label=${this.hass.localize("ui.common.refresh")}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown
|
||||
slot="toolbar-icon"
|
||||
@wa-select=${this._handleSystemLogOverflowAction}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.path=${mdiDotsVertical}
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown-item value="show-full-logs">
|
||||
<ha-svg-icon slot="icon" .path=${mdiText}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.logs.show_full_logs")}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
`
|
||||
: nothing}
|
||||
${search}
|
||||
<div class="content">
|
||||
${showSystemLog
|
||||
${this._selectedLogProvider === "core" && !this._detail
|
||||
? html`
|
||||
<system-log-card
|
||||
.hass=${this.hass}
|
||||
.header=${this._logProviders.find(
|
||||
(p) => p.key === this._selectedLogProvider
|
||||
)!.name}
|
||||
.filter=${this._filter}
|
||||
.showFilters=${this._showSystemLogFilters}
|
||||
@system-log-filters-changed=${this
|
||||
._handleSystemLogFiltersChanged}
|
||||
@switch-log-view=${this._showDetail}
|
||||
></system-log-card>
|
||||
`
|
||||
: html`<error-log-card
|
||||
@@ -335,7 +194,6 @@ export class HaConfigLogs extends LitElement {
|
||||
|
||||
private _showDetail() {
|
||||
this._detail = !this._detail;
|
||||
this._showSystemLogFilters = false;
|
||||
}
|
||||
|
||||
private _openPicker(ev: Event) {
|
||||
@@ -350,8 +208,6 @@ export class HaConfigLogs extends LitElement {
|
||||
}
|
||||
this._selectedLogProvider = provider;
|
||||
this._filter = "";
|
||||
this._showSystemLogFilters = false;
|
||||
this._systemLogFiltersCount = 0;
|
||||
navigate(`/config/logs?provider=${this._selectedLogProvider}`);
|
||||
}
|
||||
|
||||
@@ -486,101 +342,24 @@ export class HaConfigLogs extends LitElement {
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
}
|
||||
.search-row {
|
||||
.search {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 56px;
|
||||
width: 100%;
|
||||
gap: var(--ha-space-4);
|
||||
padding: 0 var(--ha-space-4);
|
||||
background: var(--primary-background-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-row.with-pane {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
var(--sidepane-width, 250px) minmax(0, 1fr)
|
||||
auto;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.search-row.with-pane .filter-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 var(--ha-space-4);
|
||||
border-inline-end: 1px solid var(--divider-color);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-row.with-pane .search-input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
margin-inline-start: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.search-row.with-pane .clear-chip {
|
||||
justify-self: end;
|
||||
margin-inline-start: var(--ha-space-4);
|
||||
margin-inline-end: var(--ha-space-4);
|
||||
}
|
||||
|
||||
search-input-outlined {
|
||||
search-input {
|
||||
display: block;
|
||||
flex: 1;
|
||||
--mdc-text-field-fill-color: var(--sidebar-background-color);
|
||||
--mdc-text-field-idle-line-color: var(--divider-color);
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
inset-inline-end: -4px;
|
||||
inset-inline-start: initial;
|
||||
min-width: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
background-color: var(--primary-color);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
text-align: center;
|
||||
padding: 0 2px;
|
||||
color: var(--text-primary-color);
|
||||
search-input.header {
|
||||
--mdc-ripple-color: transparant;
|
||||
margin-left: -16px;
|
||||
margin-inline-start: -16px;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
.content {
|
||||
direction: ltr;
|
||||
height: calc(
|
||||
100vh -
|
||||
1px - var(--header-height, 0px) - var(
|
||||
--safe-area-inset-top,
|
||||
0px
|
||||
) - var(--safe-area-inset-bottom, 0px) -
|
||||
56px
|
||||
);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
ha-assist-chip {
|
||||
--ha-assist-chip-container-shape: 10px;
|
||||
--ha-assist-chip-container-color: var(--card-background-color);
|
||||
}
|
||||
|
||||
.clear-chip {
|
||||
white-space: nowrap;
|
||||
}
|
||||
ha-generic-picker {
|
||||
--md-list-item-leading-icon-color: var(--ha-color-primary-50);
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import { mdiDotsVertical, mdiDownload, mdiRefresh, mdiText } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/ha-filter-states";
|
||||
import "../../../components/buttons/ha-call-service-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-list";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-spinner";
|
||||
import { getSignedPath } from "../../../data/auth";
|
||||
import { getErrorLogDownloadUrl } from "../../../data/error_log";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import type { LoggedError, SystemLogErrorType } from "../../../data/system_log";
|
||||
import type { LoggedError } from "../../../data/system_log";
|
||||
import {
|
||||
fetchSystemLog,
|
||||
getLoggedErrorIntegration,
|
||||
@@ -19,6 +25,7 @@ import type { HomeAssistant } from "../../../types";
|
||||
import { fileDownload } from "../../../util/file_download";
|
||||
import { showSystemLogDetailDialog } from "./show-dialog-system-log-detail";
|
||||
import { formatSystemLogTime } from "./util";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
|
||||
@customElement("system-log-card")
|
||||
export class SystemLogCard extends LitElement {
|
||||
@@ -26,45 +33,15 @@ export class SystemLogCard extends LitElement {
|
||||
|
||||
@property() public filter = "";
|
||||
|
||||
@property({ type: Boolean, attribute: "show-filters" })
|
||||
public showFilters = false;
|
||||
@property() public header?: string;
|
||||
|
||||
public loaded = false;
|
||||
|
||||
@state() private _items?: LoggedError[];
|
||||
|
||||
@state() private _levelFilter: string[] = [];
|
||||
|
||||
@state() private _errorTypeFilter: (SystemLogErrorType | "unknown")[] = [];
|
||||
|
||||
public async fetchData(): Promise<void> {
|
||||
this._items = undefined;
|
||||
this._items = await fetchSystemLog(this.hass);
|
||||
}
|
||||
|
||||
public async clearLogs(): Promise<void> {
|
||||
await this.hass.callService("system_log", "clear");
|
||||
this._items = [];
|
||||
}
|
||||
|
||||
public async downloadLogs(): Promise<void> {
|
||||
const timeString = new Date().toISOString().replace(/:/g, "-");
|
||||
const downloadUrl = getErrorLogDownloadUrl(this.hass);
|
||||
const logFileName = `home-assistant_${timeString}.log`;
|
||||
const signedUrl = await getSignedPath(this.hass, downloadUrl);
|
||||
fileDownload(signedUrl.path, logFileName);
|
||||
}
|
||||
|
||||
public get activeFiltersCount(): number {
|
||||
return this._levelFilter.length + this._errorTypeFilter.length;
|
||||
}
|
||||
|
||||
public get hasItems(): boolean {
|
||||
return (this._items?.length || 0) > 0;
|
||||
}
|
||||
|
||||
public clearFilters(): void {
|
||||
this._levelFilter = [];
|
||||
this._errorTypeFilter = [];
|
||||
this._notifyFiltersState();
|
||||
this._items = await fetchSystemLog(this.hass!);
|
||||
}
|
||||
|
||||
private _timestamp(item: LoggedError): string {
|
||||
@@ -87,30 +64,8 @@ export class SystemLogCard extends LitElement {
|
||||
}
|
||||
|
||||
private _getFilteredItems = memoizeOne(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
items: LoggedError[],
|
||||
filter: string,
|
||||
levelFilter: string[],
|
||||
errorTypeFilter: (SystemLogErrorType | "unknown")[]
|
||||
) =>
|
||||
(localize: LocalizeFunc, items: LoggedError[], filter: string) =>
|
||||
items.filter((item: LoggedError) => {
|
||||
if (levelFilter.length && !levelFilter.includes(item.level)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (errorTypeFilter.length) {
|
||||
const matchesKnown =
|
||||
item.error_type !== undefined &&
|
||||
errorTypeFilter.includes(item.error_type);
|
||||
const matchesUnknown =
|
||||
item.error_type === undefined &&
|
||||
errorTypeFilter.includes("unknown");
|
||||
if (!matchesKnown && !matchesUnknown) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
const integration = getLoggedErrorIntegration(item);
|
||||
return (
|
||||
@@ -119,14 +74,6 @@ export class SystemLogCard extends LitElement {
|
||||
) ||
|
||||
item.source[0].toLowerCase().includes(filter) ||
|
||||
item.name.toLowerCase().includes(filter) ||
|
||||
(item.error_type &&
|
||||
(item.error_type.includes(filter) ||
|
||||
this.hass
|
||||
.localize(
|
||||
`ui.panel.config.logs.error_type.${item.error_type}`
|
||||
)
|
||||
.toLowerCase()
|
||||
.includes(filter))) ||
|
||||
(integration &&
|
||||
domainToName(localize, integration)
|
||||
.toLowerCase()
|
||||
@@ -135,203 +82,203 @@ export class SystemLogCard extends LitElement {
|
||||
this._multipleMessages(item).toLowerCase().includes(filter)
|
||||
);
|
||||
}
|
||||
|
||||
return item;
|
||||
})
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (this._items === undefined) {
|
||||
return html`
|
||||
<div class="loading-container">
|
||||
<ha-spinner></ha-spinner>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const filteredItems = this._getFilteredItems(
|
||||
this.hass.localize,
|
||||
this._items,
|
||||
this.filter.toLowerCase(),
|
||||
this._levelFilter,
|
||||
this._errorTypeFilter
|
||||
);
|
||||
|
||||
const levels = [...new Set(this._items.map((item) => item.level))];
|
||||
const errorTypes = [
|
||||
...new Set(
|
||||
this._items
|
||||
.map((item) => item.error_type)
|
||||
.filter((type): type is SystemLogErrorType => Boolean(type))
|
||||
),
|
||||
];
|
||||
|
||||
const filteredItems = this._items
|
||||
? this._getFilteredItems(
|
||||
this.hass.localize,
|
||||
this._items,
|
||||
this.filter.toLowerCase()
|
||||
)
|
||||
: [];
|
||||
const integrations = filteredItems.length
|
||||
? filteredItems.map((item) => getLoggedErrorIntegration(item))
|
||||
: [];
|
||||
return html`
|
||||
<div class="system-log-intro">
|
||||
<ha-card outlined>
|
||||
${this._items === undefined
|
||||
? html`
|
||||
<div class="loading-container">
|
||||
<ha-spinner></ha-spinner>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="header">
|
||||
<h1 class="card-header">${this.header || "Logs"}</h1>
|
||||
<div class="header-buttons">
|
||||
<ha-icon-button
|
||||
.path=${mdiDownload}
|
||||
@click=${this._downloadLogs}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.logs.download_logs"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
.path=${mdiRefresh}
|
||||
@click=${this.fetchData}
|
||||
.label=${this.hass.localize("ui.common.refresh")}
|
||||
></ha-icon-button>
|
||||
|
||||
const hasActiveFilters = this.activeFiltersCount > 0;
|
||||
|
||||
const listContent =
|
||||
this._items.length === 0
|
||||
? html`
|
||||
<div class="card-content empty-content">
|
||||
${this.hass.localize("ui.panel.config.logs.no_issues")}
|
||||
</div>
|
||||
`
|
||||
: filteredItems.length === 0 && (this.filter || hasActiveFilters)
|
||||
? html`
|
||||
<div class="card-content">
|
||||
${this.filter
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.logs.no_issues_search",
|
||||
{
|
||||
term: this.filter,
|
||||
}
|
||||
)
|
||||
: this.hass.localize("ui.panel.config.logs.no_issues")}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="list-wrapper">
|
||||
<ha-list>
|
||||
${filteredItems.map(
|
||||
(item, idx) => html`
|
||||
<ha-list-item
|
||||
@click=${this._openLog}
|
||||
.logItem=${item}
|
||||
twoline
|
||||
>
|
||||
${item.message[0]}
|
||||
<span slot="secondary" class="secondary">
|
||||
${this._timestamp(item)} –
|
||||
${html`(<span class=${item.level}
|
||||
>${this.hass.localize(
|
||||
`ui.panel.config.logs.level.${item.level}`
|
||||
)}</span
|
||||
>) `}
|
||||
${item.error_type
|
||||
? html`(<span class="error-type-text"
|
||||
>${this.hass.localize(
|
||||
`ui.panel.config.logs.error_type.${item.error_type}`
|
||||
)}</span
|
||||
>) `
|
||||
: nothing}
|
||||
${integrations[idx]
|
||||
? `${domainToName(
|
||||
this.hass.localize,
|
||||
integrations[idx]!
|
||||
)}${
|
||||
isCustomIntegrationError(item)
|
||||
? ` (${this.hass.localize(
|
||||
"ui.panel.config.logs.custom_integration"
|
||||
)})`
|
||||
: ""
|
||||
}`
|
||||
: item.source[0]}
|
||||
${item.count > 1
|
||||
? html` - ${this._multipleMessages(item)} `
|
||||
: nothing}
|
||||
</span>
|
||||
</ha-list-item>
|
||||
<ha-dropdown @wa-select=${this._handleOverflowAction}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.path=${mdiDotsVertical}
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown-item value="show-full-logs">
|
||||
<ha-svg-icon slot="icon" .path=${mdiText}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.logs.show_full_logs"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
${this._items.length === 0
|
||||
? html`
|
||||
<div class="card-content empty-content">
|
||||
${this.hass.localize("ui.panel.config.logs.no_issues")}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</ha-list>
|
||||
</div>
|
||||
`;
|
||||
: filteredItems.length === 0 && this.filter
|
||||
? html`<div class="card-content">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.logs.no_issues_search",
|
||||
{ term: this.filter }
|
||||
)}
|
||||
</div>`
|
||||
: html`<ha-list
|
||||
>${filteredItems.map(
|
||||
(item, idx) => html`
|
||||
<ha-list-item
|
||||
@click=${this._openLog}
|
||||
.logItem=${item}
|
||||
twoline
|
||||
>
|
||||
${item.message[0]}
|
||||
<span slot="secondary" class="secondary">
|
||||
${this._timestamp(item)} –
|
||||
${html`(<span class=${item.level}
|
||||
>${this.hass.localize(
|
||||
`ui.panel.config.logs.level.${item.level}`
|
||||
)}</span
|
||||
>) `}
|
||||
${integrations[idx]
|
||||
? `${domainToName(
|
||||
this.hass!.localize,
|
||||
integrations[idx]!
|
||||
)}${
|
||||
isCustomIntegrationError(item)
|
||||
? ` (${this.hass.localize(
|
||||
"ui.panel.config.logs.custom_integration"
|
||||
)})`
|
||||
: ""
|
||||
}`
|
||||
: item.source[0]}
|
||||
${item.count > 1
|
||||
? html` - ${this._multipleMessages(item)} `
|
||||
: nothing}
|
||||
</span>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}</ha-list
|
||||
>`}
|
||||
|
||||
return this.showFilters
|
||||
? html`
|
||||
<div class="content-layout">
|
||||
<div class="pane">
|
||||
<div class="pane-content">
|
||||
<ha-filter-states
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.logs.level_filter"
|
||||
)}
|
||||
.states=${levels.map((level) => ({
|
||||
value: level,
|
||||
label: this.hass.localize(
|
||||
`ui.panel.config.logs.level.${level}`
|
||||
),
|
||||
}))}
|
||||
.value=${this._levelFilter}
|
||||
@data-table-filter-changed=${this._levelFilterChanged}
|
||||
></ha-filter-states>
|
||||
<ha-filter-states
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.logs.classification"
|
||||
)}
|
||||
.states=${[
|
||||
...errorTypes.map((errorType) => ({
|
||||
value: errorType,
|
||||
label: this.hass.localize(
|
||||
`ui.panel.config.logs.error_type.${errorType}`
|
||||
),
|
||||
})),
|
||||
{
|
||||
value: "unknown",
|
||||
label: this.hass.localize("ui.panel.config.logs.other"),
|
||||
},
|
||||
]}
|
||||
.value=${this._errorTypeFilter}
|
||||
@data-table-filter-changed=${this._errorTypeFilterChanged}
|
||||
></ha-filter-states>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-main">${listContent}</div>
|
||||
</div>
|
||||
`
|
||||
: listContent;
|
||||
<div class="card-actions">
|
||||
<ha-call-service-button
|
||||
.hass=${this.hass}
|
||||
domain="system_log"
|
||||
service="clear"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.logs.clear"
|
||||
)}</ha-call-service-button
|
||||
>
|
||||
</div>
|
||||
`}
|
||||
</ha-card>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps): void {
|
||||
super.firstUpdated(changedProps);
|
||||
this.fetchData();
|
||||
this._notifyFiltersState();
|
||||
}
|
||||
|
||||
private _levelFilterChanged(ev): void {
|
||||
this._levelFilter = ev.detail.value || [];
|
||||
this._notifyFiltersState();
|
||||
}
|
||||
|
||||
private _errorTypeFilterChanged(ev): void {
|
||||
this._errorTypeFilter = ev.detail.value || [];
|
||||
this._notifyFiltersState();
|
||||
}
|
||||
|
||||
private _notifyFiltersState(): void {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("system-log-filters-changed", {
|
||||
detail: {
|
||||
open: this.showFilters,
|
||||
count: this.activeFiltersCount,
|
||||
},
|
||||
})
|
||||
this.loaded = true;
|
||||
this.addEventListener("hass-service-called", (ev) =>
|
||||
this.serviceCalled(ev)
|
||||
);
|
||||
}
|
||||
|
||||
protected serviceCalled(ev): void {
|
||||
// Check if this is for us
|
||||
if (ev.detail.success && ev.detail.domain === "system_log") {
|
||||
// Do the right thing depending on service
|
||||
if (ev.detail.service === "clear") {
|
||||
this._items = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _handleOverflowAction(ev: HaDropdownSelectEvent) {
|
||||
if (ev.detail.item.value === "show-full-logs") {
|
||||
// @ts-ignore
|
||||
fireEvent(this, "switch-log-view");
|
||||
}
|
||||
}
|
||||
|
||||
private async _downloadLogs() {
|
||||
const timeString = new Date().toISOString().replace(/:/g, "-");
|
||||
const downloadUrl = getErrorLogDownloadUrl(this.hass);
|
||||
const logFileName = `home-assistant_${timeString}.log`;
|
||||
const signedUrl = await getSignedPath(this.hass, downloadUrl);
|
||||
fileDownload(signedUrl.path, logFileName);
|
||||
}
|
||||
|
||||
private _openLog(ev: Event): void {
|
||||
const item = (ev.currentTarget as any).logItem;
|
||||
showSystemLogDetailDialog(this, { item });
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
direction: var(--direction);
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
background: var(--primary-background-color);
|
||||
ha-card {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
:host {
|
||||
direction: var(--direction);
|
||||
}
|
||||
ha-list {
|
||||
direction: ltr;
|
||||
background: var(--card-background-color);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.header-buttons {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
color: var(--ha-card-header-color, var(--primary-text-color));
|
||||
font-family: var(--ha-card-header-font-family, inherit);
|
||||
font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
|
||||
letter-spacing: -0.012em;
|
||||
line-height: var(--ha-line-height-expanded);
|
||||
display: block;
|
||||
margin-block-start: 0px;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
}
|
||||
|
||||
.system-log-intro {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
@@ -341,39 +288,6 @@ export class SystemLogCard extends LitElement {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.content-layout {
|
||||
display: flex;
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: var(--card-background-color);
|
||||
}
|
||||
|
||||
.list-wrapper {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
min-height: 100%;
|
||||
background: var(--card-background-color);
|
||||
}
|
||||
|
||||
.pane {
|
||||
flex: 0 0 var(--sidepane-width, 250px);
|
||||
width: var(--sidepane-width, 250px);
|
||||
border-inline-end: 1px solid var(--divider-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
background: var(--primary-background-color);
|
||||
}
|
||||
|
||||
.pane-content {
|
||||
overflow: auto;
|
||||
background: var(--primary-background-color);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
@@ -382,33 +296,15 @@ export class SystemLogCard extends LitElement {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.error-type-text {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
min-height: 100%;
|
||||
background: var(--card-background-color);
|
||||
}
|
||||
|
||||
.row-secondary {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.content-layout {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.pane {
|
||||
width: 100%;
|
||||
border-inline-end: none;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,14 +13,13 @@ import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-list";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-password-field";
|
||||
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";
|
||||
import "../../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../../components/ha-textfield";
|
||||
import "../../../components/input/ha-input";
|
||||
import type { HaInput } from "../../../components/input/ha-input";
|
||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||
import {
|
||||
type AccessPoint,
|
||||
@@ -233,7 +232,9 @@ export class HassioNetwork extends LitElement {
|
||||
${this._wifiConfiguration.auth === "wpa-psk" ||
|
||||
this._wifiConfiguration.auth === "wep"
|
||||
? html`
|
||||
<ha-password-field
|
||||
<ha-input
|
||||
type="password"
|
||||
password-toggle
|
||||
id="psk"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.wifi_password"
|
||||
@@ -241,7 +242,7 @@ export class HassioNetwork extends LitElement {
|
||||
.version=${"wifi"}
|
||||
@change=${this._handleInputValueChangedWifi}
|
||||
>
|
||||
</ha-password-field>
|
||||
</ha-input>
|
||||
`
|
||||
: nothing}
|
||||
`
|
||||
@@ -388,7 +389,7 @@ export class HassioNetwork extends LitElement {
|
||||
const { ip, mask, prefix } = parseAddress(address);
|
||||
return html`
|
||||
<div class="address-row">
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
id="address"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.ip"
|
||||
@@ -399,10 +400,10 @@ export class HassioNetwork extends LitElement {
|
||||
@change=${this._handleInputValueChanged}
|
||||
.disabled=${disableInputs}
|
||||
>
|
||||
</ha-textfield>
|
||||
</ha-input>
|
||||
${version === "ipv6"
|
||||
? html`
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
id="prefix"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.prefix"
|
||||
@@ -413,10 +414,10 @@ export class HassioNetwork extends LitElement {
|
||||
@change=${this._handleInputValueChanged}
|
||||
.disabled=${disableInputs}
|
||||
>
|
||||
</ha-textfield>
|
||||
</ha-input>
|
||||
`
|
||||
: html`
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
id="netmask"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.netmask"
|
||||
@@ -427,7 +428,7 @@ export class HassioNetwork extends LitElement {
|
||||
@change=${this._handleInputValueChanged}
|
||||
.disabled=${disableInputs}
|
||||
>
|
||||
</ha-textfield>
|
||||
</ha-input>
|
||||
`}
|
||||
${this._interface![version].address.length > 1 &&
|
||||
!disableInputs
|
||||
@@ -461,7 +462,7 @@ export class HassioNetwork extends LitElement {
|
||||
</ha-button>
|
||||
`
|
||||
: nothing}
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
id="gateway"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.gateway"
|
||||
@@ -471,12 +472,12 @@ export class HassioNetwork extends LitElement {
|
||||
@change=${this._handleInputValueChanged}
|
||||
.disabled=${disableInputs}
|
||||
>
|
||||
</ha-textfield>
|
||||
</ha-input>
|
||||
<div class="nameservers">
|
||||
${nameservers.map(
|
||||
(nameserver: string, index: number) => html`
|
||||
<div class="address-row">
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
id="nameserver"
|
||||
.label=${`${this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.dns_server"
|
||||
@@ -486,10 +487,11 @@ export class HassioNetwork extends LitElement {
|
||||
.index=${index}
|
||||
@change=${this._handleInputValueChanged}
|
||||
>
|
||||
</ha-textfield>
|
||||
</ha-input>
|
||||
${this._interface![version].nameservers?.length > 1
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="end"
|
||||
.label=${this.hass.localize("ui.common.delete")}
|
||||
.path=${mdiDeleteOutline}
|
||||
.version=${version}
|
||||
@@ -677,12 +679,13 @@ export class HassioNetwork extends LitElement {
|
||||
}
|
||||
|
||||
private _handleInputValueChanged(ev: Event): void {
|
||||
const source = ev.target as HaTextField;
|
||||
const source = ev.target as HaInput;
|
||||
const value = source.value;
|
||||
const version = (ev.target as any).version as "ipv4" | "ipv6";
|
||||
const id = source.id;
|
||||
|
||||
if (!value || !this._interface?.[version]) {
|
||||
source.reportValidity();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -718,7 +721,7 @@ export class HassioNetwork extends LitElement {
|
||||
}
|
||||
|
||||
private _handleInputValueChangedWifi(ev: Event): void {
|
||||
const source = ev.target as HaTextField;
|
||||
const source = ev.target as HaInput;
|
||||
const value = source.value;
|
||||
const id = source.id;
|
||||
|
||||
@@ -727,6 +730,7 @@ export class HassioNetwork extends LitElement {
|
||||
!this._wifiConfiguration ||
|
||||
this._wifiConfiguration![id] === value
|
||||
) {
|
||||
source.reportValidity();
|
||||
return;
|
||||
}
|
||||
this._dirty = true;
|
||||
@@ -819,26 +823,25 @@ export class HassioNetwork extends LitElement {
|
||||
--expansion-panel-summary-padding: 0 16px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.address-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--ha-space-2);
|
||||
align-items: center;
|
||||
}
|
||||
.address-row ha-textfield {
|
||||
.address-row ha-input {
|
||||
flex: 1;
|
||||
}
|
||||
.address-row #prefix {
|
||||
flex: none;
|
||||
width: 95px;
|
||||
}
|
||||
ha-icon-button {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.address-row ha-icon-button {
|
||||
--ha-icon-button-size: 36px;
|
||||
margin-top: 16px;
|
||||
margin-top: var(--ha-space-5);
|
||||
}
|
||||
ha-dropdown {
|
||||
display: block;
|
||||
|
||||
@@ -4,14 +4,14 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/ha-dialog-footer";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import "../../../components/ha-switch";
|
||||
import type { HaSwitch } from "../../../components/ha-switch";
|
||||
import "../../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../../components/ha-textfield";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/input/ha-input";
|
||||
import type { HaInput } from "../../../components/input/ha-input";
|
||||
import { createAuthForUser } from "../../../data/auth";
|
||||
import type { User } from "../../../data/user";
|
||||
import {
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
import type { AddUserDialogParams } from "./show-dialog-add-user";
|
||||
import "../../../components/ha-password-field";
|
||||
|
||||
@customElement("dialog-add-user")
|
||||
export class DialogAddUser extends LitElement {
|
||||
@@ -100,7 +99,7 @@ export class DialogAddUser extends LitElement {
|
||||
<div>
|
||||
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
||||
${this._allowChangeName
|
||||
? html`<ha-textfield
|
||||
? html`<ha-input
|
||||
class="name"
|
||||
name="name"
|
||||
.label=${this.hass.localize(
|
||||
@@ -114,9 +113,9 @@ export class DialogAddUser extends LitElement {
|
||||
@input=${this._handleValueChanged}
|
||||
@blur=${this._maybePopulateUsername}
|
||||
autofocus
|
||||
></ha-textfield>`
|
||||
></ha-input>`
|
||||
: ""}
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
class="username"
|
||||
name="username"
|
||||
.label=${this.hass.localize(
|
||||
@@ -127,9 +126,11 @@ export class DialogAddUser extends LitElement {
|
||||
@input=${this._handleValueChanged}
|
||||
.validationMessage=${this.hass.localize("ui.common.error_required")}
|
||||
?autofocus=${!this._allowChangeName}
|
||||
></ha-textfield>
|
||||
></ha-input>
|
||||
|
||||
<ha-password-field
|
||||
<ha-input
|
||||
type="password"
|
||||
password-toggle
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.users.add_user.password"
|
||||
)}
|
||||
@@ -138,9 +139,11 @@ export class DialogAddUser extends LitElement {
|
||||
required
|
||||
@input=${this._handleValueChanged}
|
||||
.validationMessage=${this.hass.localize("ui.common.error_required")}
|
||||
></ha-password-field>
|
||||
></ha-input>
|
||||
|
||||
<ha-password-field
|
||||
<ha-input
|
||||
type="password"
|
||||
password-toggle
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.users.add_user.password_confirm"
|
||||
)}
|
||||
@@ -154,7 +157,7 @@ export class DialogAddUser extends LitElement {
|
||||
.errorMessage=${this.hass.localize(
|
||||
"ui.panel.config.users.add_user.password_not_match"
|
||||
)}
|
||||
></ha-password-field>
|
||||
></ha-input>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
@@ -245,7 +248,7 @@ export class DialogAddUser extends LitElement {
|
||||
|
||||
private _handleValueChanged(ev: ValueChangedEvent<string>): void {
|
||||
this._error = undefined;
|
||||
const target = ev.target as HaTextField;
|
||||
const target = ev.target as HaInput;
|
||||
this[`_${target.name}`] = target.value;
|
||||
}
|
||||
|
||||
@@ -318,11 +321,6 @@ export class DialogAddUser extends LitElement {
|
||||
display: flex;
|
||||
padding: 8px 0;
|
||||
}
|
||||
ha-textfield,
|
||||
ha-password-field {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
ha-md-list-item {
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
|
||||
@@ -22,6 +22,7 @@ export class EnergyOverviewViewStrategy extends ReactiveElement {
|
||||
dense_section_placement: true,
|
||||
max_columns: 3,
|
||||
footer: {
|
||||
column_span: 1.1,
|
||||
card: {
|
||||
type: "energy-date-selection",
|
||||
collection_key: collectionKey,
|
||||
|
||||
@@ -23,6 +23,7 @@ export class EnergyViewStrategy extends ReactiveElement {
|
||||
max_columns: 3,
|
||||
sections: [],
|
||||
footer: {
|
||||
column_span: 1.1,
|
||||
card: {
|
||||
type: "energy-date-selection",
|
||||
collection_key: collectionKey,
|
||||
|
||||
@@ -21,6 +21,7 @@ export class GasViewStrategy extends ReactiveElement {
|
||||
max_columns: 3,
|
||||
sections: [{ type: "grid", cards: [], column_span: 3 }],
|
||||
footer: {
|
||||
column_span: 1.1,
|
||||
card: {
|
||||
type: "energy-date-selection",
|
||||
collection_key: collectionKey,
|
||||
|
||||
@@ -22,6 +22,7 @@ export class WaterViewStrategy extends ReactiveElement {
|
||||
max_columns: 3,
|
||||
sections: [{ type: "grid", cards: [], column_span: 3 }],
|
||||
footer: {
|
||||
column_span: 1.1,
|
||||
card: {
|
||||
type: "energy-date-selection",
|
||||
collection_key: collectionKey,
|
||||
|
||||
@@ -12,7 +12,7 @@ import { endOfDay, isSameDay } from "date-fns";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { PropertyValueMap, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -32,8 +32,8 @@ import "../../../components/ha-relative-time";
|
||||
import "../../../components/ha-select";
|
||||
import "../../../components/ha-sortable";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../../components/ha-textfield";
|
||||
import "../../../components/input/ha-input";
|
||||
import type { HaInput } from "../../../components/input/ha-input";
|
||||
import { isUnavailableState } from "../../../data/entity/entity";
|
||||
import type { TodoItem } from "../../../data/todo";
|
||||
import {
|
||||
@@ -92,6 +92,8 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
|
||||
@state() private _reordering = false;
|
||||
|
||||
@query("ha-input", true) private _input!: HaInput;
|
||||
|
||||
private _unsubItems?: Promise<UnsubscribeFunc>;
|
||||
|
||||
connectedCallback(): void {
|
||||
@@ -284,24 +286,25 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
this._todoListSupportsFeature(TodoListEntityFeature.CREATE_TODO_ITEM)
|
||||
? html`
|
||||
<div class="addRow">
|
||||
<ha-textfield
|
||||
class="addBox"
|
||||
<ha-input
|
||||
.placeholder=${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.add_item"
|
||||
)}
|
||||
@keydown=${this._addKeyPress}
|
||||
.disabled=${unavailable}
|
||||
></ha-textfield>
|
||||
<ha-icon-button
|
||||
class="addButton"
|
||||
.path=${mdiPlus}
|
||||
.title=${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.add_item"
|
||||
)}
|
||||
.disabled=${unavailable}
|
||||
@click=${this._addItem}
|
||||
>
|
||||
</ha-icon-button>
|
||||
<ha-icon-button
|
||||
slot="end"
|
||||
class="addButton"
|
||||
.path=${mdiPlus}
|
||||
.title=${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.add_item"
|
||||
)}
|
||||
.disabled=${unavailable}
|
||||
@click=${this._addItem}
|
||||
>
|
||||
</ha-icon-button>
|
||||
</ha-input>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
@@ -664,8 +667,8 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
});
|
||||
}
|
||||
|
||||
private get _newItem(): HaTextField {
|
||||
return this.shadowRoot!.querySelector(".addBox") as HaTextField;
|
||||
private get _newItem(): HaInput {
|
||||
return this._input;
|
||||
}
|
||||
|
||||
private _addItem(ev): void {
|
||||
@@ -779,10 +782,7 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
.addRow ha-icon-button {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 16px;
|
||||
--ha-icon-button-size: 32px;
|
||||
}
|
||||
|
||||
.addRow,
|
||||
@@ -901,7 +901,7 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
inset-inline-end: initial;
|
||||
}
|
||||
|
||||
ha-textfield {
|
||||
ha-input {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ export class HuiDialogEditViewFooter extends LitElement {
|
||||
<hui-view-footer-settings-editor
|
||||
.hass=${this.hass}
|
||||
.config=${this._config}
|
||||
.maxColumns=${this._params.maxColumns}
|
||||
@config-changed=${this._configChanged}
|
||||
></hui-view-footer-settings-editor>
|
||||
`;
|
||||
@@ -105,7 +106,7 @@ export class HuiDialogEditViewFooter extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${title}
|
||||
width="medium"
|
||||
.width=${this._yamlMode ? "full" : "large"}
|
||||
@closed=${this._dialogClosed}
|
||||
class=${this._yamlMode ? "yaml-mode" : ""}
|
||||
>
|
||||
|
||||
@@ -1,48 +1,53 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import {
|
||||
DEFAULT_FOOTER_MAX_WIDTH_PX,
|
||||
type LovelaceViewFooterConfig,
|
||||
} from "../../../../data/lovelace/config/view";
|
||||
import type { LovelaceViewFooterConfig } from "../../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
|
||||
const SCHEMA = [
|
||||
{
|
||||
name: "max_width",
|
||||
selector: {
|
||||
number: {
|
||||
min: 100,
|
||||
max: 1600,
|
||||
step: 10,
|
||||
unit_of_measurement: "px",
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const satisfies HaFormSchema[];
|
||||
|
||||
@customElement("hui-view-footer-settings-editor")
|
||||
export class HuiViewFooterSettingsEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public config?: LovelaceViewFooterConfig;
|
||||
|
||||
@property({ attribute: false }) public maxColumns = 4;
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(maxColumns: number) =>
|
||||
[
|
||||
{
|
||||
name: "column_span",
|
||||
selector: {
|
||||
number: {
|
||||
min: 1,
|
||||
max: maxColumns,
|
||||
slider_ticks: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const satisfies HaFormSchema[]
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const data = {
|
||||
max_width: this.config?.max_width || DEFAULT_FOOTER_MAX_WIDTH_PX,
|
||||
column_span: this.config?.column_span || 1,
|
||||
};
|
||||
|
||||
const schema = this._schema(this.maxColumns);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${SCHEMA}
|
||||
.schema=${schema}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
@@ -60,10 +65,19 @@ export class HuiViewFooterSettingsEditor extends LitElement {
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
private _computeLabel = (schema: SchemaUnion<typeof SCHEMA>) =>
|
||||
private _computeLabel = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) =>
|
||||
this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_view_footer.settings.${schema.name}`
|
||||
);
|
||||
|
||||
private _computeHelper = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) =>
|
||||
this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_view_footer.settings.${schema.name}_helper`
|
||||
) || "";
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { LovelaceViewFooterConfig } from "../../../../data/lovelace/config/
|
||||
export interface EditViewFooterDialogParams {
|
||||
saveConfig: (config: LovelaceViewFooterConfig) => void;
|
||||
config: LovelaceViewFooterConfig;
|
||||
maxColumns: number;
|
||||
}
|
||||
|
||||
export const showEditViewFooterDialog = (
|
||||
|
||||
@@ -7,10 +7,9 @@ import { styleMap } from "lit/directives/style-map";
|
||||
import "../../../components/ha-ripple";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import {
|
||||
DEFAULT_FOOTER_MAX_WIDTH_PX,
|
||||
type LovelaceViewConfig,
|
||||
type LovelaceViewFooterConfig,
|
||||
import type {
|
||||
LovelaceViewConfig,
|
||||
LovelaceViewFooterConfig,
|
||||
} from "../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { HuiCard } from "../cards/hui-card";
|
||||
@@ -20,6 +19,7 @@ import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog"
|
||||
import { replaceView } from "../editor/config-util";
|
||||
import { showEditViewFooterDialog } from "../editor/view-footer/show-edit-view-footer-dialog";
|
||||
import type { Lovelace } from "../types";
|
||||
import { DEFAULT_MAX_COLUMNS } from "./hui-sections-view";
|
||||
|
||||
@customElement("hui-view-footer")
|
||||
export class HuiViewFooter extends LitElement {
|
||||
@@ -97,8 +97,13 @@ export class HuiViewFooter extends LitElement {
|
||||
}
|
||||
|
||||
private _configure() {
|
||||
const viewConfig = this.lovelace.config.views[
|
||||
this.viewIndex
|
||||
] as LovelaceViewConfig;
|
||||
|
||||
showEditViewFooterDialog(this, {
|
||||
config: this.config || {},
|
||||
maxColumns: viewConfig.max_columns || DEFAULT_MAX_COLUMNS,
|
||||
saveConfig: (newConfig: LovelaceViewFooterConfig) => {
|
||||
this._saveFooterConfig(newConfig);
|
||||
},
|
||||
@@ -175,11 +180,13 @@ export class HuiViewFooter extends LitElement {
|
||||
|
||||
if (!card && !editMode) return nothing;
|
||||
|
||||
const columnSpan = this.config?.column_span || 1;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class=${classMap({ wrapper: true, "edit-mode": editMode })}
|
||||
style=${styleMap({
|
||||
"--footer-max-width": `${this.config?.max_width || DEFAULT_FOOTER_MAX_WIDTH_PX}px`,
|
||||
"--footer-column-span": String(columnSpan),
|
||||
})}
|
||||
>
|
||||
${editMode
|
||||
@@ -221,18 +228,23 @@ export class HuiViewFooter extends LitElement {
|
||||
|
||||
:host([sticky]) {
|
||||
position: sticky;
|
||||
bottom: var(--row-gap);
|
||||
bottom: 0;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
padding: var(--ha-space-2) 0;
|
||||
padding-bottom: calc(
|
||||
max(var(--ha-space-2), var(--safe-area-inset-bottom, 0px))
|
||||
padding: var(--ha-space-4) 0;
|
||||
padding-bottom: max(
|
||||
var(--ha-space-4),
|
||||
var(--safe-area-inset-bottom, 0px)
|
||||
);
|
||||
box-sizing: content-box;
|
||||
margin: 0 auto;
|
||||
max-width: var(--footer-max-width, 600px);
|
||||
max-width: calc(
|
||||
var(--footer-column-span, 1) / var(--column-count, 1) * 100% +
|
||||
(var(--footer-column-span, 1) - var(--column-count, 1)) /
|
||||
var(--column-count, 1) * var(--column-gap, 32px)
|
||||
);
|
||||
}
|
||||
|
||||
.wrapper:not(.edit-mode) {
|
||||
@@ -303,7 +315,7 @@ export class HuiViewFooter extends LitElement {
|
||||
border-bottom-left-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
background: var(--secondary-background-color);
|
||||
--ha-icon-button-size: 36px;
|
||||
--mdc-icon-button-size: 36px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../components/ha-card";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-spinner";
|
||||
import "../../components/ha-textfield";
|
||||
import "../../components/ha-password-field";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../../components/ha-alert";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-card";
|
||||
import "../../components/ha-spinner";
|
||||
import "../../components/input/ha-input";
|
||||
import { changePassword, deleteAllRefreshTokens } from "../../data/auth";
|
||||
import type { RefreshToken } from "../../data/refresh_token";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../dialogs/generic/show-dialog-box";
|
||||
import type { RefreshToken } from "../../data/refresh_token";
|
||||
import { changePassword, deleteAllRefreshTokens } from "../../data/auth";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
@customElement("ha-change-password-card")
|
||||
class HaChangePasswordCard extends LitElement {
|
||||
@@ -47,8 +46,10 @@ class HaChangePasswordCard extends LitElement {
|
||||
? html`<ha-alert alert-type="success">${this._statusMsg}</ha-alert>`
|
||||
: ""}
|
||||
|
||||
<ha-password-field
|
||||
<ha-input
|
||||
id="currentPassword"
|
||||
type="password"
|
||||
password-toggle
|
||||
name="currentPassword"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.profile.change_password.current_password"
|
||||
@@ -58,10 +59,12 @@ class HaChangePasswordCard extends LitElement {
|
||||
@input=${this._currentPasswordChanged}
|
||||
@change=${this._currentPasswordChanged}
|
||||
required
|
||||
></ha-password-field>
|
||||
></ha-input>
|
||||
|
||||
${this._currentPassword
|
||||
? html`<ha-password-field
|
||||
? html`<ha-input
|
||||
type="password"
|
||||
password-toggle
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.profile.change_password.new_password"
|
||||
)}
|
||||
@@ -72,8 +75,10 @@ class HaChangePasswordCard extends LitElement {
|
||||
@change=${this._newPasswordChanged}
|
||||
required
|
||||
autoValidate
|
||||
></ha-password-field>
|
||||
<ha-password-field
|
||||
></ha-input>
|
||||
<ha-input
|
||||
type="password"
|
||||
password-toggle
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.profile.change_password.confirm_new_password"
|
||||
)}
|
||||
@@ -84,7 +89,7 @@ class HaChangePasswordCard extends LitElement {
|
||||
@change=${this._newPasswordConfirmChanged}
|
||||
required
|
||||
autoValidate
|
||||
></ha-password-field>`
|
||||
></ha-input>`
|
||||
: ""}
|
||||
</div>
|
||||
|
||||
@@ -195,10 +200,6 @@ class HaChangePasswordCard extends LitElement {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
ha-textfield {
|
||||
margin-top: 8px;
|
||||
display: block;
|
||||
}
|
||||
#currentPassword {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@@ -64,5 +64,10 @@ export const waColorStyles = css`
|
||||
|
||||
--wa-focus-ring-color: var(--ha-color-neutral-60);
|
||||
--wa-shadow-l: 4px 8px 12px 0 rgba(0, 0, 0, 0.3);
|
||||
|
||||
--wa-form-control-background-color: var(--wa-color-surface-raised);
|
||||
--wa-form-control-border-color: var(--ha-color-border-neutral-quiet);
|
||||
--wa-form-control-value-color: var(--primary-text-color);
|
||||
--wa-form-control-placeholder-color: var(--ha-color-text-secondary);
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -14,10 +14,8 @@ export const waMainStyles = css`
|
||||
--wa-space-l: var(--ha-space-6);
|
||||
--wa-space-xl: var(--ha-space-8);
|
||||
|
||||
--wa-form-control-padding-block: 0.75em;
|
||||
--wa-form-control-value-line-height: var(--ha-line-height-condensed);
|
||||
|
||||
--wa-font-weight-action: var(--ha-font-weight-medium);
|
||||
--wa-font-weight-body: var(--ha-font-weight-normal);
|
||||
--wa-transition-normal: 150ms;
|
||||
--wa-transition-fast: 75ms;
|
||||
--wa-transition-easing: ease;
|
||||
@@ -29,13 +27,25 @@ export const waMainStyles = css`
|
||||
--wa-border-radius-s: var(--ha-border-radius-sm);
|
||||
--wa-border-radius-m: var(--ha-border-radius-md);
|
||||
--wa-border-radius-l: var(--ha-border-radius-lg);
|
||||
--wa-border-radius-pill: var(--ha-border-radius-pill);
|
||||
|
||||
--wa-line-height-condensed: var(--ha-line-height-condensed);
|
||||
|
||||
--wa-font-size-s: var(--ha-font-size-s);
|
||||
--wa-font-size-m: var(--ha-font-size-m);
|
||||
--wa-font-size-l: var(--ha-font-size-l);
|
||||
--wa-shadow-s: var(--ha-box-shadow-s);
|
||||
--wa-shadow-m: var(--ha-box-shadow-m);
|
||||
--wa-shadow-l: var(--ha-box-shadow-l);
|
||||
|
||||
--wa-form-control-padding-block: 0.75em;
|
||||
--wa-form-control-value-line-height: var(--wa-line-height-condensed);
|
||||
--wa-form-control-value-font-weight: var(--wa-font-weight-body);
|
||||
--wa-form-control-border-radius: var(--wa-border-radius-l);
|
||||
--wa-form-control-border-style: var(--wa-border-style);
|
||||
--wa-form-control-border-width: var(--wa-border-width-s);
|
||||
--wa-form-control-height: 40px;
|
||||
--wa-form-control-padding-inline: var(--ha-space-3);
|
||||
}
|
||||
|
||||
${scrollLockStyles}
|
||||
|
||||
@@ -709,6 +709,9 @@
|
||||
"entity-state-content-picker": {
|
||||
"add": "Add",
|
||||
"custom_attribute": "Custom attribute"
|
||||
},
|
||||
"entities-picker": {
|
||||
"no_entities": "No entities"
|
||||
}
|
||||
},
|
||||
"target-picker": {
|
||||
@@ -4195,21 +4198,6 @@
|
||||
"info": "INFO",
|
||||
"debug": "DEBUG"
|
||||
},
|
||||
"error_type": {
|
||||
"auth": "Authentication",
|
||||
"connection": "Connection",
|
||||
"invalid_response": "Invalid response",
|
||||
"rate_limit": "Rate limit",
|
||||
"server": "Server",
|
||||
"slow_setup": "Slow setup",
|
||||
"timeout": "Timeout",
|
||||
"ssl": "TLS/SSL",
|
||||
"statistics": "Statistics",
|
||||
"dns": "DNS"
|
||||
},
|
||||
"level_filter": "Level",
|
||||
"classification": "Classification",
|
||||
"other": "Other",
|
||||
"custom_integration": "custom integration",
|
||||
"error_from_custom_integration": "This error originated from a custom integration.",
|
||||
"show_full_logs": "Show raw logs",
|
||||
@@ -4234,7 +4222,6 @@
|
||||
"integration": "[%key:ui::panel::config::integrations::integration%]",
|
||||
"documentation": "documentation",
|
||||
"issues": "issues",
|
||||
"error_type": "Error type",
|
||||
"first_occurred": "First occurred",
|
||||
"number_of_occurrences": "{count} {count, plural,\n one {occurrence}\n other {occurrences}\n}",
|
||||
"last_logged": "Last logged"
|
||||
@@ -8512,7 +8499,8 @@
|
||||
"edit_yaml": "[%key:ui::panel::lovelace::editor::edit_view::edit_yaml%]",
|
||||
"saving_failed": "[%key:ui::panel::lovelace::editor::edit_view::saving_failed%]",
|
||||
"settings": {
|
||||
"max_width": "Max width"
|
||||
"column_span": "Width",
|
||||
"column_span_helper": "[%key:ui::panel::lovelace::editor::edit_section::settings::column_span_helper%]"
|
||||
}
|
||||
},
|
||||
"edit_badges": {
|
||||
|
||||
35
yarn.lock
35
yarn.lock
@@ -6735,10 +6735,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"commander@npm:^14.0.3":
|
||||
version: 14.0.3
|
||||
resolution: "commander@npm:14.0.3"
|
||||
checksum: 10/dfa9ebe2a433d277de5cb0252d23b10a543d245d892db858d23b516336a835c50fd4f52bee4cd13c705cc8acb6f03dc632c73dd806f7d06d3353eb09953dd17a
|
||||
"commander@npm:^14.0.2":
|
||||
version: 14.0.2
|
||||
resolution: "commander@npm:14.0.2"
|
||||
checksum: 10/2d202db5e5f9bb770112a3c1579b893d17ac6f6d932183077308bdd96d0f87f0bbe6a68b5b9ed2cf3b2514be6bb7de637480703c0e2db9741ee1b383237deb26
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9312,7 +9312,7 @@ __metadata:
|
||||
leaflet: "npm:1.9.4"
|
||||
leaflet-draw: "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
|
||||
leaflet.markercluster: "npm:1.5.3"
|
||||
lint-staged: "npm:16.3.0"
|
||||
lint-staged: "npm:16.2.7"
|
||||
lit: "npm:3.3.2"
|
||||
lit-analyzer: "npm:2.0.3"
|
||||
lit-html: "npm:3.3.2"
|
||||
@@ -10629,20 +10629,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lint-staged@npm:16.3.0":
|
||||
version: 16.3.0
|
||||
resolution: "lint-staged@npm:16.3.0"
|
||||
"lint-staged@npm:16.2.7":
|
||||
version: 16.2.7
|
||||
resolution: "lint-staged@npm:16.2.7"
|
||||
dependencies:
|
||||
commander: "npm:^14.0.3"
|
||||
commander: "npm:^14.0.2"
|
||||
listr2: "npm:^9.0.5"
|
||||
micromatch: "npm:^4.0.8"
|
||||
nano-spawn: "npm:^2.0.0"
|
||||
pidtree: "npm:^0.6.0"
|
||||
string-argv: "npm:^0.3.2"
|
||||
tinyexec: "npm:^1.0.2"
|
||||
yaml: "npm:^2.8.2"
|
||||
yaml: "npm:^2.8.1"
|
||||
bin:
|
||||
lint-staged: bin/lint-staged.js
|
||||
checksum: 10/0ae6d4bbef000b06f36af8c16e825aa653c4ccca2c2f664a596f2f3a65c4b89f876a822c412837596651e0b54aa0ac926b697fb65d3ff959d2d89a13e506ec00
|
||||
checksum: 10/c1fd7685300800ea6d3f073cb450f9e3d2a83966e7c6785ea9608a08e77e1e8e5f1958f77b98ccd7d423daa53bb36016d6fd96a98d22234d0f7f56d7b3f360f2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -11936,6 +11936,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pidtree@npm:^0.6.0":
|
||||
version: 0.6.0
|
||||
resolution: "pidtree@npm:0.6.0"
|
||||
bin:
|
||||
pidtree: bin/pidtree.js
|
||||
checksum: 10/ea67fb3159e170fd069020e0108ba7712df9f0fd13c8db9b2286762856ddce414fb33932e08df4bfe36e91fe860b51852aee49a6f56eb4714b69634343add5df
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pinst@npm:3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "pinst@npm:3.0.0"
|
||||
@@ -15595,7 +15604,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yaml@npm:^2.8.2":
|
||||
"yaml@npm:^2.8.1":
|
||||
version: 2.8.2
|
||||
resolution: "yaml@npm:2.8.2"
|
||||
bin:
|
||||
|
||||
Reference in New Issue
Block a user