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
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-13 22:14 +0800
1"""Configuration Center layout component.
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"""
8from __future__ import annotations
10from typing import TYPE_CHECKING, Any
12from lexigram.ui import Card, Component, el
14if TYPE_CHECKING:
15 from lexigram.admin.settings.panel.types import ConfigCategory
17__all__ = ["ConfigLayout"]
20class ConfigLayout(Component):
21 """Two-column layout for Configuration Center.
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.
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 """
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
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 )
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 )
86 def _render_sidebar(self) -> Any:
87 """Render the category navigation sidebar."""
88 groups = []
90 for category in sorted(self.categories, key=lambda c: c.order):
91 is_active_category = category.name == self.active_category
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 )
101 # Spec links within category
102 spec_links = []
103 for spec in category.specs:
104 is_active_spec = spec.namespace == self.active_namespace
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 )
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 )
135 return el(
136 "nav",
137 *groups,
138 class_="w-full lg:w-64 flex-shrink-0",
139 aria_label="Configuration categories",
140 )
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 )
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 )
173 def _render_icon(self, icon_name: str) -> Any:
174 """Render an icon by name."""
175 try:
176 from lexigram.ui import get_icon
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")