django-cfg 1.4.74__py3-none-any.whl → 1.4.76__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 (54) 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/api/health/drf_views.py +27 -0
  6. django_cfg/apps/centrifugo/templates/django_cfg_centrifugo/components/testing_tools.html +1 -1
  7. django_cfg/apps/centrifugo/views/dashboard.py +13 -0
  8. django_cfg/apps/centrifugo/views/testing_api.py +74 -15
  9. django_cfg/apps/tasks/views/dashboard.py +4 -4
  10. django_cfg/core/generation/integration_generators/api.py +9 -0
  11. django_cfg/management/commands/check_endpoints.py +1 -1
  12. django_cfg/middleware/authentication.py +27 -0
  13. django_cfg/modules/django_tailwind/templates/django_tailwind/components/navbar.html +2 -2
  14. django_cfg/modules/django_unfold/callbacks/main.py +27 -25
  15. django_cfg/pyproject.toml +1 -1
  16. django_cfg/static/admin/css/constance.css +44 -0
  17. django_cfg/static/admin/css/dashboard.css +6 -170
  18. django_cfg/static/admin/css/layout.css +21 -0
  19. django_cfg/static/admin/css/tabs.css +95 -0
  20. django_cfg/static/admin/css/theme.css +74 -0
  21. django_cfg/static/admin/js/alpine/activity-tracker.js +55 -0
  22. django_cfg/static/admin/js/alpine/chart.js +101 -0
  23. django_cfg/static/admin/js/alpine/command-modal.js +159 -0
  24. django_cfg/static/admin/js/alpine/commands-panel.js +139 -0
  25. django_cfg/static/admin/js/alpine/commands-section.js +260 -0
  26. django_cfg/static/admin/js/alpine/dashboard-tabs.js +46 -0
  27. django_cfg/static/admin/js/alpine/system-metrics.js +20 -0
  28. django_cfg/static/admin/js/alpine/toggle-section.js +28 -0
  29. django_cfg/static/admin/js/utils.js +60 -0
  30. django_cfg/templates/admin/components/modal.html +1 -1
  31. django_cfg/templates/admin/constance/change_list.html +3 -42
  32. django_cfg/templates/admin/index.html +0 -8
  33. django_cfg/templates/admin/layouts/base_dashboard.html +4 -2
  34. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +104 -502
  35. django_cfg/templates/admin/sections/commands_section.html +374 -451
  36. django_cfg/templates/admin/sections/documentation_section.html +13 -33
  37. django_cfg/templates/admin/snippets/components/activity_tracker.html +27 -49
  38. django_cfg/templates/admin/snippets/components/charts_section.html +8 -74
  39. django_cfg/templates/admin/snippets/components/django_commands.html +94 -181
  40. django_cfg/templates/admin/snippets/components/system_metrics.html +18 -10
  41. django_cfg/templates/admin/snippets/tabs/app_stats_tab.html +2 -2
  42. django_cfg/templates/admin/snippets/tabs/commands_tab.html +48 -0
  43. django_cfg/templates/admin/snippets/tabs/documentation_tab.html +1 -190
  44. django_cfg/templates/admin/snippets/tabs/overview_tab.html +1 -1
  45. {django_cfg-1.4.74.dist-info → django_cfg-1.4.76.dist-info}/METADATA +1 -1
  46. {django_cfg-1.4.74.dist-info → django_cfg-1.4.76.dist-info}/RECORD +49 -41
  47. django_cfg/static/admin/js/commands.js +0 -171
  48. django_cfg/static/admin/js/dashboard.js +0 -126
  49. django_cfg/templates/admin/components/management_commands.js +0 -375
  50. django_cfg/templates/admin/snippets/components/CHARTS_GUIDE.md +0 -322
  51. django_cfg/templates/admin/snippets/components/recent_activity.html +0 -35
  52. {django_cfg-1.4.74.dist-info → django_cfg-1.4.76.dist-info}/WHEEL +0 -0
  53. {django_cfg-1.4.74.dist-info → django_cfg-1.4.76.dist-info}/entry_points.txt +0 -0
  54. {django_cfg-1.4.74.dist-info → django_cfg-1.4.76.dist-info}/licenses/LICENSE +0 -0
@@ -2,7 +2,7 @@
2
2
  <link rel="stylesheet" href="{% static 'admin/css/prose-unfold.css' %}">
