Master composer for card stack JavaScript. Combines viewport height,
Master Coordinator
Applies all viewport settings (width, height, scroll, touch) then reveals the viewport using a double-RAF pattern. Also handles HTMX afterSettle events.
Global Callback Registration
The cjm-fasthtml-keyboard-navigation library looks up JS callbacks via window[action.jsCallback](). Since our functions live on the namespaced ns object, we register global wrappers that delegate to the namespace.
global_callback_name
def global_callback_name( prefix:str, # Card stack instance prefix callback:str, # Base callback name (e.g., "jumpPageUp"))->str: # Global function name (e.g., "cs0_jumpPageUp")
Generate a prefix-unique global callback name for keyboard navigation.
generate_card_stack_js
The main entry point that composes all JS fragments into a single namespaced IIFE. The consumer calls this once in their step renderer.
generate_card_stack_js
def generate_card_stack_js( ids:CardStackHtmlIds, # HTML IDs for this instance button_ids:CardStackButtonIds, # Button IDs for keyboard triggers config:CardStackConfig, # Card stack configuration urls:CardStackUrls, # URL bundle for routing container_id:str='', # Consumer's parent container ID (for height calc) extra_scripts:Tuple=(), # Additional JS to include in the IIFE focus_position:Optional=None, # Focus slot offset (None=center, -1=bottom, 0=top))->Any: # Script element with all card stack JavaScript
Compose all card stack JS into a single namespaced IIFE.
from cjm_fasthtml_card_stack.core.config import _reset_prefix_counter# Test setup: shared fixtures for composition tests_reset_prefix_counter()config = CardStackConfig()ids = CardStackHtmlIds(prefix=config.prefix)btn = CardStackButtonIds(prefix=config.prefix)urls = CardStackUrls( nav_up="/cs/nav_up", nav_down="/cs/nav_down", nav_first="/cs/nav_first", nav_last="/cs/nav_last", nav_page_up="/cs/nav_page_up", nav_page_down="/cs/nav_page_down", nav_to_index="/cs/nav_to_index", update_viewport="/cs/update_viewport", save_width="/cs/save_width", save_scale="/cs/save_scale",)script = generate_card_stack_js(ids, btn, config, urls, container_id="my-app")js_text = script.children[0] if script.children else""# Namespace setupassert"window.cardStacks"in js_textassertf"'{config.prefix}'"in js_text# All sections present in composed outputfor section in ["Viewport Height", "Scroll Navigation", "Touch Navigation","Page Navigation", "Width Management", "Scale Management","Card Count Management", "Auto Visible Count Adjustment","Grid Template Management", "Focused Section Constraint","Boundary Index Helpers","Global Keyboard Callbacks", "Master Coordinator", "HTMX Event Listeners",]:assert section in js_text, f"Missing section: {section}"# Focused section constraint reads rowGap and sets maxHeightassertf"'{ids.viewport_section_focused}'"in js_textassert"rowGap"in js_textassert"maxHeight"in js_text# constrainFocusedSection called after recalculateHeight in coordinatorassert"ns.constrainFocusedSection"in js_text# Touch-action toggle: pan-y for overflow, none for normalassert"scrollHeight"in js_textassert"touchAction"in js_textassert"'pan-y'"in js_text# Scroll-to-top before height calculation (fixes HTMX navigation scroll position issue)assert"window.scrollTo(0, 0)"in js_text, "Missing scroll-to-top in initialization"# Zone activation in scroll and touch handlersassertf"setActiveZone('{ids.card_stack}')"in js_text# Scrollbar JS IIFE included (show_scrollbar=True by default)assert"Virtual Scrollbar"in js_textassertf"{config.prefix}-scrollbar-track"in js_textassertf"{config.prefix}-scrollbar-thumb"in js_textassert"dataset.position"in js_text # Self-contained position sync from trackassert"/cs/nav_to_index"in js_text # Posts to nav_to_index URL# Scrollbar zone activation callbackassert"scrollbarActivate"in js_text# --- Boundary no-op guard ---# Helpers read from focused_index_input (always fresh via OOB), NOT card-stack# data attrs (stale — only set on initial render).assert"ns._getFocusedIndex"in js_textassert"ns._getTotalItems"in js_textassertf"'{ids.focused_index_input}'"in js_textassert"input.value"in js_textassert"dataset.totalItems"in js_text# Up/down button ID sets populated with all six nav buttonsassert"_UP_BTN_IDS"in js_textassert"_DOWN_BTN_IDS"in js_textfor bid in (btn.nav_up, btn.nav_page_up, btn.nav_first, btn.nav_down, btn.nav_page_down, btn.nav_last):assertf"'{bid}'"in js_text, f"Missing nav button ID: {bid}"# htmx:beforeRequest listener wired with preventDefault on boundaryassert"htmx:beforeRequest"in js_textassert"_beforeRequestHandler"in js_textassert"evt.preventDefault()"in js_textprint("Composition: namespace, section presence, zone activation, scrollbar, and boundary guard tests passed!")
Composition: namespace, section presence, zone activation, scrollbar, and boundary guard tests passed!
# Test key namespace functions are exposedfor fn in ["ns.updateWidth", "ns.updateScale", "ns.updateCardCount","ns.handleCountChange", "ns._autoUpdateCount", "ns._runAutoAdjust","ns.triggerAutoAdjust", "ns.applyGridTemplate", "ns.constrainFocusedSection","ns.jumpToFirstItem", "ns.jumpToLastItem", "ns.applyAllViewportSettings","ns.recalculateHeight", "ns.decreaseWidth", "ns.increaseWidth","ns.decreaseScale", "ns.increaseScale","ns._setupScrollNav", "ns._setupTouchNav", "ns.syncCountDropdown","ns._setupSiblingObserver",]:assert fn in js_text, f"Missing function: {fn}"# Touch navigation (Pointer Events API)assert"pointerdown"in js_textassert"pointermove"in js_textassert"setPointerCapture"in js_textassert"momentumTick"in js_textprint("Composition: namespace function exposure tests passed!")
# Test auto-adjust and growth validation in composed outputassert"_autoAdjusting"in js_textassert"_autoGrowing"in js_textassert"_preGrowthItemIds"in js_textassert"_snapshotItemIds"in js_textassert"_hideNewItems"in js_textassert"_revealNewItems"in js_textassert"_validateGrowth"in js_textassert"ns._cancelAutoGrowth"in js_textassert"htmx:afterSwap"in js_text# Card count uses swap:'none' for OOB updatesassert"swap: 'none',\n values: { visible_count:"in js_textprint("Composition: auto-adjust and growth validation tests passed!")
Composition: auto-adjust and growth validation tests passed!
# Test global callback wrappers and HTMX listener cleanupprefix = config.prefixfor cb in ["jumpPageUp", "jumpToFirstItem", "decreaseWidth", "increaseWidth","decreaseScale", "increaseScale"]:assertf"window['{prefix}_{cb}']"in js_text, f"Missing global callback: {cb}"# HTMX listeners use remove-and-replace patternhandler_key =f"_csHandlers_{config.prefix.replace('-', '_')}"assertf"window.{handler_key}"in js_textassert"removeEventListener"in js_textassert"_afterSwapHandler"in js_textassert"_afterSettleHandler"in js_textprint("Composition: global callbacks and HTMX listener tests passed!")
Composition: global callbacks and HTMX listener tests passed!
# Test with disable_scroll_in_modesconfig3 = CardStackConfig(prefix="split-test", disable_scroll_in_modes=("split",))ids3 = CardStackHtmlIds(prefix=config3.prefix)btn3 = CardStackButtonIds(prefix=config3.prefix)script3 = generate_card_stack_js(ids3, btn3, config3, urls)js3 = script3.children[0] if script3.children else""assert"isScrollDisabled"in js3assert"isTouchDisabled"in js3assert"'split'"in js3print("Scroll/touch mode disabling in composed JS test passed!")# Auto-adjust is always included (no longer optional)assert"Auto Visible Count Adjustment"in js3assert"_snapshotItemIds"in js3assert"_revealNewItems"in js3assert"ns.handleCountChange"in js3print("Auto-adjust always included test passed!")