Compare commits
2 Commits
06e81fe72a
...
0bd56c7f09
| Author | SHA1 | Date | |
|---|---|---|---|
| 0bd56c7f09 | |||
| 3c2c07ebfc |
@@ -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
557
docs/Dropdown.md
Normal 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
|
||||||
@@ -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 ************* */
|
||||||
/* *********************************************** */
|
/* *********************************************** */
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
52
src/myfasthtml/controls/CycleStateControl.py
Normal file
52
src/myfasthtml/controls/CycleStateControl.py
Normal 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()
|
||||||
@@ -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;"
|
||||||
|
|||||||
15
src/myfasthtml/controls/DataGridColumnsManager.py
Normal file
15
src/myfasthtml/controls/DataGridColumnsManager.py
Normal 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
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
# });
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user