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,30 @@
1
+ """
2
+ Violit Widgets Package
3
+ Organized widget mixins for the App class
4
+ """
5
+
6
+ from .text_widgets import TextWidgetsMixin
7
+ from .input_widgets import InputWidgetsMixin
8
+ from .data_widgets import DataWidgetsMixin
9
+ from .chart_widgets import ChartWidgetsMixin
10
+ from .media_widgets import MediaWidgetsMixin
11
+ from .layout_widgets import LayoutWidgetsMixin
12
+ from .status_widgets import StatusWidgetsMixin
13
+ from .form_widgets import FormWidgetsMixin
14
+ from .chat_widgets import ChatWidgetsMixin
15
+ from .card_widgets import CardWidgetsMixin
16
+ from .list_widgets import ListWidgetsMixin
17
+
18
+ __all__ = [
19
+ 'TextWidgetsMixin',
20
+ 'InputWidgetsMixin',
21
+ 'DataWidgetsMixin',
22
+ 'ChartWidgetsMixin',
23
+ 'MediaWidgetsMixin',
24
+ 'LayoutWidgetsMixin',
25
+ 'StatusWidgetsMixin',
26
+ 'FormWidgetsMixin',
27
+ 'ChatWidgetsMixin',
28
+ 'CardWidgetsMixin',
29
+ 'ListWidgetsMixin',
30
+ ]
@@ -0,0 +1,595 @@
1
+ """
2
+ Card Widgets - Shoelace Card Components
3
+ Provides easy-to-use wrappers for Shoelace card components
4
+ """
5
+
6
+ from ..component import Component
7
+ from ..context import rendering_ctx
8
+
9
+
10
+ class CardWidgetsMixin:
11
+ """Mixin for Shoelace Card components"""
12
+
13
+ def card(self, content=None, header=None, footer=None, **kwargs):
14
+ """
15
+ Create a Shoelace card component
16
+
17
+ Args:
18
+ content: Optional card content (str). If None, use as context manager
19
+ header: Optional header content (str)
20
+ footer: Optional footer content (str)
21
+ **kwargs: Additional attributes (e.g., data_post_id="123", class_="custom")
22
+
23
+ Returns:
24
+ CardContext if content is None, otherwise None
25
+
26
+ Examples:
27
+ # Simple card with content
28
+ app.card("Hello World!")
29
+
30
+ # Card with header and footer
31
+ app.card("Content", header="Title", footer="Footer text")
32
+
33
+ # Context manager for complex content
34
+ with app.card(header="My Card"):
35
+ app.text("Line 1")
36
+ app.text("Line 2")
37
+
38
+ # With custom attributes
39
+ with app.card(data_post_id="123", class_="custom-card"):
40
+ app.text("Content")
41
+ """
42
+ cid = self._get_next_cid("card")
43
+
44
+ # Convert kwargs to HTML attributes
45
+ attrs = []
46
+ for key, value in kwargs.items():
47
+ # Convert Python naming to HTML attributes
48
+ attr_name = key.replace('_', '-')
49
+ attrs.append(f'{attr_name}="{value}"')
50
+
51
+ attrs_str = ' ' + ' '.join(attrs) if attrs else ''
52
+
53
+ if content is None:
54
+ # Context manager mode
55
+ return CardContext(self, cid, header, footer, attrs_str)
56
+ else:
57
+ # Direct content mode
58
+ def builder():
59
+ token = rendering_ctx.set(cid)
60
+
61
+ try:
62
+ # Handle callable content (Lambda support for content, header, and footer)
63
+ current_content = content
64
+ if callable(content):
65
+ current_content = content() # Execute lambda
66
+
67
+ current_header = header
68
+ if callable(header):
69
+ current_header = header()
70
+
71
+ current_footer = footer
72
+ if callable(footer):
73
+ current_footer = footer()
74
+
75
+ # Set full width for consistency
76
+ card_style = 'style="width: 100%;"'
77
+ html_parts = [f'<sl-card{attrs_str} {card_style}>']
78
+
79
+ if current_header:
80
+ html_parts.append(f'<div slot="header">{current_header}</div>')
81
+
82
+ # Don't wrap content in extra div - user provides styled HTML
83
+ html_parts.append(str(current_content))
84
+
85
+ if current_footer:
86
+ html_parts.append(f'<div slot="footer">{current_footer}</div>')
87
+
88
+ html_parts.append('</sl-card>')
89
+
90
+ # Apply full width to wrapper div for consistency
91
+ return Component("div", id=cid, content=''.join(html_parts), style="width: 100%;")
92
+ finally:
93
+ rendering_ctx.reset(token)
94
+
95
+ self._register_component(cid, builder)
96
+
97
+ def badge(self, text, variant="neutral", pill=False, pulse=False):
98
+ """
99
+ Create a Shoelace badge component
100
+
101
+ Args:
102
+ text: Badge text
103
+ variant: Badge variant (primary, success, neutral, warning, danger)
104
+ pill: Whether to display as pill shape
105
+ pulse: Whether to show pulse animation
106
+
107
+ Example:
108
+ app.badge("LIVE", variant="danger", pulse=True)
109
+ app.badge("New", variant="primary", pill=True)
110
+ """
111
+ cid = self._get_next_cid("badge")
112
+
113
+ def builder():
114
+ token = rendering_ctx.set(cid)
115
+
116
+ attrs = [f'variant="{variant}"']
117
+ if pill:
118
+ attrs.append('pill')
119
+ if pulse:
120
+ attrs.append('pulse')
121
+
122
+ attrs_str = ' '.join(attrs)
123
+ html = f'<sl-badge {attrs_str}>{text}</sl-badge>'
124
+
125
+ rendering_ctx.reset(token)
126
+ return Component("span", id=cid, content=html)
127
+
128
+ self._register_component(cid, builder)
129
+
130
+ def icon(self, name, size=None, label=None):
131
+ """
132
+ Create a Shoelace icon component
133
+
134
+ Args:
135
+ name: Icon name (from Shoelace icon library)
136
+ size: Icon size (e.g., "small", "medium", "large" or CSS value)
137
+ label: Accessibility label
138
+
139
+ Example:
140
+ app.icon("clock")
141
+ app.icon("heart-fill", size="large", label="Favorite")
142
+ """
143
+ cid = self._get_next_cid("icon")
144
+
145
+ def builder():
146
+ token = rendering_ctx.set(cid)
147
+
148
+ attrs = [f'name="{name}"']
149
+ if size:
150
+ attrs.append(f'style="font-size: {size};"' if not size in ['small', 'medium', 'large'] else f'style="font-size: var(--sl-font-size-{size});"')
151
+ if label:
152
+ attrs.append(f'label="{label}"')
153
+
154
+ attrs_str = ' '.join(attrs)
155
+ html = f'<sl-icon {attrs_str}></sl-icon>'
156
+
157
+ rendering_ctx.reset(token)
158
+ return Component("span", id=cid, content=html)
159
+
160
+ self._register_component(cid, builder)
161
+
162
+ # ============= Predefined Card Themes =============
163
+
164
+ def live_card(self, content, timestamp=None, post_id=None):
165
+ """
166
+ Create a LIVE card with danger badge and pulse animation
167
+
168
+ Args:
169
+ content: Card content (will be auto-escaped)
170
+ timestamp: Optional timestamp to display in footer
171
+ post_id: Optional post ID for data attribute
172
+
173
+ Example:
174
+ app.live_card("Breaking news!", timestamp="2026-01-18 10:30")
175
+ app.live_card(post['content'], post['created_at'], post['id'])
176
+ """
177
+ import html
178
+ escaped_content = html.escape(str(content))
179
+
180
+ header = '<div><sl-badge variant="danger" pulse><sl-icon name="circle-fill" style="font-size: 0.5rem;"></sl-icon> LIVE</sl-badge></div>'
181
+ footer = None
182
+ if timestamp:
183
+ footer = f'<div style="text-align: right; font-size: 0.85rem; color: var(--sl-color-neutral-600);"><sl-icon name="clock"></sl-icon> {timestamp}</div>'
184
+
185
+ kwargs = {"style": "margin-bottom: 1rem; width: 100%;"}
186
+ if post_id is not None:
187
+ kwargs["data_post_id"] = str(post_id)
188
+
189
+ self.card(
190
+ content=f'<div style="font-size: 1.1rem; line-height: 1.6; white-space: pre-wrap;">{escaped_content}</div>',
191
+ header=header,
192
+ footer=footer,
193
+ **kwargs
194
+ )
195
+
196
+
197
+ def styled_card(self,
198
+ content: str,
199
+ style: str = 'default',
200
+ header_badge: str = None,
201
+ header_badge_variant: str = 'neutral',
202
+ header_text: str = None,
203
+ footer_text: str = None,
204
+ data_id: str = None,
205
+ return_html: bool = False):
206
+ """Styled card with various preset styles
207
+
208
+ Args:
209
+ content: Card content (auto-escaped)
210
+ style: Card style ('default', 'live', 'admin', 'info', 'warning')
211
+ header_badge: Header badge text
212
+ header_badge_variant: Badge color ('primary', 'success', 'neutral', 'warning', 'danger')
213
+ header_text: Additional header text (e.g., timestamp)
214
+ footer_text: Footer text
215
+ data_id: ID to add as data attribute (for broadcast)
216
+ return_html: If True, return HTML string; if False, register component (for broadcast)
217
+
218
+ Returns:
219
+ HTML string if return_html=True, otherwise None
220
+
221
+ Examples:
222
+ # Display on screen
223
+ app.styled_card(
224
+ "Hello world",
225
+ style='live',
226
+ header_badge='LIVE',
227
+ footer_text='2026-01-18'
228
+ )
229
+
230
+ # Generate HTML for broadcast
231
+ html = app.styled_card(
232
+ "Hello world",
233
+ style='live',
234
+ header_badge='LIVE',
235
+ footer_text='2026-01-18',
236
+ data_id='123',
237
+ return_html=True # Return HTML only
238
+ )
239
+ """
240
+ import html as html_lib
241
+ escaped_content = html_lib.escape(str(content))
242
+
243
+ # Style-specific configuration
244
+ styles_config = {
245
+ 'live': {
246
+ 'content_style': 'font-size: 1.1rem; line-height: 1.6; white-space: pre-wrap;',
247
+ 'badge_variant': 'danger',
248
+ 'badge_pulse': True,
249
+ 'badge_icon': 'circle-fill'
250
+ },
251
+ 'admin': {
252
+ 'content_style': 'white-space: pre-wrap; line-height: 1.5;',
253
+ 'badge_variant': 'neutral',
254
+ 'badge_pill': True,
255
+ 'badge_icon': None
256
+ },
257
+ 'info': {
258
+ 'content_style': 'white-space: pre-wrap; line-height: 1.5;',
259
+ 'badge_variant': 'primary',
260
+ 'badge_icon': 'info-circle'
261
+ },
262
+ 'warning': {
263
+ 'content_style': 'white-space: pre-wrap; line-height: 1.5;',
264
+ 'badge_variant': 'warning',
265
+ 'badge_icon': 'exclamation-triangle'
266
+ },
267
+ 'default': {
268
+ 'content_style': 'white-space: pre-wrap; line-height: 1.5;',
269
+ 'badge_variant': header_badge_variant,
270
+ 'badge_icon': None
271
+ }
272
+ }
273
+
274
+ config = styles_config.get(style, styles_config['default'])
275
+
276
+ # Create header
277
+ header_parts = []
278
+ if header_badge:
279
+ badge_attrs = [f'variant="{config["badge_variant"]}"']
280
+ if config.get('badge_pulse'):
281
+ badge_attrs.append('pulse')
282
+ if config.get('badge_pill'):
283
+ badge_attrs.append('pill')
284
+
285
+ badge_content = header_badge
286
+ if config.get('badge_icon'):
287
+ badge_content = f'<sl-icon name="{config["badge_icon"]}" style="font-size: 0.5rem;"></sl-icon> {header_badge}'
288
+
289
+ header_parts.append(f'<sl-badge {" ".join(badge_attrs)}>{badge_content}</sl-badge>')
290
+
291
+ if header_text:
292
+ header_parts.append(f'<small style="color: var(--sl-color-neutral-500);"><sl-icon name="clock"></sl-icon> {header_text}</small>')
293
+
294
+ header_html = None
295
+ if header_parts:
296
+ header_html = f'<div style="display: flex; gap: 0.5rem; align-items: center;">{"".join(header_parts)}</div>'
297
+
298
+ # Create footer
299
+ footer_html = None
300
+ if footer_text:
301
+ footer_html = f'<div style="text-align: right; font-size: 0.85rem; color: var(--sl-color-neutral-600);"><sl-icon name="clock"></sl-icon> {footer_text}</div>'
302
+
303
+ # If return_html=True, return HTML only (for broadcast)
304
+ if return_html:
305
+ # Data attribute
306
+ data_attr = f' data-post-id="{data_id}"' if data_id else ''
307
+
308
+ # Wrap header in slot
309
+ header_slot = f'<div slot="header">{header_html}</div>' if header_html else ''
310
+
311
+ # Wrap footer in slot
312
+ footer_slot = f'<div slot="footer">{footer_html}</div>' if footer_html else ''
313
+
314
+ # Return full HTML (include wrapper div for layout consistency)
315
+ return f'<div style="width: 100%;"><sl-card{data_attr} style="width: 100%;">{header_slot}<div style="{config["content_style"]}">{escaped_content}</div>{footer_slot}</sl-card></div>'
316
+
317
+ # Normal mode: register component
318
+ kwargs = {}
319
+ if data_id:
320
+ kwargs['data_post_id'] = str(data_id)
321
+
322
+ self.card(
323
+ content=f'<div style="{config["content_style"]}">{escaped_content}</div>',
324
+ header=header_html,
325
+ footer=footer_html,
326
+ **kwargs
327
+ )
328
+
329
+
330
+ def card_with_actions(self,
331
+ content: str,
332
+ style: str = 'default',
333
+ header_badge: str = None,
334
+ header_badge_variant: str = 'neutral',
335
+ header_text: str = None,
336
+ footer_text: str = None,
337
+ data_id: str = None):
338
+ """
339
+ Card widget with action buttons
340
+
341
+ Arranges card and buttons using flexbox, wraps entire thing with data-id.
342
+
343
+ Args:
344
+ content: Card content
345
+ style: Card style
346
+ header_badge: Header badge
347
+ header_badge_variant: Badge color
348
+ header_text: Additional header text
349
+ footer_text: Footer text
350
+ data_id: data-post-id attribute (for broadcast removal)
351
+
352
+ Example:
353
+ # Admin page
354
+ app.card_with_actions(
355
+ content=post['content'],
356
+ style='admin',
357
+ header_badge=f'#{post["id"]}',
358
+ header_text=post['created_at'],
359
+ data_id=post['id']
360
+ )
361
+ """
362
+ import html as html_lib
363
+ escaped_content = html_lib.escape(str(content))
364
+
365
+ # Same style configuration as styled_card
366
+ styles_config = {
367
+ 'live': {
368
+ 'content_style': 'font-size: 1.1rem; line-height: 1.6; white-space: pre-wrap;',
369
+ 'badge_variant': 'danger',
370
+ 'badge_pulse': True,
371
+ 'badge_icon': 'circle-fill'
372
+ },
373
+ 'admin': {
374
+ 'content_style': 'white-space: pre-wrap; line-height: 1.5;',
375
+ 'badge_variant': 'neutral',
376
+ 'badge_pill': True,
377
+ 'badge_icon': None
378
+ },
379
+ 'info': {
380
+ 'content_style': 'white-space: pre-wrap; line-height: 1.5;',
381
+ 'badge_variant': 'primary',
382
+ 'badge_icon': 'info-circle'
383
+ },
384
+ 'warning': {
385
+ 'content_style': 'white-space: pre-wrap; line-height: 1.5;',
386
+ 'badge_variant': 'warning',
387
+ 'badge_icon': 'exclamation-triangle'
388
+ },
389
+ 'default': {
390
+ 'content_style': 'white-space: pre-wrap; line-height: 1.5;',
391
+ 'badge_variant': header_badge_variant,
392
+ 'badge_icon': None
393
+ }
394
+ }
395
+
396
+ config = styles_config.get(style, styles_config['default'])
397
+
398
+ # Create header
399
+ header_parts = []
400
+ if header_badge:
401
+ badge_attrs = [f'variant="{config["badge_variant"]}"']
402
+ if config.get('badge_pulse'):
403
+ badge_attrs.append('pulse')
404
+ if config.get('badge_pill'):
405
+ badge_attrs.append('pill')
406
+
407
+ badge_content = header_badge
408
+ if config.get('badge_icon'):
409
+ badge_content = f'<sl-icon name="{config["badge_icon"]}" style="font-size: 0.5rem;"></sl-icon> {header_badge}'
410
+
411
+ header_parts.append(f'<sl-badge {" ".join(badge_attrs)}>{badge_content}</sl-badge>')
412
+
413
+ if header_text:
414
+ header_parts.append(f'<small style="color: var(--sl-color-neutral-500);"><sl-icon name="clock"></sl-icon> {header_text}</small>')
415
+
416
+ header_html = ''
417
+ if header_parts:
418
+ header_html = f'<div slot="header" style="display: flex; gap: 0.5rem; align-items: center;">{"".join(header_parts)}</div>'
419
+
420
+ # Create footer
421
+ footer_html = ''
422
+ if footer_text:
423
+ footer_html = f'<div slot="footer"><div style="text-align: right; font-size: 0.85rem; color: var(--sl-color-neutral-600);"><sl-icon name="clock"></sl-icon> {footer_text}</div></div>'
424
+
425
+ # Data attribute
426
+ data_attr = f' data-post-id="{data_id}"' if data_id else ''
427
+
428
+ # Arrange card + action area using flexbox layout
429
+ html_content = f'''<div{data_attr} style="display: flex; gap: 1rem; align-items: flex-start; margin-bottom: 1rem;">
430
+ <div style="flex: 1;">
431
+ <sl-card style="width: 100%;">
432
+ {header_html}
433
+ <div style="{config["content_style"]}">{escaped_content}</div>
434
+ {footer_html}
435
+ </sl-card>
436
+ </div>
437
+ </div>'''
438
+
439
+ # Render with markdown
440
+ self.markdown(html_content)
441
+
442
+ def info_card(self, content, title=None):
443
+ """
444
+ Create an info card with primary variant
445
+
446
+ Args:
447
+ content: Card content
448
+ title: Optional title in header
449
+
450
+ Example:
451
+ app.info_card("Important information", title="Notice")
452
+ """
453
+ header = None
454
+ if title:
455
+ header = f'<div><sl-badge variant="primary"><sl-icon name="info-circle"></sl-icon> {title}</sl-badge></div>'
456
+
457
+ self.card(
458
+ content=f'<div style="line-height: 1.6;">{content}</div>',
459
+ header=header,
460
+ style="margin-bottom: 1rem;"
461
+ )
462
+
463
+ def success_card(self, content, title=None):
464
+ """
465
+ Create a success card with success variant
466
+
467
+ Args:
468
+ content: Card content
469
+ title: Optional title in header
470
+
471
+ Example:
472
+ app.success_card("Operation completed!", title="Success")
473
+ """
474
+ header = None
475
+ if title:
476
+ header = f'<div><sl-badge variant="success"><sl-icon name="check-circle"></sl-icon> {title}</sl-badge></div>'
477
+
478
+ self.card(
479
+ content=f'<div style="line-height: 1.6;">{content}</div>',
480
+ header=header,
481
+ style="margin-bottom: 1rem;"
482
+ )
483
+
484
+ def warning_card(self, content, title=None):
485
+ """
486
+ Create a warning card with warning variant
487
+
488
+ Args:
489
+ content: Card content
490
+ title: Optional title in header
491
+
492
+ Example:
493
+ app.warning_card("Please check your settings", title="Warning")
494
+ """
495
+ header = None
496
+ if title:
497
+ header = f'<div><sl-badge variant="warning"><sl-icon name="exclamation-triangle"></sl-icon> {title}</sl-badge></div>'
498
+
499
+ self.card(
500
+ content=f'<div style="line-height: 1.6;">{content}</div>',
501
+ header=header,
502
+ style="margin-bottom: 1rem;"
503
+ )
504
+
505
+ def danger_card(self, content, title=None):
506
+ """
507
+ Create a danger card with danger variant
508
+
509
+ Args:
510
+ content: Card content
511
+ title: Optional title in header
512
+
513
+ Example:
514
+ app.danger_card("Critical error occurred", title="Error")
515
+ """
516
+ header = None
517
+ if title:
518
+ header = f'<div><sl-badge variant="danger"><sl-icon name="x-circle"></sl-icon> {title}</sl-badge></div>'
519
+
520
+ self.card(
521
+ content=f'<div style="line-height: 1.6;">{content}</div>',
522
+ header=header,
523
+ style="margin-bottom: 1rem;"
524
+ )
525
+
526
+
527
+ class CardContext:
528
+ """Context manager for card with complex content"""
529
+
530
+ def __init__(self, app, cid, header, footer, attrs_str):
531
+ self.app = app
532
+ self.cid = cid
533
+ self.header = header
534
+ self.footer = footer
535
+ self.attrs_str = attrs_str
536
+ self.components = []
537
+
538
+ def __enter__(self):
539
+ from ..context import layout_ctx
540
+ self.token = layout_ctx.set(f"card_{self.cid}")
541
+ return self
542
+
543
+ def __exit__(self, exc_type, exc_val, exc_tb):
544
+ from ..context import layout_ctx
545
+ from ..state import get_session_store
546
+
547
+ # Collect all components added inside this context
548
+ store = get_session_store()
549
+ card_components = []
550
+
551
+ # Get components that were added in this context
552
+ for comp_cid in store['order']:
553
+ if comp_cid.startswith(f"card_{self.cid}") or len(card_components) > 0:
554
+ builder = store['builders'].get(comp_cid) or self.app.static_builders.get(comp_cid)
555
+ if builder:
556
+ card_components.append(builder().render())
557
+
558
+ # Build final card HTML
559
+ def builder():
560
+ token = rendering_ctx.set(self.cid)
561
+
562
+ try:
563
+ # Handle callable header and footer (Lambda support)
564
+ current_header = self.header
565
+ if callable(self.header):
566
+ current_header = self.header()
567
+
568
+ current_footer = self.footer
569
+ if callable(self.footer):
570
+ current_footer = self.footer()
571
+
572
+ # Add width: 100% to sl-card (consistency with broadcast)
573
+ card_style = 'style="width: 100%;"'
574
+ html_parts = [f'<sl-card{self.attrs_str} {card_style}>']
575
+
576
+ if current_header:
577
+ html_parts.append(f'<div slot="header">{current_header}</div>')
578
+
579
+ # Add collected components as content (no wrapper div)
580
+ if card_components:
581
+ html_parts.extend(card_components)
582
+
583
+ if current_footer:
584
+ html_parts.append(f'<div slot="footer">{current_footer}</div>')
585
+
586
+ html_parts.append('</sl-card>')
587
+
588
+ # Apply width: 100% to wrapper div (consistency with list_container)
589
+ return Component("div", id=self.cid, content=''.join(html_parts), style="width: 100%;")
590
+ finally:
591
+ rendering_ctx.reset(token)
592
+
593
+ self.app._register_component(self.cid, builder)
594
+ layout_ctx.reset(self.token)
595
+