diff --git a/common/components/filters.py b/common/components/filters.py
index 056ec49..b09a6b9 100644
--- a/common/components/filters.py
+++ b/common/components/filters.py
@@ -52,11 +52,11 @@ _FILTER_RADIO_CLASS = (
_FILTER_GRID_CLASS = "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-4"
-# range_slider.js wires RangeSlider; filter_bar.js wires the bar chrome
-# (Apply/Clear, presets, search injection). Widget media (search_select.js,
+# range_slider.js wires RangeSlider; ts/filter_bar.ts wires the bar chrome
+# (Apply/Clear, presets, search injection). Widget media (dist/search_select.js,
# date_range_picker.js) bubbles up from the contained FilterSelect / picker.
_RANGE_SLIDER_MEDIA = Media(js=("range_slider.js",))
-_FILTER_BAR_MEDIA = Media(js=("filter_bar.js",))
+_FILTER_BAR_MEDIA = Media(js=("dist/filter_bar.js",))
def _filter_parse(filter_json: str) -> dict:
diff --git a/e2e/test_boolean_filter_e2e.py b/e2e/test_boolean_filter_e2e.py
index ea894c8..b6e789c 100644
--- a/e2e/test_boolean_filter_e2e.py
+++ b/e2e/test_boolean_filter_e2e.py
@@ -24,7 +24,7 @@ def _bar_page(filter_json: str = "") -> str:
-
+
{FilterBar(filter_json=filter_json, preset_list_url="/p/l", preset_save_url="/p/s")}
diff --git a/e2e/test_date_filter_e2e.py b/e2e/test_date_filter_e2e.py
index dad2799..9abe8a0 100644
--- a/e2e/test_date_filter_e2e.py
+++ b/e2e/test_date_filter_e2e.py
@@ -32,7 +32,7 @@ def _bar_page(filter_json: str = "") -> str:
-
+
{PurchaseFilterBar(filter_json=filter_json, preset_list_url="/p/l", preset_save_url="/p/s")}
diff --git a/e2e/test_date_range_picker_e2e.py b/e2e/test_date_range_picker_e2e.py
index 3fcdc46..9aa78c4 100644
--- a/e2e/test_date_range_picker_e2e.py
+++ b/e2e/test_date_range_picker_e2e.py
@@ -32,7 +32,7 @@ def _bar_page(filter_json: str = "") -> str:
-
+
{PurchaseFilterBar(filter_json=filter_json, preset_list_url="/p/l", preset_save_url="/p/s")}
diff --git a/e2e/test_range_slider_e2e.py b/e2e/test_range_slider_e2e.py
index 204ce64..b1bfc74 100644
--- a/e2e/test_range_slider_e2e.py
+++ b/e2e/test_range_slider_e2e.py
@@ -16,7 +16,7 @@ def _bar_page(filter_json: str = "") -> str:
-
+
{FilterBar(filter_json=filter_json, preset_list_url="/p/l", preset_save_url="/p/s")}
diff --git a/e2e/test_string_filter_e2e.py b/e2e/test_string_filter_e2e.py
index 2f9b3a5..24fd7ac 100644
--- a/e2e/test_string_filter_e2e.py
+++ b/e2e/test_string_filter_e2e.py
@@ -19,7 +19,7 @@ def _bar_page(filter_json: str = "") -> str:
-
+
{PlatformFilterBar(filter_json=filter_json, preset_list_url="/p/l", preset_save_url="/p/s")}
diff --git a/games/static/js/filter_bar.js b/games/static/js/filter_bar.js
deleted file mode 100644
index e83fa99..0000000
--- a/games/static/js/filter_bar.js
+++ /dev/null
@@ -1,479 +0,0 @@
-/**
- * Filter bar — vanilla JavaScript implementation.
- *
- * Handles form submission, preset loading/saving, and preset list rendering.
- * No HTMX — plain fetch() and window.location for all interactions.
- */
-import { onSwap } from "./utils.js";
-
-(function () {
- "use strict";
-
- /** Build a criterion object from a value and optional second value. */
- function criterion(value, value2, modifier) {
- var c = { value: value, modifier: modifier };
- if (value2 !== null && value2 !== undefined && value2 !== "") {
- c.value2 = value2;
- }
- return c;
- }
-
- /** Read a