dars-framework 1.2.3__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 (118) hide show
  1. dars/__init__.py +0 -0
  2. dars/all.py +69 -0
  3. dars/cli/__init__.py +0 -0
  4. dars/cli/doctor/__init__.py +1 -0
  5. dars/cli/doctor/detect.py +154 -0
  6. dars/cli/doctor/doctor.py +176 -0
  7. dars/cli/doctor/installers.py +100 -0
  8. dars/cli/doctor/persist.py +62 -0
  9. dars/cli/doctor/preflight.py +33 -0
  10. dars/cli/doctor/ui.py +54 -0
  11. dars/cli/hot_reload.py +33 -0
  12. dars/cli/main.py +1107 -0
  13. dars/cli/preview.py +448 -0
  14. dars/cli/translations.py +531 -0
  15. dars/components/__init__.py +0 -0
  16. dars/components/advanced/__init__.py +8 -0
  17. dars/components/advanced/accordion.py +26 -0
  18. dars/components/advanced/card.py +33 -0
  19. dars/components/advanced/modal.py +45 -0
  20. dars/components/advanced/navbar.py +44 -0
  21. dars/components/advanced/table.py +25 -0
  22. dars/components/advanced/tabs.py +31 -0
  23. dars/components/basic/__init__.py +34 -0
  24. dars/components/basic/button.py +55 -0
  25. dars/components/basic/checkbox.py +35 -0
  26. dars/components/basic/container.py +29 -0
  27. dars/components/basic/datepicker.py +139 -0
  28. dars/components/basic/image.py +36 -0
  29. dars/components/basic/input.py +57 -0
  30. dars/components/basic/link.py +31 -0
  31. dars/components/basic/markdown.py +86 -0
  32. dars/components/basic/page.py +20 -0
  33. dars/components/basic/progressbar.py +18 -0
  34. dars/components/basic/radiobutton.py +35 -0
  35. dars/components/basic/select.py +82 -0
  36. dars/components/basic/slider.py +63 -0
  37. dars/components/basic/spinner.py +12 -0
  38. dars/components/basic/text.py +23 -0
  39. dars/components/basic/textarea.py +46 -0
  40. dars/components/basic/tooltip.py +19 -0
  41. dars/components/layout/__init__.py +0 -0
  42. dars/components/layout/anchor.py +13 -0
  43. dars/components/layout/flex.py +26 -0
  44. dars/components/layout/grid.py +45 -0
  45. dars/config.py +134 -0
  46. dars/core/__init__.py +0 -0
  47. dars/core/app.py +957 -0
  48. dars/core/component.py +284 -0
  49. dars/core/events.py +102 -0
  50. dars/core/js_bridge.py +99 -0
  51. dars/core/properties.py +127 -0
  52. dars/core/state.py +309 -0
  53. dars/dars_tests/apps_test/health_check.py +56 -0
  54. dars/dars_tests/run_tests.py +275 -0
  55. dars/dars_tests/tests/test_advanced_components.py +69 -0
  56. dars/dars_tests/tests/test_basic_components.py +88 -0
  57. dars/dars_tests/tests/test_core_and_cli.py +17 -0
  58. dars/dars_tests/tests/test_layout_components.py +58 -0
  59. dars/dars_tests/tests/test_version_check.py +21 -0
  60. dars/docs/__init__.py +0 -0
  61. dars/docs/app.md +290 -0
  62. dars/docs/cli.md +80 -0
  63. dars/docs/components.md +1679 -0
  64. dars/docs/custom_components.md +30 -0
  65. dars/docs/events.md +45 -0
  66. dars/docs/exporters.md +162 -0
  67. dars/docs/getting_started.md +79 -0
  68. dars/docs/index.md +18 -0
  69. dars/docs/scripts.md +593 -0
  70. dars/docs/state_management.md +57 -0
  71. dars/exporters/__init__.py +0 -0
  72. dars/exporters/base.py +96 -0
  73. dars/exporters/web/OLD/html_css_js_OLD4.py +1538 -0
  74. dars/exporters/web/OLD/html_css_js_old.py +1406 -0
  75. dars/exporters/web/OLD/html_css_js_old2.py +1406 -0
  76. dars/exporters/web/__init__.py +0 -0
  77. dars/exporters/web/html_css_js.py +2675 -0
  78. dars/exporters/web/vdom.py +251 -0
  79. dars/js_lib.py +206 -0
  80. dars/scripts/__init__.py +0 -0
  81. dars/scripts/dscript.py +26 -0
  82. dars/scripts/script.py +39 -0
  83. dars/security.py +195 -0
  84. dars/templates/__init__.py +0 -0
  85. dars/templates/__pycache__/__init__.cpython-311.pyc +0 -0
  86. dars/templates/examples/README.md +4 -0
  87. dars/templates/examples/__pycache__/dynamic_event_demo.cpython-311.pyc +0 -0
  88. dars/templates/examples/advanced/Modal_Demo/advanced_modal_demo.py +275 -0
  89. dars/templates/examples/advanced/SimpleDashboard/dashboard.py +437 -0
  90. dars/templates/examples/advanced/SimpleModermWeb/modern_web_app.py +452 -0
  91. dars/templates/examples/advanced/VariousComponents/all_components_demo.py +87 -0
  92. dars/templates/examples/advanced/__init__.py +0 -0
  93. dars/templates/examples/advanced/dState/state_mods_demo.py +68 -0
  94. dars/templates/examples/basic/Forms/form_components.py +516 -0
  95. dars/templates/examples/basic/Forms/simple_form.py +379 -0
  96. dars/templates/examples/basic/HelloWorld/hello_world.py +56 -0
  97. dars/templates/examples/basic/Layouts/flex_layout_responsive.py +13 -0
  98. dars/templates/examples/basic/Layouts/grid_layout_responsive.py +12 -0
  99. dars/templates/examples/basic/Layouts/layout_multipage_demo.py +23 -0
  100. dars/templates/examples/basic/Multipage/multipage_example.py +67 -0
  101. dars/templates/examples/basic/PWA/icon-192x192.png +0 -0
  102. dars/templates/examples/basic/PWA/icon-512x512.png +0 -0
  103. dars/templates/examples/basic/PWA/pwa_custom_icons.py +33 -0
  104. dars/templates/examples/basic/__init__.py +0 -0
  105. dars/templates/examples/demo/__pycache__/complete_app.cpython-311.pyc +0 -0
  106. dars/templates/examples/demo/complete_app.py +21 -0
  107. dars/templates/examples/markdown/MarkdownTemplate/README.md +159 -0
  108. dars/templates/examples/markdown/MarkdownTemplate/markdown_template.py +21 -0
  109. dars/templates/examples/markdown/MarkdownTemplate/other_docs.md +1 -0
  110. dars/templates/examples/markdown/__init__.py +0 -0
  111. dars/templates/html/__init__.py +0 -0
  112. dars/version.py +2 -0
  113. dars_framework-1.2.3.dist-info/METADATA +15 -0
  114. dars_framework-1.2.3.dist-info/RECORD +118 -0
  115. dars_framework-1.2.3.dist-info/WHEEL +5 -0
  116. dars_framework-1.2.3.dist-info/entry_points.txt +2 -0
  117. dars_framework-1.2.3.dist-info/licenses/LICENSE +21 -0
  118. dars_framework-1.2.3.dist-info/top_level.txt +1 -0
