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
@@ -0,0 +1,2675 @@
1
+ from dars.exporters.base import Exporter
2
+ from dars.scripts.dscript import dScript
3
+ from dars.core.app import App
4
+ from dars.core.component import Component
5
+ from dars.components.basic.text import Text
6
+ from dars.components.basic.button import Button
7
+ from dars.components.basic.input import Input
8
+ from dars.components.basic.container import Container
9
+ from dars.components.basic.image import Image
10
+ from dars.components.basic.link import Link
11
+ from dars.components.basic.textarea import Textarea
12
+ from dars.components.basic.checkbox import Checkbox
13
+ from dars.components.basic.radiobutton import RadioButton
14
+ from dars.components.basic.select import Select
15
+ from dars.components.basic.slider import Slider
16
+ from dars.components.basic.datepicker import DatePicker
17
+ from dars.components.advanced.card import Card
18
+ from dars.components.advanced.modal import Modal
19
+ from dars.components.advanced.navbar import Navbar
20
+ from dars.components.advanced.table import Table
21
+ from dars.components.advanced.tabs import Tabs
22
+ from dars.components.advanced.accordion import Accordion
23
+ from dars.components.basic.progressbar import ProgressBar
24
+ from dars.components.basic.spinner import Spinner
25
+ from dars.components.basic.tooltip import Tooltip
26
+ from dars.components.basic.markdown import Markdown
27
+ from typing import Dict, Any
28
+ import os
29
+ from bs4 import BeautifulSoup
30
+ from dars.exporters.web.vdom import VDomBuilder
31
+ from dars.config import load_config, resolve_paths, copy_public_dir
32
+
33
+ class HTMLCSSJSExporter(Exporter):
34
+ """Exportador para HTML, CSS y JavaScript"""
35
+
36
+ def get_platform(self) -> str:
37
+ return "html"
38
+
39
+ def export(self, app: App, output_path: str, bundle: bool = False) -> bool:
40
+ """Exporta la aplicación a HTML/CSS/JS (soporta multipágina)."""
41
+ try:
42
+ # Initialize obfuscation context for this export
43
+ # Keep original IDs to avoid breaking CSS/anchors. We still obfuscate types and events.
44
+ self._hash_ids = False
45
+ self._id_hash_map = {}
46
+ self._type_obfuscation = bool(bundle)
47
+ self._type_map = {}
48
+ self._type_seq = 0
49
+ self.create_output_directory(output_path)
50
+
51
+ # --- Copiar recursos adicionales desde la carpeta del proyecto ---
52
+ import inspect, shutil
53
+ import sys
54
+ # Determinar la raíz del proyecto desde el archivo fuente de la app
55
+ app_source = getattr(app, '__source__', None)
56
+ if app_source is None and hasattr(app, 'source_file'):
57
+ app_source = app.source_file
58
+ if app_source is None:
59
+ # Fallback: usar root del componente, pero esto no es robusto
60
+ project_root = os.getcwd()
61
+ else:
62
+ project_root = os.path.dirname(os.path.abspath(app_source))
63
+
64
+ # --- Escribir librería de reactividad (dars.min.js) embebida ---
65
+ try:
66
+ lib_dir = os.path.join(output_path, 'lib')
67
+ os.makedirs(lib_dir, exist_ok=True)
68
+ dest_js = os.path.join(lib_dir, 'dars.min.js')
69
+ from dars.js_lib import DARS_MIN_JS
70
+ with open(dest_js, 'w', encoding='utf-8') as f:
71
+ f.write(DARS_MIN_JS)
72
+ except Exception:
73
+ pass
74
+
75
+ # --- Cargar configuración si existe y copiar public/assets ---
76
+ try:
77
+ cfg, cfg_found = load_config(project_root)
78
+ except Exception:
79
+ cfg, cfg_found = ({}, False)
80
+ try:
81
+ resolved = resolve_paths(cfg if cfg else {}, project_root)
82
+ except Exception:
83
+ resolved = {"public_abs": None, "include": [], "exclude": []}
84
+
85
+ # Copiar public/assets completos al output (tanto en preview como en bundle)
86
+ try:
87
+ public_abs = resolved.get("public_abs")
88
+ include = cfg.get("include", []) if cfg else []
89
+ exclude = cfg.get("exclude", []) if cfg else []
90
+ if not public_abs:
91
+ # autodetect simple si no viene en config
92
+ cand_public = os.path.join(project_root, "public")
93
+ cand_assets = os.path.join(project_root, "assets")
94
+ if os.path.isdir(cand_public):
95
+ public_abs = cand_public
96
+ elif os.path.isdir(cand_assets):
97
+ public_abs = cand_assets
98
+ if public_abs and os.path.isdir(public_abs):
99
+ copy_public_dir(public_abs, output_path, include=include, exclude=exclude)
100
+ except Exception:
101
+ # Mejor esfuerzo, no romper export
102
+ pass
103
+
104
+ os.makedirs(output_path, exist_ok=True)
105
+
106
+ # Copiar solo recursos explícitos usados por la app
107
+ # 1) Favicon
108
+ favicon = getattr(app, 'favicon', None)
109
+ if favicon and os.path.isfile(os.path.join(project_root, favicon)):
110
+ shutil.copy2(os.path.join(project_root, favicon), os.path.join(output_path, os.path.basename(favicon)))
111
+ # 2) Iconos PWA
112
+ icons = getattr(app, 'icons', None)
113
+ if icons:
114
+ icons_dir = os.path.join(output_path, 'icons')
115
+ os.makedirs(icons_dir, exist_ok=True)
116
+ for icon in icons:
117
+ src = icon.get('src') if isinstance(icon, dict) else icon
118
+ if src and os.path.isfile(os.path.join(project_root, src)):
119
+ shutil.copy2(os.path.join(project_root, src), os.path.join(icons_dir, os.path.basename(src)))
120
+ # 3) Service Worker
121
+ sw_path = getattr(app, 'service_worker_path', None)
122
+ if sw_path and os.path.isfile(os.path.join(project_root, sw_path)):
123
+ shutil.copy2(os.path.join(project_root, sw_path), os.path.join(output_path, 'sw.js'))
124
+ # 4) Archivos estáticos definidos por el usuario
125
+ static_files = getattr(app, 'static_files', [])
126
+ for static in static_files:
127
+ src = static.get('src') if isinstance(static, dict) else static
128
+ if src and os.path.isfile(os.path.join(project_root, src)):
129
+ shutil.copy2(os.path.join(project_root, src), os.path.join(output_path, os.path.basename(src)))
130
+ # NOTA: No copiar ejecutables ni nada fuera del proyecto
131
+
132
+ base_css_content = self.generate_base_css() # Nuevo método para estilos base
133
+ custom_css_content = self.generate_custom_css(app) # Nuevo método para estilos personalizados
134
+
135
+ self.write_file(os.path.join(output_path, "runtime_css.css"), base_css_content)
136
+ self.write_file(os.path.join(output_path, "styles.css"), custom_css_content)
137
+
138
+ # Multipágina: exportar un HTML, CSS y JS por cada página registrada
139
+ if hasattr(app, "is_multipage") and app.is_multipage():
140
+ import copy
141
+ index_page = None
142
+ if hasattr(app, 'get_index_page'):
143
+ index_page = app.get_index_page()
144
+
145
+ # Exportar cada página
146
+ for slug, page in app.pages.items():
147
+ page_app = copy.copy(app)
148
+ page_app.root = page.root
149
+ if page.title:
150
+ page_app.title = page.title
151
+ if page.meta:
152
+ for k, v in page.meta.items():
153
+ setattr(page_app, k, v)
154
+
155
+ # Asegurar que root sea Container si es lista
156
+ from dars.components.basic.container import Container
157
+ if isinstance(page_app.root, list):
158
+ page_app.root = Container(children=page_app.root)
159
+
160
+ # Generar runtime específico para esta página
161
+ runtime_js = self.generate_javascript(page_app, page.root)
162
+ runtime_name = f"runtime_dars_{slug}.js" if slug != "index" else "runtime_dars.js"
163
+ self.write_file(os.path.join(output_path, runtime_name), runtime_js)
164
+ # Generar VDOM Tree JS (externo)
165
+ vdom_name = None
166
+ try:
167
+ vdom_dict = VDomBuilder(id_provider=self.get_component_id).build(page_app.root)
168
+ if bundle:
169
+ vdom_dict = self._obfuscate_vdom(vdom_dict)
170
+ import json
171
+ vdom_js = "window.__DARS_VDOM__ = " + json.dumps(vdom_dict, ensure_ascii=False, separators=(",", ":")) + ";\n"
172
+ except Exception:
173
+ vdom_js = "window.__DARS_VDOM__ = { };\n"
174
+ vdom_name = f"vdom_tree_{slug}.js" if slug != "index" else "vdom_tree.js"
175
+ self.write_file(os.path.join(output_path, vdom_name), vdom_js)
176
+ # Fase 2: escribir snapshot/version por página (solo en dev, no bundle)
177
+ if not bundle:
178
+ try:
179
+ vdom_json = self.generate_vdom_snapshot(page_app.root)
180
+ except Exception:
181
+ vdom_json = '{}'
182
+ if slug != 'index':
183
+ snapshot_name = f"snapshot_{slug}.json"
184
+ version_name = f"version_{slug}.txt"
185
+ else:
186
+ snapshot_name = "snapshot.json"
187
+ version_name = "version.txt"
188
+ self.write_file(os.path.join(output_path, snapshot_name), vdom_json)
189
+ try:
190
+ import time
191
+ version_val = str(int(time.time()*1000))
192
+ except Exception:
193
+ version_val = "1"
194
+ self.write_file(os.path.join(output_path, version_name), version_val)
195
+
196
+ # Generar scripts específicos de esta página
197
+ page_scripts = []
198
+
199
+ # Scripts globales de la app
200
+ page_scripts.extend(getattr(app, 'scripts', []))
201
+
202
+ # Scripts específicos de esta página
203
+ if hasattr(page, 'scripts'):
204
+ page_scripts.extend(page.scripts)
205
+
206
+ # Scripts de componentes dentro de la página
207
+ if hasattr(page_app.root, 'get_scripts'):
208
+ page_scripts.extend(page_app.root.get_scripts())
209
+
210
+ # Generar script.js específico para esta página
211
+ # Preparar y copiar scripts: combinados + externos
212
+ combined_js, external_srcs, combined_is_module = self._prepare_page_scripts(page_scripts, output_path, project_root)
213
+
214
+ if index_page is not None and page is index_page:
215
+ # Página index
216
+ self.write_file(os.path.join(output_path, "script.js"), combined_js)
217
+ html_content = self.generate_html(page_app, css_file="styles.css",
218
+ script_file="script.js",
219
+ runtime_file="runtime_dars.js",
220
+ extra_script_srcs=external_srcs, bundle=bundle, vdom_script=vdom_name,
221
+ script_is_module=combined_is_module)
222
+ filename = "index.html"
223
+ else:
224
+ # Otras páginas
225
+ script_name = f"script_{slug}.js"
226
+ self.write_file(os.path.join(output_path, script_name), combined_js)
227
+ html_content = self.generate_html(page_app, css_file="styles.css",
228
+ script_file=script_name,
229
+ runtime_file=runtime_name,
230
+ extra_script_srcs=external_srcs, bundle=bundle, vdom_script=vdom_name,
231
+ script_is_module=combined_is_module)
232
+ filename = f"{slug}.html"
233
+
234
+ # Mejorar formato HTML si es posible
235
+ try:
236
+ soup = BeautifulSoup(html_content, "html.parser")
237
+ html_content = soup.prettify()
238
+ except ImportError:
239
+ pass
240
+
241
+ self.write_file(os.path.join(output_path, filename), html_content)
242
+ else:
243
+ # Single-page clásico (mantener comportamiento existente)
244
+ runtime_js = self.generate_javascript(app, app.root)
245
+ self.write_file(os.path.join(output_path, "runtime_dars.js"), runtime_js)
246
+
247
+ user_scripts = list(getattr(app, 'scripts', []))
248
+ combined_js, external_srcs, combined_is_module = self._prepare_page_scripts(user_scripts, output_path, project_root)
249
+ self.write_file(os.path.join(output_path, "script.js"), combined_js)
250
+ # Generar VDOM Tree JS (externo)
251
+ vdom_name = None
252
+ try:
253
+ vdom_dict = VDomBuilder(id_provider=self.get_component_id).build(app.root)
254
+ if bundle:
255
+ vdom_dict = self._obfuscate_vdom(vdom_dict)
256
+ import json
257
+ vdom_js = "window.__DARS_VDOM__ = " + json.dumps(vdom_dict, ensure_ascii=False, separators=(",", ":")) + ";\n"
258
+ except Exception:
259
+ vdom_js = "window.__DARS_VDOM__ = { };\n"
260
+ vdom_name = "vdom_tree.js"
261
+ self.write_file(os.path.join(output_path, vdom_name), vdom_js)
262
+
263
+ html_content = self.generate_html(app, css_file="styles.css",
264
+ script_file="script.js",
265
+ runtime_file="runtime_dars.js",
266
+ extra_script_srcs=external_srcs, bundle=bundle, vdom_script=vdom_name,
267
+ script_is_module=combined_is_module)
268
+ soup = BeautifulSoup(html_content, "html.parser")
269
+ html_content = soup.prettify()
270
+
271
+ self.write_file(os.path.join(output_path, "index.html"), html_content)
272
+ # Fase 2: snapshot/version para single-page (solo en dev, no bundle)
273
+ if not bundle:
274
+ try:
275
+ vdom_json = self.generate_vdom_snapshot(app.root)
276
+ except Exception:
277
+ vdom_json = '{}'
278
+ self.write_file(os.path.join(output_path, "snapshot.json"), vdom_json)
279
+ try:
280
+ import time
281
+ version_val = str(int(time.time()*1000))
282
+ except Exception:
283
+ version_val = "1"
284
+ self.write_file(os.path.join(output_path, "version.txt"), version_val)
285
+
286
+ # Generar archivos PWA si está habilitado
287
+ if getattr(app, 'pwa_enabled', False):
288
+ self._generate_pwa_files(app, output_path)
289
+
290
+ # Limpiar bootstrap de estado para evitar duplicados en siguientes exports
291
+ try:
292
+ from dars.core.state import STATE_BOOTSTRAP
293
+ if isinstance(STATE_BOOTSTRAP, list):
294
+ STATE_BOOTSTRAP.clear()
295
+ except Exception:
296
+ pass
297
+
298
+ return True
299
+ except Exception as e:
300
+ print(f"Error al exportar: {e}")
301
+ return False
302
+
303
+
304
+ def _generate_pwa_files(self, app: 'App', output_path: str) -> None:
305
+ """Genera manifest.json, iconos y service worker para PWA"""
306
+ import json, os
307
+ # Manifest
308
+ self._generate_manifest_json(app, output_path)
309
+ # Iconos por defecto (placeholder, puedes mejorar esto)
310
+ self._generate_default_icons(output_path)
311
+ # Service worker
312
+ sw_path = getattr(app, 'service_worker_path', None)
313
+ sw_enabled = getattr(app, 'service_worker_enabled', True)
314
+ if sw_enabled:
315
+ if sw_path:
316
+ # Copiar el personalizado
317
+ import shutil
318
+ shutil.copy(sw_path, os.path.join(output_path, 'sw.js'))
319
+ else:
320
+ self._generate_basic_service_worker(output_path)
321
+
322
+ def _generate_manifest_json(self, app: 'App', output_path: str) -> None:
323
+ import json, os, shutil
324
+ manifest = {
325
+ "name": getattr(app, 'pwa_name', getattr(app, 'title', 'Dars App')),
326
+ "short_name": getattr(app, 'pwa_short_name', 'Dars'),
327
+ "description": getattr(app, 'description', 'Aplicación web progresiva creada con Dars'),
328
+ "start_url": ".",
329
+ "display": getattr(app, 'pwa_display', 'standalone'),
330
+ "background_color": getattr(app, 'background_color', '#ffffff'),
331
+ "theme_color": getattr(app, 'theme_color', '#4a90e2'),
332
+ "orientation": getattr(app, 'pwa_orientation', 'portrait')
333
+ }
334
+ icons = self._get_icons_manifest(app, output_path)
335
+ if icons is not None:
336
+ manifest["icons"] = icons
337
+ manifest_path = os.path.join(output_path, "manifest.json")
338
+ with open(manifest_path, 'w', encoding='utf-8') as f:
339
+ json.dump(manifest, f, indent=2)
340
+
341
+ def _get_icons_manifest(self, app: 'App', output_path: str) -> list:
342
+ import os, shutil
343
+ user_icons = getattr(app, 'icons', None)
344
+ if user_icons is not None:
345
+ # Si el usuario define icons=[] explícito, no ponemos icons
346
+ if isinstance(user_icons, list) and len(user_icons) == 0:
347
+ return None
348
+
349
+ # Obtener project_root para rutas relativas
350
+ app_source = getattr(app, '__source__', None)
351
+ if app_source is None and hasattr(app, 'source_file'):
352
+ app_source = app.source_file
353
+ if app_source is None:
354
+ project_root = os.getcwd()
355
+ else:
356
+ project_root = os.path.dirname(os.path.abspath(app_source))
357
+
358
+ # Si el usuario define iconos personalizados
359
+ icons_manifest = []
360
+ icons_dir = os.path.join(output_path, "icons")
361
+ os.makedirs(icons_dir, exist_ok=True)
362
+
363
+ for icon in user_icons:
364
+ if isinstance(icon, dict):
365
+ src = icon.get("src")
366
+ if src:
367
+ # Resolver ruta relativa a project_root
368
+ src_path = os.path.join(project_root, src) if not os.path.isabs(src) else src
369
+ if os.path.isfile(src_path):
370
+ # Copiamos el icono al output
371
+ dest_path = os.path.join(icons_dir, os.path.basename(src))
372
+ shutil.copy(src_path, dest_path)
373
+ icon_copy = icon.copy() # No modificar el original
374
+ icon_copy["src"] = f"/icons/{os.path.basename(src)}"
375
+ icons_manifest.append(icon_copy)
376
+ else:
377
+ # Si no existe, usar la ruta tal cual (podría ser URL)
378
+ icons_manifest.append(icon)
379
+ else:
380
+ icons_manifest.append(icon)
381
+ elif isinstance(icon, str):
382
+ # Si solo es una ruta, la copiamos y generamos el dict
383
+ src_path = os.path.join(project_root, icon) if not os.path.isabs(icon) else icon
384
+ if os.path.isfile(src_path):
385
+ dest_path = os.path.join(icons_dir, os.path.basename(icon))
386
+ shutil.copy(src_path, dest_path)
387
+ icons_manifest.append({
388
+ "src": f"icons/{os.path.basename(icon)}",
389
+ "sizes": "192x192",
390
+ "type": "image/png",
391
+ "purpose": "any maskable"
392
+ })
393
+ else:
394
+ # Si no existe, asumir que es URL
395
+ icons_manifest.append({
396
+ "src": icon,
397
+ "sizes": "192x192",
398
+ "type": "image/png"
399
+ })
400
+ return icons_manifest if icons_manifest else None
401
+
402
+ # Si no hay icons definidos, poner por defecto
403
+ return [
404
+ {
405
+ "src": "icons/icon-192x192.png",
406
+ "sizes": "192x192",
407
+ "type": "image/png",
408
+ "purpose": "any maskable"
409
+ },
410
+ {
411
+ "src": "icons/icon-512x512.png",
412
+ "sizes": "512x512",
413
+ "type": "image/png"
414
+ }
415
+ ]
416
+
417
+ def _generate_default_icons(self, output_path: str) -> None:
418
+ import os, shutil
419
+ # Ruta de los iconos PWA por defecto incluidos en el framework
420
+ base_dir = os.path.dirname(os.path.abspath(__file__))
421
+ default_icons_dir = os.path.join(base_dir, "icons", "pwa")
422
+ icons_dir = os.path.join(output_path, "icons")
423
+ os.makedirs(icons_dir, exist_ok=True)
424
+ # Copiar icon-192x192.png y icon-512x512.png si existen
425
+ for fname in ["icon-192x192.png", "icon-512x512.png"]:
426
+ src = os.path.join(default_icons_dir, fname)
427
+ dst = os.path.join(icons_dir, fname)
428
+ if os.path.isfile(src):
429
+ shutil.copy(src, dst)
430
+
431
+
432
+ def _generate_basic_service_worker(self, output_path: str) -> None:
433
+ sw_content = '''// Service Worker básico para Dars PWA
434
+ const CACHE_NAME = 'dars-pwa-cache-v1';
435
+ const urlsToCache = [
436
+ '/',
437
+ '/index.html',
438
+ '/styles.css',
439
+ '/script.js'
440
+ ];
441
+
442
+ self.addEventListener('install', event => {
443
+ event.waitUntil(
444
+ caches.open(CACHE_NAME)
445
+ .then(cache => {
446
+ console.log('Cache abierto');
447
+ return cache.addAll(urlsToCache);
448
+ })
449
+ );
450
+ });
451
+
452
+ self.addEventListener('fetch', event => {
453
+ event.respondWith(
454
+ caches.match(event.request)
455
+ .then(response => {
456
+ if (response) {
457
+ return response;
458
+ }
459
+ return fetch(event.request);
460
+ })
461
+ );
462
+ });
463
+ '''
464
+ sw_path = os.path.join(output_path, "sw.js")
465
+ with open(sw_path, 'w', encoding='utf-8') as f:
466
+ f.write(sw_content)
467
+
468
+ def _prepare_page_scripts(self, scripts, output_path: str, project_root: str):
469
+ """
470
+ Toma la lista mixta `scripts` y:
471
+ - concatena todo el JS inline en un único string (combined_js)
472
+ - copia los file scripts al output_path y devuelve la lista de src relativos (external_srcs)
473
+ Compatibilidades:
474
+ - objetos con get_code()
475
+ - dicts {'type':'inline','code':...} o {'type':'file','path':...}
476
+ - objetos con attribute 'path' o 'src' (se interpretan como file script)
477
+ - strings -> treated as inline code
478
+ """
479
+ combined_lines = []
480
+ external_srcs = [] # list of tuples (src, is_module)
481
+ combined_is_module = False
482
+ import shutil
483
+
484
+ for script in scripts or []:
485
+ # Instancia con get_code()
486
+ try:
487
+ if hasattr(script, 'get_code'):
488
+ code = script.get_code()
489
+ if code:
490
+ combined_lines.append(f"// Script: {getattr(script, '__class__', type(script)).__name__}\n{code.strip()}\n")
491
+ try:
492
+ if getattr(script, 'module', False):
493
+ combined_is_module = True
494
+ except Exception:
495
+ pass
496
+ continue
497
+ except Exception:
498
+ pass
499
+
500
+ # Dict fallback
501
+ if isinstance(script, dict):
502
+ stype = script.get('type', '').lower()
503
+ is_module = bool(script.get('module'))
504
+ if stype == 'inline' or ('code' in script and not stype):
505
+ code = script.get('code') or script.get('value') or ''
506
+ if code:
507
+ combined_lines.append(f"// Inline dict script\n{code.strip()}\n")
508
+ if is_module:
509
+ combined_is_module = True
510
+ continue
511
+ if stype == 'file' or 'path' in script:
512
+ path = script.get('path') or script.get('src') or script.get('value')
513
+ if path:
514
+ # Resolver ruta relativa a project_root
515
+ src_path = os.path.join(project_root, path) if not os.path.isabs(path) else path
516
+ if os.path.isfile(src_path):
517
+ dest_name = os.path.basename(src_path)
518
+ dest_path = os.path.join(output_path, dest_name)
519
+ try:
520
+ shutil.copy2(src_path, dest_path)
521
+ external_srcs.append((dest_name, is_module))
522
+ except Exception:
523
+ pass
524
+ else:
525
+ # si no existe en disco, asumimos que es una URL o ya accesible: usar tal cual
526
+ external_srcs.append((path, is_module))
527
+ continue
528
+ # Otros dicts con code
529
+ if 'code' in script:
530
+ code = script.get('code')
531
+ if code:
532
+ combined_lines.append(f"// Inline dict script\n{code.strip()}\n")
533
+ if is_module:
534
+ combined_is_module = True
535
+ continue
536
+
537
+ # String -> inline code
538
+ if isinstance(script, str):
539
+ combined_lines.append(f"// Inline string script\n{script.strip()}\n")
540
+ continue
541
+
542
+ # Objetos con .path o .src (file scripts)
543
+ path_attr = None
544
+ for candidate in ('path', 'src', 'file'):
545
+ if hasattr(script, candidate):
546
+ try:
547
+ path_attr = getattr(script, candidate)
548
+ break
549
+ except Exception:
550
+ continue
551
+ if path_attr:
552
+ path = path_attr
553
+ src_path = os.path.join(project_root, path) if not os.path.isabs(path) else path
554
+ if os.path.isfile(src_path):
555
+ dest_name = os.path.basename(src_path)
556
+ dest_path = os.path.join(output_path, dest_name)
557
+ try:
558
+ shutil.copy2(src_path, dest_path)
559
+ is_module = False
560
+ try:
561
+ if getattr(script, 'module', False):
562
+ is_module = True
563
+ except Exception:
564
+ pass
565
+ external_srcs.append((dest_name, is_module))
566
+ except Exception:
567
+ pass
568
+ else:
569
+ is_module = False
570
+ try:
571
+ if getattr(script, 'module', False):
572
+ is_module = True
573
+ except Exception:
574
+ pass
575
+ external_srcs.append((path, is_module))
576
+ continue
577
+
578
+ # Si no sabemos qué es, intentar str() y añadir como inline (fallback)
579
+ try:
580
+ s = str(script)
581
+ if s:
582
+ combined_lines.append(f"// Fallback script: {type(script).__name__}\n{s}\n")
583
+ except Exception:
584
+ pass
585
+
586
+ combined_js = "// Scripts específicos de esta página (combinados)\n" + "\n".join(combined_lines)
587
+
588
+ return combined_js, external_srcs, combined_is_module
589
+ def _generate_combined_script_js(self, scripts):
590
+ """Deprecated internal wrapper: usa _prepare_page_scripts sin copiar archivos.
591
+ Conserva compatibilidad devolviendo solo el combined JS (sin external refs)."""
592
+ combined_js, external_srcs = self._prepare_page_scripts(scripts, output_path=os.getcwd(), project_root=os.getcwd())
593
+ return combined_js
594
+
595
+ def generate_html(self, app: App, css_file: str = "styles.css",
596
+ script_file: str = "script.js", runtime_file: str = "runtime_dars.js", extra_script_srcs: list = None, bundle: bool = False, vdom_script: str = "vdom_tree.js", script_is_module: bool = False) -> str:
597
+ """Genera el contenido HTML con todas las propiedades de la aplicación"""
598
+ body_content = ""
599
+ from dars.components.basic.container import Container
600
+ root_component = app.root
601
+ # Protección: si root es lista, envolver en Container correctamente
602
+ if isinstance(root_component, list):
603
+ root_component = Container(*root_component)
604
+ if root_component:
605
+ body_content = self.render_component(root_component)
606
+
607
+ # VDOM snapshot ahora se sirve desde un archivo externo (vdom_script)
608
+
609
+ # Generar meta tags
610
+ meta_tags_html = self._generate_meta_tags(app)
611
+
612
+ # Generar links (favicon, manifest, etc.)
613
+ links_html = self._generate_links(app)
614
+
615
+ # Generar Open Graph tags
616
+ og_tags_html = self._generate_open_graph_tags(app)
617
+
618
+ # Generar Twitter Card tags
619
+ twitter_tags_html = self._generate_twitter_tags(app)
620
+
621
+
622
+ # Construir string de scripts externos (extra_script_srcs)
623
+ extra_scripts_html = ""
624
+ if extra_script_srcs:
625
+ for item in extra_script_srcs:
626
+ # item can be string (backward compat) or tuple (src, is_module)
627
+ if isinstance(item, tuple):
628
+ src, is_module = item
629
+ else:
630
+ src, is_module = item, False
631
+ type_attr = ' type="module"' if is_module else ''
632
+ extra_scripts_html += f' <script src="{src}"{type_attr}></script>\n'
633
+ # Incluir dars.min.js (ESM) antes de runtime/script
634
+ dars_lib_tag = '<script type="module" src="lib/dars.min.js" defer data-dars-lib></script>'
635
+
636
+ # State bootstrap: emit JSON (+ obfuscation in bundle) + module to register states
637
+ bootstrap_json_tag = ""
638
+ bootstrap_init_tag = ""
639
+ try:
640
+ from dars.core.state import STATE_BOOTSTRAP
641
+ if STATE_BOOTSTRAP:
642
+ import json as _json
643
+ from copy import deepcopy as _deepcopy
644
+ try:
645
+ from dars.scripts.script import Script as _Script
646
+ except Exception:
647
+ _Script = None
648
+
649
+ def _ser(v):
650
+ try:
651
+ if _Script and isinstance(v, _Script):
652
+ return {"code": v.get_code()}
653
+ if isinstance(v, dict):
654
+ return {k: _ser(val) for k, val in v.items()}
655
+ if isinstance(v, list):
656
+ return [_ser(x) for x in v]
657
+ return v
658
+ except Exception:
659
+ return v
660
+
661
+ _clean = _ser(_deepcopy(STATE_BOOTSTRAP))
662
+ bootstrap_json = _json.dumps(_clean, ensure_ascii=False)
663
+ if bundle:
664
+ # Obfuscate: base64-encode the bootstrap JSON
665
+ import base64 as _b64
666
+ _b64data = _b64.b64encode(bootstrap_json.encode('utf-8')).decode('ascii')
667
+ bootstrap_json_tag = f'<script type="application/octet-stream" id="dars-state-bootstrap-b64">{_b64data}</script>'
668
+ bootstrap_init_tag = (
669
+ "<script type=\"module\">\n"
670
+ "(async () => {\n"
671
+ " if (window.__DARS_STATE_BOOTSTRAPPED__) return;\n"
672
+ " const el = document.getElementById('dars-state-bootstrap-b64');\n"
673
+ " if (!el) { window.__DARS_STATE_BOOTSTRAPPED__ = true; return; }\n"
674
+ " let arr = [];\n"
675
+ " try {\n"
676
+ " const b64 = el.textContent || '';\n"
677
+ " let json = '';\n"
678
+ " if (typeof atob === 'function') json = atob(b64);\n"
679
+ " else if (typeof Buffer !== 'undefined') json = Buffer.from(b64, 'base64').toString('utf8');\n"
680
+ " arr = JSON.parse(json||'[]');\n"
681
+ " } catch(_) { arr = []; }\n"
682
+ " try {\n"
683
+ " const m = await import('./lib/dars.min.js');\n"
684
+ " const reg = m.registerState || (m.default && m.default.registerState);\n"
685
+ " if (typeof reg === 'function') { arr.forEach(s => reg(s.name, s)); }\n"
686
+ " } catch (e) {\n"
687
+ " const D = window.Dars; if (D && typeof D.registerState==='function') { arr.forEach(s => D.registerState(s.name, s)); }\n"
688
+ " }\n"
689
+ " window.__DARS_STATE_BOOTSTRAPPED__ = true;\n"
690
+ "})();\n"
691
+ "</script>"
692
+ )
693
+ else:
694
+ # Dev: keep readable JSON
695
+ bootstrap_json_tag = f'<script type="application/json" id="dars-state-bootstrap">{bootstrap_json}</script>'
696
+ bootstrap_init_tag = (
697
+ "<script type=\"module\">\n"
698
+ "(async () => {\n"
699
+ " if (window.__DARS_STATE_BOOTSTRAPPED__) return;\n"
700
+ " const el = document.getElementById('dars-state-bootstrap');\n"
701
+ " if (!el) { window.__DARS_STATE_BOOTSTRAPPED__ = true; return; }\n"
702
+ " const arr = JSON.parse(el.textContent||'[]');\n"
703
+ " try {\n"
704
+ " const m = await import('./lib/dars.min.js');\n"
705
+ " const reg = m.registerState || (m.default && m.default.registerState);\n"
706
+ " if (typeof reg === 'function') { arr.forEach(s => reg(s.name, s)); }\n"
707
+ " } catch (e) {\n"
708
+ " const D = window.Dars; if (D && typeof D.registerState==='function') { arr.forEach(s => D.registerState(s.name, s)); }\n"
709
+ " }\n"
710
+ " window.__DARS_STATE_BOOTSTRAPPED__ = true;\n"
711
+ "})();\n"
712
+ "</script>"
713
+ )
714
+ except Exception:
715
+ pass
716
+
717
+ # Derivar nombres para hot-reload incremental (opcional)
718
+ def _derive_snapshot_and_version(runtime_name: str):
719
+ if runtime_name == 'runtime_dars.js':
720
+ return ('snapshot.json', 'version.txt')
721
+ if runtime_name.startswith('runtime_dars_') and runtime_name.endswith('.js'):
722
+ slug = runtime_name[len('runtime_dars_'):-3]
723
+ return (f'snapshot_{slug}.json', f'version_{slug}.txt')
724
+ return ('snapshot.json', 'version.txt')
725
+
726
+ snapshot_name, version_name = _derive_snapshot_and_version(runtime_file)
727
+ # Incluir variables de hot-reload solo en modo dev (no bundle)
728
+ version_vars_html = ""
729
+ if not bundle:
730
+ version_vars_html = f"<script>window.__DARS_SNAPSHOT_URL = '{snapshot_name}'; window.__DARS_VERSION_URL = '{version_name}';</script>"
731
+
732
+ vdom_script_tag = ''
733
+ if vdom_script:
734
+ vdom_script_tag = f'<script src="{vdom_script}"></script>'
735
+
736
+ html_template = f"""<!DOCTYPE html>
737
+ <html lang="{app.language}">
738
+ <head>
739
+ <meta charset="{app.config.get('charset', 'UTF-8')}">
740
+ {meta_tags_html}
741
+ <title>{app.title}</title>
742
+ {links_html}
743
+ {og_tags_html}
744
+ {twitter_tags_html}
745
+ <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css\">\n <link rel=\"stylesheet\" href=\"runtime_css.css\">\n <link rel=\"stylesheet\" href=\"{css_file}\">
746
+ </head>
747
+ <body>
748
+ {body_content}
749
+ {vdom_script_tag}
750
+ {version_vars_html}
751
+ {bootstrap_json_tag}
752
+ {dars_lib_tag}
753
+ {bootstrap_init_tag}
754
+ <script src=\"{runtime_file}\"{' type=\"module\"' if script_is_module else ''} defer></script>\n{extra_scripts_html} <script src=\"{script_file}\"{' type=\"module\"' if script_is_module else ''}></script>
755
+ </body>
756
+ </html>"""
757
+
758
+ return html_template
759
+
760
+ def _obfuscate_vdom(self, vnode: dict) -> dict:
761
+ """Produce a minimal VDOM structure keeping events but hiding code.
762
+ - Keeps: type, id, key, class, text, children, events
763
+ - Events: { evName: {t:'i', b:'<base64>'} }
764
+ - Strips: style, props, any other keys
765
+ Recurses through children.
766
+ """
767
+ if not isinstance(vnode, dict):
768
+ return vnode
769
+ import base64
770
+ kept = {}
771
+ # type (obfuscated when enabled)
772
+ t = vnode.get('type')
773
+ if t is not None:
774
+ kept['type'] = self._obf_type(t) if getattr(self, '_type_obfuscation', False) else t
775
+ # id and key
776
+ if 'id' in vnode and vnode['id']:
777
+ kept['id'] = self._hash_id(str(vnode['id'])) if getattr(self, '_hash_ids', False) else vnode['id']
778
+ if 'key' in vnode and vnode['key']:
779
+ kept['key'] = str(vnode['key'])
780
+ # class: drop in obfuscated VDOM to avoid leaking names
781
+ if not getattr(self, '_type_obfuscation', False):
782
+ if 'class' in vnode:
783
+ kept['class'] = vnode.get('class')
784
+ # text retained (non-sensitive content remains visible by choice)
785
+ if 'text' in vnode:
786
+ kept['text'] = vnode.get('text')
787
+ # Obfuscate events
788
+ evs = vnode.get('events') or None
789
+ if isinstance(evs, dict) and evs:
790
+ obf = {}
791
+ for ev, spec in evs.items():
792
+ try:
793
+ code = None
794
+ if isinstance(spec, dict):
795
+ # existing shapes: {type:'inline', code:'...'} or short-forms
796
+ code = spec.get('code') or spec.get('value')
797
+ elif isinstance(spec, str):
798
+ code = spec
799
+ if code:
800
+ b64 = base64.b64encode(code.encode('utf-8')).decode('ascii')
801
+ obf[ev] = {'t': 'i', 'b': b64}
802
+ except Exception:
803
+ # if anything fails, skip this event
804
+ pass
805
+ kept['events'] = obf if obf else None
806
+ # Recurse children
807
+ ch = vnode.get('children') or []
808
+ if ch:
809
+ kept['children'] = [self._obfuscate_vdom(c) for c in ch]
810
+ return kept
811
+
812
+ def _obf_type(self, name: str) -> str:
813
+ m = getattr(self, '_type_map', None)
814
+ if m is None:
815
+ self._type_map = {}
816
+ self._type_seq = 0
817
+ m = self._type_map
818
+ if name in m:
819
+ return m[name]
820
+ self._type_seq += 1
821
+ obf = f"T{self._type_seq}"
822
+ m[name] = obf
823
+ return obf
824
+
825
+ def generate_custom_css(self, app: App) -> str:
826
+ """Genera solo los estilos personalizados de la aplicación"""
827
+ css_content = ""
828
+
829
+ # Agregar estilos globales de la aplicación definidos por el usuario
830
+ for selector, styles in app.global_styles.items():
831
+ css_content += f"{selector} {{\n"
832
+ css_content += f" {self.render_styles(styles)}\n"
833
+ css_content += "}\n\n"
834
+
835
+ # Agregar contenido de archivos CSS globales
836
+ for file_path in app.global_style_files:
837
+ try:
838
+ with open(file_path, "r", encoding="utf-8") as f:
839
+ css_content += f.read() + "\n\n"
840
+ except Exception as e:
841
+ print(f"[Dars] Warning: could not read CSS file '{file_path}': {e}")
842
+
843
+ return css_content
844
+
845
+
846
+ def _generate_meta_tags(self, app: App) -> str:
847
+ """Genera todos los meta tags de la aplicación"""
848
+ meta_tags = app.get_meta_tags()
849
+ meta_html = []
850
+
851
+ for name, content in meta_tags.items():
852
+ if content:
853
+ meta_html.append(f' <meta name="{name}" content="{content}">')
854
+
855
+ # Añadir canonical URL si está configurado
856
+ if app.canonical_url:
857
+ meta_html.append(f' <link rel="canonical" href="{app.canonical_url}">')
858
+
859
+ return '\n'.join(meta_html)
860
+
861
+ def _generate_links(self, app: App) -> str:
862
+ """Genera los enlaces en el head del HTML"""
863
+ links = []
864
+
865
+ # Favicon
866
+ if hasattr(app, 'favicon'):
867
+ links.append(f'<link rel="icon" href="{app.favicon}" type="image/x-icon">')
868
+
869
+ # Manifest
870
+ if getattr(app, 'pwa_enabled', False):
871
+ links.append('<link rel="manifest" href="manifest.json">')
872
+ # Registrar service worker si está habilitado
873
+ if getattr(app, 'service_worker_enabled', True):
874
+ links.append("""
875
+ <script>
876
+ if ('serviceWorker' in navigator) {
877
+ window.addEventListener('load', () => {
878
+ navigator.serviceWorker.register('sw.js')
879
+ .then(registration => {
880
+ console.log('ServiceWorker registration successful');
881
+ })
882
+ .catch(err => {
883
+ console.log('ServiceWorker registration failed: ', err);
884
+ });
885
+ });
886
+ }
887
+ </script>
888
+ """)
889
+ return "\n ".join(links)
890
+ def generate_custom_css(self, app: App) -> str:
891
+ """Genera solo los estilos personalizados de la aplicación"""
892
+ css_content = ""
893
+
894
+ # Agregar estilos globales de la aplicación definidos por el usuario
895
+ for selector, styles in app.global_styles.items():
896
+ css_content += f"{selector} {{\n"
897
+ css_content += f" {self.render_styles(styles)}\n"
898
+ css_content += "}\n\n"
899
+
900
+ # Agregar contenido de archivos CSS globales
901
+ for file_path in app.global_style_files:
902
+ try:
903
+ with open(file_path, "r", encoding="utf-8") as f:
904
+ css_content += f.read() + "\n\n"
905
+ except Exception as e:
906
+ print(f"[Dars] Warning: could not read CSS file '{file_path}': {e}")
907
+
908
+ return css_content
909
+
910
+ def _generate_open_graph_tags(self, app: App) -> str:
911
+ """Genera todos los tags Open Graph para redes sociales"""
912
+ og_tags = app.get_open_graph_tags()
913
+ og_html = []
914
+
915
+ for property_name, content in og_tags.items():
916
+ if content:
917
+ og_html.append(f' <meta property="{property_name}" content="{content}">')
918
+
919
+ return '\n'.join(og_html)
920
+
921
+ def _generate_twitter_tags(self, app: App) -> str:
922
+ """Genera todos los tags de Twitter Card"""
923
+ twitter_tags = app.get_twitter_tags()
924
+ twitter_html = []
925
+
926
+ for name, content in twitter_tags.items():
927
+ if content:
928
+ twitter_html.append(f' <meta name="{name}" content="{content}">')
929
+
930
+ return '\n'.join(twitter_html)
931
+
932
+ def generate_base_css(self) -> str:
933
+ """Genera el contenido CSS base"""
934
+ return """/* Estilos base de Dars */
935
+ * {
936
+ box-sizing: border-box;
937
+ }
938
+
939
+ body {
940
+ margin: 0;
941
+ padding: 0;
942
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
943
+ }
944
+
945
+ /* Estilos de componentes Dars */
946
+ .dars-container {
947
+ display: block;
948
+ }
949
+
950
+ .dars-text {
951
+ display: inline-block;
952
+ }
953
+
954
+ .dars-button {
955
+ display: inline-block;
956
+ padding: 8px 16px;
957
+ border: 1px solid #ccc;
958
+ background-color: #f8f9fa;
959
+ color: #333;
960
+ cursor: pointer;
961
+ border-radius: 4px;
962
+ font-size: 14px;
963
+ }
964
+
965
+ .dars-button:hover {
966
+ background-color: #e9ecef;
967
+ }
968
+
969
+ .dars-button:disabled {
970
+ opacity: 0.6;
971
+ cursor: not-allowed;
972
+ }
973
+
974
+ .dars-input {
975
+ display: inline-block;
976
+ padding: 8px 12px;
977
+ border: 1px solid #ccc;
978
+ border-radius: 4px;
979
+ font-size: 14px;
980
+ }
981
+
982
+ .dars-input:focus {
983
+ outline: none;
984
+ border-color: #007bff;
985
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
986
+ }
987
+
988
+ .dars-image {
989
+ max-width: 100%;
990
+ height: auto;
991
+ }
992
+
993
+ .dars-link {
994
+ color: #007bff;
995
+ text-decoration: none;
996
+ }
997
+
998
+ .dars-link:hover {
999
+ text-decoration: underline;
1000
+ }
1001
+
1002
+ .dars-textarea {
1003
+ width: 100%;
1004
+ padding: 8px 12px;
1005
+ border: 1px solid #ccc;
1006
+ border-radius: 4px;
1007
+ font-size: 14px;
1008
+ }
1009
+
1010
+ .dars-textarea:focus {
1011
+ outline: none;
1012
+ border-color: #007bff;
1013
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
1014
+ }
1015
+
1016
+ .dars-card {
1017
+ background-color: white;
1018
+ border-radius: 8px;
1019
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
1020
+ padding: 20px;
1021
+ margin-bottom: 20px;
1022
+ }
1023
+
1024
+ .dars-card h2 {
1025
+ margin-top: 0;
1026
+ margin-bottom: 15px;
1027
+ font-size: 24px;
1028
+ color: #333;
1029
+ }
1030
+
1031
+ /* Table */
1032
+ .dars-table {
1033
+ width: 100%;
1034
+ border-collapse: collapse;
1035
+ margin-bottom: 20px;
1036
+ background: white;
1037
+ }
1038
+ .dars-table th, .dars-table td {
1039
+ border: 1px solid #ddd;
1040
+ padding: 8px 12px;
1041
+ text-align: left;
1042
+ }
1043
+ .dars-table th {
1044
+ background: #f5f5f5;
1045
+ font-weight: bold;
1046
+ }
1047
+ .dars-table tr:nth-child(even) {
1048
+ background: #fafbfc;
1049
+ }
1050
+
1051
+ /* Tabs */
1052
+ .dars-tabs {
1053
+ margin-bottom: 20px;
1054
+ }
1055
+ .dars-tabs-header {
1056
+ display: flex;
1057
+ border-bottom: 2px solid #eee;
1058
+ margin-bottom: 10px;
1059
+ }
1060
+ .dars-tab {
1061
+ background: none;
1062
+ border: none;
1063
+ padding: 10px 20px;
1064
+ cursor: pointer;
1065
+ font-size: 16px;
1066
+ color: #555;
1067
+ border-bottom: 2px solid transparent;
1068
+ transition: border 0.2s, color 0.2s;
1069
+ }
1070
+ .dars-tab-active {
1071
+ color: #007bff;
1072
+ border-bottom: 2px solid #007bff;
1073
+ font-weight: bold;
1074
+ }
1075
+ .dars-tab-panel {
1076
+ display: none;
1077
+ padding: 16px 0;
1078
+ }
1079
+ .dars-tab-panel-active {
1080
+ display: block;
1081
+ }
1082
+
1083
+ /* Accordion */
1084
+ .dars-accordion {
1085
+ border-radius: 8px;
1086
+ overflow: hidden;
1087
+ background: #fff;
1088
+ margin-bottom: 20px;
1089
+ box-shadow: 0 1px 3px rgba(0,0,0,0.06);
1090
+ }
1091
+ .dars-accordion-section {
1092
+ border-bottom: 1px solid #eee;
1093
+ }
1094
+ .dars-accordion-title {
1095
+ padding: 14px 20px;
1096
+ background: #f7f7f7;
1097
+ cursor: pointer;
1098
+ font-weight: 500;
1099
+ transition: background 0.2s;
1100
+ }
1101
+ .dars-accordion-section.dars-accordion-open .dars-accordion-title {
1102
+ background: #e9ecef;
1103
+ }
1104
+ .dars-accordion-content {
1105
+ display: none;
1106
+ padding: 16px 20px;
1107
+ background: #fafbfc;
1108
+ }
1109
+ .dars-accordion-section.dars-accordion-open .dars-accordion-content {
1110
+ display: block;
1111
+ }
1112
+
1113
+ /* ProgressBar */
1114
+ .dars-progressbar {
1115
+ width: 100%;
1116
+ background: #e9ecef;
1117
+ border-radius: 8px;
1118
+ overflow: hidden;
1119
+ height: 20px;
1120
+ margin-bottom: 20px;
1121
+ }
1122
+ .dars-progressbar-bar {
1123
+ height: 100%;
1124
+ background: linear-gradient(90deg, #007bff, #4a90e2);
1125
+ transition: width 0.3s;
1126
+ }
1127
+
1128
+ /* Spinner */
1129
+ .dars-spinner {
1130
+ border: 4px solid #e9ecef;
1131
+ border-top: 4px solid #007bff;
1132
+ border-radius: 50%;
1133
+ width: 36px;
1134
+ height: 36px;
1135
+ animation: dars-spin 1s linear infinite;
1136
+ margin: 10px auto;
1137
+ }
1138
+ @keyframes dars-spin {
1139
+ 0% { transform: rotate(0deg); }
1140
+ 100% { transform: rotate(360deg); }
1141
+ }
1142
+
1143
+ /* Tooltip */
1144
+ .dars-tooltip {
1145
+ position: relative;
1146
+ display: inline-block;
1147
+ cursor: pointer;
1148
+ }
1149
+ .dars-tooltip .dars-tooltip-text {
1150
+ visibility: hidden;
1151
+ width: max-content;
1152
+ background: #333;
1153
+ color: #fff;
1154
+ text-align: center;
1155
+ border-radius: 4px;
1156
+ padding: 6px 10px;
1157
+ position: absolute;
1158
+ z-index: 10;
1159
+ opacity: 0;
1160
+ transition: opacity 0.2s;
1161
+ font-size: 13px;
1162
+ pointer-events: none;
1163
+ }
1164
+ .dars-tooltip:hover .dars-tooltip-text,
1165
+ .dars-tooltip:focus .dars-tooltip-text {
1166
+ visibility: visible;
1167
+ opacity: 1;
1168
+ }
1169
+ .dars-tooltip-top .dars-tooltip-text {
1170
+ bottom: 125%;
1171
+ left: 50%;
1172
+ transform: translateX(-50%);
1173
+ margin-bottom: 6px;
1174
+ }
1175
+ .dars-tooltip-bottom .dars-tooltip-text {
1176
+ top: 125%;
1177
+ left: 50%;
1178
+ transform: translateX(-50%);
1179
+ margin-top: 6px;
1180
+ }
1181
+ .dars-tooltip-left .dars-tooltip-text {
1182
+ right: 125%;
1183
+ top: 50%;
1184
+ transform: translateY(-50%);
1185
+ margin-right: 6px;
1186
+ }
1187
+ .dars-tooltip-right .dars-tooltip-text {
1188
+ left: 125%;
1189
+ display: none; /* Hidden by default */
1190
+ position: fixed; /* Stay in place */
1191
+ z-index: 1; /* Sit on top */
1192
+ left: 0;
1193
+ top: 0;
1194
+ width: 100%; /* Full width */
1195
+ height: 100%; /* Full height */
1196
+ overflow: auto; /* Enable scroll if needed */
1197
+ background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
1198
+ justify-content: center;
1199
+ align-items: center;
1200
+ }
1201
+ .dars-modal-hidden {
1202
+ display: none !important;
1203
+ }
1204
+
1205
+ .dars-modal-content {
1206
+ background-color: #fefefe;
1207
+ margin: auto;
1208
+ padding: 20px;
1209
+ border: 1px solid #888;
1210
+ width: 80%;
1211
+ max-width: 500px;
1212
+ border-radius: 8px;
1213
+ box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2), 0 6px 20px 0 rgba(0,0,0,0.19);
1214
+ }
1215
+
1216
+ .dars-navbar {
1217
+ display: flex;
1218
+ justify-content: space-between;
1219
+ align-items: center;
1220
+ padding: 1rem;
1221
+ background-color: #f8f9fa;
1222
+ border-bottom: 1px solid #dee2e6;
1223
+ }
1224
+
1225
+ .dars-navbar-brand {
1226
+ font-weight: bold;
1227
+ font-size: 1.25rem;
1228
+ color: #333;
1229
+ }
1230
+
1231
+ .dars-navbar-nav {
1232
+ display: flex;
1233
+ gap: 1rem;
1234
+ }
1235
+
1236
+ .dars-navbar-nav a {
1237
+ color: #007bff;
1238
+ text-decoration: none;
1239
+ padding: 0.5rem 1rem;
1240
+ }
1241
+
1242
+ .dars-navbar-nav a:hover {
1243
+ background-color: #e9ecef;
1244
+ border-radius: 4px;
1245
+ }
1246
+
1247
+ /* Estilos para nuevos componentes básicos */
1248
+
1249
+ /* Checkbox */
1250
+ .dars-checkbox-wrapper {
1251
+ display: flex;
1252
+ align-items: center;
1253
+ gap: 8px;
1254
+ margin: 4px 0;
1255
+ }
1256
+
1257
+ .dars-checkbox {
1258
+ width: 16px;
1259
+ height: 16px;
1260
+ cursor: pointer;
1261
+ }
1262
+
1263
+ .dars-checkbox:disabled {
1264
+ opacity: 0.6;
1265
+ cursor: not-allowed;
1266
+ }
1267
+
1268
+ .dars-checkbox-wrapper label {
1269
+ cursor: pointer;
1270
+ user-select: none;
1271
+ }
1272
+
1273
+ /* RadioButton */
1274
+ .dars-radio-wrapper {
1275
+ display: flex;
1276
+ align-items: center;
1277
+ gap: 8px;
1278
+ margin: 4px 0;
1279
+ }
1280
+
1281
+ .dars-radio {
1282
+ width: 16px;
1283
+ height: 16px;
1284
+ cursor: pointer;
1285
+ }
1286
+
1287
+ .dars-radio:disabled {
1288
+ opacity: 0.6;
1289
+ cursor: not-allowed;
1290
+ }
1291
+
1292
+ .dars-radio-wrapper label {
1293
+ cursor: pointer;
1294
+ user-select: none;
1295
+ }
1296
+
1297
+ /* Select */
1298
+ .dars-select {
1299
+ display: inline-block;
1300
+ padding: 8px 12px;
1301
+ border: 1px solid #ccc;
1302
+ border-radius: 4px;
1303
+ font-size: 14px;
1304
+ background-color: white;
1305
+ cursor: pointer;
1306
+ min-width: 120px;
1307
+ }
1308
+
1309
+ .dars-select:focus {
1310
+ outline: none;
1311
+ border-color: #007bff;
1312
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
1313
+ }
1314
+
1315
+ .dars-select:disabled {
1316
+ opacity: 0.6;
1317
+ cursor: not-allowed;
1318
+ background-color: #f8f9fa;
1319
+ }
1320
+
1321
+ .dars-select option:disabled {
1322
+ color: #6c757d;
1323
+ }
1324
+
1325
+ /* Slider */
1326
+ .dars-slider-wrapper {
1327
+ display: flex;
1328
+ align-items: center;
1329
+ gap: 12px;
1330
+ margin: 8px 0;
1331
+ }
1332
+
1333
+ .dars-slider-wrapper.dars-slider-vertical {
1334
+ flex-direction: column;
1335
+ align-items: stretch;
1336
+ }
1337
+
1338
+ .dars-slider {
1339
+ flex: 1;
1340
+ cursor: pointer;
1341
+ }
1342
+
1343
+ .dars-slider-horizontal .dars-slider {
1344
+ width: 100%;
1345
+ height: 6px;
1346
+ }
1347
+
1348
+ .dars-slider-vertical input[type="range"] {
1349
+ width: 8px;
1350
+ height: 160px;
1351
+ writing-mode: vertical-lr;
1352
+ direction: rtl;
1353
+ }
1354
+
1355
+ .dars-slider:disabled {
1356
+ opacity: 0.6;
1357
+ cursor: not-allowed;
1358
+ }
1359
+
1360
+ .dars-slider-value {
1361
+ font-weight: bold;
1362
+ min-width: 40px;
1363
+ text-align: center;
1364
+ padding: 4px 8px;
1365
+ background-color: #f8f9fa;
1366
+ border-radius: 4px;
1367
+ font-size: 12px;
1368
+ }
1369
+
1370
+ .dars-slider-wrapper label {
1371
+ font-weight: 500;
1372
+ margin-bottom: 4px;
1373
+ }
1374
+
1375
+ /* DatePicker */
1376
+ .dars-datepicker {
1377
+ display: inline-block;
1378
+ padding: 8px 12px;
1379
+ border: 1px solid #ccc;
1380
+ border-radius: 4px;
1381
+ font-size: 14px;
1382
+ background-color: white;
1383
+ cursor: pointer;
1384
+ }
1385
+
1386
+ .dars-datepicker:focus {
1387
+ outline: none;
1388
+ border-color: #007bff;
1389
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
1390
+ }
1391
+
1392
+ .dars-datepicker:disabled {
1393
+ opacity: 0.6;
1394
+ cursor: not-allowed;
1395
+ background-color: #f8f9fa;
1396
+ }
1397
+
1398
+ .dars-datepicker:readonly {
1399
+ background-color: #f8f9fa;
1400
+ cursor: default;
1401
+ }
1402
+
1403
+ .dars-datepicker-inline {
1404
+ display: inline-block;
1405
+ border: 1px solid #ccc;
1406
+ border-radius: 4px;
1407
+ padding: 12px;
1408
+ background-color: white;
1409
+ }
1410
+
1411
+ .dars-datepicker-inline .dars-datepicker {
1412
+ border: none;
1413
+ padding: 0;
1414
+ }
1415
+ /* Markdown Styles */
1416
+ .dars-markdown {
1417
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
1418
+ line-height: 1.6;
1419
+ color: #333;
1420
+ background-color: #ffffff;
1421
+ padding: 20px;
1422
+ border-radius: 8px;
1423
+ transition: all 0.3s ease;
1424
+ }
1425
+
1426
+ /* Dark Theme */
1427
+ .dars-markdown-dark {
1428
+ color: #e0e0e0;
1429
+ background-color: #1e1e1e;
1430
+ }
1431
+
1432
+ .dars-markdown h1,
1433
+ .dars-markdown h2,
1434
+ .dars-markdown h3,
1435
+ .dars-markdown h4,
1436
+ .dars-markdown h5,
1437
+ .dars-markdown h6 {
1438
+ margin-top: 1.5em;
1439
+ margin-bottom: 0.5em;
1440
+ font-weight: 600;
1441
+ line-height: 1.25;
1442
+ }
1443
+
1444
+ .dars-markdown-dark h1,
1445
+ .dars-markdown-dark h2,
1446
+ .dars-markdown-dark h3,
1447
+ .dars-markdown-dark h4,
1448
+ .dars-markdown-dark h5,
1449
+ .dars-markdown-dark h6 {
1450
+ color: #ffffff;
1451
+ }
1452
+
1453
+ .dars-markdown h1 { font-size: 2em; }
1454
+ .dars-markdown h2 { font-size: 1.5em; }
1455
+ .dars-markdown h3 { font-size: 1.25em; }
1456
+ .dars-markdown h4 { font-size: 1em; }
1457
+ .dars-markdown h5 { font-size: 0.875em; }
1458
+ .dars-markdown h6 { font-size: 0.85em; }
1459
+
1460
+ .dars-markdown p {
1461
+ margin-bottom: 1em;
1462
+ }
1463
+
1464
+ .dars-markdown-dark p {
1465
+ color: #cccccc;
1466
+ }
1467
+
1468
+ .dars-markdown strong {
1469
+ font-weight: 600;
1470
+ }
1471
+
1472
+ .dars-markdown em {
1473
+ font-style: italic;
1474
+ }
1475
+
1476
+ .dars-markdown ul,
1477
+ .dars-markdown ol {
1478
+ margin-bottom: 1em;
1479
+ padding-left: 2em;
1480
+ }
1481
+
1482
+ .dars-markdown-dark ul,
1483
+ .dars-markdown-dark ol {
1484
+ color: #cccccc;
1485
+ }
1486
+
1487
+ .dars-markdown li {
1488
+ margin-bottom: 0.5em;
1489
+ }
1490
+
1491
+ .dars-markdown code {
1492
+ background-color: #f6f8fa;
1493
+ padding: 0.2em 0.4em;
1494
+ border-radius: 3px;
1495
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
1496
+ font-size: 0.85em;
1497
+ color: #333;
1498
+ }
1499
+
1500
+ .dars-markdown-dark code {
1501
+ background-color: #2d2d2d;
1502
+ color: #e0e0e0;
1503
+ }
1504
+
1505
+ .dars-markdown pre {
1506
+ background-color: #f6f8fa;
1507
+ padding: 1em;
1508
+ border-radius: 3px;
1509
+ overflow: auto;
1510
+ margin-bottom: 1em;
1511
+ }
1512
+
1513
+ .dars-markdown-dark pre {
1514
+ background-color: #2d2d2d;
1515
+ border: 1px solid #404040;
1516
+ }
1517
+
1518
+ .dars-markdown pre code {
1519
+ background: none;
1520
+ padding: 0;
1521
+ }
1522
+
1523
+ .dars-markdown blockquote {
1524
+ border-left: 4px solid #ddd;
1525
+ padding-left: 1em;
1526
+ margin-left: 0;
1527
+ color: #666;
1528
+ font-style: italic;
1529
+ background-color: #f9f9f9;
1530
+ padding: 10px 15px;
1531
+ border-radius: 4px;
1532
+ }
1533
+
1534
+ .dars-markdown-dark blockquote {
1535
+ border-left-color: #555;
1536
+ color: #bbb;
1537
+ background-color: #2a2a2a;
1538
+ }
1539
+
1540
+ .dars-markdown table {
1541
+ border-collapse: collapse;
1542
+ width: 100%;
1543
+ margin-bottom: 1em;
1544
+ }
1545
+
1546
+ .dars-markdown-dark table {
1547
+ border-color: #444;
1548
+ }
1549
+
1550
+ .dars-markdown th,
1551
+ .dars-markdown td {
1552
+ border: 1px solid #ddd;
1553
+ padding: 0.5em;
1554
+ text-align: left;
1555
+ }
1556
+
1557
+ .dars-markdown-dark th,
1558
+ .dars-markdown-dark td {
1559
+ border-color: #444;
1560
+ color: #e0e0e0;
1561
+ }
1562
+
1563
+ .dars-markdown th {
1564
+ background-color: #f6f8fa;
1565
+ font-weight: 600;
1566
+ }
1567
+
1568
+ .dars-markdown-dark th {
1569
+ background-color: #333;
1570
+ }
1571
+
1572
+ .dars-markdown a {
1573
+ color: #0366d6;
1574
+ text-decoration: none;
1575
+ }
1576
+
1577
+ .dars-markdown-dark a {
1578
+ color: #4da6ff;
1579
+ }
1580
+
1581
+ .dars-markdown a:hover {
1582
+ text-decoration: underline;
1583
+ }
1584
+
1585
+ .dars-markdown-dark a:hover {
1586
+ color: #66b3ff;
1587
+ }
1588
+
1589
+ .dars-markdown img {
1590
+ max-width: 100%;
1591
+ height: auto;
1592
+ border-radius: 4px;
1593
+ }
1594
+
1595
+ .dars-markdown-dark img {
1596
+ filter: brightness(0.9);
1597
+ }
1598
+
1599
+ /* Horizontal Rule */
1600
+ .dars-markdown hr {
1601
+ border: none;
1602
+ height: 1px;
1603
+ background-color: #ddd;
1604
+ margin: 2em 0;
1605
+ }
1606
+
1607
+ .dars-markdown-dark hr {
1608
+ background-color: #444;
1609
+ }
1610
+ """
1611
+
1612
+ def build_vdom_tree(self, component: Component) -> dict:
1613
+ """Serializa un componente Dars a un VNode (snapshot VDOM para hidratación)."""
1614
+ try:
1615
+ comp_type = component.__class__.__name__
1616
+ except Exception:
1617
+ comp_type = 'Component'
1618
+
1619
+ comp_id = self.get_component_id(component)
1620
+
1621
+ # Serializar eventos (solo inline ejecutable en cliente)
1622
+ events_payload = {}
1623
+ try:
1624
+ events = getattr(component, 'events', {}) or {}
1625
+ for ev_name, handler in events.items():
1626
+ code = None
1627
+ try:
1628
+ if hasattr(handler, 'get_code'):
1629
+ code = handler.get_code()
1630
+ elif hasattr(handler, 'code'):
1631
+ code = getattr(handler, 'code')
1632
+ elif hasattr(handler, 'to_js'):
1633
+ code = handler.to_js()
1634
+ elif isinstance(handler, str):
1635
+ code = handler
1636
+ except Exception:
1637
+ code = None
1638
+ if code:
1639
+ try:
1640
+ code_str = str(code)
1641
+ except Exception:
1642
+ code_str = ''
1643
+ if code_str:
1644
+ events_payload[ev_name] = { 'type': 'inline', 'code': code_str }
1645
+ except Exception:
1646
+ events_payload = {}
1647
+
1648
+ # Props seguros (evitar funciones y objetos no serializables)
1649
+ safe_props = {}
1650
+ try:
1651
+ for k, v in (getattr(component, 'props', {}) or {}).items():
1652
+ if callable(v):
1653
+ continue
1654
+ if isinstance(v, (str, int, float, bool)) or v is None:
1655
+ safe_props[k] = v
1656
+ except Exception:
1657
+ pass
1658
+
1659
+ # Soporte para componentes de texto
1660
+ text_value = None
1661
+ try:
1662
+ if comp_type == 'Text' and hasattr(component, 'text'):
1663
+ text_value = component.text
1664
+ except Exception:
1665
+ pass
1666
+
1667
+ # Hijos
1668
+ children_nodes = []
1669
+ try:
1670
+ for child in getattr(component, 'children', []) or []:
1671
+ if child is None:
1672
+ continue
1673
+ children_nodes.append(self.build_vdom_tree(child))
1674
+ except Exception:
1675
+ children_nodes = []
1676
+
1677
+ vnode = {
1678
+ 'type': comp_type,
1679
+ 'id': comp_id,
1680
+ 'key': getattr(component, 'key', None),
1681
+ 'class': getattr(component, 'class_name', None),
1682
+ 'style': getattr(component, 'style', {}) or {},
1683
+ 'props': safe_props,
1684
+ 'events': events_payload if events_payload else None,
1685
+ 'children': children_nodes if children_nodes else []
1686
+ }
1687
+ if text_value is not None:
1688
+ vnode['text'] = text_value
1689
+ return vnode
1690
+
1691
+ def generate_vdom_snapshot(self, root_component: Component) -> str:
1692
+ """Genera el snapshot VDOM (JSON) a partir del componente raíz.
1693
+ Usa VDomBuilder para mantener consistencia con el vdom_tree.js externo.
1694
+ """
1695
+ import json
1696
+ try:
1697
+ vdom_dict = VDomBuilder(id_provider=self.get_component_id).build(root_component)
1698
+ except Exception:
1699
+ vdom_dict = {'type': 'Root', 'id': None, 'children': []}
1700
+ return json.dumps(vdom_dict, ensure_ascii=False)
1701
+
1702
+ def generate_javascript(self, app: App, page_root: Component) -> str:
1703
+ """Genera un runtime modular: hidratación + delegación de eventos + diff/patch + hot-reload incremental (polling)."""
1704
+ runtime = r"""// Dars Runtime (Hydration + Delegated Events + Diff/Patch + Hot Reload)
1705
+ (function(){
1706
+ const eventMap = new Map(); // id -> {ev: fn}
1707
+ let currentSnapshot = null;
1708
+ let currentVersion = null;
1709
+
1710
+ // Registro de componentes (skeleton). En siguientes iteraciones añadiremos create/patch por tipo built-in
1711
+ const registry = {
1712
+ // Implementación mínima segura para crear nodos cuando se agregan hijos
1713
+ 'Text': {
1714
+ create(v){
1715
+ if(!v || v.isIsland) return null;
1716
+ const el = document.createElement('span');
1717
+ if(v.id) el.id = v.id;
1718
+ if(v.class) el.className = v.class;
1719
+ if(v.style){ for(const k in v.style){ try{ el.style.setProperty(k.replace(/_/g,'-'), String(v.style[k])); }catch{} } }
1720
+ if(Object.prototype.hasOwnProperty.call(v,'text')){ el.textContent = String(v.text||''); }
1721
+ // props
1722
+ if(v.props){ for(const k in v.props){ const val=v.props[k]; try{ if(val===false||val===null||typeof val==='undefined'){ el.removeAttribute(k);} else { el.setAttribute(k, String(val)); } }catch{} } }
1723
+ return el;
1724
+ }
1725
+ },
1726
+ 'Container': {
1727
+ create(v){
1728
+ if(!v || v.isIsland) return null;
1729
+ const el = document.createElement('div');
1730
+ if(v.id) el.id = v.id;
1731
+ const base = 'dars-container';
1732
+ el.className = (v.class ? (base + ' ' + v.class) : base);
1733
+ if(v.style){ for(const k in v.style){ try{ el.style.setProperty(k.replace(/_/g,'-'), String(v.style[k])); }catch{} } }
1734
+ // props
1735
+ if(v.props){ for(const k in v.props){ const val=v.props[k]; try{ if(val===false||val===null||typeof val==='undefined'){ el.removeAttribute(k);} else { el.setAttribute(k, String(val)); } }catch{} } }
1736
+ return el;
1737
+ }
1738
+ },
1739
+ };
1740
+
1741
+ function walk(v, fn){
1742
+ if(!v) return;
1743
+ fn(v);
1744
+ const ch = v.children || [];
1745
+ for(let i=0;i<ch.length;i++){ walk(ch[i], fn); }
1746
+ }
1747
+
1748
+ function _decodeCodeB64(b64){
1749
+ try {
1750
+ if (typeof atob === 'function') return atob(b64);
1751
+ if (typeof Buffer !== 'undefined') { return Buffer.from(b64, 'base64').toString('utf8'); }
1752
+ } catch(_){ }
1753
+ return '';
1754
+ }
1755
+
1756
+ function _compileHandlerFromSpec(spec){
1757
+ try {
1758
+ if (!spec) return null;
1759
+ if (spec && spec.type === 'inline' && spec.code) {
1760
+ return new Function('event', spec.code);
1761
+ }
1762
+ const b64 = (spec && (spec.b || spec.code_b64)) || null;
1763
+ if (b64){
1764
+ const code = _decodeCodeB64(b64);
1765
+ if (code) return new Function('event', code);
1766
+ }
1767
+ } catch(_){ }
1768
+ return null;
1769
+ }
1770
+
1771
+ function bindEventsFromVNode(snapshot){
1772
+ // Construir tabla de eventos a partir del snapshot
1773
+ walk(snapshot, (v)=>{
1774
+ if(v && v.id && v.events){
1775
+ const handlers = {};
1776
+ for(const ev in v.events){
1777
+ const spec = v.events[ev];
1778
+ const fn = _compileHandlerFromSpec(spec);
1779
+ if (fn) { handlers[ev] = fn; }
1780
+ }
1781
+ if(Object.keys(handlers).length){ eventMap.set(v.id, handlers); } else { eventMap.delete(v.id); }
1782
+ }
1783
+ });
1784
+ }
1785
+
1786
+ // Utilities
1787
+ function setProps(el, props){
1788
+ if(!el || !props) return;
1789
+ for(const [k,v] of Object.entries(props)){
1790
+ try {
1791
+ if(v === false || v === null || typeof v === 'undefined'){
1792
+ el.removeAttribute(k);
1793
+ } else {
1794
+ el.setAttribute(k, String(v));
1795
+ }
1796
+ } catch(err) { /* ignore */ }
1797
+ }
1798
+ }
1799
+ function diffProps(el, oldP={}, newP={}){
1800
+ // remove
1801
+ for(const k in oldP){ if(!(k in newP)){ try{ el.removeAttribute(k); }catch{} } }
1802
+ // add/update
1803
+ for(const k in newP){ const v=newP[k]; try{ if(v===false||v===null||typeof v==='undefined'){ el.removeAttribute(k);} else { el.setAttribute(k, String(v)); } }catch{} }
1804
+ }
1805
+ function diffStyles(el, oldS={}, newS={}){
1806
+ for(const k in oldS){ if(!(k in newS)){ try{ el.style.removeProperty(k.replace(/_/g,'-')); }catch{} } }
1807
+ for(const k in newS){ const v=newS[k]; try{ el.style.setProperty(k.replace(/_/g,'-'), String(v)); }catch{} }
1808
+ }
1809
+
1810
+ // Event delegation helper (restored)
1811
+ function delegate(eventName, root){
1812
+ (root||document).addEventListener(eventName, function(e){
1813
+ let node = e.target;
1814
+ const boundary = root||document;
1815
+ while(node && node !== boundary){
1816
+ const id = node.id;
1817
+ if(id && eventMap.has(id)){
1818
+ const handlers = eventMap.get(id);
1819
+ const h = handlers[eventName];
1820
+ // If there is a dynamic handler attached on this node for the same event, let it handle and skip default
1821
+ if(node && node.__darsEv && node.__darsEv[eventName]){
1822
+ return;
1823
+ }
1824
+ if(typeof h === 'function'){
1825
+ try { h.call(node, e); } catch(err){ console.error('[Dars] handler error', err); }
1826
+ return;
1827
+ }
1828
+ }
1829
+ node = node.parentNode;
1830
+ }
1831
+ }, true);
1832
+ }
1833
+
1834
+ function typesDiffer(a,b){ return (a && b) ? a.type !== b.type : a!==b; }
1835
+
1836
+ // Elimina un subárbol del DOM (y del mapa de eventos) usando los ids del VDOM
1837
+ function removeSubtree(v){
1838
+ if(!v) return;
1839
+ // eliminar hijos primero (postorden)
1840
+ const ch = (v.children||[]);
1841
+ for(let i=0;i<ch.length;i++){ removeSubtree(ch[i]); }
1842
+ // limpiar handlers
1843
+ if(v.id){ eventMap.delete(v.id); }
1844
+ // quitar elemento del DOM
1845
+ if(v.id){ const el = document.getElementById(v.id); if(el && el.parentNode){ try{ el.parentNode.removeChild(el); }catch(_){} }}
1846
+ }
1847
+
1848
+ function updateNode(oldV, newV){
1849
+ if(!newV || !newV.id){ return { ok:false, reason:'missing-new' }; }
1850
+ let el = document.getElementById(newV.id);
1851
+ if(!el){
1852
+ // Fallback: si cambió el id entre snapshots pero es el mismo nodo lógico, reasignamos id
1853
+ const oldEl = (oldV && oldV.id) ? document.getElementById(oldV.id) : null;
1854
+ if(oldEl){ try { oldEl.id = newV.id; el = oldEl; } catch(_){} }
1855
+ }
1856
+ if(!el){ return { ok:false, reason:'missing-el' }; }
1857
+
1858
+ // Si cambia el tipo, estructura u orden de hijos, pedimos reload completo (fase 2 simplificada)
1859
+ if(typesDiffer(oldV, newV)){
1860
+ return { ok:false, reason:'type-changed' };
1861
+ }
1862
+
1863
+ const isIsland = !!newV.isIsland;
1864
+
1865
+ // class -> atributo className
1866
+ if(!isIsland && newV.class){ el.className = newV.class; }
1867
+
1868
+ // props
1869
+ if(!isIsland){ diffProps(el, (oldV&&oldV.props)||{}, newV.props||{}); }
1870
+
1871
+ // styles
1872
+ if(!isIsland){ diffStyles(el, (oldV&&oldV.style)||{}, newV.style||{}); }
1873
+
1874
+ // text
1875
+ if(!isIsland && Object.prototype.hasOwnProperty.call(newV, 'text')){
1876
+ if(el.textContent !== String(newV.text||'')){
1877
+ el.textContent = String(newV.text||'');
1878
+ }
1879
+ }
1880
+
1881
+ // events
1882
+ if(newV.events){
1883
+ const handlers = {};
1884
+ for(const ev in newV.events){
1885
+ const spec = newV.events[ev];
1886
+ const fn = _compileHandlerFromSpec(spec);
1887
+ if (fn) { handlers[ev] = fn; }
1888
+ }
1889
+ if(Object.keys(handlers).length){ eventMap.set(newV.id, handlers); } else { eventMap.delete(newV.id); }
1890
+ } else {
1891
+ eventMap.delete(newV.id);
1892
+ }
1893
+
1894
+ // hijos (reconciliación por id/key). Para islas, tratamos el subárbol como opaco.
1895
+ if(isIsland){ return { ok:true }; }
1896
+
1897
+ // Permitimos REMOCIONES sin recarga.
1898
+ const oldC = (oldV && oldV.children) ? oldV.children : [];
1899
+ const newC = (newV.children) ? newV.children : [];
1900
+
1901
+ // Construir índice de hijos viejos por id o key
1902
+ const oldIndex = new Map(); // clave -> vnode viejo
1903
+ for(let i=0;i<oldC.length;i++){
1904
+ const k = (oldC[i] && (oldC[i].id || oldC[i].key)) || null;
1905
+ if(k){ oldIndex.set(String(k), oldC[i]); }
1906
+ }
1907
+
1908
+ // Seguimiento de cuáles viejos fueron actualizados
1909
+ const seenOld = new Set();
1910
+
1911
+ // Actualizar/validar hijos nuevos
1912
+ for(let i=0;i<newC.length;i++){
1913
+ const newChild = newC[i];
1914
+ const k = (newChild && (newChild.id || newChild.key)) || null;
1915
+ if(!k){
1916
+ // sin id/key fiable: conservador => usar reconciliación por índice si existe par
1917
+ if(i < oldC.length){
1918
+ const r = updateNode(oldC[i], newChild);
1919
+ if(!r.ok){ return r; }
1920
+ seenOld.add(oldC[i]);
1921
+ continue;
1922
+ } else {
1923
+ // no podemos crear de forma segura
1924
+ return { ok:false, reason:'children-added' };
1925
+ }
1926
+ }
1927
+ const oldChild = oldIndex.get(String(k));
1928
+ if(oldChild){
1929
+ const r = updateNode(oldChild, newChild);
1930
+ if(!r.ok){ return r; }
1931
+ seenOld.add(oldChild);
1932
+ } else {
1933
+ // Fallback conservador: si hay viejo en la misma posición y el tipo coincide, lo reutilizamos
1934
+ if(i < oldC.length){
1935
+ const candidate = oldC[i];
1936
+ if(!typesDiffer(candidate, newChild)){
1937
+ const r = updateNode(candidate, newChild);
1938
+ if(!r.ok){ return r; }
1939
+ seenOld.add(candidate);
1940
+ continue;
1941
+ }
1942
+ }
1943
+ // Intentar crear subárbol si es un tipo soportado por el registry (no isla)
1944
+ const subtree = createSubtree(newChild);
1945
+ if(subtree){
1946
+ // insertar en la posición i dentro del DOM
1947
+ const refChildVNode = (i < oldC.length) ? oldC[i] : null;
1948
+ if(refChildVNode && refChildVNode.id){
1949
+ const refEl = document.getElementById(refChildVNode.id);
1950
+ if(refEl && refEl.parentNode){ refEl.parentNode.insertBefore(subtree, refEl); }
1951
+ else { el.appendChild(subtree); }
1952
+ } else {
1953
+ el.appendChild(subtree);
1954
+ }
1955
+ // marcar como visto (no había old), nada que añadir a seenOld
1956
+ continue;
1957
+ }
1958
+ // hijo nuevo de tipo no soportado => recarga por seguridad
1959
+ return { ok:false, reason:'children-added' };
1960
+ }
1961
+ }
1962
+
1963
+ // Eliminar los viejos no vistos (removidos)
1964
+ for(let i=0;i<oldC.length;i++){
1965
+ const v = oldC[i];
1966
+ if(!seenOld.has(v)){
1967
+ removeSubtree(v);
1968
+ }
1969
+ }
1970
+ return { ok:true };
1971
+ }
1972
+
1973
+ function schedule(fn){
1974
+ if(typeof requestAnimationFrame === 'function'){
1975
+ requestAnimationFrame(fn);
1976
+ } else { setTimeout(fn, 16); }
1977
+ }
1978
+
1979
+ function update(newSnapshot){
1980
+ const old = currentSnapshot;
1981
+ if(!old){
1982
+ // primera vez: solo (re)hidratar eventos
1983
+ bindEventsFromVNode(newSnapshot);
1984
+ currentSnapshot = newSnapshot;
1985
+ try{ window.__DARS_VDOM__ = newSnapshot; }catch(_){ /* ignore */ }
1986
+ return;
1987
+ }
1988
+ schedule(()=>{
1989
+ const res = updateNode(old, newSnapshot);
1990
+ if(!res.ok){
1991
+ console.warn('[Dars] Structural change detected (', res.reason, '), reloading...');
1992
+ try { location.reload(); } catch(e) { /* ignore */ }
1993
+ return;
1994
+ }
1995
+ // Re-vincular mapa de eventos por si cambió
1996
+ bindEventsFromVNode(newSnapshot);
1997
+ currentSnapshot = newSnapshot;
1998
+ try{ window.__DARS_VDOM__ = newSnapshot; }catch(_){ /* ignore */ }
1999
+ });
2000
+ }
2001
+
2002
+ function hydrate(snapshot){
2003
+ bindEventsFromVNode(snapshot);
2004
+ currentSnapshot = snapshot;
2005
+ try{ window.__DARS_VDOM__ = snapshot; }catch(_){ /* ignore */ }
2006
+
2007
+ // Delegar eventos comunes (extendido)
2008
+ const delegated = [
2009
+ 'click','dblclick',
2010
+ 'mousedown','mouseup','mouseenter','mouseleave','mousemove',
2011
+ 'keydown','keyup','keypress',
2012
+ 'change','input','submit',
2013
+ 'focus','blur'
2014
+ ];
2015
+ delegated.forEach(ev => delegate(ev, document));
2016
+ }
2017
+
2018
+ function startHotReload(){
2019
+ const vurl = (window.__DARS_VERSION_URL || 'version.txt');
2020
+ let timer = null;
2021
+ let warnedVersionMissing = false;
2022
+
2023
+ function httpGet(url, onSuccess, onError, responseType){
2024
+ try{
2025
+ const xhr = new XMLHttpRequest();
2026
+ if(responseType){ xhr.responseType = responseType; }
2027
+ xhr.open('GET', url, true);
2028
+ xhr.timeout = 5000;
2029
+ xhr.onreadystatechange = function(){
2030
+ if(xhr.readyState === 4){
2031
+ if(xhr.status >= 200 && xhr.status < 300){
2032
+ onSuccess(xhr.response);
2033
+ } else {
2034
+ onError();
2035
+ }
2036
+ }
2037
+ };
2038
+ xhr.onerror = onError;
2039
+ xhr.ontimeout = onError;
2040
+ xhr.setRequestHeader('Cache-Control', 'no-store');
2041
+ xhr.send();
2042
+ }catch(e){ onError(); }
2043
+ }
2044
+
2045
+ function tick(){
2046
+ httpGet(vurl, function(text){
2047
+ let ver = (text || '').toString().trim();
2048
+ if(ver){ warnedVersionMissing = false; }
2049
+ if(!currentVersion){ currentVersion = ver; }
2050
+ if(ver && ver !== currentVersion){
2051
+ currentVersion = ver;
2052
+ // Política solicitada: siempre recargar por completo al detectar nueva versión
2053
+ try { location.reload(); } catch(_) {}
2054
+ return;
2055
+ }
2056
+ timer = setTimeout(tick, 600);
2057
+ }, function(){
2058
+ if(!warnedVersionMissing){ console.log('[Dars] waiting for version.txt'); warnedVersionMissing = true; }
2059
+ timer = setTimeout(tick, 600);
2060
+ }, 'text');
2061
+ }
2062
+ tick();
2063
+ return ()=>{ if(timer) clearTimeout(timer); };
2064
+ }
2065
+
2066
+ document.addEventListener('DOMContentLoaded', function(){
2067
+ if(window.__DARS_VDOM__){
2068
+ hydrate(window.__DARS_VDOM__);
2069
+ } else {
2070
+ console.warn('[Dars] No VDOM snapshot found for hydration');
2071
+ }
2072
+ // Activar hot-reload incremental en dev si hay URLs definidas
2073
+ if(window.__DARS_VERSION_URL && window.__DARS_SNAPSHOT_URL){
2074
+ startHotReload();
2075
+ }
2076
+ });
2077
+ })();
2078
+ """
2079
+ return runtime
2080
+
2081
+ def get_component_id(self, component, prefix="comp"):
2082
+ """
2083
+ Devuelve el id del componente.
2084
+ - Si el componente ya tiene id definido, se respeta.
2085
+ - Si no tiene, se genera uno único y se asigna al objeto (para consistencia).
2086
+ """
2087
+ comp_id = getattr(component, "id", None)
2088
+ if not comp_id:
2089
+ comp_id = self.generate_unique_id(component, prefix=prefix)
2090
+ try:
2091
+ component.id = comp_id
2092
+ except Exception:
2093
+ # si el objeto no permite asignar, seguimos usando comp_id local
2094
+ pass
2095
+ # Hash IDs in bundle mode consistently
2096
+ if getattr(self, '_hash_ids', False) and comp_id:
2097
+ hid = self._hash_id(comp_id)
2098
+ try:
2099
+ component.id = hid
2100
+ except Exception:
2101
+ pass
2102
+ return hid
2103
+ return comp_id
2104
+
2105
+ def _hash_id(self, original: str) -> str:
2106
+ import hashlib
2107
+ m = getattr(self, '_id_hash_map', None)
2108
+ if m is None:
2109
+ self._id_hash_map = {}
2110
+ m = self._id_hash_map
2111
+ if original in m:
2112
+ return m[original]
2113
+ h = hashlib.sha256(original.encode('utf-8')).hexdigest()[:12]
2114
+ obf = 'd' + h
2115
+ m[original] = obf
2116
+ return obf
2117
+
2118
+ def render_component(self, component: Component) -> str:
2119
+ if not isinstance(component, Component):
2120
+ raise TypeError(f"render_component wait to recived an instance of Component, but recive an {component}")
2121
+ """Render an HTML component"""
2122
+ from dars.components.basic.page import Page
2123
+ from dars.components.layout.grid import GridLayout
2124
+ from dars.components.layout.flex import FlexLayout
2125
+
2126
+
2127
+ # Lista de componentes built-in de Dars que NO deben usar su propio método render()
2128
+ builtin_components = [
2129
+ Page, GridLayout, FlexLayout, Text, Button, Input, Container, Image, Link,
2130
+ Textarea, Card, Modal, Navbar, Checkbox, RadioButton, Select, Slider,
2131
+ DatePicker, Table, Tabs, Accordion, ProgressBar, Spinner, Tooltip, Markdown,
2132
+ ]
2133
+
2134
+ # Verificar si es un componente personalizado (no built-in)
2135
+ is_custom_component = True
2136
+ for builtin_type in builtin_components:
2137
+ if isinstance(component, builtin_type):
2138
+ is_custom_component = False
2139
+ break
2140
+
2141
+ if isinstance(component, Component) and is_custom_component:
2142
+ if hasattr(component, 'render') and callable(component.render):
2143
+ try:
2144
+ return component.render(self)
2145
+ except Exception as e:
2146
+ print(f"Error at rendering component {component.__class__.__name__}: {e}")
2147
+
2148
+
2149
+ if isinstance(component, Page):
2150
+ return self.render_page(component)
2151
+ if isinstance(component, GridLayout):
2152
+ return self.render_grid(component)
2153
+ if isinstance(component, FlexLayout):
2154
+ return self.render_flex(component)
2155
+ if isinstance(component, Text):
2156
+ return self.render_text(component)
2157
+ elif isinstance(component, Button):
2158
+ return self.render_button(component)
2159
+ elif isinstance(component, Input):
2160
+ return self.render_input(component)
2161
+ elif isinstance(component, Container):
2162
+ return self.render_container(component)
2163
+ elif isinstance(component, Image):
2164
+ return self.render_image(component)
2165
+ elif isinstance(component, Link):
2166
+ return self.render_link(component)
2167
+ elif isinstance(component, Textarea):
2168
+ return self.render_textarea(component)
2169
+ elif isinstance(component, Card):
2170
+ return self.render_card(component)
2171
+ elif isinstance(component, Modal):
2172
+ return self.render_modal(component)
2173
+ elif isinstance(component, Navbar):
2174
+ return self.render_navbar(component)
2175
+ elif isinstance(component, Checkbox):
2176
+ return self.render_checkbox(component)
2177
+ elif isinstance(component, RadioButton):
2178
+ return self.render_radiobutton(component)
2179
+ elif isinstance(component, Select):
2180
+ return self.render_select(component)
2181
+ elif isinstance(component, Slider):
2182
+ return self.render_slider(component)
2183
+ elif isinstance(component, DatePicker):
2184
+ return self.render_datepicker(component)
2185
+ elif isinstance(component, Table):
2186
+ return self.render_table(component)
2187
+ elif isinstance(component, Tabs):
2188
+ return self.render_tabs(component)
2189
+ elif isinstance(component, Accordion):
2190
+ return self.render_accordion(component)
2191
+ elif isinstance(component, ProgressBar):
2192
+ return self.render_progressbar(component)
2193
+ elif isinstance(component, Spinner):
2194
+ return self.render_spinner(component)
2195
+ elif isinstance(component, Tooltip):
2196
+ return self.render_tooltip(component)
2197
+ elif isinstance(component, Markdown):
2198
+ return self.render_markdown(component)
2199
+ else:
2200
+ # Componente genérico
2201
+ return self.render_generic_component(component)
2202
+
2203
+ def render_grid(self, grid):
2204
+ """Renderiza un GridLayout como un div con CSS grid."""
2205
+ component_id = self.get_component_id(grid, prefix="grid")
2206
+ class_attr = f'class="dars-grid {grid.class_name or ""}"'
2207
+ style = f'display: grid; grid-template-rows: repeat({grid.rows}, 1fr); grid-template-columns: repeat({grid.cols}, 1fr); gap: {getattr(grid, "gap", "16px")};'
2208
+ # Render anchors/positions
2209
+ children_html = ""
2210
+ layout_info = getattr(grid, 'get_child_layout', lambda: [])()
2211
+ for child_info in layout_info:
2212
+ child = child_info['child']
2213
+ row = child_info.get('row', 0) + 1
2214
+ col = child_info.get('col', 0) + 1
2215
+ row_span = child_info.get('row_span', 1)
2216
+ col_span = child_info.get('col_span', 1)
2217
+ anchor = child_info.get('anchor')
2218
+ anchor_style = ''
2219
+ if anchor:
2220
+ if isinstance(anchor, str):
2221
+ anchor_map = {
2222
+ 'top-left': 'justify-self: start; align-self: start;',
2223
+ 'top': 'justify-self: center; align-self: start;',
2224
+ 'top-right': 'justify-self: end; align-self: start;',
2225
+ 'left': 'justify-self: start; align-self: center;',
2226
+ 'center': 'justify-self: center; align-self: center;',
2227
+ 'right': 'justify-self: end; align-self: center;',
2228
+ 'bottom-left': 'justify-self: start; align-self: end;',
2229
+ 'bottom': 'justify-self: center; align-self: end;',
2230
+ 'bottom-right': 'justify-self: end; align-self: end;'
2231
+ }
2232
+ anchor_style = anchor_map.get(anchor, '')
2233
+ elif hasattr(anchor, 'x') or hasattr(anchor, 'y'):
2234
+ # AnchorPoint object
2235
+ if getattr(anchor, 'x', None):
2236
+ if anchor.x == 'left': anchor_style += 'justify-self: start;'
2237
+ elif anchor.x == 'center': anchor_style += 'justify-self: center;'
2238
+ elif anchor.x == 'right': anchor_style += 'justify-self: end;'
2239
+ elif '%' in anchor.x or 'px' in anchor.x: anchor_style += f'left: {anchor.x}; position: relative;'
2240
+ if getattr(anchor, 'y', None):
2241
+ if anchor.y == 'top': anchor_style += 'align-self: start;'
2242
+ elif anchor.y == 'center': anchor_style += 'align-self: center;'
2243
+ elif anchor.y == 'bottom': anchor_style += 'align-self: end;'
2244
+ elif '%' in anchor.y or 'px' in anchor.y: anchor_style += f'top: {anchor.y}; position: relative;'
2245
+ grid_item_style = f'grid-row: {row} / span {row_span}; grid-column: {col} / span {col_span}; {anchor_style}'
2246
+ children_html += f'<div style="{grid_item_style}">{self.render_component(child)}</div>'
2247
+ return f'<div id="{component_id}" {class_attr} style="{style}">{children_html}</div>'
2248
+
2249
+ def render_flex(self, flex):
2250
+ """Renderiza un FlexLayout como un div con CSS flexbox."""
2251
+ component_id = self.get_component_id(flex, prefix="flex")
2252
+ class_attr = f'class="dars-flex {flex.class_name or ""}"'
2253
+ style = f'display: flex; flex-direction: {getattr(flex, "direction", "row")}; flex-wrap: {getattr(flex, "wrap", "wrap")}; justify-content: {getattr(flex, "justify", "flex-start")}; align-items: {getattr(flex, "align", "stretch")}; gap: {getattr(flex, "gap", "16px")};'
2254
+ children_html = ""
2255
+ for child in flex.children:
2256
+ anchor = getattr(child, 'anchor', None)
2257
+ anchor_style = ''
2258
+ if anchor:
2259
+ if isinstance(anchor, str):
2260
+ anchor_map = {
2261
+ 'top-left': 'align-self: flex-start; justify-self: flex-start;',
2262
+ 'top': 'align-self: flex-start; margin-left: auto; margin-right: auto;',
2263
+ 'top-right': 'align-self: flex-start; margin-left: auto;',
2264
+ 'left': 'align-self: center;',
2265
+ 'center': 'align-self: center; margin-left: auto; margin-right: auto;',
2266
+ 'right': 'align-self: center; margin-left: auto;',
2267
+ 'bottom-left': 'align-self: flex-end;',
2268
+ 'bottom': 'align-self: flex-end; margin-left: auto; margin-right: auto;',
2269
+ 'bottom-right': 'align-self: flex-end; margin-left: auto;'
2270
+ }
2271
+ anchor_style = anchor_map.get(anchor, '')
2272
+ elif hasattr(anchor, 'x') or hasattr(anchor, 'y'):
2273
+ if getattr(anchor, 'x', None):
2274
+ if anchor.x == 'left': anchor_style += 'margin-right: auto;'
2275
+ elif anchor.x == 'center': anchor_style += 'margin-left: auto; margin-right: auto;'
2276
+ elif anchor.x == 'right': anchor_style += 'margin-left: auto;'
2277
+ elif '%' in anchor.x or 'px' in anchor.x: anchor_style += f'left: {anchor.x}; position: relative;'
2278
+ if getattr(anchor, 'y', None):
2279
+ if anchor.y == 'top': anchor_style += 'align-self: flex-start;'
2280
+ elif anchor.y == 'center': anchor_style += 'align-self: center;'
2281
+ elif anchor.y == 'bottom': anchor_style += 'align-self: flex-end;'
2282
+ elif '%' in anchor.y or 'px' in anchor.y: anchor_style += f'top: {anchor.y}; position: relative;'
2283
+ children_html += f'<div style="{anchor_style}">{self.render_component(child)}</div>'
2284
+ return f'<div id="{component_id}" {class_attr} style="{style}">{children_html}</div>'
2285
+
2286
+ def render_page(self, page):
2287
+ """Renderiza un componente Page como root de una página multipage"""
2288
+ component_id = self.generate_unique_id(page)
2289
+ class_attr = f'class="dars-page {page.class_name or ""}"'
2290
+ style_attr = f'style="{self.render_styles(page.style)}"' if page.style else ""
2291
+ # Renderizar hijos
2292
+ children_html = ""
2293
+ children = getattr(page, 'children', [])
2294
+ if not isinstance(children, list):
2295
+ children = []
2296
+ for child in children:
2297
+ if hasattr(child, 'render'):
2298
+ children_html += self.render_component(child)
2299
+ return f'<div id="{component_id}" {class_attr} {style_attr}>{children_html}</div>'
2300
+
2301
+
2302
+
2303
+ def render_text(self, text: Text) -> str:
2304
+ """Renderiza un componente Text"""
2305
+ component_id = self.get_component_id(text, prefix="text")
2306
+ class_attr = f'class="dars-text {text.class_name or ""}"'
2307
+ style_attr = f'style="{self.render_styles(text.style)}"' if text.style else ""
2308
+
2309
+ return f'<span id="{component_id}" {class_attr} {style_attr}>{text.text}</span>'
2310
+
2311
+ def render_button(self, button: Button) -> str:
2312
+ """Renderiza un componente Button"""
2313
+ # Asegurarse de que el botón tenga un ID
2314
+ if not hasattr(button, 'id') or not button.id:
2315
+ import uuid
2316
+ button.id = f"btn_{str(uuid.uuid4())[:8]}"
2317
+
2318
+ component_id = self.get_component_id(button, prefix="btn")
2319
+ class_attr = f'class="dars-button {button.class_name or ""}"'
2320
+ style_attr = f'style="{self.render_styles(button.style)}"' if button.style else ""
2321
+ type_attr = f'type="{button.button_type}"'
2322
+ disabled_attr = "disabled" if button.disabled else ""
2323
+
2324
+ return f'<button id="{component_id}" {class_attr} {style_attr} {type_attr} {disabled_attr}>{button.text}</button>'
2325
+
2326
+ def render_input(self, input_comp: Input) -> str:
2327
+ """Renderiza un componente Input"""
2328
+ component_id = self.get_component_id(input_comp, prefix="input")
2329
+ class_attr = f'class="dars-input {input_comp.class_name or ""}"'
2330
+ style_attr = f'style="{self.render_styles(input_comp.style)}"' if input_comp.style else ""
2331
+ type_attr = f'type="{input_comp.input_type}"'
2332
+ value_attr = f'value="{input_comp.value}"' if input_comp.value else ""
2333
+ placeholder_attr = f'placeholder="{input_comp.placeholder}"' if input_comp.placeholder else ""
2334
+ disabled_attr = "disabled" if input_comp.disabled else ""
2335
+ readonly_attr = "readonly" if input_comp.readonly else ""
2336
+ required_attr = "required" if input_comp.required else ""
2337
+
2338
+ attrs = [class_attr, style_attr, type_attr, value_attr, placeholder_attr,
2339
+ disabled_attr, readonly_attr, required_attr]
2340
+ attrs_str = " ".join(attr for attr in attrs if attr)
2341
+
2342
+ return f'<input id="{component_id}" {attrs_str} />'
2343
+
2344
+ def render_container(self, container: Container) -> str:
2345
+ """Renderiza un componente Container"""
2346
+ component_id = self.get_component_id(container, prefix="container")
2347
+ class_attr = f'class="dars-container {container.class_name or ""}"'
2348
+ style_attr = f'style="{self.render_styles(container.style)}"' if container.style else ""
2349
+
2350
+ # Protección: asegurar que children es lista de Component
2351
+ children_html = ""
2352
+ children = container.children
2353
+ if not isinstance(children, list):
2354
+ children = []
2355
+ # Aplanar si hay listas anidadas
2356
+ flat_children = []
2357
+ for child in children:
2358
+ if isinstance(child, list):
2359
+ flat_children.extend([c for c in child if hasattr(c, 'render')])
2360
+ elif hasattr(child, 'render'):
2361
+ flat_children.append(child)
2362
+ for child in flat_children:
2363
+ children_html += self.render_component(child)
2364
+
2365
+ return f'<div id="{component_id}" {class_attr} {style_attr}>{children_html}</div>'
2366
+
2367
+ def render_image(self, image: Image) -> str:
2368
+ """Renderiza un componente Image"""
2369
+ component_id = self.get_component_id(image, prefix="image")
2370
+ class_attr = f'class="dars-image {image.class_name or ""}"'
2371
+ style_attr = f'style="{self.render_styles(image.style)}"' if image.style else ""
2372
+ width_attr = f'width="{image.width}"' if image.width else ""
2373
+ height_attr = f'height="{image.height}"' if image.height else ""
2374
+
2375
+ return f'<img id="{component_id}" src="{image.src}" alt="{image.alt}" {width_attr} {height_attr} {class_attr} {style_attr} />'
2376
+
2377
+ def render_link(self, link: Link) -> str:
2378
+ """Renderiza un componente Link"""
2379
+ component_id = self.get_component_id(link, prefix="link")
2380
+ class_attr = f'class="dars-link {link.class_name or ""}"'
2381
+ style_attr = f'style="{self.render_styles(link.style)}"' if link.style else ""
2382
+ target_attr = f'target="{link.target}"'
2383
+
2384
+ return f'<a id="{component_id}" href="{link.href}" {target_attr} {class_attr} {style_attr}>{link.text}</a>'
2385
+
2386
+ def render_textarea(self, textarea: Textarea) -> str:
2387
+ """Renderiza un componente Textarea"""
2388
+ component_id = self.get_component_id(textarea, prefix="textarea")
2389
+ class_attr = f'class="dars-textarea {textarea.class_name or ""}"'
2390
+ style_attr = f'style="{self.render_styles(textarea.style)}"' if textarea.style else ""
2391
+ rows_attr = f'rows="{textarea.rows}"'
2392
+ cols_attr = f'cols="{textarea.cols}"'
2393
+ placeholder_attr = f'placeholder="{textarea.placeholder}"' if textarea.placeholder else ""
2394
+ disabled_attr = "disabled" if textarea.disabled else ""
2395
+ readonly_attr = "readonly" if textarea.readonly else ""
2396
+ required_attr = "required" if textarea.required else ""
2397
+ maxlength_attr = f'maxlength="{textarea.max_length}"' if textarea.max_length else ""
2398
+
2399
+ attrs = [class_attr, style_attr, rows_attr, cols_attr, placeholder_attr,
2400
+ disabled_attr, readonly_attr, required_attr, maxlength_attr]
2401
+ attrs_str = " ".join(attr for attr in attrs if attr)
2402
+
2403
+ return f'<textarea id="{component_id}" {attrs_str}>{textarea.value}</textarea>'
2404
+
2405
+ def render_card(self, card: Card) -> str:
2406
+ """Renderiza un componente Card"""
2407
+ component_id = self.get_component_id(card, prefix="card")
2408
+ class_attr = f'class="dars-card {card.class_name or ""}"'
2409
+ style_attr = f'style="{self.render_styles(card.style)}"' if card.style else ""
2410
+ title_html = f'<h2>{card.title}</h2>' if card.title else ""
2411
+ children_html = ""
2412
+ for child in card.children:
2413
+ children_html += self.render_component(child)
2414
+
2415
+ return f'<div id="{component_id}" {class_attr} {style_attr}>{title_html}{children_html}</div>'
2416
+
2417
+ def render_modal(self, modal: Modal) -> str:
2418
+ """Renderiza un componente Modal"""
2419
+ component_id = self.get_component_id(modal, prefix="modal")
2420
+ class_list = "dars-modal"
2421
+ if not modal.is_open:
2422
+ class_list += " dars-modal-hidden"
2423
+ if modal.class_name:
2424
+ class_list += f" {modal.class_name}"
2425
+ hidden_attr = " hidden" if not modal.is_open else ""
2426
+ display_style = "display: flex;" if modal.is_open else "display: none;"
2427
+ modal_style = f'{display_style} position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); justify-content: center; align-items: center; z-index: 1000;'
2428
+ if modal.style:
2429
+ modal_style += f' {self.render_styles(modal.style)}'
2430
+ data_enabled = f'data-enabled="{str(getattr(modal, "is_enabled", True)).lower()}"'
2431
+ title_html = f'<h2>{modal.title}</h2>' if modal.title else ""
2432
+ children_html = ""
2433
+ for child in modal.children:
2434
+ children_html += self.render_component(child)
2435
+ return (
2436
+ f'<div id="{component_id}" class="{class_list}" {data_enabled}{hidden_attr} style="{modal_style}">\n'
2437
+ f' <div class="dars-modal-content" style="background: white; padding: 20px; border-radius: 8px; max-width: 500px; width: 90%;">\n'
2438
+ f' {title_html}\n'
2439
+ f' {children_html}\n'
2440
+ f' </div>\n'
2441
+ f'</div>'
2442
+ )
2443
+
2444
+ def render_navbar(self, navbar: Navbar) -> str:
2445
+ """Renderiza un componente Navbar"""
2446
+ component_id = self.get_component_id(navbar, prefix="navbar")
2447
+ class_attr = f'class="dars-navbar {navbar.class_name or ""}"'
2448
+ style_attr = f'style="{self.render_styles(navbar.style)}"' if navbar.style else ""
2449
+ brand_html = f'<div class="dars-navbar-brand">{navbar.brand}</div>' if navbar.brand else ""
2450
+ # Soporta hijos como lista o *args (igual que Container)
2451
+ children = getattr(navbar, 'children', [])
2452
+ if callable(children):
2453
+ children = children()
2454
+ if children is None:
2455
+ children = []
2456
+ if not isinstance(children, (list, tuple)):
2457
+ children = [children]
2458
+ children_html = ""
2459
+ for child in children:
2460
+ children_html += self.render_component(child)
2461
+
2462
+ return f'<nav id="{component_id}" {class_attr} {style_attr}>{brand_html}<div class="dars-navbar-nav">{children_html}</div></nav>'
2463
+
2464
+ def render_checkbox(self, checkbox: Checkbox) -> str:
2465
+ """Renderiza un componente Checkbox"""
2466
+ component_id = self.get_component_id(checkbox, prefix="checkbox")
2467
+ class_attr = f'class="dars-checkbox {checkbox.class_name or ""}"'
2468
+ style_attr = f'style="{self.render_styles(checkbox.style)}"' if checkbox.style else ""
2469
+ checked_attr = "checked" if checkbox.checked else ""
2470
+ disabled_attr = "disabled" if checkbox.disabled else ""
2471
+ required_attr = "required" if checkbox.required else ""
2472
+ name_attr = f'name="{checkbox.name}"' if checkbox.name else ""
2473
+ value_attr = f'value="{checkbox.value}"' if checkbox.value else ""
2474
+
2475
+ attrs = [class_attr, style_attr, checked_attr, disabled_attr, required_attr, name_attr, value_attr]
2476
+ attrs_str = " ".join(attr for attr in attrs if attr)
2477
+
2478
+ label_html = f'<label for="{component_id}">{checkbox.label}</label>' if checkbox.label else ""
2479
+
2480
+ return f'<div class="dars-checkbox-wrapper"><input type="checkbox" id="{component_id}" {attrs_str}>{label_html}</div>'
2481
+
2482
+ def render_radiobutton(self, radio: RadioButton) -> str:
2483
+ """Renderiza un componente RadioButton"""
2484
+ component_id = self.get_component_id(radio, prefix="radiobutton")
2485
+ class_attr = f'class="dars-radio {radio.class_name or ""}"'
2486
+ style_attr = f'style="{self.render_styles(radio.style)}"' if radio.style else ""
2487
+ checked_attr = "checked" if radio.checked else ""
2488
+ disabled_attr = "disabled" if radio.disabled else ""
2489
+ required_attr = "required" if radio.required else ""
2490
+ name_attr = f'name="{radio.name}"'
2491
+ value_attr = f'value="{radio.value}"'
2492
+
2493
+ attrs = [class_attr, style_attr, checked_attr, disabled_attr, required_attr, name_attr, value_attr]
2494
+ attrs_str = " ".join(attr for attr in attrs if attr)
2495
+
2496
+ label_html = f'<label for="{component_id}">{radio.label}</label>' if radio.label else ""
2497
+
2498
+ return f'<div class="dars-radio-wrapper"><input type="radio" id="{component_id}" {attrs_str}>{label_html}</div>'
2499
+
2500
+ def render_select(self, select: Select) -> str:
2501
+ """Renderiza un componente Select"""
2502
+ component_id = self.get_component_id(select, prefix="select")
2503
+ class_attr = f'class="dars-select {select.class_name or ""}"'
2504
+ style_attr = f'style="{self.render_styles(select.style)}"' if select.style else ""
2505
+ disabled_attr = "disabled" if select.disabled else ""
2506
+ required_attr = "required" if select.required else ""
2507
+ multiple_attr = "multiple" if select.multiple else ""
2508
+ size_attr = f'size="{select.size}"' if select.size else ""
2509
+
2510
+ attrs = [class_attr, style_attr, disabled_attr, required_attr, multiple_attr, size_attr]
2511
+ attrs_str = " ".join(attr for attr in attrs if attr)
2512
+
2513
+ # Generar opciones
2514
+ options_html = ""
2515
+ if select.placeholder and not select.multiple:
2516
+ selected = "selected" if not select.value else ""
2517
+ options_html += f'<option value="" disabled {selected}>{select.placeholder}</option>'
2518
+
2519
+ for option in select.options:
2520
+ selected = "selected" if option.value == select.value else ""
2521
+ disabled = "disabled" if option.disabled else ""
2522
+ options_html += f'<option value="{option.value}" {selected} {disabled}>{option.label}</option>'
2523
+
2524
+ return f'<select id="{component_id}" {attrs_str}>{options_html}</select>'
2525
+
2526
+ def render_slider(self, slider: Slider) -> str:
2527
+ """Renderiza un componente Slider"""
2528
+ component_id = self.get_component_id(slider, prefix="slider")
2529
+ class_attr = f'class="dars-slider {slider.class_name or ""}"'
2530
+ style_attr = f'style="{self.render_styles(slider.style)}"' if slider.style else ""
2531
+ disabled_attr = "disabled" if slider.disabled else ""
2532
+ min_attr = f'min="{slider.min_value}"'
2533
+ max_attr = f'max="{slider.max_value}"'
2534
+ value_attr = f'value="{slider.value}"'
2535
+ step_attr = f'step="{slider.step}"'
2536
+
2537
+ attrs = [class_attr, style_attr, disabled_attr, min_attr, max_attr, value_attr, step_attr]
2538
+ attrs_str = " ".join(attr for attr in attrs if attr)
2539
+
2540
+ label_html = f'<label for="{component_id}">{slider.label}</label>' if slider.label else ""
2541
+ value_display = f'<span class="dars-slider-value">{slider.value}</span>' if slider.show_value else ""
2542
+
2543
+ wrapper_class = "dars-slider-vertical" if slider.orientation == "vertical" else "dars-slider-horizontal"
2544
+
2545
+ return f'<div class="dars-slider-wrapper {wrapper_class}">{label_html}<input type="range" id="{component_id}" {attrs_str}>{value_display}</div>'
2546
+
2547
+ def render_datepicker(self, datepicker: DatePicker) -> str:
2548
+ """Renderiza un componente DatePicker"""
2549
+ component_id = self.get_component_id(datepicker, prefix="datepicker")
2550
+ class_attr = f'class="dars-datepicker {datepicker.class_name or ""}"'
2551
+ style_attr = f'style="{self.render_styles(datepicker.style)}"' if datepicker.style else ""
2552
+ disabled_attr = "disabled" if datepicker.disabled else ""
2553
+ required_attr = "required" if datepicker.required else ""
2554
+ readonly_attr = "readonly" if datepicker.readonly else ""
2555
+ value_attr = f'value="{datepicker.value}"' if datepicker.value else ""
2556
+ placeholder_attr = f'placeholder="{datepicker.placeholder}"' if datepicker.placeholder else ""
2557
+ min_attr = f'min="{datepicker.min_date}"' if datepicker.min_date else ""
2558
+ max_attr = f'max="{datepicker.max_date}"' if datepicker.max_date else ""
2559
+
2560
+ # Determinar el tipo de input según si incluye tiempo
2561
+ input_type = "datetime-local" if datepicker.show_time else "date"
2562
+
2563
+ attrs = [class_attr, style_attr, disabled_attr, required_attr, readonly_attr,
2564
+ value_attr, placeholder_attr, min_attr, max_attr]
2565
+ attrs_str = " ".join(attr for attr in attrs if attr)
2566
+
2567
+ # Si es inline, usar un div contenedor adicional
2568
+ if datepicker.inline:
2569
+ return f'<div class="dars-datepicker-inline"><input type="{input_type}" id="{component_id}" {attrs_str}></div>'
2570
+ else:
2571
+ return f'<input type="{input_type}" id="{component_id}" {attrs_str}>'
2572
+
2573
+ def render_table(self, table: Table) -> str:
2574
+ # Renderizado HTML para Table
2575
+ thead = '<thead><tr>' + ''.join(f'<th>{col["title"]}</th>' for col in table.columns) + '</tr></thead>'
2576
+ rows = table.data[:table.page_size] if table.page_size else table.data
2577
+ tbody = '<tbody>' + ''.join(
2578
+ '<tr>' + ''.join(f'<td>{row.get(col["field"], "")}</td>' for col in table.columns) + '</tr>'
2579
+ for row in rows) + '</tbody>'
2580
+ return f'<table class="dars-table">{thead}{tbody}</table>'
2581
+
2582
+ def render_tabs(self, tabs: Tabs) -> str:
2583
+ tab_headers = ''.join(
2584
+ f'<button class="dars-tab{ " dars-tab-active" if i == tabs.selected else "" }" data-tab="{i}">{title}</button>'
2585
+ for i, title in enumerate(tabs.tabs)
2586
+ )
2587
+ panels_html = ''.join(
2588
+ f'<div class="dars-tab-panel{ " dars-tab-panel-active" if i == tabs.selected else "" }">{self.render_component(panel) if hasattr(panel, "render") else panel}</div>'
2589
+ for i, panel in enumerate(tabs.panels)
2590
+ )
2591
+ return f'<div class="dars-tabs"><div class="dars-tabs-header">{tab_headers}</div><div class="dars-tabs-panels">{panels_html}</div></div>'
2592
+
2593
+ def render_accordion(self, accordion: Accordion) -> str:
2594
+ html = '<div class="dars-accordion">'
2595
+ for i, (title, content) in enumerate(accordion.sections):
2596
+ opened = ' dars-accordion-open' if i in accordion.open_indices else ''
2597
+ html += f'<div class="dars-accordion-section{opened}"><div class="dars-accordion-title">{title}</div><div class="dars-accordion-content">{self.render_component(content) if hasattr(content, "render") else content}</div></div>'
2598
+ html += '</div>'
2599
+ return html
2600
+
2601
+ def render_progressbar(self, bar: ProgressBar) -> str:
2602
+ percent = min(max(bar.value / bar.max_value * 100, 0), 100)
2603
+ return f'<div class="dars-progressbar"><div class="dars-progressbar-bar" style="width: {percent}%;"></div></div>'
2604
+
2605
+ def render_spinner(self, spinner: Spinner) -> str:
2606
+ return '<div class="dars-spinner"></div>'
2607
+
2608
+ def render_tooltip(self, tooltip: Tooltip) -> str:
2609
+ return f'<div class="dars-tooltip dars-tooltip-{tooltip.position}">{self.render_component(tooltip.child) if hasattr(tooltip.child, "render") else tooltip.child}<span class="dars-tooltip-text">{tooltip.text}</span></div>'
2610
+
2611
+ def render_markdown(self, markdown: 'Markdown') -> str:
2612
+ """Render a Markdown component"""
2613
+ try:
2614
+ import markdown2
2615
+ # Convert markdown to HTML
2616
+ html_content = markdown2.markdown(
2617
+ markdown.content,
2618
+ extras=["fenced-code-blocks", "tables", "header-ids"]
2619
+ )
2620
+ except ImportError:
2621
+ # Fallback to basic conversion if markdown2 is not available
2622
+ html_content = self._basic_markdown_to_html(markdown.content)
2623
+
2624
+ component_id = self.get_component_id(markdown, prefix="markdown")
2625
+
2626
+ # Add dark theme class if enabled
2627
+ class_name = f"dars-markdown {markdown.class_name or ''}"
2628
+ if markdown.dark_theme:
2629
+ class_name += " dars-markdown-dark"
2630
+
2631
+ class_attr = f'class="{class_name.strip()}"'
2632
+ style_attr = f'style="{self.render_styles(markdown.style)}"' if markdown.style else ""
2633
+
2634
+ return f'<div id="{component_id}" {class_attr} {style_attr}>{html_content}</div>'
2635
+
2636
+ def _basic_markdown_to_html(self, markdown_text: str) -> str:
2637
+ """Basic markdown to HTML conversion as fallback"""
2638
+ if not markdown_text:
2639
+ return ""
2640
+
2641
+ html = markdown_text
2642
+
2643
+ # Basic replacements
2644
+ html = html.replace('**', '<strong>').replace('**', '</strong>')
2645
+ html = html.replace('*', '<em>').replace('*', '</em>')
2646
+ html = html.replace('__', '<strong>').replace('__', '</strong>')
2647
+ html = html.replace('_', '<em>').replace('_', '</em>')
2648
+
2649
+ # Headers
2650
+ html = html.replace('# ', '<h1>').replace('\n# ', '</h1>\n<h1>')
2651
+ html = html.replace('## ', '<h2>').replace('\n## ', '</h2>\n<h2>')
2652
+ html = html.replace('### ', '<h3>').replace('\n### ', '</h3>\n<h3>')
2653
+
2654
+ # Line breaks
2655
+ html = html.replace('\n\n', '<br><br>')
2656
+
2657
+ return html
2658
+ def render_generic_component(self, component: Component) -> str:
2659
+ """Renderiza un componente genérico con estructura básica"""
2660
+ component_id = self.get_component_id(component, prefix="comp")
2661
+ class_attr = f'class="{component.class_name or ""}"'
2662
+ style_attr = f'style="{self.render_styles(component.style)}"' if component.style else ""
2663
+
2664
+ # Renderizar hijos usando el exporter
2665
+ children_html = ""
2666
+ for child in component.children:
2667
+ children_html += self.render_component(child)
2668
+
2669
+ # Agregar eventos como data attributes para referencia
2670
+ events_attr = ""
2671
+ if component.events:
2672
+ for event_name in component.events:
2673
+ events_attr += f' data-event-{event_name}="true"'
2674
+
2675
+ return f'<div id="{component_id}" {class_attr} {style_attr}{events_attr}>{children_html}</div>'