Files
MyFastHtml/docs/Panel.md

29 KiB
Raw Permalink Blame History

Panel Component

Introduction

The Panel component provides a flexible three-zone layout with optional collapsible side panels. It's designed to organize content into left panel, main area, and right panel sections, with smooth toggle animations and resizable panels.

Key features:

  • Three customizable zones (left panel, main content, right panel)
  • Configurable panel titles with sticky headers
  • Toggle visibility with hide/show icons
  • Resizable panels with drag handles
  • Smooth CSS animations for show/hide transitions
  • Automatic state persistence per session
  • Configurable panel presence (enable/disable left or right)
  • Session-based width and visibility state

Common use cases:

  • Code editor with file explorer and properties panel
  • Data visualization with filters sidebar and details panel
  • Admin interface with navigation menu and tools panel
  • Documentation viewer with table of contents and metadata
  • Dashboard with configuration panel and information sidebar

Quick Start

Here's a minimal example showing a three-panel layout for a code editor:

from fasthtml.common import *
from myfasthtml.controls.Panel import Panel

# Create the panel instance
panel = Panel(parent=root_instance)

# Set content for each zone
panel.set_left(
  Div(
    H3("Files"),
    Ul(
      Li("app.py"),
      Li("config.py"),
      Li("utils.py")
    )
  )
)

panel.set_main(
  Div(
    H2("Editor"),
    Textarea("# Write your code here", rows=20, cls="w-full font-mono")
  )
)

panel.set_right(
  Div(
    H3("Properties"),
    Div("Language: Python"),
    Div("Lines: 120"),
    Div("Size: 3.2 KB")
  )
)

# Render the panel
return panel

This creates a complete panel layout with:

  • A left panel displaying a file list with a hide icon () at the top right
  • A main content area with a code editor
  • A right panel showing file properties with a hide icon () at the top right
  • Show icons (⋯) that appear in the main area when panels are hidden
  • Drag handles between panels for manual resizing
  • Automatic state persistence (visibility and width)

Note: Users can hide panels by clicking the hide icon () inside each panel. When hidden, a show icon (⋯) appears in the main area (left side for left panel, right side for right panel). Panels can be resized by dragging the handles, and all state is automatically saved in the session.

Basic Usage

Visual Structure

The Panel component consists of three zones with optional side panels:

┌────────────────────────────────────────────────────────────┐
│                                                            │
│  ┌──────────┐ │ ┌──────────────────────┐ │ ┌──────────┐    │
│  │          │ │ │                      │ │ │          │    │
│  │   Left   │ ║ │                      │ ║ │  Right   │    │
│  │  Panel   │ │ │    Main Content      │ │ │  Panel   │    │
│  │          │ │ │                      │ │ │          │    │
│  │     []  │ │ │  [⋯]         [⋯]    │ │ │  []     │    │
│  └──────────┘ │ └──────────────────────┘ │ └──────────┘    │
│               ║                          ║                 │
│            Resizer                    Resizer              │
└────────────────────────────────────────────────────────────┘

Component details:

Element Description
Left panel Optional collapsible panel (default: visible)
Main content Always-visible central content area
Right panel Optional collapsible panel (default: visible)
Hide icon () Inside each panel header, right side
Show icon (⋯) In main area when panel is hidden
Resizer (║) Drag handle to resize panels manually

Panel with title (default):

When show_left_title or show_right_title is True (default), panels display a sticky header with title and hide icon:

┌─────────────────────────────┐
│ Title               []     │  ← Header (sticky, always visible)
├─────────────────────────────┤
│                             │
│    Scrollable Content       │  ← Content area (scrolls independently)
│                             │
└─────────────────────────────┘

Panel without title:

When show_left_title or show_right_title is False, panels use the legacy layout:

┌─────────────────────────────┐
│                        []  │  ← Hide icon at top-right (absolute)
│                             │
│         Content             │
│                             │
└─────────────────────────────┘

Creating a Panel

The Panel is a MultipleInstance, meaning you can create multiple independent panels in your application. Create it by providing a parent instance:

panel = Panel(parent=root_instance)

# Or with a custom ID
panel = Panel(parent=root_instance, _id="my-panel")

# Or with custom configuration
from myfasthtml.controls.Panel import PanelConf