dars/core/component.py ADDED
@@ -0,0 +1,284 @@
1
+ from typing import Dict, Any, List, Optional, Callable, Union, Type
2
+ from abc import ABC, abstractmethod
3
+ from dars.core.events import EventTypes
4
+
5
+ class ComponentQuery:
6
+ def __init__(self, components: List['Component']):
7
+ self.components = components
8
+
9
+ def find(self,
10
+ id: Optional[str] = None,
11
+ class_name: Optional[str] = None,
12
+ type: Optional[Union[Type['Component'], str]] = None,
13
+ predicate: Optional[Callable[['Component'], bool]] = None) -> 'ComponentQuery':
14
+
15
+ """Searches for components within the currently selected components."""
16
+ results: List['Component'] = []
17
+
18
+ def match_component(comp: Component) -> bool:
19
+ if id is not None and comp.id != id:
20
+ return False
21
+ if class_name is not None and comp.class_name != class_name:
22
+ return False
23
+ if type is not None:
24
+ if isinstance(type, str):
25
+ if comp.__class__.__name__ != type:
26
+ return False
27
+ elif not isinstance(comp, type):
28
+ return False
29
+ if predicate is not None and not predicate(comp):
30
+ return False
31
+ return True
32
+
33
+ # Buscar en los hijos de todos los componentes actuales
34
+ for component in self.components:
35
+ for child in component.children:
36
+ if match_component(child):
37
+ results.append(child)
38
+ # Buscar recursivamente en los hijos del hijo
39
+ for descendant in ComponentQuery([child]).find().get():
40
+ if match_component(descendant):
41
+ results.append(descendant)
42
+
43
+ return ComponentQuery(results)
44
+
45
+ def attr(self, **attrs) -> 'ComponentQuery':
46
+ """Modifies the attributes of all found components."""
47
+ for component in self.components:
48
+ for key, value in attrs.items():
49
+ # Manejo especial para atributos comunes
50
+ if key == 'style':
51
+ component.style.update(value)
52
+ continue
53
+ elif key == 'class_name':
54
+ component.class_name = value
55
+ continue
56
+ elif key == 'events':
57
+ component.events.update(value)
58
+ continue
59
+
60
+ # Intenta establecer el atributo directamente si existe
61
+ if hasattr(component, key):
62
+ setattr(component, key, value)
63
+ # Si no existe como atributo directo, guárdalo en props
64
+ else:
65
+ component.props[key] = value
66
+ return self
67
+
68
+ def get(self) -> List['Component']:
69
+ """Returns the list of found components."""
70
+ return self.components
71
+
72
+ def first(self) -> Optional['Component']:
73
+ """Returns the first found component, or None if there is none."""
74
+ return self.components[0] if self.components else None
75
+
76
+ class Component(ABC):
77
+ def __init__(self, **props):
78
+ self.props = props
79
+ self.children: List[Component] = []
80
+ self.parent: Optional[Component] = None
81
+ self.id: Optional[str] = props.get('id')
82
+ self.class_name: str = props.get("class_name", self.__class__.__name__)
83
+ self.style: Dict[str, Any] = props.get('style', {})
84
+ self.events: Dict[str, Callable] = {}
85
+ # Stable identity hint for children reconciliation in VDOM (optional)
86
+ self.key: Optional[str] = props.get('key')
87
+
88
+ # Generic on_* props -> register as events on any component
89
+ if props:
90
+ on_map = {
91
+ 'on_click': EventTypes.CLICK,
92
+ 'on_double_click': EventTypes.DOUBLE_CLICK,
93
+ 'on_mouse_down': EventTypes.MOUSE_DOWN,
94
+ 'on_mouse_up': EventTypes.MOUSE_UP,
95
+ 'on_mouse_enter': EventTypes.MOUSE_ENTER,
96
+ 'on_mouse_leave': EventTypes.MOUSE_LEAVE,
97
+ 'on_mouse_move': EventTypes.MOUSE_MOVE,
98
+ 'on_key_down': EventTypes.KEY_DOWN,
99
+ 'on_key_up': EventTypes.KEY_UP,
100
+ 'on_key_press': EventTypes.KEY_PRESS,
101
+ 'on_change': EventTypes.CHANGE,
102
+ 'on_input': EventTypes.INPUT,
103
+ 'on_submit': EventTypes.SUBMIT,
104
+ 'on_focus': EventTypes.FOCUS,
105
+ 'on_blur': EventTypes.BLUR,
106
+ 'on_load': EventTypes.LOAD,
107
+ 'on_error': EventTypes.ERROR,
108
+ 'on_resize': EventTypes.RESIZE,
109
+ }
110
+ for k, v in list(props.items()):
111
+ if k in on_map and v is not None:
112
+ handler = v
113
+ # Normalize handler to Script-like if possible
114
+ try:
115
+ from dars.scripts.script import Script
116
+ if not isinstance(handler, Script):
117
+ if callable(handler):
118
+ from dars.scripts.dscript import dScript
119
+ handler = dScript(handler.__code__)
120
+ except Exception:
121
+ # Best-effort: keep as-is (string or callable)
122
+ pass
123
+ self.set_event(on_map[k], handler)
124
+
125
+ def add_child(self, child: 'Component'):
126
+ if isinstance(child, type) and issubclass(child, Component):
127
+ raise TypeError(f"The class {child.__name__} was passed instead of an instance. You should use {child.__name__}(...).")
128
+ child.parent = self
129
+ self.children.append(child)
130
+
131
+ def set_event(self, event_name: str, handler: Callable):
132
+ self.events[event_name] = handler
133
+
134
+ def find(self,
135
+ id: Optional[str] = None,
136
+ class_name: Optional[str] = None,
137
+ type: Optional[Union[Type['Component'], str]] = None,
138
+ predicate: Optional[Callable[['Component'], bool]] = None) -> ComponentQuery:
139
+ """Searches for components that match the specified criteria.
140
+
141
+ Args:
142
+ id: Search by component ID
143
+ class_name: Search by CSS class name
144
+ type: Search by component type (class or class name)
145
+ predicate: Custom filter function that takes a component and returns bool
146
+
147
+ Returns:
148
+ ComponentQuery that allows chaining operations and modifying attributes
149
+ """
150
+
151
+ results: List[Component] = []
152
+
153
+ def match_component(comp: Component) -> bool:
154
+ if id is not None and comp.id != id:
155
+ return False
156
+ if class_name is not None and comp.class_name != class_name:
157
+ return False
158
+ if type is not None:
159
+ if isinstance(type, str):
160
+ if comp.__class__.__name__ != type:
161
+ return False
162
+ elif not isinstance(comp, type):
163
+ return False
164
+ if predicate is not None and not predicate(comp):
165
+ return False
166
+ return True
167
+
168
+ def search_recursive(component: Component):
169
+ if match_component(component):
170
+ results.append(component)
171
+ for child in component.children:
172
+ search_recursive(child)
173
+
174
+ search_recursive(self)
175
+ return ComponentQuery(results)
176
+
177
+ def attr(self, **attrs) -> Union['Component', dict]:
178
+ """If kwargs are provided, sets attributes on the component (chained setter).
179
+ If no kwargs are provided, returns a dict with all editable component attributes (getter).
180
+ Example:
181
+ c.attr(id='new', style={'color': 'red'})
182
+ c.attr()['id'] # getter
183
+ """
184
+
185
+ if attrs:
186
+ if 'defer' in attrs:
187
+ try:
188
+ d = attrs.pop('defer')
189
+ if d:
190
+ return DeferredAttr(self, attrs)
191
+ except Exception:
192
+ pass
193
+ for key, value in attrs.items():
194
+ if key == 'style':
195
+ self.style.update(value)
196
+ continue
197
+ elif key == 'class_name':
198
+ self.class_name = value
199
+ continue
200
+ elif key == 'events':
201
+ self.events.update(value)
202
+ continue
203
+ # Allow setting on_* event properties via attr()
204
+ if key.startswith('on_') and value is not None:
205
+ try:
206
+ event_name = {
207
+ 'on_click': EventTypes.CLICK,
208
+ 'on_double_click': EventTypes.DOUBLE_CLICK,
209
+ 'on_mouse_down': EventTypes.MOUSE_DOWN,
210
+ 'on_mouse_up': EventTypes.MOUSE_UP,
211
+ 'on_mouse_enter': EventTypes.MOUSE_ENTER,
212
+ 'on_mouse_leave': EventTypes.MOUSE_LEAVE,
213
+ 'on_mouse_move': EventTypes.MOUSE_MOVE,
214
+ 'on_key_down': EventTypes.KEY_DOWN,
215
+ 'on_key_up': EventTypes.KEY_UP,
216
+ 'on_key_press': EventTypes.KEY_PRESS,
217
+ 'on_change': EventTypes.CHANGE,
218
+ 'on_input': EventTypes.INPUT,
219
+ 'on_submit': EventTypes.SUBMIT,
220
+ 'on_focus': EventTypes.FOCUS,
221
+ 'on_blur': EventTypes.BLUR,
222
+ 'on_load': EventTypes.LOAD,
223
+ 'on_error': EventTypes.ERROR,
224
+ 'on_resize': EventTypes.RESIZE,
225
+ }.get(key)
226
+ if event_name:
227
+ handler = value
228
+ from dars.scripts.script import Script
229
+ if not isinstance(handler, Script):
230
+ if callable(handler):
231
+ from dars.scripts.dscript import dScript
232
+ handler = dScript(handler.__code__)
233
+ self.set_event(event_name, handler)
234
+ continue
235
+ except Exception:
236
+ # If any error, fall back to setting as a prop
237
+ pass
238
+ if hasattr(self, key):
239
+ setattr(self, key, value)
240
+ else:
241
+ self.props[key] = value
242
+ return self
243
+ # Getter: devolver todos los atributos editables
244
+ d = dict(self.props)
245
+ d['id'] = self.id
246
+ d['class_name'] = self.class_name
247
+ d['style'] = self.style
248
+ d['events'] = self.events
249
+ return d
250
+
251
+ def mod(self, **attrs):
252
+ return DeferredAttr(self, attrs)
253
+
254
+
255
+ class DeferredAttr:
256
+ def __init__(self, component: 'Component', attrs: Dict[str, Any]):
257
+ self.component = component
258
+ self.attrs = attrs or {}
259
+
260
+ def clone_with(self) -> 'Component':
261
+ try:
262
+ import copy
263
+ clone = copy.copy(self.component)
264
+ except Exception:
265
+ clone = self.component
266
+ try:
267
+ if hasattr(clone, 'attr') and callable(getattr(clone, 'attr')):
268
+ clone.attr(**self.attrs)
269
+ except Exception:
270
+ pass
271
+ return clone
272
+
273
+ def render_children(self, exporter: 'Exporter') -> str:
274
+ """Render all children of the component using the exporter."""
275
+ children_html = ""
276
+ for child in self.children:
277
+ children_html += exporter.render_component(child)
278
+ return children_html
279
+
280
+ @abstractmethod
281
+ def render(self, exporter: 'Exporter') -> str:
282
+ pass
283
+
284
+
dars/core/events.py ADDED
@@ -0,0 +1,102 @@
1
+ from typing import Callable, Dict, Any, Optional
2
+ from abc import ABC, abstractmethod
3
+
4
+ class EventHandler:
5
+ """Event handler for components"""
6
+
7
+ def __init__(self, handler: Callable, event_type: str):
8
+ self.handler = handler
9
+ self.event_type = event_type
10
+ self.id = f"event_{id(self)}"
11
+
12
+ def __call__(self, *args, **kwargs):
13
+ return self.handler(*args, **kwargs)
14
+
15
+ class EventManager:
16
+ """Event manager for the application"""
17
+
18
+ def __init__(self):
19
+ self.handlers: Dict[str, EventHandler] = {}
20
+ self.component_events: Dict[str, Dict[str, EventHandler]] = {}
21
+
22
+ def register_event(self, component_id: str, event_type: str, handler: Callable) -> str:
23
+ """Registers an event for a component"""
24
+ event_handler = EventHandler(handler, event_type)
25
+
26
+ if component_id not in self.component_events:
27
+ self.component_events[component_id] = {}
28
+
29
+ self.component_events[component_id][event_type] = event_handler
30
+ self.handlers[event_handler.id] = event_handler
31
+
32
+ return event_handler.id
33
+
34
+ def get_event_handler(self, handler_id: str) -> Optional[EventHandler]:
35
+ """Gets all events of a component by ID"""
36
+
37
+ return self.handlers.get(handler_id)
38
+
39
+ def get_component_events(self, component_id: str) -> Dict[str, EventHandler]:
40
+ """Gets all events of a component"""
41
+ return self.component_events.get(component_id, {})
42
+
43
+ def remove_event(self, component_id: str, event_type: str):
44
+ """Removes an event from a component"""
45
+ if component_id in self.component_events:
46
+ if event_type in self.component_events[component_id]:
47
+ handler = self.component_events[component_id][event_type]
48
+ del self.handlers[handler.id]
49
+ del self.component_events[component_id][event_type]
50
+
51
+ class EventEmitter(ABC):
52
+ """Base class for components that can emit events"""
53
+
54
+ def __init__(self):
55
+ self.event_manager = EventManager()
56
+
57
+ def on(self, event_type: str, handler: Callable) -> str:
58
+ """Registers an event handler"""
59
+ component_id = getattr(self, 'id', str(id(self)))
60
+ return self.event_manager.register_event(component_id, event_type, handler)
61
+
62
+ def off(self, event_type: str):
63
+ """Removes an event handler"""
64
+ component_id = getattr(self, 'id', str(id(self)))
65
+ self.event_manager.remove_event(component_id, event_type)
66
+
67
+ @abstractmethod
68
+ def emit(self, event_type: str, *args, **kwargs):
69
+ """Emits an event"""
70
+ pass
71
+
72
+ # Tipos de eventos estándar
73
+ class EventTypes:
74
+ # Eventos de mouse
75
+ CLICK = "click"
76
+ DOUBLE_CLICK = "dblclick"
77
+ MOUSE_DOWN = "mousedown"
78
+ MOUSE_UP = "mouseup"
79
+ MOUSE_ENTER = "mouseenter"
80
+ MOUSE_LEAVE = "mouseleave"
81
+ MOUSE_MOVE = "mousemove"
82
+
83
+ # Eventos de teclado
84
+ KEY_DOWN = "keydown"
85
+ KEY_UP = "keyup"
86
+ KEY_PRESS = "keypress"
87
+
88
+ # Eventos de formulario
89
+ CHANGE = "change"
90
+ INPUT = "input"
91
+ SUBMIT = "submit"
92
+ FOCUS = "focus"
93
+ BLUR = "blur"
94
+
95
+ # Eventos de carga
96
+ LOAD = "load"
97
+ ERROR = "error"
98
+ RESIZE = "resize"
99
+
100
+ # Eventos personalizados
101
+ CUSTOM = "custom"
102
+
dars/core/js_bridge.py ADDED
@@ -0,0 +1,99 @@
1
+ import os
2
+ import subprocess
3
+ import tempfile
4
+ from typing import List, Optional, Tuple
5
+
6
+
7
+ def _run(cmd: List[str], cwd: Optional[str] = None, live: bool = False) -> Tuple[int, str, str]:
8
+ try:
9
+ if live:
10
+ p = subprocess.run(cmd, cwd=cwd)
11
+ return (p.returncode or 0, "", "")
12
+ p = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True)
13
+ return p.returncode, (p.stdout or ""), (p.stderr or "")
14
+ except Exception as e:
15
+ return 1, "", str(e)
16
+
17
+
18
+ def which(name: str) -> Optional[str]:
19
+ cmd = ["where", name] if os.name == 'nt' else ["which", name]
20
+ code, out, _ = _run(cmd)
21
+ if code == 0 and out:
22
+ return out.strip().splitlines()[0]
23
+ return None
24
+
25
+
26
+ def has_node() -> bool:
27
+ return which("node") is not None
28
+
29
+
30
+ def has_bun() -> bool:
31
+ return which("bun") is not None
32
+
33
+
34
+ def bun_add(packages: List[str], dev: bool = True, cwd: Optional[str] = None) -> bool:
35
+ if not has_bun():
36
+ return False
37
+ args = ["bun", "add"]
38
+ if dev:
39
+ args.append("-d")
40
+ args.extend(packages)
41
+ code, _, _ = _run(args, cwd=cwd, live=True)
42
+ return code == 0
43
+
44
+
45
+ def node_run(code: str) -> Tuple[int, str, str]:
46
+ if not has_node():
47
+ return 1, "", "node not found"
48
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".mjs", mode="w", encoding="utf-8") as tf:
49
+ tf.write(code)
50
+ temp = tf.name
51
+ try:
52
+ return _run(["node", temp])
53
+ finally:
54
+ try:
55
+ os.remove(temp)
56
+ except Exception:
57
+ pass
58
+
59
+
60
+ def esbuild_available() -> bool:
61
+ # Prefer bun esbuild
62
+ if has_bun():
63
+ # bun x esbuild --version
64
+ code, out, _ = _run(["bun", "x", "esbuild", "--version"])
65
+ if code == 0:
66
+ return True
67
+ # Node npx fallback
68
+ if has_node():
69
+ code, out, _ = _run(["npx", "--yes", "esbuild", "--version"]) # --yes to avoid prompt
70
+ if code == 0:
71
+ return True
72
+ return False
73
+
74
+
75
+ def ensure_esbuild(cwd: Optional[str] = None) -> bool:
76
+ if esbuild_available():
77
+ return True
78
+ # Try to add via bun (dev dep)
79
+ if has_bun():
80
+ ok = bun_add(["esbuild"], dev=True, cwd=cwd)
81
+ return ok and esbuild_available()
82
+ return False
83
+
84
+
85
+ def esbuild_minify_js(src_path: str, out_path: Optional[str] = None) -> bool:
86
+ if not esbuild_available() and not ensure_esbuild(os.path.dirname(src_path)):
87
+ return False
88
+ out = out_path or src_path
89
+ if has_bun():
90
+ code, _, _ = _run(["bun", "x", "esbuild", src_path, "--minify", "--legal-comments=none", "--platform=browser", "--format=esm", f"--outfile={out}"])
91
+ return code == 0
92
+ # Node fallback via npx
93
+ code, _, _ = _run(["npx", "--yes", "esbuild", src_path, "--minify", "--legal-comments=none", "--platform=browser", "--format=esm", f"--outfile={out}"])
94
+ return code == 0
95
+
96
+
97
+ def esbuild_minify_css(src_path: str, out_path: Optional[str] = None) -> bool:
98
+ # esbuild can minify CSS if input is CSS
99
+ return esbuild_minify_js(src_path, out_path)
@@ -0,0 +1,127 @@
1
+ from typing import Union, Optional, Dict, Any, Callable
2
+ from dataclasses import dataclass
3
+
4
+ @dataclass
5
+ class StyleProps:
6
+ """Style properties for UI components"""
7
+ # Dimensiones
8
+ width: Optional[Union[str, int]] = None
9
+ height: Optional[Union[str, int]] = None
10
+ min_width: Optional[Union[str, int]] = None
11
+ min_height: Optional[Union[str, int]] = None
12
+ max_width: Optional[Union[str, int]] = None
13
+ max_height: Optional[Union[str, int]] = None
14
+
15
+ # Espaciado
16
+ margin: Optional[Union[str, int]] = None
17
+ margin_top: Optional[Union[str, int]] = None
18
+ margin_right: Optional[Union[str, int]] = None
19
+ margin_bottom: Optional[Union[str, int]] = None
20
+ margin_left: Optional[Union[str, int]] = None
21
+ padding: Optional[Union[str, int]] = None
22
+ padding_top: Optional[Union[str, int]] = None
23
+ padding_right: Optional[Union[str, int]] = None
24
+ padding_bottom: Optional[Union[str, int]] = None
25
+ padding_left: Optional[Union[str, int]] = None
26
+
27
+ # Colores
28
+ background_color: Optional[str] = None
29
+ color: Optional[str] = None
30
+ border_color: Optional[str] = None
31
+
32
+ # Tipografía
33
+ font_size: Optional[Union[str, int]] = None
34
+ font_family: Optional[str] = None
35
+ font_weight: Optional[Union[str, int]] = None
36
+ font_style: Optional[str] = None
37
+ text_align: Optional[str] = None
38
+ text_decoration: Optional[str] = None
39
+ line_height: Optional[Union[str, int]] = None
40
+
41
+ # Bordes
42
+ border: Optional[str] = None
43
+ border_width: Optional[Union[str, int]] = None
44
+ border_style: Optional[str] = None
45
+ border_radius: Optional[Union[str, int]] = None
46
+
47
+ # Layout
48
+ display: Optional[str] = None
49
+ position: Optional[str] = None
50
+ top: Optional[Union[str, int]] = None
51
+ right: Optional[Union[str, int]] = None
52
+ bottom: Optional[Union[str, int]] = None
53
+ left: Optional[Union[str, int]] = None
54
+ z_index: Optional[int] = None
55
+
56
+ # Flexbox
57
+ flex_direction: Optional[str] = None
58
+ flex_wrap: Optional[str] = None
59
+ justify_content: Optional[str] = None
60
+ align_items: Optional[str] = None
61
+ align_content: Optional[str] = None
62
+ flex: Optional[Union[str, int]] = None
63
+ flex_grow: Optional[int] = None
64
+ flex_shrink: Optional[int] = None
65
+ flex_basis: Optional[Union[str, int]] = None
66
+
67
+ # Grid
68
+ grid_template_columns: Optional[str] = None
69
+ grid_template_rows: Optional[str] = None
70
+ grid_gap: Optional[Union[str, int]] = None
71
+ grid_column: Optional[str] = None
72
+ grid_row: Optional[str] = None
73
+
74
+ # Efectos
75
+ opacity: Optional[float] = None
76
+ box_shadow: Optional[str] = None
77
+ transform: Optional[str] = None
78
+ transition: Optional[str] = None
79
+
80
+ # Overflow
81
+ overflow: Optional[str] = None
82
+ overflow_x: Optional[str] = None
83
+ overflow_y: Optional[str] = None
84
+
85
+ def to_dict(self) -> Dict[str, Any]:
86
+ """Convierte las propiedades a un diccionario, excluyendo valores None"""
87
+ result = {}
88
+ for key, value in self.__dict__.items():
89
+ if value is not None:
90
+ # Convertir snake_case a kebab-case para CSS
91
+ css_key = key.replace('_', '-')
92
+ result[css_key] = value
93
+ return result
94
+
95
+ @dataclass
96
+ class EventProps:
97
+ """Properties of events for UI components"""
98
+ on_click: Optional[Callable] = None
99
+ on_double_click: Optional[Callable] = None
100
+ on_mouse_enter: Optional[Callable] = None
101
+ on_mouse_leave: Optional[Callable] = None
102
+ on_mouse_down: Optional[Callable] = None
103
+ on_mouse_up: Optional[Callable] = None
104
+ on_key_down: Optional[Callable] = None
105
+ on_key_up: Optional[Callable] = None
106
+ on_focus: Optional[Callable] = None
107
+ on_blur: Optional[Callable] = None
108
+ on_change: Optional[Callable] = None
109
+ on_input: Optional[Callable] = None
110
+ on_submit: Optional[Callable] = None
111
+ on_load: Optional[Callable] = None
112
+ on_error: Optional[Callable] = None
113
+
114
+ def normalize_style_value(value: Union[str, int]) -> str:
115
+ """Normalizes a style value to a CSS string"""
116
+ if isinstance(value, int):
117
+ return f"{value}px"
118
+ return str(value)
119
+
120
+ def merge_styles(*styles: Dict[str, Any]) -> Dict[str, Any]:
121
+ """Combines multiple style dictionaries"""
122
+ result = {}
123
+ for style in styles:
124
+ if style:
125
+ result.update(style)
126
+ return result
127
+