3
3
 
4
4
  <!-- Documentation Section -->
5
- <div class="space-y-6">
5
+ <div class="space-y-6" x-data="toggleSection()">
6
6
  {% if error %}
7
7
  <!-- Error State -->
8
8
  <div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-default p-6">
@@ -17,7 +17,8 @@
17
17
  {% for app_key, app in commands_by_module.items %}
18
18
  <div class="app-block bg-gradient-to-br from-white to-base-50 dark:from-base-800 dark:to-base-900 rounded-default shadow-md border border-base-200 dark:border-base-700 overflow-hidden mb-6">
19
19
  <!-- App Header (Toggle) -->
20
- <div class="app-toggle cursor-pointer select-none p-6 border-b border-base-200 dark:border-base-700 hover:bg-base-100/50 dark:hover:bg-base-900/30 transition-all duration-200"
20
+ <div @click="toggleSection('{{ app_key }}')"
21
+ class="cursor-pointer select-none p-6 border-b border-base-200 dark:border-base-700 hover:bg-base-100/50 dark:hover:bg-base-900/30 transition-all duration-200"
21
22
  data-app="{{ app_key }}">
22
23
  <div class="w-full flex items-center justify-between">
23
24
  <h3 class="text-lg font-semibold text-font-important-light dark:text-font-important-dark flex items-center">
@@ -33,12 +34,18 @@
33
34
  </div>
34
35
  </div>
35
36
  </h3>
36
- <span class="material-symbols-outlined text-base-400 dark:text-base-500 toggle-icon transition-transform duration-200 select-none">expand_more</span>
37
+ <span
38
+ class="material-symbols-outlined text-base-400 dark:text-base-500 transition-transform duration-200 select-none"
39
+ x-text="isSectionExpanded('{{ app_key }}') ? 'expand_less' : 'expand_more'"
40
+ ></span>
37
41
  </div>
38
42
  </div>
39
43
 
40
44
  <!-- App Content -->
41
- <div class="app-content bg-base-50/30 dark:bg-base-900/30 p-6 space-y-4" data-app="{{ app_key }}" style="display: none;">
45
+ <div class="app-content bg-base-50/30 dark:bg-base-900/30 p-6 space-y-4"
46
+ data-app="{{ app_key }}"
47
+ x-show="isSectionExpanded('{{ app_key }}')"
48
+ x-cloak>
42
49
 
43
50
  <!-- Commands List -->
44
51
  <div class="space-y-3">
@@ -141,32 +148,5 @@
141
148
  {% endif %}
142
149
  </div>
143
150
 
144
- <!-- JavaScript for App Toggle -->
145
- <script>
146
- document.addEventListener('DOMContentLoaded', function() {
147
- // App toggle functionality
148
- const appToggles = document.querySelectorAll('.app-toggle');
149
- appToggles.forEach(toggle => {
150
- toggle.addEventListener('click', function() {
151
- const app = this.dataset.app;
152
- const content = document.querySelector(`.app-content[data-app="${app}"]`);
153
- const icon = this.querySelector('.toggle-icon');
154
-
155
- if (content.style.display === 'none' || !content.style.display) {
156
- content.style.display = 'block';
157
- icon.textContent = 'expand_less';
158
- } else {
159
- content.style.display = 'none';
160
- icon.textContent = 'expand_more';
161
- }
162
- });
163
-
164
- // Initialize - start collapsed
165
- const app = toggle.dataset.app;
166
- const content = document.querySelector(`.app-content[data-app="${app}"]`);
167
- if (content) {
168
- content.style.display = 'none';
169
- }
170
- });
171
- });
172
- </script>
151
+ <!-- Load Alpine component -->
152
+ <script src="{% static 'admin/js/alpine/toggle-section.js' %}"></script>
@@ -16,64 +16,42 @@
16
16
  </div>
17
17
 
18
18
  <!-- Activity Tracker Grid -->
19
- <div class="overflow-x-auto pb-2">
20
- {% if activity_tracker %}
19
+ {% if activity_tracker %}
20
+ <div class="overflow-x-auto pb-2" x-data='activityTracker({{ activity_tracker|safe }})'>
21
+ {% else %}
22
+ <div class="overflow-x-auto pb-2" x-data='activityTracker([])'>
23
+ {% endif %}
24
+ <template x-if="hasData">
21
25
  <!-- GitHub-style heatmap visualization -->
