Define and implement the panel's empty and error states
Define and implement the panel's empty and error states
This commit was merged in pull request #127.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { LitElement, css, html, nothing, unsafeCSS } from "lit";
|
||||
import { LitElement, css, html, nothing, unsafeCSS, type TemplateResult } from "lit";
|
||||
import panelTokens from "../../../custom_components/nestquest/www/nestquest-panel-tokens.css?inline";
|
||||
|
||||
import { registerCustomCard, type CardConfig, type HassLike } from "../types";
|
||||
@@ -17,6 +17,14 @@ type ChildPlate = {
|
||||
due: number;
|
||||
pct: number;
|
||||
returns: string | null;
|
||||
/** Set when the child's sensors cannot resolve; the plate shows "Unknown". */
|
||||
unresolved: "missing" | "stale" | null;
|
||||
};
|
||||
|
||||
type BoardNotice = {
|
||||
icon: TemplateResult;
|
||||
headline: string;
|
||||
body: string;
|
||||
};
|
||||
|
||||
type DockWeather = {
|
||||
@@ -476,6 +484,62 @@ const boardStyles = css`
|
||||
color: var(--nq-p-ink-secondary);
|
||||
}
|
||||
|
||||
.plate.unknown {
|
||||
border: 2px dashed var(--nq-p-panel-border);
|
||||
background: linear-gradient(#f2ecdd, #e6dcc6);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.notice-wrap {
|
||||
position: absolute;
|
||||
top: 262px;
|
||||
left: 110px;
|
||||
right: 110px;
|
||||
bottom: 190px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.notice {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
width: 100%;
|
||||
max-width: 980px;
|
||||
padding: 56px 64px;
|
||||
border: 2px solid var(--nq-p-panel-border);
|
||||
border-radius: var(--nq-p-radius-panel);
|
||||
background: var(--nq-p-card-panel);
|
||||
box-shadow: var(--nq-p-panel-shadow);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.notice svg {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
color: var(--nq-p-ink-secondary);
|
||||
}
|
||||
|
||||
.notice-headline {
|
||||
font-family: var(--nq-p-font-heading);
|
||||
font-size: 40px;
|
||||
font-weight: 700;
|
||||
line-height: 1.15;
|
||||
color: var(--nq-p-ink);
|
||||
}
|
||||
|
||||
.notice-body {
|
||||
margin: 0;
|
||||
font-family: var(--nq-p-font-body);
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
color: var(--nq-p-ink-secondary);
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
@@ -569,6 +633,58 @@ const ICON_TENT = html`<svg
|
||||
<path d="M9 21l3-8 3 8"></path>
|
||||
</svg>`;
|
||||
|
||||
const ICON_COMPASS = html`<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<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>`;
|
||||
|
||||
const ICON_ALERT = html`<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<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>`;
|
||||
|
||||
const ICON_CLOUD_OFF = html`<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<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>`;
|
||||
|
||||
const BOARD_NOTICE_NOT_SET_UP: BoardNotice = {
|
||||
icon: ICON_ALERT,
|
||||
headline: "NestQuest is not set up yet",
|
||||
body: "A parent needs to finish setting up NestQuest before the party can gather.",
|
||||
};
|
||||
|
||||
const BOARD_NOTICE_UNREACHABLE: BoardNotice = {
|
||||
icon: ICON_CLOUD_OFF,
|
||||
headline: "The records cannot be reached",
|
||||
body: "The party's records are quiet right now. NestQuest will return shortly.",
|
||||
};
|
||||
|
||||
const ICON_CLOUD_SUN = html`<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
@@ -628,6 +744,7 @@ export class NestQuestPartyBoardCard extends LitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
const notice = this._boardNotice();
|
||||
return html`
|
||||
<div class="board">
|
||||
<div class="frame frame-outer"></div>
|
||||
@@ -644,10 +761,14 @@ export class NestQuestPartyBoardCard extends LitElement {
|
||||
<span class="rule"></span>
|
||||
</div>
|
||||
<p class="kicker">The Party · ${formatDay(this._now, this._timeZone())}</p>
|
||||
<div class="plates">
|
||||
${this._plates().map((plate) => this._renderPlate(plate))}
|
||||
</div>
|
||||
<p class="hint">Tap your crest to open your Quest Log</p>
|
||||
${notice
|
||||
? this._renderNotice(notice)
|
||||
: html`
|
||||
<div class="plates">
|
||||
${this._plates().map((plate) => this._renderPlate(plate))}
|
||||
</div>
|
||||
<p class="hint">Tap your crest to open your Quest Log</p>
|
||||
`}
|
||||
<div class="spacer"></div>
|
||||
${this._renderDock()}
|
||||
</div>
|
||||
@@ -687,6 +808,7 @@ export class NestQuestPartyBoardCard extends LitElement {
|
||||
}
|
||||
|
||||
private _plate(slug: string): ChildPlate {
|
||||
const unresolved = this._dueSensorResolution(slug);
|
||||
const dueSensor = this._state(`sensor.nestquest_${slug}_quests_due_today`);
|
||||
const doneSensor = this._state(
|
||||
`sensor.nestquest_${slug}_quests_completed_today`
|
||||
@@ -744,15 +866,75 @@ export class NestQuestPartyBoardCard extends LitElement {
|
||||
slug,
|
||||
name,
|
||||
initial: (name.charAt(0) || "?").toUpperCase(),
|
||||
present,
|
||||
present: unresolved === null ? present : false,
|
||||
completed,
|
||||
due,
|
||||
pct,
|
||||
returns,
|
||||
unresolved,
|
||||
};
|
||||
}
|
||||
|
||||
/** "missing" when the due sensor does not exist (integration not
|
||||
* configured), "stale" when it answers unavailable/unknown (backend
|
||||
* unreachable or the child is absent from the snapshot), null when
|
||||
* it resolves. */
|
||||
private _dueSensorResolution(slug: string): "missing" | "stale" | null {
|
||||
const sensor = this._state(`sensor.nestquest_${slug}_quests_due_today`);
|
||||
if (!sensor) {
|
||||
return "missing";
|
||||
}
|
||||
const state = String(sensor.state ?? "").trim().toLowerCase();
|
||||
if (!state || state === "unavailable" || state === "unknown") {
|
||||
return "stale";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private _boardNotice(): BoardNotice | null {
|
||||
const plates = this._plates();
|
||||
if (plates.length === 0) {
|
||||
return BOARD_NOTICE_NOT_SET_UP;
|
||||
}
|
||||
if (plates.every((plate) => plate.unresolved !== null)) {
|
||||
return plates.some((plate) => plate.unresolved === "stale")
|
||||
? BOARD_NOTICE_UNREACHABLE
|
||||
: BOARD_NOTICE_NOT_SET_UP;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private _renderNotice(notice: BoardNotice) {
|
||||
return html`
|
||||
<div class="notice-wrap">
|
||||
<div class="notice" role="status">
|
||||
${notice.icon}
|
||||
<span class="notice-headline">${notice.headline}</span>
|
||||
<p class="notice-body">${notice.body}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderPlate(plate: ChildPlate) {
|
||||
if (plate.unresolved) {
|
||||
return html`
|
||||
<div class="plate unknown">
|
||||
<span class="crest away">
|
||||
<span class="crest-face">
|
||||
<span class="initial">${plate.initial}</span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="name">${plate.name}</span>
|
||||
<span class="pill away">
|
||||
${ICON_COMPASS}
|
||||
<span>Unknown</span>
|
||||
</span>
|
||||
<span class="progress-line">—</span>
|
||||
<span class="bar"></span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
if (plate.present) {
|
||||
return html`
|
||||
<button
|
||||
|
||||
+341
-25
@@ -36,6 +36,23 @@ type OtherChild = {
|
||||
returnsWeekday: string | null;
|
||||
};
|
||||
|
||||
/** The one state the log resolves before rendering anything; see
|
||||
* design/PANEL-EMPTY-STATES.md for the decisions behind each kind. */
|
||||
type LogView =
|
||||
| { kind: "no-adventurer" }
|
||||
| { kind: "not-set-up" }
|
||||
| { kind: "unreachable" }
|
||||
| { kind: "away"; name: string; returns: string | null }
|
||||
| { kind: "empty-day"; name: string }
|
||||
| { kind: "complete-day"; name: string }
|
||||
| { kind: "normal" };
|
||||
|
||||
type LogNotice = {
|
||||
icon: TemplateResult;
|
||||
headline: string;
|
||||
body: string;
|
||||
};
|
||||
|
||||
const FORMATTER_CACHE = new Map<string, Intl.DateTimeFormat>();
|
||||
|
||||
function zonedFormatter(
|
||||
@@ -299,6 +316,10 @@ const logStyles = css`
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.crest.away .crest-face {
|
||||
background: var(--nq-p-crest-away);
|
||||
}
|
||||
|
||||
.titles {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -646,6 +667,81 @@ const logStyles = css`
|
||||
color: var(--nq-p-ink-secondary);
|
||||
}
|
||||
|
||||
.notice-wrap {
|
||||
position: absolute;
|
||||
top: 236px;
|
||||
left: var(--nq-p-page-inset);
|
||||
right: var(--nq-p-page-inset);
|
||||
bottom: 130px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.notice {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
width: 100%;
|
||||
max-width: 980px;
|
||||
padding: 56px 64px;
|
||||
border: 2px solid var(--nq-p-panel-border);
|
||||
border-radius: 20px;
|
||||
background: var(--nq-p-card-panel);
|
||||
box-shadow: var(--nq-p-panel-shadow);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.notice svg {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
color: var(--nq-p-ink-secondary);
|
||||
}
|
||||
|
||||
.notice.away {
|
||||
background: linear-gradient(#f2ecdd, #e6dcc6);
|
||||
border-style: dashed;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.notice-headline {
|
||||
font-family: var(--nq-p-font-heading);
|
||||
font-size: 40px;
|
||||
font-weight: 700;
|
||||
line-height: 1.15;
|
||||
color: var(--nq-p-ink);
|
||||
}
|
||||
|
||||
.notice-body {
|
||||
margin: 0;
|
||||
font-family: var(--nq-p-font-body);
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
color: var(--nq-p-ink-secondary);
|
||||
}
|
||||
|
||||
.notice-seal {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 78px;
|
||||
height: 78px;
|
||||
border-radius: var(--nq-p-radius-pill);
|
||||
background: var(--nq-p-seal);
|
||||
box-shadow: var(--nq-p-seal-shadow);
|
||||
color: rgba(255, 235, 235, 0.95);
|
||||
transform: rotate(-6deg);
|
||||
}
|
||||
|
||||
.notice-seal svg {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
color: rgba(255, 235, 235, 0.95);
|
||||
}
|
||||
|
||||
.scrim {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
@@ -877,6 +973,77 @@ const ICON_MOON = html`<svg
|
||||
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"></path>
|
||||
</svg>`;
|
||||
|
||||
const ICON_TENT = html`<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M3.5 21 12 3.5 20.5 21"></path>
|
||||
<path d="M9 21l3-8 3 8"></path>
|
||||
</svg>`;
|
||||
|
||||
const ICON_COMPASS = html`<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<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>`;
|
||||
|
||||
const ICON_ALERT = html`<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<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>`;
|
||||
|
||||
const ICON_CLOUD_OFF = html`<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<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>`;
|
||||
|
||||
const LOG_NOTICE_NO_ADVENTURER: LogNotice = {
|
||||
icon: ICON_COMPASS,
|
||||
headline: "No adventurer chosen",
|
||||
body: "Open The Party and tap your crest to open your quest log.",
|
||||
};
|
||||
|
||||
const LOG_NOTICE_NOT_SET_UP: LogNotice = {
|
||||
icon: ICON_ALERT,
|
||||
headline: "NestQuest is not set up yet",
|
||||
body: "A parent needs to finish setting up NestQuest.",
|
||||
};
|
||||
|
||||
const LOG_NOTICE_UNREACHABLE: LogNotice = {
|
||||
icon: ICON_CLOUD_OFF,
|
||||
headline: "The records cannot be reached",
|
||||
body: "The party's records are quiet right now. NestQuest will return shortly.",
|
||||
};
|
||||
|
||||
const WINDOW_DEFS: WindowDef[] = [
|
||||
{ key: "morning", name: "Morning", range: "Until 11:59 AM", icon: ICON_SUNRISE },
|
||||
{ key: "afternoon", name: "Afternoon", range: "12:00–5:00 PM", icon: ICON_SUN },
|
||||
@@ -935,19 +1102,162 @@ export class NestQuestQuestLogCard extends LitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
const view = this._logView();
|
||||
return html`
|
||||
<div class="board">
|
||||
<div class="frame frame-outer"></div>
|
||||
<div class="frame frame-inner"></div>
|
||||
${this._renderHeader()}
|
||||
<div class="columns">
|
||||
${WINDOW_DEFS.map((windowDef) => this._renderColumn(windowDef))}
|
||||
</div>
|
||||
${this._renderMain(view)}
|
||||
${this._renderConfirm()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderMain(view: LogView): TemplateResult {
|
||||
switch (view.kind) {
|
||||
case "no-adventurer":
|
||||
return this._renderNotice(LOG_NOTICE_NO_ADVENTURER);
|
||||
case "not-set-up":
|
||||
return this._renderNotice(LOG_NOTICE_NOT_SET_UP);
|
||||
case "unreachable":
|
||||
return this._renderNotice(LOG_NOTICE_UNREACHABLE);
|
||||
case "away": {
|
||||
const body = ["The quest log unlocks when they return."];
|
||||
if (view.returns) {
|
||||
body.push(`Returns ${view.returns}`);
|
||||
}
|
||||
return html`
|
||||
${this._renderHeader(view)}
|
||||
<div class="notice-wrap">
|
||||
<div class="notice away" role="status">
|
||||
${ICON_TENT}
|
||||
<span class="notice-headline">${view.name} is on travels</span>
|
||||
${body.map((line) => html`<p class="notice-body">${line}</p>`)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
case "empty-day":
|
||||
return html`
|
||||
${this._renderHeader(view)}
|
||||
<div class="notice-wrap">
|
||||
<div class="notice" role="status">
|
||||
${ICON_SUN}
|
||||
<span class="notice-headline">No quests today</span>
|
||||
<p class="notice-body">
|
||||
${`Nothing was posted for today, ${view.name}. Enjoy the day's rest!`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
case "complete-day":
|
||||
return html`
|
||||
${this._renderHeader(view)}
|
||||
<div class="notice-wrap">
|
||||
<div class="notice" role="status">
|
||||
<span class="notice-seal">${ICON_CHECK}</span>
|
||||
<span class="notice-headline">Quest complete</span>
|
||||
<p class="notice-body">
|
||||
${`Every quest is claimed, ${view.name}. The seal is set for today.`}
|
||||
</p>
|
||||
<p class="notice-body">Returning to The Party shortly.</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
default:
|
||||
return html`
|
||||
${this._renderHeader(view)}
|
||||
<div class="columns">
|
||||
${WINDOW_DEFS.map((windowDef) => this._renderColumn(windowDef))}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolve the log's single render state; the priority order is
|
||||
* design/PANEL-EMPTY-STATES.md §2. */
|
||||
private _logView(): LogView {
|
||||
const slug = this._childSlug();
|
||||
if (!slug) {
|
||||
return { kind: "no-adventurer" };
|
||||
}
|
||||
const resolution = this._dueSensorResolution(slug);
|
||||
if (resolution === "missing") {
|
||||
return { kind: "not-set-up" };
|
||||
}
|
||||
if (resolution === "stale") {
|
||||
return { kind: "unreachable" };
|
||||
}
|
||||
const name = this._childName(slug) ?? titleCaseSlug(slug);
|
||||
if (!this._childPresent(slug)) {
|
||||
return { kind: "away", name, returns: this._awayReturns(slug) };
|
||||
}
|
||||
const dueSensor = this._state(`sensor.nestquest_${slug}_quests_due_today`);
|
||||
const due = clampCount(asNumber(dueSensor?.state, 0));
|
||||
if (due === 0) {
|
||||
return { kind: "empty-day", name };
|
||||
}
|
||||
if (this._remaining() === 0) {
|
||||
return { kind: "complete-day", name };
|
||||
}
|
||||
return { kind: "normal" };
|
||||
}
|
||||
|
||||
/** "missing" when the due sensor does not exist (integration not
|
||||
* configured), "stale" when it answers unavailable/unknown (backend
|
||||
* unreachable or the child is absent from the snapshot), null when
|
||||
* it resolves. */
|
||||
private _dueSensorResolution(slug: string): "missing" | "stale" | null {
|
||||
const sensor = this._state(`sensor.nestquest_${slug}_quests_due_today`);
|
||||
if (!sensor) {
|
||||
return "missing";
|
||||
}
|
||||
const state = String(sensor.state ?? "").trim().toLowerCase();
|
||||
if (!state || state === "unavailable" || state === "unknown") {
|
||||
return "stale";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private _childPresent(slug: string): boolean {
|
||||
const dueSensor = this._state(`sensor.nestquest_${slug}_quests_due_today`);
|
||||
const presentAttr = dueSensor?.attributes?.present;
|
||||
if (typeof presentAttr === "boolean") {
|
||||
return presentAttr;
|
||||
}
|
||||
const presence = this._state(
|
||||
`binary_sensor.nestquest_${slug}_present_today`
|
||||
);
|
||||
if (presence) {
|
||||
return String(presence.state ?? "").trim().toLowerCase() === "on";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _awayReturns(slug: string): string | null {
|
||||
const timeZone = this._timeZone();
|
||||
const presence = this._state(
|
||||
`binary_sensor.nestquest_${slug}_present_today`
|
||||
);
|
||||
const nextPresent = parseIsoDate(
|
||||
presence?.attributes?.next_present,
|
||||
timeZone
|
||||
);
|
||||
return nextPresent ? formatDay(nextPresent, timeZone) : null;
|
||||
}
|
||||
|
||||
private _renderNotice(notice: LogNotice): TemplateResult {
|
||||
return html`
|
||||
<div class="notice-wrap">
|
||||
<div class="notice" role="status">
|
||||
${notice.icon}
|
||||
<span class="notice-headline">${notice.headline}</span>
|
||||
<p class="notice-body">${notice.body}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _timeZone(): string | undefined {
|
||||
return hassTimeZone(this.hass);
|
||||
}
|
||||
@@ -1110,18 +1420,20 @@ export class NestQuestQuestLogCard extends LitElement {
|
||||
return result.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
private _renderHeader() {
|
||||
private _renderHeader(view: LogView) {
|
||||
const slug = this._childSlug();
|
||||
const name = this._childName(slug);
|
||||
const displayName = name ?? (slug ? titleCaseSlug(slug) : null);
|
||||
const away = view.kind === "away";
|
||||
const name = away
|
||||
? view.name
|
||||
: (this._childName(slug) ?? (slug ? titleCaseSlug(slug) : null));
|
||||
const { due, completed } = this._childCounts(slug);
|
||||
const remaining = this._remaining();
|
||||
const party = this._partyCounts();
|
||||
const title = displayName ? `${displayName}'s Quest Log` : "Quest Log";
|
||||
const initial = (displayName?.charAt(0) || "?").toUpperCase();
|
||||
const title = name ? `${name}'s Quest Log` : "Quest Log";
|
||||
const initial = (name?.charAt(0) || "?").toUpperCase();
|
||||
return html`
|
||||
<header class="header">
|
||||
<span class="crest" aria-hidden="true">
|
||||
<span class="crest${away ? " away" : nothing}" aria-hidden="true">
|
||||
<span class="crest-face">
|
||||
<span class="initial">${initial}</span>
|
||||
</span>
|
||||
@@ -1129,23 +1441,27 @@ export class NestQuestQuestLogCard extends LitElement {
|
||||
<div class="titles">
|
||||
<h1 class="title">${title}</h1>
|
||||
<p class="sub">
|
||||
${formatDay(this._now, this._timeZone())} · ${completed} of ${due}
|
||||
claimed
|
||||
${formatDay(this._now, this._timeZone())} ·
|
||||
${away ? "On travels" : `${completed} of ${due} claimed`}
|
||||
</p>
|
||||
</div>
|
||||
<div class="remaining">
|
||||
<span class="d20" aria-hidden="true">
|
||||
<span class="d20-numeral">${remaining}</span>
|
||||
</span>
|
||||
<div class="remaining-text">
|
||||
<span class="remaining-count">
|
||||
${remaining} quest${remaining === 1 ? "" : "s"} left
|
||||
</span>
|
||||
<span class="party-line">
|
||||
Party progress · ${party.completed} of ${party.due} today
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
${away
|
||||
? nothing
|
||||
: html`
|
||||
<div class="remaining">
|
||||
<span class="d20" aria-hidden="true">
|
||||
<span class="d20-numeral">${remaining}</span>
|
||||
</span>
|
||||
<div class="remaining-text">
|
||||
<span class="remaining-count">
|
||||
${remaining} quest${remaining === 1 ? "" : "s"} left
|
||||
</span>
|
||||
<span class="party-line">
|
||||
Party progress · ${party.completed} of ${party.due} today
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
</header>
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user