456 files

This commit is contained in:
Home Assistant Version Control
2026-10-01 05:38:21 +00:00
parent 4192ecbaae
commit 5d6d845ebf
456 changed files with 64951 additions and 3828 deletions
@@ -151,8 +151,8 @@
.tmd-hd .ic{width:32px;height:32px;border-radius:9px;display:grid;place-items:center;background:rgba(255,255,255,.22);font-size:17px;flex:none}
.tmd-hd .tt{font-family:var(--tmd-font-display);font-weight:800;font-size:16px;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tmd-hd .tt small{display:block;font-family:var(--tmd-font-body);font-weight:600;font-size:11px;opacity:.85;margin-top:1px}
.tmd-hd .pill{margin-left:auto;font-size:11px;font-weight:800;padding:4px 9px;border-radius:999px;background:rgba(255,255,255,.25);text-transform:capitalize}
.tmd-hd .cnt{margin-left:auto;min-width:22px;height:22px;padding:0 6px;border-radius:999px;background:var(--tmd-bad);color:#fff;font-size:12px;font-weight:800;display:grid;place-items:center}
.tmd-hd .pill{margin-inline-start:auto;font-size:11px;font-weight:800;padding:4px 9px;border-radius:999px;background:rgba(255,255,255,.25);text-transform:capitalize}
.tmd-hd .cnt{margin-inline-start:auto;min-width:22px;height:22px;padding:0 6px;border-radius:999px;background:var(--tmd-bad);color:#fff;font-size:12px;font-weight:800;display:grid;place-items:center}
.tmd-bd{padding:15px}
.tmd-empty{text-align:center;padding:26px 12px;color:var(--tmd-dim);font-size:.9rem}
.av{width:var(--av,42px);height:var(--av,42px);border-radius:50%;flex:none;display:grid;place-items:center;font-family:var(--tmd-font-display);font-weight:800;color:#fff;font-size:calc(var(--av,42px)*.4);line-height:1;text-align:center;background:var(--ac,var(--tmd-accent));box-shadow:0 2px 6px rgba(0,0,0,.18);overflow:hidden}
@@ -233,7 +233,7 @@
:host([data-tm-design="graphite"]) .tmd-checklist{border:1px solid var(--tmd-border);border-radius:var(--tmd-radius-sm)}
:host([data-tm-design="graphite"]) .tmd-check{position:relative;padding:9px 12px;min-height:46px;box-sizing:border-box}
:host([data-tm-design="graphite"]) .tmd-check+.tmd-check{border-top:0}
:host([data-tm-design="graphite"]) .tmd-check+.tmd-check::before{content:"";position:absolute;top:0;left:46px;right:0;height:1px;background:var(--tmd-border)}
:host([data-tm-design="graphite"]) .tmd-check+.tmd-check::before{content:"";position:absolute;top:0;inset-inline-start:46px;inset-inline-end:0;height:1px;background:var(--tmd-border)}
:host([data-tm-design="graphite"]) .tmd-check .c-num{width:22px;height:22px;border-radius:50%;
background:transparent;border:1.5px solid var(--tmd-border);color:transparent;font-size:0}
:host([data-tm-design="graphite"]) .tmd-check.done .c-num{background:var(--tmd-good);border-color:var(--tmd-good);color:#fff;font-size:13px}
@@ -306,7 +306,7 @@
:host([data-tm-design="graphite"]) .lb-cp-score{font-variant-numeric:tabular-nums;font-weight:600;letter-spacing:-.6px}
:host([data-tm-design="graphite"]) .lb-cp-unit{color:var(--tmd-dim);font-weight:400}
:host([data-tm-design="graphite"]) .lb-cp-row.win{background:transparent}
:host([data-tm-design="graphite"]) .lb-cp-meta ha-icon{--mdc-icon-size:14px;vertical-align:-2px;margin-right:3px;color:var(--tmd-dim)}
:host([data-tm-design="graphite"]) .lb-cp-meta ha-icon{--mdc-icon-size:14px;vertical-align:-2px;margin-inline-end:3px;color:var(--tmd-dim)}
/* Overview: hairline-separated stats, then a grouped per-child list. */
:host([data-tm-design="graphite"]) .ov-kids{border:1px solid var(--tmd-border);border-radius:var(--tmd-radius-sm)}
@@ -317,7 +317,13 @@
:host([data-tm-design="graphite"]) .ov-today .val,
:host([data-tm-design="graphite"]) .pts{font-variant-numeric:tabular-nums;font-weight:600;letter-spacing:-.6px}
:host([data-tm-design="graphite"]) .nm,
:host([data-tm-design="graphite"]) .ttl{font-weight:500;letter-spacing:-.18px}`;
:host([data-tm-design="graphite"]) .ttl{font-weight:500;letter-spacing:-.18px}
/* Right-to-left (#979). apply() stamps dir on the host; an icon that points
along the reading direction (chevron, undo, backspace) carries .tm-rtl-flip
so it turns round with the layout. Text arrows (→ ↩) swap to their mirrored
character instead, which leaves LTR markup untouched. Not design-scoped. */
:host([dir="rtl"]) .tm-rtl-flip,[dir="rtl"] .tm-rtl-flip{transform:scaleX(-1)}`;
if (!document.getElementById("taskmate-design-fonts")) {
const styleEl = document.createElement("style");
@@ -410,16 +416,60 @@
return !!(window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches);
}
/**
* Text direction of HA's current language (#979): "rtl" or "ltr". HA marks
* RTL languages in its translation metadata (the same isRTL flag its own
* computeRTL reads); for a language it has no metadata for, follow the
* direction HA already gave the page.
*/
let _lastDir = "ltr";
function direction(hass) {
const lang = hass && hass.language;
const meta = hass && hass.translationMetadata && hass.translationMetadata.translations;
const root = typeof document !== "undefined" && document.documentElement;
if (lang && meta && meta[lang]) _lastDir = meta[lang].isRTL ? "rtl" : "ltr";
else _lastDir = root && root.dir === "rtl" ? "rtl" : "ltr";
return _lastDir;
}
/**
* Keep number runs reading left to right inside right-to-left text (#995).
* "3 / 1", "1,200 / 5,000" and "3-day" are one unit each, but the Unicode
* bidi algorithm lays their parts out right to left in an RTL paragraph, so
* "3 / 1" shows as "1 / 3" and "3-Day Streak" as "Day Streak-3". Each run is
* wrapped in a left-to-right isolate, and a label holding one, or opening
* with a number ("2 of 5 done", which otherwise reads "of 5 done 2"), is
* wrapped in a first-strong isolate so it keeps its own reading order (an
* English label stays English-ordered, a translated RTL one stays RTL). The
* isolates (U+2066/2068 … U+2069) are invisible, but splitting a text run
* still nudges glyph positioning by a sub-pixel, so they are only added
* while the page is right-to-left: the direction last resolved by
* direction(), which every card calls through apply() at the top of
* render() and the panel calls in its hass setter (HA has one language per
* page). In LTR, and for text with no number run that doesn't open with a
* number, the value comes back untouched. Works on plain strings: Lit text,
* the panel's HTML strings and translated messages alike.
*/
const _NUM = String.raw`\p{Nd}+(?:[.,\u066B\u066C]\p{Nd}+)*`;
const _NUM_RUN = new RegExp(String.raw`${_NUM}(?:\s*\/\s*${_NUM})+|(?<![\p{L}-])${_NUM}-\p{L}+`, "gu");
function ltrNums(text) {
if (text === null || text === undefined || _lastDir !== "rtl") return text;
const s = String(text);
const out = s.replace(_NUM_RUN, (run) => "\u2066" + run + "\u2069");
return out === s && !/^\p{Nd}/u.test(s) ? s : "\u2068" + out + "\u2069";
}
/**
* Resolve the design AND stamp the host element: data-tm-design + (in dark
* mode) data-tm-dark. Cards call this once at the top of render() and branch
* on the returned id. Returns "classic" when no design applies.
* mode) data-tm-dark + dir. Cards call this once at the top of render() and
* branch on the returned id. Returns "classic" when no design applies.
*/
function apply(el, hass, config, entity) {
const design = resolve(hass, config, entity);
if (el) {
el.setAttribute("data-tm-design", design);
el.toggleAttribute("data-tm-dark", design !== "classic" && isDark(hass, el));
el.setAttribute("dir", direction(hass));
}
// The host keeps the real id (its CSS keys off it); the card gets the
// layout id so aliased styles reuse an existing render path.
@@ -487,7 +537,7 @@
`;
}
window.__taskmate_design = { IDS, resolve, isDark, apply, active, editorOptions, styles, cssText, tokensCSS: TOKENS, colourPicker };
window.__taskmate_design = { IDS, resolve, isDark, direction, ltrNums, apply, active, editorOptions, styles, cssText, tokensCSS: TOKENS, colourPicker };
/**
* The single place that decides what a chore looks like (#750).
@@ -541,6 +591,29 @@
return { config: Object.assign({}, stub, { type: "custom:" + type, entity: entityId }) };
};
// ── Birthday "first open today" gate (#924) ───────────────────────────────
// True the first time it's asked about a child on a given day on this device,
// false after that — so the birthday confetti plays once per day, not on every
// hass update or page load. Shared by every card that celebrates, so a child
// card and a points card on the same dashboard don't both go off. Keyed on
// HA's time zone so the day rolls over with the backend, not the browser.
window.__taskmate_birthday_once = window.__taskmate_birthday_once || function (hass, childId) {
if (!childId) return false;
let day;
try {
day = new Date().toLocaleDateString("en-CA", { timeZone: hass?.config?.time_zone || undefined });
} catch (e) {
day = new Date().toLocaleDateString("en-CA");
}
const KEY = "taskmate_celebrated_day";
let seen;
try { seen = JSON.parse(window.localStorage.getItem(KEY) || "{}") || {}; } catch (e) { seen = {}; }
if (seen[childId] === day) return false;
seen[childId] = day;
try { window.localStorage.setItem(KEY, JSON.stringify(seen)); } catch (e) { /* private mode: plays each load */ }
return true;
};
// ── Chore-photo lightbox ───────────────────────────────────────────────────
// A single shared, design-AGNOSTIC image viewer for evidence photos. It is
// mounted on document.body (NOT inside any card's shadow root), so it paints
@@ -582,7 +655,7 @@
"background:#111;box-shadow:0 18px 60px rgba(0,0,0,.6);}" +
".tm-lightbox .tm-lb-cap{color:#f4f6fb;font-size:14px;font-weight:600;text-align:center;" +
"max-width:92vw;line-height:1.4;text-shadow:0 1px 2px rgba(0,0,0,.6);}" +
".tm-lightbox .tm-lb-close{position:absolute;top:14px;right:16px;width:42px;height:42px;border:none;" +
".tm-lightbox .tm-lb-close{position:absolute;top:14px;inset-inline-end:16px;width:42px;height:42px;border:none;" +
"border-radius:50%;background:rgba(255,255,255,.14);color:#fff;font-size:22px;line-height:1;" +
"cursor:pointer;transition:background .15s;}" +
".tm-lightbox .tm-lb-close:hover{background:rgba(255,255,255,.28);}" +