dars-framework 1.2.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. dars/__init__.py +0 -0
  2. dars/all.py +69 -0
  3. dars/cli/__init__.py +0 -0
  4. dars/cli/doctor/__init__.py +1 -0
  5. dars/cli/doctor/detect.py +154 -0
  6. dars/cli/doctor/doctor.py +176 -0
  7. dars/cli/doctor/installers.py +100 -0
  8. dars/cli/doctor/persist.py +62 -0
  9. dars/cli/doctor/preflight.py +33 -0
  10. dars/cli/doctor/ui.py +54 -0
  11. dars/cli/hot_reload.py +33 -0
  12. dars/cli/main.py +1107 -0
  13. dars/cli/preview.py +448 -0
  14. dars/cli/translations.py +531 -0
  15. dars/components/__init__.py +0 -0
  16. dars/components/advanced/__init__.py +8 -0
  17. dars/components/advanced/accordion.py +26 -0
  18. dars/components/advanced/card.py +33 -0
  19. dars/components/advanced/modal.py +45 -0
  20. dars/components/advanced/navbar.py +44 -0
  21. dars/components/advanced/table.py +25 -0
  22. dars/components/advanced/tabs.py +31 -0
  23. dars/components/basic/__init__.py +34 -0
  24. dars/components/basic/button.py +55 -0
  25. dars/components/basic/checkbox.py +35 -0
  26. dars/components/basic/container.py +29 -0
  27. dars/components/basic/datepicker.py +139 -0
  28. dars/components/basic/image.py +36 -0
  29. dars/components/basic/input.py +57 -0
  30. dars/components/basic/link.py +31 -0
  31. dars/components/basic/markdown.py +86 -0
  32. dars/components/basic/page.py +20 -0
  33. dars/components/basic/progressbar.py +18 -0
  34. dars/components/basic/radiobutton.py +35 -0
  35. dars/components/basic/select.py +82 -0
  36. dars/components/basic/slider.py +63 -0
  37. dars/components/basic/spinner.py +12 -0
  38. dars/components/basic/text.py +23 -0
  39. dars/components/basic/textarea.py +46 -0
  40. dars/components/basic/tooltip.py +19 -0
  41. dars/components/layout/__init__.py +0 -0
  42. dars/components/layout/anchor.py +13 -0
  43. dars/components/layout/flex.py +26 -0
  44. dars/components/layout/grid.py +45 -0
  45. dars/config.py +134 -0
  46. dars/core/__init__.py +0 -0
  47. dars/core/app.py +957 -0
  48. dars/core/component.py +284 -0
  49. dars/core/events.py +102 -0
  50. dars/core/js_bridge.py +99 -0
  51. dars/core/properties.py +127 -0
  52. dars/core/state.py +309 -0
  53. dars/dars_tests/apps_test/health_check.py +56 -0
  54. dars/dars_tests/run_tests.py +275 -0
  55. dars/dars_tests/tests/test_advanced_components.py +69 -0
  56. dars/dars_tests/tests/test_basic_components.py +88 -0
  57. dars/dars_tests/tests/test_core_and_cli.py +17 -0
  58. dars/dars_tests/tests/test_layout_components.py +58 -0
  59. dars/dars_tests/tests/test_version_check.py +21 -0
  60. dars/docs/__init__.py +0 -0
  61. dars/docs/app.md +290 -0
  62. dars/docs/cli.md +80 -0
  63. dars/docs/components.md +1679 -0
  64. dars/docs/custom_components.md +30 -0
  65. dars/docs/events.md +45 -0
  66. dars/docs/exporters.md +162 -0
  67. dars/docs/getting_started.md +79 -0
  68. dars/docs/index.md +18 -0
  69. dars/docs/scripts.md +593 -0
  70. dars/docs/state_management.md +57 -0
  71. dars/exporters/__init__.py +0 -0
  72. dars/exporters/base.py +96 -0
  73. dars/exporters/web/OLD/html_css_js_OLD4.py +1538 -0
  74. dars/exporters/web/OLD/html_css_js_old.py +1406 -0
  75. dars/exporters/web/OLD/html_css_js_old2.py +1406 -0
  76. dars/exporters/web/__init__.py +0 -0
  77. dars/exporters/web/html_css_js.py +2675 -0
  78. dars/exporters/web/vdom.py +251 -0
  79. dars/js_lib.py +206 -0
  80. dars/scripts/__init__.py +0 -0
  81. dars/scripts/dscript.py +26 -0
  82. dars/scripts/script.py +39 -0
  83. dars/security.py +195 -0
  84. dars/templates/__init__.py +0 -0
  85. dars/templates/__pycache__/__init__.cpython-311.pyc +0 -0
  86. dars/templates/examples/README.md +4 -0
  87. dars/templates/examples/__pycache__/dynamic_event_demo.cpython-311.pyc +0 -0
  88. dars/templates/examples/advanced/Modal_Demo/advanced_modal_demo.py +275 -0
  89. dars/templates/examples/advanced/SimpleDashboard/dashboard.py +437 -0
  90. dars/templates/examples/advanced/SimpleModermWeb/modern_web_app.py +452 -0
  91. dars/templates/examples/advanced/VariousComponents/all_components_demo.py +87 -0
  92. dars/templates/examples/advanced/__init__.py +0 -0
  93. dars/templates/examples/advanced/dState/state_mods_demo.py +68 -0
  94. dars/templates/examples/basic/Forms/form_components.py +516 -0
  95. dars/templates/examples/basic/Forms/simple_form.py +379 -0
  96. dars/templates/examples/basic/HelloWorld/hello_world.py +56 -0
  97. dars/templates/examples/basic/Layouts/flex_layout_responsive.py +13 -0
  98. dars/templates/examples/basic/Layouts/grid_layout_responsive.py +12 -0
  99. dars/templates/examples/basic/Layouts/layout_multipage_demo.py +23 -0
  100. dars/templates/examples/basic/Multipage/multipage_example.py +67 -0
  101. dars/templates/examples/basic/PWA/icon-192x192.png +0 -0
  102. dars/templates/examples/basic/PWA/icon-512x512.png +0 -0
  103. dars/templates/examples/basic/PWA/pwa_custom_icons.py +33 -0
  104. dars/templates/examples/basic/__init__.py +0 -0
  105. dars/templates/examples/demo/__pycache__/complete_app.cpython-311.pyc +0 -0
  106. dars/templates/examples/demo/complete_app.py +21 -0
  107. dars/templates/examples/markdown/MarkdownTemplate/README.md +159 -0
  108. dars/templates/examples/markdown/MarkdownTemplate/markdown_template.py +21 -0
  109. dars/templates/examples/markdown/MarkdownTemplate/other_docs.md +1 -0
  110. dars/templates/examples/markdown/__init__.py +0 -0
  111. dars/templates/html/__init__.py +0 -0
  112. dars/version.py +2 -0
  113. dars_framework-1.2.3.dist-info/METADATA +15 -0
  114. dars_framework-1.2.3.dist-info/RECORD +118 -0
  115. dars_framework-1.2.3.dist-info/WHEEL +5 -0
  116. dars_framework-1.2.3.dist-info/entry_points.txt +2 -0
  117. dars_framework-1.2.3.dist-info/licenses/LICENSE +21 -0
  118. dars_framework-1.2.3.dist-info/top_level.txt +1 -0
