django-cfg 1.2.7__py3-none-any.whl → 1.2.8__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 (45) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/urls.py +2 -2
  3. django_cfg/modules/django_unfold/callbacks/__init__.py +9 -0
  4. django_cfg/modules/django_unfold/callbacks/actions.py +50 -0
  5. django_cfg/modules/django_unfold/callbacks/base.py +98 -0
  6. django_cfg/modules/django_unfold/callbacks/charts.py +224 -0
  7. django_cfg/modules/django_unfold/callbacks/commands.py +40 -0
  8. django_cfg/modules/django_unfold/callbacks/main.py +191 -0
  9. django_cfg/modules/django_unfold/callbacks/revolution.py +76 -0
  10. django_cfg/modules/django_unfold/callbacks/statistics.py +240 -0
  11. django_cfg/modules/django_unfold/callbacks/system.py +180 -0
  12. django_cfg/modules/django_unfold/callbacks/users.py +65 -0
  13. django_cfg/modules/django_unfold/models/config.py +10 -3
  14. django_cfg/modules/django_unfold/tailwind.py +68 -0
  15. django_cfg/templates/admin/components/action_grid.html +49 -0
  16. django_cfg/templates/admin/components/card.html +50 -0
  17. django_cfg/templates/admin/components/data_table.html +67 -0
  18. django_cfg/templates/admin/components/metric_card.html +39 -0
  19. django_cfg/templates/admin/components/modal.html +58 -0
  20. django_cfg/templates/admin/components/progress_bar.html +25 -0
  21. django_cfg/templates/admin/components/section_header.html +26 -0
  22. django_cfg/templates/admin/components/stat_item.html +32 -0
  23. django_cfg/templates/admin/components/stats_grid.html +72 -0
  24. django_cfg/templates/admin/components/status_badge.html +28 -0
  25. django_cfg/templates/admin/components/user_avatar.html +27 -0
  26. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +7 -7
  27. django_cfg/templates/admin/snippets/components/activity_tracker.html +48 -11
  28. django_cfg/templates/admin/snippets/components/charts_section.html +63 -13
  29. django_cfg/templates/admin/snippets/components/django_commands.html +18 -18
  30. django_cfg/templates/admin/snippets/components/quick_actions.html +3 -47
  31. django_cfg/templates/admin/snippets/components/recent_activity.html +28 -38
  32. django_cfg/templates/admin/snippets/components/recent_users_table.html +22 -53
  33. django_cfg/templates/admin/snippets/components/stats_cards.html +2 -66
  34. django_cfg/templates/admin/snippets/components/system_health.html +13 -63
  35. django_cfg/templates/admin/snippets/components/system_metrics.html +8 -25
  36. django_cfg/templates/admin/snippets/tabs/commands_tab.html +1 -1
  37. django_cfg/templates/admin/snippets/tabs/overview_tab.html +4 -4
  38. django_cfg/templates/admin/snippets/zones/zones_table.html +12 -33
  39. django_cfg/templatetags/django_cfg.py +2 -1
  40. {django_cfg-1.2.7.dist-info → django_cfg-1.2.8.dist-info}/METADATA +2 -1
  41. {django_cfg-1.2.7.dist-info → django_cfg-1.2.8.dist-info}/RECORD +44 -24
  42. django_cfg/modules/django_unfold/callbacks.py +0 -795
  43. {django_cfg-1.2.7.dist-info → django_cfg-1.2.8.dist-info}/WHEEL +0 -0
  44. {django_cfg-1.2.7.dist-info → django_cfg-1.2.8.dist-info}/entry_points.txt +0 -0
  45. {django_cfg-1.2.7.dist-info → django_cfg-1.2.8.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,76 @@
1
+ """
2
+ Django Revolution integration callbacks.
3
+ """
4
+
5
+ import logging
6
+ from typing import Dict, Any, List, Tuple
7
+
8
+ from django.conf import settings
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class RevolutionCallbacks:
14
+ """Django Revolution integration callbacks."""
15
+
16
+ def get_revolution_zones_data(self) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
17
+ """Get Django Revolution zones data."""
18
+ try:
19
+ # Try to get revolution config from Django settings
20
+ revolution_config = getattr(settings, "DJANGO_REVOLUTION", {})
21
+ zones = revolution_config.get("zones", {})
22
+ api_prefix = revolution_config.get("api_prefix", "apix")
23
+
24
+ zones_data = []
25
+ total_apps = 0
26
+ total_endpoints = 0
27
+
28
+ for zone_name, zone_config in zones.items():
29
+ # Handle both dict and object access
30
+ if isinstance(zone_config, dict):
31
+ title = zone_config.get("title", zone_name.title())
32
+ description = zone_config.get("description", f"{zone_name} zone")
33
+ apps = zone_config.get("apps", [])
34
+ public = zone_config.get("public", False)
35
+ auth_required = zone_config.get("auth_required", True)
36
+ else:
37
+ # Handle object access (for ZoneConfig instances)
38
+ title = getattr(zone_config, "title", zone_name.title())
39
+ description = getattr(zone_config, "description", f"{zone_name} zone")
40
+ apps = getattr(zone_config, "apps", [])
41
+ public = getattr(zone_config, "public", False)
42
+ auth_required = getattr(zone_config, "auth_required", True)
43
+
44
+ # Count actual endpoints by checking URL patterns (simplified estimate)
45
+ endpoint_count = len(apps) * 3 # Conservative estimate
46
+
47
+ zones_data.append({
48
+ "name": zone_name,
49
+ "title": title,
50
+ "description": description,
51
+ "app_count": len(apps),
52
+ "endpoint_count": endpoint_count,
53
+ "status": "active",
54
+ "public": public,
55
+ "auth_required": auth_required,
56
+ "schema_url": f"/schema/{zone_name}/schema/",
57
+ "swagger_url": f"/schema/{zone_name}/schema/swagger/",
58
+ "redoc_url": f"/schema/{zone_name}/redoc/",
59
+ "api_url": f"/{api_prefix}/{zone_name}/",
60
+ })
61
+
62
+ total_apps += len(apps)
63
+ total_endpoints += endpoint_count
64
+
65
+ return zones_data, {
66
+ "total_apps": total_apps,
67
+ "total_endpoints": total_endpoints,
68
+ "total_zones": len(zones),
69
+ }
70
+ except Exception as e:
71
+ logger.error(f"Error getting revolution zones: {e}")
72
+ return [], {
73
+ "total_apps": 0,
74
+ "total_endpoints": 0,
75
+ "total_zones": 0,
76
+ }
@@ -0,0 +1,240 @@
1
+ """
2
+ Statistics callbacks for dashboard.
3
+ """
4
+
5
+ import logging
6
+ from typing import List, Dict, Any
7
+ from datetime import timedelta
8
+
9
+ from django.db.models import Count
10
+ from django.utils import timezone
11
+ from django.contrib.auth import get_user_model
12
+ from django.apps import apps
13
+
14
+ from ..models.dashboard import StatCard
15
+ from ..icons import Icons
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class StatisticsCallbacks:
21
+ """Statistics-related callbacks."""
22
+
23
+ def _get_user_model(self):
24
+ """Get the user model safely."""
25
+ return get_user_model()
26
+
27
+ def get_user_statistics(self) -> List[StatCard]:
28
+ """Get user-related statistics as Pydantic models."""
29
+ try:
30
+ User = self._get_user_model()
31
+
32
+ total_users = User.objects.count()
33
+ active_users = User.objects.filter(is_active=True).count()
34
+ new_users_7d = User.objects.filter(
35
+ date_joined__gte=timezone.now() - timedelta(days=7)
36
+ ).count()
37
+ staff_users = User.objects.filter(is_staff=True).count()
38
+
39
+ return [
40
+ StatCard(
41
+ title="Total Users",
42
+ value=f"{total_users:,}",
43
+ icon=Icons.PEOPLE,
44
+ change=f"+{new_users_7d}" if new_users_7d > 0 else None,
45
+ change_type="positive" if new_users_7d > 0 else "neutral",
46
+ description="Registered users",
47
+ ),
48
+ StatCard(
49
+ title="Active Users",
50
+ value=f"{active_users:,}",
51
+ icon=Icons.PERSON,
52
+ change=(
53
+ f"{(active_users/total_users*100):.1f}%"
54
+ if total_users > 0
55
+ else "0%"
56
+ ),
57
+ change_type=(
58
+ "positive" if active_users > total_users * 0.7 else "neutral"
59
+ ),
60
+ description="Currently active",
61
+ ),
62
+ StatCard(
63
+ title="New This Week",
64
+ value=f"{new_users_7d:,}",
65
+ icon=Icons.PERSON_ADD,
66
+ change_type="positive" if new_users_7d > 0 else "neutral",
67
+ description="Last 7 days",
68
+ ),
69
+ StatCard(
70
+ title="Staff Members",
71
+ value=f"{staff_users:,}",
72
+ icon=Icons.ADMIN_PANEL_SETTINGS,
73
+ change=(
74
+ f"{(staff_users/total_users*100):.1f}%" if total_users > 0 else "0%"
75
+ ),
76
+ change_type="neutral",
77
+ description="Administrative access",
78
+ ),
79
+ ]
80
+ except Exception as e:
81
+ logger.error(f"Error getting user statistics: {e}")
82
+ return [
83
+ StatCard(
84
+ title="Users",
85
+ value="N/A",
86
+ icon=Icons.PEOPLE,
87
+ description="Data unavailable",
88
+ )
89
+ ]
90
+
91
+ def get_support_statistics(self) -> List[StatCard]:
92
+ """Get support ticket statistics as Pydantic models."""
93
+ try:
94
+ # Check if support is enabled
95
+ if not self.is_support_enabled():
96
+ return []
97
+
98
+ from django_cfg.apps.support.models import Ticket
99
+
100
+ total_tickets = Ticket.objects.count()
101
+ open_tickets = Ticket.objects.filter(status='open').count()
102
+ resolved_tickets = Ticket.objects.filter(status='resolved').count()
103
+ new_tickets_7d = Ticket.objects.filter(
104
+ created_at__gte=timezone.now() - timedelta(days=7)
105
+ ).count()
106
+
107
+ return [
108
+ StatCard(
109
+ title="Total Tickets",
110
+ value=f"{total_tickets:,}",
111
+ icon=Icons.SUPPORT_AGENT,
112
+ change=f"+{new_tickets_7d}" if new_tickets_7d > 0 else None,
113
+ change_type="positive" if new_tickets_7d > 0 else "neutral",
114
+ description="All support tickets",
115
+ ),
116
+ StatCard(
117
+ title="Open Tickets",
118
+ value=f"{open_tickets:,}",
119
+ icon=Icons.PENDING,
120
+ change=(
121
+ f"{(open_tickets/total_tickets*100):.1f}%"
122
+ if total_tickets > 0
123
+ else "0%"
124
+ ),
125
+ change_type=(
126
+ "negative" if open_tickets > total_tickets * 0.3
127
+ else "positive" if open_tickets == 0
128
+ else "neutral"
129
+ ),
130
+ description="Awaiting response",
131
+ ),
132
+ StatCard(
133
+ title="Resolved",
134
+ value=f"{resolved_tickets:,}",
135
+ icon=Icons.CHECK_CIRCLE,
136
+ change=(
137
+ f"{(resolved_tickets/total_tickets*100):.1f}%"
138
+ if total_tickets > 0
139
+ else "0%"
140
+ ),
141
+ change_type="positive",
142
+ description="Successfully resolved",
143
+ ),
144
+ StatCard(
145
+ title="New This Week",
146
+ value=f"{new_tickets_7d:,}",
147
+ icon=Icons.NEW_RELEASES,
148
+ change_type="positive" if new_tickets_7d > 0 else "neutral",
149
+ description="Last 7 days",
150
+ ),
151
+ ]
152
+ except Exception as e:
153
+ logger.error(f"Error getting support statistics: {e}")
154
+ return [
155
+ StatCard(
156
+ title="Support",
157
+ value="N/A",
158
+ icon=Icons.SUPPORT_AGENT,
159
+ description="Data unavailable",
160
+ )
161
+ ]
162
+
163
+ def get_app_statistics(self) -> Dict[str, Any]:
164
+ """Get statistics for all apps and their models."""
165
+ stats = {"apps": {}, "total_records": 0, "total_models": 0, "total_apps": 0}
166
+
167
+ # Get all installed apps
168
+ for app_config in apps.get_app_configs():
169
+ app_label = app_config.label
170
+
171
+ # Skip system apps
172
+ if app_label in ["admin", "contenttypes", "sessions", "auth"]:
173
+ continue
174
+
175
+ app_stats = self._get_app_stats(app_label)
176
+ if app_stats:
177
+ stats["apps"][app_label] = app_stats
178
+ stats["total_records"] += app_stats.get("total_records", 0)
179
+ stats["total_models"] += app_stats.get("model_count", 0)
180
+ stats["total_apps"] += 1
181
+
182
+ return stats
183
+
184
+ def _get_app_stats(self, app_label: str) -> Dict[str, Any]:
185
+ """Get statistics for a specific app."""
186
+ try:
187
+ app_config = apps.get_app_config(app_label)
188
+ # Convert generator to list to avoid len() error
189
+ models_list = list(app_config.get_models())
190
+
191
+ if not models_list:
192
+ return None
193
+
194
+ app_stats = {
195
+ "name": app_config.verbose_name or app_label.title(),
196
+ "models": {},
197
+ "total_records": 0,
198
+ "model_count": len(models_list),
199
+ }
200
+
201
+ for model in models_list:
202
+ try:
203
+ # Get model statistics
204
+ model_stats = self._get_model_stats(model)
205
+ if model_stats:
206
+ app_stats["models"][model._meta.model_name] = model_stats
207
+ app_stats["total_records"] += model_stats.get("count", 0)
208
+ except Exception:
209
+ continue
210
+
211
+ return app_stats
212
+
213
+ except Exception:
214
+ return None
215
+
216
+ def _get_model_stats(self, model) -> Dict[str, Any]:
217
+ """Get statistics for a specific model."""
218
+ try:
219
+ # Get basic model info
220
+ model_stats = {
221
+ "name": model._meta.verbose_name_plural
222
+ or model._meta.verbose_name
223
+ or model._meta.model_name,
224
+ "count": model.objects.count(),
225
+ "fields_count": len(model._meta.fields),
226
+ "admin_url": f"admin:{model._meta.app_label}_{model._meta.model_name}_changelist",
227
+ }
228
+
229
+ return model_stats
230
+
231
+ except Exception:
232
+ return None
233
+
234
+ def is_support_enabled(self) -> bool:
235
+ """Check if support module is enabled."""
236
+ try:
237
+ from django_cfg.apps.support.models import Ticket
238
+ return True
239
+ except ImportError:
240
+ return False
@@ -0,0 +1,180 @@
1
+ """
2
+ System health and metrics callbacks.
3
+ """
4
+
5
+ import logging
6
+ import shutil
7
+ from typing import List, Dict, Any
8
+
9
+ from django.utils import timezone
10
+ from django.db import connection
11
+ from django.core.cache import cache
12
+
13
+ from ..models.dashboard import SystemHealthItem
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class SystemCallbacks:
19
+ """System health and metrics callbacks."""
20
+
21
+ def get_system_health(self) -> List[SystemHealthItem]:
22
+ """Get system health status as Pydantic models."""
23
+ health_items = []
24
+
25
+ # Database health
26
+ try:
27
+ with connection.cursor() as cursor:
28
+ cursor.execute("SELECT 1")
29
+ health_items.append(
30
+ SystemHealthItem(
31
+ component="database",
32
+ status="healthy",
33
+ description="Connection successful",
34
+ last_check=timezone.now().strftime("%H:%M:%S"),
35
+ health_percentage=95,
36
+ )
37
+ )
38
+ except Exception as e:
39
+ health_items.append(
40
+ SystemHealthItem(
41
+ component="database",
42
+ status="error",
43
+ description=f"Connection failed: {str(e)[:50]}",
44
+ last_check=timezone.now().strftime("%H:%M:%S"),
45
+ health_percentage=0,
46
+ )
47
+ )
48
+
49
+ # Cache health
50
+ try:
51
+ cache.set("health_check", "ok", 10)
52
+ if cache.get("health_check") == "ok":
53
+ health_items.append(
54
+ SystemHealthItem(
55
+ component="cache",
56
+ status="healthy",
57
+ description="Cache operational",
58
+ last_check=timezone.now().strftime("%H:%M:%S"),
59
+ health_percentage=90,
60
+ )
61
+ )
62
+ else:
63
+ health_items.append(
64
+ SystemHealthItem(
65
+ component="cache",
66
+ status="warning",
67
+ description="Cache not responding",
68
+ last_check=timezone.now().strftime("%H:%M:%S"),
69
+ health_percentage=50,
70
+ )
71
+ )
72
+ except Exception:
73
+ health_items.append(
74
+ SystemHealthItem(
75
+ component="cache",
76
+ status="unknown",
77
+ description="Cache not configured",
78
+ last_check=timezone.now().strftime("%H:%M:%S"),
79
+ health_percentage=0,
80
+ )
81
+ )
82
+
83
+ # Storage health
84
+ try:
85
+ total, used, free = shutil.disk_usage("/")
86
+ usage_percentage = (used / total) * 100
87
+ free_percentage = 100 - usage_percentage
88
+
89
+ if free_percentage > 20:
90
+ status = "healthy"
91
+ desc = f"Disk space: {free_percentage:.1f}% free"
92
+ elif free_percentage > 10:
93
+ status = "warning"
94
+ desc = f"Low disk space: {free_percentage:.1f}% free"
95
+ else:
96
+ status = "error"
97
+ desc = f"Critical disk space: {free_percentage:.1f}% free"
98
+
99
+ health_items.append(
100
+ SystemHealthItem(
101
+ component="storage",
102
+ status=status,
103
+ description=desc,
104
+ last_check=timezone.now().strftime("%H:%M:%S"),
105
+ health_percentage=int(free_percentage),
106
+ )
107
+ )
108
+ except Exception as e:
109
+ health_items.append(
110
+ SystemHealthItem(
111
+ component="storage",
112
+ status="error",
113
+ description="Storage check failed",
114
+ last_check=timezone.now().strftime("%H:%M:%S"),
115
+ health_percentage=0,
116
+ )
117
+ )
118
+
119
+ # API health
120
+ health_items.append(
121
+ SystemHealthItem(
122
+ component="api",
123
+ status="healthy",
124
+ description="API server running",
125
+ last_check=timezone.now().strftime("%H:%M:%S"),
126
+ health_percentage=100,
127
+ )
128
+ )
129
+
130
+ return health_items
131
+
132
+ def get_system_metrics(self) -> Dict[str, Any]:
133
+ """Get system metrics for dashboard."""
134
+ metrics = {}
135
+
136
+ # Database metrics
137
+ try:
138
+ with connection.cursor() as cursor:
139
+ cursor.execute("SELECT 1")
140
+ metrics["database"] = {
141
+ "status": "healthy",
142
+ "type": "PostgreSQL",
143
+ "health_percentage": 95,
144
+ "description": "Connection successful",
145
+ }
146
+ except Exception as e:
147
+ metrics["database"] = {
148
+ "status": "error",
149
+ "type": "PostgreSQL",
150
+ "health_percentage": 0,
151
+ "description": f"Connection failed: {str(e)}",
152
+ }
153
+
154
+ # Cache metrics
155
+ try:
156
+ cache.set("health_check", "ok", 10)
157
+ cache_result = cache.get("health_check")
158
+ if cache_result == "ok":
159
+ metrics["cache"] = {
160
+ "status": "healthy",
161
+ "type": "Memory Cache",
162
+ "health_percentage": 90,
163
+ "description": "Cache working properly",
164
+ }
165
+ else:
166
+ metrics["cache"] = {
167
+ "status": "warning",
168
+ "type": "Memory Cache",
169
+ "health_percentage": 50,
170
+ "description": "Cache response delayed",
171
+ }
172
+ except Exception as e:
173
+ metrics["cache"] = {
174
+ "status": "error",
175
+ "type": "Memory Cache",
176
+ "health_percentage": 0,
177
+ "description": f"Cache error: {str(e)}",
178
+ }
179
+
180
+ return metrics
@@ -0,0 +1,65 @@
1
+ """
2
+ Users data callbacks.
3
+ """
4
+
5
+ import logging
6
+ from typing import List, Dict, Any
7
+
8
+ from django.contrib.auth import get_user_model
9
+
10
+ from .base import get_user_admin_urls
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class UsersCallbacks:
16
+ """Users data callbacks."""
17
+
18
+ def _get_user_model(self):
19
+ """Get the user model safely."""
20
+ return get_user_model()
21
+
22
+ def get_recent_users(self) -> List[Dict[str, Any]]:
23
+ """Get recent users data for template."""
24
+ try:
25
+ # Avoid database access during app initialization
26
+ from django.apps import apps
27
+ if not apps.ready:
28
+ return []
29
+
30
+ User = self._get_user_model()
31
+ recent_users = User.objects.select_related().order_by("-date_joined")[:10]
32
+
33
+ # Get admin URLs for user model
34
+ user_admin_urls = get_user_admin_urls()
35
+
36
+ return [
37
+ {
38
+ "id": user.id,
39
+ "username": user.username,
40
+ "email": user.email or "No email",
41
+ "date_joined": (
42
+ user.date_joined.strftime("%Y-%m-%d")
43
+ if user.date_joined
44
+ else "Unknown"
45
+ ),
46
+ "is_active": user.is_active,
47
+ "is_staff": user.is_staff,
48
+ "is_superuser": user.is_superuser,
49
+ "last_login": user.last_login,
50
+ "admin_urls": {
51
+ "change": (
52
+ user_admin_urls["change"].format(id=user.id)
53
+ if user.id
54
+ else None
55
+ ),
56
+ "view": (
57
+ user_admin_urls["view"].format(id=user.id) if user.id else None
58
+ ),
59
+ },
60
+ }
61
+ for user in recent_users
62
+ ]
63
+ except Exception as e:
64
+ logger.error(f"Error getting recent users: {e}")
65
+ return []
@@ -467,21 +467,28 @@ class UnfoldConfig(BaseModel):
467
467
  if self.tab_configurations:
468
468
  unfold_settings["TABS"] = self.tab_configurations
469
469
 
470
- # Command interface
470
+ # Command interface - Enhanced for better UX
471
471
  unfold_settings["COMMAND"] = {
472
472
  "search_models": True,
473
473
  "show_history": True,
474
+ "search_callback": None, # Can be customized per project
474
475
  }
475
476
 
476
- # Inject universal CSS variables (includes object-tools flex)
477
+ # Inject universal CSS variables and custom styles
477
478
  if "STYLES" not in unfold_settings:
478
479
  unfold_settings["STYLES"] = []
479
480
 
480
481
  # Add our CSS as inline data URI
481
482
  try:
482
- from ..tailwind import get_css_variables
483
+ from ..tailwind import get_css_variables, get_modal_fix_css
483
484
  import base64
485
+
486
+ # Base CSS variables
484
487
  css_content = get_css_variables()
488
+
489
+ # Add modal scroll fix CSS
490
+ css_content += get_modal_fix_css()
491
+
485
492
  css_b64 = base64.b64encode(css_content.encode('utf-8')).decode('utf-8')
486
493
  data_uri = f"data:text/css;base64,{css_b64}"
487
494
  unfold_settings["STYLES"].append(lambda request: data_uri)
@@ -198,6 +198,74 @@ def get_css_variables() -> str:
198
198
  """
199
199
 
200
200
 
201
+ def get_modal_fix_css() -> str:
202
+ """
203
+ Get CSS fixes for modal scroll issues and other UI improvements.
204
+
205
+ Returns:
206
+ str: CSS fixes as string
207
+ """
208
+ return """
209
+ /* Modal scroll fixes and UI improvements */
210
+
211
+ /* Ensure proper modal scroll behavior */
212
+ .modal-scrollable {
213
+ max-height: calc(80vh - 8rem);
214
+ overflow-y: auto;
215
+ }
216
+
217
+ /* Command modal specific fixes */
218
+ #commandModal .overflow-y-auto {
219
+ scrollbar-width: thin;
220
+ scrollbar-color: rgb(156, 163, 175) transparent;
221
+ }
222
+
223
+ #commandModal .overflow-y-auto::-webkit-scrollbar {
224
+ width: 8px;
225
+ }
226
+
227
+ #commandModal .overflow-y-auto::-webkit-scrollbar-track {
228
+ background: transparent;
229
+ }
230
+
231
+ #commandModal .overflow-y-auto::-webkit-scrollbar-thumb {
232
+ background-color: rgb(156, 163, 175);
233
+ border-radius: 4px;
234
+ }
235
+
236
+ #commandModal .overflow-y-auto::-webkit-scrollbar-thumb:hover {
237
+ background-color: rgb(107, 114, 128);
238
+ }
239
+
240
+ /* Dark theme scrollbar */
241
+ .dark #commandModal .overflow-y-auto {
242
+ scrollbar-color: rgb(75, 85, 99) transparent;
243
+ }
244
+
245
+ .dark #commandModal .overflow-y-auto::-webkit-scrollbar-thumb {
246
+ background-color: rgb(75, 85, 99);
247
+ }
248
+
249
+ .dark #commandModal .overflow-y-auto::-webkit-scrollbar-thumb:hover {
250
+ background-color: rgb(107, 114, 128);
251
+ }
252
+
253
+ /* Improved focus states */
254
+ .focus-visible\\:outline-primary-600:focus-visible {
255
+ outline: 2px solid rgb(37, 99, 235);
256
+ outline-offset: 2px;
257
+ }
258
+
259
+ /* Better button transitions */
260
+ .transition-colors {
261
+ transition-property: color, background-color, border-color;
262
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
263
+ transition-duration: 150ms;
264
+ }
265
+
266
+ """
267
+
268
+
201
269
  def get_unfold_colors() -> Dict[str, Any]:
202
270
  """
203
271
  Get color configuration for Unfold settings.