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.
Files changed (76) hide show
  1. package/README.md +1 -1
  2. package/package.json +71 -71
  3. package/packages/flutter/.qwen/settings.json +7 -0
  4. package/packages/flutter/pubspec.yaml +20 -20
  5. package/packages/go/cronixui/cronixui.go +926 -926
  6. package/packages/python/README.md +142 -0
  7. package/packages/python/cronixui/__init__.py +15 -6
  8. package/packages/python/cronixui/__pycache__/__init__.cpython-314.pyc +0 -0
  9. package/packages/python/cronixui/__pycache__/accordion.cpython-314.pyc +0 -0
  10. package/packages/python/cronixui/__pycache__/alert.cpython-314.pyc +0 -0
  11. package/packages/python/cronixui/__pycache__/avatar.cpython-314.pyc +0 -0
  12. package/packages/python/cronixui/__pycache__/badge.cpython-314.pyc +0 -0
  13. package/packages/python/cronixui/__pycache__/button.cpython-314.pyc +0 -0
  14. package/packages/python/cronixui/__pycache__/card.cpython-314.pyc +0 -0
  15. package/packages/python/cronixui/__pycache__/command_palette.cpython-314.pyc +0 -0
  16. package/packages/python/cronixui/__pycache__/core.cpython-314.pyc +0 -0
  17. package/packages/python/cronixui/__pycache__/dropdown.cpython-314.pyc +0 -0
  18. package/packages/python/cronixui/__pycache__/form.cpython-314.pyc +0 -0
  19. package/packages/python/cronixui/__pycache__/layout.cpython-314.pyc +0 -0
  20. package/packages/python/cronixui/__pycache__/list.cpython-314.pyc +0 -0
  21. package/packages/python/cronixui/__pycache__/loading.cpython-314.pyc +0 -0
  22. package/packages/python/cronixui/__pycache__/modal.cpython-314.pyc +0 -0
  23. package/packages/python/cronixui/__pycache__/nav.cpython-314.pyc +0 -0
  24. package/packages/python/cronixui/__pycache__/pagination.cpython-314.pyc +0 -0
  25. package/packages/python/cronixui/__pycache__/progress.cpython-314.pyc +0 -0
  26. package/packages/python/cronixui/__pycache__/search.cpython-314.pyc +0 -0
  27. package/packages/python/cronixui/__pycache__/table.cpython-314.pyc +0 -0
  28. package/packages/python/cronixui/__pycache__/tabs.cpython-314.pyc +0 -0
  29. package/packages/python/cronixui/__pycache__/toast.cpython-314.pyc +0 -0
  30. package/packages/python/cronixui/__pycache__/toggle.cpython-314.pyc +0 -0
  31. package/packages/python/cronixui/__pycache__/tokens.cpython-314.pyc +0 -0
  32. package/packages/python/cronixui/__pycache__/tooltip.cpython-314.pyc +0 -0
  33. package/packages/python/cronixui/alert.py +119 -36
  34. package/packages/python/cronixui/avatar.py +129 -22
  35. package/packages/python/cronixui/badge.py +161 -24
  36. package/packages/python/cronixui/button.py +96 -27
  37. package/packages/python/cronixui/card.py +206 -33
  38. package/packages/python/cronixui/core.py +212 -23
  39. package/packages/python/cronixui/form.py +552 -141
  40. package/packages/python/cronixui/layout.py +358 -96
  41. package/packages/python/cronixui/list.py +140 -37
  42. package/packages/python/cronixui/loading.py +107 -17
  43. package/packages/python/cronixui/progress.py +189 -47
  44. package/packages/python/cronixui/table.py +118 -31
  45. package/packages/python/cronixui/tooltip.py +117 -15
  46. package/packages/react/src/components/Accordion.tsx +82 -82
  47. package/packages/react/src/components/Button.tsx +47 -47
  48. package/packages/react/src/components/Card.tsx +69 -69
  49. package/packages/react/src/components/CommandPalette.tsx +131 -131
  50. package/packages/react/src/components/Dropdown.tsx +88 -88
  51. package/packages/react/src/components/FileInput.tsx +86 -86
  52. package/packages/react/src/components/FormGroup.tsx +36 -36
  53. package/packages/react/src/components/List.tsx +55 -55
  54. package/packages/react/src/components/Pagination.tsx +107 -107
  55. package/packages/react/src/components/Progress.tsx +49 -49
  56. package/packages/react/src/components/Search.tsx +95 -95
  57. package/packages/react/src/components/Sidebar.tsx +64 -64
  58. package/packages/react/src/components/Stack.tsx +69 -69
  59. package/packages/react/src/components/Table.tsx +90 -90
  60. package/packages/react/src/components/Toast.tsx +134 -134
  61. package/packages/react/src/components/Typography.tsx +66 -66
  62. package/packages/react/src/index.ts +40 -40
  63. package/packages/react/src/styles.css +2039 -2039
  64. package/packages/rust/cronixui/src/components/avatar.rs +85 -85
  65. package/packages/rust/cronixui/src/components/breadcrumb.rs +58 -58
  66. package/packages/rust/cronixui/src/components/card.rs +259 -259
  67. package/packages/rust/cronixui/src/components/command_palette.rs +254 -254
  68. package/packages/rust/cronixui/src/components/dropdown.rs +179 -179
  69. package/packages/rust/cronixui/src/components/file_input.rs +74 -74
  70. package/packages/rust/cronixui/src/components/mod.rs +51 -51
  71. package/packages/rust/cronixui/src/components/search.rs +185 -185
  72. package/packages/rust/cronixui/src/components/skeleton.rs +63 -63
  73. package/packages/rust/cronixui/src/components/table.rs +56 -56
  74. package/packages/rust/cronixui/src/lib.rs +128 -128
  75. package/packages/web/dist/cronixui.css +97 -93
  76. 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
