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.
- dars/__init__.py +0 -0
- dars/all.py +69 -0
- dars/cli/__init__.py +0 -0
- dars/cli/doctor/__init__.py +1 -0
- dars/cli/doctor/detect.py +154 -0
- dars/cli/doctor/doctor.py +176 -0
- dars/cli/doctor/installers.py +100 -0
- dars/cli/doctor/persist.py +62 -0
- dars/cli/doctor/preflight.py +33 -0
- dars/cli/doctor/ui.py +54 -0
- dars/cli/hot_reload.py +33 -0
- dars/cli/main.py +1107 -0
- dars/cli/preview.py +448 -0
- dars/cli/translations.py +531 -0
- dars/components/__init__.py +0 -0
- dars/components/advanced/__init__.py +8 -0
- dars/components/advanced/accordion.py +26 -0
- dars/components/advanced/card.py +33 -0
- dars/components/advanced/modal.py +45 -0
- dars/components/advanced/navbar.py +44 -0
- dars/components/advanced/table.py +25 -0
- dars/components/advanced/tabs.py +31 -0
- dars/components/basic/__init__.py +34 -0
- dars/components/basic/button.py +55 -0
- dars/components/basic/checkbox.py +35 -0
- dars/components/basic/container.py +29 -0
- dars/components/basic/datepicker.py +139 -0
- dars/components/basic/image.py +36 -0
- dars/components/basic/input.py +57 -0
- dars/components/basic/link.py +31 -0
- dars/components/basic/markdown.py +86 -0
- dars/components/basic/page.py +20 -0
- dars/components/basic/progressbar.py +18 -0
- dars/components/basic/radiobutton.py +35 -0
- dars/components/basic/select.py +82 -0
- dars/components/basic/slider.py +63 -0
- dars/components/basic/spinner.py +12 -0
- dars/components/basic/text.py +23 -0
- dars/components/basic/textarea.py +46 -0
- dars/components/basic/tooltip.py +19 -0
- dars/components/layout/__init__.py +0 -0
- dars/components/layout/anchor.py +13 -0
- dars/components/layout/flex.py +26 -0
- dars/components/layout/grid.py +45 -0
- dars/config.py +134 -0
- dars/core/__init__.py +0 -0
- dars/core/app.py +957 -0
- dars/core/component.py +284 -0
- dars/core/events.py +102 -0
- dars/core/js_bridge.py +99 -0
- dars/core/properties.py +127 -0
- dars/core/state.py +309 -0
- dars/dars_tests/apps_test/health_check.py +56 -0
- dars/dars_tests/run_tests.py +275 -0
- dars/dars_tests/tests/test_advanced_components.py +69 -0
- dars/dars_tests/tests/test_basic_components.py +88 -0
- dars/dars_tests/tests/test_core_and_cli.py +17 -0
- dars/dars_tests/tests/test_layout_components.py +58 -0
- dars/dars_tests/tests/test_version_check.py +21 -0
- dars/docs/__init__.py +0 -0
- dars/docs/app.md +290 -0
- dars/docs/cli.md +80 -0
- dars/docs/components.md +1679 -0
- dars/docs/custom_components.md +30 -0
- dars/docs/events.md +45 -0
- dars/docs/exporters.md +162 -0
- dars/docs/getting_started.md +79 -0
- dars/docs/index.md +18 -0
- dars/docs/scripts.md +593 -0
- dars/docs/state_management.md +57 -0
- dars/exporters/__init__.py +0 -0
- dars/exporters/base.py +96 -0
- dars/exporters/web/OLD/html_css_js_OLD4.py +1538 -0
- dars/exporters/web/OLD/html_css_js_old.py +1406 -0
- dars/exporters/web/OLD/html_css_js_old2.py +1406 -0
- dars/exporters/web/__init__.py +0 -0
- dars/exporters/web/html_css_js.py +2675 -0
- dars/exporters/web/vdom.py +251 -0
- dars/js_lib.py +206 -0
- dars/scripts/__init__.py +0 -0
- dars/scripts/dscript.py +26 -0
- dars/scripts/script.py +39 -0
- dars/security.py +195 -0
- dars/templates/__init__.py +0 -0
- dars/templates/__pycache__/__init__.cpython-311.pyc +0 -0
- dars/templates/examples/README.md +4 -0
- dars/templates/examples/__pycache__/dynamic_event_demo.cpython-311.pyc +0 -0
- dars/templates/examples/advanced/Modal_Demo/advanced_modal_demo.py +275 -0
- dars/templates/examples/advanced/SimpleDashboard/dashboard.py +437 -0
- dars/templates/examples/advanced/SimpleModermWeb/modern_web_app.py +452 -0
- dars/templates/examples/advanced/VariousComponents/all_components_demo.py +87 -0
- dars/templates/examples/advanced/__init__.py +0 -0
- dars/templates/examples/advanced/dState/state_mods_demo.py +68 -0
- dars/templates/examples/basic/Forms/form_components.py +516 -0
- dars/templates/examples/basic/Forms/simple_form.py +379 -0
- dars/templates/examples/basic/HelloWorld/hello_world.py +56 -0
- dars/templates/examples/basic/Layouts/flex_layout_responsive.py +13 -0
- dars/templates/examples/basic/Layouts/grid_layout_responsive.py +12 -0
- dars/templates/examples/basic/Layouts/layout_multipage_demo.py +23 -0
- dars/templates/examples/basic/Multipage/multipage_example.py +67 -0
- dars/templates/examples/basic/PWA/icon-192x192.png +0 -0
- dars/templates/examples/basic/PWA/icon-512x512.png +0 -0
- dars/templates/examples/basic/PWA/pwa_custom_icons.py +33 -0
- dars/templates/examples/basic/__init__.py +0 -0
- dars/templates/examples/demo/__pycache__/complete_app.cpython-311.pyc +0 -0
- dars/templates/examples/demo/complete_app.py +21 -0
- dars/templates/examples/markdown/MarkdownTemplate/README.md +159 -0
- dars/templates/examples/markdown/MarkdownTemplate/markdown_template.py +21 -0
- dars/templates/examples/markdown/MarkdownTemplate/other_docs.md +1 -0
- dars/templates/examples/markdown/__init__.py +0 -0
- dars/templates/html/__init__.py +0 -0
- dars/version.py +2 -0
- dars_framework-1.2.3.dist-info/METADATA +15 -0
- dars_framework-1.2.3.dist-info/RECORD +118 -0
- dars_framework-1.2.3.dist-info/WHEEL +5 -0
- dars_framework-1.2.3.dist-info/entry_points.txt +2 -0
- dars_framework-1.2.3.dist-info/licenses/LICENSE +21 -0
- dars_framework-1.2.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1406 @@
|
|
|
1
|
+
from dars.exporters.base import Exporter
|
|
2
|
+
from dars.core.app import App
|
|
3
|
+
from dars.core.component import Component
|
|
4
|
+
from dars.components.basic.text import Text
|
|
5
|
+
from dars.components.basic.button import Button
|
|
6
|
+
from dars.components.basic.input import Input
|
|
7
|
+
from dars.components.basic.container import Container
|
|
8
|
+
from dars.components.basic.image import Image
|
|
9
|
+
from dars.components.basic.link import Link
|
|
10
|
+
from dars.components.basic.textarea import Textarea
|
|
11
|
+
from dars.components.basic.checkbox import Checkbox
|
|
12
|
+
from dars.components.basic.radiobutton import RadioButton
|
|
13
|
+
from dars.components.basic.select import Select
|
|
14
|
+
from dars.components.basic.slider import Slider
|
|
15
|
+
from dars.components.basic.datepicker import DatePicker
|
|
16
|
+
from dars.components.advanced.card import Card
|
|
17
|
+
from dars.components.advanced.modal import Modal
|
|
18
|
+
from dars.components.advanced.navbar import Navbar
|
|
19
|
+
from dars.components.advanced.table import Table
|
|
20
|
+
from dars.components.advanced.tabs import Tabs
|
|
21
|
+
from dars.components.advanced.accordion import Accordion
|
|
22
|
+
from dars.components.basic.progressbar import ProgressBar
|
|
23
|
+
from dars.components.basic.spinner import Spinner
|
|
24
|
+
from dars.components.basic.tooltip import Tooltip
|
|
25
|
+
from typing import Dict, Any
|
|
26
|
+
import os
|
|
27
|
+
from bs4 import BeautifulSoup
|
|
28
|
+
|
|
29
|
+
class HTMLCSSJSExporter(Exporter):
|
|
30
|
+
"""Exportador para HTML, CSS y JavaScript"""
|
|
31
|
+
|
|
32
|
+
def get_platform(self) -> str:
|
|
33
|
+
return "html"
|
|
34
|
+
|
|
35
|
+
def export(self, app: App, output_path: str) -> bool:
|
|
36
|
+
"""Exporta la aplicación a HTML/CSS/JS (soporta multipágina)."""
|
|
37
|
+
try:
|
|
38
|
+
self.create_output_directory(output_path)
|
|
39
|
+
|
|
40
|
+
# Generar CSS y JS globales (compartidos)
|
|
41
|
+
css_content = self.generate_css(app)
|
|
42
|
+
self.write_file(os.path.join(output_path, "styles.css"), css_content)
|
|
43
|
+
|
|
44
|
+
runtime_js = self.generate_javascript(app)
|
|
45
|
+
self.write_file(os.path.join(output_path, "runtime_dars.js"), runtime_js)
|
|
46
|
+
|
|
47
|
+
script_js = "" # Aquí podrías agregar lógica para scripts de usuario en el futuro
|
|
48
|
+
self.write_file(os.path.join(output_path, "script.js"), script_js)
|
|
49
|
+
|
|
50
|
+
# Multipágina: exportar un HTML, CSS y JS por cada página registrada
|
|
51
|
+
if hasattr(app, "is_multipage") and app.is_multipage():
|
|
52
|
+
import copy
|
|
53
|
+
# Determinar la página index (principal)
|
|
54
|
+
index_page = None
|
|
55
|
+
if hasattr(app, 'get_index_page'):
|
|
56
|
+
index_page = app.get_index_page()
|
|
57
|
+
# Exportar cada página
|
|
58
|
+
for slug, page in app.pages.items():
|
|
59
|
+
import copy
|
|
60
|
+
page_app = copy.copy(app)
|
|
61
|
+
page_app.root = page.root
|
|
62
|
+
if page.title:
|
|
63
|
+
page_app.title = page.title
|
|
64
|
+
if page.meta:
|
|
65
|
+
for k, v in page.meta.items():
|
|
66
|
+
setattr(page_app, k, v)
|
|
67
|
+
# --- Aseguramos que root nunca sea lista, igual que single-page ---
|
|
68
|
+
from dars.components.basic.container import Container
|
|
69
|
+
if isinstance(page_app.root, list):
|
|
70
|
+
page_app.root = Container(children=page_app.root)
|
|
71
|
+
# --- Generación idéntica a single-page, solo cambia el nombre de archivo ---
|
|
72
|
+
if index_page is not None and page is index_page:
|
|
73
|
+
css_content = self.generate_css(page_app)
|
|
74
|
+
self.write_file(os.path.join(output_path, "styles.css"), css_content)
|
|
75
|
+
# --- scripts globales + scripts de la Page ---
|
|
76
|
+
scripts = list(getattr(app, 'scripts', []))
|
|
77
|
+
if hasattr(page_app.root, 'get_scripts'):
|
|
78
|
+
scripts += page_app.root.get_scripts()
|
|
79
|
+
script_js = self._generate_combined_script_js(scripts)
|
|
80
|
+
self.write_file(os.path.join(output_path, "script.js"), script_js)
|
|
81
|
+
html_content = self.generate_html(page_app, css_file="styles.css", script_file="script.js")
|
|
82
|
+
filename = "index.html"
|
|
83
|
+
try:
|
|
84
|
+
soup = BeautifulSoup(html_content, "html.parser")
|
|
85
|
+
html_content = soup.prettify()
|
|
86
|
+
except ImportError:
|
|
87
|
+
pass
|
|
88
|
+
self.write_file(os.path.join(output_path, filename), html_content)
|
|
89
|
+
else:
|
|
90
|
+
css_content = self.generate_css(page_app)
|
|
91
|
+
scripts = list(getattr(app, 'scripts', []))
|
|
92
|
+
if hasattr(page_app.root, 'get_scripts'):
|
|
93
|
+
scripts += page_app.root.get_scripts()
|
|
94
|
+
script_js = self._generate_combined_script_js(scripts)
|
|
95
|
+
script_name = f"script_{slug}.js"
|
|
96
|
+
self.write_file(os.path.join(output_path, script_name), script_js)
|
|
97
|
+
html_content = self.generate_html(page_app, css_file="styles.css", script_file=script_name)
|
|
98
|
+
filename = f"{slug}.html"
|
|
99
|
+
try:
|
|
100
|
+
soup = BeautifulSoup(html_content, "html.parser")
|
|
101
|
+
html_content = soup.prettify()
|
|
102
|
+
except ImportError:
|
|
103
|
+
pass
|
|
104
|
+
self.write_file(os.path.join(output_path, filename), html_content)
|
|
105
|
+
self.write_file(os.path.join(output_path, f"styles_{slug}.css"), css_content)
|
|
106
|
+
# El script_{slug}.js ya fue generado correctamente arriba, y el HTML ya fue generado, no sobrescribir
|
|
107
|
+
else:
|
|
108
|
+
# Single-page clásico
|
|
109
|
+
css_content = self.generate_css(app)
|
|
110
|
+
self.write_file(os.path.join(output_path, "styles.css"), css_content)
|
|
111
|
+
script_js = "" # Aquí podrías agregar lógica para scripts de usuario en el futuro
|
|
112
|
+
self.write_file(os.path.join(output_path, "script.js"), script_js)
|
|
113
|
+
html_content = self.generate_html(app, css_file="styles.css", script_file="script.js")
|
|
114
|
+
try:
|
|
115
|
+
soup = BeautifulSoup(html_content, "html.parser")
|
|
116
|
+
html_content = soup.prettify()
|
|
117
|
+
except ImportError:
|
|
118
|
+
pass # Si no está bs4, sigue igual
|
|
119
|
+
self.write_file(os.path.join(output_path, "index.html"), html_content)
|
|
120
|
+
|
|
121
|
+
# Generar archivos PWA si está habilitado
|
|
122
|
+
if getattr(app, 'pwa_enabled', False):
|
|
123
|
+
self._generate_pwa_files(app, output_path)
|
|
124
|
+
|
|
125
|
+
return True
|
|
126
|
+
except Exception as e:
|
|
127
|
+
print(f"Error al exportar: {e}")
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _generate_pwa_files(self, app: 'App', output_path: str) -> None:
|
|
132
|
+
"""Genera manifest.json, iconos y service worker para PWA"""
|
|
133
|
+
import json, os
|
|
134
|
+
# Manifest
|
|
135
|
+
self._generate_manifest_json(app, output_path)
|
|
136
|
+
# Iconos por defecto (placeholder, puedes mejorar esto)
|
|
137
|
+
self._generate_default_icons(output_path)
|
|
138
|
+
# Service worker
|
|
139
|
+
sw_path = getattr(app, 'service_worker_path', None)
|
|
140
|
+
sw_enabled = getattr(app, 'service_worker_enabled', True)
|
|
141
|
+
if sw_enabled:
|
|
142
|
+
if sw_path:
|
|
143
|
+
# Copiar el personalizado
|
|
144
|
+
import shutil
|
|
145
|
+
shutil.copy(sw_path, os.path.join(output_path, 'sw.js'))
|
|
146
|
+
else:
|
|
147
|
+
self._generate_basic_service_worker(output_path)
|
|
148
|
+
|
|
149
|
+
def _generate_manifest_json(self, app: 'App', output_path: str) -> None:
|
|
150
|
+
import json, os, shutil
|
|
151
|
+
manifest = {
|
|
152
|
+
"name": getattr(app, 'pwa_name', getattr(app, 'title', 'Dars App')),
|
|
153
|
+
"short_name": getattr(app, 'pwa_short_name', 'Dars'),
|
|
154
|
+
"description": getattr(app, 'description', 'Aplicación web progresiva creada con Dars'),
|
|
155
|
+
"start_url": ".",
|
|
156
|
+
"display": getattr(app, 'pwa_display', 'standalone'),
|
|
157
|
+
"background_color": getattr(app, 'background_color', '#ffffff'),
|
|
158
|
+
"theme_color": getattr(app, 'theme_color', '#4a90e2'),
|
|
159
|
+
"orientation": getattr(app, 'pwa_orientation', 'portrait')
|
|
160
|
+
}
|
|
161
|
+
icons = self._get_icons_manifest(app, output_path)
|
|
162
|
+
if icons is not None:
|
|
163
|
+
manifest["icons"] = icons
|
|
164
|
+
manifest_path = os.path.join(output_path, "manifest.json")
|
|
165
|
+
with open(manifest_path, 'w', encoding='utf-8') as f:
|
|
166
|
+
json.dump(manifest, f, indent=2)
|
|
167
|
+
|
|
168
|
+
def _get_icons_manifest(self, app: 'App', output_path: str) -> list:
|
|
169
|
+
import os, shutil
|
|
170
|
+
user_icons = getattr(app, 'icons', None)
|
|
171
|
+
if user_icons is not None:
|
|
172
|
+
# Si el usuario define icons=[] explícito, no ponemos icons
|
|
173
|
+
if isinstance(user_icons, list) and len(user_icons) == 0:
|
|
174
|
+
return None
|
|
175
|
+
# Si el usuario define iconos personalizados
|
|
176
|
+
icons_manifest = []
|
|
177
|
+
icons_dir = os.path.join(output_path, "icons")
|
|
178
|
+
os.makedirs(icons_dir, exist_ok=True)
|
|
179
|
+
for icon in user_icons:
|
|
180
|
+
if isinstance(icon, dict):
|
|
181
|
+
src = icon.get("src")
|
|
182
|
+
if src and os.path.isfile(src):
|
|
183
|
+
# Copiamos el icono al output
|
|
184
|
+
dest_path = os.path.join(icons_dir, os.path.basename(src))
|
|
185
|
+
shutil.copy(src, dest_path)
|
|
186
|
+
icon["src"] = f"icons/{os.path.basename(src)}"
|
|
187
|
+
icons_manifest.append(icon)
|
|
188
|
+
elif isinstance(icon, str):
|
|
189
|
+
# Si solo es una ruta, la copiamos y generamos el dict
|
|
190
|
+
if os.path.isfile(icon):
|
|
191
|
+
dest_path = os.path.join(icons_dir, os.path.basename(icon))
|
|
192
|
+
shutil.copy(icon, dest_path)
|
|
193
|
+
icons_manifest.append({
|
|
194
|
+
"src": f"icons/{os.path.basename(icon)}",
|
|
195
|
+
"sizes": "192x192",
|
|
196
|
+
"type": "image/png",
|
|
197
|
+
"purpose": "any maskable"
|
|
198
|
+
})
|
|
199
|
+
return icons_manifest if icons_manifest else None
|
|
200
|
+
# Si no hay icons definidos, ponemos por defecto
|
|
201
|
+
return [
|
|
202
|
+
{
|
|
203
|
+
"src": "icons/icon-192x192.png",
|
|
204
|
+
"sizes": "192x192",
|
|
205
|
+
"type": "image/png",
|
|
206
|
+
"purpose": "any maskable"
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"src": "icons/icon-512x512.png",
|
|
210
|
+
"sizes": "512x512",
|
|
211
|
+
"type": "image/png"
|
|
212
|
+
}
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
def _generate_default_icons(self, output_path: str) -> None:
|
|
216
|
+
import os, shutil
|
|
217
|
+
# Ruta de los iconos PWA por defecto incluidos en el framework
|
|
218
|
+
base_dir = os.path.dirname(os.path.abspath(__file__))
|
|
219
|
+
default_icons_dir = os.path.join(base_dir, "icons", "pwa")
|
|
220
|
+
icons_dir = os.path.join(output_path, "icons")
|
|
221
|
+
os.makedirs(icons_dir, exist_ok=True)
|
|
222
|
+
# Copiar icon-192x192.png y icon-512x512.png si existen
|
|
223
|
+
for fname in ["icon-192x192.png", "icon-512x512.png"]:
|
|
224
|
+
src = os.path.join(default_icons_dir, fname)
|
|
225
|
+
dst = os.path.join(icons_dir, fname)
|
|
226
|
+
if os.path.isfile(src):
|
|
227
|
+
shutil.copy(src, dst)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _generate_basic_service_worker(self, output_path: str) -> None:
|
|
231
|
+
sw_content = '''// Service Worker básico para Dars PWA
|
|
232
|
+
const CACHE_NAME = 'dars-pwa-cache-v1';
|
|
233
|
+
const urlsToCache = [
|
|
234
|
+
'/',
|
|
235
|
+
'/index.html',
|
|
236
|
+
'/styles.css',
|
|
237
|
+
'/script.js'
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
self.addEventListener('install', event => {
|
|
241
|
+
event.waitUntil(
|
|
242
|
+
caches.open(CACHE_NAME)
|
|
243
|
+
.then(cache => {
|
|
244
|
+
console.log('Cache abierto');
|
|
245
|
+
return cache.addAll(urlsToCache);
|
|
246
|
+
})
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
self.addEventListener('fetch', event => {
|
|
251
|
+
event.respondWith(
|
|
252
|
+
caches.match(event.request)
|
|
253
|
+
.then(response => {
|
|
254
|
+
if (response) {
|
|
255
|
+
return response;
|
|
256
|
+
}
|
|
257
|
+
return fetch(event.request);
|
|
258
|
+
})
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
'''
|
|
262
|
+
sw_path = os.path.join(output_path, "sw.js")
|
|
263
|
+
with open(sw_path, 'w', encoding='utf-8') as f:
|
|
264
|
+
f.write(sw_content)
|
|
265
|
+
|
|
266
|
+
def _generate_combined_script_js(self, scripts):
|
|
267
|
+
"""Combina y concatena el código de todos los scripts (InlineScript/FileScript)"""
|
|
268
|
+
js = ""
|
|
269
|
+
for script in scripts:
|
|
270
|
+
js += f"// Script: {script.__class__.__name__}\n"
|
|
271
|
+
js += script.get_code()
|
|
272
|
+
js += "\n\n"
|
|
273
|
+
return js
|
|
274
|
+
|
|
275
|
+
def generate_html(self, app: App, css_file: str = "styles.css", script_file: str = "script.js") -> str:
|
|
276
|
+
"""Genera el contenido HTML con todas las propiedades de la aplicación"""
|
|
277
|
+
body_content = ""
|
|
278
|
+
from dars.components.basic.container import Container
|
|
279
|
+
root_component = app.root
|
|
280
|
+
# Protección: si root es lista, envolver en Container correctamente
|
|
281
|
+
if isinstance(root_component, list):
|
|
282
|
+
root_component = Container(*root_component)
|
|
283
|
+
if root_component:
|
|
284
|
+
body_content = self.render_component(root_component)
|
|
285
|
+
|
|
286
|
+
# Generar meta tags
|
|
287
|
+
meta_tags_html = self._generate_meta_tags(app)
|
|
288
|
+
|
|
289
|
+
# Generar links (favicon, manifest, etc.)
|
|
290
|
+
links_html = self._generate_links(app)
|
|
291
|
+
|
|
292
|
+
# Generar Open Graph tags
|
|
293
|
+
og_tags_html = self._generate_open_graph_tags(app)
|
|
294
|
+
|
|
295
|
+
# Generar Twitter Card tags
|
|
296
|
+
twitter_tags_html = self._generate_twitter_tags(app)
|
|
297
|
+
|
|
298
|
+
html_template = f"""<!DOCTYPE html>
|
|
299
|
+
<html lang="{app.language}">
|
|
300
|
+
<head>
|
|
301
|
+
<meta charset="{app.config.get('charset', 'UTF-8')}">
|
|
302
|
+
{meta_tags_html}
|
|
303
|
+
<title>{app.title}</title>
|
|
304
|
+
{links_html}
|
|
305
|
+
{og_tags_html}
|
|
306
|
+
{twitter_tags_html}
|
|
307
|
+
<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=\"{css_file}\">
|
|
308
|
+
</head>
|
|
309
|
+
<body>
|
|
310
|
+
{body_content}
|
|
311
|
+
<script src=\"runtime_dars.js\"></script>
|
|
312
|
+
<script src=\"{script_file}\"></script>
|
|
313
|
+
</body>
|
|
314
|
+
</html>"""
|
|
315
|
+
# No modificar el HTML con BeautifulSoup para no perder tags/scripts
|
|
316
|
+
return html_template
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _generate_meta_tags(self, app: App) -> str:
|
|
320
|
+
"""Genera todos los meta tags de la aplicación"""
|
|
321
|
+
meta_tags = app.get_meta_tags()
|
|
322
|
+
meta_html = []
|
|
323
|
+
|
|
324
|
+
for name, content in meta_tags.items():
|
|
325
|
+
if content:
|
|
326
|
+
meta_html.append(f' <meta name="{name}" content="{content}">')
|
|
327
|
+
|
|
328
|
+
# Añadir canonical URL si está configurado
|
|
329
|
+
if app.canonical_url:
|
|
330
|
+
meta_html.append(f' <link rel="canonical" href="{app.canonical_url}">')
|
|
331
|
+
|
|
332
|
+
return '\n'.join(meta_html)
|
|
333
|
+
|
|
334
|
+
def _generate_links(self, app: App) -> str:
|
|
335
|
+
"""Genera los enlaces en el head del HTML"""
|
|
336
|
+
links = []
|
|
337
|
+
|
|
338
|
+
# Favicon
|
|
339
|
+
if hasattr(app, 'favicon'):
|
|
340
|
+
links.append(f'<link rel="icon" href="{app.favicon}" type="image/x-icon">')
|
|
341
|
+
|
|
342
|
+
# Manifest
|
|
343
|
+
if getattr(app, 'pwa_enabled', False):
|
|
344
|
+
links.append('<link rel="manifest" href="manifest.json">')
|
|
345
|
+
# Registrar service worker si está habilitado
|
|
346
|
+
if getattr(app, 'service_worker_enabled', True):
|
|
347
|
+
links.append("""
|
|
348
|
+
<script>
|
|
349
|
+
if ('serviceWorker' in navigator) {
|
|
350
|
+
window.addEventListener('load', () => {
|
|
351
|
+
navigator.serviceWorker.register('sw.js')
|
|
352
|
+
.then(registration => {
|
|
353
|
+
console.log('ServiceWorker registration successful');
|
|
354
|
+
})
|
|
355
|
+
.catch(err => {
|
|
356
|
+
console.log('ServiceWorker registration failed: ', err);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
</script>
|
|
361
|
+
""")
|
|
362
|
+
return "\n ".join(links)
|
|
363
|
+
|
|
364
|
+
def _generate_open_graph_tags(self, app: App) -> str:
|
|
365
|
+
"""Genera todos los tags Open Graph para redes sociales"""
|
|
366
|
+
og_tags = app.get_open_graph_tags()
|
|
367
|
+
og_html = []
|
|
368
|
+
|
|
369
|
+
for property_name, content in og_tags.items():
|
|
370
|
+
if content:
|
|
371
|
+
og_html.append(f' <meta property="{property_name}" content="{content}">')
|
|
372
|
+
|
|
373
|
+
return '\n'.join(og_html)
|
|
374
|
+
|
|
375
|
+
def _generate_twitter_tags(self, app: App) -> str:
|
|
376
|
+
"""Genera todos los tags de Twitter Card"""
|
|
377
|
+
twitter_tags = app.get_twitter_tags()
|
|
378
|
+
twitter_html = []
|
|
379
|
+
|
|
380
|
+
for name, content in twitter_tags.items():
|
|
381
|
+
if content:
|
|
382
|
+
twitter_html.append(f' <meta name="{name}" content="{content}">')
|
|
383
|
+
|
|
384
|
+
return '\n'.join(twitter_html)
|
|
385
|
+
|
|
386
|
+
def generate_css(self, app: App) -> str:
|
|
387
|
+
"""Genera el contenido CSS"""
|
|
388
|
+
css_content = """/* Estilos base de Dars */
|
|
389
|
+
* {
|
|
390
|
+
box-sizing: border-box;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
body {
|
|
394
|
+
margin: 0;
|
|
395
|
+
padding: 0;
|
|
396
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/* Estilos de componentes Dars */
|
|
400
|
+
.dars-container {
|
|
401
|
+
display: block;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.dars-text {
|
|
405
|
+
display: inline-block;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.dars-button {
|
|
409
|
+
display: inline-block;
|
|
410
|
+
padding: 8px 16px;
|
|
411
|
+
border: 1px solid #ccc;
|
|
412
|
+
background-color: #f8f9fa;
|
|
413
|
+
color: #333;
|
|
414
|
+
cursor: pointer;
|
|
415
|
+
border-radius: 4px;
|
|
416
|
+
font-size: 14px;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.dars-button:hover {
|
|
420
|
+
background-color: #e9ecef;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.dars-button:disabled {
|
|
424
|
+
opacity: 0.6;
|
|
425
|
+
cursor: not-allowed;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.dars-input {
|
|
429
|
+
display: inline-block;
|
|
430
|
+
padding: 8px 12px;
|
|
431
|
+
border: 1px solid #ccc;
|
|
432
|
+
border-radius: 4px;
|
|
433
|
+
font-size: 14px;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.dars-input:focus {
|
|
437
|
+
outline: none;
|
|
438
|
+
border-color: #007bff;
|
|
439
|
+
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.dars-image {
|
|
443
|
+
max-width: 100%;
|
|
444
|
+
height: auto;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.dars-link {
|
|
448
|
+
color: #007bff;
|
|
449
|
+
text-decoration: none;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.dars-link:hover {
|
|
453
|
+
text-decoration: underline;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
.dars-textarea {
|
|
457
|
+
width: 100%;
|
|
458
|
+
padding: 8px 12px;
|
|
459
|
+
border: 1px solid #ccc;
|
|
460
|
+
border-radius: 4px;
|
|
461
|
+
font-size: 14px;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.dars-textarea:focus {
|
|
465
|
+
outline: none;
|
|
466
|
+
border-color: #007bff;
|
|
467
|
+
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.dars-card {
|
|
471
|
+
background-color: white;
|
|
472
|
+
border-radius: 8px;
|
|
473
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
474
|
+
padding: 20px;
|
|
475
|
+
margin-bottom: 20px;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.dars-card h2 {
|
|
479
|
+
margin-top: 0;
|
|
480
|
+
margin-bottom: 15px;
|
|
481
|
+
font-size: 24px;
|
|
482
|
+
color: #333;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/* Table */
|
|
486
|
+
.dars-table {
|
|
487
|
+
width: 100%;
|
|
488
|
+
border-collapse: collapse;
|
|
489
|
+
margin-bottom: 20px;
|
|
490
|
+
background: white;
|
|
491
|
+
}
|
|
492
|
+
.dars-table th, .dars-table td {
|
|
493
|
+
border: 1px solid #ddd;
|
|
494
|
+
padding: 8px 12px;
|
|
495
|
+
text-align: left;
|
|
496
|
+
}
|
|
497
|
+
.dars-table th {
|
|
498
|
+
background: #f5f5f5;
|
|
499
|
+
font-weight: bold;
|
|
500
|
+
}
|
|
501
|
+
.dars-table tr:nth-child(even) {
|
|
502
|
+
background: #fafbfc;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/* Tabs */
|
|
506
|
+
.dars-tabs {
|
|
507
|
+
margin-bottom: 20px;
|
|
508
|
+
}
|
|
509
|
+
.dars-tabs-header {
|
|
510
|
+
display: flex;
|
|
511
|
+
border-bottom: 2px solid #eee;
|
|
512
|
+
margin-bottom: 10px;
|
|
513
|
+
}
|
|
514
|
+
.dars-tab {
|
|
515
|
+
background: none;
|
|
516
|
+
border: none;
|
|
517
|
+
padding: 10px 20px;
|
|
518
|
+
cursor: pointer;
|
|
519
|
+
font-size: 16px;
|
|
520
|
+
color: #555;
|
|
521
|
+
border-bottom: 2px solid transparent;
|
|
522
|
+
transition: border 0.2s, color 0.2s;
|
|
523
|
+
}
|
|
524
|
+
.dars-tab-active {
|
|
525
|
+
color: #007bff;
|
|
526
|
+
border-bottom: 2px solid #007bff;
|
|
527
|
+
font-weight: bold;
|
|
528
|
+
}
|
|
529
|
+
.dars-tab-panel {
|
|
530
|
+
display: none;
|
|
531
|
+
padding: 16px 0;
|
|
532
|
+
}
|
|
533
|
+
.dars-tab-panel-active {
|
|
534
|
+
display: block;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/* Accordion */
|
|
538
|
+
.dars-accordion {
|
|
539
|
+
border-radius: 8px;
|
|
540
|
+
overflow: hidden;
|
|
541
|
+
background: #fff;
|
|
542
|
+
margin-bottom: 20px;
|
|
543
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
|
544
|
+
}
|
|
545
|
+
.dars-accordion-section {
|
|
546
|
+
border-bottom: 1px solid #eee;
|
|
547
|
+
}
|
|
548
|
+
.dars-accordion-title {
|
|
549
|
+
padding: 14px 20px;
|
|
550
|
+
background: #f7f7f7;
|
|
551
|
+
cursor: pointer;
|
|
552
|
+
font-weight: 500;
|
|
553
|
+
transition: background 0.2s;
|
|
554
|
+
}
|
|
555
|
+
.dars-accordion-section.dars-accordion-open .dars-accordion-title {
|
|
556
|
+
background: #e9ecef;
|
|
557
|
+
}
|
|
558
|
+
.dars-accordion-content {
|
|
559
|
+
display: none;
|
|
560
|
+
padding: 16px 20px;
|
|
561
|
+
background: #fafbfc;
|
|
562
|
+
}
|
|
563
|
+
.dars-accordion-section.dars-accordion-open .dars-accordion-content {
|
|
564
|
+
display: block;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/* ProgressBar */
|
|
568
|
+
.dars-progressbar {
|
|
569
|
+
width: 100%;
|
|
570
|
+
background: #e9ecef;
|
|
571
|
+
border-radius: 8px;
|
|
572
|
+
overflow: hidden;
|
|
573
|
+
height: 20px;
|
|
574
|
+
margin-bottom: 20px;
|
|
575
|
+
}
|
|
576
|
+
.dars-progressbar-bar {
|
|
577
|
+
height: 100%;
|
|
578
|
+
background: linear-gradient(90deg, #007bff, #4a90e2);
|
|
579
|
+
transition: width 0.3s;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/* Spinner */
|
|
583
|
+
.dars-spinner {
|
|
584
|
+
border: 4px solid #e9ecef;
|
|
585
|
+
border-top: 4px solid #007bff;
|
|
586
|
+
border-radius: 50%;
|
|
587
|
+
width: 36px;
|
|
588
|
+
height: 36px;
|
|
589
|
+
animation: dars-spin 1s linear infinite;
|
|
590
|
+
margin: 10px auto;
|
|
591
|
+
}
|
|
592
|
+
@keyframes dars-spin {
|
|
593
|
+
0% { transform: rotate(0deg); }
|
|
594
|
+
100% { transform: rotate(360deg); }
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/* Tooltip */
|
|
598
|
+
.dars-tooltip {
|
|
599
|
+
position: relative;
|
|
600
|
+
display: inline-block;
|
|
601
|
+
cursor: pointer;
|
|
602
|
+
}
|
|
603
|
+
.dars-tooltip .dars-tooltip-text {
|
|
604
|
+
visibility: hidden;
|
|
605
|
+
width: max-content;
|
|
606
|
+
background: #333;
|
|
607
|
+
color: #fff;
|
|
608
|
+
text-align: center;
|
|
609
|
+
border-radius: 4px;
|
|
610
|
+
padding: 6px 10px;
|
|
611
|
+
position: absolute;
|
|
612
|
+
z-index: 10;
|
|
613
|
+
opacity: 0;
|
|
614
|
+
transition: opacity 0.2s;
|
|
615
|
+
font-size: 13px;
|
|
616
|
+
pointer-events: none;
|
|
617
|
+
}
|
|
618
|
+
.dars-tooltip:hover .dars-tooltip-text,
|
|
619
|
+
.dars-tooltip:focus .dars-tooltip-text {
|
|
620
|
+
visibility: visible;
|
|
621
|
+
opacity: 1;
|
|
622
|
+
}
|
|
623
|
+
.dars-tooltip-top .dars-tooltip-text {
|
|
624
|
+
bottom: 125%;
|
|
625
|
+
left: 50%;
|
|
626
|
+
transform: translateX(-50%);
|
|
627
|
+
margin-bottom: 6px;
|
|
628
|
+
}
|
|
629
|
+
.dars-tooltip-bottom .dars-tooltip-text {
|
|
630
|
+
top: 125%;
|
|
631
|
+
left: 50%;
|
|
632
|
+
transform: translateX(-50%);
|
|
633
|
+
margin-top: 6px;
|
|
634
|
+
}
|
|
635
|
+
.dars-tooltip-left .dars-tooltip-text {
|
|
636
|
+
right: 125%;
|
|
637
|
+
top: 50%;
|
|
638
|
+
transform: translateY(-50%);
|
|
639
|
+
margin-right: 6px;
|
|
640
|
+
}
|
|
641
|
+
.dars-tooltip-right .dars-tooltip-text {
|
|
642
|
+
left: 125%;
|
|
643
|
+
top: 50%;
|
|
644
|
+
transform: translateY(-50%);
|
|
645
|
+
margin-left: 6px;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.dars-modal {
|
|
649
|
+
display: none; /* Hidden by default */
|
|
650
|
+
position: fixed; /* Stay in place */
|
|
651
|
+
z-index: 1; /* Sit on top */
|
|
652
|
+
left: 0;
|
|
653
|
+
top: 0;
|
|
654
|
+
width: 100%; /* Full width */
|
|
655
|
+
height: 100%; /* Full height */
|
|
656
|
+
overflow: auto; /* Enable scroll if needed */
|
|
657
|
+
background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
|
|
658
|
+
justify-content: center;
|
|
659
|
+
align-items: center;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.dars-modal-content {
|
|
663
|
+
background-color: #fefefe;
|
|
664
|
+
margin: auto;
|
|
665
|
+
padding: 20px;
|
|
666
|
+
border: 1px solid #888;
|
|
667
|
+
width: 80%;
|
|
668
|
+
max-width: 500px;
|
|
669
|
+
border-radius: 8px;
|
|
670
|
+
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2), 0 6px 20px 0 rgba(0,0,0,0.19);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
.dars-navbar {
|
|
674
|
+
display: flex;
|
|
675
|
+
justify-content: space-between;
|
|
676
|
+
align-items: center;
|
|
677
|
+
padding: 1rem;
|
|
678
|
+
background-color: #f8f9fa;
|
|
679
|
+
border-bottom: 1px solid #dee2e6;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.dars-navbar-brand {
|
|
683
|
+
font-weight: bold;
|
|
684
|
+
font-size: 1.25rem;
|
|
685
|
+
color: #333;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
.dars-navbar-nav {
|
|
689
|
+
display: flex;
|
|
690
|
+
gap: 1rem;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
.dars-navbar-nav a {
|
|
694
|
+
color: #007bff;
|
|
695
|
+
text-decoration: none;
|
|
696
|
+
padding: 0.5rem 1rem;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
.dars-navbar-nav a:hover {
|
|
700
|
+
background-color: #e9ecef;
|
|
701
|
+
border-radius: 4px;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/* Estilos para nuevos componentes básicos */
|
|
705
|
+
|
|
706
|
+
/* Checkbox */
|
|
707
|
+
.dars-checkbox-wrapper {
|
|
708
|
+
display: flex;
|
|
709
|
+
align-items: center;
|
|
710
|
+
gap: 8px;
|
|
711
|
+
margin: 4px 0;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
.dars-checkbox {
|
|
715
|
+
width: 16px;
|
|
716
|
+
height: 16px;
|
|
717
|
+
cursor: pointer;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
.dars-checkbox:disabled {
|
|
721
|
+
opacity: 0.6;
|
|
722
|
+
cursor: not-allowed;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
.dars-checkbox-wrapper label {
|
|
726
|
+
cursor: pointer;
|
|
727
|
+
user-select: none;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/* RadioButton */
|
|
731
|
+
.dars-radio-wrapper {
|
|
732
|
+
display: flex;
|
|
733
|
+
align-items: center;
|
|
734
|
+
gap: 8px;
|
|
735
|
+
margin: 4px 0;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
.dars-radio {
|
|
739
|
+
width: 16px;
|
|
740
|
+
height: 16px;
|
|
741
|
+
cursor: pointer;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
.dars-radio:disabled {
|
|
745
|
+
opacity: 0.6;
|
|
746
|
+
cursor: not-allowed;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
.dars-radio-wrapper label {
|
|
750
|
+
cursor: pointer;
|
|
751
|
+
user-select: none;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/* Select */
|
|
755
|
+
.dars-select {
|
|
756
|
+
display: inline-block;
|
|
757
|
+
padding: 8px 12px;
|
|
758
|
+
border: 1px solid #ccc;
|
|
759
|
+
border-radius: 4px;
|
|
760
|
+
font-size: 14px;
|
|
761
|
+
background-color: white;
|
|
762
|
+
cursor: pointer;
|
|
763
|
+
min-width: 120px;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
.dars-select:focus {
|
|
767
|
+
outline: none;
|
|
768
|
+
border-color: #007bff;
|
|
769
|
+
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
.dars-select:disabled {
|
|
773
|
+
opacity: 0.6;
|
|
774
|
+
cursor: not-allowed;
|
|
775
|
+
background-color: #f8f9fa;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
.dars-select option:disabled {
|
|
779
|
+
color: #6c757d;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/* Slider */
|
|
783
|
+
.dars-slider-wrapper {
|
|
784
|
+
display: flex;
|
|
785
|
+
align-items: center;
|
|
786
|
+
gap: 12px;
|
|
787
|
+
margin: 8px 0;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
.dars-slider-wrapper.dars-slider-vertical {
|
|
791
|
+
flex-direction: column;
|
|
792
|
+
align-items: stretch;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
.dars-slider {
|
|
796
|
+
flex: 1;
|
|
797
|
+
cursor: pointer;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
.dars-slider-horizontal .dars-slider {
|
|
801
|
+
width: 100%;
|
|
802
|
+
height: 6px;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
.dars-slider-vertical input[type="range"] {
|
|
806
|
+
width: 8px;
|
|
807
|
+
height: 160px;
|
|
808
|
+
writing-mode: vertical-lr;
|
|
809
|
+
direction: rtl;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
.dars-slider:disabled {
|
|
813
|
+
opacity: 0.6;
|
|
814
|
+
cursor: not-allowed;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
.dars-slider-value {
|
|
818
|
+
font-weight: bold;
|
|
819
|
+
min-width: 40px;
|
|
820
|
+
text-align: center;
|
|
821
|
+
padding: 4px 8px;
|
|
822
|
+
background-color: #f8f9fa;
|
|
823
|
+
border-radius: 4px;
|
|
824
|
+
font-size: 12px;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
.dars-slider-wrapper label {
|
|
828
|
+
font-weight: 500;
|
|
829
|
+
margin-bottom: 4px;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/* DatePicker */
|
|
833
|
+
.dars-datepicker {
|
|
834
|
+
display: inline-block;
|
|
835
|
+
padding: 8px 12px;
|
|
836
|
+
border: 1px solid #ccc;
|
|
837
|
+
border-radius: 4px;
|
|
838
|
+
font-size: 14px;
|
|
839
|
+
background-color: white;
|
|
840
|
+
cursor: pointer;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
.dars-datepicker:focus {
|
|
844
|
+
outline: none;
|
|
845
|
+
border-color: #007bff;
|
|
846
|
+
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
.dars-datepicker:disabled {
|
|
850
|
+
opacity: 0.6;
|
|
851
|
+
cursor: not-allowed;
|
|
852
|
+
background-color: #f8f9fa;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
.dars-datepicker:readonly {
|
|
856
|
+
background-color: #f8f9fa;
|
|
857
|
+
cursor: default;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
.dars-datepicker-inline {
|
|
861
|
+
display: inline-block;
|
|
862
|
+
border: 1px solid #ccc;
|
|
863
|
+
border-radius: 4px;
|
|
864
|
+
padding: 12px;
|
|
865
|
+
background-color: white;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
.dars-datepicker-inline .dars-datepicker {
|
|
869
|
+
border: none;
|
|
870
|
+
padding: 0;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
"""
|
|
874
|
+
|
|
875
|
+
# Agregar estilos globales de la aplicación definidos por el usuario
|
|
876
|
+
for selector, styles in app.global_styles.items():
|
|
877
|
+
css_content += f"{selector} {{\n"
|
|
878
|
+
css_content += f" {self.render_styles(styles)}\n"
|
|
879
|
+
css_content += "}\n\n"
|
|
880
|
+
|
|
881
|
+
return css_content
|
|
882
|
+
|
|
883
|
+
def generate_javascript(self, app: App) -> str:
|
|
884
|
+
"""Genera el contenido JavaScript"""
|
|
885
|
+
js_content = """// Dars Runtime
|
|
886
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
887
|
+
console.log('Dars App loaded');
|
|
888
|
+
|
|
889
|
+
// Inicializar eventos de componentes
|
|
890
|
+
initializeEvents();
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
function initializeEvents() {
|
|
894
|
+
// Los eventos específicos se agregarán aquí
|
|
895
|
+
"""
|
|
896
|
+
|
|
897
|
+
def has_component_type(component, cls):
|
|
898
|
+
if isinstance(component, cls):
|
|
899
|
+
return True
|
|
900
|
+
if hasattr(component, 'children'):
|
|
901
|
+
for child in getattr(component, 'children', []):
|
|
902
|
+
if has_component_type(child, cls):
|
|
903
|
+
return True
|
|
904
|
+
return False
|
|
905
|
+
|
|
906
|
+
has_tabs = has_component_type(app.root, Tabs) if hasattr(app, 'root') else False
|
|
907
|
+
has_accordion = has_component_type(app.root, Accordion) if hasattr(app, 'root') else False
|
|
908
|
+
# Aquí puedes añadir otros has_<componente> para lógica futura
|
|
909
|
+
|
|
910
|
+
if has_tabs:
|
|
911
|
+
js_content += " // Tabs interactivas\n"
|
|
912
|
+
js_content += """ document.querySelectorAll('.dars-tabs').forEach(function(tabsEl) {
|
|
913
|
+
const tabButtons = tabsEl.querySelectorAll('.dars-tab');
|
|
914
|
+
const panels = tabsEl.querySelectorAll('.dars-tab-panel');
|
|
915
|
+
tabButtons.forEach(function(btn, i) {
|
|
916
|
+
btn.addEventListener('click', function() {
|
|
917
|
+
tabButtons.forEach(b => b.classList.remove('dars-tab-active'));
|
|
918
|
+
panels.forEach(p => p.classList.remove('dars-tab-panel-active'));
|
|
919
|
+
btn.classList.add('dars-tab-active');
|
|
920
|
+
if (panels[i]) panels[i].classList.add('dars-tab-panel-active');
|
|
921
|
+
});
|
|
922
|
+
});
|
|
923
|
+
});\n"""
|
|
924
|
+
if has_accordion:
|
|
925
|
+
js_content += " // Accordion interactivo\n"
|
|
926
|
+
js_content += """ document.querySelectorAll('.dars-accordion').forEach(function(accEl) {
|
|
927
|
+
accEl.querySelectorAll('.dars-accordion-title').forEach(function(titleEl) {
|
|
928
|
+
titleEl.addEventListener('click', function() {
|
|
929
|
+
const section = titleEl.parentElement;
|
|
930
|
+
const isOpen = section.classList.contains('dars-accordion-open');
|
|
931
|
+
if (isOpen) {
|
|
932
|
+
section.classList.remove('dars-accordion-open');
|
|
933
|
+
} else {
|
|
934
|
+
// Si es acordeón exclusivo, cerrar otros
|
|
935
|
+
accEl.querySelectorAll('.dars-accordion-section').forEach(function(sec) {
|
|
936
|
+
sec.classList.remove('dars-accordion-open');
|
|
937
|
+
});
|
|
938
|
+
section.classList.add('dars-accordion-open');
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
});
|
|
942
|
+
});\n"""
|
|
943
|
+
js_content += "}\n\n"
|
|
944
|
+
|
|
945
|
+
# Agregar scripts de la aplicación
|
|
946
|
+
for script in app.scripts:
|
|
947
|
+
js_content += f"// Script: {script.__class__.__name__}\n"
|
|
948
|
+
js_content += script.get_code()
|
|
949
|
+
js_content += "\n\n"
|
|
950
|
+
|
|
951
|
+
return js_content
|
|
952
|
+
|
|
953
|
+
def render_component(self, component: Component) -> str:
|
|
954
|
+
"""Renderiza un componente a HTML"""
|
|
955
|
+
from dars.components.basic.page import Page
|
|
956
|
+
from dars.components.layout.grid import GridLayout
|
|
957
|
+
from dars.components.layout.flex import FlexLayout
|
|
958
|
+
if isinstance(component, Page):
|
|
959
|
+
return self.render_page(component)
|
|
960
|
+
if isinstance(component, GridLayout):
|
|
961
|
+
return self.render_grid(component)
|
|
962
|
+
if isinstance(component, FlexLayout):
|
|
963
|
+
return self.render_flex(component)
|
|
964
|
+
if isinstance(component, Text):
|
|
965
|
+
return self.render_text(component)
|
|
966
|
+
elif isinstance(component, Button):
|
|
967
|
+
return self.render_button(component)
|
|
968
|
+
elif isinstance(component, Input):
|
|
969
|
+
return self.render_input(component)
|
|
970
|
+
elif isinstance(component, Container):
|
|
971
|
+
return self.render_container(component)
|
|
972
|
+
elif isinstance(component, Image):
|
|
973
|
+
return self.render_image(component)
|
|
974
|
+
elif isinstance(component, Link):
|
|
975
|
+
return self.render_link(component)
|
|
976
|
+
elif isinstance(component, Textarea):
|
|
977
|
+
return self.render_textarea(component)
|
|
978
|
+
elif isinstance(component, Card):
|
|
979
|
+
return self.render_card(component)
|
|
980
|
+
elif isinstance(component, Modal):
|
|
981
|
+
return self.render_modal(component)
|
|
982
|
+
elif isinstance(component, Navbar):
|
|
983
|
+
return self.render_navbar(component)
|
|
984
|
+
elif isinstance(component, Checkbox):
|
|
985
|
+
return self.render_checkbox(component)
|
|
986
|
+
elif isinstance(component, RadioButton):
|
|
987
|
+
return self.render_radiobutton(component)
|
|
988
|
+
elif isinstance(component, Select):
|
|
989
|
+
return self.render_select(component)
|
|
990
|
+
elif isinstance(component, Slider):
|
|
991
|
+
return self.render_slider(component)
|
|
992
|
+
elif isinstance(component, DatePicker):
|
|
993
|
+
return self.render_datepicker(component)
|
|
994
|
+
elif isinstance(component, Table):
|
|
995
|
+
return self.render_table(component)
|
|
996
|
+
elif isinstance(component, Tabs):
|
|
997
|
+
return self.render_tabs(component)
|
|
998
|
+
elif isinstance(component, Accordion):
|
|
999
|
+
return self.render_accordion(component)
|
|
1000
|
+
elif isinstance(component, ProgressBar):
|
|
1001
|
+
return self.render_progressbar(component)
|
|
1002
|
+
elif isinstance(component, Spinner):
|
|
1003
|
+
return self.render_spinner(component)
|
|
1004
|
+
elif isinstance(component, Tooltip):
|
|
1005
|
+
return self.render_tooltip(component)
|
|
1006
|
+
else:
|
|
1007
|
+
# Componente genérico
|
|
1008
|
+
return self.render_generic_component(component)
|
|
1009
|
+
|
|
1010
|
+
def render_grid(self, grid):
|
|
1011
|
+
"""Renderiza un GridLayout como un div con CSS grid."""
|
|
1012
|
+
component_id = self.generate_unique_id(grid)
|
|
1013
|
+
class_attr = f'class="dars-grid {grid.class_name or ""}"'
|
|
1014
|
+
style = f'display: grid; grid-template-rows: repeat({grid.rows}, 1fr); grid-template-columns: repeat({grid.cols}, 1fr); gap: {getattr(grid, "gap", "16px")};'
|
|
1015
|
+
# Render anchors/positions
|
|
1016
|
+
children_html = ""
|
|
1017
|
+
layout_info = getattr(grid, 'get_child_layout', lambda: [])()
|
|
1018
|
+
for child_info in layout_info:
|
|
1019
|
+
child = child_info['child']
|
|
1020
|
+
row = child_info.get('row', 0) + 1
|
|
1021
|
+
col = child_info.get('col', 0) + 1
|
|
1022
|
+
row_span = child_info.get('row_span', 1)
|
|
1023
|
+
col_span = child_info.get('col_span', 1)
|
|
1024
|
+
anchor = child_info.get('anchor')
|
|
1025
|
+
anchor_style = ''
|
|
1026
|
+
if anchor:
|
|
1027
|
+
if isinstance(anchor, str):
|
|
1028
|
+
anchor_map = {
|
|
1029
|
+
'top-left': 'justify-self: start; align-self: start;',
|
|
1030
|
+
'top': 'justify-self: center; align-self: start;',
|
|
1031
|
+
'top-right': 'justify-self: end; align-self: start;',
|
|
1032
|
+
'left': 'justify-self: start; align-self: center;',
|
|
1033
|
+
'center': 'justify-self: center; align-self: center;',
|
|
1034
|
+
'right': 'justify-self: end; align-self: center;',
|
|
1035
|
+
'bottom-left': 'justify-self: start; align-self: end;',
|
|
1036
|
+
'bottom': 'justify-self: center; align-self: end;',
|
|
1037
|
+
'bottom-right': 'justify-self: end; align-self: end;'
|
|
1038
|
+
}
|
|
1039
|
+
anchor_style = anchor_map.get(anchor, '')
|
|
1040
|
+
elif hasattr(anchor, 'x') or hasattr(anchor, 'y'):
|
|
1041
|
+
# AnchorPoint object
|
|
1042
|
+
if getattr(anchor, 'x', None):
|
|
1043
|
+
if anchor.x == 'left': anchor_style += 'justify-self: start;'
|
|
1044
|
+
elif anchor.x == 'center': anchor_style += 'justify-self: center;'
|
|
1045
|
+
elif anchor.x == 'right': anchor_style += 'justify-self: end;'
|
|
1046
|
+
elif '%' in anchor.x or 'px' in anchor.x: anchor_style += f'left: {anchor.x}; position: relative;'
|
|
1047
|
+
if getattr(anchor, 'y', None):
|
|
1048
|
+
if anchor.y == 'top': anchor_style += 'align-self: start;'
|
|
1049
|
+
elif anchor.y == 'center': anchor_style += 'align-self: center;'
|
|
1050
|
+
elif anchor.y == 'bottom': anchor_style += 'align-self: end;'
|
|
1051
|
+
elif '%' in anchor.y or 'px' in anchor.y: anchor_style += f'top: {anchor.y}; position: relative;'
|
|
1052
|
+
grid_item_style = f'grid-row: {row} / span {row_span}; grid-column: {col} / span {col_span}; {anchor_style}'
|
|
1053
|
+
children_html += f'<div style="{grid_item_style}">{self.render_component(child)}</div>'
|
|
1054
|
+
return f'<div id="{component_id}" {class_attr} style="{style}">{children_html}</div>'
|
|
1055
|
+
|
|
1056
|
+
def render_flex(self, flex):
|
|
1057
|
+
"""Renderiza un FlexLayout como un div con CSS flexbox."""
|
|
1058
|
+
component_id = self.generate_unique_id(flex)
|
|
1059
|
+
class_attr = f'class="dars-flex {flex.class_name or ""}"'
|
|
1060
|
+
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")};'
|
|
1061
|
+
children_html = ""
|
|
1062
|
+
for child in flex.children:
|
|
1063
|
+
anchor = getattr(child, 'anchor', None)
|
|
1064
|
+
anchor_style = ''
|
|
1065
|
+
if anchor:
|
|
1066
|
+
if isinstance(anchor, str):
|
|
1067
|
+
anchor_map = {
|
|
1068
|
+
'top-left': 'align-self: flex-start; justify-self: flex-start;',
|
|
1069
|
+
'top': 'align-self: flex-start; margin-left: auto; margin-right: auto;',
|
|
1070
|
+
'top-right': 'align-self: flex-start; margin-left: auto;',
|
|
1071
|
+
'left': 'align-self: center;',
|
|
1072
|
+
'center': 'align-self: center; margin-left: auto; margin-right: auto;',
|
|
1073
|
+
'right': 'align-self: center; margin-left: auto;',
|
|
1074
|
+
'bottom-left': 'align-self: flex-end;',
|
|
1075
|
+
'bottom': 'align-self: flex-end; margin-left: auto; margin-right: auto;',
|
|
1076
|
+
'bottom-right': 'align-self: flex-end; margin-left: auto;'
|
|
1077
|
+
}
|
|
1078
|
+
anchor_style = anchor_map.get(anchor, '')
|
|
1079
|
+
elif hasattr(anchor, 'x') or hasattr(anchor, 'y'):
|
|
1080
|
+
if getattr(anchor, 'x', None):
|
|
1081
|
+
if anchor.x == 'left': anchor_style += 'margin-right: auto;'
|
|
1082
|
+
elif anchor.x == 'center': anchor_style += 'margin-left: auto; margin-right: auto;'
|
|
1083
|
+
elif anchor.x == 'right': anchor_style += 'margin-left: auto;'
|
|
1084
|
+
elif '%' in anchor.x or 'px' in anchor.x: anchor_style += f'left: {anchor.x}; position: relative;'
|
|
1085
|
+
if getattr(anchor, 'y', None):
|
|
1086
|
+
if anchor.y == 'top': anchor_style += 'align-self: flex-start;'
|
|
1087
|
+
elif anchor.y == 'center': anchor_style += 'align-self: center;'
|
|
1088
|
+
elif anchor.y == 'bottom': anchor_style += 'align-self: flex-end;'
|
|
1089
|
+
elif '%' in anchor.y or 'px' in anchor.y: anchor_style += f'top: {anchor.y}; position: relative;'
|
|
1090
|
+
children_html += f'<div style="{anchor_style}">{self.render_component(child)}</div>'
|
|
1091
|
+
return f'<div id="{component_id}" {class_attr} style="{style}">{children_html}</div>'
|
|
1092
|
+
|
|
1093
|
+
def render_page(self, page):
|
|
1094
|
+
"""Renderiza un componente Page como root de una página multipage"""
|
|
1095
|
+
component_id = self.generate_unique_id(page)
|
|
1096
|
+
class_attr = f'class="dars-page {page.class_name or ""}"'
|
|
1097
|
+
style_attr = f'style="{self.render_styles(page.style)}"' if page.style else ""
|
|
1098
|
+
# Renderizar hijos
|
|
1099
|
+
children_html = ""
|
|
1100
|
+
children = getattr(page, 'children', [])
|
|
1101
|
+
if not isinstance(children, list):
|
|
1102
|
+
children = []
|
|
1103
|
+
for child in children:
|
|
1104
|
+
if hasattr(child, 'render'):
|
|
1105
|
+
children_html += self.render_component(child)
|
|
1106
|
+
return f'<div id="{component_id}" {class_attr} {style_attr}>{children_html}</div>'
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
def render_text(self, text: Text) -> str:
|
|
1111
|
+
"""Renderiza un componente Text"""
|
|
1112
|
+
component_id = self.generate_unique_id(text)
|
|
1113
|
+
class_attr = f'class="dars-text {text.class_name or ""}"'
|
|
1114
|
+
style_attr = f'style="{self.render_styles(text.style)}"' if text.style else ""
|
|
1115
|
+
|
|
1116
|
+
return f'<span id="{component_id}" {class_attr} {style_attr}>{text.text}</span>'
|
|
1117
|
+
|
|
1118
|
+
def render_button(self, button: Button) -> str:
|
|
1119
|
+
"""Renderiza un componente Button"""
|
|
1120
|
+
component_id = self.generate_unique_id(button)
|
|
1121
|
+
class_attr = f'class="dars-button {button.class_name or ""}"'
|
|
1122
|
+
style_attr = f'style="{self.render_styles(button.style)}"' if button.style else ""
|
|
1123
|
+
disabled_attr = "disabled" if button.disabled else ""
|
|
1124
|
+
type_attr = f'type="{button.button_type}"'
|
|
1125
|
+
|
|
1126
|
+
return f'<button id="{component_id}" {class_attr} {style_attr} {type_attr} {disabled_attr}>{button.text}</button>'
|
|
1127
|
+
|
|
1128
|
+
def render_input(self, input_comp: Input) -> str:
|
|
1129
|
+
"""Renderiza un componente Input"""
|
|
1130
|
+
component_id = self.generate_unique_id(input_comp)
|
|
1131
|
+
class_attr = f'class="dars-input {input_comp.class_name or ""}"'
|
|
1132
|
+
style_attr = f'style="{self.render_styles(input_comp.style)}"' if input_comp.style else ""
|
|
1133
|
+
type_attr = f'type="{input_comp.input_type}"'
|
|
1134
|
+
value_attr = f'value="{input_comp.value}"' if input_comp.value else ""
|
|
1135
|
+
placeholder_attr = f'placeholder="{input_comp.placeholder}"' if input_comp.placeholder else ""
|
|
1136
|
+
disabled_attr = "disabled" if input_comp.disabled else ""
|
|
1137
|
+
readonly_attr = "readonly" if input_comp.readonly else ""
|
|
1138
|
+
required_attr = "required" if input_comp.required else ""
|
|
1139
|
+
|
|
1140
|
+
attrs = [class_attr, style_attr, type_attr, value_attr, placeholder_attr,
|
|
1141
|
+
disabled_attr, readonly_attr, required_attr]
|
|
1142
|
+
attrs_str = " ".join(attr for attr in attrs if attr)
|
|
1143
|
+
|
|
1144
|
+
return f'<input id="{component_id}" {attrs_str} />'
|
|
1145
|
+
|
|
1146
|
+
def render_container(self, container: Container) -> str:
|
|
1147
|
+
"""Renderiza un componente Container"""
|
|
1148
|
+
component_id = self.generate_unique_id(container)
|
|
1149
|
+
class_attr = f'class="dars-container {container.class_name or ""}"'
|
|
1150
|
+
style_attr = f'style="{self.render_styles(container.style)}"' if container.style else ""
|
|
1151
|
+
|
|
1152
|
+
# Protección: asegurar que children es lista de Component
|
|
1153
|
+
children_html = ""
|
|
1154
|
+
children = container.children
|
|
1155
|
+
if not isinstance(children, list):
|
|
1156
|
+
children = []
|
|
1157
|
+
# Aplanar si hay listas anidadas
|
|
1158
|
+
flat_children = []
|
|
1159
|
+
for child in children:
|
|
1160
|
+
if isinstance(child, list):
|
|
1161
|
+
flat_children.extend([c for c in child if hasattr(c, 'render')])
|
|
1162
|
+
elif hasattr(child, 'render'):
|
|
1163
|
+
flat_children.append(child)
|
|
1164
|
+
for child in flat_children:
|
|
1165
|
+
children_html += self.render_component(child)
|
|
1166
|
+
|
|
1167
|
+
return f'<div id="{component_id}" {class_attr} {style_attr}>{children_html}</div>'
|
|
1168
|
+
|
|
1169
|
+
def render_image(self, image: Image) -> str:
|
|
1170
|
+
"""Renderiza un componente Image"""
|
|
1171
|
+
component_id = self.generate_unique_id(image)
|
|
1172
|
+
class_attr = f'class="dars-image {image.class_name or ""}"'
|
|
1173
|
+
style_attr = f'style="{self.render_styles(image.style)}"' if image.style else ""
|
|
1174
|
+
width_attr = f'width="{image.width}"' if image.width else ""
|
|
1175
|
+
height_attr = f'height="{image.height}"' if image.height else ""
|
|
1176
|
+
|
|
1177
|
+
return f'<img id="{component_id}" src="{image.src}" alt="{image.alt}" {width_attr} {height_attr} {class_attr} {style_attr} />'
|
|
1178
|
+
|
|
1179
|
+
def render_link(self, link: Link) -> str:
|
|
1180
|
+
"""Renderiza un componente Link"""
|
|
1181
|
+
component_id = self.generate_unique_id(link)
|
|
1182
|
+
class_attr = f'class="dars-link {link.class_name or ""}"'
|
|
1183
|
+
style_attr = f'style="{self.render_styles(link.style)}"' if link.style else ""
|
|
1184
|
+
target_attr = f'target="{link.target}"'
|
|
1185
|
+
|
|
1186
|
+
return f'<a id="{component_id}" href="{link.href}" {target_attr} {class_attr} {style_attr}>{link.text}</a>'
|
|
1187
|
+
|
|
1188
|
+
def render_textarea(self, textarea: Textarea) -> str:
|
|
1189
|
+
"""Renderiza un componente Textarea"""
|
|
1190
|
+
component_id = self.generate_unique_id(textarea)
|
|
1191
|
+
class_attr = f'class="dars-textarea {textarea.class_name or ""}"'
|
|
1192
|
+
style_attr = f'style="{self.render_styles(textarea.style)}"' if textarea.style else ""
|
|
1193
|
+
rows_attr = f'rows="{textarea.rows}"'
|
|
1194
|
+
cols_attr = f'cols="{textarea.cols}"'
|
|
1195
|
+
placeholder_attr = f'placeholder="{textarea.placeholder}"' if textarea.placeholder else ""
|
|
1196
|
+
disabled_attr = "disabled" if textarea.disabled else ""
|
|
1197
|
+
readonly_attr = "readonly" if textarea.readonly else ""
|
|
1198
|
+
required_attr = "required" if textarea.required else ""
|
|
1199
|
+
maxlength_attr = f'maxlength="{textarea.max_length}"' if textarea.max_length else ""
|
|
1200
|
+
|
|
1201
|
+
attrs = [class_attr, style_attr, rows_attr, cols_attr, placeholder_attr,
|
|
1202
|
+
disabled_attr, readonly_attr, required_attr, maxlength_attr]
|
|
1203
|
+
attrs_str = " ".join(attr for attr in attrs if attr)
|
|
1204
|
+
|
|
1205
|
+
return f'<textarea id="{component_id}" {attrs_str}>{textarea.value}</textarea>'
|
|
1206
|
+
|
|
1207
|
+
def render_card(self, card: Card) -> str:
|
|
1208
|
+
"""Renderiza un componente Card"""
|
|
1209
|
+
component_id = self.generate_unique_id(card)
|
|
1210
|
+
class_attr = f'class="dars-card {card.class_name or ""}"'
|
|
1211
|
+
style_attr = f'style="{self.render_styles(card.style)}"' if card.style else ""
|
|
1212
|
+
title_html = f'<h2>{card.title}</h2>' if card.title else ""
|
|
1213
|
+
children_html = ""
|
|
1214
|
+
for child in card.children:
|
|
1215
|
+
children_html += self.render_component(child)
|
|
1216
|
+
|
|
1217
|
+
return f'<div id="{component_id}" {class_attr} {style_attr}>{title_html}{children_html}</div>'
|
|
1218
|
+
|
|
1219
|
+
def render_modal(self, modal: Modal) -> str:
|
|
1220
|
+
"""Renderiza un componente Modal"""
|
|
1221
|
+
component_id = self.generate_unique_id(modal)
|
|
1222
|
+
class_attr = f'class="dars-modal {modal.class_name or ""}"'
|
|
1223
|
+
style_attr = f'style="{self.render_styles(modal.style)}"' if modal.style else ""
|
|
1224
|
+
title_html = f'<h2>{modal.title}</h2>' if modal.title else ""
|
|
1225
|
+
children_html = ""
|
|
1226
|
+
for child in modal.children:
|
|
1227
|
+
children_html += self.render_component(child)
|
|
1228
|
+
|
|
1229
|
+
display_style = "display: flex;" if modal.is_open else "display: none;"
|
|
1230
|
+
modal_overlay_style = f'style="{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; {style_attr}"'
|
|
1231
|
+
|
|
1232
|
+
return f'<div id="{component_id}" {class_attr} {modal_overlay_style}>\n <div class="dars-modal-content" style="background: white; padding: 20px; border-radius: 8px; max-width: 500px; width: 90%;">\n {title_html}\n {children_html}\n </div>\n</div>'
|
|
1233
|
+
|
|
1234
|
+
def render_navbar(self, navbar: Navbar) -> str:
|
|
1235
|
+
"""Renderiza un componente Navbar"""
|
|
1236
|
+
component_id = self.generate_unique_id(navbar)
|
|
1237
|
+
class_attr = f'class="dars-navbar {navbar.class_name or ""}"'
|
|
1238
|
+
style_attr = f'style="{self.render_styles(navbar.style)}"' if navbar.style else ""
|
|
1239
|
+
brand_html = f'<div class="dars-navbar-brand">{navbar.brand}</div>' if navbar.brand else ""
|
|
1240
|
+
children_html = ""
|
|
1241
|
+
for child in navbar.children:
|
|
1242
|
+
children_html += self.render_component(child)
|
|
1243
|
+
|
|
1244
|
+
return f'<nav id="{component_id}" {class_attr} {style_attr}>{brand_html}<div class="dars-navbar-nav">{children_html}</div></nav>'
|
|
1245
|
+
|
|
1246
|
+
def render_checkbox(self, checkbox: Checkbox) -> str:
|
|
1247
|
+
"""Renderiza un componente Checkbox"""
|
|
1248
|
+
component_id = self.generate_unique_id(checkbox)
|
|
1249
|
+
class_attr = f'class="dars-checkbox {checkbox.class_name or ""}"'
|
|
1250
|
+
style_attr = f'style="{self.render_styles(checkbox.style)}"' if checkbox.style else ""
|
|
1251
|
+
checked_attr = "checked" if checkbox.checked else ""
|
|
1252
|
+
disabled_attr = "disabled" if checkbox.disabled else ""
|
|
1253
|
+
required_attr = "required" if checkbox.required else ""
|
|
1254
|
+
name_attr = f'name="{checkbox.name}"' if checkbox.name else ""
|
|
1255
|
+
value_attr = f'value="{checkbox.value}"' if checkbox.value else ""
|
|
1256
|
+
|
|
1257
|
+
attrs = [class_attr, style_attr, checked_attr, disabled_attr, required_attr, name_attr, value_attr]
|
|
1258
|
+
attrs_str = " ".join(attr for attr in attrs if attr)
|
|
1259
|
+
|
|
1260
|
+
label_html = f'<label for="{component_id}">{checkbox.label}</label>' if checkbox.label else ""
|
|
1261
|
+
|
|
1262
|
+
return f'<div class="dars-checkbox-wrapper"><input type="checkbox" id="{component_id}" {attrs_str}>{label_html}</div>'
|
|
1263
|
+
|
|
1264
|
+
def render_radiobutton(self, radio: RadioButton) -> str:
|
|
1265
|
+
"""Renderiza un componente RadioButton"""
|
|
1266
|
+
component_id = self.generate_unique_id(radio)
|
|
1267
|
+
class_attr = f'class="dars-radio {radio.class_name or ""}"'
|
|
1268
|
+
style_attr = f'style="{self.render_styles(radio.style)}"' if radio.style else ""
|
|
1269
|
+
checked_attr = "checked" if radio.checked else ""
|
|
1270
|
+
disabled_attr = "disabled" if radio.disabled else ""
|
|
1271
|
+
required_attr = "required" if radio.required else ""
|
|
1272
|
+
name_attr = f'name="{radio.name}"'
|
|
1273
|
+
value_attr = f'value="{radio.value}"'
|
|
1274
|
+
|
|
1275
|
+
attrs = [class_attr, style_attr, checked_attr, disabled_attr, required_attr, name_attr, value_attr]
|
|
1276
|
+
attrs_str = " ".join(attr for attr in attrs if attr)
|
|
1277
|
+
|
|
1278
|
+
label_html = f'<label for="{component_id}">{radio.label}</label>' if radio.label else ""
|
|
1279
|
+
|
|
1280
|
+
return f'<div class="dars-radio-wrapper"><input type="radio" id="{component_id}" {attrs_str}>{label_html}</div>'
|
|
1281
|
+
|
|
1282
|
+
def render_select(self, select: Select) -> str:
|
|
1283
|
+
"""Renderiza un componente Select"""
|
|
1284
|
+
component_id = self.generate_unique_id(select)
|
|
1285
|
+
class_attr = f'class="dars-select {select.class_name or ""}"'
|
|
1286
|
+
style_attr = f'style="{self.render_styles(select.style)}"' if select.style else ""
|
|
1287
|
+
disabled_attr = "disabled" if select.disabled else ""
|
|
1288
|
+
required_attr = "required" if select.required else ""
|
|
1289
|
+
multiple_attr = "multiple" if select.multiple else ""
|
|
1290
|
+
size_attr = f'size="{select.size}"' if select.size else ""
|
|
1291
|
+
|
|
1292
|
+
attrs = [class_attr, style_attr, disabled_attr, required_attr, multiple_attr, size_attr]
|
|
1293
|
+
attrs_str = " ".join(attr for attr in attrs if attr)
|
|
1294
|
+
|
|
1295
|
+
# Generar opciones
|
|
1296
|
+
options_html = ""
|
|
1297
|
+
if select.placeholder and not select.multiple:
|
|
1298
|
+
selected = "selected" if not select.value else ""
|
|
1299
|
+
options_html += f'<option value="" disabled {selected}>{select.placeholder}</option>'
|
|
1300
|
+
|
|
1301
|
+
for option in select.options:
|
|
1302
|
+
selected = "selected" if option.value == select.value else ""
|
|
1303
|
+
disabled = "disabled" if option.disabled else ""
|
|
1304
|
+
options_html += f'<option value="{option.value}" {selected} {disabled}>{option.label}</option>'
|
|
1305
|
+
|
|
1306
|
+
return f'<select id="{component_id}" {attrs_str}>{options_html}</select>'
|
|
1307
|
+
|
|
1308
|
+
def render_slider(self, slider: Slider) -> str:
|
|
1309
|
+
"""Renderiza un componente Slider"""
|
|
1310
|
+
component_id = self.generate_unique_id(slider)
|
|
1311
|
+
class_attr = f'class="dars-slider {slider.class_name or ""}"'
|
|
1312
|
+
style_attr = f'style="{self.render_styles(slider.style)}"' if slider.style else ""
|
|
1313
|
+
disabled_attr = "disabled" if slider.disabled else ""
|
|
1314
|
+
min_attr = f'min="{slider.min_value}"'
|
|
1315
|
+
max_attr = f'max="{slider.max_value}"'
|
|
1316
|
+
value_attr = f'value="{slider.value}"'
|
|
1317
|
+
step_attr = f'step="{slider.step}"'
|
|
1318
|
+
|
|
1319
|
+
attrs = [class_attr, style_attr, disabled_attr, min_attr, max_attr, value_attr, step_attr]
|
|
1320
|
+
attrs_str = " ".join(attr for attr in attrs if attr)
|
|
1321
|
+
|
|
1322
|
+
label_html = f'<label for="{component_id}">{slider.label}</label>' if slider.label else ""
|
|
1323
|
+
value_display = f'<span class="dars-slider-value">{slider.value}</span>' if slider.show_value else ""
|
|
1324
|
+
|
|
1325
|
+
wrapper_class = "dars-slider-vertical" if slider.orientation == "vertical" else "dars-slider-horizontal"
|
|
1326
|
+
|
|
1327
|
+
return f'<div class="dars-slider-wrapper {wrapper_class}">{label_html}<input type="range" id="{component_id}" {attrs_str}>{value_display}</div>'
|
|
1328
|
+
|
|
1329
|
+
def render_datepicker(self, datepicker: DatePicker) -> str:
|
|
1330
|
+
"""Renderiza un componente DatePicker"""
|
|
1331
|
+
component_id = self.generate_unique_id(datepicker)
|
|
1332
|
+
class_attr = f'class="dars-datepicker {datepicker.class_name or ""}"'
|
|
1333
|
+
style_attr = f'style="{self.render_styles(datepicker.style)}"' if datepicker.style else ""
|
|
1334
|
+
disabled_attr = "disabled" if datepicker.disabled else ""
|
|
1335
|
+
required_attr = "required" if datepicker.required else ""
|
|
1336
|
+
readonly_attr = "readonly" if datepicker.readonly else ""
|
|
1337
|
+
value_attr = f'value="{datepicker.value}"' if datepicker.value else ""
|
|
1338
|
+
placeholder_attr = f'placeholder="{datepicker.placeholder}"' if datepicker.placeholder else ""
|
|
1339
|
+
min_attr = f'min="{datepicker.min_date}"' if datepicker.min_date else ""
|
|
1340
|
+
max_attr = f'max="{datepicker.max_date}"' if datepicker.max_date else ""
|
|
1341
|
+
|
|
1342
|
+
# Determinar el tipo de input según si incluye tiempo
|
|
1343
|
+
input_type = "datetime-local" if datepicker.show_time else "date"
|
|
1344
|
+
|
|
1345
|
+
attrs = [class_attr, style_attr, disabled_attr, required_attr, readonly_attr,
|
|
1346
|
+
value_attr, placeholder_attr, min_attr, max_attr]
|
|
1347
|
+
attrs_str = " ".join(attr for attr in attrs if attr)
|
|
1348
|
+
|
|
1349
|
+
# Si es inline, usar un div contenedor adicional
|
|
1350
|
+
if datepicker.inline:
|
|
1351
|
+
return f'<div class="dars-datepicker-inline"><input type="{input_type}" id="{component_id}" {attrs_str}></div>'
|
|
1352
|
+
else:
|
|
1353
|
+
return f'<input type="{input_type}" id="{component_id}" {attrs_str}>'
|
|
1354
|
+
|
|
1355
|
+
def render_table(self, table: Table) -> str:
|
|
1356
|
+
# Renderizado HTML para Table
|
|
1357
|
+
thead = '<thead><tr>' + ''.join(f'<th>{col["title"]}</th>' for col in table.columns) + '</tr></thead>'
|
|
1358
|
+
rows = table.data[:table.page_size] if table.page_size else table.data
|
|
1359
|
+
tbody = '<tbody>' + ''.join(
|
|
1360
|
+
'<tr>' + ''.join(f'<td>{row.get(col["field"], "")}</td>' for col in table.columns) + '</tr>'
|
|
1361
|
+
for row in rows) + '</tbody>'
|
|
1362
|
+
return f'<table class="dars-table">{thead}{tbody}</table>'
|
|
1363
|
+
|
|
1364
|
+
def render_tabs(self, tabs: Tabs) -> str:
|
|
1365
|
+
tab_headers = ''.join(
|
|
1366
|
+
f'<button class="dars-tab{ " dars-tab-active" if i == tabs.selected else "" }" data-tab="{i}">{title}</button>'
|
|
1367
|
+
for i, title in enumerate(tabs.tabs)
|
|
1368
|
+
)
|
|
1369
|
+
panels_html = ''.join(
|
|
1370
|
+
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>'
|
|
1371
|
+
for i, panel in enumerate(tabs.panels)
|
|
1372
|
+
)
|
|
1373
|
+
return f'<div class="dars-tabs"><div class="dars-tabs-header">{tab_headers}</div><div class="dars-tabs-panels">{panels_html}</div></div>'
|
|
1374
|
+
|
|
1375
|
+
def render_accordion(self, accordion: Accordion) -> str:
|
|
1376
|
+
html = '<div class="dars-accordion">'
|
|
1377
|
+
for i, (title, content) in enumerate(accordion.sections):
|
|
1378
|
+
opened = ' dars-accordion-open' if i in accordion.open_indices else ''
|
|
1379
|
+
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>'
|
|
1380
|
+
html += '</div>'
|
|
1381
|
+
return html
|
|
1382
|
+
|
|
1383
|
+
def render_progressbar(self, bar: ProgressBar) -> str:
|
|
1384
|
+
percent = min(max(bar.value / bar.max_value * 100, 0), 100)
|
|
1385
|
+
return f'<div class="dars-progressbar"><div class="dars-progressbar-bar" style="width: {percent}%;"></div></div>'
|
|
1386
|
+
|
|
1387
|
+
def render_spinner(self, spinner: Spinner) -> str:
|
|
1388
|
+
return '<div class="dars-spinner"></div>'
|
|
1389
|
+
|
|
1390
|
+
def render_tooltip(self, tooltip: Tooltip) -> str:
|
|
1391
|
+
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>'
|
|
1392
|
+
|
|
1393
|
+
def render_generic_component(self, component: Component) -> str:
|
|
1394
|
+
"""Renderiza un componente genérico"""
|
|
1395
|
+
component_id = self.generate_unique_id(component)
|
|
1396
|
+
class_attr = f'class="{component.class_name or ""}"'
|
|
1397
|
+
style_attr = f'style="{self.render_styles(component.style)}"' if component.style else ""
|
|
1398
|
+
|
|
1399
|
+
# Renderizar hijos
|
|
1400
|
+
children_html = ""
|
|
1401
|
+
for child in component.children:
|
|
1402
|
+
children_html += self.render_component(child)
|
|
1403
|
+
|
|
1404
|
+
return f'<div id="{component_id}" {class_attr} {style_attr}>{children_html}</div>'
|
|
1405
|
+
|
|
1406
|
+
|