Skip to main content

FastHTML virtualized collection rendering with discrete navigation, custom scrollbar, table layout, and cell-level HTMX updates.

Project description

cjm-fasthtml-virtual-collection

Install

pip install cjm_fasthtml_virtual_collection

Project Structure

nbs/
├── components/ (4)
│   ├── collection.ipynb  # Main entry point for rendering a virtual collection.
│   ├── footer.ipynb      # Footer component showing item range indicator.
│   ├── scrollbar.ipynb   # Custom scrollbar component with proportional thumb for position indication.
│   └── table.ipynb       # Table layout rendering: header row, data rows, and cells using CSS table display.
├── core/ (4)
│   ├── button_ids.ipynb  # Hidden button ID generators for navigation triggers.
│   ├── html_ids.ipynb    # HTML element ID generators for virtual collection components.
│   ├── models.ipynb      # Data models for virtual collection state, configuration, column definitions, render contexts, and URL bundles.
│   └── windowing.ipynb   # Pure math functions for viewport window and scrollbar calculations.
├── js/ (4)
│   ├── auto_fit.ipynb   # JavaScript generator for overflow-based automatic visible row count adjustment.
│   ├── scroll.ipynb     # JavaScript generator for scroll wheel to navigation conversion.
│   ├── scrollbar.ipynb  # JavaScript generator for custom scrollbar interaction (drag thumb, click track).
│   └── touch.ipynb      # JavaScript generator for touch/swipe to navigation conversion.
├── keyboard/ (1)
│   └── actions.ipynb  # Keyboard navigation focus zone and action factories for the virtual collection.
└── routes/ (2)
    ├── handlers.ipynb  # Response builder functions for virtual collection navigation (Tier 1 API).
    └── router.ipynb    # Convenience router factory that wires up standard virtual collection routes (Tier 2 API).

Total: 15 notebooks across 5 directories

Module Dependencies

graph LR
    components_collection[components.collection<br/>components.collection]
    components_footer[components.footer<br/>components.footer]
    components_scrollbar[components.scrollbar<br/>components.scrollbar]
    components_table[components.table<br/>components.table]
    core_button_ids[core.button_ids<br/>core.button_ids]
    core_html_ids[core.html_ids<br/>core.html_ids]
    core_models[core.models<br/>core.models]
    core_windowing[core.windowing<br/>core.windowing]
    js_auto_fit[js.auto_fit<br/>js.auto_fit]
    js_scroll[js.scroll<br/>js.scroll]
    js_scrollbar[js.scrollbar<br/>js.scrollbar]
    js_touch[js.touch<br/>js.touch]
    keyboard_actions[keyboard.actions<br/>keyboard.actions]
    routes_handlers[routes.handlers<br/>routes.handlers]
    routes_router[routes.router<br/>routes.router]

    components_collection --> core_models
    components_collection --> components_scrollbar
    components_collection --> components_table
    components_collection --> core_html_ids
    components_collection --> components_footer
    components_footer --> core_windowing
    components_footer --> core_html_ids
    components_footer --> core_models
    components_scrollbar --> core_windowing
    components_scrollbar --> core_models
    components_scrollbar --> core_html_ids
    components_table --> core_models
    components_table --> core_html_ids
    js_auto_fit --> core_models
    js_auto_fit --> core_html_ids
    js_scroll --> core_html_ids
    js_scroll --> core_button_ids
    js_scrollbar --> core_models
    js_scrollbar --> core_html_ids
    js_scrollbar --> core_button_ids
    js_touch --> core_models
    js_touch --> core_html_ids
    js_touch --> core_button_ids
    keyboard_actions --> core_models
    keyboard_actions --> core_html_ids
    keyboard_actions --> core_button_ids
    routes_handlers --> core_windowing
    routes_handlers --> core_models
    routes_handlers --> components_table
    routes_handlers --> core_html_ids
    routes_handlers --> components_footer
    routes_router --> core_models
    routes_router --> routes_handlers
    routes_router --> core_html_ids

34 cross-module dependencies detected

CLI Reference

No CLI commands found in this project.

Module Overview

Detailed documentation for each module in the project:

keyboard.actions (actions.ipynb)

Keyboard navigation focus zone and action factories for the virtual collection.

Import

from cjm_fasthtml_virtual_collection.keyboard.actions import (
    create_collection_focus_zone,
    create_collection_nav_actions,
    build_collection_url_map,
    apply_nav_sync
)

Functions

def create_collection_focus_zone(
    ids: VirtualCollectionHtmlIds,  # HTML IDs for this collection instance
    hidden_input_prefix: Optional[str] = None,  # Prefix for keyboard nav hidden inputs
) -> FocusZone:  # Configured focus zone for the collection
    "Create a focus zone for a virtual collection viewport."
