python2mobile 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. examples/example_ecommerce_app.py +189 -0
  2. examples/example_todo_app.py +159 -0
  3. p2m/__init__.py +31 -0
  4. p2m/cli.py +470 -0
  5. p2m/config.py +205 -0
  6. p2m/core/__init__.py +18 -0
  7. p2m/core/api.py +191 -0
  8. p2m/core/ast_walker.py +171 -0
  9. p2m/core/database.py +192 -0
  10. p2m/core/events.py +56 -0
  11. p2m/core/render_engine.py +597 -0
  12. p2m/core/runtime.py +128 -0
  13. p2m/core/state.py +51 -0
  14. p2m/core/validator.py +284 -0
  15. p2m/devserver/__init__.py +9 -0
  16. p2m/devserver/server.py +84 -0
  17. p2m/i18n/__init__.py +7 -0
  18. p2m/i18n/translator.py +74 -0
  19. p2m/imagine/__init__.py +35 -0
  20. p2m/imagine/agent.py +463 -0
  21. p2m/imagine/legacy.py +217 -0
  22. p2m/llm/__init__.py +20 -0
  23. p2m/llm/anthropic_provider.py +78 -0
  24. p2m/llm/base.py +42 -0
  25. p2m/llm/compatible_provider.py +120 -0
  26. p2m/llm/factory.py +72 -0
  27. p2m/llm/ollama_provider.py +89 -0
  28. p2m/llm/openai_provider.py +79 -0
  29. p2m/testing/__init__.py +41 -0
  30. p2m/ui/__init__.py +43 -0
  31. p2m/ui/components.py +301 -0
  32. python2mobile-1.0.1.dist-info/METADATA +238 -0
  33. python2mobile-1.0.1.dist-info/RECORD +50 -0
  34. python2mobile-1.0.1.dist-info/WHEEL +5 -0
  35. python2mobile-1.0.1.dist-info/entry_points.txt +2 -0
  36. python2mobile-1.0.1.dist-info/top_level.txt +3 -0
  37. tests/test_basic_engine.py +281 -0
  38. tests/test_build_generation.py +603 -0
  39. tests/test_build_test_gate.py +150 -0
  40. tests/test_carousel_modal.py +84 -0
  41. tests/test_config_system.py +272 -0
  42. tests/test_i18n.py +101 -0
  43. tests/test_ifood_app_integration.py +172 -0
  44. tests/test_imagine_cli.py +133 -0
  45. tests/test_imagine_command.py +341 -0
  46. tests/test_llm_providers.py +321 -0
  47. tests/test_new_apps_integration.py +588 -0
  48. tests/test_ollama_functional.py +329 -0
  49. tests/test_real_world_apps.py +228 -0
  50. tests/test_run_integration.py +776 -0
