cjm-fasthtml-card-stack

A fixed-viewport card stack component for FastHTML with keyboard navigation, scroll-to-nav, configurable focus position, and HTMX-driven OOB updates.

Install

pip install cjm_fasthtml_card_stack

Project 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."

Button IDs (button_ids.ipynb)

Prefix-based IDs for hidden keyboard action buttons.

Import

from cjm_fasthtml_card_stack.core.button_ids import (
    CardStackButtonIds
)

Classes

@dataclass
class CardStackButtonIds:
    "Prefix-based IDs for hidden keyboard action buttons."
    
    prefix: str  # ID prefix for this card stack instance
    
    def nav_up(self) -> str:  # Navigate to previous item
            """Navigate up button."""
            return f"{self.prefix}-btn-nav-up"
    
        @property
        def nav_down(self) -> str:  # Navigate to next item
        "Navigate up button."
    
    def nav_down(self) -> str:  # Navigate to next item
            """Navigate down button."""
            return f"{self.prefix}-btn-nav-down"
    
        @property
        def nav_first(self) -> str:  # Navigate to first item
        "Navigate down button."
    
    def nav_first(self) -> str:  # Navigate to first item
            """Navigate to first item button."""
            return f"{self.prefix}-btn-nav-first"
    
        @property
        def nav_last(self) -> str:  # Navigate to last item
        "Navigate to first item button."
    
    def nav_last(self) -> str:  # Navigate to last item
            """Navigate to last item button."""
            return f"{self.prefix}-btn-nav-last"
    
        @property
        def nav_page_up(self) -> str:  # Page jump up
        "Navigate to last item button."
    
    def nav_page_up(self) -> str:  # Page jump up
            """Page up button."""
            return f"{self.prefix}-btn-nav-page-up"
    
        @property
        def nav_page_down(self) -> str:  # Page jump down
        "Page up button."
    
    def nav_page_down(self) -> str:  # Page jump down
            """Page down button."""
            return f"{self.prefix}-btn-nav-page-down"
    
        # --- Viewport control buttons ---
    
        @property
        def width_narrow(self) -> str:  # Decrease viewport width
        "Page down button."
    
    def width_narrow(self) -> str:  # Decrease viewport width
            """Narrow viewport button."""
            return f"{self.prefix}-btn-width-narrow"
    
        @property
        def width_widen(self) -> str:  # Increase viewport width
        "Narrow viewport button."
    
    def width_widen(self) -> str:  # Increase viewport width
            """Widen viewport button."""
            return f"{self.prefix}-btn-width-widen"
    
        @property
        def scale_decrease(self) -> str:  # Decrease content scale
        "Widen viewport button."
    
    def scale_decrease(self) -> str:  # Decrease content scale
            """Decrease scale button."""
            return f"{self.prefix}-btn-scale-decrease"
    
        @property
        def scale_increase(self) -> str:  # Increase content scale
        "Decrease scale button."
    
    def scale_increase(self) -> str:  # Increase content scale
        "Increase scale button."

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 config

Variables

_prefix_counter: int = 0
_DEFAULT_FOCUS_RING: str
_DEFAULT_FOCUS_SHADOW: str
_DEFAULT_FOCUS_BORDER_RADIUS: str

Constants (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 = 100

Controls (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_CALLBACKS

Focus (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_scale

JS: Page Navigation (navigation.ipynb)

JavaScript generator for page jump and first/last navigation helpers.

Import

from cjm_fasthtml_card_stack.js.navigation import (
    generate_page_nav_js
)

Functions

def generate_page_nav_js(
    button_ids: CardStackButtonIds,  # Button IDs for navigation triggers
) -> str:  # JavaScript code fragment for page navigation
    "Generate JS for page-based and first/last navigation functions."

Progress (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."

JS: Scroll Navigation (scroll.ipynb)

JavaScript generator for scroll-to-nav conversion.

Import

from cjm_fasthtml_card_stack.js.scroll import (
    generate_scroll_nav_js
)

Functions

def generate_scroll_nav_js(
    ids: CardStackHtmlIds,  # HTML IDs for this card stack instance
    button_ids: CardStackButtonIds,  # Button IDs for navigation triggers
    disable_in_modes: Tuple[str, ...] = (),  # Mode names where scroll nav is suppressed
    zone_id: str = "",  # Keyboard zone ID to activate on scroll interaction
) -> str:  # JavaScript code fragment for scroll navigation
    "Generate JS for scroll wheel to navigation conversion."

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.
    """

JS: Touch Navigation (touch.ipynb)

JavaScript generator for touch-to-nav conversion: swipe, drag,

Import

from cjm_fasthtml_card_stack.js.touch import (
    generate_touch_nav_js
)

Functions

def generate_touch_nav_js(
    ids: CardStackHtmlIds,  # HTML IDs for this card stack instance
    button_ids: CardStackButtonIds,  # Button IDs for navigation triggers
    disable_in_modes: Tuple[str, ...] = (),  # Mode names where touch nav is suppressed
    zone_id: str = "",  # Keyboard zone ID to activate on touch interaction
) -> str:  # JavaScript code fragment for touch navigation
    "Generate JS for touch gesture to navigation conversion."

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.
    """