quantum-framework 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. quantum/__init__.py +1 -0
  2. quantum/cli/__init__.py +2 -0
  3. quantum/cli/commands/__init__.py +15 -0
  4. quantum/cli/commands/build.py +417 -0
  5. quantum/cli/commands/dev.py +185 -0
  6. quantum/cli/commands/docs.py +329 -0
  7. quantum/cli/commands/lint.py +523 -0
  8. quantum/cli/commands/migrate.py +527 -0
  9. quantum/cli/commands/new.py +622 -0
  10. quantum/cli/commands/serve.py +190 -0
  11. quantum/cli/commands/test.py +193 -0
  12. quantum/cli/deploy.py +810 -0
  13. quantum/cli/hot_reload.py +951 -0
  14. quantum/cli/jobs.py +356 -0
  15. quantum/cli/mq.py +582 -0
  16. quantum/cli/pkg.py +390 -0
  17. quantum/cli/runner.py +547 -0
  18. quantum/cli/server_process.py +159 -0
  19. quantum/cli/utils.py +334 -0
  20. quantum/compiler/__init__.py +30 -0
  21. quantum/compiler/base_generator.py +367 -0
  22. quantum/compiler/cli.py +295 -0
  23. quantum/compiler/expression_transformer.py +444 -0
  24. quantum/compiler/javascript/__init__.py +10 -0
  25. quantum/compiler/javascript/generator.py +659 -0
  26. quantum/compiler/optimizer.py +270 -0
  27. quantum/compiler/python/__init__.py +10 -0
  28. quantum/compiler/python/generator.py +883 -0
  29. quantum/compiler/python/runtime.py +863 -0
  30. quantum/compiler/transpiler.py +330 -0
  31. quantum/core/__init__.py +3 -0
  32. quantum/core/ast_nodes.py +2611 -0
  33. quantum/core/expression_diagnostics.py +87 -0
  34. quantum/core/expression_stdlib.py +148 -0
  35. quantum/core/expressions.py +591 -0
  36. quantum/core/features/agents/src/__init__.py +30 -0
  37. quantum/core/features/agents/src/ast_node.py +540 -0
  38. quantum/core/features/conditionals/src/__init__.py +8 -0
  39. quantum/core/features/conditionals/src/ast_node.py +68 -0
  40. quantum/core/features/data_fetching/src/__init__.py +21 -0
  41. quantum/core/features/data_fetching/src/ast_node.py +312 -0
  42. quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
  43. quantum/core/features/data_fetching/src/html_adapter.py +474 -0
  44. quantum/core/features/data_fetching/src/parser.py +225 -0
  45. quantum/core/features/data_import/src/__init__.py +0 -0
  46. quantum/core/features/data_import/src/ast_node.py +291 -0
  47. quantum/core/features/data_import/src/runtime.py +538 -0
  48. quantum/core/features/dump/src/__init__.py +12 -0
  49. quantum/core/features/dump/src/ast_node.py +106 -0
  50. quantum/core/features/dump/src/parser.py +61 -0
  51. quantum/core/features/dump/src/runtime.py +246 -0
  52. quantum/core/features/functions/src/__init__.py +8 -0
  53. quantum/core/features/functions/src/ast_node.py +149 -0
  54. quantum/core/features/game_engine_2d/src/__init__.py +19 -0
  55. quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
  56. quantum/core/features/game_engine_2d/src/parser.py +983 -0
  57. quantum/core/features/invocation/src/__init__.py +0 -0
  58. quantum/core/features/invocation/src/ast_node.py +146 -0
  59. quantum/core/features/invocation/src/runtime.py +327 -0
  60. quantum/core/features/knowledge_base/src/__init__.py +6 -0
  61. quantum/core/features/knowledge_base/src/ast_node.py +113 -0
  62. quantum/core/features/knowledge_base/src/parser.py +82 -0
  63. quantum/core/features/logging/src/__init__.py +12 -0
  64. quantum/core/features/logging/src/ast_node.py +111 -0
  65. quantum/core/features/logging/src/parser.py +50 -0
  66. quantum/core/features/logging/src/runtime.py +190 -0
  67. quantum/core/features/loops/src/__init__.py +8 -0
  68. quantum/core/features/loops/src/ast_node.py +60 -0
  69. quantum/core/features/query/src/__init__.py +0 -0
  70. quantum/core/features/query/src/database_service.py +322 -0
  71. quantum/core/features/query/src/query_validators.py +20 -0
  72. quantum/core/features/state_management/src/__init__.py +11 -0
  73. quantum/core/features/state_management/src/ast_node.py +228 -0
  74. quantum/core/features/terminal_engine/src/__init__.py +21 -0
  75. quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
  76. quantum/core/features/terminal_engine/src/parser.py +361 -0
  77. quantum/core/features/testing_engine/src/__init__.py +41 -0
  78. quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
  79. quantum/core/features/testing_engine/src/parser.py +604 -0
  80. quantum/core/features/theming/src/__init__.py +48 -0
  81. quantum/core/features/theming/src/ast_node.py +137 -0
  82. quantum/core/features/theming/src/presets.py +405 -0
  83. quantum/core/features/ui_engine/src/__init__.py +20 -0
  84. quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
  85. quantum/core/features/ui_engine/src/parser.py +1106 -0
  86. quantum/core/features/websocket/src/__init__.py +24 -0
  87. quantum/core/features/websocket/src/ast_node.py +247 -0
  88. quantum/core/html_compat.py +299 -0
  89. quantum/core/parser.py +1235 -0
  90. quantum/core/parser_registry.py +213 -0
  91. quantum/core/parsers/__init__.py +76 -0
  92. quantum/core/parsers/ai/__init__.py +12 -0
  93. quantum/core/parsers/ai/agent_parser.py +106 -0
  94. quantum/core/parsers/ai/knowledge_parser.py +86 -0
  95. quantum/core/parsers/ai/llm_parser.py +78 -0
  96. quantum/core/parsers/ai/team_parser.py +88 -0
  97. quantum/core/parsers/base.py +322 -0
  98. quantum/core/parsers/composition/__init__.py +10 -0
  99. quantum/core/parsers/composition/import_parser.py +50 -0
  100. quantum/core/parsers/composition/slot_parser.py +48 -0
  101. quantum/core/parsers/control_flow/__init__.py +11 -0
  102. quantum/core/parsers/control_flow/if_parser.py +68 -0
  103. quantum/core/parsers/control_flow/loop_parser.py +105 -0
  104. quantum/core/parsers/control_flow/set_parser.py +89 -0
  105. quantum/core/parsers/data/__init__.py +12 -0
  106. quantum/core/parsers/data/data_parser.py +217 -0
  107. quantum/core/parsers/data/invoke_parser.py +116 -0
  108. quantum/core/parsers/data/query_parser.py +188 -0
  109. quantum/core/parsers/data/transaction_parser.py +77 -0
  110. quantum/core/parsers/events/__init__.py +9 -0
  111. quantum/core/parsers/events/dispatch_event_parser.py +44 -0
  112. quantum/core/parsers/forms/__init__.py +11 -0
  113. quantum/core/parsers/forms/action_parser.py +54 -0
  114. quantum/core/parsers/forms/flash_parser.py +34 -0
  115. quantum/core/parsers/forms/redirect_parser.py +33 -0
  116. quantum/core/parsers/functions/__init__.py +11 -0
  117. quantum/core/parsers/functions/function_parser.py +137 -0
  118. quantum/core/parsers/functions/param_parser.py +66 -0
  119. quantum/core/parsers/functions/return_parser.py +31 -0
  120. quantum/core/parsers/html/__init__.py +10 -0
  121. quantum/core/parsers/html/component_call_parser.py +130 -0
  122. quantum/core/parsers/html/html_parser.py +115 -0
  123. quantum/core/parsers/jobs/__init__.py +11 -0
  124. quantum/core/parsers/jobs/job_parser.py +71 -0
  125. quantum/core/parsers/jobs/schedule_parser.py +62 -0
  126. quantum/core/parsers/jobs/thread_parser.py +57 -0
  127. quantum/core/parsers/messaging/__init__.py +17 -0
  128. quantum/core/parsers/messaging/message_ack_parser.py +30 -0
  129. quantum/core/parsers/messaging/message_nack_parser.py +30 -0
  130. quantum/core/parsers/messaging/message_parser.py +114 -0
  131. quantum/core/parsers/messaging/queue_parser.py +61 -0
  132. quantum/core/parsers/messaging/websocket_parser.py +121 -0
  133. quantum/core/parsers/persistence/__init__.py +9 -0
  134. quantum/core/parsers/persistence/persist_parser.py +64 -0
  135. quantum/core/parsers/routing/__init__.py +9 -0
  136. quantum/core/parsers/routing/route_parser.py +41 -0
  137. quantum/core/parsers/scripting/__init__.py +12 -0
  138. quantum/core/parsers/scripting/pyclass_parser.py +62 -0
  139. quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
  140. quantum/core/parsers/scripting/pyimport_parser.py +52 -0
  141. quantum/core/parsers/scripting/python_parser.py +49 -0
  142. quantum/core/parsers/services/__init__.py +12 -0
  143. quantum/core/parsers/services/dump_parser.py +52 -0
  144. quantum/core/parsers/services/file_parser.py +48 -0
  145. quantum/core/parsers/services/log_parser.py +46 -0
  146. quantum/core/parsers/services/mail_parser.py +65 -0
  147. quantum/core/tiers.py +82 -0
  148. quantum/packages/__init__.py +28 -0
  149. quantum/packages/manager.py +413 -0
  150. quantum/packages/manifest.py +351 -0
  151. quantum/packages/registry.py +399 -0
  152. quantum/packages/resolver.py +336 -0
  153. quantum/plugins/__init__.py +33 -0
  154. quantum/plugins/hooks.py +329 -0
  155. quantum/plugins/loader.py +479 -0
  156. quantum/plugins/manifest.py +336 -0
  157. quantum/plugins/registry.py +371 -0
  158. quantum/runtime/__init__.py +28 -0
  159. quantum/runtime/action_handler.py +443 -0
  160. quantum/runtime/adapters/__init__.py +88 -0
  161. quantum/runtime/adapters/memory_adapter.py +690 -0
  162. quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
  163. quantum/runtime/adapters/redis_adapter.py +582 -0
  164. quantum/runtime/adapters/sqlite_adapter.py +414 -0
  165. quantum/runtime/agent_service.py +1133 -0
  166. quantum/runtime/api_server.py +86 -0
  167. quantum/runtime/ast_cache.py +506 -0
  168. quantum/runtime/auth_service.py +267 -0
  169. quantum/runtime/component.py +990 -0
  170. quantum/runtime/component_composer.py +319 -0
  171. quantum/runtime/component_resolver.py +174 -0
  172. quantum/runtime/database_service.py +598 -0
  173. quantum/runtime/email_service.py +162 -0
  174. quantum/runtime/error_handler.py +295 -0
  175. quantum/runtime/execution_context.py +286 -0
  176. quantum/runtime/executor_registry.py +171 -0
  177. quantum/runtime/executors/__init__.py +71 -0
  178. quantum/runtime/executors/ai/__init__.py +12 -0
  179. quantum/runtime/executors/ai/agent_executor.py +217 -0
  180. quantum/runtime/executors/ai/knowledge_executor.py +114 -0
  181. quantum/runtime/executors/ai/llm_executor.py +153 -0
  182. quantum/runtime/executors/ai/team_executor.py +171 -0
  183. quantum/runtime/executors/base.py +262 -0
  184. quantum/runtime/executors/control_flow/__init__.py +11 -0
  185. quantum/runtime/executors/control_flow/if_executor.py +93 -0
  186. quantum/runtime/executors/control_flow/loop_executor.py +307 -0
  187. quantum/runtime/executors/control_flow/set_executor.py +412 -0
  188. quantum/runtime/executors/data/__init__.py +12 -0
  189. quantum/runtime/executors/data/data_executor.py +145 -0
  190. quantum/runtime/executors/data/invoke_executor.py +176 -0
  191. quantum/runtime/executors/data/query_executor.py +256 -0
  192. quantum/runtime/executors/data/transaction_executor.py +91 -0
  193. quantum/runtime/executors/jobs/__init__.py +11 -0
  194. quantum/runtime/executors/jobs/job_executor.py +190 -0
  195. quantum/runtime/executors/jobs/schedule_executor.py +132 -0
  196. quantum/runtime/executors/jobs/thread_executor.py +127 -0
  197. quantum/runtime/executors/messaging/__init__.py +17 -0
  198. quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
  199. quantum/runtime/executors/messaging/message_executor.py +174 -0
  200. quantum/runtime/executors/messaging/queue_executor.py +103 -0
  201. quantum/runtime/executors/messaging/websocket_executor.py +197 -0
  202. quantum/runtime/executors/scripting/__init__.py +11 -0
  203. quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
  204. quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
  205. quantum/runtime/executors/scripting/python_executor.py +249 -0
  206. quantum/runtime/executors/services/__init__.py +12 -0
  207. quantum/runtime/executors/services/dump_executor.py +72 -0
  208. quantum/runtime/executors/services/file_executor.py +89 -0
  209. quantum/runtime/executors/services/log_executor.py +77 -0
  210. quantum/runtime/executors/services/mail_executor.py +81 -0
  211. quantum/runtime/expression_cache.py +498 -0
  212. quantum/runtime/file_upload_service.py +326 -0
  213. quantum/runtime/function_registry.py +118 -0
  214. quantum/runtime/game_builder.py +166 -0
  215. quantum/runtime/game_code_generator.py +2371 -0
  216. quantum/runtime/game_templates.py +2006 -0
  217. quantum/runtime/godot_code_generator.py +4681 -0
  218. quantum/runtime/godot_templates.py +1449 -0
  219. quantum/runtime/job_executor.py +1599 -0
  220. quantum/runtime/knowledge_service.py +500 -0
  221. quantum/runtime/llm_cache.py +100 -0
  222. quantum/runtime/llm_providers.py +704 -0
  223. quantum/runtime/llm_service.py +287 -0
  224. quantum/runtime/logging_setup.py +140 -0
  225. quantum/runtime/message_broker.py +364 -0
  226. quantum/runtime/message_queue_service.py +571 -0
  227. quantum/runtime/param_validation.py +184 -0
  228. quantum/runtime/pypy_compat.py +315 -0
  229. quantum/runtime/python_bridge.py +698 -0
  230. quantum/runtime/query_validators.py +304 -0
  231. quantum/runtime/renderer.py +733 -0
  232. quantum/runtime/service_container.py +444 -0
  233. quantum/runtime/terminal_builder.py +76 -0
  234. quantum/runtime/terminal_code_generator.py +607 -0
  235. quantum/runtime/terminal_templates.py +243 -0
  236. quantum/runtime/testing_builder.py +77 -0
  237. quantum/runtime/testing_code_generator.py +833 -0
  238. quantum/runtime/testing_templates.py +85 -0
  239. quantum/runtime/ui_builder.py +188 -0
  240. quantum/runtime/ui_desktop_adapter.py +1730 -0
  241. quantum/runtime/ui_desktop_templates.py +307 -0
  242. quantum/runtime/ui_html_adapter.py +2691 -0
  243. quantum/runtime/ui_html_templates.py +2297 -0
  244. quantum/runtime/ui_mobile_adapter.py +1832 -0
  245. quantum/runtime/ui_mobile_templates.py +1003 -0
  246. quantum/runtime/ui_textual_adapter.py +1866 -0
  247. quantum/runtime/ui_textual_templates.py +45 -0
  248. quantum/runtime/ui_tokens.py +465 -0
  249. quantum/runtime/ui_validator.py +365 -0
  250. quantum/runtime/validators.py +256 -0
  251. quantum/runtime/web_server.py +1766 -0
  252. quantum/runtime/websocket_adapter.py +501 -0
  253. quantum/runtime/websocket_service.py +585 -0
  254. quantum/runtime/websocket_transport.py +289 -0
  255. quantum/runtime/wsgi.py +101 -0
  256. quantum/utils/__init__.py +1 -0
  257. quantum_framework-0.9.0.dist-info/METADATA +244 -0
  258. quantum_framework-0.9.0.dist-info/RECORD +262 -0
  259. quantum_framework-0.9.0.dist-info/WHEEL +5 -0
  260. quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
  261. quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
  262. quantum_framework-0.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1730 @@
