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