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,405 @@
1
+ """The console renderer: a Quantum page as a terminal app (UI-3).
2
+
3
+ `quantum console` starts the application's server in this process and opens a
4
+ Textual app on it. The app asks each page for its view tree
5
+ (quantum/runtime/ui_view.py) instead of HTML, draws it with Textual widgets,
6
+ and sends the same q:action posts a browser sends — with a cookie jar, so
7
+ login, validation, flash and redirects are exactly the web's. Nothing of the
8
+ page's logic is translated: the runtime that serves the browser serves this.
9
+ """
10
+
11
+ import asyncio
12
+ from typing import Any, Dict, List, Optional
13
+ from urllib.parse import urljoin
14
+
15
+ import requests
16
+ from rich.text import Text
17
+ from textual.app import App, ComposeResult
18
+ from textual.containers import Grid, Horizontal, Vertical, VerticalScroll
19
+ from textual.widgets import (Button, Checkbox, Input, ProgressBar, RadioButton, RadioSet,
20
+ Select, Static, Switch, TabbedContent, TabPane)
21
+
22
+ VIEW = 'application/vnd.quantum.view+json'
23
+
24
+ # UI-2 in a terminal: the breakpoints in columns (a column is ~8 px).
25
+ BREAKPOINT_COLUMNS = {'sm': 80, 'md': 96, 'lg': 128}
26
+
27
+
28
+ def _marcado(valor) -> bool:
29
+ """UI-6: checked="true", or an expression that resolved to a true value."""
30
+ return valor is not None and str(valor).strip().lower() in ('true', '1', 'on', 'yes', 'checked')
31
+
32
+
33
+ def _numero(valor, padrao: float) -> float:
34
+ try:
35
+ return float(valor)
36
+ except (TypeError, ValueError):
37
+ return padrao
38
+
39
+
40
+ class ConsoleUI(App):
41
+ """Draws a page's view tree; buttons and forms post the page's actions."""
42
+
43
+ CSS = """
44
+ Screen { background: $surface; }
45
+ #tela { padding: 1 2; }
46
+ .window { height: auto; }
47
+ .hbox { height: auto; }
48
+ .vbox { height: auto; }
49
+ .hbox > Static { width: auto; margin-right: 1; }
50
+ .hbox > Button, .hbox > Input, .hbox > Select { margin-right: 1; }
51
+ .grow { width: 1fr; }
52
+ .panel { border: round $primary; padding: 0 1; height: auto; margin-bottom: 1; }
53
+ .flash { background: $success 30%; padding: 0 1; margin-bottom: 1; }
54
+ .flash-error { background: $error 30%; }
55
+ .badge { background: $secondary; color: $text; padding: 0 1; }
56
+ .link { min-width: 0; height: 1; border: none; background: $surface; text-style: underline; }
57
+ Button { min-width: 8; }
58
+ .grid { height: auto; grid-rows: auto; grid-gutter: 0 1; }
59
+ .table { height: auto; margin-bottom: 1; }
60
+ .row { height: auto; }
61
+ .row-head { border-bottom: solid $primary; }
62
+ .cell { height: auto; padding: 0 1; }
63
+ .label { color: $text-muted; }
64
+ .unsupported { color: $warning; }
65
+ .field-error { color: $error; }
66
+ .invalid { border: tall $error; }
67
+ TabbedContent { height: auto; }
68
+ TabPane { height: auto; }
69
+ """
70
+
71
+ def __init__(self, base_url: str, path: str = '/'):
72
+ super().__init__()
73
+ self.base_url = base_url
74
+ self.path = path
75
+ self.http = requests.Session()
76
+ self._eventos: Dict[int, Dict[str, Any]] = {}
77
+ self.titulo_da_pagina = ''
78
+ self._trava = asyncio.Lock()
79
+ self.carregando = False
80
+
81
+ def compose(self) -> ComposeResult:
82
+ yield VerticalScroll(id='tela')
83
+
84
+ async def on_mount(self) -> None:
85
+ await self.carregar(self.path)
86
+
87
+ # -- talking to the page --------------------------------------------------
88
+
89
+ def _pedir(self, metodo: str, caminho: str, dados: Optional[dict] = None) -> dict:
90
+ url = urljoin(self.base_url, caminho)
91
+ resposta = self.http.request(metodo, url, data=dados, allow_redirects=True,
92
+ headers={'Accept': VIEW, 'Referer': urljoin(self.base_url, self.path)},
93
+ timeout=30)
94
+ if resposta.headers.get('Content-Type', '').startswith(VIEW):
95
+ self.path = resposta.url[len(self.base_url.rstrip('/')):] or '/'
96
+ return resposta.json()
97
+ # An error page, a 400 from an unknown action: show it as it is.
98
+ return {'title': str(resposta.status_code), 'flash': f'{resposta.status_code} {resposta.reason}',
99
+ 'flashType': 'error', 'view': []}
100
+
101
+ async def carregar(self, caminho: str, metodo: str = 'GET', dados: Optional[dict] = None) -> None:
102
+ # One page at a time: a second click while a page is still being
103
+ # mounted removed half-mounted widgets (Textual's Select then fails).
104
+ async with self._trava:
105
+ self.carregando = True
106
+ try:
107
+ await self._carregar(caminho, metodo, dados)
108
+ # Let the new widgets finish their own mounting (a Select
109
+ # composes its label after mount) before anything replaces them.
110
+ await asyncio.sleep(0)
111
+ finally:
112
+ self.carregando = False
113
+
114
+ async def _carregar(self, caminho: str, metodo: str, dados: Optional[dict]) -> None:
115
+ pagina = self._pedir(metodo, caminho, dados)
116
+ self.titulo_da_pagina = pagina.get('title', '')
117
+ self.title = self.titulo_da_pagina
118
+ tela = self.query_one('#tela', VerticalScroll)
119
+ await tela.remove_children()
120
+ self._eventos.clear()
121
+ widgets: List = []
122
+ if pagina.get('flash'):
123
+ classes = 'flash flash-error' if pagina.get('flashType') == 'error' else 'flash'
124
+ widgets.append(Static(Text(str(pagina['flash'])), classes=classes, id='flash'))
125
+ widgets.extend(self._montar(pagina.get('view', [])))
126
+ await tela.mount_all(widgets)
127
+
128
+ # -- drawing --------------------------------------------------------------
129
+
130
+ def _montar(self, nos: List[dict], formulario: Optional[dict] = None) -> List:
131
+ widgets = [w for no in nos for w in [self._widget(no, formulario)] if w is not None]
132
+ for w in widgets:
133
+ if formulario is not None and not hasattr(w, '_quantum_formulario'):
134
+ w._quantum_formulario = formulario
135
+ return widgets
136
+
137
+ def _classes_css(self, props: dict, base: str) -> str:
138
+ classes = [base]
139
+ if str(props.get('grow', '')).lower() == 'true':
140
+ classes.append('grow')
141
+ return ' '.join(classes)
142
+
143
+ def _widget(self, no: dict, formulario: Optional[dict]):
144
+ widget = self._widget_sem_largura(no, formulario)
145
+ props = no.get('props') or {}
146
+ largura = str(props.get('width') or '')
147
+ if widget is not None and largura.isdigit():
148
+ # A width in px becomes columns (~8 px each), as the breakpoints do.
149
+ widget.styles.width = max(8, int(largura) // 8)
150
+ if widget is not None and props.get('error'):
151
+ # UI-9: the server refused this field; its message under it.
152
+ widget.add_class('invalid')
153
+ return Vertical(widget, Static(Text(str(props['error'])), classes='field-error'), classes='vbox')
154
+ return widget
155
+
156
+ def _widget_sem_largura(self, no: dict, formulario: Optional[dict]):
157
+ tipo, props = no.get('type'), no.get('props', {})
158
+ filhos = no.get('children', [])
159
+ ident = props.get('ui_id')
160
+ texto = str(props.get('text') or '')
161
+
162
+ if tipo in ('window', 'vbox', 'scrollbox', 'cardbody', 'cardfooter', 'tab'):
163
+ return Vertical(*self._montar(filhos, formulario), classes=self._classes_css(props, 'vbox'), id=ident)
164
+ if tipo == 'section':
165
+ titulo = [Static(Text(str(props['title']), style='bold'), classes='text')] if props.get('title') else []
166
+ return Vertical(*titulo, *self._montar(filhos, formulario), classes=self._classes_css(props, 'vbox'),
167
+ id=ident)
168
+ if tipo == 'card':
169
+ cartao = Vertical(*self._montar(filhos, formulario), classes='panel', id=ident)
170
+ cartao.border_title = props.get('title', '')
171
+ return cartao
172
+ if tipo == 'hbox':
173
+ empilha = props.get('stack_below')
174
+ estreito = empilha and self.size.width < BREAKPOINT_COLUMNS.get(empilha, 0)
175
+ tipo_caixa = Vertical if estreito else Horizontal
176
+ filhos_w = self._montar(filhos, formulario)
177
+ if estreito:
178
+ # Stacked: fixed widths stop applying, as in the web's CSS (UI-2).
179
+ for w in filhos_w:
180
+ w.styles.width = '100%'
181
+ return tipo_caixa(*filhos_w,
182
+ classes=self._classes_css(props, 'vbox' if estreito else 'hbox'), id=ident)
183
+ if tipo == 'grid':
184
+ grade = Grid(*self._montar(filhos, formulario), classes='grid', id=ident)
185
+ grade.styles.grid_size_columns = self._colunas_da_grade(str(props.get('columns') or '1'))
186
+ return grade
187
+ if tipo == 'panel':
188
+ painel = Vertical(*self._montar(filhos, formulario), classes='panel', id=ident)
189
+ painel.border_title = props.get('title', '')
190
+ return painel
191
+ if tipo == 'tabpanel':
192
+ abas = TabbedContent(id=ident)
193
+ for n, f in enumerate(f for f in filhos if f.get('type') == 'tab'):
194
+ abas.compose_add_child(TabPane(str(f.get('props', {}).get('title') or n + 1),
195
+ *self._montar(f.get('children', []), formulario)))
196
+ return abas
197
+ if tipo == 'form':
198
+ # The form's marker exists before its children, so each widget
199
+ # inside can point to it; the marker keeps the box and the action.
200
+ marcador = {'event': no.get('event')}
201
+ caixa = Vertical(*self._montar(filhos, marcador), classes='vbox', id=ident)
202
+ marcador['caixa'] = caixa
203
+ return caixa
204
+ if tipo == 'formitem':
205
+ rotulo = [Static(Text(str(props['label'])), classes='label')] if props.get('label') else []
206
+ return Vertical(*rotulo, *self._montar(filhos, formulario), classes='vbox', id=ident)
207
+ if tipo == 'button':
208
+ botao = Button(texto or props.get('label') or '', id=ident,
209
+ variant='error' if props.get('variant') == 'danger' else
210
+ ('primary' if props.get('variant') == 'primary' else 'default'))
211
+ if no.get('event'):
212
+ self._eventos[id(botao)] = no['event']
213
+ return botao
214
+ if tipo == 'link':
215
+ link = Button(texto or props.get('to', ''), classes='link', id=ident)
216
+ self._eventos[id(link)] = {'navigate': props.get('to', '/')}
217
+ return link
218
+ if tipo == 'input':
219
+ campo = Input(value=str(props.get('value') or ''), placeholder=props.get('placeholder', ''),
220
+ id=ident, classes=self._classes_css(props, 'input'),
221
+ password=props.get('input_type') == 'password')
222
+ campo._quantum_nome = props.get('bind')
223
+ if props.get('search'):
224
+ # UI-12: typing asks for the page again with ?bind=value.
225
+ campo._quantum_busca = int(props.get('delay') or 300)
226
+ campo._quantum_inicial = campo.value
227
+ return campo
228
+ if tipo == 'select':
229
+ opcoes = [(o.strip(), o.strip()) for o in str(props.get('options') or '').split(',') if o.strip()]
230
+ opcoes += [(str(f['props'].get('label') or f['props'].get('value') or ''),
231
+ str(f['props'].get('value') or ''))
232
+ for f in filhos if f.get('type') == 'option']
233
+ valores = [v for _, v in opcoes]
234
+ escolhido = str(props.get('value') or '')
235
+ valor = escolhido if escolhido in valores else (valores[0] if valores else Select.BLANK)
236
+ escolha = Select(opcoes, value=valor, allow_blank=not opcoes, id=ident)
237
+ escolha._quantum_nome = props.get('bind')
238
+ return escolha
239
+ if tipo == 'checkbox':
240
+ caixa = Checkbox(str(props.get('label') or ''), value=_marcado(props.get('checked')), id=ident)
241
+ caixa._quantum_nome = props.get('bind')
242
+ return caixa
243
+ if tipo == 'switch':
244
+ chave = Switch(value=_marcado(props.get('checked')))
245
+ chave._quantum_nome = props.get('bind')
246
+ return Horizontal(chave, Static(Text(str(props.get('label') or '')), classes='text'),
247
+ classes='hbox', id=ident)
248
+ if tipo == 'radio':
249
+ escolhido = str(props.get('value') or '')
250
+ opcoes = [o.strip() for o in str(props.get('options') or '').split(',') if o.strip()]
251
+ grupo = RadioSet(*[RadioButton(o, value=o == escolhido) for o in opcoes], id=ident)
252
+ grupo._quantum_nome = props.get('bind')
253
+ return grupo
254
+ if tipo == 'table':
255
+ return self._tabela(no, formulario, ident)
256
+ if tipo == 'list':
257
+ return Vertical(*self._montar(filhos, formulario), classes='vbox', id=ident)
258
+ if tipo == 'item':
259
+ return Horizontal(Static('•', classes='text'), *self._montar(filhos, formulario),
260
+ classes='hbox', id=ident)
261
+ if tipo == 'progress':
262
+ barra = ProgressBar(total=_numero(props.get('max'), 100), show_eta=False, id=ident)
263
+ barra.progress = _numero(props.get('value'), 0)
264
+ return barra
265
+ if tipo == 'image':
266
+ return Static(Text(f"[{props.get('alt') or props.get('src') or 'image'}]"), classes='text', id=ident)
267
+ if tipo in ('header', 'cardheader'):
268
+ titulo = str(props.get('title') or texto)
269
+ return Vertical(Static(Text(titulo, style='bold'), classes='text'),
270
+ *self._montar(filhos, formulario), classes='vbox', id=ident)
271
+ if tipo == 'alert':
272
+ classes = 'flash flash-error' if props.get('variant') in ('danger', 'error') else 'flash'
273
+ titulo = f"{props['title']}: " if props.get('title') else ''
274
+ return Static(Text(titulo + texto), classes=classes, id=ident)
275
+ if tipo == 'badge':
276
+ return Static(Text(texto), classes='badge', id=ident)
277
+ if tipo == 'text':
278
+ return Static(Text(texto, style='bold' if props.get('weight') == 'bold' else ''),
279
+ classes='text', id=ident)
280
+ if tipo == 'footer':
281
+ return Static(Text(texto, style='dim'), classes='text', id=ident)
282
+ if tipo == 'rule':
283
+ return Static('─' * 40, classes='text')
284
+ if tipo == 'spacer':
285
+ return Static('', classes='text')
286
+ # UI-7: outside the core set, say so — never draw something else in its place.
287
+ return Static(Text(f'[ui:{tipo} is not drawn in the console]', style='italic'),
288
+ classes='unsupported', id=ident)
289
+
290
+ def _colunas_da_grade(self, spec: str) -> int:
291
+ """UI-2 `columns="1 sm:2 lg:3"` counted in terminal columns; "1fr 2fr" is 2."""
292
+ partes = spec.split()
293
+ if len(partes) > 1 and all(p.endswith('fr') for p in partes):
294
+ return len(partes)
295
+ colunas = 1
296
+ for parte in partes:
297
+ faixa, _, valor = parte.rpartition(':')
298
+ if valor.isdigit() and (not faixa or self.size.width >= BREAKPOINT_COLUMNS.get(faixa, 10 ** 6)):
299
+ colunas = int(valor)
300
+ return max(1, colunas)
301
+
302
+ def _tabela(self, no: dict, formulario: Optional[dict], ident):
303
+ """UI-5: a header row and one row per record; a cell holds text or widgets."""
304
+ colunas = no.get('columns', [])
305
+
306
+ def celula(widgets, coluna):
307
+ caixa = Vertical(*widgets, classes='cell')
308
+ largura = str(coluna.get('width') or '')
309
+ caixa.styles.width = max(4, int(largura) // 8) if largura.isdigit() else '1fr'
310
+ return caixa
311
+
312
+ def titulo(c):
313
+ if c.get('to'):
314
+ # UI-13: a sortable header is a link, as in the browser.
315
+ link = Button(c['label'], classes='link')
316
+ self._eventos[id(link)] = {'navigate': c['to']}
317
+ return link
318
+ return Static(Text(c['label'], style='bold'))
319
+
320
+ cabecalho = Horizontal(*[celula([titulo(c)], c) for c in colunas], classes='row row-head')
321
+ linhas = [Horizontal(*[celula(self._montar(cel, formulario), c) for cel, c in zip(linha, colunas)],
322
+ classes='row')
323
+ for linha in no.get('rows', [])]
324
+ return Vertical(cabecalho, *linhas, classes='table', id=ident)
325
+
326
+ # -- events are the page's actions ---------------------------------------
327
+
328
+ def _campos_do_formulario(self, formulario: dict) -> Dict[str, str]:
329
+ """The fields a browser would post: a checked box posts 'on', an unchecked one nothing."""
330
+ campos = {}
331
+ for w in formulario['caixa'].query('Input, Select, Checkbox, Switch, RadioSet'):
332
+ nome = getattr(w, '_quantum_nome', None)
333
+ if not nome:
334
+ continue
335
+ if isinstance(w, (Checkbox, Switch)):
336
+ if w.value:
337
+ campos[nome] = 'on'
338
+ continue
339
+ if isinstance(w, RadioSet):
340
+ if w.pressed_button is not None:
341
+ campos[nome] = str(w.pressed_button.label)
342
+ continue
343
+ valor = w.value
344
+ campos[nome] = '' if valor is None or valor is Select.BLANK else str(valor)
345
+ return campos
346
+
347
+ async def on_button_pressed(self, evento: Button.Pressed) -> None:
348
+ botao = evento.button
349
+ acao = self._eventos.get(id(botao))
350
+ if acao and 'navigate' in acao:
351
+ await self.carregar(acao['navigate'])
352
+ return
353
+ formulario = getattr(botao, '_quantum_formulario', None)
354
+ if acao is None and formulario is not None:
355
+ acao = formulario.get('event')
356
+ if acao:
357
+ dados = {'action': acao['action'], **acao.get('fields', {}),
358
+ **self._campos_do_formulario(formulario)}
359
+ await self.carregar(self.path, 'POST', dados)
360
+ return
361
+ if acao:
362
+ await self.carregar(self.path, 'POST', {'action': acao['action'], **acao.get('fields', {})})
363
+
364
+ def on_input_changed(self, evento: Input.Changed) -> None:
365
+ """UI-12: search as you type — after a pause, the page with ?bind=value."""
366
+ campo = evento.input
367
+ espera = getattr(campo, '_quantum_busca', None)
368
+ if espera is None or campo.value == getattr(campo, '_quantum_inicial', None):
369
+ return
370
+ anterior = getattr(self, '_busca_timer', None)
371
+ if anterior is not None:
372
+ anterior.stop()
373
+ nome, valor = campo._quantum_nome, campo.value
374
+ self._busca_timer = self.set_timer(
375
+ espera / 1000, lambda: self.run_worker(self._buscar(nome, valor), exclusive=True, group='busca'))
376
+
377
+ async def _buscar(self, nome: str, valor: str) -> None:
378
+ from urllib.parse import parse_qsl, urlencode, urlsplit
379
+ partes = urlsplit(self.path)
380
+ parametros = {k: v for k, v in parse_qsl(partes.query) if k not in (nome, 'page')}
381
+ if valor:
382
+ parametros[nome] = valor
383
+ await self.carregar(partes.path + ('?' + urlencode(parametros) if parametros else ''))
384
+ # The page was drawn again: the field keeps the focus and the cursor at the end.
385
+ for campo in self.query(Input):
386
+ if getattr(campo, '_quantum_nome', None) == nome and getattr(campo, '_quantum_busca', None):
387
+ campo.focus()
388
+ campo.cursor_position = len(campo.value)
389
+ break
390
+
391
+ async def on_input_submitted(self, evento: Input.Submitted) -> None:
392
+ formulario = getattr(evento.input, '_quantum_formulario', None)
393
+ acao = formulario.get('event') if formulario is not None else None
394
+ if acao:
395
+ dados = {'action': acao['action'], **acao.get('fields', {}), **self._campos_do_formulario(formulario)}
396
+ await self.carregar(self.path, 'POST', dados)
397
+
398
+
399
+ def run_console(config_path: str = 'quantum.config.yaml', path: str = '/') -> int:
400
+ """`quantum console`: the application's server in a thread, the page in the terminal."""
401
+ from quantum.runtime.local_server import local_server
402
+
403
+ with local_server(config_path) as base_url:
404
+ ConsoleUI(base_url, path).run()
405
+ return 0
@@ -0,0 +1,56 @@
1
+ """The desktop renderer: a Quantum application in its own window (UI-4).
2
+
3
+ `quantum desktop` starts the application's server on a free local port and
4
+ opens a native window (pywebview) on it. The window is a browser without the
5
+ browser's chrome: pages, q:action, sessions and UI-2 layout are exactly the
6
+ web's, because it is the web's server that answers. Nothing of a page's logic
7
+ is translated.
8
+ """
9
+
10
+ import sys
11
+ from urllib.parse import urljoin
12
+
13
+ INSTALL_HINT = 'pip install "quantum-framework[desktop]"'
14
+
15
+ DEFAULT_WIDTH = 1024
16
+ DEFAULT_HEIGHT = 720
17
+
18
+
19
+ def _webview():
20
+ try:
21
+ import webview
22
+ except ImportError:
23
+ return None
24
+ return webview
25
+
26
+
27
+ def open_window(webview, url: str, title: str = 'Quantum',
28
+ width: int = DEFAULT_WIDTH, height: int = DEFAULT_HEIGHT):
29
+ """Create the window on `url`; its title follows each page's <title>."""
30
+ janela = webview.create_window(title, url, width=width, height=height, min_size=(360, 480))
31
+
32
+ def ao_carregar():
33
+ titulo = janela.evaluate_js('document.title')
34
+ if titulo:
35
+ janela.set_title(str(titulo))
36
+
37
+ janela.events.loaded += ao_carregar
38
+ return janela
39
+
40
+
41
+ def run_desktop(config_path: str = 'quantum.config.yaml', path: str = '/',
42
+ width: int = DEFAULT_WIDTH, height: int = DEFAULT_HEIGHT,
43
+ webview=None) -> int:
44
+ """`quantum desktop`: the application's server in a thread, the pages in a window."""
45
+ webview = webview or _webview()
46
+ if webview is None:
47
+ print(f'[ERROR] quantum desktop needs pywebview: {INSTALL_HINT}', file=sys.stderr)
48
+ return 1
49
+ from quantum.runtime.local_server import local_server
50
+
51
+ with local_server(config_path) as base_url:
52
+ open_window(webview, urljoin(base_url, path.lstrip('/')), width=width, height=height)
53
+ # Blocks until the window closes; the server stops with it.
54
+ webview.start(private_mode=True)
55
+ return 0
56
+
@@ -0,0 +1,193 @@
1
+ """Forms that know their action's rules (M1, UI-9).
2
+
3
+ A `<ui:form on-submit="criar">` posts to `<q:action name="criar">`, whose
4
+ `q:param`s already say what each field must be. The form takes those rules
5
+ as HTML attributes — `required`, `minlength`, `min`, `type="number"`… — so the
6
+ browser checks them before posting; the server still validates (ACT-2). When
7
+ the server's validation fails, the next render of that form shows the values
8
+ that were sent (never a password) and each error next to its field.
9
+
10
+ What a field gets is visible in the HTML it draws; `<ui:form rules="off">`
11
+ turns it off; an attribute written on the field always wins.
12
+ """
13
+
14
+ import copy
15
+ import html
16
+ from typing import Dict, Optional, Tuple
17
+
18
+ from quantum.core.features.ui_engine.src.ast_nodes import (
19
+ UICheckboxNode, UIInputNode, UIRadioNode, UISelectNode, UISwitchNode)
20
+
21
+ FIELDS = (UIInputNode, UISelectNode, UIRadioNode, UICheckboxNode, UISwitchNode)
22
+
23
+ # q:param type -> the input type the browser checks
24
+ _HTML_TYPE = {'integer': 'number', 'number': 'number', 'decimal': 'number', 'float': 'number',
25
+ 'email': 'email', 'url': 'url', 'date': 'date'}
26
+
27
+
28
+ class FormContext:
29
+ """What a form knows while its fields are drawn."""
30
+
31
+ def __init__(self, params=None, state: Optional[dict] = None, rules: bool = True,
32
+ initial: Optional[dict] = None, action=None, config=None):
33
+ self.all_params = list(params or [])
34
+ self.params = {p.name: p for p in self.all_params} if rules else {}
35
+ self.errors: Dict[str, str] = (state or {}).get('errors') or {}
36
+ self.values: Optional[Dict[str, str]] = (state or {}).get('values') if state else None
37
+ self.initial: Dict[str, object] = initial or {} # UI-10: values="{row}"
38
+ self.action = action
39
+ self.config = config or {}
40
+
41
+
42
+ def form_context(renderer, form_node, action_name: Optional[str]) -> Optional[FormContext]:
43
+ if not action_name:
44
+ return None
45
+ acao = (getattr(renderer, '_ui_action_nodes', None) or {}).get(action_name)
46
+ estado = getattr(renderer, 'form_state', None)
47
+ if not estado or estado.get('action') != action_name:
48
+ estado = None
49
+ config = getattr(renderer, 'config', None) or {}
50
+ params = []
51
+ if acao is not None:
52
+ from quantum.runtime.table_params import TableParamsError, params_of
53
+ try:
54
+ params = params_of(acao, config)
55
+ except TableParamsError as exc:
56
+ raise ValueError(f'<ui:form on-submit="{action_name}">: {exc}') from exc
57
+ inicial = {}
58
+ expressao = getattr(form_node, 'values', None)
59
+ if expressao:
60
+ linha = renderer._apply_databinding(expressao)
61
+ if isinstance(linha, list) and len(linha) == 1:
62
+ linha = linha[0] # a q:query of one row
63
+ if linha not in (None, ''):
64
+ if not isinstance(linha, dict):
65
+ raise ValueError(f'<ui:form values="{expressao}"> needs a row (a record), '
66
+ f'got {type(linha).__name__} (UI-10)')
67
+ inicial = linha
68
+ return FormContext(params, estado, getattr(form_node, 'rules', None) != 'off', inicial, acao, config)
69
+
70
+
71
+ def prepare_field(node, ctx: Optional[FormContext], escape: bool) -> Tuple[object, Optional[str]]:
72
+ """A copy of the field with the action's rules and the values sent, and its error.
73
+
74
+ `escape`: the HTML renderer writes attribute values as they are, so a
75
+ value sent back to the page is escaped here; the view tree is not (UI-3).
76
+ """
77
+ nome = getattr(node, 'bind', None)
78
+ if ctx is None or not nome:
79
+ return node, None
80
+ copia = copy.copy(node)
81
+ param = ctx.params.get(nome)
82
+ if param is not None:
83
+ if isinstance(copia, UIInputNode):
84
+ if param.required and not copia.required:
85
+ copia.required = True
86
+ for campo in ('minlength', 'maxlength'):
87
+ valor = getattr(param, campo, None)
88
+ if getattr(copia, campo) is None and valor is not None:
89
+ setattr(copia, campo, int(valor))
90
+ for campo in ('min', 'max'):
91
+ valor = getattr(param, campo, None)
92
+ if getattr(copia, campo) is None and valor is not None:
93
+ setattr(copia, campo, str(valor))
94
+ padrao = getattr(param, 'pattern', None) or ''
95
+ # The server searches (re.search); HTML's pattern must match the
96
+ # whole value. Only an anchored ^…$ pattern means the same in both.
97
+ if not copia.pattern and padrao.startswith('^') and padrao.endswith('$'):
98
+ copia.pattern = padrao[1:-1]
99
+ tipo = _HTML_TYPE.get(str(getattr(param, 'type', '') or '').lower())
100
+ if tipo and (copia.input_type or 'text') == 'text':
101
+ copia.input_type = tipo
102
+ elif isinstance(copia, (UISelectNode, UIRadioNode)):
103
+ if not copia.options and not getattr(copia, 'children', None) and getattr(param, 'enum', None):
104
+ copia.options = param.enum
105
+ if ctx.values is None and nome in ctx.initial:
106
+ # UI-10: an edit form opens with the row's value (unless the field sets its own).
107
+ inicial = ctx.initial[nome]
108
+ if isinstance(copia, (UICheckboxNode, UISwitchNode)):
109
+ if getattr(copia, 'checked', None) is None:
110
+ copia.checked = 'true' if inicial not in (None, '', 0, '0', False, 'false') else 'false'
111
+ elif getattr(copia, 'value', None) in (None, '') and inicial is not None and not (isinstance(copia, UIInputNode) and copia.input_type == 'password'):
112
+ copia.value = html.escape(str(inicial), quote=True) if escape else str(inicial)
113
+ if ctx.values is not None:
114
+ if isinstance(copia, (UICheckboxNode, UISwitchNode)):
115
+ # A browser sends a checked box and omits an unchecked one.
116
+ copia.checked = 'true' if nome in ctx.values else 'false'
117
+ elif nome in ctx.values and not (isinstance(copia, UIInputNode) and copia.input_type == 'password'):
118
+ valor = str(ctx.values[nome])
119
+ copia.value = html.escape(valor, quote=True) if escape else valor
120
+ erro = ctx.errors.get(nome)
121
+ if erro:
122
+ copia.ui_class = ((getattr(copia, 'ui_class', None) or '') + ' q-invalid').strip()
123
+ return copia, erro
124
+
125
+
126
+ FORM_CSS = """
127
+ /* UI-9: a field the server's validation refused, and its message */
128
+ .q-invalid, .q-invalid input { border-color: var(--q-danger, #dc2626) !important; }
129
+ .q-field-error { display: block; color: var(--q-danger, #dc2626); font-size: 13px; margin-top: 4px; }
130
+ """
131
+
132
+
133
+ def has_fields(nodes) -> bool:
134
+ """Whether a form's markup already has fields of its own."""
135
+ for no in nodes or []:
136
+ if isinstance(no, FIELDS) or has_fields(getattr(no, 'children', None)):
137
+ return True
138
+ return False
139
+
140
+
141
+ def has_button(nodes) -> bool:
142
+ from quantum.core.features.ui_engine.src.ast_nodes import UIButtonNode
143
+ for no in nodes or []:
144
+ if isinstance(no, UIButtonNode) or has_button(getattr(no, 'children', None)):
145
+ return True
146
+ return False
147
+
148
+
149
+ def generated_fields(form_node, ctx: Optional[FormContext]) -> list:
150
+ """UI-10: a form with no fields draws one per param of its action, and a button."""
151
+ from quantum.core.features.ui_engine.src.ast_nodes import (
152
+ UIButtonNode, UIFormItemNode, UIOptionNode)
153
+ from quantum.runtime.table_params import humanize, reference_options
154
+ filhos = list(getattr(form_node, 'children', None) or [])
155
+ if ctx is None or not ctx.params or has_fields(filhos):
156
+ return filhos
157
+ gerados = []
158
+ for param in ctx.all_params:
159
+ rotulo = humanize(param.name)
160
+ if str(param.type).lower() == 'boolean':
161
+ campo = UICheckboxNode()
162
+ campo.bind, campo.label = param.name, rotulo
163
+ gerados.append(campo)
164
+ continue
165
+ referencia = getattr(param, 'references', None)
166
+ if referencia and ctx.action is not None:
167
+ campo = UISelectNode()
168
+ campo.bind = param.name
169
+ if not param.required:
170
+ vazio = UIOptionNode()
171
+ vazio.value, vazio.label = '', '—'
172
+ campo.children.append(vazio)
173
+ for valor, texto in reference_options(ctx.config, ctx.action.table_datasource, referencia):
174
+ opcao = UIOptionNode()
175
+ opcao.value, opcao.label = valor, texto
176
+ campo.children.append(opcao)
177
+ elif param.enum:
178
+ campo = UISelectNode()
179
+ campo.bind = param.name
180
+ else:
181
+ campo = UIInputNode()
182
+ campo.bind = param.name
183
+ item = UIFormItemNode()
184
+ item.label = rotulo
185
+ item.children.append(campo)
186
+ gerados.append(item)
187
+ if not has_button(filhos):
188
+ botao = UIButtonNode()
189
+ botao.content = getattr(form_node, 'submit', None) or 'Save'
190
+ botao.variant = 'primary'
191
+ gerados.append(botao)
192
+ return filhos + gerados
193
+