cronixui 1.1.2 → 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 +1 -1
- 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,51 +1,154 @@
|
|
|
1
|
-
"""CronixUI List Component
|
|
1
|
+
"""CronixUI List Component.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Generates HTML for lists with optional icons, titles, subtitles, and actions.
|
|
4
|
+
No browser DOM APIs are used - all output is HTML strings or data structures.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ListElement:
|
|
15
|
+
"""Represents a rendered list element."""
|
|
16
|
+
|
|
17
|
+
tag: str = "div"
|
|
18
|
+
classes: List[str] = field(default_factory=list)
|
|
19
|
+
attributes: Dict[str, str] = field(default_factory=dict)
|
|
20
|
+
inner_html: str = ""
|
|
21
|
+
|
|
22
|
+
def render_html(self) -> str:
|
|
23
|
+
"""Render as HTML string.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Complete HTML for the list element
|
|
27
|
+
"""
|
|
28
|
+
class_str = " ".join(self.classes)
|
|
29
|
+
class_attr = f' class="{class_str}"' if class_str else ""
|
|
30
|
+
attrs_str = "".join(f' {k}="{v}"' for k, v in self.attributes.items())
|
|
31
|
+
return f"<{self.tag}{class_attr}{attrs_str}>{self.inner_html}</{self.tag}>"
|
|
32
|
+
|
|
33
|
+
def render(self) -> "ListElement":
|
|
34
|
+
"""Return self for API compatibility."""
|
|
35
|
+
return self
|
|
5
36
|
|
|
6
37
|
|
|
7
38
|
class List:
|
|
8
|
-
"""List component.
|
|
39
|
+
"""List component for displaying collections of items.
|
|
40
|
+
|
|
41
|
+
Items can be simple strings or dictionaries with optional keys:
|
|
42
|
+
- icon: SVG markup for the item icon
|
|
43
|
+
- title: Primary text for the item
|
|
44
|
+
- subtitle: Secondary/description text
|
|
45
|
+
- actions: HTML string for action buttons/links
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
items: List of items (strings or dicts)
|
|
49
|
+
clickable: Whether items should appear clickable (default: False)
|
|
50
|
+
|
|
51
|
+
Example:
|
|
52
|
+
>>> simple = List(["Item 1", "Item 2", "Item 3"])
|
|
53
|
+
>>> print(simple.render_html())
|
|
54
|
+
<div class="cn-list">
|
|
55
|
+
<div class="cn-list-item"><div class="cn-list-item-content">Item 1</div></div>
|
|
56
|
+
<div class="cn-list-item"><div class="cn-list-item-content">Item 2</div></div>
|
|
57
|
+
<div class="cn-list-item"><div class="cn-list-item-content">Item 3</div></div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
>>> rich = List([
|
|
61
|
+
... {
|
|
62
|
+
... "icon": "<svg>...</svg>",
|
|
63
|
+
... "title": "Dashboard",
|
|
64
|
+
... "subtitle": "Main overview",
|
|
65
|
+
... "actions": "<button>Edit</button>",
|
|
66
|
+
... },
|
|
67
|
+
... {"title": "Settings", "subtitle": "App configuration"},
|
|
68
|
+
... ], clickable=True)
|
|
69
|
+
>>> print(rich.render_html())
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, items: List[Any], clickable: bool = False):
|
|
73
|
+
if not items:
|
|
74
|
+
raise ValueError("items cannot be empty")
|
|
9
75
|
|
|
10
|
-
def __init__(self, items: list, clickable: bool = False):
|
|
11
76
|
self.items = items
|
|
12
77
|
self.clickable = clickable
|
|
13
|
-
self.element = self._render()
|
|
14
78
|
|
|
15
|
-
def
|
|
16
|
-
|
|
79
|
+
def render(self) -> ListElement:
|
|
80
|
+
"""Render the list as a ListElement.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
ListElement containing all list items
|
|
84
|
+
"""
|
|
85
|
+
item_parts = []
|
|
17
86
|
for item in self.items:
|
|
18
|
-
|
|
87
|
+
item_classes = ["cn-list-item"]
|
|
19
88
|
if self.clickable:
|
|
20
|
-
|
|
89
|
+
item_classes.append("cn-list-item-clickable")
|
|
21
90
|
|
|
22
91
|
if isinstance(item, dict):
|
|
23
|
-
|
|
24
|
-
icon_el = create_el("span", "cn-list-item-icon")
|
|
25
|
-
icon_el.innerHTML = icon
|
|
26
|
-
item_el.appendChild(icon_el)
|
|
27
|
-
|
|
28
|
-
content = create_el("div", "cn-list-item-content")
|
|
29
|
-
if title := item.get("title"):
|
|
30
|
-
title_el = create_el("div", "cn-list-item-title")
|
|
31
|
-
title_el.textContent = title
|
|
32
|
-
content.appendChild(title_el)
|
|
33
|
-
|
|
34
|
-
if subtitle := item.get("subtitle"):
|
|
35
|
-
subtitle_el = create_el("div", "cn-list-item-subtitle")
|
|
36
|
-
subtitle_el.textContent = subtitle
|
|
37
|
-
content.appendChild(subtitle_el)
|
|
38
|
-
|
|
39
|
-
item_el.appendChild(content)
|
|
40
|
-
|
|
41
|
-
if actions := item.get("actions"):
|
|
42
|
-
actions_el = create_el("div", "cn-list-item-actions")
|
|
43
|
-
actions_el.innerHTML = actions
|
|
44
|
-
item_el.appendChild(actions_el)
|
|
92
|
+
item_parts.append(self._render_dict_item(item, item_classes))
|
|
45
93
|
else:
|
|
46
|
-
content =
|
|
47
|
-
|
|
48
|
-
|
|
94
|
+
content = self._esc(str(item))
|
|
95
|
+
item_parts.append(
|
|
96
|
+
f'<div class="{" ".join(item_classes)}">'
|
|
97
|
+
f'<div class="cn-list-item-content">{content}</div>'
|
|
98
|
+
f"</div>"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return ListElement(
|
|
102
|
+
classes=["cn-list"],
|
|
103
|
+
inner_html="".join(item_parts),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def _render_dict_item(self, item: dict, item_classes: List[str]) -> str:
|
|
107
|
+
"""Render a single dict-based list item."""
|
|
108
|
+
class_str = " ".join(item_classes)
|
|
109
|
+
parts = [f'<div class="{class_str}">']
|
|
110
|
+
|
|
111
|
+
# Icon
|
|
112
|
+
if icon := item.get("icon"):
|
|
113
|
+
parts.append(f'<span class="cn-list-item-icon">{icon}</span>')
|
|
114
|
+
|
|
115
|
+
# Content
|
|
116
|
+
content_parts = []
|
|
117
|
+
if title := item.get("title"):
|
|
118
|
+
content_parts.append(
|
|
119
|
+
f'<div class="cn-list-item-title">{self._esc(title)}</div>'
|
|
120
|
+
)
|
|
121
|
+
if subtitle := item.get("subtitle"):
|
|
122
|
+
content_parts.append(
|
|
123
|
+
f'<div class="cn-list-item-subtitle">{self._esc(subtitle)}</div>'
|
|
124
|
+
)
|
|
125
|
+
if content_parts:
|
|
126
|
+
parts.append(
|
|
127
|
+
f'<div class="cn-list-item-content">{"".join(content_parts)}</div>'
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Actions
|
|
131
|
+
if actions := item.get("actions"):
|
|
132
|
+
parts.append(f'<div class="cn-list-item-actions">{actions}</div>')
|
|
133
|
+
|
|
134
|
+
parts.append("</div>")
|
|
135
|
+
return "".join(parts)
|
|
136
|
+
|
|
137
|
+
def render_html(self) -> str:
|
|
138
|
+
"""Render the list as an HTML string.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
HTML string representation of the list
|
|
142
|
+
"""
|
|
143
|
+
return self.render().render_html()
|
|
49
144
|
|
|
50
|
-
|
|
51
|
-
|
|
145
|
+
@staticmethod
|
|
146
|
+
def _esc(text: str) -> str:
|
|
147
|
+
"""Escape HTML special characters."""
|
|
148
|
+
return (
|
|
149
|
+
text.replace("&", "&")
|
|
150
|
+
.replace("<", "<")
|
|
151
|
+
.replace(">", ">")
|
|
152
|
+
.replace('"', """)
|
|
153
|
+
.replace("'", "'")
|
|
154
|
+
)
|
|
@@ -1,36 +1,126 @@
|
|
|
1
|
-
"""CronixUI Loading Components
|
|
1
|
+
"""CronixUI Loading Components.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Generates HTML for spinners and skeleton loading placeholders.
|
|
4
|
+
No browser DOM APIs are used - all output is HTML strings or data structures.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class LoadingElement:
|
|
15
|
+
"""Represents a rendered loading element."""
|
|
16
|
+
|
|
17
|
+
tag: str = "div"
|
|
18
|
+
classes: List[str] = field(default_factory=list)
|
|
19
|
+
attributes: Dict[str, str] = field(default_factory=dict)
|
|
20
|
+
inner_html: str = ""
|
|
21
|
+
|
|
22
|
+
def render_html(self) -> str:
|
|
23
|
+
"""Render as HTML string.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Complete HTML for the loading element
|
|
27
|
+
"""
|
|
28
|
+
class_str = " ".join(self.classes)
|
|
29
|
+
class_attr = f' class="{class_str}"' if class_str else ""
|
|
30
|
+
attrs_str = "".join(f' {k}="{v}"' for k, v in self.attributes.items())
|
|
31
|
+
return f"<{self.tag}{class_attr}{attrs_str}>{self.inner_html}</{self.tag}>"
|
|
32
|
+
|
|
33
|
+
def render(self) -> "LoadingElement":
|
|
34
|
+
"""Return self for API compatibility."""
|
|
35
|
+
return self
|
|
4
36
|
|
|
5
37
|
|
|
6
38
|
class Spinner:
|
|
7
|
-
"""Loading spinner component.
|
|
39
|
+
"""Loading spinner component.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
size: Spinner size - sm, md, lg (default: md)
|
|
43
|
+
|
|
44
|
+
Example:
|
|
45
|
+
>>> spinner = Spinner(size="lg")
|
|
46
|
+
>>> print(spinner.render_html())
|
|
47
|
+
<div class="cn-spinner cn-spinner-lg"></div>
|
|
48
|
+
"""
|
|
8
49
|
|
|
9
50
|
SIZES = ("sm", "md", "lg")
|
|
10
51
|
|
|
11
52
|
def __init__(self, size: str = "md"):
|
|
12
|
-
|
|
13
|
-
|
|
53
|
+
if size not in self.SIZES:
|
|
54
|
+
raise ValueError(f"Invalid size '{size}'. Must be one of {self.SIZES}")
|
|
55
|
+
|
|
56
|
+
self.size = size
|
|
57
|
+
|
|
58
|
+
def render(self) -> LoadingElement:
|
|
59
|
+
"""Render the spinner as a LoadingElement.
|
|
14
60
|
|
|
15
|
-
|
|
16
|
-
|
|
61
|
+
Returns:
|
|
62
|
+
LoadingElement representing the spinner
|
|
63
|
+
"""
|
|
64
|
+
classes = ["cn-spinner"]
|
|
17
65
|
if self.size != "md":
|
|
18
|
-
|
|
19
|
-
|
|
66
|
+
classes.append(f"cn-spinner-{self.size}")
|
|
67
|
+
|
|
68
|
+
return LoadingElement(classes=classes)
|
|
69
|
+
|
|
70
|
+
def render_html(self) -> str:
|
|
71
|
+
"""Render the spinner as an HTML string.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
HTML string representation of the spinner
|
|
75
|
+
"""
|
|
76
|
+
return self.render().render_html()
|
|
20
77
|
|
|
21
78
|
|
|
22
79
|
class Skeleton:
|
|
23
|
-
"""Skeleton loading placeholder.
|
|
80
|
+
"""Skeleton loading placeholder for content while it loads.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
variant: Skeleton type - text, title, avatar (default: text)
|
|
84
|
+
width: Optional CSS width for the skeleton (e.g. "200px", "50%")
|
|
85
|
+
|
|
86
|
+
Example:
|
|
87
|
+
>>> skeleton = Skeleton(variant="title", width="300px")
|
|
88
|
+
>>> print(skeleton.render_html())
|
|
89
|
+
<div class="cn-skeleton cn-skeleton-title" style="width: 300px;"></div>
|
|
90
|
+
"""
|
|
24
91
|
|
|
25
92
|
VARIANTS = ("text", "title", "avatar")
|
|
26
93
|
|
|
27
|
-
def __init__(self, variant: str = "text", width: str = None):
|
|
28
|
-
|
|
94
|
+
def __init__(self, variant: str = "text", width: Optional[str] = None):
|
|
95
|
+
if variant not in self.VARIANTS:
|
|
96
|
+
raise ValueError(
|
|
97
|
+
f"Invalid variant '{variant}'. Must be one of {self.VARIANTS}"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
self.variant = variant
|
|
29
101
|
self.width = width
|
|
30
|
-
self.element = self._render()
|
|
31
102
|
|
|
32
|
-
def
|
|
33
|
-
|
|
103
|
+
def render(self) -> LoadingElement:
|
|
104
|
+
"""Render the skeleton as a LoadingElement.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
LoadingElement representing the skeleton placeholder
|
|
108
|
+
"""
|
|
109
|
+
classes = ["cn-skeleton", f"cn-skeleton-{self.variant}"]
|
|
110
|
+
|
|
111
|
+
attrs: Dict[str, str] = {}
|
|
34
112
|
if self.width:
|
|
35
|
-
|
|
36
|
-
|
|
113
|
+
attrs["style"] = f"width: {self.width};"
|
|
114
|
+
|
|
115
|
+
return LoadingElement(
|
|
116
|
+
classes=classes,
|
|
117
|
+
attributes=attrs,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def render_html(self) -> str:
|
|
121
|
+
"""Render the skeleton as an HTML string.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
HTML string representation of the skeleton placeholder
|
|
125
|
+
"""
|
|
126
|
+
return self.render().render_html()
|
|
@@ -1,10 +1,63 @@
|
|
|
1
|
-
"""CronixUI Progress Components
|
|
1
|
+
"""CronixUI Progress Components.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Generates HTML for progress bars and stat/metric displays.
|
|
4
|
+
No browser DOM APIs are used - all output is HTML strings or data structures.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ProgressElement:
|
|
15
|
+
"""Represents a rendered progress/stat element."""
|
|
16
|
+
|
|
17
|
+
tag: str = "div"
|
|
18
|
+
classes: List[str] = field(default_factory=list)
|
|
19
|
+
attributes: Dict[str, str] = field(default_factory=dict)
|
|
20
|
+
inner_html: str = ""
|
|
21
|
+
|
|
22
|
+
def render_html(self) -> str:
|
|
23
|
+
"""Render as HTML string.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Complete HTML for the element
|
|
27
|
+
"""
|
|
28
|
+
class_str = " ".join(self.classes)
|
|
29
|
+
class_attr = f' class="{class_str}"' if class_str else ""
|
|
30
|
+
attrs_str = "".join(f' {k}="{v}"' for k, v in self.attributes.items())
|
|
31
|
+
return f"<{self.tag}{class_attr}{attrs_str}>{self.inner_html}</{self.tag}>"
|
|
32
|
+
|
|
33
|
+
def render(self) -> "ProgressElement":
|
|
34
|
+
"""Return self for API compatibility."""
|
|
35
|
+
return self
|
|
4
36
|
|
|
5
37
|
|
|
6
38
|
class Progress:
|
|
7
|
-
"""Progress bar component.
|
|
39
|
+
"""Progress bar component for showing completion percentage.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
value: Current progress value (default: 0)
|
|
43
|
+
max: Maximum value (default: 100)
|
|
44
|
+
variant: Progress variant - default, success, warning, error (default: default)
|
|
45
|
+
size: Progress bar size - sm, md, lg (default: md)
|
|
46
|
+
show_label: Whether to show value/max text (default: False)
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
>>> progress = Progress(value=75, max=100, show_label=True)
|
|
50
|
+
>>> print(progress.render_html())
|
|
51
|
+
<div>
|
|
52
|
+
<div class="cn-progress-label"><span>75</span><span>100</span></div>
|
|
53
|
+
<div class="cn-progress">
|
|
54
|
+
<div class="cn-progress-bar" style="width: 75.0%;"></div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
>>> success = Progress(value=100, variant="success", size="lg")
|
|
59
|
+
>>> print(success.render_html())
|
|
60
|
+
"""
|
|
8
61
|
|
|
9
62
|
VARIANTS = ("default", "success", "warning", "error")
|
|
10
63
|
SIZES = ("sm", "md", "lg")
|
|
@@ -17,74 +70,163 @@ class Progress:
|
|
|
17
70
|
size: str = "md",
|
|
18
71
|
show_label: bool = False,
|
|
19
72
|
):
|
|
73
|
+
if max <= 0:
|
|
74
|
+
raise ValueError("max must be greater than 0")
|
|
75
|
+
if value < 0:
|
|
76
|
+
raise ValueError("value cannot be negative")
|
|
77
|
+
if variant not in self.VARIANTS:
|
|
78
|
+
raise ValueError(
|
|
79
|
+
f"Invalid variant '{variant}'. Must be one of {self.VARIANTS}"
|
|
80
|
+
)
|
|
81
|
+
if size not in self.SIZES:
|
|
82
|
+
raise ValueError(f"Invalid size '{size}'. Must be one of {self.SIZES}")
|
|
83
|
+
|
|
20
84
|
self.value = value
|
|
21
85
|
self.max = max
|
|
22
|
-
self.variant = variant
|
|
23
|
-
self.size = size
|
|
86
|
+
self.variant = variant
|
|
87
|
+
self.size = size
|
|
24
88
|
self.show_label = show_label
|
|
25
|
-
self.element = self._render()
|
|
26
89
|
|
|
27
|
-
def
|
|
28
|
-
|
|
90
|
+
def render(self) -> ProgressElement:
|
|
91
|
+
"""Render the progress bar as a ProgressElement.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
ProgressElement representing the progress bar
|
|
95
|
+
"""
|
|
96
|
+
parts = []
|
|
29
97
|
|
|
98
|
+
# Label
|
|
30
99
|
if self.show_label:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
progress = create_el("div", "cn-progress")
|
|
100
|
+
parts.append(
|
|
101
|
+
f'<div class="cn-progress-label">'
|
|
102
|
+
f"<span>{int(self.value)}</span>"
|
|
103
|
+
f"<span>{int(self.max)}</span>"
|
|
104
|
+
f"</div>"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Progress bar container
|
|
108
|
+
bar_classes = ["cn-progress"]
|
|
41
109
|
if self.size != "md":
|
|
42
|
-
|
|
110
|
+
bar_classes.append(f"cn-progress-{self.size}")
|
|
43
111
|
if self.variant != "default":
|
|
44
|
-
|
|
112
|
+
bar_classes.append(f"cn-progress-{self.variant}")
|
|
45
113
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
114
|
+
# Bar fill
|
|
115
|
+
percentage = (self.value / self.max) * 100
|
|
116
|
+
bar_fill = (
|
|
117
|
+
f'<div class="cn-progress-bar" style="width: {percentage}%;"></div>'
|
|
118
|
+
)
|
|
49
119
|
|
|
50
|
-
|
|
51
|
-
|
|
120
|
+
parts.append(
|
|
121
|
+
f'<div class="{" ".join(bar_classes)}">{bar_fill}</div>'
|
|
122
|
+
)
|
|
52
123
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
124
|
+
return ProgressElement(inner_html="".join(parts))
|
|
125
|
+
|
|
126
|
+
def render_html(self) -> str:
|
|
127
|
+
"""Render the progress bar as an HTML string.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
HTML string representation of the progress bar
|
|
131
|
+
"""
|
|
132
|
+
return self.render().render_html()
|
|
133
|
+
|
|
134
|
+
def with_value(self, value: float) -> "Progress":
|
|
135
|
+
"""Return a new Progress with updated value (immutable pattern).
|
|
136
|
+
|
|
137
|
+
Since components generate strings, we can't update in place after rendering.
|
|
138
|
+
Use this to create an updated copy.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
value: New progress value
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
New Progress instance with the updated value
|
|
145
|
+
"""
|
|
146
|
+
return Progress(
|
|
147
|
+
value=value,
|
|
148
|
+
max=self.max,
|
|
149
|
+
variant=self.variant,
|
|
150
|
+
size=self.size,
|
|
151
|
+
show_label=self.show_label,
|
|
152
|
+
)
|
|
58
153
|
|
|
59
154
|
|
|
60
155
|
class Stat:
|
|
61
|
-
"""Stat component for displaying metrics.
|
|
156
|
+
"""Stat component for displaying metrics with optional delta/ trend.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
value: Primary stat value (e.g. "1,234")
|
|
160
|
+
label: Stat label (e.g. "Total Users")
|
|
161
|
+
delta: Optional delta text (e.g. "+12%")
|
|
162
|
+
delta_type: Optional delta type for styling (e.g. "positive", "negative")
|
|
163
|
+
|
|
164
|
+
Example:
|
|
165
|
+
>>> stat = Stat(value="1,234", label="Total Users", delta="+12%", delta_type="positive")
|
|
166
|
+
>>> print(stat.render_html())
|
|
167
|
+
<div class="cn-stat">
|
|
168
|
+
<div class="cn-stat-value">1,234</div>
|
|
169
|
+
<div class="cn-stat-label">Total Users</div>
|
|
170
|
+
<div class="cn-stat-delta cn-stat-delta-positive">+12%</div>
|
|
171
|
+
</div>
|
|
172
|
+
"""
|
|
62
173
|
|
|
63
174
|
def __init__(
|
|
64
|
-
self,
|
|
175
|
+
self,
|
|
176
|
+
value: str,
|
|
177
|
+
label: str,
|
|
178
|
+
delta: Optional[str] = None,
|
|
179
|
+
delta_type: Optional[str] = None,
|
|
65
180
|
):
|
|
181
|
+
if not value:
|
|
182
|
+
raise ValueError("value cannot be empty")
|
|
183
|
+
if not label:
|
|
184
|
+
raise ValueError("label cannot be empty")
|
|
185
|
+
|
|
66
186
|
self.value = value
|
|
67
187
|
self.label = label
|
|
68
188
|
self.delta = delta
|
|
69
189
|
self.delta_type = delta_type
|
|
70
|
-
self.element = self._render()
|
|
71
|
-
|
|
72
|
-
def _render(self):
|
|
73
|
-
el = create_el("div", "cn-stat")
|
|
74
190
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
el.appendChild(value_el)
|
|
191
|
+
def render(self) -> ProgressElement:
|
|
192
|
+
"""Render the stat as a ProgressElement.
|
|
78
193
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
194
|
+
Returns:
|
|
195
|
+
ProgressElement representing the stat display
|
|
196
|
+
"""
|
|
197
|
+
parts = [
|
|
198
|
+
f'<div class="cn-stat-value">{self._esc(self.value)}</div>',
|
|
199
|
+
f'<div class="cn-stat-label">{self._esc(self.label)}</div>',
|
|
200
|
+
]
|
|
82
201
|
|
|
83
202
|
if self.delta:
|
|
84
|
-
|
|
85
|
-
|
|
203
|
+
delta_classes = ["cn-stat-delta"]
|
|
204
|
+
if self.delta_type:
|
|
205
|
+
delta_classes.append(f"cn-stat-delta-{self.delta_type}")
|
|
206
|
+
parts.append(
|
|
207
|
+
f'<div class="{" ".join(delta_classes)}">{self._esc(self.delta)}</div>'
|
|
86
208
|
)
|
|
87
|
-
delta_el.textContent = self.delta
|
|
88
|
-
el.appendChild(delta_el)
|
|
89
209
|
|
|
90
|
-
return
|
|
210
|
+
return ProgressElement(
|
|
211
|
+
classes=["cn-stat"],
|
|
212
|
+
inner_html="".join(parts),
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def render_html(self) -> str:
|
|
216
|
+
"""Render the stat as an HTML string.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
HTML string representation of the stat
|
|
220
|
+
"""
|
|
221
|
+
return self.render().render_html()
|
|
222
|
+
|
|
223
|
+
@staticmethod
|
|
224
|
+
def _esc(text: str) -> str:
|
|
225
|
+
"""Escape HTML special characters."""
|
|
226
|
+
return (
|
|
227
|
+
text.replace("&", "&")
|
|
228
|
+
.replace("<", "<")
|
|
229
|
+
.replace(">", ">")
|
|
230
|
+
.replace('"', """)
|
|
231
|
+
.replace("'", "'")
|
|
232
|
+
)
|