django-cfg 1.4.75__py3-none-any.whl → 1.4.77__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.

Potentially problematic release.


This version of django-cfg might be problematic. Click here for more details.

Files changed (57) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/agents/__init__.py +1 -1
  3. django_cfg/apps/agents/integration/registry.py +1 -1
  4. django_cfg/apps/agents/patterns/content_agents.py +1 -1
  5. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/testing_tools.html +1 -1
  6. django_cfg/apps/centrifugo/views/dashboard.py +13 -0
  7. django_cfg/apps/centrifugo/views/testing_api.py +74 -15
  8. django_cfg/apps/tasks/views/dashboard.py +4 -4
  9. django_cfg/core/generation/integration_generators/api.py +5 -2
  10. django_cfg/management/commands/check_endpoints.py +1 -1
  11. django_cfg/modules/django_client/core/generator/python/models_generator.py +6 -6
  12. django_cfg/modules/django_client/core/generator/python/templates/client/main_client.py.jinja +0 -2
  13. django_cfg/modules/django_client/core/generator/python/templates/client/sync_main_client.py.jinja +0 -1
  14. django_cfg/modules/django_client/core/generator/python/templates/main_init.py.jinja +2 -1
  15. django_cfg/modules/django_client/core/generator/python/templates/models/enums.py.jinja +7 -1
  16. django_cfg/modules/django_tailwind/templates/django_tailwind/components/navbar.html +2 -2
  17. django_cfg/modules/django_unfold/callbacks/main.py +27 -25
  18. django_cfg/pyproject.toml +1 -1
  19. django_cfg/static/admin/css/constance.css +44 -0
  20. django_cfg/static/admin/css/dashboard.css +6 -170
  21. django_cfg/static/admin/css/layout.css +21 -0
  22. django_cfg/static/admin/css/tabs.css +95 -0
  23. django_cfg/static/admin/css/theme.css +74 -0
  24. django_cfg/static/admin/js/alpine/activity-tracker.js +55 -0
  25. django_cfg/static/admin/js/alpine/chart.js +101 -0
  26. django_cfg/static/admin/js/alpine/command-modal.js +159 -0
  27. django_cfg/static/admin/js/alpine/commands-panel.js +139 -0
  28. django_cfg/static/admin/js/alpine/commands-section.js +260 -0
  29. django_cfg/static/admin/js/alpine/dashboard-tabs.js +46 -0
  30. django_cfg/static/admin/js/alpine/system-metrics.js +20 -0
  31. django_cfg/static/admin/js/alpine/toggle-section.js +28 -0
  32. django_cfg/static/admin/js/utils.js +60 -0
  33. django_cfg/templates/admin/components/modal.html +1 -1
  34. django_cfg/templates/admin/constance/change_list.html +3 -42
  35. django_cfg/templates/admin/index.html +0 -8
  36. django_cfg/templates/admin/layouts/base_dashboard.html +4 -2
  37. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +104 -502
  38. django_cfg/templates/admin/sections/commands_section.html +374 -451
  39. django_cfg/templates/admin/sections/documentation_section.html +13 -33
  40. django_cfg/templates/admin/snippets/components/activity_tracker.html +27 -49
  41. django_cfg/templates/admin/snippets/components/charts_section.html +8 -74
  42. django_cfg/templates/admin/snippets/components/django_commands.html +94 -181
  43. django_cfg/templates/admin/snippets/components/system_metrics.html +18 -10
  44. django_cfg/templates/admin/snippets/tabs/app_stats_tab.html +2 -2
  45. django_cfg/templates/admin/snippets/tabs/commands_tab.html +48 -0
  46. django_cfg/templates/admin/snippets/tabs/documentation_tab.html +1 -190
  47. django_cfg/templates/admin/snippets/tabs/overview_tab.html +1 -1
  48. {django_cfg-1.4.75.dist-info → django_cfg-1.4.77.dist-info}/METADATA +1 -1
  49. {django_cfg-1.4.75.dist-info → django_cfg-1.4.77.dist-info}/RECORD +52 -44
  50. django_cfg/static/admin/js/commands.js +0 -171
  51. django_cfg/static/admin/js/dashboard.js +0 -126
  52. django_cfg/templates/admin/components/management_commands.js +0 -375
  53. django_cfg/templates/admin/snippets/components/CHARTS_GUIDE.md +0 -322
  54. django_cfg/templates/admin/snippets/components/recent_activity.html +0 -35
  55. {django_cfg-1.4.75.dist-info → django_cfg-1.4.77.dist-info}/WHEEL +0 -0
  56. {django_cfg-1.4.75.dist-info → django_cfg-1.4.77.dist-info}/entry_points.txt +0 -0
  57. {django_cfg-1.4.75.dist-info → django_cfg-1.4.77.dist-info}/licenses/LICENSE +0 -0
