Metadata-Version: 2.4
Name: cjm-fasthtml-keyboard-navigation
Version: 0.0.23
Summary: A declarative keyboard navigation framework for FastHTML applications with multi-zone focus management, mode switching, and HTMX integration.
Author-email: "Christian J. Mills" <9126128+cj-mills@users.noreply.github.com>
License: Apache-2.0
Project-URL: Repository, https://github.com/cj-mills/cjm-fasthtml-keyboard-navigation
Project-URL: Documentation, https://cj-mills.github.io/cjm-fasthtml-keyboard-navigation
Keywords: nbdev,jupyter,notebook,python
Classifier: Natural Language :: English
Classifier: Intended Audience :: Developers
Classifier: Development Status :: 3 - Alpha
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: python-fasthtml==0.13.4
Requires-Dist: cjm_fasthtml_tailwind
Requires-Dist: cjm_fasthtml_daisyui
Requires-Dist: cjm_fasthtml_lucide_icons
Requires-Dist: cjm_fasthtml_design_system>=0.0.8
Dynamic: license-file

# cjm-fasthtml-keyboard-navigation


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

## Install

``` bash
pip install cjm_fasthtml_keyboard_navigation
```

## Project Structure

    nbs/
    ├── components/ (3)
    │   ├── hints.ipynb        # Components for displaying keyboard shortcut hints to users.
    │   ├── hints_modal.ipynb  # Modal-based keyboard shortcut reference with scannable grouped layout and `?` key trigger.
    │   └── system.ipynb       # High-level API for rendering complete keyboard navigation systems.
    ├── core/ (6)
    │   ├── actions.ipynb      # Declarative keyboard action bindings supporting HTMX triggers and JS callbacks.
    │   ├── focus_zone.ipynb   # Configuration for focusable containers with navigable items.
    │   ├── key_mapping.ipynb  # Configurable key-to-direction mappings for customizable navigation keys.
    │   ├── manager.ipynb      # Coordinates keyboard navigation across multiple zones, modes, and actions.
    │   ├── modes.ipynb        # Configuration for keyboard modes that change navigation and action behavior.
    │   └── navigation.ipynb   # Protocols and implementations for keyboard navigation within focus zones.
    ├── htmx/ (2)
    │   ├── buttons.ipynb  # Generate hidden HTMX action buttons triggered by keyboard events.
    │   └── inputs.ipynb   # Generate hidden inputs for HTMX integration with keyboard navigation.
    └── js/ (3)
        ├── coordinator.ipynb  # Global coordinator for hierarchical keyboard system management with single-listener event dispatch.
        ├── generators.ipynb   # Generate complete keyboard navigation JavaScript from configuration.
        └── utils.ipynb        # Core JavaScript utility generators for keyboard navigation.

Total: 14 notebooks across 4 directories

## Module Dependencies

``` mermaid
graph LR
    components_hints[components.hints<br/>Keyboard Hints]
    components_hints_modal[components.hints_modal<br/>Keyboard Hints Modal]
    components_system[components.system<br/>Keyboard System]
    core_actions[core.actions<br/>Key Actions]
    core_focus_zone[core.focus_zone<br/>Focus Zone]
    core_key_mapping[core.key_mapping<br/>Key Mapping]
    core_manager[core.manager<br/>Zone Manager]
    core_modes[core.modes<br/>Keyboard Modes]
    core_navigation[core.navigation<br/>Navigation Patterns]
    htmx_buttons[htmx.buttons<br/>Action Buttons]
    htmx_inputs[htmx.inputs<br/>Hidden Inputs]
    js_coordinator[js.coordinator<br/>Keyboard Coordinator]
    js_generators[js.generators<br/>Script Generators]
    js_utils[js.utils<br/>JavaScript Utilities]

    components_hints --> core_manager
    components_hints --> core_key_mapping
    components_hints --> core_focus_zone
    components_hints --> core_modes
    components_hints --> core_actions
    components_hints --> core_navigation
    components_hints_modal --> core_manager
    components_hints_modal --> components_hints
    components_hints_modal --> core_focus_zone
    components_hints_modal --> core_key_mapping
    components_hints_modal --> core_actions
    components_hints_modal --> core_navigation
    components_hints_modal --> core_modes
    components_system --> components_hints
    components_system --> htmx_inputs
    components_system --> core_actions
    components_system --> core_manager
    components_system --> htmx_buttons
    components_system --> js_generators
    components_system --> core_focus_zone
    core_actions --> core_key_mapping
    core_focus_zone --> core_navigation
    core_manager --> core_actions
    core_manager --> core_navigation
    core_manager --> core_key_mapping
    core_manager --> core_focus_zone
    core_manager --> core_modes
    core_modes --> core_navigation
    htmx_buttons --> core_actions
    htmx_buttons --> core_manager
    htmx_buttons --> core_focus_zone
    htmx_inputs --> core_manager
    htmx_inputs --> core_focus_zone
    js_generators --> js_utils
    js_generators --> core_actions
    js_generators --> js_coordinator
    js_generators --> core_manager
    js_generators --> core_focus_zone
```

*38 cross-module dependencies detected*

## CLI Reference

No CLI commands found in this project.

## Module Overview

Detailed documentation for each module in the project:

### Key Actions (`actions.ipynb`)

> Declarative keyboard action bindings supporting HTMX triggers and JS
> callbacks.

#### Import

``` python
from cjm_fasthtml_keyboard_navigation.core.actions import (
    KeyAction
)
```

#### Classes

