2 Commits

10 changed files with 1089 additions and 111 deletions

View File

@@ -1,10 +1,18 @@
# Technical Writer Persona # Technical Writer Mode
You are now acting as a **Technical Writer** specialized in user-facing documentation. You are now in **Technical Writer Mode** - specialized mode for writing user-facing documentation for the MyFastHtml project.
## Your Role ## Primary Objective
Create comprehensive user documentation by:
1. Reading the source code to understand the component
2. Proposing structure for validation
3. Writing documentation following established patterns
4. Requesting feedback after completion
## What You Handle
Focus on creating and improving **user documentation** for the MyFastHtml library:
- README sections and examples - README sections and examples
- Usage guides and tutorials - Usage guides and tutorials
- Getting started documentation - Getting started documentation
@@ -18,47 +26,316 @@ Focus on creating and improving **user documentation** for the MyFastHtml librar
- Code comments - Code comments
- CLAUDE.md (handled by developers) - CLAUDE.md (handled by developers)
## Documentation Principles ## Technical Writer Rules (TW)
**Clarity First:** ### TW-1: Standard Documentation Structure
- Write for developers who are new to MyFastHtml
- Explain the "why" not just the "what"
- Use concrete, runnable examples
- Progressive complexity (simple → advanced)
**Structure:** Every component documentation MUST follow this structure in order:
- Start with the problem being solved
- Show minimal working example
- Explain key concepts
- Provide variations and advanced usage
- Link to related documentation
**Examples Must:** | Section | Purpose | Required |
- Be complete and runnable |---------|---------|----------|
- Include necessary imports | **Introduction** | What it is, key features, common use cases | Yes |
- Show expected output when relevant | **Quick Start** | Minimal working example | Yes |
- Use realistic variable names | **Basic Usage** | Visual structure, creation, configuration | Yes |
- Follow the project's code standards (PEP 8, snake_case, English) | **Advanced Features** | Complex use cases, customization | If applicable |
| **Examples** | 3-4 complete, practical examples | Yes |
| **Developer Reference** | Technical details for component developers | Yes |
## Communication Style **Introduction template:**
```markdown
## Introduction
**Conversations:** French or English (match user's language) The [Component] component provides [brief description]. It handles [main functionality] out of the box.
**Written documentation:** English only
## Workflow **Key features:**
1. **Ask questions** to understand what needs documentation - Feature 1
2. **Propose structure** before writing content - Feature 2
3. **Wait for validation** before proceeding - Feature 3
4. **Write incrementally** - one section at a time
5. **Request feedback** after each section
## Style Evolution **Common use cases:**
The documentation style will improve iteratively based on feedback. Start with clear, simple writing and refine over time. - Use case 1
- Use case 2
- Use case 3
```
## Exiting This Persona **Quick Start template:**
```markdown
## Quick Start
To return to normal mode: Here's a minimal example showing [what it does]:
- Use `/developer` to switch to developer mode
\`\`\`python
[Complete, runnable code]
\`\`\`
This creates a complete [component] with:
- Bullet point 1
- Bullet point 2
**Note:** [Important default behavior or tip]
```
### TW-2: Visual Structure Diagrams
**Principle:** Include ASCII diagrams to illustrate component structure.
**Use box-drawing characters:** `┌ ┐ └ ┘ ─ │ ├ ┤ ┬ ┴ ┼`
**Example for a dropdown:**
```
Closed state:
┌──────────────┐
│ Button ▼ │
└──────────────┘
Open state (position="below", align="left"):
┌──────────────┐
│ Button ▼ │
├──────────────┴─────────┐
│ Dropdown Content │
│ - Option 1 │
│ - Option 2 │
└────────────────────────┘
```
**Rules:**
- Label all important elements
- Show different states when relevant (open/closed, visible/hidden)
- Keep diagrams simple and focused
- Use comments in diagrams when needed
### TW-3: Component Details Tables
**Principle:** Use markdown tables to summarize information.
**Component elements table:**
```markdown
| Element | Description |
|---------------|-----------------------------------------------|
| Left panel | Optional collapsible panel (default: visible) |
| Main content | Always-visible central content area |
```
**Constructor parameters table:**
```markdown
| Parameter | Type | Description | Default |
|------------|-------------|------------------------------------|-----------|
| `parent` | Instance | Parent instance (required) | - |
| `position` | str | Vertical position: "below"/"above" | `"below"` |
```
**State properties table:**
```markdown
| Name | Type | Description | Default |
|----------|---------|------------------------------|---------|
| `opened` | boolean | Whether dropdown is open | `False` |
```
**CSS classes table:**
```markdown
| Class | Element |
|-----------------------|---------------------------------------|
| `mf-dropdown-wrapper` | Container with relative positioning |
| `mf-dropdown` | Dropdown content panel |
```
**Commands table:**
```markdown
| Name | Description |
|-----------|-------------------------------------------------|
| `close()` | Closes the dropdown |
| `click()` | Handles click events (toggle or close behavior) |
```
### TW-4: Code Examples Standards
**All code examples must:**
1. **Be complete and runnable** - Include all necessary imports
2. **Use realistic variable names** - Not `foo`, `bar`, `x`
3. **Follow PEP 8** - snake_case, proper indentation
4. **Include comments** - Only when clarifying non-obvious logic
**Standard imports block:**
```python
from fasthtml.common import *
from myfasthtml.controls.ComponentName import ComponentName
from myfasthtml.core.instances import RootInstance
```
**Example with commands:**
```python
from fasthtml.common import *
from myfasthtml.controls.Dropdown import Dropdown
from myfasthtml.controls.helpers import mk
from myfasthtml.core.commands import Command
# Define action
def do_something():
return "Result"
# Create command
cmd = Command("action", "Description", do_something)
# Create component with command
dropdown = Dropdown(
parent=root,
button=Button("Menu", cls="btn"),
content=Div(
mk.button("Action", command=cmd, cls="btn btn-ghost")
)
)
```
**Avoid:**
- Incomplete snippets without imports
- Abstract examples without context
- `...` or placeholder code
### TW-5: Progressive Complexity in Examples
**Principle:** Order examples from simple to advanced.
**Example naming pattern:**
```markdown
### Example 1: [Simple Use Case]
[Most basic, common usage]
### Example 2: [Intermediate Use Case]
[Common variation or configuration]
### Example 3: [Advanced Use Case]
[Complex scenario or customization]
### Example 4: [Integration Example]
[Combined with other components or commands]
```
**Each example must include:**
- Descriptive title
- Brief explanation of what it demonstrates
- Complete, runnable code
- Comments for non-obvious parts
### TW-6: Developer Reference Section
**Principle:** Include technical details for developers working on the component.
**Required subsections:**
```markdown
---
## Developer Reference
This section contains technical details for developers working on the [Component] component itself.
### State
| Name | Type | Description | Default |
|----------|---------|------------------------------|---------|
| `opened` | boolean | Whether dropdown is open | `False` |
### Commands
| Name | Description |
|-----------|-------------------------------------------------|
| `close()` | Closes the dropdown |
### Public Methods
| Method | Description | Returns |
|------------|----------------------------|----------------------|
| `toggle()` | Toggles open/closed state | Content tuple |
| `render()` | Renders complete component | `Div` |
### Constructor Parameters
| Parameter | Type | Description | Default |
|------------|-------------|------------------------------------|-----------|
| `parent` | Instance | Parent instance (required) | - |
### High Level Hierarchical Structure
\`\`\`
Div(id="{id}")
├── Div(cls="wrapper")
│ ├── Div(cls="button")
│ │ └── [Button content]
│ └── Div(id="{id}-content")
│ └── [Content]
└── Script
\`\`\`
### Element IDs
| Name | Description |
|------------------|--------------------------------|
| `{id}` | Root container |
| `{id}-content` | Content panel |
**Note:** `{id}` is the instance ID (auto-generated or custom `_id`).
### Internal Methods
| Method | Description |
|-----------------|------------------------------------------|
| `_mk_content()` | Renders the content panel |
```
### TW-7: Communication Language
**Conversations**: French or English (match user's language)
**Written documentation**: English only
**No emojis** in documentation unless explicitly requested.
### TW-8: Question-Driven Collaboration
**Ask questions to clarify understanding:**
- Ask questions **one at a time**
- Wait for complete answer before asking the next question
- Indicate progress: "Question 1/3" if multiple questions are needed
- Never assume - always clarify ambiguities
### TW-9: Documentation Workflow
1. **Receive request** - User specifies component/feature to document
2. **Read source code** - Understand implementation thoroughly
3. **Propose structure** - Present outline with sections
4. **Wait for validation** - Get approval before writing
5. **Write documentation** - Follow all TW rules
6. **Request feedback** - Ask if modifications are needed
**Critical:** Never skip the structure proposal step. Always get validation before writing.
### TW-10: File Location
Documentation files are created in the `docs/` folder:
- Component docs: `docs/ComponentName.md`
- Feature docs: `docs/Feature Name.md`
---
## Managing Rules
To disable a specific rule, the user can say:
- "Disable TW-2" (do not include ASCII diagrams)
- "Enable TW-2" (re-enable a previously disabled rule)
When a rule is disabled, acknowledge it and adapt behavior accordingly.
## Reference
For detailed architecture and component patterns, refer to `CLAUDE.md` in the project root.
## Other Personas
- Use `/developer` to switch to development mode
- Use `/unit-tester` to switch to unit testing mode
- Use `/reset` to return to default Claude Code mode - Use `/reset` to return to default Claude Code mode

557
docs/Dropdown.md Normal file
View File

@@ -0,0 +1,557 @@
# Dropdown Component
## Introduction
The Dropdown component provides an interactive dropdown menu that toggles open or closed when clicking a trigger button. It handles positioning, automatic closing behavior, and keyboard navigation out of the box.
**Key features:**
- Toggle open/close on button click
- Automatic close when clicking outside
- Keyboard support (ESC to close)
- Configurable vertical position (above or below the button)
- Configurable horizontal alignment (left, right, or center)
- Session-based state management
- HTMX-powered updates without page reload
**Common use cases:**
- Navigation menus
- User account menus
- Action menus (edit, delete, share)
- Filter or sort options
- Context-sensitive toolbars
- Settings quick access
## Quick Start
Here's a minimal example showing a dropdown menu with navigation links:
```python
from fasthtml.common import *
from myfasthtml.controls.Dropdown import Dropdown
from myfasthtml.core.instances import RootInstance
# Create root instance and dropdown
root = RootInstance(session)
dropdown = Dropdown(
parent=root,
button=Button("Menu", cls="btn"),
content=Ul(
Li(A("Home", href="/")),
Li(A("Settings", href="/settings")),
Li(A("Logout", href="/logout"))
)
)
# Render the dropdown
return dropdown
```
This creates a complete dropdown with:
- A "Menu" button that toggles the dropdown
- A list of navigation links displayed below the button
- Automatic closing when clicking outside the dropdown
- ESC key support to close the dropdown
**Note:** The dropdown opens below the button and aligns to the left by default. Users can click anywhere outside the dropdown to close it, or press ESC on the keyboard.
## Basic Usage
### Visual Structure
The Dropdown component consists of a trigger button and a content panel:
```
Closed state:
┌──────────────┐
│ Button ▼ │
└──────────────┘
Open state (position="below", align="left"):
┌──────────────┐
│ Button ▼ │
├──────────────┴─────────┐
│ Dropdown Content │
│ - Option 1 │
│ - Option 2 │
│ - Option 3 │
└────────────────────────┘
Open state (position="above", align="right"):
┌────────────────────────┐
│ Dropdown Content │
│ - Option 1 │
│ - Option 2 │
├──────────────┬─────────┘
│ Button ▲ │
└──────────────┘
```
**Component details:**
| Element | Description |
|-----------|------------------------------------------------|
| Button | Trigger element that toggles the dropdown |
| Content | Panel containing the dropdown menu items |
| Wrapper | Container with relative positioning for anchor |
### Creating a Dropdown
The Dropdown is a `MultipleInstance`, meaning you can create multiple independent dropdowns in your application. Create it by providing a parent instance:
```python
dropdown = Dropdown(parent=root_instance, button=my_button, content=my_content)
# Or with a custom ID
dropdown = Dropdown(parent=root_instance, button=my_button, content=my_content, _id="my-dropdown")
```
### Button and Content
The dropdown requires two main elements:
**Button:** The trigger element that users click to toggle the dropdown.
```python
# Simple text button
dropdown = Dropdown(
parent=root,
button=Button("Click me", cls="btn btn-primary"),
content=my_content
)
# Button with icon
dropdown = Dropdown(
parent=root,
button=Div(
icon_svg,
Span("Options"),
cls="flex items-center gap-2"
),
content=my_content
)
# Just an icon
dropdown = Dropdown(
parent=root,
button=icon_svg,
content=my_content
)
```
**Content:** Any FastHTML element to display in the dropdown panel.
```python
# Simple list
content = Ul(
Li("Option 1"),
Li("Option 2"),
Li("Option 3"),
cls="menu"
)
# Complex content with sections
content = Div(
Div("User Actions", cls="font-bold p-2"),
Hr(),
Button("Edit Profile", cls="btn btn-ghost w-full"),
Button("Settings", cls="btn btn-ghost w-full"),
Hr(),
Button("Logout", cls="btn btn-error w-full")
)
```
### Positioning Options
The Dropdown supports two positioning parameters:
**`position`** - Vertical position relative to the button:
- `"below"` (default): Dropdown appears below the button
- `"above"`: Dropdown appears above the button
**`align`** - Horizontal alignment relative to the button:
- `"left"` (default): Dropdown aligns to the left edge of the button
- `"right"`: Dropdown aligns to the right edge of the button
- `"center"`: Dropdown is centered relative to the button
```python
# Default: below + left
dropdown = Dropdown(parent=root, button=btn, content=menu)
# Above the button, aligned right
dropdown = Dropdown(parent=root, button=btn, content=menu, position="above", align="right")
# Below the button, centered
dropdown = Dropdown(parent=root, button=btn, content=menu, position="below", align="center")
```
**Visual examples of all combinations:**
```
position="below", align="left" position="below", align="center" position="below", align="right"
┌────────┐ ┌────────┐ ┌────────┐
│ Button │ │ Button │ │ Button │
├────────┴────┐ ┌────┴────────┴────┐ ┌────────────┴────────┤
│ Content │ │ Content │ │ Content │
└─────────────┘ └──────────────────┘ └─────────────────────┘
position="above", align="left" position="above", align="center" position="above", align="right"
┌─────────────┐ ┌──────────────────┐ ┌─────────────────────┐
│ Content │ │ Content │ │ Content │
├────────┬────┘ └────┬────────┬────┘ └────────────┬────────┤
│ Button │ │ Button │ │ Button │
└────────┘ └────────┘ └────────┘
```
## Advanced Features
### Automatic Close Behavior
The Dropdown automatically closes in two scenarios:
**Click outside:** When the user clicks anywhere outside the dropdown, it closes automatically. This is handled by the Mouse component listening for global click events.
**ESC key:** When the user presses the ESC key, the dropdown closes. This is handled by the Keyboard component.
```python
# Both behaviors are enabled by default - no configuration needed
dropdown = Dropdown(parent=root, button=btn, content=menu)
```
**How it works internally:**
- The `Mouse` component detects clicks and sends `is_inside` and `is_button` parameters
- If `is_button` is true, the dropdown toggles
- If `is_inside` is false (clicked outside), the dropdown closes
- The `Keyboard` component listens for ESC and triggers the close command
### Programmatic Control
You can control the dropdown programmatically using its methods and commands:
```python
# Toggle the dropdown state
dropdown.toggle()
# Close the dropdown
dropdown.close()
# Access commands for use with other controls
close_cmd = dropdown.commands.close()
click_cmd = dropdown.commands.click()
```
**Using commands with buttons:**
```python
from myfasthtml.controls.helpers import mk
# Create a button that closes the dropdown
close_button = mk.button("Close", command=dropdown.commands.close())
# Add it to the dropdown content
dropdown = Dropdown(
parent=root,
button=Button("Menu"),
content=Div(
Ul(Li("Option 1"), Li("Option 2")),
close_button
)
)
```
### CSS Customization
The Dropdown uses CSS classes that you can customize:
| Class | Element |
|-----------------------|---------------------------------------|
| `mf-dropdown-wrapper` | Container with relative positioning |
| `mf-dropdown-btn` | Button wrapper |
| `mf-dropdown` | Dropdown content panel |
| `mf-dropdown-below` | Applied when position="below" |
| `mf-dropdown-above` | Applied when position="above" |
| `mf-dropdown-left` | Applied when align="left" |
| `mf-dropdown-right` | Applied when align="right" |
| `mf-dropdown-center` | Applied when align="center" |
| `is-visible` | Applied when dropdown is open |
**Example customization:**
```css
/* Change dropdown background and border */
.mf-dropdown {
background-color: #1f2937;
border: 1px solid #374151;
border-radius: 0.5rem;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
}
/* Add animation */
.mf-dropdown {
opacity: 0;
transform: translateY(-10px);
transition: opacity 0.2s ease, transform 0.2s ease;
}
.mf-dropdown.is-visible {
opacity: 1;
transform: translateY(0);
}
/* Style for above position */
.mf-dropdown-above {
transform: translateY(10px);
}
.mf-dropdown-above.is-visible {
transform: translateY(0);
}
```
## Examples
### Example 1: Navigation Menu
A simple navigation dropdown menu:
```python
from fasthtml.common import *
from myfasthtml.controls.Dropdown import Dropdown
dropdown = Dropdown(
parent=root,
button=Button("Navigation", cls="btn btn-ghost"),
content=Ul(
Li(A("Dashboard", href="/dashboard", cls="block p-2 hover:bg-base-200")),
Li(A("Projects", href="/projects", cls="block p-2 hover:bg-base-200")),
Li(A("Tasks", href="/tasks", cls="block p-2 hover:bg-base-200")),
Li(A("Reports", href="/reports", cls="block p-2 hover:bg-base-200")),
cls="menu p-2"
)
)
return dropdown
```
### Example 2: User Account Menu
A user menu aligned to the right, typically placed in a header:
```python
from fasthtml.common import *
from myfasthtml.controls.Dropdown import Dropdown
# User avatar button
user_button = Div(
Img(src="/avatar.png", cls="w-8 h-8 rounded-full"),
Span("John Doe", cls="ml-2"),
cls="flex items-center gap-2 cursor-pointer"
)
# Account menu content
account_menu = Div(
Div(
Div("John Doe", cls="font-bold"),
Div("john@example.com", cls="text-sm opacity-60"),
cls="p-3 border-b"
),
Ul(
Li(A("Profile", href="/profile", cls="block p-2 hover:bg-base-200")),
Li(A("Settings", href="/settings", cls="block p-2 hover:bg-base-200")),
Li(A("Billing", href="/billing", cls="block p-2 hover:bg-base-200")),
cls="menu p-2"
),
Div(
A("Sign out", href="/logout", cls="block p-2 text-error hover:bg-base-200"),
cls="border-t"
),
cls="w-56"
)
# Align right so it doesn't overflow the viewport
dropdown = Dropdown(
parent=root,
button=user_button,
content=account_menu,
align="right"
)
return dropdown
```
### Example 3: Action Menu Above Button
A dropdown that opens above the trigger, useful when the button is at the bottom of the screen:
```python
from fasthtml.common import *
from myfasthtml.controls.Dropdown import Dropdown
# Action button with icon
action_button = Button(
Span("+", cls="text-xl"),
cls="btn btn-circle btn-primary"
)
# Quick actions menu
actions_menu = Div(
Button("New Document", cls="btn btn-ghost btn-sm w-full justify-start"),
Button("Upload File", cls="btn btn-ghost btn-sm w-full justify-start"),
Button("Create Folder", cls="btn btn-ghost btn-sm w-full justify-start"),
Button("Import Data", cls="btn btn-ghost btn-sm w-full justify-start"),
cls="flex flex-col p-2 w-40"
)
# Open above and center-aligned
dropdown = Dropdown(
parent=root,
button=action_button,
content=actions_menu,
position="above",
align="center"
)
return dropdown
```
### Example 4: Dropdown with Commands
A dropdown containing action buttons that execute commands:
```python
from fasthtml.common import *
from myfasthtml.controls.Dropdown import Dropdown
from myfasthtml.controls.helpers import mk
from myfasthtml.core.commands import Command
# Define actions
def edit_item():
return "Editing..."
def delete_item():
return "Deleted!"
def share_item():
return "Shared!"
# Create commands
edit_cmd = Command("edit", "Edit item", edit_item)
delete_cmd = Command("delete", "Delete item", delete_item)
share_cmd = Command("share", "Share item", share_item)
# Build menu with command buttons
actions_menu = Div(
mk.button("Edit", command=edit_cmd, cls="btn btn-ghost btn-sm w-full justify-start"),
mk.button("Share", command=share_cmd, cls="btn btn-ghost btn-sm w-full justify-start"),
Hr(cls="my-1"),
mk.button("Delete", command=delete_cmd, cls="btn btn-ghost btn-sm w-full justify-start text-error"),
cls="flex flex-col p-2"
)
dropdown = Dropdown(
parent=root,
button=Button("Actions", cls="btn btn-sm"),
content=actions_menu
)
return dropdown
```
---
## Developer Reference
This section contains technical details for developers working on the Dropdown component itself.
### State
The Dropdown component maintains its state via `DropdownState`:
| Name | Type | Description | Default |
|----------|---------|------------------------------|---------|
| `opened` | boolean | Whether dropdown is open | `False` |
### Commands
Available commands for programmatic control:
| Name | Description |
|-----------|-------------------------------------------------|
| `close()` | Closes the dropdown |
| `click()` | Handles click events (toggle or close behavior) |
**Command details:**
- `close()`: Sets `opened` to `False` and returns updated content
- `click()`: Receives `combination`, `is_inside`, and `is_button` parameters
- If `is_button` is `True`: toggles the dropdown
- If `is_inside` is `False`: closes the dropdown
### Public Methods
| Method | Description | Returns |
|------------|----------------------------|----------------------|
| `toggle()` | Toggles open/closed state | Content tuple |
| `close()` | Closes the dropdown | Content tuple |
| `render()` | Renders complete component | `Div` |
### Constructor Parameters
| Parameter | Type | Description | Default |
|------------|-------------|------------------------------------|-----------|
| `parent` | Instance | Parent instance (required) | - |
| `content` | Any | Content to display in dropdown | `None` |
| `button` | Any | Trigger element | `None` |
| `_id` | str | Custom ID for the instance | `None` |
| `position` | str | Vertical position: "below"/"above" | `"below"` |
| `align` | str | Horizontal align: "left"/"right"/"center" | `"left"` |
### High Level Hierarchical Structure
```
Div(id="{id}")
├── Div(cls="mf-dropdown-wrapper")
│ ├── Div(cls="mf-dropdown-btn")
│ │ └── [Button content]
│ └── Div(id="{id}-content", cls="mf-dropdown mf-dropdown-{position} mf-dropdown-{align} [is-visible]")
│ └── [Dropdown content]
├── Keyboard(id="{id}-keyboard")
│ └── ESC → close command
└── Mouse(id="{id}-mouse")
└── click → click command
```
### Element IDs
| Name | Description |
|------------------|--------------------------------|
| `{id}` | Root dropdown container |
| `{id}-content` | Dropdown content panel |
| `{id}-keyboard` | Keyboard handler component |
| `{id}-mouse` | Mouse handler component |
**Note:** `{id}` is the Dropdown instance ID (auto-generated or custom `_id`).
### Internal Methods
| Method | Description |
|-----------------|------------------------------------------|
| `_mk_content()` | Renders the dropdown content panel |
| `on_click()` | Handles click events from Mouse component |
**Method details:**
- `_mk_content()`:
- Builds CSS classes based on `position` and `align`
- Adds `is-visible` class when `opened` is `True`
- Returns a tuple containing the content `Div`
- `on_click(combination, is_inside, is_button)`:
- Called by Mouse component on click events
- `is_button`: `True` if click was on the button
- `is_inside`: `True` if click was inside the dropdown
- Returns updated content for HTMX swap

View File

@@ -26,21 +26,18 @@
width: 16px; width: 16px;
min-width: 16px; min-width: 16px;
height: 16px; height: 16px;
margin-top: auto;
} }
.mf-icon-20 { .mf-icon-20 {
width: 20px; width: 20px;
min-width: 20px; min-width: 20px;
height: 20px; height: 20px;
margin-top: auto;
} }
.mf-icon-24 { .mf-icon-24 {
width: 24px; width: 24px;
min-width: 24px; min-width: 24px;
height: 24px; height: 24px;
margin-top: auto;
} }
@@ -48,14 +45,12 @@
width: 28px; width: 28px;
min-width: 28px; min-width: 28px;
height: 28px; height: 28px;
margin-top: auto;
} }
.mf-icon-32 { .mf-icon-32 {
width: 32px; width: 32px;
min-width: 32px; min-width: 32px;
height: 32px; height: 32px;
margin-top: auto;
} }
/* /*
@@ -470,7 +465,6 @@
.mf-dropdown-wrapper { .mf-dropdown-wrapper {
position: relative; /* CRUCIAL for the anchor */ position: relative; /* CRUCIAL for the anchor */
display: inline-block;
} }
@@ -478,15 +472,19 @@
display: none; display: none;
position: absolute; position: absolute;
top: 100%; top: 100%;
left: 0px; left: 0;
z-index: 1; z-index: 50;
width: 200px; min-width: 200px;
border: 1px solid black; padding: 0.5rem;
padding: 10px;
box-sizing: border-box; box-sizing: border-box;
overflow-x: auto; overflow-x: auto;
/*opacity: 0;*/
/*transition: opacity 0.2s ease-in-out;*/ /* DaisyUI styling */
background-color: var(--color-base-100);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
box-shadow: 0 4px 6px -1px color-mix(in oklab, var(--color-neutral) 20%, #0000),
0 2px 4px -2px color-mix(in oklab, var(--color-neutral) 20%, #0000);
} }
.mf-dropdown.is-visible { .mf-dropdown.is-visible {
@@ -494,6 +492,36 @@
opacity: 1; opacity: 1;
} }
/* Dropdown vertical positioning */
.mf-dropdown-below {
top: 100%;
bottom: auto;
}
.mf-dropdown-above {
bottom: 100%;
top: auto;
}
/* Dropdown horizontal alignment */
.mf-dropdown-left {
left: 0;
right: auto;
transform: none;
}
.mf-dropdown-right {
right: 0;
left: auto;
transform: none;
}
.mf-dropdown-center {
left: 50%;
right: auto;
transform: translateX(-50%);
}
/* *********************************************** */ /* *********************************************** */
/* ************** TreeView Component ************* */ /* ************** TreeView Component ************* */
/* *********************************************** */ /* *********************************************** */

