cronixui 1.0.5 → 1.1.0

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.
Files changed (130) hide show
  1. package/README.md +35 -5
  2. package/package.json +21 -3
  3. package/packages/go/cronixui/cronixui.go +784 -237
  4. package/packages/go/cronixui/go.mod +34 -9
  5. package/packages/go/cronixui/go.sum +666 -0
  6. package/packages/python/cronixui/__init__.py +131 -1
  7. package/packages/python/cronixui/alert.py +61 -0
  8. package/packages/python/cronixui/avatar.py +50 -0
  9. package/packages/python/cronixui/badge.py +46 -0
  10. package/packages/python/cronixui/button.py +64 -0
  11. package/packages/python/cronixui/card.py +62 -0
  12. package/packages/python/cronixui/form.py +255 -0
  13. package/packages/python/cronixui/layout.py +143 -0
  14. package/packages/python/cronixui/list.py +51 -0
  15. package/packages/python/cronixui/loading.py +36 -0
  16. package/packages/python/cronixui/progress.py +90 -0
  17. package/packages/python/cronixui/table.py +48 -0
  18. package/packages/python/cronixui/tokens.py +200 -0
  19. package/packages/python/cronixui/tooltip.py +28 -0
  20. package/packages/react/src/components/Accordion.tsx +82 -0
  21. package/packages/react/src/components/Alert.tsx +80 -0
  22. package/packages/react/src/components/Avatar.tsx +54 -0
  23. package/packages/react/src/components/Badge.tsx +32 -0
  24. package/packages/react/src/components/Breadcrumb.tsx +50 -0
  25. package/packages/react/src/components/Button.tsx +47 -0
  26. package/packages/react/src/components/Card.tsx +69 -0
  27. package/packages/react/src/components/Checkbox.tsx +30 -0
  28. package/packages/react/src/components/CommandPalette.tsx +131 -0
  29. package/packages/react/src/components/Container.tsx +26 -0
  30. package/packages/react/src/components/Dropdown.tsx +88 -0
  31. package/packages/react/src/components/FileInput.tsx +86 -0
  32. package/packages/react/src/components/Footer.tsx +36 -0
  33. package/packages/react/src/components/FormGroup.tsx +36 -0
  34. package/packages/react/src/components/Header.tsx +29 -0
  35. package/packages/react/src/components/Input.tsx +54 -0
  36. package/packages/react/src/components/List.tsx +55 -0
  37. package/packages/react/src/components/Modal.tsx +89 -0
  38. package/packages/react/src/components/Nav.tsx +63 -0
  39. package/packages/react/src/components/Pagination.tsx +107 -0
  40. package/packages/react/src/components/Progress.tsx +49 -0
  41. package/packages/react/src/components/Radio.tsx +64 -0
  42. package/packages/react/src/components/Search.tsx +95 -0
  43. package/packages/react/src/components/Select.tsx +41 -0
  44. package/packages/react/src/components/Sidebar.tsx +64 -0
  45. package/packages/react/src/components/Skeleton.tsx +39 -0
  46. package/packages/react/src/components/Slider.tsx +32 -0
  47. package/packages/react/src/components/Spinner.tsx +24 -0
  48. package/packages/react/src/components/Stack.tsx +69 -0
  49. package/packages/react/src/components/Stat.tsx +35 -0
  50. package/packages/react/src/components/Table.tsx +90 -0
  51. package/packages/react/src/components/Tabs.tsx +85 -0
  52. package/packages/react/src/components/Tag.tsx +30 -0
  53. package/packages/react/src/components/Textarea.tsx +21 -0
  54. package/packages/react/src/components/Toast.tsx +134 -0
  55. package/packages/react/src/components/Toggle.tsx +58 -0
  56. package/packages/react/src/components/Tooltip.tsx +31 -0
  57. package/packages/react/src/components/Typography.tsx +66 -0
  58. package/packages/react/src/index.ts +40 -0
  59. package/packages/react/src/styles.css +2039 -0
  60. package/packages/react/src/tokens/index.ts +94 -0
  61. package/packages/rust/cronixui/src/colors.rs +135 -0
  62. package/packages/rust/cronixui/src/components/accordion.rs +47 -0
  63. package/packages/rust/cronixui/src/components/alert.rs +95 -0
  64. package/packages/rust/cronixui/src/components/avatar.rs +85 -0
  65. package/packages/rust/cronixui/src/components/badge.rs +35 -0
  66. package/packages/rust/cronixui/src/components/breadcrumb.rs +58 -0
  67. package/packages/rust/cronixui/src/components/button.rs +70 -0
  68. package/packages/rust/cronixui/src/components/card.rs +259 -0
  69. package/packages/rust/cronixui/src/components/command_palette.rs +254 -0
  70. package/packages/rust/cronixui/src/components/dropdown.rs +179 -0
  71. package/packages/rust/cronixui/src/components/file_input.rs +74 -0
  72. package/packages/rust/cronixui/src/components/input.rs +21 -0
  73. package/packages/rust/cronixui/src/components/list.rs +38 -0
  74. package/packages/rust/cronixui/src/components/mod.rs +51 -0
  75. package/packages/rust/cronixui/src/{modal.rs → components/modal.rs} +15 -1
  76. package/packages/rust/cronixui/src/components/nav.rs +19 -0
  77. package/packages/rust/cronixui/src/{pagination.rs → components/pagination.rs} +14 -13
  78. package/packages/rust/cronixui/src/components/progress.rs +50 -0
  79. package/packages/rust/cronixui/src/components/search.rs +185 -0
  80. package/packages/rust/cronixui/src/components/skeleton.rs +63 -0
  81. package/packages/rust/cronixui/src/components/spinner.rs +21 -0
  82. package/packages/rust/cronixui/src/components/table.rs +56 -0
  83. package/packages/rust/cronixui/src/components/tabs.rs +43 -0
  84. package/packages/rust/cronixui/src/components/toast.rs +69 -0
  85. package/packages/rust/cronixui/src/{toggle.rs → components/toggle.rs} +7 -5
  86. package/packages/rust/cronixui/src/components/tooltip.rs +11 -0
  87. package/packages/rust/cronixui/src/lib.rs +111 -62
  88. package/packages/rust/cronixui/src/tokens.rs +107 -0
  89. package/packages/web/src/tokens.ts +120 -0
  90. package/packages/web/src/variables.css +81 -81
  91. package/packages/python/cronixui/pyproject.toml +0 -11
  92. package/packages/react/src/components/Accordion.jsx +0 -50
  93. package/packages/react/src/components/Alert.jsx +0 -62
  94. package/packages/react/src/components/Avatar.jsx +0 -34
  95. package/packages/react/src/components/Badge.jsx +0 -15
  96. package/packages/react/src/components/Breadcrumb.jsx +0 -27
  97. package/packages/react/src/components/Button.jsx +0 -21
  98. package/packages/react/src/components/Card.jsx +0 -23
  99. package/packages/react/src/components/Checkbox.jsx +0 -27
  100. package/packages/react/src/components/CommandPalette.jsx +0 -93
  101. package/packages/react/src/components/Dropdown.jsx +0 -48
  102. package/packages/react/src/components/FileInput.jsx +0 -44
  103. package/packages/react/src/components/Input.jsx +0 -22
  104. package/packages/react/src/components/List.jsx +0 -29
  105. package/packages/react/src/components/Modal.jsx +0 -65
  106. package/packages/react/src/components/Nav.jsx +0 -50
  107. package/packages/react/src/components/Pagination.jsx +0 -81
  108. package/packages/react/src/components/Progress.jsx +0 -23
  109. package/packages/react/src/components/Radio.jsx +0 -50
  110. package/packages/react/src/components/Search.jsx +0 -70
  111. package/packages/react/src/components/Select.jsx +0 -33
  112. package/packages/react/src/components/Skeleton.jsx +0 -15
  113. package/packages/react/src/components/Slider.jsx +0 -29
  114. package/packages/react/src/components/Spinner.jsx +0 -5
  115. package/packages/react/src/components/Stat.jsx +0 -19
  116. package/packages/react/src/components/Table.jsx +0 -48
  117. package/packages/react/src/components/Tabs.jsx +0 -65
  118. package/packages/react/src/components/Tag.jsx +0 -19
  119. package/packages/react/src/components/Textarea.jsx +0 -17
  120. package/packages/react/src/components/Toast.jsx +0 -78
  121. package/packages/react/src/components/Toggle.jsx +0 -34
  122. package/packages/react/src/components/Tooltip.jsx +0 -12
  123. package/packages/react/src/index.d.ts +0 -103
  124. package/packages/react/src/index.js +0 -33
  125. package/packages/rust/cronixui/src/accordion.rs +0 -49
  126. package/packages/rust/cronixui/src/command_palette.rs +0 -62
  127. package/packages/rust/cronixui/src/dropdown.rs +0 -31
  128. package/packages/rust/cronixui/src/search.rs +0 -49
  129. package/packages/rust/cronixui/src/tabs.rs +0 -23
  130. package/packages/rust/cronixui/src/toast.rs +0 -70