conf = PanelConf(left=True, right=False)  # Only left panel enabled
panel = Panel(parent=root_instance, conf=conf)

Content Zones

The Panel provides three content zones:

┌─────────────────────────────────────────────────────┐
│  Left Panel  │  Main Content  │  Right Panel        │
│  (optional)  │   (required)   │  (optional)         │
└─────────────────────────────────────────────────────┘

Zone details:

Zone Typical Use Required
left Navigation, file explorer, filters, table of contents No
main Primary content, editor, visualization, results Yes
right Properties, tools, metadata, debug info, settings No

Setting Content

Use the set_*() methods to add content to each zone:

# Main content (always visible)
panel.set_main(
  Div(
    H1("Dashboard"),
    P("This is the main content area")
  )
)

# Left panel (optional)
panel.set_left(
  Div(
    H3("Navigation"),
    Ul(
      Li("Home"),
      Li("Settings"),
      Li("About")
    )
  )
)

# Right panel (optional)
panel.set_right(
  Div(
    H3("Tools"),
    Button("Export"),
    Button("Refresh")
  )
)

Method chaining:

The set_main() method returns self, enabling method chaining:

panel = Panel(parent=root_instance)
  .set_main(Div("Main content"))
  .set_left(Div("Left content"))

Panel Configuration

By default, both left and right panels are enabled with titles. You can customize this with PanelConf:

from myfasthtml.controls.Panel import PanelConf

# Only left panel enabled
conf = PanelConf(left=True, right=False)
panel = Panel(parent=root_instance, conf=conf)

# Only right panel enabled
conf = PanelConf(left=False, right=True)
panel = Panel(parent=root_instance, conf=conf)

# Both panels enabled (default)
conf = PanelConf(left=True, right=True)
panel = Panel(parent=root_instance, conf=conf)

# No side panels (main content only)
conf = PanelConf(left=False, right=False)
panel = Panel(parent=root_instance, conf=conf)

Customizing panel titles:

# Custom titles for panels
conf = PanelConf(
    left=True,
    right=True,
    left_title="Explorer",      # Custom title for left panel
    right_title="Properties"    # Custom title for right panel
)
panel = Panel(parent=root_instance, conf=conf)

Disabling panel titles:

When titles are disabled, panels use the legacy layout without a sticky header:

# Disable titles (legacy layout)
conf = PanelConf(
    left=True,
    right=True,
    show_left_title=False,
    show_right_title=False
)
panel = Panel(parent=root_instance, conf=conf)

Disabling show icons:

You can hide the show icons (⋯) that appear when panels are hidden. This means users can only show panels programmatically:

# Disable show icons (programmatic control only)
conf = PanelConf(
    left=True,
    right=True,
    show_display_left=False,   # No show icon for left panel
    show_display_right=False   # No show icon for right panel
)
panel = Panel(parent=root_instance, conf=conf)

Note: When a panel is disabled in configuration, it won't render at all. When a panel is hidden (via toggle), it renders but with zero width and overflow hidden.

Advanced Features

Toggling Panel Visibility

Each visible panel includes a hide icon () in its top-right corner. When hidden, a show icon (⋯) appears in the main area:

User interaction:

  • Hide panel: Click the icon inside the panel
  • Show panel: Click the ⋯ icon in the main area

Icon positions:

  • Hide icons (): Always at top-right of each panel
  • Show icon for left panel (⋯): Top-left of main area
  • Show icon for right panel (⋯): Top-right of main area

Visual states:

Panel Visible:
┌──────────┐
│ Content  │
│     []  │  ← Hide icon visible
└──────────┘

Panel Hidden:
┌──────────────────┐
│ [⋯]       Main   │  ← Show icon visible in main
└──────────────────┘

Animation:

When toggling visibility:

  • Hiding: Panel width animates to 0px over 0.3s
  • Showing: Panel width animates to its saved width over 0.3s
  • Content remains in DOM (state preserved)
  • Smooth CSS transition with ease timing

Note: The animation only works when showing (panel appearing). When hiding, the transition currently doesn't apply due to HTMX swap timing. This is a known limitation.

Resizable Panels

