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
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 23:18 +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 md:flex-row gap-6",
65 ),
66 )
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 )
85 def _render_sidebar(self) -> Any:
86 """Render the category navigation sidebar."""
87 groups = []
89 for category in sorted(self.categories, key=lambda c: c.order):
90 is_active_category = category.name == self.active_category
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 )
100 # Spec links within category
101 spec_links = []
102 for spec in category.specs:
103 is_active_spec = spec.namespace == self.active_namespace
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 )
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 )
134 return el(
135 "nav",
136 *groups,
137 class_="w-full md:w-64 flex-shrink-0",
138 aria_label="Configuration categories",
139 )
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 )
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 )
172 def _render_icon(self, icon_name: str) -> Any:
173 """Render an icon by name."""
174 try:
175 from lexigram.ui import get_icon
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")