dars/core/app.py ADDED
@@ -0,0 +1,957 @@
1
+ from typing import Optional, List, Dict, Any
2
+
3
+ from dars.exporters.base import Exporter
4
+ from dars.scripts.script import Script
5
+ from .component import Component
6
+ from .events import EventManager
7
+
8
+ class Page:
9
+ """Represents an individual page in the Dars app (multipage)."""
10
+ def __init__(self, name: str, root: 'Component', title: str = None, meta: dict = None, index: bool = False, scripts: Optional[List[Any]] = None):
11
+ self.name = name # slug o nombre de la página
12
+ self.root = root # componente raíz de la página
13
+ self.title = title
14
+ self.meta = meta or {}
15
+ self.index = index # ¿Es la página principal?
16
+ self.scripts: List[Any] = list(scripts) if scripts else []
17
+
18
+ def attr(self, **attrs):
19
+ """Setter/getter for Page attributes, similar to Component.attr().
20
+ If kwargs are provided, sets attributes; otherwise, returns a dict with the editable attributes."""
21
+
22
+ if attrs:
23
+ for key, value in attrs.items():
24
+ if hasattr(self, key):
25
+ setattr(self, key, value)
26
+ else:
27
+ self.meta[key] = value
28
+ return self
29
+ # Getter
30
+ d = dict(self.meta)
31
+ d['name'] = self.name
32
+ d['root'] = self.root
33
+ d['title'] = self.title
34
+ d['index'] = self.index
35
+ d['scripts'] = list(self.scripts)
36
+ return d
37
+ # -----------------------------
38
+ # Métodos para manejar scripts
39
+ # -----------------------------
40
+ def add_script(self, script: Any):
41
+ """Adds a script to this page.
42
+ - If 'script' is an instance (e.g., InlineScript/FileScript/DScript), it is added as is.
43
+ - If 'script' is a string, it is interpreted as an InlineScript (code).
44
+ - If 'script' is a dict, it is added as is (fallback).
45
+ Returns self to allow call chaining."""
46
+
47
+ # si es str => interpretarlo como inline
48
+ if isinstance(script, str):
49
+ created = self._make_inline_script(script)
50
+ self.scripts.append(created)
51
+ return self
52
+
53
+ # si es dict => fallback, guardarlo
54
+ if isinstance(script, dict):
55
+ self.scripts.append(script)
56
+ return self
57
+
58
+ # si ya es una instancia de "Script" (no podemos verificar tipo concreto sin dependencia),
59
+ # asumimos que es un script válido y lo añadimos.
60
+ self.scripts.append(script)
61
+ return self
62
+
63
+ # alias corto (pedido)
64
+ def addscript(self, script: Any):
65
+ return self.add_script(script)
66
+
67
+ def add_inline_script(self, code: str, **kwargs):
68
+ """Convenience: adds an InlineScript to the page (code = JS or similar)."""
69
+ s = self._make_inline_script(code, **kwargs)
70
+ self.scripts.append(s)
71
+ return self
72
+
73
+ def add_file_script(self, path: str, **kwargs):
74
+ """Convenience: adds a FileScript (reference to a .js/.ts/etc. file)."""
75
+ s = self._make_file_script(path, **kwargs)
76
+ self.scripts.append(s)
77
+ return self
78
+
79
+ def add_dscript(self, obj: Any, **kwargs):
80
+ """Convenience: attempts to create/add a DScript (if the class exists)."""
81
+ s = self._make_dscript(obj, **kwargs)
82
+ self.scripts.append(s)
83
+ return self
84
+
85
+ def get_scripts(self) -> List[Any]:
86
+ """Returns the list of scripts added to the page."""
87
+ return list(self.scripts)
88
+
89
+ # -----------------------------
90
+ # Helpers para construcción segura
91
+ # -----------------------------
92
+ def _make_inline_script(self, code: str, **kwargs) -> Any:
93
+ """Attempts to create an InlineScript instance if it exists in dars.scripts.*.
94
+ Otherwise, returns a fallback dict: {'type': 'inline', 'code': ..., **kwargs}"""
95
+
96
+ try:
97
+ # intentamos import común (ajusta según tu layout de módulos si hace falta)
98
+ from dars.scripts.script import InlineScript # type: ignore
99
+ return InlineScript(code, **kwargs)
100
+ except Exception:
101
+ try:
102
+ from dars.scripts.script import InlineScript # type: ignore
103
+ return InlineScript(code, **kwargs)
104
+ except Exception:
105
+ # fallback: dict simple que contiene lo mínimo
106
+ return {'type': 'inline', 'code': code, **kwargs}
107
+
108
+ def _make_file_script(self, path: str, **kwargs) -> Any:
109
+ """Attempts to create a FileScript instance if it exists. Otherwise, returns a fallback dict."""
110
+
111
+ try:
112
+ from dars.scripts.script import FileScript # type: ignore
113
+ return FileScript(path, **kwargs)
114
+ except Exception:
115
+ try:
116
+ from dars.scripts.script import FileScript # type: ignore
117
+ return FileScript(path, **kwargs)
118
+ except Exception:
119
+ return {'type': 'file', 'path': path, **kwargs}
120
+
121
+ def _make_dscript(self, obj: Any, **kwargs) -> Any:
122
+ """Attempts to create a DScript instance if it exists. Otherwise, stores the object with a marker."""
123
+ try:
124
+ from dars.scripts.dscript import dScript # type: ignore
125
+ return dScript(obj, **kwargs)
126
+ except Exception:
127
+ # si ya es dict o similar, solo anotamos el tipo
128
+ return {'type': 'dscript', 'value': obj, **kwargs}
129
+
130
+ class App:
131
+ """Main class that represents a Dars application"""
132
+
133
+ def rTimeCompile(self, exporter=None, port=None, add_file_types=".py, .js, .css", watchfiledialog=False):
134
+ """
135
+ Generates a quick preview of the app on a local server using an exporter
136
+ (default: HTMLCSSJSExporter) and serving the files from a temporary directory.
137
+ Does not open the browser automatically. The server stops with Ctrl+C.
138
+ You can pass the port as a command-line argument: python main.py --port 8080
139
+ """
140
+ import threading
141
+ import time
142
+ import sys
143
+ import os
144
+ import inspect
145
+ import importlib.util
146
+ from pathlib import Path
147
+ from contextlib import contextmanager
148
+ import shutil
149
+ import traceback
150
+
151
+ self.watchfiledialog = watchfiledialog
152
+
153
+ @contextmanager
154
+ def pushd(path):
155
+ """Cambia temporalmente el cwd y lo restaura al salir."""
156
+ old = os.getcwd()
157
+ os.chdir(path)
158
+ try:
159
+ yield
160
+ finally:
161
+ os.chdir(old)
162
+
163
+ # Rich para mensajes bonitos
164
+ try:
165
+ from rich.console import Console
166
+ from rich.panel import Panel
167
+ from rich.text import Text
168
+ except ImportError:
169
+ Console = None
170
+ console = Console() if 'Console' in locals() else None
171
+
172
+ # Leer puerto de sys.argv si no se pasa explícito
173
+ if port is None:
174
+ port = 8000
175
+ for i, arg in enumerate(sys.argv):
176
+ if arg in ('--port', '-p') and i + 1 < len(sys.argv):
177
+ try:
178
+ port = int(sys.argv[i + 1])
179
+ except Exception:
180
+ pass
181
+ # --- Normalizar add_file_types => lista de extensiones que empiezan con '.' ---
182
+ def _normalize_exts(exts):
183
+ if not exts:
184
+ return ['.py']
185
+ # aceptar string con comas
186
+ if isinstance(exts, str):
187
+ parts = [p.strip() for p in exts.split(',') if p.strip()]
188
+ elif isinstance(exts, (list, tuple, set)):
189
+ parts = [str(p).strip() for p in exts if p]
190
+ else:
191
+ parts = [str(exts).strip()]
192
+
193
+ normalized = []
194
+ for p in parts:
195
+ if not p:
196
+ continue
197
+ if not p.startswith('.'):
198
+ p = '.' + p
199
+ normalized.append(p.lower())
200
+ # siempre incluir .py (comportamiento: .py + los adicionales)
201
+ if '.py' not in normalized:
202
+ normalized.insert(0, '.py')
203
+ # eliminar duplicados preservando orden
204
+ seen = set()
205
+ result = []
206
+ for e in normalized:
207
+ if e not in seen:
208
+ seen.add(e)
209
+ result.append(e)
210
+ return result
211
+
212
+ # Lista final de extensiones a vigilar (ej: ['.py', '.js', '.css'])
213
+ watch_exts = _normalize_exts(add_file_types)
214
+
215
+ # Importar exportador por defecto si no se pasa
216
+ if exporter is None:
217
+ try:
218
+ from dars.exporters.web.html_css_js import HTMLCSSJSExporter
219
+ except ImportError:
220
+ print("Could not import HTMLCSSJSExporter")
221
+ return
222
+ exporter = HTMLCSSJSExporter()
223
+
224
+ # Importar PreviewServer
225
+ try:
226
+ from dars.cli.preview import PreviewServer
227
+ except ImportError:
228
+ print("Could not import PreviewServer")
229
+ return
230
+
231
+ shutdown_event = threading.Event()
232
+ watchers = [] # aquí guardaremos todos los watchers
233
+
234
+ # Debounce / lock para evitar reloads concurrentes
235
+ reload_lock = threading.Lock()
236
+ last_reload_at = 0.0
237
+ MIN_RELOAD_INTERVAL = 0.4 # segundos
238
+
239
+ try:
240
+ # Detectar archivo principal de la app (el que ejecutaste con `python archivo.py`)
241
+ app_file = None
242
+ for frame in inspect.stack():
243
+ if frame.function == "<module>":
244
+ app_file = frame.filename
245
+ break
246
+ if not app_file:
247
+ app_file = sys.argv[0]
248
+
249
+ project_root = os.path.dirname(os.path.abspath(app_file))
250
+ if project_root not in sys.path:
251
+ sys.path.insert(0, project_root)
252
+
253
+ preview_dir = os.path.join(project_root, "dars_preview")
254
+ cwd_original = os.getcwd()
255
+
256
+ # limpiar preview anterior
257
+ if os.path.exists(preview_dir):
258
+ try:
259
+ shutil.rmtree(preview_dir)
260
+ except Exception as e:
261
+ msg = f"Warning: Could not clean previous preview directory: {e}"
262
+ console.print(f"[yellow]{msg}[/yellow]") if console else print(msg)
263
+
264
+ os.makedirs(preview_dir, exist_ok=True)
265
+
266
+ # Advertir si no hay archivo de configuración
267
+ try:
268
+ from dars.config import load_config
269
+ cfg, cfg_found = load_config(project_root)
270
+ except Exception:
271
+ cfg, cfg_found = ({}, False)
272
+ if not cfg_found:
273
+ warn_msg = "[Dars] Warning: dars.config.json not found. Run 'dars init --update' to create it in existing projects."
274
+ if console:
275
+ console.print(f"[yellow]{warn_msg}[/yellow]")
276
+ else:
277
+ print(warn_msg)
278
+
279
+ # export inicial desde el root usando la instancia actual (self)
280
+ with pushd(project_root):
281
+ exporter.export(self, preview_dir, bundle=False)
282
+
283
+ url = f"http://localhost:{port}"
284
+ app_title = getattr(self, 'title', 'Dars App')
285
+ if console:
286
+ panel = Panel(
287
+ Text(f"✔ App running successfully\n\nName: {app_title}\nPreview available at: {url}\n\nPress Ctrl+C to stop the server.",
288
+ style="bold green", justify="center"),
289
+ title="Dars Preview", border_style="cyan")
290
+ console.print(panel)
291
+ else:
292
+ print(f"[Dars] App '{app_title}' running. Preview at {url}")
293
+
294
+ server = PreviewServer(preview_dir, port)
295
+ try:
296
+ if not server.start():
297
+ (console.print("[red]Could not start preview server.[/red]")
298
+ if console else print("Could not start preview server."))
299
+ return
300
+
301
+ # --- HOT RELOAD ---
302
+ from dars.cli.hot_reload import FileWatcher
303
+
304
+ def _collect_project_files_by_ext(root, exts):
305
+ files = []
306
+ for dirpath, dirnames, filenames in os.walk(root):
307
+ # excluir preview_dir, .git y __pycache__
308
+ if os.path.abspath(dirpath).startswith(os.path.abspath(preview_dir)):
309
+ continue
310
+ if '.git' in dirpath or '__pycache__' in dirpath:
311
+ continue
312
+ for fname in filenames:
313
+ for ext in exts:
314
+ if fname.lower().endswith(ext):
315
+ files.append(os.path.join(dirpath, fname))
316
+ break
317
+ return files
318
+
319
+
320
+ def reload_and_export(changed_file=None):
321
+ nonlocal last_reload_at
322
+ now = time.time()
323
+ # debounce rápido
324
+ if now - last_reload_at < MIN_RELOAD_INTERVAL:
325
+ return
326
+ with reload_lock:
327
+ last_reload_at = time.time()
328
+ if console:
329
+ console.print(f"[yellow]Detected change in {changed_file}. Reloading...[/yellow]")
330
+ else:
331
+ print(f"[Dars] Detected change in {changed_file}. Reloading...")
332
+
333
+ try:
334
+ if project_root not in sys.path:
335
+ sys.path.insert(0, project_root)
336
+
337
+ with pushd(project_root):
338
+ # --- Limpiar del cache todos los módulos que pertenecen al proyecto ---
339
+ to_remove = []
340
+ for name, mod in list(sys.modules.items()):
341
+ try:
342
+ mod_file = getattr(mod, '__file__', None)
343
+ if not mod_file:
344
+ continue
345
+ # normalizar paths
346
+ mod_file_abs = os.path.abspath(mod_file)
347
+ if mod_file_abs.startswith(os.path.abspath(project_root)):
348
+ to_remove.append(name)
349
+ except Exception:
350
+ continue
351
+
352
+ for name in to_remove:
353
+ try:
354
+ del sys.modules[name]
355
+ except Exception:
356
+ pass
357
+
358
+ # también borrar cualquier nombre temporal 'dars_app' si existiese
359
+ sys.modules.pop("dars_app", None)
360
+
361
+ # Importar el archivo principal en un nombre único (para limpieza segura)
362
+ unique_name = f"dars_app_reload_{int(time.time()*1000)}"
363
+ spec = importlib.util.spec_from_file_location(unique_name, app_file)
364
+ module = importlib.util.module_from_spec(spec)
365
+ spec.loader.exec_module(module)
366
+
367
+ # Buscar nueva instancia App en el módulo recargado
368
+ new_app = None
369
+ for v in vars(module).values():
370
+ try:
371
+ if isinstance(v, App):
372
+ new_app = v
373
+ break
374
+ except Exception:
375
+ # si isinstance falla por alguna razón, ignorar
376
+ pass
377
+
378
+ # fallback por nombre de clase (por si App es distinto objeto)
379
+ if not new_app:
380
+ for v in vars(module).values():
381
+ try:
382
+ if hasattr(v, '__class__') and v.__class__.__name__ == 'App':
383
+ new_app = v
384
+ break
385
+ except Exception:
386
+ pass
387
+
388
+ if not new_app:
389
+ (console.print("[red]No App instance found after reload.")
390
+ if console else print("[Dars] No App instance found after reload."))
391
+ return
392
+
393
+ # Exportar la nueva instancia
394
+ exporter.export(new_app, preview_dir, bundle=False)
395
+
396
+ (console.print("[green]App reloaded and re-exported successfully.[/green]")
397
+ if console else print("[Dars] App reloaded and re-exported successfully."))
398
+
399
+ except Exception as e:
400
+ tb = traceback.format_exc()
401
+ (console.print(f"[red]Hot reload failed: {e}\n{tb}[/red]")
402
+ if console else print(f"[Dars] Hot reload failed: {e}\n{tb}"))
403
+
404
+ # --- Crear watchers para todos los archivos .py dentro del proyecto (recursivo) ---
405
+ files_to_watch = _collect_project_files_by_ext(project_root, watch_exts)
406
+
407
+
408
+ # Si no hay archivos detectados (raro), al menos mirar app_file
409
+ if not files_to_watch:
410
+ files_to_watch = [app_file]
411
+
412
+ for f in files_to_watch:
413
+ try:
414
+ # FileWatcher espera una función sin argumentos; usamos lambda que captura f
415
+ w = FileWatcher(f, lambda f=f: reload_and_export(f))
416
+ w.start()
417
+ watchers.append(w)
418
+ except Exception as e:
419
+ if console:
420
+ console.print(f"[yellow]Warning: could not watch {f}: {e}[/yellow]")
421
+ else:
422
+ print(f"[Dars] Warning: could not watch {f}: {e}")
423
+
424
+ if console:
425
+ # Mostrar rutas relativas para que no sea tan largo
426
+ rel_paths = [os.path.relpath(f, project_root) for f in files_to_watch]
427
+ max_show = 80 # número máximo de líneas a mostrar
428
+ if len(rel_paths) > max_show:
429
+ shown = rel_paths[:max_show]
430
+ shown.append(f"... (+{len(rel_paths)-max_show} más)")
431
+ else:
432
+ shown = rel_paths or ["(ninguno)"]
433
+
434
+ from rich.table import Table
435
+ table = Table(show_header=False, box=None, padding=0)
436
+ table.add_column("Files", style="bold")
437
+ for p in shown:
438
+ table.add_row(p)
439
+
440
+ panel = Panel(
441
+ table,
442
+ title=f"Watching {len(files_to_watch)} files · Exts: {', '.join(watch_exts)}",
443
+ subtitle=f"Project root: {os.path.basename(project_root)}",
444
+ border_style="magenta"
445
+ )
446
+ if self.watchfiledialog:
447
+ console.print(panel)
448
+ else:
449
+ if self.watchfiledialog:
450
+ print(f"[Dars] Watching {len(files_to_watch)} files in {project_root}:")
451
+ for f in files_to_watch:
452
+ print(" -", os.path.relpath(f, project_root))
453
+
454
+ # Loop principal: espera a Ctrl+C
455
+ while not shutdown_event.is_set():
456
+ shutdown_event.wait(timeout=1) # Espera sin consumir CPU
457
+
458
+ except KeyboardInterrupt:
459
+ shutdown_event.set()
460
+ for w in watchers:
461
+ try:
462
+ w.stop()
463
+ except Exception:
464
+ pass
465
+ (console.print("\n[cyan]Stopping preview and watcher...[/cyan]")
466
+ if console else print("\n[Dars] Stopping preview and watcher..."))
467
+ finally:
468
+ # Detener watchers y servidor
469
+ try:
470
+ server.stop()
471
+ except Exception:
472
+ pass
473
+ for w in watchers:
474
+ try:
475
+ w.stop()
476
+ except Exception:
477
+ pass
478
+ (console.print("[green]Preview stopped.[/green]")
479
+ if console else print("[Dars] Preview stopped."))
480
+
481
+ except PermissionError as e:
482
+ msg = f"Warning: Could not clean temp directory due to permissions: {e}"
483
+ console.print(f"[yellow]{msg}[/yellow]") if console else print(msg)
484
+ except Exception as e:
485
+ msg = f"Unexpected error in fast preview: {e}\n{traceback.format_exc()}"
486
+ console.print(f"[red]{msg}[/red]") if console else print(msg)
487
+ finally:
488
+ # Restaurar cwd y limpiar preview
489
+ try:
490
+ os.chdir(cwd_original)
491
+ except Exception:
492
+ pass
493
+ try:
494
+ shutil.rmtree(preview_dir)
495
+ (console.print("[yellow]Preview files deleted.[/yellow]")
496
+ if console else print("Preview files deleted."))
497
+ except Exception as e:
498
+ msg = f"Could not delete preview directory: {e}"
499
+ console.print(f"[red]{msg}[/red]") if console else print(msg)
500
+
501
+
502
+ def __init__(
503
+ self,
504
+ title: str = "Dars App",
505
+ description: str = "",
506
+ author: str = "",
507
+ keywords: List[str] = None,
508
+ language: str = "en",
509
+ favicon: str = "",
510
+ icon: str = "",
511
+ apple_touch_icon: str = "",
512
+ manifest: str = "",
513
+ theme_color: str = "#000000",
514
+ background_color: str = "#ffffff",
515
+ service_worker_path: str = "",
516
+ service_worker_enabled: bool = False,
517
+ **config
518
+ ):
519
+ # Propiedades básicas de la aplicación
520
+ self.title = title
521
+ self.description = description
522
+ self.author = author
523
+ self.keywords = keywords or []
524
+ self.language = language
525
+
526
+ # Iconos y favicon
527
+ self.favicon = favicon
528
+ self.icon = icon # Para PWA y meta tags
529
+ self.apple_touch_icon = apple_touch_icon
530
+ self.manifest = manifest # Para PWA manifest.json
531
+
532
+ # Colores para PWA y tema
533
+ self.icons = config.get('icons', [])
534
+ self.theme_color = theme_color
535
+ self.background_color = background_color
536
+ self.service_worker_path = service_worker_path
537
+ self.service_worker_enabled = service_worker_enabled
538
+
539
+ # Propiedades Open Graph (para redes sociales)
540
+
541
+ #
542
+ # [RECOMENDACIÓN DARS]
543
+ # Para lanzar la compilación/preview rápido de tu app, añade al final de tu archivo principal:
544
+ # if __name__ == "__main__":
545
+ # app.rTimeCompile() # o app.timeCompile()
546
+ # Así tendrás preview instantáneo y control explícito, sin efectos colaterales.
547
+ #
548
+ self.og_title = config.get('og_title', title)
549
+ self.og_description = config.get('og_description', description)
550
+ self.og_image = config.get('og_image', '')
551
+ self.og_url = config.get('og_url', '')
552
+ self.og_type = config.get('og_type', 'website')
553
+ self.og_site_name = config.get('og_site_name', '')
554
+
555
+ # Twitter Cards
556
+ self.twitter_card = config.get('twitter_card', 'summary')
557
+ self.twitter_site = config.get('twitter_site', '')
558
+ self.twitter_creator = config.get('twitter_creator', '')
559
+
560
+ # SEO y robots
561
+ self.robots = config.get('robots', 'index, follow')
562
+ self.canonical_url = config.get('canonical_url', '')
563
+
564
+ # PWA configuración
565
+ self.pwa_enabled = config.get('pwa_enabled', False)
566
+ self.pwa_name = config.get('pwa_name', title)
567
+ self.pwa_short_name = config.get('pwa_short_name', title[:12])
568
+ self.pwa_display = config.get('pwa_display', 'standalone')
569
+ self.pwa_orientation = config.get('pwa_orientation', 'portrait')
570
+
571
+ # Propiedades del framework
572
+ self.root: Optional[Component] = None # Single-page mode
573
+ self._pages: Dict[str, Page] = {} # Multipage mode
574
+ self._index_page: str = None # Nombre de la página principal (si existe)
575
+ self.scripts: List['Script'] = []
576
+ self.global_styles: Dict[str, Any] = {}
577
+ self.global_style_files: List[str] = []
578
+ self.event_manager = EventManager()
579
+ self.config = config
580
+
581
+ # Configuración por defecto
582
+ self.config.setdefault('viewport', {
583
+ 'width': 'device-width',
584
+ 'initial_scale': 1.0,
585
+ 'user_scalable': 'yes'
586
+ })
587
+ self.config.setdefault('theme', 'light')
588
+ self.config.setdefault('responsive', True)
589
+ self.config.setdefault('charset', 'UTF-8')
590
+
591
+ def set_root(self, component: Component):
592
+ """Sets the root component of the application (backward-compatible single-page mode)."""
593
+ self.root = component
594
+
595
+ def add_page(self, name: str, root: 'Component', title: str = None, meta: dict = None, index: bool = False):
596
+ """
597
+ Adds a multipage page to the app.
598
+ `name` is the slug/key, `root` the root component.
599
+ If `index=True`, this page will be the main one (exported as index.html).
600
+ If multiple pages have `index=True`, the last registered one will be the main page.
601
+ """
602
+ if name in self._pages:
603
+ raise ValueError(f"Page already exists with this name: '{name}'")
604
+ self._pages[name] = Page(name, root, title, meta, index=index)
605
+ if index:
606
+ self._index_page = name
607
+
608
+
609
+ def get_page(self, name: str) -> 'Page':
610
+ """Obtain one registered page by name."""
611
+ return self._pages.get(name)
612
+
613
+ def get_index_page(self) -> 'Page':
614
+ """
615
+ Returns the index page, or the first one if none has index=True.
616
+ """
617
+ # Prioridad: explícita, luego la primera
618
+ if hasattr(self, '_index_page') and self._index_page and self._index_page in self._pages:
619
+ return self._pages[self._index_page]
620
+ for page in self._pages.values():
621
+ if getattr(page, 'index', False):
622
+ return page
623
+ # Si ninguna marcada, devolver la primera
624
+ if self._pages:
625
+ return list(self._pages.values())[0]
626
+ return None
627
+
628
+
629
+ @property
630
+ def pages(self) -> Dict[str, 'Page']:
631
+ """Returns the registered pages dictionary (multipage)."""
632
+ return self._pages
633
+
634
+ def is_multipage(self) -> bool:
635
+ """Indicate if the app is in multipage mode."""
636
+ return bool(self._pages)
637
+
638
+ def add_script(self, script: 'Script'):
639
+ """Adds a script to the app"""
640
+ self.scripts.append(script)
641
+
642
+ def add_global_style(self, selector: str = None, styles: Dict[str, Any] = None, file_path: str = None):
643
+ """
644
+ Adds a global style to the app.
645
+
646
+ - If file_path is provided, the CSS file is read and stored.
647
+ - If selector and styles are provided, they are stored as inline CSS rules.
648
+ - It is invalid to mix file_path with selector/styles.
649
+ """
650
+ if file_path:
651
+ if selector or styles:
652
+ raise ValueError("Cannot use selector/styles when file_path is provided.")
653
+ if file_path not in self.global_style_files:
654
+ self.global_style_files.append(file_path)
655
+ return self
656
+
657
+ if not selector or not styles:
658
+ raise ValueError("Must provide selector and styles when file_path is not used.")
659
+
660
+ self.global_styles[selector] = styles
661
+ return self
662
+
663
+ def set_theme(self, theme: str):
664
+ """Set the theme for the app"""
665
+ self.config['theme'] = theme
666
+
667
+ def set_favicon(self, favicon_path: str):
668
+ """Set the favicon for the app"""
669
+ self.favicon = favicon_path
670
+
671
+ def set_icon(self, icon_path: str):
672
+ """Set the principal icon for the app"""
673
+ self.icon = icon_path
674
+
675
+ def set_apple_touch_icon(self, icon_path: str):
676
+ """Set de icon for apple devices"""
677
+ self.apple_touch_icon = icon_path
678
+
679
+ def set_manifest(self, manifest_path: str):
680
+ """Set the manifes for PWA"""
681
+ self.manifest = manifest_path
682
+
683
+ def add_keyword(self, keyword: str):
684
+ """Add a keyword for SEO"""
685
+ if keyword not in self.keywords:
686
+ self.keywords.append(keyword)
687
+
688
+ def add_keywords(self, keywords: List[str]):
689
+ """Add multiple keywords for SEO"""
690
+ for keyword in keywords:
691
+ self.add_keyword(keyword)
692
+
693
+ def set_open_graph(self, **og_data):
694
+ """Configure properties of Open Graph for social media sharing"""
695
+ if 'title' in og_data:
696
+ self.og_title = og_data['title']
697
+ if 'description' in og_data:
698
+ self.og_description = og_data['description']
699
+ if 'image' in og_data:
700
+ self.og_image = og_data['image']
701
+ if 'url' in og_data:
702
+ self.og_url = og_data['url']
703
+ if 'type' in og_data:
704
+ self.og_type = og_data['type']
705
+ if 'site_name' in og_data:
706
+ self.og_site_name = og_data['site_name']
707
+
708
+ def set_twitter_card(self, card_type: str = 'summary', site: str = '', creator: str = ''):
709
+ """Set the Twitter Card meta tags"""
710
+ self.twitter_card = card_type
711
+ if site:
712
+ self.twitter_site = site
713
+ if creator:
714
+ self.twitter_creator = creator
715
+
716
+ def enable_pwa(self, name: str = None, short_name: str = None, display: str = 'standalone'):
717
+ """Enable PWA settings (Progressive Web App)"""
718
+ self.pwa_enabled = True
719
+ if name:
720
+ self.pwa_name = name
721
+ if short_name:
722
+ self.pwa_short_name = short_name
723
+ self.pwa_display = display
724
+
725
+ def set_theme_colors(self, theme_color: str, background_color: str = None):
726
+ """Select the theme color of the PWA theme and browsers themes """
727
+ self.theme_color = theme_color
728
+ if background_color:
729
+ self.background_color = background_color
730
+
731
+ def get_meta_tags(self) -> Dict[str, str]:
732
+ """Obtain all tags of as a dictionary"""
733
+ meta_tags = {}
734
+
735
+ # Meta tags básicos
736
+ if self.description:
737
+ meta_tags['description'] = self.description
738
+ if self.author:
739
+ meta_tags['author'] = self.author
740
+ if self.keywords:
741
+ meta_tags['keywords'] = ', '.join(self.keywords)
742
+ if self.robots:
743
+ meta_tags['robots'] = self.robots
744
+
745
+ # Viewport
746
+ viewport_parts = []
747
+ for key, value in self.config['viewport'].items():
748
+ if key == 'initial_scale':
749
+ viewport_parts.append(f'initial-scale={value}')
750
+ elif key == 'user_scalable':
751
+ viewport_parts.append(f'user-scalable={value}')
752
+ else:
753
+ viewport_parts.append(f'{key.replace("_", "-")}={value}')
754
+ meta_tags['viewport'] = ', '.join(viewport_parts)
755
+
756
+ # PWA y tema
757
+ meta_tags['theme-color'] = self.theme_color
758
+ if self.pwa_enabled:
759
+ meta_tags['mobile-web-app-capable'] = 'yes'
760
+ meta_tags['apple-mobile-web-app-capable'] = 'yes'
761
+ meta_tags['apple-mobile-web-app-status-bar-style'] = 'default'
762
+ meta_tags['apple-mobile-web-app-title'] = self.pwa_short_name
763
+
764
+ return meta_tags
765
+
766
+ def get_open_graph_tags(self) -> Dict[str, str]:
767
+ """ Obtain all tags of Open Graph"""
768
+ og_tags = {}
769
+
770
+ if self.og_title:
771
+ og_tags['og:title'] = self.og_title
772
+ if self.og_description:
773
+ og_tags['og:description'] = self.og_description
774
+ if self.og_image:
775
+ og_tags['og:image'] = self.og_image
776
+ if self.og_url:
777
+ og_tags['og:url'] = self.og_url
778
+ if self.og_type:
779
+ og_tags['og:type'] = self.og_type
780
+ if self.og_site_name:
781
+ og_tags['og:site_name'] = self.og_site_name
782
+
783
+ return og_tags
784
+
785
+ def get_twitter_tags(self) -> Dict[str, str]:
786
+ """Obtain all tags of Twitter Cards"""
787
+ twitter_tags = {}
788
+
789
+ if self.twitter_card:
790
+ twitter_tags['twitter:card'] = self.twitter_card
791
+ if self.twitter_site:
792
+ twitter_tags['twitter:site'] = self.twitter_site
793
+ if self.twitter_creator:
794
+ twitter_tags['twitter:creator'] = self.twitter_creator
795
+
796
+ return twitter_tags
797
+
798
+ def export(self, exporter: 'Exporter', output_path: str) -> bool:
799
+ """Exports the application to the specified path using the exporter"""
800
+ if not self.root:
801
+ raise ValueError("No se ha establecido un componente raíz")
802
+
803
+ return exporter.export(self, output_path)
804
+
805
+ def validate(self) -> List[str]:
806
+ """Validate the applicatiob and return a error lines"""
807
+ errors = []
808
+
809
+ # Validar título
810
+ if not self.title:
811
+ errors.append("The application title can't be empty.")
812
+
813
+ # Validación single-page y multipage
814
+ if self.is_multipage():
815
+ if not self._pages:
816
+ errors.append("The app is on multipage mode but there are no pages registered.")
817
+ for name, page in self._pages.items():
818
+ if not page.root:
819
+ errors.append(f"The page '{name}' hasn't a root component.")
820
+ else:
821
+ errors.extend(self._validate_component(page.root, path=f"pages['{name}']"))
822
+ else:
823
+ if not self.root:
824
+ errors.append("Can't find a root component (single-page mode)")
825
+ else:
826
+ errors.extend(self._validate_component(self.root))
827
+
828
+ return errors
829
+
830
+ def _validate_component(self, component: Component, path: str = "root") -> List[str]:
831
+ """Validate a component and its children recursively"""
832
+ errors = []
833
+
834
+ # Validar que el componente tenga un método render
835
+ if not hasattr(component, 'render'):
836
+ errors.append(f"The component in {path} doesn't have render method")
837
+
838
+ # Validar hijos
839
+ for i, child in enumerate(component.children):
840
+ child_path = f"{path}.children[{i}]"
841
+ errors.extend(self._validate_component(child, child_path))
842
+
843
+ return errors
844
+
845
+ def _count_components(self, component: Component) -> int:
846
+ """Count the total number of components in the app"""
847
+ count = 1
848
+ for child in component.children:
849
+ count += self._count_components(child)
850
+ return count
851
+ def get_component_tree(self) -> str:
852
+ """
853
+ Returns a legible representation of the component tree.
854
+ """
855
+ def tree_str(component, indent=0):
856
+ pad = ' ' * indent
857
+ s = f"{pad}- {component.__class__.__name__} (id={getattr(component, 'id', None)})"
858
+ for child in getattr(component, 'children', []):
859
+ s += '\n' + tree_str(child, indent + 1)
860
+ return s
861
+
862
+ if self.is_multipage():
863
+ if not self._pages:
864
+ return "[Dars] No pages registered."
865
+ result = []
866
+ for name, page in self._pages.items():
867
+ result.append(f"Página: {name} (title={page.title})\n" + tree_str(page.root))
868
+ return '\n\n'.join(result)
869
+ elif self.root:
870
+ return tree_str(self.root)
871
+ else:
872
+ return "[Dars] No root component defined."
873
+
874
+ def _component_to_dict(self, component: Component) -> Dict[str, Any]:
875
+ """Convert a component to a dictionary for inspection"""
876
+ return {
877
+ 'type': component.__class__.__name__,
878
+ 'id': component.id,
879
+ 'class_name': component.class_name,
880
+ 'props': component.props,
881
+ 'style': component.style,
882
+ 'children': [self._component_to_dict(child) for child in component.children]
883
+ }
884
+
885
+ def find_component_by_id(self, component_id: str) -> Optional[Component]:
886
+ """Find a component by its ID (soporta multipage y single-page)"""
887
+ if self.is_multipage():
888
+ for page in self._pages.values():
889
+ result = self._find_component_recursive(page.root, component_id)
890
+ if result:
891
+ return result
892
+ return None
893
+ elif self.root:
894
+ return self._find_component_recursive(self.root, component_id)
895
+ else:
896
+ return None
897
+
898
+ def _find_component_recursive(self, component: Component, target_id: str) -> Optional[Component]:
899
+ """Search components recursively by ID"""
900
+ if component.id == target_id:
901
+ return component
902
+ for child in getattr(component, 'children', []):
903
+ result = self._find_component_recursive(child, target_id)
904
+ if result:
905
+ return result
906
+ return None
907
+
908
+ def get_stats(self) -> Dict[str, Any]:
909
+ """Return application stadistics (single-page and multipage)"""
910
+ if self.is_multipage():
911
+ total_components = 0
912
+ max_depth = 0
913
+ for page in self._pages.values():
914
+ if page.root:
915
+ total_components += self._count_components(page.root)
916
+ depth = self._calculate_max_depth(page.root)
917
+ max_depth = max(max_depth, depth)
918
+ return {
919
+ 'total_components': total_components,
920
+ 'max_depth': max_depth,
921
+ 'scripts_count': len(self.scripts),
922
+ 'global_styles_count': len(self.global_styles),
923
+ 'total_pages': len(self._pages)
924
+ }
925
+ elif self.root:
926
+ return {
927
+ 'total_components': self._count_components(self.root),
928
+ 'max_depth': self._calculate_max_depth(self.root),
929
+ 'scripts_count': len(self.scripts),
930
+ 'global_styles_count': len(self.global_styles),
931
+ 'total_pages': 1
932
+ }
933
+ else:
934
+ return {
935
+ 'total_components': 0,
936
+ 'max_depth': 0,
937
+ 'scripts_count': len(self.scripts),
938
+ 'global_styles_count': len(self.global_styles),
939
+ 'total_pages': 0
940
+ }
941
+
942
+ def calculate_max_depth(self) -> int:
943
+ """Calculates the maximun depth of a component tree (single page and multipage)"""
944
+ if self.is_multipage():
945
+ return max((self._calculate_max_depth(page.root) for page in self._pages.values() if page.root), default=0)
946
+ elif self.root:
947
+ return self._calculate_max_depth(self.root)
948
+ else:
949
+ return 0
950
+
951
+ def _calculate_max_depth(self, component: Component, current_depth: int = 0) -> int:
952
+ """Calculates the maximun depth of a component tree (internal use)"""
953
+ if not component or not getattr(component, 'children', []):
954
+ return current_depth
955
+ return max(self._calculate_max_depth(child, current_depth + 1) for child in component.children)
956
+
957
+