Both left and right panels can be resized by users via drag handles:

  • Drag handle location:
    • Left panel: Right edge (vertical bar)
    • Right panel: Left edge (vertical bar)
  • Width constraints: 150px (minimum) to 500px (maximum)
  • Persistence: Resized width is automatically saved in session state
  • No transition during resize: CSS transitions are disabled during manual dragging for smooth performance

How to resize:

  1. Hover over the panel edge (cursor changes to resize cursor)
  2. Click and drag left/right
  3. Release to set the new width
  4. Width is saved automatically and persists in the session

Initial widths:

  • Left panel: 250px
  • Right panel: 250px

These defaults can be customized via state after creation if needed.

State Persistence

The Panel automatically persists its state within the user's session:

State Property Description Default
left_visible Whether left panel is visible True
right_visible Whether right panel is visible True
left_width Left panel width in pixels 250
right_width Right panel width in pixels 250

State changes (toggle visibility, resize width) are automatically saved and restored within the session.

Accessing state:

# Check current state
is_left_visible = panel._state.left_visible
left_panel_width = panel._state.left_width

# Programmatically update state (not recommended - use commands instead)
panel._state.left_visible = False  # Better to use toggle_side command

Programmatic Control

You can control panels programmatically using commands:

# Toggle panel visibility
toggle_left = panel.commands.set_side_visible("left", visible=False)  # Hide left
toggle_right = panel.commands.set_side_visible("right", visible=True)  # Show right

# Update panel width
update_left_width = panel.commands.update_side_width("left")
update_right_width = panel.commands.update_side_width("right")

These commands are typically used with buttons or other interactive elements:

from myfasthtml.controls.helpers import mk

# Add buttons to toggle panels
hide_left_btn = mk.button("Hide Left", command=panel.commands.set_side_visible("left", False))
show_left_btn = mk.button("Show Left", command=panel.commands.set_side_visible("left", True))

# Add to your layout
panel.set_main(
  Div(
    hide_left_btn,
    show_left_btn,
    H1("Main Content")
  )
)

Command details:

  • toggle_side(side, visible): Sets panel visibility explicitly

    • side: "left" or "right"
    • visible: True (show) or False (hide)
    • Returns: tuple of (panel_element, show_icon_element) for HTMX swap
  • update_side_width(side): Updates panel width from HTMX request

    • side: "left" or "right"
    • Width value comes from JavaScript resize handler
    • Returns: updated panel element for HTMX swap

CSS Customization

The Panel uses CSS classes that you can customize:

Class Element
mf-panel Root panel container
mf-panel-left Left panel container
mf-panel-right Right panel container
mf-panel-main Main content area
mf-panel-with-title Panel using title layout (no padding-top)
mf-panel-body Grid container for header + content
mf-panel-header Sticky header with title and hide icon
mf-panel-content Scrollable content area
mf-panel-hide-icon Hide icon () inside panels
mf-panel-show-icon Show icon (⋯) in main area
mf-panel-show-icon-left Show icon for left panel
mf-panel-show-icon-right Show icon for right panel
mf-resizer Resize handle base class
mf-resizer-left Left panel resize handle
mf-resizer-right Right panel resize handle
mf-hidden Applied to hidden panels
no-transition Disables transition during manual resize

Example customization:

/* Change panel background color */
.mf-panel-left,
.mf-panel-right {
    background-color: #f9fafb;
}

/* Customize hide icon appearance */
.mf-panel-hide-icon:hover {
    background-color: rgba(0, 0, 0, 0.1);
    color: #ef4444;
}

/* Change transition timing */
.mf-panel-left,
.mf-panel-right {
    transition: width 0.5s ease-in-out; /* Slower animation */
}

/* Style resizer handles */
.mf-resizer {
    background-color: #e5e7eb;
}

.mf-resizer:hover {
    background-color: #3b82f6;
}

Examples

Example 1: Code Editor Layout

A typical code editor with file explorer, editor, and properties panel:

from fasthtml.common import *
from myfasthtml.controls.Panel import Panel

# Create panel
panel = Panel(parent=root_instance)

