violit 0.0.4.post1__py3-none-any.whl → 0.0.5__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/app.py +2229 -1988
- violit/context.py +1 -0
- violit/state.py +33 -0
- violit/widgets/__init__.py +30 -30
- violit/widgets/card_widgets.py +595 -595
- violit/widgets/data_widgets.py +529 -529
- violit/widgets/layout_widgets.py +419 -419
- violit/widgets/text_widgets.py +413 -413
- {violit-0.0.4.post1.dist-info → violit-0.0.5.dist-info}/METADATA +1 -1
- {violit-0.0.4.post1.dist-info → violit-0.0.5.dist-info}/RECORD +13 -13
- {violit-0.0.4.post1.dist-info → violit-0.0.5.dist-info}/WHEEL +0 -0
- {violit-0.0.4.post1.dist-info → violit-0.0.5.dist-info}/licenses/LICENSE +0 -0
- {violit-0.0.4.post1.dist-info → violit-0.0.5.dist-info}/top_level.txt +0 -0
violit/widgets/layout_widgets.py
CHANGED
|
@@ -1,419 +1,419 @@
|
|
|
1
|
-
"""Layout Widgets Mixin for Violit"""
|
|
2
|
-
|
|
3
|
-
from typing import Union, Callable, Optional, List
|
|
4
|
-
from ..component import Component
|
|
5
|
-
from ..context import rendering_ctx, fragment_ctx, layout_ctx
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class LayoutWidgetsMixin:
|
|
9
|
-
"""Layout widgets (columns, container, expander, tabs, empty, dialog)"""
|
|
10
|
-
|
|
11
|
-
def columns(self, spec=2, gap="1rem"):
|
|
12
|
-
"""Create column layout - spec can be an int (equal width) or list of weights"""
|
|
13
|
-
if isinstance(spec, int):
|
|
14
|
-
count = spec
|
|
15
|
-
weights = ["1fr"] * count
|
|
16
|
-
else:
|
|
17
|
-
count = len(spec)
|
|
18
|
-
weights = [f"{w}fr" for w in spec]
|
|
19
|
-
|
|
20
|
-
columns_id = self._get_next_cid("columns_container")
|
|
21
|
-
|
|
22
|
-
# Create individual column objects
|
|
23
|
-
column_objects = []
|
|
24
|
-
for i in range(count):
|
|
25
|
-
col = ColumnObject(self, columns_id, i, count, gap)
|
|
26
|
-
column_objects.append(col)
|
|
27
|
-
|
|
28
|
-
# Register the columns container builder
|
|
29
|
-
def builder():
|
|
30
|
-
from ..state import get_session_store
|
|
31
|
-
store = get_session_store()
|
|
32
|
-
|
|
33
|
-
# Collect HTML from all columns
|
|
34
|
-
columns_html = []
|
|
35
|
-
for i in range(count):
|
|
36
|
-
col_id = f"{columns_id}_col_{i}"
|
|
37
|
-
col_content = []
|
|
38
|
-
# Check static
|
|
39
|
-
for cid, b in self.static_fragment_components.get(col_id, []):
|
|
40
|
-
col_content.append(b().render())
|
|
41
|
-
# Check session
|
|
42
|
-
for cid, b in store['fragment_components'].get(col_id, []):
|
|
43
|
-
col_content.append(b().render())
|
|
44
|
-
columns_html.append(f'<div class="column-item">{"".join(col_content)}</div>')
|
|
45
|
-
|
|
46
|
-
grid_tmpl = " ".join(weights)
|
|
47
|
-
container_html = f'<div id="{columns_id}" class="columns" style="display: grid; grid-template-columns: {grid_tmpl}; gap: {gap};">{"".join(columns_html)}</div>'
|
|
48
|
-
return Component("div", id=f"{columns_id}_wrapper", content=container_html)
|
|
49
|
-
|
|
50
|
-
self._register_component(columns_id, builder)
|
|
51
|
-
|
|
52
|
-
return column_objects
|
|
53
|
-
|
|
54
|
-
def container(self, border=True, **kwargs):
|
|
55
|
-
"""
|
|
56
|
-
Create a container for grouping elements
|
|
57
|
-
|
|
58
|
-
Args:
|
|
59
|
-
border: Whether to show border (card style)
|
|
60
|
-
**kwargs: Additional HTML attributes (e.g., data_post_id="123", style="...")
|
|
61
|
-
|
|
62
|
-
Example:
|
|
63
|
-
with app.container(data_post_id="123"):
|
|
64
|
-
app.text("Content")
|
|
65
|
-
app.button("Delete")
|
|
66
|
-
"""
|
|
67
|
-
cid = self._get_next_cid("container")
|
|
68
|
-
|
|
69
|
-
class ContainerContext:
|
|
70
|
-
def __init__(self, app, container_id, border, attrs):
|
|
71
|
-
self.app = app
|
|
72
|
-
self.container_id = container_id
|
|
73
|
-
self.border = border
|
|
74
|
-
self.attrs = attrs
|
|
75
|
-
|
|
76
|
-
def __enter__(self):
|
|
77
|
-
# Register builder BEFORE entering context
|
|
78
|
-
def builder():
|
|
79
|
-
from ..state import get_session_store
|
|
80
|
-
store = get_session_store()
|
|
81
|
-
|
|
82
|
-
# Render child components
|
|
83
|
-
htmls = []
|
|
84
|
-
# Static first
|
|
85
|
-
for cid, b in self.app.static_fragment_components.get(self.container_id, []):
|
|
86
|
-
htmls.append(b().render())
|
|
87
|
-
# Dynamic next
|
|
88
|
-
for cid, b in store['fragment_components'].get(self.container_id, []):
|
|
89
|
-
htmls.append(b().render())
|
|
90
|
-
|
|
91
|
-
border_class = "card" if self.border else ""
|
|
92
|
-
inner_html = "".join(htmls)
|
|
93
|
-
|
|
94
|
-
# Pass kwargs to Component
|
|
95
|
-
return Component("div", id=self.container_id, content=inner_html, class_=border_class, **self.attrs)
|
|
96
|
-
|
|
97
|
-
self.app._register_component(self.container_id, builder)
|
|
98
|
-
|
|
99
|
-
# Now set fragment context
|
|
100
|
-
from ..context import fragment_ctx
|
|
101
|
-
self.token = fragment_ctx.set(self.container_id)
|
|
102
|
-
return self
|
|
103
|
-
|
|
104
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
105
|
-
from ..context import fragment_ctx
|
|
106
|
-
fragment_ctx.reset(self.token)
|
|
107
|
-
|
|
108
|
-
def __getattr__(self, name):
|
|
109
|
-
return getattr(self.app, name)
|
|
110
|
-
|
|
111
|
-
return ContainerContext(self, cid, border, kwargs)
|
|
112
|
-
|
|
113
|
-
def expander(self, label, expanded=False):
|
|
114
|
-
"""Create an expandable/collapsible section"""
|
|
115
|
-
cid = self._get_next_cid("expander")
|
|
116
|
-
|
|
117
|
-
class ExpanderContext:
|
|
118
|
-
def __init__(self, app, expander_id, label, expanded):
|
|
119
|
-
self.app = app
|
|
120
|
-
self.expander_id = expander_id
|
|
121
|
-
self.label = label
|
|
122
|
-
self.expanded = expanded
|
|
123
|
-
|
|
124
|
-
def __enter__(self):
|
|
125
|
-
# Register builder BEFORE entering context
|
|
126
|
-
def builder():
|
|
127
|
-
from ..state import get_session_store
|
|
128
|
-
store = get_session_store()
|
|
129
|
-
|
|
130
|
-
# Render child components
|
|
131
|
-
htmls = []
|
|
132
|
-
# Static
|
|
133
|
-
for cid, b in self.app.static_fragment_components.get(self.expander_id, []):
|
|
134
|
-
htmls.append(b().render())
|
|
135
|
-
# Dynamic
|
|
136
|
-
for cid, b in store['fragment_components'].get(self.expander_id, []):
|
|
137
|
-
htmls.append(b().render())
|
|
138
|
-
|
|
139
|
-
inner_html = "".join(htmls)
|
|
140
|
-
open_attr = "open" if self.expanded else ""
|
|
141
|
-
html = f'''
|
|
142
|
-
<sl-details {open_attr} style="margin-bottom:1rem;">
|
|
143
|
-
<span slot="summary" style="font-weight:500;">{self.label}</span>
|
|
144
|
-
<div style="padding:0.5rem 0;">{inner_html}</div>
|
|
145
|
-
</sl-details>
|
|
146
|
-
'''
|
|
147
|
-
return Component("div", id=self.expander_id, content=html)
|
|
148
|
-
|
|
149
|
-
self.app._register_component(self.expander_id, builder)
|
|
150
|
-
|
|
151
|
-
# Now set fragment context for children
|
|
152
|
-
from ..context import fragment_ctx
|
|
153
|
-
self.token = fragment_ctx.set(self.expander_id)
|
|
154
|
-
return self
|
|
155
|
-
|
|
156
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
157
|
-
from ..context import fragment_ctx
|
|
158
|
-
fragment_ctx.reset(self.token)
|
|
159
|
-
|
|
160
|
-
def __getattr__(self, name):
|
|
161
|
-
return getattr(self.app, name)
|
|
162
|
-
|
|
163
|
-
return ExpanderContext(self, cid, label, expanded)
|
|
164
|
-
|
|
165
|
-
def tabs(self, labels: List[str]):
|
|
166
|
-
"""Create tabbed interface"""
|
|
167
|
-
cid = self._get_next_cid("tabs")
|
|
168
|
-
|
|
169
|
-
class TabsManager:
|
|
170
|
-
def __init__(self, app, tabs_id, labels):
|
|
171
|
-
self.app = app
|
|
172
|
-
self.tabs_id = tabs_id
|
|
173
|
-
self.labels = labels
|
|
174
|
-
self.tab_objects = []
|
|
175
|
-
|
|
176
|
-
# Create tab objects immediately
|
|
177
|
-
for i, label in enumerate(self.labels):
|
|
178
|
-
tab_obj = TabObject(self.app, f"{self.tabs_id}_tab_{i}", label, i == 0)
|
|
179
|
-
self.tab_objects.append(tab_obj)
|
|
180
|
-
|
|
181
|
-
# Register tabs builder immediately
|
|
182
|
-
self._register_builder()
|
|
183
|
-
|
|
184
|
-
def _register_builder(self):
|
|
185
|
-
def builder():
|
|
186
|
-
from ..state import get_session_store
|
|
187
|
-
store = get_session_store()
|
|
188
|
-
|
|
189
|
-
# Build tab headers
|
|
190
|
-
headers = []
|
|
191
|
-
for i, label in enumerate(self.labels):
|
|
192
|
-
active = "active" if i == 0 else ""
|
|
193
|
-
headers.append(f'<sl-tab slot="nav" panel="panel-{i}" {active}>{label}</sl-tab>')
|
|
194
|
-
|
|
195
|
-
# Build tab panels
|
|
196
|
-
panels = []
|
|
197
|
-
for i, tab_obj in enumerate(self.tab_objects):
|
|
198
|
-
active = "active" if i == 0 else ""
|
|
199
|
-
# Render tab content
|
|
200
|
-
tab_htmls = []
|
|
201
|
-
# Check static
|
|
202
|
-
for cid, b in self.app.static_fragment_components.get(tab_obj.tab_id, []):
|
|
203
|
-
tab_htmls.append(b().render())
|
|
204
|
-
# Check session
|
|
205
|
-
for cid, b in store['fragment_components'].get(tab_obj.tab_id, []):
|
|
206
|
-
tab_htmls.append(b().render())
|
|
207
|
-
|
|
208
|
-
panel_content = "".join(tab_htmls)
|
|
209
|
-
panels.append(f'<sl-tab-panel name="panel-{i}" {active}>{panel_content}</sl-tab-panel>')
|
|
210
|
-
|
|
211
|
-
html = f'''
|
|
212
|
-
<sl-tab-group>
|
|
213
|
-
{"".join(headers)}
|
|
214
|
-
{"".join(panels)}
|
|
215
|
-
</sl-tab-group>
|
|
216
|
-
'''
|
|
217
|
-
return Component("div", id=self.tabs_id, content=html)
|
|
218
|
-
|
|
219
|
-
self.app._register_component(self.tabs_id, builder)
|
|
220
|
-
|
|
221
|
-
def __enter__(self):
|
|
222
|
-
return self.tab_objects
|
|
223
|
-
|
|
224
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
225
|
-
pass
|
|
226
|
-
|
|
227
|
-
# Make it iterable and indexable
|
|
228
|
-
def __iter__(self):
|
|
229
|
-
return iter(self.tab_objects)
|
|
230
|
-
|
|
231
|
-
def __getitem__(self, index):
|
|
232
|
-
return self.tab_objects[index]
|
|
233
|
-
|
|
234
|
-
def __len__(self):
|
|
235
|
-
return len(self.tab_objects)
|
|
236
|
-
|
|
237
|
-
return TabsManager(self, cid, labels)
|
|
238
|
-
|
|
239
|
-
def empty(self):
|
|
240
|
-
"""Create an empty container that can be updated later"""
|
|
241
|
-
cid = self._get_next_cid("empty")
|
|
242
|
-
|
|
243
|
-
class EmptyContainer:
|
|
244
|
-
def __init__(self, app, container_id):
|
|
245
|
-
self.app = app
|
|
246
|
-
self.container_id = container_id
|
|
247
|
-
self._content_builder = None
|
|
248
|
-
|
|
249
|
-
# Register initial empty builder
|
|
250
|
-
def builder():
|
|
251
|
-
if self._content_builder:
|
|
252
|
-
return self._content_builder()
|
|
253
|
-
return Component("div", id=container_id, content="")
|
|
254
|
-
|
|
255
|
-
app._register_component(container_id, builder)
|
|
256
|
-
|
|
257
|
-
def write(self, content):
|
|
258
|
-
"""Update the empty container with new content"""
|
|
259
|
-
def new_builder():
|
|
260
|
-
return Component("div", id=self.container_id, content=str(content))
|
|
261
|
-
self._content_builder = new_builder
|
|
262
|
-
|
|
263
|
-
def __getattr__(self, name):
|
|
264
|
-
# Proxy to app for method calls
|
|
265
|
-
return getattr(self.app, name)
|
|
266
|
-
|
|
267
|
-
return EmptyContainer(self, cid)
|
|
268
|
-
|
|
269
|
-
def dialog(self, title):
|
|
270
|
-
"""Create a modal dialog (decorator)"""
|
|
271
|
-
def decorator(func):
|
|
272
|
-
dialog_id = f"dialog_{func.__name__}"
|
|
273
|
-
|
|
274
|
-
# Create a function to open the dialog
|
|
275
|
-
def open_dialog(*args, **kwargs):
|
|
276
|
-
# Set fragment context for dialog content
|
|
277
|
-
token = fragment_ctx.set(dialog_id)
|
|
278
|
-
|
|
279
|
-
# Execute the dialog content function
|
|
280
|
-
func(*args, **kwargs)
|
|
281
|
-
|
|
282
|
-
# Build dialog HTML
|
|
283
|
-
def builder():
|
|
284
|
-
from ..state import get_session_store
|
|
285
|
-
store = get_session_store()
|
|
286
|
-
|
|
287
|
-
# Render dialog content
|
|
288
|
-
htmls = []
|
|
289
|
-
for child_cid, child_builder in store['fragment_components'].get(dialog_id, []):
|
|
290
|
-
htmls.append(child_builder().render())
|
|
291
|
-
|
|
292
|
-
inner_html = "".join(htmls)
|
|
293
|
-
html = f'''
|
|
294
|
-
<sl-dialog id="{dialog_id}_modal" label="{title}" open>
|
|
295
|
-
<div style="padding:1rem;">{inner_html}</div>
|
|
296
|
-
<sl-button slot="footer" variant="primary" onclick="document.getElementById('{dialog_id}_modal').hide()">Close</sl-button>
|
|
297
|
-
</sl-dialog>
|
|
298
|
-
<script>
|
|
299
|
-
document.getElementById('{dialog_id}_modal').show();
|
|
300
|
-
</script>
|
|
301
|
-
'''
|
|
302
|
-
return Component("div", id=dialog_id, content=html)
|
|
303
|
-
|
|
304
|
-
fragment_ctx.reset(token)
|
|
305
|
-
self._register_component(dialog_id, builder)
|
|
306
|
-
|
|
307
|
-
return open_dialog
|
|
308
|
-
return decorator
|
|
309
|
-
|
|
310
|
-
def list_container(self, id: Optional[str] = None, gap: str = None, **style_props):
|
|
311
|
-
"""Create a vertical flex container for lists
|
|
312
|
-
|
|
313
|
-
General list layout container using predefined styles.
|
|
314
|
-
|
|
315
|
-
Args:
|
|
316
|
-
id: Container ID (for broadcast removal)
|
|
317
|
-
gap: Item spacing (CSS value, default: predefined 1rem)
|
|
318
|
-
**style_props: Additional style properties (if needed)
|
|
319
|
-
|
|
320
|
-
Example:
|
|
321
|
-
with app.list_container(id="posts_container"):
|
|
322
|
-
for post in posts:
|
|
323
|
-
app.styled_card(...)
|
|
324
|
-
"""
|
|
325
|
-
cid = id or self._get_next_cid("list_container")
|
|
326
|
-
|
|
327
|
-
class ListContainerContext:
|
|
328
|
-
def __init__(self, app, container_id, gap, style_props):
|
|
329
|
-
self.app = app
|
|
330
|
-
self.container_id = container_id
|
|
331
|
-
self.gap = gap
|
|
332
|
-
self.style_props = style_props
|
|
333
|
-
|
|
334
|
-
def __enter__(self):
|
|
335
|
-
# Register builder
|
|
336
|
-
def builder():
|
|
337
|
-
from ..state import get_session_store
|
|
338
|
-
store = get_session_store()
|
|
339
|
-
|
|
340
|
-
# Render child components
|
|
341
|
-
htmls = []
|
|
342
|
-
# Static
|
|
343
|
-
for cid, b in self.app.static_fragment_components.get(self.container_id, []):
|
|
344
|
-
htmls.append(b().render())
|
|
345
|
-
# Dynamic
|
|
346
|
-
for cid, b in store['fragment_components'].get(self.container_id, []):
|
|
347
|
-
htmls.append(b().render())
|
|
348
|
-
|
|
349
|
-
# Use predefined class + optional customizations
|
|
350
|
-
extra_styles = []
|
|
351
|
-
if self.gap:
|
|
352
|
-
extra_styles.append(f"gap: {self.gap}")
|
|
353
|
-
for k, v in self.style_props.items():
|
|
354
|
-
extra_styles.append(f"{k.replace('_', '-')}: {v}")
|
|
355
|
-
|
|
356
|
-
style_str = "; ".join(extra_styles) if extra_styles else None
|
|
357
|
-
|
|
358
|
-
inner_html = "".join(htmls)
|
|
359
|
-
if style_str:
|
|
360
|
-
return Component("div", id=self.container_id, content=inner_html, class_="violit-list-container", style=style_str)
|
|
361
|
-
else:
|
|
362
|
-
return Component("div", id=self.container_id, content=inner_html, class_="violit-list-container")
|
|
363
|
-
|
|
364
|
-
self.app._register_component(self.container_id, builder)
|
|
365
|
-
|
|
366
|
-
# Set fragment context
|
|
367
|
-
self.token = fragment_ctx.set(self.container_id)
|
|
368
|
-
return self
|
|
369
|
-
|
|
370
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
371
|
-
fragment_ctx.reset(self.token)
|
|
372
|
-
|
|
373
|
-
def __getattr__(self, name):
|
|
374
|
-
return getattr(self.app, name)
|
|
375
|
-
|
|
376
|
-
return ListContainerContext(self, cid, gap, style_props)
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
class ColumnObject:
|
|
380
|
-
"""Represents a single column in a column layout"""
|
|
381
|
-
def __init__(self, app, columns_id, col_index, total_cols, gap):
|
|
382
|
-
self.app = app
|
|
383
|
-
self.columns_id = columns_id
|
|
384
|
-
self.col_index = col_index
|
|
385
|
-
self.col_id = f"{columns_id}_col_{col_index}"
|
|
386
|
-
|
|
387
|
-
def __enter__(self):
|
|
388
|
-
from ..context import fragment_ctx, rendering_ctx
|
|
389
|
-
self.token = fragment_ctx.set(self.col_id)
|
|
390
|
-
# We don't set rendering_ctx here because individual widgets inside will set their own
|
|
391
|
-
return self
|
|
392
|
-
|
|
393
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
394
|
-
from ..context import fragment_ctx
|
|
395
|
-
fragment_ctx.reset(self.token)
|
|
396
|
-
|
|
397
|
-
def __getattr__(self, name):
|
|
398
|
-
"""Proxy to app for method calls within column context"""
|
|
399
|
-
return getattr(self.app, name)
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
class TabObject:
|
|
403
|
-
"""Represents a single tab in a tab group"""
|
|
404
|
-
def __init__(self, app, tab_id, label, active):
|
|
405
|
-
self.app = app
|
|
406
|
-
self.tab_id = tab_id
|
|
407
|
-
self.label = label
|
|
408
|
-
self.active = active
|
|
409
|
-
|
|
410
|
-
def __enter__(self):
|
|
411
|
-
self.token = fragment_ctx.set(self.tab_id)
|
|
412
|
-
return self
|
|
413
|
-
|
|
414
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
415
|
-
fragment_ctx.reset(self.token)
|
|
416
|
-
|
|
417
|
-
def __getattr__(self, name):
|
|
418
|
-
return getattr(self.app, name)
|
|
419
|
-
|
|
1
|
+
"""Layout Widgets Mixin for Violit"""
|
|
2
|
+
|
|
3
|
+
from typing import Union, Callable, Optional, List
|
|
4
|
+
from ..component import Component
|
|
5
|
+
from ..context import rendering_ctx, fragment_ctx, layout_ctx
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LayoutWidgetsMixin:
|
|
9
|
+
"""Layout widgets (columns, container, expander, tabs, empty, dialog)"""
|
|
10
|
+
|
|
11
|
+
def columns(self, spec=2, gap="1rem"):
|
|
12
|
+
"""Create column layout - spec can be an int (equal width) or list of weights"""
|
|
13
|
+
if isinstance(spec, int):
|
|
14
|
+
count = spec
|
|
15
|
+
weights = ["1fr"] * count
|
|
16
|
+
else:
|
|
17
|
+
count = len(spec)
|
|
18
|
+
weights = [f"{w}fr" for w in spec]
|
|
19
|
+
|
|
20
|
+
columns_id = self._get_next_cid("columns_container")
|
|
21
|
+
|
|
22
|
+
# Create individual column objects
|
|
23
|
+
column_objects = []
|
|
24
|
+
for i in range(count):
|
|
25
|
+
col = ColumnObject(self, columns_id, i, count, gap)
|
|
26
|
+
column_objects.append(col)
|
|
27
|
+
|
|
28
|
+
# Register the columns container builder
|
|
29
|
+
def builder():
|
|
30
|
+
from ..state import get_session_store
|
|
31
|
+
store = get_session_store()
|
|
32
|
+
|
|
33
|
+
# Collect HTML from all columns
|
|
34
|
+
columns_html = []
|
|
35
|
+
for i in range(count):
|
|
36
|
+
col_id = f"{columns_id}_col_{i}"
|
|
37
|
+
col_content = []
|
|
38
|
+
# Check static
|
|
39
|
+
for cid, b in self.static_fragment_components.get(col_id, []):
|
|
40
|
+
col_content.append(b().render())
|
|
41
|
+
# Check session
|
|
42
|
+
for cid, b in store['fragment_components'].get(col_id, []):
|
|
43
|
+
col_content.append(b().render())
|
|
44
|
+
columns_html.append(f'<div class="column-item">{"".join(col_content)}</div>')
|
|
45
|
+
|
|
46
|
+
grid_tmpl = " ".join(weights)
|
|
47
|
+
container_html = f'<div id="{columns_id}" class="columns" style="display: grid; grid-template-columns: {grid_tmpl}; gap: {gap};">{"".join(columns_html)}</div>'
|
|
48
|
+
return Component("div", id=f"{columns_id}_wrapper", content=container_html)
|
|
49
|
+
|
|
50
|
+
self._register_component(columns_id, builder)
|
|
51
|
+
|
|
52
|
+
return column_objects
|
|
53
|
+
|
|
54
|
+
def container(self, border=True, **kwargs):
|
|
55
|
+
"""
|
|
56
|
+
Create a container for grouping elements
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
border: Whether to show border (card style)
|
|
60
|
+
**kwargs: Additional HTML attributes (e.g., data_post_id="123", style="...")
|
|
61
|
+
|
|
62
|
+
Example:
|
|
63
|
+
with app.container(data_post_id="123"):
|
|
64
|
+
app.text("Content")
|
|
65
|
+
app.button("Delete")
|
|
66
|
+
"""
|
|
67
|
+
cid = self._get_next_cid("container")
|
|
68
|
+
|
|
69
|
+
class ContainerContext:
|
|
70
|
+
def __init__(self, app, container_id, border, attrs):
|
|
71
|
+
self.app = app
|
|
72
|
+
self.container_id = container_id
|
|
73
|
+
self.border = border
|
|
74
|
+
self.attrs = attrs
|
|
75
|
+
|
|
76
|
+
def __enter__(self):
|
|
77
|
+
# Register builder BEFORE entering context
|
|
78
|
+
def builder():
|
|
79
|
+
from ..state import get_session_store
|
|
80
|
+
store = get_session_store()
|
|
81
|
+
|
|
82
|
+
# Render child components
|
|
83
|
+
htmls = []
|
|
84
|
+
# Static first
|
|
85
|
+
for cid, b in self.app.static_fragment_components.get(self.container_id, []):
|
|
86
|
+
htmls.append(b().render())
|
|
87
|
+
# Dynamic next
|
|
88
|
+
for cid, b in store['fragment_components'].get(self.container_id, []):
|
|
89
|
+
htmls.append(b().render())
|
|
90
|
+
|
|
91
|
+
border_class = "card" if self.border else ""
|
|
92
|
+
inner_html = "".join(htmls)
|
|
93
|
+
|
|
94
|
+
# Pass kwargs to Component
|
|
95
|
+
return Component("div", id=self.container_id, content=inner_html, class_=border_class, **self.attrs)
|
|
96
|
+
|
|
97
|
+
self.app._register_component(self.container_id, builder)
|
|
98
|
+
|
|
99
|
+
# Now set fragment context
|
|
100
|
+
from ..context import fragment_ctx
|
|
101
|
+
self.token = fragment_ctx.set(self.container_id)
|
|
102
|
+
return self
|
|
103
|
+
|
|
104
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
105
|
+
from ..context import fragment_ctx
|
|
106
|
+
fragment_ctx.reset(self.token)
|
|
107
|
+
|
|
108
|
+
def __getattr__(self, name):
|
|
109
|
+
return getattr(self.app, name)
|
|
110
|
+
|
|
111
|
+
return ContainerContext(self, cid, border, kwargs)
|
|
112
|
+
|
|
113
|
+
def expander(self, label, expanded=False):
|
|
114
|
+
"""Create an expandable/collapsible section"""
|
|
115
|
+
cid = self._get_next_cid("expander")
|
|
116
|
+
|
|
117
|
+
class ExpanderContext:
|
|
118
|
+
def __init__(self, app, expander_id, label, expanded):
|
|
119
|
+
self.app = app
|
|
120
|
+
self.expander_id = expander_id
|
|
121
|
+
self.label = label
|
|
122
|
+
self.expanded = expanded
|
|
123
|
+
|
|
124
|
+
def __enter__(self):
|
|
125
|
+
# Register builder BEFORE entering context
|
|
126
|
+
def builder():
|
|
127
|
+
from ..state import get_session_store
|
|
128
|
+
store = get_session_store()
|
|
129
|
+
|
|
130
|
+
# Render child components
|
|
131
|
+
htmls = []
|
|
132
|
+
# Static
|
|
133
|
+
for cid, b in self.app.static_fragment_components.get(self.expander_id, []):
|
|
134
|
+
htmls.append(b().render())
|
|
135
|
+
# Dynamic
|
|
136
|
+
for cid, b in store['fragment_components'].get(self.expander_id, []):
|
|
137
|
+
htmls.append(b().render())
|
|
138
|
+
|
|
139
|
+
inner_html = "".join(htmls)
|
|
140
|
+
open_attr = "open" if self.expanded else ""
|
|
141
|
+
html = f'''
|
|
142
|
+
<sl-details {open_attr} style="margin-bottom:1rem;">
|
|
143
|
+
<span slot="summary" style="font-weight:500;">{self.label}</span>
|
|
144
|
+
<div style="padding:0.5rem 0;">{inner_html}</div>
|
|
145
|
+
</sl-details>
|
|
146
|
+
'''
|
|
147
|
+
return Component("div", id=self.expander_id, content=html)
|
|
148
|
+
|
|
149
|
+
self.app._register_component(self.expander_id, builder)
|
|
150
|
+
|
|
151
|
+
# Now set fragment context for children
|
|
152
|
+
from ..context import fragment_ctx
|
|
153
|
+
self.token = fragment_ctx.set(self.expander_id)
|
|
154
|
+
return self
|
|
155
|
+
|
|
156
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
157
|
+
from ..context import fragment_ctx
|
|
158
|
+
fragment_ctx.reset(self.token)
|
|
159
|
+
|
|
160
|
+
def __getattr__(self, name):
|
|
161
|
+
return getattr(self.app, name)
|
|
162
|
+
|
|
163
|
+
return ExpanderContext(self, cid, label, expanded)
|
|
164
|
+
|
|
165
|
+
def tabs(self, labels: List[str]):
|
|
166
|
+
"""Create tabbed interface"""
|
|
167
|
+
cid = self._get_next_cid("tabs")
|
|
168
|
+
|
|
169
|
+
class TabsManager:
|
|
170
|
+
def __init__(self, app, tabs_id, labels):
|
|
171
|
+
self.app = app
|
|
172
|
+
self.tabs_id = tabs_id
|
|
173
|
+
self.labels = labels
|
|
174
|
+
self.tab_objects = []
|
|
175
|
+
|
|
176
|
+
# Create tab objects immediately
|
|
177
|
+
for i, label in enumerate(self.labels):
|
|
178
|
+
tab_obj = TabObject(self.app, f"{self.tabs_id}_tab_{i}", label, i == 0)
|
|
179
|
+
self.tab_objects.append(tab_obj)
|
|
180
|
+
|
|
181
|
+
# Register tabs builder immediately
|
|
182
|
+
self._register_builder()
|
|
183
|
+
|
|
184
|
+
def _register_builder(self):
|
|
185
|
+
def builder():
|
|
186
|
+
from ..state import get_session_store
|
|
187
|
+
store = get_session_store()
|
|
188
|
+
|
|
189
|
+
# Build tab headers
|
|
190
|
+
headers = []
|
|
191
|
+
for i, label in enumerate(self.labels):
|
|
192
|
+
active = "active" if i == 0 else ""
|
|
193
|
+
headers.append(f'<sl-tab slot="nav" panel="panel-{i}" {active}>{label}</sl-tab>')
|
|
194
|
+
|
|
195
|
+
# Build tab panels
|
|
196
|
+
panels = []
|
|
197
|
+
for i, tab_obj in enumerate(self.tab_objects):
|
|
198
|
+
active = "active" if i == 0 else ""
|
|
199
|
+
# Render tab content
|
|
200
|
+
tab_htmls = []
|
|
201
|
+
# Check static
|
|
202
|
+
for cid, b in self.app.static_fragment_components.get(tab_obj.tab_id, []):
|
|
203
|
+
tab_htmls.append(b().render())
|
|
204
|
+
# Check session
|
|
205
|
+
for cid, b in store['fragment_components'].get(tab_obj.tab_id, []):
|
|
206
|
+
tab_htmls.append(b().render())
|
|
207
|
+
|
|
208
|
+
panel_content = "".join(tab_htmls)
|
|
209
|
+
panels.append(f'<sl-tab-panel name="panel-{i}" {active}>{panel_content}</sl-tab-panel>')
|
|
210
|
+
|
|
211
|
+
html = f'''
|
|
212
|
+
<sl-tab-group>
|
|
213
|
+
{"".join(headers)}
|
|
214
|
+
{"".join(panels)}
|
|
215
|
+
</sl-tab-group>
|
|
216
|
+
'''
|
|
217
|
+
return Component("div", id=self.tabs_id, content=html)
|
|
218
|
+
|
|
219
|
+
self.app._register_component(self.tabs_id, builder)
|
|
220
|
+
|
|
221
|
+
def __enter__(self):
|
|
222
|
+
return self.tab_objects
|
|
223
|
+
|
|
224
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
225
|
+
pass
|
|
226
|
+
|
|
227
|
+
# Make it iterable and indexable
|
|
228
|
+
def __iter__(self):
|
|
229
|
+
return iter(self.tab_objects)
|
|
230
|
+
|
|
231
|
+
def __getitem__(self, index):
|
|
232
|
+
return self.tab_objects[index]
|
|
233
|
+
|
|
234
|
+
def __len__(self):
|
|
235
|
+
return len(self.tab_objects)
|
|
236
|
+
|
|
237
|
+
return TabsManager(self, cid, labels)
|
|
238
|
+
|
|
239
|
+
def empty(self):
|
|
240
|
+
"""Create an empty container that can be updated later"""
|
|
241
|
+
cid = self._get_next_cid("empty")
|
|
242
|
+
|
|
243
|
+
class EmptyContainer:
|
|
244
|
+
def __init__(self, app, container_id):
|
|
245
|
+
self.app = app
|
|
246
|
+
self.container_id = container_id
|
|
247
|
+
self._content_builder = None
|
|
248
|
+
|
|
249
|
+
# Register initial empty builder
|
|
250
|
+
def builder():
|
|
251
|
+
if self._content_builder:
|
|
252
|
+
return self._content_builder()
|
|
253
|
+
return Component("div", id=container_id, content="")
|
|
254
|
+
|
|
255
|
+
app._register_component(container_id, builder)
|
|
256
|
+
|
|
257
|
+
def write(self, content):
|
|
258
|
+
"""Update the empty container with new content"""
|
|
259
|
+
def new_builder():
|
|
260
|
+
return Component("div", id=self.container_id, content=str(content))
|
|
261
|
+
self._content_builder = new_builder
|
|
262
|
+
|
|
263
|
+
def __getattr__(self, name):
|
|
264
|
+
# Proxy to app for method calls
|
|
265
|
+
return getattr(self.app, name)
|
|
266
|
+
|
|
267
|
+
return EmptyContainer(self, cid)
|
|
268
|
+
|
|
269
|
+
def dialog(self, title):
|
|
270
|
+
"""Create a modal dialog (decorator)"""
|
|
271
|
+
def decorator(func):
|
|
272
|
+
dialog_id = f"dialog_{func.__name__}"
|
|
273
|
+
|
|
274
|
+
# Create a function to open the dialog
|
|
275
|
+
def open_dialog(*args, **kwargs):
|
|
276
|
+
# Set fragment context for dialog content
|
|
277
|
+
token = fragment_ctx.set(dialog_id)
|
|
278
|
+
|
|
279
|
+
# Execute the dialog content function
|
|
280
|
+
func(*args, **kwargs)
|
|
281
|
+
|
|
282
|
+
# Build dialog HTML
|
|
283
|
+
def builder():
|
|
284
|
+
from ..state import get_session_store
|
|
285
|
+
store = get_session_store()
|
|
286
|
+
|
|
287
|
+
# Render dialog content
|
|
288
|
+
htmls = []
|
|
289
|
+
for child_cid, child_builder in store['fragment_components'].get(dialog_id, []):
|
|
290
|
+
htmls.append(child_builder().render())
|
|
291
|
+
|
|
292
|
+
inner_html = "".join(htmls)
|
|
293
|
+
html = f'''
|
|
294
|
+
<sl-dialog id="{dialog_id}_modal" label="{title}" open>
|
|
295
|
+
<div style="padding:1rem;">{inner_html}</div>
|
|
296
|
+
<sl-button slot="footer" variant="primary" onclick="document.getElementById('{dialog_id}_modal').hide()">Close</sl-button>
|
|
297
|
+
</sl-dialog>
|
|
298
|
+
<script>
|
|
299
|
+
document.getElementById('{dialog_id}_modal').show();
|
|
300
|
+
</script>
|
|
301
|
+
'''
|
|
302
|
+
return Component("div", id=dialog_id, content=html)
|
|
303
|
+
|
|
304
|
+
fragment_ctx.reset(token)
|
|
305
|
+
self._register_component(dialog_id, builder)
|
|
306
|
+
|
|
307
|
+
return open_dialog
|
|
308
|
+
return decorator
|
|
309
|
+
|
|
310
|
+
def list_container(self, id: Optional[str] = None, gap: str = None, **style_props):
|
|
311
|
+
"""Create a vertical flex container for lists
|
|
312
|
+
|
|
313
|
+
General list layout container using predefined styles.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
id: Container ID (for broadcast removal)
|
|
317
|
+
gap: Item spacing (CSS value, default: predefined 1rem)
|
|
318
|
+
**style_props: Additional style properties (if needed)
|
|
319
|
+
|
|
320
|
+
Example:
|
|
321
|
+
with app.list_container(id="posts_container"):
|
|
322
|
+
for post in posts:
|
|
323
|
+
app.styled_card(...)
|
|
324
|
+
"""
|
|
325
|
+
cid = id or self._get_next_cid("list_container")
|
|
326
|
+
|
|
327
|
+
class ListContainerContext:
|
|
328
|
+
def __init__(self, app, container_id, gap, style_props):
|
|
329
|
+
self.app = app
|
|
330
|
+
self.container_id = container_id
|
|
331
|
+
self.gap = gap
|
|
332
|
+
self.style_props = style_props
|
|
333
|
+
|
|
334
|
+
def __enter__(self):
|
|
335
|
+
# Register builder
|
|
336
|
+
def builder():
|
|
337
|
+
from ..state import get_session_store
|
|
338
|
+
store = get_session_store()
|
|
339
|
+
|
|
340
|
+
# Render child components
|
|
341
|
+
htmls = []
|
|
342
|
+
# Static
|
|
343
|
+
for cid, b in self.app.static_fragment_components.get(self.container_id, []):
|
|
344
|
+
htmls.append(b().render())
|
|
345
|
+
# Dynamic
|
|
346
|
+
for cid, b in store['fragment_components'].get(self.container_id, []):
|
|
347
|
+
htmls.append(b().render())
|
|
348
|
+
|
|
349
|
+
# Use predefined class + optional customizations
|
|
350
|
+
extra_styles = []
|
|
351
|
+
if self.gap:
|
|
352
|
+
extra_styles.append(f"gap: {self.gap}")
|
|
353
|
+
for k, v in self.style_props.items():
|
|
354
|
+
extra_styles.append(f"{k.replace('_', '-')}: {v}")
|
|
355
|
+
|
|
356
|
+
style_str = "; ".join(extra_styles) if extra_styles else None
|
|
357
|
+
|
|
358
|
+
inner_html = "".join(htmls)
|
|
359
|
+
if style_str:
|
|
360
|
+
return Component("div", id=self.container_id, content=inner_html, class_="violit-list-container", style=style_str)
|
|
361
|
+
else:
|
|
362
|
+
return Component("div", id=self.container_id, content=inner_html, class_="violit-list-container")
|
|
363
|
+
|
|
364
|
+
self.app._register_component(self.container_id, builder)
|
|
365
|
+
|
|
366
|
+
# Set fragment context
|
|
367
|
+
self.token = fragment_ctx.set(self.container_id)
|
|
368
|
+
return self
|
|
369
|
+
|
|
370
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
371
|
+
fragment_ctx.reset(self.token)
|
|
372
|
+
|
|
373
|
+
def __getattr__(self, name):
|
|
374
|
+
return getattr(self.app, name)
|
|
375
|
+
|
|
376
|
+
return ListContainerContext(self, cid, gap, style_props)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
class ColumnObject:
|
|
380
|
+
"""Represents a single column in a column layout"""
|
|
381
|
+
def __init__(self, app, columns_id, col_index, total_cols, gap):
|
|
382
|
+
self.app = app
|
|
383
|
+
self.columns_id = columns_id
|
|
384
|
+
self.col_index = col_index
|
|
385
|
+
self.col_id = f"{columns_id}_col_{col_index}"
|
|
386
|
+
|
|
387
|
+
def __enter__(self):
|
|
388
|
+
from ..context import fragment_ctx, rendering_ctx
|
|
389
|
+
self.token = fragment_ctx.set(self.col_id)
|
|
390
|
+
# We don't set rendering_ctx here because individual widgets inside will set their own
|
|
391
|
+
return self
|
|
392
|
+
|
|
393
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
394
|
+
from ..context import fragment_ctx
|
|
395
|
+
fragment_ctx.reset(self.token)
|
|
396
|
+
|
|
397
|
+
def __getattr__(self, name):
|
|
398
|
+
"""Proxy to app for method calls within column context"""
|
|
399
|
+
return getattr(self.app, name)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
class TabObject:
|
|
403
|
+
"""Represents a single tab in a tab group"""
|
|
404
|
+
def __init__(self, app, tab_id, label, active):
|
|
405
|
+
self.app = app
|
|
406
|
+
self.tab_id = tab_id
|
|
407
|
+
self.label = label
|
|
408
|
+
self.active = active
|
|
409
|
+
|
|
410
|
+
def __enter__(self):
|
|
411
|
+
self.token = fragment_ctx.set(self.tab_id)
|
|
412
|
+
return self
|
|
413
|
+
|
|
414
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
415
|
+
fragment_ctx.reset(self.token)
|
|
416
|
+
|
|
417
|
+
def __getattr__(self, name):
|
|
418
|
+
return getattr(self.app, name)
|
|
419
|
+
|