def create_collection_nav_actions(
    zone_id: str,  # Focus zone ID to restrict actions to
    button_ids: VirtualCollectionButtonIds,  # Button IDs for HTMX triggers
    disable_in_modes: Tuple[str, ...] = (),  # Mode names that disable navigation
) -> Tuple[KeyAction, ...]:  # Standard collection navigation actions
    "Create standard keyboard navigation actions for a virtual collection."
def build_collection_url_map(
    button_ids: VirtualCollectionButtonIds,  # Button IDs for this collection
    urls: VirtualCollectionUrls,  # URL bundle for routing
) -> Dict[str, str]:  # Mapping of button ID -> route URL
    "Build url_map for render_keyboard_system with all collection buttons."
def apply_nav_sync(
    kb_system: KeyboardSystem,         # Rendered keyboard system to patch
    ids: VirtualCollectionHtmlIds,     # HTML IDs for this collection
) -> None:  # Modifies kb_system.action_buttons in place
    """
    Add hx-sync to nav buttons so rapid input aborts stale in-flight requests.
    
    Prevents race conditions between cursor-only and full-window OOB responses
    during rapid keyboard or scroll wheel navigation.
    """

js.auto_fit (auto_fit.ipynb)

JavaScript generator for overflow-based automatic visible row count adjustment.

Import

from cjm_fasthtml_virtual_collection.js.auto_fit import (
    generate_auto_fit_js,
    auto_fit_callback_name
)

Functions

def generate_auto_fit_js(
    ids: VirtualCollectionHtmlIds,       # HTML IDs for this collection
    config: VirtualCollectionConfig,      # Collection config
    urls: VirtualCollectionUrls,          # URL bundle (for update_viewport)
    total_items: int = 0,                 # Total item count
    initial_visible: int = 1,             # Initial visible row count
) -> str:  # JavaScript code fragment
    """
    Generate JS for overflow-based auto-fit of visible row count.
    
    Measures actual table overflow against wrapper height. Grows incrementally
    with opacity:0 validation, shrinks via batch estimation. Adapted from the
    cjm-fasthtml-card-stack auto_adjust pattern.
    """
def auto_fit_callback_name(
    config: VirtualCollectionConfig,  # Collection config (for prefix)
) -> str:  # Global JS function name
    "Get the global callback name for viewport-fit's resize_callback."

core.button_ids (button_ids.ipynb)

Hidden button ID generators for navigation triggers.

Import

from cjm_fasthtml_virtual_collection.core.button_ids import (
    VirtualCollectionButtonIds
)

Classes

@dataclass
class VirtualCollectionButtonIds:
    "Hidden button IDs for keyboard/scroll navigation triggers."
    
    prefix: str  # Instance prefix (e.g., 'vc0', 'fb')
    
    def nav_up(self) -> str: return f"{self.prefix}-btn-nav-up"
    
        @property
        def nav_down(self) -> str: return f"{self.prefix}-btn-nav-down"
    
    def nav_down(self) -> str: return f"{self.prefix}-btn-nav-down"
    
        @property
        def nav_page_up(self) -> str: return f"{self.prefix}-btn-nav-page-up"
    
    def nav_page_up(self) -> str: return f"{self.prefix}-btn-nav-page-up"
    
        @property
        def nav_page_down(self) -> str: return f"{self.prefix}-btn-nav-page-down"
    
    def nav_page_down(self) -> str: return f"{self.prefix}-btn-nav-page-down"
    
        @property
        def nav_first(self) -> str: return f"{self.prefix}-btn-nav-first"
    
    def nav_first(self) -> str: return f"{self.prefix}-btn-nav-first"
    
        @property
        def nav_last(self) -> str: return f"{self.prefix}-btn-nav-last"
    
    def nav_last(self) -> str: return f"{self.prefix}-btn-nav-last"
    
        # -- Action buttons --
        @property
        def activate(self) -> str: return f"{self.prefix}-btn-activate"
    
    def activate(self) -> str: return f"{self.prefix}-btn-activate"

components.collection (collection.ipynb)

Main entry point for rendering a virtual collection.

Import

from cjm_fasthtml_virtual_collection.components.collection import (
    render_virtual_collection
)

Functions

def render_virtual_collection(
    items: list,                                # Full item list
    config: VirtualCollectionConfig,             # Collection config
    state: VirtualCollectionState,               # Collection state
    ids: VirtualCollectionHtmlIds,               # HTML IDs
    urls: VirtualCollectionUrls,                 # URL bundle
    render_cell: Optional[Callable] = None,      # Table layout cell render callback
    render_item: Optional[Callable] = None,      # Grid layout item render callback
    render_empty: Optional[Callable] = None,     # Empty state callback: () -> FT component
) -> Div:  # Complete collection element
    "Render a complete virtual collection with wrapper, table, scrollbar, and footer."

components.footer (footer.ipynb)

