/**
* TaskMate Reward Progress Card
* Full-screen motivational display showing a single reward's progress.
* Designed for wall tablets as a persistent motivation display.
*
* 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 _safeColor = (c, d) => (typeof c === "string" && /^#[0-9a-fA-F]{3,8}$/.test(c) ? c : d);
class TaskMateRewardProgressCard extends LitElement {
static get properties() {
return { hass: { type: Object }, config: { type: Object } };
}
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; }
ha-card { overflow: hidden; }
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
background: var(--taskmate-header-bg, var(--primary-color));
color: var(--text-primary-color, #fff);
gap: 12px;
}
.header-content { display: flex; align-items: center; gap: 10px; min-width: 0; }
.header-icon { --mdc-icon-size: 26px; opacity: 0.9; flex-shrink: 0; }
.header-title {
font-size: 1.1rem; font-weight: 600;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.card-content { padding: 20px 18px; }
/* Reward hero section */
.reward-hero {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 12px 0 20px;
gap: 10px;
}
.reward-icon-wrap {
width: 90px;
height: 90px;
border-radius: 50%;
background: var(--primary-color);
display: flex;
align-items: center;
justify-content: center;
box-shadow: var(--ha-card-box-shadow, none);
animation: hero-float 3s ease-in-out infinite;
}
@keyframes hero-float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
.reward-icon-wrap ha-icon { --mdc-icon-size: 52px; color: var(--text-primary-color, #fff); }
.reward-name {
font-size: 1.6rem;
font-weight: 700;
color: var(--primary-text-color);
line-height: 1.2;
}
.reward-description {
font-size: 0.9rem;
color: var(--secondary-text-color);
max-width: 280px;
}
/* Children progress blocks */
.children-section {
display: flex;
flex-direction: column;
gap: 16px;
}
.child-progress-block {
background: var(--secondary-background-color, #f8f8f8);
border-radius: 16px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 10px;
}
.child-progress-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.child-progress-left {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.child-avatar {
width: 40px;
height: 40px;
min-width: 40px;
border-radius: 50%;
background: var(--primary-color);
display: flex;
align-items: center;
justify-content: center;
}
.child-avatar ha-icon { --mdc-icon-size: 24px; color: var(--text-primary-color, #fff); }
.child-progress-name {
font-size: 1rem;
font-weight: 600;
color: var(--primary-text-color);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.child-points-label {
font-size: 0.8rem;
color: var(--secondary-text-color);
}
.child-progress-cost {
text-align: right;
flex-shrink: 0;
}
.cost-label {
font-size: 0.75rem;
color: var(--secondary-text-color);
text-transform: uppercase;
letter-spacing: 0.06em;
}
.cost-value {
font-size: 1.2rem;
font-weight: 700;
color: var(--primary-color);
display: flex;
align-items: center;
gap: 3px;
justify-content: flex-end;
}
.cost-value ha-icon { --mdc-icon-size: 16px; color: #ca8a04; }
/* Big animated progress bar */
.big-progress-wrap {
display: flex;
flex-direction: column;
gap: 6px;
}
.big-progress-bar {
height: 22px;
background: var(--divider-color, #e0e0e0);
border-radius: 11px;
overflow: hidden;
position: relative;
}
.big-progress-fill {
height: 100%;
border-radius: 11px;
transition: width 0.8s cubic-bezier(0.34, 1.56, 0.64, 1);
position: relative;
overflow: hidden;
}
.big-progress-fill::after {
content: '';
position: absolute;
top: 0; left: -100%;
width: 60%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% { left: -100%; }
100% { left: 200%; }
}
.big-progress-fill.affordable {
background: var(--success-color, #4caf50);
}
.big-progress-fill.close {
background: var(--warning-color, #ff9800);
}
.big-progress-fill.far {
background: var(--primary-color);
}
.big-progress-fill.complete {
background: var(--success-color, #4caf50);
}
.progress-stat-row {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
font-size: 0.82rem;
}
.progress-have {
font-weight: 600;
color: var(--primary-text-color);
}
.progress-need {
color: var(--secondary-text-color);
}
.progress-pct {
font-weight: 700;
font-size: 0.95rem;
}
.progress-pct.affordable { color: var(--success-color, #4caf50); }
.progress-pct.close { color: var(--warning-color, #ff9800); }
.progress-pct.far { color: var(--primary-color); }
/* Can afford badge */
.can-afford-badge {
display: inline-flex;
align-items: center;
gap: 5px;
background: color-mix(in srgb, var(--success-color, #4caf50) 15%, transparent);
color: var(--success-color, #4caf50);
border-radius: 20px;
padding: 5px 12px;
font-size: 0.85rem;
font-weight: 700;
animation: pulse-green 2s ease-in-out infinite;
align-self: center;
}
.can-afford-badge ha-icon { --mdc-icon-size: 16px; }
@keyframes pulse-green {
0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--success-color, #4caf50) 30%, transparent); }
50% { box-shadow: 0 0 0 6px color-mix(in srgb, var(--success-color, #4caf50) 0%, transparent); }
}
/* Jackpot section */
.jackpot-badge {
display: inline-flex;
align-items: center;
gap: 5px;
background: #ca8a04;
color: var(--text-primary-color, #fff);
border-radius: 20px;
padding: 4px 12px;
font-size: 0.8rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
align-self: flex-start;
}
.jackpot-badge ha-icon { --mdc-icon-size: 14px; }
/* Availability badges (low stock / sold out / expiring / expired) */
.availability-badge {
display: inline-flex;
align-items: center;
border-radius: 20px;
padding: 3px 10px;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
align-self: flex-start;
}
.availability-badge.badge-sold-out {
background: color-mix(in srgb, var(--secondary-text-color, #757575) 20%, transparent);
color: var(--secondary-text-color, #757575);
}
.availability-badge.badge-expired {
background: color-mix(in srgb, var(--secondary-text-color, #757575) 20%, transparent);
color: var(--secondary-text-color, #757575);
}
.availability-badge.badge-low-stock {
background: color-mix(in srgb, var(--error-color, #db4437) 18%, transparent);
color: var(--error-color, #db4437);
}
.availability-badge.badge-expiring-soon {
background: color-mix(in srgb, var(--warning-color, #ff9800) 20%, transparent);
color: var(--warning-color, #ff9800);
}
.jackpot-pool {
display: flex;
flex-direction: column;
gap: 8px;
background: color-mix(in srgb, #ca8a04 8%, transparent);
border: 1px solid color-mix(in srgb, #ca8a04 25%, transparent);
border-radius: 12px;
padding: 12px;
}
.jackpot-pool-title {
font-size: 0.8rem;
font-weight: 600;
color: var(--secondary-text-color);
text-transform: uppercase;
letter-spacing: 0.06em;
}
.jackpot-contributors {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.jackpot-contributor {
display: flex;
align-items: center;
gap: 6px;
background: var(--card-background-color, #fff);
border-radius: 20px;
padding: 4px 10px 4px 6px;
font-size: 0.82rem;
font-weight: 600;
color: var(--primary-text-color);
}
.jackpot-contributor .mini-avatar {
width: 22px; height: 22px;
border-radius: 50%;
background: #ca8a04;
display: flex; align-items: center; justify-content: center;
}
.jackpot-contributor .mini-avatar ha-icon { --mdc-icon-size: 13px; color: var(--text-primary-color, #fff); }
/* Error / empty */
.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, #db4437); }
.error-state ha-icon, .empty-state ha-icon { --mdc-icon-size: 48px; margin-bottom: 12px; opacity: 0.5; }
/* Responsive */
@media (max-width: 480px) {
.card-content { padding: 14px 12px; }
.reward-icon-wrap { width: 70px; height: 70px; }
.reward-icon-wrap ha-icon { --mdc-icon-size: 40px; }
.reward-name { font-size: 1.3rem; }
.big-progress-bar { height: 18px; border-radius: 9px; }
.child-progress-block { padding: 12px; }
}
/* ── Designed (playroom / console / cleanpro) — shared .tmd kit comes
from taskmate-design.js styles(); only card-specific layout below. ── */
.rp-wrap { text-align: center; }
.rp-hero-emoji { font-size: 60px; line-height: 1; margin: 4px 0;
filter: drop-shadow(0 6px 10px rgba(0,0,0,.15)); }
.rp-hero-icon { width: 84px; height: 84px; border-radius: 50%; margin: 4px auto;
display: grid; place-items: center; background: var(--tmd-accent);
box-shadow: var(--tmd-shadow); }
.rp-hero-icon ha-icon { --mdc-icon-size: 46px; color: #fff; }
:host([data-tm-design="console"]) .rp-hero-emoji {
filter: drop-shadow(0 0 18px color-mix(in srgb, var(--tmd-accent) 70%, transparent)); }
.rp-name { font-size: 24px; color: var(--tmd-accent); }
.rp-desc { font-size: 13px; margin-top: 4px; max-width: 280px; margin-left: auto; margin-right: auto; }
.rp-jackpot { margin-top: 8px; background: var(--tmd-gold); color: #3a2e26;
border-color: transparent; font-weight: 800; text-transform: uppercase;
letter-spacing: 0.04em; font-size: 11px; }
.rp-avail { margin-top: 8px; font-weight: 800; text-transform: uppercase;
letter-spacing: 0.04em; font-size: 11px; border-color: transparent; }
.rp-avail-dim { background: color-mix(in srgb, var(--tmd-dim) 20%, transparent); color: var(--tmd-dim); }
.rp-avail-bad { background: color-mix(in srgb, var(--tmd-bad) 18%, transparent); color: var(--tmd-bad); }
.rp-avail-warn { background: color-mix(in srgb, var(--tmd-warn) 20%, transparent); color: var(--tmd-warn); }
.rp-cost { margin-top: 8px; background: var(--tmd-gold); color: #3a2e26;
border-color: transparent; font-weight: 800; }
.rp-bar { height: 24px; margin-top: 16px; }
.rp-pct { color: var(--tmd-accent); }
.rp-pct-c { font-size: 38px; margin-top: 12px; }
.rp-need-c { font-weight: 800; font-size: 15px; margin-top: 2px; }
.rp-stat { justify-content: space-between; margin-top: 10px; }
.rp-stat .rp-pct { font-size: 34px; }
.rp-stat-r { text-align: right; }
.rp-frac { font-size: 18px; }
.rp-need { font-size: 12px; }
.rp-kids { grid-template-columns: repeat(var(--n, 3), 1fr); gap: 9px; margin-top: 16px; }
@media (max-width: 430px) { .rp-kids { grid-template-columns: repeat(auto-fit, minmax(64px, 1fr)); } }
.rp-kid { background: var(--tmd-surface-2); border: 1px solid var(--tmd-border);
border-radius: var(--tmd-radius-sm); padding: 10px; display: flex;
flex-direction: column; align-items: center; gap: 4px; }
.rp-kid .av { margin: 0 auto 2px; }
.rp-kid-pts { font-size: 20px; color: var(--tmd-accent); }
.rp-kid-pts span { font-size: 12px; margin-left: 1px; }
.rp-kid-name { font-size: 12px; font-weight: 800; }
.rp-kid-bar { width: 100%; height: 7px; margin-top: 3px; }
`;
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(this._t('reward_progress.error.entity_required'));
this.config = {
title: null,
reward_id: null,
child_id: null,
...config,
};
}
getCardSize() { return 5; }
static getConfigElement() { return document.createElement("taskmate-reward-progress-card-editor"); }
static getStubConfig() {
return { entity: "sensor.taskmate_overview", title: "Reward Goal" };
}
render() {
if (!this.hass || !this.config) return html``;
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);
const entity = this.hass.states[this.config.entity];
if (!entity) return html`