quantum-framework 0.9.0__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.
Files changed (262) hide show
  1. quantum/__init__.py +1 -0
  2. quantum/cli/__init__.py +2 -0
  3. quantum/cli/commands/__init__.py +15 -0
  4. quantum/cli/commands/build.py +417 -0
  5. quantum/cli/commands/dev.py +185 -0
  6. quantum/cli/commands/docs.py +329 -0
  7. quantum/cli/commands/lint.py +523 -0
  8. quantum/cli/commands/migrate.py +527 -0
  9. quantum/cli/commands/new.py +622 -0
  10. quantum/cli/commands/serve.py +190 -0
  11. quantum/cli/commands/test.py +193 -0
  12. quantum/cli/deploy.py +810 -0
  13. quantum/cli/hot_reload.py +951 -0
  14. quantum/cli/jobs.py +356 -0
  15. quantum/cli/mq.py +582 -0
  16. quantum/cli/pkg.py +390 -0
  17. quantum/cli/runner.py +547 -0
  18. quantum/cli/server_process.py +159 -0
  19. quantum/cli/utils.py +334 -0
  20. quantum/compiler/__init__.py +30 -0
  21. quantum/compiler/base_generator.py +367 -0
  22. quantum/compiler/cli.py +295 -0
  23. quantum/compiler/expression_transformer.py +444 -0
  24. quantum/compiler/javascript/__init__.py +10 -0
  25. quantum/compiler/javascript/generator.py +659 -0
  26. quantum/compiler/optimizer.py +270 -0
  27. quantum/compiler/python/__init__.py +10 -0
  28. quantum/compiler/python/generator.py +883 -0
  29. quantum/compiler/python/runtime.py +863 -0
  30. quantum/compiler/transpiler.py +330 -0
  31. quantum/core/__init__.py +3 -0
  32. quantum/core/ast_nodes.py +2611 -0
  33. quantum/core/expression_diagnostics.py +87 -0
  34. quantum/core/expression_stdlib.py +148 -0
  35. quantum/core/expressions.py +591 -0
  36. quantum/core/features/agents/src/__init__.py +30 -0
  37. quantum/core/features/agents/src/ast_node.py +540 -0
  38. quantum/core/features/conditionals/src/__init__.py +8 -0
  39. quantum/core/features/conditionals/src/ast_node.py +68 -0
  40. quantum/core/features/data_fetching/src/__init__.py +21 -0
  41. quantum/core/features/data_fetching/src/ast_node.py +312 -0
  42. quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
  43. quantum/core/features/data_fetching/src/html_adapter.py +474 -0
  44. quantum/core/features/data_fetching/src/parser.py +225 -0
  45. quantum/core/features/data_import/src/__init__.py +0 -0
  46. quantum/core/features/data_import/src/ast_node.py +291 -0
  47. quantum/core/features/data_import/src/runtime.py +538 -0
  48. quantum/core/features/dump/src/__init__.py +12 -0
  49. quantum/core/features/dump/src/ast_node.py +106 -0
  50. quantum/core/features/dump/src/parser.py +61 -0
  51. quantum/core/features/dump/src/runtime.py +246 -0
  52. quantum/core/features/functions/src/__init__.py +8 -0
  53. quantum/core/features/functions/src/ast_node.py +149 -0
  54. quantum/core/features/game_engine_2d/src/__init__.py +19 -0
  55. quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
  56. quantum/core/features/game_engine_2d/src/parser.py +983 -0
  57. quantum/core/features/invocation/src/__init__.py +0 -0
  58. quantum/core/features/invocation/src/ast_node.py +146 -0
  59. quantum/core/features/invocation/src/runtime.py +327 -0
  60. quantum/core/features/knowledge_base/src/__init__.py +6 -0
  61. quantum/core/features/knowledge_base/src/ast_node.py +113 -0
  62. quantum/core/features/knowledge_base/src/parser.py +82 -0
  63. quantum/core/features/logging/src/__init__.py +12 -0
  64. quantum/core/features/logging/src/ast_node.py +111 -0
  65. quantum/core/features/logging/src/parser.py +50 -0
  66. quantum/core/features/logging/src/runtime.py +190 -0
  67. quantum/core/features/loops/src/__init__.py +8 -0
  68. quantum/core/features/loops/src/ast_node.py +60 -0
  69. quantum/core/features/query/src/__init__.py +0 -0
  70. quantum/core/features/query/src/database_service.py +322 -0
  71. quantum/core/features/query/src/query_validators.py +20 -0
  72. quantum/core/features/state_management/src/__init__.py +11 -0
  73. quantum/core/features/state_management/src/ast_node.py +228 -0
  74. quantum/core/features/terminal_engine/src/__init__.py +21 -0
  75. quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
  76. quantum/core/features/terminal_engine/src/parser.py +361 -0
  77. quantum/core/features/testing_engine/src/__init__.py +41 -0
  78. quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
  79. quantum/core/features/testing_engine/src/parser.py +604 -0
  80. quantum/core/features/theming/src/__init__.py +48 -0
  81. quantum/core/features/theming/src/ast_node.py +137 -0
  82. quantum/core/features/theming/src/presets.py +405 -0
  83. quantum/core/features/ui_engine/src/__init__.py +20 -0
  84. quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
  85. quantum/core/features/ui_engine/src/parser.py +1106 -0
  86. quantum/core/features/websocket/src/__init__.py +24 -0
  87. quantum/core/features/websocket/src/ast_node.py +247 -0
  88. quantum/core/html_compat.py +299 -0
  89. quantum/core/parser.py +1235 -0
  90. quantum/core/parser_registry.py +213 -0
  91. quantum/core/parsers/__init__.py +76 -0
  92. quantum/core/parsers/ai/__init__.py +12 -0
  93. quantum/core/parsers/ai/agent_parser.py +106 -0
  94. quantum/core/parsers/ai/knowledge_parser.py +86 -0
  95. quantum/core/parsers/ai/llm_parser.py +78 -0
  96. quantum/core/parsers/ai/team_parser.py +88 -0
  97. quantum/core/parsers/base.py +322 -0
  98. quantum/core/parsers/composition/__init__.py +10 -0
  99. quantum/core/parsers/composition/import_parser.py +50 -0
  100. quantum/core/parsers/composition/slot_parser.py +48 -0
  101. quantum/core/parsers/control_flow/__init__.py +11 -0
  102. quantum/core/parsers/control_flow/if_parser.py +68 -0
  103. quantum/core/parsers/control_flow/loop_parser.py +105 -0
  104. quantum/core/parsers/control_flow/set_parser.py +89 -0
  105. quantum/core/parsers/data/__init__.py +12 -0
  106. quantum/core/parsers/data/data_parser.py +217 -0
  107. quantum/core/parsers/data/invoke_parser.py +116 -0
  108. quantum/core/parsers/data/query_parser.py +188 -0
  109. quantum/core/parsers/data/transaction_parser.py +77 -0
  110. quantum/core/parsers/events/__init__.py +9 -0
  111. quantum/core/parsers/events/dispatch_event_parser.py +44 -0
  112. quantum/core/parsers/forms/__init__.py +11 -0
  113. quantum/core/parsers/forms/action_parser.py +54 -0
  114. quantum/core/parsers/forms/flash_parser.py +34 -0
  115. quantum/core/parsers/forms/redirect_parser.py +33 -0
  116. quantum/core/parsers/functions/__init__.py +11 -0
  117. quantum/core/parsers/functions/function_parser.py +137 -0
  118. quantum/core/parsers/functions/param_parser.py +66 -0
  119. quantum/core/parsers/functions/return_parser.py +31 -0
  120. quantum/core/parsers/html/__init__.py +10 -0
  121. quantum/core/parsers/html/component_call_parser.py +130 -0
  122. quantum/core/parsers/html/html_parser.py +115 -0
  123. quantum/core/parsers/jobs/__init__.py +11 -0
  124. quantum/core/parsers/jobs/job_parser.py +71 -0
  125. quantum/core/parsers/jobs/schedule_parser.py +62 -0
  126. quantum/core/parsers/jobs/thread_parser.py +57 -0
  127. quantum/core/parsers/messaging/__init__.py +17 -0
  128. quantum/core/parsers/messaging/message_ack_parser.py +30 -0
  129. quantum/core/parsers/messaging/message_nack_parser.py +30 -0
  130. quantum/core/parsers/messaging/message_parser.py +114 -0
  131. quantum/core/parsers/messaging/queue_parser.py +61 -0
  132. quantum/core/parsers/messaging/websocket_parser.py +121 -0
  133. quantum/core/parsers/persistence/__init__.py +9 -0
  134. quantum/core/parsers/persistence/persist_parser.py +64 -0
  135. quantum/core/parsers/routing/__init__.py +9 -0
  136. quantum/core/parsers/routing/route_parser.py +41 -0
  137. quantum/core/parsers/scripting/__init__.py +12 -0
  138. quantum/core/parsers/scripting/pyclass_parser.py +62 -0
  139. quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
  140. quantum/core/parsers/scripting/pyimport_parser.py +52 -0
  141. quantum/core/parsers/scripting/python_parser.py +49 -0
  142. quantum/core/parsers/services/__init__.py +12 -0
  143. quantum/core/parsers/services/dump_parser.py +52 -0
  144. quantum/core/parsers/services/file_parser.py +48 -0
  145. quantum/core/parsers/services/log_parser.py +46 -0
  146. quantum/core/parsers/services/mail_parser.py +65 -0
  147. quantum/core/tiers.py +82 -0
  148. quantum/packages/__init__.py +28 -0
  149. quantum/packages/manager.py +413 -0
  150. quantum/packages/manifest.py +351 -0
  151. quantum/packages/registry.py +399 -0
  152. quantum/packages/resolver.py +336 -0
  153. quantum/plugins/__init__.py +33 -0
  154. quantum/plugins/hooks.py +329 -0
  155. quantum/plugins/loader.py +479 -0
  156. quantum/plugins/manifest.py +336 -0
  157. quantum/plugins/registry.py +371 -0
  158. quantum/runtime/__init__.py +28 -0
  159. quantum/runtime/action_handler.py +443 -0
  160. quantum/runtime/adapters/__init__.py +88 -0
  161. quantum/runtime/adapters/memory_adapter.py +690 -0
  162. quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
  163. quantum/runtime/adapters/redis_adapter.py +582 -0
  164. quantum/runtime/adapters/sqlite_adapter.py +414 -0
  165. quantum/runtime/agent_service.py +1133 -0
  166. quantum/runtime/api_server.py +86 -0
  167. quantum/runtime/ast_cache.py +506 -0
  168. quantum/runtime/auth_service.py +267 -0
  169. quantum/runtime/component.py +990 -0
  170. quantum/runtime/component_composer.py +319 -0
  171. quantum/runtime/component_resolver.py +174 -0
  172. quantum/runtime/database_service.py +598 -0
  173. quantum/runtime/email_service.py +162 -0
  174. quantum/runtime/error_handler.py +295 -0
  175. quantum/runtime/execution_context.py +286 -0
  176. quantum/runtime/executor_registry.py +171 -0
  177. quantum/runtime/executors/__init__.py +71 -0
  178. quantum/runtime/executors/ai/__init__.py +12 -0
  179. quantum/runtime/executors/ai/agent_executor.py +217 -0
  180. quantum/runtime/executors/ai/knowledge_executor.py +114 -0
  181. quantum/runtime/executors/ai/llm_executor.py +153 -0
  182. quantum/runtime/executors/ai/team_executor.py +171 -0
  183. quantum/runtime/executors/base.py +262 -0
  184. quantum/runtime/executors/control_flow/__init__.py +11 -0
  185. quantum/runtime/executors/control_flow/if_executor.py +93 -0
  186. quantum/runtime/executors/control_flow/loop_executor.py +307 -0
  187. quantum/runtime/executors/control_flow/set_executor.py +412 -0
  188. quantum/runtime/executors/data/__init__.py +12 -0
  189. quantum/runtime/executors/data/data_executor.py +145 -0
  190. quantum/runtime/executors/data/invoke_executor.py +176 -0
  191. quantum/runtime/executors/data/query_executor.py +256 -0
  192. quantum/runtime/executors/data/transaction_executor.py +91 -0
  193. quantum/runtime/executors/jobs/__init__.py +11 -0
  194. quantum/runtime/executors/jobs/job_executor.py +190 -0
  195. quantum/runtime/executors/jobs/schedule_executor.py +132 -0
  196. quantum/runtime/executors/jobs/thread_executor.py +127 -0
  197. quantum/runtime/executors/messaging/__init__.py +17 -0
  198. quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
  199. quantum/runtime/executors/messaging/message_executor.py +174 -0
  200. quantum/runtime/executors/messaging/queue_executor.py +103 -0
  201. quantum/runtime/executors/messaging/websocket_executor.py +197 -0
  202. quantum/runtime/executors/scripting/__init__.py +11 -0
  203. quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
  204. quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
  205. quantum/runtime/executors/scripting/python_executor.py +249 -0
  206. quantum/runtime/executors/services/__init__.py +12 -0
  207. quantum/runtime/executors/services/dump_executor.py +72 -0
  208. quantum/runtime/executors/services/file_executor.py +89 -0
  209. quantum/runtime/executors/services/log_executor.py +77 -0
  210. quantum/runtime/executors/services/mail_executor.py +81 -0
  211. quantum/runtime/expression_cache.py +498 -0
  212. quantum/runtime/file_upload_service.py +326 -0
  213. quantum/runtime/function_registry.py +118 -0
  214. quantum/runtime/game_builder.py +166 -0
  215. quantum/runtime/game_code_generator.py +2371 -0
  216. quantum/runtime/game_templates.py +2006 -0
  217. quantum/runtime/godot_code_generator.py +4681 -0
  218. quantum/runtime/godot_templates.py +1449 -0
  219. quantum/runtime/job_executor.py +1599 -0
  220. quantum/runtime/knowledge_service.py +500 -0
  221. quantum/runtime/llm_cache.py +100 -0
  222. quantum/runtime/llm_providers.py +704 -0
  223. quantum/runtime/llm_service.py +287 -0
  224. quantum/runtime/logging_setup.py +140 -0
  225. quantum/runtime/message_broker.py +364 -0
  226. quantum/runtime/message_queue_service.py +571 -0
  227. quantum/runtime/param_validation.py +184 -0
  228. quantum/runtime/pypy_compat.py +315 -0
  229. quantum/runtime/python_bridge.py +698 -0
  230. quantum/runtime/query_validators.py +304 -0
  231. quantum/runtime/renderer.py +733 -0
  232. quantum/runtime/service_container.py +444 -0
  233. quantum/runtime/terminal_builder.py +76 -0
  234. quantum/runtime/terminal_code_generator.py +607 -0
  235. quantum/runtime/terminal_templates.py +243 -0
  236. quantum/runtime/testing_builder.py +77 -0
  237. quantum/runtime/testing_code_generator.py +833 -0
  238. quantum/runtime/testing_templates.py +85 -0
  239. quantum/runtime/ui_builder.py +188 -0
  240. quantum/runtime/ui_desktop_adapter.py +1730 -0
  241. quantum/runtime/ui_desktop_templates.py +307 -0
  242. quantum/runtime/ui_html_adapter.py +2691 -0
  243. quantum/runtime/ui_html_templates.py +2297 -0
  244. quantum/runtime/ui_mobile_adapter.py +1832 -0
  245. quantum/runtime/ui_mobile_templates.py +1003 -0
  246. quantum/runtime/ui_textual_adapter.py +1866 -0
  247. quantum/runtime/ui_textual_templates.py +45 -0
  248. quantum/runtime/ui_tokens.py +465 -0
  249. quantum/runtime/ui_validator.py +365 -0
  250. quantum/runtime/validators.py +256 -0
  251. quantum/runtime/web_server.py +1766 -0
  252. quantum/runtime/websocket_adapter.py +501 -0
  253. quantum/runtime/websocket_service.py +585 -0
  254. quantum/runtime/websocket_transport.py +289 -0
  255. quantum/runtime/wsgi.py +101 -0
  256. quantum/utils/__init__.py +1 -0
  257. quantum_framework-0.9.0.dist-info/METADATA +244 -0
  258. quantum_framework-0.9.0.dist-info/RECORD +262 -0
  259. quantum_framework-0.9.0.dist-info/WHEEL +5 -0
  260. quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
  261. quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
  262. quantum_framework-0.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1854 @@