Footer component showing item range indicator.

Import

from cjm_fasthtml_virtual_collection.components.footer import (
    render_footer
)

Functions

def render_footer(state: VirtualCollectionState,     # Collection state
                  ids: VirtualCollectionHtmlIds,      # HTML IDs
                  oob: bool = False,                  # Whether to include hx-swap-oob
                 ) -> Div:  # Footer element
    "Render the footer with item range indicator."

routes.handlers (handlers.ipynb)

Response builder functions for virtual collection navigation (Tier 1 API).

Import

from cjm_fasthtml_virtual_collection.routes.handlers import (
    build_nav_response,
    build_cursor_move_response,
    handle_navigate,
    handle_navigate_to_index,
    handle_update_viewport,
    handle_focus_row,
    handle_activate,
    handle_sort
)

Functions

def _render_window_start_oob(
    state: VirtualCollectionState,     # Current state
    ids: VirtualCollectionHtmlIds,     # HTML IDs
) -> Hidden:  # Hidden input with OOB swap
    "Render OOB hidden input carrying the current window_start for JS thumb positioning."
def build_nav_response(
    items: list,                            # Full item list
    state: VirtualCollectionState,          # Current state (already mutated)
    config: VirtualCollectionConfig,        # Collection config
    ids: VirtualCollectionHtmlIds,          # HTML IDs
    render_cell: Callable,                  # Consumer cell render callback
    focus_url: str = "",                    # URL for click-to-focus
) -> Tuple:  # OOB elements (slot OOBs + footer + window_start input)
    "Build OOB response for navigation: all visible slots + footer + window_start."
def build_cursor_move_response(
    old_cursor: int,                        # Previous cursor index
    items: list,                            # Full item list
    state: VirtualCollectionState,          # Current state (cursor already updated)
    config: VirtualCollectionConfig,        # Collection config
    ids: VirtualCollectionHtmlIds,          # HTML IDs
    render_cell: Callable,                  # Consumer cell render callback
    focus_url: str = "",                    # URL for click-to-focus
) -> Tuple:  # OOB elements (affected slot OOBs + footer + window_start input)
    "Build OOB response for cursor-only move: swap just the affected slots."
def _is_cursor_visible(
    state: VirtualCollectionState,  # Current state
) -> bool:  # Whether cursor is within the visible window
    "Check if the cursor index is within the current visible window."
def _append_cursor_change(
    result: Tuple,                          # Base response tuple
    items: list,                            # Full item list
    state: VirtualCollectionState,          # Current state
    on_cursor_change: Optional[Callable],   # Callback: (item, cursor_index, state) -> Tuple
) -> Tuple:  # Response with appended cursor change OOB elements
    "Append on_cursor_change callback results to a response tuple."
def _scroll_to_cursor(
    state: VirtualCollectionState,  # Current state (mutated in place)
) -> None
    "Scroll window to bring an off-screen cursor into view."
def handle_navigate(
    direction: str,                         # 'up', 'down', 'page_up', 'page_down', 'first', 'last'
    items: list,                            # Full item list
    state: VirtualCollectionState,          # Current state (mutated in place)
    config: VirtualCollectionConfig,        # Collection config
    ids: VirtualCollectionHtmlIds,          # HTML IDs
    render_cell: Callable,                  # Consumer cell render callback
    focus_url: str = "",                    # URL for click-to-focus
    is_skippable: Optional[Callable[[Any], bool]] = None,  # Predicate: item -> skip?
    on_cursor_change: Optional[Callable] = None,  # Callback: (item, cursor_index, state) -> Tuple
) -> Tuple:  # OOB elements
    "Navigate in a direction. Mutates state in place."
def _call_action_callback(
    callback: Callable,  # Consumer callback to invoke
    item: Any,  # Item at the cursor position
    row_index: int,  # Row index
    state: VirtualCollectionState,  # Current VC state
    request: Any = None,  # FastHTML request (passed if callback accepts it)
) -> Any:  # Callback result
    "Call an action callback, passing request if the callback signature accepts it."
def handle_navigate_to_index(
    target_index: int,                      # Target window_start
    items: list,                            # Full item list
    state: VirtualCollectionState,          # Current state (mutated in place)
    config: VirtualCollectionConfig,        # Collection config
    ids: VirtualCollectionHtmlIds,          # HTML IDs
    render_cell: Callable,                  # Consumer cell render callback
    focus_url: str = "",                    # URL for click-to-focus
) -> Tuple:  # OOB elements
    "Navigate to a specific index. Mutates state.window_start in place."
def _build_container_response(
    items: list,                            # Full item list
    state: VirtualCollectionState,          # Current state
    config: VirtualCollectionConfig,        # Collection config
    ids: VirtualCollectionHtmlIds,          # HTML IDs
    render_cell: Callable,                  # Consumer cell render callback
    focus_url: str = "",                    # URL for click-to-focus
) -> Tuple:  # OOB elements (container + footer + window_start input)
    "Build OOB response that replaces the entire rows container with new slots."
