117 files
This commit is contained in:
@@ -95,6 +95,23 @@
|
||||
|
||||
window.__taskmate_is_parent = isTaskmateParent;
|
||||
|
||||
/**
|
||||
* Stable id of a badge entry from the badges sensor.
|
||||
*
|
||||
* ChildBadgesSensor publishes earned/available entries keyed `badge_id`, but
|
||||
* several card templates were written against `b.id` — undefined on those
|
||||
* entries, so every "is this the badge that was just earned?" test compared
|
||||
* against the string "undefined" and never matched (#752). Read the id
|
||||
* through this helper so both spellings work.
|
||||
*/
|
||||
function badgeId(badge) {
|
||||
if (!badge) return "";
|
||||
const id = badge.badge_id != null ? badge.badge_id : badge.id;
|
||||
return id != null ? String(id) : "";
|
||||
}
|
||||
|
||||
window.__taskmate_badge_id = badgeId;
|
||||
|
||||
/**
|
||||
* Event-driven update guard for LitElement cards.
|
||||
*
|
||||
|
||||
@@ -36,58 +36,54 @@ class TaskMateBadgesCard extends LitElement {
|
||||
super();
|
||||
this._filterTier = "all";
|
||||
this._justEarned = null;
|
||||
this._unsubscribeEvents = null;
|
||||
this._subscribing = false;
|
||||
this._seenBadgeIds = null;
|
||||
this._justEarnedTimeout = null;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._subscribeEvents();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._unsubscribeEvents?.();
|
||||
this._unsubscribeEvents = null;
|
||||
if (this._justEarnedTimeout) {
|
||||
clearTimeout(this._justEarnedTimeout);
|
||||
this._justEarnedTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
_subscribeEvents() {
|
||||
if (!this.hass) return;
|
||||
if (this._unsubscribeEvents || this._subscribing) return;
|
||||
this._subscribing = true;
|
||||
this.hass.connection.subscribeEvents((event) => {
|
||||
const data = event.data || {};
|
||||
if (data.child_id && this.config?.child_id && String(data.child_id) !== String(this.config.child_id)) return;
|
||||
if (data.badge_id) {
|
||||
this._justEarned = String(data.badge_id);
|
||||
this.requestUpdate();
|
||||
this._justEarnedTimeout = setTimeout(() => {
|
||||
this._justEarned = null;
|
||||
this.requestUpdate();
|
||||
}, 1800);
|
||||
}
|
||||
}, "taskmate_badge_earned").then(unsub => {
|
||||
this._subscribing = false;
|
||||
if (!this.isConnected) {
|
||||
unsub();
|
||||
return;
|
||||
}
|
||||
this._unsubscribeEvents = unsub;
|
||||
}).catch(() => {
|
||||
this._subscribing = false;
|
||||
});
|
||||
/* Newly-earned badges are detected by diffing the badges sensor's `earned`
|
||||
list across hass updates (#752). The card used to subscribe to the custom
|
||||
`taskmate_badge_earned` event, but Home Assistant refuses a custom-event
|
||||
subscription from a non-admin user and logs an error every time — so the
|
||||
highlight never fired for children, and the log filled up. `state_changed`
|
||||
is allowlisted for everyone, so the diff works for every user. */
|
||||
_trackEarnedBadges() {
|
||||
const earned = this.hass?.states?.[this.config?.entity]?.attributes?.earned;
|
||||
if (!Array.isArray(earned)) return false;
|
||||
const ids = new Set(earned.map(b => window.__taskmate_badge_id(b)).filter(Boolean));
|
||||
|
||||
// First observation is the baseline — never flash a badge earned earlier.
|
||||
if (this._seenBadgeIds === null) {
|
||||
this._seenBadgeIds = ids;
|
||||
return false;
|
||||
}
|
||||
const fresh = [...ids].filter(id => !this._seenBadgeIds.has(id));
|
||||
this._seenBadgeIds = ids;
|
||||
if (!fresh.length) return false;
|
||||
|
||||
this._justEarned = fresh[0];
|
||||
clearTimeout(this._justEarnedTimeout);
|
||||
this._justEarnedTimeout = setTimeout(() => {
|
||||
this._justEarned = null;
|
||||
this.requestUpdate();
|
||||
}, 1800);
|
||||
return true;
|
||||
}
|
||||
|
||||
shouldUpdate(changedProps) {
|
||||
if (changedProps.has("hass")) {
|
||||
return window.__taskmate_hasChanged
|
||||
const flashed = this._trackEarnedBadges();
|
||||
const relevant = window.__taskmate_hasChanged
|
||||
? window.__taskmate_hasChanged(changedProps.get("hass"), this.hass, this.config?.entity)
|
||||
: true;
|
||||
return flashed || relevant;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -557,7 +553,7 @@ class TaskMateBadgesCard extends LitElement {
|
||||
}
|
||||
|
||||
_renderEarned(b) {
|
||||
const justEarned = this._justEarned && String(this._justEarned) === String(b.id);
|
||||
const justEarned = !!this._justEarned && this._justEarned === window.__taskmate_badge_id(b);
|
||||
return html`
|
||||
<div class="badge earned tier-${b.tier} ${justEarned ? 'just-earned' : ''}">
|
||||
${b.point_bonus > 0 ? html`<div class="badge-bonus">+${b.point_bonus}</div>` : ''}
|
||||
|
||||
@@ -43,9 +43,11 @@ class TaskMateChildCard extends LitElement {
|
||||
|
||||
shouldUpdate(changedProps) {
|
||||
if (changedProps.has("hass")) {
|
||||
return window.__taskmate_hasChanged
|
||||
const flashed = this._trackEarnedBadges();
|
||||
const relevant = window.__taskmate_hasChanged
|
||||
? window.__taskmate_hasChanged(changedProps.get("hass"), this.hass, this.config?.entity)
|
||||
: true;
|
||||
return flashed || relevant;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -66,19 +68,17 @@ class TaskMateChildCard extends LitElement {
|
||||
// Badge strip state
|
||||
this._earnedBadges = [];
|
||||
this._justEarnedBadge = null;
|
||||
this._badgeEventUnsub = null;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._subscribeBadgeEvents();
|
||||
this._seenBadgeIds = null;
|
||||
this._justEarnedTimeout = null;
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._stopTimerTick();
|
||||
this._badgeEventUnsub?.();
|
||||
this._badgeEventUnsub = null;
|
||||
if (this._justEarnedTimeout) {
|
||||
clearTimeout(this._justEarnedTimeout);
|
||||
this._justEarnedTimeout = null;
|
||||
}
|
||||
if (this._audioContext) {
|
||||
this._audioContext.close().catch(() => {});
|
||||
this._audioContext = null;
|
||||
@@ -118,27 +118,37 @@ class TaskMateChildCard extends LitElement {
|
||||
return fn ? fn(this.hass, key, params) : key;
|
||||
}
|
||||
|
||||
_subscribeBadgeEvents() {
|
||||
if (this._badgeEventUnsub || this._badgeSubscribing) return;
|
||||
if (!this.hass?.connection) return;
|
||||
this._badgeSubscribing = true;
|
||||
this.hass.connection.subscribeEvents((event) => {
|
||||
const data = event.data || {};
|
||||
const configChild = this.config?.child_id;
|
||||
if (configChild && data.child_id && String(data.child_id) !== String(configChild)) return;
|
||||
if (data.badge_id) {
|
||||
this._justEarnedBadge = String(data.badge_id);
|
||||
this.requestUpdate();
|
||||
setTimeout(() => { this._justEarnedBadge = null; this.requestUpdate(); }, 1800);
|
||||
}
|
||||
}, "taskmate_badge_earned").then(unsub => {
|
||||
this._badgeSubscribing = false;
|
||||
if (!this.isConnected) {
|
||||
unsub();
|
||||
return;
|
||||
}
|
||||
this._badgeEventUnsub = unsub;
|
||||
}).catch(() => { this._badgeSubscribing = false; });
|
||||
/* Newly-earned badges are detected by diffing this child's badges sensor
|
||||
`earned` list across hass updates (#752). The card used to subscribe to the
|
||||
custom `taskmate_badge_earned` event, but Home Assistant refuses a
|
||||
custom-event subscription from a non-admin user — logging an error every
|
||||
time — so the strip never lit up for a child. `state_changed` is
|
||||
allowlisted for everyone, so the diff works for every user. */
|
||||
_trackEarnedBadges() {
|
||||
const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config?.entity))
|
||||
|| this.hass?.states?.[this.config?.entity]?.attributes || {};
|
||||
const child = (attrs.children || []).find(c => c.id === this.config?.child_id)
|
||||
|| (!this.config?.child_id && (attrs.children || [])[0]);
|
||||
const earned = this._resolveBadgesEntity(child)?.attributes?.earned;
|
||||
if (!Array.isArray(earned)) return false;
|
||||
const ids = new Set(earned.map(b => window.__taskmate_badge_id(b)).filter(Boolean));
|
||||
|
||||
// First observation is the baseline — never flash a badge earned earlier.
|
||||
if (this._seenBadgeIds === null) {
|
||||
this._seenBadgeIds = ids;
|
||||
return false;
|
||||
}
|
||||
const fresh = [...ids].filter(id => !this._seenBadgeIds.has(id));
|
||||
this._seenBadgeIds = ids;
|
||||
if (!fresh.length) return false;
|
||||
|
||||
this._justEarnedBadge = fresh[0];
|
||||
clearTimeout(this._justEarnedTimeout);
|
||||
this._justEarnedTimeout = setTimeout(() => {
|
||||
this._justEarnedBadge = null;
|
||||
this.requestUpdate();
|
||||
}, 1800);
|
||||
return true;
|
||||
}
|
||||
|
||||
_tierColor(tier) {
|
||||
@@ -874,7 +884,7 @@ class TaskMateChildCard extends LitElement {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Chore number wrapper (icon removed) */
|
||||
/* Chore number wrapper — holds the number-or-picture badge */
|
||||
.chore-number-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -902,6 +912,14 @@ class TaskMateChildCard extends LitElement {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* A chore picture sits where the digit would, so it has to carry the
|
||||
same white-on-colour weight the numeral gets from its text-shadow. */
|
||||
.chore-number-badge ha-icon {
|
||||
--mdc-icon-size: 22px;
|
||||
color: #fff;
|
||||
filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.25));
|
||||
}
|
||||
|
||||
.chore-card:hover .chore-number-badge {
|
||||
transform: rotate(5deg) scale(1.1);
|
||||
}
|
||||
@@ -1137,10 +1155,6 @@ class TaskMateChildCard extends LitElement {
|
||||
filter: saturate(0.7);
|
||||
}
|
||||
|
||||
.chore-card.completed .chore-icon-container {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.chore-card.completed .chore-name {
|
||||
color: var(--primary-text-color);
|
||||
opacity: 0.7;
|
||||
@@ -1608,6 +1622,7 @@ class TaskMateChildCard extends LitElement {
|
||||
.chore-card { padding: 10px 12px; gap: 8px; min-height: 54px; flex-wrap: nowrap; }
|
||||
.chore-info { flex: 1; min-width: 0; overflow: hidden; }
|
||||
.chore-number-badge { width: 30px; height: 30px; min-width: 30px; font-size: 1rem; }
|
||||
.chore-number-badge ha-icon { --mdc-icon-size: 18px; }
|
||||
.chore-name { font-size: 0.95rem; }
|
||||
.chore-points { font-size: 0.82rem; }
|
||||
.chore-checkbox { width: 34px; height: 34px; min-width: 34px; border-radius: 8px; }
|
||||
@@ -1969,6 +1984,15 @@ class TaskMateChildCard extends LitElement {
|
||||
.tmd-undochip[disabled] { opacity: 0.6; cursor: default; }
|
||||
.q-undo { background: var(--tmd-surface-2); color: var(--tmd-dim); border: 1px solid var(--tmd-border); padding: 7px 10px; font-size: 14px; }
|
||||
|
||||
/* Designed: an explicit chore picture in the glyph slot. The emoji it
|
||||
replaces is sized by font-size, which an ha-icon ignores, so each row
|
||||
type restates its own size. Inherits the row accent so the icon reads
|
||||
as part of the style rather than a pasted-in glyph. */
|
||||
.tmd-glyph-icon { color: var(--ac, var(--tmd-accent)); display: block; }
|
||||
.tmd-chore .ch-emoji .tmd-glyph-icon { --mdc-icon-size: 22px; }
|
||||
.tmd-quest .q-emoji .tmd-glyph-icon { --mdc-icon-size: 19px; }
|
||||
.tmd-check .c-emoji .tmd-glyph-icon { --mdc-icon-size: 18px; }
|
||||
|
||||
/* Designed: pending-points + countdown chips on the header/section */
|
||||
.tmd-pending {
|
||||
margin-left: auto; display: inline-flex; align-items: center; gap: 4px;
|
||||
@@ -2205,7 +2229,7 @@ class TaskMateChildCard extends LitElement {
|
||||
<div class="badge-strip" @click=${() => this._openBadgesView()} title="${this._t('badges.label')}">
|
||||
<span class="badge-strip-label">${this._t('badges.label')}</span>
|
||||
${earnedBadges.slice(0, 5).map(b => html`
|
||||
<div class="badge-mini tier-${b.tier} ${this._justEarnedBadge && String(this._justEarnedBadge) === String(b.id) ? 'just-earned' : ''}"
|
||||
<div class="badge-mini tier-${b.tier} ${this._justEarnedBadge === window.__taskmate_badge_id(b) ? 'just-earned' : ''}"
|
||||
style="--t: ${this._tierColor(b.tier)}">
|
||||
<ha-icon icon="${b.icon || 'mdi:medal'}"></ha-icon>
|
||||
</div>
|
||||
@@ -2332,6 +2356,20 @@ class TaskMateChildCard extends LitElement {
|
||||
return "⭐";
|
||||
}
|
||||
|
||||
/** What a designed chore row shows in its glyph slot.
|
||||
*
|
||||
* An explicit picture wins over the keyword guess (#745) — otherwise
|
||||
* choosing mdi:watering-can changed nothing, because _choreEmoji only ever
|
||||
* matched on words. The guess stays as the fallback so chores with no
|
||||
* picture, which is all of them until someone sets one, look as they did.
|
||||
* The icon inherits the row's accent so it reads as part of the style.
|
||||
*/
|
||||
_choreGlyph(chore) {
|
||||
return chore.icon
|
||||
? html`<ha-icon icon="${chore.icon}" class="tmd-glyph-icon"></ha-icon>`
|
||||
: this._choreEmoji(chore);
|
||||
}
|
||||
|
||||
/** Mirror of _renderChoreCard's "completed today" detection, designed branch only. */
|
||||
_isChoreDone(chore, child, todaysCompletions) {
|
||||
const childCompletionsToday = (todaysCompletions || []).filter(
|
||||
@@ -2411,7 +2449,7 @@ class TaskMateChildCard extends LitElement {
|
||||
return {
|
||||
chore, child, done, loading, onAct, index: i, dimmed,
|
||||
tone: this._designTone(i),
|
||||
emoji: this._choreEmoji(chore),
|
||||
glyph: this._choreGlyph(chore),
|
||||
points: chore.effective_points ?? chore.points,
|
||||
timed: chore.task_type === "timed",
|
||||
mandatory: chore.mandatory === true,
|
||||
@@ -2465,7 +2503,7 @@ class TaskMateChildCard extends LitElement {
|
||||
<div class="tmd-badges" @click=${() => this._openBadgesView()} title="${this._t("badges.label")}">
|
||||
<span class="lbl">${this._t("badges.label")}</span>
|
||||
${earnedBadges.slice(0, 5).map(b => html`
|
||||
<div class="tmd-badge-mini ${this._justEarnedBadge && String(this._justEarnedBadge) === String(b.id) ? "just-earned" : ""}"
|
||||
<div class="tmd-badge-mini ${this._justEarnedBadge === window.__taskmate_badge_id(b) ? "just-earned" : ""}"
|
||||
style="--t:${this._tierColor(b.tier)}">
|
||||
<ha-icon icon="${b.icon || "mdi:medal"}"></ha-icon>
|
||||
</div>`)}
|
||||
@@ -2569,7 +2607,7 @@ class TaskMateChildCard extends LitElement {
|
||||
${rows.map(r => r.timed ? this._designTimed(r) : html`
|
||||
<div class="tmd-chore ${r.done ? "done" : ""} ${r.mandatory ? "mandatory" : ""} ${r.dimmed ? "dimmed" : ""}" style="--ac:${r.tone}">
|
||||
<div class="num-badge" style="${r.done ? "--ac:var(--tmd-good)" : ""}">${r.done ? "✓" : r.index + 1}</div>
|
||||
<span class="ch-emoji">${r.emoji}</span>
|
||||
<span class="ch-emoji">${r.glyph}</span>
|
||||
<div class="ch-mid">
|
||||
<div class="ch-name">${r.chore.name}</div>
|
||||
${r.done ? "" : html`<div class="chip soft" style="margin-top:3px">+${r.points} ⭐</div>`}
|
||||
@@ -2589,7 +2627,7 @@ class TaskMateChildCard extends LitElement {
|
||||
${rows.map(r => r.timed ? this._designTimed(r) : html`
|
||||
<div class="tmd-quest ${r.done ? "done" : ""} ${r.mandatory ? "mandatory" : ""} ${r.dimmed ? "dimmed" : ""}" style="--ac:${r.tone}">
|
||||
<div class="num q-num" style="${r.done ? "color:var(--tmd-good)" : ""}">${r.done ? "✓" : String(r.index + 1).padStart(2, "0")}</div>
|
||||
<span class="q-emoji">${r.emoji}</span>
|
||||
<span class="q-emoji">${r.glyph}</span>
|
||||
<div class="q-mid">
|
||||
<div class="q-name">${r.chore.name}</div>
|
||||
${r.done
|
||||
@@ -2611,7 +2649,7 @@ class TaskMateChildCard extends LitElement {
|
||||
${rows.map(r => r.timed ? this._designTimed(r) : html`
|
||||
<div class="tmd-check ${r.done ? "done" : ""} ${r.mandatory ? "mandatory" : ""} ${r.dimmed ? "dimmed" : ""}" style="--ac:${r.tone}">
|
||||
<div class="c-num" style="${r.done ? "--ac:var(--tmd-good)" : ""}">${r.done ? "✓" : r.index + 1}</div>
|
||||
<span class="c-emoji">${r.emoji}</span>
|
||||
<span class="c-emoji">${r.glyph}</span>
|
||||
<div class="c-mid">
|
||||
<div class="c-name">${r.chore.name}</div>
|
||||
${this._designChoreMeta(r)}
|
||||
@@ -3289,6 +3327,23 @@ class TaskMateChildCard extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
/** The coloured badge at the head of a chore row.
|
||||
*
|
||||
* A chore's picture takes the digit's place when one is set (#745) — the
|
||||
* number is only positional, whereas the icon is what a child actually
|
||||
* recognises. Chores with no picture keep their number, which is every
|
||||
* chore that predates the field. Shared by the standard and timed rows so
|
||||
* a picture can't work on one and be invisible on the other.
|
||||
*/
|
||||
_choreNumberBadge(chore, colorClass, choreNumber) {
|
||||
return html`
|
||||
<div class="chore-number-badge ${colorClass}">
|
||||
${chore.icon
|
||||
? html`<ha-icon icon="${chore.icon}"></ha-icon>`
|
||||
: choreNumber}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
_renderChoreCard(chore, child, pointsIcon, todaysCompletions = [], choreIndex = 0) {
|
||||
const isLoading = this._loading[chore.id];
|
||||
const isCelebrating = this._celebrating === chore.id;
|
||||
@@ -3398,7 +3453,7 @@ class TaskMateChildCard extends LitElement {
|
||||
>
|
||||
<div class="chore-info">
|
||||
<div class="chore-number-wrapper">
|
||||
<div class="chore-number-badge ${colorClass}">${choreNumber}</div>
|
||||
${this._choreNumberBadge(chore, colorClass, choreNumber)}
|
||||
</div>
|
||||
<div class="chore-details">
|
||||
<div class="chore-name">${chore.name}${chore.mandatory ? html`<span class="mandatory-badge">⚠ ${this._t('child.mandatory')}</span>` : ''}</div>
|
||||
@@ -3525,7 +3580,7 @@ class TaskMateChildCard extends LitElement {
|
||||
<div class="timed-chore-card ${state} ${isLoading ? 'loading' : ''}">
|
||||
<div class="timed-top-row">
|
||||
<div class="chore-number-wrapper">
|
||||
<div class="chore-number-badge ${colorClass}">${choreNumber}</div>
|
||||
${this._choreNumberBadge(chore, colorClass, choreNumber)}
|
||||
</div>
|
||||
<div class="chore-details">
|
||||
<div class="chore-name">${chore.name}</div>
|
||||
|
||||
@@ -466,6 +466,28 @@ export function createIncentiveCard(P) {
|
||||
return this._getAttrs().points_name || this._t("common.stars");
|
||||
}
|
||||
|
||||
/* ── Roles (#749) ──────────────────────────────────────────────────────
|
||||
Applying an existing bonus/penalty is a day-to-day parent action, so
|
||||
admins *and* TaskMate parents get the Apply buttons. Defining one is
|
||||
structural config, so the manage controls (pencil, add/edit/delete) are
|
||||
admin-only — matching the `_parent` / `_admin` service gates. A child on
|
||||
a shared tablet sees the list with no buttons. */
|
||||
|
||||
_canApply() {
|
||||
return window.__taskmate_is_parent(this.hass);
|
||||
}
|
||||
|
||||
_canManage() {
|
||||
return !!this.hass?.user?.is_admin;
|
||||
}
|
||||
|
||||
_toggleEditMode() {
|
||||
if (!this._canManage()) return;
|
||||
this._editMode = !this._editMode;
|
||||
this._editingBonus = null;
|
||||
this._showNewForm = false;
|
||||
}
|
||||
|
||||
_getVisibleBonuses() {
|
||||
const child = this._getSelectedChild();
|
||||
if (!child) return this._getBonuses();
|
||||
@@ -482,7 +504,7 @@ export function createIncentiveCard(P) {
|
||||
|
||||
async _applyBonus(bonus) {
|
||||
const child = this._getSelectedChild();
|
||||
if (!child) return;
|
||||
if (!child || !this._canApply()) return;
|
||||
const key = bonus.id;
|
||||
if (this._loading[key]) return;
|
||||
this._loading = { ...this._loading, [key]: true };
|
||||
@@ -630,7 +652,7 @@ export function createIncentiveCard(P) {
|
||||
<ha-icon icon="mdi:trash-can-outline"></ha-icon>
|
||||
</button>
|
||||
</div>
|
||||
` : html`
|
||||
` : this._canApply() ? html`
|
||||
<button class="apply-btn"
|
||||
?disabled=${isLoading || !child}
|
||||
@click=${() => this._applyBonus(b)}>
|
||||
@@ -639,7 +661,7 @@ export function createIncentiveCard(P) {
|
||||
: html`<ha-icon icon="${P.applyIcon}"></ha-icon> ${this._t('common.apply')}`
|
||||
}
|
||||
</button>
|
||||
`}
|
||||
` : ""}
|
||||
</div>
|
||||
${isEditing ? this._renderEditForm() : ""}
|
||||
`;
|
||||
@@ -745,10 +767,12 @@ export function createIncentiveCard(P) {
|
||||
${bonuses.length ? html`
|
||||
<span class="bonus-count">${bonuses.length}</span>
|
||||
` : ""}
|
||||
<button class="icon-btn ${this._editMode ? "active" : ""}" title="${this._tp('manage_title')}"
|
||||
@click=${() => { this._editMode = !this._editMode; this._editingBonus = null; this._showNewForm = false; }}>
|
||||
<ha-icon icon="mdi:pencil"></ha-icon>
|
||||
</button>
|
||||
${this._canManage() ? html`
|
||||
<button class="icon-btn ${this._editMode ? "active" : ""}" title="${this._tp('manage_title')}"
|
||||
@click=${this._toggleEditMode}>
|
||||
<ha-icon icon="mdi:pencil"></ha-icon>
|
||||
</button>
|
||||
` : ""}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -776,7 +800,7 @@ export function createIncentiveCard(P) {
|
||||
`}
|
||||
` : ""}
|
||||
|
||||
${child && !this._editMode ? html`
|
||||
${child && !this._editMode && this._canApply() ? html`
|
||||
<div style="text-align:center; font-size:0.8rem; color:var(--text-secondary); padding-top:4px;">
|
||||
${this._tp('applying_to', { childName: child.name, points: child.points, pointsName: this._getPointsName() })}
|
||||
</div>
|
||||
@@ -831,6 +855,7 @@ export function createIncentiveCard(P) {
|
||||
}
|
||||
|
||||
_designApplyBtn(b, klass) {
|
||||
if (!this._canApply()) return "";
|
||||
const child = this._getSelectedChild();
|
||||
const isLoading = this._loading[b.id];
|
||||
return html`
|
||||
@@ -955,11 +980,13 @@ export function createIncentiveCard(P) {
|
||||
<span class="ic">${icon}</span>
|
||||
<span class="tt">${this.config.title || this._tp('default_title')}${
|
||||
bonuses.length ? html`<small>${bonuses.length}</small>` : ""}</span>
|
||||
<button class="btn ghost sm" style="margin-left:auto"
|
||||
title="${this._tp('manage_title')}"
|
||||
@click=${() => { this._editMode = !this._editMode; this._editingBonus = null; this._showNewForm = false; }}>
|
||||
<ha-icon icon="mdi:pencil"></ha-icon>
|
||||
</button>
|
||||
${this._canManage() ? html`
|
||||
<button class="btn ghost sm" style="margin-left:auto"
|
||||
title="${this._tp('manage_title')}"
|
||||
@click=${this._toggleEditMode}>
|
||||
<ha-icon icon="mdi:pencil"></ha-icon>
|
||||
</button>
|
||||
` : ""}
|
||||
</div>`;
|
||||
|
||||
const rows = design === "playroom"
|
||||
@@ -984,7 +1011,7 @@ export function createIncentiveCard(P) {
|
||||
<ha-icon icon="mdi:plus"></ha-icon> ${this._tp('new_bonus')}
|
||||
</button>`)
|
||||
: ""}
|
||||
${child && !this._editMode
|
||||
${child && !this._editMode && this._canApply()
|
||||
? html`<div class="d-foot">${this._tp('applying_to', { childName: child.name, points: child.points, pointsName: this._getPointsName() })}</div>`
|
||||
: ""}`;
|
||||
|
||||
|
||||
@@ -1403,12 +1403,14 @@ class TaskMatePanel extends HTMLElement {
|
||||
}
|
||||
|
||||
async _doSaveChore() {
|
||||
this._syncIconPickers();
|
||||
const d = this._dialog.data;
|
||||
if (!d.name || !d.name.trim()) { this._showToast("err", this._t("panel.toast_name_required")); return; }
|
||||
const wasAdd = this._dialog.mode === "add";
|
||||
const base = {
|
||||
name: d.name.trim(),
|
||||
description: d.description || "",
|
||||
icon: d.icon || "",
|
||||
points: Number(d.points) || 0,
|
||||
assigned_to: d.assigned_to || [],
|
||||
requires_approval: !!d.requires_approval,
|
||||
@@ -5933,8 +5935,20 @@ class TaskMatePanel extends HTMLElement {
|
||||
|
||||
_styles() {
|
||||
return `<style>
|
||||
/* Fill the panel area without depending on a percentage-height chain.
|
||||
HA nests us as taskmate-panel → ha-panel-custom → partial-panel-resolver
|
||||
→ ha-drawer, and the middle two are display:inline / height:auto. A
|
||||
height:100% here only resolves while some ancestor happens to carry a
|
||||
definite height; where it doesn't, the shell collapses to its content
|
||||
height and the nav column visibly stops partway down every short page
|
||||
(#754). A flex column of exactly 100dvh is immune to the ancestor
|
||||
chain: panel_custom gives the element the whole viewport — HA draws no
|
||||
toolbar above it — so 100dvh is the full panel area, not an overflow.
|
||||
It must be height, not min-height: the shell scrolls its own body,
|
||||
and a floor-only rule would let a long section (Settings) grow the
|
||||
panel past the viewport and scroll the nav column off the page. */
|
||||
taskmate-panel {
|
||||
display: block; height: 100%;
|
||||
display: flex; flex-direction: column; height: 100dvh;
|
||||
|
||||
--tm-bg: var(--primary-background-color, #fafafa);
|
||||
--tm-surface-0: var(--card-background-color, #fff);
|
||||
@@ -6002,7 +6016,11 @@ class TaskMatePanel extends HTMLElement {
|
||||
.tm-shell {
|
||||
display: grid;
|
||||
grid-template-columns: var(--tm-sidebar-w) 1fr;
|
||||
height: 100%;
|
||||
/* Flex to fill the panel rather than height:100% — see the note on
|
||||
taskmate-panel above (#754). min-height:0 lets the grid shrink
|
||||
instead of being forced to its content height. */
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user