``` python
@dataclass
class KeyAction:
    "A keyboard shortcut binding."
    
    key: str  # JavaScript key name (e.g., "Enter", " ", "ArrowUp")
    modifiers: frozenset[str] = field(...)
    htmx_trigger: Optional[str]  # ID of hidden button to click
    js_callback: Optional[str]  # JS function name to call
    mode_enter: Optional[str]  # mode name to enter
    mode_exit: bool = False  # exit current mode (return to default)
    prevent_default: bool = True  # call e.preventDefault()
    stop_propagation: bool = False  # call e.stopPropagation()
    zone_ids: Optional[tuple[str, ...]]  # only in these zones (None = all)
    mode_names: Optional[tuple[str, ...]]  # only in these modes (None = all)
    not_modes: Optional[tuple[str, ...]]  # not in these modes
    custom_condition: Optional[str]  # raw JS expression for additional conditions
    description: str = ''  # human-readable description for hints
    hint_group: str = 'General'  # grouping for keyboard hints display
    show_in_hints: bool = True  # whether to show in keyboard hints
    
    def matches_context(
            self,
            zone_id: str,  # current active zone
            mode_name: str  # current mode
        ) -> bool:         # True if action is valid in this context
        "Check if action is valid for given zone and mode."
    
    def is_documentation_only(self) -> bool:  # True if no action path is set (advisory hint only)
            """Check if this is a documentation-only KeyAction (no handler fires).
            
            Documentation-only actions appear in keyboard hints but do not trigger
            HTMX, JS callbacks, or mode transitions. Useful for documenting purely
            client-side keyboard interactions (e.g., text-selector caret movement
            implemented as a separate event listener).
            """
            return (
                self.htmx_trigger is None
                and self.js_callback is None
                and self.mode_enter is None
                and not self.mode_exit
            )
    
        def get_display_key(self) -> str: # formatted key combo for display
        "Check if this is a documentation-only KeyAction (no handler fires).

Documentation-only actions appear in keyboard hints but do not trigger
HTMX, JS callbacks, or mode transitions. Useful for documenting purely
client-side keyboard interactions (e.g., text-selector caret movement
implemented as a separate event listener)."
    
    def get_display_key(self) -> str: # formatted key combo for display
            """Get formatted key combination for display."""
            return format_key_combo(self.key, self.modifiers)
    
        def to_js_config(self) -> dict: # JavaScript-compatible configuration
        "Get formatted key combination for display."
    
    def to_js_config(self) -> dict: # JavaScript-compatible configuration
            """Convert to JavaScript configuration object."""
            return {
                "key": self.key,
        "Convert to JavaScript configuration object."
    
    def documentation_only(
            cls,
            key: str,                                       # JavaScript key name (e.g., "ArrowLeft")
            description: str,                               # human-readable description for hints
            *,
            modifiers: frozenset[str] = frozenset(),        # required modifiers
            zone_ids: Optional[tuple[str, ...]] = None,     # restrict to these zones
            mode_names: Optional[tuple[str, ...]] = None,   # restrict to these modes
            not_modes: Optional[tuple[str, ...]] = None,    # not in these modes
            hint_group: str = "General",                    # hint group label
        ) -> "KeyAction":                                   # KeyAction that appears in hints only
        "Create a KeyAction that ONLY appears in keyboard hints — no handler fires.

Use for documenting client-side-only key interactions (e.g., a text-selector
that handles ArrowLeft / ArrowRight via its own DOM event listener — the
keyboard-navigation library shouldn't fire any action, but the user should
still see those keys in the hints modal).

The returned action has all action paths unset (no htmx_trigger, js_callback,
mode_enter, or mode_exit) and explicitly disables prevent_default and
stop_propagation so the client-side handler receives the event unaltered."
```

### Action Buttons (`buttons.ipynb`)

> Generate hidden HTMX action buttons triggered by keyboard events.

#### Import

``` python
from cjm_fasthtml_keyboard_navigation.htmx.buttons import (
    build_htmx_trigger,
    render_action_button,
    render_action_buttons
)
```

#### Functions

``` python
def build_htmx_trigger(
    key: str,                           # JavaScript key name
    modifiers: frozenset[str] = frozenset(),  # modifier keys
    input_selector: str = "input, textarea, select, [contenteditable]"  # input elements to exclude
) -> str:                               # HTMX trigger expression
    "Build HTMX trigger expression for keyboard event."
```

``` python
def render_action_button(
    action: KeyAction,           # the action configuration
    url: str,                    # POST URL for the action
    target: str,                 # HTMX target selector
    include: str = "",           # hx-include selector
    swap: str = "outerHTML",     # hx-swap value
    vals: dict | None = None,    # hx-vals dictionary (JSON values to include in request)
    use_htmx_trigger: bool = False,  # use hx-trigger (False = JS triggerClick only)
    input_selector: str = "input, textarea, select, [contenteditable]"  # inputs to exclude from trigger
) -> Button | None:              # hidden button or None if not HTMX action
    "Render a hidden HTMX button for a keyboard action."
```

``` python
def render_action_buttons(
    manager: ZoneManager,                         # the zone manager configuration
    url_map: dict[str, str],                      # action button ID -> URL
    target_map: dict[str, str],                   # action button ID -> target selector
    include_map: dict[str, str] | None = None,    # action button ID -> include selector
    swap_map: dict[str, str] | None = None,       # action button ID -> swap value
    vals_map: dict[str, dict] | None = None,      # action button ID -> hx-vals dict
    use_htmx_triggers: bool = False,              # use hx-trigger (False = JS triggerClick only)
    container_id: str = "kb-action-buttons"       # container element ID
) -> Div:                                         # container with all action buttons
    "Render all hidden HTMX action buttons for keyboard navigation."
```

### Keyboard Coordinator (`coordinator.ipynb`)

> Global coordinator for hierarchical keyboard system management with
> single-listener event dispatch.

#### Import

``` python
from cjm_fasthtml_keyboard_navigation.js.coordinator import (
    js_coordinator_setup
)
```

#### Functions

``` python
def js_coordinator_setup() -> str: # JavaScript coordinator singleton code
    "Generate the global keyboard coordinator singleton."
```

### Focus Zone (`focus_zone.ipynb`)

> Configuration for focusable containers with navigable items.

#### Import

``` python
from cjm_fasthtml_keyboard_navigation.core.focus_zone import (
    FocusZone
)
```

#### Classes

``` python
@dataclass
class FocusZone:
    "A focusable container with navigable items."
    
    id: str  # HTML element ID of the container
    label: Optional[str]  # human-readable label for keyboard-hints display (falls back to id when None)
    item_selector: Optional[str]  # CSS selector for items (None = scroll only)
    navigation: Union[NavigationPattern, LinearVertical] = field(...)
    navigation_throttle_ms: int = 0  # minimum ms between navigation events (0 = no throttle)
    item_focus_classes: tuple[str, ...] = (str(ring(2)), str(ring_dui.primary))  # CSS classes for focused item
    item_focus_attribute: str = 'data-focused'  # attribute set to "true" on focused item
    zone_focus_classes: tuple[str, ...] = (str(ring(2)), str(ring_dui.primary), str(inset_ring(2)))
    data_attributes: tuple[str, ...] = ()  # data attributes to extract from focused item
    on_focus_change: Optional[str]  # called when focused item changes
    on_navigate: Optional[str]  # called on any navigation (for side effects like audition)
    on_zone_enter: Optional[str]  # called when zone becomes active
    on_zone_leave: Optional[str]  # called when zone loses focus
    activate_child_id: Optional[str]  # system_id of the child to activate (declarative path)
    activate_child_callback: Optional[str]  # JS function name (takes precedence over activate_child_id)
    activate_description: Optional[str]  # per-zone hint text (e.g., "Activate browser"); None = use manager default
    scroll_behavior: str = 'smooth'  # "smooth" or "auto"
    scroll_block: str = 'nearest'  # "start", "center", "end", "nearest"
    hidden_input_prefix: str = ''  # prefix for auto-generated hidden input IDs
    initial_index: int = 0  # initial focused item index
    
    def has_items(self) -> bool: # True if zone has selectable items
            """Check if zone has selectable items."""
            return self.item_selector is not None
    
        def has_activation(self) -> bool: # True if zone has child-activation wiring
        "Check if zone has selectable items."
    
    def has_activation(self) -> bool: # True if zone has child-activation wiring
            """Check if the zone has child-activation wiring (declarative or callback).
            
            Used by the hints renderer to decide whether to emit an "Activate panel"
            row, and by the JS dispatcher to know whether to attempt activation on
            the manager's activate_keys. Equivalent to
            `activate_child_id is not None or activate_child_callback is not None`.
            """
            return self.activate_child_id is not None or self.activate_child_callback is not None
    
        def get_display_label(self) -> str: # human-readable label, falling back to id
        "Check if the zone has child-activation wiring (declarative or callback).

Used by the hints renderer to decide whether to emit an "Activate panel"
row, and by the JS dispatcher to know whether to attempt activation on
the manager's activate_keys. Equivalent to
`activate_child_id is not None or activate_child_callback is not None`."
    
    def get_display_label(self) -> str: # human-readable label, falling back to id
            """Get the label for keyboard-hints display, falling back to id when label is None."""
            return self.label if self.label is not None else self.id
    
        def get_hidden_input_id(
            self,
            attr: str  # the data attribute name
        ) -> str:      # the hidden input element ID
        "Get the label for keyboard-hints display, falling back to id when label is None."
    
    def get_hidden_input_id(
            self,
            attr: str  # the data attribute name
        ) -> str:      # the hidden input element ID
        "Get the hidden input ID for a data attribute."
    
    def to_js_config(self) -> dict: # JavaScript-compatible configuration
            """Convert to JavaScript configuration object."""
            return {
                "id": self.id,
        "Convert to JavaScript configuration object."
```

