Coverage for src/lexigram/admin/ui/organisms/secondary_nav.py: 0%
35 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"""Secondary navigation column for cluster centers (e.g. Infrastructure).
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"""
8from __future__ import annotations
10from typing import Any
12from lexigram.ui import Component, el
15class ClusterLayout(Component):
16 """Two-column content layout for cluster centers.
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 """
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
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 )
42class SecondaryNav(Component):
43 """Secondary sidebar column shown inside a cluster center.
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 """
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
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 )
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 )
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 ]
108 nodes: list[Any] = [header]
109 if children:
110 nodes.append(el("div", *children, class_="space-y-1"))
112 return el("div", *nodes, class_="mb-4")
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
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")
128__all__ = ["ClusterLayout", "SecondaryNav"]