web-in-python-lol 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- web_in_python_lol-0.1.0/Engine/__init__.py +2 -0
- web_in_python_lol-0.1.0/Engine/core.py +453 -0
- web_in_python_lol-0.1.0/Engine/setup.py +10 -0
- web_in_python_lol-0.1.0/PKG-INFO +10 -0
- web_in_python_lol-0.1.0/pyproject.toml +21 -0
- web_in_python_lol-0.1.0/setup.cfg +4 -0
- web_in_python_lol-0.1.0/web_in_python_lol.egg-info/PKG-INFO +10 -0
- web_in_python_lol-0.1.0/web_in_python_lol.egg-info/SOURCES.txt +8 -0
- web_in_python_lol-0.1.0/web_in_python_lol.egg-info/dependency_links.txt +1 -0
- web_in_python_lol-0.1.0/web_in_python_lol.egg-info/top_level.txt +2 -0
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import sqlite3, re, json, traceback, threading, webbrowser
|
|
2
|
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
3
|
+
from urllib.parse import parse_qs, urlparse
|
|
4
|
+
|
|
5
|
+
# --- DATABASE LAYER ---
|
|
6
|
+
class Database:
|
|
7
|
+
def __init__(self, db_name="system_data.db"):
|
|
8
|
+
self.db_name = db_name
|
|
9
|
+
self.lock = threading.Lock()
|
|
10
|
+
self._init_db()
|
|
11
|
+
|
|
12
|
+
def _init_db(self):
|
|
13
|
+
with self.lock:
|
|
14
|
+
with sqlite3.connect(self.db_name, check_same_thread=False) as conn:
|
|
15
|
+
conn.execute("CREATE TABLE IF NOT EXISTS storage (key TEXT PRIMARY KEY, value TEXT, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
|
|
16
|
+
|
|
17
|
+
def save(self, key, data):
|
|
18
|
+
with self.lock:
|
|
19
|
+
with sqlite3.connect(self.db_name, check_same_thread=False) as conn:
|
|
20
|
+
conn.execute("INSERT OR REPLACE INTO storage (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)", (key, json.dumps(data)))
|
|
21
|
+
|
|
22
|
+
def load(self, key, default=None):
|
|
23
|
+
with self.lock:
|
|
24
|
+
with sqlite3.connect(self.db_name, check_same_thread=False) as conn:
|
|
25
|
+
row = conn.execute("SELECT value FROM storage WHERE key = ?", (key,)).fetchone()
|
|
26
|
+
return json.loads(row[0]) if row else default
|
|
27
|
+
|
|
28
|
+
def get_last_update(self):
|
|
29
|
+
with self.lock:
|
|
30
|
+
with sqlite3.connect(self.db_name, check_same_thread=False) as conn:
|
|
31
|
+
res = conn.execute("SELECT MAX(updated_at) FROM storage").fetchone()
|
|
32
|
+
return res[0] if res and res[0] else "0"
|
|
33
|
+
|
|
34
|
+
# --- CORE ENGINE ---
|
|
35
|
+
class ShadowEngine(BaseHTTPRequestHandler):
|
|
36
|
+
def do_GET(self):
|
|
37
|
+
if self.path == '/__poll__':
|
|
38
|
+
self.send_response(200); self.end_headers()
|
|
39
|
+
self.wfile.write(str(self.server.app_instance.db.get_last_update()).encode())
|
|
40
|
+
return
|
|
41
|
+
self.handle_request()
|
|
42
|
+
|
|
43
|
+
def do_POST(self):
|
|
44
|
+
length = int(self.headers.get('Content-Length', 0))
|
|
45
|
+
params = {k: v[0] for k, v in parse_qs(self.rfile.read(length).decode()).items()}
|
|
46
|
+
func, args = self.find_route(urlparse(self.path).path)
|
|
47
|
+
if func: func(self.server.app_instance, params, *args, is_post=True)
|
|
48
|
+
self.send_response(303); self.send_header('Location', self.headers.get('Referer', '/')); self.end_headers()
|
|
49
|
+
|
|
50
|
+
def handle_request(self):
|
|
51
|
+
func, args = self.find_route(urlparse(self.path).path)
|
|
52
|
+
params = {k: v[0] for k, v in parse_qs(urlparse(self.path).query).items()}
|
|
53
|
+
self.send_response(200); self.send_header("Content-type", "text/html"); self.end_headers()
|
|
54
|
+
try:
|
|
55
|
+
content = func(self.server.app_instance, params, *args) if func else "<h1>404</h1>"
|
|
56
|
+
except:
|
|
57
|
+
content = f"<pre style='color:red;'>{traceback.format_exc()}</pre>"
|
|
58
|
+
self.wfile.write(self.server.app_instance._wrap(content).encode())
|
|
59
|
+
|
|
60
|
+
def find_route(self, path):
|
|
61
|
+
for pattern, func in self.server.app_instance.routes:
|
|
62
|
+
match = pattern.match(path)
|
|
63
|
+
if match: return func, match.groups()
|
|
64
|
+
return None, []
|
|
65
|
+
|
|
66
|
+
class WebApp:
|
|
67
|
+
def __init__(self, name="ShadowUI"):
|
|
68
|
+
self.name = name
|
|
69
|
+
self.db = Database()
|
|
70
|
+
self.routes = []
|
|
71
|
+
|
|
72
|
+
def store(self, k, v): self.db.save(k, v)
|
|
73
|
+
def fetch(self, k, d=None): return self.db.load(k, d)
|
|
74
|
+
def page(self, path):
|
|
75
|
+
def d(f): self.routes.append((re.compile(f"^{path}$"), f)); return f
|
|
76
|
+
return d
|
|
77
|
+
|
|
78
|
+
def _wrap(self, content):
|
|
79
|
+
return f"""
|
|
80
|
+
<html>
|
|
81
|
+
<head>
|
|
82
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
83
|
+
<script src="https://unpkg.com/lucide@latest"></script>
|
|
84
|
+
<style>
|
|
85
|
+
body {{ margin:0; background:#0e1117; color:#fff; font-family:sans-serif; overflow-x: hidden; }}
|
|
86
|
+
* {{ box-sizing:border-box; }}
|
|
87
|
+
|
|
88
|
+
.nav-links {{ display: flex; align-items: center; }}
|
|
89
|
+
.menu-toggle {{
|
|
90
|
+
display: none; cursor: pointer; color: #6366f1;
|
|
91
|
+
background: none; border: none; padding: 5px;
|
|
92
|
+
}}
|
|
93
|
+
/* Fix icon centering */
|
|
94
|
+
.menu-toggle i {{ display: block; }}
|
|
95
|
+
|
|
96
|
+
@media (max-width: 768px) {{
|
|
97
|
+
.nav-links {{
|
|
98
|
+
display: none; flex-direction: column; width: 100%;
|
|
99
|
+
position: absolute; top: 60px; left: 0;
|
|
100
|
+
background: #000; padding: 20px; border-bottom: 1px solid #333;
|
|
101
|
+
}}
|
|
102
|
+
.nav-links.active {{ display: flex; }}
|
|
103
|
+
.nav-links a {{ margin: 10px 0 !important; width: 100%; text-align: center; }}
|
|
104
|
+
.menu-toggle {{ display: block; }}
|
|
105
|
+
}}
|
|
106
|
+
div[style*="max-width:1000px"] {{ max-width: 100% !important; padding: 20px !important; }}
|
|
107
|
+
</style>
|
|
108
|
+
<script>
|
|
109
|
+
function toggleMenu() {{
|
|
110
|
+
const nav = document.getElementById('mobile-nav');
|
|
111
|
+
nav.classList.toggle('active');
|
|
112
|
+
}}
|
|
113
|
+
// Initialize icons after page load
|
|
114
|
+
window.onload = () => lucide.createIcons();
|
|
115
|
+
|
|
116
|
+
let last=null;
|
|
117
|
+
setInterval(async()=>{{
|
|
118
|
+
let r=await fetch('/__poll__');
|
|
119
|
+
let t=await r.text();
|
|
120
|
+
if(last&&t!==last)location.reload();
|
|
121
|
+
last=t;
|
|
122
|
+
}},1500);
|
|
123
|
+
</script>
|
|
124
|
+
</head>
|
|
125
|
+
<body>{content}</body>
|
|
126
|
+
</html>"""
|
|
127
|
+
|
|
128
|
+
def start(self, port=8080):
|
|
129
|
+
srv = HTTPServer(("0.0.0.0", port), ShadowEngine)
|
|
130
|
+
srv.app_instance = self
|
|
131
|
+
print(f"Running on port {port}"); srv.serve_forever()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class Component:
|
|
136
|
+
def __init__(self, style=None):
|
|
137
|
+
self._styles = style or {}
|
|
138
|
+
|
|
139
|
+
def css(self):
|
|
140
|
+
return "; ".join([f"{k.replace('_', '-')}:{v}" for k, v in self._styles.items()])
|
|
141
|
+
|
|
142
|
+
# --- HELPER ---
|
|
143
|
+
def set_style(self, prop, val):
|
|
144
|
+
self._styles[prop] = val
|
|
145
|
+
return self
|
|
146
|
+
def on_click(self, js_code):
|
|
147
|
+
return self.set_style("onclick", js_code)
|
|
148
|
+
|
|
149
|
+
# --- 1. LAYOUT & FLEXBOX (The most used) ---
|
|
150
|
+
def display(self, v): return self.set_style('display', v)
|
|
151
|
+
def flex(self, v): return self.set_style('flex', v)
|
|
152
|
+
def flex_direction(self, v): return self.set_style('flex-direction', v)
|
|
153
|
+
def justify_content(self, v): return self.set_style('justify-content', v)
|
|
154
|
+
def align_items(self, v): return self.set_style('align-items', v)
|
|
155
|
+
def gap(self, v): return self.set_style('gap', v)
|
|
156
|
+
def flex_wrap(self, v): return self.set_style('flex-wrap', v)
|
|
157
|
+
def flex_grow(self, v): return self.set_style('flex-grow', v)
|
|
158
|
+
def order(self, v): return self.set_style('order', v)
|
|
159
|
+
|
|
160
|
+
# --- 2. SPACING ---
|
|
161
|
+
def padding(self, v): return self.set_style('padding', v)
|
|
162
|
+
def p_top(self, v): return self.set_style('padding-top', v)
|
|
163
|
+
def p_bottom(self, v): return self.set_style('padding-bottom', v)
|
|
164
|
+
def p_left(self, v): return self.set_style('padding-left', v)
|
|
165
|
+
def p_right(self, v): return self.set_style('padding-right', v)
|
|
166
|
+
def margin(self, v): return self.set_style('margin', v)
|
|
167
|
+
def m_top(self, v): return self.set_style('margin-top', v)
|
|
168
|
+
def m_bottom(self, v): return self.set_style('margin-bottom', v)
|
|
169
|
+
def m_left(self, v): return self.set_style('margin-left', v)
|
|
170
|
+
def m_right(self, v): return self.set_style('margin-right', v)
|
|
171
|
+
|
|
172
|
+
# --- 3. SIZING ---
|
|
173
|
+
def width(self, v): return self.set_style('width', v)
|
|
174
|
+
def min_width(self, v): return self.set_style('min-width', v)
|
|
175
|
+
def max_width(self, v): return self.set_style('max-width', v)
|
|
176
|
+
def height(self, v): return self.set_style('height', v)
|
|
177
|
+
def min_height(self, v): return self.set_style('min-height', v)
|
|
178
|
+
def max_height(self, v): return self.set_style('max-height', v)
|
|
179
|
+
|
|
180
|
+
# --- 4. COLOR & BACKGROUND ---
|
|
181
|
+
def bg(self, v): return self.set_style('background', v)
|
|
182
|
+
def bg_color(self, v): return self.set_style('background-color', v)
|
|
183
|
+
def bg_image(self, v): return self.set_style('background-image', v)
|
|
184
|
+
def bg_size(self, v): return self.set_style('background-size', v)
|
|
185
|
+
def color(self, v): return self.set_style('color', v)
|
|
186
|
+
def opacity(self, v): return self.set_style('opacity', v)
|
|
187
|
+
|
|
188
|
+
# --- 5. BORDERS & RADIUS ---
|
|
189
|
+
def border(self, v): return self.set_style('border', v)
|
|
190
|
+
def border_top(self, v): return self.set_style('border-top', v)
|
|
191
|
+
def border_bottom(self, v): return self.set_style('border-bottom', v)
|
|
192
|
+
def border_left(self, v): return self.set_style('border-left', v)
|
|
193
|
+
def border_right(self, v): return self.set_style('border-right', v)
|
|
194
|
+
def border_color(self, v): return self.set_style('border-color', v)
|
|
195
|
+
def border_style(self, v): return self.set_style('border-style', v)
|
|
196
|
+
def radius(self, v): return self.set_style('border-radius', v)
|
|
197
|
+
def r_top_left(self, v): return self.set_style('border-top-left-radius', v)
|
|
198
|
+
def r_top_right(self, v): return self.set_style('border-top-right-radius', v)
|
|
199
|
+
def outline(self, v): return self.set_style('outline', v)
|
|
200
|
+
|
|
201
|
+
# --- 6. TYPOGRAPHY ---
|
|
202
|
+
def font_size(self, v): return self.set_style('font-size', v)
|
|
203
|
+
def weight(self, v): return self.set_style('font-weight', v)
|
|
204
|
+
def font_family(self, v): return self.set_style('font-family', v)
|
|
205
|
+
def align(self, v): return self.set_style('text-align', v)
|
|
206
|
+
def decoration(self, v): return self.set_style('text-decoration', v)
|
|
207
|
+
def transform(self, v): return self.set_style('text-transform', v)
|
|
208
|
+
def line_height(self, v): return self.set_style('line-height', v)
|
|
209
|
+
def letter_spacing(self, v): return self.set_style('letter-spacing', v)
|
|
210
|
+
def white_space(self, v): return self.set_style('white-space', v)
|
|
211
|
+
|
|
212
|
+
# --- 7. POSITIONING ---
|
|
213
|
+
def position(self, v): return self.set_style('position', v)
|
|
214
|
+
def top(self, v): return self.set_style('top', v)
|
|
215
|
+
def bottom(self, v): return self.set_style('bottom', v)
|
|
216
|
+
def left(self, v): return self.set_style('left', v)
|
|
217
|
+
def right(self, v): return self.set_style('right', v)
|
|
218
|
+
def z_index(self, v): return self.set_style('z-index', v)
|
|
219
|
+
def overflow(self, v): return self.set_style('overflow', v)
|
|
220
|
+
|
|
221
|
+
# --- 8. EFFECTS & TRANSITIONS ---
|
|
222
|
+
def shadow(self, v): return self.set_style('box-shadow', v)
|
|
223
|
+
def transition(self, v): return self.set_style('transition', v)
|
|
224
|
+
def cursor(self, v): return self.set_style('cursor', v)
|
|
225
|
+
def filter(self, v): return self.set_style('filter', v)
|
|
226
|
+
def blur(self, v): return self.set_style('backdrop-filter', f'blur({v})')
|
|
227
|
+
def transform_effect(self, v): return self.set_style('transform', v)
|
|
228
|
+
|
|
229
|
+
# --- 9. GRID SPECIFIC ---
|
|
230
|
+
def grid_cols(self, v): return self.set_style('grid-template-columns', v)
|
|
231
|
+
def grid_rows(self, v): return self.set_style('grid-template-rows', v)
|
|
232
|
+
def grid_area(self, v): return self.set_style('grid-area', v)
|
|
233
|
+
|
|
234
|
+
# --- 10. UTILITIES ---
|
|
235
|
+
def hide(self): return self.set_style('display', 'none')
|
|
236
|
+
def show(self): return self.set_style('display', 'block')
|
|
237
|
+
def pointer(self): return self.set_style('cursor', 'pointer')
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def render(self): return ""
|
|
241
|
+
|
|
242
|
+
class Group(Component):
|
|
243
|
+
def __init__(self, items):
|
|
244
|
+
super().__init__()
|
|
245
|
+
self.items = items
|
|
246
|
+
def render(self):
|
|
247
|
+
return "".join([i.render() if hasattr(i, 'render') else str(i) for i in self.items])
|
|
248
|
+
|
|
249
|
+
class Container(Component):
|
|
250
|
+
def __init__(self, items):
|
|
251
|
+
super().__init__()
|
|
252
|
+
self.items = items
|
|
253
|
+
def render(self):
|
|
254
|
+
# Default container styles can still be overridden by chaining
|
|
255
|
+
style = {"max-width":"1000px", "margin":"0 auto", "padding":"40px"}
|
|
256
|
+
style.update(self._styles)
|
|
257
|
+
self._styles = style
|
|
258
|
+
return f'<div style="{self.css()}">{Group(self.items).render()}</div>'
|
|
259
|
+
|
|
260
|
+
class Card(Component):
|
|
261
|
+
def __init__(self, items):
|
|
262
|
+
super().__init__()
|
|
263
|
+
self.items = items
|
|
264
|
+
def render(self):
|
|
265
|
+
style = {"background":"#1a1d23", "padding":"20px", "border-radius":"12px", "border":"1px solid #333"}
|
|
266
|
+
style.update(self._styles)
|
|
267
|
+
self._styles = style
|
|
268
|
+
return f'<div style="{self.css()}">{Group(self.items).render()}</div>'
|
|
269
|
+
|
|
270
|
+
class Text(Component):
|
|
271
|
+
def __init__(self, text):
|
|
272
|
+
super().__init__()
|
|
273
|
+
self._content = text # Changed from self.text to avoid killing .text() method
|
|
274
|
+
def render(self):
|
|
275
|
+
return f'<div style="{self.css()}">{self._content}</div>'
|
|
276
|
+
|
|
277
|
+
class Button(Component):
|
|
278
|
+
def __init__(self, text, primary=True):
|
|
279
|
+
super().__init__()
|
|
280
|
+
self.text, self.p = text, primary
|
|
281
|
+
|
|
282
|
+
def render(self):
|
|
283
|
+
bg = "#6366f1" if self.p else "#333"
|
|
284
|
+
base = {"background":bg, "color":"#fff", "border":"none", "padding":"10px 20px", "border-radius":"8px", "cursor":"pointer", "font-weight":"bold"}
|
|
285
|
+
base.update(self._styles)
|
|
286
|
+
|
|
287
|
+
# --- NEW LOGIC: Separate JS Events from CSS ---
|
|
288
|
+
js_events = " ".join([f'{k}="{v}"' for k, v in base.items() if k.startswith("on")])
|
|
289
|
+
css_only = "; ".join([f"{k.replace('_', '-')}:{v}" for k, v in base.items() if not k.startswith("on")])
|
|
290
|
+
|
|
291
|
+
return f'<button {js_events} style="{css_only}">{self.text}</button>'
|
|
292
|
+
|
|
293
|
+
class Row(Component):
|
|
294
|
+
def __init__(self, items, gap="20px"):
|
|
295
|
+
super().__init__()
|
|
296
|
+
self.items = items
|
|
297
|
+
self._internal_gap = gap
|
|
298
|
+
|
|
299
|
+
def render(self):
|
|
300
|
+
g = self._styles.get('gap', self._internal_gap)
|
|
301
|
+
# Added flex-wrap: wrap
|
|
302
|
+
base = {"display": "flex", "flex-direction": "row", "flex-wrap": "wrap", "gap": g, "align-items": "center"}
|
|
303
|
+
base.update(self._styles)
|
|
304
|
+
self._styles = base
|
|
305
|
+
return f'<div style="{self.css()}">{Group(self.items).render()}</div>'
|
|
306
|
+
|
|
307
|
+
class TextInput(Component):
|
|
308
|
+
def __init__(self, label, name, placeholder="", type="text", value=""):
|
|
309
|
+
super().__init__()
|
|
310
|
+
self.label, self.name, self.ph, self.type, self.val = label, name, placeholder, type, value
|
|
311
|
+
|
|
312
|
+
def render(self):
|
|
313
|
+
base = {"width": "100%", "padding": "12px", "background": "#000", "border": "1px solid #333",
|
|
314
|
+
"color": "#fff", "border-radius": "8px", "outline": "none", "margin-top": "5px"}
|
|
315
|
+
base.update(self._styles)
|
|
316
|
+
self._styles = base
|
|
317
|
+
return f'''
|
|
318
|
+
<div style="margin-bottom:15px; width:100%;">
|
|
319
|
+
<label style="color:#888; font-size:12px; font-weight:bold;">{self.label}</label>
|
|
320
|
+
<input type="{self.type}" name="{self.name}" placeholder="{self.ph}" value="{self.val}" style="{self.css()}">
|
|
321
|
+
</div>'''
|
|
322
|
+
|
|
323
|
+
class Form(Component):
|
|
324
|
+
def __init__(self, action, items, method="POST"):
|
|
325
|
+
super().__init__()
|
|
326
|
+
self.action, self.items, self.method = action, items, method
|
|
327
|
+
def render(self):
|
|
328
|
+
# Forms usually just need standard padding or margins
|
|
329
|
+
return f'<form action="{self.action}" method="{self.method}" style="{self.css()}">{Group(self.items).render()}</form>'
|
|
330
|
+
|
|
331
|
+
class Column(Component):
|
|
332
|
+
def __init__(self, items, gap="10px"):
|
|
333
|
+
super().__init__()
|
|
334
|
+
self.items = items
|
|
335
|
+
self._internal_gap = gap # Changed from self.gap
|
|
336
|
+
|
|
337
|
+
def render(self):
|
|
338
|
+
# Allow the .gap() method to override the default constructor gap
|
|
339
|
+
g = self._styles.get('gap', self._internal_gap)
|
|
340
|
+
base = {"display": "flex", "flex-direction": "column", "gap": g}
|
|
341
|
+
base.update(self._styles)
|
|
342
|
+
self._styles = base
|
|
343
|
+
return f'<div style="{self.css()}">{Group(self.items).render()}</div>'
|
|
344
|
+
|
|
345
|
+
class Grid(Component):
|
|
346
|
+
def __init__(self, items, columns=3, gap="20px", min_width="250px"):
|
|
347
|
+
super().__init__()
|
|
348
|
+
self.items, self.cols, self.gap, self.min_w = items, columns, gap, min_width
|
|
349
|
+
|
|
350
|
+
def render(self):
|
|
351
|
+
# Instead of fixed columns, we use repeat(auto-fit) for responsiveness
|
|
352
|
+
base = {
|
|
353
|
+
"display": "grid",
|
|
354
|
+
"grid-template-columns": f"repeat(auto-fit, minmax({self.min_w}, 1fr))",
|
|
355
|
+
"gap": self.gap
|
|
356
|
+
}
|
|
357
|
+
base.update(self._styles)
|
|
358
|
+
self._styles = base
|
|
359
|
+
return f'<div style="{self.css()}">{Group(self.items).render()}</div>'
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class Navbar(Component):
|
|
363
|
+
def __init__(self, brand_name, links):
|
|
364
|
+
super().__init__()
|
|
365
|
+
self.brand, self.links = brand_name, links
|
|
366
|
+
|
|
367
|
+
def render(self):
|
|
368
|
+
base = {
|
|
369
|
+
"padding": "15px 40px", "background": "#000", "border-bottom": "1px solid #333",
|
|
370
|
+
"display": "flex", "justify-content": "space-between", "align-items": "center",
|
|
371
|
+
"position":"sticky", "top":"0", "z-index": "1000"
|
|
372
|
+
}
|
|
373
|
+
base.update(self._styles)
|
|
374
|
+
self._styles = base
|
|
375
|
+
|
|
376
|
+
btns = "".join([
|
|
377
|
+
f'<a href="{u}" style="color:#888; margin-left:20px; text-decoration:none; font-size:14px; font-weight:500;">{n}</a>'
|
|
378
|
+
for n, u in self.links
|
|
379
|
+
])
|
|
380
|
+
|
|
381
|
+
return f'''
|
|
382
|
+
<nav style="{self.css()}">
|
|
383
|
+
<div style="font-weight:bold; font-size:20px; color:#6366f1;">{self.brand}</div>
|
|
384
|
+
|
|
385
|
+
<button class="menu-toggle" onclick="toggleMenu()">
|
|
386
|
+
<i data-lucide="menu"></i>
|
|
387
|
+
</button>
|
|
388
|
+
|
|
389
|
+
<div id="mobile-nav" class="nav-links">
|
|
390
|
+
{btns}
|
|
391
|
+
</div>
|
|
392
|
+
</nav>'''
|
|
393
|
+
|
|
394
|
+
class Image(Component):
|
|
395
|
+
def __init__(self, src, alt="image"):
|
|
396
|
+
super().__init__()
|
|
397
|
+
self.src, self.alt = src, alt
|
|
398
|
+
def render(self):
|
|
399
|
+
base = {"max-width": "100%", "height": "auto", "border-radius": "8px"}
|
|
400
|
+
base.update(self._styles)
|
|
401
|
+
self._styles = base
|
|
402
|
+
return f'<img src="{self.src}" alt="{self.alt}" style="{self.css()}">'
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
class Badge(Component):
|
|
406
|
+
def __init__(self, text, color="#6366f1"):
|
|
407
|
+
super().__init__()
|
|
408
|
+
self.text, self.color = text, color
|
|
409
|
+
def render(self):
|
|
410
|
+
base = {"background": f"{self.color}22", "color": self.color, "padding": "4px 10px",
|
|
411
|
+
"border-radius": "6px", "font-size": "11px", "font-weight": "bold", "display": "inline-block"}
|
|
412
|
+
base.update(self._styles)
|
|
413
|
+
self._styles = base
|
|
414
|
+
return f'<span style="{self.css()}">{self.text}</span>'
|
|
415
|
+
|
|
416
|
+
class Spacer(Component):
|
|
417
|
+
def __init__(self, h="20px", w="20px"):
|
|
418
|
+
super().__init__()
|
|
419
|
+
self.h, self.w = h, w
|
|
420
|
+
def render(self):
|
|
421
|
+
return f'<div style="height:{self.h}; width:{self.w};"></div>'
|
|
422
|
+
|
|
423
|
+
class Icon(Component):
|
|
424
|
+
def __init__(self, name, size=20, color="currentColor"):
|
|
425
|
+
super().__init__()
|
|
426
|
+
self.name, self.size, self.color = name, size, color
|
|
427
|
+
def render(self):
|
|
428
|
+
return f'<i data-lucide="{self.name}" style="width:{self.size}px; height:{self.size}px; color:{self.color}; {self.css()}"></i>'
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="web-in-python-lol",
|
|
5
|
+
version="0.1.0",
|
|
6
|
+
description="A lightweight Python-only UI engine for rapid dashboards",
|
|
7
|
+
author="Basel Ezzat",
|
|
8
|
+
packages=find_packages(),
|
|
9
|
+
install_requires=[], # Since you used only standard libs, keep this empty!
|
|
10
|
+
)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: web-in-python-lol
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A responsive, Python-only UI engine for rapid dashboards
|
|
5
|
+
Author-email: Basel Ezzat <lmcteam206@gmail.com>
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.7
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "web-in-python-lol"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Basel Ezzat", email="lmcteam206@gmail.com" },
|
|
10
|
+
]
|
|
11
|
+
description = "A responsive, Python-only UI engine for rapid dashboards"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.7"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[tool.setuptools.packages.find]
|
|
21
|
+
where = ["."]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: web-in-python-lol
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A responsive, Python-only UI engine for rapid dashboards
|
|
5
|
+
Author-email: Basel Ezzat <lmcteam206@gmail.com>
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.7
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|