22
- <div id="activity-heatmap" class="inline-flex gap-1"></div>
23
-
24
- <script>
25
- (function() {
26
- const activityData = {{ activity_tracker }};
27
- const heatmap = document.getElementById('activity-heatmap');
28
-
29
- if (activityData && activityData.length > 0) {
30
- // Group days into weeks (7 days per column)
31
- const weeks = [];
32
- for (let i = 0; i < activityData.length; i += 7) {
33
- weeks.push(activityData.slice(i, i + 7));
34
- }
35
-
36
- // Create week columns
37
- weeks.forEach(week => {
38
- const weekColumn = document.createElement('div');
39
- weekColumn.className = 'flex flex-col gap-1';
40
-
41
- week.forEach(day => {
42
- const cell = document.createElement('div');
43
- cell.className = 'w-3 h-3 rounded-sm transition-all hover:ring-2 hover:ring-blue-400 cursor-pointer';
44
-
45
- // Color based on activity level
46
- if (day.count === 0) {
47
- cell.className += ' bg-gray-200 dark:bg-gray-700';
48
- } else if (day.count <= 2) {
49
- cell.className += ' bg-green-200 dark:bg-green-800';
50
- } else if (day.count <= 5) {
51
- cell.className += ' bg-green-400 dark:bg-green-600';
52
- } else if (day.count <= 10) {
53
- cell.className += ' bg-green-600 dark:bg-green-500';
54
- } else {
55
- cell.className += ' bg-green-800 dark:bg-green-400';
56
- }
57
-
58
- cell.title = `${day.date}: ${day.count} activities`;
59
- weekColumn.appendChild(cell);
60
- });
26
+ <div class="inline-flex gap-1">
27
+ <template x-for="(week, weekIndex) in weeks" :key="weekIndex">
28
+ <div class="flex flex-col gap-1">
29
+ <template x-for="(day, dayIndex) in week" :key="dayIndex">
30
+ <div
31
+ class="w-3 h-3 rounded-sm transition-all hover:ring-2 hover:ring-blue-400 cursor-pointer"
32
+ :class="getCellColor(day.count)"
33
+ :title="getCellTitle(day)"
34
+ ></div>
35
+ </template>
36
+ </div>
37
+ </template>
38
+ </div>
39
+ </template>
61
40
 
62
- heatmap.appendChild(weekColumn);
63
- });
64
- }
65
- })();
66
- </script>
67
- {% else %}
41
+ <template x-if="!hasData">
68
42
  <div class="flex items-center justify-center p-8 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
69
43
  <div class="text-center">
70
44
  <span class="material-icons text-4xl text-gray-400 dark:text-gray-500 mb-2">timeline</span>
71
45
  <p class="text-gray-600 dark:text-gray-400">No activity data available</p>
72
46
  </div>
73
47
  </div>
74
- {% endif %}
48
+ </template>
75
49
  </div>
76
50
 
51
+ <!-- Load Alpine component -->
52
+ {% load static %}
53
+ <script src="{% static 'admin/js/alpine/activity-tracker.js' %}"></script>
54
+
77
55
  <!-- Summary Stats -->
78
56
  <div class="grid grid-cols-3 gap-4 pt-4 border-t border-gray-200 dark:border-gray-700">
79
57
  <div class="text-center">
@@ -85,7 +63,7 @@
85
63
  <div class="text-xs text-gray-600 dark:text-gray-400">Weeks</div>
86
64
  </div>
87
65
  <div class="text-center">
88
- <div class="text-lg font-semibold text-blue-600 dark:text-blue-400">{{ activity_tracker|length|default:"0" }}</div>
66
+ <div class="text-lg font-semibold text-blue-600 dark:text-blue-400">{{ activity_tracker|length|default:0 }}</div>
89
67
  <div class="text-xs text-gray-600 dark:text-gray-400">Data points</div>
90
68
  </div>
91
69
  </div>
@@ -32,44 +32,9 @@
32
32
  <span class="text-sm text-font-subtle-light dark:text-font-subtle-dark">Growth Trends</span>
33
33
  </div>
34
34
  {% if charts.user_registrations %}
35
- <div class="relative h-[300px]">
36
- <canvas id="userRegistrationsChart"></canvas>
35
+ <div class="relative h-[300px]" x-data='chart({{ charts.user_registrations_json|safe }}, "line")'>
36
+ <canvas x-ref="canvas"></canvas>
37
37
  </div>