@@ -0,0 +1,597 @@
1
+ """
2
+ P2M Render Engine - Converts component tree to HTML with inline CSS
3
+ and full WebSocket-based interactivity.
4
+ """
5
+
6
+ from typing import Any, Dict, List
7
+ from jinja2 import Template
8
+
9
+
10
+ # WebSocket client injected into every page
11
+ _WS_SCRIPT = """
12
+ (function () {
13
+ var ws = null;
14
+ var reconnectTimer = null;
15
+
16
+ function connect() {
17
+ var proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
18
+ ws = new WebSocket(proto + '//' + window.location.host + '/ws');
19
+
20
+ ws.onopen = function () {
21
+ clearTimeout(reconnectTimer);
22
+ };
23
+
24
+ ws.onmessage = function (e) {
25
+ try {
26
+ var msg = JSON.parse(e.data);
27
+ if (msg.type === 'render') {
28
+ var el = document.getElementById('p2m-content');
29
+ if (el) el.innerHTML = msg.html;
30
+ } else if (msg.type === 'error') {
31
+ console.error('[P2M]', msg.message);
32
+ }
33
+ } catch (err) {
34
+ console.error('[P2M] parse error', err);
35
+ }
36
+ };
37
+
38
+ ws.onclose = function () {
39
+ reconnectTimer = setTimeout(connect, 1000);
40
+ };
41
+
42
+ ws.onerror = function () { ws.close(); };
43
+ }
44
+
45
+ connect();
46
+
47
+ /* handleClick(action, ...args) */
48
+ window.handleClick = function (action) {
49
+ var args = Array.prototype.slice.call(arguments, 1);
50
+ if (ws && ws.readyState === 1) {
51
+ ws.send(JSON.stringify({ type: 'click', action: action, args: args }));
52
+ }
53
+ };
54
+
55
+ /* handleChange(action, value) — for inputs */
56
+ window.handleChange = function (action, value) {
57
+ if (ws && ws.readyState === 1) {
58
+ ws.send(JSON.stringify({ type: 'change', action: action, value: value }));
59
+ }
60
+ };
61
+ })();
62
+ """
63
+
64
+ _PAGE_TEMPLATE = """<!DOCTYPE html>
65
+ <html lang="en">
66
+ <head>
67
+ <meta charset="UTF-8">
68
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
69
+ <title>P2M App</title>
70
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='75' font-size='75'>🐍</text></svg>">
71
+ <style>
72
+ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
73
+ html, body {
74
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
75
+ background: #e5e7eb;
76
+ min-height: 100vh;
77
+ display: flex;
78
+ justify-content: center;
79
+ align-items: flex-start;
80
+ padding: 20px 0;
81
+ }
82
+ .mobile-frame {
83
+ width: 390px;
84
+ height: 844px;
85
+ border: 10px solid #1a1a1a;
86
+ border-radius: 44px;
87
+ overflow: hidden;
88
+ box-shadow: 0 30px 80px rgba(0,0,0,.35), 0 0 0 1px #333;
89
+ position: relative;
90
+ background: #fff;
91
+ flex-shrink: 0;
92
+ /* transform creates a containing block for position:fixed children,
93
+ keeping modals, overlays and sticky bars inside the phone frame */
94
+ transform: translate(0, 0);
95
+ }
96
+ .mobile-notch {
97
+ position: absolute;
98
+ top: 0; left: 50%;
99
+ transform: translateX(-50%);
100
+ width: 120px; height: 28px;
101
+ background: #1a1a1a;
102
+ border-radius: 0 0 20px 20px;
103
+ z-index: 20;
104
+ }
105
+ .mobile-content {
106
+ width: 100%; height: 100%;
107
+ overflow-y: auto;
108
+ background: #fff;
109
+ padding-top: 28px;
110
+ }
111
+ button {
112
+ cursor: pointer; border: none;
113
+ font-family: inherit;
114
+ transition: opacity .15s, transform .1s;
115
+ }
116
+ button:active { transform: scale(.97); opacity: .85; }
117
+ input, textarea, select {
118
+ font-family: inherit;
119
+ border: 1px solid #d1d5db;
120
+ border-radius: .5rem;
121
+ padding: .65rem .75rem;
122
+ font-size: 1rem;
123
+ transition: border-color .2s;
124
+ width: 100%;
125
+ }
126
+ input:focus, textarea:focus {
127
+ outline: none;
128
+ border-color: #3b82f6;
129
+ box-shadow: 0 0 0 3px rgba(59,130,246,.15);
130
+ }
131
+ a { color: #3b82f6; text-decoration: none; cursor: pointer; }
132
+ a:hover { text-decoration: underline; }
133
+ ul { list-style: none; }
134
+ </style>
135
+ </head>
136
+ <body>
137
+ <div class="mobile-frame">
138
+ <div class="mobile-notch"></div>
139
+ <div class="mobile-content" id="p2m-content">
140
+ {{ content }}
141
+ </div>
142
+ </div>
143
+ <script>{{ ws_script }}</script>
144
+ </body>
145
+ </html>"""
146
+
147
+
148
+ class RenderEngine:
149
+ """Renders a P2M component tree → HTML with Tailwind inline styles."""
150
+
151
+ # ------------------------------------------------------------------ #
152
+ # Tailwind → CSS mapping
153
+ # ------------------------------------------------------------------ #
154
+ TAILWIND_CLASSES = {
155
+ # Background colors
156
+ "bg-white": "background-color:#ffffff;",
157
+ "bg-black": "background-color:#000000;",
158
+ "bg-gray-50": "background-color:#f9fafb;",
159
+ "bg-gray-100": "background-color:#f3f4f6;",
160
+ "bg-gray-200": "background-color:#e5e7eb;",
161
+ "bg-gray-300": "background-color:#d1d5db;",
162
+ "bg-gray-400": "background-color:#9ca3af;",
163
+ "bg-gray-500": "background-color:#6b7280;",
164
+ "bg-gray-600": "background-color:#4b5563;",
165
+ "bg-gray-700": "background-color:#374151;",
166
+ "bg-gray-800": "background-color:#1f2937;",
167
+ "bg-gray-900": "background-color:#111827;",
168
+ "bg-blue-50": "background-color:#eff6ff;",
169
+ "bg-blue-100": "background-color:#dbeafe;",
170
+ "bg-blue-400": "background-color:#60a5fa;",
171
+ "bg-blue-500": "background-color:#3b82f6;",
172
+ "bg-blue-600": "background-color:#2563eb;",
173
+ "bg-blue-700": "background-color:#1d4ed8;",
174
+ "bg-green-50": "background-color:#f0fdf4;",
175
+ "bg-green-100": "background-color:#dcfce7;",
176
+ "bg-green-500": "background-color:#22c55e;",
177
+ "bg-green-600": "background-color:#16a34a;",
178
+ "bg-red-50": "background-color:#fef2f2;",
179
+ "bg-red-100": "background-color:#fee2e2;",
180
+ "bg-red-500": "background-color:#ef4444;",
181
+ "bg-red-600": "background-color:#dc2626;",
182
+ "bg-yellow-50": "background-color:#fefce8;",
183
+ "bg-yellow-100": "background-color:#fef9c3;",
184
+ "bg-yellow-400": "background-color:#facc15;",
185
+ "bg-yellow-500": "background-color:#eab308;",
186
+ "bg-orange-100": "background-color:#ffedd5;",
187
+ "bg-orange-500": "background-color:#f97316;",
188
+ "bg-purple-100": "background-color:#f3e8ff;",
189
+ "bg-purple-500": "background-color:#a855f7;",
190
+ "bg-purple-600": "background-color:#9333ea;",
191
+ "bg-pink-100": "background-color:#fce7f3;",
192
+ "bg-pink-500": "background-color:#ec4899;",
193
+ "bg-indigo-600": "background-color:#4f46e5;",
194
+ "bg-teal-500": "background-color:#14b8a6;",
195
+ "bg-emerald-500": "background-color:#10b981;",
196
+ "bg-emerald-600": "background-color:#059669;",
197
+ "bg-transparent": "background-color:transparent;",
198
+ # Text colors
199
+ "text-white": "color:#ffffff;",
200
+ "text-black": "color:#000000;",
201
+ "text-gray-100": "color:#f3f4f6;",
202
+ "text-gray-200": "color:#e5e7eb;",
203
+ "text-gray-300": "color:#d1d5db;",
204
+ "text-gray-400": "color:#9ca3af;",
205
+ "text-gray-500": "color:#6b7280;",
206
+ "text-gray-600": "color:#4b5563;",
207
+ "text-gray-700": "color:#374151;",
208
+ "text-gray-800": "color:#1f2937;",
209
+ "text-gray-900": "color:#111827;",
210
+ "text-blue-400": "color:#60a5fa;",
211
+ "text-blue-500": "color:#3b82f6;",
212
+ "text-blue-600": "color:#2563eb;",
213
+ "text-blue-700": "color:#1d4ed8;",
214
+ "text-green-500": "color:#22c55e;",
215
+ "text-green-600": "color:#16a34a;",
216
+ "text-red-400": "color:#f87171;",
217
+ "text-red-500": "color:#ef4444;",
218
+ "text-red-600": "color:#dc2626;",
219
+ "text-yellow-400": "color:#facc15;",
220
+ "text-yellow-500": "color:#eab308;",
221
+ "text-orange-500": "color:#f97316;",
222
+ "text-purple-600": "color:#9333ea;",
223
+ "text-indigo-600": "color:#4f46e5;",
224
+ "text-emerald-600": "color:#059669;",
225
+ # Font size
226
+ "text-xs": "font-size:.75rem; line-height:1rem;",
227
+ "text-sm": "font-size:.875rem; line-height:1.25rem;",
228
+ "text-base": "font-size:1rem; line-height:1.5rem;",
229
+ "text-lg": "font-size:1.125rem; line-height:1.75rem;",
230
+ "text-xl": "font-size:1.25rem; line-height:1.75rem;",
231
+ "text-2xl": "font-size:1.5rem; line-height:2rem;",
232
+ "text-3xl": "font-size:1.875rem; line-height:2.25rem;",
233
+ "text-4xl": "font-size:2.25rem; line-height:2.5rem;",
234
+ "text-5xl": "font-size:3rem; line-height:1;",
235
+ "text-6xl": "font-size:3.75rem; line-height:1;",
236
+ # Font weight
237
+ "font-light": "font-weight:300;",
238
+ "font-normal": "font-weight:400;",
239
+ "font-medium": "font-weight:500;",
240
+ "font-semibold": "font-weight:600;",
241
+ "font-bold": "font-weight:700;",
242
+ "font-extrabold": "font-weight:800;",
243
+ # Text alignment
244
+ "text-left": "text-align:left;",
245
+ "text-center": "text-align:center;",
246
+ "text-right": "text-align:right;",
247
+ # Text decoration/transform
248
+ "uppercase": "text-transform:uppercase;",
249
+ "lowercase": "text-transform:lowercase;",
250
+ "capitalize": "text-transform:capitalize;",
251
+ "underline": "text-decoration:underline;",
252
+ "line-through": "text-decoration:line-through;",
253
+ "italic": "font-style:italic;",
254
+ # Tracking/leading
255
+ "tracking-wide": "letter-spacing:.025em;",
256
+ "tracking-wider": "letter-spacing:.05em;",
257
+ "tracking-widest": "letter-spacing:.1em;",
258
+ "leading-none": "line-height:1;",
259
+ "leading-tight": "line-height:1.25;",
260
+ "leading-snug": "line-height:1.375;",
261
+ "leading-normal": "line-height:1.5;",
262
+ "leading-relaxed": "line-height:1.625;",
263
+ "leading-loose": "line-height:2;",
264
+ # Opacity
265
+ "opacity-0": "opacity:0;",
266
+ "opacity-25": "opacity:.25;",
267
+ "opacity-50": "opacity:.5;",
268
+ "opacity-75": "opacity:.75;",
269
+ "opacity-100": "opacity:1;",
270
+ # Display
271
+ "block": "display:block;",
272
+ "inline": "display:inline;",
273
+ "inline-block": "display:inline-block;",
274
+ "flex": "display:flex;",
275
+ "inline-flex": "display:inline-flex;",
276
+ "grid": "display:grid;",
277
+ "hidden": "display:none;",
278
+ # Flex
279
+ "flex-row": "flex-direction:row;",
280
+ "flex-col": "flex-direction:column;",
281
+ "flex-wrap": "flex-wrap:wrap;",
282
+ "flex-nowrap": "flex-wrap:nowrap;",
283
+ "flex-1": "flex:1 1 0%;",
284
+ "flex-auto": "flex:1 1 auto;",
285
+ "flex-none": "flex:none;",
286
+ "flex-grow": "flex-grow:1;",
287
+ "flex-shrink-0": "flex-shrink:0;",
288
+ "items-start": "align-items:flex-start;",
289
+ "items-center": "align-items:center;",
290
+ "items-end": "align-items:flex-end;",
291
+ "items-stretch": "align-items:stretch;",
292
+ "items-baseline": "align-items:baseline;",
293
+ "justify-start": "justify-content:flex-start;",
294
+ "justify-center": "justify-content:center;",
295
+ "justify-end": "justify-content:flex-end;",
296
+ "justify-between": "justify-content:space-between;",
297
+ "justify-around": "justify-content:space-around;",
298
+ "justify-evenly": "justify-content:space-evenly;",
299
+ "self-start": "align-self:flex-start;",
300
+ "self-center": "align-self:center;",
301
+ "self-end": "align-self:flex-end;",
302
+ "self-stretch": "align-self:stretch;",
303
+ # Gap
304
+ "gap-0": "gap:0;",
305
+ "gap-1": "gap:.25rem;",
306
+ "gap-2": "gap:.5rem;",
307
+ "gap-3": "gap:.75rem;",
308
+ "gap-4": "gap:1rem;",
309
+ "gap-5": "gap:1.25rem;",
310
+ "gap-6": "gap:1.5rem;",
311
+ "gap-8": "gap:2rem;",
312
+ "gap-10": "gap:2.5rem;",
313
+ "gap-x-2": "column-gap:.5rem;",
314
+ "gap-x-3": "column-gap:.75rem;",
315
+ "gap-x-4": "column-gap:1rem;",
316
+ "gap-y-2": "row-gap:.5rem;",
317
+ "gap-y-3": "row-gap:.75rem;",
318
+ "gap-y-4": "row-gap:1rem;",
319
+ # space-y simulated as gap (works inside flex-col)
320
+ "space-y-1": "gap:.25rem;",
321
+ "space-y-2": "gap:.5rem;",
322
+ "space-y-3": "gap:.75rem;",
323
+ "space-y-4": "gap:1rem;",
324
+ "space-y-6": "gap:1.5rem;",
325
+ "space-y-8": "gap:2rem;",
326
+ "space-x-2": "gap:.5rem;",
327
+ "space-x-3": "gap:.75rem;",
328
+ "space-x-4": "gap:1rem;",
329
+ # Padding
330
+ "p-0": "padding:0;",
331
+ "p-1": "padding:.25rem;",
332
+ "p-2": "padding:.5rem;",
333
+ "p-3": "padding:.75rem;",
334
+ "p-4": "padding:1rem;",
335
+ "p-5": "padding:1.25rem;",
336
+ "p-6": "padding:1.5rem;",
337
+ "p-8": "padding:2rem;",
338
+ "p-10": "padding:2.5rem;",
339
+ "p-12": "padding:3rem;",
340
+ "px-1": "padding-left:.25rem; padding-right:.25rem;",
341
+ "px-2": "padding-left:.5rem; padding-right:.5rem;",
342
+ "px-3": "padding-left:.75rem; padding-right:.75rem;",
343
+ "px-4": "padding-left:1rem; padding-right:1rem;",
344
+ "px-5": "padding-left:1.25rem; padding-right:1.25rem;",
345
+ "px-6": "padding-left:1.5rem; padding-right:1.5rem;",
346
+ "px-8": "padding-left:2rem; padding-right:2rem;",
347
+ "py-1": "padding-top:.25rem; padding-bottom:.25rem;",
348
+ "py-2": "padding-top:.5rem; padding-bottom:.5rem;",
349
+ "py-3": "padding-top:.75rem; padding-bottom:.75rem;",
350
+ "py-4": "padding-top:1rem; padding-bottom:1rem;",
351
+ "py-5": "padding-top:1.25rem; padding-bottom:1.25rem;",
352
+ "py-6": "padding-top:1.5rem; padding-bottom:1.5rem;",
353
+ "py-8": "padding-top:2rem; padding-bottom:2rem;",
354
+ "pt-1": "padding-top:.25rem;", "pt-2": "padding-top:.5rem;",
355
+ "pt-4": "padding-top:1rem;", "pt-6": "padding-top:1.5rem;",
356
+ "pt-8": "padding-top:2rem;", "pt-12": "padding-top:3rem;",
357
+ "pb-1": "padding-bottom:.25rem;", "pb-2": "padding-bottom:.5rem;",
358
+ "pb-4": "padding-bottom:1rem;", "pb-6": "padding-bottom:1.5rem;",
359
+ "pb-8": "padding-bottom:2rem;",
360
+ "pl-2": "padding-left:.5rem;", "pl-3": "padding-left:.75rem;",
361
+ "pl-4": "padding-left:1rem;", "pl-9": "padding-left:2.25rem;",
362
+ "pr-2": "padding-right:.5rem;", "pr-3": "padding-right:.75rem;",
363
+ "pr-4": "padding-right:1rem;",
364
+ # Margin
365
+ "m-0": "margin:0;", "m-1": "margin:.25rem;",
366
+ "m-2": "margin:.5rem;", "m-4": "margin:1rem;",
367
+ "mx-auto": "margin-left:auto; margin-right:auto;",
368
+ "mx-2": "margin-left:.5rem; margin-right:.5rem;",
369
+ "my-2": "margin-top:.5rem; margin-bottom:.5rem;",
370
+ "my-4": "margin-top:1rem; margin-bottom:1rem;",
371
+ "my-6": "margin-top:1.5rem; margin-bottom:1.5rem;",
372
+ "mt-1": "margin-top:.25rem;", "mt-2": "margin-top:.5rem;",
373
+ "mt-3": "margin-top:.75rem;", "mt-4": "margin-top:1rem;",
374
+ "mt-6": "margin-top:1.5rem;", "mt-8": "margin-top:2rem;",
375
+ "mb-1": "margin-bottom:.25rem;", "mb-2": "margin-bottom:.5rem;",
376
+ "mb-3": "margin-bottom:.75rem;", "mb-4": "margin-bottom:1rem;",
377
+ "mb-6": "margin-bottom:1.5rem;", "mb-8": "margin-bottom:2rem;",
378
+ "mb-10": "margin-bottom:2.5rem;", "mb-12": "margin-bottom:3rem;",
379
+ "ml-1": "margin-left:.25rem;", "ml-2": "margin-left:.5rem;",
380
+ "ml-3": "margin-left:.75rem;", "ml-4": "margin-left:1rem;",
381
+ "ml-auto": "margin-left:auto;",
382
+ "mr-1": "margin-right:.25rem;", "mr-2": "margin-right:.5rem;",
383
+ "mr-3": "margin-right:.75rem;", "mr-4": "margin-right:1rem;",
384
+ # Width / Height
385
+ "w-full": "width:100%;",
386
+ "w-1/2": "width:50%;", "w-1/3": "width:33.333%;",
387
+ "w-1/4": "width:25%;", "w-2/3": "width:66.666%;",
388
+ "w-3/4": "width:75%;",
389
+ "w-4": "width:1rem;", "w-6": "width:1.5rem;",
390
+ "w-8": "width:2rem;", "w-10": "width:2.5rem;",
391
+ "w-12": "width:3rem;", "w-16": "width:4rem;",
392
+ "h-full": "height:100%;",
393
+ "h-4": "height:1rem;", "h-6": "height:1.5rem;",
394
+ "h-8": "height:2rem;", "h-10": "height:2.5rem;",
395
+ "h-12": "height:3rem;", "h-16": "height:4rem;",
396
+ "h-screen": "height:100vh;",
397
+ "min-h-screen": "min-height:100vh;",
398
+ "min-h-full": "min-height:100%;",
399
+ "max-w-xs": "max-width:20rem;",
400
+ "max-w-sm": "max-width:24rem;",
401
+ "max-w-md": "max-width:28rem;",
402
+ "max-w-lg": "max-width:32rem;",
403
+ "max-w-xl": "max-width:36rem;",
404
+ "max-w-full": "max-width:100%;",
405
+ # Border radius
406
+ "rounded-none": "border-radius:0;",
407
+ "rounded-sm": "border-radius:.125rem;",
408
+ "rounded": "border-radius:.25rem;",
409
+ "rounded-md": "border-radius:.375rem;",
410
+ "rounded-lg": "border-radius:.5rem;",
411
+ "rounded-xl": "border-radius:.75rem;",
412
+ "rounded-2xl": "border-radius:1rem;",
413
+ "rounded-3xl": "border-radius:1.5rem;",
414
+ "rounded-full": "border-radius:9999px;",
415
+ "rounded-t-xl": "border-top-left-radius:.75rem; border-top-right-radius:.75rem;",
416
+ "rounded-t-2xl": "border-top-left-radius:1rem; border-top-right-radius:1rem;",
417
+ "rounded-b-xl": "border-bottom-left-radius:.75rem; border-bottom-right-radius:.75rem;",
418
+ # Border
419
+ "border": "border:1px solid #e5e7eb;",
420
+ "border-0": "border:0;",
421
+ "border-2": "border:2px solid #e5e7eb;",
422
+ "border-t": "border-top:1px solid #e5e7eb;",
423
+ "border-b": "border-bottom:1px solid #e5e7eb;",
424
+ "border-gray-100": "border-color:#f3f4f6;",
425
+ "border-gray-200": "border-color:#e5e7eb;",
426
+ "border-gray-300": "border-color:#d1d5db;",
427
+ "border-blue-200": "border-color:#bfdbfe;",
428
+ "border-blue-500": "border-color:#3b82f6;",
429
+ # Shadows
430
+ "shadow-sm": "box-shadow:0 1px 2px rgba(0,0,0,.05);",
431
+ "shadow": "box-shadow:0 1px 3px rgba(0,0,0,.1),0 1px 2px rgba(0,0,0,.06);",
432
+ "shadow-md": "box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -1px rgba(0,0,0,.06);",
433
+ "shadow-lg": "box-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -2px rgba(0,0,0,.05);",
434
+ "shadow-xl": "box-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 10px 10px -5px rgba(0,0,0,.04);",
435
+ "shadow-none": "box-shadow:none;",
436
+ # Position
437
+ "relative": "position:relative;",
438
+ "absolute": "position:absolute;",
439
+ "fixed": "position:fixed;",
440
+ "sticky": "position:sticky;",
441
+ "inset-0": "top:0; right:0; bottom:0; left:0;",
442
+ "top-0": "top:0;", "bottom-0": "bottom:0;",
443
+ "left-0": "left:0;", "right-0": "right:0;",
444
+ # Z-index
445
+ "z-10": "z-index:10;", "z-20": "z-index:20;", "z-50": "z-index:50;",
446
+ # Overflow
447
+ "overflow-hidden": "overflow:hidden;",
448
+ "overflow-auto": "overflow:auto;",
449
+ "overflow-y-auto": "overflow-y:auto;",
450
+ "overflow-x-auto": "overflow-x:auto;",
451
+ "overflow-x-hidden": "overflow-x:hidden;",
452
+ # Cursor
453
+ "cursor-pointer": "cursor:pointer;",
454
+ "cursor-default": "cursor:default;",
455
+ # Pointer events
456
+ "pointer-events-none": "pointer-events:none;",
457
+ # Misc
458
+ "truncate": "overflow:hidden; text-overflow:ellipsis; white-space:nowrap;",
459
+ "whitespace-nowrap": "white-space:nowrap;",
460
+ "break-words": "word-break:break-word;",
461
+ "select-none": "user-select:none;",
462
+ "resize-none": "resize:none;",
463
+ "outline-none": "outline:none;",
464
+ }
465
+
466
+ # ------------------------------------------------------------------ #
467
+ # Public API
468
+ # ------------------------------------------------------------------ #
469
+
470
+ def render(self, component_tree: Dict[str, Any], mobile_frame: bool = True) -> str:
471
+ """Render component tree to a full HTML page."""
472
+ content = self._render_component(component_tree)
473
+ if mobile_frame:
474
+ tmpl = Template(_PAGE_TEMPLATE)
475
+ return tmpl.render(content=content, ws_script=_WS_SCRIPT)
476
+ return f"<!DOCTYPE html><html><body>{content}<script>{_WS_SCRIPT}</script></body></html>"
477
+
478
+ def render_content(self, component_tree: Dict[str, Any]) -> str:
479
+ """Render only the inner HTML (used for WebSocket live updates)."""
480
+ return self._render_component(component_tree)
481
+
482
+ # ------------------------------------------------------------------ #
483
+ # Internal rendering
484
+ # ------------------------------------------------------------------ #
485
+
486
+ def _render_component(self, component: Dict[str, Any]) -> str:
487
+ if not isinstance(component, dict):
488
+ return str(component)
489
+
490
+ ctype = component.get("type", "div")
491
+ props = component.get("props", {})
492
+ children = component.get("children", [])
493
+
494
+ tag, attrs = self._resolve_tag_attrs(ctype, props)
495
+
496
+ # Build inner HTML
497
+ inner = ""
498
+ if ctype == "Text":
499
+ inner = self._escape(props.get("value", ""))
500
+ elif ctype == "Button":
501
+ inner = self._escape(props.get("label", ""))
502
+ elif ctype == "Badge":
503
+ inner = self._escape(props.get("label", ""))
504
+ elif ctype == "Icon":
505
+ inner = self._escape(props.get("name", ""))
506
+
507
+ for child in children:
508
+ if isinstance(child, dict):
509
+ inner += self._render_component(child)
510
+ else:
511
+ inner += self._escape(str(child))
512
+
513
+ if tag in ("input", "img", "br", "hr"):
514
+ return f"<{tag} {attrs}/>"
515
+ return f"<{tag} {attrs}>{inner}</{tag}>"
516
+
517
+ def _resolve_tag_attrs(self, ctype: str, props: Dict[str, Any]):
518
+ tag_map = {
519
+ "Container": "div", "Column": "div", "Row": "div",
520
+ "Card": "div", "ScrollView": "div", "Screen": "div",
521
+ "Navigator": "div", "Modal": "div", "Carousel": "div",
522
+ "Text": "p", "Button": "button",
523
+ "Input": "input", "Image": "img",
524
+ "List": "ul", "Badge": "span", "Icon": "span",
525
+ }
526
+ tag = tag_map.get(ctype, "div")
527
+ parts = []
528
+
529
+ # ------ Inline styles from Tailwind classes ------
530
+ style_parts = []
531
+ raw_class = props.get("class", "")
532
+ if raw_class:
533
+ for cls in raw_class.split():
534
+ css = self.TAILWIND_CLASSES.get(cls)
535
+ if css:
536
+ style_parts.append(css)
537
+
538
+ if "style" in props and props["style"]:
539
+ style_parts.append(props["style"])
540
+
541
+ if ctype == "Carousel":
542
+ style_parts.append(
543
+ "display:flex;flex-direction:row;overflow-x:auto;"
544
+ "-webkit-overflow-scrolling:touch;scrollbar-width:none;"
545
+ )
546
+ elif ctype == "Modal" and props.get("visible") is False:
547
+ style_parts.append("display:none;")
548
+
549
+ if style_parts:
550
+ parts.append(f'style="{" ".join(style_parts)}"')
551
+
552
+ # ------ Component-specific attributes ------
553
+ if ctype == "Button":
554
+ parts.append('type="button"')
555
+ on_click = props.get("on_click")
556
+ if on_click:
557
+ name = on_click.__name__ if callable(on_click) else str(on_click)
558
+ click_args = props.get("on_click_args") or []
559
+ if click_args:
560
+ js_args = ", ".join(self._js_str(a) for a in click_args)
561
+ parts.append(f"onclick=\"handleClick('{name}', {js_args})\"")
562
+ else:
563
+ parts.append(f"onclick=\"handleClick('{name}')\"")
564
+
565
+ elif ctype == "Input":
566
+ input_type = props.get("input_type", "text")
567
+ parts.append(f'type="{input_type}"')
568
+ if "placeholder" in props and props["placeholder"]:
569
+ parts.append(f'placeholder="{self._escape_attr(props["placeholder"])}"')
570
+ if "value" in props and props["value"] is not None:
571
+ parts.append(f'value="{self._escape_attr(str(props["value"]))}"')
572
+ on_change = props.get("on_change")
573
+ if on_change:
574
+ name = on_change.__name__ if callable(on_change) else str(on_change)
575
+ parts.append(f"oninput=\"handleChange('{name}', this.value)\"")
576
+
577
+ elif ctype == "Image":
578
+ if "src" in props:
579
+ parts.append(f'src="{props["src"]}"')
580
+ if "alt" in props:
581
+ parts.append(f'alt="{self._escape_attr(props["alt"])}"')
582
+
583
+ return tag, " ".join(parts)
584
+
585
+ @staticmethod
586
+ def _escape(text: str) -> str:
587
+ return str(text).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
588
+
589
+ @staticmethod
590
+ def _escape_attr(text: str) -> str:
591
+ return str(text).replace('"', "&quot;").replace("'", "&#39;")
592
+
593
+ @staticmethod
594
+ def _js_str(value) -> str:
595
+ """Wrap a value as a single-quoted JS string literal safe inside an HTML attribute."""
596
+ s = str(value).replace("\\", "\\\\").replace("'", "\\'")
597
+ return f"'{s}'"