Coverage for src/lexigram/admin/settings/panel/layout.py: 30%

37 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-24 23:18 +0800

1"""Configuration Center layout component. 

2 

3This is a nested layout that renders inside the admin shell's #main-content. 

4It provides a secondary sidebar for configuration categories while the main 

5admin chrome (topbar, primary sidebar) remains intact. 

6""" 

7 

8from __future__ import annotations 

9 

10from typing import TYPE_CHECKING, Any 

11 

12from lexigram.ui import Card, Component, el 

13 

14if TYPE_CHECKING: 

15 from lexigram.admin.settings.panel.types import ConfigCategory 

16 

17__all__ = ["ConfigLayout"] 

18 

19 

20class ConfigLayout(Component): 

21 """Two-column layout for Configuration Center. 

22 

23 Renders a secondary sidebar with category groups and a main content area 

24 for the active configuration form. Designed to be placed inside the admin 

25 shell's main content area. 

26 

27 Example: 

28 ```python 

29 layout = ConfigLayout( 

30 categories=categories, 

31 active_category="env", 

32 active_namespace="system.core", 

33 content=form_component, 

34 ) 

35 return await self.render_admin(request, layout, title="Configuration") 

36 ``` 

37 """ 

38 

39 def __init__( 

40 self, 

41 categories: list[ConfigCategory], 

42 active_category: str | None = None, 

43 active_namespace: str | None = None, 

44 content: Any = None, 

45 title: str = "Configuration", 

46 **props, 

47 ) -> None: 

48 super().__init__(**props) 

49 self.categories = categories 

50 self.active_category = active_category 

51 self.active_namespace = active_namespace 

52 self.content = content 

53 self.title = title 

54 

55 def render(self) -> Any: 

56 """Render the full two-column configuration layout.""" 

57 return el( 

58 "div", 

59 self._render_header(), 

60 el( 

61 "div", 

62 self._render_sidebar(), 

63 self._render_main(), 

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

65 ), 

66 ) 

67 

68 def _render_header(self) -> Any: 

69 """Render page header with title and description.""" 

70 return el( 

71 "div", 

72 el( 

73 "h1", 

74 self.title, 

75 class_="text-2xl font-bold text-foreground", 

76 ), 

77 el( 

78 "p", 

79 "Manage system configuration, environment variables, and application settings.", 

80 class_="text-muted-foreground mt-1", 

81 ), 

82 class_="mb-2", 

83 ) 

84 

85 def _render_sidebar(self) -> Any: 

86 """Render the category navigation sidebar.""" 

87 groups = [] 

88 

89 for category in sorted(self.categories, key=lambda c: c.order): 

90 is_active_category = category.name == self.active_category 

91 

92 # Category header 

93 header = el( 

94 "div", 

95 self._render_icon(category.icon), 

96 el("span", category.label, class_="font-medium"), 

97 class_=f"flex items-center gap-2 px-3 py-2 text-sm {'text-primary-700 dark:text-primary-400' if is_active_category else 'text-muted-foreground dark:text-muted-foreground'}", 

98 ) 

99 

100 # Spec links within category 

101 spec_links = [] 

102 for spec in category.specs: 

103 is_active_spec = spec.namespace == self.active_namespace 

104 

105 spec_links.append( 

106 el( 

107 "a", 

108 el("span", spec.label or spec.namespace, class_="truncate"), 

109 href=f"/admin/settings/{spec.namespace}", 

110 hx_get=f"/admin/settings/{spec.namespace}", 

111 hx_target="#main-content", 

112 hx_swap="innerHTML", 

113 hx_push_url="true", 

114 class_=f"block px-3 py-2 pl-9 text-sm rounded-lg transition-colors {'bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-400 font-medium' if is_active_spec else 'text-muted-foreground hover:bg-muted dark:text-muted-foreground dark:hover:bg-card'}", 

115 ), 

116 ) 

117 

118 # Category group 

119 groups.append( 

120 el( 

121 "div", 

122 header, 

123 el("div", *spec_links, class_="space-y-1") 

124 if spec_links 

125 else el( 

126 "div", 

127 "No configurations", 

128 class_="px-3 py-2 pl-9 text-xs text-muted-foreground italic", 

129 ), 

130 class_="mb-4", 

131 ), 

132 ) 

133 

134 return el( 

135 "nav", 

136 *groups, 

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

138 aria_label="Configuration categories", 

139 ) 

140 

141 def _render_main(self) -> Any: 

142 """Render the main content area.""" 

143 if self.content: 

144 return el( 

145 "div", 

146 el("div", self.content, class_="p-6"), 

147 class_="flex-1 min-w-0", 

148 ) 

149 

150 # Empty state 

151 return Card( 

152 children=[ 

153 el( 

154 "div", 

155 el("div", "⚙️", class_="text-5xl mb-4"), 

156 el( 

157 "h3", 

158 "Select a Configuration", 

159 class_="text-lg font-semibold text-foreground", 

160 ), 

161 el( 

162 "p", 

163 "Choose a configuration from the sidebar to view and edit settings.", 

164 class_="text-muted-foreground mt-2 max-w-sm", 

165 ), 

166 class_="text-center py-16", 

167 ), 

168 ], 

169 class_="flex-1", 

170 ) 

171 

172 def _render_icon(self, icon_name: str) -> Any: 

173 """Render an icon by name.""" 

174 try: 

175 from lexigram.ui import get_icon 

176 

177 return get_icon(icon_name, class_name="w-5 h-5 opacity-70") 

178 except (ImportError, ModuleNotFoundError, AttributeError): 

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