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
@@ -0,0 +1,142 @@
1
+ # CronixUI Python Package
2
+
3
+ A dark-themed UI toolkit with crimson accents and Outfit typography, implemented in pure Python.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install cronixui
9
+ ```
10
+
11
+ For development:
12
+
13
+ ```bash
14
+ pip install cronixui[dev]
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ```python
20
+ from cronixui import Button, Card, Toast, Badge
21
+ from cronixui.tokens import BG, ACCENT, TEXT
22
+
23
+ # Create a button
24
+ btn = Button("Click me", variant="primary")
25
+ print(btn.render_html())
26
+ # <button class="cn-btn cn-btn-primary">Click me</button>
27
+
28
+ # Create a card
29
+ card = Card(title="My Card", body="Card content")
30
+ print(card.render_html())
31
+ # <div class="cn-card">...</div>
32
+
33
+ # Show a toast
34
+ toast = Toast.success("Operation completed!")
35
+ print(toast.message) # Operation completed!
36
+
37
+ # Use a badge
38
+ badge = Badge("New", variant="success")
39
+ print(badge.render_html())
40
+ # <span class="cn-badge cn-badge-success">New</span>
41
+ ```
42
+
43
+ ## Components
44
+
45
+ All components generate HTML strings and don't require a browser to run:
46
+
47
+ - **Button** - Buttons with variants (primary, ghost, outline, danger, success)
48
+ - **ButtonGroup** - Group multiple buttons together
49
+ - **Card** - Card containers with header, body, footer
50
+ - **Badge** - Status badges with semantic colors
51
+ - **Tag** - Removable tags with close buttons
52
+ - **Avatar** - User avatars with initials
53
+ - **AvatarGroup** - Grouped avatar display
54
+ - **Toast** - Toast notification data
55
+ - **Alert** - Alert messages with variants
56
+ - **Progress** - Progress bars
57
+ - **Stat** - Statistics display
58
+ - **Table** - Data tables
59
+ - **List** - Lists with icons and actions
60
+ - **Form** - Form inputs (Input, Textarea, Select, Checkbox, Radio, etc.)
61
+ - **Layout** - Layout components (Header, Sidebar, Footer, Container)
62
+ - **Tooltip** - Tooltip wrappers
63
+ - **Loading** - Spinners and skeletons
64
+ - **Tabs** - Tab navigation
65
+ - **Accordion** - Collapsible sections
66
+ - **Modal** - Modal dialogs
67
+ - **Dropdown** - Dropdown menus
68
+ - **Nav** - Navigation bars
69
+ - **Pagination** - Pagination controls
70
+ - **CommandPalette** - Command palette data
71
+ - **Search** - Search component data
72
+
73
+ ## Design Tokens
74
+
75
+ Access design tokens for consistent theming:
76
+
77
+ ```python
78
+ from cronixui.tokens import (
79
+ Color, BG, SURFACE, ACCENT, TEXT,
80
+ typography, spacing, radius, shadow
81
+ )
82
+
83
+ # Colors
84
+ print(BG.hex) # #0a0a0a
85
+ print(ACCENT.rgb) # (107, 35, 35)
86
+
87
+ # Typography
88
+ print(typography.font_family) # 'Outfit', ...
89
+ print(typography.lg) # 16
90
+
91
+ # Spacing
92
+ print(spacing.space_4) # 16
93
+
94
+ # Border radius
95
+ print(radius.default) # 10
96
+
97
+ # Shadows
98
+ print(shadow.lg) # 0 8px 24px rgba(0, 0, 0, 0.5)
99
+ ```
100
+
101
+ ## Development
102
+
103
+ ### Running tests
104
+
105
+ ```bash
106
+ pytest
107
+ ```
108
+
109
+ With coverage:
110
+
111
+ ```bash
112
+ pytest --cov=cronixui --cov-report=html
113
+ ```
114
+
115
+ ### Code formatting
116
+
117
+ ```bash
118
+ black .
119
+ ruff check .
120
+ ```
121
+
122
+ ### Type checking
123
+
124
+ ```bash
125
+ mypy cronixui
126
+ ```
127
+
128
+ ### Building the package
129
+
130
+ ```bash
131
+ python -m build
132
+ ```
133
+
134
+ ### Publishing to PyPI
135
+
136
+ ```bash
137
+ python -m twine upload dist/*
138
+ ```
139
+
140
+ ## License
141
+
142
+ GPL 3.0, see LICENSE for details.
@@ -24,7 +24,14 @@ from .tooltip import Tooltip
24
24
  from .layout import Header, Sidebar, Footer, Container, Divider, Section
