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.
@@ -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
+