# Left panel: File Explorer
panel.set_left(
  Div(
    H3("Explorer", cls="font-bold mb-2"),
    Div(
      Div("📁 src", cls="font-mono cursor-pointer"),
      Div("  📄 app.py", cls="font-mono ml-4 cursor-pointer"),
      Div("  📄 config.py", cls="font-mono ml-4 cursor-pointer"),
      Div("📁 tests", cls="font-mono cursor-pointer"),
      Div("  📄 test_app.py", cls="font-mono ml-4 cursor-pointer"),
      cls="space-y-1"
    ),
    cls="p-4"
  )
)

# Main: Code Editor
panel.set_main(
  Div(
    Div(
      Span("app.py", cls="font-bold"),
      Span("Python", cls="text-sm opacity-60 ml-2"),
      cls="border-b pb-2 mb-2"
    ),
    Textarea(
      """def main():
print("Hello, World!")

if __name__ == "__main__":
main()""",
      rows=20,
      cls="w-full font-mono text-sm p-2 border rounded"
    ),
    cls="p-4"
  )
)

# Right panel: Properties and Tools
panel.set_right(
  Div(
    H3("Properties", cls="font-bold mb-2"),
    Div("Language: Python", cls="text-sm mb-1"),
    Div("Lines: 5", cls="text-sm mb-1"),
    Div("Size: 87 bytes", cls="text-sm mb-4"),
    
    H3("Tools", cls="font-bold mb-2 mt-4"),
    Button("Run", cls="btn btn-sm btn-primary w-full mb-2"),
    Button("Debug", cls="btn btn-sm w-full mb-2"),
    Button("Format", cls="btn btn-sm w-full"),
    cls="p-4"
  )
)

return panel

Example 2: Dashboard with Filters

A data dashboard with filters sidebar and details panel:

from fasthtml.common import *
from myfasthtml.controls.Panel import Panel

# Create panel
panel = Panel(parent=root_instance)

# Left panel: Filters
panel.set_left(
  Div(
    H3("Filters", cls="font-bold mb-3"),
    
    Div(
      Label("Date Range", cls="label"),
      Select(
        Option("Last 7 days"),
        Option("Last 30 days"),
        Option("Last 90 days"),
        cls="select select-bordered w-full"
      ),
      cls="mb-3"
    ),
    
    Div(
      Label("Category", cls="label"),
      Div(
        Label(Input(type="checkbox", cls="checkbox"), " Sales", cls="label cursor-pointer"),
        Label(Input(type="checkbox", cls="checkbox"), " Marketing", cls="label cursor-pointer"),
        Label(Input(type="checkbox", cls="checkbox"), " Support", cls="label cursor-pointer"),
        cls="space-y-2"
      ),
      cls="mb-3"
    ),
    
    Button("Apply Filters", cls="btn btn-primary w-full"),
    cls="p-4"
  )
)

# Main: Dashboard Charts
panel.set_main(
  Div(
    H1("Analytics Dashboard", cls="text-2xl font-bold mb-4"),
    
    Div(
      Div(
        Div("Total Revenue", cls="stat-title"),
        Div("$45,231", cls="stat-value"),
        Div("+12% from last month", cls="stat-desc"),
        cls="stat"
      ),
      Div(
        Div("Active Users", cls="stat-title"),
        Div("2,345", cls="stat-value"),
        Div("+8% from last month", cls="stat-desc"),
        cls="stat"
      ),
      cls="stats shadow mb-4"
    ),
    
    Div("[Chart placeholder - Revenue over time]", cls="border rounded p-8 text-center"),
    cls="p-4"
  )
)

# Right panel: Details and Insights
panel.set_right(
  Div(
    H3("Key Insights", cls="font-bold mb-3"),
    
    Div(
      Div("🎯 Top Performing", cls="font-bold mb-1"),
      Div("Product A: $12,450", cls="text-sm"),
      Div("Product B: $8,920", cls="text-sm mb-3")
    ),
    
    Div(
      Div("📊 Trending Up", cls="font-bold mb-1"),
      Div("Category: Electronics", cls="text-sm"),
      Div("+23% this week", cls="text-sm mb-3")
    ),
    
    Div(
      Div("⚠️ Needs Attention", cls="font-bold mb-1"),
      Div("Low stock: Item X", cls="text-sm"),
      Div("Response time: +15%", cls="text-sm")
    ),
    cls="p-4"
  )
)