def handle_update_viewport(
    visible_rows: int,                      # New visible row count
    items: list,                            # Full item list
    state: VirtualCollectionState,          # Current state (mutated in place)
    config: VirtualCollectionConfig,        # Collection config
    ids: VirtualCollectionHtmlIds,          # HTML IDs
    render_cell: Callable,                  # Consumer cell render callback
    is_auto: bool = True,                   # Whether from auto-fit
    focus_url: str = "",                    # URL for click-to-focus
) -> Tuple:  # OOB elements
    "Update viewport with new row count. Mutates state in place."
def handle_focus_row(
    row_index: int,                         # Row index to focus
    items: list,                            # Full item list
    state: VirtualCollectionState,          # Current state (mutated in place)
    config: VirtualCollectionConfig,        # Collection config
    ids: VirtualCollectionHtmlIds,          # HTML IDs
    render_cell: Callable,                  # Consumer cell render callback
    focus_url: str = "",                    # URL for click-to-focus
    on_refocus: Optional[Callable] = None,  # Callback when clicking already-focused row: (item, row_index, state) -> Tuple
    is_skippable: Optional[Callable[[Any], bool]] = None,  # Predicate: item -> skip?
    on_cursor_change: Optional[Callable] = None,  # Callback: (item, cursor_index, state) -> Tuple
    request: Any = None,  # FastHTML request (passed to on_refocus if it accepts it)
) -> Tuple:  # OOB elements (affected slot OOBs + footer + window_start input)
    """
    Move cursor to a specific row via click/tap.
    
    If the clicked row is skippable, returns empty (no-op).
    If `on_refocus` is provided and the clicked row is already the cursor,
    delegates to `on_refocus` instead of the normal cursor-move logic.
    """
def handle_activate(
    items: list,                            # Full item list
    state: VirtualCollectionState,          # Current state
    config: VirtualCollectionConfig,        # Collection config
    ids: VirtualCollectionHtmlIds,          # HTML IDs
    render_cell: Callable,                  # Consumer cell render callback
    on_activate: Callable,                  # Consumer callback: (item, row_index, state[, request]) -> Tuple of OOB elements
    focus_url: str = "",                    # URL for click-to-focus
    request: Any = None,  # FastHTML request (passed to on_activate if it accepts it)
) -> Tuple:  # OOB elements from consumer callback
    "Activate the focused row via Space/Enter. Delegates to consumer callback."
def handle_sort(
    column_key: str,                        # Column key to sort by
    items: list,                            # Full item list
    state: VirtualCollectionState,          # Current state (mutated in place)
    config: VirtualCollectionConfig,        # Collection config
    ids: VirtualCollectionHtmlIds,          # HTML IDs
    render_cell: Callable,                  # Consumer cell render callback
    sort_callback: Callable,                # Consumer: (items, column_key, ascending) -> sorted items
    sort_url: str = "",                     # Sort URL for header re-render
    focus_url: str = "",                    # URL for click-to-focus
    is_skippable: Optional[Callable[[Any], bool]] = None,  # Predicate: item -> skip?
    on_cursor_change: Optional[Callable] = None,  # Callback: (item, cursor_index, state) -> Tuple
) -> Tuple:  # OOB elements (header + rows + footer + window_start)
    "Sort by column. Toggles direction if same column, resets window to start."

core.html_ids (html_ids.ipynb)

HTML element ID generators for virtual collection components.

Import

from cjm_fasthtml_virtual_collection.core.html_ids import (
    VirtualCollectionHtmlIds
)

Classes

