dars-framework 1.0.6__tar.gz → 1.0.7__tar.gz

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 (81) hide show
  1. {dars_framework-1.0.6/dars_framework.egg-info → dars_framework-1.0.7}/PKG-INFO +6 -4
  2. {dars_framework-1.0.6 → dars_framework-1.0.7}/README.md +4 -2
  3. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/core/app.py +319 -48
  4. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/exporters/base.py +13 -4
  5. dars_framework-1.0.6/dars/exporters/web/html_css_js.py → dars_framework-1.0.7/dars/exporters/web/OLD/html_css_js_OLD4.py +109 -127
  6. dars_framework-1.0.7/dars/exporters/web/html_css_js.py +1671 -0
  7. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/templates/examples/advanced/advanced_modal_demo.py +2 -2
  8. dars_framework-1.0.7/dars/templates/examples/demo/complete_app.py +24 -0
  9. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/version.py +2 -2
  10. {dars_framework-1.0.6 → dars_framework-1.0.7/dars_framework.egg-info}/PKG-INFO +6 -4
  11. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars_framework.egg-info/SOURCES.txt +1 -0
  12. dars_framework-1.0.7/dars_framework.egg-info/requires.txt +4 -0
  13. {dars_framework-1.0.6 → dars_framework-1.0.7}/pyproject.toml +2 -4
  14. dars_framework-1.0.6/dars/templates/examples/demo/complete_app.py +0 -720
  15. dars_framework-1.0.6/dars_framework.egg-info/requires.txt +0 -4
  16. {dars_framework-1.0.6 → dars_framework-1.0.7}/LICENSE +0 -0
  17. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/__init__.py +0 -0
  18. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/all.py +0 -0
  19. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/cli/__init__.py +0 -0
  20. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/cli/hot_reload.py +0 -0
  21. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/cli/main.py +0 -0
  22. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/cli/preview.py +0 -0
  23. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/cli/translations.py +0 -0
  24. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/__init__.py +0 -0
  25. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/advanced/__init__.py +0 -0
  26. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/advanced/accordion.py +0 -0
  27. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/advanced/card.py +0 -0
  28. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/advanced/modal.py +0 -0
  29. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/advanced/navbar.py +0 -0
  30. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/advanced/table.py +0 -0
  31. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/advanced/tabs.py +0 -0
  32. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/__init__.py +0 -0
  33. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/button.py +0 -0
  34. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/checkbox.py +0 -0
  35. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/container.py +0 -0
  36. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/datepicker.py +0 -0
  37. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/image.py +0 -0
  38. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/input.py +0 -0
  39. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/link.py +0 -0
  40. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/page.py +0 -0
  41. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/progressbar.py +0 -0
  42. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/radiobutton.py +0 -0
  43. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/select.py +0 -0
  44. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/slider.py +0 -0
  45. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/spinner.py +0 -0
  46. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/text.py +0 -0
  47. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/textarea.py +0 -0
  48. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/basic/tooltip.py +0 -0
  49. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/layout/__init__.py +0 -0
  50. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/layout/anchor.py +0 -0
  51. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/layout/flex.py +0 -0
  52. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/components/layout/grid.py +0 -0
  53. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/core/__init__.py +0 -0
  54. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/core/component.py +0 -0
  55. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/core/events.py +0 -0
  56. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/core/properties.py +0 -0
  57. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/docs/__init__.py +0 -0
  58. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/exporters/__init__.py +0 -0
  59. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/exporters/web/OLD/html_css_js_old.py +0 -0
  60. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/exporters/web/OLD/html_css_js_old2.py +0 -0
  61. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/exporters/web/__init__.py +0 -0
  62. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/scripts/__init__.py +0 -0
  63. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/scripts/dscript.py +0 -0
  64. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/scripts/script.py +0 -0
  65. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/templates/__init__.py +0 -0
  66. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/templates/examples/advanced/all_components_demo.py +0 -0
  67. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/templates/examples/advanced/dashboard.py +0 -0
  68. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/templates/examples/advanced/modern_web_app.py +0 -0
  69. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/templates/examples/basic/flex_layout_responsive.py +0 -0
  70. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/templates/examples/basic/form_components.py +0 -0
  71. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/templates/examples/basic/grid_layout_responsive.py +0 -0
  72. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/templates/examples/basic/hello_world.py +0 -0
  73. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/templates/examples/basic/layout_multipage_demo.py +0 -0
  74. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/templates/examples/basic/multipage_example.py +0 -0
  75. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/templates/examples/basic/pwa_custom_icons.py +0 -0
  76. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/templates/examples/basic/simple_form.py +0 -0
  77. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars/templates/html/__init__.py +0 -0
  78. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars_framework.egg-info/dependency_links.txt +0 -0
  79. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars_framework.egg-info/entry_points.txt +0 -0
  80. {dars_framework-1.0.6 → dars_framework-1.0.7}/dars_framework.egg-info/top_level.txt +0 -0
  81. {dars_framework-1.0.6 → dars_framework-1.0.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dars-framework
3
- Version: 1.0.6
3
+ Version: 1.0.7
4
4
  Summary: Dars is a Python UI framework for building modern, interactive web apps with only Python code. Write your interface in Python, export it to static HTML/CSS/JS, and deploy anywhere.
5
5
  Author-email: ztamdev <zondax2009@gmail.com>
6
6
  License: MIT License
@@ -29,13 +29,15 @@ Description-Content-Type: text/markdown
29
29
  License-File: LICENSE
30
30
  Requires-Dist: rich
31
31
  Requires-Dist: bs4
32
+ Requires-Dist: uvicorn
32
33
  Requires-Dist: fastapi
33
- Requires-Dist: uvicorn[standard]
34
34
  Dynamic: license-file
35
35
 
36
- # Dars-Framework
36
+ # Dars Framework
37
37
 
38
- Dars is a Python UI framework for building modern, interactive web apps with only Python code. Write your interface in Python, export it to static HTML/CSS/JS, and deploy anywhere.
38
+ Dars Landing Page: https://ztamdev.github.io/Dars-Framework/
39
+
40
+ Dars is a Python UI framework for building modern, interactive web apps with Python code. Write your interface in Python, export it to static HTML/CSS/JS, and deploy anywhere.
39
41
 
40
42
  > Some Javascript or frontend stack required.
41
43
 
@@ -1,6 +1,8 @@
1
- # Dars-Framework
1
+ # Dars Framework
2
2
 
3
- Dars is a Python UI framework for building modern, interactive web apps with only Python code. Write your interface in Python, export it to static HTML/CSS/JS, and deploy anywhere.
3
+ Dars Landing Page: https://ztamdev.github.io/Dars-Framework/
4
+
5
+ Dars is a Python UI framework for building modern, interactive web apps with Python code. Write your interface in Python, export it to static HTML/CSS/JS, and deploy anywhere.
4
6
 
5
7
  > Some Javascript or frontend stack required.
6
8
 
@@ -4,12 +4,13 @@ from .events import EventManager
4
4
 
5
5
  class Page:
6
6
  """Representa una página individual en la app Dars (multipágina)."""
7
- def __init__(self, name: str, root: 'Component', title: str = None, meta: dict = None, index: bool = False):
7
+ def __init__(self, name: str, root: 'Component', title: str = None, meta: dict = None, index: bool = False, scripts: Optional[List[Any]] = None):
8
8
  self.name = name # slug o nombre de la página
9
9
  self.root = root # componente raíz de la página
10
10
  self.title = title
11
11
  self.meta = meta or {}
12
12
  self.index = index # ¿Es la página principal?
13
+ self.scripts: List[Any] = list(scripts) if scripts else []
13
14
 
14
15
  def attr(self, **attrs):
15
16
  """
@@ -29,12 +30,110 @@ class Page:
29
30
  d['root'] = self.root
30
31
  d['title'] = self.title
31
32
  d['index'] = self.index
33
+ d['scripts'] = list(self.scripts)
32
34
  return d
35
+ # -----------------------------
36
+ # Métodos para manejar scripts
37
+ # -----------------------------
38
+ def add_script(self, script: Any):
39
+ """
40
+ Agrega un script a esta página.
41
+ - Si 'script' es una instancia (p. ej. InlineScript/FileScript/DScript), se añade tal cual.
42
+ - Si 'script' es una cadena, se interpreta como InlineScript (código).
43
+ - Si 'script' es un dict, se añade tal cual (fallback).
44
+ Devuelve self para encadenar llamadas.
45
+ """
46
+ # si es str => interpretarlo como inline
47
+ if isinstance(script, str):
48
+ created = self._make_inline_script(script)
49
+ self.scripts.append(created)
50
+ return self
51
+
52
+ # si es dict => fallback, guardarlo
53
+ if isinstance(script, dict):
54
+ self.scripts.append(script)
55
+ return self
56
+
57
+ # si ya es una instancia de "Script" (no podemos verificar tipo concreto sin dependencia),
58
+ # asumimos que es un script válido y lo añadimos.
59
+ self.scripts.append(script)
60
+ return self
61
+
62
+ # alias corto (pedido)
63
+ def addscript(self, script: Any):
64
+ return self.add_script(script)
65
+
66
+ def add_inline_script(self, code: str, **kwargs):
67
+ """Convenience: añade un InlineScript a la página (code = JS o similar)."""
68
+ s = self._make_inline_script(code, **kwargs)
69
+ self.scripts.append(s)
70
+ return self
71
+
72
+ def add_file_script(self, path: str, **kwargs):
73
+ """Convenience: añade un FileScript (referencia a archivo .js/.ts/etc.)"""
74
+ s = self._make_file_script(path, **kwargs)
75
+ self.scripts.append(s)
76
+ return self
77
+
78
+ def add_dscript(self, obj: Any, **kwargs):
79
+ """Convenience: intenta crear/añadir un DScript (si existe la clase)."""
80
+ s = self._make_dscript(obj, **kwargs)
81
+ self.scripts.append(s)
82
+ return self
83
+
84
+ def get_scripts(self) -> List[Any]:
85
+ """Retorna la lista de scripts añadidos a la página."""
86
+ return list(self.scripts)
87
+
88
+ # -----------------------------
89
+ # Helpers para construcción segura
90
+ # -----------------------------
91
+ def _make_inline_script(self, code: str, **kwargs) -> Any:
92
+ """
93
+ Intenta crear una instancia InlineScript si existe en dars.scripts.*.
94
+ Si no, devuelve un dict fallback: {'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 import InlineScript # type: ignore
99
+ return InlineScript(code, **kwargs)
100
+ except Exception:
101
+ try:
102
+ from dars.scripts.inline 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
+ """
110
+ Intenta crear una instancia FileScript si existe. Si no, devuelve dict fallback.
111
+ """
112
+ try:
113
+ from dars.scripts import FileScript # type: ignore
114
+ return FileScript(path, **kwargs)
115
+ except Exception:
116
+ try:
117
+ from dars.scripts.file import FileScript # type: ignore
118
+ return FileScript(path, **kwargs)
119
+ except Exception:
120
+ return {'type': 'file', 'path': path, **kwargs}
121
+
122
+ def _make_dscript(self, obj: Any, **kwargs) -> Any:
123
+ """
124
+ Intenta crear una instancia DScript si existe. Si no, guarda el objeto con marca.
125
+ """
126
+ try:
127
+ from dars.scripts import DScript # type: ignore
128
+ return DScript(obj, **kwargs)
129
+ except Exception:
130
+ # si ya es dict o similar, solo anotamos el tipo
131
+ return {'type': 'dscript', 'value': obj, **kwargs}
33
132
 
34
133
  class App:
35
134
  """Clase principal que representa una aplicación Dars"""
36
135
 
37
- def rTimeCompile(self, exporter=None, port=None):
136
+ def rTimeCompile(self, exporter=None, port=None, add_file_types=None):
38
137
  """
39
138
  Genera una preview rápida de la app en un servidor local usando un exportador
40
139
  (por defecto HTMLCSSJSExporter) y sirviendo los archivos en un directorio temporal.
@@ -80,7 +179,40 @@ class App:
80
179
  port = int(sys.argv[i + 1])
81
180
  except Exception:
82
181
  pass
83
-
182
+ # --- Normalizar add_file_types => lista de extensiones que empiezan con '.' ---
183
+ def _normalize_exts(exts):
184
+ if not exts:
185
+ return ['.py']
186
+ # aceptar string con comas
187
+ if isinstance(exts, str):
188
+ parts = [p.strip() for p in exts.split(',') if p.strip()]
189
+ elif isinstance(exts, (list, tuple, set)):
190
+ parts = [str(p).strip() for p in exts if p]
191
+ else:
192
+ parts = [str(exts).strip()]
193
+
194
+ normalized = []
195
+ for p in parts:
196
+ if not p:
197
+ continue
198
+ if not p.startswith('.'):
199
+ p = '.' + p
200
+ normalized.append(p.lower())
201
+ # siempre incluir .py (comportamiento: .py + los adicionales)
202
+ if '.py' not in normalized:
203
+ normalized.insert(0, '.py')
204
+ # eliminar duplicados preservando orden
205
+ seen = set()
206
+ result = []
207
+ for e in normalized:
208
+ if e not in seen:
209
+ seen.add(e)
210
+ result.append(e)
211
+ return result
212
+
213
+ # Lista final de extensiones a vigilar (ej: ['.py', '.js', '.css'])
214
+ watch_exts = _normalize_exts(add_file_types)
215
+
84
216
  # Importar exportador por defecto si no se pasa
85
217
  if exporter is None:
86
218
  try:
@@ -98,8 +230,15 @@ class App:
98
230
  return
99
231
 
100
232
  shutdown_event = threading.Event()
233
+ watchers = [] # aquí guardaremos todos los watchers
234
+
235
+ # Debounce / lock para evitar reloads concurrentes
236
+ reload_lock = threading.Lock()
237
+ last_reload_at = 0.0
238
+ MIN_RELOAD_INTERVAL = 0.4 # segundos
239
+
101
240
  try:
102
- # Detectar archivo principal de la app
241
+ # Detectar archivo principal de la app (el que ejecutaste con `python archivo.py`)
103
242
  app_file = None
104
243
  for frame in inspect.stack():
105
244
  if frame.function == "<module>":
@@ -125,7 +264,7 @@ class App:
125
264
 
126
265
  os.makedirs(preview_dir, exist_ok=True)
127
266
 
128
- # export inicial desde el root
267
+ # export inicial desde el root usando la instancia actual (self)
129
268
  with pushd(project_root):
130
269
  exporter.export(self, preview_dir)
131
270
 
@@ -150,55 +289,178 @@ class App:
150
289
  # --- HOT RELOAD ---
151
290
  from dars.cli.hot_reload import FileWatcher
152
291
 
153
- def reload_and_export():
154
- if console:
155
- console.print("[yellow]Detected app file change. Reloading...[/yellow]")
156
- else:
157
- print("[Dars] Detected app file change. Reloading...")
158
-
159
- try:
160
- if project_root not in sys.path:
161
- sys.path.insert(0, project_root)
162
-
163
- with pushd(project_root):
164
- # Recargar módulo
165
- spec = importlib.util.spec_from_file_location("dars_app", app_file)
166
- module = importlib.util.module_from_spec(spec)
167
- spec.loader.exec_module(module)
168
-
169
- # Buscar instancia App
170
- new_app = None
171
- for v in vars(module).values():
172
- if isinstance(v, App):
173
- new_app = v
292
+ def _collect_project_files_by_ext(root, exts):
293
+ files = []
294
+ for dirpath, dirnames, filenames in os.walk(root):
295
+ # excluir preview_dir, .git y __pycache__
296
+ if os.path.abspath(dirpath).startswith(os.path.abspath(preview_dir)):
297
+ continue
298
+ if '.git' in dirpath or '__pycache__' in dirpath:
299
+ continue
300
+ for fname in filenames:
301
+ for ext in exts:
302
+ if fname.lower().endswith(ext):
303
+ files.append(os.path.join(dirpath, fname))
174
304
  break
175
-
176
- if not new_app:
177
- (console.print("[red]No App instance found after reload.[/red]")
178
- if console else print("[Dars] No App instance found after reload."))
179
- return
180
-
181
- exporter.export(new_app, preview_dir)
182
-
183
- (console.print("[green]App reloaded and re-exported successfully.[/green]")
184
- if console else print("[Dars] App reloaded and re-exported successfully."))
185
-
305
+ return files
306
+
307
+
308
+ def reload_and_export(changed_file=None):
309
+ nonlocal last_reload_at
310
+ now = time.time()
311
+ # debounce rápido
312
+ if now - last_reload_at < MIN_RELOAD_INTERVAL:
313
+ return
314
+ with reload_lock:
315
+ last_reload_at = time.time()
316
+ if console:
317
+ console.print(f"[yellow]Detected change in {changed_file}. Reloading...[/yellow]")
318
+ else:
319
+ print(f"[Dars] Detected change in {changed_file}. Reloading...")
320
+
321
+ try:
322
+ if project_root not in sys.path:
323
+ sys.path.insert(0, project_root)
324
+
325
+ with pushd(project_root):
326
+ # --- Limpiar del cache todos los módulos que pertenecen al proyecto ---
327
+ to_remove = []
328
+ for name, mod in list(sys.modules.items()):
329
+ try:
330
+ mod_file = getattr(mod, '__file__', None)
331
+ if not mod_file:
332
+ continue
333
+ # normalizar paths
334
+ mod_file_abs = os.path.abspath(mod_file)
335
+ if mod_file_abs.startswith(os.path.abspath(project_root)):
336
+ to_remove.append(name)
337
+ except Exception:
338
+ continue
339
+
340
+ for name in to_remove:
341
+ try:
342
+ del sys.modules[name]
343
+ except Exception:
344
+ pass
345
+
346
+ # también borrar cualquier nombre temporal 'dars_app' si existiese
347
+ sys.modules.pop("dars_app", None)
348
+
349
+ # Importar el archivo principal en un nombre único (para limpieza segura)
350
+ unique_name = f"dars_app_reload_{int(time.time()*1000)}"
351
+ spec = importlib.util.spec_from_file_location(unique_name, app_file)
352
+ module = importlib.util.module_from_spec(spec)
353
+ spec.loader.exec_module(module)
354
+
355
+ # Buscar nueva instancia App en el módulo recargado
356
+ new_app = None
357
+ for v in vars(module).values():
358
+ try:
359
+ if isinstance(v, App):
360
+ new_app = v
361
+ break
362
+ except Exception:
363
+ # si isinstance falla por alguna razón, ignorar
364
+ pass
365
+
366
+ # fallback por nombre de clase (por si App es distinto objeto)
367
+ if not new_app:
368
+ for v in vars(module).values():
369
+ try:
370
+ if hasattr(v, '__class__') and v.__class__.__name__ == 'App':
371
+ new_app = v
372
+ break
373
+ except Exception:
374
+ pass
375
+
376
+ if not new_app:
377
+ (console.print("[red]No App instance found after reload.[/red]")
378
+ if console else print("[Dars] No App instance found after reload."))
379
+ return
380
+
381
+ # Exportar la nueva instancia
382
+ exporter.export(new_app, preview_dir)
383
+
384
+ (console.print("[green]App reloaded and re-exported successfully.[/green]")
385
+ if console else print("[Dars] App reloaded and re-exported successfully."))
386
+
387
+ except Exception as e:
388
+ tb = traceback.format_exc()
389
+ (console.print(f"[red]Hot reload failed: {e}\n{tb}[/red]")
390
+ if console else print(f"[Dars] Hot reload failed: {e}\n{tb}"))
391
+
392
+ # --- Crear watchers para todos los archivos .py dentro del proyecto (recursivo) ---
393
+ files_to_watch = _collect_project_files_by_ext(project_root, watch_exts)
394
+
395
+
396
+ # Si no hay archivos detectados (raro), al menos mirar app_file
397
+ if not files_to_watch:
398
+ files_to_watch = [app_file]
399
+
400
+ for f in files_to_watch:
401
+ try:
402
+ # FileWatcher espera una función sin argumentos; usamos lambda que captura f
403
+ w = FileWatcher(f, lambda f=f: reload_and_export(f))
404
+ w.start()
405
+ watchers.append(w)
186
406
  except Exception as e:
187
- (console.print(f"[red]Hot reload failed: {e}[/red]")
188
- if console else print(f"[Dars] Hot reload failed: {e}"))
189
-
190
- watcher = FileWatcher(app_file, reload_and_export)
191
- watcher.start()
407
+ if console:
408
+ console.print(f"[yellow]Warning: could not watch {f}: {e}[/yellow]")
409
+ else:
410
+ print(f"[Dars] Warning: could not watch {f}: {e}")
411
+
412
+ if console:
413
+ # Mostrar rutas relativas para que no sea tan largo
414
+ rel_paths = [os.path.relpath(f, project_root) for f in files_to_watch]
415
+ max_show = 80 # número máximo de líneas a mostrar
416
+ if len(rel_paths) > max_show:
417
+ shown = rel_paths[:max_show]
418
+ shown.append(f"... (+{len(rel_paths)-max_show} más)")
419
+ else:
420
+ shown = rel_paths or ["(ninguno)"]
421
+
422
+ from rich.table import Table
423
+ table = Table(show_header=False, box=None, padding=0)
424
+ table.add_column("Files", style="bold")
425
+ for p in shown:
426
+ table.add_row(p)
427
+
428
+ panel = Panel(
429
+ table,
430
+ title=f"Watching {len(files_to_watch)} files · Exts: {', '.join(watch_exts)}",
431
+ subtitle=f"Project root: {os.path.basename(project_root)}",
432
+ border_style="magenta"
433
+ )
434
+ console.print(panel)
435
+ else:
436
+ print(f"[Dars] Watching {len(files_to_watch)} files in {project_root}:")
437
+ for f in files_to_watch:
438
+ print(" -", os.path.relpath(f, project_root))
192
439
 
440
+ # Loop principal: espera a Ctrl+C
193
441
  while not shutdown_event.is_set():
194
- shutdown_event.wait(timeout=1) # Espera hasta que se pida cerrar, sin consumir CPU
442
+ shutdown_event.wait(timeout=1) # Espera sin consumir CPU
443
+
195
444
  except KeyboardInterrupt:
196
445
  shutdown_event.set()
197
- watcher.stop()
446
+ for w in watchers:
447
+ try:
448
+ w.stop()
449
+ except Exception:
450
+ pass
198
451
  (console.print("\n[cyan]Stopping preview and watcher...[/cyan]")
199
452
  if console else print("\n[Dars] Stopping preview and watcher..."))
200
453
  finally:
201
- server.stop()
454
+ # Detener watchers y servidor
455
+ try:
456
+ server.stop()
457
+ except Exception:
458
+ pass
459
+ for w in watchers:
460
+ try:
461
+ w.stop()
462
+ except Exception:
463
+ pass
202
464
  (console.print("[green]Preview stopped.[/green]")
203
465
  if console else print("[Dars] Preview stopped."))
204
466
 
@@ -209,7 +471,11 @@ class App:
209
471
  msg = f"Unexpected error in fast preview: {e}\n{traceback.format_exc()}"
210
472
  console.print(f"[red]{msg}[/red]") if console else print(msg)
211
473
  finally:
212
- os.chdir(cwd_original)
474
+ # Restaurar cwd y limpiar preview
475
+ try:
476
+ os.chdir(cwd_original)
477
+ except Exception:
478
+ pass
213
479
  try:
214
480
  shutil.rmtree(preview_dir)
215
481
  (console.print("[yellow]Preview files deleted.[/yellow]")
@@ -218,7 +484,6 @@ class App:
218
484
  msg = f"Could not delete preview directory: {e}"
219
485
  console.print(f"[red]{msg}[/red]") if console else print(msg)
220
486
 
221
-
222
487
 
223
488
  def __init__(
224
489
  self,
@@ -539,7 +804,13 @@ class App:
539
804
  errors.extend(self._validate_component(child, child_path))
540
805
 
541
806
  return errors
542
-
807
+
808
+ def _count_components(self, component: Component) -> int:
809
+ """Cuenta el número total de componentes en la app (single-page y multipage)"""
810
+ count = 1
811
+ for child in component.children:
812
+ count += self._count_components(child)
813
+ return count
543
814
  def get_component_tree(self) -> str:
544
815
  """
545
816
  Devuelve una representación legible (string) del árbol de componentes.
@@ -61,9 +61,18 @@ class Exporter(ABC):
61
61
 
62
62
  return "; ".join(css_rules)
63
63
 
64
- def generate_unique_id(self, component: 'Component') -> str:
65
- """Genera un ID único para un componente si no tiene uno"""
66
- if component.id:
64
+ def generate_unique_id(self, component: 'Component', prefix: str = "component") -> str:
65
+ """Genera un ID único para un componente si no tiene uno definido."""
66
+ # Si el usuario ya puso un id, usarlo siempre
67
+ if getattr(component, "id", None):
67
68
  return component.id
68
- return f"component_{id(component)}"
69
+
70
+ # Si no tiene id, generar uno único pero persistente
71
+ unique = f"{prefix}_{hex(id(component))}"
72
+ try:
73
+ component.id = unique # lo guardamos en el componente
74
+ except Exception:
75
+ pass # por si el objeto no permite asignación
76
+ return unique
77
+
69
78