Coverage for src / lexigram / ui / atoms / layout.py: 100%

109 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-10 04:11 +0800

1from __future__ import annotations 

2 

3from typing import Any 

4 

5from lexigram.ui.core.base import Component, el 

6 

7 

8class Row(Component): 

9 """Grid row component (flexible layout).""" 

10 

11 def __init__(self, *children, cols: int = 1, gap: int = 4, **props) -> None: 

12 super().__init__(cols=cols, gap=gap, **props) 

13 if children: 

14 self.children = list(children) 

15 self.cols = cols 

16 self.gap = gap 

17 

18 def render(self) -> Any: 

19 from lexigram.ui.core.base import raw, render_to_string 

20 

21 children_html = [ 

22 raw(render_to_string(c)) 

23 if hasattr(c, "__html__") or hasattr(c, "render") 

24 else str(c) 

25 for c in self.children 

26 ] 

27 # Merge layout classes with any custom classes in props 

28 cls = f"grid grid-cols-{self.cols} gap-{self.gap}" 

29 custom_cls = self.props.get("class_", self.props.get("class")) 

30 if custom_cls: 

31 cls = f"{cls} {custom_cls}" 

32 

33 # Create a copy of props to avoid mutating state, and remove layout-specifics 

34 attrs = { 

35 k: v 

36 for k, v in self.props.items() 

37 if k not in ("cols", "gap", "class_", "class") 

38 } 

39 return el("div", *children_html, class_=cls, **attrs) 

40 

41 

42class Col(Component): 

43 """Grid column component (vertical flow).""" 

44 

45 def __init__( 

46 self, 

47 *children, 

48 gap: int = 4, 

49 span: int | None = None, 

50 **props, 

51 ) -> None: 

52 super().__init__(gap=gap, span=span, **props) 

53 if children: 

54 self.children = list(children) 

55 self.gap = gap 

56 self.span = span 

57 

58 def render(self) -> Any: 

59 from lexigram.ui.core.base import raw, render_to_string 

60 

61 children_html = [ 

62 raw(render_to_string(c)) 

63 if hasattr(c, "__html__") or hasattr(c, "render") 

64 else str(c) 

65 for c in self.children 

66 ] 

67 cls = f"flex flex-col gap-{self.gap}" 

68 

69 # Add span class if specified 

70 if self.span: 

71 cls = f"{cls} col-span-{self.span}" 

72 

73 custom_cls = self.props.get("class_", self.props.get("class")) 

74 if custom_cls: 

75 cls = f"{cls} {custom_cls}" 

76 

77 attrs = { 

78 k: v 

79 for k, v in self.props.items() 

80 if k not in ("gap", "span", "class_", "class") 

81 } 

82 return el("div", *children_html, class_=cls, **attrs) 

83 

84 

85class Aside(Component): 

86 """A layout component for asides/sidebars within a page.""" 

87 

88 def __init__( 

89 self, 

90 *children, 

91 position: str = "left", 

92 width: str = "w-64", 

93 **props, 

94 ) -> None: 

95 super().__init__(position=position, width=width, **props) 

96 if children: 

97 self.children = list(children) 

98 self.position = position 

99 self.width = width 

100 

101 def render(self) -> Any: 

102 from lexigram.ui.core.base import raw, render_to_string 

103 

104 children_html = [ 

105 raw(render_to_string(c)) 

106 if hasattr(c, "__html__") or hasattr(c, "render") 

107 else str(c) 

108 for c in self.children 

109 ] 

110 cls = f"flex-shrink-0 {self.width} bg-card border-r border-border h-full overflow-y-auto" 

111 if self.position == "right": 

112 cls = cls.replace("border-r", "border-l") 

113 

114 custom_cls = self.props.get("class_", self.props.get("class")) 

115 if custom_cls: 

116 cls = f"{cls} {custom_cls}" 

117 

118 attrs = { 

119 k: v 

120 for k, v in self.props.items() 

121 if k not in ("position", "width", "class_", "class") 

122 } 

123 return el("aside", *children_html, class_=cls, **attrs) 

124 

125 

126class Grid(Component): 

127 """Generic CSS Grid component.""" 

128 

129 def __init__( 

130 self, 

131 *children, 

132 cols: int | dict[str, int] = 1, 

133 gap: int = 4, 

134 **props, 

135 ) -> None: 

136 super().__init__(cols=cols, gap=gap, **props) 

137 if children: 

138 self.children = list(children) 

139 self.cols = cols 

140 self.gap = gap 

141 

142 def render(self) -> Any: 

143 cls = "grid" 

144 if isinstance(self.cols, int): 

145 cls += f" grid-cols-{self.cols}" 

146 elif isinstance(self.cols, dict): 

147 if "default" in self.cols: 

148 cls += f" grid-cols-{self.cols['default']}" 

149 for break_pt, count in self.cols.items(): 

150 if break_pt != "default": 

151 cls += f" {break_pt}:grid-cols-{count}" 

152 

153 cls += f" gap-{self.gap}" 

154 custom_cls = self.props.get("class_", self.props.get("class")) 

155 if custom_cls: 

156 cls = f"{cls} {custom_cls}" 

157 

158 from lexigram.ui.core.base import raw, render_to_string 

159 

160 children_html = [ 

161 raw(render_to_string(c)) 

162 if hasattr(c, "__html__") or hasattr(c, "render") 

163 else str(c) 

164 for c in self.children 

165 ] 

166 attrs = { 

167 k: v 

168 for k, v in self.props.items() 

169 if k not in ("cols", "gap", "class_", "class") 

170 } 

171 return el("div", *children_html, class_=cls, **attrs) 

172 

173 

174class Stack(Component): 

175 """Vertical stack component (flex column).""" 

176 

177 def __init__(self, *children, gap: int = 4, **props) -> None: 

178 super().__init__(gap=gap, **props) 

179 if children: 

180 self.children = list(children) 

181 self.gap = gap 

182 

183 def render(self) -> Any: 

184 cls = f"flex flex-col gap-{self.gap}" 

185 custom_cls = self.props.get("class_", self.props.get("class")) 

186 if custom_cls: 

187 cls = f"{cls} {custom_cls}" 

188 

189 from lexigram.ui.core.base import raw, render_to_string 

190 

191 children_html = [ 

192 raw(render_to_string(c)) 

193 if hasattr(c, "__html__") or hasattr(c, "render") 

194 else str(c) 

195 for c in self.children 

196 ] 

197 attrs = { 

198 k: v for k, v in self.props.items() if k not in ("gap", "class_", "class") 

199 } 

200 return el("div", *children_html, class_=cls, **attrs) 

201 

202 

203class Container(Component): 

204 def __init__(self, *children, **props) -> None: 

205 super().__init__(**props) 

206 if children: 

207 self.children = list(children) 

208 

209 def render(self) -> Any: 

210 from lexigram.ui.core.base import raw, render_to_string 

211 

212 children_html = [ 

213 raw(render_to_string(c)) 

214 if hasattr(c, "__html__") or hasattr(c, "render") 

215 else str(c) 

216 for c in self.children 

217 ] 

218 cls = "max-w-7xl mx-auto px-4" 

219 custom_cls = self.props.get("class_", self.props.get("class")) 

220 if custom_cls: 

221 cls = f"{cls} {custom_cls}" 

222 

223 attrs = {k: v for k, v in self.props.items() if k not in ("class_", "class")} 

224 return el("div", *children_html, class_=cls, **attrs)