Coverage for src / lexigram / admin / ui / organisms / systembox.py: 0%
63 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-11 02:25 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-11 02:25 +0800
1from __future__ import annotations
3from typing import Any
5from lexigram.ui import Component, el
8class SystemBox(Component):
9 """
10 A compact system menu display for the sidebar footer that mirrors the
11 behavior of `UserBox`. It renders compact icons inline and exposes
12 block-style items (e.g., Settings) in a dropdown for discoverability.
13 """
15 def __init__(
16 self,
17 system_menu_items: list[dict] | None = None,
18 direction: str = "up",
19 user: Any | None = None,
20 **props,
21 ) -> None:
22 super().__init__(
23 system_menu_items=system_menu_items,
24 direction=direction,
25 user=user,
26 **props,
27 )
28 self.system_menu_items = system_menu_items or []
29 self.direction = direction
30 self.user = user
32 def render(self) -> Any:
33 block_items = []
34 compact_nodes = []
36 # Old RBACChecker logic removed
38 for m in self.system_menu_items:
39 label = m.get("label") or ""
40 href = m.get("href", "#")
41 icon = m.get("icon")
42 permission = m.get("permission")
44 if permission and self.user:
45 try:
46 # Check permission using AuthenticatedUserProtocol.has_permission
47 has_perm = False
48 if hasattr(self.user, "has_permission"):
49 has_perm = self.user.has_permission(permission)
50 elif hasattr(self.user, "permissions"):
51 # Basic check
52 has_perm = permission in self.user.permissions
54 if not has_perm:
55 # Skip guarded items when user lacks permission
56 continue
57 except Exception as _perm_err: # noqa: BLE001 — user permission objects may raise anything; conservative skip
58 from lexigram.logging import get_logger
60 logger = get_logger(__name__)
61 logger.exception(
62 "Error while evaluating permission '%s' for user", permission
63 )
64 # Conservative: skip if we can't evaluate permission
65 continue
67 # Build content
68 if icon:
69 try:
70 from lexigram.ui import get_icon
72 content = get_icon(icon, class_name="w-5 h-5")
73 except Exception as _icon_err: # noqa: BLE001 — icon rendering is best-effort; fall back to plain text
74 from lexigram.logging import get_logger
76 logger = get_logger(__name__)
77 logger.exception("Failed to render icon '%s'", icon)
78 content = el("span", label)
79 else:
80 content = el(
81 "span",
82 label,
83 class_="text-sm text-foreground",
84 )
86 # Use any attributes supplied on the menu item (e.g., data-test hooks)
87 attrs = dict(m.get("attrs", {}))
89 # Block style (full-width) items are rendered in dropdown or as block anchors
90 if m.get("render") == "block":
91 # Create SidebarItem-like structure for block items
92 first_letter = label[0] if label else ""
94 # Check if icon is available for this item to optionally invoke it?
95 # For now, just use label logic as icons are usually in 'compact_nodes' if intended for existing system box.
96 # But 'Configuration' might have an icon in metadata?
97 # If icon exists, we might want to hide it in mini mode like SidebarItem.
99 # Icon (if present in metadata)
100 icon_node = ""
101 # Logic: If icon is present, show it always (adjust margin). Hide first letter.
102 # If no icon, show first letter in mini mode.
104 has_icon = False
105 if icon:
106 has_icon = True
107 try:
108 from lexigram.ui import get_icon
110 # Icon Container: Show in both modes. Handle margin dynamically.
111 icon_node = el(
112 "div",
113 get_icon(icon, class_name="w-5 h-5"),
114 class_="transition-all duration-200",
115 **{"x-bind:class": "sidebarMini ? 'mr-0' : 'mr-3'"},
116 )
117 except (KeyError, ValueError):
118 pass
120 first_letter_node = ""
121 if not has_icon:
122 first_letter_node = el(
123 "span",
124 first_letter,
125 class_="font-bold text-xs w-6 h-6 flex items-center justify-center shrink-0 rounded-lg bg-muted dark:bg-card text-muted-foreground",
126 x_show="sidebarMini",
127 )
129 block_items.append(
130 el(
131 "a",
132 icon_node,
133 # Full Label
134 el(
135 "span",
136 label,
137 class_="truncate whitespace-nowrap",
138 x_show="!sidebarMini",
139 ),
140 # Mini Label (First Letter)
141 first_letter_node,
142 href=href,
143 class_=(
144 "group flex items-center px-3 py-2 rounded-xl text-sm font-medium transition-colors duration-200 text-muted-foreground dark:text-muted-foreground hover:bg-muted dark:hover:bg-card/50 hover:text-primary-600 dark:hover:text-primary-400"
145 ),
146 **{
147 "x-bind:class": "sidebarMini ? 'justify-center' : ''",
148 # Merge other attrs
149 **attrs,
150 },
151 hx_get=href,
152 hx_target="#main-content",
153 hx_swap="innerHTML",
154 hx_push_url="true",
155 ),
156 )
157 else:
158 compact_nodes.append(
159 el(
160 "a",
161 content,
162 href=href,
163 title=label,
164 class_=(
165 "inline-flex items-center justify-center p-2 rounded-md text-muted-foreground hover:bg-muted dark:text-foreground dark:hover:bg-card"
166 ),
167 **attrs,
168 ),
169 )
171 # Inline compact area
172 compact_bar = (
173 el("div", *compact_nodes, class_="flex items-center space-x-2")
174 if compact_nodes
175 else ""
176 )
178 # Render block items as a stacked list (flat, not popup)
179 block_area = ""
180 if block_items:
181 block_area = el(
182 "div",
183 *block_items,
184 class_="px-3 py-2 space-y-1",
185 )
187 # Place block items above compact icons to emulate stacked sidebar layout
188 return el(
189 "div",
190 block_area,
191 compact_bar,
192 class_="flex flex-col px-3 py-2 border-b border-border",
193 )