cronixui 1.1.1 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -8
- package/package.json +71 -71
- package/packages/flutter/.qwen/settings.json +7 -0
- package/packages/flutter/pubspec.yaml +20 -20
- package/packages/go/cronixui/cronixui.go +926 -926
- package/packages/python/README.md +142 -0
- package/packages/python/cronixui/__init__.py +15 -6
- package/packages/python/cronixui/__pycache__/__init__.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/accordion.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/alert.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/avatar.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/badge.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/button.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/card.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/command_palette.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/core.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/dropdown.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/form.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/layout.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/list.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/loading.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/modal.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/nav.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/pagination.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/progress.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/search.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/table.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/tabs.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/toast.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/toggle.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/tokens.cpython-314.pyc +0 -0
- package/packages/python/cronixui/__pycache__/tooltip.cpython-314.pyc +0 -0
- package/packages/python/cronixui/alert.py +119 -36
- package/packages/python/cronixui/avatar.py +129 -22
- package/packages/python/cronixui/badge.py +161 -24
- package/packages/python/cronixui/button.py +96 -27
- package/packages/python/cronixui/card.py +206 -33
- package/packages/python/cronixui/core.py +212 -23
- package/packages/python/cronixui/form.py +552 -141
- package/packages/python/cronixui/layout.py +358 -96
- package/packages/python/cronixui/list.py +140 -37
- package/packages/python/cronixui/loading.py +107 -17
- package/packages/python/cronixui/progress.py +189 -47
- package/packages/python/cronixui/table.py +118 -31
- package/packages/python/cronixui/tooltip.py +117 -15
- package/packages/react/src/components/Accordion.tsx +82 -82
- package/packages/react/src/components/Button.tsx +47 -47
- package/packages/react/src/components/Card.tsx +69 -69
- package/packages/react/src/components/CommandPalette.tsx +131 -131
- package/packages/react/src/components/Dropdown.tsx +88 -88
- package/packages/react/src/components/FileInput.tsx +86 -86
- package/packages/react/src/components/FormGroup.tsx +36 -36
- package/packages/react/src/components/List.tsx +55 -55
- package/packages/react/src/components/Pagination.tsx +107 -107
- package/packages/react/src/components/Progress.tsx +49 -49
- package/packages/react/src/components/Search.tsx +95 -95
- package/packages/react/src/components/Sidebar.tsx +64 -64
- package/packages/react/src/components/Stack.tsx +69 -69
- package/packages/react/src/components/Table.tsx +90 -90
- package/packages/react/src/components/Toast.tsx +134 -134
- package/packages/react/src/components/Typography.tsx +66 -66
- package/packages/react/src/index.ts +40 -40
- package/packages/react/src/styles.css +2039 -2039
- package/packages/rust/cronixui/src/components/avatar.rs +85 -85
- package/packages/rust/cronixui/src/components/breadcrumb.rs +58 -58
- package/packages/rust/cronixui/src/components/card.rs +259 -259
- package/packages/rust/cronixui/src/components/command_palette.rs +254 -254
- package/packages/rust/cronixui/src/components/dropdown.rs +179 -179
- package/packages/rust/cronixui/src/components/file_input.rs +74 -74
- package/packages/rust/cronixui/src/components/mod.rs +51 -51
- package/packages/rust/cronixui/src/components/search.rs +185 -185
- package/packages/rust/cronixui/src/components/skeleton.rs +63 -63
- package/packages/rust/cronixui/src/components/table.rs +56 -56
- package/packages/rust/cronixui/src/lib.rs +128 -128
- package/packages/web/dist/cronixui.css +97 -93
- package/packages/web/dist/cronixui.min.css +1 -1
|
@@ -1,11 +1,63 @@
|
|
|
1
|
-
"""CronixUI Form Components
|
|
1
|
+
"""CronixUI Form Components.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Generates HTML for form inputs, textareas, checkboxes, radios, selects, sliders,
|
|
4
|
+
file inputs, and form field wrappers.
|
|
5
|
+
No browser DOM APIs are used - all output is HTML strings or data structures.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Dict, List, Optional, Tuple, Union
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class FormElement:
|
|
16
|
+
"""Represents a rendered form element."""
|
|
17
|
+
|
|
18
|
+
tag: str = "div"
|
|
19
|
+
classes: List[str] = field(default_factory=list)
|
|
20
|
+
attributes: Dict[str, str] = field(default_factory=dict)
|
|
21
|
+
inner_html: str = ""
|
|
22
|
+
|
|
23
|
+
def render_html(self) -> str:
|
|
24
|
+
"""Render as HTML string.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Complete HTML for the form element
|
|
28
|
+
"""
|
|
29
|
+
class_str = " ".join(self.classes)
|
|
30
|
+
class_attr = f' class="{class_str}"' if class_str else ""
|
|
31
|
+
attrs_str = "".join(f' {k}="{v}"' for k, v in self.attributes.items())
|
|
32
|
+
return f"<{self.tag}{class_attr}{attrs_str}>{self.inner_html}</{self.tag}>"
|
|
33
|
+
|
|
34
|
+
def render(self) -> "FormElement":
|
|
35
|
+
"""Return self for API compatibility."""
|
|
36
|
+
return self
|
|
5
37
|
|
|
6
38
|
|
|
7
39
|
class Input:
|
|
8
|
-
"""Input component.
|
|
40
|
+
"""Input component for text and other input types.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
placeholder: Placeholder text
|
|
44
|
+
size: Input size - sm, md, lg (default: md)
|
|
45
|
+
error: Whether input has an error state (default: False)
|
|
46
|
+
disabled: Whether input is disabled (default: False)
|
|
47
|
+
icon: Optional SVG icon markup
|
|
48
|
+
name: Optional input name attribute
|
|
49
|
+
value: Optional initial value
|
|
50
|
+
input_type: HTML input type (text, email, password, etc.) (default: text)
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
>>> inp = Input(placeholder="Enter your name", size="lg")
|
|
54
|
+
>>> print(inp.render_html())
|
|
55
|
+
<input class="cn-input cn-input-lg" placeholder="Enter your name" />
|
|
56
|
+
|
|
57
|
+
>>> with_icon = Input(placeholder="Search...", icon="<svg>...</svg>")
|
|
58
|
+
>>> print(with_icon.render_html())
|
|
59
|
+
<div class="cn-input-icon-wrapper"><span class="cn-input-icon">...</span><input class="cn-input" placeholder="Search..." /></div>
|
|
60
|
+
"""
|
|
9
61
|
|
|
10
62
|
SIZES = ("sm", "md", "lg")
|
|
11
63
|
|
|
@@ -16,178 +68,433 @@ class Input:
|
|
|
16
68
|
error: bool = False,
|
|
17
69
|
disabled: bool = False,
|
|
18
70
|
icon: Optional[str] = None,
|
|
71
|
+
name: Optional[str] = None,
|
|
72
|
+
value: Optional[str] = None,
|
|
73
|
+
input_type: str = "text",
|
|
19
74
|
):
|
|
75
|
+
if size not in self.SIZES:
|
|
76
|
+
raise ValueError(f"Invalid size '{size}'. Must be one of {self.SIZES}")
|
|
77
|
+
|
|
20
78
|
self.placeholder = placeholder
|
|
21
|
-
self.size = size
|
|
79
|
+
self.size = size
|
|
22
80
|
self.error = error
|
|
23
81
|
self.disabled = disabled
|
|
24
82
|
self.icon = icon
|
|
25
|
-
self.
|
|
83
|
+
self.name = name
|
|
84
|
+
self.value = value
|
|
85
|
+
self.input_type = input_type
|
|
26
86
|
|
|
27
|
-
def
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if self.error:
|
|
35
|
-
input_el.classList.add("cn-input-error")
|
|
36
|
-
|
|
37
|
-
icon_el = create_el("span", "cn-input-icon")
|
|
38
|
-
icon_el.innerHTML = self.icon
|
|
39
|
-
wrapper.appendChild(icon_el)
|
|
40
|
-
wrapper.appendChild(input_el)
|
|
41
|
-
return wrapper
|
|
42
|
-
|
|
43
|
-
el = create_el("input", "cn-input")
|
|
44
|
-
el.setAttribute("placeholder", self.placeholder)
|
|
45
|
-
if self.disabled:
|
|
46
|
-
el.setAttribute("disabled", "")
|
|
47
|
-
if self.error:
|
|
48
|
-
el.classList.add("cn-input-error")
|
|
87
|
+
def render(self) -> FormElement:
|
|
88
|
+
"""Render the input as a FormElement.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
FormElement representing the input
|
|
92
|
+
"""
|
|
93
|
+
classes = ["cn-input"]
|
|
49
94
|
if self.size != "md":
|
|
50
|
-
|
|
51
|
-
|
|
95
|
+
classes.append(f"cn-input-{self.size}")
|
|
96
|
+
if self.error:
|
|
97
|
+
classes.append("cn-input-error")
|
|
98
|
+
|
|
99
|
+
attrs: Dict[str, str] = {
|
|
100
|
+
"type": self.input_type,
|
|
101
|
+
"placeholder": self.placeholder,
|
|
102
|
+
}
|
|
103
|
+
if self.name is not None:
|
|
104
|
+
attrs["name"] = self.name
|
|
105
|
+
if self.value is not None:
|
|
106
|
+
attrs["value"] = self.value
|
|
107
|
+
if self.disabled:
|
|
108
|
+
attrs["disabled"] = ""
|
|
109
|
+
|
|
110
|
+
attrs_str = "".join(f' {k}="{v}"' for k, v in attrs.items())
|
|
111
|
+
inner = f"<input{attrs_str} />"
|
|
112
|
+
|
|
113
|
+
if self.icon:
|
|
114
|
+
wrapper_classes = "cn-input-icon-wrapper"
|
|
115
|
+
icon_html = f'<span class="cn-input-icon">{self.icon}</span>'
|
|
116
|
+
inner = f'<div class="{wrapper_classes}">{icon_html}{inner}</div>'
|
|
117
|
+
|
|
118
|
+
return FormElement(inner_html=inner)
|
|
119
|
+
|
|
120
|
+
def render_html(self) -> str:
|
|
121
|
+
"""Render the input as an HTML string.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
HTML string representation of the input
|
|
125
|
+
"""
|
|
126
|
+
return self.render().render_html()
|
|
52
127
|
|
|
53
128
|
|
|
54
129
|
class Textarea:
|
|
55
|
-
"""Textarea component.
|
|
130
|
+
"""Textarea component for multi-line text input.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
placeholder: Placeholder text
|
|
134
|
+
rows: Number of visible rows (default: 4)
|
|
135
|
+
name: Optional name attribute
|
|
136
|
+
disabled: Whether textarea is disabled (default: False)
|
|
137
|
+
|
|
138
|
+
Example:
|
|
139
|
+
>>> ta = Textarea(placeholder="Write your message...", rows=6)
|
|
140
|
+
>>> print(ta.render_html())
|
|
141
|
+
<textarea class="cn-input cn-textarea" placeholder="Write your message..." rows="6"></textarea>
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
def __init__(
|
|
145
|
+
self,
|
|
146
|
+
placeholder: str = "",
|
|
147
|
+
rows: int = 4,
|
|
148
|
+
name: Optional[str] = None,
|
|
149
|
+
disabled: bool = False,
|
|
150
|
+
):
|
|
151
|
+
if rows < 1:
|
|
152
|
+
raise ValueError("rows must be at least 1")
|
|
56
153
|
|
|
57
|
-
def __init__(self, placeholder: str = "", rows: int = 4):
|
|
58
154
|
self.placeholder = placeholder
|
|
59
155
|
self.rows = rows
|
|
60
|
-
self.
|
|
156
|
+
self.name = name
|
|
157
|
+
self.disabled = disabled
|
|
158
|
+
|
|
159
|
+
def render(self) -> FormElement:
|
|
160
|
+
"""Render the textarea as a FormElement.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
FormElement representing the textarea
|
|
164
|
+
"""
|
|
165
|
+
attrs: Dict[str, str] = {
|
|
166
|
+
"placeholder": self.placeholder,
|
|
167
|
+
"rows": str(self.rows),
|
|
168
|
+
}
|
|
169
|
+
if self.name is not None:
|
|
170
|
+
attrs["name"] = self.name
|
|
171
|
+
if self.disabled:
|
|
172
|
+
attrs["disabled"] = ""
|
|
173
|
+
|
|
174
|
+
attrs_str = "".join(f' {k}="{v}"' for k, v in attrs.items())
|
|
175
|
+
inner = f'<textarea class="cn-input cn-textarea"{attrs_str}></textarea>'
|
|
176
|
+
|
|
177
|
+
return FormElement(inner_html=inner)
|
|
178
|
+
|
|
179
|
+
def render_html(self) -> str:
|
|
180
|
+
"""Render the textarea as an HTML string.
|
|
61
181
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return el
|
|
182
|
+
Returns:
|
|
183
|
+
HTML string representation of the textarea
|
|
184
|
+
"""
|
|
185
|
+
return self.render().render_html()
|
|
67
186
|
|
|
68
187
|
|
|
69
188
|
class FormField:
|
|
70
|
-
"""Form field with label and
|
|
189
|
+
"""Form field wrapper with label, error message, and help text.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
label: Field label text
|
|
193
|
+
input_component: An Input, Textarea, or any component with render_html()
|
|
194
|
+
error: Optional error message text
|
|
195
|
+
help_text: Optional help/description text
|
|
196
|
+
required: Whether to mark the field as required (default: False)
|
|
197
|
+
|
|
198
|
+
Example:
|
|
199
|
+
>>> field = FormField(
|
|
200
|
+
... label="Email",
|
|
201
|
+
... input_component=Input(placeholder="you@example.com"),
|
|
202
|
+
... help_text="We'll never share your email.",
|
|
203
|
+
... )
|
|
204
|
+
>>> print(field.render_html())
|
|
205
|
+
<div class="cn-form-group">
|
|
206
|
+
<label class="cn-form-label">Email</label>
|
|
207
|
+
<input class="cn-input" placeholder="you@example.com" />
|
|
208
|
+
<span class="cn-form-help">We'll never share your email.</span>
|
|
209
|
+
</div>
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
def __init__(
|
|
213
|
+
self,
|
|
214
|
+
label: str,
|
|
215
|
+
input_component: Union[Input, Textarea, Checkbox, Radio, Select, Slider, "HasRenderHtml"],
|
|
216
|
+
error: Optional[str] = None,
|
|
217
|
+
help_text: Optional[str] = None,
|
|
218
|
+
required: bool = False,
|
|
219
|
+
):
|
|
220
|
+
if not label:
|
|
221
|
+
raise ValueError("label cannot be empty")
|
|
71
222
|
|
|
72
|
-
def __init__(self, label: str, input_el, error: str = None, help_text: str = None):
|
|
73
223
|
self.label = label
|
|
74
|
-
self.input =
|
|
224
|
+
self.input = input_component
|
|
75
225
|
self.error = error
|
|
76
226
|
self.help_text = help_text
|
|
77
|
-
self.
|
|
227
|
+
self.required = required
|
|
78
228
|
|
|
79
|
-
def
|
|
80
|
-
|
|
229
|
+
def render(self) -> FormElement:
|
|
230
|
+
"""Render the form field as a FormElement.
|
|
81
231
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
232
|
+
Returns:
|
|
233
|
+
FormElement wrapping the label, input, and optional messages
|
|
234
|
+
"""
|
|
235
|
+
required_mark = ' <span class="cn-form-required">*</span>' if self.required else ""
|
|
236
|
+
label_text = f'{self._esc(self.label)}{required_mark}'
|
|
85
237
|
|
|
86
|
-
if hasattr(self.input, "
|
|
87
|
-
|
|
238
|
+
if hasattr(self.input, "render_html"):
|
|
239
|
+
input_html = self.input.render_html()
|
|
88
240
|
else:
|
|
89
|
-
|
|
241
|
+
input_html = str(self.input)
|
|
242
|
+
|
|
243
|
+
parts = [
|
|
244
|
+
f'<label class="cn-form-label">{label_text}</label>',
|
|
245
|
+
input_html,
|
|
246
|
+
]
|
|
90
247
|
|
|
91
248
|
if self.error:
|
|
92
|
-
|
|
93
|
-
error_el.textContent = self.error
|
|
94
|
-
group.appendChild(error_el)
|
|
249
|
+
parts.append(f'<span class="cn-form-error">{self._esc(self.error)}</span>')
|
|
95
250
|
|
|
96
251
|
if self.help_text:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
252
|
+
parts.append(f'<span class="cn-form-help">{self._esc(self.help_text)}</span>')
|
|
253
|
+
|
|
254
|
+
return FormElement(
|
|
255
|
+
classes=["cn-form-group"],
|
|
256
|
+
inner_html="".join(parts),
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def render_html(self) -> str:
|
|
260
|
+
"""Render the form field as an HTML string.
|
|
100
261
|
|
|
101
|
-
|
|
262
|
+
Returns:
|
|
263
|
+
HTML string representation of the form field
|
|
264
|
+
"""
|
|
265
|
+
return self.render().render_html()
|
|
266
|
+
|
|
267
|
+
@staticmethod
|
|
268
|
+
def _esc(text: str) -> str:
|
|
269
|
+
"""Escape HTML special characters."""
|
|
270
|
+
return (
|
|
271
|
+
text.replace("&", "&")
|
|
272
|
+
.replace("<", "<")
|
|
273
|
+
.replace(">", ">")
|
|
274
|
+
.replace('"', """)
|
|
275
|
+
.replace("'", "'")
|
|
276
|
+
)
|
|
102
277
|
|
|
103
278
|
|
|
104
279
|
class Checkbox:
|
|
105
|
-
"""Checkbox component.
|
|
280
|
+
"""Checkbox component with label.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
label: Checkbox label text
|
|
284
|
+
checked: Whether checkbox is initially checked (default: False)
|
|
285
|
+
disabled: Whether checkbox is disabled (default: False)
|
|
286
|
+
name: Optional name attribute
|
|
287
|
+
|
|
288
|
+
Example:
|
|
289
|
+
>>> cb = Checkbox("Accept terms", checked=True)
|
|
290
|
+
>>> print(cb.render_html())
|
|
291
|
+
<label class="cn-checkbox"><input type="checkbox" checked="" /><span class="cn-checkbox-box"></span><span class="cn-checkbox-label">Accept terms</span></label>
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
def __init__(
|
|
295
|
+
self,
|
|
296
|
+
label: str,
|
|
297
|
+
checked: bool = False,
|
|
298
|
+
disabled: bool = False,
|
|
299
|
+
name: Optional[str] = None,
|
|
300
|
+
):
|
|
301
|
+
if not label:
|
|
302
|
+
raise ValueError("label cannot be empty")
|
|
106
303
|
|
|
107
|
-
def __init__(self, label: str, checked: bool = False, disabled: bool = False):
|
|
108
304
|
self.label = label
|
|
109
305
|
self.checked = checked
|
|
110
306
|
self.disabled = disabled
|
|
111
|
-
self.
|
|
307
|
+
self.name = name
|
|
112
308
|
|
|
113
|
-
def
|
|
114
|
-
|
|
115
|
-
if self.disabled:
|
|
116
|
-
el.classList.add("disabled")
|
|
309
|
+
def render(self) -> FormElement:
|
|
310
|
+
"""Render the checkbox as a FormElement.
|
|
117
311
|
|
|
118
|
-
|
|
119
|
-
|
|
312
|
+
Returns:
|
|
313
|
+
FormElement representing the checkbox
|
|
314
|
+
"""
|
|
315
|
+
attrs: Dict[str, str] = {"type": "checkbox"}
|
|
316
|
+
if self.name is not None:
|
|
317
|
+
attrs["name"] = self.name
|
|
120
318
|
if self.checked:
|
|
121
|
-
|
|
319
|
+
attrs["checked"] = ""
|
|
122
320
|
if self.disabled:
|
|
123
|
-
|
|
321
|
+
attrs["disabled"] = ""
|
|
124
322
|
|
|
125
|
-
|
|
126
|
-
label_el = create_el("span", "cn-checkbox-label")
|
|
127
|
-
label_el.textContent = self.label
|
|
323
|
+
attrs_str = "".join(f' {k}="{v}"' for k, v in attrs.items())
|
|
128
324
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
325
|
+
label_classes = "cn-checkbox"
|
|
326
|
+
if self.disabled:
|
|
327
|
+
label_classes += " disabled"
|
|
328
|
+
|
|
329
|
+
inner = (
|
|
330
|
+
f'<label class="{label_classes}">'
|
|
331
|
+
f'<input{attrs_str} />'
|
|
332
|
+
f'<span class="cn-checkbox-box"></span>'
|
|
333
|
+
f'<span class="cn-checkbox-label">{self._esc(self.label)}</span>'
|
|
334
|
+
f"</label>"
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
return FormElement(inner_html=inner)
|
|
338
|
+
|
|
339
|
+
def render_html(self) -> str:
|
|
340
|
+
"""Render the checkbox as an HTML string.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
HTML string representation of the checkbox
|
|
344
|
+
"""
|
|
345
|
+
return self.render().render_html()
|
|
346
|
+
|
|
347
|
+
@staticmethod
|
|
348
|
+
def _esc(text: str) -> str:
|
|
349
|
+
"""Escape HTML special characters."""
|
|
350
|
+
return (
|
|
351
|
+
text.replace("&", "&")
|
|
352
|
+
.replace("<", "<")
|
|
353
|
+
.replace(">", ">")
|
|
354
|
+
.replace('"', """)
|
|
355
|
+
.replace("'", "'")
|
|
356
|
+
)
|
|
133
357
|
|
|
134
358
|
|
|
135
359
|
class Radio:
|
|
136
|
-
"""Radio button component.
|
|
360
|
+
"""Radio button group component.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
name: Radio group name (shared name attribute)
|
|
364
|
+
options: List of (value, label) tuples or plain strings
|
|
365
|
+
selected: Currently selected value
|
|
366
|
+
disabled: Whether entire group is disabled (default: False)
|
|
367
|
+
|
|
368
|
+
Example:
|
|
369
|
+
>>> radio = Radio("color", [("red", "Red"), ("blue", "Blue")], selected="red")
|
|
370
|
+
>>> print(radio.render_html())
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
def __init__(
|
|
374
|
+
self,
|
|
375
|
+
name: str,
|
|
376
|
+
options: List[Union[Tuple[str, str], str]],
|
|
377
|
+
selected: Optional[str] = None,
|
|
378
|
+
disabled: bool = False,
|
|
379
|
+
):
|
|
380
|
+
if not name:
|
|
381
|
+
raise ValueError("name cannot be empty")
|
|
382
|
+
if not options:
|
|
383
|
+
raise ValueError("options cannot be empty")
|
|
137
384
|
|
|
138
|
-
def __init__(self, name: str, options: list, selected: str = None):
|
|
139
385
|
self.name = name
|
|
140
386
|
self.options = options
|
|
141
387
|
self.selected = selected
|
|
142
|
-
self.
|
|
388
|
+
self.disabled = disabled
|
|
389
|
+
|
|
390
|
+
def render(self) -> FormElement:
|
|
391
|
+
"""Render the radio group as a FormElement.
|
|
143
392
|
|
|
144
|
-
|
|
145
|
-
|
|
393
|
+
Returns:
|
|
394
|
+
FormElement containing all radio options
|
|
395
|
+
"""
|
|
396
|
+
parts = []
|
|
146
397
|
for option in self.options:
|
|
147
398
|
if isinstance(option, tuple):
|
|
148
399
|
value, label = option
|
|
149
400
|
else:
|
|
150
401
|
value = label = option
|
|
151
402
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
403
|
+
input_attrs: Dict[str, str] = {
|
|
404
|
+
"type": "radio",
|
|
405
|
+
"name": self.name,
|
|
406
|
+
"value": value,
|
|
407
|
+
}
|
|
157
408
|
if value == self.selected:
|
|
158
|
-
|
|
409
|
+
input_attrs["checked"] = ""
|
|
410
|
+
if self.disabled:
|
|
411
|
+
input_attrs["disabled"] = ""
|
|
412
|
+
|
|
413
|
+
attrs_str = "".join(f' {k}="{v}"' for k, v in input_attrs.items())
|
|
414
|
+
|
|
415
|
+
parts.append(
|
|
416
|
+
f'<label class="cn-radio">'
|
|
417
|
+
f'<input{attrs_str} />'
|
|
418
|
+
f'<span class="cn-radio-box"></span>'
|
|
419
|
+
f'<span class="cn-radio-label">{self._esc(label)}</span>'
|
|
420
|
+
f"</label>"
|
|
421
|
+
)
|
|
159
422
|
|
|
160
|
-
|
|
161
|
-
label_el = create_el("span", "cn-radio-label")
|
|
162
|
-
label_el.textContent = label
|
|
423
|
+
return FormElement(inner_html="".join(parts))
|
|
163
424
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
el.appendChild(label_el)
|
|
167
|
-
container.appendChild(el)
|
|
425
|
+
def render_html(self) -> str:
|
|
426
|
+
"""Render the radio group as an HTML string.
|
|
168
427
|
|
|
169
|
-
|
|
428
|
+
Returns:
|
|
429
|
+
HTML string representation of the radio group
|
|
430
|
+
"""
|
|
431
|
+
return self.render().render_html()
|
|
432
|
+
|
|
433
|
+
@staticmethod
|
|
434
|
+
def _esc(text: str) -> str:
|
|
435
|
+
"""Escape HTML special characters."""
|
|
436
|
+
return (
|
|
437
|
+
text.replace("&", "&")
|
|
438
|
+
.replace("<", "<")
|
|
439
|
+
.replace(">", ">")
|
|
440
|
+
.replace('"', """)
|
|
441
|
+
.replace("'", "'")
|
|
442
|
+
)
|
|
170
443
|
|
|
171
444
|
|
|
172
445
|
class Select:
|
|
173
|
-
"""Select dropdown component.
|
|
446
|
+
"""Select dropdown component.
|
|
447
|
+
|
|
448
|
+
Args:
|
|
449
|
+
options: List of (value, label) tuples or plain strings
|
|
450
|
+
placeholder: Optional placeholder/disabled first option
|
|
451
|
+
name: Optional name attribute
|
|
452
|
+
disabled: Whether select is disabled (default: False)
|
|
453
|
+
|
|
454
|
+
Example:
|
|
455
|
+
>>> sel = Select(
|
|
456
|
+
... options=[("1", "One"), ("2", "Two")],
|
|
457
|
+
... placeholder="Choose...",
|
|
458
|
+
... )
|
|
459
|
+
>>> print(sel.render_html())
|
|
460
|
+
"""
|
|
461
|
+
|
|
462
|
+
def __init__(
|
|
463
|
+
self,
|
|
464
|
+
options: List[Union[Tuple[str, str], str]],
|
|
465
|
+
placeholder: str = "",
|
|
466
|
+
name: Optional[str] = None,
|
|
467
|
+
disabled: bool = False,
|
|
468
|
+
):
|
|
469
|
+
if not options:
|
|
470
|
+
raise ValueError("options cannot be empty")
|
|
174
471
|
|
|
175
|
-
def __init__(self, options: list, placeholder: str = ""):
|
|
176
472
|
self.options = options
|
|
177
473
|
self.placeholder = placeholder
|
|
178
|
-
self.
|
|
474
|
+
self.name = name
|
|
475
|
+
self.disabled = disabled
|
|
476
|
+
|
|
477
|
+
def render(self) -> FormElement:
|
|
478
|
+
"""Render the select as a FormElement.
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
FormElement representing the select dropdown
|
|
482
|
+
"""
|
|
483
|
+
parts = []
|
|
484
|
+
|
|
485
|
+
select_attrs: Dict[str, str] = {}
|
|
486
|
+
if self.name is not None:
|
|
487
|
+
select_attrs["name"] = self.name
|
|
488
|
+
if self.disabled:
|
|
489
|
+
select_attrs["disabled"] = ""
|
|
179
490
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
select = create_el("select", "cn-select")
|
|
491
|
+
attrs_str = "".join(f' {k}="{v}"' for k, v in select_attrs.items())
|
|
492
|
+
attrs_str = f' class="cn-select"{attrs_str}' if attrs_str else ' class="cn-select"'
|
|
183
493
|
|
|
184
494
|
if self.placeholder:
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
placeholder_option.setAttribute("selected", "")
|
|
189
|
-
placeholder_option.textContent = self.placeholder
|
|
190
|
-
select.appendChild(placeholder_option)
|
|
495
|
+
parts.append(
|
|
496
|
+
f'<option value="" disabled selected>{self._esc(self.placeholder)}</option>'
|
|
497
|
+
)
|
|
191
498
|
|
|
192
499
|
for option in self.options:
|
|
193
500
|
if isinstance(option, tuple):
|
|
@@ -195,61 +502,165 @@ class Select:
|
|
|
195
502
|
else:
|
|
196
503
|
value = label = option
|
|
197
504
|
|
|
198
|
-
|
|
199
|
-
opt.setAttribute("value", value)
|
|
200
|
-
opt.textContent = label
|
|
201
|
-
select.appendChild(opt)
|
|
505
|
+
parts.append(f'<option value="{self._esc(value)}">{self._esc(label)}</option>')
|
|
202
506
|
|
|
203
|
-
|
|
204
|
-
|
|
507
|
+
inner = f"<select{attrs_str}>{''.join(parts)}</select>"
|
|
508
|
+
|
|
509
|
+
return FormElement(
|
|
510
|
+
classes=["cn-select-wrapper"],
|
|
511
|
+
inner_html=inner,
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
def render_html(self) -> str:
|
|
515
|
+
"""Render the select as an HTML string.
|
|
516
|
+
|
|
517
|
+
Returns:
|
|
518
|
+
HTML string representation of the select dropdown
|
|
519
|
+
"""
|
|
520
|
+
return self.render().render_html()
|
|
521
|
+
|
|
522
|
+
@staticmethod
|
|
523
|
+
def _esc(text: str) -> str:
|
|
524
|
+
"""Escape HTML special characters."""
|
|
525
|
+
return (
|
|
526
|
+
text.replace("&", "&")
|
|
527
|
+
.replace("<", "<")
|
|
528
|
+
.replace(">", ">")
|
|
529
|
+
.replace('"', """)
|
|
530
|
+
.replace("'", "'")
|
|
531
|
+
)
|
|
205
532
|
|
|
206
533
|
|
|
207
534
|
class Slider:
|
|
208
|
-
"""Slider component.
|
|
535
|
+
"""Slider (range input) component.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
min: Minimum value (default: 0)
|
|
539
|
+
max: Maximum value (default: 100)
|
|
540
|
+
value: Initial value (default: 50)
|
|
541
|
+
name: Optional name attribute
|
|
542
|
+
step: Optional step value
|
|
543
|
+
|
|
544
|
+
Example:
|
|
545
|
+
>>> slider = Slider(min=0, max=100, value=50)
|
|
546
|
+
>>> print(slider.render_html())
|
|
547
|
+
<input class="cn-slider" type="range" min="0" max="100" value="50" />
|
|
548
|
+
"""
|
|
549
|
+
|
|
550
|
+
def __init__(
|
|
551
|
+
self,
|
|
552
|
+
min: float = 0,
|
|
553
|
+
max: float = 100,
|
|
554
|
+
value: float = 50,
|
|
555
|
+
name: Optional[str] = None,
|
|
556
|
+
step: Optional[float] = None,
|
|
557
|
+
):
|
|
558
|
+
if min >= max:
|
|
559
|
+
raise ValueError("min must be less than max")
|
|
560
|
+
if value < min or value > max:
|
|
561
|
+
raise ValueError("value must be between min and max")
|
|
209
562
|
|
|
210
|
-
def __init__(self, min: float = 0, max: float = 100, value: float = 50):
|
|
211
563
|
self.min = min
|
|
212
564
|
self.max = max
|
|
213
565
|
self.value = value
|
|
214
|
-
self.
|
|
566
|
+
self.name = name
|
|
567
|
+
self.step = step
|
|
568
|
+
|
|
569
|
+
def render(self) -> FormElement:
|
|
570
|
+
"""Render the slider as a FormElement.
|
|
571
|
+
|
|
572
|
+
Returns:
|
|
573
|
+
FormElement representing the range input
|
|
574
|
+
"""
|
|
575
|
+
attrs: Dict[str, str] = {
|
|
576
|
+
"type": "range",
|
|
577
|
+
"min": str(self.min),
|
|
578
|
+
"max": str(self.max),
|
|
579
|
+
"value": str(self.value),
|
|
580
|
+
}
|
|
581
|
+
if self.name is not None:
|
|
582
|
+
attrs["name"] = self.name
|
|
583
|
+
if self.step is not None:
|
|
584
|
+
attrs["step"] = str(self.step)
|
|
585
|
+
|
|
586
|
+
attrs_str = "".join(f' {k}="{v}"' for k, v in attrs.items())
|
|
587
|
+
inner = f'<input class="cn-slider"{attrs_str} />'
|
|
215
588
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
589
|
+
return FormElement(inner_html=inner)
|
|
590
|
+
|
|
591
|
+
def render_html(self) -> str:
|
|
592
|
+
"""Render the slider as an HTML string.
|
|
593
|
+
|
|
594
|
+
Returns:
|
|
595
|
+
HTML string representation of the slider
|
|
596
|
+
"""
|
|
597
|
+
return self.render().render_html()
|
|
223
598
|
|
|
224
599
|
|
|
225
600
|
class FileInput:
|
|
226
|
-
"""File input component.
|
|
601
|
+
"""File input component with drag-and-drop styling.
|
|
602
|
+
|
|
603
|
+
Args:
|
|
604
|
+
accept: Accepted file types (e.g. ".png,.jpg")
|
|
605
|
+
multiple: Whether multiple files can be selected (default: False)
|
|
606
|
+
name: Optional name attribute
|
|
607
|
+
|
|
608
|
+
Example:
|
|
609
|
+
>>> file = FileInput(accept=".png,.jpg", multiple=True)
|
|
610
|
+
>>> print(file.render_html())
|
|
611
|
+
"""
|
|
227
612
|
|
|
228
|
-
|
|
613
|
+
_UPLOAD_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>'
|
|
614
|
+
|
|
615
|
+
def __init__(
|
|
616
|
+
self,
|
|
617
|
+
accept: str = "",
|
|
618
|
+
multiple: bool = False,
|
|
619
|
+
name: Optional[str] = None,
|
|
620
|
+
):
|
|
229
621
|
self.accept = accept
|
|
230
622
|
self.multiple = multiple
|
|
231
|
-
self.
|
|
623
|
+
self.name = name
|
|
232
624
|
|
|
233
|
-
def
|
|
234
|
-
|
|
625
|
+
def render(self) -> FormElement:
|
|
626
|
+
"""Render the file input as a FormElement.
|
|
235
627
|
|
|
236
|
-
|
|
237
|
-
|
|
628
|
+
Returns:
|
|
629
|
+
FormElement representing the file input with label
|
|
630
|
+
"""
|
|
631
|
+
input_attrs: Dict[str, str] = {"type": "file"}
|
|
632
|
+
if self.name is not None:
|
|
633
|
+
input_attrs["name"] = self.name
|
|
238
634
|
if self.accept:
|
|
239
|
-
|
|
635
|
+
input_attrs["accept"] = self.accept
|
|
240
636
|
if self.multiple:
|
|
241
|
-
|
|
637
|
+
input_attrs["multiple"] = ""
|
|
638
|
+
|
|
639
|
+
input_attrs_str = "".join(f' {k}="{v}"' for k, v in input_attrs.items())
|
|
640
|
+
|
|
641
|
+
inner = (
|
|
642
|
+
f'<div class="cn-file-input">'
|
|
643
|
+
f'<input{input_attrs_str} />'
|
|
644
|
+
f'<div class="cn-file-input-label">'
|
|
645
|
+
f'<div class="cn-file-input-icon">{self._UPLOAD_ICON}</div>'
|
|
646
|
+
f'<div class="cn-file-input-text">Drag and drop or <span>browse</span></div>'
|
|
647
|
+
f"</div>"
|
|
648
|
+
f"</div>"
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
return FormElement(inner_html=inner)
|
|
652
|
+
|
|
653
|
+
def render_html(self) -> str:
|
|
654
|
+
"""Render the file input as an HTML string.
|
|
242
655
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
656
|
+
Returns:
|
|
657
|
+
HTML string representation of the file input
|
|
658
|
+
"""
|
|
659
|
+
return self.render().render_html()
|
|
246
660
|
|
|
247
|
-
text = create_el("div", "cn-file-input-text")
|
|
248
|
-
text.innerHTML = "Drag and drop or <span>browse</span>"
|
|
249
661
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
wrapper.appendChild(input_el)
|
|
253
|
-
wrapper.appendChild(label)
|
|
662
|
+
class HasRenderHtml:
|
|
663
|
+
"""Protocol-like base for type hints: any object with render_html()."""
|
|
254
664
|
|
|
255
|
-
|
|
665
|
+
def render_html(self) -> str:
|
|
666
|
+
...
|