Coverage for src / lexigram / ui / atoms / file_upload.py: 14%

44 statements  

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

1""" 

2FileUpload component for file uploads with validation and preview. 

3 

4Supports single file uploads with type/size validation and image previews. 

5""" 

6 

7from __future__ import annotations 

8 

9from typing import Any 

10 

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

12 

13 

14class FileUpload(Component): 

15 """ 

16 File upload component with validation and preview. 

17 

18 Example: 

19 FileUpload( 

20 name="avatar", 

21 label="Profile Picture", 

22 accept="image/*", 

23 max_size=5 * 1024 * 1024, # 5MB 

24 preview=True 

25 ) 

26 """ 

27 

28 def __init__( 

29 self, 

30 name: str, 

31 label: str | None = None, 

32 accept: str | None = None, # e.g., "image/*", ".pdf,.doc" 

33 max_size: int | None = None, # in bytes 

34 preview: bool = False, # Show image preview 

35 required: bool = False, 

36 disabled: bool = False, 

37 error: str | None = None, 

38 help_text: str | None = None, 

39 **props, 

40 ): 

41 """ 

42 Initialize file upload component. 

43 

44 Args: 

45 name: Input name attribute 

46 label: Label text 

47 accept: Accepted file types 

48 max_size: Maximum file size in bytes 

49 preview: Show image preview 

50 required: Whether field is required 

51 disabled: Whether field is disabled 

52 error: Error message 

53 help_text: Help text 

54 **props: Additional properties 

55 """ 

56 super().__init__( 

57 name=name, 

58 label=label, 

59 accept=accept, 

60 max_size=max_size, 

61 preview=preview, 

62 required=required, 

63 disabled=disabled, 

64 error=error, 

65 help_text=help_text, 

66 **props, 

67 ) 

68 self.name = name 

69 self.label = label or name.replace("_", " ").title() 

70 self.accept = accept 

71 self.max_size = max_size 

72 self.preview = preview 

73 self.required = required 

74 self.disabled = disabled 

75 self.error = error 

76 self.help_text = help_text 

77 

78 def render(self) -> Any: 

79 """Render the file upload component.""" 

80 # File input classes 

81 input_classes = f"block w-full text-sm text-foreground border border-input rounded-lg cursor-pointer bg-card focus:outline-none focus:ring-2 focus:ring-ring {'border-destructive' if self.error else ''} disabled:opacity-50 disabled:cursor-not-allowed" 

82 

83 # Build file input 

84 input_attrs = { 

85 "type": "file", 

86 "name": self.name, 

87 "id": self.name, 

88 "class_": input_classes, 

89 "disabled": self.disabled, 

90 "required": self.required, 

91 } 

92 

93 if self.accept: 

94 input_attrs["accept"] = self.accept 

95 

96 # Add data attributes for validation 

97 if self.max_size: 

98 input_attrs["data_max_size"] = str(self.max_size) 

99 

100 # Add aria attributes for accessibility 

101 if self.help_text or self.error: 

102 describedby_ids = [] 

103 if self.help_text: 

104 describedby_ids.append(f"{self.name}-help") 

105 if self.error: 

106 describedby_ids.append(f"{self.name}-error") 

107 input_attrs["aria_describedby"] = " ".join(describedby_ids) 

108 if self.error: 

109 input_attrs["aria_invalid"] = "true" 

110 

111 # Pass through HTMX and other attributes 

112 input_attrs.update( 

113 { 

114 key: value 

115 for key, value in self.props.items() 

116 if key 

117 not in [ 

118 "name", 

119 "label", 

120 "accept", 

121 "max_size", 

122 "preview", 

123 "required", 

124 "disabled", 

125 "error", 

126 "help_text", 

127 ] 

128 } 

129 ) 

130 

131 file_input = el("input", **input_attrs) 

132 

133 # Help text 

134 help_el = "" 

135 if self.help_text: 

136 help_el = el( 

137 "p", 

138 self.help_text, 

139 id=f"{self.name}-help", 

140 class_="mt-1 text-sm text-muted-foreground", 

141 ) 

142 

143 # Error message 

144 error_el = "" 

145 if self.error: 

146 error_el = el( 

147 "p", 

148 self.error, 

149 id=f"{self.name}-error", 

150 class_="mt-2 text-sm text-destructive", 

151 ) 

152 

153 # Preview container (hidden by default, shown via JS) 

154 preview_el = "" 

155 if self.preview: 

156 preview_el = el( 

157 "div", 

158 el("img", src="", alt="Preview", class_="max-w-xs rounded-lg"), 

159 id=f"{self.name}_preview", 

160 class_="mt-4 hidden", 

161 ) 

162 

163 # File info display 

164 file_info_el = el( 

165 "div", 

166 id=f"{self.name}_info", 

167 class_="mt-2 text-sm text-muted-foreground hidden", 

168 ) 

169 

170 return el( 

171 "div", 

172 el( 

173 "label", 

174 self.label, 

175 for_=self.name, 

176 class_="block text-sm font-medium text-foreground mb-2", 

177 ), 

178 file_input, 

179 help_el, 

180 file_info_el, 

181 preview_el, 

182 error_el, 

183 class_="mb-4", 

184 )