### Script Generators (`generators.ipynb`)

> Generate complete keyboard navigation JavaScript from configuration.

#### Import

``` python
from cjm_fasthtml_keyboard_navigation.js.generators import (
    js_zone_state,
    js_focus_management,
    js_zone_switching,
    js_navigation,
    js_mode_management,
    js_action_dispatch,
    js_keyboard_handler,
    js_state_notification,
    js_initialization,
    js_global_api,
    generate_keyboard_script
)
```

#### Functions

``` python
def js_zone_state() -> str: # JavaScript state and getter/setter code
    "Generate JavaScript code for zone state management."
```

``` python
def js_focus_management() -> str: # JavaScript focus management code
    "Generate JavaScript code for focus management."
```

``` python
def js_zone_switching() -> str: # JavaScript zone switching code
    "Generate JavaScript code for zone switching."
```

``` python
def js_navigation() -> str: # JavaScript navigation code
    """Generate JavaScript code for item navigation."""
    return '''
// === Navigation ===
// Track last navigation time per zone for throttling
let lastNavigationTime = {};

function getNavigationPattern(zoneId) {
    // Check if mode overrides navigation
    const modeConfig = getModeConfig(currentMode);
    if (modeConfig && modeConfig.navigationOverride) {
        return modeConfig.navigationOverride;
    }
    // Use zone's pattern
    const zone = getZoneConfig(zoneId);
    return zone ? zone.navigationPattern : 'linear_vertical';
    "Generate JavaScript code for item navigation."
```

``` python
def js_mode_management() -> str: # JavaScript mode management code
    "Generate JavaScript code for mode management."
```

``` python
def js_action_dispatch() -> str: # JavaScript action dispatch code
    "Generate JavaScript code for action dispatch."
```

``` python
def js_keyboard_handler() -> str: # JavaScript keyboard handler code
    "Generate JavaScript code for keyboard event handling."
```

``` python
def js_state_notification() -> str: # JavaScript state notification code
    "Generate JavaScript code for state change notification."
```

``` python
def js_initialization() -> str: # JavaScript initialization code
    "Generate JavaScript code for initialization with focus recovery."
```

``` python
def js_global_api() -> str:  # JavaScript global API exposure code
    "Generate JavaScript code to expose control functions globally."
```

``` python
def generate_keyboard_script(
    manager: ZoneManager  # the zone manager configuration
) -> str:                 # complete JavaScript code wrapped in IIFE
    "Generate complete keyboard navigation JavaScript from ZoneManager."
```

### Keyboard Hints (`hints.ipynb`)

> Components for displaying keyboard shortcut hints to users.

#### Import

``` python
from cjm_fasthtml_keyboard_navigation.components.hints import (
    NAV_ICON_MAP,
    KEY_ICON_MAP,
    get_key_icon,
    render_hint_badge,
    create_nav_icon_hint,
    create_modifier_key_hint,
    render_hint_group,
    group_actions_by_hint_group,
    group_actions_by_zone_and_hint_group,
    mode_context_label,
    derive_navigation_hints,
    derive_hierarchy_hints,
    derive_mode_exit_hints,
    render_hints_from_actions,
    render_keyboard_hints
)
```

#### Functions

``` python
def get_key_icon(
    key_name: str,                      # key name to look up (case-insensitive)
    size: int = icons.dense_inline      # icon size (V11 dense_inline role)
) -> FT | None:                         # icon component or None if no icon mapping
    "Get a lucide icon for a key name, if one exists."
```

``` python
def render_hint_badge(
    key_display: Union[str, FT],  # formatted key string or icon component
    description: str,              # action description
    style: str = "ghost",          # badge style (ghost, outline, soft, dash)
    auto_icon: bool = False        # auto-convert known keys to icons
) -> Div:                          # hint badge component
    "Render a single keyboard hint as a badge."
```

``` python
def create_nav_icon_hint(
    icon_name: str,      # lucide icon name (e.g., "arrow-down-up")
    description: str,    # action description
    style: str = "ghost" # badge style
) -> Div:                # hint badge with icon
    "Create a hint badge with a lucide icon."
```

``` python
def create_modifier_key_hint(
    modifier: str,       # modifier key name (e.g., "shift", "ctrl")
    key_icon_or_text: Union[str, FT],  # the main key icon or text
    description: str,    # action description
    style: str = "ghost" # badge style
) -> Div:                # hint badge with modifier + key
    "Create a hint badge with a modifier key and main key."
```

``` python
def render_hint_group(
    group_name: str,         # group header text
    hints: list[tuple[str, str]],  # list of (key_display, description) tuples
    badge_style: str = "ghost"     # badge style for this group
) -> Div:                    # group container with header and hints
    "Render a group of related keyboard hints."
```

``` python
def group_actions_by_hint_group(
    actions: tuple[KeyAction, ...]  # actions to group
) -> dict[str, list[KeyAction]]:    # grouped actions
    "Group actions by their hint_group attribute."
```

``` python
def group_actions_by_zone_and_hint_group(
    manager: ZoneManager,  # the zone manager whose actions to group
) -> list[tuple[Optional[str], str, list[KeyAction]]]:  # ordered (zone_label_or_None, hint_group, actions) tuples
    """
    Group actions for hint display, scoped by zone and hint_group.
    
    Returns an ordered list of (zone_label, hint_group, actions) tuples:
    
    - **Shared section** (zone_label=None) appears FIRST, containing actions
      with zone_ids=None (truly global) or zone_ids covering all zones in the
      manager. Single-zone managers route all actions through this section,
      preserving single-zone consumer rendering (no zone-label prefix).
    - **Per-zone sections** follow, in the order zones are declared on the
      manager. zone_label is FocusZone.get_display_label() (label, falling
      back to id). Actions with zone_ids matching exactly one zone land here.
    - **Within each section**, hint_groups appear in insertion (first-seen)
      order. Actions are emitted in their original tuple order.
    
    Actions with show_in_hints=False or empty description are excluded.
    
    Edge cases:
    - Multi-zone partial coverage (zone_ids touches >1 zone but not all):
      routed to shared. Rare in practice; documents the action as shared.
    - zone_ids referencing a zone not in manager.zones: silently dropped.
    """
```

