Phase 4: Page() collects component media; drop manual scripts= threading

Page() now calls collect_media(content) and emits the ModuleScript /
StaticScript tags itself, so views no longer thread scripts= for
component-owned JS. The list views (game/session/purchase/device/
platform/playevent) compose with Fragment(filter_bar, content) instead of
mark_safe(str(filter_bar) + str(content)) — keeping the node tree intact
so the filter bar's media (filter_bar.js + search_select.js +
range_slider.js, and date_range_picker.js on purchases) reaches Page().
The stats views drop _STATS_SCRIPTS; YearPicker's datepicker.umd.js is
collected from its declared media.

The scripts= argument remains for page-specific glue not owned by a
component (the add-form helpers add_game.js / add_purchase.js /
add_session.js, alongside search_select.js for their form widgets).

Adds regression tests asserting the list and stats pages auto-load their
widget scripts with no scripts= in the view, and documents the node/
media model in CLAUDE.md.

https://claude.ai/code/session_01BKurBhE3Qj25p7Bfsg7EeK
This commit is contained in:
Claude
2026-06-13 07:32:35 +00:00
parent 0819ddb87d
commit 2d3ae4e04f
10 changed files with 57 additions and 53 deletions
+16 -2
View File
@@ -276,9 +276,23 @@ def Page(
scripts: SafeText | str = "",
mastered: bool = False,
) -> SafeText:
"""Assemble a full HTML document around `content` (the fast_app equivalent)."""
"""Assemble a full HTML document around `content` (the fast_app equivalent).
Scripts are collected from `content`'s component tree: every component
declares its JS via `Media`, and `collect_media` gathers (deduped) the union
for the whole page. The `scripts` argument remains for page-specific glue
that isn't owned by a reusable component (e.g. the add-form helpers).
"""
from common.components import ModuleScript, StaticScript, collect_media
from games.views.general import global_current_year, model_counts
media = collect_media(content)
collected_scripts = "".join(
[str(ModuleScript(name)) for name in media.js]
+ [str(StaticScript(name)) for name in media.js_external]
)
all_scripts = collected_scripts + (str(scripts) if scripts else "")
counts = model_counts(request)
year = global_current_year(request)["global_current_year"]
navbar = Navbar(
@@ -328,7 +342,7 @@ def Page(
f' <div id="main-container" class="flex flex-1 flex-col pt-8 pb-16">{content}</div>\n'
f' <span class="fixed left-2 bottom-2 text-xs text-slate-300 dark:text-slate-600">{version()} ({version_date()})</span>\n'
" </div>\n"
f" {scripts}\n"
f" {all_scripts}\n"
f" {_main_script(mastered)}\n"
" <!-- hx-swap-oob makes sure the modal gets removed upon any HTMX response -->\n"
' <div id="global-modal-container" hx-swap-oob="true"></div>\n'