quantum-framework 0.9.0__py3-none-any.whl

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