``` python
def mode_context_label(
    action: KeyAction,  # action whose mode constraints to summarize
) -> Optional[str]:     # short chip-friendly label, or None for unrestricted actions
    """
    Derive a short mode-context label from a KeyAction's mode constraints.
    
    Returns None when the action has no mode restrictions (works in any mode —
    no chip should render). Returns the mode name(s) when mode_names is set.
    Returns 'default' when not_modes is set (action is excluded from specified
    modes, so it fires in the default mode).
    
    Examples:
        mode_names=("split",)             -> "split"
        mode_names=("token-select",)      -> "token-select"
        mode_names=("split", "edit")      -> "split + edit"
        not_modes=("split",)              -> "default"
        no mode constraints               -> None
    """
```

``` python
def derive_navigation_hints(
    manager: ZoneManager,  # the zone manager whose navigation to derive hints from
) -> list[tuple[str, str]]:  # ordered (display_key, description) tuples
    """
    Derive built-in navigation hint rows from manager.key_mapping + zone patterns.
    
    Walks every zone with `has_items()` true, unions the navigable directions
    via each zone's `navigation.get_supported_directions()`, and emits hint
    rows using the actual keys from `manager.key_mapping`. Replaces the
    earlier hardcoded `↑/↓ Navigate items` row, which was wrong under custom
    key_mappings (wasd, vim, etc.).
    
    Returns rows for each direction pair (up/down, left/right) whose keys are
    *actually* bound and *actually* used by some zone:
    
    - **Vertical pair** (up/down): emitted when any zone has a pattern that
      supports up/down navigation. Display uses `key_mapping.up[0]` /
      `key_mapping.down[0]`.
    - **Horizontal pair** (left/right): emitted when any zone supports
      left/right navigation, UNLESS the in-zone horizontal keys collide with
      the manager's zone-switch keys (prev_zone_key/next_zone_key). In that
      case the zone-switch row already documents those keys; emitting a
      second row would be misleading (the zone-switch path wins at runtime).
    
    Returns an empty list when no zone supports key-based navigation (e.g.,
    all zones are ScrollOnly, or no zone has an item_selector).
    """
```

``` python
def derive_hierarchy_hints(
    manager: ZoneManager,        # the zone manager whose hierarchy keys to surface
    *,
    is_child: bool = False,      # True when this manager is rendered as a child in a hierarchical hints modal
) -> list[tuple[str, str]]:      # ordered (display_key, description) rows; fold into manager-derived Navigation group
    """
    Derive built-in hierarchy-key hint rows: Esc deactivation + Enter/Space activation.
    
    These keys are baked into the JS dispatcher (`js_keyboard_handler` in
    `generators.py`), not declared as `KeyAction`s — so a renderer iterating
    only `manager.actions` would miss them. This helper bridges that gap, analogous
    to `derive_navigation_hints` for navigation keys.
    
    Returns rows that fold into the manager-derived "Navigation" group (callers
    extend `nav_rows` with the result before emitting `_render_modal_group`).
    
    Emission rules:
    
    - **Escape — Deactivate panel** is emitted only when `is_child=True`. The JS
      dispatcher's Escape→deactivate-child branch fires when the manager has a
      parent in the coordinator hierarchy at runtime; the renderer can't know
      parent state at render time, but the modal's *multi-manager mode*
      (`child_managers=[...]`) is the canonical signal that a given manager IS a
      child in the hierarchy being documented. Callers pass `is_child=True` for
      every entry in `child_managers`.
    
    - **Enter / Space — Activate panel** is emitted when the manager has at
      least one zone with activation wiring (`activate_child_id` or
      `activate_child_callback`) AND `manager.activate_keys` is non-empty.
      The description defaults to `manager.activate_description` ("Activate
      panel"); consumers override at the manager level for site-specific text.
      Empty `activate_keys` opts out entirely (no row emitted).
    
    The hierarchy demo's on-page legend has long listed both keys; this helper
    is what lets the same information land in the modal too.
    """
```

``` python
def derive_mode_exit_hints(
    manager: ZoneManager,                          # the zone manager whose mode exit keys to surface
) -> list[tuple[str, str, Optional[str]]]:         # ordered (display_key, description, mode_name) rows; mode_name drives the V13 mode chip
    """
    Derive built-in mode-exit hint rows for every non-default mode that defines an exit_key.
    
    Mode exit is handled by the JS dispatcher (`js_keyboard_handler`'s
    `currentModeConfig.exitKey === key` branch at `generators.py`) when a mode
    has a non-empty `exit_key`. Like Escape-deactivate-child, this is not a
    declared `KeyAction` — so the renderer needs a derivation seam.
    
    Returned rows include the mode_name as the third tuple element. The modal
    renderer uses this to attach the V13 mode chip ("split", "edit", etc.) to
    the row, so the user sees the row is only meaningful while in that mode.
    
    Honors `mode.exit_modifiers` via `format_key_combo` so chord exits like
    Ctrl+Escape display correctly. Modes with empty `exit_key` (e.g., the
    implicit `NAVIGATION_MODE` whose exit_key is `""`) are skipped silently.
    """
```

``` python
def render_hints_from_actions(
    actions: tuple[KeyAction, ...],  # actions to display hints for
    badge_style: str = "ghost"       # badge style
) -> Div:                            # container with all hint groups
    "Render keyboard hints from action configurations."
```

``` python
def render_keyboard_hints(
    manager: ZoneManager,                      # the zone manager
    include_navigation: bool = True,           # include navigation hints
    include_zone_switch: bool = True,          # include zone switching hints
    badge_style: str = "ghost",                # badge style
    container_id: str = "kb-hints",            # container element ID
    use_icons: bool = True                     # use lucide icons for nav hints
) -> Div:                                      # complete hints component
    "Render complete keyboard hints for a zone manager."
```

#### Variables

``` python
NAV_ICON_MAP = {2 items}
KEY_ICON_MAP = {9 items}
```

### Keyboard Hints Modal (`hints_modal.ipynb`)

> Modal-based keyboard shortcut reference with scannable grouped layout
> and `?` key trigger.

#### Import

``` python
from cjm_fasthtml_keyboard_navigation.components.hints_modal import (
    render_keyboard_hints_trigger,
    render_keyboard_hints_modal
)
```

#### Functions

``` python
def _render_key_combo(
    display_key: str,  # formatted key combo string (e.g., "Ctrl+Shift+\u2191")
) -> Div:              # container with kbd elements for each key part
    "Render a key combination as a sequence of kbd elements joined by `+`."
```

``` python
def _render_hint_row(
    display_key: str,                       # formatted key combo string
    description: str,                       # action description
    mode_label: Optional[str] = None,        # mode-context chip text (e.g., "split", "default"); None for no chip
) -> Div:                                    # single shortcut row with key, description, and optional mode chip
    "Render a single shortcut row: key combo on left, description (with optional mode chip) on right."
```

``` python
def _render_modal_group(
    group_name: str,                                                   # group header text
    rows: list[tuple[str, str, Optional[str]]],                         # list of (display_key, description, mode_label)
) -> Div:                                                              # group container with header and rows
    """
    Render a group of related shortcuts with a header.
    
    `break_util.inside.avoid_column` keeps the entire group (header + rows)
    in one column inside the modal body's CSS-columns layout — groups never
    split across columns, preserving "form follows function" scannability.
    """
```