38
- <script>
39
- document.addEventListener('DOMContentLoaded', function() {
40
- const ctx = document.getElementById('userRegistrationsChart');
41
- const chartData = {{ charts.user_registrations_json|safe }};
42
-
43
- if (ctx && typeof Chart !== 'undefined') {
44
- try {
45
- new Chart(ctx, {
46
- type: 'line',
47
- data: chartData,
48
- options: {
49
- responsive: true,
50
- maintainAspectRatio: false,
51
- plugins: {
52
- legend: {
53
- display: true,
54
- position: 'top'
55
- }
56
- },
57
- scales: {
58
- y: {
59
- beginAtZero: true,
60
- ticks: {
61
- precision: 0
62
- }
63
- }
64
- }
65
- }
66
- });
67
- } catch (error) {
68
- console.error('Error creating registration chart:', error);
69
- }
70
- }
71
- });
72
- </script>
73
38
 
74
39
  <!-- Fallback data table if chart doesn't render -->
75
40
  <div class="mt-4 text-xs text-font-subtle-light dark:text-font-subtle-dark">
@@ -106,44 +71,9 @@
106
71
  <span class="text-sm text-font-subtle-light dark:text-font-subtle-dark">Activity Levels</span>
107
72
  </div>
108
73
  {% if charts.user_activity %}
109
- <div class="relative h-[300px]">
110
- <canvas id="userActivityChart"></canvas>
74
+ <div class="relative h-[300px]" x-data='chart({{ charts.user_activity_json|safe }}, "bar")'>
75
+ <canvas x-ref="canvas"></canvas>
111
76
  </div>
112
- <script>
113
- document.addEventListener('DOMContentLoaded', function() {
114
- const ctx = document.getElementById('userActivityChart');
115
- const chartData = {{ charts.user_activity_json|safe }};
116
-
117
- if (ctx && typeof Chart !== 'undefined') {
118
- try {
119
- new Chart(ctx, {
120
- type: 'bar',
121
- data: chartData,
122
- options: {
123
- responsive: true,
124
- maintainAspectRatio: false,
125
- plugins: {
126
- legend: {
127
- display: true,
128
- position: 'top'
129
- }
130
- },
131
- scales: {
132
- y: {
133
- beginAtZero: true,
134
- ticks: {
135
- precision: 0
136
- }
137
- }
138
- }
139
- }
140
- });
141
- } catch (error) {
142
- console.error('Error creating activity chart:', error);
143
- }
144
- }
145
- });
146
- </script>
147
77
 
148
78
  <!-- Fallback data table if chart doesn't render -->
149
79
  <div class="mt-4 text-xs text-font-subtle-light dark:text-font-subtle-dark">
@@ -177,3 +107,7 @@
177
107
  </div>
178
108
  {% endif %}
179
109
  </div>
110
+
111
+ <!-- Load Chart Alpine component -->
112
+ {% load static %}
113
+ <script src="{% static 'admin/js/alpine/chart.js' %}"></script>
@@ -1,7 +1,7 @@
1
1
  {% load unfold %}
2
2
 
3
3
  <!-- Django Commands Section -->
4
- <div class="mt-8 w-full">
4
+ <div class="mt-8 w-full" x-data="commandsPanel({{ django_commands.total_commands }})">
5
5
  <!-- Header -->
6
6
  <div class="flex items-center justify-between mb-6">
7
7
  <div class="flex items-center">
@@ -12,26 +12,27 @@
12
12
  Django Commands
13
13
  </h2>
14
14
  <span class="ml-4 text-xs text-font-subtle-light dark:text-font-subtle-dark bg-base-100 dark:bg-base-800 px-2 py-1 rounded-full">
15
- <span id="commandsCount">{{ django_commands.total_commands }}</span> commands
15
+ <span x-text="visibleCommands"></span> commands
16
16
  </span>
17
17
  </div>
18
-
18
+
19
19
  <!-- Search Box -->
20
20
  <div class="flex items-center space-x-3">
21
21
  <div class="bg-white border border-base-200 flex flex-row items-center px-3 rounded-default relative shadow-xs w-64 focus-within:outline-2 focus-within:-outline-offset-2 focus-within:outline-primary-600 dark:bg-base-900 dark:border-base-700">
