diff --git a/common/components/primitives.py b/common/components/primitives.py
index ef1ec21..16a5dae 100644
--- a/common/components/primitives.py
+++ b/common/components/primitives.py
@@ -542,9 +542,15 @@ def StaticScript(filename: str) -> SafeText:
return mark_safe(f'')
-# Media for the Flowbite-datepicker year picker (vendored UMD bundle). Declared
-# on the YearPicker node so Page() loads it wherever a YearPicker appears.
-_YEAR_PICKER_MEDIA = Media(js_external=("datepicker.umd.js",))
+# Media for the Flowbite-datepicker year picker: the vendored UMD bundle
+# (classic script) plus the ts/year_picker.ts glue (compiled module). Declared
+# on the YearPicker node so Page() loads both wherever a YearPicker appears. The
+# UMD bundle is a classic script (runs during parse) while year_picker.js is a
+# deferred module (runs after parse), so Datepicker is defined by the time the
+# module executes regardless of tag order.
+_YEAR_PICKER_MEDIA = Media(
+ js_external=("datepicker.umd.js",), js=("dist/year_picker.js",)
+)
def YearPicker(
@@ -589,44 +595,7 @@ def YearPicker(
data-available-years="{years_csv}"
data-selected-year="{selected}"
data-url-template="{url_template}">
-
-""",
+""",
media=_YEAR_PICKER_MEDIA,
)
diff --git a/games/static/js/year_picker.js b/games/static/js/year_picker.js
deleted file mode 100644
index 7eeb843..0000000
--- a/games/static/js/year_picker.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import { onSwap } from "./utils.js";
-
-onSwap("#year-picker-input", function(pickerEl) {
- const selectedYear = pickerEl.dataset.selectedYear;
- const urlTemplate = pickerEl.dataset.urlTemplate;
- const currentYear = new Date().getFullYear();
- const availableYears = new Set(
- pickerEl.dataset.availableYears
- .split(",")
- .map(s => parseInt(s.trim()))
- .filter(n => !isNaN(n))
- );
-
- const picker = new Datepicker(pickerEl, {
- pickLevel: 2,
- format: "yyyy",
- minDate: new Date(1999, 0, 1),
- maxDate: new Date(currentYear, 11, 31),
- autohide: false,
- orientation: "bottom end",
- showOnClick: false,
- showOnFocus: false,
- beforeShowYear: (date) => ({ enabled: availableYears.has(date.getFullYear()) }),
- });
- pickerEl._pickerInstance = picker;
-
- picker.element.addEventListener("changeDate", (event) => {
- const year = event.detail.date?.getFullYear();
- if (year && urlTemplate) {
- window.location.href = urlTemplate.replace("__year__", year);
- }
- });
-
- if (selectedYear) {
- picker.dates = [new Date(parseInt(selectedYear), 0, 1)];
- picker.update();
- }
-});
diff --git a/ts/year_picker.ts b/ts/year_picker.ts
new file mode 100644
index 0000000..a11d3aa
--- /dev/null
+++ b/ts/year_picker.ts
@@ -0,0 +1,54 @@
+/**
+ * YearPicker — wires the Flowbite-datepicker year grid behind the YearPicker
+ * component (common/components/primitives.py). The component renders a hidden
+ * #year-picker-input carrying data-available-years / data-selected-year /
+ * data-url-template; this turns it into a year-level Datepicker and navigates
+ * to the chosen year's URL. Datepicker comes from the vendored UMD bundle
+ * (datepicker.umd.js), loaded as a classic script before this module runs.
+ */
+import { onSwap } from "./utils.js";
+
+declare const Datepicker: any;
+
+// The Alpine toggle button reaches the Datepicker instance through this prop.
+interface PickerElement extends HTMLInputElement {
+ _pickerInstance?: any;
+}
+
+onSwap("#year-picker-input", (element) => {
+ const pickerElement = element as PickerElement;
+ const selectedYear = pickerElement.dataset.selectedYear;
+ const urlTemplate = pickerElement.dataset.urlTemplate;
+ const currentYear = new Date().getFullYear();
+ const availableYears = new Set(
+ (pickerElement.dataset.availableYears ?? "")
+ .split(",")
+ .map((part) => parseInt(part.trim(), 10))
+ .filter((year) => !isNaN(year))
+ );
+
+ const picker = new Datepicker(pickerElement, {
+ pickLevel: 2,
+ format: "yyyy",
+ minDate: new Date(1999, 0, 1),
+ maxDate: new Date(currentYear, 11, 31),
+ autohide: false,
+ orientation: "bottom end",
+ showOnClick: false,
+ showOnFocus: false,
+ beforeShowYear: (date: Date) => ({ enabled: availableYears.has(date.getFullYear()) }),
+ });
+ pickerElement._pickerInstance = picker;
+
+ picker.element.addEventListener("changeDate", (event: Event) => {
+ const year = (event as CustomEvent).detail.date?.getFullYear();
+ if (year && urlTemplate) {
+ window.location.href = urlTemplate.replace("__year__", String(year));
+ }
+ });
+
+ if (selectedYear) {
+ picker.dates = [new Date(parseInt(selectedYear, 10), 0, 1)];
+ picker.update();
+ }
+});