aecode 0.2.0__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.
aecode/ui_engine.py ADDED
@@ -0,0 +1,661 @@
1
+ import re
2
+ import json
3
+ import webbrowser
4
+ from pathlib import Path
5
+ from lark import Lark, Transformer, v_args
6
+
7
+ AEUI_GRAMMAR = r'''
8
+ start: statement*
9
+
10
+ ?statement: page_stmt
11
+ | nav_stmt
12
+ | hero_stmt
13
+ | grid_stmt
14
+ | btn_stmt
15
+ | testimonials_stmt
16
+ | form_stmt
17
+ | canvas_stmt
18
+ | palette_stmt
19
+ | sizes_stmt
20
+ | tools_stmt
21
+ | footer_stmt
22
+ | state_stmt
23
+ | comment
24
+
25
+ page_stmt: "PAGE" (PATH | IDENT)
26
+ state_stmt: "STATE" /.+/
27
+ nav_stmt: "NAV" "[" [nav_item ("," nav_item)*] "]"
28
+ ?nav_item: links_call | nav_text
29
+ links_call: "Links(" [LINK_ITEM ("," LINK_ITEM)*] ")"
30
+ nav_text: /[^,\]\r\n]+/
31
+
32
+ hero_stmt: "HERO" ESCAPED_STRING [ALIGN]
33
+ ALIGN: "center" | "left" | "right"
34
+
35
+ grid_stmt: "GRID" INT (">" grid_target | block)
36
+ grid_target: card_call | statement | block
37
+ block: "{" statement* "}" | "[" statement* "]"
38
+ card_call: "CARD" "(" [FIELD ("," FIELD)*] ")"
39
+
40
+ btn_stmt: "BTN" ESCAPED_STRING
41
+ testimonials_stmt: "TESTIMONIALS"
42
+ form_stmt: "FORM" ESCAPED_STRING
43
+ footer_stmt: "FOOTER"
44
+
45
+ canvas_stmt: "CANVAS" DIMENSIONS
46
+ palette_stmt: "PALETTE" "[" [COLOR ("," COLOR)*] "]"
47
+ sizes_stmt: "SIZES" "[" [NUMBER ("," NUMBER)*] "]"
48
+ tools_stmt: "TOOLS" "[" [TOOL ("," TOOL)*] "]"
49
+
50
+ DIMENSIONS: /[0-9]+x[0-9]+/
51
+ COLOR: /#[0-9a-fA-F]{3,8}/
52
+ TOOL: /[a-zA-Z]+/
53
+ FIELD: /[a-zA-Z0-9_]+/
54
+ LINK_ITEM: /[^,\)\r\n]+/
55
+ PATH: /\/[a-zA-Z0-9_\-\/]*/
56
+ IDENT: /[a-zA-Z0-9_\-]+/
57
+
58
+ comment: /\/\/.*/
59
+
60
+ %import common.ESCAPED_STRING
61
+ %import common.INT
62
+ %import common.NUMBER
63
+ %import common.WS
64
+ %ignore WS
65
+ '''
66
+
67
+ _aeui_parser = None
68
+ def get_parser():
69
+ global _aeui_parser
70
+ if _aeui_parser is None:
71
+ _aeui_parser = Lark(AEUI_GRAMMAR, parser='earley')
72
+ return _aeui_parser
73
+
74
+ class AeUITransformer(Transformer):
75
+ def __init__(self):
76
+ super().__init__()
77
+ self.title = "AeUI Page"
78
+ self.has_canvas = False
79
+
80
+ def page_stmt(self, children):
81
+ val = str(children[0]).strip()
82
+ self.title = val
83
+ return ""
84
+
85
+ def state_stmt(self, children):
86
+ return ""
87
+
88
+ def comment(self, children):
89
+ return ""
90
+
91
+ def nav_stmt(self, children):
92
+ items = children
93
+ nav_html = '<nav class="bg-gray-900 text-white px-8 py-4 flex justify-between items-center shadow-md">\n'
94
+ for item in items:
95
+ if isinstance(item, dict) and item.get('type') == 'links':
96
+ nav_html += ' <div class="flex space-x-6">\n'
97
+ for link in item['links']:
98
+ nav_html += f' <a href="#" class="text-gray-300 hover:text-amber-400 transition">{link}</a>\n'
99
+ nav_html += ' </div>\n'
100
+ else:
101
+ text = str(item).strip()
102
+ if text:
103
+ nav_html += f' <div class="font-extrabold text-2xl text-amber-500">{text}</div>\n'
104
+ nav_html += '</nav>\n'
105
+ return nav_html
106
+
107
+ def links_call(self, children):
108
+ links = [str(c).strip() for c in children if str(c).strip()]
109
+ return {'type': 'links', 'links': links}
110
+
111
+ def nav_text(self, children):
112
+ return str(children[0]).strip()
113
+
114
+ def hero_stmt(self, children):
115
+ raw_text = children[0]
116
+ text = json.loads(raw_text) if raw_text.startswith('"') else str(raw_text)
117
+ align = "text-center"
118
+ if len(children) > 1 and children[1]:
119
+ align_val = str(children[1]).lower()
120
+ align = "text-center" if "center" in align_val else "text-left"
121
+ return f'''<header class="bg-gradient-to-b from-gray-900 via-gray-800 to-gray-900 text-white py-24 px-6 {align}">
122
+ <div class="max-w-4xl mx-auto">
123
+ <h1 class="text-5xl md:text-6xl font-black tracking-tight mb-6">{text}</h1>
124
+ </div>
125
+ </header>'''
126
+
127
+ def block(self, children):
128
+ # Flatten and filter out empty strings
129
+ items = [c for c in children if c and isinstance(c, str)]
130
+ return items
131
+
132
+ def card_call(self, children):
133
+ fields = [str(c).strip() for c in children]
134
+ return {'type': 'cards', 'fields': fields}
135
+
136
+ def grid_target(self, children):
137
+ return children[0]
138
+
139
+ def grid_stmt(self, children):
140
+ cols = int(children[0])
141
+ target = children[1] if len(children) > 1 else None
142
+
143
+ grid_html = f'<section class="max-w-6xl mx-auto px-6 py-16">\n <div class="grid grid-cols-1 md:grid-cols-{cols} gap-8">\n'
144
+
145
+ if isinstance(target, dict) and target.get('type') == 'cards':
146
+ # Generate placeholder cards
147
+ for i in range(cols):
148
+ grid_html += f''' <div class="bg-gray-900 rounded-2xl border border-gray-800 hover:border-amber-500/50 p-6 shadow-xl transition-all hover:-translate-y-2">
149
+ <div class="h-44 bg-gradient-to-tr from-amber-950/40 to-gray-800 rounded-xl mb-4"></div>
150
+ <h3 class="text-xl font-bold text-white mb-2">Card {i+1}</h3>
151
+ <p class="text-gray-400 mb-4">Descripcion</p>
152
+ <button class="px-4 py-2 bg-amber-600 hover:bg-amber-500 text-white font-semibold rounded-lg">Action</button>
153
+ </div>\n'''
154
+ elif isinstance(target, list):
155
+ # Nested statements inside block [ stmt1 stmt2 ] or { stmt1 stmt2 }
156
+ for item in target:
157
+ grid_html += f' <div class="flex flex-col justify-center items-center w-full">{item}</div>\n'
158
+ elif isinstance(target, str) and target:
159
+ # Single statement nested
160
+ grid_html += f' <div class="flex flex-col justify-center items-center w-full">{target}</div>\n'
161
+ else:
162
+ for i in range(cols):
163
+ grid_html += f''' <div class="bg-gray-900 rounded-2xl border border-gray-800 p-6 shadow-xl">
164
+ <h3 class="text-xl font-bold text-white mb-2">Item {i+1}</h3>
165
+ </div>\n'''
166
+
167
+ grid_html += ' </div>\n</section>\n'
168
+ return grid_html
169
+
170
+ def btn_stmt(self, children):
171
+ raw = children[0]
172
+ label = json.loads(raw) if raw.startswith('"') else str(raw)
173
+ return f'''<div class="text-center py-8">
174
+ <button class="px-10 py-4 bg-gradient-to-r from-amber-500 to-orange-600 text-gray-950 font-black text-xl rounded-2xl shadow-2xl hover:scale-105 transition">{label}</button>
175
+ </div>'''
176
+
177
+ def testimonials_stmt(self, children):
178
+ return '''<section class="max-w-6xl mx-auto px-6 py-12">
179
+ <h2 class="text-3xl font-bold text-center text-white mb-8">Lo que dicen nuestros usuarios</h2>
180
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
181
+ <div class="bg-gray-900 border border-gray-800 p-6 rounded-2xl shadow-md">
182
+ <p class="text-gray-300 italic mb-4">"AeCode redujo nuestros costos de tokens de IA en más de 80%. Esencial para cualquier agente."</p>
183
+ <span class="font-bold text-amber-400">— Sarah C., AI Lead</span>
184
+ </div>
185
+ <div class="bg-gray-900 border border-gray-800 p-6 rounded-2xl shadow-md">
186
+ <p class="text-gray-300 italic mb-4">"Construir scrapers y dashboards en 3 líneas de código parece magia. 10/10."</p>
187
+ <span class="font-bold text-amber-400">— David R., Senior Engineer</span>
188
+ </div>
189
+ </div>
190
+ </section>'''
191
+
192
+ def form_stmt(self, children):
193
+ raw = children[0]
194
+ form_title = json.loads(raw) if raw.startswith('"') else str(raw)
195
+ return f'''<section class="max-w-xl mx-auto px-6 py-10">
196
+ <div class="bg-gray-900 border border-gray-800 p-8 rounded-2xl shadow-xl text-center">
197
+ <h3 class="text-2xl font-bold text-white mb-4">{form_title}</h3>
198
+ <div class="flex flex-col sm:flex-row gap-3">
199
+ <input type="email" placeholder="tu@email.com" class="flex-1 bg-gray-800 border border-gray-700 rounded-xl px-4 py-3 text-white placeholder-gray-500 focus:outline-none focus:border-amber-500">
200
+ <button class="px-6 py-3 bg-amber-500 hover:bg-amber-400 text-gray-950 font-bold rounded-xl transition">Enviar</button>
201
+ </div>
202
+ </div>
203
+ </section>'''
204
+
205
+ def canvas_stmt(self, children):
206
+ self.has_canvas = True
207
+ dims = str(children[0]).split('x')
208
+ w = dims[0] if len(dims) > 0 else "900"
209
+ h = dims[1] if len(dims) > 1 else "550"
210
+ return f'''<div class="max-w-6xl mx-auto px-4 pt-6 pb-2 flex justify-center">
211
+ <canvas id="paintCanvas" width="{w}" height="{h}" class="bg-white rounded-2xl shadow-2xl border-4 border-gray-800 cursor-crosshair touch-none max-w-full block"></canvas>
212
+ </div>'''
213
+
214
+ def palette_stmt(self, children):
215
+ self.has_canvas = True
216
+ colors = [str(c).strip() for c in children]
217
+ swatches = []
218
+ for i, c in enumerate(colors):
219
+ active = "ring-4 ring-amber-400 scale-110" if i == 0 else ""
220
+ swatches.append(f'<button type="button" class="palette-swatch w-9 h-9 rounded-full border-2 border-gray-700 shadow-md transition-all duration-150 hover:scale-110 focus:outline-none {active}" data-color="{c}" style="background-color: {c};" title="{c}"></button>')
221
+ return f'''<div class="flex flex-wrap items-center justify-center gap-3 my-2 px-4">
222
+ <span class="text-xs uppercase font-extrabold tracking-wider text-gray-400">Color:</span>
223
+ <div class="flex flex-wrap items-center gap-2.5 bg-gray-900 border border-gray-800 p-2.5 rounded-2xl shadow-lg">
224
+ {''.join(swatches)}
225
+ </div>
226
+ </div>'''
227
+
228
+ def sizes_stmt(self, children):
229
+ self.has_canvas = True
230
+ sizes = [str(c).strip() for c in children]
231
+ size_btns = []
232
+ for i, s in enumerate(sizes):
233
+ active = "bg-amber-500 text-gray-950 font-bold ring-2 ring-amber-400" if i == 1 or (len(sizes) == 1 and i == 0) else "bg-gray-800 text-gray-300 hover:bg-gray-700 font-semibold"
234
+ size_btns.append(f'<button type="button" class="size-btn px-4 py-2 rounded-xl text-sm transition-all duration-150 border border-gray-700 focus:outline-none {active}" data-size="{s}">{s}px</button>')
235
+ return f'''<div class="flex flex-wrap items-center justify-center gap-3 my-2 px-4">
236
+ <span class="text-xs uppercase font-extrabold tracking-wider text-gray-400">Grosor:</span>
237
+ <div class="flex flex-wrap items-center gap-2 bg-gray-900 border border-gray-800 p-2.5 rounded-2xl shadow-lg">
238
+ {''.join(size_btns)}
239
+ </div>
240
+ </div>'''
241
+
242
+ def tools_stmt(self, children):
243
+ self.has_canvas = True
244
+ tools = [str(c).strip().lower() for c in children]
245
+ tool_icons = {
246
+ "brush": "✏️ Pincel",
247
+ "eraser": "🧹 Borrador",
248
+ "clear": "🗑️ Limpiar",
249
+ "download": "💾 Descargar"
250
+ }
251
+ tool_btns = []
252
+ for t in tools:
253
+ label = tool_icons.get(t, t.capitalize())
254
+ active = "bg-amber-500 text-gray-950 ring-2 ring-amber-400" if t == "brush" else "bg-gray-800 text-gray-200 hover:bg-gray-700"
255
+ tool_btns.append(f'<button type="button" class="tool-btn px-4 py-2 rounded-xl text-sm font-bold transition-all duration-150 border border-gray-700 focus:outline-none flex items-center gap-2 {active}" data-tool="{t}">{label}</button>')
256
+ return f'''<div class="flex flex-wrap items-center justify-center gap-3 my-2 px-4">
257
+ <span class="text-xs uppercase font-extrabold tracking-wider text-gray-400">Herramientas:</span>
258
+ <div class="flex flex-wrap items-center gap-2.5 bg-gray-900 border border-gray-800 p-2.5 rounded-2xl shadow-lg">
259
+ {''.join(tool_btns)}
260
+ </div>
261
+ </div>'''
262
+
263
+ def footer_stmt(self, children):
264
+ return '''<footer class="bg-gray-900 text-white py-8 px-6 text-center border-t border-gray-800">
265
+ <p class="text-gray-400">Generado con AeCode</p>
266
+ </footer>'''
267
+
268
+ def start(self, children):
269
+ # Collect all non-empty HTML parts
270
+ return [c for c in children if c and isinstance(c, str)]
271
+
272
+ def _get_canvas_script():
273
+ return """<script>
274
+ document.addEventListener('DOMContentLoaded', () => {
275
+ const canvas = document.getElementById('paintCanvas');
276
+ if (!canvas) return;
277
+
278
+ const ctx = canvas.getContext('2d');
279
+ const backgroundColor = '#ffffff';
280
+
281
+ function fillBackground() {
282
+ ctx.fillStyle = backgroundColor;
283
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
284
+ }
285
+ fillBackground();
286
+
287
+ let isDrawing = false;
288
+ let lastX = 0;
289
+ let lastY = 0;
290
+ let currentColor = '#000000';
291
+ let currentSize = 5;
292
+ let currentTool = 'brush';
293
+
294
+ function getCoords(e) {
295
+ const rect = canvas.getBoundingClientRect();
296
+ const scaleX = canvas.width / rect.width;
297
+ const scaleY = canvas.height / rect.height;
298
+ let clientX, clientY;
299
+ if (e.touches && e.touches.length > 0) {
300
+ clientX = e.touches[0].clientX;
301
+ clientY = e.touches[0].clientY;
302
+ } else {
303
+ clientX = e.clientX;
304
+ clientY = e.clientY;
305
+ }
306
+ return {
307
+ x: (clientX - rect.left) * scaleX,
308
+ y: (clientY - rect.top) * scaleY
309
+ };
310
+ }
311
+
312
+ function startDrawing(e) {
313
+ e.preventDefault();
314
+ isDrawing = true;
315
+ const pos = getCoords(e);
316
+ lastX = pos.x;
317
+ lastY = pos.y;
318
+ draw(e);
319
+ }
320
+
321
+ function draw(e) {
322
+ if (!isDrawing) return;
323
+ e.preventDefault();
324
+ const pos = getCoords(e);
325
+
326
+ ctx.beginPath();
327
+ ctx.lineCap = 'round';
328
+ ctx.lineJoin = 'round';
329
+ ctx.lineWidth = currentSize;
330
+ ctx.strokeStyle = (currentTool === 'eraser') ? backgroundColor : currentColor;
331
+
332
+ ctx.moveTo(lastX, lastY);
333
+ ctx.lineTo(pos.x, pos.y);
334
+ ctx.stroke();
335
+
336
+ lastX = pos.x;
337
+ lastY = pos.y;
338
+ }
339
+
340
+ function stopDrawing(e) {
341
+ if (!isDrawing) return;
342
+ isDrawing = false;
343
+ }
344
+
345
+ canvas.addEventListener('mousedown', startDrawing);
346
+ canvas.addEventListener('mousemove', draw);
347
+ canvas.addEventListener('mouseup', stopDrawing);
348
+ canvas.addEventListener('mouseleave', stopDrawing);
349
+
350
+ canvas.addEventListener('touchstart', startDrawing, { passive: false });
351
+ canvas.addEventListener('touchmove', draw, { passive: false });
352
+ canvas.addEventListener('touchend', stopDrawing, { passive: false });
353
+ canvas.addEventListener('touchcancel', stopDrawing, { passive: false });
354
+
355
+ // Swatches
356
+ const swatches = document.querySelectorAll('.palette-swatch');
357
+ swatches.forEach(swatch => {
358
+ swatch.addEventListener('click', () => {
359
+ currentColor = swatch.getAttribute('data-color');
360
+ if (currentTool === 'eraser') {
361
+ selectTool('brush');
362
+ }
363
+ swatches.forEach(s => s.classList.remove('ring-4', 'ring-amber-400', 'scale-110'));
364
+ swatch.classList.add('ring-4', 'ring-amber-400', 'scale-110');
365
+ });
366
+ });
367
+ if (swatches.length > 0) {
368
+ currentColor = swatches[0].getAttribute('data-color') || '#000000';
369
+ }
370
+
371
+ // Sizes
372
+ const sizeBtns = document.querySelectorAll('.size-btn');
373
+ sizeBtns.forEach(btn => {
374
+ btn.addEventListener('click', () => {
375
+ currentSize = parseFloat(btn.getAttribute('data-size'));
376
+ sizeBtns.forEach(b => {
377
+ b.classList.remove('bg-amber-500', 'text-gray-950', 'ring-2', 'ring-amber-400');
378
+ b.classList.add('bg-gray-800', 'text-gray-300');
379
+ });
380
+ btn.classList.remove('bg-gray-800', 'text-gray-300');
381
+ btn.classList.add('bg-amber-500', 'text-gray-950', 'ring-2', 'ring-amber-400');
382
+ });
383
+ });
384
+ const initialActiveSize = document.querySelector('.size-btn.bg-amber-500');
385
+ if (initialActiveSize) {
386
+ currentSize = parseFloat(initialActiveSize.getAttribute('data-size')) || 5;
387
+ } else if (sizeBtns.length > 0) {
388
+ currentSize = parseFloat(sizeBtns[0].getAttribute('data-size')) || 5;
389
+ }
390
+
391
+ // Tools
392
+ const toolBtns = document.querySelectorAll('.tool-btn');
393
+ function selectTool(toolName) {
394
+ currentTool = toolName;
395
+ toolBtns.forEach(b => {
396
+ const t = b.getAttribute('data-tool');
397
+ if (t === 'clear' || t === 'download') return;
398
+ if (t === toolName) {
399
+ b.classList.remove('bg-gray-800', 'text-gray-200');
400
+ b.classList.add('bg-amber-500', 'text-gray-950', 'ring-2', 'ring-amber-400');
401
+ } else {
402
+ b.classList.remove('bg-amber-500', 'text-gray-950', 'ring-2', 'ring-amber-400');
403
+ b.classList.add('bg-gray-800', 'text-gray-200');
404
+ }
405
+ });
406
+ }
407
+
408
+ toolBtns.forEach(btn => {
409
+ const tool = btn.getAttribute('data-tool');
410
+ btn.addEventListener('click', () => {
411
+ if (tool === 'brush') {
412
+ selectTool('brush');
413
+ } else if (tool === 'eraser') {
414
+ selectTool('eraser');
415
+ } else if (tool === 'clear') {
416
+ fillBackground();
417
+ } else if (tool === 'download') {
418
+ const link = document.createElement('a');
419
+ link.download = 'aepaint.png';
420
+ link.href = canvas.toDataURL('image/png');
421
+ link.click();
422
+ }
423
+ });
424
+ });
425
+ });
426
+ </script>"""
427
+
428
+ def build_ui(code):
429
+ try:
430
+ parser = get_parser()
431
+ tree = parser.parse(code)
432
+ transformer = AeUITransformer()
433
+ body_parts = transformer.transform(tree)
434
+ title = transformer.title
435
+ has_canvas = transformer.has_canvas
436
+ except Exception as e:
437
+ # Fallback to regex line-by-line if grammar encounters an untracked edge case
438
+ return _build_ui_fallback(code)
439
+
440
+ script_tag = _get_canvas_script() if has_canvas else ""
441
+ html = f"""<!DOCTYPE html>
442
+ <html lang="es">
443
+ <head>
444
+ <meta charset="UTF-8">
445
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
446
+ <title>{title}</title>
447
+ <script src="https://cdn.tailwindcss.com"></script>
448
+ <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;800;900&display=swap" rel="stylesheet">
449
+ <style>body {{ font-family: 'Plus Jakarta Sans', sans-serif; }}</style>
450
+ </head>
451
+ <body class="bg-gray-950 text-gray-100 min-h-screen pb-12">
452
+ {''.join(body_parts)}
453
+ {script_tag}
454
+ </body>
455
+ </html>"""
456
+ return html
457
+
458
+ def _build_ui_fallback(code):
459
+ title = "AeUI Page"
460
+ body_parts = []
461
+ has_canvas = False
462
+ lines = [l.strip() for l in code.strip().split('\n') if l.strip() and not l.strip().startswith('//')]
463
+
464
+ for line in lines:
465
+ if line.startswith('PAGE'):
466
+ parts = line.split()
467
+ if len(parts) > 1:
468
+ title = parts[1]
469
+ elif line.startswith('STATE'):
470
+ pass
471
+ elif line.startswith('NAV'):
472
+ content = extract_brackets(line)
473
+ items = split_items(content)
474
+ nav_html = '<nav class="bg-gray-900 text-white px-8 py-4 flex justify-between items-center shadow-md">\n'
475
+ for item in items:
476
+ if 'Links(' in item:
477
+ links = extract_parens(item)
478
+ link_items = split_items(links)
479
+ nav_html += ' <div class="flex space-x-6">\n'
480
+ for link in link_items:
481
+ nav_html += f' <a href="#" class="text-gray-300 hover:text-amber-400 transition">{link}</a>\n'
482
+ nav_html += ' </div>\n'
483
+ else:
484
+ nav_html += f' <div class="font-extrabold text-2xl text-amber-500">{item}</div>\n'
485
+ nav_html += '</nav>\n'
486
+ body_parts.append(nav_html)
487
+ elif line.startswith('HERO'):
488
+ m = re.search(r'HERO\s+"([^"]+)"(?:\s+(.*))?', line)
489
+ if m:
490
+ text = m.group(1)
491
+ styles = m.group(2) or ""
492
+ align = "text-center" if "center" in styles else "text-left"
493
+ body_parts.append(f'''<header class="bg-gradient-to-b from-gray-900 via-gray-800 to-gray-900 text-white py-24 px-6 {align}">
494
+ <div class="max-w-4xl mx-auto">
495
+ <h1 class="text-5xl md:text-6xl font-black tracking-tight mb-6">{text}</h1>
496
+ </div>
497
+ </header>''')
498
+ elif line.startswith('GRID'):
499
+ m = re.search(r'GRID\s+(\d+)\s*>\s*(.*)', line)
500
+ if m:
501
+ cols = m.group(1)
502
+ grid_html = f'<section class="max-w-6xl mx-auto px-6 py-16">\n <div class="grid grid-cols-1 md:grid-cols-{cols} gap-8">\n'
503
+ for i in range(int(cols)):
504
+ grid_html += f''' <div class="bg-gray-900 rounded-2xl border border-gray-800 hover:border-amber-500/50 p-6 shadow-xl transition-all hover:-translate-y-2">
505
+ <div class="h-44 bg-gradient-to-tr from-amber-950/40 to-gray-800 rounded-xl mb-4"></div>
506
+ <h3 class="text-xl font-bold text-white mb-2">Card {i+1}</h3>
507
+ <p class="text-gray-400 mb-4">Descripcion</p>
508
+ <button class="px-4 py-2 bg-amber-600 hover:bg-amber-500 text-white font-semibold rounded-lg">Action</button>
509
+ </div>\n'''
510
+ grid_html += ' </div>\n</section>\n'
511
+ body_parts.append(grid_html)
512
+ elif line.startswith('BTN'):
513
+ m = re.search(r'BTN\s+"([^"]+)"', line)
514
+ if m:
515
+ label = m.group(1)
516
+ body_parts.append(f'''<div class="text-center py-8">
517
+ <button class="px-10 py-4 bg-gradient-to-r from-amber-500 to-orange-600 text-gray-950 font-black text-xl rounded-2xl shadow-2xl hover:scale-105 transition">{label}</button>
518
+ </div>''')
519
+ elif line.startswith('TESTIMONIALS'):
520
+ body_parts.append('''<section class="max-w-6xl mx-auto px-6 py-12">
521
+ <h2 class="text-3xl font-bold text-center text-white mb-8">Lo que dicen nuestros usuarios</h2>
522
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
523
+ <div class="bg-gray-900 border border-gray-800 p-6 rounded-2xl shadow-md">
524
+ <p class="text-gray-300 italic mb-4">"AeCode redujo nuestros costos de tokens de IA en más de 80%. Esencial para cualquier agente."</p>
525
+ <span class="font-bold text-amber-400">— Sarah C., AI Lead</span>
526
+ </div>
527
+ <div class="bg-gray-900 border border-gray-800 p-6 rounded-2xl shadow-md">
528
+ <p class="text-gray-300 italic mb-4">"Construir scrapers y dashboards en 3 líneas de código parece magia. 10/10."</p>
529
+ <span class="font-bold text-amber-400">— David R., Senior Engineer</span>
530
+ </div>
531
+ </div>
532
+ </section>''')
533
+ elif line.startswith('FORM'):
534
+ m = re.search(r'FORM\s+"([^"]+)"', line)
535
+ form_title = m.group(1) if m else "Suscríbete"
536
+ body_parts.append(f'''<section class="max-w-xl mx-auto px-6 py-10">
537
+ <div class="bg-gray-900 border border-gray-800 p-8 rounded-2xl shadow-xl text-center">
538
+ <h3 class="text-2xl font-bold text-white mb-4">{form_title}</h3>
539
+ <div class="flex flex-col sm:flex-row gap-3">
540
+ <input type="email" placeholder="tu@email.com" class="flex-1 bg-gray-800 border border-gray-700 rounded-xl px-4 py-3 text-white placeholder-gray-500 focus:outline-none focus:border-amber-500">
541
+ <button class="px-6 py-3 bg-amber-500 hover:bg-amber-400 text-gray-950 font-bold rounded-xl transition">Enviar</button>
542
+ </div>
543
+ </div>
544
+ </section>''')
545
+ elif line.startswith('CANVAS'):
546
+ has_canvas = True
547
+ m = re.search(r'CANVAS\s+(\d+)x(\d+)', line)
548
+ w = m.group(1) if m else "900"
549
+ h = m.group(2) if m else "550"
550
+ body_parts.append(f'''<div class="max-w-6xl mx-auto px-4 pt-6 pb-2 flex justify-center">
551
+ <canvas id="paintCanvas" width="{w}" height="{h}" class="bg-white rounded-2xl shadow-2xl border-4 border-gray-800 cursor-crosshair touch-none max-w-full block"></canvas>
552
+ </div>''')
553
+ elif line.startswith('PALETTE'):
554
+ has_canvas = True
555
+ content = extract_brackets(line)
556
+ colors = [c.strip() for c in split_items(content)]
557
+ swatches = []
558
+ for i, c in enumerate(colors):
559
+ active = "ring-4 ring-amber-400 scale-110" if i == 0 else ""
560
+ swatches.append(f'<button type="button" class="palette-swatch w-9 h-9 rounded-full border-2 border-gray-700 shadow-md transition-all duration-150 hover:scale-110 focus:outline-none {active}" data-color="{c}" style="background-color: {c};" title="{c}"></button>')
561
+ body_parts.append(f'''<div class="flex flex-wrap items-center justify-center gap-3 my-2 px-4">
562
+ <span class="text-xs uppercase font-extrabold tracking-wider text-gray-400">Color:</span>
563
+ <div class="flex flex-wrap items-center gap-2.5 bg-gray-900 border border-gray-800 p-2.5 rounded-2xl shadow-lg">
564
+ {''.join(swatches)}
565
+ </div>
566
+ </div>''')
567
+ elif line.startswith('SIZES'):
568
+ has_canvas = True
569
+ content = extract_brackets(line)
570
+ sizes = [s.strip() for s in split_items(content)]
571
+ size_btns = []
572
+ for i, s in enumerate(sizes):
573
+ active = "bg-amber-500 text-gray-950 font-bold ring-2 ring-amber-400" if i == 1 or (len(sizes)==1 and i==0) else "bg-gray-800 text-gray-300 hover:bg-gray-700 font-semibold"
574
+ size_btns.append(f'<button type="button" class="size-btn px-4 py-2 rounded-xl text-sm transition-all duration-150 border border-gray-700 focus:outline-none {active}" data-size="{s}">{s}px</button>')
575
+ body_parts.append(f'''<div class="flex flex-wrap items-center justify-center gap-3 my-2 px-4">
576
+ <span class="text-xs uppercase font-extrabold tracking-wider text-gray-400">Grosor:</span>
577
+ <div class="flex flex-wrap items-center gap-2 bg-gray-900 border border-gray-800 p-2.5 rounded-2xl shadow-lg">
578
+ {''.join(size_btns)}
579
+ </div>
580
+ </div>''')
581
+ elif line.startswith('TOOLS'):
582
+ has_canvas = True
583
+ content = extract_brackets(line)
584
+ tools = [t.strip().lower() for t in split_items(content)]
585
+ tool_icons = {
586
+ "brush": "✏️ Pincel",
587
+ "eraser": "🧹 Borrador",
588
+ "clear": "🗑️ Limpiar",
589
+ "download": "💾 Descargar"
590
+ }
591
+ tool_btns = []
592
+ for t in tools:
593
+ label = tool_icons.get(t, t.capitalize())
594
+ active = "bg-amber-500 text-gray-950 ring-2 ring-amber-400" if t == "brush" else "bg-gray-800 text-gray-200 hover:bg-gray-700"
595
+ tool_btns.append(f'<button type="button" class="tool-btn px-4 py-2 rounded-xl text-sm font-bold transition-all duration-150 border border-gray-700 focus:outline-none flex items-center gap-2 {active}" data-tool="{t}">{label}</button>')
596
+ body_parts.append(f'''<div class="flex flex-wrap items-center justify-center gap-3 my-2 px-4">
597
+ <span class="text-xs uppercase font-extrabold tracking-wider text-gray-400">Herramientas:</span>
598
+ <div class="flex flex-wrap items-center gap-2.5 bg-gray-900 border border-gray-800 p-2.5 rounded-2xl shadow-lg">
599
+ {''.join(tool_btns)}
600
+ </div>
601
+ </div>''')
602
+ elif line.startswith('FOOTER'):
603
+ body_parts.append('''<footer class="bg-gray-900 text-white py-8 px-6 text-center border-t border-gray-800">
604
+ <p class="text-gray-400">Generado con AeCode</p>
605
+ </footer>''')
606
+
607
+ script_tag = _get_canvas_script() if has_canvas else ""
608
+ html = f"""<!DOCTYPE html>
609
+ <html lang="es">
610
+ <head>
611
+ <meta charset="UTF-8">
612
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
613
+ <title>{title}</title>
614
+ <script src="https://cdn.tailwindcss.com"></script>
615
+ <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;800;900&display=swap" rel="stylesheet">
616
+ <style>body {{ font-family: 'Plus Jakarta Sans', sans-serif; }}</style>
617
+ </head>
618
+ <body class="bg-gray-950 text-gray-100 min-h-screen pb-12">
619
+ {''.join(body_parts)}
620
+ {script_tag}
621
+ </body>
622
+ </html>"""
623
+ return html
624
+
625
+ def extract_brackets(text):
626
+ m = re.search(r'\[(.*?)\]', text)
627
+ return m.group(1) if m else ""
628
+
629
+ def extract_parens(text):
630
+ m = re.search(r'\((.*?)\)', text)
631
+ return m.group(1) if m else ""
632
+
633
+ def split_items(text):
634
+ items = []
635
+ current = []
636
+ depth = 0
637
+ for char in text:
638
+ if char == '(':
639
+ depth += 1
640
+ current.append(char)
641
+ elif char == ')':
642
+ depth -= 1
643
+ current.append(char)
644
+ elif char == ',' and depth == 0:
645
+ items.append(''.join(current).strip())
646
+ current = []
647
+ else:
648
+ current.append(char)
649
+ if current:
650
+ items.append(''.join(current).strip())
651
+ return [i for i in items if i]
652
+
653
+ def build_and_open(code, output="output.html"):
654
+ html = build_ui(code)
655
+ Path(output).write_text(html, encoding='utf-8')
656
+ print(f"Generado: {output}")
657
+ try:
658
+ webbrowser.open(Path(output).as_uri())
659
+ except:
660
+ pass
661
+ return html