@dataclass
class VirtualCollectionHtmlIds:
    "HTML element ID generators for a virtual collection instance."
    
    prefix: str  # Instance prefix (e.g., 'vc0', 'fb')
    
    def collection(self) -> str: return f"{self.prefix}-collection"
    
        # -- Wrapper (viewport-fit target) --
        @property
        def wrapper(self) -> str: return f"{self.prefix}-wrapper"
    
    def wrapper(self) -> str: return f"{self.prefix}-wrapper"
    
        # -- Table container --
        @property
        def table(self) -> str: return f"{self.prefix}-table"
    
    def table(self) -> str: return f"{self.prefix}-table"
    
        # -- Header --
        @property
        def header(self) -> str: return f"{self.prefix}-header"
    
    def header(self) -> str: return f"{self.prefix}-header"
    
        # -- Viewport (kept for viewport-fit compatibility) --
        @property
        def viewport(self) -> str: return f"{self.prefix}-viewport"
    
    def viewport(self) -> str: return f"{self.prefix}-viewport"
    
        # -- Rows container (table-row-group) --
        @property
        def rows(self) -> str: return f"{self.prefix}-rows"
    
    def rows(self) -> str: return f"{self.prefix}-rows"
    
        # -- Slot IDs (position-based, display:contents wrappers) --
        def slot_id(self, slot_index: int) -> str:  # ID for a viewport slot
    
    def slot_id(self, slot_index: int) -> str:  # ID for a viewport slot
            return f"{self.prefix}-slot-{slot_index}"
    
        # -- Dynamic row/cell/item IDs (data-based, on inner content) --
        def row_id(self, index: int) -> str:  # ID for a table row
    
    def row_id(self, index: int) -> str:  # ID for a table row
            return f"{self.prefix}-row-{index}"
    
        def cell_id(self, row_index: int, col_key: str) -> str:  # ID for a table cell
    
    def cell_id(self, row_index: int, col_key: str) -> str:  # ID for a table cell
            return f"{self.prefix}-row-{row_index}-col-{col_key}"
    
        def item_id(self, index: int) -> str:  # ID for a grid item
    
    def item_id(self, index: int) -> str:  # ID for a grid item
            return f"{self.prefix}-item-{index}"
    
        # -- Custom scrollbar --
        @property
        def scrollbar_track(self) -> str: return f"{self.prefix}-scrollbar-track"
    
    def scrollbar_track(self) -> str: return f"{self.prefix}-scrollbar-track"
    
        @property
        def scrollbar_thumb(self) -> str: return f"{self.prefix}-scrollbar-thumb"
    
    def scrollbar_thumb(self) -> str: return f"{self.prefix}-scrollbar-thumb"
    
        # -- Footer --
        @property
        def footer(self) -> str: return f"{self.prefix}-footer"
    
    def footer(self) -> str: return f"{self.prefix}-footer"
    
        # -- Progress indicator --
        @property
        def progress(self) -> str: return f"{self.prefix}-progress"
    
    def progress(self) -> str: return f"{self.prefix}-progress"
    
        # -- Hidden inputs --
        @property
        def window_start_input(self) -> str: return f"{self.prefix}-window-start-input"
    
    def window_start_input(self) -> str: return f"{self.prefix}-window-start-input"

core.models (models.ipynb)

Data models for virtual collection state, configuration, column definitions, render contexts, and URL bundles.

Import

from cjm_fasthtml_virtual_collection.core.models import (
    ColumnDef,
    VirtualCollectionConfig,
    VirtualCollectionState,
    RowRenderContext,
    CellRenderContext,
    VirtualCollectionUrls
)

Functions

def _auto_prefix() -> str:  # Auto-generated prefix like 'vc0', 'vc1', etc.
    "Generate a unique prefix for a virtual collection instance."

Classes

@dataclass
class ColumnDef:
    "Column definition for table layout."
    
    key: str  # Unique column identifier (used in cell IDs)
    header: str = ''  # Display text for header
    sortable: bool = False  # Whether column header is clickable for sort
    header_cls: str = ''  # Additional CSS classes for header cell
    cell_cls: str = ''  # Additional CSS classes for data cells
@dataclass
class VirtualCollectionConfig:
    "Initialization-time configuration for a virtual collection."
    
    prefix: str = ''  # HTML ID prefix (auto-generated if empty)
    layout: str = 'table'  # 'table' or 'grid'
    columns: Tuple[ColumnDef, ...] = ()  # Column definitions
    columns_per_row: int = 4  # Items per grid row
    grid_gap: str = '1rem'  # Gap between grid items
    disable_scroll_in_modes: Tuple[str, ...] = ()  # Mode-based scroll suppression
    show_scrollbar: bool = True  # Show custom scrollbar
    min_thumb_height: int = 24  # Minimum scrollbar thumb height (px)
    
@dataclass
class VirtualCollectionState:
    "Mutable runtime state for a virtual collection."
    
    window_start: int = 0  # First visible row index
    visible_rows: int = 0  # Number of visible rows (from auto-fit)
    total_items: int = 0  # Total item count (set by consumer)
    viewport_height: float = 0.0  # Measured viewport height (px)
    is_auto_mode: bool = True  # Auto-adjust visible rows from viewport
    cursor_index: int = -1  # Keyboard cursor position (-1 = none)
    sort_column: str = ''  # Current sort column key (empty = unsorted)
    sort_ascending: bool = True  # Sort direction
@dataclass
class RowRenderContext:
    "Context passed to row/item render callback."
    
    index: int  # Row index in the full collection
    total_items: int  # Total item count
    is_cursor: bool = False  # Whether this row is the keyboard cursor
    is_first_visible: bool = False  # First row in current window
    is_last_visible: bool = False  # Last row in current window
@dataclass
class CellRenderContext:
    "Context passed to cell render callback."
    
    row_index: int  # Row index in the full collection
    column: ColumnDef  # Column definition
    total_items: int  # Total item count
    is_cursor: bool = False  # Whether this row is the keyboard cursor
