Files
Home-Assistant/custom_components/taskmate/www/taskmate-badges-card.js
T
2026-07-08 10:43:39 -04:00

779 lines
26 KiB
JavaScript

/**
* TaskMate Badges Card
* Displays a full grid of achievement badges for a single child.
* Earned badges show tier colour and earn date; locked badges show
* closest-criterion progress bar.
*
* Config:
* entity - (required) full entity id, e.g. sensor.taskmate_badges_mia
* title - card title override
*
* Version: 1.0.0
*/
const LitElement = customElements.get("hui-masonry-view")
? Object.getPrototypeOf(customElements.get("hui-masonry-view"))
: Object.getPrototypeOf(customElements.get("hui-view"));
const html = LitElement.prototype.html;
const css = LitElement.prototype.css;
const DESIGN_HEADER = "#6c5ce7";
const _safeColor = (c, d) => (typeof c === "string" && /^#[0-9a-fA-F]{3,8}$/.test(c) ? c : d);
class TaskMateBadgesCard extends LitElement {
static get properties() {
return {
hass: { type: Object },
config: { type: Object },
_filterTier: { type: String },
_justEarned: { type: String },
};
}
constructor() {
super();
this._filterTier = "all";
this._justEarned = null;
this._unsubscribeEvents = null;
this._subscribing = false;
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;
});
}
shouldUpdate(changedProps) {
if (changedProps.has("hass")) {
return window.__taskmate_hasChanged
? window.__taskmate_hasChanged(changedProps.get("hass"), this.hass, this.config?.entity)
: true;
}
return true;
}
_t(key, params) {
const fn = window.__taskmate_localize;
return fn ? fn(this.hass, key, params) : key;
}
static get styles() {
const base = css`
:host {
display: block;
--t-bronze: #e08a3c;
--t-silver: #a8b8c8;
--t-gold: #ffc107;
--t-platinum: #4dd9e8;
--t-bronze-bg: linear-gradient(135deg, #fdf0e4 0%, #f9dfc2 100%);
--t-silver-bg: linear-gradient(135deg, #f0f3f7 0%, #dce4ec 100%);
--t-gold-bg: linear-gradient(135deg, #fffde6 0%, #fff3b0 100%);
--t-platinum-bg: linear-gradient(135deg, #e8fbfd 0%, #c4f0f6 100%);
--t-locked-bg: linear-gradient(135deg, #f8f9fa 0%, #eef0f2 100%);
}
ha-card { overflow: hidden; }
/* ── Header ── */
.badges-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 20px 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
gap: 12px;
}
.badges-head-left {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
min-width: 0;
}
.child-avatar {
width: 44px;
height: 44px;
border-radius: 50%;
background: rgba(255,255,255,0.25);
border: 2px solid rgba(255,255,255,0.5);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
--mdc-icon-size: 26px;
color: white;
}
.badges-title {
font-size: 1.1rem;
font-weight: 700;
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.badges-count {
font-size: 0.75rem;
color: rgba(255,255,255,0.8);
margin-top: 2px;
}
.badges-filter {
background: rgba(255,255,255,0.2);
border: 1px solid rgba(255,255,255,0.35);
color: #fff;
padding: 6px 10px;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
flex-shrink: 0;
cursor: pointer;
backdrop-filter: blur(4px);
}
.badges-filter option {
color: var(--primary-text-color, #333);
background: var(--card-background-color, #fff);
}
/* ── Grid ── */
.badge-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 14px;
padding: 18px;
}
/* ── Tier colour helpers ── */
.tier-bronze { --t: var(--t-bronze); --t-glow: rgba(224,138,60,0.35); --t-bg: var(--t-bronze-bg); }
.tier-silver { --t: var(--t-silver); --t-glow: rgba(168,184,200,0.35); --t-bg: var(--t-silver-bg); }
.tier-gold { --t: var(--t-gold); --t-glow: rgba(255,193,7,0.40); --t-bg: var(--t-gold-bg); }
.tier-platinum { --t: var(--t-platinum); --t-glow: rgba(77,217,232,0.40); --t-bg: var(--t-platinum-bg); }
/* ── Badge tile ── */
.badge {
background: var(--t-locked-bg);
border-radius: 16px;
padding: 18px 12px 14px;
text-align: center;
position: relative;
transition: transform 0.2s ease, box-shadow 0.2s ease;
border: 2px dashed var(--divider-color, #d0d5db);
}
.badge.earned {
background: var(--t-bg);
border: 2.5px solid var(--t);
border-style: solid;
box-shadow: 0 4px 16px var(--t-glow), 0 1px 3px rgba(0,0,0,0.06);
}
.badge.earned:hover {
transform: translateY(-3px) scale(1.02);
box-shadow: 0 8px 24px var(--t-glow), 0 2px 6px rgba(0,0,0,0.08);
}
.badge.locked {
opacity: 0.5;
filter: grayscale(0.3);
}
.badge.locked:hover {
opacity: 0.65;
transform: scale(1.02);
}
.badge.just-earned {
animation: earn-pop 0.6s cubic-bezier(0.34, 1.56, 0.64, 1), earn-shimmer 1.5s ease-out 0.6s;
}
@keyframes earn-pop {
0% { transform: scale(0.3) rotate(-8deg); opacity: 0; }
60% { transform: scale(1.12) rotate(2deg); opacity: 1; }
100% { transform: scale(1) rotate(0deg); }
}
@keyframes earn-shimmer {
0% { box-shadow: 0 0 0 0 var(--t-glow), 0 4px 16px var(--t-glow); }
50% { box-shadow: 0 0 0 12px transparent, 0 4px 24px var(--t-glow); }
100% { box-shadow: 0 4px 16px var(--t-glow), 0 1px 3px rgba(0,0,0,0.06); }
}
.badge-icon {
width: 58px;
height: 58px;
margin: 0 auto 10px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
--mdc-icon-size: 30px;
background: var(--divider-color, #e0e0e0);
color: var(--secondary-text-color);
transition: transform 0.2s ease;
}
.badge.earned .badge-icon {
background: var(--t);
color: #fff;
box-shadow: 0 3px 10px var(--t-glow);
}
.badge.earned:hover .badge-icon {
transform: scale(1.1) rotate(-5deg);
}
.tier-gold.earned .badge-icon,
.tier-bronze.earned .badge-icon {
color: #2d2000;
}
.badge-tier {
font-size: 0.6rem;
text-transform: uppercase;
letter-spacing: 1.2px;
font-weight: 800;
color: var(--t, var(--secondary-text-color));
margin-bottom: 4px;
}
.badge-name {
font-size: 0.82rem;
font-weight: 700;
color: var(--primary-text-color);
margin-bottom: 3px;
line-height: 1.25;
}
.badge.locked .badge-name { color: var(--secondary-text-color); }
.badge-meta {
font-size: 0.65rem;
color: var(--secondary-text-color);
margin-bottom: 6px;
font-weight: 500;
}
.badge.earned .badge-meta {
color: var(--t);
font-weight: 600;
}
.badge-progress {
height: 7px;
background: var(--divider-color, #ddd);
border-radius: 4px;
overflow: hidden;
margin-top: 8px;
}
.badge-progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--t), color-mix(in srgb, var(--t), white 25%));
border-radius: 4px;
transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.badge-progress-label {
font-size: 0.65rem;
color: var(--secondary-text-color);
margin-top: 4px;
font-weight: 600;
}
.badge-bonus {
position: absolute;
top: -4px;
right: -4px;
background: linear-gradient(135deg, var(--t), color-mix(in srgb, var(--t), black 15%));
color: #fff;
font-size: 0.65rem;
font-weight: 800;
padding: 3px 7px;
border-radius: 999px;
box-shadow: 0 2px 6px rgba(0,0,0,0.15);
z-index: 1;
}
.tier-gold .badge-bonus,
.tier-bronze .badge-bonus {
color: #2d2000;
}
/* ── Empty / error states ── */
.error-state, .empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
color: var(--secondary-text-color);
text-align: center;
}
.error-state { color: var(--error-color, #f44336); }
.error-state ha-icon, .empty-state ha-icon {
--mdc-icon-size: 48px;
margin-bottom: 12px;
opacity: 0.5;
}
/* ── Designed styles (playroom / console / cleanpro) ── */
/* Shared .tmd kit + tokens come from taskmate-design.js styles(). */
.bd-filter { gap: 6px; flex-wrap: wrap; margin-bottom: 11px; }
.bd-chip { cursor: pointer; }
.bd-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(82px, 1fr)); gap: 9px; max-height: 360px; overflow-y: auto; }
.bd-tile {
background: var(--tmd-surface-2);
border-radius: 16px;
padding: 11px 6px;
text-align: center;
position: relative;
}
.bd-tile.locked { opacity: 0.55; }
.bd-bonus {
position: absolute;
top: 6px;
right: 8px;
font-size: 10px;
font-weight: 800;
color: var(--tmd-good);
}
.bd-ic {
width: 44px;
height: 44px;
border-radius: 50%;
margin: 0 auto 6px;
display: grid;
place-items: center;
font-size: 22px;
--mdc-icon-size: 24px;
}
.bd-ic.earned {
background: color-mix(in srgb, var(--bt, var(--tmd-accent)) 22%, var(--tmd-surface));
box-shadow: 0 0 0 3px color-mix(in srgb, var(--bt, var(--tmd-accent)) 55%, transparent);
color: var(--bt, var(--tmd-accent));
}
.bd-ic.locked {
background: var(--tmd-surface);
box-shadow: inset 0 0 0 2px var(--tmd-border);
color: var(--tmd-dim);
}
.bd-name { font-weight: 800; font-size: 11.5px; }
.bd-tile.locked .bd-name { color: var(--tmd-dim); }
.bd-date { font-size: 10px; }
.bd-prog-label { font-size: 10px; margin-top: 3px; }
/* Console tile overrides */
:host([data-tm-design="console"]) .bd-tile { border-radius: 8px; }
:host([data-tm-design="console"]) .bd-tile.earned {
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--bt, var(--tmd-accent)) 50%, transparent),
0 0 16px color-mix(in srgb, var(--bt, var(--tmd-accent)) 22%, transparent);
}
:host([data-tm-design="console"]) .bd-tile.locked {
background: #0b1424;
border: 1px solid var(--tmd-border);
opacity: 1;
}
:host([data-tm-design="console"]) .bd-ic.earned {
background: transparent;
box-shadow: inset 0 0 0 1px var(--bt, var(--tmd-accent)),
0 0 14px color-mix(in srgb, var(--bt, var(--tmd-accent)) 55%, transparent);
}
:host([data-tm-design="console"]) .bd-ic.locked {
box-shadow: inset 0 0 0 1px var(--tmd-border);
}
:host([data-tm-design="console"]) .bd-name { font-weight: 700; font-size: 11px; }
/* Clean Pro tile overrides */
:host([data-tm-design="cleanpro"]) .bd-tile {
background: transparent;
border: 1px solid var(--tmd-border);
border-radius: 11px;
}
:host([data-tm-design="cleanpro"]) .bd-tile.locked { background: var(--tmd-surface-2); }
:host([data-tm-design="cleanpro"]) .bd-ic { width: 40px; height: 40px; font-size: 20px; }
:host([data-tm-design="cleanpro"]) .bd-ic.earned {
background: color-mix(in srgb, var(--bt, var(--tmd-accent)) 14%, transparent);
box-shadow: none;
}
:host([data-tm-design="cleanpro"]) .bd-ic.locked { box-shadow: none; }
:host([data-tm-design="cleanpro"]) .bd-name { font-weight: 600; }
`;
const tokens = window.__taskmate_design && window.__taskmate_design.styles
? window.__taskmate_design.styles() : null;
return tokens ? [tokens, base] : base;
}
setConfig(config) {
if (!config.entity) {
throw new Error("taskmate-badges-card: 'entity' is required (e.g. sensor.taskmate_badges_mia)");
}
this.config = { title: null, ...config };
// Extract child_id from entity name for event matching:
// sensor.taskmate_badges_<slug> → slug
const match = config.entity.match(/sensor\.taskmate_badges_(.+)/);
this._childSlug = match ? match[1] : null;
}
getCardSize() { return 5; }
static getConfigElement() { return document.createElement("taskmate-badges-card-editor"); }
static getStubConfig() {
return { entity: "sensor.taskmate_overview" };
}
_tierLabel(tier) {
return this._t("badge.tier_" + (tier || "bronze"));
}
_formatDate(iso) {
if (!iso) return "";
try {
return new Date(iso).toLocaleDateString(undefined, { day: "numeric", month: "short" });
} catch {
return iso;
}
}
render() {
if (!this.hass || !this.config) return html``;
const entity = this.hass.states[this.config.entity];
if (!entity) {
return html`<ha-card><div class="error-state"><ha-icon icon="mdi:alert-circle"></ha-icon><div>${this._t('badges.entity_not_found', { entity: this.config.entity })}</div></div></ha-card>`;
}
if (entity.state === "unavailable" || entity.state === "unknown") {
return html`<ha-card><div class="error-state"><ha-icon icon="mdi:alert-circle"></ha-icon><div>${this._t('common.unavailable')}</div></div></ha-card>`;
}
const attrs = entity.attributes || {};
const isPreview = !attrs.earned && !attrs.available;
const earned = isPreview
? [{ id: "s1", name: "Early Bird", icon: "mdi:weather-sunny", tier: "gold", point_bonus: 10, earned_at: new Date().toISOString() },
{ id: "s2", name: "Streak Star", icon: "mdi:fire", tier: "silver", point_bonus: 5, earned_at: new Date().toISOString() }]
: (attrs.earned || []);
const available = isPreview
? [{ id: "s3", name: "Helping Hand", icon: "mdi:hand-heart", tier: "bronze", point_bonus: 5, progress_pct: 60, progress_label: "3 / 5" }]
: (attrs.available || []);
const childName = isPreview ? "Child" : (attrs.child_name || attrs.name || "");
const childAvatar = attrs.child_avatar || "mdi:account-circle";
const totalBadges = attrs.total_badges || (earned.length + available.length);
const filter = this._filterTier;
const filteredEarned = filter === "all" ? earned : earned.filter(b => b.tier === filter);
const filteredLocked = filter === "all" ? available : available.filter(b => b.tier === filter);
const latestEarned = earned.length > 0 ? earned[0] : null;
const countLabel = earned.length > 0 && latestEarned
? this._t("badges.count_label_latest", { earned: earned.length, total: totalBadges, latest: latestEarned.name })
: this._t("badges.count_label", { earned: earned.length, total: totalBadges });
const title = this.config.title || (childName ? this._t("badges.title_with_name", { name: childName }) : this._t("badges.default_title"));
const design = window.__taskmate_design
? window.__taskmate_design.apply(this, this.hass, this.config, this.config.entity)
: "classic";
if (design !== "classic") {
return this._renderDesigned(design, {
title, childName, earned, available, totalBadges,
filteredEarned, filteredLocked, filter,
});
}
return html`
<ha-card>
<div class="badges-head">
<div class="badges-head-left">
<div class="child-avatar">
<ha-icon icon="${childAvatar}"></ha-icon>
</div>
<div>
<div class="badges-title">${title}</div>
<div class="badges-count">${countLabel}</div>
</div>
</div>
<select class="badges-filter"
.value=${filter}
@change=${(e) => { this._filterTier = e.target.value; }}>
<option value="all">${this._t("badges.all_tiers")}</option>
<option value="bronze">${this._t("badge.tier_bronze")}</option>
<option value="silver">${this._t("badge.tier_silver")}</option>
<option value="gold">${this._t("badge.tier_gold")}</option>
<option value="platinum">${this._t("badge.tier_platinum")}</option>
</select>
</div>
<div class="badge-grid">
${filteredEarned.map(b => this._renderEarned(b))}
${filteredLocked.map(b => this._renderLocked(b))}
</div>
</ha-card>
`;
}
_renderEarned(b) {
const justEarned = this._justEarned && String(this._justEarned) === String(b.id);
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>` : ''}
<div class="badge-icon">
<ha-icon icon="${b.icon || 'mdi:medal'}"></ha-icon>
</div>
<div class="badge-tier">${this._tierLabel(b.tier)}</div>
<div class="badge-name">${b.name}</div>
<div class="badge-meta">${this._t("badge.earned_date", { date: this._formatDate(b.earned_at) })}</div>
</div>
`;
}
_renderLocked(b) {
// progress: either provided directly, or computed from closest_criterion
const pct = (b.progress_pct != null) ? b.progress_pct
: (b.closest_criterion ? Math.min(100, Math.round((b.closest_criterion.current / b.closest_criterion.target) * 100)) : 0);
const progressLabel = b.progress_label
|| (b.closest_criterion ? `${b.closest_criterion.current} / ${b.closest_criterion.target}` : null);
return html`
<div class="badge locked tier-${b.tier}">
${b.point_bonus > 0 ? html`<div class="badge-bonus" style="opacity:0.4">+${b.point_bonus}</div>` : ''}
<div class="badge-icon">
<ha-icon icon="${b.icon || 'mdi:medal-outline'}"></ha-icon>
</div>
<div class="badge-tier">${this._tierLabel(b.tier)}</div>
<div class="badge-name">${b.name}</div>
${progressLabel ? html`
<div class="badge-progress">
<div class="badge-progress-fill" style="width:${pct}%"></div>
</div>
<div class="badge-progress-label">${progressLabel}</div>
` : ''}
</div>
`;
}
}
// Designed-style render methods are added to the prototype below.
TaskMateBadgesCard.prototype._tierTone = function (tier) {
// Map badge tiers onto shared design tokens (kit defines --tmd-gold).
switch (tier) {
case "platinum": return "var(--tmd-accent)";
case "gold": return "var(--tmd-gold)";
case "silver": return "var(--tmd-dim)";
case "bronze": return "var(--tmd-c6)";
default: return "var(--tmd-accent)";
}
};
TaskMateBadgesCard.prototype._designFilters = function (filter) {
const tiers = ["all", "bronze", "silver", "gold", "platinum"];
return html`
<div class="row bd-filter">
${tiers.map((t) => html`
<button class="chip bd-chip ${t === filter ? "soft" : ""}"
@click="${() => { this._filterTier = t; this.requestUpdate(); }}">
${t === "all" ? this._t("badges.all_tiers") : this._tierLabel(t)}
</button>`)}
</div>`;
};
TaskMateBadgesCard.prototype._designEarnedTile = function (b) {
const tone = this._tierTone(b.tier);
const a = b.icon || "mdi:medal";
const icon = a.startsWith("mdi:")
? html`<ha-icon icon="${a}"></ha-icon>` : a;
return html`
<div class="bd-tile earned" style="--bt:${tone}">
${b.point_bonus > 0 ? html`<div class="bd-bonus">+${b.point_bonus}</div>` : ""}
<div class="bd-ic earned" style="--bt:${tone}">${icon}</div>
<div class="bd-name">${b.name}</div>
<div class="muted bd-date">${this._formatDate(b.earned_at)}</div>
</div>`;
};
TaskMateBadgesCard.prototype._designLockedTile = function (b) {
const tone = this._tierTone(b.tier);
const pct = (b.progress_pct != null) ? b.progress_pct
: (b.closest_criterion ? Math.min(100, Math.round((b.closest_criterion.current / b.closest_criterion.target) * 100)) : 0);
const progressLabel = b.progress_label
|| (b.closest_criterion ? `${b.closest_criterion.current} / ${b.closest_criterion.target}` : null);
const a = b.icon || "mdi:medal-outline";
const icon = a.startsWith("mdi:") ? html`<ha-icon icon="${a}"></ha-icon>` : a;
return html`
<div class="bd-tile locked" style="--bt:${tone}">
${b.point_bonus > 0 ? html`<div class="bd-bonus" style="opacity:0.45">+${b.point_bonus}</div>` : ""}
<div class="bd-ic locked">${icon}</div>
<div class="bd-name">${b.name}</div>
${progressLabel ? html`
<div class="bar" style="height:7px;margin-top:6px"><i style="width:${pct}%"></i></div>
<div class="muted bd-prog-label">${progressLabel}</div>` : ""}
</div>`;
};
TaskMateBadgesCard.prototype._renderDesigned = function (design, d) {
const hd = _safeColor(this.config.header_color, DESIGN_HEADER);
const ic = design === "console" ? "✦" : design === "cleanpro" ? "◈" : "🎖️";
const sub = d.childName;
const countPill = this._t("badges.count_label", { earned: d.earned.length, total: d.totalBadges });
return html`
<ha-card class="tmd" style="--hd:${hd}">
<div class="tmd-hd">
<span class="ic">${ic}</span>
<span class="tt">${d.title}${sub ? html`<small>${sub}</small>` : ""}</span>
<span class="pill">${countPill}</span>
</div>
<div class="tmd-bd">
${this._designFilters(d.filter)}
${(d.filteredEarned.length + d.filteredLocked.length) === 0
? html`<div class="tmd-empty">${this._t("badges.count_label", { earned: 0, total: d.totalBadges })}</div>`
: html`<div class="bd-grid">
${d.filteredEarned.map((b) => this._designEarnedTile(b))}
${d.filteredLocked.map((b) => this._designLockedTile(b))}
</div>`}
</div>
</ha-card>`;
};
// Editor: entity + per-card design override.
class TaskMateBadgesCardEditor extends LitElement {
static get properties() {
return { hass: { type: Object }, config: { type: Object } };
}
setConfig(config) { this.config = config; }
_t(key, params) {
const fn = window.__taskmate_localize;
return fn ? fn(this.hass, key, params) : key;
}
_buildSchema() {
return [
{ name: 'entity', selector: { entity: { domain: 'sensor' } } },
{ name: 'title', selector: { text: {} } },
{
name: 'card_design',
selector: {
select: {
options: window.__taskmate_design
? window.__taskmate_design.editorOptions(this._t.bind(this))
: [{ value: 'global', label: 'Use global default' }],
mode: 'dropdown',
},
},
},
];
}
_computeLabel = (entry) => {
const labels = {
entity: this._t('common.entity'),
title: this._t('common.editor.card_title'),
card_design: this._t('common.design.field_label'),
};
return labels[entry.name] ?? entry.name;
};
render() {
if (!this.config) return html``;
const data = {
entity: this.config.entity || '',
title: this.config.title || '',
card_design: this.config.card_design || 'global',
};
return html`
<p style="padding:0 0 8px;color:var(--secondary-text-color)">${this._t('badges.editor_help')}</p>
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${this._buildSchema()}
.computeLabel=${this._computeLabel}
@value-changed=${this._formChanged}
></ha-form>
`;
}
_formChanged(e) {
const newValues = e.detail.value || {};
const newConfig = { ...this.config };
for (const [key, value] of Object.entries(newValues)) {
if (value === '' || value === null || value === undefined) {
delete newConfig[key];
} else if (key === 'card_design' && value === 'global') {
delete newConfig[key];
} else {
newConfig[key] = value;
}
}
this.dispatchEvent(new CustomEvent('config-changed', {
detail: { config: newConfig }, bubbles: true, composed: true,
}));
}
}
customElements.define("taskmate-badges-card", TaskMateBadgesCard);
customElements.define("taskmate-badges-card-editor", TaskMateBadgesCardEditor);
window.customCards = window.customCards || [];
window.customCards.push({
type: "taskmate-badges-card",
name: "TaskMate Badges",
description: "Achievement badge grid for a single child",
preview: true,
getEntitySuggestion: (hass, entityId) =>
window.__taskmate_suggest(hass, entityId, "taskmate-badges-card", "overview"),
});
const _tmBadgesVersion = new URLSearchParams(
Array.from(document.querySelectorAll('script[src*="/taskmate-badges-card.js"]'))
.map(s => s.src.split("?")[1]).find(Boolean) || ""
).get("v") || "?";
console.info(
"%c TASKMATE BADGES CARD %c v" + _tmBadgesVersion + " ",
"background:#9b59b6;color:white;font-weight:bold;padding:2px 4px;border-radius:4px 0 0 4px;",
"background:#2c3e50;color:white;font-weight:bold;padding:2px 4px;border-radius:0 4px 4px 0;"
);