diff --git a/.claude/commands/technical-writer.md b/.claude/commands/technical-writer.md index 9d31b37..524c1b1 100644 --- a/.claude/commands/technical-writer.md +++ b/.claude/commands/technical-writer.md @@ -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 - Usage guides and tutorials - Getting started documentation @@ -18,47 +26,316 @@ Focus on creating and improving **user documentation** for the MyFastHtml librar - Code comments - CLAUDE.md (handled by developers) -## Documentation Principles +## Technical Writer Rules (TW) -**Clarity First:** -- Write for developers who are new to MyFastHtml -- Explain the "why" not just the "what" -- Use concrete, runnable examples -- Progressive complexity (simple → advanced) +### TW-1: Standard Documentation Structure -**Structure:** -- Start with the problem being solved -- Show minimal working example -- Explain key concepts -- Provide variations and advanced usage -- Link to related documentation +Every component documentation MUST follow this structure in order: -**Examples Must:** -- Be complete and runnable -- Include necessary imports -- Show expected output when relevant -- Use realistic variable names -- Follow the project's code standards (PEP 8, snake_case, English) +| Section | Purpose | Required | +|---------|---------|----------| +| **Introduction** | What it is, key features, common use cases | Yes | +| **Quick Start** | Minimal working example | Yes | +| **Basic Usage** | Visual structure, creation, configuration | Yes | +| **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) -**Written documentation:** English only +The [Component] component provides [brief description]. It handles [main functionality] out of the box. -## Workflow +**Key features:** -1. **Ask questions** to understand what needs documentation -2. **Propose structure** before writing content -3. **Wait for validation** before proceeding -4. **Write incrementally** - one section at a time -5. **Request feedback** after each section +- Feature 1 +- Feature 2 +- Feature 3 -## 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: -- Use `/developer` to switch to developer mode +Here's a minimal example showing [what it does]: + +\`\`\`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 diff --git a/docs/Dropdown.md b/docs/Dropdown.md new file mode 100644 index 0000000..f4c19f6 --- /dev/null +++ b/docs/Dropdown.md @@ -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 diff --git a/src/myfasthtml/assets/myfasthtml.css b/src/myfasthtml/assets/myfasthtml.css index 5fcfbcd..29eb6ff 100644 --- a/src/myfasthtml/assets/myfasthtml.css +++ b/src/myfasthtml/assets/myfasthtml.css @@ -26,21 +26,18 @@ width: 16px; min-width: 16px; height: 16px; - margin-top: auto; } .mf-icon-20 { width: 20px; min-width: 20px; height: 20px; - margin-top: auto; } .mf-icon-24 { width: 24px; min-width: 24px; height: 24px; - margin-top: auto; } @@ -48,14 +45,12 @@ width: 28px; min-width: 28px; height: 28px; - margin-top: auto; } .mf-icon-32 { width: 32px; min-width: 32px; height: 32px; - margin-top: auto; } /* @@ -470,7 +465,6 @@ .mf-dropdown-wrapper { position: relative; /* CRUCIAL for the anchor */ - display: inline-block; } @@ -478,15 +472,19 @@ display: none; position: absolute; top: 100%; - left: 0px; - z-index: 1; - width: 200px; - border: 1px solid black; - padding: 10px; + left: 0; + z-index: 50; + min-width: 200px; + padding: 0.5rem; box-sizing: border-box; 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 { @@ -494,6 +492,36 @@ 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 ************* */ /* *********************************************** */ diff --git a/src/myfasthtml/assets/myfasthtml.js b/src/myfasthtml/assets/myfasthtml.js index 376544c..c697774 100644 --- a/src/myfasthtml/assets/myfasthtml.js +++ b/src/myfasthtml/assets/myfasthtml.js @@ -410,6 +410,18 @@ function getCellId(event) { 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. * Handles dynamic hx-vals with "js:functionName()" syntax. @@ -1498,7 +1510,6 @@ function initDataGridMouseOver(gridId) { } const wrapper = document.getElementById(`tw_${gridId}`); - const selectionModeDiv = document.getElementById(`tsm_${gridId}`); // Track hover state let currentHoverRow = null; @@ -1512,6 +1523,7 @@ function initDataGridMouseOver(gridId) { const cell = e.target.closest('.dt2-cell'); if (!cell) return; + const selectionModeDiv = document.getElementById(`tsm_${gridId}`); const selectionMode = selectionModeDiv?.getAttribute('selection-mode'); if (selectionMode === 'row') { diff --git a/src/myfasthtml/controls/CycleStateControl.py b/src/myfasthtml/controls/CycleStateControl.py new file mode 100644 index 0000000..ebb8ad9 --- /dev/null +++ b/src/myfasthtml/controls/CycleStateControl.py @@ -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() diff --git a/src/myfasthtml/controls/DataGrid.py b/src/myfasthtml/controls/DataGrid.py index a030f40..759ec04 100644 --- a/src/myfasthtml/controls/DataGrid.py +++ b/src/myfasthtml/controls/DataGrid.py @@ -10,6 +10,8 @@ from fasthtml.components import * from pandas import DataFrame 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.Mouse import Mouse 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.optimized_ft import OptimizedDiv 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_p2 import checkbox_checked16_regular @@ -112,6 +115,13 @@ class Commands(BaseCommands): 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): return Command("OnClick", "Click on the table", @@ -127,13 +137,32 @@ class DataGrid(MultipleInstance): self._state = DatagridState(self, save_state=self._settings.save_state) self.commands = Commands(self) 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.bind_command("QueryChanged", self.commands.filter()) self._datagrid_filter.bind_command("CancelQuery", 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() + + # 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 def _df(self): @@ -322,6 +351,14 @@ class DataGrid(MultipleInstance): 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): resize_cmd = self.commands.set_column_width() move_cmd = self.commands.move_column() @@ -535,8 +572,7 @@ class DataGrid(MultipleInstance): return Div( *[Div(selection_type=s_type, element_id=f"{elt_id}") for s_type, elt_id in selected], id=f"tsm_{self._id}", - # selection_mode=f"{self._state.selection.selection_mode}", - selection_mode=f"column", + selection_mode=f"{self._state.selection.selection_mode}", **extra_attr, ) @@ -606,17 +642,16 @@ class DataGrid(MultipleInstance): if self._state.ne_df is None: 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( - 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(), Script(f"initDataGrid('{self._id}');"), - Mouse(self, combinations=mouse_support), + Mouse(self, combinations=self._mouse_support), id=self._id, cls="grid", style="height: 100%; grid-template-rows: auto 1fr;" diff --git a/src/myfasthtml/controls/DataGridColumnsManager.py b/src/myfasthtml/controls/DataGridColumnsManager.py new file mode 100644 index 0000000..a4266f1 --- /dev/null +++ b/src/myfasthtml/controls/DataGridColumnsManager.py @@ -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 diff --git a/src/myfasthtml/controls/Dropdown.py b/src/myfasthtml/controls/Dropdown.py index 68fdccf..4323e2f 100644 --- a/src/myfasthtml/controls/Dropdown.py +++ b/src/myfasthtml/controls/Dropdown.py @@ -29,18 +29,43 @@ class DropdownState: class Dropdown(MultipleInstance): """ - Represents a dropdown component that can be toggled open or closed. This class is used - to create interactive dropdown elements, allowing for container and button customization. - The dropdown provides functionality to manage its state, including opening, closing, and - handling user interactions. + Interactive dropdown component that toggles open/closed on button click. + + Provides automatic close behavior when clicking outside or pressing ESC. + 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) self.button = Div(button) if not isinstance(button, FT) else button self.content = content self.commands = Commands(self) self._state = DropdownState() + self._position = position + self._align = align def toggle(self): self._state.opened = not self._state.opened @@ -50,57 +75,32 @@ class Dropdown(MultipleInstance): self._state.opened = False 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": - self._state.opened = is_inside + if is_button: + self._state.opened = not self._state.opened + else: + self._state.opened = is_inside return self._mk_content() def _mk_content(self): + position_cls = f"mf-dropdown-{self._position}" + align_cls = f"mf-dropdown-{self._align}" 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"), def render(self): return 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(), cls="mf-dropdown-wrapper" ), 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 ) def __ft__(self): 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'; -# } -# } -# }); diff --git a/src/myfasthtml/controls/helpers.py b/src/myfasthtml/controls/helpers.py index eff9651..c831da6 100644 --- a/src/myfasthtml/controls/helpers.py +++ b/src/myfasthtml/controls/helpers.py @@ -78,6 +78,7 @@ class mk: merged_cls = merge_classes(f"mf-icon-{size}", 'icon-btn' if can_select else '', 'mmt-btn' if can_hover else '', + 'flex items-center justify-center', cls, kwargs)