View File

@@ -410,6 +410,18 @@ function getCellId(event) {
return {cell_id: null}; return {cell_id: null};
} }
/**
* Check if the click was on a dropdown button element.
* Used with hx-vals="js:getDropdownExtra()" for Dropdown toggle behavior.
*
* @param {MouseEvent} event - The mouse event
* @returns {Object} Object with is_button boolean property
*/
function getDropdownExtra(event) {
const button = event.target.closest('.mf-dropdown-btn');
return {is_button: button !== null};
}
/** /**
* Shared utility function for triggering HTMX actions from keyboard/mouse bindings. * Shared utility function for triggering HTMX actions from keyboard/mouse bindings.
* Handles dynamic hx-vals with "js:functionName()" syntax. * Handles dynamic hx-vals with "js:functionName()" syntax.
@@ -1498,7 +1510,6 @@ function initDataGridMouseOver(gridId) {
} }
const wrapper = document.getElementById(`tw_${gridId}`); const wrapper = document.getElementById(`tw_${gridId}`);
const selectionModeDiv = document.getElementById(`tsm_${gridId}`);
// Track hover state // Track hover state
let currentHoverRow = null; let currentHoverRow = null;
@@ -1512,6 +1523,7 @@ function initDataGridMouseOver(gridId) {
const cell = e.target.closest('.dt2-cell'); const cell = e.target.closest('.dt2-cell');
if (!cell) return; if (!cell) return;
const selectionModeDiv = document.getElementById(`tsm_${gridId}`);
const selectionMode = selectionModeDiv?.getAttribute('selection-mode'); const selectionMode = selectionModeDiv?.getAttribute('selection-mode');
if (selectionMode === 'row') { if (selectionMode === 'row') {

View File

@@ -0,0 +1,52 @@
from fasthtml.components import Div
from myfasthtml.controls.BaseCommands import BaseCommands
from myfasthtml.controls.helpers import mk
from myfasthtml.core.commands import Command
from myfasthtml.core.dbmanager import DbObject
from myfasthtml.core.instances import MultipleInstance
class CycleState(DbObject):
def __init__(self, owner, save_state):
with self.initializing():
super().__init__(owner, save_state=save_state)
self.state = None
class Commands(BaseCommands):
def cycle_state(self):
return Command("CycleState",
"Cycle state",
self._owner,
self._owner.cycle_state).htmx(target=f"#{self._id}")
class CycleStateControl(MultipleInstance):
def __init__(self, parent, controls: dict, _id=None, save_state=True):
super().__init__(parent, _id)
self._state = CycleState(self, save_state)
self.controls_by_states = controls
self.commands = Commands(self)
# init the state if required
if self._state.state is None and controls:
self._state.state = next(iter(controls.keys()))
def cycle_state(self):
keys = list(self.controls_by_states.keys())
current_idx = keys.index(self._state.state)
self._state.state = keys[(current_idx + 1) % len(keys)]
return self
def get_state(self):
return self._state.state
def render(self):
return mk.mk(
Div(self.controls_by_states[self._state.state], id=self._id),
command=self.commands.cycle_state()
)
def __ft__(self):
return self.render()

View File

@@ -10,6 +10,8 @@ from fasthtml.components import *
from pandas import DataFrame from pandas import DataFrame
from myfasthtml.controls.BaseCommands import BaseCommands from myfasthtml.controls.BaseCommands import BaseCommands
from myfasthtml.controls.CycleStateControl import CycleStateControl
from myfasthtml.controls.DataGridColumnsManager import DataGridColumnsManager
from myfasthtml.controls.DataGridQuery import DataGridQuery, DG_QUERY_FILTER from myfasthtml.controls.DataGridQuery import DataGridQuery, DG_QUERY_FILTER
from myfasthtml.controls.Mouse import Mouse from myfasthtml.controls.Mouse import Mouse
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridRowState, \ from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridRowState, \
@@ -21,6 +23,7 @@ from myfasthtml.core.dbmanager import DbObject
from myfasthtml.core.instances import MultipleInstance from myfasthtml.core.instances import MultipleInstance
from myfasthtml.core.optimized_ft import OptimizedDiv from myfasthtml.core.optimized_ft import OptimizedDiv
from myfasthtml.core.utils import make_safe_id from myfasthtml.core.utils import make_safe_id
from myfasthtml.icons.carbon import row, column, grid
from myfasthtml.icons.fluent import checkbox_unchecked16_regular from myfasthtml.icons.fluent import checkbox_unchecked16_regular
from myfasthtml.icons.fluent_p2 import checkbox_checked16_regular from myfasthtml.icons.fluent_p2 import checkbox_checked16_regular
@@ -112,6 +115,13 @@ class Commands(BaseCommands):
self._owner.filter self._owner.filter
) )
def change_selection_mode(self):
return Command("ChangeSelectionMode",
"Change selection mode",
self._owner,
self._owner.change_selection_mode
)
def on_click(self): def on_click(self):
return Command("OnClick", return Command("OnClick",
"Click on the table", "Click on the table",
@@ -127,14 +137,33 @@ class DataGrid(MultipleInstance):
self._state = DatagridState(self, save_state=self._settings.save_state) self._state = DatagridState(self, save_state=self._settings.save_state)
self.commands = Commands(self) self.commands = Commands(self)
self.init_from_dataframe(self._state.ne_df, init_state=False) # state comes from DatagridState self.init_from_dataframe(self._state.ne_df, init_state=False) # state comes from DatagridState
# add DataGridQuery
self._datagrid_filter = DataGridQuery(self) self._datagrid_filter = DataGridQuery(self)
self._datagrid_filter.bind_command("QueryChanged", self.commands.filter()) self._datagrid_filter.bind_command("QueryChanged", self.commands.filter())
self._datagrid_filter.bind_command("CancelQuery", self.commands.filter()) self._datagrid_filter.bind_command("CancelQuery", self.commands.filter())
self._datagrid_filter.bind_command("ChangeFilterType", self.commands.filter()) self._datagrid_filter.bind_command("ChangeFilterType", self.commands.filter())
# update the filter
self._state.filtered[FILTER_INPUT_CID] = self._datagrid_filter.get_query() self._state.filtered[FILTER_INPUT_CID] = self._datagrid_filter.get_query()
# add Selection Selector
selection_types = {
"row": mk.icon(row, tooltip="Row selection"),
"column": mk.icon(column, tooltip="Column selection"),
"cell": mk.icon(grid, tooltip="Cell selection")
}
self._selection_mode_selector = CycleStateControl(self, controls=selection_types, save_state=False)
self._selection_mode_selector.bind_command("CycleState", self.commands.change_selection_mode())
# add columns manager
self._columns_manager = DataGridColumnsManager(self)
# other definitions
self._mouse_support = {
"click": {"command": self.commands.on_click(), "hx_vals": "js:getCellId()"},
"ctrl+click": {"command": self.commands.on_click(), "hx_vals": "js:getCellId()"},
"shift+click": {"command": self.commands.on_click(), "hx_vals": "js:getCellId()"},
}
@property @property
def _df(self): def _df(self):
return self._state.ne_df return self._state.ne_df
@@ -161,7 +190,8 @@ class DataGrid(MultipleInstance):
return None return None
for col_id, values in self._state.filtered.items(): for col_id, values in self._state.filtered.items():
if col_id == FILTER_INPUT_CID and values is not None: if col_id == FILTER_INPUT_CID:
if values is not None:
if self._datagrid_filter.get_query_type() == DG_QUERY_FILTER: if self._datagrid_filter.get_query_type() == DG_QUERY_FILTER:
visible_columns = [c.col_id for c in self._state.columns if c.visible and c.col_id in df.columns] visible_columns = [c.col_id for c in self._state.columns if c.visible and c.col_id in df.columns]
df = df[df[visible_columns].map(lambda x: values.lower() in str(x).lower()).any(axis=1)] df = df[df[visible_columns].map(lambda x: values.lower() in str(x).lower()).any(axis=1)]
@@ -321,6 +351,14 @@ class DataGrid(MultipleInstance):
return self.render_partial() return self.render_partial()
def change_selection_mode(self):
logger.debug(f"change_selection_mode")
new_state = self._selection_mode_selector.get_state()
logger.debug(f" {new_state=}")
self._state.selection.selection_mode = new_state
self._state.save()
return self.render_partial()
def mk_headers(self): def mk_headers(self):
resize_cmd = self.commands.set_column_width() resize_cmd = self.commands.set_column_width()
move_cmd = self.commands.move_column() move_cmd = self.commands.move_column()
@@ -528,14 +566,13 @@ class DataGrid(MultipleInstance):
selected = [] selected = []
if self._state.selection.selected: if self._state.selection.selected:
#selected.append(("cell", self._get_element_id_from_pos("cell", self._state.selection.selected))) # selected.append(("cell", self._get_element_id_from_pos("cell", self._state.selection.selected)))
selected.append(("focus", self._get_element_id_from_pos("cell", self._state.selection.selected))) selected.append(("focus", self._get_element_id_from_pos("cell", self._state.selection.selected)))
return Div( return Div(
*[Div(selection_type=s_type, element_id=f"{elt_id}") for s_type, elt_id in selected], *[Div(selection_type=s_type, element_id=f"{elt_id}") for s_type, elt_id in selected],
id=f"tsm_{self._id}", id=f"tsm_{self._id}",
#selection_mode=f"{self._state.selection.selection_mode}", selection_mode=f"{self._state.selection.selection_mode}",
selection_mode=f"column",
**extra_attr, **extra_attr,
) )
@@ -605,17 +642,16 @@ class DataGrid(MultipleInstance):
if self._state.ne_df is None: if self._state.ne_df is None:
return Div("No data to display !") return Div("No data to display !")
mouse_support = {
"click": {"command": self.commands.on_click(), "hx_vals": "js:getCellId()"},
"ctrl+click": {"command": self.commands.on_click(), "hx_vals": "js:getCellId()"},
"shift+click": {"command": self.commands.on_click(), "hx_vals": "js:getCellId()"},
}
return Div( return Div(
Div(self._datagrid_filter, cls="mb-2"), Div(self._datagrid_filter,
Div(
self._selection_mode_selector,
self._columns_manager,
cls="flex"),
cls="flex items-center justify-between mb-2"),
self.mk_table(), self.mk_table(),
Script(f"initDataGrid('{self._id}');"), Script(f"initDataGrid('{self._id}');"),
Mouse(self, combinations=mouse_support), Mouse(self, combinations=self._mouse_support),
id=self._id, id=self._id,
cls="grid", cls="grid",
style="height: 100%; grid-template-rows: auto 1fr;" style="height: 100%; grid-template-rows: auto 1fr;"

View File

@@ -0,0 +1,15 @@
from fasthtml.components import Div
from myfasthtml.controls.Dropdown import Dropdown
from myfasthtml.controls.helpers import mk
from myfasthtml.icons.fluent_p1 import settings16_regular
class DataGridColumnsManager(Dropdown):
def __init__(self, parent, _id=None):
super().__init__(parent, _id=_id, align="right")
self.button = mk.icon(settings16_regular)
self.content = Div("DataGridColumnsManager")
def columns(self):
return self._parent._state.columns

View File

@@ -57,7 +57,7 @@ class Commands(BaseCommands):
class DataGridQuery(MultipleInstance): class DataGridQuery(MultipleInstance):
def __init__(self, parent, _id=None): def __init__(self, parent, _id=None):
super().__init__(parent, _id=_id or "-query") super().__init__(parent, _id=_id)
self.commands = Commands(self) self.commands = Commands(self)
self._state = DataGridFilterState(self) self._state = DataGridFilterState(self)

View File

@@ -29,18 +29,43 @@ class DropdownState:
class Dropdown(MultipleInstance): class Dropdown(MultipleInstance):
""" """
Represents a dropdown component that can be toggled open or closed. This class is used Interactive dropdown component that toggles open/closed on button click.
to create interactive dropdown elements, allowing for container and button customization.
The dropdown provides functionality to manage its state, including opening, closing, and Provides automatic close behavior when clicking outside or pressing ESC.
handling user interactions. Supports configurable positioning relative to the trigger button.
Args:
parent: Parent instance (required).
content: Content to display in the dropdown panel.
button: Trigger element that toggles the dropdown.
_id: Custom ID for the instance.
position: Vertical position relative to button.
- "below" (default): Dropdown appears below the button.
- "above": Dropdown appears above the button.
align: Horizontal alignment relative to button.
- "left" (default): Aligns to the left edge of the button.
- "right": Aligns to the right edge of the button.
- "center": Centers relative to the button.
Example:
dropdown = Dropdown(
parent=root,
button=Button("Menu"),
content=Ul(Li("Option 1"), Li("Option 2")),
position="below",
align="right"
)
""" """
def __init__(self, parent, content=None, button=None, _id=None): def __init__(self, parent, content=None, button=None, _id=None,
position="below", align="left"):
super().__init__(parent, _id=_id) super().__init__(parent, _id=_id)
self.button = Div(button) if not isinstance(button, FT) else button self.button = Div(button) if not isinstance(button, FT) else button
self.content = content self.content = content
self.commands = Commands(self) self.commands = Commands(self)
self._state = DropdownState() self._state = DropdownState()
self._position = position
self._align = align
def toggle(self): def toggle(self):
self._state.opened = not self._state.opened self._state.opened = not self._state.opened
@@ -50,57 +75,32 @@ class Dropdown(MultipleInstance):
self._state.opened = False self._state.opened = False
return self._mk_content() return self._mk_content()
def on_click(self, combination, is_inside: bool): def on_click(self, combination, is_inside: bool, is_button: bool = False):
if combination == "click": if combination == "click":
if is_button:
self._state.opened = not self._state.opened
else:
self._state.opened = is_inside self._state.opened = is_inside
return self._mk_content() return self._mk_content()
def _mk_content(self): def _mk_content(self):
position_cls = f"mf-dropdown-{self._position}"
align_cls = f"mf-dropdown-{self._align}"
return Div(self.content, return Div(self.content,
cls=f"mf-dropdown {'is-visible' if self._state.opened else ''}", cls=f"mf-dropdown {position_cls} {align_cls} {'is-visible' if self._state.opened else ''}",
id=f"{self._id}-content"), id=f"{self._id}-content"),
def render(self): def render(self):
return Div( return Div(
Div( Div(
Div(self.button) if self.button else Div("None"), Div(self.button if self.button else "None", cls="mf-dropdown-btn"),
self._mk_content(), self._mk_content(),
cls="mf-dropdown-wrapper" cls="mf-dropdown-wrapper"
), ),
Keyboard(self, _id="-keyboard").add("esc", self.commands.close()), Keyboard(self, _id="-keyboard").add("esc", self.commands.close()),
Mouse(self, "-mouse").add("click", self.commands.click()), Mouse(self, "-mouse").add("click", self.commands.click(), hx_vals="js:getDropdownExtra()"),
id=self._id id=self._id
) )
def __ft__(self): def __ft__(self):
return self.render() return self.render()
# document.addEventListener('htmx:afterSwap', function(event) {
# const targetElement = event.detail.target; // L'élément qui a été mis à jour (#popup-unique-id)
#
# // Vérifie si c'est bien notre popup
# if (targetElement.classList.contains('mf-popup-container')) {
#
# // Trouver l'élément déclencheur HTMX (le bouton existant)
# // HTMX stocke l'élément déclencheur dans event.detail.elt
# const trigger = document.querySelector('#mon-bouton-existant');
#
# if (trigger) {
# // Obtenir les coordonnées de l'élément déclencheur par rapport à la fenêtre
# const rect = trigger.getBoundingClientRect();
#
# // L'élément du popup à positionner
# const popup = targetElement;
#
# // Appliquer la position au conteneur du popup
# // On utilise window.scrollY pour s'assurer que la position est absolue par rapport au document,
# // et non seulement à la fenêtre (car le popup est en position: absolute, pas fixed)
#
# // Top: Juste en dessous de l'élément déclencheur
# popup.style.top = (rect.bottom + window.scrollY) + 'px';
#
# // Left: Aligner avec le côté gauche de l'élément déclencheur
# popup.style.left = (rect.left + window.scrollX) + 'px';
# }
# }
# });

View File

@@ -78,6 +78,7 @@ class mk:
merged_cls = merge_classes(f"mf-icon-{size}", merged_cls = merge_classes(f"mf-icon-{size}",
'icon-btn' if can_select else '', 'icon-btn' if can_select else '',
'mmt-btn' if can_hover else '', 'mmt-btn' if can_hover else '',
'flex items-center justify-center',
cls, cls,
kwargs) kwargs)