First set of upgrades for the Panel component

This commit is contained in:
2025-12-20 11:52:44 +01:00
parent 1347f12618
commit 81a80a47b6
6 changed files with 1634 additions and 177 deletions

812
docs/Panel.md Normal file
View File

@@ -0,0 +1,812 @@
# 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)
- 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:
```python
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, top right corner |
| Show icon (⋯) | In main area when panel is hidden |
| Resizer (║) | Drag handle to resize panels manually |
### 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:
```python
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:
```python
# 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:
```python
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. You can customize this with `PanelConf`:
```python
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)
```
**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:**
```python
# 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:
```python
# Toggle panel visibility
toggle_left = panel.commands.toggle_side("left", visible=False) # Hide left
toggle_right = panel.commands.toggle_side("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:
```python
from myfasthtml.controls.helpers import mk
# Add buttons to toggle panels
hide_left_btn = mk.button("Hide Left", command=panel.commands.toggle_side("left", False))
show_left_btn = mk.button("Show Left", command=panel.commands.toggle_side("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-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:**
```css
/* 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:
```python
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:
```python
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:
```python
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:
```python
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.toggle_side("left", False),
cls="btn btn-sm"
)
toggle_right_btn = mk.button(
"Toggle Right Panel",
command=panel.commands.toggle_side("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 | `True` |
| `right` | boolean | Enable/disable right panel | `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
```
Div(id="{id}", cls="mf-panel")
├── Div(id="{id}_panel_left", cls="mf-panel-left [mf-hidden]")
│ ├── Div(id="{id}_content_left")
│ │ ├── Div (hide icon)
│ │ └── [Left content]
│ └── Div (resizer-left)
├── Div(cls="mf-panel-main")
│ ├── Div(id="{id}_show_left", cls="hidden|mf-panel-show-icon-left")
│ ├── [Main content]
│ └── Div(id="{id}_show_right", cls="hidden|mf-panel-show-icon-right")
├── Div(id="{id}_panel_right", cls="mf-panel-right [mf-hidden]")
│ ├── Div (resizer-right)
│ └── Div(id="{id}_content_right")
│ ├── Div (hide icon)
│ └── [Right content]
└── Script # initResizer('{id}')
```
**Note:**
- Left panel: content then resizer (resizer on right edge)
- Right panel: resizer then content (resizer on left edge)
- `[mf-hidden]` class is conditionally applied when panel is hidden
### Element IDs
| Name | Description |
|----------------------|-------------------------------------|
| `{id}` | Root panel container |
| `{id}_panel_left` | Left panel container |
| `{id}_panel_right` | Right panel container |
| `{id}_content_left` | Left panel content wrapper |
| `{id}_content_right` | Right panel 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`).
### 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`

View File

@@ -2,7 +2,9 @@
## Introduction
The TabsManager component provides a dynamic tabbed interface for organizing multiple views within your FastHTML application. It handles tab creation, activation, closing, and content management with automatic state persistence and HTMX-powered interactions.
The TabsManager component provides a dynamic tabbed interface for organizing multiple views within your FastHTML
application. It handles tab creation, activation, closing, and content management with automatic state persistence and
HTMX-powered interactions.
**Key features:**
@@ -52,7 +54,9 @@ This creates a complete tabbed interface with:
- A search menu (⊞ icon) for quick tab navigation when many tabs are open
- Automatic HTMX updates when switching or closing tabs
**Note:** Tabs are interactive by default. Users can click tab labels to switch views, click close buttons to remove tabs, or use the search menu to find tabs quickly. All interactions update the UI without page reload thanks to HTMX integration.
**Note:** Tabs are interactive by default. Users can click tab labels to switch views, click close buttons to remove
tabs, or use the search menu to find tabs quickly. All interactions update the UI without page reload thanks to HTMX
integration.
## Basic Usage
@@ -63,9 +67,9 @@ The TabsManager component consists of a header with tab buttons and a content ar
```
┌────────────────────────────────────────────────────────────┐
│ Tab Header │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────┐ │
│ │ Tab 1 × │ │ Tab 2 × │ │ Tab 3 × │ │ ⊞ │ │
│ └──────────┘ └──────────┘ └──────────┘ └────┘ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────┐
│ │ Tab 1 × │ │ Tab 2 × │ │ Tab 3 × │ │ ⊞ │
│ └──────────┘ └──────────┘ └──────────┘ └────┘
├────────────────────────────────────────────────────────────┤
│ │
│ │
@@ -77,16 +81,17 @@ The TabsManager component consists of a header with tab buttons and a content ar
**Component details:**
| Element | Description |
|-------------------|--------------------------------------------------|
| Tab buttons | Clickable labels to switch between tabs |
| Close button (×) | Removes the tab and its content |
| Search menu (⊞) | Dropdown menu to search and filter tabs |
| Content area | Displays the active tab's content |
| Element | Description |
|------------------|-----------------------------------------|
| Tab buttons | Clickable labels to switch between tabs |
| Close button (×) | Removes the tab and its content |
| Search menu (⊞) | Dropdown menu to search and filter tabs |
| Content area | Displays the active tab's content |
### Creating a TabsManager
The TabsManager is a `MultipleInstance`, meaning you can create multiple independent tab managers in your application. Create it by providing a parent instance:
The TabsManager is a `MultipleInstance`, meaning you can create multiple independent tab managers in your application.
Create it by providing a parent instance:
```python
tabs = TabsManager(parent=root_instance)
@@ -102,12 +107,13 @@ Use the `create_tab()` method to add a new tab:
```python
# Create a tab with custom content
tab_id = tabs.create_tab(
label="My Tab",
component=Div(H1("Content"), P("Tab content here"))
label="My Tab",
component=Div(H1("Content"), P("Tab content here"))
)
# Create with a MyFastHtml control
from myfasthtml.controls.VisNetwork import VisNetwork
network = VisNetwork(parent=tabs, nodes=nodes_data, edges=edges_data)
tab_id = tabs.create_tab("Network View", network)
@@ -116,6 +122,7 @@ tab_id = tabs.create_tab("Background Tab", content, activate=False)
```
**Parameters:**
- `label` (str): Display text shown in the tab button
- `component` (Any): Content to display in the tab (FastHTML elements or MyFastHtml controls)
- `activate` (bool): Whether to make this tab active immediately (default: True)
@@ -135,10 +142,12 @@ tabs.show_tab(tab_id, activate=False)
```
**Parameters:**
- `tab_id` (str): The UUID of the tab to show
- `activate` (bool): Whether to make this tab active (default: True)
**Note:** The first time a tab is shown, its content is sent to the client and cached. Subsequent activations just toggle visibility without re-sending content.
**Note:** The first time a tab is shown, its content is sent to the client and cached. Subsequent activations just
toggle visibility without re-sending content.
### Closing Tabs
@@ -150,6 +159,7 @@ tabs.close_tab(tab_id)
```
**What happens when closing:**
1. Tab is removed from the tab list and order
2. Content is removed from cache and client
3. If the closed tab was active, the first remaining tab becomes active
@@ -163,14 +173,15 @@ Use the `change_tab_content()` method to update an existing tab's content and la
# Update tab content and label
new_content = Div(H1("Updated"), P("New content"))
tabs.change_tab_content(
tab_id=tab_id,
label="Updated Tab",
component=new_content,
activate=True
tab_id=tab_id,
label="Updated Tab",
component=new_content,
activate=True
)
```
**Parameters:**
- `tab_id` (str): The UUID of the tab to update
- `label` (str): New label for the tab
- `component` (Any): New content to display
@@ -187,13 +198,14 @@ When creating multiple tabs programmatically, you can use auto-increment to gene
```python
# Using the on_new_tab method with auto_increment
def create_multiple_tabs():
# Creates "Untitled_0", "Untitled_1", "Untitled_2"
tabs.on_new_tab("Untitled", content, auto_increment=True)
tabs.on_new_tab("Untitled", content, auto_increment=True)
tabs.on_new_tab("Untitled", content, auto_increment=True)
# Creates "Untitled_0", "Untitled_1", "Untitled_2"
tabs.on_new_tab("Untitled", content, auto_increment=True)
tabs.on_new_tab("Untitled", content, auto_increment=True)
tabs.on_new_tab("Untitled", content, auto_increment=True)
```
**How it works:**
- The TabsManager maintains an internal counter (`_tab_count`)
- When `auto_increment=True`, the counter value is appended to the label
- Counter increments with each auto-incremented tab creation
@@ -218,11 +230,13 @@ tab_id_2 = tabs.create_tab("Network", network)
**Detection criteria:**
A tab is considered a duplicate if all three match:
- Same `label`
- Same `component_type` (component class prefix)
- Same `component_id` (component instance ID)
**Note:** This only works with `BaseInstance` components (MyFastHtml controls). Plain FastHTML elements don't have IDs and will always create new tabs.
**Note:** This only works with `BaseInstance` components (MyFastHtml controls). Plain FastHTML elements don't have IDs
and will always create new tabs.
### Dynamic Content Updates
@@ -232,14 +246,16 @@ You can update tabs dynamically during the session:
# Initial tab creation
tab_id = tabs.create_tab("Data View", Div("Loading..."))
# Later, update with actual data
def load_data():
data_content = Div(H2("Data"), P("Loaded content"))
tabs.change_tab_content(tab_id, "Data View", data_content)
# Returns HTMX response to update the UI
data_content = Div(H2("Data"), P("Loaded content"))
tabs.change_tab_content(tab_id, "Data View", data_content)
# Returns HTMX response to update the UI
```
**Use cases:**
- Loading data asynchronously
- Refreshing tab content based on user actions
- Updating visualizations with new data
@@ -257,6 +273,7 @@ The built-in search menu helps users navigate when many tabs are open:
```
**How to access:**
- Click the ⊞ icon in the tab header
- Start typing to filter tabs by label
- Click a result to activate that tab
@@ -276,11 +293,13 @@ tabs.show_tab(tab_id, oob=False) # Swap into HTMX target only
```
**When to use `oob=False`:**
- When you want to control the exact HTMX target
- When combining with other HTMX responses
- When the tab activation is triggered by a command with a specific target
**When to use `oob=True` (default):**
- Most common use case
- Allows other controls to trigger tab changes without caring about targets
- Enables automatic UI updates across multiple elements
@@ -289,18 +308,18 @@ tabs.show_tab(tab_id, oob=False) # Swap into HTMX target only
The TabsManager uses CSS classes that you can customize:
| Class | Element |
|----------------------------|----------------------------------|
| `mf-tabs-manager` | Root tabs manager container |
| `mf-tabs-header-wrapper` | Header wrapper (buttons + menu) |
| `mf-tabs-header` | Tab buttons container |
| `mf-tab-button` | Individual tab button |
| `mf-tab-active` | Active tab button (modifier) |
| `mf-tab-label` | Tab label text |
| `mf-tab-close-btn` | Close button (×) |
| `mf-tab-content-wrapper` | Content area container |
| `mf-tab-content` | Individual tab content |
| `mf-empty-content` | Empty state when no tabs |
| Class | Element |
|--------------------------|---------------------------------|
| `mf-tabs-manager` | Root tabs manager container |
| `mf-tabs-header-wrapper` | Header wrapper (buttons + menu) |
| `mf-tabs-header` | Tab buttons container |
| `mf-tab-button` | Individual tab button |
| `mf-tab-active` | Active tab button (modifier) |
| `mf-tab-label` | Tab label text |
| `mf-tab-close-btn` | Close button (×) |
| `mf-tab-content-wrapper` | Content area container |
| `mf-tab-content` | Individual tab content |
| `mf-empty-content` | Empty state when no tabs |
**Example customization:**
@@ -340,30 +359,30 @@ tabs = TabsManager(parent=root, _id="app-tabs")
# Dashboard view
dashboard = Div(
H1("Dashboard"),
Div(
Div("Total Users: 1,234", cls="stat"),
Div("Active Sessions: 56", cls="stat"),
Div("Revenue: $12,345", cls="stat"),
cls="stats-grid"
)
H1("Dashboard"),
Div(
Div("Total Users: 1,234", cls="stat"),
Div("Active Sessions: 56", cls="stat"),
Div("Revenue: $12,345", cls="stat"),
cls="stats-grid"
)
)
# Analytics view
analytics = Div(
H1("Analytics"),
P("Detailed analytics and reports"),
Div("Chart placeholder", cls="chart-container")
H1("Analytics"),
P("Detailed analytics and reports"),
Div("Chart placeholder", cls="chart-container")
)
# Settings view
settings = Div(
H1("Settings"),
Form(
Label("Username:", Input(name="username", value="admin")),
Label("Email:", Input(name="email", value="admin@example.com")),
Button("Save", type="submit"),
)
H1("Settings"),
Form(
Label("Username:", Input(name="username", value="admin")),
Label("Email:", Input(name="email", value="admin@example.com")),
Button("Save", type="submit"),
)
)
# Create tabs
@@ -392,28 +411,30 @@ tabs = TabsManager(parent=root, _id="network-tabs")
# Create initial tab with welcome message
tabs.create_tab("Welcome", Div(
H1("Network Visualizer"),
P("Click 'Add Network' to create a new network visualization")
H1("Network Visualizer"),
P("Click 'Add Network' to create a new network visualization")
))
# Function to create a new network tab
def add_network_tab():
# Define network data
nodes = [
{"id": 1, "label": "Node 1"},
{"id": 2, "label": "Node 2"},
{"id": 3, "label": "Node 3"}
]
edges = [
{"from": 1, "to": 2},
{"from": 2, "to": 3}
]
# Define network data
nodes = [
{"id": 1, "label": "Node 1"},
{"id": 2, "label": "Node 2"},
{"id": 3, "label": "Node 3"}
]
edges = [
{"from": 1, "to": 2},
{"from": 2, "to": 3}
]
# Create network instance
network = VisNetwork(parent=tabs, nodes=nodes, edges=edges)
# Use auto-increment to create unique labels
return tabs.on_new_tab("Network", network, auto_increment=True)
# Create network instance
network = VisNetwork(parent=tabs, nodes=nodes, edges=edges)
# Use auto-increment to create unique labels
return tabs.on_new_tab("Network", network, auto_increment=True)
# Create command for adding networks
add_cmd = Command("add_network", "Add network tab", add_network_tab)
@@ -443,12 +464,14 @@ tabs = TabsManager(parent=root, _id="editor-tabs")
doc1_id = tabs.create_tab("Document 1", Textarea("Initial content 1", rows=10))
doc2_id = tabs.create_tab("Document 2", Textarea("Initial content 2", rows=10))
# Function to refresh a document's content
def refresh_document(tab_id, doc_name):
# Simulate loading new content
new_content = Textarea(f"Refreshed content for {doc_name}\nTimestamp: {datetime.now()}", rows=10)
tabs.change_tab_content(tab_id, doc_name, new_content)
return tabs._mk_tabs_controller(oob=True), tabs._mk_tabs_header_wrapper(oob=True)
# Simulate loading new content
new_content = Textarea(f"Refreshed content for {doc_name}\nTimestamp: {datetime.now()}", rows=10)
tabs.change_tab_content(tab_id, doc_name, new_content)
return tabs._mk_tabs_controller(oob=True), tabs._mk_tabs_header_wrapper(oob=True)
# Create refresh commands
refresh_doc1 = Command("refresh_1", "Refresh doc 1", refresh_document, doc1_id, "Document 1")
@@ -456,9 +479,9 @@ refresh_doc2 = Command("refresh_2", "Refresh doc 2", refresh_document, doc2_id,
# Add refresh buttons
controls = Div(
mk.button("Refresh Document 1", command=refresh_doc1, cls="btn btn-sm"),
mk.button("Refresh Document 2", command=refresh_doc2, cls="btn btn-sm"),
cls="controls-bar"
mk.button("Refresh Document 1", command=refresh_doc1, cls="btn btn-sm"),
mk.button("Refresh Document 2", command=refresh_doc2, cls="btn btn-sm"),
cls="controls-bar"
)
return Div(controls, tabs)
@@ -480,19 +503,21 @@ tabs = TabsManager(parent=root, _id="dynamic-tabs")
# Create initial placeholder tab
tabs.create_tab("Start", Div(
H2("Welcome"),
P("Click 'New Tab' to create numbered tabs")
H2("Welcome"),
P("Click 'New Tab' to create numbered tabs")
))
# Function to create a new numbered tab
def create_numbered_tab():
content = Div(
H2("New Tab Content"),
P(f"This tab was created dynamically"),
Input(placeholder="Enter some text...", cls="input")
)
# Auto-increment creates "Tab_0", "Tab_1", "Tab_2", etc.
return tabs.on_new_tab("Tab", content, auto_increment=True)
content = Div(
H2("New Tab Content"),
P(f"This tab was created dynamically"),
Input(placeholder="Enter some text...", cls="input")
)
# Auto-increment creates "Tab_0", "Tab_1", "Tab_2", etc.
return tabs.on_new_tab("Tab", content, auto_increment=True)
# Create command
new_tab_cmd = Command("new_tab", "Create new tab", create_numbered_tab)
@@ -501,8 +526,8 @@ new_tab_cmd = Command("new_tab", "Create new tab", create_numbered_tab)
new_tab_button = mk.button("New Tab", command=new_tab_cmd, cls="btn btn-primary")
return Div(
Div(new_tab_button, cls="toolbar"),
tabs
Div(new_tab_button, cls="toolbar"),
tabs
)
```
@@ -516,13 +541,13 @@ This section contains technical details for developers working on the TabsManage
The TabsManager component maintains the following state properties:
| Name | Type | Description | Default |
|----------------------------|-----------------|------------------------------------------------------|---------|
| `tabs` | dict[str, Any] | Dictionary of tab metadata (id, label, component) | `{}` |
| `tabs_order` | list[str] | Ordered list of tab IDs | `[]` |
| `active_tab` | str \| None | ID of the currently active tab | `None` |
| `ns_tabs_content` | dict[str, Any] | Cache of tab content (raw, not wrapped) | `{}` |
| `ns_tabs_sent_to_client` | set | Set of tab IDs already sent to client | `set()` |
| Name | Type | Description | Default |
|--------------------------|----------------|---------------------------------------------------|---------|
| `tabs` | dict[str, Any] | Dictionary of tab metadata (id, label, component) | `{}` |
| `tabs_order` | list[str] | Ordered list of tab IDs | `[]` |
| `active_tab` | str \| None | ID of the currently active tab | `None` |
| `ns_tabs_content` | dict[str, Any] | Cache of tab content (raw, not wrapped) | `{}` |
| `ns_tabs_sent_to_client` | set | Set of tab IDs already sent to client | `set()` |
**Note:** Properties prefixed with `ns_` are not persisted in the database and exist only for the session.
@@ -530,24 +555,24 @@ The TabsManager component maintains the following state properties:
Available commands for programmatic control:
| Name | Description |
|-----------------------------------------|------------------------------------------------|
| `show_tab(tab_id)` | Activate or show a specific tab |
| `close_tab(tab_id)` | Close a specific tab |
| Name | Description |
|---------------------------------------------|--------------------------------------------|
| `show_tab(tab_id)` | Activate or show a specific tab |
| `close_tab(tab_id)` | Close a specific tab |
| `add_tab(label, component, auto_increment)` | Add a new tab with optional auto-increment |
### Public Methods
| Method | Description |
|---------------------------------------------------------|------------------------------------------------------|
| `create_tab(label, component, activate=True)` | Create a new tab or reuse existing duplicate |
| `show_tab(tab_id, activate=True, oob=True)` | Send tab to client and/or activate it |
| `close_tab(tab_id)` | Close and remove a tab |
| `change_tab_content(tab_id, label, component, activate=True)` | Update existing tab's label and content |
| `on_new_tab(label, component, auto_increment=False)` | Create and show tab with auto-increment support |
| `add_tab_btn()` | Returns add tab button element |
| `get_state()` | Returns the TabsManagerState object |
| `render()` | Renders the complete TabsManager component |
| Method | Description |
|---------------------------------------------------------------|-------------------------------------------------|
| `create_tab(label, component, activate=True)` | Create a new tab or reuse existing duplicate |
| `show_tab(tab_id, activate=True, oob=True)` | Send tab to client and/or activate it |
| `close_tab(tab_id)` | Close and remove a tab |
| `change_tab_content(tab_id, label, component, activate=True)` | Update existing tab's label and content |
| `on_new_tab(label, component, auto_increment=False)` | Create and show tab with auto-increment support |
| `add_tab_btn()` | Returns add tab button element |
| `get_state()` | Returns the TabsManagerState object |
| `render()` | Renders the complete TabsManager component |
### High Level Hierarchical Structure
@@ -573,15 +598,15 @@ Div(id="{id}", cls="mf-tabs-manager")
### Element IDs
| Name | Description |
|-------------------------------|------------------------------------------|
| `{id}` | Root tabs manager container |
| `{id}-controller` | Hidden controller managing active state |
| `{id}-header-wrapper` | Header wrapper (buttons + search) |
| `{id}-header` | Tab buttons container |
| `{id}-content-wrapper` | Content area wrapper |
| `{id}-{tab_id}-content` | Individual tab content |
| `{id}-search` | Search component ID |
| Name | Description |
|-------------------------|-----------------------------------------|
| `{id}` | Root tabs manager container |
| `{id}-controller` | Hidden controller managing active state |
| `{id}-header-wrapper` | Header wrapper (buttons + search) |
| `{id}-header` | Tab buttons container |
| `{id}-content-wrapper` | Content area wrapper |
| `{id}-{tab_id}-content` | Individual tab content |
| `{id}-search` | Search component ID |
**Note:** `{id}` is the TabsManager instance ID, `{tab_id}` is the UUID of each tab.
@@ -589,22 +614,22 @@ Div(id="{id}", cls="mf-tabs-manager")
These methods are used internally for rendering:
| Method | Description |
|-----------------------------------------|-------------------------------------------------------|
| `_mk_tabs_controller(oob=False)` | Renders the hidden controller element |
| `_mk_tabs_header_wrapper(oob=False)` | Renders the header wrapper with buttons and search |
| `_mk_tab_button(tab_data)` | Renders a single tab button |
| `_mk_tab_content_wrapper()` | Renders the content wrapper with active tab content |
| `_mk_tab_content(tab_id, content)` | Renders individual tab content div |
| `_mk_show_tabs_menu()` | Renders the search dropdown menu |
| `_wrap_tab_content(tab_content)` | Wraps tab content for HTMX out-of-band insertion |
| `_get_or_create_tab_content(tab_id)` | Gets tab content from cache or creates it |
| `_dynamic_get_content(tab_id)` | Retrieves component from InstancesManager |
| `_tab_already_exists(label, component)` | Checks if duplicate tab exists |
| `_add_or_update_tab(...)` | Internal method to add/update tab in state |
| `_get_ordered_tabs()` | Returns tabs ordered by tabs_order list |
| `_get_tab_list()` | Returns list of tab dictionaries in order |
| `_get_tab_count()` | Returns and increments internal tab counter |
| Method | Description |
|-----------------------------------------|-----------------------------------------------------|
| `_mk_tabs_controller(oob=False)` | Renders the hidden controller element |
| `_mk_tabs_header_wrapper(oob=False)` | Renders the header wrapper with buttons and search |
| `_mk_tab_button(tab_data)` | Renders a single tab button |
| `_mk_tab_content_wrapper()` | Renders the content wrapper with active tab content |
| `_mk_tab_content(tab_id, content)` | Renders individual tab content div |
| `_mk_show_tabs_menu()` | Renders the search dropdown menu |
| `_wrap_tab_content(tab_content)` | Wraps tab content for HTMX out-of-band insertion |
| `_get_or_create_tab_content(tab_id)` | Gets tab content from cache or creates it |
| `_dynamic_get_content(tab_id)` | Retrieves component from InstancesManager |
| `_tab_already_exists(label, component)` | Checks if duplicate tab exists |
| `_add_or_update_tab(...)` | Internal method to add/update tab in state |
| `_get_ordered_tabs()` | Returns tabs ordered by tabs_order list |
| `_get_tab_list()` | Returns list of tab dictionaries in order |
| `_get_tab_count()` | Returns and increments internal tab counter |
### Tab Metadata Structure
@@ -612,11 +637,12 @@ Each tab in the `tabs` dictionary has the following structure:
```python
{
'id': 'uuid-string', # Unique tab identifier
'label': 'Tab Label', # Display label
'component_type': 'prefix', # Component class prefix (or None)
'component_id': 'instance-id' # Component instance ID (or None)
'id': 'uuid-string', # Unique tab identifier
'label': 'Tab Label', # Display label
'component_type': 'prefix', # Component class prefix (or None)
'component_id': 'instance-id' # Component instance ID (or None)
}
```
**Note:** `component_type` and `component_id` are `None` for plain FastHTML elements that don't inherit from `BaseInstance`.
**Note:** `component_type` and `component_id` are `None` for plain FastHTML elements that don't inherit from
`BaseInstance`.