return panel

Example 3: Simple Layout (Main Content Only)

A minimal panel with no side panels, focusing only on main content:

from fasthtml.common import *
from myfasthtml.controls.Panel import Panel, PanelConf

# Create panel with both side panels disabled
conf = PanelConf(left=False, right=False)
panel = Panel(parent=root_instance, conf=conf)

# Only main content
panel.set_main(
  Article(
    H1("Welcome to My Blog", cls="text-3xl font-bold mb-4"),
    P("This is a simple layout focusing entirely on the main content."),
    P("No side panels distract from the reading experience."),
    P("The content takes up the full width of the container."),
    cls="prose max-w-none p-8"
  )
)

return panel

Example 4: Dynamic Panel Updates

Controlling panels programmatically based on user interaction:

from fasthtml.common import *
from myfasthtml.controls.Panel import Panel
from myfasthtml.controls.helpers import mk
from myfasthtml.core.commands import Command

# Create panel
panel = Panel(parent=root_instance)

# Set up content
panel.set_left(
  Div(
    H3("Navigation"),
    Ul(
      Li("Dashboard"),
      Li("Reports"),
      Li("Settings")
    )
  )
)

panel.set_right(
  Div(
    H3("Debug Info"),
    Div("Session ID: abc123"),
    Div("User: Admin"),
    Div("Timestamp: 2024-01-15")
  )
)

# Create control buttons
toggle_left_btn = mk.button(
  "Toggle Left Panel",
  command=panel.commands.set_side_visible("left", False),
  cls="btn btn-sm"
)

toggle_right_btn = mk.button(
  "Toggle Right Panel",
  command=panel.commands.set_side_visible("right", False),
  cls="btn btn-sm"
)

show_all_btn = mk.button(
  "Show All Panels",
  command=Command(
    "show_all",
    "Show all panels",
    lambda: (
            panel.toggle_side("left", True),
            panel.toggle_side("right", True)
    )
  ),
  cls="btn btn-sm btn-primary"
)

# Main content with controls
panel.set_main(
  Div(
    H1("Panel Controls Demo", cls="text-2xl font-bold mb-4"),

    Div(
      toggle_left_btn,
      toggle_right_btn,
      show_all_btn,
      cls="space-x-2 mb-4"
    ),

    P("Use the buttons above to toggle panels programmatically."),
    P("You can also use the hide () and show (⋯) icons."),

    cls="p-4"
  )
)

return panel

Developer Reference

This section contains technical details for developers working on the Panel component itself.

Configuration

The Panel component uses PanelConf dataclass for configuration:

Property Type Description Default
left boolean Enable/disable left panel False
right boolean Enable/disable right panel True
left_title string Title displayed in left panel header "Left"
right_title string Title displayed in right panel header "Right"
show_left_title boolean Show title header on left panel True
show_right_title boolean Show title header on right panel True
show_display_left boolean Show the "show" icon when left is hidden True
show_display_right boolean Show the "show" icon when right is hidden True

State

The Panel component maintains the following state properties via PanelState:

Name Type Description Default
left_visible boolean True if the left panel is visible True
right_visible boolean True if the right panel is visible True
left_width integer Width of the left panel in pixels 250
right_width integer Width of the right panel in pixels 250

Commands

Available commands for programmatic control:

Name Description
toggle_side(side, visible) Sets panel visibility (side: "left"/"right", visible: True/False)
update_side_width(side) Updates panel width from HTMX request (side: "left"/"right")

Note: The old toggle_side(side) command without the visible parameter is deprecated but still available in the codebase.

Public Methods

Method Description Returns
set_main(content) Sets the main content area self
set_left(content) Sets the left panel content Div
set_right(content) Sets the right panel content Div
render() Renders the complete panel Div

High Level Hierarchical Structure

With title (default, show_*_title=True):