22
22
  <span class="material-symbols-outlined md-18 text-base-400 dark:text-base-500">search</span>
23
- <input
24
- type="text"
25
- id="commandSearch"
26
- placeholder="Search commands..."
23
+ <input
24
+ type="text"
25
+ x-ref="searchInput"
26
+ x-model="searchQuery"
27
+ @input="search()"
28
+ placeholder="Search commands..."
27
29
  class="grow font-medium min-w-0 overflow-hidden p-2 placeholder-font-subtle-light truncate focus:outline-hidden dark:bg-base-900 dark:placeholder-font-subtle-dark dark:text-font-default-dark"
28
- oninput="searchCommands(this.value)"
29
30
  >
30
31
  </div>
31
- <button
32
- id="clearSearch"
33
- onclick="clearSearch()"
34
- class="px-3 py-2 text-xs text-font-subtle-light dark:text-font-subtle-dark hover:text-font-default-light dark:hover:text-font-default-dark transition-colors hidden"
32
+ <button
33
+ x-show="showClearButton"
34
+ @click="clearSearch()"
35
+ class="px-3 py-2 text-xs text-font-subtle-light dark:text-font-subtle-dark hover:text-font-default-light dark:hover:text-font-default-dark transition-colors"
35
36
  >
36
37
  Clear
37
38
  </button>
@@ -44,8 +45,10 @@
44
45
  {% for category, commands in django_commands.categorized.items %}
45
46
  <div class="bg-white dark:bg-base-900 rounded-lg border border-base-200 dark:border-base-700 overflow-hidden shadow-sm mb-4">
46
47
  <!-- Category Header -->
47
- <button
48
- onclick="toggleCategory('{{ category }}')"
48
+ <button
49
+ type="button"
50
+ data-category="{{ category }}"
51
+ @click="toggleCategory('{{ category }}')"
49
52
  class="w-full p-4 flex items-center justify-between hover:bg-base-100 dark:hover:bg-base-700 transition-colors"
50
53
  >
51
54
  <div class="flex items-center">
@@ -57,13 +60,20 @@
57
60
  {{ commands|length }} commands
58
61
  </span>
59
62
  </div>
60
- <span class="material-icons text-font-subtle-light dark:text-font-subtle-dark transition-transform" id="icon-{{ category }}" style="transform: rotate(-90deg);">
61
- expand_less
62
- </span>
63
+ <span
64
+ class="material-icons text-font-subtle-light dark:text-font-subtle-dark transition-transform"
65
+ :class="isCategoryExpanded('{{ category }}') ? 'rotate-0' : '-rotate-90'"
66
+ x-text="isCategoryExpanded('{{ category }}') ? 'expand_less' : 'expand_more'"
67
+ ></span>
63
68
  </button>
64
-
65
- <!-- Collapsible Content -->
66
- <div id="content-{{ category }}" class="border-t border-base-200 dark:border-base-700" style="display: none;">
69
+
70
+ <!-- Collapsible Content -->
71
+ <div
72
+ id="content-{{ category }}"
73
+ class="border-t border-base-200 dark:border-base-700"
74
+ x-show="isCategoryExpanded('{{ category }}')"
75
+ x-cloak
76
+ >
67
77
  <div class="p-4">
68
78
  <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
69
79
  {% for command in commands %}
@@ -122,17 +132,17 @@
122
132
  {% endif %}
123
133
  </div>
124
134
 
125
- <!-- Action Buttons - Custom due to onclick handlers -->
135
+ <!-- Action Buttons -->
126
136
  <div class="flex gap-2">
127
137
  <button
128
- onclick="copyToClipboard('python manage.py {{ command.name }}')"
138
+ @click="window.copyToClipboard('python manage.py {{ command.name }}')"
129
139
  class="flex-1 inline-flex items-center justify-center px-3 py-2 bg-base-100 dark:bg-base-700 hover:bg-base-200 dark:hover:bg-base-600 text-font-default-light dark:text-font-default-dark rounded-lg text-xs font-medium transition-colors"
130
140
  title="Copy command to clipboard">
131
141
  <span class="material-icons text-xs mr-1">content_copy</span>
132
142
  Copy
133
143
  </button>
134
144
  <button
