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,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")