@dataclass
class VirtualCollectionUrls:
    "URL bundle for HTMX endpoints."
    
    nav_up: str = ''  # Navigate up one row
    nav_down: str = ''  # Navigate down one row
    nav_page_up: str = ''  # Navigate up one page
    nav_page_down: str = ''  # Navigate down one page
    nav_first: str = ''  # Navigate to first row
    nav_last: str = ''  # Navigate to last row
    nav_to_index: str = ''  # Navigate to specific row index
    update_viewport: str = ''  # Update visible_rows (auto-fit)
    focus_row: str = ''  # Move cursor to a specific row (click/tap)
    activate: str = ''  # Activate focused row (Space/Enter)
    sort: str = ''  # Sort by column (header click)

Variables

_prefix_counter: int = 0

routes.router (router.ipynb)

Convenience router factory that wires up standard virtual collection routes (Tier 2 API).

Import

from cjm_fasthtml_virtual_collection.routes.router import (
    init_virtual_collection_router
)

Functions

def init_virtual_collection_router(
    config: VirtualCollectionConfig,                    # Collection config
    state_getter: Callable[[], VirtualCollectionState],  # Function to get current state
    state_setter: Callable[[VirtualCollectionState], None],  # Function to save state
    get_items: Callable[[], list],                       # Function to get current items
    render_cell: Callable,                               # Cell render callback
    on_activate: Optional[Callable] = None,              # Consumer callback for Space/Enter on focused row
    on_refocus: Optional[Callable] = None,               # Consumer callback when clicking already-focused row
    sort_callback: Optional[Callable] = None,            # Consumer callback: (items, column_key, ascending) -> None
    is_skippable: Optional[Callable] = None,             # Predicate: (item) -> bool, cursor skips these items
    on_cursor_change: Optional[Callable] = None,         # Callback: (item, cursor_index, state) -> Tuple of extra OOB elements
    route_prefix: str = "/collection",                   # Route prefix
) -> Tuple[APIRouter, VirtualCollectionUrls]:  # (router, urls) tuple
    "Initialize an APIRouter with all standard virtual collection routes."

js.scroll (scroll.ipynb)

JavaScript generator for scroll wheel to navigation conversion.

Import

from cjm_fasthtml_virtual_collection.js.scroll import (
    SCROLL_THRESHOLD,
    NAVIGATION_COOLDOWN,
    TRACKPAD_COOLDOWN,
    generate_scroll_nav_js
)

Functions

def generate_scroll_nav_js(
    ids: VirtualCollectionHtmlIds,       # HTML IDs for this collection
    button_ids: VirtualCollectionButtonIds,  # Button IDs for nav triggers
    disable_in_modes: Tuple[str, ...] = (),  # Mode names where scroll is suppressed
) -> str:  # JavaScript IIFE
    "Generate JS for scroll wheel to navigation conversion."

Variables

SCROLL_THRESHOLD = 1  # Minimum accumulated delta to trigger navigation (px)
NAVIGATION_COOLDOWN = 100  # Mouse wheel cooldown (ms)
TRACKPAD_COOLDOWN = 250  # Trackpad cooldown (ms) — higher to prevent rapid-fire

components.scrollbar (scrollbar.ipynb)

Custom scrollbar component with proportional thumb for position indication.

Import

from cjm_fasthtml_virtual_collection.components.scrollbar import (
    render_scrollbar_thumb,
    render_scrollbar
)

Functions

def render_scrollbar_thumb(
    state: VirtualCollectionState,       # Collection state
    config: VirtualCollectionConfig,      # Collection config
    ids: VirtualCollectionHtmlIds,        # HTML IDs
    track_height: float = 600.0,          # Track height for min thumb calculation
    oob: bool = False,                    # Whether to include hx-swap-oob
) -> Div:  # Thumb element
    "Render the scrollbar thumb at the correct position."
def render_scrollbar(
    state: VirtualCollectionState,       # Collection state
    config: VirtualCollectionConfig,      # Collection config
    ids: VirtualCollectionHtmlIds,        # HTML IDs
) -> Div:  # Complete scrollbar (track + thumb)
    "Render the custom scrollbar with track and proportional thumb."

js.scrollbar (scrollbar.ipynb)

JavaScript generator for custom scrollbar interaction (drag thumb, click track).

Import

from cjm_fasthtml_virtual_collection.js.scrollbar import (
    generate_scrollbar_js
)

Functions

def generate_scrollbar_js(
    ids: VirtualCollectionHtmlIds,   # HTML IDs for this collection
    urls: VirtualCollectionUrls,     # URL bundle (for nav_to_index)
) -> str:  # JavaScript code fragment
    "Generate JS for custom scrollbar: thumb positioning from hidden input + drag/click interaction."

components.table (table.ipynb)