135
- onclick="executeCommand('{{ command.name }}')"
145
+ @click="window.executeCommand('{{ command.name }}')"
136
146
  class="flex-1 inline-flex items-center justify-center px-3 py-2 bg-green-600 hover:bg-green-700 dark:bg-green-500 dark:hover:bg-green-600 text-white rounded-lg text-xs font-medium transition-colors"
137
147
  title="Execute command">
138
148
  <span class="material-icons text-xs mr-1">play_arrow</span>
@@ -148,7 +158,20 @@
148
158
  </div>
149
159
  {% endfor %}
150
160
  </div>
151
-
161
+
162
+ <!-- No Results Message -->
163
+ <div x-show="showNoResults" class="text-center py-12">
164
+ <div class="flex flex-col items-center">
165
+ <span class="material-icons text-6xl text-base-400 dark:text-base-500 mb-4">search_off</span>
166
+ <h3 class="text-lg font-medium text-font-important-light dark:text-font-important-dark mb-2">
167
+ No Commands Found
168
+ </h3>
169
+ <p class="text-font-subtle-light dark:text-font-subtle-dark max-w-md mx-auto">
170
+ No commands match your search criteria. Try different keywords or clear the search.
171
+ </p>
172
+ </div>
173
+ </div>
174
+
152
175
  {% else %}
153
176
  <!-- No Commands -->
154
177
  <div class="text-center py-12">
@@ -165,8 +188,8 @@
165
188
  {% endif %}
166
189
  </div>
167
190
 
168
- <!-- Command Execution Modal -->
169
- <div id="commandModal" class="fixed inset-0 bg-black/80 backdrop-blur-sm hidden z-50">
191
+ <!-- Command Execution Modal (Alpine.js) -->
192
+ <div x-data="commandModal" x-show="open" x-cloak class="fixed inset-0 bg-black/80 backdrop-blur-sm z-50">
170
193
  <div class="flex items-center justify-center min-h-screen p-4">
171
194
  <div class="bg-white dark:bg-base-900 rounded-lg shadow-2xl max-w-4xl w-full max-h-[80vh] flex flex-col border border-base-200 dark:border-base-700">
172
195
  <!-- Modal Header -->
@@ -180,29 +203,59 @@
180
203
  Command Execution
181
204
  </h3>
182
205
  <p class="text-sm text-font-subtle-light dark:text-font-subtle-dark">
183
- Running: <span id="commandName" class="font-mono text-primary-600 dark:text-primary-400"></span>
206
+ Running: <span x-text="commandName" class="font-mono text-primary-600 dark:text-primary-400"></span>
184
207
  </p>
185
208
  </div>
186
209
  </div>
187
- <button onclick="closeCommandModal()" class="p-2 text-font-subtle-light dark:text-font-subtle-dark hover:text-font-default-light dark:hover:text-font-default-dark hover:bg-base-100 dark:hover:bg-base-700 rounded-lg transition-colors">
210
+ <button @click="close()" class="p-2 text-font-subtle-light dark:text-font-subtle-dark hover:text-font-default-light dark:hover:text-font-default-dark hover:bg-base-100 dark:hover:bg-base-700 rounded-lg transition-colors">
188
211
  <span class="material-icons">close</span>
189
212
  </button>
190
213
  </div>
191
-
214
+
215
+ <!-- Tabs Navigation -->
216
+ <div class="flex gap-1 px-4 pt-2 border-b border-base-200 dark:border-base-700">
217
+ <button
218
+ @click="activeTab = 'output'"
219
+ :class="activeTab === 'output' ? 'active' : ''"
220
+ class="command-tab px-4 py-2 text-sm font-medium rounded-t-lg transition-colors"
221
+ >
222
+ <span class="material-icons text-sm mr-1 align-middle">terminal</span>
223
+ Output
224
+ </button>
225
+ <button
226
+ @click="activeTab = 'docs'"
227
+ :class="activeTab === 'docs' ? 'active' : ''"
228
+ class="command-tab px-4 py-2 text-sm font-medium rounded-t-lg transition-colors"
229
+ >
230
+ <span class="material-icons text-sm mr-1 align-middle">help_outline</span>
231
+ Documentation
232
+ </button>
233
+ </div>
234
+
192
235
  <!-- Modal Body -->
