violit 0.0.4.post1__py3-none-any.whl → 0.0.6__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.
@@ -1,255 +1,308 @@
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)
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, *args):
13
+ """Display success alert"""
14
+ self.alert(*args, variant="success", icon="check-circle")
15
+
16
+ def warning(self, *args):
17
+ """Display warning alert"""
18
+ self.alert(*args, variant="warning", icon="exclamation-triangle")
19
+
20
+ def error(self, *args):
21
+ """Display error alert"""
22
+ self.alert(*args, variant="danger", icon="x-circle")
23
+
24
+ def info(self, *args):
25
+ """Display info alert"""
26
+ self.alert(*args, variant="primary", icon="info-circle")
27
+
28
+ def alert(self, *args, variant="primary", icon=None):
29
+ """Display alert message with Signal support (multiple arguments supported)"""
30
+ import html as html_lib
31
+ from ..state import State, ComputedState
32
+
33
+ cid = self._get_next_cid("alert")
34
+ def builder():
35
+ # Signal handling for multiple arguments
36
+ parts = []
37
+ token = rendering_ctx.set(cid)
38
+
39
+ try:
40
+ for arg in args:
41
+ if isinstance(arg, (State, ComputedState)):
42
+ parts.append(str(arg.value))
43
+ elif callable(arg):
44
+ parts.append(str(arg()))
45
+ else:
46
+ parts.append(str(arg))
47
+ finally:
48
+ rendering_ctx.reset(token)
49
+
50
+ val = " ".join(parts)
51
+
52
+ # XSS protection: escape content
53
+ escaped_val = html_lib.escape(str(val))
54
+
55
+ icon_html = f'<sl-icon slot="icon" name="{icon}"></sl-icon>' if icon else ""
56
+ html_output = f'<sl-alert variant="{variant}" open>{icon_html}{escaped_val}</sl-alert>'
57
+ return Component("div", id=cid, content=html_output)
58
+ self._register_component(cid, builder)
59
+
60
+ def toast(self, *args, icon="info-circle", variant="primary"):
61
+ """Display toast notification (Signal support via evaluation)"""
62
+ import json
63
+ from ..state import State, ComputedState
64
+
65
+ # Check if any argument requires dynamic binding
66
+ is_dynamic = any(isinstance(a, (State, ComputedState, Callable)) for a in args)
67
+
68
+ if is_dynamic:
69
+ cid = self._get_next_cid("toast_trigger")
70
+ def builder():
71
+ token = rendering_ctx.set(cid)
72
+ parts = []
73
+ for arg in args:
74
+ if isinstance(arg, (State, ComputedState)):
75
+ parts.append(str(arg.value))
76
+ elif callable(arg):
77
+ parts.append(str(arg()))
78
+ else:
79
+ parts.append(str(arg))
80
+ val = " ".join(parts)
81
+ rendering_ctx.reset(token)
82
+
83
+ # XSS protection: safely escape with JSON.stringify
84
+ safe_val = json.dumps(str(val))
85
+ safe_variant = json.dumps(str(variant))
86
+ safe_icon = json.dumps(str(icon))
87
+ code = f"createToast({safe_val}, {safe_variant}, {safe_icon})"
88
+ return Component("script", id=cid, content=code)
89
+ self._register_component(cid, builder)
90
+ else:
91
+ message = " ".join(str(a) for a in args)
92
+ # XSS protection: safely escape with JSON.stringify
93
+ safe_message = json.dumps(str(message))
94
+ safe_variant = json.dumps(str(variant))
95
+ safe_icon = json.dumps(str(icon))
96
+ code = f"createToast({safe_message}, {safe_variant}, {safe_icon})"
97
+ self._enqueue_eval(code, toast_data={"message": str(message), "icon": str(icon), "variant": str(variant)})
98
+
99
+ def balloons(self):
100
+ """Display balloons animation"""
101
+ code = "createBalloons()"
102
+ self._enqueue_eval(code, effect="balloons")
103
+
104
+ def snow(self):
105
+ """Display snow animation"""
106
+ code = "createSnow()"
107
+ self._enqueue_eval(code, effect="snow")
108
+
109
+ def exception(self, exception: Exception):
110
+ """Display exception with traceback"""
111
+ import traceback
112
+ import html as html_lib
113
+
114
+ cid = self._get_next_cid("exception")
115
+ tb = "".join(traceback.format_exception(type(exception), exception, exception.__traceback__))
116
+
117
+ def builder():
118
+ # XSS protection: escape exception message and traceback
119
+ escaped_name = html_lib.escape(type(exception).__name__)
120
+ escaped_msg = html_lib.escape(str(exception))
121
+ escaped_tb = html_lib.escape(tb)
122
+
123
+ html_output = f'''
124
+ <sl-alert variant="danger" open style="margin-bottom:1rem;">
125
+ <sl-icon slot="icon" name="exclamation-octagon"></sl-icon>
126
+ <strong>{escaped_name}:</strong> {escaped_msg}
127
+ <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>
128
+ </sl-alert>
129
+ '''
130
+ return Component("div", id=cid, content=html_output)
131
+ self._register_component(cid, builder)
132
+
133
+ def _enqueue_eval(self, code, **lite_data):
134
+ """Internal helper to enqueue JS evaluation or store for lite mode"""
135
+ if self.mode == 'ws':
136
+ store = get_session_store()
137
+ if 'eval_queue' not in store: store['eval_queue'] = []
138
+ store['eval_queue'].append(code)
139
+ else:
140
+ store = get_session_store()
141
+ if 'toasts' not in store: store['toasts'] = []
142
+ if 'effects' not in store: store['effects'] = []
143
+
144
+ if 'toast_data' in lite_data:
145
+ store['toasts'].append(lite_data['toast_data'])
146
+ if 'effect' in lite_data:
147
+ store['effects'].append(lite_data['effect'])
148
+
149
+ def progress(self, value=0, *args):
150
+ """Display progress bar with Signal support"""
151
+ import html as html_lib
152
+ from ..state import State, ComputedState
153
+
154
+ cid = self._get_next_cid("progress")
155
+
156
+ def builder():
157
+ # Handle Signal
158
+ val_num = value
159
+ if isinstance(value, (State, ComputedState)):
160
+ token = rendering_ctx.set(cid)
161
+ val_num = value.value
162
+ rendering_ctx.reset(token)
163
+ elif callable(value):
164
+ token = rendering_ctx.set(cid)
165
+ val_num = value()
166
+ rendering_ctx.reset(token)
167
+
168
+ # Resolve text args
169
+ parts = []
170
+ if args:
171
+ token = rendering_ctx.set(cid)
172
+ for arg in args:
173
+ if isinstance(arg, (State, ComputedState)):
174
+ parts.append(str(arg.value))
175
+ elif callable(arg):
176
+ parts.append(str(arg()))
177
+ else:
178
+ parts.append(str(arg))
179
+ rendering_ctx.reset(token)
180
+ progress_text = " ".join(parts)
181
+ else:
182
+ progress_text = f"{val_num}%"
183
+
184
+ # XSS protection: escape text
185
+ escaped_text = html_lib.escape(str(progress_text))
186
+
187
+ html_output = f'''
188
+ <div style="margin-bottom:0.5rem;">
189
+ <div style="display:flex;justify-content:space-between;margin-bottom:0.25rem;">
190
+ <span style="font-size:0.875rem;color:var(--sl-text);">{escaped_text}</span>
191
+ <span style="font-size:0.875rem;color:var(--sl-text-muted);">{val_num}%</span>
192
+ </div>
193
+ <sl-progress-bar value="{val_num}"></sl-progress-bar>
194
+ </div>
195
+ '''
196
+ return Component("div", id=cid, content=html_output)
197
+ self._register_component(cid, builder)
198
+
199
+ def spinner(self, *args):
200
+ """Display loading spinner"""
201
+ import html as html_lib
202
+ from ..state import State, ComputedState
203
+
204
+ cid = self._get_next_cid("spinner")
205
+
206
+ def builder():
207
+ parts = []
208
+ if args:
209
+ token = rendering_ctx.set(cid)
210
+ for arg in args:
211
+ if isinstance(arg, (State, ComputedState)):
212
+ parts.append(str(arg.value))
213
+ elif callable(arg):
214
+ parts.append(str(arg()))
215
+ else:
216
+ parts.append(str(arg))
217
+ rendering_ctx.reset(token)
218
+ text = " ".join(parts)
219
+ else:
220
+ text = "Loading..."
221
+
222
+ # XSS protection: escape text
223
+ escaped_text = html_lib.escape(str(text))
224
+
225
+ html_output = f'''
226
+ <div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:1rem;">
227
+ <sl-spinner style="font-size:1.5rem;"></sl-spinner>
228
+ <span style="color:var(--sl-text-muted);font-size:0.875rem;">{escaped_text}</span>
229
+ </div>
230
+ '''
231
+ return Component("div", id=cid, content=html_output)
232
+ self._register_component(cid, builder)
233
+
234
+ def status(self, label: str, state: str = "running", expanded: bool = True):
235
+ from ..context import fragment_ctx
236
+
237
+ cid = self._get_next_cid("status")
238
+
239
+ class StatusContext:
240
+ def __init__(self, app, status_id, label, state, expanded):
241
+ self.app = app
242
+ self.status_id = status_id
243
+ self.label = label
244
+ self.state = state
245
+ self.expanded = expanded
246
+ self.token = None
247
+
248
+ def __enter__(self):
249
+ # Register builder
250
+ def builder():
251
+ store = get_session_store()
252
+
253
+ # Collect nested content
254
+ htmls = []
255
+ # Check static
256
+ for cid_child, b in self.app.static_fragment_components.get(self.status_id, []):
257
+ htmls.append(b().render())
258
+ # Check session
259
+ for cid_child, b in store['fragment_components'].get(self.status_id, []):
260
+ htmls.append(b().render())
261
+
262
+ inner_html = "".join(htmls)
263
+
264
+ # Status icon and color based on state
265
+ if self.state == "running":
266
+ icon = '<sl-spinner style="font-size:1rem;"></sl-spinner>'
267
+ border_color = "var(--sl-primary)"
268
+ elif self.state == "complete":
269
+ icon = '<sl-icon name="check-circle-fill" style="color:#10b981;font-size:1rem;"></sl-icon>'
270
+ border_color = "#10b981"
271
+ elif self.state == "error":
272
+ icon = '<sl-icon name="x-circle-fill" style="color:#ef4444;font-size:1rem;"></sl-icon>'
273
+ border_color = "#ef4444"
274
+ else:
275
+ icon = '<sl-icon name="info-circle-fill" style="color:var(--sl-primary);font-size:1rem;"></sl-icon>'
276
+ border_color = "var(--sl-primary)"
277
+
278
+ # XSS protection: escape label
279
+ import html as html_lib
280
+ escaped_label = html_lib.escape(str(self.label))
281
+
282
+ # Build status container
283
+ html_output = f'''
284
+ <sl-details {"open" if self.expanded else ""} style="margin-bottom:1rem;">
285
+ <div slot="summary" style="display:flex;align-items:center;gap:0.5rem;font-weight:600;">
286
+ {icon}
287
+ <span>{escaped_label}</span>
288
+ </div>
289
+ <div style="padding:0.5rem 0 0 1.5rem;border-left:2px solid {border_color};margin-left:0.5rem;">
290
+ {inner_html}
291
+ </div>
292
+ </sl-details>
293
+ '''
294
+ return Component("div", id=self.status_id, content=html_output)
295
+
296
+ self.app._register_component(self.status_id, builder)
297
+
298
+ self.token = fragment_ctx.set(self.status_id)
299
+ return self
300
+
301
+ def __exit__(self, exc_type, exc_val, exc_tb):
302
+ if self.token:
303
+ fragment_ctx.reset(self.token)
304
+
305
+ def __getattr__(self, name):
306
+ return getattr(self.app, name)
307
+
308
+ return StatusContext(self, cid, label, state, expanded)