From edca25affa6cab1dc08b3574433995cf6bd38aea Mon Sep 17 00:00:00 2001 From: Joshua Date: Sun, 20 Sep 2026 10:08:38 -0400 Subject: [PATCH] Add the Lit card toolchain and auto-register the Lovelace bundle --- .gitignore | 1 + README.md | 14 + custom_components/nestquest/__init__.py | 3 + custom_components/nestquest/frontend.py | 35 + custom_components/nestquest/manifest.json | 4 + .../nestquest/www/nestquest-cards.js | 572 ++++++++ frontend/package-lock.json | 1232 +++++++++++++++++ frontend/package.json | 15 + frontend/src/cards/admin.ts | 35 + frontend/src/cards/party-board.ts | 35 + frontend/src/cards/quest-log.ts | 35 + frontend/src/nestquest-cards.ts | 3 + frontend/src/types.ts | 20 + frontend/tsconfig.json | 13 + frontend/vite.config.ts | 19 + tests/conftest.py | 38 + tests/test_frontend.py | 93 ++ 17 files changed, 2167 insertions(+) create mode 100644 custom_components/nestquest/frontend.py create mode 100644 custom_components/nestquest/www/nestquest-cards.js create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/src/cards/admin.ts create mode 100644 frontend/src/cards/party-board.ts create mode 100644 frontend/src/cards/quest-log.ts create mode 100644 frontend/src/nestquest-cards.ts create mode 100644 frontend/src/types.ts create mode 100644 frontend/tsconfig.json create mode 100644 frontend/vite.config.ts create mode 100644 tests/test_frontend.py diff --git a/.gitignore b/.gitignore index 7b33e7f..cc771bf 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ __pycache__/ .venv/ .pytest_cache/ *.metadata.json +frontend/node_modules/ diff --git a/README.md b/README.md index 5f54fdd..7887abe 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,20 @@ To set up the development environment and install dependencies: uv sync ``` +### Frontend cards + +The Lovelace cards live in `frontend/` (TypeScript + Lit) and build to a +single bundle at `custom_components/nestquest/www/nestquest-cards.js`. The +integration registers that bundle as a Lovelace resource automatically on +setup — do not add it by hand. Card YAML is documented in +`design/ENTITIES-AND-SERVICES.md` §4. + +```bash +cd frontend +npm install +npm run build +``` + ### Running Tests Run the test suite with `pytest`: diff --git a/custom_components/nestquest/__init__.py b/custom_components/nestquest/__init__.py index 143e9e0..53e395d 100644 --- a/custom_components/nestquest/__init__.py +++ b/custom_components/nestquest/__init__.py @@ -28,6 +28,7 @@ from .const import ( LOGGER, PLATFORMS, ) +from .frontend import async_register_frontend from .coordinator import NestQuestCoordinator from .db import NestQuestDatabase from .materialize import materialize as _materialize_run @@ -410,11 +411,13 @@ def _deregister_services(hass: HomeAssistant) -> None: async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool: """Set up the NestQuest integration. YAML configuration is not used, returns True.""" + await async_register_frontend(hass) return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up NestQuest from a config entry.""" + await async_register_frontend(hass) hass.data.setdefault(DOMAIN, {}) existing = hass.data[DOMAIN].get(entry.entry_id) if existing is not None: diff --git a/custom_components/nestquest/frontend.py b/custom_components/nestquest/frontend.py new file mode 100644 index 0000000..c46ccbc --- /dev/null +++ b/custom_components/nestquest/frontend.py @@ -0,0 +1,35 @@ +"""Lovelace card bundle registration for NestQuest.""" +from __future__ import annotations + +from pathlib import Path + +from homeassistant.components.frontend import add_extra_js_url +from homeassistant.components.http import StaticPathConfig +from homeassistant.core import HomeAssistant + +WWW_DIR = Path(__file__).parent / "www" +FRONTEND_URL_BASE = "/nestquest-static" +BUNDLE_FILENAME = "nestquest-cards.js" +BUNDLE_URL = f"{FRONTEND_URL_BASE}/{BUNDLE_FILENAME}" +_REGISTERED_ATTR = "_nestquest_frontend_registered" + + +async def async_register_frontend(hass: HomeAssistant) -> None: + """Serve the card bundle and inject it into every Lovelace page. + + Idempotent across config entries: static paths and extra JS URLs are + process-global, so a second entry must not register them again. + """ + if getattr(hass, _REGISTERED_ATTR, False): + return + await hass.http.async_register_static_paths( + [ + StaticPathConfig( + FRONTEND_URL_BASE, + str(WWW_DIR), + cache_headers=False, + ) + ] + ) + add_extra_js_url(hass, BUNDLE_URL) + setattr(hass, _REGISTERED_ATTR, True) diff --git a/custom_components/nestquest/manifest.json b/custom_components/nestquest/manifest.json index ffef4b2..a85ae11 100644 --- a/custom_components/nestquest/manifest.json +++ b/custom_components/nestquest/manifest.json @@ -5,6 +5,10 @@ "@overseer" ], "config_flow": true, + "dependencies": [ + "frontend", + "http" + ], "documentation": "https://github.com/talon2king/NestQuest", "integration_type": "hub", "iot_class": "local_polling", diff --git a/custom_components/nestquest/www/nestquest-cards.js b/custom_components/nestquest/www/nestquest-cards.js new file mode 100644 index 0000000..8f85055 --- /dev/null +++ b/custom_components/nestquest/www/nestquest-cards.js @@ -0,0 +1,572 @@ +const U = globalThis, k = U.ShadowRoot && (U.ShadyCSS === void 0 || U.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype, Y = /* @__PURE__ */ Symbol(), I = /* @__PURE__ */ new WeakMap(); +let nt = class { + constructor(t, e, s) { + if (this._$cssResult$ = !0, s !== Y) 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 (k && t === void 0) { + const s = e !== void 0 && e.length === 1; + s && (t = I.get(e)), t === void 0 && ((this.o = t = new CSSStyleSheet()).replaceSync(this.cssText), s && I.set(e, t)); + } + return t; + } + toString() { + return this.cssText; + } +}; +const ot = (r) => new nt(typeof r == "string" ? r : r + "", void 0, Y), ht = (r, t) => { + if (k) r.adoptedStyleSheets = t.map((e) => e instanceof CSSStyleSheet ? e : e.styleSheet); + else for (const e of t) { + const s = document.createElement("style"), i = U.litNonce; + i !== void 0 && s.setAttribute("nonce", i), s.textContent = e.cssText, r.appendChild(s); + } +}, q = k ? (r) => r : (r) => r instanceof CSSStyleSheet ? ((t) => { + let e = ""; + for (const s of t.cssRules) e += s.cssText; + return ot(e); +})(r) : r; +const { is: at, defineProperty: lt, getOwnPropertyDescriptor: ct, getOwnPropertyNames: dt, getOwnPropertySymbols: pt, getPrototypeOf: ut } = Object, H = globalThis, Q = H.trustedTypes, $t = Q ? Q.emptyScript : "", _t = H.reactiveElementPolyfillSupport, b = (r, t) => r, R = { toAttribute(r, t) { + switch (t) { + case Boolean: + r = r ? $t : null; + break; + case Object: + case Array: + r = r == null ? r : JSON.stringify(r); + } + return r; +}, fromAttribute(r, t) { + let e = r; + switch (t) { + case Boolean: + e = r !== null; + break; + case Number: + e = r === null ? null : Number(r); + break; + case Object: + case Array: + try { + e = JSON.parse(r); + } catch { + e = null; + } + } + return e; +} }, tt = (r, t) => !at(r, t), W = { attribute: !0, type: String, converter: R, reflect: !1, useDefault: !1, hasChanged: tt }; +Symbol.metadata ??= /* @__PURE__ */ Symbol("metadata"), H.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap(); +let y = 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 = W) { + 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 s = /* @__PURE__ */ Symbol(), i = this.getPropertyDescriptor(t, s, e); + i !== void 0 && lt(this.prototype, t, i); + } + } + static getPropertyDescriptor(t, e, s) { + const { get: i, set: n } = ct(this.prototype, t) ?? { get() { + return this[e]; + }, set(o) { + this[e] = o; + } }; + return { get: i, set(o) { + const l = i?.call(this); + n?.call(this, o), this.requestUpdate(t, l, s); + }, configurable: !0, enumerable: !0 }; + } + static getPropertyOptions(t) { + return this.elementProperties.get(t) ?? W; + } + static _$Ei() { + if (this.hasOwnProperty(b("elementProperties"))) return; + const t = ut(this); + t.finalize(), t.l !== void 0 && (this.l = [...t.l]), this.elementProperties = new Map(t.elementProperties); + } + static finalize() { + if (this.hasOwnProperty(b("finalized"))) return; + if (this.finalized = !0, this._$Ei(), this.hasOwnProperty(b("properties"))) { + const e = this.properties, s = [...dt(e), ...pt(e)]; + for (const i of s) this.createProperty(i, e[i]); + } + const t = this[Symbol.metadata]; + if (t !== null) { + const e = litPropertyMetadata.get(t); + if (e !== void 0) for (const [s, i] of e) this.elementProperties.set(s, i); + } + this._$Eh = /* @__PURE__ */ new Map(); + for (const [e, s] of this.elementProperties) { + const i = this._$Eu(e, s); + i !== void 0 && this._$Eh.set(i, e); + } + this.elementStyles = this.finalizeStyles(this.styles); + } + static finalizeStyles(t) { + const e = []; + if (Array.isArray(t)) { + const s = new Set(t.flat(1 / 0).reverse()); + for (const i of s) e.unshift(q(i)); + } else t !== void 0 && e.push(q(t)); + return e; + } + static _$Eu(t, e) { + const s = e.attribute; + return s === !1 ? void 0 : typeof s == "string" ? s : typeof t == "string" ? t.toLowerCase() : void 0; + } + constructor() { + super(), this._$Ep = void 0, this.isUpdatePending = !1, this.hasUpdated = !1, this._$Em = null, this._$Ev(); + } + _$Ev() { + this._$ES = new Promise((t) => this.enableUpdating = t), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t) => t(this)); + } + addController(t) { + (this._$EO ??= /* @__PURE__ */ new Set()).add(t), this.renderRoot !== void 0 && this.isConnected && t.hostConnected?.(); + } + removeController(t) { + this._$EO?.delete(t); + } + _$E_() { + const t = /* @__PURE__ */ new Map(), e = this.constructor.elementProperties; + for (const s of e.keys()) this.hasOwnProperty(s) && (t.set(s, this[s]), delete this[s]); + t.size > 0 && (this._$Ep = t); + } + createRenderRoot() { + const t = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions); + return ht(t, this.constructor.elementStyles), t; + } + connectedCallback() { + this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(!0), this._$EO?.forEach((t) => t.hostConnected?.()); + } + enableUpdating(t) { + } + disconnectedCallback() { + this._$EO?.forEach((t) => t.hostDisconnected?.()); + } + attributeChangedCallback(t, e, s) { + this._$AK(t, s); + } + _$ET(t, e) { + const s = this.constructor.elementProperties.get(t), i = this.constructor._$Eu(t, s); + if (i !== void 0 && s.reflect === !0) { + const n = (s.converter?.toAttribute !== void 0 ? s.converter : R).toAttribute(e, s.type); + this._$Em = t, n == null ? this.removeAttribute(i) : this.setAttribute(i, n), this._$Em = null; + } + } + _$AK(t, e) { + const s = this.constructor, i = s._$Eh.get(t); + if (i !== void 0 && this._$Em !== i) { + const n = s.getPropertyOptions(i), o = typeof n.converter == "function" ? { fromAttribute: n.converter } : n.converter?.fromAttribute !== void 0 ? n.converter : R; + this._$Em = i; + const l = o.fromAttribute(e, n.type); + this[i] = l ?? this._$Ej?.get(i) ?? l, this._$Em = null; + } + } + requestUpdate(t, e, s, i = !1, n) { + if (t !== void 0) { + const o = this.constructor; + if (i === !1 && (n = this[t]), s ??= o.getPropertyOptions(t), !((s.hasChanged ?? tt)(n, e) || s.useDefault && s.reflect && n === this._$Ej?.get(t) && !this.hasAttribute(o._$Eu(t, s)))) return; + this.C(t, e, s); + } + this.isUpdatePending === !1 && (this._$ES = this._$EP()); + } + C(t, e, { useDefault: s, reflect: i, wrapped: n }, o) { + s && !(this._$Ej ??= /* @__PURE__ */ new Map()).has(t) && (this._$Ej.set(t, o ?? e ?? this[t]), n !== !0 || o !== void 0) || (this._$AL.has(t) || (this.hasUpdated || s || (e = void 0), this._$AL.set(t, e)), i === !0 && this._$Em !== t && (this._$Eq ??= /* @__PURE__ */ new Set()).add(t)); + } + async _$EP() { + this.isUpdatePending = !0; + try { + await this._$ES; + } catch (e) { + Promise.reject(e); + } + const t = this.scheduleUpdate(); + return t != null && await t, !this.isUpdatePending; + } + scheduleUpdate() { + return this.performUpdate(); + } + performUpdate() { + if (!this.isUpdatePending) return; + if (!this.hasUpdated) { + if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) { + for (const [i, n] of this._$Ep) this[i] = n; + this._$Ep = void 0; + } + const s = this.constructor.elementProperties; + if (s.size > 0) for (const [i, n] of s) { + const { wrapped: o } = n, l = this[i]; + o !== !0 || this._$AL.has(i) || l === void 0 || this.C(i, void 0, n, l); + } + } + let t = !1; + const e = this._$AL; + try { + t = this.shouldUpdate(e), t ? (this.willUpdate(e), this._$EO?.forEach((s) => s.hostUpdate?.()), this.update(e)) : this._$EM(); + } catch (s) { + throw t = !1, this._$EM(), s; + } + t && this._$AE(e); + } + willUpdate(t) { + } + _$AE(t) { + this._$EO?.forEach((e) => e.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = !0, this.firstUpdated(t)), this.updated(t); + } + _$EM() { + this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = !1; + } + get updateComplete() { + return this.getUpdateComplete(); + } + getUpdateComplete() { + return this._$ES; + } + shouldUpdate(t) { + return !0; + } + update(t) { + this._$Eq &&= this._$Eq.forEach((e) => this._$ET(e, this[e])), this._$EM(); + } + updated(t) { + } + firstUpdated(t) { + } +}; +y.elementStyles = [], y.shadowRootOptions = { mode: "open" }, y[b("elementProperties")] = /* @__PURE__ */ new Map(), y[b("finalized")] = /* @__PURE__ */ new Map(), _t?.({ ReactiveElement: y }), (H.reactiveElementVersions ??= []).push("2.1.2"); +const L = globalThis, V = (r) => r, O = L.trustedTypes, K = O ? O.createPolicy("lit-html", { createHTML: (r) => r }) : void 0, et = "$lit$", _ = `lit$${Math.random().toFixed(9).slice(2)}$`, st = "?" + _, ft = `<${st}>`, g = document, w = () => g.createComment(""), C = (r) => r === null || typeof r != "object" && typeof r != "function", z = Array.isArray, At = (r) => z(r) || typeof r?.[Symbol.iterator] == "function", T = `[ +\f\r]`, S = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, Z = /-->/g, J = />/g, f = RegExp(`>|${T}(?:([^\\s"'>=/]+)(${T}*=${T}*(?:[^ +\f\r"'\`<>=]|("|')|))|$)`, "g"), F = /'/g, G = /"/g, it = /^(?:script|style|textarea|title)$/i, mt = (r) => (t, ...e) => ({ _$litType$: r, strings: t, values: e }), j = mt(1), v = /* @__PURE__ */ Symbol.for("lit-noChange"), d = /* @__PURE__ */ Symbol.for("lit-nothing"), X = /* @__PURE__ */ new WeakMap(), A = g.createTreeWalker(g, 129); +function rt(r, t) { + if (!z(r) || !r.hasOwnProperty("raw")) throw Error("invalid template strings array"); + return K !== void 0 ? K.createHTML(t) : t; +} +const gt = (r, t) => { + const e = r.length - 1, s = []; + let i, n = t === 2 ? "" : t === 3 ? "" : "", o = S; + for (let l = 0; l < e; l++) { + const h = r[l]; + let c, p, a = -1, u = 0; + for (; u < h.length && (o.lastIndex = u, p = o.exec(h), p !== null); ) u = o.lastIndex, o === S ? p[1] === "!--" ? o = Z : p[1] !== void 0 ? o = J : p[2] !== void 0 ? (it.test(p[2]) && (i = RegExp("" ? (o = i ?? S, a = -1) : p[1] === void 0 ? a = -2 : (a = o.lastIndex - p[2].length, c = p[1], o = p[3] === void 0 ? f : p[3] === '"' ? G : F) : o === G || o === F ? o = f : o === Z || o === J ? o = S : (o = f, i = void 0); + const $ = o === f && r[l + 1].startsWith("/>") ? " " : ""; + n += o === S ? h + ft : a >= 0 ? (s.push(c), h.slice(0, a) + et + h.slice(a) + _ + $) : h + _ + (a === -2 ? l : $); + } + return [rt(r, n + (r[e] || "") + (t === 2 ? "" : t === 3 ? "" : "")), s]; +}; +class P { + constructor({ strings: t, _$litType$: e }, s) { + let i; + this.parts = []; + let n = 0, o = 0; + const l = t.length - 1, h = this.parts, [c, p] = gt(t, e); + if (this.el = P.createElement(c, s), A.currentNode = this.el.content, e === 2 || e === 3) { + const a = this.el.content.firstChild; + a.replaceWith(...a.childNodes); + } + for (; (i = A.nextNode()) !== null && h.length < l; ) { + if (i.nodeType === 1) { + if (i.hasAttributes()) for (const a of i.getAttributeNames()) if (a.endsWith(et)) { + const u = p[o++], $ = i.getAttribute(a).split(_), N = /([.?@])?(.*)/.exec(u); + h.push({ type: 1, index: n, name: N[2], strings: $, ctor: N[1] === "." ? vt : N[1] === "?" ? Et : N[1] === "@" ? St : M }), i.removeAttribute(a); + } else a.startsWith(_) && (h.push({ type: 6, index: n }), i.removeAttribute(a)); + if (it.test(i.tagName)) { + const a = i.textContent.split(_), u = a.length - 1; + if (u > 0) { + i.textContent = O ? O.emptyScript : ""; + for (let $ = 0; $ < u; $++) i.append(a[$], w()), A.nextNode(), h.push({ type: 2, index: ++n }); + i.append(a[u], w()); + } + } + } else if (i.nodeType === 8) if (i.data === st) h.push({ type: 2, index: n }); + else { + let a = -1; + for (; (a = i.data.indexOf(_, a + 1)) !== -1; ) h.push({ type: 7, index: n }), a += _.length - 1; + } + n++; + } + } + static createElement(t, e) { + const s = g.createElement("template"); + return s.innerHTML = t, s; + } +} +function E(r, t, e = r, s) { + if (t === v) return t; + let i = s !== void 0 ? e._$Co?.[s] : e._$Cl; + const n = C(t) ? void 0 : t._$litDirective$; + return i?.constructor !== n && (i?._$AO?.(!1), n === void 0 ? i = void 0 : (i = new n(r), i._$AT(r, e, s)), s !== void 0 ? (e._$Co ??= [])[s] = i : e._$Cl = i), i !== void 0 && (t = E(r, i._$AS(r, t.values), i, s)), t; +} +class yt { + constructor(t, e) { + this._$AV = [], this._$AN = void 0, this._$AD = t, this._$AM = e; + } + get parentNode() { + return this._$AM.parentNode; + } + get _$AU() { + return this._$AM._$AU; + } + u(t) { + const { el: { content: e }, parts: s } = this._$AD, i = (t?.creationScope ?? g).importNode(e, !0); + A.currentNode = i; + let n = A.nextNode(), o = 0, l = 0, h = s[0]; + for (; h !== void 0; ) { + if (o === h.index) { + let c; + h.type === 2 ? c = new x(n, n.nextSibling, this, t) : h.type === 1 ? c = new h.ctor(n, h.name, h.strings, this, t) : h.type === 6 && (c = new bt(n, this, t)), this._$AV.push(c), h = s[++l]; + } + o !== h?.index && (n = A.nextNode(), o++); + } + return A.currentNode = g, i; + } + p(t) { + let e = 0; + for (const s of this._$AV) s !== void 0 && (s.strings !== void 0 ? (s._$AI(t, s, e), e += s.strings.length - 2) : s._$AI(t[e])), e++; + } +} +class x { + get _$AU() { + return this._$AM?._$AU ?? this._$Cv; + } + constructor(t, e, s, i) { + this.type = 2, this._$AH = d, this._$AN = void 0, this._$AA = t, this._$AB = e, this._$AM = s, this.options = i, this._$Cv = i?.isConnected ?? !0; + } + get parentNode() { + let t = this._$AA.parentNode; + const e = this._$AM; + return e !== void 0 && t?.nodeType === 11 && (t = e.parentNode), t; + } + get startNode() { + return this._$AA; + } + get endNode() { + return this._$AB; + } + _$AI(t, e = this) { + t = E(this, t, e), C(t) ? t === d || t == null || t === "" ? (this._$AH !== d && this._$AR(), this._$AH = d) : t !== this._$AH && t !== v && this._(t) : t._$litType$ !== void 0 ? this.$(t) : t.nodeType !== void 0 ? this.T(t) : At(t) ? this.k(t) : this._(t); + } + O(t) { + return this._$AA.parentNode.insertBefore(t, this._$AB); + } + T(t) { + this._$AH !== t && (this._$AR(), this._$AH = this.O(t)); + } + _(t) { + this._$AH !== d && C(this._$AH) ? this._$AA.nextSibling.data = t : this.T(g.createTextNode(t)), this._$AH = t; + } + $(t) { + const { values: e, _$litType$: s } = t, i = typeof s == "number" ? this._$AC(t) : (s.el === void 0 && (s.el = P.createElement(rt(s.h, s.h[0]), this.options)), s); + if (this._$AH?._$AD === i) this._$AH.p(e); + else { + const n = new yt(i, this), o = n.u(this.options); + n.p(e), this.T(o), this._$AH = n; + } + } + _$AC(t) { + let e = X.get(t.strings); + return e === void 0 && X.set(t.strings, e = new P(t)), e; + } + k(t) { + z(this._$AH) || (this._$AH = [], this._$AR()); + const e = this._$AH; + let s, i = 0; + for (const n of t) i === e.length ? e.push(s = new x(this.O(w()), this.O(w()), this, this.options)) : s = e[i], s._$AI(n), i++; + i < e.length && (this._$AR(s && s._$AB.nextSibling, i), e.length = i); + } + _$AR(t = this._$AA.nextSibling, e) { + for (this._$AP?.(!1, !0, e); t !== this._$AB; ) { + const s = V(t).nextSibling; + V(t).remove(), t = s; + } + } + setConnected(t) { + this._$AM === void 0 && (this._$Cv = t, this._$AP?.(t)); + } +} +class M { + get tagName() { + return this.element.tagName; + } + get _$AU() { + return this._$AM._$AU; + } + constructor(t, e, s, i, n) { + this.type = 1, this._$AH = d, this._$AN = void 0, this.element = t, this.name = e, this._$AM = i, this.options = n, s.length > 2 || s[0] !== "" || s[1] !== "" ? (this._$AH = Array(s.length - 1).fill(new String()), this.strings = s) : this._$AH = d; + } + _$AI(t, e = this, s, i) { + const n = this.strings; + let o = !1; + if (n === void 0) t = E(this, t, e, 0), o = !C(t) || t !== this._$AH && t !== v, o && (this._$AH = t); + else { + const l = t; + let h, c; + for (t = n[0], h = 0; h < n.length - 1; h++) c = E(this, l[s + h], e, h), c === v && (c = this._$AH[h]), o ||= !C(c) || c !== this._$AH[h], c === d ? t = d : t !== d && (t += (c ?? "") + n[h + 1]), this._$AH[h] = c; + } + o && !i && this.j(t); + } + j(t) { + t === d ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t ?? ""); + } +} +class vt extends M { + constructor() { + super(...arguments), this.type = 3; + } + j(t) { + this.element[this.name] = t === d ? void 0 : t; + } +} +class Et extends M { + constructor() { + super(...arguments), this.type = 4; + } + j(t) { + this.element.toggleAttribute(this.name, !!t && t !== d); + } +} +class St extends M { + constructor(t, e, s, i, n) { + super(t, e, s, i, n), this.type = 5; + } + _$AI(t, e = this) { + if ((t = E(this, t, e, 0) ?? d) === v) return; + const s = this._$AH, i = t === d && s !== d || t.capture !== s.capture || t.once !== s.once || t.passive !== s.passive, n = t !== d && (s === d || i); + i && this.element.removeEventListener(this.name, this, s), n && 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 bt { + constructor(t, e, s) { + this.element = t, this.type = 6, this._$AN = void 0, this._$AM = e, this.options = s; + } + get _$AU() { + return this._$AM._$AU; + } + _$AI(t) { + E(this, t); + } +} +const wt = L.litHtmlPolyfillSupport; +wt?.(P, x), (L.litHtmlVersions ??= []).push("3.3.3"); +const Ct = (r, t, e) => { + const s = e?.renderBefore ?? t; + let i = s._$litPart$; + if (i === void 0) { + const n = e?.renderBefore ?? null; + s._$litPart$ = i = new x(t.insertBefore(w(), n), n, void 0, e ?? {}); + } + return i._$AI(r), i; +}; +const D = globalThis; +class m extends y { + constructor() { + super(...arguments), this.renderOptions = { host: this }, this._$Do = void 0; + } + createRenderRoot() { + const t = super.createRenderRoot(); + return this.renderOptions.renderBefore ??= t.firstChild, t; + } + update(t) { + const e = this.render(); + this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t), this._$Do = Ct(e, this.renderRoot, this.renderOptions); + } + connectedCallback() { + super.connectedCallback(), this._$Do?.setConnected(!0); + } + disconnectedCallback() { + super.disconnectedCallback(), this._$Do?.setConnected(!1); + } + render() { + return v; + } +} +m._$litElement$ = !0, m.finalized = !0, D.litElementHydrateSupport?.({ LitElement: m }); +const Pt = D.litElementPolyfillSupport; +Pt?.({ LitElement: m }); +(D.litElementVersions ??= []).push("4.2.2"); +function B(r) { + window.customCards = window.customCards ?? [], window.customCards.push(r); +} +class xt extends m { + static properties = { + hass: { attribute: !1 }, + _config: { state: !0 } + }; + hass; + _config; + setConfig(t) { + if (!t || typeof t != "object") + throw new Error("Invalid configuration"); + this._config = t; + } + getCardSize() { + return 6; + } + render() { + return j`
NestQuest Party Board
`; + } +} +customElements.define("nestquest-party-board-card", xt); +B({ + type: "nestquest-party-board-card", + name: "NestQuest Party Board", + description: "Kids' attract screen for present and away adventurers." +}); +class Nt extends m { + static properties = { + hass: { attribute: !1 }, + _config: { state: !0 } + }; + hass; + _config; + setConfig(t) { + if (!t || typeof t != "object") + throw new Error("Invalid configuration"); + this._config = t; + } + getCardSize() { + return 8; + } + render() { + return j`
NestQuest Quest Log
`; + } +} +customElements.define("nestquest-quest-log-card", Nt); +B({ + type: "nestquest-quest-log-card", + name: "NestQuest Quest Log", + description: "One child's daily quests, grouped by window." +}); +class Ut extends m { + static properties = { + hass: { attribute: !1 }, + _config: { state: !0 } + }; + hass; + _config; + setConfig(t) { + if (!t || typeof t != "object") + throw new Error("Invalid configuration"); + this._config = t; + } + getCardSize() { + return 8; + } + render() { + return j`
NestQuest Admin
`; + } +} +customElements.define("nestquest-admin-card", Ut); +B({ + type: "nestquest-admin-card", + name: "NestQuest Admin", + description: "Parent phone view for today, tasks, schedule, and history." +}); diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..2a21a54 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1232 @@ +{ + "name": "nestquest-cards", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nestquest-cards", + "dependencies": { + "lit": "^3.3.0" + }, + "devDependencies": { + "typescript": "^5.9.2", + "vite": "^7.1.5" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.6.0.tgz", + "integrity": "sha512-VHb0ALPMTlgKjM6yIxxoQNnpKyUKLD04VzeQdsiXkMqkvYlAHxq9glGLmgbb889/1GsohSOAjvQYoiBppXFqrQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@lit/reactive-element": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz", + "integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.4.tgz", + "integrity": "sha512-I+BSHzTAhKN2n7ZwGZsegGcZjDpLqFOMAtJz/u6uFGe0pUFbq56dEHjqJV/ZUdRJtNXNxA+hREUatZBvMR3Oiw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.4.tgz", + "integrity": "sha512-pu3BdjS2LtEzRu2elmGzS3fIeWSZy4BMDIaLNwjorO76+k2d0LMluijhsDx3KQyQBQ/lLUZCQA9/s6csvUfuhw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.4.tgz", + "integrity": "sha512-xfSrj9MHnWK9GaSqT9U0ImHtH/N8WZlHLx4cZHiuLcqs640hvZ3hLPd5UR2AZS57FaE8HrRUSpltbZdWRxHiDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.4.tgz", + "integrity": "sha512-bqU99PLJb/dqb3S0GIMdeuyAEETSUgZBoqXYd3Sd+WCsV+MmPhnN6JrotWyir31+QgH7EvvE5/mwGJlEoci8Fw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.4.tgz", + "integrity": "sha512-JinsFZ5G40oXQb+sUuiA5x689vhr6dDYK0H0NL+rwKdL6CqnmYN8PE4ZwfRSoIjrCxqTQG/SLfTtSvHeGxoVlw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.4.tgz", + "integrity": "sha512-GAdA4UxpiNm27cLHr2GqXBpAD0x9FqwYBY7/YSP0Ss0/PNi4k8gbviqpIpYbVSRBaS2ZcegXEzgTQMbRNCwxCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.4.tgz", + "integrity": "sha512-qDd6NoA1znaLjp4jR5U/KWCdLAKDJNB8W9ChbbDaKbo0xA+Atln5HK6LFCZ4oJQpemtRZA288DCirFRjrspptw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.4.tgz", + "integrity": "sha512-WtB5Tz5KTNINb8ZA+8sQ7bmjuS1JrRT7YverYIhUGdWWDlpzVWmIwuZE+jidkEXUn1l0zrEkaIMa8dHF3NGcsA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.4.tgz", + "integrity": "sha512-VcQ3L1tjnkKzWjryAVaFhHEWcqOfICX9uxVVoDzm2t0DpgKRHd2zOpVrJc0xsWeBZcBFyYROCIBdyR/fS174pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.4.tgz", + "integrity": "sha512-6+ZQX6P5s0cMDN2Ypb8Lbm2+/sZYmZjdaYny992ujUU9UKi/4CWoJWsl1pNvjWJHNHGK51m+jKGLlh1ylb2ifQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.4.tgz", + "integrity": "sha512-D72ZnvkFkBXOfzMMQLcwfPLyGkKb7HZ9/mf97B7v6/P5Lbv4oFOtSY/uHbS8lH6uKUOxoKiuokdb50XZSzzbJw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.4.tgz", + "integrity": "sha512-piU6BxeqA3O9KSu3kRCIQQtNqFFaTu21SEV4FwaRZowpnj3bLaWPZHw+xFqCs0XlJ+aOH3PTRWGoglH+mKA/OA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.4.tgz", + "integrity": "sha512-/5PGpHwqt2EEEOUs1XwzubE/ucr0dWDQ+to3zqi4Ds7EWpwtQ79wXc4JBoxqj/OwpawTsKWzJxHfSuBOq3DrWA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.4.tgz", + "integrity": "sha512-cX3beZDLWt7G2oJF+nhChiT+qtaihs+S2xi7ziGmVB+2pwPng6D0Ed0HmElQOgv2UsUmSJJLGwpBao/3TDx3VA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.4.tgz", + "integrity": "sha512-1uz2mGWHyptR7DgHHrlbdRAjXK7v7elGZ9lMja910/RP+ZYbX6xAmCiU9UZSX4hqmgtHMv6lr5l3kq1HIOpcag==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.4.tgz", + "integrity": "sha512-nLS8topojxyz7SRpKR2IODRpQ0XPZ+xaOXvT3+hqK/Uy8Lo5HFgkkIBiIrCu5tL5YqzTvgovGw55PwpahTAGig==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.4.tgz", + "integrity": "sha512-gs7DRKotr3l3q+jGPQBjH0ng1FjlEDm5ueQrkw5JtQvtLyEIcLASqAEaor56BhkKRzk+IcQzrcanBdb/bBQn8g==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.4.tgz", + "integrity": "sha512-791ET7W17NnScOZM7h4dX5hYspxE28htPFsb1awY/NRR8+PRNkS53e475rDdxXXDrP+kwnCcNWg9CX5ztn/Aqw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.4.tgz", + "integrity": "sha512-iwZQRcmj7g88g3tzefIrQY7qvmuA/cfYwhrDtTBhsmukO4U2huVO5W+86XacUMRvdSFVAc6kZUZy21JaRwiB9w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.4.tgz", + "integrity": "sha512-dVHFp9gRWrdTpnqQuGfCwd7hOQDatK1VCP2iWhLY/cGrOQs/ucFzJ6A5SRqbXX12ZDI8EUuejSM5kwg+ja7Png==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.4.tgz", + "integrity": "sha512-t3NlauOW6gxZVVFcBEnO62Cb4wbyDFL416gTg1uFI/2tgqYQlf69FbSE115Ajre9I+c26Lk4mcmdFUsS/DGifQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.4.tgz", + "integrity": "sha512-xWuIaSye5FWZF8+UYtVEcHtRJDN5kN9Kfgxx3Kq8XIov9KSKbc1fiqQCm90SKrgQbUXZelbnUhnlUJmfSE7P9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.4.tgz", + "integrity": "sha512-9ALJJUOg/ZflMJepVo2PlgsGxSaxN7SQ4Z8GoZfVlarWr6r3rkHUNsd/zAio7p4YMtChSMXPionxej4Hkf6CXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.4.tgz", + "integrity": "sha512-blj9z5qx/Pv4WU0W1NMFDB97e0JH5ed+aZGywW8WCvp/NhWX/4PFAq5uu6Q0AebNn+Vo6KzUYDT++JzTT5ojlQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.4.tgz", + "integrity": "sha512-Erx822VRBwLa124shbj+wNXe//BOgMEctDV0m1aqTQdNO1S69DgNUCFKC1RCeZfixs1J31l6igk1ziyXErbigQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lit": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.3.tgz", + "integrity": "sha512-fycuvZg/hkpozL00lm1pEJH5nN/lr9ZXd6mJI2HSN4+Bzc+LDNdEApJ6HFbPkdFNHLvOplIIuJvxkS4XUxqirw==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^2.1.0", + "lit-element": "^4.2.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-element": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz", + "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0", + "@lit/reactive-element": "^2.1.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-html": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.3.tgz", + "integrity": "sha512-el8M6jK2o3RXBnrSHX3ZKrsN8zEV63pSExTO1wYJz7QndGYZ8353e2a5PPX+qHe2aGayfnchQmkAojaWAREOIA==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.4.tgz", + "integrity": "sha512-4U0liVayNIoLp3GFl1FcI8561WepLnZ1rqfraGh7S9B3Ur5F9S283y8Futii7RUU2C/97tOBmBy7nYvhoiOpbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.4", + "@rollup/rollup-android-arm64": "4.63.4", + "@rollup/rollup-darwin-arm64": "4.63.4", + "@rollup/rollup-darwin-x64": "4.63.4", + "@rollup/rollup-freebsd-arm64": "4.63.4", + "@rollup/rollup-freebsd-x64": "4.63.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.4", + "@rollup/rollup-linux-arm-musleabihf": "4.63.4", + "@rollup/rollup-linux-arm64-gnu": "4.63.4", + "@rollup/rollup-linux-arm64-musl": "4.63.4", + "@rollup/rollup-linux-loong64-gnu": "4.63.4", + "@rollup/rollup-linux-loong64-musl": "4.63.4", + "@rollup/rollup-linux-ppc64-gnu": "4.63.4", + "@rollup/rollup-linux-ppc64-musl": "4.63.4", + "@rollup/rollup-linux-riscv64-gnu": "4.63.4", + "@rollup/rollup-linux-riscv64-musl": "4.63.4", + "@rollup/rollup-linux-s390x-gnu": "4.63.4", + "@rollup/rollup-linux-x64-gnu": "4.63.4", + "@rollup/rollup-linux-x64-musl": "4.63.4", + "@rollup/rollup-openbsd-x64": "4.63.4", + "@rollup/rollup-openharmony-arm64": "4.63.4", + "@rollup/rollup-win32-arm64-msvc": "4.63.4", + "@rollup/rollup-win32-ia32-msvc": "4.63.4", + "@rollup/rollup-win32-x64-gnu": "4.63.4", + "@rollup/rollup-win32-x64-msvc": "4.63.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..7e6dc9b --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,15 @@ +{ + "name": "nestquest-cards", + "private": true, + "type": "module", + "scripts": { + "build": "vite build" + }, + "dependencies": { + "lit": "^3.3.0" + }, + "devDependencies": { + "typescript": "^5.9.2", + "vite": "^7.1.5" + } +} diff --git a/frontend/src/cards/admin.ts b/frontend/src/cards/admin.ts new file mode 100644 index 0000000..b15993b --- /dev/null +++ b/frontend/src/cards/admin.ts @@ -0,0 +1,35 @@ +import { LitElement, html } from "lit"; + +import { registerCustomCard, type CardConfig, type HassLike } from "../types"; + +export class NestQuestAdminCard extends LitElement { + static properties = { + hass: { attribute: false }, + _config: { state: true }, + }; + + hass?: HassLike; + _config?: CardConfig; + + setConfig(config: CardConfig): void { + if (!config || typeof config !== "object") { + throw new Error("Invalid configuration"); + } + this._config = config; + } + + getCardSize(): number { + return 8; + } + + render() { + return html`
NestQuest Admin
`; + } +} + +customElements.define("nestquest-admin-card", NestQuestAdminCard); +registerCustomCard({ + type: "nestquest-admin-card", + name: "NestQuest Admin", + description: "Parent phone view for today, tasks, schedule, and history.", +}); diff --git a/frontend/src/cards/party-board.ts b/frontend/src/cards/party-board.ts new file mode 100644 index 0000000..d19bf8b --- /dev/null +++ b/frontend/src/cards/party-board.ts @@ -0,0 +1,35 @@ +import { LitElement, html } from "lit"; + +import { registerCustomCard, type CardConfig, type HassLike } from "../types"; + +export class NestQuestPartyBoardCard extends LitElement { + static properties = { + hass: { attribute: false }, + _config: { state: true }, + }; + + hass?: HassLike; + _config?: CardConfig; + + setConfig(config: CardConfig): void { + if (!config || typeof config !== "object") { + throw new Error("Invalid configuration"); + } + this._config = config; + } + + getCardSize(): number { + return 6; + } + + render() { + return html`
NestQuest Party Board
`; + } +} + +customElements.define("nestquest-party-board-card", NestQuestPartyBoardCard); +registerCustomCard({ + type: "nestquest-party-board-card", + name: "NestQuest Party Board", + description: "Kids' attract screen for present and away adventurers.", +}); diff --git a/frontend/src/cards/quest-log.ts b/frontend/src/cards/quest-log.ts new file mode 100644 index 0000000..d56a788 --- /dev/null +++ b/frontend/src/cards/quest-log.ts @@ -0,0 +1,35 @@ +import { LitElement, html } from "lit"; + +import { registerCustomCard, type CardConfig, type HassLike } from "../types"; + +export class NestQuestQuestLogCard extends LitElement { + static properties = { + hass: { attribute: false }, + _config: { state: true }, + }; + + hass?: HassLike; + _config?: CardConfig; + + setConfig(config: CardConfig): void { + if (!config || typeof config !== "object") { + throw new Error("Invalid configuration"); + } + this._config = config; + } + + getCardSize(): number { + return 8; + } + + render() { + return html`
NestQuest Quest Log
`; + } +} + +customElements.define("nestquest-quest-log-card", NestQuestQuestLogCard); +registerCustomCard({ + type: "nestquest-quest-log-card", + name: "NestQuest Quest Log", + description: "One child's daily quests, grouped by window.", +}); diff --git a/frontend/src/nestquest-cards.ts b/frontend/src/nestquest-cards.ts new file mode 100644 index 0000000..3d08e0e --- /dev/null +++ b/frontend/src/nestquest-cards.ts @@ -0,0 +1,3 @@ +import "./cards/party-board"; +import "./cards/quest-log"; +import "./cards/admin"; diff --git a/frontend/src/types.ts b/frontend/src/types.ts new file mode 100644 index 0000000..4854ed6 --- /dev/null +++ b/frontend/src/types.ts @@ -0,0 +1,20 @@ +export type HassLike = Record; + +export type CardConfig = Record; + +export type CustomCardInfo = { + type: string; + name: string; + description: string; +}; + +declare global { + interface Window { + customCards?: CustomCardInfo[]; + } +} + +export function registerCustomCard(info: CustomCardInfo): void { + window.customCards = window.customCards ?? []; + window.customCards.push(info); +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..8963332 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM"], + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "isolatedModules": true + }, + "include": ["src"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..1c3265e --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + build: { + outDir: "../custom_components/nestquest/www", + emptyOutDir: false, + sourcemap: false, + lib: { + entry: "src/nestquest-cards.ts", + formats: ["es"], + fileName: () => "nestquest-cards.js", + }, + rollupOptions: { + output: { + inlineDynamicImports: true, + }, + }, + }, +}); diff --git a/tests/conftest.py b/tests/conftest.py index e2dfd3d..6d6c9c4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -51,6 +51,8 @@ _HA_MODULES = ( "homeassistant.components", "homeassistant.components.sensor", "homeassistant.components.binary_sensor", + "homeassistant.components.http", + "homeassistant.components.frontend", ) def _find_spec(name: str): @@ -96,6 +98,8 @@ for _parent, _child in ( ("homeassistant", "components"), ("homeassistant.components", "sensor"), ("homeassistant.components", "binary_sensor"), + ("homeassistant.components", "http"), + ("homeassistant.components", "frontend"), ): setattr(sys.modules[_parent], _child, sys.modules[f"{_parent}.{_child}"]) @@ -429,6 +433,28 @@ _sensor_mock.SensorStateClass = SensorStateClass _binary_sensor_mock = _ha_mock("homeassistant.components.binary_sensor") _binary_sensor_mock.BinarySensorEntity = BinarySensorEntity + +@dataclass(frozen=True) +class StaticPathConfig: + """Stand-in mirroring homeassistant.components.http.StaticPathConfig.""" + + url_path: str + path: str + cache_headers: bool = True + + +def _add_extra_js_url(hass, url, es5=False): + """Stand-in mirroring homeassistant.components.frontend.add_extra_js_url.""" + urls = getattr(hass, "extra_js_urls", None) + if urls is None: + hass.extra_js_urls = [] + urls = hass.extra_js_urls + urls.append(url) + + +_ha_mock("homeassistant.components.http").StaticPathConfig = StaticPathConfig +_ha_mock("homeassistant.components.frontend").add_extra_js_url = _add_extra_js_url + import voluptuous as vol @@ -549,6 +575,16 @@ def _async_track_time_change(hass, action, hour=None, minute=None, second=None): _ha_mock("homeassistant.helpers.event").async_track_time_change = _async_track_time_change +class HttpRegistry: + """Models HA's ``hass.http`` static-path registration.""" + + def __init__(self): + self.static_paths: list[StaticPathConfig] = [] + + async def async_register_static_paths(self, configs): + self.static_paths.extend(list(configs)) + + class EventBus: """Models HA's ``hass.bus`` event bus. @@ -697,6 +733,8 @@ def make_hass() -> tuple: hass.time_change = TimeChangeRegistry(hass) hass.services = ServiceRegistry(hass) hass.bus = EventBus() + hass.http = HttpRegistry() + hass.extra_js_urls: list[str] = [] # A valid IANA time zone (the day-rollover listener reads it to # compute "today" in HA local time). hass.config.time_zone = "UTC" diff --git a/tests/test_frontend.py b/tests/test_frontend.py new file mode 100644 index 0000000..74e51a0 --- /dev/null +++ b/tests/test_frontend.py @@ -0,0 +1,93 @@ +"""Tests for the Lovelace card toolchain and auto-registered resource.""" +from __future__ import annotations + +import json +from pathlib import Path + +from conftest import wire_entry_to_registry + +from custom_components.nestquest import async_setup, async_setup_entry +from custom_components.nestquest.frontend import ( + BUNDLE_FILENAME, + BUNDLE_URL, + FRONTEND_URL_BASE, + WWW_DIR, + async_register_frontend, +) + +REPO_ROOT = Path(__file__).parent.parent +FRONTEND_DIR = REPO_ROOT / "frontend" +BUNDLE_PATH = WWW_DIR / BUNDLE_FILENAME +CARD_TYPES = ( + "nestquest-party-board-card", + "nestquest-quest-log-card", + "nestquest-admin-card", +) + + +def test_frontend_package_documents_vite_lit_build() -> None: + package = json.loads((FRONTEND_DIR / "package.json").read_text(encoding="utf-8")) + assert package["scripts"]["build"] == "vite build" + assert "lit" in package["dependencies"] + assert "typescript" in package["devDependencies"] + assert "vite" in package["devDependencies"] + + +def test_card_modules_exist_as_separate_sources() -> None: + src = FRONTEND_DIR / "src" + assert (src / "nestquest-cards.ts").is_file() + assert (src / "cards" / "party-board.ts").is_file() + assert (src / "cards" / "quest-log.ts").is_file() + assert (src / "cards" / "admin.ts").is_file() + entry = (src / "nestquest-cards.ts").read_text(encoding="utf-8") + assert "./cards/party-board" in entry + assert "./cards/quest-log" in entry + assert "./cards/admin" in entry + + +def test_card_sources_have_no_visual_editor() -> None: + for path in (FRONTEND_DIR / "src" / "cards").glob("*.ts"): + text = path.read_text(encoding="utf-8") + assert "getConfigElement" not in text + assert "setConfig" in text + assert "getCardSize" in text + + +def test_built_bundle_contains_all_three_cards() -> None: + assert BUNDLE_PATH.is_file() + bundle = BUNDLE_PATH.read_text(encoding="utf-8") + for card_type in CARD_TYPES: + assert card_type in bundle + + +async def test_setup_registers_static_path_and_extra_js(hass, make_entry) -> None: + entry = wire_entry_to_registry(make_entry(), hass.registry) + assert await async_setup_entry(hass, entry) is True + assert len(hass.http.static_paths) == 1 + registered = hass.http.static_paths[0] + assert registered.url_path == FRONTEND_URL_BASE + assert registered.path == str(WWW_DIR) + assert hass.extra_js_urls == [BUNDLE_URL] + + +async def test_async_setup_registers_frontend_without_a_config_entry(hass) -> None: + assert await async_setup(hass, {}) is True + assert hass.extra_js_urls == [BUNDLE_URL] + assert hass.http.static_paths[0].url_path == FRONTEND_URL_BASE + + +async def test_frontend_registration_is_idempotent(hass, make_entry) -> None: + await async_register_frontend(hass) + await async_register_frontend(hass) + entry_a = wire_entry_to_registry(make_entry(entry_id="a"), hass.registry) + entry_b = wire_entry_to_registry(make_entry(entry_id="b"), hass.registry) + assert await async_setup_entry(hass, entry_a) is True + assert await async_setup_entry(hass, entry_b) is True + assert len(hass.http.static_paths) == 1 + assert hass.extra_js_urls == [BUNDLE_URL] + + +def test_readme_documents_npm_build() -> None: + readme = (REPO_ROOT / "README.md").read_text(encoding="utf-8") + assert "npm run build" in readme + assert "nestquest-cards.js" in readme