From ce5328fe3444e0a64242e07aff7a74739a08258f Mon Sep 17 00:00:00 2001 From: Kodjo Sossouvi Date: Wed, 26 Nov 2025 20:53:12 +0100 Subject: [PATCH] Added first controls --- .gitignore | 1 + CLAUDE.md | 425 + Makefile | 1 + README.md | 28 +- docs/Keyboard Support.md | 345 + docs/Mouse Support.md | 439 + pyproject.toml | 2 + requirements.txt | 11 +- src/app.py | 81 + src/logging.yaml | 54 + src/myfasthtml/assets/myfasthtml.css | 467 +- src/myfasthtml/assets/myfasthtml.js | 1354 + src/myfasthtml/assets/vis-network.min.js | 34 + src/myfasthtml/auth/README.md | 2 +- src/myfasthtml/auth/routes.py | 13 +- src/myfasthtml/auth/utils.py | 50 +- src/myfasthtml/controls/BaseCommands.py | 5 + src/myfasthtml/controls/Boundaries.py | 62 + src/myfasthtml/controls/CommandsDebugger.py | 39 + src/myfasthtml/controls/Dropdown.py | 94 + src/myfasthtml/controls/FileUpload.py | 98 + src/myfasthtml/controls/InstancesDebugger.py | 34 + src/myfasthtml/controls/Keyboard.py | 23 + src/myfasthtml/controls/Layout.py | 326 + src/myfasthtml/controls/Mouse.py | 23 + src/myfasthtml/controls/Search.py | 90 + src/myfasthtml/controls/TabsManager.py | 404 + src/myfasthtml/controls/UserProfile.py | 83 + src/myfasthtml/controls/VisNetwork.py | 100 + src/myfasthtml/controls/helpers.py | 46 +- src/myfasthtml/core/AuthProxy.py | 17 + src/myfasthtml/core/bindings.py | 12 +- src/myfasthtml/core/commands.py | 77 +- src/myfasthtml/core/dbmanager.py | 118 + src/myfasthtml/core/instances.py | 218 + src/myfasthtml/core/matching_utils.py | 85 + src/myfasthtml/core/network_utils.py | 238 + src/myfasthtml/core/utils.py | 120 +- src/myfasthtml/icons/Readme.md | 14 +- src/myfasthtml/icons/antd.py | 2460 +- src/myfasthtml/icons/carbon.py | 10797 +----- src/myfasthtml/icons/fa.py | 3218 +- src/myfasthtml/icons/fluent.py | 33583 ++--------------- src/myfasthtml/icons/fluent_p1.py | 2712 ++ src/myfasthtml/icons/fluent_p2.py | 2705 ++ src/myfasthtml/icons/fluent_p3.py | 1986 + src/myfasthtml/icons/ionicons4.py | 9341 +---- src/myfasthtml/icons/ionicons5.py | 6619 +--- src/myfasthtml/icons/manage_icons.py | 154 + src/myfasthtml/icons/material.py | 22985 ++--------- src/myfasthtml/icons/material_p1.py | 3892 ++ src/myfasthtml/icons/material_p2.py | 2748 ++ src/myfasthtml/icons/tabler.py | 12380 +----- src/myfasthtml/myfastapp.py | 34 +- src/myfasthtml/test/matcher.py | 64 + tests/controls/conftest.py | 23 + tests/controls/test_tabsmanager.py | 126 + tests/core/test_commands.py | 223 +- tests/core/test_db_object.py | 264 + tests/core/test_instances.py | 399 + tests/core/test_matching_utils.py | 105 + tests/core/test_network_utils.py | 648 + tests/html/keyboard_support.js | 450 + tests/html/mouse_support.js | 634 + tests/html/test_keyboard_support.html | 309 + tests/html/test_mouse_support.html | 356 + tests/testclient/test_finds.py | 42 + tests/testclient/test_matches.py | 7 +- 68 files changed, 37849 insertions(+), 87048 deletions(-) create mode 100644 CLAUDE.md create mode 100644 docs/Keyboard Support.md create mode 100644 docs/Mouse Support.md create mode 100644 src/app.py create mode 100644 src/logging.yaml create mode 100644 src/myfasthtml/assets/myfasthtml.js create mode 100644 src/myfasthtml/assets/vis-network.min.js create mode 100644 src/myfasthtml/controls/BaseCommands.py create mode 100644 src/myfasthtml/controls/Boundaries.py create mode 100644 src/myfasthtml/controls/CommandsDebugger.py create mode 100644 src/myfasthtml/controls/Dropdown.py create mode 100644 src/myfasthtml/controls/FileUpload.py create mode 100644 src/myfasthtml/controls/InstancesDebugger.py create mode 100644 src/myfasthtml/controls/Keyboard.py create mode 100644 src/myfasthtml/controls/Layout.py create mode 100644 src/myfasthtml/controls/Mouse.py create mode 100644 src/myfasthtml/controls/Search.py create mode 100644 src/myfasthtml/controls/TabsManager.py create mode 100644 src/myfasthtml/controls/UserProfile.py create mode 100644 src/myfasthtml/controls/VisNetwork.py create mode 100644 src/myfasthtml/core/AuthProxy.py create mode 100644 src/myfasthtml/core/dbmanager.py create mode 100644 src/myfasthtml/core/instances.py create mode 100644 src/myfasthtml/core/matching_utils.py create mode 100644 src/myfasthtml/core/network_utils.py create mode 100644 src/myfasthtml/icons/fluent_p1.py create mode 100644 src/myfasthtml/icons/fluent_p2.py create mode 100644 src/myfasthtml/icons/fluent_p3.py create mode 100644 src/myfasthtml/icons/manage_icons.py create mode 100644 src/myfasthtml/icons/material_p1.py create mode 100644 src/myfasthtml/icons/material_p2.py create mode 100644 tests/controls/conftest.py create mode 100644 tests/controls/test_tabsmanager.py create mode 100644 tests/core/test_db_object.py create mode 100644 tests/core/test_instances.py create mode 100644 tests/core/test_matching_utils.py create mode 100644 tests/core/test_network_utils.py create mode 100644 tests/html/keyboard_support.js create mode 100644 tests/html/mouse_support.js create mode 100644 tests/html/test_keyboard_support.html create mode 100644 tests/html/test_mouse_support.html create mode 100644 tests/testclient/test_finds.py diff --git a/.gitignore b/.gitignore index 2cfd859..5e0e0fc 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ app.egg-info *.pyc .mypy_cache .coverage +.myFastHtmlDb htmlcov .cache .venv diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c0e94ba --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,425 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +MyFastHtml is a Python utility library that simplifies FastHTML application development by providing: +- Command management system for client-server interactions +- Bidirectional data binding system +- Predefined authentication pages and routes +- Interactive control helpers +- Session-based instance management + +**Tech Stack**: Python 3.12+, FastHTML, HTMX, DaisyUI 5, Tailwind CSS 4 + +## Development Workflow and Guidelines + +### Development Process + +**Code must always be testable**. Before writing any code: + +1. **Explain available options first** - Present different approaches to solve the problem +2. **Wait for validation** - Ensure mutual understanding of requirements before implementation +3. **No code without approval** - Only proceed after explicit validation + +### Collaboration Style + +**Ask questions to clarify understanding or suggest alternative approaches:** +- Ask questions **one at a time** +- Wait for complete answer before asking the next question +- Indicate progress: "Question 1/5" if multiple questions are needed +- Never assume - always clarify ambiguities + +### Communication + +**Conversations**: French or English +**Code, documentation, comments**: English only + +### Code Standards + +**Follow PEP 8** conventions strictly: +- Variable and function names: `snake_case` +- Explicit, descriptive naming +- **No emojis in code** + +**Documentation**: +- Use Google or NumPy docstring format +- Document all public functions and classes +- Include type hints where applicable + +### Dependency Management + +**When introducing new dependencies:** +- List all external dependencies explicitly +- Propose alternatives using Python standard library when possible +- Explain why each dependency is needed + +### Unit Testing with pytest + +**Test naming patterns:** +- Passing tests: `test_i_can_xxx` - Tests that should succeed +- Failing tests: `test_i_cannot_xxx` - Edge cases that should raise errors/exceptions + +**Test structure:** +- Use **functions**, not classes (unless inheritance is required) +- Before writing tests, **list all planned tests with explanations** +- Wait for validation before implementing tests + +**Example:** +```python +def test_i_can_create_command_with_valid_name(): + """Test that a command can be created with a valid name.""" + cmd = Command("valid_name", "description", lambda: None) + assert cmd.name == "valid_name" + +def test_i_cannot_create_command_with_empty_name(): + """Test that creating a command with empty name raises ValueError.""" + with pytest.raises(ValueError): + Command("", "description", lambda: None) +``` + +### File Management + +**Always specify the full file path** when adding or modifying files: +``` +✅ Modifying: src/myfasthtml/core/commands.py +✅ Creating: tests/core/test_new_feature.py +``` + +### Error Handling + +**When errors occur:** +1. **Explain the problem clearly first** +2. **Do not propose a fix immediately** +3. **Wait for validation** that the diagnosis is correct +4. Only then propose solutions + +## Development Commands + +### Testing +```bash +# Run all tests +pytest + +# Run specific test file +pytest tests/core/test_bindings.py + +# Run specific test +pytest tests/core/test_bindings.py::test_function_name + +# Run tests with verbose output +pytest -v +``` + +### Cleaning +```bash +# Clean build artifacts and cache +make clean + +# Clean package distribution files +make clean-package + +# Clean test artifacts (.sesskey, test databases) +make clean-tests + +# Clean everything including source artifacts +make clean-all +``` + +### Package Building +```bash +# Build distribution +python -m build + +# Install in development mode +pip install -e . +``` + +## Architecture Overview + +### Core System: Commands + +Commands abstract HTMX interactions by encapsulating server-side actions. Located in `src/myfasthtml/core/commands.py`. + +**Key classes:** +- `BaseCommand`: Base class for all commands with HTMX integration +- `Command`: Standard command that executes a Python callable +- `LambdaCommand`: Inline command for simple operations +- `CommandsManager`: Global registry for command execution + +**How commands work:** +1. Create command with action: `cmd = Command("name", "description", callable)` +2. Command auto-registers with `CommandsManager` +3. `cmd.get_htmx_params()` generates HTMX attributes (`hx-post`, `hx-vals`) +4. HTMX posts to `/myfasthtml/commands` route with `c_id` +5. `CommandsManager` routes to correct command's `execute()` method + +**Command customization:** +```python +# Change HTMX target and swap +cmd.htmx(target="#result", swap="innerHTML") + +# Bind to observable data (disables swap by default) +cmd.bind(data_object) +``` + +### Core System: Bindings + +Bidirectional data binding system connects UI components with Python data objects. Located in `src/myfasthtml/core/bindings.py`. + +**Key concepts:** +- **Observable objects**: Use `make_observable()` from myutils to enable change detection +- **Three-phase lifecycle**: Create → Activate (bind_ft) → Deactivate +- **Detection modes**: How changes are detected (ValueChange, AttributePresence, SelectValueChange) +- **Update modes**: How UI updates (ValueChange, AttributePresence, SelectValueChange) +- **Data converters**: Transform data between UI and Python representations + +**Binding flow:** +1. User changes input → HTMX posts to `/myfasthtml/bindings` +2. `Binding.update()` receives form data, updates observable object +3. Observable triggers change event → `Binding.notify()` +4. All bound UI elements update via HTMX swap-oob + +**Helper usage:** +```python +from myfasthtml.controls.helpers import mk + +# Bind input and label to same data +input_elt = Input(name="field") +label_elt = Label() + +mk.manage_binding(input_elt, Binding(data, "attr")) +mk.manage_binding(label_elt, Binding(data, "attr")) +``` + +**Important binding notes:** +- Elements MUST have a `name` attribute to trigger updates +- Multiple elements can bind to same data attribute +- First binding call uses `init_binding=True` to set initial value +- Bindings route through `/myfasthtml/bindings` endpoint + +### Core System: Instances + +Session-scoped instance management system. Located in `src/myfasthtml/core/instances.py`. + +**Key classes:** +- `BaseInstance`: Base for all managed instances +- `SingleInstance`: One instance per parent per session +- `UniqueInstance`: One instance ever per session (singleton-like) +- `RootInstance`: Top-level singleton for application +- `InstancesManager`: Global registry with session-based isolation + +**Instance creation pattern:** +```python +# __new__ checks registry before creating +# If instance exists with same (session_id, _id), returns existing +instance = MyInstance(parent, session, _id="optional") +``` + +**Automatic ID generation:** +- SingleInstance: `snake_case_class_name` +- UniqueInstance: `snake_case_class_name` +- Regular BaseInstance: `parent_prefix-uuid` + +### Application Setup + +**Main entry point**: `create_app()` in `src/myfasthtml/myfastapp.py` + +```python +from myfasthtml.myfastapp import create_app + +app, rt = create_app( + daisyui=True, # Include DaisyUI CSS + vis=True, # Include vis-network.js + protect_routes=True, # Enable auth beforeware + mount_auth_app=False, # Mount auth routes + base_url=None # Base URL for auth +) +``` + +**What create_app does:** +1. Adds MyFastHtml CSS/JS assets via custom static route +2. Optionally adds DaisyUI 5 + Tailwind CSS 4 +3. Optionally adds vis-network.js +4. Mounts `/myfasthtml` app for commands and bindings routes +5. Optionally sets up auth routes and beforeware +6. Creates AuthProxy instance if auth enabled + +### Authentication System + +Located in `src/myfasthtml/auth/`. Integrates with FastAPI backend (myauth package). + +**Key components:** +- `auth/utils.py`: JWT helpers, beforeware for route protection +- `auth/routes.py`: Login, register, logout routes +- `auth/pages/`: LoginPage, RegisterPage, WelcomePage components + +**How auth works:** +1. Beforeware checks `access_token` in session before each route +2. Auto-refreshes token if < 5 minutes to expiry +3. Redirects to `/login` if token invalid/missing +4. Protected routes receive `auth` parameter with user info + +**Session structure:** +```python +{ + 'access_token': 'jwt_token', + 'refresh_token': 'refresh_token', + 'user_info': { + 'email': 'user@example.com', + 'username': 'user', + 'roles': ['admin'], + 'id': 'uuid', + 'created_at': 'timestamp', + 'updated_at': 'timestamp' + } +} +``` + +## Project Structure + +``` +src/myfasthtml/ +├── myfastapp.py # Main app factory (create_app) +├── core/ +│ ├── commands.py # Command system +│ ├── bindings.py # Binding system +│ ├── instances.py # Instance management +│ ├── utils.py # Utilities, routes app +│ ├── constants.py # Routes, constants +│ ├── dbmanager.py # Database helpers +│ ├── AuthProxy.py # Auth proxy instance +│ └── network_utils.py # Network utilities +├── controls/ +│ ├── helpers.py # mk class with UI helpers +│ ├── BaseCommands.py # Base command implementations +│ ├── Search.py # Search control +│ └── Keyboard.py # Keyboard shortcuts +├── auth/ +│ ├── utils.py # JWT, beforeware +│ ├── routes.py # Auth routes +│ └── pages/ # Login, Register, Welcome pages +├── icons/ # Icon libraries (fluent, material, etc.) +├── assets/ # CSS/JS files +└── test/ # Test utilities + +tests/ +├── core/ # Core system tests +├── testclient/ # TestClient and TestableElement tests +├── auth/ # Authentication tests +├── controls/ # Control tests +└── html/ # HTML component tests +``` + +## Testing System + +**Custom test client**: `myfasthtml.test.TestClient` extends FastHTML test client + +**Key features:** +- `user.open(path)`: Navigate to route +- `user.find_element(selector)`: Find element by CSS selector +- `user.should_see(text)`: Assert text in response +- Returns `TestableElement` objects with component-specific methods + +**Testable element types:** +- `TestableInput`: `.send(value)` +- `TestableCheckbox`: `.check()`, `.uncheck()`, `.toggle()` +- `TestableTextarea`: `.send(value)`, `.append(text)`, `.clear()` +- `TestableSelect`: `.select(value)`, `.select_by_text(text)`, `.deselect(value)` +- `TestableRange`: `.set(value)`, `.increase()`, `.decrease()` +- `TestableRadio`: `.select()` +- `TestableButton`: `.click()` +- `TestableDatalist`: `.send(value)`, `.select_suggestion(value)` + +**Test pattern:** +```python +def test_component(user, rt): + @rt("/") + def index(): + data = Data("initial") + component = Component(name="field") + label = Label() + + mk.manage_binding(component, Binding(data)) + mk.manage_binding(label, Binding(data)) + + return component, label + + user.open("/") + user.should_see("initial") + + elem = user.find_element("selector") + elem.method("new_value") + user.should_see("new_value") +``` + +## Important Patterns + +### Creating interactive buttons with commands +```python +from myfasthtml.controls.helpers import mk +from myfasthtml.core.commands import Command + +def action(): + return "Result" + +cmd = Command("action", "Description", action) +button = mk.button("Click", command=cmd) +``` + +### Bidirectional binding +```python +from myutils.observable import make_observable +from myfasthtml.core.bindings import Binding +from myfasthtml.controls.helpers import mk + +data = make_observable(Data("value")) +input_elt = Input(name="field") +label_elt = Label() + +mk.manage_binding(input_elt, Binding(data, "value")) +mk.manage_binding(label_elt, Binding(data, "value")) +``` + +### Using helpers (mk class) +```python +# Button with command +mk.button("Text", command=cmd, cls="btn-primary") + +# Icon with command +mk.icon(icon_svg, size=20, command=cmd) + +# Label with icon +mk.label("Text", icon=icon_svg, size="sm") + +# Generic wrapper +mk.mk(element, command=cmd, binding=binding) +``` + +## Common Gotchas + +1. **Bindings require `name` attribute**: Without it, form data won't include the field +2. **Commands auto-register**: Don't manually register with CommandsManager +3. **Instances use __new__ caching**: Same (session, id) returns existing instance +4. **First binding needs init**: Use `init_binding=True` to set initial value +5. **Observable required for bindings**: Use `make_observable()` from myutils +6. **Auth routes need base_url**: Pass `base_url` to `create_app()` for proper auth API calls + +## Dependencies + +**Core:** +- python-fasthtml: Web framework +- myauth: Authentication backend +- mydbengine: Database abstraction +- myutils: Observable pattern, utilities + +**UI:** +- DaisyUI 5: Component library +- Tailwind CSS 4: Styling +- vis-network: Network visualization + +**Development:** +- pytest: Testing framework +- httpx: HTTP client for tests +- python-dotenv: Environment variables diff --git a/Makefile b/Makefile index 68ae0f7..537c16a 100644 --- a/Makefile +++ b/Makefile @@ -25,3 +25,4 @@ clean: clean-build clean-tests clean-all : clean rm -rf src/.sesskey rm -rf src/Users.db + rm -rf src/.myFastHtmlDb diff --git a/README.md b/README.md index 10da9d8..046c816 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ if __name__ == "__main__": ```python from fasthtml import serve -from myfasthtml.controls.helpers import mk_button +from myfasthtml.controls.helpers import mk from myfasthtml.core.commands import Command from myfasthtml.myfastapp import create_app @@ -82,7 +82,7 @@ app, rt = create_app(protect_routes=False) @rt("/") def get_homepage(): - return mk_button("Click Me!", command=hello_command) + return mk.button("Click Me!", command=hello_command) if __name__ == "__main__": @@ -97,11 +97,17 @@ if __name__ == "__main__": ### Bind components ```python +from dataclasses import dataclass + +from myfasthtml.controls.helpers import mk + + @dataclass class Data: value: str = "Hello World" checked: bool = False + # Binds an Input with a label mk.mk(Input(name="input_name"), binding=Binding(data, attr="value").htmx(trigger="input changed")), mk.mk(Label("Text"), binding=Binding(data, attr="value")), @@ -815,6 +821,24 @@ mk.manage_binding(label_elt, Binding(data)) # Input won't trigger updates, but label will still display data ``` +## Authentication + +session +``` +{'access_token': 'xxx', + 'refresh_token': 'yyy', + 'user_info': { + 'email': 'admin@myauth.com', + 'username': 'admin', + 'roles': ['admin'], + 'user_settings': {}, + 'id': 'uuid', + 'created_at': '2025-11-10T15:52:59.006213', + 'updated_at': '2025-11-10T15:52:59.006213' + } +} +``` + ## Contributing We welcome contributions! To get started: diff --git a/docs/Keyboard Support.md b/docs/Keyboard Support.md new file mode 100644 index 0000000..11a5802 --- /dev/null +++ b/docs/Keyboard Support.md @@ -0,0 +1,345 @@ +# Keyboard Support - Test Instructions + +## ⚠️ Breaking Change + +**Version 2.0** uses HTMX configuration objects instead of simple URL strings. The old format is **not supported**. + +**Old format (no longer supported)**: +```javascript +{"A": "/url"} +``` + +**New format (required)**: +```javascript +{"A": {"hx-post": "/url"}} +``` + +## Files + +- `keyboard_support.js` - Main keyboard support library with smart timeout logic +- `test_keyboard_support.html` - Test page to verify functionality + +## Key Features + +### Multiple Simultaneous Triggers + +**IMPORTANT**: If multiple elements listen to the same combination, **ALL** of them will be triggered: + +```javascript +add_keyboard_support('modal', '{"esc": "/close-modal"}'); +add_keyboard_support('editor', '{"esc": "/cancel-edit"}'); +add_keyboard_support('sidebar', '{"esc": "/hide-sidebar"}'); + +// Pressing ESC will trigger all 3 URLs simultaneously +``` + +This is crucial for use cases like the ESC key, which often needs to cancel multiple actions at once (close modal, cancel edit, hide panels, etc.). + +### Smart Timeout Logic (Longest Match) + +The library uses **a single global timeout** based on the sequence state, not on individual elements: + +**Key principle**: If **any element** has a longer sequence possible, **all matching elements wait**. + +Examples: + +**Example 1**: Three elements, same combination +```javascript +add_keyboard_support('elem1', '{"esc": "/url1"}'); +add_keyboard_support('elem2', '{"esc": "/url2"}'); +add_keyboard_support('elem3', '{"esc": "/url3"}'); +// Press ESC → ALL 3 trigger immediately (no longer sequences exist) +``` + +**Example 2**: Mixed - one has longer sequence +```javascript +add_keyboard_support('elem1', '{"A": "/url1"}'); +add_keyboard_support('elem2', '{"A": "/url2"}'); +add_keyboard_support('elem3', '{"A": "/url3", "A B": "/url3b"}'); +// Press A: +// - elem3 has "A B" possible → EVERYONE WAITS 500ms +// - If B arrives: only elem3 triggers with "A B" +// - If timeout expires: elem1, elem2, elem3 ALL trigger with "A" +``` + +**Example 3**: Different combinations +```javascript +add_keyboard_support('elem1', '{"A B": "/url1"}'); +add_keyboard_support('elem2', '{"C D": "/url2"}'); +// Press A: elem1 waits for B, elem2 not affected +// Press C: elem2 waits for D, elem1 not affected +``` + +The timeout is tied to the **sequence being typed**, not to individual elements. + +### Smart Timeout Logic (Longest Match) + +Keyboard shortcuts are **disabled** when typing in input fields: +- `` elements +- `