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,2106 @@
1
+ """
2
+ UI Engine - HTML Templates and HtmlBuilder
3
+
4
+ Provides:
5
+ - HtmlBuilder: Structured HTML code emitter with proper indentation
6
+ - HTML_TEMPLATE: Shell HTML5 document template
7
+ - CSS_RESET: Basic CSS reset
8
+ - CSS_THEME: Default theme variables and base styles
9
+ """
10
+
11
+
12
+ # ==========================================================================
13
+ # HtmlBuilder - Structured HTML code emitter
14
+ # ==========================================================================
15
+
16
+ class HtmlBuilder:
17
+ """Builds well-formatted HTML with proper indentation."""
18
+
19
+ def __init__(self, indent_size: int = 2):
20
+ self._lines: list[str] = []
21
+ self._indent: int = 0
22
+ self._indent_size: int = indent_size
23
+
24
+ @property
25
+ def _pad(self) -> str:
26
+ return ' ' * (self._indent * self._indent_size)
27
+
28
+ def line(self, code: str = '') -> 'HtmlBuilder':
29
+ self._lines.append(f"{self._pad}{code}" if code else '')
30
+ return self
31
+
32
+ def raw(self, code: str) -> 'HtmlBuilder':
33
+ for ln in code.split('\n'):
34
+ self._lines.append(f"{self._pad}{ln}")
35
+ return self
36
+
37
+ def blank(self) -> 'HtmlBuilder':
38
+ self._lines.append('')
39
+ return self
40
+
41
+ def indent(self) -> 'HtmlBuilder':
42
+ self._indent += 1
43
+ return self
44
+
45
+ def dedent(self) -> 'HtmlBuilder':
46
+ self._indent = max(0, self._indent - 1)
47
+ return self
48
+
49
+ def open_tag(self, tag: str, attrs: dict = None, self_closing: bool = False) -> 'HtmlBuilder':
50
+ attr_str = ''
51
+ if attrs:
52
+ parts = []
53
+ for k, v in attrs.items():
54
+ if v is True:
55
+ parts.append(k)
56
+ elif v is not None and v is not False:
57
+ parts.append(f'{k}="{v}"')
58
+ if parts:
59
+ attr_str = ' ' + ' '.join(parts)
60
+ if self_closing:
61
+ self.line(f'<{tag}{attr_str} />')
62
+ else:
63
+ self.line(f'<{tag}{attr_str}>')
64
+ return self
65
+
66
+ def close_tag(self, tag: str) -> 'HtmlBuilder':
67
+ self.line(f'</{tag}>')
68
+ return self
69
+
70
+ def text(self, content: str) -> 'HtmlBuilder':
71
+ self.line(content)
72
+ return self
73
+
74
+ def comment(self, text: str) -> 'HtmlBuilder':
75
+ self.line(f'<!-- {text} -->')
76
+ return self
77
+
78
+ def build(self) -> str:
79
+ return '\n'.join(self._lines)
80
+
81
+
82
+ # ==========================================================================
83
+ # CSS Reset
84
+ # ==========================================================================
85
+
86
+ CSS_RESET = """\
87
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
88
+ html { font-size: 16px; line-height: 1.5; -webkit-font-smoothing: antialiased; }
89
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
90
+ img, picture, video, canvas, svg { display: block; max-width: 100%; }
91
+ input, button, textarea, select { font: inherit; }
92
+ """
93
+
94
+
95
+ # ==========================================================================
96
+ # CSS Theme
97
+ # ==========================================================================
98
+
99
+ CSS_THEME = """\
100
+ :root {
101
+ --q-primary: #3b82f6;
102
+ --q-secondary: #64748b;
103
+ --q-success: #22c55e;
104
+ --q-danger: #ef4444;
105
+ --q-warning: #f59e0b;
106
+ --q-info: #06b6d4;
107
+ --q-light: #f8fafc;
108
+ --q-dark: #1e293b;
109
+ --q-bg: #ffffff;
110
+ --q-text: #1e293b;
111
+ --q-border: #e2e8f0;
112
+ --q-radius: 6px;
113
+ --q-shadow: 0 1px 3px rgba(0,0,0,0.1);
114
+ --q-font-xs: 0.75rem;
115
+ --q-font-sm: 0.875rem;
116
+ --q-font-md: 1rem;
117
+ --q-font-lg: 1.25rem;
118
+ --q-font-xl: 1.5rem;
119
+ --q-font-2xl: 2rem;
120
+ }
121
+
122
+ body { background: var(--q-bg); color: var(--q-text); min-height: 100vh; }
123
+
124
+ /* Window */
125
+ .q-window { width: 100%; min-height: 100vh; display: flex; flex-direction: column; }
126
+
127
+ /* Panel */
128
+ .q-panel { border: 1px solid var(--q-border); border-radius: var(--q-radius); padding: 16px; box-shadow: var(--q-shadow); }
129
+ .q-panel-title { font-weight: 600; margin-bottom: 12px; font-size: var(--q-font-lg); border-bottom: 1px solid var(--q-border); padding-bottom: 8px; }
130
+
131
+ /* Buttons */
132
+ .q-btn { display: inline-flex; align-items: center; justify-content: center; padding: 8px 16px; border: 1px solid var(--q-border); border-radius: var(--q-radius); cursor: pointer; font-weight: 500; transition: all 0.15s; background: var(--q-bg); }
133
+ .q-btn:hover { opacity: 0.9; }
134
+ .q-btn-primary { background: var(--q-primary); color: white; border-color: var(--q-primary); }
135
+ .q-btn-secondary { background: var(--q-secondary); color: white; border-color: var(--q-secondary); }
136
+ .q-btn-danger { background: var(--q-danger); color: white; border-color: var(--q-danger); }
137
+ .q-btn-success { background: var(--q-success); color: white; border-color: var(--q-success); }
138
+ .q-btn:disabled { opacity: 0.5; cursor: not-allowed; }
139
+
140
+ /* Inputs */
141
+ .q-input { width: 100%; padding: 8px 12px; border: 1px solid var(--q-border); border-radius: var(--q-radius); outline: none; transition: border-color 0.15s; }
142
+ .q-input:focus { border-color: var(--q-primary); box-shadow: 0 0 0 3px rgba(59,130,246,0.15); }
143
+
144
+ /* Checkbox / Switch */
145
+ .q-checkbox { display: flex; align-items: center; gap: 8px; cursor: pointer; }
146
+ .q-switch { position: relative; display: inline-flex; align-items: center; gap: 8px; cursor: pointer; }
147
+ .q-switch input { display: none; }
148
+ .q-switch-track { width: 40px; height: 22px; background: var(--q-border); border-radius: 11px; position: relative; transition: background 0.2s; }
149
+ .q-switch input:checked + .q-switch-track { background: var(--q-primary); }
150
+ .q-switch-thumb { width: 18px; height: 18px; background: white; border-radius: 50%; position: absolute; top: 2px; left: 2px; transition: transform 0.2s; }
151
+ .q-switch input:checked + .q-switch-track .q-switch-thumb { transform: translateX(18px); }
152
+
153
+ /* Table */
154
+ .q-table { width: 100%; border-collapse: collapse; }
155
+ .q-table th, .q-table td { padding: 10px 12px; text-align: left; border-bottom: 1px solid var(--q-border); }
156
+ .q-table th { font-weight: 600; background: var(--q-light); }
157
+ .q-table tr:hover { background: var(--q-light); }
158
+
159
+ /* Tabs */
160
+ .q-tabs { display: flex; flex-direction: column; }
161
+ .q-tab-headers { display: flex; border-bottom: 2px solid var(--q-border); gap: 0; }
162
+ .q-tab-header { padding: 10px 20px; cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -2px; font-weight: 500; color: var(--q-secondary); transition: all 0.15s; background: none; border-top: none; border-left: none; border-right: none; }
163
+ .q-tab-header.active { color: var(--q-primary); border-bottom-color: var(--q-primary); }
164
+ .q-tab-content { display: none; padding: 16px 0; }
165
+ .q-tab-content.active { display: block; }
166
+
167
+ /* Form */
168
+ .q-form { display: flex; flex-direction: column; gap: 16px; }
169
+ .q-formitem { display: flex; flex-direction: row; align-items: center; gap: 12px; }
170
+ .q-formitem-label { min-width: 120px; font-weight: 500; }
171
+
172
+ /* Badge */
173
+ .q-badge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 12px; font-size: var(--q-font-xs); font-weight: 600; background: var(--q-light); color: var(--q-text); }
174
+ .q-badge-primary { background: var(--q-primary); color: white; }
175
+ .q-badge-secondary { background: var(--q-secondary); color: white; }
176
+ .q-badge-success { background: var(--q-success); color: white; }
177
+ .q-badge-danger { background: var(--q-danger); color: white; }
178
+ .q-badge-warning { background: var(--q-warning); color: white; }
179
+
180
+ /* Loading */
181
+ .q-loading { display: flex; align-items: center; gap: 8px; color: var(--q-secondary); }
182
+ .q-spinner { width: 20px; height: 20px; border: 2px solid var(--q-border); border-top-color: var(--q-primary); border-radius: 50%; animation: q-spin 0.6s linear infinite; }
183
+ @keyframes q-spin { to { transform: rotate(360deg); } }
184
+
185
+ /* Log */
186
+ .q-log { font-family: 'SF Mono', 'Fira Code', monospace; font-size: var(--q-font-sm); background: var(--q-dark); color: #e2e8f0; padding: 12px; border-radius: var(--q-radius); overflow-y: auto; max-height: 400px; }
187
+
188
+ /* Progress */
189
+ progress { width: 100%; height: 8px; border-radius: 4px; overflow: hidden; appearance: none; }
190
+ progress::-webkit-progress-bar { background: var(--q-light); border-radius: 4px; }
191
+ progress::-webkit-progress-value { background: var(--q-primary); border-radius: 4px; }
192
+
193
+ /* Header / Footer */
194
+ .q-header { padding: 12px 16px; background: var(--q-dark); color: white; font-weight: 600; font-size: var(--q-font-lg); }
195
+ .q-footer { padding: 12px 16px; background: var(--q-light); color: var(--q-secondary); border-top: 1px solid var(--q-border); font-size: var(--q-font-sm); }
196
+
197
+ /* Scrollbox */
198
+ .q-scrollbox { overflow: auto; }
199
+
200
+ /* Accordion / Section */
201
+ details.q-section { border: 1px solid var(--q-border); border-radius: var(--q-radius); margin-bottom: 4px; }
202
+ details.q-section summary { padding: 10px 16px; cursor: pointer; font-weight: 500; }
203
+ details.q-section .q-section-content { padding: 12px 16px; border-top: 1px solid var(--q-border); }
204
+ section.q-section-group { display: flex; flex-direction: column; gap: 8px; }
205
+ section.q-section-group > .q-section-title { font-weight: 600; }
206
+
207
+ /* Spacer */
208
+ .q-spacer { flex: 1; }
209
+
210
+ /* Select */
211
+ .q-select { width: 100%; padding: 8px 12px; border: 1px solid var(--q-border); border-radius: var(--q-radius); outline: none; background: var(--q-bg); }
212
+ .q-select:focus { border-color: var(--q-primary); }
213
+
214
+ /* Menu */
215
+ .q-menu { display: flex; gap: 4px; }
216
+ .q-menu-item { padding: 8px 16px; cursor: pointer; border-radius: var(--q-radius); transition: background 0.15s; }
217
+ .q-menu-item:hover { background: var(--q-light); }
218
+
219
+ /* DividedBox */
220
+ .q-dividedbox { display: flex; gap: 0; }
221
+ .q-dividedbox-h { flex-direction: row; }
222
+ .q-dividedbox-v { flex-direction: column; }
223
+ .q-divider { background: var(--q-border); flex-shrink: 0; }
224
+ .q-dividedbox-h > .q-divider { width: 4px; cursor: col-resize; }
225
+ .q-dividedbox-v > .q-divider { height: 4px; cursor: row-resize; }
226
+
227
+ /* Responsive Layout */
228
+ .q-window > header, .q-window > .q-header { flex-shrink: 0; }
229
+ .q-window > footer, .q-window > .q-footer { flex-shrink: 0; margin-top: auto; }
230
+ .q-window > div:not(.q-header):not(.q-footer) { flex: 1; overflow: auto; }
231
+ .q-panel { flex-shrink: 0; }
232
+
233
+ /* Media Queries for smaller screens */
234
+ @media (max-width: 900px) {
235
+ .q-window > div[style*="flex-direction: row"] { flex-direction: column !important; }
236
+ .q-window > div > div[style*="width: 280px"] { width: 100% !important; }
237
+ .q-window > div > div[style*="width: 50%"] { width: 100% !important; }
238
+ }
239
+
240
+ /* ============================================
241
+ Component Library Styles
242
+ ============================================ */
243
+
244
+ /* Card */
245
+ .q-card { background: var(--q-bg); border: 1px solid var(--q-border); border-radius: var(--q-radius); box-shadow: var(--q-shadow); overflow: hidden; }
246
+ .q-card-elevated { box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
247
+ .q-card-outlined { box-shadow: none; border-width: 2px; }
248
+ .q-card-image { width: 100%; }
249
+ .q-card-image img { width: 100%; height: auto; display: block; }
250
+ .q-card-header { padding: 16px; border-bottom: 1px solid var(--q-border); }
251
+ .q-card-title { font-size: var(--q-font-lg); font-weight: 600; margin: 0; }
252
+ .q-card-subtitle { font-size: var(--q-font-sm); color: var(--q-secondary); margin-top: 4px; }
253
+ .q-card-body { padding: 16px; }
254
+ .q-card-footer { padding: 12px 16px; background: var(--q-light); border-top: 1px solid var(--q-border); display: flex; gap: 8px; justify-content: flex-end; }
255
+
256
+ /* Modal */
257
+ .q-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: none; justify-content: center; align-items: center; z-index: 1000; }
258
+ .q-modal-overlay.q-modal-open { display: flex; }
259
+ .q-modal { background: var(--q-bg); border-radius: var(--q-radius); box-shadow: 0 8px 32px rgba(0,0,0,0.2); max-width: 500px; width: 90%; max-height: 90vh; overflow: auto; }
260
+ .q-modal-sm { max-width: 300px; }
261
+ .q-modal-lg { max-width: 800px; }
262
+ .q-modal-xl { max-width: 1100px; }
263
+ .q-modal-full { max-width: 95vw; max-height: 95vh; }
264
+ .q-modal-header { display: flex; justify-content: space-between; align-items: center; padding: 16px; border-bottom: 1px solid var(--q-border); }
265
+ .q-modal-title { font-size: var(--q-font-lg); font-weight: 600; margin: 0; }
266
+ .q-modal-close { background: none; border: none; font-size: 20px; cursor: pointer; padding: 4px 8px; color: var(--q-secondary); }
267
+ .q-modal-close:hover { color: var(--q-text); }
268
+ .q-modal-body { padding: 16px; }
269
+
270
+ /* Chart */
271
+ .q-chart { padding: 16px; }
272
+ .q-chart-title { font-size: var(--q-font-lg); font-weight: 600; margin-bottom: 16px; }
273
+ .q-chart-container { display: flex; flex-direction: column; gap: 8px; }
274
+ .q-chart-bar-group { display: flex; align-items: center; gap: 12px; }
275
+ .q-chart-label { min-width: 80px; font-size: var(--q-font-sm); }
276
+ .q-chart-bar { height: 24px; border-radius: 4px; display: flex; align-items: center; padding: 0 8px; min-width: 30px; transition: width 0.3s ease; }
277
+ .q-chart-value { font-size: var(--q-font-xs); color: white; font-weight: 600; }
278
+
279
+ /* Avatar */
280
+ .q-avatar { position: relative; display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; border-radius: 50%; background: var(--q-primary); color: white; font-weight: 600; overflow: hidden; }
281
+ .q-avatar-xs { width: 24px; height: 24px; font-size: var(--q-font-xs); }
282
+ .q-avatar-sm { width: 32px; height: 32px; font-size: var(--q-font-sm); }
283
+ .q-avatar-lg { width: 56px; height: 56px; font-size: var(--q-font-lg); }
284
+ .q-avatar-xl { width: 72px; height: 72px; font-size: var(--q-font-xl); }
285
+ .q-avatar-square { border-radius: var(--q-radius); }
286
+ .q-avatar img { width: 100%; height: 100%; object-fit: cover; }
287
+ .q-avatar-initials { text-transform: uppercase; }
288
+ .q-avatar-status { position: absolute; bottom: 0; right: 0; width: 12px; height: 12px; border-radius: 50%; border: 2px solid var(--q-bg); }
289
+ .q-avatar-status-online { background: var(--q-success); }
290
+ .q-avatar-status-offline { background: var(--q-secondary); }
291
+ .q-avatar-status-away { background: var(--q-warning); }
292
+ .q-avatar-status-busy { background: var(--q-danger); }
293
+
294
+ /* Tooltip */
295
+ .q-tooltip-wrapper { position: relative; display: inline-block; }
296
+ .q-tooltip-text { visibility: hidden; position: absolute; z-index: 100; padding: 6px 10px; background: var(--q-dark); color: white; font-size: var(--q-font-sm); border-radius: var(--q-radius); white-space: nowrap; opacity: 0; transition: opacity 0.2s; }
297
+ .q-tooltip-wrapper:hover .q-tooltip-text { visibility: visible; opacity: 1; }
298
+ .q-tooltip-top .q-tooltip-text { bottom: 100%; left: 50%; transform: translateX(-50%); margin-bottom: 8px; }
299
+ .q-tooltip-bottom .q-tooltip-text { top: 100%; left: 50%; transform: translateX(-50%); margin-top: 8px; }
300
+ .q-tooltip-left .q-tooltip-text { right: 100%; top: 50%; transform: translateY(-50%); margin-right: 8px; }
301
+ .q-tooltip-right .q-tooltip-text { left: 100%; top: 50%; transform: translateY(-50%); margin-left: 8px; }
302
+
303
+ /* Dropdown */
304
+ .q-dropdown { position: relative; display: inline-block; }
305
+ .q-dropdown-trigger { display: inline-flex; align-items: center; gap: 4px; padding: 8px 12px; background: var(--q-bg); border: 1px solid var(--q-border); border-radius: var(--q-radius); cursor: pointer; }
306
+ .q-dropdown-arrow { font-size: var(--q-font-xs); }
307
+ .q-dropdown-menu { position: absolute; top: 100%; left: 0; min-width: 160px; background: var(--q-bg); border: 1px solid var(--q-border); border-radius: var(--q-radius); box-shadow: var(--q-shadow); z-index: 100; display: none; margin-top: 4px; }
308
+ .q-dropdown-right .q-dropdown-menu { left: auto; right: 0; }
309
+ .q-dropdown:focus-within .q-dropdown-menu,
310
+ .q-dropdown-hover:hover .q-dropdown-menu { display: block; }
311
+ .q-dropdown-menu .q-menu-item { display: block; width: 100%; padding: 8px 12px; text-align: left; }
312
+
313
+ /* Alert */
314
+ .q-alert { display: flex; align-items: flex-start; gap: 12px; padding: 12px 16px; border-radius: var(--q-radius); margin-bottom: 8px; }
315
+ .q-alert-info { background: rgba(6,182,212,0.1); border: 1px solid var(--q-info); color: var(--q-info); }
316
+ .q-alert-success { background: rgba(34,197,94,0.1); border: 1px solid var(--q-success); color: var(--q-success); }
317
+ .q-alert-warning { background: rgba(245,158,11,0.1); border: 1px solid var(--q-warning); color: var(--q-warning); }
318
+ .q-alert-danger { background: rgba(239,68,68,0.1); border: 1px solid var(--q-danger); color: var(--q-danger); }
319
+ .q-alert-icon { flex-shrink: 0; font-size: var(--q-font-lg); }
320
+ .q-alert-content { flex: 1; }
321
+ .q-alert-title { font-weight: 600; margin-bottom: 4px; }
322
+ .q-alert-message { font-size: var(--q-font-sm); opacity: 0.9; }
323
+ .q-alert-dismiss { background: none; border: none; cursor: pointer; padding: 0 4px; font-size: var(--q-font-lg); opacity: 0.6; }
324
+ .q-alert-dismiss:hover { opacity: 1; }
325
+
326
+ /* Breadcrumb */
327
+ .q-breadcrumb { padding: 8px 0; }
328
+ .q-breadcrumb-list { display: flex; align-items: center; list-style: none; gap: 0; flex-wrap: wrap; }
329
+ .q-breadcrumb-item { display: flex; align-items: center; }
330
+ .q-breadcrumb-link { color: var(--q-primary); text-decoration: none; }
331
+ .q-breadcrumb-link:hover { text-decoration: underline; }
332
+ .q-breadcrumb-text { color: var(--q-text); }
333
+ .q-breadcrumb-current .q-breadcrumb-text { font-weight: 500; }
334
+ .q-breadcrumb-separator { margin: 0 8px; color: var(--q-secondary); }
335
+ .q-breadcrumb-icon { margin-right: 4px; }
336
+
337
+ /* Pagination */
338
+ .q-pagination { display: flex; align-items: center; gap: 16px; padding: 8px 0; }
339
+ .q-pagination-total { font-size: var(--q-font-sm); color: var(--q-secondary); }
340
+ .q-pagination-controls { display: flex; align-items: center; gap: 8px; }
341
+ .q-pagination-prev, .q-pagination-next { padding: 6px 12px; background: var(--q-bg); border: 1px solid var(--q-border); border-radius: var(--q-radius); cursor: pointer; }
342
+ .q-pagination-prev:hover, .q-pagination-next:hover { background: var(--q-light); }
343
+ .q-pagination-pages { padding: 0 12px; font-weight: 500; }
344
+ .q-pagination-jump { display: flex; align-items: center; gap: 8px; font-size: var(--q-font-sm); }
345
+ .q-pagination-input { width: 60px; padding: 4px 8px; border: 1px solid var(--q-border); border-radius: var(--q-radius); }
346
+
347
+ /* Skeleton */
348
+ .q-skeleton { background: linear-gradient(90deg, var(--q-light) 25%, #e2e8f0 50%, var(--q-light) 75%); background-size: 200% 100%; border-radius: var(--q-radius); }
349
+ .q-skeleton-animated { animation: q-skeleton-loading 1.5s infinite; }
350
+ @keyframes q-skeleton-loading { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
351
+ .q-skeleton-text { height: 16px; margin-bottom: 8px; }
352
+ .q-skeleton-text:last-child { width: 60%; }
353
+ .q-skeleton-circle { width: 48px; height: 48px; border-radius: 50%; }
354
+ .q-skeleton-rect { height: 100px; }
355
+ .q-skeleton-card { border: 1px solid var(--q-border); border-radius: var(--q-radius); padding: 16px; }
356
+
357
+ /* ============================================
358
+ Toast Notifications
359
+ ============================================ */
360
+
361
+ /* Toast Container */
362
+ .q-toast-container { position: fixed; z-index: 9999; display: flex; flex-direction: column; gap: 8px; pointer-events: none; max-width: 400px; }
363
+ .q-toast-container.q-toast-top-right { top: 16px; right: 16px; }
364
+ .q-toast-container.q-toast-top-left { top: 16px; left: 16px; }
365
+ .q-toast-container.q-toast-bottom-right { bottom: 16px; right: 16px; }
366
+ .q-toast-container.q-toast-bottom-left { bottom: 16px; left: 16px; }
367
+ .q-toast-container.q-toast-top-center { top: 16px; left: 50%; transform: translateX(-50%); }
368
+ .q-toast-container.q-toast-bottom-center { bottom: 16px; left: 50%; transform: translateX(-50%); }
369
+
370
+ /* Toast */
371
+ .q-toast { display: flex; align-items: flex-start; gap: 12px; padding: 12px 16px; border-radius: var(--q-radius); box-shadow: 0 4px 12px rgba(0,0,0,0.15); pointer-events: auto; animation: q-toast-in 0.3s ease; background: var(--q-bg); border: 1px solid var(--q-border); }
372
+ .q-toast.q-toast-out { animation: q-toast-out 0.3s ease forwards; }
373
+ .q-toast-info { border-left: 4px solid var(--q-info); }
374
+ .q-toast-success { border-left: 4px solid var(--q-success); }
375
+ .q-toast-warning { border-left: 4px solid var(--q-warning); }
376
+ .q-toast-danger { border-left: 4px solid var(--q-danger); }
377
+ .q-toast-icon { flex-shrink: 0; font-size: var(--q-font-lg); }
378
+ .q-toast-info .q-toast-icon { color: var(--q-info); }
379
+ .q-toast-success .q-toast-icon { color: var(--q-success); }
380
+ .q-toast-warning .q-toast-icon { color: var(--q-warning); }
381
+ .q-toast-danger .q-toast-icon { color: var(--q-danger); }
382
+ .q-toast-content { flex: 1; min-width: 0; }
383
+ .q-toast-title { font-weight: 600; margin-bottom: 2px; }
384
+ .q-toast-message { font-size: var(--q-font-sm); color: var(--q-secondary); }
385
+ .q-toast-close { flex-shrink: 0; background: none; border: none; cursor: pointer; padding: 0; font-size: var(--q-font-lg); color: var(--q-secondary); opacity: 0.6; }
386
+ .q-toast-close:hover { opacity: 1; }
387
+ @keyframes q-toast-in { from { opacity: 0; transform: translateX(100%); } to { opacity: 1; transform: translateX(0); } }
388
+ @keyframes q-toast-out { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(100%); } }
389
+
390
+ /* ============================================
391
+ Carousel / Slider
392
+ ============================================ */
393
+
394
+ .q-carousel { position: relative; overflow: hidden; border-radius: var(--q-radius); }
395
+ .q-carousel-track { display: flex; transition: transform 0.5s ease; }
396
+ .q-carousel.q-carousel-fade .q-carousel-track { display: block; }
397
+ .q-carousel-slide { flex: 0 0 100%; min-width: 100%; }
398
+ .q-carousel.q-carousel-fade .q-carousel-slide { position: absolute; top: 0; left: 0; width: 100%; opacity: 0; transition: opacity 0.5s ease; }
399
+ .q-carousel.q-carousel-fade .q-carousel-slide.active { position: relative; opacity: 1; }
400
+
401
+ /* Carousel Arrows */
402
+ .q-carousel-arrow { position: absolute; top: 50%; transform: translateY(-50%); background: rgba(0,0,0,0.5); color: white; border: none; width: 40px; height: 40px; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: var(--q-font-lg); z-index: 10; transition: background 0.2s; }
403
+ .q-carousel-arrow:hover { background: rgba(0,0,0,0.7); }
404
+ .q-carousel-arrow:disabled { opacity: 0.3; cursor: not-allowed; }
405
+ .q-carousel-prev { left: 12px; }
406
+ .q-carousel-next { right: 12px; }
407
+
408
+ /* Carousel Indicators */
409
+ .q-carousel-indicators { position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%); display: flex; gap: 8px; z-index: 10; }
410
+ .q-carousel-indicator { width: 10px; height: 10px; border-radius: 50%; background: rgba(255,255,255,0.5); border: none; cursor: pointer; transition: background 0.2s; }
411
+ .q-carousel-indicator.active { background: white; }
412
+ .q-carousel-indicator:hover { background: rgba(255,255,255,0.8); }
413
+
414
+ /* ============================================
415
+ Stepper / Wizard
416
+ ============================================ */
417
+
418
+ .q-stepper { display: flex; flex-direction: column; }
419
+ .q-stepper-horizontal .q-stepper-header { flex-direction: row; }
420
+ .q-stepper-vertical { flex-direction: row; }
421
+ .q-stepper-vertical .q-stepper-header { flex-direction: column; width: 200px; }
422
+ .q-stepper-vertical .q-stepper-content { flex: 1; }
423
+
424
+ /* Stepper Header */
425
+ .q-stepper-header { display: flex; gap: 0; margin-bottom: 24px; }
426
+ .q-stepper-horizontal .q-stepper-header { align-items: center; }
427
+
428
+ /* Step Item */
429
+ .q-step-item { display: flex; align-items: center; flex: 1; }
430
+ .q-stepper-vertical .q-step-item { flex-direction: column; align-items: flex-start; padding: 16px 0; }
431
+
432
+ /* Step Indicator */
433
+ .q-step-indicator { width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: var(--q-font-sm); background: var(--q-light); color: var(--q-secondary); border: 2px solid var(--q-border); flex-shrink: 0; transition: all 0.3s; }
434
+ .q-step-item.active .q-step-indicator { background: var(--q-primary); color: white; border-color: var(--q-primary); }
435
+ .q-step-item.completed .q-step-indicator { background: var(--q-success); color: white; border-color: var(--q-success); }
436
+ .q-step-item.error .q-step-indicator { background: var(--q-danger); color: white; border-color: var(--q-danger); }
437
+
438
+ /* Step Labels */
439
+ .q-step-labels { margin-left: 12px; }
440
+ .q-stepper-vertical .q-step-labels { margin-left: 0; margin-top: 8px; }
441
+ .q-step-title { font-weight: 500; color: var(--q-text); }
442
+ .q-step-item.active .q-step-title { color: var(--q-primary); }
443
+ .q-step-description { font-size: var(--q-font-sm); color: var(--q-secondary); margin-top: 2px; }
444
+ .q-step-optional { font-size: var(--q-font-xs); color: var(--q-secondary); font-style: italic; }
445
+
446
+ /* Step Connector */
447
+ .q-step-connector { flex: 1; height: 2px; background: var(--q-border); margin: 0 8px; }
448
+ .q-stepper-vertical .q-step-connector { width: 2px; height: 100%; min-height: 40px; margin: 0; margin-left: 15px; }
449
+ .q-step-item.completed + .q-step-item .q-step-connector,
450
+ .q-step-item.completed .q-step-connector { background: var(--q-success); }
451
+
452
+ /* Stepper Content */
453
+ .q-stepper-content { padding: 16px 0; }
454
+ .q-step-content { display: none; }
455
+ .q-step-content.active { display: block; animation: q-fade-in 0.3s ease; }
456
+
457
+ /* Stepper Actions */
458
+ .q-stepper-actions { display: flex; gap: 12px; justify-content: flex-end; padding-top: 16px; border-top: 1px solid var(--q-border); margin-top: 16px; }
459
+
460
+ /* ============================================
461
+ Calendar / Date Picker
462
+ ============================================ */
463
+
464
+ .q-calendar { background: var(--q-bg); border: 1px solid var(--q-border); border-radius: var(--q-radius); padding: 16px; width: 300px; }
465
+
466
+ /* Calendar Header */
467
+ .q-calendar-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
468
+ .q-calendar-title { font-weight: 600; font-size: var(--q-font-md); }
469
+ .q-calendar-nav { display: flex; gap: 8px; }
470
+ .q-calendar-nav button { background: none; border: 1px solid var(--q-border); border-radius: var(--q-radius); width: 28px; height: 28px; cursor: pointer; display: flex; align-items: center; justify-content: center; }
471
+ .q-calendar-nav button:hover { background: var(--q-light); }
472
+
473
+ /* Calendar Grid */
474
+ .q-calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
475
+ .q-calendar-weekday { text-align: center; font-size: var(--q-font-xs); font-weight: 600; color: var(--q-secondary); padding: 8px 0; }
476
+
477
+ /* Calendar Days */
478
+ .q-calendar-day { text-align: center; padding: 8px 0; font-size: var(--q-font-sm); border-radius: var(--q-radius); cursor: pointer; transition: all 0.15s; }
479
+ .q-calendar-day:hover { background: var(--q-light); }
480
+ .q-calendar-day.today { font-weight: 600; color: var(--q-primary); }
481
+ .q-calendar-day.selected { background: var(--q-primary); color: white; }
482
+ .q-calendar-day.in-range { background: rgba(59,130,246,0.15); }
483
+ .q-calendar-day.range-start { background: var(--q-primary); color: white; border-radius: var(--q-radius) 0 0 var(--q-radius); }
484
+ .q-calendar-day.range-end { background: var(--q-primary); color: white; border-radius: 0 var(--q-radius) var(--q-radius) 0; }
485
+ .q-calendar-day.other-month { color: var(--q-secondary); opacity: 0.5; }
486
+ .q-calendar-day.disabled { color: var(--q-secondary); opacity: 0.3; cursor: not-allowed; pointer-events: none; }
487
+ .q-calendar-day.week-number { font-size: var(--q-font-xs); color: var(--q-secondary); cursor: default; }
488
+ .q-calendar-day.week-number:hover { background: none; }
489
+
490
+ /* Date Picker (Input + Calendar popup) */
491
+ .q-date-picker { position: relative; }
492
+ .q-date-picker-input { width: 100%; padding: 8px 12px; padding-right: 36px; border: 1px solid var(--q-border); border-radius: var(--q-radius); }
493
+ .q-date-picker-input:focus { border-color: var(--q-primary); outline: none; box-shadow: 0 0 0 3px rgba(59,130,246,0.15); }
494
+ .q-date-picker-icon { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); color: var(--q-secondary); pointer-events: none; }
495
+ .q-date-picker-clear { position: absolute; right: 32px; top: 50%; transform: translateY(-50%); background: none; border: none; color: var(--q-secondary); cursor: pointer; padding: 4px; }
496
+ .q-date-picker-clear:hover { color: var(--q-text); }
497
+ .q-date-picker-dropdown { position: absolute; top: 100%; left: 0; margin-top: 4px; z-index: 100; box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: none; }
498
+ .q-date-picker-dropdown.open { display: block; animation: q-fade-in 0.2s ease; }
499
+ """
500
+
501
+
502
+ # ==========================================================================
503
+ # CSS Animations
504
+ # ==========================================================================
505
+
506
+ CSS_ANIMATIONS = """\
507
+ /* ============================================
508
+ Quantum UI Animation System
509
+ ============================================ */
510
+
511
+ /* Animation container base */
512
+ .q-animate {
513
+ will-change: transform, opacity;
514
+ }
515
+
516
+ /* Animation variables (customizable via inline styles) */
517
+ .q-animate {
518
+ --q-anim-duration: 300ms;
519
+ --q-anim-delay: 0ms;
520
+ --q-anim-easing: ease;
521
+ --q-anim-repeat: 1;
522
+ --q-anim-direction: normal;
523
+ }
524
+
525
+ /* ----------------------------------------
526
+ Fade Animations
527
+ ---------------------------------------- */
528
+ @keyframes q-fade-in {
529
+ from { opacity: 0; }
530
+ to { opacity: 1; }
531
+ }
532
+
533
+ @keyframes q-fade-out {
534
+ from { opacity: 1; }
535
+ to { opacity: 0; }
536
+ }
537
+
538
+ .q-anim-fade-in,
539
+ .q-anim-fade {
540
+ animation: q-fade-in var(--q-anim-duration) var(--q-anim-easing) var(--q-anim-delay) var(--q-anim-repeat) var(--q-anim-direction) both;
541
+ }
542
+
543
+ .q-anim-fade-out {
544
+ animation: q-fade-out var(--q-anim-duration) var(--q-anim-easing) var(--q-anim-delay) var(--q-anim-repeat) var(--q-anim-direction) both;
545
+ }
546
+
547
+ /* ----------------------------------------
548
+ Slide Animations
549
+ ---------------------------------------- */
550
+ @keyframes q-slide-in-left {
551
+ from { transform: translateX(-100%); opacity: 0; }
552
+ to { transform: translateX(0); opacity: 1; }
553
+ }
554
+
555
+ @keyframes q-slide-in-right {
556
+ from { transform: translateX(100%); opacity: 0; }
557
+ to { transform: translateX(0); opacity: 1; }
558
+ }
559
+
560
+ @keyframes q-slide-in-up {
561
+ from { transform: translateY(100%); opacity: 0; }
562
+ to { transform: translateY(0); opacity: 1; }
563
+ }
564
+
565
+ @keyframes q-slide-in-down {
566
+ from { transform: translateY(-100%); opacity: 0; }
567
+ to { transform: translateY(0); opacity: 1; }
568
+ }
569
+
570
+ .q-anim-slide,
571
+ .q-anim-slide-left,
572
+ .q-anim-slide-in-left {
573
+ animation: q-slide-in-left var(--q-anim-duration) var(--q-anim-easing) var(--q-anim-delay) var(--q-anim-repeat) var(--q-anim-direction) both;
574
+ }
575
+
576
+ .q-anim-slide-right,
577
+ .q-anim-slide-in-right {
578
+ animation: q-slide-in-right var(--q-anim-duration) var(--q-anim-easing) var(--q-anim-delay) var(--q-anim-repeat) var(--q-anim-direction) both;
579
+ }
580
+
581
+ .q-anim-slide-up,
582
+ .q-anim-slide-in-up {
583
+ animation: q-slide-in-up var(--q-anim-duration) var(--q-anim-easing) var(--q-anim-delay) var(--q-anim-repeat) var(--q-anim-direction) both;
584
+ }
585
+
586
+ .q-anim-slide-down,
587
+ .q-anim-slide-in-down {
588
+ animation: q-slide-in-down var(--q-anim-duration) var(--q-anim-easing) var(--q-anim-delay) var(--q-anim-repeat) var(--q-anim-direction) both;
589
+ }
590
+
591
+ /* ----------------------------------------
592
+ Scale Animations
593
+ ---------------------------------------- */
594
+ @keyframes q-scale-in {
595
+ from { transform: scale(0); opacity: 0; }
596
+ to { transform: scale(1); opacity: 1; }
597
+ }
598
+
599
+ @keyframes q-scale-out {
600
+ from { transform: scale(1); opacity: 1; }
601
+ to { transform: scale(0); opacity: 0; }
602
+ }
603
+
604
+ .q-anim-scale,
605
+ .q-anim-scale-in {
606
+ animation: q-scale-in var(--q-anim-duration) var(--q-anim-easing) var(--q-anim-delay) var(--q-anim-repeat) var(--q-anim-direction) both;
607
+ }
608
+
609
+ .q-anim-scale-out {
610
+ animation: q-scale-out var(--q-anim-duration) var(--q-anim-easing) var(--q-anim-delay) var(--q-anim-repeat) var(--q-anim-direction) both;
611
+ }
612
+
613
+ /* ----------------------------------------
614
+ Rotate Animations
615
+ ---------------------------------------- */
616
+ @keyframes q-rotate {
617
+ from { transform: rotate(0deg); }
618
+ to { transform: rotate(360deg); }
619
+ }
620
+
621
+ @keyframes q-rotate-in {
622
+ from { transform: rotate(-180deg); opacity: 0; }
623
+ to { transform: rotate(0deg); opacity: 1; }
624
+ }
625
+
626
+ .q-anim-rotate {
627
+ animation: q-rotate var(--q-anim-duration) var(--q-anim-easing) var(--q-anim-delay) var(--q-anim-repeat) var(--q-anim-direction) both;
628
+ }
629
+
630
+ .q-anim-rotate-in {
631
+ animation: q-rotate-in var(--q-anim-duration) var(--q-anim-easing) var(--q-anim-delay) var(--q-anim-repeat) var(--q-anim-direction) both;
632
+ }
633
+
634
+ /* ----------------------------------------
635
+ Bounce Animation
636
+ ---------------------------------------- */
637
+ @keyframes q-bounce {
638
+ 0%, 20%, 53%, 100% {
639
+ animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
640
+ transform: translateY(0);
641
+ }
642
+ 40%, 43% {
643
+ animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
644
+ transform: translateY(-30px);
645
+ }
646
+ 70% {
647
+ animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
648
+ transform: translateY(-15px);
649
+ }
650
+ 80% {
651
+ transform: translateY(0);
652
+ }
653
+ 90% {
654
+ transform: translateY(-4px);
655
+ }
656
+ }
657
+
658
+ .q-anim-bounce {
659
+ animation: q-bounce var(--q-anim-duration) var(--q-anim-easing) var(--q-anim-delay) var(--q-anim-repeat) var(--q-anim-direction) both;
660
+ transform-origin: center bottom;
661
+ }
662
+
663
+ /* ----------------------------------------
664
+ Pulse Animation
665
+ ---------------------------------------- */
666
+ @keyframes q-pulse {
667
+ 0% { transform: scale(1); }
668
+ 50% { transform: scale(1.05); }
669
+ 100% { transform: scale(1); }
670
+ }
671
+
672
+ .q-anim-pulse {
673
+ animation: q-pulse var(--q-anim-duration) var(--q-anim-easing) var(--q-anim-delay) var(--q-anim-repeat) var(--q-anim-direction) both;
674
+ }
675
+
676
+ /* ----------------------------------------
677
+ Shake Animation
678
+ ---------------------------------------- */
679
+ @keyframes q-shake {
680
+ 0%, 100% { transform: translateX(0); }
681
+ 10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
682
+ 20%, 40%, 60%, 80% { transform: translateX(5px); }
683
+ }
684
+
685
+ .q-anim-shake {
686
+ animation: q-shake var(--q-anim-duration) var(--q-anim-easing) var(--q-anim-delay) var(--q-anim-repeat) var(--q-anim-direction) both;
687
+ }
688
+
689
+ /* ----------------------------------------
690
+ Trigger States
691
+ ---------------------------------------- */
692
+ /* on-load: Play immediately (default) */
693
+ .q-trigger-on-load {
694
+ /* Animation plays on page load by default */
695
+ }
696
+
697
+ /* on-hover: Play on mouse hover */
698
+ .q-trigger-on-hover {
699
+ animation-play-state: paused;
700
+ }
701
+ .q-trigger-on-hover:hover {
702
+ animation-play-state: running;
703
+ }
704
+
705
+ /* on-click: Play on click (requires JS) */
706
+ .q-trigger-on-click {
707
+ animation-play-state: paused;
708
+ }
709
+ .q-trigger-on-click.q-anim-active {
710
+ animation-play-state: running;
711
+ }
712
+
713
+ /* on-visible: Play when visible (requires IntersectionObserver JS) */
714
+ .q-trigger-on-visible {
715
+ animation-play-state: paused;
716
+ }
717
+ .q-trigger-on-visible.q-anim-visible {
718
+ animation-play-state: running;
719
+ }
720
+
721
+ /* ----------------------------------------
722
+ Transition Effects (for inline use)
723
+ ---------------------------------------- */
724
+ .q-transition {
725
+ transition-property: transform, opacity;
726
+ transition-duration: var(--q-anim-duration);
727
+ transition-timing-function: var(--q-anim-easing);
728
+ transition-delay: var(--q-anim-delay);
729
+ }
730
+
731
+ /* Common transition presets */
732
+ .q-transition-scale:hover {
733
+ transform: scale(var(--q-transition-scale, 1.05));
734
+ }
735
+
736
+ .q-transition-scale:active {
737
+ transform: scale(var(--q-transition-scale-active, 0.95));
738
+ }
739
+
740
+ .q-transition-fade:hover {
741
+ opacity: var(--q-transition-opacity, 0.8);
742
+ }
743
+
744
+ .q-transition-lift:hover {
745
+ transform: translateY(var(--q-transition-lift, -2px));
746
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
747
+ }
748
+
749
+ /* ----------------------------------------
750
+ Easing Functions
751
+ ---------------------------------------- */
752
+ .q-easing-linear { --q-anim-easing: linear; }
753
+ .q-easing-ease { --q-anim-easing: ease; }
754
+ .q-easing-ease-in { --q-anim-easing: ease-in; }
755
+ .q-easing-ease-out { --q-anim-easing: ease-out; }
756
+ .q-easing-ease-in-out { --q-anim-easing: ease-in-out; }
757
+ .q-easing-spring { --q-anim-easing: cubic-bezier(0.175, 0.885, 0.32, 1.275); }
758
+ .q-easing-bounce { --q-anim-easing: cubic-bezier(0.68, -0.55, 0.265, 1.55); }
759
+
760
+ /* ----------------------------------------
761
+ Reduced Motion Support
762
+ ---------------------------------------- */
763
+ @media (prefers-reduced-motion: reduce) {
764
+ .q-animate,
765
+ .q-transition,
766
+ [class*="q-anim-"] {
767
+ animation: none !important;
768
+ transition: none !important;
769
+ }
770
+ }
771
+ """
772
+
773
+
774
+ # ==========================================================================
775
+ # HTML Template
776
+ # ==========================================================================
777
+
778
+ HTML_TEMPLATE = """\
779
+ <!DOCTYPE html>
780
+ <html lang="en">
781
+ <head>
782
+ <meta charset="UTF-8">
783
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
784
+ <title>{title}</title>
785
+ <style>
786
+ {css}
787
+ </style>
788
+ </head>
789
+ <body>
790
+ {body}
791
+ {js}
792
+ </body>
793
+ </html>
794
+ """
795
+
796
+
797
+ # ==========================================================================
798
+ # Tab JS
799
+ # ==========================================================================
800
+
801
+ TAB_JS = """\
802
+ <script>
803
+ document.querySelectorAll('.q-tab-header').forEach(function(btn) {
804
+ btn.addEventListener('click', function() {
805
+ var group = this.dataset.tabGroup;
806
+ var target = this.dataset.tab;
807
+ document.querySelectorAll('.q-tab-header[data-tab-group="' + group + '"]').forEach(function(b) { b.classList.remove('active'); });
808
+ document.querySelectorAll('.q-tab-content[data-tab-group="' + group + '"]').forEach(function(c) { c.classList.remove('active'); });
809
+ this.classList.add('active');
810
+ document.getElementById(target).classList.add('active');
811
+ });
812
+ });
813
+ </script>
814
+ """
815
+
816
+
817
+ # ==========================================================================
818
+ # Animation JS
819
+ # ==========================================================================
820
+
821
+ ANIMATION_JS = """\
822
+ <script>
823
+ // Quantum UI Animation Manager
824
+ window.__quantumAnimate = (function() {
825
+ 'use strict';
826
+
827
+ // IntersectionObserver for on-visible trigger
828
+ var visibilityObserver = null;
829
+
830
+ function initVisibilityObserver() {
831
+ if (visibilityObserver) return;
832
+ if (typeof IntersectionObserver === 'undefined') {
833
+ console.warn('IntersectionObserver not supported; on-visible trigger disabled');
834
+ return;
835
+ }
836
+
837
+ visibilityObserver = new IntersectionObserver(function(entries) {
838
+ entries.forEach(function(entry) {
839
+ if (entry.isIntersecting) {
840
+ entry.target.classList.add('q-anim-visible');
841
+ // Optionally unobserve after first trigger
842
+ if (entry.target.dataset.animOnce !== 'false') {
843
+ visibilityObserver.unobserve(entry.target);
844
+ }
845
+ }
846
+ });
847
+ }, {
848
+ threshold: 0.1,
849
+ rootMargin: '0px 0px -50px 0px'
850
+ });
851
+ }
852
+
853
+ function setupClickTriggers() {
854
+ document.querySelectorAll('.q-trigger-on-click').forEach(function(el) {
855
+ el.addEventListener('click', function() {
856
+ // Remove and re-add animation class to restart
857
+ var classes = this.className.match(/q-anim-[\\w-]+/g) || [];
858
+ var self = this;
859
+ classes.forEach(function(cls) {
860
+ self.classList.remove(cls);
861
+ });
862
+ // Force reflow
863
+ void this.offsetWidth;
864
+ classes.forEach(function(cls) {
865
+ self.classList.add(cls);
866
+ });
867
+ this.classList.add('q-anim-active');
868
+ });
869
+
870
+ // Remove active class when animation ends
871
+ el.addEventListener('animationend', function() {
872
+ this.classList.remove('q-anim-active');
873
+ });
874
+ });
875
+ }
876
+
877
+ function setupVisibilityTriggers() {
878
+ initVisibilityObserver();
879
+ if (!visibilityObserver) return;
880
+
881
+ document.querySelectorAll('.q-trigger-on-visible').forEach(function(el) {
882
+ visibilityObserver.observe(el);
883
+ });
884
+ }
885
+
886
+ function parseTransition(transitionStr) {
887
+ // Parse transition shorthand: "scale:0.95:100ms" or "scale:0.95"
888
+ if (!transitionStr) return null;
889
+ var parts = transitionStr.split(':');
890
+ return {
891
+ type: parts[0] || 'scale',
892
+ value: parts[1] || '0.95',
893
+ duration: parts[2] || '150ms'
894
+ };
895
+ }
896
+
897
+ function applyTransition(el, transitionStr) {
898
+ var t = parseTransition(transitionStr);
899
+ if (!t) return;
900
+
901
+ el.classList.add('q-transition');
902
+ el.style.setProperty('--q-anim-duration', t.duration);
903
+
904
+ if (t.type === 'scale') {
905
+ el.style.setProperty('--q-transition-scale', t.value);
906
+ el.classList.add('q-transition-scale');
907
+ } else if (t.type === 'fade') {
908
+ el.style.setProperty('--q-transition-opacity', t.value);
909
+ el.classList.add('q-transition-fade');
910
+ } else if (t.type === 'lift') {
911
+ el.style.setProperty('--q-transition-lift', t.value);
912
+ el.classList.add('q-transition-lift');
913
+ }
914
+ }
915
+
916
+ function setupTransitions() {
917
+ document.querySelectorAll('[data-transition]').forEach(function(el) {
918
+ applyTransition(el, el.dataset.transition);
919
+ });
920
+ }
921
+
922
+ // Initialize all animation triggers
923
+ function init() {
924
+ setupClickTriggers();
925
+ setupVisibilityTriggers();
926
+ setupTransitions();
927
+ }
928
+
929
+ // Run on DOM ready
930
+ if (document.readyState === 'loading') {
931
+ document.addEventListener('DOMContentLoaded', init);
932
+ } else {
933
+ init();
934
+ }
935
+
936
+ // Public API
937
+ return {
938
+ init: init,
939
+ parseTransition: parseTransition,
940
+ applyTransition: applyTransition,
941
+
942
+ // Programmatically trigger animation on element
943
+ trigger: function(el, animationType) {
944
+ if (typeof el === 'string') {
945
+ el = document.querySelector(el);
946
+ }
947
+ if (!el) return;
948
+
949
+ // Remove existing animation classes
950
+ var classes = el.className.match(/q-anim-[\\w-]+/g) || [];
951
+ classes.forEach(function(cls) {
952
+ el.classList.remove(cls);
953
+ });
954
+
955
+ // Force reflow
956
+ void el.offsetWidth;
957
+
958
+ // Add new animation
959
+ if (animationType) {
960
+ el.classList.add('q-anim-' + animationType);
961
+ }
962
+ },
963
+
964
+ // Reset animation (for replay)
965
+ reset: function(el) {
966
+ if (typeof el === 'string') {
967
+ el = document.querySelector(el);
968
+ }
969
+ if (!el) return;
970
+
971
+ var classes = el.className.match(/q-anim-[\\w-]+/g) || [];
972
+ classes.forEach(function(cls) {
973
+ el.classList.remove(cls);
974
+ });
975
+ el.classList.remove('q-anim-active', 'q-anim-visible');
976
+ void el.offsetWidth; // Force reflow
977
+ }
978
+ };
979
+ })();
980
+ </script>
981
+ """
982
+
983
+
984
+ # ==========================================================================
985
+ # Form Validation CSS
986
+ # ==========================================================================
987
+
988
+ VALIDATION_CSS = """\
989
+ /* ============================================
990
+ Quantum UI Form Validation Styles
991
+ ============================================ */
992
+
993
+ /* Input wrapper for validation */
994
+ .q-input-wrapper {
995
+ position: relative;
996
+ width: 100%;
997
+ }
998
+
999
+ /* Error message display */
1000
+ .q-error-message {
1001
+ color: var(--q-danger);
1002
+ font-size: var(--q-font-sm);
1003
+ margin-top: 4px;
1004
+ min-height: 1.2em;
1005
+ }
1006
+
1007
+ /* Validation summary */
1008
+ .q-validation-summary {
1009
+ background: rgba(239, 68, 68, 0.1);
1010
+ border: 1px solid var(--q-danger);
1011
+ border-radius: var(--q-radius);
1012
+ padding: 12px 16px;
1013
+ margin-bottom: 16px;
1014
+ }
1015
+
1016
+ .q-validation-summary ul {
1017
+ margin: 0;
1018
+ padding-left: 20px;
1019
+ }
1020
+
1021
+ .q-validation-summary li {
1022
+ color: var(--q-danger);
1023
+ font-size: var(--q-font-sm);
1024
+ }
1025
+
1026
+ /* Input states */
1027
+ .q-input.q-invalid,
1028
+ .q-input:invalid:not(:placeholder-shown) {
1029
+ border-color: var(--q-danger);
1030
+ }
1031
+
1032
+ .q-input.q-invalid:focus,
1033
+ .q-input:invalid:not(:placeholder-shown):focus {
1034
+ box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.15);
1035
+ }
1036
+
1037
+ .q-input.q-valid,
1038
+ .q-input:valid:not(:placeholder-shown) {
1039
+ border-color: var(--q-success);
1040
+ }
1041
+
1042
+ .q-input.q-valid:focus,
1043
+ .q-input:valid:not(:placeholder-shown):focus {
1044
+ box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.15);
1045
+ }
1046
+
1047
+ /* Required field indicator */
1048
+ .q-formitem-label.q-required::after {
1049
+ content: " *";
1050
+ color: var(--q-danger);
1051
+ }
1052
+
1053
+ /* Shake animation for invalid fields */
1054
+ @keyframes q-shake-invalid {
1055
+ 0%, 100% { transform: translateX(0); }
1056
+ 10%, 30%, 50%, 70%, 90% { transform: translateX(-4px); }
1057
+ 20%, 40%, 60%, 80% { transform: translateX(4px); }
1058
+ }
1059
+
1060
+ .q-input.q-shake {
1061
+ animation: q-shake-invalid 0.4s ease-in-out;
1062
+ }
1063
+ """
1064
+
1065
+
1066
+ # ==========================================================================
1067
+ # Form Validation JS
1068
+ # ==========================================================================
1069
+
1070
+ VALIDATION_JS = """\
1071
+ <script>
1072
+ // Quantum UI Form Validation System
1073
+ window.__qValidation = (function() {
1074
+ 'use strict';
1075
+
1076
+ // Registered custom validators per form
1077
+ var formValidators = {};
1078
+
1079
+ // Built-in validation types
1080
+ var builtInTypes = {
1081
+ email: /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/,
1082
+ url: /^https?:\\/\\/[\\w\\-]+(\\.[\\w\\-]+)+[/#?]?.*$/,
1083
+ phone: /^[\\+]?[(]?[0-9]{1,3}[)]?[-\\s\\.]?[0-9]{1,4}[-\\s\\.]?[0-9]{4,6}$/,
1084
+ alphanumeric: /^[a-zA-Z0-9]+$/,
1085
+ alpha: /^[a-zA-Z]+$/,
1086
+ numeric: /^[0-9]+$/,
1087
+ integer: /^-?[0-9]+$/,
1088
+ decimal: /^-?[0-9]+(\\.[0-9]+)?$/
1089
+ };
1090
+
1091
+ // Default error messages
1092
+ var defaultMessages = {
1093
+ required: 'This field is required',
1094
+ email: 'Please enter a valid email address',
1095
+ url: 'Please enter a valid URL',
1096
+ phone: 'Please enter a valid phone number',
1097
+ pattern: 'Please match the required format',
1098
+ minlength: 'Please enter at least {0} characters',
1099
+ maxlength: 'Please enter no more than {0} characters',
1100
+ min: 'Please enter a value greater than or equal to {0}',
1101
+ max: 'Please enter a value less than or equal to {0}',
1102
+ match: 'Fields do not match'
1103
+ };
1104
+
1105
+ // Format message with parameters
1106
+ function formatMessage(template, params) {
1107
+ if (!params) return template;
1108
+ if (!Array.isArray(params)) params = [params];
1109
+ return template.replace(/\\{(\\d+)\\}/g, function(match, index) {
1110
+ return typeof params[index] !== 'undefined' ? params[index] : match;
1111
+ });
1112
+ }
1113
+
1114
+ // Show error for an input
1115
+ function showError(input, message) {
1116
+ input.classList.add('q-invalid');
1117
+ input.classList.remove('q-valid');
1118
+
1119
+ var errorEl = document.getElementById(input.id + '-error');
1120
+ if (errorEl) {
1121
+ errorEl.textContent = message;
1122
+ errorEl.style.display = 'block';
1123
+ }
1124
+
1125
+ // Shake animation
1126
+ input.classList.add('q-shake');
1127
+ setTimeout(function() {
1128
+ input.classList.remove('q-shake');
1129
+ }, 400);
1130
+ }
1131
+
1132
+ // Clear error for an input
1133
+ function clearError(input) {
1134
+ input.classList.remove('q-invalid');
1135
+ input.classList.add('q-valid');
1136
+
1137
+ var errorEl = document.getElementById(input.id + '-error');
1138
+ if (errorEl) {
1139
+ errorEl.style.display = 'none';
1140
+ }
1141
+ }
1142
+
1143
+ // Validate a single input
1144
+ function validateInput(input, form) {
1145
+ var value = input.value;
1146
+ var customMessage = input.dataset.errorMessage;
1147
+
1148
+ // Required validation
1149
+ if (input.hasAttribute('required') && !value.trim()) {
1150
+ showError(input, customMessage || defaultMessages.required);
1151
+ return false;
1152
+ }
1153
+
1154
+ // Skip other validations if empty and not required
1155
+ if (!value.trim()) {
1156
+ clearError(input);
1157
+ return true;
1158
+ }
1159
+
1160
+ // Type validation
1161
+ var inputType = input.type;
1162
+ if (inputType === 'email' && builtInTypes.email && !builtInTypes.email.test(value)) {
1163
+ showError(input, customMessage || defaultMessages.email);
1164
+ return false;
1165
+ }
1166
+
1167
+ if (inputType === 'url' && builtInTypes.url && !builtInTypes.url.test(value)) {
1168
+ showError(input, customMessage || defaultMessages.url);
1169
+ return false;
1170
+ }
1171
+
1172
+ // Pattern validation
1173
+ var pattern = input.getAttribute('pattern');
1174
+ if (pattern) {
1175
+ var regex = new RegExp('^' + pattern + '$');
1176
+ if (!regex.test(value)) {
1177
+ showError(input, customMessage || defaultMessages.pattern);
1178
+ return false;
1179
+ }
1180
+ }
1181
+
1182
+ // Min/Max length validation
1183
+ var minlength = input.getAttribute('minlength');
1184
+ if (minlength && value.length < parseInt(minlength, 10)) {
1185
+ showError(input, customMessage || formatMessage(defaultMessages.minlength, minlength));
1186
+ return false;
1187
+ }
1188
+
1189
+ var maxlength = input.getAttribute('maxlength');
1190
+ if (maxlength && value.length > parseInt(maxlength, 10)) {
1191
+ showError(input, customMessage || formatMessage(defaultMessages.maxlength, maxlength));
1192
+ return false;
1193
+ }
1194
+
1195
+ // Min/Max value validation (for number inputs)
1196
+ if (inputType === 'number') {
1197
+ var numValue = parseFloat(value);
1198
+ var min = input.getAttribute('min');
1199
+ if (min !== null && numValue < parseFloat(min)) {
1200
+ showError(input, customMessage || formatMessage(defaultMessages.min, min));
1201
+ return false;
1202
+ }
1203
+
1204
+ var max = input.getAttribute('max');
1205
+ if (max !== null && numValue > parseFloat(max)) {
1206
+ showError(input, customMessage || formatMessage(defaultMessages.max, max));
1207
+ return false;
1208
+ }
1209
+ }
1210
+
1211
+ // Custom validators
1212
+ var customValidators = input.dataset.validators;
1213
+ if (customValidators && form) {
1214
+ var formId = form.id;
1215
+ var validators = formValidators[formId] || [];
1216
+ var validatorNames = customValidators.split(',').map(function(s) { return s.trim(); });
1217
+
1218
+ for (var i = 0; i < validatorNames.length; i++) {
1219
+ var validatorName = validatorNames[i];
1220
+ var validator = validators.find(function(v) { return v.name === validatorName; });
1221
+ if (validator) {
1222
+ var result = runValidator(validator, value, form);
1223
+ if (!result.valid) {
1224
+ showError(input, result.message || customMessage || 'Validation failed');
1225
+ return false;
1226
+ }
1227
+ }
1228
+ }
1229
+ }
1230
+
1231
+ clearError(input);
1232
+ return true;
1233
+ }
1234
+
1235
+ // Run a custom validator
1236
+ function runValidator(validator, value, form) {
1237
+ // Pattern validation
1238
+ if (validator.pattern) {
1239
+ var regex = new RegExp(validator.pattern);
1240
+ if (!regex.test(value)) {
1241
+ return { valid: false, message: validator.message };
1242
+ }
1243
+ }
1244
+
1245
+ // Type-based validation
1246
+ if (validator.type && builtInTypes[validator.type]) {
1247
+ if (!builtInTypes[validator.type].test(value)) {
1248
+ return { valid: false, message: validator.message || defaultMessages[validator.type] };
1249
+ }
1250
+ }
1251
+
1252
+ // Match validation (compare to another field)
1253
+ if (validator.match && form) {
1254
+ var otherInput = form.querySelector('[name="' + validator.match + '"]');
1255
+ if (otherInput && value !== otherInput.value) {
1256
+ return { valid: false, message: validator.message || defaultMessages.match };
1257
+ }
1258
+ }
1259
+
1260
+ // Min/Max validation
1261
+ if (validator.minlength && value.length < validator.minlength) {
1262
+ return { valid: false, message: validator.message || formatMessage(defaultMessages.minlength, validator.minlength) };
1263
+ }
1264
+ if (validator.maxlength && value.length > validator.maxlength) {
1265
+ return { valid: false, message: validator.message || formatMessage(defaultMessages.maxlength, validator.maxlength) };
1266
+ }
1267
+
1268
+ // Custom expression
1269
+ if (typeof validator.expression === 'function') {
1270
+ try {
1271
+ var result = validator.expression(value, form);
1272
+ if (!result) {
1273
+ return { valid: false, message: validator.message };
1274
+ }
1275
+ } catch (e) {
1276
+ console.warn('Validator expression error:', e);
1277
+ return { valid: false, message: 'Validation error' };
1278
+ }
1279
+ }
1280
+
1281
+ return { valid: true };
1282
+ }
1283
+
1284
+ // Validate entire form
1285
+ function validateForm(formId) {
1286
+ var form = document.getElementById(formId);
1287
+ if (!form) return true;
1288
+
1289
+ var inputs = form.querySelectorAll('input, select, textarea');
1290
+ var allValid = true;
1291
+ var errors = [];
1292
+
1293
+ inputs.forEach(function(input) {
1294
+ if (input.id && !validateInput(input, form)) {
1295
+ allValid = false;
1296
+ var errorEl = document.getElementById(input.id + '-error');
1297
+ if (errorEl) {
1298
+ errors.push(errorEl.textContent);
1299
+ }
1300
+ }
1301
+ });
1302
+
1303
+ // Form-level validators (not tied to specific fields)
1304
+ var formLevelValidators = (formValidators[formId] || []).filter(function(v) {
1305
+ return v.field;
1306
+ });
1307
+
1308
+ formLevelValidators.forEach(function(validator) {
1309
+ var targetInput = form.querySelector('[name="' + validator.field + '"]');
1310
+ if (targetInput) {
1311
+ var result = runValidator(validator, targetInput.value, form);
1312
+ if (!result.valid) {
1313
+ showError(targetInput, result.message);
1314
+ allValid = false;
1315
+ errors.push(result.message);
1316
+ }
1317
+ }
1318
+ });
1319
+
1320
+ // Update validation summary if present
1321
+ var summaryEl = document.getElementById(formId + '-summary');
1322
+ if (summaryEl) {
1323
+ if (errors.length > 0) {
1324
+ var ul = '<ul>' + errors.map(function(e) { return '<li>' + e + '</li>'; }).join('') + '</ul>';
1325
+ summaryEl.innerHTML = ul;
1326
+ summaryEl.style.display = 'block';
1327
+ } else {
1328
+ summaryEl.style.display = 'none';
1329
+ }
1330
+ }
1331
+
1332
+ return allValid;
1333
+ }
1334
+
1335
+ // Setup real-time validation on blur/input
1336
+ function setupRealtimeValidation() {
1337
+ document.addEventListener('blur', function(e) {
1338
+ var input = e.target;
1339
+ if (input.matches && input.matches('input, select, textarea')) {
1340
+ var form = input.closest('form');
1341
+ if (form && form.dataset.validation) {
1342
+ validateInput(input, form);
1343
+ }
1344
+ }
1345
+ }, true);
1346
+
1347
+ document.addEventListener('input', function(e) {
1348
+ var input = e.target;
1349
+ if (input.matches && input.matches('input, select, textarea') && input.classList.contains('q-invalid')) {
1350
+ var form = input.closest('form');
1351
+ if (form && form.dataset.validation) {
1352
+ validateInput(input, form);
1353
+ }
1354
+ }
1355
+ }, true);
1356
+ }
1357
+
1358
+ // Initialize on DOM ready
1359
+ if (document.readyState === 'loading') {
1360
+ document.addEventListener('DOMContentLoaded', setupRealtimeValidation);
1361
+ } else {
1362
+ setupRealtimeValidation();
1363
+ }
1364
+
1365
+ // Public API
1366
+ return {
1367
+ validateForm: validateForm,
1368
+ validateInput: validateInput,
1369
+ showError: showError,
1370
+ clearError: clearError,
1371
+ registerValidator: function(formId, validator) {
1372
+ if (!formValidators[formId]) {
1373
+ formValidators[formId] = [];
1374
+ }
1375
+ formValidators[formId].push(validator);
1376
+ },
1377
+ addValidationType: function(name, regex, defaultMessage) {
1378
+ builtInTypes[name] = regex;
1379
+ if (defaultMessage) {
1380
+ defaultMessages[name] = defaultMessage;
1381
+ }
1382
+ }
1383
+ };
1384
+ })();
1385
+ </script>
1386
+ """
1387
+
1388
+
1389
+ # ==========================================================================
1390
+ # Toast Notification JS
1391
+ # ==========================================================================
1392
+
1393
+ TOAST_JS = r"""
1394
+ // String RAW de proposito: era uma tripla NAO-raw, entao o Python
1395
+ // colapsava os \' e o JS saia com `dismiss('' + id + '')` — erro de
1396
+ // sintaxe que derrubava ESTE MODULO INTEIRO no browser. O resultado era
1397
+ // __quantumToast / __quantumCalendar indefinidos: nenhum toast fechava e
1398
+ // nenhum calendario navegava, em nenhuma pagina, desde sempre.
1399
+ <script>
1400
+ // Quantum UI Toast Notification System
1401
+ window.__quantumToast = (function() {
1402
+ 'use strict';
1403
+
1404
+ var containers = {};
1405
+ var toastId = 0;
1406
+
1407
+ // Icons for variants
1408
+ var icons = {
1409
+ info: '&#9432;',
1410
+ success: '&#10003;',
1411
+ warning: '&#9888;',
1412
+ danger: '&#10007;'
1413
+ };
1414
+
1415
+ // Get or create container for position
1416
+ function getContainer(position) {
1417
+ if (containers[position]) return containers[position];
1418
+
1419
+ var container = document.createElement('div');
1420
+ container.className = 'q-toast-container q-toast-' + position;
1421
+ document.body.appendChild(container);
1422
+ containers[position] = container;
1423
+ return container;
1424
+ }
1425
+
1426
+ // Create toast element
1427
+ function createToast(options) {
1428
+ var id = 'toast-' + (++toastId);
1429
+ var toast = document.createElement('div');
1430
+ toast.id = id;
1431
+ toast.className = 'q-toast q-toast-' + (options.variant || 'info');
1432
+
1433
+ var html = '';
1434
+
1435
+ // Icon
1436
+ if (options.icon !== false) {
1437
+ html += '<span class="q-toast-icon">' + (options.icon || icons[options.variant] || icons.info) + '</span>';
1438
+ }
1439
+
1440
+ // Content
1441
+ html += '<div class="q-toast-content">';
1442
+ if (options.title) {
1443
+ html += '<div class="q-toast-title">' + options.title + '</div>';
1444
+ }
1445
+ html += '<div class="q-toast-message">' + (options.message || '') + '</div>';
1446
+ html += '</div>';
1447
+
1448
+ // Close button
1449
+ if (options.dismissible !== false) {
1450
+ html += '<button class="q-toast-close" onclick="__quantumToast.dismiss(\'' + id + '\')">&times;</button>';
1451
+ }
1452
+
1453
+ toast.innerHTML = html;
1454
+ return toast;
1455
+ }
1456
+
1457
+ // Show toast
1458
+ function show(options) {
1459
+ var position = options.position || 'top-right';
1460
+ var container = getContainer(position);
1461
+ var toast = createToast(options);
1462
+
1463
+ container.appendChild(toast);
1464
+
1465
+ // Auto dismiss
1466
+ var duration = options.duration !== undefined ? options.duration : 3000;
1467
+ if (duration > 0) {
1468
+ setTimeout(function() {
1469
+ dismiss(toast.id);
1470
+ }, duration);
1471
+ }
1472
+
1473
+ return toast.id;
1474
+ }
1475
+
1476
+ // Dismiss toast
1477
+ function dismiss(id) {
1478
+ var toast = document.getElementById(id);
1479
+ if (!toast) return;
1480
+
1481
+ toast.classList.add('q-toast-out');
1482
+ setTimeout(function() {
1483
+ if (toast.parentNode) {
1484
+ toast.parentNode.removeChild(toast);
1485
+ }
1486
+ }, 300);
1487
+ }
1488
+
1489
+ // Dismiss all toasts
1490
+ function dismissAll() {
1491
+ Object.keys(containers).forEach(function(pos) {
1492
+ var container = containers[pos];
1493
+ var toasts = container.querySelectorAll('.q-toast');
1494
+ toasts.forEach(function(t) { dismiss(t.id); });
1495
+ });
1496
+ }
1497
+
1498
+ // Shorthand methods
1499
+ function info(message, options) {
1500
+ return show(Object.assign({ message: message, variant: 'info' }, options || {}));
1501
+ }
1502
+
1503
+ function success(message, options) {
1504
+ return show(Object.assign({ message: message, variant: 'success' }, options || {}));
1505
+ }
1506
+
1507
+ function warning(message, options) {
1508
+ return show(Object.assign({ message: message, variant: 'warning' }, options || {}));
1509
+ }
1510
+
1511
+ function danger(message, options) {
1512
+ return show(Object.assign({ message: message, variant: 'danger' }, options || {}));
1513
+ }
1514
+
1515
+ return {
1516
+ show: show,
1517
+ dismiss: dismiss,
1518
+ dismissAll: dismissAll,
1519
+ info: info,
1520
+ success: success,
1521
+ warning: warning,
1522
+ danger: danger,
1523
+ error: danger // alias
1524
+ };
1525
+ })();
1526
+ </script>
1527
+ """
1528
+
1529
+
1530
+ # ==========================================================================
1531
+ # Carousel JS
1532
+ # ==========================================================================
1533
+
1534
+ CAROUSEL_JS = """\
1535
+ <script>
1536
+ // Quantum UI Carousel System
1537
+ window.__quantumCarousel = (function() {
1538
+ 'use strict';
1539
+
1540
+ var carousels = {};
1541
+
1542
+ function init(id, options) {
1543
+ var carousel = document.getElementById(id);
1544
+ if (!carousel) return;
1545
+
1546
+ options = options || {};
1547
+ var track = carousel.querySelector('.q-carousel-track');
1548
+ var slides = carousel.querySelectorAll('.q-carousel-slide');
1549
+ var indicators = carousel.querySelectorAll('.q-carousel-indicator');
1550
+ var prevBtn = carousel.querySelector('.q-carousel-prev');
1551
+ var nextBtn = carousel.querySelector('.q-carousel-next');
1552
+
1553
+ var state = {
1554
+ current: options.current || 0,
1555
+ total: slides.length,
1556
+ autoPlay: options.autoPlay || false,
1557
+ interval: options.interval || 5000,
1558
+ loop: options.loop !== false,
1559
+ animation: options.animation || 'slide',
1560
+ timer: null
1561
+ };
1562
+
1563
+ carousels[id] = state;
1564
+
1565
+ function goTo(index) {
1566
+ if (index < 0) {
1567
+ index = state.loop ? state.total - 1 : 0;
1568
+ } else if (index >= state.total) {
1569
+ index = state.loop ? 0 : state.total - 1;
1570
+ }
1571
+
1572
+ state.current = index;
1573
+
1574
+ if (state.animation === 'slide') {
1575
+ track.style.transform = 'translateX(-' + (index * 100) + '%)';
1576
+ } else {
1577
+ // Fade animation
1578
+ slides.forEach(function(s, i) {
1579
+ s.classList.toggle('active', i === index);
1580
+ });
1581
+ }
1582
+
1583
+ // Update indicators
1584
+ indicators.forEach(function(ind, i) {
1585
+ ind.classList.toggle('active', i === index);
1586
+ });
1587
+
1588
+ // Update arrow states if not looping
1589
+ if (!state.loop) {
1590
+ if (prevBtn) prevBtn.disabled = index === 0;
1591
+ if (nextBtn) nextBtn.disabled = index === state.total - 1;
1592
+ }
1593
+
1594
+ // Callback
1595
+ if (options.onChange) {
1596
+ options.onChange(index);
1597
+ }
1598
+ }
1599
+
1600
+ function next() {
1601
+ goTo(state.current + 1);
1602
+ }
1603
+
1604
+ function prev() {
1605
+ goTo(state.current - 1);
1606
+ }
1607
+
1608
+ function startAutoPlay() {
1609
+ if (state.timer) clearInterval(state.timer);
1610
+ state.timer = setInterval(next, state.interval);
1611
+ }
1612
+
1613
+ function stopAutoPlay() {
1614
+ if (state.timer) {
1615
+ clearInterval(state.timer);
1616
+ state.timer = null;
1617
+ }
1618
+ }
1619
+
1620
+ // Event listeners
1621
+ if (prevBtn) prevBtn.addEventListener('click', prev);
1622
+ if (nextBtn) nextBtn.addEventListener('click', next);
1623
+
1624
+ indicators.forEach(function(ind, i) {
1625
+ ind.addEventListener('click', function() { goTo(i); });
1626
+ });
1627
+
1628
+ // Pause on hover
1629
+ carousel.addEventListener('mouseenter', stopAutoPlay);
1630
+ carousel.addEventListener('mouseleave', function() {
1631
+ if (state.autoPlay) startAutoPlay();
1632
+ });
1633
+
1634
+ // Initialize
1635
+ goTo(state.current);
1636
+ if (state.autoPlay) startAutoPlay();
1637
+
1638
+ return {
1639
+ goTo: goTo,
1640
+ next: next,
1641
+ prev: prev,
1642
+ startAutoPlay: startAutoPlay,
1643
+ stopAutoPlay: stopAutoPlay,
1644
+ getCurrent: function() { return state.current; }
1645
+ };
1646
+ }
1647
+
1648
+ return { init: init, get: function(id) { return carousels[id]; } };
1649
+ })();
1650
+ </script>
1651
+ """
1652
+
1653
+
1654
+ # ==========================================================================
1655
+ # Stepper JS
1656
+ # ==========================================================================
1657
+
1658
+ STEPPER_JS = """\
1659
+ <script>
1660
+ // Quantum UI Stepper System
1661
+ window.__quantumStepper = (function() {
1662
+ 'use strict';
1663
+
1664
+ var steppers = {};
1665
+
1666
+ function init(id, options) {
1667
+ var stepper = document.getElementById(id);
1668
+ if (!stepper) return;
1669
+
1670
+ options = options || {};
1671
+ var steps = stepper.querySelectorAll('.q-step-item');
1672
+ var contents = stepper.querySelectorAll('.q-step-content');
1673
+
1674
+ var state = {
1675
+ current: options.current || 0,
1676
+ total: steps.length,
1677
+ linear: options.linear !== false,
1678
+ completed: []
1679
+ };
1680
+
1681
+ steppers[id] = state;
1682
+
1683
+ function updateUI() {
1684
+ steps.forEach(function(step, i) {
1685
+ step.classList.remove('active', 'completed');
1686
+ if (i === state.current) {
1687
+ step.classList.add('active');
1688
+ } else if (state.completed.indexOf(i) !== -1) {
1689
+ step.classList.add('completed');
1690
+ }
1691
+ });
1692
+
1693
+ contents.forEach(function(content, i) {
1694
+ content.classList.toggle('active', i === state.current);
1695
+ });
1696
+ }
1697
+
1698
+ function canGoTo(index) {
1699
+ if (!state.linear) return true;
1700
+ // In linear mode, can only go to completed steps or current+1
1701
+ if (index <= state.current) return true;
1702
+ if (index === state.current + 1 && state.completed.indexOf(state.current) !== -1) return true;
1703
+ return false;
1704
+ }
1705
+
1706
+ function goTo(index) {
1707
+ if (index < 0 || index >= state.total) return false;
1708
+ if (!canGoTo(index)) return false;
1709
+
1710
+ state.current = index;
1711
+ updateUI();
1712
+
1713
+ if (options.onChange) {
1714
+ options.onChange(index);
1715
+ }
1716
+
1717
+ return true;
1718
+ }
1719
+
1720
+ function next() {
1721
+ return goTo(state.current + 1);
1722
+ }
1723
+
1724
+ function prev() {
1725
+ return goTo(state.current - 1);
1726
+ }
1727
+
1728
+ function complete(index) {
1729
+ if (index === undefined) index = state.current;
1730
+ if (state.completed.indexOf(index) === -1) {
1731
+ state.completed.push(index);
1732
+ }
1733
+ updateUI();
1734
+
1735
+ // Check if all completed
1736
+ if (state.completed.length === state.total && options.onComplete) {
1737
+ options.onComplete();
1738
+ }
1739
+ }
1740
+
1741
+ function setError(index, message) {
1742
+ if (index === undefined) index = state.current;
1743
+ var step = steps[index];
1744
+ if (step) {
1745
+ step.classList.add('error');
1746
+ // Remove from completed if present
1747
+ var ci = state.completed.indexOf(index);
1748
+ if (ci !== -1) state.completed.splice(ci, 1);
1749
+ }
1750
+ }
1751
+
1752
+ function clearError(index) {
1753
+ if (index === undefined) index = state.current;
1754
+ var step = steps[index];
1755
+ if (step) {
1756
+ step.classList.remove('error');
1757
+ }
1758
+ }
1759
+
1760
+ // Click handlers for clickable steps
1761
+ if (options.clickable) {
1762
+ steps.forEach(function(step, i) {
1763
+ step.style.cursor = 'pointer';
1764
+ step.addEventListener('click', function() {
1765
+ goTo(i);
1766
+ });
1767
+ });
1768
+ }
1769
+
1770
+ // Initialize
1771
+ updateUI();
1772
+
1773
+ return {
1774
+ goTo: goTo,
1775
+ next: next,
1776
+ prev: prev,
1777
+ complete: complete,
1778
+ setError: setError,
1779
+ clearError: clearError,
1780
+ getCurrent: function() { return state.current; },
1781
+ isCompleted: function(i) { return state.completed.indexOf(i) !== -1; }
1782
+ };
1783
+ }
1784
+
1785
+ return { init: init, get: function(id) { return steppers[id]; } };
1786
+ })();
1787
+ </script>
1788
+ """
1789
+
1790
+
1791
+ # ==========================================================================
1792
+ # Calendar JS
1793
+ # ==========================================================================
1794
+
1795
+ CALENDAR_JS = r"""
1796
+ // String RAW de proposito: era uma tripla NAO-raw, entao o Python
1797
+ // colapsava os \' e o JS saia com `dismiss('' + id + '')` — erro de
1798
+ // sintaxe que derrubava ESTE MODULO INTEIRO no browser. O resultado era
1799
+ // __quantumToast / __quantumCalendar indefinidos: nenhum toast fechava e
1800
+ // nenhum calendario navegava, em nenhuma pagina, desde sempre.
1801
+ <script>
1802
+ // Quantum UI Calendar System
1803
+ window.__quantumCalendar = (function() {
1804
+ 'use strict';
1805
+
1806
+ var calendars = {};
1807
+
1808
+ var monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
1809
+ 'July', 'August', 'September', 'October', 'November', 'December'];
1810
+ var dayNames = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
1811
+
1812
+ function init(id, options) {
1813
+ var container = document.getElementById(id);
1814
+ if (!container) return;
1815
+
1816
+ options = options || {};
1817
+
1818
+ // O `value=` inicial vale para os TRES modos.
1819
+ //
1820
+ // rangeStart, rangeEnd e multiple eram sempre null/[], e `selected`
1821
+ // recebia `parseDate(options.value)` com a string INTEIRA - que em
1822
+ // range/multiple e "2026-01-05,2026-01-10" e vira Invalid Date. O
1823
+ // servidor renderizava a selecao certa e, no load, este init a
1824
+ // apagava: o calendario aparecia por um instante com as datas
1825
+ // marcadas e ficava vazio.
1826
+ var mode = options.mode || 'single';
1827
+ var initial = String(options.value || '').split(',')
1828
+ .map(function (s) { return s.trim(); })
1829
+ .filter(Boolean)
1830
+ .map(parseDate)
1831
+ .filter(Boolean);
1832
+
1833
+ // E a vista abre no mes da primeira data escolhida, nao no mes
1834
+ // atual: com value="2020-03-15" o calendario abria em hoje e a
1835
+ // selecao ficava invisivel ate o usuario navegar ate la.
1836
+ var firstPick = initial.length ? initial[0] : new Date();
1837
+
1838
+ var state = {
1839
+ mode: mode,
1840
+ selected: mode === 'single' ? (initial[0] || null) : null,
1841
+ rangeStart: mode === 'range' ? (initial[0] || null) : null,
1842
+ rangeEnd: mode === 'range' ? (initial[1] || null) : null,
1843
+ multiple: mode === 'multiple' ? initial : [],
1844
+ viewMonth: firstPick.getMonth(),
1845
+ viewYear: firstPick.getFullYear(),
1846
+ minDate: options.minDate ? parseDate(options.minDate) : null,
1847
+ maxDate: options.maxDate ? parseDate(options.maxDate) : null,
1848
+ disabledDates: (options.disabledDates || '').split(',').filter(Boolean).map(parseDate),
1849
+ disabledDays: (options.disabledDays || '').split(',').filter(Boolean).map(Number),
1850
+ firstDayOfWeek: options.firstDayOfWeek || 0,
1851
+ showWeekNumbers: options.showWeekNumbers || false
1852
+ };
1853
+
1854
+ calendars[id] = state;
1855
+
1856
+ function parseDate(str) {
1857
+ if (!str) return null;
1858
+ var s = String(str).trim();
1859
+ // `new Date("2026-01-05")` e lido como MEIA-NOITE UTC, mas
1860
+ // formatDate e todo o resto deste modulo usam os getters
1861
+ // LOCAIS. A oeste de Greenwich isso volta um dia: quem esta em
1862
+ // UTC-3 pedia 05/01 e o calendario marcava 04/01, enquanto o
1863
+ // servidor (date.fromisoformat, sem fuso) marcava 05/01. Os
1864
+ // dois lados discordavam em um dia, o ano inteiro.
1865
+ //
1866
+ // Uma data ISO sem hora e uma data de calendario, nao um
1867
+ // instante: montada com os componentes, no fuso local.
1868
+ var m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(s);
1869
+ if (m) return new Date(+m[1], +m[2] - 1, +m[3]);
1870
+ var d = new Date(s);
1871
+ return isNaN(d.getTime()) ? null : d;
1872
+ }
1873
+
1874
+ function formatDate(d) {
1875
+ if (!d) return '';
1876
+ return d.getFullYear() + '-' +
1877
+ String(d.getMonth() + 1).padStart(2, '0') + '-' +
1878
+ String(d.getDate()).padStart(2, '0');
1879
+ }
1880
+
1881
+ function isSameDay(d1, d2) {
1882
+ if (!d1 || !d2) return false;
1883
+ return d1.getFullYear() === d2.getFullYear() &&
1884
+ d1.getMonth() === d2.getMonth() &&
1885
+ d1.getDate() === d2.getDate();
1886
+ }
1887
+
1888
+ function isDisabled(date) {
1889
+ if (state.minDate && date < state.minDate) return true;
1890
+ if (state.maxDate && date > state.maxDate) return true;
1891
+ if (state.disabledDays.indexOf(date.getDay()) !== -1) return true;
1892
+ return state.disabledDates.some(function(d) { return isSameDay(d, date); });
1893
+ }
1894
+
1895
+ function render() {
1896
+ var html = '';
1897
+
1898
+ // Header
1899
+ html += '<div class="q-calendar-header">';
1900
+ html += '<button class="q-calendar-nav" onclick="__quantumCalendar.prevMonth(\'' + id + '\')">&lt;</button>';
1901
+ html += '<span class="q-calendar-title">' + monthNames[state.viewMonth] + ' ' + state.viewYear + '</span>';
1902
+ html += '<button class="q-calendar-nav" onclick="__quantumCalendar.nextMonth(\'' + id + '\')">&gt;</button>';
1903
+ html += '</div>';
1904
+
1905
+ // Grid
1906
+ html += '<div class="q-calendar-grid">';
1907
+
1908
+ // Weekday headers
1909
+ var orderedDays = dayNames.slice(state.firstDayOfWeek).concat(dayNames.slice(0, state.firstDayOfWeek));
1910
+ if (state.showWeekNumbers) html += '<div class="q-calendar-weekday"></div>';
1911
+ orderedDays.forEach(function(d) {
1912
+ html += '<div class="q-calendar-weekday">' + d + '</div>';
1913
+ });
1914
+
1915
+ // Days
1916
+ var firstDay = new Date(state.viewYear, state.viewMonth, 1);
1917
+ var lastDay = new Date(state.viewYear, state.viewMonth + 1, 0);
1918
+ var startDay = (firstDay.getDay() - state.firstDayOfWeek + 7) % 7;
1919
+
1920
+ // Previous month days
1921
+ var prevMonth = new Date(state.viewYear, state.viewMonth, 0);
1922
+ for (var i = startDay - 1; i >= 0; i--) {
1923
+ var day = prevMonth.getDate() - i;
1924
+ html += '<div class="q-calendar-day other-month">' + day + '</div>';
1925
+ }
1926
+
1927
+ // Current month days
1928
+ var today = new Date();
1929
+ for (var d = 1; d <= lastDay.getDate(); d++) {
1930
+ var date = new Date(state.viewYear, state.viewMonth, d);
1931
+ var classes = ['q-calendar-day'];
1932
+
1933
+ if (isSameDay(date, today)) classes.push('today');
1934
+ if (isDisabled(date)) classes.push('disabled');
1935
+
1936
+ if (state.mode === 'single' && isSameDay(date, state.selected)) {
1937
+ classes.push('selected');
1938
+ } else if (state.mode === 'range') {
1939
+ if (isSameDay(date, state.rangeStart)) classes.push('range-start', 'selected');
1940
+ if (isSameDay(date, state.rangeEnd)) classes.push('range-end', 'selected');
1941
+ if (state.rangeStart && state.rangeEnd && date > state.rangeStart && date < state.rangeEnd) {
1942
+ classes.push('in-range');
1943
+ }
1944
+ } else if (state.mode === 'multiple') {
1945
+ if (state.multiple.some(function(s) { return isSameDay(s, date); })) {
1946
+ classes.push('selected');
1947
+ }
1948
+ }
1949
+
1950
+ html += '<div class="' + classes.join(' ') + '" onclick="__quantumCalendar.selectDate(\'' + id + '\', ' + d + ')">' + d + '</div>';
1951
+ }
1952
+
1953
+ // Next month days
1954
+ var remaining = (7 - ((startDay + lastDay.getDate()) % 7)) % 7;
1955
+ for (var n = 1; n <= remaining; n++) {
1956
+ html += '<div class="q-calendar-day other-month">' + n + '</div>';
1957
+ }
1958
+
1959
+ html += '</div>';
1960
+
1961
+ container.innerHTML = html;
1962
+ }
1963
+
1964
+ function selectDate(day) {
1965
+ var date = new Date(state.viewYear, state.viewMonth, day);
1966
+ if (isDisabled(date)) return;
1967
+
1968
+ if (state.mode === 'single') {
1969
+ state.selected = date;
1970
+ } else if (state.mode === 'range') {
1971
+ if (!state.rangeStart || state.rangeEnd) {
1972
+ state.rangeStart = date;
1973
+ state.rangeEnd = null;
1974
+ } else {
1975
+ if (date < state.rangeStart) {
1976
+ state.rangeEnd = state.rangeStart;
1977
+ state.rangeStart = date;
1978
+ } else {
1979
+ state.rangeEnd = date;
1980
+ }
1981
+ }
1982
+ } else if (state.mode === 'multiple') {
1983
+ var idx = state.multiple.findIndex(function(d) { return isSameDay(d, date); });
1984
+ if (idx === -1) {
1985
+ state.multiple.push(date);
1986
+ } else {
1987
+ state.multiple.splice(idx, 1);
1988
+ }
1989
+ }
1990
+
1991
+ render();
1992
+
1993
+ if (options.onChange) {
1994
+ options.onChange(getValue());
1995
+ }
1996
+ }
1997
+
1998
+ function getValue() {
1999
+ if (state.mode === 'single') return state.selected ? formatDate(state.selected) : null;
2000
+ if (state.mode === 'range') return { start: formatDate(state.rangeStart), end: formatDate(state.rangeEnd) };
2001
+ if (state.mode === 'multiple') return state.multiple.map(formatDate);
2002
+ }
2003
+
2004
+ function prevMonth() {
2005
+ state.viewMonth--;
2006
+ if (state.viewMonth < 0) {
2007
+ state.viewMonth = 11;
2008
+ state.viewYear--;
2009
+ }
2010
+ render();
2011
+ }
2012
+
2013
+ function nextMonth() {
2014
+ state.viewMonth++;
2015
+ if (state.viewMonth > 11) {
2016
+ state.viewMonth = 0;
2017
+ state.viewYear++;
2018
+ }
2019
+ render();
2020
+ }
2021
+
2022
+ // Initialize
2023
+ render();
2024
+
2025
+ return {
2026
+ getValue: getValue,
2027
+ selectDate: selectDate,
2028
+ prevMonth: prevMonth,
2029
+ nextMonth: nextMonth,
2030
+ render: render
2031
+ };
2032
+ }
2033
+
2034
+ return {
2035
+ init: init,
2036
+ get: function(id) { return calendars[id]; },
2037
+ selectDate: function(id, day) {
2038
+ var cal = calendars[id];
2039
+ if (cal && cal.selectDate) cal.selectDate(day);
2040
+ },
2041
+ prevMonth: function(id) {
2042
+ var cal = calendars[id];
2043
+ if (cal && cal.prevMonth) cal.prevMonth();
2044
+ },
2045
+ nextMonth: function(id) {
2046
+ var cal = calendars[id];
2047
+ if (cal && cal.nextMonth) cal.nextMonth();
2048
+ }
2049
+ };
2050
+ })();
2051
+ </script>
2052
+ """
2053
+
2054
+
2055
+ RESPONSIVE_CSS = """\n/* UI-2: responsive layout (sm < 640px, md < 768px, lg < 1024px) */
2056
+ .q-grow { flex: 1 1 0; min-width: 0; }
2057
+ .q-grow > input, .q-grow > .q-input { width: 100%; }
2058
+ .q-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
2059
+ .q-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
2060
+ .q-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
2061
+ .q-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
2062
+ .q-cols-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
2063
+ .q-cols-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
2064
+ @media (max-width: 639.98px) {
2065
+ .q-stack-sm { flex-direction: column !important; align-items: stretch !important; }
2066
+ .q-stack-sm > * { width: auto !important; max-width: 100%; }
2067
+ .q-hide-sm { display: none !important; }
2068
+ }
2069
+ @media (min-width: 640px) {
2070
+ .q-hide-above-sm { display: none !important; }
2071
+ .q-sm-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
2072
+ .q-sm-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
2073
+ .q-sm-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
2074
+ .q-sm-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
2075
+ .q-sm-cols-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
2076
+ .q-sm-cols-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
2077
+ }
2078
+ @media (max-width: 767.98px) {
2079
+ .q-stack-md { flex-direction: column !important; align-items: stretch !important; }
2080
+ .q-stack-md > * { width: auto !important; max-width: 100%; }
2081
+ .q-hide-md { display: none !important; }
2082
+ }
2083
+ @media (min-width: 768px) {
2084
+ .q-hide-above-md { display: none !important; }
2085
+ .q-md-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
2086
+ .q-md-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
2087
+ .q-md-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
2088
+ .q-md-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
2089
+ .q-md-cols-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
2090
+ .q-md-cols-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
2091
+ }
2092
+ @media (max-width: 1023.98px) {
2093
+ .q-stack-lg { flex-direction: column !important; align-items: stretch !important; }
2094
+ .q-stack-lg > * { width: auto !important; max-width: 100%; }
2095
+ .q-hide-lg { display: none !important; }
2096
+ }
2097
+ @media (min-width: 1024px) {
2098
+ .q-hide-above-lg { display: none !important; }
2099
+ .q-lg-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
2100
+ .q-lg-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
2101
+ .q-lg-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
2102
+ .q-lg-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
2103
+ .q-lg-cols-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
2104
+ .q-lg-cols-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
2105
+ }
2106
+ """