From 0d2e0d6f26a5be705da4e66ce65e12b44ae74874 Mon Sep 17 00:00:00 2001 From: dev_agent <17+dev_agent@code.cubecraftcreations.com> Date: Sun, 20 Sep 2026 13:54:14 -0400 Subject: [PATCH] Add the quest log card (1b) read-only Add the quest log card (1b) read-only --- .../nestquest/www/nestquest-cards.js | 1839 ++++++++++++++--- .../nestquest/www/nestquest-panel-tokens.css | 2 + design/tokens/nestquest-panel-tokens.css | 2 + frontend/src/cards/quest-log.ts | 1417 ++++++++++++- 4 files changed, 2958 insertions(+), 302 deletions(-) diff --git a/custom_components/nestquest/www/nestquest-cards.js b/custom_components/nestquest/www/nestquest-cards.js index 747ba7f..85e4f59 100644 --- a/custom_components/nestquest/www/nestquest-cards.js +++ b/custom_components/nestquest/www/nestquest-cards.js @@ -1,15 +1,15 @@ -const T = globalThis, L = T.ShadowRoot && (T.ShadyCSS === void 0 || T.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype, j = /* @__PURE__ */ Symbol(), K = /* @__PURE__ */ new WeakMap(); -let ct = class { +const L = globalThis, Y = L.ShadowRoot && (L.ShadyCSS === void 0 || L.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype, tt = /* @__PURE__ */ Symbol(), rt = /* @__PURE__ */ new WeakMap(); +let Nt = class { constructor(t, e, n) { - if (this._$cssResult$ = !0, n !== j) throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead."); + if (this._$cssResult$ = !0, n !== tt) throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead."); this.cssText = t, this.t = e; } get styleSheet() { let t = this.o; const e = this.t; - if (L && t === void 0) { + if (Y && t === void 0) { const n = e !== void 0 && e.length === 1; - n && (t = K.get(e)), t === void 0 && ((this.o = t = new CSSStyleSheet()).replaceSync(this.cssText), n && K.set(e, t)); + n && (t = rt.get(e)), t === void 0 && ((this.o = t = new CSSStyleSheet()).replaceSync(this.cssText), n && rt.set(e, t)); } return t; } @@ -17,101 +17,101 @@ let ct = class { return this.cssText; } }; -const $ = (s) => new ct(typeof s == "string" ? s : s + "", void 0, j), _t = (s, ...t) => { - const e = s.length === 1 ? s[0] : t.reduce((n, i, r) => n + ((a) => { - if (a._$cssResult$ === !0) return a.cssText; - if (typeof a == "number") return a; - throw Error("Value passed to 'css' function must be a 'css' function result: " + a + ". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security."); - })(i) + s[r + 1], s[0]); - return new ct(e, s, j); -}, $t = (s, t) => { - if (L) s.adoptedStyleSheets = t.map((e) => e instanceof CSSStyleSheet ? e : e.styleSheet); +const b = (i) => new Nt(typeof i == "string" ? i : i + "", void 0, tt), Tt = (i, ...t) => { + const e = i.length === 1 ? i[0] : t.reduce((n, s, r) => n + ((o) => { + if (o._$cssResult$ === !0) return o.cssText; + if (typeof o == "number") return o; + throw Error("Value passed to 'css' function must be a 'css' function result: " + o + ". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security."); + })(s) + i[r + 1], i[0]); + return new Nt(e, i, tt); +}, Dt = (i, t) => { + if (Y) i.adoptedStyleSheets = t.map((e) => e instanceof CSSStyleSheet ? e : e.styleSheet); else for (const e of t) { - const n = document.createElement("style"), i = T.litNonce; - i !== void 0 && n.setAttribute("nonce", i), n.textContent = e.cssText, s.appendChild(n); + const n = document.createElement("style"), s = L.litNonce; + s !== void 0 && n.setAttribute("nonce", s), n.textContent = e.cssText, i.appendChild(n); } -}, J = L ? (s) => s : (s) => s instanceof CSSStyleSheet ? ((t) => { +}, ot = Y ? (i) => i : (i) => i instanceof CSSStyleSheet ? ((t) => { let e = ""; for (const n of t.cssRules) e += n.cssText; - return $(e); -})(s) : s; -const { is: vt, defineProperty: qt, getOwnPropertyDescriptor: xt, getOwnPropertyNames: wt, getOwnPropertySymbols: At, getPrototypeOf: kt } = Object, M = globalThis, G = M.trustedTypes, Et = G ? G.emptyScript : "", Ct = M.reactiveElementPolyfillSupport, E = (s, t) => s, F = { toAttribute(s, t) { + return b(e); +})(i) : i; +const { is: Ht, defineProperty: jt, getOwnPropertyDescriptor: Lt, getOwnPropertyNames: Rt, getOwnPropertySymbols: Bt, getPrototypeOf: Ft } = Object, F = globalThis, at = F.trustedTypes, Qt = at ? at.emptyScript : "", Wt = F.reactiveElementPolyfillSupport, U = (i, t) => i, X = { toAttribute(i, t) { switch (t) { case Boolean: - s = s ? Et : null; + i = i ? Qt : null; break; case Object: case Array: - s = s == null ? s : JSON.stringify(s); + i = i == null ? i : JSON.stringify(i); } - return s; -}, fromAttribute(s, t) { - let e = s; + return i; +}, fromAttribute(i, t) { + let e = i; switch (t) { case Boolean: - e = s !== null; + e = i !== null; break; case Number: - e = s === null ? null : Number(s); + e = i === null ? null : Number(i); break; case Object: case Array: try { - e = JSON.parse(s); + e = JSON.parse(i); } catch { e = null; } } return e; -} }, ht = (s, t) => !vt(s, t), X = { attribute: !0, type: String, converter: F, reflect: !1, useDefault: !1, hasChanged: ht }; -Symbol.metadata ??= /* @__PURE__ */ Symbol("metadata"), M.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap(); -let x = class extends HTMLElement { +} }, Mt = (i, t) => !Ht(i, t), lt = { attribute: !0, type: String, converter: X, reflect: !1, useDefault: !1, hasChanged: Mt }; +Symbol.metadata ??= /* @__PURE__ */ Symbol("metadata"), F.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap(); +let T = class extends HTMLElement { static addInitializer(t) { this._$Ei(), (this.l ??= []).push(t); } static get observedAttributes() { return this.finalize(), this._$Eh && [...this._$Eh.keys()]; } - static createProperty(t, e = X) { + static createProperty(t, e = lt) { if (e.state && (e.attribute = !1), this._$Ei(), this.prototype.hasOwnProperty(t) && ((e = Object.create(e)).wrapped = !0), this.elementProperties.set(t, e), !e.noAccessor) { - const n = /* @__PURE__ */ Symbol(), i = this.getPropertyDescriptor(t, n, e); - i !== void 0 && qt(this.prototype, t, i); + const n = /* @__PURE__ */ Symbol(), s = this.getPropertyDescriptor(t, n, e); + s !== void 0 && jt(this.prototype, t, s); } } static getPropertyDescriptor(t, e, n) { - const { get: i, set: r } = xt(this.prototype, t) ?? { get() { + const { get: s, set: r } = Lt(this.prototype, t) ?? { get() { return this[e]; - }, set(a) { - this[e] = a; + }, set(o) { + this[e] = o; } }; - return { get: i, set(a) { - const l = i?.call(this); - r?.call(this, a), this.requestUpdate(t, l, n); + return { get: s, set(o) { + const l = s?.call(this); + r?.call(this, o), this.requestUpdate(t, l, n); }, configurable: !0, enumerable: !0 }; } static getPropertyOptions(t) { - return this.elementProperties.get(t) ?? X; + return this.elementProperties.get(t) ?? lt; } static _$Ei() { - if (this.hasOwnProperty(E("elementProperties"))) return; - const t = kt(this); + if (this.hasOwnProperty(U("elementProperties"))) return; + const t = Ft(this); t.finalize(), t.l !== void 0 && (this.l = [...t.l]), this.elementProperties = new Map(t.elementProperties); } static finalize() { - if (this.hasOwnProperty(E("finalized"))) return; - if (this.finalized = !0, this._$Ei(), this.hasOwnProperty(E("properties"))) { - const e = this.properties, n = [...wt(e), ...At(e)]; - for (const i of n) this.createProperty(i, e[i]); + if (this.hasOwnProperty(U("finalized"))) return; + if (this.finalized = !0, this._$Ei(), this.hasOwnProperty(U("properties"))) { + const e = this.properties, n = [...Rt(e), ...Bt(e)]; + for (const s of n) this.createProperty(s, e[s]); } const t = this[Symbol.metadata]; if (t !== null) { const e = litPropertyMetadata.get(t); - if (e !== void 0) for (const [n, i] of e) this.elementProperties.set(n, i); + if (e !== void 0) for (const [n, s] of e) this.elementProperties.set(n, s); } this._$Eh = /* @__PURE__ */ new Map(); for (const [e, n] of this.elementProperties) { - const i = this._$Eu(e, n); - i !== void 0 && this._$Eh.set(i, e); + const s = this._$Eu(e, n); + s !== void 0 && this._$Eh.set(s, e); } this.elementStyles = this.finalizeStyles(this.styles); } @@ -119,8 +119,8 @@ let x = class extends HTMLElement { const e = []; if (Array.isArray(t)) { const n = new Set(t.flat(1 / 0).reverse()); - for (const i of n) e.unshift(J(i)); - } else t !== void 0 && e.push(J(t)); + for (const s of n) e.unshift(ot(s)); + } else t !== void 0 && e.push(ot(t)); return e; } static _$Eu(t, e) { @@ -146,7 +146,7 @@ let x = class extends HTMLElement { } createRenderRoot() { const t = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions); - return $t(t, this.constructor.elementStyles), t; + return Dt(t, this.constructor.elementStyles), t; } connectedCallback() { this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(!0), this._$EO?.forEach((t) => t.hostConnected?.()); @@ -160,31 +160,31 @@ let x = class extends HTMLElement { this._$AK(t, n); } _$ET(t, e) { - const n = this.constructor.elementProperties.get(t), i = this.constructor._$Eu(t, n); - if (i !== void 0 && n.reflect === !0) { - const r = (n.converter?.toAttribute !== void 0 ? n.converter : F).toAttribute(e, n.type); - this._$Em = t, r == null ? this.removeAttribute(i) : this.setAttribute(i, r), this._$Em = null; + const n = this.constructor.elementProperties.get(t), s = this.constructor._$Eu(t, n); + if (s !== void 0 && n.reflect === !0) { + const r = (n.converter?.toAttribute !== void 0 ? n.converter : X).toAttribute(e, n.type); + this._$Em = t, r == null ? this.removeAttribute(s) : this.setAttribute(s, r), this._$Em = null; } } _$AK(t, e) { - const n = this.constructor, i = n._$Eh.get(t); - if (i !== void 0 && this._$Em !== i) { - const r = n.getPropertyOptions(i), a = typeof r.converter == "function" ? { fromAttribute: r.converter } : r.converter?.fromAttribute !== void 0 ? r.converter : F; - this._$Em = i; - const l = a.fromAttribute(e, r.type); - this[i] = l ?? this._$Ej?.get(i) ?? l, this._$Em = null; + const n = this.constructor, s = n._$Eh.get(t); + if (s !== void 0 && this._$Em !== s) { + const r = n.getPropertyOptions(s), o = typeof r.converter == "function" ? { fromAttribute: r.converter } : r.converter?.fromAttribute !== void 0 ? r.converter : X; + this._$Em = s; + const l = o.fromAttribute(e, r.type); + this[s] = l ?? this._$Ej?.get(s) ?? l, this._$Em = null; } } - requestUpdate(t, e, n, i = !1, r) { + requestUpdate(t, e, n, s = !1, r) { if (t !== void 0) { - const a = this.constructor; - if (i === !1 && (r = this[t]), n ??= a.getPropertyOptions(t), !((n.hasChanged ?? ht)(r, e) || n.useDefault && n.reflect && r === this._$Ej?.get(t) && !this.hasAttribute(a._$Eu(t, n)))) return; + const o = this.constructor; + if (s === !1 && (r = this[t]), n ??= o.getPropertyOptions(t), !((n.hasChanged ?? Mt)(r, e) || n.useDefault && n.reflect && r === this._$Ej?.get(t) && !this.hasAttribute(o._$Eu(t, n)))) return; this.C(t, e, n); } this.isUpdatePending === !1 && (this._$ES = this._$EP()); } - C(t, e, { useDefault: n, reflect: i, wrapped: r }, a) { - n && !(this._$Ej ??= /* @__PURE__ */ new Map()).has(t) && (this._$Ej.set(t, a ?? e ?? this[t]), r !== !0 || a !== void 0) || (this._$AL.has(t) || (this.hasUpdated || n || (e = void 0), this._$AL.set(t, e)), i === !0 && this._$Em !== t && (this._$Eq ??= /* @__PURE__ */ new Set()).add(t)); + C(t, e, { useDefault: n, reflect: s, wrapped: r }, o) { + n && !(this._$Ej ??= /* @__PURE__ */ new Map()).has(t) && (this._$Ej.set(t, o ?? e ?? this[t]), r !== !0 || o !== void 0) || (this._$AL.has(t) || (this.hasUpdated || n || (e = void 0), this._$AL.set(t, e)), s === !0 && this._$Em !== t && (this._$Eq ??= /* @__PURE__ */ new Set()).add(t)); } async _$EP() { this.isUpdatePending = !0; @@ -203,13 +203,13 @@ let x = class extends HTMLElement { if (!this.isUpdatePending) return; if (!this.hasUpdated) { if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) { - for (const [i, r] of this._$Ep) this[i] = r; + for (const [s, r] of this._$Ep) this[s] = r; this._$Ep = void 0; } const n = this.constructor.elementProperties; - if (n.size > 0) for (const [i, r] of n) { - const { wrapped: a } = r, l = this[i]; - a !== !0 || this._$AL.has(i) || l === void 0 || this.C(i, void 0, r, l); + if (n.size > 0) for (const [s, r] of n) { + const { wrapped: o } = r, l = this[s]; + o !== !0 || this._$AL.has(s) || l === void 0 || this.C(s, void 0, r, l); } } let t = !1; @@ -246,70 +246,70 @@ let x = class extends HTMLElement { firstUpdated(t) { } }; -x.elementStyles = [], x.shadowRootOptions = { mode: "open" }, x[E("elementProperties")] = /* @__PURE__ */ new Map(), x[E("finalized")] = /* @__PURE__ */ new Map(), Ct?.({ ReactiveElement: x }), (M.reactiveElementVersions ??= []).push("2.1.2"); -const B = globalThis, Y = (s) => s, z = B.trustedTypes, tt = z ? z.createPolicy("lit-html", { createHTML: (s) => s }) : void 0, ut = "$lit$", y = `lit$${Math.random().toFixed(9).slice(2)}$`, ft = "?" + y, St = `<${ft}>`, q = document, C = () => q.createComment(""), S = (s) => s === null || typeof s != "object" && typeof s != "function", Q = Array.isArray, Nt = (s) => Q(s) || typeof s?.[Symbol.iterator] == "function", D = `[ -\f\r]`, k = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, et = /-->/g, nt = />/g, b = RegExp(`>|${D}(?:([^\\s"'>=/]+)(${D}*=${D}*(?:[^ -\f\r"'\`<>=]|("|')|))|$)`, "g"), st = /'/g, it = /"/g, gt = /^(?:script|style|textarea|title)$/i, Pt = (s) => (t, ...e) => ({ _$litType$: s, strings: t, values: e }), g = Pt(1), w = /* @__PURE__ */ Symbol.for("lit-noChange"), c = /* @__PURE__ */ Symbol.for("lit-nothing"), rt = /* @__PURE__ */ new WeakMap(), _ = q.createTreeWalker(q, 129); -function mt(s, t) { - if (!Q(s) || !s.hasOwnProperty("raw")) throw Error("invalid template strings array"); - return tt !== void 0 ? tt.createHTML(t) : t; +T.elementStyles = [], T.shadowRootOptions = { mode: "open" }, T[U("elementProperties")] = /* @__PURE__ */ new Map(), T[U("finalized")] = /* @__PURE__ */ new Map(), Wt?.({ ReactiveElement: T }), (F.reactiveElementVersions ??= []).push("2.1.2"); +const et = globalThis, dt = (i) => i, R = et.trustedTypes, pt = R ? R.createPolicy("lit-html", { createHTML: (i) => i }) : void 0, zt = "$lit$", x = `lit$${Math.random().toFixed(9).slice(2)}$`, Pt = "?" + x, Zt = `<${Pt}>`, S = document, I = () => S.createComment(""), D = (i) => i === null || typeof i != "object" && typeof i != "function", nt = Array.isArray, Vt = (i) => nt(i) || typeof i?.[Symbol.iterator] == "function", W = `[ +\f\r]`, P = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, ct = /-->/g, ht = />/g, q = RegExp(`>|${W}(?:([^\\s"'>=/]+)(${W}*=${W}*(?:[^ +\f\r"'\`<>=]|("|')|))|$)`, "g"), ut = /'/g, ft = /"/g, Ot = /^(?:script|style|textarea|title)$/i, Kt = (i) => (t, ...e) => ({ _$litType$: i, strings: t, values: e }), g = Kt(1), E = /* @__PURE__ */ Symbol.for("lit-noChange"), m = /* @__PURE__ */ Symbol.for("lit-nothing"), mt = /* @__PURE__ */ new WeakMap(), A = S.createTreeWalker(S, 129); +function Ut(i, t) { + if (!nt(i) || !i.hasOwnProperty("raw")) throw Error("invalid template strings array"); + return pt !== void 0 ? pt.createHTML(t) : t; } -const Tt = (s, t) => { - const e = s.length - 1, n = []; - let i, r = t === 2 ? "" : t === 3 ? "" : "", a = k; +const Jt = (i, t) => { + const e = i.length - 1, n = []; + let s, r = t === 2 ? "" : t === 3 ? "" : "", o = P; for (let l = 0; l < e; l++) { - const o = s[l]; - let d, h, p = -1, f = 0; - for (; f < o.length && (a.lastIndex = f, h = a.exec(o), h !== null); ) f = a.lastIndex, a === k ? h[1] === "!--" ? a = et : h[1] !== void 0 ? a = nt : h[2] !== void 0 ? (gt.test(h[2]) && (i = RegExp("" ? (a = i ?? k, p = -1) : h[1] === void 0 ? p = -2 : (p = a.lastIndex - h[2].length, d = h[1], a = h[3] === void 0 ? b : h[3] === '"' ? it : st) : a === it || a === st ? a = b : a === et || a === nt ? a = k : (a = b, i = void 0); - const u = a === b && s[l + 1].startsWith("/>") ? " " : ""; - r += a === k ? o + St : p >= 0 ? (n.push(d), o.slice(0, p) + ut + o.slice(p) + y + u) : o + y + (p === -2 ? l : u); + const a = i[l]; + let p, u, d = -1, h = 0; + for (; h < a.length && (o.lastIndex = h, u = o.exec(a), u !== null); ) h = o.lastIndex, o === P ? u[1] === "!--" ? o = ct : u[1] !== void 0 ? o = ht : u[2] !== void 0 ? (Ot.test(u[2]) && (s = RegExp("" ? (o = s ?? P, d = -1) : u[1] === void 0 ? d = -2 : (d = o.lastIndex - u[2].length, p = u[1], o = u[3] === void 0 ? q : u[3] === '"' ? ft : ut) : o === ft || o === ut ? o = q : o === ct || o === ht ? o = P : (o = q, s = void 0); + const c = o === q && i[l + 1].startsWith("/>") ? " " : ""; + r += o === P ? a + Zt : d >= 0 ? (n.push(p), a.slice(0, d) + zt + a.slice(d) + x + c) : a + x + (d === -2 ? l : c); } - return [mt(s, r + (s[e] || "") + (t === 2 ? "" : t === 3 ? "" : "")), n]; + return [Ut(i, r + (i[e] || "") + (t === 2 ? "" : t === 3 ? "" : "")), n]; }; -class N { +class H { constructor({ strings: t, _$litType$: e }, n) { - let i; + let s; this.parts = []; - let r = 0, a = 0; - const l = t.length - 1, o = this.parts, [d, h] = Tt(t, e); - if (this.el = N.createElement(d, n), _.currentNode = this.el.content, e === 2 || e === 3) { - const p = this.el.content.firstChild; - p.replaceWith(...p.childNodes); + let r = 0, o = 0; + const l = t.length - 1, a = this.parts, [p, u] = Jt(t, e); + if (this.el = H.createElement(p, n), A.currentNode = this.el.content, e === 2 || e === 3) { + const d = this.el.content.firstChild; + d.replaceWith(...d.childNodes); } - for (; (i = _.nextNode()) !== null && o.length < l; ) { - if (i.nodeType === 1) { - if (i.hasAttributes()) for (const p of i.getAttributeNames()) if (p.endsWith(ut)) { - const f = h[a++], u = i.getAttribute(p).split(y), m = /([.?@])?(.*)/.exec(f); - o.push({ type: 1, index: r, name: m[2], strings: u, ctor: m[1] === "." ? Ot : m[1] === "?" ? Mt : m[1] === "@" ? Ut : U }), i.removeAttribute(p); - } else p.startsWith(y) && (o.push({ type: 6, index: r }), i.removeAttribute(p)); - if (gt.test(i.tagName)) { - const p = i.textContent.split(y), f = p.length - 1; - if (f > 0) { - i.textContent = z ? z.emptyScript : ""; - for (let u = 0; u < f; u++) i.append(p[u], C()), _.nextNode(), o.push({ type: 2, index: ++r }); - i.append(p[f], C()); + for (; (s = A.nextNode()) !== null && a.length < l; ) { + if (s.nodeType === 1) { + if (s.hasAttributes()) for (const d of s.getAttributeNames()) if (d.endsWith(zt)) { + const h = u[o++], c = s.getAttribute(d).split(x), f = /([.?@])?(.*)/.exec(h); + a.push({ type: 1, index: r, name: f[2], strings: c, ctor: f[1] === "." ? Xt : f[1] === "?" ? Yt : f[1] === "@" ? te : Q }), s.removeAttribute(d); + } else d.startsWith(x) && (a.push({ type: 6, index: r }), s.removeAttribute(d)); + if (Ot.test(s.tagName)) { + const d = s.textContent.split(x), h = d.length - 1; + if (h > 0) { + s.textContent = R ? R.emptyScript : ""; + for (let c = 0; c < h; c++) s.append(d[c], I()), A.nextNode(), a.push({ type: 2, index: ++r }); + s.append(d[h], I()); } } - } else if (i.nodeType === 8) if (i.data === ft) o.push({ type: 2, index: r }); + } else if (s.nodeType === 8) if (s.data === Pt) a.push({ type: 2, index: r }); else { - let p = -1; - for (; (p = i.data.indexOf(y, p + 1)) !== -1; ) o.push({ type: 7, index: r }), p += y.length - 1; + let d = -1; + for (; (d = s.data.indexOf(x, d + 1)) !== -1; ) a.push({ type: 7, index: r }), d += x.length - 1; } r++; } } static createElement(t, e) { - const n = q.createElement("template"); + const n = S.createElement("template"); return n.innerHTML = t, n; } } -function A(s, t, e = s, n) { - if (t === w) return t; - let i = n !== void 0 ? e._$Co?.[n] : e._$Cl; - const r = S(t) ? void 0 : t._$litDirective$; - return i?.constructor !== r && (i?._$AO?.(!1), r === void 0 ? i = void 0 : (i = new r(s), i._$AT(s, e, n)), n !== void 0 ? (e._$Co ??= [])[n] = i : e._$Cl = i), i !== void 0 && (t = A(s, i._$AS(s, t.values), i, n)), t; +function M(i, t, e = i, n) { + if (t === E) return t; + let s = n !== void 0 ? e._$Co?.[n] : e._$Cl; + const r = D(t) ? void 0 : t._$litDirective$; + return s?.constructor !== r && (s?._$AO?.(!1), r === void 0 ? s = void 0 : (s = new r(i), s._$AT(i, e, n)), n !== void 0 ? (e._$Co ??= [])[n] = s : e._$Cl = s), s !== void 0 && (t = M(i, s._$AS(i, t.values), s, n)), t; } -class zt { +class Gt { constructor(t, e) { this._$AV = [], this._$AN = void 0, this._$AD = t, this._$AM = e; } @@ -320,29 +320,29 @@ class zt { return this._$AM._$AU; } u(t) { - const { el: { content: e }, parts: n } = this._$AD, i = (t?.creationScope ?? q).importNode(e, !0); - _.currentNode = i; - let r = _.nextNode(), a = 0, l = 0, o = n[0]; - for (; o !== void 0; ) { - if (a === o.index) { - let d; - o.type === 2 ? d = new P(r, r.nextSibling, this, t) : o.type === 1 ? d = new o.ctor(r, o.name, o.strings, this, t) : o.type === 6 && (d = new Ht(r, this, t)), this._$AV.push(d), o = n[++l]; + const { el: { content: e }, parts: n } = this._$AD, s = (t?.creationScope ?? S).importNode(e, !0); + A.currentNode = s; + let r = A.nextNode(), o = 0, l = 0, a = n[0]; + for (; a !== void 0; ) { + if (o === a.index) { + let p; + a.type === 2 ? p = new z(r, r.nextSibling, this, t) : a.type === 1 ? p = new a.ctor(r, a.name, a.strings, this, t) : a.type === 6 && (p = new ee(r, this, t)), this._$AV.push(p), a = n[++l]; } - a !== o?.index && (r = _.nextNode(), a++); + o !== a?.index && (r = A.nextNode(), o++); } - return _.currentNode = q, i; + return A.currentNode = S, s; } p(t) { let e = 0; for (const n of this._$AV) n !== void 0 && (n.strings !== void 0 ? (n._$AI(t, n, e), e += n.strings.length - 2) : n._$AI(t[e])), e++; } } -class P { +class z { get _$AU() { return this._$AM?._$AU ?? this._$Cv; } - constructor(t, e, n, i) { - this.type = 2, this._$AH = c, this._$AN = void 0, this._$AA = t, this._$AB = e, this._$AM = n, this.options = i, this._$Cv = i?.isConnected ?? !0; + constructor(t, e, n, s) { + this.type = 2, this._$AH = m, this._$AN = void 0, this._$AA = t, this._$AB = e, this._$AM = n, this.options = s, this._$Cv = s?.isConnected ?? !0; } get parentNode() { let t = this._$AA.parentNode; @@ -356,7 +356,7 @@ class P { return this._$AB; } _$AI(t, e = this) { - t = A(this, t, e), S(t) ? t === c || t == null || t === "" ? (this._$AH !== c && this._$AR(), this._$AH = c) : t !== this._$AH && t !== w && this._(t) : t._$litType$ !== void 0 ? this.$(t) : t.nodeType !== void 0 ? this.T(t) : Nt(t) ? this.k(t) : this._(t); + t = M(this, t, e), D(t) ? t === m || t == null || t === "" ? (this._$AH !== m && this._$AR(), this._$AH = m) : t !== this._$AH && t !== E && this._(t) : t._$litType$ !== void 0 ? this.$(t) : t.nodeType !== void 0 ? this.T(t) : Vt(t) ? this.k(t) : this._(t); } O(t) { return this._$AA.parentNode.insertBefore(t, this._$AB); @@ -365,92 +365,92 @@ class P { this._$AH !== t && (this._$AR(), this._$AH = this.O(t)); } _(t) { - this._$AH !== c && S(this._$AH) ? this._$AA.nextSibling.data = t : this.T(q.createTextNode(t)), this._$AH = t; + this._$AH !== m && D(this._$AH) ? this._$AA.nextSibling.data = t : this.T(S.createTextNode(t)), this._$AH = t; } $(t) { - const { values: e, _$litType$: n } = t, i = typeof n == "number" ? this._$AC(t) : (n.el === void 0 && (n.el = N.createElement(mt(n.h, n.h[0]), this.options)), n); - if (this._$AH?._$AD === i) this._$AH.p(e); + const { values: e, _$litType$: n } = t, s = typeof n == "number" ? this._$AC(t) : (n.el === void 0 && (n.el = H.createElement(Ut(n.h, n.h[0]), this.options)), n); + if (this._$AH?._$AD === s) this._$AH.p(e); else { - const r = new zt(i, this), a = r.u(this.options); - r.p(e), this.T(a), this._$AH = r; + const r = new Gt(s, this), o = r.u(this.options); + r.p(e), this.T(o), this._$AH = r; } } _$AC(t) { - let e = rt.get(t.strings); - return e === void 0 && rt.set(t.strings, e = new N(t)), e; + let e = mt.get(t.strings); + return e === void 0 && mt.set(t.strings, e = new H(t)), e; } k(t) { - Q(this._$AH) || (this._$AH = [], this._$AR()); + nt(this._$AH) || (this._$AH = [], this._$AR()); const e = this._$AH; - let n, i = 0; - for (const r of t) i === e.length ? e.push(n = new P(this.O(C()), this.O(C()), this, this.options)) : n = e[i], n._$AI(r), i++; - i < e.length && (this._$AR(n && n._$AB.nextSibling, i), e.length = i); + let n, s = 0; + for (const r of t) s === e.length ? e.push(n = new z(this.O(I()), this.O(I()), this, this.options)) : n = e[s], n._$AI(r), s++; + s < e.length && (this._$AR(n && n._$AB.nextSibling, s), e.length = s); } _$AR(t = this._$AA.nextSibling, e) { for (this._$AP?.(!1, !0, e); t !== this._$AB; ) { - const n = Y(t).nextSibling; - Y(t).remove(), t = n; + const n = dt(t).nextSibling; + dt(t).remove(), t = n; } } setConnected(t) { this._$AM === void 0 && (this._$Cv = t, this._$AP?.(t)); } } -class U { +class Q { get tagName() { return this.element.tagName; } get _$AU() { return this._$AM._$AU; } - constructor(t, e, n, i, r) { - this.type = 1, this._$AH = c, this._$AN = void 0, this.element = t, this.name = e, this._$AM = i, this.options = r, n.length > 2 || n[0] !== "" || n[1] !== "" ? (this._$AH = Array(n.length - 1).fill(new String()), this.strings = n) : this._$AH = c; + constructor(t, e, n, s, r) { + this.type = 1, this._$AH = m, this._$AN = void 0, this.element = t, this.name = e, this._$AM = s, this.options = r, n.length > 2 || n[0] !== "" || n[1] !== "" ? (this._$AH = Array(n.length - 1).fill(new String()), this.strings = n) : this._$AH = m; } - _$AI(t, e = this, n, i) { + _$AI(t, e = this, n, s) { const r = this.strings; - let a = !1; - if (r === void 0) t = A(this, t, e, 0), a = !S(t) || t !== this._$AH && t !== w, a && (this._$AH = t); + let o = !1; + if (r === void 0) t = M(this, t, e, 0), o = !D(t) || t !== this._$AH && t !== E, o && (this._$AH = t); else { const l = t; - let o, d; - for (t = r[0], o = 0; o < r.length - 1; o++) d = A(this, l[n + o], e, o), d === w && (d = this._$AH[o]), a ||= !S(d) || d !== this._$AH[o], d === c ? t = c : t !== c && (t += (d ?? "") + r[o + 1]), this._$AH[o] = d; + let a, p; + for (t = r[0], a = 0; a < r.length - 1; a++) p = M(this, l[n + a], e, a), p === E && (p = this._$AH[a]), o ||= !D(p) || p !== this._$AH[a], p === m ? t = m : t !== m && (t += (p ?? "") + r[a + 1]), this._$AH[a] = p; } - a && !i && this.j(t); + o && !s && this.j(t); } j(t) { - t === c ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t ?? ""); + t === m ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t ?? ""); } } -class Ot extends U { +class Xt extends Q { constructor() { super(...arguments), this.type = 3; } j(t) { - this.element[this.name] = t === c ? void 0 : t; + this.element[this.name] = t === m ? void 0 : t; } } -class Mt extends U { +class Yt extends Q { constructor() { super(...arguments), this.type = 4; } j(t) { - this.element.toggleAttribute(this.name, !!t && t !== c); + this.element.toggleAttribute(this.name, !!t && t !== m); } } -class Ut extends U { - constructor(t, e, n, i, r) { - super(t, e, n, i, r), this.type = 5; +class te extends Q { + constructor(t, e, n, s, r) { + super(t, e, n, s, r), this.type = 5; } _$AI(t, e = this) { - if ((t = A(this, t, e, 0) ?? c) === w) return; - const n = this._$AH, i = t === c && n !== c || t.capture !== n.capture || t.once !== n.once || t.passive !== n.passive, r = t !== c && (n === c || i); - i && this.element.removeEventListener(this.name, this, n), r && this.element.addEventListener(this.name, this, t), this._$AH = t; + if ((t = M(this, t, e, 0) ?? m) === E) return; + const n = this._$AH, s = t === m && n !== m || t.capture !== n.capture || t.once !== n.once || t.passive !== n.passive, r = t !== m && (n === m || s); + s && this.element.removeEventListener(this.name, this, n), r && this.element.addEventListener(this.name, this, t), this._$AH = t; } handleEvent(t) { typeof this._$AH == "function" ? this._$AH.call(this.options?.host ?? this.element, t) : this._$AH.handleEvent(t); } } -class Ht { +class ee { constructor(t, e, n) { this.element = t, this.type = 6, this._$AN = void 0, this._$AM = e, this.options = n; } @@ -458,22 +458,22 @@ class Ht { return this._$AM._$AU; } _$AI(t) { - A(this, t); + M(this, t); } } -const Dt = B.litHtmlPolyfillSupport; -Dt?.(N, P), (B.litHtmlVersions ??= []).push("3.3.3"); -const Rt = (s, t, e) => { +const ne = { I: z }, ie = et.litHtmlPolyfillSupport; +ie?.(H, z), (et.litHtmlVersions ??= []).push("3.3.3"); +const se = (i, t, e) => { const n = e?.renderBefore ?? t; - let i = n._$litPart$; - if (i === void 0) { + let s = n._$litPart$; + if (s === void 0) { const r = e?.renderBefore ?? null; - n._$litPart$ = i = new P(t.insertBefore(C(), r), r, void 0, e ?? {}); + n._$litPart$ = s = new z(t.insertBefore(I(), r), r, void 0, e ?? {}); } - return i._$AI(s), i; + return s._$AI(i), s; }; -const W = globalThis; -class v extends x { +const it = globalThis; +let C = class extends T { constructor() { super(...arguments), this.renderOptions = { host: this }, this._$Do = void 0; } @@ -483,7 +483,7 @@ class v extends x { } update(t) { const e = this.render(); - this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t), this._$Do = Rt(e, this.renderRoot, this.renderOptions); + this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t), this._$Do = se(e, this.renderRoot, this.renderOptions); } connectedCallback() { super.connectedCallback(), this._$Do?.setConnected(!0); @@ -492,51 +492,51 @@ class v extends x { super.disconnectedCallback(), this._$Do?.setConnected(!1); } render() { - return w; + return E; } +}; +C._$litElement$ = !0, C.finalized = !0, it.litElementHydrateSupport?.({ LitElement: C }); +const re = it.litElementPolyfillSupport; +re?.({ LitElement: C }); +(it.litElementVersions ??= []).push("4.2.2"); +const It = ':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 st(i) { + window.customCards = window.customCards ?? [], window.customCards.push(i); } -v._$litElement$ = !0, v.finalized = !0, W.litElementHydrateSupport?.({ LitElement: v }); -const It = W.litElementPolyfillSupport; -It?.({ LitElement: v }); -(W.litElementVersions ??= []).push("4.2.2"); -const yt = ':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-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 V(s) { - window.customCards = window.customCards ?? [], window.customCards.push(s); -} -const at = /* @__PURE__ */ new Map(); -function O(s, t) { - const e = `${s ?? "local"}|${JSON.stringify(t)}`, n = at.get(e); +const gt = /* @__PURE__ */ new Map(); +function B(i, t) { + const e = `${i ?? "local"}|${JSON.stringify(t)}`, n = gt.get(e); if (n) return n; - let i; + let s; try { - i = new Intl.DateTimeFormat("en-US", { + s = new Intl.DateTimeFormat("en-US", { ...t, - ...s ? { timeZone: s } : {} + ...i ? { timeZone: i } : {} }); } catch { - i = new Intl.DateTimeFormat("en-US", t); + s = new Intl.DateTimeFormat("en-US", t); } - return at.set(e, i), i; + return gt.set(e, s), s; } -function Ft(s) { - const t = s?.config; +function oe(i) { + const t = i?.config; if (!t || typeof t != "object") return; const e = t.time_zone; return typeof e == "string" && e.trim() ? e.trim() : void 0; } -function R(s, t) { - return `${O(t, { weekday: "long" }).format(s)}, ${O(t, { month: "short", day: "numeric" }).format(s)}`; +function Z(i, t) { + return `${B(t, { weekday: "long" }).format(i)}, ${B(t, { month: "short", day: "numeric" }).format(i)}`; } -function Lt(s, t) { - return O(t, { +function ae(i, t) { + return B(t, { hour: "numeric", minute: "2-digit", hour12: !0 - }).format(s); + }).format(i); } -const jt = { +const le = { "clear-night": "Clear", cloudy: "Cloudy", exceptional: "Clear", @@ -552,7 +552,7 @@ const jt = { sunny: "Sunny", windy: "Windy", "windy-variant": "Windy" -}, Bt = { +}, de = { "clear-night": "Clear night skies", cloudy: "Grey skies today", exceptional: "A striking day", @@ -569,26 +569,26 @@ const jt = { windy: "A blustery day", "windy-variant": "A blustery day" }; -function ot(s) { - return typeof s == "string" ? s.trim() : ""; +function _t(i) { + return typeof i == "string" ? i.trim() : ""; } -function I(s, t) { - if (s == null || s === "") +function V(i, t) { + if (i == null || i === "") return t; - const e = typeof s == "number" ? s : Number(s); + const e = typeof i == "number" ? i : Number(i); return Number.isFinite(e) ? e : t; } -function lt(s) { - return Number.isFinite(s) ? Math.min(100, Math.max(0, s)) : 0; +function yt(i) { + return Number.isFinite(i) ? Math.min(100, Math.max(0, i)) : 0; } -function pt(s) { - return Math.max(0, Math.round(s)); +function bt(i) { + return Math.max(0, Math.round(i)); } -function Qt(s) { - return s.split(/[-_]+/).filter(Boolean).map((t) => t.charAt(0).toUpperCase() + t.slice(1)).join(" "); +function pe(i) { + return i.split(/[-_]+/).filter(Boolean).map((t) => t.charAt(0).toUpperCase() + t.slice(1)).join(" "); } -function Wt(s, t) { - const e = O(t, { +function ce(i, t) { + const e = B(t, { year: "numeric", month: "2-digit", day: "2-digit", @@ -596,10 +596,10 @@ function Wt(s, t) { minute: "2-digit", second: "2-digit", hourCycle: "h23" - }).formatToParts(new Date(s)), n = (r) => { - const a = e.find((l) => l.type === r); - return a ? Number(a.value) : Number.NaN; - }, i = Date.UTC( + }).formatToParts(new Date(i)), n = (r) => { + const o = e.find((l) => l.type === r); + return o ? Number(o.value) : Number.NaN; + }, s = Date.UTC( n("year"), n("month") - 1, n("day"), @@ -607,32 +607,32 @@ function Wt(s, t) { n("minute"), n("second") ); - return Number.isFinite(i) ? i - s : 0; + return Number.isFinite(s) ? s - i : 0; } -function Vt(s, t) { - if (typeof s != "string") +function he(i, t) { + if (typeof i != "string") return null; - const e = s.split("-"); + const e = i.split("-"); if (e.length !== 3) return null; - const [n, i, r] = e.map((o) => Number(o)); - if (!n || !i || !r) + const [n, s, r] = e.map((a) => Number(a)); + if (!n || !s || !r) return null; if (!t) { - const o = new Date(n, i - 1, r); - return Number.isNaN(o.getTime()) ? null : o; + const a = new Date(n, s - 1, r); + return Number.isNaN(a.getTime()) ? null : a; } - const a = Date.UTC(n, i - 1, r), l = new Date(a - Wt(a, t)); + const o = Date.UTC(n, s - 1, r), l = new Date(o - ce(o, t)); return Number.isNaN(l.getTime()) ? null : l; } -function Zt(s) { - const t = jt[s]; - return t || s.charAt(0).toUpperCase() + s.slice(1); +function ue(i) { + const t = le[i]; + return t || i.charAt(0).toUpperCase() + i.slice(1); } -function Kt(s, t) { - return Bt[s] ?? t; +function fe(i, t) { + return de[i] ?? t; } -const dt = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Jt = "polygon(50% 0, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%)", Gt = _t` +const vt = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", me = "polygon(50% 0, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%)", ge = Tt` * { box-sizing: border-box; } @@ -704,7 +704,7 @@ const dt = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Jt = "polygon(50% justify-content: center; width: 34px; height: 34px; - clip-path: ${$(Jt)}; + clip-path: ${b(me)}; background: var(--nq-brand-gradient); } @@ -798,7 +798,7 @@ const dt = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Jt = "polygon(50% width: 188px; height: 214px; padding: 4px; - clip-path: ${$(dt)}; + clip-path: ${b(vt)}; background: rgba(255, 255, 255, 0.22); } @@ -809,7 +809,7 @@ const dt = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Jt = "polygon(50% align-items: center; justify-content: center; padding-bottom: 70px; - clip-path: ${$(dt)}; + clip-path: ${b(vt)}; background: var(--nq-p-crest); } @@ -971,7 +971,7 @@ const dt = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Jt = "polygon(50% font-weight: 800; line-height: 1; } -`, Xt = g` -`, Yt = g``, ye = g` -`, te = g``, be = g` `; -class ee extends v { +class ve extends C { static properties = { hass: { attribute: !1 }, _config: { state: !0 }, _now: { state: !0 } }; - static styles = [$(yt), Gt]; + static styles = [b(It), ge]; hass; _config; _now = /* @__PURE__ */ new Date(); @@ -1052,7 +1052,7 @@ class ee extends v { -

The Party · ${R(this._now, this._timeZone())}

+

The Party · ${Z(this._now, this._timeZone())}

${this._plates().map((t) => this._renderPlate(t))}
@@ -1063,7 +1063,7 @@ class ee extends v { `; } _timeZone() { - return Ft(this.hass); + return oe(this.hass); } _state(t) { const n = this.hass?.states?.[t]; @@ -1083,36 +1083,36 @@ class ee extends v { _plate(t) { const e = this._state(`sensor.nestquest_${t}_quests_due_today`), n = this._state( `sensor.nestquest_${t}_quests_completed_today` - ), i = this._state( + ), s = this._state( `sensor.nestquest_${t}_completion_pct_today` ), r = this._state( `binary_sensor.nestquest_${t}_present_today` - ), a = e?.attributes ?? {}, l = pt(I(e?.state, 0)), o = pt(I(n?.state, 0)), d = i ? I(i.state, Number.NaN) : Number.NaN, h = Number.isFinite(d) ? lt(d) : l > 0 ? lt(o / l * 100) : 0; - let p = !0; - const f = a.present; - typeof f == "boolean" ? p = f : r && (p = String(r.state ?? "").trim().toLowerCase() === "on"); - let u = ""; - const m = a.child_name; - if (typeof m == "string" && m.trim()) - u = m.trim(); + ), o = e?.attributes ?? {}, l = bt(V(e?.state, 0)), a = bt(V(n?.state, 0)), p = s ? V(s.state, Number.NaN) : Number.NaN, u = Number.isFinite(p) ? yt(p) : l > 0 ? yt(a / l * 100) : 0; + let d = !0; + const h = o.present; + typeof h == "boolean" ? d = h : r && (d = String(r.state ?? "").trim().toLowerCase() === "on"); + let c = ""; + const f = o.child_name; + if (typeof f == "string" && f.trim()) + c = f.trim(); else { - const H = r?.attributes?.child_name; - typeof H == "string" && H.trim() && (u = H.trim()); + const N = r?.attributes?.child_name; + typeof N == "string" && N.trim() && (c = N.trim()); } - u || (u = Qt(t)); - const Z = Vt( + c || (c = pe(t)); + const _ = he( r?.attributes?.next_present, this._timeZone() - ), bt = Z ? `Returns ${R(Z, this._timeZone())}` : null; + ), v = _ ? `Returns ${Z(_, this._timeZone())}` : null; return { slug: t, - name: u, - initial: (u.charAt(0) || "?").toUpperCase(), - present: p, - completed: o, + name: c, + initial: (c.charAt(0) || "?").toUpperCase(), + present: d, + completed: a, due: l, - pct: h, - returns: bt + pct: u, + returns: v }; } _renderPlate(t) { @@ -1130,7 +1130,7 @@ class ee extends v { ${t.name} - ${Xt} + ${_e} Home today @@ -1149,7 +1149,7 @@ class ee extends v { ${t.name} - ${Yt} + ${ye} On travels ${t.returns ?? "Returns —"} @@ -1158,11 +1158,11 @@ class ee extends v { `; } _openQuestLog(t) { - const e = ot(this._config?.quest_log_path); + const e = _t(this._config?.quest_log_path); e && (window.history.pushState(null, "", `${e.replace(/\/+$/, "")}/${t}`), window.dispatchEvent(new Event("location-changed"))); } _dockWeather() { - const t = ot(this._config?.weather_entity); + const t = _t(this._config?.weather_entity); if (!t) return null; const e = this._state(t); @@ -1171,21 +1171,21 @@ class ee extends v { const n = String(e.state ?? "").trim(); if (!n || n === "unavailable" || n === "unknown") return null; - const i = e.attributes ?? {}; - let r = null, a = null; - const l = i.forecast; + const s = e.attributes ?? {}; + let r = null, o = null; + const l = s.forecast; if (Array.isArray(l) && l.length > 0) { - const o = l[0]; - if (o && typeof o == "object") { - const d = o; - r = this._optionalNumber(d.temperature), a = this._optionalNumber(d.templow); + const a = l[0]; + if (a && typeof a == "object") { + const p = a; + r = this._optionalNumber(p.temperature), o = this._optionalNumber(p.templow); } } return { condition: n, - temperature: this._optionalNumber(i.temperature), + temperature: this._optionalNumber(s.temperature), high: r, - low: a + low: o }; } _optionalNumber(t) { @@ -1199,16 +1199,16 @@ class ee extends v { if (!t) return g`
- ${R(this._now, this._timeZone())} + ${Z(this._now, this._timeZone())} - ${Lt(this._now, this._timeZone())} + ${ae(this._now, this._timeZone())}
`; - const e = Zt(t.condition), n = Kt(t.condition, e), i = t.temperature === null ? c : g`${Math.round(t.temperature)}°`, r = t.high === null || t.low === null ? null : `${Math.round(t.high)}° / ${Math.round(t.low)}°`; + const e = ue(t.condition), n = fe(t.condition, e), s = t.temperature === null ? m : g`${Math.round(t.temperature)}°`, r = t.high === null || t.low === null ? null : `${Math.round(t.high)}° / ${Math.round(t.low)}°`; return g`
- ${te} - ${i} + ${be} + ${s} ${r === null ? e : `${e} · ${r}`} @@ -1219,45 +1219,1288 @@ class ee extends v { `; } } -customElements.define("nestquest-party-board-card", ee); -V({ +customElements.define("nestquest-party-board-card", ve); +st({ type: "nestquest-party-board-card", name: "NestQuest Party Board", description: "Kids' attract screen for present and away adventurers." }); -class ne extends v { +const $e = { CHILD: 2 }, xe = (i) => (...t) => ({ _$litDirective$: i, values: t }); +let qe = class { + constructor(t) { + } + get _$AU() { + return this._$AM._$AU; + } + _$AT(t, e, n) { + this._$Ct = t, this._$AM = e, this._$Ci = n; + } + _$AS(t, e) { + return this.update(t, e); + } + update(t, e) { + return this.render(...e); + } +}; +const { I: we } = ne, $t = (i) => i, xt = () => document.createComment(""), O = (i, t, e) => { + const n = i._$AA.parentNode, s = t === void 0 ? i._$AB : t._$AA; + if (e === void 0) { + const r = n.insertBefore(xt(), s), o = n.insertBefore(xt(), s); + e = new we(r, o, i, i.options); + } else { + const r = e._$AB.nextSibling, o = e._$AM, l = o !== i; + if (l) { + let a; + e._$AQ?.(i), e._$AM = i, e._$AP !== void 0 && (a = i._$AU) !== o._$AU && e._$AP(a); + } + if (r !== s || l) { + let a = e._$AA; + for (; a !== r; ) { + const p = $t(a).nextSibling; + $t(n).insertBefore(a, s), a = p; + } + } + } + return e; +}, w = (i, t, e = i) => (i._$AI(t, e), i), ke = {}, Ae = (i, t = ke) => i._$AH = t, Ce = (i) => i._$AH, K = (i) => { + i._$AR(), i._$AA.remove(); +}; +const qt = (i, t, e) => { + const n = /* @__PURE__ */ new Map(); + for (let s = t; s <= e; s++) n.set(i[s], s); + return n; +}, wt = xe(class extends qe { + constructor(i) { + if (super(i), i.type !== $e.CHILD) throw Error("repeat() can only be used in text expressions"); + } + dt(i, t, e) { + let n; + e === void 0 ? e = t : t !== void 0 && (n = t); + const s = [], r = []; + let o = 0; + for (const l of i) s[o] = n ? n(l, o) : o, r[o] = e(l, o), o++; + return { values: r, keys: s }; + } + render(i, t, e) { + return this.dt(i, t, e).values; + } + update(i, [t, e, n]) { + const s = Ce(i), { values: r, keys: o } = this.dt(t, e, n); + if (!Array.isArray(s)) return this.ut = o, r; + const l = this.ut ??= [], a = []; + let p, u, d = 0, h = s.length - 1, c = 0, f = r.length - 1; + for (; d <= h && c <= f; ) if (s[d] === null) d++; + else if (s[h] === null) h--; + else if (l[d] === o[c]) a[c] = w(s[d], r[c]), d++, c++; + else if (l[h] === o[f]) a[f] = w(s[h], r[f]), h--, f--; + else if (l[d] === o[f]) a[f] = w(s[d], r[f]), O(i, a[f + 1], s[d]), d++, f--; + else if (l[h] === o[c]) a[c] = w(s[h], r[c]), O(i, s[d], s[h]), h--, c++; + else if (p === void 0 && (p = qt(o, c, f), u = qt(l, d, h)), p.has(l[d])) if (p.has(l[h])) { + const _ = u.get(o[c]), v = _ !== void 0 ? s[_] : null; + if (v === null) { + const N = O(i, s[d]); + w(N, r[c]), a[c] = N; + } else a[c] = w(v, r[c]), O(i, s[d], v), s[_] = null; + c++; + } else K(s[h]), h--; + else K(s[d]), d++; + for (; c <= f; ) { + const _ = O(i, a[f + 1]); + w(_, r[c]), a[c++] = _; + } + for (; d <= h; ) { + const _ = s[d++]; + _ !== null && K(_); + } + return this.ut = o, Ae(i, a), E; + } +}), kt = /* @__PURE__ */ new Map(); +function j(i, t) { + const e = `${i ?? "local"}|${JSON.stringify(t)}`, n = kt.get(e); + if (n) + return n; + let s; + try { + s = new Intl.DateTimeFormat("en-US", { + ...t, + ...i ? { timeZone: i } : {} + }); + } catch { + s = new Intl.DateTimeFormat("en-US", t); + } + return kt.set(e, s), s; +} +function Se(i) { + const t = i?.config; + if (!t || typeof t != "object") + return; + const e = t.time_zone; + return typeof e == "string" && e.trim() ? e.trim() : void 0; +} +function Ee(i, t) { + return `${j(t, { weekday: "long" }).format(i)}, ${j(t, { month: "short", day: "numeric" }).format(i)}`; +} +function Ne(i, t) { + return j(t, { weekday: "long" }).format(i); +} +function Te(i, t) { + const e = new Date(i); + return Number.isNaN(e.getTime()) ? "" : j(t, { + hour: "numeric", + minute: "2-digit", + hour12: !0 + }).format(e); +} +function Me(i) { + if (!i) + return null; + const t = /^(\d{1,2}):(\d{2})$/.exec(i.trim()); + if (!t) + return null; + const e = Number(t[1]); + if (e > 23) + return null; + const n = e >= 12 ? "PM" : "AM"; + return `${e % 12 === 0 ? 12 : e % 12}:${t[2]} ${n}`; +} +function k(i) { + return typeof i == "string" ? i.trim() : ""; +} +function y(i, t) { + if (i == null || i === "") + return t; + const e = typeof i == "number" ? i : Number(i); + return Number.isFinite(e) ? e : t; +} +function $(i) { + return Math.max(0, Math.round(i)); +} +function At(i) { + return i.split(/[-_]+/).filter(Boolean).map((t) => t.charAt(0).toUpperCase() + t.slice(1)).join(" "); +} +function ze(i, t) { + const e = j(t, { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hourCycle: "h23" + }).formatToParts(new Date(i)), n = (r) => { + const o = e.find((l) => l.type === r); + return o ? Number(o.value) : Number.NaN; + }, s = Date.UTC( + n("year"), + n("month") - 1, + n("day"), + n("hour"), + n("minute"), + n("second") + ); + return Number.isFinite(s) ? s - i : 0; +} +function Pe(i, t) { + if (typeof i != "string") + return null; + const e = i.split("-"); + if (e.length !== 3) + return null; + const [n, s, r] = e.map((a) => Number(a)); + if (!n || !s || !r) + return null; + if (!t) { + const a = new Date(n, s - 1, r); + return Number.isNaN(a.getTime()) ? null : a; + } + const o = Date.UTC(n, s - 1, r), l = new Date(o - ze(o, t)); + return Number.isNaN(l.getTime()) ? null : l; +} +function Oe() { + const i = window.location.pathname.split("/").filter(Boolean); + return i.length > 0 ? i[i.length - 1].toLowerCase() : ""; +} +function Ue(i) { + if (!i || typeof i != "object") + return null; + const t = i, e = k(t.state).toLowerCase(); + if (e !== "open" && e !== "completed") + return null; + const n = y(t.id, 0); + return n ? { + id: n, + title: k(t.title) || "Quest", + icon: k(t.icon) || null, + window: k(t.window).toLowerCase(), + due_time: k(t.due_time) || null, + state: e, + overdue: t.overdue === !0, + completed_at: k(t.completed_at) || null + } : null; +} +const Ct = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Ie = "polygon(50% 0, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%)", St = [-9, 6, -4], De = Tt` + * { + box-sizing: border-box; + } + + :host { + display: block; + height: 100%; + --nq-brand-gradient-h: linear-gradient( + 90deg, + var(--nq-brand-blue), + var(--nq-brand-purple) + ); + } + + .board { + position: relative; + display: flex; + flex-direction: column; + height: 1080px; + padding: 0; + overflow: hidden; + background: var(--nq-p-parchment); + box-shadow: var(--nq-p-vignette); + font-family: var(--nq-p-font-body); + } + + .frame { + position: absolute; + pointer-events: none; + } + + .frame-outer { + inset: 26px; + border: var(--nq-p-frame-outer); + border-radius: 14px; + } + + .frame-inner { + inset: 36px; + border: var(--nq-p-frame-inner); + border-radius: 8px; + } + + .header { + position: absolute; + top: 56px; + left: var(--nq-p-page-inset); + right: var(--nq-p-page-inset); + display: flex; + align-items: center; + gap: 28px; + } + + .crest { + position: relative; + flex: none; + width: 96px; + height: 110px; + padding: 3px; + clip-path: ${b(Ct)}; + background: rgba(255, 255, 255, 0.22); + } + + .crest-face { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + padding-bottom: 32px; + clip-path: ${b(Ct)}; + background: var(--nq-p-crest); + } + + .crest .initial { + font-family: var(--nq-p-font-display); + font-size: 44px; + font-weight: 700; + line-height: 1; + color: #ffffff; + } + + .titles { + display: flex; + flex-direction: column; + gap: 8px; + min-width: 0; + } + + .title { + margin: 0; + font-family: var(--nq-p-font-display); + font-size: var(--nq-p-size-title); + font-weight: 700; + line-height: 1.1; + color: var(--nq-p-ink); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .sub { + margin: 0; + font-family: var(--nq-p-font-heading); + font-size: 20px; + font-weight: 600; + letter-spacing: 0.24em; + text-transform: uppercase; + line-height: 1.2; + color: var(--nq-p-ink-secondary); + } + + .remaining { + margin-left: auto; + flex: none; + display: flex; + align-items: center; + gap: 20px; + padding: 18px 28px; + border: 2px solid var(--nq-p-panel-border); + border-radius: 14px; + background: var(--nq-p-card-panel); + box-shadow: var(--nq-p-panel-shadow); + } + + .d20 { + flex: none; + display: inline-flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + clip-path: ${b(Ie)}; + background: var(--nq-brand-gradient); + } + + .d20-numeral { + font-family: var(--nq-p-font-heading); + font-size: 19px; + font-weight: 700; + line-height: 1; + color: #ffffff; + } + + .remaining-text { + display: flex; + flex-direction: column; + gap: 4px; + } + + .remaining-count { + font-family: var(--nq-p-font-heading); + font-size: 28px; + font-weight: 700; + line-height: 1.1; + color: var(--nq-p-ink); + } + + .party-line { + font-family: var(--nq-p-font-body); + font-size: var(--nq-p-size-body); + font-weight: 600; + line-height: 1.2; + color: var(--nq-p-ink-secondary); + } + + .columns { + position: absolute; + top: 236px; + left: var(--nq-p-page-inset); + right: var(--nq-p-page-inset); + bottom: 130px; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 34px; + } + + .column { + display: flex; + flex-direction: column; + min-height: 0; + } + + .column-head { + flex: none; + display: flex; + align-items: center; + gap: 14px; + padding-bottom: 14px; + border-bottom: var(--nq-p-rule); + } + + .column-head svg { + flex: none; + width: 34px; + height: 34px; + color: var(--nq-p-ink-secondary); + } + + .column-names { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; + } + + .column-name { + font-family: var(--nq-p-font-heading); + font-size: var(--nq-p-size-section); + font-weight: 700; + line-height: 1.1; + color: var(--nq-p-ink); + } + + .column-range { + font-family: var(--nq-p-font-heading); + font-size: var(--nq-p-size-label); + font-weight: 600; + letter-spacing: var(--nq-p-track-label); + text-transform: uppercase; + line-height: 1.2; + color: var(--nq-p-ink-secondary); + } + + .column-count { + margin-left: auto; + flex: none; + font-family: var(--nq-p-font-heading); + font-size: 26px; + font-weight: 700; + line-height: 1.1; + color: var(--nq-p-ink-secondary); + } + + .stack { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + overflow: visible; + gap: var(--nq-p-quest-gap); + margin-top: 22px; + } + + .quest { + position: relative; + display: flex; + align-items: center; + gap: 22px; + min-height: var(--nq-p-quest-min-height); + padding: var(--nq-p-quest-pad); + border: 1px solid var(--nq-p-card-border); + border-radius: var(--nq-p-radius-card); + background: var(--nq-p-card-open); + box-shadow: var(--nq-p-card-shadow); + -webkit-tap-highlight-color: transparent; + transition: transform var(--nq-dur-micro) var(--nq-ease-out); + } + + .quest.tappable { + cursor: pointer; + } + + .quest.tappable:active { + transform: scale(0.98); + } + + .quest.sealed { + background: var(--nq-p-card-done); + opacity: 0.82; + box-shadow: none; + } + + .tile { + flex: none; + display: inline-flex; + align-items: center; + justify-content: center; + width: var(--nq-p-tile-size); + height: var(--nq-p-tile-size); + border-radius: 10px; + background: var(--nq-p-icon-tile); + color: #ffffff; + } + + .tile svg { + width: 34px; + height: 34px; + } + + .sealed .tile { + background: var(--nq-p-icon-tile-done); + color: var(--nq-p-ink-secondary); + } + + .quest .body { + flex: 1 1 0; + min-width: 0; + display: flex; + flex-direction: column; + gap: 6px; + } + + .quest .quest-title { + font-family: var(--nq-p-font-heading); + font-size: var(--nq-p-size-quest); + font-weight: 700; + line-height: 1.15; + color: var(--nq-p-ink); + overflow-wrap: anywhere; + } + + .sealed .quest-title { + color: var(--nq-p-ink-muted); + text-decoration: line-through; + } + + .quest .meta { + font-family: var(--nq-p-font-body); + font-size: var(--nq-p-size-body); + font-weight: 600; + line-height: 1.2; + color: var(--nq-p-ink-secondary); + } + + .quest .meta.late { + font-family: var(--nq-p-font-heading); + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--nq-p-ink-late); + } + + button.complete { + flex: none; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 10px; + height: var(--nq-p-button-height); + min-width: var(--nq-p-button-min-width); + border: none; + border-radius: 10px; + background: var(--nq-brand-gradient); + color: #ffffff; + font-family: var(--nq-p-font-heading); + font-size: 24px; + font-weight: 700; + line-height: 1; + box-shadow: 0 3px 0 rgba(40, 20, 60, 0.35); + cursor: pointer; + -webkit-tap-highlight-color: transparent; + transition: transform var(--nq-dur-micro) var(--nq-ease-out); + } + + button.complete:active { + transform: scale(0.98); + } + + button.complete svg { + width: 30px; + height: 30px; + } + + button:focus-visible { + outline: 3px solid var(--nq-brand-purple); + outline-offset: 4px; + } + + .seal { + position: absolute; + right: 16px; + top: -12px; + display: inline-flex; + align-items: center; + justify-content: center; + width: var(--nq-p-seal-size); + height: var(--nq-p-seal-size); + border-radius: var(--nq-p-radius-pill); + background: var(--nq-p-seal); + box-shadow: var(--nq-p-seal-shadow); + color: rgba(255, 235, 235, 0.95); + transform: rotate(var(--seal-rot, 0deg)); + animation: seal-in var(--nq-dur-base) var(--nq-ease-out); + pointer-events: none; + } + + .seal svg { + width: 38px; + height: 38px; + } + + @keyframes seal-in { + from { + opacity: 0; + transform: scale(1.15) rotate(var(--seal-rot, 0deg)); + } + to { + opacity: 1; + transform: scale(1) rotate(var(--seal-rot, 0deg)); + } + } + + .rollup { + border: 2px dashed var(--nq-p-panel-border); + border-radius: 14px; + padding: 16px 22px; + } + + .rollup-label { + display: block; + font-family: var(--nq-p-font-heading); + font-size: 18px; + font-weight: 600; + letter-spacing: var(--nq-p-track-label); + text-transform: uppercase; + line-height: 1.2; + color: var(--nq-p-ink-secondary); + } + + .rollup-body { + margin: 6px 0 0; + font-family: var(--nq-p-font-body); + font-size: 23px; + font-weight: 600; + line-height: 1.35; + color: var(--nq-p-ink-secondary); + } + + .scrim { + position: absolute; + inset: 0; + z-index: 10; + background: rgba(30, 20, 10, 0.62); + } + + .dialog { + position: absolute; + top: 196px; + left: 460px; + width: 1000px; + border: 3px solid rgba(92, 62, 26, 0.5); + border-radius: 20px; + background: var(--nq-p-card-panel); + box-shadow: 0 26px 60px rgba(20, 10, 0, 0.5); + padding: 64px 68px 56px; + animation: dialog-in var(--nq-dur-modal) var(--nq-ease-out); + } + + @keyframes dialog-in { + from { + opacity: 0; + transform: scale(0.96); + } + to { + opacity: 1; + transform: scale(1); + } + } + + .dialog-header { + display: flex; + align-items: center; + gap: 24px; + } + + .dialog-tile { + flex: none; + display: inline-flex; + align-items: center; + justify-content: center; + width: 104px; + height: 104px; + border-radius: 12px; + background: var(--nq-p-icon-tile); + color: #ffffff; + } + + .dialog-tile svg { + width: 56px; + height: 56px; + } + + .dialog-titles { + display: flex; + flex-direction: column; + gap: 8px; + min-width: 0; + } + + .dialog-kicker { + font-family: var(--nq-p-font-heading); + font-size: var(--nq-p-size-label); + font-weight: 600; + letter-spacing: 0.24em; + text-transform: uppercase; + line-height: 1.2; + color: var(--nq-p-ink-secondary); + } + + .dialog-quest-title { + font-family: var(--nq-p-font-display); + font-size: 58px; + font-weight: 700; + line-height: 1.1; + color: var(--nq-p-ink); + overflow-wrap: anywhere; + } + + .dialog-body { + margin: 28px 0 0; + font-family: var(--nq-p-font-body); + font-size: 32px; + font-weight: 600; + line-height: 1.35; + color: #3f2f1c; + } + + .dialog-buttons { + display: flex; + gap: 24px; + margin-top: 40px; + } + + button.confirm-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 12px; + height: var(--nq-p-confirm-button); + border-radius: 10px; + font-family: var(--nq-p-font-heading); + font-weight: 700; + line-height: 1; + cursor: pointer; + -webkit-tap-highlight-color: transparent; + transition: transform var(--nq-dur-micro) var(--nq-ease-out); + } + + button.confirm-button:active { + transform: scale(0.98); + } + + button.not-yet { + flex: 1; + border: 2px solid rgba(92, 62, 26, 0.5); + background: rgba(92, 62, 26, 0.1); + color: var(--nq-p-ink); + font-size: 30px; + } + + button.not-yet svg { + width: 34px; + height: 34px; + } + + button.do-complete { + flex: 2; + border: none; + background: var(--nq-brand-gradient); + color: #ffffff; + font-size: 32px; + } + + button.do-complete svg { + width: 38px; + height: 38px; + } +`, J = g``, He = g``, G = g``, je = g``, Le = g``, Re = g``, Et = [ + { key: "morning", name: "Morning", range: "Until 11:59 AM", icon: je }, + { key: "afternoon", name: "Afternoon", range: "12:00–5:00 PM", icon: Le }, + { key: "evening", name: "Evening", range: "5:00–9:00 PM", icon: Re } +]; +class Be extends C { static properties = { hass: { attribute: !1 }, - _config: { state: !0 } + _config: { state: !0 }, + _now: { state: !0 }, + _confirm: { state: !0 } }; - static styles = [$(yt)]; + static styles = [b(It), De]; hass; _config; + _now = /* @__PURE__ */ new Date(); + _confirm = null; + _clockTimer; + _idleTimer; + _confirmTimer; setConfig(t) { if (!t || typeof t != "object") throw new Error("Invalid configuration"); this._config = t; } getCardSize() { - return 8; + return 22; + } + connectedCallback() { + super.connectedCallback(), this._now = /* @__PURE__ */ new Date(), this._clockTimer = window.setInterval(() => { + this._now = /* @__PURE__ */ new Date(); + }, 6e4), window.addEventListener("pointerdown", this._onActivity, !0), window.addEventListener("touchstart", this._onActivity, !0), window.addEventListener("keydown", this._onActivity, !0), this._armIdle(); + } + disconnectedCallback() { + this._stopClock(), this._clearIdle(), this._clearConfirmTimer(), window.removeEventListener("pointerdown", this._onActivity, !0), window.removeEventListener("touchstart", this._onActivity, !0), window.removeEventListener("keydown", this._onActivity, !0), super.disconnectedCallback(); } render() { - return g`
NestQuest Quest Log
`; + return g` +
+
+
+ ${this._renderHeader()} +
+ ${Et.map((t) => this._renderColumn(t))} +
+ ${this._renderConfirm()} +
+ `; } + _timeZone() { + return Se(this.hass); + } + _state(t) { + const n = this.hass?.states?.[t]; + return n && typeof n == "object" ? n : null; + } + _childSlug() { + return Oe(); + } + _boardPath() { + return k(this._config?.board_path).replace(/\/+$/, ""); + } + _idleSeconds() { + const t = y(this._config?.idle_return_seconds, 40); + return t > 0 ? t : 40; + } + _confirmSeconds() { + const t = y(this._config?.confirm_timeout_seconds, 15); + return t > 0 ? t : 15; + } + _childName(t) { + if (!t) + return null; + const e = this._state(`sensor.nestquest_${t}_quests_due_today`)?.attributes?.child_name; + return typeof e == "string" && e.trim() ? e.trim() : null; + } + _childCounts(t) { + const e = this._state(`sensor.nestquest_${t}_quests_due_today`), n = this._state( + `sensor.nestquest_${t}_quests_completed_today` + ), s = $(y(e?.state, 0)), r = $(y(n?.state, 0)); + return { due: s, completed: r }; + } + _instances() { + const t = this._childSlug(); + if (!t) + return []; + const e = this._state(`sensor.nestquest_${t}_quests_due_today`)?.attributes?.instances; + return Array.isArray(e) ? e.map(Ue).filter((n) => n !== null) : []; + } + _remaining() { + const t = this._childSlug(); + if (!t) + return 0; + const e = this._state( + `sensor.nestquest_${t}_quests_remaining_today` + ); + if (e) + return $(y(e.state, 0)); + const { due: n, completed: s } = this._childCounts(t); + return $(n - s); + } + _partyCounts() { + const t = this._state( + "sensor.nestquest_household_quests_due_today" + ), e = this._state( + "sensor.nestquest_household_quests_completed_today" + ); + if (t || e) + return { + completed: $(y(e?.state, 0)), + due: $(y(t?.state, 0)) + }; + const n = this._childSlug(), { due: s, completed: r } = this._childCounts(n); + return { completed: r, due: s }; + } + _otherChildren() { + const t = this.hass?.states; + if (!t) + return []; + const e = this._childSlug(), n = this._timeZone(), s = []; + for (const r of Object.keys(t)) { + const o = /^sensor\.nestquest_(.+)_quests_due_today$/.exec(r); + if (!o || o[1] === "household" || o[1] === e) + continue; + const l = o[1], a = t[r]?.attributes ?? {}; + let p = ""; + typeof a.child_name == "string" && a.child_name.trim() ? p = a.child_name.trim() : p = At(l); + let u = !0; + if (typeof a.present == "boolean") + u = a.present; + else { + const _ = this._state( + `binary_sensor.nestquest_${l}_present_today` + ); + u = _ === null || String(_.state ?? "").trim().toLowerCase() !== "off"; + } + const d = this._state( + `sensor.nestquest_${l}_quests_completed_today` + ), h = $(y(t[r]?.state, 0)), c = $(y(d?.state, 0)); + let f = null; + if (!u) { + const _ = this._state( + `binary_sensor.nestquest_${l}_present_today` + ), v = Pe( + _?.attributes?.next_present, + n + ); + f = v ? Ne(v, n) : null; + } + s.push({ + slug: l, + name: p, + present: u, + due: h, + completed: c, + returnsWeekday: f + }); + } + return s.sort((r, o) => r.name.localeCompare(o.name)); + } + _renderHeader() { + const t = this._childSlug(), n = this._childName(t) ?? (t ? At(t) : null), { due: s, completed: r } = this._childCounts(t), o = this._remaining(), l = this._partyCounts(), a = n ? `${n}'s Quest Log` : "Quest Log", p = (n?.charAt(0) || "?").toUpperCase(); + return g` +
+ +
+

${a}

+

+ ${Ee(this._now, this._timeZone())} · ${r} of ${s} + claimed +

+
+
+ +
+ + ${o} quest${o === 1 ? "" : "s"} left + + + Party progress · ${l.completed} of ${l.due} today + +
+
+
+ `; + } + _renderColumn(t) { + const e = this._instances().filter( + (l) => l.window === t.key + ), n = e.filter((l) => l.state === "open").sort( + (l, a) => (l.due_time ?? "99:99").localeCompare(a.due_time ?? "99:99") + ), s = e.filter( + (l) => l.state === "completed" + ), r = e.length; + let o = m; + if (r > 0) { + const a = 644 - (t.key === "afternoon" && this._otherChildren().length > 0 ? 160 : 0), p = Math.min( + 24, + Math.max(8, Math.floor((a - r * 74) / Math.max(r - 1, 1))) + ), u = Math.min( + 116, + Math.max(58, Math.floor((a - (r - 1) * p) / r)) + ), d = Math.max(56, Math.min(72, u - 2)); + let h = `--nq-p-quest-gap: ${p}px; --nq-p-quest-min-height: ${u}px; --nq-p-button-height: ${d}px`; + if (r >= 5) { + const c = Math.max(0, Math.min(22, Math.floor((u - d - 2) / 2))), f = Math.max(44, Math.min(64, u - 2 * c - 2)); + h += `; --nq-p-quest-pad: ${c}px; --nq-p-tile-size: ${f}px`; + } + o = h; + } + return g` +
+
+ ${t.icon} +
+ ${t.name} + ${t.range} +
+ ${s.length}/${e.length} +
+
+ ${wt( + n, + (l) => l.id, + (l) => this._renderQuest(l, !1, 0) + )} + ${wt( + s, + (l) => l.id, + (l, a) => this._renderQuest(l, !0, a) + )} + ${t.key === "afternoon" ? this._renderRollup() : m} +
+
+ `; + } + _renderQuest(t, e, n) { + if (e) { + const o = St[n % St.length], l = t.completed_at ? Te(t.completed_at, this._timeZone()) : ""; + return g` +
+ +
+ ${t.title} + + ${l ? `Claimed ${l}` : "Claimed"} + +
+ +
+ `; + } + const s = Me(t.due_time), r = t.overdue ? { + text: s ? `Overdue · due ${s}` : "Overdue", + late: !0 + } : { + text: s ? `Due by ${s}` : "Due today", + late: !1 + }; + return g` +
this._openConfirm(t.id)} + @keydown=${(o) => { + (o.key === "Enter" || o.key === " ") && (o.preventDefault(), this._openConfirm(t.id)); + }} + > + +
+ ${t.title} + ${r.text} +
+ +
+ `; + } + _renderRollup() { + const t = this._otherChildren(); + if (t.length === 0) + return m; + const e = t.slice(0, 2).map((n) => n.present ? n.due > 0 && n.completed >= n.due ? `${n.name} has finished their log.` : n.due > n.completed ? `${n.name} has ${n.due - n.completed} quests left.` : `${n.name} has no quests today.` : n.returnsWeekday ? `${n.name} is on travels until ${n.returnsWeekday}.` : `${n.name} is on travels.`); + return g` +
+ Party roll-up +

${e.join(" ")}

+
+ `; + } + _renderConfirm() { + if (this._confirm === null) + return m; + const t = this._instances().find( + (n) => n.id === this._confirm + ); + if (!t) + return m; + const e = Et.find((n) => n.key === t.window); + return g` +
this._closeConfirm()}> + +
+ `; + } + _openConfirm(t) { + this._confirm = t, this._armConfirmTimer(); + } + _closeConfirm() { + this._clearConfirmTimer(), this._confirm = null; + } + _confirmComplete() { + this._closeConfirm(); + } + _armConfirmTimer() { + this._clearConfirmTimer(), this._confirmTimer = window.setTimeout(() => { + this._confirmTimer = void 0, this._closeConfirm(); + }, this._confirmSeconds() * 1e3); + } + _clearConfirmTimer() { + this._confirmTimer !== void 0 && (window.clearTimeout(this._confirmTimer), this._confirmTimer = void 0); + } + _armIdle() { + this._clearIdle(), this._idleTimer = window.setTimeout(() => { + this._idleTimer = void 0, this._returnToBoard(); + }, this._idleSeconds() * 1e3); + } + _clearIdle() { + this._idleTimer !== void 0 && (window.clearTimeout(this._idleTimer), this._idleTimer = void 0); + } + _returnToBoard() { + const t = this._boardPath(); + t && (window.history.pushState(null, "", t), window.dispatchEvent(new Event("location-changed"))); + } + _stopClock() { + this._clockTimer !== void 0 && (window.clearInterval(this._clockTimer), this._clockTimer = void 0); + } + _onActivity = () => { + this._armIdle(), this._confirm !== null && this._armConfirmTimer(); + }; } -customElements.define("nestquest-quest-log-card", ne); -V({ +customElements.define("nestquest-quest-log-card", Be); +st({ type: "nestquest-quest-log-card", name: "NestQuest Quest Log", description: "One child's daily quests, grouped by window." }); -const se = ":host{--nq-brand-blue: #3B3AB8;--nq-brand-purple: #A035CC;--nq-brand-gradient: linear-gradient(135deg, var(--nq-brand-blue), var(--nq-brand-purple));--nq-brand-gradient-h: linear-gradient(90deg, var(--nq-brand-blue), var(--nq-brand-purple));--nq-a-page: #F4F4F6;--nq-a-surface: #FFFFFF;--nq-a-surface-subtle: #FAFAFA;--nq-a-selected: #EBEBF8;--nq-a-border: #E4E4EA;--nq-a-divider: #EDEDF1;--nq-a-ink: #18181D;--nq-a-ink-secondary: #52525E;--nq-a-ink-tertiary: #70707E;--nq-a-success: #15803D;--nq-a-danger: #DC2626;--nq-a-danger-strong: #B91C1C;--nq-a-danger-bg: #FEE2E2;--nq-a-warning-ink: #92400E;--nq-a-warning-ink-2: #78350F;--nq-a-warning-bg: #FEF3C7;--nq-a-warning-border: rgba(217,119,6,.35);--nq-a-info-bg: #DBEAFE;--nq-a-info-ink: #1E3A8A;--nq-a-info-icon: #1D4ED8;--nq-a-reversal: #7D28A0;--nq-a-font: Nunito, system-ui, sans-serif;--nq-a-size-screen: 24px;--nq-a-size-hero: 32px;--nq-a-size-card-title: 19px;--nq-a-size-stat: 22px;--nq-a-size-row: 13.5px;--nq-a-size-meta: 11px;--nq-a-size-label: 10px;--nq-a-track-label: .1em;--nq-a-density-page-pad: 14px 16px 92px;--nq-a-density-gap: 10px;--nq-a-density-card-pad: 13px;--nq-a-density-row-pad: 11px 13px;--nq-a-density-radius: 8px;--nq-a-density-shadow: none;--nq-a-tabbar-height: 70px;--nq-a-tap-min: 44px;--nq-a-radius-pill: 9999px;--nq-a-radius-sheet: 20px 20px 0 0;--nq-a-sheet-shadow: 0 -8px 28px rgba(9,9,11,.18);--nq-a-progress-height: 6px;--nq-ease-out: cubic-bezier(0, 0, .2, 1);--nq-dur-micro: .12s;--nq-dur-base: .2s;--nq-dur-modal: .35s}"; -class ie extends v { +const Fe = ":host{--nq-brand-blue: #3B3AB8;--nq-brand-purple: #A035CC;--nq-brand-gradient: linear-gradient(135deg, var(--nq-brand-blue), var(--nq-brand-purple));--nq-brand-gradient-h: linear-gradient(90deg, var(--nq-brand-blue), var(--nq-brand-purple));--nq-a-page: #F4F4F6;--nq-a-surface: #FFFFFF;--nq-a-surface-subtle: #FAFAFA;--nq-a-selected: #EBEBF8;--nq-a-border: #E4E4EA;--nq-a-divider: #EDEDF1;--nq-a-ink: #18181D;--nq-a-ink-secondary: #52525E;--nq-a-ink-tertiary: #70707E;--nq-a-success: #15803D;--nq-a-danger: #DC2626;--nq-a-danger-strong: #B91C1C;--nq-a-danger-bg: #FEE2E2;--nq-a-warning-ink: #92400E;--nq-a-warning-ink-2: #78350F;--nq-a-warning-bg: #FEF3C7;--nq-a-warning-border: rgba(217,119,6,.35);--nq-a-info-bg: #DBEAFE;--nq-a-info-ink: #1E3A8A;--nq-a-info-icon: #1D4ED8;--nq-a-reversal: #7D28A0;--nq-a-font: Nunito, system-ui, sans-serif;--nq-a-size-screen: 24px;--nq-a-size-hero: 32px;--nq-a-size-card-title: 19px;--nq-a-size-stat: 22px;--nq-a-size-row: 13.5px;--nq-a-size-meta: 11px;--nq-a-size-label: 10px;--nq-a-track-label: .1em;--nq-a-density-page-pad: 14px 16px 92px;--nq-a-density-gap: 10px;--nq-a-density-card-pad: 13px;--nq-a-density-row-pad: 11px 13px;--nq-a-density-radius: 8px;--nq-a-density-shadow: none;--nq-a-tabbar-height: 70px;--nq-a-tap-min: 44px;--nq-a-radius-pill: 9999px;--nq-a-radius-sheet: 20px 20px 0 0;--nq-a-sheet-shadow: 0 -8px 28px rgba(9,9,11,.18);--nq-a-progress-height: 6px;--nq-ease-out: cubic-bezier(0, 0, .2, 1);--nq-dur-micro: .12s;--nq-dur-base: .2s;--nq-dur-modal: .35s}"; +class Qe extends C { static properties = { hass: { attribute: !1 }, _config: { state: !0 } }; - static styles = [$(se)]; + static styles = [b(Fe)]; hass; _config; setConfig(t) { @@ -1272,14 +2515,14 @@ class ie extends v { return g`
NestQuest Admin
`; } } -customElements.define("nestquest-admin-card", ie); -V({ +customElements.define("nestquest-admin-card", Qe); +st({ type: "nestquest-admin-card", name: "NestQuest Admin", description: "Parent phone view for today, tasks, schedule, and history." }); -const re = "/nestquest-static/nestquest-fonts.css"; +const We = "/nestquest-static/nestquest-fonts.css"; if (!document.querySelector('link[data-nq-fonts=""]')) { - const s = document.createElement("link"); - s.rel = "stylesheet", s.href = re, s.dataset.nqFonts = "", document.head.appendChild(s); + const i = document.createElement("link"); + i.rel = "stylesheet", i.href = We, i.dataset.nqFonts = "", document.head.appendChild(i); } diff --git a/custom_components/nestquest/www/nestquest-panel-tokens.css b/custom_components/nestquest/www/nestquest-panel-tokens.css index f997486..9f51ab6 100644 --- a/custom_components/nestquest/www/nestquest-panel-tokens.css +++ b/custom_components/nestquest/www/nestquest-panel-tokens.css @@ -77,6 +77,8 @@ /* ── Metrics: touch ───────────────────────────────────── */ --nq-p-quest-min-height: 116px; /* whole card is the tap target */ --nq-p-quest-gap: 24px; /* separation between tap targets */ + --nq-p-quest-pad: 22px; /* padding inside a quest card */ + --nq-p-tile-size: 64px; /* quest icon tile */ --nq-p-button-height: 72px; /* "Complete" button on a quest card */ --nq-p-button-min-width: 150px; --nq-p-confirm-button: 108px; /* confirm dialog buttons */ diff --git a/design/tokens/nestquest-panel-tokens.css b/design/tokens/nestquest-panel-tokens.css index f997486..9f51ab6 100644 --- a/design/tokens/nestquest-panel-tokens.css +++ b/design/tokens/nestquest-panel-tokens.css @@ -77,6 +77,8 @@ /* ── Metrics: touch ───────────────────────────────────── */ --nq-p-quest-min-height: 116px; /* whole card is the tap target */ --nq-p-quest-gap: 24px; /* separation between tap targets */ + --nq-p-quest-pad: 22px; /* padding inside a quest card */ + --nq-p-tile-size: 64px; /* quest icon tile */ --nq-p-button-height: 72px; /* "Complete" button on a quest card */ --nq-p-button-min-width: 150px; --nq-p-confirm-button: 108px; /* confirm dialog buttons */ diff --git a/frontend/src/cards/quest-log.ts b/frontend/src/cards/quest-log.ts index b51929a..ed27510 100644 --- a/frontend/src/cards/quest-log.ts +++ b/frontend/src/cards/quest-log.ts @@ -1,18 +1,905 @@ -import { LitElement, html, unsafeCSS } from "lit"; +import { LitElement, css, html, nothing, unsafeCSS, type TemplateResult } from "lit"; +import { repeat } from "lit/directives/repeat.js"; import panelTokens from "../../../custom_components/nestquest/www/nestquest-panel-tokens.css?inline"; import { registerCustomCard, type CardConfig, type HassLike } from "../types"; +type StateObject = { + state: unknown; + attributes?: Record | null; +}; + +type PanelInstance = { + id: number; + title: string; + icon: string | null; + window: string; + due_time: string | null; + state: string; + overdue: boolean; + completed_at: string | null; +}; + +type WindowDef = { + key: string; + name: string; + range: string; + icon: TemplateResult; +}; + +type OtherChild = { + slug: string; + name: string; + present: boolean; + due: number; + completed: number; + returnsWeekday: string | null; +}; + +const FORMATTER_CACHE = new Map(); + +function zonedFormatter( + timeZone: string | undefined, + options: Intl.DateTimeFormatOptions +): Intl.DateTimeFormat { + const key = `${timeZone ?? "local"}|${JSON.stringify(options)}`; + const cached = FORMATTER_CACHE.get(key); + if (cached) { + return cached; + } + let formatter: Intl.DateTimeFormat; + try { + formatter = new Intl.DateTimeFormat("en-US", { + ...options, + ...(timeZone ? { timeZone } : {}), + }); + } catch { + formatter = new Intl.DateTimeFormat("en-US", options); + } + FORMATTER_CACHE.set(key, formatter); + return formatter; +} + +function hassTimeZone(hass: HassLike | undefined): string | undefined { + const config = hass?.config; + if (!config || typeof config !== "object") { + return undefined; + } + const timeZone = (config as Record).time_zone; + return typeof timeZone === "string" && timeZone.trim() + ? timeZone.trim() + : undefined; +} + +function formatDay(date: Date, timeZone: string | undefined): string { + return `${zonedFormatter(timeZone, { weekday: "long" }).format(date)}, ${zonedFormatter(timeZone, { month: "short", day: "numeric" }).format(date)}`; +} + +function formatWeekday(date: Date, timeZone: string | undefined): string { + return zonedFormatter(timeZone, { weekday: "long" }).format(date); +} + +function formatInstant(value: string, timeZone: string | undefined): string { + const parsed = new Date(value); + if (Number.isNaN(parsed.getTime())) { + return ""; + } + return zonedFormatter(timeZone, { + hour: "numeric", + minute: "2-digit", + hour12: true, + }).format(parsed); +} + +function formatWallClock(value: string | null): string | null { + if (!value) { + return null; + } + const match = /^(\d{1,2}):(\d{2})$/.exec(value.trim()); + if (!match) { + return null; + } + const hours = Number(match[1]); + if (hours > 23) { + return null; + } + const suffix = hours >= 12 ? "PM" : "AM"; + const hour12 = hours % 12 === 0 ? 12 : hours % 12; + return `${hour12}:${match[2]} ${suffix}`; +} + +function asString(value: unknown): string { + return typeof value === "string" ? value.trim() : ""; +} + +function asNumber(value: unknown, fallback: number): number { + if (value === null || value === undefined || value === "") { + return fallback; + } + const parsed = typeof value === "number" ? value : Number(value); + return Number.isFinite(parsed) ? parsed : fallback; +} + +function clampCount(value: number): number { + return Math.max(0, Math.round(value)); +} + +function titleCaseSlug(slug: string): string { + return slug + .split(/[-_]+/) + .filter(Boolean) + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); +} + +function zoneOffsetMs(instant: number, timeZone: string): number { + const parts = zonedFormatter(timeZone, { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hourCycle: "h23", + }).formatToParts(new Date(instant)); + const read = (type: Intl.DateTimeFormatPartTypes): number => { + const part = parts.find((entry) => entry.type === type); + return part ? Number(part.value) : Number.NaN; + }; + const asUtc = Date.UTC( + read("year"), + read("month") - 1, + read("day"), + read("hour"), + read("minute"), + read("second") + ); + return Number.isFinite(asUtc) ? asUtc - instant : 0; +} + +function parseIsoDate(value: unknown, timeZone?: string): Date | null { + if (typeof value !== "string") { + return null; + } + const parts = value.split("-"); + if (parts.length !== 3) { + return null; + } + const [year, month, day] = parts.map((part) => Number(part)); + if (!year || !month || !day) { + return null; + } + if (!timeZone) { + const local = new Date(year, month - 1, day); + return Number.isNaN(local.getTime()) ? null : local; + } + const utcMs = Date.UTC(year, month - 1, day); + const date = new Date(utcMs - zoneOffsetMs(utcMs, timeZone)); + return Number.isNaN(date.getTime()) ? null : date; +} + +function childSlugFromPath(): string { + const segments = window.location.pathname.split("/").filter(Boolean); + return segments.length > 0 + ? segments[segments.length - 1].toLowerCase() + : ""; +} + +function toPanelInstance(value: unknown): PanelInstance | null { + if (!value || typeof value !== "object") { + return null; + } + const row = value as Record; + const state = asString(row.state).toLowerCase(); + if (state !== "open" && state !== "completed") { + return null; + } + const id = asNumber(row.id, 0); + if (!id) { + return null; + } + return { + id, + title: asString(row.title) || "Quest", + icon: asString(row.icon) || null, + window: asString(row.window).toLowerCase(), + due_time: asString(row.due_time) || null, + state, + overdue: row.overdue === true, + completed_at: asString(row.completed_at) || null, + }; +} + +const SHIELD_CLIP = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)"; +const HEXAGON_CLIP = + "polygon(50% 0, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%)"; + +const SEAL_ROTATIONS = [-9, 6, -4]; + +const logStyles = css` + * { + box-sizing: border-box; + } + + :host { + display: block; + height: 100%; + --nq-brand-gradient-h: linear-gradient( + 90deg, + var(--nq-brand-blue), + var(--nq-brand-purple) + ); + } + + .board { + position: relative; + display: flex; + flex-direction: column; + height: 1080px; + padding: 0; + overflow: hidden; + background: var(--nq-p-parchment); + box-shadow: var(--nq-p-vignette); + font-family: var(--nq-p-font-body); + } + + .frame { + position: absolute; + pointer-events: none; + } + + .frame-outer { + inset: 26px; + border: var(--nq-p-frame-outer); + border-radius: 14px; + } + + .frame-inner { + inset: 36px; + border: var(--nq-p-frame-inner); + border-radius: 8px; + } + + .header { + position: absolute; + top: 56px; + left: var(--nq-p-page-inset); + right: var(--nq-p-page-inset); + display: flex; + align-items: center; + gap: 28px; + } + + .crest { + position: relative; + flex: none; + width: 96px; + height: 110px; + padding: 3px; + clip-path: ${unsafeCSS(SHIELD_CLIP)}; + background: rgba(255, 255, 255, 0.22); + } + + .crest-face { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + padding-bottom: 32px; + clip-path: ${unsafeCSS(SHIELD_CLIP)}; + background: var(--nq-p-crest); + } + + .crest .initial { + font-family: var(--nq-p-font-display); + font-size: 44px; + font-weight: 700; + line-height: 1; + color: #ffffff; + } + + .titles { + display: flex; + flex-direction: column; + gap: 8px; + min-width: 0; + } + + .title { + margin: 0; + font-family: var(--nq-p-font-display); + font-size: var(--nq-p-size-title); + font-weight: 700; + line-height: 1.1; + color: var(--nq-p-ink); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .sub { + margin: 0; + font-family: var(--nq-p-font-heading); + font-size: 20px; + font-weight: 600; + letter-spacing: 0.24em; + text-transform: uppercase; + line-height: 1.2; + color: var(--nq-p-ink-secondary); + } + + .remaining { + margin-left: auto; + flex: none; + display: flex; + align-items: center; + gap: 20px; + padding: 18px 28px; + border: 2px solid var(--nq-p-panel-border); + border-radius: 14px; + background: var(--nq-p-card-panel); + box-shadow: var(--nq-p-panel-shadow); + } + + .d20 { + flex: none; + display: inline-flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + clip-path: ${unsafeCSS(HEXAGON_CLIP)}; + background: var(--nq-brand-gradient); + } + + .d20-numeral { + font-family: var(--nq-p-font-heading); + font-size: 19px; + font-weight: 700; + line-height: 1; + color: #ffffff; + } + + .remaining-text { + display: flex; + flex-direction: column; + gap: 4px; + } + + .remaining-count { + font-family: var(--nq-p-font-heading); + font-size: 28px; + font-weight: 700; + line-height: 1.1; + color: var(--nq-p-ink); + } + + .party-line { + font-family: var(--nq-p-font-body); + font-size: var(--nq-p-size-body); + font-weight: 600; + line-height: 1.2; + color: var(--nq-p-ink-secondary); + } + + .columns { + position: absolute; + top: 236px; + left: var(--nq-p-page-inset); + right: var(--nq-p-page-inset); + bottom: 130px; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 34px; + } + + .column { + display: flex; + flex-direction: column; + min-height: 0; + } + + .column-head { + flex: none; + display: flex; + align-items: center; + gap: 14px; + padding-bottom: 14px; + border-bottom: var(--nq-p-rule); + } + + .column-head svg { + flex: none; + width: 34px; + height: 34px; + color: var(--nq-p-ink-secondary); + } + + .column-names { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; + } + + .column-name { + font-family: var(--nq-p-font-heading); + font-size: var(--nq-p-size-section); + font-weight: 700; + line-height: 1.1; + color: var(--nq-p-ink); + } + + .column-range { + font-family: var(--nq-p-font-heading); + font-size: var(--nq-p-size-label); + font-weight: 600; + letter-spacing: var(--nq-p-track-label); + text-transform: uppercase; + line-height: 1.2; + color: var(--nq-p-ink-secondary); + } + + .column-count { + margin-left: auto; + flex: none; + font-family: var(--nq-p-font-heading); + font-size: 26px; + font-weight: 700; + line-height: 1.1; + color: var(--nq-p-ink-secondary); + } + + .stack { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + overflow: visible; + gap: var(--nq-p-quest-gap); + margin-top: 22px; + } + + .quest { + position: relative; + display: flex; + align-items: center; + gap: 22px; + min-height: var(--nq-p-quest-min-height); + padding: var(--nq-p-quest-pad); + border: 1px solid var(--nq-p-card-border); + border-radius: var(--nq-p-radius-card); + background: var(--nq-p-card-open); + box-shadow: var(--nq-p-card-shadow); + -webkit-tap-highlight-color: transparent; + transition: transform var(--nq-dur-micro) var(--nq-ease-out); + } + + .quest.tappable { + cursor: pointer; + } + + .quest.tappable:active { + transform: scale(0.98); + } + + .quest.sealed { + background: var(--nq-p-card-done); + opacity: 0.82; + box-shadow: none; + } + + .tile { + flex: none; + display: inline-flex; + align-items: center; + justify-content: center; + width: var(--nq-p-tile-size); + height: var(--nq-p-tile-size); + border-radius: 10px; + background: var(--nq-p-icon-tile); + color: #ffffff; + } + + .tile svg { + width: 34px; + height: 34px; + } + + .sealed .tile { + background: var(--nq-p-icon-tile-done); + color: var(--nq-p-ink-secondary); + } + + .quest .body { + flex: 1 1 0; + min-width: 0; + display: flex; + flex-direction: column; + gap: 6px; + } + + .quest .quest-title { + font-family: var(--nq-p-font-heading); + font-size: var(--nq-p-size-quest); + font-weight: 700; + line-height: 1.15; + color: var(--nq-p-ink); + overflow-wrap: anywhere; + } + + .sealed .quest-title { + color: var(--nq-p-ink-muted); + text-decoration: line-through; + } + + .quest .meta { + font-family: var(--nq-p-font-body); + font-size: var(--nq-p-size-body); + font-weight: 600; + line-height: 1.2; + color: var(--nq-p-ink-secondary); + } + + .quest .meta.late { + font-family: var(--nq-p-font-heading); + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--nq-p-ink-late); + } + + button.complete { + flex: none; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 10px; + height: var(--nq-p-button-height); + min-width: var(--nq-p-button-min-width); + border: none; + border-radius: 10px; + background: var(--nq-brand-gradient); + color: #ffffff; + font-family: var(--nq-p-font-heading); + font-size: 24px; + font-weight: 700; + line-height: 1; + box-shadow: 0 3px 0 rgba(40, 20, 60, 0.35); + cursor: pointer; + -webkit-tap-highlight-color: transparent; + transition: transform var(--nq-dur-micro) var(--nq-ease-out); + } + + button.complete:active { + transform: scale(0.98); + } + + button.complete svg { + width: 30px; + height: 30px; + } + + button:focus-visible { + outline: 3px solid var(--nq-brand-purple); + outline-offset: 4px; + } + + .seal { + position: absolute; + right: 16px; + top: -12px; + display: inline-flex; + align-items: center; + justify-content: center; + width: var(--nq-p-seal-size); + height: var(--nq-p-seal-size); + border-radius: var(--nq-p-radius-pill); + background: var(--nq-p-seal); + box-shadow: var(--nq-p-seal-shadow); + color: rgba(255, 235, 235, 0.95); + transform: rotate(var(--seal-rot, 0deg)); + animation: seal-in var(--nq-dur-base) var(--nq-ease-out); + pointer-events: none; + } + + .seal svg { + width: 38px; + height: 38px; + } + + @keyframes seal-in { + from { + opacity: 0; + transform: scale(1.15) rotate(var(--seal-rot, 0deg)); + } + to { + opacity: 1; + transform: scale(1) rotate(var(--seal-rot, 0deg)); + } + } + + .rollup { + border: 2px dashed var(--nq-p-panel-border); + border-radius: 14px; + padding: 16px 22px; + } + + .rollup-label { + display: block; + font-family: var(--nq-p-font-heading); + font-size: 18px; + font-weight: 600; + letter-spacing: var(--nq-p-track-label); + text-transform: uppercase; + line-height: 1.2; + color: var(--nq-p-ink-secondary); + } + + .rollup-body { + margin: 6px 0 0; + font-family: var(--nq-p-font-body); + font-size: 23px; + font-weight: 600; + line-height: 1.35; + color: var(--nq-p-ink-secondary); + } + + .scrim { + position: absolute; + inset: 0; + z-index: 10; + background: rgba(30, 20, 10, 0.62); + } + + .dialog { + position: absolute; + top: 196px; + left: 460px; + width: 1000px; + border: 3px solid rgba(92, 62, 26, 0.5); + border-radius: 20px; + background: var(--nq-p-card-panel); + box-shadow: 0 26px 60px rgba(20, 10, 0, 0.5); + padding: 64px 68px 56px; + animation: dialog-in var(--nq-dur-modal) var(--nq-ease-out); + } + + @keyframes dialog-in { + from { + opacity: 0; + transform: scale(0.96); + } + to { + opacity: 1; + transform: scale(1); + } + } + + .dialog-header { + display: flex; + align-items: center; + gap: 24px; + } + + .dialog-tile { + flex: none; + display: inline-flex; + align-items: center; + justify-content: center; + width: 104px; + height: 104px; + border-radius: 12px; + background: var(--nq-p-icon-tile); + color: #ffffff; + } + + .dialog-tile svg { + width: 56px; + height: 56px; + } + + .dialog-titles { + display: flex; + flex-direction: column; + gap: 8px; + min-width: 0; + } + + .dialog-kicker { + font-family: var(--nq-p-font-heading); + font-size: var(--nq-p-size-label); + font-weight: 600; + letter-spacing: 0.24em; + text-transform: uppercase; + line-height: 1.2; + color: var(--nq-p-ink-secondary); + } + + .dialog-quest-title { + font-family: var(--nq-p-font-display); + font-size: 58px; + font-weight: 700; + line-height: 1.1; + color: var(--nq-p-ink); + overflow-wrap: anywhere; + } + + .dialog-body { + margin: 28px 0 0; + font-family: var(--nq-p-font-body); + font-size: 32px; + font-weight: 600; + line-height: 1.35; + color: #3f2f1c; + } + + .dialog-buttons { + display: flex; + gap: 24px; + margin-top: 40px; + } + + button.confirm-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 12px; + height: var(--nq-p-confirm-button); + border-radius: 10px; + font-family: var(--nq-p-font-heading); + font-weight: 700; + line-height: 1; + cursor: pointer; + -webkit-tap-highlight-color: transparent; + transition: transform var(--nq-dur-micro) var(--nq-ease-out); + } + + button.confirm-button:active { + transform: scale(0.98); + } + + button.not-yet { + flex: 1; + border: 2px solid rgba(92, 62, 26, 0.5); + background: rgba(92, 62, 26, 0.1); + color: var(--nq-p-ink); + font-size: 30px; + } + + button.not-yet svg { + width: 34px; + height: 34px; + } + + button.do-complete { + flex: 2; + border: none; + background: var(--nq-brand-gradient); + color: #ffffff; + font-size: 32px; + } + + button.do-complete svg { + width: 38px; + height: 38px; + } +`; + +const ICON_CHECK = html``; + +const ICON_X = html``; + +const ICON_STAR = html``; + +const ICON_SUNRISE = html``; + +const ICON_SUN = html``; + +const ICON_MOON = html``; + +const WINDOW_DEFS: WindowDef[] = [ + { key: "morning", name: "Morning", range: "Until 11:59 AM", icon: ICON_SUNRISE }, + { key: "afternoon", name: "Afternoon", range: "12:00–5:00 PM", icon: ICON_SUN }, + { key: "evening", name: "Evening", range: "5:00–9:00 PM", icon: ICON_MOON }, +]; + export class NestQuestQuestLogCard extends LitElement { static properties = { hass: { attribute: false }, _config: { state: true }, + _now: { state: true }, + _confirm: { state: true }, }; - static styles = [unsafeCSS(panelTokens)]; + static styles = [unsafeCSS(panelTokens), logStyles]; hass?: HassLike; _config?: CardConfig; + _now = new Date(); + _confirm: number | null = null; + private _clockTimer?: number; + private _idleTimer?: number; + private _confirmTimer?: number; setConfig(config: CardConfig): void { if (!config || typeof config !== "object") { @@ -22,12 +909,534 @@ export class NestQuestQuestLogCard extends LitElement { } getCardSize(): number { - return 8; + return 22; + } + + connectedCallback(): void { + super.connectedCallback(); + this._now = new Date(); + this._clockTimer = window.setInterval(() => { + this._now = new Date(); + }, 60000); + window.addEventListener("pointerdown", this._onActivity, true); + window.addEventListener("touchstart", this._onActivity, true); + window.addEventListener("keydown", this._onActivity, true); + this._armIdle(); + } + + disconnectedCallback(): void { + this._stopClock(); + this._clearIdle(); + this._clearConfirmTimer(); + window.removeEventListener("pointerdown", this._onActivity, true); + window.removeEventListener("touchstart", this._onActivity, true); + window.removeEventListener("keydown", this._onActivity, true); + super.disconnectedCallback(); } render() { - return html`
NestQuest Quest Log
`; + return html` +
+
+
+ ${this._renderHeader()} +
+ ${WINDOW_DEFS.map((windowDef) => this._renderColumn(windowDef))} +
+ ${this._renderConfirm()} +
+ `; } + + private _timeZone(): string | undefined { + return hassTimeZone(this.hass); + } + + private _state(entityId: string): StateObject | null { + const states = this.hass?.states as Record | undefined; + const stateObj = states?.[entityId]; + return stateObj && typeof stateObj === "object" ? stateObj : null; + } + + private _childSlug(): string { + return childSlugFromPath(); + } + + private _boardPath(): string { + return asString(this._config?.board_path).replace(/\/+$/, ""); + } + + private _idleSeconds(): number { + const seconds = asNumber(this._config?.idle_return_seconds, 40); + return seconds > 0 ? seconds : 40; + } + + private _confirmSeconds(): number { + const seconds = asNumber(this._config?.confirm_timeout_seconds, 15); + return seconds > 0 ? seconds : 15; + } + + private _childName(slug: string): string | null { + if (!slug) { + return null; + } + const name = this._state(`sensor.nestquest_${slug}_quests_due_today`) + ?.attributes?.child_name; + if (typeof name === "string" && name.trim()) { + return name.trim(); + } + return null; + } + + private _childCounts(slug: string): { due: number; completed: number } { + const dueSensor = this._state(`sensor.nestquest_${slug}_quests_due_today`); + const completedSensor = this._state( + `sensor.nestquest_${slug}_quests_completed_today` + ); + const due = clampCount(asNumber(dueSensor?.state, 0)); + const completed = clampCount(asNumber(completedSensor?.state, 0)); + return { due, completed }; + } + + private _instances(): PanelInstance[] { + const slug = this._childSlug(); + if (!slug) { + return []; + } + const payload = this._state(`sensor.nestquest_${slug}_quests_due_today`) + ?.attributes?.instances; + if (!Array.isArray(payload)) { + return []; + } + return payload + .map(toPanelInstance) + .filter((instance): instance is PanelInstance => instance !== null); + } + + private _remaining(): number { + const slug = this._childSlug(); + if (!slug) { + return 0; + } + const sensor = this._state( + `sensor.nestquest_${slug}_quests_remaining_today` + ); + if (sensor) { + return clampCount(asNumber(sensor.state, 0)); + } + const { due, completed } = this._childCounts(slug); + return clampCount(due - completed); + } + + private _partyCounts(): { completed: number; due: number } { + const householdDue = this._state( + "sensor.nestquest_household_quests_due_today" + ); + const householdCompleted = this._state( + "sensor.nestquest_household_quests_completed_today" + ); + if (householdDue || householdCompleted) { + return { + completed: clampCount(asNumber(householdCompleted?.state, 0)), + due: clampCount(asNumber(householdDue?.state, 0)), + }; + } + const slug = this._childSlug(); + const { due, completed } = this._childCounts(slug); + return { completed, due }; + } + + private _otherChildren(): OtherChild[] { + const states = this.hass?.states as + | Record + | undefined; + if (!states) { + return []; + } + const slug = this._childSlug(); + const timeZone = this._timeZone(); + const result: OtherChild[] = []; + for (const entityId of Object.keys(states)) { + const match = /^sensor\.nestquest_(.+)_quests_due_today$/.exec(entityId); + if (!match || match[1] === "household" || match[1] === slug) { + continue; + } + const childSlug = match[1]; + const attrs = states[entityId]?.attributes ?? {}; + let name = ""; + if (typeof attrs.child_name === "string" && attrs.child_name.trim()) { + name = attrs.child_name.trim(); + } else { + name = titleCaseSlug(childSlug); + } + let present = true; + if (typeof attrs.present === "boolean") { + present = attrs.present; + } else { + const presence = this._state( + `binary_sensor.nestquest_${childSlug}_present_today` + ); + present = + presence === null || + String(presence.state ?? "").trim().toLowerCase() !== "off"; + } + const completedSensor = this._state( + `sensor.nestquest_${childSlug}_quests_completed_today` + ); + const due = clampCount(asNumber(states[entityId]?.state, 0)); + const completed = clampCount(asNumber(completedSensor?.state, 0)); + let returnsWeekday: string | null = null; + if (!present) { + const presence = this._state( + `binary_sensor.nestquest_${childSlug}_present_today` + ); + const nextPresent = parseIsoDate( + presence?.attributes?.next_present, + timeZone + ); + returnsWeekday = nextPresent + ? formatWeekday(nextPresent, timeZone) + : null; + } + result.push({ + slug: childSlug, + name, + present, + due, + completed, + returnsWeekday, + }); + } + return result.sort((a, b) => a.name.localeCompare(b.name)); + } + + private _renderHeader() { + const slug = this._childSlug(); + const name = this._childName(slug); + const displayName = name ?? (slug ? titleCaseSlug(slug) : null); + const { due, completed } = this._childCounts(slug); + const remaining = this._remaining(); + const party = this._partyCounts(); + const title = displayName ? `${displayName}'s Quest Log` : "Quest Log"; + const initial = (displayName?.charAt(0) || "?").toUpperCase(); + return html` +
+ +
+

${title}

+

+ ${formatDay(this._now, this._timeZone())} · ${completed} of ${due} + claimed +

+
+
+ +
+ + ${remaining} quest${remaining === 1 ? "" : "s"} left + + + Party progress · ${party.completed} of ${party.due} today + +
+
+
+ `; + } + + private _renderColumn(windowDef: WindowDef) { + const instances = this._instances().filter( + (instance) => instance.window === windowDef.key + ); + const open = instances + .filter((instance) => instance.state === "open") + .sort((a, b) => + (a.due_time ?? "99:99").localeCompare(b.due_time ?? "99:99") + ); + const sealed = instances.filter( + (instance) => instance.state === "completed" + ); + const count = instances.length; + let stackStyle: string | typeof nothing = nothing; + if (count > 0) { + const rollupReserve = + windowDef.key === "afternoon" && this._otherChildren().length > 0 + ? 160 + : 0; + const available = 644 - rollupReserve; + const gap = Math.min( + 24, + Math.max(8, Math.floor((available - count * 74) / Math.max(count - 1, 1))) + ); + const minHeight = Math.min( + 116, + Math.max(58, Math.floor((available - (count - 1) * gap) / count)) + ); + const buttonHeight = Math.max(56, Math.min(72, minHeight - 2)); + let stackVars = `--nq-p-quest-gap: ${gap}px; --nq-p-quest-min-height: ${minHeight}px; --nq-p-button-height: ${buttonHeight}px`; + if (count >= 5) { + const pad = Math.max(0, Math.min(22, Math.floor((minHeight - buttonHeight - 2) / 2))); + const tile = Math.max(44, Math.min(64, minHeight - 2 * pad - 2)); + stackVars += `; --nq-p-quest-pad: ${pad}px; --nq-p-tile-size: ${tile}px`; + } + stackStyle = stackVars; + } + return html` +
+
+ ${windowDef.icon} +
+ ${windowDef.name} + ${windowDef.range} +
+ ${sealed.length}/${instances.length} +
+
+ ${repeat( + open, + (instance) => instance.id, + (instance) => this._renderQuest(instance, false, 0) + )} + ${repeat( + sealed, + (instance) => instance.id, + (instance, index) => this._renderQuest(instance, true, index) + )} + ${windowDef.key === "afternoon" ? this._renderRollup() : nothing} +
+
+ `; + } + + private _renderQuest( + instance: PanelInstance, + sealed: boolean, + sealedIndex: number + ) { + if (sealed) { + const rotation = SEAL_ROTATIONS[sealedIndex % SEAL_ROTATIONS.length]; + const claimedAt = instance.completed_at + ? formatInstant(instance.completed_at, this._timeZone()) + : ""; + return html` +
+ +
+ ${instance.title} + + ${claimedAt ? `Claimed ${claimedAt}` : "Claimed"} + +
+ +
+ `; + } + const dueAt = formatWallClock(instance.due_time); + const meta = instance.overdue + ? { + text: dueAt ? `Overdue · due ${dueAt}` : "Overdue", + late: true, + } + : { + text: dueAt ? `Due by ${dueAt}` : "Due today", + late: false, + }; + return html` +
this._openConfirm(instance.id)} + @keydown=${(event: KeyboardEvent) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + this._openConfirm(instance.id); + } + }} + > + +
+ ${instance.title} + ${meta.text} +
+ +
+ `; + } + + private _renderRollup() { + const others = this._otherChildren(); + if (others.length === 0) { + return nothing; + } + const sentences = others.slice(0, 2).map((child) => { + if (!child.present) { + return child.returnsWeekday + ? `${child.name} is on travels until ${child.returnsWeekday}.` + : `${child.name} is on travels.`; + } + if (child.due > 0 && child.completed >= child.due) { + return `${child.name} has finished their log.`; + } + if (child.due > child.completed) { + return `${child.name} has ${child.due - child.completed} quests left.`; + } + return `${child.name} has no quests today.`; + }); + return html` +
+ Party roll-up +

${sentences.join(" ")}

+
+ `; + } + + private _renderConfirm() { + if (this._confirm === null) { + return nothing; + } + const instance = this._instances().find( + (candidate) => candidate.id === this._confirm + ); + if (!instance) { + return nothing; + } + const windowDef = WINDOW_DEFS.find((entry) => entry.key === instance.window); + return html` +
this._closeConfirm()}> + +
+ `; + } + + private _openConfirm(instanceId: number): void { + this._confirm = instanceId; + this._armConfirmTimer(); + } + + private _closeConfirm(): void { + this._clearConfirmTimer(); + this._confirm = null; + } + + private _confirmComplete(): void { + this._closeConfirm(); + } + + private _armConfirmTimer(): void { + this._clearConfirmTimer(); + this._confirmTimer = window.setTimeout(() => { + this._confirmTimer = undefined; + this._closeConfirm(); + }, this._confirmSeconds() * 1000); + } + + private _clearConfirmTimer(): void { + if (this._confirmTimer !== undefined) { + window.clearTimeout(this._confirmTimer); + this._confirmTimer = undefined; + } + } + + private _armIdle(): void { + this._clearIdle(); + this._idleTimer = window.setTimeout(() => { + this._idleTimer = undefined; + this._returnToBoard(); + }, this._idleSeconds() * 1000); + } + + private _clearIdle(): void { + if (this._idleTimer !== undefined) { + window.clearTimeout(this._idleTimer); + this._idleTimer = undefined; + } + } + + private _returnToBoard(): void { + const path = this._boardPath(); + if (!path) { + return; + } + window.history.pushState(null, "", path); + window.dispatchEvent(new Event("location-changed")); + } + + private _stopClock(): void { + if (this._clockTimer !== undefined) { + window.clearInterval(this._clockTimer); + this._clockTimer = undefined; + } + } + + private _onActivity = (): void => { + this._armIdle(); + if (this._confirm !== null) { + this._armConfirmTimer(); + } + }; } customElements.define("nestquest-quest-log-card", NestQuestQuestLogCard);