25
25
  from .form import Input, Textarea, FormField, Checkbox, Radio, Select, Slider, FileInput
26
26
  from .progress import Progress, Stat
27
- from .core import init, query, query_all, create_el
27
+ from .core import (
28
+ HtmlElement,
29
+ ComponentGroup,
30
+ el,
31
+ classes,
32
+ attrs,
33
+ escape_html,
34
+ )
28
35
  from .tokens import (
29
36
  BG,
30
37
  SURFACE,
@@ -71,7 +78,7 @@ from .tokens import (
71
78
  Layout,
72
79
  )
73
80
 
74
- __version__ = "1.0.6"
81
+ __version__ = "1.1.2"
75
82
  __all__ = [
76
83
  "Toast",
77
84
  "Toggle",
@@ -115,10 +122,12 @@ __all__ = [
115
122
  "FileInput",
116
123
  "Progress",
117
124
  "Stat",
118
- "init",
119
- "query",
120
- "query_all",
121
- "create_el",
125
+ "HtmlElement",
126
+ "ComponentGroup",
127
+ "el",
128
+ "classes",
129
+ "attrs",
130
+ "escape_html",
122
131
  "BG",
123
132
  "SURFACE",
124
133
  "SURFACE_2",
@@ -1,14 +1,70 @@
1
- """CronixUI Alert Component"""
1
+ """CronixUI Alert Component.
2
2
 
3
- from typing import Optional
4
- from .core import create_el
3
+ Generates HTML for alert banners with icon, title, message, and optional dismiss button.
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
+ from .core import HtmlElement
13
+
14
+
15
+ @dataclass
16
+ class AlertElement:
17
+ """Represents a rendered alert element."""
18
+
19
+ tag: str = "div"
20
+ classes: List[str] = field(default_factory=list)
21
+ attributes: Dict[str, str] = field(default_factory=dict)
22
+ inner_html: str = ""
23
+
24
+ def render_html(self) -> str:
25
+ """Render the alert as HTML string.
26
+
27
+ Returns:
28
+ Complete HTML for the alert including icon, content, and optional close button
29
+ """
30
+ class_str = " ".join(self.classes)
31
+ class_attr = f' class="{class_str}"' if class_str else ""
32
+ attrs_str = "".join(f' {k}="{v}"' for k, v in self.attributes.items())
33
+ return f"<{self.tag}{class_attr}{attrs_str}>{self.inner_html}</{self.tag}>"
34
+
35
+ def render(self) -> "AlertElement":
36
+ """Return self for API compatibility."""
37
+ return self
5
38
 
6
39
 
7
40
  class Alert:
8
- """Alert component for messages."""
41
+ """Alert component for displaying informational, success, warning, or error messages.
42
+
43
+ Args:
44
+ message: The alert message text
45
+ title: Optional alert title
46
+ variant: Alert variant - info, success, warning, error (default: info)
47
+ dismissible: Whether to show a close button (default: False)
48
+
49
+ Example:
50
+ >>> alert = Alert("Your changes were saved.", title="Success", variant="success")
51
+ >>> print(alert.render_html())
52
+ <div class="cn-alert cn-alert-success">...</div>
53
+
54
+ >>> dismissible = Alert("Dismiss me", dismissible=True)
55
+ >>> print(dismissible.render_html())
56
+ <div class="cn-alert cn-alert-info" data-dismissible="true">...</div>
57
+ """
9
58
 
10
59
  VARIANTS = ("info", "success", "warning", "error")
11
60
 
61
+ _ICONS = {
62
+ "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>',
63
+ "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>',
64
+ "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>',
65
+ "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>',
66
+ }
67
+
12
68
  def __init__(
13
69
  self,
14
70
  message: str,
@@ -16,46 +72,73 @@ class Alert:
16
72
  variant: str = "info",
17
73
  dismissible: bool = False,
18
74
  ):
75
+ if variant not in self.VARIANTS:
76
+ raise ValueError(
77
+ f"Invalid variant '{variant}'. Must be one of {self.VARIANTS}"
78
+ )
79
+ if not message:
80
+ raise ValueError("message cannot be empty")
81
+
19
82
  self.message = message
20
83
  self.title = title
21
- self.variant = variant if variant in self.VARIANTS else "info"
84
+ self.variant = variant
22
85
  self.dismissible = dismissible
23
- self.element = self._render()
24
86
 
25
- def _render(self):
26
- el = create_el("div", f"cn-alert cn-alert-{self.variant}")
87
+ def render(self) -> AlertElement:
88
+ """Render the alert as an AlertElement data structure.
89
+
90
+ Returns:
91
+ AlertElement representing the complete alert banner
92
+ """
93
+ icon_svg = self._ICONS.get(self.variant, "")
94
+
95
+ parts = []
27
96
 
28
- icon_svg = self._get_icon_svg()
97
+ # Icon
29
98
  if icon_svg:
30
- icon = create_el("span", "cn-alert-icon")
31
- icon.innerHTML = icon_svg
32
- el.appendChild(icon)
99
+ parts.append(f'<span class="cn-alert-icon">{icon_svg}</span>')
33
100
 
34
- content = create_el("div", "cn-alert-content")
101
+ # Content area
102
+ content_parts = []
35
103
  if self.title:
36
- title_el = create_el("div", "cn-alert-title")
37
- title_el.textContent = self.title
38
- content.appendChild(title_el)
104
+ content_parts.append(
105
+ f'<div class="cn-alert-title">{self._esc(self.title)}</div>'
106
+ )
107
+ content_parts.append(
108
+ f'<div class="cn-alert-message">{self._esc(self.message)}</div>'
109
+ )
110
+ parts.append(f'<div class="cn-alert-content">{"".join(content_parts)}</div>')
39
111
 
40
- message_el = create_el("div", "cn-alert-message")
41
- message_el.textContent = self.message
42
- content.appendChild(message_el)
112
+ # Dismiss button
113
+ if self.dismissible:
114
+ parts.append(
115
+ '<button class="cn-alert-close" data-dismiss="alert">&times;</button>'
116
+ )
43
117
 
44
- el.appendChild(content)
118
+ inner = "".join(parts)
119
+ dismiss_attr = ' data-dismissible="true"' if self.dismissible else ""
45
120
 
46
- if self.dismissible:
47
- close_btn = create_el("button", "cn-alert-close")
48
- close_btn.innerHTML = "&times;"
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)
121
+ return AlertElement(
122
+ classes=["cn-alert", f"cn-alert-{self.variant}"],
123
+ attributes={"data-dismissible": "true"} if self.dismissible else {},
124
+ inner_html=inner,
125
+ )
126
+
127
+ def render_html(self) -> str:
128
+ """Render the alert as an HTML string.
129
+
130
+ Returns:
131
+ Complete HTML string for the alert
132
+ """
133
+ return self.render().render_html()
134
+
135
+ @staticmethod
136
+ def _esc(text: str) -> str:
137
+ """Escape HTML special characters."""
138
+ return (
139
+ text.replace("&", "&amp;")
140
+ .replace("<", "&lt;")
141
+ .replace(">", "&gt;")
142
+ .replace('"', "&quot;")
143
+ .replace("'", "&#x27;")
144
+ )
@@ -1,11 +1,62 @@
1
- """CronixUI Avatar Component"""
1
+ """CronixUI Avatar Component.
2
2
 
3
- from typing import Optional
4
- from .core import create_el
3
+ Generates HTML for user avatars (images or initials) and avatar groups.
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
+ from .core import HtmlElement
13
+
14
+
15
+ @dataclass
16
+ class AvatarElement:
17
+ """Represents a rendered avatar element."""
18
+
19
+ tag: str = "div"
20
+ classes: List[str] = field(default_factory=list)
21
+ attributes: Dict[str, str] = field(default_factory=dict)
22
+ inner_html: str = ""
23
+
24
+ def render_html(self) -> str:
25
+ """Render the avatar as HTML string.
26
+
27
+ Returns:
28
+ Complete HTML for the avatar element
29
+ """
30
+ class_str = " ".join(self.classes)
31
+ class_attr = f' class="{class_str}"' if class_str else ""
32
+ attrs_str = "".join(f' {k}="{v}"' for k, v in self.attributes.items())
33
+ return f"<{self.tag}{class_attr}{attrs_str}>{self.inner_html}</{self.tag}>"
34
+
35
+ def render(self) -> "AvatarElement":
36
+ """Return self for API compatibility."""
37
+ return self
5
38
 
6
39
 
7
40
  class Avatar:
8
- """Avatar component for user images or initials."""
41
+ """Avatar component for displaying user images or initials.
42
+
43
+ Args:
44
+ initials: User initials to display (e.g. "JD")
45
+ image_url: URL to user's avatar image
46
+ size: Avatar size - sm, md, lg, xl (default: md)
47
+
48
+ Note:
49
+ If both image_url and initials are provided, image_url takes precedence.
50
+
51
+ Example:
52
+ >>> avatar = Avatar(initials="JD", size="lg")
53
+ >>> print(avatar.render_html())
54
+ <div class="cn-avatar cn-avatar-lg">JD</div>
55
+
56
+ >>> img_avatar = Avatar(image_url="/path/to/photo.jpg")
57
+ >>> print(img_avatar.render_html())
58
+ <div class="cn-avatar"><img src="/path/to/photo.jpg" alt="" /></div>
59
+ """
9
60
 
10
61
  SIZES = ("sm", "md", "lg", "xl")
11
62
 
@@ -15,36 +66,92 @@ class Avatar:
15
66
  image_url: Optional[str] = None,
16
67
  size: str = "md",
17
68
  ):
69
+ if size not in self.SIZES:
70
+ raise ValueError(f"Invalid size '{size}'. Must be one of {self.SIZES}")
71
+ if not initials and not image_url:
72
+ raise ValueError("At least one of 'initials' or 'image_url' must be provided")
73
+
18
74
  self.initials = initials
19
75
  self.image_url = image_url
20
- self.size = size if size in self.SIZES else "md"
21
- self.element = self._render()
76
+ self.size = size
77
+
78
+ def render(self) -> AvatarElement:
79
+ """Render the avatar as an AvatarElement data structure.
22
80
 
23
- def _render(self):
24
- el = create_el("div", "cn-avatar")
81
+ Returns:
82
+ AvatarElement representing the avatar
83
+ """
84
+ classes = ["cn-avatar"]
25
85
  if self.size != "md":
26
- el.classList.add(f"cn-avatar-{self.size}")
86
+ classes.append(f"cn-avatar-{self.size}")
27
87
 
28
88
  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)
89
+ alt_text = self.initials[:2].upper() if self.initials else "Avatar"
90
+ inner = f'<img src="{self.image_url}" alt="{self._esc(alt_text)}" />'
33
91
  elif self.initials:
34
- el.textContent = self.initials[:2].upper()
92
+ inner = self._esc(self.initials[:2].upper())
93
+ else:
94
+ inner = ""
95
+
96
+ return AvatarElement(classes=classes, inner_html=inner)
97
+
98
+ def render_html(self) -> str:
99
+ """Render the avatar as an HTML string.
100
+
101
+ Returns:
102
+ HTML string representation of the avatar
103
+ """
104
+ return self.render().render_html()
35
105
 
36
- return el
106
+ @staticmethod
107
+ def _esc(text: str) -> str:
108
+ """Escape HTML special characters."""
109
+ return (
110
+ text.replace("&", "&amp;")
111
+ .replace("<", "&lt;")
112
+ .replace(">", "&gt;")
113
+ .replace('"', "&quot;")
114
+ .replace("'", "&#x27;")
115
+ )
37
116
 
38
117
 
39
118
  class AvatarGroup:
40
- """Group of overlapping avatars."""
119
+ """Group of overlapping avatars, typically for showing multiple users.
120
+
121
+ Args:
122
+ *avatars: Avatar instances to include in the group
123
+
124
+ Example:
125
+ >>> avatars = AvatarGroup(
126
+ ... Avatar(initials="AB"),
127
+ ... Avatar(initials="CD"),
128
+ ... Avatar(initials="EF"),
129
+ ... )
130
+ >>> print(avatars.render_html())
131
+ <div class="cn-avatar-group">...</div>
132
+ """
41
133
 
42
134
  def __init__(self, *avatars: Avatar):
135
+ if not avatars:
136
+ raise ValueError("At least one avatar must be provided")
43
137
  self.avatars = avatars
44
- self.element = self._render()
45
138
 
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
139
+ def render(self) -> AvatarElement:
140
+ """Render the avatar group as an AvatarElement.
141
+
142
+ Returns:
143
+ AvatarElement wrapping all avatar children
144
+ """
145
+ children_html = "".join(a.render_html() for a in self.avatars)
146
+ return AvatarElement(
147
+ classes=["cn-avatar-group"],
148
+ inner_html=children_html,
149
+ )
150
+
151
+ def render_html(self) -> str:
152
+ """Render the avatar group as an HTML string.
153
+
154
+ Returns:
155
+ HTML string representation of the group
156
+ """
157
+ return self.render().render_html()