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.
- examples/example_ecommerce_app.py +189 -0
- examples/example_todo_app.py +159 -0
- p2m/__init__.py +31 -0
- p2m/cli.py +470 -0
- p2m/config.py +205 -0
- p2m/core/__init__.py +18 -0
- p2m/core/api.py +191 -0
- p2m/core/ast_walker.py +171 -0
- p2m/core/database.py +192 -0
- p2m/core/events.py +56 -0
- p2m/core/render_engine.py +597 -0
- p2m/core/runtime.py +128 -0
- p2m/core/state.py +51 -0
- p2m/core/validator.py +284 -0
- p2m/devserver/__init__.py +9 -0
- p2m/devserver/server.py +84 -0
- p2m/i18n/__init__.py +7 -0
- p2m/i18n/translator.py +74 -0
- p2m/imagine/__init__.py +35 -0
- p2m/imagine/agent.py +463 -0
- p2m/imagine/legacy.py +217 -0
- p2m/llm/__init__.py +20 -0
- p2m/llm/anthropic_provider.py +78 -0
- p2m/llm/base.py +42 -0
- p2m/llm/compatible_provider.py +120 -0
- p2m/llm/factory.py +72 -0
- p2m/llm/ollama_provider.py +89 -0
- p2m/llm/openai_provider.py +79 -0
- p2m/testing/__init__.py +41 -0
- p2m/ui/__init__.py +43 -0
- p2m/ui/components.py +301 -0
- python2mobile-1.0.1.dist-info/METADATA +238 -0
- python2mobile-1.0.1.dist-info/RECORD +50 -0
- python2mobile-1.0.1.dist-info/WHEEL +5 -0
- python2mobile-1.0.1.dist-info/entry_points.txt +2 -0
- python2mobile-1.0.1.dist-info/top_level.txt +3 -0
- tests/test_basic_engine.py +281 -0
- tests/test_build_generation.py +603 -0
- tests/test_build_test_gate.py +150 -0
- tests/test_carousel_modal.py +84 -0
- tests/test_config_system.py +272 -0
- tests/test_i18n.py +101 -0
- tests/test_ifood_app_integration.py +172 -0
- tests/test_imagine_cli.py +133 -0
- tests/test_imagine_command.py +341 -0
- tests/test_llm_providers.py +321 -0
- tests/test_new_apps_integration.py +588 -0
- tests/test_ollama_functional.py +329 -0
- tests/test_real_world_apps.py +228 -0
- 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("&", "&").replace("<", "<").replace(">", ">")
|
|
588
|
+
|
|
589
|
+
@staticmethod
|
|
590
|
+
def _escape_attr(text: str) -> str:
|
|
591
|
+
return str(text).replace('"', """).replace("'", "'")
|
|
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}'"
|