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,46 +1,183 @@
|
|
|
1
|
-
"""CronixUI Badge & Tag Components
|
|
1
|
+
"""CronixUI Badge & Tag Components.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Generates HTML for status badges and removable tags.
|
|
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 Callable, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class BadgeElement:
|
|
15
|
+
"""Represents a rendered badge or tag element."""
|
|
16
|
+
|
|
17
|
+
tag: str = "span"
|
|
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 badge 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) -> "BadgeElement":
|
|
34
|
+
"""Return self for API compatibility."""
|
|
35
|
+
return self
|
|
4
36
|
|
|
5
37
|
|
|
6
38
|
class Badge:
|
|
7
|
-
"""Badge component for status indicators.
|
|
39
|
+
"""Badge component for status indicators and labels.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
text: Badge text content
|
|
43
|
+
variant: Badge variant - default, accent, success, warning, error, info (default: default)
|
|
44
|
+
solid: Whether to use solid styling (default: False)
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
>>> badge = Badge("Active", variant="success")
|
|
48
|
+
>>> print(badge.render_html())
|
|
49
|
+
<span class="cn-badge cn-badge-success">Active</span>
|
|
50
|
+
|
|
51
|
+
>>> solid_badge = Badge("New", variant="accent", solid=True)
|
|
52
|
+
>>> print(solid_badge.render_html())
|
|
53
|
+
<span class="cn-badge cn-badge-accent cn-badge-solid">New</span>
|
|
54
|
+
"""
|
|
8
55
|
|
|
9
56
|
VARIANTS = ("default", "accent", "success", "warning", "error", "info")
|
|
10
57
|
|
|
11
|
-
def __init__(
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
text: str,
|
|
61
|
+
variant: str = "default",
|
|
62
|
+
solid: bool = False,
|
|
63
|
+
):
|
|
64
|
+
if not text:
|
|
65
|
+
raise ValueError("text cannot be empty")
|
|
66
|
+
if variant not in self.VARIANTS:
|
|
67
|
+
raise ValueError(
|
|
68
|
+
f"Invalid variant '{variant}'. Must be one of {self.VARIANTS}"
|
|
69
|
+
)
|
|
70
|
+
|
|
12
71
|
self.text = text
|
|
13
|
-
self.variant = variant
|
|
72
|
+
self.variant = variant
|
|
14
73
|
self.solid = solid
|
|
15
|
-
self.element = self._render()
|
|
16
74
|
|
|
17
|
-
def
|
|
18
|
-
|
|
75
|
+
def render(self) -> BadgeElement:
|
|
76
|
+
"""Render the badge as a BadgeElement data structure.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
BadgeElement representing the badge
|
|
80
|
+
"""
|
|
81
|
+
classes = ["cn-badge", f"cn-badge-{self.variant}"]
|
|
19
82
|
if self.solid:
|
|
20
|
-
classes
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
83
|
+
classes.append("cn-badge-solid")
|
|
84
|
+
|
|
85
|
+
return BadgeElement(
|
|
86
|
+
classes=classes,
|
|
87
|
+
inner_html=self._esc(self.text),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def render_html(self) -> str:
|
|
91
|
+
"""Render the badge as an HTML string.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
HTML string representation of the badge
|
|
95
|
+
"""
|
|
96
|
+
return self.render().render_html()
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def _esc(text: str) -> str:
|
|
100
|
+
"""Escape HTML special characters."""
|
|
101
|
+
return (
|
|
102
|
+
text.replace("&", "&")
|
|
103
|
+
.replace("<", "<")
|
|
104
|
+
.replace(">", ">")
|
|
105
|
+
.replace('"', """)
|
|
106
|
+
.replace("'", "'")
|
|
107
|
+
)
|
|
24
108
|
|
|
25
109
|
|
|
26
110
|
class Tag:
|
|
27
|
-
"""Tag component for labels.
|
|
111
|
+
"""Tag component for labels with optional removal.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
text: Tag text content
|
|
115
|
+
removable: Whether to show a remove button (default: False)
|
|
116
|
+
on_remove: Callback name for removal (stored as data attribute)
|
|
117
|
+
|
|
118
|
+
Example:
|
|
119
|
+
>>> tag = Tag("Python")
|
|
120
|
+
>>> print(tag.render_html())
|
|
121
|
+
<span class="cn-tag">Python</span>
|
|
122
|
+
|
|
123
|
+
>>> removable = Tag("JavaScript", removable=True, on_remove="handleRemove")
|
|
124
|
+
>>> print(removable.render_html())
|
|
125
|
+
<span class="cn-tag" data-on-remove="handleRemove">JavaScript<span class="cn-tag-remove">...</span></span>
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
_REMOVE_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M18 6L6 18M6 6l12 12"/></svg>'
|
|
129
|
+
|
|
130
|
+
def __init__(
|
|
131
|
+
self,
|
|
132
|
+
text: str,
|
|
133
|
+
removable: bool = False,
|
|
134
|
+
on_remove: Optional[str] = None,
|
|
135
|
+
):
|
|
136
|
+
if not text:
|
|
137
|
+
raise ValueError("text cannot be empty")
|
|
28
138
|
|
|
29
|
-
def __init__(self, text: str, removable: bool = False, on_remove=None):
|
|
30
139
|
self.text = text
|
|
31
140
|
self.removable = removable
|
|
32
141
|
self.on_remove = on_remove
|
|
33
|
-
self.element = self._render()
|
|
34
142
|
|
|
35
|
-
def
|
|
36
|
-
|
|
37
|
-
|
|
143
|
+
def render(self) -> BadgeElement:
|
|
144
|
+
"""Render the tag as a BadgeElement data structure.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
BadgeElement representing the tag
|
|
148
|
+
"""
|
|
149
|
+
attrs: Dict[str, str] = {}
|
|
150
|
+
if self.removable and self.on_remove:
|
|
151
|
+
attrs["data-on-remove"] = self.on_remove
|
|
152
|
+
|
|
153
|
+
inner_parts = [self._esc(self.text)]
|
|
38
154
|
|
|
39
155
|
if self.removable:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
156
|
+
inner_parts.append(
|
|
157
|
+
f'<span class="cn-tag-remove">{self._REMOVE_ICON}</span>'
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
return BadgeElement(
|
|
161
|
+
classes=["cn-tag"],
|
|
162
|
+
attributes=attrs,
|
|
163
|
+
inner_html="".join(inner_parts),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def render_html(self) -> str:
|
|
167
|
+
"""Render the tag as an HTML string.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
HTML string representation of the tag
|
|
171
|
+
"""
|
|
172
|
+
return self.render().render_html()
|
|
45
173
|
|
|
46
|
-
|
|
174
|
+
@staticmethod
|
|
175
|
+
def _esc(text: str) -> str:
|
|
176
|
+
"""Escape HTML special characters."""
|
|
177
|
+
return (
|
|
178
|
+
text.replace("&", "&")
|
|
179
|
+
.replace("<", "<")
|
|
180
|
+
.replace(">", ">")
|
|
181
|
+
.replace('"', """)
|
|
182
|
+
.replace("'", "'")
|
|
183
|
+
)
|
|
@@ -1,12 +1,44 @@
|
|
|
1
1
|
"""CronixUI Button Component"""
|
|
2
2
|
|
|
3
|
-
from typing import Optional, Callable
|
|
4
|
-
from
|
|
5
|
-
|
|
3
|
+
from typing import Optional, Callable, Dict, Any, List
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class ButtonElement:
|
|
9
|
+
"""Represents a rendered button element."""
|
|
10
|
+
|
|
11
|
+
tag: str = "button"
|
|
12
|
+
classes: List[str] = field(default_factory=list)
|
|
13
|
+
attributes: Dict[str, str] = field(default_factory=dict)
|
|
14
|
+
text: str = ""
|
|
15
|
+
onclick: Optional[Callable] = field(default=None, repr=False)
|
|
16
|
+
|
|
17
|
+
def render(self) -> str:
|
|
18
|
+
"""Render the button as HTML string."""
|
|
19
|
+
class_str = " ".join(self.classes)
|
|
20
|
+
attrs_str = " ".join(f'{k}="{v}"' for k, v in self.attributes.items())
|
|
21
|
+
attrs_str = f" {attrs_str}" if attrs_str else ""
|
|
22
|
+
|
|
23
|
+
return f'<{self.tag} class="{class_str}"{attrs_str}>{self.text}</{self.tag}>'
|
|
6
24
|
|
|
7
25
|
|
|
8
26
|
class Button:
|
|
9
|
-
"""Button component with variants.
|
|
27
|
+
"""Button component with variants.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
text: Button text
|
|
31
|
+
variant: Button variant (default, primary, ghost, outline, danger, success)
|
|
32
|
+
size: Button size (sm, md, lg)
|
|
33
|
+
icon: Whether button is icon-only
|
|
34
|
+
disabled: Whether button is disabled
|
|
35
|
+
onclick: Click handler callback (for documentation purposes)
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
>>> btn = Button("Click me", variant="primary")
|
|
39
|
+
>>> print(btn.render())
|
|
40
|
+
<button class="cn-btn cn-btn-primary">Click me</button>
|
|
41
|
+
"""
|
|
10
42
|
|
|
11
43
|
VARIANTS = ("default", "primary", "ghost", "outline", "danger", "success")
|
|
12
44
|
SIZES = ("sm", "md", "lg")
|
|
@@ -20,45 +52,82 @@ class Button:
|
|
|
20
52
|
disabled: bool = False,
|
|
21
53
|
onclick: Optional[Callable] = None,
|
|
22
54
|
):
|
|
55
|
+
if variant not in self.VARIANTS:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f"Invalid variant '{variant}'. Must be one of {self.VARIANTS}"
|
|
58
|
+
)
|
|
59
|
+
if size not in self.SIZES:
|
|
60
|
+
raise ValueError(f"Invalid size '{size}'. Must be one of {self.SIZES}")
|
|
61
|
+
|
|
23
62
|
self.text = text
|
|
24
|
-
self.variant = variant
|
|
25
|
-
self.size = size
|
|
63
|
+
self.variant = variant
|
|
64
|
+
self.size = size
|
|
26
65
|
self.icon = icon
|
|
27
66
|
self.disabled = disabled
|
|
28
67
|
self.onclick = onclick
|
|
29
|
-
self.element = self._render()
|
|
30
68
|
|
|
31
|
-
def
|
|
32
|
-
|
|
69
|
+
def render(self) -> ButtonElement:
|
|
70
|
+
"""Render the button as a ButtonElement.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
ButtonElement object representing the rendered button
|
|
74
|
+
"""
|
|
75
|
+
classes = ["cn-btn", f"cn-btn-{self.variant}"]
|
|
76
|
+
|
|
33
77
|
if self.size != "md":
|
|
34
|
-
|
|
78
|
+
classes.append(f"cn-btn-{self.size}")
|
|
79
|
+
|
|
35
80
|
if self.icon:
|
|
36
|
-
|
|
81
|
+
classes.append("cn-btn-icon")
|
|
82
|
+
|
|
83
|
+
attributes: Dict[str, str] = {}
|
|
37
84
|
if self.disabled:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
85
|
+
attributes["disabled"] = ""
|
|
86
|
+
|
|
87
|
+
return ButtonElement(
|
|
88
|
+
classes=classes,
|
|
89
|
+
attributes=attributes,
|
|
90
|
+
text=self.text,
|
|
91
|
+
onclick=self.onclick,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def render_html(self) -> str:
|
|
95
|
+
"""Render the button as HTML string.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
HTML string representation of the button
|
|
99
|
+
"""
|
|
100
|
+
return self.render().render()
|
|
43
101
|
|
|
44
|
-
def disable(self):
|
|
102
|
+
def disable(self) -> None:
|
|
103
|
+
"""Disable the button."""
|
|
45
104
|
self.disabled = True
|
|
46
|
-
self.element.setAttribute("disabled", "")
|
|
47
105
|
|
|
48
|
-
def enable(self):
|
|
106
|
+
def enable(self) -> None:
|
|
107
|
+
"""Enable the button."""
|
|
49
108
|
self.disabled = False
|
|
50
|
-
self.element.removeAttribute("disabled")
|
|
51
109
|
|
|
52
110
|
|
|
53
111
|
class ButtonGroup:
|
|
54
|
-
"""Button group component.
|
|
112
|
+
"""Button group component.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
*buttons: Button instances to include in the group
|
|
116
|
+
|
|
117
|
+
Example:
|
|
118
|
+
>>> group = ButtonGroup(Button("Left"), Button("Right"))
|
|
119
|
+
>>> print(group.render_html())
|
|
120
|
+
"""
|
|
55
121
|
|
|
56
122
|
def __init__(self, *buttons: Button):
|
|
57
123
|
self.buttons = buttons
|
|
58
|
-
self.element = self._render()
|
|
59
124
|
|
|
60
|
-
def
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
125
|
+
def render_html(self) -> str:
|
|
126
|
+
"""Render the button group as HTML string.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
HTML string representation of the button group
|
|
130
|
+
"""
|
|
131
|
+
buttons_html = "".join(btn.render_html() for btn in self.buttons)
|
|
132
|
+
return f'<div class="cn-btn-group">{buttons_html}</div>'
|
|
133
|
+
|
|
@@ -1,62 +1,235 @@
|
|
|
1
|
-
"""CronixUI Card Component
|
|
1
|
+
"""CronixUI Card Component.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Generates HTML for cards with header, body, footer, and optional icon variants.
|
|
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 CardElement:
|
|
15
|
+
"""Represents a rendered card 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 the card as HTML string.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Complete HTML for the card 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) -> "CardElement":
|
|
34
|
+
"""Return self for API compatibility."""
|
|
35
|
+
return self
|
|
5
36
|
|
|
6
37
|
|
|
7
38
|
class Card:
|
|
8
|
-
"""Card container component.
|
|
39
|
+
"""Card container component with optional header, body, and footer sections.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
title: Optional card title
|
|
43
|
+
subtitle: Optional card subtitle
|
|
44
|
+
clickable: Whether card should appear clickable (default: False)
|
|
45
|
+
body: Optional card body content (HTML string)
|
|
46
|
+
footer: Optional card footer content (HTML string)
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
>>> card = Card(
|
|
50
|
+
... title="Welcome",
|
|
51
|
+
... subtitle="Getting started guide",
|
|
52
|
+
... body="<p>Card body content here.</p>",
|
|
53
|
+
... footer="<a href='#'>Learn more</a>",
|
|
54
|
+
... )
|
|
55
|
+
>>> print(card.render_html())
|
|
56
|
+
<div class="cn-card">
|
|
57
|
+
<div class="cn-card-header">
|
|
58
|
+
<h3 class="cn-card-title">Welcome</h3>
|
|
59
|
+
<p class="cn-card-subtitle">Getting started guide</p>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="cn-card-body"><p>Card body content here.</p></div>
|
|
62
|
+
<div class="cn-card-footer"><a href='#'>Learn more</a></div>
|
|
63
|
+
</div>
|
|
64
|
+
"""
|
|
9
65
|
|
|
10
66
|
def __init__(
|
|
11
67
|
self,
|
|
12
68
|
title: Optional[str] = None,
|
|
13
69
|
subtitle: Optional[str] = None,
|
|
14
70
|
clickable: bool = False,
|
|
71
|
+
body: Optional[str] = None,
|
|
72
|
+
footer: Optional[str] = None,
|
|
15
73
|
):
|
|
16
74
|
self.title = title
|
|
17
75
|
self.subtitle = subtitle
|
|
18
76
|
self.clickable = clickable
|
|
19
|
-
self.
|
|
77
|
+
self._body = body
|
|
78
|
+
self._footer = footer
|
|
20
79
|
|
|
21
|
-
def
|
|
22
|
-
|
|
80
|
+
def render(self) -> CardElement:
|
|
81
|
+
"""Render the card as a CardElement data structure.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
CardElement representing the complete card
|
|
85
|
+
"""
|
|
86
|
+
classes = ["cn-card"]
|
|
23
87
|
if self.clickable:
|
|
24
|
-
|
|
88
|
+
classes.append("cn-card-clickable")
|
|
89
|
+
|
|
90
|
+
parts = []
|
|
25
91
|
|
|
92
|
+
# Header
|
|
26
93
|
if self.title or self.subtitle:
|
|
27
|
-
|
|
94
|
+
header_parts = []
|
|
28
95
|
if self.title:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
el.appendChild(header)
|
|
33
|
-
|
|
96
|
+
header_parts.append(
|
|
97
|
+
f'<h3 class="cn-card-title">{self._esc(self.title)}</h3>'
|
|
98
|
+
)
|
|
34
99
|
if self.subtitle:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
100
|
+
header_parts.append(
|
|
101
|
+
f'<p class="cn-card-subtitle">{self._esc(self.subtitle)}</p>'
|
|
102
|
+
)
|
|
103
|
+
parts.append(f'<div class="cn-card-header">{"".join(header_parts)}</div>')
|
|
104
|
+
|
|
105
|
+
# Body
|
|
106
|
+
if self._body is not None:
|
|
107
|
+
parts.append(f'<div class="cn-card-body">{self._body}</div>')
|
|
108
|
+
|
|
109
|
+
# Footer
|
|
110
|
+
if self._footer is not None:
|
|
111
|
+
parts.append(f'<div class="cn-card-footer">{self._footer}</div>')
|
|
112
|
+
|
|
113
|
+
return CardElement(
|
|
114
|
+
classes=classes,
|
|
115
|
+
inner_html="".join(parts),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def render_html(self) -> str:
|
|
119
|
+
"""Render the card as an HTML string.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
HTML string representation of the card
|
|
123
|
+
"""
|
|
124
|
+
return self.render().render_html()
|
|
125
|
+
|
|
126
|
+
def with_body(self, content: str) -> "Card":
|
|
127
|
+
"""Set the card body content and return self for chaining.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
content: HTML string for the card body
|
|
38
131
|
|
|
39
|
-
|
|
132
|
+
Returns:
|
|
133
|
+
Self for method chaining
|
|
40
134
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
self.
|
|
135
|
+
Example:
|
|
136
|
+
>>> card = Card(title="Title").with_body("<p>Body content</p>")
|
|
137
|
+
"""
|
|
138
|
+
self._body = content
|
|
139
|
+
return self
|
|
45
140
|
|
|
46
|
-
def
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
141
|
+
def with_footer(self, content: str) -> "Card":
|
|
142
|
+
"""Set the card footer content and return self for chaining.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
content: HTML string for the card footer
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Self for method chaining
|
|
149
|
+
|
|
150
|
+
Example:
|
|
151
|
+
>>> card = Card(title="Title").with_footer("<a href='#'>Link</a>")
|
|
152
|
+
"""
|
|
153
|
+
self._footer = content
|
|
154
|
+
return self
|
|
155
|
+
|
|
156
|
+
@staticmethod
|
|
157
|
+
def _esc(text: str) -> str:
|
|
158
|
+
"""Escape HTML special characters."""
|
|
159
|
+
return (
|
|
160
|
+
text.replace("&", "&")
|
|
161
|
+
.replace("<", "<")
|
|
162
|
+
.replace(">", ">")
|
|
163
|
+
.replace('"', """)
|
|
164
|
+
.replace("'", "'")
|
|
165
|
+
)
|
|
50
166
|
|
|
51
167
|
|
|
52
168
|
class CardIcon:
|
|
53
|
-
"""Card with icon.
|
|
169
|
+
"""Card variant with an icon display.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
icon_svg: SVG markup string for the icon
|
|
173
|
+
title: Optional card title
|
|
174
|
+
subtitle: Optional card subtitle
|
|
54
175
|
|
|
55
|
-
|
|
176
|
+
Example:
|
|
177
|
+
>>> icon_card = CardIcon(
|
|
178
|
+
... icon_svg='<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/></svg>',
|
|
179
|
+
... title="Settings",
|
|
180
|
+
... )
|
|
181
|
+
>>> print(icon_card.render_html())
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
def __init__(
|
|
185
|
+
self,
|
|
186
|
+
icon_svg: str,
|
|
187
|
+
title: Optional[str] = None,
|
|
188
|
+
subtitle: Optional[str] = None,
|
|
189
|
+
):
|
|
190
|
+
if not icon_svg:
|
|
191
|
+
raise ValueError("icon_svg cannot be empty")
|
|
56
192
|
self.icon_svg = icon_svg
|
|
57
|
-
self.
|
|
193
|
+
self.title = title
|
|
194
|
+
self.subtitle = subtitle
|
|
195
|
+
|
|
196
|
+
def render(self) -> CardElement:
|
|
197
|
+
"""Render the icon card as a CardElement.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
CardElement representing the icon card
|
|
201
|
+
"""
|
|
202
|
+
classes = ["cn-card", "cn-card-icon"]
|
|
203
|
+
|
|
204
|
+
parts = [f'<div class="cn-card-icon-inner">{self.icon_svg}</div>']
|
|
205
|
+
|
|
206
|
+
if self.title:
|
|
207
|
+
parts.append(f'<h3 class="cn-card-title">{self._esc(self.title)}</h3>')
|
|
208
|
+
if self.subtitle:
|
|
209
|
+
parts.append(
|
|
210
|
+
f'<p class="cn-card-subtitle">{self._esc(self.subtitle)}</p>'
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
return CardElement(
|
|
214
|
+
classes=classes,
|
|
215
|
+
inner_html="".join(parts),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def render_html(self) -> str:
|
|
219
|
+
"""Render the icon card as an HTML string.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
HTML string representation of the icon card
|
|
223
|
+
"""
|
|
224
|
+
return self.render().render_html()
|
|
58
225
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return
|
|
226
|
+
@staticmethod
|
|
227
|
+
def _esc(text: str) -> str:
|
|
228
|
+
"""Escape HTML special characters."""
|
|
229
|
+
return (
|
|
230
|
+
text.replace("&", "&")
|
|
231
|
+
.replace("<", "<")
|
|
232
|
+
.replace(">", ">")
|
|
233
|
+
.replace('"', """)
|
|
234
|
+
.replace("'", "'")
|
|
235
|
+
)
|