- from .core import create_el
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__(self, text: str, variant: str = "default", solid: bool = False):
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 if variant in self.VARIANTS else "default"
72
+ self.variant = variant
14
73
  self.solid = solid
15
- self.element = self._render()
16
74
 
17
- def _render(self):
18
- classes = f"cn-badge cn-badge-{self.variant}"
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 += " cn-badge-solid"
21
- el = create_el("span", classes)
22
- el.textContent = self.text
23
- return el
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("&", "&amp;")
103
+ .replace("<", "&lt;")
104
+ .replace(">", "&gt;")
105
+ .replace('"', "&quot;")
106
+ .replace("'", "&#x27;")
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 _render(self):
36
- el = create_el("span", "cn-tag")
37
- el.textContent = self.text
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
- 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)
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
- return el
174
+ @staticmethod
175
+ def _esc(text: str) -> str:
176
+ """Escape HTML special characters."""
177
+ return (
178
+ text.replace("&", "&amp;")
179
+ .replace("<", "&lt;")
180
+ .replace(">", "&gt;")
181
+ .replace('"', "&quot;")
182
+ .replace("'", "&#x27;")
183
+ )
@@ -1,12 +1,44 @@
1
1
  """CronixUI Button Component"""
2
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
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 if variant in self.VARIANTS else "default"
25
- self.size = size if size in self.SIZES else "md"
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 _render(self):
32
- el = create_el("button", f"cn-btn cn-btn-{self.variant}")
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
- el.classList.add(f"cn-btn-{self.size}")
78
+ classes.append(f"cn-btn-{self.size}")
79
+
35
80
  if self.icon:
36
- el.classList.add("cn-btn-icon")
81
+ classes.append("cn-btn-icon")
82
+
83
+ attributes: Dict[str, str] = {}
37
84
  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
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 _render(self):
61
- el = create_el("div", "cn-btn-group")
62
- for btn in self.buttons:
63
- el.appendChild(btn.element)
64
- return el
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
- from typing import Optional
4
- from .core import create_el
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.element = self._render()
77
+ self._body = body
78
+ self._footer = footer
20
79
 
21
- def _render(self):
22
- el = create_el("div", "cn-card")
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
- el.classList.add("cn-card-clickable")
88
+ classes.append("cn-card-clickable")
89
+
90
+ parts = []
25
91
 
92
+ # Header
26
93
  if self.title or self.subtitle:
27
- header = create_el("div", "cn-card-header")
94
+ header_parts = []
28
95
  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
-
96
+ header_parts.append(
97
+ f'<h3 class="cn-card-title">{self._esc(self.title)}</h3>'
98
+ )
34
99
  if self.subtitle:
35
- subtitle_el = create_el("p", "cn-card-subtitle")
36
- subtitle_el.textContent = self.subtitle
37
- header.appendChild(subtitle_el)
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
- return el
132
+ Returns:
133
+ Self for method chaining
40
134
 
41
- def set_body(self, content: str):
42
- body = create_el("div", "cn-card-body")
43
- body.innerHTML = content
44
- self.element.appendChild(body)
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 set_footer(self, content: str):
47
- footer = create_el("div", "cn-card-footer")
48
- footer.innerHTML = content
49
- self.element.appendChild(footer)
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("&", "&amp;")
161
+ .replace("<", "&lt;")
162
+ .replace(">", "&gt;")
163
+ .replace('"', "&quot;")
164
+ .replace("'", "&#x27;")
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
- def __init__(self, icon_svg: str):
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.element = self._render()
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
- def _render(self):
60
- el = create_el("div", "cn-card-icon")
61
- el.innerHTML = self.icon_svg
62
- return el
226
+ @staticmethod
227
+ def _esc(text: str) -> str:
228
+ """Escape HTML special characters."""
229
+ return (
230
+ text.replace("&", "&amp;")
231
+ .replace("<", "&lt;")
232
+ .replace(">", "&gt;")
233
+ .replace('"', "&quot;")
234
+ .replace("'", "&#x27;")
235
+ )