/** * TaskMate Wishlist Card (#932) * * One child's wishlist: things they want, what they have saved towards each, * and what the family has pledged. The child adds a wish (it waits for a * grown-up), moves points in, takes their own savings back out, and asks to * redeem once it's funded — which becomes an ordinary reward claim for the * parent. Pledges are entered by a parent in the admin panel. * * Reads the overview sensor (children, points name/icon) plus the child's own * wishlist sensor, found by its `wishlist_child_id` attribute. Pictures are * stored images, signed per viewer through auth/sign_path before use. * * Single layout, so it consumes the design tokens directly (like the family * goal card) rather than carrying a second _renderDesigned() template. Every * --tmd-* reference has the card's original value as its fallback, so classic * — which defines no tokens — renders as designed. Layout classes carry a * wl- prefix because the shared design kit defines its own .btn / .bar. */ 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); // Keeps number runs like "3 / 1" reading left to right in RTL text (#995); // identity until the design layer has loaded (it is also what stamps dir). const _ltrNums = (s) => (window.__taskmate_design && window.__taskmate_design.ltrNums ? window.__taskmate_design.ltrNums(s) : s); // "a / b" for a template: the same text nodes as a literal `${a} / ${b}` in // LTR (so it renders pixel-for-pixel as before), the isolated string in RTL. const _ltrRatio = (a, b) => { const s = `${a} / ${b}`; const r = _ltrNums(s); return r === s ? html`${a} / ${b}` : r; }; const DEFAULT_ACCENT = "#e91e63"; const IMAGE_PREFIX = "/api/taskmate/image/"; const TARGET_STEP = 10; const MAX_TARGET = 100000; /** * A wish link is typed by a child. Only an absolute http(s) URL is ever * rendered as a link — never javascript:, data: or a relative path — even * though the backend already refuses anything else. */ function safeLink(value) { if (typeof value !== "string" || !value) return null; try { const url = new URL(value); if (url.protocol !== "http:" && url.protocol !== "https:") return null; return { href: url.href, host: url.hostname.replace(/^www\./, "") }; } catch (_e) { return null; } } class TaskMateWishlistCard extends LitElement { static get properties() { return { hass: { type: Object }, config: { type: Object }, _sheet: { state: true }, _signed: { state: true }, }; } constructor() { super(); this._sheet = null; this._signed = {}; this._inflight = new Set(); } setConfig(config) { if (!config || !config.entity) { throw new Error("A TaskMate overview entity is required"); } this.config = config; } static getStubConfig() { return { entity: "sensor.taskmate_overview" }; } static getConfigElement() { return document.createElement("taskmate-wishlist-card-editor"); } getCardSize() { return 5; } shouldUpdate(changedProps) { if (changedProps.has("hass")) { const old = changedProps.get("hass"); const wlId = this._wishlistEntityId(); if (old && wlId && old.states && this.hass.states[wlId] !== old.states[wlId]) return true; return window.__taskmate_hasChanged ? window.__taskmate_hasChanged(old, this.hass, this.config && this.config.entity) : true; } return true; } _t(key, params) { const fn = window.__taskmate_localize; return fn ? fn(this.hass, key, params) : key; } /** * Resolve the active design, stamp data-tm-design on the host, and set the * accent. The accent is an inline host property, not a template