Coverage for src / lexigram / admin / ui / molecules / layout_switcher.py: 23%
30 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-11 17:07 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-11 17:07 +0800
1from __future__ import annotations
3from typing import TYPE_CHECKING, Any
5from lexigram.admin.ui.htmx_attrs import HTMXAttrs
6from lexigram.ui import Component, Zones, el
8if TYPE_CHECKING:
9 from lexigram.ui.state import TableState
12class LayoutSwitcher(Component):
13 """Simple layout switcher (stack | sidebar) for DataTable.
15 Renders compact options and uses HTMX to request the table fragment
16 with the chosen layout_type query param.
17 """
19 def __init__(
20 self,
21 current: str = "stack",
22 resource_prefix: str | None = None,
23 state: TableState | None = None,
24 **props: Any,
25 ) -> None:
26 super().__init__(**props)
27 self.current = current or "stack"
28 self.resource_prefix = resource_prefix or ""
29 self.state = state
31 def render(self) -> Any:
32 from lexigram.ui import ActionButton
34 # Helper to generate button props
35 def get_props(type_name: str, icon_name: str, label_text: str) -> Any:
36 is_active = self.current == type_name
38 # Base props
39 props = {
40 "label": "",
41 "icon": icon_name,
42 "variant": "ghost",
43 "size": "sm",
44 "title": label_text,
45 "aria_label": label_text,
46 "aria-pressed": "true" if is_active else "false",
47 }
49 # Active state: visual indicator + disable interaction
50 if is_active:
51 # Add background color manually via class since 'variant' might not cover this specific "active toggle" look
52 # We want it to look "pressed" or "selected"
53 props["class_"] = (
54 "bg-muted text-primary-600 dark:text-primary-400 cursor-default pointer-events-none"
55 )
56 # Remove HTMX attributes to disable behavior
57 else:
58 # Generate HTMX attrs using the new factory pattern
59 if self.state:
60 updated_state = self.state.with_layout(type_name) # type: ignore[arg-type]
61 htmx_attrs = HTMXAttrs.for_full_refresh(
62 updated_state,
63 self.resource_prefix,
64 push_url=True,
65 )
66 # Convert hx-* to hx_* for element builder
67 for k, v in htmx_attrs.items():
68 props[k.replace("-", "_")] = v
69 else:
70 # Fallback if no state provided
71 props.update(
72 {
73 "hx_get": f"{self.resource_prefix.rstrip('/')}/?layout_type={type_name}",
74 "hx_target": Zones.TABLE.selector,
75 "hx_swap": "outerHTML",
76 "hx_params": "none",
77 "hx_push_url": "true",
78 },
79 )
81 props.update(
82 {
83 "hx_on": f"click:console.log('layout:{type_name}')",
84 "class_": "text-muted-foreground hover:text-foreground hover:bg-muted dark:text-muted-foreground dark:hover:text-foreground dark:hover:bg-card",
85 },
86 )
88 return props
90 stack_props = get_props("stack", "panel-top", "Stack view")
91 sidebar_props = get_props("sidebar", "panel-left", "Sidebar view")
93 stack_btn = ActionButton(**stack_props)
94 sidebar_btn = ActionButton(**sidebar_props)
96 return el(
97 "div",
98 el(
99 "div",
100 stack_btn.render(),
101 sidebar_btn.render(),
102 class_="inline-flex items-center gap-1 bg-card rounded-md p-1 border border-border",
103 ),
104 # Hidden on small screens, visible on md+ (desktop only)
105 class_="layout-switcher hidden md:inline-flex items-center gap-2 text-sm",
106 )