331 files

This commit is contained in:
Home Assistant Version Control
2026-10-04 05:26:42 +00:00
parent 22ba8b4648
commit c4d601bfba
331 changed files with 10227 additions and 50159 deletions
@@ -444,7 +444,9 @@ export class MaintenanceBatteryFleetSection extends LitElement {
: b.no_sensor
? html`<span class="bf-offline bf-nosensor">${t("battery_fleet_no_sensor", L)}</span>`
: nothing;
const type = html`<span class="bf-type">${b.quantity}× ${b.battery_type}</span>`;
// The full type as a tooltip: a long one ends in an ellipsis (D#162).
const typeText = `${b.quantity}× ${b.battery_type}`;
const type = html`<span class="bf-type" title=${typeText}>${typeText}</span>`;
const writer = this._canWrite;
const showMark = writer && (o.mark === "always" || b.no_sensor || b.can_mark_replaced);
return html`
@@ -659,7 +661,10 @@ export class MaintenanceBatteryFleetSection extends LitElement {
}
static styles = css`
/* The query container of the row layout (see "In a narrow list" below):
* the list's own width, not the screen's. */
.bf-card {
container: bf-fleet / inline-size;
background: var(--card-background-color, #fff);
border: 1px solid var(--divider-color);
border-radius: 10px;
@@ -817,9 +822,16 @@ export class MaintenanceBatteryFleetSection extends LitElement {
font-size: 12px;
font-style: italic;
}
/* Every row shares the list's columns, so ONE long type ("1× BATTERY
* PACK") widened the type column of all of them (D#162): it stops at
* 12em and ends in an ellipsis, the full text is the tooltip. */
.bf-type {
color: var(--secondary-text-color);
font-size: 13px;
max-width: 12em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.bf-recharge {
color: var(--secondary-text-color);
@@ -835,18 +847,37 @@ export class MaintenanceBatteryFleetSection extends LitElement {
flex: 0 0 auto;
cursor: help;
}
/* On phones the row cannot fit name + chips + curve + bar + date in ONE
* line: the decorations yield (the percentage still carries the number)
* and the row wraps to two lines - the name spans the full width, the
* status chip moves under it (left, into the name column) and the rest
* keeps its pinned subgrid column, so type / percentage / date / eye
* stay aligned across rows. Without this the fixed max-content columns
* overflowed 400 px and the chips overlapped the wrapped names. */
@media (max-width: 640px) {
/* In a narrow list the row cannot fit name + chips + curve + bar + date
* in ONE line: the decorations yield (the percentage still carries the
* number) and the row wraps to two lines - the name spans the full
* width, the status chip moves under it (left, into the name column)
* and the rest keeps its pinned subgrid column, so type / percentage /
* date / eye stay aligned across rows. Without this the fixed
* max-content columns overflowed 400 px and the chips overlapped the
* wrapped names.
*
* The LIST's width decides, not the screen's (D#162): the Lovelace card
* sits in a dashboard column of 400-500 px on a wide screen, kept the
* one-line layout there and cut off the badge, the date and the hide
* button. 680 px of list is where the one-line rows stop fitting;
* desktop task pages (700 px and more) keep them. */
@container bf-fleet (max-width: 680px) {
.bf-spark,
.bf-bar {
display: none;
}
/* No fixed gaps: a column a phone leaves empty (status chip moved,
* curve and bar hidden) cost its 8 px gap all the same, and together
* with one long type the rows ran 25 px past a 402 px phone. Each
* occupied column brings its own 6 px instead, and the type column
* may shrink - its text ends in an ellipsis. */
.bf-rows {
column-gap: 0;
grid-template-columns: minmax(0, 1fr) max-content minmax(0, max-content) repeat(7, max-content);
}
.bf-row > :not(.bf-dev) {
margin-inline-start: 6px;
}
.bf-row {
row-gap: 2px;
}
@@ -864,12 +895,10 @@ export class MaintenanceBatteryFleetSection extends LitElement {
grid-row: 1;
justify-self: end;
}
/* The column shrinks, the ellipsis comes from the base rule (a cap in
* vw measured the screen, not the list). */
.bf-type {
grid-row: 2;
max-width: 44vw;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.bf-recharge,
.bf-level,
@@ -14,6 +14,7 @@ import { REQUIRED_COMPLETION_LABELS } from "./required-completion-labels";
import { PhotoUploadController } from "../helpers/photo-upload-controller";
import { parseDurationMinutes } from "../helpers/duration";
import "./ms-date-field";
import "./ms-cost-input";
import "./ms-photo-picker";
import { photoPickerStyles } from "./ms-photo-picker";
@@ -173,10 +174,14 @@ export class MaintenanceCompleteDialog extends LitElement {
entry_id: this.entryId,
task_id: this.taskId,
};
if (this._notes) data.notes = this._notes;
// Trimmed like the required check reads it: a text area easily keeps a
// trailing line break, which the history would show as an empty line.
const notes = this._notes.trim();
if (notes) data.notes = notes;
if (this._cost) {
// #200: a credit is negative; the server holds the bounds.
const cost = parseFloat(this._cost);
if (!isNaN(cost) && cost >= 0) data.cost = cost;
if (!isNaN(cost)) data.cost = cost;
}
// Whole minutes — the server coerces to int (helpers/duration).
const dur = parseDurationMinutes(this._duration);
@@ -309,9 +314,15 @@ export class MaintenanceCompleteDialog extends LitElement {
return this.requiredFields.filter((f) => !filled[f]);
}
/** Does the task demand this detail? Its label then drops "(optional)" —
* "Notes (optional) *" contradicted itself. */
private _demands(field: string): boolean {
return this.requiredFields.includes(field);
}
/** Marker appended to a required field's label. */
private _req(field: string) {
return this.requiredFields.includes(field) ? html`<span class="req-mark" aria-hidden="true">*</span>` : nothing;
return this._demands(field) ? html`<span class="req-mark" aria-hidden="true">*</span>` : nothing;
}
/** A buy task's price: restock qty (packages) × the part's price per
@@ -485,20 +496,22 @@ export class MaintenanceCompleteDialog extends LitElement {
only sees the title + Cancel/Complete buttons — the original
bug report. Native inputs always render. -->
<label class="field">
<span class="field-label">${t("notes_optional", L)}${this._req("notes")}</span>
<input type="text" class="field-input"
<span class="field-label">${this._demands("notes") ? t("notes_label", L) : t("notes_optional", L)}${this._req("notes")}</span>
<!-- Several lines (#202): what was done, one line each. The
history and the printouts keep the line breaks; they show
notes as plain text, so no Markdown hint here. -->
<textarea class="field-input notes-input" rows="3"
.value=${this._notes}
@input=${(e: Event) => (this._notes = (e.target as HTMLInputElement).value)} />
@input=${(e: Event) => (this._notes = (e.target as HTMLTextAreaElement).value)}></textarea>
</label>
<label class="field">
<span class="field-label">${t("cost_optional", L)}${this._req("cost")}</span>
<input type="number" step="0.01" min="0" class="field-input"
.value=${this._cost}
@input=${(e: Event) => (this._cost = (e.target as HTMLInputElement).value)} />
<div class="field">
<span class="field-label">${this._demands("cost") ? t("cost", L) : t("cost_optional", L)}${this._req("cost")}</span>
<ms-cost-input .lang=${L} .value=${this._cost}
@value-changed=${(e: CustomEvent<{ value: string }>) => (this._cost = e.detail.value)}></ms-cost-input>
${this._renderCostSuggestion(L)}
</label>
</div>
<label class="field">
<span class="field-label">${t("duration_minutes", L)}${this._req("duration")}</span>
<span class="field-label">${this._demands("duration") ? t("quick_complete_defaults_duration", L) : t("duration_minutes", L)}${this._req("duration")}</span>
<input type="number" step="1" min="0" inputmode="numeric" class="field-input"
.value=${this._duration}
@input=${(e: Event) => (this._duration = (e.target as HTMLInputElement).value)} />
@@ -519,7 +532,7 @@ export class MaintenanceCompleteDialog extends LitElement {
</button>`}
</div>
<div class="field">
<span class="field-label">${t("completion_photos_optional", L)}${this._req("photo")}</span>
<span class="field-label">${this._demands("photo") ? t("completion_photos", L) : t("completion_photos_optional", L)}${this._req("photo")}</span>
${this._photos.photos.length > 0
? html`<div class="photo-strip">
${this._photos.photos.map((p) => html`
@@ -582,6 +595,7 @@ export class MaintenanceCompleteDialog extends LitElement {
margin-left: 2px;
font-weight: 600;
}
.notes-input { resize: vertical; line-height: 1.4; }
/* #104: one-click cost suggestion from parts — quiet link-style chip. */
.cost-note {
font-size: 12px;
@@ -106,8 +106,10 @@ export class MaintenanceGroupDialog extends LitElement {
const L = this._lang;
const title = this._groupId ? t("edit_group", L) : t("new_group", L);
// Our own title element: Home Assistant's dialog draws no `heading`.
return html`
<ha-dialog open @closed=${this._close} heading="${title}">
<ha-dialog open @closed=${this._close}>
<div class="dialog-title">${title}</div>
<div class="content">
${this._error ? html`<div class="error">${this._error}</div>` : nothing}
<ms-textfield
@@ -169,6 +171,7 @@ export class MaintenanceGroupDialog extends LitElement {
}
static styles = css`
.dialog-title { font-size: 18px; font-weight: 500; padding-bottom: 12px; }
.content {
display: flex;
flex-direction: column;
@@ -18,6 +18,7 @@ import { PhotoUploadController } from "../helpers/photo-upload-controller";
import { parseDurationMinutes } from "../helpers/duration";
import { confirmAction } from "../helpers/confirm";
import "./ms-date-field";
import "./ms-cost-input";
import "./history-photo";
import "./ms-photo-picker";
import { photoPickerStyles } from "./ms-photo-picker";
@@ -404,15 +405,14 @@ export class MaintenanceHistoryEditDialog extends LitElement {
.value=${d.notes ?? ""}></textarea>
</label>
<div class="row">
<label>
<div class="cost-field">
<span>${t("cost", L)}</span>
<input type="number" min="0" step="0.01"
.value=${d.cost != null ? String(d.cost) : ""}
@input=${(e: Event) => {
const v = (e.target as HTMLInputElement).value;
<ms-cost-input .lang=${L} .value=${d.cost != null ? String(d.cost) : ""}
@value-changed=${(e: CustomEvent<{ value: string }>) => {
const v = e.detail.value;
this._set("cost", v ? Number(v) : null);
}} />
</label>
}}></ms-cost-input>
</div>
<label>
<span>${t("duration", L)}</span>
<input type="number" min="0" step="1" inputmode="numeric"
@@ -531,6 +531,8 @@ export class MaintenanceHistoryEditDialog extends LitElement {
}
label { display: flex; flex-direction: column; gap: 4px; font-size: 13px; }
label span { color: var(--secondary-text-color); }
.cost-field { display: flex; flex-direction: column; gap: 4px; font-size: 13px; min-width: 0; }
.cost-field > span { color: var(--secondary-text-color); }
.row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
input, textarea {
padding: 8px; font-size: 14px;
@@ -100,8 +100,10 @@ export class SeasonalOverridesDialog extends LitElement {
render() {
if (!this._open) return html``;
const L = this._lang;
// Our own title element: Home Assistant's dialog draws no `heading`.
return html`
<ha-dialog open @closed=${this._close} heading="${t("seasonal_overrides_title", L)}">
<ha-dialog open @closed=${this._close}>
<div class="dialog-title">${t("seasonal_overrides_title", L)}</div>
<div class="content">
<p class="hint">${t("seasonal_overrides_hint", L)}</p>
${this._error ? html`<div class="error">${this._error}</div>` : nothing}
@@ -138,6 +140,7 @@ export class SeasonalOverridesDialog extends LitElement {
}
static styles = css`
.dialog-title { font-size: 18px; font-weight: 500; padding-bottom: 12px; }
.content {
min-width: 320px;
max-width: 480px;
@@ -1240,6 +1240,14 @@ export class MaintenanceSettingsView extends LitElement {
<span class="setting-label">${t("settings_max_per_day", L)}</span>
${this._intSetting("max_notifications_per_day", n.max_per_day)}
</label>
<!-- Every Snooze uses it — the task menu, a card, voice, the phone's
button — so it is not tied to the Snooze action button (D#193:
it only showed while that button was on). -->
<label class="setting-row snooze-duration">
<span class="setting-label">${t("settings_snooze_hours", L)}</span>
${this._intSetting("snooze_duration_hours", a.snooze_duration_hours)}
</label>
<div class="setting-hint">${t("settings_snooze_hours_hint", L)}</div>
<label class="setting-row">
<span class="setting-label">${t("settings_bundling", L)}</span>
@@ -1336,12 +1344,6 @@ export class MaintenanceSettingsView extends LitElement {
<input type="checkbox" .checked=${a.snooze_enabled}
@change=${(e: Event) => this._updateSetting("action_snooze_enabled", (e.target as HTMLInputElement).checked)} />
</label>
${a.snooze_enabled ? html`
<label class="setting-row sub-row">
<span class="setting-desc">${t("settings_snooze_hours", L)}</span>
${this._intSetting("snooze_duration_hours", a.snooze_duration_hours)}
</label>
` : nothing}
<label class="setting-row">
<span class="setting-label">${t("settings_weekly_digest", L)}</span>
<input type="checkbox" .checked=${a.weekly_digest_enabled}
@@ -29,6 +29,7 @@ import { REQUIRED_COMPLETION_KEYS, REQUIRED_COMPLETION_LABELS } from "./required
import "./ms-textfield";
import { defaultNotifyIcon } from "../helpers/notify-icons";
import "./ms-date-field";
import "./ms-cost-input";
const MAINTENANCE_TYPE_KEYS = ["cleaning", "inspection", "replacement", "calibration", "service", "reading", "custom"];
const PRIORITY_KEYS = ["low", "normal", "high"];
@@ -1034,12 +1035,11 @@ export class MaintenanceTaskDialog extends LitElement {
.value=${this._qcNotes}
@input=${(e: Event) => { this._qcNotes = (e.target as HTMLInputElement).value; }}
></ms-textfield>
<ms-textfield
label="${t("quick_complete_defaults_cost", L)}"
type="number" min="0" step="0.01"
.value=${this._qcCost}
@input=${(e: Event) => { this._qcCost = (e.target as HTMLInputElement).value; }}
></ms-textfield>
<div class="qc-cost">
<span class="qc-cost-label">${t("quick_complete_defaults_cost", L)}</span>
<ms-cost-input .lang=${L} .value=${this._qcCost}
@value-changed=${(e: CustomEvent<{ value: string }>) => { this._qcCost = e.detail.value; }}></ms-cost-input>
</div>
<ms-textfield
label="${t("quick_complete_defaults_duration", L)}"
type="number" min="0" step="1"
@@ -1788,7 +1788,7 @@ export class MaintenanceTaskDialog extends LitElement {
const qcd: Record<string, unknown> = {};
if (this._qcNotes.trim()) qcd.notes = this._qcNotes.trim();
const cost = parseFloat(this._qcCost);
if (!isNaN(cost) && cost >= 0) qcd.cost = cost;
if (!isNaN(cost)) qcd.cost = cost;
const dur = parseDurationMinutes(this._qcDuration);
if (dur !== null) qcd.duration = dur;
if (this._qcFeedback) qcd.feedback = this._qcFeedback;
@@ -3588,6 +3588,8 @@ export class MaintenanceTaskDialog extends LitElement {
font-weight: 500;
color: var(--secondary-text-color);
}
.qc-cost { display: flex; flex-direction: column; gap: 4px; margin: 8px 0; }
.qc-cost-label { font-size: 12px; color: var(--secondary-text-color); }
.field-help {
font-size: 12px;
color: var(--secondary-text-color);