``` python
def _render_section_header(
    label: str,  # human-readable section label
) -> Div:        # styled section-header element
    """
    Render a section header for one manager in a multi-manager modal.
    
    Visually distinct from group headers (`_render_modal_group`):
    - Section headers: font-sm + bold + secondary tier + top border + extra spacing
    - Group headers:   font-xs + semibold + muted tier + bottom border (no top)
    
    `break_util.after.avoid` keeps a section header attached to its first
    group inside the CSS-columns layout — prevents the orphan-header case where
    a header lands at the bottom of one column and its first group at the top
    of the next. (`break_util.inside.avoid_column` exists but only applies to
    breaks INSIDE the element; for "don't break right after", `break-after: avoid`
    is the correct CSS property.)
    """
```

``` python
def _render_manager_groups(
    manager: ZoneManager,                # the manager to render groups for
    include_zone_switch: bool = True,    # include zone-switch hint when multi-zone
    is_child: bool = False,              # True when this manager is rendered as a child in a hierarchical modal
) -> list[FT]:                           # flat list of group FT elements (no wrapping Div)
    """
    Render the keyboard-shortcut groups for a single ZoneManager.
    
    Composes:
    1. **Manager-derived "Navigation" group**: derived nav rows from
       `derive_navigation_hints(manager)` (via `manager.key_mapping` + each zone's
       `navigation.get_supported_directions()`), plus the Switch-panel row when
       `include_zone_switch=True` and multi-zone, **plus** library-baked
       hierarchy rows from `derive_hierarchy_hints(manager, is_child=is_child)`
       (Esc deactivate-child when is_child=True; Enter/Space activate-panel
       when the manager has activatable zones), **plus** library-baked
       mode-exit rows from `derive_mode_exit_hints(manager)` (each tagged
       with its mode chip so the user sees the row only applies in that mode).
       **Plus** any consumer actions whose `hint_group == "Navigation"` AND
       land in the shared section (zone_label=None) — they fold into the
       same group rather than rendering a duplicate "Navigation" header.
    2. **Zone-aware action groups**: from `group_actions_by_zone_and_hint_group`,
       with `"<zone label> — <hint_group>"` headers for per-zone sections and
       plain `"<hint_group>"` for shared sections (other than Navigation).
    
    Per-zone "Navigation" groups stay separate (they're zone-scoped — distinct
    from the manager-level Navigation row). Only the shared-section Navigation
    rows merge into the manager-derived group.
    
    Returned as a flat list (no wrapping Div) so callers can interleave section
    headers between groups when rendering multi-manager hierarchies.
    
    The `is_child` flag is set by `_render_modal_body` for every entry in
    `child_managers` — the canonical signal that a manager is a child in the
    hierarchy being documented. See `derive_hierarchy_hints` for the underlying
    emission rules.
    """
```

``` python
def _render_modal_body(
    manager: ZoneManager,                            # primary keyboard zone manager
    include_zone_switch: bool = True,                # include zone-switch hint (single-manager: when multi-zone; multi-manager: per-manager)
    include_navigation: bool = True,                 # DEPRECATED: no-op. Built-in nav row derived from key_mapping now.
    child_managers: Optional[Sequence[ZoneManager]] = None,  # additional managers for hierarchical hint display (each renders as a labeled section)
) -> Div:                                            # modal body with grouped shortcuts
    """
    Render the modal body with grouped keyboard shortcuts.
    
    **Single-manager mode** (`child_managers=None`, the common case):
    - Renders one manager's content as a flat list of groups (no section header).
    - Layout: derived "Navigation" group → zone-aware action groups.
    
    **Multi-manager mode** (`child_managers=[...]`, hierarchical keyboard systems):
    - Renders the primary manager as a labeled section (header = `manager.get_display_label()`),
      followed by each child manager as its own labeled section. Used for
      coordinator-based hierarchies where multiple ZoneManagers cooperate
      (e.g., parent + N children pattern in `cjm-fasthtml-keyboard-navigation`'s
      hierarchy demo). Each manager's content composes via `_render_manager_groups`.
    - Each child manager is rendered with `is_child=True`, which surfaces the
      library-baked Escape→deactivate-parent hint in that child's Navigation
      group (Escape is built into the JS dispatcher's hierarchy handling).
    
    Layout details (same in both modes):
    - Each group container has `break-inside: avoid-column` so the CSS-columns
      layout never splits a group across columns.
    - Section headers (multi-manager mode) carry `break-after: avoid`
      to keep each header attached to its first group.
    - Mode-restricted actions get a small chip ("split", "default", etc.)
      derived from `mode_context_label(action)`.
    - Per-manager: shared-section actions with `hint_group="Navigation"` fold
      into the manager-derived Navigation group (no duplicate group headers).
    - Library-baked hierarchy + mode-exit rows (Esc deactivate, Enter/Space
      activate, per-mode exit) also fold into the same Navigation group via
      `derive_hierarchy_hints` and `derive_mode_exit_hints`.
    
    History: an earlier version hardcoded `↑/↓ Navigate items` regardless of
    the manager's actual `key_mapping`. That row was wrong under custom
    mappings (wasd, vim) and was dropped in G4. This version derives the
    nav row from `manager.key_mapping` via `derive_navigation_hints`, which
    is accurate under any KeyMapping configuration. The same derivation
    discipline extends to hierarchy keys (Esc/Enter/Space) and mode-exit keys.
    """
```

``` python
def render_keyboard_hints_trigger(
    modal_id: str = "kb-hints-modal",             # ID of the modal dialog to open
    icon_size: IconSize = icons.ghost_button,     # lucide icon size (V11.R3 ghost-button: "full" — pairs with V1.modal_disclosure at btn-xs)
) -> Button:                                      # ghost button with keyboard icon
    "Render a keyboard icon button that opens the hints modal."
```

``` python
def _render_question_mark_listener(
    modal_id: str,  # ID of the modal dialog to toggle
) -> Script:        # script element with global `?` key listener
    """
    Render a global `?` key listener that toggles the hints modal.
    
    Uses a named function stored on `window` so that HTMX re-renders
    replace the previous listener instead of accumulating duplicates.
    """
```

``` python
def render_keyboard_hints_modal(
    manager: ZoneManager,                                   # primary keyboard zone manager
    modal_id: str = "kb-hints-modal",                        # HTML ID for the modal dialog
    include_navigation: bool = True,                         # DEPRECATED: no-op kept for backward compat. See _render_modal_body.
    include_zone_switch: bool = True,                        # include zone-switch hint (auto-hidden for single zone)
    enable_question_mark_key: bool = True,                   # add global `?` key listener
    title: str = "Keyboard Shortcuts",                       # modal title text
    child_managers: Optional[Sequence[ZoneManager]] = None,  # child managers for hierarchical hint display (each rendered as a labeled section)
) -> tuple[FT, FT, FT]:                                     # (modal_dialog, trigger_button, question_mark_script)
    """
    Render a modal-based keyboard shortcut reference.
    
    Returns three components:
    - `modal_dialog`: The Dialog element (place anywhere in page)
    - `trigger_button`: Small keyboard icon button (place in step header)
    - `question_mark_script`: Global `?` key listener Script (place in page)
    
    If `enable_question_mark_key` is False, `question_mark_script` is an empty Div.
    
    **Hierarchical hints** (`child_managers=[...]`): when working with multiple
    ZoneManagers coordinated by `window.kbCoordinator` (parent + N children), pass
    the parent as `manager` and the children as `child_managers`. The modal will
    render each as a labeled section using `manager.get_display_label()` for
    section headers. Set `label` on each ZoneManager for human-readable headers;
    falls back to `system_id` otherwise.
    
    Modal width ladder (R2 cap + optimal-space response — see
    layout-system.md M1–M6 modes): grows responsively with viewport. Combined
    with `columns.sm` on the body, this gives 1 column at narrow widths,
    2 columns at laptop full-screen (lg breakpoint with max_w._4xl), and
    3 columns at desktop full-screen (2xl breakpoint with max_w._7xl).
    Modal width is an upper bound; DaisyUI's `modal_box` sizes the actual
    modal to its content within that bound, so short content stays compact.
    """
```

