1c5bff8651
The *FilterBar family (FilterBar / SessionFilterBar / PurchaseFilterBar / DeviceFilterBar / PlatformFilterBar / PlayEventFilterBar) previously shared the collapsible chrome through a free `_filter_bar(fields, ...)` helper that each function called at the end. Replace that with a `_FilterBarBase` BaseComponent: it owns the chrome render() and declares `media = _FILTER_BAR_MEDIA`, and each bar is now a subclass implementing `build_fields()`. The per-entity field-building bodies move verbatim into module-level `_<entity>_fields(existing, ...)` functions that each subclass delegates to, so the large bodies are untouched (no reindentation) and the diff stays reviewable. Media still bubbles: BaseComponent.collect_media() merges the bar's own filter_bar.js with the search_select.js / range_slider.js / date_range_picker.js declared by the contained widgets. Call sites are unchanged — `FilterBar(filter_json=..., preset_list_url=...)` now instantiates a Node instead of calling a function, and both `str(bar)` and `collect_media(bar)` behave as before. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>