From 3bd30a0b9f663a2470a02d6c2fa8badc045af4e4 Mon Sep 17 00:00:00 2001 From: dev_agent <17+dev_agent@code.cubecraftcreations.com> Date: Sat, 26 Sep 2026 08:23:09 -0400 Subject: [PATCH] Kids' panel: render the definition icon on the open quest card (#245) --- .../nestquest/www/nestquest-cards.js | 244 +++++++++--------- frontend/src/cards/quest-log.ts | 2 +- frontend/tests/render-quest-log.mjs | 24 +- tests/test_panel_cards.py | 112 ++++++++ 4 files changed, 255 insertions(+), 127 deletions(-) diff --git a/custom_components/nestquest/www/nestquest-cards.js b/custom_components/nestquest/www/nestquest-cards.js index 4a61072..6f276d1 100644 --- a/custom_components/nestquest/www/nestquest-cards.js +++ b/custom_components/nestquest/www/nestquest-cards.js @@ -1,5 +1,5 @@ const F = globalThis, se = F.ShadowRoot && (F.ShadyCSS === void 0 || F.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype, ie = /* @__PURE__ */ Symbol(), ce = /* @__PURE__ */ new WeakMap(); -let Ie = class { +let Pe = class { constructor(e, t, n) { if (this._$cssResult$ = !0, n !== ie) throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead."); this.cssText = e, this.t = t; @@ -17,14 +17,14 @@ let Ie = class { return this.cssText; } }; -const v = (s) => new Ie(typeof s == "string" ? s : s + "", void 0, ie), He = (s, ...e) => { +const v = (s) => new Pe(typeof s == "string" ? s : s + "", void 0, ie), Ie = (s, ...e) => { const t = s.length === 1 ? s[0] : e.reduce((n, i, a) => n + ((r) => { if (r._$cssResult$ === !0) return r.cssText; if (typeof r == "number") return r; throw Error("Value passed to 'css' function must be a 'css' function result: " + r + ". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security."); })(i) + s[a + 1], s[0]); - return new Ie(t, s, ie); -}, Ge = (s, e) => { + return new Pe(t, s, ie); +}, We = (s, e) => { if (se) s.adoptedStyleSheets = e.map((t) => t instanceof CSSStyleSheet ? t : t.styleSheet); else for (const t of e) { const n = document.createElement("style"), i = F.litNonce; @@ -35,10 +35,10 @@ const v = (s) => new Ie(typeof s == "string" ? s : s + "", void 0, ie), He = (s, for (const n of e.cssRules) t += n.cssText; return v(t); })(s) : s; -const { is: Ke, defineProperty: Je, getOwnPropertyDescriptor: Xe, getOwnPropertyNames: Ye, getOwnPropertySymbols: et, getPrototypeOf: tt } = Object, Z = globalThis, pe = Z.trustedTypes, nt = pe ? pe.emptyScript : "", st = Z.reactiveElementPolyfillSupport, P = (s, e) => s, ne = { toAttribute(s, e) { +const { is: Ge, defineProperty: Ke, getOwnPropertyDescriptor: Je, getOwnPropertyNames: Xe, getOwnPropertySymbols: Ye, getPrototypeOf: et } = Object, Z = globalThis, pe = Z.trustedTypes, tt = pe ? pe.emptyScript : "", nt = Z.reactiveElementPolyfillSupport, P = (s, e) => s, ne = { toAttribute(s, e) { switch (e) { case Boolean: - s = s ? nt : null; + s = s ? tt : null; break; case Object: case Array: @@ -63,7 +63,7 @@ const { is: Ke, defineProperty: Je, getOwnPropertyDescriptor: Xe, getOwnProperty } } return t; -} }, je = (s, e) => !Ke(s, e), he = { attribute: !0, type: String, converter: ne, reflect: !1, useDefault: !1, hasChanged: je }; +} }, He = (s, e) => !Ge(s, e), he = { attribute: !0, type: String, converter: ne, reflect: !1, useDefault: !1, hasChanged: He }; Symbol.metadata ??= /* @__PURE__ */ Symbol("metadata"), Z.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap(); let T = class extends HTMLElement { static addInitializer(e) { @@ -75,11 +75,11 @@ let T = class extends HTMLElement { static createProperty(e, t = he) { if (t.state && (t.attribute = !1), this._$Ei(), this.prototype.hasOwnProperty(e) && ((t = Object.create(t)).wrapped = !0), this.elementProperties.set(e, t), !t.noAccessor) { const n = /* @__PURE__ */ Symbol(), i = this.getPropertyDescriptor(e, n, t); - i !== void 0 && Je(this.prototype, e, i); + i !== void 0 && Ke(this.prototype, e, i); } } static getPropertyDescriptor(e, t, n) { - const { get: i, set: a } = Xe(this.prototype, e) ?? { get() { + const { get: i, set: a } = Je(this.prototype, e) ?? { get() { return this[t]; }, set(r) { this[t] = r; @@ -94,13 +94,13 @@ let T = class extends HTMLElement { } static _$Ei() { if (this.hasOwnProperty(P("elementProperties"))) return; - const e = tt(this); + const e = et(this); e.finalize(), e.l !== void 0 && (this.l = [...e.l]), this.elementProperties = new Map(e.elementProperties); } static finalize() { if (this.hasOwnProperty(P("finalized"))) return; if (this.finalized = !0, this._$Ei(), this.hasOwnProperty(P("properties"))) { - const t = this.properties, n = [...Ye(t), ...et(t)]; + const t = this.properties, n = [...Xe(t), ...Ye(t)]; for (const i of n) this.createProperty(i, t[i]); } const e = this[Symbol.metadata]; @@ -146,7 +146,7 @@ let T = class extends HTMLElement { } createRenderRoot() { const e = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions); - return Ge(e, this.constructor.elementStyles), e; + return We(e, this.constructor.elementStyles), e; } connectedCallback() { this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(!0), this._$EO?.forEach((e) => e.hostConnected?.()); @@ -178,7 +178,7 @@ let T = class extends HTMLElement { requestUpdate(e, t, n, i = !1, a) { if (e !== void 0) { const r = this.constructor; - if (i === !1 && (a = this[e]), n ??= r.getPropertyOptions(e), !((n.hasChanged ?? je)(a, t) || n.useDefault && n.reflect && a === this._$Ej?.get(e) && !this.hasAttribute(r._$Eu(e, n)))) return; + if (i === !1 && (a = this[e]), n ??= r.getPropertyOptions(e), !((n.hasChanged ?? He)(a, t) || n.useDefault && n.reflect && a === this._$Ej?.get(e) && !this.hasAttribute(r._$Eu(e, n)))) return; this.C(e, t, n); } this.isUpdatePending === !1 && (this._$ES = this._$EP()); @@ -246,43 +246,43 @@ let T = class extends HTMLElement { firstUpdated(e) { } }; -T.elementStyles = [], T.shadowRootOptions = { mode: "open" }, T[P("elementProperties")] = /* @__PURE__ */ new Map(), T[P("finalized")] = /* @__PURE__ */ new Map(), st?.({ ReactiveElement: T }), (Z.reactiveElementVersions ??= []).push("2.1.2"); -const ae = globalThis, ue = (s) => s, Q = ae.trustedTypes, fe = Q ? Q.createPolicy("lit-html", { createHTML: (s) => s }) : void 0, Re = "$lit$", k = `lit$${Math.random().toFixed(9).slice(2)}$`, Ue = "?" + k, it = `<${Ue}>`, A = document, I = () => A.createComment(""), H = (s) => s === null || typeof s != "object" && typeof s != "function", re = Array.isArray, at = (s) => re(s) || typeof s?.[Symbol.iterator] == "function", K = `[ +T.elementStyles = [], T.shadowRootOptions = { mode: "open" }, T[P("elementProperties")] = /* @__PURE__ */ new Map(), T[P("finalized")] = /* @__PURE__ */ new Map(), nt?.({ ReactiveElement: T }), (Z.reactiveElementVersions ??= []).push("2.1.2"); +const ae = globalThis, ue = (s) => s, Q = ae.trustedTypes, fe = Q ? Q.createPolicy("lit-html", { createHTML: (s) => s }) : void 0, je = "$lit$", k = `lit$${Math.random().toFixed(9).slice(2)}$`, Re = "?" + k, st = `<${Re}>`, A = document, I = () => A.createComment(""), H = (s) => s === null || typeof s != "object" && typeof s != "function", re = Array.isArray, it = (s) => re(s) || typeof s?.[Symbol.iterator] == "function", K = `[ \f\r]`, L = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, me = /-->/g, ge = />/g, $ = RegExp(`>|${K}(?:([^\\s"'>=/]+)(${K}*=${K}*(?:[^ -\f\r"'\`<>=]|("|')|))|$)`, "g"), _e = /'/g, ye = /"/g, De = /^(?:script|style|textarea|title)$/i, Fe = (s) => (e, ...t) => ({ _$litType$: s, strings: e, values: t }), d = Fe(1), be = Fe(2), S = /* @__PURE__ */ Symbol.for("lit-noChange"), g = /* @__PURE__ */ Symbol.for("lit-nothing"), ve = /* @__PURE__ */ new WeakMap(), C = A.createTreeWalker(A, 129); -function Qe(s, e) { +\f\r"'\`<>=]|("|')|))|$)`, "g"), _e = /'/g, ye = /"/g, Ue = /^(?:script|style|textarea|title)$/i, De = (s) => (e, ...t) => ({ _$litType$: s, strings: e, values: t }), d = De(1), be = De(2), S = /* @__PURE__ */ Symbol.for("lit-noChange"), g = /* @__PURE__ */ Symbol.for("lit-nothing"), ve = /* @__PURE__ */ new WeakMap(), C = A.createTreeWalker(A, 129); +function Fe(s, e) { if (!re(s) || !s.hasOwnProperty("raw")) throw Error("invalid template strings array"); return fe !== void 0 ? fe.createHTML(e) : e; } -const rt = (s, e) => { +const at = (s, e) => { const t = s.length - 1, n = []; let i, a = e === 2 ? "" : e === 3 ? "" : "", r = L; for (let l = 0; l < t; l++) { const o = s[l]; let p, u, c = -1, f = 0; - for (; f < o.length && (r.lastIndex = f, u = r.exec(o), u !== null); ) f = r.lastIndex, r === L ? u[1] === "!--" ? r = me : u[1] !== void 0 ? r = ge : u[2] !== void 0 ? (De.test(u[2]) && (i = RegExp("" ? (r = i ?? L, c = -1) : u[1] === void 0 ? c = -2 : (c = r.lastIndex - u[2].length, p = u[1], r = u[3] === void 0 ? $ : u[3] === '"' ? ye : _e) : r === ye || r === _e ? r = $ : r === me || r === ge ? r = L : (r = $, i = void 0); + for (; f < o.length && (r.lastIndex = f, u = r.exec(o), u !== null); ) f = r.lastIndex, r === L ? u[1] === "!--" ? r = me : u[1] !== void 0 ? r = ge : u[2] !== void 0 ? (Ue.test(u[2]) && (i = RegExp("" ? (r = i ?? L, c = -1) : u[1] === void 0 ? c = -2 : (c = r.lastIndex - u[2].length, p = u[1], r = u[3] === void 0 ? $ : u[3] === '"' ? ye : _e) : r === ye || r === _e ? r = $ : r === me || r === ge ? r = L : (r = $, i = void 0); const h = r === $ && s[l + 1].startsWith("/>") ? " " : ""; - a += r === L ? o + it : c >= 0 ? (n.push(p), o.slice(0, c) + Re + o.slice(c) + k + h) : o + k + (c === -2 ? l : h); + a += r === L ? o + st : c >= 0 ? (n.push(p), o.slice(0, c) + je + o.slice(c) + k + h) : o + k + (c === -2 ? l : h); } - return [Qe(s, a + (s[t] || "") + (e === 2 ? "" : e === 3 ? "" : "")), n]; + return [Fe(s, a + (s[t] || "") + (e === 2 ? "" : e === 3 ? "" : "")), n]; }; class j { constructor({ strings: e, _$litType$: t }, n) { let i; this.parts = []; let a = 0, r = 0; - const l = e.length - 1, o = this.parts, [p, u] = rt(e, t); + const l = e.length - 1, o = this.parts, [p, u] = at(e, t); if (this.el = j.createElement(p, n), C.currentNode = this.el.content, t === 2 || t === 3) { const c = this.el.content.firstChild; c.replaceWith(...c.childNodes); } for (; (i = C.nextNode()) !== null && o.length < l; ) { if (i.nodeType === 1) { - if (i.hasAttributes()) for (const c of i.getAttributeNames()) if (c.endsWith(Re)) { + if (i.hasAttributes()) for (const c of i.getAttributeNames()) if (c.endsWith(je)) { const f = u[r++], h = i.getAttribute(c).split(k), m = /([.?@])?(.*)/.exec(f); - o.push({ type: 1, index: a, name: m[2], strings: h, ctor: m[1] === "." ? lt : m[1] === "?" ? ct : m[1] === "@" ? dt : W }), i.removeAttribute(c); + o.push({ type: 1, index: a, name: m[2], strings: h, ctor: m[1] === "." ? ot : m[1] === "?" ? lt : m[1] === "@" ? ct : W }), i.removeAttribute(c); } else c.startsWith(k) && (o.push({ type: 6, index: a }), i.removeAttribute(c)); - if (De.test(i.tagName)) { + if (Ue.test(i.tagName)) { const c = i.textContent.split(k), f = c.length - 1; if (f > 0) { i.textContent = Q ? Q.emptyScript : ""; @@ -290,7 +290,7 @@ class j { i.append(c[f], I()); } } - } else if (i.nodeType === 8) if (i.data === Ue) o.push({ type: 2, index: a }); + } else if (i.nodeType === 8) if (i.data === Re) o.push({ type: 2, index: a }); else { let c = -1; for (; (c = i.data.indexOf(k, c + 1)) !== -1; ) o.push({ type: 7, index: a }), c += k.length - 1; @@ -309,7 +309,7 @@ function z(s, e, t = s, n) { const a = H(e) ? void 0 : e._$litDirective$; return i?.constructor !== a && (i?._$AO?.(!1), a === void 0 ? i = void 0 : (i = new a(s), i._$AT(s, t, n)), n !== void 0 ? (t._$Co ??= [])[n] = i : t._$Cl = i), i !== void 0 && (e = z(s, i._$AS(s, e.values), i, n)), e; } -class ot { +class rt { constructor(e, t) { this._$AV = [], this._$AN = void 0, this._$AD = e, this._$AM = t; } @@ -326,7 +326,7 @@ class ot { for (; o !== void 0; ) { if (r === o.index) { let p; - o.type === 2 ? p = new N(a, a.nextSibling, this, e) : o.type === 1 ? p = new o.ctor(a, o.name, o.strings, this, e) : o.type === 6 && (p = new pt(a, this, e)), this._$AV.push(p), o = n[++l]; + o.type === 2 ? p = new N(a, a.nextSibling, this, e) : o.type === 1 ? p = new o.ctor(a, o.name, o.strings, this, e) : o.type === 6 && (p = new dt(a, this, e)), this._$AV.push(p), o = n[++l]; } r !== o?.index && (a = C.nextNode(), r++); } @@ -356,7 +356,7 @@ class N { return this._$AB; } _$AI(e, t = this) { - e = z(this, e, t), H(e) ? e === g || e == null || e === "" ? (this._$AH !== g && this._$AR(), this._$AH = g) : e !== this._$AH && e !== S && this._(e) : e._$litType$ !== void 0 ? this.$(e) : e.nodeType !== void 0 ? this.T(e) : at(e) ? this.k(e) : this._(e); + e = z(this, e, t), H(e) ? e === g || e == null || e === "" ? (this._$AH !== g && this._$AR(), this._$AH = g) : e !== this._$AH && e !== S && this._(e) : e._$litType$ !== void 0 ? this.$(e) : e.nodeType !== void 0 ? this.T(e) : it(e) ? this.k(e) : this._(e); } O(e) { return this._$AA.parentNode.insertBefore(e, this._$AB); @@ -368,10 +368,10 @@ class N { this._$AH !== g && H(this._$AH) ? this._$AA.nextSibling.data = e : this.T(A.createTextNode(e)), this._$AH = e; } $(e) { - const { values: t, _$litType$: n } = e, i = typeof n == "number" ? this._$AC(e) : (n.el === void 0 && (n.el = j.createElement(Qe(n.h, n.h[0]), this.options)), n); + const { values: t, _$litType$: n } = e, i = typeof n == "number" ? this._$AC(e) : (n.el === void 0 && (n.el = j.createElement(Fe(n.h, n.h[0]), this.options)), n); if (this._$AH?._$AD === i) this._$AH.p(t); else { - const a = new ot(i, this), r = a.u(this.options); + const a = new rt(i, this), r = a.u(this.options); a.p(t), this.T(r), this._$AH = a; } } @@ -421,7 +421,7 @@ class W { e === g ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, e ?? ""); } } -class lt extends W { +class ot extends W { constructor() { super(...arguments), this.type = 3; } @@ -429,7 +429,7 @@ class lt extends W { this.element[this.name] = e === g ? void 0 : e; } } -class ct extends W { +class lt extends W { constructor() { super(...arguments), this.type = 4; } @@ -437,7 +437,7 @@ class ct extends W { this.element.toggleAttribute(this.name, !!e && e !== g); } } -class dt extends W { +class ct extends W { constructor(e, t, n, i, a) { super(e, t, n, i, a), this.type = 5; } @@ -450,7 +450,7 @@ class dt extends W { typeof this._$AH == "function" ? this._$AH.call(this.options?.host ?? this.element, e) : this._$AH.handleEvent(e); } } -class pt { +class dt { constructor(e, t, n) { this.element = e, this.type = 6, this._$AN = void 0, this._$AM = t, this.options = n; } @@ -461,9 +461,9 @@ class pt { z(this, e); } } -const ht = { I: N }, ut = ae.litHtmlPolyfillSupport; -ut?.(j, N), (ae.litHtmlVersions ??= []).push("3.3.3"); -const ft = (s, e, t) => { +const pt = { I: N }, ht = ae.litHtmlPolyfillSupport; +ht?.(j, N), (ae.litHtmlVersions ??= []).push("3.3.3"); +const ut = (s, e, t) => { const n = t?.renderBefore ?? e; let i = n._$litPart$; if (i === void 0) { @@ -483,7 +483,7 @@ let M = class extends T { } update(e) { const t = this.render(); - this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(e), this._$Do = ft(t, this.renderRoot, this.renderOptions); + this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(e), this._$Do = ut(t, this.renderRoot, this.renderOptions); } connectedCallback() { super.connectedCallback(), this._$Do?.setConnected(!0); @@ -496,14 +496,14 @@ let M = class extends T { } }; M._$litElement$ = !0, M.finalized = !0, oe.litElementHydrateSupport?.({ LitElement: M }); -const mt = oe.litElementPolyfillSupport; -mt?.({ LitElement: M }); +const ft = oe.litElementPolyfillSupport; +ft?.({ LitElement: M }); (oe.litElementVersions ??= []).push("4.2.2"); -const Ve = ':host{--nq-p-parchment-top: #f0e4c7;--nq-p-parchment-bottom: #ddcca3;--nq-p-parchment: radial-gradient(ellipse at 25% 10%, rgba(255,255,255,.5), transparent 55%), radial-gradient(ellipse at 85% 90%, rgba(120,86,44,.28), transparent 60%), repeating-linear-gradient(93deg, rgba(150,115,70,.05) 0 2px, transparent 2px 6px), repeating-linear-gradient(2deg, rgba(150,115,70,.04) 0 3px, transparent 3px 7px), linear-gradient(var(--nq-p-parchment-top), var(--nq-p-parchment-bottom));--nq-p-vignette: inset 0 0 200px rgba(80,52,20,.35);--nq-p-card-open: linear-gradient(#fdf7e6, #f2e7c9);--nq-p-card-done: linear-gradient(#f0e8d3, #e6dcc2);--nq-p-card-panel: linear-gradient(#fcf6e6, #f1e6ca);--nq-p-card-border: rgba(120,88,48,.38);--nq-p-panel-border: rgba(92,62,26,.4);--nq-p-card-shadow: 0 3px 0 rgba(120,88,48,.2), inset 0 1px 0 rgba(255,255,255,.7);--nq-p-panel-shadow: 0 6px 0 rgba(92,62,26,.18), inset 0 2px 0 rgba(255,255,255,.7);--nq-p-frame-outer: 2px solid rgba(92,62,26,.45);--nq-p-frame-inner: 1px solid rgba(92,62,26,.28);--nq-p-rule: 2px solid rgba(92,62,26,.35);--nq-p-ink: #2b1f14;--nq-p-ink-secondary: #5c452a;--nq-p-ink-muted: #6f6455;--nq-p-ink-away: #4d4433;--nq-p-ink-late: #8f1526;--nq-brand-blue: #3B3AB8;--nq-brand-purple: #A035CC;--nq-brand-gradient: linear-gradient(135deg, var(--nq-brand-blue), var(--nq-brand-purple));--nq-p-icon-tile: var(--nq-brand-gradient);--nq-p-icon-tile-done: rgba(92,62,26,.16);--nq-p-crest: var(--nq-brand-gradient);--nq-p-crest-away: linear-gradient(135deg, #6b6b7a, #7a7286);--nq-p-seal: radial-gradient(circle at 35% 30%, #a8283a, #6d1322);--nq-p-seal-shadow: 0 4px 10px rgba(60,10,20,.4), inset 0 0 0 4px rgba(255,255,255,.14);--nq-p-seal-size: 78px;--nq-p-dock-bg: rgba(43,31,20,.9);--nq-p-dock-ink: #f7efdb;--nq-p-dock-ink-secondary: #d8c9a6;--nq-p-dock-divider: rgba(233,220,189,.3);--nq-p-dock-height: 84px;--nq-p-font-display: "Cinzel Decorative", Cinzel, serif;--nq-p-font-heading: Cinzel, serif;--nq-p-font-body: Nunito, system-ui, sans-serif;--nq-p-size-hero: 86px;--nq-p-size-wordmark: 66px;--nq-p-size-title: 56px;--nq-p-size-name: 48px;--nq-p-size-section: 32px;--nq-p-size-quest: 30px;--nq-p-size-body: 22px;--nq-p-size-label: 21px;--nq-p-track-label: .14em;--nq-p-track-kicker: .3em;--nq-p-quest-min-height: 116px;--nq-p-quest-gap: 24px;--nq-p-quest-pad: 22px;--nq-p-tile-size: 64px;--nq-p-button-height: 72px;--nq-p-button-min-width: 150px;--nq-p-confirm-button: 108px;--nq-p-radius-card: 12px;--nq-p-radius-panel: 20px;--nq-p-radius-pill: 9999px;--nq-p-page-inset: 62px;--nq-p-frame-inset: 26px;--nq-ease-out: cubic-bezier(0, 0, .2, 1);--nq-dur-micro: .12s;--nq-dur-base: .2s;--nq-dur-modal: .35s}'; +const Qe = ':host{--nq-p-parchment-top: #f0e4c7;--nq-p-parchment-bottom: #ddcca3;--nq-p-parchment: radial-gradient(ellipse at 25% 10%, rgba(255,255,255,.5), transparent 55%), radial-gradient(ellipse at 85% 90%, rgba(120,86,44,.28), transparent 60%), repeating-linear-gradient(93deg, rgba(150,115,70,.05) 0 2px, transparent 2px 6px), repeating-linear-gradient(2deg, rgba(150,115,70,.04) 0 3px, transparent 3px 7px), linear-gradient(var(--nq-p-parchment-top), var(--nq-p-parchment-bottom));--nq-p-vignette: inset 0 0 200px rgba(80,52,20,.35);--nq-p-card-open: linear-gradient(#fdf7e6, #f2e7c9);--nq-p-card-done: linear-gradient(#f0e8d3, #e6dcc2);--nq-p-card-panel: linear-gradient(#fcf6e6, #f1e6ca);--nq-p-card-border: rgba(120,88,48,.38);--nq-p-panel-border: rgba(92,62,26,.4);--nq-p-card-shadow: 0 3px 0 rgba(120,88,48,.2), inset 0 1px 0 rgba(255,255,255,.7);--nq-p-panel-shadow: 0 6px 0 rgba(92,62,26,.18), inset 0 2px 0 rgba(255,255,255,.7);--nq-p-frame-outer: 2px solid rgba(92,62,26,.45);--nq-p-frame-inner: 1px solid rgba(92,62,26,.28);--nq-p-rule: 2px solid rgba(92,62,26,.35);--nq-p-ink: #2b1f14;--nq-p-ink-secondary: #5c452a;--nq-p-ink-muted: #6f6455;--nq-p-ink-away: #4d4433;--nq-p-ink-late: #8f1526;--nq-brand-blue: #3B3AB8;--nq-brand-purple: #A035CC;--nq-brand-gradient: linear-gradient(135deg, var(--nq-brand-blue), var(--nq-brand-purple));--nq-p-icon-tile: var(--nq-brand-gradient);--nq-p-icon-tile-done: rgba(92,62,26,.16);--nq-p-crest: var(--nq-brand-gradient);--nq-p-crest-away: linear-gradient(135deg, #6b6b7a, #7a7286);--nq-p-seal: radial-gradient(circle at 35% 30%, #a8283a, #6d1322);--nq-p-seal-shadow: 0 4px 10px rgba(60,10,20,.4), inset 0 0 0 4px rgba(255,255,255,.14);--nq-p-seal-size: 78px;--nq-p-dock-bg: rgba(43,31,20,.9);--nq-p-dock-ink: #f7efdb;--nq-p-dock-ink-secondary: #d8c9a6;--nq-p-dock-divider: rgba(233,220,189,.3);--nq-p-dock-height: 84px;--nq-p-font-display: "Cinzel Decorative", Cinzel, serif;--nq-p-font-heading: Cinzel, serif;--nq-p-font-body: Nunito, system-ui, sans-serif;--nq-p-size-hero: 86px;--nq-p-size-wordmark: 66px;--nq-p-size-title: 56px;--nq-p-size-name: 48px;--nq-p-size-section: 32px;--nq-p-size-quest: 30px;--nq-p-size-body: 22px;--nq-p-size-label: 21px;--nq-p-track-label: .14em;--nq-p-track-kicker: .3em;--nq-p-quest-min-height: 116px;--nq-p-quest-gap: 24px;--nq-p-quest-pad: 22px;--nq-p-tile-size: 64px;--nq-p-button-height: 72px;--nq-p-button-min-width: 150px;--nq-p-confirm-button: 108px;--nq-p-radius-card: 12px;--nq-p-radius-panel: 20px;--nq-p-radius-pill: 9999px;--nq-p-page-inset: 62px;--nq-p-frame-inset: 26px;--nq-ease-out: cubic-bezier(0, 0, .2, 1);--nq-dur-micro: .12s;--nq-dur-base: .2s;--nq-dur-modal: .35s}'; function le(s) { window.customCards = window.customCards ?? [], window.customCards.push(s); } -const gt = [ +const mt = [ "nestquest_quest_completed", "nestquest_quest_uncompleted", "nestquest_quest_missed", @@ -524,7 +524,7 @@ function V(s, e) { } return xe.set(t, i), i; } -function _t(s) { +function gt(s) { const e = s?.config; if (!e || typeof e != "object") return; @@ -534,14 +534,14 @@ function _t(s) { function J(s, e) { return `${V(e, { weekday: "long" }).format(s)}, ${V(e, { month: "short", day: "numeric" }).format(s)}`; } -function yt(s, e) { +function _t(s, e) { return V(e, { hour: "numeric", minute: "2-digit", hour12: !0 }).format(s); } -const bt = { +const yt = { "clear-night": "Clear", cloudy: "Cloudy", exceptional: "Clear", @@ -557,7 +557,7 @@ const bt = { sunny: "Sunny", windy: "Windy", "windy-variant": "Windy" -}, vt = { +}, bt = { "clear-night": "Clear night skies", cloudy: "Grey skies today", exceptional: "A striking day", @@ -583,7 +583,7 @@ function X(s, e) { const t = typeof s == "number" ? s : Number(s); return Number.isFinite(t) ? t : e; } -function xt(s) { +function vt(s) { if (s === null || typeof s != "object" || Array.isArray(s)) return ""; const e = s.slug; @@ -595,10 +595,10 @@ function ke(s) { function $e(s) { return Math.max(0, Math.round(s)); } -function wt(s) { +function xt(s) { return s.split(/[-_]+/).filter(Boolean).map((e) => e.charAt(0).toUpperCase() + e.slice(1)).join(" "); } -function kt(s, e) { +function wt(s, e) { const t = V(e, { year: "numeric", month: "2-digit", @@ -620,7 +620,7 @@ function kt(s, e) { ); return Number.isFinite(i) ? i - s : 0; } -function $t(s, e) { +function kt(s, e) { if (typeof s != "string") return null; const t = s.split("-"); @@ -633,17 +633,17 @@ function $t(s, e) { const o = new Date(n, i - 1, a); return Number.isNaN(o.getTime()) ? null : o; } - const r = Date.UTC(n, i - 1, a), l = new Date(r - kt(r, e)); + const r = Date.UTC(n, i - 1, a), l = new Date(r - wt(r, e)); return Number.isNaN(l.getTime()) ? null : l; } -function qt(s) { - const e = bt[s]; +function $t(s) { + const e = yt[s]; return e || s.charAt(0).toUpperCase() + s.slice(1); } -function Ct(s, e) { - return vt[s] ?? e; +function qt(s, e) { + return bt[s] ?? e; } -const qe = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Mt = "polygon(50% 0, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%)", At = He` +const qe = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Ct = "polygon(50% 0, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%)", Mt = Ie` * { box-sizing: border-box; } @@ -715,7 +715,7 @@ const qe = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Mt = "polygon(50% justify-content: center; width: 34px; height: 34px; - clip-path: ${v(Mt)}; + clip-path: ${v(Ct)}; background: var(--nq-brand-gradient); } @@ -1038,7 +1038,7 @@ const qe = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Mt = "polygon(50% font-weight: 800; line-height: 1; } -`, St = d` -`, Tt = d``, St = d` -`, zt = d``, Tt = d` -`, Et = d``, zt = d` -`, Nt = d``, Et = d` `, Ce = { - icon: Et, + icon: zt, headline: "NestQuest is not set up yet", body: "A parent needs to finish setting up NestQuest before the party can gather." -}, Lt = { - icon: Nt, +}, Nt = { + icon: Et, headline: "The records cannot be reached", body: "The party's records are quiet right now. NestQuest will return shortly." -}, Bt = d` `; -class Ot extends M { +class Bt extends M { static properties = { hass: { attribute: !1 }, _config: { state: !0 }, _now: { state: !0 } }; - static styles = [v(Ve), At]; + static styles = [v(Qe), Mt]; hass; _config; _now = /* @__PURE__ */ new Date(); @@ -1156,7 +1156,7 @@ class Ot extends M { t === this._subGeneration && (this._unsubscribeLive(), this._armSubscribeRetry()); }); }; - for (const i of gt) + for (const i of mt) n( e.subscribeEvents(() => this.requestUpdate(), i) ); @@ -1219,7 +1219,7 @@ class Ot extends M { `; } _timeZone() { - return _t(this.hass); + return gt(this.hass); } _state(e) { const n = this.hass?.states?.[e]; @@ -1243,7 +1243,7 @@ class Ot extends M { const t = this._state( "sensor.nestquest_household_quests_due_today" )?.attributes?.child_roster; - return Array.isArray(t) ? t.map((n) => xt(n)).filter((n) => n.length > 0) : []; + return Array.isArray(t) ? t.map((n) => vt(n)).filter((n) => n.length > 0) : []; } _plates() { return this._childSlugs().map((e) => this._plate(e)); @@ -1267,8 +1267,8 @@ class Ot extends M { const G = r?.attributes?.child_name; typeof G == "string" && G.trim() && (m = G.trim()); } - m || (m = wt(e)); - const x = $t( + m || (m = xt(e)); + const x = kt( r?.attributes?.next_present, this._timeZone() ), R = x ? `Returns ${J(x, this._timeZone())}` : null; @@ -1297,7 +1297,7 @@ class Ot extends M { } _boardNotice() { const e = this._plates(); - return e.length === 0 ? Ce : e.every((t) => t.unresolved !== null) ? e.some((t) => t.unresolved === "stale") ? Lt : Ce : null; + return e.length === 0 ? Ce : e.every((t) => t.unresolved !== null) ? e.some((t) => t.unresolved === "stale") ? Nt : Ce : null; } _renderNotice(e) { return d` @@ -1320,7 +1320,7 @@ class Ot extends M { ${e.name} - ${zt} + ${Tt} Unknown — @@ -1340,7 +1340,7 @@ class Ot extends M { ${e.name} - ${St} + ${At} Home today @@ -1359,7 +1359,7 @@ class Ot extends M { ${e.name} - ${Tt} + ${St} On travels ${e.returns ?? "Returns —"} @@ -1411,13 +1411,13 @@ class Ot extends M {
${J(this._now, this._timeZone())} - ${yt(this._now, this._timeZone())} + ${_t(this._now, this._timeZone())}
`; - const t = qt(e.condition), n = Ct(e.condition, t), i = e.temperature === null ? g : d`${Math.round(e.temperature)}°`, a = e.high === null || e.low === null ? null : `${Math.round(e.high)}° / ${Math.round(e.low)}°`; + const t = $t(e.condition), n = qt(e.condition, t), i = e.temperature === null ? g : d`${Math.round(e.temperature)}°`, a = e.high === null || e.low === null ? null : `${Math.round(e.high)}° / ${Math.round(e.low)}°`; return d`
- ${Bt} + ${Lt} ${i} @@ -1429,14 +1429,14 @@ class Ot extends M { `; } } -customElements.define("nestquest-party-board-card", Ot); +customElements.define("nestquest-party-board-card", Bt); le({ type: "nestquest-party-board-card", name: "NestQuest Party Board", description: "Kids' attract screen for present and away adventurers." }); -const Pt = { CHILD: 2 }, It = (s) => (...e) => ({ _$litDirective$: s, values: e }); -let Ht = class { +const Ot = { CHILD: 2 }, Pt = (s) => (...e) => ({ _$litDirective$: s, values: e }); +let It = class { constructor(e) { } get _$AU() { @@ -1452,11 +1452,11 @@ let Ht = class { return this.render(...t); } }; -const { I: jt } = ht, Me = (s) => s, Ae = () => document.createComment(""), B = (s, e, t) => { +const { I: Ht } = pt, Me = (s) => s, Ae = () => document.createComment(""), B = (s, e, t) => { const n = s._$AA.parentNode, i = e === void 0 ? s._$AB : e._$AA; if (t === void 0) { const a = n.insertBefore(Ae(), i), r = n.insertBefore(Ae(), i); - t = new jt(a, r, s, s.options); + t = new Ht(a, r, s, s.options); } else { const a = t._$AB.nextSibling, r = t._$AM, l = r !== s; if (l) { @@ -1472,16 +1472,16 @@ const { I: jt } = ht, Me = (s) => s, Ae = () => document.createComment(""), B = } } return t; -}, q = (s, e, t = s) => (s._$AI(e, t), s), Rt = {}, Ut = (s, e = Rt) => s._$AH = e, Dt = (s) => s._$AH, Y = (s) => { +}, q = (s, e, t = s) => (s._$AI(e, t), s), jt = {}, Rt = (s, e = jt) => s._$AH = e, Ut = (s) => s._$AH, Y = (s) => { s._$AR(), s._$AA.remove(); }; const Se = (s, e, t) => { const n = /* @__PURE__ */ new Map(); for (let i = e; i <= t; i++) n.set(s[i], i); return n; -}, Te = It(class extends Ht { +}, Te = Pt(class extends It { constructor(s) { - if (super(s), s.type !== Pt.CHILD) throw Error("repeat() can only be used in text expressions"); + if (super(s), s.type !== Ot.CHILD) throw Error("repeat() can only be used in text expressions"); } dt(s, e, t) { let n; @@ -1495,7 +1495,7 @@ const Se = (s, e, t) => { return this.dt(s, e, t).values; } update(s, [e, t, n]) { - const i = Dt(s), { values: a, keys: r } = this.dt(e, t, n); + const i = Ut(s), { values: a, keys: r } = this.dt(e, t, n); if (!Array.isArray(i)) return this.ut = r, a; const l = this.ut ??= [], o = []; let p, u, c = 0, f = i.length - 1, h = 0, m = a.length - 1; @@ -1522,9 +1522,9 @@ const Se = (s, e, t) => { const _ = i[c++]; _ !== null && Y(_); } - return this.ut = r, Ut(s, o), S; + return this.ut = r, Rt(s, o), S; } -}), Ft = [ +}), Dt = [ { kind: "lucide", key: "lucide:bed", @@ -1994,7 +1994,7 @@ const Se = (s, e, t) => { "m3 7 2 2 4-4" ] } -], Qt = [ +], Ft = [ { kind: "fa", key: "fa:broom", @@ -2215,20 +2215,20 @@ const Se = (s, e, t) => { "M384 512L96 512c-53 0-96-43-96-96L0 96C0 43 43 0 96 0L400 0c26.5 0 48 21.5 48 48l0 288c0 20.9-13.4 38.7-32 45.3l0 66.7c17.7 0 32 14.3 32 32s-14.3 32-32 32l-32 0zM96 384c-17.7 0-32 14.3-32 32s14.3 32 32 32l256 0 0-64-256 0zm32-232c0 13.3 10.7 24 24 24l176 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-176 0c-13.3 0-24 10.7-24 24zm24 72c-13.3 0-24 10.7-24 24s10.7 24 24 24l176 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-176 0z" ] } -], O = { kind: "fallback", key: null }, Vt = 8, ze = new Map(Ft.map((s) => [s.name, s])), Zt = new Map(Qt.map((s) => [s.name, s])); -function Wt(s) { +], O = { kind: "fallback", key: null }, Qt = 8, ze = new Map(Dt.map((s) => [s.name, s])), Vt = new Map(Ft.map((s) => [s.name, s])); +function Zt(s) { const e = [...s]; - return e.length === 0 || e.length > Vt ? !1 : !e.some((t) => new RegExp("[\\s<>&]|\\p{Cc}", "u").test(t)); + return e.length === 0 || e.length > Qt ? !1 : !e.some((t) => new RegExp("[\\s<>&]|\\p{Cc}", "u").test(t)); } -function Gt(s) { +function Wt(s) { if (!s || !s.trim()) return O; const e = s.indexOf(":"); if (e === -1) return ze.get(s) ?? O; const t = s.slice(0, e), n = s.slice(e + 1); - return t === "lucide" ? ze.get(n) ?? O : t === "fa" ? Zt.get(n) ?? O : t === "emoji" && Wt(n) ? { kind: "emoji", key: s, text: n } : O; + return t === "lucide" ? ze.get(n) ?? O : t === "fa" ? Vt.get(n) ?? O : t === "emoji" && Zt(n) ? { kind: "emoji", key: s, text: n } : O; } -function Ee(s, e) { - const t = Gt(s); +function Gt(s, e) { + const t = Wt(s); switch (t.kind) { case "lucide": return d` -`, Ze = d``, Ve = d`
- ${Ze} + ${Ve} No quests today

${`Nothing was posted for today, ${e.name}. Enjoy the day's rest!`} @@ -3614,7 +3614,7 @@ class kn extends M { return d` ${this._renderHeader(e)}

- ${Pe.map((t) => this._renderColumn(t))} + ${Oe.map((t) => this._renderColumn(t))}
`; } @@ -3662,7 +3662,7 @@ class kn extends M { _awayReturns(e) { const t = this._timeZone(), n = this._state( `binary_sensor.nestquest_${e}_present_today` - ), i = Le( + ), i = Ne( n?.attributes?.next_present, t ); @@ -3808,7 +3808,7 @@ class kn extends M { if (!u) { const _ = this._state( `binary_sensor.nestquest_${l}_present_today` - ), x = Le( + ), x = Ne( _?.attributes?.next_present, n ); @@ -3915,10 +3915,10 @@ class kn extends M { } _renderQuest(e, t, n) { if (t) { - const r = Oe[n % Oe.length], l = e.completed_at ? Yt(e.completed_at, this._timeZone()) : ""; + const r = Be[n % Be.length], l = e.completed_at ? Yt(e.completed_at, this._timeZone()) : ""; return d`
- +
${e.title} @@ -3954,7 +3954,7 @@ class kn extends M { (r.key === "Enter" || r.key === " ") && (r.preventDefault(), this._openConfirm(e.id)); }} > - +
${e.title} ${a.text} @@ -3990,7 +3990,7 @@ class kn extends M { ); if (!e) return g; - const t = Pe.find((n) => n.key === e.window); + const t = Oe.find((n) => n.key === e.window); return d`
this._closeConfirm()}>
- +
${instance.title} diff --git a/frontend/tests/render-quest-log.mjs b/frontend/tests/render-quest-log.mjs index f0ccdc9..d0fbe93 100644 --- a/frontend/tests/render-quest-log.mjs +++ b/frontend/tests/render-quest-log.mjs @@ -7,10 +7,10 @@ * the card is mounted at the given URL (it resolves its child from the * pathname's last segment), after the optional probe wait one snapshot of * the render is taken mid-wait, then after the optional idle wait stdout - * receives one JSON object {location, headline, countdown, html, probe} — - * the final window pathname, the rendered title, the complete-screen - * countdown text, the rendered shadow DOM, and the probe snapshot when - * probe_ms was given. The Python panel tests drive this harness so the + * receives one JSON object {location, headline, countdown, html, tiles, + * probe} — the final window pathname, the rendered title, the + * complete-screen countdown text, the rendered shadow DOM, each quest + * card's icon tile, and the probe snapshot when probe_ms was given. The Python panel tests drive this harness so the * assertions run against the exact bundle HACS ships. */ import { readFileSync } from "node:fs"; @@ -56,6 +56,22 @@ const snapshot = () => ({ element.shadowRoot.querySelector(".complete-countdown")?.textContent?.trim() ?? "", html: element.shadowRoot.innerHTML, + // Each quest card's icon tile: the registry glyph it rendered (its + // data-icon key and svg fill), its text (an emoji), and whether it shows + // the static star (the star's polygon). + tiles: [...element.shadowRoot.querySelectorAll(".quest")].map((quest) => { + const tile = quest.querySelector(".tile"); + const glyph = tile?.querySelector("[data-icon]"); + return { + id: Number(quest.dataset.instanceId), + sealed: quest.classList.contains("sealed"), + icon: glyph?.getAttribute("data-icon") ?? null, + fill: glyph?.getAttribute("fill") ?? null, + paths: tile?.querySelectorAll("path").length ?? 0, + text: tile?.textContent?.trim() ?? "", + star: Boolean(tile?.querySelector("polygon")), + }; + }), }); let probe = null; diff --git a/tests/test_panel_cards.py b/tests/test_panel_cards.py index 2b01162..87aabd5 100644 --- a/tests/test_panel_cards.py +++ b/tests/test_panel_cards.py @@ -886,3 +886,115 @@ def test_quest_complete_screen_returns_to_the_board_after_the_countdown() -> Non assert result["probe"]["countdown"] == "Returning to The Party in 1 seconds" # …and at zero the card has returned to the party board. assert result["location"] == "/nestquest" + + +# --- Quest card icons (definition icon on the open tile) --------------------- + + +def _quest_row(instance_id: int, icon: str | None, state: str = "open") -> dict: + """One ``instances`` attribute row for Ada's quests-due sensor.""" + row = { + "id": instance_id, + "child_id": 1, + "title": f"Quest {instance_id}", + "icon": icon, + "window": "morning", + "due_time": "08:00", + "state": state, + "overdue": False, + "completed_at": None, + "on_time": None, + } + if state == "completed": + row["completed_at"] = "2026-09-26T07:30:00+00:00" + row["on_time"] = True + return row + + +def _quest_log_tiles(rows: list[dict]) -> dict[int, dict]: + """Render Ada's quest log with ``rows`` and key its tiles by instance id.""" + states = _three_child_states() + states["sensor.nestquest_ada_quests_due_today"] = _state( + str(len(rows)), child_name="Ada", present=True, instances=rows + ) + result = _render_quest_log( + {"type": "custom:nestquest-quest-log-card"}, + states, + url="http://homeassistant.local/nestquest/ada", + ) + return {tile["id"]: tile for tile in result["tiles"]} + + +def test_open_quest_tile_renders_the_definition_icon() -> None: + """The open card's tile shows the definition's namespaced icon, drawn + from the bundled registry: Lucide as a stroked svg, Font Awesome as a + filled svg, an emoji as its text, and a legacy bare name as Lucide.""" + tiles = _quest_log_tiles( + [ + _quest_row(1, "lucide:bed"), + _quest_row(2, "fa:broom"), + _quest_row(3, "emoji:🦊"), + _quest_row(4, "bath"), + ] + ) + assert tiles[1]["icon"] == "lucide:bed" + assert tiles[1]["fill"] == "none" + assert tiles[1]["paths"] > 0 + assert tiles[2]["icon"] == "fa:broom" + assert tiles[2]["fill"] == "currentColor" + assert tiles[2]["paths"] > 0 + assert tiles[3]["icon"] == "emoji:🦊" + assert tiles[3]["text"] == "🦊" + assert tiles[4]["icon"] == "lucide:bath" + assert tiles[4]["fill"] == "none" + for tile in tiles.values(): + assert not tile["sealed"] + assert not tile["star"] + + +def test_open_quest_tile_falls_back_to_the_star() -> None: + """An unknown name, an unknown namespace, or no icon at all keeps the + open card's current star glyph.""" + tiles = _quest_log_tiles( + [ + _quest_row(1, "lucide:not-a-real-icon"), + _quest_row(2, "fa:not-a-real-icon"), + _quest_row(3, "mdi:bed"), + _quest_row(4, ""), + _quest_row(5, None), + ] + ) + assert set(tiles) == {1, 2, 3, 4, 5} + for tile in tiles.values(): + assert tile["star"] + assert tile["icon"] is None + + +def test_sealed_quest_tile_keeps_the_star() -> None: + """A sealed card keeps its star tile (and its wax seal) whatever the + definition's icon; only the open card shows the quest's icon.""" + tiles = _quest_log_tiles( + [ + _quest_row(1, "lucide:bed", state="completed"), + _quest_row(2, "fa:broom", state="completed"), + _quest_row(3, "emoji:🦊", state="completed"), + ] + ) + for tile in tiles.values(): + assert tile["sealed"] + assert tile["star"] + assert tile["icon"] is None + assert tile["text"] == "" + + +def test_only_the_open_tile_calls_the_icon_resolver() -> None: + source = _read(QUEST_LOG) + sealed_start = source.index('
", sealed_start)] + assert '' in sealed_block + assert "renderQuestIcon" not in sealed_block + assert ( + source.count("renderQuestIcon(instance.icon, ICON_STAR)") == 1 + ), "only the open card's tile resolves the definition icon" + open_start = source.index('class="quest tappable"') + assert "renderQuestIcon(instance.icon, ICON_STAR)" in source[open_start:]