193
- <div class="flex-1 p-4" style="min-height: 0;">
194
- <div id="commandOutput" class="bg-base-100 dark:bg-base-800 rounded-lg border border-base-200 dark:border-base-700 overflow-y-auto p-4 text-sm font-mono text-font-default-light dark:text-font-default-dark whitespace-pre-wrap leading-relaxed break-words" style="height: calc(80vh - 200px); max-height: 60vh;"></div>
236
+ <div class="flex-1 p-4 min-h-0">
237
+ <!-- Output Tab Content -->
238
+ <div x-show="activeTab === 'output'" class="h-full">
239
+ <div x-html="outputHtml" class="bg-base-100 dark:bg-base-800 rounded-lg border border-base-200 dark:border-base-700 overflow-y-auto p-4 text-sm font-mono text-font-default-light dark:text-font-default-dark whitespace-pre-wrap leading-relaxed break-words h-full"></div>
240
+ </div>
241
+
242
+ <!-- Documentation Tab Content -->
243
+ <div x-show="activeTab === 'docs'" class="h-full">
244
+ <div class="bg-base-100 dark:bg-base-800 rounded-lg border border-base-200 dark:border-base-700 overflow-y-auto p-4 h-full">
245
+ <div x-html="docsHtml" class="prose prose-sm dark:prose-invert max-w-none"></div>
246
+ </div>
247
+ </div>
195
248
  </div>
196
-
249
+
197
250
  <!-- Modal Footer -->
198
251
  <div class="flex items-center justify-between p-4 border-t border-base-200 dark:border-base-700 bg-base-50 dark:bg-base-800">
199
252
  <div class="flex items-center space-x-3">
200
- <div id="commandStatus" class="flex items-center">
201
- <div class="w-3 h-3 bg-yellow-500 rounded-full mr-2 animate-pulse"></div>
202
- <span class="text-sm font-medium text-font-default-light dark:text-font-default-dark">Executing...</span>
253
+ <div class="flex items-center">
254
+ <div :class="statusClass" class="w-3 h-3 rounded-full mr-2"></div>
255
+ <span x-text="statusText" class="text-sm font-medium text-font-default-light dark:text-font-default-dark"></span>
203
256
  </div>
204
257
  </div>
205
- <button onclick="closeCommandModal()" class="px-4 py-2 bg-base-100 dark:bg-base-700 hover:bg-base-200 dark:hover:bg-base-600 text-font-default-light dark:text-font-default-dark rounded-lg transition-colors font-medium">
258
+ <button @click="close()" class="px-4 py-2 bg-base-100 dark:bg-base-700 hover:bg-base-200 dark:hover:bg-base-600 text-font-default-light dark:text-font-default-dark rounded-lg transition-colors font-medium">
206
259
  Close
207
260
  </button>
208
261
  </div>
@@ -210,148 +263,8 @@
210
263
  </div>
211
264
  </div>
212
265
 
