Metadata-Version: 2.2
Name: cjm-fasthtml-virtual-collection
Version: 0.0.15
Summary: FastHTML virtualized collection rendering with discrete navigation, custom scrollbar, table layout, and cell-level HTMX updates.
Home-page: https://github.com/cj-mills/cjm-fasthtml-virtual-collection
Author: Christian J. Mills
Author-email: 9126128+cj-mills@users.noreply.github.com
License: Apache-2.0
Keywords: nbdev jupyter notebook python
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Natural Language :: English
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: python-fasthtml
Requires-Dist: cjm-fasthtml-app-core
Requires-Dist: cjm-fasthtml-tailwind
Requires-Dist: cjm-fasthtml-daisyui
Requires-Dist: cjm_fasthtml_lucide_icons
Requires-Dist: cjm-fasthtml-keyboard-navigation
Requires-Dist: cjm-fasthtml-viewport-fit
Requires-Dist: cjm_fasthtml_virtual_scrollbar
Provides-Extra: dev
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: keywords
Dynamic: license
Dynamic: provides-extra
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# cjm-fasthtml-virtual-collection


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

## Install

``` bash
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 calculations and navigation.
    ├── 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

``` mermaid
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 --> components_footer
    components_collection --> core_models
    components_collection --> components_scrollbar
    components_collection --> components_table
    components_collection --> core_html_ids
    components_footer --> core_windowing
    components_footer --> core_html_ids
    components_footer --> core_models
    components_scrollbar --> core_html_ids
    components_scrollbar --> core_models
    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_html_ids
    js_scrollbar --> core_models
    js_touch --> core_html_ids
    js_touch --> core_models
    js_touch --> core_button_ids
    keyboard_actions --> core_html_ids
    keyboard_actions --> core_models
    keyboard_actions --> core_button_ids
    routes_handlers --> core_windowing
    routes_handlers --> components_footer
    routes_handlers --> core_models
    routes_handlers --> components_scrollbar
    routes_handlers --> core_html_ids
    routes_handlers --> components_table
    routes_router --> routes_handlers
    routes_router --> core_models
    routes_router --> core_html_ids
```

*33 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

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

#### Functions

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

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

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

``` python
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

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

#### Functions

``` python
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,                 # Initial total item count (fallback)
    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.
    
    total_items is read dynamically from the scrollbar's data-total-items
    attribute so that add/delete operations are reflected without regenerating JS.
    """
```

``` python
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

``` python
from cjm_fasthtml_virtual_collection.core.button_ids import (
    VirtualCollectionButtonIds
)
```

#### Classes

``` python
@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

``` python
from cjm_fasthtml_virtual_collection.components.collection import (
    render_virtual_collection
)
```

#### Functions

``` python
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

``` python
from cjm_fasthtml_virtual_collection.components.footer import (
    render_footer
)
```

#### Functions

``` python
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

``` python
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,
    build_items_changed_response
)
```

#### Functions

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

``` python
def _render_scrollbar_nav_oob(
    state: VirtualCollectionState,     # Current state
    config: VirtualCollectionConfig,   # Collection config
    ids: VirtualCollectionHtmlIds,     # HTML IDs
) -> 'Any':  # Scrollbar element with OOB swap (or None if no scrollbar configured)
    "Render OOB scrollbar. Hidden via CSS class when items fit in viewport."
```

``` python
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 + scrollbar)
    "Build OOB response for navigation: all visible slots + footer + window_start + scrollbar."
```

``` python
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 + scrollbar)
    "Build OOB response for cursor-only move: swap just the affected slots."
```

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

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

``` python
def _scroll_to_cursor(
    state: VirtualCollectionState,  # Current state (mutated in place)
) -> None
    "Scroll window to bring an off-screen cursor into view."
```

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

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

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

``` python
def _render_scrollbar_oob(
    state: VirtualCollectionState,     # Current state
    config: VirtualCollectionConfig,   # Collection config
    ids: VirtualCollectionHtmlIds,     # HTML IDs
) -> Any:  # Scrollbar element with OOB swap (or None if no scrollbar configured)
    "Render OOB scrollbar. Hidden via CSS class when items fit in viewport."
```

``` python
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 [+ scrollbar])
    "Build OOB response that replaces the entire rows container with new slots."
```

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

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

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

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

``` python
def build_items_changed_response(
    items:list,  # Full item list (already mutated by consumer)
    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?
    refit_callback:str="",  # JS auto-fit call expression (from auto_fit_callback_name)
) -> Tuple:  # OOB elements (container + scrollbar + footer + window_start [+ refit trigger])
    """
    Rebuild the VC after the consumer modifies the item list externally.
    
    Updates total_items, clamps window_start and cursor_index, then replaces
    the entire rows container via OOB. Use this after deleting, adding, or
    filtering items — any operation that changes the item count.
    
    Pass `refit_callback` (from `auto_fit_callback_name(config)`) to trigger
    auto-fit re-evaluation after the update — needed when items are added and
    the viewport may have room for more rows than currently rendered.
    """
```

### core.html_ids (`html_ids.ipynb`)

> HTML element ID generators for virtual collection components.

#### Import

``` python
from cjm_fasthtml_virtual_collection.core.html_ids import (
    VirtualCollectionHtmlIds
)
```

#### Classes

``` python
@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"
    
        # -- Refit trigger (OOB target for auto-fit re-evaluation after item mutations) --
        @property
        def refit_trigger(self) -> str: return f"{self.prefix}-refit-trigger"
    
    def refit_trigger(self) -> str: return f"{self.prefix}-refit-trigger"
```

### core.models (`models.ipynb`)

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

#### Import

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

#### Functions

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

#### Classes

``` python
@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
```

``` python
@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)
    
```

``` python
@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
```

``` python
@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
```

``` python
@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
```

``` python
@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

``` python
_prefix_counter: int = 0
```

### routes.router (`router.ipynb`)

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

#### Import

``` python
from cjm_fasthtml_virtual_collection.routes.router import (
    init_virtual_collection_router
)
```

#### Functions

``` python
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

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

#### Functions

``` python
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

``` python
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

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

#### Functions

``` python
def _map_to_scrollbar(
    state: VirtualCollectionState,
    config: VirtualCollectionConfig,
    ids: VirtualCollectionHtmlIds,
):  # (ScrollbarState, ScrollbarConfig, ScrollbarIds)
    "Map VC types to scrollbar lib types."
```

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

``` python
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

``` python
from cjm_fasthtml_virtual_collection.js.scrollbar import (
    generate_scrollbar_js
)
```

#### Functions

``` python
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

``` python
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,
    render_visible_cells_oob
)
```

#### Functions

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

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

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

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

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

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

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

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

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

``` python
def render_visible_cells_oob(
    column: ColumnDef,                # Column to re-render
    item_indices: List[int],          # Item indices that changed
    items: list,                      # Full items list
    state: VirtualCollectionState,    # Current VC state (for window bounds)
    ids: VirtualCollectionHtmlIds,    # HTML IDs
    render_cell: Callable,            # Consumer cell render callback
) -> Tuple[Div, ...]:                # OOB cell elements for visible items only
    "Batch-render OOB cell updates for specified items within the visible window."
```

#### Variables

``` python
SORT_ICON_SIZE = 3  # Tailwind size scale for sort indicator icons
```

### js.touch (`touch.ipynb`)

> JavaScript generator for touch/swipe to navigation conversion.

#### Import

``` python
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

``` python
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

``` python
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 calculations and navigation.

#### Import

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

#### Functions

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

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

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

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

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