1
+ """
2
+ UI Engine - Desktop Adapter (pywebview wrapper)
3
+
4
+ Generates interactive Python pywebview desktop applications with:
5
+ - Bidirectional Python-JS communication via pywebview's JS API
6
+ - Reactive state management (QuantumState)
7
+ - Generated API class from q:function nodes (QuantumAPI)
8
+ - Event transformation (on-click, on-submit, bind)
9
+ - Component bridge for the interactive widgets (toast, carousel/slide,
10
+ stepper/step, calendar, date-picker): user interaction is pushed into
11
+ QuantumState and Python can drive the widgets through QuantumAPI
12
+ """
13
+
14
+ import json
15
+ import re
16
+ from typing import List, Dict, Any, Optional
17
+
18
+ from quantum.core.ast_nodes import QuantumNode
19
+ from quantum.core.features.state_management.src.ast_node import SetNode
20
+ from quantum.core.features.functions.src.ast_node import FunctionNode
21
+ from quantum.core.features.ui_engine.src.ast_nodes import (
22
+ UICalendarNode, UICarouselNode, UIDatePickerNode, UISlideNode,
23
+ UIStepNode, UIStepperNode, UIToastNode, UIToastContainerNode,
24
+ )
25
+ from quantum.runtime.ui_html_adapter import UIHtmlAdapter, completed_step_indices
26
+ from quantum.runtime.ui_desktop_templates import (
27
+ QUANTUM_STATE_CLASS,
28
+ QUANTUM_API_CLASS,
29
+ JS_BRIDGE_CODE,
30
+ DESKTOP_TEMPLATE,
31
+ )
32
+
33
+
34
+ # ==========================================================================
35
+ # UI component bridge (toast, carousel/slide, stepper/step, calendar,
36
+ # date-picker) - injected next to JS_BRIDGE_CODE in the generated page.
37
+ #
38
+ # The markup contract used here is the one published by
39
+ # quantum/runtime/ui_html_templates.py (.q-carousel-track, .q-step-item,
40
+ # .q-calendar-day, .q-date-picker-input, .q-toast-container, ...).
41
+ # When the page also ships the shared controllers (__quantumCarousel,
42
+ # __quantumStepper, __quantumCalendar, __quantumToast) the bridge drives
43
+ # those instead of touching the DOM itself, so both paths stay in sync.
44
+ #
45
+ # __QUANTUM_UI_SPEC__ is replaced with the component descriptors collected
46
+ # from the AST.
47
+ # ==========================================================================
48
+
49
+ UI_COMPONENT_BRIDGE_JS = """\
50
+ <script>
51
+ // Quantum Desktop - UI component bridge
52
+ window.__quantumUI = (function() {
53
+ 'use strict';
54
+
55
+ var SPEC = __QUANTUM_UI_SPEC__;
56
+
57
+ var MONTHS = ['January', 'February', 'March', 'April', 'May', 'June',
58
+ 'July', 'August', 'September', 'October', 'November', 'December'];
59
+ var TOAST_ICONS = {
60
+ info: '&#9432;', success: '&#10003;', warning: '&#9888;', danger: '&#10007;'
61
+ };
62
+
63
+ var watchers = {};
64
+ var toastSeq = 0;
65
+ var toastContainers = {};
66
+
67
+ // ------------------------------------------------------------------
68
+ // Generic helpers
69
+ // ------------------------------------------------------------------
70
+
71
+ function nodes(root, selector) {
72
+ return Array.prototype.slice.call((root || document).querySelectorAll(selector));
73
+ }
74
+
75
+ function toInt(value, fallback) {
76
+ var n = parseInt(value, 10);
77
+ return isNaN(n) ? fallback : n;
78
+ }
79
+
80
+ function isTruthy(value) {
81
+ return !(value === false || value === null || value === undefined ||
82
+ value === 0 || value === '' || value === 'false' || value === '0');
83
+ }
84
+
85
+ function escapeHtml(text) {
86
+ return String(text === null || text === undefined ? '' : text)
87
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
88
+ }
89
+
90
+ function elementFor(spec, selector, excludeInside) {
91
+ if (spec.__el && document.body.contains(spec.__el)) return spec.__el;
92
+ var found = null;
93
+ if (spec.element_id) found = document.getElementById(spec.element_id);
94
+ if (!found) {
95
+ var candidates = nodes(document, selector);
96
+ // Alguns componentes do adapter html aparecem DUAS vezes com a
97
+ // mesma classe: o <ui:calendar> real e o calendario dentro do
98
+ // popup do date-picker sao ambos `.q-calendar`. Pegar por indice
99
+ // fazia o date-picker dirigir o binding de outro calendario —
100
+ // escrevendo no lugar errado, sem sinal nenhum. Quem passa
101
+ // `excludeInside` esta dizendo qual container nao conta.
102
+ if (excludeInside) {
103
+ candidates = candidates.filter(function(el) {
104
+ return !el.closest || !el.closest(excludeInside);
105
+ });
106
+ }
107
+ found = candidates[spec.index] || null;
108
+ }
109
+ spec.__el = found;
110
+ return found;
111
+ }
112
+
113
+ // Returns the handle the page controller registered for this element,
114
+ // or null when the page never initialised one.
115
+ function pageHandle(globalName, el) {
116
+ var api = window[globalName];
117
+ if (!api || !el || !el.id || typeof api.get !== 'function') return null;
118
+ return api.get(el.id) || null;
119
+ }
120
+
121
+ function pushState(name, value) {
122
+ if (!name) return;
123
+ if (window.__quantumState) window.__quantumState[name] = value;
124
+ if (window.__quantumCall) window.__quantumCall('ui_set_state', {name: name, value: value});
125
+ }
126
+
127
+ function callHandler(handler, payload) {
128
+ if (handler && window.__quantumCall) window.__quantumCall(handler, payload || {});
129
+ }
130
+
131
+ function watchState(name, fn) {
132
+ if (!name) return;
133
+ if (!watchers[name]) watchers[name] = [];
134
+ watchers[name].push(fn);
135
+ }
136
+
137
+ // Chain onto the desktop bridge so Python state pushes reach the widgets.
138
+ function installStateHook() {
139
+ var previous = window.__quantumStateUpdate;
140
+ window.__quantumStateUpdate = function(name, value) {
141
+ if (previous) previous(name, value);
142
+ var list = watchers[name] || [];
143
+ list.forEach(function(fn) {
144
+ try { fn(value); } catch (err) { console.warn('Quantum UI watcher failed:', err); }
145
+ });
146
+ };
147
+ }
148
+
149
+ function findSpec(list, ref) {
150
+ if (ref === undefined || ref === null || ref === '') return list[0] || null;
151
+ if (typeof ref === 'number') return list[ref] || null;
152
+ for (var i = 0; i < list.length; i++) {
153
+ if (list[i].element_id === ref || list[i].bind === ref) return list[i];
154
+ }
155
+ return list[toInt(ref, -1)] || null;
156
+ }
157
+
158
+ // ------------------------------------------------------------------
159
+ // ui:toast-container / ui:toast
160
+ // ------------------------------------------------------------------
161
+
162
+ function defaultToastPosition() {
163
+ if (SPEC.toast_containers.length) return SPEC.toast_containers[0].position;
164
+ if (SPEC.toasts.length) return SPEC.toasts[0].position;
165
+ return 'top-right';
166
+ }
167
+
168
+ function maxToastsFor(position) {
169
+ for (var i = 0; i < SPEC.toast_containers.length; i++) {
170
+ if (SPEC.toast_containers[i].position === position) {
171
+ return SPEC.toast_containers[i].max_toasts;
172
+ }
173
+ }
174
+ return SPEC.toast_containers.length ? SPEC.toast_containers[0].max_toasts : 0;
175
+ }
176
+
177
+ function toastContainer(position) {
178
+ if (toastContainers[position] && document.body.contains(toastContainers[position])) {
179
+ return toastContainers[position];
180
+ }
181
+ var el = document.querySelector('.q-toast-container.q-toast-' + position);
182
+ if (!el) {
183
+ el = document.createElement('div');
184
+ el.className = 'q-toast-container q-toast-' + position;
185
+ document.body.appendChild(el);
186
+ }
187
+ toastContainers[position] = el;
188
+ return el;
189
+ }
190
+
191
+ function toastShow(options) {
192
+ options = options || {};
193
+ var position = options.position || defaultToastPosition();
194
+
195
+ if (window.__quantumToast && window.__quantumToast.show) {
196
+ var forwarded = {};
197
+ for (var key in options) {
198
+ if (Object.prototype.hasOwnProperty.call(options, key)) forwarded[key] = options[key];
199
+ }
200
+ forwarded.position = position;
201
+ return window.__quantumToast.show(forwarded);
202
+ }
203
+
204
+ // Page has no toast controller: build the toast from the markup contract.
205
+ var container = toastContainer(position);
206
+ var id = 'q-desktop-toast-' + (++toastSeq);
207
+ var toast = document.createElement('div');
208
+ toast.id = id;
209
+ toast.className = 'q-toast q-toast-' + (options.variant || 'info');
210
+
211
+ var icon = options.icon || TOAST_ICONS[options.variant] || TOAST_ICONS.info;
212
+ var html = '';
213
+ if (options.icon !== false) html += '<span class="q-toast-icon">' + icon + '</span>';
214
+ html += '<div class="q-toast-content">';
215
+ if (options.title) html += '<div class="q-toast-title">' + escapeHtml(options.title) + '</div>';
216
+ html += '<div class="q-toast-message">' + escapeHtml(options.message || '') + '</div>';
217
+ html += '</div>';
218
+ if (options.dismissible !== false) {
219
+ html += '<button type="button" class="q-toast-close">&times;</button>';
220
+ }
221
+ toast.innerHTML = html;
222
+
223
+ var closeBtn = toast.querySelector('.q-toast-close');
224
+ if (closeBtn) {
225
+ closeBtn.addEventListener('click', function() {
226
+ toastDismiss(id);
227
+ callHandler(options.on_close, {id: id});
228
+ });
229
+ }
230
+
231
+ container.appendChild(toast);
232
+
233
+ var max = maxToastsFor(position);
234
+ if (max > 0) {
235
+ var visible = nodes(container, '.q-toast');
236
+ while (visible.length > max) { toastDismiss(visible.shift().id); }
237
+ }
238
+
239
+ var duration = options.duration === undefined ? 3000 : options.duration;
240
+ if (duration > 0) {
241
+ setTimeout(function() {
242
+ toastDismiss(id);
243
+ callHandler(options.on_close, {id: id});
244
+ }, duration);
245
+ }
246
+ return id;
247
+ }
248
+
249
+ function toastDismiss(id) {
250
+ if (!id) return;
251
+ if (window.__quantumToast && window.__quantumToast.dismiss &&
252
+ String(id).indexOf('q-desktop-toast-') !== 0) {
253
+ return window.__quantumToast.dismiss(id);
254
+ }
255
+ var el = document.getElementById(id);
256
+ if (!el) return;
257
+ el.classList.add('q-toast-out');
258
+ setTimeout(function() { if (el.parentNode) el.parentNode.removeChild(el); }, 300);
259
+ }
260
+
261
+ function toastDismissAll() {
262
+ if (window.__quantumToast && window.__quantumToast.dismissAll) {
263
+ window.__quantumToast.dismissAll();
264
+ }
265
+ nodes(document, '.q-toast-container .q-toast').forEach(function(t) { toastDismiss(t.id); });
266
+ }
267
+
268
+ function toastOptions(spec) {
269
+ return {
270
+ variant: spec.variant,
271
+ title: spec.title,
272
+ message: spec.message,
273
+ position: spec.position,
274
+ duration: spec.duration,
275
+ dismissible: spec.dismissible,
276
+ icon: spec.icon || undefined,
277
+ on_close: spec.on_close
278
+ };
279
+ }
280
+
281
+ function toastWire(spec) {
282
+ // An inline <ui:toast> is rendered by the HTML adapter; a bound one is
283
+ // toggled by Python state, an unbound one auto-dismisses like a toast.
284
+ var el = elementFor(spec, '.q-toast');
285
+
286
+ function setVisible(visible) {
287
+ if (spec.__visible === visible) return;
288
+ spec.__visible = visible;
289
+ if (el) {
290
+ el.style.display = visible ? '' : 'none';
291
+ if (visible) el.classList.remove('q-toast-out');
292
+ } else if (visible) {
293
+ spec.__floating = toastShow(toastOptions(spec));
294
+ } else if (spec.__floating) {
295
+ toastDismiss(spec.__floating);
296
+ spec.__floating = null;
297
+ }
298
+ if (visible && spec.duration > 0) {
299
+ setTimeout(function() {
300
+ if (!spec.__visible) return;
301
+ setVisible(false);
302
+ if (spec.show) pushState(spec.show, false);
303
+ callHandler(spec.on_close, {});
304
+ }, spec.duration);
305
+ }
306
+ }
307
+
308
+ if (el) {
309
+ var closeBtn = el.querySelector('.q-toast-close');
310
+ if (closeBtn) {
311
+ closeBtn.addEventListener('click', function() {
312
+ setVisible(false);
313
+ if (spec.show) pushState(spec.show, false);
314
+ callHandler(spec.on_close, {});
315
+ });
316
+ }
317
+ }
318
+
319
+ if (spec.show) {
320
+ spec.__visible = true;
321
+ setVisible(false);
322
+ watchState(spec.show, function(value) { setVisible(isTruthy(value)); });
323
+ } else {
324
+ spec.__visible = false;
325
+ setVisible(true);
326
+ }
327
+ }
328
+
329
+ // ------------------------------------------------------------------
330
+ // ui:carousel / ui:slide
331
+ // ------------------------------------------------------------------
332
+
333
+ function carouselTotal(el, spec) {
334
+ var count = nodes(el, '.q-carousel-slide').length;
335
+ return count || spec.slides.length;
336
+ }
337
+
338
+ function normalizeIndex(index, total, loop) {
339
+ if (!total) return 0;
340
+ if (index < 0) return loop ? total - 1 : 0;
341
+ if (index >= total) return loop ? 0 : total - 1;
342
+ return index;
343
+ }
344
+
345
+ function carouselApply(el, index) {
346
+ var slides = nodes(el, '.q-carousel-slide');
347
+ var fade = el.classList.contains('q-carousel-fade');
348
+ var track = el.querySelector('.q-carousel-track');
349
+ if (fade) {
350
+ slides.forEach(function(slide, i) { slide.classList.toggle('active', i === index); });
351
+ } else if (track) {
352
+ track.style.transform = 'translateX(-' + (index * 100) + '%)';
353
+ }
354
+ nodes(el, '.q-carousel-indicator').forEach(function(ind, i) {
355
+ ind.classList.toggle('active', i === index);
356
+ });
357
+ }
358
+
359
+ function carouselCurrent(el, spec) {
360
+ var indicators = nodes(el, '.q-carousel-indicator');
361
+ for (var i = 0; i < indicators.length; i++) {
362
+ if (indicators[i].classList.contains('active')) return i;
363
+ }
364
+ var slides = nodes(el, '.q-carousel-slide');
365
+ for (var k = 0; k < slides.length; k++) {
366
+ if (slides[k].classList.contains('active')) return k;
367
+ }
368
+ var track = el.querySelector('.q-carousel-track');
369
+ if (track && track.style.transform) {
370
+ var match = /translateX\\(\\s*-?([0-9.]+)%/.exec(track.style.transform);
371
+ if (match) return Math.round(parseFloat(match[1]) / 100);
372
+ }
373
+ return spec.current;
374
+ }
375
+
376
+ function carouselCommit(spec, index, notify) {
377
+ var changed = spec.current !== index;
378
+ spec.current = index;
379
+ if (!changed && !notify) return index;
380
+ if (changed) {
381
+ if (spec.bind) pushState(spec.bind, index);
382
+ callHandler(spec.on_change, {index: index});
383
+ }
384
+ return index;
385
+ }
386
+
387
+ function carouselGoTo(ref, index) {
388
+ var spec = findSpec(SPEC.carousels, ref);
389
+ if (!spec) return -1;
390
+ var el = elementFor(spec, '.q-carousel');
391
+ if (!el) return -1;
392
+ var total = carouselTotal(el, spec);
393
+ if (!total) return -1;
394
+
395
+ var target = normalizeIndex(toInt(index, spec.current), total, spec.loop);
396
+ var handle = pageHandle('__quantumCarousel', el);
397
+ if (handle && typeof handle.goTo === 'function') {
398
+ handle.goTo(target);
399
+ } else {
400
+ var indicators = nodes(el, '.q-carousel-indicator');
401
+ // Reuse the page controller's own listeners when it owns the DOM.
402
+ if (handle && indicators[target]) indicators[target].click();
403
+ if (carouselCurrent(el, spec) !== target) carouselApply(el, target);
404
+ }
405
+ return carouselCommit(spec, target, false);
406
+ }
407
+
408
+ function carouselStep(ref, delta) {
409
+ var spec = findSpec(SPEC.carousels, ref);
410
+ if (!spec) return -1;
411
+ return carouselGoTo(spec.element_id || spec.index, spec.current + delta);
412
+ }
413
+
414
+ function carouselShowSlide(slideRef) {
415
+ for (var i = 0; i < SPEC.carousels.length; i++) {
416
+ var slides = SPEC.carousels[i].slides;
417
+ for (var s = 0; s < slides.length; s++) {
418
+ if (slides[s].element_id && slides[s].element_id === slideRef) {
419
+ return carouselGoTo(i, s);
420
+ }
421
+ }
422
+ }
423
+ return -1;
424
+ }
425
+
426
+ function carouselAutoPlay(spec, el) {
427
+ if (!spec.auto_play || spec.interval <= 0) return;
428
+ if (pageHandle('__quantumCarousel', el)) return; // page controller already advances it
429
+ var timer = null;
430
+ function start() {
431
+ if (timer) return;
432
+ timer = setInterval(function() { carouselStep(spec.element_id || spec.index, 1); }, spec.interval);
433
+ }
434
+ function stop() { if (timer) { clearInterval(timer); timer = null; } }
435
+ el.addEventListener('mouseenter', stop);
436
+ el.addEventListener('mouseleave', start);
437
+ start();
438
+ }
439
+
440
+ function carouselWire(spec) {
441
+ var el = elementFor(spec, '.q-carousel');
442
+ if (!el) return;
443
+ var driven = !!pageHandle('__quantumCarousel', el);
444
+ var total = carouselTotal(el, spec);
445
+ var ref = spec.element_id || spec.index;
446
+
447
+ if (driven) {
448
+ // The page owns the widget: watch the controls and mirror the result.
449
+ el.addEventListener('click', function(ev) {
450
+ var hit = ev.target && ev.target.closest
451
+ ? ev.target.closest('.q-carousel-indicator, .q-carousel-prev, .q-carousel-next')
452
+ : null;
453
+ if (!hit) return;
454
+ setTimeout(function() { carouselCommit(spec, carouselCurrent(el, spec), false); }, 0);
455
+ });
456
+ } else {
457
+ var prevBtn = el.querySelector('.q-carousel-prev');
458
+ var nextBtn = el.querySelector('.q-carousel-next');
459
+ if (prevBtn) prevBtn.addEventListener('click', function() { carouselStep(ref, -1); });
460
+ if (nextBtn) nextBtn.addEventListener('click', function() { carouselStep(ref, 1); });
461
+ nodes(el, '.q-carousel-indicator').forEach(function(ind, i) {
462
+ ind.addEventListener('click', function() { carouselGoTo(ref, i); });
463
+ });
464
+ carouselApply(el, normalizeIndex(spec.current, total, spec.loop));
465
+ }
466
+
467
+ if (spec.bind) {
468
+ watchState(spec.bind, function(value) {
469
+ var index = toInt(value, spec.current);
470
+ if (index !== spec.current) carouselGoTo(ref, index);
471
+ });
472
+ }
473
+
474
+ carouselAutoPlay(spec, el);
475
+ }
476
+
477
+ // ------------------------------------------------------------------
478
+ // ui:stepper / ui:step
479
+ // ------------------------------------------------------------------
480
+
481
+ function stepperItems(el) {
482
+ return nodes(el, '.q-step-item');
483
+ }
484
+
485
+ function stepperApply(el, index) {
486
+ stepperItems(el).forEach(function(item, i) {
487
+ item.classList.toggle('active', i === index);
488
+ });
489
+ nodes(el, '.q-step-content').forEach(function(content, i) {
490
+ content.classList.toggle('active', i === index);
491
+ });
492
+ }
493
+
494
+ function stepperCurrent(el, spec) {
495
+ var items = stepperItems(el);
496
+ for (var i = 0; i < items.length; i++) {
497
+ if (items[i].classList.contains('active')) return i;
498
+ }
499
+ return spec.current;
500
+ }
501
+
502
+ function stepperCanGoTo(spec, index) {
503
+ if (!spec.linear) return true;
504
+ if (index <= spec.current) return true;
505
+ return index === spec.current + 1 && spec.completed.indexOf(spec.current) !== -1;
506
+ }
507
+
508
+ function stepperMarkStates(el, spec) {
509
+ // ui:step attributes that describe the initial state of each step.
510
+ stepperItems(el).forEach(function(item, i) {
511
+ var step = spec.steps[i];
512
+ if (!step) return;
513
+ if (step.completed && spec.completed.indexOf(i) === -1) spec.completed.push(i);
514
+ item.classList.toggle('completed', spec.completed.indexOf(i) !== -1 && i !== spec.current);
515
+ if (step.error) {
516
+ item.classList.add('error');
517
+ item.setAttribute('title', step.error);
518
+ }
519
+ if (step.optional) item.setAttribute('data-optional', 'true');
520
+ });
521
+ }
522
+
523
+ function stepperCommit(spec, index, notify) {
524
+ var changed = spec.current !== index;
525
+ spec.current = index;
526
+ if (changed || notify) {
527
+ if (spec.bind) pushState(spec.bind, index);
528
+ callHandler(spec.on_change, {index: index});
529
+ }
530
+ return index;
531
+ }
532
+
533
+ function stepperGoTo(ref, index, force) {
534
+ var spec = findSpec(SPEC.steppers, ref);
535
+ if (!spec) return -1;
536
+ var el = elementFor(spec, '.q-stepper');
537
+ if (!el) return -1;
538
+ var total = stepperItems(el).length || spec.steps.length;
539
+ var target = toInt(index, spec.current);
540
+ if (!total || target < 0 || target >= total) return -1;
541
+ if (!force && !stepperCanGoTo(spec, target)) return -1;
542
+
543
+ var handle = pageHandle('__quantumStepper', el);
544
+ if (handle && typeof handle.goTo === 'function') {
545
+ handle.goTo(target);
546
+ } else {
547
+ stepperApply(el, target);
548
+ stepperMarkStates(el, spec);
549
+ }
550
+ return stepperCommit(spec, target, false);
551
+ }
552
+
553
+ function stepperComplete(ref, index) {
554
+ var spec = findSpec(SPEC.steppers, ref);
555
+ if (!spec) return -1;
556
+ var el = elementFor(spec, '.q-stepper');
557
+ if (!el) return -1;
558
+ var target = index === undefined || index === null ? spec.current : toInt(index, spec.current);
559
+ if (spec.completed.indexOf(target) === -1) spec.completed.push(target);
560
+
561
+ var handle = pageHandle('__quantumStepper', el);
562
+ if (handle && typeof handle.complete === 'function') {
563
+ handle.complete(target);
564
+ } else {
565
+ var item = stepperItems(el)[target];
566
+ if (item) {
567
+ item.classList.remove('error');
568
+ if (target !== spec.current) item.classList.add('completed');
569
+ }
570
+ }
571
+
572
+ var total = stepperItems(el).length || spec.steps.length;
573
+ if (total && spec.completed.length >= total) callHandler(spec.on_complete, {});
574
+ return target;
575
+ }
576
+
577
+ function stepperStep(ref, delta) {
578
+ var spec = findSpec(SPEC.steppers, ref);
579
+ if (!spec) return -1;
580
+ return stepperGoTo(spec.element_id || spec.index, spec.current + delta, delta < 0);
581
+ }
582
+
583
+ function stepperShowStep(stepRef) {
584
+ for (var i = 0; i < SPEC.steppers.length; i++) {
585
+ var steps = SPEC.steppers[i].steps;
586
+ for (var s = 0; s < steps.length; s++) {
587
+ if (steps[s].element_id && steps[s].element_id === stepRef) {
588
+ return stepperGoTo(i, s, true);
589
+ }
590
+ }
591
+ }
592
+ return -1;
593
+ }
594
+
595
+ function stepperWire(spec) {
596
+ var el = elementFor(spec, '.q-stepper');
597
+ if (!el) return;
598
+ var driven = !!pageHandle('__quantumStepper', el);
599
+ var ref = spec.element_id || spec.index;
600
+
601
+ stepperMarkStates(el, spec);
602
+ if (!driven) stepperApply(el, spec.current);
603
+
604
+ if (driven) {
605
+ el.addEventListener('click', function(ev) {
606
+ var hit = ev.target && ev.target.closest ? ev.target.closest('.q-step-item') : null;
607
+ if (!hit) return;
608
+ setTimeout(function() { stepperCommit(spec, stepperCurrent(el, spec), false); }, 0);
609
+ });
610
+ } else if (spec.clickable) {
611
+ stepperItems(el).forEach(function(item, i) {
612
+ item.style.cursor = 'pointer';
613
+ item.addEventListener('click', function() { stepperGoTo(ref, i); });
614
+ });
615
+ }
616
+
617
+ if (spec.bind) {
618
+ watchState(spec.bind, function(value) {
619
+ var index = toInt(value, spec.current);
620
+ if (index !== spec.current) stepperGoTo(ref, index, true);
621
+ });
622
+ }
623
+ }
624
+
625
+ // ------------------------------------------------------------------
626
+ // ui:calendar
627
+ // ------------------------------------------------------------------
628
+
629
+ function pad2(n) { return (n < 10 ? '0' : '') + n; }
630
+
631
+ // True only when the page registered a calendar handle that can select and
632
+ // repaint on its own; a bare state entry is not enough to own the widget.
633
+ function calendarDriven(el) {
634
+ var handle = pageHandle('__quantumCalendar', el);
635
+ return !!(handle && typeof handle.selectDate === 'function' &&
636
+ typeof handle.render === 'function');
637
+ }
638
+
639
+ function calendarView(el) {
640
+ // Reads the month currently rendered in the calendar header.
641
+ var title = el.querySelector('.q-calendar-title');
642
+ var now = new Date();
643
+ if (!title) return {month: now.getMonth(), year: now.getFullYear()};
644
+ var parts = String(title.textContent || '').trim().split(/\\s+/);
645
+ var month = MONTHS.indexOf(parts[0]);
646
+ var year = toInt(parts[1], now.getFullYear());
647
+ return {month: month === -1 ? now.getMonth() : month, year: year};
648
+ }
649
+
650
+ function calendarIso(el, day) {
651
+ var view = calendarView(el);
652
+ return view.year + '-' + pad2(view.month + 1) + '-' + pad2(day);
653
+ }
654
+
655
+ function calendarValue(spec) {
656
+ if (spec.mode === 'range') return {start: spec.range_start || '', end: spec.range_end || ''};
657
+ if (spec.mode === 'multiple') return spec.multiple.slice();
658
+ return spec.selected || '';
659
+ }
660
+
661
+ function calendarSelectIso(spec, iso) {
662
+ if (spec.mode === 'single') {
663
+ spec.selected = iso;
664
+ } else if (spec.mode === 'range') {
665
+ if (!spec.range_start || spec.range_end) {
666
+ spec.range_start = iso;
667
+ spec.range_end = '';
668
+ } else if (iso < spec.range_start) {
669
+ spec.range_end = spec.range_start;
670
+ spec.range_start = iso;
671
+ } else {
672
+ spec.range_end = iso;
673
+ }
674
+ } else {
675
+ var at = spec.multiple.indexOf(iso);
676
+ if (at === -1) spec.multiple.push(iso); else spec.multiple.splice(at, 1);
677
+ }
678
+ }
679
+
680
+ function calendarPaint(el, spec) {
681
+ // Only used when the page has no calendar controller repainting itself.
682
+ var view = calendarView(el);
683
+ nodes(el, '.q-calendar-day').forEach(function(cell) {
684
+ if (cell.classList.contains('other-month') || cell.classList.contains('week-number')) return;
685
+ var day = toInt(cell.textContent, 0);
686
+ if (!day) return;
687
+ var iso = view.year + '-' + pad2(view.month + 1) + '-' + pad2(day);
688
+ cell.classList.remove('selected', 'range-start', 'range-end', 'in-range');
689
+ if (spec.mode === 'single') {
690
+ cell.classList.toggle('selected', iso === spec.selected);
691
+ } else if (spec.mode === 'range') {
692
+ if (iso === spec.range_start) cell.classList.add('range-start', 'selected');
693
+ if (iso === spec.range_end) cell.classList.add('range-end', 'selected');
694
+ if (spec.range_start && spec.range_end && iso > spec.range_start && iso < spec.range_end) {
695
+ cell.classList.add('in-range');
696
+ }
697
+ } else if (spec.multiple.indexOf(iso) !== -1) {
698
+ cell.classList.add('selected');
699
+ }
700
+ });
701
+ }
702
+
703
+ function calendarDayCell(el, iso) {
704
+ var view = calendarView(el);
705
+ var prefix = view.year + '-' + pad2(view.month + 1) + '-';
706
+ if (iso.indexOf(prefix) !== 0) return null;
707
+ var day = toInt(iso.slice(prefix.length), 0);
708
+ var match = null;
709
+ nodes(el, '.q-calendar-day').forEach(function(cell) {
710
+ if (match || cell.classList.contains('other-month')) return;
711
+ if (toInt(cell.textContent, -1) === day) match = cell;
712
+ });
713
+ return match;
714
+ }
715
+
716
+ function calendarNavigateTo(el, iso) {
717
+ // Walk the header buttons until the wanted month is on screen.
718
+ var buttons = nodes(el, '.q-calendar-nav, .q-calendar-nav button, .q-calendar-header button');
719
+ if (buttons.length < 2) return false;
720
+ var back = buttons[0];
721
+ var forward = buttons[buttons.length - 1];
722
+ var target = toInt(iso.slice(0, 4), 0) * 12 + toInt(iso.slice(5, 7), 1) - 1;
723
+ for (var guard = 0; guard < 36; guard++) {
724
+ var view = calendarView(el);
725
+ var current = view.year * 12 + view.month;
726
+ if (current === target) return true;
727
+ (current > target ? back : forward).click();
728
+ if (calendarView(el).year * 12 + calendarView(el).month === current) return false;
729
+ }
730
+ return false;
731
+ }
732
+
733
+ function calendarCommit(spec, el) {
734
+ var value = calendarValue(spec);
735
+ if (spec.bind) pushState(spec.bind, value);
736
+ callHandler(spec.on_change, {value: value});
737
+ if (!calendarDriven(el)) calendarPaint(el, spec);
738
+ return value;
739
+ }
740
+
741
+ function calendarSelect(ref, value) {
742
+ var spec = findSpec(SPEC.calendars, ref);
743
+ if (!spec) return null;
744
+ var el = elementFor(spec, '.q-calendar', '.q-date-picker-dropdown');
745
+ if (!el) return null;
746
+ var iso = String(value === null || value === undefined ? '' : value);
747
+ if (!iso) {
748
+ spec.selected = '';
749
+ spec.range_start = '';
750
+ spec.range_end = '';
751
+ spec.multiple = [];
752
+ return calendarCommit(spec, el);
753
+ }
754
+ calendarNavigateTo(el, iso);
755
+ var cell = calendarDayCell(el, iso);
756
+ if (cell && calendarDriven(el)) {
757
+ cell.click(); // let the page controller own the selection
758
+ }
759
+ calendarSelectIso(spec, iso);
760
+ return calendarCommit(spec, el);
761
+ }
762
+
763
+ function calendarWire(spec) {
764
+ var el = elementFor(spec, '.q-calendar', '.q-date-picker-dropdown');
765
+ if (!el) return;
766
+
767
+ el.addEventListener('click', function(ev) {
768
+ var target = ev.target;
769
+ if (!target || !target.classList) return;
770
+
771
+ if (target.closest && target.closest('.q-calendar-nav, .q-calendar-header button')) {
772
+ setTimeout(function() {
773
+ var view = calendarView(el);
774
+ callHandler(spec.on_month_change, {month: view.month, year: view.year});
775
+ if (!calendarDriven(el)) calendarPaint(el, spec);
776
+ }, 0);
777
+ return;
778
+ }
779
+
780
+ var cell = target.classList.contains('q-calendar-day')
781
+ ? target
782
+ : (target.closest ? target.closest('.q-calendar-day') : null);
783
+ if (!cell) return;
784
+ if (cell.classList.contains('other-month') || cell.classList.contains('disabled') ||
785
+ cell.classList.contains('week-number')) return;
786
+ var day = toInt(cell.textContent, 0);
787
+ if (!day) return;
788
+ calendarSelectIso(spec, calendarIso(el, day));
789
+ calendarCommit(spec, el);
790
+ });
791
+
792
+ if (spec.bind) {
793
+ watchState(spec.bind, function(value) {
794
+ if (value === null || value === undefined) return;
795
+ var iso = typeof value === 'object' ? (value.start || '') : String(value);
796
+ if (!iso) return;
797
+ if (spec.mode === 'single' && iso === spec.selected) return;
798
+ if (spec.mode === 'range' && iso === spec.range_start) return;
799
+ calendarSelect(spec.element_id || spec.index, iso);
800
+ });
801
+ }
802
+
803
+ if (spec.selected || spec.range_start || spec.multiple.length) {
804
+ if (!calendarDriven(el)) calendarPaint(el, spec);
805
+ }
806
+ }
807
+
808
+ // ------------------------------------------------------------------
809
+ // ui:date-picker
810
+ // ------------------------------------------------------------------
811
+
812
+ // O date-picker tem DOIS campos: o visivel, formatado conforme
813
+ // `format` (09/15/2026), e um hidden com o valor canonico ISO
814
+ // (2026-09-15) que carrega o `name` do bind — e o que um formulario
815
+ // submete e o que o Python espera receber.
816
+ //
817
+ // Esta ponte lia e escrevia o campo VISIVEL. Com format != ISO, o
818
+ // Python recebia "09/15/2026" enquanto o campo canonico ao lado tinha
819
+ // "2026-09-15": dado corrompido em silencio, sem erro nenhum. E
820
+ // datePickerSet escrevia ISO cru no campo de exibicao, quebrando tambem
821
+ // o formato que o usuario ve.
822
+ //
823
+ // A autoridade sobre esse par de campos e __quantumDatePicker, do
824
+ // adapter html: setValue(id, iso) escreve os dois, getValue(id) le o
825
+ // canonico. Delegar e o conserto; reimplementar formatacao aqui seria
826
+ // criar uma segunda verdade.
827
+
828
+ function datePickerDisplay(spec) {
829
+ var el = elementFor(spec, '.q-date-picker');
830
+ if (!el) return null;
831
+ return el.querySelector('.q-date-picker-input') || el.querySelector('input');
832
+ }
833
+
834
+ // O campo canonico. `#<id>-value` e o que o adapter html emite; o
835
+ // input[type=hidden] e a rede de seguranca se o id mudar de forma.
836
+ function datePickerValueField(spec) {
837
+ var el = elementFor(spec, '.q-date-picker');
838
+ if (!el) return null;
839
+ return (el.id && document.getElementById(el.id + '-value')) ||
840
+ el.querySelector('input[type="hidden"]');
841
+ }
842
+
843
+ function datePickerIso(spec) {
844
+ var el = elementFor(spec, '.q-date-picker');
845
+ if (el && el.id && window.__quantumDatePicker &&
846
+ typeof __quantumDatePicker.getValue === 'function') {
847
+ return __quantumDatePicker.getValue(el.id);
848
+ }
849
+ var hidden = datePickerValueField(spec);
850
+ if (hidden) return hidden.value;
851
+ var display = datePickerDisplay(spec);
852
+ return display ? display.value : '';
853
+ }
854
+
855
+ function datePickerSet(ref, value) {
856
+ var spec = findSpec(SPEC.date_pickers, ref);
857
+ if (!spec) return null;
858
+ var iso = value === null || value === undefined ? '' : String(value);
859
+ var el = elementFor(spec, '.q-date-picker');
860
+
861
+ if (el && el.id && window.__quantumDatePicker &&
862
+ typeof __quantumDatePicker.setValue === 'function') {
863
+ // Escreve o hidden E reformata a exibicao, numa so chamada.
864
+ __quantumDatePicker.setValue(el.id, iso);
865
+ } else {
866
+ var hidden = datePickerValueField(spec);
867
+ var display = datePickerDisplay(spec);
868
+ if (hidden) hidden.value = iso;
869
+ if (display) display.value = iso;
870
+ }
871
+
872
+ spec.value = iso;
873
+ if (spec.bind) pushState(spec.bind, iso);
874
+ callHandler(spec.on_change, {value: iso});
875
+ return iso;
876
+ }
877
+
878
+ function datePickerReport(spec) {
879
+ var iso = datePickerIso(spec);
880
+ if (iso === spec.value) return;
881
+ spec.value = iso;
882
+ if (spec.bind) pushState(spec.bind, iso);
883
+ callHandler(spec.on_change, {value: iso});
884
+ }
885
+
886
+ function datePickerWire(spec) {
887
+ var el = elementFor(spec, '.q-date-picker');
888
+ if (!el) return;
889
+ spec.value = datePickerIso(spec);
890
+
891
+ var hidden = datePickerValueField(spec);
892
+ if (hidden) hidden.addEventListener('change', function() { datePickerReport(spec); });
893
+
894
+ var display = datePickerDisplay(spec);
895
+ if (display) display.addEventListener('change', function() { datePickerReport(spec); });
896
+
897
+ var clear = el.querySelector('.q-date-picker-clear');
898
+ if (clear) {
899
+ clear.addEventListener('click', function() {
900
+ datePickerSet(spec.element_id || spec.index, '');
901
+ });
902
+ }
903
+
904
+ // A calendar rendered inside the popup reports through the picker.
905
+ el.addEventListener('click', function(ev) {
906
+ var cell = ev.target && ev.target.closest ? ev.target.closest('.q-calendar-day') : null;
907
+ if (!cell || cell.classList.contains('other-month') || cell.classList.contains('disabled')) return;
908
+ setTimeout(function() { datePickerReport(spec); }, 0);
909
+ });
910
+
911
+ if (spec.bind) {
912
+ watchState(spec.bind, function(value) {
913
+ var iso = value === null || value === undefined ? '' : String(value);
914
+ if (iso === spec.value) return;
915
+ spec.value = iso;
916
+ if (el.id && window.__quantumDatePicker &&
917
+ typeof __quantumDatePicker.setValue === 'function') {
918
+ __quantumDatePicker.setValue(el.id, iso);
919
+ } else {
920
+ var h = datePickerValueField(spec);
921
+ var d = datePickerDisplay(spec);
922
+ if (h) h.value = iso;
923
+ if (d) d.value = iso;
924
+ }
925
+ });
926
+ }
927
+ }
928
+
929
+ // ------------------------------------------------------------------
930
+ // Bootstrap
931
+ // ------------------------------------------------------------------
932
+
933
+ function init() {
934
+ SPEC.toasts.forEach(toastWire);
935
+ SPEC.carousels.forEach(carouselWire);
936
+ SPEC.steppers.forEach(stepperWire);
937
+ SPEC.calendars.forEach(calendarWire);
938
+ SPEC.date_pickers.forEach(datePickerWire);
939
+ }
940
+
941
+ installStateHook();
942
+ if (document.readyState === 'loading') {
943
+ document.addEventListener('DOMContentLoaded', init);
944
+ } else {
945
+ setTimeout(init, 0);
946
+ }
947
+
948
+ return {
949
+ spec: SPEC,
950
+ toast: toastShow,
951
+ dismissToast: toastDismiss,
952
+ dismissToasts: toastDismissAll,
953
+ carouselGoTo: carouselGoTo,
954
+ carouselNext: function(ref) { return carouselStep(ref, 1); },
955
+ carouselPrev: function(ref) { return carouselStep(ref, -1); },
956
+ showSlide: carouselShowSlide,
957
+ stepperGoTo: function(ref, index) { return stepperGoTo(ref, index, true); },
958
+ stepperNext: function(ref) { return stepperStep(ref, 1); },
959
+ stepperPrev: function(ref) { return stepperStep(ref, -1); },
960
+ stepperComplete: stepperComplete,
961
+ showStep: stepperShowStep,
962
+ calendarSelect: calendarSelect,
963
+ datePickerSet: datePickerSet
964
+ };
965
+ })();
966
+ </script>
967
+ """
968
+
969
+
970
+ class UIDesktopAdapter:
971
+ """Generates Python pywebview app with JS bridge from UI AST nodes."""
972
+
973
+ def __init__(self):
974
+ self._functions: Dict[str, FunctionNode] = {}
975
+ self._state_vars: Dict[str, Any] = {}
976
+ self._persist_config: Dict[str, Dict] = {} # name -> {scope, key, ttl, encrypt}
977
+
978
+ # Interactive component descriptors, in document order (the same order
979
+ # the HTML adapter renders them, so index-based lookup matches the DOM)
980
+ self._carousels: List[Dict[str, Any]] = []
981
+ self._steppers: List[Dict[str, Any]] = []
982
+ self._calendars: List[Dict[str, Any]] = []
983
+ self._date_pickers: List[Dict[str, Any]] = []
984
+ self._toasts: List[Dict[str, Any]] = []
985
+ self._toast_containers: List[Dict[str, Any]] = []
986
+
987
+ def generate(
988
+ self,
989
+ windows: List[QuantumNode],
990
+ ui_children: List[QuantumNode],
991
+ title: str = "Quantum UI",
992
+ functions: Optional[Dict[str, FunctionNode]] = None,
993
+ state_vars: Optional[Dict[str, Any]] = None,
994
+ persist_config: Optional[Dict[str, Dict]] = None,
995
+ width: int = 1024,
996
+ height: int = 768,
997
+ ) -> str:
998
+ """Generate Python pywebview app with JS bridge from UI AST.
999
+
1000
+ Args:
1001
+ windows: List of UIWindowNode elements.
1002
+ ui_children: List of top-level UI nodes (outside windows).
1003
+ title: Window title.
1004
+ functions: Dict of function name -> FunctionNode from q:function.
1005
+ state_vars: Dict of variable name -> initial value from q:set.
1006
+ persist_config: Dict of variable name -> persistence config.
1007
+ width: Window width in pixels.
1008
+ height: Window height in pixels.
1009
+
1010
+ Returns:
1011
+ Generated Python source code as string.
1012
+ """
1013
+ self._functions = functions or {}
1014
+ self._state_vars = dict(state_vars or {})
1015
+ self._persist_config = persist_config or {}
1016
+
1017
+ # Collect the interactive components so the bridge can wire them and
1018
+ # seed the state variables they are bound to
1019
+ self._reset_components()
1020
+ self._collect_nodes(windows)
1021
+ self._collect_nodes(ui_children)
1022
+ self._seed_component_state()
1023
+
1024
+ # Generate HTML with desktop_mode=True for event transformation
1025
+ html_adapter = UIHtmlAdapter(desktop_mode=True)
1026
+ html = html_adapter.generate(windows, ui_children, title)
1027
+
1028
+ # Inject JS bridge before </body>
1029
+ binding_script = html_adapter._generate_binding_script()
1030
+ js_injection = JS_BRIDGE_CODE
1031
+ if binding_script:
1032
+ js_injection += '\n' + binding_script
1033
+ component_script = self._generate_component_script()
1034
+ if component_script:
1035
+ js_injection += '\n' + component_script
1036
+
1037
+ html = html.replace('</body>', f'{js_injection}\n</body>')
1038
+
1039
+ # Generate state initialization code
1040
+ state_init = self._generate_state_init()
1041
+
1042
+ # Generate function methods
1043
+ function_methods = self._generate_functions()
1044
+
1045
+ # Generate persistence config as Python dict literal
1046
+ persist_config_str = self._generate_persist_config()
1047
+
1048
+ # Build the final Python code
1049
+ return DESKTOP_TEMPLATE.format(
1050
+ quantum_state_class=QUANTUM_STATE_CLASS,
1051
+ quantum_api_class=QUANTUM_API_CLASS.format(
1052
+ state_init=state_init,
1053
+ function_methods=function_methods,
1054
+ persist_config=persist_config_str,
1055
+ ),
1056
+ title=repr(title),
1057
+ width=width,
1058
+ height=height,
1059
+ html_content=repr(html),
1060
+ )
1061
+
1062
+ def _generate_persist_config(self) -> str:
1063
+ """Generate Python dict literal for persistence configuration.
1064
+
1065
+ Returns:
1066
+ String representation of the persistence config dict.
1067
+ """
1068
+ if not self._persist_config:
1069
+ return '{}'
1070
+
1071
+ parts = []
1072
+ for name, config in self._persist_config.items():
1073
+ scope = config.get('scope', 'local')
1074
+ key = config.get('key', name)
1075
+ ttl = config.get('ttl')
1076
+ encrypt = config.get('encrypt', False)
1077
+
1078
+ config_parts = [
1079
+ f"'scope': '{scope}'",
1080
+ f"'key': '{key}'",
1081
+ f"'encrypt': {encrypt}",
1082
+ ]
1083
+ if ttl:
1084
+ config_parts.append(f"'ttl': {ttl}")
1085
+
1086
+ parts.append(f"'{name}': {{{', '.join(config_parts)}}}")
1087
+
1088
+ return '{' + ', '.join(parts) + '}'
1089
+
1090
+ def _generate_state_init(self) -> str:
1091
+ """Generate Python code to initialize state variables.
1092
+
1093
+ Returns:
1094
+ Indented Python code for state initialization.
1095
+ """
1096
+ if not self._state_vars:
1097
+ return ' pass # No state variables'
1098
+
1099
+ lines = []
1100
+ for name, value in self._state_vars.items():
1101
+ # Convert value to Python representation
1102
+ py_value = self._convert_value(value)
1103
+ lines.append(f" self.state.set('{name}', {py_value})")
1104
+
1105
+ return '\n'.join(lines)
1106
+
1107
+ def _generate_functions(self) -> str:
1108
+ """Generate Python methods from q:function nodes.
1109
+
1110
+ Also appends the component API methods (toast, carousel, stepper,
1111
+ calendar, date-picker) when the app uses those components.
1112
+
1113
+ Returns:
1114
+ Python code for all function methods.
1115
+ """
1116
+ methods = []
1117
+ for func_name, func_node in self._functions.items():
1118
+ method_code = self._generate_function_method(func_name, func_node)
1119
+ methods.append(method_code)
1120
+
1121
+ component_methods = self._generate_component_methods()
1122
+ if component_methods:
1123
+ methods.append(component_methods)
1124
+
1125
+ if not methods:
1126
+ return ' pass # No functions defined'
1127
+
1128
+ return '\n\n'.join(methods)
1129
+
1130
+ def _generate_function_method(self, func_name: str, func_node: FunctionNode) -> str:
1131
+ """Generate a Python method from a q:function node.
1132
+
1133
+ Args:
1134
+ func_name: Name of the function.
1135
+ func_node: FunctionNode AST node.
1136
+
1137
+ Returns:
1138
+ Python method code as string.
1139
+ """
1140
+ lines = [f" def {func_name}(self, args=None):"]
1141
+ lines.append(f' """Generated from q:function {func_name}"""')
1142
+
1143
+ # Generate body from function statements
1144
+ if func_node.body:
1145
+ for stmt in func_node.body:
1146
+ stmt_code = self._generate_statement(stmt, func_name=func_name)
1147
+ if stmt_code:
1148
+ lines.append(stmt_code)
1149
+ else:
1150
+ lines.append(" pass")
1151
+
1152
+ return '\n'.join(lines)
1153
+
1154
+ def _generate_statement(self, stmt: QuantumNode, indent: int = 8,
1155
+ func_name: str = '?') -> str:
1156
+ """Generate Python code from a statement node.
1157
+
1158
+ Args:
1159
+ stmt: A statement node (SetNode, etc.).
1160
+ indent: Number of spaces for indentation.
1161
+ func_name: q:function being generated, for the error message.
1162
+
1163
+ Returns:
1164
+ Python code line.
1165
+ """
1166
+ pad = ' ' * indent
1167
+
1168
+ if isinstance(stmt, SetNode):
1169
+ # Handle q:set statements
1170
+ value_expr = self._convert_expression(stmt.value)
1171
+ return f"{pad}self.state.set('{stmt.name}', {value_expr})"
1172
+
1173
+ # O que este backend nao sabe traduzir vira um ERRO, nao um `pass`.
1174
+ #
1175
+ # So SetNode e tratado, e todo o resto virava
1176
+ # `pass # Unsupported statement type: ...`. O caso comum e um
1177
+ # <q:script> (que o parser entrega como HTMLNode, com JavaScript
1178
+ # dentro): a funcao inteira do usuario virava um no-op. O botao
1179
+ # chamava, a API respondia sem erro nenhum, e nada acontecia — nem
1180
+ # na tela, nem no log, nem no console.
1181
+ #
1182
+ # Levantar na CHAMADA, e nao na geracao, e de proposito: o resto do
1183
+ # aplicativo continua sendo gerado e rodando, e so a funcao que
1184
+ # depende do que falta reclama — dizendo o que falta e onde.
1185
+ rotulo = self._describe_unsupported(stmt)
1186
+ mensagem = (
1187
+ f"q:function {func_name!r}: the desktop backend cannot translate "
1188
+ f"{rotulo} — it only supports q:set. Rewrite the body with q:set, "
1189
+ f"or run this component on the web target."
1190
+ )
1191
+ return f"{pad}raise NotImplementedError({mensagem!r})"
1192
+
1193
+ @staticmethod
1194
+ def _describe_unsupported(stmt: QuantumNode) -> str:
1195
+ """Nome legivel do que nao pode ser traduzido."""
1196
+ tag = getattr(stmt, 'tag', None) or getattr(stmt, 'name', None)
1197
+ tipo = type(stmt).__name__
1198
+ if tag:
1199
+ return f"<{tag}> ({tipo})"
1200
+ return tipo
1201
+
1202
+ def _convert_expression(self, expr: str) -> str:
1203
+ """Convert a Quantum expression {var + 1} to Python.
1204
+
1205
+ Args:
1206
+ expr: Expression string, possibly with {brackets}.
1207
+
1208
+ Returns:
1209
+ Python expression string.
1210
+ """
1211
+ if expr is None:
1212
+ return 'None'
1213
+
1214
+ # Remove surrounding braces if present
1215
+ expr = expr.strip()
1216
+ if expr.startswith('{') and expr.endswith('}'):
1217
+ expr = expr[1:-1].strip()
1218
+
1219
+ # Replace variable references with self.state.get('var')
1220
+ # Pattern: word that's not a Python keyword or number
1221
+ def replace_var(match):
1222
+ var = match.group(0)
1223
+ # Skip Python keywords and built-ins
1224
+ if var in ('True', 'False', 'None', 'and', 'or', 'not', 'if', 'else', 'for', 'in', 'is'):
1225
+ return var
1226
+ # Skip if it looks like a number
1227
+ if var.isdigit():
1228
+ return var
1229
+ # Skip if it's a method call (followed by parenthesis)
1230
+ return f"self.state.get('{var}')"
1231
+
1232
+ # Match word boundaries for variable names
1233
+ result = re.sub(r'\b([a-zA-Z_][a-zA-Z0-9_]*)\b', replace_var, expr)
1234
+
1235
+ return result
1236
+
1237
+ def _convert_value(self, value: Any) -> str:
1238
+ """Convert a value to Python literal representation.
1239
+
1240
+ Args:
1241
+ value: Value to convert (can be string, number, bool, etc.).
1242
+
1243
+ Returns:
1244
+ Python literal string.
1245
+ """
1246
+ if value is None:
1247
+ return 'None'
1248
+
1249
+ # Check if it's a string representation of a number
1250
+ if isinstance(value, str):
1251
+ # Try to parse as number
1252
+ if value.isdigit():
1253
+ return value
1254
+ try:
1255
+ float(value)
1256
+ return value
1257
+ except ValueError:
1258
+ pass
1259
+
1260
+ # Boolean strings
1261
+ if value.lower() == 'true':
1262
+ return 'True'
1263
+ if value.lower() == 'false':
1264
+ return 'False'
1265
+
1266
+ # Regular string
1267
+ return repr(value)
1268
+
1269
+ # Already a Python value
1270
+ return repr(value)
1271
+
1272
+ # ------------------------------------------------------------------
1273
+ # Interactive components: collection
1274
+ # ------------------------------------------------------------------
1275
+
1276
+ def _reset_components(self):
1277
+ """Clear the collected component descriptors (generate() may re-run)."""
1278
+ self._carousels = []
1279
+ self._steppers = []
1280
+ self._calendars = []
1281
+ self._date_pickers = []
1282
+ self._toasts = []
1283
+ self._toast_containers = []
1284
+
1285
+ def _has_components(self) -> bool:
1286
+ """True when the app uses at least one bridged component."""
1287
+ return bool(self._carousels or self._steppers or self._calendars or
1288
+ self._date_pickers or self._toasts or self._toast_containers)
1289
+
1290
+ def _collect_nodes(self, nodes: Optional[List[QuantumNode]]):
1291
+ """Walk a list of nodes collecting component descriptors."""
1292
+ for node in nodes or []:
1293
+ self._collect_node(node)
1294
+
1295
+ def _collect_node(self, node: QuantumNode):
1296
+ """Collect a single node (depth-first, document order)."""
1297
+ if isinstance(node, UICarouselNode):
1298
+ self._collect_carousel(node)
1299
+ elif isinstance(node, UISlideNode):
1300
+ self._collect_slide(node)
1301
+ elif isinstance(node, UIStepperNode):
1302
+ self._collect_stepper(node)
1303
+ elif isinstance(node, UIStepNode):
1304
+ self._collect_step(node)
1305
+ elif isinstance(node, UICalendarNode):
1306
+ self._collect_calendar(node)
1307
+ elif isinstance(node, UIDatePickerNode):
1308
+ self._collect_date_picker(node)
1309
+ elif isinstance(node, UIToastNode):
1310
+ self._collect_toast(node)
1311
+ elif isinstance(node, UIToastContainerNode):
1312
+ self._collect_toast_container(node)
1313
+ else:
1314
+ self._collect_nodes(getattr(node, 'children', None))
1315
+
1316
+ def _collect_carousel(self, node: UICarouselNode):
1317
+ """Descriptor for <ui:carousel> plus the ids of its <ui:slide> children."""
1318
+ slides = [
1319
+ {'element_id': self._node_id(child)}
1320
+ for child in (node.children or [])
1321
+ if isinstance(child, UISlideNode)
1322
+ ]
1323
+ self._carousels.append({
1324
+ 'index': len(self._carousels),
1325
+ 'element_id': self._node_id(node),
1326
+ 'bind': self._bind_name(node.bind),
1327
+ 'current': self._as_int(node.current, 0),
1328
+ 'auto_play': self._as_bool(node.auto_play, False),
1329
+ 'interval': self._as_int(node.interval, 5000),
1330
+ 'loop': self._as_bool(node.loop, True),
1331
+ 'animation': node.animation or 'slide',
1332
+ 'show_arrows': self._as_bool(node.show_arrows, True),
1333
+ 'show_indicators': self._as_bool(node.show_indicators, True),
1334
+ 'on_change': node.on_change,
1335
+ 'slides': slides,
1336
+ })
1337
+ self._collect_nodes(node.children)
1338
+
1339
+ def _collect_slide(self, node: UISlideNode):
1340
+ """<ui:slide> carries no state of its own; its content may."""
1341
+ self._collect_nodes(node.children)
1342
+
1343
+ def _collect_stepper(self, node: UIStepperNode):
1344
+ """Descriptor for <ui:stepper> plus the state of each <ui:step>."""
1345
+ step_nodes = [c for c in (node.children or []) if isinstance(c, UIStepNode)]
1346
+ current = self._as_int(node.current, 0)
1347
+
1348
+ # A MESMA funcao que o adapter html usa para decidir isso. Aqui havia
1349
+ # uma segunda regra — `list(range(current)) if linear else []` — que
1350
+ # discordava dela de dois jeitos, e como esta ponte roda DEPOIS do
1351
+ # markup estatico, quem ganhava era a versao errada:
1352
+ #
1353
+ # - num stepper nao-linear ela emitia lista vazia e apagava, no boot,
1354
+ # as marcas que o html tinha desenhado certo;
1355
+ # - `_as_bool(child.completed, False)` colapsava "nao setado" e
1356
+ # "setado como false", entao um passo com completed="false" antes
1357
+ # do atual ganhava a marca de volta.
1358
+ concluidos = completed_step_indices(step_nodes, current)
1359
+
1360
+ steps = []
1361
+ for i, child in enumerate(step_nodes):
1362
+ steps.append({
1363
+ 'element_id': self._node_id(child),
1364
+ 'title': child.title,
1365
+ 'description': child.description,
1366
+ 'optional': self._as_bool(child.optional, False),
1367
+ 'completed': i in concluidos,
1368
+ 'error': child.error,
1369
+ })
1370
+ self._steppers.append({
1371
+ 'index': len(self._steppers),
1372
+ 'element_id': self._node_id(node),
1373
+ 'bind': self._bind_name(node.bind),
1374
+ 'current': current,
1375
+ 'orientation': node.orientation or 'horizontal',
1376
+ 'linear': self._as_bool(node.linear, True),
1377
+ 'clickable': self._as_bool(node.clickable, False),
1378
+ 'show_labels': self._as_bool(node.show_labels, True),
1379
+ 'on_change': node.on_change,
1380
+ 'on_complete': node.on_complete,
1381
+ 'steps': steps,
1382
+ 'completed': concluidos,
1383
+ })
1384
+ self._collect_nodes(node.children)
1385
+
1386
+ def _collect_step(self, node: UIStepNode):
1387
+ """<ui:step> state is recorded by its stepper; recurse into content."""
1388
+ self._collect_nodes(node.children)
1389
+
1390
+ def _collect_calendar(self, node: UICalendarNode):
1391
+ """Descriptor for <ui:calendar>."""
1392
+ mode = node.mode if node.mode in UICalendarNode.VALID_MODES else 'single'
1393
+ value = node.value or ''
1394
+ parts = [p.strip() for p in value.split(',') if p.strip()]
1395
+ self._calendars.append({
1396
+ 'index': len(self._calendars),
1397
+ 'element_id': self._node_id(node),
1398
+ 'bind': self._bind_name(node.bind),
1399
+ 'mode': mode,
1400
+ 'selected': parts[0] if mode == 'single' and parts else '',
1401
+ 'range_start': parts[0] if mode == 'range' and parts else '',
1402
+ 'range_end': parts[1] if mode == 'range' and len(parts) > 1 else '',
1403
+ 'multiple': parts if mode == 'multiple' else [],
1404
+ 'min_date': node.min_date,
1405
+ 'max_date': node.max_date,
1406
+ 'first_day_of_week': self._as_int(node.first_day_of_week, 0),
1407
+ 'on_change': node.on_change,
1408
+ 'on_month_change': node.on_month_change,
1409
+ })
1410
+
1411
+ def _collect_date_picker(self, node: UIDatePickerNode):
1412
+ """Descriptor for <ui:date-picker>."""
1413
+ self._date_pickers.append({
1414
+ 'index': len(self._date_pickers),
1415
+ 'element_id': self._node_id(node),
1416
+ 'bind': self._bind_name(node.bind),
1417
+ 'value': node.value or '',
1418
+ 'placeholder': node.placeholder,
1419
+ 'format': node.format,
1420
+ 'min_date': node.min_date,
1421
+ 'max_date': node.max_date,
1422
+ 'clearable': self._as_bool(node.clearable, True),
1423
+ 'on_change': node.on_change,
1424
+ })
1425
+
1426
+ def _collect_toast(self, node: UIToastNode):
1427
+ """Descriptor for an inline <ui:toast>."""
1428
+ self._toasts.append({
1429
+ 'index': len(self._toasts),
1430
+ 'element_id': self._node_id(node),
1431
+ 'variant': node.variant or 'info',
1432
+ 'position': node.position or 'top-right',
1433
+ 'duration': self._as_int(node.duration, 3000),
1434
+ 'dismissible': self._as_bool(node.dismissible, True),
1435
+ 'icon': node.icon,
1436
+ 'title': node.title,
1437
+ 'message': node.message or '',
1438
+ 'show': self._bind_name(node.show),
1439
+ 'on_close': node.on_close,
1440
+ })
1441
+
1442
+ def _collect_toast_container(self, node: UIToastContainerNode):
1443
+ """Descriptor for <ui:toast-container> (defaults for raised toasts)."""
1444
+ self._toast_containers.append({
1445
+ 'index': len(self._toast_containers),
1446
+ 'element_id': self._node_id(node),
1447
+ 'position': node.position or 'top-right',
1448
+ 'max_toasts': self._as_int(node.max_toasts, 5),
1449
+ })
1450
+
1451
+ def _seed_component_state(self):
1452
+ """Declare the state variables the components are bound to.
1453
+
1454
+ A binding that has no matching q:set would otherwise never exist on the
1455
+ Python side, so the first value pushed from the UI would be the only
1456
+ thing the app ever knows about it.
1457
+ """
1458
+ for spec in self._carousels:
1459
+ if spec['bind'] and spec['bind'] not in self._state_vars:
1460
+ self._state_vars[spec['bind']] = spec['current']
1461
+
1462
+ for spec in self._steppers:
1463
+ if spec['bind'] and spec['bind'] not in self._state_vars:
1464
+ self._state_vars[spec['bind']] = spec['current']
1465
+
1466
+ for spec in self._calendars:
1467
+ if not spec['bind'] or spec['bind'] in self._state_vars:
1468
+ continue
1469
+ if spec['mode'] == 'multiple':
1470
+ self._state_vars[spec['bind']] = list(spec['multiple'])
1471
+ elif spec['mode'] == 'range':
1472
+ self._state_vars[spec['bind']] = {
1473
+ 'start': spec['range_start'], 'end': spec['range_end'],
1474
+ }
1475
+ else:
1476
+ self._state_vars[spec['bind']] = spec['selected']
1477
+
1478
+ for spec in self._date_pickers:
1479
+ if spec['bind'] and spec['bind'] not in self._state_vars:
1480
+ self._state_vars[spec['bind']] = spec['value']
1481
+
1482
+ for spec in self._toasts:
1483
+ if spec['show'] and spec['show'] not in self._state_vars:
1484
+ self._state_vars[spec['show']] = False
1485
+
1486
+ # ------------------------------------------------------------------
1487
+ # Interactive components: code generation
1488
+ # ------------------------------------------------------------------
1489
+
1490
+ def _generate_component_script(self) -> str:
1491
+ """Generate the JS bridge for the interactive components.
1492
+
1493
+ Returns:
1494
+ A <script> block wiring toast/carousel/stepper/calendar/date-picker
1495
+ to QuantumState, or '' when the app uses none of them.
1496
+ """
1497
+ if not self._has_components():
1498
+ return ''
1499
+
1500
+ spec = {
1501
+ 'carousels': self._carousels,
1502
+ 'steppers': self._steppers,
1503
+ 'calendars': self._calendars,
1504
+ 'date_pickers': self._date_pickers,
1505
+ 'toasts': self._toasts,
1506
+ 'toast_containers': self._toast_containers,
1507
+ }
1508
+ return UI_COMPONENT_BRIDGE_JS.replace(
1509
+ '__QUANTUM_UI_SPEC__', json.dumps(spec, sort_keys=True)
1510
+ )
1511
+
1512
+ def _generate_component_methods(self) -> str:
1513
+ """Generate the QuantumAPI methods that drive the components.
1514
+
1515
+ Returns:
1516
+ Python method code (4-space indented) or '' when the app uses none
1517
+ of the bridged components.
1518
+ """
1519
+ if not self._has_components():
1520
+ return ''
1521
+
1522
+ blocks = ["""\
1523
+ def _ui_eval(self, expression):
1524
+ \"\"\"Evaluate a JS expression in the webview (UI component bridge).\"\"\"
1525
+ if not self._window:
1526
+ return None
1527
+ try:
1528
+ return self._window.evaluate_js(expression)
1529
+ except Exception as exc:
1530
+ print('Quantum UI bridge error: ' + str(exc))
1531
+ return None
1532
+
1533
+ def _ui_call(self, method, *args):
1534
+ \"\"\"Call window.__quantumUI.<method> with JSON encoded arguments.\"\"\"
1535
+ import json
1536
+ payload = ', '.join(json.dumps(arg) for arg in args)
1537
+ return self._ui_eval(
1538
+ 'window.__quantumUI && window.__quantumUI.' + method + '(' + payload + ')'
1539
+ )
1540
+
1541
+ def ui_set_state(self, args=None):
1542
+ \"\"\"Store a value reported by the UI component bridge.\"\"\"
1543
+ args = args or {}
1544
+ name = args.get('name')
1545
+ if not name:
1546
+ return False
1547
+ self.state.set(name, args.get('value'))
1548
+ return True"""]
1549
+
1550
+ if self._toasts or self._toast_containers:
1551
+ blocks.append(self._generate_toast_methods())
1552
+ if self._carousels:
1553
+ blocks.append(self._generate_carousel_methods())
1554
+ if self._steppers:
1555
+ blocks.append(self._generate_stepper_methods())
1556
+ if self._calendars:
1557
+ blocks.append(self._generate_calendar_methods())
1558
+ if self._date_pickers:
1559
+ blocks.append(self._generate_date_picker_methods())
1560
+
1561
+ return '\n\n'.join(blocks)
1562
+
1563
+ def _generate_toast_methods(self) -> str:
1564
+ """QuantumAPI methods for ui:toast / ui:toast-container."""
1565
+ if self._toast_containers:
1566
+ position = self._toast_containers[0]['position']
1567
+ else:
1568
+ position = self._toasts[0]['position']
1569
+ duration = self._toasts[0]['duration'] if self._toasts else 3000
1570
+
1571
+ return f"""\
1572
+ def ui_toast(self, args=None):
1573
+ \"\"\"Show a toast notification (ui:toast / ui:toast-container).\"\"\"
1574
+ args = args or {{}}
1575
+ options = {{
1576
+ 'variant': args.get('variant', 'info'),
1577
+ 'title': args.get('title'),
1578
+ 'message': args.get('message', ''),
1579
+ 'position': args.get('position', {position!r}),
1580
+ 'duration': args.get('duration', {duration}),
1581
+ 'dismissible': args.get('dismissible', True),
1582
+ }}
1583
+ if args.get('icon') is not None:
1584
+ options['icon'] = args.get('icon')
1585
+ return self._ui_call('toast', options)
1586
+
1587
+ def ui_toast_dismiss_all(self, args=None):
1588
+ \"\"\"Dismiss every toast currently on screen.\"\"\"
1589
+ return self._ui_call('dismissToasts')"""
1590
+
1591
+ def _generate_carousel_methods(self) -> str:
1592
+ """QuantumAPI methods for ui:carousel (and ui:slide when it has an id)."""
1593
+ code = """\
1594
+ def ui_carousel_goto(self, args=None):
1595
+ \"\"\"Show a slide of a ui:carousel: {'carousel': ref, 'index': n}.\"\"\"
1596
+ args = args or {}
1597
+ try:
1598
+ index = int(args.get('index', 0))
1599
+ except (TypeError, ValueError):
1600
+ index = 0
1601
+ return self._ui_call('carouselGoTo', args.get('carousel', 0), index)
1602
+
1603
+ def ui_carousel_next(self, args=None):
1604
+ \"\"\"Advance a ui:carousel to the next slide.\"\"\"
1605
+ args = args or {}
1606
+ return self._ui_call('carouselNext', args.get('carousel', 0))
1607
+
1608
+ def ui_carousel_prev(self, args=None):
1609
+ \"\"\"Move a ui:carousel back to the previous slide.\"\"\"
1610
+ args = args or {}
1611
+ return self._ui_call('carouselPrev', args.get('carousel', 0))"""
1612
+
1613
+ has_slide_ids = any(
1614
+ slide['element_id'] for spec in self._carousels for slide in spec['slides']
1615
+ )
1616
+ if has_slide_ids:
1617
+ code += """
1618
+
1619
+ def ui_slide_show(self, args=None):
1620
+ \"\"\"Show the ui:slide with the given id: {'slide': 'slide-id'}.\"\"\"
1621
+ args = args or {}
1622
+ return self._ui_call('showSlide', args.get('slide'))"""
1623
+ return code
1624
+
1625
+ def _generate_stepper_methods(self) -> str:
1626
+ """QuantumAPI methods for ui:stepper (and ui:step when it has an id)."""
1627
+ code = """\
1628
+ def ui_stepper_goto(self, args=None):
1629
+ \"\"\"Jump a ui:stepper to a step: {'stepper': ref, 'index': n}.\"\"\"
1630
+ args = args or {}
1631
+ try:
1632
+ index = int(args.get('index', 0))
1633
+ except (TypeError, ValueError):
1634
+ index = 0
1635
+ return self._ui_call('stepperGoTo', args.get('stepper', 0), index)
1636
+
1637
+ def ui_stepper_next(self, args=None):
1638
+ \"\"\"Advance a ui:stepper to the next step (honours linear mode).\"\"\"
1639
+ args = args or {}
1640
+ return self._ui_call('stepperNext', args.get('stepper', 0))
1641
+
1642
+ def ui_stepper_prev(self, args=None):
1643
+ \"\"\"Move a ui:stepper back to the previous step.\"\"\"
1644
+ args = args or {}
1645
+ return self._ui_call('stepperPrev', args.get('stepper', 0))
1646
+
1647
+ def ui_stepper_complete(self, args=None):
1648
+ \"\"\"Mark a ui:step as completed: {'stepper': ref, 'index': n}.\"\"\"
1649
+ args = args or {}
1650
+ index = args.get('index')
1651
+ if index is not None:
1652
+ try:
1653
+ index = int(index)
1654
+ except (TypeError, ValueError):
1655
+ index = None
1656
+ return self._ui_call('stepperComplete', args.get('stepper', 0), index)"""
1657
+
1658
+ has_step_ids = any(
1659
+ step['element_id'] for spec in self._steppers for step in spec['steps']
1660
+ )
1661
+ if has_step_ids:
1662
+ code += """
1663
+
1664
+ def ui_step_show(self, args=None):
1665
+ \"\"\"Show the ui:step with the given id: {'step': 'step-id'}.\"\"\"
1666
+ args = args or {}
1667
+ return self._ui_call('showStep', args.get('step'))"""
1668
+ return code
1669
+
1670
+ def _generate_calendar_methods(self) -> str:
1671
+ """QuantumAPI methods for ui:calendar."""
1672
+ return """\
1673
+ def ui_calendar_select(self, args=None):
1674
+ \"\"\"Select a date in a ui:calendar: {'calendar': ref, 'value': ISO date}.
1675
+
1676
+ Passing an empty value clears the current selection.
1677
+ \"\"\"
1678
+ args = args or {}
1679
+ return self._ui_call('calendarSelect', args.get('calendar', 0), args.get('value'))"""
1680
+
1681
+ def _generate_date_picker_methods(self) -> str:
1682
+ """QuantumAPI methods for ui:date-picker."""
1683
+ return """\
1684
+ def ui_date_picker_set(self, args=None):
1685
+ \"\"\"Set a ui:date-picker value: {'picker': ref, 'value': ISO date}.\"\"\"
1686
+ args = args or {}
1687
+ return self._ui_call('datePickerSet', args.get('picker', 0), args.get('value'))"""
1688
+
1689
+ # ------------------------------------------------------------------
1690
+ # Attribute helpers
1691
+ # ------------------------------------------------------------------
1692
+
1693
+ @staticmethod
1694
+ def _node_id(node: QuantumNode) -> Optional[str]:
1695
+ """Return the explicit id of a UI node (ui_id), if any."""
1696
+ return getattr(node, 'ui_id', None) or None
1697
+
1698
+ @staticmethod
1699
+ def _bind_name(bind: Optional[str]) -> Optional[str]:
1700
+ """Normalize a binding reference ('{name}' and 'name' are the same)."""
1701
+ if not bind:
1702
+ return None
1703
+ name = str(bind).strip()
1704
+ if name.startswith('{') and name.endswith('}'):
1705
+ name = name[1:-1].strip()
1706
+ return name or None
1707
+
1708
+ @staticmethod
1709
+ def _as_int(value: Any, default: int) -> int:
1710
+ """Coerce an attribute to int, falling back to a default."""
1711
+ if value is None or isinstance(value, bool):
1712
+ return default
1713
+ try:
1714
+ return int(str(value).strip())
1715
+ except (TypeError, ValueError):
1716
+ return default
1717
+
1718
+ @staticmethod
1719
+ def _as_bool(value: Any, default: bool) -> bool:
1720
+ """Coerce an attribute to bool, falling back to a default."""
1721
+ if value is None:
1722
+ return default
1723
+ if isinstance(value, bool):
1724
+ return value
1725
+ text = str(value).strip().lower()
1726
+ if text in ('true', '1', 'yes', 'on'):
1727
+ return True
1728
+ if text in ('false', '0', 'no', 'off', ''):
1729
+ return False
1730
+ return default