Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59ff392433 | ||
|
|
7f2f903fa9 |
@@ -1,31 +0,0 @@
|
||||
# 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.
|
||||
@@ -161,10 +161,6 @@ function fakeApi(initial: QuestDefinition[]) {
|
||||
}
|
||||
const match = /^\/api\/v1\/admin\/quest-definitions\/(\d+)$/.exec(path);
|
||||
if (method === "PATCH" && match) {
|
||||
// Like the API's strict validate_icon: a Lucide name never has a colon.
|
||||
if (typeof body.icon === "string" && /^lucide:.*:/.test(body.icon)) {
|
||||
return jsonResponse({ detail: "icon is not a valid icon key" }, 422);
|
||||
}
|
||||
const id = Number(match[1]);
|
||||
definitions = definitions.map((d) =>
|
||||
d.id === id
|
||||
@@ -467,6 +463,7 @@ describe("DefinitionsTab", () => {
|
||||
path: "/api/v1/admin/quest-definitions/11",
|
||||
body: {
|
||||
title: "Take out the bins",
|
||||
icon: null,
|
||||
rule: {
|
||||
rule_type: "monthly_day",
|
||||
interval: 1,
|
||||
@@ -1090,7 +1087,7 @@ describe("DefinitionsTab icon picker", () => {
|
||||
await waitFor(() => expect(rowIcon(screen.getByTestId("definition-11"))).toBe("trash-2"));
|
||||
});
|
||||
|
||||
it("an unchanged icon is omitted on edit and shows as selected", async () => {
|
||||
it("an unchanged icon is still sent on edit and shows as selected", async () => {
|
||||
await renderReady();
|
||||
expect(rowIcon(screen.getByTestId("definition-10"))).toBe("smile");
|
||||
fireEvent.click(screen.getByTestId("definition-10"));
|
||||
@@ -1104,7 +1101,7 @@ describe("DefinitionsTab icon picker", () => {
|
||||
fireEvent.click(within(s).getByRole("button", { name: "Save changes" }));
|
||||
});
|
||||
|
||||
expect(api.writes()[0].body).not.toHaveProperty("icon");
|
||||
expect((api.writes()[0].body as { icon: string }).icon).toBe("smile");
|
||||
});
|
||||
|
||||
it("an unknown legacy icon falls back to the generic glyph and is kept on save", async () => {
|
||||
@@ -1124,7 +1121,7 @@ describe("DefinitionsTab icon picker", () => {
|
||||
fireEvent.click(within(s).getByRole("button", { name: "Save changes" }));
|
||||
});
|
||||
|
||||
expect(api.writes()[0].body).not.toHaveProperty("icon");
|
||||
expect((api.writes()[0].body as { icon: string }).icon).toBe("rocket-ship-9000");
|
||||
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
||||
expect(rowIcon(screen.getByTestId("definition-20"))).toBeNull();
|
||||
});
|
||||
@@ -1411,7 +1408,7 @@ describe("DefinitionsTab Font Awesome and emoji icons", () => {
|
||||
await waitFor(() => expect(rowGlyph(screen.getByTestId("definition-52"))).toBe("fa:paw"));
|
||||
});
|
||||
|
||||
it("an unchanged emoji is omitted on edit", async () => {
|
||||
it("an unchanged emoji is sent as-is on edit", async () => {
|
||||
await renderReady([{ ...BRUSH, id: 53, icon: "emoji:🐶" }]);
|
||||
fireEvent.click(screen.getByTestId("definition-53"));
|
||||
const s = sheet();
|
||||
@@ -1419,7 +1416,7 @@ describe("DefinitionsTab Font Awesome and emoji icons", () => {
|
||||
target: { value: "Feed the dog" },
|
||||
});
|
||||
await save(s);
|
||||
expect(api.writes()[0].body).not.toHaveProperty("icon");
|
||||
expect((api.writes()[0].body as { icon: string }).icon).toBe("emoji:🐶");
|
||||
});
|
||||
|
||||
it("No icon and an emptied emoji field both clear the icon to null", async () => {
|
||||
@@ -1445,110 +1442,6 @@ describe("DefinitionsTab Font Awesome and emoji icons", () => {
|
||||
});
|
||||
});
|
||||
|
||||
/* ── Legacy icons ───────────────────────────────────────────────────── */
|
||||
|
||||
describe("DefinitionsTab legacy icon edits", () => {
|
||||
// The API reads a stored legacy `mdi:tooth` back as `lucide:mdi:tooth`,
|
||||
// which its strict write validation rejects if sent back.
|
||||
const LEGACY: QuestDefinition = { ...BRUSH, id: 60, icon: "lucide:mdi:tooth" };
|
||||
|
||||
beforeEach(() => {
|
||||
sessionStorage.clear();
|
||||
storeTokens({ access_token: "at-123", expires_in: 600 });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.unstubAllGlobals();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
function radio(s: HTMLElement, group: "Icon" | "Font Awesome", label: string): HTMLElement {
|
||||
return within(within(s).getByRole("radiogroup", { name: group })).getByRole("radio", {
|
||||
name: label,
|
||||
});
|
||||
}
|
||||
|
||||
async function save(s: HTMLElement) {
|
||||
await act(async () => {
|
||||
fireEvent.click(within(s).getByRole("button", { name: "Save changes" }));
|
||||
});
|
||||
}
|
||||
|
||||
it("editing only the title omits the legacy icon and saves", async () => {
|
||||
await renderReady([LEGACY]);
|
||||
fireEvent.click(screen.getByTestId("definition-60"));
|
||||
const s = sheet();
|
||||
fireEvent.change(within(s).getByRole("textbox", { name: "Title" }), {
|
||||
target: { value: "Brush teeth well" },
|
||||
});
|
||||
await save(s);
|
||||
|
||||
expect(api.writes()).toHaveLength(1);
|
||||
expect(api.writes()[0].method).toBe("PATCH");
|
||||
expect(api.writes()[0].body).not.toHaveProperty("icon");
|
||||
expect((api.writes()[0].body as { title: string }).title).toBe("Brush teeth well");
|
||||
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
||||
expect(await screen.findByText("Brush teeth well")).toBeTruthy();
|
||||
});
|
||||
|
||||
for (const [label, choose, sent] of [
|
||||
["a Lucide", (s: HTMLElement) => fireEvent.click(radio(s, "Icon", "Dog")), "lucide:dog"],
|
||||
["a Font Awesome", (s: HTMLElement) => fireEvent.click(radio(s, "Font Awesome", "Dog")), "fa:dog"],
|
||||
[
|
||||
"an emoji",
|
||||
(s: HTMLElement) =>
|
||||
fireEvent.change(within(s).getByRole("textbox", { name: "Emoji" }), {
|
||||
target: { value: "🦷" },
|
||||
}),
|
||||
"emoji:🦷",
|
||||
],
|
||||
["No icon", (s: HTMLElement) => fireEvent.click(radio(s, "Icon", "No icon")), null],
|
||||
] as const) {
|
||||
it(`choosing ${label} replacement for the legacy icon sends ${JSON.stringify(sent)}`, async () => {
|
||||
await renderReady([LEGACY]);
|
||||
fireEvent.click(screen.getByTestId("definition-60"));
|
||||
const s = sheet();
|
||||
choose(s);
|
||||
await save(s);
|
||||
|
||||
expect(api.writes()).toHaveLength(1);
|
||||
expect(api.writes()[0].body).toHaveProperty("icon", sent);
|
||||
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
||||
});
|
||||
}
|
||||
|
||||
it("an invalid emoji replacing the legacy icon is still rejected before the request", async () => {
|
||||
await renderReady([LEGACY]);
|
||||
fireEvent.click(screen.getByTestId("definition-60"));
|
||||
const s = sheet();
|
||||
fireEvent.change(within(s).getByRole("textbox", { name: "Emoji" }), {
|
||||
target: { value: "a b" },
|
||||
});
|
||||
await save(s);
|
||||
|
||||
expect(api.writes()).toHaveLength(0);
|
||||
expect(within(s).getByText(/Enter a single emoji/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("creating a task still sends icon, even when none is chosen", async () => {
|
||||
await renderReady([LEGACY]);
|
||||
fireEvent.click(screen.getByRole("button", { name: "New" }));
|
||||
const s = sheet();
|
||||
fireEvent.change(within(s).getByRole("textbox", { name: "Title" }), {
|
||||
target: { value: "Floss" },
|
||||
});
|
||||
fireEvent.click(within(s).getByRole("button", { name: /Assigned children/ }));
|
||||
fireEvent.click(within(s).getByRole("checkbox", { name: "Declan" }));
|
||||
fireEvent.click(within(s).getByRole("button", { name: "Morning" }));
|
||||
await save(s);
|
||||
|
||||
expect(api.writes()).toHaveLength(1);
|
||||
expect(api.writes()[0].method).toBe("POST");
|
||||
expect(api.writes()[0].body).toHaveProperty("icon", null);
|
||||
});
|
||||
});
|
||||
|
||||
/* ── Start date ─────────────────────────────────────────────────────── */
|
||||
|
||||
function startDateInput(s: HTMLElement): HTMLInputElement {
|
||||
|
||||
@@ -462,10 +462,6 @@ function EditSheet({
|
||||
// assignee_child_ids, because the API replaces the whole set and inactive
|
||||
// children cannot be sent back.
|
||||
const [originalActiveIds] = useState(() => draft.assigneeIds);
|
||||
// The icon as loaded: while it is unchanged, the PATCH omits `icon` (absent
|
||||
// means unchanged), because a legacy value read back as `lucide:<old>` would
|
||||
// fail the API's strict icon validation if sent back.
|
||||
const [originalIcon] = useState(() => draft.icon);
|
||||
const inactiveAssignees =
|
||||
target === "new"
|
||||
? []
|
||||
@@ -548,7 +544,6 @@ function EditSheet({
|
||||
} else {
|
||||
const edit: DefinitionEditBody = { ...body };
|
||||
if (keepsAssignees) delete edit.assignee_child_ids;
|
||||
if (draft.icon === originalIcon) delete edit.icon;
|
||||
await updateDefinition(target.id, edit);
|
||||
}
|
||||
onSaved();
|
||||
|
||||
@@ -3,18 +3,12 @@ import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
// The offline generator is the only writer of both registry artefacts.
|
||||
import { OUTPUT_PATHS, REPO_ROOT, SOURCE_PATH, renderRegistry } from "../../../tools/icons/generate.mjs";
|
||||
import { EMOJI_MAX_CODE_POINTS, FALLBACK_ICON, FA_ICONS, LUCIDE_ICONS, resolveIcon } from "./registry.generated";
|
||||
import { FALLBACK_ICON, FA_ICONS, LUCIDE_ICONS, resolveIcon } from "./registry.generated";
|
||||
|
||||
type Curated = { lucide: { name: string; label: string }[]; fa: { name: string; label: string }[] };
|
||||
|
||||
const curated = JSON.parse(readFileSync(SOURCE_PATH, "utf8")) as Curated;
|
||||
|
||||
type EmojiPayloadCase = { payload: string; valid: boolean; why: string };
|
||||
|
||||
const emojiPayloadCases = JSON.parse(
|
||||
readFileSync(join(REPO_ROOT, "tools", "icons", "emoji-payload-cases.json"), "utf8"),
|
||||
) as { cases: EmojiPayloadCase[] };
|
||||
|
||||
describe("shared icon registry — one source of truth", () => {
|
||||
it("both committed artefacts are byte-identical to a fresh generation", async () => {
|
||||
const fresh = await renderRegistry();
|
||||
@@ -92,24 +86,6 @@ describe("resolveIcon", () => {
|
||||
}
|
||||
expect(FALLBACK_ICON).toEqual({ kind: "fallback", key: null });
|
||||
});
|
||||
|
||||
// tests/test_core_icons.py asserts the same table against the backend's
|
||||
// validate_icon, so the two emoji rules cannot drift apart silently.
|
||||
it.each(emojiPayloadCases.cases)("emoji verdict matches the shared table: $why", ({ payload, valid }) => {
|
||||
const value = `emoji:${payload}`;
|
||||
expect(resolveIcon(value)).toEqual(valid ? { kind: "emoji", key: value, text: payload } : FALLBACK_ICON);
|
||||
});
|
||||
|
||||
it("the shared emoji table covers the contract edges", () => {
|
||||
const verdicts = new Map(emojiPayloadCases.cases.map(({ payload, valid }) => [payload, valid]));
|
||||
expect(verdicts.get("\ufeff")).toBe(false);
|
||||
expect(verdicts.get("👨\u200d👩\u200d👧\u200d👦")).toBe(true);
|
||||
expect(verdicts.get("🐕 ")).toBe(false);
|
||||
expect(verdicts.get("🐕\u0000")).toBe(false);
|
||||
for (const markup of ["<b>", "a>b", "a&b"]) expect(verdicts.get(markup)).toBe(false);
|
||||
expect(verdicts.get("⭐".repeat(EMOJI_MAX_CODE_POINTS))).toBe(true);
|
||||
expect(verdicts.get("⭐".repeat(EMOJI_MAX_CODE_POINTS + 1))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
function filesUnder(dir: string): string[] {
|
||||
|
||||
@@ -6,9 +6,8 @@ namespace is one of :data:`ICON_NAMESPACES`:
|
||||
- ``lucide:<name>`` / ``fa:<name>`` — ``<name>`` is a lowercase kebab
|
||||
name (ASCII ``[a-z0-9-]``, not starting or ending with ``-``).
|
||||
- ``emoji:<payload>`` — a short emoji string of at most
|
||||
:data:`EMOJI_MAX_CODE_POINTS` code points, with no whitespace (JS
|
||||
``\\s``, so including U+FEFF), no control characters and none of
|
||||
``<``, ``>``, ``&``.
|
||||
:data:`EMOJI_MAX_CODE_POINTS` code points, with no whitespace, no
|
||||
control characters and none of ``<``, ``>``, ``&``.
|
||||
|
||||
Rows written before namespacing hold a bare Lucide name (``dog``).
|
||||
:func:`validate_icon` (writes) normalizes such a legacy value to
|
||||
@@ -34,12 +33,7 @@ ICON_NAMESPACES = frozenset(
|
||||
EMOJI_MAX_CODE_POINTS = 8
|
||||
|
||||
_KEBAB_NAME = re.compile(r"[a-z0-9](?:[a-z0-9-]*[a-z0-9])?")
|
||||
# U+FEFF (BOM / zero-width no-break space) is neither str.isspace() nor
|
||||
# ``Cc`` in Python, but JS ``\s`` matches it, so it is listed here to keep
|
||||
# this rule identical to the frontends' isEmojiPayload (/[\s<>&]|\p{Cc}/u).
|
||||
# Other format characters, notably the ZWJ U+200D that emoji sequences
|
||||
# need, stay valid in both runtimes.
|
||||
_EMOJI_FORBIDDEN = frozenset("<>&\ufeff")
|
||||
_EMOJI_FORBIDDEN = frozenset("<>&")
|
||||
|
||||
|
||||
def _is_kebab_name(value: str) -> bool:
|
||||
|
||||
@@ -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 Pe = class {
|
||||
let Ie = 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 Pe = class {
|
||||
return this.cssText;
|
||||
}
|
||||
};
|
||||
const v = (s) => new Pe(typeof s == "string" ? s : s + "", void 0, ie), Ie = (s, ...e) => {
|
||||
const v = (s) => new Ie(typeof s == "string" ? s : s + "", void 0, ie), He = (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 Pe(t, s, ie);
|
||||
}, We = (s, e) => {
|
||||
return new Ie(t, s, ie);
|
||||
}, Ge = (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 Pe(typeof s == "string" ? s : s + "", void 0, ie), Ie = (s,
|
||||
for (const n of e.cssRules) t += n.cssText;
|
||||
return v(t);
|
||||
})(s) : s;
|
||||
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) {
|
||||
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) {
|
||||
switch (e) {
|
||||
case Boolean:
|
||||
s = s ? tt : null;
|
||||
s = s ? nt : null;
|
||||
break;
|
||||
case Object:
|
||||
case Array:
|
||||
@@ -63,7 +63,7 @@ const { is: Ge, defineProperty: Ke, getOwnPropertyDescriptor: Je, getOwnProperty
|
||||
}
|
||||
}
|
||||
return t;
|
||||
} }, He = (s, e) => !Ge(s, e), he = { attribute: !0, type: String, converter: ne, reflect: !1, useDefault: !1, hasChanged: He };
|
||||
} }, je = (s, e) => !Ke(s, e), he = { attribute: !0, type: String, converter: ne, reflect: !1, useDefault: !1, hasChanged: je };
|
||||
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 && Ke(this.prototype, e, i);
|
||||
i !== void 0 && Je(this.prototype, e, i);
|
||||
}
|
||||
}
|
||||
static getPropertyDescriptor(e, t, n) {
|
||||
const { get: i, set: a } = Je(this.prototype, e) ?? { get() {
|
||||
const { get: i, set: a } = Xe(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 = et(this);
|
||||
const e = tt(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 = [...Xe(t), ...Ye(t)];
|
||||
const t = this.properties, n = [...Ye(t), ...et(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 We(e, this.constructor.elementStyles), e;
|
||||
return Ge(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 ?? He)(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 ?? je)(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(), 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 = `[
|
||||
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 = `[
|
||||
\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, 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) {
|
||||
\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) {
|
||||
if (!re(s) || !s.hasOwnProperty("raw")) throw Error("invalid template strings array");
|
||||
return fe !== void 0 ? fe.createHTML(e) : e;
|
||||
}
|
||||
const at = (s, e) => {
|
||||
const rt = (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 ? (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);
|
||||
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);
|
||||
const h = r === $ && s[l + 1].startsWith("/>") ? " " : "";
|
||||
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);
|
||||
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);
|
||||
}
|
||||
return [Fe(s, a + (s[t] || "<?>") + (e === 2 ? "</svg>" : e === 3 ? "</math>" : "")), n];
|
||||
return [Qe(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] = at(e, t);
|
||||
const l = e.length - 1, o = this.parts, [p, u] = rt(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(je)) {
|
||||
if (i.hasAttributes()) for (const c of i.getAttributeNames()) if (c.endsWith(Re)) {
|
||||
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] === "." ? ot : m[1] === "?" ? lt : m[1] === "@" ? ct : W }), i.removeAttribute(c);
|
||||
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);
|
||||
} else c.startsWith(k) && (o.push({ type: 6, index: a }), i.removeAttribute(c));
|
||||
if (Ue.test(i.tagName)) {
|
||||
if (De.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 === Re) o.push({ type: 2, index: a });
|
||||
} else if (i.nodeType === 8) if (i.data === Ue) 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 rt {
|
||||
class ot {
|
||||
constructor(e, t) {
|
||||
this._$AV = [], this._$AN = void 0, this._$AD = e, this._$AM = t;
|
||||
}
|
||||
@@ -326,7 +326,7 @@ class rt {
|
||||
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 dt(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 pt(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) : it(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) : at(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(Fe(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(Qe(n.h, n.h[0]), this.options)), n);
|
||||
if (this._$AH?._$AD === i) this._$AH.p(t);
|
||||
else {
|
||||
const a = new rt(i, this), r = a.u(this.options);
|
||||
const a = new ot(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 ot extends W {
|
||||
class lt extends W {
|
||||
constructor() {
|
||||
super(...arguments), this.type = 3;
|
||||
}
|
||||
@@ -429,7 +429,7 @@ class ot extends W {
|
||||
this.element[this.name] = e === g ? void 0 : e;
|
||||
}
|
||||
}
|
||||
class lt extends W {
|
||||
class ct extends W {
|
||||
constructor() {
|
||||
super(...arguments), this.type = 4;
|
||||
}
|
||||
@@ -437,7 +437,7 @@ class lt extends W {
|
||||
this.element.toggleAttribute(this.name, !!e && e !== g);
|
||||
}
|
||||
}
|
||||
class ct extends W {
|
||||
class dt extends W {
|
||||
constructor(e, t, n, i, a) {
|
||||
super(e, t, n, i, a), this.type = 5;
|
||||
}
|
||||
@@ -450,7 +450,7 @@ class ct extends W {
|
||||
typeof this._$AH == "function" ? this._$AH.call(this.options?.host ?? this.element, e) : this._$AH.handleEvent(e);
|
||||
}
|
||||
}
|
||||
class dt {
|
||||
class pt {
|
||||
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 dt {
|
||||
z(this, e);
|
||||
}
|
||||
}
|
||||
const pt = { I: N }, ht = ae.litHtmlPolyfillSupport;
|
||||
ht?.(j, N), (ae.litHtmlVersions ??= []).push("3.3.3");
|
||||
const ut = (s, e, t) => {
|
||||
const ht = { I: N }, ut = ae.litHtmlPolyfillSupport;
|
||||
ut?.(j, N), (ae.litHtmlVersions ??= []).push("3.3.3");
|
||||
const ft = (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 = ut(t, this.renderRoot, this.renderOptions);
|
||||
this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(e), this._$Do = ft(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 ft = oe.litElementPolyfillSupport;
|
||||
ft?.({ LitElement: M });
|
||||
const mt = oe.litElementPolyfillSupport;
|
||||
mt?.({ LitElement: M });
|
||||
(oe.litElementVersions ??= []).push("4.2.2");
|
||||
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}';
|
||||
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}';
|
||||
function le(s) {
|
||||
window.customCards = window.customCards ?? [], window.customCards.push(s);
|
||||
}
|
||||
const mt = [
|
||||
const gt = [
|
||||
"nestquest_quest_completed",
|
||||
"nestquest_quest_uncompleted",
|
||||
"nestquest_quest_missed",
|
||||
@@ -524,7 +524,7 @@ function V(s, e) {
|
||||
}
|
||||
return xe.set(t, i), i;
|
||||
}
|
||||
function gt(s) {
|
||||
function _t(s) {
|
||||
const e = s?.config;
|
||||
if (!e || typeof e != "object")
|
||||
return;
|
||||
@@ -534,14 +534,14 @@ function gt(s) {
|
||||
function J(s, e) {
|
||||
return `${V(e, { weekday: "long" }).format(s)}, ${V(e, { month: "short", day: "numeric" }).format(s)}`;
|
||||
}
|
||||
function _t(s, e) {
|
||||
function yt(s, e) {
|
||||
return V(e, {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: !0
|
||||
}).format(s);
|
||||
}
|
||||
const yt = {
|
||||
const bt = {
|
||||
"clear-night": "Clear",
|
||||
cloudy: "Cloudy",
|
||||
exceptional: "Clear",
|
||||
@@ -557,7 +557,7 @@ const yt = {
|
||||
sunny: "Sunny",
|
||||
windy: "Windy",
|
||||
"windy-variant": "Windy"
|
||||
}, bt = {
|
||||
}, vt = {
|
||||
"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 vt(s) {
|
||||
function xt(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 xt(s) {
|
||||
function wt(s) {
|
||||
return s.split(/[-_]+/).filter(Boolean).map((e) => e.charAt(0).toUpperCase() + e.slice(1)).join(" ");
|
||||
}
|
||||
function wt(s, e) {
|
||||
function kt(s, e) {
|
||||
const t = V(e, {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
@@ -620,7 +620,7 @@ function wt(s, e) {
|
||||
);
|
||||
return Number.isFinite(i) ? i - s : 0;
|
||||
}
|
||||
function kt(s, e) {
|
||||
function $t(s, e) {
|
||||
if (typeof s != "string")
|
||||
return null;
|
||||
const t = s.split("-");
|
||||
@@ -633,17 +633,17 @@ function kt(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 - wt(r, e));
|
||||
const r = Date.UTC(n, i - 1, a), l = new Date(r - kt(r, e));
|
||||
return Number.isNaN(l.getTime()) ? null : l;
|
||||
}
|
||||
function $t(s) {
|
||||
const e = yt[s];
|
||||
function qt(s) {
|
||||
const e = bt[s];
|
||||
return e || s.charAt(0).toUpperCase() + s.slice(1);
|
||||
}
|
||||
function qt(s, e) {
|
||||
return bt[s] ?? e;
|
||||
function Ct(s, e) {
|
||||
return vt[s] ?? e;
|
||||
}
|
||||
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`
|
||||
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`
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -715,7 +715,7 @@ const qe = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Ct = "polygon(50%
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
clip-path: ${v(Ct)};
|
||||
clip-path: ${v(Mt)};
|
||||
background: var(--nq-brand-gradient);
|
||||
}
|
||||
|
||||
@@ -1038,7 +1038,7 @@ const qe = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Ct = "polygon(50%
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
}
|
||||
`, At = d`<svg
|
||||
`, St = 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%)", Ct = "polygon(50%
|
||||
>
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<path d="m9 12 2 2 4-4"></path>
|
||||
</svg>`, St = d`<svg
|
||||
</svg>`, Tt = 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%)", Ct = "polygon(50%
|
||||
>
|
||||
<path d="M3.5 21 12 3.5 20.5 21"></path>
|
||||
<path d="M9 21l3-8 3 8"></path>
|
||||
</svg>`, Tt = d`<svg
|
||||
</svg>`, zt = 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%)", Ct = "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>`, zt = d`<svg
|
||||
</svg>`, Et = 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%)", Ct = "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>`, Et = d`<svg
|
||||
</svg>`, Nt = 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%)", Ct = "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: zt,
|
||||
icon: Et,
|
||||
headline: "NestQuest is not set up yet",
|
||||
body: "A parent needs to finish setting up NestQuest before the party can gather."
|
||||
}, Nt = {
|
||||
icon: Et,
|
||||
}, Lt = {
|
||||
icon: Nt,
|
||||
headline: "The records cannot be reached",
|
||||
body: "The party's records are quiet right now. NestQuest will return shortly."
|
||||
}, Lt = d`<svg
|
||||
}, Bt = 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%)", Ct = "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 Bt extends M {
|
||||
class Ot extends M {
|
||||
static properties = {
|
||||
hass: { attribute: !1 },
|
||||
_config: { state: !0 },
|
||||
_now: { state: !0 }
|
||||
};
|
||||
static styles = [v(Qe), Mt];
|
||||
static styles = [v(Ve), At];
|
||||
hass;
|
||||
_config;
|
||||
_now = /* @__PURE__ */ new Date();
|
||||
@@ -1156,7 +1156,7 @@ class Bt extends M {
|
||||
t === this._subGeneration && (this._unsubscribeLive(), this._armSubscribeRetry());
|
||||
});
|
||||
};
|
||||
for (const i of mt)
|
||||
for (const i of gt)
|
||||
n(
|
||||
e.subscribeEvents(() => this.requestUpdate(), i)
|
||||
);
|
||||
@@ -1219,7 +1219,7 @@ class Bt extends M {
|
||||
`;
|
||||
}
|
||||
_timeZone() {
|
||||
return gt(this.hass);
|
||||
return _t(this.hass);
|
||||
}
|
||||
_state(e) {
|
||||
const n = this.hass?.states?.[e];
|
||||
@@ -1243,7 +1243,7 @@ class Bt extends M {
|
||||
const t = this._state(
|
||||
"sensor.nestquest_household_quests_due_today"
|
||||
)?.attributes?.child_roster;
|
||||
return Array.isArray(t) ? t.map((n) => vt(n)).filter((n) => n.length > 0) : [];
|
||||
return Array.isArray(t) ? t.map((n) => xt(n)).filter((n) => n.length > 0) : [];
|
||||
}
|
||||
_plates() {
|
||||
return this._childSlugs().map((e) => this._plate(e));
|
||||
@@ -1267,8 +1267,8 @@ class Bt extends M {
|
||||
const G = r?.attributes?.child_name;
|
||||
typeof G == "string" && G.trim() && (m = G.trim());
|
||||
}
|
||||
m || (m = xt(e));
|
||||
const x = kt(
|
||||
m || (m = wt(e));
|
||||
const x = $t(
|
||||
r?.attributes?.next_present,
|
||||
this._timeZone()
|
||||
), R = x ? `Returns ${J(x, this._timeZone())}` : null;
|
||||
@@ -1297,7 +1297,7 @@ class Bt extends M {
|
||||
}
|
||||
_boardNotice() {
|
||||
const e = this._plates();
|
||||
return e.length === 0 ? Ce : e.every((t) => t.unresolved !== null) ? e.some((t) => t.unresolved === "stale") ? Nt : Ce : null;
|
||||
return e.length === 0 ? Ce : e.every((t) => t.unresolved !== null) ? e.some((t) => t.unresolved === "stale") ? Lt : Ce : null;
|
||||
}
|
||||
_renderNotice(e) {
|
||||
return d`
|
||||
@@ -1320,7 +1320,7 @@ class Bt extends M {
|
||||
</span>
|
||||
<span class="name">${e.name}</span>
|
||||
<span class="pill away">
|
||||
${Tt}
|
||||
${zt}
|
||||
<span>Unknown</span>
|
||||
</span>
|
||||
<span class="progress-line">—</span>
|
||||
@@ -1340,7 +1340,7 @@ class Bt extends M {
|
||||
</span>
|
||||
<span class="name">${e.name}</span>
|
||||
<span class="pill">
|
||||
${At}
|
||||
${St}
|
||||
<span>Home today</span>
|
||||
</span>
|
||||
<span class="progress-line">
|
||||
@@ -1359,7 +1359,7 @@ class Bt extends M {
|
||||
</span>
|
||||
<span class="name">${e.name}</span>
|
||||
<span class="pill away">
|
||||
${St}
|
||||
${Tt}
|
||||
<span>On travels</span>
|
||||
</span>
|
||||
<span class="progress-line">${e.returns ?? "Returns —"}</span>
|
||||
@@ -1411,13 +1411,13 @@ class Bt extends M {
|
||||
<div class="dock">
|
||||
<span class="date">${J(this._now, this._timeZone())}</span>
|
||||
<span class="divider"></span>
|
||||
<span class="clock">${_t(this._now, this._timeZone())}</span>
|
||||
<span class="clock">${yt(this._now, this._timeZone())}</span>
|
||||
</div>
|
||||
`;
|
||||
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)}°`;
|
||||
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)}°`;
|
||||
return d`
|
||||
<div class="dock">
|
||||
${Lt}
|
||||
${Bt}
|
||||
${i}
|
||||
<span class="divider"></span>
|
||||
<span class="condition">
|
||||
@@ -1429,14 +1429,14 @@ class Bt extends M {
|
||||
`;
|
||||
}
|
||||
}
|
||||
customElements.define("nestquest-party-board-card", Bt);
|
||||
customElements.define("nestquest-party-board-card", Ot);
|
||||
le({
|
||||
type: "nestquest-party-board-card",
|
||||
name: "NestQuest Party Board",
|
||||
description: "Kids' attract screen for present and away adventurers."
|
||||
});
|
||||
const Ot = { CHILD: 2 }, Pt = (s) => (...e) => ({ _$litDirective$: s, values: e });
|
||||
let It = class {
|
||||
const Pt = { CHILD: 2 }, It = (s) => (...e) => ({ _$litDirective$: s, values: e });
|
||||
let Ht = class {
|
||||
constructor(e) {
|
||||
}
|
||||
get _$AU() {
|
||||
@@ -1452,11 +1452,11 @@ let It = class {
|
||||
return this.render(...t);
|
||||
}
|
||||
};
|
||||
const { I: Ht } = pt, Me = (s) => s, Ae = () => document.createComment(""), B = (s, e, t) => {
|
||||
const { I: jt } = ht, 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 Ht(a, r, s, s.options);
|
||||
t = new jt(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: Ht } = pt, Me = (s) => s, Ae = () => document.createComment(""), B =
|
||||
}
|
||||
}
|
||||
return t;
|
||||
}, q = (s, e, t = s) => (s._$AI(e, t), s), jt = {}, Rt = (s, e = jt) => s._$AH = e, Ut = (s) => s._$AH, Y = (s) => {
|
||||
}, q = (s, e, t = s) => (s._$AI(e, t), s), Rt = {}, Ut = (s, e = Rt) => s._$AH = e, Dt = (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 = Pt(class extends It {
|
||||
}, Te = It(class extends Ht {
|
||||
constructor(s) {
|
||||
if (super(s), s.type !== Ot.CHILD) throw Error("repeat() can only be used in text expressions");
|
||||
if (super(s), s.type !== Pt.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 = Ut(s), { values: a, keys: r } = this.dt(e, t, n);
|
||||
const i = Dt(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, Rt(s, o), S;
|
||||
return this.ut = r, Ut(s, o), S;
|
||||
}
|
||||
}), Dt = [
|
||||
}), Ft = [
|
||||
{
|
||||
kind: "lucide",
|
||||
key: "lucide:bed",
|
||||
@@ -1994,7 +1994,7 @@ const Se = (s, e, t) => {
|
||||
"m3 7 2 2 4-4"
|
||||
]
|
||||
}
|
||||
], Ft = [
|
||||
], Qt = [
|
||||
{
|
||||
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 }, 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 > Qt ? !1 : !e.some((t) => new RegExp("[\\s<>&]|\\p{Cc}", "u").test(t));
|
||||
}
|
||||
], 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) {
|
||||
const e = [...s];
|
||||
return e.length === 0 || e.length > Vt ? !1 : !e.some((t) => new RegExp("[\\s<>&]|\\p{Cc}", "u").test(t));
|
||||
}
|
||||
function Gt(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" ? Vt.get(n) ?? O : t === "emoji" && Zt(n) ? { kind: "emoji", key: s, text: n } : O;
|
||||
return t === "lucide" ? ze.get(n) ?? O : t === "fa" ? Zt.get(n) ?? O : t === "emoji" && Wt(n) ? { kind: "emoji", key: s, text: n } : O;
|
||||
}
|
||||
function Gt(s, e) {
|
||||
const t = Wt(s);
|
||||
function Ee(s, e) {
|
||||
const t = Gt(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"
|
||||
], Ee = /* @__PURE__ */ new Map();
|
||||
], Ne = /* @__PURE__ */ new Map();
|
||||
function E(s, e) {
|
||||
const t = `${s ?? "local"}|${JSON.stringify(e)}`, n = Ee.get(t);
|
||||
const t = `${s ?? "local"}|${JSON.stringify(e)}`, n = Ne.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 Ee.set(t, i), i;
|
||||
return Ne.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 Ne(s, e) {
|
||||
function Le(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 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`
|
||||
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`
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -2499,7 +2499,7 @@ const Le = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", dn = "polygon(50%
|
||||
width: 96px;
|
||||
height: 110px;
|
||||
padding: 3px;
|
||||
clip-path: ${v(Le)};
|
||||
clip-path: ${v(Be)};
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
}
|
||||
|
||||
@@ -2510,7 +2510,7 @@ const Le = "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(Le)};
|
||||
clip-path: ${v(Be)};
|
||||
background: var(--nq-p-crest);
|
||||
}
|
||||
|
||||
@@ -3341,7 +3341,7 @@ const Le = "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>`, Ve = d`<svg
|
||||
</svg>`, Ze = d`<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
@@ -3437,9 +3437,9 @@ const Le = "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."
|
||||
}, Oe = [
|
||||
}, Pe = [
|
||||
{ key: "morning", name: "Morning", range: "Until 11:59 AM", icon: un },
|
||||
{ key: "afternoon", name: "Afternoon", range: "12:00–5:00 PM", icon: Ve },
|
||||
{ key: "afternoon", name: "Afternoon", range: "12:00–5:00 PM", icon: Ze },
|
||||
{ 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(Qe), pn];
|
||||
static styles = [v(Ve), 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">
|
||||
${Ve}
|
||||
${Ze}
|
||||
<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">
|
||||
${Oe.map((t) => this._renderColumn(t))}
|
||||
${Pe.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 = Ne(
|
||||
), i = Le(
|
||||
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 = Ne(
|
||||
), x = Le(
|
||||
_?.attributes?.next_present,
|
||||
n
|
||||
);
|
||||
@@ -3915,10 +3915,10 @@ class kn extends M {
|
||||
}
|
||||
_renderQuest(e, t, n) {
|
||||
if (t) {
|
||||
const r = Be[n % Be.length], l = e.completed_at ? Yt(e.completed_at, this._timeZone()) : "";
|
||||
const r = Oe[n % Oe.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">${te}</span>
|
||||
<span class="tile" aria-hidden="true">${Ee(e.icon, 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">${Gt(e.icon, te)}</span>
|
||||
<span class="tile" aria-hidden="true">${Ee(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 = Oe.find((n) => n.key === e.window);
|
||||
const t = Pe.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 Ze(s) {
|
||||
function We(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 = Ze(s.url_path).replace(/^\/+|\/+$/g, "");
|
||||
const e = We(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 = Ze(e.weather_entity), a = {
|
||||
const n = `/${Mn(e)}`, i = We(e.weather_entity), a = {
|
||||
type: "custom:nestquest-party-board-card",
|
||||
quest_log_path: n
|
||||
};
|
||||
|
||||
+2
-27
@@ -84,33 +84,8 @@ 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 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).
|
||||
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.
|
||||
|
||||
## 4. Schedule (1f)
|
||||
|
||||
|
||||
@@ -78,6 +78,5 @@ 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 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.
|
||||
- Icon per quest definition is chosen by the parent from a Lucide subset; the subset is
|
||||
not yet defined (`ADMIN-SPEC.md` §3.3).
|
||||
|
||||
@@ -35,7 +35,7 @@ implementer's call), shaped:
|
||||
{
|
||||
"instances": [
|
||||
{ "id": 4821, "definition_id": 12, "child_id": 3, "title": "Fill bird feeder",
|
||||
"icon": "lucide:bird", "window": "afternoon", "due_time": "17:00",
|
||||
"icon": "bird", "window": "afternoon", "due_time": "17:00",
|
||||
"state": "open", "overdue": true, "completed_at": null, "on_time": null }
|
||||
]
|
||||
}
|
||||
|
||||
+2
-11
@@ -91,14 +91,7 @@ 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`, 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.
|
||||
1. **Icon tile** 64×64, radius 10px, `--nq-p-icon-tile`, Lucide glyph 34px white.
|
||||
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,
|
||||
@@ -113,9 +106,7 @@ 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`. 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-icon-tile-done` with the glyph in `--nq-p-ink-secondary`. 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`.
|
||||
|
||||
|
||||
@@ -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">${ICON_STAR}</span>
|
||||
<span class="tile" aria-hidden="true">${renderQuestIcon(instance.icon, ICON_STAR)}</span>
|
||||
<div class="body">
|
||||
<span class="quest-title">${instance.title}</span>
|
||||
<span class="meta">
|
||||
|
||||
@@ -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, 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
|
||||
* 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
|
||||
* assertions run against the exact bundle HACS ships.
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
@@ -56,22 +56,6 @@ 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;
|
||||
|
||||
@@ -6,9 +6,6 @@ lets legacy bare Lucide names render without a data migration.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from custom_components.nestquest.core.icons import (
|
||||
@@ -73,8 +70,6 @@ def test_validate_icon_clears(value: str | None) -> None:
|
||||
"emoji:a&b", # entity start
|
||||
"emoji:🐕\x00", # control character
|
||||
"emoji:🐕\x07🐕", # control character between emoji
|
||||
"emoji:\ufeff", # U+FEFF BOM: JS \s matches it, so reject to agree
|
||||
"emoji:🐕\ufeff", # trailing U+FEFF
|
||||
# Edge whitespace/controls are part of the value, never trimmed away.
|
||||
"emoji:🐕 ", # trailing space
|
||||
"emoji:🐕\n", # trailing newline
|
||||
@@ -114,39 +109,3 @@ def test_validate_icon_rejects_non_strings(value: object) -> None:
|
||||
)
|
||||
def test_normalize_icon_for_read(value: str | None, expected: str | None) -> None:
|
||||
assert normalize_icon_for_read(value) == expected
|
||||
|
||||
|
||||
# The emoji-payload rule is duplicated in the generated JS resolver
|
||||
# (``isEmojiPayload`` in tools/icons/generate.mjs). Both suites assert
|
||||
# this one table — admin/src/icons/registry.test.ts runs it through
|
||||
# resolveIcon — so a divergence between the runtimes fails a test.
|
||||
_EMOJI_PARITY_CASES = json.loads(
|
||||
(
|
||||
Path(__file__).parent.parent / "tools" / "icons" / "emoji-payload-cases.json"
|
||||
).read_text(encoding="utf-8")
|
||||
)["cases"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"case", _EMOJI_PARITY_CASES, ids=[case["why"] for case in _EMOJI_PARITY_CASES]
|
||||
)
|
||||
def test_validate_icon_emoji_verdict_matches_shared_table(case: dict) -> None:
|
||||
value = "emoji:" + case["payload"]
|
||||
if case["valid"]:
|
||||
assert validate_icon(value) == value
|
||||
else:
|
||||
with pytest.raises(ValueError, match=r"^icon\b"):
|
||||
validate_icon(value)
|
||||
|
||||
|
||||
def test_emoji_parity_table_covers_the_contract_edges() -> None:
|
||||
payloads = {case["payload"]: case["valid"] for case in _EMOJI_PARITY_CASES}
|
||||
assert payloads["\ufeff"] is False
|
||||
assert payloads["🐕 "] is False
|
||||
assert payloads["🐕\x00"] is False
|
||||
assert payloads["<b>"] is False and payloads["a>b"] is False
|
||||
assert payloads["a&b"] is False
|
||||
assert payloads["⭐" * EMOJI_MAX_CODE_POINTS] is True
|
||||
assert payloads["⭐" * (EMOJI_MAX_CODE_POINTS + 1)] is False
|
||||
family = "👨\u200d👩\u200d👧\u200d👦"
|
||||
assert payloads[family] is True
|
||||
|
||||
@@ -886,115 +886,3 @@ 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:]
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"_comment": "Shared emoji-payload verdicts. tests/test_core_icons.py (validate_icon) and admin/src/icons/registry.test.ts (resolveIcon) both assert every row, so the Python and JS rules cannot diverge silently.",
|
||||
"cases": [
|
||||
{ "payload": "🐕", "valid": true, "why": "plain emoji" },
|
||||
{ "payload": "👨👩👧👦", "valid": true, "why": "ZWJ family (U+200D allowed)" },
|
||||
{ "payload": "⭐⭐⭐⭐⭐⭐⭐⭐", "valid": true, "why": "exactly 8 code points" },
|
||||
{ "payload": "⭐⭐⭐⭐⭐⭐⭐⭐⭐", "valid": false, "why": "9 code points, over the cap" },
|
||||
{ "payload": "", "valid": false, "why": "empty" },
|
||||
{ "payload": "", "valid": false, "why": "U+FEFF BOM alone" },
|
||||
{ "payload": "🐕", "valid": false, "why": "trailing U+FEFF" },
|
||||
{ "payload": "🐕 ", "valid": false, "why": "trailing space" },
|
||||
{ "payload": "a b", "valid": false, "why": "inner space" },
|
||||
{ "payload": "🐕 ", "valid": false, "why": "no-break space" },
|
||||
{ "payload": "🐕 ", "valid": false, "why": "ideographic space" },
|
||||
{ "payload": "🐕
", "valid": false, "why": "line separator" },
|
||||
{ "payload": "🐕\n", "valid": false, "why": "newline" },
|
||||
{ "payload": "🐕\u0000", "valid": false, "why": "NUL control" },
|
||||
{ "payload": "🐕\u0007🐕", "valid": false, "why": "BEL control" },
|
||||
{ "payload": "🐕\u001c", "valid": false, "why": "file-separator control" },
|
||||
{ "payload": "🐕\u0085", "valid": false, "why": "NEL C1 control" },
|
||||
{ "payload": "🐕\u007f", "valid": false, "why": "DEL control" },
|
||||
{ "payload": "<b>", "valid": false, "why": "markup <" },
|
||||
{ "payload": "a>b", "valid": false, "why": "markup >" },
|
||||
{ "payload": "a&b", "valid": false, "why": "entity start &" }
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user