From c5b313a5e33d7d41d6e27a0ccff16a1777604b74 Mon Sep 17 00:00:00 2001 From: dev_agent <17+dev_agent@code.cubecraftcreations.com> Date: Sat, 26 Sep 2026 08:09:51 -0400 Subject: [PATCH 1/8] Fix hidden hint text behind the party-board dock (#244) --- .../nestquest/www/nestquest-cards.js | 5 ++-- frontend/src/cards/party-board.ts | 5 ++-- tests/test_panel_cards.py | 30 +++++++++++++++++++ 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/custom_components/nestquest/www/nestquest-cards.js b/custom_components/nestquest/www/nestquest-cards.js index d67e35d..8f0296d 100644 --- a/custom_components/nestquest/www/nestquest-cards.js +++ b/custom_components/nestquest/www/nestquest-cards.js @@ -906,11 +906,12 @@ const $t = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", $e = "polygon(50% .hint { position: absolute; - top: 930px; + top: 896px; left: 0; right: 0; + margin: 0; font-family: var(--nq-p-font-body); - font-size: 24px; + font-size: 27px; font-weight: 600; line-height: 1.4; text-align: center; diff --git a/frontend/src/cards/party-board.ts b/frontend/src/cards/party-board.ts index 65c7925..0a7569b 100644 --- a/frontend/src/cards/party-board.ts +++ b/frontend/src/cards/party-board.ts @@ -497,11 +497,12 @@ const boardStyles = css` .hint { position: absolute; - top: 930px; + top: 896px; left: 0; right: 0; + margin: 0; font-family: var(--nq-p-font-body); - font-size: 24px; + font-size: 27px; font-weight: 600; line-height: 1.4; text-align: center; diff --git a/tests/test_panel_cards.py b/tests/test_panel_cards.py index 2b01162..2ef824f 100644 --- a/tests/test_panel_cards.py +++ b/tests/test_panel_cards.py @@ -411,6 +411,36 @@ def test_panel_hard_rules_hold_in_the_card_sources() -> None: assert "@click" not in sealed_template +def test_party_board_hint_clears_the_dock() -> None: + """PANEL-SPEC §2/§2.3: the hint line reads fully above the dock at + 1920x1080. The hint's line box (top + font-size x line-height) must + end above the dock's top edge (1080 - bottom inset - dock height) + with a visible gap, at the 27px/600 design size. The hint is a
,
+ so its user-agent 1em top margin must be reset or it shifts the box
+ down by a whole font-size."""
+ style = _style_literal(_read(PARTY_BOARD), STYLE_CONSTANTS[PARTY_BOARD])
+ rules = {selector.strip(): body for selector, body in _css_rules(style)}
+ sizes = _size_tokens(_read(REWARD_TOKENS))
+
+ def px(body: str, prop: str) -> float:
+ declared = re.search(rf"(? None:
"""PANEL-SPEC §1: no body text below 22px. Every font-size in the
panel styles resolves against the panel size tokens or an explicit
--
2.55.0
From 9916d83138c963193fd2620684e04dc8367fdead Mon Sep 17 00:00:00 2001
From: dev_agent <17+dev_agent@code.cubecraftcreations.com>
Date: Sat, 26 Sep 2026 09:08:59 -0400
Subject: [PATCH 2/8] Fix the Quest Log header crest/shield to match the design
(resubmission of PR #247) (#252)
---
.../nestquest/www/nestquest-cards.js | 4 +-
frontend/src/cards/quest-log.ts | 4 +-
tests/test_panel_cards.py | 68 +++++++++++++++++++
3 files changed, 72 insertions(+), 4 deletions(-)
diff --git a/custom_components/nestquest/www/nestquest-cards.js b/custom_components/nestquest/www/nestquest-cards.js
index 8f0296d..b221b38 100644
--- a/custom_components/nestquest/www/nestquest-cards.js
+++ b/custom_components/nestquest/www/nestquest-cards.js
@@ -1765,7 +1765,7 @@ const Nt = "polygon(0 0, 100% 0, 100% 62%, 50% 100%, 0 62%)", Ye = "polygon(50%
flex: none;
width: 96px;
height: 110px;
- padding: 3px;
+ padding: 4px;
clip-path: ${v(Nt)};
background: rgba(255, 255, 255, 0.22);
}
@@ -3091,7 +3091,7 @@ class un extends A {
const e = this._childSlug(), n = t.kind === "away", i = n ? t.name : this._childName(e) ?? (e ? Y(e) : null), { due: r, completed: o } = this._childCounts(e), l = this._remaining(), a = this._partyCounts(), p = i ? `${i}'s Quest Log` : "Quest Log", u = (i?.charAt(0) || "?").toUpperCase();
return d`
${n}
${`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)}
The Party · ${formatDay(this._now, this._timeZone())} The Party · ${K(this._now, this._timeZone())} The Party · ${Y(this._now, this._timeZone())} ${n}
${`Nothing was posted for today, ${t.name}. Enjoy the day's rest!`}
@@ -2912,7 +2882,7 @@ class gn extends A {
return d`
${this._renderHeader(t)}
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."""
--
2.55.0
From 744d80e645d71e4c3cacc42c09115623f557a62d Mon Sep 17 00:00:00 2001
From: dev_agent <17+dev_agent@code.cubecraftcreations.com>
Date: Sat, 26 Sep 2026 09:37:01 -0400
Subject: [PATCH 4/8] Quest bugs: household-local overdue clock and the panel
Complete no-op (#254)
---
api/routes_admin.py | 54 +-
api/routes_panel.py | 27 +-
api/scheduler.py | 13 +-
custom_components/nestquest/core/const.py | 7 +
custom_components/nestquest/core/settings.py | 43 ++
.../nestquest/core/settings_store.py | 9 +-
.../nestquest/www/nestquest-cards.js | 616 +++++++++---------
frontend/tests/render-quest-log.mjs | 31 +-
frontend/tsconfig.json | 1 +
tests/test_api_admin_settings.py | 9 +-
tests/test_api_household_clock.py | 272 ++++++++
tests/test_panel_cards.py | 63 ++
12 files changed, 792 insertions(+), 353 deletions(-)
create mode 100644 tests/test_api_household_clock.py
diff --git a/api/routes_admin.py b/api/routes_admin.py
index b70e8eb..9f9890c 100644
--- a/api/routes_admin.py
+++ b/api/routes_admin.py
@@ -867,7 +867,7 @@ class AdminSettingsResponse(BaseModel):
"""The effective settings as the admin plane serializes them.
Mirrors the core :class:`~nestquest_core.settings.NestQuestSettings`
- — all TEN documented fields, resolved (every absent stored field
+ — all ELEVEN documented fields, resolved (every absent stored field
already fell back to its default in the core). The same shape both
the GET and the PATCH route answer with.
"""
@@ -882,6 +882,9 @@ class AdminSettingsResponse(BaseModel):
afternoon_reminder_enabled: bool
end_of_day_report_enabled: bool
celebration_enabled: bool
+ #: The household IANA time zone the API reads its clock in; ``""``
+ #: means the API host's local time.
+ timezone: str
class AdminMissedSweepResponse(BaseModel):
@@ -1178,6 +1181,26 @@ def _local_now() -> datetime.datetime:
return datetime.datetime.now().astimezone()
+async def _household_clock(
+ database: object,
+) -> tuple[object, datetime.datetime]:
+ """Return the stored settings and ONE household-local clock read.
+
+ The API host may run UTC while the household does not, so the one
+ :func:`_local_now` read is re-expressed in the stored settings'
+ ``timezone`` (:meth:`NestQuestSettings.household_now`); an unset
+ zone keeps the host's local time.
+ """
+ settings = await core_settings_store.load_settings(database)
+ return settings, settings.household_now(_local_now())
+
+
+async def _household_today(database: object) -> datetime.date:
+ """Return the household-local date of ONE :func:`_household_clock` read."""
+ _settings, now = await _household_clock(database)
+ return now.date()
+
+
def _rule_from_request(rule: AdminRuleRequest) -> core_recurrence.ScheduleRule:
"""Build the core ScheduleRule from the request's ``rule`` object.
@@ -1496,7 +1519,7 @@ async def admin_create_quest_definition(
response_model=AdminOccurrencesPreviewResponse,
)
async def admin_preview_occurrences(
- body: AdminOccurrencesPreviewRequest,
+ body: AdminOccurrencesPreviewRequest, request: Request
) -> AdminOccurrencesPreviewResponse:
"""Return the first ``count`` dates the supplied rule fires on.
@@ -1509,12 +1532,15 @@ async def admin_preview_occurrences(
call over the bounded window ``[start_date, start_date + 366
days]`` — ``start_date`` defaulting to ONE :func:`_local_now` clock
read — truncated to ``count``. Every core ``ValueError`` is 422
- (:func:`_raise_preview_error`). No database access.
+ (:func:`_raise_preview_error`). The only database access is the
+ settings read behind the household clock.
"""
start_date = (
body.start_date
if body.start_date is not None
- else _local_now().date().isoformat()
+ else (
+ await _household_today(request.app.state.db.database)
+ ).isoformat()
)
try:
rule = _rule_from_request(body.rule)
@@ -1839,7 +1865,7 @@ async def admin_presence_override_consequence(
body.start_date,
body.end_date,
body.is_present,
- today=_local_now().date(),
+ today=await _household_today(state.database),
)
)
except ValueError as error:
@@ -1926,7 +1952,7 @@ async def admin_uncomplete_instance(
"""
state: DatabaseState = request.app.state.db
database = state.database
- now = _local_now()
+ _settings, now = await _household_clock(database)
sub = claims.get("sub")
try:
result = await core_completion.uncomplete_instance(
@@ -1992,7 +2018,7 @@ async def admin_regenerate(
"""
state: DatabaseState = request.app.state.db
database = state.database
- today = _local_now().date()
+ today = await _household_today(state.database)
if body.scope == "household":
end_date = today + datetime.timedelta(
@@ -2077,7 +2103,7 @@ def _settings_response(
"""Serialize one core NestQuestSettings into the documented payload.
Explicit field-by-field (not ``dataclasses.asdict``) so the payload
- stays pinned to the ten documented fields even if the core
+ stays pinned to the eleven documented fields even if the core
dataclass later grows another one.
"""
return AdminSettingsResponse(
@@ -2091,6 +2117,7 @@ def _settings_response(
afternoon_reminder_enabled=settings.afternoon_reminder_enabled,
end_of_day_report_enabled=settings.end_of_day_report_enabled,
celebration_enabled=settings.celebration_enabled,
+ timezone=settings.timezone,
)
@@ -2122,7 +2149,7 @@ async def admin_history_query(
``ValueError`` is 422 (:func:`_raise_history_error`).
"""
state: DatabaseState = request.app.state.db
- today = _local_now().date()
+ today = await _household_today(state.database)
try:
rows = await core_history.query_history(
state.database, filter, start, end, today=today
@@ -2165,7 +2192,7 @@ async def admin_history_csv(
JSON route's (:func:`_raise_history_error`).
"""
state: DatabaseState = request.app.state.db
- today = _local_now().date()
+ today = await _household_today(state.database)
try:
csv_text = await core_history.export_history_csv(
state.database, filter, start, end, today=today
@@ -2188,7 +2215,7 @@ async def admin_history_csv(
response_model=AdminSettingsResponse,
)
async def admin_get_settings(request: Request) -> AdminSettingsResponse:
- """Return the current effective settings (all ten documented fields).
+ """Return the current effective settings (all eleven documented fields).
Requires a valid admin JWT (the router's shared
:func:`~api.auth.require_admin` dependency — the ONE check; this
@@ -2280,7 +2307,7 @@ async def admin_run_missed_sweep(
"""
state: DatabaseState = request.app.state.db
database = state.database
- today = _local_now().date()
+ today = await _household_today(state.database)
try:
events = await core_sweep.run_missed_sweep(database, today=today)
except ValueError as error:
@@ -2309,8 +2336,9 @@ async def admin_snapshot(request: Request) -> AdminSnapshotResponse:
:class:`AdminSnapshotResponse`.
"""
state: DatabaseState = request.app.state.db
+ settings, now = await _household_clock(state.database)
snapshot = await core_snapshot.build_snapshot(
- state.database, core_settings.NestQuestSettings(), _local_now()
+ state.database, settings, now
)
return AdminSnapshotResponse(
today_iso=snapshot.today_iso,
diff --git a/api/routes_panel.py b/api/routes_panel.py
index 6405384..200c431 100644
--- a/api/routes_panel.py
+++ b/api/routes_panel.py
@@ -8,9 +8,9 @@ this router inherits the check).
The snapshot route is a thin adapter over the bundled core: it performs
NO business logic. It reads the live database off ``app.state.db``,
-resolves ONE timezone-aware ``now`` (the API host's local time — the
-API is the household's "local" host now that the domain no longer lives
-inside Home Assistant), calls the pure
+resolves ONE timezone-aware ``now`` (the host clock re-expressed in the
+stored household ``timezone`` setting — the API host may run UTC while
+the household does not), calls the pure
:func:`nestquest_core.snapshot.build_snapshot`, shapes each child's
instances through :func:`nestquest_core.snapshot.instance_payload` with
``include_missed=False`` (D-009: missed instances are OMITTED from the
@@ -63,7 +63,7 @@ from api.dependencies import require_panel_token
from api.nestquest_core import (
core_completion,
core_events,
- core_settings,
+ core_settings_store,
core_snapshot,
)
from api import transitions as api_transitions
@@ -178,6 +178,20 @@ def _local_now() -> datetime.datetime:
return datetime.datetime.now().astimezone()
+async def _household_clock(
+ database: object,
+) -> tuple[object, datetime.datetime]:
+ """Return the stored settings and ONE household-local clock read.
+
+ The API host may run UTC while the household does not, so the one
+ :func:`_local_now` read is re-expressed in the stored settings'
+ ``timezone`` (:meth:`NestQuestSettings.household_now`); an unset
+ zone keeps the host's local time.
+ """
+ settings = await core_settings_store.load_settings(database)
+ return settings, settings.household_now(_local_now())
+
+
@router.get(
"/snapshot",
summary="Today's household snapshot",
@@ -196,8 +210,9 @@ async def panel_snapshot(request: Request) -> PanelSnapshotResponse:
:class:`PanelSnapshotResponse`.
"""
state: DatabaseState = request.app.state.db
+ settings, now = await _household_clock(state.database)
snapshot = await core_snapshot.build_snapshot(
- state.database, core_settings.NestQuestSettings(), _local_now()
+ state.database, settings, now
)
return PanelSnapshotResponse(
today_iso=snapshot.today_iso,
@@ -282,7 +297,7 @@ async def panel_complete_instance(
detail="Quest instance not found for this child",
)
- now = _local_now()
+ _settings, now = await _household_clock(database)
try:
result = await core_completion.complete_instance(
database,
diff --git a/api/scheduler.py b/api/scheduler.py
index 32dcc07..092447b 100644
--- a/api/scheduler.py
+++ b/api/scheduler.py
@@ -8,7 +8,8 @@ configured ``day_rollover_time`` — read fresh from the settings store
at the top of EVERY cycle, so an admin's settings change applies on
the next scheduling decision without a restart — then runs the
HA-free core's :func:`nestquest_core.sweep.run_missed_sweep` for the
-API host's local date and publishes each returned
+household-local date (the stored ``timezone`` setting; the API host's
+local date when unset) and publishes each returned
``nestquest_quest_missed`` event on the app's ONE transition publisher
(the same SSE stream every route publishes through).
@@ -130,7 +131,11 @@ class MissedSweepScheduler:
)
await self._sleep(_RETRY_BACKOFF_SECONDS)
continue
- delay = _delay_seconds(rollover, self._clock())
+ # The host may run UTC: the rollover is a household wall-clock
+ # time, so the clock read is re-expressed in the stored zone.
+ delay = _delay_seconds(
+ rollover, settings.household_now(self._clock())
+ )
LOGGER.info(
"NestQuest missed sweep scheduled for %s (%.0fs from now)",
settings.day_rollover_time,
@@ -138,7 +143,9 @@ class MissedSweepScheduler:
)
await self._sleep(delay)
try:
- await self._run_once(self._clock().date())
+ await self._run_once(
+ settings.household_now(self._clock()).date()
+ )
except Exception:
# Not silent either: the sweep error is logged and the
# loop backs off before the next cycle, whose watermark
diff --git a/custom_components/nestquest/core/const.py b/custom_components/nestquest/core/const.py
index d8aba4b..5074190 100644
--- a/custom_components/nestquest/core/const.py
+++ b/custom_components/nestquest/core/const.py
@@ -112,6 +112,13 @@ CONF_AFTERNOON_REMINDER_ENABLED = "afternoon_reminder_enabled"
CONF_END_OF_DAY_REPORT_ENABLED = "end_of_day_report_enabled"
CONF_CELEBRATION_ENABLED = "celebration_enabled"
DEFAULT_AUTOMATION_ENABLED = True
+# The household's IANA time zone (e.g. ``America/New_York``) the API
+# service reads its clock in. The empty string keeps the historical
+# behaviour — the API host's own local time — so an existing install is
+# unchanged until the owner sets it. The integration itself always uses
+# Home Assistant's configured ``time_zone`` and ignores this field.
+CONF_TIMEZONE = "timezone"
+DEFAULT_TIMEZONE = ""
# Panel-plane API client (Feature 18): the integration reaches the
# NestQuest API service (Feature 16) over HTTP instead of the database.
diff --git a/custom_components/nestquest/core/settings.py b/custom_components/nestquest/core/settings.py
index c432335..1fd57a5 100644
--- a/custom_components/nestquest/core/settings.py
+++ b/custom_components/nestquest/core/settings.py
@@ -35,6 +35,7 @@ from collections.abc import Callable
from dataclasses import dataclass
from types import MappingProxyType
from typing import Any, Mapping
+from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from .const import (
CONF_AFTERNOON_REMINDER_ENABLED,
@@ -47,12 +48,14 @@ from .const import (
CONF_MORNING_SUMMARY_ENABLED,
CONF_MORNING_SUMMARY_TIME,
CONF_NOTIFY_TARGET,
+ CONF_TIMEZONE,
DEFAULT_AFTERNOON_REMINDER_TIME,
DEFAULT_AUTOMATION_ENABLED,
DEFAULT_DAY_ROLLOVER_TIME,
DEFAULT_END_OF_DAY_REPORT_TIME,
DEFAULT_HORIZON_DAYS,
DEFAULT_MORNING_SUMMARY_TIME,
+ DEFAULT_TIMEZONE,
LOGGER,
TIME_PATTERN,
)
@@ -153,6 +156,29 @@ def _resolve_optional_str(value: Any) -> str:
return value.strip()
+def _resolve_timezone(value: Any) -> str:
+ """Validate a household time zone and return it.
+
+ The empty string means "the API host's local time" (the historical
+ behaviour); anything else must be an IANA key :class:`ZoneInfo`
+ resolves (e.g. ``America/New_York``). A non-str (including
+ ``None``) is rejected; the ``None``-as-absent mapping lives ONLY in
+ :meth:`NestQuestSettings.from_options` (via :func:`_option_value`).
+ """
+ if not isinstance(value, str):
+ raise ValueError(f"timezone must be a string, got {value!r}")
+ if value == "":
+ return value
+ try:
+ ZoneInfo(value)
+ except (ZoneInfoNotFoundError, ValueError) as error:
+ raise ValueError(
+ f"timezone must be an IANA time zone name such as "
+ f"'America/New_York', got {value!r}"
+ ) from error
+ return value
+
+
#: The single field table the two builders share. Each row is
#: ``(field_name, CONF_* key, default, validator)``. ``from_options``
#: applies every validator (raising on the first invalid value) while
@@ -214,6 +240,7 @@ _FIELDS: tuple[tuple[str, str, Any, Callable[[Any], Any]], ...] = (
DEFAULT_AUTOMATION_ENABLED,
_resolve_bool,
),
+ ("timezone", CONF_TIMEZONE, DEFAULT_TIMEZONE, _resolve_timezone),
)
@@ -243,6 +270,7 @@ class NestQuestSettings:
afternoon_reminder_enabled: bool = DEFAULT_AUTOMATION_ENABLED
end_of_day_report_enabled: bool = DEFAULT_AUTOMATION_ENABLED
celebration_enabled: bool = DEFAULT_AUTOMATION_ENABLED
+ timezone: str = DEFAULT_TIMEZONE
def __post_init__(self) -> None:
# Validate STRICTLY at construction so a hand-built instance
@@ -281,6 +309,21 @@ class NestQuestSettings:
raise ValueError(
f"{name} must be a boolean, got {value!r}"
)
+ _resolve_timezone(self.timezone)
+
+ def household_now(self, instant: datetime.datetime) -> datetime.datetime:
+ """Return the aware ``instant`` expressed in the household's zone.
+
+ The API service has no Home Assistant ``time_zone``, so its one
+ clock read (an aware host-local ``now``) is converted here: with
+ :attr:`timezone` set the SAME instant is re-expressed in that
+ zone, so ``.date()`` and ``.time()`` are the household's wall
+ clock even when the host runs UTC. With :attr:`timezone` empty
+ the instant is returned unchanged (the host's local time).
+ """
+ if not self.timezone:
+ return instant
+ return instant.astimezone(ZoneInfo(self.timezone))
@property
def day_rollover_hour_minute(self) -> tuple[int, int]:
diff --git a/custom_components/nestquest/core/settings_store.py b/custom_components/nestquest/core/settings_store.py
index 9f9c91f..319b3d8 100644
--- a/custom_components/nestquest/core/settings_store.py
+++ b/custom_components/nestquest/core/settings_store.py
@@ -18,7 +18,7 @@ API client; the two are deliberately NOT unified here, and neither
side reads the other's values.
STORED SERIALIZATION — the document at ``SETTINGS_META_KEY`` is a JSON
-object holding exactly the TEN settings fields, keyed by field name
+object holding exactly the ELEVEN settings fields, keyed by field name
(which equals the ``CONF_*`` option key for every field), complete
after the first update:
@@ -30,7 +30,10 @@ after the first update:
core's normalizer),
- ``morning_summary_enabled``, ``afternoon_reminder_enabled``,
``end_of_day_report_enabled``, ``celebration_enabled`` — real
- booleans.
+ booleans,
+- ``timezone`` — ``""`` (the API host's local time) or an IANA time
+ zone name such as ``America/New_York``. A document stored before
+ this field existed simply lacks it and resolves to ``""``.
Only values whose core validators ACCEPTED are ever written, so a
stored document always round-trips through the core's option builders.
@@ -172,7 +175,7 @@ async def update_settings(
update can never leave a half-applied document behind.
The validated changes are merged onto the current effective
- settings and the merged (complete, ten-field) document is persisted
+ settings and the merged (complete, eleven-field) document is persisted
in one atomic upsert; the updated settings are returned. The whole
read → merge → write span runs inside the per-database settings
lock, so concurrent updates queue instead of interleaving.
diff --git a/custom_components/nestquest/www/nestquest-cards.js b/custom_components/nestquest/www/nestquest-cards.js
index e5452d7..453bd1a 100644
--- a/custom_components/nestquest/www/nestquest-cards.js
+++ b/custom_components/nestquest/www/nestquest-cards.js
@@ -1,15 +1,15 @@
-const B = globalThis, nt = B.ShadowRoot && (B.ShadyCSS === void 0 || B.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype, st = /* @__PURE__ */ Symbol(), lt = /* @__PURE__ */ new WeakMap();
-let Pt = class {
+const B = globalThis, lt = B.ShadowRoot && (B.ShadyCSS === void 0 || B.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype, ct = /* @__PURE__ */ Symbol(), ft = /* @__PURE__ */ new WeakMap();
+let Dt = class {
constructor(t, e, n) {
- if (this._$cssResult$ = !0, n !== st) throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");
+ if (this._$cssResult$ = !0, n !== ct) throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");
this.cssText = t, this.t = e;
}
get styleSheet() {
let t = this.o;
const e = this.t;
- if (nt && t === void 0) {
+ if (lt && t === void 0) {
const n = e !== void 0 && e.length === 1;
- n && (t = lt.get(e)), t === void 0 && ((this.o = t = new CSSStyleSheet()).replaceSync(this.cssText), n && lt.set(e, t));
+ n && (t = ft.get(e)), t === void 0 && ((this.o = t = new CSSStyleSheet()).replaceSync(this.cssText), n && ft.set(e, t));
}
return t;
}
@@ -17,28 +17,28 @@ let Pt = class {
return this.cssText;
}
};
-const x = (s) => new Pt(typeof s == "string" ? s : s + "", void 0, st), It = (s, ...t) => {
+const x = (s) => new Dt(typeof s == "string" ? s : s + "", void 0, ct), Ht = (s, ...t) => {
const e = s.length === 1 ? s[0] : t.reduce((n, i, r) => n + ((o) => {
if (o._$cssResult$ === !0) return o.cssText;
if (typeof o == "number") return o;
throw Error("Value passed to 'css' function must be a 'css' function result: " + o + ". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.");
})(i) + s[r + 1], s[0]);
- return new Pt(e, s, st);
-}, Qt = (s, t) => {
- if (nt) s.adoptedStyleSheets = t.map((e) => e instanceof CSSStyleSheet ? e : e.styleSheet);
+ return new Dt(e, s, ct);
+}, Jt = (s, t) => {
+ if (lt) s.adoptedStyleSheets = t.map((e) => e instanceof CSSStyleSheet ? e : e.styleSheet);
else for (const e of t) {
const n = document.createElement("style"), i = B.litNonce;
i !== void 0 && n.setAttribute("nonce", i), n.textContent = e.cssText, s.appendChild(n);
}
-}, ct = nt ? (s) => s : (s) => s instanceof CSSStyleSheet ? ((t) => {
+}, mt = lt ? (s) => s : (s) => s instanceof CSSStyleSheet ? ((t) => {
let e = "";
for (const n of t.cssRules) e += n.cssText;
return x(e);
})(s) : s;
-const { is: Zt, defineProperty: Wt, getOwnPropertyDescriptor: Gt, getOwnPropertyNames: Vt, getOwnPropertySymbols: Kt, getPrototypeOf: Jt } = Object, Z = globalThis, dt = Z.trustedTypes, Xt = dt ? dt.emptyScript : "", Yt = Z.reactiveElementPolyfillSupport, I = (s, t) => s, et = { toAttribute(s, t) {
+const { is: Xt, defineProperty: Yt, getOwnPropertyDescriptor: te, getOwnPropertyNames: ee, getOwnPropertySymbols: ne, getPrototypeOf: se } = Object, V = globalThis, gt = V.trustedTypes, ie = gt ? gt.emptyScript : "", re = V.reactiveElementPolyfillSupport, I = (s, t) => s, it = { toAttribute(s, t) {
switch (t) {
case Boolean:
- s = s ? Xt : null;
+ s = s ? ie : null;
break;
case Object:
case Array:
@@ -63,23 +63,23 @@ const { is: Zt, defineProperty: Wt, getOwnPropertyDescriptor: Gt, getOwnProperty
}
}
return e;
-} }, Ut = (s, t) => !Zt(s, t), pt = { attribute: !0, type: String, converter: et, reflect: !1, useDefault: !1, hasChanged: Ut };
-Symbol.metadata ??= /* @__PURE__ */ Symbol("metadata"), Z.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap();
-let N = class extends HTMLElement {
+} }, Bt = (s, t) => !Xt(s, t), _t = { attribute: !0, type: String, converter: it, reflect: !1, useDefault: !1, hasChanged: Bt };
+Symbol.metadata ??= /* @__PURE__ */ Symbol("metadata"), V.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap();
+let E = class extends HTMLElement {
static addInitializer(t) {
this._$Ei(), (this.l ??= []).push(t);
}
static get observedAttributes() {
return this.finalize(), this._$Eh && [...this._$Eh.keys()];
}
- static createProperty(t, e = pt) {
+ static createProperty(t, e = _t) {
if (e.state && (e.attribute = !1), this._$Ei(), this.prototype.hasOwnProperty(t) && ((e = Object.create(e)).wrapped = !0), this.elementProperties.set(t, e), !e.noAccessor) {
const n = /* @__PURE__ */ Symbol(), i = this.getPropertyDescriptor(t, n, e);
- i !== void 0 && Wt(this.prototype, t, i);
+ i !== void 0 && Yt(this.prototype, t, i);
}
}
static getPropertyDescriptor(t, e, n) {
- const { get: i, set: r } = Gt(this.prototype, t) ?? { get() {
+ const { get: i, set: r } = te(this.prototype, t) ?? { get() {
return this[e];
}, set(o) {
this[e] = o;
@@ -90,17 +90,17 @@ let N = class extends HTMLElement {
}, configurable: !0, enumerable: !0 };
}
static getPropertyOptions(t) {
- return this.elementProperties.get(t) ?? pt;
+ return this.elementProperties.get(t) ?? _t;
}
static _$Ei() {
if (this.hasOwnProperty(I("elementProperties"))) return;
- const t = Jt(this);
+ const t = se(this);
t.finalize(), t.l !== void 0 && (this.l = [...t.l]), this.elementProperties = new Map(t.elementProperties);
}
static finalize() {
if (this.hasOwnProperty(I("finalized"))) return;
if (this.finalized = !0, this._$Ei(), this.hasOwnProperty(I("properties"))) {
- const e = this.properties, n = [...Vt(e), ...Kt(e)];
+ const e = this.properties, n = [...ee(e), ...ne(e)];
for (const i of n) this.createProperty(i, e[i]);
}
const t = this[Symbol.metadata];
@@ -119,8 +119,8 @@ let N = class extends HTMLElement {
const e = [];
if (Array.isArray(t)) {
const n = new Set(t.flat(1 / 0).reverse());
- for (const i of n) e.unshift(ct(i));
- } else t !== void 0 && e.push(ct(t));
+ for (const i of n) e.unshift(mt(i));
+ } else t !== void 0 && e.push(mt(t));
return e;
}
static _$Eu(t, e) {
@@ -146,7 +146,7 @@ let N = class extends HTMLElement {
}
createRenderRoot() {
const t = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);
- return Qt(t, this.constructor.elementStyles), t;
+ return Jt(t, this.constructor.elementStyles), t;
}
connectedCallback() {
this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(!0), this._$EO?.forEach((t) => t.hostConnected?.());
@@ -162,14 +162,14 @@ let N = class extends HTMLElement {
_$ET(t, e) {
const n = this.constructor.elementProperties.get(t), i = this.constructor._$Eu(t, n);
if (i !== void 0 && n.reflect === !0) {
- const r = (n.converter?.toAttribute !== void 0 ? n.converter : et).toAttribute(e, n.type);
+ const r = (n.converter?.toAttribute !== void 0 ? n.converter : it).toAttribute(e, n.type);
this._$Em = t, r == null ? this.removeAttribute(i) : this.setAttribute(i, r), this._$Em = null;
}
}
_$AK(t, e) {
const n = this.constructor, i = n._$Eh.get(t);
if (i !== void 0 && this._$Em !== i) {
- const r = n.getPropertyOptions(i), o = typeof r.converter == "function" ? { fromAttribute: r.converter } : r.converter?.fromAttribute !== void 0 ? r.converter : et;
+ const r = n.getPropertyOptions(i), o = typeof r.converter == "function" ? { fromAttribute: r.converter } : r.converter?.fromAttribute !== void 0 ? r.converter : it;
this._$Em = i;
const l = o.fromAttribute(e, r.type);
this[i] = l ?? this._$Ej?.get(i) ?? l, this._$Em = null;
@@ -178,7 +178,7 @@ let N = class extends HTMLElement {
requestUpdate(t, e, n, i = !1, r) {
if (t !== void 0) {
const o = this.constructor;
- if (i === !1 && (r = this[t]), n ??= o.getPropertyOptions(t), !((n.hasChanged ?? Ut)(r, e) || n.useDefault && n.reflect && r === this._$Ej?.get(t) && !this.hasAttribute(o._$Eu(t, n)))) return;
+ if (i === !1 && (r = this[t]), n ??= o.getPropertyOptions(t), !((n.hasChanged ?? Bt)(r, e) || n.useDefault && n.reflect && r === this._$Ej?.get(t) && !this.hasAttribute(o._$Eu(t, n)))) return;
this.C(t, e, n);
}
this.isUpdatePending === !1 && (this._$ES = this._$EP());
@@ -246,51 +246,51 @@ let N = class extends HTMLElement {
firstUpdated(t) {
}
};
-N.elementStyles = [], N.shadowRootOptions = { mode: "open" }, N[I("elementProperties")] = /* @__PURE__ */ new Map(), N[I("finalized")] = /* @__PURE__ */ new Map(), Yt?.({ ReactiveElement: N }), (Z.reactiveElementVersions ??= []).push("2.1.2");
-const it = globalThis, ht = (s) => s, F = it.trustedTypes, ut = F ? F.createPolicy("lit-html", { createHTML: (s) => s }) : void 0, Lt = "$lit$", $ = `lit$${Math.random().toFixed(9).slice(2)}$`, Rt = "?" + $, te = `<${Rt}>`, S = document, U = () => S.createComment(""), L = (s) => s === null || typeof s != "object" && typeof s != "function", rt = Array.isArray, ee = (s) => rt(s) || typeof s?.[Symbol.iterator] == "function", V = `[
-\f\r]`, z = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, ft = /-->/g, mt = />/g, q = RegExp(`>|${V}(?:([^\\s"'>=/]+)(${V}*=${V}*(?:[^
-\f\r"'\`<>=]|("|')|))|$)`, "g"), gt = /'/g, _t = /"/g, jt = /^(?:script|style|textarea|title)$/i, ne = (s) => (t, ...e) => ({ _$litType$: s, strings: t, values: e }), d = ne(1), T = /* @__PURE__ */ Symbol.for("lit-noChange"), g = /* @__PURE__ */ Symbol.for("lit-nothing"), yt = /* @__PURE__ */ new WeakMap(), C = S.createTreeWalker(S, 129);
-function Dt(s, t) {
- if (!rt(s) || !s.hasOwnProperty("raw")) throw Error("invalid template strings array");
- return ut !== void 0 ? ut.createHTML(t) : t;
+E.elementStyles = [], E.shadowRootOptions = { mode: "open" }, E[I("elementProperties")] = /* @__PURE__ */ new Map(), E[I("finalized")] = /* @__PURE__ */ new Map(), re?.({ ReactiveElement: E }), (V.reactiveElementVersions ??= []).push("2.1.2");
+const dt = globalThis, yt = (s) => s, F = dt.trustedTypes, bt = F ? F.createPolicy("lit-html", { createHTML: (s) => s }) : void 0, Ft = "$lit$", $ = `lit$${Math.random().toFixed(9).slice(2)}$`, Zt = "?" + $, oe = `<${Zt}>`, S = document, U = () => S.createComment(""), R = (s) => s === null || typeof s != "object" && typeof s != "function", pt = Array.isArray, ae = (s) => pt(s) || typeof s?.[Symbol.iterator] == "function", X = `[
+\f\r]`, z = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, vt = /-->/g, xt = />/g, q = RegExp(`>|${X}(?:([^\\s"'>=/]+)(${X}*=${X}*(?:[^
+\f\r"'\`<>=]|("|')|))|$)`, "g"), wt = /'/g, $t = /"/g, Qt = /^(?:script|style|textarea|title)$/i, le = (s) => (t, ...e) => ({ _$litType$: s, strings: t, values: e }), d = le(1), T = /* @__PURE__ */ Symbol.for("lit-noChange"), g = /* @__PURE__ */ Symbol.for("lit-nothing"), qt = /* @__PURE__ */ new WeakMap(), A = S.createTreeWalker(S, 129);
+function Wt(s, t) {
+ if (!pt(s) || !s.hasOwnProperty("raw")) throw Error("invalid template strings array");
+ return bt !== void 0 ? bt.createHTML(t) : t;
}
-const se = (s, t) => {
+const ce = (s, t) => {
const e = s.length - 1, n = [];
let i, r = t === 2 ? "