1
+ """
2
+ AST Nodes for UI Engine (ui: namespace)
3
+
4
+ All UI-specific AST nodes for the Quantum multi-target UI engine.
5
+ These represent containers, widgets, and layout primitives that
6
+ compile to HTML/CSS, Python Textual (TUI), or Desktop (pywebview).
7
+ """
8
+
9
+ from typing import Dict, Any, List, Optional
10
+ import sys
11
+ from pathlib import Path
12
+ from quantum.core.ast_nodes import QuantumNode
13
+
14
+
15
+ # ============================================
16
+ # LAYOUT MIXIN
17
+ # ============================================
18
+
19
+ class UILayoutMixin:
20
+ """Shared layout attributes for all UI containers and some widgets."""
21
+
22
+ def _init_layout_attrs(self):
23
+ self.gap: Optional[str] = None
24
+ self.padding: Optional[str] = None
25
+ self.margin: Optional[str] = None
26
+ self.align: Optional[str] = None # start, center, end, stretch
27
+ self.justify: Optional[str] = None # start, center, end, between, around
28
+ self.width: Optional[str] = None # auto, fill, number, percentage
29
+ self.height: Optional[str] = None
30
+ self.background: Optional[str] = None
31
+ self.color: Optional[str] = None
32
+ self.border: Optional[str] = None
33
+ self.ui_id: Optional[str] = None
34
+ self.ui_class: Optional[str] = None
35
+ self.visible: Optional[str] = None
36
+
37
+ def _layout_to_dict(self) -> Dict[str, Any]:
38
+ """Return layout attributes as dict (for to_dict)."""
39
+ d = {}
40
+ for attr in ('gap', 'padding', 'margin', 'align', 'justify',
41
+ 'width', 'height', 'background', 'color', 'border',
42
+ 'ui_id', 'ui_class', 'visible'):
43
+ val = getattr(self, attr, None)
44
+ if val is not None:
45
+ d[attr] = val
46
+ return d
47
+
48
+ def _validate_layout(self) -> List[str]:
49
+ """Validate layout attributes."""
50
+ errors = []
51
+ if self.align and self.align not in ('start', 'center', 'end', 'stretch'):
52
+ errors.append(f"Invalid align value: {self.align}")
53
+ if self.justify and self.justify not in ('start', 'center', 'end', 'between', 'around'):
54
+ errors.append(f"Invalid justify value: {self.justify}")
55
+ return errors
56
+
57
+
58
+ # ============================================
59
+ # CONTAINERS
60
+ # ============================================
61
+
62
+ class UIWindowNode(QuantumNode, UILayoutMixin):
63
+ """Represents <ui:window> - Top-level window container."""
64
+
65
+ def __init__(self, title: Optional[str] = None):
66
+ self.title = title
67
+ self.children: List[QuantumNode] = []
68
+ self._init_layout_attrs()
69
+
70
+ def add_child(self, child: QuantumNode):
71
+ self.children.append(child)
72
+
73
+ def to_dict(self) -> Dict[str, Any]:
74
+ d = {"type": "ui_window", "title": self.title,
75
+ "children_count": len(self.children)}
76
+ d.update(self._layout_to_dict())
77
+ return d
78
+
79
+ def validate(self) -> List[str]:
80
+ errors = self._validate_layout()
81
+ for child in self.children:
82
+ if hasattr(child, 'validate'):
83
+ errors.extend(child.validate())
84
+ return errors
85
+
86
+
87
+ class UIHBoxNode(QuantumNode, UILayoutMixin):
88
+ """Represents <ui:hbox> - Horizontal flex container."""
89
+
90
+ def __init__(self):
91
+ self.children: List[QuantumNode] = []
92
+ self._init_layout_attrs()
93
+
94
+ def add_child(self, child: QuantumNode):
95
+ self.children.append(child)
96
+
97
+ def to_dict(self) -> Dict[str, Any]:
98
+ d = {"type": "ui_hbox", "children_count": len(self.children)}
99
+ d.update(self._layout_to_dict())
100
+ return d
101
+
102
+ def validate(self) -> List[str]:
103
+ errors = self._validate_layout()
104
+ for child in self.children:
105
+ if hasattr(child, 'validate'):
106
+ errors.extend(child.validate())
107
+ return errors
108
+
109
+
110
+ class UIVBoxNode(QuantumNode, UILayoutMixin):
111
+ """Represents <ui:vbox> - Vertical flex container."""
112
+
113
+ def __init__(self):
114
+ self.children: List[QuantumNode] = []
115
+ self._init_layout_attrs()
116
+
117
+ def add_child(self, child: QuantumNode):
118
+ self.children.append(child)
119
+
120
+ def to_dict(self) -> Dict[str, Any]:
121
+ d = {"type": "ui_vbox", "children_count": len(self.children)}
122
+ d.update(self._layout_to_dict())
123
+ return d
124
+
125
+ def validate(self) -> List[str]:
126
+ errors = self._validate_layout()
127
+ for child in self.children:
128
+ if hasattr(child, 'validate'):
129
+ errors.extend(child.validate())
130
+ return errors
131
+
132
+
133
+ class UIPanelNode(QuantumNode, UILayoutMixin):
134
+ """Represents <ui:panel> - Bordered container with optional title."""
135
+
136
+ def __init__(self):
137
+ self.title: Optional[str] = None
138
+ self.collapsible: bool = False
139
+ self.children: List[QuantumNode] = []
140
+ self._init_layout_attrs()
141
+
142
+ def add_child(self, child: QuantumNode):
143
+ self.children.append(child)
144
+
145
+ def to_dict(self) -> Dict[str, Any]:
146
+ d = {"type": "ui_panel", "title": self.title,
147
+ "collapsible": self.collapsible, "children_count": len(self.children)}
148
+ d.update(self._layout_to_dict())
149
+ return d
150
+
151
+ def validate(self) -> List[str]:
152
+ errors = self._validate_layout()
153
+ for child in self.children:
154
+ if hasattr(child, 'validate'):
155
+ errors.extend(child.validate())
156
+ return errors
157
+
158
+
159
+ class UITabPanelNode(QuantumNode, UILayoutMixin):
160
+ """Represents <ui:tabpanel> - Tabbed content container."""
161
+
162
+ def __init__(self):
163
+ self.children: List[QuantumNode] = [] # Should contain UITabNode
164
+ self._init_layout_attrs()
165
+
166
+ def add_child(self, child: QuantumNode):
167
+ self.children.append(child)
168
+
169
+ def to_dict(self) -> Dict[str, Any]:
170
+ d = {"type": "ui_tabpanel", "children_count": len(self.children)}
171
+ d.update(self._layout_to_dict())
172
+ return d
173
+
174
+ def validate(self) -> List[str]:
175
+ errors = self._validate_layout()
176
+ for child in self.children:
177
+ if hasattr(child, 'validate'):
178
+ errors.extend(child.validate())
179
+ return errors
180
+
181
+
182
+ class UITabNode(QuantumNode, UILayoutMixin):
183
+ """Represents <ui:tab> - Individual tab inside a tabpanel."""
184
+
185
+ def __init__(self):
186
+ self.title: str = ""
187
+ self.children: List[QuantumNode] = []
188
+ self._init_layout_attrs()
189
+
190
+ def add_child(self, child: QuantumNode):
191
+ self.children.append(child)
192
+
193
+ def to_dict(self) -> Dict[str, Any]:
194
+ d = {"type": "ui_tab", "title": self.title,
195
+ "children_count": len(self.children)}
196
+ d.update(self._layout_to_dict())
197
+ return d
198
+
199
+ def validate(self) -> List[str]:
200
+ errors = self._validate_layout()
201
+ if not self.title:
202
+ errors.append("Tab title is required")
203
+ for child in self.children:
204
+ if hasattr(child, 'validate'):
205
+ errors.extend(child.validate())
206
+ return errors
207
+
208
+
209
+ class UIGridNode(QuantumNode, UILayoutMixin):
210
+ """Represents <ui:grid> - CSS grid container."""
211
+
212
+ def __init__(self):
213
+ self.columns: Optional[str] = None # e.g. "3", "1fr 2fr 1fr"
214
+ self.children: List[QuantumNode] = []
215
+ self._init_layout_attrs()
216
+
217
+ def add_child(self, child: QuantumNode):
218
+ self.children.append(child)
219
+
220
+ def to_dict(self) -> Dict[str, Any]:
221
+ d = {"type": "ui_grid", "columns": self.columns,
222
+ "children_count": len(self.children)}
223
+ d.update(self._layout_to_dict())
224
+ return d
225
+
226
+ def validate(self) -> List[str]:
227
+ errors = self._validate_layout()
228
+ for child in self.children:
229
+ if hasattr(child, 'validate'):
230
+ errors.extend(child.validate())
231
+ return errors
232
+
233
+
234
+ class UIAccordionNode(QuantumNode, UILayoutMixin):
235
+ """Represents <ui:accordion> - Collapsible sections container."""
236
+
237
+ def __init__(self):
238
+ self.children: List[QuantumNode] = [] # Should contain UISectionNode
239
+ self._init_layout_attrs()
240
+
241
+ def add_child(self, child: QuantumNode):
242
+ self.children.append(child)
243
+
244
+ def to_dict(self) -> Dict[str, Any]:
245
+ d = {"type": "ui_accordion", "children_count": len(self.children)}
246
+ d.update(self._layout_to_dict())
247
+ return d
248
+
249
+ def validate(self) -> List[str]:
250
+ errors = self._validate_layout()
251
+ for child in self.children:
252
+ if hasattr(child, 'validate'):
253
+ errors.extend(child.validate())
254
+ return errors
255
+
256
+
257
+ class UISectionNode(QuantumNode, UILayoutMixin):
258
+ """Represents <ui:section> - Collapsible section (inside accordion)."""
259
+
260
+ def __init__(self):
261
+ self.title: str = ""
262
+ self.expanded: bool = False
263
+ self.children: List[QuantumNode] = []
264
+ self._init_layout_attrs()
265
+
266
+ def add_child(self, child: QuantumNode):
267
+ self.children.append(child)
268
+
269
+ def to_dict(self) -> Dict[str, Any]:
270
+ d = {"type": "ui_section", "title": self.title,
271
+ "expanded": self.expanded, "children_count": len(self.children)}
272
+ d.update(self._layout_to_dict())
273
+ return d
274
+
275
+ def validate(self) -> List[str]:
276
+ errors = self._validate_layout()
277
+ if not self.title:
278
+ errors.append("Section title is required")
279
+ for child in self.children:
280
+ if hasattr(child, 'validate'):
281
+ errors.extend(child.validate())
282
+ return errors
283
+
284
+
285
+ class UIDividedBoxNode(QuantumNode, UILayoutMixin):
286
+ """Represents <ui:dividedbox> - Resizable split container."""
287
+
288
+ def __init__(self):
289
+ self.direction: str = "horizontal" # horizontal, vertical
290
+ self.children: List[QuantumNode] = []
291
+ self._init_layout_attrs()
292
+
293
+ def add_child(self, child: QuantumNode):
294
+ self.children.append(child)
295
+
296
+ def to_dict(self) -> Dict[str, Any]:
297
+ d = {"type": "ui_dividedbox", "direction": self.direction,
298
+ "children_count": len(self.children)}
299
+ d.update(self._layout_to_dict())
300
+ return d
301
+
302
+ def validate(self) -> List[str]:
303
+ errors = self._validate_layout()
304
+ if self.direction not in ('horizontal', 'vertical'):
305
+ errors.append(f"Invalid dividedbox direction: {self.direction}")
306
+ for child in self.children:
307
+ if hasattr(child, 'validate'):
308
+ errors.extend(child.validate())
309
+ return errors
310
+
311
+
312
+ class UIFormNode(QuantumNode, UILayoutMixin):
313
+ """Represents <ui:form> - Form container with validation support."""
314
+
315
+ def __init__(self):
316
+ self.on_submit: Optional[str] = None
317
+ self.children: List[QuantumNode] = []
318
+
319
+ # Validation mode: 'client', 'server', or 'both' (default)
320
+ self.validation_mode: str = "both"
321
+
322
+ # Show validation feedback inline (default) or in summary
323
+ self.error_display: str = "inline" # 'inline', 'summary', 'both'
324
+
325
+ # Disable HTML5 native validation (use custom JS instead)
326
+ self.novalidate: bool = False
327
+
328
+ # Custom validators defined at form level
329
+ self.validators: List['UIValidatorNode'] = []
330
+
331
+ self._init_layout_attrs()
332
+
333
+ def add_child(self, child: QuantumNode):
334
+ self.children.append(child)
335
+
336
+ def add_validator(self, validator: 'UIValidatorNode'):
337
+ """Add a custom validator to the form."""
338
+ self.validators.append(validator)
339
+
340
+ def to_dict(self) -> Dict[str, Any]:
341
+ d = {"type": "ui_form", "on_submit": self.on_submit,
342
+ "validation_mode": self.validation_mode,
343
+ "error_display": self.error_display,
344
+ "novalidate": self.novalidate,
345
+ "validators_count": len(self.validators),
346
+ "children_count": len(self.children)}
347
+ d.update(self._layout_to_dict())
348
+ return d
349
+
350
+ def validate(self) -> List[str]:
351
+ errors = self._validate_layout()
352
+ if self.validation_mode not in ('client', 'server', 'both'):
353
+ errors.append(f"Invalid validation_mode: {self.validation_mode}")
354
+ if self.error_display not in ('inline', 'summary', 'both'):
355
+ errors.append(f"Invalid error_display: {self.error_display}")
356
+ for child in self.children:
357
+ if hasattr(child, 'validate'):
358
+ errors.extend(child.validate())
359
+ for validator in self.validators:
360
+ if hasattr(validator, 'validate'):
361
+ errors.extend(validator.validate())
362
+ return errors
363
+
364
+
365
+ class UIValidatorNode(QuantumNode):
366
+ """
367
+ Represents <ui:validator> - Custom validation rule.
368
+
369
+ Can be defined at form level (reusable) or inline in an input.
370
+
371
+ Examples:
372
+ <ui:validator name="phone" pattern="^\d{10,11}$" message="Invalid phone number" />
373
+ <ui:validator name="passwordMatch" field="password" match="confirm_password"
374
+ message="Passwords must match" />
375
+ <ui:validator name="customEmail" type="email" message="Please enter a valid email" />
376
+ """
377
+
378
+ def __init__(self):
379
+ # Validator identification
380
+ self.name: Optional[str] = None # Unique name for reference
381
+
382
+ # Validation rule types
383
+ self.rule_type: Optional[str] = None # 'pattern', 'email', 'url', 'phone', 'match', 'custom'
384
+ self.pattern: Optional[str] = None # Regex pattern
385
+ self.match: Optional[str] = None # Field name to match against
386
+ self.min: Optional[str] = None # Min value
387
+ self.max: Optional[str] = None # Max value
388
+ self.minlength: Optional[int] = None # Min length
389
+ self.maxlength: Optional[int] = None # Max length
390
+
391
+ # Custom validation function (for 'custom' type)
392
+ self.expression: Optional[str] = None # Custom JS expression
393
+ self.server_expression: Optional[str] = None # Custom server-side expression
394
+
395
+ # Error handling
396
+ self.message: Optional[str] = None # Error message to display
397
+ self.field: Optional[str] = None # Target field (for form-level validators)
398
+
399
+ # When to trigger validation
400
+ self.trigger: str = "submit" # 'blur', 'change', 'input', 'submit'
401
+
402
+ def to_dict(self) -> Dict[str, Any]:
403
+ return {
404
+ "type": "ui_validator",
405
+ "name": self.name,
406
+ "rule_type": self.rule_type,
407
+ "pattern": self.pattern,
408
+ "match": self.match,
409
+ "min": self.min,
410
+ "max": self.max,
411
+ "minlength": self.minlength,
412
+ "maxlength": self.maxlength,
413
+ "expression": self.expression,
414
+ "message": self.message,
415
+ "field": self.field,
416
+ "trigger": self.trigger,
417
+ }
418
+
419
+ def validate(self) -> List[str]:
420
+ errors = []
421
+ if not self.name:
422
+ errors.append("Validator requires 'name' attribute")
423
+ if self.trigger not in ('blur', 'change', 'input', 'submit'):
424
+ errors.append(f"Invalid validator trigger: {self.trigger}")
425
+ # Must have at least one validation rule
426
+ has_rule = (self.pattern or self.match or self.rule_type or
427
+ self.min or self.max or self.minlength or self.maxlength or
428
+ self.expression)
429
+ if not has_rule:
430
+ errors.append(f"Validator '{self.name}' must define at least one validation rule")
431
+ return errors
432
+
433
+
434
+ class UIFormItemNode(QuantumNode, UILayoutMixin):
435
+ """Represents <ui:formitem> - Form item with label."""
436
+
437
+ def __init__(self):
438
+ self.label: Optional[str] = None
439
+ self.children: List[QuantumNode] = []
440
+ self._init_layout_attrs()
441
+
442
+ def add_child(self, child: QuantumNode):
443
+ self.children.append(child)
444
+
445
+ def to_dict(self) -> Dict[str, Any]:
446
+ d = {"type": "ui_formitem", "label": self.label,
447
+ "children_count": len(self.children)}
448
+ d.update(self._layout_to_dict())
449
+ return d
450
+
451
+ def validate(self) -> List[str]:
452
+ errors = self._validate_layout()
453
+ for child in self.children:
454
+ if hasattr(child, 'validate'):
455
+ errors.extend(child.validate())
456
+ return errors
457
+
458
+
459
+ class UISpacerNode(QuantumNode, UILayoutMixin):
460
+ """Represents <ui:spacer> - Flexible space filler."""
461
+
462
+ def __init__(self):
463
+ self.size: Optional[str] = None
464
+ self._init_layout_attrs()
465
+
466
+ def to_dict(self) -> Dict[str, Any]:
467
+ d = {"type": "ui_spacer", "size": self.size}
468
+ d.update(self._layout_to_dict())
469
+ return d
470
+
471
+ def validate(self) -> List[str]:
472
+ return self._validate_layout()
473
+
474
+
475
+ class UIScrollBoxNode(QuantumNode, UILayoutMixin):
476
+ """Represents <ui:scrollbox> - Scrollable container."""
477
+
478
+ def __init__(self):
479
+ self.children: List[QuantumNode] = []
480
+ self._init_layout_attrs()
481
+
482
+ def add_child(self, child: QuantumNode):
483
+ self.children.append(child)
484
+
485
+ def to_dict(self) -> Dict[str, Any]:
486
+ d = {"type": "ui_scrollbox", "children_count": len(self.children)}
487
+ d.update(self._layout_to_dict())
488
+ return d
489
+
490
+ def validate(self) -> List[str]:
491
+ errors = self._validate_layout()
492
+ for child in self.children:
493
+ if hasattr(child, 'validate'):
494
+ errors.extend(child.validate())
495
+ return errors
496
+
497
+
498
+ # ============================================
499
+ # WIDGETS
500
+ # ============================================
501
+
502
+ class UITextNode(QuantumNode, UILayoutMixin):
503
+ """Represents <ui:text> - Text display widget."""
504
+
505
+ def __init__(self):
506
+ self.content: str = ""
507
+ self.size: Optional[str] = None # xs, sm, md, lg, xl, 2xl
508
+ self.weight: Optional[str] = None # normal, bold, light
509
+ self._init_layout_attrs()
510
+
511
+ def to_dict(self) -> Dict[str, Any]:
512
+ d = {"type": "ui_text", "content": self.content,
513
+ "size": self.size, "weight": self.weight}
514
+ d.update(self._layout_to_dict())
515
+ return d
516
+
517
+ def validate(self) -> List[str]:
518
+ return self._validate_layout()
519
+
520
+
521
+ class UIButtonNode(QuantumNode, UILayoutMixin):
522
+ """Represents <ui:button> - Clickable button widget."""
523
+
524
+ def __init__(self):
525
+ self.content: str = ""
526
+ self.on_click: Optional[str] = None
527
+ self.variant: Optional[str] = None # primary, secondary, danger, success
528
+ self.disabled: bool = False
529
+ self._init_layout_attrs()
530
+
531
+ def to_dict(self) -> Dict[str, Any]:
532
+ d = {"type": "ui_button", "content": self.content,
533
+ "on_click": self.on_click, "variant": self.variant,
534
+ "disabled": self.disabled}
535
+ d.update(self._layout_to_dict())
536
+ return d
537
+
538
+ def validate(self) -> List[str]:
539
+ return self._validate_layout()
540
+
541
+
542
+ class UIInputNode(QuantumNode, UILayoutMixin):
543
+ """Represents <ui:input> - Text input widget with validation support."""
544
+
545
+ def __init__(self):
546
+ self.bind: Optional[str] = None
547
+ self.placeholder: Optional[str] = None
548
+ self.input_type: str = "text" # text, password, email, number
549
+ self.on_change: Optional[str] = None
550
+ self.on_submit: Optional[str] = None
551
+
552
+ # Validation attributes (HTML5 + server-side)
553
+ self.required: bool = False
554
+ self.min: Optional[str] = None # Min value (number/date)
555
+ self.max: Optional[str] = None # Max value (number/date)
556
+ self.minlength: Optional[int] = None # Min string length
557
+ self.maxlength: Optional[int] = None # Max string length
558
+ self.pattern: Optional[str] = None # Regex pattern
559
+ self.error_message: Optional[str] = None # Custom error message
560
+
561
+ # Custom validators (references to ui:validator rules)
562
+ self.validators: List[str] = []
563
+
564
+ self._init_layout_attrs()
565
+
566
+ def to_dict(self) -> Dict[str, Any]:
567
+ d = {"type": "ui_input", "bind": self.bind,
568
+ "placeholder": self.placeholder, "input_type": self.input_type,
569
+ "required": self.required, "min": self.min, "max": self.max,
570
+ "minlength": self.minlength, "maxlength": self.maxlength,
571
+ "pattern": self.pattern, "error_message": self.error_message,
572
+ "validators": self.validators}
573
+ d.update(self._layout_to_dict())
574
+ return d
575
+
576
+ def validate(self) -> List[str]:
577
+ errors = self._validate_layout()
578
+ # Validate that minlength <= maxlength if both specified
579
+ if self.minlength is not None and self.maxlength is not None:
580
+ if self.minlength > self.maxlength:
581
+ errors.append(f"Input minlength ({self.minlength}) cannot exceed maxlength ({self.maxlength})")
582
+ return errors
583
+
584
+
585
+ class UICheckboxNode(QuantumNode, UILayoutMixin):
586
+ """Represents <ui:checkbox> - Checkbox widget."""
587
+
588
+ def __init__(self):
589
+ self.bind: Optional[str] = None
590
+ self.label: Optional[str] = None
591
+ self._init_layout_attrs()
592
+
593
+ def to_dict(self) -> Dict[str, Any]:
594
+ d = {"type": "ui_checkbox", "bind": self.bind, "label": self.label}
595
+ d.update(self._layout_to_dict())
596
+ return d
597
+
598
+ def validate(self) -> List[str]:
599
+ return self._validate_layout()
600
+
601
+
602
+ class UIRadioNode(QuantumNode, UILayoutMixin):
603
+ """Represents <ui:radio> - Radio button group."""
604
+
605
+ def __init__(self):
606
+ self.bind: Optional[str] = None
607
+ self.options: Optional[str] = None # comma-separated or source ref
608
+ self._init_layout_attrs()
609
+
610
+ def to_dict(self) -> Dict[str, Any]:
611
+ d = {"type": "ui_radio", "bind": self.bind, "options": self.options}
612
+ d.update(self._layout_to_dict())
613
+ return d
614
+
615
+ def validate(self) -> List[str]:
616
+ return self._validate_layout()
617
+
618
+
619
+ class UISwitchNode(QuantumNode, UILayoutMixin):
620
+ """Represents <ui:switch> - Toggle switch widget."""
621
+
622
+ def __init__(self):
623
+ self.bind: Optional[str] = None
624
+ self.label: Optional[str] = None
625
+ self._init_layout_attrs()
626
+
627
+ def to_dict(self) -> Dict[str, Any]:
628
+ d = {"type": "ui_switch", "bind": self.bind, "label": self.label}
629
+ d.update(self._layout_to_dict())
630
+ return d
631
+
632
+ def validate(self) -> List[str]:
633
+ return self._validate_layout()
634
+
635
+
636
+ class UISelectNode(QuantumNode, UILayoutMixin):
637
+ """Represents <ui:select> - Dropdown select widget."""
638
+
639
+ def __init__(self):
640
+ self.bind: Optional[str] = None
641
+ self.options: Optional[str] = None
642
+ self.source: Optional[str] = None
643
+ self.children: List[QuantumNode] = [] # UIOptionNode children
644
+ self._init_layout_attrs()
645
+
646
+ def add_child(self, child: QuantumNode):
647
+ self.children.append(child)
648
+
649
+ def to_dict(self) -> Dict[str, Any]:
650
+ d = {"type": "ui_select", "bind": self.bind,
651
+ "options": self.options, "source": self.source,
652
+ "children_count": len(self.children)}
653
+ d.update(self._layout_to_dict())
654
+ return d
655
+
656
+ def validate(self) -> List[str]:
657
+ return self._validate_layout()
658
+
659
+
660
+ class UITableNode(QuantumNode, UILayoutMixin):
661
+ """Represents <ui:table> - Data table widget."""
662
+
663
+ def __init__(self):
664
+ self.source: Optional[str] = None
665
+ self.children: List[QuantumNode] = [] # UIColumnNode children
666
+ self._init_layout_attrs()
667
+
668
+ def add_child(self, child: QuantumNode):
669
+ self.children.append(child)
670
+
671
+ def to_dict(self) -> Dict[str, Any]:
672
+ d = {"type": "ui_table", "source": self.source,
673
+ "children_count": len(self.children)}
674
+ d.update(self._layout_to_dict())
675
+ return d
676
+
677
+ def validate(self) -> List[str]:
678
+ return self._validate_layout()
679
+
680
+
681
+ class UIColumnNode(QuantumNode):
682
+ """Represents <ui:column> - Table column definition."""
683
+
684
+ def __init__(self):
685
+ self.key: Optional[str] = None
686
+ self.label: Optional[str] = None
687
+ self.column_width: Optional[str] = None
688
+ self.align: Optional[str] = None # left, center, right
689
+
690
+ def to_dict(self) -> Dict[str, Any]:
691
+ return {"type": "ui_column", "key": self.key,
692
+ "label": self.label, "width": self.column_width,
693
+ "align": self.align}
694
+
695
+ def validate(self) -> List[str]:
696
+ errors = []
697
+ if not self.key:
698
+ errors.append("Column key is required")
699
+ return errors
700
+
701
+
702
+ class UIListNode(QuantumNode, UILayoutMixin):
703
+ """Represents <ui:list> - Repeating list widget."""
704
+
705
+ def __init__(self):
706
+ self.source: Optional[str] = None
707
+ self.as_var: Optional[str] = None # loop variable name
708
+ self.children: List[QuantumNode] = []
709
+ self._init_layout_attrs()
710
+
711
+ def add_child(self, child: QuantumNode):
712
+ self.children.append(child)
713
+
714
+ def to_dict(self) -> Dict[str, Any]:
715
+ d = {"type": "ui_list", "source": self.source,
716
+ "as": self.as_var, "children_count": len(self.children)}
717
+ d.update(self._layout_to_dict())
718
+ return d
719
+
720
+ def validate(self) -> List[str]:
721
+ return self._validate_layout()
722
+
723
+
724
+ class UIItemNode(QuantumNode, UILayoutMixin):
725
+ """Represents <ui:item> - List item container."""
726
+
727
+ def __init__(self):
728
+ self.children: List[QuantumNode] = []
729
+ self._init_layout_attrs()
730
+
731
+ def add_child(self, child: QuantumNode):
732
+ self.children.append(child)
733
+
734
+ def to_dict(self) -> Dict[str, Any]:
735
+ d = {"type": "ui_item", "children_count": len(self.children)}
736
+ d.update(self._layout_to_dict())
737
+ return d
738
+
739
+ def validate(self) -> List[str]:
740
+ errors = self._validate_layout()
741
+ for child in self.children:
742
+ if hasattr(child, 'validate'):
743
+ errors.extend(child.validate())
744
+ return errors
745
+
746
+
747
+ class UIImageNode(QuantumNode, UILayoutMixin):
748
+ """Represents <ui:image> - Image display widget."""
749
+
750
+ def __init__(self):
751
+ self.src: Optional[str] = None
752
+ self.alt: Optional[str] = None
753
+ self._init_layout_attrs()
754
+
755
+ def to_dict(self) -> Dict[str, Any]:
756
+ d = {"type": "ui_image", "src": self.src, "alt": self.alt}
757
+ d.update(self._layout_to_dict())
758
+ return d
759
+
760
+ def validate(self) -> List[str]:
761
+ errors = self._validate_layout()
762
+ if not self.src:
763
+ errors.append("Image src is required")
764
+ return errors
765
+
766
+
767
+ class UILinkNode(QuantumNode, UILayoutMixin):
768
+ """Represents <ui:link> - Hyperlink widget."""
769
+
770
+ def __init__(self):
771
+ self.to: Optional[str] = None
772
+ self.external: bool = False
773
+ self.content: str = ""
774
+ self._init_layout_attrs()
775
+
776
+ def to_dict(self) -> Dict[str, Any]:
777
+ d = {"type": "ui_link", "to": self.to,
778
+ "external": self.external, "content": self.content}
779
+ d.update(self._layout_to_dict())
780
+ return d
781
+
782
+ def validate(self) -> List[str]:
783
+ errors = self._validate_layout()
784
+ if not self.to:
785
+ errors.append("Link 'to' attribute is required")
786
+ return errors
787
+
788
+
789
+ class UIProgressNode(QuantumNode, UILayoutMixin):
790
+ """Represents <ui:progress> - Progress bar widget."""
791
+
792
+ def __init__(self):
793
+ self.value: Optional[str] = None
794
+ self.max: Optional[str] = None
795
+ self._init_layout_attrs()
796
+
797
+ def to_dict(self) -> Dict[str, Any]:
798
+ d = {"type": "ui_progress", "value": self.value, "max": self.max}
799
+ d.update(self._layout_to_dict())
800
+ return d
801
+
802
+ def validate(self) -> List[str]:
803
+ return self._validate_layout()
804
+
805
+
806
+ class UITreeNode(QuantumNode, UILayoutMixin):
807
+ """Represents <ui:tree> - Tree view widget."""
808
+
809
+ def __init__(self):
810
+ self.source: Optional[str] = None
811
+ self.on_select: Optional[str] = None
812
+ self._init_layout_attrs()
813
+
814
+ def to_dict(self) -> Dict[str, Any]:
815
+ d = {"type": "ui_tree", "source": self.source,
816
+ "on_select": self.on_select}
817
+ d.update(self._layout_to_dict())
818
+ return d
819
+
820
+ def validate(self) -> List[str]:
821
+ return self._validate_layout()
822
+
823
+
824
+ class UIMenuNode(QuantumNode, UILayoutMixin):
825
+ """Represents <ui:menu> - Menu container."""
826
+
827
+ def __init__(self):
828
+ self.children: List[QuantumNode] = [] # UIOptionNode children
829
+ self._init_layout_attrs()
830
+
831
+ def add_child(self, child: QuantumNode):
832
+ self.children.append(child)
833
+
834
+ def to_dict(self) -> Dict[str, Any]:
835
+ d = {"type": "ui_menu", "children_count": len(self.children)}
836
+ d.update(self._layout_to_dict())
837
+ return d
838
+
839
+ def validate(self) -> List[str]:
840
+ errors = self._validate_layout()
841
+ for child in self.children:
842
+ if hasattr(child, 'validate'):
843
+ errors.extend(child.validate())
844
+ return errors
845
+
846
+
847
+ class UIOptionNode(QuantumNode):
848
+ """Represents <ui:option> - Menu/select option."""
849
+
850
+ def __init__(self):
851
+ self.value: Optional[str] = None
852
+ self.label: Optional[str] = None
853
+ self.on_click: Optional[str] = None
854
+
855
+ def to_dict(self) -> Dict[str, Any]:
856
+ return {"type": "ui_option", "value": self.value,
857
+ "label": self.label, "on_click": self.on_click}
858
+
859
+ def validate(self) -> List[str]:
860
+ return []
861
+
862
+
863
+ class UILogNode(QuantumNode, UILayoutMixin):
864
+ """Represents <ui:log> - Scrollable log output widget."""
865
+
866
+ def __init__(self):
867
+ self.auto_scroll: bool = True
868
+ self.max_lines: Optional[int] = None
869
+ self._init_layout_attrs()
870
+
871
+ def to_dict(self) -> Dict[str, Any]:
872
+ d = {"type": "ui_log", "auto_scroll": self.auto_scroll,
873
+ "max_lines": self.max_lines}
874
+ d.update(self._layout_to_dict())
875
+ return d
876
+
877
+ def validate(self) -> List[str]:
878
+ return self._validate_layout()
879
+
880
+
881
+ class UIMarkdownNode(QuantumNode, UILayoutMixin):
882
+ """Represents <ui:markdown> - Markdown rendered content."""
883
+
884
+ def __init__(self):
885
+ self.content: str = ""
886
+ self._init_layout_attrs()
887
+
888
+ def to_dict(self) -> Dict[str, Any]:
889
+ d = {"type": "ui_markdown", "content_length": len(self.content)}
890
+ d.update(self._layout_to_dict())
891
+ return d
892
+
893
+ def validate(self) -> List[str]:
894
+ return self._validate_layout()
895
+
896
+
897
+ class UIHeaderNode(QuantumNode, UILayoutMixin):
898
+ """Represents <ui:header> - Page/window header."""
899
+
900
+ def __init__(self):
901
+ self.title: Optional[str] = None
902
+ self.content: str = ""
903
+ self.children: List[QuantumNode] = []
904
+ self._init_layout_attrs()
905
+
906
+ def add_child(self, child: QuantumNode):
907
+ self.children.append(child)
908
+
909
+ def to_dict(self) -> Dict[str, Any]:
910
+ d = {"type": "ui_header", "title": self.title,
911
+ "content": self.content, "children_count": len(self.children)}
912
+ d.update(self._layout_to_dict())
913
+ return d
914
+
915
+ def validate(self) -> List[str]:
916
+ return self._validate_layout()
917
+
918
+
919
+ class UIFooterNode(QuantumNode, UILayoutMixin):
920
+ """Represents <ui:footer> - Page/window footer."""
921
+
922
+ def __init__(self):
923
+ self.content: str = ""
924
+ self.children: List[QuantumNode] = []
925
+ self._init_layout_attrs()
926
+
927
+ def add_child(self, child: QuantumNode):
928
+ self.children.append(child)
929
+
930
+ def to_dict(self) -> Dict[str, Any]:
931
+ d = {"type": "ui_footer", "content": self.content,
932
+ "children_count": len(self.children)}
933
+ d.update(self._layout_to_dict())
934
+ return d
935
+
936
+ def validate(self) -> List[str]:
937
+ return self._validate_layout()
938
+
939
+
940
+ class UIRuleNode(QuantumNode):
941
+ """Represents <ui:rule> - Horizontal rule/separator."""
942
+
943
+ def __init__(self):
944
+ pass
945
+
946
+ def to_dict(self) -> Dict[str, Any]:
947
+ return {"type": "ui_rule"}
948
+
949
+ def validate(self) -> List[str]:
950
+ return []
951
+
952
+
953
+ class UILoadingNode(QuantumNode, UILayoutMixin):
954
+ """Represents <ui:loading> - Loading indicator widget."""
955
+
956
+ def __init__(self):
957
+ self.text: Optional[str] = None
958
+ self._init_layout_attrs()
959
+
960
+ def to_dict(self) -> Dict[str, Any]:
961
+ d = {"type": "ui_loading", "text": self.text}
962
+ d.update(self._layout_to_dict())
963
+ return d
964
+
965
+ def validate(self) -> List[str]:
966
+ return self._validate_layout()
967
+
968
+
969
+ class UIBadgeNode(QuantumNode, UILayoutMixin):
970
+ """Represents <ui:badge> - Small status badge."""
971
+
972
+ def __init__(self):
973
+ self.content: str = ""
974
+ self.variant: Optional[str] = None # primary, secondary, danger, success, warning
975
+ self.badge_color: Optional[str] = None
976
+ self._init_layout_attrs()
977
+
978
+ def to_dict(self) -> Dict[str, Any]:
979
+ d = {"type": "ui_badge", "content": self.content,
980
+ "variant": self.variant, "color": self.badge_color}
981
+ d.update(self._layout_to_dict())
982
+ return d
983
+
984
+ def validate(self) -> List[str]:
985
+ return self._validate_layout()
986
+
987
+
988
+ # ============================================
989
+ # ANIMATION MIXIN
990
+ # ============================================
991
+
992
+ class UIAnimationMixin:
993
+ """Shared animation attributes for UI components that support animations."""
994
+
995
+ # Valid animation types
996
+ ANIMATION_TYPES = ('fade', 'slide', 'scale', 'rotate', 'slide-left', 'slide-right',
997
+ 'slide-up', 'slide-down', 'fade-in', 'fade-out', 'scale-in',
998
+ 'scale-out', 'bounce', 'pulse', 'shake')
999
+
1000
+ # Valid animation triggers
1001
+ ANIMATION_TRIGGERS = ('on-load', 'on-hover', 'on-click', 'on-visible', 'none')
1002
+
1003
+ # Valid easing functions
1004
+ EASING_FUNCTIONS = ('ease', 'ease-in', 'ease-out', 'ease-in-out', 'linear',
1005
+ 'spring', 'bounce')
1006
+
1007
+ def _init_animation_attrs(self):
1008
+ """Initialize animation attributes."""
1009
+ self.animate: Optional[str] = None # Animation preset (e.g., 'slide-in-left')
1010
+ self.transition: Optional[str] = None # Transition shorthand (e.g., 'scale:0.95:100ms')
1011
+ self.anim_type: Optional[str] = None # Animation type for ui:animate
1012
+ self.anim_duration: Optional[str] = None # Duration in ms or CSS format
1013
+ self.anim_delay: Optional[str] = None # Delay in ms or CSS format
1014
+ self.anim_easing: Optional[str] = None # Easing function
1015
+ self.anim_repeat: Optional[str] = None # Repeat count ('1', '2', 'infinite')
1016
+ self.anim_trigger: Optional[str] = None # Trigger: on-load, on-hover, on-click, on-visible
1017
+ self.anim_direction: Optional[str] = None # normal, reverse, alternate, alternate-reverse
1018
+
1019
+ def _animation_to_dict(self) -> Dict[str, Any]:
1020
+ """Return animation attributes as dict."""
1021
+ d = {}
1022
+ for attr in ('animate', 'transition', 'anim_type', 'anim_duration',
1023
+ 'anim_delay', 'anim_easing', 'anim_repeat', 'anim_trigger',
1024
+ 'anim_direction'):
1025
+ val = getattr(self, attr, None)
1026
+ if val is not None:
1027
+ d[attr] = val
1028
+ return d
1029
+
1030
+ def _validate_animation(self) -> List[str]:
1031
+ """Validate animation attributes."""
1032
+ errors = []
1033
+ if self.anim_type and self.anim_type not in self.ANIMATION_TYPES:
1034
+ errors.append(f"Invalid animation type: {self.anim_type}")
1035
+ if self.anim_trigger and self.anim_trigger not in self.ANIMATION_TRIGGERS:
1036
+ errors.append(f"Invalid animation trigger: {self.anim_trigger}")
1037
+ if self.anim_easing and self.anim_easing not in self.EASING_FUNCTIONS:
1038
+ errors.append(f"Invalid easing function: {self.anim_easing}")
1039
+ return errors
1040
+
1041
+ def has_animation(self) -> bool:
1042
+ """Check if this node has any animation configured."""
1043
+ return any([
1044
+ self.animate,
1045
+ self.transition,
1046
+ self.anim_type,
1047
+ ])
1048
+
1049
+
1050
+ # ============================================
1051
+ # ANIMATION NODE
1052
+ # ============================================
1053
+
1054
+ class UIAnimateNode(QuantumNode, UILayoutMixin, UIAnimationMixin):
1055
+ """
1056
+ Represents <ui:animate> - Animation wrapper container.
1057
+
1058
+ Wraps child elements and applies animation effects.
1059
+
1060
+ Syntax examples:
1061
+ <ui:animate type="fade" duration="300" trigger="on-load">
1062
+ <ui:panel>Content fades in</ui:panel>
1063
+ </ui:animate>
1064
+
1065
+ <ui:animate type="slide-left" delay="200" easing="ease-out">
1066
+ <ui:text>Slides in from left</ui:text>
1067
+ </ui:animate>
1068
+ """
1069
+
1070
+ def __init__(self):
1071
+ self.children: List[QuantumNode] = []
1072
+ self._init_layout_attrs()
1073
+ self._init_animation_attrs()
1074
+
1075
+ def add_child(self, child: QuantumNode):
1076
+ self.children.append(child)
1077
+
1078
+ def to_dict(self) -> Dict[str, Any]:
1079
+ d = {
1080
+ "type": "ui_animate",
1081
+ "children_count": len(self.children),
1082
+ }
1083
+ d.update(self._layout_to_dict())
1084
+ d.update(self._animation_to_dict())
1085
+ return d
1086
+
1087
+ def validate(self) -> List[str]:
1088
+ errors = self._validate_layout()
1089
+ errors.extend(self._validate_animation())
1090
+ if not self.anim_type and not self.animate:
1091
+ errors.append("ui:animate requires 'type' or 'animate' attribute")
1092
+ for child in self.children:
1093
+ if hasattr(child, 'validate'):
1094
+ errors.extend(child.validate())
1095
+ return errors
1096
+
1097
+
1098
+ # ============================================
1099
+ # COMPONENT LIBRARY NODES
1100
+ # ============================================
1101
+
1102
+ class UICardNode(QuantumNode, UILayoutMixin):
1103
+ """Represents <ui:card> - Card with header, body, footer."""
1104
+
1105
+ def __init__(self):
1106
+ self.title: Optional[str] = None
1107
+ self.subtitle: Optional[str] = None
1108
+ self.image: Optional[str] = None # Header image URL
1109
+ self.variant: Optional[str] = None # default, elevated, outlined
1110
+ self.children: List[QuantumNode] = []
1111
+ self._init_layout_attrs()
1112
+
1113
+ def add_child(self, child: QuantumNode):
1114
+ self.children.append(child)
1115
+
1116
+ def to_dict(self) -> Dict[str, Any]:
1117
+ d = {"type": "ui_card", "title": self.title,
1118
+ "subtitle": self.subtitle, "image": self.image,
1119
+ "variant": self.variant, "children_count": len(self.children)}
1120
+ d.update(self._layout_to_dict())
1121
+ return d
1122
+
1123
+ def validate(self) -> List[str]:
1124
+ errors = self._validate_layout()
1125
+ for child in self.children:
1126
+ if hasattr(child, 'validate'):
1127
+ errors.extend(child.validate())
1128
+ return errors
1129
+
1130
+
1131
+ class UICardHeaderNode(QuantumNode, UILayoutMixin):
1132
+ """Represents <ui:card-header> - Card header section."""
1133
+
1134
+ def __init__(self):
1135
+ self.children: List[QuantumNode] = []
1136
+ self._init_layout_attrs()
1137
+
1138
+ def add_child(self, child: QuantumNode):
1139
+ self.children.append(child)
1140
+
1141
+ def to_dict(self) -> Dict[str, Any]:
1142
+ d = {"type": "ui_card_header", "children_count": len(self.children)}
1143
+ d.update(self._layout_to_dict())
1144
+ return d
1145
+
1146
+ def validate(self) -> List[str]:
1147
+ errors = self._validate_layout()
1148
+ for child in self.children:
1149
+ if hasattr(child, 'validate'):
1150
+ errors.extend(child.validate())
1151
+ return errors
1152
+
1153
+
1154
+ class UICardBodyNode(QuantumNode, UILayoutMixin):
1155
+ """Represents <ui:card-body> - Card body section."""
1156
+
1157
+ def __init__(self):
1158
+ self.children: List[QuantumNode] = []
1159
+ self._init_layout_attrs()
1160
+
1161
+ def add_child(self, child: QuantumNode):
1162
+ self.children.append(child)
1163
+
1164
+ def to_dict(self) -> Dict[str, Any]:
1165
+ d = {"type": "ui_card_body", "children_count": len(self.children)}
1166
+ d.update(self._layout_to_dict())
1167
+ return d
1168
+
1169
+ def validate(self) -> List[str]:
1170
+ errors = self._validate_layout()
1171
+ for child in self.children:
1172
+ if hasattr(child, 'validate'):
1173
+ errors.extend(child.validate())
1174
+ return errors
1175
+
1176
+
1177
+ class UICardFooterNode(QuantumNode, UILayoutMixin):
1178
+ """Represents <ui:card-footer> - Card footer section."""
1179
+
1180
+ def __init__(self):
1181
+ self.children: List[QuantumNode] = []
1182
+ self._init_layout_attrs()
1183
+
1184
+ def add_child(self, child: QuantumNode):
1185
+ self.children.append(child)
1186
+
1187
+ def to_dict(self) -> Dict[str, Any]:
1188
+ d = {"type": "ui_card_footer", "children_count": len(self.children)}
1189
+ d.update(self._layout_to_dict())
1190
+ return d
1191
+
1192
+ def validate(self) -> List[str]:
1193
+ errors = self._validate_layout()
1194
+ for child in self.children:
1195
+ if hasattr(child, 'validate'):
1196
+ errors.extend(child.validate())
1197
+ return errors
1198
+
1199
+
1200
+ class UIModalNode(QuantumNode, UILayoutMixin):
1201
+ """Represents <ui:modal> - Modal/dialog with open/close."""
1202
+
1203
+ def __init__(self):
1204
+ self.title: Optional[str] = None
1205
+ self.modal_id: Optional[str] = None # Unique ID for targeting
1206
+ self.open: bool = False # Initial state
1207
+ self.closable: bool = True # Show close button
1208
+ self.size: Optional[str] = None # sm, md, lg, xl, full
1209
+ self.children: List[QuantumNode] = []
1210
+ self._init_layout_attrs()
1211
+
1212
+ def add_child(self, child: QuantumNode):
1213
+ self.children.append(child)
1214
+
1215
+ def to_dict(self) -> Dict[str, Any]:
1216
+ d = {"type": "ui_modal", "title": self.title,
1217
+ "modal_id": self.modal_id, "open": self.open,
1218
+ "closable": self.closable, "size": self.size,
1219
+ "children_count": len(self.children)}
1220
+ d.update(self._layout_to_dict())
1221
+ return d
1222
+
1223
+ def validate(self) -> List[str]:
1224
+ errors = self._validate_layout()
1225
+ for child in self.children:
1226
+ if hasattr(child, 'validate'):
1227
+ errors.extend(child.validate())
1228
+ return errors
1229
+
1230
+
1231
+ class UIChartNode(QuantumNode, UILayoutMixin):
1232
+ """Represents <ui:chart> - Simple charts (bar, line, pie)."""
1233
+
1234
+ def __init__(self):
1235
+ self.chart_type: str = "bar" # bar, line, pie, doughnut
1236
+ self.source: Optional[str] = None # Data source binding
1237
+ self.labels: Optional[str] = None # Comma-separated labels
1238
+ self.values: Optional[str] = None # Comma-separated values
1239
+ self.title: Optional[str] = None
1240
+ self.colors: Optional[str] = None # Comma-separated colors
1241
+ self._init_layout_attrs()
1242
+
1243
+ def to_dict(self) -> Dict[str, Any]:
1244
+ d = {"type": "ui_chart", "chart_type": self.chart_type,
1245
+ "source": self.source, "labels": self.labels,
1246
+ "values": self.values, "title": self.title,
1247
+ "colors": self.colors}
1248
+ d.update(self._layout_to_dict())
1249
+ return d
1250
+
1251
+ def validate(self) -> List[str]:
1252
+ errors = self._validate_layout()
1253
+ if self.chart_type not in ('bar', 'line', 'pie', 'doughnut'):
1254
+ errors.append(f"Invalid chart type: {self.chart_type}")
1255
+ return errors
1256
+
1257
+
1258
+ class UIAvatarNode(QuantumNode, UILayoutMixin):
1259
+ """Represents <ui:avatar> - User avatar with image/initials."""
1260
+
1261
+ def __init__(self):
1262
+ self.src: Optional[str] = None # Image URL
1263
+ self.name: Optional[str] = None # Name for initials fallback
1264
+ self.size: Optional[str] = None # xs, sm, md, lg, xl
1265
+ self.shape: str = "circle" # circle, square
1266
+ self.status: Optional[str] = None # online, offline, away, busy
1267
+ self._init_layout_attrs()
1268
+
1269
+ def to_dict(self) -> Dict[str, Any]:
1270
+ d = {"type": "ui_avatar", "src": self.src,
1271
+ "name": self.name, "size": self.size,
1272
+ "shape": self.shape, "status": self.status}
1273
+ d.update(self._layout_to_dict())
1274
+ return d
1275
+
1276
+ def validate(self) -> List[str]:
1277
+ errors = self._validate_layout()
1278
+ if self.shape not in ('circle', 'square'):
1279
+ errors.append(f"Invalid avatar shape: {self.shape}")
1280
+ return errors
1281
+
1282
+
1283
+ class UITooltipNode(QuantumNode, UILayoutMixin):
1284
+ """Represents <ui:tooltip> - Tooltip on hover."""
1285
+
1286
+ def __init__(self):
1287
+ self.content: str = "" # Tooltip text
1288
+ self.position: str = "top" # top, bottom, left, right
1289
+ self.children: List[QuantumNode] = [] # Element to attach tooltip to
1290
+ self._init_layout_attrs()
1291
+
1292
+ def add_child(self, child: QuantumNode):
1293
+ self.children.append(child)
1294
+
1295
+ def to_dict(self) -> Dict[str, Any]:
1296
+ d = {"type": "ui_tooltip", "content": self.content,
1297
+ "position": self.position, "children_count": len(self.children)}
1298
+ d.update(self._layout_to_dict())
1299
+ return d
1300
+
1301
+ def validate(self) -> List[str]:
1302
+ errors = self._validate_layout()
1303
+ if self.position not in ('top', 'bottom', 'left', 'right'):
1304
+ errors.append(f"Invalid tooltip position: {self.position}")
1305
+ return errors
1306
+
1307
+
1308
+ class UIDropdownNode(QuantumNode, UILayoutMixin):
1309
+ """Represents <ui:dropdown> - Dropdown menu."""
1310
+
1311
+ def __init__(self):
1312
+ self.label: Optional[str] = None # Trigger button label
1313
+ self.trigger: str = "click" # click, hover
1314
+ self.dropdown_align: str = "left" # left, right
1315
+ self.children: List[QuantumNode] = [] # UIOptionNode children
1316
+ self._init_layout_attrs()
1317
+
1318
+ def add_child(self, child: QuantumNode):
1319
+ self.children.append(child)
1320
+
1321
+ def to_dict(self) -> Dict[str, Any]:
1322
+ d = {"type": "ui_dropdown", "label": self.label,
1323
+ "trigger": self.trigger, "dropdown_align": self.dropdown_align,
1324
+ "children_count": len(self.children)}
1325
+ d.update(self._layout_to_dict())
1326
+ return d
1327
+
1328
+ def validate(self) -> List[str]:
1329
+ errors = self._validate_layout()
1330
+ if self.trigger not in ('click', 'hover'):
1331
+ errors.append(f"Invalid dropdown trigger: {self.trigger}")
1332
+ return errors
1333
+
1334
+
1335
+ class UIAlertNode(QuantumNode, UILayoutMixin):
1336
+ """Represents <ui:alert> - Alert/notification box."""
1337
+
1338
+ def __init__(self):
1339
+ self.content: str = ""
1340
+ self.title: Optional[str] = None
1341
+ self.variant: str = "info" # info, success, warning, danger
1342
+ self.dismissible: bool = False
1343
+ self.icon: Optional[str] = None # Icon name
1344
+ self.children: List[QuantumNode] = []
1345
+ self._init_layout_attrs()
1346
+
1347
+ def add_child(self, child: QuantumNode):
1348
+ self.children.append(child)
1349
+
1350
+ def to_dict(self) -> Dict[str, Any]:
1351
+ d = {"type": "ui_alert", "content": self.content,
1352
+ "title": self.title, "variant": self.variant,
1353
+ "dismissible": self.dismissible, "icon": self.icon,
1354
+ "children_count": len(self.children)}
1355
+ d.update(self._layout_to_dict())
1356
+ return d
1357
+
1358
+ def validate(self) -> List[str]:
1359
+ errors = self._validate_layout()
1360
+ if self.variant not in ('info', 'success', 'warning', 'danger'):
1361
+ errors.append(f"Invalid alert variant: {self.variant}")
1362
+ return errors
1363
+
1364
+
1365
+ class UIBreadcrumbNode(QuantumNode, UILayoutMixin):
1366
+ """Represents <ui:breadcrumb> - Navigation breadcrumbs."""
1367
+
1368
+ def __init__(self):
1369
+ self.separator: str = "/" # Separator character
1370
+ self.children: List[QuantumNode] = [] # UIBreadcrumbItemNode children
1371
+ self._init_layout_attrs()
1372
+
1373
+ def add_child(self, child: QuantumNode):
1374
+ self.children.append(child)
1375
+
1376
+ def to_dict(self) -> Dict[str, Any]:
1377
+ d = {"type": "ui_breadcrumb", "separator": self.separator,
1378
+ "children_count": len(self.children)}
1379
+ d.update(self._layout_to_dict())
1380
+ return d
1381
+
1382
+ def validate(self) -> List[str]:
1383
+ errors = self._validate_layout()
1384
+ for child in self.children:
1385
+ if hasattr(child, 'validate'):
1386
+ errors.extend(child.validate())
1387
+ return errors
1388
+
1389
+
1390
+ class UIBreadcrumbItemNode(QuantumNode):
1391
+ """Represents <ui:breadcrumb-item> - Individual breadcrumb item."""
1392
+
1393
+ def __init__(self):
1394
+ self.label: str = ""
1395
+ self.to: Optional[str] = None # Link URL (None = current/last item)
1396
+ self.icon: Optional[str] = None
1397
+
1398
+ def to_dict(self) -> Dict[str, Any]:
1399
+ return {"type": "ui_breadcrumb_item", "label": self.label,
1400
+ "to": self.to, "icon": self.icon}
1401
+
1402
+ def validate(self) -> List[str]:
1403
+ return []
1404
+
1405
+
1406
+ class UIPaginationNode(QuantumNode, UILayoutMixin):
1407
+ """Represents <ui:pagination> - Pagination controls."""
1408
+
1409
+ def __init__(self):
1410
+ self.total: Optional[str] = None # Total items
1411
+ self.page_size: str = "10" # Items per page
1412
+ self.current: str = "1" # Current page
1413
+ self.bind: Optional[str] = None # Binding for current page
1414
+ self.on_change: Optional[str] = None # Callback on page change
1415
+ self.show_total: bool = False # Show total items count
1416
+ self.show_jump: bool = False # Show page jump input
1417
+ self._init_layout_attrs()
1418
+
1419
+ def to_dict(self) -> Dict[str, Any]:
1420
+ d = {"type": "ui_pagination", "total": self.total,
1421
+ "page_size": self.page_size, "current": self.current,
1422
+ "bind": self.bind, "on_change": self.on_change,
1423
+ "show_total": self.show_total, "show_jump": self.show_jump}
1424
+ d.update(self._layout_to_dict())
1425
+ return d
1426
+
1427
+ def validate(self) -> List[str]:
1428
+ return self._validate_layout()
1429
+
1430
+
1431
+ class UISkeletonNode(QuantumNode, UILayoutMixin):
1432
+ """Represents <ui:skeleton> - Loading skeleton placeholder."""
1433
+
1434
+ def __init__(self):
1435
+ self.variant: str = "text" # text, circle, rect, card
1436
+ self.lines: int = 1 # Number of lines for text variant
1437
+ self.animated: bool = True # Animate the skeleton
1438
+ self._init_layout_attrs()
1439
+
1440
+ def to_dict(self) -> Dict[str, Any]:
1441
+ d = {"type": "ui_skeleton", "variant": self.variant,
1442
+ "lines": self.lines, "animated": self.animated}
1443
+ d.update(self._layout_to_dict())
1444
+ return d
1445
+
1446
+ def validate(self) -> List[str]:
1447
+ errors = self._validate_layout()
1448
+ if self.variant not in ('text', 'circle', 'rect', 'card'):
1449
+ errors.append(f"Invalid skeleton variant: {self.variant}")
1450
+ return errors
1451
+
1452
+
1453
+ # ============================================
1454
+ # TOAST NOTIFICATION
1455
+ # ============================================
1456
+
1457
+ class UIToastNode(QuantumNode, UILayoutMixin):
1458
+ """
1459
+ Represents <ui:toast> - Toast notification.
1460
+
1461
+ Displays temporary notification messages that auto-dismiss.
1462
+
1463
+ Attributes:
1464
+ variant: Style variant (info, success, warning, danger)
1465
+ position: Screen position (top-right, top-left, bottom-right, bottom-left, top-center, bottom-center)
1466
+ duration: Auto-dismiss time in ms (0 = no auto-dismiss)
1467
+ dismissible: Show close button
1468
+ icon: Icon to display
1469
+
1470
+ Example:
1471
+ <ui:toast variant="success" position="top-right" duration="3000">
1472
+ File saved successfully!
1473
+ </ui:toast>
1474
+ """
1475
+
1476
+ VALID_VARIANTS = {'info', 'success', 'warning', 'danger'}
1477
+ VALID_POSITIONS = {'top-right', 'top-left', 'bottom-right', 'bottom-left',
1478
+ 'top-center', 'bottom-center'}
1479
+
1480
+ def __init__(self):
1481
+ self.variant: str = "info"
1482
+ self.position: str = "top-right"
1483
+ self.duration: int = 3000 # ms, 0 = no auto-dismiss
1484
+ self.dismissible: bool = True
1485
+ self.icon: Optional[str] = None
1486
+ self.title: Optional[str] = None
1487
+ self.message: str = ""
1488
+ self.show: Optional[str] = None # Binding to control visibility
1489
+ self.on_close: Optional[str] = None # Callback on close
1490
+ self._init_layout_attrs()
1491
+
1492
+ def to_dict(self) -> Dict[str, Any]:
1493
+ d = {"type": "ui_toast", "variant": self.variant,
1494
+ "position": self.position, "duration": self.duration,
1495
+ "dismissible": self.dismissible, "icon": self.icon,
1496
+ "title": self.title, "message": self.message,
1497
+ "show": self.show, "on_close": self.on_close}
1498
+ d.update(self._layout_to_dict())
1499
+ return d
1500
+
1501
+ def validate(self) -> List[str]:
1502
+ errors = self._validate_layout()
1503
+ if self.variant not in self.VALID_VARIANTS:
1504
+ errors.append(f"Invalid toast variant: {self.variant}. Valid: {', '.join(self.VALID_VARIANTS)}")
1505
+ if self.position not in self.VALID_POSITIONS:
1506
+ errors.append(f"Invalid toast position: {self.position}. Valid: {', '.join(self.VALID_POSITIONS)}")
1507
+ return errors
1508
+
1509
+
1510
+ class UIToastContainerNode(QuantumNode, UILayoutMixin):
1511
+ """
1512
+ Represents <ui:toast-container> - Container for toast notifications.
1513
+
1514
+ A single container should be placed in the application to manage all toasts.
1515
+
1516
+ Attributes:
1517
+ position: Default position for toasts
1518
+ max_toasts: Maximum visible toasts (older ones are dismissed)
1519
+
1520
+ Example:
1521
+ <ui:toast-container position="top-right" max-toasts="5" />
1522
+ """
1523
+
1524
+ def __init__(self):
1525
+ self.position: str = "top-right"
1526
+ self.max_toasts: int = 5
1527
+ self._init_layout_attrs()
1528
+
1529
+ def to_dict(self) -> Dict[str, Any]:
1530
+ d = {"type": "ui_toast_container", "position": self.position,
1531
+ "max_toasts": self.max_toasts}
1532
+ d.update(self._layout_to_dict())
1533
+ return d
1534
+
1535
+ def validate(self) -> List[str]:
1536
+ return self._validate_layout()
1537
+
1538
+
1539
+ # ============================================
1540
+ # CAROUSEL / SLIDER
1541
+ # ============================================
1542
+
1543
+ class UICarouselNode(QuantumNode, UILayoutMixin):
1544
+ """
1545
+ Represents <ui:carousel> - Image/content carousel slider.
1546
+
1547
+ Displays slides with navigation controls and optional auto-play.
1548
+
1549
+ Attributes:
1550
+ auto_play: Enable automatic slide advancement
1551
+ interval: Time between slides in ms (for auto-play)
1552
+ show_indicators: Show dot indicators
1553
+ show_arrows: Show prev/next arrows
1554
+ loop: Loop back to start after last slide
1555
+ animation: Slide animation type (slide, fade)
1556
+
1557
+ Example:
1558
+ <ui:carousel auto-play="true" interval="5000" show-indicators="true">
1559
+ <ui:slide><ui:image src="img1.jpg" /></ui:slide>
1560
+ <ui:slide><ui:image src="img2.jpg" /></ui:slide>
1561
+ <ui:slide><ui:image src="img3.jpg" /></ui:slide>
1562
+ </ui:carousel>
1563
+ """
1564
+
1565
+ def __init__(self):
1566
+ self.auto_play: bool = False
1567
+ self.interval: int = 5000 # ms
1568
+ self.show_indicators: bool = True
1569
+ self.show_arrows: bool = True
1570
+ self.loop: bool = True
1571
+ self.animation: str = "slide" # slide, fade
1572
+ self.current: int = 0 # Current slide index
1573
+ self.bind: Optional[str] = None # Binding for current index
1574
+ self.on_change: Optional[str] = None # Callback on slide change
1575
+ self.children: List[QuantumNode] = [] # UISlideNode children
1576
+ self._init_layout_attrs()
1577
+
1578
+ def add_child(self, child: QuantumNode):
1579
+ self.children.append(child)
1580
+
1581
+ def to_dict(self) -> Dict[str, Any]:
1582
+ d = {"type": "ui_carousel", "auto_play": self.auto_play,
1583
+ "interval": self.interval, "show_indicators": self.show_indicators,
1584
+ "show_arrows": self.show_arrows, "loop": self.loop,
1585
+ "animation": self.animation, "current": self.current,
1586
+ "bind": self.bind, "on_change": self.on_change,
1587
+ "children_count": len(self.children)}
1588
+ d.update(self._layout_to_dict())
1589
+ return d
1590
+
1591
+ def validate(self) -> List[str]:
1592
+ errors = self._validate_layout()
1593
+ if self.animation not in ('slide', 'fade'):
1594
+ errors.append(f"Invalid carousel animation: {self.animation}")
1595
+ for child in self.children:
1596
+ if hasattr(child, 'validate'):
1597
+ errors.extend(child.validate())
1598
+ return errors
1599
+
1600
+
1601
+ class UISlideNode(QuantumNode, UILayoutMixin):
1602
+ """
1603
+ Represents <ui:slide> - Single slide in a carousel.
1604
+
1605
+ Can contain any content (images, text, cards, etc.)
1606
+
1607
+ Example:
1608
+ <ui:slide>
1609
+ <ui:image src="hero.jpg" alt="Hero image" />
1610
+ <ui:text size="xl">Welcome!</ui:text>
1611
+ </ui:slide>
1612
+ """
1613
+
1614
+ def __init__(self):
1615
+ self.children: List[QuantumNode] = []
1616
+ self._init_layout_attrs()
1617
+
1618
+ def add_child(self, child: QuantumNode):
1619
+ self.children.append(child)
1620
+
1621
+ def to_dict(self) -> Dict[str, Any]:
1622
+ d = {"type": "ui_slide", "children_count": len(self.children)}
1623
+ d.update(self._layout_to_dict())
1624
+ return d
1625
+
1626
+ def validate(self) -> List[str]:
1627
+ errors = self._validate_layout()
1628
+ for child in self.children:
1629
+ if hasattr(child, 'validate'):
1630
+ errors.extend(child.validate())
1631
+ return errors
1632
+
1633
+
1634
+ # ============================================
1635
+ # STEPPER / WIZARD
1636
+ # ============================================
1637
+
1638
+ class UIStepperNode(QuantumNode, UILayoutMixin):
1639
+ """
1640
+ Represents <ui:stepper> - Multi-step wizard/progress indicator.
1641
+
1642
+ Shows progress through a multi-step process with navigation.
1643
+
1644
+ Attributes:
1645
+ orientation: Layout direction (horizontal, vertical)
1646
+ current: Current step index (0-based)
1647
+ linear: If true, must complete steps in order
1648
+ show_labels: Show step labels
1649
+ clickable: Allow clicking on steps to navigate
1650
+ bind: Binding for current step
1651
+
1652
+ Example:
1653
+ <ui:stepper current="1" linear="true">
1654
+ <ui:step title="Account" description="Create account">
1655
+ <ui:form>...</ui:form>
1656
+ </ui:step>
1657
+ <ui:step title="Profile" description="Add details">
1658
+ <ui:form>...</ui:form>
1659
+ </ui:step>
1660
+ <ui:step title="Complete" description="All done!">
1661
+ <ui:text>Success!</ui:text>
1662
+ </ui:step>
1663
+ </ui:stepper>
1664
+ """
1665
+
1666
+ def __init__(self):
1667
+ self.orientation: str = "horizontal" # horizontal, vertical
1668
+ self.current: int = 0
1669
+ self.linear: bool = True # Must complete in order
1670
+ self.show_labels: bool = True
1671
+ self.clickable: bool = False # Allow clicking on steps
1672
+ self.bind: Optional[str] = None # Binding for current step
1673
+ self.on_change: Optional[str] = None # Callback on step change
1674
+ self.on_complete: Optional[str] = None # Callback when all steps done
1675
+ self.children: List[QuantumNode] = [] # UIStepNode children
1676
+ self._init_layout_attrs()
1677
+
1678
+ def add_child(self, child: QuantumNode):
1679
+ self.children.append(child)
1680
+
1681
+ def to_dict(self) -> Dict[str, Any]:
1682
+ d = {"type": "ui_stepper", "orientation": self.orientation,
1683
+ "current": self.current, "linear": self.linear,
1684
+ "show_labels": self.show_labels, "clickable": self.clickable,
1685
+ "bind": self.bind, "on_change": self.on_change,
1686
+ "on_complete": self.on_complete,
1687
+ "children_count": len(self.children)}
1688
+ d.update(self._layout_to_dict())
1689
+ return d
1690
+
1691
+ def validate(self) -> List[str]:
1692
+ errors = self._validate_layout()
1693
+ if self.orientation not in ('horizontal', 'vertical'):
1694
+ errors.append(f"Invalid stepper orientation: {self.orientation}")
1695
+ for child in self.children:
1696
+ if hasattr(child, 'validate'):
1697
+ errors.extend(child.validate())
1698
+ return errors
1699
+
1700
+
1701
+ class UIStepNode(QuantumNode, UILayoutMixin):
1702
+ """
1703
+ Represents <ui:step> - Single step in a stepper.
1704
+
1705
+ Attributes:
1706
+ title: Step title (shown in indicator)
1707
+ description: Optional description
1708
+ icon: Optional icon
1709
+ optional: If true, step can be skipped
1710
+ completed: Override completed state
1711
+
1712
+ Example:
1713
+ <ui:step title="Payment" description="Enter payment details" icon="credit-card">
1714
+ <ui:form>...</ui:form>
1715
+ </ui:step>
1716
+ """
1717
+
1718
+ def __init__(self):
1719
+ self.title: str = ""
1720
+ self.description: Optional[str] = None
1721
+ self.icon: Optional[str] = None
1722
+ self.optional: bool = False
1723
+ self.completed: Optional[bool] = None # Override state
1724
+ self.error: Optional[str] = None # Error message for this step
1725
+ self.children: List[QuantumNode] = [] # Step content
1726
+ self._init_layout_attrs()
1727
+
1728
+ def add_child(self, child: QuantumNode):
1729
+ self.children.append(child)
1730
+
1731
+ def to_dict(self) -> Dict[str, Any]:
1732
+ d = {"type": "ui_step", "title": self.title,
1733
+ "description": self.description, "icon": self.icon,
1734
+ "optional": self.optional, "completed": self.completed,
1735
+ "error": self.error, "children_count": len(self.children)}
1736
+ d.update(self._layout_to_dict())
1737
+ return d
1738
+
1739
+ def validate(self) -> List[str]:
1740
+ errors = self._validate_layout()
1741
+ if not self.title:
1742
+ errors.append("Step title is required")
1743
+ for child in self.children:
1744
+ if hasattr(child, 'validate'):
1745
+ errors.extend(child.validate())
1746
+ return errors
1747
+
1748
+
1749
+ # ============================================
1750
+ # CALENDAR / DATE PICKER
1751
+ # ============================================
1752
+
1753
+ class UICalendarNode(QuantumNode, UILayoutMixin):
1754
+ """
1755
+ Represents <ui:calendar> - Calendar date picker.
1756
+
1757
+ Displays a calendar for date selection.
1758
+
1759
+ Attributes:
1760
+ mode: Selection mode (single, range, multiple)
1761
+ value: Selected date(s) in ISO format
1762
+ min_date: Minimum selectable date
1763
+ max_date: Maximum selectable date
1764
+ disabled_dates: List of disabled dates
1765
+ show_week_numbers: Show week numbers
1766
+ first_day_of_week: 0=Sunday, 1=Monday, etc.
1767
+ locale: Locale for month/day names
1768
+
1769
+ Example:
1770
+ <ui:calendar mode="single" bind="selectedDate" min-date="2024-01-01" />
1771
+ <ui:calendar mode="range" bind="dateRange" />
1772
+ """
1773
+
1774
+ VALID_MODES = {'single', 'range', 'multiple'}
1775
+
1776
+ def __init__(self):
1777
+ self.mode: str = "single" # single, range, multiple
1778
+ self.value: Optional[str] = None # ISO date or comma-separated
1779
+ self.bind: Optional[str] = None # Binding for selected date(s)
1780
+ self.min_date: Optional[str] = None # Min date (ISO)
1781
+ self.max_date: Optional[str] = None # Max date (ISO)
1782
+ self.disabled_dates: Optional[str] = None # Comma-separated ISO dates
1783
+ self.disabled_days: Optional[str] = None # Days of week (0-6) to disable
1784
+ self.show_week_numbers: bool = False
1785
+ self.first_day_of_week: int = 0 # 0=Sunday, 1=Monday
1786
+ self.locale: str = "en"
1787
+ self.inline: bool = True # Show inline vs popup
1788
+ self.on_change: Optional[str] = None # Callback on date change
1789
+ self.on_month_change: Optional[str] = None # Callback on month navigation
1790
+ self._init_layout_attrs()
1791
+
1792
+ def to_dict(self) -> Dict[str, Any]:
1793
+ d = {"type": "ui_calendar", "mode": self.mode,
1794
+ "value": self.value, "bind": self.bind,
1795
+ "min_date": self.min_date, "max_date": self.max_date,
1796
+ "disabled_dates": self.disabled_dates,
1797
+ "disabled_days": self.disabled_days,
1798
+ "show_week_numbers": self.show_week_numbers,
1799
+ "first_day_of_week": self.first_day_of_week,
1800
+ "locale": self.locale, "inline": self.inline,
1801
+ "on_change": self.on_change, "on_month_change": self.on_month_change}
1802
+ d.update(self._layout_to_dict())
1803
+ return d
1804
+
1805
+ def validate(self) -> List[str]:
1806
+ errors = self._validate_layout()
1807
+ if self.mode not in self.VALID_MODES:
1808
+ errors.append(f"Invalid calendar mode: {self.mode}. Valid: {', '.join(self.VALID_MODES)}")
1809
+ if self.first_day_of_week not in range(7):
1810
+ errors.append(f"first_day_of_week must be 0-6, got {self.first_day_of_week}")
1811
+ return errors
1812
+
1813
+
1814
+ class UIDatePickerNode(QuantumNode, UILayoutMixin):
1815
+ """
1816
+ Represents <ui:date-picker> - Date input with calendar popup.
1817
+
1818
+ Combines a text input with a calendar dropdown.
1819
+
1820
+ Attributes:
1821
+ value: Selected date in ISO format
1822
+ placeholder: Input placeholder text
1823
+ format: Display format (e.g., "MM/DD/YYYY")
1824
+ clearable: Show clear button
1825
+
1826
+ Example:
1827
+ <ui:date-picker bind="birthDate" placeholder="Select date" format="MM/DD/YYYY" />
1828
+ """
1829
+
1830
+ def __init__(self):
1831
+ self.value: Optional[str] = None
1832
+ self.bind: Optional[str] = None
1833
+ self.placeholder: str = "Select date"
1834
+ self.format: str = "YYYY-MM-DD" # Display format
1835
+ self.min_date: Optional[str] = None
1836
+ self.max_date: Optional[str] = None
1837
+ self.clearable: bool = True
1838
+ self.disabled: bool = False
1839
+ self.required: bool = False
1840
+ self.on_change: Optional[str] = None
1841
+ self._init_layout_attrs()
1842
+
1843
+ def to_dict(self) -> Dict[str, Any]:
1844
+ d = {"type": "ui_date_picker", "value": self.value,
1845
+ "bind": self.bind, "placeholder": self.placeholder,
1846
+ "format": self.format, "min_date": self.min_date,
1847
+ "max_date": self.max_date, "clearable": self.clearable,
1848
+ "disabled": self.disabled, "required": self.required,
1849
+ "on_change": self.on_change}
1850
+ d.update(self._layout_to_dict())
1851
+ return d
1852
+
1853
+ def validate(self) -> List[str]:
1854
+ return self._validate_layout()