violit 0.0.1__py3-none-any.whl

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.
@@ -0,0 +1,173 @@
1
+ """Media Widgets Mixin for Violit"""
2
+
3
+ from typing import Union, Optional
4
+ import base64
5
+ from ..component import Component
6
+ from ..context import rendering_ctx
7
+
8
+
9
+ class MediaWidgetsMixin:
10
+ """Media widgets (image, audio, video)"""
11
+
12
+ def image(self, image, caption=None, width=None, use_column_width=False, **props):
13
+ """Display image from various sources"""
14
+ cid = self._get_next_cid("image")
15
+
16
+ def builder():
17
+ # Handle different image sources
18
+ img_src = ""
19
+
20
+ if isinstance(image, str):
21
+ # URL or file path
22
+ if image.startswith(('http://', 'https://')):
23
+ img_src = image
24
+ else:
25
+ # Local file - read and convert to base64
26
+ try:
27
+ import os
28
+ if os.path.exists(image):
29
+ with open(image, 'rb') as f:
30
+ img_data = f.read()
31
+ img_base64 = base64.b64encode(img_data).decode('utf-8')
32
+ # Detect image type
33
+ ext = os.path.splitext(image)[1].lower()
34
+ mime_types = {'.jpg': 'jpeg', '.jpeg': 'jpeg', '.png': 'png', '.gif': 'gif', '.webp': 'webp'}
35
+ mime = mime_types.get(ext, 'png')
36
+ img_src = f"data:image/{mime};base64,{img_base64}"
37
+ except:
38
+ img_src = image # Fallback to treating as URL
39
+ elif hasattr(image, 'read'):
40
+ # File-like object
41
+ img_data = image.read()
42
+ img_base64 = base64.b64encode(img_data).decode('utf-8')
43
+ img_src = f"data:image/png;base64,{img_base64}"
44
+ else:
45
+ # Try numpy array (PIL Image, etc.)
46
+ try:
47
+ from PIL import Image
48
+ import io
49
+ import numpy as np
50
+
51
+ if isinstance(image, np.ndarray):
52
+ pil_img = Image.fromarray(image)
53
+ else:
54
+ pil_img = image
55
+
56
+ buf = io.BytesIO()
57
+ pil_img.save(buf, format='PNG')
58
+ buf.seek(0)
59
+ img_base64 = base64.b64encode(buf.read()).decode('utf-8')
60
+ img_src = f"data:image/png;base64,{img_base64}"
61
+ except:
62
+ img_src = str(image)
63
+
64
+ # Build image HTML
65
+ width_style = ""
66
+ if use_column_width or width == "auto":
67
+ width_style = "width: 100%;"
68
+ elif width:
69
+ width_style = f"width: {width}px;"
70
+
71
+ caption_html = ""
72
+ if caption:
73
+ caption_html = f'<div style="text-align:center;margin-top:0.5rem;color:var(--sl-text-muted);font-size:0.875rem;">{caption}</div>'
74
+
75
+ html = f'''
76
+ <div class="image-container" style="text-align:center;">
77
+ <img src="{img_src}" style="{width_style} height:auto;border-radius:0.5rem;" alt="{caption or ''}" />
78
+ {caption_html}
79
+ </div>
80
+ '''
81
+ return Component("div", id=cid, content=html)
82
+
83
+ self._register_component(cid, builder)
84
+
85
+ def audio(self, audio, format="audio/mp3", start_time=0, **props):
86
+ """Display audio player"""
87
+ cid = self._get_next_cid("audio")
88
+
89
+ def builder():
90
+ # Handle different audio sources
91
+ audio_src = ""
92
+
93
+ if isinstance(audio, str):
94
+ if audio.startswith(('http://', 'https://')):
95
+ audio_src = audio
96
+ else:
97
+ # Local file - read and convert to base64
98
+ try:
99
+ import os
100
+ if os.path.exists(audio):
101
+ with open(audio, 'rb') as f:
102
+ audio_data = f.read()
103
+ audio_base64 = base64.b64encode(audio_data).decode('utf-8')
104
+ audio_src = f"data:{format};base64,{audio_base64}"
105
+ except:
106
+ audio_src = audio
107
+ elif hasattr(audio, 'read'):
108
+ audio_data = audio.read()
109
+ audio_base64 = base64.b64encode(audio_data).decode('utf-8')
110
+ audio_src = f"data:{format};base64,{audio_base64}"
111
+ else:
112
+ # Numpy array (audio waveform)
113
+ try:
114
+ import numpy as np
115
+ import scipy.io.wavfile as wavfile
116
+ import io
117
+
118
+ buf = io.BytesIO()
119
+ wavfile.write(buf, 44100, audio)
120
+ buf.seek(0)
121
+ audio_base64 = base64.b64encode(buf.read()).decode('utf-8')
122
+ audio_src = f"data:audio/wav;base64,{audio_base64}"
123
+ except:
124
+ audio_src = str(audio)
125
+
126
+ html = f'''
127
+ <audio controls style="width:100%;border-radius:0.5rem;">
128
+ <source src="{audio_src}" type="{format}">
129
+ Your browser does not support the audio element.
130
+ </audio>
131
+ '''
132
+ return Component("div", id=cid, content=html)
133
+
134
+ self._register_component(cid, builder)
135
+
136
+ def video(self, video, format="video/mp4", start_time=0, **props):
137
+ """Display video player"""
138
+ cid = self._get_next_cid("video")
139
+
140
+ def builder():
141
+ # Handle different video sources
142
+ video_src = ""
143
+
144
+ if isinstance(video, str):
145
+ if video.startswith(('http://', 'https://')):
146
+ video_src = video
147
+ else:
148
+ # Local file - read and convert to base64
149
+ try:
150
+ import os
151
+ if os.path.exists(video):
152
+ with open(video, 'rb') as f:
153
+ video_data = f.read()
154
+ video_base64 = base64.b64encode(video_data).decode('utf-8')
155
+ video_src = f"data:{format};base64,{video_base64}"
156
+ except:
157
+ video_src = video
158
+ elif hasattr(video, 'read'):
159
+ video_data = video.read()
160
+ video_base64 = base64.b64encode(video_data).decode('utf-8')
161
+ video_src = f"data:{format};base64,{video_base64}"
162
+ else:
163
+ video_src = str(video)
164
+
165
+ html = f'''
166
+ <video controls style="width:100%;border-radius:0.5rem;">
167
+ <source src="{video_src}" type="{format}">
168
+ Your browser does not support the video element.
169
+ </video>
170
+ '''
171
+ return Component("div", id=cid, content=html)
172
+
173
+ self._register_component(cid, builder)
@@ -0,0 +1,255 @@
1
+ """Status Widgets Mixin for Violit"""
2
+
3
+ from typing import Union, Callable, Optional
4
+ from ..component import Component
5
+ from ..context import rendering_ctx
6
+ from ..state import get_session_store, State
7
+
8
+
9
+ class StatusWidgetsMixin:
10
+ """Status display widgets (success, info, warning, error, toast, progress, spinner, status, balloons, snow, exception)"""
11
+
12
+ def success(self, content):
13
+ """Display success alert"""
14
+ self.alert(content, "success", "check-circle")
15
+
16
+ def warning(self, content):
17
+ """Display warning alert"""
18
+ self.alert(content, "warning", "exclamation-triangle")
19
+
20
+ def error(self, content):
21
+ """Display error alert"""
22
+ self.alert(content, "danger", "x-circle")
23
+
24
+ def info(self, content):
25
+ """Display info alert"""
26
+ self.alert(content, "primary", "info-circle")
27
+
28
+ def alert(self, content: Union[str, Callable, State], variant="primary", icon=None):
29
+ """Display alert message with Signal support"""
30
+ import html as html_lib
31
+
32
+ cid = self._get_next_cid("alert")
33
+ def builder():
34
+ # Signal handling
35
+ val = content
36
+ if isinstance(content, State):
37
+ token = rendering_ctx.set(cid)
38
+ val = content.value
39
+ rendering_ctx.reset(token)
40
+ elif callable(content):
41
+ token = rendering_ctx.set(cid)
42
+ val = content()
43
+ rendering_ctx.reset(token)
44
+
45
+ # XSS protection: escape content
46
+ escaped_val = html_lib.escape(str(val))
47
+
48
+ icon_html = f'<sl-icon slot="icon" name="{icon}"></sl-icon>' if icon else ""
49
+ html_output = f'<sl-alert variant="{variant}" open>{icon_html}{escaped_val}</sl-alert>'
50
+ return Component("div", id=cid, content=html_output)
51
+ self._register_component(cid, builder)
52
+
53
+ def toast(self, message: Union[str, Callable, State], icon="info-circle", variant="primary"):
54
+ """Display toast notification (Signal support via evaluation)"""
55
+ import json
56
+
57
+ if isinstance(message, (State, Callable)):
58
+ # Special case: dynamic toast label isn't common but for consistency:
59
+ cid = self._get_next_cid("toast_trigger")
60
+ def builder():
61
+ token = rendering_ctx.set(cid)
62
+ val = message.value if isinstance(message, State) else message()
63
+ rendering_ctx.reset(token)
64
+
65
+ # XSS protection: safely escape with JSON.stringify
66
+ safe_val = json.dumps(str(val))
67
+ safe_variant = json.dumps(str(variant))
68
+ safe_icon = json.dumps(str(icon))
69
+ code = f"createToast({safe_val}, {safe_variant}, {safe_icon})"
70
+ return Component("script", id=cid, content=code)
71
+ self._register_component(cid, builder)
72
+ else:
73
+ # XSS protection: safely escape with JSON.stringify
74
+ safe_message = json.dumps(str(message))
75
+ safe_variant = json.dumps(str(variant))
76
+ safe_icon = json.dumps(str(icon))
77
+ code = f"createToast({safe_message}, {safe_variant}, {safe_icon})"
78
+ self._enqueue_eval(code, toast_data={"message": str(message), "icon": str(icon), "variant": str(variant)})
79
+
80
+ def balloons(self):
81
+ """Display balloons animation"""
82
+ code = "createBalloons()"
83
+ self._enqueue_eval(code, effect="balloons")
84
+
85
+ def snow(self):
86
+ """Display snow animation"""
87
+ code = "createSnow()"
88
+ self._enqueue_eval(code, effect="snow")
89
+
90
+ def exception(self, exception: Exception):
91
+ """Display exception with traceback"""
92
+ import traceback
93
+ import html as html_lib
94
+
95
+ cid = self._get_next_cid("exception")
96
+ tb = "".join(traceback.format_exception(type(exception), exception, exception.__traceback__))
97
+
98
+ def builder():
99
+ # XSS protection: escape exception message and traceback
100
+ escaped_name = html_lib.escape(type(exception).__name__)
101
+ escaped_msg = html_lib.escape(str(exception))
102
+ escaped_tb = html_lib.escape(tb)
103
+
104
+ html_output = f'''
105
+ <sl-alert variant="danger" open style="margin-bottom:1rem;">
106
+ <sl-icon slot="icon" name="exclamation-octagon"></sl-icon>
107
+ <strong>{escaped_name}:</strong> {escaped_msg}
108
+ <pre style="margin-top:0.5rem;padding:0.5rem;background:rgba(0,0,0,0.1);border-radius:0.25rem;overflow-x:auto;font-size:0.85rem;">{escaped_tb}</pre>
109
+ </sl-alert>
110
+ '''
111
+ return Component("div", id=cid, content=html_output)
112
+ self._register_component(cid, builder)
113
+
114
+ def _enqueue_eval(self, code, **lite_data):
115
+ """Internal helper to enqueue JS evaluation or store for lite mode"""
116
+ if self.mode == 'ws':
117
+ store = get_session_store()
118
+ if 'eval_queue' not in store: store['eval_queue'] = []
119
+ store['eval_queue'].append(code)
120
+ else:
121
+ store = get_session_store()
122
+ if 'toasts' not in store: store['toasts'] = []
123
+ if 'effects' not in store: store['effects'] = []
124
+
125
+ if 'toast_data' in lite_data:
126
+ store['toasts'].append(lite_data['toast_data'])
127
+ if 'effect' in lite_data:
128
+ store['effects'].append(lite_data['effect'])
129
+
130
+ def progress(self, value=0, text=None):
131
+ """Display progress bar with Signal support"""
132
+ import html as html_lib
133
+
134
+ cid = self._get_next_cid("progress")
135
+
136
+ def builder():
137
+ # Handle Signal
138
+ val_num = value
139
+ if isinstance(value, State):
140
+ token = rendering_ctx.set(cid)
141
+ val_num = value.value
142
+ rendering_ctx.reset(token)
143
+ elif callable(value):
144
+ token = rendering_ctx.set(cid)
145
+ val_num = value()
146
+ rendering_ctx.reset(token)
147
+
148
+ progress_text = text or f"{val_num}%"
149
+ # XSS protection: escape text
150
+ escaped_text = html_lib.escape(str(progress_text))
151
+
152
+ html_output = f'''
153
+ <div style="margin-bottom:0.5rem;">
154
+ <div style="display:flex;justify-content:space-between;margin-bottom:0.25rem;">
155
+ <span style="font-size:0.875rem;color:var(--sl-text);">{escaped_text}</span>
156
+ <span style="font-size:0.875rem;color:var(--sl-text-muted);">{val_num}%</span>
157
+ </div>
158
+ <sl-progress-bar value="{val_num}"></sl-progress-bar>
159
+ </div>
160
+ '''
161
+ return Component("div", id=cid, content=html_output)
162
+ self._register_component(cid, builder)
163
+
164
+ def spinner(self, text="Loading..."):
165
+ """Display loading spinner"""
166
+ import html as html_lib
167
+
168
+ cid = self._get_next_cid("spinner")
169
+
170
+ # XSS protection: escape text
171
+ escaped_text = html_lib.escape(str(text))
172
+
173
+ html_output = f'''
174
+ <div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:1rem;">
175
+ <sl-spinner style="font-size:1.5rem;"></sl-spinner>
176
+ <span style="color:var(--sl-text-muted);font-size:0.875rem;">{escaped_text}</span>
177
+ </div>
178
+ '''
179
+ return Component("div", id=cid, content=html_output)
180
+
181
+ def status(self, label: str, state: str = "running", expanded: bool = True):
182
+ from ..context import fragment_ctx
183
+
184
+ cid = self._get_next_cid("status")
185
+
186
+ class StatusContext:
187
+ def __init__(self, app, status_id, label, state, expanded):
188
+ self.app = app
189
+ self.status_id = status_id
190
+ self.label = label
191
+ self.state = state
192
+ self.expanded = expanded
193
+ self.token = None
194
+
195
+ def __enter__(self):
196
+ # Register builder
197
+ def builder():
198
+ store = get_session_store()
199
+
200
+ # Collect nested content
201
+ htmls = []
202
+ # Check static
203
+ for cid_child, b in self.app.static_fragment_components.get(self.status_id, []):
204
+ htmls.append(b().render())
205
+ # Check session
206
+ for cid_child, b in store['fragment_components'].get(self.status_id, []):
207
+ htmls.append(b().render())
208
+
209
+ inner_html = "".join(htmls)
210
+
211
+ # Status icon and color based on state
212
+ if self.state == "running":
213
+ icon = '<sl-spinner style="font-size:1rem;"></sl-spinner>'
214
+ border_color = "var(--sl-primary)"
215
+ elif self.state == "complete":
216
+ icon = '<sl-icon name="check-circle-fill" style="color:#10b981;font-size:1rem;"></sl-icon>'
217
+ border_color = "#10b981"
218
+ elif self.state == "error":
219
+ icon = '<sl-icon name="x-circle-fill" style="color:#ef4444;font-size:1rem;"></sl-icon>'
220
+ border_color = "#ef4444"
221
+ else:
222
+ icon = '<sl-icon name="info-circle-fill" style="color:var(--sl-primary);font-size:1rem;"></sl-icon>'
223
+ border_color = "var(--sl-primary)"
224
+
225
+ # XSS protection: escape label
226
+ import html as html_lib
227
+ escaped_label = html_lib.escape(str(self.label))
228
+
229
+ # Build status container
230
+ html_output = f'''
231
+ <sl-details {"open" if self.expanded else ""} style="margin-bottom:1rem;">
232
+ <div slot="summary" style="display:flex;align-items:center;gap:0.5rem;font-weight:600;">
233
+ {icon}
234
+ <span>{escaped_label}</span>
235
+ </div>
236
+ <div style="padding:0.5rem 0 0 1.5rem;border-left:2px solid {border_color};margin-left:0.5rem;">
237
+ {inner_html}
238
+ </div>
239
+ </sl-details>
240
+ '''
241
+ return Component("div", id=self.status_id, content=html_output)
242
+
243
+ self.app._register_component(self.status_id, builder)
244
+
245
+ self.token = fragment_ctx.set(self.status_id)
246
+ return self
247
+
248
+ def __exit__(self, exc_type, exc_val, exc_tb):
249
+ if self.token:
250
+ fragment_ctx.reset(self.token)
251
+
252
+ def __getattr__(self, name):
253
+ return getattr(self.app, name)
254
+
255
+ return StatusContext(self, cid, label, state, expanded)