cjm-fasthtml-card-stack
Install
pip install cjm_fasthtml_card_stackProject Structure
nbs/
├── components/ (5)
│ ├── controls.ipynb # Width slider, scale slider, and card count selector components.
│ ├── progress.ipynb # Progress indicator showing the current position within the card stack.
│ ├── settings_modal.ipynb # Modal-based card stack settings with card count slider (auto toggle), width slider, and optional scale slider.
│ ├── states.ipynb # Loading, empty, and placeholder card components for the card stack viewport.
│ └── viewport.ipynb # Card stack viewport with 3-section CSS Grid layout, slot rendering,
├── core/ (5)
│ ├── button_ids.ipynb # Prefix-based IDs for hidden keyboard action buttons.
│ ├── config.ipynb # Configuration dataclasses for card stack initialization and visual styling.
│ ├── constants.ipynb # CSS class constants, type aliases, and default values for the card stack library.
│ ├── html_ids.ipynb # Prefix-based HTML ID generator for card stack DOM elements.
│ └── models.ipynb # Core dataclasses for card stack state, render context, and URL routing.
├── helpers/ (1)
│ └── focus.ipynb # Focus position resolution, viewport window calculation, and OOB focus sync.
├── js/ (8)
│ ├── auto_adjust.ipynb # JavaScript generator for automatic visible card count adjustment.
│ ├── controls.ipynb # JavaScript generators for width, scale, and card count management.
│ ├── core.ipynb # Master composer for card stack JavaScript. Combines viewport height,
│ ├── navigation.ipynb # JavaScript generator for page jump and first/last navigation helpers.
│ ├── scroll.ipynb # JavaScript generator for scroll-to-nav conversion.
│ ├── sync.ipynb # Synced navigation between two card stacks — source stack navigation drives target stack to same index
│ ├── touch.ipynb # JavaScript generator for touch-to-nav conversion: swipe, drag,
│ └── viewport.ipynb # JavaScript generator for dynamic viewport height calculation.
├── keyboard/ (1)
│ └── actions.ipynb # Keyboard navigation focus zone and action factories for the card stack.
└── routes/ (2)
├── handlers.ipynb # Response builder functions for card stack operations (Tier 1 API).
└── router.ipynb # Convenience router factory that wires up standard card stack routes (Tier 2 API).
Total: 22 notebooks across 6 directories
Module Dependencies
graph LR
components_controls[components.controls<br/>Controls]
components_progress[components.progress<br/>Progress]
components_settings_modal[components.settings_modal<br/>Settings Modal]
components_states[components.states<br/>States]
components_viewport[components.viewport<br/>Viewport]
core_button_ids[core.button_ids<br/>Button IDs]
core_config[core.config<br/>Config]
core_constants[core.constants<br/>Constants]
core_html_ids[core.html_ids<br/>HTML IDs]
core_models[core.models<br/>Models]
helpers_focus[helpers.focus<br/>Focus]
js_auto_adjust[js.auto_adjust<br/>JS: Auto Adjust]
js_controls[js.controls<br/>JS: Controls]
js_core[js.core<br/>JS: Core]
js_navigation[js.navigation<br/>JS: Page Navigation]
js_scroll[js.scroll<br/>JS: Scroll Navigation]
js_sync[js.sync<br/>sync]
js_touch[js.touch<br/>JS: Touch Navigation]
js_viewport[js.viewport<br/>JS: Viewport Height]
keyboard_actions[keyboard.actions<br/>Actions]
routes_handlers[routes.handlers<br/>Handlers]
routes_router[routes.router<br/>Router]
components_controls --> core_config
components_controls --> core_html_ids
components_progress --> core_html_ids
components_settings_modal --> core_config
components_settings_modal --> core_html_ids
components_states --> core_html_ids
components_viewport --> core_constants
components_viewport --> core_models
components_viewport --> helpers_focus
components_viewport --> core_config
components_viewport --> components_states
components_viewport --> core_html_ids
helpers_focus --> core_html_ids
js_auto_adjust --> core_config
js_auto_adjust --> core_models
js_auto_adjust --> core_constants
js_auto_adjust --> core_html_ids
js_controls --> core_constants
js_controls --> core_config
js_controls --> core_models
js_controls --> core_html_ids
js_core --> js_scroll
js_core --> js_auto_adjust
js_core --> js_controls
js_core --> core_models
js_core --> core_constants
js_core --> core_config
js_core --> js_navigation
js_core --> core_button_ids
js_core --> js_viewport
js_core --> js_touch
js_core --> core_html_ids
js_navigation --> core_button_ids
js_scroll --> core_constants
js_scroll --> core_button_ids
js_scroll --> core_html_ids
js_touch --> core_constants
js_touch --> core_button_ids
js_touch --> core_html_ids
js_viewport --> core_html_ids
keyboard_actions --> core_models
keyboard_actions --> core_config
keyboard_actions --> js_core
keyboard_actions --> core_button_ids
keyboard_actions --> core_html_ids
routes_handlers --> helpers_focus
routes_handlers --> components_viewport
routes_handlers --> core_models
routes_handlers --> components_progress
routes_handlers --> core_config
routes_handlers --> core_html_ids
routes_router --> core_models
routes_router --> core_config
routes_router --> routes_handlers
routes_router --> core_html_ids
55 cross-module dependencies detected
CLI Reference
No CLI commands found in this project.
Module Overview
Detailed documentation for each module in the project:
Actions (actions.ipynb)
Keyboard navigation focus zone and action factories for the card stack.
Import
from cjm_fasthtml_card_stack.keyboard.actions import (
create_card_stack_focus_zone,
create_card_stack_nav_actions,
build_card_stack_url_map,
render_card_stack_action_buttons
)Functions
def create_card_stack_focus_zone(
ids: CardStackHtmlIds, # HTML IDs for this card stack instance
on_focus_change: Optional[str] = None, # JS callback name on focus change
hidden_input_prefix: Optional[str] = None, # Prefix for keyboard nav hidden inputs
data_attributes: Tuple[str, ...] = (), # Data attributes to track on focused items
) -> FocusZone: # Configured focus zone for the card stack
"Create a focus zone for a card stack viewport."def create_card_stack_nav_actions(
zone_id: str, # Focus zone ID to restrict actions to
button_ids: CardStackButtonIds, # Button IDs for HTMX triggers
config: CardStackConfig, # Config (for prefix-unique callback names)
disable_in_modes: Tuple[str, ...] = (), # Mode names that disable navigation
) -> Tuple[KeyAction, ...]: # Standard card stack navigation actions
"Create standard keyboard navigation actions for a card stack."def build_card_stack_url_map(
button_ids: CardStackButtonIds, # Button IDs for this card stack instance
urls: CardStackUrls, # URL bundle for routing
) -> Dict[str, str]: # Mapping of button ID -> route URL
"""
Build url_map for render_keyboard_system with all card stack navigation buttons.
Returns a dict mapping button IDs to URLs for all navigation actions:
nav_up, nav_down, nav_first, nav_last, nav_page_up, nav_page_down.
Merge with consumer's own action URLs when building the keyboard system:
url_map = {**build_card_stack_url_map(btn_ids, urls), **my_action_urls}
"""def render_card_stack_action_buttons(
button_ids: CardStackButtonIds, # Button IDs for this card stack instance
urls: CardStackUrls, # URL bundle for routing
ids: CardStackHtmlIds, # HTML IDs (for hx-include of focused_index_input)
) -> 'FT': # Div containing hidden action buttons
"""
Render hidden HTMX buttons for JS-callback-triggered navigation actions.
Creates buttons for: page_up, page_down, first, last.
These are clicked programmatically by the card stack's JS functions.
Must be included in the DOM alongside the keyboard system's own buttons.
"""JS: Auto Adjust (auto_adjust.ipynb)
JavaScript generator for automatic visible card count adjustment.
Import
from cjm_fasthtml_card_stack.js.auto_adjust import *Functions
def _generate_auto_adjust_js(
ids: CardStackHtmlIds, # HTML IDs for this instance
config: CardStackConfig, # Config for auto mode check
urls: CardStackUrls, # URL bundle (update_viewport)
focus_position: Optional[int] = None, # Focus slot offset (None=center, -1=bottom, 0=top)
) -> str: # JS code fragment for auto visible count adjustment
"Generate JS for automatic visible count adjustment based on overflow detection."Config (config.ipynb)
Configuration dataclasses for card stack initialization and visual styling.
Import
from cjm_fasthtml_card_stack.core.config import (
CardStackStyleConfig,
CardStackConfig
)Functions
def _auto_prefix() -> str: # Unique prefix string (e.g., "cs0", "cs1")
"""Generate an auto-incrementing unique prefix."""
global _prefix_counter
p = f"cs{_prefix_counter}"
_prefix_counter += 1
return p
def _reset_prefix_counter() -> None
"Generate an auto-incrementing unique prefix."def _reset_prefix_counter() -> None
"Reset the prefix counter (for testing only)."Classes
@dataclass
class CardStackStyleConfig:
"Visual styling for a card stack instance."
section_gap: str = '1rem' # Gap between cards in each section
slot_padding: str = '0.25rem' # Padding around context card content
viewport_padding_x: str = '0.5rem' # Horizontal outer container padding
viewport_padding_y: str = '0.5rem' # Vertical outer container padding
focus_padding_x: str = '0.5rem' # Horizontal focused section padding
focus_padding_b: str = '1rem' # Bottom focused section padding
focus_ring: str = _DEFAULT_FOCUS_RING # Ring classes for focused card
focus_shadow: str = _DEFAULT_FOCUS_SHADOW # Shadow classes for focused card
focus_border_radius: str = _DEFAULT_FOCUS_BORDER_RADIUS # Border radius class for focused card
def css_vars_style(
self,
prefix: str, # Card stack instance prefix
) -> str: # Inline style string with CSS custom property declarations
"Generate CSS custom property declarations as an inline style string."@dataclass
class CardStackConfig:
"Initialization-time settings for a card stack instance."
prefix: str = field(...) # HTML ID prefix (auto-generated if omitted)
visible_count_options: Tuple[int, ...] = (1, 3, 5, 7, 9) # Choices for card count dropdown
card_width_min: int = 30 # Width slider minimum (rem)
card_width_max: int = 120 # Width slider maximum (rem)
card_width_step: int = 5 # Width slider step (rem)
card_scale_min: int = 50 # Scale slider minimum (%)
card_scale_max: int = 200 # Scale slider maximum (%)
card_scale_step: int = 10 # Scale slider step (%)
click_to_focus: bool = False # Whether context cards get transparent click overlay
disable_scroll_in_modes: Tuple[str, ...] = () # Mode names where scroll-to-nav is suppressed
show_scrollbar: bool = True # Show virtual scrollbar for mouse-driven scrubbing
style: CardStackStyleConfig = field(...) # Visual styling configVariables
_prefix_counter: int = 0
_DEFAULT_FOCUS_RING: str
_DEFAULT_FOCUS_SHADOW: str
_DEFAULT_FOCUS_BORDER_RADIUS: strConstants (constants.ipynb)
CSS class constants, type aliases, and default values for the card stack library.
Import
from cjm_fasthtml_card_stack.core.constants import (
CardRole,
SCROLL_THRESHOLD,
NAVIGATION_COOLDOWN,
TRACKPAD_COOLDOWN,
TOUCH_SWIPE_THRESHOLD,
TOUCH_MOMENTUM_MIN_VELOCITY,
TOUCH_MOMENTUM_FRICTION,
TOUCH_PINCH_THRESHOLD,
TOUCH_VELOCITY_SAMPLES,
DEFAULT_VISIBLE_COUNT,
DEFAULT_CARD_WIDTH,
DEFAULT_CARD_SCALE,
width_storage_key,
scale_storage_key,
card_count_storage_key,
auto_count_storage_key
)Functions
def width_storage_key(
prefix: str # Card stack instance prefix
) -> str: # localStorage key for card width
"Generate localStorage key for card width."def scale_storage_key(
prefix: str # Card stack instance prefix
) -> str: # localStorage key for card scale
"Generate localStorage key for card scale."def card_count_storage_key(
prefix: str # Card stack instance prefix
) -> str: # localStorage key for card count
"Generate localStorage key for card count."def auto_count_storage_key(
prefix: str # Card stack instance prefix
) -> str: # localStorage key for auto card count mode
"Generate localStorage key for auto card count mode."Variables
SCROLL_THRESHOLD: int = 1
NAVIGATION_COOLDOWN: int = 100
TRACKPAD_COOLDOWN: int = 250
TOUCH_SWIPE_THRESHOLD: int = 30
TOUCH_MOMENTUM_MIN_VELOCITY: float = 0.5
TOUCH_MOMENTUM_FRICTION: float = 0.95
TOUCH_PINCH_THRESHOLD: int = 30
TOUCH_VELOCITY_SAMPLES: int = 5
DEFAULT_VISIBLE_COUNT: int = 1
DEFAULT_CARD_WIDTH: int = 80
DEFAULT_CARD_SCALE: int = 100Controls (controls.ipynb)
Width slider, scale slider, and card count selector components.
Import
from cjm_fasthtml_card_stack.components.controls import (
render_width_slider,
render_scale_slider,
render_card_count_select
)Functions
def render_width_slider(
config: CardStackConfig, # Card stack configuration
ids: CardStackHtmlIds, # HTML IDs for this instance
card_width: int = 80, # Current card width in rem
) -> Any: # Width slider component
"Render the card stack width slider control."def render_scale_slider(
config: CardStackConfig, # Card stack configuration
ids: CardStackHtmlIds, # HTML IDs for this instance
card_scale: int = 100, # Current scale percentage
) -> Any: # Scale slider component
"Render the card stack scale slider control."def render_card_count_select(
config: CardStackConfig, # Card stack configuration
ids: CardStackHtmlIds, # HTML IDs for this instance
current_count: int = 1, # Currently selected card count
is_auto_mode: bool = True, # Whether auto-adjust mode is active
) -> Any: # Card count dropdown component
"Render the card count dropdown selector."JS: Controls (controls.ipynb)
JavaScript generators for width, scale, and card count management.
Import
from cjm_fasthtml_card_stack.js.controls import *Functions
def _generate_width_mgmt_js(
ids: CardStackHtmlIds, # HTML IDs for this instance
config: CardStackConfig, # Config with slider bounds
urls: CardStackUrls, # URL bundle (save_width)
) -> str: # JS code fragment for width management
"Generate JS for width slider management."def _generate_scale_mgmt_js(
ids: CardStackHtmlIds, # HTML IDs for this instance
config: CardStackConfig, # Config with slider bounds
urls: CardStackUrls, # URL bundle (save_scale)
) -> str: # JS code fragment for scale management
"Generate JS for scale slider management."def _generate_card_count_mgmt_js(
ids: CardStackHtmlIds, # HTML IDs for this instance
config: CardStackConfig, # Config with count options
urls: CardStackUrls, # URL bundle (update_viewport)
) -> str: # JS code fragment for card count management
"Generate JS for card count selector management."JS: Core (core.ipynb)
Master composer for card stack JavaScript. Combines viewport height,
Import
from cjm_fasthtml_card_stack.js.core import (
global_callback_name,
generate_card_stack_js
)Functions
def _generate_coordinator_js(
ids: CardStackHtmlIds, # HTML IDs for this instance
config: CardStackConfig, # Config for prefix-unique listener guards
focus_position: Optional[int] = None, # Focus slot offset (None=center, -1=bottom, 0=top)
) -> str: # JS code fragment for master coordinator
"Generate JS for the master coordinator and HTMX listener."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."def _generate_global_callbacks_js(
config: CardStackConfig, # Config with prefix
) -> str: # JS code fragment registering global wrappers
"Register global wrappers for keyboard navigation system."def generate_card_stack_js(
"Compose all card stack JS into a single namespaced IIFE."Variables
_GLOBAL_CALLBACKSFocus (focus.ipynb)
Focus position resolution, viewport window calculation, and OOB focus sync.
Import
from cjm_fasthtml_card_stack.helpers.focus import (
resolve_focus_slot,
calculate_viewport_window,
render_focus_oob
)Functions
def resolve_focus_slot(
focus_position: Optional[int], # Slot offset (None=center, -1=bottom, 0=top)
visible_count: int, # Number of visible card slots
) -> int: # Resolved 0-indexed slot position
"Resolve focus_position to an actual slot index within the viewport."def calculate_viewport_window(
focused_index: int, # Index of the focused item
total_items: int, # Total number of items
visible_count: int, # Number of visible card slots
focus_position: Optional[int] = None, # Focus slot (None=center)
) -> List[int]: # Item indices for each slot (negative or >= total_items for placeholders)
"Calculate which item indices should be visible in each viewport slot."def render_focus_oob(
focused_index: int, # The item index to focus
ids: CardStackHtmlIds, # HTML IDs for this card stack instance
form_input_name: str = "focused_index", # Field name for the form input
) -> Tuple[Hidden, ...]: # Hidden inputs with OOB swap
"Render OOB hidden inputs to synchronize focus after HTMX swap."Handlers (handlers.ipynb)
Response builder functions for card stack operations (Tier 1 API).
Import
from cjm_fasthtml_card_stack.routes.handlers import (
build_slots_response,
build_nav_response,
card_stack_navigate,
card_stack_navigate_to_index,
card_stack_update_viewport,
card_stack_save_width,
card_stack_save_scale
)Functions
def build_slots_response(
card_items: List[Any], # All data items
state: CardStackState, # Current card stack state
config: CardStackConfig, # Card stack configuration
ids: CardStackHtmlIds, # HTML IDs for this instance
urls: CardStackUrls, # URL bundle for navigation
render_card: Callable, # Card renderer callback
) -> List[Any]: # OOB slot elements (3 viewport sections)
"Build OOB slot updates for the viewport sections only."def build_nav_response(
card_items: List[Any], # All data items
state: CardStackState, # Current card stack state
config: CardStackConfig, # Card stack configuration
ids: CardStackHtmlIds, # HTML IDs for this instance
urls: CardStackUrls, # URL bundle for navigation
render_card: Callable, # Card renderer callback
progress_label: str = "Item", # Label for progress indicator
form_input_name: str = "focused_index", # Name for the focused index hidden input
) -> Tuple: # OOB elements (slots + progress + focus + scrollbar)
"Build full OOB response for navigation: slots + progress + focus inputs + scrollbar."def card_stack_navigate(
direction: str, # "up", "down", "first", "last", "page_up", "page_down"
card_items: List[Any], # All data items
state: CardStackState, # Current card stack state (mutated in place)
config: CardStackConfig, # Card stack configuration
ids: CardStackHtmlIds, # HTML IDs for this instance
urls: CardStackUrls, # URL bundle for navigation
render_card: Callable, # Card renderer callback
progress_label: str = "Item", # Label for progress indicator
form_input_name: str = "focused_index", # Name for the focused index hidden input
) -> Tuple: # OOB elements (slots + progress + focus)
"Navigate to a different item. Mutates state.focused_index in place."def card_stack_navigate_to_index(
target_index: int, # Target item index to navigate to
card_items: List[Any], # All data items
state: CardStackState, # Current card stack state (mutated in place)
config: CardStackConfig, # Card stack configuration
ids: CardStackHtmlIds, # HTML IDs for this instance
urls: CardStackUrls, # URL bundle for navigation
render_card: Callable, # Card renderer callback
progress_label: str = "Item", # Label for progress indicator
form_input_name: str = "focused_index", # Name for the focused index hidden input
) -> Tuple: # OOB elements (slots + progress + focus)
"Navigate to a specific item index. Mutates state.focused_index in place."def card_stack_update_viewport(
visible_count: int, # New number of visible cards
card_items: List[Any], # All data items
state: CardStackState, # Current card stack state (mutated in place)
config: CardStackConfig, # Card stack configuration
ids: CardStackHtmlIds, # HTML IDs for this instance
urls: CardStackUrls, # URL bundle for navigation
render_card: Callable, # Card renderer callback
is_auto: bool = True, # Whether this update came from auto-adjust mode
) -> Tuple: # OOB section elements (3 viewport sections + scrollbar)
"Update viewport with new card count via OOB section swaps. Mutates state in place."def card_stack_save_width(
state: CardStackState, # Current card stack state (mutated in place)
card_width: int, # Card stack width in rem
config: CardStackConfig, # Card stack configuration (for clamping bounds)
) -> None: # No response (swap=none on client)
"Save card stack width. Mutates state.card_width in place."def card_stack_save_scale(
state: CardStackState, # Current card stack state (mutated in place)
card_scale: int, # Card stack scale percentage
config: CardStackConfig, # Card stack configuration (for clamping bounds)
) -> None: # No response (swap=none on client)
"Save card stack scale. Mutates state.card_scale in place."HTML IDs (html_ids.ipynb)
Prefix-based HTML ID generator for card stack DOM elements.
Import
from cjm_fasthtml_card_stack.core.html_ids import (
CardStackHtmlIds
)Classes
@dataclass
class CardStackHtmlIds:
"Prefix-based HTML ID generator for card stack DOM elements."
prefix: str # ID prefix for this card stack instance
def card_stack(self) -> str: # Full-width scroll capture container
"""Outer card stack container."""
return f"{self.prefix}-card-stack"
@property
def card_stack_inner(self) -> str: # Width-constrained CSS Grid container
"Outer card stack container."
def card_stack_inner(self) -> str: # Width-constrained CSS Grid container
"""Inner grid container for 3-section layout."""
return f"{self.prefix}-card-stack-inner"
@property
def card_stack_empty(self) -> str: # Empty state placeholder
"Inner grid container for 3-section layout."
def card_stack_empty(self) -> str: # Empty state placeholder
"""Empty state container."""
return f"{self.prefix}-card-stack-empty"
# --- Viewport sections ---
@property
def viewport_section_before(self) -> str: # Cards before focused (1fr, justify-end)
"Empty state container."
def viewport_section_before(self) -> str: # Cards before focused (1fr, justify-end)
"""Viewport section for context cards before focused card."""
return f"{self.prefix}-viewport-section-before"
@property
def viewport_section_focused(self) -> str: # Focused card (auto)
"Viewport section for context cards before focused card."
def viewport_section_focused(self) -> str: # Focused card (auto)
"""Viewport section for the focused card."""
return f"{self.prefix}-viewport-section-focused"
@property
def viewport_section_after(self) -> str: # Cards after focused (1fr, justify-start)
"Viewport section for the focused card."
def viewport_section_after(self) -> str: # Cards after focused (1fr, justify-start)
"""Viewport section for context cards after focused card."""
return f"{self.prefix}-viewport-section-after"
# --- Dynamic slot IDs ---
def viewport_slot(
self,
item_index: int # Item index (negative or >= total for placeholders)
) -> str: # Slot element ID tied to virtual item position
"Viewport section for context cards after focused card."
def viewport_slot(
self,
item_index: int # Item index (negative or >= total for placeholders)
) -> str: # Slot element ID tied to virtual item position
"ID for a viewport slot. Works for real items and placeholders."
def card_count_select(self) -> str: # Card count dropdown
"""Card count selector dropdown."""
return f"{self.prefix}-card-count-select"
@property
def card_count_slider(self) -> str: # Card count range slider
"Card count selector dropdown."
def card_count_slider(self) -> str: # Card count range slider
"""Card count range slider."""
return f"{self.prefix}-card-count-slider"
@property
def card_count_auto_toggle(self) -> str: # Auto mode toggle
"Card count range slider."
def card_count_auto_toggle(self) -> str: # Auto mode toggle
"""Card count auto mode toggle."""
return f"{self.prefix}-card-count-auto-toggle"
@property
def width_slider(self) -> str: # Width range slider
"Card count auto mode toggle."
def width_slider(self) -> str: # Width range slider
"""Card stack width slider."""
return f"{self.prefix}-width-slider"
@property
def scale_slider(self) -> str: # Scale range slider
"Card stack width slider."
def scale_slider(self) -> str: # Scale range slider
"""Card stack scale slider."""
return f"{self.prefix}-scale-slider"
@property
def settings_modal(self) -> str: # Settings modal dialog
"Card stack scale slider."
def settings_modal(self) -> str: # Settings modal dialog
"""Card stack settings modal."""
return f"{self.prefix}-settings-modal"
# --- Status elements ---
@property
def progress(self) -> str: # Progress indicator
"Card stack settings modal."
def progress(self) -> str: # Progress indicator
"""Progress indicator element."""
return f"{self.prefix}-progress"
@property
def loading(self) -> str: # Loading state container
"Progress indicator element."
def loading(self) -> str: # Loading state container
"""Loading state container."""
return f"{self.prefix}-loading"
# --- Hidden inputs ---
@property
def focused_index_input(self) -> str: # Hidden input for keyboard nav focus recovery
"Loading state container."
def focused_index_input(self) -> str: # Hidden input for keyboard nav focus recovery
"Hidden input storing the focused index for HTMX submissions."Models (models.ipynb)
Core dataclasses for card stack state, render context, and URL routing.
Import
from cjm_fasthtml_card_stack.core.models import (
CardStackState,
CardRenderContext,
CardStackUrls
)Classes
@dataclass
class CardStackState:
"Viewport state for a card stack instance."
focused_index: int = 0 # Index of focused item in the items list
visible_count: int = 1 # Number of card slots visible in viewport (auto-adjust grows from here)
card_width: int = 80 # Max width of card stack inner container in rem
card_scale: int = 100 # Content scale percentage (50-200)
active_mode: Optional[str] # Current interaction mode name (consumer-defined)
focus_position: Optional[int] # Slot offset for focused card (None=center, -1=bottom)
is_auto_mode: bool = True # Whether auto-adjust mode is active@dataclass
class CardRenderContext:
"Context passed to the consumer's render_card callback."
card_role: str # "focused" or "context"
index: int # Item's position in the full items list
total_items: int # Total item count
is_first: bool # Whether this is the first item
is_last: bool # Whether this is the last item
active_mode: Optional[str] # Current interaction mode
card_scale: int # Scale percentage (50-200)
distance_from_focus: int # Signed slot offset from focused card (0=focused)@dataclass
class CardStackUrls:
"URL bundle for card stack navigation and viewport operations."
nav_up: str = '' # Navigate to previous item
nav_down: str = '' # Navigate to next item
nav_first: str = '' # Navigate to first item
nav_last: str = '' # Navigate to last item
nav_page_up: str = '' # Page jump up
nav_page_down: str = '' # Page jump down
nav_to_index: str = '' # Navigate to specific index (click-to-focus)
update_viewport: str = '' # Change visible_count (full viewport re-render)
save_width: str = '' # Persist card_width
save_scale: str = '' # Persist card_scaleProgress (progress.ipynb)
Progress indicator showing the current position within the card stack.
Import
from cjm_fasthtml_card_stack.components.progress import (
render_progress_indicator
)Functions
def render_progress_indicator(
focused_index: int, # Currently focused item index (0-based)
total_items: int, # Total number of items
ids: CardStackHtmlIds, # HTML IDs for this card stack instance
label: str = "Item", # Label prefix (e.g., "Item", "Segment", "Card")
oob: bool = False, # Whether to render as OOB swap
) -> Any: # Progress indicator component
"Render position indicator showing current item in the collection."Router (router.ipynb)
Convenience router factory that wires up standard card stack routes (Tier 2 API).
Import
from cjm_fasthtml_card_stack.routes.router import (
init_card_stack_router
)Functions
def init_card_stack_router(
config: CardStackConfig, # Card stack configuration
state_getter: Callable[[], CardStackState], # Function to get current state
state_setter: Callable[[CardStackState], None], # Function to save state
get_items: Callable[[], List[Any]], # Function to get current items list
render_card: Callable, # Card renderer callback: (item, CardRenderContext) -> FT
route_prefix: str = "/card-stack", # Route prefix for all card stack routes
progress_label: str = "Item", # Label for progress indicator
) -> Tuple[APIRouter, CardStackUrls]: # (router, urls) tuple
"Initialize an APIRouter with all standard card stack routes."Settings Modal (settings_modal.ipynb)
Modal-based card stack settings with card count slider (auto toggle), width slider, and optional scale slider.
Import
from cjm_fasthtml_card_stack.components.settings_modal import (
render_settings_trigger,
render_card_stack_settings_modal
)Functions
def _render_section_label(
text: str, # label text
) -> Span: # styled label element
"Render a settings section label."def _render_slider_with_labels(
slider_id: str, # HTML ID for the range input
min_val: int, # slider minimum
max_val: int, # slider maximum
step_val: int, # slider step
current_val: int, # current value
low_label: str, # label for low end
high_label: str, # label for high end
oninput: str, # JS oninput handler
disabled: bool = False, # whether slider is disabled
) -> Div: # slider row with end labels
"Render a range slider with low/high labels."def _render_card_count_section(
config: CardStackConfig, # card stack config with visible_count_options
ids: CardStackHtmlIds, # HTML IDs for this instance
current_count: int = 1, # currently selected card count
is_auto_mode: bool = True, # whether auto-adjust mode is active
) -> Div: # card count section with auto toggle and slider
"Render the card count section with auto toggle and discrete slider."def render_settings_trigger(
modal_id: str, # ID of the settings modal dialog to open
icon_size: int = 4, # lucide icon size
) -> Button: # ghost button with sliders-horizontal icon
"Render a settings icon button that opens the card stack settings modal."def render_card_stack_settings_modal(
config: CardStackConfig, # card stack configuration
ids: CardStackHtmlIds, # HTML IDs for this instance
current_count: int = 1, # currently selected card count
is_auto_mode: bool = True, # whether auto-adjust mode is active
card_width: int = 80, # current card width in rem
show_scale: bool = False, # whether to include the scale slider
card_scale: int = 100, # current scale percentage (if show_scale=True)
title: str = "Display Settings", # modal title text
) -> tuple[FT, FT]: # (modal_dialog, trigger_button)
"""
Render a modal-based card stack settings panel.
Returns two components:
- `modal_dialog`: The Dialog element (place anywhere in page)
- `trigger_button`: Small settings icon button (place in toolbar)
"""States (states.ipynb)
Loading, empty, and placeholder card components for the card stack viewport.
Import
from cjm_fasthtml_card_stack.components.states import (
render_placeholder_card,
render_loading_state,
render_empty_state
)Functions
def render_placeholder_card(
placeholder_type: Literal["start", "end"], # Which edge of the list
) -> Any: # Placeholder card component
"Render a placeholder card for viewport edges."def render_loading_state(
ids: CardStackHtmlIds, # HTML IDs for this card stack instance
message: str = "Loading...", # Loading message text
) -> Any: # Loading component
"Render loading state with spinner and message."def render_empty_state(
ids: CardStackHtmlIds, # HTML IDs for this card stack instance
title: str = "No items available", # Main empty state message
subtitle: str = "", # Optional subtitle text
) -> Any: # Empty state component
"Render empty state when no items exist."sync (sync.ipynb)
Synced navigation between two card stacks — source stack navigation drives target stack to same index
Import
from cjm_fasthtml_card_stack.js.sync import (
generate_card_stack_sync_js
)Functions
def generate_card_stack_sync_js(
source_input_id:str, # ID of source stack's focused_index hidden input
target_nav_url:str, # URL for target stack's nav_to_index route
toggle_fn_name:str="toggleSyncedNav", # Name for window toggle function
sync_key:str="_cardStackSync", # Window key for state + cleanup
) -> str: # Standalone JS snippet (not inside any IIFE)
"""
Generate JS for synced navigation between two card stacks.
Source stack navigation drives target stack to the same focused index.
Toggle on/off via window[toggle_fn_name]() or toolbar button.
The settle handler and source input lookup are deferred — they work
even if the source stack hasn't initialized when this JS first runs.
Out-of-range indices are clamped server-side by nav_to_index.
"""Viewport (viewport.ipynb)
Card stack viewport with 3-section CSS Grid layout, slot rendering,
Import
from cjm_fasthtml_card_stack.components.viewport import (
render_slot_card,
render_all_slots_oob,
render_card_stack_scrollbar,
render_viewport
)Functions
def _render_mode_sync_script(
active_mode: Optional[str] = None, # Active keyboard mode name (None = navigation)
zone_id: str = "", # Card stack zone ID (skip sync if another zone is active)
) -> Any: # Script element that syncs keyboard mode state
"""
Generate script to sync keyboard navigation mode with rendered UI state.
When zone_id is provided, only syncs if this zone is the currently active zone.
This prevents a dual-stack OOB response from one stack exiting split mode
on the other stack's keyboard system.
"""def _render_click_overlay(
item_index: int, # Index of the item this slot represents
urls: CardStackUrls, # URL bundle for navigation
) -> Any: # Transparent click overlay element
"Render transparent click-to-focus overlay for a context card slot."def render_slot_card(
slot_index: int, # Index of this slot in the viewport (0-based)
focus_slot: int, # Which slot is the focused position
card_items: List[Any], # Full items list
item_index: int, # Item index (negative or >= len for placeholder)
render_card: Callable, # Callback: (item, CardRenderContext) -> FT
state: CardStackState, # Current card stack state
config: CardStackConfig, # Card stack configuration
ids: CardStackHtmlIds, # HTML IDs for this instance
urls: CardStackUrls, # URL bundle for navigation
oob: bool = False, # Whether to render as OOB swap
) -> Any: # Slot content wrapper
"Render a single card for a viewport slot."def render_all_slots_oob(
card_items: List[Any], # All data items
state: CardStackState, # Current card stack state
config: CardStackConfig, # Card stack configuration
ids: CardStackHtmlIds, # HTML IDs for this instance
urls: CardStackUrls, # URL bundle for navigation
render_card: Callable, # Card renderer callback
) -> List[Any]: # List of OOB elements (3 sections)
"Render all viewport sections with OOB swap for granular updates."def _grid_template_rows(
focus_position: Optional[int] = None, # Focus slot offset (None=center, -1=bottom, 0=top)
) -> str: # CSS grid-template-rows value
"Compute CSS grid-template-rows based on focus position intent."def _map_to_scrollbar(
state: CardStackState,
config: CardStackConfig,
total_items: int,
): # (ScrollbarState, ScrollbarConfig, ScrollbarIds)
"Map card stack types to scrollbar lib types."def render_card_stack_scrollbar(
state: CardStackState, # Card stack state
config: CardStackConfig, # Card stack config
total_items: int, # Total item count
oob: bool = False, # Whether to include hx-swap-oob
) -> Any: # Scrollbar element (or hidden div if not needed)
"Render the virtual scrollbar for a card stack instance."def render_viewport(
card_items: List[Any], # All data items
state: CardStackState, # Current card stack state
config: CardStackConfig, # Card stack configuration
ids: CardStackHtmlIds, # HTML IDs for this instance
urls: CardStackUrls, # URL bundle for navigation
render_card: Callable, # Card renderer callback
form_input_name: str = "focused_index", # Name for the focused index hidden input
) -> Any: # Viewport component with 3-section layout
"Render the card stack viewport with 3-section CSS Grid layout."JS: Viewport Height (viewport.ipynb)
JavaScript generator for dynamic viewport height calculation.
Import
from cjm_fasthtml_card_stack.js.viewport import (
generate_viewport_height_js
)Functions
def generate_viewport_height_js(
ids: CardStackHtmlIds, # HTML IDs for this card stack instance
container_id: str = "", # Unused — kept for API compatibility
) -> str: # JavaScript code fragment for viewport height calculation
"""
Generate JS for dynamic viewport height calculation via cjm-fasthtml-viewport-fit.
Delegates to the viewport-fit library's individual generator functions.
The card stack coordinator handles HTMX settle events separately.
"""