213
- <!-- JavaScript for command search functionality -->
214
- <script>
215
- function searchCommands(query) {
216
- const searchQuery = query.toLowerCase().trim();
217
- const categories = document.querySelectorAll('[id^="content-"]');
218
- const clearButton = document.getElementById('clearSearch');
219
- const commandsCount = document.getElementById('commandsCount');
220
- let visibleCommands = 0;
221
-
222
- // Show/hide clear button
223
- if (searchQuery) {
224
- clearButton.classList.remove('hidden');
225
- } else {
226
- clearButton.classList.add('hidden');
227
- }
228
-
229
- categories.forEach(category => {
230
- const categoryName = category.id.replace('content-', '');
231
- const commands = category.querySelectorAll('.command-item');
232
- let categoryHasVisibleCommands = false;
233
-
234
- commands.forEach(command => {
235
- const commandName = command.querySelector('.command-name').textContent.toLowerCase();
236
- const commandDesc = command.querySelector('.command-description')?.textContent.toLowerCase() || '';
237
-
238
- if (!searchQuery || commandName.includes(searchQuery) || commandDesc.includes(searchQuery)) {
239
- command.style.display = 'block';
240
- categoryHasVisibleCommands = true;
241
- visibleCommands++;
242
- } else {
243
- command.style.display = 'none';
244
- }
245
- });
246
-
247
- // Show/hide category based on whether it has visible commands
248
- const categoryHeader = document.querySelector(`button[onclick="toggleCategory('${categoryName}')"]`);
249
- const categoryContainer = categoryHeader.parentElement;
250
-
251
- if (categoryHasVisibleCommands) {
252
- categoryContainer.style.display = 'block';
253
-
254
- // Auto-expand categories when searching
255
- if (searchQuery) {
256
- category.style.display = 'block';
257
- const icon = categoryHeader.querySelector('.material-icons');
258
- if (icon) {
259
- icon.textContent = 'expand_less';
260
- icon.style.transform = 'rotate(0deg)';
261
- }
262
- }
263
- } else {
264
- categoryContainer.style.display = 'none';
265
- }
266
- });
267
-
268
- // Update commands count
269
- commandsCount.textContent = visibleCommands;
270
-
271
- // Show "no results" message if no commands found
272
- showNoResultsMessage(visibleCommands === 0 && searchQuery);
273
- }
274
-
275
- function clearSearch() {
276
- const searchInput = document.getElementById('commandSearch');
277
- const clearButton = document.getElementById('clearSearch');
278
- const commandsCount = document.getElementById('commandsCount');
279
-
280
- searchInput.value = '';
281
- clearButton.classList.add('hidden');
282
-
283
- // Show all commands and categories
284
- const categories = document.querySelectorAll('[id^="content-"]');
285
- const allCommands = document.querySelectorAll('.command-item');
286
-
287
- categories.forEach(category => {
288
- const categoryName = category.id.replace('content-', '');
289
- const categoryHeader = document.querySelector(`button[onclick="toggleCategory('${categoryName}')"]`);
290
- categoryHeader.parentElement.style.display = 'block';
291
- // Reset to collapsed state
292
- category.style.display = 'none';
293
- const icon = categoryHeader.querySelector('.material-icons');
294
- if (icon) {
295
- icon.textContent = 'expand_less';
296
- icon.style.transform = 'rotate(-90deg)';
297
- }
298
- });
299
-
300
- allCommands.forEach(command => {
301
- command.style.display = 'block';
302
- });
303
-
304
- // Reset commands count
305
- commandsCount.textContent = '{{ django_commands.total_commands }}';
306
-
307
- // Hide no results message
308
- showNoResultsMessage(false);
309
- }
310
-
311
- function showNoResultsMessage(show) {
312
- let noResultsDiv = document.getElementById('noSearchResults');
313
-
314
- if (show && !noResultsDiv) {
315
- // Create no results message
316
- noResultsDiv = document.createElement('div');
317
- noResultsDiv.id = 'noSearchResults';
318
- noResultsDiv.className = 'text-center py-12';
319
- noResultsDiv.innerHTML = `
320
- <div class="flex flex-col items-center">
321
- <span class="material-icons text-6xl text-base-400 dark:text-base-500 mb-4">search_off</span>
322
- <h3 class="text-lg font-medium text-font-important-light dark:text-font-important-dark mb-2">
323
- No Commands Found
324
- </h3>
325
- <p class="text-font-subtle-light dark:text-font-subtle-dark max-w-md mx-auto">
326
- No commands match your search criteria. Try different keywords or clear the search.
327
- </p>
328
- </div>
329
- `;
330
-
331
- // Insert after the commands container
332
- const commandsContainer = document.querySelector('.space-y-4');
333
- commandsContainer.parentNode.insertBefore(noResultsDiv, commandsContainer.nextSibling);
334
- } else if (!show && noResultsDiv) {
335
- noResultsDiv.remove();
336
- }
337
- }
338
-
339
- // Add search keyboard shortcuts
340
- document.addEventListener('DOMContentLoaded', function() {
341
- const searchInput = document.getElementById('commandSearch');
342
-
343
- // Focus search with Ctrl+F or Cmd+F
344
- document.addEventListener('keydown', function(e) {
345
- if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
346
- e.preventDefault();
347
- searchInput.focus();
348
- }
349
-
350
- // Clear search with Escape
351
- if (e.key === 'Escape' && document.activeElement === searchInput) {
352
- clearSearch();
353
- searchInput.blur();
354
- }
355
- });
356
- });
357
- </script>
266
+ <!-- Alpine.js Components & Styles -->
267
+ {% load static %}
268
+ <link rel="stylesheet" href="{% static 'admin/css/tabs.css' %}">
269
+ <script src="{% static 'admin/js/alpine/command-modal.js' %}"></script>
270
+ <script src="{% static 'admin/js/alpine/commands-panel.js' %}"></script>