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.
- quantum/__init__.py +1 -0
- quantum/cli/__init__.py +2 -0
- quantum/cli/commands/__init__.py +15 -0
- quantum/cli/commands/build.py +417 -0
- quantum/cli/commands/dev.py +185 -0
- quantum/cli/commands/docs.py +329 -0
- quantum/cli/commands/lint.py +523 -0
- quantum/cli/commands/migrate.py +527 -0
- quantum/cli/commands/new.py +622 -0
- quantum/cli/commands/serve.py +190 -0
- quantum/cli/commands/test.py +193 -0
- quantum/cli/deploy.py +810 -0
- quantum/cli/hot_reload.py +951 -0
- quantum/cli/jobs.py +356 -0
- quantum/cli/mq.py +582 -0
- quantum/cli/pkg.py +390 -0
- quantum/cli/runner.py +547 -0
- quantum/cli/server_process.py +159 -0
- quantum/cli/utils.py +334 -0
- quantum/compiler/__init__.py +30 -0
- quantum/compiler/base_generator.py +367 -0
- quantum/compiler/cli.py +295 -0
- quantum/compiler/expression_transformer.py +444 -0
- quantum/compiler/javascript/__init__.py +10 -0
- quantum/compiler/javascript/generator.py +659 -0
- quantum/compiler/optimizer.py +270 -0
- quantum/compiler/python/__init__.py +10 -0
- quantum/compiler/python/generator.py +883 -0
- quantum/compiler/python/runtime.py +863 -0
- quantum/compiler/transpiler.py +330 -0
- quantum/core/__init__.py +3 -0
- quantum/core/ast_nodes.py +2611 -0
- quantum/core/expression_diagnostics.py +87 -0
- quantum/core/expression_stdlib.py +148 -0
- quantum/core/expressions.py +591 -0
- quantum/core/features/agents/src/__init__.py +30 -0
- quantum/core/features/agents/src/ast_node.py +540 -0
- quantum/core/features/conditionals/src/__init__.py +8 -0
- quantum/core/features/conditionals/src/ast_node.py +68 -0
- quantum/core/features/data_fetching/src/__init__.py +21 -0
- quantum/core/features/data_fetching/src/ast_node.py +312 -0
- quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
- quantum/core/features/data_fetching/src/html_adapter.py +474 -0
- quantum/core/features/data_fetching/src/parser.py +225 -0
- quantum/core/features/data_import/src/__init__.py +0 -0
- quantum/core/features/data_import/src/ast_node.py +291 -0
- quantum/core/features/data_import/src/runtime.py +538 -0
- quantum/core/features/dump/src/__init__.py +12 -0
- quantum/core/features/dump/src/ast_node.py +106 -0
- quantum/core/features/dump/src/parser.py +61 -0
- quantum/core/features/dump/src/runtime.py +246 -0
- quantum/core/features/functions/src/__init__.py +8 -0
- quantum/core/features/functions/src/ast_node.py +149 -0
- quantum/core/features/game_engine_2d/src/__init__.py +19 -0
- quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
- quantum/core/features/game_engine_2d/src/parser.py +983 -0
- quantum/core/features/invocation/src/__init__.py +0 -0
- quantum/core/features/invocation/src/ast_node.py +146 -0
- quantum/core/features/invocation/src/runtime.py +327 -0
- quantum/core/features/knowledge_base/src/__init__.py +6 -0
- quantum/core/features/knowledge_base/src/ast_node.py +113 -0
- quantum/core/features/knowledge_base/src/parser.py +82 -0
- quantum/core/features/logging/src/__init__.py +12 -0
- quantum/core/features/logging/src/ast_node.py +111 -0
- quantum/core/features/logging/src/parser.py +50 -0
- quantum/core/features/logging/src/runtime.py +190 -0
- quantum/core/features/loops/src/__init__.py +8 -0
- quantum/core/features/loops/src/ast_node.py +60 -0
- quantum/core/features/query/src/__init__.py +0 -0
- quantum/core/features/query/src/database_service.py +322 -0
- quantum/core/features/query/src/query_validators.py +20 -0
- quantum/core/features/state_management/src/__init__.py +11 -0
- quantum/core/features/state_management/src/ast_node.py +228 -0
- quantum/core/features/terminal_engine/src/__init__.py +21 -0
- quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
- quantum/core/features/terminal_engine/src/parser.py +361 -0
- quantum/core/features/testing_engine/src/__init__.py +41 -0
- quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
- quantum/core/features/testing_engine/src/parser.py +604 -0
- quantum/core/features/theming/src/__init__.py +48 -0
- quantum/core/features/theming/src/ast_node.py +137 -0
- quantum/core/features/theming/src/presets.py +405 -0
- quantum/core/features/ui_engine/src/__init__.py +20 -0
- quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
- quantum/core/features/ui_engine/src/parser.py +1106 -0
- quantum/core/features/websocket/src/__init__.py +24 -0
- quantum/core/features/websocket/src/ast_node.py +247 -0
- quantum/core/html_compat.py +299 -0
- quantum/core/parser.py +1235 -0
- quantum/core/parser_registry.py +213 -0
- quantum/core/parsers/__init__.py +76 -0
- quantum/core/parsers/ai/__init__.py +12 -0
- quantum/core/parsers/ai/agent_parser.py +106 -0
- quantum/core/parsers/ai/knowledge_parser.py +86 -0
- quantum/core/parsers/ai/llm_parser.py +78 -0
- quantum/core/parsers/ai/team_parser.py +88 -0
- quantum/core/parsers/base.py +322 -0
- quantum/core/parsers/composition/__init__.py +10 -0
- quantum/core/parsers/composition/import_parser.py +50 -0
- quantum/core/parsers/composition/slot_parser.py +48 -0
- quantum/core/parsers/control_flow/__init__.py +11 -0
- quantum/core/parsers/control_flow/if_parser.py +68 -0
- quantum/core/parsers/control_flow/loop_parser.py +105 -0
- quantum/core/parsers/control_flow/set_parser.py +89 -0
- quantum/core/parsers/data/__init__.py +12 -0
- quantum/core/parsers/data/data_parser.py +217 -0
- quantum/core/parsers/data/invoke_parser.py +116 -0
- quantum/core/parsers/data/query_parser.py +188 -0
- quantum/core/parsers/data/transaction_parser.py +77 -0
- quantum/core/parsers/events/__init__.py +9 -0
- quantum/core/parsers/events/dispatch_event_parser.py +44 -0
- quantum/core/parsers/forms/__init__.py +11 -0
- quantum/core/parsers/forms/action_parser.py +54 -0
- quantum/core/parsers/forms/flash_parser.py +34 -0
- quantum/core/parsers/forms/redirect_parser.py +33 -0
- quantum/core/parsers/functions/__init__.py +11 -0
- quantum/core/parsers/functions/function_parser.py +137 -0
- quantum/core/parsers/functions/param_parser.py +66 -0
- quantum/core/parsers/functions/return_parser.py +31 -0
- quantum/core/parsers/html/__init__.py +10 -0
- quantum/core/parsers/html/component_call_parser.py +130 -0
- quantum/core/parsers/html/html_parser.py +115 -0
- quantum/core/parsers/jobs/__init__.py +11 -0
- quantum/core/parsers/jobs/job_parser.py +71 -0
- quantum/core/parsers/jobs/schedule_parser.py +62 -0
- quantum/core/parsers/jobs/thread_parser.py +57 -0
- quantum/core/parsers/messaging/__init__.py +17 -0
- quantum/core/parsers/messaging/message_ack_parser.py +30 -0
- quantum/core/parsers/messaging/message_nack_parser.py +30 -0
- quantum/core/parsers/messaging/message_parser.py +114 -0
- quantum/core/parsers/messaging/queue_parser.py +61 -0
- quantum/core/parsers/messaging/websocket_parser.py +121 -0
- quantum/core/parsers/persistence/__init__.py +9 -0
- quantum/core/parsers/persistence/persist_parser.py +64 -0
- quantum/core/parsers/routing/__init__.py +9 -0
- quantum/core/parsers/routing/route_parser.py +41 -0
- quantum/core/parsers/scripting/__init__.py +12 -0
- quantum/core/parsers/scripting/pyclass_parser.py +62 -0
- quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
- quantum/core/parsers/scripting/pyimport_parser.py +52 -0
- quantum/core/parsers/scripting/python_parser.py +49 -0
- quantum/core/parsers/services/__init__.py +12 -0
- quantum/core/parsers/services/dump_parser.py +52 -0
- quantum/core/parsers/services/file_parser.py +48 -0
- quantum/core/parsers/services/log_parser.py +46 -0
- quantum/core/parsers/services/mail_parser.py +65 -0
- quantum/core/tiers.py +82 -0
- quantum/packages/__init__.py +28 -0
- quantum/packages/manager.py +413 -0
- quantum/packages/manifest.py +351 -0
- quantum/packages/registry.py +399 -0
- quantum/packages/resolver.py +336 -0
- quantum/plugins/__init__.py +33 -0
- quantum/plugins/hooks.py +329 -0
- quantum/plugins/loader.py +479 -0
- quantum/plugins/manifest.py +336 -0
- quantum/plugins/registry.py +371 -0
- quantum/runtime/__init__.py +28 -0
- quantum/runtime/action_handler.py +443 -0
- quantum/runtime/adapters/__init__.py +88 -0
- quantum/runtime/adapters/memory_adapter.py +690 -0
- quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
- quantum/runtime/adapters/redis_adapter.py +582 -0
- quantum/runtime/adapters/sqlite_adapter.py +414 -0
- quantum/runtime/agent_service.py +1133 -0
- quantum/runtime/api_server.py +86 -0
- quantum/runtime/ast_cache.py +506 -0
- quantum/runtime/auth_service.py +267 -0
- quantum/runtime/component.py +990 -0
- quantum/runtime/component_composer.py +319 -0
- quantum/runtime/component_resolver.py +174 -0
- quantum/runtime/database_service.py +598 -0
- quantum/runtime/email_service.py +162 -0
- quantum/runtime/error_handler.py +295 -0
- quantum/runtime/execution_context.py +286 -0
- quantum/runtime/executor_registry.py +171 -0
- quantum/runtime/executors/__init__.py +71 -0
- quantum/runtime/executors/ai/__init__.py +12 -0
- quantum/runtime/executors/ai/agent_executor.py +217 -0
- quantum/runtime/executors/ai/knowledge_executor.py +114 -0
- quantum/runtime/executors/ai/llm_executor.py +153 -0
- quantum/runtime/executors/ai/team_executor.py +171 -0
- quantum/runtime/executors/base.py +262 -0
- quantum/runtime/executors/control_flow/__init__.py +11 -0
- quantum/runtime/executors/control_flow/if_executor.py +93 -0
- quantum/runtime/executors/control_flow/loop_executor.py +307 -0
- quantum/runtime/executors/control_flow/set_executor.py +412 -0
- quantum/runtime/executors/data/__init__.py +12 -0
- quantum/runtime/executors/data/data_executor.py +145 -0
- quantum/runtime/executors/data/invoke_executor.py +176 -0
- quantum/runtime/executors/data/query_executor.py +256 -0
- quantum/runtime/executors/data/transaction_executor.py +91 -0
- quantum/runtime/executors/jobs/__init__.py +11 -0
- quantum/runtime/executors/jobs/job_executor.py +190 -0
- quantum/runtime/executors/jobs/schedule_executor.py +132 -0
- quantum/runtime/executors/jobs/thread_executor.py +127 -0
- quantum/runtime/executors/messaging/__init__.py +17 -0
- quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
- quantum/runtime/executors/messaging/message_executor.py +174 -0
- quantum/runtime/executors/messaging/queue_executor.py +103 -0
- quantum/runtime/executors/messaging/websocket_executor.py +197 -0
- quantum/runtime/executors/scripting/__init__.py +11 -0
- quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
- quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
- quantum/runtime/executors/scripting/python_executor.py +249 -0
- quantum/runtime/executors/services/__init__.py +12 -0
- quantum/runtime/executors/services/dump_executor.py +72 -0
- quantum/runtime/executors/services/file_executor.py +89 -0
- quantum/runtime/executors/services/log_executor.py +77 -0
- quantum/runtime/executors/services/mail_executor.py +81 -0
- quantum/runtime/expression_cache.py +498 -0
- quantum/runtime/file_upload_service.py +326 -0
- quantum/runtime/function_registry.py +118 -0
- quantum/runtime/game_builder.py +166 -0
- quantum/runtime/game_code_generator.py +2371 -0
- quantum/runtime/game_templates.py +2006 -0
- quantum/runtime/godot_code_generator.py +4681 -0
- quantum/runtime/godot_templates.py +1449 -0
- quantum/runtime/job_executor.py +1599 -0
- quantum/runtime/knowledge_service.py +500 -0
- quantum/runtime/llm_cache.py +100 -0
- quantum/runtime/llm_providers.py +704 -0
- quantum/runtime/llm_service.py +287 -0
- quantum/runtime/logging_setup.py +140 -0
- quantum/runtime/message_broker.py +364 -0
- quantum/runtime/message_queue_service.py +571 -0
- quantum/runtime/param_validation.py +184 -0
- quantum/runtime/pypy_compat.py +315 -0
- quantum/runtime/python_bridge.py +698 -0
- quantum/runtime/query_validators.py +304 -0
- quantum/runtime/renderer.py +733 -0
- quantum/runtime/service_container.py +444 -0
- quantum/runtime/terminal_builder.py +76 -0
- quantum/runtime/terminal_code_generator.py +607 -0
- quantum/runtime/terminal_templates.py +243 -0
- quantum/runtime/testing_builder.py +77 -0
- quantum/runtime/testing_code_generator.py +833 -0
- quantum/runtime/testing_templates.py +85 -0
- quantum/runtime/ui_builder.py +188 -0
- quantum/runtime/ui_desktop_adapter.py +1730 -0
- quantum/runtime/ui_desktop_templates.py +307 -0
- quantum/runtime/ui_html_adapter.py +2691 -0
- quantum/runtime/ui_html_templates.py +2297 -0
- quantum/runtime/ui_mobile_adapter.py +1832 -0
- quantum/runtime/ui_mobile_templates.py +1003 -0
- quantum/runtime/ui_textual_adapter.py +1866 -0
- quantum/runtime/ui_textual_templates.py +45 -0
- quantum/runtime/ui_tokens.py +465 -0
- quantum/runtime/ui_validator.py +365 -0
- quantum/runtime/validators.py +256 -0
- quantum/runtime/web_server.py +1766 -0
- quantum/runtime/websocket_adapter.py +501 -0
- quantum/runtime/websocket_service.py +585 -0
- quantum/runtime/websocket_transport.py +289 -0
- quantum/runtime/wsgi.py +101 -0
- quantum/utils/__init__.py +1 -0
- quantum_framework-0.9.0.dist-info/METADATA +244 -0
- quantum_framework-0.9.0.dist-info/RECORD +262 -0
- quantum_framework-0.9.0.dist-info/WHEEL +5 -0
- quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
- quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
- quantum_framework-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1854 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AST Nodes for UI Engine (ui: namespace)
|
|
3
|
+
|
|
4
|
+
All UI-specific AST nodes for the Quantum multi-target UI engine.
|
|
5
|
+
These represent containers, widgets, and layout primitives that
|
|
6
|
+
compile to HTML/CSS, Python Textual (TUI), or Desktop (pywebview).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Dict, Any, List, Optional
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from quantum.core.ast_nodes import QuantumNode
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ============================================
|
|
16
|
+
# LAYOUT MIXIN
|
|
17
|
+
# ============================================
|
|
18
|
+
|
|
19
|
+
class UILayoutMixin:
|
|
20
|
+
"""Shared layout attributes for all UI containers and some widgets."""
|
|
21
|
+
|
|
22
|
+
def _init_layout_attrs(self):
|
|
23
|
+
self.gap: Optional[str] = None
|
|
24
|
+
self.padding: Optional[str] = None
|
|
25
|
+
self.margin: Optional[str] = None
|
|
26
|
+
self.align: Optional[str] = None # start, center, end, stretch
|
|
27
|
+
self.justify: Optional[str] = None # start, center, end, between, around
|
|
28
|
+
self.width: Optional[str] = None # auto, fill, number, percentage
|
|
29
|
+
self.height: Optional[str] = None
|
|
30
|
+
self.background: Optional[str] = None
|
|
31
|
+
self.color: Optional[str] = None
|
|
32
|
+
self.border: Optional[str] = None
|
|
33
|
+
self.ui_id: Optional[str] = None
|
|
34
|
+
self.ui_class: Optional[str] = None
|
|
35
|
+
self.visible: Optional[str] = None
|
|
36
|
+
|
|
37
|
+
def _layout_to_dict(self) -> Dict[str, Any]:
|
|
38
|
+
"""Return layout attributes as dict (for to_dict)."""
|
|
39
|
+
d = {}
|
|
40
|
+
for attr in ('gap', 'padding', 'margin', 'align', 'justify',
|
|
41
|
+
'width', 'height', 'background', 'color', 'border',
|
|
42
|
+
'ui_id', 'ui_class', 'visible'):
|
|
43
|
+
val = getattr(self, attr, None)
|
|
44
|
+
if val is not None:
|
|
45
|
+
d[attr] = val
|
|
46
|
+
return d
|
|
47
|
+
|
|
48
|
+
def _validate_layout(self) -> List[str]:
|
|
49
|
+
"""Validate layout attributes."""
|
|
50
|
+
errors = []
|
|
51
|
+
if self.align and self.align not in ('start', 'center', 'end', 'stretch'):
|
|
52
|
+
errors.append(f"Invalid align value: {self.align}")
|
|
53
|
+
if self.justify and self.justify not in ('start', 'center', 'end', 'between', 'around'):
|
|
54
|
+
errors.append(f"Invalid justify value: {self.justify}")
|
|
55
|
+
return errors
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ============================================
|
|
59
|
+
# CONTAINERS
|
|
60
|
+
# ============================================
|
|
61
|
+
|
|
62
|
+
class UIWindowNode(QuantumNode, UILayoutMixin):
|
|
63
|
+
"""Represents <ui:window> - Top-level window container."""
|
|
64
|
+
|
|
65
|
+
def __init__(self, title: Optional[str] = None):
|
|
66
|
+
self.title = title
|
|
67
|
+
self.children: List[QuantumNode] = []
|
|
68
|
+
self._init_layout_attrs()
|
|
69
|
+
|
|
70
|
+
def add_child(self, child: QuantumNode):
|
|
71
|
+
self.children.append(child)
|
|
72
|
+
|
|
73
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
74
|
+
d = {"type": "ui_window", "title": self.title,
|
|
75
|
+
"children_count": len(self.children)}
|
|
76
|
+
d.update(self._layout_to_dict())
|
|
77
|
+
return d
|
|
78
|
+
|
|
79
|
+
def validate(self) -> List[str]:
|
|
80
|
+
errors = self._validate_layout()
|
|
81
|
+
for child in self.children:
|
|
82
|
+
if hasattr(child, 'validate'):
|
|
83
|
+
errors.extend(child.validate())
|
|
84
|
+
return errors
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class UIHBoxNode(QuantumNode, UILayoutMixin):
|
|
88
|
+
"""Represents <ui:hbox> - Horizontal flex container."""
|
|
89
|
+
|
|
90
|
+
def __init__(self):
|
|
91
|
+
self.children: List[QuantumNode] = []
|
|
92
|
+
self._init_layout_attrs()
|
|
93
|
+
|
|
94
|
+
def add_child(self, child: QuantumNode):
|
|
95
|
+
self.children.append(child)
|
|
96
|
+
|
|
97
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
98
|
+
d = {"type": "ui_hbox", "children_count": len(self.children)}
|
|
99
|
+
d.update(self._layout_to_dict())
|
|
100
|
+
return d
|
|
101
|
+
|
|
102
|
+
def validate(self) -> List[str]:
|
|
103
|
+
errors = self._validate_layout()
|
|
104
|
+
for child in self.children:
|
|
105
|
+
if hasattr(child, 'validate'):
|
|
106
|
+
errors.extend(child.validate())
|
|
107
|
+
return errors
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class UIVBoxNode(QuantumNode, UILayoutMixin):
|
|
111
|
+
"""Represents <ui:vbox> - Vertical flex container."""
|
|
112
|
+
|
|
113
|
+
def __init__(self):
|
|
114
|
+
self.children: List[QuantumNode] = []
|
|
115
|
+
self._init_layout_attrs()
|
|
116
|
+
|
|
117
|
+
def add_child(self, child: QuantumNode):
|
|
118
|
+
self.children.append(child)
|
|
119
|
+
|
|
120
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
121
|
+
d = {"type": "ui_vbox", "children_count": len(self.children)}
|
|
122
|
+
d.update(self._layout_to_dict())
|
|
123
|
+
return d
|
|
124
|
+
|
|
125
|
+
def validate(self) -> List[str]:
|
|
126
|
+
errors = self._validate_layout()
|
|
127
|
+
for child in self.children:
|
|
128
|
+
if hasattr(child, 'validate'):
|
|
129
|
+
errors.extend(child.validate())
|
|
130
|
+
return errors
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class UIPanelNode(QuantumNode, UILayoutMixin):
|
|
134
|
+
"""Represents <ui:panel> - Bordered container with optional title."""
|
|
135
|
+
|
|
136
|
+
def __init__(self):
|
|
137
|
+
self.title: Optional[str] = None
|
|
138
|
+
self.collapsible: bool = False
|
|
139
|
+
self.children: List[QuantumNode] = []
|
|
140
|
+
self._init_layout_attrs()
|
|
141
|
+
|
|
142
|
+
def add_child(self, child: QuantumNode):
|
|
143
|
+
self.children.append(child)
|
|
144
|
+
|
|
145
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
146
|
+
d = {"type": "ui_panel", "title": self.title,
|
|
147
|
+
"collapsible": self.collapsible, "children_count": len(self.children)}
|
|
148
|
+
d.update(self._layout_to_dict())
|
|
149
|
+
return d
|
|
150
|
+
|
|
151
|
+
def validate(self) -> List[str]:
|
|
152
|
+
errors = self._validate_layout()
|
|
153
|
+
for child in self.children:
|
|
154
|
+
if hasattr(child, 'validate'):
|
|
155
|
+
errors.extend(child.validate())
|
|
156
|
+
return errors
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class UITabPanelNode(QuantumNode, UILayoutMixin):
|
|
160
|
+
"""Represents <ui:tabpanel> - Tabbed content container."""
|
|
161
|
+
|
|
162
|
+
def __init__(self):
|
|
163
|
+
self.children: List[QuantumNode] = [] # Should contain UITabNode
|
|
164
|
+
self._init_layout_attrs()
|
|
165
|
+
|
|
166
|
+
def add_child(self, child: QuantumNode):
|
|
167
|
+
self.children.append(child)
|
|
168
|
+
|
|
169
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
170
|
+
d = {"type": "ui_tabpanel", "children_count": len(self.children)}
|
|
171
|
+
d.update(self._layout_to_dict())
|
|
172
|
+
return d
|
|
173
|
+
|
|
174
|
+
def validate(self) -> List[str]:
|
|
175
|
+
errors = self._validate_layout()
|
|
176
|
+
for child in self.children:
|
|
177
|
+
if hasattr(child, 'validate'):
|
|
178
|
+
errors.extend(child.validate())
|
|
179
|
+
return errors
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class UITabNode(QuantumNode, UILayoutMixin):
|
|
183
|
+
"""Represents <ui:tab> - Individual tab inside a tabpanel."""
|
|
184
|
+
|
|
185
|
+
def __init__(self):
|
|
186
|
+
self.title: str = ""
|
|
187
|
+
self.children: List[QuantumNode] = []
|
|
188
|
+
self._init_layout_attrs()
|
|
189
|
+
|
|
190
|
+
def add_child(self, child: QuantumNode):
|
|
191
|
+
self.children.append(child)
|
|
192
|
+
|
|
193
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
194
|
+
d = {"type": "ui_tab", "title": self.title,
|
|
195
|
+
"children_count": len(self.children)}
|
|
196
|
+
d.update(self._layout_to_dict())
|
|
197
|
+
return d
|
|
198
|
+
|
|
199
|
+
def validate(self) -> List[str]:
|
|
200
|
+
errors = self._validate_layout()
|
|
201
|
+
if not self.title:
|
|
202
|
+
errors.append("Tab title is required")
|
|
203
|
+
for child in self.children:
|
|
204
|
+
if hasattr(child, 'validate'):
|
|
205
|
+
errors.extend(child.validate())
|
|
206
|
+
return errors
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class UIGridNode(QuantumNode, UILayoutMixin):
|
|
210
|
+
"""Represents <ui:grid> - CSS grid container."""
|
|
211
|
+
|
|
212
|
+
def __init__(self):
|
|
213
|
+
self.columns: Optional[str] = None # e.g. "3", "1fr 2fr 1fr"
|
|
214
|
+
self.children: List[QuantumNode] = []
|
|
215
|
+
self._init_layout_attrs()
|
|
216
|
+
|
|
217
|
+
def add_child(self, child: QuantumNode):
|
|
218
|
+
self.children.append(child)
|
|
219
|
+
|
|
220
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
221
|
+
d = {"type": "ui_grid", "columns": self.columns,
|
|
222
|
+
"children_count": len(self.children)}
|
|
223
|
+
d.update(self._layout_to_dict())
|
|
224
|
+
return d
|
|
225
|
+
|
|
226
|
+
def validate(self) -> List[str]:
|
|
227
|
+
errors = self._validate_layout()
|
|
228
|
+
for child in self.children:
|
|
229
|
+
if hasattr(child, 'validate'):
|
|
230
|
+
errors.extend(child.validate())
|
|
231
|
+
return errors
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class UIAccordionNode(QuantumNode, UILayoutMixin):
|
|
235
|
+
"""Represents <ui:accordion> - Collapsible sections container."""
|
|
236
|
+
|
|
237
|
+
def __init__(self):
|
|
238
|
+
self.children: List[QuantumNode] = [] # Should contain UISectionNode
|
|
239
|
+
self._init_layout_attrs()
|
|
240
|
+
|
|
241
|
+
def add_child(self, child: QuantumNode):
|
|
242
|
+
self.children.append(child)
|
|
243
|
+
|
|
244
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
245
|
+
d = {"type": "ui_accordion", "children_count": len(self.children)}
|
|
246
|
+
d.update(self._layout_to_dict())
|
|
247
|
+
return d
|
|
248
|
+
|
|
249
|
+
def validate(self) -> List[str]:
|
|
250
|
+
errors = self._validate_layout()
|
|
251
|
+
for child in self.children:
|
|
252
|
+
if hasattr(child, 'validate'):
|
|
253
|
+
errors.extend(child.validate())
|
|
254
|
+
return errors
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class UISectionNode(QuantumNode, UILayoutMixin):
|
|
258
|
+
"""Represents <ui:section> - Collapsible section (inside accordion)."""
|
|
259
|
+
|
|
260
|
+
def __init__(self):
|
|
261
|
+
self.title: str = ""
|
|
262
|
+
self.expanded: bool = False
|
|
263
|
+
self.children: List[QuantumNode] = []
|
|
264
|
+
self._init_layout_attrs()
|
|
265
|
+
|
|
266
|
+
def add_child(self, child: QuantumNode):
|
|
267
|
+
self.children.append(child)
|
|
268
|
+
|
|
269
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
270
|
+
d = {"type": "ui_section", "title": self.title,
|
|
271
|
+
"expanded": self.expanded, "children_count": len(self.children)}
|
|
272
|
+
d.update(self._layout_to_dict())
|
|
273
|
+
return d
|
|
274
|
+
|
|
275
|
+
def validate(self) -> List[str]:
|
|
276
|
+
errors = self._validate_layout()
|
|
277
|
+
if not self.title:
|
|
278
|
+
errors.append("Section title is required")
|
|
279
|
+
for child in self.children:
|
|
280
|
+
if hasattr(child, 'validate'):
|
|
281
|
+
errors.extend(child.validate())
|
|
282
|
+
return errors
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class UIDividedBoxNode(QuantumNode, UILayoutMixin):
|
|
286
|
+
"""Represents <ui:dividedbox> - Resizable split container."""
|
|
287
|
+
|
|
288
|
+
def __init__(self):
|
|
289
|
+
self.direction: str = "horizontal" # horizontal, vertical
|
|
290
|
+
self.children: List[QuantumNode] = []
|
|
291
|
+
self._init_layout_attrs()
|
|
292
|
+
|
|
293
|
+
def add_child(self, child: QuantumNode):
|
|
294
|
+
self.children.append(child)
|
|
295
|
+
|
|
296
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
297
|
+
d = {"type": "ui_dividedbox", "direction": self.direction,
|
|
298
|
+
"children_count": len(self.children)}
|
|
299
|
+
d.update(self._layout_to_dict())
|
|
300
|
+
return d
|
|
301
|
+
|
|
302
|
+
def validate(self) -> List[str]:
|
|
303
|
+
errors = self._validate_layout()
|
|
304
|
+
if self.direction not in ('horizontal', 'vertical'):
|
|
305
|
+
errors.append(f"Invalid dividedbox direction: {self.direction}")
|
|
306
|
+
for child in self.children:
|
|
307
|
+
if hasattr(child, 'validate'):
|
|
308
|
+
errors.extend(child.validate())
|
|
309
|
+
return errors
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class UIFormNode(QuantumNode, UILayoutMixin):
|
|
313
|
+
"""Represents <ui:form> - Form container with validation support."""
|
|
314
|
+
|
|
315
|
+
def __init__(self):
|
|
316
|
+
self.on_submit: Optional[str] = None
|
|
317
|
+
self.children: List[QuantumNode] = []
|
|
318
|
+
|
|
319
|
+
# Validation mode: 'client', 'server', or 'both' (default)
|
|
320
|
+
self.validation_mode: str = "both"
|
|
321
|
+
|
|
322
|
+
# Show validation feedback inline (default) or in summary
|
|
323
|
+
self.error_display: str = "inline" # 'inline', 'summary', 'both'
|
|
324
|
+
|
|
325
|
+
# Disable HTML5 native validation (use custom JS instead)
|
|
326
|
+
self.novalidate: bool = False
|
|
327
|
+
|
|
328
|
+
# Custom validators defined at form level
|
|
329
|
+
self.validators: List['UIValidatorNode'] = []
|
|
330
|
+
|
|
331
|
+
self._init_layout_attrs()
|
|
332
|
+
|
|
333
|
+
def add_child(self, child: QuantumNode):
|
|
334
|
+
self.children.append(child)
|
|
335
|
+
|
|
336
|
+
def add_validator(self, validator: 'UIValidatorNode'):
|
|
337
|
+
"""Add a custom validator to the form."""
|
|
338
|
+
self.validators.append(validator)
|
|
339
|
+
|
|
340
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
341
|
+
d = {"type": "ui_form", "on_submit": self.on_submit,
|
|
342
|
+
"validation_mode": self.validation_mode,
|
|
343
|
+
"error_display": self.error_display,
|
|
344
|
+
"novalidate": self.novalidate,
|
|
345
|
+
"validators_count": len(self.validators),
|
|
346
|
+
"children_count": len(self.children)}
|
|
347
|
+
d.update(self._layout_to_dict())
|
|
348
|
+
return d
|
|
349
|
+
|
|
350
|
+
def validate(self) -> List[str]:
|
|
351
|
+
errors = self._validate_layout()
|
|
352
|
+
if self.validation_mode not in ('client', 'server', 'both'):
|
|
353
|
+
errors.append(f"Invalid validation_mode: {self.validation_mode}")
|
|
354
|
+
if self.error_display not in ('inline', 'summary', 'both'):
|
|
355
|
+
errors.append(f"Invalid error_display: {self.error_display}")
|
|
356
|
+
for child in self.children:
|
|
357
|
+
if hasattr(child, 'validate'):
|
|
358
|
+
errors.extend(child.validate())
|
|
359
|
+
for validator in self.validators:
|
|
360
|
+
if hasattr(validator, 'validate'):
|
|
361
|
+
errors.extend(validator.validate())
|
|
362
|
+
return errors
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
class UIValidatorNode(QuantumNode):
|
|
366
|
+
"""
|
|
367
|
+
Represents <ui:validator> - Custom validation rule.
|
|
368
|
+
|
|
369
|
+
Can be defined at form level (reusable) or inline in an input.
|
|
370
|
+
|
|
371
|
+
Examples:
|
|
372
|
+
<ui:validator name="phone" pattern="^\d{10,11}$" message="Invalid phone number" />
|
|
373
|
+
<ui:validator name="passwordMatch" field="password" match="confirm_password"
|
|
374
|
+
message="Passwords must match" />
|
|
375
|
+
<ui:validator name="customEmail" type="email" message="Please enter a valid email" />
|
|
376
|
+
"""
|
|
377
|
+
|
|
378
|
+
def __init__(self):
|
|
379
|
+
# Validator identification
|
|
380
|
+
self.name: Optional[str] = None # Unique name for reference
|
|
381
|
+
|
|
382
|
+
# Validation rule types
|
|
383
|
+
self.rule_type: Optional[str] = None # 'pattern', 'email', 'url', 'phone', 'match', 'custom'
|
|
384
|
+
self.pattern: Optional[str] = None # Regex pattern
|
|
385
|
+
self.match: Optional[str] = None # Field name to match against
|
|
386
|
+
self.min: Optional[str] = None # Min value
|
|
387
|
+
self.max: Optional[str] = None # Max value
|
|
388
|
+
self.minlength: Optional[int] = None # Min length
|
|
389
|
+
self.maxlength: Optional[int] = None # Max length
|
|
390
|
+
|
|
391
|
+
# Custom validation function (for 'custom' type)
|
|
392
|
+
self.expression: Optional[str] = None # Custom JS expression
|
|
393
|
+
self.server_expression: Optional[str] = None # Custom server-side expression
|
|
394
|
+
|
|
395
|
+
# Error handling
|
|
396
|
+
self.message: Optional[str] = None # Error message to display
|
|
397
|
+
self.field: Optional[str] = None # Target field (for form-level validators)
|
|
398
|
+
|
|
399
|
+
# When to trigger validation
|
|
400
|
+
self.trigger: str = "submit" # 'blur', 'change', 'input', 'submit'
|
|
401
|
+
|
|
402
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
403
|
+
return {
|
|
404
|
+
"type": "ui_validator",
|
|
405
|
+
"name": self.name,
|
|
406
|
+
"rule_type": self.rule_type,
|
|
407
|
+
"pattern": self.pattern,
|
|
408
|
+
"match": self.match,
|
|
409
|
+
"min": self.min,
|
|
410
|
+
"max": self.max,
|
|
411
|
+
"minlength": self.minlength,
|
|
412
|
+
"maxlength": self.maxlength,
|
|
413
|
+
"expression": self.expression,
|
|
414
|
+
"message": self.message,
|
|
415
|
+
"field": self.field,
|
|
416
|
+
"trigger": self.trigger,
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
def validate(self) -> List[str]:
|
|
420
|
+
errors = []
|
|
421
|
+
if not self.name:
|
|
422
|
+
errors.append("Validator requires 'name' attribute")
|
|
423
|
+
if self.trigger not in ('blur', 'change', 'input', 'submit'):
|
|
424
|
+
errors.append(f"Invalid validator trigger: {self.trigger}")
|
|
425
|
+
# Must have at least one validation rule
|
|
426
|
+
has_rule = (self.pattern or self.match or self.rule_type or
|
|
427
|
+
self.min or self.max or self.minlength or self.maxlength or
|
|
428
|
+
self.expression)
|
|
429
|
+
if not has_rule:
|
|
430
|
+
errors.append(f"Validator '{self.name}' must define at least one validation rule")
|
|
431
|
+
return errors
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
class UIFormItemNode(QuantumNode, UILayoutMixin):
|
|
435
|
+
"""Represents <ui:formitem> - Form item with label."""
|
|
436
|
+
|
|
437
|
+
def __init__(self):
|
|
438
|
+
self.label: Optional[str] = None
|
|
439
|
+
self.children: List[QuantumNode] = []
|
|
440
|
+
self._init_layout_attrs()
|
|
441
|
+
|
|
442
|
+
def add_child(self, child: QuantumNode):
|
|
443
|
+
self.children.append(child)
|
|
444
|
+
|
|
445
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
446
|
+
d = {"type": "ui_formitem", "label": self.label,
|
|
447
|
+
"children_count": len(self.children)}
|
|
448
|
+
d.update(self._layout_to_dict())
|
|
449
|
+
return d
|
|
450
|
+
|
|
451
|
+
def validate(self) -> List[str]:
|
|
452
|
+
errors = self._validate_layout()
|
|
453
|
+
for child in self.children:
|
|
454
|
+
if hasattr(child, 'validate'):
|
|
455
|
+
errors.extend(child.validate())
|
|
456
|
+
return errors
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
class UISpacerNode(QuantumNode, UILayoutMixin):
|
|
460
|
+
"""Represents <ui:spacer> - Flexible space filler."""
|
|
461
|
+
|
|
462
|
+
def __init__(self):
|
|
463
|
+
self.size: Optional[str] = None
|
|
464
|
+
self._init_layout_attrs()
|
|
465
|
+
|
|
466
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
467
|
+
d = {"type": "ui_spacer", "size": self.size}
|
|
468
|
+
d.update(self._layout_to_dict())
|
|
469
|
+
return d
|
|
470
|
+
|
|
471
|
+
def validate(self) -> List[str]:
|
|
472
|
+
return self._validate_layout()
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
class UIScrollBoxNode(QuantumNode, UILayoutMixin):
|
|
476
|
+
"""Represents <ui:scrollbox> - Scrollable container."""
|
|
477
|
+
|
|
478
|
+
def __init__(self):
|
|
479
|
+
self.children: List[QuantumNode] = []
|
|
480
|
+
self._init_layout_attrs()
|
|
481
|
+
|
|
482
|
+
def add_child(self, child: QuantumNode):
|
|
483
|
+
self.children.append(child)
|
|
484
|
+
|
|
485
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
486
|
+
d = {"type": "ui_scrollbox", "children_count": len(self.children)}
|
|
487
|
+
d.update(self._layout_to_dict())
|
|
488
|
+
return d
|
|
489
|
+
|
|
490
|
+
def validate(self) -> List[str]:
|
|
491
|
+
errors = self._validate_layout()
|
|
492
|
+
for child in self.children:
|
|
493
|
+
if hasattr(child, 'validate'):
|
|
494
|
+
errors.extend(child.validate())
|
|
495
|
+
return errors
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
# ============================================
|
|
499
|
+
# WIDGETS
|
|
500
|
+
# ============================================
|
|
501
|
+
|
|
502
|
+
class UITextNode(QuantumNode, UILayoutMixin):
|
|
503
|
+
"""Represents <ui:text> - Text display widget."""
|
|
504
|
+
|
|
505
|
+
def __init__(self):
|
|
506
|
+
self.content: str = ""
|
|
507
|
+
self.size: Optional[str] = None # xs, sm, md, lg, xl, 2xl
|
|
508
|
+
self.weight: Optional[str] = None # normal, bold, light
|
|
509
|
+
self._init_layout_attrs()
|
|
510
|
+
|
|
511
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
512
|
+
d = {"type": "ui_text", "content": self.content,
|
|
513
|
+
"size": self.size, "weight": self.weight}
|
|
514
|
+
d.update(self._layout_to_dict())
|
|
515
|
+
return d
|
|
516
|
+
|
|
517
|
+
def validate(self) -> List[str]:
|
|
518
|
+
return self._validate_layout()
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
class UIButtonNode(QuantumNode, UILayoutMixin):
|
|
522
|
+
"""Represents <ui:button> - Clickable button widget."""
|
|
523
|
+
|
|
524
|
+
def __init__(self):
|
|
525
|
+
self.content: str = ""
|
|
526
|
+
self.on_click: Optional[str] = None
|
|
527
|
+
self.variant: Optional[str] = None # primary, secondary, danger, success
|
|
528
|
+
self.disabled: bool = False
|
|
529
|
+
self._init_layout_attrs()
|
|
530
|
+
|
|
531
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
532
|
+
d = {"type": "ui_button", "content": self.content,
|
|
533
|
+
"on_click": self.on_click, "variant": self.variant,
|
|
534
|
+
"disabled": self.disabled}
|
|
535
|
+
d.update(self._layout_to_dict())
|
|
536
|
+
return d
|
|
537
|
+
|
|
538
|
+
def validate(self) -> List[str]:
|
|
539
|
+
return self._validate_layout()
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
class UIInputNode(QuantumNode, UILayoutMixin):
|
|
543
|
+
"""Represents <ui:input> - Text input widget with validation support."""
|
|
544
|
+
|
|
545
|
+
def __init__(self):
|
|
546
|
+
self.bind: Optional[str] = None
|
|
547
|
+
self.placeholder: Optional[str] = None
|
|
548
|
+
self.input_type: str = "text" # text, password, email, number
|
|
549
|
+
self.on_change: Optional[str] = None
|
|
550
|
+
self.on_submit: Optional[str] = None
|
|
551
|
+
|
|
552
|
+
# Validation attributes (HTML5 + server-side)
|
|
553
|
+
self.required: bool = False
|
|
554
|
+
self.min: Optional[str] = None # Min value (number/date)
|
|
555
|
+
self.max: Optional[str] = None # Max value (number/date)
|
|
556
|
+
self.minlength: Optional[int] = None # Min string length
|
|
557
|
+
self.maxlength: Optional[int] = None # Max string length
|
|
558
|
+
self.pattern: Optional[str] = None # Regex pattern
|
|
559
|
+
self.error_message: Optional[str] = None # Custom error message
|
|
560
|
+
|
|
561
|
+
# Custom validators (references to ui:validator rules)
|
|
562
|
+
self.validators: List[str] = []
|
|
563
|
+
|
|
564
|
+
self._init_layout_attrs()
|
|
565
|
+
|
|
566
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
567
|
+
d = {"type": "ui_input", "bind": self.bind,
|
|
568
|
+
"placeholder": self.placeholder, "input_type": self.input_type,
|
|
569
|
+
"required": self.required, "min": self.min, "max": self.max,
|
|
570
|
+
"minlength": self.minlength, "maxlength": self.maxlength,
|
|
571
|
+
"pattern": self.pattern, "error_message": self.error_message,
|
|
572
|
+
"validators": self.validators}
|
|
573
|
+
d.update(self._layout_to_dict())
|
|
574
|
+
return d
|
|
575
|
+
|
|
576
|
+
def validate(self) -> List[str]:
|
|
577
|
+
errors = self._validate_layout()
|
|
578
|
+
# Validate that minlength <= maxlength if both specified
|
|
579
|
+
if self.minlength is not None and self.maxlength is not None:
|
|
580
|
+
if self.minlength > self.maxlength:
|
|
581
|
+
errors.append(f"Input minlength ({self.minlength}) cannot exceed maxlength ({self.maxlength})")
|
|
582
|
+
return errors
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
class UICheckboxNode(QuantumNode, UILayoutMixin):
|
|
586
|
+
"""Represents <ui:checkbox> - Checkbox widget."""
|
|
587
|
+
|
|
588
|
+
def __init__(self):
|
|
589
|
+
self.bind: Optional[str] = None
|
|
590
|
+
self.label: Optional[str] = None
|
|
591
|
+
self._init_layout_attrs()
|
|
592
|
+
|
|
593
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
594
|
+
d = {"type": "ui_checkbox", "bind": self.bind, "label": self.label}
|
|
595
|
+
d.update(self._layout_to_dict())
|
|
596
|
+
return d
|
|
597
|
+
|
|
598
|
+
def validate(self) -> List[str]:
|
|
599
|
+
return self._validate_layout()
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
class UIRadioNode(QuantumNode, UILayoutMixin):
|
|
603
|
+
"""Represents <ui:radio> - Radio button group."""
|
|
604
|
+
|
|
605
|
+
def __init__(self):
|
|
606
|
+
self.bind: Optional[str] = None
|
|
607
|
+
self.options: Optional[str] = None # comma-separated or source ref
|
|
608
|
+
self._init_layout_attrs()
|
|
609
|
+
|
|
610
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
611
|
+
d = {"type": "ui_radio", "bind": self.bind, "options": self.options}
|
|
612
|
+
d.update(self._layout_to_dict())
|
|
613
|
+
return d
|
|
614
|
+
|
|
615
|
+
def validate(self) -> List[str]:
|
|
616
|
+
return self._validate_layout()
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
class UISwitchNode(QuantumNode, UILayoutMixin):
|
|
620
|
+
"""Represents <ui:switch> - Toggle switch widget."""
|
|
621
|
+
|
|
622
|
+
def __init__(self):
|
|
623
|
+
self.bind: Optional[str] = None
|
|
624
|
+
self.label: Optional[str] = None
|
|
625
|
+
self._init_layout_attrs()
|
|
626
|
+
|
|
627
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
628
|
+
d = {"type": "ui_switch", "bind": self.bind, "label": self.label}
|
|
629
|
+
d.update(self._layout_to_dict())
|
|
630
|
+
return d
|
|
631
|
+
|
|
632
|
+
def validate(self) -> List[str]:
|
|
633
|
+
return self._validate_layout()
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
class UISelectNode(QuantumNode, UILayoutMixin):
|
|
637
|
+
"""Represents <ui:select> - Dropdown select widget."""
|
|
638
|
+
|
|
639
|
+
def __init__(self):
|
|
640
|
+
self.bind: Optional[str] = None
|
|
641
|
+
self.options: Optional[str] = None
|
|
642
|
+
self.source: Optional[str] = None
|
|
643
|
+
self.children: List[QuantumNode] = [] # UIOptionNode children
|
|
644
|
+
self._init_layout_attrs()
|
|
645
|
+
|
|
646
|
+
def add_child(self, child: QuantumNode):
|
|
647
|
+
self.children.append(child)
|
|
648
|
+
|
|
649
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
650
|
+
d = {"type": "ui_select", "bind": self.bind,
|
|
651
|
+
"options": self.options, "source": self.source,
|
|
652
|
+
"children_count": len(self.children)}
|
|
653
|
+
d.update(self._layout_to_dict())
|
|
654
|
+
return d
|
|
655
|
+
|
|
656
|
+
def validate(self) -> List[str]:
|
|
657
|
+
return self._validate_layout()
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
class UITableNode(QuantumNode, UILayoutMixin):
|
|
661
|
+
"""Represents <ui:table> - Data table widget."""
|
|
662
|
+
|
|
663
|
+
def __init__(self):
|
|
664
|
+
self.source: Optional[str] = None
|
|
665
|
+
self.children: List[QuantumNode] = [] # UIColumnNode children
|
|
666
|
+
self._init_layout_attrs()
|
|
667
|
+
|
|
668
|
+
def add_child(self, child: QuantumNode):
|
|
669
|
+
self.children.append(child)
|
|
670
|
+
|
|
671
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
672
|
+
d = {"type": "ui_table", "source": self.source,
|
|
673
|
+
"children_count": len(self.children)}
|
|
674
|
+
d.update(self._layout_to_dict())
|
|
675
|
+
return d
|
|
676
|
+
|
|
677
|
+
def validate(self) -> List[str]:
|
|
678
|
+
return self._validate_layout()
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
class UIColumnNode(QuantumNode):
|
|
682
|
+
"""Represents <ui:column> - Table column definition."""
|
|
683
|
+
|
|
684
|
+
def __init__(self):
|
|
685
|
+
self.key: Optional[str] = None
|
|
686
|
+
self.label: Optional[str] = None
|
|
687
|
+
self.column_width: Optional[str] = None
|
|
688
|
+
self.align: Optional[str] = None # left, center, right
|
|
689
|
+
|
|
690
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
691
|
+
return {"type": "ui_column", "key": self.key,
|
|
692
|
+
"label": self.label, "width": self.column_width,
|
|
693
|
+
"align": self.align}
|
|
694
|
+
|
|
695
|
+
def validate(self) -> List[str]:
|
|
696
|
+
errors = []
|
|
697
|
+
if not self.key:
|
|
698
|
+
errors.append("Column key is required")
|
|
699
|
+
return errors
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
class UIListNode(QuantumNode, UILayoutMixin):
|
|
703
|
+
"""Represents <ui:list> - Repeating list widget."""
|
|
704
|
+
|
|
705
|
+
def __init__(self):
|
|
706
|
+
self.source: Optional[str] = None
|
|
707
|
+
self.as_var: Optional[str] = None # loop variable name
|
|
708
|
+
self.children: List[QuantumNode] = []
|
|
709
|
+
self._init_layout_attrs()
|
|
710
|
+
|
|
711
|
+
def add_child(self, child: QuantumNode):
|
|
712
|
+
self.children.append(child)
|
|
713
|
+
|
|
714
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
715
|
+
d = {"type": "ui_list", "source": self.source,
|
|
716
|
+
"as": self.as_var, "children_count": len(self.children)}
|
|
717
|
+
d.update(self._layout_to_dict())
|
|
718
|
+
return d
|
|
719
|
+
|
|
720
|
+
def validate(self) -> List[str]:
|
|
721
|
+
return self._validate_layout()
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
class UIItemNode(QuantumNode, UILayoutMixin):
|
|
725
|
+
"""Represents <ui:item> - List item container."""
|
|
726
|
+
|
|
727
|
+
def __init__(self):
|
|
728
|
+
self.children: List[QuantumNode] = []
|
|
729
|
+
self._init_layout_attrs()
|
|
730
|
+
|
|
731
|
+
def add_child(self, child: QuantumNode):
|
|
732
|
+
self.children.append(child)
|
|
733
|
+
|
|
734
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
735
|
+
d = {"type": "ui_item", "children_count": len(self.children)}
|
|
736
|
+
d.update(self._layout_to_dict())
|
|
737
|
+
return d
|
|
738
|
+
|
|
739
|
+
def validate(self) -> List[str]:
|
|
740
|
+
errors = self._validate_layout()
|
|
741
|
+
for child in self.children:
|
|
742
|
+
if hasattr(child, 'validate'):
|
|
743
|
+
errors.extend(child.validate())
|
|
744
|
+
return errors
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
class UIImageNode(QuantumNode, UILayoutMixin):
|
|
748
|
+
"""Represents <ui:image> - Image display widget."""
|
|
749
|
+
|
|
750
|
+
def __init__(self):
|
|
751
|
+
self.src: Optional[str] = None
|
|
752
|
+
self.alt: Optional[str] = None
|
|
753
|
+
self._init_layout_attrs()
|
|
754
|
+
|
|
755
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
756
|
+
d = {"type": "ui_image", "src": self.src, "alt": self.alt}
|
|
757
|
+
d.update(self._layout_to_dict())
|
|
758
|
+
return d
|
|
759
|
+
|
|
760
|
+
def validate(self) -> List[str]:
|
|
761
|
+
errors = self._validate_layout()
|
|
762
|
+
if not self.src:
|
|
763
|
+
errors.append("Image src is required")
|
|
764
|
+
return errors
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
class UILinkNode(QuantumNode, UILayoutMixin):
|
|
768
|
+
"""Represents <ui:link> - Hyperlink widget."""
|
|
769
|
+
|
|
770
|
+
def __init__(self):
|
|
771
|
+
self.to: Optional[str] = None
|
|
772
|
+
self.external: bool = False
|
|
773
|
+
self.content: str = ""
|
|
774
|
+
self._init_layout_attrs()
|
|
775
|
+
|
|
776
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
777
|
+
d = {"type": "ui_link", "to": self.to,
|
|
778
|
+
"external": self.external, "content": self.content}
|
|
779
|
+
d.update(self._layout_to_dict())
|
|
780
|
+
return d
|
|
781
|
+
|
|
782
|
+
def validate(self) -> List[str]:
|
|
783
|
+
errors = self._validate_layout()
|
|
784
|
+
if not self.to:
|
|
785
|
+
errors.append("Link 'to' attribute is required")
|
|
786
|
+
return errors
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
class UIProgressNode(QuantumNode, UILayoutMixin):
|
|
790
|
+
"""Represents <ui:progress> - Progress bar widget."""
|
|
791
|
+
|
|
792
|
+
def __init__(self):
|
|
793
|
+
self.value: Optional[str] = None
|
|
794
|
+
self.max: Optional[str] = None
|
|
795
|
+
self._init_layout_attrs()
|
|
796
|
+
|
|
797
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
798
|
+
d = {"type": "ui_progress", "value": self.value, "max": self.max}
|
|
799
|
+
d.update(self._layout_to_dict())
|
|
800
|
+
return d
|
|
801
|
+
|
|
802
|
+
def validate(self) -> List[str]:
|
|
803
|
+
return self._validate_layout()
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
class UITreeNode(QuantumNode, UILayoutMixin):
|
|
807
|
+
"""Represents <ui:tree> - Tree view widget."""
|
|
808
|
+
|
|
809
|
+
def __init__(self):
|
|
810
|
+
self.source: Optional[str] = None
|
|
811
|
+
self.on_select: Optional[str] = None
|
|
812
|
+
self._init_layout_attrs()
|
|
813
|
+
|
|
814
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
815
|
+
d = {"type": "ui_tree", "source": self.source,
|
|
816
|
+
"on_select": self.on_select}
|
|
817
|
+
d.update(self._layout_to_dict())
|
|
818
|
+
return d
|
|
819
|
+
|
|
820
|
+
def validate(self) -> List[str]:
|
|
821
|
+
return self._validate_layout()
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
class UIMenuNode(QuantumNode, UILayoutMixin):
|
|
825
|
+
"""Represents <ui:menu> - Menu container."""
|
|
826
|
+
|
|
827
|
+
def __init__(self):
|
|
828
|
+
self.children: List[QuantumNode] = [] # UIOptionNode children
|
|
829
|
+
self._init_layout_attrs()
|
|
830
|
+
|
|
831
|
+
def add_child(self, child: QuantumNode):
|
|
832
|
+
self.children.append(child)
|
|
833
|
+
|
|
834
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
835
|
+
d = {"type": "ui_menu", "children_count": len(self.children)}
|
|
836
|
+
d.update(self._layout_to_dict())
|
|
837
|
+
return d
|
|
838
|
+
|
|
839
|
+
def validate(self) -> List[str]:
|
|
840
|
+
errors = self._validate_layout()
|
|
841
|
+
for child in self.children:
|
|
842
|
+
if hasattr(child, 'validate'):
|
|
843
|
+
errors.extend(child.validate())
|
|
844
|
+
return errors
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
class UIOptionNode(QuantumNode):
|
|
848
|
+
"""Represents <ui:option> - Menu/select option."""
|
|
849
|
+
|
|
850
|
+
def __init__(self):
|
|
851
|
+
self.value: Optional[str] = None
|
|
852
|
+
self.label: Optional[str] = None
|
|
853
|
+
self.on_click: Optional[str] = None
|
|
854
|
+
|
|
855
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
856
|
+
return {"type": "ui_option", "value": self.value,
|
|
857
|
+
"label": self.label, "on_click": self.on_click}
|
|
858
|
+
|
|
859
|
+
def validate(self) -> List[str]:
|
|
860
|
+
return []
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
class UILogNode(QuantumNode, UILayoutMixin):
|
|
864
|
+
"""Represents <ui:log> - Scrollable log output widget."""
|
|
865
|
+
|
|
866
|
+
def __init__(self):
|
|
867
|
+
self.auto_scroll: bool = True
|
|
868
|
+
self.max_lines: Optional[int] = None
|
|
869
|
+
self._init_layout_attrs()
|
|
870
|
+
|
|
871
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
872
|
+
d = {"type": "ui_log", "auto_scroll": self.auto_scroll,
|
|
873
|
+
"max_lines": self.max_lines}
|
|
874
|
+
d.update(self._layout_to_dict())
|
|
875
|
+
return d
|
|
876
|
+
|
|
877
|
+
def validate(self) -> List[str]:
|
|
878
|
+
return self._validate_layout()
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
class UIMarkdownNode(QuantumNode, UILayoutMixin):
|
|
882
|
+
"""Represents <ui:markdown> - Markdown rendered content."""
|
|
883
|
+
|
|
884
|
+
def __init__(self):
|
|
885
|
+
self.content: str = ""
|
|
886
|
+
self._init_layout_attrs()
|
|
887
|
+
|
|
888
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
889
|
+
d = {"type": "ui_markdown", "content_length": len(self.content)}
|
|
890
|
+
d.update(self._layout_to_dict())
|
|
891
|
+
return d
|
|
892
|
+
|
|
893
|
+
def validate(self) -> List[str]:
|
|
894
|
+
return self._validate_layout()
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
class UIHeaderNode(QuantumNode, UILayoutMixin):
|
|
898
|
+
"""Represents <ui:header> - Page/window header."""
|
|
899
|
+
|
|
900
|
+
def __init__(self):
|
|
901
|
+
self.title: Optional[str] = None
|
|
902
|
+
self.content: str = ""
|
|
903
|
+
self.children: List[QuantumNode] = []
|
|
904
|
+
self._init_layout_attrs()
|
|
905
|
+
|
|
906
|
+
def add_child(self, child: QuantumNode):
|
|
907
|
+
self.children.append(child)
|
|
908
|
+
|
|
909
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
910
|
+
d = {"type": "ui_header", "title": self.title,
|
|
911
|
+
"content": self.content, "children_count": len(self.children)}
|
|
912
|
+
d.update(self._layout_to_dict())
|
|
913
|
+
return d
|
|
914
|
+
|
|
915
|
+
def validate(self) -> List[str]:
|
|
916
|
+
return self._validate_layout()
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
class UIFooterNode(QuantumNode, UILayoutMixin):
|
|
920
|
+
"""Represents <ui:footer> - Page/window footer."""
|
|
921
|
+
|
|
922
|
+
def __init__(self):
|
|
923
|
+
self.content: str = ""
|
|
924
|
+
self.children: List[QuantumNode] = []
|
|
925
|
+
self._init_layout_attrs()
|
|
926
|
+
|
|
927
|
+
def add_child(self, child: QuantumNode):
|
|
928
|
+
self.children.append(child)
|
|
929
|
+
|
|
930
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
931
|
+
d = {"type": "ui_footer", "content": self.content,
|
|
932
|
+
"children_count": len(self.children)}
|
|
933
|
+
d.update(self._layout_to_dict())
|
|
934
|
+
return d
|
|
935
|
+
|
|
936
|
+
def validate(self) -> List[str]:
|
|
937
|
+
return self._validate_layout()
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
class UIRuleNode(QuantumNode):
|
|
941
|
+
"""Represents <ui:rule> - Horizontal rule/separator."""
|
|
942
|
+
|
|
943
|
+
def __init__(self):
|
|
944
|
+
pass
|
|
945
|
+
|
|
946
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
947
|
+
return {"type": "ui_rule"}
|
|
948
|
+
|
|
949
|
+
def validate(self) -> List[str]:
|
|
950
|
+
return []
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
class UILoadingNode(QuantumNode, UILayoutMixin):
|
|
954
|
+
"""Represents <ui:loading> - Loading indicator widget."""
|
|
955
|
+
|
|
956
|
+
def __init__(self):
|
|
957
|
+
self.text: Optional[str] = None
|
|
958
|
+
self._init_layout_attrs()
|
|
959
|
+
|
|
960
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
961
|
+
d = {"type": "ui_loading", "text": self.text}
|
|
962
|
+
d.update(self._layout_to_dict())
|
|
963
|
+
return d
|
|
964
|
+
|
|
965
|
+
def validate(self) -> List[str]:
|
|
966
|
+
return self._validate_layout()
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
class UIBadgeNode(QuantumNode, UILayoutMixin):
|
|
970
|
+
"""Represents <ui:badge> - Small status badge."""
|
|
971
|
+
|
|
972
|
+
def __init__(self):
|
|
973
|
+
self.content: str = ""
|
|
974
|
+
self.variant: Optional[str] = None # primary, secondary, danger, success, warning
|
|
975
|
+
self.badge_color: Optional[str] = None
|
|
976
|
+
self._init_layout_attrs()
|
|
977
|
+
|
|
978
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
979
|
+
d = {"type": "ui_badge", "content": self.content,
|
|
980
|
+
"variant": self.variant, "color": self.badge_color}
|
|
981
|
+
d.update(self._layout_to_dict())
|
|
982
|
+
return d
|
|
983
|
+
|
|
984
|
+
def validate(self) -> List[str]:
|
|
985
|
+
return self._validate_layout()
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
# ============================================
|
|
989
|
+
# ANIMATION MIXIN
|
|
990
|
+
# ============================================
|
|
991
|
+
|
|
992
|
+
class UIAnimationMixin:
|
|
993
|
+
"""Shared animation attributes for UI components that support animations."""
|
|
994
|
+
|
|
995
|
+
# Valid animation types
|
|
996
|
+
ANIMATION_TYPES = ('fade', 'slide', 'scale', 'rotate', 'slide-left', 'slide-right',
|
|
997
|
+
'slide-up', 'slide-down', 'fade-in', 'fade-out', 'scale-in',
|
|
998
|
+
'scale-out', 'bounce', 'pulse', 'shake')
|
|
999
|
+
|
|
1000
|
+
# Valid animation triggers
|
|
1001
|
+
ANIMATION_TRIGGERS = ('on-load', 'on-hover', 'on-click', 'on-visible', 'none')
|
|
1002
|
+
|
|
1003
|
+
# Valid easing functions
|
|
1004
|
+
EASING_FUNCTIONS = ('ease', 'ease-in', 'ease-out', 'ease-in-out', 'linear',
|
|
1005
|
+
'spring', 'bounce')
|
|
1006
|
+
|
|
1007
|
+
def _init_animation_attrs(self):
|
|
1008
|
+
"""Initialize animation attributes."""
|
|
1009
|
+
self.animate: Optional[str] = None # Animation preset (e.g., 'slide-in-left')
|
|
1010
|
+
self.transition: Optional[str] = None # Transition shorthand (e.g., 'scale:0.95:100ms')
|
|
1011
|
+
self.anim_type: Optional[str] = None # Animation type for ui:animate
|
|
1012
|
+
self.anim_duration: Optional[str] = None # Duration in ms or CSS format
|
|
1013
|
+
self.anim_delay: Optional[str] = None # Delay in ms or CSS format
|
|
1014
|
+
self.anim_easing: Optional[str] = None # Easing function
|
|
1015
|
+
self.anim_repeat: Optional[str] = None # Repeat count ('1', '2', 'infinite')
|
|
1016
|
+
self.anim_trigger: Optional[str] = None # Trigger: on-load, on-hover, on-click, on-visible
|
|
1017
|
+
self.anim_direction: Optional[str] = None # normal, reverse, alternate, alternate-reverse
|
|
1018
|
+
|
|
1019
|
+
def _animation_to_dict(self) -> Dict[str, Any]:
|
|
1020
|
+
"""Return animation attributes as dict."""
|
|
1021
|
+
d = {}
|
|
1022
|
+
for attr in ('animate', 'transition', 'anim_type', 'anim_duration',
|
|
1023
|
+
'anim_delay', 'anim_easing', 'anim_repeat', 'anim_trigger',
|
|
1024
|
+
'anim_direction'):
|
|
1025
|
+
val = getattr(self, attr, None)
|
|
1026
|
+
if val is not None:
|
|
1027
|
+
d[attr] = val
|
|
1028
|
+
return d
|
|
1029
|
+
|
|
1030
|
+
def _validate_animation(self) -> List[str]:
|
|
1031
|
+
"""Validate animation attributes."""
|
|
1032
|
+
errors = []
|
|
1033
|
+
if self.anim_type and self.anim_type not in self.ANIMATION_TYPES:
|
|
1034
|
+
errors.append(f"Invalid animation type: {self.anim_type}")
|
|
1035
|
+
if self.anim_trigger and self.anim_trigger not in self.ANIMATION_TRIGGERS:
|
|
1036
|
+
errors.append(f"Invalid animation trigger: {self.anim_trigger}")
|
|
1037
|
+
if self.anim_easing and self.anim_easing not in self.EASING_FUNCTIONS:
|
|
1038
|
+
errors.append(f"Invalid easing function: {self.anim_easing}")
|
|
1039
|
+
return errors
|
|
1040
|
+
|
|
1041
|
+
def has_animation(self) -> bool:
|
|
1042
|
+
"""Check if this node has any animation configured."""
|
|
1043
|
+
return any([
|
|
1044
|
+
self.animate,
|
|
1045
|
+
self.transition,
|
|
1046
|
+
self.anim_type,
|
|
1047
|
+
])
|
|
1048
|
+
|
|
1049
|
+
|
|
1050
|
+
# ============================================
|
|
1051
|
+
# ANIMATION NODE
|
|
1052
|
+
# ============================================
|
|
1053
|
+
|
|
1054
|
+
class UIAnimateNode(QuantumNode, UILayoutMixin, UIAnimationMixin):
|
|
1055
|
+
"""
|
|
1056
|
+
Represents <ui:animate> - Animation wrapper container.
|
|
1057
|
+
|
|
1058
|
+
Wraps child elements and applies animation effects.
|
|
1059
|
+
|
|
1060
|
+
Syntax examples:
|
|
1061
|
+
<ui:animate type="fade" duration="300" trigger="on-load">
|
|
1062
|
+
<ui:panel>Content fades in</ui:panel>
|
|
1063
|
+
</ui:animate>
|
|
1064
|
+
|
|
1065
|
+
<ui:animate type="slide-left" delay="200" easing="ease-out">
|
|
1066
|
+
<ui:text>Slides in from left</ui:text>
|
|
1067
|
+
</ui:animate>
|
|
1068
|
+
"""
|
|
1069
|
+
|
|
1070
|
+
def __init__(self):
|
|
1071
|
+
self.children: List[QuantumNode] = []
|
|
1072
|
+
self._init_layout_attrs()
|
|
1073
|
+
self._init_animation_attrs()
|
|
1074
|
+
|
|
1075
|
+
def add_child(self, child: QuantumNode):
|
|
1076
|
+
self.children.append(child)
|
|
1077
|
+
|
|
1078
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1079
|
+
d = {
|
|
1080
|
+
"type": "ui_animate",
|
|
1081
|
+
"children_count": len(self.children),
|
|
1082
|
+
}
|
|
1083
|
+
d.update(self._layout_to_dict())
|
|
1084
|
+
d.update(self._animation_to_dict())
|
|
1085
|
+
return d
|
|
1086
|
+
|
|
1087
|
+
def validate(self) -> List[str]:
|
|
1088
|
+
errors = self._validate_layout()
|
|
1089
|
+
errors.extend(self._validate_animation())
|
|
1090
|
+
if not self.anim_type and not self.animate:
|
|
1091
|
+
errors.append("ui:animate requires 'type' or 'animate' attribute")
|
|
1092
|
+
for child in self.children:
|
|
1093
|
+
if hasattr(child, 'validate'):
|
|
1094
|
+
errors.extend(child.validate())
|
|
1095
|
+
return errors
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
# ============================================
|
|
1099
|
+
# COMPONENT LIBRARY NODES
|
|
1100
|
+
# ============================================
|
|
1101
|
+
|
|
1102
|
+
class UICardNode(QuantumNode, UILayoutMixin):
|
|
1103
|
+
"""Represents <ui:card> - Card with header, body, footer."""
|
|
1104
|
+
|
|
1105
|
+
def __init__(self):
|
|
1106
|
+
self.title: Optional[str] = None
|
|
1107
|
+
self.subtitle: Optional[str] = None
|
|
1108
|
+
self.image: Optional[str] = None # Header image URL
|
|
1109
|
+
self.variant: Optional[str] = None # default, elevated, outlined
|
|
1110
|
+
self.children: List[QuantumNode] = []
|
|
1111
|
+
self._init_layout_attrs()
|
|
1112
|
+
|
|
1113
|
+
def add_child(self, child: QuantumNode):
|
|
1114
|
+
self.children.append(child)
|
|
1115
|
+
|
|
1116
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1117
|
+
d = {"type": "ui_card", "title": self.title,
|
|
1118
|
+
"subtitle": self.subtitle, "image": self.image,
|
|
1119
|
+
"variant": self.variant, "children_count": len(self.children)}
|
|
1120
|
+
d.update(self._layout_to_dict())
|
|
1121
|
+
return d
|
|
1122
|
+
|
|
1123
|
+
def validate(self) -> List[str]:
|
|
1124
|
+
errors = self._validate_layout()
|
|
1125
|
+
for child in self.children:
|
|
1126
|
+
if hasattr(child, 'validate'):
|
|
1127
|
+
errors.extend(child.validate())
|
|
1128
|
+
return errors
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
class UICardHeaderNode(QuantumNode, UILayoutMixin):
|
|
1132
|
+
"""Represents <ui:card-header> - Card header section."""
|
|
1133
|
+
|
|
1134
|
+
def __init__(self):
|
|
1135
|
+
self.children: List[QuantumNode] = []
|
|
1136
|
+
self._init_layout_attrs()
|
|
1137
|
+
|
|
1138
|
+
def add_child(self, child: QuantumNode):
|
|
1139
|
+
self.children.append(child)
|
|
1140
|
+
|
|
1141
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1142
|
+
d = {"type": "ui_card_header", "children_count": len(self.children)}
|
|
1143
|
+
d.update(self._layout_to_dict())
|
|
1144
|
+
return d
|
|
1145
|
+
|
|
1146
|
+
def validate(self) -> List[str]:
|
|
1147
|
+
errors = self._validate_layout()
|
|
1148
|
+
for child in self.children:
|
|
1149
|
+
if hasattr(child, 'validate'):
|
|
1150
|
+
errors.extend(child.validate())
|
|
1151
|
+
return errors
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
class UICardBodyNode(QuantumNode, UILayoutMixin):
|
|
1155
|
+
"""Represents <ui:card-body> - Card body section."""
|
|
1156
|
+
|
|
1157
|
+
def __init__(self):
|
|
1158
|
+
self.children: List[QuantumNode] = []
|
|
1159
|
+
self._init_layout_attrs()
|
|
1160
|
+
|
|
1161
|
+
def add_child(self, child: QuantumNode):
|
|
1162
|
+
self.children.append(child)
|
|
1163
|
+
|
|
1164
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1165
|
+
d = {"type": "ui_card_body", "children_count": len(self.children)}
|
|
1166
|
+
d.update(self._layout_to_dict())
|
|
1167
|
+
return d
|
|
1168
|
+
|
|
1169
|
+
def validate(self) -> List[str]:
|
|
1170
|
+
errors = self._validate_layout()
|
|
1171
|
+
for child in self.children:
|
|
1172
|
+
if hasattr(child, 'validate'):
|
|
1173
|
+
errors.extend(child.validate())
|
|
1174
|
+
return errors
|
|
1175
|
+
|
|
1176
|
+
|
|
1177
|
+
class UICardFooterNode(QuantumNode, UILayoutMixin):
|
|
1178
|
+
"""Represents <ui:card-footer> - Card footer section."""
|
|
1179
|
+
|
|
1180
|
+
def __init__(self):
|
|
1181
|
+
self.children: List[QuantumNode] = []
|
|
1182
|
+
self._init_layout_attrs()
|
|
1183
|
+
|
|
1184
|
+
def add_child(self, child: QuantumNode):
|
|
1185
|
+
self.children.append(child)
|
|
1186
|
+
|
|
1187
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1188
|
+
d = {"type": "ui_card_footer", "children_count": len(self.children)}
|
|
1189
|
+
d.update(self._layout_to_dict())
|
|
1190
|
+
return d
|
|
1191
|
+
|
|
1192
|
+
def validate(self) -> List[str]:
|
|
1193
|
+
errors = self._validate_layout()
|
|
1194
|
+
for child in self.children:
|
|
1195
|
+
if hasattr(child, 'validate'):
|
|
1196
|
+
errors.extend(child.validate())
|
|
1197
|
+
return errors
|
|
1198
|
+
|
|
1199
|
+
|
|
1200
|
+
class UIModalNode(QuantumNode, UILayoutMixin):
|
|
1201
|
+
"""Represents <ui:modal> - Modal/dialog with open/close."""
|
|
1202
|
+
|
|
1203
|
+
def __init__(self):
|
|
1204
|
+
self.title: Optional[str] = None
|
|
1205
|
+
self.modal_id: Optional[str] = None # Unique ID for targeting
|
|
1206
|
+
self.open: bool = False # Initial state
|
|
1207
|
+
self.closable: bool = True # Show close button
|
|
1208
|
+
self.size: Optional[str] = None # sm, md, lg, xl, full
|
|
1209
|
+
self.children: List[QuantumNode] = []
|
|
1210
|
+
self._init_layout_attrs()
|
|
1211
|
+
|
|
1212
|
+
def add_child(self, child: QuantumNode):
|
|
1213
|
+
self.children.append(child)
|
|
1214
|
+
|
|
1215
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1216
|
+
d = {"type": "ui_modal", "title": self.title,
|
|
1217
|
+
"modal_id": self.modal_id, "open": self.open,
|
|
1218
|
+
"closable": self.closable, "size": self.size,
|
|
1219
|
+
"children_count": len(self.children)}
|
|
1220
|
+
d.update(self._layout_to_dict())
|
|
1221
|
+
return d
|
|
1222
|
+
|
|
1223
|
+
def validate(self) -> List[str]:
|
|
1224
|
+
errors = self._validate_layout()
|
|
1225
|
+
for child in self.children:
|
|
1226
|
+
if hasattr(child, 'validate'):
|
|
1227
|
+
errors.extend(child.validate())
|
|
1228
|
+
return errors
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
class UIChartNode(QuantumNode, UILayoutMixin):
|
|
1232
|
+
"""Represents <ui:chart> - Simple charts (bar, line, pie)."""
|
|
1233
|
+
|
|
1234
|
+
def __init__(self):
|
|
1235
|
+
self.chart_type: str = "bar" # bar, line, pie, doughnut
|
|
1236
|
+
self.source: Optional[str] = None # Data source binding
|
|
1237
|
+
self.labels: Optional[str] = None # Comma-separated labels
|
|
1238
|
+
self.values: Optional[str] = None # Comma-separated values
|
|
1239
|
+
self.title: Optional[str] = None
|
|
1240
|
+
self.colors: Optional[str] = None # Comma-separated colors
|
|
1241
|
+
self._init_layout_attrs()
|
|
1242
|
+
|
|
1243
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1244
|
+
d = {"type": "ui_chart", "chart_type": self.chart_type,
|
|
1245
|
+
"source": self.source, "labels": self.labels,
|
|
1246
|
+
"values": self.values, "title": self.title,
|
|
1247
|
+
"colors": self.colors}
|
|
1248
|
+
d.update(self._layout_to_dict())
|
|
1249
|
+
return d
|
|
1250
|
+
|
|
1251
|
+
def validate(self) -> List[str]:
|
|
1252
|
+
errors = self._validate_layout()
|
|
1253
|
+
if self.chart_type not in ('bar', 'line', 'pie', 'doughnut'):
|
|
1254
|
+
errors.append(f"Invalid chart type: {self.chart_type}")
|
|
1255
|
+
return errors
|
|
1256
|
+
|
|
1257
|
+
|
|
1258
|
+
class UIAvatarNode(QuantumNode, UILayoutMixin):
|
|
1259
|
+
"""Represents <ui:avatar> - User avatar with image/initials."""
|
|
1260
|
+
|
|
1261
|
+
def __init__(self):
|
|
1262
|
+
self.src: Optional[str] = None # Image URL
|
|
1263
|
+
self.name: Optional[str] = None # Name for initials fallback
|
|
1264
|
+
self.size: Optional[str] = None # xs, sm, md, lg, xl
|
|
1265
|
+
self.shape: str = "circle" # circle, square
|
|
1266
|
+
self.status: Optional[str] = None # online, offline, away, busy
|
|
1267
|
+
self._init_layout_attrs()
|
|
1268
|
+
|
|
1269
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1270
|
+
d = {"type": "ui_avatar", "src": self.src,
|
|
1271
|
+
"name": self.name, "size": self.size,
|
|
1272
|
+
"shape": self.shape, "status": self.status}
|
|
1273
|
+
d.update(self._layout_to_dict())
|
|
1274
|
+
return d
|
|
1275
|
+
|
|
1276
|
+
def validate(self) -> List[str]:
|
|
1277
|
+
errors = self._validate_layout()
|
|
1278
|
+
if self.shape not in ('circle', 'square'):
|
|
1279
|
+
errors.append(f"Invalid avatar shape: {self.shape}")
|
|
1280
|
+
return errors
|
|
1281
|
+
|
|
1282
|
+
|
|
1283
|
+
class UITooltipNode(QuantumNode, UILayoutMixin):
|
|
1284
|
+
"""Represents <ui:tooltip> - Tooltip on hover."""
|
|
1285
|
+
|
|
1286
|
+
def __init__(self):
|
|
1287
|
+
self.content: str = "" # Tooltip text
|
|
1288
|
+
self.position: str = "top" # top, bottom, left, right
|
|
1289
|
+
self.children: List[QuantumNode] = [] # Element to attach tooltip to
|
|
1290
|
+
self._init_layout_attrs()
|
|
1291
|
+
|
|
1292
|
+
def add_child(self, child: QuantumNode):
|
|
1293
|
+
self.children.append(child)
|
|
1294
|
+
|
|
1295
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1296
|
+
d = {"type": "ui_tooltip", "content": self.content,
|
|
1297
|
+
"position": self.position, "children_count": len(self.children)}
|
|
1298
|
+
d.update(self._layout_to_dict())
|
|
1299
|
+
return d
|
|
1300
|
+
|
|
1301
|
+
def validate(self) -> List[str]:
|
|
1302
|
+
errors = self._validate_layout()
|
|
1303
|
+
if self.position not in ('top', 'bottom', 'left', 'right'):
|
|
1304
|
+
errors.append(f"Invalid tooltip position: {self.position}")
|
|
1305
|
+
return errors
|
|
1306
|
+
|
|
1307
|
+
|
|
1308
|
+
class UIDropdownNode(QuantumNode, UILayoutMixin):
|
|
1309
|
+
"""Represents <ui:dropdown> - Dropdown menu."""
|
|
1310
|
+
|
|
1311
|
+
def __init__(self):
|
|
1312
|
+
self.label: Optional[str] = None # Trigger button label
|
|
1313
|
+
self.trigger: str = "click" # click, hover
|
|
1314
|
+
self.dropdown_align: str = "left" # left, right
|
|
1315
|
+
self.children: List[QuantumNode] = [] # UIOptionNode children
|
|
1316
|
+
self._init_layout_attrs()
|
|
1317
|
+
|
|
1318
|
+
def add_child(self, child: QuantumNode):
|
|
1319
|
+
self.children.append(child)
|
|
1320
|
+
|
|
1321
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1322
|
+
d = {"type": "ui_dropdown", "label": self.label,
|
|
1323
|
+
"trigger": self.trigger, "dropdown_align": self.dropdown_align,
|
|
1324
|
+
"children_count": len(self.children)}
|
|
1325
|
+
d.update(self._layout_to_dict())
|
|
1326
|
+
return d
|
|
1327
|
+
|
|
1328
|
+
def validate(self) -> List[str]:
|
|
1329
|
+
errors = self._validate_layout()
|
|
1330
|
+
if self.trigger not in ('click', 'hover'):
|
|
1331
|
+
errors.append(f"Invalid dropdown trigger: {self.trigger}")
|
|
1332
|
+
return errors
|
|
1333
|
+
|
|
1334
|
+
|
|
1335
|
+
class UIAlertNode(QuantumNode, UILayoutMixin):
|
|
1336
|
+
"""Represents <ui:alert> - Alert/notification box."""
|
|
1337
|
+
|
|
1338
|
+
def __init__(self):
|
|
1339
|
+
self.content: str = ""
|
|
1340
|
+
self.title: Optional[str] = None
|
|
1341
|
+
self.variant: str = "info" # info, success, warning, danger
|
|
1342
|
+
self.dismissible: bool = False
|
|
1343
|
+
self.icon: Optional[str] = None # Icon name
|
|
1344
|
+
self.children: List[QuantumNode] = []
|
|
1345
|
+
self._init_layout_attrs()
|
|
1346
|
+
|
|
1347
|
+
def add_child(self, child: QuantumNode):
|
|
1348
|
+
self.children.append(child)
|
|
1349
|
+
|
|
1350
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1351
|
+
d = {"type": "ui_alert", "content": self.content,
|
|
1352
|
+
"title": self.title, "variant": self.variant,
|
|
1353
|
+
"dismissible": self.dismissible, "icon": self.icon,
|
|
1354
|
+
"children_count": len(self.children)}
|
|
1355
|
+
d.update(self._layout_to_dict())
|
|
1356
|
+
return d
|
|
1357
|
+
|
|
1358
|
+
def validate(self) -> List[str]:
|
|
1359
|
+
errors = self._validate_layout()
|
|
1360
|
+
if self.variant not in ('info', 'success', 'warning', 'danger'):
|
|
1361
|
+
errors.append(f"Invalid alert variant: {self.variant}")
|
|
1362
|
+
return errors
|
|
1363
|
+
|
|
1364
|
+
|
|
1365
|
+
class UIBreadcrumbNode(QuantumNode, UILayoutMixin):
|
|
1366
|
+
"""Represents <ui:breadcrumb> - Navigation breadcrumbs."""
|
|
1367
|
+
|
|
1368
|
+
def __init__(self):
|
|
1369
|
+
self.separator: str = "/" # Separator character
|
|
1370
|
+
self.children: List[QuantumNode] = [] # UIBreadcrumbItemNode children
|
|
1371
|
+
self._init_layout_attrs()
|
|
1372
|
+
|
|
1373
|
+
def add_child(self, child: QuantumNode):
|
|
1374
|
+
self.children.append(child)
|
|
1375
|
+
|
|
1376
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1377
|
+
d = {"type": "ui_breadcrumb", "separator": self.separator,
|
|
1378
|
+
"children_count": len(self.children)}
|
|
1379
|
+
d.update(self._layout_to_dict())
|
|
1380
|
+
return d
|
|
1381
|
+
|
|
1382
|
+
def validate(self) -> List[str]:
|
|
1383
|
+
errors = self._validate_layout()
|
|
1384
|
+
for child in self.children:
|
|
1385
|
+
if hasattr(child, 'validate'):
|
|
1386
|
+
errors.extend(child.validate())
|
|
1387
|
+
return errors
|
|
1388
|
+
|
|
1389
|
+
|
|
1390
|
+
class UIBreadcrumbItemNode(QuantumNode):
|
|
1391
|
+
"""Represents <ui:breadcrumb-item> - Individual breadcrumb item."""
|
|
1392
|
+
|
|
1393
|
+
def __init__(self):
|
|
1394
|
+
self.label: str = ""
|
|
1395
|
+
self.to: Optional[str] = None # Link URL (None = current/last item)
|
|
1396
|
+
self.icon: Optional[str] = None
|
|
1397
|
+
|
|
1398
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1399
|
+
return {"type": "ui_breadcrumb_item", "label": self.label,
|
|
1400
|
+
"to": self.to, "icon": self.icon}
|
|
1401
|
+
|
|
1402
|
+
def validate(self) -> List[str]:
|
|
1403
|
+
return []
|
|
1404
|
+
|
|
1405
|
+
|
|
1406
|
+
class UIPaginationNode(QuantumNode, UILayoutMixin):
|
|
1407
|
+
"""Represents <ui:pagination> - Pagination controls."""
|
|
1408
|
+
|
|
1409
|
+
def __init__(self):
|
|
1410
|
+
self.total: Optional[str] = None # Total items
|
|
1411
|
+
self.page_size: str = "10" # Items per page
|
|
1412
|
+
self.current: str = "1" # Current page
|
|
1413
|
+
self.bind: Optional[str] = None # Binding for current page
|
|
1414
|
+
self.on_change: Optional[str] = None # Callback on page change
|
|
1415
|
+
self.show_total: bool = False # Show total items count
|
|
1416
|
+
self.show_jump: bool = False # Show page jump input
|
|
1417
|
+
self._init_layout_attrs()
|
|
1418
|
+
|
|
1419
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1420
|
+
d = {"type": "ui_pagination", "total": self.total,
|
|
1421
|
+
"page_size": self.page_size, "current": self.current,
|
|
1422
|
+
"bind": self.bind, "on_change": self.on_change,
|
|
1423
|
+
"show_total": self.show_total, "show_jump": self.show_jump}
|
|
1424
|
+
d.update(self._layout_to_dict())
|
|
1425
|
+
return d
|
|
1426
|
+
|
|
1427
|
+
def validate(self) -> List[str]:
|
|
1428
|
+
return self._validate_layout()
|
|
1429
|
+
|
|
1430
|
+
|
|
1431
|
+
class UISkeletonNode(QuantumNode, UILayoutMixin):
|
|
1432
|
+
"""Represents <ui:skeleton> - Loading skeleton placeholder."""
|
|
1433
|
+
|
|
1434
|
+
def __init__(self):
|
|
1435
|
+
self.variant: str = "text" # text, circle, rect, card
|
|
1436
|
+
self.lines: int = 1 # Number of lines for text variant
|
|
1437
|
+
self.animated: bool = True # Animate the skeleton
|
|
1438
|
+
self._init_layout_attrs()
|
|
1439
|
+
|
|
1440
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1441
|
+
d = {"type": "ui_skeleton", "variant": self.variant,
|
|
1442
|
+
"lines": self.lines, "animated": self.animated}
|
|
1443
|
+
d.update(self._layout_to_dict())
|
|
1444
|
+
return d
|
|
1445
|
+
|
|
1446
|
+
def validate(self) -> List[str]:
|
|
1447
|
+
errors = self._validate_layout()
|
|
1448
|
+
if self.variant not in ('text', 'circle', 'rect', 'card'):
|
|
1449
|
+
errors.append(f"Invalid skeleton variant: {self.variant}")
|
|
1450
|
+
return errors
|
|
1451
|
+
|
|
1452
|
+
|
|
1453
|
+
# ============================================
|
|
1454
|
+
# TOAST NOTIFICATION
|
|
1455
|
+
# ============================================
|
|
1456
|
+
|
|
1457
|
+
class UIToastNode(QuantumNode, UILayoutMixin):
|
|
1458
|
+
"""
|
|
1459
|
+
Represents <ui:toast> - Toast notification.
|
|
1460
|
+
|
|
1461
|
+
Displays temporary notification messages that auto-dismiss.
|
|
1462
|
+
|
|
1463
|
+
Attributes:
|
|
1464
|
+
variant: Style variant (info, success, warning, danger)
|
|
1465
|
+
position: Screen position (top-right, top-left, bottom-right, bottom-left, top-center, bottom-center)
|
|
1466
|
+
duration: Auto-dismiss time in ms (0 = no auto-dismiss)
|
|
1467
|
+
dismissible: Show close button
|
|
1468
|
+
icon: Icon to display
|
|
1469
|
+
|
|
1470
|
+
Example:
|
|
1471
|
+
<ui:toast variant="success" position="top-right" duration="3000">
|
|
1472
|
+
File saved successfully!
|
|
1473
|
+
</ui:toast>
|
|
1474
|
+
"""
|
|
1475
|
+
|
|
1476
|
+
VALID_VARIANTS = {'info', 'success', 'warning', 'danger'}
|
|
1477
|
+
VALID_POSITIONS = {'top-right', 'top-left', 'bottom-right', 'bottom-left',
|
|
1478
|
+
'top-center', 'bottom-center'}
|
|
1479
|
+
|
|
1480
|
+
def __init__(self):
|
|
1481
|
+
self.variant: str = "info"
|
|
1482
|
+
self.position: str = "top-right"
|
|
1483
|
+
self.duration: int = 3000 # ms, 0 = no auto-dismiss
|
|
1484
|
+
self.dismissible: bool = True
|
|
1485
|
+
self.icon: Optional[str] = None
|
|
1486
|
+
self.title: Optional[str] = None
|
|
1487
|
+
self.message: str = ""
|
|
1488
|
+
self.show: Optional[str] = None # Binding to control visibility
|
|
1489
|
+
self.on_close: Optional[str] = None # Callback on close
|
|
1490
|
+
self._init_layout_attrs()
|
|
1491
|
+
|
|
1492
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1493
|
+
d = {"type": "ui_toast", "variant": self.variant,
|
|
1494
|
+
"position": self.position, "duration": self.duration,
|
|
1495
|
+
"dismissible": self.dismissible, "icon": self.icon,
|
|
1496
|
+
"title": self.title, "message": self.message,
|
|
1497
|
+
"show": self.show, "on_close": self.on_close}
|
|
1498
|
+
d.update(self._layout_to_dict())
|
|
1499
|
+
return d
|
|
1500
|
+
|
|
1501
|
+
def validate(self) -> List[str]:
|
|
1502
|
+
errors = self._validate_layout()
|
|
1503
|
+
if self.variant not in self.VALID_VARIANTS:
|
|
1504
|
+
errors.append(f"Invalid toast variant: {self.variant}. Valid: {', '.join(self.VALID_VARIANTS)}")
|
|
1505
|
+
if self.position not in self.VALID_POSITIONS:
|
|
1506
|
+
errors.append(f"Invalid toast position: {self.position}. Valid: {', '.join(self.VALID_POSITIONS)}")
|
|
1507
|
+
return errors
|
|
1508
|
+
|
|
1509
|
+
|
|
1510
|
+
class UIToastContainerNode(QuantumNode, UILayoutMixin):
|
|
1511
|
+
"""
|
|
1512
|
+
Represents <ui:toast-container> - Container for toast notifications.
|
|
1513
|
+
|
|
1514
|
+
A single container should be placed in the application to manage all toasts.
|
|
1515
|
+
|
|
1516
|
+
Attributes:
|
|
1517
|
+
position: Default position for toasts
|
|
1518
|
+
max_toasts: Maximum visible toasts (older ones are dismissed)
|
|
1519
|
+
|
|
1520
|
+
Example:
|
|
1521
|
+
<ui:toast-container position="top-right" max-toasts="5" />
|
|
1522
|
+
"""
|
|
1523
|
+
|
|
1524
|
+
def __init__(self):
|
|
1525
|
+
self.position: str = "top-right"
|
|
1526
|
+
self.max_toasts: int = 5
|
|
1527
|
+
self._init_layout_attrs()
|
|
1528
|
+
|
|
1529
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1530
|
+
d = {"type": "ui_toast_container", "position": self.position,
|
|
1531
|
+
"max_toasts": self.max_toasts}
|
|
1532
|
+
d.update(self._layout_to_dict())
|
|
1533
|
+
return d
|
|
1534
|
+
|
|
1535
|
+
def validate(self) -> List[str]:
|
|
1536
|
+
return self._validate_layout()
|
|
1537
|
+
|
|
1538
|
+
|
|
1539
|
+
# ============================================
|
|
1540
|
+
# CAROUSEL / SLIDER
|
|
1541
|
+
# ============================================
|
|
1542
|
+
|
|
1543
|
+
class UICarouselNode(QuantumNode, UILayoutMixin):
|
|
1544
|
+
"""
|
|
1545
|
+
Represents <ui:carousel> - Image/content carousel slider.
|
|
1546
|
+
|
|
1547
|
+
Displays slides with navigation controls and optional auto-play.
|
|
1548
|
+
|
|
1549
|
+
Attributes:
|
|
1550
|
+
auto_play: Enable automatic slide advancement
|
|
1551
|
+
interval: Time between slides in ms (for auto-play)
|
|
1552
|
+
show_indicators: Show dot indicators
|
|
1553
|
+
show_arrows: Show prev/next arrows
|
|
1554
|
+
loop: Loop back to start after last slide
|
|
1555
|
+
animation: Slide animation type (slide, fade)
|
|
1556
|
+
|
|
1557
|
+
Example:
|
|
1558
|
+
<ui:carousel auto-play="true" interval="5000" show-indicators="true">
|
|
1559
|
+
<ui:slide><ui:image src="img1.jpg" /></ui:slide>
|
|
1560
|
+
<ui:slide><ui:image src="img2.jpg" /></ui:slide>
|
|
1561
|
+
<ui:slide><ui:image src="img3.jpg" /></ui:slide>
|
|
1562
|
+
</ui:carousel>
|
|
1563
|
+
"""
|
|
1564
|
+
|
|
1565
|
+
def __init__(self):
|
|
1566
|
+
self.auto_play: bool = False
|
|
1567
|
+
self.interval: int = 5000 # ms
|
|
1568
|
+
self.show_indicators: bool = True
|
|
1569
|
+
self.show_arrows: bool = True
|
|
1570
|
+
self.loop: bool = True
|
|
1571
|
+
self.animation: str = "slide" # slide, fade
|
|
1572
|
+
self.current: int = 0 # Current slide index
|
|
1573
|
+
self.bind: Optional[str] = None # Binding for current index
|
|
1574
|
+
self.on_change: Optional[str] = None # Callback on slide change
|
|
1575
|
+
self.children: List[QuantumNode] = [] # UISlideNode children
|
|
1576
|
+
self._init_layout_attrs()
|
|
1577
|
+
|
|
1578
|
+
def add_child(self, child: QuantumNode):
|
|
1579
|
+
self.children.append(child)
|
|
1580
|
+
|
|
1581
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1582
|
+
d = {"type": "ui_carousel", "auto_play": self.auto_play,
|
|
1583
|
+
"interval": self.interval, "show_indicators": self.show_indicators,
|
|
1584
|
+
"show_arrows": self.show_arrows, "loop": self.loop,
|
|
1585
|
+
"animation": self.animation, "current": self.current,
|
|
1586
|
+
"bind": self.bind, "on_change": self.on_change,
|
|
1587
|
+
"children_count": len(self.children)}
|
|
1588
|
+
d.update(self._layout_to_dict())
|
|
1589
|
+
return d
|
|
1590
|
+
|
|
1591
|
+
def validate(self) -> List[str]:
|
|
1592
|
+
errors = self._validate_layout()
|
|
1593
|
+
if self.animation not in ('slide', 'fade'):
|
|
1594
|
+
errors.append(f"Invalid carousel animation: {self.animation}")
|
|
1595
|
+
for child in self.children:
|
|
1596
|
+
if hasattr(child, 'validate'):
|
|
1597
|
+
errors.extend(child.validate())
|
|
1598
|
+
return errors
|
|
1599
|
+
|
|
1600
|
+
|
|
1601
|
+
class UISlideNode(QuantumNode, UILayoutMixin):
|
|
1602
|
+
"""
|
|
1603
|
+
Represents <ui:slide> - Single slide in a carousel.
|
|
1604
|
+
|
|
1605
|
+
Can contain any content (images, text, cards, etc.)
|
|
1606
|
+
|
|
1607
|
+
Example:
|
|
1608
|
+
<ui:slide>
|
|
1609
|
+
<ui:image src="hero.jpg" alt="Hero image" />
|
|
1610
|
+
<ui:text size="xl">Welcome!</ui:text>
|
|
1611
|
+
</ui:slide>
|
|
1612
|
+
"""
|
|
1613
|
+
|
|
1614
|
+
def __init__(self):
|
|
1615
|
+
self.children: List[QuantumNode] = []
|
|
1616
|
+
self._init_layout_attrs()
|
|
1617
|
+
|
|
1618
|
+
def add_child(self, child: QuantumNode):
|
|
1619
|
+
self.children.append(child)
|
|
1620
|
+
|
|
1621
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1622
|
+
d = {"type": "ui_slide", "children_count": len(self.children)}
|
|
1623
|
+
d.update(self._layout_to_dict())
|
|
1624
|
+
return d
|
|
1625
|
+
|
|
1626
|
+
def validate(self) -> List[str]:
|
|
1627
|
+
errors = self._validate_layout()
|
|
1628
|
+
for child in self.children:
|
|
1629
|
+
if hasattr(child, 'validate'):
|
|
1630
|
+
errors.extend(child.validate())
|
|
1631
|
+
return errors
|
|
1632
|
+
|
|
1633
|
+
|
|
1634
|
+
# ============================================
|
|
1635
|
+
# STEPPER / WIZARD
|
|
1636
|
+
# ============================================
|
|
1637
|
+
|
|
1638
|
+
class UIStepperNode(QuantumNode, UILayoutMixin):
|
|
1639
|
+
"""
|
|
1640
|
+
Represents <ui:stepper> - Multi-step wizard/progress indicator.
|
|
1641
|
+
|
|
1642
|
+
Shows progress through a multi-step process with navigation.
|
|
1643
|
+
|
|
1644
|
+
Attributes:
|
|
1645
|
+
orientation: Layout direction (horizontal, vertical)
|
|
1646
|
+
current: Current step index (0-based)
|
|
1647
|
+
linear: If true, must complete steps in order
|
|
1648
|
+
show_labels: Show step labels
|
|
1649
|
+
clickable: Allow clicking on steps to navigate
|
|
1650
|
+
bind: Binding for current step
|
|
1651
|
+
|
|
1652
|
+
Example:
|
|
1653
|
+
<ui:stepper current="1" linear="true">
|
|
1654
|
+
<ui:step title="Account" description="Create account">
|
|
1655
|
+
<ui:form>...</ui:form>
|
|
1656
|
+
</ui:step>
|
|
1657
|
+
<ui:step title="Profile" description="Add details">
|
|
1658
|
+
<ui:form>...</ui:form>
|
|
1659
|
+
</ui:step>
|
|
1660
|
+
<ui:step title="Complete" description="All done!">
|
|
1661
|
+
<ui:text>Success!</ui:text>
|
|
1662
|
+
</ui:step>
|
|
1663
|
+
</ui:stepper>
|
|
1664
|
+
"""
|
|
1665
|
+
|
|
1666
|
+
def __init__(self):
|
|
1667
|
+
self.orientation: str = "horizontal" # horizontal, vertical
|
|
1668
|
+
self.current: int = 0
|
|
1669
|
+
self.linear: bool = True # Must complete in order
|
|
1670
|
+
self.show_labels: bool = True
|
|
1671
|
+
self.clickable: bool = False # Allow clicking on steps
|
|
1672
|
+
self.bind: Optional[str] = None # Binding for current step
|
|
1673
|
+
self.on_change: Optional[str] = None # Callback on step change
|
|
1674
|
+
self.on_complete: Optional[str] = None # Callback when all steps done
|
|
1675
|
+
self.children: List[QuantumNode] = [] # UIStepNode children
|
|
1676
|
+
self._init_layout_attrs()
|
|
1677
|
+
|
|
1678
|
+
def add_child(self, child: QuantumNode):
|
|
1679
|
+
self.children.append(child)
|
|
1680
|
+
|
|
1681
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1682
|
+
d = {"type": "ui_stepper", "orientation": self.orientation,
|
|
1683
|
+
"current": self.current, "linear": self.linear,
|
|
1684
|
+
"show_labels": self.show_labels, "clickable": self.clickable,
|
|
1685
|
+
"bind": self.bind, "on_change": self.on_change,
|
|
1686
|
+
"on_complete": self.on_complete,
|
|
1687
|
+
"children_count": len(self.children)}
|
|
1688
|
+
d.update(self._layout_to_dict())
|
|
1689
|
+
return d
|
|
1690
|
+
|
|
1691
|
+
def validate(self) -> List[str]:
|
|
1692
|
+
errors = self._validate_layout()
|
|
1693
|
+
if self.orientation not in ('horizontal', 'vertical'):
|
|
1694
|
+
errors.append(f"Invalid stepper orientation: {self.orientation}")
|
|
1695
|
+
for child in self.children:
|
|
1696
|
+
if hasattr(child, 'validate'):
|
|
1697
|
+
errors.extend(child.validate())
|
|
1698
|
+
return errors
|
|
1699
|
+
|
|
1700
|
+
|
|
1701
|
+
class UIStepNode(QuantumNode, UILayoutMixin):
|
|
1702
|
+
"""
|
|
1703
|
+
Represents <ui:step> - Single step in a stepper.
|
|
1704
|
+
|
|
1705
|
+
Attributes:
|
|
1706
|
+
title: Step title (shown in indicator)
|
|
1707
|
+
description: Optional description
|
|
1708
|
+
icon: Optional icon
|
|
1709
|
+
optional: If true, step can be skipped
|
|
1710
|
+
completed: Override completed state
|
|
1711
|
+
|
|
1712
|
+
Example:
|
|
1713
|
+
<ui:step title="Payment" description="Enter payment details" icon="credit-card">
|
|
1714
|
+
<ui:form>...</ui:form>
|
|
1715
|
+
</ui:step>
|
|
1716
|
+
"""
|
|
1717
|
+
|
|
1718
|
+
def __init__(self):
|
|
1719
|
+
self.title: str = ""
|
|
1720
|
+
self.description: Optional[str] = None
|
|
1721
|
+
self.icon: Optional[str] = None
|
|
1722
|
+
self.optional: bool = False
|
|
1723
|
+
self.completed: Optional[bool] = None # Override state
|
|
1724
|
+
self.error: Optional[str] = None # Error message for this step
|
|
1725
|
+
self.children: List[QuantumNode] = [] # Step content
|
|
1726
|
+
self._init_layout_attrs()
|
|
1727
|
+
|
|
1728
|
+
def add_child(self, child: QuantumNode):
|
|
1729
|
+
self.children.append(child)
|
|
1730
|
+
|
|
1731
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1732
|
+
d = {"type": "ui_step", "title": self.title,
|
|
1733
|
+
"description": self.description, "icon": self.icon,
|
|
1734
|
+
"optional": self.optional, "completed": self.completed,
|
|
1735
|
+
"error": self.error, "children_count": len(self.children)}
|
|
1736
|
+
d.update(self._layout_to_dict())
|
|
1737
|
+
return d
|
|
1738
|
+
|
|
1739
|
+
def validate(self) -> List[str]:
|
|
1740
|
+
errors = self._validate_layout()
|
|
1741
|
+
if not self.title:
|
|
1742
|
+
errors.append("Step title is required")
|
|
1743
|
+
for child in self.children:
|
|
1744
|
+
if hasattr(child, 'validate'):
|
|
1745
|
+
errors.extend(child.validate())
|
|
1746
|
+
return errors
|
|
1747
|
+
|
|
1748
|
+
|
|
1749
|
+
# ============================================
|
|
1750
|
+
# CALENDAR / DATE PICKER
|
|
1751
|
+
# ============================================
|
|
1752
|
+
|
|
1753
|
+
class UICalendarNode(QuantumNode, UILayoutMixin):
|
|
1754
|
+
"""
|
|
1755
|
+
Represents <ui:calendar> - Calendar date picker.
|
|
1756
|
+
|
|
1757
|
+
Displays a calendar for date selection.
|
|
1758
|
+
|
|
1759
|
+
Attributes:
|
|
1760
|
+
mode: Selection mode (single, range, multiple)
|
|
1761
|
+
value: Selected date(s) in ISO format
|
|
1762
|
+
min_date: Minimum selectable date
|
|
1763
|
+
max_date: Maximum selectable date
|
|
1764
|
+
disabled_dates: List of disabled dates
|
|
1765
|
+
show_week_numbers: Show week numbers
|
|
1766
|
+
first_day_of_week: 0=Sunday, 1=Monday, etc.
|
|
1767
|
+
locale: Locale for month/day names
|
|
1768
|
+
|
|
1769
|
+
Example:
|
|
1770
|
+
<ui:calendar mode="single" bind="selectedDate" min-date="2024-01-01" />
|
|
1771
|
+
<ui:calendar mode="range" bind="dateRange" />
|
|
1772
|
+
"""
|
|
1773
|
+
|
|
1774
|
+
VALID_MODES = {'single', 'range', 'multiple'}
|
|
1775
|
+
|
|
1776
|
+
def __init__(self):
|
|
1777
|
+
self.mode: str = "single" # single, range, multiple
|
|
1778
|
+
self.value: Optional[str] = None # ISO date or comma-separated
|
|
1779
|
+
self.bind: Optional[str] = None # Binding for selected date(s)
|
|
1780
|
+
self.min_date: Optional[str] = None # Min date (ISO)
|
|
1781
|
+
self.max_date: Optional[str] = None # Max date (ISO)
|
|
1782
|
+
self.disabled_dates: Optional[str] = None # Comma-separated ISO dates
|
|
1783
|
+
self.disabled_days: Optional[str] = None # Days of week (0-6) to disable
|
|
1784
|
+
self.show_week_numbers: bool = False
|
|
1785
|
+
self.first_day_of_week: int = 0 # 0=Sunday, 1=Monday
|
|
1786
|
+
self.locale: str = "en"
|
|
1787
|
+
self.inline: bool = True # Show inline vs popup
|
|
1788
|
+
self.on_change: Optional[str] = None # Callback on date change
|
|
1789
|
+
self.on_month_change: Optional[str] = None # Callback on month navigation
|
|
1790
|
+
self._init_layout_attrs()
|
|
1791
|
+
|
|
1792
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1793
|
+
d = {"type": "ui_calendar", "mode": self.mode,
|
|
1794
|
+
"value": self.value, "bind": self.bind,
|
|
1795
|
+
"min_date": self.min_date, "max_date": self.max_date,
|
|
1796
|
+
"disabled_dates": self.disabled_dates,
|
|
1797
|
+
"disabled_days": self.disabled_days,
|
|
1798
|
+
"show_week_numbers": self.show_week_numbers,
|
|
1799
|
+
"first_day_of_week": self.first_day_of_week,
|
|
1800
|
+
"locale": self.locale, "inline": self.inline,
|
|
1801
|
+
"on_change": self.on_change, "on_month_change": self.on_month_change}
|
|
1802
|
+
d.update(self._layout_to_dict())
|
|
1803
|
+
return d
|
|
1804
|
+
|
|
1805
|
+
def validate(self) -> List[str]:
|
|
1806
|
+
errors = self._validate_layout()
|
|
1807
|
+
if self.mode not in self.VALID_MODES:
|
|
1808
|
+
errors.append(f"Invalid calendar mode: {self.mode}. Valid: {', '.join(self.VALID_MODES)}")
|
|
1809
|
+
if self.first_day_of_week not in range(7):
|
|
1810
|
+
errors.append(f"first_day_of_week must be 0-6, got {self.first_day_of_week}")
|
|
1811
|
+
return errors
|
|
1812
|
+
|
|
1813
|
+
|
|
1814
|
+
class UIDatePickerNode(QuantumNode, UILayoutMixin):
|
|
1815
|
+
"""
|
|
1816
|
+
Represents <ui:date-picker> - Date input with calendar popup.
|
|
1817
|
+
|
|
1818
|
+
Combines a text input with a calendar dropdown.
|
|
1819
|
+
|
|
1820
|
+
Attributes:
|
|
1821
|
+
value: Selected date in ISO format
|
|
1822
|
+
placeholder: Input placeholder text
|
|
1823
|
+
format: Display format (e.g., "MM/DD/YYYY")
|
|
1824
|
+
clearable: Show clear button
|
|
1825
|
+
|
|
1826
|
+
Example:
|
|
1827
|
+
<ui:date-picker bind="birthDate" placeholder="Select date" format="MM/DD/YYYY" />
|
|
1828
|
+
"""
|
|
1829
|
+
|
|
1830
|
+
def __init__(self):
|
|
1831
|
+
self.value: Optional[str] = None
|
|
1832
|
+
self.bind: Optional[str] = None
|
|
1833
|
+
self.placeholder: str = "Select date"
|
|
1834
|
+
self.format: str = "YYYY-MM-DD" # Display format
|
|
1835
|
+
self.min_date: Optional[str] = None
|
|
1836
|
+
self.max_date: Optional[str] = None
|
|
1837
|
+
self.clearable: bool = True
|
|
1838
|
+
self.disabled: bool = False
|
|
1839
|
+
self.required: bool = False
|
|
1840
|
+
self.on_change: Optional[str] = None
|
|
1841
|
+
self._init_layout_attrs()
|
|
1842
|
+
|
|
1843
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1844
|
+
d = {"type": "ui_date_picker", "value": self.value,
|
|
1845
|
+
"bind": self.bind, "placeholder": self.placeholder,
|
|
1846
|
+
"format": self.format, "min_date": self.min_date,
|
|
1847
|
+
"max_date": self.max_date, "clearable": self.clearable,
|
|
1848
|
+
"disabled": self.disabled, "required": self.required,
|
|
1849
|
+
"on_change": self.on_change}
|
|
1850
|
+
d.update(self._layout_to_dict())
|
|
1851
|
+
return d
|
|
1852
|
+
|
|
1853
|
+
def validate(self) -> List[str]:
|
|
1854
|
+
return self._validate_layout()
|