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

37 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-13 22:14 +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 lg:flex-row gap-6", 

65 ), 

66 class_="space-y-6", 

67 ) 

68 

69 def _render_header(self) -> Any: 

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

71 return el( 

72 "div", 

73 el( 

74 "h1", 

75 self.title, 

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

77 ), 

78 el( 

79 "p", 

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

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

82 ), 

83 class_="mb-2", 

84 ) 

85 

86 def _render_sidebar(self) -> Any: 

87 """Render the category navigation sidebar.""" 

88 groups = [] 

89 

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

91 is_active_category = category.name == self.active_category 

92 

93 # Category header 

94 header = el( 

95 "div", 

96 self._render_icon(category.icon), 

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

98 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'}", 

99 ) 

100 

101 # Spec links within category 

102 spec_links = [] 

103 for spec in category.specs: 

104 is_active_spec = spec.namespace == self.active_namespace 

105 

106 spec_links.append( 

107 el( 

108 "a", 

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

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

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

112 hx_target="#main-content", 

113 hx_swap="innerHTML", 

114 hx_push_url="true", 

115 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'}", 

116 ), 

117 ) 

118 

119 # Category group 

120 groups.append( 

121 el( 

122 "div", 

123 header, 

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

125 if spec_links 

126 else el( 

127 "div", 

128 "No configurations", 

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

130 ), 

131 class_="mb-4", 

132 ), 

133 ) 

134 

135 return el( 

136 "nav", 

137 *groups, 

138 class_="w-full lg:w-64 flex-shrink-0", 

139 aria_label="Configuration categories", 

140 ) 

141 

142 def _render_main(self) -> Any: 

143 """Render the main content area.""" 

144 if self.content: 

145 return el( 

146 "div", 

147 self.content, 

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

149 ) 

150 

151 # Empty state 

152 return Card( 

153 children=[ 

154 el( 

155 "div", 

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

157 el( 

158 "h3", 

159 "Select a Configuration", 

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

161 ), 

162 el( 

163 "p", 

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

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

166 ), 

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

168 ), 

169 ], 

170 class_="flex-1", 

171 ) 

172 

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

174 """Render an icon by name.""" 

175 try: 

176 from lexigram.ui import get_icon 

177 

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

179 except (ImportError, ModuleNotFoundError, AttributeError): 

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