Table layout rendering: header row, data rows, and cells using CSS table display.

Import

from cjm_fasthtml_virtual_collection.components.table import (
    SORT_ICON_SIZE,
    render_header_cell,
    render_header_row,
    render_data_cell,
    render_data_row,
    render_slot,
    render_table_rows,
    render_cell_oob,
    render_row_oob
)

Functions

def _sort_indicator(column: ColumnDef,  # Column definition
                    state: VirtualCollectionState,  # Collection state (for current sort)
                   ) -> Any:  # Sort icon element or empty string
    "Render sort indicator icon for a sortable header cell."
def render_header_cell(column: ColumnDef,  # Column definition
                       state: VirtualCollectionState,  # Collection state
                       sort_url: str = "",  # Sort URL (empty = no click-to-sort)
                      ) -> Div:  # Header cell element
    "Render a single table header cell with optional sort indicator."
def render_header_row(config: VirtualCollectionConfig,  # Collection config
                      ids: VirtualCollectionHtmlIds,     # HTML IDs
                      state: VirtualCollectionState = None,  # Collection state (for sort indicators)
                      sort_url: str = "",                # Sort URL (empty = no click-to-sort)
                     ) -> Div:  # Header row element
    "Render the table header row with optional sort indicators."
def render_data_cell(item: Any,                    # Data item
                     column: ColumnDef,             # Column definition
                     row_index: int,                # Row index
                     total_items: int,              # Total item count
                     ids: VirtualCollectionHtmlIds,  # HTML IDs
                     render_cell: Callable,          # Consumer cell render callback
                     is_cursor: bool = False,        # Whether row is keyboard cursor
                    ) -> Div:  # Cell element with stable ID
    "Render a single data cell with a stable ID for OOB updates."
def render_data_row(item: Any,                       # Data item
                    row_index: int,                   # Row index in full collection
                    config: VirtualCollectionConfig,   # Collection config
                    state: VirtualCollectionState,     # Collection state
                    ids: VirtualCollectionHtmlIds,     # HTML IDs
                    render_cell: Callable,             # Consumer cell render callback
                    focus_url: str = "",               # URL for click-to-focus (empty = disabled)
                   ) -> Div:  # Row element with stable ID
    "Render a single data row with all cells."
def render_slot(
    slot_index: int,                       # Position in viewport (0-based)
    item: Any,                             # Data item
    item_index: int,                       # Row index in full collection
    config: VirtualCollectionConfig,       # Collection config
    state: VirtualCollectionState,         # Collection state
    ids: VirtualCollectionHtmlIds,         # HTML IDs
    render_cell: Callable,                 # Consumer cell render callback
    oob: bool = False,                     # Whether to render as OOB swap
    focus_url: str = "",                   # URL for click-to-focus (empty = disabled)
) -> Div:  # Slot wrapper (display:contents) containing the data row
    "Render a viewport slot wrapping a data row with display:contents."
def render_table_rows(items: list,                       # Full item list
                      config: VirtualCollectionConfig,    # Collection config
                      state: VirtualCollectionState,      # Collection state
                      ids: VirtualCollectionHtmlIds,      # HTML IDs
                      render_cell: Callable,              # Consumer cell render callback
                      focus_url: str = "",                # URL for click-to-focus (empty = disabled)
                     ) -> Div:  # Table-row-group container with slot wrappers
    "Render all visible rows in the current window as a table-row-group."
def render_cell_oob(item: Any,                       # Data item
                    column: ColumnDef,                # Column to render
                    row_index: int,                   # Row index
                    total_items: int,                 # Total item count
                    ids: VirtualCollectionHtmlIds,    # HTML IDs
                    render_cell: Callable,            # Consumer cell render callback
                    is_cursor: bool = False,          # Whether row is keyboard cursor
                   ) -> Div:  # Cell element with hx-swap-oob
    "Render a single cell with OOB swap for targeted update."
def render_row_oob(item: Any,                       # Data item
                   row_index: int,                   # Row index
                   config: VirtualCollectionConfig,  # Collection config
                   state: VirtualCollectionState,    # Collection state
                   ids: VirtualCollectionHtmlIds,    # HTML IDs
                   render_cell: Callable,            # Consumer cell render callback
                  ) -> Div:  # Row element with hx-swap-oob
    "Render a full row with OOB swap for targeted update."

Variables

SORT_ICON_SIZE = 3  # Tailwind size scale for sort indicator icons

js.touch (touch.ipynb)

JavaScript generator for touch/swipe to navigation conversion.

Import

from cjm_fasthtml_virtual_collection.js.touch import (
    TOUCH_SWIPE_THRESHOLD,
    TOUCH_MOMENTUM_MIN_VELOCITY,
    TOUCH_MOMENTUM_FRICTION,
    TOUCH_VELOCITY_SAMPLES,
    generate_touch_nav_js
)

