updated Panel implementation

This commit is contained in:
2025-12-20 19:21:30 +01:00
parent 81a80a47b6
commit 9f69a6bc5b
5 changed files with 125 additions and 68 deletions

View File

@@ -737,43 +737,63 @@ codebase.
``` ```
Div(id="{id}", cls="mf-panel") Div(id="{id}", cls="mf-panel")
├── Div(id="{id}_panel_left", cls="mf-panel-left [mf-hidden]") ├── Div(id="{id}_pl", cls="mf-panel-left [mf-hidden]")
│ ├── Div(id="{id}_content_left") │ ├── Div (hide icon)
│ ├── Div (hide icon) │ ├── Div(id="{id}_cl")
│ │ └── [Left content] │ │ └── [Left content]
│ └── Div (resizer-left) │ └── Div (resizer-left)
├── Div(cls="mf-panel-main") ├── Div(cls="mf-panel-main")
│ ├── Div(id="{id}_show_left", cls="hidden|mf-panel-show-icon-left") │ ├── Div(id="{id}_show_left", cls="hidden|mf-panel-show-icon-left")
│ ├── [Main content] │ ├── Div(id="{id}_m", cls="mf-panel-main")
│ │ └── [Main content]
│ └── Div(id="{id}_show_right", cls="hidden|mf-panel-show-icon-right") │ └── Div(id="{id}_show_right", cls="hidden|mf-panel-show-icon-right")
├── Div(id="{id}_panel_right", cls="mf-panel-right [mf-hidden]") ├── Div(id="{id}_pr", cls="mf-panel-right [mf-hidden]")
│ ├── Div (resizer-right) │ ├── Div (resizer-right)
── Div(id="{id}_content_right") ── Div (hide icon)
── Div (hide icon) ── Div(id="{id}_cr")
│ └── [Right content] │ └── [Right content]
└── Script # initResizer('{id}') └── Script # initResizer('{id}')
``` ```
**Note:** **Note:**
- Left panel: content then resizer (resizer on right edge) - Left panel: hide icon, then content, then resizer (resizer on right edge)
- Right panel: resizer then content (resizer on left edge) - Right panel: resizer, then hide icon, then content (resizer on left edge)
- Hide icons are positioned at panel root level (not inside content div)
- Main content has an outer wrapper and inner content div with ID
- `[mf-hidden]` class is conditionally applied when panel is hidden - `[mf-hidden]` class is conditionally applied when panel is hidden
### Element IDs ### Element IDs
| Name | Description | | Name | Description |
|----------------------|-------------------------------------| |------------------|-------------------------------------|
| `{id}` | Root panel container | | `{id}` | Root panel container |
| `{id}_panel_left` | Left panel container | | `{id}_pl` | Left panel container |
| `{id}_panel_right` | Right panel container | | `{id}_pr` | Right panel container |
| `{id}_content_left` | Left panel content wrapper | | `{id}_cl` | Left panel content wrapper |
| `{id}_content_right` | Right panel content wrapper | | `{id}_cr` | Right panel content wrapper |
| `{id}_show_left` | Show icon for left panel (in main) | | `{id}_m` | Main content wrapper |
| `{id}_show_right` | Show icon for right panel (in main) | | `{id}_show_left` | Show icon for left panel (in main) |
| `{id}_show_right`| Show icon for right panel (in main) |
**Note:** `{id}` is the Panel instance ID (auto-generated UUID or custom `_id`). **Note:** `{id}` is the Panel instance ID (auto-generated UUID or custom `_id`).
**ID Management:**
The Panel component uses the `PanelIds` helper class to manage element IDs consistently. Access IDs programmatically:
```python
panel = Panel(parent=root_instance)
# Access IDs via get_ids()
panel.get_ids().panel("left") # Returns "{id}_pl"
panel.get_ids().panel("right") # Returns "{id}_pr"
panel.get_ids().left # Returns "{id}_cl"
panel.get_ids().right # Returns "{id}_cr"
panel.get_ids().main # Returns "{id}_m"
panel.get_ids().content("left") # Returns "{id}_cl"
```
### Internal Methods ### Internal Methods
These methods are used internally for rendering: These methods are used internally for rendering:

