diff --git a/docs/Panel.md b/docs/Panel.md index 70a5b36..47afbf1 100644 --- a/docs/Panel.md +++ b/docs/Panel.md @@ -737,43 +737,63 @@ codebase. ``` Div(id="{id}", cls="mf-panel") -├── Div(id="{id}_panel_left", cls="mf-panel-left [mf-hidden]") -│ ├── Div(id="{id}_content_left") -│ │ ├── Div (hide icon) +├── Div(id="{id}_pl", cls="mf-panel-left [mf-hidden]") +│ ├── Div (hide icon) +│ ├── Div(id="{id}_cl") │ │ └── [Left content] │ └── Div (resizer-left) ├── Div(cls="mf-panel-main") │ ├── 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}_panel_right", cls="mf-panel-right [mf-hidden]") +├── Div(id="{id}_pr", cls="mf-panel-right [mf-hidden]") │ ├── Div (resizer-right) -│ └── Div(id="{id}_content_right") -│ ├── Div (hide icon) +│ ├── Div (hide icon) +│ └── Div(id="{id}_cr") │ └── [Right content] └── Script # initResizer('{id}') ``` **Note:** -- Left panel: content then resizer (resizer on right edge) -- Right panel: resizer then content (resizer on left edge) +- Left panel: hide icon, then content, then resizer (resizer on right 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 ### Element IDs -| Name | Description | -|----------------------|-------------------------------------| -| `{id}` | Root panel container | -| `{id}_panel_left` | Left panel container | -| `{id}_panel_right` | Right panel container | -| `{id}_content_left` | Left panel content wrapper | -| `{id}_content_right` | Right panel content wrapper | -| `{id}_show_left` | Show icon for left panel (in main) | -| `{id}_show_right` | Show icon for right panel (in main) | +| Name | Description | +|------------------|-------------------------------------| +| `{id}` | Root panel container | +| `{id}_pl` | Left panel container | +| `{id}_pr` | Right panel container | +| `{id}_cl` | Left panel content wrapper | +| `{id}_cr` | Right panel content wrapper | +| `{id}_m` | Main content wrapper | +| `{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`). +**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 These methods are used internally for rendering: diff --git a/src/myfasthtml/assets/myfasthtml.css b/src/myfasthtml/assets/myfasthtml.css index cddbe5b..e0a1a89 100644 --- a/src/myfasthtml/assets/myfasthtml.css +++ b/src/myfasthtml/assets/myfasthtml.css @@ -439,7 +439,7 @@ flex: 1; overflow: auto; background-color: var(--color-base-100); - padding: 1rem; + padding: 0.5rem; border-top: 1px solid var(--color-border-primary); } @@ -673,6 +673,7 @@ height: 100%; overflow: auto; transition: width 0.3s ease, min-width 0.3s ease, max-width 0.3s ease; + padding-top: 25px; } /* Left panel specific */ @@ -683,6 +684,7 @@ /* Right panel specific */ .mf-panel-right { border-left: 1px solid var(--color-border-primary); + padding-left: 0.5rem; } .mf-panel-main { @@ -700,6 +702,7 @@ max-width: 0; overflow: hidden; border: none; + padding: 0; } /* No transition during manual resize - common for both panels */ @@ -712,10 +715,10 @@ .mf-panel-hide-icon, .mf-panel-show-icon { position: absolute; - top: 0.5rem; + top: 0; + right: 0; cursor: pointer; z-index: 10; - padding: 0.25rem; border-radius: 0.25rem; } @@ -724,11 +727,6 @@ 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 */ .mf-panel-show-icon-left { left: 0.5rem; diff --git a/src/myfasthtml/controls/InstancesDebugger.py b/src/myfasthtml/controls/InstancesDebugger.py index 3c9c3b0..26e7904 100644 --- a/src/myfasthtml/controls/InstancesDebugger.py +++ b/src/myfasthtml/controls/InstancesDebugger.py @@ -13,7 +13,7 @@ class InstancesDebugger(SingleInstance): self._command = Command("ShowInstance", "Display selected Instance", 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): nodes, edges = self._get_nodes_and_edges() diff --git a/src/myfasthtml/controls/Panel.py b/src/myfasthtml/controls/Panel.py index ac8afe6..d7242b6 100644 --- a/src/myfasthtml/controls/Panel.py +++ b/src/myfasthtml/controls/Panel.py @@ -13,6 +13,31 @@ from myfasthtml.icons.fluent_p1 import more_horizontal20_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 class PanelConf: left: bool = True @@ -35,7 +60,7 @@ class Commands(BaseCommands): f"Toggle {side} side panel", self._owner, 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"]): """ @@ -51,7 +76,7 @@ class Commands(BaseCommands): f"Update {side} side panel width", self._owner, 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): @@ -72,6 +97,10 @@ class Panel(MultipleInstance): self._main = None self._right = None self._left = None + self._ids = PanelIds(self) + + def get_ids(self): + return self._ids def update_side_width(self, side, width): if side == "left": @@ -95,11 +124,11 @@ class Panel(MultipleInstance): def set_right(self, 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): 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"]): enabled = self.conf.left if side == "left" else self.conf.right @@ -125,24 +154,34 @@ class Panel(MultipleInstance): panel_cls = f"mf-panel-{side}" if not visible: panel_cls += " mf-hidden" - + # Left panel: content then resizer (resizer on the right) # Right panel: resizer then content (resizer on the left) if side == "left": return Div( - Div(hide_icon, content, id=f"{self._id}_content_{side}"), + hide_icon, + Div(content, id=self._ids.content(side)), resizer, cls=panel_cls, - id=f"{self._id}_panel_{side}" + id=self._ids.panel(side) ) else: return Div( resizer, - Div(hide_icon, content, id=f"{self._id}_content_{side}"), + hide_icon, + Div(content, id=self._ids.content(side)), 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"]): """ Create show icon for a panel side if it's hidden. @@ -170,12 +209,7 @@ class Panel(MultipleInstance): def render(self): return Div( self._mk_panel("left"), - Div( - self._mk_show_icon("left"), - self._main, - self._mk_show_icon("right"), - cls="mf-panel-main" - ), + self._mk_main(), self._mk_panel("right"), Script(f"initResizer('{self._id}');"), cls="mf-panel", diff --git a/tests/controls/test_panel.py b/tests/controls/test_panel.py index 81e3d77..4c95872 100644 --- a/tests/controls/test_panel.py +++ b/tests/controls/test_panel.py @@ -233,13 +233,14 @@ class TestPanelRender: - id: Required for HTMX targeting during toggle/resize operations - 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 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 - id=f"{panel._id}_panel_left", + id=panel.get_ids().panel("left"), cls=Contains("mf-panel-left") ) @@ -252,7 +253,7 @@ class TestPanelRender: - cls Contains "mf-hidden": CSS class required for width animation """ 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")) @@ -268,7 +269,7 @@ class TestPanelRender: rendered = panel.render() # 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" # 3. Right panel @@ -277,17 +278,18 @@ class TestPanelRender: """Test that right panel has resizer before content div. 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 - 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 expected = Div( Div(cls=Contains("mf-resizer-right")), # resizer - Div(id=f"{panel._id}_content_right"), # content div, tested in detail later - id=f"{panel._id}_panel_right", + TestIcon("subtract20_regular"), # hide icon + Div(id=panel.get_ids().right), # content div, tested in detail later + id=panel.get_ids().panel("right"), cls=Contains("mf-panel-right") ) @@ -300,7 +302,7 @@ class TestPanelRender: - cls Contains "mf-hidden": CSS class required for width animation """ 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")) @@ -316,7 +318,7 @@ class TestPanelRender: rendered = panel.render() # 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" # 4. Resizers @@ -330,7 +332,7 @@ class TestPanelRender: - cls Contains "mf-resizer": Base CSS class for resizer styling - 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"))) expected = Div( @@ -351,7 +353,7 @@ class TestPanelRender: - cls Contains "mf-resizer": Base CSS class for resizer styling - 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"))) expected = Div( @@ -372,10 +374,10 @@ class TestPanelRender: - TestIconNotStr("subtract20_regular"): Verify correct icon is used for hiding - 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) - 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" # Verify it contains the subtract icon @@ -392,10 +394,10 @@ class TestPanelRender: - TestIconNotStr("subtract20_regular"): Verify correct icon is used for hiding - 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) - 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" # Verify it contains the subtract icon @@ -412,8 +414,7 @@ class TestPanelRender: - cls Contains "hidden": Tailwind class to hide icon when panel is visible - id: Required for HTMX swap-oob targeting """ - main_panel = find_one(panel.render(), Div(cls=Contains("mf-panel-main"))) - show_icon = find_one(main_panel, Div(id=f"{panel._id}_show_left")) + show_icon = find_one(panel.render(), Div(id=f"{panel._id}_show_left")) expected = Div( cls=Contains("hidden"), @@ -430,8 +431,7 @@ class TestPanelRender: - TestIconNotStr("more_horizontal20_regular"): Verify correct icon is used for showing """ panel._state.left_visible = False - main_panel = find_one(panel.render(), Div(cls=Contains("mf-panel-main"))) - show_icon = find_one(main_panel, Div(id=f"{panel._id}_show_left")) + show_icon = find_one(panel.render(), Div(id=f"{panel._id}_show_left")) expected = Div( TestIconNotStr("more_horizontal20_regular"), @@ -449,8 +449,7 @@ class TestPanelRender: - TestIconNotStr("more_horizontal20_regular"): Verify correct icon is used for showing """ panel._state.right_visible = False - main_panel = find_one(panel.render(), Div(cls=Contains("mf-panel-main"))) - show_icon = find_one(main_panel, Div(id=f"{panel._id}_show_right")) + show_icon = find_one(panel.render(), Div(id=f"{panel._id}_show_right")) expected = Div( TestIconNotStr("more_horizontal20_regular"), @@ -466,16 +465,22 @@ class TestPanelRender: """Test that main panel contains show icons and content in correct order. 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) - 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 expected = Div( 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 cls="mf-panel-main" )