grimoireplot 0.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.
- grimoireplot/__init__.py +5 -0
- grimoireplot/client.py +166 -0
- grimoireplot/common.py +32 -0
- grimoireplot/create_some_plots.py +352 -0
- grimoireplot/main.py +284 -0
- grimoireplot/models.py +210 -0
- grimoireplot/server.py +90 -0
- grimoireplot/ui.py +200 -0
- grimoireplot/ui_elements.py +772 -0
- grimoireplot-0.0.1.dist-info/METADATA +217 -0
- grimoireplot-0.0.1.dist-info/RECORD +13 -0
- grimoireplot-0.0.1.dist-info/WHEEL +4 -0
- grimoireplot-0.0.1.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright © 2026 Idiap Research Institute <contact@idiap.ch>
|
|
2
|
+
# SPDX-FileContributor: William Droz <william.droz@idiap.ch>
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Styled UI components for GrimoirePlot.
|
|
7
|
+
|
|
8
|
+
This module provides factory functions for creating consistently styled UI elements.
|
|
9
|
+
All appearance customization is centralized here for easy theming.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Callable, Any, Optional
|
|
13
|
+
from nicegui import ui
|
|
14
|
+
|
|
15
|
+
# ============================================================================
|
|
16
|
+
# THEME CONFIGURATION
|
|
17
|
+
# ============================================================================
|
|
18
|
+
|
|
19
|
+
# Color palette - mystical/grimoire inspired
|
|
20
|
+
COLORS = {
|
|
21
|
+
# Primary colors
|
|
22
|
+
"primary": "#8B5CF6", # Violet
|
|
23
|
+
"primary_dark": "#7C3AED",
|
|
24
|
+
"primary_light": "#A78BFA",
|
|
25
|
+
# Secondary colors
|
|
26
|
+
"secondary": "#06B6D4", # Cyan
|
|
27
|
+
"secondary_dark": "#0891B2",
|
|
28
|
+
"secondary_light": "#22D3EE",
|
|
29
|
+
# Accent colors
|
|
30
|
+
"accent": "#F59E0B", # Amber
|
|
31
|
+
"accent_dark": "#D97706",
|
|
32
|
+
"accent_light": "#FBBF24",
|
|
33
|
+
# Semantic colors
|
|
34
|
+
"success": "#10B981", # Emerald
|
|
35
|
+
"warning": "#F59E0B", # Amber
|
|
36
|
+
"error": "#EF4444", # Red
|
|
37
|
+
"info": "#3B82F6", # Blue
|
|
38
|
+
# Background colors (dark theme)
|
|
39
|
+
"bg_dark": "#0F0F1A", # Deep dark
|
|
40
|
+
"bg_card": "#1A1A2E", # Card background
|
|
41
|
+
"bg_elevated": "#252542", # Elevated elements
|
|
42
|
+
"bg_hover": "#2D2D4A", # Hover state
|
|
43
|
+
# Text colors
|
|
44
|
+
"text_primary": "#F8FAFC", # Near white
|
|
45
|
+
"text_secondary": "#94A3B8", # Muted
|
|
46
|
+
"text_muted": "#64748B", # Very muted
|
|
47
|
+
# Border colors
|
|
48
|
+
"border": "#3B3B5C",
|
|
49
|
+
"border_accent": "#8B5CF6",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# CSS for global styling
|
|
53
|
+
GLOBAL_CSS = """
|
|
54
|
+
:root {
|
|
55
|
+
--nicegui-default-padding: 0.75rem;
|
|
56
|
+
--nicegui-default-gap: 0.75rem;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Disable ALL transitions and animations */
|
|
60
|
+
*, *::before, *::after {
|
|
61
|
+
transition: none !important;
|
|
62
|
+
animation: none !important;
|
|
63
|
+
animation-duration: 0s !important;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
body {
|
|
67
|
+
background: linear-gradient(135deg, #0F0F1A 0%, #1A1A2E 50%, #16213E 100%) !important;
|
|
68
|
+
min-height: 100vh;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* Glassmorphism card effect */
|
|
72
|
+
.glass-card {
|
|
73
|
+
background: rgba(26, 26, 46, 0.7) !important;
|
|
74
|
+
backdrop-filter: blur(12px) !important;
|
|
75
|
+
-webkit-backdrop-filter: blur(12px) !important;
|
|
76
|
+
border: 1px solid rgba(139, 92, 246, 0.2) !important;
|
|
77
|
+
border-radius: 16px !important;
|
|
78
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3) !important;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.glass-card:hover {
|
|
82
|
+
border-color: rgba(139, 92, 246, 0.4) !important;
|
|
83
|
+
box-shadow: 0 8px 32px rgba(139, 92, 246, 0.15) !important;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* Gradient text */
|
|
87
|
+
.gradient-text {
|
|
88
|
+
background: linear-gradient(135deg, #8B5CF6 0%, #06B6D4 100%);
|
|
89
|
+
-webkit-background-clip: text;
|
|
90
|
+
-webkit-text-fill-color: transparent;
|
|
91
|
+
background-clip: text;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* Glow effects */
|
|
95
|
+
.glow-primary {
|
|
96
|
+
box-shadow: 0 0 20px rgba(139, 92, 246, 0.4);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.glow-subtle {
|
|
100
|
+
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* Tab styling */
|
|
104
|
+
.q-tab--active {
|
|
105
|
+
background: linear-gradient(135deg, rgba(139, 92, 246, 0.2) 0%, rgba(6, 182, 212, 0.2) 100%) !important;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.q-tab__indicator {
|
|
109
|
+
background: linear-gradient(90deg, #8B5CF6 0%, #06B6D4 100%) !important;
|
|
110
|
+
height: 3px !important;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Delete badge styling */
|
|
114
|
+
.delete-badge {
|
|
115
|
+
cursor: pointer !important;
|
|
116
|
+
transition: all 0.2s ease !important;
|
|
117
|
+
opacity: 0.7;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.delete-badge:hover {
|
|
121
|
+
opacity: 1 !important;
|
|
122
|
+
transform: scale(1.1) !important;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* Plot container */
|
|
126
|
+
.plot-container {
|
|
127
|
+
background: rgba(26, 26, 46, 0.5) !important;
|
|
128
|
+
border-radius: 12px !important;
|
|
129
|
+
border: 1px solid rgba(139, 92, 246, 0.15) !important;
|
|
130
|
+
overflow: hidden !important;
|
|
131
|
+
padding: 36px 12px 12px 12px !important;
|
|
132
|
+
position: relative;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.plot-container .js-plotly-plot,
|
|
136
|
+
.plot-container .plot-container.plotly,
|
|
137
|
+
.plot-container .svg-container {
|
|
138
|
+
width: 100% !important;
|
|
139
|
+
max-width: 100% !important;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.plot-container .q-badge {
|
|
143
|
+
position: absolute !important;
|
|
144
|
+
top: 6px !important;
|
|
145
|
+
right: 6px !important;
|
|
146
|
+
z-index: 10;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.plot-container:hover {
|
|
150
|
+
border-color: rgba(139, 92, 246, 0.4) !important;
|
|
151
|
+
box-shadow: 0 12px 40px rgba(139, 92, 246, 0.15) !important;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* Scrollbar styling */
|
|
155
|
+
::-webkit-scrollbar {
|
|
156
|
+
width: 8px;
|
|
157
|
+
height: 8px;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
::-webkit-scrollbar-track {
|
|
161
|
+
background: #1A1A2E;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
::-webkit-scrollbar-thumb {
|
|
165
|
+
background: linear-gradient(180deg, #8B5CF6 0%, #06B6D4 100%);
|
|
166
|
+
border-radius: 4px;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
::-webkit-scrollbar-thumb:hover {
|
|
170
|
+
background: linear-gradient(180deg, #A78BFA 0%, #22D3EE 100%);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* Animation for new elements */
|
|
174
|
+
@keyframes fadeInUp {
|
|
175
|
+
from {
|
|
176
|
+
opacity: 0;
|
|
177
|
+
transform: translateY(20px);
|
|
178
|
+
}
|
|
179
|
+
to {
|
|
180
|
+
opacity: 1;
|
|
181
|
+
transform: translateY(0);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.animate-in {
|
|
186
|
+
animation: fadeInUp 0.4s ease-out;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* Button gradients */
|
|
190
|
+
.btn-gradient-danger {
|
|
191
|
+
background: linear-gradient(135deg, #EF4444 0%, #DC2626 100%) !important;
|
|
192
|
+
border: none !important;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.btn-gradient-primary {
|
|
196
|
+
background: linear-gradient(135deg, #8B5CF6 0%, #7C3AED 100%) !important;
|
|
197
|
+
border: none !important;
|
|
198
|
+
}
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def setup_theme():
|
|
203
|
+
"""Initialize the theme with custom colors and CSS.
|
|
204
|
+
|
|
205
|
+
Call this once at the start of your page.
|
|
206
|
+
"""
|
|
207
|
+
# Set Quasar colors
|
|
208
|
+
ui.colors(
|
|
209
|
+
primary=COLORS["primary"],
|
|
210
|
+
secondary=COLORS["secondary"],
|
|
211
|
+
accent=COLORS["accent"],
|
|
212
|
+
dark=COLORS["bg_dark"],
|
|
213
|
+
dark_page=COLORS["bg_dark"],
|
|
214
|
+
positive=COLORS["success"],
|
|
215
|
+
negative=COLORS["error"],
|
|
216
|
+
info=COLORS["info"],
|
|
217
|
+
warning=COLORS["warning"],
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Add global CSS
|
|
221
|
+
ui.add_css(GLOBAL_CSS)
|
|
222
|
+
|
|
223
|
+
# Enable dark mode
|
|
224
|
+
ui.dark_mode(True)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# ============================================================================
|
|
228
|
+
# BUTTON FACTORIES
|
|
229
|
+
# ============================================================================
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def create_btn_delete(on_click: Callable[[], Any]) -> ui.button:
|
|
233
|
+
"""Create a styled delete button with icon.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
on_click: Callback function when button is clicked.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Styled delete button.
|
|
240
|
+
"""
|
|
241
|
+
btn = ui.button(icon="close", on_click=on_click)
|
|
242
|
+
btn.props("round flat size=sm")
|
|
243
|
+
btn.classes(
|
|
244
|
+
"btn-gradient-danger text-white opacity-70 hover:opacity-100 transition-all"
|
|
245
|
+
)
|
|
246
|
+
btn.style("width: 24px; height: 24px; min-height: 24px;")
|
|
247
|
+
return btn
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def create_btn_primary(
|
|
251
|
+
text: str, on_click: Optional[Callable[[], Any]] = None, icon: Optional[str] = None
|
|
252
|
+
) -> ui.button:
|
|
253
|
+
"""Create a styled primary action button.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
text: Button label text.
|
|
257
|
+
on_click: Callback function when button is clicked.
|
|
258
|
+
icon: Optional icon name.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Styled primary button.
|
|
262
|
+
"""
|
|
263
|
+
btn = ui.button(text, on_click=on_click, icon=icon)
|
|
264
|
+
btn.props("rounded unelevated")
|
|
265
|
+
btn.classes("btn-gradient-primary text-white font-medium px-6")
|
|
266
|
+
return btn
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def create_btn_secondary(
|
|
270
|
+
text: str, on_click: Optional[Callable[[], Any]] = None, icon: Optional[str] = None
|
|
271
|
+
) -> ui.button:
|
|
272
|
+
"""Create a styled secondary action button.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
text: Button label text.
|
|
276
|
+
on_click: Callback function when button is clicked.
|
|
277
|
+
icon: Optional icon name.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Styled secondary button.
|
|
281
|
+
"""
|
|
282
|
+
btn = ui.button(text, on_click=on_click, icon=icon)
|
|
283
|
+
btn.props("rounded outline")
|
|
284
|
+
btn.classes("text-white border-violet-500 hover:bg-violet-500/20 transition-all")
|
|
285
|
+
return btn
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def create_btn_ghost(
|
|
289
|
+
text: str, on_click: Optional[Callable[[], Any]] = None, icon: Optional[str] = None
|
|
290
|
+
) -> ui.button:
|
|
291
|
+
"""Create a styled ghost/flat button.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
text: Button label text.
|
|
295
|
+
on_click: Callback function when button is clicked.
|
|
296
|
+
icon: Optional icon name.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
Styled ghost button.
|
|
300
|
+
"""
|
|
301
|
+
btn = ui.button(text, on_click=on_click, icon=icon)
|
|
302
|
+
btn.props("flat rounded")
|
|
303
|
+
btn.classes("text-slate-300 hover:text-white hover:bg-white/10 transition-all")
|
|
304
|
+
return btn
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def create_btn_danger(
|
|
308
|
+
text: str, on_click: Optional[Callable[[], Any]] = None, icon: Optional[str] = None
|
|
309
|
+
) -> ui.button:
|
|
310
|
+
"""Create a styled danger/destructive button.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
text: Button label text.
|
|
314
|
+
on_click: Callback function when button is clicked.
|
|
315
|
+
icon: Optional icon name.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
Styled danger button.
|
|
319
|
+
"""
|
|
320
|
+
btn = ui.button(text, on_click=on_click, icon=icon)
|
|
321
|
+
btn.props("rounded unelevated")
|
|
322
|
+
btn.classes("btn-gradient-danger text-white font-medium")
|
|
323
|
+
return btn
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# ============================================================================
|
|
327
|
+
# BADGE FACTORIES
|
|
328
|
+
# ============================================================================
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def create_delete_badge(on_click: Callable[[Any], Any]) -> ui.badge:
|
|
332
|
+
"""Create a floating delete badge (x button).
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
on_click: Callback function for click event.
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
Styled delete badge.
|
|
339
|
+
"""
|
|
340
|
+
badge = ui.badge("x")
|
|
341
|
+
badge.props("floating rounded color=red")
|
|
342
|
+
badge.classes("delete-badge cursor-pointer")
|
|
343
|
+
badge.style("font-size: 10px; padding: 2px 6px;")
|
|
344
|
+
badge.on("click", on_click)
|
|
345
|
+
return badge
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def create_count_badge(count: int | str, color: str = "primary") -> ui.badge:
|
|
349
|
+
"""Create a count/notification badge.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
count: Number or text to display.
|
|
353
|
+
color: Badge color (Quasar color name).
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
Styled count badge.
|
|
357
|
+
"""
|
|
358
|
+
badge = ui.badge(str(count))
|
|
359
|
+
badge.props(f"rounded color={color}")
|
|
360
|
+
badge.classes("text-xs font-bold")
|
|
361
|
+
return badge
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# ============================================================================
|
|
365
|
+
# CARD & CONTAINER FACTORIES
|
|
366
|
+
# ============================================================================
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def create_glass_card() -> ui.card:
|
|
370
|
+
"""Create a glassmorphism styled card container.
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
Styled glass card (use as context manager).
|
|
374
|
+
"""
|
|
375
|
+
card = ui.card()
|
|
376
|
+
card.classes("glass-card animate-in")
|
|
377
|
+
return card
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def create_elevated_card() -> ui.card:
|
|
381
|
+
"""Create an elevated card with subtle shadow.
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
Styled elevated card (use as context manager).
|
|
385
|
+
"""
|
|
386
|
+
card = ui.card()
|
|
387
|
+
card.classes("glow-subtle rounded-xl")
|
|
388
|
+
card.style(
|
|
389
|
+
f"background: {COLORS['bg_card']}; border: 1px solid {COLORS['border']};"
|
|
390
|
+
)
|
|
391
|
+
return card
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def create_plot_container() -> ui.element:
|
|
395
|
+
"""Create a container for plot display.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
Styled div container for plots.
|
|
399
|
+
"""
|
|
400
|
+
container = ui.element("div")
|
|
401
|
+
container.classes("plot-container p-2 relative")
|
|
402
|
+
return container
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def create_section(title: Optional[str] = None) -> ui.element:
|
|
406
|
+
"""Create a styled section with optional title.
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
title: Optional section title.
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
Section element (use as context manager).
|
|
413
|
+
"""
|
|
414
|
+
section = ui.element("section")
|
|
415
|
+
section.classes("mb-6")
|
|
416
|
+
if title:
|
|
417
|
+
with section:
|
|
418
|
+
create_heading(title, level=2)
|
|
419
|
+
return section
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
# ============================================================================
|
|
423
|
+
# TEXT & LABEL FACTORIES
|
|
424
|
+
# ============================================================================
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def create_heading(text: str, level: int = 1, gradient: bool = True) -> ui.label:
|
|
428
|
+
"""Create a styled heading.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
text: Heading text.
|
|
432
|
+
level: Heading level (1-4).
|
|
433
|
+
gradient: Whether to apply gradient text effect.
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
Styled label as heading.
|
|
437
|
+
"""
|
|
438
|
+
sizes = {
|
|
439
|
+
1: "text-4xl font-bold",
|
|
440
|
+
2: "text-2xl font-semibold",
|
|
441
|
+
3: "text-xl font-medium",
|
|
442
|
+
4: "text-lg font-medium",
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
label = ui.label(text)
|
|
446
|
+
label.classes(sizes.get(level, sizes[1]))
|
|
447
|
+
if gradient:
|
|
448
|
+
label.classes("gradient-text")
|
|
449
|
+
else:
|
|
450
|
+
label.style(f"color: {COLORS['text_primary']};")
|
|
451
|
+
return label
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def create_label(text: str, muted: bool = False) -> ui.label:
|
|
455
|
+
"""Create a styled text label.
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
text: Label text.
|
|
459
|
+
muted: Whether to use muted/secondary styling.
|
|
460
|
+
|
|
461
|
+
Returns:
|
|
462
|
+
Styled label.
|
|
463
|
+
"""
|
|
464
|
+
label = ui.label(text)
|
|
465
|
+
if muted:
|
|
466
|
+
label.classes("text-slate-400")
|
|
467
|
+
else:
|
|
468
|
+
label.style(f"color: {COLORS['text_primary']};")
|
|
469
|
+
return label
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def create_empty_state(message: str, icon: str = "auto_awesome") -> ui.element:
|
|
473
|
+
"""Create a styled empty state message.
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
message: Message to display.
|
|
477
|
+
icon: Icon name to show.
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
Container with empty state styling.
|
|
481
|
+
"""
|
|
482
|
+
with ui.column().classes(
|
|
483
|
+
"items-center justify-center py-12 opacity-60"
|
|
484
|
+
) as container:
|
|
485
|
+
ui.icon(icon, size="xl").classes("text-violet-400 mb-4")
|
|
486
|
+
ui.label(message).classes("text-slate-400 text-lg text-center")
|
|
487
|
+
return container
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
# ============================================================================
|
|
491
|
+
# TAB FACTORIES
|
|
492
|
+
# ============================================================================
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def create_tabs() -> ui.tabs:
|
|
496
|
+
"""Create styled tabs container.
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
Styled tabs component.
|
|
500
|
+
"""
|
|
501
|
+
tabs = ui.tabs()
|
|
502
|
+
tabs.classes("w-full")
|
|
503
|
+
tabs.props("dense indicator-color=transparent active-color=white")
|
|
504
|
+
tabs.style("""
|
|
505
|
+
background: rgba(26, 26, 46, 0.5);
|
|
506
|
+
border-radius: 12px;
|
|
507
|
+
padding: 4px;
|
|
508
|
+
""")
|
|
509
|
+
return tabs
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def create_tab(name: str, icon: Optional[str] = None) -> ui.tab:
|
|
513
|
+
"""Create a styled tab.
|
|
514
|
+
|
|
515
|
+
Args:
|
|
516
|
+
name: Tab name/label.
|
|
517
|
+
icon: Optional icon name.
|
|
518
|
+
|
|
519
|
+
Returns:
|
|
520
|
+
Styled tab component.
|
|
521
|
+
"""
|
|
522
|
+
if icon:
|
|
523
|
+
tab = ui.tab(name, icon=icon)
|
|
524
|
+
else:
|
|
525
|
+
tab = ui.tab(name)
|
|
526
|
+
tab.classes("rounded-lg transition-all")
|
|
527
|
+
tab.style("color: #94A3B8;")
|
|
528
|
+
return tab
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def create_tab_with_delete(name: str, on_delete: Callable[[Any], Any]) -> ui.tab:
|
|
532
|
+
"""Create a tab with a delete badge.
|
|
533
|
+
|
|
534
|
+
Args:
|
|
535
|
+
name: Tab name/label.
|
|
536
|
+
on_delete: Callback for delete action.
|
|
537
|
+
|
|
538
|
+
Returns:
|
|
539
|
+
Tab with delete badge.
|
|
540
|
+
"""
|
|
541
|
+
with ui.tab(name).classes("rounded-lg transition-all") as tab:
|
|
542
|
+
tab.style("color: #94A3B8;")
|
|
543
|
+
create_delete_badge(on_delete)
|
|
544
|
+
return tab
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def create_tab_panels(tabs: ui.tabs, value=None) -> ui.tab_panels:
|
|
548
|
+
"""Create styled tab panels container.
|
|
549
|
+
|
|
550
|
+
Args:
|
|
551
|
+
tabs: Associated tabs component.
|
|
552
|
+
value: Initial selected tab.
|
|
553
|
+
|
|
554
|
+
Returns:
|
|
555
|
+
Styled tab panels.
|
|
556
|
+
"""
|
|
557
|
+
panels = ui.tab_panels(tabs, value=value)
|
|
558
|
+
panels.classes("w-full")
|
|
559
|
+
panels.style("background: transparent;")
|
|
560
|
+
return panels
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def create_tab_panel(name: str) -> ui.tab_panel:
|
|
564
|
+
"""Create a styled tab panel.
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
name: Panel name (must match tab name).
|
|
568
|
+
|
|
569
|
+
Returns:
|
|
570
|
+
Styled tab panel.
|
|
571
|
+
"""
|
|
572
|
+
panel = ui.tab_panel(name)
|
|
573
|
+
panel.classes("p-4")
|
|
574
|
+
return panel
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
# ============================================================================
|
|
578
|
+
# GRID FACTORIES
|
|
579
|
+
# ============================================================================
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def create_plot_grid(columns: int = 2) -> ui.grid:
|
|
583
|
+
"""Create a responsive grid for plots.
|
|
584
|
+
|
|
585
|
+
Args:
|
|
586
|
+
columns: Number of columns.
|
|
587
|
+
|
|
588
|
+
Returns:
|
|
589
|
+
Styled grid component.
|
|
590
|
+
"""
|
|
591
|
+
grid = ui.grid(columns=columns)
|
|
592
|
+
grid.classes("w-full gap-6 p-4")
|
|
593
|
+
return grid
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def create_flex_row(gap: str = "4") -> ui.row:
|
|
597
|
+
"""Create a styled flex row.
|
|
598
|
+
|
|
599
|
+
Args:
|
|
600
|
+
gap: Tailwind gap value.
|
|
601
|
+
|
|
602
|
+
Returns:
|
|
603
|
+
Styled row component.
|
|
604
|
+
"""
|
|
605
|
+
row = ui.row()
|
|
606
|
+
row.classes(f"items-center gap-{gap}")
|
|
607
|
+
return row
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
# ============================================================================
|
|
611
|
+
# DIALOG FACTORIES
|
|
612
|
+
# ============================================================================
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def create_confirm_dialog(
|
|
616
|
+
title: str,
|
|
617
|
+
message: str,
|
|
618
|
+
on_confirm: Callable[[], Any],
|
|
619
|
+
confirm_text: str = "Delete",
|
|
620
|
+
cancel_text: str = "Cancel",
|
|
621
|
+
) -> ui.dialog:
|
|
622
|
+
"""Create a styled confirmation dialog.
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
title: Dialog title.
|
|
626
|
+
message: Confirmation message.
|
|
627
|
+
on_confirm: Callback on confirm.
|
|
628
|
+
confirm_text: Confirm button text.
|
|
629
|
+
cancel_text: Cancel button text.
|
|
630
|
+
|
|
631
|
+
Returns:
|
|
632
|
+
Dialog component (call .open() to show).
|
|
633
|
+
"""
|
|
634
|
+
with ui.dialog() as dialog, create_glass_card():
|
|
635
|
+
ui.label(title).classes("text-xl font-bold text-white mb-2")
|
|
636
|
+
ui.label(message).classes("text-slate-300 mb-6")
|
|
637
|
+
|
|
638
|
+
with ui.row().classes("justify-end gap-3"):
|
|
639
|
+
create_btn_ghost(cancel_text, on_click=dialog.close)
|
|
640
|
+
create_btn_danger(
|
|
641
|
+
confirm_text, on_click=lambda: (on_confirm(), dialog.close())
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
return dialog
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
# ============================================================================
|
|
648
|
+
# PLOT DISPLAY
|
|
649
|
+
# ============================================================================
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def create_plotly_chart(fig_data: dict) -> ui.plotly:
|
|
653
|
+
"""Create a styled Plotly chart.
|
|
654
|
+
|
|
655
|
+
Applies dark theme styling to the chart.
|
|
656
|
+
|
|
657
|
+
Args:
|
|
658
|
+
fig_data: Plotly figure data dict.
|
|
659
|
+
|
|
660
|
+
Returns:
|
|
661
|
+
Styled plotly component.
|
|
662
|
+
"""
|
|
663
|
+
# Ensure dark theme layout
|
|
664
|
+
if "layout" not in fig_data:
|
|
665
|
+
fig_data["layout"] = {}
|
|
666
|
+
|
|
667
|
+
layout = fig_data["layout"]
|
|
668
|
+
layout.setdefault("paper_bgcolor", "rgba(0,0,0,0)")
|
|
669
|
+
layout.setdefault("plot_bgcolor", "rgba(26, 26, 46, 0.3)")
|
|
670
|
+
layout.setdefault("font", {"color": "#94A3B8"})
|
|
671
|
+
layout.setdefault("height", 450)
|
|
672
|
+
layout.setdefault("autosize", True)
|
|
673
|
+
|
|
674
|
+
# Disable Plotly animations
|
|
675
|
+
layout.setdefault("transition", {"duration": 0})
|
|
676
|
+
|
|
677
|
+
# Style axes
|
|
678
|
+
for axis in ["xaxis", "yaxis"]:
|
|
679
|
+
if axis not in layout:
|
|
680
|
+
layout[axis] = {}
|
|
681
|
+
layout[axis].setdefault("gridcolor", "rgba(139, 92, 246, 0.1)")
|
|
682
|
+
layout[axis].setdefault("linecolor", "rgba(139, 92, 246, 0.3)")
|
|
683
|
+
layout[axis].setdefault("tickcolor", "#64748B")
|
|
684
|
+
|
|
685
|
+
# Style legend
|
|
686
|
+
layout.setdefault(
|
|
687
|
+
"legend",
|
|
688
|
+
{
|
|
689
|
+
"bgcolor": "rgba(26, 26, 46, 0.8)",
|
|
690
|
+
"bordercolor": "rgba(139, 92, 246, 0.2)",
|
|
691
|
+
"font": {"color": "#94A3B8"},
|
|
692
|
+
},
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
# Enable responsive mode via Plotly config
|
|
696
|
+
if "config" not in fig_data:
|
|
697
|
+
fig_data["config"] = {}
|
|
698
|
+
fig_data["config"].setdefault("responsive", True)
|
|
699
|
+
|
|
700
|
+
chart = ui.plotly(fig_data).classes("w-full").style("height: 450px;")
|
|
701
|
+
return chart
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
# ============================================================================
|
|
705
|
+
# HEADER / NAVIGATION
|
|
706
|
+
# ============================================================================
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def create_header(
|
|
710
|
+
title: str = "GrimoirePlot", subtitle: Optional[str] = None
|
|
711
|
+
) -> ui.element:
|
|
712
|
+
"""Create a styled page header.
|
|
713
|
+
|
|
714
|
+
Args:
|
|
715
|
+
title: Main title text.
|
|
716
|
+
subtitle: Optional subtitle.
|
|
717
|
+
|
|
718
|
+
Returns:
|
|
719
|
+
Header container element.
|
|
720
|
+
"""
|
|
721
|
+
with ui.element("header").classes("mb-8 text-center py-6") as header:
|
|
722
|
+
# Logo/icon
|
|
723
|
+
ui.icon("auto_stories", size="xl").classes("text-violet-400 mb-2")
|
|
724
|
+
|
|
725
|
+
# Title with gradient
|
|
726
|
+
create_heading(title, level=1, gradient=True)
|
|
727
|
+
|
|
728
|
+
if subtitle:
|
|
729
|
+
ui.label(subtitle).classes("text-slate-400 mt-2")
|
|
730
|
+
|
|
731
|
+
return header
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def create_footer() -> ui.element:
|
|
735
|
+
"""Create a styled page footer.
|
|
736
|
+
|
|
737
|
+
Returns:
|
|
738
|
+
Footer container element.
|
|
739
|
+
"""
|
|
740
|
+
with ui.element("footer").classes("mt-auto py-4 text-center") as footer:
|
|
741
|
+
ui.label("GrimoirePlot").classes("text-slate-500 text-sm")
|
|
742
|
+
return footer
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
# ============================================================================
|
|
746
|
+
# LAYOUT HELPERS
|
|
747
|
+
# ============================================================================
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def create_page_container() -> ui.element:
|
|
751
|
+
"""Create the main page container with proper styling.
|
|
752
|
+
|
|
753
|
+
Returns:
|
|
754
|
+
Container element (use as context manager).
|
|
755
|
+
"""
|
|
756
|
+
container = ui.element("div")
|
|
757
|
+
container.classes("container mx-auto px-4 py-6 min-h-screen flex flex-col")
|
|
758
|
+
return container
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
def create_divider() -> ui.element:
|
|
762
|
+
"""Create a styled horizontal divider.
|
|
763
|
+
|
|
764
|
+
Returns:
|
|
765
|
+
Divider element.
|
|
766
|
+
"""
|
|
767
|
+
div = ui.element("hr")
|
|
768
|
+
div.classes("border-0 h-px my-6")
|
|
769
|
+
div.style(
|
|
770
|
+
"background: linear-gradient(90deg, transparent, rgba(139, 92, 246, 0.3), transparent);"
|
|
771
|
+
)
|
|
772
|
+
return div
|