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 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
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_table
components_collection --> core_html_ids
components_collection --> core_models
components_collection --> components_scrollbar
components_collection --> components_footer
components_footer --> core_models
components_footer --> core_html_ids
components_footer --> core_windowing
components_scrollbar --> core_models
components_scrollbar --> core_html_ids
components_table --> core_html_ids
components_table --> core_models
js_auto_fit --> core_models
js_auto_fit --> core_html_ids
js_scroll --> core_button_ids
js_scroll --> core_html_ids
js_scrollbar --> core_models
js_scrollbar --> core_html_ids
js_touch --> core_models
js_touch --> core_button_ids
js_touch --> core_html_ids
keyboard_actions --> core_models
keyboard_actions --> core_button_ids
keyboard_actions --> core_html_ids
routes_handlers --> components_table
routes_handlers --> core_windowing
routes_handlers --> core_html_ids
routes_handlers --> core_models
routes_handlers --> components_scrollbar
routes_handlers --> components_collection
routes_handlers --> components_footer
routes_router --> routes_handlers
routes_router --> core_html_ids
routes_router --> core_models
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, # 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.
"""
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 _build_table_wrapper(
items: list, # Full item list
state: VirtualCollectionState, # Collection state
config: VirtualCollectionConfig, # Collection config
ids: VirtualCollectionHtmlIds, # HTML IDs
render_cell: Callable, # Consumer cell render callback
focus_url: str = "", # URL for click-to-focus
sort_url: str = "", # URL for sortable column headers
render_empty: Optional[Callable] = None, # Empty state callback: () -> FT component
) -> Div: # Wrapper Div containing the table (and optional empty-region sibling)
"""
Build the wrapper Div for the table layout.
Wrapper is ALWAYS a flex column. Its children differ between cases:
- Data case (state.total_items > 0 or render_empty is None): single child,
a table Div with header-group + body-group as CSS-table internals. The
table renders as a flex item with its intrinsic height; w.full on the
table fills the wrapper width via the flex container's cross-axis stretch.
- Empty case (state.total_items == 0 and render_empty is provided): two
children — the header-only table + a sibling empty-region Div below. The
empty-region uses grow() to fill remaining vertical space, which resolves
against the wrapper's flex column layout.
The wrapper class is invariant across cases — both shapes use the same
flex-column container. This is load-bearing for the mutation-via-OOB path:
build_items_changed_response uses hx-swap-oob='innerHTML' to swap children
only (preserving the wrapper element identity, which preserves viewport-fit's
inline height styles). Since innerHTML swap doesn't touch the wrapper's
class attribute, the class must work for both empty and populated states —
so flex-col is always-on. CSS tables render identically as a flex item or
in block context with respect to w.full + intrinsic height, so the data
case is unchanged behaviorally.
"""
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,
build_items_changed_response
)
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 _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 with fresh data attributes."
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."
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."
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 _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 with fresh data attributes."
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."
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/scrollbar.
If the clicked row is skippable, finds the nearest focusable row (searching
forward first, then backward) and focuses that instead. This ensures scrollbar
drag to a skippable region still moves cursor to the closest data row.
If `on_refocus` is provided and the target row is already the cursor,
delegates to `on_refocus` instead of the normal cursor-move logic.
When the target row is off-screen (e.g. scrollbar drag to distant row),
scrolls the viewport and rebuilds all visible slots.
"""
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."
def build_items_changed_response(
"""
Rebuild the VC after the consumer modifies the item list externally.
Updates total_items, clamps window_start and cursor_index, then emits OOB
swaps reflecting the new item count. Use this after any operation that
changes the item count (delete, add, filter).
Two swap strategies, opt-in via the `render_empty` parameter:
- **`render_empty is None` (default — backward-compatible):** the OOB swap
targets the rows container (`ids.rows`) only via `outerHTML`. Header and
wrapper stay as initially rendered. Existing behavior for consumers that
haven't opted into empty-state support.
- **`render_empty is not None`:** the OOB swap targets the wrapper's INNER
content (`ids.wrapper` with `hx-swap-oob='innerHTML'`). The wrapper
element itself is preserved — keeping its inline height styles (set by
viewport-fit at initial render or via window resize) intact across the
swap. Only the wrapper's children are replaced: the header-only table +
empty-region sibling when items go to zero, or the header+body-group
table when items are restored. This handles populated↔empty transitions
cleanly without HTMX's outerHTML attribute-settle behavior stripping
viewport-fit's inline styles. Pair with `sort_url=urls.sort` so the
re-rendered header preserves sort URLs.
*Why innerHTML, not outerHTML*: HTMX's outerHTML OOB swap has a two-phase
attribute lifecycle — at swap time it preserves the old element's
attributes on the new element (for CSS transitions), then at settle
time (~38ms later via setTimeout) it strips attributes that aren't in
the response. Server-rendered wrappers carry no inline style attribute,
so HTMX's settle pass removes viewport-fit's runtime-applied height
styles. The innerHTML mode sidesteps this entirely: the wrapper element
identity is preserved, so its style attribute isn't touched.
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
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"
# -- 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
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)
scrollbar_focus: str = '' # Move cursor via scrollbar drag/click (no refocus)
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 _map_to_scrollbar(
state: VirtualCollectionState,
config: VirtualCollectionConfig,
ids: VirtualCollectionHtmlIds,
): # (ScrollbarState, ScrollbarConfig, ScrollbarIds)
"""
Map VC types to scrollbar lib types.
Uses cursor-based model (like card-stack): scrollbar position tracks
the focused row, not the viewport window offset. This ensures dragging
the thumb to the very top/bottom always reaches the first/last item.
Always visible (auto_hide=False) since the scrollbar serves as a
position indicator, not just an overflow indicator.
"""
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 scrollbar_focus)
) -> str: # JavaScript code fragment
"Generate JS for custom scrollbar: drag/click navigates focus position."
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,
render_visible_cells_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."
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
SORT_ICON_SIZE # Tailwind size scale for sort indicator icons (V11 dense_inline role)
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 calculations and navigation.
Import
from cjm_fasthtml_virtual_collection.core.windowing import (
clamp_window_start,
compute_window,
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 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."
Metadata
Release files for cjm-fasthtml-virtual-collection 0.0.27
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| cjm_fasthtml_virtual_collection-0.0.27.tar.gz | 68.3 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| cjm_fasthtml_virtual_collection-0.0.27-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 119.1 kB
Release files / cjm_fasthtml_virtual_collection-0.0.27.tar.gz
| Download URL | cjm_fasthtml_virtual_collection-0.0.27.tar.gz |
|---|---|
| Size | 68.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0e249d569e4d79878d26d88259fbe47f32b84687f034f29d7f5602e317b15254
|
|
BLAKE2b-256 checksum How to use checksums |
44a7a4f2850b3c4db069159fca533492a05265116b0fa8a278ada9df239db589
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.13
|
Release files / cjm_fasthtml_virtual_collection-0.0.27-py3-none-any.whl
| Download URL | cjm_fasthtml_virtual_collection-0.0.27-py3-none-any.whl |
|---|---|
| Size | 50.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0ab2151adc009bfbb0f423342a9493561de0b204c526c3fa15f00ade6d6bc751
|
|
BLAKE2b-256 checksum How to use checksums |
00ef485cf7a510d3cd886f0f16895887c4ed2728b238fc7a655ca7a443e57d23
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.13
|
Security for cjm-fasthtml-virtual-collection 0.0.27
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.