Coverage for src / lexigram / admin / ui / organisms / userbox.py: 13%
45 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 UserBox(Component):
9 """
10 A compact user profile display for headers or sidebars.
11 """
13 def __init__(
14 self,
15 username: str,
16 avatar_url: str | None = None,
17 direction: str = "down",
18 position: str = "right",
19 roles: list[str] | None = None,
20 user_menu_items: list[dict] | None = None,
21 user: Any | None = None,
22 **props,
23 ) -> None:
24 super().__init__(
25 username=username,
26 avatar_url=avatar_url,
27 direction=direction,
28 position=position,
29 roles=roles,
30 user_menu_items=user_menu_items,
31 user=user,
32 **props,
33 )
34 self.username = username
35 self.avatar_url = avatar_url
36 self.direction = direction
37 self.position = position
38 self.roles = roles or []
39 self.user_menu_items = user_menu_items or []
40 self.user = user
42 def render(self) -> Any:
43 from lexigram.ui import Dropdown
45 if not self.avatar_url:
46 avatar = el(
47 "div",
48 self.username[0].upper(),
49 class_="w-8 h-8 rounded-full bg-primary-100 text-primary-600 flex items-center justify-center font-bold text-sm dark:bg-primary-900/30 dark:text-primary-400 border border-border",
50 )
51 else:
52 avatar = el(
53 "img",
54 src=self.avatar_url,
55 class_="w-8 h-8 rounded-full border border-border",
56 )
58 name = el(
59 "div",
60 self.username,
61 class_="font-medium text-sm text-foreground truncate flex-1 text-left",
62 x_show="!sidebarMini",
63 )
65 trigger = el(
66 "div",
67 avatar,
68 name,
69 el(
70 "svg",
71 el("path", d="M19 9l-7 7-7-7"),
72 class_="ml-auto h-4 w-4 text-muted-foreground",
73 fill="none",
74 viewBox="0 0 24 24",
75 stroke="currentColor",
76 ),
77 class_="flex items-center space-x-3 p-2 hover:bg-card transition-colors cursor-pointer w-full border border-transparent hover:border-border",
78 **{"x-bind:class": "sidebarMini ? 'justify-center' : ''"},
79 )
81 # Dropdown Items
82 items = []
84 # RBACChecker logic removed
86 for menu_item in self.user_menu_items:
87 required_permission = menu_item.get("permission")
88 if required_permission and self.user:
89 try:
90 # Check permission using AuthenticatedUserProtocol.has_permission
91 has_perm = False
92 if hasattr(self.user, "has_permission"):
93 has_perm = self.user.has_permission(required_permission)
94 elif hasattr(self.user, "permissions"):
95 # Basic check
96 has_perm = required_permission in self.user.permissions
98 if not has_perm:
99 continue
100 except (AttributeError, TypeError):
101 continue
103 # Allow menu items to supply attributes (data-test hooks, etc.)
104 attrs = dict(menu_item.get("attrs", {}))
105 href = menu_item.get("href", "#")
107 # By default render as a block item in dropdown and make it HTMX-enabled
108 # so clicking updates the main content area (consistent with system menu)
109 # Icon rendering
110 icon_node: Any = ""
111 if menu_item.get("icon"):
112 from lexigram.ui import Icon
114 icon_node = Icon(
115 menu_item["icon"],
116 class_name="w-4 h-4 mr-2 text-muted-foreground group-hover:text-muted-foreground dark:text-muted-foreground",
117 )
119 items.append(
120 el(
121 "a",
122 icon_node,
123 menu_item.get("label", ""),
124 href=href,
125 class_="group flex items-center px-4 py-2 text-sm text-foreground hover:bg-muted dark:hover:bg-card rounded-md",
126 hx_get=href,
127 hx_target="#main-content",
128 hx_swap="innerHTML",
129 hx_push_url="true",
130 **attrs,
131 ),
132 )
134 if items:
135 items.append(el("div", class_="h-px my-1 bg-muted"))
137 items.append(
138 el(
139 "a",
140 "Sign out",
141 href="/admin/logout",
142 class_="block px-4 py-2 text-sm text-destructive hover:bg-destructive/10 rounded-md",
143 ),
144 )
146 return Dropdown(
147 trigger=trigger,
148 items=items,
149 direction=self.direction,
150 position=self.position,
151 )