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
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"""Form Widgets Mixin for Violit"""
|
|
2
|
+
|
|
3
|
+
from typing import Union, Callable, Optional
|
|
4
|
+
from ..component import Component
|
|
5
|
+
from ..context import rendering_ctx, fragment_ctx
|
|
6
|
+
from ..state import get_session_store
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FormWidgetsMixin:
|
|
10
|
+
"""Form-related widgets (form, form_submit_button, button, download_button, link_button, page_link)"""
|
|
11
|
+
|
|
12
|
+
def button(self, text: Union[str, Callable], on_click: Optional[Callable] = None, variant="primary", **props):
|
|
13
|
+
"""Display button"""
|
|
14
|
+
cid = self._get_next_cid("btn")
|
|
15
|
+
def builder():
|
|
16
|
+
token = rendering_ctx.set(cid)
|
|
17
|
+
bt = text() if callable(text) else text
|
|
18
|
+
rendering_ctx.reset(token)
|
|
19
|
+
attrs = self.engine.click_attrs(cid)
|
|
20
|
+
return Component("sl-button", id=cid, content=bt, variant=variant, **attrs, **props)
|
|
21
|
+
self._register_component(cid, builder, action=on_click)
|
|
22
|
+
|
|
23
|
+
def download_button(self, label, data, file_name, mime="text/plain", on_click=None, **props):
|
|
24
|
+
"""Download button (Streamlit-compatible interface)
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
label: Button label
|
|
28
|
+
data: Data to download (str, bytes, or file-like)
|
|
29
|
+
file_name: Name for the downloaded file
|
|
30
|
+
mime: MIME type of the file
|
|
31
|
+
on_click: Optional callback when button is clicked (called AFTER download)
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
None
|
|
35
|
+
"""
|
|
36
|
+
cid = self._get_next_cid("download_btn")
|
|
37
|
+
|
|
38
|
+
def builder():
|
|
39
|
+
import base64
|
|
40
|
+
|
|
41
|
+
# Convert data to downloadable format
|
|
42
|
+
if isinstance(data, str):
|
|
43
|
+
data_bytes = data.encode('utf-8')
|
|
44
|
+
elif isinstance(data, bytes):
|
|
45
|
+
data_bytes = data
|
|
46
|
+
else:
|
|
47
|
+
# Try to convert to string
|
|
48
|
+
data_bytes = str(data).encode('utf-8')
|
|
49
|
+
|
|
50
|
+
# Create data URL
|
|
51
|
+
data_base64 = base64.b64encode(data_bytes).decode('utf-8')
|
|
52
|
+
data_url = f"data:{mime};base64,{data_base64}"
|
|
53
|
+
|
|
54
|
+
# Check for Native Mode (pywebview)
|
|
55
|
+
is_native = False
|
|
56
|
+
try:
|
|
57
|
+
import webview
|
|
58
|
+
if len(webview.windows) > 0:
|
|
59
|
+
is_native = True
|
|
60
|
+
except ImportError:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
if is_native:
|
|
64
|
+
# Native Mode: Use Server-Side Save Dialog
|
|
65
|
+
def native_save_action(v=None):
|
|
66
|
+
try:
|
|
67
|
+
import webview
|
|
68
|
+
import os
|
|
69
|
+
|
|
70
|
+
# Open Save Dialog
|
|
71
|
+
# Open Save Dialog
|
|
72
|
+
ext = file_name.split('.')[-1] if '.' in file_name else "*"
|
|
73
|
+
file_types = (f"{ext.upper()} File (*.{ext})", "All files (*.*)")
|
|
74
|
+
save_location = webview.windows[0].create_file_dialog(
|
|
75
|
+
webview.SAVE_DIALOG,
|
|
76
|
+
save_filename=file_name,
|
|
77
|
+
file_types=file_types
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if save_location:
|
|
81
|
+
if isinstance(save_location, list): save_location = save_location[0]
|
|
82
|
+
with open(save_location, "wb") as f:
|
|
83
|
+
f.write(data_bytes)
|
|
84
|
+
|
|
85
|
+
# Toast is not easily accessible here without app reference or a way to push JS
|
|
86
|
+
# But we can try pushing a toast if we are in a callback
|
|
87
|
+
# For now, just print to console or rely on OS feedback (file created)
|
|
88
|
+
print(f"[Native] Saved to {save_location}")
|
|
89
|
+
|
|
90
|
+
# Try to trigger a success toast via eval if possible
|
|
91
|
+
from ..state import get_session_store
|
|
92
|
+
store = get_session_store()
|
|
93
|
+
if 'toasts' not in store: store['toasts'] = []
|
|
94
|
+
store['toasts'].append({"message": f"Saved to {os.path.basename(save_location)}", "variant": "success", "icon": "check-circle"})
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
print(f"[Native] Save failed: {e}")
|
|
98
|
+
|
|
99
|
+
# Register the native save action
|
|
100
|
+
# We need to register it with the SAME cid
|
|
101
|
+
# NOTE: The outer _register_component calls with action=on_click (None).
|
|
102
|
+
# We need to override that or use a different mechanism.
|
|
103
|
+
# Since we are inside builder, we can re-register or use a specific event handler?
|
|
104
|
+
# Actually, the simplest way is to overwrite the action in the store right here,
|
|
105
|
+
# BUT builder is called during render. Registering action during render is tricky for the *current* cycle
|
|
106
|
+
# if the component ID is already registered.
|
|
107
|
+
# However, this builder is run by the framework.
|
|
108
|
+
|
|
109
|
+
# BETTER APPROACH: Set the onclick to sendAction
|
|
110
|
+
# and ensure the action mapped to this CID is our native_save_action.
|
|
111
|
+
|
|
112
|
+
# We'll rely on the fact that if we provide an onclick behavior that sends action,
|
|
113
|
+
# we need the backend to execute native_save_action.
|
|
114
|
+
|
|
115
|
+
# Let's monkey-patch the action for this specific instance if we can,
|
|
116
|
+
# OR return a component that has the right onclick attribute.
|
|
117
|
+
|
|
118
|
+
# In App.register_component, actions are stored.
|
|
119
|
+
# We can't easily change the registered action from *inside* the builder
|
|
120
|
+
# because the registration happens *outside* usually (lines 51 self._register_component).
|
|
121
|
+
|
|
122
|
+
# TRICK: We will not use the `on_click` argument passed to download_button for the native logic.
|
|
123
|
+
# Instead, we define the action wrapper here and stick it into the store manually?
|
|
124
|
+
# Or we can just modify the way download_button registers itself.
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
if is_native:
|
|
128
|
+
# Override global action for this component to be the save dialog
|
|
129
|
+
from ..state import get_session_store
|
|
130
|
+
store = get_session_store()
|
|
131
|
+
store['actions'][cid] = native_save_action
|
|
132
|
+
|
|
133
|
+
# Check if we're in lite mode or ws mode
|
|
134
|
+
if self.mode == 'lite':
|
|
135
|
+
html = f'''
|
|
136
|
+
<sl-button variant="primary" hx-post="/action/{cid}" hx-swap="none" hx-trigger="click">
|
|
137
|
+
<sl-icon slot="prefix" name="download"></sl-icon>
|
|
138
|
+
{label}
|
|
139
|
+
</sl-button>
|
|
140
|
+
'''
|
|
141
|
+
else:
|
|
142
|
+
html = f'''
|
|
143
|
+
<sl-button variant="primary" onclick="window.sendAction('{cid}')">
|
|
144
|
+
<sl-icon slot="prefix" name="download"></sl-icon>
|
|
145
|
+
{label}
|
|
146
|
+
</sl-button>
|
|
147
|
+
'''
|
|
148
|
+
else:
|
|
149
|
+
# Web Mode: JS Download
|
|
150
|
+
download_script = f"""
|
|
151
|
+
<script>
|
|
152
|
+
window.download_{cid} = function() {{
|
|
153
|
+
const link = document.createElement('a');
|
|
154
|
+
link.href = '{data_url}';
|
|
155
|
+
link.download = '{file_name}';
|
|
156
|
+
document.body.appendChild(link);
|
|
157
|
+
link.click();
|
|
158
|
+
document.body.removeChild(link);
|
|
159
|
+
}};
|
|
160
|
+
</script>
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
html = f'''
|
|
164
|
+
{download_script}
|
|
165
|
+
<sl-button variant="primary" onclick="download_{cid}()">
|
|
166
|
+
<sl-icon slot="prefix" name="download"></sl-icon>
|
|
167
|
+
{label}
|
|
168
|
+
</sl-button>
|
|
169
|
+
'''
|
|
170
|
+
return Component("div", id=cid, content=html)
|
|
171
|
+
|
|
172
|
+
self._register_component(cid, builder, action=on_click)
|
|
173
|
+
|
|
174
|
+
def link_button(self, label, url, **props):
|
|
175
|
+
"""Display link button"""
|
|
176
|
+
cid = self._get_next_cid("link_btn")
|
|
177
|
+
|
|
178
|
+
def builder():
|
|
179
|
+
html = f'''
|
|
180
|
+
<sl-button variant="primary" href="{url}" target="_blank">
|
|
181
|
+
<sl-icon slot="prefix" name="box-arrow-up-right"></sl-icon>
|
|
182
|
+
{label}
|
|
183
|
+
</sl-button>
|
|
184
|
+
'''
|
|
185
|
+
return Component("div", id=cid, content=html)
|
|
186
|
+
|
|
187
|
+
self._register_component(cid, builder)
|
|
188
|
+
|
|
189
|
+
def page_link(self, page, label, icon=None, **props):
|
|
190
|
+
"""Display page navigation link"""
|
|
191
|
+
cid = self._get_next_cid("page_link")
|
|
192
|
+
|
|
193
|
+
def builder():
|
|
194
|
+
icon_html = f'<sl-icon slot="prefix" name="{icon}"></sl-icon>' if icon else ""
|
|
195
|
+
# In a real implementation, this would trigger page navigation
|
|
196
|
+
# For now, just render as a styled link
|
|
197
|
+
html = f'''
|
|
198
|
+
<a href="{page}" style="display:inline-flex;align-items:center;gap:0.5rem;color:var(--sl-primary);text-decoration:none;padding:0.5rem 1rem;border-radius:0.25rem;transition:background 0.2s;">
|
|
199
|
+
{icon_html}
|
|
200
|
+
{label}
|
|
201
|
+
</a>
|
|
202
|
+
'''
|
|
203
|
+
return Component("div", id=cid, content=html)
|
|
204
|
+
|
|
205
|
+
self._register_component(cid, builder)
|
|
206
|
+
|
|
207
|
+
def switch_page(self, page):
|
|
208
|
+
"""Switch to a different page (navigation)"""
|
|
209
|
+
# This would be implemented with the navigation system
|
|
210
|
+
# For now, we can use JavaScript to navigate
|
|
211
|
+
code = f"window.location.href = '{page}';"
|
|
212
|
+
if self.mode == 'ws':
|
|
213
|
+
store = get_session_store()
|
|
214
|
+
if 'eval_queue' not in store: store['eval_queue'] = []
|
|
215
|
+
store['eval_queue'].append(code)
|
|
216
|
+
else:
|
|
217
|
+
# For lite mode, we could inject a script
|
|
218
|
+
cid = self._get_next_cid("page_switch")
|
|
219
|
+
def builder():
|
|
220
|
+
html = f'<script>{code}</script>'
|
|
221
|
+
return Component("div", id=cid, content=html, style="display:none;")
|
|
222
|
+
self._register_component(cid, builder)
|
|
223
|
+
|
|
224
|
+
def form(self, key=None, clear_on_submit=False):
|
|
225
|
+
"""Create a form container"""
|
|
226
|
+
form_id = f"form_{key}" if key else self._get_next_cid("form")
|
|
227
|
+
|
|
228
|
+
class FormContext:
|
|
229
|
+
def __init__(self, app, form_id, clear_on_submit):
|
|
230
|
+
self.app = app
|
|
231
|
+
self.form_id = form_id
|
|
232
|
+
self.clear_on_submit = clear_on_submit
|
|
233
|
+
self.submitted = False
|
|
234
|
+
self.form_data = {}
|
|
235
|
+
|
|
236
|
+
def __enter__(self):
|
|
237
|
+
self.token = fragment_ctx.set(self.form_id)
|
|
238
|
+
return self
|
|
239
|
+
|
|
240
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
241
|
+
fragment_ctx.reset(self.token)
|
|
242
|
+
|
|
243
|
+
# Register form builder
|
|
244
|
+
def builder():
|
|
245
|
+
store = get_session_store()
|
|
246
|
+
|
|
247
|
+
# Render form components
|
|
248
|
+
htmls = []
|
|
249
|
+
# Check static
|
|
250
|
+
for cid, b in self.app.static_fragment_components.get(self.form_id, []):
|
|
251
|
+
htmls.append(b().render())
|
|
252
|
+
# Check session
|
|
253
|
+
for cid, b in store['fragment_components'].get(self.form_id, []):
|
|
254
|
+
htmls.append(b().render())
|
|
255
|
+
|
|
256
|
+
inner_html = "".join(htmls)
|
|
257
|
+
html = f'''
|
|
258
|
+
<form id="{self.form_id}_element" style="display:flex;flex-direction:column;gap:1rem;padding:1rem;border:1px solid var(--sl-border);border-radius:0.5rem;background:var(--sl-bg-card);">
|
|
259
|
+
{inner_html}
|
|
260
|
+
</form>
|
|
261
|
+
'''
|
|
262
|
+
return Component("div", id=self.form_id, content=html)
|
|
263
|
+
|
|
264
|
+
self.app._register_component(self.form_id, builder)
|
|
265
|
+
|
|
266
|
+
def __getattr__(self, name):
|
|
267
|
+
return getattr(self.app, name)
|
|
268
|
+
|
|
269
|
+
return FormContext(self, form_id, clear_on_submit)
|
|
270
|
+
|
|
271
|
+
def form_submit_button(self, label="Submit", on_click=None, **props):
|
|
272
|
+
"""Form submit button"""
|
|
273
|
+
cid = self._get_next_cid("form_submit")
|
|
274
|
+
|
|
275
|
+
def action():
|
|
276
|
+
# Collect form data and call on_click
|
|
277
|
+
if on_click:
|
|
278
|
+
on_click()
|
|
279
|
+
|
|
280
|
+
def builder():
|
|
281
|
+
attrs = self.engine.click_attrs(cid)
|
|
282
|
+
html = f'''
|
|
283
|
+
<sl-button type="submit" variant="primary" **{attrs}>
|
|
284
|
+
<sl-icon slot="prefix" name="check-circle"></sl-icon>
|
|
285
|
+
{label}
|
|
286
|
+
</sl-button>
|
|
287
|
+
'''
|
|
288
|
+
return Component("div", id=cid, content=html)
|
|
289
|
+
|
|
290
|
+
self._register_component(cid, builder, action=action)
|
|
291
|
+
|
|
292
|
+
def save_file(self, data, file_path, toast_message=None):
|
|
293
|
+
"""Save data to local file system
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
data: Data to save (str, bytes, or file-like object)
|
|
297
|
+
file_path: Path where to save the file
|
|
298
|
+
toast_message: Optional success message to show
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
bool: True if successful, False otherwise
|
|
302
|
+
"""
|
|
303
|
+
import os
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
# Convert data to bytes if needed
|
|
307
|
+
if isinstance(data, str):
|
|
308
|
+
data_bytes = data.encode('utf-8')
|
|
309
|
+
elif isinstance(data, bytes):
|
|
310
|
+
data_bytes = data
|
|
311
|
+
elif hasattr(data, 'read'):
|
|
312
|
+
# File-like object
|
|
313
|
+
data_bytes = data.read()
|
|
314
|
+
if isinstance(data_bytes, str):
|
|
315
|
+
data_bytes = data_bytes.encode('utf-8')
|
|
316
|
+
else:
|
|
317
|
+
data_bytes = str(data).encode('utf-8')
|
|
318
|
+
|
|
319
|
+
# Create directory if needed
|
|
320
|
+
directory = os.path.dirname(file_path)
|
|
321
|
+
if directory and not os.path.exists(directory):
|
|
322
|
+
os.makedirs(directory)
|
|
323
|
+
|
|
324
|
+
# Write file
|
|
325
|
+
with open(file_path, 'wb') as f:
|
|
326
|
+
f.write(data_bytes)
|
|
327
|
+
|
|
328
|
+
# Show toast if message provided
|
|
329
|
+
if toast_message:
|
|
330
|
+
self.toast(toast_message, variant="success", icon="check-circle")
|
|
331
|
+
|
|
332
|
+
return True
|
|
333
|
+
|
|
334
|
+
except Exception as e:
|
|
335
|
+
self.toast(f"Failed to save file: {str(e)}", variant="danger", icon="x-circle")
|
|
336
|
+
return False
|
|
337
|
+
|
|
338
|
+
def download_file(self, data, file_name, mime="application/octet-stream", toast_message=None):
|
|
339
|
+
"""Trigger file download (auto-detects Native/Web mode)
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
data: Data to download (str, bytes, or file-like object)
|
|
343
|
+
file_name: Name for the downloaded file
|
|
344
|
+
mime: MIME type of the file
|
|
345
|
+
toast_message: Optional message to show
|
|
346
|
+
|
|
347
|
+
This is a helper that works in button callbacks.
|
|
348
|
+
For declarative UI, use download_button() instead.
|
|
349
|
+
"""
|
|
350
|
+
import os
|
|
351
|
+
|
|
352
|
+
# Convert data to bytes
|
|
353
|
+
if isinstance(data, str):
|
|
354
|
+
data_bytes = data.encode('utf-8')
|
|
355
|
+
elif isinstance(data, bytes):
|
|
356
|
+
data_bytes = data
|
|
357
|
+
elif hasattr(data, 'read'):
|
|
358
|
+
data_bytes = data.read()
|
|
359
|
+
if isinstance(data_bytes, str):
|
|
360
|
+
data_bytes = data_bytes.encode('utf-8')
|
|
361
|
+
else:
|
|
362
|
+
data_bytes = str(data).encode('utf-8')
|
|
363
|
+
|
|
364
|
+
# Check if running in Native mode
|
|
365
|
+
is_native = False
|
|
366
|
+
try:
|
|
367
|
+
import webview
|
|
368
|
+
if len(webview.windows) > 0:
|
|
369
|
+
is_native = True
|
|
370
|
+
except ImportError:
|
|
371
|
+
pass
|
|
372
|
+
|
|
373
|
+
if is_native:
|
|
374
|
+
# Native Mode: File save dialog
|
|
375
|
+
try:
|
|
376
|
+
import webview
|
|
377
|
+
ext = file_name.split('.')[-1] if '.' in file_name else "*"
|
|
378
|
+
file_types = (f"{ext.upper()} File (*.{ext})", "All files (*.*)")
|
|
379
|
+
|
|
380
|
+
save_location = webview.windows[0].create_file_dialog(
|
|
381
|
+
webview.SAVE_DIALOG,
|
|
382
|
+
save_filename=file_name,
|
|
383
|
+
file_types=file_types
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
if save_location:
|
|
387
|
+
if isinstance(save_location, list):
|
|
388
|
+
save_location = save_location[0]
|
|
389
|
+
|
|
390
|
+
with open(save_location, "wb") as f:
|
|
391
|
+
f.write(data_bytes)
|
|
392
|
+
|
|
393
|
+
msg = toast_message or f"Saved to {os.path.basename(save_location)}"
|
|
394
|
+
self.toast(msg, variant="success")
|
|
395
|
+
|
|
396
|
+
except Exception as e:
|
|
397
|
+
self.toast(f"Save failed: {str(e)}", variant="danger")
|
|
398
|
+
else:
|
|
399
|
+
# Web Mode: JavaScript download
|
|
400
|
+
import base64
|
|
401
|
+
data_b64 = base64.b64encode(data_bytes).decode('utf-8')
|
|
402
|
+
|
|
403
|
+
from ..state import get_session_store
|
|
404
|
+
store = get_session_store()
|
|
405
|
+
if 'eval_queue' not in store:
|
|
406
|
+
store['eval_queue'] = []
|
|
407
|
+
|
|
408
|
+
js_code = f"""
|
|
409
|
+
(function() {{
|
|
410
|
+
const a = document.createElement('a');
|
|
411
|
+
a.href = 'data:{mime};base64,{data_b64}';
|
|
412
|
+
a.download = '{file_name}';
|
|
413
|
+
document.body.appendChild(a);
|
|
414
|
+
a.click();
|
|
415
|
+
document.body.removeChild(a);
|
|
416
|
+
}})();
|
|
417
|
+
"""
|
|
418
|
+
store['eval_queue'].append(js_code)
|
|
419
|
+
|
|
420
|
+
msg = toast_message or f"Downloading {file_name}"
|
|
421
|
+
self.toast(msg, variant="success")
|