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