From f78a78c0d97f96c7d8afd20a0f0aa403c397c286 Mon Sep 17 00:00:00 2001 From: Joshua Date: Sun, 27 Sep 2026 12:55:54 -0400 Subject: [PATCH] panel: add a return control to the Quest Log header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A child who opened their Quest Log could only get back to the Party board by waiting out the 40 s idle return. The header now carries a 72px "The Party" button (left chevron, left of the crest) that calls the existing _returnToBoard() path the idle return uses. The idle return, crest-tap navigation and 1c auto-return are unchanged. Documented in PANEL-SPEC §3; jsdom test taps it and asserts navigation to the strategy-computed board_path; bundle rebuilt. Task: 987d2d0f-a5d1-4bb5-a1f3-80fd6adbf5e9 Co-Authored-By: Claude Opus 5.5 --- .../nestquest/www/nestquest-cards.js | 189 +++++++++++------- design/PANEL-SPEC.md | 8 + frontend/src/cards/quest-log.ts | 55 +++++ frontend/tests/render-quest-log.mjs | 15 +- tests/test_panel_cards.py | 47 ++++- 5 files changed, 241 insertions(+), 73 deletions(-) diff --git a/custom_components/nestquest/www/nestquest-cards.js b/custom_components/nestquest/www/nestquest-cards.js index 9706aa2..a819a0b 100644 --- a/custom_components/nestquest/www/nestquest-cards.js +++ b/custom_components/nestquest/www/nestquest-cards.js @@ -65,7 +65,7 @@ const { is: nt, defineProperty: it, getOwnPropertyDescriptor: st, getOwnProperty return t; } }, Ze = (i, e) => !nt(i, e), ye = { attribute: !0, type: String, converter: ae, reflect: !1, useDefault: !1, hasChanged: Ze }; Symbol.metadata ??= /* @__PURE__ */ Symbol("metadata"), K.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap(); -let z = class extends HTMLElement { +let T = class extends HTMLElement { static addInitializer(e) { this._$Ei(), (this.l ??= []).push(e); } @@ -246,7 +246,7 @@ let z = class extends HTMLElement { firstUpdated(e) { } }; -z.elementStyles = [], z.shadowRootOptions = { mode: "open" }, z[I("elementProperties")] = /* @__PURE__ */ new Map(), z[I("finalized")] = /* @__PURE__ */ new Map(), ct?.({ ReactiveElement: z }), (K.reactiveElementVersions ??= []).push("2.1.2"); +T.elementStyles = [], T.shadowRootOptions = { mode: "open" }, T[I("elementProperties")] = /* @__PURE__ */ new Map(), T[I("finalized")] = /* @__PURE__ */ new Map(), ct?.({ ReactiveElement: T }), (K.reactiveElementVersions ??= []).push("2.1.2"); const pe = globalThis, be = (i) => i, V = pe.trustedTypes, ve = V ? V.createPolicy("lit-html", { createHTML: (i) => i }) : void 0, Qe = "$lit$", k = `lit$${Math.random().toFixed(9).slice(2)}$`, We = "?" + k, dt = `<${We}>`, A = document, P = () => A.createComment(""), H = (i) => i === null || typeof i != "object" && typeof i != "function", he = Array.isArray, pt = (i) => he(i) || typeof i?.[Symbol.iterator] == "function", Y = `[ \f\r]`, L = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, xe = /-->/g, we = />/g, $ = RegExp(`>|${Y}(?:([^\\s"'>=/]+)(${Y}*=${Y}*(?:[^ \f\r"'\`<>=]|("|')|))|$)`, "g"), ke = /'/g, $e = /"/g, Ge = /^(?:script|style|textarea|title)$/i, Ke = (i) => (e, ...t) => ({ _$litType$: i, strings: e, values: t }), d = Ke(1), qe = Ke(2), S = /* @__PURE__ */ Symbol.for("lit-noChange"), g = /* @__PURE__ */ Symbol.for("lit-nothing"), Me = /* @__PURE__ */ new WeakMap(), M = A.createTreeWalker(A, 129); @@ -266,13 +266,13 @@ const ht = (i, e) => { } return [Je(i, a + (i[t] || "") + (e === 2 ? "" : e === 3 ? "" : "")), n]; }; -class U { +class R { constructor({ strings: e, _$litType$: t }, n) { let s; this.parts = []; let a = 0, r = 0; const l = e.length - 1, o = this.parts, [p, u] = ht(e, t); - if (this.el = U.createElement(p, n), M.currentNode = this.el.content, t === 2 || t === 3) { + if (this.el = R.createElement(p, n), M.currentNode = this.el.content, t === 2 || t === 3) { const c = this.el.content.firstChild; c.replaceWith(...c.childNodes); } @@ -303,11 +303,11 @@ class U { return n.innerHTML = e, n; } } -function T(i, e, t = i, n) { +function z(i, e, t = i, n) { if (e === S) return e; let s = n !== void 0 ? t._$Co?.[n] : t._$Cl; const a = H(e) ? void 0 : e._$litDirective$; - return s?.constructor !== a && (s?._$AO?.(!1), a === void 0 ? s = void 0 : (s = new a(i), s._$AT(i, t, n)), n !== void 0 ? (t._$Co ??= [])[n] = s : t._$Cl = s), s !== void 0 && (e = T(i, s._$AS(i, e.values), s, n)), e; + return s?.constructor !== a && (s?._$AO?.(!1), a === void 0 ? s = void 0 : (s = new a(i), s._$AT(i, t, n)), n !== void 0 ? (t._$Co ??= [])[n] = s : t._$Cl = s), s !== void 0 && (e = z(i, s._$AS(i, e.values), s, n)), e; } class ut { constructor(e, t) { @@ -356,7 +356,7 @@ class N { return this._$AB; } _$AI(e, t = this) { - e = T(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) : pt(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) : pt(e) ? this.k(e) : this._(e); } O(e) { return this._$AA.parentNode.insertBefore(e, this._$AB); @@ -368,7 +368,7 @@ 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, s = typeof n == "number" ? this._$AC(e) : (n.el === void 0 && (n.el = U.createElement(Je(n.h, n.h[0]), this.options)), n); + const { values: t, _$litType$: n } = e, s = typeof n == "number" ? this._$AC(e) : (n.el === void 0 && (n.el = R.createElement(Je(n.h, n.h[0]), this.options)), n); if (this._$AH?._$AD === s) this._$AH.p(t); else { const a = new ut(s, this), r = a.u(this.options); @@ -377,7 +377,7 @@ class N { } _$AC(e) { let t = Me.get(e.strings); - return t === void 0 && Me.set(e.strings, t = new U(e)), t; + return t === void 0 && Me.set(e.strings, t = new R(e)), t; } k(e) { he(this._$AH) || (this._$AH = [], this._$AR()); @@ -409,11 +409,11 @@ class J { _$AI(e, t = this, n, s) { const a = this.strings; let r = !1; - if (a === void 0) e = T(this, e, t, 0), r = !H(e) || e !== this._$AH && e !== S, r && (this._$AH = e); + if (a === void 0) e = z(this, e, t, 0), r = !H(e) || e !== this._$AH && e !== S, r && (this._$AH = e); else { const l = e; let o, p; - for (e = a[0], o = 0; o < a.length - 1; o++) p = T(this, l[n + o], t, o), p === S && (p = this._$AH[o]), r ||= !H(p) || p !== this._$AH[o], p === g ? e = g : e !== g && (e += (p ?? "") + a[o + 1]), this._$AH[o] = p; + for (e = a[0], o = 0; o < a.length - 1; o++) p = z(this, l[n + o], t, o), p === S && (p = this._$AH[o]), r ||= !H(p) || p !== this._$AH[o], p === g ? e = g : e !== g && (e += (p ?? "") + a[o + 1]), this._$AH[o] = p; } r && !s && this.j(e); } @@ -442,7 +442,7 @@ class gt extends J { super(e, t, n, s, a), this.type = 5; } _$AI(e, t = this) { - if ((e = T(this, e, t, 0) ?? g) === S) return; + if ((e = z(this, e, t, 0) ?? g) === S) return; const n = this._$AH, s = e === g && n !== g || e.capture !== n.capture || e.once !== n.once || e.passive !== n.passive, a = e !== g && (n === g || s); s && this.element.removeEventListener(this.name, this, n), a && this.element.addEventListener(this.name, this, e), this._$AH = e; } @@ -458,11 +458,11 @@ class _t { return this._$AM._$AU; } _$AI(e) { - T(this, e); + z(this, e); } } const yt = { I: N }, bt = pe.litHtmlPolyfillSupport; -bt?.(U, N), (pe.litHtmlVersions ??= []).push("3.3.3"); +bt?.(R, N), (pe.litHtmlVersions ??= []).push("3.3.3"); const vt = (i, e, t) => { const n = t?.renderBefore ?? e; let s = n._$litPart$; @@ -473,7 +473,7 @@ const vt = (i, e, t) => { return s._$AI(i), s; }; const ue = globalThis; -let C = class extends z { +let C = class extends T { constructor() { super(...arguments), this.renderOptions = { host: this }, this._$Do = void 0; } @@ -592,7 +592,7 @@ function Ct(i) { function Se(i) { return Number.isFinite(i) ? Math.min(100, Math.max(0, i)) : 0; } -function ze(i) { +function Te(i) { return Math.max(0, Math.round(i)); } function At(i) { @@ -620,7 +620,7 @@ function St(i, e) { ); return Number.isFinite(s) ? s - i : 0; } -function zt(i, e) { +function Tt(i, e) { if (typeof i != "string") return null; const t = i.split("-"); @@ -636,14 +636,14 @@ function zt(i, e) { const r = Date.UTC(n, s - 1, a), l = new Date(r - St(r, e)); return Number.isNaN(l.getTime()) ? null : l; } -function Tt(i) { +function zt(i) { const e = qt[i]; return e || i.charAt(0).toUpperCase() + i.slice(1); } function Et(i, e) { return Mt[i] ?? e; } -const Te = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Ee = "M50 3 44.5 24.1 50 26 55.5 24.1Z M100 50 75.9 44.5 74 50 75.9 55.5Z M50 97 55.5 75.9 50 74 44.5 75.9Z M0 50 24.1 55.5 26 50 24.1 44.5Z M29.1 29.1 29.7 36.3 36.3 29.7Z M70.9 29.1 63.7 29.7 70.3 36.3Z M70.9 70.9 70.3 63.7 63.7 70.3Z M29.1 70.9 36.3 70.3 29.7 63.7Z", Nt = "M50 26.5 70.4 38.2 70.4 61.8 50 73.5 29.6 61.8 29.6 38.2Z", Lt = "M29.6 38.2H70.4M29.6 61.8H70.4M50 26.5 36.5 38.2M50 26.5 63.5 38.2M50 73.5 36.5 61.8M50 73.5 63.5 61.8M36.5 38.2V61.8M63.5 38.2V61.8M29.6 38.2 36.5 61.8M36.5 38.2 29.6 61.8M70.4 38.2 63.5 61.8M63.5 38.2 70.4 61.8", Ot = d``, Ut = d``, Rt = d` -`, Rt = d``, Ut = d` `, Ne = { - icon: Ut, + icon: Rt, headline: "NestQuest is not set up yet", body: "A parent needs to finish setting up NestQuest before the party can gather." }, jt = { - icon: Rt, + icon: Ut, headline: "The records cannot be reached", body: "The party's records are quiet right now. NestQuest will return shortly." }, Dt = d`${$t(this._now, this._timeZone())} `; - const t = Tt(e.condition), n = Et(e.condition, t), s = 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 = zt(e.condition), n = Et(e.condition, t), s = 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`
${Dt} @@ -1539,8 +1539,8 @@ const Be = (i, e, t) => { else if (p === void 0 && (p = Be(r, h, m), u = Be(l, c, f)), p.has(l[c])) if (p.has(l[f])) { const _ = u.get(r[h]), v = _ !== void 0 ? s[_] : null; if (v === null) { - const R = O(i, s[c]); - q(R, a[h]), o[h] = R; + const U = O(i, s[c]); + q(U, a[h]), o[h] = U; } else o[h] = q(v, a[h]), O(i, s[c], v), s[_] = null; h++; } else ne(s[f]), f--; @@ -2387,7 +2387,7 @@ function pn(i, e) { ); return Number.isFinite(s) ? s - i : 0; } -function Ue(i, e) { +function Re(i, e) { if (typeof i != "string") return null; const t = i.split("-"); @@ -2470,7 +2470,7 @@ function gn(i) { function _n(i, e) { return mn[i] ?? e; } -const Re = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", yn = "polygon(50% 0, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%)", je = [-9, 6, -4], bn = Ve` +const Ue = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", yn = "polygon(50% 0, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%)", je = [-9, 6, -4], bn = Ve` * { box-sizing: border-box; } @@ -2524,13 +2524,44 @@ const Re = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", yn = "polygon(50% gap: 28px; } + button.return { + flex: none; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 10px; + height: var(--nq-p-button-height); + padding: 0 28px 0 20px; + border: 2px solid rgba(92, 62, 26, 0.5); + border-radius: 10px; + background: rgba(92, 62, 26, 0.1); + color: var(--nq-p-ink); + font-family: var(--nq-p-font-heading); + font-size: 26px; + font-weight: 700; + line-height: 1; + white-space: nowrap; + cursor: pointer; + -webkit-tap-highlight-color: transparent; + transition: transform var(--nq-dur-micro) var(--nq-ease-out); + } + + button.return:active { + transform: scale(0.98); + } + + button.return svg { + width: 34px; + height: 34px; + } + .crest { position: relative; flex: none; width: 96px; height: 110px; padding: 4px; - clip-path: ${x(Re)}; + clip-path: ${x(Ue)}; background: rgba(255, 255, 255, 0.22); } @@ -2541,7 +2572,7 @@ const Re = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", yn = "polygon(50% align-items: center; justify-content: center; padding-bottom: 32px; - clip-path: ${x(Re)}; + clip-path: ${x(Ue)}; background: var(--nq-p-crest); } @@ -3343,6 +3374,16 @@ const Re = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", yn = "polygon(50% > +`, xn = d``, se = d` -`, xn = d``, wn = d` -`, wn = d``, kn = d` -`, kn = d``, $n = d` -`, $n = d``, qn = d` -`, qn = d``, Mn = d` -`, Mn = d``, Cn = d` -`, Cn = d``, An = d` -`, An = { - icon: $n, +`, Sn = { + icon: qn, headline: "No adventurer chosen", body: "Open The Party and tap your crest to open your quest log." -}, Sn = { - icon: qn, +}, Tn = { + icon: Mn, headline: "NestQuest is not set up yet", body: "A parent needs to finish setting up NestQuest." }, zn = { - icon: Mn, + icon: Cn, headline: "The records cannot be reached", body: "The party's records are quiet right now. NestQuest will return shortly." }, De = [ - { key: "morning", name: "Morning", range: "Until 11:59 AM", icon: xn }, + { key: "morning", name: "Morning", range: "Until 11:59 AM", icon: wn }, { key: "afternoon", name: "Afternoon", range: "12:00–5:00 PM", icon: Ye }, - { key: "evening", name: "Evening", range: "5:00–9:00 PM", icon: wn } + { key: "evening", name: "Evening", range: "5:00–9:00 PM", icon: kn } ], W = class W extends C { constructor() { super(...arguments), this._now = /* @__PURE__ */ new Date(), this._confirm = null, this._optimistic = /* @__PURE__ */ new Map(), this._toast = null, this._countdown = null, this._unsubs = [], this._subscribed = !1, this._subGeneration = 0, this._onActivity = () => { @@ -3554,9 +3595,9 @@ const Re = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", yn = "polygon(50% _renderMain(e) { switch (e.kind) { case "no-adventurer": - return this._renderNotice(An); - case "not-set-up": return this._renderNotice(Sn); + case "not-set-up": + return this._renderNotice(Tn); case "unreachable": return this._renderNotice(zn); case "away": { @@ -3565,7 +3606,7 @@ const Re = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", yn = "polygon(50% ${this._renderHeader(e)}
- ${kn} + ${$n} ${e.name} is on travels ${t.map((n) => d`

${n}

`)}
@@ -3669,7 +3710,7 @@ const Re = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", yn = "polygon(50% _awayReturns(e) { const t = this._timeZone(), n = this._state( `binary_sensor.nestquest_${e}_present_today` - ), s = Ue( + ), s = Re( n?.attributes?.next_present, t ); @@ -3815,7 +3856,7 @@ const Re = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", yn = "polygon(50% if (!u) { const _ = this._state( `binary_sensor.nestquest_${l}_present_today` - ), v = Ue( + ), v = Re( _?.attributes?.next_present, n ); @@ -3836,6 +3877,16 @@ const Re = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", yn = "polygon(50% const t = this._childSlug(), n = e.kind === "away", s = n ? e.name : this._childName(t) ?? (t ? ie(t) : null), { due: a, completed: r } = this._childCounts(t), l = this._remaining(), o = this._partyCounts(), p = s ? `${s}'s Quest Log` : "Quest Log", u = (s?.charAt(0) || "?").toUpperCase(); return d`
+ ${this._boardPath() ? d` + + ` : g}