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
« 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.
4Supports single file uploads with type/size validation and image previews.
5"""
7from __future__ import annotations
9from typing import Any
11from lexigram.ui.core.base import Component, el
14class FileUpload(Component):
15 """
16 File upload component with validation and preview.
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 """
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.
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
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"
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 }
93 if self.accept:
94 input_attrs["accept"] = self.accept
96 # Add data attributes for validation
97 if self.max_size:
98 input_attrs["data_max_size"] = str(self.max_size)
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"
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 )
131 file_input = el("input", **input_attrs)
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 )
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 )
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 )
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 )
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 )