django-adminflow 1.0.0__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 (47) hide show
  1. adminflow/__init__.py +1 -0
  2. adminflow/apps.py +22 -0
  3. adminflow/context_processors.py +72 -0
  4. adminflow/forms.py +89 -0
  5. adminflow/import_export.py +224 -0
  6. adminflow/static/adminflow/img/logo.png +0 -0
  7. adminflow/templates/admin/actions.html +79 -0
  8. adminflow/templates/admin/base.html +1263 -0
  9. adminflow/templates/admin/base_site.html +5 -0
  10. adminflow/templates/admin/change_form.html +315 -0
  11. adminflow/templates/admin/change_list.html +216 -0
  12. adminflow/templates/admin/change_list_results.html +211 -0
  13. adminflow/templates/admin/delete_confirmation.html +55 -0
  14. adminflow/templates/admin/delete_selected_confirmation.html +118 -0
  15. adminflow/templates/admin/edit_inline/stacked.html +85 -0
  16. adminflow/templates/admin/edit_inline/tabular.html +81 -0
  17. adminflow/templates/admin/filter.html +17 -0
  18. adminflow/templates/admin/import_export/change_list_export_item.html +8 -0
  19. adminflow/templates/admin/import_export/change_list_import_item.html +8 -0
  20. adminflow/templates/admin/import_export/export.html +199 -0
  21. adminflow/templates/admin/includes/fieldset.html +73 -0
  22. adminflow/templates/admin/includes/toast.html +102 -0
  23. adminflow/templates/admin/index.html +227 -0
  24. adminflow/templates/admin/logged_out.html +38 -0
  25. adminflow/templates/admin/login.html +144 -0
  26. adminflow/templates/admin/object_history.html +60 -0
  27. adminflow/templates/admin/pagination.html +30 -0
  28. adminflow/templates/admin/search_form.html +28 -0
  29. adminflow/templates/admin/submit_line.html +48 -0
  30. adminflow/templates/admin/verify_2fa.html +280 -0
  31. adminflow/templates/otp/email/token.html +64 -0
  32. adminflow/templates/otp/email/token.txt +11 -0
  33. adminflow/templates/registration/logged_out.html +38 -0
  34. adminflow/templates/registration/password_change_form.html +272 -0
  35. adminflow/templates/simple_history/object_history.html +186 -0
  36. adminflow/templates/simple_history/object_history_form.html +103 -0
  37. adminflow/templates/simple_history/submit_line.html +17 -0
  38. adminflow/templatetags/__init__.py +1 -0
  39. adminflow/templatetags/__pycache__/__init__.cpython-314.pyc +0 -0
  40. adminflow/templatetags/__pycache__/adminflow_tags.cpython-314.pyc +0 -0
  41. adminflow/templatetags/adminflow_tags.py +233 -0
  42. adminflow/user_admin.py +662 -0
  43. adminflow/views.py +207 -0
  44. django_adminflow-1.0.0.dist-info/METADATA +398 -0
  45. django_adminflow-1.0.0.dist-info/RECORD +47 -0
  46. django_adminflow-1.0.0.dist-info/WHEEL +5 -0
  47. django_adminflow-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1263 @@