@@ -12,9 +12,66 @@ from .pagination import Pagination
12
12
  from .command_palette import CommandPalette, CommandPaletteItem
13
13
  from .search import Search, SearchItem
14
14
  from .nav import Nav
15
+ from .button import Button, ButtonGroup
16
+ from .card import Card, CardIcon
17
+ from .badge import Badge, Tag
18
+ from .avatar import Avatar, AvatarGroup
19
+ from .alert import Alert
20
+ from .loading import Spinner, Skeleton
21
+ from .table import Table
22
+ from .list import List
23
+ from .tooltip import Tooltip
24
+ from .layout import Header, Sidebar, Footer, Container, Divider, Section
25
+ from .form import Input, Textarea, FormField, Checkbox, Radio, Select, Slider, FileInput
26
+ from .progress import Progress, Stat
15
27
  from .core import init, query, query_all, create_el
28
+ from .tokens import (
29
+ BG,
30
+ SURFACE,
31
+ SURFACE_2,
32
+ SURFACE_3,
33
+ SURFACE_4,
34
+ TEXT,
35
+ TEXT_MUTED,
36
+ TEXT_DIM,
37
+ ACCENT,
38
+ ACCENT_HOVER,
39
+ ACCENT_LIGHT,
40
+ ACCENT_GLOW,
41
+ ACCENT_TEXT,
42
+ SUCCESS,
43
+ SUCCESS_BORDER,
44
+ SUCCESS_TEXT,
45
+ WARNING,
46
+ WARNING_BORDER,
47
+ WARNING_TEXT,
48
+ ERROR,
49
+ ERROR_BORDER,
50
+ ERROR_TEXT,
51
+ INFO,
52
+ INFO_BORDER,
53
+ INFO_TEXT,
54
+ BORDER,
55
+ BORDER_HOVER,
56
+ BORDER_FOCUS,
57
+ typography,
58
+ spacing,
59
+ radius,
60
+ shadow,
61
+ transition,
62
+ z_index,
63
+ layout,
64
+ Color,
65
+ Typography,
66
+ Spacing,
67
+ Radius,
68
+ Shadow,
69
+ Transition,
70
+ ZIndex,
71
+ Layout,
72
+ )
16
73
 
