cronixui 1.1.1 → 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 +12 -8
- 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
|
@@ -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
|
|
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.
|
|
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
|
-
"
|
|
119
|
-
"
|
|
120
|
-
"
|
|
121
|
-
"
|
|
125
|
+
"HtmlElement",
|
|
126
|
+
"ComponentGroup",
|
|
127
|
+
"el",
|
|
128
|
+
"classes",
|
|
129
|
+
"attrs",
|
|
130
|
+
"escape_html",
|
|
122
131
|
"BG",
|
|
123
132
|
"SURFACE",
|
|
124
133
|
"SURFACE_2",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,14 +1,70 @@
|
|
|
1
|
-
"""CronixUI Alert Component
|
|
1
|
+
"""CronixUI Alert Component.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
84
|
+
self.variant = variant
|
|
22
85
|
self.dismissible = dismissible
|
|
23
|
-
self.element = self._render()
|
|
24
86
|
|
|
25
|
-
def
|
|
26
|
-
|
|
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
|
-
|
|
97
|
+
# Icon
|
|
29
98
|
if icon_svg:
|
|
30
|
-
|
|
31
|
-
icon.innerHTML = icon_svg
|
|
32
|
-
el.appendChild(icon)
|
|
99
|
+
parts.append(f'<span class="cn-alert-icon">{icon_svg}</span>')
|
|
33
100
|
|
|
34
|
-
|
|
101
|
+
# Content area
|
|
102
|
+
content_parts = []
|
|
35
103
|
if self.title:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
112
|
+
# Dismiss button
|
|
113
|
+
if self.dismissible:
|
|
114
|
+
parts.append(
|
|
115
|
+
'<button class="cn-alert-close" data-dismiss="alert">×</button>'
|
|
116
|
+
)
|
|
43
117
|
|
|
44
|
-
|
|
118
|
+
inner = "".join(parts)
|
|
119
|
+
dismiss_attr = ' data-dismissible="true"' if self.dismissible else ""
|
|
45
120
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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("&", "&")
|
|
140
|
+
.replace("<", "<")
|
|
141
|
+
.replace(">", ">")
|
|
142
|
+
.replace('"', """)
|
|
143
|
+
.replace("'", "'")
|
|
144
|
+
)
|
|
@@ -1,11 +1,62 @@
|
|
|
1
|
-
"""CronixUI Avatar Component
|
|
1
|
+
"""CronixUI Avatar Component.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
21
|
-
|
|
76
|
+
self.size = size
|
|
77
|
+
|
|
78
|
+
def render(self) -> AvatarElement:
|
|
79
|
+
"""Render the avatar as an AvatarElement data structure.
|
|
22
80
|
|
|
23
|
-
|
|
24
|
-
|
|
81
|
+
Returns:
|
|
82
|
+
AvatarElement representing the avatar
|
|
83
|
+
"""
|
|
84
|
+
classes = ["cn-avatar"]
|
|
25
85
|
if self.size != "md":
|
|
26
|
-
|
|
86
|
+
classes.append(f"cn-avatar-{self.size}")
|
|
27
87
|
|
|
28
88
|
if self.image_url:
|
|
29
|
-
|
|
30
|
-
img.
|
|
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
|
-
|
|
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
|
-
|
|
106
|
+
@staticmethod
|
|
107
|
+
def _esc(text: str) -> str:
|
|
108
|
+
"""Escape HTML special characters."""
|
|
109
|
+
return (
|
|
110
|
+
text.replace("&", "&")
|
|
111
|
+
.replace("<", "<")
|
|
112
|
+
.replace(">", ">")
|
|
113
|
+
.replace('"', """)
|
|
114
|
+
.replace("'", "'")
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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()
|