- ${Ht}
+ ${Bt}
No quests today
${`Nothing was posted for today, ${t.name}. Enjoy the day's rest!`}
@@ -2876,7 +2912,7 @@ class un extends A {
return d`
${this._renderHeader(t)}
- ${Ot.map((e) => this._renderColumn(e))}
+ ${zt.map((e) => this._renderColumn(e))}
`;
}
@@ -2942,14 +2978,14 @@ class un extends A {
`;
}
_timeZone() {
- return De(this.hass);
+ return Fe(this.hass);
}
_state(t) {
const n = this.hass?.states?.[t];
return n && typeof n == "object" ? n : null;
}
_childSlug() {
- return Ze();
+ return Ke();
}
_boardPath() {
return w(this._config?.board_path).replace(/\/+$/, "");
@@ -2985,7 +3021,7 @@ class un extends A {
const e = this._state(`sensor.nestquest_${t}_quests_due_today`)?.attributes?.instances;
if (!Array.isArray(e))
return [];
- const n = e.map(Ge).filter((r) => r !== null);
+ const n = e.map(Je).filter((r) => r !== null);
if (this._optimistic.size === 0)
return n;
let i = !1;
@@ -3070,11 +3106,11 @@ class un extends A {
if (!u) {
const _ = this._state(
`binary_sensor.nestquest_${l}_present_today`
- ), x = Et(
+ ), v = Et(
_?.attributes?.next_present,
n
);
- m = x ? He(x, n) : null;
+ m = v ? Qe(v, n) : null;
}
i.push({
slug: l,
@@ -3160,12 +3196,12 @@ class un extends A {
${i.length}/${e.length}
- ${St(
+ ${Tt(
n,
(l) => l.id,
(l) => this._renderQuest(l, !1, 0)
)}
- ${St(
+ ${Tt(
i,
(l) => l.id,
(l, a) => this._renderQuest(l, !0, a)
@@ -3177,7 +3213,7 @@ class un extends A {
}
_renderQuest(t, e, n) {
if (e) {
- const o = Mt[n % Mt.length], l = t.completed_at ? Be(t.completed_at, this._timeZone()) : "";
+ const o = Ot[n % Ot.length], l = t.completed_at ? Ze(t.completed_at, this._timeZone()) : "";
return d`
${tt}
@@ -3197,7 +3233,7 @@ class un extends A {
`;
}
- const i = Qe(t.due_time), r = t.overdue ? {
+ const i = Ge(t.due_time), r = t.overdue ? {
text: i ? `Overdue · due ${i}` : "Overdue",
late: !0
} : {
@@ -3252,7 +3288,7 @@ class un extends A {
);
if (!t)
return g;
- const e = Ot.find((n) => n.key === t.window);
+ const e = zt.find((n) => n.key === t.window);
return d`
this._closeConfirm()}>
this._closeConfirm()}
>
- ${en}
+ ${rn}
Not yet
`;
- const e = Je(t.condition), n = Xe(t.condition, e), i = t.temperature === null ? g : d`
${Math.round(t.temperature)}°`, r = t.high === null || t.low === null ? null : `${Math.round(t.high)}° / ${Math.round(t.low)}°`;
+ const e = tn(t.condition), n = en(t.condition, e), i = t.temperature === null ? g : d`
${Math.round(t.temperature)}°`, r = t.high === null || t.low === null ? null : `${Math.round(t.high)}° / ${Math.round(t.low)}°`;
return d`
- ${cn}
+ ${hn}
${i}
@@ -3460,19 +3496,19 @@ class un extends A {
this._logView().kind !== "complete-day" && this._armIdle(), this._confirm !== null && this._armConfirmTimer();
};
}
-customElements.define("nestquest-quest-log-card", un);
+customElements.define("nestquest-quest-log-card", gn);
at({
type: "nestquest-quest-log-card",
name: "NestQuest Quest Log",
description: "One child's daily quests, grouped by window."
});
-const fn = ":host{--nq-brand-blue: #3B3AB8;--nq-brand-purple: #A035CC;--nq-brand-gradient: linear-gradient(135deg, var(--nq-brand-blue), var(--nq-brand-purple));--nq-brand-gradient-h: linear-gradient(90deg, var(--nq-brand-blue), var(--nq-brand-purple));--nq-a-page: #F4F4F6;--nq-a-surface: #FFFFFF;--nq-a-surface-subtle: #FAFAFA;--nq-a-selected: #EBEBF8;--nq-a-border: #E4E4EA;--nq-a-divider: #EDEDF1;--nq-a-ink: #18181D;--nq-a-ink-secondary: #52525E;--nq-a-ink-tertiary: #70707E;--nq-a-success: #15803D;--nq-a-danger: #DC2626;--nq-a-danger-strong: #B91C1C;--nq-a-danger-bg: #FEE2E2;--nq-a-warning-ink: #92400E;--nq-a-warning-ink-2: #78350F;--nq-a-warning-bg: #FEF3C7;--nq-a-warning-border: rgba(217,119,6,.35);--nq-a-info-bg: #DBEAFE;--nq-a-info-ink: #1E3A8A;--nq-a-info-icon: #1D4ED8;--nq-a-reversal: #7D28A0;--nq-a-font: Nunito, system-ui, sans-serif;--nq-a-size-screen: 24px;--nq-a-size-hero: 32px;--nq-a-size-card-title: 19px;--nq-a-size-stat: 22px;--nq-a-size-row: 13.5px;--nq-a-size-meta: 11px;--nq-a-size-label: 10px;--nq-a-track-label: .1em;--nq-a-density-page-pad: 14px 16px 92px;--nq-a-density-gap: 10px;--nq-a-density-card-pad: 13px;--nq-a-density-row-pad: 11px 13px;--nq-a-density-radius: 8px;--nq-a-density-shadow: none;--nq-a-tabbar-height: 70px;--nq-a-tap-min: 44px;--nq-a-radius-pill: 9999px;--nq-a-radius-sheet: 20px 20px 0 0;--nq-a-sheet-shadow: 0 -8px 28px rgba(9,9,11,.18);--nq-a-progress-height: 6px;--nq-ease-out: cubic-bezier(0, 0, .2, 1);--nq-dur-micro: .12s;--nq-dur-base: .2s;--nq-dur-modal: .35s}";
-class mn extends A {
+const _n = ":host{--nq-brand-blue: #3B3AB8;--nq-brand-purple: #A035CC;--nq-brand-gradient: linear-gradient(135deg, var(--nq-brand-blue), var(--nq-brand-purple));--nq-brand-gradient-h: linear-gradient(90deg, var(--nq-brand-blue), var(--nq-brand-purple));--nq-a-page: #F4F4F6;--nq-a-surface: #FFFFFF;--nq-a-surface-subtle: #FAFAFA;--nq-a-selected: #EBEBF8;--nq-a-border: #E4E4EA;--nq-a-divider: #EDEDF1;--nq-a-ink: #18181D;--nq-a-ink-secondary: #52525E;--nq-a-ink-tertiary: #70707E;--nq-a-success: #15803D;--nq-a-danger: #DC2626;--nq-a-danger-strong: #B91C1C;--nq-a-danger-bg: #FEE2E2;--nq-a-warning-ink: #92400E;--nq-a-warning-ink-2: #78350F;--nq-a-warning-bg: #FEF3C7;--nq-a-warning-border: rgba(217,119,6,.35);--nq-a-info-bg: #DBEAFE;--nq-a-info-ink: #1E3A8A;--nq-a-info-icon: #1D4ED8;--nq-a-reversal: #7D28A0;--nq-a-font: Nunito, system-ui, sans-serif;--nq-a-size-screen: 24px;--nq-a-size-hero: 32px;--nq-a-size-card-title: 19px;--nq-a-size-stat: 22px;--nq-a-size-row: 13.5px;--nq-a-size-meta: 11px;--nq-a-size-label: 10px;--nq-a-track-label: .1em;--nq-a-density-page-pad: 14px 16px 92px;--nq-a-density-gap: 10px;--nq-a-density-card-pad: 13px;--nq-a-density-row-pad: 11px 13px;--nq-a-density-radius: 8px;--nq-a-density-shadow: none;--nq-a-tabbar-height: 70px;--nq-a-tap-min: 44px;--nq-a-radius-pill: 9999px;--nq-a-radius-sheet: 20px 20px 0 0;--nq-a-sheet-shadow: 0 -8px 28px rgba(9,9,11,.18);--nq-a-progress-height: 6px;--nq-ease-out: cubic-bezier(0, 0, .2, 1);--nq-dur-micro: .12s;--nq-dur-base: .2s;--nq-dur-modal: .35s}";
+class yn extends A {
static properties = {
hass: { attribute: !1 },
_config: { state: !0 }
};
- static styles = [v(fn)];
+ static styles = [x(_n)];
hass;
_config;
setConfig(t) {
@@ -3487,38 +3523,38 @@ class mn extends A {
return d`NestQuest Admin
`;
}
}
-customElements.define("nestquest-admin-card", mn);
+customElements.define("nestquest-admin-card", yn);
at({
type: "nestquest-admin-card",
name: "NestQuest Admin",
description: "Parent phone view for today, tasks, schedule, and history."
});
-function Bt(s) {
+function Ft(s) {
return typeof s == "string" ? s.trim() : "";
}
-function gn(s) {
+function bn(s) {
if (s === null || typeof s != "object" || Array.isArray(s))
return "";
const t = s.slug;
return typeof t == "string" ? t.trim() : "";
}
-function _n(s) {
- const t = Bt(s.url_path).replace(/^\/+|\/+$/g, "");
+function vn(s) {
+ const t = Ft(s.url_path).replace(/^\/+|\/+$/g, "");
return t || (window.location.pathname.split("/").filter(Boolean)[0] ?? "");
}
-function yn(s) {
+function xn(s) {
if (s === null || typeof s != "object" || Array.isArray(s))
return "";
const t = s.name;
return typeof t == "string" ? t.trim() : "";
}
-function bn(s) {
+function wn(s) {
const n = s?.states?.["sensor.nestquest_household_quests_due_today"]?.attributes?.child_roster;
- return Array.isArray(n) ? n.map((i) => ({ slug: gn(i), name: yn(i) })).filter((i) => i.slug.length > 0) : [];
+ return Array.isArray(n) ? n.map((i) => ({ slug: bn(i), name: xn(i) })).filter((i) => i.slug.length > 0) : [];
}
-class vn {
+class $n {
static async generate(t, e) {
- const n = `/${_n(t)}`, i = Bt(t.weather_entity), r = {
+ const n = `/${vn(t)}`, i = Ft(t.weather_entity), r = {
type: "custom:nestquest-party-board-card",
quest_log_path: n
};
@@ -3535,7 +3571,7 @@ class vn {
type: "panel",
cards: [r]
},
- ...bn(e).map((l) => ({
+ ...wn(e).map((l) => ({
path: l.slug,
title: l.name ? `${l.name}'s Quest Log` : `${l.slug}'s Quest Log`,
type: "panel",
@@ -3546,9 +3582,9 @@ class vn {
}
}
window.customStrategies = window.customStrategies ?? {};
-window.customStrategies["nestquest-party"] = vn;
-const xn = "/nestquest-static/nestquest-fonts.css";
+window.customStrategies["nestquest-party"] = $n;
+const qn = "/nestquest-static/nestquest-fonts.css";
if (!document.querySelector('link[data-nq-fonts=""]')) {
const s = document.createElement("link");
- s.rel = "stylesheet", s.href = xn, s.dataset.nqFonts = "", document.head.appendChild(s);
+ s.rel = "stylesheet", s.href = qn, s.dataset.nqFonts = "", document.head.appendChild(s);
}
diff --git a/design/PANEL-SPEC.md b/design/PANEL-SPEC.md
index 4e5f14b..a4d4355 100644
--- a/design/PANEL-SPEC.md
+++ b/design/PANEL-SPEC.md
@@ -22,10 +22,14 @@ Screens `1a`, `1b`, `1j`, `1c` of the mockups. Two cards:
Default view on the wall. Layout top to bottom:
-1. **Wordmark row** at y=64. Rule · d20 hexagon · `NestQuest` (66px/900 display, brand
- gradient via `background-clip: text`) · d20 · rule. d20 marks are 34px hexagons
- (`clip-path: polygon(50% 0, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%)`) filled with
- the brand gradient, numeral 15px/700 Cinzel.
+1. **Wordmark row** at y=64. Rule · monogram · `NestQuest` (66px/900 Cinzel Decorative
+ display, brand gradient via `background-clip: text`) · rule — the horizontal lockup of
+ `design/logos/NestQuest_Logo.png`. The monogram is an 80px inline SVG (no raster, no
+ remote asset): a compass rose — four long cardinal points, four short diagonal points,
+ a ring broken by the points — around a d20 hexagon with facet lines and a knocked-out
+ Cinzel Decorative "N", filled with the brand gradient (`--nq-brand-blue` →
+ `--nq-brand-purple`, 135°). Gaps are SVG masks, so the mark stays transparent over the
+ parchment.
2. **Kicker** `The Party · , ` — 21px/600 Cinzel, tracking .30em, uppercase.
3. **Adventurer plates**, 3-up grid, 56px gap, page inset 110px, top y=262. One plate per
child returned by the party sensor, in configured order.
diff --git a/frontend/src/cards/party-board.ts b/frontend/src/cards/party-board.ts
index 0a7569b..8a7c8eb 100644
--- a/frontend/src/cards/party-board.ts
+++ b/frontend/src/cards/party-board.ts
@@ -231,8 +231,76 @@ function conditionPhrase(condition: string, label: string): string {
}
const SHIELD_CLIP = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)";
-const HEXAGON_CLIP =
- "polygon(50% 0, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%)";
+// The brand monogram (design/logos/NestQuest_Logo.png): a compass rose —
+// four long cardinal points, four short diagonal points, a ring broken by
+// the points — around a d20 hexagon carrying a knocked-out "N". Drawn on
+// a 100-unit grid and filled with the brand gradient; the masks cut the
+// parchment gaps so the mark stays transparent on any backdrop.
+const MONOGRAM_POINTS =
+ "M50 3 44.5 24.1 50 26 55.5 24.1Z M100 50 75.9 44.5 74 50 75.9 55.5Z " +
+ "M50 97 55.5 75.9 50 74 44.5 75.9Z M0 50 24.1 55.5 26 50 24.1 44.5Z " +
+ "M29.1 29.1 29.7 36.3 36.3 29.7Z M70.9 29.1 63.7 29.7 70.3 36.3Z " +
+ "M70.9 70.9 70.3 63.7 63.7 70.3Z M29.1 70.9 36.3 70.3 29.7 63.7Z";
+const MONOGRAM_HEX = "M50 26.5 70.4 38.2 70.4 61.8 50 73.5 29.6 61.8 29.6 38.2Z";
+const MONOGRAM_FACETS =
+ "M29.6 38.2H70.4M29.6 61.8H70.4M50 26.5 36.5 38.2M50 26.5 63.5 38.2" +
+ "M50 73.5 36.5 61.8M50 73.5 63.5 61.8M36.5 38.2V61.8M63.5 38.2V61.8" +
+ "M29.6 38.2 36.5 61.8M36.5 38.2 29.6 61.8" +
+ "M70.4 38.2 63.5 61.8M63.5 38.2 70.4 61.8";
+const MONOGRAM = html``;
const boardStyles = css`
* {
@@ -299,23 +367,11 @@ const boardStyles = css`
background: rgba(92, 62, 26, 0.35);
}
- .d20 {
+ .monogram {
flex: none;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 34px;
- height: 34px;
- clip-path: ${unsafeCSS(HEXAGON_CLIP)};
- background: var(--nq-brand-gradient);
- }
-
- .d20-numeral {
- font-family: var(--nq-p-font-heading);
- font-size: 15px;
- font-weight: 700;
- line-height: 1;
- color: #ffffff;
+ display: block;
+ width: 80px;
+ height: 80px;
}
.wordmark {
@@ -862,13 +918,8 @@ export class NestQuestPartyBoardCard extends LitElement {
-
- 20
-
+ ${MONOGRAM}
NestQuest
-
- 20
-
The Party · ${formatDay(this._now, this._timeZone())}
diff --git a/tests/test_panel_cards.py b/tests/test_panel_cards.py
index 09e41c8..668327e 100644
--- a/tests/test_panel_cards.py
+++ b/tests/test_panel_cards.py
@@ -600,6 +600,40 @@ def _render_party_board(
return json.loads(completed.stdout)
+def test_party_board_wordmark_row_is_the_monogram_lockup() -> None:
+ """PANEL-SPEC §2 item 1: the wordmark row is rule · compass-rose
+ monogram · NestQuest · rule. The monogram is an inline SVG filled
+ with the brand gradient — the old d20 hexagon pair and its "20"
+ numerals are gone, and nothing is fetched from a remote URL or
+ embedded as a raster logo."""
+ source = _read(PARTY_BOARD)
+ assert "d20" not in _style_literal(source, STYLE_CONSTANTS[PARTY_BOARD])
+ assert 'class="d20' not in source
+ assert "HEXAGON_CLIP" not in source
+
+ html = _render_party_board(
+ {"type": "custom:nestquest-party-board-card"},
+ _three_child_states(),
+ )["html"]
+ row = re.search(r'(.*?)
', html, re.DOTALL)
+ assert row is not None
+ row_html = row.group(1)
+ classes = re.findall(r'<(?:span|svg) class="([^"]+)"', row_html)
+ assert classes == ["rule", "monogram", "wordmark", "rule"]
+ assert '" in row_html
+ assert "d20" not in row_html
+ assert "
![]()
20<" not in bundle
+ assert "data:image/png" not in bundle
+ assert not re.search(r"https?://", bundle)
+
+
def test_party_board_renders_discovered_children_in_sort_order() -> None:
"""No child_order key: three plates render in the roster's sort
order (the coordinator snapshot's order), one per child."""