#### Variables

``` python
_BUILTIN_NAV_GROUP = 'Navigation'
```

### Hidden Inputs (`inputs.ipynb`)

> Generate hidden inputs for HTMX integration with keyboard navigation.

#### Import

``` python
from cjm_fasthtml_keyboard_navigation.htmx.inputs import (
    render_zone_hidden_inputs,
    render_hidden_inputs,
    build_include_selector,
    build_all_zones_include_selector
)
```

#### Functions

``` python
def render_zone_hidden_inputs(
    zone: FocusZone  # the focus zone configuration
) -> list:           # list of Hidden input components
    "Render hidden inputs for a single zone's data attributes."
```

``` python
def render_hidden_inputs(
    manager: ZoneManager,            # the zone manager configuration
    include_state: bool = False,     # include state tracking inputs
    container_id: str = "kb-hidden-inputs"  # container element ID
) -> Div:                            # container with all hidden inputs
    """
    Render all hidden inputs for keyboard navigation.
    
    Deduplicates inputs by ID - zones with the same hidden_input_prefix
    will share inputs rather than creating duplicates.
    """
```

``` python
def build_include_selector(
    zone: FocusZone,              # the zone to include inputs from
    include_state: bool = False   # include state inputs
) -> str:                         # CSS selector for hx-include
    "Build hx-include selector for zone's hidden inputs."
```

``` python
def build_all_zones_include_selector(
    manager: ZoneManager,         # the zone manager
    include_state: bool = False   # include state inputs
) -> str:                         # CSS selector for all zones
    """
    Build hx-include selector for all zones' hidden inputs.
    
    Deduplicates selectors - zones with the same hidden_input_prefix
    will only include each input once.
    """
```

### Key Mapping (`key_mapping.ipynb`)

> Configurable key-to-direction mappings for customizable navigation
> keys.

#### Import

``` python
from cjm_fasthtml_keyboard_navigation.core.key_mapping import (
    ARROW_KEYS,
    WASD_KEYS,
    VIM_KEYS,
    NUMPAD_KEYS,
    ARROWS_AND_WASD,
    ARROWS_AND_VIM,
    KEY_DISPLAY_MAP,
    KeyMapping,
    format_key_for_display,
    format_key_combo
)
```

#### Functions

``` python
def format_key_for_display(
    key: str  # the JavaScript key name
) -> str:     # human-readable display string
    "Format a key name for user display."
```

``` python
def format_key_combo(
    key: str,                        # the main key
    modifiers: frozenset[str] = frozenset() # modifier keys (shift, ctrl, alt, meta)
) -> str:                            # formatted string like "Ctrl+Shift+A"
    "Format a key combination for display."
```

#### Classes

``` python
class KeyMapping:
    "Maps physical keys to navigation directions."
    
    def get_direction(
            self,
            key: str  # the pressed key (e.g., "ArrowUp", "w")
        ) -> str | None: # the direction ("up", "down", "left", "right") or None
        "Get direction for a given key press."
    
    def all_keys(self) -> tuple[str, ...]: # all mapped keys
            """Return all mapped keys."""
            return self.up + self.down + self.left + self.right
    
        def to_js_map(self) -> dict[str, str]: # {key: direction} mapping
        "Return all mapped keys."
    
    def to_js_map(self) -> dict[str, str]: # {key: direction} mapping
            """Convert to JavaScript-compatible key-to-direction map."""
            result = {}
            for key in self.up
        "Convert to JavaScript-compatible key-to-direction map."
```

#### Variables

``` python
ARROW_KEYS
WASD_KEYS
VIM_KEYS
NUMPAD_KEYS
ARROWS_AND_WASD
ARROWS_AND_VIM
KEY_DISPLAY_MAP: dict[str, str]
```

### Zone Manager (`manager.ipynb`)

> Coordinates keyboard navigation across multiple zones, modes, and
> actions.

#### Import

``` python
from cjm_fasthtml_keyboard_navigation.core.manager import (
    ZoneManager
)
```

#### Classes

``` python
@dataclass
class ZoneManager:
    "Coordinates keyboard navigation across zones."
    
    zones: tuple[FocusZone, ...]  # all focus zones
    system_id: Optional[str]  # unique ID for coordinator registration (defaults to initial zone ID)
    label: Optional[str]  # human-readable label for keyboard-hints display (falls back to system_id when None)
    prev_zone_key: str = 'ArrowLeft'  # key to switch to previous zone
    next_zone_key: str = 'ArrowRight'  # key to switch to next zone
    zone_switch_modifiers: frozenset[str] = field(...)
    wrap_zones: bool = True  # wrap from last zone to first
    key_mapping: KeyMapping = field(...)
    initial_zone_id: Optional[str]  # defaults to first zone
    modes: tuple[KeyboardMode, ...] = ()  # custom modes (navigation mode is implicit)
    default_mode: str = 'navigation'  # mode to return to after exiting others
    actions: tuple[KeyAction, ...] = ()  # keyboard action bindings
    activate_keys: tuple[str, ...] = ('Enter', ' ')  # keys that fire child activation
    activate_description: str = 'Activate panel'  # default hint text when zone-level activate_description is None
    on_zone_change: Optional[str]  # called when active zone changes
    on_mode_change: Optional[str]  # called when mode changes
    on_state_change: Optional[str]  # called on any state change (for persistence)
    skip_when_input_focused: bool = True  # ignore keys in input/textarea
    input_selector: str = 'input, textarea, select, [contenteditable]'  # elements to skip
    htmx_settle_event: str = 'htmx:afterSettle'  # event to reinitialize on
    expose_state_globally: bool = False  # expose state on window object
    global_state_name: str = 'keyboardNavState'  # name for global state
    state_hidden_inputs: bool = False  # write state to hidden inputs
    
    def get_zone(
            self,
            zone_id: str  # zone ID to find
        ) -> Optional[FocusZone]: # the zone or None
        "Get zone by ID."
    
    def get_initial_zone_id(self) -> str: # the initial zone ID
            """Get initial zone ID."""
            return self.initial_zone_id or self.zones[0].id
    
        def get_display_label(self) -> str: # human-readable label, falling back to system_id
        "Get initial zone ID."
    
    def get_display_label(self) -> str: # human-readable label, falling back to system_id
            """Get the label for keyboard-hints display, falling back to system_id when label is None.
    
            Used by hierarchical hints-modal rendering to label child manager sections
            (`render_keyboard_hints_modal(..., child_managers=[...])`). Set `label` to
            a human-friendly string (e.g., "Alpha List") so the modal's section headers
            read clearly instead of using technical system_id values (e.g., "child-a").
            """
            return self.label if self.label is not None else self.system_id
    
        def get_all_modes(self) -> tuple[KeyboardMode, ...]: # all modes including default
        "Get the label for keyboard-hints display, falling back to system_id when label is None.

Used by hierarchical hints-modal rendering to label child manager sections
(`render_keyboard_hints_modal(..., child_managers=[...])`). Set `label` to
a human-friendly string (e.g., "Alpha List") so the modal's section headers
read clearly instead of using technical system_id values (e.g., "child-a")."
    
    def get_all_modes(self) -> tuple[KeyboardMode, ...]: # all modes including default
            """Get all modes including the default navigation mode."""
            return (NAVIGATION_MODE,) + self.modes
    
        def get_mode(
            self,
            mode_name: str  # mode name to find
        ) -> Optional[KeyboardMode]: # the mode or None
        "Get all modes including the default navigation mode."
    
    def get_mode(
            self,
            mode_name: str  # mode name to find
        ) -> Optional[KeyboardMode]: # the mode or None
        "Get mode by name."
    
    def get_actions_for_context(
            self,
            zone_id: str,  # current zone
            mode_name: str  # current mode
        ) -> list[KeyAction]: # actions valid in this context
        "Get actions valid for given zone and mode."
    
    def get_all_data_attributes(self) -> set[str]: # unique data attributes across all zones
            """Get all unique data attributes from all zones."""
            attrs = set()
            for zone in self.zones
        "Get all unique data attributes from all zones."
    
    def has_activatable_zone(self) -> bool: # True if any zone declares child-activation wiring
            """Check if any zone in the manager has child-activation wiring.
    
            Used by the hints renderer to decide whether to emit an "Activate panel"
            row at the manager level. When False, the manager has no activation seam
            and `activate_keys` is effectively dormant for this manager.
            """
            return any(zone.has_activation() for zone in self.zones)
    
        def to_js_config(self) -> dict: # JavaScript-compatible configuration
        "Check if any zone in the manager has child-activation wiring.

Used by the hints renderer to decide whether to emit an "Activate panel"
row at the manager level. When False, the manager has no activation seam
and `activate_keys` is effectively dormant for this manager."
    
    def to_js_config(self) -> dict: # JavaScript-compatible configuration
            """Convert to JavaScript configuration object."""
            return {
                "systemId": self.system_id,
        "Convert to JavaScript configuration object."
```