17
- __version__ = "1.0.4"
74
+ __version__ = "1.0.6"
18
75
  __all__ = [
19
76
  "Toast",
20
77
  "Toggle",
@@ -28,8 +85,81 @@ __all__ = [
28
85
  "Search",
29
86
  "SearchItem",
30
87
  "Nav",
88
+ "Button",
89
+ "ButtonGroup",
90
+ "Card",
91
+ "CardIcon",
92
+ "Badge",
93
+ "Tag",
94
+ "Avatar",
95
+ "AvatarGroup",
96
+ "Alert",
97
+ "Spinner",
98
+ "Skeleton",
99
+ "Table",
100
+ "List",
101
+ "Tooltip",
102
+ "Header",
103
+ "Sidebar",
104
+ "Footer",
105
+ "Container",
106
+ "Divider",
107
+ "Section",
108
+ "Input",
109
+ "Textarea",
110
+ "FormField",
111
+ "Checkbox",
112
+ "Radio",
113
+ "Select",
114
+ "Slider",
115
+ "FileInput",
116
+ "Progress",
117
+ "Stat",
31
118
  "init",
32
119
  "query",
33
120
  "query_all",
34
121
  "create_el",
122
+ "BG",
123
+ "SURFACE",
124
+ "SURFACE_2",
125
+ "SURFACE_3",
126
+ "SURFACE_4",
127
+ "TEXT",
128
+ "TEXT_MUTED",
129
+ "TEXT_DIM",
130
+ "ACCENT",
131
+ "ACCENT_HOVER",
132
+ "ACCENT_LIGHT",
133
+ "ACCENT_GLOW",
134
+ "ACCENT_TEXT",
135
+ "SUCCESS",
136
+ "SUCCESS_BORDER",
137
+ "SUCCESS_TEXT",
138
+ "WARNING",
139
+ "WARNING_BORDER",
140
+ "WARNING_TEXT",
141
+ "ERROR",
142
+ "ERROR_BORDER",
143
+ "ERROR_TEXT",
144
+ "INFO",
145
+ "INFO_BORDER",
146
+ "INFO_TEXT",
147
+ "BORDER",
148
+ "BORDER_HOVER",
149
+ "BORDER_FOCUS",
150
+ "Color",
151
+ "Typography",
152
+ "Spacing",
153
+ "Radius",
154
+ "Shadow",
155
+ "Transition",
156
+ "ZIndex",
157
+ "Layout",
158
+ "typography",
159
+ "spacing",
160
+ "radius",
161
+ "shadow",
162
+ "transition",
163
+ "z_index",
164
+ "layout",
35
165
  ]
@@ -0,0 +1,61 @@
1
+ """CronixUI Alert Component"""
2
+
3
+ from typing import Optional
4
+ from .core import create_el
5
+
6
+
7
+ class Alert:
8
+ """Alert component for messages."""
9
+
10
+ VARIANTS = ("info", "success", "warning", "error")
11
+
12
+ def __init__(
13
+ self,
14
+ message: str,
15
+ title: Optional[str] = None,
16
+ variant: str = "info",
17
+ dismissible: bool = False,
18
+ ):
19
+ self.message = message
20
+ self.title = title
21
+ self.variant = variant if variant in self.VARIANTS else "info"
22
+ self.dismissible = dismissible
23
+ self.element = self._render()
24
+
25
+ def _render(self):
26
+ el = create_el("div", f"cn-alert cn-alert-{self.variant}")
27
+
28
+ icon_svg = self._get_icon_svg()
29
+ if icon_svg:
30
+ icon = create_el("span", "cn-alert-icon")
31
+ icon.innerHTML = icon_svg
32
+ el.appendChild(icon)
33
+
34
+ content = create_el("div", "cn-alert-content")
35
+ if self.title:
36
+ title_el = create_el("div", "cn-alert-title")
37
+ title_el.textContent = self.title
38
+ content.appendChild(title_el)
39
+
40
+ message_el = create_el("div", "cn-alert-message")
41
+ message_el.textContent = self.message
42
+ content.appendChild(message_el)
43
+
44
+ el.appendChild(content)
45
+
46
+ if self.dismissible:
47
+ close_btn = create_el("button", "cn-alert-close")
48
+ close_btn.innerHTML = "×"
49
+ close_btn.addEventListener("click", lambda: el.remove())
50
+ el.appendChild(close_btn)
51
+
52
+ return el
53
+
54
+ def _get_icon_svg(self):
55
+ icons = {
56
+ "info": '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg>',
57
+ "success": '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>',
58
+ "warning": '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
59
+ "error": '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>',
60
+ }
61
+ return icons.get(self.variant)
@@ -0,0 +1,50 @@
1
+ """CronixUI Avatar Component"""
2
+
3
+ from typing import Optional
4
+ from .core import create_el
5
+
6
+
7
+ class Avatar:
8
+ """Avatar component for user images or initials."""
9
+
10
+ SIZES = ("sm", "md", "lg", "xl")
11
+
12
+ def __init__(
13
+ self,
14
+ initials: Optional[str] = None,
15
+ image_url: Optional[str] = None,
16
+ size: str = "md",
17
+ ):
18
+ self.initials = initials
19
+ self.image_url = image_url
20
+ self.size = size if size in self.SIZES else "md"
21
+ self.element = self._render()
22
+
23
+ def _render(self):
24
+ el = create_el("div", "cn-avatar")
25
+ if self.size != "md":
26
+ el.classList.add(f"cn-avatar-{self.size}")
27
+
28
+ if self.image_url:
29
+ img = create_el("img")
30
+ img.setAttribute("src", self.image_url)
31
+ img.setAttribute("alt", self.initials or "Avatar")
32
+ el.appendChild(img)
33
+ elif self.initials:
34
+ el.textContent = self.initials[:2].upper()
35
+
36
+ return el
37
+
38
+
39
+ class AvatarGroup:
40
+ """Group of overlapping avatars."""
41
+
42
+ def __init__(self, *avatars: Avatar):
43
+ self.avatars = avatars
44
+ self.element = self._render()
45
+
46
+ def _render(self):
47
+ el = create_el("div", "cn-avatar-group")
48
+ for avatar in self.avatars:
49
+ el.appendChild(avatar.element)
50
+ return el
@@ -0,0 +1,46 @@
1
+ """CronixUI Badge & Tag Components"""
2
+
3
+ from .core import create_el
4
+
5
+
6
+ class Badge:
7
+ """Badge component for status indicators."""
8
+
9
+ VARIANTS = ("default", "accent", "success", "warning", "error", "info")
10
+
11
+ def __init__(self, text: str, variant: str = "default", solid: bool = False):
12
+ self.text = text
13
+ self.variant = variant if variant in self.VARIANTS else "default"
14
+ self.solid = solid
15
+ self.element = self._render()
16
+
17
+ def _render(self):
18
+ classes = f"cn-badge cn-badge-{self.variant}"
19
+ if self.solid:
20
+ classes += " cn-badge-solid"
21
+ el = create_el("span", classes)
22
+ el.textContent = self.text
23
+ return el
24
+
25
+
26
+ class Tag:
27
+ """Tag component for labels."""
28
+
29
+ def __init__(self, text: str, removable: bool = False, on_remove=None):
30
+ self.text = text
31
+ self.removable = removable
32
+ self.on_remove = on_remove
33
+ self.element = self._render()
34
+
35
+ def _render(self):
36
+ el = create_el("span", "cn-tag")
37
+ el.textContent = self.text
38
+
39
+ if self.removable:
40
+ remove_btn = create_el("span", "cn-tag-remove")
41
+ remove_btn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M18 6L6 18M6 6l12 12"/></svg>'
42
+ if self.on_remove:
43
+ remove_btn.addEventListener("click", self.on_remove)
44
+ el.appendChild(remove_btn)
45
+
46
+ return el
@@ -0,0 +1,64 @@
1
+ """CronixUI Button Component"""
2
+
3
+ from typing import Optional, Callable
4
+ from .core import create_el
5
+ from .tokens import ACCENT, SURFACE_2, SURFACE_3, ERROR, SUCCESS, TEXT
6
+
7
+
8
+ class Button:
9
+ """Button component with variants."""
10
+
11
+ VARIANTS = ("default", "primary", "ghost", "outline", "danger", "success")
12
+ SIZES = ("sm", "md", "lg")
13
+
14
+ def __init__(
15
+ self,
16
+ text: str,
17
+ variant: str = "default",
18
+ size: str = "md",
19
+ icon: bool = False,
20
+ disabled: bool = False,
21
+ onclick: Optional[Callable] = None,
22
+ ):
23
+ self.text = text
24
+ self.variant = variant if variant in self.VARIANTS else "default"
25
+ self.size = size if size in self.SIZES else "md"
26
+ self.icon = icon
27
+ self.disabled = disabled
28
+ self.onclick = onclick
29
+ self.element = self._render()
30
+
31
+ def _render(self):
32
+ el = create_el("button", f"cn-btn cn-btn-{self.variant}")
33
+ if self.size != "md":
34
+ el.classList.add(f"cn-btn-{self.size}")
35
+ if self.icon:
36
+ el.classList.add("cn-btn-icon")
37
+ if self.disabled:
38
+ el.setAttribute("disabled", "")
39
+ el.textContent = self.text
40
+ if self.onclick:
41
+ el.addEventListener("click", self.onclick)
42
+ return el
43
+
44
+ def disable(self):
45
+ self.disabled = True
46
+ self.element.setAttribute("disabled", "")
47
+
48
+ def enable(self):
49
+ self.disabled = False
50
+ self.element.removeAttribute("disabled")
51
+
52
+
53
+ class ButtonGroup:
54
+ """Button group component."""
55
+
56
+ def __init__(self, *buttons: Button):
57
+ self.buttons = buttons
58
+ self.element = self._render()
59
+
60
+ def _render(self):
61
+ el = create_el("div", "cn-btn-group")
62
+ for btn in self.buttons:
63
+ el.appendChild(btn.element)
64
+ return el
@@ -0,0 +1,62 @@
1
+ """CronixUI Card Component"""
2
+
3
+ from typing import Optional
4
+ from .core import create_el
5
+
6
+
7
+ class Card:
8
+ """Card container component."""
9
+
10
+ def __init__(
11
+ self,
12
+ title: Optional[str] = None,
13
+ subtitle: Optional[str] = None,
14
+ clickable: bool = False,
15
+ ):
16
+ self.title = title
17
+ self.subtitle = subtitle
18
+ self.clickable = clickable
19
+ self.element = self._render()
20
+
21
+ def _render(self):
22
+ el = create_el("div", "cn-card")
23
+ if self.clickable:
24
+ el.classList.add("cn-card-clickable")
25
+
26
+ if self.title or self.subtitle:
27
+ header = create_el("div", "cn-card-header")
28
+ if self.title:
29
+ title_el = create_el("h3", "cn-card-title")
30
+ title_el.textContent = self.title
31
+ header.appendChild(title_el)
32
+ el.appendChild(header)
33
+
34
+ if self.subtitle:
35
+ subtitle_el = create_el("p", "cn-card-subtitle")
36
+ subtitle_el.textContent = self.subtitle
37
+ header.appendChild(subtitle_el)
38
+
39
+ return el
40
+
41
+ def set_body(self, content: str):
42
+ body = create_el("div", "cn-card-body")
43
+ body.innerHTML = content
44
+ self.element.appendChild(body)
45
+
46
+ def set_footer(self, content: str):
47
+ footer = create_el("div", "cn-card-footer")
48
+ footer.innerHTML = content
49
+ self.element.appendChild(footer)
50
+
51
+
52
+ class CardIcon:
53
+ """Card with icon."""
54
+
55
+ def __init__(self, icon_svg: str):
56
+ self.icon_svg = icon_svg
57
+ self.element = self._render()
58
+
59
+ def _render(self):
60
+ el = create_el("div", "cn-card-icon")
61
+ el.innerHTML = self.icon_svg
62
+ return el
@@ -0,0 +1,255 @@
1
+ """CronixUI Form Components"""
2
+
3
+ from typing import Optional, Callable
4
+ from .core import create_el
5
+
6
+
7
+ class Input:
8
+ """Input component."""
9
+
10
+ SIZES = ("sm", "md", "lg")
11
+
12
+ def __init__(
13
+ self,
14
+ placeholder: str = "",
15
+ size: str = "md",
16
+ error: bool = False,
17
+ disabled: bool = False,
18
+ icon: Optional[str] = None,
19
+ ):
20
+ self.placeholder = placeholder
21
+ self.size = size if size in self.SIZES else "md"
22
+ self.error = error
23
+ self.disabled = disabled
24
+ self.icon = icon
25
+ self.element = self._render()
26
+
27
+ def _render(self):
28
+ if self.icon:
29
+ wrapper = create_el("div", "cn-input-icon-wrapper")
30
+ input_el = create_el("input", "cn-input")
31
+ input_el.setAttribute("placeholder", self.placeholder)
32
+ if self.disabled:
33
+ input_el.setAttribute("disabled", "")
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")
49
+ if self.size != "md":
50
+ el.classList.add(f"cn-input-{self.size}")
51
+ return el
52
+
53
+
54
+ class Textarea:
55
+ """Textarea component."""
56
+
57
+ def __init__(self, placeholder: str = "", rows: int = 4):
58
+ self.placeholder = placeholder
59
+ self.rows = rows
60
+ self.element = self._render()
61
+
62
+ def _render(self):
63
+ el = create_el("textarea", "cn-input cn-textarea")
64
+ el.setAttribute("placeholder", self.placeholder)
65
+ el.setAttribute("rows", str(self.rows))
66
+ return el
67
+
68
+
69
+ class FormField:
70
+ """Form field with label and error."""
71
+
72
+ def __init__(self, label: str, input_el, error: str = None, help_text: str = None):
73
+ self.label = label
74
+ self.input = input_el
75
+ self.error = error
76
+ self.help_text = help_text
77
+ self.element = self._render()
78
+
79
+ def _render(self):
80
+ group = create_el("div", "cn-form-group")
81
+
82
+ label_el = create_el("label", "cn-form-label")
83
+ label_el.textContent = self.label
84
+ group.appendChild(label_el)
85
+
86
+ if hasattr(self.input, "element"):
87
+ group.appendChild(self.input.element)
88
+ else:
89
+ group.appendChild(self.input)
90
+
91
+ if self.error:
92
+ error_el = create_el("span", "cn-form-error")
93
+ error_el.textContent = self.error
94
+ group.appendChild(error_el)
95
+
96
+ if self.help_text:
97
+ help_el = create_el("span", "cn-form-help")
98
+ help_el.textContent = self.help_text
99
+ group.appendChild(help_el)
100
+
101
+ return group
102
+
103
+
104
+ class Checkbox:
105
+ """Checkbox component."""
106
+
107
+ def __init__(self, label: str, checked: bool = False, disabled: bool = False):
108
+ self.label = label
109
+ self.checked = checked
110
+ self.disabled = disabled
111
+ self.element = self._render()
112
+
113
+ def _render(self):
114
+ el = create_el("label", "cn-checkbox")
115
+ if self.disabled:
116
+ el.classList.add("disabled")
117
+
118
+ input_el = create_el("input")
119
+ input_el.setAttribute("type", "checkbox")
120
+ if self.checked:
121
+ input_el.setAttribute("checked", "")
122
+ if self.disabled:
123
+ input_el.setAttribute("disabled", "")
124
+
125
+ box = create_el("span", "cn-checkbox-box")
126
+ label_el = create_el("span", "cn-checkbox-label")
127
+ label_el.textContent = self.label
128
+
129
+ el.appendChild(input_el)
130
+ el.appendChild(box)
131
+ el.appendChild(label_el)
132
+ return el
133
+
134
+
135
+ class Radio:
136
+ """Radio button component."""
137
+
138
+ def __init__(self, name: str, options: list, selected: str = None):
139
+ self.name = name
140
+ self.options = options
141
+ self.selected = selected
142
+ self.element = self._render()
143
+
144
+ def _render(self):
145
+ container = create_el("div")
146
+ for option in self.options:
147
+ if isinstance(option, tuple):
148
+ value, label = option
149
+ else:
150
+ value = label = option
151
+
152
+ el = create_el("label", "cn-radio")
153
+ input_el = create_el("input")
154
+ input_el.setAttribute("type", "radio")
155
+ input_el.setAttribute("name", self.name)
156
+ input_el.setAttribute("value", value)
157
+ if value == self.selected:
158
+ input_el.setAttribute("checked", "")
159
+
160
+ box = create_el("span", "cn-radio-box")
161
+ label_el = create_el("span", "cn-radio-label")
162
+ label_el.textContent = label
163
+
164
+ el.appendChild(input_el)
165
+ el.appendChild(box)
166
+ el.appendChild(label_el)
167
+ container.appendChild(el)
168
+
169
+ return container
170
+
171
+
172
+ class Select:
173
+ """Select dropdown component."""
174
+
175
+ def __init__(self, options: list, placeholder: str = ""):
176
+ self.options = options
177
+ self.placeholder = placeholder
178
+ self.element = self._render()
179
+
180
+ def _render(self):
181
+ wrapper = create_el("div", "cn-select-wrapper")
182
+ select = create_el("select", "cn-select")
183
+
184
+ if self.placeholder:
185
+ placeholder_option = create_el("option")
186
+ placeholder_option.setAttribute("value", "")
187
+ placeholder_option.setAttribute("disabled", "")
188
+ placeholder_option.setAttribute("selected", "")
189
+ placeholder_option.textContent = self.placeholder
190
+ select.appendChild(placeholder_option)
191
+
192
+ for option in self.options:
193
+ if isinstance(option, tuple):
194
+ value, label = option
195
+ else:
196
+ value = label = option
197
+
198
+ opt = create_el("option")
199
+ opt.setAttribute("value", value)
200
+ opt.textContent = label
201
+ select.appendChild(opt)
202
+
203
+ wrapper.appendChild(select)
204
+ return wrapper
205
+
206
+
207
+ class Slider:
208
+ """Slider component."""
209
+
210
+ def __init__(self, min: float = 0, max: float = 100, value: float = 50):
211
+ self.min = min
212
+ self.max = max
213
+ self.value = value
214
+ self.element = self._render()
215
+
216
+ def _render(self):
217
+ el = create_el("input", "cn-slider")
218
+ el.setAttribute("type", "range")
219
+ el.setAttribute("min", str(self.min))
220
+ el.setAttribute("max", str(self.max))
221
+ el.setAttribute("value", str(self.value))
222
+ return el
223
+
224
+
225
+ class FileInput:
226
+ """File input component."""
227
+
228
+ def __init__(self, accept: str = "", multiple: bool = False):
229
+ self.accept = accept
230
+ self.multiple = multiple
231
+ self.element = self._render()
232
+
233
+ def _render(self):
234
+ wrapper = create_el("div", "cn-file-input")
235
+
236
+ input_el = create_el("input")
237
+ input_el.setAttribute("type", "file")
238
+ if self.accept:
239
+ input_el.setAttribute("accept", self.accept)
240
+ if self.multiple:
241
+ input_el.setAttribute("multiple", "")
242
+
243
+ label = create_el("div", "cn-file-input-label")
244
+ icon = create_el("div", "cn-file-input-icon")
245
+ icon.innerHTML = '<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>'
246
+
247
+ text = create_el("div", "cn-file-input-text")
248
+ text.innerHTML = "Drag and drop or <span>browse</span>"
249
+
250
+ label.appendChild(icon)
251
+ label.appendChild(text)
252
+ wrapper.appendChild(input_el)
253
+ wrapper.appendChild(label)
254
+
255
+ return wrapper