1
+ {% load i18n static adminflow_tags log %}<!DOCTYPE html>
2
+ <html lang="{{ adminflow_language|default:'en' }}" class="light" {% if adminflow_rtl %}dir="rtl"{% endif %}>
3
+ <head>
4
+ <meta charset="utf-8"/>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <title>{% block title %}{% endblock %} | {{ adminflow_title }}</title>
7
+
8
+ {% if adminflow_favicon %}
9
+ <link rel="icon" href="{{ adminflow_favicon }}" type="image/x-icon"/>
10
+ {% endif %}
11
+
12
+ <!-- Google Fonts & Material Icons -->
13
+ <link rel="preconnect" href="https://fonts.googleapis.com"/>
14
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
15
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
16
+ <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"/>
17
+
18
+ <!-- Tailwind CSS -->
19
+ <script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
20
+
21
+ <!-- Flatpickr CSS & JS -->
22
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css"/>
23
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/themes/light.css" id="flatpickr-theme"/>
24
+ <script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
25
+
26
+ <script id="tailwind-config">
27
+ tailwind.config = {
28
+ darkMode: "class",
29
+ theme: {
30
+ extend: {
31
+ colors: {
32
+ primary: {
33
+ DEFAULT: "{{ adminflow_primary_color|default:'#2563EB' }}",
34
+ fixed: "{{ adminflow_primary_fixed|default:'#dbe1ff' }}",
35
+ dim: "{{ adminflow_primary_fixed_dim|default:'#b4c5ff' }}"
36
+ },
37
+ secondary: {
38
+ DEFAULT: "{{ adminflow_secondary_color|default:'#64748B' }}",
39
+ fixed: "{{ adminflow_secondary_fixed|default:'#d8e2ff' }}",
40
+ dim: "{{ adminflow_secondary_fixed_dim|default:'#adc6ff' }}"
41
+ },
42
+ success: {
43
+ DEFAULT: "{{ adminflow_success_color|default:'#22C55E' }}",
44
+ fixed: "{{ adminflow_tertiary_fixed|default:'#6ffbbe' }}",
45
+ dim: "{{ adminflow_tertiary_fixed_dim|default:'#4edea3' }}"
46
+ },
47
+ warning: {
48
+ DEFAULT: "{{ adminflow_warning_color|default:'#F59E0B' }}"
49
+ },
50
+ danger: {
51
+ DEFAULT: "{{ adminflow_danger_color|default:'#EF4444' }}"
52
+ },
53
+ info: {
54
+ DEFAULT: "{{ adminflow_info_color|default:'#06B6D4' }}"
55
+ },
56
+ surface: {
57
+ DEFAULT: "{{ adminflow_bg_color|default:'#f8f9ff' }}",
58
+ dim: "{{ adminflow_surface_dim|default:'#cbdbf5' }}",
59
+ bright: "{{ adminflow_surface_bright|default:'#f8f9ff' }}",
60
+ lowest: "{{ adminflow_surface_container_lowest|default:'#ffffff' }}",
61
+ low: "{{ adminflow_surface_container_low|default:'#eff4ff' }}",
62
+ container: "{{ adminflow_surface_container|default:'#e5eeff' }}",
63
+ high: "{{ adminflow_surface_container_high|default:'#dce9ff' }}",
64
+ highest: "{{ adminflow_surface_container_highest|default:'#d3e4fe' }}"
65
+ },
66
+ "on-surface": "{{ adminflow_on_surface|default:'#0b1c30' }}",
67
+ "on-surface-variant": "{{ adminflow_on_surface_variant|default:'#434655' }}",
68
+ "inverse-surface": "{{ adminflow_inverse_surface|default:'#213145' }}",
69
+ "inverse-on-surface": "{{ adminflow_inverse_on_surface|default:'#eaf1ff' }}",
70
+ "outline-variant": "{{ adminflow_outline_variant|default:'#c3c6d7' }}",
71
+ "outline": "{{ adminflow_outline|default:'#737686' }}",
72
+ "sidebar-bg": "{{ adminflow_sidebar_color|default:'#0f172a' }}"
73
+ },
74
+ borderRadius: {
75
+ DEFAULT: "0.25rem",
76
+ lg: "0.5rem",
77
+ xl: "{{ adminflow_border_radius|default:'12px' }}",
78
+ full: "9999px"
79
+ },
80
+ spacing: {
81
+ "density-compact": "4px",
82
+ "density-comfortable": "12px",
83
+ gutter: "16px",
84
+ "container-margin": "24px",
85
+ "base-unit": "4px"
86
+ },
87
+ fontFamily: {
88
+ sans: ["{{ adminflow_font|default:'Inter' }}", "sans-serif"],
89
+ mono: ["JetBrains Mono", "monospace"]
90
+ }
91
+ }
92
+ }
93
+ }
94
+ </script>
95
+
96
+ <style>
97
+ .material-symbols-outlined {
98
+ font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
99
+ vertical-align: middle;
100
+ }
101
+ .glass-card {
102
+ background: rgba(255, 255, 255, 0.7);
103
+ backdrop-filter: blur(10px);
104
+ border: 1px solid rgba(226, 232, 240, 0.8);
105
+ }
106
+ .dark .glass-card {
107
+ background: rgba(15, 23, 42, 0.6);
108
+ backdrop-filter: blur(10px);
109
+ border: 1px solid rgba(51, 65, 85, 0.5);
110
+ }
111
+ .custom-scrollbar::-webkit-scrollbar {
112
+ width: 4px;
113
+ height: 4px;
114
+ }
115
+ .custom-scrollbar::-webkit-scrollbar-track {
116
+ background: transparent;
117
+ }
118
+ .custom-scrollbar::-webkit-scrollbar-thumb {
119
+ background: #cbd5e1;
120
+ border-radius: 10px;
121
+ }
122
+ .dark .custom-scrollbar::-webkit-scrollbar-thumb {
123
+ background: #475569;
124
+ }
125
+
126
+ /* Related widget action wrapper style */
127
+ .related-widget-wrapper {
128
+ display: flex !important;
129
+ align-items: center !important;
130
+ gap: 6px !important;
131
+ width: 100% !important;
132
+ }
133
+ .related-widget-wrapper select {
134
+ flex: 1 !important;
135
+ min-width: 0 !important;
136
+ }
137
+ .related-widget-wrapper a {
138
+ display: inline-flex !important;
139
+ align-items: center !important;
140
+ justify-content: center !important;
141
+ flex-shrink: 0 !important;
142
+ opacity: 0.8;
143
+ transition: opacity 0.2s;
144
+ }
145
+ .related-widget-wrapper a:hover {
146
+ opacity: 1;
147
+ }
148
+ .related-widget-wrapper a img {
149
+ width: 16px !important;
150
+ height: 16px !important;
151
+ object-fit: contain !important;
152
+ }
153
+ .related-widget-wrapper .add-related {
154
+ display: none !important;
155
+ }
156
+
157
+ /* ── Collapsed Mini Sidebar Styles ── */
158
+ #admin-sidebar.sidebar-collapsed {
159
+ width: 64px !important;
160
+ }
161
+ #admin-sidebar.sidebar-collapsed .sidebar-text {
162
+ display: none !important;
163
+ }
164
+ #admin-sidebar.sidebar-collapsed .brand-header {
165
+ padding-left: 0.5rem !important;
166
+ padding-right: 0.5rem !important;
167
+ justify-content: center !important;
168
+ }
169
+ #admin-sidebar.sidebar-collapsed .brand-header a {
170
+ justify-content: center !important;
171
+ gap: 0 !important;
172
+ }
173
+ #admin-sidebar.sidebar-collapsed .sidebar-toggle-btn {
174
+ display: none !important;
175
+ }
176
+ #admin-sidebar.sidebar-collapsed nav {
177
+ padding-left: 0.5rem !important;
178
+ padding-right: 0.5rem !important;
179
+ }
180
+ #admin-sidebar.sidebar-collapsed .sidebar-link {
181
+ justify-content: center !important;
182
+ padding-left: 0 !important;
183
+ padding-right: 0 !important;
184
+ width: 44px !important;
185
+ height: 44px !important;
186
+ margin-left: auto !important;
187
+ margin-right: auto !important;
188
+ border-radius: 12px !important;
189
+ }
190
+ #admin-sidebar.sidebar-collapsed .sidebar-app-group {
191
+ padding-top: 0.5rem !important;
192
+ margin-top: 0.5rem !important;
193
+ border-top: 1px solid rgba(255, 255, 255, 0.08) !important;
194
+ }
195
+ #admin-sidebar.sidebar-collapsed .sidebar-footer {
196
+ padding-left: 0.5rem !important;
197
+ padding-right: 0.5rem !important;
198
+ }
199
+
200
+ /* Object History Table Styles */
201
+ #change-history {
202
+ width: 100% !important;
203
+ border-collapse: collapse !important;
204
+ margin-top: 16px !important;
205
+ background-color: #ffffff !important;
206
+ border-radius: 12px !important;
207
+ border: 1px solid rgba(226, 232, 240, 0.8) !important;
208
+ overflow: hidden !important;
209
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -2px rgba(0, 0, 0, 0.05) !important;
210
+ }
211
+ .dark #change-history {
212
+ background-color: #1e293b !important;
213
+ border-color: #334155 !important;
214
+ }
215
+ #change-history th, #change-history td {
216
+ padding: 12px 16px !important;
217
+ text-align: left !important;
218
+ font-size: 12px !important;
219
+ border-bottom: 1px solid rgba(226, 232, 240, 0.8) !important;
220
+ color: #334155 !important;
221
+ }
222
+ .dark #change-history th, .dark #change-history td {
223
+ border-bottom-color: #334155 !important;
224
+ color: #cbd5e1 !important;
225
+ }
226
+ #change-history thead {
227
+ background-color: #f8fafc !important;
228
+ }
229
+ .dark #change-history thead {
230
+ background-color: #0f172a !important;
231
+ }
232
+ #change-history thead th {
233
+ font-weight: 600 !important;
234
+ color: #475569 !important;
235
+ text-transform: uppercase !important;
236
+ letter-spacing: 0.05em !important;
237
+ font-size: 10px !important;
238
+ }
239
+ .dark #change-history thead th {
240
+ color: #94a3b8 !important;
241
+ }
242
+ #change-history tbody tr {
243
+ transition: background-color 150ms ease !important;
244
+ }
245
+ #change-history tbody tr:hover {
246
+ background-color: #f8fafc !important;
247
+ }
248
+ .dark #change-history tbody tr:hover {
249
+ background-color: rgba(30, 41, 59, 0.5) !important;
250
+ }
251
+ #change-history tbody tr:last-child th,
252
+ #change-history tbody tr:last-child td {
253
+ border-bottom: none !important;
254
+ }
255
+ #change-history a {
256
+ color: {{ adminflow_primary_color|default:'#2563EB' }} !important;
257
+ text-decoration: none !important;
258
+ font-weight: 500 !important;
259
+ }
260
+ #change-history a:hover {
261
+ text-decoration: underline !important;
262
+ }
263
+ #content-main p {
264
+ font-size: 13px !important;
265
+ color: #64748b !important;
266
+ margin-top: 8px !important;
267
+ margin-bottom: 16px !important;
268
+ }
269
+ .dark #content-main p {
270
+ color: #94a3b8 !important;
271
+ }
272
+
273
+ /* Submit Row / Revert Page styling fallback */
274
+ .submit-row {
275
+ display: flex !important;
276
+ flex-wrap: wrap !important;
277
+ align-items: center !important;
278
+ gap: 8px !important;
279
+ padding: 16px !important;
280
+ border: 1px solid rgba(226, 232, 240, 0.8) !important;
281
+ background-color: rgba(248, 250, 252, 0.5) !important;
282
+ border-radius: 12px !important;
283
+ margin-top: 24px !important;
284
+ margin-bottom: 24px !important;
285
+ width: 100% !important;
286
+ }
287
+ .dark .submit-row {
288
+ border-color: #334155 !important;
289
+ background-color: rgba(30, 41, 59, 0.5) !important;
290
+ }
291
+ .submit-row input[type="submit"], .submit-row button {
292
+ padding: 8px 24px !important;
293
+ border-radius: 8px !important;
294
+ font-size: 12px !important;
295
+ font-weight: 700 !important;
296
+ cursor: pointer !important;
297
+ transition: all 150ms ease !important;
298
+ outline: none !important;
299
+ border: none !important;
300
+ }
301
+ .submit-row input[type="submit"].default, .submit-row button.default {
302
+ background-color: {{ adminflow_primary_color|default:'#2563EB' }} !important;
303
+ color: #ffffff !important;
304
+ box-shadow: 0 4px 6px -1px rgba(37, 99, 235, 0.2) !important;
305
+ }
306
+ .submit-row input[type="submit"].default:hover, .submit-row button.default:hover {
307
+ background-color: {{ adminflow_primary_color|default:'#2563EB' }}ee !important;
308
+ transform: translateY(-1px) !important;
309
+ }
310
+ .submit-row input[type="submit"].default:active, .submit-row button.default:active {
311
+ transform: translateY(0) !important;
312
+ }
313
+ .submit-row a, .submit-row .closelink {
314
+ padding: 8px 16px !important;
315
+ border-radius: 8px !important;
316
+ font-size: 12px !important;
317
+ font-weight: 700 !important;
318
+ text-decoration: none !important;
319
+ transition: all 150ms ease !important;
320
+ display: inline-block !important;
321
+ background-color: #ffffff !important;
322
+ border: 1px solid rgba(226, 232, 240, 0.8) !important;
323
+ color: #475569 !important;
324
+ }
325
+ .dark .submit-row a, .dark .submit-row .closelink {
326
+ background-color: #1e293b !important;
327
+ border-color: #334155 !important;
328
+ color: #cbd5e1 !important;
329
+ }
330
+ .submit-row a:hover, .submit-row .closelink:hover {
331
+ background-color: #f8fafc !important;
332
+ color: #0f172a !important;
333
+ }
334
+ .dark .submit-row a:hover, .dark .submit-row .closelink:hover {
335
+ background-color: #0f172a !important;
336
+ color: #ffffff !important;
337
+ }
338
+
339
+ /* Hide Django's default DateTimeShortcuts links and icons */
340
+ .datetimeshortcuts {
341
+ display: none !important;
342
+ }
343
+ /* Add Flatpickr custom styling to match our design tokens */
344
+ .flatpickr-calendar {
345
+ border-radius: 12px !important;
346
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05) !important;
347
+ border: 1px solid rgba(226, 232, 240, 0.8) !important;
348
+ font-family: inherit !important;
349
+ }
350
+ .dark .flatpickr-calendar {
351
+ border: 1px solid rgba(51, 65, 85, 0.5) !important;
352
+ background: #1e293b !important;
353
+ }
354
+ .flatpickr-day.selected {
355
+ background: {{ adminflow_primary_color|default:'#2563EB' }} !important;
356
+ border-color: {{ adminflow_primary_color|default:'#2563EB' }} !important;
357
+ }
358
+ .flatpickr-input[readonly] {
359
+ background-color: transparent !important;
360
+ }
361
+
362
+ /* Style Django's FilteredSelectMultiple selector widget */
363
+ .selector {
364
+ display: flex !important;
365
+ flex-direction: column !important;
366
+ gap: 16px !important;
367
+ width: 100% !important;
368
+ margin-top: 8px !important;
369
+ }
370
+ @media (min-width: 768px) {
371
+ .selector {
372
+ flex-direction: row !important;
373
+ align-items: center !important;
374
+ }
375
+ }
376
+ .selector-available, .selector-chosen {
377
+ flex: 1 !important;
378
+ min-width: 0 !important;
379
+ border: 1px solid rgba(226, 232, 240, 0.8) !important;
380
+ background-color: #ffffff !important;
381
+ border-radius: 12px !important;
382
+ padding: 16px !important;
383
+ box-shadow: 0 1px 3px rgba(0,0,0,0.05) !important;
384
+ }
385
+ .dark .selector-available, .dark .selector-chosen {
386
+ border: 1px solid rgba(51, 65, 85, 0.5) !important;
387
+ background-color: #0f172a !important;
388
+ }
389
+ .selector h2 {
390
+ font-size: 11px !important;
391
+ font-weight: 700 !important;
392
+ text-transform: uppercase !important;
393
+ letter-spacing: 0.05em !important;
394
+ color: #475569 !important;
395
+ margin-bottom: 8px !important;
396
+ border: none !important;
397
+ background: none !important;
398
+ padding: 0 !important;
399
+ }
400
+ .dark .selector h2 {
401
+ color: #94a3b8 !important;
402
+ }
403
+ .selector .selector-filter {
404
+ display: flex !important;
405
+ align-items: center !important;
406
+ border: 1px solid rgba(226, 232, 240, 0.8) !important;
407
+ border-radius: 8px !important;
408
+ padding: 4px 8px !important;
409
+ margin-bottom: 8px !important;
410
+ background-color: #f8fafc !important;
411
+ }
412
+ .dark .selector .selector-filter {
413
+ border-color: #334155 !important;
414
+ background-color: #1e293b !important;
415
+ }
416
+ .selector .selector-filter label {
417
+ margin-right: 6px !important;
418
+ display: flex !important;
419
+ align-items: center !important;
420
+ }
421
+ .selector .selector-filter input {
422
+ border: none !important;
423
+ background: transparent !important;
424
+ padding: 4px !important;
425
+ font-size: 12px !important;
426
+ outline: none !important;
427
+ width: 100% !important;
428
+ box-shadow: none !important;
429
+ }
430
+ .selector select {
431
+ width: 100% !important;
432
+ min-height: 150px !important;
433
+ border: 1px solid rgba(226, 232, 240, 0.8) !important;
434
+ border-radius: 8px !important;
435
+ padding: 8px !important;
436
+ font-size: 11px !important;
437
+ outline: none !important;
438
+ background-color: #ffffff !important;
439
+ color: #0f172a !important;
440
+ overflow-x: auto !important;
441
+ }
442
+ .dark .selector select {
443
+ border-color: #334155 !important;
444
+ background-color: #0f172a !important;
445
+ color: #e2e8f0 !important;
446
+ }
447
+ .selector select option {
448
+ width: max-content !important;
449
+ min-width: 100% !important;
450
+ padding: 4px 6px !important;
451
+ color: #0f172a !important;
452
+ }
453
+ .dark .selector select option {
454
+ color: #e2e8f0 !important;
455
+ }
456
+ .list-footer-display {
457
+ display: none !important;
458
+ }
459
+ .selector-chosen--with-filtered .list-footer-display {
460
+ display: flex !important;
461
+ align-items: center !important;
462
+ gap: 4px !important;
463
+ font-size: 11px !important;
464
+ color: #ef4444 !important;
465
+ margin-top: 8px !important;
466
+ font-weight: 500 !important;
467
+ }
468
+ .list-footer-display__clear {
469
+ color: {{ adminflow_primary_color|default:'#2563EB' }} !important;
470
+ text-decoration: underline !important;
471
+ cursor: pointer !important;
472
+ }
473
+ .selector-chooser {
474
+ display: flex !important;
475
+ flex-direction: row !important;
476
+ gap: 8px !important;
477
+ align-items: center !important;
478
+ justify-content: center !important;
479
+ list-style: none !important;
480
+ padding: 0 !important;
481
+ margin: 12px 0 !important;
482
+ }
483
+ @media (min-width: 768px) {
484
+ .selector-chooser {
485
+ flex-direction: column !important;
486
+ margin: 0 !important;
487
+ }
488
+ }
489
+ .selector-chooser li {
490
+ list-style: none !important;
491
+ }
492
+ .selector-chooser button, .selector-chooser a {
493
+ display: inline-flex !important;
494
+ align-items: center !important;
495
+ justify-content: center !important;
496
+ width: 32px !important;
497
+ height: 32px !important;
498
+ background-color: {{ adminflow_primary_color|default:'#2563EB' }} !important;
499
+ color: transparent !important;
500
+ font-size: 0 !important;
501
+ border-radius: 8px !important;
502
+ position: relative !important;
503
+ transition: all 0.2s !important;
504
+ border: none !important;
505
+ cursor: pointer !important;
506
+ padding: 0 !important;
507
+ }
508
+ .selector-chooser button:hover, .selector-chooser a:hover {
509
+ opacity: 0.9 !important;
510
+ transform: scale(1.05) !important;
511
+ }
512
+ .selector-chooser button.selector-add::before, .selector-chooser a.selector-add::before {
513
+ content: "→" !important;
514
+ position: absolute !important;
515
+ color: #ffffff !important;
516
+ font-size: 16px !important;
517
+ font-weight: bold !important;
518
+ }
519
+ .selector-chooser button.selector-remove::before, .selector-chooser a.selector-remove::before {
520
+ content: "←" !important;
521
+ position: absolute !important;
522
+ color: #ffffff !important;
523
+ font-size: 16px !important;
524
+ font-weight: bold !important;
525
+ }
526
+ .selector-chooser button.selector-chooseall::before, .selector-chooser a.selector-chooseall::before {
527
+ content: "»" !important;
528
+ position: absolute !important;
529
+ color: #ffffff !important;
530
+ font-size: 18px !important;
531
+ font-weight: bold !important;
532
+ }
533
+ .selector-chooser button.selector-clearall::before, .selector-chooser a.selector-clearall::before {
534
+ content: "«" !important;
535
+ position: absolute !important;
536
+ color: #ffffff !important;
537
+ font-size: 18px !important;
538
+ font-weight: bold !important;
539
+ }
540
+ button.selector-chooseall,
541
+ button.selector-clearall,
542
+ .selector-available a.selector-chooseall,
543
+ .selector-chosen a.selector-clearall {
544
+ display: inline-block !important;
545
+ font-size: 11px !important;
546
+ font-weight: bold !important;
547
+ color: {{ adminflow_primary_color|default:'#2563EB' }} !important;
548
+ margin-top: 8px !important;
549
+ text-decoration: none !important;
550
+ background: none !important;
551
+ border: none !important;
552
+ padding: 0 !important;
553
+ cursor: pointer !important;
554
+ }
555
+ button.selector-chooseall:hover,
556
+ button.selector-clearall:hover,
557
+ .selector-available a.selector-chooseall:hover,
558
+ .selector-chosen a.selector-clearall:hover {
559
+ text-decoration: underline !important;
560
+ }
561
+
562
+ /* Hide Django inline empty form template row */
563
+ .empty-form {
564
+ display: none !important;
565
+ }
566
+ /* Style dynamic Django inline Add row link as a premium button */
567
+ .add-row td {
568
+ padding: 12px 16px !important;
569
+ }
570
+ .add-row a {
571
+ display: inline-flex !important;
572
+ align-items: center !important;
573
+ gap: 6px !important;
574
+ background-color: {{ adminflow_primary_color|default:'#2563EB' }} !important;
575
+ color: #ffffff !important;
576
+ font-weight: 600 !important;
577
+ font-size: 11px !important;
578
+ padding: 6px 12px !important;
579
+ border-radius: 8px !important;
580
+ text-decoration: none !important;
581
+ transition: opacity 0.2s !important;
582
+ }
583
+ .add-row a:hover {
584
+ opacity: 0.9 !important;
585
+ }
586
+ .add-row a::before {
587
+ content: "+" !important;
588
+ font-size: 14px !important;
589
+ font-weight: bold !important;
590
+ line-height: 1 !important;
591
+ }
592
+ </style>
593
+ {% block extrahead %}{% endblock %}
594
+ {% block extrastyle %}{% endblock %}
595
+ </head>
596
+ <body class="{% block bodyclass %}{% endblock %} bg-surface text-on-surface font-sans min-h-screen flex flex-col custom-scrollbar overflow-x-hidden">
597
+
598
+ {% if not is_popup and user.is_authenticated %}
599
+ <!-- Mobile Sidebar Backdrop -->
600
+ <div id="sidebar-backdrop" class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm z-40 hidden md:hidden" onclick="toggleMobileSidebar()"></div>
601
+
602
+ <!-- Sidebar Navigation -->
603
+ <aside id="admin-sidebar" style="color: {{ adminflow_sidebar_text_color|default:'#94a3b8' }};" class="w-[{{ adminflow_sidebar_width|default:'280' }}px] bg-sidebar-bg fixed top-0 bottom-0 left-0 -translate-x-full md:translate-x-0 md:flex flex-col z-50 transition-all duration-300 flex">
604
+ <!-- Sidebar Brand Header -->
605
+ <div class="h-16 flex items-center justify-between px-6 border-b border-slate-700/50 shrink-0 brand-header">
606
+ <a href="{% url 'admin:index' %}" class="flex items-center gap-3 font-semibold tracking-tight" style="color: {{ adminflow_sidebar_active_text_color|default:'#ffffff' }};" title="{{ adminflow_title }}">
607
+ {% if adminflow_logo %}
608
+ <img src="{{ adminflow_logo }}" alt="{{ adminflow_title }}" class="h-8 w-auto shrink-0"/>
609
+ {% else %}
610
+ <div class="h-9 w-9 rounded-xl bg-primary flex items-center justify-center text-white shrink-0 shadow-md">
611
+ <span class="material-symbols-outlined text-[22px]">hub</span>
612
+ </div>
613
+ {% endif %}
614
+ <span class="text-lg sidebar-text font-bold tracking-tight">{{ adminflow_title }}</span>
615
+ </a>
616
+
617
+ {% if adminflow_sidebar_collapsible %}
618
+ <button onclick="toggleSidebar()" class="p-1.5 rounded-lg hover:bg-slate-800/60 transition-all opacity-80 hover:opacity-100 sidebar-toggle-btn" title="{% translate 'Toggle Sidebar' %}">
619
+ <span class="material-symbols-outlined text-[20px]" id="sidebar-toggle-icon">first_page</span>
620
+ </button>
621
+ {% endif %}
622
+ </div>
623
+
624
+ <!-- Sidebar Menu Items -->
625
+ <nav class="flex-1 overflow-y-auto py-4 px-3 space-y-1.5 custom-scrollbar">
626
+ {% get_admin_app_list as apps %}
627
+
628
+ <a href="{% url 'admin:index' %}" class="sidebar-link flex items-center gap-3 px-4 py-2.5 rounded-xl transition-all hover:bg-slate-800/50 {% if request.path == '/admin/' %}bg-primary shadow-md{% endif %}" style="color: {% if request.path == '/admin/' %}{{ adminflow_sidebar_active_text_color|default:'#ffffff' }}{% else %}{{ adminflow_sidebar_text_color|default:'#94a3b8' }}{% endif %};" title="{% translate 'Dashboard' %}">
629
+ <span class="material-symbols-outlined text-[22px] shrink-0">dashboard</span>
630
+ <span class="text-body-sm sidebar-text font-medium">{% translate "Dashboard" %}</span>
631
+ </a>
632
+
633
+ {% for app in apps %}
634
+ {% if app.app_label != 'otp_email' and app.app_label != 'otp_static' and app.app_label != 'otp_totp' %}
635
+ <div class="space-y-1 pt-2 sidebar-app-group" id="sidebar-app-{{ app.app_label }}">
636
+ <div class="flex items-center justify-between px-4 py-1.5 text-[11px] font-bold uppercase tracking-wider sidebar-text opacity-50">
637
+ <span>{{ app.name }}</span>
638
+ </div>
639
+
640
+ {% for model in app.models %}
641
+ {% if model.object_name != 'UserInformation' and model.object_name != 'EmailDevice' and model.object_name != 'StaticDevice' and model.object_name != 'TOTPDevice' %}
642
+ {% url 'admin:'|add:app.app_label|add:'_'|add:model.object_name|lower|add:'_changelist' as model_changelist_url %}
643
+ <a href="{{ model_changelist_url }}" class="sidebar-link flex items-center gap-3 px-4 py-2.5 rounded-xl transition-all hover:bg-slate-800/50 {% if request.path == model_changelist_url %}bg-primary shadow-md{% endif %}" style="color: {% if request.path == model_changelist_url %}{{ adminflow_sidebar_active_text_color|default:'#ffffff' }}{% else %}{{ adminflow_sidebar_text_color|default:'#94a3b8' }}{% endif %};" title="{{ model.name }}">
644
+ <span class="material-symbols-outlined text-[22px] shrink-0">{{ model|adminflow_icon }}</span>
645
+ <span class="text-body-sm sidebar-text font-medium">{{ model.name }}</span>
646
+ </a>
647
+ {% endif %}
648
+ {% endfor %}
649
+ </div>
650
+ {% endif %}
651
+ {% empty %}
652
+ <p class="text-xs opacity-60 px-4">{% translate "No applications available" %}</p>
653
+ {% endfor %}
654
+
655
+ <!-- Quick System Utilities -->
656
+ <div class="pt-4 border-t border-slate-700/30 sidebar-app-group">
657
+ <div class="px-4 py-1 text-[11px] font-bold uppercase tracking-wider sidebar-text opacity-50">System</div>
658
+
659
+ <a href="{% url 'admin:password_change' %}" class="sidebar-link flex items-center gap-3 px-4 py-2.5 rounded-xl transition-all hover:bg-slate-800/50 {% if request.path == '/admin/password_change/' %}bg-primary shadow-md{% endif %}" style="color: {% if request.path == '/admin/password_change/' %}{{ adminflow_sidebar_active_text_color|default:'#ffffff' }}{% else %}{{ adminflow_sidebar_text_color|default:'#94a3b8' }}{% endif %};" title="{% translate 'Change Password' %}">
660
+ <span class="material-symbols-outlined text-[22px] shrink-0">password</span>
661
+ <span class="text-body-sm sidebar-text font-medium">{% translate "Change Password" %}</span>
662
+ </a>
663
+ </div>
664
+ </nav>
665
+
666
+ <!-- Sidebar Footer -->
667
+ <div class="p-3 border-t border-slate-700/50 flex flex-col gap-2 shrink-0 sidebar-footer">
668
+ <form method="post" action="{% url 'admin:logout' %}" class="w-full">
669
+ {% csrf_token %}
670
+ <button type="submit" class="sidebar-link w-full flex items-center gap-3 px-4 py-2.5 rounded-xl text-rose-400 hover:bg-rose-900/20 hover:text-rose-300 transition-all text-left" title="{% translate 'Log Out' %}">
671
+ <span class="material-symbols-outlined text-[22px] shrink-0">logout</span>
672
+ <span class="text-body-sm sidebar-text font-medium">{% translate "Log Out" %}</span>
673
+ </button>
674
+ </form>
675
+
676
+ <div class="px-4 text-[10px] text-slate-500 sidebar-text">
677
+ &copy; {{ adminflow_company }}
678
+ </div>
679
+ </div>
680
+ </aside>
681
+ {% endif %}
682
+
683
+ <!-- Content Shell -->
684
+ <div class="flex-1 flex flex-col min-h-screen {% if not is_popup and user.is_authenticated %}md:ml-[{{ adminflow_sidebar_width|default:'280' }}px]{% endif %} transition-all duration-300" id="main-content-shell">
685
+ {% if not is_popup and user.is_authenticated %}
686
+ <!-- Header / Top Bar -->
687
+ <header class="sticky top-0 z-40 flex justify-between items-center px-3 md:px-6 h-14 md:h-16 w-full bg-surface-lowest border-b border-outline-variant shadow-sm">
688
+ <div class="flex items-center gap-2 md:gap-4 flex-1">
689
+ <!-- Mobile Sidebar Toggle -->
690
+ <button onclick="toggleMobileSidebar()" class="md:hidden p-2 rounded-lg hover:bg-surface-low transition-colors text-on-surface-variant">
691
+ <span class="material-symbols-outlined">menu</span>
692
+ </button>
693
+
694
+ <!-- Mobile Brand (visible only on mobile) -->
695
+ <a href="{% url 'admin:index' %}" class="md:hidden flex items-center gap-2 font-bold text-on-surface text-sm tracking-tight">
696
+ {% if adminflow_logo %}
697
+ <img src="{{ adminflow_logo }}" alt="{{ adminflow_title }}" class="h-6 w-auto"/>
698
+ {% else %}
699
+ <div class="h-7 w-7 rounded-md bg-primary flex items-center justify-center text-white">
700
+ <span class="material-symbols-outlined text-[18px]">hub</span>
701
+ </div>
702
+ {% endif %}
703
+ {{ adminflow_title }}
704
+ </a>
705
+
706
+ {% if adminflow_enable_global_search %}
707
+ <!-- Global Search (hidden on mobile) -->
708
+ <div class="relative w-full max-w-md cursor-pointer hidden md:block" onclick="openCommandPalette()">
709
+ <span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant opacity-60">search</span>
710
+ <input type="text" readonly class="w-full pl-10 pr-4 py-1.5 rounded-lg border border-outline-variant bg-surface-low focus:outline-none text-body-sm cursor-pointer" placeholder="Search resources, actions (Cmd + K)"/>
711
+ </div>
712
+ <!-- Mobile Search Button -->
713
+ <button onclick="openCommandPalette()" class="md:hidden ml-auto p-2 rounded-lg hover:bg-surface-low transition-colors text-on-surface-variant">
714
+ <span class="material-symbols-outlined text-[20px]">search</span>
715
+ </button>
716
+ {% endif %}
717
+ </div>
718
+
719
+ <!-- User Options / Badges -->
720
+ <div class="flex items-center gap-3">
721
+ {% if adminflow_enable_notifications %}
722
+ {% get_admin_log 5 as notification_log for_user user %}
723
+ <div class="relative" id="notifications-dropdown-container">
724
+ <button onclick="toggleNotificationsDropdown()" class="p-2 rounded-full hover:bg-surface-high transition-colors text-on-surface-variant relative focus:outline-none">
725
+ <span class="material-symbols-outlined">notifications</span>
726
+ {% if notification_log %}
727
+ <span class="absolute top-1.5 right-1.5 w-2 h-2 rounded-full bg-danger"></span>
728
+ {% endif %}
729
+ </button>
730
+ <!-- Notifications Dropdown Popover -->
731
+ <div id="notifications-dropdown" class="absolute right-0 mt-2 w-80 bg-surface-lowest border border-outline-variant rounded-2xl shadow-2xl z-[9999] hidden flex-col overflow-hidden animate-in fade-in slide-in-from-top-2 duration-150">
732
+ <div class="p-4 border-b border-outline-variant bg-surface-low/30 flex items-center justify-between">
733
+ <h4 class="text-xs font-bold text-on-surface uppercase tracking-wider flex items-center gap-1.5">
734
+ <span class="material-symbols-outlined text-[16px] text-primary">notifications</span>
735
+ {% translate 'Recent Actions' %}
736
+ </h4>
737
+ {% if notification_log %}
738
+ <span class="text-[9px] font-black bg-danger/10 text-danger px-1.5 py-0.5 rounded-full">NEW</span>
739
+ {% endif %}
740
+ </div>
741
+ <div class="max-h-72 overflow-y-auto divide-y divide-outline-variant/40">
742
+ {% if notification_log %}
743
+ {% for entry in notification_log %}
744
+ {% if entry.get_admin_url %}
745
+ <a href="{{ entry.get_admin_url }}" class="p-3.5 hover:bg-surface-low/20 transition-colors flex items-start gap-3 border-none block">
746
+ {% else %}
747
+ <div class="p-3.5 flex items-start gap-3 opacity-60">
748
+ {% endif %}
749
+ <div class="w-7 h-7 rounded-lg flex items-center justify-center shrink-0 {% if entry.is_addition %}bg-success/15 text-success{% elif entry.is_change %}bg-primary/15 text-primary{% else %}bg-danger/15 text-danger{% endif %}">
750
+ <span class="material-symbols-outlined text-[16px]">
751
+ {% if entry.is_addition %}add_circle{% elif entry.is_change %}edit{% else %}delete{% endif %}
752
+ </span>
753
+ </div>
754
+ <div class="space-y-0.5 text-left">
755
+ <p class="text-[11px] font-bold text-on-surface leading-tight">
756
+ {% if entry.is_addition %}Added{% elif entry.is_change %}Changed{% else %}Deleted{% endif %} {{ entry.object_repr }}
757
+ </p>
758
+ <p class="text-[10px] text-on-surface-variant/80">
759
+ {{ entry.content_type.name|capfirst }}
760
+ </p>
761
+ <p class="text-[9px] text-on-surface-variant/60 font-medium">
762
+ {{ entry.action_time|date:"M d, g:i a" }}
763
+ </p>
764
+ </div>
765
+ {% if entry.get_admin_url %}
766
+ </a>
767
+ {% else %}
768
+ </div>
769
+ {% endif %}
770
+ {% endfor %}
771
+ {% else %}
772
+ <div class="p-6 text-center text-on-surface-variant/70 space-y-2">
773
+ <span class="material-symbols-outlined text-3xl opacity-30">notifications_off</span>
774
+ <p class="text-xs font-medium">No recent actions logged</p>
775
+ </div>
776
+ {% endif %}
777
+ </div>
778
+ </div>
779
+ </div>
780
+ {% endif %}
781
+
782
+ <div class="h-8 w-px bg-outline-variant mx-1"></div>
783
+
784
+ <!-- Profile Dropdown -->
785
+ <div class="relative" id="profile-dropdown-container">
786
+ <button onclick="toggleProfileDropdown()" class="flex items-center gap-2 pl-2 focus:outline-none group">
787
+ <div class="text-right hidden sm:block">
788
+ <p class="text-xs font-bold text-on-surface">{{ user.get_username|upper }}</p>
789
+ <p class="text-[10px] text-on-surface-variant">{% if user.is_superuser %}Superuser{% else %}Staff{% endif %}</p>
790
+ </div>
791
+ <div class="w-8 h-8 rounded-full bg-primary/20 flex items-center justify-center text-primary font-bold overflow-hidden border border-primary/20">
792
+ {{ user.get_username|slice:":2"|upper }}
793
+ </div>
794
+ </button>
795
+
796
+ <div id="profile-dropdown" class="hidden absolute right-0 mt-2 w-48 bg-surface-lowest border border-outline-variant rounded-xl shadow-lg py-2 z-50">
797
+ <div class="px-4 py-2 border-b border-outline-variant">
798
+ <p class="text-xs font-semibold text-on-surface">Signed in as</p>
799
+ <p class="text-xs text-on-surface-variant truncate font-mono">{{ user.email|default:user.get_username }}</p>
800
+ </div>
801
+ {% if site_url %}
802
+ <a href="{{ site_url }}" class="flex items-center gap-2 px-4 py-2 text-xs text-on-surface hover:bg-surface-low"><span class="material-symbols-outlined text-sm">open_in_new</span>{% translate "View Site" %}</a>
803
+ {% endif %}
804
+ <a href="{% url 'admin:password_change' %}" class="flex items-center gap-2 px-4 py-2 text-xs text-on-surface hover:bg-surface-low"><span class="material-symbols-outlined text-sm">settings</span>{% translate "Change Password" %}</a>
805
+ <form method="post" action="{% url 'admin:logout' %}">
806
+ {% csrf_token %}
807
+ <button type="submit" class="w-full flex items-center gap-2 px-4 py-2 text-xs text-rose-500 hover:bg-rose-50 text-left">
808
+ <span class="material-symbols-outlined text-sm">logout</span>{% translate "Log Out" %}
809
+ </button>
810
+ </form>
811
+ </div>
812
+ </div>
813
+ </div>
814
+ </header>
815
+ {% endif %}
816
+
817
+ <!-- Main Breadcrumbs & Alert Messages Banner -->
818
+ <div class="px-3 py-3 md:px-6 md:py-4 flex-1 flex flex-col">
819
+ {% block breadcrumbs %}
820
+ {% endblock %}
821
+
822
+ {% block messages %}
823
+ {% include "admin/includes/toast.html" %}
824
+ {% endblock %}
825
+
826
+ <!-- Actual Main Content -->
827
+ {% block content %}
828
+ {% endblock %}
829
+ </div>
830
+ </div>
831
+
832
+ {% if adminflow_enable_command_palette %}
833
+ <!-- Command Palette Backdrop -->
834
+ <div id="command-palette" class="hidden fixed inset-0 bg-slate-900/60 backdrop-filter backdrop-blur-sm z-50 flex items-start justify-center pt-[10vh]">
835
+ <div class="bg-surface-lowest border border-outline-variant w-full max-w-xl rounded-2xl shadow-2xl overflow-hidden animate-in fade-in zoom-in-95 duration-100">
836
+ <!-- Search bar -->
837
+ <div class="flex items-center gap-3 px-4 border-b border-outline-variant">
838
+ <span class="material-symbols-outlined text-on-surface-variant opacity-60">search</span>
839
+ <input type="text" id="command-palette-input" class="w-full py-4 bg-transparent outline-none border-none text-body-md text-on-surface focus:ring-0" placeholder="Type a command or model name..."/>
840
+ <kbd class="px-2 py-1 bg-surface-low border border-outline-variant rounded text-[10px] text-on-surface-variant uppercase font-mono">esc</kbd>
841
+ </div>
842
+
843
+ <!-- Quick list -->
844
+ <div class="p-3 max-h-[300px] overflow-y-auto custom-scrollbar space-y-1" id="command-palette-results">
845
+ <div class="px-3 py-1.5 text-[11px] font-bold text-on-surface-variant opacity-60 uppercase tracking-wider">Navigation</div>
846
+ <a href="{% url 'admin:index' %}" class="flex items-center justify-between px-3 py-2 rounded-lg hover:bg-primary/10 hover:text-primary transition-all text-body-sm">
847
+ <span class="flex items-center gap-3"><span class="material-symbols-outlined text-sm">dashboard</span>Go to Dashboard</span>
848
+ <span class="text-xs text-on-surface-variant font-mono">⌘D</span>
849
+ </a>
850
+
851
+ <div class="px-3 py-1.5 pt-3 text-[11px] font-bold text-on-surface-variant opacity-60 uppercase tracking-wider">Registered Models</div>
852
+ {% for app in apps %}
853
+ {% for model in app.models %}
854
+ <a href="{{ model.admin_url }}" class="flex items-center justify-between px-3 py-2 rounded-lg hover:bg-primary/10 hover:text-primary transition-all text-body-sm search-item" data-search="{{ model.name }} {{ app.name }}">
855
+ <span class="flex items-center gap-3"><span class="material-symbols-outlined text-sm">{{ model.object_name|adminflow_icon }}</span>Manage {{ model.name }}</span>
856
+ <span class="text-xs text-on-surface-variant font-mono">{{ app.name }}</span>
857
+ </a>
858
+ {% endfor %}
859
+ {% endfor %}
860
+ </div>
861
+ </div>
862
+ </div>
863
+ {% endif %}
864
+
865
+ <!-- Core Scripts -->
866
+ <script>
867
+ // Collapsible Sidebar logic
868
+ let sidebarCollapsed = localStorage.getItem('sidebar_collapsed') === 'true';
869
+ const sidebar = document.getElementById('admin-sidebar');
870
+ const mainContent = document.getElementById('main-content-shell');
871
+ const sidebarIcon = document.getElementById('sidebar-toggle-icon');
872
+ const sidebarTexts = document.querySelectorAll('.sidebar-text');
873
+
874
+ function applySidebarState() {
875
+ if (!sidebar) return;
876
+ const isDesktop = window.innerWidth >= 768;
877
+ if (sidebarCollapsed && isDesktop) {
878
+ sidebar.classList.add('sidebar-collapsed');
879
+ sidebar.style.width = '64px';
880
+ if (mainContent) mainContent.style.marginLeft = '64px';
881
+ if (sidebarIcon) sidebarIcon.textContent = 'menu_open';
882
+ sidebarTexts.forEach(el => el.classList.add('hidden'));
883
+ } else {
884
+ sidebar.classList.remove('sidebar-collapsed');
885
+ sidebar.style.width = '{{ adminflow_sidebar_width|default:"280" }}px';
886
+ if (mainContent) mainContent.style.marginLeft = isDesktop ? '{{ adminflow_sidebar_width|default:"280" }}px' : '0px';
887
+ if (sidebarIcon) sidebarIcon.textContent = 'first_page';
888
+ sidebarTexts.forEach(el => el.classList.remove('hidden'));
889
+ }
890
+ }
891
+
892
+ function toggleSidebar() {
893
+ sidebarCollapsed = !sidebarCollapsed;
894
+ localStorage.setItem('sidebar_collapsed', sidebarCollapsed);
895
+ applySidebarState();
896
+ }
897
+
898
+ // Hover auto-expand sidebar when collapsed
899
+ if (sidebar) {
900
+ sidebar.addEventListener('mouseenter', () => {
901
+ if (sidebarCollapsed && window.innerWidth >= 768) {
902
+ sidebar.classList.remove('sidebar-collapsed');
903
+ sidebar.style.width = '{{ adminflow_sidebar_width|default:"280" }}px';
904
+ sidebar.classList.add('shadow-2xl');
905
+ sidebarTexts.forEach(el => el.classList.remove('hidden'));
906
+ }
907
+ });
908
+
909
+ sidebar.addEventListener('mouseleave', () => {
910
+ if (sidebarCollapsed && window.innerWidth >= 768) {
911
+ sidebar.classList.add('sidebar-collapsed');
912
+ sidebar.style.width = '64px';
913
+ sidebar.classList.remove('shadow-2xl');
914
+ sidebarTexts.forEach(el => el.classList.add('hidden'));
915
+ }
916
+ });
917
+ }
918
+
919
+ // Initial Sidebar Apply & Resize Listener
920
+ applySidebarState();
921
+ window.addEventListener('resize', applySidebarState);
922
+
923
+ // Mobile sidebar control
924
+ const sidebarBackdrop = document.getElementById('sidebar-backdrop');
925
+ let mobileSidebarOpen = false;
926
+
927
+ function toggleMobileSidebar() {
928
+ if (!sidebar) return;
929
+ mobileSidebarOpen = !mobileSidebarOpen;
930
+ if (mobileSidebarOpen) {
931
+ sidebar.classList.remove('-translate-x-full');
932
+ sidebar.classList.add('translate-x-0');
933
+ if (sidebarBackdrop) sidebarBackdrop.classList.remove('hidden');
934
+ document.body.style.overflow = 'hidden';
935
+ } else {
936
+ sidebar.classList.add('-translate-x-full');
937
+ sidebar.classList.remove('translate-x-0');
938
+ if (sidebarBackdrop) sidebarBackdrop.classList.add('hidden');
939
+ document.body.style.overflow = '';
940
+ }
941
+ }
942
+
943
+ // Close mobile sidebar on link click
944
+ if (sidebar && window.innerWidth < 768) {
945
+ sidebar.querySelectorAll('a').forEach(link => {
946
+ link.addEventListener('click', () => {
947
+ if (mobileSidebarOpen) toggleMobileSidebar();
948
+ });
949
+ });
950
+ }
951
+
952
+ // Notifications & Profile Dropdown logic
953
+ function toggleNotificationsDropdown() {
954
+ const dropdown = document.getElementById('notifications-dropdown');
955
+ if (dropdown) dropdown.classList.toggle('hidden');
956
+ const profileDropdown = document.getElementById('profile-dropdown');
957
+ if (profileDropdown) profileDropdown.classList.add('hidden');
958
+ }
959
+
960
+ function toggleProfileDropdown() {
961
+ const dropdown = document.getElementById('profile-dropdown');
962
+ if (dropdown) dropdown.classList.toggle('hidden');
963
+ const notificationsDropdown = document.getElementById('notifications-dropdown');
964
+ if (notificationsDropdown) notificationsDropdown.classList.add('hidden');
965
+ }
966
+
967
+ window.addEventListener('click', function(e) {
968
+ const profileDropdown = document.getElementById('profile-dropdown');
969
+ if (profileDropdown && !e.target.closest('#profile-dropdown-container')) {
970
+ profileDropdown.classList.add('hidden');
971
+ }
972
+ const notificationsDropdown = document.getElementById('notifications-dropdown');
973
+ if (notificationsDropdown && !e.target.closest('#notifications-dropdown-container')) {
974
+ notificationsDropdown.classList.add('hidden');
975
+ }
976
+ });
977
+
978
+ // Dark Mode Logic
979
+ let darkMode = localStorage.getItem('dark_mode') === 'true';
980
+ if (darkMode || (localStorage.getItem('dark_mode') === null && window.matchMedia('(prefers-color-scheme: dark)').matches && "{{ adminflow_theme }}" === "system")) {
981
+ document.documentElement.classList.add('dark');
982
+ } else {
983
+ document.documentElement.classList.remove('dark');
984
+ }
985
+
986
+ function toggleDarkMode() {
987
+ darkMode = !darkMode;
988
+ localStorage.setItem('dark_mode', darkMode);
989
+ if (darkMode) {
990
+ document.documentElement.classList.add('dark');
991
+ } else {
992
+ document.documentElement.classList.remove('dark');
993
+ }
994
+ }
995
+
996
+ // Command Palette Logic
997
+ const cmdPalette = document.getElementById('command-palette');
998
+ const cmdInput = document.getElementById('command-palette-input');
999
+
1000
+ function openCommandPalette() {
1001
+ if (cmdPalette) {
1002
+ cmdPalette.classList.remove('hidden');
1003
+ if (cmdInput) cmdInput.focus();
1004
+ }
1005
+ }
1006
+
1007
+ function closeCommandPalette() {
1008
+ if (cmdPalette) cmdPalette.classList.add('hidden');
1009
+ }
1010
+
1011
+ // Hotkey cmd+k or ctrl+k
1012
+ window.addEventListener('keydown', function(e) {
1013
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
1014
+ e.preventDefault();
1015
+ openCommandPalette();
1016
+ }
1017
+ if (e.key === 'Escape') {
1018
+ closeCommandPalette();
1019
+ }
1020
+ });
1021
+
1022
+ if (cmdPalette) {
1023
+ cmdPalette.addEventListener('click', function(e) {
1024
+ if (e.target === cmdPalette) {
1025
+ closeCommandPalette();
1026
+ }
1027
+ });
1028
+ }
1029
+
1030
+ // Command Palette Search filter
1031
+ if (cmdInput) {
1032
+ cmdInput.addEventListener('input', function(e) {
1033
+ const query = e.target.value.toLowerCase();
1034
+ const items = document.querySelectorAll('.search-item');
1035
+ items.forEach(item => {
1036
+ const searchData = item.getAttribute('data-search').toLowerCase();
1037
+ if (searchData.includes(query)) {
1038
+ item.style.display = 'flex';
1039
+ } else {
1040
+ item.style.display = 'none';
1041
+ }
1042
+ });
1043
+ });
1044
+ }
1045
+
1046
+ // Dynamic format conversion utility
1047
+ function convertMomentToFlatpickr(format) {
1048
+ if (!format) return "M j, Y";
1049
+ return format
1050
+ .replace(/MMMM/g, 'F') // July
1051
+ .replace(/MMM/g, 'M') // Jul
1052
+ .replace(/MM/g, 'm') // 07
1053
+ .replace(/M/g, 'n') // 7
1054
+ .replace(/dddd/g, 'l') // Wednesday
1055
+ .replace(/ddd/g, 'D') // Wed
1056
+ .replace(/dd/g, 'd') // 22
1057
+ .replace(/d/g, 'j') // 22
1058
+ .replace(/yyyy/g, 'Y') // 2026
1059
+ .replace(/yy/g, 'y') // 26
1060
+ .replace(/HH/g, 'H') // 24hr with leading zero
1061
+ .replace(/H/g, 'G') // 24hr without leading zero
1062
+ .replace(/hh/g, 'h') // 12hr with leading zero
1063
+ .replace(/h/g, 'g') // 12hr without leading zero
1064
+ .replace(/mm/g, 'i') // minutes
1065
+ .replace(/ss/g, 'S') // seconds
1066
+ .replace(/a/g, 'K') // AM/PM
1067
+ .replace(/A/g, 'K'); // AM/PM
1068
+ }
1069
+
1070
+ // Flatpickr Initializer
1071
+ function initFlatpickr(container = document) {
1072
+ // Bind Flatpickr to Date fields (.vDateField)
1073
+ container.querySelectorAll('.vDateField').forEach(el => {
1074
+ flatpickr(el, {
1075
+ dateFormat: "Y-m-d",
1076
+ allowInput: true,
1077
+ altInput: true,
1078
+ altFormat: convertMomentToFlatpickr("{{ adminflow_date_format|default:'MMM dd, yyyy' }}"),
1079
+ });
1080
+ });
1081
+
1082
+ // Bind Flatpickr to Time fields (.vTimeField)
1083
+ container.querySelectorAll('.vTimeField').forEach(el => {
1084
+ flatpickr(el, {
1085
+ enableTime: true,
1086
+ noCalendar: true,
1087
+ dateFormat: "H:i:S",
1088
+ allowInput: true,
1089
+ altInput: true,
1090
+ altFormat: convertMomentToFlatpickr("{% if adminflow_time_format == '12h' %}hh:mm a{% else %}HH:mm{% endif %}"),
1091
+ });
1092
+ });
1093
+ }
1094
+
1095
+ // Tooltip adder for selector options
1096
+ function addSelectorOptionsTooltip() {
1097
+ document.querySelectorAll('.selector select option').forEach(opt => {
1098
+ if (!opt.hasAttribute('title')) {
1099
+ opt.setAttribute('title', opt.textContent.trim());
1100
+ }
1101
+ });
1102
+ }
1103
+
1104
+ // Tooltip adder for chooser buttons
1105
+ function setupChooserTooltips() {
1106
+ document.querySelectorAll('.selector-chooser button, .selector-chooser a').forEach(btn => {
1107
+ const text = btn.textContent.trim();
1108
+ if (text && !btn.hasAttribute('title')) {
1109
+ btn.setAttribute('title', text);
1110
+ }
1111
+ });
1112
+ }
1113
+
1114
+ // Initialize flatpickr on page load
1115
+ document.addEventListener("DOMContentLoaded", function() {
1116
+ initFlatpickr();
1117
+ updateFlatpickrTheme(document.documentElement.classList.contains('dark'));
1118
+ addSelectorOptionsTooltip();
1119
+ setupChooserTooltips();
1120
+
1121
+ // Re-bind when dynamic inline forms are added
1122
+ if (window.django && window.django.jQuery) {
1123
+ django.jQuery(document).on('formset:added', function(event, $row) {
1124
+ initFlatpickr($row[0]);
1125
+ });
1126
+ }
1127
+
1128
+ // Also re-apply tooltips when moving items via click
1129
+ document.addEventListener('click', function(e) {
1130
+ if (e.target.closest('.selector')) {
1131
+ setTimeout(function() {
1132
+ addSelectorOptionsTooltip();
1133
+ setupChooserTooltips();
1134
+ }, 50);
1135
+ }
1136
+ });
1137
+
1138
+ // Custom Tooltip Element Initialization
1139
+ const tooltipEl = document.createElement('div');
1140
+ tooltipEl.id = 'adminflow-custom-tooltip';
1141
+ tooltipEl.className = 'fixed hidden z-[9999] px-3 py-1.5 bg-slate-900 text-white text-[11px] font-semibold rounded-lg shadow-lg pointer-events-none opacity-0 transition-opacity duration-150 border border-slate-700/60 max-w-sm whitespace-pre-wrap';
1142
+ document.body.appendChild(tooltipEl);
1143
+
1144
+ let tooltipTimeout;
1145
+
1146
+ document.addEventListener('mouseover', function(e) {
1147
+ // Ignore standard dropdown option tags since they do not trigger mouse events reliably in Chrome/Safari
1148
+ if (e.target.tagName === 'OPTION') return;
1149
+
1150
+ const target = e.target.closest('[title], [data-custom-tooltip], input, textarea, select, .selector-chooser button, .selector-chooser a');
1151
+ if (!target) return;
1152
+
1153
+ // Dynamically prepare tooltips for dropdowns and inputs
1154
+ if (target.matches('select')) {
1155
+ if (target.multiple) {
1156
+ const selectedText = Array.from(target.selectedOptions).map(o => o.textContent.trim()).join(', ');
1157
+ target.setAttribute('data-custom-tooltip', selectedText);
1158
+ } else if (target.selectedIndex !== -1) {
1159
+ const selectedOption = target.options[target.selectedIndex];
1160
+ if (selectedOption) {
1161
+ target.setAttribute('data-custom-tooltip', selectedOption.textContent.trim());
1162
+ }
1163
+ }
1164
+ } else if (target.matches('input[type="text"], input[type="number"], input[type="email"], input[type="url"], input[type="date"], input:not([type]), textarea')) {
1165
+ target.setAttribute('data-custom-tooltip', target.value.trim());
1166
+ } else if (target.matches('.selector-chooser button, .selector-chooser a')) {
1167
+ const text = target.textContent.trim();
1168
+ if (text) target.setAttribute('data-custom-tooltip', text);
1169
+ } else if (target.hasAttribute('title')) {
1170
+ const titleText = target.getAttribute('title');
1171
+ if (titleText) {
1172
+ target.setAttribute('data-custom-tooltip', titleText);
1173
+ target.removeAttribute('title');
1174
+ }
1175
+ }
1176
+
1177
+ const tooltipText = target.getAttribute('data-custom-tooltip');
1178
+ if (!tooltipText || !tooltipText.trim()) return;
1179
+
1180
+ clearTimeout(tooltipTimeout);
1181
+ tooltipTimeout = setTimeout(function() {
1182
+ tooltipEl.textContent = tooltipText;
1183
+ tooltipEl.classList.remove('hidden');
1184
+
1185
+ // Position immediately near cursor (handles static hover positions)
1186
+ const tooltipWidth = tooltipEl.offsetWidth;
1187
+ const tooltipHeight = tooltipEl.offsetHeight;
1188
+
1189
+ let top = e.clientY + 14;
1190
+ let left = e.clientX + 14;
1191
+
1192
+ if (left + tooltipWidth > window.innerWidth) {
1193
+ left = e.clientX - tooltipWidth - 10;
1194
+ }
1195
+ if (top + tooltipHeight > window.innerHeight) {
1196
+ top = e.clientY - tooltipHeight - 10;
1197
+ }
1198
+
1199
+ tooltipEl.style.top = top + 'px';
1200
+ tooltipEl.style.left = left + 'px';
1201
+ tooltipEl.style.opacity = '1';
1202
+ }, 50); // Fast 50ms display delay
1203
+ });
1204
+
1205
+ document.addEventListener('mousemove', function(e) {
1206
+ if (tooltipEl.classList.contains('hidden')) return;
1207
+
1208
+ const tooltipWidth = tooltipEl.offsetWidth;
1209
+ const tooltipHeight = tooltipEl.offsetHeight;
1210
+
1211
+ let top = e.clientY + 14;
1212
+ let left = e.clientX + 14;
1213
+
1214
+ // Viewport boundaries checks
1215
+ if (left + tooltipWidth > window.innerWidth) {
1216
+ left = e.clientX - tooltipWidth - 10;
1217
+ }
1218
+ if (top + tooltipHeight > window.innerHeight) {
1219
+ top = e.clientY - tooltipHeight - 10;
1220
+ }
1221
+
1222
+ tooltipEl.style.top = top + 'px';
1223
+ tooltipEl.style.left = left + 'px';
1224
+ });
1225
+
1226
+ function hideCustomTooltip() {
1227
+ clearTimeout(tooltipTimeout);
1228
+ tooltipEl.style.opacity = '0';
1229
+ tooltipEl.classList.add('hidden');
1230
+ }
1231
+
1232
+ document.addEventListener('mouseout', function(e) {
1233
+ if (e.target.closest('[data-custom-tooltip], [title], input, textarea, select, .selector-chooser button, .selector-chooser a')) {
1234
+ hideCustomTooltip();
1235
+ }
1236
+ });
1237
+
1238
+ // Dismiss tooltip immediately on click or scroll
1239
+ document.addEventListener('mousedown', hideCustomTooltip);
1240
+ document.addEventListener('scroll', hideCustomTooltip, true);
1241
+ });
1242
+
1243
+ // Swap Flatpickr themes when toggling dark mode
1244
+ function updateFlatpickrTheme(isDark) {
1245
+ const themeLink = document.getElementById('flatpickr-theme');
1246
+ if (themeLink) {
1247
+ if (isDark) {
1248
+ themeLink.href = "https://cdn.jsdelivr.net/npm/flatpickr/dist/themes/dark.css";
1249
+ } else {
1250
+ themeLink.href = "https://cdn.jsdelivr.net/npm/flatpickr/dist/themes/light.css";
1251
+ }
1252
+ }
1253
+ }
1254
+
1255
+ // Hook into toggleDarkMode to update flatpickr theme
1256
+ const originalToggleDarkMode = toggleDarkMode;
1257
+ toggleDarkMode = function() {
1258
+ originalToggleDarkMode();
1259
+ updateFlatpickrTheme(document.documentElement.classList.contains('dark'));
1260
+ };
1261
+ </script>
1262
+ </body>
1263
+ </html>