django_cfg/__init__.py CHANGED
@@ -32,7 +32,7 @@ Example:
32
32
  default_app_config = "django_cfg.apps.DjangoCfgConfig"
33
33
 
34
34
  # Version information
35
- __version__ = "1.4.75"
35
+ __version__ = "1.4.77"
36
36
  __license__ = "MIT"
37
37
 
38
38
  # Import registry for organized lazy loading
@@ -32,7 +32,7 @@ __all__ = [
32
32
  def __getattr__(name):
33
33
  """Lazy import for agents components."""
34
34
  if name == "DjangoAgent":
35
- from .core.agent import DjangoAgent
35
+ from .core.django_agent import DjangoAgent
36
36
  return DjangoAgent
37
37
  elif name == "SimpleOrchestrator":
38
38
  from .core.orchestrator import SimpleOrchestrator
@@ -7,7 +7,7 @@ from typing import Any, Dict, List, Optional, Type
7
7
 
8
8
  from django.contrib.auth.models import User
9
9
 
10
- from ..core.agent import DjangoAgent
10
+ from ..core.django_agent import DjangoAgent
11
11
  from ..core.dependencies import DjangoDeps
12
12
  from ..core.orchestrator import SimpleOrchestrator
13
13
  from ..models.registry import AgentDefinition
@@ -6,7 +6,7 @@ from typing import Any, Dict, List
6
6
 
7
7
  from pydantic import BaseModel, Field
8
8
 
9
- from ..core.agent import DjangoAgent
9
+ from ..core.django_agent import DjangoAgent
10
10
  from ..core.dependencies import ContentDeps, RunContext
11
11
  from ..core.models import ValidationResult
12
12
 
@@ -289,7 +289,7 @@
289
289
  parseInt(document.getElementById('pub-ack-timeout').value)
290
290
  ).then(result => {
291
291
  if (result.success) {
292
- alert('Message published successfully!\\nMessage ID: ' + result.message_id + '\\nDelivered: ' + result.delivered + '\\nACKs: ' + result.acks_received);
292
+ alert('Message published successfully!\nMessage ID: ' + result.message_id + '\nDelivered: ' + result.delivered + '\nACKs: ' + result.acks_received);
293
293
  } else {
294
294
  alert('Failed to publish message: ' + result.error);
295
295
  }
@@ -4,12 +4,25 @@ Dashboard view for Centrifugo monitoring.
4
4
 
5
5
  from django.contrib.admin.views.decorators import staff_member_required
6
6
  from django.shortcuts import render
7
+ from django.urls import reverse
7
8
 
8
9
 
9
10
  @staff_member_required
10
11
  def dashboard_view(request):
11
12
  """Render the Centrifugo dashboard template."""
13
+
14
+ # Navigation items for the navbar
15
+ nav_items = [
16
+ {
17
+ 'label': 'Logs',
18
+ 'url': reverse('admin:django_cfg_centrifugo_centrifugolog_changelist'),
19
+ 'icon': 'list_alt',
20
+ 'active': False,
21
+ },
22
+ ]
23
+
12
24
  context = {
13
25
  'page_title': 'Centrifugo Monitor Dashboard',
26
+ 'centrifugo_nav_items': nav_items,
14
27
  }
15
28
  return render(request, 'django_cfg_centrifugo/pages/dashboard.html', context)
@@ -119,10 +119,11 @@ class CentrifugoTestingAPIViewSet(viewsets.ViewSet):
119
119
  raise ValueError("Centrifugo not configured")
120
120
 
121
121
  headers = {"Content-Type": "application/json"}
122
- if config.wrapper_api_key:
123
- headers["X-API-Key"] = config.wrapper_api_key
122
+ # Use centrifugo_api_key for direct Centrifugo API calls
123
+ if config.centrifugo_api_key:
124
+ headers["X-API-Key"] = config.centrifugo_api_key
124
125
 
125
- # Use wrapper URL as base
126
+ # Use wrapper URL as base (which points to Centrifugo in local setup)
126
127
  base_url = config.wrapper_url.rstrip("/")
127
128
 
128
129
  self._http_client = httpx.AsyncClient(
@@ -300,7 +301,7 @@ class CentrifugoTestingAPIViewSet(viewsets.ViewSet):
300
301
  self, channel: str, data: Dict[str, Any], wait_for_ack: bool, ack_timeout: int
301
302
  ) -> Dict[str, Any]:
302
303
  """
303
- Publish message to wrapper API.
304
+ Publish message to Centrifugo API.
304
305
 
305
306
  Args:
306
307
  channel: Target channel
@@ -309,20 +310,78 @@ class CentrifugoTestingAPIViewSet(viewsets.ViewSet):
309
310
  ack_timeout: ACK timeout in seconds
310
311
 
311
312
  Returns:
312
- Wrapper API response
313
+ Centrifugo API response formatted for wrapper compatibility
313
314
  """
314
- payload = {
315
- "channel": channel,
316
- "data": data,
317
- "wait_for_ack": wait_for_ack,
318
- }
315
+ import uuid
316
+ import time
317
+ from ..services.logging import CentrifugoLogger
318
+
319
+ # Generate unique message ID
320
+ message_id = str(uuid.uuid4())
321
+ start_time = time.time()
322
+
323
+ # Create log entry
324
+ log_entry = await CentrifugoLogger.create_log_async(
325
+ message_id=message_id,
326
+ channel=channel,
327
+ data=data,
328
+ wait_for_ack=wait_for_ack,
329
+ ack_timeout=ack_timeout if wait_for_ack else None,
330
+ is_notification=True,
331
+ user=self.request.user if self.request and self.request.user.is_authenticated else None,
332
+ )
333
+
334
+ try:
335
+ # Centrifugo API format: POST /api with method in body
336
+ payload = {
337
+ "method": "publish",
338
+ "params": {
339
+ "channel": channel,
340
+ "data": data,
341
+ }
342
+ }
319
343
 
320
- if wait_for_ack:
321
- payload["ack_timeout"] = ack_timeout
344
+ response = await self.http_client.post("/api", json=payload)
345
+ response.raise_for_status()
346
+ result = response.json()
322
347
 
323
- response = await self.http_client.post("/api/publish", json=payload)
324
- response.raise_for_status()
325
- return response.json()
348
+ # Calculate duration
349
+ duration_ms = int((time.time() - start_time) * 1000)
350
+
351
+ # Mark as success
352
+ if log_entry:
353
+ await CentrifugoLogger.mark_success_async(
354
+ log_entry,
355
+ acks_received=0,
356
+ duration_ms=duration_ms,
357
+ )
358
+
359
+ # Transform Centrifugo response to match wrapper API format
360
+ return {
361
+ "published": True,
362
+ "message_id": message_id,
363
+ "channel": channel,
364
+ "acks_received": 0,
365
+ "delivered": True,
366
+ }
367
+
368
+ except Exception as e:
369
+ # Calculate duration
370
+ duration_ms = int((time.time() - start_time) * 1000)
371
+
372
+ # Mark as failed
373
+ if log_entry:
374
+ from asgiref.sync import sync_to_async
375
+ from ..models import CentrifugoLog
376
+
377
+ await sync_to_async(CentrifugoLog.objects.mark_failed)(
378
+ log_instance=log_entry,
379
+ error_code=type(e).__name__,
380
+ error_message=str(e),
381
+ duration_ms=duration_ms,
382
+ )
383
+
384
+ raise
326
385
 
327
386
  async def _send_ack_to_wrapper(
328
387
  self, message_id: str, client_id: str
@@ -33,12 +33,12 @@ def dashboard_view(request):
33
33
  {
34
34
  'label': 'Task History',
35
35
  'url': '/admin/django_dramatiq/task/',
36
- 'icon': '📜',
36
+ 'icon': 'history',
37
37
  },
38
38
  {
39
39
  'label': 'Settings',
40
40
  'url': '/admin/constance/config/',
41
- 'icon': '⚙️',
41
+ 'icon': 'settings',
42
42
  },
43
43
  ]
44
44
 
@@ -59,12 +59,12 @@ def dashboard_view(request):
59
59
  {
60
60
  'label': 'Task History',
61
61
  'url': '/admin/django_dramatiq/task/',
62
- 'icon': '📜',
62
+ 'icon': 'history',
63
63
  },
64
64
  {
65
65
  'label': 'Settings',
66
66
  'url': '/admin/constance/config/',
67
- 'icon': '⚙️',
67
+ 'icon': 'settings',
68
68
  },
69
69
  ]
70
70
 
@@ -220,9 +220,12 @@ class APIFrameworksGenerator:
220
220
  Dictionary with Spectacular settings
221
221
  """
222
222
  # Import authentication extension to register it with drf-spectacular
223
+ # Only import if apps are ready to avoid warnings
223
224
  try:
224
- from django_cfg.middleware import authentication # noqa: F401
225
- except ImportError:
225
+ from django.apps import apps
226
+ if apps.ready:
227
+ from django_cfg.middleware import authentication # noqa: F401
228
+ except (ImportError, RuntimeError):
226
229
  pass
227
230
 
228
231
  # Check if Spectacular settings exist (from OpenAPI Client or elsewhere)
@@ -13,7 +13,7 @@ import json
13
13
  from django.core.management.base import BaseCommand
14
14
  from django.urls import reverse
15
15
 
16
- from django_cfg.apps.api.endpoints.checker import check_all_endpoints
16
+ from django_cfg.apps.api.endpoints.endpoints_status.checker import check_all_endpoints
17
17
 
18
18
 
19
19
  class Command(BaseCommand):
@@ -267,11 +267,11 @@ class ModelsGenerator:
267
267
  if schema.properties:
268
268
  for prop in schema.properties.values():
269
269
  if prop.enum and prop.name:
270
- enum_names.add(prop.name.replace(".", ""))
270
+ enum_names.add(self.base.sanitize_enum_name(prop.name))
271
271
  elif prop.ref and prop.ref in self.context.schemas:
272
272
  ref_schema = self.context.schemas[prop.ref]
273
273
  if ref_schema.enum:
274
- enum_names.add(prop.ref.replace(".", ""))
274
+ enum_names.add(self.base.sanitize_enum_name(prop.ref))
275
275
 
276
276
  # Request models
277
277
  for name, schema in schemas.items():
@@ -282,11 +282,11 @@ class ModelsGenerator:
282
282
  if schema.properties:
283
283
  for prop in schema.properties.values():
284
284
  if prop.enum and prop.name:
285
- enum_names.add(prop.name.replace(".", ""))
285
+ enum_names.add(self.base.sanitize_enum_name(prop.name))
286
286
  elif prop.ref and prop.ref in self.context.schemas:
287
287
  ref_schema = self.context.schemas[prop.ref]
288
288
  if ref_schema.enum:
289
- enum_names.add(prop.ref.replace(".", ""))
289
+ enum_names.add(self.base.sanitize_enum_name(prop.ref))
290
290
 
291
291
  # Patch models
292
292
  for name, schema in schemas.items():
@@ -297,11 +297,11 @@ class ModelsGenerator:
297
297
  if schema.properties:
298
298
  for prop in schema.properties.values():
299
299
  if prop.enum and prop.name:
300
- enum_names.add(prop.name.replace(".", ""))
300
+ enum_names.add(self.base.sanitize_enum_name(prop.name))
301
301
  elif prop.ref and prop.ref in self.context.schemas:
302
302
  ref_schema = self.context.schemas[prop.ref]
303
303
  if ref_schema.enum:
304
- enum_names.add(prop.ref.replace(".", ""))
304
+ enum_names.add(self.base.sanitize_enum_name(prop.ref))
305
305
 
306
306
  template = self.jinja_env.get_template('models/app_models.py.jinja')
307
307
  content = template.render(
@@ -36,13 +36,11 @@ class APIClient:
36
36
  self._client = RetryAsyncClient(
37
37
  base_url=self.base_url,
38
38
  retry_config=retry_config,
39
- timeout=30.0,
40
39
  **kwargs,
41
40
  )
42
41
  else:
43
42
  self._client = httpx.AsyncClient(
44
43
  base_url=self.base_url,
45
- timeout=30.0,
46
44
  **kwargs,
47
45
  )
48
46
 
@@ -25,7 +25,6 @@ class SyncAPIClient:
25
25
  self.base_url = base_url.rstrip('/')
26
26
  self._client = httpx.Client(
27
27
  base_url=self.base_url,
28
- timeout=30.0,
29
28
  **kwargs,
30
29
  )
31
30
 
@@ -38,7 +38,8 @@ from .{{ tag.slug }} import {{ tag.class_name }}
38
38
  {% endfor %}
39
39
  {% if has_enums %}
40
40
  from . import enums
41
- from .enums import {{ enum_names | join(', ') }}
41
+ # NOTE: Individual enum imports commented out due to invalid Python syntax with dotted names
42
+ # from .enums import {{ enum_names | join(', ') }}
42
43
  {% endif %}
43
44
 
44
45
  TOKEN_KEY = "auth_token"
@@ -1,4 +1,10 @@
1
- from enum import IntEnum, StrEnum
1
+ from enum import IntEnum, Enum
2
+
3
+ # Python 3.10 compatibility: StrEnum was added in Python 3.11
4
+ # Use str + Enum instead for backward compatibility
5
+ class StrEnum(str, Enum):
6
+ """String Enum for Python 3.10+ compatibility"""
7
+ pass
2
8
 
3
9
 
4
10
  {% for enum in enums %}
@@ -67,13 +67,13 @@ Custom navbar (extending):
67
67
  <nav class="hidden md:flex items-center space-x-1">
68
68
  {% for item in nav_items %}
69
69
  <a href="{{ item.url }}"
70
- class="px-3 py-2 rounded-md text-sm font-medium transition-colors
70
+ class="px-3 py-2 rounded-md text-sm font-medium transition-colors flex items-center gap-1.5
71
71
  {% if item.active %}
72
72
  bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white
73
73
  {% else %}
74
74
  text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white
75
75
  {% endif %}">
76
- {% if item.icon %}<span class="mr-1">{{ item.icon }}</span>{% endif %}
76
+ {% if item.icon %}<span class="material-icons text-sm">{{ item.icon }}</span>{% endif %}
77
77
  {{ item.label }}
78
78
  </a>
79
79
  {% endfor %}
@@ -8,10 +8,11 @@ import json
8
8
  import logging
9
9
  from typing import Any, Dict
10
10
 
11
+ from django.template.loader import render_to_string
11
12
  from django.utils import timezone
12
13
 
13
14
  from django_cfg.core.state import get_current_config
14
- from django_cfg.modules.django_dashboard.debug import save_section_render
15
+ from django_cfg.modules.django_dashboard.debug import save_dashboard_render
15
16
  from django_cfg.modules.django_dashboard.sections.commands import CommandsSection
16
17
  from django_cfg.modules.django_dashboard.sections.documentation import DocumentationSection
17
18
 
@@ -113,48 +114,41 @@ class UnfoldCallbacks(
113
114
  time_range=time_range,
114
115
  navigation=navigation
115
116
  )
116
- # Debug: save render (only in debug mode)
117
- if config and config.debug:
118
- save_section_render('overview', overview_section)
119
117
  except Exception as e:
120
118
  logger.error(f"Failed to render overview section: {e}", exc_info=True)
121
119
  overview_section = None
122
120
 
123
121
  try:
124
122
  stats_section = StatsSection(request).render()
125
- # Debug: save render (only in debug mode)
126
- if config and config.debug:
127
- save_section_render('stats', stats_section)
128
123
  except Exception as e:
129
124
  logger.error(f"Failed to render stats section: {e}", exc_info=True)
130
125
  stats_section = None
131
126
 
132
127
  try:
133
128
  system_section = SystemSection(request).render()
134
- # Debug: save render (only in debug mode)
135
- if config and config.debug:
136
- save_section_render('system', system_section)
137
129
  except Exception as e:
138
130
  logger.error(f"Failed to render system section: {e}", exc_info=True)
139
131
  system_section = None
140
132
 
141
133
  try:
142
- commands_section = CommandsSection(request).render()
143
- # Debug: save render (only in debug mode)
144
- if config and config.debug:
145
- save_section_render('commands', commands_section)
134
+ # Generate documentation content first
135
+ doc_section = DocumentationSection(request)
136
+ doc_data = doc_section.get_data_old() # Get markdown/HTML content
137
+ documentation_content = doc_data.get('documentation_content') or doc_data.get('readme_content', '')
138
+ documentation_section = doc_section.render()
146
139
  except Exception as e:
147
- logger.error(f"Failed to render commands section: {e}", exc_info=True)
148
- commands_section = None
140
+ logger.error(f"Failed to render documentation section: {e}", exc_info=True)
141
+ documentation_section = None
142
+ documentation_content = None
149
143
 
150
144
  try:
151
- documentation_section = DocumentationSection(request).render()
152
- # Debug: save render (only in debug mode)
153
- if config and config.debug:
154
- save_section_render('documentation', documentation_section)
145
+ # Render commands section with documentation content
146
+ commands_section = CommandsSection(request).render(
147
+ documentation_content=documentation_content
148
+ )
155
149
  except Exception as e:
156
- logger.error(f"Failed to render documentation section: {e}", exc_info=True)
157
- documentation_section = None
150
+ logger.error(f"Failed to render commands section: {e}", exc_info=True)
151
+ commands_section = None
158
152
 
159
153
  # Extract custom widgets from context if provided by project's dashboard_callback
160
154
  custom_widgets = context.get('custom_widgets', [])
@@ -172,9 +166,6 @@ class UnfoldCallbacks(
172
166
  custom_widgets=custom_widgets,
173
167
  custom_metrics=custom_metrics
174
168
  )
175
- # Debug: save render (only in debug mode)
176
- if config and config.debug:
177
- save_section_render('widgets', widgets_section)
178
169
  except Exception as e:
179
170
  logger.error(f"Failed to render widgets section: {e}", exc_info=True)
180
171
  widgets_section = None
@@ -202,6 +193,9 @@ class UnfoldCallbacks(
202
193
  "documentation_section": documentation_section,
203
194
  "widgets_section": widgets_section,
204
195
 
196
+ # Documentation content for commands tab
197
+ "documentation_content": documentation_content,
198
+
205
199
  # Statistics cards
206
200
  "cards": cards_data,
207
201
  "user_stats": [card.to_dict() for card in user_stats],
@@ -296,6 +290,14 @@ class UnfoldCallbacks(
296
290
  # activity_tracker_data = context.get('activity_tracker', [])
297
291
  # logger.info(f"Activity tracker data count: {len(activity_tracker_data)}")
298
292
 
293
+ # Debug: save full rendered page (only in debug mode)
294
+ if config and config.debug:
295
+ try:
296
+ full_html = render_to_string('admin/index.html', context, request)
297
+ save_dashboard_render(full_html, name='dashboard_full_page', context=context)
298
+ except Exception as e:
299
+ logger.error(f"Failed to save full dashboard render: {e}", exc_info=True)
300
+
299
301
  return context
300
302
 
301
303
  except Exception as e:
django_cfg/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "django-cfg"
7
- version = "1.4.75"
7
+ version = "1.4.77"
8
8
  description = "Django AI framework with built-in agents, type-safe Pydantic v2 configuration, and 8 enterprise apps. Replace settings.py, validate at startup, 90% less code. Production-ready AI workflows for Django."
9
9
  readme = "README.md"
10
10
  keywords = [ "django", "configuration", "pydantic", "settings", "type-safety", "pydantic-settings", "django-environ", "startup-validation", "ide-autocomplete", "ai-agents", "enterprise-django", "django-settings", "type-safe-config",]
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Constance Settings UI Styles
3
+ *
4
+ * Enhanced UI for Django Constance dynamic settings
5
+ */
6
+
7
+ /* Smooth transitions for all interactive elements */
8
+ .constance-expandable {
9
+ transition: all 0.2s ease-in-out;
10
+ }
11
+
12
+ /* Better hover states */
13
+ .constance tr:hover {
14
+ background-color: rgba(0, 0, 0, 0.02);
15
+ }
16
+
17
+ .dark .constance tr:hover {
18
+ background-color: rgba(255, 255, 255, 0.02);
19
+ }
20
+
21
+ /* Improved focus states for accessibility */
22
+ .constance a:focus,
23
+ .constance button:focus {
24
+ outline: 2px solid rgb(59, 130, 246);
25
+ outline-offset: 2px;
26
+ border-radius: 4px;
27
+ }
28
+
29
+ /* Better spacing for long text */
30
+ .constance .break-words {
31
+ word-break: break-word;
32
+ overflow-wrap: break-word;
33
+ hyphens: auto;
34
+ }
35
+
36
+ /* Smooth animations for expand/collapse */
37
+ .constance [x-show] {
38
+ transition: opacity 0.2s ease-in-out;
39
+ }
40
+
41
+ /* Icon rotation animation */
42
+ .constance .material-symbols-outlined {
43
+ transition: transform 0.3s ease-in-out;
44
+ }