violit 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- violit/__init__.py +3 -0
- violit/app.py +1984 -0
- violit/broadcast.py +690 -0
- violit/broadcast_primitives.py +197 -0
- violit/component.py +38 -0
- violit/context.py +10 -0
- violit/engine.py +33 -0
- violit/state.py +76 -0
- violit/theme.py +749 -0
- violit/widgets/__init__.py +30 -0
- violit/widgets/card_widgets.py +595 -0
- violit/widgets/chart_widgets.py +253 -0
- violit/widgets/chat_widgets.py +221 -0
- violit/widgets/data_widgets.py +529 -0
- violit/widgets/form_widgets.py +421 -0
- violit/widgets/input_widgets.py +745 -0
- violit/widgets/layout_widgets.py +419 -0
- violit/widgets/list_widgets.py +107 -0
- violit/widgets/media_widgets.py +173 -0
- violit/widgets/status_widgets.py +255 -0
- violit/widgets/text_widgets.py +413 -0
- violit-0.0.1.dist-info/METADATA +504 -0
- violit-0.0.1.dist-info/RECORD +26 -0
- violit-0.0.1.dist-info/WHEEL +5 -0
- violit-0.0.1.dist-info/licenses/LICENSE +21 -0
- violit-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""UI primitives for broadcast system"""
|
|
2
|
+
|
|
3
|
+
# UI Primitives Definition
|
|
4
|
+
|
|
5
|
+
UI_PRIMITIVES = {
|
|
6
|
+
# List Operations
|
|
7
|
+
'list.append': {
|
|
8
|
+
'description': 'Append item to list (supports custom HTML)',
|
|
9
|
+
'params': ['list_key', 'item_data', 'position'],
|
|
10
|
+
'defaults': {'position': 'prepend'},
|
|
11
|
+
'notes': 'Uses item_data.html if available, otherwise creates default card',
|
|
12
|
+
'example': {
|
|
13
|
+
'type': 'list.append',
|
|
14
|
+
'params': {
|
|
15
|
+
'list_key': 'posts',
|
|
16
|
+
'item_data': 'e.detail', # Use custom HTML if e.detail.html exists
|
|
17
|
+
'position': 'prepend'
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
'list.remove': {
|
|
22
|
+
'description': 'Remove item from list',
|
|
23
|
+
'params': ['list_key', 'item_id'],
|
|
24
|
+
'defaults': {},
|
|
25
|
+
'example': {
|
|
26
|
+
'type': 'list.remove',
|
|
27
|
+
'params': {
|
|
28
|
+
'list_key': 'posts',
|
|
29
|
+
'item_id': 'e.detail.post_id'
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
'list.update': {
|
|
34
|
+
'description': 'Update specific item in list',
|
|
35
|
+
'params': ['list_key', 'item_id', 'item_data'],
|
|
36
|
+
'defaults': {},
|
|
37
|
+
'example': {
|
|
38
|
+
'type': 'list.update',
|
|
39
|
+
'params': {
|
|
40
|
+
'list_key': 'posts',
|
|
41
|
+
'item_id': 'e.detail.id',
|
|
42
|
+
'item_data': 'e.detail'
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
'list.replace_all': {
|
|
47
|
+
'description': 'Replace entire list (snapshot sync)',
|
|
48
|
+
'params': ['list_key', 'items'],
|
|
49
|
+
'defaults': {},
|
|
50
|
+
'example': {
|
|
51
|
+
'type': 'list.replace_all',
|
|
52
|
+
'params': {
|
|
53
|
+
'list_key': 'posts',
|
|
54
|
+
'items': 'e.detail.posts'
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
# State Operations
|
|
60
|
+
'state.set': {
|
|
61
|
+
'description': 'Set state value',
|
|
62
|
+
'params': ['state_key', 'value'],
|
|
63
|
+
'defaults': {},
|
|
64
|
+
'example': {
|
|
65
|
+
'type': 'state.set',
|
|
66
|
+
'params': {
|
|
67
|
+
'state_key': 'user_count',
|
|
68
|
+
'value': 'e.detail.count'
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
'state.increment': {
|
|
73
|
+
'description': 'Increment state value (numeric only)',
|
|
74
|
+
'params': ['state_key', 'amount'],
|
|
75
|
+
'defaults': {'amount': 1},
|
|
76
|
+
'example': {
|
|
77
|
+
'type': 'state.increment',
|
|
78
|
+
'params': {
|
|
79
|
+
'state_key': 'post_count',
|
|
80
|
+
'amount': 1
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
'state.decrement': {
|
|
85
|
+
'description': 'Decrement state value (numeric only)',
|
|
86
|
+
'params': ['state_key', 'amount'],
|
|
87
|
+
'defaults': {'amount': 1},
|
|
88
|
+
'example': {
|
|
89
|
+
'type': 'state.decrement',
|
|
90
|
+
'params': {
|
|
91
|
+
'state_key': 'post_count',
|
|
92
|
+
'amount': 1
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
# DOM Operations
|
|
98
|
+
'dom.insert': {
|
|
99
|
+
'description': 'Insert HTML element',
|
|
100
|
+
'params': ['container_id', 'html', 'position'],
|
|
101
|
+
'defaults': {'position': 'prepend'},
|
|
102
|
+
'example': {
|
|
103
|
+
'type': 'dom.insert',
|
|
104
|
+
'params': {
|
|
105
|
+
'container_id': 'notifications',
|
|
106
|
+
'html': 'e.detail.html',
|
|
107
|
+
'position': 'prepend'
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
'dom.remove': {
|
|
112
|
+
'description': 'Remove DOM element',
|
|
113
|
+
'params': ['selector', 'animate'],
|
|
114
|
+
'defaults': {'animate': True},
|
|
115
|
+
'example': {
|
|
116
|
+
'type': 'dom.remove',
|
|
117
|
+
'params': {
|
|
118
|
+
'selector': '[data-item-id="123"]',
|
|
119
|
+
'animate': True
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
'dom.update': {
|
|
124
|
+
'description': 'Update DOM element content',
|
|
125
|
+
'params': ['selector', 'html'],
|
|
126
|
+
'defaults': {},
|
|
127
|
+
'example': {
|
|
128
|
+
'type': 'dom.update',
|
|
129
|
+
'params': {
|
|
130
|
+
'selector': '#status',
|
|
131
|
+
'html': 'e.detail.status'
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
# Feedback Operations
|
|
137
|
+
'feedback.toast': {
|
|
138
|
+
'description': 'Display toast notification',
|
|
139
|
+
'params': ['message', 'variant', 'duration'],
|
|
140
|
+
'defaults': {'variant': 'neutral', 'duration': 3000},
|
|
141
|
+
'example': {
|
|
142
|
+
'type': 'feedback.toast',
|
|
143
|
+
'params': {
|
|
144
|
+
'message': "'Task completed!'",
|
|
145
|
+
'variant': 'success',
|
|
146
|
+
'duration': 3000
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
'feedback.badge': {
|
|
151
|
+
'description': 'Update badge value',
|
|
152
|
+
'params': ['badge_id', 'value'],
|
|
153
|
+
'defaults': {},
|
|
154
|
+
'example': {
|
|
155
|
+
'type': 'feedback.badge',
|
|
156
|
+
'params': {
|
|
157
|
+
'badge_id': 'notification-badge',
|
|
158
|
+
'value': 'e.detail.count'
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def get_primitive_names():
|
|
166
|
+
"""Return list of available primitive names"""
|
|
167
|
+
return list(UI_PRIMITIVES.keys())
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def get_primitive_info(primitive_name):
|
|
171
|
+
"""Return info for a specific primitive"""
|
|
172
|
+
return UI_PRIMITIVES.get(primitive_name)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def validate_primitive(primitive_dict):
|
|
176
|
+
"""Validate primitive dictionary structure"""
|
|
177
|
+
if 'type' not in primitive_dict:
|
|
178
|
+
return False, "Missing 'type' field"
|
|
179
|
+
|
|
180
|
+
prim_type = primitive_dict['type']
|
|
181
|
+
if prim_type not in UI_PRIMITIVES:
|
|
182
|
+
return False, f"Unknown primitive: {prim_type}"
|
|
183
|
+
|
|
184
|
+
if 'params' not in primitive_dict:
|
|
185
|
+
return False, "Missing 'params' field"
|
|
186
|
+
|
|
187
|
+
prim_info = UI_PRIMITIVES[prim_type]
|
|
188
|
+
required_params = prim_info['params']
|
|
189
|
+
provided_params = primitive_dict['params'].keys()
|
|
190
|
+
defaults = prim_info.get('defaults', {})
|
|
191
|
+
missing_params = [p for p in required_params if p not in provided_params and p not in defaults]
|
|
192
|
+
|
|
193
|
+
if missing_params:
|
|
194
|
+
return False, f"Missing required params: {missing_params}"
|
|
195
|
+
|
|
196
|
+
return True, "OK"
|
|
197
|
+
|
violit/component.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
import html
|
|
3
|
+
|
|
4
|
+
class Component:
|
|
5
|
+
def __init__(self, tag: Optional[str], id: str, escape_content: bool = False, **props):
|
|
6
|
+
self.tag = tag
|
|
7
|
+
self.id = id
|
|
8
|
+
self.escape_content = escape_content # XSS protection
|
|
9
|
+
self.props = props
|
|
10
|
+
|
|
11
|
+
def render(self) -> str:
|
|
12
|
+
if self.tag is None:
|
|
13
|
+
content = str(self.props.get('content', ''))
|
|
14
|
+
# Escape content if enabled
|
|
15
|
+
return html.escape(content) if self.escape_content else content
|
|
16
|
+
|
|
17
|
+
attrs = []
|
|
18
|
+
for k, v in self.props.items():
|
|
19
|
+
if k == 'content': continue
|
|
20
|
+
clean_k = k.replace('_', '-') if not k.startswith('on') else k
|
|
21
|
+
if clean_k.startswith('on'):
|
|
22
|
+
attrs.append(f'{clean_k}="{v}"')
|
|
23
|
+
else:
|
|
24
|
+
if v is True: attrs.append(clean_k)
|
|
25
|
+
elif v is False or v is None: continue
|
|
26
|
+
else:
|
|
27
|
+
# Escape attribute values for XSS protection
|
|
28
|
+
escaped_v = html.escape(str(v), quote=True)
|
|
29
|
+
attrs.append(f'{clean_k}="{escaped_v}"')
|
|
30
|
+
|
|
31
|
+
props_str = " ".join(attrs)
|
|
32
|
+
content = self.props.get('content', '')
|
|
33
|
+
|
|
34
|
+
# Escape content if enabled
|
|
35
|
+
if self.escape_content:
|
|
36
|
+
content = html.escape(str(content))
|
|
37
|
+
|
|
38
|
+
return f"<{self.tag} id=\"{self.id}\" {props_str}>{content}</{self.tag}>"
|
violit/context.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import contextvars
|
|
2
|
+
|
|
3
|
+
# Global Contexts
|
|
4
|
+
session_ctx = contextvars.ContextVar("session_id", default=None)
|
|
5
|
+
rendering_ctx = contextvars.ContextVar("rendering_component", default=None)
|
|
6
|
+
fragment_ctx = contextvars.ContextVar("current_fragment", default=None)
|
|
7
|
+
layout_ctx = contextvars.ContextVar("layout_ctx", default="main") # "main" or "sidebar"
|
|
8
|
+
|
|
9
|
+
# Global Reference for App Instance (used for initial theme sync)
|
|
10
|
+
app_instance_ref = [None]
|
violit/engine.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from typing import List, Dict, Callable
|
|
2
|
+
from starlette.websockets import WebSocket
|
|
3
|
+
from .component import Component
|
|
4
|
+
|
|
5
|
+
class LiteEngine:
|
|
6
|
+
def click_attrs(self, cid: str):
|
|
7
|
+
return {"hx-post": f"/action/{cid}", "hx-swap": "none"}
|
|
8
|
+
|
|
9
|
+
def wrap_oob(self, components: List[Component]):
|
|
10
|
+
html = ""
|
|
11
|
+
for comp in components:
|
|
12
|
+
rendered = comp.render().strip()
|
|
13
|
+
# Inject hx-swap-oob="true" into the root tag of the component
|
|
14
|
+
tag_end = rendered.find(' ')
|
|
15
|
+
if tag_end == -1: tag_end = rendered.find('>')
|
|
16
|
+
html += rendered[:tag_end] + ' hx-swap-oob="true"' + rendered[tag_end:]
|
|
17
|
+
return html
|
|
18
|
+
|
|
19
|
+
class WsEngine:
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self.sockets: Dict[str, WebSocket] = {}
|
|
22
|
+
|
|
23
|
+
def click_attrs(self, cid: str):
|
|
24
|
+
return {"onclick": f"window.sendAction('{cid}')"}
|
|
25
|
+
|
|
26
|
+
async def push_updates(self, sid: str, components: List[Component]):
|
|
27
|
+
if sid in self.sockets:
|
|
28
|
+
payload = [{"id": c.id, "html": c.render()} for c in components]
|
|
29
|
+
await self.sockets[sid].send_json({"type": "update", "payload": payload})
|
|
30
|
+
|
|
31
|
+
async def push_eval(self, sid: str, code: str):
|
|
32
|
+
if sid in self.sockets:
|
|
33
|
+
await self.sockets[sid].send_json({"type": "eval", "code": code})
|
violit/state.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from typing import Any, Dict, Set
|
|
2
|
+
from cachetools import TTLCache
|
|
3
|
+
from .context import session_ctx, rendering_ctx, app_instance_ref
|
|
4
|
+
from .theme import Theme
|
|
5
|
+
|
|
6
|
+
class DependencyTracker:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.subscribers: Dict[str, Set[str]] = {}
|
|
9
|
+
|
|
10
|
+
def register_dependency(self, state_name: str, component_id: str):
|
|
11
|
+
if state_name not in self.subscribers:
|
|
12
|
+
self.subscribers[state_name] = set()
|
|
13
|
+
self.subscribers[state_name].add(component_id)
|
|
14
|
+
|
|
15
|
+
def get_dirty_components(self, state_name: str) -> Set[str]:
|
|
16
|
+
return self.subscribers.get(state_name, set())
|
|
17
|
+
|
|
18
|
+
GLOBAL_STORE = TTLCache(maxsize=1000, ttl=1800)
|
|
19
|
+
|
|
20
|
+
def get_session_store():
|
|
21
|
+
sid = session_ctx.get()
|
|
22
|
+
if sid not in GLOBAL_STORE:
|
|
23
|
+
initial_theme = 'light'
|
|
24
|
+
if app_instance_ref[0]:
|
|
25
|
+
initial_theme = app_instance_ref[0].theme_manager.preset_name
|
|
26
|
+
|
|
27
|
+
GLOBAL_STORE[sid] = {
|
|
28
|
+
'states': {},
|
|
29
|
+
'tracker': DependencyTracker(),
|
|
30
|
+
'builders': {},
|
|
31
|
+
'actions': {},
|
|
32
|
+
'component_count': 0,
|
|
33
|
+
'fragment_components': {},
|
|
34
|
+
'order': [],
|
|
35
|
+
'sidebar_order': [],
|
|
36
|
+
'theme': Theme(initial_theme)
|
|
37
|
+
}
|
|
38
|
+
return GLOBAL_STORE[sid]
|
|
39
|
+
|
|
40
|
+
class State:
|
|
41
|
+
def __init__(self, name: str, default_value: Any):
|
|
42
|
+
object.__setattr__(self, 'name', name)
|
|
43
|
+
object.__setattr__(self, 'default_value', default_value)
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def value(self):
|
|
47
|
+
store = get_session_store()
|
|
48
|
+
current_comp_id = rendering_ctx.get()
|
|
49
|
+
if current_comp_id:
|
|
50
|
+
store['tracker'].register_dependency(self.name, current_comp_id)
|
|
51
|
+
return store['states'].get(self.name, self.default_value)
|
|
52
|
+
|
|
53
|
+
@value.setter
|
|
54
|
+
def value(self, new_value: Any):
|
|
55
|
+
self.set(new_value)
|
|
56
|
+
|
|
57
|
+
def set(self, new_value: Any):
|
|
58
|
+
store = get_session_store()
|
|
59
|
+
store['states'][self.name] = new_value
|
|
60
|
+
if 'dirty_states' not in store: store['dirty_states'] = set()
|
|
61
|
+
store['dirty_states'].add(self.name)
|
|
62
|
+
|
|
63
|
+
def __setattr__(self, attr: str, val: Any):
|
|
64
|
+
if attr == 'value':
|
|
65
|
+
self.set(val)
|
|
66
|
+
else:
|
|
67
|
+
object.__setattr__(self, attr, val)
|
|
68
|
+
|
|
69
|
+
def __str__(self):
|
|
70
|
+
return str(self.value)
|
|
71
|
+
|
|
72
|
+
def __call__(self):
|
|
73
|
+
return self.value
|
|
74
|
+
|
|
75
|
+
def __repr__(self):
|
|
76
|
+
return f"State({self.name}, {self.value})"
|