Div(id="{id}", cls="mf-panel")
├── Div(id="{id}_pl", cls="mf-panel-left mf-panel-with-title [mf-hidden]")
│   ├── Div(cls="mf-panel-body")
│   │   ├── Div(cls="mf-panel-header")
│   │   │   ├── Div [Title text]
│   │   │   └── Div (hide icon)
│   │   └── Div(id="{id}_cl", cls="mf-panel-content")
│   │       └── [Left content - scrollable]
│   └── Div (resizer-left)
├── Div(cls="mf-panel-main")
│   ├── Div(id="{id}_show_left", cls="hidden|mf-panel-show-icon-left")
│   ├── Div(id="{id}_m", cls="mf-panel-main")
│   │   └── [Main content]
│   └── Div(id="{id}_show_right", cls="hidden|mf-panel-show-icon-right")
├── Div(id="{id}_pr", cls="mf-panel-right mf-panel-with-title [mf-hidden]")
│   ├── Div (resizer-right)
│   └── Div(cls="mf-panel-body")
│       ├── Div(cls="mf-panel-header")
│       │   ├── Div [Title text]
│       │   └── Div (hide icon)
│       └── Div(id="{id}_cr", cls="mf-panel-content")
│           └── [Right content - scrollable]
└── Script  # initResizer('{id}')

Without title (legacy, show_*_title=False):

Div(id="{id}", cls="mf-panel")
├── Div(id="{id}_pl", cls="mf-panel-left [mf-hidden]")
│   ├── Div (hide icon - absolute positioned)
│   ├── Div(id="{id}_cl")
│   │   └── [Left content]
│   └── Div (resizer-left)
├── Div(cls="mf-panel-main")
│   ├── Div(id="{id}_show_left", cls="hidden|mf-panel-show-icon-left")
│   ├── Div(id="{id}_m", cls="mf-panel-main")
│   │   └── [Main content]
│   └── Div(id="{id}_show_right", cls="hidden|mf-panel-show-icon-right")
├── Div(id="{id}_pr", cls="mf-panel-right [mf-hidden]")
│   ├── Div (resizer-right)
│   ├── Div (hide icon - absolute positioned)
│   └── Div(id="{id}_cr")
│       └── [Right content]
└── Script  # initResizer('{id}')

Note:

  • With title: uses grid layout (mf-panel-body) with sticky header and scrollable content
  • Without title: hide icon is absolutely positioned at top-right with padding-top on panel
  • Left panel: body/content then resizer (resizer on right edge)
  • Right panel: resizer then body/content (resizer on left edge)
  • [mf-hidden] class is conditionally applied when panel is hidden
  • mf-panel-with-title class removes default padding-top when using title layout

Element IDs

Name Description
{id} Root panel container
{id}_pl Left panel container
{id}_pr Right panel container
{id}_cl Left panel content wrapper
{id}_cr Right panel content wrapper
{id}_m Main content wrapper
{id}_show_left Show icon for left panel (in main)
{id}_show_right Show icon for right panel (in main)

Note: {id} is the Panel instance ID (auto-generated UUID or custom _id).

ID Management:

The Panel component uses the PanelIds helper class to manage element IDs consistently. Access IDs programmatically:

panel = Panel(parent=root_instance)

# Access IDs via get_ids()
panel.get_ids().panel("left")   # Returns "{id}_pl"
panel.get_ids().panel("right")  # Returns "{id}_pr"
panel.get_ids().left            # Returns "{id}_cl"
panel.get_ids().right           # Returns "{id}_cr"
panel.get_ids().main            # Returns "{id}_m"
panel.get_ids().content("left") # Returns "{id}_cl"

Internal Methods

These methods are used internally for rendering:

Method Description
_mk_panel(side) Renders a panel (left or right) with all elements
_mk_show_icon(side) Renders the show icon for a panel

Method details:

  • _mk_panel(side):

    • Checks if panel is enabled in config
    • Creates resizer with command and data attributes
    • Creates hide icon with toggle command
    • Applies mf-hidden class if panel is not visible
    • Returns None if panel is disabled
  • _mk_show_icon(side):

    • Checks if panel is enabled in config
    • Returns None if panel is disabled or visible
    • Applies hidden (Tailwind) class if panel is visible
    • Applies positioning class based on side

JavaScript Integration

The Panel component uses JavaScript for manual resizing:

initResizer(panelId):

  • Initializes drag-and-drop resize functionality
  • Adds/removes no-transition class during drag
  • Sends width updates to server via HTMX
  • Constrains width between 150px and 500px

File: src/myfasthtml/assets/myfasthtml.js