View File

@@ -439,7 +439,7 @@
flex: 1; flex: 1;
overflow: auto; overflow: auto;
background-color: var(--color-base-100); background-color: var(--color-base-100);
padding: 1rem; padding: 0.5rem;
border-top: 1px solid var(--color-border-primary); border-top: 1px solid var(--color-border-primary);
} }
@@ -673,6 +673,7 @@
height: 100%; height: 100%;
overflow: auto; overflow: auto;
transition: width 0.3s ease, min-width 0.3s ease, max-width 0.3s ease; transition: width 0.3s ease, min-width 0.3s ease, max-width 0.3s ease;
padding-top: 25px;
} }
/* Left panel specific */ /* Left panel specific */
@@ -683,6 +684,7 @@
/* Right panel specific */ /* Right panel specific */
.mf-panel-right { .mf-panel-right {
border-left: 1px solid var(--color-border-primary); border-left: 1px solid var(--color-border-primary);
padding-left: 0.5rem;
} }
.mf-panel-main { .mf-panel-main {
@@ -700,6 +702,7 @@
max-width: 0; max-width: 0;
overflow: hidden; overflow: hidden;
border: none; border: none;
padding: 0;
} }
/* No transition during manual resize - common for both panels */ /* No transition during manual resize - common for both panels */
@@ -712,10 +715,10 @@
.mf-panel-hide-icon, .mf-panel-hide-icon,
.mf-panel-show-icon { .mf-panel-show-icon {
position: absolute; position: absolute;
top: 0.5rem; top: 0;
right: 0;
cursor: pointer; cursor: pointer;
z-index: 10; z-index: 10;
padding: 0.25rem;
border-radius: 0.25rem; border-radius: 0.25rem;
} }
@@ -724,11 +727,6 @@
background-color: var(--color-bg-hover, rgba(0, 0, 0, 0.05)); background-color: var(--color-bg-hover, rgba(0, 0, 0, 0.05));
} }
/* Hide icon always on the right */
.mf-panel-hide-icon {
right: 0.5rem;
}
/* Show icon positioning */ /* Show icon positioning */
.mf-panel-show-icon-left { .mf-panel-show-icon-left {
left: 0.5rem; left: 0.5rem;

View File

@@ -13,7 +13,7 @@ class InstancesDebugger(SingleInstance):
self._command = Command("ShowInstance", self._command = Command("ShowInstance",
"Display selected Instance", "Display selected Instance",
self, self,
self.on_network_event).htmx(target=f"#{self._panel.get_id()}_r") self.on_network_event).htmx(target=f"#{self._panel.get_ids().right}")
def render(self): def render(self):
nodes, edges = self._get_nodes_and_edges() nodes, edges = self._get_nodes_and_edges()

View File

@@ -13,6 +13,31 @@ from myfasthtml.icons.fluent_p1 import more_horizontal20_regular
from myfasthtml.icons.fluent_p2 import subtract20_regular from myfasthtml.icons.fluent_p2 import subtract20_regular
class PanelIds:
def __init__(self, owner):
self._owner = owner
@property
def main(self):
return f"{self._owner.get_id()}_m"
@property
def right(self):
""" Right panel's content"""
return f"{self._owner.get_id()}_cr"
@property
def left(self):
""" Left panel's content"""
return f"{self._owner.get_id()}_cl"
def panel(self, side: Literal["left", "right"]):
return f"{self._owner.get_id()}_pl" if side == "left" else f"{self._owner.get_id()}_pr"
def content(self, side: Literal["left", "right"]):
return self.left if side == "left" else self.right
@dataclass @dataclass
class PanelConf: class PanelConf:
left: bool = True left: bool = True
@@ -35,7 +60,7 @@ class Commands(BaseCommands):
f"Toggle {side} side panel", f"Toggle {side} side panel",
self._owner, self._owner,
self._owner.toggle_side, self._owner.toggle_side,
args=[side, visible]).htmx(target=f"#{self._id}_panel_{side}") args=[side, visible]).htmx(target=f"#{self._owner.get_ids().panel(side)}")
def update_side_width(self, side: Literal["left", "right"]): def update_side_width(self, side: Literal["left", "right"]):
""" """
@@ -51,7 +76,7 @@ class Commands(BaseCommands):
f"Update {side} side panel width", f"Update {side} side panel width",
self._owner, self._owner,
self._owner.update_side_width, self._owner.update_side_width,
args=[side]).htmx(target=f"#{self._id}_panel_{side}") args=[side]).htmx(target=f"#{self._owner.get_ids().panel(side)}")
class Panel(MultipleInstance): class Panel(MultipleInstance):
@@ -72,6 +97,10 @@ class Panel(MultipleInstance):
self._main = None self._main = None
self._right = None self._right = None
self._left = None self._left = None
self._ids = PanelIds(self)
def get_ids(self):
return self._ids
def update_side_width(self, side, width): def update_side_width(self, side, width):
if side == "left": if side == "left":
@@ -95,11 +124,11 @@ class Panel(MultipleInstance):
def set_right(self, right): def set_right(self, right):
self._right = right self._right = right
return Div(self._right, id=f"{self._id}_r") return Div(self._right, id=self._ids.right)
def set_left(self, left): def set_left(self, left):
self._left = left self._left = left
return Div(self._left, id=f"{self._id}_l") return Div(self._left, id=self._ids.left)
def _mk_panel(self, side: Literal["left", "right"]): def _mk_panel(self, side: Literal["left", "right"]):
enabled = self.conf.left if side == "left" else self.conf.right enabled = self.conf.left if side == "left" else self.conf.right
@@ -125,24 +154,34 @@ class Panel(MultipleInstance):
panel_cls = f"mf-panel-{side}" panel_cls = f"mf-panel-{side}"
if not visible: if not visible:
panel_cls += " mf-hidden" panel_cls += " mf-hidden"
# Left panel: content then resizer (resizer on the right) # Left panel: content then resizer (resizer on the right)
# Right panel: resizer then content (resizer on the left) # Right panel: resizer then content (resizer on the left)
if side == "left": if side == "left":
return Div( return Div(
Div(hide_icon, content, id=f"{self._id}_content_{side}"), hide_icon,
Div(content, id=self._ids.content(side)),
resizer, resizer,
cls=panel_cls, cls=panel_cls,
id=f"{self._id}_panel_{side}" id=self._ids.panel(side)
) )
else: else:
return Div( return Div(
resizer, resizer,
Div(hide_icon, content, id=f"{self._id}_content_{side}"), hide_icon,
Div(content, id=self._ids.content(side)),
cls=panel_cls, cls=panel_cls,
id=f"{self._id}_panel_{side}" id=self._ids.panel(side)
) )
def _mk_main(self):
return Div(
self._mk_show_icon("left"),
Div(self._main, id=self._ids.main, cls="mf-panel-main"),
self._mk_show_icon("right"),
cls="mf-panel-main"
),
def _mk_show_icon(self, side: Literal["left", "right"]): def _mk_show_icon(self, side: Literal["left", "right"]):
""" """
Create show icon for a panel side if it's hidden. Create show icon for a panel side if it's hidden.
@@ -170,12 +209,7 @@ class Panel(MultipleInstance):
def render(self): def render(self):
return Div( return Div(
self._mk_panel("left"), self._mk_panel("left"),
Div( self._mk_main(),
self._mk_show_icon("left"),
self._main,
self._mk_show_icon("right"),
cls="mf-panel-main"
),
self._mk_panel("right"), self._mk_panel("right"),
Script(f"initResizer('{self._id}');"), Script(f"initResizer('{self._id}');"),
cls="mf-panel", cls="mf-panel",

View File

@@ -233,13 +233,14 @@ class TestPanelRender:
- id: Required for HTMX targeting during toggle/resize operations - id: Required for HTMX targeting during toggle/resize operations
- cls Contains "mf-panel-left": CSS class for left panel styling - cls Contains "mf-panel-left": CSS class for left panel styling
""" """
left_panel = find_one(panel.render(), Div(id=f"{panel._id}_panel_left")) left_panel = find_one(panel.render(), Div(id=panel.get_ids().panel("left")))
# Step 1: Validate left panel global structure # Step 1: Validate left panel global structure
expected = Div( expected = Div(
Div(id=f"{panel._id}_content_left"), # content div, tested in detail later TestIcon("subtract20_regular"),
Div(id=panel.get_ids().left), # content div, tested in detail later
Div(cls=Contains("mf-resizer-left")), # resizer Div(cls=Contains("mf-resizer-left")), # resizer
id=f"{panel._id}_panel_left", id=panel.get_ids().panel("left"),
cls=Contains("mf-panel-left") cls=Contains("mf-panel-left")
) )
@@ -252,7 +253,7 @@ class TestPanelRender:
- cls Contains "mf-hidden": CSS class required for width animation - cls Contains "mf-hidden": CSS class required for width animation
""" """
panel._state.left_visible = False panel._state.left_visible = False
left_panel = find_one(panel.render(), Div(id=f"{panel._id}_panel_left")) left_panel = find_one(panel.render(), Div(id=panel.get_ids().panel("left")))
expected = Div(cls=Contains("mf-hidden")) expected = Div(cls=Contains("mf-hidden"))
@@ -268,7 +269,7 @@ class TestPanelRender:
rendered = panel.render() rendered = panel.render()
# Verify left panel is not present # Verify left panel is not present
left_panels = find(rendered, Div(id=f"{panel._id}_panel_left")) left_panels = find(rendered, Div(id=panel.get_ids().panel("left")))
assert len(left_panels) == 0, "Left panel should not be present when conf.left=False" assert len(left_panels) == 0, "Left panel should not be present when conf.left=False"
# 3. Right panel # 3. Right panel
@@ -277,17 +278,18 @@ class TestPanelRender:
"""Test that right panel has resizer before content div. """Test that right panel has resizer before content div.
Why these elements matter: Why these elements matter:
- Order (resizer then content): Critical for positioning resizer on the left side - Order (resizer then hide icon then content): Critical for positioning resizer on the left side
- id: Required for HTMX targeting during toggle/resize operations - id: Required for HTMX targeting during toggle/resize operations
- cls Contains "mf-panel-right": CSS class for right panel styling - cls Contains "mf-panel-right": CSS class for right panel styling
""" """
right_panel = find_one(panel.render(), Div(id=f"{panel._id}_panel_right")) right_panel = find_one(panel.render(), Div(id=panel.get_ids().panel("right")))
# Step 1: Validate right panel global structure # Step 1: Validate right panel global structure
expected = Div( expected = Div(
Div(cls=Contains("mf-resizer-right")), # resizer Div(cls=Contains("mf-resizer-right")), # resizer
Div(id=f"{panel._id}_content_right"), # content div, tested in detail later TestIcon("subtract20_regular"), # hide icon
id=f"{panel._id}_panel_right", Div(id=panel.get_ids().right), # content div, tested in detail later
id=panel.get_ids().panel("right"),
cls=Contains("mf-panel-right") cls=Contains("mf-panel-right")
) )
@@ -300,7 +302,7 @@ class TestPanelRender:
- cls Contains "mf-hidden": CSS class required for width animation - cls Contains "mf-hidden": CSS class required for width animation
""" """
panel._state.right_visible = False panel._state.right_visible = False
right_panel = find_one(panel.render(), Div(id=f"{panel._id}_panel_right")) right_panel = find_one(panel.render(), Div(id=panel.get_ids().panel("right")))
expected = Div(cls=Contains("mf-hidden")) expected = Div(cls=Contains("mf-hidden"))
@@ -316,7 +318,7 @@ class TestPanelRender:
rendered = panel.render() rendered = panel.render()
# Verify right panel is not present # Verify right panel is not present
right_panels = find(rendered, Div(id=f"{panel._id}_panel_right")) right_panels = find(rendered, Div(id=panel.get_ids().panel("right")))
assert len(right_panels) == 0, "Right panel should not be present when conf.right=False" assert len(right_panels) == 0, "Right panel should not be present when conf.right=False"
# 4. Resizers # 4. Resizers
@@ -330,7 +332,7 @@ class TestPanelRender:
- cls Contains "mf-resizer": Base CSS class for resizer styling - cls Contains "mf-resizer": Base CSS class for resizer styling
- cls Contains "mf-resizer-left": Left-specific CSS class for positioning - cls Contains "mf-resizer-left": Left-specific CSS class for positioning
""" """
left_panel = find_one(panel.render(), Div(id=f"{panel._id}_panel_left")) left_panel = find_one(panel.render(), Div(id=panel.get_ids().panel("left")))
resizer = find_one(left_panel, Div(cls=Contains("mf-resizer-left"))) resizer = find_one(left_panel, Div(cls=Contains("mf-resizer-left")))
expected = Div( expected = Div(
@@ -351,7 +353,7 @@ class TestPanelRender:
- cls Contains "mf-resizer": Base CSS class for resizer styling - cls Contains "mf-resizer": Base CSS class for resizer styling
- cls Contains "mf-resizer-right": Right-specific CSS class for positioning - cls Contains "mf-resizer-right": Right-specific CSS class for positioning
""" """
right_panel = find_one(panel.render(), Div(id=f"{panel._id}_panel_right")) right_panel = find_one(panel.render(), Div(id=panel.get_ids().panel("right")))
resizer = find_one(right_panel, Div(cls=Contains("mf-resizer-right"))) resizer = find_one(right_panel, Div(cls=Contains("mf-resizer-right")))
expected = Div( expected = Div(
@@ -372,10 +374,10 @@ class TestPanelRender:
- TestIconNotStr("subtract20_regular"): Verify correct icon is used for hiding - TestIconNotStr("subtract20_regular"): Verify correct icon is used for hiding
- cls Contains "mf-panel-hide-icon": CSS class for hide icon positioning - cls Contains "mf-panel-hide-icon": CSS class for hide icon positioning
""" """
left_content = find_one(panel.render(), Div(id=f"{panel._id}_content_left")) left_panel = find_one(panel.render(), Div(id=panel.get_ids().panel("left")))
# Find the hide icon (should be wrapped by mk.icon) # Find the hide icon (should be wrapped by mk.icon)
hide_icons = find(left_content, Div(cls=Contains("mf-panel-hide-icon"))) hide_icons = find(left_panel, Div(cls=Contains("mf-panel-hide-icon")))
assert len(hide_icons) == 1, "Left panel should contain exactly one hide icon" assert len(hide_icons) == 1, "Left panel should contain exactly one hide icon"
# Verify it contains the subtract icon # Verify it contains the subtract icon
@@ -392,10 +394,10 @@ class TestPanelRender:
- TestIconNotStr("subtract20_regular"): Verify correct icon is used for hiding - TestIconNotStr("subtract20_regular"): Verify correct icon is used for hiding
- cls Contains "mf-panel-hide-icon": CSS class for hide icon positioning - cls Contains "mf-panel-hide-icon": CSS class for hide icon positioning
""" """
right_content = find_one(panel.render(), Div(id=f"{panel._id}_content_right")) right_panel = find_one(panel.render(), Div(id=panel.get_ids().panel("right")))
# Find the hide icon (should be wrapped by mk.icon) # Find the hide icon (should be wrapped by mk.icon)
hide_icons = find(right_content, Div(cls=Contains("mf-panel-hide-icon"))) hide_icons = find(right_panel, Div(cls=Contains("mf-panel-hide-icon")))
assert len(hide_icons) == 1, "Right panel should contain exactly one hide icon" assert len(hide_icons) == 1, "Right panel should contain exactly one hide icon"
# Verify it contains the subtract icon # Verify it contains the subtract icon
@@ -412,8 +414,7 @@ class TestPanelRender:
- cls Contains "hidden": Tailwind class to hide icon when panel is visible - cls Contains "hidden": Tailwind class to hide icon when panel is visible
- id: Required for HTMX swap-oob targeting - id: Required for HTMX swap-oob targeting
""" """
main_panel = find_one(panel.render(), Div(cls=Contains("mf-panel-main"))) show_icon = find_one(panel.render(), Div(id=f"{panel._id}_show_left"))
show_icon = find_one(main_panel, Div(id=f"{panel._id}_show_left"))
expected = Div( expected = Div(
cls=Contains("hidden"), cls=Contains("hidden"),
@@ -430,8 +431,7 @@ class TestPanelRender:
- TestIconNotStr("more_horizontal20_regular"): Verify correct icon is used for showing - TestIconNotStr("more_horizontal20_regular"): Verify correct icon is used for showing
""" """
panel._state.left_visible = False panel._state.left_visible = False
main_panel = find_one(panel.render(), Div(cls=Contains("mf-panel-main"))) show_icon = find_one(panel.render(), Div(id=f"{panel._id}_show_left"))
show_icon = find_one(main_panel, Div(id=f"{panel._id}_show_left"))
expected = Div( expected = Div(
TestIconNotStr("more_horizontal20_regular"), TestIconNotStr("more_horizontal20_regular"),
@@ -449,8 +449,7 @@ class TestPanelRender:
- TestIconNotStr("more_horizontal20_regular"): Verify correct icon is used for showing - TestIconNotStr("more_horizontal20_regular"): Verify correct icon is used for showing
""" """
panel._state.right_visible = False panel._state.right_visible = False
main_panel = find_one(panel.render(), Div(cls=Contains("mf-panel-main"))) show_icon = find_one(panel.render(), Div(id=f"{panel._id}_show_right"))
show_icon = find_one(main_panel, Div(id=f"{panel._id}_show_right"))
expected = Div( expected = Div(
TestIconNotStr("more_horizontal20_regular"), TestIconNotStr("more_horizontal20_regular"),
@@ -466,16 +465,22 @@ class TestPanelRender:
"""Test that main panel contains show icons and content in correct order. """Test that main panel contains show icons and content in correct order.
Why these elements matter: Why these elements matter:
- 3 children: show_icon_left + content + show_icon_right - 3 children: show_icon_left + inner main div + show_icon_right
- Order: Show icons must be positioned correctly (left then right) - Order: Show icons must be positioned correctly (left then right)
- cls="mf-panel-main": CSS class for main panel styling - cls="mf-panel-main": CSS class for main panel styling
- Inner div with id: Main content wrapper for HTMX targeting
""" """
main_panel = find_one(panel.render(), Div(cls=Contains("mf-panel-main"))) # Find all Divs with cls="mf-panel-main" (there are 2: outer wrapper and inner content)
main_panels = find(panel.render(), Div(cls=Contains("mf-panel-main")))
assert len(main_panels) == 2, "Should find outer wrapper and inner content div"
# The outer wrapper is the first one (depth-first search)
main_panel = main_panels[0]
# Step 1: Validate main panel structure # Step 1: Validate main panel structure
expected = Div( expected = Div(
Div(id=f"{panel._id}_show_left"), # show icon left Div(id=f"{panel._id}_show_left"), # show icon left
Div("Main content"), # actual content Div(id=panel.get_ids().main), # inner main content wrapper
Div(id=f"{panel._id}_show_right"), # show icon right Div(id=f"{panel._id}_show_right"), # show icon right
cls="mf-panel-main" cls="mf-panel-main"
) )