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.
- violit/__init__.py +3 -0
- violit/app.py +1984 -0
- violit/broadcast.py +690 -0
- violit/broadcast_primitives.py +197 -0
- violit/component.py +38 -0
- violit/context.py +10 -0
- violit/engine.py +33 -0
- violit/state.py +76 -0
- violit/theme.py +749 -0
- violit/widgets/__init__.py +30 -0
- violit/widgets/card_widgets.py +595 -0
- violit/widgets/chart_widgets.py +253 -0
- violit/widgets/chat_widgets.py +221 -0
- violit/widgets/data_widgets.py +529 -0
- violit/widgets/form_widgets.py +421 -0
- violit/widgets/input_widgets.py +745 -0
- violit/widgets/layout_widgets.py +419 -0
- violit/widgets/list_widgets.py +107 -0
- violit/widgets/media_widgets.py +173 -0
- violit/widgets/status_widgets.py +255 -0
- violit/widgets/text_widgets.py +413 -0
- violit-0.0.1.dist-info/METADATA +504 -0
- violit-0.0.1.dist-info/RECORD +26 -0
- violit-0.0.1.dist-info/WHEEL +5 -0
- violit-0.0.1.dist-info/licenses/LICENSE +21 -0
- violit-0.0.1.dist-info/top_level.txt +1 -0
violit/broadcast.py
ADDED
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
"""WebSocket-based real-time broadcasting system"""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import threading
|
|
5
|
+
from typing import Dict, Any, Callable, Optional, List
|
|
6
|
+
import json
|
|
7
|
+
import uuid
|
|
8
|
+
|
|
9
|
+
from .broadcast_primitives import UI_PRIMITIVES, validate_primitive
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Broadcaster:
|
|
13
|
+
"""WebSocket-based real-time broadcasting system"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, app):
|
|
16
|
+
self.app = app
|
|
17
|
+
self._bindings: Dict[str, Dict] = {}
|
|
18
|
+
self._primitives = UI_PRIMITIVES
|
|
19
|
+
self._router_injected = False
|
|
20
|
+
|
|
21
|
+
# Core: Event Broadcasting
|
|
22
|
+
|
|
23
|
+
def get_active_sessions(self) -> list:
|
|
24
|
+
if not hasattr(self.app, 'ws_engine') or not self.app.ws_engine:
|
|
25
|
+
return []
|
|
26
|
+
return list(self.app.ws_engine.sockets.keys())
|
|
27
|
+
|
|
28
|
+
async def _broadcast_eval_async(self, js_code: str, exclude_session: Optional[str] = None):
|
|
29
|
+
if not hasattr(self.app, 'ws_engine') or not self.app.ws_engine:
|
|
30
|
+
print("[BROADCAST] WebSocket engine not available.")
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
active_sids = self.get_active_sessions()
|
|
34
|
+
|
|
35
|
+
if exclude_session:
|
|
36
|
+
active_sids = [sid for sid in active_sids if sid != exclude_session]
|
|
37
|
+
|
|
38
|
+
print(f"[BROADCAST] Starting: {len(active_sids)} sessions")
|
|
39
|
+
|
|
40
|
+
success_count = 0
|
|
41
|
+
for sid in active_sids:
|
|
42
|
+
try:
|
|
43
|
+
await self.app.ws_engine.push_eval(sid, js_code)
|
|
44
|
+
success_count += 1
|
|
45
|
+
except Exception as e:
|
|
46
|
+
print(f"[BROADCAST] Failed for session {sid[:8]}...: {e}")
|
|
47
|
+
|
|
48
|
+
print(f"[BROADCAST] Completed: {success_count}/{len(active_sids)} successful")
|
|
49
|
+
|
|
50
|
+
def eval_all(self, js_code: str, exclude_current: bool = False):
|
|
51
|
+
"""Send JavaScript code to all clients (low-level API)
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
js_code: JavaScript code to execute
|
|
55
|
+
exclude_current: Exclude current session
|
|
56
|
+
"""
|
|
57
|
+
from .context import session_ctx
|
|
58
|
+
|
|
59
|
+
exclude_session = None
|
|
60
|
+
if exclude_current:
|
|
61
|
+
try:
|
|
62
|
+
exclude_session = session_ctx.get()
|
|
63
|
+
except:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
def run_broadcast():
|
|
67
|
+
try:
|
|
68
|
+
loop = asyncio.new_event_loop()
|
|
69
|
+
asyncio.set_event_loop(loop)
|
|
70
|
+
loop.run_until_complete(self._broadcast_eval_async(js_code, exclude_session))
|
|
71
|
+
loop.close()
|
|
72
|
+
except Exception as e:
|
|
73
|
+
print(f"[BROADCAST] Execution failed: {e}")
|
|
74
|
+
|
|
75
|
+
thread = threading.Thread(target=run_broadcast, daemon=True)
|
|
76
|
+
thread.start()
|
|
77
|
+
|
|
78
|
+
def broadcast_event(self, event_name: str, data: Dict[str, Any],
|
|
79
|
+
exclude_current: bool = False):
|
|
80
|
+
"""Broadcast domain event to all clients
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
event_name: Event name (e.g. 'post_added', 'user_joined')
|
|
84
|
+
data: Event data (must be JSON-serializable)
|
|
85
|
+
exclude_current: Exclude current session
|
|
86
|
+
"""
|
|
87
|
+
# Auto-generate event ID for deduplication
|
|
88
|
+
if '_eventId' not in data:
|
|
89
|
+
data['_eventId'] = str(uuid.uuid4())
|
|
90
|
+
|
|
91
|
+
data_json = json.dumps(data)
|
|
92
|
+
|
|
93
|
+
js_code = f"""
|
|
94
|
+
(function() {{
|
|
95
|
+
const event = new CustomEvent('{event_name}', {{
|
|
96
|
+
detail: {data_json}
|
|
97
|
+
}});
|
|
98
|
+
window.dispatchEvent(event);
|
|
99
|
+
console.log('🔔 Event received: {event_name}');
|
|
100
|
+
}})();
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
self.eval_all(js_code, exclude_current=exclude_current)
|
|
104
|
+
|
|
105
|
+
# Widget-Level Bindings
|
|
106
|
+
|
|
107
|
+
def bind_list(self,
|
|
108
|
+
list_key: str,
|
|
109
|
+
on_append: str = None,
|
|
110
|
+
on_remove: str = None,
|
|
111
|
+
on_update: str = None,
|
|
112
|
+
on_replace: str = None) -> None:
|
|
113
|
+
"""Register event bindings for list widget
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
list_key: List widget key
|
|
117
|
+
on_append: Event name to trigger append
|
|
118
|
+
on_remove: Event name to trigger remove
|
|
119
|
+
on_update: Event name to trigger update
|
|
120
|
+
on_replace: Event name to trigger replace_all
|
|
121
|
+
"""
|
|
122
|
+
if on_append:
|
|
123
|
+
self._register_binding(on_append, {
|
|
124
|
+
'type': 'list.append',
|
|
125
|
+
'params': {
|
|
126
|
+
'list_key': list_key,
|
|
127
|
+
'item_data': 'e.detail',
|
|
128
|
+
'position': 'prepend'
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
if on_remove:
|
|
133
|
+
self._register_binding(on_remove, {
|
|
134
|
+
'type': 'list.remove',
|
|
135
|
+
'params': {
|
|
136
|
+
'list_key': list_key,
|
|
137
|
+
'item_id': 'e.detail.id || e.detail.post_id'
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
if on_update:
|
|
142
|
+
self._register_binding(on_update, {
|
|
143
|
+
'type': 'list.update',
|
|
144
|
+
'params': {
|
|
145
|
+
'list_key': list_key,
|
|
146
|
+
'item_id': 'e.detail.id',
|
|
147
|
+
'item_data': 'e.detail'
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
if on_replace:
|
|
152
|
+
self._register_binding(on_replace, {
|
|
153
|
+
'type': 'list.replace_all',
|
|
154
|
+
'params': {
|
|
155
|
+
'list_key': list_key,
|
|
156
|
+
'items': 'e.detail.items || e.detail'
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
def bind_state(self,
|
|
161
|
+
state_key: str,
|
|
162
|
+
on_set: str = None,
|
|
163
|
+
on_increment: str = None,
|
|
164
|
+
on_decrement: str = None) -> None:
|
|
165
|
+
"""Register event bindings for state
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
state_key: State key
|
|
169
|
+
on_set: Event name to trigger set
|
|
170
|
+
on_increment: Event name to trigger increment
|
|
171
|
+
on_decrement: Event name to trigger decrement
|
|
172
|
+
"""
|
|
173
|
+
if on_set:
|
|
174
|
+
self._register_binding(on_set, {
|
|
175
|
+
'type': 'state.set',
|
|
176
|
+
'params': {
|
|
177
|
+
'state_key': state_key,
|
|
178
|
+
'value': 'e.detail.value || e.detail'
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
if on_increment:
|
|
183
|
+
self._register_binding(on_increment, {
|
|
184
|
+
'type': 'state.increment',
|
|
185
|
+
'params': {
|
|
186
|
+
'state_key': state_key,
|
|
187
|
+
'amount': 'e.detail.amount || 1'
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
if on_decrement:
|
|
192
|
+
self._register_binding(on_decrement, {
|
|
193
|
+
'type': 'state.decrement',
|
|
194
|
+
'params': {
|
|
195
|
+
'state_key': state_key,
|
|
196
|
+
'amount': 'e.detail.amount || 1'
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
def bind_feedback(self,
|
|
201
|
+
on_toast: str = None,
|
|
202
|
+
message_expr: str = None,
|
|
203
|
+
variant: str = 'neutral',
|
|
204
|
+
duration: int = 3000) -> None:
|
|
205
|
+
"""Register feedback (toast) binding
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
on_toast: Event name to trigger toast
|
|
209
|
+
message_expr: Message JS expression (default: 'e.detail.message')
|
|
210
|
+
variant: Toast variant
|
|
211
|
+
duration: Display duration (ms)
|
|
212
|
+
"""
|
|
213
|
+
if on_toast:
|
|
214
|
+
self._register_binding(on_toast, {
|
|
215
|
+
'type': 'feedback.toast',
|
|
216
|
+
'params': {
|
|
217
|
+
'message': message_expr or 'e.detail.message',
|
|
218
|
+
'variant': f"'{variant}'",
|
|
219
|
+
'duration': duration
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
# App-Level Bindings
|
|
224
|
+
|
|
225
|
+
def bind_event(self,
|
|
226
|
+
event_name: str,
|
|
227
|
+
primitives: List[Dict[str, Any]],
|
|
228
|
+
dedupe: bool = True) -> None:
|
|
229
|
+
"""Register app-level event binding (multiple primitives)
|
|
230
|
+
|
|
231
|
+
Use when one event affects multiple UI elements
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
event_name: Event name
|
|
235
|
+
primitives: List of primitives to execute
|
|
236
|
+
dedupe: Prevent duplicate events (default: True)
|
|
237
|
+
"""
|
|
238
|
+
for primitive in primitives:
|
|
239
|
+
# Validate primitive
|
|
240
|
+
valid, msg = validate_primitive(primitive)
|
|
241
|
+
if not valid:
|
|
242
|
+
print(f"[BROADCAST] Warning: Invalid primitive for event '{event_name}': {msg}")
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
self._register_binding(event_name, primitive)
|
|
246
|
+
|
|
247
|
+
# Set metadata
|
|
248
|
+
if dedupe:
|
|
249
|
+
if event_name not in self._bindings:
|
|
250
|
+
self._bindings[event_name] = {'primitives': [], 'meta': {}}
|
|
251
|
+
self._bindings[event_name]['meta']['dedupe'] = True
|
|
252
|
+
|
|
253
|
+
# ========== Internal: Binding Registry ==========
|
|
254
|
+
|
|
255
|
+
def _register_binding(self, event_name: str, primitive: Dict):
|
|
256
|
+
"""Register binding in registry (internal)"""
|
|
257
|
+
if event_name not in self._bindings:
|
|
258
|
+
self._bindings[event_name] = {
|
|
259
|
+
'primitives': [],
|
|
260
|
+
'meta': {'dedupe': True} # Default: prevent duplicates
|
|
261
|
+
}
|
|
262
|
+
self._bindings[event_name]['primitives'].append(primitive)
|
|
263
|
+
|
|
264
|
+
def get_bindings(self) -> Dict:
|
|
265
|
+
"""Return currently registered bindings (for debugging)"""
|
|
266
|
+
return self._bindings
|
|
267
|
+
|
|
268
|
+
# ========== Router & Helpers Injection ==========
|
|
269
|
+
|
|
270
|
+
def register_js_helpers(self) -> str:
|
|
271
|
+
"""
|
|
272
|
+
Register client-side JavaScript helper functions
|
|
273
|
+
|
|
274
|
+
Provides safe DOM manipulation functions for XSS protection.
|
|
275
|
+
Call once at page startup.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
<script> tag containing JavaScript helper functions
|
|
279
|
+
"""
|
|
280
|
+
return """
|
|
281
|
+
<script>
|
|
282
|
+
// ========== Violit JS Helpers (XSS-Safe) ==========
|
|
283
|
+
|
|
284
|
+
window.violit = window.violit || {};
|
|
285
|
+
|
|
286
|
+
// HTML escape (XSS protection)
|
|
287
|
+
window.violit.escapeHtml = (text) => {
|
|
288
|
+
if (!text) return '';
|
|
289
|
+
const div = document.createElement('div');
|
|
290
|
+
div.textContent = text;
|
|
291
|
+
return div.innerHTML;
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// Convert newlines to <br> (safe)
|
|
295
|
+
window.violit.nl2br = (text) => {
|
|
296
|
+
return window.violit.escapeHtml(text).replace(/\\n/g, '<br>');
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// Create Live Card (same structure as Python styled_card)
|
|
300
|
+
window.violit.createLiveCard = (data) => {
|
|
301
|
+
// Create wrapper div (layout consistency)
|
|
302
|
+
const wrapper = document.createElement('div');
|
|
303
|
+
wrapper.style.cssText = 'width: 100%;';
|
|
304
|
+
|
|
305
|
+
const card = document.createElement('sl-card');
|
|
306
|
+
card.setAttribute('data-post-id', data.id);
|
|
307
|
+
card.style.cssText = 'width: 100%;';
|
|
308
|
+
|
|
309
|
+
// Header - LIVE badge only (same as Python styled_card)
|
|
310
|
+
const headerWrapper = document.createElement('div');
|
|
311
|
+
headerWrapper.setAttribute('slot', 'header');
|
|
312
|
+
|
|
313
|
+
const headerInner = document.createElement('div');
|
|
314
|
+
headerInner.style.cssText = 'display: flex; gap: 0.5rem; align-items: center;';
|
|
315
|
+
|
|
316
|
+
const badge = document.createElement('sl-badge');
|
|
317
|
+
badge.setAttribute('variant', 'danger');
|
|
318
|
+
badge.setAttribute('pulse', '');
|
|
319
|
+
badge.innerHTML = '<sl-icon name="circle-fill" style="font-size: 0.5rem;"></sl-icon> LIVE';
|
|
320
|
+
|
|
321
|
+
headerInner.appendChild(badge);
|
|
322
|
+
headerWrapper.appendChild(headerInner);
|
|
323
|
+
|
|
324
|
+
// Body - content (XSS protection: use escapeHtml)
|
|
325
|
+
const body = document.createElement('div');
|
|
326
|
+
body.style.cssText = 'font-size: 1.1rem; line-height: 1.6; white-space: pre-wrap;';
|
|
327
|
+
body.textContent = data.content; // textContent auto-escapes
|
|
328
|
+
|
|
329
|
+
// Footer - timestamp (same as Python styled_card)
|
|
330
|
+
const footerWrapper = document.createElement('div');
|
|
331
|
+
footerWrapper.setAttribute('slot', 'footer');
|
|
332
|
+
|
|
333
|
+
const footerInner = document.createElement('div');
|
|
334
|
+
footerInner.style.cssText = 'text-align: right; font-size: 0.85rem; color: var(--sl-color-neutral-600);';
|
|
335
|
+
footerInner.innerHTML = `<sl-icon name="clock"></sl-icon> ${window.violit.escapeHtml(data.created_at)}`;
|
|
336
|
+
|
|
337
|
+
footerWrapper.appendChild(footerInner);
|
|
338
|
+
|
|
339
|
+
card.appendChild(headerWrapper);
|
|
340
|
+
card.appendChild(body);
|
|
341
|
+
card.appendChild(footerWrapper);
|
|
342
|
+
|
|
343
|
+
wrapper.appendChild(card);
|
|
344
|
+
|
|
345
|
+
return wrapper; // Return wrapper
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
console.log('✅ Violit helpers loaded (XSS-safe)');
|
|
349
|
+
</script>
|
|
350
|
+
"""
|
|
351
|
+
|
|
352
|
+
def inject_all(self) -> None:
|
|
353
|
+
"""
|
|
354
|
+
Auto-inject helpers + router into HTML template (convenience method)
|
|
355
|
+
|
|
356
|
+
This method calls both register_js_helpers() and inject_router()
|
|
357
|
+
to automatically inject into HTML template.
|
|
358
|
+
|
|
359
|
+
Call once at page startup or in setup function.
|
|
360
|
+
|
|
361
|
+
Example:
|
|
362
|
+
def setup_bindings():
|
|
363
|
+
# ... binding setup ...
|
|
364
|
+
app.broadcaster.inject_all() # Auto-inject
|
|
365
|
+
"""
|
|
366
|
+
if self._router_injected:
|
|
367
|
+
print("[BROADCAST] Warning: Scripts already injected!")
|
|
368
|
+
return
|
|
369
|
+
|
|
370
|
+
helpers_script = self.register_js_helpers()
|
|
371
|
+
router_script = self.inject_router()
|
|
372
|
+
|
|
373
|
+
# Modify HTML_TEMPLATE in violit.app module
|
|
374
|
+
from . import app as vl_app_module
|
|
375
|
+
vl_app_module.HTML_TEMPLATE = vl_app_module.HTML_TEMPLATE.replace(
|
|
376
|
+
'</body>',
|
|
377
|
+
f'{helpers_script}{router_script}</body>'
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
print("[BROADCAST] [OK] Helpers and Router injected into HTML template")
|
|
381
|
+
|
|
382
|
+
def inject_router(self) -> str:
|
|
383
|
+
"""
|
|
384
|
+
Inject single event router + binding data
|
|
385
|
+
|
|
386
|
+
This method should be called once at page startup.
|
|
387
|
+
Injects a single router that handles all events and registered bindings.
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
<script> tag (router + binding data)
|
|
391
|
+
|
|
392
|
+
Example:
|
|
393
|
+
# Manual injection (advanced users)
|
|
394
|
+
vl.app.HTML_TEMPLATE = vl.app.HTML_TEMPLATE.replace(
|
|
395
|
+
'</body>',
|
|
396
|
+
f'{app.broadcaster.register_js_helpers()}'
|
|
397
|
+
f'{app.broadcaster.inject_router()}'
|
|
398
|
+
f'</body>'
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
# Auto injection (recommended)
|
|
402
|
+
app.broadcaster.inject_all()
|
|
403
|
+
"""
|
|
404
|
+
if self._router_injected:
|
|
405
|
+
print("[BROADCAST] Warning: Router already injected!")
|
|
406
|
+
|
|
407
|
+
self._router_injected = True
|
|
408
|
+
bindings_json = json.dumps(self._bindings, ensure_ascii=False)
|
|
409
|
+
|
|
410
|
+
return f"""
|
|
411
|
+
<script>
|
|
412
|
+
// ========== Violit Event Router (Single Instance) ==========
|
|
413
|
+
|
|
414
|
+
window.violit = window.violit || {{}};
|
|
415
|
+
window.violit.eventCache = window.violit.eventCache || new Map(); // Prevent duplicates
|
|
416
|
+
window.violit.bindings = {bindings_json};
|
|
417
|
+
|
|
418
|
+
// ========== UI Primitives Implementation ==========
|
|
419
|
+
|
|
420
|
+
window.violit.primitives = {{
|
|
421
|
+
// ===== List Operations =====
|
|
422
|
+
'list.append': (params) => {{
|
|
423
|
+
const container = document.getElementById(`${{params.list_key}}_reactive_list_container`);
|
|
424
|
+
if (!container) {{
|
|
425
|
+
console.warn(`[Violit] List container not found: ${{params.list_key}}`);
|
|
426
|
+
return;
|
|
427
|
+
}}
|
|
428
|
+
|
|
429
|
+
const data = params.item_data;
|
|
430
|
+
let element;
|
|
431
|
+
|
|
432
|
+
// Use custom HTML if provided (flexibility)
|
|
433
|
+
if (data.html) {{
|
|
434
|
+
const temp = document.createElement('div');
|
|
435
|
+
temp.innerHTML = data.html;
|
|
436
|
+
element = temp.firstChild;
|
|
437
|
+
}} else {{
|
|
438
|
+
// Create default card (backward compatibility)
|
|
439
|
+
element = window.violit.createLiveCard(data);
|
|
440
|
+
}}
|
|
441
|
+
|
|
442
|
+
if (params.position === 'prepend') {{
|
|
443
|
+
container.insertBefore(element, container.firstChild);
|
|
444
|
+
}} else {{
|
|
445
|
+
container.appendChild(element);
|
|
446
|
+
}}
|
|
447
|
+
|
|
448
|
+
// Animation
|
|
449
|
+
element.style.opacity = '0';
|
|
450
|
+
element.style.transform = 'translateY(-20px)';
|
|
451
|
+
setTimeout(() => {{
|
|
452
|
+
element.style.transition = 'all 0.5s ease-out';
|
|
453
|
+
element.style.opacity = '1';
|
|
454
|
+
element.style.transform = 'translateY(0)';
|
|
455
|
+
}}, 10);
|
|
456
|
+
|
|
457
|
+
console.log(`[Violit] List append: ${{params.list_key}}`, data.html ? '(custom HTML)' : '(default card)');
|
|
458
|
+
}},
|
|
459
|
+
|
|
460
|
+
'list.remove': (params) => {{
|
|
461
|
+
const itemId = params.item_id;
|
|
462
|
+
const cards = document.querySelectorAll(`[data-post-id="${{itemId}}"]`);
|
|
463
|
+
|
|
464
|
+
cards.forEach(card => {{
|
|
465
|
+
card.style.transition = 'all 500ms ease-out';
|
|
466
|
+
card.style.opacity = '0';
|
|
467
|
+
card.style.transform = 'translateX(50px)';
|
|
468
|
+
setTimeout(() => card.remove(), 500);
|
|
469
|
+
}});
|
|
470
|
+
|
|
471
|
+
console.log(`[Violit] List remove: ${{itemId}}`);
|
|
472
|
+
}},
|
|
473
|
+
|
|
474
|
+
'list.update': (params) => {{
|
|
475
|
+
const itemId = params.item_id;
|
|
476
|
+
const card = document.querySelector(`[data-post-id="${{itemId}}"]`);
|
|
477
|
+
if (card) {{
|
|
478
|
+
const newCard = window.violit.createLiveCard(params.item_data);
|
|
479
|
+
card.replaceWith(newCard);
|
|
480
|
+
console.log(`[Violit] List update: ${{itemId}}`);
|
|
481
|
+
}}
|
|
482
|
+
}},
|
|
483
|
+
|
|
484
|
+
'list.replace_all': (params) => {{
|
|
485
|
+
const container = document.getElementById(`${{params.list_key}}_reactive_list_container`);
|
|
486
|
+
if (container) {{
|
|
487
|
+
container.innerHTML = '';
|
|
488
|
+
params.items.forEach(item => {{
|
|
489
|
+
const card = window.violit.createLiveCard(item);
|
|
490
|
+
container.appendChild(card);
|
|
491
|
+
}});
|
|
492
|
+
console.log(`[Violit] List replace all: ${{params.list_key}}`);
|
|
493
|
+
}}
|
|
494
|
+
}},
|
|
495
|
+
|
|
496
|
+
// ===== State Operations =====
|
|
497
|
+
'state.set': (params) => {{
|
|
498
|
+
const elements = document.querySelectorAll(`[data-state-key="${{params.state_key}}"]`);
|
|
499
|
+
elements.forEach(el => {{
|
|
500
|
+
el.textContent = params.value;
|
|
501
|
+
}});
|
|
502
|
+
console.log(`[Violit] State set: ${{params.state_key}} = ${{params.value}}`);
|
|
503
|
+
}},
|
|
504
|
+
|
|
505
|
+
'state.increment': (params) => {{
|
|
506
|
+
const elements = document.querySelectorAll(`[data-state-key="${{params.state_key}}"]`);
|
|
507
|
+
elements.forEach(el => {{
|
|
508
|
+
const current = parseInt(el.textContent) || 0;
|
|
509
|
+
el.textContent = current + (params.amount || 1);
|
|
510
|
+
}});
|
|
511
|
+
console.log(`[Violit] State increment: ${{params.state_key}} +${{params.amount || 1}}`);
|
|
512
|
+
}},
|
|
513
|
+
|
|
514
|
+
'state.decrement': (params) => {{
|
|
515
|
+
const elements = document.querySelectorAll(`[data-state-key="${{params.state_key}}"]`);
|
|
516
|
+
elements.forEach(el => {{
|
|
517
|
+
const current = parseInt(el.textContent) || 0;
|
|
518
|
+
el.textContent = current - (params.amount || 1);
|
|
519
|
+
}});
|
|
520
|
+
console.log(`[Violit] State decrement: ${{params.state_key}} -${{params.amount || 1}}`);
|
|
521
|
+
}},
|
|
522
|
+
|
|
523
|
+
// ===== DOM Operations =====
|
|
524
|
+
'dom.insert': (params) => {{
|
|
525
|
+
const container = document.getElementById(params.container_id);
|
|
526
|
+
if (container) {{
|
|
527
|
+
const temp = document.createElement('div');
|
|
528
|
+
temp.innerHTML = params.html;
|
|
529
|
+
const element = temp.firstChild;
|
|
530
|
+
|
|
531
|
+
if (params.position === 'prepend') {{
|
|
532
|
+
container.insertBefore(element, container.firstChild);
|
|
533
|
+
}} else {{
|
|
534
|
+
container.appendChild(element);
|
|
535
|
+
}}
|
|
536
|
+
console.log(`[Violit] DOM insert: ${{params.container_id}}`);
|
|
537
|
+
}}
|
|
538
|
+
}},
|
|
539
|
+
|
|
540
|
+
'dom.remove': (params) => {{
|
|
541
|
+
const elements = document.querySelectorAll(params.selector);
|
|
542
|
+
elements.forEach(el => {{
|
|
543
|
+
if (params.animate) {{
|
|
544
|
+
el.style.transition = 'opacity 300ms';
|
|
545
|
+
el.style.opacity = '0';
|
|
546
|
+
setTimeout(() => el.remove(), 300);
|
|
547
|
+
}} else {{
|
|
548
|
+
el.remove();
|
|
549
|
+
}}
|
|
550
|
+
}});
|
|
551
|
+
console.log(`[Violit] DOM remove: ${{params.selector}}`);
|
|
552
|
+
}},
|
|
553
|
+
|
|
554
|
+
'dom.update': (params) => {{
|
|
555
|
+
const elements = document.querySelectorAll(params.selector);
|
|
556
|
+
elements.forEach(el => {{
|
|
557
|
+
el.innerHTML = params.html;
|
|
558
|
+
}});
|
|
559
|
+
console.log(`[Violit] DOM update: ${{params.selector}}`);
|
|
560
|
+
}},
|
|
561
|
+
|
|
562
|
+
// ===== Feedback Operations =====
|
|
563
|
+
'feedback.toast': (params) => {{
|
|
564
|
+
const alert = document.createElement('sl-alert');
|
|
565
|
+
alert.variant = params.variant || 'neutral';
|
|
566
|
+
alert.closable = true;
|
|
567
|
+
alert.duration = params.duration || 3000;
|
|
568
|
+
|
|
569
|
+
const iconName = {{
|
|
570
|
+
'success': 'check-circle',
|
|
571
|
+
'warning': 'exclamation-triangle',
|
|
572
|
+
'danger': 'exclamation-octagon',
|
|
573
|
+
'neutral': 'info-circle'
|
|
574
|
+
}}[params.variant] || 'info-circle';
|
|
575
|
+
|
|
576
|
+
alert.innerHTML = `<sl-icon slot="icon" name="${{iconName}}"></sl-icon>${{params.message}}`;
|
|
577
|
+
document.body.appendChild(alert);
|
|
578
|
+
alert.toast();
|
|
579
|
+
|
|
580
|
+
console.log(`[Violit] Toast: ${{params.message}}`);
|
|
581
|
+
}},
|
|
582
|
+
|
|
583
|
+
'feedback.badge': (params) => {{
|
|
584
|
+
const badge = document.querySelector(`#${{params.badge_id}}`);
|
|
585
|
+
if (badge) {{
|
|
586
|
+
badge.textContent = params.value;
|
|
587
|
+
console.log(`[Violit] Badge update: ${{params.badge_id}} = ${{params.value}}`);
|
|
588
|
+
}}
|
|
589
|
+
}}
|
|
590
|
+
}};
|
|
591
|
+
|
|
592
|
+
// ========== Event Router ==========
|
|
593
|
+
|
|
594
|
+
window.violit.routeEvent = (eventName, detail) => {{
|
|
595
|
+
const binding = window.violit.bindings[eventName];
|
|
596
|
+
if (!binding) {{
|
|
597
|
+
console.warn(`[Violit] No binding found for event: ${{eventName}}`);
|
|
598
|
+
return;
|
|
599
|
+
}}
|
|
600
|
+
|
|
601
|
+
// Check duplicates
|
|
602
|
+
if (binding.meta?.dedupe) {{
|
|
603
|
+
const eventId = detail._eventId || JSON.stringify(detail);
|
|
604
|
+
if (window.violit.eventCache.has(eventId)) {{
|
|
605
|
+
console.log(`[Violit] Duplicate event ignored: ${{eventName}}`);
|
|
606
|
+
return;
|
|
607
|
+
}}
|
|
608
|
+
window.violit.eventCache.set(eventId, Date.now());
|
|
609
|
+
|
|
610
|
+
// Clean cache (keep only recent 100)
|
|
611
|
+
if (window.violit.eventCache.size > 100) {{
|
|
612
|
+
const firstKey = window.violit.eventCache.keys().next().value;
|
|
613
|
+
window.violit.eventCache.delete(firstKey);
|
|
614
|
+
}}
|
|
615
|
+
}}
|
|
616
|
+
|
|
617
|
+
// Execute primitives
|
|
618
|
+
binding.primitives.forEach(primitive => {{
|
|
619
|
+
try {{
|
|
620
|
+
// Evaluate parameters (execute JS expressions)
|
|
621
|
+
const evaluatedParams = {{}};
|
|
622
|
+
const e = {{ detail }}; // Event object
|
|
623
|
+
|
|
624
|
+
for (const [key, value] of Object.entries(primitive.params)) {{
|
|
625
|
+
if (typeof value === 'string' && value.includes('e.detail')) {{
|
|
626
|
+
try {{
|
|
627
|
+
evaluatedParams[key] = eval(value);
|
|
628
|
+
}} catch (err) {{
|
|
629
|
+
console.warn(`[Violit] Failed to evaluate param ${{key}}: ${{value}}`, err);
|
|
630
|
+
evaluatedParams[key] = value;
|
|
631
|
+
}}
|
|
632
|
+
}} else {{
|
|
633
|
+
evaluatedParams[key] = value;
|
|
634
|
+
}}
|
|
635
|
+
}}
|
|
636
|
+
|
|
637
|
+
// Execute primitive
|
|
638
|
+
const primitiveFunc = window.violit.primitives[primitive.type];
|
|
639
|
+
if (primitiveFunc) {{
|
|
640
|
+
primitiveFunc(evaluatedParams);
|
|
641
|
+
}} else {{
|
|
642
|
+
console.error(`[Violit] Unknown primitive: ${{primitive.type}}`);
|
|
643
|
+
}}
|
|
644
|
+
}} catch (error) {{
|
|
645
|
+
console.error(`[Violit] Error executing primitive ${{primitive.type}}:`, error);
|
|
646
|
+
}}
|
|
647
|
+
}});
|
|
648
|
+
|
|
649
|
+
console.log(`✅ Event routed: ${{eventName}}`);
|
|
650
|
+
}};
|
|
651
|
+
|
|
652
|
+
// ========== Global Event Listener (Single Instance) ==========
|
|
653
|
+
|
|
654
|
+
// Prevent duplicate listener registration on page reload
|
|
655
|
+
if (!window.violit.listenersRegistered) {{
|
|
656
|
+
// Connect all custom events to router
|
|
657
|
+
Object.keys(window.violit.bindings).forEach(eventName => {{
|
|
658
|
+
window.addEventListener(eventName, (e) => {{
|
|
659
|
+
window.violit.routeEvent(eventName, e.detail);
|
|
660
|
+
}});
|
|
661
|
+
}});
|
|
662
|
+
|
|
663
|
+
window.violit.listenersRegistered = true;
|
|
664
|
+
console.log('✅ Violit Event Router loaded');
|
|
665
|
+
console.log('📋 Registered events:', Object.keys(window.violit.bindings));
|
|
666
|
+
}} else {{
|
|
667
|
+
console.log('⚠️ Violit Event Router already loaded (skipped duplicate registration)');
|
|
668
|
+
}}
|
|
669
|
+
</script>
|
|
670
|
+
"""
|
|
671
|
+
|
|
672
|
+
# ========== Legacy/Compatibility APIs ==========
|
|
673
|
+
|
|
674
|
+
def reload_all(self, exclude_current: bool = False):
|
|
675
|
+
"""Reload all client pages (Legacy API)"""
|
|
676
|
+
self.eval_all("window.location.reload();", exclude_current=exclude_current)
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
# Convenience function
|
|
680
|
+
def create_broadcaster(app) -> Broadcaster:
|
|
681
|
+
"""
|
|
682
|
+
Create Broadcaster instance
|
|
683
|
+
|
|
684
|
+
Args:
|
|
685
|
+
app: Violit App instance
|
|
686
|
+
|
|
687
|
+
Returns:
|
|
688
|
+
Broadcaster instance
|
|
689
|
+
"""
|
|
690
|
+
return Broadcaster(app)
|