Compare commits

..
Author SHA1 Message Date
overseerandClaude Opus 5.5 1bddc9fde9 Docs: namespaced icon keys, picker sources, and third-party notices
- ADMIN-SPEC §3.3: picker sources (curated Lucide, curated Font Awesome
  Free solid subset, emoji input), stored lucide:/fa:/emoji: keys, legacy
  bare-name handling, and fallback glyphs.
- PANEL-SPEC §3.2/§3.3: open-card tile resolves the keys (Lucide stroked,
  Font Awesome filled, emoji as text) with the star fallback; the sealed
  card keeps its static star.
- DESIGN-HANDOFF: mark the icon-subset open question resolved (dated).
- ENTITIES-AND-SERVICES: payload example uses a namespaced icon key.
- THIRD-PARTY-NOTICES.md: Font Awesome Free (icons CC BY 4.0, fonts
  SIL OFL 1.1, code MIT) and Lucide (ISC), generated from
  tools/icons/curated-icons.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 08:27:14 -04:00
dev_agent 3bd30a0b9f Kids' panel: render the definition icon on the open quest card (#245) 2026-09-26 08:23:09 -04:00
dev_agent a93f91d4af Admin PWA: Lucide + Font Awesome + emoji icon picker (#243) 2026-09-26 08:15:13 -04:00
9 changed files with 328 additions and 134 deletions
+31
View File
@@ -0,0 +1,31 @@
# Third-party notices
NestQuest bundles glyph data from the icon sets below. The curated names are listed in
`tools/icons/curated-icons.json`; `tools/icons/generate.mjs` reads each glyph's SVG path
data from the locally installed packages and generates it into both bundles
(`admin/src/icons/registry.generated.ts` and `frontend/src/icons/registry.generated.ts`).
Nothing is loaded from a CDN or fetched at runtime. The package versions used are recorded
in the header of each generated file.
## Font Awesome Free
- Project: <https://fontawesome.com> · repository: <https://github.com/FortAwesome/Font-Awesome>
- Package: `@fortawesome/free-solid-svg-icons`
- Licence: <https://fontawesome.com/license/free>
- Icons: [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)
- Fonts: [SIL OFL 1.1](https://openfontlicense.org)
- Code: [MIT](https://opensource.org/licenses/MIT)
- Use in NestQuest: a curated subset of the Free **solid** icon set, embedded as SVG path
data. No fonts are bundled.
Font Awesome Free by @fontawesome — https://fontawesome.com — icons licensed under CC BY 4.0.
## Lucide
- Project: <https://lucide.dev> · repository: <https://github.com/lucide-icons/lucide>
- Package: `lucide-react`
- Licence: [ISC](https://github.com/lucide-icons/lucide/blob/main/LICENSE)
- Use in NestQuest: a curated subset of Lucide icons, embedded as SVG path data.
Lucide icons are Copyright (c) Lucide Contributors, licensed under the ISC License.
Portions derived from Feather are Copyright (c) Cole Bemis, licensed under the MIT License.
+122 -122
View File
@@ -1,5 +1,5 @@
const F = globalThis, se = F.ShadowRoot && (F.ShadyCSS === void 0 || F.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype, ie = /* @__PURE__ */ Symbol(), ce = /* @__PURE__ */ new WeakMap();
let Ie = class {
let Pe = class {
constructor(e, t, n) {
if (this._$cssResult$ = !0, n !== ie) throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");
this.cssText = e, this.t = t;
@@ -17,14 +17,14 @@ let Ie = class {
return this.cssText;
}
};
const v = (s) => new Ie(typeof s == "string" ? s : s + "", void 0, ie), He = (s, ...e) => {
const v = (s) => new Pe(typeof s == "string" ? s : s + "", void 0, ie), Ie = (s, ...e) => {
const t = s.length === 1 ? s[0] : e.reduce((n, i, a) => n + ((r) => {
if (r._$cssResult$ === !0) return r.cssText;
if (typeof r == "number") return r;
throw Error("Value passed to 'css' function must be a 'css' function result: " + r + ". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.");
})(i) + s[a + 1], s[0]);
return new Ie(t, s, ie);
}, Ge = (s, e) => {
return new Pe(t, s, ie);
}, We = (s, e) => {
if (se) s.adoptedStyleSheets = e.map((t) => t instanceof CSSStyleSheet ? t : t.styleSheet);
else for (const t of e) {
const n = document.createElement("style"), i = F.litNonce;
@@ -35,10 +35,10 @@ const v = (s) => new Ie(typeof s == "string" ? s : s + "", void 0, ie), He = (s,
for (const n of e.cssRules) t += n.cssText;
return v(t);
})(s) : s;
const { is: Ke, defineProperty: Je, getOwnPropertyDescriptor: Xe, getOwnPropertyNames: Ye, getOwnPropertySymbols: et, getPrototypeOf: tt } = Object, Z = globalThis, pe = Z.trustedTypes, nt = pe ? pe.emptyScript : "", st = Z.reactiveElementPolyfillSupport, P = (s, e) => s, ne = { toAttribute(s, e) {
const { is: Ge, defineProperty: Ke, getOwnPropertyDescriptor: Je, getOwnPropertyNames: Xe, getOwnPropertySymbols: Ye, getPrototypeOf: et } = Object, Z = globalThis, pe = Z.trustedTypes, tt = pe ? pe.emptyScript : "", nt = Z.reactiveElementPolyfillSupport, P = (s, e) => s, ne = { toAttribute(s, e) {
switch (e) {
case Boolean:
s = s ? nt : null;
s = s ? tt : null;
break;
case Object:
case Array:
@@ -63,7 +63,7 @@ const { is: Ke, defineProperty: Je, getOwnPropertyDescriptor: Xe, getOwnProperty
}
}
return t;
} }, je = (s, e) => !Ke(s, e), he = { attribute: !0, type: String, converter: ne, reflect: !1, useDefault: !1, hasChanged: je };
} }, He = (s, e) => !Ge(s, e), he = { attribute: !0, type: String, converter: ne, reflect: !1, useDefault: !1, hasChanged: He };
Symbol.metadata ??= /* @__PURE__ */ Symbol("metadata"), Z.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap();
let T = class extends HTMLElement {
static addInitializer(e) {
@@ -75,11 +75,11 @@ let T = class extends HTMLElement {
static createProperty(e, t = he) {
if (t.state && (t.attribute = !1), this._$Ei(), this.prototype.hasOwnProperty(e) && ((t = Object.create(t)).wrapped = !0), this.elementProperties.set(e, t), !t.noAccessor) {
const n = /* @__PURE__ */ Symbol(), i = this.getPropertyDescriptor(e, n, t);
i !== void 0 && Je(this.prototype, e, i);
i !== void 0 && Ke(this.prototype, e, i);
}
}
static getPropertyDescriptor(e, t, n) {
const { get: i, set: a } = Xe(this.prototype, e) ?? { get() {
const { get: i, set: a } = Je(this.prototype, e) ?? { get() {
return this[t];
}, set(r) {
this[t] = r;
@@ -94,13 +94,13 @@ let T = class extends HTMLElement {
}
static _$Ei() {
if (this.hasOwnProperty(P("elementProperties"))) return;
const e = tt(this);
const e = et(this);
e.finalize(), e.l !== void 0 && (this.l = [...e.l]), this.elementProperties = new Map(e.elementProperties);
}
static finalize() {
if (this.hasOwnProperty(P("finalized"))) return;
if (this.finalized = !0, this._$Ei(), this.hasOwnProperty(P("properties"))) {
const t = this.properties, n = [...Ye(t), ...et(t)];
const t = this.properties, n = [...Xe(t), ...Ye(t)];
for (const i of n) this.createProperty(i, t[i]);
}
const e = this[Symbol.metadata];
@@ -146,7 +146,7 @@ let T = class extends HTMLElement {
}
createRenderRoot() {
const e = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);
return Ge(e, this.constructor.elementStyles), e;
return We(e, this.constructor.elementStyles), e;
}
connectedCallback() {
this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(!0), this._$EO?.forEach((e) => e.hostConnected?.());
@@ -178,7 +178,7 @@ let T = class extends HTMLElement {
requestUpdate(e, t, n, i = !1, a) {
if (e !== void 0) {
const r = this.constructor;
if (i === !1 && (a = this[e]), n ??= r.getPropertyOptions(e), !((n.hasChanged ?? je)(a, t) || n.useDefault && n.reflect && a === this._$Ej?.get(e) && !this.hasAttribute(r._$Eu(e, n)))) return;
if (i === !1 && (a = this[e]), n ??= r.getPropertyOptions(e), !((n.hasChanged ?? He)(a, t) || n.useDefault && n.reflect && a === this._$Ej?.get(e) && !this.hasAttribute(r._$Eu(e, n)))) return;
this.C(e, t, n);
}
this.isUpdatePending === !1 && (this._$ES = this._$EP());
@@ -246,43 +246,43 @@ let T = class extends HTMLElement {
firstUpdated(e) {
}
};
T.elementStyles = [], T.shadowRootOptions = { mode: "open" }, T[P("elementProperties")] = /* @__PURE__ */ new Map(), T[P("finalized")] = /* @__PURE__ */ new Map(), st?.({ ReactiveElement: T }), (Z.reactiveElementVersions ??= []).push("2.1.2");
const ae = globalThis, ue = (s) => s, Q = ae.trustedTypes, fe = Q ? Q.createPolicy("lit-html", { createHTML: (s) => s }) : void 0, Re = "$lit$", k = `lit$${Math.random().toFixed(9).slice(2)}$`, Ue = "?" + k, it = `<${Ue}>`, A = document, I = () => A.createComment(""), H = (s) => s === null || typeof s != "object" && typeof s != "function", re = Array.isArray, at = (s) => re(s) || typeof s?.[Symbol.iterator] == "function", K = `[
T.elementStyles = [], T.shadowRootOptions = { mode: "open" }, T[P("elementProperties")] = /* @__PURE__ */ new Map(), T[P("finalized")] = /* @__PURE__ */ new Map(), nt?.({ ReactiveElement: T }), (Z.reactiveElementVersions ??= []).push("2.1.2");
const ae = globalThis, ue = (s) => s, Q = ae.trustedTypes, fe = Q ? Q.createPolicy("lit-html", { createHTML: (s) => s }) : void 0, je = "$lit$", k = `lit$${Math.random().toFixed(9).slice(2)}$`, Re = "?" + k, st = `<${Re}>`, A = document, I = () => A.createComment(""), H = (s) => s === null || typeof s != "object" && typeof s != "function", re = Array.isArray, it = (s) => re(s) || typeof s?.[Symbol.iterator] == "function", K = `[
\f\r]`, L = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, me = /-->/g, ge = />/g, $ = RegExp(`>|${K}(?:([^\\s"'>=/]+)(${K}*=${K}*(?:[^
\f\r"'\`<>=]|("|')|))|$)`, "g"), _e = /'/g, ye = /"/g, De = /^(?:script|style|textarea|title)$/i, Fe = (s) => (e, ...t) => ({ _$litType$: s, strings: e, values: t }), d = Fe(1), be = Fe(2), S = /* @__PURE__ */ Symbol.for("lit-noChange"), g = /* @__PURE__ */ Symbol.for("lit-nothing"), ve = /* @__PURE__ */ new WeakMap(), C = A.createTreeWalker(A, 129);
function Qe(s, e) {
\f\r"'\`<>=]|("|')|))|$)`, "g"), _e = /'/g, ye = /"/g, Ue = /^(?:script|style|textarea|title)$/i, De = (s) => (e, ...t) => ({ _$litType$: s, strings: e, values: t }), d = De(1), be = De(2), S = /* @__PURE__ */ Symbol.for("lit-noChange"), g = /* @__PURE__ */ Symbol.for("lit-nothing"), ve = /* @__PURE__ */ new WeakMap(), C = A.createTreeWalker(A, 129);
function Fe(s, e) {
if (!re(s) || !s.hasOwnProperty("raw")) throw Error("invalid template strings array");
return fe !== void 0 ? fe.createHTML(e) : e;
}
const rt = (s, e) => {
const at = (s, e) => {
const t = s.length - 1, n = [];
let i, a = e === 2 ? "<svg>" : e === 3 ? "<math>" : "", r = L;
for (let l = 0; l < t; l++) {
const o = s[l];
let p, u, c = -1, f = 0;
for (; f < o.length && (r.lastIndex = f, u = r.exec(o), u !== null); ) f = r.lastIndex, r === L ? u[1] === "!--" ? r = me : u[1] !== void 0 ? r = ge : u[2] !== void 0 ? (De.test(u[2]) && (i = RegExp("</" + u[2], "g")), r = $) : u[3] !== void 0 && (r = $) : r === $ ? u[0] === ">" ? (r = i ?? L, c = -1) : u[1] === void 0 ? c = -2 : (c = r.lastIndex - u[2].length, p = u[1], r = u[3] === void 0 ? $ : u[3] === '"' ? ye : _e) : r === ye || r === _e ? r = $ : r === me || r === ge ? r = L : (r = $, i = void 0);
for (; f < o.length && (r.lastIndex = f, u = r.exec(o), u !== null); ) f = r.lastIndex, r === L ? u[1] === "!--" ? r = me : u[1] !== void 0 ? r = ge : u[2] !== void 0 ? (Ue.test(u[2]) && (i = RegExp("</" + u[2], "g")), r = $) : u[3] !== void 0 && (r = $) : r === $ ? u[0] === ">" ? (r = i ?? L, c = -1) : u[1] === void 0 ? c = -2 : (c = r.lastIndex - u[2].length, p = u[1], r = u[3] === void 0 ? $ : u[3] === '"' ? ye : _e) : r === ye || r === _e ? r = $ : r === me || r === ge ? r = L : (r = $, i = void 0);
const h = r === $ && s[l + 1].startsWith("/>") ? " " : "";
a += r === L ? o + it : c >= 0 ? (n.push(p), o.slice(0, c) + Re + o.slice(c) + k + h) : o + k + (c === -2 ? l : h);
a += r === L ? o + st : c >= 0 ? (n.push(p), o.slice(0, c) + je + o.slice(c) + k + h) : o + k + (c === -2 ? l : h);
}
return [Qe(s, a + (s[t] || "<?>") + (e === 2 ? "</svg>" : e === 3 ? "</math>" : "")), n];
return [Fe(s, a + (s[t] || "<?>") + (e === 2 ? "</svg>" : e === 3 ? "</math>" : "")), n];
};
class j {
constructor({ strings: e, _$litType$: t }, n) {
let i;
this.parts = [];
let a = 0, r = 0;
const l = e.length - 1, o = this.parts, [p, u] = rt(e, t);
const l = e.length - 1, o = this.parts, [p, u] = at(e, t);
if (this.el = j.createElement(p, n), C.currentNode = this.el.content, t === 2 || t === 3) {
const c = this.el.content.firstChild;
c.replaceWith(...c.childNodes);
}
for (; (i = C.nextNode()) !== null && o.length < l; ) {
if (i.nodeType === 1) {
if (i.hasAttributes()) for (const c of i.getAttributeNames()) if (c.endsWith(Re)) {
if (i.hasAttributes()) for (const c of i.getAttributeNames()) if (c.endsWith(je)) {
const f = u[r++], h = i.getAttribute(c).split(k), m = /([.?@])?(.*)/.exec(f);
o.push({ type: 1, index: a, name: m[2], strings: h, ctor: m[1] === "." ? lt : m[1] === "?" ? ct : m[1] === "@" ? dt : W }), i.removeAttribute(c);
o.push({ type: 1, index: a, name: m[2], strings: h, ctor: m[1] === "." ? ot : m[1] === "?" ? lt : m[1] === "@" ? ct : W }), i.removeAttribute(c);
} else c.startsWith(k) && (o.push({ type: 6, index: a }), i.removeAttribute(c));
if (De.test(i.tagName)) {
if (Ue.test(i.tagName)) {
const c = i.textContent.split(k), f = c.length - 1;
if (f > 0) {
i.textContent = Q ? Q.emptyScript : "";
@@ -290,7 +290,7 @@ class j {
i.append(c[f], I());
}
}
} else if (i.nodeType === 8) if (i.data === Ue) o.push({ type: 2, index: a });
} else if (i.nodeType === 8) if (i.data === Re) o.push({ type: 2, index: a });
else {
let c = -1;
for (; (c = i.data.indexOf(k, c + 1)) !== -1; ) o.push({ type: 7, index: a }), c += k.length - 1;
@@ -309,7 +309,7 @@ function z(s, e, t = s, n) {
const a = H(e) ? void 0 : e._$litDirective$;
return i?.constructor !== a && (i?._$AO?.(!1), a === void 0 ? i = void 0 : (i = new a(s), i._$AT(s, t, n)), n !== void 0 ? (t._$Co ??= [])[n] = i : t._$Cl = i), i !== void 0 && (e = z(s, i._$AS(s, e.values), i, n)), e;
}
class ot {
class rt {
constructor(e, t) {
this._$AV = [], this._$AN = void 0, this._$AD = e, this._$AM = t;
}
@@ -326,7 +326,7 @@ class ot {
for (; o !== void 0; ) {
if (r === o.index) {
let p;
o.type === 2 ? p = new N(a, a.nextSibling, this, e) : o.type === 1 ? p = new o.ctor(a, o.name, o.strings, this, e) : o.type === 6 && (p = new pt(a, this, e)), this._$AV.push(p), o = n[++l];
o.type === 2 ? p = new N(a, a.nextSibling, this, e) : o.type === 1 ? p = new o.ctor(a, o.name, o.strings, this, e) : o.type === 6 && (p = new dt(a, this, e)), this._$AV.push(p), o = n[++l];
}
r !== o?.index && (a = C.nextNode(), r++);
}
@@ -356,7 +356,7 @@ class N {
return this._$AB;
}
_$AI(e, t = this) {
e = z(this, e, t), H(e) ? e === g || e == null || e === "" ? (this._$AH !== g && this._$AR(), this._$AH = g) : e !== this._$AH && e !== S && this._(e) : e._$litType$ !== void 0 ? this.$(e) : e.nodeType !== void 0 ? this.T(e) : at(e) ? this.k(e) : this._(e);
e = z(this, e, t), H(e) ? e === g || e == null || e === "" ? (this._$AH !== g && this._$AR(), this._$AH = g) : e !== this._$AH && e !== S && this._(e) : e._$litType$ !== void 0 ? this.$(e) : e.nodeType !== void 0 ? this.T(e) : it(e) ? this.k(e) : this._(e);
}
O(e) {
return this._$AA.parentNode.insertBefore(e, this._$AB);
@@ -368,10 +368,10 @@ class N {
this._$AH !== g && H(this._$AH) ? this._$AA.nextSibling.data = e : this.T(A.createTextNode(e)), this._$AH = e;
}
$(e) {
const { values: t, _$litType$: n } = e, i = typeof n == "number" ? this._$AC(e) : (n.el === void 0 && (n.el = j.createElement(Qe(n.h, n.h[0]), this.options)), n);
const { values: t, _$litType$: n } = e, i = typeof n == "number" ? this._$AC(e) : (n.el === void 0 && (n.el = j.createElement(Fe(n.h, n.h[0]), this.options)), n);
if (this._$AH?._$AD === i) this._$AH.p(t);
else {
const a = new ot(i, this), r = a.u(this.options);
const a = new rt(i, this), r = a.u(this.options);
a.p(t), this.T(r), this._$AH = a;
}
}
@@ -421,7 +421,7 @@ class W {
e === g ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, e ?? "");
}
}
class lt extends W {
class ot extends W {
constructor() {
super(...arguments), this.type = 3;
}
@@ -429,7 +429,7 @@ class lt extends W {
this.element[this.name] = e === g ? void 0 : e;
}
}
class ct extends W {
class lt extends W {
constructor() {
super(...arguments), this.type = 4;
}
@@ -437,7 +437,7 @@ class ct extends W {
this.element.toggleAttribute(this.name, !!e && e !== g);
}
}
class dt extends W {
class ct extends W {
constructor(e, t, n, i, a) {
super(e, t, n, i, a), this.type = 5;
}
@@ -450,7 +450,7 @@ class dt extends W {
typeof this._$AH == "function" ? this._$AH.call(this.options?.host ?? this.element, e) : this._$AH.handleEvent(e);
}
}
class pt {
class dt {
constructor(e, t, n) {
this.element = e, this.type = 6, this._$AN = void 0, this._$AM = t, this.options = n;
}
@@ -461,9 +461,9 @@ class pt {
z(this, e);
}
}
const ht = { I: N }, ut = ae.litHtmlPolyfillSupport;
ut?.(j, N), (ae.litHtmlVersions ??= []).push("3.3.3");
const ft = (s, e, t) => {
const pt = { I: N }, ht = ae.litHtmlPolyfillSupport;
ht?.(j, N), (ae.litHtmlVersions ??= []).push("3.3.3");
const ut = (s, e, t) => {
const n = t?.renderBefore ?? e;
let i = n._$litPart$;
if (i === void 0) {
@@ -483,7 +483,7 @@ let M = class extends T {
}
update(e) {
const t = this.render();
this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(e), this._$Do = ft(t, this.renderRoot, this.renderOptions);
this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(e), this._$Do = ut(t, this.renderRoot, this.renderOptions);
}
connectedCallback() {
super.connectedCallback(), this._$Do?.setConnected(!0);
@@ -496,14 +496,14 @@ let M = class extends T {
}
};
M._$litElement$ = !0, M.finalized = !0, oe.litElementHydrateSupport?.({ LitElement: M });
const mt = oe.litElementPolyfillSupport;
mt?.({ LitElement: M });
const ft = oe.litElementPolyfillSupport;
ft?.({ LitElement: M });
(oe.litElementVersions ??= []).push("4.2.2");
const Ve = ':host{--nq-p-parchment-top: #f0e4c7;--nq-p-parchment-bottom: #ddcca3;--nq-p-parchment: radial-gradient(ellipse at 25% 10%, rgba(255,255,255,.5), transparent 55%), radial-gradient(ellipse at 85% 90%, rgba(120,86,44,.28), transparent 60%), repeating-linear-gradient(93deg, rgba(150,115,70,.05) 0 2px, transparent 2px 6px), repeating-linear-gradient(2deg, rgba(150,115,70,.04) 0 3px, transparent 3px 7px), linear-gradient(var(--nq-p-parchment-top), var(--nq-p-parchment-bottom));--nq-p-vignette: inset 0 0 200px rgba(80,52,20,.35);--nq-p-card-open: linear-gradient(#fdf7e6, #f2e7c9);--nq-p-card-done: linear-gradient(#f0e8d3, #e6dcc2);--nq-p-card-panel: linear-gradient(#fcf6e6, #f1e6ca);--nq-p-card-border: rgba(120,88,48,.38);--nq-p-panel-border: rgba(92,62,26,.4);--nq-p-card-shadow: 0 3px 0 rgba(120,88,48,.2), inset 0 1px 0 rgba(255,255,255,.7);--nq-p-panel-shadow: 0 6px 0 rgba(92,62,26,.18), inset 0 2px 0 rgba(255,255,255,.7);--nq-p-frame-outer: 2px solid rgba(92,62,26,.45);--nq-p-frame-inner: 1px solid rgba(92,62,26,.28);--nq-p-rule: 2px solid rgba(92,62,26,.35);--nq-p-ink: #2b1f14;--nq-p-ink-secondary: #5c452a;--nq-p-ink-muted: #6f6455;--nq-p-ink-away: #4d4433;--nq-p-ink-late: #8f1526;--nq-brand-blue: #3B3AB8;--nq-brand-purple: #A035CC;--nq-brand-gradient: linear-gradient(135deg, var(--nq-brand-blue), var(--nq-brand-purple));--nq-p-icon-tile: var(--nq-brand-gradient);--nq-p-icon-tile-done: rgba(92,62,26,.16);--nq-p-crest: var(--nq-brand-gradient);--nq-p-crest-away: linear-gradient(135deg, #6b6b7a, #7a7286);--nq-p-seal: radial-gradient(circle at 35% 30%, #a8283a, #6d1322);--nq-p-seal-shadow: 0 4px 10px rgba(60,10,20,.4), inset 0 0 0 4px rgba(255,255,255,.14);--nq-p-seal-size: 78px;--nq-p-dock-bg: rgba(43,31,20,.9);--nq-p-dock-ink: #f7efdb;--nq-p-dock-ink-secondary: #d8c9a6;--nq-p-dock-divider: rgba(233,220,189,.3);--nq-p-dock-height: 84px;--nq-p-font-display: "Cinzel Decorative", Cinzel, serif;--nq-p-font-heading: Cinzel, serif;--nq-p-font-body: Nunito, system-ui, sans-serif;--nq-p-size-hero: 86px;--nq-p-size-wordmark: 66px;--nq-p-size-title: 56px;--nq-p-size-name: 48px;--nq-p-size-section: 32px;--nq-p-size-quest: 30px;--nq-p-size-body: 22px;--nq-p-size-label: 21px;--nq-p-track-label: .14em;--nq-p-track-kicker: .3em;--nq-p-quest-min-height: 116px;--nq-p-quest-gap: 24px;--nq-p-quest-pad: 22px;--nq-p-tile-size: 64px;--nq-p-button-height: 72px;--nq-p-button-min-width: 150px;--nq-p-confirm-button: 108px;--nq-p-radius-card: 12px;--nq-p-radius-panel: 20px;--nq-p-radius-pill: 9999px;--nq-p-page-inset: 62px;--nq-p-frame-inset: 26px;--nq-ease-out: cubic-bezier(0, 0, .2, 1);--nq-dur-micro: .12s;--nq-dur-base: .2s;--nq-dur-modal: .35s}';
const Qe = ':host{--nq-p-parchment-top: #f0e4c7;--nq-p-parchment-bottom: #ddcca3;--nq-p-parchment: radial-gradient(ellipse at 25% 10%, rgba(255,255,255,.5), transparent 55%), radial-gradient(ellipse at 85% 90%, rgba(120,86,44,.28), transparent 60%), repeating-linear-gradient(93deg, rgba(150,115,70,.05) 0 2px, transparent 2px 6px), repeating-linear-gradient(2deg, rgba(150,115,70,.04) 0 3px, transparent 3px 7px), linear-gradient(var(--nq-p-parchment-top), var(--nq-p-parchment-bottom));--nq-p-vignette: inset 0 0 200px rgba(80,52,20,.35);--nq-p-card-open: linear-gradient(#fdf7e6, #f2e7c9);--nq-p-card-done: linear-gradient(#f0e8d3, #e6dcc2);--nq-p-card-panel: linear-gradient(#fcf6e6, #f1e6ca);--nq-p-card-border: rgba(120,88,48,.38);--nq-p-panel-border: rgba(92,62,26,.4);--nq-p-card-shadow: 0 3px 0 rgba(120,88,48,.2), inset 0 1px 0 rgba(255,255,255,.7);--nq-p-panel-shadow: 0 6px 0 rgba(92,62,26,.18), inset 0 2px 0 rgba(255,255,255,.7);--nq-p-frame-outer: 2px solid rgba(92,62,26,.45);--nq-p-frame-inner: 1px solid rgba(92,62,26,.28);--nq-p-rule: 2px solid rgba(92,62,26,.35);--nq-p-ink: #2b1f14;--nq-p-ink-secondary: #5c452a;--nq-p-ink-muted: #6f6455;--nq-p-ink-away: #4d4433;--nq-p-ink-late: #8f1526;--nq-brand-blue: #3B3AB8;--nq-brand-purple: #A035CC;--nq-brand-gradient: linear-gradient(135deg, var(--nq-brand-blue), var(--nq-brand-purple));--nq-p-icon-tile: var(--nq-brand-gradient);--nq-p-icon-tile-done: rgba(92,62,26,.16);--nq-p-crest: var(--nq-brand-gradient);--nq-p-crest-away: linear-gradient(135deg, #6b6b7a, #7a7286);--nq-p-seal: radial-gradient(circle at 35% 30%, #a8283a, #6d1322);--nq-p-seal-shadow: 0 4px 10px rgba(60,10,20,.4), inset 0 0 0 4px rgba(255,255,255,.14);--nq-p-seal-size: 78px;--nq-p-dock-bg: rgba(43,31,20,.9);--nq-p-dock-ink: #f7efdb;--nq-p-dock-ink-secondary: #d8c9a6;--nq-p-dock-divider: rgba(233,220,189,.3);--nq-p-dock-height: 84px;--nq-p-font-display: "Cinzel Decorative", Cinzel, serif;--nq-p-font-heading: Cinzel, serif;--nq-p-font-body: Nunito, system-ui, sans-serif;--nq-p-size-hero: 86px;--nq-p-size-wordmark: 66px;--nq-p-size-title: 56px;--nq-p-size-name: 48px;--nq-p-size-section: 32px;--nq-p-size-quest: 30px;--nq-p-size-body: 22px;--nq-p-size-label: 21px;--nq-p-track-label: .14em;--nq-p-track-kicker: .3em;--nq-p-quest-min-height: 116px;--nq-p-quest-gap: 24px;--nq-p-quest-pad: 22px;--nq-p-tile-size: 64px;--nq-p-button-height: 72px;--nq-p-button-min-width: 150px;--nq-p-confirm-button: 108px;--nq-p-radius-card: 12px;--nq-p-radius-panel: 20px;--nq-p-radius-pill: 9999px;--nq-p-page-inset: 62px;--nq-p-frame-inset: 26px;--nq-ease-out: cubic-bezier(0, 0, .2, 1);--nq-dur-micro: .12s;--nq-dur-base: .2s;--nq-dur-modal: .35s}';
function le(s) {
window.customCards = window.customCards ?? [], window.customCards.push(s);
}
const gt = [
const mt = [
"nestquest_quest_completed",
"nestquest_quest_uncompleted",
"nestquest_quest_missed",
@@ -524,7 +524,7 @@ function V(s, e) {
}
return xe.set(t, i), i;
}
function _t(s) {
function gt(s) {
const e = s?.config;
if (!e || typeof e != "object")
return;
@@ -534,14 +534,14 @@ function _t(s) {
function J(s, e) {
return `${V(e, { weekday: "long" }).format(s)}, ${V(e, { month: "short", day: "numeric" }).format(s)}`;
}
function yt(s, e) {
function _t(s, e) {
return V(e, {
hour: "numeric",
minute: "2-digit",
hour12: !0
}).format(s);
}
const bt = {
const yt = {
"clear-night": "Clear",
cloudy: "Cloudy",
exceptional: "Clear",
@@ -557,7 +557,7 @@ const bt = {
sunny: "Sunny",
windy: "Windy",
"windy-variant": "Windy"
}, vt = {
}, bt = {
"clear-night": "Clear night skies",
cloudy: "Grey skies today",
exceptional: "A striking day",
@@ -583,7 +583,7 @@ function X(s, e) {
const t = typeof s == "number" ? s : Number(s);
return Number.isFinite(t) ? t : e;
}
function xt(s) {
function vt(s) {
if (s === null || typeof s != "object" || Array.isArray(s))
return "";
const e = s.slug;
@@ -595,10 +595,10 @@ function ke(s) {
function $e(s) {
return Math.max(0, Math.round(s));
}
function wt(s) {
function xt(s) {
return s.split(/[-_]+/).filter(Boolean).map((e) => e.charAt(0).toUpperCase() + e.slice(1)).join(" ");
}
function kt(s, e) {
function wt(s, e) {
const t = V(e, {
year: "numeric",
month: "2-digit",
@@ -620,7 +620,7 @@ function kt(s, e) {
);
return Number.isFinite(i) ? i - s : 0;
}
function $t(s, e) {
function kt(s, e) {
if (typeof s != "string")
return null;
const t = s.split("-");
@@ -633,17 +633,17 @@ function $t(s, e) {
const o = new Date(n, i - 1, a);
return Number.isNaN(o.getTime()) ? null : o;
}
const r = Date.UTC(n, i - 1, a), l = new Date(r - kt(r, e));
const r = Date.UTC(n, i - 1, a), l = new Date(r - wt(r, e));
return Number.isNaN(l.getTime()) ? null : l;
}
function qt(s) {
const e = bt[s];
function $t(s) {
const e = yt[s];
return e || s.charAt(0).toUpperCase() + s.slice(1);
}
function Ct(s, e) {
return vt[s] ?? e;
function qt(s, e) {
return bt[s] ?? e;
}
const qe = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Mt = "polygon(50% 0, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%)", At = He`
const qe = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Ct = "polygon(50% 0, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%)", Mt = Ie`
* {
box-sizing: border-box;
}
@@ -715,7 +715,7 @@ const qe = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Mt = "polygon(50%
justify-content: center;
width: 34px;
height: 34px;
clip-path: ${v(Mt)};
clip-path: ${v(Ct)};
background: var(--nq-brand-gradient);
}
@@ -1038,7 +1038,7 @@ const qe = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Mt = "polygon(50%
font-weight: 800;
line-height: 1;
}
`, St = d`<svg
`, At = d`<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@@ -1049,7 +1049,7 @@ const qe = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Mt = "polygon(50%
>
<circle cx="12" cy="12" r="10"></circle>
<path d="m9 12 2 2 4-4"></path>
</svg>`, Tt = d`<svg
</svg>`, St = d`<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@@ -1060,7 +1060,7 @@ const qe = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Mt = "polygon(50%
>
<path d="M3.5 21 12 3.5 20.5 21"></path>
<path d="M9 21l3-8 3 8"></path>
</svg>`, zt = d`<svg
</svg>`, Tt = d`<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@@ -1071,7 +1071,7 @@ const qe = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Mt = "polygon(50%
>
<circle cx="12" cy="12" r="10"></circle>
<polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon>
</svg>`, Et = d`<svg
</svg>`, zt = d`<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@@ -1083,7 +1083,7 @@ const qe = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Mt = "polygon(50%
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"></path>
<path d="M12 9v4"></path>
<path d="M12 17h.01"></path>
</svg>`, Nt = d`<svg
</svg>`, Et = d`<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@@ -1095,14 +1095,14 @@ const qe = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Mt = "polygon(50%
<path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z"></path>
<path d="m2 2 20 20"></path>
</svg>`, Ce = {
icon: Et,
icon: zt,
headline: "NestQuest is not set up yet",
body: "A parent needs to finish setting up NestQuest before the party can gather."
}, Lt = {
icon: Nt,
}, Nt = {
icon: Et,
headline: "The records cannot be reached",
body: "The party's records are quiet right now. NestQuest will return shortly."
}, Bt = d`<svg
}, Lt = d`<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@@ -1118,13 +1118,13 @@ const qe = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Mt = "polygon(50%
<path d="M15.947 12.65a4 4 0 0 0-5.925-4.128"></path>
<path d="M13 22H7a5 5 0 1 1 4.9-6H13a3 3 0 0 1 0 6Z"></path>
</svg>`;
class Ot extends M {
class Bt extends M {
static properties = {
hass: { attribute: !1 },
_config: { state: !0 },
_now: { state: !0 }
};
static styles = [v(Ve), At];
static styles = [v(Qe), Mt];
hass;
_config;
_now = /* @__PURE__ */ new Date();
@@ -1156,7 +1156,7 @@ class Ot extends M {
t === this._subGeneration && (this._unsubscribeLive(), this._armSubscribeRetry());
});
};
for (const i of gt)
for (const i of mt)
n(
e.subscribeEvents(() => this.requestUpdate(), i)
);
@@ -1219,7 +1219,7 @@ class Ot extends M {
`;
}
_timeZone() {
return _t(this.hass);
return gt(this.hass);
}
_state(e) {
const n = this.hass?.states?.[e];
@@ -1243,7 +1243,7 @@ class Ot extends M {
const t = this._state(
"sensor.nestquest_household_quests_due_today"
)?.attributes?.child_roster;
return Array.isArray(t) ? t.map((n) => xt(n)).filter((n) => n.length > 0) : [];
return Array.isArray(t) ? t.map((n) => vt(n)).filter((n) => n.length > 0) : [];
}
_plates() {
return this._childSlugs().map((e) => this._plate(e));
@@ -1267,8 +1267,8 @@ class Ot extends M {
const G = r?.attributes?.child_name;
typeof G == "string" && G.trim() && (m = G.trim());
}
m || (m = wt(e));
const x = $t(
m || (m = xt(e));
const x = kt(
r?.attributes?.next_present,
this._timeZone()
), R = x ? `Returns ${J(x, this._timeZone())}` : null;
@@ -1297,7 +1297,7 @@ class Ot extends M {
}
_boardNotice() {
const e = this._plates();
return e.length === 0 ? Ce : e.every((t) => t.unresolved !== null) ? e.some((t) => t.unresolved === "stale") ? Lt : Ce : null;
return e.length === 0 ? Ce : e.every((t) => t.unresolved !== null) ? e.some((t) => t.unresolved === "stale") ? Nt : Ce : null;
}
_renderNotice(e) {
return d`
@@ -1320,7 +1320,7 @@ class Ot extends M {
</span>
<span class="name">${e.name}</span>
<span class="pill away">
${zt}
${Tt}
<span>Unknown</span>
</span>
<span class="progress-line">&mdash;</span>
@@ -1340,7 +1340,7 @@ class Ot extends M {
</span>
<span class="name">${e.name}</span>
<span class="pill">
${St}
${At}
<span>Home today</span>
</span>
<span class="progress-line">
@@ -1359,7 +1359,7 @@ class Ot extends M {
</span>
<span class="name">${e.name}</span>
<span class="pill away">
${Tt}
${St}
<span>On travels</span>
</span>
<span class="progress-line">${e.returns ?? "Returns —"}</span>
@@ -1411,13 +1411,13 @@ class Ot extends M {
<div class="dock">
<span class="date">${J(this._now, this._timeZone())}</span>
<span class="divider"></span>
<span class="clock">${yt(this._now, this._timeZone())}</span>
<span class="clock">${_t(this._now, this._timeZone())}</span>
</div>
`;
const t = qt(e.condition), n = Ct(e.condition, t), i = e.temperature === null ? g : d`<span class="temp">${Math.round(e.temperature)}°</span>`, a = e.high === null || e.low === null ? null : `${Math.round(e.high)}° / ${Math.round(e.low)}°`;
const t = $t(e.condition), n = qt(e.condition, t), i = e.temperature === null ? g : d`<span class="temp">${Math.round(e.temperature)}°</span>`, a = e.high === null || e.low === null ? null : `${Math.round(e.high)}° / ${Math.round(e.low)}°`;
return d`
<div class="dock">
${Bt}
${Lt}
${i}
<span class="divider"></span>
<span class="condition">
@@ -1429,14 +1429,14 @@ class Ot extends M {
`;
}
}
customElements.define("nestquest-party-board-card", Ot);
customElements.define("nestquest-party-board-card", Bt);
le({
type: "nestquest-party-board-card",
name: "NestQuest Party Board",
description: "Kids' attract screen for present and away adventurers."
});
const Pt = { CHILD: 2 }, It = (s) => (...e) => ({ _$litDirective$: s, values: e });
let Ht = class {
const Ot = { CHILD: 2 }, Pt = (s) => (...e) => ({ _$litDirective$: s, values: e });
let It = class {
constructor(e) {
}
get _$AU() {
@@ -1452,11 +1452,11 @@ let Ht = class {
return this.render(...t);
}
};
const { I: jt } = ht, Me = (s) => s, Ae = () => document.createComment(""), B = (s, e, t) => {
const { I: Ht } = pt, Me = (s) => s, Ae = () => document.createComment(""), B = (s, e, t) => {
const n = s._$AA.parentNode, i = e === void 0 ? s._$AB : e._$AA;
if (t === void 0) {
const a = n.insertBefore(Ae(), i), r = n.insertBefore(Ae(), i);
t = new jt(a, r, s, s.options);
t = new Ht(a, r, s, s.options);
} else {
const a = t._$AB.nextSibling, r = t._$AM, l = r !== s;
if (l) {
@@ -1472,16 +1472,16 @@ const { I: jt } = ht, Me = (s) => s, Ae = () => document.createComment(""), B =
}
}
return t;
}, q = (s, e, t = s) => (s._$AI(e, t), s), Rt = {}, Ut = (s, e = Rt) => s._$AH = e, Dt = (s) => s._$AH, Y = (s) => {
}, q = (s, e, t = s) => (s._$AI(e, t), s), jt = {}, Rt = (s, e = jt) => s._$AH = e, Ut = (s) => s._$AH, Y = (s) => {
s._$AR(), s._$AA.remove();
};
const Se = (s, e, t) => {
const n = /* @__PURE__ */ new Map();
for (let i = e; i <= t; i++) n.set(s[i], i);
return n;
}, Te = It(class extends Ht {
}, Te = Pt(class extends It {
constructor(s) {
if (super(s), s.type !== Pt.CHILD) throw Error("repeat() can only be used in text expressions");
if (super(s), s.type !== Ot.CHILD) throw Error("repeat() can only be used in text expressions");
}
dt(s, e, t) {
let n;
@@ -1495,7 +1495,7 @@ const Se = (s, e, t) => {
return this.dt(s, e, t).values;
}
update(s, [e, t, n]) {
const i = Dt(s), { values: a, keys: r } = this.dt(e, t, n);
const i = Ut(s), { values: a, keys: r } = this.dt(e, t, n);
if (!Array.isArray(i)) return this.ut = r, a;
const l = this.ut ??= [], o = [];
let p, u, c = 0, f = i.length - 1, h = 0, m = a.length - 1;
@@ -1522,9 +1522,9 @@ const Se = (s, e, t) => {
const _ = i[c++];
_ !== null && Y(_);
}
return this.ut = r, Ut(s, o), S;
return this.ut = r, Rt(s, o), S;
}
}), Ft = [
}), Dt = [
{
kind: "lucide",
key: "lucide:bed",
@@ -1994,7 +1994,7 @@ const Se = (s, e, t) => {
"m3 7 2 2 4-4"
]
}
], Qt = [
], Ft = [
{
kind: "fa",
key: "fa:broom",
@@ -2215,20 +2215,20 @@ const Se = (s, e, t) => {
"M384 512L96 512c-53 0-96-43-96-96L0 96C0 43 43 0 96 0L400 0c26.5 0 48 21.5 48 48l0 288c0 20.9-13.4 38.7-32 45.3l0 66.7c17.7 0 32 14.3 32 32s-14.3 32-32 32l-32 0zM96 384c-17.7 0-32 14.3-32 32s14.3 32 32 32l256 0 0-64-256 0zm32-232c0 13.3 10.7 24 24 24l176 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-176 0c-13.3 0-24 10.7-24 24zm24 72c-13.3 0-24 10.7-24 24s10.7 24 24 24l176 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-176 0z"
]
}
], O = { kind: "fallback", key: null }, Vt = 8, ze = new Map(Ft.map((s) => [s.name, s])), Zt = new Map(Qt.map((s) => [s.name, s]));
function Wt(s) {
], O = { kind: "fallback", key: null }, Qt = 8, ze = new Map(Dt.map((s) => [s.name, s])), Vt = new Map(Ft.map((s) => [s.name, s]));
function Zt(s) {
const e = [...s];
return e.length === 0 || e.length > Vt ? !1 : !e.some((t) => new RegExp("[\\s<>&]|\\p{Cc}", "u").test(t));
return e.length === 0 || e.length > Qt ? !1 : !e.some((t) => new RegExp("[\\s<>&]|\\p{Cc}", "u").test(t));
}
function Gt(s) {
function Wt(s) {
if (!s || !s.trim()) return O;
const e = s.indexOf(":");
if (e === -1) return ze.get(s) ?? O;
const t = s.slice(0, e), n = s.slice(e + 1);
return t === "lucide" ? ze.get(n) ?? O : t === "fa" ? Zt.get(n) ?? O : t === "emoji" && Wt(n) ? { kind: "emoji", key: s, text: n } : O;
return t === "lucide" ? ze.get(n) ?? O : t === "fa" ? Vt.get(n) ?? O : t === "emoji" && Zt(n) ? { kind: "emoji", key: s, text: n } : O;
}
function Ee(s, e) {
const t = Gt(s);
function Gt(s, e) {
const t = Wt(s);
switch (t.kind) {
case "lucide":
return d`<svg
@@ -2263,9 +2263,9 @@ const Kt = [
"nestquest_quest_uncompleted",
"nestquest_quest_missed",
"nestquest_child_day_complete"
], Ne = /* @__PURE__ */ new Map();
], Ee = /* @__PURE__ */ new Map();
function E(s, e) {
const t = `${s ?? "local"}|${JSON.stringify(e)}`, n = Ne.get(t);
const t = `${s ?? "local"}|${JSON.stringify(e)}`, n = Ee.get(t);
if (n)
return n;
let i;
@@ -2277,7 +2277,7 @@ function E(s, e) {
} catch {
i = new Intl.DateTimeFormat("en-US", e);
}
return Ne.set(t, i), i;
return Ee.set(t, i), i;
}
function Jt(s) {
const e = s?.config;
@@ -2356,7 +2356,7 @@ function nn(s, e) {
);
return Number.isFinite(i) ? i - s : 0;
}
function Le(s, e) {
function Ne(s, e) {
if (typeof s != "string")
return null;
const t = s.split("-");
@@ -2439,7 +2439,7 @@ function ln(s) {
function cn(s, e) {
return on[s] ?? e;
}
const Be = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", dn = "polygon(50% 0, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%)", Oe = [-9, 6, -4], pn = He`
const Le = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", dn = "polygon(50% 0, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%)", Be = [-9, 6, -4], pn = Ie`
* {
box-sizing: border-box;
}
@@ -2499,7 +2499,7 @@ const Be = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", dn = "polygon(50%
width: 96px;
height: 110px;
padding: 3px;
clip-path: ${v(Be)};
clip-path: ${v(Le)};
background: rgba(255, 255, 255, 0.22);
}
@@ -2510,7 +2510,7 @@ const Be = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", dn = "polygon(50%
align-items: center;
justify-content: center;
padding-bottom: 32px;
clip-path: ${v(Be)};
clip-path: ${v(Le)};
background: var(--nq-p-crest);
}
@@ -3341,7 +3341,7 @@ const Be = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", dn = "polygon(50%
<path d="M22 22H2"></path>
<path d="m8 6 4-4 4 4"></path>
<path d="M16 18a4 4 0 0 0-8 0"></path>
</svg>`, Ze = d`<svg
</svg>`, Ve = d`<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@@ -3437,9 +3437,9 @@ const Be = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", dn = "polygon(50%
icon: yn,
headline: "The records cannot be reached",
body: "The party's records are quiet right now. NestQuest will return shortly."
}, Pe = [
}, Oe = [
{ key: "morning", name: "Morning", range: "Until 11:59 AM", icon: un },
{ key: "afternoon", name: "Afternoon", range: "12:00–5:00 PM", icon: Ze },
{ key: "afternoon", name: "Afternoon", range: "12:00–5:00 PM", icon: Ve },
{ key: "evening", name: "Evening", range: "5:00–9:00 PM", icon: fn }
];
class kn extends M {
@@ -3452,7 +3452,7 @@ class kn extends M {
_toast: { state: !0 },
_countdown: { state: !0 }
};
static styles = [v(Ve), pn];
static styles = [v(Qe), pn];
hass;
_config;
_now = /* @__PURE__ */ new Date();
@@ -3570,7 +3570,7 @@ class kn extends M {
${this._renderHeader(e)}
<div class="notice-wrap">
<div class="notice" role="status">
${Ze}
${Ve}
<span class="notice-headline">No quests today</span>
<p class="notice-body">
${`Nothing was posted for today, ${e.name}. Enjoy the day's rest!`}
@@ -3614,7 +3614,7 @@ class kn extends M {
return d`
${this._renderHeader(e)}
<div class="columns">
${Pe.map((t) => this._renderColumn(t))}
${Oe.map((t) => this._renderColumn(t))}
</div>
`;
}
@@ -3662,7 +3662,7 @@ class kn extends M {
_awayReturns(e) {
const t = this._timeZone(), n = this._state(
`binary_sensor.nestquest_${e}_present_today`
), i = Le(
), i = Ne(
n?.attributes?.next_present,
t
);
@@ -3808,7 +3808,7 @@ class kn extends M {
if (!u) {
const _ = this._state(
`binary_sensor.nestquest_${l}_present_today`
), x = Le(
), x = Ne(
_?.attributes?.next_present,
n
);
@@ -3915,10 +3915,10 @@ class kn extends M {
}
_renderQuest(e, t, n) {
if (t) {
const r = Oe[n % Oe.length], l = e.completed_at ? Yt(e.completed_at, this._timeZone()) : "";
const r = Be[n % Be.length], l = e.completed_at ? Yt(e.completed_at, this._timeZone()) : "";
return d`
<div class="quest sealed" data-instance-id=${e.id}>
<span class="tile" aria-hidden="true">${Ee(e.icon, te)}</span>
<span class="tile" aria-hidden="true">${te}</span>
<div class="body">
<span class="quest-title">${e.title}</span>
<span class="meta">
@@ -3954,7 +3954,7 @@ class kn extends M {
(r.key === "Enter" || r.key === " ") && (r.preventDefault(), this._openConfirm(e.id));
}}
>
<span class="tile" aria-hidden="true">${Ee(e.icon, te)}</span>
<span class="tile" aria-hidden="true">${Gt(e.icon, te)}</span>
<div class="body">
<span class="quest-title">${e.title}</span>
<span class="meta ${a.late ? "late" : g}">${a.text}</span>
@@ -3990,7 +3990,7 @@ class kn extends M {
);
if (!e)
return g;
const t = Pe.find((n) => n.key === e.window);
const t = Oe.find((n) => n.key === e.window);
return d`
<div class="scrim" @click=${() => this._closeConfirm()}>
<div
@@ -4231,7 +4231,7 @@ le({
name: "NestQuest Admin",
description: "Parent phone view for today, tasks, schedule, and history."
});
function We(s) {
function Ze(s) {
return typeof s == "string" ? s.trim() : "";
}
function Cn(s) {
@@ -4241,7 +4241,7 @@ function Cn(s) {
return typeof e == "string" ? e.trim() : "";
}
function Mn(s) {
const e = We(s.url_path).replace(/^\/+|\/+$/g, "");
const e = Ze(s.url_path).replace(/^\/+|\/+$/g, "");
return e || (window.location.pathname.split("/").filter(Boolean)[0] ?? "");
}
function An(s) {
@@ -4256,7 +4256,7 @@ function Sn(s) {
}
class Tn {
static async generate(e, t) {
const n = `/${Mn(e)}`, i = We(e.weather_entity), a = {
const n = `/${Mn(e)}`, i = Ze(e.weather_entity), a = {
type: "custom:nestquest-party-board-card",
quest_log_path: n
};
+27 -2
View File
@@ -84,8 +84,33 @@ Footer: `Cancel` (flex 1, bordered) + `Save changes` (flex 2, gradient), both 13
padding. Sheet enters over the dimmed list in `--nq-dur-modal`.
### 3.3 Icon picker
Each definition carries a Lucide glyph, shown on both surfaces. **The allowed subset is not
yet defined** — pick 24–40 household-relevant glyphs and ship them as a grid picker.
Each definition carries one optional icon, shown on both surfaces. The picker offers three
sources:
- **Lucide** — a grid of the curated Lucide glyphs, drawn stroked.
- **Font Awesome** — a grid of a curated subset of the Font Awesome Free *solid* set,
drawn filled.
- **Emoji** — a text input; the typed emoji is stored as entered (not trimmed) and must be
1–8 code points with no whitespace, control characters, `<`, `>` or `&`.
Both curated lists come from `tools/icons/curated-icons.json`; their glyph path data is
generated into the admin and panel bundles by `tools/icons/generate.mjs` (no CDN, no
runtime fetch). See `THIRD-PARTY-NOTICES.md` for licences.
The stored value is a **namespaced key**:
| Source | Stored key | Example |
|---|---|---|
| Lucide | `lucide:<name>` | `lucide:dog` |
| Font Awesome | `fa:<name>` | `fa:paw` |
| Emoji | `emoji:<grapheme>` | `emoji:🐶` |
`<name>` is a lowercase kebab name (`[a-z0-9-]`, no leading or trailing `-`). A **legacy
bare name** written before namespacing (`dog`) reads as `lucide:<name>` — the API returns it
namespaced, and any write of a bare name is stored as `lucide:<name>`; no data migration. An
empty value clears the icon. An empty, unknown, or unresolvable key renders the **fallback
glyph** — in the admin, the generic task glyph (clipboard with a check); on the panel, the
star (`PANEL-SPEC.md` §3.2).
## 4. Schedule (1f)
+3 -2
View File
@@ -78,5 +78,6 @@ The panel speaks tabletop; the admin speaks plainly. Both sit on the same `quest
decide before shipping the card.
- No gamification is in scope. XP, levels, streaks and rarity were explicitly deferred,
but the append-only event log already supports computing them later.
- Icon per quest definition is chosen by the parent from a Lucide subset; the subset is
not yet defined (`ADMIN-SPEC.md` §3.3).
- Icon per quest definition is chosen by the parent from curated Lucide and Font Awesome
Free sets or as an emoji, stored as a namespaced key (`lucide:`/`fa:`/`emoji:`) —
resolved 2026-09-26, see `ADMIN-SPEC.md` §3.3.
+1 -1
View File
@@ -35,7 +35,7 @@ implementer's call), shaped:
{
"instances": [
{ "id": 4821, "definition_id": 12, "child_id": 3, "title": "Fill bird feeder",
"icon": "bird", "window": "afternoon", "due_time": "17:00",
"icon": "lucide:bird", "window": "afternoon", "due_time": "17:00",
"state": "open", "overdue": true, "completed_at": null, "on_time": null }
]
}
+11 -2
View File
@@ -91,7 +91,14 @@ collapse the column, the three-column rhythm is the layout.
`--nq-p-quest-min-height` 116px, radius 12px, `--nq-p-card-open`, 1px
`--nq-p-card-border`, `--nq-p-card-shadow`, padding 22px, 22px gap, row layout:
1. **Icon tile** 64×64, radius 10px, `--nq-p-icon-tile`, Lucide glyph 34px white.
1. **Icon tile** 64×64, radius 10px, `--nq-p-icon-tile`, glyph 34px white — the
definition's icon, resolved from its namespaced key (`ADMIN-SPEC.md` §3.3):
`lucide:<name>` draws the curated Lucide glyph **stroked** (fill none, stroke-width 2,
round caps and joins); `fa:<name>` draws the curated Font Awesome glyph **filled**;
`emoji:<grapheme>` renders the emoji **as text**. A legacy bare name resolves as
`lucide:<name>`. No icon, or any key that does not resolve (unknown namespace or name,
malformed emoji), falls back to the **star** glyph. Glyph data is bundled from the
shared registry — nothing is fetched at runtime.
2. **Text** title 30px/700 Cinzel `--nq-p-ink`; meta 22px/600 Nunito
`--nq-p-ink-secondary` reading `Due by <h:mm A>`. If past due:
`Overdue · due <h:mm A>` in 22px/700 Cinzel uppercase, tracking .10em,
@@ -106,7 +113,9 @@ Both the card body and the button open the confirm dialog (§4). Press feedback:
### 3.3 Quest card — sealed
`--nq-p-card-done`, `opacity: .82`, no shadow, **not tappable**. Icon tile
`--nq-p-icon-tile-done` with the glyph in `--nq-p-ink-secondary`. Title 30px/700 Cinzel
`--nq-p-icon-tile-done` with the glyph in `--nq-p-ink-secondary`. The sealed tile keeps
the static star glyph — it does **not** render the definition icon — alongside the wax
seal below. Title 30px/700 Cinzel
`--nq-p-ink-muted` with `text-decoration: line-through`. Meta reads
`Claimed <h:mm A>` 22px/600 Nunito `--nq-p-ink-secondary`.
+1 -1
View File
@@ -2057,7 +2057,7 @@ export class NestQuestQuestLogCard extends LitElement {
: "";
return html`
<div class="quest sealed" data-instance-id=${instance.id}>
<span class="tile" aria-hidden="true">${renderQuestIcon(instance.icon, ICON_STAR)}</span>
<span class="tile" aria-hidden="true">${ICON_STAR}</span>
<div class="body">
<span class="quest-title">${instance.title}</span>
<span class="meta">
+20 -4
View File
@@ -7,10 +7,10 @@
* the card is mounted at the given URL (it resolves its child from the
* pathname's last segment), after the optional probe wait one snapshot of
* the render is taken mid-wait, then after the optional idle wait stdout
* receives one JSON object {location, headline, countdown, html, probe} —
* the final window pathname, the rendered title, the complete-screen
* countdown text, the rendered shadow DOM, and the probe snapshot when
* probe_ms was given. The Python panel tests drive this harness so the
* receives one JSON object {location, headline, countdown, html, tiles,
* probe} — the final window pathname, the rendered title, the
* complete-screen countdown text, the rendered shadow DOM, each quest
* card's icon tile, and the probe snapshot when probe_ms was given. The Python panel tests drive this harness so the
* assertions run against the exact bundle HACS ships.
*/
import { readFileSync } from "node:fs";
@@ -56,6 +56,22 @@ const snapshot = () => ({
element.shadowRoot.querySelector(".complete-countdown")?.textContent?.trim() ??
"",
html: element.shadowRoot.innerHTML,
// Each quest card's icon tile: the registry glyph it rendered (its
// data-icon key and svg fill), its text (an emoji), and whether it shows
// the static star (the star's polygon).
tiles: [...element.shadowRoot.querySelectorAll(".quest")].map((quest) => {
const tile = quest.querySelector(".tile");
const glyph = tile?.querySelector("[data-icon]");
return {
id: Number(quest.dataset.instanceId),
sealed: quest.classList.contains("sealed"),
icon: glyph?.getAttribute("data-icon") ?? null,
fill: glyph?.getAttribute("fill") ?? null,
paths: tile?.querySelectorAll("path").length ?? 0,
text: tile?.textContent?.trim() ?? "",
star: Boolean(tile?.querySelector("polygon")),
};
}),
});
let probe = null;
+112
View File
@@ -886,3 +886,115 @@ def test_quest_complete_screen_returns_to_the_board_after_the_countdown() -> Non
assert result["probe"]["countdown"] == "Returning to The Party in 1 seconds"
# …and at zero the card has returned to the party board.
assert result["location"] == "/nestquest"
# --- Quest card icons (definition icon on the open tile) ---------------------
def _quest_row(instance_id: int, icon: str | None, state: str = "open") -> dict:
"""One ``instances`` attribute row for Ada's quests-due sensor."""
row = {
"id": instance_id,
"child_id": 1,
"title": f"Quest {instance_id}",
"icon": icon,
"window": "morning",
"due_time": "08:00",
"state": state,
"overdue": False,
"completed_at": None,
"on_time": None,
}
if state == "completed":
row["completed_at"] = "2026-09-26T07:30:00+00:00"
row["on_time"] = True
return row
def _quest_log_tiles(rows: list[dict]) -> dict[int, dict]:
"""Render Ada's quest log with ``rows`` and key its tiles by instance id."""
states = _three_child_states()
states["sensor.nestquest_ada_quests_due_today"] = _state(
str(len(rows)), child_name="Ada", present=True, instances=rows
)
result = _render_quest_log(
{"type": "custom:nestquest-quest-log-card"},
states,
url="http://homeassistant.local/nestquest/ada",
)
return {tile["id"]: tile for tile in result["tiles"]}
def test_open_quest_tile_renders_the_definition_icon() -> None:
"""The open card's tile shows the definition's namespaced icon, drawn
from the bundled registry: Lucide as a stroked svg, Font Awesome as a
filled svg, an emoji as its text, and a legacy bare name as Lucide."""
tiles = _quest_log_tiles(
[
_quest_row(1, "lucide:bed"),
_quest_row(2, "fa:broom"),
_quest_row(3, "emoji:🦊"),
_quest_row(4, "bath"),
]
)
assert tiles[1]["icon"] == "lucide:bed"
assert tiles[1]["fill"] == "none"
assert tiles[1]["paths"] > 0
assert tiles[2]["icon"] == "fa:broom"
assert tiles[2]["fill"] == "currentColor"
assert tiles[2]["paths"] > 0
assert tiles[3]["icon"] == "emoji:🦊"
assert tiles[3]["text"] == "🦊"
assert tiles[4]["icon"] == "lucide:bath"
assert tiles[4]["fill"] == "none"
for tile in tiles.values():
assert not tile["sealed"]
assert not tile["star"]
def test_open_quest_tile_falls_back_to_the_star() -> None:
"""An unknown name, an unknown namespace, or no icon at all keeps the
open card's current star glyph."""
tiles = _quest_log_tiles(
[
_quest_row(1, "lucide:not-a-real-icon"),
_quest_row(2, "fa:not-a-real-icon"),
_quest_row(3, "mdi:bed"),
_quest_row(4, ""),
_quest_row(5, None),
]
)
assert set(tiles) == {1, 2, 3, 4, 5}
for tile in tiles.values():
assert tile["star"]
assert tile["icon"] is None
def test_sealed_quest_tile_keeps_the_star() -> None:
"""A sealed card keeps its star tile (and its wax seal) whatever the
definition's icon; only the open card shows the quest's icon."""
tiles = _quest_log_tiles(
[
_quest_row(1, "lucide:bed", state="completed"),
_quest_row(2, "fa:broom", state="completed"),
_quest_row(3, "emoji:🦊", state="completed"),
]
)
for tile in tiles.values():
assert tile["sealed"]
assert tile["star"]
assert tile["icon"] is None
assert tile["text"] == ""
def test_only_the_open_tile_calls_the_icon_resolver() -> None:
source = _read(QUEST_LOG)
sealed_start = source.index('<div class="quest sealed"')
sealed_block = source[sealed_start : source.index("</div>", sealed_start)]
assert '<span class="tile" aria-hidden="true">${ICON_STAR}</span>' in sealed_block
assert "renderQuestIcon" not in sealed_block
assert (
source.count("renderQuestIcon(instance.icon, ICON_STAR)") == 1
), "only the open card's tile resolves the definition icon"
open_start = source.index('class="quest tappable"')
assert "renderQuestIcon(instance.icon, ICON_STAR)" in source[open_start:]