Functions

def generate_touch_nav_js(
    ids: VirtualCollectionHtmlIds,       # HTML IDs for this collection
    button_ids: VirtualCollectionButtonIds,  # Button IDs for nav triggers (unused, kept for API consistency)
    urls: VirtualCollectionUrls,         # URL bundle for direct HTMX ajax calls
    step_distance: int = TOUCH_SWIPE_THRESHOLD,  # Drag distance in px to trigger one nav step
    disable_in_modes: Tuple[str, ...] = (),  # Mode names where touch is suppressed
) -> str:  # JavaScript IIFE
    "Generate JS for touch gesture to navigation conversion."

Variables

TOUCH_SWIPE_THRESHOLD: int = 30
TOUCH_MOMENTUM_MIN_VELOCITY: float = 0.5
TOUCH_MOMENTUM_FRICTION: float = 0.95
TOUCH_VELOCITY_SAMPLES: int = 5

core.windowing (windowing.ipynb)

Pure math functions for viewport window and scrollbar calculations.

Import

from cjm_fasthtml_virtual_collection.core.windowing import (
    clamp_window_start,
    compute_window,
    compute_scrollbar,
    navigate,
    navigate_cursor,
    find_nearest_focusable
)

Functions

def clamp_window_start(window_start: int,  # Requested first visible row
                       visible_rows: int,   # Number of visible rows
                       total_items: int,     # Total item count
                      ) -> int:              # Clamped window_start
    "Clamp window_start to valid range."
def compute_window(window_start: int,   # First visible row index (already clamped)
                   visible_rows: int,    # Number of visible rows
                   total_items: int,     # Total item count
                  ) -> Tuple[int, int]:  # (render_start, render_end) exclusive end
    "Compute the range of rows to render."
def compute_scrollbar(window_start: int,      # First visible row index
                      visible_rows: int,       # Number of visible rows
                      total_items: int,        # Total item count
                      track_height: float,     # Scrollbar track height in px
                      min_thumb_height: int = 24,  # Minimum thumb height in px
                     ) -> Tuple[float, float]: # (thumb_top_percent, thumb_height_percent)
    "Compute scrollbar thumb position and size as percentages."
def navigate(window_start: int,   # Current first visible row
             direction: str,       # 'up', 'down', 'page_up', 'page_down', 'first', 'last'
             visible_rows: int,    # Number of visible rows
             total_items: int,     # Total item count
            ) -> int:              # New window_start (clamped)
    "Compute new window_start for a navigation action."
def navigate_cursor(
    cursor_index: int,    # Current cursor position (-1 treated as window_start)
    direction: str,       # 'up' or 'down'
    window_start: int,    # Current first visible row
    visible_rows: int,    # Number of visible rows
    total_items: int,     # Total item count
    is_skippable: Optional[Callable[[int], bool]] = None,  # Predicate: index -> skip this item?
) -> Tuple[int, int, bool]:  # (new_cursor, new_window_start, window_changed)
    "Move cursor up/down within the visible window, scrolling at edges."
def find_nearest_focusable(
    index: int,           # Starting index to search from
    total_items: int,     # Total item count
    is_skippable: Optional[Callable[[int], bool]] = None,  # Predicate: index -> skip?
    direction: int = 1,   # Search direction: 1 (forward) or -1 (backward)
) -> int:  # Nearest focusable index, or -1 if none found
    "Find the nearest non-skippable index from a starting position."

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cjm_fasthtml_virtual_collection-0.0.8.tar.gz (56.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cjm_fasthtml_virtual_collection-0.0.8-py3-none-any.whl (52.6 kB view details)

Uploaded Python 3

File details

Details for the file cjm_fasthtml_virtual_collection-0.0.8.tar.gz.

File metadata

File hashes

Hashes for cjm_fasthtml_virtual_collection-0.0.8.tar.gz
Algorithm Hash digest
SHA256 8fdf660faaed7b7c0a6f83d858c5b7780a080eca7b41a7d4f8f2ddadc22302e7
MD5 d49c98b8a1c37545bf26a5a41d477632
BLAKE2b-256 fe5a09be5df70a7edfefb7e6077a7f2627088d7594688865f0b102c97ff0dd4c

See more details on using hashes here.

File details

Details for the file cjm_fasthtml_virtual_collection-0.0.8-py3-none-any.whl.

File metadata

File hashes

Hashes for cjm_fasthtml_virtual_collection-0.0.8-py3-none-any.whl
Algorithm Hash digest
SHA256 8fed1f2f57c13b750a9ce8d10777abd0b135e02f69e9cee5045645c783fe1112
MD5 820cf0fee5984e9b00e56df783b12119
BLAKE2b-256 22fb17b502b3539d1c217561c66d7b530b63eb00e05fea1b2e94313a8431ec0a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page