Coverage for src/lexigram/admin/ui/organisms/secondary_nav.py: 94%

35 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-21 14:56 +0800

1"""Secondary navigation column for cluster centers (e.g. Infrastructure). 

2 

3Mirrors the settings Configuration Center sidebar design: one group per 

4top-level entry — an icon+label header row with indented child links — 

5without a card container. 

6""" 

7 

8from __future__ import annotations 

9 

10from typing import Any 

11 

12from lexigram.ui import Component, el 

13 

14 

15class ClusterLayout(Component): 

16 """Two-column content layout for cluster centers. 

17 

18 Renders the secondary nav sidebar next to the page content, inside 

19 the content section (settings ConfigLayout pattern). Cluster pages 

20 own their layout so ``#main-content`` stays a plain wrapper. 

21 """ 

22 

23 def __init__( 

24 self, 

25 items: list[dict[str, Any]], 

26 content: Any, 

27 **props: Any, 

28 ) -> None: 

29 super().__init__(items=items, content=content, **props) 

30 self.items = items 

31 self.content = content 

32 

33 def render(self) -> Any: 

34 return el( 

35 "div", 

36 SecondaryNav(items=self.items).render(), 

37 el("div", self.content, class_="flex-1 min-w-0"), 

38 class_="flex flex-col md:flex-row gap-6", 

39 ) 

40 

41 

42class SecondaryNav(Component): 

43 """Secondary sidebar column shown inside a cluster center. 

44 

45 Renders a flat vertical list of linked sections (with optional nested 

46 child links) beside the main content area, styled after the settings 

47 sidebar groups. 

48 """ 

49 

50 def __init__( 

51 self, 

52 items: list[dict[str, Any]], 

53 title: str = "", 

54 **props, 

55 ) -> None: 

56 super().__init__(items=items, title=title, **props) 

57 self.items = items 

58 self.title = title 

59 

60 def render(self) -> Any: 

61 groups = [self._render_group(item) for item in self.items] 

62 return el( 

63 "nav", 

64 *groups, 

65 class_="w-full md:w-64 flex-shrink-0", 

66 aria_label=self.title or "Secondary navigation", 

67 ) 

68 

69 def _render_group(self, item: dict[str, Any]) -> Any: 

70 """Render one group: header link plus indented child links.""" 

71 active = bool(item.get("active")) 

72 header = el( 

73 "a", 

74 self._render_header_icon(item.get("icon")), 

75 el("span", item.get("label", ""), class_="font-medium"), 

76 href=item.get("href", "#"), 

77 class_=( 

78 "flex items-center gap-2 px-3 py-2 text-sm " 

79 + ( 

80 "text-primary-700 dark:text-primary-400" 

81 if active 

82 else "text-muted-foreground dark:text-muted-foreground" 

83 ) 

84 ), 

85 title=item.get("label", ""), 

86 ) 

87 

88 children = [ 

89 el( 

90 "a", 

91 el("span", child.get("label", ""), class_="truncate"), 

92 href=child.get("href", "#"), 

93 class_=( 

94 "block px-3 py-2 pl-9 text-sm rounded-lg transition-colors " 

95 + ( 

96 "bg-primary-50 text-primary-700 dark:bg-primary-900/30 " 

97 "dark:text-primary-400 font-medium" 

98 if child.get("active") 

99 else "text-muted-foreground hover:bg-muted " 

100 "dark:text-muted-foreground dark:hover:bg-card" 

101 ) 

102 ), 

103 title=child.get("label", ""), 

104 ) 

105 for child in item.get("children", []) 

106 ] 

107 

108 nodes: list[Any] = [header] 

109 if children: 

110 nodes.append(el("div", *children, class_="space-y-1")) 

111 

112 return el("div", *nodes, class_="mb-4") 

113 

114 def _render_header_icon(self, icon_name: Any) -> Any: 

115 if not icon_name: 

116 return el("span", class_="w-5 h-5 flex-shrink-0") 

117 try: 

118 from lexigram.ui import get_icon 

119 

120 return get_icon( 

121 icon_name, 

122 class_name="w-5 h-5 opacity-70", 

123 ) 

124 except (ImportError, ModuleNotFoundError, AttributeError): 

125 return el("span", "●", class_="text-muted-foreground") 

126 

127 

128__all__ = ["ClusterLayout", "SecondaryNav"]