393 files
This commit is contained in:
+163
-157
@@ -14,15 +14,21 @@
|
||||
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { sharedStyles, t, STATUS_COLORS, formatDate, formatInterval, formatRecurrence, formatCost, formatDuration, currencySymbolOf, langOf, syncCurrencyDecimals} from "../styles";
|
||||
import { describeWsError } from "../ws-errors";
|
||||
import { sharedStyles, t, formatDate, formatInterval, formatRecurrence, formatCost, formatDuration, currencySymbolOf, langOf, syncCurrencyDecimals} from "../styles";
|
||||
import { runWs } from "../helpers/ws-run";
|
||||
import { focusModalShell, modalShellStyles, renderModalShell } from "../helpers/modal-shell";
|
||||
import { isoDateLocal } from "../helpers/calendar-bucket";
|
||||
import { buildCompleteDialogArgs } from "../helpers/complete-dialog-args";
|
||||
import { phaseLabel } from "../helpers/phases";
|
||||
import { buildHistoryEntryDraft } from "../helpers/history-draft";
|
||||
import { readingSlotDelta } from "../helpers/reading-slots";
|
||||
import { taskRef } from "../helpers/reference";
|
||||
import { renderHistoryEntry } from "../renderers/history";
|
||||
import { newestFirst, renderHistoryEntry } from "../renderers/history";
|
||||
import { renderStatusBadge, statusColor } from "../renderers/status";
|
||||
import { fetchSettingsOnce } from "../helpers/settings-cache";
|
||||
import { canWrite, NO_DELEGATION, type WriteAccess } from "../helpers/permissions";
|
||||
import { confirmAction } from "../helpers/confirm";
|
||||
import { ToastTimer } from "../helpers/toast";
|
||||
import { renderWeibullSection } from "../renderers/weibull";
|
||||
import { renderPredictionSection } from "../renderers/prediction";
|
||||
import { renderRecommendationBars } from "../renderers/recommendation";
|
||||
@@ -69,7 +75,10 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
completion_actions: false,
|
||||
};
|
||||
@state() private _toast = "";
|
||||
private _featuresLoaded = false;
|
||||
private readonly _toastTimer = new ToastTimer();
|
||||
/** Operator-write delegation (helpers/permissions): Edit / Archive /
|
||||
* Delete follow canWrite() like the panel, not `is_admin` alone. */
|
||||
@state() private _access: WriteAccess = NO_DELEGATION;
|
||||
/** Currency symbol for the complete dialog's cost suggestion — read off
|
||||
* the same settings response as the feature flags. */
|
||||
private _currencySymbol = "";
|
||||
@@ -81,6 +90,9 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
/** Open the dialog. Loads fresh data from /object via WS so dialog stays in
|
||||
* sync even if the underlying card has stale data. */
|
||||
public async openFor(entryId: string, taskId: string): Promise<void> {
|
||||
// Reopened for ANOTHER task while open: never show the previous one
|
||||
// under the new ids until the fresh load lands.
|
||||
if (entryId !== this._entryId || taskId !== this._taskId) this._task = null;
|
||||
this._entryId = entryId;
|
||||
this._taskId = taskId;
|
||||
this._error = "";
|
||||
@@ -97,60 +109,70 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
|
||||
/** Pull the active feature flags so adaptive sections only render when
|
||||
* Adaptive / Seasonal / Environmental are actually enabled (matches the
|
||||
* panel's behaviour). Cached after first load. */
|
||||
* panel's behaviour), plus the write delegation and currency. Through
|
||||
* the page-wide settings cache (one fetch per page, invalidated by
|
||||
* global/update) — the private fetch here bypassed it and never
|
||||
* re-read after a settings change (DRY audit 2026-09-26). A failed
|
||||
* fetch serves the all-off fallback and asks again next open. */
|
||||
private async _loadFeatures(): Promise<void> {
|
||||
if (this._featuresLoaded) return;
|
||||
try {
|
||||
const r = await this.hass.connection.sendMessagePromise<{
|
||||
features?: Partial<AdvancedFeatures>;
|
||||
budget?: { currency_symbol?: string; currency_decimals?: number };
|
||||
}>({ type: "maintenance_supporter/settings" });
|
||||
if (r?.features) {
|
||||
this._features = { ...this._features, ...r.features };
|
||||
}
|
||||
this._currencySymbol = currencySymbolOf(r?.budget);
|
||||
syncCurrencyDecimals(r?.budget);
|
||||
this._featuresLoaded = true;
|
||||
} catch {
|
||||
// Settings endpoint unavailable — leave defaults (all false), the
|
||||
// adaptive panel will then stay hidden.
|
||||
}
|
||||
const settings = await fetchSettingsOnce(this.hass);
|
||||
this._features = { ...this._features, ...settings.features };
|
||||
this._access = settings.access;
|
||||
this._currencySymbol = currencySymbolOf(settings.budget ?? undefined);
|
||||
syncCurrencyDecimals(settings.budget ?? undefined);
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this._loadSeq++; // a load still in flight belongs to the closed dialog
|
||||
this._open = false;
|
||||
this._task = null;
|
||||
this._error = "";
|
||||
this._toastTimer.clear();
|
||||
this._toast = "";
|
||||
}
|
||||
|
||||
private _showToast(msg: string, ms?: number): void {
|
||||
this._toast = msg;
|
||||
this._toastTimer.schedule(() => { this._toast = ""; }, ms);
|
||||
}
|
||||
|
||||
/** Only the newest `_loadTask` may land: the answer for a task the dialog
|
||||
* was opened for BEFORE (or after it closed) looked up the CURRENT task
|
||||
* id in the wrong object, found nothing, and left the dialog stuck on
|
||||
* "Loading" (bug audit 2026-09-26 #2). */
|
||||
private _loadSeq = 0;
|
||||
|
||||
private async _loadTask(): Promise<void> {
|
||||
if (!this._entryId || !this._taskId) return;
|
||||
try {
|
||||
const r = await this.hass.connection.sendMessagePromise<MaintenanceObjectFull>({
|
||||
type: "maintenance_supporter/object",
|
||||
entry_id: this._entryId,
|
||||
});
|
||||
this._objectName = r.object?.name || "";
|
||||
const found = (r.tasks || []).find((t) => t.id === this._taskId);
|
||||
this._task = found ?? null;
|
||||
this._taskRef = taskRef(r.object, found);
|
||||
} catch (e) {
|
||||
this._error = describeWsError(e, this._lang);
|
||||
const entryId = this._entryId;
|
||||
const taskId = this._taskId;
|
||||
if (!entryId || !taskId) return;
|
||||
const seq = ++this._loadSeq;
|
||||
let failure = "";
|
||||
const r = await runWs<MaintenanceObjectFull>(
|
||||
this,
|
||||
{ type: "maintenance_supporter/object", entry_id: entryId },
|
||||
{ onError: (m) => { failure = m; } },
|
||||
);
|
||||
if (seq !== this._loadSeq) return;
|
||||
if (r === undefined) {
|
||||
this._error = failure;
|
||||
return;
|
||||
}
|
||||
this._objectName = r?.object?.name || "";
|
||||
const found = (r?.tasks || []).find((t) => t.id === taskId);
|
||||
this._task = found ?? null;
|
||||
this._taskRef = taskRef(r?.object, found);
|
||||
// A task deleted meanwhile: say so instead of "Loading" forever.
|
||||
if (!found) this._error = t("ws_err_not_found", this._lang);
|
||||
}
|
||||
|
||||
private async _runWs(payload: Record<string, unknown>): Promise<boolean> {
|
||||
this._busy = true;
|
||||
/** helpers/ws-run runWs bound to this dialog's busy flag and error line. */
|
||||
private _runWs<T = unknown>(payload: Record<string, unknown>): Promise<T | undefined> {
|
||||
this._error = "";
|
||||
try {
|
||||
await this.hass.connection.sendMessagePromise(payload);
|
||||
this._busy = false;
|
||||
return true;
|
||||
} catch (e) {
|
||||
this._error = describeWsError(e, this._lang);
|
||||
this._busy = false;
|
||||
return false;
|
||||
}
|
||||
return runWs<T>(this, payload, {
|
||||
busy: (b) => { this._busy = b; },
|
||||
onError: (m) => { this._error = m; },
|
||||
});
|
||||
}
|
||||
|
||||
private _notifyChanged(action: string): void {
|
||||
@@ -195,8 +217,9 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
task,
|
||||
objects,
|
||||
lang: this._lang,
|
||||
checklist: task.checklist || [],
|
||||
adaptiveEnabled: !!task.adaptive_config?.enabled,
|
||||
// The feature switches gate the checklist and the adaptive
|
||||
// feedback exactly like the panel (DRY audit 2026-09-26).
|
||||
features: this._features,
|
||||
currencySymbol: this._currencySymbol,
|
||||
}),
|
||||
);
|
||||
@@ -215,7 +238,7 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
task_id: this._taskId,
|
||||
reason: this._skipReason.trim() || null,
|
||||
});
|
||||
if (ok) {
|
||||
if (ok !== undefined) {
|
||||
this._notifyChanged("skip");
|
||||
this.close();
|
||||
}
|
||||
@@ -229,7 +252,7 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
task_id: this._taskId,
|
||||
date: this._resetDate || undefined,
|
||||
});
|
||||
if (ok) {
|
||||
if (ok !== undefined) {
|
||||
this._notifyChanged("reset");
|
||||
this.close();
|
||||
}
|
||||
@@ -258,15 +281,19 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
|
||||
private async _onDelete(): Promise<void> {
|
||||
if (!this._entryId || !this._taskId) return;
|
||||
const confirmText = t("delete_task_confirm", this._lang)
|
||||
|| `Delete "${this._task?.name}"?`;
|
||||
if (!window.confirm(confirmText)) return;
|
||||
const confirmed = await confirmAction(this.hass, {
|
||||
title: t("delete", this._lang),
|
||||
message: t("delete_task_confirm", this._lang),
|
||||
confirmText: t("delete", this._lang),
|
||||
danger: true,
|
||||
});
|
||||
if (!confirmed) return;
|
||||
const ok = await this._runWs({
|
||||
type: "maintenance_supporter/task/delete",
|
||||
entry_id: this._entryId,
|
||||
task_id: this._taskId,
|
||||
});
|
||||
if (ok) {
|
||||
if (ok !== undefined) {
|
||||
this._notifyChanged("delete");
|
||||
this.close();
|
||||
}
|
||||
@@ -279,7 +306,7 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
entry_id: this._entryId,
|
||||
task_id: this._taskId,
|
||||
});
|
||||
if (ok) {
|
||||
if (ok !== undefined) {
|
||||
this._notifyChanged("archive");
|
||||
this.close();
|
||||
}
|
||||
@@ -292,7 +319,7 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
entry_id: this._entryId,
|
||||
task_id: this._taskId,
|
||||
});
|
||||
if (ok) {
|
||||
if (ok !== undefined) {
|
||||
this._notifyChanged("unarchive");
|
||||
this.close();
|
||||
}
|
||||
@@ -315,41 +342,32 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
task_id: this._taskId,
|
||||
interval: this._task.suggested_interval,
|
||||
});
|
||||
if (ok) {
|
||||
this._toast = t("suggestion_applied", this._lang);
|
||||
if (ok !== undefined) {
|
||||
this._showToast(t("suggestion_applied", this._lang));
|
||||
this._notifyChanged("apply_suggestion");
|
||||
// Refresh local task so the recommendation card hides
|
||||
await this._loadTask();
|
||||
setTimeout(() => { this._toast = ""; }, 2500);
|
||||
}
|
||||
}
|
||||
|
||||
private async _reanalyzeInterval(): Promise<void> {
|
||||
if (!this._entryId || !this._taskId) return;
|
||||
this._busy = true;
|
||||
this._error = "";
|
||||
try {
|
||||
const r = await this.hass.connection.sendMessagePromise<{
|
||||
recommended_interval: number | null;
|
||||
confidence: string;
|
||||
data_points: number;
|
||||
}>({
|
||||
type: "maintenance_supporter/task/analyze_interval",
|
||||
entry_id: this._entryId,
|
||||
task_id: this._taskId,
|
||||
});
|
||||
this._toast = r.recommended_interval
|
||||
// The analyzer always works in DAYS (helpers/interval_analyzer.py), so
|
||||
// the unit is pinned here rather than taken from the task's own unit.
|
||||
? `${t("reanalyze_result", this._lang)}: ${formatInterval(r.recommended_interval, "days", this._lang)} (${r.data_points} pts)`
|
||||
: t("reanalyze_insufficient_data", this._lang);
|
||||
await this._loadTask();
|
||||
setTimeout(() => { this._toast = ""; }, 3500);
|
||||
} catch (e) {
|
||||
this._error = describeWsError(e, this._lang);
|
||||
} finally {
|
||||
this._busy = false;
|
||||
}
|
||||
const r = await this._runWs<{
|
||||
recommended_interval: number | null;
|
||||
confidence: string;
|
||||
data_points: number;
|
||||
}>({
|
||||
type: "maintenance_supporter/task/analyze_interval",
|
||||
entry_id: this._entryId,
|
||||
task_id: this._taskId,
|
||||
});
|
||||
if (r === undefined) return;
|
||||
this._showToast(r?.recommended_interval
|
||||
// The analyzer always works in DAYS (helpers/interval_analyzer.py), so
|
||||
// the unit is pinned here rather than taken from the task's own unit.
|
||||
? `${t("reanalyze_result", this._lang)}: ${formatInterval(r.recommended_interval, "days", this._lang)} (${r.data_points} pts)`
|
||||
: t("reanalyze_insufficient_data", this._lang));
|
||||
await this._loadTask();
|
||||
}
|
||||
|
||||
private _onEditHistoryEntry(entry: HistoryEntry): void {
|
||||
@@ -378,11 +396,13 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
task.interval_confidence || "medium", L,
|
||||
)}
|
||||
<div class="recommendation-actions">
|
||||
<button class="btn primary"
|
||||
@click=${this._applySuggestion} ?disabled=${this._busy}>
|
||||
<ha-icon icon="mdi:check"></ha-icon>
|
||||
${t("apply_suggestion", L)}
|
||||
</button>
|
||||
${canWrite(this.hass?.user, this._access)
|
||||
? html`<button class="btn primary qa-apply-suggestion"
|
||||
@click=${this._applySuggestion} ?disabled=${this._busy}>
|
||||
<ha-icon icon="mdi:check"></ha-icon>
|
||||
${t("apply_suggestion", L)}
|
||||
</button>`
|
||||
: nothing /* task/apply_suggestion is write tier */}
|
||||
<button class="btn"
|
||||
@click=${this._reanalyzeInterval} ?disabled=${this._busy}>
|
||||
<ha-icon icon="mdi:refresh"></ha-icon>
|
||||
@@ -440,55 +460,51 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
* existing history-edit dialog (which lives in the same dialog-mount). */
|
||||
private _renderDetails(task: MaintenanceTask) {
|
||||
const L = this._lang;
|
||||
// The list payload carries only the most recent entries (payload diet,
|
||||
// 20) — the stats come from the server's totals over the WHOLE history,
|
||||
// not from that window: a task done 30 times read "20" here (DRY audit
|
||||
// 2026-09-26). The list itself stays the recent window, newest first by
|
||||
// timestamp (a backdated #133 completion is appended last).
|
||||
const history = (task.history || []) as HistoryEntry[];
|
||||
const completed = history.filter((h) => h.type === "completed");
|
||||
const totalCost = completed.reduce(
|
||||
(s, h) => s + (typeof h.cost === "number" ? h.cost : 0),
|
||||
0,
|
||||
);
|
||||
const avgDuration = (() => {
|
||||
const durs = completed
|
||||
.map((h) => (typeof h.duration === "number" ? h.duration : null))
|
||||
.filter((d): d is number => d != null);
|
||||
if (!durs.length) return null;
|
||||
return Math.round(durs.reduce((s, d) => s + d, 0) / durs.length);
|
||||
})();
|
||||
const totalEntries = task.history_count ?? history.length;
|
||||
const recent = newestFirst(history).slice(0, 20);
|
||||
|
||||
return html`
|
||||
<div class="details">
|
||||
<div class="stats-grid">
|
||||
<div class="stat">
|
||||
<span class="stat-label">${t("times_performed", L)}</span>
|
||||
<span class="stat-value">${completed.length}</span>
|
||||
<span class="stat-value">${task.times_performed ?? 0}</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">${t("total_cost", L)}</span>
|
||||
<span class="stat-value">${formatCost(totalCost, this._currencySymbol, L)}</span>
|
||||
<span class="stat-value">${formatCost(task.total_cost ?? 0, this._currencySymbol, L)}</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">${t("avg_duration", L)}</span>
|
||||
<span class="stat-value">${formatDuration(avgDuration, L)}</span>
|
||||
<span class="stat-value">${formatDuration(task.average_duration != null ? Math.round(task.average_duration) : null, L)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="history-header">
|
||||
<strong>${t("history", L)}</strong>
|
||||
<span class="history-count">${history.length}</span>
|
||||
<span class="history-count">${totalEntries}</span>
|
||||
</div>
|
||||
${history.length === 0
|
||||
? html`<div class="history-empty">${t("history_empty", L)}</div>`
|
||||
: html`
|
||||
<div class="history-list">
|
||||
${[...history].reverse().slice(0, 20).map((entry) => renderHistoryEntry(entry, {
|
||||
${recent.map((entry) => renderHistoryEntry(entry, {
|
||||
lang: L,
|
||||
hass: this.hass,
|
||||
currencySymbol: this._currencySymbol,
|
||||
openEdit: (e) => this._onEditHistoryEntry(e),
|
||||
// task/history/update is write tier: no pencil otherwise.
|
||||
openEdit: canWrite(this.hass?.user, this._access) ? (e) => this._onEditHistoryEntry(e) : undefined,
|
||||
readingUnit: task.reading_unit,
|
||||
readingSlotDelta: (e, slotId) => readingSlotDelta(history, e, slotId),
|
||||
taskRef: this._taskRef,
|
||||
}, { compact: true }))}
|
||||
${history.length > 20
|
||||
? html`<div class="history-more">… +${history.length - 20} ${t("older_entries", L)}</div>`
|
||||
${totalEntries > recent.length
|
||||
? html`<div class="history-more">… +${totalEntries - recent.length} ${t("older_entries", L)}</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
`}
|
||||
@@ -500,17 +516,16 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
if (!this._open) return nothing;
|
||||
const L = this._lang;
|
||||
const task = this._task;
|
||||
const isAdmin = (this.hass?.user?.is_admin ?? true) as boolean;
|
||||
const writer = canWrite(this.hass?.user, this._access);
|
||||
|
||||
return html`
|
||||
<div class="backdrop" @click=${this.close}></div>
|
||||
<div class="dialog" role="dialog" aria-modal="true">
|
||||
return renderModalShell(() => this.close(), html`
|
||||
${task
|
||||
? html`
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
<span class="status-dot" style="background: ${STATUS_COLORS[task.status] || "#ccc"}"></span>
|
||||
<span class="status-dot" style="background: ${statusColor(task)}"></span>
|
||||
<span class="task-name">${task.name}</span>
|
||||
${renderStatusBadge(task, L)}
|
||||
</div>
|
||||
<div class="object">
|
||||
<button class="link-inline" @click=${() => {
|
||||
@@ -597,30 +612,33 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
${t("reset", L)}
|
||||
</ha-button>
|
||||
</div>
|
||||
${isAdmin
|
||||
? html`
|
||||
<div class="actions secondary-row">
|
||||
<ha-button size="small" appearance="outlined" variant="neutral" @click=${this._onEdit} .disabled=${this._busy}>
|
||||
<ha-icon slot="start" icon="mdi:pencil"></ha-icon>
|
||||
${t("edit", L)}
|
||||
</ha-button>
|
||||
<ha-button size="small" appearance="outlined" variant="neutral" @click=${this._onQr} .disabled=${this._busy}>
|
||||
<ha-icon slot="start" icon="mdi:qrcode"></ha-icon>
|
||||
${t("qr_code", L)}
|
||||
</ha-button>
|
||||
<ha-button size="small" appearance="outlined" variant="neutral"
|
||||
@click=${task.archived ? this._onUnarchive : this._onArchive}
|
||||
.disabled=${this._busy}>
|
||||
<ha-icon slot="start" icon="${task.archived ? 'mdi:archive-arrow-up-outline' : 'mdi:archive-outline'}"></ha-icon>
|
||||
${task.archived ? t("unarchive", L) : t("archive", L)}
|
||||
</ha-button>
|
||||
<ha-button size="small" appearance="outlined" variant="danger" class="danger" @click=${this._onDelete} .disabled=${this._busy}>
|
||||
<ha-icon slot="start" icon="mdi:delete"></ha-icon>
|
||||
${t("delete", L)}
|
||||
</ha-button>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<!-- QR is read tier (the panel offers it to operators too);
|
||||
Edit / Archive / Delete follow canWrite() — the panel's
|
||||
operator delegation, not is_admin alone. -->
|
||||
<div class="actions secondary-row">
|
||||
${writer
|
||||
? html`<ha-button size="small" appearance="outlined" variant="neutral" class="qa-edit" @click=${this._onEdit} .disabled=${this._busy}>
|
||||
<ha-icon slot="start" icon="mdi:pencil"></ha-icon>
|
||||
${t("edit", L)}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
<ha-button size="small" appearance="outlined" variant="neutral" class="qa-qr" @click=${this._onQr} .disabled=${this._busy}>
|
||||
<ha-icon slot="start" icon="mdi:qrcode"></ha-icon>
|
||||
${t("qr_code", L)}
|
||||
</ha-button>
|
||||
${writer
|
||||
? html`<ha-button size="small" appearance="outlined" variant="neutral" class="qa-archive"
|
||||
@click=${task.archived ? this._onUnarchive : this._onArchive}
|
||||
.disabled=${this._busy}>
|
||||
<ha-icon slot="start" icon="${task.archived ? 'mdi:archive-arrow-up-outline' : 'mdi:archive-outline'}"></ha-icon>
|
||||
${task.archived ? t("unarchive", L) : t("archive", L)}
|
||||
</ha-button>
|
||||
<ha-button size="small" appearance="outlined" variant="danger" class="danger qa-delete" @click=${this._onDelete} .disabled=${this._busy}>
|
||||
<ha-icon slot="start" icon="mdi:delete"></ha-icon>
|
||||
${t("delete", L)}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="details-toggle">
|
||||
<button class="link" @click=${() => { this._showDetails = !this._showDetails; }}>
|
||||
<ha-icon icon="${this._showDetails ? 'mdi:chevron-up' : 'mdi:chevron-down'}"></ha-icon>
|
||||
@@ -649,30 +667,18 @@ export class MaintenanceTaskQuickActionsDialog extends LitElement {
|
||||
</div>
|
||||
`}
|
||||
`
|
||||
: html`<div class="loading">${t("loading", L)}</div>`}
|
||||
</div>
|
||||
`;
|
||||
: this._error
|
||||
? html`<div class="error">${this._error}</div>`
|
||||
: html`<div class="loading">${t("loading", L)}</div>`}
|
||||
`);
|
||||
}
|
||||
|
||||
static styles = [sharedStyles, css`
|
||||
:host { display: contents; }
|
||||
.backdrop {
|
||||
position: fixed; inset: 0; z-index: 100;
|
||||
background: rgba(0,0,0,0.5);
|
||||
}
|
||||
.dialog {
|
||||
position: fixed; left: 50%; top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 95vw; max-width: 460px;
|
||||
max-height: 92vh; overflow: auto;
|
||||
background: var(--card-background-color, var(--ha-card-background, #1c1c1c));
|
||||
color: var(--primary-text-color);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.4);
|
||||
padding: 20px;
|
||||
display: flex; flex-direction: column; gap: 14px;
|
||||
z-index: 101;
|
||||
}
|
||||
protected updated(changed: Map<string, unknown>): void {
|
||||
if (changed.has("_open") && this._open) focusModalShell(this.shadowRoot);
|
||||
}
|
||||
|
||||
static styles = [sharedStyles, modalShellStyles, css`
|
||||
:host { display: contents; --ms-modal-max-width: 460px; }
|
||||
.header { display: flex; flex-direction: column; gap: 6px; }
|
||||
.title { display: flex; align-items: center; gap: 10px; }
|
||||
.status-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
|
||||
|
||||
Reference in New Issue
Block a user