From ce3924b5cab9803eb4bd8999ed9c6f0d16cf032a Mon Sep 17 00:00:00 2001 From: Kodjo Sossouvi Date: Fri, 5 Dec 2025 18:38:44 +0100 Subject: [PATCH] Added comprehensive documentation for TreeView and Layout components, including usage examples, advanced features, and developer references. --- docs/Layout.md | 574 ++++++++++++++++++++++++++-- docs/TreeView.md | 596 ++++++++++++++++++++++++++++++ src/myfasthtml/controls/Layout.py | 12 + 3 files changed, 1160 insertions(+), 22 deletions(-) create mode 100644 docs/TreeView.md diff --git a/docs/Layout.md b/docs/Layout.md index 52c0527..70797d8 100644 --- a/docs/Layout.md +++ b/docs/Layout.md @@ -1,11 +1,505 @@ -# Layout control +# Layout Component -## Overview +## Introduction -This component renders the global layout of the application. -This is only one instance per session. +The Layout component provides a complete application structure with fixed header and footer, a scrollable main content +area, and optional collapsible side drawers. It's designed to be the foundation of your FastHTML application's UI. -## State +**Key features:** + +- Fixed header and footer that stay visible while scrolling +- Collapsible left and right drawers for navigation, tools, or auxiliary content +- Resizable drawers with drag handles +- Automatic state persistence per session +- Single instance per session (singleton pattern) + +**Common use cases:** + +- Application with navigation sidebar +- Dashboard with tools panel +- Admin interface with settings drawer +- Documentation site with table of contents + +## Quick Start + +Here's a minimal example showing an application with a navigation sidebar: + +```python +from fasthtml.common import * +from myfasthtml.controls.Layout import Layout +from myfasthtml.controls.helpers import mk +from myfasthtml.core.commands import Command + +# Create the layout instance +layout = Layout(parent=root_instance, app_name="My App") + +# Add navigation items to the left drawer +layout.left_drawer.add( + mk.mk(Div("Home"), command=Command(...)) +) +layout.left_drawer.add( + mk.mk(Div("About"), command=Command(...)) +) +layout.left_drawer.add( + mk.mk(Div("Contact"), command=Command(...)) +) + +# Set the main content +layout.set_main( + Div( + H1("Welcome"), + P("This is the main content area") + ) +) + +# Render the layout +return layout +``` + +This creates a complete application layout with: + +- A header displaying the app name and drawer toggle button +- A collapsible left drawer with interactive navigation items +- A main content area that updates when navigation items are clicked +- An empty footer + +**Note:** Navigation items use commands to update the main content area without page reload. See the Commands section +below for details. + +## Basic Usage + +### Creating a Layout + +The Layout component is a `SingleInstance`, meaning there's only one instance per session. Create it by providing a +parent instance and an application name: + +```python +layout = Layout(parent=root_instance, app_name="My Application") +``` + +### Content Zones + +The Layout provides six content zones where you can add components: + +``` +┌──────────────────────────────────────────────────────────┐ +│ Header │ +│ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ header_left │ │ header_right │ │ +│ └─────────────────┘ └─────────────────┘ │ +├─────────┬────────────────────────────────────┬───────────┤ +│ │ │ │ +│ left │ │ right │ +│ drawer │ Main Content │ drawer │ +│ │ │ │ +│ │ │ │ +├─────────┴────────────────────────────────────┴───────────┤ +│ Footer │ +│ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ footer_left │ │ footer_right │ │ +│ └─────────────────┘ └─────────────────┘ │ +└──────────────────────────────────────────────────────────┘ +``` + +**Zone details:** + +| Zone | Typical Use | +|----------------|-----------------------------------------------| +| `header_left` | App logo, menu button, breadcrumbs | +| `header_right` | User profile, notifications, settings | +| `left_drawer` | Navigation menu, tree view, filters | +| `right_drawer` | Tools panel, properties inspector, debug info | +| `footer_left` | Copyright, legal links, version | +| `footer_right` | Status indicators, connection state | + +### Adding Content to Zones + +Use the `.add()` method to add components to any zone: + +```python +# Header +layout.header_left.add(Div("Logo")) +layout.header_right.add(Div("User: Admin")) + +# Drawers +layout.left_drawer.add(Div("Navigation")) +layout.right_drawer.add(Div("Tools")) + +# Footer +layout.footer_left.add(Div("© 2024 My App")) +layout.footer_right.add(Div("v1.0.0")) +``` + +### Setting Main Content + +The main content area displays your page content and can be updated dynamically: + +```python +# Set initial content +layout.set_main( + Div( + H1("Dashboard"), + P("Welcome to your dashboard") + ) +) + +# Update later (typically via commands) +layout.set_main( + Div( + H1("Settings"), + P("Configure your preferences") + ) +) +``` + +### Controlling Drawers + +By default, both drawers are visible. The drawer state is managed automatically: + +- Users can toggle drawers using the icon buttons in the header +- Users can resize drawers by dragging the handle +- Drawer state persists within the session + +The initial drawer widths are: + +- Left drawer: 250px +- Right drawer: 250px + +These can be adjusted by users and the state is preserved automatically. + +## Content System + +### Understanding Groups + +Each content zone (header_left, header_right, drawers, footer) supports **groups** to organize related items. Groups are +separated visually by dividers and can have optional labels. + +### Adding Content to Groups + +When adding content, you can optionally specify a group name: + +```python +# Add items to different groups in the left drawer +layout.left_drawer.add(Div("Dashboard"), group="main") +layout.left_drawer.add(Div("Analytics"), group="main") +layout.left_drawer.add(Div("Settings"), group="preferences") +layout.left_drawer.add(Div("Profile"), group="preferences") +``` + +This creates two groups: + +- **main**: Dashboard, Analytics +- **preferences**: Settings, Profile + +A visual divider automatically appears between groups. + +### Custom Group Labels + +You can provide a custom FastHTML element to display as the group header: + +```python +# Add a styled group header +layout.left_drawer.add_group( + "Navigation", + group_ft=Div("MAIN MENU", cls="font-bold text-sm opacity-60 px-2 py-1") +) + +# Then add items to this group +layout.left_drawer.add(Div("Home"), group="Navigation") +layout.left_drawer.add(Div("About"), group="Navigation") +``` + +### Ungrouped Content + +If you don't specify a group, content is added to the default (`None`) group: + +```python +# These items are in the default group +layout.left_drawer.add(Div("Quick Action 1")) +layout.left_drawer.add(Div("Quick Action 2")) +``` + +### Preventing Duplicates + +The Content system automatically prevents adding duplicate items based on their `id` attribute: + +```python +item = Div("Unique Item", id="my-item") +layout.left_drawer.add(item) +layout.left_drawer.add(item) # Ignored - already added +``` + +### Group Rendering Options + +Groups render differently depending on the zone: + +**In drawers** (vertical layout): + +- Groups stack vertically +- Dividers are horizontal lines +- Group labels appear above their content + +**In header/footer** (horizontal layout): + +- Groups arrange side-by-side +- Dividers are vertical lines +- Group labels are typically hidden + +## Advanced Features + +### Resizable Drawers + +Both drawers can be resized by users via drag handles: + +- **Drag handle location**: + - Left drawer: Right edge + - Right drawer: Left edge +- **Width constraints**: 150px (minimum) to 600px (maximum) +- **Persistence**: Resized width is automatically saved in the session state + +Users can drag the handle to adjust drawer width. The new width is preserved throughout their session. + +### Programmatic Drawer Control + +You can control drawers programmatically using commands: + +```python +# Toggle drawer visibility +toggle_left = layout.commands.toggle_drawer("left") +toggle_right = layout.commands.toggle_drawer("right") + +# Update drawer width +update_left_width = layout.commands.update_drawer_width("left", width=300) +update_right_width = layout.commands.update_drawer_width("right", width=350) +``` + +These commands are typically used with buttons or other interactive elements: + +```python +# Add a button to toggle the right drawer +button = mk.button("Toggle Tools", command=layout.commands.toggle_drawer("right")) +layout.header_right.add(button) +``` + +### State Persistence + +The Layout automatically persists its state within the user's session: + +| State Property | Description | Default | +|----------------------|---------------------------------|---------| +| `left_drawer_open` | Whether left drawer is visible | `True` | +| `right_drawer_open` | Whether right drawer is visible | `True` | +| `left_drawer_width` | Left drawer width in pixels | `250` | +| `right_drawer_width` | Right drawer width in pixels | `250` | + +State changes (toggle, resize) are automatically saved and restored within the session. + +### Dynamic Content Updates + +Content zones can be updated dynamically during the session: + +```python +# Initial setup +layout.left_drawer.add(Div("Item 1")) + + +# Later, add more items (e.g., in a command handler) +def add_dynamic_content(): + layout.left_drawer.add(Div("New Item"), group="dynamic") + return layout.left_drawer # Return updated drawer for HTMX swap +``` + +**Note**: When updating content dynamically, you typically return the updated zone to trigger an HTMX swap. + +### CSS Customization + +The Layout uses CSS classes that you can customize: + +| Class | Element | +|----------------------------|----------------------------------| +| `mf-layout` | Root layout container | +| `mf-layout-header` | Header section | +| `mf-layout-footer` | Footer section | +| `mf-layout-main` | Main content area | +| `mf-layout-drawer` | Drawer container | +| `mf-layout-left-drawer` | Left drawer specifically | +| `mf-layout-right-drawer` | Right drawer specifically | +| `mf-layout-drawer-content` | Scrollable content within drawer | +| `mf-resizer` | Resize handle | +| `mf-layout-group` | Content group wrapper | + +You can override these classes in your custom CSS to change colors, spacing, or behavior. + +### User Profile Integration + +The Layout automatically includes a UserProfile component in the header right area. This component handles user +authentication display and logout functionality when auth is enabled. + +## Examples + +### Example 1: Dashboard with Navigation Sidebar + +A typical dashboard application with a navigation menu in the left drawer: + +```python +from fasthtml.common import * +from myfasthtml.controls.Layout import Layout +from myfasthtml.controls.helpers import mk +from myfasthtml.core.commands import Command + +# Create layout +layout = Layout(parent=root_instance, app_name="Analytics Dashboard") + + +# Navigation menu in left drawer +def show_dashboard(): + layout.set_main(Div(H1("Dashboard"), P("Overview of your metrics"))) + return layout._mk_main() + + +def show_reports(): + layout.set_main(Div(H1("Reports"), P("Detailed analytics reports"))) + return layout._mk_main() + + +def show_settings(): + layout.set_main(Div(H1("Settings"), P("Configure your preferences"))) + return layout._mk_main() + + +# Add navigation items with groups +layout.left_drawer.add_group("main", group_ft=Div("MENU", cls="font-bold text-xs px-2 opacity-60")) +layout.left_drawer.add(mk.mk(Div("Dashboard"), command=Command("nav_dash", "Show dashboard", show_dashboard)), + group="main") +layout.left_drawer.add(mk.mk(Div("Reports"), command=Command("nav_reports", "Show reports", show_reports)), + group="main") + +layout.left_drawer.add_group("config", group_ft=Div("CONFIGURATION", cls="font-bold text-xs px-2 opacity-60")) +layout.left_drawer.add(mk.mk(Div("Settings"), command=Command("nav_settings", "Show settings", show_settings)), + group="config") + +# Header content +layout.header_left.add(Div("📊 Analytics", cls="font-bold")) + +# Footer +layout.footer_left.add(Div("© 2024 Analytics Co.")) +layout.footer_right.add(Div("v1.0.0")) + +# Set initial main content +layout.set_main(Div(H1("Dashboard"), P("Overview of your metrics"))) +``` + +### Example 2: Development Tool with Debug Panel + +An application with development tools in the right drawer: + +```python +from fasthtml.common import * +from myfasthtml.controls.Layout import Layout +from myfasthtml.controls.helpers import mk + +# Create layout +layout = Layout(parent=root_instance, app_name="Dev Tools") + +# Main content: code editor +layout.set_main( + Div( + H2("Code Editor"), + Textarea("# Write your code here", rows=20, cls="w-full font-mono") + ) +) + +# Right drawer: debug and tools +layout.right_drawer.add_group("debug", group_ft=Div("DEBUG INFO", cls="font-bold text-xs px-2 opacity-60")) +layout.right_drawer.add(Div("Console output here..."), group="debug") +layout.right_drawer.add(Div("Variables: x=10, y=20"), group="debug") + +layout.right_drawer.add_group("tools", group_ft=Div("TOOLS", cls="font-bold text-xs px-2 opacity-60")) +layout.right_drawer.add(Button("Run Code"), group="tools") +layout.right_drawer.add(Button("Clear Console"), group="tools") + +# Header +layout.header_left.add(Div("DevTools IDE")) +layout.header_right.add(Button("Save")) +``` + +### Example 3: Minimal Layout (Main Content Only) + +A simple layout without drawers, focusing only on main content: + +```python +from fasthtml.common import * +from myfasthtml.controls.Layout import Layout + +# Create layout +layout = Layout(parent=root_instance, app_name="Simple Blog") + +# Header +layout.header_left.add(Div("My Blog", cls="text-xl font-bold")) +layout.header_right.add(A("About", href="/about")) + +# Main content +layout.set_main( + Article( + H1("Welcome to My Blog"), + P("This is a simple blog layout without side drawers."), + P("The focus is on the content in the center.") + ) +) + +# Footer +layout.footer_left.add(Div("© 2024 Blog Author")) +layout.footer_right.add(A("RSS", href="/rss")) + +# Note: Drawers are present but can be collapsed by users if not needed +``` + +### Example 4: Dynamic Content Loading + +Loading content dynamically based on user interaction: + +```python +from fasthtml.common import * +from myfasthtml.controls.Layout import Layout +from myfasthtml.controls.helpers import mk +from myfasthtml.core.commands import Command + +layout = Layout(parent=root_instance, app_name="Dynamic App") + + +# Function that loads content dynamically +def load_page(page_name): + # Simulate loading different content + content = { + "home": Div(H1("Home"), P("Welcome to the home page")), + "profile": Div(H1("Profile"), P("User profile information")), + "settings": Div(H1("Settings"), P("Application settings")), + } + layout.set_main(content.get(page_name, Div("Page not found"))) + return layout._mk_main() + + +# Create navigation commands +pages = ["home", "profile", "settings"] +for page in pages: + cmd = Command(f"load_{page}", f"Load {page} page", load_page, page) + layout.left_drawer.add( + mk.mk(Div(page.capitalize()), command=cmd) + ) + +# Set initial content +layout.set_main(Div(H1("Home"), P("Welcome to the home page"))) +``` + +--- + +## Developer Reference + +This section contains technical details for developers working on the Layout component itself. + +### State + +The Layout component maintains the following state properties: | Name | Type | Description | Default | |----------------------|---------|----------------------------------|---------| @@ -14,32 +508,28 @@ This is only one instance per session. | `left_drawer_width` | integer | Width of the left drawer | 250 | | `right_drawer_width` | integer | Width of the right drawer | 250 | -## Commands +### Commands + +Available commands for programmatic control: | Name | Description | |-----------------------------------------|----------------------------------------------------------------------------------------| | `toggle_drawer(side)` | Toggles the drawer on the specified side | | `update_drawer_width(side, width=None)` | Updates the drawer width on the specified side. The width is given by the HTMX request | -## Ids +### Public Methods -| Name | Description | -|-------------|-------------------| -| `layout` | Singleton | -| `layout_h` | header | -| `layout_hl` | header left side | -| `layout_hr` | header right side | -| `layout_f` | footer | -| `layout_fl` | footer left side | -| `layout_fr` | footer right side | -| `layout_ld` | left drawer | -| `layout_rd` | right drawer | +| Method | Description | +|---------------------|-----------------------------| +| `set_main(content)` | Sets the main content area | +| `render()` | Renders the complete layout | + +### High Level Hierarchical Structure -## High Level Hierarchical Structure ``` Div(id="layout") ├── Header -│ ├── Div(id="layout_hl") +│ ├── Div(id="layout_hl") │ │ ├── Icon # Left drawer icon button │ │ └── Div # Left content for the header │ └── Div(id="layout_hr") @@ -47,7 +537,47 @@ Div(id="layout") │ └── UserProfile # user profile icon button ├── Div # Left Drawer ├── Main # Main content -├── Div # Right Drawer +├── Div # Right Drawer ├── Footer # Footer └── Script # To initialize the resizing -``` \ No newline at end of file +``` + +### Element IDs + +| Name | Description | +|-------------|-------------------------------------| +| `layout` | Root layout container (singleton) | +| `layout_h` | Header section (not currently used) | +| `layout_hl` | Header left side | +| `layout_hr` | Header right side | +| `layout_f` | Footer section (not currently used) | +| `layout_fl` | Footer left side | +| `layout_fr` | Footer right side | +| `layout_ld` | Left drawer | +| `layout_rd` | Right drawer | + +### Internal Methods + +These methods are used internally for rendering: + +| Method | Description | +|---------------------------|--------------------------------------------------------| +| `_mk_header()` | Renders the header component | +| `_mk_footer()` | Renders the footer component | +| `_mk_main()` | Renders the main content area | +| `_mk_left_drawer()` | Renders the left drawer | +| `_mk_right_drawer()` | Renders the right drawer | +| `_mk_left_drawer_icon()` | Renders the left drawer toggle icon | +| `_mk_right_drawer_icon()` | Renders the right drawer toggle icon | +| `_mk_content_wrapper()` | Static method to wrap content with groups and dividers | + +### Content Class + +The `Layout.Content` nested class manages content zones: + +| Method | Description | +|-----------------------------------|----------------------------------------------------------| +| `add(content, group=None)` | Adds content to a group, prevents duplicates based on ID | +| `add_group(group, group_ft=None)` | Creates a new group with optional custom header element | +| `get_content()` | Returns dictionary of groups and their content | +| `get_groups()` | Returns list of (group_name, group_ft) tuples | \ No newline at end of file diff --git a/docs/TreeView.md b/docs/TreeView.md new file mode 100644 index 0000000..07a5254 --- /dev/null +++ b/docs/TreeView.md @@ -0,0 +1,596 @@ +# TreeView Component + +## Introduction + +The TreeView component provides an interactive hierarchical data visualization with full CRUD operations. It's designed for displaying tree-structured data like file systems, organizational charts, or navigation menus with inline editing capabilities. + +**Key features:** + +- Expand/collapse nodes with visual indicators +- Add child and sibling nodes dynamically +- Inline rename with keyboard support (ESC to cancel) +- Delete nodes (only leaf nodes without children) +- Node selection tracking +- Persistent state per session +- Configurable icons per node type + +**Common use cases:** + +- File/folder browser +- Category/subcategory management +- Organizational hierarchy viewer +- Navigation menu builder +- Document outline editor + +## Quick Start + +Here's a minimal example showing a file system tree: + +```python +from fasthtml.common import * +from myfasthtml.controls.TreeView import TreeView, TreeNode + +# Create TreeView instance +tree = TreeView(parent=root_instance, _id="file-tree") + +# Add root folder +root = TreeNode(id="root", label="Documents", type="folder") +tree.add_node(root) + +# Add some files +file1 = TreeNode(id="file1", label="report.pdf", type="file") +file2 = TreeNode(id="file2", label="budget.xlsx", type="file") +tree.add_node(file1, parent_id="root") +tree.add_node(file2, parent_id="root") + +# Expand root to show children +tree.expand_all() + +# Render the tree +return tree +``` + +This creates an interactive tree where users can: +- Click chevrons to expand/collapse folders +- Click labels to select items +- Use action buttons (visible on hover) to add, rename, or delete nodes + +**Note:** All interactions use commands and update via HTMX without page reload. + +## Basic Usage + +### Creating a TreeView + +TreeView is a `MultipleInstance`, allowing multiple trees per session. Create it with a parent instance: + +```python +tree = TreeView(parent=root_instance, _id="my-tree") +``` + +### TreeNode Structure + +Nodes are represented by the `TreeNode` dataclass: + +```python +from myfasthtml.controls.TreeView import TreeNode + +node = TreeNode( + id="unique-id", # Auto-generated UUID if not provided + label="Node Label", # Display text + type="default", # Type for icon mapping + parent=None, # Parent node ID (None for root) + children=[] # List of child node IDs +) +``` + +### Adding Nodes + +Add nodes using the `add_node()` method: + +```python +# Add root node +root = TreeNode(id="root", label="Root", type="folder") +tree.add_node(root) + +# Add child node +child = TreeNode(label="Child 1", type="item") +tree.add_node(child, parent_id="root") + +# Add with specific position +sibling = TreeNode(label="Child 2", type="item") +tree.add_node(sibling, parent_id="root", insert_index=0) # Insert at start +``` + +### Visual Structure + +``` +TreeView +├── Root Node 1 +│ ├── [>] Child 1-1 # Collapsed node with children +│ ├── [ ] Child 1-2 # Leaf node (no children) +│ └── [v] Child 1-3 # Expanded node +│ ├── [ ] Grandchild +│ └── [ ] Grandchild +└── Root Node 2 + └── [>] Child 2-1 +``` + +**Legend:** +- `[>]` - Collapsed node (has children) +- `[v]` - Expanded node (has children) +- `[ ]` - Leaf node (no children) + +### Expanding Nodes + +Control node expansion programmatically: + +```python +# Expand all nodes with children +tree.expand_all() + +# Expand specific nodes by adding to opened list +tree._state.opened.append("node-id") +``` + +**Note:** Users can also toggle nodes by clicking the chevron icon. + +## Interactive Features + +### Node Selection + +Users can select nodes by clicking on labels. The selected node is visually highlighted: + +```python +# Programmatically select a node +tree._state.selected = "node-id" + +# Check current selection +current = tree._state.selected +``` + +### Adding Nodes + +Users can add nodes via action buttons (visible on hover): + +**Add Child:** +- Adds a new node as a child of the target node +- Automatically expands the parent +- Creates node with same type as parent + +**Add Sibling:** +- Adds a new node next to the target node (same parent) +- Inserts after the target node +- Cannot add sibling to root nodes + +```python +# Programmatically add child +tree._add_child(parent_id="root", new_label="New Child") + +# Programmatically add sibling +tree._add_sibling(node_id="child1", new_label="New Sibling") +``` + +### Renaming Nodes + +Users can rename nodes via the edit button: + +1. Click the edit icon (visible on hover) +2. Input field appears with current label +3. Press Enter to save (triggers command) +4. Press ESC to cancel (keyboard shortcut) + +```python +# Programmatically start rename +tree._start_rename("node-id") + +# Save rename +tree._save_rename("node-id", "New Label") + +# Cancel rename +tree._cancel_rename() +``` + +### Deleting Nodes + +Users can delete nodes via the delete button: + +**Restrictions:** +- Can only delete leaf nodes (no children) +- Attempting to delete a node with children raises an error +- Deleted node is removed from parent's children list + +```python +# Programmatically delete node +tree._delete_node("node-id") # Raises ValueError if node has children +``` + +## Content System + +### Node Types and Icons + +Assign types to nodes for semantic grouping and custom icon display: + +```python +# Define node types +root = TreeNode(label="Project", type="project") +folder = TreeNode(label="src", type="folder") +file = TreeNode(label="main.py", type="python-file") + +# Configure icons for types +tree.set_icon_config({ + "project": "fluent.folder_open", + "folder": "fluent.folder", + "python-file": "fluent.document_python" +}) +``` + +**Note:** Icon configuration is stored in state and persists within the session. + +### Hierarchical Organization + +Nodes automatically maintain parent-child relationships: + +```python +# Get node's children +node = tree._state.items["node-id"] +child_ids = node.children + +# Get node's parent +parent_id = node.parent + +# Navigate tree programmatically +for child_id in node.children: + child_node = tree._state.items[child_id] + print(child_node.label) +``` + +### Finding Root Nodes + +Root nodes are nodes without a parent: + +```python +root_nodes = [ + node_id for node_id, node in tree._state.items.items() + if node.parent is None +] +``` + +## Advanced Features + +### Keyboard Shortcuts + +TreeView includes keyboard support for common operations: + +| Key | Action | +|-----|--------| +| `ESC` | Cancel rename operation | + +Additional shortcuts can be added via the Keyboard component: + +```python +from myfasthtml.controls.Keyboard import Keyboard + +tree = TreeView(parent=root_instance) +# ESC handler is automatically included for cancel rename +``` + +### State Management + +TreeView maintains persistent state within the session: + +| State Property | Type | Description | +|----------------|------|-------------| +| `items` | `dict[str, TreeNode]` | All nodes indexed by ID | +| `opened` | `list[str]` | IDs of expanded nodes | +| `selected` | `str \| None` | Currently selected node ID | +| `editing` | `str \| None` | Node being renamed (if any) | +| `icon_config` | `dict[str, str]` | Type-to-icon mapping | + +### Dynamic Updates + +TreeView updates are handled via commands that return the updated tree: + +```python +# Commands automatically target the tree for HTMX swap +cmd = tree.commands.toggle_node("node-id") +# When executed, returns updated TreeView with new state +``` + +### CSS Customization + +TreeView uses CSS classes for styling: + +| Class | Element | +|-------|---------| +| `mf-treeview` | Root container | +| `mf-treenode-container` | Container for node and its children | +| `mf-treenode` | Individual node row | +| `mf-treenode.selected` | Selected node highlight | +| `mf-treenode-label` | Node label text | +| `mf-treenode-input` | Input field during rename | +| `mf-treenode-actions` | Action buttons container (hover) | + +You can override these classes to customize appearance: + +```css +.mf-treenode.selected { + background-color: #e0f2fe; + border-left: 3px solid #0284c7; +} + +.mf-treenode-actions { + opacity: 0; + transition: opacity 0.2s; +} + +.mf-treenode:hover .mf-treenode-actions { + opacity: 1; +} +``` + +## Examples + +### Example 1: File System Browser + +A file/folder browser with different node types: + +```python +from fasthtml.common import * +from myfasthtml.controls.TreeView import TreeView, TreeNode + +# Create tree +tree = TreeView(parent=root_instance, _id="file-browser") + +# Configure icons +tree.set_icon_config({ + "folder": "fluent.folder", + "python": "fluent.document_python", + "text": "fluent.document_text" +}) + +# Build file structure +root = TreeNode(id="root", label="my-project", type="folder") +tree.add_node(root) + +src = TreeNode(id="src", label="src", type="folder") +tree.add_node(src, parent_id="root") + +main = TreeNode(label="main.py", type="python") +utils = TreeNode(label="utils.py", type="python") +tree.add_node(main, parent_id="src") +tree.add_node(utils, parent_id="src") + +readme = TreeNode(label="README.md", type="text") +tree.add_node(readme, parent_id="root") + +# Expand to show structure +tree.expand_all() + +return tree +``` + +### Example 2: Category Management + +Managing product categories with inline editing: + +```python +from fasthtml.common import * +from myfasthtml.controls.TreeView import TreeView, TreeNode + +tree = TreeView(parent=root_instance, _id="categories") + +# Root categories +electronics = TreeNode(id="elec", label="Electronics", type="category") +tree.add_node(electronics) + +# Subcategories +computers = TreeNode(label="Computers", type="subcategory") +phones = TreeNode(label="Phones", type="subcategory") +tree.add_node(computers, parent_id="elec") +tree.add_node(phones, parent_id="elec") + +# Products (leaf nodes) +laptop = TreeNode(label="Laptops", type="product") +desktop = TreeNode(label="Desktops", type="product") +tree.add_node(laptop, parent_id=computers.id) +tree.add_node(desktop, parent_id=computers.id) + +tree.expand_all() +return tree +``` + +### Example 3: Document Outline Editor + +Building a document outline with headings: + +```python +from fasthtml.common import * +from myfasthtml.controls.TreeView import TreeView, TreeNode + +tree = TreeView(parent=root_instance, _id="outline") + +# Document structure +doc = TreeNode(id="doc", label="My Document", type="document") +tree.add_node(doc) + +# Chapters +ch1 = TreeNode(id="ch1", label="Chapter 1: Introduction", type="heading1") +ch2 = TreeNode(id="ch2", label="Chapter 2: Methods", type="heading1") +tree.add_node(ch1, parent_id="doc") +tree.add_node(ch2, parent_id="doc") + +# Sections +sec1_1 = TreeNode(label="1.1 Background", type="heading2") +sec1_2 = TreeNode(label="1.2 Objectives", type="heading2") +tree.add_node(sec1_1, parent_id="ch1") +tree.add_node(sec1_2, parent_id="ch1") + +# Subsections +subsec = TreeNode(label="1.1.1 Historical Context", type="heading3") +tree.add_node(subsec, parent_id=sec1_1.id) + +tree.expand_all() +return tree +``` + +### Example 4: Dynamic Tree with Event Handling + +Responding to tree events with custom logic: + +```python +from fasthtml.common import * +from myfasthtml.controls.TreeView import TreeView, TreeNode +from myfasthtml.controls.helpers import mk +from myfasthtml.core.commands import Command + +tree = TreeView(parent=root_instance, _id="dynamic-tree") + +# Initial structure +root = TreeNode(id="root", label="Tasks", type="folder") +tree.add_node(root) + +# Function to handle selection +def on_node_selected(node_id): + # Custom logic when node is selected + node = tree._state.items[node_id] + tree._select_node(node_id) + + # Update a detail panel elsewhere in the UI + return Div( + H3(f"Selected: {node.label}"), + P(f"Type: {node.type}"), + P(f"Children: {len(node.children)}") + ) + +# Override select command with custom handler +# (In practice, you'd extend the Commands class or use event callbacks) + +tree.expand_all() +return tree +``` + +--- + +## Developer Reference + +This section contains technical details for developers working on the TreeView component itself. + +### State + +The TreeView component maintains the following state properties: + +| Name | Type | Description | Default | +|------|------|-------------|---------| +| `items` | `dict[str, TreeNode]` | All nodes indexed by ID | `{}` | +| `opened` | `list[str]` | Expanded node IDs | `[]` | +| `selected` | `str \| None` | Selected node ID | `None` | +| `editing` | `str \| None` | Node being renamed | `None` | +| `icon_config` | `dict[str, str]` | Type-to-icon mapping | `{}` | + +### Commands + +Available commands for programmatic control: + +| Name | Description | +|------|-------------| +| `toggle_node(node_id)` | Toggle expand/collapse state | +| `add_child(parent_id)` | Add child node to parent | +| `add_sibling(node_id)` | Add sibling node after target | +| `start_rename(node_id)` | Enter rename mode for node | +| `save_rename(node_id)` | Save renamed node label | +| `cancel_rename()` | Cancel rename operation | +| `delete_node(node_id)` | Delete node (if no children) | +| `select_node(node_id)` | Select a node | + +All commands automatically target the TreeView component for HTMX updates. + +### Public Methods + +| Method | Description | +|--------|-------------| +| `add_node(node, parent_id, insert_index)` | Add a node to the tree | +| `expand_all()` | Expand all nodes with children | +| `set_icon_config(config)` | Configure icons for node types | +| `render()` | Render the complete TreeView | + +### TreeNode Dataclass + +```python +@dataclass +class TreeNode: + id: str # Unique identifier (auto-generated UUID) + label: str = "" # Display text + type: str = "default" # Node type for icon mapping + parent: Optional[str] = None # Parent node ID + children: list[str] = [] # Child node IDs +``` + +### High Level Hierarchical Structure + +``` +Div(id="treeview", cls="mf-treeview") +├── Div(cls="mf-treenode-container", data-node-id="root1") +│ ├── Div(cls="mf-treenode") +│ │ ├── Icon # Toggle chevron +│ │ ├── Span(cls="mf-treenode-label") | Input(cls="mf-treenode-input") +│ │ └── Div(cls="mf-treenode-actions") +│ │ ├── Icon # Add child +│ │ ├── Icon # Rename +│ │ └── Icon # Delete +│ └── Div(cls="mf-treenode-container") # Child nodes (if expanded) +│ └── ... +├── Div(cls="mf-treenode-container", data-node-id="root2") +│ └── ... +└── Keyboard # ESC handler +``` + +### Element IDs and Attributes + +| Attribute | Element | Description | +|-----------|---------|-------------| +| `id` | Root Div | TreeView component ID | +| `data-node-id` | Node container | Node's unique ID | + +### Internal Methods + +These methods are used internally for rendering and state management: + +| Method | Description | +|--------|-------------| +| `_toggle_node(node_id)` | Toggle expand/collapse state | +| `_add_child(parent_id, new_label)` | Add child node implementation | +| `_add_sibling(node_id, new_label)` | Add sibling node implementation | +| `_start_rename(node_id)` | Enter rename mode | +| `_save_rename(node_id, node_label)` | Save renamed node | +| `_cancel_rename()` | Cancel rename operation | +| `_delete_node(node_id)` | Delete node if no children | +| `_select_node(node_id)` | Select a node | +| `_render_action_buttons(node_id)` | Render hover action buttons | +| `_render_node(node_id, level)` | Recursively render node and children | + +### Commands Class + +The `Commands` nested class provides command factory methods: + +| Method | Returns | +|--------|---------| +| `toggle_node(node_id)` | Command to toggle node | +| `add_child(parent_id)` | Command to add child | +| `add_sibling(node_id)` | Command to add sibling | +| `start_rename(node_id)` | Command to start rename | +| `save_rename(node_id)` | Command to save rename | +| `cancel_rename()` | Command to cancel rename | +| `delete_node(node_id)` | Command to delete node | +| `select_node(node_id)` | Command to select node | + +All commands are automatically configured with HTMX targeting. + +### Integration with Keyboard Component + +TreeView includes a Keyboard component for ESC key handling: + +```python +Keyboard(self, {"esc": self.commands.cancel_rename()}, _id="-keyboard") +``` + +This enables users to press ESC to cancel rename operations without clicking. diff --git a/src/myfasthtml/controls/Layout.py b/src/myfasthtml/controls/Layout.py index 180fd57..758a335 100644 --- a/src/myfasthtml/controls/Layout.py +++ b/src/myfasthtml/controls/Layout.py @@ -138,6 +138,18 @@ class Layout(SingleInstance): return self def toggle_drawer(self, side: Literal["left", "right"]): + """ + Toggle the state of a drawer (open or close) based on the specified side. This + method also generates the corresponding icon and drawer elements for the + selected side. + + :param side: The side of the drawer to toggle. Must be either "left" or "right". + :type side: Literal["left", "right"] + :return: A tuple containing the updated drawer icon and drawer elements for + the specified side. + :rtype: Tuple[Any, Any] + :raises ValueError: If the provided `side` is not "left" or "right". + """ logger.debug(f"Toggle drawer: {side=}, {self._state.left_drawer_open=}") if side == "left": self._state.left_drawer_open = not self._state.left_drawer_open