### Keyboard Modes (`modes.ipynb`)

> Configuration for keyboard modes that change navigation and action
> behavior.

#### Import

``` python
from cjm_fasthtml_keyboard_navigation.core.modes import (
    NAVIGATION_MODE,
    KeyboardMode
)
```

#### Classes

``` python
@dataclass
class KeyboardMode:
    "A named mode that changes keyboard behavior."
    
    name: str  # unique mode name (e.g., "navigation", "split", "audition")
    enter_key: Optional[str]  # key to enter mode (None = programmatic only)
    enter_modifiers: frozenset[str] = field(...)
    exit_key: str = 'Escape'  # key to exit mode
    exit_modifiers: frozenset[str] = field(...)
    zone_ids: Optional[tuple[str, ...]]  # only available in these zones (None = all)
    navigation_override: Optional[NavigationPattern]  # override zone's navigation pattern
    on_enter: Optional[str]  # called when entering mode
    on_exit: Optional[str]  # called when exiting mode
    indicator_text: Optional[str]  # text shown in UI when mode is active
    exit_on_zone_change: bool = True  # exit mode when switching zones
    
    def is_available_in_zone(
            self,
            zone_id: str  # the zone to check
        ) -> bool:        # True if mode is available in zone
        "Check if mode is available in given zone."
    
    def to_js_config(self) -> dict: # JavaScript-compatible configuration
            """Convert to JavaScript configuration object."""
            return {
                "name": self.name,
        "Convert to JavaScript configuration object."
```

#### Variables

``` python
NAVIGATION_MODE
```

### Navigation Patterns (`navigation.ipynb`)

> Protocols and implementations for keyboard navigation within focus
> zones.

#### Import

``` python
from cjm_fasthtml_keyboard_navigation.core.navigation import (
    Direction,
    NavigationPattern,
    LinearVertical,
    LinearHorizontal,
    ScrollOnly,
    Grid
)
```

#### Classes

``` python
@runtime_checkable
class NavigationPattern(Protocol):
    "Protocol for navigation within a focus zone."
    
    def name(self) -> str: # unique identifier for this pattern
            """Return the pattern name."""
            ...
    
        def get_next_index(
            self,
            current: int,      # current focused index
            direction: Direction, # navigation direction
            total: int,        # total number of items
            columns: int = 1   # number of columns (for grid navigation)
        ) -> int:              # the new index after navigation
        "Return the pattern name."
    
    def get_next_index(
            self,
            current: int,      # current focused index
            direction: Direction, # navigation direction
            total: int,        # total number of items
            columns: int = 1   # number of columns (for grid navigation)
        ) -> int:              # the new index after navigation
        "Calculate next index given current position and direction."
    
    def get_supported_directions(self) -> tuple[Direction, ...]: # directions this pattern responds to
        "Return which arrow key directions this pattern handles."
```

``` python
@dataclass
class LinearVertical:
    "Up/Down navigation through a vertical list."
    
    wrap: bool = False  # wrap from last item to first (and vice versa)
    
    def name(self) -> str:
            """Return the pattern name."""
            return "linear_vertical"
    
        def get_supported_directions(self) -> tuple[Direction, ...]: # ("up", "down")
        "Return the pattern name."
    
    def get_supported_directions(self) -> tuple[Direction, ...]: # ("up", "down")
            """Return supported directions."""
            return ("up", "down")
    
        def get_next_index(
            self,
            current: int,      # current focused index
            direction: Direction, # "up" or "down"
            total: int,        # total number of items
            columns: int = 1   # unused for linear navigation
        ) -> int:              # the new index
        "Return supported directions."
    
    def get_next_index(
            self,
            current: int,      # current focused index
            direction: Direction, # "up" or "down"
            total: int,        # total number of items
            columns: int = 1   # unused for linear navigation
        ) -> int:              # the new index
        "Calculate next index for vertical navigation."
```

``` python
@dataclass
class LinearHorizontal:
    "Left/Right navigation through a horizontal list."
    
    wrap: bool = False  # wrap from last item to first (and vice versa)
    
    def name(self) -> str:
            """Return the pattern name."""
            return "linear_horizontal"
    
        def get_supported_directions(self) -> tuple[Direction, ...]: # ("left", "right")
        "Return the pattern name."
    
    def get_supported_directions(self) -> tuple[Direction, ...]: # ("left", "right")
            """Return supported directions."""
            return ("left", "right")
    
        def get_next_index(
            self,
            current: int,      # current focused index
            direction: Direction, # "left" or "right"
            total: int,        # total number of items
            columns: int = 1   # unused for linear navigation
        ) -> int:              # the new index
        "Return supported directions."
    
    def get_next_index(
            self,
            current: int,      # current focused index
            direction: Direction, # "left" or "right"
            total: int,        # total number of items
            columns: int = 1   # unused for linear navigation
        ) -> int:              # the new index
        "Calculate next index for horizontal navigation."
```

