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,1832 @@
1
+ """
2
+ UI Engine - React Native Mobile Adapter
3
+
4
+ Transforms UI AST nodes into a standalone React Native mobile application.
5
+ Generates App.js file with:
6
+ - Mapped UI components to React Native equivalents
7
+ - State management with useState hooks
8
+ - Event handlers from q:function nodes
9
+ - StyleSheet for styling
10
+
11
+ Component Mapping:
12
+ - ui:window -> SafeAreaView + View
13
+ - ui:text -> Text
14
+ - ui:button -> TouchableOpacity
15
+ - ui:input -> TextInput
16
+ - ui:hbox -> View (flexDirection: 'row')
17
+ - ui:vbox -> View (flexDirection: 'column')
18
+ - ui:image -> Image
19
+ - ui:list -> FlatList
20
+ - etc.
21
+ """
22
+
23
+ import re
24
+ from typing import List, Optional, Set, Dict, Any
25
+
26
+ from quantum.core.ast_nodes import QuantumNode
27
+ from quantum.core.features.conditionals.src.ast_node import IfNode
28
+ from quantum.core.features.loops.src.ast_node import LoopNode
29
+ from quantum.core.features.state_management.src.ast_node import SetNode
30
+ from quantum.core.features.functions.src.ast_node import FunctionNode
31
+ from quantum.core.features.ui_engine.src.ast_nodes import (
32
+ UIWindowNode, UIHBoxNode, UIVBoxNode, UIPanelNode,
33
+ UITabPanelNode, UITabNode, UIGridNode, UIAccordionNode,
34
+ UISectionNode, UIDividedBoxNode, UIFormNode, UIFormItemNode,
35
+ UISpacerNode, UIScrollBoxNode,
36
+ UITextNode, UIButtonNode, UIInputNode, UICheckboxNode,
37
+ UIRadioNode, UISwitchNode, UISelectNode, UITableNode,
38
+ UIColumnNode, UIListNode, UIItemNode, UIImageNode,
39
+ UILinkNode, UIProgressNode, UITreeNode, UIMenuNode,
40
+ UIOptionNode, UILogNode, UIMarkdownNode, UIHeaderNode,
41
+ UIFooterNode, UIRuleNode, UILoadingNode, UIBadgeNode,
42
+ UICardNode, UICardHeaderNode, UICardBodyNode, UICardFooterNode,
43
+ UIModalNode, UIAlertNode, UIAvatarNode, UIDropdownNode,
44
+ UIToastNode, UIToastContainerNode, UICarouselNode, UISlideNode,
45
+ UIStepperNode, UIStepNode, UICalendarNode, UIDatePickerNode,
46
+ UIAnimateNode, UIChartNode, UITooltipNode, UIBreadcrumbNode,
47
+ UIBreadcrumbItemNode, UIPaginationNode, UISkeletonNode,
48
+ )
49
+ from quantum.runtime.ui_mobile_templates import (
50
+ JsBuilder,
51
+ REACT_NATIVE_APP_TEMPLATE,
52
+ generate_imports,
53
+ get_base_styles,
54
+ rn_spacing,
55
+ rn_size,
56
+ rn_color,
57
+ rn_font_size,
58
+ rn_align,
59
+ rn_justify,
60
+ js_string,
61
+ js_number,
62
+ )
63
+
64
+
65
+ class UIReactNativeAdapter:
66
+ """Generates React Native app from UI AST nodes."""
67
+
68
+ def __init__(self):
69
+ self._widget_counter = 0
70
+ self._components_used: Set[str] = set()
71
+ self._styles_used: Set[str] = set()
72
+ self._dynamic_styles: List[str] = []
73
+ self._state_vars: Dict[str, Any] = {}
74
+ self._functions: Dict[str, FunctionNode] = {}
75
+ self._features_used: Set[str] = set()
76
+
77
+ def _next_id(self, prefix: str = 'q') -> str:
78
+ """Generate unique widget ID."""
79
+ self._widget_counter += 1
80
+ return f"{prefix}_{self._widget_counter}"
81
+
82
+ def generate(
83
+ self,
84
+ windows: List[QuantumNode],
85
+ ui_children: List[QuantumNode],
86
+ title: str = "Quantum UI",
87
+ functions: Optional[Dict[str, FunctionNode]] = None,
88
+ state_vars: Optional[Dict[str, Any]] = None,
89
+ ) -> str:
90
+ """Generate complete React Native app from UI AST.
91
+
92
+ Args:
93
+ windows: List of UIWindowNode elements.
94
+ ui_children: List of top-level UI nodes (outside windows).
95
+ title: App title (used in comments).
96
+ functions: Dict of function name -> FunctionNode from q:function.
97
+ state_vars: Dict of variable name -> initial value from q:set.
98
+
99
+ Returns:
100
+ Generated JavaScript/React Native source code.
101
+ """
102
+ # Reset state
103
+ self._widget_counter = 0
104
+ self._components_used = set()
105
+ self._styles_used = set()
106
+ self._dynamic_styles = []
107
+ self._state_vars = state_vars or {}
108
+ self._functions = functions or {}
109
+ self._features_used = set()
110
+
111
+ # Build JSX content
112
+ jsx_builder = JsBuilder()
113
+ jsx_builder._indent = 3 # Inside SafeAreaView
114
+
115
+ # Render windows
116
+ for window in windows:
117
+ self._render_node(window, jsx_builder)
118
+
119
+ # Render top-level children
120
+ for child in ui_children:
121
+ self._render_node(child, jsx_builder)
122
+
123
+ jsx_content = jsx_builder.build()
124
+ if not jsx_content.strip():
125
+ jsx_content = ' <View style={styles.container}>\n <Text>Empty UI</Text>\n </View>'
126
+
127
+ # Generate state declarations
128
+ state_declarations = self._generate_state_declarations()
129
+
130
+ # Generate function declarations
131
+ function_declarations = self._generate_functions()
132
+
133
+ # Generate imports
134
+ imports = generate_imports(self._components_used)
135
+
136
+ # Generate styles
137
+ styles_content = get_base_styles(self._styles_used)
138
+ if self._dynamic_styles:
139
+ if styles_content:
140
+ styles_content += '\n'
141
+ styles_content += '\n'.join(self._dynamic_styles)
142
+
143
+ # Build final code
144
+ return REACT_NATIVE_APP_TEMPLATE.format(
145
+ imports=imports,
146
+ state_declarations=state_declarations,
147
+ function_declarations=function_declarations,
148
+ jsx_content=jsx_content,
149
+ styles_content=styles_content,
150
+ )
151
+
152
+ def get_features_used(self) -> Set[str]:
153
+ """Return set of features used during generation."""
154
+ return self._features_used.copy()
155
+
156
+ # ------------------------------------------------------------------
157
+ # State and Functions Generation
158
+ # ------------------------------------------------------------------
159
+
160
+ def _generate_state_declarations(self) -> str:
161
+ """Generate useState declarations for state variables."""
162
+ if not self._state_vars:
163
+ return '// No state variables'
164
+
165
+ lines = []
166
+ for name, value in self._state_vars.items():
167
+ js_value = self._convert_value(value)
168
+ # Capitalize first letter for setter
169
+ setter_name = f"set{name[0].upper()}{name[1:]}"
170
+ lines.append(f"const [{name}, {setter_name}] = useState({js_value});")
171
+
172
+ return '\n'.join(lines)
173
+
174
+ def _generate_functions(self) -> str:
175
+ """Generate function declarations from q:function nodes."""
176
+ if not self._functions:
177
+ return '// No function handlers'
178
+
179
+ functions = []
180
+ for func_name, func_node in self._functions.items():
181
+ func_code = self._generate_function(func_name, func_node)
182
+ functions.append(func_code)
183
+
184
+ return '\n\n'.join(functions)
185
+
186
+ def _generate_function(self, func_name: str, func_node: FunctionNode) -> str:
187
+ """Generate a JavaScript function from q:function node."""
188
+ lines = [f"const {func_name} = useCallback((args) => {{"]
189
+
190
+ # Generate body from function statements
191
+ if func_node.body:
192
+ for stmt in func_node.body:
193
+ stmt_code = self._generate_statement(stmt)
194
+ if stmt_code:
195
+ lines.append(f" {stmt_code}")
196
+ else:
197
+ lines.append(" // Empty function")
198
+
199
+ # Close with dependencies
200
+ deps = self._get_state_deps(func_node)
201
+ deps_str = ', '.join(deps) if deps else ''
202
+ lines.append(f"}}, [{deps_str}]);")
203
+
204
+ return '\n'.join(lines)
205
+
206
+ def _generate_statement(self, stmt: QuantumNode) -> str:
207
+ """Generate JavaScript code from a statement node."""
208
+ if isinstance(stmt, SetNode):
209
+ value_expr = self._convert_expression(stmt.value)
210
+ setter_name = f"set{stmt.name[0].upper()}{stmt.name[1:]}"
211
+ return f"{setter_name}({value_expr});"
212
+
213
+ return f"// Unsupported statement: {type(stmt).__name__}"
214
+
215
+ def _get_state_deps(self, func_node: FunctionNode) -> List[str]:
216
+ """Get state variables used in a function for useCallback deps."""
217
+ deps = set()
218
+ for stmt in func_node.body or []:
219
+ if isinstance(stmt, SetNode):
220
+ # Check if value references other state vars
221
+ if stmt.value:
222
+ for var_name in self._state_vars.keys():
223
+ if var_name in str(stmt.value):
224
+ deps.add(var_name)
225
+ return sorted(deps)
226
+
227
+ def _convert_expression(self, expr: str) -> str:
228
+ """Convert a Quantum expression to JavaScript."""
229
+ if expr is None:
230
+ return 'null'
231
+
232
+ expr = expr.strip()
233
+ if expr.startswith('{') and expr.endswith('}'):
234
+ expr = expr[1:-1].strip()
235
+
236
+ # Replace variable references
237
+ for var_name in self._state_vars.keys():
238
+ expr = re.sub(rf'\b{var_name}\b', var_name, expr)
239
+
240
+ return expr
241
+
242
+ def _convert_value(self, value: Any) -> str:
243
+ """Convert a value to JavaScript literal."""
244
+ if value is None:
245
+ return 'null'
246
+
247
+ if isinstance(value, str):
248
+ # Already a JS expression (starts/ends with quotes)
249
+ if (value.startswith("'") and value.endswith("'")) or \
250
+ (value.startswith('"') and value.endswith('"')):
251
+ return value
252
+
253
+ if value.isdigit():
254
+ return value
255
+ try:
256
+ float(value)
257
+ return value
258
+ except ValueError:
259
+ pass
260
+
261
+ if value.lower() == 'true':
262
+ return 'true'
263
+ if value.lower() == 'false':
264
+ return 'false'
265
+
266
+ return js_string(value)
267
+
268
+ return str(value)
269
+
270
+ # ------------------------------------------------------------------
271
+ # Style Helpers
272
+ # ------------------------------------------------------------------
273
+
274
+ def _build_style_props(self, node, base_style: str = None) -> Dict[str, str]:
275
+ """Build style properties for a node."""
276
+ styles = []
277
+ if base_style:
278
+ styles.append(f"styles.{base_style}")
279
+
280
+ # Check for dynamic style from layout attributes
281
+ dynamic_id = self._generate_layout_style(node)
282
+ if dynamic_id:
283
+ styles.append(f"styles.{dynamic_id}")
284
+
285
+ if not styles:
286
+ return {}
287
+
288
+ if len(styles) == 1:
289
+ return {'style': styles[0]}
290
+ else:
291
+ return {'style': f"[{', '.join(styles)}]"}
292
+
293
+ def _generate_layout_style(self, node) -> Optional[str]:
294
+ """Generate dynamic style for layout attributes.
295
+
296
+ Returns style name if generated, None otherwise.
297
+ """
298
+ rules = []
299
+
300
+ if hasattr(node, 'gap') and node.gap:
301
+ rules.append(f" gap: {rn_spacing(node.gap)},")
302
+ self._features_used.add('gap')
303
+
304
+ if hasattr(node, 'padding') and node.padding:
305
+ rules.append(f" padding: {rn_spacing(node.padding)},")
306
+
307
+ if hasattr(node, 'margin') and node.margin:
308
+ rules.append(f" margin: {rn_spacing(node.margin)},")
309
+
310
+ if hasattr(node, 'align') and node.align:
311
+ rules.append(f" alignItems: {rn_align(node.align)},")
312
+
313
+ if hasattr(node, 'justify') and node.justify:
314
+ rules.append(f" justifyContent: {rn_justify(node.justify)},")
315
+
316
+ if hasattr(node, 'width') and node.width:
317
+ val = rn_size(node.width)
318
+ if val != 'undefined':
319
+ rules.append(f" width: {val},")
320
+
321
+ if hasattr(node, 'height') and node.height:
322
+ val = rn_size(node.height)
323
+ if val != 'undefined':
324
+ rules.append(f" height: {val},")
325
+
326
+ if hasattr(node, 'background') and node.background:
327
+ rules.append(f" backgroundColor: {rn_color(node.background)},")
328
+
329
+ if hasattr(node, 'color') and node.color:
330
+ rules.append(f" color: {rn_color(node.color)},")
331
+
332
+ if hasattr(node, 'border') and node.border:
333
+ rules.append(" borderWidth: 1,")
334
+ rules.append(f" borderColor: {rn_color('border')},")
335
+
336
+ if hasattr(node, 'visible') and node.visible == 'false':
337
+ rules.append(" display: 'none',")
338
+
339
+ if rules:
340
+ style_id = self._next_id('dyn')
341
+ style_def = f" {style_id}: {{\n" + '\n'.join(rules) + "\n },"
342
+ self._dynamic_styles.append(style_def)
343
+ return style_id
344
+
345
+ return None
346
+
347
+ # ------------------------------------------------------------------
348
+ # Node Rendering Dispatch
349
+ # ------------------------------------------------------------------
350
+
351
+ def _render_node(self, node: QuantumNode, js: JsBuilder):
352
+ """Dispatch node to its render method."""
353
+ # Containers
354
+ if isinstance(node, UIWindowNode):
355
+ self._render_window(node, js)
356
+ elif isinstance(node, UIHBoxNode):
357
+ self._render_hbox(node, js)
358
+ elif isinstance(node, UIVBoxNode):
359
+ self._render_vbox(node, js)
360
+ elif isinstance(node, UIPanelNode):
361
+ self._render_panel(node, js)
362
+ elif isinstance(node, UITabPanelNode):
363
+ self._render_tabpanel(node, js)
364
+ elif isinstance(node, UITabNode):
365
+ self._render_tab(node, js)
366
+ elif isinstance(node, UIGridNode):
367
+ self._render_grid(node, js)
368
+ elif isinstance(node, UIAccordionNode):
369
+ self._render_accordion(node, js)
370
+ elif isinstance(node, UISectionNode):
371
+ self._render_section(node, js)
372
+ elif isinstance(node, UIScrollBoxNode):
373
+ self._render_scrollbox(node, js)
374
+ elif isinstance(node, UIFormNode):
375
+ self._render_form(node, js)
376
+ elif isinstance(node, UIFormItemNode):
377
+ self._render_formitem(node, js)
378
+ elif isinstance(node, UISpacerNode):
379
+ self._render_spacer(node, js)
380
+ elif isinstance(node, UIDividedBoxNode):
381
+ self._render_dividedbox(node, js)
382
+ # Widgets
383
+ elif isinstance(node, UITextNode):
384
+ self._render_text(node, js)
385
+ elif isinstance(node, UIButtonNode):
386
+ self._render_button(node, js)
387
+ elif isinstance(node, UIInputNode):
388
+ self._render_input(node, js)
389
+ elif isinstance(node, UICheckboxNode):
390
+ self._render_checkbox(node, js)
391
+ elif isinstance(node, UIRadioNode):
392
+ self._render_radio(node, js)
393
+ elif isinstance(node, UISwitchNode):
394
+ self._render_switch(node, js)
395
+ elif isinstance(node, UISelectNode):
396
+ self._render_select(node, js)
397
+ elif isinstance(node, UITableNode):
398
+ self._render_table(node, js)
399
+ elif isinstance(node, UIListNode):
400
+ self._render_list(node, js)
401
+ elif isinstance(node, UIItemNode):
402
+ self._render_item(node, js)
403
+ elif isinstance(node, UIImageNode):
404
+ self._render_image(node, js)
405
+ elif isinstance(node, UILinkNode):
406
+ self._render_link(node, js)
407
+ elif isinstance(node, UIProgressNode):
408
+ self._render_progress(node, js)
409
+ elif isinstance(node, UITreeNode):
410
+ self._render_tree(node, js)
411
+ elif isinstance(node, UIMenuNode):
412
+ self._render_menu(node, js)
413
+ elif isinstance(node, UIOptionNode):
414
+ self._render_option(node, js)
415
+ elif isinstance(node, UILogNode):
416
+ self._render_log(node, js)
417
+ elif isinstance(node, UIMarkdownNode):
418
+ self._render_markdown(node, js)
419
+ elif isinstance(node, UIHeaderNode):
420
+ self._render_header(node, js)
421
+ elif isinstance(node, UIFooterNode):
422
+ self._render_footer(node, js)
423
+ elif isinstance(node, UIRuleNode):
424
+ self._render_rule(node, js)
425
+ elif isinstance(node, UILoadingNode):
426
+ self._render_loading(node, js)
427
+ elif isinstance(node, UIBadgeNode):
428
+ self._render_badge(node, js)
429
+ # Component Library nodes
430
+ elif isinstance(node, UICardNode):
431
+ self._render_card(node, js)
432
+ elif isinstance(node, UICardHeaderNode):
433
+ self._render_card_header(node, js)
434
+ elif isinstance(node, UICardBodyNode):
435
+ self._render_card_body(node, js)
436
+ elif isinstance(node, UICardFooterNode):
437
+ self._render_card_footer(node, js)
438
+ elif isinstance(node, UIModalNode):
439
+ self._render_modal(node, js)
440
+ elif isinstance(node, UIAlertNode):
441
+ self._render_alert(node, js)
442
+ elif isinstance(node, UIAvatarNode):
443
+ self._render_avatar(node, js)
444
+ elif isinstance(node, UIDropdownNode):
445
+ self._render_dropdown(node, js)
446
+ elif isinstance(node, UIToastNode):
447
+ self._render_toast(node, js)
448
+ elif isinstance(node, UIToastContainerNode):
449
+ self._render_toast_container(node, js)
450
+ elif isinstance(node, UICarouselNode):
451
+ self._render_carousel(node, js)
452
+ elif isinstance(node, UISlideNode):
453
+ self._render_slide(node, js)
454
+ elif isinstance(node, UIStepperNode):
455
+ self._render_stepper(node, js)
456
+ elif isinstance(node, UIStepNode):
457
+ self._render_step(node, js)
458
+ elif isinstance(node, UICalendarNode):
459
+ self._render_calendar(node, js)
460
+ elif isinstance(node, UIDatePickerNode):
461
+ self._render_date_picker(node, js)
462
+ elif isinstance(node, UIAnimateNode):
463
+ self._render_animate(node, js)
464
+ elif isinstance(node, UIChartNode):
465
+ self._render_chart(node, js)
466
+ elif isinstance(node, UITooltipNode):
467
+ self._render_tooltip(node, js)
468
+ elif isinstance(node, UIBreadcrumbNode):
469
+ self._render_breadcrumb(node, js)
470
+ elif isinstance(node, UIBreadcrumbItemNode):
471
+ self._render_breadcrumb_item(node, js)
472
+ elif isinstance(node, UIPaginationNode):
473
+ self._render_pagination(node, js)
474
+ elif isinstance(node, UISkeletonNode):
475
+ self._render_skeleton(node, js)
476
+ # Quantum passthrough
477
+ elif isinstance(node, SetNode):
478
+ js.comment(f'q:set {node.name} = {node.value}')
479
+ elif isinstance(node, LoopNode):
480
+ js.comment(f'q:loop {node.var_name}')
481
+ elif isinstance(node, IfNode):
482
+ js.comment(f'q:if {node.condition}')
483
+
484
+ def _render_children(self, children: list, js: JsBuilder):
485
+ for child in children:
486
+ self._render_node(child, js)
487
+
488
+ # ------------------------------------------------------------------
489
+ # Container Renders
490
+ # ------------------------------------------------------------------
491
+
492
+ def _render_window(self, node: UIWindowNode, js: JsBuilder):
493
+ self._components_used.add('View')
494
+ self._styles_used.add('container')
495
+
496
+ style_props = self._build_style_props(node, 'container')
497
+ js.jsx_open('View', style_props)
498
+ js.indent()
499
+ self._render_children(node.children, js)
500
+ js.dedent()
501
+ js.jsx_close('View')
502
+
503
+ def _render_hbox(self, node: UIHBoxNode, js: JsBuilder):
504
+ self._components_used.add('View')
505
+ self._styles_used.add('hbox')
506
+
507
+ style_props = self._build_style_props(node, 'hbox')
508
+ js.jsx_open('View', style_props)
509
+ js.indent()
510
+ self._render_children(node.children, js)
511
+ js.dedent()
512
+ js.jsx_close('View')
513
+
514
+ def _render_vbox(self, node: UIVBoxNode, js: JsBuilder):
515
+ self._components_used.add('View')
516
+ self._styles_used.add('vbox')
517
+
518
+ style_props = self._build_style_props(node, 'vbox')
519
+ js.jsx_open('View', style_props)
520
+ js.indent()
521
+ self._render_children(node.children, js)
522
+ js.dedent()
523
+ js.jsx_close('View')
524
+
525
+ def _render_panel(self, node: UIPanelNode, js: JsBuilder):
526
+ self._components_used.add('View')
527
+ self._components_used.add('Text')
528
+ self._styles_used.add('panel')
529
+
530
+ style_props = self._build_style_props(node, 'panel')
531
+ js.jsx_open('View', style_props)
532
+ js.indent()
533
+ if node.title:
534
+ js.jsx_open('Text', {'style': 'styles.panelTitle'})
535
+ js.indent()
536
+ js.jsx_text(node.title)
537
+ js.dedent()
538
+ js.jsx_close('Text')
539
+ self._render_children(node.children, js)
540
+ js.dedent()
541
+ js.jsx_close('View')
542
+
543
+ def _render_tabpanel(self, node: UITabPanelNode, js: JsBuilder):
544
+ self._components_used.add('View')
545
+ self._components_used.add('Text')
546
+ self._components_used.add('TouchableOpacity')
547
+ self._styles_used.add('tab')
548
+
549
+ # Generate tab state
550
+ tab_state_id = self._next_id('activeTab')
551
+
552
+ js.jsx_open('View')
553
+ js.indent()
554
+
555
+ # Tab headers
556
+ js.comment('Tab headers')
557
+ js.jsx_open('View', {'style': 'styles.tabHeader'})
558
+ js.indent()
559
+
560
+ tabs = [c for c in node.children if isinstance(c, UITabNode)]
561
+ for i, tab in enumerate(tabs):
562
+ is_active = '{' + f'{tab_state_id} === {i}' + '}'
563
+ on_press = '{() => set' + f'{tab_state_id[0].upper()}{tab_state_id[1:]}({i})' + '}'
564
+
565
+ js.jsx_open('TouchableOpacity', {
566
+ 'style': f'[styles.tabButton, {tab_state_id} === {i} && styles.tabButtonActive]',
567
+ 'onPress': on_press,
568
+ })
569
+ js.indent()
570
+ js.jsx_open('Text', {
571
+ 'style': f'[styles.tabButtonText, {tab_state_id} === {i} && styles.tabButtonTextActive]'
572
+ })
573
+ js.indent()
574
+ js.jsx_text(tab.title)
575
+ js.dedent()
576
+ js.jsx_close('Text')
577
+ js.dedent()
578
+ js.jsx_close('TouchableOpacity')
579
+
580
+ js.dedent()
581
+ js.jsx_close('View')
582
+
583
+ # Tab contents
584
+ js.comment('Tab contents')
585
+ for i, tab in enumerate(tabs):
586
+ js.line(f'{{{tab_state_id} === {i} && (')
587
+ js.indent()
588
+ js.jsx_open('View', {'style': 'styles.tabContent'})
589
+ js.indent()
590
+ self._render_children(tab.children, js)
591
+ js.dedent()
592
+ js.jsx_close('View')
593
+ js.dedent()
594
+ js.line(')}')
595
+
596
+ js.dedent()
597
+ js.jsx_close('View')
598
+
599
+ # Add tab state to state vars
600
+ self._state_vars[tab_state_id] = '0'
601
+
602
+ def _render_tab(self, node: UITabNode, js: JsBuilder):
603
+ # Standalone tab renders as View
604
+ self._components_used.add('View')
605
+ style_props = self._build_style_props(node)
606
+ js.jsx_open('View', style_props if style_props else None)
607
+ js.indent()
608
+ self._render_children(node.children, js)
609
+ js.dedent()
610
+ js.jsx_close('View')
611
+
612
+ def _render_grid(self, node: UIGridNode, js: JsBuilder):
613
+ self._components_used.add('View')
614
+
615
+ # Grid layout using flexWrap
616
+ grid_id = self._next_id('grid')
617
+ cols = node.columns or '2'
618
+ try:
619
+ n = int(cols)
620
+ item_width = f"'{100 / n:.2f}%'"
621
+ except ValueError:
622
+ item_width = "'50%'"
623
+
624
+ self._dynamic_styles.append(f""" {grid_id}: {{
625
+ flexDirection: 'row',
626
+ flexWrap: 'wrap',
627
+ }},
628
+ {grid_id}Item: {{
629
+ width: {item_width},
630
+ }},""")
631
+
632
+ js.jsx_open('View', {'style': f'styles.{grid_id}'})
633
+ js.indent()
634
+ self._render_children(node.children, js)
635
+ js.dedent()
636
+ js.jsx_close('View')
637
+
638
+ def _render_accordion(self, node: UIAccordionNode, js: JsBuilder):
639
+ self._components_used.add('View')
640
+ self._styles_used.add('accordion')
641
+
642
+ js.jsx_open('View')
643
+ js.indent()
644
+ self._render_children(node.children, js)
645
+ js.dedent()
646
+ js.jsx_close('View')
647
+
648
+ def _render_section(self, node: UISectionNode, js: JsBuilder):
649
+ self._components_used.add('View')
650
+ self._components_used.add('Text')
651
+ self._components_used.add('TouchableOpacity')
652
+ self._styles_used.add('accordion')
653
+
654
+ section_id = self._next_id('expanded')
655
+ initial = 'true' if node.expanded else 'false'
656
+ self._state_vars[section_id] = initial
657
+
658
+ js.jsx_open('View', {'style': 'styles.section'})
659
+ js.indent()
660
+
661
+ # Header (toggle)
662
+ js.jsx_open('TouchableOpacity', {
663
+ 'style': 'styles.sectionHeader',
664
+ 'onPress': '{() => set' + f'{section_id[0].upper()}{section_id[1:]}(!{section_id})' + '}',
665
+ })
666
+ js.indent()
667
+ js.jsx_open('Text', {'style': 'styles.sectionHeaderText'})
668
+ js.indent()
669
+ js.jsx_text(node.title)
670
+ js.dedent()
671
+ js.jsx_close('Text')
672
+ js.dedent()
673
+ js.jsx_close('TouchableOpacity')
674
+
675
+ # Content (conditional)
676
+ js.line(f'{{{section_id} && (')
677
+ js.indent()
678
+ js.jsx_open('View', {'style': 'styles.sectionContent'})
679
+ js.indent()
680
+ self._render_children(node.children, js)
681
+ js.dedent()
682
+ js.jsx_close('View')
683
+ js.dedent()
684
+ js.line(')}')
685
+
686
+ js.dedent()
687
+ js.jsx_close('View')
688
+
689
+ def _render_scrollbox(self, node: UIScrollBoxNode, js: JsBuilder):
690
+ self._components_used.add('ScrollView')
691
+ self._styles_used.add('scrollbox')
692
+
693
+ style_props = self._build_style_props(node, 'scrollbox')
694
+ js.jsx_open('ScrollView', style_props)
695
+ js.indent()
696
+ self._render_children(node.children, js)
697
+ js.dedent()
698
+ js.jsx_close('ScrollView')
699
+
700
+ def _render_form(self, node: UIFormNode, js: JsBuilder):
701
+ self._components_used.add('View')
702
+ self._styles_used.add('form')
703
+
704
+ attrs = {'style': 'styles.form'}
705
+ js.jsx_open('View', attrs)
706
+ js.indent()
707
+ self._render_children(node.children, js)
708
+
709
+ # If there's an on_submit, the button should call it
710
+ if node.on_submit:
711
+ self._components_used.add('TouchableOpacity')
712
+ self._components_used.add('Text')
713
+ self._styles_used.add('button')
714
+ js.jsx_open('TouchableOpacity', {
715
+ 'style': 'styles.button',
716
+ 'onPress': '{' + node.on_submit + '}',
717
+ })
718
+ js.indent()
719
+ js.jsx_open('Text', {'style': 'styles.buttonText'})
720
+ js.indent()
721
+ js.jsx_text('Submit')
722
+ js.dedent()
723
+ js.jsx_close('Text')
724
+ js.dedent()
725
+ js.jsx_close('TouchableOpacity')
726
+
727
+ js.dedent()
728
+ js.jsx_close('View')
729
+
730
+ def _render_formitem(self, node: UIFormItemNode, js: JsBuilder):
731
+ self._components_used.add('View')
732
+ self._components_used.add('Text')
733
+ self._styles_used.add('form')
734
+
735
+ js.jsx_open('View', {'style': 'styles.formItem'})
736
+ js.indent()
737
+ if node.label:
738
+ js.jsx_open('Text', {'style': 'styles.formLabel'})
739
+ js.indent()
740
+ js.jsx_text(node.label)
741
+ js.dedent()
742
+ js.jsx_close('Text')
743
+ self._render_children(node.children, js)
744
+ js.dedent()
745
+ js.jsx_close('View')
746
+
747
+ def _render_spacer(self, node: UISpacerNode, js: JsBuilder):
748
+ self._components_used.add('View')
749
+ self._styles_used.add('spacer')
750
+
751
+ if node.size:
752
+ spacer_id = self._next_id('spacer')
753
+ size_val = rn_spacing(node.size)
754
+ self._dynamic_styles.append(f""" {spacer_id}: {{
755
+ height: {size_val},
756
+ width: {size_val},
757
+ }},""")
758
+ js.jsx_open('View', {'style': f'styles.{spacer_id}'}, self_closing=True)
759
+ else:
760
+ js.jsx_open('View', {'style': 'styles.spacer'}, self_closing=True)
761
+
762
+ def _render_dividedbox(self, node: UIDividedBoxNode, js: JsBuilder):
763
+ self._components_used.add('View')
764
+
765
+ direction = 'row' if node.direction == 'horizontal' else 'column'
766
+ div_id = self._next_id('divided')
767
+ self._dynamic_styles.append(f""" {div_id}: {{
768
+ flexDirection: '{direction}',
769
+ flex: 1,
770
+ }},""")
771
+
772
+ js.jsx_open('View', {'style': f'styles.{div_id}'})
773
+ js.indent()
774
+ self._render_children(node.children, js)
775
+ js.dedent()
776
+ js.jsx_close('View')
777
+
778
+ # ------------------------------------------------------------------
779
+ # Widget Renders
780
+ # ------------------------------------------------------------------
781
+
782
+ def _render_text(self, node: UITextNode, js: JsBuilder):
783
+ self._components_used.add('Text')
784
+ self._styles_used.add('text')
785
+
786
+ style_parts = ['styles.text']
787
+ if node.weight == 'bold':
788
+ style_parts.append('styles.textBold')
789
+
790
+ # Dynamic font size
791
+ if node.size:
792
+ size_id = self._next_id('textSize')
793
+ font_size = rn_font_size(node.size)
794
+ self._dynamic_styles.append(f" {size_id}: {{ fontSize: {font_size} }},")
795
+ style_parts.append(f'styles.{size_id}')
796
+
797
+ # Layout styles
798
+ layout_id = self._generate_layout_style(node)
799
+ if layout_id:
800
+ style_parts.append(f'styles.{layout_id}')
801
+
802
+ style_val = style_parts[0] if len(style_parts) == 1 else f'[{", ".join(style_parts)}]'
803
+ js.jsx_open('Text', {'style': style_val})
804
+ js.indent()
805
+
806
+ # Check for data binding expressions
807
+ content = node.content
808
+ if '{' in content and '}' in content:
809
+ # Convert {var} to {var} (JSX expression)
810
+ js.jsx_text(content)
811
+ else:
812
+ js.jsx_text(content)
813
+
814
+ js.dedent()
815
+ js.jsx_close('Text')
816
+
817
+ def _render_button(self, node: UIButtonNode, js: JsBuilder):
818
+ self._components_used.add('TouchableOpacity')
819
+ self._components_used.add('Text')
820
+ self._styles_used.add('button')
821
+
822
+ style_parts = ['styles.button']
823
+ text_style = 'styles.buttonText'
824
+
825
+ if node.variant:
826
+ variant_map = {
827
+ 'secondary': 'buttonSecondary',
828
+ 'danger': 'buttonDanger',
829
+ 'success': 'buttonSuccess',
830
+ }
831
+ if node.variant in variant_map:
832
+ style_parts.append(f'styles.{variant_map[node.variant]}')
833
+
834
+ if node.disabled:
835
+ style_parts.append('styles.buttonDisabled')
836
+
837
+ style_val = style_parts[0] if len(style_parts) == 1 else f'[{", ".join(style_parts)}]'
838
+
839
+ attrs = {'style': style_val}
840
+ if node.on_click:
841
+ attrs['onPress'] = '{' + node.on_click + '}'
842
+ if node.disabled:
843
+ attrs['disabled'] = '{true}'
844
+
845
+ js.jsx_open('TouchableOpacity', attrs)
846
+ js.indent()
847
+ js.jsx_open('Text', {'style': text_style})
848
+ js.indent()
849
+ js.jsx_text(node.content)
850
+ js.dedent()
851
+ js.jsx_close('Text')
852
+ js.dedent()
853
+ js.jsx_close('TouchableOpacity')
854
+
855
+ def _render_input(self, node: UIInputNode, js: JsBuilder):
856
+ self._components_used.add('TextInput')
857
+ self._styles_used.add('input')
858
+
859
+ attrs = {'style': 'styles.input'}
860
+
861
+ if node.placeholder:
862
+ attrs['placeholder'] = node.placeholder
863
+
864
+ if node.bind:
865
+ attrs['value'] = '{' + node.bind + '}'
866
+ setter = f"set{node.bind[0].upper()}{node.bind[1:]}"
867
+ attrs['onChangeText'] = '{' + setter + '}'
868
+
869
+ if node.input_type == 'password':
870
+ attrs['secureTextEntry'] = '{true}'
871
+ elif node.input_type == 'email':
872
+ attrs['keyboardType'] = 'email-address'
873
+ elif node.input_type == 'number':
874
+ attrs['keyboardType'] = 'numeric'
875
+
876
+ js.jsx_open('TextInput', attrs, self_closing=True)
877
+
878
+ def _render_checkbox(self, node: UICheckboxNode, js: JsBuilder):
879
+ self._components_used.add('View')
880
+ self._components_used.add('Text')
881
+ self._components_used.add('TouchableOpacity')
882
+ self._styles_used.add('checkbox')
883
+
884
+ check_id = node.bind or self._next_id('checked')
885
+ if node.bind and node.bind not in self._state_vars:
886
+ self._state_vars[node.bind] = 'false'
887
+
888
+ js.jsx_open('TouchableOpacity', {
889
+ 'style': 'styles.checkboxRow',
890
+ 'onPress': '{() => set' + f'{check_id[0].upper()}{check_id[1:]}(!{check_id})' + '}',
891
+ })
892
+ js.indent()
893
+ js.jsx_open('View', {
894
+ 'style': f'[styles.checkbox, {check_id} && styles.checkboxChecked]'
895
+ })
896
+ js.dedent()
897
+ js.jsx_close('View')
898
+ if node.label:
899
+ js.jsx_open('Text', {'style': 'styles.checkboxLabel'})
900
+ js.indent()
901
+ js.jsx_text(node.label)
902
+ js.dedent()
903
+ js.jsx_close('Text')
904
+ js.jsx_close('TouchableOpacity')
905
+
906
+ def _render_radio(self, node: UIRadioNode, js: JsBuilder):
907
+ self._components_used.add('View')
908
+ self._components_used.add('Text')
909
+ self._components_used.add('TouchableOpacity')
910
+ self._styles_used.add('checkbox')
911
+
912
+ radio_id = node.bind or self._next_id('radio')
913
+ if node.bind and node.bind not in self._state_vars:
914
+ self._state_vars[node.bind] = "''"
915
+
916
+ js.jsx_open('View')
917
+ js.indent()
918
+ if node.options:
919
+ for opt in node.options.split(','):
920
+ opt = opt.strip()
921
+ js.jsx_open('TouchableOpacity', {
922
+ 'style': 'styles.checkboxRow',
923
+ 'onPress': '{() => set' + f'{radio_id[0].upper()}{radio_id[1:]}({js_string(opt)})' + '}',
924
+ })
925
+ js.indent()
926
+ js.jsx_open('View', {
927
+ 'style': f'[styles.checkbox, {radio_id} === {js_string(opt)} && styles.checkboxChecked]'
928
+ })
929
+ js.dedent()
930
+ js.jsx_close('View')
931
+ js.jsx_open('Text', {'style': 'styles.checkboxLabel'})
932
+ js.indent()
933
+ js.jsx_text(opt)
934
+ js.dedent()
935
+ js.jsx_close('Text')
936
+ js.jsx_close('TouchableOpacity')
937
+ js.dedent()
938
+ js.jsx_close('View')
939
+
940
+ def _render_switch(self, node: UISwitchNode, js: JsBuilder):
941
+ self._components_used.add('View')
942
+ self._components_used.add('Text')
943
+ self._components_used.add('Switch')
944
+ self._styles_used.add('switch')
945
+
946
+ switch_id = node.bind or self._next_id('switch')
947
+ if node.bind and node.bind not in self._state_vars:
948
+ self._state_vars[node.bind] = 'false'
949
+
950
+ js.jsx_open('View', {'style': 'styles.switchRow'})
951
+ js.indent()
952
+ js.jsx_open('Switch', {
953
+ 'value': '{' + switch_id + '}',
954
+ 'onValueChange': '{set' + f'{switch_id[0].upper()}{switch_id[1:]}' + '}',
955
+ }, self_closing=True)
956
+ if node.label:
957
+ js.jsx_open('Text')
958
+ js.indent()
959
+ js.jsx_text(node.label)
960
+ js.dedent()
961
+ js.jsx_close('Text')
962
+ js.dedent()
963
+ js.jsx_close('View')
964
+
965
+ def _render_select(self, node: UISelectNode, js: JsBuilder):
966
+ # React Native doesn't have a built-in Select
967
+ # Using a simple View with TouchableOpacity options
968
+ self._components_used.add('View')
969
+ self._components_used.add('Text')
970
+ self._components_used.add('TouchableOpacity')
971
+ self._styles_used.add('select')
972
+
973
+ select_id = node.bind or self._next_id('select')
974
+ if node.bind and node.bind not in self._state_vars:
975
+ self._state_vars[node.bind] = "''"
976
+
977
+ js.jsx_open('View', {'style': 'styles.select'})
978
+ js.indent()
979
+ if node.options:
980
+ for opt in node.options.split(','):
981
+ opt = opt.strip()
982
+ js.jsx_open('TouchableOpacity', {
983
+ 'onPress': '{() => set' + f'{select_id[0].upper()}{select_id[1:]}({js_string(opt)})' + '}',
984
+ })
985
+ js.indent()
986
+ js.jsx_open('Text')
987
+ js.indent()
988
+ js.jsx_text(opt)
989
+ js.dedent()
990
+ js.jsx_close('Text')
991
+ js.jsx_close('TouchableOpacity')
992
+ js.dedent()
993
+ js.jsx_close('View')
994
+
995
+ def _render_table(self, node: UITableNode, js: JsBuilder):
996
+ # Simplified table as rows
997
+ self._components_used.add('View')
998
+ self._components_used.add('Text')
999
+ self._styles_used.add('list')
1000
+
1001
+ columns = [c for c in node.children if isinstance(c, UIColumnNode)]
1002
+
1003
+ js.jsx_open('View')
1004
+ js.indent()
1005
+
1006
+ # Header row
1007
+ js.jsx_open('View', {'style': 'styles.listItem'})
1008
+ js.indent()
1009
+ for col in columns:
1010
+ js.jsx_open('Text', {'style': 'styles.textBold'})
1011
+ js.indent()
1012
+ js.jsx_text(col.label or col.key or '')
1013
+ js.dedent()
1014
+ js.jsx_close('Text')
1015
+ js.dedent()
1016
+ js.jsx_close('View')
1017
+
1018
+ # Data rows (placeholder)
1019
+ if node.source:
1020
+ js.comment(f'Data source: {node.source}')
1021
+
1022
+ js.dedent()
1023
+ js.jsx_close('View')
1024
+
1025
+ def _render_list(self, node: UIListNode, js: JsBuilder):
1026
+ self._components_used.add('FlatList')
1027
+ self._components_used.add('View')
1028
+ self._styles_used.add('list')
1029
+
1030
+ list_id = self._next_id('list')
1031
+
1032
+ if node.source:
1033
+ # Use FlatList for data source
1034
+ js.jsx_open('FlatList', {
1035
+ 'data': '{' + node.source.strip('{}') + '}',
1036
+ 'renderItem': '{({item}) => <View style={styles.listItem}><Text>{item}</Text></View>}',
1037
+ 'keyExtractor': '{(item, index) => index.toString()}',
1038
+ }, self_closing=True)
1039
+ else:
1040
+ # Static list
1041
+ js.jsx_open('View')
1042
+ js.indent()
1043
+ self._render_children(node.children, js)
1044
+ js.dedent()
1045
+ js.jsx_close('View')
1046
+
1047
+ def _render_item(self, node: UIItemNode, js: JsBuilder):
1048
+ self._components_used.add('View')
1049
+ self._styles_used.add('list')
1050
+
1051
+ js.jsx_open('View', {'style': 'styles.listItem'})
1052
+ js.indent()
1053
+ self._render_children(node.children, js)
1054
+ js.dedent()
1055
+ js.jsx_close('View')
1056
+
1057
+ def _render_image(self, node: UIImageNode, js: JsBuilder):
1058
+ self._components_used.add('Image')
1059
+ self._styles_used.add('image')
1060
+ self._features_used.add('image')
1061
+
1062
+ attrs = {'style': 'styles.image'}
1063
+
1064
+ if node.src:
1065
+ if node.src.startswith('http'):
1066
+ attrs['source'] = "{{ uri: '" + node.src + "' }}"
1067
+ else:
1068
+ attrs['source'] = '{require(' + js_string(node.src) + ')}'
1069
+
1070
+ if node.alt:
1071
+ attrs['accessibilityLabel'] = node.alt
1072
+
1073
+ js.jsx_open('Image', attrs, self_closing=True)
1074
+
1075
+ def _render_link(self, node: UILinkNode, js: JsBuilder):
1076
+ self._components_used.add('Text')
1077
+ self._components_used.add('TouchableOpacity')
1078
+ self._styles_used.add('link')
1079
+
1080
+ if node.external:
1081
+ self._features_used.add('link_external')
1082
+
1083
+ js.jsx_open('TouchableOpacity')
1084
+ js.indent()
1085
+ js.jsx_open('Text', {'style': 'styles.link'})
1086
+ js.indent()
1087
+ js.jsx_text(node.content)
1088
+ js.dedent()
1089
+ js.jsx_close('Text')
1090
+ js.dedent()
1091
+ js.jsx_close('TouchableOpacity')
1092
+
1093
+ def _render_progress(self, node: UIProgressNode, js: JsBuilder):
1094
+ self._components_used.add('View')
1095
+ self._styles_used.add('progress')
1096
+
1097
+ # Calculate percentage
1098
+ value = node.value or '0'
1099
+ max_val = node.max or '100'
1100
+ progress_id = self._next_id('prog')
1101
+
1102
+ self._dynamic_styles.append(f""" {progress_id}: {{
1103
+ width: '{value}%',
1104
+ }},""")
1105
+
1106
+ js.jsx_open('View', {'style': 'styles.progressContainer'})
1107
+ js.indent()
1108
+ js.jsx_open('View', {'style': f'[styles.progressBar, styles.{progress_id}]'}, self_closing=True)
1109
+ js.dedent()
1110
+ js.jsx_close('View')
1111
+
1112
+ def _render_tree(self, node: UITreeNode, js: JsBuilder):
1113
+ # Simple tree placeholder
1114
+ self._components_used.add('View')
1115
+ self._components_used.add('Text')
1116
+
1117
+ js.jsx_open('View')
1118
+ js.indent()
1119
+ if node.source:
1120
+ js.jsx_open('Text')
1121
+ js.indent()
1122
+ js.jsx_text(f'Tree: {node.source}')
1123
+ js.dedent()
1124
+ js.jsx_close('Text')
1125
+ js.dedent()
1126
+ js.jsx_close('View')
1127
+
1128
+ def _render_menu(self, node: UIMenuNode, js: JsBuilder):
1129
+ self._components_used.add('View')
1130
+ self._styles_used.add('menu')
1131
+
1132
+ js.jsx_open('View', {'style': 'styles.menu'})
1133
+ js.indent()
1134
+ self._render_children(node.children, js)
1135
+ js.dedent()
1136
+ js.jsx_close('View')
1137
+
1138
+ def _render_option(self, node: UIOptionNode, js: JsBuilder):
1139
+ self._components_used.add('TouchableOpacity')
1140
+ self._components_used.add('Text')
1141
+ self._styles_used.add('menu')
1142
+
1143
+ attrs = {'style': 'styles.menuItem'}
1144
+ if node.on_click:
1145
+ attrs['onPress'] = '{' + node.on_click + '}'
1146
+
1147
+ js.jsx_open('TouchableOpacity', attrs)
1148
+ js.indent()
1149
+ js.jsx_open('Text', {'style': 'styles.menuItemText'})
1150
+ js.indent()
1151
+ js.jsx_text(node.label or node.value or '')
1152
+ js.dedent()
1153
+ js.jsx_close('Text')
1154
+ js.dedent()
1155
+ js.jsx_close('TouchableOpacity')
1156
+
1157
+ def _render_log(self, node: UILogNode, js: JsBuilder):
1158
+ # Log as ScrollView with Text
1159
+ self._components_used.add('ScrollView')
1160
+ self._components_used.add('Text')
1161
+
1162
+ log_id = self._next_id('log')
1163
+ self._dynamic_styles.append(f""" {log_id}: {{
1164
+ backgroundColor: '#1e293b',
1165
+ padding: 12,
1166
+ borderRadius: 6,
1167
+ maxHeight: 200,
1168
+ }},
1169
+ {log_id}Text: {{
1170
+ fontFamily: 'monospace',
1171
+ fontSize: 12,
1172
+ color: '#e2e8f0',
1173
+ }},""")
1174
+
1175
+ js.jsx_open('ScrollView', {'style': f'styles.{log_id}'})
1176
+ js.indent()
1177
+ js.jsx_open('Text', {'style': f'styles.{log_id}Text'})
1178
+ js.indent()
1179
+ js.jsx_text('Log output...')
1180
+ js.dedent()
1181
+ js.jsx_close('Text')
1182
+ js.dedent()
1183
+ js.jsx_close('ScrollView')
1184
+
1185
+ def _render_markdown(self, node: UIMarkdownNode, js: JsBuilder):
1186
+ # Markdown as plain Text (would need react-native-markdown)
1187
+ self._components_used.add('Text')
1188
+
1189
+ js.jsx_open('Text')
1190
+ js.indent()
1191
+ js.jsx_text(node.content)
1192
+ js.dedent()
1193
+ js.jsx_close('Text')
1194
+
1195
+ def _render_header(self, node: UIHeaderNode, js: JsBuilder):
1196
+ self._components_used.add('View')
1197
+ self._components_used.add('Text')
1198
+ self._styles_used.add('header')
1199
+
1200
+ js.jsx_open('View', {'style': 'styles.header'})
1201
+ js.indent()
1202
+ js.jsx_open('Text', {'style': 'styles.headerText'})
1203
+ js.indent()
1204
+ if node.title:
1205
+ js.jsx_text(node.title)
1206
+ elif node.content:
1207
+ js.jsx_text(node.content)
1208
+ js.dedent()
1209
+ js.jsx_close('Text')
1210
+ self._render_children(node.children, js)
1211
+ js.dedent()
1212
+ js.jsx_close('View')
1213
+
1214
+ def _render_footer(self, node: UIFooterNode, js: JsBuilder):
1215
+ self._components_used.add('View')
1216
+ self._components_used.add('Text')
1217
+ self._styles_used.add('footer')
1218
+
1219
+ js.jsx_open('View', {'style': 'styles.footer'})
1220
+ js.indent()
1221
+ if node.content:
1222
+ js.jsx_open('Text', {'style': 'styles.footerText'})
1223
+ js.indent()
1224
+ js.jsx_text(node.content)
1225
+ js.dedent()
1226
+ js.jsx_close('Text')
1227
+ self._render_children(node.children, js)
1228
+ js.dedent()
1229
+ js.jsx_close('View')
1230
+
1231
+ def _render_rule(self, node: UIRuleNode, js: JsBuilder):
1232
+ self._components_used.add('View')
1233
+ self._styles_used.add('rule')
1234
+
1235
+ js.jsx_open('View', {'style': 'styles.rule'}, self_closing=True)
1236
+
1237
+ def _render_loading(self, node: UILoadingNode, js: JsBuilder):
1238
+ self._components_used.add('View')
1239
+ self._components_used.add('ActivityIndicator')
1240
+ self._components_used.add('Text')
1241
+ self._styles_used.add('loading')
1242
+
1243
+ js.jsx_open('View', {'style': 'styles.loading'})
1244
+ js.indent()
1245
+ js.jsx_open('ActivityIndicator', {'size': 'small', 'color': '#3b82f6'}, self_closing=True)
1246
+ if node.text:
1247
+ js.jsx_open('Text', {'style': 'styles.loadingText'})
1248
+ js.indent()
1249
+ js.jsx_text(node.text)
1250
+ js.dedent()
1251
+ js.jsx_close('Text')
1252
+ js.dedent()
1253
+ js.jsx_close('View')
1254
+
1255
+ def _render_badge(self, node: UIBadgeNode, js: JsBuilder):
1256
+ self._components_used.add('View')
1257
+ self._components_used.add('Text')
1258
+ self._styles_used.add('badge')
1259
+
1260
+ style_parts = ['styles.badge']
1261
+ text_style_parts = ['styles.badgeText']
1262
+
1263
+ if node.variant:
1264
+ variant_map = {
1265
+ 'primary': ('badgePrimary', 'badgePrimaryText'),
1266
+ 'success': ('badgeSuccess', 'badgePrimaryText'),
1267
+ 'danger': ('badgeDanger', 'badgePrimaryText'),
1268
+ 'warning': ('badgeWarning', 'badgePrimaryText'),
1269
+ }
1270
+ if node.variant in variant_map:
1271
+ badge_style, text_style = variant_map[node.variant]
1272
+ style_parts.append(f'styles.{badge_style}')
1273
+ text_style_parts.append(f'styles.{text_style}')
1274
+
1275
+ style_val = style_parts[0] if len(style_parts) == 1 else f'[{", ".join(style_parts)}]'
1276
+ text_style_val = text_style_parts[0] if len(text_style_parts) == 1 else f'[{", ".join(text_style_parts)}]'
1277
+
1278
+ js.jsx_open('View', {'style': style_val})
1279
+ js.indent()
1280
+ js.jsx_open('Text', {'style': text_style_val})
1281
+ js.indent()
1282
+ js.jsx_text(node.content)
1283
+ js.dedent()
1284
+ js.jsx_close('Text')
1285
+ js.dedent()
1286
+ js.jsx_close('View')
1287
+
1288
+ # ------------------------------------------------------------------
1289
+ # Component Library Renders
1290
+ # ------------------------------------------------------------------
1291
+
1292
+ def _render_card(self, node: UICardNode, js: JsBuilder):
1293
+ self._components_used.add('View')
1294
+ self._styles_used.add('card')
1295
+
1296
+ style_props = self._build_style_props(node, 'card')
1297
+ js.jsx_open('View', style_props)
1298
+ js.indent()
1299
+ self._render_children(node.children, js)
1300
+ js.dedent()
1301
+ js.jsx_close('View')
1302
+
1303
+ def _render_card_header(self, node: UICardHeaderNode, js: JsBuilder):
1304
+ self._components_used.add('View')
1305
+ self._styles_used.add('card')
1306
+
1307
+ js.jsx_open('View', {'style': 'styles.cardHeader'})
1308
+ js.indent()
1309
+ self._render_children(node.children, js)
1310
+ js.dedent()
1311
+ js.jsx_close('View')
1312
+
1313
+ def _render_card_body(self, node: UICardBodyNode, js: JsBuilder):
1314
+ self._components_used.add('View')
1315
+ self._styles_used.add('card')
1316
+
1317
+ js.jsx_open('View', {'style': 'styles.cardBody'})
1318
+ js.indent()
1319
+ self._render_children(node.children, js)
1320
+ js.dedent()
1321
+ js.jsx_close('View')
1322
+
1323
+ def _render_card_footer(self, node: UICardFooterNode, js: JsBuilder):
1324
+ self._components_used.add('View')
1325
+ self._styles_used.add('card')
1326
+
1327
+ js.jsx_open('View', {'style': 'styles.cardFooter'})
1328
+ js.indent()
1329
+ self._render_children(node.children, js)
1330
+ js.dedent()
1331
+ js.jsx_close('View')
1332
+
1333
+ def _render_modal(self, node: UIModalNode, js: JsBuilder):
1334
+ self._components_used.add('Modal')
1335
+ self._components_used.add('View')
1336
+ self._components_used.add('Text')
1337
+ self._components_used.add('TouchableOpacity')
1338
+ self._styles_used.add('modal')
1339
+
1340
+ modal_id = node.modal_id or self._next_id('modal')
1341
+ if modal_id not in self._state_vars:
1342
+ self._state_vars[modal_id] = 'true' if node.open else 'false'
1343
+
1344
+ js.jsx_open('Modal', {
1345
+ 'visible': '{' + modal_id + '}',
1346
+ 'transparent': '{true}',
1347
+ 'animationType': 'fade',
1348
+ })
1349
+ js.indent()
1350
+ js.jsx_open('View', {'style': 'styles.modalOverlay'})
1351
+ js.indent()
1352
+ js.jsx_open('View', {'style': 'styles.modalContent'})
1353
+ js.indent()
1354
+
1355
+ # Title
1356
+ if node.title:
1357
+ js.jsx_open('Text', {'style': 'styles.modalTitle'})
1358
+ js.indent()
1359
+ js.jsx_text(node.title)
1360
+ js.dedent()
1361
+ js.jsx_close('Text')
1362
+
1363
+ # Close button
1364
+ if node.closable:
1365
+ js.jsx_open('TouchableOpacity', {
1366
+ 'style': 'styles.modalClose',
1367
+ 'onPress': '{() => set' + f'{modal_id[0].upper()}{modal_id[1:]}(false)' + '}',
1368
+ })
1369
+ js.indent()
1370
+ js.jsx_open('Text')
1371
+ js.indent()
1372
+ js.jsx_text('✕')
1373
+ js.dedent()
1374
+ js.jsx_close('Text')
1375
+ js.dedent()
1376
+ js.jsx_close('TouchableOpacity')
1377
+
1378
+ self._render_children(node.children, js)
1379
+ js.dedent()
1380
+ js.jsx_close('View')
1381
+ js.dedent()
1382
+ js.jsx_close('View')
1383
+ js.dedent()
1384
+ js.jsx_close('Modal')
1385
+
1386
+ def _render_alert(self, node: UIAlertNode, js: JsBuilder):
1387
+ self._components_used.add('View')
1388
+ self._components_used.add('Text')
1389
+ self._styles_used.add('alert')
1390
+
1391
+ variant_map = {
1392
+ 'info': 'alertInfo',
1393
+ 'success': 'alertSuccess',
1394
+ 'warning': 'alertWarning',
1395
+ 'danger': 'alertDanger',
1396
+ }
1397
+ variant_style = variant_map.get(node.variant, 'alertInfo')
1398
+
1399
+ js.jsx_open('View', {'style': f'[styles.alert, styles.{variant_style}]'})
1400
+ js.indent()
1401
+ if node.title:
1402
+ js.jsx_open('Text', {'style': 'styles.alertTitle'})
1403
+ js.indent()
1404
+ js.jsx_text(node.title)
1405
+ js.dedent()
1406
+ js.jsx_close('Text')
1407
+ if node.content:
1408
+ js.jsx_open('Text', {'style': 'styles.alertText'})
1409
+ js.indent()
1410
+ js.jsx_text(node.content)
1411
+ js.dedent()
1412
+ js.jsx_close('Text')
1413
+ self._render_children(node.children, js)
1414
+ js.dedent()
1415
+ js.jsx_close('View')
1416
+
1417
+ def _render_avatar(self, node: UIAvatarNode, js: JsBuilder):
1418
+ self._components_used.add('View')
1419
+ self._components_used.add('Text')
1420
+ self._styles_used.add('avatar')
1421
+
1422
+ size_map = {
1423
+ 'xs': 24,
1424
+ 'sm': 32,
1425
+ 'md': 40,
1426
+ 'lg': 56,
1427
+ 'xl': 72,
1428
+ }
1429
+ size = size_map.get(node.size, 40)
1430
+ avatar_id = self._next_id('avatar')
1431
+
1432
+ self._dynamic_styles.append(f""" {avatar_id}: {{
1433
+ width: {size},
1434
+ height: {size},
1435
+ borderRadius: {size // 2},
1436
+ }},""")
1437
+
1438
+ if node.src:
1439
+ self._components_used.add('Image')
1440
+ js.jsx_open('Image', {
1441
+ 'source': "{{ uri: '" + node.src + "' }}",
1442
+ 'style': f'[styles.avatar, styles.{avatar_id}]',
1443
+ }, self_closing=True)
1444
+ else:
1445
+ # Show initials
1446
+ initials = ''
1447
+ if node.name:
1448
+ parts = node.name.split()
1449
+ initials = ''.join(p[0].upper() for p in parts[:2])
1450
+
1451
+ js.jsx_open('View', {'style': f'[styles.avatar, styles.{avatar_id}]'})
1452
+ js.indent()
1453
+ js.jsx_open('Text', {'style': 'styles.avatarText'})
1454
+ js.indent()
1455
+ js.jsx_text(initials or '?')
1456
+ js.dedent()
1457
+ js.jsx_close('Text')
1458
+ js.dedent()
1459
+ js.jsx_close('View')
1460
+
1461
+ def _render_dropdown(self, node: UIDropdownNode, js: JsBuilder):
1462
+ self._components_used.add('View')
1463
+ self._components_used.add('Text')
1464
+ self._components_used.add('TouchableOpacity')
1465
+ self._styles_used.add('dropdown')
1466
+
1467
+ dropdown_id = self._next_id('dropdownOpen')
1468
+ self._state_vars[dropdown_id] = 'false'
1469
+
1470
+ js.jsx_open('View')
1471
+ js.indent()
1472
+
1473
+ # Trigger button
1474
+ js.jsx_open('TouchableOpacity', {
1475
+ 'style': 'styles.dropdownTrigger',
1476
+ 'onPress': '{() => set' + f'{dropdown_id[0].upper()}{dropdown_id[1:]}(!{dropdown_id})' + '}',
1477
+ })
1478
+ js.indent()
1479
+ js.jsx_open('Text')
1480
+ js.indent()
1481
+ js.jsx_text(node.label or 'Select')
1482
+ js.dedent()
1483
+ js.jsx_close('Text')
1484
+ js.dedent()
1485
+ js.jsx_close('TouchableOpacity')
1486
+
1487
+ # Dropdown menu
1488
+ js.line(f'{{{dropdown_id} && (')
1489
+ js.indent()
1490
+ js.jsx_open('View', {'style': 'styles.dropdownMenu'})
1491
+ js.indent()
1492
+ self._render_children(node.children, js)
1493
+ js.dedent()
1494
+ js.jsx_close('View')
1495
+ js.dedent()
1496
+ js.line(')}')
1497
+
1498
+ js.dedent()
1499
+ js.jsx_close('View')
1500
+
1501
+ def _render_toast(self, node: UIToastNode, js: JsBuilder):
1502
+ self._components_used.add('View')
1503
+ self._components_used.add('Text')
1504
+ self._styles_used.add('toast')
1505
+
1506
+ variant_map = {
1507
+ 'info': 'toastInfo',
1508
+ 'success': 'toastSuccess',
1509
+ 'warning': 'toastWarning',
1510
+ 'danger': 'toastDanger',
1511
+ }
1512
+ variant_style = variant_map.get(node.variant, 'toastInfo')
1513
+
1514
+ js.jsx_open('View', {'style': f'[styles.toast, styles.{variant_style}]'})
1515
+ js.indent()
1516
+ if node.title:
1517
+ js.jsx_open('Text', {'style': 'styles.toastTitle'})
1518
+ js.indent()
1519
+ js.jsx_text(node.title)
1520
+ js.dedent()
1521
+ js.jsx_close('Text')
1522
+ js.jsx_open('Text', {'style': 'styles.toastText'})
1523
+ js.indent()
1524
+ js.jsx_text(node.message)
1525
+ js.dedent()
1526
+ js.jsx_close('Text')
1527
+ js.dedent()
1528
+ js.jsx_close('View')
1529
+
1530
+ def _render_toast_container(self, node: UIToastContainerNode, js: JsBuilder):
1531
+ self._components_used.add('View')
1532
+ self._styles_used.add('toast')
1533
+
1534
+ js.jsx_open('View', {'style': 'styles.toastContainer'}, self_closing=True)
1535
+
1536
+ def _render_carousel(self, node: UICarouselNode, js: JsBuilder):
1537
+ self._components_used.add('ScrollView')
1538
+ self._components_used.add('View')
1539
+ self._styles_used.add('carousel')
1540
+
1541
+ js.jsx_open('ScrollView', {
1542
+ 'horizontal': '{true}',
1543
+ 'pagingEnabled': '{true}',
1544
+ 'showsHorizontalScrollIndicator': '{false}',
1545
+ 'style': 'styles.carousel',
1546
+ })
1547
+ js.indent()
1548
+ self._render_children(node.children, js)
1549
+ js.dedent()
1550
+ js.jsx_close('ScrollView')
1551
+
1552
+ def _render_slide(self, node: UISlideNode, js: JsBuilder):
1553
+ self._components_used.add('View')
1554
+ self._styles_used.add('carousel')
1555
+
1556
+ js.jsx_open('View', {'style': 'styles.slide'})
1557
+ js.indent()
1558
+ self._render_children(node.children, js)
1559
+ js.dedent()
1560
+ js.jsx_close('View')
1561
+
1562
+ def _render_stepper(self, node: UIStepperNode, js: JsBuilder):
1563
+ self._components_used.add('View')
1564
+ self._components_used.add('Text')
1565
+ self._styles_used.add('stepper')
1566
+
1567
+ stepper_id = node.bind or self._next_id('currentStep')
1568
+ if stepper_id not in self._state_vars:
1569
+ self._state_vars[stepper_id] = str(node.current)
1570
+
1571
+ steps = [c for c in node.children if isinstance(c, UIStepNode)]
1572
+
1573
+ orientation = 'row' if node.orientation == 'horizontal' else 'column'
1574
+ stepper_style_id = self._next_id('stepper')
1575
+ self._dynamic_styles.append(f""" {stepper_style_id}: {{
1576
+ flexDirection: '{orientation}',
1577
+ alignItems: 'center',
1578
+ }},""")
1579
+
1580
+ js.jsx_open('View', {'style': f'styles.{stepper_style_id}'})
1581
+ js.indent()
1582
+
1583
+ # Step indicators
1584
+ for i, step in enumerate(steps):
1585
+ is_active = f'{stepper_id} === {i}'
1586
+ is_complete = f'{stepper_id} > {i}'
1587
+
1588
+ js.jsx_open('View', {'style': 'styles.stepItem'})
1589
+ js.indent()
1590
+
1591
+ # Circle
1592
+ js.jsx_open('View', {
1593
+ 'style': f'[styles.stepCircle, {is_active} && styles.stepCircleActive, {is_complete} && styles.stepCircleComplete]'
1594
+ })
1595
+ js.indent()
1596
+ js.jsx_open('Text', {'style': 'styles.stepNumber'})
1597
+ js.indent()
1598
+ js.jsx_text(str(i + 1))
1599
+ js.dedent()
1600
+ js.jsx_close('Text')
1601
+ js.dedent()
1602
+ js.jsx_close('View')
1603
+
1604
+ # Label
1605
+ if node.show_labels and step.title:
1606
+ js.jsx_open('Text', {'style': 'styles.stepLabel'})
1607
+ js.indent()
1608
+ js.jsx_text(step.title)
1609
+ js.dedent()
1610
+ js.jsx_close('Text')
1611
+
1612
+ js.dedent()
1613
+ js.jsx_close('View')
1614
+
1615
+ js.dedent()
1616
+ js.jsx_close('View')
1617
+
1618
+ # Step content
1619
+ for i, step in enumerate(steps):
1620
+ js.line(f'{{{stepper_id} === {i} && (')
1621
+ js.indent()
1622
+ js.jsx_open('View', {'style': 'styles.stepContent'})
1623
+ js.indent()
1624
+ self._render_children(step.children, js)
1625
+ js.dedent()
1626
+ js.jsx_close('View')
1627
+ js.dedent()
1628
+ js.line(')}')
1629
+
1630
+ def _render_step(self, node: UIStepNode, js: JsBuilder):
1631
+ # Standalone step renders as View
1632
+ self._components_used.add('View')
1633
+
1634
+ js.jsx_open('View')
1635
+ js.indent()
1636
+ self._render_children(node.children, js)
1637
+ js.dedent()
1638
+ js.jsx_close('View')
1639
+
1640
+ def _render_calendar(self, node: UICalendarNode, js: JsBuilder):
1641
+ # Simplified calendar placeholder (real impl would need a calendar lib)
1642
+ self._components_used.add('View')
1643
+ self._components_used.add('Text')
1644
+ self._styles_used.add('calendar')
1645
+
1646
+ js.jsx_open('View', {'style': 'styles.calendar'})
1647
+ js.indent()
1648
+ js.jsx_open('Text', {'style': 'styles.calendarPlaceholder'})
1649
+ js.indent()
1650
+ js.jsx_text('Calendar (requires react-native-calendars)')
1651
+ js.dedent()
1652
+ js.jsx_close('Text')
1653
+ js.dedent()
1654
+ js.jsx_close('View')
1655
+
1656
+ def _render_date_picker(self, node: UIDatePickerNode, js: JsBuilder):
1657
+ self._components_used.add('View')
1658
+ self._components_used.add('TextInput')
1659
+ self._components_used.add('TouchableOpacity')
1660
+ self._components_used.add('Text')
1661
+ self._styles_used.add('input')
1662
+
1663
+ picker_id = node.bind or self._next_id('date')
1664
+ if node.bind and node.bind not in self._state_vars:
1665
+ self._state_vars[node.bind] = "''"
1666
+
1667
+ attrs = {'style': 'styles.input'}
1668
+ if node.placeholder:
1669
+ attrs['placeholder'] = node.placeholder
1670
+ if node.bind:
1671
+ attrs['value'] = '{' + node.bind + '}'
1672
+
1673
+ js.jsx_open('View', {'style': 'styles.datePickerContainer'})
1674
+ js.indent()
1675
+ js.jsx_open('TextInput', attrs, self_closing=True)
1676
+ js.comment('Note: For full date picker, use @react-native-community/datetimepicker')
1677
+ js.dedent()
1678
+ js.jsx_close('View')
1679
+
1680
+ # ------------------------------------------------------------------
1681
+ # Additional Component Renderers
1682
+ # ------------------------------------------------------------------
1683
+
1684
+ def _render_animate(self, node: UIAnimateNode, js: JsBuilder):
1685
+ """Render animation wrapper using Animated API"""
1686
+ self._components_used.add('Animated')
1687
+ self._components_used.add('View')
1688
+ self._styles_used.add('animate')
1689
+
1690
+ anim_type = getattr(node, 'type', 'fade') or 'fade'
1691
+ duration = getattr(node, 'duration', 300) or 300
1692
+
1693
+ js.comment(f'Animation: {anim_type} ({duration}ms)')
1694
+ js.jsx_open('Animated.View', {'style': 'styles.animate'})
1695
+ js.indent()
1696
+ self._render_children(node.children, js)
1697
+ js.dedent()
1698
+ js.jsx_close('Animated.View')
1699
+
1700
+ def _render_chart(self, node: UIChartNode, js: JsBuilder):
1701
+ """Render chart placeholder - requires react-native-chart-kit"""
1702
+ self._components_used.add('View')
1703
+ self._components_used.add('Text')
1704
+ self._styles_used.add('chart')
1705
+
1706
+ chart_type = getattr(node, 'type', 'bar') or 'bar'
1707
+
1708
+ js.jsx_open('View', {'style': 'styles.chart'})
1709
+ js.indent()
1710
+ js.jsx_open('Text', {'style': 'styles.chartPlaceholder'})
1711
+ js.indent()
1712
+ js.jsx_text(f'{chart_type.title()} Chart (requires react-native-chart-kit)')
1713
+ js.dedent()
1714
+ js.jsx_close('Text')
1715
+ js.dedent()
1716
+ js.jsx_close('View')
1717
+
1718
+ def _render_tooltip(self, node: UITooltipNode, js: JsBuilder):
1719
+ """Render tooltip wrapper"""
1720
+ self._components_used.add('View')
1721
+ self._components_used.add('Text')
1722
+ self._styles_used.add('tooltip')
1723
+
1724
+ content = getattr(node, 'content', '') or ''
1725
+
1726
+ js.jsx_open('View', {'style': 'styles.tooltipContainer'})
1727
+ js.indent()
1728
+ self._render_children(node.children, js)
1729
+ if content:
1730
+ js.jsx_open('View', {'style': 'styles.tooltip'})
1731
+ js.indent()
1732
+ js.jsx_open('Text', {'style': 'styles.tooltipText'})
1733
+ js.indent()
1734
+ js.jsx_text(content)
1735
+ js.dedent()
1736
+ js.jsx_close('Text')
1737
+ js.dedent()
1738
+ js.jsx_close('View')
1739
+ js.dedent()
1740
+ js.jsx_close('View')
1741
+
1742
+ def _render_breadcrumb(self, node: UIBreadcrumbNode, js: JsBuilder):
1743
+ """Render breadcrumb navigation"""
1744
+ self._components_used.add('View')
1745
+ self._components_used.add('Text')
1746
+ self._styles_used.add('breadcrumb')
1747
+
1748
+ js.jsx_open('View', {'style': 'styles.breadcrumb'})
1749
+ js.indent()
1750
+ self._render_children(node.children, js)
1751
+ js.dedent()
1752
+ js.jsx_close('View')
1753
+
1754
+ def _render_breadcrumb_item(self, node: UIBreadcrumbItemNode, js: JsBuilder):
1755
+ """Render breadcrumb item"""
1756
+ self._components_used.add('TouchableOpacity')
1757
+ self._components_used.add('Text')
1758
+ self._styles_used.add('breadcrumb')
1759
+
1760
+ label = getattr(node, 'label', '') or getattr(node, 'content', '') or ''
1761
+ is_active = getattr(node, 'active', False)
1762
+
1763
+ style = 'styles.breadcrumbItemActive' if is_active else 'styles.breadcrumbItem'
1764
+
1765
+ js.jsx_open('TouchableOpacity', {'style': style})
1766
+ js.indent()
1767
+ js.jsx_open('Text', {'style': 'styles.breadcrumbText'})
1768
+ js.indent()
1769
+ js.jsx_text(label)
1770
+ js.dedent()
1771
+ js.jsx_close('Text')
1772
+ js.dedent()
1773
+ js.jsx_close('TouchableOpacity')
1774
+
1775
+ def _render_pagination(self, node: UIPaginationNode, js: JsBuilder):
1776
+ """Render pagination controls"""
1777
+ self._components_used.add('View')
1778
+ self._components_used.add('TouchableOpacity')
1779
+ self._components_used.add('Text')
1780
+ self._styles_used.add('pagination')
1781
+
1782
+ total = getattr(node, 'total', 1) or 1
1783
+ current = getattr(node, 'current', 1) or 1
1784
+
1785
+ js.jsx_open('View', {'style': 'styles.pagination'})
1786
+ js.indent()
1787
+ # Previous button
1788
+ js.jsx_open('TouchableOpacity', {'style': 'styles.paginationButton'})
1789
+ js.indent()
1790
+ js.jsx_open('Text')
1791
+ js.jsx_text('←')
1792
+ js.dedent()
1793
+ js.jsx_close('Text')
1794
+ js.dedent()
1795
+ js.jsx_close('TouchableOpacity')
1796
+ # Page indicator
1797
+ js.jsx_open('Text', {'style': 'styles.paginationText'})
1798
+ js.indent()
1799
+ js.jsx_text(f'{current} / {total}')
1800
+ js.dedent()
1801
+ js.jsx_close('Text')
1802
+ # Next button
1803
+ js.jsx_open('TouchableOpacity', {'style': 'styles.paginationButton'})
1804
+ js.indent()
1805
+ js.jsx_open('Text')
1806
+ js.jsx_text('→')
1807
+ js.dedent()
1808
+ js.jsx_close('Text')
1809
+ js.dedent()
1810
+ js.jsx_close('TouchableOpacity')
1811
+ js.dedent()
1812
+ js.jsx_close('View')
1813
+
1814
+ def _render_skeleton(self, node: UISkeletonNode, js: JsBuilder):
1815
+ """Render skeleton loading placeholder"""
1816
+ self._components_used.add('View')
1817
+ self._styles_used.add('skeleton')
1818
+
1819
+ variant = getattr(node, 'variant', 'text') or 'text'
1820
+ width = getattr(node, 'width', None)
1821
+ height = getattr(node, 'height', None)
1822
+
1823
+ style_parts = ['styles.skeleton']
1824
+ if variant == 'circle':
1825
+ style_parts.append('styles.skeletonCircle')
1826
+ elif variant == 'rect':
1827
+ style_parts.append('styles.skeletonRect')
1828
+
1829
+ style = style_parts[0] if len(style_parts) == 1 else f'[{", ".join(style_parts)}]'
1830
+
1831
+ attrs = {'style': style}
1832
+ js.jsx_open('View', attrs, self_closing=True)