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

1from __future__ import annotations 

2 

3from typing import TYPE_CHECKING, Any 

4 

5from lexigram.admin.ui.htmx_attrs import HTMXAttrs 

6from lexigram.ui import Component, Zones, el 

7 

8if TYPE_CHECKING: 

9 from lexigram.ui.state import TableState 

10 

11 

12class LayoutSwitcher(Component): 

13 """Simple layout switcher (stack | sidebar) for DataTable. 

14 

15 Renders compact options and uses HTMX to request the table fragment 

16 with the chosen layout_type query param. 

17 """ 

18 

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 

30 

31 def render(self) -> Any: 

32 from lexigram.ui import ActionButton 

33 

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 

37 

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 } 

48 

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 ) 

80 

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 ) 

87 

88 return props 

89 

90 stack_props = get_props("stack", "panel-top", "Stack view") 

91 sidebar_props = get_props("sidebar", "panel-left", "Sidebar view") 

92 

93 stack_btn = ActionButton(**stack_props) 

94 sidebar_btn = ActionButton(**sidebar_props) 

95 

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 )