updated apps

This commit is contained in:
2026-07-14 23:57:03 -04:00
parent 6cc7212cef
commit 010e828e9c
797 changed files with 45153 additions and 4246 deletions
+166 -89
View File
@@ -133,6 +133,7 @@ class TaskMatePanel extends HTMLElement {
this._bulkMode = false; // chores multi-select mode
this._bulkSel = new Set(); // selected chore ids for bulk actions
this._activeTab = "children";
this._mobileNavOpen = false; // mobile section-picker dropdown state
this._dialog = null;
this._dialogInitialHash = null; // for confirm-on-leave
this._filter = ""; // per-tab search/filter
@@ -413,11 +414,13 @@ class TaskMatePanel extends HTMLElement {
const act = t.dataset.act;
if (act === "tab") {
this._activeTab = t.dataset.tab; this._filter = ""; this._render();
this._activeTab = t.dataset.tab; this._filter = ""; this._mobileNavOpen = false; this._render();
// Settings tab lists HA users for the parent-role picker (#661); load then repaint.
if (this._activeTab === "settings") this._ensureHaUsers().then(() => this._render());
return;
}
if (act === "toggle-mobile-nav") { this._mobileNavOpen = !this._mobileNavOpen; this._render(); return; }
if (act === "close-mobile-nav") { this._mobileNavOpen = false; this._render(); return; }
if (act === "close-dialog") { this._closeDialog(); return; }
if (act === "clear-field") { this._setEntityField(t.dataset.field, ""); this._render(); return; }
if (act === "pick-entity") { this._setEntityField(t.dataset.field, t.dataset.value); this._closeEntityDropdown(); this._render(); return; }
@@ -2136,7 +2139,6 @@ class TaskMatePanel extends HTMLElement {
this._zoneCache = {};
this._applyDesign();
this._bindHaPickers();
this._scrollActiveMobileTabIntoView();
return;
}
@@ -2157,10 +2159,6 @@ class TaskMatePanel extends HTMLElement {
savedScroll = dialogBody ? dialogBody.scrollTop : 0;
}
const mtabsZone = this.querySelector('[data-zone="mtabs"]');
const existingMtabs = mtabsZone ? mtabsZone.querySelector(".tm-mobile-tabs") : null;
const savedMtabsScroll = existingMtabs ? existingMtabs.scrollLeft : 0;
if (this._dialog) {
const openSet = this._dialog._openAdvanced instanceof Set ? this._dialog._openAdvanced : new Set();
this.querySelectorAll("details.tm-advanced[data-section]").forEach(el => {
@@ -2190,13 +2188,7 @@ class TaskMatePanel extends HTMLElement {
if (newBody) newBody.scrollTop = savedScroll;
}
if (savedMtabsScroll) {
const newMtabs = this.querySelector(".tm-mobile-tabs");
if (newMtabs) newMtabs.scrollLeft = savedMtabsScroll;
}
this._applyDesign();
this._scrollActiveMobileTabIntoView();
}
// The admin panel intentionally stays CLASSIC regardless of the global card
@@ -2207,19 +2199,6 @@ class TaskMatePanel extends HTMLElement {
if (shell) shell.removeAttribute("data-tm-design");
}
_scrollActiveMobileTabIntoView() {
const nav = this.querySelector(".tm-mobile-tabs");
if (!nav) return;
const active = nav.querySelector(".tm-mtab-active");
if (!active) return;
const navRect = nav.getBoundingClientRect();
const btnRect = active.getBoundingClientRect();
if (btnRect.left < navRect.left || btnRect.right > navRect.right) {
const target = active.offsetLeft - (nav.clientWidth - active.offsetWidth) / 2;
nav.scrollTo({ left: Math.max(0, target), behavior: "smooth" });
}
}
_sidebarGroups() {
const counts = this._state ? {
children: (this._state.children || []).length,
@@ -2324,19 +2303,43 @@ class TaskMatePanel extends HTMLElement {
_mobileTabs() {
const groups = this._sidebarGroups();
const open = !!this._mobileNavOpen;
const current = groups.flatMap(g => g.items).find(it => it.id === this._activeTab)
|| { id: this._activeTab, label: "", icon: "mdi:cog-outline" };
const curUrgent = current.id === "activity" && current.count > 0;
const curShowCount = current.count != null && current.count > 0;
return `
<nav class="tm-mobile-tabs">
${groups.flatMap(g => g.items).map(it => {
const active = it.id === this._activeTab;
const urgent = it.id === "activity" && it.count > 0;
const showCount = it.count != null && it.count > 0;
return `
<button type="button" class="tm-mtab ${active ? "tm-mtab-active" : ""}" data-act="tab" data-tab="${it.id}">
${this._esc(it.label)}${showCount ? `<span class="tm-mtab-pill ${urgent ? "tm-mtab-pill-urgent" : ""}">${it.count}</span>` : ""}
</button>
`;
}).join("")}
</nav>
<div class="tm-mobilenav ${open ? "tm-mobilenav-open" : ""}">
<button type="button" class="tm-mnav-trigger" data-act="toggle-mobile-nav"
aria-haspopup="true" aria-expanded="${open ? "true" : "false"}">
<span class="tm-mnav-ico"><ha-icon icon="${this._esc(current.icon)}"></ha-icon></span>
<span class="tm-mnav-main">
<span class="tm-mnav-crumb">TaskMate</span>
<span class="tm-mnav-title">${this._esc(current.label)}</span>
</span>
${curShowCount ? `<span class="tm-mnav-count ${curUrgent ? "tm-mnav-count-urgent" : ""}">${current.count}</span>` : ""}
<span class="tm-mnav-chev"><ha-icon icon="mdi:chevron-down"></ha-icon></span>
</button>
${open ? `
<div class="tm-mnav-scrim" data-act="close-mobile-nav"></div>
<nav class="tm-mnav-sheet">
${groups.map(g => `
<div class="tm-mnav-grp-head">${this._esc(g.head)}</div>
${g.items.map(it => {
const active = it.id === this._activeTab;
const urgent = it.id === "activity" && it.count > 0;
const showCount = it.count != null && it.count > 0;
return `
<button type="button" class="tm-mnav-item ${active ? "tm-mnav-item-active" : ""}" data-act="tab" data-tab="${it.id}">
<ha-icon class="tm-mnav-item-ico" icon="${this._esc(it.icon)}"></ha-icon>
<span class="tm-mnav-item-label">${this._esc(it.label)}</span>
${showCount ? `<span class="tm-mnav-count ${urgent ? "tm-mnav-count-urgent" : ""}">${it.count}</span>` : ""}
</button>`;
}).join("")}
`).join("")}
</nav>
` : ""}
</div>
`;
}
@@ -2441,7 +2444,7 @@ class TaskMatePanel extends HTMLElement {
<div class="tm-meta">${child.availability_entity ? `<code>${this._esc(child.availability_entity)}</code>${child.availability_inverted ? ` <em>${this._t("panel.child_inverted")}</em>` : ""}` : `${this._t("panel.child_no_availability")}`}${child.unavailability_entity ? ` · ${this._t("panel.child_busy_label")} <code>${this._esc(child.unavailability_entity)}</code>` : ""}</div>
</div>
</div>
<div class="tm-stats-row">
<div class="tm-stats-row" style="grid-template-columns: 1fr 1fr;">
<div class="tm-stat tm-stat-highlight"><div class="tm-stat-value">${this._fmtNum(child.points || 0)}</div><div class="tm-stat-label">${this._esc(pointsName)}</div></div>
<div class="tm-stat"><div class="tm-stat-value">${this._fmtNum(child.level || 1)}</div><div class="tm-stat-label">${this._t("panel.child_stat_level")}</div></div>
<div class="tm-stat"><div class="tm-stat-value">${this._fmtNum(child.total_points_earned || 0)}</div><div class="tm-stat-label">${this._t("panel.child_stat_earned")}</div></div>
@@ -3381,7 +3384,7 @@ class TaskMatePanel extends HTMLElement {
const count = (tpl.chores || []).length;
const pts = (tpl.chores || []).reduce((s, c) => s + (c.points || 0), 0);
return `
<div class="tm-manage-tpl ${locked ? "tm-manage-tpl-locked" : ""}">
<div class="tm-manage-tpl ${locked ? "tm-manage-tpl-locked" : ""}" data-act="tpl-select" data-id="${this._esc(tpl.id)}" style="cursor:pointer">
<div class="tm-tpl-icon"><ha-icon icon="${this._esc(tpl.icon || "mdi:clipboard-list")}"></ha-icon></div>
<div class="tm-manage-tpl-info">
<div class="tm-manage-tpl-name">${this._esc(tpl.name)}${locked ? ' <span class="tm-text-vfaint" style="font-size:12px">🔒</span>' : ""}</div>
@@ -3740,9 +3743,9 @@ class TaskMatePanel extends HTMLElement {
</div></div>
<div class="tm-section-body">
${(this._haUsers || []).filter(u => !u.is_admin).map(u => `
<label class="tm-setting-row" style="cursor:pointer">
<label class="tm-setting-row tm-role-row">
<div class="tm-setting-label">${this._esc(u.name)}</div>
<input type="checkbox" data-parent-user="${this._esc(u.id)}" ${(s.parent_user_ids || []).includes(u.id) ? "checked" : ""}>
<input type="checkbox" class="tm-role-check" data-parent-user="${this._esc(u.id)}" ${(s.parent_user_ids || []).includes(u.id) ? "checked" : ""}>
</label>`).join("")
|| `<p class="tm-meta">${this._t("panel.settings_parents_empty")}</p>`}
</div>
@@ -4084,7 +4087,7 @@ class TaskMatePanel extends HTMLElement {
<h4>${this._t("panel.notif_recipients_parents")}</h4>
${ns.recipients.parents.map(p => `
<div class="tm-row" style="display:flex;gap:10px;align-items:center;padding:8px 0;border-top:1px solid var(--tm-border,#3a3a3a)">
<input type="text" value="${this._esc(p.name)}" data-act="notif-rename-parent" data-parent-id="${this._esc(p.id)}" style="flex:1;background:transparent;border:none;color:inherit;font-size:14px;font-weight:500">
<input type="text" value="${this._esc(p.name)}" data-act="notif-rename-parent" data-parent-id="${this._esc(p.id)}" style="flex:1;min-width:60px;background:transparent;border:none;color:inherit;font-size:14px;font-weight:500">
<select class="tm-notif-select" data-act="notif-set-parent-notify" data-parent-id="${this._esc(p.id)}">
${optTags(p.notify_service)}
</select>
@@ -4127,7 +4130,7 @@ class TaskMatePanel extends HTMLElement {
<td>
<div style="display:flex;gap:10px;align-items:flex-start">
<input type="checkbox" class="tm-notif-switch" data-act="notif-set-master" data-type-id="${this._esc(t.id)}" ${active ? "checked" : ""}>
<div>
<div style="max-width:240px;white-space:normal">
<strong>${this._t("notification." + t.id + ".name")}</strong>
<div class="tm-meta">${this._t("notification." + t.id + ".description")}</div>
${t.id === "streak_at_risk" ? `
@@ -4183,8 +4186,8 @@ class TaskMatePanel extends HTMLElement {
_renderNotifCustomSection(ns, recipients) {
return `
<div class="tm-card" style="margin-bottom:16px">
<div style="display:flex;justify-content:space-between;align-items:center">
<h3>${this._t("panel.notif_section_custom")}</h3>
<div style="display:flex;flex-wrap:wrap;gap:10px;justify-content:space-between;align-items:center">
<h3 style="margin:0">${this._t("panel.notif_section_custom")}</h3>
<button type="button" class="tm-btn tm-btn-raised" data-act="notif-add-custom">+ ${this._t("panel.notif_add_custom")}</button>
</div>
<p class="tm-meta">${this._t("panel.notif_section_custom_desc")}</p>
@@ -5426,8 +5429,8 @@ class TaskMatePanel extends HTMLElement {
}
@keyframes tm-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.45; } }
/* ===== Mobile tabs (hidden by default, shown on narrow) ===== */
.tm-mobile-tabs { display: none; }
/* ===== Mobile section picker (hidden by default, shown on narrow) ===== */
.tm-mobilenav { display: none; }
/* Approval banner */
.tm-approval-banner {
@@ -5945,6 +5948,11 @@ class TaskMatePanel extends HTMLElement {
border-top: 1px solid var(--tm-border-soft);
}
.tm-setting-row:first-child { border-top: 0; }
.tm-role-row { grid-template-columns: 1fr auto; cursor: pointer; }
.tm-role-check {
width: 20px; height: 20px; margin: 0;
justify-self: end; accent-color: var(--tm-accent); cursor: pointer;
}
.tm-setting-label { color: var(--tm-text); font-size: 13px; font-weight: 500; }
.tm-setting-label small { display: block; color: var(--tm-text-faint); font-weight: 400; font-size: 12px; margin-top: 2px; }
.tm-difficulty-mults { display: flex; gap: 12px; flex-wrap: wrap; }
@@ -5965,7 +5973,17 @@ class TaskMatePanel extends HTMLElement {
grid-template-columns: 56px minmax(140px, 1fr) auto 36px;
gap: 12px;
}
.tm-period-row ha-icon-picker { max-width: 56px; }
.tm-period-row ha-icon-picker {
width: 56px; max-width: 56px; height: 56px;
border-radius: var(--tm-radius-sm, 8px); overflow: hidden;
/* Older HA renders the picker as a filled mwc-textfield: drop the fill
and its dark active-indicator line so no stray underline shows. */
--mdc-text-field-fill-color: transparent;
--mdc-text-field-idle-line-color: transparent;
--mdc-text-field-hover-line-color: transparent;
--mdc-text-field-focused-line-color: transparent;
--mdc-text-field-disabled-line-color: transparent;
}
.tm-period-row > .tm-input { max-width: none; }
.tm-vacation-row {
grid-template-columns: minmax(140px, 1fr) auto 36px;
@@ -6563,7 +6581,7 @@ class TaskMatePanel extends HTMLElement {
background: var(--tm-surface-0); border: 1px solid var(--tm-border);
border-radius: var(--tm-radius-sm); padding: 7px 10px;
color: var(--tm-text); font-size: 12.5px; font-family: inherit;
cursor: pointer; max-width: 240px; transition: border-color 0.1s;
cursor: pointer; min-width: 0; max-width: 240px; transition: border-color 0.1s;
}
.tm-notif-select:hover { border-color: var(--tm-border-strong); }
.tm-notif-select:focus { outline: 0; border-color: var(--tm-accent); box-shadow: var(--tm-shadow-focus); }
@@ -6602,54 +6620,112 @@ class TaskMatePanel extends HTMLElement {
.tm-sidebar { display: none; }
.tm-menu-btn { display: inline-flex; }
.tm-topbar { padding: 0 12px; }
.tm-mobile-tabs {
display: flex;
overflow-x: auto;
overflow-y: hidden;
overscroll-behavior: contain;
touch-action: pan-x;
scrollbar-width: none;
padding: 0 12px;
/* Redundant with the section picker below — hide on mobile. */
.tm-crumbs { display: none; }
.tm-approval-pill { margin-left: auto; }
/* Section picker replaces the old horizontal tab strip. */
.tm-main { position: relative; }
.tm-mobilenav {
display: block;
position: relative;
z-index: 5;
flex-shrink: 0;
background: var(--tm-surface-0);
border-bottom: 1px solid var(--tm-border);
flex-shrink: 0;
padding: 8px 12px;
}
.tm-mobile-tabs::-webkit-scrollbar { display: none; }
.tm-mtab {
position: relative;
background: transparent;
border: 0;
color: var(--tm-text-faint);
padding: 12px 14px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
font-family: inherit;
white-space: nowrap;
display: flex; align-items: center; gap: 6px;
transition: color 0.1s var(--tm-easing);
}
.tm-mtab:hover { color: var(--tm-text); }
.tm-mtab-active { color: var(--tm-text); }
.tm-mtab-active::after {
content: ""; position: absolute;
left: 14px; right: 14px; bottom: -1px;
height: 2px; background: var(--tm-accent);
border-radius: 2px 2px 0 0;
}
.tm-mtab-pill {
.tm-mnav-trigger {
width: 100%;
display: flex; align-items: center; gap: 10px;
background: var(--tm-surface-2);
color: var(--tm-text-muted);
font-size: 10.5px; font-weight: 600;
padding: 1px 6px; border-radius: 999px;
border: 1px solid var(--tm-border);
border-radius: var(--tm-radius-md, 12px);
padding: 8px 12px;
cursor: pointer;
font-family: inherit;
color: var(--tm-text);
text-align: left;
transition: background 0.12s var(--tm-easing), border-color 0.12s var(--tm-easing);
}
.tm-mtab-active .tm-mtab-pill {
.tm-mnav-trigger:active { background: var(--tm-surface-3); }
.tm-mobilenav-open .tm-mnav-trigger { border-color: var(--tm-accent); }
.tm-mnav-ico {
width: 30px; height: 30px; flex-shrink: 0;
border-radius: var(--tm-radius-sm, 8px);
background: var(--tm-accent-soft);
color: var(--tm-accent-text);
display: grid; place-items: center;
--mdc-icon-size: 20px;
}
.tm-mtab-pill-urgent {
background: var(--tm-warning-soft) !important;
color: var(--tm-warning) !important;
.tm-mnav-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.tm-mnav-crumb { font-size: 11px; color: var(--tm-text-faint); line-height: 1.2; }
.tm-mnav-title {
font-size: 15px; font-weight: 600; line-height: 1.25;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.tm-mnav-chev {
color: var(--tm-text-muted); flex-shrink: 0;
--mdc-icon-size: 22px;
transition: transform 0.18s var(--tm-easing);
}
.tm-mobilenav-open .tm-mnav-chev { transform: rotate(180deg); }
.tm-mnav-count {
flex-shrink: 0;
background: var(--tm-surface-0);
border: 1px solid var(--tm-border);
color: var(--tm-text-muted);
font-size: 11px; font-weight: 600;
padding: 1px 7px; border-radius: 999px;
}
.tm-mnav-count-urgent {
background: var(--tm-warning-soft);
border-color: transparent;
color: var(--tm-warning);
}
.tm-mnav-scrim {
position: absolute;
top: 100%; left: 0; right: 0;
height: 100vh;
background: rgba(10, 15, 22, 0.32);
z-index: 30;
animation: tm-mnav-fade 0.18s var(--tm-easing);
}
.tm-mnav-sheet {
position: absolute;
top: calc(100% + 6px); left: 8px; right: 8px;
z-index: 31;
background: var(--tm-surface-0);
border: 1px solid var(--tm-border);
border-radius: var(--tm-radius-md, 14px);
box-shadow: 0 20px 48px rgba(0, 0, 0, 0.24);
max-height: calc(100vh - 200px);
overflow-y: auto;
padding: 4px 0 8px;
animation: tm-mnav-drop 0.18s var(--tm-easing);
}
.tm-mnav-grp-head {
font-size: 11px; font-weight: 700; letter-spacing: 0.06em;
text-transform: uppercase; color: var(--tm-text-faint);
padding: 12px 16px 4px;
}
.tm-mnav-item {
display: flex; align-items: center; gap: 12px; width: 100%;
background: none; border: 0; font-family: inherit; cursor: pointer;
padding: 11px 16px; color: var(--tm-text); text-align: left;
}
.tm-mnav-item:active { background: var(--tm-surface-2); }
.tm-mnav-item-ico {
flex-shrink: 0; color: var(--tm-text-muted);
--mdc-icon-size: 20px;
}
.tm-mnav-item-label { flex: 1; font-size: 15px; }
.tm-mnav-item-active { background: var(--tm-accent-soft); color: var(--tm-accent-text); }
.tm-mnav-item-active .tm-mnav-item-ico { color: var(--tm-accent-text); }
@keyframes tm-mnav-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes tm-mnav-drop {
from { opacity: 0; transform: translateY(-8px) scale(0.985); }
to { opacity: 1; transform: none; }
}
.tm-body { padding: 16px; }
.tm-toolbar { flex-direction: column; align-items: stretch; }
@@ -6658,6 +6734,7 @@ class TaskMatePanel extends HTMLElement {
.tm-scrim { padding: 0; }
.tm-dialog-body .tm-field-row { grid-template-columns: 1fr; }
.tm-setting-row { grid-template-columns: 1fr; gap: 6px; padding: 12px 16px; }
.tm-role-row { grid-template-columns: 1fr auto; gap: 12px; align-items: center; }
.tm-section-head, .tm-setting-row { padding-left: 16px; padding-right: 16px; }
.tm-timeline-row { grid-template-columns: 1fr auto; }
.tm-timeline-time, .tm-timeline-icon { display: none; }