Coverage for src / lexigram / ui / organisms / admin.py: 92%
48 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-10 04:11 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-10 04:11 +0800
1"""Admin UI organisms — AdminCard and PageLayout.
3Components for building admin dashboard pages with ShadCN-compatible styling.
4"""
6from __future__ import annotations
8from typing import Any
10from lexigram.ui.core.base import Component, el, raw, render_to_string
13class AdminCard(Component):
14 """ShadCN-styled card for admin dashboard sections.
16 Args:
17 title: Optional card header text.
18 content: Card body — a string, htpy element, or Component.
19 **props: Additional HTML attributes applied to the outer div.
20 """
22 def __init__(
23 self,
24 title: str | Any | None = None,
25 content: str | Any | None = None,
26 **props: Any,
27 ) -> None:
28 super().__init__(title=title, content=content, **props)
29 self.title = title
30 self.content = content
32 def render(self) -> Any:
33 cls = (
34 "bg-card text-card-foreground border border-border rounded-xl "
35 "shadow-sm overflow-hidden"
36 )
37 custom_cls = self.props.get("class_") or self.props.get("class")
38 if custom_cls:
39 cls = f"{cls} {custom_cls}"
41 attrs = {
42 k: v
43 for k, v in self.props.items()
44 if k not in ("title", "content", "class_", "class")
45 }
47 header_el = None
48 if self.title:
49 header_el = el(
50 "div",
51 str(self.title),
52 class_="px-6 py-4 border-b border-border font-semibold text-card-foreground",
53 )
55 body_content: str
56 if self.content is None:
57 body_content = ""
58 elif hasattr(self.content, "__html__") or hasattr(self.content, "render"):
59 body_content = render_to_string(self.content)
60 else:
61 body_content = str(self.content)
63 body_el = el("div", raw(body_content), class_="px-6 py-4")
65 return el("div", header_el, body_el, class_=cls, **attrs)
68class PageLayout(Component):
69 """Full-page admin layout wrapper with a header bar and content area.
71 Args:
72 title: Page title rendered in the header.
73 children: Main page content — string, htpy element, Component, or list thereof.
74 actions: Optional list of elements (e.g. Buttons) rendered in the header right side.
75 **props: Additional HTML attributes applied to the outer wrapper div.
76 """
78 def __init__(
79 self,
80 title: str = "",
81 children: Any = None,
82 actions: list[Any] | None = None,
83 **props: Any,
84 ) -> None:
85 super().__init__(**props)
86 self.title = title
87 if children is not None:
88 self.children = children if isinstance(children, list) else [children]
89 else:
90 self.children = []
91 self.actions = actions or []
93 def render(self) -> Any:
94 wrapper_cls = "flex flex-col min-h-full"
95 custom_cls = self.props.get("class_") or self.props.get("class")
96 if custom_cls:
97 wrapper_cls = f"{wrapper_cls} {custom_cls}"
99 attrs = {
100 k: v
101 for k, v in self.props.items()
102 if k not in ("title", "children", "actions", "class_", "class")
103 }
105 title_el = el(
106 "h1",
107 self.title,
108 class_="text-2xl font-semibold text-foreground",
109 )
111 actions_el = None
112 if self.actions:
113 rendered_actions = [
114 raw(render_to_string(a))
115 if hasattr(a, "__html__") or hasattr(a, "render")
116 else str(a)
117 for a in self.actions
118 ]
119 actions_el = el(
120 "div",
121 *rendered_actions,
122 class_="flex items-center gap-2",
123 )
125 header_el = el(
126 "div",
127 title_el,
128 actions_el,
129 class_="flex items-center justify-between px-6 py-4 border-b border-border bg-background",
130 )
132 rendered_children = [
133 raw(render_to_string(c))
134 if hasattr(c, "__html__") or hasattr(c, "render")
135 else str(c)
136 for c in self.children
137 ]
138 content_el = el(
139 "div",
140 *rendered_children,
141 class_="flex-1 p-6",
142 )
144 return el("div", header_el, content_el, class_=wrapper_cls, **attrs)
147__all__ = ["AdminCard", "PageLayout"]