``` python
@dataclass
class ScrollOnly:
    "No item navigation, zone is scrollable content only."
    
    def name(self) -> str:
            """Return the pattern name."""
            return "scroll_only"
    
        def get_supported_directions(self) -> tuple[Direction, ...]: # empty tuple
        "Return the pattern name."
    
    def get_supported_directions(self) -> tuple[Direction, ...]: # empty tuple
            """Return no supported directions."""
            return ()
    
        def get_next_index(
            self,
            current: int,      # current index (unused)
            direction: Direction, # direction (unused)
            total: int,        # total items (unused)
            columns: int = 1   # columns (unused)
        ) -> int:              # always returns current
        "Return no supported directions."
    
    def get_next_index(
            self,
            current: int,      # current index (unused)
            direction: Direction, # direction (unused)
            total: int,        # total items (unused)
            columns: int = 1   # columns (unused)
        ) -> int:              # always returns current
        "Return current index unchanged."
```

``` python
@dataclass
class Grid:
    "2D grid navigation (placeholder for future implementation)."
    
    columns: int = 4  # number of columns in the grid
    wrap_horizontal: bool = True  # wrap at row edges
    wrap_vertical: bool = False  # wrap at grid top/bottom
    
    def name(self) -> str:
            """Return the pattern name."""
            return "grid"
    
        def get_supported_directions(self) -> tuple[Direction, ...]: # all four directions
        "Return the pattern name."
    
    def get_supported_directions(self) -> tuple[Direction, ...]: # all four directions
            """Return all four directions."""
            return ("up", "down", "left", "right")
    
        def get_next_index(
            self,
            current: int,      # current focused index
            direction: Direction, # navigation direction
            total: int,        # total number of items
            columns: int = 0   # override columns (0 = use self.columns)
        ) -> int:              # the new index
        "Return all four directions."
    
    def get_next_index(
            self,
            current: int,      # current focused index
            direction: Direction, # navigation direction
            total: int,        # total number of items
            columns: int = 0   # override columns (0 = use self.columns)
        ) -> int:              # the new index
        "Calculate next index for 2D grid navigation."
```

### Keyboard System (`system.ipynb`)

> High-level API for rendering complete keyboard navigation systems.

#### Import

``` python
from cjm_fasthtml_keyboard_navigation.components.system import (
    KeyboardSystem,
    render_keyboard_system,
    quick_keyboard_system
)
```

#### Functions

``` python
def _build_auto_include_map(
    manager: ZoneManager,        # the zone manager configuration
    include_state: bool = False  # include state inputs in selector
) -> dict[str, str]:             # action button ID -> include selector
    "Auto-generate include_map based on actions and their zone constraints."
```

``` python
def render_keyboard_system(
    manager: ZoneManager,                         # the zone manager configuration
    url_map: dict[str, str],                      # action button ID -> URL
    target_map: dict[str, str],                   # action button ID -> target selector
    include_map: dict[str, str] | None = None,    # action button ID -> include selector (auto-generated if None)
    swap_map: dict[str, str] | None = None,       # action button ID -> swap value
    vals_map: dict[str, dict] | None = None,      # action button ID -> hx-vals dict
    show_hints: bool = True,                      # render keyboard hints UI
    hints_badge_style: str = "ghost",             # badge style for hints
    include_state_inputs: bool = False            # include state tracking inputs
) -> KeyboardSystem:                              # complete keyboard system
    "Render complete keyboard navigation system."
```

``` python
def quick_keyboard_system(
    zones: tuple[FocusZone, ...],                # focus zones
    actions: tuple[KeyAction, ...],              # keyboard actions
    url_map: dict[str, str],                     # action URLs
    target_map: dict[str, str],                  # action targets
    **kwargs                                     # additional ZoneManager/render options
) -> KeyboardSystem:                             # complete keyboard system
    "Quick setup for simple keyboard navigation."
```

#### Classes

``` python
@dataclass
class KeyboardSystem:
    """
    Container for all keyboard navigation components.
    
    Carries `manager` so consumers building child systems can hand the underlying
    `ZoneManager` to `render_keyboard_hints_modal(..., child_managers=[system.manager])`
    without reconstructing it. Purely additive — pre-existing consumers that
    unpack the rendered DOM pieces continue to work unchanged.
    """
    
    script: Script  # the keyboard navigation JavaScript
    hidden_inputs: Div  # hidden inputs for HTMX
    action_buttons: Div  # hidden action buttons for HTMX
    hints: Optional[Div]  # optional keyboard hints UI
    manager: Optional[ZoneManager]  # underlying zone manager (Optional for backward compat; populated by render_keyboard_system)
    
    def all_components(self) -> tuple:  # all components as tuple
            """Return all components for easy unpacking into render.
            
            Excludes `manager` — it's data, not a rendered DOM component.
            """
            components = [self.script, self.hidden_inputs, self.action_buttons]
            if self.hints
        "Return all components for easy unpacking into render.

Excludes `manager` — it's data, not a rendered DOM component."
```

### JavaScript Utilities (`utils.ipynb`)

> Core JavaScript utility generators for keyboard navigation.

#### Import

``` python
from cjm_fasthtml_keyboard_navigation.js.utils import (
    js_config_from_dict,
    js_input_detection,
    js_focus_ring_helpers,
    js_scroll_into_view,
    js_hidden_input_update,
    js_trigger_click,
    js_get_data_attributes,
    js_get_modifiers,
    js_all_utils
)
```

#### Functions

``` python
def js_config_from_dict(
    config: dict[str, Any],  # Python dict to convert
    var_name: str = "cfg"    # JavaScript variable name
) -> str:                    # JavaScript const declaration
    "Generate JavaScript const declaration from Python dict."
```

``` python
def js_input_detection(
    selector: str = "input, textarea, select, [contenteditable='true']"  # CSS selector for input elements
) -> str:  # JavaScript function definition
    "Generate JavaScript function to detect if input element is focused."
```

``` python
def js_focus_ring_helpers(
    default_classes: tuple[str, ...] = (str(ring(2)), str(ring_dui.primary))  # default focus ring CSS classes
) -> str:  # JavaScript function definitions
    "Generate JavaScript functions for adding/removing focus ring classes."
```

``` python
def js_scroll_into_view(
    behavior: str = "smooth",  # "smooth" or "auto"
    block: str = "nearest"     # "start", "center", "end", "nearest"
) -> str:  # JavaScript function definition
    "Generate JavaScript function to scroll element into view."
```

``` python
def js_hidden_input_update() -> str: # JavaScript function definition
    "Generate JavaScript function to update hidden input values."
```

``` python
def js_trigger_click() -> str: # JavaScript function definition
    "Generate JavaScript function to programmatically click a button."
```

``` python
def js_get_data_attributes() -> str: # JavaScript function definition
    "Generate JavaScript function to extract data attributes from element."
```

``` python
def js_get_modifiers() -> str: # JavaScript function definition
    "Generate JavaScript function to extract modifier keys from event."
```

``` python
def js_all_utils(
    input_selector: str = "input, textarea, select, [contenteditable='true']",  # input element selector
    default_focus_classes: tuple[str, ...] = (str(ring(2)), str(ring_dui.primary)),  # focus ring classes
    scroll_behavior: str = "smooth",  # scroll behavior
    scroll_block: str = "nearest"     # scroll block alignment
) -> str:  # all utility functions combined
    "Generate all JavaScript utility functions."
```
