/** * 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`
${this._t('common.entity_not_found', { entity: this.config.entity })}
`; if (entity.state === "unavailable" || entity.state === "unknown") return html`
${this._t('reward_progress.unavailable')}
`; const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config.entity)) || entity.attributes || {}; const rewards = attrs.rewards || []; const children = attrs.children || []; const pointsIcon = attrs.points_icon || "mdi:star"; const pointsName = attrs.points_name || this._t("common.points"); // Pick reward let reward = this.config.reward_id ? rewards.find(r => r.id === this.config.reward_id) : rewards[0]; if (!reward) return html`
${this._t('reward_progress.no_rewards')}
`; // Which children to show let showChildren = children; if (this.config.child_id) showChildren = children.filter(c => c.id === this.config.child_id); if (reward.assigned_to?.length) showChildren = showChildren.filter(c => reward.assigned_to.includes(c.id)); if (!showChildren.length) showChildren = children; const isJackpot = reward.is_jackpot; const isSoldOut = reward.is_sold_out === true; const isExpired = reward.is_expired === true; const daysUntilExpiry = (typeof reward.days_until_expiry === 'number') ? reward.days_until_expiry : null; let availabilityLabel = ''; let availabilityClass = ''; if (isExpired) { availabilityLabel = this._t('rewards.expired'); availabilityClass = 'badge-expired'; } else if (isSoldOut) { availabilityLabel = this._t('rewards.sold_out'); availabilityClass = 'badge-sold-out'; } else if (typeof reward.quantity === 'number' && reward.quantity > 0 && reward.quantity <= 3) { availabilityLabel = this._t('rewards.only_n_left', { count: reward.quantity }); availabilityClass = 'badge-low-stock'; } else if (typeof daysUntilExpiry === 'number' && daysUntilExpiry >= 0 && daysUntilExpiry <= 7) { availabilityLabel = daysUntilExpiry === 0 ? this._t('rewards.expires_today') : (daysUntilExpiry === 1 ? this._t('rewards.expires_in_days', { count: daysUntilExpiry }) : this._t('rewards.expires_in_days_plural', { count: daysUntilExpiry })); availabilityClass = 'badge-expiring-soon'; } const headerColor = _safeColor(this.config.header_color, ''); const headerStyle = headerColor ? `--taskmate-header-bg: ${headerColor};` : ''; return html` ${headerStyle ? html`` : ''}
${this.config.title || this._t('reward_progress.default_title')}
${reward.name}
${reward.description ? html`
${reward.description}
` : ''} ${availabilityLabel ? html`
${availabilityLabel}
` : ''} ${isJackpot ? html`
${this._t('reward_progress.jackpot_reward')}
` : ''}
${isJackpot ? this._renderJackpot(reward, showChildren, pointsIcon, pointsName) : html`
${showChildren.map(child => this._renderChildProgress(child, reward, pointsIcon, pointsName))}
`}
`; } _renderChildProgress(child, reward, pointsIcon, pointsName) { const cost = reward.calculated_costs?.[child.id] ?? reward.cost; const have = child.points || 0; const pct = Math.min(100, Math.round((have / cost) * 100)); const canAfford = have >= cost; const close = pct >= 70; const cls = canAfford ? "complete" : close ? "close" : "far"; const pctCls = canAfford ? "affordable" : close ? "close" : "far"; return html`
${child.name}
${have} ${pointsName}
${this._t('common.goal')}
${cost}
${have} / ${cost} ${pointsName} ${canAfford ? html`${this._t('reward_progress.ready_to_claim_emoji')}` : html`${this._t('reward_progress.more_needed', { amount: cost - have })}`} ${pct}%
${canAfford ? html`
${this._t('reward_progress.ready_to_claim')}
` : ''}
`; } _renderJackpot(reward, children, pointsIcon, pointsName) { const cost = reward.calculated_costs ? Object.values(reward.calculated_costs)[0] ?? reward.cost : reward.cost; const totalHave = children.reduce((s, c) => s + (c.points || 0), 0); const pct = Math.min(100, Math.round((totalHave / cost) * 100)); const canAfford = totalHave >= cost; const close = pct >= 70; const cls = canAfford ? "complete" : close ? "close" : "far"; const pctCls = canAfford ? "affordable" : close ? "close" : "far"; return html`
${this._t('reward_progress.combined_points_pool')}
${children.map(child => html`
${child.name}: ${child.points}
`)}
${this._t('reward_progress.total_pool')}
${totalHave} ${pointsName} ${this._t('reward_progress.combined')}
${this._t('common.goal')}
${cost}
${totalHave} / ${cost} ${pointsName} ${canAfford ? html`${this._t('reward_progress.ready')}` : html`${this._t('reward_progress.more_needed', { amount: cost - totalHave })}`} ${pct}%
${canAfford ? html`
${this._t('reward_progress.ready_to_claim')}
` : ''}
`; } /* ══════════════════════════════════════════════════════════════════════ DESIGNED STYLES (playroom / console / cleanpro) — see taskmate-design.js Ported from docs/design/redesigns (§card-reward-progress). Wall display for ONE reward, FIXED cost only. ══════════════════════════════════════════════════════════════════════ */ _designTone(i) { return `var(--tmd-c${(i % 6) + 1})`; } _av(child, tone, size) { const a = (child && child.avatar) || ""; const inner = a.startsWith("mdi:") ? html`` : a ? html`${child.name}` : ((child && child.name) || "?").split(" ").map(w => w[0]).slice(0, 2).join("").toUpperCase(); return html`
${inner}
`; } _renderDesigned(design) { const hd = _safeColor(this.config.header_color, '#00897b'); const wrap = (body, icon) => html`
🎯 ${this.config.title || this._t('reward_progress.default_title')}${this._t('common.goal')}
${body}
`; const entity = this.hass.states[this.config.entity]; if (!entity) return wrap(html`
${this._t('common.entity_not_found', { entity: this.config.entity })}
`); if (entity.state === "unavailable" || entity.state === "unknown") return wrap(html`
${this._t('reward_progress.unavailable')}
`); const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config.entity)) || entity.attributes || {}; const rewards = attrs.rewards || []; const children = attrs.children || []; const pointsName = attrs.points_name || this._t("common.points"); let reward = this.config.reward_id ? rewards.find(r => r.id === this.config.reward_id) : rewards[0]; if (!reward) return wrap(html`
${this._t('reward_progress.no_rewards')}
`); let showChildren = children; if (this.config.child_id) showChildren = children.filter(c => c.id === this.config.child_id); if (reward.assigned_to?.length) showChildren = showChildren.filter(c => reward.assigned_to.includes(c.id)); if (!showChildren.length) showChildren = children; const isJackpot = reward.is_jackpot; let cost, have, contributors; if (isJackpot) { cost = reward.calculated_costs ? (Object.values(reward.calculated_costs)[0] ?? reward.cost) : reward.cost; have = showChildren.reduce((s, c) => s + (c.points || 0), 0); contributors = showChildren.map((c, i) => ({ child: c, points: c.points || 0, tone: this._designTone(i) })); } else { const ref = showChildren[0]; cost = (ref && reward.calculated_costs?.[ref.id]) ?? reward.cost; contributors = showChildren.map((c, i) => ({ child: c, points: c.points || 0, tone: this._designTone(i), cost: reward.calculated_costs?.[c.id] ?? reward.cost, })); have = contributors.length === 1 ? contributors[0].points : contributors.reduce((s, c) => s + c.points, 0); if (contributors.length === 1) cost = contributors[0].cost; } const pct = cost > 0 ? Math.min(100, Math.round((have / cost) * 100)) : 0; const remaining = Math.max(0, cost - have); // Availability badge — same precedence as the classic render. const isSoldOut = reward.is_sold_out === true; const isExpired = reward.is_expired === true; const daysUntilExpiry = (typeof reward.days_until_expiry === 'number') ? reward.days_until_expiry : null; let availabilityLabel = ''; let availabilityTone = ''; if (isExpired) { availabilityLabel = this._t('rewards.expired'); availabilityTone = 'dim'; } else if (isSoldOut) { availabilityLabel = this._t('rewards.sold_out'); availabilityTone = 'dim'; } else if (typeof reward.quantity === 'number' && reward.quantity > 0 && reward.quantity <= 3) { availabilityLabel = this._t('rewards.only_n_left', { count: reward.quantity }); availabilityTone = 'bad'; } else if (typeof daysUntilExpiry === 'number' && daysUntilExpiry >= 0 && daysUntilExpiry <= 7) { availabilityLabel = daysUntilExpiry === 0 ? this._t('rewards.expires_today') : (daysUntilExpiry === 1 ? this._t('rewards.expires_in_days', { count: daysUntilExpiry }) : this._t('rewards.expires_in_days_plural', { count: daysUntilExpiry })); availabilityTone = 'warn'; } return wrap(html`${this._designBody(reward, design, cost, have, pct, remaining, contributors, pointsName, availabilityLabel, availabilityTone)}`); } _designBody(reward, design, cost, have, pct, remaining, contributors, pointsName, availabilityLabel, availabilityTone) { const icon = reward.icon || "mdi:gift"; const isIcon = typeof icon === "string" && icon.startsWith("mdi:"); const hero = isIcon ? html`
` : html`
${icon}
`; return html`
${hero}
${reward.name}
${reward.description ? html`
${reward.description}
` : ''} ${reward.is_jackpot ? html`
🎰 ${this._t('reward_progress.jackpot_reward')}
` : ''} ${availabilityLabel ? html`
${availabilityLabel}
` : ''}
⭐ ${cost} ${pointsName}
${design === "cleanpro" ? html`
${pct}%
${have} / ${cost}
${this._t('reward_progress.more_needed', { amount: remaining })}
` : html`
${pct}%
${remaining > 0 ? this._t('reward_progress.more_needed', { amount: remaining }) : this._t('reward_progress.ready_to_claim')}
`}
${contributors.map((c) => html`
${this._av(c.child, c.tone, 36)}
${c.points}
${c.child.name}
${design === "cleanpro" && !reward.is_jackpot && c.cost > 0 ? html`
` : ''}
`)}
`; } } class TaskMateRewardProgressCardEditor extends LitElement { static get properties() { return { hass: { type: Object }, config: { type: Object } }; } _t(key, params) { const fn = window.__taskmate_localize; return fn ? fn(this.hass, key, params) : key; } static get styles() { return css` :host { display: block; } ha-form { display: block; margin-bottom: 16px; } .colour-field { display: flex; flex-direction: column; gap: 8px; padding: 12px 16px; border: 1px solid var(--outline-color, var(--divider-color, #e0e0e0)); border-radius: 4px; background: var(--mdc-text-field-fill-color, var(--card-background-color)); } .colour-field-label { font-size: 0.82rem; color: var(--primary-color); font-weight: 500; } .colour-field-body { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; } .colour-swatch-wrapper { position: relative; width: 36px; height: 36px; border-radius: 50%; overflow: hidden; cursor: pointer; border: 2px solid var(--divider-color, #e0e0e0); flex-shrink: 0; } .colour-swatch-wrapper input[type="color"] { position: absolute; inset: 0; opacity: 0; cursor: pointer; border: 0; padding: 0; } .colour-swatch-preview { position: absolute; inset: 0; pointer-events: none; } .colour-hex { font-family: var(--code-font-family, monospace); font-size: 0.85rem; color: var(--secondary-text-color); min-width: 70px; } .colour-presets { display: flex; gap: 6px; flex-wrap: wrap; } .preset-swatch { width: 22px; height: 22px; border-radius: 50%; cursor: pointer; border: 2px solid var(--divider-color, #e0e0e0); transition: transform 0.1s; padding: 0; } .preset-swatch:hover { transform: scale(1.15); } .preset-swatch.active { border-color: var(--primary-text-color); box-shadow: 0 0 0 2px var(--primary-color); } .colour-reset { font-size: 0.78rem; color: var(--secondary-text-color); background: none; border: 1px solid var(--divider-color, #e0e0e0); border-radius: 4px; padding: 4px 10px; cursor: pointer; margin-left: auto; } .colour-helper { color: var(--secondary-text-color); font-size: 0.82rem; line-height: 1.3; } `; } setConfig(config) { this.config = config; } _buildSchema() { const attrs = (window.__taskmate_attrs && window.__taskmate_attrs(this.hass, this.config?.entity)) || (this.config?.entity ? this.hass?.states?.[this.config.entity]?.attributes : null) || {}; const rewards = attrs.rewards || []; const children = attrs.children || []; return [ { name: 'entity', selector: { entity: { domain: 'sensor' } } }, { name: 'title', selector: { text: {} } }, { name: 'reward_id', selector: { select: { options: [ { value: '', label: this._t('reward_progress.editor.first_available') }, ...rewards.map((r) => ({ value: r.id, label: r.name })), ], mode: 'dropdown', }, }, }, { name: 'child_id', selector: { select: { options: [ { value: '', label: this._t('reward_progress.editor.child_filter_all') }, ...children.map((c) => ({ value: c.id, label: c.name })), ], mode: 'dropdown', }, }, }, { 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.editor.overview_entity'), title: this._t('common.editor.card_title'), reward_id: this._t('reward_progress.editor.reward_to_display'), child_id: this._t('common.editor.filter_by_child'), card_design: this._t('common.design.field_label'), }; return labels[entry.name] ?? entry.name; }; _computeHelper = (entry) => { const helpers = { entity: this._t('common.editor.overview_entity_helper'), reward_id: this._t('reward_progress.editor.reward_helper'), child_id: this._t('reward_progress.editor.child_helper'), }; return helpers[entry.name] ?? ''; }; render() { if (!this.hass || !this.config) return html``; const data = { entity: this.config.entity || '', title: this.config.title || '', reward_id: this.config.reward_id || '', child_id: this.config.child_id || '', card_design: this.config.card_design || 'global', }; return html` ${this._renderColourPicker('header_color', '')} `; } _renderColourPicker(key, defaultValue) { const d = window.__taskmate_design; const current = this.config[key] || defaultValue; if (!d || !d.colourPicker) return html``; return d.colourPicker({ defaultValue, current, label: this._t('common.editor.header_colour'), helper: this._t('common.editor.header_colour_helper'), resetLabel: this._t('common.reset'), onInput: (v) => this._update(key, v), onPreset: (v) => this._update(key, v), onReset: () => this._update(key, defaultValue), }); } _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, })); } _update(key, value) { const cfg = { ...this.config, [key]: value }; if (!value) delete cfg[key]; this.dispatchEvent(new CustomEvent('config-changed', { detail: { config: cfg }, bubbles: true, composed: true })); } } customElements.define("taskmate-reward-progress-card", TaskMateRewardProgressCard); customElements.define("taskmate-reward-progress-card-editor", TaskMateRewardProgressCardEditor); window.customCards = window.customCards || []; window.customCards.push({ type: "taskmate-reward-progress-card", name: "TaskMate Reward Progress", description: "Full-screen motivational reward progress display", preview: true, getEntitySuggestion: (hass, entityId) => window.__taskmate_suggest(hass, entityId, "taskmate-reward-progress-card", "overview"), }); // Version is injected by the HA resource URL (?v=x.x.x) and read from the DOM const _tmVersion = new URLSearchParams( Array.from(document.querySelectorAll('script[src*="/taskmate-reward-progress-card.js"]')) .map(s => s.src.split("?")[1]).find(Boolean) || "" ).get("v") || "?"; console.info( "%c TASKMATE REWARD PROGRESS CARD %c v" + _tmVersion + " ", "background:#03a9f4;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;" );